Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added bot commands regarding team info #3

Merged
merged 16 commits into from
Oct 23, 2022
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",
feldlime marked this conversation as resolved.
Show resolved Hide resolved
"Далее модели будут запрашиваться по адресу: {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