wantzel.py 10.5 KB
Newer Older
1
# encoding: utf-8
2
"""
Mindiell's avatar
Mindiell committed
3
Bot Wantzel from La Quadrature du Net.
4

Mindiell's avatar
Mindiell committed
5 6
License : AGPLv3
Doc     : https://wiki.laquadrature.net/Wantzel
7 8 9
"""

import re
10 11

from irc import IrcClientFactory
12
from twisted.internet import reactor, task
13 14

import config
15
from logs import Log
16

17 18 19 20 21 22
# bot modules
from admin import Admin
from fun import fun
from monitor import Monitor
from op import Op
from rp import Rp
23

24 25 26 27 28 29
class Wantzel(object):
    """
    Wantzel bot.
    """
    def __init__(self):
        """
30
        Initialization of all utility objects and bot over IRC.
31
        """
32 33 34 35
        self.admin = Admin()
        self.monitor = Monitor()
        self.op = Op()
        self.rp = Rp()
36
        # Connection to IRC
37 38 39
        self.irc = IrcClientFactory(config)
        self.irc.set_privmsg = self.set_privmsg
        reactor.connectTCP(config.server, config.port, self.irc)
40
        self.test_timer = False
41
        # Loop call
42
        self.loop = task.LoopingCall(self.timer)
43
        self.loop.start(config.timer)
44 45 46 47 48

    def timer(self):
        """
        This method launches function regularly (see config.timer).
        """
49
        Log.debug("Timer called")
50 51 52 53
        # Testing timer ?
        if self.test_timer:
            self.send_message("#lqdn-travail", "Timer fonctionnel!")        
            self.test_timer = False
54 55 56 57 58 59 60 61 62 63 64
        # Tweeting Press reviews f necessary
        self.rp.tweet()
        # Bot tries to list RP channel users, if it can't, then it is no more an op and shall
        # complain
        self.irc.client.sendLine("NAMES %s" % config.RP_CHANNEL)
        # Update topic based on number of articles waiting in queue 
        topic = self.rp.count_articles()
        if topic != "":
            self.irc.client.topic(config.RP_CHANNEL, topic)
        # Tell on channel if a wiki was modified since last time
        self.send_message("#lqdn-travail", self.monitor.wiki_updates())
65
        # Cleaning points of mastering rp
66
        self.rp.clean_master_rp()
67 68 69 70 71

    def set_privmsg(self):
        """
        This method set the methods to call for each callback received from IRC.
        """
72
        # When receiving a message
73
        self.irc.client.privmsg = self.on_privmsg
74 75 76
        # When the bot discover it is no more op
        self.irc.client.irc_unknown = self.irc_unknown
        # When bot mode is changed
77 78
        self.irc.client.modeChanged = self.mode_changed

79
    def mode_changed(self, user, channel, flag_set, modes, args):
80
        """
81
        Callback called whenever bot mode is changed.
82
        """
83
        Log.debug("Mode changed : %s %s %s %s %s" % (user, channel, flag_set, modes, args))
Mindiell's avatar
Mindiell committed
84
        if "o" in modes and self.irc.client.nickname in args:
85
            self.send_message(channel, self.op.get_op_mode(user, flag_set))
86 87 88 89

    def irc_unknown(self, prefix, command, params):
        """
        This Callback is called whenever the bot tries to perform the command "NAMES" on the
Mindiell's avatar
Mindiell committed
90
        RP_CHANNEL channel. If it occurs, then the bot knows that it is no more op and beg for
91 92
        a mode change to actual operators on the channel.
        """
93
        Log.debug("UNKNOWN %s %s %s" % (prefix, command, params))
94
        if command=="RPL_NAMREPLY":
95 96
            # Beg for operator mode ?
            self.send_message(params[2], self.op.need_op_mode("@"+self.irc.client.nickname, params))
97

98
    def send_message(self, channel, multiline_message=""):
99 100 101
        """
        Sends a message on specified channel, cutting each line in a new message
        """
102 103
        if isinstance(multiline_message, list):
            multiline_message = "\n".join(multiline_message)
104
        for message in multiline_message.splitlines():
105 106 107 108
            self.irc.client.msg(channel, message)

    def on_privmsg(self, user, channel, msg):
        """
109 110
        Wantzel can understand a lot of commands. Commands followed by a (*)
        are accessible only to moderators:
111 112 113
        - help
            Returns a message about how to use the bot.
            If a command is passed after help, the message explains how to use
114
            the command.
115
            Bot answers in private in order not to spam channel
116
        - rp(acp) <url>
117
            Add an article in the database with a specific flag
118 119
        - status <url>
            Retrieve some informations about an article in the database
120
        - stats
Mindiell's avatar
Mindiell committed
121
            Show some statistics about the RP
122
        - kill (*)
Mindiell's avatar
Mindiell committed
123
            Kill an article by giving it a score of -100
124
        - admin list (*)
125
            List rights in private
126
        - admin add (*)
127 128 129
            Add one or more new moderator to list
        - admin del (*)
            Delete one or more moderator from list
130
        - admin timer
131
            Relaunch a timer
132 133
        """
        # Cleaning user name
Mindiell's avatar
Mindiell committed
134
        user = re.search("([^!]*)!", user).group(1)
135
        Log.debug("Message received: %s %s %s" % (user, channel, msg))
Mindiell's avatar
Mindiell committed
136
        # Never answer to botself
