Skip to content

Commit

Permalink
Moderation (#45)
Browse files Browse the repository at this point in the history
* fixes

* fixes

* add ro command

* add documentation for new commands

* add ban (+docs) add messages about errors

* refactor exception, move logic send answer to handler

* rename commnet with chat name

* bugfix deprecated function

* now minus triggers must be in first line

* remove logging

* add save in karma events table all events with karma

* add warns, add events restriction

* add get info user

* update formating info
  • Loading branch information
bomzheg authored Aug 19, 2020
1 parent 7a79332 commit a9bd222
Show file tree
Hide file tree
Showing 14 changed files with 302 additions and 59 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ moderator commands list:
* EEE - count of minutes
* FFF - count of seconds (less that 30 seconds will be mean 30 seconds)
* you have to specify one or more duration part without spaces
* !warn, !w - official warn user from moderator
* !info - information about user (karma changes, restrictions, warns)

superuser commands list:
* /generate_invite_logchat - if bot is admin in chat of LOG_CHAT_ID from config.py bot generates invite link to that
Expand Down
4 changes: 3 additions & 1 deletion app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@
SUPERUSERS = {GLOBAL_ADMIN_ID, BOMZHEG_ID, BORNTOHACK_ID, ENTERESSI_ID, VADIM_ID,
RUD_ID, LET45FC_ID, STUDENT_ID}
LOG_CHAT_ID = -1001404337089
DUMP_CHAT_ID = -1001459777201 # Fucin' Testing Area
DUMP_CHAT_ID = -1001459777201 # ⚙️Testing Area >>> Python Scripts

DATE_FORMAT = '%d.%m.%Y'

WEBHOOK_HOST = os.getenv("WEBHOOK_HOST")
WEBHOOK_PORT = os.getenv("WEBHOOK_PORT", default=443)
Expand Down
8 changes: 6 additions & 2 deletions app/filters/karma_change.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def get_karma_trigger(text: str) -> typing.Optional[int]:
"""
if has_plus_karma(get_first_word(text)):
return +1
if has_minus_karma(text):
if has_minus_karma(get_first_line(text)):
return -1
return None

Expand All @@ -63,6 +63,10 @@ def has_minus_karma(text: str) -> bool:
return text in MINUS or (text.split(maxsplit=1)[0] == text and text[0] in MINUS_EMOJI)


def get_first_line(text: str) -> str:
return text.splitlines()[0]


def get_target_user(message: types.Message) -> typing.Optional[types.user.User]:
"""
Target user can be take from reply or by mention
Expand Down Expand Up @@ -123,4 +127,4 @@ def is_bot_username(username: str):
"""
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[-2:] == "bot"
return username is not None and username[-3:] == "bot"
8 changes: 4 additions & 4 deletions app/handlers/change_karma.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from app.misc import dp
from app.models.chat import Chat
from app.models.user import User
from app.models.user_karma import UserKarma
from app.services.user_getter import UserGetter
from app.services.change_karma import change_karma
from app.utils.exceptions import UserWithoutUserIdError, SubZeroKarma

how_change = {
Expand Down Expand Up @@ -46,10 +46,10 @@ async def karma_change(message: types.Message, karma: dict, user: User, chat: Ch
return logger.info("user {user} try to change self or bot karma ", user=user.tg_id)

try:
uk, power = await UserKarma.change_or_create(
uk, power = await change_karma(
target_user=target_user,
chat=chat,
user_changed=user,
user=user,
how_change=karma['karma_change']
)
except SubZeroKarma:
Expand All @@ -59,7 +59,7 @@ async def karma_change(message: types.Message, karma: dict, user: User, chat: Ch
return_text = (
"Вы {how_change} карму "
"{name} до {karma_new} "
"({power:+.1f})".format(
"({power:+.2f})".format(
how_change=how_change[karma['karma_change']],
name=hbold(target_user.fullname),
karma_new=hbold(uk.karma_round),
Expand Down
115 changes: 99 additions & 16 deletions app/handlers/moderator.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import typing
from datetime import timedelta

from aiogram import types
from aiogram.dispatcher.filters.builtin import Text, Command
from aiogram.utils.exceptions import BadRequest
from aiogram.utils.markdown import hide_link
from aiogram.dispatcher.filters.builtin import Command
from aiogram.utils.exceptions import BadRequest, Unauthorized
from aiogram.utils.markdown import hide_link, quote_html
from loguru import logger

from app.misc import dp
from app.utils.timedelta_functions import parse_timedelta_from_message, format_timedelta
from app.misc import dp, bot
from app.utils.timedelta_functions import parse_timedelta_from_text, format_timedelta
from app.utils.exceptions import TimedeltaParseError
from app.models import ModeratorEvent, Chat, User
from app.services.user_info import get_user_info
FOREVER_DURATION = timedelta(days=366)
DEFAULT_DURATION = timedelta(hours=1)


async def report_filter(message: types.Message):
Expand All @@ -18,9 +23,6 @@ async def report_filter(message: types.Message):
return False
if await Command(commands=['report', 'admin'], prefixes='/!@').check(message):
return True
if await Text(equals='@admin', ignore_case=True).check(message):
print("text with @admin 0_o")
return True
return False


Expand Down Expand Up @@ -49,17 +51,32 @@ def need_notify_admin(admin: types.ChatMember):
return admin.can_delete_messages or admin.can_restrict_members or admin.status == types.ChatMemberStatus.CREATOR


def get_moderator_message_args(text: str) -> typing.Tuple[str, str]:
_, *args = text.split(maxsplit=2) # in text: command_duration_comments
if not args:
return "", ""
duration_text = args[0]
if len(args) == 1:
return duration_text, ""
return duration_text, " ".join(args[1:])


@dp.message_handler(
commands=["ro", "mute"],
commands_prefix="!",
is_reply=True,
user_can_restrict_members=True,
bot_can_restrict_members=True,
)
async def cmd_ro(message: types.Message):
duration = await parse_timedelta_from_message(message)
if not duration:
return
async def cmd_ro(message: types.Message, chat: Chat):
duration_text, comment = get_moderator_message_args(message.text)
if duration_text:
try:
duration = parse_timedelta_from_text(duration_text)
except TimedeltaParseError as e:
return await message.reply(f"Не могу распознать время. {quote_html(e.text)}")
else:
duration = DEFAULT_DURATION

try:
await message.chat.restrict(
Expand All @@ -74,6 +91,15 @@ async def cmd_ro(message: types.Message):
except BadRequest as e:
logger.error("Failed to restrict chat member: {error!r}", error=e)
return False
else:
await ModeratorEvent.save_new_action(
moderator=message.from_user,
user=message.reply_to_message.from_user,
chat=chat,
type_restriction="ro",
duration=duration,
comment=comment
)

await message.reply(
"Пользователь {user} сможет <b>только читать</b> сообщения на протяжении {duration}".format(
Expand All @@ -90,10 +116,15 @@ async def cmd_ro(message: types.Message):
user_can_restrict_members=True,
bot_can_restrict_members=True,
)
async def cmd_ban(message: types.Message):
duration = await parse_timedelta_from_message(message)
if not duration:
return
async def cmd_ban(message: types.Message, chat: Chat):
duration_text, comment = get_moderator_message_args(message.text)
if duration_text:
try:
duration = parse_timedelta_from_text(duration_text)
except TimedeltaParseError as e:
return await message.reply(f"Не могу распознать время. {quote_html(e.text)}")
else:
duration = DEFAULT_DURATION

try:
await message.chat.kick(message.reply_to_message.from_user.id, until_date=duration)
Expand All @@ -106,6 +137,15 @@ async def cmd_ban(message: types.Message):
except BadRequest as e:
logger.error("Failed to kick chat member: {error!r}", error=e)
return False
else:
await ModeratorEvent.save_new_action(
moderator=message.from_user,
user=message.reply_to_message.from_user,
chat=chat,
type_restriction="ban",
duration=duration,
comment=comment
)

text = "Пользователь {user} попал в бан этого чата.".format(
user=message.reply_to_message.from_user.get_mention(),
Expand All @@ -132,3 +172,46 @@ async def cmd_ro_bot_not_admin(message: types.Message):
)
async def cmd_ro_bot_not_admin(message: types.Message):
await message.delete()


@dp.message_handler(
commands=["w", "warn"],
commands_prefix="!",
is_reply=True,
user_can_restrict_members=True,
)
async def cmd_warn(message: types.Message, chat: Chat):
args = message.text.split(maxsplit=1)
if len(args) == 1:
comment = ""
else:
comment = args[1]

await ModeratorEvent.save_new_action(
moderator=message.from_user,
user=message.reply_to_message.from_user,
chat=chat,
type_restriction="warn",
comment=comment
)

text = "Пользователь {user} получил официальное предупреждение от модератора".format(
user=message.reply_to_message.from_user.get_mention(),
)
await message.reply(text)


@dp.message_handler(commands="info", commands_prefix='!', is_reply=True)
async def get_info_about_user(message: types.Message, chat: Chat):
target_user = await User.get_or_create_from_tg_user(message.reply_to_message.from_user)
info = await get_user_info(target_user, chat)
try:
await bot.send_message(
message.from_user.id,
f"Данные на {target_user.mention_link}:\n" + "\n".join(info),
disable_web_page_preview=True
)
except Unauthorized:
await message.reply("Напишите мне в личку /start и повторите команду.")
finally:
await message.delete()
8 changes: 3 additions & 5 deletions app/middlewares/acl.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,16 @@ def __init__(self):
self.lock_factory = LockFactory()

async def setup_chat(self, data: dict, user: types.User, chat: Optional[types.Chat] = None):
logger.debug("starting setup chat {chat}", chat=chat.id)
try:
async with self.lock_factory.get_lock(f"{chat.id}:{user.id}"):
logger.debug("in lock setup chat {chat}", chat=chat.id)
async with self.lock_factory.get_lock(f"{user.id}"):
user = await User.get_or_create_from_tg_user(user)
if chat and chat.type != 'private':
if chat and chat.type != 'private':
async with self.lock_factory.get_lock(f"{chat.id}"):
chat = await Chat.get_or_create_from_tg_chat(chat)

except Exception as e:
logger.error("troubles with db")
raise e
logger.debug("after lock setup chat {chat}", chat=chat)
data["user"] = user
data["chat"] = chat

Expand Down
8 changes: 6 additions & 2 deletions app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from .chat import Chat, ChatType
from .user import User
from .user_karma import UserKarma
from .karma_actions import KarmaEvent
from .moderator_actions import ModeratorEvent

__all__ = [Chat, ChatType, User, UserKarma]
__all__ = [Chat, ChatType, User, UserKarma, KarmaEvent]
__models__ = [
'app.models.user',
'app.models.chat',
'app.models.user_karma'
'app.models.user_karma',
'app.models.karma_actions',
'app.models.moderator_actions',
]
47 changes: 47 additions & 0 deletions app/models/karma_actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from tortoise import fields
from tortoise.models import Model

from .user import User
from .chat import Chat
from app import config


class KarmaEvent(Model):
id_ = fields.IntField(pk=True, source_field="id")
user_from: fields.ForeignKeyRelation[User] = fields.ForeignKeyField(
'models.User', related_name='i_change_karma_events')
user_to: fields.ForeignKeyRelation[User] = fields.ForeignKeyField(
'models.User', related_name='my_karma_events')
chat: fields.ForeignKeyRelation[Chat] = fields.ForeignKeyField(
'models.Chat', related_name='karma_events')
date = fields.DatetimeField(auto_now=True, null=False)
how_change = fields.FloatField(
description="how match change karma in percent of possible power"
)
how_match_change = fields.FloatField(
description="how match user_from change karma user_to in absolute"
)
comment = fields.TextField(null=True)

class Meta:
table = 'karma_events'

def __str__(self):
return (
f"KarmaEvent {self.id_} from user {self.user_from.id} to {self.user_to.id}, "
f"date {self.date}, change {self.how_change}"
)
__repr__ = __str__

@classmethod
async def get_last_by_user(cls, user: User, chat: Chat, limit: int = 10):
return await cls.filter(
user_to=user,
chat=chat
).order_by('-date').limit(limit).prefetch_related("user_from").all()

def format_event(self):
return (
f"{self.date.date().strftime(config.DATE_FORMAT)} "
f"{self.user_from.mention_no_link} изменил карму на {self.how_change:.0%} своей силы. "
) + (f"\"{self.comment}\"" if self.comment is not None else "")
Loading

0 comments on commit a9bd222

Please sign in to comment.