refa: clean verify user message

This commit is contained in:
xtaodada 2024-08-07 23:33:57 +08:00
parent 4f0607c034
commit f1f10b454a
Signed by: xtaodada
GPG Key ID: 4CBB3F4FA8C85659
14 changed files with 190 additions and 396 deletions

View File

@ -1,5 +1,4 @@
import contextlib
from datetime import datetime, timedelta
from cashews import cache
from pyrogram import filters
@ -8,17 +7,45 @@ from pyrogram.types import ChatMemberUpdated
from pyromod.utils.errors import TimeoutConversationError
from sticker.languages import MSG_PUBLIC, ADMIN_MSG, MSG, VERIFY_TIME
from sticker.scheduler import add_delete_message_job
from sticker.scheduler import add_ban_chat_member_job
from sticker.service_message import ServiceMessage
from sticker.single_utils import Client
from sticker import bot, log
from sticker.single_utils import Client, Message
from sticker import bot, log, LogAction
async def start_verify(client: "Client", chat, user):
key = f"wait:{chat.id}:{user.id}"
await cache.set(key, "True", expire=VERIFY_TIME + 5)
try:
msg: "Message" = await client.send_message(
chat.id, MSG % (user.mention, user.mention)
)
except Exception:
return
await log(chat, user, LogAction.REQUEST)
try:
msg_: "Message" = await client.listen(
chat.id, filters=filters.user(user.id), timeout=VERIFY_TIME
)
await msg.delay_delete(1)
await msg_.delay_delete(1)
if not msg_.sticker:
add_ban_chat_member_job(chat.id, user.id)
await log(chat, user, LogAction.FAIL_ERROR)
await ServiceMessage.try_delete(user.id, chat.id)
else:
await cache.delete(key)
await log(chat, user, LogAction.ACCEPT)
except TimeoutConversationError:
await msg.delay_delete(1)
add_ban_chat_member_job(chat.id, user.id)
await log(chat, user, LogAction.FAIL_TIMEOUT)
await ServiceMessage.try_delete(user.id, chat.id)
@bot.on_chat_member_updated()
async def invite(client: Client, chat_member_updated: ChatMemberUpdated):
chat = chat_member_updated.chat
if await cache.get(f"cid:{chat.id}"):
return
member = chat_member_updated.new_chat_member
old_member = chat_member_updated.old_chat_member
if not member:
@ -27,6 +54,17 @@ async def invite(client: Client, chat_member_updated: ChatMemberUpdated):
return
user = member.user
old_user = old_member.user if old_member else None
if user.is_self:
if member.status not in {
ChatMemberStatus.ADMINISTRATOR,
ChatMemberStatus.MEMBER,
}:
return
await log(chat, chat_member_updated.from_user, LogAction.NEW_GROUP)
if chat.username:
with contextlib.suppress(Exception):
await client.send_message(chat.id, MSG_PUBLIC)
return
if user.is_verified or user.is_bot or user.is_deleted or user.is_support:
return
if member.status not in {ChatMemberStatus.MEMBER}:
@ -43,13 +81,6 @@ async def invite(client: Client, chat_member_updated: ChatMemberUpdated):
}
):
return
if user.is_self:
with contextlib.suppress(Exception):
await log(chat, chat_member_updated.from_user, "NEW_GROUP")
if chat.username:
with contextlib.suppress(Exception):
await client.send_message(chat.id, MSG_PUBLIC)
return
from_user = chat_member_updated.from_user
if from_user and from_user.id == user.id:
from_user = None
@ -61,42 +92,8 @@ async def invite(client: Client, chat_member_updated: ChatMemberUpdated):
and (await bot.get_chat_member(chat.id, from_user.id)).status
in {ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.OWNER}
):
try:
msg = await client.send_message(chat.id, ADMIN_MSG)
except Exception:
with contextlib.suppress(Exception):
msg: "Message" = await client.send_message(chat.id, ADMIN_MSG)
await msg.delay_delete()
return
add_delete_message_job(msg)
return
try:
msg = await client.send_message(chat.id, MSG % (user.mention, user.mention))
except Exception:
return
try:
with contextlib.suppress(Exception):
await log(chat, user, "REQUEST")
msg_ = await client.listen(chat.id, filters=filters.user(user.id), timeout=VERIFY_TIME)
with contextlib.suppress(Exception):
await msg.delete()
if not msg_.sticker:
with contextlib.suppress(Exception):
await bot.ban_chat_member(
chat.id, user.id, datetime.now() + timedelta(minutes=5)
)
with contextlib.suppress(Exception):
await log(chat, user, "FAIL_ERROR")
await ServiceMessage.try_delete(user.id, chat.id)
else:
with contextlib.suppress(Exception):
await log(chat, user, "ACCEPT")
with contextlib.suppress(Exception):
await msg_.delete()
except TimeoutConversationError:
with contextlib.suppress(Exception):
await msg.delete()
with contextlib.suppress(Exception):
await bot.ban_chat_member(
chat.id, user.id, datetime.now() + timedelta(minutes=5)
)
with contextlib.suppress(Exception):
await log(chat, user, "FAIL_TIMEOUT")
await ServiceMessage.try_delete(user.id, chat.id)
await start_verify(client, chat, user)

