Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add report service #118

Merged
merged 15 commits into from
Oct 24, 2023
4 changes: 2 additions & 2 deletions app/config/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from app.models.config import LogConfig


def load_log_config(app_dir: Path) -> LogConfig:
def load_log_config(app_dir: Path, log_chat_id: int) -> LogConfig:
return LogConfig(
log_chat_id=-1001404337089,
log_chat_id=log_chat_id,
log_path=app_dir / "log",
)
14 changes: 9 additions & 5 deletions app/config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
def load_config(config_dir: Path = None) -> Config:
app_dir: Path = Path(__file__).parent.parent.parent
config_dir = config_dir or app_dir / 'config'
load_dotenv(str(config_dir / '.env'))
log_config = load_log_config(app_dir=app_dir)

with (config_dir / "bot-config.yaml").open('r', encoding="utf-8") as f:
config_file_data = yaml.load(f, Loader=yaml.FullLoader)

log_config = load_log_config(app_dir=app_dir, log_chat_id=config_file_data['log_chat_id'])
logging_setup(config_dir, log_config)

load_dotenv(str(config_dir / '.env'))
_bot_token = os.getenv("KARMA_BOT_TOKEN")
with (config_dir / "bot-config.yml").open('r', encoding="utf-8") as f:
config_file_data = yaml.load(f, Loader=yaml.FullLoader)

return Config(
auto_restriction=load_karmic_restriction_config(),
Expand All @@ -34,7 +36,9 @@ def load_config(config_dir: Path = None) -> Config:
bot_token=_bot_token,
superusers=frozenset(config_file_data['superusers']),
log=log_config,
dump_chat_id=-1001459777201, # ⚙️Testing Area >>> Python Scripts,
dump_chat_id=config_file_data['dump_chat_id'],
tg_client=TgClientConfig(bot_token=_bot_token),
storage=load_storage(config_file_data["storage"]),
report_karma_award=config_file_data.get("report_karma_award", 0),
callback_query_answer_cache_time=config_file_data.get("callback_query_answer_cache_time", 3600)
)
6 changes: 3 additions & 3 deletions app/filters/karma_change.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ def has_minus_karma(possible_trigger: str) -> typing.Optional[float]:
return None
if possible_trigger in MINUS_TRIGGERS:
return -INF
# i think next function run:
# has_spaces(possible_trigger)
# newer will be true. may be we can remove it from condition
# TODO: I think next function run:
# has_spaces(possible_trigger)
# will never be true. Maybe we can remove it from condition
if not has_spaces(possible_trigger) and possible_trigger[0] in MINUS_EMOJI:
return -INF
if possible_trigger[0:len(MINUS)] == MINUS:
Expand Down
40 changes: 25 additions & 15 deletions app/filters/tg_permissions.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
# from https://github.com/aiogram/bot/blob/master/app/filters/has_permissions.py
import logging
from dataclasses import dataclass
from typing import Any

from aiogram import Bot, types
from aiogram.enums import ChatMemberStatus
from aiogram.filters import Filter
from aiogram.filters import BaseFilter

from app.models.db import Chat, User
from app.services.find_target_user import get_target_user

logger = logging.getLogger(__name__)


@dataclass
class HasPermissions(Filter):
class HasPermissions(BaseFilter):
"""
Validate the user has specified permissions in chat
"""
Expand Down Expand Up @@ -40,28 +44,34 @@ def __post_init__(self):
arg: True for arg in self.ARGUMENTS.values() if getattr(self, arg)
}

def _get_cached_value(self, _message: types.Message) -> types.ChatMember | None:
def _get_cached_value(self, user: types.User, chat: Chat) -> types.ChatMember | None:
return None # TODO

def _set_cached_value(self, _message: types.Message, _member: types.ChatMember):
def _set_cached_value(self, user: types.User, chat: Chat, _member: types.ChatMember):
return None # TODO

async def _get_chat_member(self, message: types.Message, bot: Bot):
chat_member = self._get_cached_value(message)
async def _get_chat_member(self, update: types.TelegramObject, user: types.User, chat: Chat, bot: Bot):
chat_member = self._get_cached_value(user, chat)

