wantzel.py 24.9 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
Mindiell's avatar
Mindiell committed
10
11
import sqlite3
import time
12
13
14
15

import feedparser
from irc import IrcClientFactory
import MySQLdb
16
from twisted.internet import reactor
Mindiell's avatar
Mindiell committed
17
from twitter import Twitter, OAuth
18
19
20
21

import config
from messages import messages

Mindiell's avatar
Mindiell committed
22
23
24
25
26
27
28
29
LOG_FILE = "wantzel.log"
DEBUG = 3
WARNING = 2
INFO = 1
ERROR = 0
LOG_LEVEL = DEBUG

class Utils(object):
30
31
32
    """
    Simple utility class to log easily.
    """
Mindiell's avatar
Mindiell committed
33
34
    @classmethod
    def log(cls, message):
35
36
37
38
39
        """
        Logging message with timestamp.
        """
        actual_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        with open(LOG_FILE, 'a') as file_handle:
40
            try:
41
                file_handle.write("%s: %s\n" % (actual_time, message.encode("utf-8")))
42
43
            except UnicodeDecodeError:
                file_handle.write("%s: %s\n" % (actual_time, message))
Mindiell's avatar
Mindiell committed
44
45
46

    @classmethod
    def debug(cls, message):
47
48
49
50
        """
        Manage DEBUG level of logging.
        """
        if LOG_LEVEL >= DEBUG:
Mindiell's avatar
Mindiell committed
51
52
53
54
            cls.log("%s: %s" % ("DEBUG", message))

    @classmethod
    def warning(cls, message):
55
56
57
58
        """
        Manage WARNING level of logging.
        """
        if LOG_LEVEL >= WARNING:
Mindiell's avatar
Mindiell committed
59
60
61
62
            cls.log("%s: %s" % ("WARNING", message))

    @classmethod
    def info(cls, message):
63
64
65
66
        """
        Manage INFO level of logging.
        """
        if LOG_LEVEL >= INFO:
Mindiell's avatar
Mindiell committed
67
68
69
70
            cls.log("%s: %s" % ("INFO", message))

    @classmethod
    def error(cls, message):
71
72
73
74
        """
        Manage ERROR level of logging.
        """
        if LOG_LEVEL >= ERROR:
Mindiell's avatar
Mindiell committed
75
76
77
78
            cls.log("%s: %s" % ("ERROR", message))



Mindiell's avatar
Mindiell committed
79
80
def get_cursor():
    """
Mindiell's avatar
Mindiell committed
81
    This function connects to a MySQL database and returns a usable cursor.
Mindiell's avatar
Mindiell committed
82
    """
Mindiell's avatar
Mindiell committed
83
    connection = MySQLdb.connect(
Mindiell's avatar
Mindiell committed
84
85
86
87
88
        host=config.dbserver,
        user=config.dbuser,
        passwd=config.dbpassword,
        db=config.dbname
    )
Mindiell's avatar
Mindiell committed
89
90
    if connection:
        return connection.cursor()
Mindiell's avatar
Mindiell committed
91
92
    return None

93

94
def get_url(message):
Mindiell's avatar
Mindiell committed
95
    """
96
    Retrieve the url in the message.
Mindiell's avatar
Mindiell committed
97
    """
98
99
    # Let's get the url
    result = re.search("(https?[^ ]+)", message)
Mindiell's avatar
Mindiell committed
100
    if not result:
101
        return
102
    url = result.group(1)
Mindiell's avatar
Mindiell committed
103
104
105
106
107
108
109
110
    # Removing anchor if needed
    result = re.search("^([^#]*)", url)
    if result:
        url = result.group(1)
    # Removing trackers
    url = re.sub("[?&](utm_medium|utm_source|utm_campaign|xtor)=[^&]*", "", url)
    return url

111

Mindiell's avatar
Mindiell committed
112
113
114
115
116
117
118
def is_moderator(name):
    """
    This function verify if a user is a moderator.
    """
    connection = sqlite3.connect(config.sqlite_db)
    cursor = connection.cursor()
    cursor.execute("SELECT count(*) FROM moderator WHERE name=?", (name, ))
119
    if int(cursor.fetchone()[0]) == 1:
Mindiell's avatar
Mindiell committed
120
121
122
123
        return True
    return False


