Skip to content

Commit

Permalink
Merge pull request #3 from groundmax/feature/telegram_bot
Browse files Browse the repository at this point in the history
added bot commands regarding team info
  • Loading branch information
groundmax authored Oct 23, 2022
2 parents 726309a + 30733fc commit 6cc67b1
Show file tree
Hide file tree
Showing 16 changed files with 731 additions and 114 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ jobs:
runs-on: ubuntu-20.04
env:
DB_URL: postgresql://user:pass@127.0.0.1:5432/db
BOT_TOKEN: BOT_TOKEN
BOT_NAME: BOT_NAME

services:

Expand Down
21 changes: 20 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
import asyncio

from requestor.bot import dp, config, register_handlers, bot, BotCommands
from requestor.services import make_db_service
from requestor.log import setup_logging

async def main():
db_service = make_db_service(config)
register_handlers(dp, db_service, config)
setup_logging(config)

await bot.set_my_commands(commands=BotCommands.get_bot_commands())
await db_service.setup()
try:
await dp.start_polling()
finally:
await db_service.cleanup()


if __name__ == "__main__":
pass
asyncio.run(main())
323 changes: 220 additions & 103 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ orjson = "^3.5.3"
alembic = "^1.6.5"
aiohttp = "^3.7.4"
asyncpg = "^0.23.0"
aiogram = "^2.22.2"
uvloop = "^0.17.0"

[tool.poetry.dev-dependencies]
black = "22.3.0"
Expand Down
11 changes: 11 additions & 0 deletions requestor/bot/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .commands import BotCommands
from .create_bot import bot, config, dp
from .handlers import register_handlers

__all__ = (
"dp",
"bot",
"config",
"register_handlers",
"BotCommands",
)
67 changes: 67 additions & 0 deletions requestor/bot/bot_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import typing as tp

from aiogram import types
from aiogram.utils.markdown import bold, escape_md, text

from requestor.models import Model, TeamInfo

from .constants import DATETIME_FORMAT


# TODO: somehow try generalize this func to reduce duplicate code
def parse_msg_with_team_info(
message: types.Message,
) -> tp.Tuple[tp.Optional[str], tp.Optional[TeamInfo]]:
args = message.get_args().split()
n_args = len(args)
if n_args == 4:
token, title, api_base_url, api_key = args
elif n_args == 3:
token, title, api_base_url = args
api_key = None

try:
return token, TeamInfo(
title=title, chat_id=message.chat.id, api_base_url=api_base_url, api_key=api_key
)
except NameError:
return None, None


def parse_msg_with_model_info(
message: types.Message,
) -> tp.Tuple[tp.Optional[str], tp.Optional[str]]:
args = message.get_args().split(maxsplit=1)
n_args = len(args)
if n_args == 2:
name, description = args
elif n_args == 1:
name = args[0]
description = None

try:
return name, description
except NameError:
return None, None


def generate_model_description(model: Model, model_num: int) -> str:
description = model.description or "Отсутствует"
created_at = model.created_at.strftime(DATETIME_FORMAT)
return text(
bold(model_num),
f"{bold('Название')}: {escape_md(model.name)}",
f"{bold('Описание')}: {escape_md(description)}",
f"{bold('Дата добавления (UTC)')}: {escape_md(created_at)}",
sep="\n",
)


def generate_models_description(models: tp.List[Model]) -> str:
model_descriptions = []
for model_num, model in enumerate(models, 1):
model_description = generate_model_description(model, model_num)
model_descriptions.append(model_description)

reply = "\n\n".join(model_descriptions)
return reply
129 changes: 129 additions & 0 deletions requestor/bot/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import typing as tp
from dataclasses import dataclass
from enum import Enum

from aiogram.types import BotCommand
from aiogram.utils.markdown import text

from .constants import TEAM_MODELS_DISPLAY_LIMIT


@dataclass
class CommandDescription:
command_name: str
short_description: str
long_description: tp.Optional[str] = None