17
plugins/clean_message.py Normal file
View File

@ -0,0 +1,17 @@
from cashews import cache
from pyrogram import filters
from sticker import bot
from sticker.single_utils import Message
@bot.on_message(filters=filters.group & ~filters.service, group=1)
async def clean_message(_, message: "Message"):
if not message.from_user:
return
cid = message.chat.id
uid = message.from_user.id
key = f"wait:{cid}:{uid}"
result = await cache.get(key)
if result:
await message.delay_delete(1)

View File

@ -1,13 +1,12 @@
import contextlib
from cashews import cache
from pyrogram.types import ChatJoinRequest
from pyrogram import filters
from sticker.languages import MSG, MSG_SUCCESS, MSG_FAILURE, VERIFY_TIME
from sticker.single_utils import Client
from sticker.scheduler import add_decline_request_job, rem_decline_request_job
from sticker import bot, log
from sticker import bot, log, LogAction
from pyromod.utils.errors import TimeoutConversationError
@ -15,24 +14,24 @@ from pyromod.utils.errors import TimeoutConversationError
@bot.on_chat_join_request()
async def new_member(client: Client, chat_join_request: ChatJoinRequest):
chat = chat_join_request.chat
await cache.set(f"cid:{chat.id}", "True", expire=3600, exist=True)
user = chat_join_request.from_user
add_decline_request_job(chat_join_request)
try:
with contextlib.suppress(Exception):
await log(chat, user, "REQUEST")
await client.ask(user.id, MSG % (chat.title, chat.title), filters=filters.sticker, timeout=VERIFY_TIME)
await log(chat, user, LogAction.REQUEST)
await client.ask(
user.id,
MSG % (chat.title, chat.title),
filters=filters.sticker,
timeout=VERIFY_TIME,
)
with contextlib.suppress(Exception):
await client.send_message(user.id, MSG_SUCCESS)
await chat_join_request.approve()
with contextlib.suppress(Exception):
await log(chat, user, "ACCEPT")
with contextlib.suppress(Exception):
await log(chat, user, LogAction.ACCEPT)
rem_decline_request_job(chat_join_request)
except TimeoutConversationError:
with contextlib.suppress(Exception):
await client.send_message(user.id, MSG_FAILURE)
with contextlib.suppress(Exception):
await chat_join_request.decline()
with contextlib.suppress(Exception):
await log(chat, user, "FAIL_TIMEOUT")
await log(chat, user, LogAction.FAIL_TIMEOUT)

View File