if chat_member is None:
admins = await bot.get_chat_administrators(message.chat.id)
target_user_id = self.get_target_id(message, bot)
admins = await bot.get_chat_administrators(chat.chat_id)
target_user_id = self.get_target_id(update, user, bot)
if target_user_id is None:
return False
try:
chat_member = next(filter(lambda member: member.user.id == target_user_id, admins))
except StopIteration:
return False
self._set_cached_value(message, chat_member)
self._set_cached_value(user, chat, chat_member)
return chat_member

async def __call__(self, message: types.Message, bot: Bot) -> bool | dict[str, Any]:
chat_member = await self._get_chat_member(message, bot)
async def __call__(
self, update: types.TelegramObject,
event_from_user: types.User,
chat: Chat,
bot: Bot
) -> bool | dict[str, Any]:
chat_member = await self._get_chat_member(update, event_from_user, chat, bot)
if not chat_member:
return False
if chat_member.status == ChatMemberStatus.CREATOR:
Expand All @@ -72,8 +82,8 @@ async def __call__(self, message: types.Message, bot: Bot) -> bool | dict[str, A

return {self.PAYLOAD_ARGUMENT_NAME: chat_member}

def get_target_id(self, message: types.Message, bot: Bot) -> int | None:
return message.from_user.id
def get_target_id(self, update: types.TelegramObject, user: types.User, bot: Bot) -> int | None:
return user.id


@dataclass
Expand All @@ -84,7 +94,7 @@ class TargetHasPermissions(HasPermissions):
can_be_same: bool = False
can_be_bot: bool = False

def get_target_id(self, message: types.Message, bot: Bot) -> int | None:
def get_target_id(self, message: types.Message, user: types.User, bot: Bot) -> int | None:
target_user = get_target_user(message, self.can_be_same, self.can_be_bot)
if target_user is None:
return None
Expand All @@ -108,5 +118,5 @@ class BotHasPermissions(HasPermissions):
}
PAYLOAD_ARGUMENT_NAME = "bot_member"

def get_target_id(self, message: types.Message, bot: Bot) -> int | None:
def get_target_id(self, message: types.Message, user: types.User, bot: Bot) -> int | None:
return bot.id
35 changes: 35 additions & 0 deletions app/handlers/keyboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton

from app.models.db import User, KarmaEvent, ModeratorEvent
from app.models.db.report import Report


class KarmaCancelCb(CallbackData, prefix="karma_cancel"):
Expand All @@ -16,6 +17,21 @@ class WarnCancelCb(CallbackData, prefix="warn_cancel"):
moderator_event_id: int


class ApproveReportCb(CallbackData, prefix="approve_report"):
report_id: int
reporter_id: int


class DeclineReportCb(CallbackData, prefix="decline_report"):
report_id: int
reporter_id: int


class CancelReportCb(CallbackData, prefix="cancel_report"):
report_id: int
reporter_id: int


def get_kb_karma_cancel(
user: User, karma_event: KarmaEvent, rollback_karma: float, moderator_event: ModeratorEvent
) -> InlineKeyboardMarkup:
Expand Down Expand Up @@ -86,3 +102,22 @@ def get_paste_kb():
),
]]
)


def get_report_reaction_kb(user: User, report: Report) -> InlineKeyboardMarkup:
approve = InlineKeyboardButton(
text='Подтвердить',
callback_data=ApproveReportCb(report_id=report.id, reporter_id=user.id).pack()
)
decline = InlineKeyboardButton(
text='Отклонить',
callback_data=DeclineReportCb(report_id=report.id, reporter_id=user.id).pack()
)
cancel = InlineKeyboardButton(
text='Отменить',
callback_data=CancelReportCb(report_id=report.id, reporter_id=user.id).pack()
)
return InlineKeyboardMarkup(inline_keyboard=[
[approve, decline],
[cancel]
])
144 changes: 132 additions & 12 deletions app/handlers/moderator.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import asyncio
import random
from contextlib import suppress

from aiogram import Bot, F, Router, types
from aiogram.enums import ChatMemberStatus
from aiogram.exceptions import TelegramUnauthorizedError
from aiogram.filters import Command, CommandObject
from aiogram.exceptions import TelegramUnauthorizedError, TelegramBadRequest
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.handlers import keyboards as kb
from app.models.config import Config
from app.models.db import Chat, User
from app.models.db.report import ReportStatus
from app.services.moderation import (
ban_user,
delete_moderator_event,
Expand All @@ -23,26 +27,36 @@
warn_user,
)
from app.services.remove_message import delete_message, remove_kb
from app.services.report import register_report, resolve_report, reward_reporter
from app.services.user_info import get_user_info
from app.utils.exceptions import ModerationError, TimedeltaParseError
from app.utils.log import Logger

