diff --git a/.gitignore b/.gitignore index d9cfdd9..d061b2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,195 @@ -token +config.ini log *.pyc *.db +.idea +.vscode + +# Created by https://www.gitignore.io/api/linux,emacs,python + +### Emacs ### +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile +projectile-bookmarks.eld + +# directory configuration +.dir-locals.el + +# saveplace +places + +# url cache +url/cache/ + +# cedet +ede-projects.el + +# smex +smex-items + +# company-statistics +company-statistics-cache.el + +# anaconda-mode +anaconda-mode/ + +### Linux ### + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +bin/ +include/ +pip-selfcheck.json + diff --git a/README.md b/README.md index 431e59d..cf93bd0 100644 --- a/README.md +++ b/README.md @@ -7,4 +7,4 @@ Es necesario tener el archivo "token" para enviar mensajes con el bot. Es necesario instalar la librería [python-telegram-bot](https://github.com/leandrotoledo/python-telegram-bot), junto con algunos paquetes adicionales: # apt-get install python-libxml2 - # pip install python-telegram-bot pyquery cssselect lxml + # pip install python-telegram-bot pyquery cssselect lxml emoji diff --git a/auxilliary_methods.py b/auxilliary_methods.py new file mode 100644 index 0000000..fb91298 --- /dev/null +++ b/auxilliary_methods.py @@ -0,0 +1,112 @@ +#!/usr/bin/python3.5 +# -*- coding: utf-8 -*- + +from urllib.request import urlopen +from pyquery import PyQuery +import telegram +from repository import find_user_by_telegram_user_id, check_user_permission +from emoji import emojize +from datetime import datetime +import time +import schedule +import threading + + +def get_who(): + + while True: + try: + url = 'https://sugus.eii.us.es/en_sugus.html' + #html = AssertionErrorurlopen(url).read() + html = urlopen(url).read() + pq = PyQuery(html) + break + except: + raise + + ul = pq('ul.usuarios > li') + who = [w.text() for w in ul.items() if w.text() != "Parece que no hay nadie."] + + return who + + +def check_type_and_text_start(aText = None, + aUName = None, + cText = None, + aType = None, + cType = None, + cUId = None, + perm_required=None): + # Si perm_required es None y cUId no es None entonces se busca que el usuario esté en cualquier grupo + + result = True + + if cType is not None: + result = aType == cType + if cUId is not None: + if perm_required is None: # Comprobar solo usuario y permiso + result = result and find_user_by_telegram_user_id(cUId) + else: # Comprobar usuario y permisos + for a in perm_required: + if check_user_permission(cUId, a): + result = result and True + if cText is not None: + result = result and aText.startswith(cText) + + return result + +def check_permissions(user_id, required_perm): + return all([check_user_permission(user_id, perm) for perm in required_perm]) + +def show_list(header, contains, positions = None): + + result = [header + "\n"] + + if contains: + if positions: + for a in contains: + a_ordered = [str(a[i]) for i in positions] + result.append("{} {}\n".format(emojize(":small_blue_diamond:", use_aliases=True), " ".join(a_ordered))) + else: + if isinstance(contains[0], str): + rows = ["{} {}\n".format(emojize(":small_blue_diamond:", use_aliases=True) ,a) for a in contains] + else: + rows = ["{} {}\n".format(emojize(":small_blue_diamond:", use_aliases=True) ," ".join(a)) for a in contains] + result += rows + + return "".join(result) + + +def check_date(date): + res = True + + try: + if datetime.strptime(date, '%d-%m-%Y').date() < datetime.today().date(): + res = False + except ValueError: + res = False + return res + +def extract_user_from_update(update): + try: + user = update.callback_query.from_user + except AttributeError: + user = update.message.from_user + + return user + + +def init_scheduler(): + cease_continuous_run = threading.Event() + + class ScheduleThread(threading.Thread): + @classmethod + def run(cls): + while not cease_continuous_run.is_set(): + schedule.run_pending() + time.sleep(1) + + continuous_thread = ScheduleThread() + continuous_thread.start() + + return cease_continuous_run diff --git a/config_sample.ini b/config_sample.ini new file mode 100644 index 0000000..bf70679 --- /dev/null +++ b/config_sample.ini @@ -0,0 +1,5 @@ +[Database] +route = sugusBotDB.db +[Telegram] +token = XXXX +id_admin = XXXX diff --git a/conversation_session.py b/conversation_session.py new file mode 100644 index 0000000..fdeefdb --- /dev/null +++ b/conversation_session.py @@ -0,0 +1,67 @@ +#!/usr/bin/python3.5 +# -*- coding: utf-8 -*- + + +class conversation_session(): + def __init__(self): + self.__inConversation = {} + + @staticmethod + def __g_key(act_user, conversation_id=None): + # Generate the key based on actUser and conversationId + if conversation_id is None: + conversation_id = act_user.id + + return str(act_user.id) + "-" + str(conversation_id) + + def __add(self, key, options): + self.__inConversation[key] = options + + def add_option(self, act_user, option, value, conversation_id=None): + """Añade el par de valores (option, value) para el usuario""" + key = self.__g_key(act_user, conversation_id) + if not self.get(act_user): + self.__inConversation[key] = {option: value} + else: + self.__inConversation[key][option] = value + + def del_option(self, act_user, option, conversation_id=None): + """Elimina la opción 'option' del usuario""" + key = self.__g_key(act_user, conversation_id) + del self.__inConversation[key][option] + + def empty(self, act_user, conversation_id=None): + """Elimina todas las opciones para el usuario""" + key = self.__g_key(act_user, conversation_id) + del self.__inConversation[key] + + def get(self, act_user, conversation_id=None): + """Devuelve todas las opciones del usuario""" + key = self.__g_key(act_user, conversation_id) + try: + result = self.__inConversation[key] + except KeyError as error: + result = [] + pass + return result + + def contain_option(self, act_user, option, conversation_id=None, c_opt_value=None): + """Comprueba si existe la opción 'option' para el usuario""" + options = self.get(act_user, conversation_id) + if option in options: + if c_opt_value == options[option]: + return True + elif c_opt_value == None: + return True + else: + return False + else: + return False + + def status(self, act_user, conversation_id=None): + """Dice si el usuario tiene alguna sesión en curso""" + key = self.__g_key(act_user, conversation_id) + if key in self.__inConversation: + return True + else: + return False diff --git a/handlers.py b/handlers.py new file mode 100644 index 0000000..16beec6 --- /dev/null +++ b/handlers.py @@ -0,0 +1,416 @@ +#!/usr/bin/python3.6 +# -*- coding: utf-8 -*- + +import repository +import telegram +from emoji import emojize +import auxilliary_methods +import logging + +""" +This module contains the functions that handlers execute. +""" + + +# Help functions +def help_eat(): + header = "Elige una de las opciones: " + contain = [['/help', 'Ayuda']] + contain = contain + [['/como', 'Yo como aquí'], + ['/nocomo', 'Yo no como aquí'], + ['/quiencome', '¿Quien come aquí?']] + return auxilliary_methods.show_list(header, contain) + + +def help_event(): + header = "Elige una de las opciones: " + contain = [['/help', 'Ayuda']] + contain += [['/events', 'Listar eventos'], + ['/addevent', 'Añadir un evento'], + ['/removeevent', 'Eliminar un evento']] + contain += [['/leaveevent', 'Abandonar un evento'], + ['/jointoevent', 'Unirte a un evento'], + ['/participants', 'Listar participantes']] + return auxilliary_methods.show_list(header, contain) + + +def help_group(): + header = "Elige una de las opciones: " + contain = [['/help', 'Ayuda'], ['/groups', 'Listar grupos'], + ['/addgroup', 'Añadir un grupo']] + contain += [['/addtogroup', 'Añadir a alguien a un grupo'], + ['/delfromgroup', 'Sacar a alguien de un grupo']] + return auxilliary_methods.show_list(header, contain) + + +# Command handlers definitions +def start(bot, update): + user_id = update.effective_user.id + user_name = update.effective_user.username + send_text = repository.update_user(user_id, user_name) + update.message.reply_text('¡Hola! Soy @SugusBot, escribe "/help" para ver' + ' la lista de comandos disponibles.') + + +def help(bot, update): + header = "Elige una de las opciones: " + contain = [['/help', 'Ayuda'], ['/who', '¿Quien hay en Sugus?'], + ['/comida', 'Opciones de comida']] + contain = contain + [['/group', 'Opciones de permisos']] + contain = contain + [['/event', 'Opciones de eventos']] + send_text = auxilliary_methods.show_list(header, contain) + if update.message: + update.message.reply_text(send_text) + elif update.callback_query: + update.callback_query.message.reply_text(send_text) + id = update.callback_query.id + bot.answerCallbackQuery(id) + +def who(bot, update): + actText = update.message.text + actType = update.message.chat.type + + if auxilliary_methods.check_type_and_text_start(aText=actText, + cText='/who', + aType=actType, + cType='private'): + max_retry = 2 + for i in range(max_retry): + try: + who = auxilliary_methods.get_who() + if not who: + send_text = u"Parece que no hay nadie... {}".format( + emojize(":disappointed_face:", use_aliases=True)) + else: + send_text = auxilliary_methods.show_list( + u"Miembros en SUGUS:", who) + break + except Exception as e: + if i is max_retry - 1: + print("Hubo un error repetitivo al intentar conectar al " + "servidor: ", e) + send_text = u"Hubo algún error al realizar la petición" + \ + u" a la web de sugus" + + if send_text is not None: + update.message.reply_text(send_text) + else: + update.message.reply_text(help()) + + +def como(bot, update): + user_id = auxilliary_methods.extract_user_from_update(update).id + user_name = update.effective_user.username + repository.update_user(user_id, user_name) + send_text = repository.add_to_event('comida', user_id) + update.callback_query.message.reply_text(send_text) + id = update.callback_query.id + bot.answerCallbackQuery(id) + + +def no_como(bot, update): + user_id = auxilliary_methods.extract_user_from_update(update).id + send_text = repository.remove_from_event('comida', user_id) + update.callback_query.message.reply_text(send_text) + id = update.callback_query.id + bot.answerCallbackQuery(id) + +def quien_come(bot, update): + quiencome = repository.find_users_by_event('comida') + if quiencome: + send_text = auxilliary_methods.show_list("Hoy come en Sugus:", + quiencome, [2]) + else: + send_text = 'De momento nadie come en Sugus' + update.callback_query.message.reply_text(send_text) + id = update.callback_query.id + bot.answerCallbackQuery(id) + + +def comida(bot, update): + actText = update.message.text + actType = update.message.chat.type + + comida_btns = [[telegram.InlineKeyboardButton('Help', callback_data='help')], + [telegram.InlineKeyboardButton('Como', callback_data='como')], + [telegram.InlineKeyboardButton('No Como', callback_data='no_como')], + [telegram.InlineKeyboardButton('Quién come?', callback_data='quien_come')]] + + if auxilliary_methods.check_type_and_text_start(aText=actText, + cText='/comida', + aType=actType, + cType='private'): + reply_markup = telegram.InlineKeyboardMarkup(comida_btns) + update.message.reply_text('Elige una de las opciones:', reply_markup=reply_markup) + + +def group(bot, update): + actText = update.message.text + actType = update.message.chat.type + + if auxilliary_methods.check_type_and_text_start(aText=actText, + cText='/group', + aType=actType, + cType='private'): + send_text = help_eat() + + if send_text is not None: + update.message.reply_text(send_text) + else: + update.message.reply_text(help()) + + +def add_group(bot, update): + actText = update.message.text + actType = update.message.chat.type + + if auxilliary_methods.check_type_and_text_start(aText=actText, + cText='/addgroup', + aType=actType, + cType='private', + cUId=update.message. + from_user.id, + perm_required=["admin"]): + rtext = actText.replace('/addgroup ', '').replace('/addgroup', '') + send_text = repository.add_permission_group(rtext) + + if send_text is not None: + update.message.reply_text(send_text) + else: + update.message.reply_text(help()) + + +def add_to_group(bot, update): + actText = update.message.text + actType = update.message.chat.type + + if auxilliary_methods.check_type_and_text_start(aText=actText, + cText='/addtogroup', + aType=actType, + cType='private', + cUId=update.message. + from_user.id, + perm_required=["admin", + "sugus"]): + rtext = actText.replace('/addtogroup ', '').replace('/addtogroup', '')\ + .split(" ") + db_user = repository.find_user_by_telegram_user_name(rtext[0]) + + if len(rtext) != 2: + send_text = "Formato incorrecto. El formato debe ser: \n" + \ + "'/addtogroup @username group_name'" + elif not db_user: + send_text = "Nombre de usuario '" + rtext[0] + \ + "' no encontrado en la base de datos" + else: + send_text = repository.add_user_permission(db_user[1], rtext[1]) + + if send_text is not None: + update.message.reply_text(send_text) + else: + update.message.reply_text(help()) + + +def del_from_group(bot, update): + actText = update.message.text + actType = update.message.chat.type + + if auxilliary_methods.check_type_and_text_start(aText=actText, + cText='/delfromgroup', + aType=actType, + cType='private', + cUId=update.message. + from_user.id, + perm_required=["admin", + "sugus"]): + rtext = actText.split(" ") + + if len(rtext) != 3: + send_text = "Has introducido el comando de manera incorrecta." + \ + "El formato debe ser:\n'/delfromgroup @usermane " + \ + "groupname'" + else: + user = repository.find_user_by_telegram_user_name(rtext[1]) + send_text = repository.remove_from_group(user[1], rtext[2]) + + if send_text is not None: + update.message.reply_text(send_text) + else: + update.message.reply_text(help()) + + +def groups(bot, update): + actText = update.message.text + actType = update.message.chat.type + + if auxilliary_methods.check_type_and_text_start(aText=actText, + cText='/groups', + aType=actType, + cType='private', + cUId=update.message. + from_user.id): + send_text = auxilliary_methods.show_list( + u"Grupos de permisos disponibles:", + repository.list_permission_group()) + + if send_text is not None: + update.message.reply_text(send_text) + else: + update.message.reply_text(help()) + + +def event(bot, update): + actText = update.message.text + actType = update.message.chat.type + + if auxilliary_methods.check_type_and_text_start(aText=actText, + cText='/event', + aType=actType, + cType='private'): + send_text = help_event() + + if send_text is not None: + update.message.reply_text(send_text) + else: + update.message.reply_text(help()) + + +def events(bot, update): + actText = update.message.text + actType = update.message.chat.type + + if auxilliary_methods.check_type_and_text_start(aText=actText, + cText='/events', + aType=actType, + cType='private'): + send_text = auxilliary_methods.show_list( + u"Elige una de las listas:", repository.list_events(), + [0, 1]) + + if send_text is not None: + update.message.reply_text(send_text) + else: + update.message.reply_text(help()) + + +def add_event(bot, update): + actText = update.message.text + actType = update.message.chat.type + + if auxilliary_methods.check_type_and_text_start(aText=actText, + cText='/addevent', + aType=actType, + cType='private', + cUId=update.message. + from_user.id, + perm_required=["admin", + "sugus"]): + rtext = actText.replace('/addevent ', '').replace('/addevent', '')\ + .split(" ") + if len(rtext) < 2: + send_text = "Formato incorrecto. El formato debe ser: \n " + \ + "'/addevent nombre-evento dd-mm-aaaa'" + elif not auxilliary_methods.check_date(rtext[len(rtext) - 1]): + send_text = "Formato de fecha incorrecto ('dd-mm-aaaa') " + \ + "o la fecha ya ha pasado" + else: + event_name = ' '.join([str(x) for x in rtext[0:len(rtext) - 1]]) + send_text = repository.add_event(event_name, rtext[len(rtext) - 1], + update.message.from_user.id) + + if send_text is not None: + update.message.reply_text(send_text) + else: + update.message.reply_text(help()) + + +def remove_event(bot, update): + user_id = update.effective_user.id + if not auxilliary_methods.check_permissions(user_id, ['admin']): + update.message.reply_text('No tienes permiso') + return + elif update.message: + event_btns = [] + for name in repository.list_events(): + btn = telegram.InlineKeyboardButton(str(name[0]), + callback_data = 'remove_event.'+str(name[0])) + event_btns.append([btn]) + reply_markup = telegram.InlineKeyboardMarkup(event_btns) + update.message.reply_text('Elige una de las opciones:', reply_markup=reply_markup) + return + elif update.callback_query: + event_name = update.callback_query.data.split('.')[1] + repository.remove_event(event_name) + send_text = 'Has eliminado el evento ' + event_name + update.callback_query.message.reply_text(send_text) + id = update.callback_query.id + bot.answerCallbackQuery(id) + return + + +def join_to_event(bot, update): + if update.message: + event_btns = [] + for name in repository.list_events(): + btn = telegram.InlineKeyboardButton(str(name[0]), + callback_data = 'join_event.'+str(name[0])) + event_btns.append([btn]) + reply_markup = telegram.InlineKeyboardMarkup(event_btns) + update.message.reply_text('Elige una de las opciones:', reply_markup=reply_markup) + return + elif update.callback_query: + event_name = update.callback_query.data.split('.')[1] + user_id = update.callback_query.from_user.id + user_name = update.effective_user.username + repository.update_user(user_id, user_name) + repository.add_to_event(event_name, user_id) + update.callback_query.message.reply_text("Te has unido a " + event_name) + id = update.callback_query.id + bot.answerCallbackQuery(id) + return + +def participants(bot, update): + if update.message: + event_btns = [] + for name in repository.list_events(): + btn = telegram.InlineKeyboardButton(str(name[0]), + callback_data = 'participants.'+str(name[0])) + event_btns.append([btn]) + reply_markup = telegram.InlineKeyboardMarkup(event_btns) + update.message.reply_text('Elige una de los eventos:', reply_markup=reply_markup) + return + elif update.callback_query: + event_name = update.callback_query.data.split('.')[1] + user_id = update.callback_query.from_user.id + repository.add_to_event(event_name, user_id) + send_text = auxilliary_methods.show_list( + 'Participantes en ' + event_name, + repository.find_users_by_event(event_name), [2]) + update.callback_query.message.reply_text(send_text) + id = update.callback_query.id + bot.answerCallbackQuery(id) + return + + +def leave_event(bot, update): + if update.message: + event_btns = [] + for name in repository.list_events(): + btn = telegram.InlineKeyboardButton(str(name[0]), + callback_data = 'leave_event.'+str(name[0])) + event_btns.append([btn]) + reply_markup = telegram.InlineKeyboardMarkup(event_btns) + update.message.reply_text('Elige una de las opciones:', reply_markup=reply_markup) + return + elif update.callback_query: + event_name = update.callback_query.data.split('.')[1] + user_id = update.callback_query.from_user.id + repository.remove_from_event(event_name, user_id) + update.callback_query.message.reply_text("Has abandonado el evento " + event_name) + id = update.callback_query.id + bot.answerCallbackQuery(id) + return + + +def error(bot, update, error): + logging.warn('Update "%s" caused error "%s"' % (update, error)) diff --git a/messaging.py b/messaging.py new file mode 100644 index 0000000..e90ce8a --- /dev/null +++ b/messaging.py @@ -0,0 +1,57 @@ +#!/usr/bin/python3.5 +# -*- coding: utf-8 -*- + +import telegram +from urllib.error import URLError +import time + +bot = None + + +def create_bot(token): + # Create bot object + global bot + bot = telegram.Bot(token) + + +def getUpdates(LAST_UPDATE_ID, timeout = 30): + while True: + try: + updates = bot.getUpdates(LAST_UPDATE_ID, timeout=timeout, network_delay=2.0) + except telegram.TelegramError as error: + if error.message == "Timed out": + print(u"Timed out! Retrying...") + elif error.message == "Bad Gateway": + print("Bad gateway. Retrying...") + else: + raise + + except URLError as error: + print("URLError! Retrying...") + time.sleep(1) + except Exception as e: + print("Exception: " + e) + print('Ignore errors') + pass + else: + break + return updates + +def sendMessages(send_text, chat_id): + while True: + try: + bot.sendMessage(chat_id=chat_id, text=send_text) + print("Mensaje enviado a id: " + str(chat_id)) + break + except telegram.TelegramError as error: + if error.message == "Timed out": + print("Timed out! Retrying...") + else: + print(error) + except URLError as error: + print("URLError! Retrying to send message...") + time.sleep(1) + except Exception as e: + print("Exception: " + e) + print('Ignore exception') + pass diff --git a/repository.py b/repository.py new file mode 100644 index 0000000..e244b25 --- /dev/null +++ b/repository.py @@ -0,0 +1,326 @@ +#!/usr/bin/python3.5 +# -*- coding: utf-8 -*- + +import sqlite3 +from datetime import datetime +import logging + +conn = None +user_cache = list() +user_cache_last_update = None +db = None + + +def connection(database): + global conn, db + conn = sqlite3.connect(database) + db = database + + +def check_connection(): + global conn + if conn is None: + connection(db) + + +def sec_init(id_admin): + global conn + c = conn.cursor() + c.execute('CREATE TABLE IF NOT EXISTS event_table(id_event INTEGER PRIMARY KEY, date TEXT, name TEXT, creator TEXT, UNIQUE(date, name))') + c.execute('CREATE TABLE IF NOT EXISTS userTable(id_user INTEGER PRIMARY KEY, id_user_telegram NUMBER UNIQUE, user_name text UNIQUE)') + c.execute('CREATE TABLE IF NOT EXISTS rel_user_event(user INTEGER, event INTEGER, date TEXT, FOREIGN KEY(user) REFERENCES userTable(id_user), FOREIGN KEY(event) REFERENCES event_table(id_event), UNIQUE(user, event) ON CONFLICT REPLACE)') + c.execute('CREATE TABLE IF NOT EXISTS permissionTable(id_permission INTEGER PRIMARY KEY, permission TEXT, UNIQUE(permission))') + c.execute('CREATE TABLE IF NOT EXISTS rel_user_permission(user INTEGER, permission INTEGER, FOREIGN KEY(user) REFERENCES userTable(id_user), FOREIGN KEY(permission) REFERENCES permissionTable(id_permission))') + + if not c.execute('SELECT COUNT(*) FROM permissionTable').fetchone()[0]: + c.executemany('INSERT INTO permissionTable(permission) VALUES (?)', [('admin',), ('sugus',)]) + c.execute('INSERT INTO userTable(id_user_telegram) VALUES (?)', (id_admin,)) + #es necesario? + permission = c.execute('SELECT id_permission FROM permissionTable WHERE permission = ?', ('admin',)).fetchone()[0] + c.execute('INSERT INTO rel_user_permission VALUES (?, ?)', (1, permission)) + + if not c.execute('SELECT COUNT(*) FROM event_table').fetchone()[0]: + c.execute('INSERT INTO event_table(date, name) VALUES (?, ?)', ("", "comida")) + + conn.commit() + c.close() + conn = None + + +def add_event(event_name, event_date, creator): + check_connection() + + if not find_event_by_name(event_name): + c = conn.cursor() + c.execute('INSERT INTO event_table(date, name, creator) VALUES (?, ?, ?)', (event_date, event_name, creator)) + conn.commit() + c.close() + return "Evento " + event_name + " creado" + else: + return "El evento " + event_name + " ya existe" + + +def add_to_event(event_name, user_id): + check_connection() + user = find_user_by_telegram_user_id(telegram_user_id=user_id) + event = find_event_by_name(event_name=event_name) + + if event and user: + c = conn.cursor() + date = datetime.now().strftime("%d-%m-%y %H:%M:%S") + + c.execute('insert into rel_user_event values(?, ?, ?)', (user[0], event[0], date)) + conn.commit() + c.close() + result = "Añadido a " + event_name + + elif not user: + result = "No estás registrado en el sistema" + else: + result = "Evento no encontrado" + + return result + + +def find_event_by_name(event_name): + check_connection() + c = conn.cursor() + h = c.execute('select * from event_table where name=?', (event_name,)).fetchone() + + c.close() + + return h + + +def find_users_by_event(event_name): + check_connection() + event = find_event_by_name(event_name=event_name) + + if event: + c = conn.cursor() + + result = c.execute('SELECT * FROM userTable INNER JOIN rel_user_event ON userTable.id_user = rel_user_event.user where rel_user_event.event = ?', (event[0],)).fetchall() + + c.close() + else: + result = "Evento no encontrado" + + return result + + +def remove_from_event(event_name, telegram_user_id): + check_connection() + event = find_event_by_name(event_name=event_name) + user = find_user_by_telegram_user_id(telegram_user_id=telegram_user_id) + +# if any([('@' + name) in i for i in find_users_by_event(event_name)]): + if event and user: + c = conn.cursor() + + c.execute('delete from rel_user_event where event=? and user=?', (event[0], user[0])) + conn.commit() + + c.close() + result = "Has sido eliminado del evento " + event_name + else: + result = "Evento o usuario no encontrado" + + return result + + +# el evento solo lo puede vaciar un usuario con privilegios +def empty_event(event_name): + check_connection() + logging.info("Vamos a proceder a vaciar el evento: " + event_name) + event = find_event_by_name(event_name=event_name) + + if event: + c = conn.cursor() + + c.execute('DELETE FROM rel_user_event WHERE event=?', (event[0],)) + + result = "El evento " + event_name + " ha sido vaciado de usuarios" + + conn.commit() + + c.close() + else: + result = 'El evento ' + event_name + ' no existe' + return result + + +def list_events(): + check_connection() + c = conn.cursor() + h = c.execute('select distinct name, date from event_table').fetchall() + c.close() + + return h + + +#el evento solo lo puede borrar un usuario con privilegios +def remove_event(event_name): + check_connection() + event = find_event_by_name(event_name) + + if event: + empty_event(event[2]) + c = conn.cursor() + h = c.execute('DELETE FROM event_table WHERE name=?', (event_name,)) + result = "El evento " + event_name + " ha sido eliminado" + conn.commit() + c.close() + else: + result = "El evento " + event_name + " no existe" + + +def find_user_by_telegram_user_id(telegram_user_id): + check_connection() + c = conn.cursor() + h = c.execute('select * from userTable where id_user_telegram=?', (telegram_user_id,)).fetchone() + + c.close() + + return h + + +def find_user_by_telegram_user_name(telegram_user_name): + check_connection() + + if not telegram_user_name.startswith("@"): + telegram_user_name = "@" + telegram_user_name + + c = conn.cursor() + h = c.execute('select * from userTable where user_name=?', (telegram_user_name,)).fetchone() + + c.close() + + return h + + +def check_user_permission(user_id, permission): + check_connection() + c = conn.cursor() + h = c.execute('select * from userTable INNER JOIN rel_user_permission ON userTable.id_user = rel_user_permission.user INNER JOIN permissionTable ON permissionTable.id_permission = rel_user_permission.permission where userTable.id_user_telegram = ? and permissionTable.permission = ?', (user_id, permission)).fetchone() + c.close() + + return bool(h) + + +def remove_from_group(user_id, permission): + check_connection() + if check_user_permission(user_id, permission): + c = conn.cursor() + h = c.execute('DELETE from rel_user_permission where user=' + '(SELECT id_user FROM userTable where id_user_telegram=?) ' + 'and permission=(SELECT id_permission FROM permissionTable ' + 'WHERE permission=?)', (user_id, permission)) + conn.commit() + c.close() + result = "El usuario ha sido eliminado del grupo " + permission + else: + result = "El usuario no se encuentra en el grupo " + permission + return result + + +def add_permission_group(permission_name): + check_connection() + if permission_name and permission_name is not " ": + permission_name = permission_name.replace(" ", "_") + c = conn.cursor() + + c.execute('INSERT INTO permissionTable(permission) VALUES (?)', (permission_name,)) + conn.commit() + c.close() + return "Añadido grupo de permiso '" + str(permission_name) + "'" + else: + return "Grupo de permiso no válido: " + str(permission_name) + + +def list_permission_group(): + check_connection() + c = conn.cursor() + + h = c.execute('SELECT permission FROM permissionTable').fetchall() + if h: + h = [i[0] for i in h] + + c.close() + return h + + +def add_user_permission(id_user_telegram, permission): + check_connection() + c = conn.cursor() + permission = c.execute('SELECT id_permission FROM permissionTable WHERE permission = ?', (permission,)).fetchone() + ret = "El rol indicado no existe" + + if permission: + id_user = c.execute('SELECT id_user from userTable WHERE id_user_telegram = ?', (id_user_telegram,)).fetchone()[0] + c.execute('INSERT INTO rel_user_permission VALUES (?, ?)', (id_user, permission[0])) + conn.commit() + ret = "Rol añadido a usuario " + str(id_user_telegram) + + c.close() + return ret + + +def update_user(id_user_telegram, user_name, force_update=False): + check_connection() + global user_cache, user_cache_last_update + result = None + stop = False + date = datetime.now().strftime("%j_%p") + + if user_cache_last_update != date or force_update: + print("Clean user_cache") + + user_cache = list() + user_cache_last_update = date + + if id_user_telegram in user_cache: # In cache + return stop, result + else: + user = find_user_by_telegram_user_id(telegram_user_id=id_user_telegram) + + if user and user[2] == '@' + user_name: # In DB and not modified + user_cache.append(id_user_telegram) + return stop, result + elif user: # In DB modified + try: + c = conn.cursor() + c.execute('UPDATE userTable SET user_name = ? WHERE id_user_telegram = ?', + ("@" + user_name, id_user_telegram)) + + conn.commit() + c.close() + except: + result = "No he podido actualizarte en la base de datos" + return True, result + finally: + user_cache.append(int(id_user_telegram)) + return stop, result + + else: + try: + c = conn.cursor() + c.execute('INSERT INTO userTable(id_user_telegram, user_name) VALUES (?, ?)', + (id_user_telegram, "@" + user_name)) + + conn.commit() + c.close() + except: + result = "No he podido guardarte en la base de datos" + return True, result + finally: + user_cache.append(int(id_user_telegram)) + return stop, result + +def add_user(user_id, user_name): + check_connection() + c = conn.cursor() + c.execute('INSERT INTO userTable(id_user_telegram, user_name) VALUES (?, ?)', + (user_id, user_name)) + conn.commit() + c.close() + return "Registrado en el sistema '" + str(user_name) + "'" diff --git a/requirements.txt b/requirements.txt index 855ac7c..593929a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ -pyquery -python-telegram-bot +pyquery==1.3.0 +python-telegram-bot==8.1.1 cssselect -lxml +lxml==4.1.1 +emoji==0.4.5 +schedule==0.5.0 diff --git a/sugusbot.py b/sugusbot.py index 18454dc..1c75af4 100755 --- a/sugusbot.py +++ b/sugusbot.py @@ -1,288 +1,112 @@ -#!/usr/bin/python +#!/usr/bin/python3.6 # -*- coding: utf-8 -*- import logging -from urllib.request import urlopen -from urllib.error import URLError -import time -from pyquery import PyQuery -import telegram -import string +import repository +import handlers +import schedule -import codecs -import sys -import os +from configparser import ConfigParser +from telegram.ext import Updater, CommandHandler, MessageHandler, Filters, \ + CallbackQueryHandler -import sqlite3 -from datetime import datetime +from auxilliary_methods import init_scheduler +from repository import empty_event +# Init logging +logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s ' + '- %(message)s', level=logging.INFO) -token = None -conn = sqlite3.connect('sugusBotDB.db') +logger = logging.getLogger(__name__) +# Retrieve configuration from config file +config = ConfigParser() +config.read('config.ini') +database = config['Database']['route'] +token = config['Telegram']['token'] +id_admin = config['Telegram']['id_admin'] -with open('token', 'rb') as token_file: - token = token_file.readline().decode('ascii')[:-1] - -# Create bot object -bot = telegram.Bot(token) - - -def secInit(): - c = conn.cursor() - c.execute('create table if not exists eventTable(date text, event text, name text, UNIQUE(event, name) ON CONFLICT REPLACE)') - conn.commit() - c.close() +# Start database connection +repository.connection(database) def main(): + # Database initialization + repository.sec_init(id_admin) + + # EventHandler creation + updater = Updater(token) + + dispatcher = updater.dispatcher + + # Assign functions to handlers + dispatcher.add_handler(CommandHandler('start', + handlers.start)) + dispatcher.add_handler(CallbackQueryHandler(handlers.help, + pattern = 'help')) + dispatcher.add_handler(CommandHandler('help', + handlers.help)) + dispatcher.add_handler(CommandHandler('who', + handlers.who)) + dispatcher.add_handler(CallbackQueryHandler(handlers.como, + pattern = 'como')) + dispatcher.add_handler(CommandHandler('como', + handlers.como)) + dispatcher.add_handler(CallbackQueryHandler(handlers.no_como, + pattern = 'no_como')) + dispatcher.add_handler(CallbackQueryHandler(handlers.quien_come, + pattern = 'quien_come')) + dispatcher.add_handler(CommandHandler('quiencome', + handlers.quien_come)) + dispatcher.add_handler(CommandHandler('comida', + handlers.comida)) + dispatcher.add_handler(CommandHandler('group', + handlers.group)) + dispatcher.add_handler(CommandHandler('addgroup', + handlers.add_group)) + dispatcher.add_handler(CommandHandler('addtogroup', + handlers.add_to_group)) + dispatcher.add_handler(CommandHandler('delfromgroup', + handlers.del_from_group)) + dispatcher.add_handler(CommandHandler('groups', + handlers.groups)) + dispatcher.add_handler(CommandHandler('event', + handlers.event)) + dispatcher.add_handler(CommandHandler('events', + handlers.events)) + dispatcher.add_handler(CommandHandler('addevent', + handlers.add_event)) + dispatcher.add_handler(CallbackQueryHandler(handlers.remove_event, + pattern = '^remove_event.')) + dispatcher.add_handler(CommandHandler('removeevent', + handlers.remove_event)) + dispatcher.add_handler(CallbackQueryHandler(handlers.join_to_event, + pattern = '^join_event.')) + dispatcher.add_handler(CommandHandler('jointoevent', + handlers.join_to_event)) + dispatcher.add_handler(CallbackQueryHandler(handlers.participants, + pattern = '^participants.')) + dispatcher.add_handler(CommandHandler('participants', + handlers.participants)) + dispatcher.add_handler(CallbackQueryHandler(handlers.leave_event, + pattern = '^leave_event.')) + dispatcher.add_handler(CommandHandler('leaveevent', + handlers.leave_event)) + + # Error handler, for logging purposes + dispatcher.add_error_handler(handlers.error) + + # Clean event comida every day + schedule.every().day.at("0:30").do(empty_event, 'comida').tag('empty_event comida') + init_scheduler() + + # Start the bot + updater.start_polling() + + # Run the bot until you press Ctrl-C or the process receives SIGINT, + # SIGTERM or SIGABRT. This should be used most of the time, since + # start_polling() is non-blocking and will stop the bot gracefully. + updater.idle() - secInit() - - # UTF-8 console stuff thingies - sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach()) - - # Init logging - logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') - - # Discard old updates, sent before the bot was started - num_discarded = 0 - - # Get last update ID - LAST_UPDATE_ID = None - - while True: - updates = bot.getUpdates(LAST_UPDATE_ID, timeout=1, network_delay=2.0) - if updates is not None and updates: - num_discarded = num_discarded + len(updates) - LAST_UPDATE_ID = updates[-1].update_id + 1 - else: - break - - print("Discarded {} old updates".format(num_discarded)) - - # Main loop - print('Working...') - while True: - updates = getUpdates(LAST_UPDATE_ID) - - for update in updates: - message = update.message - actText = message.text - actType = message.chat.type - chat_id = message.chat.id - update_id = update.update_id - actUser = message.from_user.username - - send_text = None - - periodicCheck() - - if checkTypeAndTextStart(aText= actText, cText='/who', aType=actType, cType='private'): - who = getWho() - - if not who: - #changes in emojis in python3 telegram version - send_text = u"Parece que no hay nadie... {}".format(telegram.Emoji.DISAPPOINTED_FACE) - else: - send_text = showList(u"Miembros en SUGUS:", who) - - if checkTypeAndTextStart(aText= actText, cText='/como', aType=actType, cType='private'): - send_text = addTo('comida', actUser) - - if checkTypeAndTextStart(aText= actText, cText='/nocomo', aType=actType, cType='private'): - send_text = removeFromEvent('comida', actUser) - - if checkTypeAndTextStart(aText= actText, cText='/quiencome', aType=actType, cType='private'): - if len(findByEvent('comida')) != 0: - send_text = showList(u"Hoy come en Sugus:", findByEvent('comida'), [2, 0]) - else: - send_text = 'De momento nadie come en Sugus' - - if send_text != None: - sendMessages(send_text, chat_id) - elif checkTypeAndTextStart(aType=actType, cType='private'): - sendMessages(help(), chat_id) - else: - print("Mensaje enviado y no publicado por: "+str(actUser)) - - LAST_UPDATE_ID = update_id + 1 - - -def checkTypeAndTextStart(aText = None, aUName = None, cText = None, aType = None, cType = None, cUName = None): - - result = True - - if cType != None: - result = result and aType == cType - if cUName != None: - if aUName in cUName: - result = result and False - if cText != None: - result = result and aText.startswith(cText) - - return result - -def showList(header, contains, positions = None): - result = '{}'.format(header) - if contains != None: - for a in contains: - #changes in emojis in python3 telegram version - result = '{}\n {}'.format(result, telegram.Emoji.SMALL_BLUE_DIAMOND) - if positions != None: - for i in positions: - result = '{} {} '.format(result, a[i]) - else: - result = '{} {} '.format(result, a[:]) - return result - -def periodicCheck(): - - ## Remove periodic comida - actDate = datetime.now().strftime("%d-%m-%y") - actComida = findByEvent('comida') - - for a in actComida: - if a[0] != actDate: - removeFromEvent('comida', a[2][1:]) - -def help(): - header = "Elige una de las opciones: " - contain = [['/help', 'Ayuda'], ['/who','¿Quien hay en Sugus?'], ['/como','Yo como aquí']] - contain = contain + [['/nocomo', 'Yo no como aquí'], ['/quiencome', '¿Quien come aquí?']] - return showList(header, contain, [0, 1]) - -def getUpdates(LAST_UPDATE_ID, timeout = 30): - while True: - try: - updates = bot.getUpdates(LAST_UPDATE_ID, timeout=timeout, network_delay=2.0) - except telegram.TelegramError as error: - if error.message == "Timed out": - print(u"Timed out! Retrying...") - elif error.message == "Bad Gateway": - print("Bad gateway. Retrying...") - else: - raise - - except URLError as error: - print("URLError! Retrying...") - time.sleep(1) - except Exception as e: - print("Exception: " + e) - print('Ignore errors') - pass - else: - break - return updates - -def sendMessages(send_text, chat_id): - while True: - try: - bot.sendMessage(chat_id=chat_id, text=send_text) - print("Mensaje enviado a id: " + str(chat_id)) - break - except telegram.TelegramError as error: - if error.message == "Timed out": - print("Timed out! Retrying...") - else: - print(error) - except URLError as error: - print("URLError! Retrying to send message...") - time.sleep(1) - except Exception as e: - print("Exception: " + e) - print('Ignore exception') - pass - -def getWho(): - while True: - try: - url = 'https://sugus.eii.us.es/en_sugus.html' - #html = AssertionErrorurlopen(url).read() - html = urlopen(url).read() - pq = PyQuery(html) - break - except: - raise - - ul = pq('ul.usuarios > li') - who = [w.text() for w in ul.items() if w.text() != "Parece que no hay nadie."] - - return who - -def addTo(event, name): - - if event and name: - c = conn.cursor() - date = datetime.now().strftime("%d-%m-%y") - - c.execute('insert into eventTable values(?, ?, ?)', (date, event.replace(" ",""), u'@'+name.replace(" ", ""))) - conn.commit() - c.close() - result = name + ' añadido a ' + event - - elif name: - result = "No tienes nombre de usuario o alias. \n Es necesario para poder añadirte a un evento" - else: - result = "No se ha podido añadir el usuario @" + name+ " a la lista " + name - - return result - -def findByEvent(event): - c = conn.cursor() - - result = c.execute('select * from eventTable where event=?', (event.replace(" ",""),)).fetchall() - - c.close() - - return result - -def removeFromEvent(event, name): - - if any([('@' + name) in i for i in findByEvent(event)]): - c = conn.cursor() - - c.execute('delete from eventTable where event=? and name=?', (event, u'@' + name)) - conn.commit() - - c.close() - result = "Has sido eliminado del evento " + event - else: - result = "No estás en el evento " + event - - return result - -def emptyEvent(event, name): - - if u'@' + name in findByEvent(event): - c = conn.cursor() - - c.execute('delete from eventTable where event=?', (event)) - - result = "El evento " + event +" ha sido eliminado" - conn.commit() - - c.close() - else: - result = 'El evento ' + event + ' NO ha sido eliminado' - - return result - -def listEvents(): - c = conn.cursor() - - h = c.execute('select distinct event from eventTable') - - return h if __name__ == '__main__': - while True: - try: - main() - except Exception as e: - if os.stat('log').st_size > 1024: - permission = 'w' - else: - permission = 'a' - with open('log',permission) as f: - f.write(str(datetime.now().strftime("%d-%m-%y"))+"\n") - f.write(str(e)+"\n") + main()