From 8486d2e0bf32af36b45c4e6c1a7e51240831b5b3 Mon Sep 17 00:00:00 2001 From: jsdio Date: Fri, 13 Oct 2023 23:05:25 +0300 Subject: [PATCH] feat: [#49] tg_bot --- src/bot_aiogram/Makefile | 3 + src/bot_aiogram/poetry.toml | 3 + src/bot_aiogram/pyproject.toml | 142 ++++++++++++++++++ src/bot_aiogram/tgbot/handlers/voice.py | 33 ++++ src/bot_aiogram/tgbot/misc/utils.py | 4 + src/bot_aiogram/tgbot/settings.py | 9 ++ .../tgbot/split_settings/__init__.py | 7 + src/bot_aiogram/tgbot/split_settings/api.py | 20 +++ src/bot_aiogram/tgbot/split_settings/tgbot.py | 22 +++ src/bot_aiogram/tgbot/split_settings/utils.py | 4 + 10 files changed, 247 insertions(+) create mode 100644 src/bot_aiogram/Makefile create mode 100644 src/bot_aiogram/poetry.toml create mode 100644 src/bot_aiogram/pyproject.toml create mode 100644 src/bot_aiogram/tgbot/handlers/voice.py create mode 100644 src/bot_aiogram/tgbot/misc/utils.py create mode 100644 src/bot_aiogram/tgbot/settings.py create mode 100644 src/bot_aiogram/tgbot/split_settings/__init__.py create mode 100644 src/bot_aiogram/tgbot/split_settings/api.py create mode 100644 src/bot_aiogram/tgbot/split_settings/tgbot.py create mode 100644 src/bot_aiogram/tgbot/split_settings/utils.py diff --git a/src/bot_aiogram/Makefile b/src/bot_aiogram/Makefile new file mode 100644 index 0000000..3fbb3fe --- /dev/null +++ b/src/bot_aiogram/Makefile @@ -0,0 +1,3 @@ +include ../../common_makefile.mk + +PROJECT_FOLDERS = tgbot diff --git a/src/bot_aiogram/poetry.toml b/src/bot_aiogram/poetry.toml new file mode 100644 index 0000000..53b35d3 --- /dev/null +++ b/src/bot_aiogram/poetry.toml @@ -0,0 +1,3 @@ +[virtualenvs] +create = true +in-project = true diff --git a/src/bot_aiogram/pyproject.toml b/src/bot_aiogram/pyproject.toml new file mode 100644 index 0000000..521ac59 --- /dev/null +++ b/src/bot_aiogram/pyproject.toml @@ -0,0 +1,142 @@ +[build-system] +build-backend = "poetry.core.masonry.api" +requires = ["poetry-core"] + +[tool.black] +line-length = 120 +target-version = ['py311'] + +[tool.isort] +known_first_party = ["backend", "tests"] +line_length = 120 +profile = "black" +py_version = "311" + +[tool.poetry] +authors = ["jsdio@jsdio.ru"] +description = "" +name = "fastapi_project" +readme = "README.md" +version = "0.1.0" + +[tool.poetry.dependencies] +aiogram = "~2.18" +environs = "~9.0" +pytest-asyncio = "^0.21.1" +python = "^3.11" + +[tool.poetry.dev-dependencies] +black = "^23.7.0" +isort = "^5.12.0" +pylint = "^2.17.5" +pylint-pydantic = "^0.2.4" +pylint-pytest = "^1.1.2" +pyright = "^1.1.318" +pyupgrade = "^3.10.1" +ruff = "^0.0.282" +sort-all = "^1.2.0" +toml-sort = "^0.23.1" + +[tool.pylint] +disable = [ + "broad-except", + "cannot-enumerate-pytest-fixtures", + "consider-using-from-import", + "consider-using-sys-exit", + "duplicate-code", + "fixme", + "missing-docstring", + "no-member", + "protected-access", + "too-few-public-methods", + "too-many-instance-attributes", + "too-many-locals", + "too-many-statements", + "unnecessary-ellipsis" +] +extension-pkg-allow-list = [ + "orjson", + "pydantic" +] +ignore-path = [ + "^.*venv/.*$" +] +load-plugins = [ + "pylint_pydantic", + "pylint_pytest" +] +max-args = 15 +max-line-length = 120 +recursive = true + +[tool.pylint.basic] +argument-rgx = "^_{0,2}[a-z][a-z0-9_]*$" +attr-rgx = "^_{0,2}[a-z][a-z0-9_]*$" +class-attribute-rgx = "^_{0,2}[a-zA-Z][a-zA-Z0-9_]*$" +variable-rgx = "^_{0,2}[a-z][a-z0-9_]*$" + +[tool.pyright] +exclude = [ + ".pytest_cache", + ".venv" +] +pythonPlatform = "All" +pythonVersion = "3.11" +reportConstantRedefenition = "none" +reportMissingTypeStubs = "none" +reportPrivateUsage = "information" +reportPropertyTypeMismatch = "warning" +reportUninitializedInstanceVariable = "warning" +reportUnknownMemberType = "none" +reportUnnecessaryTypeIgnoreComment = "warning" +reportUntypedFunctionDecorator = "warning" +typeCheckingMode = "strict" +useLibraryCodeForTypes = true +venv = ".venv" +venvPath = "." + +[tool.ruff] +ignore = [ + # Pyright automatically infers the type of `self` + "ANN101", + # Pyright automatically infers the type of `cls` + "ANN102", + # In some cases actively detrimental; somewhat conflicts with black + "COM", + # Ignore missing docstrings + "D102", + # In combination with D213, this results in noisy diffs and inconsistencies + # See also . + "D200", + # This results inconsistencies between function and class docstrings + # See also . + "D202", + # D211 is preferred since the extra blank line isn't visually useful + "D203", + # D213 is preferred since it's more readable and allows more characters + "D212", + # Ignore missing docstrings + "D414", + # Covered by D401, which is more restrictive + "D415", + # Type-checkers interpret redundant `as` as exporting an item + "PLC0414", + # Permit using alias for 'import' + "PLR0402", + # Causes churn and awful looking import blocks for little gain + "TCH" +] +select = ["ALL"] + +[tool.ruff.per-file-ignores] +"tests/*" = [ + "D100", + "D103", + "D104", + "S101" +] + +[tool.tomlsort] +all = true +ignore_case = true +in_place = true diff --git a/src/bot_aiogram/tgbot/handlers/voice.py b/src/bot_aiogram/tgbot/handlers/voice.py new file mode 100644 index 0000000..4fa8faa --- /dev/null +++ b/src/bot_aiogram/tgbot/handlers/voice.py @@ -0,0 +1,33 @@ +import io + +import aiogram +import aiohttp + +import tgbot.settings as tgbot_settings + + +async def voice_response(message_voice: aiogram.types.Message): + config: tgbot_settings.Settings = message_voice.bot.get("config") + voice_file_id: str = message_voice.voice.file_id + file_info = await message_voice.bot.get_file(voice_file_id) + file_path: str = file_info.file_path + voice_data: io.BytesIO = io.BytesIO() + voice_data.name = "voice.ogg" + voice_data.seek(0) + await message_voice.bot.download_file(file_path, destination=voice_data) + await message_voice.bot.send_chat_action(message_voice.from_user.id, "typing") + async with aiohttp.ClientSession() as session: + async with session.post( + f"{config.api.api_url}/api/v1/voice/", + data={"voice": voice_data}, + ) as resp: + if resp.status == 200: + voice_answer = await resp.read() + await message_voice.answer_voice(voice_answer) + else: + await message_voice.answer("Not recognized text") + await session.close() + + +def register_voice_response(dp: aiogram.Dispatcher): + dp.register_message_handler(voice_response, content_types=aiogram.types.ContentType.VOICE) diff --git a/src/bot_aiogram/tgbot/misc/utils.py b/src/bot_aiogram/tgbot/misc/utils.py new file mode 100644 index 0000000..459631e --- /dev/null +++ b/src/bot_aiogram/tgbot/misc/utils.py @@ -0,0 +1,4 @@ +import pathlib + +BASE_PATH = pathlib.Path(__file__).parent.parent.parent.parent.resolve() +ENV_PATH = BASE_PATH / ".env" diff --git a/src/bot_aiogram/tgbot/settings.py b/src/bot_aiogram/tgbot/settings.py new file mode 100644 index 0000000..9769600 --- /dev/null +++ b/src/bot_aiogram/tgbot/settings.py @@ -0,0 +1,9 @@ +import pydantic +import pydantic_settings + +import tgbot.split_settings as app_split_settings + + +class Settings(pydantic_settings.BaseSettings): + api: app_split_settings.ApiSettings = pydantic.Field(default_factory=app_split_settings.ApiSettings) + tgbot: app_split_settings.TgBotSettings = pydantic.Field(default_factory=app_split_settings.TgBotSettings) diff --git a/src/bot_aiogram/tgbot/split_settings/__init__.py b/src/bot_aiogram/tgbot/split_settings/__init__.py new file mode 100644 index 0000000..8db5fca --- /dev/null +++ b/src/bot_aiogram/tgbot/split_settings/__init__.py @@ -0,0 +1,7 @@ +from .api import * +from .tgbot import * + +__all__ = [ + "ApiSettings", + "TgBotSettings", +] diff --git a/src/bot_aiogram/tgbot/split_settings/api.py b/src/bot_aiogram/tgbot/split_settings/api.py new file mode 100644 index 0000000..68bb82b --- /dev/null +++ b/src/bot_aiogram/tgbot/split_settings/api.py @@ -0,0 +1,20 @@ +import pydantic_settings + +import tgbot.split_settings.utils as split_settings_utils + + +class ApiSettings(pydantic_settings.BaseSettings): + model_config = pydantic_settings.SettingsConfigDict( + env_file=split_settings_utils.ENV_PATH, + env_prefix="API_", + env_file_encoding="utf-8", + extra="ignore", + ) + + url: str + port: int + protocol: str + + @property + def api_url(self) -> str: + return f"{self.protocol}://{self.url}:{self.port}" diff --git a/src/bot_aiogram/tgbot/split_settings/tgbot.py b/src/bot_aiogram/tgbot/split_settings/tgbot.py new file mode 100644 index 0000000..0f44e09 --- /dev/null +++ b/src/bot_aiogram/tgbot/split_settings/tgbot.py @@ -0,0 +1,22 @@ +import pydantic +import pydantic_settings + +import tgbot.split_settings.utils as split_settings_utils + + +class TgBotSettings(pydantic_settings.BaseSettings): + model_config = pydantic_settings.SettingsConfigDict( + env_file=split_settings_utils.ENV_PATH, + env_prefix="BOT_", + env_file_encoding="utf-8", + extra="ignore", + ) + + token: pydantic.SecretStr = pydantic.Field( + default=..., validation_alias=pydantic.AliasChoices("token", "bot_token") + ) + admins: str = pydantic.Field(default="") + + @pydantic.field_validator("admins") + def validate_bot_admins(cls, v: str) -> list[int]: + return list(map(int, v.split(","))) diff --git a/src/bot_aiogram/tgbot/split_settings/utils.py b/src/bot_aiogram/tgbot/split_settings/utils.py new file mode 100644 index 0000000..c29b42e --- /dev/null +++ b/src/bot_aiogram/tgbot/split_settings/utils.py @@ -0,0 +1,4 @@ +import pathlib + +BASE_PATH = pathlib.Path(__file__).parent.parent.parent.resolve() +ENV_PATH = BASE_PATH / ".env"