diff --git a/src/beebot.py b/src/beebot.py index b18f660..01cb76a 100644 --- a/src/beebot.py +++ b/src/beebot.py @@ -11,8 +11,8 @@ from typing import Optional import requests import telegram.constants from bs4 import BeautifulSoup -from telegram import Update -from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes +from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton +from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes, CallbackQueryHandler log_dir = os.getenv("BEEBOT_LOGS") if not log_dir: @@ -35,6 +35,12 @@ logging.basicConfig( logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) + +def flag(code): + offset = 127462 - ord('A') + code = code.upper() + return chr(ord(code[0]) + offset) + chr(ord(code[1]) + offset) + class BeeBot: MENU_URL = "https://www.epfl.ch/campus/restaurants-shops-hotels/fr/industrie21-epfl-valais-wallis/?date={date}" RESTO_ID = 545 @@ -43,6 +49,34 @@ class BeeBot: DB_PATH = os.path.join(BASE_DIR, "database.db") TEMPLATE_PATH = os.path.join(BASE_DIR, "menu.typ") IMG_DIR = "/tmp/menu_images" + LANGUAGES = { + "en": { + "emoji": flag("gb"), + "name": "English" + }, + "fr": { + "emoji": flag("fr"), + "name": "Français" + }, + "de": { + "emoji": flag("de"), + "name": "Deutsch" + } + } + CATEGORIES = { + "E": { + "name": "Étudiant" + }, + "D": { + "name": "Doctorant" + }, + "C": { + "name": "Campus" + }, + "V": { + "name": "Visiteur" + } + } def __init__(self, token: str): self.tg_token: str = token @@ -63,6 +97,8 @@ class BeeBot: app = ApplicationBuilder().token(self.tg_token).build() app.add_handler(CommandHandler("week", self.cmd_week)) app.add_handler(CommandHandler("today", self.cmd_today)) + app.add_handler(CommandHandler("settings", self.cmd_settings)) + app.add_handler(CallbackQueryHandler(self.cb_handler)) logger.info("Starting bot") app.run_polling() @@ -75,6 +111,74 @@ class BeeBot: logger.debug("Received /today") await self.request_menu(update, context, True) + async def cmd_settings(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + logger.debug("Received /settings") + menu = self.get_settings_menu(update) + reply_markup = InlineKeyboardMarkup(menu) + await update.effective_chat.send_message(text="Your preferences", reply_markup=reply_markup) + + async def cb_handler(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + query = update.callback_query + if query.data == "change_language": + await self.cb_change_language(update, context) + elif query.data.startswith("set_language"): + await self.cb_set_language(update, context) + elif query.data == "change_categories": + await self.cb_change_categories(update, context) + elif query.data.startswith("toggle_category"): + await self.cb_toggle_category(update, context) + elif query.data == "back_to_settings": + menu = self.get_settings_menu(update) + reply_markup = InlineKeyboardMarkup(menu) + await update.effective_message.edit_text(text="Your preferences", reply_markup=reply_markup) + + async def cb_change_language(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + logger.debug("Clicked 'Change language'") + menu = self.get_language_menu(update) + reply_markup = InlineKeyboardMarkup(menu) + await update.effective_message.edit_text(text="Choose a language", reply_markup=reply_markup) + + async def cb_set_language(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + lang = update.callback_query.data.split(":")[1] + logger.debug(f"Clicked 'Set language to {lang}'") + cur = self.db_con.cursor() + cur.execute( + "UPDATE user SET lang=? WHERE telegram_id=?", + (lang, update.effective_user.id) + ) + self.db_con.commit() + cur.close() + menu = self.get_language_menu(update) + reply_markup = InlineKeyboardMarkup(menu) + await update.effective_message.edit_text(text="Choose a language", reply_markup=reply_markup) + + async def cb_change_categories(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + logger.debug("Clicked 'Change categories'") + menu = self.get_categories_menu(update) + reply_markup = InlineKeyboardMarkup(menu) + await update.effective_message.edit_text(text="Choose the price categories to display", reply_markup=reply_markup) + + async def cb_toggle_category(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + category = update.callback_query.data.split(":")[1] + logger.debug(f"Clicked 'Toggle category {category}'") + categories: set[str] = self.get_user_pref(update)["categories"] + + if category in categories: + categories.remove(category) + else: + categories.add(category) + + cur = self.db_con.cursor() + cur.execute( + "UPDATE user SET categories=? WHERE telegram_id=?", + (",".join(categories), update.effective_user.id) + ) + self.db_con.commit() + cur.close() + menu = self.get_categories_menu(update) + reply_markup = InlineKeyboardMarkup(menu) + await update.effective_message.edit_text(text="Choose the price categories to display", reply_markup=reply_markup) + async def request_menu(self, update: Update, context: ContextTypes.DEFAULT_TYPE, today_only: bool) -> None: chat_id = update.effective_chat.id await context.bot.send_chat_action(chat_id=chat_id, action=telegram.constants.ChatAction.TYPING) @@ -113,6 +217,7 @@ class BeeBot: "id" INTEGER, "telegram_id" INTEGER NOT NULL UNIQUE, "categories" TEXT, + "lang" TEXT NOT NULL DEFAULT 'fr', "created_at" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY("id" AUTOINCREMENT) ); @@ -123,27 +228,29 @@ class BeeBot: def get_user_pref(self, update: Update) -> dict: user_id = update.effective_user.id cur = self.db_con.cursor() - res = cur.execute("SELECT categories FROM user WHERE telegram_id=?", (user_id,)) + res = cur.execute("SELECT categories, lang FROM user WHERE telegram_id=?", (user_id,)) user = res.fetchone() cur.close() if user is None: self.create_user(user_id) return { - "categories": {"E", "D", "C", "V"} + "categories": {"E", "D", "C", "V"}, + "lang": "fr" } categories = set(user[0].split(",")) return { - "categories": categories + "categories": categories, + "lang": user[1] } def create_user(self, telegram_id: int) -> None: logger.debug(f"New user with id {telegram_id}") cur = self.db_con.cursor() cur.execute( - "INSERT INTO user (telegram_id, categories) VALUES (?, ?)", - (telegram_id, "E,D,C,V") + "INSERT INTO user (telegram_id, categories, lang) VALUES (?, ?, ?)", + (telegram_id, "E,D,C,V", "fr") ) self.db_con.commit() cur.close() @@ -270,6 +377,52 @@ class BeeBot: return menus + def get_settings_menu(self, update: Update) -> list[list[InlineKeyboardButton]]: + prefs = self.get_user_pref(update) + menu = [ + [ + InlineKeyboardButton(f"Language: {prefs['lang']}", callback_data="change_language") + ], + [ + InlineKeyboardButton(f"Categories: {' / '.join(prefs['categories'])}", callback_data="change_categories") + ] + ] + return menu + + def get_language_menu(self, update: Update) -> list[list[InlineKeyboardButton]]: + prefs = self.get_user_pref(update) + buttons = [] + for lang, data in self.LANGUAGES.items(): + extra = " ✅" if prefs["lang"] == lang else "" + buttons.append( + InlineKeyboardButton( + data["emoji"] + extra, + callback_data=f"set_language:{lang}" + ) + ) + menu = [buttons[i:i+2] for i in range(0, len(buttons), 2)] + menu.append([ + InlineKeyboardButton("Back to settings", callback_data="back_to_settings") + ]) + return menu + + def get_categories_menu(self, update: Update) -> list[list[InlineKeyboardButton]]: + prefs = self.get_user_pref(update) + buttons = [] + for categ, data in self.CATEGORIES.items(): + extra = " ✅" if categ in prefs["categories"] else " ❌" + buttons.append( + InlineKeyboardButton( + data["name"] + extra, + callback_data=f"toggle_category:{categ}" + ) + ) + menu = [buttons[i:i+2] for i in range(0, len(buttons), 2)] + menu.append([ + InlineKeyboardButton("Back to settings", callback_data="back_to_settings") + ]) + return menu + if __name__ == '__main__': logger.info("Welcome to BeeBot !") bot = BeeBot(os.getenv("TELEGRAM_TOKEN"))