added i18n + minor improvements / cleanup

This commit is contained in:
Louis Heredero 2024-09-14 15:42:05 +02:00
parent b7bf0e7fa2
commit 379f5dfb50
Signed by: HEL
GPG Key ID: 8D83DE470F8544E7
2 changed files with 140 additions and 63 deletions

View File

@ -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,17 +211,20 @@ 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 today_only: if not os.path.exists(menu_path) or self.is_outdated(menu_id, today_only):
await self.fetch_today_menu() if today_only:
else: await self.fetch_today_menu()
await self.fetch_week_menu() else:
await self.fetch_week_menu()
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): 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) async with self.gen_lock:
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 context.bot.send_photo(chat_id=chat_id, photo=img_path) await context.bot.send_photo(chat_id=chat_id, photo=img_path)
@ -225,25 +243,31 @@ 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()
prefs = {
"categories": {"E", "D", "C", "V"},
"lang": "fr"
}
if user is None: if user is None:
self.create_user(user_id) self.create_user(user_id)
return { else:
"categories": {"E", "D", "C", "V"}, categories = set(user[0].split(","))
"lang": "fr" prefs = {
"categories": categories,
"lang": user[1]
} }
categories = set(user[0].split(",")) if context is not None:
return { context.user_data["categories"] = prefs["categories"]
"categories": categories, context.user_data["lang"] = prefs["lang"]
"lang": user[1]
} 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}")
@ -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
View 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..."
}
}