Commit 6582ec6d authored by Mindiell's avatar Mindiell

All specific functions have been moved into specific modules. Code is clearer now.

parent b4eac9cc
#encoding: utf-8
"""
Monitoring methods.
"""
import re
import sqlite3
import config
from logs import Log
from messages import messages
from utils import get_cursor, get_url, is_moderator
class Admin():
def admin(self, user, msg):
"""
Manage moderation.
A sub-command should be behind the !~admin command.
"""
Log.debug("admin command")
# Searching for a command after admin keyword
command = re.search("[~!]admin (list|add|del|timer)", msg)
if command:
command = command.group(1)
if command == "list":
return self.admin_list(user)
elif command == "add":
return self.admin_add(user, msg)
elif command == "del":
return self.admin_del(user, msg)
elif command == "timer":
return self.admin_timer(user)
def admin_list(self, user):
"""
List actual moderators.
"""
Log.debug("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"))
return messages["admin_list"] % ", ".join(sorted(names))
else:
return messages["not_moderator"]
def admin_add(self, user, msg):
"""
Add some new moderators if not existing yet.
"""
Log.debug("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.strip() for name in result.group(1).split(",") if name.strip() != ""]
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 = 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]
connection.executemany("INSERT INTO moderator (name) VALUES (?)", values)
connection.commit()
return messages["admin_add"] % ", ".join(names)
else:
return messages["admin_add_empty"]
except Exception:
return ""
else:
return messages["not_moderator"]
def admin_del(self, user, msg):
"""
Delete a moderator from list.
"""
Log.debug("admin_del command")
if is_moderator(user):
try:
names = []
result = re.search("[!~]admin del (([^,]+, ?)+)?(.*)", msg)
if result.group(1):
names = [name.strip() for name in result.group(1).split(",") if name.strip() != ""]
names.append(result.group(3))
names = list(set(names))
Log.debug(names)
connection = sqlite3.connect(config.sqlite_db)
for name in names:
connection.execute("DELETE FROM moderator WHERE name=?", (name, ))
connection.commit()
return messages["admin_del"] % ", ".join(names)
except Exception:
return ""
else:
return messages["not_moderator"]
def admin_timer(self, user):
"""
Relaunch a timer.
"""
Log.debug("admin_timer command")
if is_moderator(user):
return "REACTOR"
else:
return messages["not_moderator"]
#encoding: utf-8
"""
Fun commands.
"""
from random import choice
def fun(user, channel, message):
"""
This function is there to add some fun commands which could be not used easily.
"""
# Specific answer to Deltree, the animal's joke
if user.lower()=="deltree" and msg=="\_o< ~ Coin ~ >o_/":
animal = choice([
{
"left": """><((('>""",
"right": """<')))><""",
"sound": "blub",
},
{
"left": """=^..^=""",
"right": """=^..^=""",
"sound": "meow",
},
{
"left": """ˁ˚ᴥ˚ˀ""",
"right": """ˁ˚ᴥ˚ˀ""",
"sound": "wouf",
},
{
"left": """\_o<""",
"right": """>o_/""",
"sound": "coin",
},
{
"left": """^(*(oo)*)^""",
"right": """^(*(oo)*)^""",
"sound": "grouïk",
},
{
"left": """~~(__^·>""",
"right": """<·^__)~~""",
"sound": "yiik",
},
])
return messages["coin_deltree"] % (
animal["left"],
animal["sound"],
animal["sound"],
animal["right"],
)
return ""
#encoding: utf-8
"""
Utilities classes.
"""
import time
import config
class Log(object):
"""
Simple utility class to log easily.
"""
@classmethod
def log(cls, message):
"""
Logging message with timestamp.
"""
actual_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
with open(config.LOG_FILE, 'a') as file_handle:
try:
file_handle.write("%s: %s\n" % (actual_time, message.encode("utf-8")))
except UnicodeDecodeError:
file_handle.write("%s: %s\n" % (actual_time, message))
@classmethod
def debug(cls, message):
"""
Manage DEBUG level of logging.
"""
if config.LOG_LEVEL >= config.DEBUG:
cls.log("%s: %s" % ("DEBUG", message))
@classmethod
def warning(cls, message):
"""
Manage WARNING level of logging.
"""
if config.LOG_LEVEL >= config.WARNING:
cls.log("%s: %s" % ("WARNING", message))
@classmethod
def info(cls, message):
"""
Manage INFO level of logging.
"""
if config.LOG_LEVEL >= config.INFO:
cls.log("%s: %s" % ("INFO", message))
@classmethod
def error(cls, message):
"""
Manage ERROR level of logging.
"""
if config.LOG_LEVEL >= config.ERROR:
cls.log("%s: %s" % ("ERROR", message))
......@@ -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(cpam) ~status ~kill ~stats et ~admin.
"""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>""",
......@@ -19,7 +19,7 @@ messages = {
"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>""",
L'utilisation se fait sous la forme: ~rp(cpa) <url de l'article à ajouter>""",
"help_status":
"""Cette commande sert à retrouver les informations concernant un article ajouté à la Revue de Presse (https://wiki.laquadrature.net/Revue_de_presse)
......
#encoding: utf-8
"""
Monitoring methods.
"""
import feedparser
import sqlite3
import time
import config
from logs import Log
from messages import messages
class Wiki():
def __init__(self, name, url):
# wiki's name
self.name = name
# base url
self.url = url
# default last_entry_published
self.last_entry_updated = time.strptime("2000-01-01", "%Y-%m-%d")
# See if there is a later last_entry_published for wiki
connection = sqlite3.connect(config.sqlite_db)
for row in connection.execute(
"SELECT last_entry_updated FROM wikis WHERE name=?",
(self.name,)
):
self.last_entry_updated = time.strptime(
row[0].encode("utf-8"),
"%Y-%m-%d %H:%M:%S %Z"
)
Log.debug("Dernière mise à jour du wiki: %s" % self.last_entry_updated)
def set_last_entry_updated(self, last_entry_updated):
self.last_entry_updated = last_entry_updated
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=? WHERE name=?",
(last_entry_updated, self.name)
)
connection.commit()
class Monitor():
def __init__(self):
# List of wikis to monitor
self.wikis = []
for wiki in config.wikis["mediawiki"]:
self.wikis.append(Wiki(wiki["name"], wiki["url"]))
def wiki_updates(self):
"""
This method loops over each wiki to monitor.
"""
messages = []
for wiki in self.wikis:
url = wiki.url + "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 wiki.last_entry_updated < entry.updated_parsed:
# Ecriture de la mise à jour sur le canal de travail
messages.append(messages["wiki_update"] % (
entry.author.encode("utf-8"),
entry.title.encode("utf-8"),
entry.link.encode("utf-8"),
))
# Save last_entry_published
wiki.set_last_entry_updated(entry.updated_parsed)
return messages
#encoding: utf-8
"""
Channel operator methods.
"""
import re
import config
from logs import Log
from messages import messages
class Op():
def __init__(self):
# Sequence for op_mode verification (fibonacci)
self.op_sequence = [1, 2, 3, 5, 8, 13, 21, 34, 55]
self.op_offset = 0
self.op_counter = 0
def need_op_mode(self, me, params):
"""
Send a message on channel RP_CHANNEL to beg an op mode to each actual operators.
params is an array with :
- params[0]:
- params[1]:
- params[2]: the channel
- params[3]: the list of all users on the channel
"""
Log.debug("Names : %s" % params)
message = ""
if params[2]==config.RP_CHANNEL:
ops = [user[1:] for user in params[3].split() if user[0]=="@"]
if me not in params[3]:
# Testing based on fibonacci sequence
self.op_counter += 1
Log.debug("op_counter : %s" % self.op_counter)
Log.debug("op_offset : %s" % self.op_offset)
Log.debug("op_sequence : %s" % self.op_sequence[self.op_offset])
if self.op_counter>self.op_sequence[self.op_offset]:
message = messages["please_op"] % ", ".join(ops)
# Then reset op_counter
self.op_counter = 0
# And move the sequence further in order not to spam channel
if self.op_offset<len(self.op_sequence)-1:
self.op_offset += 1
return message
def get_op_mode(self, user, flag_set):
# Cleaning user name
user = re.search("([^!]*)!", user).group(1)
if flag_set:
# reset counter and sequence
self.op_counter = 0
self.op_offset = 0
# thanks to user
return messages["oped"] % user
else:
# bad user ;o(
return messages["deoped"] % user
#encoding: utf-8
"""
Press review methods.
"""
import feedparser
import sqlite3
import time
import config
from logs import Log
from messages import messages
from utils import get_cursor, get_url, is_moderator
class Rp():
def __init__(self):
"""
Initialisation of the press review utilities
"""
# Date of next cleaning
next_week = time.localtime(time.mktime(time.localtime())+(config.MASTER_CLEANING*86400))
self.next_cleaning = time.strptime("%s-%s-%s %s" % (
next_week.tm_year,
next_week.tm_mon,
next_week.tm_mday,
time.tzname[0]
), "%Y-%m-%d %Z")
# Number of press review articles actually waiting
self.number = None
# default last_entry_published for tweets
self.last_entry_published = time.strptime("2000-01-01", "%Y-%m-%d")
# See if there is a later last_entry_published for tweets
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"
)
Log.debug("Dernier tweet: %s" % self.last_entry_published)
def tweet(self):
"""
Tweet some RP based on french and english feeds
"""
for feed in config.feeds:
self.rp_to_twitter(feed)
def rp_to_twitter(self, feed):
"""
By parsing the RSS feed of the press-review, we know what to tweet.
"""
Log.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")
language = "fr"
if "/en/" in feed:
language = "en"
entries = feedparser.parse(feed)['entries']
entries.reverse()
Log.debug(self.last_entry_published)
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:
# 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")
if len(title) + min(len(link),23) > 135:
# What is the number of chars we need to remove
excess = len(title) + min(len(link),23) - 135
title = ''.join([title[:-(excess + 2)], ' …'])
tweet(messages["tweet_rp_%s" % language] % (
title,
link
))
Log.debug(entry.published_parsed)
Log.debug(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
else:
Log.debug(entry.title)
Log.debug(entry.published_parsed)
def count_articles(self):
"""
Count number of articles not done in RP and returns new topic of the press review channel
if necessary.
"""
Log.debug("count_articles method")
topic = ""
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])
Log.debug("Found %s articles." % number)
if self.number != number:
topic = messages["topic"] % number
self.number = number
return topic
def clean_master_rp(self):
"""
This method cleans known users from rp_mastering each MASTER_CLEANING days
"""
if time.localtime()>self.next_cleaning:
# Cleaning users
connection = sqlite3.connect(config.sqlite_db)
connection.execute("UPDATE rpator SET score=score-1")
connection.execute("DELETE FROM rpator WHERE score<1")
connection.commit()
# Date of next cleaning
next_week = time.localtime(time.mktime(time.localtime())+(MASTER_CLEANING*86400))
self.next_cleaning = time.strptime("%s-%s-%s %s" % (
next_week.tm_year,
next_week.tm_mon,
next_week.tm_mday,
time.tzname[0]
), "%Y-%m-%d %Z")
def status(self, user, msg):
"""
Retrieving status of the article in rp database.
"""
url = get_url(msg)
Log.debug("url: %s" % url)
if not url:
return ""
# Looking for such an article in database
cursor = get_cursor()
# 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 cite, nid, note
FROM presse
WHERE url = %s
OR url = %s""",
(url, url2)
)
rows = cursor.fetchall()
if not rows:
return messages["status_unknown_article"] % user
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]
else:
message += "non publié / "
return message[:-3]
def kill(self, user, msg):
"""
Kill an article by setting its score to -100.
"""
Log.debug("kill command")
if is_moderator(user):
url = get_url(msg)
Log.debug("url: %s" % url)
if url == "":
return message
elif url == "http":
return messages["rp_http"] % user
# Looking for such an article in database
cursor = get_cursor()
# 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)
)
rows = cursor.fetchall()
if not rows:
return messages["kill_none"] % url
else:
cursor.execute("UPDATE presse SET note=-100 WHERE id=%s", (rows[0][0], ))
return messages["kill_done"] % url
return messages["not_moderator"]
def stats(self):
"""
Returns stats on articles in press review.
"""
Log.debug("stats command")
cursor = get_cursor()
periods = [1, 3, 7, 15]
notes = [0, 3, 4]
notnull = 0
somethingatall = 0
for note in notes:
notnull = 0
period_result = ""
for period in periods:
cursor.execute("""
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:
period_result = period_result + "%sj:%s, " % (period, rows[0][0])
notnull = 1
somethingatall = 1
if notnull:
return "note>=%s: " % note + period_result[:-2]
if somethingatall == 0:
return messages["stats_bravo"] % periods[-1]
def rp(self, command, user, channel, msg):
"""
Adding the article in rp database.
"""
cite = 0
note = 1
answer = False
url = get_url(msg)
Log.debug("url: %s" % url)
if not url:
return ""
# 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
# Looking for such an article in database
cursor = get_cursor()
# 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)
)
rows = cursor.fetchall()
if not rows:
Log.debug("Adding an article by %s: %s" % (user, url))
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
""",
(url, user, cite, note)
)
answer = True
else:
if rows[0][2] != user:
Log.debug("Adding a point by %s on %s" % (user, rows[0][0]))
cursor.execute(
"UPDATE presse SET note=note+1 WHERE id=%s",
(rows[0][0], )
)
note = rows[0][1]+1
answer = True
if note>=3:
# Update number of articles to do
self.count_articles()
if answer:
# Answer is now based on where, who, note, and a little magic
return self.did_rp(channel, user, note)
def did_rp(self, channel, user, note):
"""
Answers after a "rp" command has been submitted. The answer is based on the channel, the
user and the note of the article.
"""