124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
def tweet(message):
    """
    Tweet message on specified account
    """
    Utils.debug("tweet method")
    auth = OAuth(
        config.TOKEN,
        config.TOKENSEC,
        config.CONSKEY,
        config.CONSSEC
    )
    twitter = Twitter(auth=auth)
    try:
        Utils.debug("Tweeting: %s" % message)
        twitter.statuses.update(status=message)
    except Exception:
        pass


143
144
145
146
147
148
149
150
class Wantzel(object):
    """
    Wantzel bot.
    """
    def __init__(self):
        """
        Initialization of bot over IRC.
        """
151
        self.number = None
152
        # default last_entry_published for tweets
Mindiell's avatar
Mindiell committed
153
154
155
156
157
158
159
160
        self.last_entry_published = time.strptime("2000-01-01", "%Y-%m-%d")
        # See if there is something in the db
        connection = sqlite3.connect(config.sqlite_db)
        for row in connection.execute("SELECT last_entry_published FROM tweets"):
            self.last_entry_published = time.strptime(
                row[0].encode("utf-8"),
                "%Y-%m-%d %H:%M:%S %Z"
            )
161
        Utils.debug("Dernier tweet: %s" % self.last_entry_published)
162
163
164
165
166
167
168
169
170
171
172
        # default last_entry_published for wiki
        self.last_entry_updated = time.strptime("2000-01-01", "%Y-%m-%d")
        # See if there is something in the db
        connection = sqlite3.connect(config.sqlite_db)
        for row in connection.execute("SELECT last_entry_updated FROM wikis"):
            self.last_entry_updated = time.strptime(
                row[0].encode("utf-8"),
                "%Y-%m-%d %H:%M:%S %Z"
            )
        Utils.debug("Dernière mise à jour du wiki: %s" % self.last_entry_updated)

173
174
175
        self.irc = IrcClientFactory(config)
        self.irc.set_privmsg = self.set_privmsg
        reactor.connectTCP(config.server, config.port, self.irc)
Mindiell's avatar
Mindiell committed
176
177
178
179
180
181
182
        # Prepare timer
        reactor.callLater(config.timer, self.timer)

    def timer(self):
        """
        This method launches function regularly (see config.timer).
        """
Mindiell's avatar
Mindiell committed
183
        Utils.debug("Timer called")
Mindiell's avatar
Mindiell committed
184
185
186
        self.rp_to_twitter("http://www.laquadrature.net/fr/revue-de-presse/feed")
        self.rp_to_twitter("http://www.laquadrature.net/en/press-review/feed")
        self.count_articles()
187
        self.op_verification()
188
        self.wiki_updates()
Mindiell's avatar
Mindiell committed
189
190
        # Recalling the timer
        reactor.callLater(config.timer, self.timer)
191
192
193
194
195
196

    def set_privmsg(self):
        """
        This method set the methods to call for each callback received from IRC.
        """
        self.irc.client.privmsg = self.on_privmsg
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
        self.irc.client.irc_unknown = self.test_unknown
        self.irc.client.modeChanged = self.mode_changed

    def test_unknown(self, prefix, command, params):
        Utils.debug("UNKNOWN %s %s %s" % (prefix, command, params))
        if command=="RPL_NAMREPLY":
            self.names(params)

    def op_verification(self):
        """
        Verify if wantzel is chan operator, if not it complains on the chan <3.
        """
        Utils.debug("op_verification method")
        # On liste les utilisateurs du canal de la rp
        self.irc.client.sendLine("NAMES #lqdn-rp")

    def mode_changed(self, user, channel, flag_set, modes, args):
        Utils.debug("Mode changed : %s %s %s %s %s" % (user, channel, flag_set, modes, args))
Mindiell's avatar
Mindiell committed
215
        if "o" in modes and self.irc.client.nickname in args:
216
217
218
219
220
221
222
223
224
225
226
227
228
            # Cleaning user name
            user = re.search("([^!]*)!", user).group(1)
            if flag_set:
                self.send_message(channel, messages["oped"] % user)
            else:
                self.send_message(channel, messages["deoped"] % user)

    def names(self, params):
        Utils.debug("Names : %s" % params)
        if params[2]=="#lqdn-rp":
            ops = [user[1:] for user in params[3].split() if user[0]=="@"]
            if "@"+self.irc.client.nickname not in params[3]:
                self.send_message(params[2], messages["please_op"] % ", ".join(ops))
