wip: Правки по standard.welcome

This commit is contained in:
Maxim Slipenko 2024-08-13 21:24:40 +03:00
parent 81ddb8509f
commit 15cb6afb34
10 changed files with 679 additions and 476 deletions

View File

@ -1 +1 @@
from .main import module_init from .main import module_init, module_late_init

View File

@ -6,6 +6,9 @@
"version": "1.0.0", "version": "1.0.0",
"privileged": true, "privileged": true,
"dependencies": { "dependencies": {
"required": {
"standard.config": "^1.0.0"
},
"optional": { "optional": {
"standard.command_helper": "^1.0.0", "standard.command_helper": "^1.0.0",
"standard.filters": "^1.0.0" "standard.filters": "^1.0.0"

View File

@ -1,94 +1,165 @@
import asyncio import asyncio
import random import random
from string import Template
from typing import TYPE_CHECKING
from aiogram import Bot, Router, types from aiogram import Bot, Router, types
from aiogram.enums import ChatMemberStatus from aiogram.enums import ChatMemberStatus, ParseMode
from aiogram.filters import JOIN_TRANSITION, LEAVE_TRANSITION, ChatMemberUpdatedFilter from aiogram.filters import JOIN_TRANSITION, LEAVE_TRANSITION, ChatMemberUpdatedFilter
from aiogram.types import ChatMemberUpdated from aiogram.types import ChatMemberUpdated, PollAnswer
from ocab_core.modules_system.public_api import log, register_router from ocab_core.modules_system.public_api import get_module, log, register_router
from .utils import get_plural_form from .utils import MultiKeyDict, get_plural_form, key_from_poll, key_from_user_chat
from .verifications_methods.base import VerificationCallback, VerificationMethod from .verifications_methods.base import BaseTask, VerificationCallback
from .verifications_methods.iamhuman import IAmHumanButton
from .verifications_methods.math import MathButtonsVerification
from .verifications_methods.question import QuestionButtonsVerification
# По хорошему, надо вынести в конфиг, но пока оставим так. # from .verifications_methods.simple import SimpleInlineButtonsMethod
verification_methods = [ # from .verifications_methods.iamhuman import IAmHumanButton
IAmHumanButton(), from .verifications_methods.math import MathInlineButtonsTask, MathPollTask
# IAmHumanInput(), from .verifications_methods.question import QuestionInlineButtonsTask, QuestionPollTask
MathButtonsVerification(), from .verifications_methods.simple import (
# MathInputVerificationMethod(), SimpleVariantsBaseTask,
QuestionButtonsVerification(), SimpleVariantsBaseTaskConfig,
# QuestionInputVerification(), )
from .verifications_methods.utils import user_mention
# from .verifications_methods.question import QuestionButtonsVerification
all_tasks = [
MathInlineButtonsTask,
MathPollTask,
QuestionInlineButtonsTask,
QuestionPollTask,
] ]
verification_tasks = {}
class TaskManager:
def __init__(self, config: "IConfig"):
self.config = config
self.available_tasks = []
self.max_attempts = 1
def init(self):
for cls in all_tasks:
type_name = cls.type_name()
if self.config.get(f"welcome::tasks::{type_name}::enabled"):
self.available_tasks.append(cls)
self.max_attempts = self.config.get("welcome::max_attempts")
def build_random_task(self, event, bot, attempt_number=1) -> BaseTask:
cls = random.choice(self.available_tasks) # nosec
obj = cls(
event,
bot,
timeout_func=verify_timeout,
attempt_number=attempt_number,
max_attempts=self.max_attempts,
)
if isinstance(obj, SimpleVariantsBaseTask):
cfg = SimpleVariantsBaseTaskConfig(obj.type_name(), self.config)
obj.set_config(cfg)
return obj
verification_tasks = MultiKeyDict()
if TYPE_CHECKING:
from ocab_modules.standard.config import IConfig
async def new_member_handler(event: ChatMemberUpdated, bot: Bot): async def new_member_handler(event: ChatMemberUpdated, bot: Bot):
# НЕ СРАБОТАЕТ, ЕСЛИ ЧЕЛОВЕК УЖЕ ОГРАНИЧЕН В ПРАВАХ (RESTRICTED) # НЕ СРАБОТАЕТ, ЕСЛИ ЧЕЛОВЕК УЖЕ ОГРАНИЧЕН В ПРАВАХ (RESTRICTED)
if event.new_chat_member.status == ChatMemberStatus.MEMBER: if event.new_chat_member.status == ChatMemberStatus.MEMBER:
user_id = event.from_user.id task = task_manager.build_random_task(event, bot)
chat_id = event.chat.id keys = await task.run()
method: VerificationMethod = random.choice(verification_methods) # nosec verification_tasks.add(task, keys)
await method.pre_task(
chat_id,
user_id,
bot,
)
task_data = await method.create_task(event, bot)
task_data["user_id"] = user_id
task_data["chat_id"] = chat_id
task_data["method"] = method
task_data["timeout"] = method.timeout(task_data)
task = asyncio.create_task(verify_timeout(bot, task_data))
verification_tasks[(user_id, chat_id)] = {
"task": task,
"task_data": task_data,
}
async def left_member_handler(event: ChatMemberUpdated, bot: Bot): async def left_member_handler(event: ChatMemberUpdated, bot: Bot):
user_id = event.from_user.id user_id = event.from_user.id
chat_id = event.chat.id chat_id = event.chat.id
if (user_id, chat_id) not in verification_tasks: key = key_from_user_chat(user_id, chat_id)
if not verification_tasks.exists(key):
return return
task = verification_tasks[(user_id, chat_id)] task = verification_tasks.get(key)
task["task"].cancel() await task.end(success=False)
verification_tasks.pop((user_id, chat_id), None) verification_tasks.pop((user_id, chat_id), None)
task_data = task["task_data"]
method: VerificationMethod = task_data["method"]
await method.post_task(task_data, bot, success=False)
async def verify_timeout(bot: Bot, task_data: dict): async def verify_timeout(task: BaseTask):
user_id = task.from_user_id
chat_id = task.from_chat_id
try: try:
chat_id = task_data["chat_id"] timeout = task.get_timeout()
user_id = task_data["user_id"] # log(f"Start timeout {timeout}")
method: VerificationMethod = task_data["method"] await asyncio.sleep(timeout)
await asyncio.sleep(task_data["timeout"]) await task.end(success=False)
await method.post_task(task_data, success=False)
chat_member = await bot.get_chat_member(chat_id, user_id)
if chat_member.status in [ChatMemberStatus.MEMBER, ChatMemberStatus.RESTRICTED]:
await bot.ban_chat_member(chat_id, user_id)
await task.bot.ban_chat_member(chat_id, user_id)
except Exception as e: except Exception as e:
log(f"Error in verify_timeout: {e}") log(f"Error in verify_timeout: {e}")
finally: finally:
verification_tasks.pop((user_id, chat_id), None) verification_tasks.remove(key_from_user_chat(user_id, chat_id))
async def success_end(task: BaseTask):
await task.end()
await asyncio.sleep(3)
if config.get("welcome::show_success_message"):
await task.bot.send_message(
task.from_chat_id,
Template(config.get("welcome::success_message")).substitute(
mention=user_mention(task.from_user)
),
parse_mode=ParseMode.HTML,
)
async def handle_poll_verification(answer: PollAnswer, bot: Bot):
key = key_from_poll(answer.poll_id)
if not verification_tasks.exists(key):
return
task: BaseTask = verification_tasks.get(key)
if task.from_user_id != answer.user.id:
return
result = await task.verify(answer.option_ids[0])
if result:
await success_end(task)
return
await task.end(success=False)
current_attempt = task.attempt_number
if current_attempt >= task_manager.max_attempts:
await task.bot.ban_chat_member(task.from_chat_id, task.from_user_id)
return
await asyncio.sleep(5)
current_attempt = current_attempt + 1
new_task = task_manager.build_random_task(
task.event, task.bot, attempt_number=current_attempt
)
keys = await new_task.run()
log(keys)
verification_tasks.add(new_task, keys)
async def handle_inline_button_verification( async def handle_inline_button_verification(
@ -101,71 +172,152 @@ async def handle_inline_button_verification(
await callback_query.answer("Эта кнопка не для вас!", show_alert=True) await callback_query.answer("Эта кнопка не для вас!", show_alert=True)
return return
if (user_id, chat_id) not in verification_tasks: key = key_from_user_chat(user_id, chat_id)
if not verification_tasks.exists(key):
await callback_query.answer() await callback_query.answer()
return return
verification_task = verification_tasks[(user_id, chat_id)] task: BaseTask = verification_tasks.get(key)
task_data = verification_task["task_data"]
method: VerificationMethod = task_data["method"]
task_data["answer"] = callback_data.answer
result = await method.verify(task_data) result = await task.verify(callback_data.answer)
if result: if result:
verification_task["task"].cancel() await success_end(task)
await method.post_task(task_data, bot)
return return
if "attempts_count" not in task_data: await task.end(success=False)
await callback_query.answer("Неправильный ответ!", show_alert=True)
return
attempts_count = task_data["attempts_count"] current_attempt = task.attempt_number
verification_task["task"].cancel() if current_attempt >= task_manager.max_attempts:
attempts_count = attempts_count - 1
if attempts_count > 0:
await callback_query.answer(
"Неправильный ответ! "
+ "У вас еще "
+ get_plural_form(attempts_count, "попытка", "попытки", "попыток"),
show_alert=True,
)
task_data["timeout"] = method.timeout(task_data)
task_data["attempts_count"] = attempts_count
else:
task_data["timeout"] = 0
await callback_query.answer() await callback_query.answer()
await task.bot.ban_chat_member(chat_id, user_id)
task = asyncio.create_task(verify_timeout(bot, task_data))
verification_task["task"] = task
# Эта строчка нужна, т.к. во время cancel происходит pop
verification_tasks[(user_id, chat_id)] = verification_task
async def handle_input_verification(message: types.Message, bot: Bot):
user_id = message.from_user.id
chat_id = message.chat.id
if (user_id, chat_id) not in verification_tasks:
return return
task = verification_tasks[(user_id, chat_id)]
task_data = task["task_data"]
method: VerificationMethod = task_data["method"]
task_data["answer"] = message.text
task_data["answer_message_id"] = message.message_id
result = await method.verify(task_data) await callback_query.answer(
Template(config.get("welcome::retry_message")).substitute(
attempts=get_plural_form(
task_manager.max_attempts - current_attempt,
"попытка",
"попытки",
"попыток",
)
),
show_alert=True,
)
current_attempt = current_attempt + 1
new_task = task_manager.build_random_task(
task.event, task.bot, attempt_number=current_attempt
)
keys = await new_task.run()
verification_tasks.add(new_task, keys)
if result:
task["task"].cancel() config: "IConfig" = get_module("standard.config", "config")
await method.post_task(task_data, bot)
task_manager = TaskManager(config)
async def module_init(): async def module_init():
config.register("welcome::timeout", "int", default_value=45)
config.register("welcome::max_attempts", "int", default_value=2)
config.register(
"welcome::retry_message",
"string",
default_value="Неправильный ответ! У вас еще $attempts",
)
config.register("welcome::show_success_message", "boolean", default_value=True)
config.register(
"welcome::success_message",
"string",
default_value="$mention, вы успешно прошли проверку!",
)
# MATH BUTTONS
config.register(
"welcome::tasks::math_buttons::enabled", "boolean", default_value=True
)
config.register(
"welcome::tasks::math_buttons::message_text",
"int",
default_value="Привет, $mention!\n"
"Решите простую математическую задачу,"
"чтобы подтвердить, что вы не робот:\n\n<b>$task</b>",
)
config.register(
"welcome::tasks::math_buttons::retry_message_text",
"int",
default_value="$mention, неправильный ответ! У вас еще $attempts\n"
"Решите простую математическую задачу,"
"чтобы подтвердить, что вы не робот:\n\n<b>$task</b>",
)
config.register("welcome::tasks::math_buttons::timeout", "int", default_value=None)
# MATH POLL
config.register("welcome::tasks::math_poll::enabled", "boolean", default_value=True)
config.register(
"welcome::tasks::math_poll::message_text",
"string",
default_value="Привет, $mention!\n"
"Решите простую математическую задачу,"
"чтобы подтвердить, что вы не робот:\n\n$task",
)
config.register(
"welcome::tasks::math_poll::retry_message_text",
"string",
default_value="$mention, неправильный ответ! У вас еще $attempts\n"
"Решите простую математическую задачу,"
"чтобы подтвердить, что вы не робот:\n\n$task",
)
config.register("welcome::tasks::math_poll::timeout", "int", default_value=None)
# QUESTION BUTTONS
config.register(
"welcome::tasks::question_buttons::enabled", "boolean", default_value=True
)
config.register(
"welcome::tasks::question_buttons::message_text",
"string",
default_value="Привет, $mention!\n"
"Решите простую математическую задачу,"
"чтобы подтвердить, что вы не робот:\n\n<b>$task</b>",
)
config.register(
"welcome::tasks::question_buttons::retry_message_text",
"string",
default_value="$mention, неправильный ответ! У вас еще $attempts\n"
"Решите простую математическую задачу,"
"чтобы подтвердить, что вы не робот:\n\n<b>$task</b>",
)
config.register(
"welcome::tasks::question_buttons::timeout", "int", default_value=None
)
# QUESTION POLL
config.register(
"welcome::tasks::question_poll::enabled", "boolean", default_value=True
)
config.register(
"welcome::tasks::question_poll::message_text",
"string",
default_value="Привет, $mention!\n"
"Решите простую математическую задачу,"
"чтобы подтвердить, что вы не робот:\n\n$task",
)
config.register(
"welcome::tasks::question_poll::retry_message_text",
"string",
default_value="$mention, неправильный ответ! У вас еще $attempts\n"
"Решите простую математическую задачу,"
"чтобы подтвердить, что вы не робот:\n\n$task",
)
config.register("welcome::tasks::question_poll::timeout", "int", default_value=None)
router = Router() router = Router()
router.chat_member(ChatMemberUpdatedFilter(JOIN_TRANSITION))(new_member_handler) router.chat_member(ChatMemberUpdatedFilter(JOIN_TRANSITION))(new_member_handler)
@ -175,6 +327,11 @@ async def module_init():
router.callback_query(VerificationCallback.filter())( router.callback_query(VerificationCallback.filter())(
handle_inline_button_verification handle_inline_button_verification
) )
router.message()(handle_input_verification) router.poll_answer()(handle_poll_verification)
# router.message()(handle_input_verification)
register_router(router) register_router(router)
async def module_late_init():
task_manager.init()

View File

@ -7,3 +7,39 @@ def get_plural_form(number, singular, genitive_singular, plural):
return f"{number} {genitive_singular}" return f"{number} {genitive_singular}"
else: else:
return f"{number} {plural}" return f"{number} {plural}"
class MultiKeyDict:
def __init__(self):
self.value_to_keys = {} # Словарь значений и связанных с ними ключей
self.key_to_value = {} # Словарь ключей и связанных с ними значений
def add(self, value, keys):
# Добавляем значение в словарь с множеством ключей
self.value_to_keys[value] = set(keys)
# Для каждого ключа создаем запись в словаре key_to_value
for key in keys:
self.key_to_value[key] = value
def get(self, key):
return self.key_to_value.get(key)
def exists(self, key):
return key in self.key_to_value
def remove(self, key):
if key in self.key_to_value:
value = self.key_to_value.pop(key)
self.value_to_keys[value].remove(key)
for k in self.value_to_keys[value]:
del self.key_to_value[k]
def key_from_user_chat(user_id, chat_id):
return f"uc:{user_id}_{chat_id}"
def key_from_poll(poll_id):
return f"p:{poll_id}"

View File

@ -1,133 +1,102 @@
import time import asyncio
from functools import wraps
from aiogram import Bot from aiogram import Bot
from aiogram.exceptions import TelegramBadRequest from aiogram.exceptions import TelegramBadRequest
from aiogram.filters.callback_data import CallbackData from aiogram.filters.callback_data import CallbackData
from aiogram.types import ChatMemberUpdated, ChatPermissions from aiogram.types import ChatMemberUpdated
from .utils import user_mention from ocab_core.modules_system.public_api import log
from .utils import mute_user, unmute_user
async def mute_user(chat_id, user_id, until, bot: Bot): class BaseTask:
end_time = until + int(time.time()) def __init__(
await bot.restrict_chat_member( self,
chat_id, event: ChatMemberUpdated,
user_id, bot: Bot,
until_date=end_time, timeout_func=None,
use_independent_chat_permissions=True, attempt_number=1,
permissions=ChatPermissions( max_attempts=1,
can_send_messages=False, ):
can_send_audios=False, self.bot = bot
can_send_documents=False, self.event = event
can_send_photos=False, self.timeout_func = timeout_func
can_send_videos=False, self.attempt_number = attempt_number
can_send_video_notes=False, self.max_attempts = max_attempts
can_send_voice_notes=False, self.timeout_func_task = None
can_send_polls=False,
can_send_other_messages=False, @property
can_add_web_page_previews=False, def from_chat_id(self):
can_change_info=False, return self.event.chat.id
can_invite_users=False,
can_pin_messages=False, @property
can_manage_topics=False, def from_user_id(self):
), return self.event.from_user.id
)
@property
def from_user(self):
return self.event.from_user
@property
def attemps_left(self):
return self.max_attempts - self.attempt_number + 1
async def start_timeout_func(self):
if self.timeout_func:
self.timeout_func_task = asyncio.create_task(self.timeout_func(self))
@staticmethod
def type_name():
raise NotImplementedError()
async def run(self):
raise NotImplementedError()
async def verify(self, data):
raise NotImplementedError()
async def end(self, success=True):
raise NotImplementedError()
async def get_timeout():
raise NotImplementedError()
async def unmute_user(chat_id, user_id, bot: Bot): def mute_while_task(cls):
await bot.restrict_chat_member( original_run = getattr(cls, "run", None)
chat_id, original_end = getattr(cls, "end", None)
user_id,
use_independent_chat_permissions=True,
permissions=ChatPermissions(
can_send_messages=True,
can_send_audios=True,
can_send_documents=True,
can_send_photos=True,
can_send_videos=True,
can_send_video_notes=True,
can_send_voice_notes=True,
can_send_polls=True,
can_send_other_messages=True,
can_add_web_page_previews=True,
can_change_info=True,
can_invite_users=True,
can_pin_messages=True,
can_manage_topics=True,
),
)
if not original_run and not original_end:
return cls
class VerificationMethod: @wraps(original_run)
async def wrapped_run(self: BaseTask):
def timeout(self, task_data=None) -> int: chat_id = self.from_chat_id
""" user_id = self.from_user_id
Время ожидания
"""
return 30
def method_name(self):
pass
async def pre_task(self, chat_id, user_id, bot: Bot):
pass
async def create_task(self, event: ChatMemberUpdated, bot: Bot):
pass
async def post_task(self, task_data, bot: Bot, success=True):
pass
async def verify(self, task_data):
pass
class InputVerificationMethod(VerificationMethod):
async def post_task(self, task_data, bot: Bot, success=True, user=None):
chat_id = task_data["chat_id"]
message_id = task_data["message_id"]
answer_message_id = task_data["answer_message_id"]
await bot.delete_message(chat_id, message_id)
await bot.delete_message(chat_id, answer_message_id)
if not success or user is None:
return
await bot.send_message(
chat_id,
f"{user_mention(user)}, успешно прошли проверку! "
"Пожалуйста, соблюдайте правила группы.",
)
class InlineButtonVerificationMethod(VerificationMethod):
async def pre_task(self, chat_id, user_id, bot: Bot):
try: try:
await mute_user(chat_id, user_id, 0, bot) await mute_user(chat_id, user_id, 0, self.bot)
except TelegramBadRequest: except TelegramBadRequest as e:
log(e)
pass pass
return await original_run(self)
async def post_task(self, task_data, bot: Bot, success=True, user=None): @wraps(original_end)
user_id = task_data["user_id"] async def wrapped_end(self: BaseTask, success=True):
chat_id = task_data["chat_id"] await original_end(self, success)
message_id = task_data["message_id"] if success:
chat_id = self.from_chat_id
user_id = self.from_user_id
try:
await unmute_user(chat_id, user_id, self.bot)
except TelegramBadRequest as e:
log(e)
pass
await bot.delete_message(chat_id, message_id) cls.run = wrapped_run
cls.end = wrapped_end
try: return cls
await unmute_user(chat_id, user_id, bot)
except TelegramBadRequest:
pass
if not success or user is None:
return
await bot.send_message(
chat_id,
f"{user_mention(user)}, успешно прошли проверку! "
"Пожалуйста, соблюдайте правила группы.",
)
class VerificationCallback(CallbackData, prefix="verify"): class VerificationCallback(CallbackData, prefix="verify"):

View File

@ -1,78 +0,0 @@
import random
from aiogram import Bot
from aiogram.enums import ParseMode
from aiogram.types import ChatMemberUpdated, InlineKeyboardButton, InlineKeyboardMarkup
from .base import (
InlineButtonVerificationMethod,
InputVerificationMethod,
VerificationCallback,
)
from .utils import user_mention
class IAmHumanButton(InlineButtonVerificationMethod):
def __init__(self):
pass
def method_name(self):
return "i_am_human_button"
async def create_task(self, event: ChatMemberUpdated, bot: Bot):
user_id = event.from_user.id
chat_id = event.chat.id
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Я человек!",
callback_data=VerificationCallback(
user_id=user_id, chat_id=chat_id, answer="OK"
).pack(),
)
]
]
)
message = await bot.send_message(
chat_id,
f"Привет, {user_mention(event.from_user)}! ",
"Нажмите кнопку, чтобы подтвердить, что вы не робот.",
reply_markup=keyboard,
parse_mode=ParseMode.HTML,
)
return {"message_id": message.message_id}
async def verify(self, task_data):
return True
class IAmHumanInput(InputVerificationMethod):
def __init__(self):
pass
def method_name(self):
return "i_am_human_input"
def get_text(self):
return random.choice(["Я человек", "Я не робот"]) # nosec
async def create_task(self, event: ChatMemberUpdated, bot: Bot):
chat_id = event.chat.id
text = self.get_text()
message = await bot.send_message(
chat_id,
f"Привет, {user_mention(event.from_user)}! "
f'Напишите "{text}", чтобы подтвердить, что вы не робот.',
parse_mode=ParseMode.HTML,
)
return {"message_id": message.message_id, "correct": text}
async def verify(self, task_data):
correct: str = task_data["correct"]
answer: str = task_data["answer"]
return answer.lower() == correct.lower()

View File

@ -1,63 +1,9 @@
import random import random
from aiogram import Bot from .simple import SimpleInlineButtonsTask, SimplePollTask, SimpleVariantsBaseTask
from aiogram.enums import ParseMode
from aiogram.types import ChatMemberUpdated, InlineKeyboardButton, InlineKeyboardMarkup
from .base import (
InlineButtonVerificationMethod,
InputVerificationMethod,
VerificationCallback,
)
from .utils import user_mention
class MathInputVerificationMethod(InputVerificationMethod): class BaseMathTask(SimpleVariantsBaseTask):
def __init__(self):
pass
def method_name(self):
return "math_input"
def generate_math_problem(self):
a = random.randint(1, 10) # nosec
b = random.randint(1, 10) # nosec
operation = random.choice(["+", "-", "*"]) # nosec
if operation == "+":
answer = a + b
elif operation == "-":
answer = a - b
else:
answer = a * b
return f"{a} {operation} {b}", str(answer)
async def create_task(self, event: ChatMemberUpdated, bot: Bot):
chat_id = event.chat.id
problem, answer = self.generate_math_problem()
message = await bot.send_message(
chat_id,
f"Привет, {user_mention(event.from_user)}! "
"Решите простую математическую задачу, "
"чтобы подтвердить, что вы не робот:\\n"
f"{problem} = ?",
parse_mode=ParseMode.HTML,
)
return {"message_id": message.message_id, "correct": answer}
async def verify(self, task_data):
correct: str = task_data["correct"]
answer: str = task_data["answer"]
return answer.strip() == correct
class MathButtonsVerification(InlineButtonVerificationMethod):
def __init__(self):
pass
def method_name(self):
return "math_buttons"
def generate_math_problem(self): def generate_math_problem(self):
a = random.randint(1, 10) # nosec a = random.randint(1, 10) # nosec
b = random.randint(1, 10) # nosec b = random.randint(1, 10) # nosec
@ -70,52 +16,38 @@ class MathButtonsVerification(InlineButtonVerificationMethod):
answer = a * b answer = a * b
return f"{a} {operation} {b}", answer return f"{a} {operation} {b}", answer
async def create_task(self, event: ChatMemberUpdated, bot: Bot): async def init(self):
user_id = event.from_user.id
chat_id = event.chat.id
problem, correct_answer = self.generate_math_problem() problem, correct_answer = self.generate_math_problem()
options = [correct_answer] self.variants = [correct_answer]
while len(options) < 4: while len(self.variants) < 4:
wrong_answer = random.randint( wrong_answer = random.randint(
correct_answer - 5, correct_answer + 5 correct_answer - 5, correct_answer + 5
) # nosec ) # nosec
if wrong_answer not in options: if wrong_answer not in self.variants:
options.append(wrong_answer) self.variants.append(wrong_answer)
random.shuffle(options) # nosec random.shuffle(self.variants) # nosec
keyboard = InlineKeyboardMarkup( self.variants = [str(x) for x in self.variants]
inline_keyboard=[
[
InlineKeyboardButton(
text=str(option),
callback_data=VerificationCallback(
user_id=user_id, chat_id=chat_id, answer=str(option)
).pack(),
)
for option in options
]
]
)
message = await bot.send_message( self.task = f"{problem} = ?"
chat_id, self.correct = str(correct_answer)
f"Привет, {user_mention(event.from_user)}! "
"Решите простую математическую задачу, "
"чтобы подтвердить, что вы не робот:\\n"
f"{problem} = ?",
reply_markup=keyboard,
parse_mode=ParseMode.HTML,
)
return {
"message_id": message.message_id,
"correct": str(correct_answer),
"attempts_count": 2,
}
async def verify(self, task_data): class MathInlineButtonsTask(BaseMathTask, SimpleInlineButtonsTask):
correct: str = task_data["correct"] """
answer: str = task_data["answer"] Математическая задача с выбором через inline-кнопки
"""
return answer == correct @staticmethod
def type_name():
return "math_buttons"
class MathPollTask(BaseMathTask, SimplePollTask):
"""
Математическая задача с выбором через Poll
"""
@staticmethod
def type_name():
return "math_poll"

View File

@ -1,16 +1,9 @@
import random import random
from aiogram import Bot from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.enums import ParseMode
from aiogram.types import ChatMemberUpdated, InlineKeyboardButton, InlineKeyboardMarkup
from ocab_modules.standard.welcome.verifications_methods.utils import user_mention from .base import VerificationCallback
from .simple import SimpleInlineButtonsTask, SimplePollTask, SimpleVariantsBaseTask
from .base import (
InlineButtonVerificationMethod,
InputVerificationMethod,
VerificationCallback,
)
QUESTIONS = [ QUESTIONS = [
( (
@ -42,85 +35,45 @@ QUESTIONS = [
] ]
class QuestionInputVerification(InputVerificationMethod): class BaseQuestionsTask(SimpleVariantsBaseTask):
def __init__(self): async def init(self):
pass question, correct_answer, wrong_answers = random.choice(QUESTIONS) # nosec
def method_name(self):
return "question_input"
def get_random_question(self):
return random.choice(QUESTIONS) # nosec
async def create_task(self, event: ChatMemberUpdated, bot: Bot):
chat_id = event.chat.id
question, answer, _ = self.get_random_question()
message = await bot.send_message(
chat_id,
f"Привет, {user_mention(event.from_user)}! "
"Пожалуйста, ответьте на следующий вопрос, "
f"чтобы подтвердить, что вы не робот: {question}",
parse_mode=ParseMode.HTML,
)
return {"message_id": message.message_id, "correct": answer.lower()}
async def verify(self, task_data):
correct: str = task_data["correct"]
answer: str = task_data["answer"]
return answer.lower().strip() == correct
class QuestionButtonsVerification(InlineButtonVerificationMethod):
def __init__(self):
pass
def method_name(self):
return "question_inline"
def get_random_question(self):
return random.choice(QUESTIONS) # nosec
async def create_task(self, event: ChatMemberUpdated, bot: Bot):
user_id = event.from_user.id
chat_id = event.chat.id
question, correct_answer, wrong_answers = self.get_random_question()
options = [correct_answer] + wrong_answers options = [correct_answer] + wrong_answers
random.shuffle(options) # nosec random.shuffle(options) # nosec
keyboard = InlineKeyboardMarkup( self.variants = [str(x) for x in options]
self.task = question
self.correct = correct_answer
async def verify(self, data):
return self.variants[data] == self.correct
class QuestionInlineButtonsTask(BaseQuestionsTask, SimpleInlineButtonsTask):
@staticmethod
def type_name():
return "question_buttons"
def build_keyboard(self):
return InlineKeyboardMarkup(
inline_keyboard=[ inline_keyboard=[
[ [
InlineKeyboardButton( InlineKeyboardButton(
text=option, text=str(option),
callback_data=VerificationCallback( callback_data=VerificationCallback(
user_id=user_id, chat_id=chat_id, answer=str(i) user_id=self.from_user_id,
chat_id=self.from_chat_id,
answer=str(i),
).pack(), ).pack(),
) )
for i, option in enumerate(self.variants)
] ]
for i, option in enumerate(options)
] ]
) )
message = await bot.send_message(
chat_id,
f"Привет, {user_mention(event.from_user)}! "
"Пожалуйста, ответьте на следующий вопрос, "
f"чтобы подтвердить, что вы не робот: {question}",
reply_markup=keyboard,
parse_mode=ParseMode.HTML,
)
return { class QuestionPollTask(BaseQuestionsTask, SimplePollTask):
"message_id": message.message_id, @staticmethod
"correct": correct_answer, def type_name():
"options": options, return "question_poll"
"attempts_count": 2,
}
async def verify(self, task_data):
correct: str = task_data["correct"]
answer: str = task_data["answer"]
return task_data["options"][int(answer)] == correct

View File

@ -0,0 +1,178 @@
from string import Template
from aiogram import Bot
from aiogram.enums import ParseMode, PollType
from aiogram.types import ChatMemberUpdated, InlineKeyboardButton, InlineKeyboardMarkup
from ..utils import get_plural_form, key_from_poll, key_from_user_chat
from .base import BaseTask, VerificationCallback, mute_while_task
from .utils import user_mention
class SimpleBaseTask(BaseTask):
pass
class SimpleVariantsBaseTaskConfig:
def __init__(self, task_type, config: dict):
self.config = config
self.task_type = task_type
@property
def timeout(self):
timeout = self.config.get(f"welcome::tasks::{self.task_type}::timeout")
if timeout is None:
return self.config.get("welcome::timeout")
return timeout
@property
def task_message_text(self):
return self.config.get(f"welcome::tasks::{self.task_type}::message_text")
@property
def task_retry_message_text(self):
return self.config.get(f"welcome::tasks::{self.task_type}::retry_message_text")
class SimpleVariantsBaseTask(SimpleBaseTask):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.config = None
self.variants = []
self.task = ""
self.correct = None
self.task_message_id = None
def set_config(self, cfg: SimpleVariantsBaseTaskConfig):
self.config = cfg
def get_timeout(self):
return self.config.timeout
@mute_while_task
class SimpleInlineButtonsTask(SimpleVariantsBaseTask):
async def init(self):
raise NotImplementedError()
def build_keyboard(self):
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text=str(option),
callback_data=VerificationCallback(
user_id=self.from_user_id,
chat_id=self.from_chat_id,
answer=str(option),
).pack(),
)
for option in self.variants
]
]
)
async def run(self):
await self.init()
message_template = Template(
self.config.task_message_text
if self.attempt_number == 1
else self.config.task_retry_message_text
)
chat_id = self.from_chat_id
message = await self.bot.send_message(
chat_id,
text=message_template.substitute(
mention=user_mention(self.from_user),
task=self.task,
attempts=get_plural_form(
self.attemps_left, "попытка", "попытки", "попыток"
),
),
reply_markup=self.build_keyboard(),
parse_mode=ParseMode.HTML,
)
self.task_message_id = message.message_id
await self.start_timeout_func()
return [key_from_user_chat(self.from_user_id, self.from_chat_id)]
async def verify(self, data):
return self.correct == data
async def end(self, success=True):
await self.bot.delete_message(self.from_chat_id, self.task_message_id)
if self.timeout_func_task:
self.timeout_func_task.cancel()
@mute_while_task
class SimplePollTask(SimpleVariantsBaseTask):
def __init__(
self,
event: ChatMemberUpdated,
bot: Bot,
timeout_func=None,
attempt_number=1,
max_attempts=1,
):
super().__init__(event, bot, timeout_func, attempt_number, max_attempts)
self.correct_index = None
async def init(self):
raise NotImplementedError()
async def run(self):
await self.init()
self.correct_index = self.variants.index(self.correct)
message_template = Template(
self.config.task_message_text
if self.attempt_number == 1
else self.config.task_retry_message_text
)
chat_id = self.from_chat_id
message = await self.bot.send_poll(
chat_id,
question=message_template.substitute(
mention=self.from_user.first_name,
task=self.task,
attempts=get_plural_form(
self.attemps_left, "попытка", "попытки", "попыток"
),
),
options=self.variants,
type=PollType.QUIZ,
correct_option_id=self.correct_index,
allows_multiple_answers=False,
is_anonymous=False,
# parse_mode=ParseMode.HTML
)
self.task_message_id = message.message_id
await self.start_timeout_func()
return [
key_from_poll(message.poll.id),
key_from_user_chat(self.from_user_id, self.from_chat_id),
]
async def verify(self, data):
return self.correct_index == data
async def end(self, success=True):
await self.bot.delete_message(self.from_chat_id, self.task_message_id)
if self.timeout_func_task:
self.timeout_func_task.cancel()

View File

@ -1,5 +1,8 @@
import time
from aiogram import Bot
from aiogram.enums import ParseMode from aiogram.enums import ParseMode
from aiogram.types import User from aiogram.types import ChatPermissions, User
def user_mention(user: User, mode=ParseMode.HTML): def user_mention(user: User, mode=ParseMode.HTML):
@ -9,3 +12,53 @@ def user_mention(user: User, mode=ParseMode.HTML):
return f"[{user.first_name}](tg://user?id={user.id})" return f"[{user.first_name}](tg://user?id={user.id})"
else: else:
raise ValueError(f"Unknown parse mode {mode}") raise ValueError(f"Unknown parse mode {mode}")
async def mute_user(chat_id, user_id, until, bot: Bot):
end_time = until + int(time.time())
await bot.restrict_chat_member(
chat_id,
user_id,
until_date=end_time,
use_independent_chat_permissions=True,
permissions=ChatPermissions(
can_send_messages=False,
can_send_audios=False,
can_send_documents=False,
can_send_photos=False,
can_send_videos=False,
can_send_video_notes=False,
can_send_voice_notes=False,
can_send_polls=False,
can_send_other_messages=False,
can_add_web_page_previews=False,
can_change_info=False,
can_invite_users=False,
can_pin_messages=False,
can_manage_topics=False,
),
)
async def unmute_user(chat_id, user_id, bot: Bot):
await bot.restrict_chat_member(
chat_id,
user_id,
use_independent_chat_permissions=True,
permissions=ChatPermissions(
can_send_messages=True,
can_send_audios=True,
can_send_documents=True,
can_send_photos=True,
can_send_videos=True,
can_send_video_notes=True,
can_send_voice_notes=True,
can_send_polls=True,
can_send_other_messages=True,
can_add_web_page_previews=True,
can_change_info=True,
can_invite_users=True,
can_pin_messages=True,
can_manage_topics=True,
),
)