137
        if user!=config.nickname:
Mindiell's avatar
Mindiell committed
138 139 140 141 142
            # If it's a query, bot should answer to the user as the channel
            if "#" not in channel:
                channel = user
            # Help command, specific
            if "wantzel" in msg and ("help" in msg or "aide" in msg):
143
                self.help(user, channel, msg)
Mindiell's avatar
Mindiell committed
144
            # Find known command
145 146
            command = re.search("[!~](rp[acp]*|status|kill|help|stats|admin)", msg)
            Log.debug("Command: %s" % command)
Mindiell's avatar
Mindiell committed
147
            if command:
148
                Log.debug("group(0): %s" % command.group(0))
Mindiell's avatar
Mindiell committed
149
                command = command.group(1)
150
                Log.debug("Command: %s" % command)
Mindiell's avatar
Mindiell committed
151
                if command.startswith("rp"):
152 153
                    Log.debug("Calling self.rp")
                    self.send_message(channel, self.rp.rp(command, user, channel, msg))
154
                if command.startswith("status"):
155 156
                    Log.debug("Calling self.status")
                    self.send_message(channel, self.rp.status(user, msg))
157
                elif command == "help":
158
                    Log.debug("Calling self.help")
159
                    self.help(user, channel, msg)
160
                elif command == "kill":
161 162
                    Log.debug("Calling self.kill")
                    self.send_message(channel, self.rp.kill(user, msg))
163
                elif command == "stats":
164 165
                    Log.debug("Calling self.stats")
                    self.send_message(channel, self.rp.stats())
166
                elif command == "admin":
167 168 169 170
                    Log.debug("Calling self.admin")
                    result = self.admin.admin(user, msg)
                    if result=="REACTOR":
                        try:
171 172
                            # Re-creating timer
                            self.loop = task.LoopingCall(self.timer)
173
                            self.loop.start(config.timer)
174 175
                        except Exception:
                            pass
176 177 178
                    elif result=="REACTOR":
                        self.test_timer = True
                        self.send_message(channel, "Test du timer en cours...")
179 180 181
                    else:
                        self.send_message(channel, result)
            self.send_message(channel, fun(user, channel, msg))
182

183
    def help(self, user, channel, msg):
184 185
        """
        Show global help.
186
        If a known command is behind the ~!help command, an adequate message is
187 188
        returned.
        """
189
        Log.debug("help command")
Mindiell's avatar
Mindiell committed
190
        # Searching for a command after help keyword
191
        command = re.search("[!~]help (help|rp|status|stats|kill|admin)", msg)
Mindiell's avatar
Mindiell committed
192 193
        if command:
            command = command.group(1)
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
            if command=="help":
                self.send_message(
                    user,
                    """Bravo!
                    Tu viens d'entrer dans le monde récursif où l'aide sert à expliciter l'aide."""
                )
            elif command=="rp":
                self.send_message(
                    user,
                    """Cette commande sert à ajouter un article à la Revue de Presse (https://wiki.laquadrature.net/Revue_de_presse)
                    L'utilisation se fait sous la forme: ~rp(cpa) <url de l'article à ajouter>"""
                )
            elif command=="status":
                self.send_message(
                    user,
                    """Cette commande sert à retrouver les informations concernant un article ajouté à la Revue de Presse (https://wiki.laquadrature.net/Revue_de_presse)
                    L'utilisation se fait sous la forme: ~status <url de l'article>"""
                )
            elif command=="stats":
                self.send_message(
                    user,
                    """Cette commande permet de fournir quelques statistiques sur la Revue de Presse (https://wiki.laquadrature.net/Revue_de_presse)
                    Les statistiques sont calculées sur des notes supérieurs ou égales à 0, 3, et 4. Et sur les 1, 3, 7, et 15 derniers jours."""
                )
            elif command=="kill":
                self.send_message(
                    user,
                    """*Attention* seuls les vrais rp-jedis ont accès à cette commande <3
                    Fixe la note de l'article donné en paramètre à -100.
                    Utile en cas d'erreur ou pour s'assurer que l'article ne sera pas publié dans la RP
                    Utilisation: ~kill <url de l'article>"""
                )
            elif command=="admin":
                self.send_message(
                    user,
                    """*Attention* seuls les vrais rp-jedis ont accès à cette commande <3
                    Permet de gérer la liste des utilisateurs ayant un accès privilégié. Il n'y a qu'un seul niveau de privilège.
                    Utilisations:
                    ~admin list => Fournit la liste des utilisateurs privilégiés
                    ~admin add user[, user]> => Ajoute un ou plusieurs utilisateurs à la liste
                    ~admin del user[, user] => Supprime un ou plusieurs utilisateurs de la liste
                    ~admin timer => Relance un timer pour gérer le topic et les tweets"""
                )
            else:
                self.send_message(
                    user,
                    """Désolé, je ne connais pas cette commande."""
                )
Mindiell's avatar
Mindiell committed
242
        else:
243 244 245 246 247 248
            self.send_message(
                user,
                """Mes commandes sont : ~help ~rp(cpa) ~status ~kill ~stats et ~admin.
                Pour plus d'informations, voir ici: https://wiki.laquadrature.net/Wantzel
                Pour obtenir de l'aide sur une commande en particulier, il suffit de taper ~help <commande>"""
            )
249 250 251


if __name__ == '__main__':
252
    Wantzel()
253
    reactor.run()