229

230
    def send_message(self, channel, multiline_message):
231
232
233
        """
        Sends a message on specified channel, cutting each line in a new message
        """
234
        for message in multiline_message.splitlines():
235
236
237
238
            self.irc.client.msg(channel, message)

    def on_privmsg(self, user, channel, msg):
        """
Mindiell's avatar
Mindiell committed
239
240
        Wantzel can understand a lot of commands. Commands followed by a (*)
        are accessible only to moderators:
241
242
243
        - help
            Returns a message about how to use the bot.
            If a command is passed after help, the message explains how to use
244
            the command.
245
        - rp(acp) <url>
Mindiell's avatar
Mindiell committed
246
            Add an article in the database
247
248
        - status <url>
            Retrieve some informations about an article in the database
249
        - stats
Mindiell's avatar
Mindiell committed
250
            Show some statistics about the RP
Mindiell's avatar
Mindiell committed
251
        - kill (*)
Mindiell's avatar
Mindiell committed
252
            Kill an article by giving it a score of -100
253
        - admin list (*)
Mindiell's avatar
Mindiell committed
254
            List rights in private
255
        - admin add (*)
256
257
258
            Add one or more new moderator to list
        - admin del (*)
            Delete one or more moderator from list
259
        - admin timer
260
            Relaunch a timer
261
262
        """
        # Cleaning user name
Mindiell's avatar
Mindiell committed
263
        user = re.search("([^!]*)!", user).group(1)
Mindiell's avatar
Mindiell committed
264
        Utils.debug("Message received: %s %s %s" % (user, channel, msg))
Mindiell's avatar
Mindiell committed
265
        # Never answer to botself
266
        if user != config.nickname:
Mindiell's avatar
Mindiell committed
267
268
269
270
271
            # 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):
272
                self.help(user, channel, msg)
Mindiell's avatar
Mindiell committed
273
            # Find known command
274
            command = re.search("[!~](rp[acp]*|status|kill|help|stats|admin)", msg)
Mindiell's avatar
Mindiell committed
275
            Utils.debug("Command: %s" % command)
Mindiell's avatar
Mindiell committed
276
            if command:
Mindiell's avatar
Mindiell committed
277
                Utils.debug("group(0): %s" % command.group(0))
Mindiell's avatar
Mindiell committed
278
                command = command.group(1)
Mindiell's avatar
Mindiell committed
279
                Utils.debug("Command: %s" % command)
Mindiell's avatar
Mindiell committed
280
                if command.startswith("rp"):
Mindiell's avatar
Mindiell committed
281
                    Utils.debug("Calling self.rp")
282
                    self.rp(command, user, channel, msg)
283
284
285
                if command.startswith("status"):
                    Utils.debug("Calling self.status")
                    self.status(command, user, channel, msg)
286
                elif command == "help":
Mindiell's avatar
Mindiell committed
287
                    Utils.debug("Calling self.help")
288
                    self.help(user, channel, msg)
289
                elif command == "kill":
Mindiell's avatar
Mindiell committed
290
                    Utils.debug("Calling self.kill")
Mindiell's avatar
Mindiell committed
291
                    self.kill(user, channel, msg)
292
                elif command == "stats":
Mindiell's avatar
Mindiell committed
293
                    Utils.debug("Calling self.stats")
294
295
                    self.stats(channel)
                elif command == "admin":
Mindiell's avatar
Mindiell committed
296
                    Utils.debug("Calling self.admin")
Mindiell's avatar
Mindiell committed
297
                    self.admin(user, channel, msg)
Mindiell's avatar
Mindiell committed
298
299
300
        # No more giving the title of an url
        #if title and website:
        #    self.send_message(channel, messages["title"] % (title, website))
301

302
    def help(self, user, channel, msg):
303
304
        """
        Show global help.
305
        If a known command is behind the ~!help command, an adequate message is
306
307
        returned.
        """
Mindiell's avatar
Mindiell committed
308
        Utils.debug("help command")
Mindiell's avatar
Mindiell committed
309
        # Searching for a command after help keyword
310
        command = re.search("[!~]help (help|rp|status|stats|kill|admin)", msg)
Mindiell's avatar
Mindiell committed
311
312
        if command:
            command = command.group(1)
313
            self.send_message(user, messages["help_"+command])
Mindiell's avatar
Mindiell committed
314
        else:
