commit 41e459521f16ce12f66be3bab9c2b0a62356c0c7 Author: Ilya Bezrukov Date: Fri Jul 19 04:32:27 2024 +0300 Initial, nothing works diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e61a067 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea/ +__pycache__/ +venv/ +.venv/ +logs/ +*.env diff --git a/i18n.yaml b/i18n.yaml new file mode 100644 index 0000000..dd5275b --- /dev/null +++ b/i18n.yaml @@ -0,0 +1,3 @@ +en: + start: "Hello, {message.from_user.full_name}" + help: "" diff --git a/mybot/__init__.py b/mybot/__init__.py new file mode 100644 index 0000000..7534ce3 --- /dev/null +++ b/mybot/__init__.py @@ -0,0 +1,12 @@ +from .config import Config +from .logger import create_logger +from .bot import create_bot +from .i18n import I18N + + +def main(): + config = Config() + logger = create_logger("mybot", config.LOG_LEVEL) + i18n = I18N(logger, config.I18N_PATH) + bot = create_bot(config, logger, i18n) + bot.infinity_polling(config.TIMEOUT, config.DROP_PENDING, config.POLLING_TIMEOUT) diff --git a/mybot/__main__.py b/mybot/__main__.py new file mode 100644 index 0000000..29c2e0a --- /dev/null +++ b/mybot/__main__.py @@ -0,0 +1,5 @@ +from . import main + + +if __name__ == "__main__": + main() diff --git a/mybot/bot.py b/mybot/bot.py new file mode 100644 index 0000000..37d609f --- /dev/null +++ b/mybot/bot.py @@ -0,0 +1,20 @@ +from telebot import TeleBot + +from .handlers import register_handlers +from .middlewares import setup_middlewares + + +def create_bot(config, logger, i18n): + bot = TeleBot(config.BOT_TOKEN, + parse_mode=config.PARSE_MODE, + skip_pending=config.DROP_PENDING, + num_threads=config.NUM_THREADS, + use_class_middlewares=True) + + logger.debug("Setting up middlewares") + setup_middlewares(bot, logger, i18n) + + logger.debug("Registering handlers") + register_handlers(bot) + + return bot diff --git a/mybot/config.py b/mybot/config.py new file mode 100644 index 0000000..d71acaa --- /dev/null +++ b/mybot/config.py @@ -0,0 +1,23 @@ +import os + + +class Config: + # bot setup + BOT_TOKEN: str = os.getenv("BOT_TOKEN") + OWNER_ID: int = int(os.getenv("OWNER_ID", 1)) + LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO") + + # bot behaviour + DROP_PENDING: bool = bool(int(os.getenv("DROP_PENDING", True))) + TIMEOUT: int = int(os.getenv("TIMEOUT", 20)) + POLLING_TIMEOUT: int = int(os.getenv("POLLING_TIMEOUT", 20)) + NUM_THREADS: int = int(os.getenv("NUM_THREADS", 2)) + PARSE_MODE: str = os.getenv("PARSE_MODE", "html") + + # i18n + I18N_PATH: str = os.getenv("I18N_PATH", "i18n.yaml") + I18N_LANG: str = os.getenv("I18N_LANG", "en") + + def __init__(self): + if not self.BOT_TOKEN: + raise RuntimeError("Missing BOT_TOKEN") diff --git a/mybot/handlers/__init__.py b/mybot/handlers/__init__.py new file mode 100644 index 0000000..3f3bf51 --- /dev/null +++ b/mybot/handlers/__init__.py @@ -0,0 +1,7 @@ +from telebot import TeleBot + +from . import basic + + +def register_handlers(bot: TeleBot): + basic.register_handlers(bot) diff --git a/mybot/handlers/basic.py b/mybot/handlers/basic.py new file mode 100644 index 0000000..0198d5f --- /dev/null +++ b/mybot/handlers/basic.py @@ -0,0 +1,15 @@ +from telebot import TeleBot +from telebot import types as tt + + +def start(message: tt.Message, bot: TeleBot, logger, t): + bot.send_message(message.chat.id, t("start")) + + +def help_(message: tt.Message, bot: TeleBot, logger, t): + bot.send_message(message.chat.id, t("help")) + + +def register_handlers(bot: TeleBot): + bot.register_message_handler(start, commands=["start"]) + bot.register_message_handler(help_, commands=["help"]) diff --git a/mybot/i18n.py b/mybot/i18n.py new file mode 100644 index 0000000..38d08cf --- /dev/null +++ b/mybot/i18n.py @@ -0,0 +1,48 @@ +import logging +from typing import Optional + +from yaml import safe_load + + +class I18N: + def __init__(self, logger: logging.Logger, path="i18n.yaml", fallback_lang="en"): + self.logger = logger + self._path = path + self._fallback_lang = fallback_lang + self._dict = dict() + self.load() + + @property + def path(self): + return self._path + + @property + def fallback_lang(self): + return self._fallback_lang + + def load(self): + self._dict.clear() + with open(self._path) as f: + self._dict = safe_load(f) + if self.fallback_lang not in self._dict: + raise RuntimeError("I18N file doesn't contain fallback language section") + + def get(self, phrase: str, lang: Optional[str] = None): + lang = lang or self.fallback_lang + if lang not in self._dict: + self.logger.warning(f"Language '{lang}' not found in i18n, using fallback") + lang = self.fallback_lang + lang_dict = self._dict.get(lang) + result = lang_dict.get(phrase) + if result is None: + self.logger.error(f"Phrase '{phrase}' not found in language '{lang}'") + result = f"" + return result + + def __call__(self, phrase: str, lang: Optional[str] = None, *args, **kwargs): + return self.get(phrase, lang).format(*args, **kwargs) + + def custom_call(self, lang: Optional[str] = None, *args, **kwargs): + def call(phrase: str, ilang: Optional[str] = None, *iargs, **ikwargs): + return self(phrase, lang or ilang, *args, *iargs, **kwargs, **ikwargs) + return call diff --git a/mybot/logger.py b/mybot/logger.py new file mode 100644 index 0000000..d49aacb --- /dev/null +++ b/mybot/logger.py @@ -0,0 +1,30 @@ +import sys +import os +import logging +from typing import Optional + + +def create_logger(name: str, + level: [str, int], + stream_stderr: bool = False, + stream_format: str = "%(asctime)s | %(name)s [%(levelname)s] %(message)s", + file: bool = False, + file_path: Optional[None] = None, + file_format: str = "%(asctime)s | %(name)s [%(levelname)s] %(message)s"): + logger = logging.getLogger(name) + logger.setLevel(level) + + stream_handler = logging.StreamHandler(sys.stderr if stream_stderr else sys.stdout) + stream_handler.setFormatter(logging.Formatter(stream_format)) + logger.addHandler(stream_handler) + + if file: + if not file_path: + file_path = os.path.join("logs", name) + os.makedirs(os.path.dirname(file_path), exist_ok=True) + file_handler = logging.FileHandler(file_path) + file_handler.setFormatter(logging.Formatter(file_format)) + logger.addHandler(file_handler) + + return logger + diff --git a/mybot/middlewares/__init__.py b/mybot/middlewares/__init__.py new file mode 100644 index 0000000..23b0449 --- /dev/null +++ b/mybot/middlewares/__init__.py @@ -0,0 +1,9 @@ +import logging + +from telebot import TeleBot + +from .arguments import ExtraArguments + + +def setup_middlewares(bot: TeleBot, logger: logging.Logger, i18n): + bot.setup_middleware(ExtraArguments(bot, logger, i18n)) diff --git a/mybot/middlewares/arguments.py b/mybot/middlewares/arguments.py new file mode 100644 index 0000000..f712b97 --- /dev/null +++ b/mybot/middlewares/arguments.py @@ -0,0 +1,34 @@ +import logging +from functools import singledispatch + +from telebot import TeleBot +from telebot.handler_backends import BaseMiddleware +from telebot import types as tt + + +class ExtraArguments(BaseMiddleware): + def __init__(self, bot: TeleBot, logger: logging.Logger, i18n): + super().__init__() + self.bot = bot + self.logger = logger + self.i18n = i18n + self.update_types = ["message", "callback_query"] + + @singledispatch + def pre_process(self, obj, data: dict): + raise TypeError() + + @pre_process.register + def _(self, message: tt.Message, data: dict): + data["bot"] = self.bot + data["logger"] = self.logger + data["t"] = self.i18n.custom_call(message=message) + + @pre_process.register + def _(self, callback: tt.CallbackQuery, data: dict): + data["bot"] = self.bot + data["logger"] = self.logger + data["t"] = self.i18n.custom_call(callback=callback) + + def post_process(self, message, data: dict, exception: BaseException): + pass diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..48acc7d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pytelegrambotapi +pyyaml