@ -1,25 +1,23 @@
import contextlib
from cashews import cache
from pyrogram import filters
from pyrogram.enums import ChatMemberStatus
from datetime import datetime, timedelta
from pyromod.utils.errors import TimeoutConversationError
from sticker.languages import RE_MSG, VERIFY_TIME
from sticker.scheduler import add_delete_message_job
from sticker.scheduler import add_ban_chat_member_job
from sticker.single_utils import Message, Client
from sticker import bot, log
from sticker import bot, log, LogAction
@bot.on_message(filters=filters.group & filters.command("reverify"))
async def re_verify(client: Client, message: Message):
if not message.from_user or not message.reply_to_message:
reply_to: "Message" = message.reply_to_message
if not message.from_user or not reply_to:
msg: Message = await message.reply("请回复一条消息来使 Ta 重新验证。")
add_delete_message_job(message, 10)
add_delete_message_job(msg, 10)
await message.delay_delete(10)
await msg.delay_delete(10)
return
if not message.reply_to_message.from_user:
if not reply_to.from_user:
return
chat = message.chat
user = message.from_user
@ -27,7 +25,7 @@ async def re_verify(client: Client, message: Message):
if member.status not in [ChatMemberStatus.OWNER, ChatMemberStatus.ADMINISTRATOR]:
return
user = message.reply_to_message.from_user
user = reply_to.from_user
if (
user.is_self
or user.is_verified
@ -37,46 +35,36 @@ async def re_verify(client: Client, message: Message):
):
return
member = await client.get_chat_member(chat.id, user.id)
with contextlib.suppress(Exception):
await message.delete()
await message.delay_delete(1)
key = f"wait:{chat.id}:{user.id}"
await cache.set(key, "True", expire=VERIFY_TIME + 5)
try:
msg = await message.reply_to_message.reply(RE_MSG % (user.mention, user.mention))
msg = await reply_to.reply(RE_MSG % (user.mention, user.mention))
except Exception as _:
return
try:
msg_ = await client.listen(chat.id, filters=filters.user(user.id), timeout=VERIFY_TIME)
with contextlib.suppress(Exception):
await msg.delete()
msg_ = await client.listen(
chat.id, filters=filters.user(user.id), timeout=VERIFY_TIME
)
await msg.delay_delete(1)
await msg_.delay_delete(1)
if not msg_.sticker:
with contextlib.suppress(Exception):
await message.reply_to_message.delete()
await reply_to.delay_delete(1)
if member.status not in [
ChatMemberStatus.OWNER,
ChatMemberStatus.ADMINISTRATOR,
]:
with contextlib.suppress(Exception):
await bot.ban_chat_member(
chat.id, user.id, datetime.now() + timedelta(minutes=5)
)
with contextlib.suppress(Exception):
await log(chat, user, "FAIL_ERROR")
add_ban_chat_member_job(chat.id, user.id)
await log(chat, user, LogAction.FAIL_ERROR)
else:
with contextlib.suppress(Exception):
await log(chat, user, "ACCEPT")
with contextlib.suppress(Exception):
await msg_.delete()
await cache.delete(key)
await log(chat, user, LogAction.ACCEPT)
except TimeoutConversationError:
with contextlib.suppress(Exception):
await msg.delete()
with contextlib.suppress(Exception):
await message.reply_to_message.delete()
await msg.delay_delete(1)
await reply_to.delay_delete(1)
if member.status not in [
ChatMemberStatus.OWNER,
ChatMemberStatus.ADMINISTRATOR,
]:
with contextlib.suppress(Exception):
await bot.ban_chat_member(
chat.id, user.id, datetime.now() + timedelta(minutes=5)
)
with contextlib.suppress(Exception):
await log(chat, user, "FAIL_TIMEOUT")
add_ban_chat_member_job(chat.id, user.id)
await log(chat, user, LogAction.FAIL_TIMEOUT)

View File

@ -1,21 +0,0 @@
"""
pyromod - A monkeypatcher add-on for Pyrogram
Copyright (C) 2020 Cezar H. <https://github.com/usernein>
This file is part of pyromod.
pyromod is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pyromod is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with pyromod. If not, see <https://www.gnu.org/licenses/>.
"""
from .filters import dice

View File

@ -1,28 +0,0 @@
"""
pyromod - A monkeypatcher add-on for Pyrogram
Copyright (C) 2020 Cezar H. <https://github.com/usernein>
This file is part of pyromod.
pyromod is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pyromod is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with pyromod. If not, see <https://www.gnu.org/licenses/>.
"""
import pyrogram
def dice(ctx, message):
return hasattr(message, "dice") and message.dice
pyrogram.filters.dice = dice

View File

@ -1,20 +0,0 @@
"""
pyromod - A monkeypatcher add-on for Pyrogram
Copyright (C) 2020 Cezar H. <https://github.com/usernein>
This file is part of pyromod.
pyromod is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pyromod is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with pyromod. If not, see <https://www.gnu.org/licenses/>.
"""
from .helpers import ikb, bki, ntb, btn, kb, kbtn, array_chunk, force_reply

View File