315
            self.send_message(user, messages["help"])
316

317
    def rp(self, command, user, channel, msg):
318
319
320
        """
        Adding the article in rp database.
        """
Mindiell's avatar
Mindiell committed
321
322
323
324
        Utils.debug("rp command : %s" % command)
        Utils.debug("rp user : %s" % user)
        Utils.debug("rp channel : %s" % channel)
        Utils.debug("rp msg : %s" % msg)
325
        cite = 0
326
        note = 1
327
        url = get_url(msg)
Mindiell's avatar
Mindiell committed
328
        Utils.debug("url: %s" % url)
329
        if not url:
Mindiell's avatar
Mindiell committed
330
            return
331

332
333
334
335
336
337
338
339
340
341
342
        # Managing flags
        # LQdN is quoted
        if "c" in command:
            cite += 1
        # the article speak about LQdN
        if command.count("p") > 1:
            cite += 2
        # Archive this article
        if "a" in command:
            cite += 4

Mindiell's avatar
Mindiell committed
343
344
        # Looking for such an article in database
        cursor = get_cursor()
345
346
347
348
349
350
351
352
353
354
355
356
        # We need to be able to retrieve an url with "http" or "https"
        if url.startswith("https"):
            url2 = "http" + url[5:]
        else:
            url2 = "https" + url[4:]
        cursor.execute("""
            SELECT id, note, provenance
            FROM presse
            WHERE url = %s
            OR url = %s""",
            (url, url2)
        )
357
358
        rows = cursor.fetchall()
        if not rows:
Mindiell's avatar
Mindiell committed
359
            Utils.debug("Adding an article by %s: %s" % (user, url))
360
361
            cursor.execute("""
                INSERT INTO presse SET
362
                url=%s, provenance=%s, cite=%s, note=%s, datec=NOW(), title='',
363
364
                lang='', published=0, nid=0, screenshot=0, fetched=0, seemscite=0
                """,
365
                (url, user, cite, note)
366
            )
Mindiell's avatar
Mindiell committed
367
            self.send_message(channel, messages["rp_new_article"] % user)
368
        else:
369
            if rows[0][2] != user:
Mindiell's avatar
Mindiell committed
370
                Utils.debug("Adding a point by %s on %s" % (user, rows[0][0]))
371
                cursor.execute(
372
373
374
                    "UPDATE presse SET note=note+1 WHERE id=%s",
                    (rows[0][0], )
                )
375
            if (rows[0][1]+1) < 3:
Mindiell's avatar
Mindiell committed
376
                self.send_message(channel, messages["rp_known_article"] % user)
377
            else:
Mindiell's avatar
Mindiell committed
378
                self.send_message(channel, messages["rp_taken_article"] % user)
Mindiell's avatar
Mindiell committed
379
380
                # Update number of articles to do
                self.count_articles()
381

382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
    def status(self, command, user, channel, msg):
        """
        Retrieving status of the article in rp database.
        """
        Utils.debug("rp command : %s" % command)
        Utils.debug("rp user : %s" % user)
        Utils.debug("rp channel : %s" % channel)
        Utils.debug("rp msg : %s" % msg)
        url = get_url(msg)
        Utils.debug("url: %s" % url)
        if not url:
            return

        # Looking for such an article in database
        cursor = get_cursor()
397
398
399
400
401
        # We need to be able to retrieve an url with "http" or "https"
        if url.startswith("https"):
            url2 = "http" + url[5:]
        else:
            url2 = "https" + url[4:]
402
        cursor.execute("""
403
            SELECT cite, nid, note
404
            FROM presse
405
406
407
408
            WHERE url = %s
            OR url = %s""",
            (url, url2)
        )
409
410
411
412
        rows = cursor.fetchall()
        if not rows:
            self.send_message(channel, messages["status_unknown_article"] % user)
        else:
413
414
415
416
417
418
419
420
421
            message = "%s: note %s / " % (user, rows[0][2])
            if rows[0][0] & 1:
                message += "cite LQdN / "
            if rows[0][0] & 2:
                message += "parle de LQdN / "
            if rows[0][0] & 4:
                message += "archivé / "
            if rows[0][1] > 0:
                message += "publié (https://laquadrature.net/node/%s) / " % rows[0][1]
422
            else:
423
424
                message += "non publié / "
            self.send_message(channel, message[:-3])
