From 76cf36fb67cef200768fe527cedd9aa664828a9b Mon Sep 17 00:00:00 2001 From: omg-xtao <100690902+omg-xtao@users.noreply.github.com> Date: Mon, 25 Mar 2024 20:48:19 +0800 Subject: [PATCH] :sparkles: Support Ignore unbound user command in group --- .../versions/369fb74daad9_groups_ignore.py | 28 +++++ core/handler/hookhandler.py | 3 + core/plugin/__init__.py | 12 ++- gram_core | 2 +- plugins/genshin/avatar_list.py | 4 +- plugins/genshin/daily_note.py | 4 +- plugins/genshin/ledger.py | 2 +- plugins/genshin/player_cards.py | 6 +- plugins/genshin/redeem.py | 4 +- plugins/genshin/sign.py | 4 +- plugins/genshin/stats.py | 4 +- plugins/group/captcha.py | 30 ++---- plugins/group/ignore_unbound_user.py | 101 ++++++++++++++++++ plugins/tools/chat_administrators.py | 41 +++++++ 14 files changed, 205 insertions(+), 40 deletions(-) create mode 100644 alembic/versions/369fb74daad9_groups_ignore.py create mode 100644 core/handler/hookhandler.py create mode 100644 plugins/group/ignore_unbound_user.py create mode 100644 plugins/tools/chat_administrators.py diff --git a/alembic/versions/369fb74daad9_groups_ignore.py b/alembic/versions/369fb74daad9_groups_ignore.py new file mode 100644 index 0000000..c8deb85 --- /dev/null +++ b/alembic/versions/369fb74daad9_groups_ignore.py @@ -0,0 +1,28 @@ +"""groups_ignore + +Revision ID: 369fb74daad9 +Revises: cb37027ecae8 +Create Date: 2024-03-25 17:29:35.378726 + +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "369fb74daad9" +down_revision = "cb37027ecae8" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("groups", sa.Column("is_ignore", sa.Integer(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("groups", "is_ignore") + # ### end Alembic commands ### diff --git a/core/handler/hookhandler.py b/core/handler/hookhandler.py new file mode 100644 index 0000000..86b9839 --- /dev/null +++ b/core/handler/hookhandler.py @@ -0,0 +1,3 @@ +from gram_core.handler.hookhandler import HookHandler + +__all__ = ("HookHandler",) diff --git a/core/plugin/__init__.py b/core/plugin/__init__.py index e0b3051..cd6257b 100644 --- a/core/plugin/__init__.py +++ b/core/plugin/__init__.py @@ -1,6 +1,13 @@ """插件""" -from gram_core.plugin._handler import conversation, error_handler, handler +from gram_core.plugin._handler import ( + conversation, + error_handler, + handler, + ConversationDataType, + ConversationData, + HandlerData, +) from gram_core.plugin._job import TimeType, job from gram_core.plugin._plugin import Plugin, PluginType, get_all_plugins @@ -11,6 +18,9 @@ __all__ = ( "handler", "error_handler", "conversation", + "ConversationDataType", + "ConversationData", + "HandlerData", "job", "TimeType", ) diff --git a/gram_core b/gram_core index 3057fba..4c2eac2 160000 --- a/gram_core +++ b/gram_core @@ -1 +1 @@ -Subproject commit 3057fba7a6e84be60ece1ab98e6cd012aba37e78 +Subproject commit 4c2eac29b1529ddffc94f9e22f6735b6d213a4bb diff --git a/plugins/genshin/avatar_list.py b/plugins/genshin/avatar_list.py index 3be55f2..9b82025 100644 --- a/plugins/genshin/avatar_list.py +++ b/plugins/genshin/avatar_list.py @@ -154,8 +154,8 @@ class AvatarListPlugin(Plugin): name_card = (await self.assets_service.namecard(210001).navbar()).as_uri() return name_card, avatar, nickname, rarity - @handler.command("avatars", block=False) - @handler.message(filters.Regex(r"^(全部)?练度统计$"), block=False) + @handler.command("avatars", cookie=True, block=False) + @handler.message(filters.Regex(r"^(全部)?练度统计$"), cookie=True, block=False) async def avatar_list(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE"): user_id = await self.get_real_user_id(update) user_name = self.get_real_user_name(update) diff --git a/plugins/genshin/daily_note.py b/plugins/genshin/daily_note.py index db4b67d..8f866ae 100644 --- a/plugins/genshin/daily_note.py +++ b/plugins/genshin/daily_note.py @@ -100,8 +100,8 @@ class DailyNotePlugin(Plugin): [[InlineKeyboardButton(">> 设置状态提醒 <<", url=create_deep_linked_url(bot_username, "daily_note_tasks"))]] ) - @handler.command("dailynote", block=False) - @handler.message(filters.Regex("^当前状态(.*)"), block=False) + @handler.command("dailynote", cookie=True, block=False) + @handler.message(filters.Regex("^当前状态(.*)"), cookie=True, block=False) async def command_start(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> Optional[int]: message = update.effective_message user_id = await self.get_real_user_id(update) diff --git a/plugins/genshin/ledger.py b/plugins/genshin/ledger.py index 3603fda..7cd7e89 100644 --- a/plugins/genshin/ledger.py +++ b/plugins/genshin/ledger.py @@ -72,7 +72,7 @@ class LedgerPlugin(Plugin): ) return render_result - @handler.command(command="ledger", block=False) + @handler.command(command="ledger", cookie=True, block=False) @handler.message(filters=filters.Regex("^旅行札记查询(.*)"), block=False) async def command_start(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None: user_id = await self.get_real_user_id(update) diff --git a/plugins/genshin/player_cards.py b/plugins/genshin/player_cards.py index 8e4dc06..b567aae 100644 --- a/plugins/genshin/player_cards.py +++ b/plugins/genshin/player_cards.py @@ -158,9 +158,9 @@ class PlayerCards(Plugin): uid = player_info.player_id return uid, ch_name - @handler.command(command="player_card", block=False) - @handler.command(command="player_cards", block=False) - @handler.message(filters=filters.Regex("^角色卡片查询(.*)"), block=False) + @handler.command(command="player_card", player=True, block=False) + @handler.command(command="player_cards", player=True, block=False) + @handler.message(filters=filters.Regex("^角色卡片查询(.*)"), player=True, block=False) async def player_cards(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None: user_id = await self.get_real_user_id(update) message = update.effective_message diff --git a/plugins/genshin/redeem.py b/plugins/genshin/redeem.py index 363cbcf..cc73ba4 100644 --- a/plugins/genshin/redeem.py +++ b/plugins/genshin/redeem.py @@ -40,8 +40,8 @@ class Redeem(Plugin): msg = e.message return msg - @handler.command(command="redeem", block=False) - @handler.message(filters=filters.Regex("^兑换码兑换(.*)"), block=False) + @handler.command(command="redeem", cookie=True, block=False) + @handler.message(filters=filters.Regex("^兑换码兑换(.*)"), cookie=True, block=False) async def command_start(self, update: Update, context: CallbackContext) -> None: user_id = await self.get_real_user_id(update) message = update.effective_message diff --git a/plugins/genshin/sign.py b/plugins/genshin/sign.py index 47a76d8..834433b 100644 --- a/plugins/genshin/sign.py +++ b/plugins/genshin/sign.py @@ -66,8 +66,8 @@ class Sign(Plugin): await self.sign_service.add(user) return "开启自动签到成功" - @handler.command(command="sign", block=False) - @handler.message(filters=filters.Regex("^每日签到(.*)"), block=False) + @handler.command(command="sign", cookie=True, block=False) + @handler.message(filters=filters.Regex("^每日签到(.*)"), cookie=True, block=False) @handler.command(command="start", filters=filters.Regex("sign$"), block=False) async def command_start(self, update: Update, context: CallbackContext) -> None: user_id = await self.get_real_user_id(update) diff --git a/plugins/genshin/stats.py b/plugins/genshin/stats.py index a78aec7..72b8378 100644 --- a/plugins/genshin/stats.py +++ b/plugins/genshin/stats.py @@ -27,8 +27,8 @@ class PlayerStatsPlugins(Plugin): self.template_service = template self.helper = helper - @handler.command("stats", block=False) - @handler.message(filters.Regex("^玩家统计查询(.*)"), block=False) + @handler.command("stats", player=True, block=False) + @handler.message(filters.Regex("^玩家统计查询(.*)"), player=True, block=False) async def command_start(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> Optional[int]: user_id = await self.get_real_user_id(update) message = update.effective_message diff --git a/plugins/group/captcha.py b/plugins/group/captcha.py index d581bbb..ac250a2 100644 --- a/plugins/group/captcha.py +++ b/plugins/group/captcha.py @@ -1,7 +1,7 @@ import asyncio import random import time -from typing import Tuple, Union, Optional, TYPE_CHECKING, List +from typing import Tuple, Union, Optional, TYPE_CHECKING from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ChatPermissions, ChatMember, Message, User from telegram.constants import ParseMode @@ -15,6 +15,7 @@ from core.dependence.redisdb import RedisDB from core.handler.callbackqueryhandler import CallbackQueryHandler from core.plugin import Plugin, handler from core.services.quiz.services import QuizService +from plugins.tools.chat_administrators import ChatAdministrators from utils.chatmember import extract_status_change from utils.log import logger @@ -63,25 +64,6 @@ class GroupCaptcha(Plugin): return f"[{user_id}]({tg_link})" return f"[{escape_markdown(user_id, version=version)}]({tg_link})" - async def get_chat_administrators( - self, context: "ContextTypes.DEFAULT_TYPE", chat_id: Union[str, int] - ) -> Tuple[ChatMember]: - qname = f"plugin:group_captcha:chat_administrators:{chat_id}" - result: "List[bytes]" = await self.cache.lrange(qname, 0, -1) - if len(result) > 0: - return ChatMember.de_list([jsonlib.loads(str(_data, encoding="utf-8")) for _data in result], context.bot) - chat_administrators = await context.bot.get_chat_administrators(chat_id) - async with self.cache.pipeline(transaction=True) as pipe: - for chat_administrator in chat_administrators: - await pipe.lpush(qname, chat_administrator.to_json()) - await pipe.expire(qname, self.ttl) - await pipe.execute() - return chat_administrators - - @staticmethod - def is_admin(chat_administrators: Tuple[ChatMember], user_id: int) -> bool: - return any(admin.user.id == user_id for admin in chat_administrators) - async def kick_member_job(self, context: "ContextTypes.DEFAULT_TYPE"): job = context.job logger.info("踢出用户 user_id[%s] 在 chat_id[%s]", job.user_id, job.chat_id) @@ -152,8 +134,8 @@ class GroupCaptcha(Plugin): message = callback_query.message chat = message.chat logger.info("用户 %s[%s] 在群 %s[%s] 点击Auth管理员命令", user.full_name, user.id, chat.title, chat.id) - chat_administrators = await self.get_chat_administrators(context, chat_id=chat.id) - if not self.is_admin(chat_administrators, user.id): + chat_administrators = await ChatAdministrators.get_chat_administrators(self.cache, context, chat_id=chat.id) + if not ChatAdministrators.is_admin(chat_administrators, user.id): logger.debug("用户 %s[%s] 在群 %s[%s] 非群管理", user.full_name, user.id, chat.title, chat.id) await callback_query.answer(text="你不是管理!\n" + self.user_mismatch, show_alert=True) return @@ -350,8 +332,8 @@ class GroupCaptcha(Plugin): logger.info("用户 %s[%s] 尝试加入群 %s[%s]", user.full_name, user.id, chat.title, chat.id) if user.is_bot: return - chat_administrators = await self.get_chat_administrators(context, chat_id=chat.id) - if self.is_admin(chat_administrators, from_user.id): + chat_administrators = await ChatAdministrators.get_chat_administrators(self.cache, context, chat_id=chat.id) + if ChatAdministrators.is_admin(chat_administrators, from_user.id): await chat.send_message("派蒙检测到管理员邀请,自动放行了!") return question_id_list = await self.quiz_service.get_question_id_list() diff --git a/plugins/group/ignore_unbound_user.py b/plugins/group/ignore_unbound_user.py new file mode 100644 index 0000000..72bb776 --- /dev/null +++ b/plugins/group/ignore_unbound_user.py @@ -0,0 +1,101 @@ +from typing import TYPE_CHECKING + +from telegram.constants import ChatType +from telegram.ext import ApplicationHandlerStop, filters + +from gram_core.dependence.redisdb import RedisDB +from gram_core.plugin import Plugin, handler, HandlerData +from gram_core.services.groups.services import GroupService +from gram_core.services.players import PlayersService +from gram_core.services.users.services import UserAdminService +from plugins.tools.chat_administrators import ChatAdministrators +from utils.log import logger + +if TYPE_CHECKING: + from telegram import Update, Message + from telegram.ext import ContextTypes + +IGNORE_UNBOUND_USER_OPEN = """成功开启 忽略未绑定用户触发部分命令 功能,派蒙将不会响应未绑定用户的部分命令 + +- 此功能开启后,将会导致新用户无法快速绑定账号,请在群规则中注明 绑定链接 https://t.me/{}?start=setcookies 或者其他使用说明。 +""" +IGNORE_UNBOUND_USER_CLOSE = """成功关闭 忽略未绑定用户触发部分命令 功能,派蒙将开始响应未绑定用户的部分命令""" + + +class IgnoreUnboundUser(Plugin): + def __init__( + self, + players_service: PlayersService, + group_service: GroupService, + user_admin_service: UserAdminService, + redis: RedisDB, + ): + self.players_service = players_service + self.group_service = group_service + self.user_admin_service = user_admin_service + self.cache = redis.client + + async def initialize(self) -> None: + self.application.run_preprocessor(self.check_update) + + async def check_account(self, user_id: int) -> bool: + return bool(await self.players_service.get_player(user_id)) + + async def check_group(self, group_id: int) -> bool: + return await self.group_service.is_ignore(group_id) + + async def check_update(self, update: "Update", _, __, context: "ContextTypes.DEFAULT_TYPE", data: "HandlerData"): + if not isinstance(data, HandlerData): + return + if not data.player: + return + chat = update.effective_chat + if (not chat) or chat.type not in [ChatType.SUPERGROUP, ChatType.GROUP]: + return + if not await self.check_group(chat.id): + # 未开启此功能 + return + message = update.effective_message + if message: + text = message.text or message.caption + if text and context.bot.username in text: + # 机器人被提及 + return + uid = await self.get_real_user_id(update) + if await self.check_account(uid): + # 已绑定账号 + return + self.log_user(update, logger.info, "群组 %s[%s] 拦截了未绑定用户触发命令", chat.title, chat.id) + raise ApplicationHandlerStop + + async def check_permission(self, chat_id: int, user_id: int, context: "ContextTypes.DEFAULT_TYPE") -> bool: + if await self.user_admin_service.is_admin(user_id): + return True + admins = await ChatAdministrators.get_chat_administrators(self.cache, context, chat_id) + return ChatAdministrators.is_admin(admins, user_id) + + async def reply_and_delete(self, message: "Message", text: str): + reply = await message.reply_text(text) + self.add_delete_message_job(message) + self.add_delete_message_job(reply) + + @handler.command("ignore_unbound_user", filters=filters.ChatType.SUPERGROUP | filters.ChatType.GROUP, block=False) + async def ignore_unbound_user(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE"): + user_id = await self.get_real_user_id(update) + message = update.effective_message + chat_id = update.effective_chat.id + if not await self.check_permission(chat_id, user_id, context): + await self.reply_and_delete(message, "您没有权限执行此操作") + return + self.log_user(update, logger.info, "更改群组 未绑定用户触发命令 功能状态") + group = await self.group_service.get_group_by_id(chat_id) + if not group: + await self.reply_and_delete(message, "群组信息出现错误,请尝试重新添加机器人到群组") + return + group.is_ignore = not group.is_ignore + await self.group_service.update_group(group) + if group.is_ignore: + text = IGNORE_UNBOUND_USER_OPEN.format(context.bot.username) + else: + text = IGNORE_UNBOUND_USER_CLOSE + await message.reply_text(text) diff --git a/plugins/tools/chat_administrators.py b/plugins/tools/chat_administrators.py new file mode 100644 index 0000000..b181f8b --- /dev/null +++ b/plugins/tools/chat_administrators.py @@ -0,0 +1,41 @@ +from typing import Union, Tuple, TYPE_CHECKING, List, Any + +from telegram import ChatMember + +try: + import ujson as jsonlib +except ImportError: + import json as jsonlib + + +if TYPE_CHECKING: + from redis import Redis + + from telegram.ext import ContextTypes + + +class ChatAdministrators: + QNAME = "plugin:group_captcha:chat_administrators" + TTL = 1 * 60 * 60 + + @staticmethod + async def get_chat_administrators( + cache: "Redis", + context: "ContextTypes.DEFAULT_TYPE", + chat_id: Union[str, int], + ) -> Union[Tuple[ChatMember, ...], Any]: + qname = f"{ChatAdministrators.QNAME}:{chat_id}" + result: "List[bytes]" = await cache.lrange(qname, 0, -1) + if len(result) > 0: + return ChatMember.de_list([jsonlib.loads(str(_data, encoding="utf-8")) for _data in result], context.bot) + chat_administrators = await context.bot.get_chat_administrators(chat_id) + async with cache.pipeline(transaction=True) as pipe: + for chat_administrator in chat_administrators: + await pipe.lpush(qname, chat_administrator.to_json()) + await pipe.expire(qname, ChatAdministrators.TTL) + await pipe.execute() + return chat_administrators + + @staticmethod + def is_admin(chat_administrators: Tuple[ChatMember], user_id: int) -> bool: + return any(admin.user.id == user_id for admin in chat_administrators)