From 20c248c35dd166cb10eb219bf79d4c0ce8dac7f6 Mon Sep 17 00:00:00 2001 From: Quentin Gibeaux Date: Wed, 5 Sep 2018 15:38:58 +0200 Subject: [PATCH] add redmine bot --- bots/redminebot/LICENSE | 16 +++ bots/redminebot/redminebot.py | 194 +++++++++++++++++++++++++++++++++ bots/redminebot/redminebot.py~ | 190 ++++++++++++++++++++++++++++++++ bots/redminebot/redminebot.sh | 8 ++ bots/redminebot/redminebot.sh~ | 11 ++ 5 files changed, 419 insertions(+) create mode 100644 bots/redminebot/LICENSE create mode 100755 bots/redminebot/redminebot.py create mode 100755 bots/redminebot/redminebot.py~ create mode 100755 bots/redminebot/redminebot.sh create mode 100755 bots/redminebot/redminebot.sh~ diff --git a/bots/redminebot/LICENSE b/bots/redminebot/LICENSE new file mode 100644 index 0000000..7d017c3 --- /dev/null +++ b/bots/redminebot/LICENSE @@ -0,0 +1,16 @@ +Copyright (C) 2017 Benjamin Drieu + +This is heavily derived from IRC bot for Redmine, whose licence is: + + IRC bot for Redmine + Copyright (C) 2011 Jasmin Rahimic + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. diff --git a/bots/redminebot/redminebot.py b/bots/redminebot/redminebot.py new file mode 100755 index 0000000..cb9472b --- /dev/null +++ b/bots/redminebot/redminebot.py @@ -0,0 +1,194 @@ +#!/usr/bin/python -u +# -*- coding: utf-8 -1 -*- + +# Import some necessary libraries. +import socket, sys, time, csv, Queue, random, re, pdb, select, os.path, datetime +from threading import Thread +import feedparser +import xml.dom.minidom +from time import mktime, localtime + +import iso8601 + + +# IRC configuration + +default_server = "irc.eu.freenode.net" +default_nickname = "agirbot" +help_list = ["help", "faq", "aide"] +hello_list = ["hello", "yo", "bonjour", "salut"] + +######################### +### Class Definitions ### +######################### + +class Project(object): + def __init__(self, project, channel): + self.name = project + self.channel = channel + self.redmine_next = datetime.datetime.utcnow() + self.redmine_latest = datetime.datetime.utcnow() + + def set_ircsock ( self, ircsock ): + self.ircsock = ircsock + + def redmine(self): + t = datetime.datetime.utcnow() +# print "Running: %s (%s) for %s" % ( t, self.redmine_next, self.channel ) + if t >= self.redmine_next: + latest_new = self.redmine_latest + + self.redmine_next = self.redmine_next + datetime.timedelta(seconds=10) + redmine = feedparser.parse("http://agir.april.org/projects/%s/activity.atom?show_issues=1" % self.name) + + 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) + self.ircsock.send("PRIVMSG {0} :{1}\n".format(self.channel, msg.encode('utf-8', 'ignore'))) + + # find the new latest time + if td.replace(tzinfo=None) > latest_new: + latest_new = td.replace(tzinfo=None) + self.redmine_latest = latest_new + + +# Defines a bot +class Bot(object): + + def __init__(self, server, botnick): + self.botnick = botnick + 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 + self.projects = [ ] + + def connect(self): + self.ircsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.ircsock.connect((self.server, 6667)) + self.ircsock.send("USER {0} {0} {0} :Robot Agir April" + ".\n".format(self.botnick)) # bot authentication + self.ircsock.send("NICK {}\n".format(self.botnick)) # Assign the nick to the bot. + if os.path.isfile("password.txt"): + with open("password.txt", 'r') as f: + password = f.read() + if registered == True: + self.ircsock.send("PRIVMSG {} {} {} {}".format("NickServ","IDENTIFY", self.botnick, password)) + + def add_project(self, project): + project.set_ircsock ( self.ircsock ) + self.ircsock.send("JOIN {} \n".format(project.channel)) # Joins channel + self.projects.append(project) + + def get_project(self, name): + for project in self.projects: + if name[0] != '#' and project.name == name: + return project + elif name[0] == '#' and project.channel == name: + return project + + # Main loop + def loop(self): + last_read = datetime.datetime.utcnow() + while 1: # Loop forever + ready_to_read, b, c = select.select([self.ircsock],[],[], 1) + 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): + raise Exception('timeout: nothing to read on socket since 10 minutes') + + # Responds to server Pings. + def pong(self, ircmsg): + response = "PONG :" + ircmsg.split("PING :")[1] + "\n" + self.ircsock.send(response) + self._redmine() + + def _redmine(self): + 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. + if ( ':!' in ircmsg or self.botnick.lower() in ircmsg.lower() ) and \ + actor != self.botnick and \ + "PRIVMSG".lower() in ircmsg.lower(): + if self.hello_regex.search(ircmsg): + self.bot_hello(channel, 'Yo!') + if self.help_regex.search(ircmsg): + self.bot_help(channel) + if 'refresh' in ircmsg.lower(): + self.ircsock.send("PRIVMSG {} :Raffraîchissement en cours\n".format(channel)) + project = self.get_project(channel) + if project: + project.redmine() + self.ircsock.send("PRIVMSG {} :Fait !\n".format(channel)) + + # 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): + self.ircsock.send("PRIVMSG {0} :{1}\n".format(channel, greeting)) + + # Explains what the bot is when queried. + def bot_help(self, channel): + self.ircsock.send("PRIVMSG {} :Bonjour, je suis un bot qui reconnaît les options !help, !refresh et !bonjour\n".format(channel)) + 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)) + + # Reads the messages from the server and adds them to the Queue and prints + # them to the console. This function will be run in a thread, see below. + def msg_handler(self): # pragma: no cover (this excludes this function from testing) + new_msg = self.ircsock.recv(2048) # receive data from the server + new_msg = new_msg.strip('\n\r') # removing any unnecessary linebreaks + + if new_msg != '' and new_msg.find("PING :") == -1: + print(datetime.datetime.now().isoformat() + " " + new_msg) + 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: +# print "Wrong message:", ircmsg + return None, None, None + + # Compile regex + def get_regex(self, options): + pattern = "(" + for s in options: + pattern += s + pattern += '|' + pattern = pattern[:-1] + pattern += ")" + return pattern + + +########################## +### The main function. ### +########################## + +def main(): + print datetime.datetime.now().isoformat() + " redmine bot starting…" + redmine_bot = Bot(default_server, default_nickname) + redmine_bot.connect() + redmine_bot.add_project(Project('gdtc','#gdtc')) + redmine_bot.add_project(Project('admins','#april-admin')) + return redmine_bot.loop() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bots/redminebot/redminebot.py~ b/bots/redminebot/redminebot.py~ new file mode 100755 index 0000000..fb15ec1 --- /dev/null +++ b/bots/redminebot/redminebot.py~ @@ -0,0 +1,190 @@ +#!/usr/bin/python -u +# -*- coding: utf-8 -1 -*- + +# Import some necessary libraries. +import socket, sys, time, csv, Queue, random, re, pdb, select, os.path, datetime +from threading import Thread +import feedparser +import xml.dom.minidom +from time import mktime, localtime + +import iso8601 + + +# IRC configuration + +default_server = "irc.eu.freenode.net" +default_nickname = "agirbot" +help_list = ["help", "info", "faq", "aide"] +hello_list = ["hello", "yo", "bonjour", "salut"] + +######################### +### Class Definitions ### +######################### + +class Project(object): + def __init__(self, project, channel): + self.name = project + self.channel = channel + self.redmine_next = datetime.datetime.utcnow() + self.redmine_latest = datetime.datetime.utcnow() + + def set_ircsock ( self, ircsock ): + self.ircsock = ircsock + + def redmine(self): + t = datetime.datetime.utcnow() +# print "Running: %s (%s) for %s" % ( t, self.redmine_next, self.channel ) + if t >= self.redmine_next: + latest_new = self.redmine_latest + + self.redmine_next = self.redmine_next + datetime.timedelta(seconds=10) + redmine = feedparser.parse("http://agir.april.org/projects/%s/activity.atom?show_issues=1" % self.name) + + 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) + self.ircsock.send("PRIVMSG {0} :{1}\n".format(self.channel, msg.encode('utf-8', 'ignore'))) + + # find the new latest time + if td.replace(tzinfo=None) > latest_new: + latest_new = td.replace(tzinfo=None) + self.redmine_latest = latest_new + + +# Defines a bot +class Bot(object): + + def __init__(self, server, botnick): + self.botnick = botnick + 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 + self.projects = [ ] + + def connect(self): + self.ircsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.ircsock.connect((self.server, 6667)) + self.ircsock.send("USER {0} {0} {0} :Robot Agir April" + ".\n".format(self.botnick)) # bot authentication + self.ircsock.send("NICK {}\n".format(self.botnick)) # Assign the nick to the bot. + if os.path.isfile("password.txt"): + with open("password.txt", 'r') as f: + password = f.read() + if registered == True: + self.ircsock.send("PRIVMSG {} {} {} {}".format("NickServ","IDENTIFY", self.botnick, password)) + + def add_project(self, project): + project.set_ircsock ( self.ircsock ) + self.ircsock.send("JOIN {} \n".format(project.channel)) # Joins channel + self.projects.append(project) + + def get_project(self, name): + for project in self.projects: + if name[0] != '#' and project.name == name: + return project + elif name[0] == '#' and project.channel == name: + return project + + # Main loop + def loop(self): + while 1: # Loop forever + ready_to_read, b, c = select.select([self.ircsock],[],[], 1) + if ready_to_read: + ircmsg = self.msg_handler() + ircmsg, actor, channel = self.parse_messages(ircmsg) + if ircmsg is not None: + self.message_response(ircmsg, actor, channel) + + # Responds to server Pings. + def pong(self, ircmsg): + response = "PONG :" + ircmsg.split("PING :")[1] + "\n" + self.ircsock.send(response) + self._redmine() + + def _redmine(self): + 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. + if ( ':!' in ircmsg or self.botnick.lower() in ircmsg.lower() ) and \ + actor != self.botnick and \ + "PRIVMSG".lower() in ircmsg.lower(): + if self.hello_regex.search(ircmsg): + self.bot_hello(channel, 'Yo!') + if self.help_regex.search(ircmsg): + self.bot_help(channel) + if 'refresh' in ircmsg.lower(): + self.ircsock.send("PRIVMSG {} :Raffraîchissement en cours\n".format(channel)) + project = self.get_project(channel) + if project: + project.redmine() + self.ircsock.send("PRIVMSG {} :Fait !\n".format(channel)) + + # 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): + self.ircsock.send("PRIVMSG {0} :{1}\n".format(channel, greeting)) + + # Explains what the bot is when queried. + def bot_help(self, channel): + self.ircsock.send("PRIVMSG {} :Bonjour, je suis un bot qui reconnaît les options !help, !refresh et !bonjour\n".format(channel)) + 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)) + + # Reads the messages from the server and adds them to the Queue and prints + # them to the console. This function will be run in a thread, see below. + def msg_handler(self): # pragma: no cover (this excludes this function from testing) + new_msg = self.ircsock.recv(2048) # receive data from the server + new_msg = new_msg.strip('\n\r') # removing any unnecessary linebreaks + + if new_msg != '' and new_msg.find("PING :") == -1: + print(datetime.datetime.now().isoformat() + " " + new_msg) + 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: +# print "Wrong message:", ircmsg + return None, None, None + + # Compile regex + def get_regex(self, options): + pattern = "(" + for s in options: + pattern += s + pattern += '|' + pattern = pattern[:-1] + pattern += ")" + return pattern + + +########################## +### The main function. ### +########################## + +def main(): + print datetime.datetime.now().isoformat() + " redmine bot starting…" + redmine_bot = Bot(default_server, default_nickname) + redmine_bot.connect() + redmine_bot.add_project(Project('gdtc','#gdtc')) + redmine_bot.add_project(Project('admins','#april-admin')) + return redmine_bot.loop() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bots/redminebot/redminebot.sh b/bots/redminebot/redminebot.sh new file mode 100755 index 0000000..20ad049 --- /dev/null +++ b/bots/redminebot/redminebot.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +BINDIR="`dirname "$0"`" +HOMEDIR="$BINDIR" + +cd "$BINDIR" + +nohup $BINDIR/redminebot.py >>"$HOMEDIR/redminebot.log" 2>&1 & diff --git a/bots/redminebot/redminebot.sh~ b/bots/redminebot/redminebot.sh~ new file mode 100755 index 0000000..5b12799 --- /dev/null +++ b/bots/redminebot/redminebot.sh~ @@ -0,0 +1,11 @@ +#!/bin/sh + +BINDIR="`dirname "$0"`" +HOMEDIR="$BINDIR" + +echo "BINDIR=$BINDIR" +echo "HOMEDIR=$HOMEDIR" + +cd "$BINDIR" + +nohup $BINDIR/redminebot.py >>"$HOMEDIR/redminebot.log" 2>&1 &