wantzel.py 10.6 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 = ""
41
        # Loop call
42
        self.loop_started = False
43
        self.loop = task.LoopingCall(self.timer)
44 45 46 47 48

    def timer(self):
        """
        This method launches function regularly (see config.timer).
        """
49
        Log.debug("Timer called")
50
        # Testing timer ?
51 52 53
        if self.test_timer!="":
            self.send_message(self.test_timer, "Timer fonctionnel!")        
            self.test_timer = ""
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 97 98
            # Launch loop if necessary
            if not self.loop_started:
                self.loop.start(config.timer)
                self.loop_started = True
99 100
            # Beg for operator mode ?
            self.send_message(params[2], self.op.need_op_mode("@"+self.irc.client.nickname, params))
101

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

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

187
    def help(self, user, channel, msg):
188 189
        """
        Show global help.
190
        If a known command is behind the ~!help command, an adequate message is
191 192
        returned.
        """
193
        Log.debug("help command")
Mindiell's avatar
Mindiell committed
194
        # Searching for a command after help keyword
195
        command = re.search("[!~]help (help|rp|status|stats|kill|admin)", msg)
Mindiell's avatar
Mindiell committed
196 197
        if command:
            command = command.group(1)
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 242 243 244 245
            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
246
        else:
247 248 249 250 251 252
            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>"""
            )
253 254 255


if __name__ == '__main__':
256
    Wantzel()
257
    reactor.run()