2021-12-05 18:30:01 +01:00
|
|
|
|
#!/usr/bin/env python3
|
2018-09-05 15:38:58 +02:00
|
|
|
|
# -*- coding: utf-8 -1 -*-
|
|
|
|
|
|
2021-12-05 18:30:01 +01:00
|
|
|
|
# Need: python3-iso8601 python3-feedparser
|
|
|
|
|
|
2018-09-05 15:38:58 +02:00
|
|
|
|
# Import some necessary libraries.
|
2021-12-05 18:30:01 +01:00
|
|
|
|
import socket, sys, time, csv, queue, random, re, pdb, select, os.path, datetime
|
2018-09-05 15:38:58 +02:00
|
|
|
|
from threading import Thread
|
|
|
|
|
import feedparser
|
|
|
|
|
import xml.dom.minidom
|
|
|
|
|
from time import mktime, localtime
|
|
|
|
|
|
|
|
|
|
import iso8601
|
2021-12-05 18:30:01 +01:00
|
|
|
|
import configparser
|
2018-09-05 15:38:58 +02:00
|
|
|
|
|
2019-08-22 01:33:25 +02:00
|
|
|
|
# Help configuration.
|
2018-09-05 15:38:58 +02:00
|
|
|
|
help_list = ["help", "faq", "aide"]
|
|
|
|
|
hello_list = ["hello", "yo", "bonjour", "salut"]
|
2019-08-22 01:33:25 +02:00
|
|
|
|
|
|
|
|
|
# Load configuration.
|
2021-12-05 18:30:01 +01:00
|
|
|
|
configurationFilename = "/etc/redminebot/redminebot.conf"
|
2020-02-05 10:29:53 +01:00
|
|
|
|
if len(sys.argv) == 2:
|
2021-12-05 18:30:01 +01:00
|
|
|
|
configurationFilename = sys.argv[1]
|
2020-02-05 10:29:53 +01:00
|
|
|
|
|
2019-08-22 01:33:25 +02:00
|
|
|
|
if os.path.isfile(configurationFilename):
|
2021-12-05 18:30:01 +01:00
|
|
|
|
print("Using configuration file: " + configurationFilename)
|
|
|
|
|
config = configparser.RawConfigParser()
|
2019-08-22 01:33:25 +02:00
|
|
|
|
config.read(configurationFilename)
|
|
|
|
|
|
2021-12-05 18:30:01 +01:00
|
|
|
|
default_server = config.get("IRCSection", "irc.server")
|
|
|
|
|
default_nickname = config.get("IRCSection", "irc.nickname")
|
|
|
|
|
registered = config.get("IRCSection", "irc.registered")
|
|
|
|
|
password = config.get("IRCSection", "irc.password")
|
2019-08-25 19:42:54 +02:00
|
|
|
|
|
2021-12-05 18:30:01 +01:00
|
|
|
|
projectId1 = config.get("IRCSection", "irc.projects.1.id")
|
|
|
|
|
projectChannel1 = config.get("IRCSection", "irc.projects.1.channel")
|
|
|
|
|
projectId2 = config.get("IRCSection", "irc.projects.2.id")
|
|
|
|
|
projectChannel2 = config.get("IRCSection", "irc.projects.2.channel")
|
|
|
|
|
projectId3 = config.get("IRCSection", "irc.projects.3.id")
|
|
|
|
|
projectChannel3 = config.get("IRCSection", "irc.projects.3.channel")
|
2019-08-22 02:05:21 +02:00
|
|
|
|
|
2019-08-22 01:33:25 +02:00
|
|
|
|
else:
|
2021-12-05 18:30:01 +01:00
|
|
|
|
print("Missing configuration file.")
|
2019-08-22 01:33:25 +02:00
|
|
|
|
sys.exit()
|
|
|
|
|
|
2018-09-05 15:38:58 +02:00
|
|
|
|
|
|
|
|
|
#########################
|
|
|
|
|
### Class Definitions ###
|
|
|
|
|
#########################
|
|
|
|
|
|
2021-12-05 18:30:01 +01:00
|
|
|
|
|
2018-09-05 15:38:58 +02:00
|
|
|
|
class Project(object):
|
|
|
|
|
def __init__(self, project, channel):
|
|
|
|
|
self.name = project
|
2021-12-05 18:30:01 +01:00
|
|
|
|
self.channel = channel
|
2018-09-05 15:38:58 +02:00
|
|
|
|
self.redmine_next = datetime.datetime.utcnow()
|
|
|
|
|
self.redmine_latest = datetime.datetime.utcnow()
|
|
|
|
|
|
2021-12-05 18:30:01 +01:00
|
|
|
|
def set_ircsock(self, ircsock):
|
2018-09-05 15:38:58 +02:00
|
|
|
|
self.ircsock = ircsock
|
|
|
|
|
|
|
|
|
|
def redmine(self):
|
|
|
|
|
t = datetime.datetime.utcnow()
|
2021-12-05 18:30:01 +01:00
|
|
|
|
# print ("Running: %s (%s) for %s" % ( t, self.redmine_next, self.channel ))
|
2018-09-05 15:38:58 +02:00
|
|
|
|
if t >= self.redmine_next:
|
|
|
|
|
latest_new = self.redmine_latest
|
2021-12-05 18:30:01 +01:00
|
|
|
|
|
2018-09-05 15:38:58 +02:00
|
|
|
|
self.redmine_next = self.redmine_next + datetime.timedelta(seconds=10)
|
2021-12-05 18:30:01 +01:00
|
|
|
|
redmine = feedparser.parse(
|
|
|
|
|
"http://agir.april.org/projects/%s/activity.atom?show_issues=1"
|
|
|
|
|
% self.name
|
|
|
|
|
)
|
|
|
|
|
|
2018-09-05 15:38:58 +02:00
|
|
|
|
for i in reversed(range(len(redmine.entries))):
|
|
|
|
|
e = redmine.entries[i]
|
|
|
|
|
et = e.updated_parsed
|
|
|
|
|
td = iso8601.parse_date(e.updated)
|
|
|
|
|
if td.replace(tzinfo=None) > self.redmine_latest:
|
|
|
|
|
msg = "Redmine: (%s): %s" % (e.link, e.title)
|
2021-12-05 18:30:01 +01:00
|
|
|
|
self.ircsock.send(
|
|
|
|
|
"PRIVMSG {0} :{1}\n".format(
|
|
|
|
|
self.channel, msg.encode("utf-8", "ignore")
|
|
|
|
|
).encode()
|
|
|
|
|
)
|
|
|
|
|
|
2018-09-05 15:38:58 +02:00
|
|
|
|
# find the new latest time
|
|
|
|
|
if td.replace(tzinfo=None) > latest_new:
|
|
|
|
|
latest_new = td.replace(tzinfo=None)
|
|
|
|
|
self.redmine_latest = latest_new
|
2021-12-05 18:30:01 +01:00
|
|
|
|
|
2018-09-05 15:38:58 +02:00
|
|
|
|
|
|
|
|
|
# Defines a bot
|
|
|
|
|
class Bot(object):
|
|
|
|
|
def __init__(self, server, botnick):
|
2021-12-05 18:30:01 +01:00
|
|
|
|
self.botnick = botnick
|
2018-09-05 15:38:58 +02:00
|
|
|
|
self.hello_regex = re.compile(self.get_regex(hello_list), re.I)
|
|
|
|
|
self.help_regex = re.compile(self.get_regex(help_list), re.I)
|
|
|
|
|
self.server = server
|
2021-12-05 18:30:01 +01:00
|
|
|
|
self.projects = []
|
2018-09-05 15:38:58 +02:00
|
|
|
|
|
|
|
|
|
def connect(self):
|
|
|
|
|
self.ircsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
|
self.ircsock.connect((self.server, 6667))
|
2021-12-05 18:30:01 +01:00
|
|
|
|
self.ircsock.send(
|
|
|
|
|
"USER {0} {0} {0} :Robot Agir April" ".\n".format(self.botnick).encode()
|
|
|
|
|
) # bot authentication
|
|
|
|
|
self.ircsock.send(
|
|
|
|
|
"NICK {}\n".format(self.botnick).encode()
|
|
|
|
|
) # Assign the nick to the bot.
|
2019-08-22 01:33:25 +02:00
|
|
|
|
|
|
|
|
|
if not password.strip() and registered == True:
|
2021-12-05 18:30:01 +01:00
|
|
|
|
print("PRIVMSG {} {} {}".format("NickServ", "IDENTIFY", password))
|
|
|
|
|
self.ircsock.send(
|
|
|
|
|
"PRIVMSG {} :{} {} {}".format(
|
|
|
|
|
"NickServ", "IDENTIFY", self.botnick, password
|
|
|
|
|
).encode()
|
|
|
|
|
)
|
2018-09-05 15:38:58 +02:00
|
|
|
|
|
|
|
|
|
def add_project(self, project):
|
2021-12-05 18:30:01 +01:00
|
|
|
|
project.set_ircsock(self.ircsock)
|
|
|
|
|
self.ircsock.send("JOIN {} \n".format(project.channel).encode()) # Joins channel
|
2018-09-05 15:38:58 +02:00
|
|
|
|
self.projects.append(project)
|
|
|
|
|
|
|
|
|
|
def get_project(self, name):
|
|
|
|
|
for project in self.projects:
|
2021-12-05 18:30:01 +01:00
|
|
|
|
if name[0] != "#" and project.name == name:
|
2018-09-05 15:38:58 +02:00
|
|
|
|
return project
|
2021-12-05 18:30:01 +01:00
|
|
|
|
elif name[0] == "#" and project.channel == name:
|
2018-09-05 15:38:58 +02:00
|
|
|
|
return project
|
|
|
|
|
|
|
|
|
|
# Main loop
|
|
|
|
|
def loop(self):
|
|
|
|
|
last_read = datetime.datetime.utcnow()
|
|
|
|
|
while 1: # Loop forever
|
2021-12-05 18:30:01 +01:00
|
|
|
|
ready_to_read, b, c = select.select([self.ircsock], [], [], 1)
|
2018-09-05 15:38:58 +02:00
|
|
|
|
if ready_to_read:
|
|
|
|
|
last_read = datetime.datetime.utcnow()
|
|
|
|
|
ircmsg = self.msg_handler()
|
|
|
|
|
ircmsg, actor, channel = self.parse_messages(ircmsg)
|
|
|
|
|
if ircmsg is not None:
|
|
|
|
|
self.message_response(ircmsg, actor, channel)
|
|
|
|
|
if datetime.datetime.utcnow() - last_read > datetime.timedelta(minutes=10):
|
2021-12-05 18:30:01 +01:00
|
|
|
|
raise Exception("timeout: nothing to read on socket since 10 minutes")
|
2018-09-05 15:38:58 +02:00
|
|
|
|
|
|
|
|
|
# Responds to server Pings.
|
|
|
|
|
def pong(self, ircmsg):
|
|
|
|
|
response = "PONG :" + ircmsg.split("PING :")[1] + "\n"
|
2021-12-05 18:30:01 +01:00
|
|
|
|
self.ircsock.send(response.encode())
|
2018-09-05 15:38:58 +02:00
|
|
|
|
self._redmine()
|
|
|
|
|
|
2021-12-05 18:30:01 +01:00
|
|
|
|
def _redmine(self):
|
2018-09-05 15:38:58 +02:00
|
|
|
|
for project in self.projects:
|
|
|
|
|
project.redmine()
|
|
|
|
|
|
|
|
|
|
# Parses messages and responds to them appropriately.
|
|
|
|
|
def message_response(self, ircmsg, actor, channel):
|
|
|
|
|
# If someone talks to (or refers to) the bot.
|
2021-12-05 18:30:01 +01:00
|
|
|
|
if (
|
|
|
|
|
(":!" in ircmsg or self.botnick.lower() in ircmsg.lower())
|
|
|
|
|
and actor != self.botnick
|
|
|
|
|
and "PRIVMSG".lower() in ircmsg.lower()
|
|
|
|
|
):
|
2018-09-05 15:38:58 +02:00
|
|
|
|
if self.hello_regex.search(ircmsg):
|
2021-12-05 18:30:01 +01:00
|
|
|
|
self.bot_hello(channel, "Yo!")
|
2018-09-05 15:38:58 +02:00
|
|
|
|
if self.help_regex.search(ircmsg):
|
|
|
|
|
self.bot_help(channel)
|
2021-12-05 18:30:01 +01:00
|
|
|
|
if "refresh" in ircmsg.lower():
|
|
|
|
|
self.ircsock.send(
|
|
|
|
|
"PRIVMSG {} :Raffraîchissement en cours\n".format(channel).encode()
|
|
|
|
|
)
|
2018-09-05 15:38:58 +02:00
|
|
|
|
project = self.get_project(channel)
|
|
|
|
|
if project:
|
|
|
|
|
project.redmine()
|
2021-12-05 18:30:01 +01:00
|
|
|
|
self.ircsock.send("PRIVMSG {} :Fait !\n".format(channel).encode())
|
2018-09-05 15:38:58 +02:00
|
|
|
|
|
|
|
|
|
# If the server pings us then we've got to respond!
|
|
|
|
|
if ircmsg.find("PING :") != -1:
|
|
|
|
|
self.pong(ircmsg)
|
|
|
|
|
|
|
|
|
|
# Responds to a user that inputs "Hello Mybot".
|
|
|
|
|
def bot_hello(self, channel, greeting):
|
2021-12-05 18:30:01 +01:00
|
|
|
|
self.ircsock.send("PRIVMSG {0} :{1}\n".format(channel, greeting).encode())
|
2018-09-05 15:38:58 +02:00
|
|
|
|
|
|
|
|
|
# Explains what the bot is when queried.
|
|
|
|
|
def bot_help(self, channel):
|
2021-12-05 18:30:01 +01:00
|
|
|
|
self.ircsock.send(
|
|
|
|
|
"PRIVMSG {} :Bonjour, je suis un bot qui reconnaît les options !help, !refresh et !bonjour\n".format(
|
|
|
|
|
channel
|
|
|
|
|
).encode()
|
|
|
|
|
)
|
|
|
|
|
self.ircsock.send(
|
|
|
|
|
"PRIVMSG {} :Périodiquement, je vais afficher les actualités de http://agir.april.org/projects/{}.\n".format(
|
|
|
|
|
channel, self.get_project(channel).name
|
|
|
|
|
).encode()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Reads the messages from the server and adds them to the queue and prints
|
2018-09-05 15:38:58 +02:00
|
|
|
|
# them to the console. This function will be run in a thread, see below.
|
2021-12-05 18:30:01 +01:00
|
|
|
|
def msg_handler(
|
|
|
|
|
self,
|
|
|
|
|
): # pragma: no cover (this excludes this function from testing)
|
2018-09-05 15:38:58 +02:00
|
|
|
|
new_msg = self.ircsock.recv(2048) # receive data from the server
|
2020-07-04 18:24:58 +02:00
|
|
|
|
if not new_msg:
|
2021-12-05 19:24:15 +01:00
|
|
|
|
print("Empty recv. It seems I’ve lost my mind. I stop to be reborn.", new_msg)
|
|
|
|
|
#sys.exit(7)
|
|
|
|
|
time.sleep(1)
|
2020-07-04 18:24:58 +02:00
|
|
|
|
else:
|
2021-12-05 18:30:01 +01:00
|
|
|
|
new_msg = new_msg.strip("\n\r".encode()) # removing any unnecessary linebreaks
|
|
|
|
|
if new_msg != "" and new_msg.find("PING :".encode()) == -1:
|
|
|
|
|
print(datetime.datetime.now().isoformat() + " " + new_msg.decode())
|
2018-09-05 15:38:58 +02:00
|
|
|
|
return new_msg
|
|
|
|
|
|
|
|
|
|
# Checks for messages.
|
|
|
|
|
def parse_messages(self, ircmsg):
|
|
|
|
|
try:
|
|
|
|
|
actor = ircmsg.split(":")[1].split("!")[0]
|
|
|
|
|
try:
|
|
|
|
|
target = ircmsg.split(":")[1].split(" ")[2]
|
|
|
|
|
except:
|
|
|
|
|
target = None
|
|
|
|
|
return " ".join(ircmsg.split()), actor, target
|
|
|
|
|
except:
|
2021-12-05 18:30:01 +01:00
|
|
|
|
# print ("Wrong message:", ircmsg)
|
2018-09-05 15:38:58 +02:00
|
|
|
|
return None, None, None
|
|
|
|
|
|
|
|
|
|
# Compile regex
|
|
|
|
|
def get_regex(self, options):
|
|
|
|
|
pattern = "("
|
|
|
|
|
for s in options:
|
|
|
|
|
pattern += s
|
2021-12-05 18:30:01 +01:00
|
|
|
|
pattern += "|"
|
2018-09-05 15:38:58 +02:00
|
|
|
|
pattern = pattern[:-1]
|
|
|
|
|
pattern += ")"
|
|
|
|
|
return pattern
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
##########################
|
|
|
|
|
### The main function. ###
|
|
|
|
|
##########################
|
|
|
|
|
|
2021-12-05 18:30:01 +01:00
|
|
|
|
|
2018-09-05 15:38:58 +02:00
|
|
|
|
def main():
|
2021-12-05 18:30:01 +01:00
|
|
|
|
print(datetime.datetime.now().isoformat() + " redmine bot starting…")
|
2018-09-05 15:38:58 +02:00
|
|
|
|
redmine_bot = Bot(default_server, default_nickname)
|
|
|
|
|
redmine_bot.connect()
|
2019-08-22 02:05:21 +02:00
|
|
|
|
if projectId1 and projectChannel1:
|
|
|
|
|
redmine_bot.add_project(Project(projectId1, projectChannel1))
|
|
|
|
|
if projectId2 and projectChannel2:
|
|
|
|
|
redmine_bot.add_project(Project(projectId2, projectChannel2))
|
|
|
|
|
if projectId3 and projectChannel3:
|
|
|
|
|
redmine_bot.add_project(Project(projectId3, projectChannel3))
|
2018-09-05 15:38:58 +02:00
|
|
|
|
return redmine_bot.loop()
|
2021-12-05 18:30:01 +01:00
|
|
|
|
|
2018-09-05 15:38:58 +02:00
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2021-12-05 18:30:01 +01:00
|
|
|
|
sys.exit(main())
|