from . import keyboards as kb

logger = Logger(__name__)
router = Router(name=__name__)


@router.message(
F.chat.type.in_(["group", "supergroup"]),
F.reply_to_message,
HasTargetFilter(),
Command('report', 'admin', 'spam', prefix="/!@"),
)
async def report(message: types.Message, bot: Bot):
async def report_message(message: types.Message, chat: Chat, user: User, target: User, bot: Bot):
logger.info("user {user} report for message {message}", user=message.from_user.id, message=message.message_id)
answer_template = "Спасибо за сообщение. Мы обязательно разберёмся. "
answer_message = "Спасибо за сообщение. Мы обязательно разберёмся"
admins_mention = await get_mentions_admins(message.chat, bot)
await message.reply(answer_template + admins_mention + " ")

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
)

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


@router.message(
Expand Down Expand Up @@ -248,11 +262,117 @@ async def cmd_unhandled(message: types.Message):
await delete_message(message)


@router.callback_query(kb.WarnCancelCb.filter())
@router.callback_query(
kb.WarnCancelCb.filter(),
MagicData(F.user.tg_id == F.callback_data.from_user.id)
)
async def cancel_warn(callback_query: types.CallbackQuery, callback_data: kb.WarnCancelCb):
from_user = callback_query.from_user
if callback_data.user_id != from_user.id:
return await callback_query.answer("Эта кнопка не для Вас", cache_time=3600)
await delete_moderator_event(callback_data.moderator_event_id, moderator=from_user)
await callback_query.answer("Предупреждение было отменено", show_alert=True)
await callback_query.answer("Вы отменили предупреждение", show_alert=True)
await callback_query.message.delete()


@router.callback_query(
kb.ApproveReportCb.filter(),
HasPermissions(can_restrict_members=True),
)
async def approve_report_handler(
callback_query: types.CallbackQuery,
callback_data: kb.ApproveReportCb,
user: User,
chat: Chat,
bot: Bot,
config: Config
):
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 config.report_karma_award:
bomzheg marked this conversation as resolved.
Show resolved Hide resolved
karma_change_result = await reward_reporter(
reporter_id=callback_data.reporter_id,
chat=chat,
reward_amount=config.report_karma_award,
bot=bot
)
await callback_query.message.edit_text(
"<b>{reporter}</b> получил <b>+{reward_amount}</b> кармы в награду за репорт!".format(
reporter=hd.quote(karma_change_result.karma_event.user_to.fullname),
reward_amount=config.report_karma_award
)
)
await callback_query.answer("Вы подтвердили репорт")

with suppress(TelegramBadRequest):
await callback_query.message.reply_to_message.delete()
with suppress(TelegramBadRequest):
await callback_query.message.edit_reply_markup()


@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
):
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
)
await callback_query.answer("Вы отклонили репорт")

with suppress(TelegramBadRequest):
await callback_query.message.reply_to_message.delete()
with suppress(TelegramBadRequest):
await callback_query.message.delete()


@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
):
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
)
await callback_query.answer("Вы отменили репорт")

with suppress(TelegramBadRequest):
await callback_query.message.reply_to_message.delete()
with suppress(TelegramBadRequest):
await callback_query.message.delete()


@router.callback_query(
kb.WarnCancelCb.filter(),
MagicData(F.callback_data.user_id != F.callback_query.from_user.id)
)
@router.callback_query(
kb.CancelReportCb.filter(),
MagicData(F.user.id != F.callback_data.reporter_id)
)
@router.callback_query(kb.ApproveReportCb.filter(), ~HasPermissions(can_restrict_members=True))
@router.callback_query(kb.DeclineReportCb.filter(), ~HasPermissions(can_restrict_members=True))
async def unauthorized_button_action(callback_query: types.CallbackQuery, config: Config):
await callback_query.answer(
"Эта кнопка не для Вас",
cache_time=config.callback_query_answer_cache_time
)
Loading
Loading