Commit d7af98dd authored by Mindiell's avatar Mindiell

A big cleaning based on pylint and PEP8.

parent e6d8df3e
#encoding: utf-8
# encoding: utf-8
"""
Ce fichier contient les messages utilisés par Wantzel.
Il les recharge à chaque fois qu'on lui fournit la commande '~reload' sur IRC.
"""
messages = {
"hello":
"""Bonjour, je suis le bot de la Quadrature du Net, vous pouvez me demander de l'aide si besoin. (wantzel help)""",
"hello":
"""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 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>""",
"help":
"""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 <commande>""",
"help_help":
"""Bravo!
Tu viens d'entrer dans le monde récursif où l'aide sert à expliciter l'aide.""",
"help_help":
"""Bravo!
Tu viens d'entrer dans le monde récursif où l'aide sert à expliciter l'aide.""",
"help_rp":
"""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 <url de l'article à ajouter>""",
#Plusieurs options sont possibles et cumulables: a, c, p
#l'option a => ~rpa permet d'archiver un article (il ne sera pas ajouté à la Revue de Presse""",
"help_rp":
"""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 <url de l'article à ajouter>""",
"help_stats":
"""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.""",
"help_stats":
"""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.""",
"help_kill":
"""*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>""",
"help_kill":
"""*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>""",
"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
~admin timer => Relance un timer pour gérer le topic et les tweets""",
"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
~admin timer => Relance un timer pour gérer le topic et les tweets""",
"rp_http":
"""Merci %s, mais je prends en compte uniquement les adresses internet qui commencent par http ou https""",
"rp_http":
"""Merci %s, mais je prends en compte uniquement les adresses internet qui commencent par http ou https""",
"rp_new_article":
"""Merci %s, cette url a été ajoutée à la revue de presse ! À présent, que dirais-tu d'aider à choisir les extraits de l'article à publier sur #lqdn-rp ? <3""",
"rp_new_article":
"""Merci %s, cette url a été ajoutée à la revue de presse ! À présent, que dirais-tu d'aider à choisir les extraits de l'article à publier sur #lqdn-rp ? <3""",
"rp_known_article":
"""Merci %s! Un point a été ajouté à cet article : à partir de 3, il pourra être repris dans la revue de presse. D'ailleurs, que dirais-tu d'aider à choisir les extraits à publier sur #lqdn-rp ? <3""",
"rp_known_article":
"""Merci %s! Un point a été ajouté à cet article : à partir de 3, il pourra être repris dans la revue de presse. D'ailleurs, que dirais-tu d'aider à choisir les extraits à publier sur #lqdn-rp ? <3""",
"rp_taken_article":
"""Merci %s! Un point a été ajouté à cet article : il va être repris dans la revue de presse. D'ailleurs, que dirais-tu d'aider à choisir les extraits à publier sur #lqdn-rp ? <3""",
"rp_taken_article":
"""Merci %s! Un point a été ajouté à cet article : il va être repris dans la revue de presse. D'ailleurs, que dirais-tu d'aider à choisir les extraits à publier sur #lqdn-rp ? <3""",
"kill_none":
"""%s n'existe pas dans la base de données""",
"kill_none":
"""%s n'existe pas dans la base de données""",
"kill_done":
"""%s mis à -100""",
"kill_done":
"""%s mis à -100""",
"stats_bravo":
"""Bravo les neurones, rien en retard depuis ces %s derniers jours!""",
"stats_bravo":
"""Bravo les neurones, rien en retard depuis ces %s derniers jours!""",
"title":
"""Titre: %s (à %s)""",
"title":
"""Titre: %s (à %s)""",
"admin_list":
"""Liste des modérateurs actuels: %s""",
"admin_list":
"""Liste des modérateurs actuels: %s""",
"admin_add":
"""%s a(ont) été ajouté(s) à la liste des modérateurs""",
"admin_add":
"""%s a(ont) été ajouté(s) à la liste des modérateurs""",
"admin_add_empty":
"""Ces utilisateurs sont déjà 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.""",
"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.""",
"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.""",
"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_fr":
"""[fr] %s — %s""",
"tweet_rp_fr":
"""[fr] %s — %s""",
"tweet_rp_en":
"""[en] %s — %s""",
"tweet_rp_en":
"""[en] %s — %s""",
"reload":
"""Configuration à jour""", #La configuration a été mise à jour, merci <3""",
"new_starter":
"""Afin d'éviter des commandes prises en compte par plusieurs bots, il est désormais fortement conseillé d'utiliser des commandes commençant par '~', merci.""",
"new_starter":
"""Afin d'éviter des commandes prises en compte par plusieurs bots, il est désormais fortement conseillé d'utiliser des commandes commençant par '~', merci.""",
}
#encoding: utf-8
# encoding: utf-8
"""
Bot Wantzel from La Quadrature du Net.
......@@ -13,17 +13,15 @@ TODO:
- obtenir un lien vers une vidéo
"""
import feedparser
import HTMLParser
import importlib
from irc import IrcClientFactory
import MySQLdb
import re
import sqlite3
import time
import feedparser
from irc import IrcClientFactory
import MySQLdb
from twisted.internet import reactor
from twitter import Twitter, OAuth
import urllib
import config
from messages import messages
......@@ -36,36 +34,55 @@ ERROR = 0
LOG_LEVEL = DEBUG
class Utils(object):
"""
Simple utility class to log easily.
"""
@classmethod
def log(cls, message):
with open(LOG_FILE, 'a') as f:
"""
Logging message with timestamp.
"""
actual_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
with open(LOG_FILE, 'a') as file_handle:
try:
f.write("%s: %s\n" % (time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), message.encode("utf-8")))
except:
f.write("%s: Erreur de log\n" % time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
file_handle.write("%s: %s\n" % (actual_time, message.encode("utf-8")))
except Exception:
file_handle.write("%s: Erreur de log\n" % actual_time)
try:
f.write("%s: %s\n" % (time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), message))
except:
f.write("%s: Seconde erreur de log\n" % time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
file_handle.write("%s: %s\n" % (actual_time, message))
except Exception:
file_handle.write("%s: Seconde erreur de log\n" % actual_time)
@classmethod
def debug(cls, message):
if LOG_LEVEL>=DEBUG:
"""
Manage DEBUG level of logging.
"""
if LOG_LEVEL >= DEBUG:
cls.log("%s: %s" % ("DEBUG", message))
@classmethod
def warning(cls, message):
if LOG_LEVEL>=WARNING:
"""
Manage WARNING level of logging.
"""
if LOG_LEVEL >= WARNING:
cls.log("%s: %s" % ("WARNING", message))
@classmethod
def info(cls, message):
if LOG_LEVEL>=INFO:
"""
Manage INFO level of logging.
"""
if LOG_LEVEL >= INFO:
cls.log("%s: %s" % ("INFO", message))
@classmethod
def error(cls, message):
if LOG_LEVEL>=ERROR:
"""
Manage ERROR level of logging.
"""
if LOG_LEVEL >= ERROR:
cls.log("%s: %s" % ("ERROR", message))
......@@ -84,6 +101,7 @@ def get_cursor():
return connection.cursor()
return None
def get_url(message):
"""
Retrieve the url in the message.
......@@ -101,27 +119,6 @@ def get_url(message):
url = re.sub("[?&](utm_medium|utm_source|utm_campaign|xtor)=[^&]*", "", url)
return url
def get_title(message):
title = ""
website = ""
exceptions = [
"twitter.com",
"github.com",
]
try:
url = get_url(message)
for exception in exceptions:
if exception in url:
return ("", "")
website = re.search("//([^/]*)", url).group(1)
f = urllib.URLopener().open(url)
content = f.read()
title = re.search("<title>([^<]+)</title>", content).group(1)
# Unescaping HTML entities and removing multiple lines
title = HTMLParser.HTMLParser().unescape(re.sub("\n|\r", "", title))
except:
return ("", "")
return (title, website)
def is_moderator(name):
"""
......@@ -130,11 +127,30 @@ def is_moderator(name):
connection = sqlite3.connect(config.sqlite_db)
cursor = connection.cursor()
cursor.execute("SELECT count(*) FROM moderator WHERE name=?", (name, ))
if int(cursor.fetchone()[0])==1:
if int(cursor.fetchone()[0]) == 1:
return True
return False
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
class Wantzel(object):
"""
Wantzel bot.
......@@ -177,11 +193,11 @@ class Wantzel(object):
"""
self.irc.client.privmsg = self.on_privmsg
def send_message(self, channel, messages):
def send_message(self, channel, multiline_message):
"""
Sends a message on specified channel, cutting each line in a new message
"""
for message in messages.splitlines():
for message in multiline_message.splitlines():
self.irc.client.msg(channel, message)
def on_privmsg(self, user, channel, msg):
......@@ -191,7 +207,7 @@ class Wantzel(object):
- help
Returns a message about how to use the bot.
If a command is passed after help, the message explains how to use
the command.
the command.
- rp(acp)
Add an article in the database
- stats
......@@ -201,27 +217,23 @@ class Wantzel(object):
- admin list (*)
List rights in private
- admin add (*)
Add a new moderator to list
- admin remove (*)
Remove a moderator from list
Add one or more new moderator to list
- admin del (*)
Delete one or more moderator from list
- admin timer
Relaunch a timer
"""
# Cleaning user name
user = re.search("([^!]*)!", user).group(1)
Utils.debug("Message received: %s %s %s" % (user, channel, msg))
# Whatever is done, get the title of an existing url in a message
title = ""
if "http" in msg:
title, website = get_title(msg)
# Never answer to botself
if user!=config.nickname:
if user != config.nickname:
# 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):
self.help(user, channel, msg)
self.help(channel, msg)
# Find known command
command = re.search("[!~](rp[acp]*|kill|help|stats|admin)", msg)
Utils.debug("Command: %s" % command)
......@@ -234,23 +246,23 @@ class Wantzel(object):
if command.startswith("rp"):
Utils.debug("Calling self.rp")
self.rp(command, user, channel, msg)
elif command=="help":
elif command == "help":
Utils.debug("Calling self.help")
self.help(user, channel, msg)
elif command=="kill":
self.help(channel, msg)
elif command == "kill":
Utils.debug("Calling self.kill")
self.kill(user, channel, msg)
elif command=="stats":
elif command == "stats":
Utils.debug("Calling self.stats")
self.stats(user, channel, msg)
elif command=="admin":
self.stats(channel)
elif command == "admin":
Utils.debug("Calling self.admin")
self.admin(user, channel, msg)
# No more giving the title of an url
#if title and website:
# self.send_message(channel, messages["title"] % (title, website))
def help(self, user, channel, msg):
def help(self, channel, msg):
"""
Show global help.
If a known command is behind the ~help command, an adequate message is
......@@ -258,10 +270,10 @@ class Wantzel(object):
"""
Utils.debug("help command")
# Searching for a command after help keyword
command = re.search("~help (help|rp|stats|kill|admin|timer)", msg)
command = re.search("~help (help|rp|stats|kill|admin)", msg)
if command:
command = command.group(1)
self.send_message(channel, messages["help_"+command])
self.send_message(channel, messages["help_"+command])
else:
self.send_message(channel, messages["help"])
......@@ -277,9 +289,9 @@ class Wantzel(object):
note = 1
url = get_url(msg)
Utils.debug("url: %s" % url)
if url=="":
if url == "":
return
elif url=="http":
elif url == "http":
self.send_message(channel, messages["rp_http"] % user)
return
......@@ -292,14 +304,14 @@ class Wantzel(object):
if "c" in command:
cite += 2
# the article speak about LQdN
if command.count("p")>1:
if command.count("p") > 1:
cite += 2
# Archive this article
if "a" in command:
note = -2
Utils.debug("Adding an article by %s: %s" % (user, url))
result = cursor.execute(
"""INSERT INTO presse SET
cursor.execute(
"""INSERT INTO presse SET
url=%s, provenance=%s, cite=%s, note=%s, datec=NOW(), title='',
lang='', published=0, nid=0, screenshot=0, fetched=0, seemscite=0
""",
......@@ -307,13 +319,13 @@ class Wantzel(object):
)
self.send_message(channel, messages["rp_new_article"] % user)
else:
if rows[0][2]!=user:
if rows[0][2] != user:
Utils.debug("Adding a point by %s on %s" % (user, rows[0][0]))
result = cursor.execute(
cursor.execute(
"UPDATE presse SET note=note+1 WHERE id=%s",
(rows[0][0], )
)
if (rows[0][1]+1)<3:
if (rows[0][1]+1) < 3:
self.send_message(channel, messages["rp_known_article"] % user)
else:
self.send_message(channel, messages["rp_taken_article"] % user)
......@@ -328,9 +340,9 @@ class Wantzel(object):
if is_moderator(user):
url = get_url(msg)
Utils.debug("url: %s" % url)
if url=="":
if url == "":
return
elif url=="http":
elif url == "http":
self.send_message(channel, messages["rp_http"] % user)
return
# Looking for such an article in database
......@@ -345,14 +357,14 @@ class Wantzel(object):
else:
self.send_message(channel, messages["not_moderator"])
def stats(self, user, channel, msg):
def stats(self, channel):
"""
Returns stats on articles in press review.
"""
Utils.debug("stats command")
cursor = get_cursor()
periods = [1, 3, 7, 15]
notes = [0, 3 ,4]
notes = [0, 3, 4]
notnull = 0
somethingatall = 0
result = ""
......@@ -361,20 +373,20 @@ class Wantzel(object):
result = result + "note>=%s: " % note
for period in periods:
cursor.execute(
"""SELECT COUNT(id) AS cid FROM presse
"""SELECT COUNT(id) AS cid FROM presse
WHERE nid=0
AND datec>(NOW()-INTERVAL %s DAY)
AND note>=%s""",
(period, note)
)
rows = cursor.fetchall()
if rows[0][0]>0:
if rows[0][0] > 0:
result = result + "%sj:%s, " % (period, rows[0][0])
notnull = 1
somethingatall = 1
if notnull:
result = result[:-2] + "\n"
if somethingatall==0:
if somethingatall == 0:
result = messages["stats_bravo"] % periods[-1]
self.send_message(channel, result)
......@@ -388,16 +400,16 @@ class Wantzel(object):
command = re.search("~admin (list|add|del|timer)", msg)
if command:
command = command.group(1)
if command=="list":
self.admin_list(user, channel, msg)
elif command=="add":
if command == "list":
self.admin_list(user, channel)
elif command == "add":
self.admin_add(user, channel, msg)
elif command=="del":
elif command == "del":
self.admin_del(user, channel, msg)
elif command=="del":
self.admin_timer(user, channel, msg)
elif command == "timer":
self.admin_timer(user, channel)
def admin_list(self, user, channel, msg):
def admin_list(self, user, channel):
"""
List actual moderators.
"""
......@@ -422,13 +434,13 @@ class Wantzel(object):
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 = [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])
names = list(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]
......@@ -437,7 +449,7 @@ class Wantzel(object):
self.send_message(channel, messages["admin_add"] % ", ".join(names))
else:
self.send_message(channel, messages["admin_add_empty"])
except:
except Exception:
pass
else:
self.send_message(channel, messages["not_moderator"])
......@@ -452,21 +464,21 @@ class Wantzel(object):
names = []
result = re.search("~admin del (([^,]+, ?)+)?(.*)", msg)
if result.group(1):
names = [name for name in result.group(1).split(", ") if name!=""]
names = [name for name in result.group(1).split(", ") if name != ""]
names.append(result.group(3))
names = set(names)
names = list(set(names))
Utils.debug(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:
except Exception:
pass
else:
self.send_message(channel, messages["not_moderator"])
def admin_timer(self, user, channel, msg):
def admin_timer(self, user, channel):
"""
Relaunch a timer.
"""
......@@ -475,7 +487,7 @@ class Wantzel(object):
try:
# Recalling the timer
reactor.callLater(config.timer, self.timer)
except:
except Exception:
pass
else:
self.send_message(channel, messages["not_moderator"])
......@@ -494,9 +506,8 @@ class Wantzel(object):
rows = cursor.fetchall()
number = int(rows[0][0])
Utils.debug("Found %s articles." % number)
if self.number!=number:
if self.number != number:
self.irc.client.topic("#lqdn-rp", messages["topic"] % number)
pass
self.number = number
def rp_to_twitter(self, rss):
......@@ -506,13 +517,11 @@ class Wantzel(object):
Utils.debug("rp_to_twitter method")
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"
)
now.tm_year,
now.tm_mon,
now.tm_mday,
time.tzname[0]
), "%Y-%m-%d %Z")
language = "fr"
if "/en/" in rss:
language = "en"
......@@ -524,7 +533,7 @@ class Wantzel(object):
# lesser than future
if today < entry.published_parsed < now:
if self.last_entry_published < entry.published_parsed:
self.tweet(messages["tweet_rp_%s" % language] % (
tweet(messages["tweet_rp_%s" % language] % (
entry.title.encode("utf-8"),
entry.link.encode("utf-8")
))
......@@ -548,25 +557,7 @@ class Wantzel(object):
Utils.debug(entry.title)
Utils.debug(entry.published_parsed)
def tweet(self, 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:
pass