Skip to content

Commit

Permalink
Merge pull request #136 from Lamroy95/resolve-linked-reports
Browse files Browse the repository at this point in the history
Resolve linked reports
  • Loading branch information
bomzheg authored Nov 23, 2023
2 parents f27bd30 + 5ab9c74 commit e853bb3
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 112 deletions.
16 changes: 16 additions & 0 deletions app/filters/reports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from aiogram import types
from aiogram.filters import BaseFilter

from app.infrastructure.database.models import Chat
from app.infrastructure.database.repo.report import ReportRepo


class HasResolvedReport(BaseFilter):
"""Check if reported message already resolved report"""

async def __call__(
self, message: types.Message, chat: Chat, report_repo: ReportRepo
) -> bool:
return await report_repo.has_resolved_report(
chat_id=chat.chat_id, message_id=message.reply_to_message.message_id
)
192 changes: 105 additions & 87 deletions app/handlers/moderator.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import asyncio
import random

from aiogram import Bot, F, Router, types
from aiogram.enums import ChatMemberStatus
from aiogram.exceptions import TelegramUnauthorizedError
from aiogram.filters import Command, CommandObject, MagicData
from aiogram.utils.text_decorations import html_decoration as hd
from tortoise.transactions import in_transaction

from app.filters import (
BotHasPermissions,
HasPermissions,
HasTargetFilter,
TargetHasPermissions,
)
from app.filters.reports import HasResolvedReport
from app.handlers import keyboards as kb
from app.infrastructure.database.models import Chat, ChatSettings, ReportStatus, User
from app.infrastructure.database.repo.report import ReportRepo
from app.models.config import Config
from app.services.moderation import (
ban_user,
delete_moderator_event,
get_duration,
get_mentions_admins,
ro_user,
warn_user,
)
Expand All @@ -29,7 +29,14 @@
delete_message,
remove_kb,
)
from app.services.report import register_report, resolve_report, reward_reporter
from app.services.report import (
cancel_report,
cleanup_reports_dialog,
register_report,
resolve_report,
reward_reporter,
set_report_bot_reply,
)
from app.services.user_info import get_user_info
from app.utils.exceptions import ModerationError, TimedeltaParseError
from app.utils.log import Logger
Expand All @@ -42,32 +49,51 @@
@router.message(
F.chat.type.in_(["group", "supergroup"]),
HasTargetFilter(),
~HasResolvedReport(),
Command("report", "admin", "spam", prefix="/!@"),
)
async def report_message(
message: types.Message, chat: Chat, user: User, target: User, bot: Bot
message: types.Message,
chat: Chat,
user: User,
target: User,
bot: Bot,
report_repo: ReportRepo,
):
logger.info(
"user {user} report for message {message}",
"User {user} reported message {message} in chat {chat}",
user=message.from_user.id,
message=message.message_id,
chat=message.chat.id,
)
answer_message = "Спасибо за сообщение. Мы обязательно разберёмся"
admins_mention = await get_mentions_admins(message.chat, bot)

async with in_transaction() as db_session:
report = await register_report(
reporter=user,
reported_user=target,
chat=chat,
reported_message=message.reply_to_message,
db_session=db_session,
)
report = await register_report(
reporter=user,
reported_user=target,
chat=chat,
reported_message=message.reply_to_message,
command_message=message,
report_repo=report_repo,
)