425

426
427
    def kill(self, user, channel, msg):
        """
Mindiell's avatar
Mindiell committed
428
        Kill an article by setting its score to -100.
429
        """
Mindiell's avatar
Mindiell committed
430
        Utils.debug("kill command")
Mindiell's avatar
Mindiell committed
431
432
        if is_moderator(user):
            url = get_url(msg)
Mindiell's avatar
Mindiell committed
433
            Utils.debug("url: %s" % url)
434
            if url == "":
Mindiell's avatar
Mindiell committed
435
                return
436
            elif url == "http":
Mindiell's avatar
Mindiell committed
437
438
439
440
                self.send_message(channel, messages["rp_http"] % user)
                return
            # Looking for such an article in database
            cursor = get_cursor()
441
442
443
444
445
446
447
448
449
450
451
452
            # We need to be able to retrieve an url with "http" or "https"
            if url.startswith("https"):
                url2 = "http" + url[5:]
            else:
                url2 = "https" + url[4:]
            cursor.execute("""
                SELECT id, note
                FROM presse
                WHERE url = %s
                OR url = %s""",
                (url, url2)
            )
Mindiell's avatar
Mindiell committed
453
454
455
456
457
458
            rows = cursor.fetchall()
            if not rows:
                self.send_message(channel, messages["kill_none"] % url)
            else:
                cursor.execute("UPDATE presse SET note=-100 WHERE id=%s", (rows[0][0], ))
                self.send_message(channel, messages["kill_done"] % url)
Mindiell's avatar
Mindiell committed
459
        else:
Mindiell's avatar
Mindiell committed
460
            self.send_message(channel, messages["not_moderator"])
461

462
    def stats(self, channel):
463
464
465
        """
        Returns stats on articles in press review.
        """
Mindiell's avatar
Mindiell committed
466
        Utils.debug("stats command")
Mindiell's avatar
Mindiell committed
467
        cursor = get_cursor()
468
        periods = [1, 3, 7, 15]
469
        notes = [0, 3, 4]
470
471
472
473
        notnull = 0
        somethingatall = 0
        for note in notes:
            notnull = 0
474
            period_result = ""
475
            for period in periods:
476
477
478
                cursor.execute("""
                    SELECT COUNT(id) AS cid
                    FROM presse
479
480
481
482
483
484
                    WHERE nid=0
                    AND datec>(NOW()-INTERVAL %s DAY)
                    AND note>=%s""",
                    (period, note)
                )
                rows = cursor.fetchall()
485
                if rows[0][0] > 0:
486
                    period_result = period_result + "%sj:%s, " % (period, rows[0][0])
487
                    notnull = 1
Mindiell's avatar
Mindiell committed
488
                    somethingatall = 1
489
            if notnull:
490
                self.send_message(channel, "note>=%s: " % note + period_result[:-2])
491
        if somethingatall == 0:
492
            self.send_message(channel, messages["stats_bravo"] % periods[-1])
493

Mindiell's avatar
Mindiell committed
494
495
496
    def admin(self, user, channel, msg):
        """
        Manage moderation.
497
        A sub-command should be behind the !~admin command.
Mindiell's avatar
Mindiell committed
498
        """
Mindiell's avatar
Mindiell committed
499
        Utils.debug("admin command")
Mindiell's avatar
Mindiell committed
500
        # Searching for a command after admin keyword
501
        command = re.search("[~!]admin (list|add|del|timer)", msg)
Mindiell's avatar
Mindiell committed
502
503
        if command:
            command = command.group(1)
504
505
506
            if command == "list":
                self.admin_list(user, channel)
            elif command == "add":
Mindiell's avatar
Mindiell committed
507
                self.admin_add(user, channel, msg)
508
            elif command == "del":
Mindiell's avatar
Mindiell committed
509
                self.admin_del(user, channel, msg)
510
511
            elif command == "timer":
                self.admin_timer(user, channel)
Mindiell's avatar
Mindiell committed
512

513
    def admin_list(self, user, channel):
Mindiell's avatar
Mindiell committed
514
515
516
        """
        List actual moderators.
        """
Mindiell's avatar
Mindiell committed
517
        Utils.debug("admin_list command")
Mindiell's avatar
Mindiell committed
518
519
520
521
522
        if is_moderator(user):
            connection = sqlite3.connect(config.sqlite_db)
            names = []
            for row in connection.execute("SELECT name FROM moderator"):
                names.append(row[0].encode("utf-8"))
