diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
deleted file mode 100644
index 08e15a60..00000000
--- a/.github/workflows/semgrep.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-on:
- pull_request: {}
- push:
- branches:
- - main
- - master
- paths:
- - .github/workflows/semgrep.yml
- schedule:
- - cron: '0 0 * * 0'
-name: Semgrep
-jobs:
- semgrep:
- name: Scan
- runs-on: ubuntu-20.04
- env:
- SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
- container:
- image: returntocorp/semgrep
- steps:
- - uses: actions/checkout@v3
- - run: semgrep ci
diff --git a/app/__main__.py b/app/__main__.py
index e49445f8..578e8891 100644
--- a/app/__main__.py
+++ b/app/__main__.py
@@ -1,5 +1,7 @@
+import asyncio
+
from app.config.main import load_config
from app.utils.cli import cli
if __name__ == "__main__":
- cli(load_config())
+ asyncio.run(cli(load_config()))
diff --git a/app/config/moderation.py b/app/config/moderation.py
index 7d89effe..e62c500f 100644
--- a/app/config/moderation.py
+++ b/app/config/moderation.py
@@ -2,13 +2,14 @@
from functools import partial
from aiogram import Bot
+from aiogram.types import ChatPermissions
from app.models.common import TypeRestriction
DEFAULT_RESTRICT_DURATION = timedelta(hours=1)
FOREVER_RESTRICT_DURATION = timedelta(days=666)
-RO_ACTION = partial(Bot.restrict_chat_member, can_send_messages=False)
+RO_ACTION = partial(Bot.restrict_chat_member, permissions=ChatPermissions(can_send_messages=False))
BAN_ACTION = Bot.kick_chat_member
action_for_restrict = {
TypeRestriction.ban: BAN_ACTION,
diff --git a/app/filters/__init__.py b/app/filters/__init__.py
index 9a2acbc0..f430827d 100644
--- a/app/filters/__init__.py
+++ b/app/filters/__init__.py
@@ -1,30 +1,7 @@
-from functools import partialmethod
-
-from aiogram import Dispatcher
-
-from app.models.config import Config
from app.utils.log import Logger
+from .karma_change import KarmaFilter
+from .tg_permissions import BotHasPermissions, HasPermissions
+from .has_target import HasTargetFilter
logger = Logger(__name__)
-
-
-def setup(dispatcher: Dispatcher, config: Config):
- logger.info("Configure filters...")
- from .superuser import IsSuperuserFilter
- from .karma_change import KarmaFilter
- from .tg_permissions import BotHasPermissions, HasPermissions
- from .has_target import HasTargetFilter
-
- text_messages = [
- dispatcher.message_handlers,
- dispatcher.edited_message_handlers,
- dispatcher.callback_query_handlers,
- ]
- IsSuperuserFilter.check = partialmethod(IsSuperuserFilter.check, config.superusers)
-
- dispatcher.filters_factory.bind(KarmaFilter, event_handlers=text_messages)
- dispatcher.filters_factory.bind(IsSuperuserFilter, event_handlers=text_messages)
- dispatcher.filters_factory.bind(BotHasPermissions, event_handlers=text_messages)
- dispatcher.filters_factory.bind(HasPermissions, event_handlers=text_messages)
- dispatcher.filters_factory.bind(HasTargetFilter, event_handlers=text_messages)
diff --git a/app/filters/has_target.py b/app/filters/has_target.py
index 030a8d44..6c3c48b1 100644
--- a/app/filters/has_target.py
+++ b/app/filters/has_target.py
@@ -1,26 +1,20 @@
-import typing
from dataclasses import dataclass
-from aiogram import types
-from aiogram.dispatcher.filters import BoundFilter
+from aiogram.filters import BaseFilter
+from aiogram.types import Message
+from app.models import dto
from app.services.find_target_user import get_target_user
@dataclass
-class HasTargetFilter(BoundFilter):
- key = "has_target"
- has_target: typing.Optional[typing.Dict[str, bool]]
+class HasTargetFilter(BaseFilter):
+ can_be_same: bool = False
+ can_be_bot: bool = False
- def __post_init__(self):
- if self.has_target is True:
- self.has_target = {}
-
- async def check(self, message: types.Message) -> typing.Dict[str, types.User]:
- can_be_same = self.has_target.get("can_be_same", False)
- can_be_bot = self.has_target.get("can_be_bot", False)
- target_user = get_target_user(message, can_be_same, can_be_bot)
+ async def __call__(self, message: Message) -> dict[str, dto.TargetUser]:
+ target_user = get_target_user(message, self.can_be_same, self.can_be_bot)
if target_user is None:
return {}
- rez = {'target': target_user}
+ rez = {"target": target_user}
return rez
diff --git a/app/filters/karma_change.py b/app/filters/karma_change.py
index dccb7994..402ad26e 100644
--- a/app/filters/karma_change.py
+++ b/app/filters/karma_change.py
@@ -2,8 +2,7 @@
from dataclasses import dataclass
from aiogram import types
-from aiogram.dispatcher.filters import BoundFilter
-from aiogram.dispatcher.handler import ctx_data
+from aiogram.filters import BaseFilter
from app.config.karmic_triggers import (
PLUS,
@@ -20,19 +19,15 @@
@dataclass
-class KarmaFilter(BoundFilter):
+class KarmaFilter(BaseFilter):
"""
Filtered message should be change karma
"""
- key = "karma_change"
-
- karma_change: bool
-
- async def check(self, message: types.Message) -> dict[str, dict[str, float]]:
- data = ctx_data.get()
- settings: ChatSettings = data.get("chat_settings", None)
- if settings is None or not settings.karma_counting:
+ async def __call__(
+ self, message: types.Message, chat_settings: ChatSettings,
+ ) -> dict[str, dict[str, float]]:
+ if chat_settings is None or not chat_settings.karma_counting:
return {}
return await check(message)
diff --git a/app/filters/superuser.py b/app/filters/superuser.py
deleted file mode 100644
index cc1341d4..00000000
--- a/app/filters/superuser.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from dataclasses import dataclass
-from typing import Iterable
-
-from aiogram.dispatcher.filters import BoundFilter
-from aiogram.dispatcher.handler import ctx_data
-
-from app.models.db import User
-
-
-@dataclass
-class IsSuperuserFilter(BoundFilter):
- key = "is_superuser"
- is_superuser: bool = None
-
- async def check(self, superusers: Iterable[int], event) -> bool:
- data = ctx_data.get()
- user: User = data["user"]
- return user.tg_id in superusers
diff --git a/app/filters/tg_permissions.py b/app/filters/tg_permissions.py
index dca3d560..fb0aaa37 100644
--- a/app/filters/tg_permissions.py
+++ b/app/filters/tg_permissions.py
@@ -1,9 +1,9 @@
# from https://github.com/aiogram/bot/blob/master/app/filters/has_permissions.py
-import typing
from dataclasses import dataclass
+from typing import Any, Union, Dict
-from aiogram import types
-from aiogram.dispatcher.filters import Filter
+from aiogram import types, Bot
+from aiogram.filters import Filter
@dataclass
@@ -11,7 +11,6 @@ class HasPermissions(Filter):
"""
Validate the user has specified permissions in chat
"""
-
can_post_messages: bool = False
can_edit_messages: bool = False
can_delete_messages: bool = False
@@ -38,30 +37,17 @@ def __post_init__(self):
arg: True for arg in self.ARGUMENTS.values() if getattr(self, arg)
}
- @classmethod
- def validate(
- cls, full_config: typing.Dict[str, typing.Any]
- ) -> typing.Optional[typing.Dict[str, typing.Any]]:
- config = {}
- for alias, argument in cls.ARGUMENTS.items():
- if alias in full_config:
- config[argument] = full_config.pop(alias)
- return config
-
def _get_cached_value(self, message: types.Message):
- try:
- return message.conf[self.PAYLOAD_ARGUMENT_NAME]
- except KeyError:
- return None
+ return None # TODO
def _set_cached_value(self, message: types.Message, member: types.ChatMember):
- message.conf[self.PAYLOAD_ARGUMENT_NAME] = member
+ return None # TODO
- async def _get_chat_member(self, message: types.Message):
+ async def _get_chat_member(self, message: types.Message, bot: Bot):
chat_member: types.ChatMember = self._get_cached_value(message)
if chat_member is None:
- admins = await message.chat.get_administrators()
- target_user_id = self.get_target_id(message)
+ admins = await bot.get_chat_administrators(message.chat.id)
+ target_user_id = self.get_target_id(message, bot)
try:
chat_member = next(filter(lambda member: member.user.id == target_user_id, admins))
except StopIteration:
@@ -69,13 +55,11 @@ async def _get_chat_member(self, message: types.Message):
self._set_cached_value(message, chat_member)
return chat_member
- async def check(
- self, message: types.Message
- ) -> typing.Union[bool, typing.Dict[str, typing.Any]]:
- chat_member = await self._get_chat_member(message)
+ async def __call__(self, message: types.Message, bot: Bot) -> Union[bool, Dict[str, Any]]:
+ chat_member = await self._get_chat_member(message, bot)
if not chat_member:
return False
- if chat_member.status == types.ChatMemberStatus.CREATOR:
+ if chat_member.status == "creator":
return chat_member
for permission, value in self.required_permissions.items():
if not getattr(chat_member, permission):
@@ -83,7 +67,7 @@ async def check(
return {self.PAYLOAD_ARGUMENT_NAME: chat_member}
- def get_target_id(self, message: types.Message) -> int:
+ def get_target_id(self, message: types.Message, bot: Bot) -> int:
return message.from_user.id
@@ -104,5 +88,5 @@ class BotHasPermissions(HasPermissions):
}
PAYLOAD_ARGUMENT_NAME = "bot_member"
- def get_target_id(self, message: types.Message) -> int:
- return message.bot.id
+ def get_target_id(self, message: types.Message, bot: Bot) -> int:
+ return bot.id
diff --git a/app/handlers/__init__.py b/app/handlers/__init__.py
index 2f38811a..84b922c8 100644
--- a/app/handlers/__init__.py
+++ b/app/handlers/__init__.py
@@ -1,19 +1,20 @@
-from . import admin_bot
+from aiogram import Dispatcher, Bot
+
from . import base
from . import change_karma
from . import errors
-from . import import_karma
from . import karma
from . import moderator
from . import settings
+from . import superuser
+from ..models.config import Config
+
-__all__ = [
- errors,
- admin_bot,
- base,
- karma,
- change_karma,
- import_karma,
- moderator,
- settings,
-]
+def setup(dp: Dispatcher, bot: Bot, config: Config):
+ errors.setup(dp, bot, config)
+ dp.include_router(base.router)
+ dp.include_router(change_karma.router)
+ dp.include_router(karma.router)
+ dp.include_router(moderator.router)
+ dp.include_router(settings.router)
+ dp.include_router(superuser.setup_superuser(config))
diff --git a/app/handlers/admin_bot.py b/app/handlers/admin_bot.py
deleted file mode 100644
index b34883db..00000000
--- a/app/handlers/admin_bot.py
+++ /dev/null
@@ -1,108 +0,0 @@
-import io
-import json
-
-from aiogram import types
-
-from app.misc import bot, dp
-from app.models.config import Config
-from app.models.db import (
- Chat,
- User,
- UserKarma,
-)
-from app.utils.log import Logger
-from app.utils.send_text_file import send_log_files
-
-logger = Logger(__name__)
-
-
-@dp.message_handler(is_superuser=True, commands='update_log')
-@dp.throttled(rate=30)
-@dp.async_task
-async def get_log(_: types.Message, config: Config):
- await send_log_files(bot, config.log)
-
-
-@dp.message_handler(is_superuser=True, commands='logchat')
-@dp.throttled(rate=30)
-async def get_logchat(message: types.Message, config: Config):
- log_ch = await bot.get_chat(config.log.log_chat_id)
- await message.answer(log_ch.invite_link, disable_notification=True)
-
-
-@dp.message_handler(is_superuser=True, commands='generate_invite_logchat')
-@dp.throttled(rate=120)
-async def generate_logchat_link(message: types.Message, config: Config):
- await message.reply(await bot.export_chat_invite_link(config.log.log_chat_id), disable_notification=True)
-
-
-@dp.message_handler(is_superuser=True, commands=["exception"])
-@dp.throttled(rate=30)
-async def cmd_exception(_: types.Message):
- raise Exception('user press /exception')
-
-
-@dp.message_handler(is_superuser=True, commands='get_out')
-async def leave_chat(message: types.Message):
- await message.bot.leave_chat(message.chat.id)
-
-
-@dp.message_handler(is_superuser=True, commands='dump')
-@dp.throttled(rate=120)
-async def get_dump(_: types.Message, config: Config):
- await send_dump_bd(config)
-
-
-async def send_dump_bd(config: Config):
- with open(config.db.db_path, 'rb') as f:
- await bot.send_document(config.dump_chat_id, f)
-
-
-@dp.message_handler(is_superuser=True, commands='json')
-@dp.throttled(rate=120)
-async def get_dump(_: types.Message, config: Config):
- dct = await UserKarma.all_to_json()
-
- await bot.send_document(
- config.dump_chat_id,
- ("dump.json", io.StringIO(json.dumps(dct, ensure_ascii=False, indent=2)))
- )
-
-
-@dp.message_handler(is_superuser=True, commands='add_manual', commands_prefix='!')
-@dp.throttled(rate=2)
-async def add_manual(message: types.Message, chat: Chat, user: User):
- """
- superuser send !add_manual 46866565 666.13 то change karma of user with id 46866565 to 666.13
- :param message:
- :param chat:
- :param user:
- :return:
- """
- logger.warning("superuser {user} send command !add_manual", user=user.tg_id)
- args = message.text.partition(' ')[2]
- try:
- users_karmas = list(
- map(
- lambda x: (int(x[0]), float(x[1])),
- (uk.split(" ") for uk in args.split('\n'))
- )
- )
- except ValueError:
- return await message.reply(
- "Жду сообщение вида \n!add_manual [user_id karma]\n"
- "user_id должно быть целым числом, а карма числом с плавающей точкой"
- )
- for user_id, karma in users_karmas:
- target_user, _ = await User.get_or_create(tg_id=user_id)
- uk, _ = await UserKarma.get_or_create(user=target_user, chat=chat)
- uk.karma = karma
- await uk.save()
- logger.warning(
- "superuser {user} change manual karma for {target} to new karma {karma} in chat {chat}",
- user=user.tg_id,
- target=target_user.tg_id,
- karma=karma,
- chat=chat.chat_id
- )
- await message.reply("Кармы успешно обновлены", disable_notification=True)
diff --git a/app/handlers/base.py b/app/handlers/base.py
index 6b6e9478..b5cbb330 100644
--- a/app/handlers/base.py
+++ b/app/handlers/base.py
@@ -1,17 +1,17 @@
-from aiogram import types
-from aiogram.dispatcher import FSMContext
+from aiogram import types, F, Router
+from aiogram.filters import Command
+from aiogram.fsm.context import FSMContext
from aiogram.utils.markdown import hpre, hbold
-from app.misc import dp
from app.models.db import Chat
from app.utils.log import Logger
logger = Logger(__name__)
+router = Router(name=__name__)
-@dp.message_handler(commands=["start"], commands_prefix='!/')
-@dp.throttled(rate=3)
+@router.message(Command("start", prefix='!/'))
async def cmd_start(message: types.Message):
logger.info("User {user} start conversation with bot", user=message.from_user.id)
await message.answer(
@@ -22,8 +22,7 @@ async def cmd_start(message: types.Message):
)
-@dp.message_handler(commands=["help"], commands_prefix='!/')
-@dp.throttled(rate=3)
+@router.message(Command("help", prefix='!/'))
async def cmd_help(message: types.Message):
logger.info("User {user} read help in {chat}", user=message.from_user.id, chat=message.chat.id)
await message.reply(
@@ -44,15 +43,13 @@ async def cmd_help(message: types.Message):
)
-@dp.message_handler(commands=["about"], commands_prefix='!')
-@dp.throttled(rate=3)
+@router.message(Command("about", prefix='!'))
async def cmd_about(message: types.Message):
logger.info("User {user} about", user=message.from_user.id)
await message.reply('Исходники по ссылке https://github.com/bomzheg/KarmaBot')
-@dp.message_handler(commands='idchat', commands_prefix='!')
-@dp.throttled(rate=30)
+@router.message(Command('idchat', prefix='!'))
async def get_idchat(message: types.Message):
text = (
f"id этого чата: {hpre(message.chat.id)}\n"
@@ -66,20 +63,19 @@ async def get_idchat(message: types.Message):
await message.reply(text, disable_notification=True)
-@dp.message_handler(state='*', commands='cancel')
-@dp.throttled(rate=3)
+@router.message(Command('cancel'))
async def cancel_state(message: types.Message, state: FSMContext):
current_state = await state.get_state()
if current_state is None:
return
logger.info(f'Cancelling state {current_state}')
# Cancel state and inform user about it
- await state.finish()
+ await state.clear()
# And remove keyboard (just in case)
await message.reply('Диалог прекращён, данные удалены', reply_markup=types.ReplyKeyboardRemove())
-@dp.message_handler(content_types=types.ContentTypes.MIGRATE_TO_CHAT_ID)
+@router.message(F.message.content_types == types.ContentType.MIGRATE_TO_CHAT_ID)
async def chat_migrate(message: types.Message, chat: Chat):
old_id = message.chat.id
new_id = message.migrate_to_chat_id
diff --git a/app/handlers/change_karma.py b/app/handlers/change_karma.py
index 18f39b20..dd2321fa 100644
--- a/app/handlers/change_karma.py
+++ b/app/handlers/change_karma.py
@@ -1,11 +1,9 @@
import asyncio
-import typing
-from aiogram import types
+from aiogram import types, F, Bot, Router
from aiogram.types import ContentType
-from aiogram.utils.markdown import quote_html
+from aiogram.utils.text_decorations import html_decoration as hd
-from app.misc import dp
from app.models.db import Chat, User
from app.services.adaptive_trottle import AdaptiveThrottle
from app.services.change_karma import change_karma, cancel_karma_change
@@ -15,8 +13,10 @@
from app.utils.log import Logger
from . import keyboards as kb
from app.models.config import Config
+from app.filters import HasTargetFilter, KarmaFilter
logger = Logger(__name__)
+router = Router(name=__name__)
a_throttle = AdaptiveThrottle()
@@ -33,7 +33,7 @@ async def too_fast_change_karma(message: types.Message, *_, **__):
return await message.reply("Вы слишком часто меняете карму")
-@dp.message_handler(karma_change=True, has_target=True, content_types=[
+@router.message(HasTargetFilter(), KarmaFilter(), F.content_type.in_([
ContentType.TEXT,
ContentType.STICKER,
@@ -44,10 +44,11 @@ async def too_fast_change_karma(message: types.Message, *_, **__):
ContentType.PHOTO,
ContentType.VIDEO,
ContentType.VOICE,
-])
+]))
@a_throttle.throttled(rate=30, on_throttled=too_fast_change_karma)
-@dp.throttled(rate=1)
-async def karma_change(message: types.Message, karma: dict, user: User, chat: Chat, target: User, config: Config):
+async def karma_change(
+ message: types.Message, karma: dict, user: User, chat: Chat, target: User, config: Config, bot: Bot
+):
try:
result_change_karma = await change_karma(
target_user=target,
@@ -55,7 +56,7 @@ async def karma_change(message: types.Message, karma: dict, user: User, chat: Ch
user=user,
how_change=karma['karma_change'],
comment=karma['comment'],
- bot=message.bot,
+ bot=bot,
)
except SubZeroKarma:
return await message.reply("У Вас слишком мало кармы для этого")
@@ -82,9 +83,9 @@ async def karma_change(message: types.Message, karma: dict, user: User, chat: Ch
msg = await message.reply(
"{actor_name}, Вы {how_change} карму {target_name} до {karma_new:.2f} ({power:+.2f})"
"\n\n{notify_text}".format(
- actor_name=quote_html(user.fullname),
+ actor_name=hd.quote(user.fullname),
how_change=get_how_change_text(karma['karma_change']),
- target_name=quote_html(target.fullname),
+ target_name=hd.quote(target.fullname),
karma_new=result_change_karma.karma_after,
power=result_change_karma.abs_change,
notify_text=notify_text,
@@ -101,15 +102,12 @@ async def karma_change(message: types.Message, karma: dict, user: User, chat: Ch
asyncio.create_task(remove_kb(msg, config.time_to_cancel_actions))
-@dp.callback_query_handler(kb.cb_karma_cancel.filter())
-async def cancel_karma(callback_query: types.CallbackQuery, callback_data: typing.Dict[str, str]):
- user_cancel_id = int(callback_data['user_id'])
- if user_cancel_id != callback_query.from_user.id:
+@router.callback_query(kb.KarmaCancelCb.filter())
+async def cancel_karma(callback_query: types.CallbackQuery, callback_data: kb.KarmaCancelCb, bot: Bot):
+ if callback_data.user_id != callback_query.from_user.id:
return await callback_query.answer("Эта кнопка не для Вас", cache_time=3600)
- karma_event_id = int(callback_data['karma_event_id'])
- rollback_karma = float(callback_data['rollback_karma'])
- moderator_event_id = callback_data['moderator_event_id']
- moderator_event_id = None if moderator_event_id == "null" else int(moderator_event_id)
- await cancel_karma_change(karma_event_id, rollback_karma, moderator_event_id, callback_query.bot)
+ rollback_karma = float(callback_data.rollback_karma)
+ moderator_event_id = None if callback_data.moderator_event_id == "null" else callback_data.moderator_event_id
+ await cancel_karma_change(callback_data.karma_event_id, rollback_karma, moderator_event_id, bot)
await callback_query.answer("Вы отменили изменение кармы", show_alert=True)
await callback_query.message.delete()
diff --git a/app/handlers/errors.py b/app/handlers/errors.py
index e7909d79..df40b1fa 100644
--- a/app/handlers/errors.py
+++ b/app/handlers/errors.py
@@ -1,39 +1,46 @@
-from aiogram import types
-from aiogram.utils.exceptions import CantParseEntities, BadRequest
-from aiogram.utils.markdown import quote_html
+import json
+from functools import partial
-from app.config.main import load_config
-from app.misc import dp, bot
+from aiogram import Dispatcher, Bot
+from aiogram.exceptions import TelegramBadRequest
+from aiogram.types.error_event import ErrorEvent
+from aiogram.utils.text_decorations import html_decoration as hd
+
+from app.models.config import Config
+from app.utils.exceptions import Throttled
from app.utils.log import Logger
logger = Logger(__name__)
-@dp.errors_handler()
-async def errors_handler(update: types.Update, exception: Exception):
+async def errors_handler(error: ErrorEvent, bot: Bot, config: Config):
try:
- raise exception
- except CantParseEntities as e:
- logger.error("Cause exception {e} in update {update}", e=e, update=update)
- return True
- except BadRequest as e:
- if "rights" in e.args[0] and "send" in e.args[0]:
- if update.message and update.message.chat:
- logger.info("bot are muted in chat {chat}", chat=update.message.chat.id)
+ raise error.exception
+ except Throttled:
+ return
+ except TelegramBadRequest as e:
+ if "rights" in e.message and "send" in e.message:
+ if error.update.message and error.update.message.chat:
+ logger.info("bot are muted in chat {chat}", chat=error.update.message.chat.id)
else:
- logger.info("bot can't send message (no rights) in update {update}", update=update)
- return True
+ logger.info("bot can't send message (no rights) in update {update}", update=error.update)
+ return
+ except Exception:
+ pass
logger.exception(
"Cause exception {e} in update {update}",
- e=exception, update=update, exc_info=exception
+ e=error.exception, update=error.update, exc_info=error.exception
)
await bot.send_message(
- load_config().log.log_chat_id,
- f"Получено исключение {quote_html(exception)}\n"
- f"во время обработки апдейта {quote_html(update)}\n"
- f"{quote_html(exception.args)}"
+ config.log.log_chat_id,
+ f"Получено исключение {hd.quote(str(error.exception))}\n"
+ f"во время обработки апдейта {hd.quote(error.update.json(exclude_none=True, ensure_ascii=False))}\n"
+ f"{hd.quote(json.dumps(error.exception.args))}"
)
- return True
+
+
+def setup(dp: Dispatcher, bot: Bot, config: Config):
+ dp.errors.register(partial(errors_handler, bot=bot, config=config))
diff --git a/app/handlers/import_karma.py b/app/handlers/import_karma.py
deleted file mode 100644
index 9cd4cf01..00000000
--- a/app/handlers/import_karma.py
+++ /dev/null
@@ -1,224 +0,0 @@
-import io
-import json
-import typing
-from contextlib import suppress
-from warnings import warn
-
-from aiogram import types
-from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
-from aiogram.utils.callback_data import CallbackData
-from aiogram.utils.exceptions import TelegramAPIError
-from aiogram.utils.markdown import hbold, quote_html
-
-from app.config.main import load_config
-from app.misc import dp, bot
-from app.models.db import (
- Chat,
- User,
- UserKarma,
-)
-from app.services.user_getter import UserGetter
-from app.utils.exceptions import CantImportFromAxenia
-from app.utils.from_axenia import axenia_rating
-
-
-config = load_config()
-type_karmas = typing.Tuple[str, typing.Optional[str], float]
-type_approve_item = typing.Dict[str, typing.Union[str, float, types.User]]
-approve_cb = CallbackData("approve_import", "chat_id", "index", "y_n")
-processing_text = "Сообщение обрабатывается, выполнено ~{:.2%}"
-
-jsons_path = config.app_dir / "jsons"
-jsons_path.mkdir(exist_ok=True, parents=True)
-APPROVE_FILE = jsons_path / "approve.json"
-PROBLEMS_FILE = jsons_path / "problems.json"
-
-
-async def check_chat_creator(message: types.Message) -> bool:
- admins = await message.chat.get_administrators()
- return message.from_user.id in {admin.user.id for admin in admins if admin.status == types.ChatMemberStatus.CREATOR}
-
-
-@dp.message_handler(check_chat_creator, commands="init_from_axenia", commands_prefix='!')
-@dp.throttled(rate=24 * 3600)
-async def init_from_axenia(message: types.Message, chat: Chat):
- msg = await message.reply(processing_text.format(-0.01))
- chat_id = chat.chat_id
- try:
- log_users, to_approve, problems = await process_import_users_karmas(
- await axenia_rating(chat_id), chat, msg
- )
- except CantImportFromAxenia:
- return await message.answer("Что-то пошло не так. вероятно аксения и не знала о существовании этого чата")
- await msg.delete()
-
- await message.answer_document((
- "import_log.json", io.StringIO(json.dumps(log_users, ensure_ascii=False, indent=2))
- ))
-
- await message.reply('Список карм пользователей импортирован из Аксении', disable_web_page_preview=True)
- if problems:
- await send_problems_list(problems, chat.chat_id)
- if to_approve:
- await start_approve_karmas(to_approve, config.dump_chat_id)
-
-
-async def process_import_users_karmas(karmas_list: typing.List[type_karmas], chat: Chat, message: types.Message = None):
- """
-
- :param karmas_list:
- :param chat:
- :param message: by bot in that be output percent of completed
- :return:
- """
- if message is not None and message.from_user.id != bot.id:
- message = None
- warn("message is filled but it not from bot", UserWarning)
-
- log_users = []
- problems = []
- to_approve = []
- async with UserGetter(config.tg_client) as user_getter:
- for i, karma_elem in enumerate(karmas_list):
- if message is not None and i % 5 == 0:
- with suppress(TelegramAPIError):
- await message.edit_text(processing_text.format(i / len(karmas_list)))
-
- name, username, karma = karma_elem
- user = await try_get_user_by_username(username)
- if user is None:
- user_tg = await user_getter.get_user(username, name, chat.chat_id)
- if user_tg is not None:
- log_users.append(dict(source=(name, username, karma), found=user_tg.as_json()))
- user = await User.get_or_create_from_tg_user(user_tg)
-
- if user is not None:
- if username is not None and username == user.username:
- await save_karma(user, chat.chat_id, karma)
- else:
- to_approve.append(
- dict(name=name, username=username, karma=karma, founded_user=user_tg.as_json())
- )
- else:
- problems.append((name, username, karma))
-
- return log_users, to_approve, problems
-
-
-async def send_problems_list(problems: typing.List[type_karmas], chat_id: int):
- problems_users = get_text_problems_users(problems)
- await bot.send_message(chat_id, problems_users, disable_web_page_preview=True)
-
-
-async def try_get_user_by_username(username: typing.Optional[str]) -> typing.Optional[User]:
- if username is None:
- return None
- user = await User.get_or_none(username=username)
- return user
-
-
-async def save_karma(user: User, chat_id: int, karma: float):
- uk, _ = await UserKarma.get_or_create(user=user, chat_id=chat_id)
- uk.karma = karma
- await uk.save()
-
-
-async def start_approve_karmas(to_approve: typing.List[type_approve_item], chat_id: int):
- save_approve_list(to_approve)
- await bot.send_message(chat_id, **next_approve(get_element_approve(0), 0, chat_id))
-
-
-def save_approve_list(to_approve: typing.List[type_approve_item]):
- with APPROVE_FILE.open("w") as f:
- json.dump(to_approve, f)
- with PROBLEMS_FILE.open("w") as f:
- json.dump([], f)
-
-
-def get_element_approve(index: int) -> typing.Optional[type_approve_item]:
- with APPROVE_FILE.open("r") as f:
- to_approve: typing.List[type_approve_item] = json.load(f)
- try:
- return to_approve[index]
- except IndexError:
- return None
-
-
-def next_approve(approve_item: type_approve_item, index: int, chat_id: int):
- if approve_item is None:
- return dict(
- text=(
- "Все сомнительные пользователи проверены.\n"
- f"{get_problems_list()}"
- ),
- reply_markup=None
- )
- else:
- return dict(
- text=(
- f"Данные из Аксении: "
- f"{quote_html(approve_item['name'])} "
- f"@{quote_html(approve_item['username'])} "
- f"{hbold(approve_item['karma'])}\n"
- f"Найденный пользователь {approve_item['founded_user']}"
- ),
- reply_markup=get_kb_approve(index, chat_id)
- )
-
-
-def get_kb_approve(index: int, chat_id: int) -> InlineKeyboardMarkup:
- return InlineKeyboardMarkup(inline_keyboard=[[
- InlineKeyboardButton(
- text="Да", callback_data=approve_cb.new(index=index, chat_id=chat_id, y_n="yes")
- ),
- InlineKeyboardButton(
- text="Нет", callback_data=approve_cb.new(index=index, chat_id=chat_id, y_n="no_one")
- )
- ]])
-
-
-@dp.callback_query_handler(approve_cb.filter(y_n="no_one"), is_superuser=True)
-@dp.throttled(rate=3)
-async def not_save_user_karma(callback_query: types.CallbackQuery, callback_data: typing.Dict[str, str]):
- await callback_query.answer()
- index = int(callback_data["index"])
- chat_id = int(callback_data["chat_id"])
- elem = get_element_approve(index)
-
- save_problems_list((elem['name'], elem['username'], elem['karma']))
- await callback_query.message.edit_text(**next_approve(get_element_approve(index + 1), index + 1, chat_id))
-
-
-@dp.callback_query_handler(approve_cb.filter(y_n="yes"), is_superuser=True)
-@dp.throttled(rate=3)
-async def save_user_karma(callback_query: types.CallbackQuery, callback_data: typing.Dict[str, str]):
- await callback_query.answer()
- index = int(callback_data["index"])
- chat_id = int(callback_data["chat_id"])
- elem = get_element_approve(index)
- user_tg = types.User(**json.loads(elem['founded_user']))
-
- user = await User.get_or_create_from_tg_user(user_tg)
- await save_karma(user, chat_id, elem['karma'])
- await callback_query.message.edit_text(**next_approve(get_element_approve(index + 1), index + 1, chat_id))
-
-
-def save_problems_list(problem: type_karmas):
- with PROBLEMS_FILE.open("r") as f:
- problems = json.load(f)
- problems.append(problem)
- with PROBLEMS_FILE.open("w") as f:
- json.dump(problems, f)
-
-
-def get_problems_list() -> str:
- with PROBLEMS_FILE.open("r") as f:
- problems = json.load(f)
- return get_text_problems_users(problems)
-
-
-def get_text_problems_users(problems: typing.List[type_karmas]) -> str:
- problems_users = "Список пользователей с проблемами:"
- for name, username, karma in problems:
- problems_users += f"\n{quote_html(name)} @{quote_html(username)} {hbold(karma)}"
- return problems_users
diff --git a/app/handlers/karma.py b/app/handlers/karma.py
index b917da84..33a300a2 100644
--- a/app/handlers/karma.py
+++ b/app/handlers/karma.py
@@ -1,10 +1,9 @@
import asyncio
-from aiogram import types
-from aiogram.types import ChatType
-from aiogram.utils.markdown import hpre
+from aiogram import types, F, Router
+from aiogram.filters import Command
+from aiogram.utils.text_decorations import html_decoration as hd
-from app.misc import dp
from app.models.config import Config
from app.models.db import (
Chat,
@@ -20,10 +19,10 @@
logger = Logger(__name__)
+router = Router(name=__name__)
-@dp.message_handler(commands=["top"], commands_prefix='!', chat_type=types.ChatType.PRIVATE)
-@dp.throttled(rate=2)
+@router.message(Command("top", prefix='!'), F.chat.type == "private")
async def get_top_from_private(message: types.Message, user: User):
parts = message.text.split(maxsplit=1)
if len(parts) > 1:
@@ -32,7 +31,7 @@ async def get_top_from_private(message: types.Message, user: User):
return await message.reply(
"Эту команду можно использовать только в группах "
"или с указанием id нужного чата, например:"
- "\n" + hpre("!top -1001399056118")
+ "\n" + hd.code("!top -1001399056118")
)
logger.info("user {user} ask top karma of chat {chat}", user=user.tg_id, chat=chat.chat_id)
text = await get_karma_top(chat, user)
@@ -40,8 +39,7 @@ async def get_top_from_private(message: types.Message, user: User):
await message.reply(text, disable_web_page_preview=True)
-@dp.message_handler(commands=["top"], commands_prefix='!')
-@dp.throttled(rate=60 * 5)
+@router.message(Command("top", prefix='!'))
async def get_top(message: types.Message, chat: Chat, user: User):
logger.info("user {user} ask top karma of chat {chat}", user=user.tg_id, chat=chat.chat_id)
text = await get_karma_top(chat, user)
@@ -49,8 +47,7 @@ async def get_top(message: types.Message, chat: Chat, user: User):
await message.reply(text, disable_web_page_preview=True)
-@dp.message_handler(chat_type=[ChatType.GROUP, ChatType.SUPERGROUP], commands=["me"], commands_prefix='!')
-@dp.throttled(rate=15)
+@router.message(F.chat.type.in_(["group", "supergroup"]), Command("me", prefix='!'))
async def get_top(message: types.Message, chat: Chat, user: User, config: Config):
logger.info("user {user} ask his karma in chat {chat}", user=user.tg_id, chat=chat.chat_id)
uk, number_in_top = await get_me_chat_info(chat=chat, user=user)
@@ -62,8 +59,7 @@ async def get_top(message: types.Message, chat: Chat, user: User, config: Config
asyncio.create_task(delete_message(message, config.time_to_remove_temp_messages))
-@dp.message_handler(chat_type=ChatType.PRIVATE, commands=["me"], commands_prefix='!')
-@dp.throttled(rate=15)
+@router.message(F.chat.type == "private", Command("me", prefix='!'))
async def get_top(message: types.Message, user: User):
logger.info("user {user} ask his karma", user=user.tg_id)
uks = await get_me_info(user)
diff --git a/app/handlers/keyboards.py b/app/handlers/keyboards.py
index 5336678f..957af462 100644
--- a/app/handlers/keyboards.py
+++ b/app/handlers/keyboards.py
@@ -1,19 +1,25 @@
+from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
-from aiogram.utils.callback_data import CallbackData
from app.models.db import User, KarmaEvent, ModeratorEvent
-cb_karma_cancel = CallbackData("karma_cancel", "user_id", "karma_event_id", "rollback_karma", "moderator_event_id")
+
+class KarmaCancelCb(CallbackData, prefix="karma_cancel"):
+ user_id: int
+ karma_event_id: int
+ rollback_karma: str
+ moderator_event_id: int | str
def get_kb_karma_cancel(
user: User, karma_event: KarmaEvent, rollback_karma: float, moderator_event: ModeratorEvent
) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(
- "Отменить", callback_data=cb_karma_cancel.new(
+ text="Отменить",
+ callback_data=KarmaCancelCb(
user_id=user.tg_id,
karma_event_id=karma_event.id_,
rollback_karma=f"{rollback_karma:.2f}",
moderator_event_id=moderator_event.id_ if moderator_event is not None else "null",
- )
+ ).pack()
)]])
diff --git a/app/handlers/moderator.py b/app/handlers/moderator.py
index 983f366a..e73ceb7c 100644
--- a/app/handlers/moderator.py
+++ b/app/handlers/moderator.py
@@ -1,8 +1,9 @@
-from aiogram import types
-from aiogram.utils.exceptions import Unauthorized
-from aiogram.utils.markdown import hide_link, quote_html
+from aiogram import types, F, Bot, Router
+from aiogram.exceptions import TelegramUnauthorizedError
+from aiogram.filters import Command
+from aiogram.utils.text_decorations import html_decoration as hd
-from app.misc import dp, bot
+from app.filters import HasTargetFilter, HasPermissions, BotHasPermissions
from app.models.config import Config
from app.models.db import Chat, User
from app.services.moderation import warn_user, ro_user, ban_user, get_duration
@@ -13,84 +14,80 @@
logger = Logger(__name__)
+router = Router(name=__name__)
-@dp.message_handler(
- chat_type=[types.ChatType.GROUP, types.ChatType.SUPERGROUP],
- is_reply=True,
- commands=['report', 'admin', 'spam'],
- commands_prefix="/!@",
+@router.message(
+ F.chat.type.in_(["group", "supergroup"]),
+ F.reply_to_message,
+ Command('report', 'admin', 'spam', prefix="/!@"),
)
-@dp.throttled(rate=5)
-async def report(message: types.Message):
+async def report(message: types.Message, bot: Bot):
logger.info("user {user} report for message {message}", user=message.from_user.id, message=message.message_id)
answer_template = "Спасибо за сообщение. Мы обязательно разберёмся. "
- admins_mention = await get_mentions_admins(message.chat)
+ admins_mention = await get_mentions_admins(message.chat, bot)
await message.reply(answer_template + admins_mention + " ")
-async def get_mentions_admins(chat: types.Chat):
- admins = await chat.get_administrators()
+async def get_mentions_admins(chat: types.Chat, bot: Bot):
+ admins = await bot.get_chat_administrators(chat.id)
admins_mention = ""
for admin in admins:
if admin.user.is_bot:
continue
if need_notify_admin(admin):
- admins_mention += hide_link(admin.user.url)
+ admins_mention += hd.link("", admin.user.url)
return admins_mention
def need_notify_admin(admin: types.ChatMemberAdministrator | types.ChatMemberOwner):
- return admin.can_delete_messages or admin.can_restrict_members or admin.status == types.ChatMemberStatus.CREATOR
+ return admin.can_delete_messages or admin.can_restrict_members or admin.status == "creator"
-@dp.message_handler(
- commands=["ro", "mute"],
- commands_prefix="!",
- has_target=True,
- user_can_restrict_members=True,
- bot_can_restrict_members=True,
+@router.message(
+ HasTargetFilter(),
+ Command(commands=["ro", "mute"], prefix="!"),
+ HasPermissions(can_restrict_members=True),
+ BotHasPermissions(can_restrict_members=True),
)
-async def cmd_ro(message: types.Message, user: User, target: User, chat: Chat):
+async def cmd_ro(message: types.Message, user: User, target: User, chat: Chat, bot: Bot):
try:
duration, comment = get_duration(message.text)
except TimedeltaParseError as e:
- return await message.reply(f"Не могу распознать время. {quote_html(e.text)}")
+ return await message.reply(f"Не могу распознать время. {hd.quote(e.text)}")
try:
- success_text = await ro_user(chat, target, user, duration, comment, message.bot)
+ success_text = await ro_user(chat, target, user, duration, comment, bot)
except ModerationError as e:
logger.error("Failed to restrict chat member: {error!r}", error=e)
else:
await message.reply(success_text)
-@dp.message_handler(
- commands=["ban"],
- commands_prefix="!",
- has_target=True,
- user_can_restrict_members=True,
- bot_can_restrict_members=True,
+@router.message(
+ HasTargetFilter(),
+ Command(commands=["ban"], prefix="!"),
+ HasPermissions(can_restrict_members=True),
+ BotHasPermissions(can_restrict_members=True),
)
-async def cmd_ban(message: types.Message, user: User, target: User, chat: Chat):
+async def cmd_ban(message: types.Message, user: User, target: User, chat: Chat, bot: Bot):
try:
duration, comment = get_duration(message.text)
except TimedeltaParseError as e:
- return await message.reply(f"Не могу распознать время. {quote_html(e.text)}")
+ return await message.reply(f"Не могу распознать время. {hd.quote(e.text)}")
try:
- success_text = await ban_user(chat, target, user, duration, comment, message.bot)
+ success_text = await ban_user(chat, target, user, duration, comment, bot)
except ModerationError as e:
logger.error("Failed to kick chat member: {error!r}", error=e, exc_info=e)
else:
await message.reply(success_text)
-@dp.message_handler(
- commands=["w", "warn"],
- commands_prefix="!",
- has_target=True,
- user_can_restrict_members=True,
+@router.message(
+ HasTargetFilter(),
+ Command(commands=["w", "warn"], prefix="!"),
+ HasPermissions(can_restrict_members=True),
)
async def cmd_warn(message: types.Message, chat: Chat, target: User, user: User):
args = message.text.split(maxsplit=1)
@@ -109,8 +106,8 @@ async def cmd_warn(message: types.Message, chat: Chat, target: User, user: User)
await message.reply(text)
-@dp.message_handler(commands="info", commands_prefix='!', has_target=dict(can_be_same=True))
-async def get_info_about_user(message: types.Message, chat: Chat, target: User, config: Config):
+@router.message(HasTargetFilter(can_be_same=True), Command("info", prefix='!'))
+async def get_info_about_user(message: types.Message, chat: Chat, target: User, config: Config, bot: Bot):
info = await get_user_info(target, chat, config.date_format)
target_karma = await target.get_karma(chat)
if target_karma is None:
@@ -122,31 +119,29 @@ async def get_info_about_user(message: types.Message, chat: Chat, target: User,
information,
disable_web_page_preview=True
)
- except Unauthorized:
- me = await bot.me
+ except TelegramUnauthorizedError:
+ me = await bot.me()
await message.reply(
- f'{message.from_user.get_mention()}, напишите мне в личку '
+ f'{message.from_user.mention_html()}, напишите мне в личку '
f'/start и повторите команду.'
)
finally:
await delete_message(message)
-@dp.message_handler(
- commands=["ro", "mute", "ban"],
- commands_prefix="!",
- has_target=True,
- user_can_restrict_members=True,
+@router.message(
+ HasTargetFilter(),
+ Command(commands=["ro", "mute", "ban"], prefix="!"),
+ HasPermissions(can_restrict_members=True),
)
async def cmd_ro_bot_not_admin(message: types.Message):
"""бот без прав модератора"""
await message.reply("Чтобы я выполнял функции модератора, дайте мне соответствующие права")
-@dp.message_handler(
- commands=["ro", "mute", "ban", "warn"],
- commands_prefix="!",
- bot_can_delete_messages=True,
+@router.message(
+ Command(commands=["ro", "mute", "ban", "warn", "w"], prefix="!"),
+ BotHasPermissions(can_delete_messages=True),
)
async def cmd_ro_bot_not_admin(message: types.Message):
"""юзер без прав модератора"""
diff --git a/app/handlers/settings.py b/app/handlers/settings.py
index bad624b0..ac9f6b30 100644
--- a/app/handlers/settings.py
+++ b/app/handlers/settings.py
@@ -1,21 +1,24 @@
-from aiogram import types
+from aiogram import types, Router
+from aiogram.filters import Command
-from app.misc import dp
+from app.filters import HasPermissions
from app.models.db import Chat
from app.services.settings import enable_karmic_restriction, disable_karmic_restriction, get_settings_card, \
enable_karma_counting, disable_karma_counting
-@dp.message_handler(commands="settings", commands_prefix="!/")
+router = Router(name=__name__)
+
+
+@router.message(Command(commands="settings", prefix="!/"))
async def get_settings(message: types.Message, chat: Chat):
settings_card = await get_settings_card(chat)
await message.answer(settings_card)
-@dp.message_handler(
- commands="enable_karmic_ro",
- user_can_restrict_members=True,
- commands_prefix='!/',
+@router.message(
+ Command("enable_karmic_ro", prefix='!/'),
+ HasPermissions(can_restrict_members=True),
)
async def enable_karmic_ro_cmd(message: types.Message, chat: Chat):
await enable_karmic_restriction(chat)
@@ -30,20 +33,18 @@ async def enable_karmic_ro_cmd(message: types.Message, chat: Chat):
)
-@dp.message_handler(
- commands="disable_karmic_ro",
- user_can_restrict_members=True,
- commands_prefix='!/',
+@router.message(
+ Command("disable_karmic_ro", prefix='!/'),
+ HasPermissions(can_restrict_members=True),
)
async def disable_karmic_ro_cmd(message: types.Message, chat: Chat):
await disable_karmic_restriction(chat)
await message.reply("Кармобаны отключены")
-@dp.message_handler(
- commands="enable_karma",
- user_can_delete_messages=True,
- commands_prefix='!/',
+@router.message(
+ Command("enable_karma", prefix='!/'),
+ HasPermissions(can_delete_messages=True),
)
async def enable_karma(message: types.Message, chat: Chat):
await enable_karma_counting(chat)
@@ -52,10 +53,9 @@ async def enable_karma(message: types.Message, chat: Chat):
)
-@dp.message_handler(
- commands="disable_karma",
- user_can_delete_messages=True,
- commands_prefix='!/',
+@router.message(
+ Command("disable_karma", prefix='!/'),
+ HasPermissions(can_delete_messages=True),
)
async def disable_karma(message: types.Message, chat: Chat):
await disable_karma_counting(chat)
diff --git a/app/handlers/superuser.py b/app/handlers/superuser.py
new file mode 100644
index 00000000..cdb16c37
--- /dev/null
+++ b/app/handlers/superuser.py
@@ -0,0 +1,37 @@
+from typing import Iterable
+
+from aiogram import Bot, Router
+from aiogram.filters import Command
+from aiogram.types import Message, BufferedInputFile
+from functools import partial
+
+from app.models.config import Config
+
+
+async def is_superuser(message: Message, superusers: Iterable[int]) -> bool:
+ return message.from_user.id in superusers
+
+
+async def exception(message: Message):
+ raise RuntimeError(message.text)
+
+
+async def leave_chat(message: Message, bot: Bot):
+ await bot.leave_chat(message.chat.id)
+
+
+async def get_dump(_: Message, config: Config, bot: Bot):
+ with open(config.db.db_path, 'rb') as f:
+ await bot.send_document(config.dump_chat_id, BufferedInputFile(f.read(), "karma.db"))
+
+
+def setup_superuser(bot_config: Config) -> Router:
+ router = Router(name=__name__)
+ is_superuser_ = partial(is_superuser, superusers=bot_config.superusers)
+ router.message.filter(is_superuser_)
+
+ router.message.register(exception, Command(commands="exception"))
+ router.message.register(leave_chat, Command(commands="get_out"))
+ router.message.register(get_dump, Command(commands="dump"))
+
+ return router
diff --git a/app/middlewares/__init__.py b/app/middlewares/__init__.py
index 635481aa..26825e13 100644
--- a/app/middlewares/__init__.py
+++ b/app/middlewares/__init__.py
@@ -1,18 +1,22 @@
# partially from https://github.com/aiogram/bot
from aiogram import Dispatcher
-from aiogram.contrib.middlewares.logging import LoggingMiddleware
from app.middlewares.config_middleware import ConfigMiddleware
from app.middlewares.db_middleware import DBMiddleware
+from app.middlewares.fix_target_middleware import FixTargetMiddleware
from app.models.config import Config
+from app.utils.lock_factory import LockFactory
from app.utils.log import Logger
logger = Logger(__name__)
-def setup(dispatcher: Dispatcher, config: Config):
+def setup(dispatcher: Dispatcher, lock_factory: LockFactory, config: Config):
logger.info("Configure middlewares...")
- dispatcher.middleware.setup(DBMiddleware(tg_client_config=config.tg_client))
- dispatcher.middleware.setup(ConfigMiddleware(config))
- dispatcher.middleware.setup(LoggingMiddleware())
+ db_middleware_ = DBMiddleware(lock_factory)
+ dispatcher.update.outer_middleware.register(ConfigMiddleware(config))
+ dispatcher.errors.outer_middleware.register(ConfigMiddleware(config))
+ dispatcher.message.outer_middleware.register(db_middleware_)
+ dispatcher.callback_query.outer_middleware.register(db_middleware_)
+ dispatcher.message.middleware.register(FixTargetMiddleware(tg_client_config=config.tg_client))
diff --git a/app/middlewares/config_middleware.py b/app/middlewares/config_middleware.py
index 121bd321..2bbded1a 100644
--- a/app/middlewares/config_middleware.py
+++ b/app/middlewares/config_middleware.py
@@ -1,4 +1,6 @@
-from aiogram.dispatcher.middlewares import LifetimeControllerMiddleware
+from typing import Callable, Dict, Any, Awaitable
+
+from aiogram import BaseMiddleware
from aiogram.types.base import TelegramObject
from app.models.config import Config
@@ -8,10 +10,13 @@
logger = Logger(__name__)
-class ConfigMiddleware(LifetimeControllerMiddleware):
+class ConfigMiddleware(BaseMiddleware):
def __init__(self, config: Config):
super(ConfigMiddleware, self).__init__()
self.config = config
- async def pre_process(self, obj: TelegramObject, data: dict, *args):
+ async def __call__(self, handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], event: TelegramObject,
+ data: Dict[str, Any]) -> Any:
data["config"]: Config = self.config
+ return await handler(event, data)
+
diff --git a/app/middlewares/db_middleware.py b/app/middlewares/db_middleware.py
index 9a87b819..8cfe1b10 100644
--- a/app/middlewares/db_middleware.py
+++ b/app/middlewares/db_middleware.py
@@ -1,14 +1,12 @@
# partially from https://github.com/aiogram/bot
-from typing import Optional
+from typing import Optional, Callable, Dict, Any, Awaitable
-from aiogram import types
-from aiogram.dispatcher.handler import CancelHandler
-from aiogram.dispatcher.middlewares import BaseMiddleware
+from aiogram import types, BaseMiddleware
+from aiogram.dispatcher.event.bases import CancelHandler
+from aiogram.types import TelegramObject
-from app.models.config import TgClientConfig
from app.models.db import Chat, User
-from app.services.find_target_user import get_db_user_by_tg_user
from app.services.settings import get_chat_settings
from app.utils.lock_factory import LockFactory
from app.utils.log import Logger
@@ -18,17 +16,29 @@
class DBMiddleware(BaseMiddleware):
- def __init__(self, tg_client_config: TgClientConfig):
+ def __init__(self, lock_factory: LockFactory):
super(DBMiddleware, self).__init__()
- self.lock_factory = LockFactory()
- self.tg_client_config = tg_client_config
+ self.lock_factory = lock_factory
+
+ async def __call__(
+ self,
+ handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
+ event: TelegramObject,
+ data: Dict[str, Any]
+ ) -> Any:
+ chat: types.Chat = data.get("event_chat", None)
+ user: types.User = data.get("event_from_user", None)
+ if isinstance(event, types.Message) and event.sender_chat:
+ raise CancelHandler
+ await self.setup_chat(data, user, chat)
+ return await handler(event, data)
async def setup_chat(self, data: dict, user: types.User, chat: Optional[types.Chat] = None):
try:
- async with self.lock_factory.get_lock(f"{user.id}"):
+ async with self.lock_factory.get_lock(user.id):
user = await User.get_or_create_from_tg_user(user)
if chat and chat.type != 'private':
- async with self.lock_factory.get_lock(f"{chat.id}"):
+ async with self.lock_factory.get_lock(chat.id):
chat = await Chat.get_or_create_from_tg_chat(chat)
data["chat_settings"] = await get_chat_settings(chat=chat)
@@ -37,22 +47,3 @@ async def setup_chat(self, data: dict, user: types.User, chat: Optional[types.Ch
raise e
data["user"] = user
data["chat"] = chat
-
- async def fix_target(self, data: dict):
- try:
- target: types.User = data['target']
- except KeyError:
- return
- target = await get_db_user_by_tg_user(target, self.tg_client_config)
- data['target'] = target
-
- async def on_pre_process_message(self, message: types.Message, data: dict):
- if message.sender_chat:
- raise CancelHandler
- await self.setup_chat(data, message.from_user, message.chat)
-
- async def on_process_message(self, _: types.Message, data: dict):
- await self.fix_target(data)
-
- async def on_pre_process_callback_query(self, query: types.CallbackQuery, data: dict):
- await self.setup_chat(data, query.from_user, query.message.chat if query.message else None)
diff --git a/app/middlewares/fix_target_middleware.py b/app/middlewares/fix_target_middleware.py
new file mode 100644
index 00000000..e61c9c51
--- /dev/null
+++ b/app/middlewares/fix_target_middleware.py
@@ -0,0 +1,24 @@
+from typing import Callable, Any, Awaitable
+
+from aiogram import BaseMiddleware
+from aiogram.types import TelegramObject
+
+from app.models.config import TgClientConfig
+from app.services.find_target_user import get_db_user_by_tg_user
+
+
+class FixTargetMiddleware(BaseMiddleware):
+ def __init__(self, tg_client_config: TgClientConfig):
+ super(FixTargetMiddleware, self).__init__()
+ self.tg_client_config = tg_client_config
+
+ async def __call__(
+ self,
+ handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
+ event: TelegramObject,
+ data: dict[str, Any],
+ ) -> Any:
+ if target := data.get("target", None):
+ target = await get_db_user_by_tg_user(target, self.tg_client_config)
+ data['target'] = target
+ return await handler(event, data)
diff --git a/app/misc.py b/app/misc.py
deleted file mode 100644
index f49e2cf5..00000000
--- a/app/misc.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# partially from https://github.com/aiogram/bot
-from aiogram import Bot, Dispatcher, types
-
-from app.config import load_config
-from app.models.config import Config
-from app.utils.log import Logger
-
-
-logger = Logger(__name__)
-current_config = load_config()
-
-bot = Bot(current_config.bot_token, parse_mode=types.ParseMode.HTML)
-dp = Dispatcher(bot, storage=current_config.storage.create_storage())
-
-
-def setup(config: Config):
- from app import filters
- from app import middlewares
- from app.utils import executor
- logger.debug(f"As application dir using: {config.app_dir}")
-
- middlewares.setup(dp, config)
- filters.setup(dp, config)
- executor.setup(config)
-
- logger.info("Configure handlers...")
- # noinspection PyUnresolvedReferences
- import app.handlers
diff --git a/app/models/config/storage.py b/app/models/config/storage.py
index 97348dac..5931df92 100644
--- a/app/models/config/storage.py
+++ b/app/models/config/storage.py
@@ -3,10 +3,14 @@
from enum import Enum
-from aiogram.contrib.fsm_storage.memory import MemoryStorage
-from aiogram.contrib.fsm_storage.redis import RedisStorage2
-from aiogram.dispatcher.storage import BaseStorage
-from loguru import logger
+from aiogram.fsm.storage.base import BaseStorage
+from aiogram.fsm.storage.memory import MemoryStorage
+from aiogram.fsm.storage.redis import RedisStorage
+from redis.asyncio.client import Redis
+
+from app.utils.log import Logger
+
+logger = Logger(__name__)
class StorageType(Enum):
@@ -36,7 +40,7 @@ class RedisConfig:
port: int = 6379
db: int = 1
- def create_redis_storage(self) -> RedisStorage2:
+ def create_redis_storage(self) -> RedisStorage:
logger.info("created storage for {self}", self=self)
- return RedisStorage2(host=self.url, port=self.port, db=self.db)
+ return RedisStorage(Redis(host=self.url, port=self.port, db=self.db))
diff --git a/app/models/db/chat.py b/app/models/db/chat.py
index de8791ba..3708979a 100644
--- a/app/models/db/chat.py
+++ b/app/models/db/chat.py
@@ -1,7 +1,7 @@
import typing
from enum import Enum
-from aiogram.utils.markdown import hlink, quote_html
+from aiogram.utils.text_decorations import html_decoration as hd
from tortoise import fields
from tortoise.exceptions import DoesNotExist
from tortoise.models import Model
@@ -66,7 +66,7 @@ async def get_or_create_from_tg_chat(cls, chat):
@property
def mention(self):
- return hlink(self.title, f"t.me/{self.username}") if self.username else quote_html(self.title)
+ return hd.link(self.title, f"t.me/{self.username}") if self.username else hd.quote(self.title)
def __str__(self):
rez = f"Chat with type: {self.type_} with ID {self.chat_id}, title: {self.title}"
diff --git a/app/models/db/db.py b/app/models/db/db.py
index c0c00c82..3a101c0f 100644
--- a/app/models/db/db.py
+++ b/app/models/db/db.py
@@ -1,7 +1,3 @@
-from functools import partial
-
-from aiogram import Dispatcher
-from aiogram.utils.executor import Executor
from tortoise import Tortoise, run_async
from app.models.config import DBConfig
@@ -13,10 +9,6 @@
karma_filters = ("-karma", "uc_id")
-async def on_startup(_: Dispatcher, db_config: DBConfig):
- await db_init(db_config)
-
-
async def db_init(db_config: DBConfig):
db_url = db_config.create_url_config()
logger.info("connecting to db {db_url}", db_url=db_url)
@@ -26,15 +18,10 @@ async def db_init(db_config: DBConfig):
)
-async def on_shutdown(_: Dispatcher):
+async def on_shutdown():
await Tortoise.close_connections()
-def setup(executor: Executor, db_config: DBConfig):
- executor.on_startup(partial(on_startup, db_config=db_config))
- executor.on_shutdown(on_shutdown)
-
-
async def generate_schemas_db(db_config: DBConfig):
await db_init(db_config)
await Tortoise.generate_schemas()
diff --git a/app/models/db/karma_actions.py b/app/models/db/karma_actions.py
index 44aedffa..6b39b0fc 100644
--- a/app/models/db/karma_actions.py
+++ b/app/models/db/karma_actions.py
@@ -1,4 +1,4 @@
-from aiogram.utils.markdown import quote_html
+from aiogram.utils.text_decorations import html_decoration as hd
from tortoise import fields
from tortoise.models import Model
@@ -48,7 +48,7 @@ def format_event(self, date_format: str):
f"{self.how_change_absolute:.2f} ({self.how_change:.0%} своей силы.) "
)
if self.comment:
- rez += f'"{quote_html(self.comment)}"'
+ rez += f'"{hd.quote(self.comment)}"'
return rez
diff --git a/app/models/db/moderator_actions.py b/app/models/db/moderator_actions.py
index e09f2df6..0489e7ed 100644
--- a/app/models/db/moderator_actions.py
+++ b/app/models/db/moderator_actions.py
@@ -1,6 +1,6 @@
from datetime import timedelta
-from aiogram.utils.markdown import quote_html
+from aiogram.utils.text_decorations import html_decoration as hd
from tortoise import fields
from tortoise.models import Model
@@ -50,7 +50,7 @@ async def save_new_action(
chat=chat,
type_restriction=type_restriction,
timedelta_restriction=duration,
- comment=comment
+ comment=comment[:200]
)
await moderator_event.save(using_db=using_db)
return moderator_event
@@ -72,5 +72,5 @@ def format_event(self, date_format: str) -> str:
rez += f"от {self.moderator.mention_no_link}"
if self.comment:
- rez += f" \"{quote_html(self.comment)}\""
+ rez += f" \"{hd.quote(self.comment)}\""
return rez
diff --git a/app/models/db/user.py b/app/models/db/user.py
index c39172ec..3f3c0137 100644
--- a/app/models/db/user.py
+++ b/app/models/db/user.py
@@ -1,7 +1,7 @@
from datetime import datetime
from aiogram import types
-from aiogram.utils.markdown import hlink, quote_html
+from aiogram.utils.text_decorations import html_decoration as hd
from tortoise import fields
from tortoise.exceptions import DoesNotExist
from tortoise.models import Model
@@ -9,6 +9,7 @@
from app.models.common import TypeRestriction
from app.utils.exceptions import UserWithoutUserIdError
from .chat import Chat
+from .. import dto
class User(Model):
@@ -66,7 +67,7 @@ async def update_user_data(self, user_tg):
await self.save()
@classmethod
- async def get_or_create_from_tg_user(cls, user_tg: types.User):
+ async def get_or_create_from_tg_user(cls, user_tg: types.User | dto.TargetUser):
if user_tg.id is None:
try:
return await cls.get(username__iexact=user_tg.username)
@@ -83,14 +84,14 @@ async def get_or_create_from_tg_user(cls, user_tg: types.User):
@property
def mention_link(self):
- return hlink(self.fullname, f"tg://user?id={self.tg_id}")
+ return hd.link(self.fullname, f"tg://user?id={self.tg_id}")
@property
def mention_no_link(self):
if self.username:
- rez = hlink(self.fullname, f"t.me/{self.username}")
+ rez = hd.link(self.fullname, f"t.me/{self.username}")
else:
- rez = quote_html(self.fullname)
+ rez = hd.quote(self.fullname)
return rez
@property
diff --git a/app/models/dto/__init__.py b/app/models/dto/__init__.py
new file mode 100644
index 00000000..c396fa19
--- /dev/null
+++ b/app/models/dto/__init__.py
@@ -0,0 +1 @@
+from .user import TargetUser # noqa: F401
diff --git a/app/models/dto/user.py b/app/models/dto/user.py
new file mode 100644
index 00000000..f0f7e9e0
--- /dev/null
+++ b/app/models/dto/user.py
@@ -0,0 +1,22 @@
+from dataclasses import dataclass
+
+from aiogram import types
+
+
+@dataclass
+class TargetUser:
+ id: int | None = None
+ username: str | None = None
+ first_name: str | None = None
+ last_name: str | None = None
+ is_bot: bool | None = None
+
+ @classmethod
+ def from_aiogram(cls, user: types.User):
+ return cls(
+ id=user.id,
+ username=user.username,
+ first_name=user.first_name,
+ last_name=user.last_name,
+ is_bot=user.is_bot,
+ )
diff --git a/app/services/adaptive_trottle.py b/app/services/adaptive_trottle.py
index 6ce94d9e..330ff8e8 100644
--- a/app/services/adaptive_trottle.py
+++ b/app/services/adaptive_trottle.py
@@ -4,9 +4,9 @@
from datetime import datetime, timedelta
from aiogram import types
-from aiogram.utils.exceptions import Throttled
from app.models.db import User, Chat
+from app.utils.exceptions import Throttled
from app.utils.log import Logger
@@ -95,4 +95,9 @@ async def process_on_throttled(
else:
chat: Chat = kwargs['chat']
user: User = kwargs['user']
- raise Throttled(key=key, rate=rate, chat_id=chat.chat_id, user_id=user.tg_id)
+ raise Throttled(
+ key=key,
+ rate=rate,
+ chat_id=chat.chat_id,
+ user_id=user.tg_id,
+ )
diff --git a/app/services/change_karma.py b/app/services/change_karma.py
index 5bf5f087..f1f75775 100644
--- a/app/services/change_karma.py
+++ b/app/services/change_karma.py
@@ -1,4 +1,5 @@
from aiogram import Bot
+from aiogram.types import ChatPermissions
from tortoise.transactions import in_transaction
from app.config.main import load_config
@@ -49,7 +50,7 @@ async def change_karma(user: User, target_user: User, chat: Chat, how_change: fl
chat=chat,
how_change=relative_change,
how_change_absolute=abs_change,
- comment=comment,
+ comment=comment[:200],
)
await ke.save(using_db=conn)
logger.info(
@@ -111,10 +112,12 @@ async def cancel_karma_change(karma_event_id: int, rollback_karma: float, modera
await bot.restrict_chat_member(
chat_id=chat_id,
user_id=restricted_user.tg_id,
- can_send_messages=True,
- can_send_media_messages=True,
- can_add_web_page_previews=True,
- can_send_other_messages=True,
+ permissions=ChatPermissions(
+ can_send_messages=True,
+ can_send_media_messages=True,
+ can_add_web_page_previews=True,
+ can_send_other_messages=True,
+ )
)
elif moderator_event.type_restriction == TypeRestriction.karmic_ban.name:
diff --git a/app/services/find_target_user.py b/app/services/find_target_user.py
index f7469cbc..8767e911 100644
--- a/app/services/find_target_user.py
+++ b/app/services/find_target_user.py
@@ -1,10 +1,10 @@
-import typing
from contextlib import suppress
from aiogram import types
from pyrogram.errors import UsernameNotOccupied
from tortoise.exceptions import MultipleObjectsReturned
+from app.models import dto
from app.models.config import TgClientConfig
from app.models.db import User
from app.services.user_getter import UserGetter
@@ -15,7 +15,7 @@
logger = Logger(__name__)
-def get_target_user(message: types.Message, can_be_same=False, can_be_bot=False) -> typing.Optional[types.user.User]:
+def get_target_user(message: types.Message, can_be_same=False, can_be_bot=False) -> dto.TargetUser | None:
"""
Target user can be take from reply or by mention
:param message:
@@ -23,7 +23,7 @@ def get_target_user(message: types.Message, can_be_same=False, can_be_bot=False)
:param can_be_bot:
:return:
"""
- author_user = message.from_user
+ author_user = dto.TargetUser.from_aiogram(message.from_user)
target_user = get_replied_user(message)
if has_target_user(target_user, author_user, can_be_same, can_be_bot):
@@ -40,7 +40,12 @@ def get_target_user(message: types.Message, can_be_same=False, can_be_bot=False)
return None
-def has_target_user(target_user: types.User, author_user: types.User, can_be_same, can_be_bot):
+def has_target_user(
+ target_user: dto.TargetUser,
+ author_user: dto.TargetUser,
+ can_be_same: bool,
+ can_be_bot: bool,
+) -> bool:
"""
:return: True if target_user exist, not is author and not bot
"""
@@ -56,7 +61,7 @@ def has_target_user(target_user: types.User, author_user: types.User, can_be_sam
return True
-def is_one_user(user_1: types.User, user_2: types.User):
+def is_one_user(user_1: dto.TargetUser | None, user_2: dto.TargetUser | None) -> bool:
if all([
user_1.id is not None,
user_2.id is not None,
@@ -73,7 +78,7 @@ def is_one_user(user_1: types.User, user_2: types.User):
return False
-def get_mentioned_user(message: types.Message) -> typing.Optional[types.User]:
+def get_mentioned_user(message: types.Message) -> dto.TargetUser | None:
possible_mentioned_text = message.text or message.caption
if not possible_mentioned_text:
return None
@@ -82,42 +87,42 @@ def get_mentioned_user(message: types.Message) -> typing.Optional[types.User]:
return None
for ent in entities:
if ent.type == "text_mention":
- return ent.user
+ return dto.TargetUser.from_aiogram(ent.user)
elif ent.type == "mention":
- username = ent.get_text(possible_mentioned_text).lstrip("@")
- return types.User(username=username)
+ username = ent.extract_from(possible_mentioned_text).lstrip("@")
+ return dto.TargetUser(username=username)
return None
-def get_replied_user(message: types.Message) -> typing.Optional[types.User]:
+def get_replied_user(message: types.Message) -> dto.TargetUser | None:
if message.reply_to_message:
- return message.reply_to_message.from_user
+ return dto.TargetUser.from_aiogram(message.reply_to_message.from_user)
return None
-def get_id_user(message: types.Message) -> types.User | None:
+def get_id_user(message: types.Message) -> dto.TargetUser | None:
text = message.text or message.caption or ""
for word in text.lower().split():
if word.startswith("id"):
with suppress(ValueError):
user_id = int(word.removeprefix("id"))
- return types.User(id=user_id)
+ return dto.TargetUser(id=user_id)
return None
-def is_bot_username(username: str):
+def is_bot_username(username: str) -> bool:
"""
this function deprecated. user can use username like @alice_bot and it don't say that it is bot
"""
return username is not None and username[-3:] == "bot"
-async def get_db_user_by_tg_user(target: types.User, tg_client_config: TgClientConfig) -> User:
+async def get_db_user_by_tg_user(target: dto.TargetUser, tg_client_config: TgClientConfig) -> User:
exception: Exception
try:
target_user = await User.get_or_create_from_tg_user(target)
except MultipleObjectsReturned as e:
- logger.warning("Strange, multiple username? chek id={id}, username={username}",
+ logger.warning("Strange, multiple username? check id={id}, username={username}",
id=target.id, username=target.username)
exception = e
# In target can be user with only username
diff --git a/app/services/moderation.py b/app/services/moderation.py
index 972a5f1e..099f8a2a 100644
--- a/app/services/moderation.py
+++ b/app/services/moderation.py
@@ -1,9 +1,9 @@
-import typing
from datetime import timedelta
from aiogram import Bot
-from aiogram.types import ChatMemberStatus, ChatMemberRestricted
-from aiogram.utils.exceptions import BadRequest
+from aiogram.types import ChatMemberRestricted
+from aiogram.exceptions import TelegramBadRequest
+from tortoise.backends.base.client import TransactionContext
from app.config import moderation
from app.config.main import load_config
@@ -18,7 +18,7 @@
config = load_config()
-async def warn_user(moderator: User, target_user: User, chat: Chat, comment: str):
+async def warn_user(moderator: User, target_user: User, chat: Chat, comment: str) -> ModeratorEvent:
return await ModeratorEvent.save_new_action(
moderator=moderator,
user=target_user,
@@ -28,7 +28,7 @@ async def warn_user(moderator: User, target_user: User, chat: Chat, comment: str
)
-async def ban_user(chat: Chat, target: User, admin: User, duration: timedelta, comment: str, bot: Bot):
+async def ban_user(chat: Chat, target: User, admin: User, duration: timedelta, comment: str, bot: Bot) -> str:
await restrict(
bot=bot,
chat=chat,
@@ -44,7 +44,7 @@ async def ban_user(chat: Chat, target: User, admin: User, duration: timedelta, c
return text
-async def ro_user(chat: Chat, target: User, admin: User, duration: timedelta, comment: str, bot: Bot):
+async def ro_user(chat: Chat, target: User, admin: User, duration: timedelta, comment: str, bot: Bot) -> str:
await restrict(
bot=bot,
chat=chat,
@@ -69,7 +69,7 @@ async def restrict(
comment: str,
type_restriction: TypeRestriction,
using_db=None
-):
+) -> ModeratorEvent:
try:
# restrict in telegram
await moderation.action_for_restrict[type_restriction](
@@ -78,9 +78,9 @@ async def restrict(
user_id=target.tg_id,
until_date=duration,
)
- except BadRequest as e:
+ except TelegramBadRequest as e:
raise CantRestrict(
- text=e.text, user_id=target.tg_id, chat_id=chat.chat_id,
+ text=e.message, user_id=target.tg_id, chat_id=chat.chat_id,
reason=comment, type_event=type_restriction.name
)
else:
@@ -104,7 +104,7 @@ async def restrict(
return moderator_event
-def get_moderator_message_args(text: str) -> typing.Tuple[str, str]:
+def get_moderator_message_args(text: str) -> tuple[str, str]:
_, *args = text.split(maxsplit=2) # in text: command_duration_comments like: "!ro 13d don't flood"
if not args:
return "", ""
@@ -114,7 +114,7 @@ def get_moderator_message_args(text: str) -> typing.Tuple[str, str]:
return duration_text, " ".join(args[1:])
-def get_duration(text: str):
+def get_duration(text: str) -> tuple[timedelta, str]:
duration_text, comment = get_moderator_message_args(text)
if duration_text:
duration = parse_timedelta_from_text(duration_text)
@@ -123,19 +123,24 @@ def get_duration(text: str):
return duration, comment
-async def user_has_now_ro(user: User, chat: Chat, bot: Bot):
+async def user_has_now_ro(user: User, chat: Chat, bot: Bot) -> bool:
chat_member = await bot.get_chat_member(chat_id=chat.chat_id, user_id=user.tg_id)
- if chat_member.status == ChatMemberStatus.RESTRICTED:
+ if chat_member.status == "restricted":
assert isinstance(chat_member, ChatMemberRestricted)
return not chat_member.can_send_messages
- return chat_member.status in (ChatMemberStatus.BANNED, ChatMemberStatus.KICKED)
+ return chat_member.status in ("banned", "kicked")
-async def auto_restrict(target: User, chat: Chat, bot: Bot, using_db=None) -> typing.Tuple[int, ModeratorEvent]:
+async def auto_restrict(
+ target: User,
+ chat: Chat,
+ bot: Bot,
+ using_db: TransactionContext | None = None
+) -> tuple[int, ModeratorEvent]:
"""
return count auto restrict
"""
- bot_user = await User.get_or_create_from_tg_user(await bot.me)
+ bot_user = await User.get_or_create_from_tg_user(await bot.me())
count_auto_restrict = await get_count_auto_restrict(target, chat, bot_user=bot_user)
logger.info(
@@ -161,10 +166,15 @@ async def auto_restrict(target: User, chat: Chat, bot: Bot, using_db=None) -> ty
return count_auto_restrict + 1, moderator_event
-async def get_count_auto_restrict(target: User, chat: Chat, bot_user: User = None, bot: Bot = None):
+async def get_count_auto_restrict(
+ target: User,
+ chat: Chat,
+ bot_user: User | None = None,
+ bot: Bot | None = None,
+) -> int:
assert bot is not None or bot_user is not None, "One of bot and bot_user must be not None"
if bot_user is None:
- bot_user = await User.get_or_create_from_tg_user(await bot.me)
+ bot_user = await User.get_or_create_from_tg_user(await bot.me())
return await ModeratorEvent.filter(
moderator=bot_user, user=target, chat=chat,
type_restriction__in=(TypeRestriction.karmic_ro.name, TypeRestriction.karmic_ban.name),
diff --git a/app/services/remove_message.py b/app/services/remove_message.py
index 5347196c..205b05ce 100644
--- a/app/services/remove_message.py
+++ b/app/services/remove_message.py
@@ -2,17 +2,16 @@
from contextlib import suppress
from aiogram import types
-from aiogram.utils.exceptions import (MessageToEditNotFound, MessageCantBeEdited, MessageCantBeDeleted,
- MessageToDeleteNotFound)
+from aiogram.exceptions import (TelegramNotFound, TelegramUnauthorizedError)
async def remove_kb(message: types.Message, sleep_time: int = 0):
await asyncio.sleep(sleep_time)
- with suppress(MessageToEditNotFound, MessageCantBeEdited):
+ with suppress(TelegramNotFound, TelegramUnauthorizedError):
await message.edit_reply_markup()
async def delete_message(message: types.Message, sleep_time: int = 0):
await asyncio.sleep(sleep_time)
- with suppress(MessageCantBeDeleted, MessageToDeleteNotFound):
+ with suppress(TelegramNotFound, TelegramUnauthorizedError):
await message.delete()
diff --git a/app/utils/cli.py b/app/utils/cli.py
index 59a8d9fc..c3bedb4a 100644
--- a/app/utils/cli.py
+++ b/app/utils/cli.py
@@ -1,9 +1,16 @@
# partially from https://github.com/aiogram/bot
import argparse
+import asyncio
-from app.utils.log import Logger
+from aiogram import Bot, Dispatcher
import app
+from app.models.db import db
+from app.utils.executor import on_startup_webhook, on_startup_notify
+from app.utils.lock_factory import LockFactory
+from app.utils.log import Logger
+from app import middlewares
+from app import handlers
from app.models.config import Config
@@ -24,20 +31,29 @@ def create_parser():
return arg_parser
-def cli(config: Config):
-
+async def cli(config: Config):
+ bot = Bot(config.bot_token, parse_mode="HTML")
+ dp = Dispatcher(storage=config.storage.create_storage())
parser = create_parser()
namespace = parser.parse_args()
- from app import misc
- from app.utils.executor import runner
-
- misc.setup(config)
- if namespace.polling:
- logger.info("starting polling...")
+ await db.db_init(config.db)
+ logger.debug(f"As application dir using: {config.app_dir}")
+ lock_factory = LockFactory()
+ middlewares.setup(dp, lock_factory, config)
+ logger.info("Configure handlers...")
+ handlers.setup(dp, bot, config)
+ await on_startup_notify(bot, config)
+ try:
+ asyncio.create_task(lock_factory.check_and_clear())
+ if namespace.polling:
+ logger.info("starting polling...")
+ await dp.start_polling(bot)
+ else:
+ logger.info("starting webhook...")
+ await on_startup_webhook(bot, config.webhook)
+ raise NotImplementedError("webhook are not implemented now")
+ finally:
+ await db.on_shutdown()
+ await bot.session.close()
- runner.skip_updates = namespace.skip_updates
- runner.start_polling(reset_webhook=True)
- else:
- logger.info("starting webhook...")
- runner.start_webhook(**config.webhook.listener_kwargs)
diff --git a/app/utils/exceptions.py b/app/utils/exceptions.py
index ff5f7384..d3935d37 100644
--- a/app/utils/exceptions.py
+++ b/app/utils/exceptions.py
@@ -78,3 +78,11 @@ def __init__(self, reason: str = None, type_event: str = None, *args, **kwargs):
class CantRestrict(ModerationError):
pass
+
+
+class Throttled(RuntimeError):
+ def __init__(self, key: str, chat_id: int, user_id: int, rate: int | float):
+ self.key = key
+ self.chat_id = chat_id
+ self.user_id = user_id
+ self.rate = rate
diff --git a/app/utils/executor.py b/app/utils/executor.py
index 8211ce11..b67e9d04 100644
--- a/app/utils/executor.py
+++ b/app/utils/executor.py
@@ -1,37 +1,25 @@
# partially from https://github.com/aiogram/bot
from contextlib import suppress
-from functools import partial
-from aiogram import Dispatcher
-from aiogram.utils.exceptions import TelegramAPIError
-from aiogram.utils.executor import Executor
+from aiogram import Bot
+from aiogram.exceptions import TelegramAPIError
-from app.misc import dp
from app.models.config import Config, WebhookConfig
-from app.models.db import db
from app.utils.log import Logger
logger = Logger(__name__)
-runner = Executor(dp)
-async def on_startup_webhook(dispatcher: Dispatcher, webhook_config: WebhookConfig):
+async def on_startup_webhook(bot: Bot, webhook_config: WebhookConfig):
webhook_url = webhook_config.external_url
logger.info("Configure Web-Hook URL to: {url}", url=webhook_url)
- await dispatcher.bot.set_webhook(webhook_url)
+ await bot.set_webhook(webhook_url)
-async def on_startup_notify(dispatcher: Dispatcher, config: Config):
+async def on_startup_notify(bot: Bot, config: Config):
with suppress(TelegramAPIError):
- await dispatcher.bot.send_message(
+ await bot.send_message(
chat_id=config.log.log_chat_id, text="Bot started", disable_notification=True
)
logger.info("Notified about bot is started.")
-
-
-def setup(config: Config):
- logger.info("Configure executor...")
- db.setup(runner, config.db)
- runner.on_startup(partial(on_startup_webhook, webhook_config=config.webhook), webhook=True, polling=False)
- runner.on_startup(partial(on_startup_notify, config=config))
diff --git a/app/utils/from_axenia.py b/app/utils/from_axenia.py
deleted file mode 100644
index 08c91a67..00000000
--- a/app/utils/from_axenia.py
+++ /dev/null
@@ -1,52 +0,0 @@
-import aiohttp
-from bs4 import BeautifulSoup as Soup
-
-from app.utils.exceptions import CantImportFromAxenia
-from app.utils.log import Logger
-
-
-logger = Logger(__name__)
-
-
-def username_by_link(link: str) -> str:
- return link.split('=')[1]
-
-
-def parse_rating(html):
- soup = Soup(html, 'lxml')
- main_soup = soup.find('main', role='main')
-
- for tbody in main_soup.find('div', class_="carousel-inner").find_all('tbody'):
- for tr in tbody.find_all('tr'):
- tds = tr.find_all('td')
- name = tds[0].text
- try:
- username = username_by_link(tds[0].find('a').get('href'))
- except AttributeError:
- username = None
- karma = tds[1].text
- yield name, username, karma
-
-
-async def get_html(url):
- timeout = aiohttp.ClientTimeout(total=60)
- async with aiohttp.ClientSession(timeout=timeout) as session:
- async with session.get(url) as r:
- if not r.status == 200:
- raise CantImportFromAxenia(chat_id=url.split('=')[1])
- return await r.text()
-
-
-def get_link_by_chat_id(chat_id: int) -> str:
- return f"http://axeniabot.ru/?chat_id={chat_id}"
-
-
-async def axenia_rating(chat_id):
- logger.info("load axenia karmas for chat {chat}", chat=chat_id)
- return list(
- parse_rating(
- await get_html(
- get_link_by_chat_id(chat_id)
- )
- )
- )
diff --git a/app/utils/lock_factory.py b/app/utils/lock_factory.py
index c6bba4ce..eafbf779 100644
--- a/app/utils/lock_factory.py
+++ b/app/utils/lock_factory.py
@@ -17,7 +17,6 @@ def get_lock(self, id_: typing.Any):
# Это костыль, по хорошему эта таска должна создаваться в ините,
# для этого милваря должна создаваться после бота и диспетчера
- asyncio.create_task(self._check_and_clear())
return self._locks.setdefault(id_, asyncio.Lock())
@@ -34,7 +33,7 @@ def clear_free(self):
del self._locks[key]
logger.debug("remove lock for {key}", key=key)
- async def _check_and_clear(self, cool_down: int = 1800):
+ async def check_and_clear(self, cool_down: int = 1800):
while True:
await asyncio.sleep(cool_down)
self.clear_free()
diff --git a/requirements.test.txt b/requirements.test.txt
index 28ecacab..5a3eb72a 100644
--- a/requirements.test.txt
+++ b/requirements.test.txt
@@ -1,2 +1,2 @@
-flake8
-pytest
+flake8~=6.0.0
+pytest~=7.2.1
diff --git a/requirements.txt b/requirements.txt
index 78adb1b0..3eb23e69 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,11 +1,11 @@
-aiogram
-aiohttp # для загрузки кармы с сайта Аксении для последующего парсинга
-loguru
-tortoise-orm
-lxml # для парсинга кармы из Аксении
-beautifulsoup4 # для парсинга кармы из Аксении
-python-dotenv
-Pyrogram # для резолва username через клиент апи
-pyyml
-colorlog
-aioredis
+aiogram==3.0.0b6
+aiohttp~=3.8.3
+tortoise-orm~=0.19.3
+lxml~=4.9.2
+beautifulsoup4~=4.11.2
+python-dotenv~=0.21.1
+Pyrogram~=2.0.97
+pyyml~=0.0.2
+colorlog~=6.7.0
+redis~=4.4.2
+
diff --git a/tests/karma/fixtures/fixtures_karma.py b/tests/karma/fixtures/fixtures_karma.py
index f0f35685..9f350aaf 100644
--- a/tests/karma/fixtures/fixtures_karma.py
+++ b/tests/karma/fixtures/fixtures_karma.py
@@ -1,4 +1,5 @@
import typing
+from datetime import datetime
from random import choice
from string import ascii_letters
@@ -55,6 +56,9 @@ def get_next_words(count_symbols: int = 10) -> str:
def get_message_with_text(text: str) -> types.Message:
- return types.Message(**{
- 'text': text,
- })
+ return types.Message(
+ message_id=1,
+ date=datetime.now(),
+ text=text,
+ chat=types.Chat(id=1, type="group"),
+ )
diff --git a/tests/karma/test_correct_karma_filter.py b/tests/karma/test_correct_karma_filter.py
index 27fd09e4..8410f3ef 100644
--- a/tests/karma/test_correct_karma_filter.py
+++ b/tests/karma/test_correct_karma_filter.py
@@ -1,11 +1,13 @@
+import pytest
+
from .common import plus_texts, minus_texts, punctuations, filter_check, SPACES, INF
from .fixtures import generate_phrases_next_word, get_message_with_text
-def test_correct_plus():
- for text in plus_texts:
- for phrase in generate_phrases_next_word(text, punctuations, SPACES):
- check_plus(phrase)
+@pytest.mark.parametrize("text", plus_texts)
+def test_correct_plus(text: str):
+ for phrase in generate_phrases_next_word(text, punctuations, SPACES):
+ check_plus(phrase)
def check_plus(text_with_plus_trigger: str):
@@ -14,10 +16,10 @@ def check_plus(text_with_plus_trigger: str):
assert filter_rez['karma']['karma_change'] == INF, str(msg)
-def test_correct_minus():
- for text in minus_texts:
- for phrase in generate_phrases_next_word(text, punctuations, SPACES):
- check_minus_reply(phrase)
+@pytest.mark.parametrize("text", minus_texts)
+def test_correct_minus(text: str):
+ for phrase in generate_phrases_next_word(text, punctuations, SPACES):
+ check_minus_reply(phrase)
def check_minus_reply(text_with_minus_trigger: str):
diff --git a/tests/target/common.py b/tests/target/common.py
index 60900576..b0c37f2e 100644
--- a/tests/target/common.py
+++ b/tests/target/common.py
@@ -11,5 +11,5 @@
def filter_check(message: types.Message, conf: typing.Dict[str, bool]):
- target_filter = HasTargetFilter(has_target=conf)
- return asyncio.run(target_filter.check(message))
+ target_filter = HasTargetFilter(**conf)
+ return asyncio.run(target_filter(message))
diff --git a/tests/target/correct_targets.py b/tests/target/correct_targets.py
index 6586e42b..269c3159 100644
--- a/tests/target/correct_targets.py
+++ b/tests/target/correct_targets.py
@@ -1,3 +1,4 @@
+import pytest
from aiogram import types
from .common import filter_check, CONF_CANT_BE_SAME
@@ -12,21 +13,20 @@ def test_reply_target():
check_target(target_user, msg)
-def test_mention_target():
+@pytest.mark.parametrize("phrase", get_parts())
+def test_mention_target(phrase: list[str]):
author_user = get_from_user(36, "Rajesh")
target_user = get_from_user(99, "Howard")
- for phrase in get_parts():
- msg = get_message_with_mention(author_user, target_user, phrase)
- check_target(target_user, msg)
+ msg = get_message_with_mention(author_user, target_user, phrase)
+ check_target(target_user, msg)
-def test_text_mention_target():
+@pytest.mark.parametrize("phrase", get_parts())
+def test_text_mention_target(phrase: list[str]):
target_user = get_from_user(69, "Bernadette")
author_user = get_from_user(53, "Amy")
-
- for phrase in get_parts():
- msg = get_message_with_text_mention(author_user, target_user, phrase)
- check_target(target_user, msg)
+ msg = get_message_with_text_mention(author_user, target_user, phrase)
+ check_target(target_user, msg)
def check_target(target_user: dict, msg: types.Message):
diff --git a/tests/target/fixtures/targets.py b/tests/target/fixtures/targets.py
index 68338936..c92130ac 100644
--- a/tests/target/fixtures/targets.py
+++ b/tests/target/fixtures/targets.py
@@ -1,4 +1,5 @@
import typing
+from datetime import datetime
from random import choice
from string import ascii_letters
@@ -21,11 +22,14 @@ def get_words(count_symbols: int = 10) -> str:
def get_message_with_reply(author_user: dict, target_user: dict, text: str) -> types.Message:
- return types.Message(**{
- 'from': author_user,
- 'text': text,
- 'reply_to_message': get_reply_message(target_user)
- })
+ return types.Message(
+ message_id=1,
+ date=datetime.now(),
+ chat=types.Chat(id=1, type="group"),
+ from_user=types.User(**author_user),
+ text=text,
+ reply_to_message=get_reply_message(target_user),
+ )
def get_message_with_mention(author_user: dict, target_user: dict, text_precursors: typing.List[str]) -> types.Message:
@@ -33,21 +37,24 @@ def get_message_with_mention(author_user: dict, target_user: dict, text_precurso
text_precursors[1] = username
msg_text = " ".join(text_precursors)
start_entity_pos = len(text_precursors[0]) + 1 # добавляем длину пробела
- return types.Message(**{
- 'from': author_user,
- 'text': msg_text,
- 'entities': [
+ return types.Message(
+ from_user=types.User(**author_user),
+ text=msg_text,
+ entities=[
get_entity_mention(start_entity_pos, len(username)),
- ]
- })
+ ],
+ message_id=1,
+ date=datetime.now(),
+ chat=types.Chat(id=1, type="group")
+ )
-def get_entity_mention(offset, length):
- return {
+def get_entity_mention(offset, length) -> types.MessageEntity:
+ return types.MessageEntity(**{
"offset": offset,
"length": length,
"type": "mention"
- }
+ })
def get_message_with_text_mention(
@@ -59,22 +66,25 @@ def get_message_with_text_mention(
text_precursors[1] = first_name
msg_text = " ".join(text_precursors)
start_entity_pos = len(text_precursors[0]) + 1 # добавляем длину пробела
- return types.Message(**{
- 'from': author_user,
- 'text': msg_text,
- 'entities': [
+ return types.Message(
+ from_user=types.User(**author_user),
+ text=msg_text,
+ entities=[
get_entity_text_mention(start_entity_pos, target_user)
- ]
- })
+ ],
+ message_id=1,
+ date=datetime.now(),
+ chat=types.Chat(id=1, type="group")
+ )
-def get_entity_text_mention(offset, user: dict):
- return {
+def get_entity_text_mention(offset, user: dict) -> types.MessageEntity:
+ return types.MessageEntity(**{
"offset": offset,
"length": len(user["first_name"]),
"type": "text_mention",
"user": user
- }
+ })
def get_user_username(user: dict) -> str:
@@ -82,14 +92,18 @@ def get_user_username(user: dict) -> str:
def get_reply_message(user_dict):
- return types.Message(**{
- 'from': user_dict
- })
+ return types.Message(
+ from_user=types.User(**user_dict),
+ chat=types.Chat(id=1, type="chat"),
+ date=datetime.now(),
+ message_id=1,
+ )
-def get_from_user(id_=777, username=None, first_name=None):
+def get_from_user(id_=777, username=None, first_name="Bob"):
return {
'id': id_,
'username': username,
- 'first_name': first_name
+ 'first_name': first_name,
+ 'is_bot': False,
}
diff --git a/tests/target/test_auto_target.py b/tests/target/test_auto_target.py
index e888d7f6..b92fd965 100644
--- a/tests/target/test_auto_target.py
+++ b/tests/target/test_auto_target.py
@@ -1,3 +1,4 @@
+import pytest
from aiogram import types
from .common import filter_check, CONF_CAN_BE_SAME, CONF_CANT_BE_SAME
@@ -5,27 +6,25 @@
get_message_with_text_mention, get_message_with_mention, get_parts)
-def test_auto_reply():
+@pytest.mark.parametrize("phrase", get_parts())
+def test_auto_reply(phrase: list[str]):
user = get_from_user(321, "Kripke")
- for phrase in get_parts():
- msg = get_message_with_reply(user, user, " ".join(phrase))
- check_msg_auto_target(user, msg)
+ msg = get_message_with_reply(user, user, " ".join(phrase))
+ check_msg_auto_target(user, msg)
-def test_auto_mention():
+@pytest.mark.parametrize("phrase", get_parts())
+def test_auto_mention(phrase: list[str]):
user = get_from_user(321, "Kripke")
+ msg = get_message_with_mention(user, user, phrase)
+ check_msg_auto_target(user, msg)
- for phrase in get_parts():
- msg = get_message_with_mention(user, user, phrase)
- check_msg_auto_target(user, msg)
-
-def test_auto_text_mention():
+@pytest.mark.parametrize("phrase", get_parts())
+def test_auto_text_mention(phrase: list[str]):
user = get_from_user(321, first_name="Barry")
-
- for phrase in get_parts():
- msg = get_message_with_text_mention(user, user, phrase)
- check_msg_auto_target(user, msg)
+ msg = get_message_with_text_mention(user, user, phrase)
+ check_msg_auto_target(user, msg)
def check_msg_auto_target(user: dict, msg: types.Message):
@@ -38,4 +37,14 @@ def check_msg_auto_target(user: dict, msg: types.Message):
if founded_user.id is None:
assert founded_user.username == target_user.username, f"msg text {{{msg.text}}} user: {{{user}}}"
else:
- assert founded_user == target_user, f"msg text {{{msg.text}}} user: {{{user}}}"
+ assert are_users_equals(founded_user, target_user), f"msg text {{{msg.text}}} user: {{{user}}}"
+
+
+def are_users_equals(expected: types.User, actual: types.User) -> bool:
+ return all([
+ expected.id == actual.id,
+ expected.is_bot == actual.is_bot,
+ expected.username == actual.username,
+ expected.first_name == actual.first_name,
+ expected.last_name == actual.last_name,
+ ])