@ -1,86 +0,0 @@
from pyrogram.types import (
InlineKeyboardButton,
InlineKeyboardMarkup,
KeyboardButton,
ReplyKeyboardMarkup,
ForceReply,
)
def ikb(rows=None):
if rows is None:
rows = []
lines = []
for row in rows:
line = []
for button in row:
button = btn(*button) # InlineKeyboardButton
line.append(button)
lines.append(line)
return InlineKeyboardMarkup(inline_keyboard=lines)
# return {'inline_keyboard': lines}
def btn(text, value, type="callback_data"):
return InlineKeyboardButton(text, **{type: value})
# return {'text': text, type: value}
# The inverse of above
def bki(keyboard):
lines = []
for row in keyboard.inline_keyboard:
line = []
for button in row:
button = ntb(button) # btn() format
line.append(button)
lines.append(line)
return lines
# return ikb() format
def ntb(button):
for btn_type in [
"callback_data",
"url",
"switch_inline_query",
"switch_inline_query_current_chat",
"callback_game",
]:
value = getattr(button, btn_type)
if value:
break
button = [button.text, value]
if btn_type != "callback_data":
button.append(btn_type)
return button
# return {'text': text, type: value}
def kb(rows=None, **kwargs):
if rows is None:
rows = []
lines = []
for row in rows:
line = []
for button in row:
button_type = type(button)
if button_type == str:
button = KeyboardButton(button)
elif button_type == dict:
button = KeyboardButton(**button)
line.append(button)
lines.append(line)
return ReplyKeyboardMarkup(keyboard=lines, **kwargs)
kbtn = KeyboardButton
def force_reply(selective=True):
return ForceReply(selective=selective)
def array_chunk(input_, size):
return [input_[i : i + size] for i in range(0, len(input_), size)]

View File

@ -19,16 +19,18 @@ along with pyromod. If not, see <https://www.gnu.org/licenses/>.
"""
import asyncio
import contextlib
import functools
import pyrogram
from typing import Dict
from sticker.scheduler import add_delete_message_job
from ..utils import patch, patchable
from ..utils.errors import ListenerCanceled, TimeoutConversationError
pyrogram.errors.ListenerCanceled = ListenerCanceled
LOCK = asyncio.Lock()
DONE = []
@patch(pyrogram.client.Client)
@ -63,6 +65,7 @@ class Client:
@patchable
def clear_listener(self, chat_id, future):
with contextlib.suppress(KeyError):
if future == self.listening[chat_id]["future"]:
self.listening.pop(chat_id, None)
@ -90,25 +93,33 @@ class MessageHandler:
@patchable
async def resolve_listener(self, client, message, *args):
global LOCK, DONE
async with LOCK:
listener = client.listening.get(message.chat.id)
if listener:
with contextlib.suppress(ValueError):
DONE.remove(listener)
if listener and not listener["future"].done():
listener["future"].set_result(message)
else:
return
if listener and listener["future"].done():
client.clear_listener(message.chat.id, listener["future"])
await self.user_callback(client, message, *args)
@patchable
async def check(self, client, update):
global LOCK, DONE
async with LOCK:
listener = client.listening.get(update.chat.id)
if listener and not listener["future"].done():
return (
await listener["filters"](client, update)
if callable(listener["filters"])
else True
)
if listener and (listener not in DONE) and (not listener["future"].done()):
if callable(listener["filters"]):
result = await listener["filters"](client, update)
if result:
DONE.append(listener)
return result
else:
DONE.append(listener)
return True
return await self.filters(client, update) if callable(self.filters) else True

View File

@ -1,20 +0,0 @@
"""
pyromod - A monkeypatcher add-on for Pyrogram
Copyright (C) 2020 Cezar H. <https://github.com/usernein>
This file is part of pyromod.
pyromod is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pyromod is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with pyromod. If not, see <https://www.gnu.org/licenses/>.
"""
from .pagination import Pagination

View File

@ -1,93 +0,0 @@
"""
pyromod - A monkeypatcher add-on for Pyrogram
Copyright (C) 2020 Cezar H. <https://github.com/usernein>
This file is part of pyromod.
pyromod is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pyromod is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with pyromod. If not, see <https://www.gnu.org/licenses/>.
"""
import math
from ..helpers import array_chunk
class Pagination:
def __init__(self, objects, page_data=None, item_data=None, item_title=None):
default_page_callback = lambda x: str(x)
default_item_callback = lambda i, pg: f"[{pg}] {i}"
self.objects = objects
self.page_data = page_data or default_page_callback
self.item_data = item_data or default_item_callback
self.item_title = item_title or default_item_callback
def create(self, page, lines=5, columns=1):
quant_per_page = lines * columns
page = 1 if page <= 0 else page
offset = (page - 1) * quant_per_page
stop = offset + quant_per_page
cutted = self.objects[offset:stop]
total = len(self.objects)
pages_range = [
*range(1, math.ceil(total / quant_per_page) + 1)
] # each item is a page
last_page = len(pages_range)
nav = []
if page <= 3:
for n in [1, 2, 3]:
if n not in pages_range:
continue
text = f"· {n} ·" if n == page else n
nav.append((text, self.page_data(n)))
if last_page >= 4:
nav.append(("4 " if last_page > 5 else 4, self.page_data(4)))
if last_page > 4:
nav.append(
(
f"{last_page} »" if last_page > 5 else last_page,
self.page_data(last_page),
)
)
elif page >= last_page - 2:
nav.extend(
[
("« 1" if last_page > 5 else 1, self.page_data(1)),
(
f" {last_page - 3}" if last_page > 5 else last_page - 3,
self.page_data(last_page - 3),
),
]
)
for n in range(last_page - 2, last_page + 1):
text = f"· {n} ·" if n == page else n
nav.append((text, self.page_data(n)))
else:
nav = [
("« 1", self.page_data(1)),
(f" {page - 1}", self.page_data(page - 1)),
(f"· {page} ·", "noop"),
(f"{page + 1} ", self.page_data(page + 1)),
(f"{last_page} »", self.page_data(last_page)),
]
buttons = [
(self.item_title(item, page), self.item_data(item, page)) for item in cutted
]
kb_lines = array_chunk(buttons, columns)
if last_page > 1:
kb_lines.append(nav)
return kb_lines

View File

@ -1,5 +1,7 @@
import contextlib
import sys
from enum import Enum
from cashews import cache
from datetime import datetime, timezone
from logging import getLogger, StreamHandler, CRITICAL, INFO, basicConfig, DEBUG
@ -54,22 +56,48 @@ bot = Client(
)
async def log(chat, user, action):
class LogAction(str, Enum):
FAIL_ERROR = "FAIL_ERROR"
FAIL_TIMEOUT = "FAIL_TIMEOUT"
ACCEPT = "ACCEPT"
NEW_GROUP = "NEW_GROUP"
REQUEST = "REQUEST"
async def log(chat, user, action: LogAction):
scheduler.add_job(
_log,
args=[chat, user, action],
replace_existing=False,
)
async def _log(chat, user, action: LogAction):
if not Config.LOG_CHANNEL:
return
me = await bot.get_me()
event = {
"FAIL_ERROR": "回答错误",
"FAIL_TIMEOUT": "回答超时",
"ACCEPT": "通过验证",
"NEW_GROUP": "加入群组",
"REQUEST": "发起验证",
LogAction.FAIL_ERROR: "回答错误",
LogAction.FAIL_TIMEOUT: "回答超时",
LogAction.ACCEPT: "通过验证",
LogAction.NEW_GROUP: "加入群组",
LogAction.REQUEST: "发起验证",
}
msg = """#%s
群组: %s
群组id: <code>%s</code>
用户: #id%s
昵称<code>%s</code>
OPBot: #bot%s
事件: %s"""
msg %= (action, chat.title, chat.id, user.id if user else "", me.id, event[action])
user_name = user.full_name if user else ""
msg %= (
action.value,
chat.title,
chat.id,
user.id if user else "",
user_name,
me.id,
event[action],
)
await bot.send_message(Config.LOG_CHANNEL, msg)

View File

@ -1,4 +1,4 @@
VERIFY_TIME = 60
VERIFY_TIME = 180
MSG_PUBLIC = """您好,我发现此群组为公开群组,您需要联系创建者打开 `管理员批准后才能入群` 功能,我才能更好地工作。"""
MSG_SUCCESS = """验证成功,您已经成为群组的一员了!