523
            self.send_message(channel, messages["admin_list"] % ", ".join(sorted(names)))
Mindiell's avatar
Mindiell committed
524
525
526
527
528
529
530
        else:
            self.send_message(channel, messages["not_moderator"])

    def admin_add(self, user, channel, msg):
        """
        Add some new moderators if not existing yet.
        """
Mindiell's avatar
Mindiell committed
531
        Utils.debug("admin_add command")
Mindiell's avatar
Mindiell committed
532
533
534
535
        if is_moderator(user):
            try:
                names = []
                connection = sqlite3.connect(config.sqlite_db)
536
                result = re.search("[!~]admin add (([^,]+, ?)+)?(.*)", msg)
Mindiell's avatar
Mindiell committed
537
                if result.group(1):
538
                    names = [name.strip() for name in result.group(1).split(",") if name.strip() != ""]
Mindiell's avatar
Mindiell committed
539
540
541
542
543
                names.append(result.group(3))
                # Do not add actual moderators
                moderators = []
                for row in connection.execute("SELECT name FROM moderator"):
                    moderators.append(row[0].encode("utf-8"))
544
                names = list(set([name for name in names if name not in moderators]))
Mindiell's avatar
Mindiell committed
545
546
547
548
549
550
551
552
                if names:
                    # Converting set in list of tuples
                    values = [(name,) for name in names]
                    connection.executemany("INSERT INTO moderator (name) VALUES (?)", values)
                    connection.commit()
                    self.send_message(channel, messages["admin_add"] % ", ".join(names))
                else:
                    self.send_message(channel, messages["admin_add_empty"])
553
            except Exception:
Mindiell's avatar
Mindiell committed
554
555
556
557
558
559
560
561
                pass
        else:
            self.send_message(channel, messages["not_moderator"])

    def admin_del(self, user, channel, msg):
        """
        Delete a moderator from list.
        """
Mindiell's avatar
Mindiell committed
562
        Utils.debug("admin_del command")
Mindiell's avatar
Mindiell committed
563
564
565
        if is_moderator(user):
            try:
                names = []
566
                result = re.search("[!~]admin del (([^,]+, ?)+)?(.*)", msg)
Mindiell's avatar
Mindiell committed
567
                if result.group(1):
568
                    names = [name.strip() for name in result.group(1).split(",") if name.strip() != ""]
Mindiell's avatar
Mindiell committed
569
                names.append(result.group(3))
570
                names = list(set(names))
Mindiell's avatar
Mindiell committed
571
                Utils.debug(names)
Mindiell's avatar
Mindiell committed
572
573
574
575
576
                connection = sqlite3.connect(config.sqlite_db)
                for name in names:
                    connection.execute("DELETE FROM moderator WHERE name=?", (name, ))
                connection.commit()
                self.send_message(channel, messages["admin_del"] % ", ".join(names))
577
            except Exception:
Mindiell's avatar
Mindiell committed
578
579
580
581
                pass
        else:
            self.send_message(channel, messages["not_moderator"])

582
    def admin_timer(self, user, channel):
583
584
585
586
587
588
589
590
        """
        Relaunch a timer.
        """
        Utils.debug("admin_timer command")
        if is_moderator(user):
            try:
                # Recalling the timer
                reactor.callLater(config.timer, self.timer)
591
            except Exception:
592
593
594
595
                pass
        else:
            self.send_message(channel, messages["not_moderator"])

Mindiell's avatar
Mindiell committed
596
597
598
599
600
    def count_articles(self):
        """
        Count number of articles not done in RP and updates the topic of the
        press review channel if needed.
        """
Mindiell's avatar
Mindiell committed
601
        Utils.debug("count_articles method")
Mindiell's avatar
Mindiell committed
602
603
604
605
606
607
608
        cursor = get_cursor()
        cursor.execute("""SELECT COUNT(*) FROM presse
            WHERE DATE_SUB(NOW(), INTERVAL 2 MONTH)<datec
            AND note > 2
            AND nid = 0""")
        rows = cursor.fetchall()
        number = int(rows[0][0])
609
        Utils.debug("Found %s articles." % number)
610
        if self.number != number:
Mindiell's avatar
Mindiell committed
611
            self.irc.client.topic("#lqdn-rp", messages["topic"] % number)
