diff --git a/config.py.sample b/config.py.sample index 63f0b870ff1bed1378552083102da9072b8a9035..6bacba48b798b35b5174c7307d95f1ed17a92a61 100644 --- a/config.py.sample +++ b/config.py.sample @@ -3,6 +3,7 @@ Configuration for IRC bot Wantzel. """ +# IRC access server = 'irc.freenode.net' port = 6667 nickname = 'testBot' @@ -10,7 +11,22 @@ password = '', channels = [ '#testchannel' ] + +# External drupal db, MySQL dbuser = "root" dbpassword = "" dbserver = "localhost" dbname = "db" + +# Internal db, sqlite is sufficient +sqlite_db = "db.sqlite3" + +# Timer for chrono-methods, in seconds (float) +timer = 180 + +# Twitter account +TOKEN = "" +TOKENSEC = "" +CONSKEY = "" +CONSSEC = "" + diff --git a/messages.py b/messages.py index c3840ddee87ea861f614df8de48cfb4e809c5713..7b02437f24c831bcb35e1e6f684640ac758add55 100644 --- a/messages.py +++ b/messages.py @@ -9,7 +9,7 @@ messages = { """Bonjour, je suis le bot de la Quadrature du Net, vous pouvez me demander de l'aide si besoin. (wantzel help)""", "help": -"""Mes commandes sont : !help !rp(cpa) !kill !stats. +"""Mes commandes sont : !help !rp(cpa) !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 """, @@ -28,10 +28,18 @@ L'utilisation se fait sous la forme: !rp """, 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.""", "help_kill": -"""Fixe la note de l'article donné en paramètre à -100. +"""*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 -*Attention* seuls les vrais rp-jedis ont accès à cette commande <3""", +Utilisation: !kill """, + +"help_admin": +"""*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""", "rp_http": """Merci %s, mais je prends en compte uniquement les adresses internet qui commencent par http ou https""", @@ -57,4 +65,27 @@ Utilisation: !kill "title": """Titre: %s (à %s)""", +"admin_list": +"""Liste des modérateurs actuels: %s""", + +"admin_add": +"""%s a(ont) été ajouté(s) à la liste des modérateurs""", + +"admin_add_empty": +"""Ces utilisateurs sont déjà modérateurs""", + +"admin_del": +"""%s a(ont) été retiré(s) de la liste des modérateurs.""", + +"not_moderator": +"""Désolé, il ne semble pas que vous ayez les droits pour cette commande.""", + +"topic": +"""Canal de la revue de presse de La Quadrature du Net ~ %s articles en attente ~ Mode d'emploi https://wiki.laquadrature.net/Revue_de_presse ~ Une arme, le savoir est. Le diffuser, notre devoir c'est.""", + +"tweet_rp": +"""[La Quadrature du Net - Revue de presse] %s — %s""", + +"reload": +"""Configuration à jour""", #La configuration a été mise à jour, merci <3""", } diff --git a/wantzel.py b/wantzel.py index 3e840df2fe13172268aa8af449bb6c6552ec262f..d78975f6dfdaa91970c649c3a56e13c698b473d0 100644 --- a/wantzel.py +++ b/wantzel.py @@ -6,15 +6,22 @@ License : AGPLv3 Doc : https://wiki.laquadrature.net/Wantzel TODO: -- Ajouter la gestion des droits pour certaines commandes -- Mettre une valeur par défaut pour les champs concernés -- Afficher les titres des urls fournies sur le canal +- Ajouter des commandes permettant de gérer le mediakit (moderators only) + - Parser les urls et voir ce qu'on peut faire: + - upload de vidéo + - tag de vidéo + - obtenir un lien vers une vidéo """ +import feedparser +import importlib from irc import IrcClientFactory import MySQLdb import re +import sqlite3 +import time from twisted.internet import reactor +from twitter import Twitter, OAuth import urllib import config @@ -22,16 +29,16 @@ from messages import messages def get_cursor(): """ - This function connects to a database and returns a usable cursor. + This function connects to a MySQL database and returns a usable cursor. """ - db = MySQLdb.connect( + connection = MySQLdb.connect( host=config.dbserver, user=config.dbuser, passwd=config.dbpassword, db=config.dbname ) - if db: - return db.cursor() + if connection: + return connection.cursor() return None def get_url(message): @@ -70,6 +77,18 @@ def get_title(message): title = re.sub("&", "&", title) return (title, website) +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, )) + if int(cursor.fetchone()[0])==1: + return True + return False + + class Wantzel(object): """ Wantzel bot. @@ -78,9 +97,32 @@ class Wantzel(object): """ Initialization of bot over IRC. """ + self.number = 0 + # default last_entry_published + 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" + ) self.irc = IrcClientFactory(config) self.irc.set_privmsg = self.set_privmsg reactor.connectTCP(config.server, config.port, self.irc) + # Prepare timer + reactor.callLater(config.timer, self.timer) + + def timer(self): + """ + This method launches function regularly (see config.timer). + """ + print("Timer called") + 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() + # Recalling the timer + reactor.callLater(config.timer, self.timer) def set_privmsg(self): """ @@ -98,7 +140,8 @@ class Wantzel(object): def on_privmsg(self, user, channel, msg): """ - Wantzel can understand some commands : + Wantzel can understand a lot of commands. Commands followed by a (*) + are accessible only to moderators: - help Returns a message about how to use the bot. If a command is passed after help, the message explains how to use @@ -107,8 +150,14 @@ class Wantzel(object): Add an article in the database - stats Show some statistics about the RP - - kill + - kill (*) Kill an article by giving it a score of -100 + - moderate list (*) + List rights in private + - moderate add (*) + Add a new moderator to list + - moderate remove (*) + Remove a moderator from list """ # Cleaning user name user = re.search("([^!]*)!", user).group(1) @@ -126,7 +175,7 @@ class Wantzel(object): if "wantzel" in msg and ("help" in msg or "aide" in msg): self.help(user, channel, msg) # Find known command - command = re.search("!(rp[acp]*|kill|help|stats)", msg) + command = re.search("!(rp[acp]*|kill|help|stats|admin)", msg) if command: command = command.group(1) print("Command: %s" % command) @@ -138,6 +187,8 @@ class Wantzel(object): self.kill(user, channel, msg) elif command=="stats": self.stats(user, channel, msg) + elif command=="admin": + self.admin(user, channel, msg) if title and website: self.send_message(channel, messages["title"] % (title, website)) @@ -160,7 +211,7 @@ class Wantzel(object): """ print("help command") # Searching for a command after help keyword - command = re.search("!help (stats|rp|help|kill)", msg) + command = re.search("!help (stats|rp|help|kill|admin)", msg) if command: command = command.group(1) self.send_message(user, messages["help_"+command]) @@ -196,8 +247,6 @@ class Wantzel(object): # Archive this article if "a" in command: note -= 2 - #TODO: Gérer les autres champs qui n'ont pas de valeur par défaut - # lang, published, nid, screenshot, title, fetched, seemscite print("Adding an article by %s: %s" % (user, url)) result = cursor.execute( """INSERT INTO presse SET @@ -218,37 +267,40 @@ class Wantzel(object): self.send_message(channel, messages["rp_known_article"] % user) else: self.send_message(channel, messages["rp_taken_article"] % user) + # Update number of articles to do + self.count_articles() def kill(self, user, channel, msg): """ Kill an article by setting its score to -100. """ - #TODO: Gérer les droits de cette commande print("kill command") - url = get_url(msg) - print("url: %s" % url) - if url=="": - return - elif url=="http": - self.send_message(channel, messages["rp_http"] % user) - return - # Looking for such an article in database - cursor = get_cursor() - cursor.execute("SELECT id, note FROM presse WHERE url=%s", (url, )) - rows = cursor.fetchall() - if not rows: - self.send_message(channel, messages["kill_none"] % url) + if is_moderator(user): + url = get_url(msg) + print("url: %s" % url) + if url=="": + return + elif url=="http": + self.send_message(channel, messages["rp_http"] % user) + return + # Looking for such an article in database + cursor = get_cursor() + cursor.execute("SELECT id, note FROM presse WHERE url=%s", (url, )) + 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) else: - cursor.execute("UPDATE presse SET note=-100 WHERE id=%s", (rows[0][0], )) - self.send_message(channel, messages["kill_done"] % url) + self.send_message(channel, messages["not_moderator"]) def stats(self, user, channel, msg): """ Returns stats on articles in press review. """ print("stats command") - db = MySQLdb.connect(host="localhost", user="root", passwd="root", db="site") - cursor = db.cursor() + cursor = get_cursor() periods = [1, 3, 7, 15] notes = [0, 3 ,4] notnull = 0 @@ -276,6 +328,169 @@ class Wantzel(object): result = messages["stats_bravo"] % periods[-1] self.send_message(channel, result) + def admin(self, user, channel, msg): + """ + Manage moderation. + A sub-command should be behind the !admin command. + """ + print("admin command") + # Searching for a command after admin keyword + command = re.search("!admin (list|add|del)", msg) + if command: + command = command.group(1) + if command=="list": + self.admin_list(user, channel, msg) + elif command=="add": + self.admin_add(user, channel, msg) + elif command=="del": + self.admin_del(user, channel, msg) + + def admin_list(self, user, channel, msg): + """ + List actual moderators. + """ + print("admin_list command") + 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")) + self.send_message(channel, messages["admin_list"] % ", ".join(names)) + else: + self.send_message(channel, messages["not_moderator"]) + + def admin_add(self, user, channel, msg): + """ + Add some new moderators if not existing yet. + """ + print("admin_add command") + if is_moderator(user): + try: + names = [] + connection = sqlite3.connect(config.sqlite_db) + result = re.search("!admin add (([^,]+, ?)+)?(.*)", msg) + if result.group(1): + names = [name for name in result.group(1).split(", ") if name!=""] + 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")) + names = set([name for name in names if name not in moderators]) + 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"]) + except: + pass + else: + self.send_message(channel, messages["not_moderator"]) + + def admin_del(self, user, channel, msg): + """ + Delete a moderator from list. + """ + print("admin_del command") + if is_moderator(user): + try: + names = [] + result = re.search("!admin del (([^,]+, ?)+)?(.*)", msg) + if result.group(1): + names = [name for name in result.group(1).split(", ") if name!=""] + names.append(result.group(3)) + names = set(names) + print(names) + 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)) + except: + pass + else: + self.send_message(channel, messages["not_moderator"]) + + def count_articles(self): + """ + Count number of articles not done in RP and updates the topic of the + press review channel if needed. + """ + cursor = get_cursor() + cursor.execute("""SELECT COUNT(*) FROM presse + WHERE DATE_SUB(NOW(), INTERVAL 2 MONTH) 2 + AND nid = 0""") + rows = cursor.fetchall() + number = int(rows[0][0]) + if self.number!=number: + self.irc.client.topic("#mytipy", messages["topic"] % number) + pass + self.number = number + + def rp_to_twitter(self, rss): + """ + By parsing the RSS feed of the press-review, we know what to tweet. + """ + print("rp_to_twitter method") + now = time.localtime() + today = time.strptime("%s-%s-%s %s" % ( + now.tm_year, + now.tm_mon, + now.tm_mday, + tm_isdst + ), + "%Y-%m-%d %Z" + ) + entries = feedparser.parse(rss)['entries'] + entries.reverse() + 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: + self.tweet(messages["tweet_rp"] % ( + entry.title.encode("utf-8"), + entry.link.encode("utf-8") + )) + print(entry.published_parsed) + print(entry.title) + # 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 + + def tweet(self, message): + """ + Tweet message on specified account + """ + print("tweet method") + auth = OAuth( + config.TOKEN, + config.TOKENSEC, + config.CONSKEY, + config.CONSSEC + ) + twitter = Twitter(auth=auth) + try: + print("Tweeting: %s", message) + #twitter.statuses.update(status=message) + except: + pass + if __name__ == '__main__': wantzel = Wantzel()