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

Restrict on negative karma #72

Merged
merged 28 commits into from
Nov 27, 2020
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1089dda
add transaction to change_karma services
Nov 9, 2020
45de209
split ban and ro to handler part and service part
Nov 9, 2020
10a2dcf
fixes, separate logic between functions, refactoring
Nov 10, 2020
627ede4
add __all__ for linter fix
Nov 10, 2020
bd6282a
add automatically restriction after very low karma
Nov 10, 2020
dc73e4a
fixes
Nov 10, 2020
2634b76
add config param to enable/disable auto restrict on negative karma
Nov 11, 2020
c4a2c69
move render message in separate function
Nov 11, 2020
d91399f
move condition to separate function
Nov 11, 2020
997b1a1
move TypeRestriction to models package
Nov 11, 2020
3dc84a5
rename variable
Nov 11, 2020
12c4fbf
add function for check has user restriction or not
Nov 11, 2020
687af15
fix exception clause
bomzheg Nov 26, 2020
f8e157c
fix typehint (change destination in pyrogram)
bomzheg Nov 26, 2020
342c24f
add auto restrict on negative karma
bomzheg Nov 26, 2020
51a2ab0
last one restriction on negative karma must be ban
bomzheg Nov 26, 2020
3116eb0
text fixes
bomzheg Nov 26, 2020
644b5f1
update .gitignore
Nov 26, 2020
bae0093
fix texts
Nov 26, 2020
96d6394
refactor more logic karmic ro
Nov 26, 2020
b347aad
user cant negative karma to target with RO
Nov 26, 2020
f423165
BUGFIXES, fix cancel change karma
Nov 26, 2020
a467f30
correct shield restricted from decrease karma and notify user for that
Nov 27, 2020
105f639
fix typos
Nov 27, 2020
0ce1153
fix race condition in case one user change karma and make it less tha…
Nov 27, 2020
71e8745
rename consts
Nov 27, 2020
87d6844
add few info to README.md
Nov 27, 2020
b8fc588
Merge branch 'master' into restrict-on-negative-karma
bomzheg Nov 27, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,24 @@
"""
import os
import secrets
import typing
from datetime import timedelta
from functools import partial
from pathlib import Path

from aiogram import Bot
from dotenv import load_dotenv

from app.models.common import TypeRestriction

app_dir: Path = Path(__file__).parent.parent
load_dotenv(str(app_dir / '.env'))

PLUS = "+"
PLUS_WORDS = frozenset(
{"спасибо", "спс", "спасибочки", "благодарю", "пасиба", "пасеба", "посеба", "благодарочка", "thx", "мерси", "выручил"}
)
PLUS_WORDS = frozenset({
bomzheg marked this conversation as resolved.
Show resolved Hide resolved
"спасибо", "спс", "спасибочки", "благодарю", "пасиба", "пасеба", "посеба",
"благодарочка", "thx", "мерси", "выручил",
})
PLUS_TRIGGERS = frozenset({PLUS, *PLUS_WORDS})
PLUS_EMOJI = frozenset({"👍", })
MINUS = "-"
Expand All @@ -22,6 +29,38 @@

TIME_TO_CANCEL_ACTIONS = 60

DEFAULT_DURATION = timedelta(hours=1) # длительность ограничения по умолчанию
bomzheg marked this conversation as resolved.
Show resolved Hide resolved
FOREVER_DURATION = timedelta(days=666) # длительность ограничения "навсегда"

# auto restrict when karma less than NEGATIVE_KARMA_TO_RESTRICT
ENABLE_AUTO_RESTRICT_ON_NEGATIVE_KARMA = bool(int(os.getenv("ENABLE_AUTO_RESTRICT_ON_NEGATIVE_KARMA", default=0)))

NEGATIVE_KARMA_TO_RESTRICT = -100
KARMA_AFTER_RESTRICT = -80


class RestrictionPlanElem(typing.NamedTuple):
duration: timedelta
type_restriction: TypeRestriction


RESTRICTIONS_PLAN: typing.List[RestrictionPlanElem] = [
bomzheg marked this conversation as resolved.
Show resolved Hide resolved
RestrictionPlanElem(timedelta(days=7), TypeRestriction.auto_for_negative_carma),
RestrictionPlanElem(timedelta(days=30), TypeRestriction.auto_for_negative_carma),
RestrictionPlanElem(FOREVER_DURATION, TypeRestriction.ban),
]

RO_ACTION = partial(Bot.restrict_chat_member, can_send_messages=False)
BAN_ACTION = Bot.kick_chat_member
AUTO_RESTRICT_ACTION = RO_ACTION

action_for_restrict = {
TypeRestriction.ban: BAN_ACTION,
TypeRestriction.ro: RO_ACTION,
TypeRestriction.auto_for_negative_carma: AUTO_RESTRICT_ACTION,
}
COMMENT_AUTO_RESTRICT = f"Карма ниже {NEGATIVE_KARMA_TO_RESTRICT}"

PROG_NAME = "KarmaBot"
PROG_DESC = (
"This program is a Python 3+ script. The script launches a bot in Telegram,"
Expand Down
35 changes: 31 additions & 4 deletions app/handlers/change_karma.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
from app.utils.exceptions import SubZeroKarma, AutoLike
from app.services.remove_message import remove_kb_after_sleep
from . import keyboards as kb
from ..services.adaptive_trottle import AdaptiveThrottle

from app.services.adaptive_trottle import AdaptiveThrottle
from app.services.moderation import TypeRestriction, it_was_last_one_auto_restrict
from app.utils.timedelta_functions import format_timedelta

a_throttle = AdaptiveThrottle()

Expand All @@ -37,12 +38,13 @@ async def too_fast_change_karma(message: types.Message, *_, **__):
async def karma_change(message: types.Message, karma: dict, user: User, chat: Chat, target: User):

try:
uk, abs_change, karma_event = await change_karma(
uk, abs_change, karma_event, count_auto_restrict = await change_karma(
target_user=target,
chat=chat,
user=user,
how_change=karma['karma_change'],
comment=karma['comment']
comment=karma['comment'],
bot=message.bot,
)
except SubZeroKarma:
return await message.reply("У Вас слишком мало кармы для этого")
Expand All @@ -59,9 +61,34 @@ async def karma_change(message: types.Message, karma: dict, user: User, chat: Ch
disable_web_page_preview=True,
reply_markup=kb.get_kb_karma_cancel(user, karma_event)
)
if count_auto_restrict:
await message.answer(await render_text_auto_restrict(count_auto_restrict, target))
asyncio.create_task(remove_kb_after_sleep(msg, config.TIME_TO_CANCEL_ACTIONS))


async def render_text_auto_restrict(count_auto_restrict: int, target: User):
# TODO чото надо сделать с этим чтобы понятно объяснить за что RO и что будет в следующий раз
if it_was_last_one_auto_restrict(count_auto_restrict):
about_next = ""
bomzheg marked this conversation as resolved.
Show resolved Hide resolved
else:
about_next = (
f"Вам установлена карма {config.KARMA_AFTER_RESTRICT}. "
f"Если Ваша карма снова достигнет {config.NEGATIVE_KARMA_TO_RESTRICT} "
f"Ваш RO будет перманентный."
)
return (
"{target}, Уровень вашей кармы снизился ниже {negative_limit}. "
"За это вы попадаете в {type_restriction} на срок {duration}!\n"
"{about_next}".format(
target=target.mention_link,
negative_limit=config.NEGATIVE_KARMA_TO_RESTRICT,
type_restriction=TypeRestriction.ro.name,
duration=format_timedelta(config.RESTRICTIONS_PLAN[count_auto_restrict - 1]),
about_next=about_next,
)
)


@dp.callback_query_handler(kb.cb_karma_cancel.filter())
async def cancel_karma(callback_query: types.CallbackQuery, callback_data: typing.Dict[str, str]):
if int(callback_data['user_id']) != callback_query.from_user.id:
Expand Down
94 changes: 13 additions & 81 deletions app/handlers/moderator.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import typing
from datetime import timedelta

from aiogram import types
from aiogram.utils.exceptions import BadRequest, Unauthorized
from aiogram.utils.exceptions import Unauthorized
from aiogram.utils.markdown import hide_link, quote_html
from loguru import logger

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.utils.exceptions import TimedeltaParseError, ModerationError
from app.models import Chat, User
from app.services.user_info import get_user_info
from app.services.moderation import warn_user
from app.services.moderation import warn_user, ro_user, ban_user, get_duration
from app.services.remove_message import delete_message

FOREVER_DURATION = timedelta(days=366)
DEFAULT_DURATION = timedelta(hours=1)


@dp.message_handler(
types.ChatType.is_group_or_super_group,
Expand Down Expand Up @@ -47,65 +40,25 @@ 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 like: "!ro 13d don't flood"
if not args:
return "", ""
duration_text = args[0]
if len(args) == 1:
return duration_text, ""
return duration_text, " ".join(args[1:])


def get_duration(text: str):
duration_text, comment = get_moderator_message_args(text)
if duration_text:
duration = parse_timedelta_from_text(duration_text)
else:
duration = DEFAULT_DURATION
return duration, comment


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

try:
await message.chat.restrict(target.tg_id, can_send_messages=False, until_date=duration)
logger.info(
"User {user} restricted by {admin} for {duration}",
user=target.tg_id,
admin=user.tg_id,
duration=duration,
)
except BadRequest as e:
success_text = await ro_user(message.chat, target, user, duration, comment, message.bot)
except ModerationError as e:
logger.error("Failed to restrict chat member: {error!r}", error=e)
return False
else:
await ModeratorEvent.save_new_action(
moderator=user,
user=target,
chat=chat,
type_restriction="ro",
duration=duration,
comment=comment
)

await message.reply(
"Пользователь {user} сможет <b>только читать</b> сообщения на протяжении {duration}".format(
user=target.mention_link,
duration=format_timedelta(duration),
)
)
await message.reply(success_text)


@dp.message_handler(
Expand All @@ -115,39 +68,18 @@ async def cmd_ro(message: types.Message, chat: Chat, user: User, target: User):
user_can_restrict_members=True,
bot_can_restrict_members=True,
)
async def cmd_ban(message: types.Message, chat: Chat, user: User, target: User):
async def cmd_ban(message: types.Message, user: User, target: User):
try:
duration, comment = get_duration(message.text)
except TimedeltaParseError as e:
return await message.reply(f"Не могу распознать время. {quote_html(e.text)}")

try:
await message.chat.kick(target.tg_id, until_date=duration)
logger.info(
"User {user} kicked by {admin} for {duration}",
user=target.tg_id,
admin=user.tg_id,
duration=duration,
)
except BadRequest as e:
success_text = await ban_user(message.chat, target, user, duration, comment, message.bot)
except ModerationError as e:
logger.error("Failed to kick chat member: {error!r}", error=e)
return False
else:
await ModeratorEvent.save_new_action(
moderator=user,
user=target,
chat=chat,
type_restriction="ban",
duration=duration,
comment=comment
)

text = "Пользователь {user} попал в бан этого чата.".format(
user=target.mention_link,
)
if duration < FOREVER_DURATION:
text += " Он сможет вернуться через {duration}".format(duration=format_timedelta(duration))
await message.reply(text)
await message.reply(success_text)


@dp.message_handler(
Expand Down Expand Up @@ -187,7 +119,7 @@ async def get_info_about_user(message: types.Message, chat: Chat, target: User):
disable_web_page_preview=True
)
except Unauthorized:
await message.reply("Напишите мне в личку /start и повторите команду.")
await message.reply(f"{message.from_user.get_mention()}, напишите мне в личку /start и повторите команду.")
finally:
await delete_message(message)

Expand Down
8 changes: 8 additions & 0 deletions app/models/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from enum import Enum


class TypeRestriction(Enum):
ro = "ro"
ban = "ban"
warn = "warn"
auto_for_negative_carma = "auto_for_negative_carma"
8 changes: 5 additions & 3 deletions app/models/moderator_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ class Meta:
table = 'moderator_events'

def __repr__(self):
# noinspection PyUnresolvedReferences
return (
f"KarmaEvent {self.id_} from moderator {self.moderator.id} to {self.user.id}, date {self.date}, "
f"ModeratorEvent {self.id_} from moderator {self.moderator_id} to {self.user_id}, date {self.date}, "
f"type_restriction {self.type_restriction} timedelta_restriction {self.timedelta_restriction}"
)

Expand All @@ -41,7 +42,8 @@ async def save_new_action(
chat: Chat,
type_restriction: str,
duration: timedelta = None,
comment: str = ""
comment: str = "",
using_db=None,
):
moderator_event = ModeratorEvent(
moderator=moderator,
Expand All @@ -51,7 +53,7 @@ async def save_new_action(
timedelta_restriction=duration,
comment=comment
)
await moderator_event.save()
await moderator_event.save(using_db=using_db)
return moderator_event

@classmethod
Expand Down
16 changes: 16 additions & 0 deletions app/models/user.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from datetime import datetime

from aiogram import types
from aiogram.utils.markdown import hlink, quote_html
from tortoise import fields
Expand All @@ -6,6 +8,7 @@

from app.utils.exceptions import UserWithoutUserIdError
from .chat import Chat
from .common import TypeRestriction


class User(Model):
Expand All @@ -17,6 +20,8 @@ class User(Model):
is_bot: bool = fields.BooleanField(null=True)
# noinspection PyUnresolvedReferences
karma: fields.ReverseRelation['UserKarma']
# noinspection PyUnresolvedReferences
my_restriction_events: fields.ReverseRelation['ModeratorEvent']

class Meta:
table = "users"
Expand Down Expand Up @@ -116,6 +121,17 @@ async def get_number_in_top_karma(self, chat: Chat) -> int:
uk: "UserKarma" = await self.get_uk(chat)
return await uk.number_in_top()

async def has_now_ro(self, chat: Chat):
my_restrictions = await self.my_restriction_events.filter(
chat=chat,
type_restriction=TypeRestriction.ro.name
).all()
for my_restriction in my_restrictions:
if my_restriction.timedelta_restriction \
and my_restriction.date + my_restriction.timedelta_restriction > datetime.now():
return True
return False

def to_json(self):
return dict(
id=self.id,
Expand Down
Loading