commands_description = (
(
"start",
"Начало работы с ботом",
),
(
"help",
"Список доступных команд",
),
(
"register_team",
"Регистрация команды",
text(
"С помощью этой команды можно зарегистрировать свою команду",
"Принимает на вход аргументы через пробел:",
"token - токен, который генерируется индивидуально для каждой команды.",
"title - название команды, без пробелов и кавычек.",
"api_base_url - хост, по которому будет находиться API команды.",
"api_key - опционально, токен для запрашивания API.",
"Пример использования:",
"/register_team MyToken MyTeamName http://myapi.ru/api/v1 MyApiKey",
sep="\n",
),
),
(
"update_team",
"Обновление информации команды",
text(
"С помощью этой команды можно обновить хост или токен API.",
"Для этого используются соответствующие аргументы через пробел.",
"api_base_url - хост, по которому будет находиться API команды.",
"api_key - токен для запрашивания API.",
"Пример использования для обновления хоста:",
"/update_team api_base_url http://myapi.ru/api/v2",
sep="\n",
),
),
(
"show_team",
"Вывод информации по текущей команде",
"Выводит название команды, хост и API токен",
),
(
"add_model",
"Добавление новой модели",
text(
"С помощью этой команды можно добавить модель для проверки.",
"Для этого используются следующие аргументы:",
"name - название модели, без пробелов и кавычек.",
"description - опционально, более подробное описание модели",
"Пример использования для добавления модели:",
"/add_model lightfm_64",
"Далее модели будут запрашиваться по адресу: {api_base_url}/{name}/{user_id}",
(
"То есть адрес для запроса выглядит, например, так: "
"http://myapi.ru/api/v1/lightfm_64/178"
),
"Пример использования для добавления модели с описанием:",
"/add_model lightfm_64 Добавили фичи по юзерам и айтемам",
sep="\n",
),
),
(
"show_models",
"Вывод информации по добавленным моделям",
text(
"С помощью этой команды можно вывести следующую информацию:",
(
"Название, описание (если присутствует) и дату добавления модели по UTC. "
f"Если было добавлено более {TEAM_MODELS_DISPLAY_LIMIT} моделей, "
f"то выведутся последние {TEAM_MODELS_DISPLAY_LIMIT} по дате добавления "
"в обратном хронологическом порядке."
),
sep="\n",
),
),
# TODO: create request command
("request", "Запрос рекомендаций по модели", "Какое-то описание"),
)

cmd2cls_desc = {args[0]: CommandDescription(*args) for args in commands_description}


# it can be initialized via Enum("BotCommands", cmd2cls_desc)
# but IDE doesn't provide you with helper annotations
# and you can not add class methods without "hacks"
# TODO: think of simple way instantiate a frozen class
# with typehinting from IDE
class BotCommands(Enum):
start: CommandDescription = cmd2cls_desc["start"]
help: CommandDescription = cmd2cls_desc["help"]
register_team: CommandDescription = cmd2cls_desc["register_team"]
update_team: CommandDescription = cmd2cls_desc["update_team"]
show_team: CommandDescription = cmd2cls_desc["show_team"]
add_model: CommandDescription = cmd2cls_desc["add_model"]
show_models: CommandDescription = cmd2cls_desc["show_models"]

@classmethod
def get_bot_commands(cls) -> tp.List[BotCommand]:
return [
BotCommand(command=command.name, description=command.value.short_description)
for command in BotCommands
]

@classmethod
def get_description_for_available_commands(cls) -> str:
descriptions = []
for command in BotCommands:
if command not in (BotCommands.start, BotCommands.help):
descriptions.append(f"/{command.name}\n{command.value.long_description}")

return "\n\n".join(descriptions)
18 changes: 18 additions & 0 deletions requestor/bot/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import typing as tp

AVAILABLE_FOR_UPDATE: tp.Final = {
"api_base_url",
"api_key",
}

INCORRECT_DATA_IN_MSG: tp.Final = (
"Пожалуйста, введите данные в корректном формате. Используйте команду /help для справки."
)

TEAM_NOT_FOUND_MSG: tp.Final = (
"Команда от вашего чата не найдена. Скорее всего, вы еще не регистрировались."
)

TEAM_MODELS_DISPLAY_LIMIT: tp.Final = 10

DATETIME_FORMAT: tp.Final = "%Y-%m-%d %H:%M:%S"
10 changes: 10 additions & 0 deletions requestor/bot/create_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from aiogram import Bot
from aiogram.dispatcher import Dispatcher

from requestor.settings import get_config

config = get_config()

bot = Bot(token=config.telegram_config.bot_token)

dp = Dispatcher(bot)
Loading

0 comments on commit 6cc67b1

Please sign in to comment.