add a logger

This commit is contained in:
Romain H 2022-02-27 15:15:18 +01:00 committed by Romain Hv
parent d9894fbd87
commit 93ab327955

View File

@ -6,11 +6,16 @@ import re
import select import select
import os.path import os.path
import configparser import configparser
import logging
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Generator from typing import Generator, Tuple, List
import feedparser import feedparser
LOGGER = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO,
format="%(asctime)s %(levelname)s %(threadName)s %(name)s %(message)s")
# Help configuration. # Help configuration.
HELP_LIST = ["help", "faq", "aide"] HELP_LIST = ["help", "faq", "aide"]
HELLO_LIST = ["hello", "yo", "bonjour", "salut"] HELLO_LIST = ["hello", "yo", "bonjour", "salut"]
@ -21,7 +26,7 @@ if len(sys.argv) == 2:
CONFIG_FILENAME = sys.argv[1] CONFIG_FILENAME = sys.argv[1]
if os.path.isfile(CONFIG_FILENAME): if os.path.isfile(CONFIG_FILENAME):
print(f"Using configuration file: {CONFIG_FILENAME}") LOGGER.info('Using configuration file: %s', CONFIG_FILENAME)
CONFIG = configparser.ConfigParser() CONFIG = configparser.ConfigParser()
CONFIG.read(CONFIG_FILENAME) CONFIG.read(CONFIG_FILENAME)
@ -38,7 +43,7 @@ if os.path.isfile(CONFIG_FILENAME):
PROJECT_CHANNEL_3 = CONFIG.get("IRCSection", "irc.projects.3.channel") PROJECT_CHANNEL_3 = CONFIG.get("IRCSection", "irc.projects.3.channel")
else: else:
print("Missing configuration file.") LOGGER.error("Missing configuration file.")
sys.exit() sys.exit()
@ -50,7 +55,7 @@ class Project:
self.redmine_next = datetime.now(tz=timezone.utc) self.redmine_next = datetime.now(tz=timezone.utc)
self.redmine_latest = datetime.now(tz=timezone.utc) self.redmine_latest = datetime.now(tz=timezone.utc)
def redmine(self) -> Generator[str, None, None]: def generate_messages(self) -> Generator[str, None, None]:
now = datetime.now(tz=timezone.utc) now = datetime.now(tz=timezone.utc)
if now < self.redmine_next: if now < self.redmine_next:
return return
@ -73,7 +78,7 @@ class Project:
self.redmine_latest = latest_new self.redmine_latest = latest_new
def generate_command_regex(options): def generate_command_regex(options: List[str]) -> str:
pattern = "(" pattern = "("
for option in options: for option in options:
pattern += option pattern += option
@ -83,19 +88,17 @@ def generate_command_regex(options):
return pattern return pattern
def parse_irc_messages(irc_msg): def parse_irc_messages(irc_msg: str) -> Tuple[str, str, str]:
try: try:
actor = irc_msg.split(":")[1].split("!")[0] actor = irc_msg.split(":")[1].split("!")[0]
try: try:
target = irc_msg.split(":")[1].split(" ")[2] target = irc_msg.split(":")[1].split(" ")[2]
except Exception as exc: except Exception:
print("Exception raised to irc message parsing:") LOGGER.exception("Exception raised to irc message parsing")
print(exc)
target = None target = None
return " ".join(irc_msg.split()), actor, target return " ".join(irc_msg.split()), actor, target
except Exception as exc: except Exception:
print("Exception raised to irc message parsing:") LOGGER.exception("Exception raised to irc message parsing")
print(exc)
return None, None, None return None, None, None
@ -106,7 +109,7 @@ def parse_redmine_datetime(redmine_date: str) -> datetime:
class Bot: class Bot:
def __init__(self, server, bot_nick): def __init__(self, server: str, bot_nick: str):
self.bot_nick = bot_nick self.bot_nick = bot_nick
self.hello_regex = re.compile(generate_command_regex(HELLO_LIST), re.I) self.hello_regex = re.compile(generate_command_regex(HELLO_LIST), re.I)
self.help_regex = re.compile(generate_command_regex(HELP_LIST), re.I) self.help_regex = re.compile(generate_command_regex(HELP_LIST), re.I)
@ -114,25 +117,28 @@ class Bot:
self.projects = [] self.projects = []
self.irc_socket = None self.irc_socket = None
def send(self, msg: str): def send_raw(self, msg: str) -> None:
self.irc_socket.send(msg.encode("utf-8", "ignore")) self.irc_socket.send(msg.encode("utf-8", "ignore"))
def connect(self): def send_privmsg(self, channel: str, message: str) -> None:
self.send_raw(f"PRIVMSG {channel} :{message}\n")
def connect(self) -> None:
self.irc_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.irc_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.irc_socket.connect((self.server, 6667)) self.irc_socket.connect((self.server, 6667))
self.send(f"USER {self.bot_nick} {self.bot_nick} {self.bot_nick} :Robot Agir April" self.send_raw(f"USER {self.bot_nick} {self.bot_nick} {self.bot_nick} :Robot Agir April"
".\n") # bot authentication f".\n") # bot authentication
self.send(f"NICK {self.bot_nick}\n") # Assign the nick to the bot. self.send_raw(f"NICK {self.bot_nick}\n") # Assign the nick to the bot.
if not PASSWORD.strip() and REGISTERED is True: if not PASSWORD.strip() and REGISTERED is True:
self.send("PRIVMSG NickServ :IDENTIFY {self.botnick} {password}") self.send_privmsg("NickServ", f"IDENTIFY {self.bot_nick} {PASSWORD}")
def add_project(self, project): def add_project(self, project: str) -> None:
self.send("JOIN {} \n".format(project.channel)) # Joins channel self.send_raw("JOIN {} \n".format(project.channel)) # Joins channel
self.projects.append(project) self.projects.append(project)
def get_project(self, name): def get_project(self, name: str) -> str:
for project in self.projects: for project in self.projects:
if name[0] != '#' and project.name == name: if name[0] != '#' and project.name == name:
return project return project
@ -141,7 +147,7 @@ class Bot:
return None return None
# Main loop # Main loop
def loop(self): def loop_forever(self) -> None:
last_read = datetime.utcnow() last_read = datetime.utcnow()
while True: # Loop forever while True: # Loop forever
@ -159,32 +165,32 @@ class Bot:
raise Exception('timeout: nothing to read on socket since 10 minutes') raise Exception('timeout: nothing to read on socket since 10 minutes')
# Responds to server Pings. # Responds to server Pings.
def pong(self, irc_msg): def pong(self, irc_msg: str) -> None:
response = f"PONG :{irc_msg.split('PING :')[1]}\n" response = f"PONG :{irc_msg.split('PING :')[1]}\n"
self.send(response) self.send_raw(response)
self._check_global_updates() self._check_global_updates()
def _check_global_updates(self): def _check_global_updates(self) -> None:
print('Checking projects updates....') LOGGER.info("Checking all projects updates...")
for project in self.projects: for project in self.projects:
for message in project.redmine(): for message in project.generate_messages():
self.send(f"PRIVMSG {project.channel} :{message}\n") self.send_privmsg(project.channel, message)
def _check_project_updates(self, channel_name: str): def _check_project_updates(self, channel_name: str) -> None:
print('Checking projects updates....') LOGGER.info("Checking project of channel %s updates...", channel_name)
project = self.get_project(channel_name) project = self.get_project(channel_name)
if not project: if not project:
print(f'Cannot find project for channel {channel_name}') LOGGER.error("Cannot find project for channel %s", channel_name)
return return
for message in project.redmine(): for message in project.redmine():
self.send(f"PRIVMSG {project.channel} :{message}\n") self.send_privmsg(project.channel, message)
# Parses messages and responds to them appropriately. # Parses messages and responds to them appropriately.
def message_response(self, irc_msg, actor, channel): def message_response(self, irc_msg: str, actor: str, channel: str) -> None:
# If someone talks to (or refers to) the bot. # If someone talks to (or refers to) the bot.
if (':!' in irc_msg or self.bot_nick.lower() in irc_msg.lower()) and \ if (':!' in irc_msg or self.bot_nick.lower() in irc_msg.lower()) and \
actor != self.bot_nick and \ actor != self.bot_nick and \
@ -194,41 +200,46 @@ class Bot:
if self.help_regex.search(irc_msg): if self.help_regex.search(irc_msg):
self.bot_help(channel) self.bot_help(channel)
if 'refresh' in irc_msg.lower(): if 'refresh' in irc_msg.lower():
self.send(f"PRIVMSG {channel} :Raffraîchissement en cours\n") self.send_privmsg(channel, "Raffraîchissement en cours")
self._check_project_updates(channel) self._check_project_updates(channel)
self.send(f"PRIVMSG {channel} :Fait !\n") self.send_privmsg(channel, "Fait !")
# If the server pings us then we've got to respond! # If the server pings us then we've got to respond!
if irc_msg.find("PING :") != -1: if irc_msg.find("PING :") != -1:
self.pong(irc_msg) self.pong(irc_msg)
# Responds to a user that inputs "Hello Mybot". # Responds to a user that inputs "Hello Mybot".
def bot_hello(self, channel, greeting): def bot_hello(self, channel: str, greeting: str) -> None:
self.send(f"PRIVMSG {channel} :{greeting}\n") self.send_privmsg(channel, greeting)
# Explains what the bot is when queried. # Explains what the bot is when queried.
def bot_help(self, channel): def bot_help(self, channel: str) -> None:
self.send(f"PRIVMSG {channel} :Bonjour, je suis un bot qui reconnaît les options !help, !refresh et !bonjour\n") self.send_privmsg(channel, "Bonjour, je suis un bot qui reconnaît les options !help, !refresh et !bonjour")
project_name = self.get_project(channel).name project = self.get_project(channel)
self.send(f"PRIVMSG {channel} :Périodiquement, je vais afficher les actualités de "
f"http://agir.april.org/projects/{project_name}.\n") if not project:
LOGGER.warning("Project of channel %s not found", channel)
return
self.send_privmsg(channel, f"Périodiquement, je vais afficher les actualités "
f"de http://agir.april.org/projects/{project.name}.")
# Reads the messages from the server and adds them to the Queue and prints # 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. # 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) def msg_handler(self) -> str: # pragma: no cover (this excludes this function from testing)
new_msg = self.irc_socket.recv(2048) # receive data from the server new_msg = self.irc_socket.recv(2048) # receive data from the server
if not new_msg: if not new_msg:
print("Empty recv. It seems Ive lost my mind. I stop to be reborn.") LOGGER.error("Empty recv. It seems Ive lost my mind. I stop to be reborn.")
sys.exit(7) sys.exit(7)
else: else:
new_msg = new_msg.decode("utf-8").strip('\n\r') # removing any unnecessary linebreaks new_msg = new_msg.decode("utf-8").strip('\n\r') # removing any unnecessary linebreaks
if new_msg != '' and new_msg.find("PING :") == -1: if new_msg != '' and new_msg.find("PING :") == -1:
print(datetime.now().isoformat() + " " + new_msg) LOGGER.debug(new_msg)
return new_msg return new_msg
def main(): def main() -> None:
print(datetime.now().isoformat() + " redmine bot starting…") LOGGER.info("redmine bot starting…")
redmine_bot = Bot(DEFAULT_SERVER, DEFAULT_NICKNAME) redmine_bot = Bot(DEFAULT_SERVER, DEFAULT_NICKNAME)
redmine_bot.connect() redmine_bot.connect()
if PROJECT_ID_1 and PROJECT_CHANNEL_1: if PROJECT_ID_1 and PROJECT_CHANNEL_1:
@ -237,8 +248,8 @@ def main():
redmine_bot.add_project(Project(PROJECT_ID_2, PROJECT_CHANNEL_2)) redmine_bot.add_project(Project(PROJECT_ID_2, PROJECT_CHANNEL_2))
if PROJECT_ID_3 and PROJECT_CHANNEL_3: if PROJECT_ID_3 and PROJECT_CHANNEL_3:
redmine_bot.add_project(Project(PROJECT_ID_3, PROJECT_CHANNEL_3)) redmine_bot.add_project(Project(PROJECT_ID_3, PROJECT_CHANNEL_3))
return redmine_bot.loop() redmine_bot.loop_forever()
if __name__ == "__main__": if __name__ == "__main__":
sys.exit(main()) main()