Skip to content

Commit

Permalink
Restrict on negative karma (#72)
Browse files Browse the repository at this point in the history
* add transaction to change_karma services

* split ban and ro to handler part and service part

* fixes, separate logic between functions, refactoring

* add __all__ for linter fix

* add automatically restriction after very low karma

* fixes

* add config param to enable/disable auto restrict on negative karma

* move render message in separate function

* move condition to separate function

* move TypeRestriction to models package

* rename variable

* add function for check has user restriction or not

* fix exception clause

* fix typehint (change destination in pyrogram)

* add auto restrict on negative karma

* last one restriction on negative karma must be ban

* text fixes

* update .gitignore

* fix texts

* refactor more logic karmic ro

* user cant negative karma to target with RO
new exception and Exception group - for skip negative karma process
remove deprecated filters

* BUGFIXES, fix cancel change karma

* correct shield restricted from decrease karma and notify user for that

* fix typos
remove unused comments
remove unused imports

* fix race condition in case one user change karma and make it less than -100, bot change karma to -80 than another user change same karma and than first user cancel his action.
Now it case correct work cancel imitate that user newer do cancelled action

* rename consts

* add few info to README.md

Co-authored-by: Yuriy Chebyshev <cheb.yuriy@gmail.com>
  • Loading branch information
bomzheg and Yuriy Chebyshev authored Nov 27, 2020
1 parent 681e69e commit 13a23fc
Show file tree
Hide file tree
Showing 19 changed files with 497 additions and 172 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ log/
*/.pytest_cache/
jsons/
*.session
__pycache__/
db_data/
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ to the group administrators
* !idchat - get id of chat, your id, and id of replayed user

moderator commands list:
* !ro !mute [DURATION] - restrict replyed user for DURATION.
* !ban [DURATION] - kick replyed user for DURATION
* !ro !mute [DURATION] [@mention] - restrict replied or mentioned user for DURATION.
* !ban [DURATION] [@mention] - kick replied user for DURATION
* DURATION in format [AAAy][BBBw][CCCd][DDDh][EEEm][FFFs] where:
* AAA - count of years (more that one years is permanent)
* BBB - count of weeks
Expand All @@ -30,8 +30,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)
* !warn, !w [@mention] - official warn user from moderator
* !info [@mention] - 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
55 changes: 52 additions & 3 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,33 @@
"""
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({
"спасибо",
"спс",
"спасибочки",
"благодарю",
"пасиба",
"пасеба",
"посеба",
"благодарочка",
"thx",
"мерси",
"выручил",
})
PLUS_TRIGGERS = frozenset({PLUS, *PLUS_WORDS})
PLUS_EMOJI = frozenset({"👍", })
MINUS = "-"
Expand All @@ -22,6 +38,39 @@

TIME_TO_CANCEL_ACTIONS = 60

DEFAULT_RESTRICT_DURATION = timedelta(hours=1)
FOREVER_RESTRICT_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] = [
RestrictionPlanElem(timedelta(days=7), TypeRestriction.karmic_ro),
RestrictionPlanElem(timedelta(days=30), TypeRestriction.karmic_ro),
RestrictionPlanElem(FOREVER_RESTRICT_DURATION, TypeRestriction.karmic_ban),
]

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

action_for_restrict = {
TypeRestriction.ban: BAN_ACTION,
TypeRestriction.ro: RO_ACTION,
TypeRestriction.karmic_ro: RO_ACTION,
TypeRestriction.karmic_ban: BAN_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
77 changes: 63 additions & 14 deletions app/handlers/change_karma.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@

from aiogram import types
from aiogram.types import ContentType
from aiogram.utils.markdown import hbold
from loguru import logger

from app.misc import dp
from app import config
from app.models import Chat, User
from app.services.change_karma import change_karma, cancel_karma_change
from app.utils.exceptions import SubZeroKarma, AutoLike
from app.utils.exceptions import SubZeroKarma, CantChangeKarma, DontOffendRestricted
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 it_was_last_one_auto_restrict
from app.utils.timedelta_functions import format_timedelta


a_throttle = AdaptiveThrottle()
Expand All @@ -37,35 +39,82 @@ 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(
result_change_karma = 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("У Вас слишком мало кармы для этого")
except AutoLike:
except DontOffendRestricted:
return await message.reply("Не обижай его, он и так наказан!")
except CantChangeKarma as e:
logger.info("user {user} can't change karma, {e}", user=user.tg_id, e=e)
return
if result_change_karma.count_auto_restrict:
notify_auto_restrict_text = await render_text_auto_restrict(result_change_karma.count_auto_restrict, target)
else:
notify_auto_restrict_text = ""

# How match karma was changed. Sign show changed difference, not difference for cancel
how_changed_karma = result_change_karma.user_karma.karma \
- result_change_karma.karma_after \
+ result_change_karma.abs_change

msg = await message.reply(
"Вы {how_change} карму {name} до {karma_new} ({power:+.2f})".format(
"Вы {how_change} карму <b>{name}</b> до <b>{karma_new:.2f}</b> ({power:+.2f})"
"\n\n{notify_auto_restrict_text}".format(
how_change=get_how_change_text(karma['karma_change']),
name=hbold(target.fullname),
karma_new=hbold(uk.karma_round),
power=abs_change,
name=target.fullname,
karma_new=result_change_karma.karma_after,
power=result_change_karma.abs_change,
notify_auto_restrict_text=notify_auto_restrict_text
),
disable_web_page_preview=True,
reply_markup=kb.get_kb_karma_cancel(user, karma_event)
reply_markup=kb.get_kb_karma_cancel(
user=user,
karma_event=result_change_karma.karma_event,
rollback_karma=-how_changed_karma,
moderator_event=result_change_karma.moderator_event,
)
)
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 и что будет в следующий раз
text = "{target}, Уровень вашей кармы стал ниже {negative_limit}.\n".format(
target=target.mention_link,
negative_limit=config.NEGATIVE_KARMA_TO_RESTRICT,
)
if it_was_last_one_auto_restrict(count_auto_restrict):
text += "Это был последний разрешённый раз. Теперь вы получаете вечное наказание."
else:
text += (
"За это вы наказаны на срок {duration}\n"
"Вам установлена карма {karma_after}. "
"Если Ваша карма снова достигнет {karma_to_restrict} "
"Ваше наказание будет строже.".format(
duration=format_timedelta(config.RESTRICTIONS_PLAN[count_auto_restrict - 1].duration),
karma_after=config.KARMA_AFTER_RESTRICT,
karma_to_restrict=config.NEGATIVE_KARMA_TO_RESTRICT,
)
)
return text


@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:
return await callback_query.answer("Эта кнопка не для вас", cache_time=3600)
await cancel_karma_change(callback_data['action_id'])
user_cancel_id = int(callback_data['user_id'])
if user_cancel_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)
await callback_query.answer("Вы отменили изменение кармы", show_alert=True)
await callback_query.message.delete()
5 changes: 2 additions & 3 deletions app/handlers/karma.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ async def get_top_from_private(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)
if len(parts) > 1:
chat = await Chat.get(chat_id=int(parts[1]))
else:
return await message.reply(
"Эту команду можно использовать только в группах "
"или с указанием id нужного чата, например:"
Expand All @@ -37,15 +36,15 @@ async def get_top(message: types.Message, chat: Chat, user: User):
await message.reply(text, disable_web_page_preview=True)


@dp.message_handler(ChatType.is_group_or_super_group, commands=["me"], commands_prefix='!')
@dp.message_handler(chat_type=[ChatType.GROUP, ChatType.SUPERGROUP], commands=["me"], commands_prefix='!')
@dp.throttled(rate=15)
async def get_top(message: types.Message, chat: Chat, user: User):
logger.info("user {user} ask his karma in chat {chat}", user=user.tg_id, chat=chat.chat_id)
uk, _ = await UserKarma.get_or_create(chat=chat, user=user)
await message.reply(f"Ваша карма в данном чате: {uk.karma_round}", disable_web_page_preview=True)


@dp.message_handler(ChatType.is_private, commands=["me"], commands_prefix='!')
@dp.message_handler(chat_type=ChatType.PRIVATE, commands=["me"], commands_prefix='!')
@dp.throttled(rate=15)
async def get_top(message: types.Message, user: User):
logger.info("user {user} ask his karma", user=user.tg_id)
Expand Down
15 changes: 11 additions & 4 deletions app/handlers/keyboards.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.utils.callback_data import CallbackData

from app.models import User, KarmaEvent
from app.models import User, KarmaEvent, ModeratorEvent

cb_karma_cancel = CallbackData("karma_cancel", "user_id", "action_id")
cb_karma_cancel = CallbackData("karma_cancel", "user_id", "karma_event_id", "rollback_karma", "moderator_event_id")


def get_kb_karma_cancel(user: User, action: KarmaEvent) -> InlineKeyboardMarkup:
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(user_id=user.tg_id, action_id=action.id_)
"Отменить", callback_data=cb_karma_cancel.new(
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",
)
)]])
Loading

2 comments on commit 13a23fc

@alekssamos
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Как правильно запустить тесты? Ни один не проходит.

@bomzheg
Copy link
Owner Author

@bomzheg bomzheg commented on 13a23fc Jan 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Как правильно запустить тесты? Ни один не проходит.

python -m pytest
pls, next time ask it in "issues"

Please sign in to comment.