Mindiell's avatar
Mindiell committed
612
613
614
615
616
617
        self.number = number

    def rp_to_twitter(self, rss):
        """
        By parsing the RSS feed of the press-review, we know what to tweet.
        """
Mindiell's avatar
Mindiell committed
618
        Utils.debug("rp_to_twitter method")
Mindiell's avatar
Mindiell committed
619
620
        now = time.localtime()
        today = time.strptime("%s-%s-%s %s" % (
621
622
623
624
625
            now.tm_year,
            now.tm_mon,
            now.tm_mday,
            time.tzname[0]
            ), "%Y-%m-%d %Z")
Mindiell's avatar
Mindiell committed
626
627
628
        language = "fr"
        if "/en/" in rss:
            language = "en"
Mindiell's avatar
Mindiell committed
629
630
        entries = feedparser.parse(rss)['entries']
        entries.reverse()
631
        Utils.debug(self.last_entry_published)
Mindiell's avatar
Mindiell committed
632
633
634
635
636
        for entry in entries:
            # if date of publication is greater than today, midnight, and
            # lesser than future
            if today < entry.published_parsed < now:
                if self.last_entry_published < entry.published_parsed:
okhin's avatar
okhin committed
637
638
639
640
                    # Let's see if we can truncate the lenght of the tweet
                    # We have 5 chars for the language, so max-length is 135
                    title = entry.title.encode("utf-8")
                    link = entry.link.encode("utf-8")
641
                    if len(title) + min(len(link),23) > 135:
okhin's avatar
okhin committed
642
                        # What is the number of chars we need to remove
643
644
                        excess = len(title) + min(len(link),23) - 135
                        title = ''.join([title[:-(excess + 4)], ' ...'])
645
                    tweet(messages["tweet_rp_%s" % language] % (
okhin's avatar
okhin committed
646
647
                        title,
                        link
Mindiell's avatar
Mindiell committed
648
                    ))
Mindiell's avatar
Mindiell committed
649
                    Utils.debug(entry.published_parsed)
Mindiell's avatar
Mindiell committed
650
                    Utils.debug(entry.title)
Mindiell's avatar
Mindiell committed
651
652
653
654
655
656
657
658
659
660
661
662
663
664
                    # Save last_entry_published
                    self.last_entry_published = entry.published_parsed
                    last_entry_published = time.strftime(
                        "%Y-%m-%d %H:%M:%S %Z",
                        self.last_entry_published
                    )
                    connection = sqlite3.connect(config.sqlite_db)
                    connection.execute(
                        "UPDATE tweets SET last_entry_published=?",
                        (last_entry_published,)
                    )
                    connection.commit()
                    # Tweet only one message in order not to spam
                    return
665
666
667
                else:
                    Utils.debug(entry.title)
                    Utils.debug(entry.published_parsed)
Mindiell's avatar
Mindiell committed
668

669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
    def wiki_updates(self):
        url = "https://wiki.laquadrature.net/api.php?days=1&limit=50&translations=filter&action=feedrecentchanges&feedformat=atom"
        now = time.localtime()
        today = time.strptime("%s-%s-%s %s" % (
            now.tm_year,
            now.tm_mon,
            now.tm_mday,
            time.tzname[0]
            ), "%Y-%m-%d %Z")
        entries = feedparser.parse(url)['entries']
        for entry in entries:
            # if date of update is greater than today midnight
            if today < entry.updated_parsed:
                if self.last_entry_updated < entry.updated_parsed:
                    # Ecriture de la mise à jour sur le canal de travail
684
                    self.send_message("#lqdn-travail", messages["wiki_update"] % (
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
                        entry.author.encode("utf-8"),
                        entry.title.encode("utf-8"),
                        entry.link.encode("utf-8"),
                    ))
                    # Save last_entry_published
                    self.last_entry_updated = entry.updated_parsed
                    last_entry_updated = time.strftime(
                        "%Y-%m-%d %H:%M:%S %Z",
                        self.last_entry_updated
                    )
                    connection = sqlite3.connect(config.sqlite_db)
                    connection.execute(
                        "UPDATE wikis SET last_entry_updated=?",
                        (last_entry_updated,)
                    )
                    connection.commit()

702
703

if __name__ == '__main__':
704
    Wantzel()
705
    reactor.run()