added i18n + minor improvements / cleanup
This commit is contained in:
		
							
								
								
									
										146
									
								
								src/beebot.py
									
									
									
									
									
								
							
							
						
						
									
										146
									
								
								src/beebot.py
									
									
									
									
									
								
							| @@ -6,13 +6,13 @@ import os | |||||||
| import sqlite3 | import sqlite3 | ||||||
| import subprocess | import subprocess | ||||||
| from sqlite3 import Connection | from sqlite3 import Connection | ||||||
| from typing import Optional | from typing import Optional, Callable | ||||||
|  |  | ||||||
| import requests | import requests | ||||||
| import telegram.constants | import telegram.constants | ||||||
| from bs4 import BeautifulSoup | from bs4 import BeautifulSoup | ||||||
| from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton | from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton | ||||||
| from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes, CallbackQueryHandler | from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes, CallbackQueryHandler, Application | ||||||
|  |  | ||||||
| log_dir = os.getenv("BEEBOT_LOGS") | log_dir = os.getenv("BEEBOT_LOGS") | ||||||
| if not log_dir: | if not log_dir: | ||||||
| @@ -65,27 +65,27 @@ class BeeBot: | |||||||
|     } |     } | ||||||
|     CATEGORIES = { |     CATEGORIES = { | ||||||
|         "E": { |         "E": { | ||||||
|             "name": "Étudiant" |             "name": "category.student" | ||||||
|         }, |         }, | ||||||
|         "D": { |         "D": { | ||||||
|             "name": "Doctorant" |             "name": "category.phd_student" | ||||||
|         }, |         }, | ||||||
|         "C": { |         "C": { | ||||||
|             "name": "Campus" |             "name": "category.campus" | ||||||
|         }, |         }, | ||||||
|         "V": { |         "V": { | ||||||
|             "name": "Visiteur" |             "name": "category.visitor" | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     def __init__(self, token: str): |     def __init__(self, token: str): | ||||||
|         self.tg_token: str = token |         self.tg_token: str = token | ||||||
|         self.tg_app = None |         self.tg_app: Optional[Application] = None | ||||||
|         self.cache: dict[str, str] = {} |         self.cache: dict[str, str] = {} | ||||||
|         self.db_con: Optional[Connection] = None |         self.db_con: Optional[Connection] = None | ||||||
|         self.locks: dict[str, bool] = {} |         self.langs: Optional[dict[str, dict[str, str]]] = None | ||||||
|         self.fetch_lock = asyncio.Lock() |         self.fetch_lock: asyncio.Lock = asyncio.Lock() | ||||||
|         self.gen_lock = asyncio.Lock() |         self.gen_lock: asyncio.Lock = asyncio.Lock() | ||||||
|  |  | ||||||
|     def mainloop(self): |     def mainloop(self): | ||||||
|         logger.info("Initialization") |         logger.info("Initialization") | ||||||
| @@ -93,15 +93,27 @@ class BeeBot: | |||||||
|         self.db_con = sqlite3.connect(self.DB_PATH) |         self.db_con = sqlite3.connect(self.DB_PATH) | ||||||
|         self.check_database() |         self.check_database() | ||||||
|         self.load_cache() |         self.load_cache() | ||||||
|  |         self.load_i18n() | ||||||
|  |  | ||||||
|         app = ApplicationBuilder().token(self.tg_token).build() |         self.tg_app = ApplicationBuilder().token(self.tg_token).build() | ||||||
|         app.add_handler(CommandHandler("week", self.cmd_week)) |         self.tg_app.add_handler(CommandHandler("week", self.cmd_week)) | ||||||
|         app.add_handler(CommandHandler("today", self.cmd_today)) |         self.tg_app.add_handler(CommandHandler("today", self.cmd_today)) | ||||||
|         app.add_handler(CommandHandler("settings", self.cmd_settings)) |         self.tg_app.add_handler(CommandHandler("settings", self.cmd_settings)) | ||||||
|         app.add_handler(CallbackQueryHandler(self.cb_handler)) |         self.tg_app.add_handler(CallbackQueryHandler(self.cb_handler)) | ||||||
|  |  | ||||||
|         logger.info("Starting bot") |         logger.info("Starting bot") | ||||||
|         app.run_polling() |         self.tg_app.run_polling() | ||||||
|  |  | ||||||
|  |     def load_i18n(self) -> None: | ||||||
|  |         with open(os.path.join(self.BASE_DIR, "lang.json"), "r") as f: | ||||||
|  |             self.langs = json.load(f) | ||||||
|  |  | ||||||
|  |     def i18n(self, lang: str, key: str) -> str: | ||||||
|  |         if lang not in self.langs: | ||||||
|  |             lang = "en" | ||||||
|  |         if key not in self.langs[lang]: | ||||||
|  |             return f"[{key}]" | ||||||
|  |         return self.langs[lang][key] | ||||||
|  |  | ||||||
|     async def cmd_week(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: |     async def cmd_week(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | ||||||
|         logger.debug("Received /week") |         logger.debug("Received /week") | ||||||
| @@ -113,9 +125,10 @@ class BeeBot: | |||||||
|  |  | ||||||
|     async def cmd_settings(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: |     async def cmd_settings(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | ||||||
|         logger.debug("Received /settings") |         logger.debug("Received /settings") | ||||||
|         menu = self.get_settings_menu(update) |         lang = self.get_user_pref(update, context)["lang"] | ||||||
|  |         menu = self.get_settings_menu(context) | ||||||
|         reply_markup = InlineKeyboardMarkup(menu) |         reply_markup = InlineKeyboardMarkup(menu) | ||||||
|         await update.effective_chat.send_message(text="Your preferences", reply_markup=reply_markup) |         await update.effective_chat.send_message(text=self.i18n(lang, "menu.settings"), reply_markup=reply_markup) | ||||||
|  |  | ||||||
|     async def cb_handler(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: |     async def cb_handler(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | ||||||
|         query = update.callback_query |         query = update.callback_query | ||||||
| @@ -128,15 +141,22 @@ class BeeBot: | |||||||
|         elif query.data.startswith("toggle_category"): |         elif query.data.startswith("toggle_category"): | ||||||
|             await self.cb_toggle_category(update, context) |             await self.cb_toggle_category(update, context) | ||||||
|         elif query.data == "back_to_settings": |         elif query.data == "back_to_settings": | ||||||
|             menu = self.get_settings_menu(update) |             await self.show_menu(update, context, "menu.settings", self.get_settings_menu) | ||||||
|             reply_markup = InlineKeyboardMarkup(menu) |  | ||||||
|             await update.effective_message.edit_text(text="Your preferences", reply_markup=reply_markup) |     async def show_menu(self, | ||||||
|  |                         update: Update, | ||||||
|  |                         context: ContextTypes.DEFAULT_TYPE, | ||||||
|  |                         text_key: str, | ||||||
|  |                         menu_func: Callable[[ContextTypes.DEFAULT_TYPE], list[list[InlineKeyboardButton]]]) -> None: | ||||||
|  |         reply_markup = InlineKeyboardMarkup(menu_func(context)) | ||||||
|  |         await update.effective_message.edit_text( | ||||||
|  |             text=self.i18n(context.user_data["lang"], text_key), | ||||||
|  |             reply_markup=reply_markup | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     async def cb_change_language(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: |     async def cb_change_language(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | ||||||
|         logger.debug("Clicked 'Change language'") |         logger.debug("Clicked 'Change language'") | ||||||
|         menu = self.get_language_menu(update) |         await self.show_menu(update, context, "menu.languages", self.get_language_menu) | ||||||
|         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: |     async def cb_set_language(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | ||||||
|         lang = update.callback_query.data.split(":")[1] |         lang = update.callback_query.data.split(":")[1] | ||||||
| @@ -148,21 +168,17 @@ class BeeBot: | |||||||
|         ) |         ) | ||||||
|         self.db_con.commit() |         self.db_con.commit() | ||||||
|         cur.close() |         cur.close() | ||||||
|         menu = self.get_language_menu(update) |         self.get_user_pref(update, context) | ||||||
|         reply_markup = InlineKeyboardMarkup(menu) |         await self.show_menu(update, context, "menu.languages", self.get_language_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: |     async def cb_change_categories(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | ||||||
|         logger.debug("Clicked 'Change categories'") |         logger.debug("Clicked 'Change categories'") | ||||||
|         menu = self.get_categories_menu(update) |         await self.show_menu(update, context, "menu.categories", self.get_categories_menu) | ||||||
|         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: |     async def cb_toggle_category(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | ||||||
|         category = update.callback_query.data.split(":")[1] |         category = update.callback_query.data.split(":")[1] | ||||||
|         logger.debug(f"Clicked 'Toggle category {category}'") |         logger.debug(f"Clicked 'Toggle category {category}'") | ||||||
|         categories: set[str] = self.get_user_pref(update)["categories"] |         categories: set[str] = context.user_data["categories"] | ||||||
|  |  | ||||||
|         if category in categories: |         if category in categories: | ||||||
|             categories.remove(category) |             categories.remove(category) | ||||||
|         else: |         else: | ||||||
| @@ -175,9 +191,8 @@ class BeeBot: | |||||||
|         ) |         ) | ||||||
|         self.db_con.commit() |         self.db_con.commit() | ||||||
|         cur.close() |         cur.close() | ||||||
|         menu = self.get_categories_menu(update) |         self.get_user_pref(update, context) | ||||||
|         reply_markup = InlineKeyboardMarkup(menu) |         await self.show_menu(update, context, "menu.categories", self.get_categories_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: |     async def request_menu(self, update: Update, context: ContextTypes.DEFAULT_TYPE, today_only: bool) -> None: | ||||||
|         chat_id = update.effective_chat.id |         chat_id = update.effective_chat.id | ||||||
| @@ -196,8 +211,9 @@ class BeeBot: | |||||||
|         # If menu needs to be fetched |         # If menu needs to be fetched | ||||||
|         if not os.path.exists(menu_path) or self.is_outdated(menu_id, today_only): |         if not os.path.exists(menu_path) or self.is_outdated(menu_id, today_only): | ||||||
|             # Notify user |             # Notify user | ||||||
|             msg = await update.message.reply_text("The menu is being updated, please wait...") |             msg = await update.message.reply_text(self.i18n(prefs["lang"], "notif.wait_updating")) | ||||||
|             async with self.fetch_lock: |             async with self.fetch_lock: | ||||||
|  |                 if not os.path.exists(menu_path) or self.is_outdated(menu_id, today_only): | ||||||
|                     if today_only: |                     if today_only: | ||||||
|                         await self.fetch_today_menu() |                         await self.fetch_today_menu() | ||||||
|                     else: |                     else: | ||||||
| @@ -205,6 +221,8 @@ class BeeBot: | |||||||
|             await msg.delete() |             await msg.delete() | ||||||
|  |  | ||||||
|         # If image needs to be (re)generated |         # If image needs to be (re)generated | ||||||
|  |         if not os.path.exists(img_path) or self.is_outdated(img_id, today_only): | ||||||
|  |             async with self.gen_lock: | ||||||
|                 if not os.path.exists(img_path) or self.is_outdated(img_id, today_only): |                 if not os.path.exists(img_path) or self.is_outdated(img_id, today_only): | ||||||
|                     await self.gen_image(today_only, categories, img_path, img_id) |                     await self.gen_image(today_only, categories, img_path, img_id) | ||||||
|  |  | ||||||
| @@ -225,26 +243,32 @@ class BeeBot: | |||||||
|         self.db_con.commit() |         self.db_con.commit() | ||||||
|         cur.close() |         cur.close() | ||||||
|  |  | ||||||
|     def get_user_pref(self, update: Update) -> dict: |     def get_user_pref(self, update: Update, context: Optional[ContextTypes.DEFAULT_TYPE] = None) -> dict: | ||||||
|         user_id = update.effective_user.id |         user_id = update.effective_user.id | ||||||
|         cur = self.db_con.cursor() |         cur = self.db_con.cursor() | ||||||
|         res = cur.execute("SELECT categories, lang FROM user WHERE telegram_id=?", (user_id,)) |         res = cur.execute("SELECT categories, lang FROM user WHERE telegram_id=?", (user_id,)) | ||||||
|         user = res.fetchone() |         user = res.fetchone() | ||||||
|         cur.close() |         cur.close() | ||||||
|  |  | ||||||
|         if user is None: |         prefs = { | ||||||
|             self.create_user(user_id) |  | ||||||
|             return { |  | ||||||
|             "categories": {"E", "D", "C", "V"}, |             "categories": {"E", "D", "C", "V"}, | ||||||
|             "lang": "fr" |             "lang": "fr" | ||||||
|         } |         } | ||||||
|  |         if user is None: | ||||||
|  |             self.create_user(user_id) | ||||||
|  |         else: | ||||||
|             categories = set(user[0].split(",")) |             categories = set(user[0].split(",")) | ||||||
|         return { |             prefs = { | ||||||
|                 "categories": categories, |                 "categories": categories, | ||||||
|                 "lang": user[1] |                 "lang": user[1] | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |         if context is not None: | ||||||
|  |             context.user_data["categories"] = prefs["categories"] | ||||||
|  |             context.user_data["lang"] = prefs["lang"] | ||||||
|  |  | ||||||
|  |         return prefs | ||||||
|  |  | ||||||
|     def create_user(self, telegram_id: int) -> None: |     def create_user(self, telegram_id: int) -> None: | ||||||
|         logger.debug(f"New user with id {telegram_id}") |         logger.debug(f"New user with id {telegram_id}") | ||||||
|         cur = self.db_con.cursor() |         cur = self.db_con.cursor() | ||||||
| @@ -377,23 +401,29 @@ class BeeBot: | |||||||
|  |  | ||||||
|         return menus |         return menus | ||||||
|  |  | ||||||
|     def get_settings_menu(self, update: Update) -> list[list[InlineKeyboardButton]]: |     def get_settings_menu(self, context: ContextTypes.DEFAULT_TYPE) -> list[list[InlineKeyboardButton]]: | ||||||
|         prefs = self.get_user_pref(update) |         lang = context.user_data["lang"] | ||||||
|         menu = [ |         menu = [ | ||||||
|             [ |             [ | ||||||
|                 InlineKeyboardButton(f"Language: {prefs['lang']}", callback_data="change_language") |                 InlineKeyboardButton( | ||||||
|  |                     self.i18n(lang, "setting.language").format(lang), | ||||||
|  |                     callback_data="change_language" | ||||||
|  |                 ) | ||||||
|             ], |             ], | ||||||
|             [ |             [ | ||||||
|                 InlineKeyboardButton(f"Categories: {' / '.join(prefs['categories'])}", callback_data="change_categories") |                 InlineKeyboardButton( | ||||||
|  |                     self.i18n(lang, "setting.categories").format(" / ".join(context.user_data["categories"])), | ||||||
|  |                     callback_data="change_categories" | ||||||
|  |                 ) | ||||||
|             ] |             ] | ||||||
|         ] |         ] | ||||||
|         return menu |         return menu | ||||||
|  |  | ||||||
|     def get_language_menu(self, update: Update) -> list[list[InlineKeyboardButton]]: |     def get_language_menu(self, context: ContextTypes.DEFAULT_TYPE) -> list[list[InlineKeyboardButton]]: | ||||||
|         prefs = self.get_user_pref(update) |         user_lang = context.user_data["lang"] | ||||||
|         buttons = [] |         buttons = [] | ||||||
|         for lang, data in self.LANGUAGES.items(): |         for lang, data in self.LANGUAGES.items(): | ||||||
|             extra = " ✅" if prefs["lang"] == lang else "" |             extra = " ✅" if user_lang == lang else "" | ||||||
|             buttons.append( |             buttons.append( | ||||||
|                 InlineKeyboardButton( |                 InlineKeyboardButton( | ||||||
|                     data["emoji"] + extra, |                     data["emoji"] + extra, | ||||||
| @@ -402,24 +432,30 @@ class BeeBot: | |||||||
|             ) |             ) | ||||||
|         menu = [buttons[i:i+2] for i in range(0, len(buttons), 2)] |         menu = [buttons[i:i+2] for i in range(0, len(buttons), 2)] | ||||||
|         menu.append([ |         menu.append([ | ||||||
|             InlineKeyboardButton("Back to settings", callback_data="back_to_settings") |             InlineKeyboardButton( | ||||||
|  |                 self.i18n(user_lang, "menu.back_to_settings"), | ||||||
|  |                 callback_data="back_to_settings" | ||||||
|  |             ) | ||||||
|         ]) |         ]) | ||||||
|         return menu |         return menu | ||||||
|  |  | ||||||
|     def get_categories_menu(self, update: Update) -> list[list[InlineKeyboardButton]]: |     def get_categories_menu(self, context: ContextTypes.DEFAULT_TYPE) -> list[list[InlineKeyboardButton]]: | ||||||
|         prefs = self.get_user_pref(update) |         lang = context.user_data["lang"] | ||||||
|         buttons = [] |         buttons = [] | ||||||
|         for categ, data in self.CATEGORIES.items(): |         for categ, data in self.CATEGORIES.items(): | ||||||
|             extra = " ✅" if categ in prefs["categories"] else " ❌" |             extra = " ✅" if categ in context.user_data["categories"] else " ❌" | ||||||
|             buttons.append( |             buttons.append( | ||||||
|                 InlineKeyboardButton( |                 InlineKeyboardButton( | ||||||
|                     data["name"] + extra, |                     self.i18n(lang, data["name"]) + extra, | ||||||
|                     callback_data=f"toggle_category:{categ}" |                     callback_data=f"toggle_category:{categ}" | ||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|         menu = [buttons[i:i+2] for i in range(0, len(buttons), 2)] |         menu = [buttons[i:i+2] for i in range(0, len(buttons), 2)] | ||||||
|         menu.append([ |         menu.append([ | ||||||
|             InlineKeyboardButton("Back to settings", callback_data="back_to_settings") |             InlineKeyboardButton( | ||||||
|  |                 self.i18n(lang, "menu.back_to_settings"), | ||||||
|  |                 callback_data="back_to_settings" | ||||||
|  |             ) | ||||||
|         ]) |         ]) | ||||||
|         return menu |         return menu | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										41
									
								
								src/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/lang.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | { | ||||||
|  |   "fr": { | ||||||
|  |     "category.student": "Étudiant", | ||||||
|  |     "category.phd_student": "Doctorant", | ||||||
|  |     "category.campus": "Campus", | ||||||
|  |     "category.visitor": "Visiteur", | ||||||
|  |     "menu.settings": "Vos préférences", | ||||||
|  |     "menu.languages": "Choisissez une langue\n(Ceci n'affectera pas la langue des menus)", | ||||||
|  |     "menu.categories": "Choisissez les catégories de prix à afficher", | ||||||
|  |     "menu.back_to_settings": "Retour aux paramètres", | ||||||
|  |     "setting.language": "Langue: {}", | ||||||
|  |     "setting.categories": "Catégories: {}", | ||||||
|  |     "notif.wait_updating": "Le menu est en train d'être mis à jour, veuillez patienter..." | ||||||
|  |   }, | ||||||
|  |   "en": { | ||||||
|  |     "category.student": "Student", | ||||||
|  |     "category.phd_student": "PhD Student", | ||||||
|  |     "category.campus": "Campus", | ||||||
|  |     "category.visitor": "Visitor", | ||||||
|  |     "menu.settings": "Your preferences", | ||||||
|  |     "menu.languages": "Choose a language\n(This will not affect the menu language)", | ||||||
|  |     "menu.categories": "Choose the price categories to display", | ||||||
|  |     "menu.back_to_settings": "Back to settings", | ||||||
|  |     "setting.language": "Language: {}", | ||||||
|  |     "setting.categories": "Categories: {}", | ||||||
|  |     "notif.wait_updating": "The menu is being updated, please wait..." | ||||||
|  |   }, | ||||||
|  |   "de": { | ||||||
|  |     "category.student": "Student", | ||||||
|  |     "category.phd_student": "Doktorand", | ||||||
|  |     "category.campus": "Campus", | ||||||
|  |     "category.visitor": "Besucher", | ||||||
|  |     "menu.settings": "Ihre Präferenzen", | ||||||
|  |     "menu.languages": "Wählen Sie eine Sprache\n(Dies hat keinen Einfluss auf die Menüsprache)", | ||||||
|  |     "menu.categories": "Wählen Sie die anzuzeigenden Preiskategorien", | ||||||
|  |     "menu.back_to_settings": "Zurück zu Einstellungen", | ||||||
|  |     "setting.language": "Sprache: {}", | ||||||
|  |     "setting.categories": "Kategorien: {}", | ||||||
|  |     "notif.wait_updating": "Das Menü wird gerade aktualisiert, bitte warten Sie..." | ||||||
|  |   } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user