View File

@ -5,6 +5,7 @@ import pytz
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from pyrogram.types import ChatJoinRequest
from sticker.languages import VERIFY_TIME
from sticker.single_utils import Message
scheduler = AsyncIOScheduler(timezone="Asia/ShangHai")
@ -24,6 +25,17 @@ async def decline_request(chat_join_request: ChatJoinRequest):
return False
async def ban_chat_member(chat_id: int, user_id: int):
from sticker import bot
with contextlib.suppress(Exception):
await bot.ban_chat_member(
chat_id, user_id, datetime.datetime.now() + datetime.timedelta(minutes=5)
)
return True
return False
def add_delete_message_job(message: Message, delete_seconds: int = 60):
scheduler.add_job(
delete_message,
@ -45,7 +57,17 @@ def add_decline_request_job(chat_join_request: ChatJoinRequest):
name=f"{chat_join_request.chat.id}|{chat_join_request.from_user.id}|decline_request",
args=[chat_join_request],
run_date=datetime.datetime.now(pytz.timezone("Asia/Shanghai"))
+ datetime.timedelta(seconds=60),
+ datetime.timedelta(seconds=VERIFY_TIME),
replace_existing=True,
)
def add_ban_chat_member_job(chat_id: int, user_id: int):
scheduler.add_job(
ban_chat_member,
id=f"{chat_id}|{user_id}|ban_chat_member",
name=f"{chat_id}|{user_id}|ban_chat_member",
args=[chat_id, user_id],
replace_existing=True,
)