reaction_keyboard = kb.get_report_reaction_kb(report=report, user=user)
await message.reply(
bot_reply = await message.reply(
f"{answer_message}.{admins_mention}", reply_markup=reaction_keyboard
)
await set_report_bot_reply(report, bot_reply, report_repo)


@router.message(
F.chat.type.in_(["group", "supergroup"]),
HasTargetFilter(),
HasResolvedReport(),
Command("report", "admin", "spam", prefix="/!@"),
)
async def report_already_reported(message: types.Message):
reply = await message.reply("Сообщение уже было рассмотрено ранее")
asyncio.create_task(cleanup_command_dialog(reply, True, delay=60))


@router.message(
Expand All @@ -80,42 +106,6 @@ async def report_private(message: types.Message):
)


async def get_mentions_admins(
chat: types.Chat,
bot: Bot,
ignore_anonymous: bool = True,
):
admins = await bot.get_chat_administrators(chat.id)
random.shuffle(admins) # чтобы попадались разные админы
admins_mention = ""
notifiable_admins = [
admin for admin in admins if need_notify_admin(admin, ignore_anonymous)
]
random_five_admins = notifiable_admins[:5]
for admin in random_five_admins:
admins_mention += hidden_link(admin.user.url)
return admins_mention


def need_notify_admin(
admin: types.ChatMemberAdministrator | types.ChatMemberOwner,
ignore_anonymous: bool = True,
):
"""
Проверяет, нужно ли уведомлять администратора о жалобе.
:param admin: Администратор, которого нужно проверить.
:param ignore_anonymous: Игнорировать ли анонимных администраторов.
"""
if admin.user.is_bot or (ignore_anonymous and admin.is_anonymous):
return False
return (
admin.status == ChatMemberStatus.CREATOR
or admin.can_delete_messages
or admin.can_restrict_members
)


@router.message(
F.chat.type.in_(["group", "supergroup"]),
HasTargetFilter(),
Expand Down Expand Up @@ -311,7 +301,9 @@ async def cancel_warn(
await delete_moderator_event(callback_data.moderator_event_id, moderator=from_user)

await callback_query.answer("Вы отменили предупреждение", show_alert=True)
await cleanup_command_dialog(message=callback_query.message, delete_bot_reply=True)
await cleanup_command_dialog(
bot_message=callback_query.message, delete_bot_reply=True
)


@router.callback_query(
Expand All @@ -326,71 +318,97 @@ async def approve_report_handler(
bot: Bot,
config: Config,
chat_settings: ChatSettings,
report_repo: ReportRepo,
):
async with in_transaction() as db_session:
await resolve_report(
report_id=callback_data.report_id,
resolved_by=user,
resolution=ReportStatus.APPROVED,
db_session=db_session,
)
if chat_settings.karma_counting and config.report_karma_award:
logger.info(
"Moderator {moderator} approved report {report}",
moderator=callback_query.from_user.id,
report=callback_data.report_id,
)
first_report, *linked_reports = await resolve_report(
report_id=callback_data.report_id,
resolved_by=user,
resolution=ReportStatus.APPROVED,
report_repo=report_repo,
)
award_enabled = chat_settings.karma_counting and config.report_karma_award
if award_enabled:
karma_change_result = await reward_reporter(
reporter_id=callback_data.reporter_id,
reporter_id=first_report.reporter.id,
chat=chat,
reward_amount=config.report_karma_award,
bot=bot,
)
await callback_query.message.edit_text(
await bot.edit_message_text(
"<b>{reporter}</b> получил <b>+{reward_amount}</b> кармы "
"в награду за репорт{admin_url}".format(
reporter=hd.quote(karma_change_result.karma_event.user_to.fullname),
reward_amount=config.report_karma_award,
admin_url=hidden_link(user.link),
)
),
chat_id=first_report.chat.chat_id,
message_id=first_report.bot_reply_message_id,
)
delete_bot_reply = False
else:
delete_bot_reply = True

await callback_query.answer("Вы подтвердили репорт", show_alert=delete_bot_reply)
await cleanup_command_dialog(
message=callback_query.message, delete_bot_reply=delete_bot_reply
await callback_query.answer("Вы подтвердили репорт", show_alert=not award_enabled)
await cleanup_reports_dialog(
first_report=first_report,
linked_reports=linked_reports,
delete_first_reply=not award_enabled,
bot=bot,
)


@router.callback_query(
kb.DeclineReportCb.filter(), HasPermissions(can_restrict_members=True)
)
async def decline_report_handler(
callback_query: types.CallbackQuery, callback_data: kb.DeclineReportCb, user: User
callback_query: types.CallbackQuery,
callback_data: kb.DeclineReportCb,
user: User,
bot: Bot,
report_repo: ReportRepo,
):
async with in_transaction() as db_session:
await resolve_report(
report_id=callback_data.report_id,
resolved_by=user,
resolution=ReportStatus.DECLINED,
db_session=db_session,
)
logger.info(
"Moderator {moderator} declined report {report}",
moderator=callback_query.from_user.id,
report=callback_data.report_id,
)
first_report, *linked_reports = await resolve_report(
report_id=callback_data.report_id,
resolved_by=user,
resolution=ReportStatus.DECLINED,
report_repo=report_repo,
)
await cleanup_reports_dialog(
first_report, linked_reports, delete_first_reply=True, bot=bot
)
await callback_query.answer("Вы отклонили репорт", show_alert=True)
await cleanup_command_dialog(message=callback_query.message, delete_bot_reply=True)


@router.callback_query(
kb.CancelReportCb.filter(), MagicData(F.user.id == F.callback_data.reporter_id)
)
async def cancel_report_handler(
callback_query: types.CallbackQuery, callback_data: kb.CancelReportCb, user: User
callback_query: types.CallbackQuery,
callback_data: kb.CancelReportCb,
user: User,
report_repo: ReportRepo,
):
async with in_transaction() as db_session:
await resolve_report(
report_id=callback_data.report_id,
resolved_by=user,
resolution=ReportStatus.CANCELLED,
db_session=db_session,
)
logger.info(
"User {user} cancelled report {report}",
user=callback_query.from_user.id,
report=callback_data.report_id,
)
await cancel_report(
report_id=callback_data.report_id,
resolved_by=user,
report_repo=report_repo,
)
await callback_query.answer("Вы отменили репорт", show_alert=True)
await cleanup_command_dialog(message=callback_query.message, delete_bot_reply=True)
await cleanup_command_dialog(
bot_message=callback_query.message, delete_bot_reply=True
)


@router.callback_query(
Expand Down
2 changes: 2 additions & 0 deletions app/infrastructure/database/models/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class Report(Model):
)
created_time = fields.DatetimeField(auto_now=True, null=False)
resolution_time = fields.DatetimeField(null=True)
command_message_id = fields.BigIntField(generated=False, null=False)
bot_reply_message_id = fields.BigIntField(generated=False, null=True)
reported_message_id = fields.BigIntField(generated=False, null=False)
reported_message_content = fields.CharField(
null=False, max_length=TG_MESSAGE_MAX_LEN
Expand Down
66 changes: 66 additions & 0 deletions app/infrastructure/database/repo/report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from typing import Iterable

import aiogram
from tortoise import BaseDBAsyncClient

from app.infrastructure.database.models import Chat, Report, ReportStatus, User


class ReportRepo:
def __init__(self, session: BaseDBAsyncClient | None = None):
self.session = session

async def create(
self,
reporter: User,
reported_user: User,
chat: Chat,
reported_message: aiogram.types.Message,
command_message: aiogram.types.Message,
status: ReportStatus,
) -> Report:
report = await Report.create(
reporter=reporter,
reported_user=reported_user,
chat=chat,
command_message_id=command_message.message_id,
reported_message_id=reported_message.message_id,
reported_message_content=reported_message.html_text,
status=status,
using_db=self.session,
)
return report

async def save(self, report: Report, fields: Iterable[str] | None = None):
await report.save(update_fields=fields, using_db=self.session)

async def get_report_by_id(self, report_id: int) -> Report:
return await Report.get(id=report_id, using_db=self.session)

async def get_linked_pending_reports(self, report_id: int) -> Iterable[Report]:
report = await Report.get(id=report_id, using_db=self.session).prefetch_related(
"chat"
)
return await (
Report.filter(
chat=report.chat,
reported_message_id=report.reported_message_id,
status=ReportStatus.PENDING,
)
.prefetch_related("chat", "reporter")
.order_by("created_time")
.using_db(self.session)
.all()
)

async def has_resolved_report(self, chat_id: int, message_id: int) -> bool:
"""Return True, if provided message has reports with status Approved or Declined"""
return await (
Report.filter(
chat__chat_id=chat_id,
reported_message_id=message_id,
status__in=[ReportStatus.APPROVED.value, ReportStatus.DECLINED.value],
)
.using_db(self.session)
.exists()
)
Loading

0 comments on commit e853bb3

Please sign in to comment.