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 = { ...@@ -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)""", """Bonjour, je suis le bot de la Quadrature du Net, vous pouvez me demander de l'aide si besoin. (wantzel help)""",
"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 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>""", Pour obtenir de l'aide sur une commande en particulier, il suffit de taper ~help <commande>""",
...@@ -19,7 +19,7 @@ messages = { ...@@ -19,7 +19,7 @@ messages = {
"help_rp": "help_rp":
"""Cette commande sert à ajouter un article à la Revue de Presse (https://wiki.laquadrature.net/Revue_de_presse) """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": "help_status":
"""Cette commande sert à retrouver les informations concernant un article ajouté à la Revue de Presse (https://wiki.laquadrature.net/Revue_de_presse) """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()