修复 quiz 模块问题并添加验证群验证插件

修复 `quiz` 模块问题并添加验证群验证插件
This commit is contained in:
洛水居室 2022-08-31 14:46:04 +08:00 committed by GitHub
parent aa882c3f93
commit 3ec99cc3b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 668 additions and 28 deletions

View File

@ -23,4 +23,7 @@ ERROR_NOTIFICATION_CHAT_ID=chat_id
CHANNELS=[{ "name": "", "chat_id": 1}] CHANNELS=[{ "name": "", "chat_id": 1}]
# bot 管理员 # bot 管理员
ADMINS=[{ "username": "", "user_id": 1 }] ADMINS=[{ "username": "", "user_id": -1 }]
# 群验证功能
JOINING_VERIFICATION_GROUPS=[]

View File

@ -11,6 +11,7 @@ load_dotenv()
env = os.getenv env = os.getenv
def str_to_bool(value: Any) -> bool: def str_to_bool(value: Any) -> bool:
"""Return whether the provided string (or any value really) represents true. Otherwise false. """Return whether the provided string (or any value really) represents true. Otherwise false.
Just like plugin server stringToBoolean. Just like plugin server stringToBoolean.
@ -19,8 +20,9 @@ def str_to_bool(value: Any) -> bool:
return False return False
return str(value).lower() in ("y", "yes", "t", "true", "on", "1") return str(value).lower() in ("y", "yes", "t", "true", "on", "1")
_config = { _config = {
"debug": str_to_bool(os.getenv('DEBUG', 'True')), "debug": str_to_bool(os.getenv('DEBUG', 'False')),
"mysql": { "mysql": {
"host": env("DB_HOST", "127.0.0.1"), "host": env("DB_HOST", "127.0.0.1"),
@ -55,6 +57,8 @@ _config = {
# 在环境变量里的格式是 json: [{"username": "", "user_id": 1}] # 在环境变量里的格式是 json: [{"username": "", "user_id": 1}]
*ujson.loads(env('ADMINS', '[]')) *ujson.loads(env('ADMINS', '[]'))
], ],
"joining_verification_groups": env('JOINING_VERIFICATION_GROUPS', '[]'),
} }
config = Storage(_config) config = Storage(_config)

View File

@ -0,0 +1,14 @@
from utils.mysql import MySQL
from utils.redisdb import RedisDB
from utils.service.manager import listener_service
from .cache import QuizCache
from .repositories import QuizRepository
from .services import QuizService
@listener_service()
def create_quiz_service(mysql: MySQL, redis: RedisDB):
_repository = QuizRepository(mysql)
_cache = QuizCache(redis)
_service = QuizService(_repository, _cache)
return _service

View File

@ -1,9 +1,9 @@
from typing import List from typing import List
import ujson import ujson
from app.quiz.models import Question, Answer
from utils.redisdb import RedisDB from utils.redisdb import RedisDB
from .models import Question, Answer
class QuizCache: class QuizCache:
@ -30,15 +30,18 @@ class QuizCache:
async def get_one_question(self, question_id: int) -> Question: async def get_one_question(self, question_id: int) -> Question:
qname = f"{self.question_qname}:{question_id}" qname = f"{self.question_qname}:{question_id}"
data = await self.client.get(qname) data = await self.client.get(qname)
return Question.de_json(ujson.loads(data)) json_data = str(data, encoding="utf-8")
return Question.de_json(ujson.loads(json_data))
async def get_one_answer(self, answer_id: int) -> str: async def get_one_answer(self, answer_id: int) -> Answer:
qname = f"{self.answer_qname}:{answer_id}" qname = f"{self.answer_qname}:{answer_id}"
return await self.client.get(qname) data = await self.client.get(qname)
json_data = str(data, encoding="utf-8")
return Answer.de_json(ujson.loads(json_data))
async def add_question(self, question_list: List[Question] = None): async def add_question(self, question_list: List[Question] = None):
for question in question_list: for question in question_list:
await self.client.set(f"{self.question_qname}:{question.question_id}", str(question)) await self.client.set(f"{self.question_qname}:{question.question_id}", ujson.dumps(question.to_dict()))
question_id_list = [question.question_id for question in question_list] question_id_list = [question.question_id for question in question_list]
await self.client.lpush(f"{self.question_qname}:id_list", *question_id_list) await self.client.lpush(f"{self.question_qname}:id_list", *question_id_list)
return await self.client.llen(f"{self.question_qname}:id_list") return await self.client.llen(f"{self.question_qname}:id_list")
@ -57,7 +60,7 @@ class QuizCache:
async def add_answer(self, answer_list: List[Answer] = None): async def add_answer(self, answer_list: List[Answer] = None):
for answer in answer_list: for answer in answer_list:
await self.client.set(f"{self.answer_qname}:{answer.answer_id}", str(answer)) await self.client.set(f"{self.answer_qname}:{answer.answer_id}", ujson.dumps(answer.to_dict()))
answer_id_list = [answer.answer_id for answer in answer_list] answer_id_list = [answer.answer_id for answer in answer_list]
await self.client.lpush(f"{self.answer_qname}:id_list", *answer_id_list) await self.client.lpush(f"{self.answer_qname}:id_list", *answer_id_list)
return await self.client.llen(f"{self.answer_qname}:id_list") return await self.client.llen(f"{self.answer_qname}:id_list")

View File

@ -78,7 +78,7 @@ class Question(BaseObject):
def to_dict(self) -> JSONDict: def to_dict(self) -> JSONDict:
data = super().to_dict() data = super().to_dict()
if self.answers: if self.answers:
data["sub_item"] = [e.to_dict() for e in self.answers] data["answers"] = [e.to_dict() for e in self.answers]
return data return data
@classmethod @classmethod
@ -86,7 +86,7 @@ class Question(BaseObject):
data = cls._parse_data(data) data = cls._parse_data(data)
if not data: if not data:
return None return None
data["sub_item"] = Answer.de_list(data.get("sub_item")) data["answers"] = Answer.de_list(data.get("answers"))
return cls(**data) return cls(**data)
__slots__ = ("question_id", "text", "answers") __slots__ = ("question_id", "text", "answers")

View File

@ -15,14 +15,14 @@ class QuizRepository:
query = select(QuestionDB) query = select(QuestionDB)
results = await session.exec(query) results = await session.exec(query)
questions = results.all() questions = results.all()
return [question[0] for question in questions] return questions
async def get_answers_form_question_id(self, question_id: int) -> List[AnswerDB]: async def get_answers_form_question_id(self, question_id: int) -> List[AnswerDB]:
async with self.mysql.Session() as session: async with self.mysql.Session() as session:
query = select(AnswerDB).where(AnswerDB.question_id == question_id) query = select(AnswerDB).where(AnswerDB.question_id == question_id)
results = await session.exec(query) results = await session.exec(query)
answers = results.all() answers = results.all()
return [answer[0] for answer in answers] return answers
async def add_question(self, question: QuestionDB): async def add_question(self, question: QuestionDB):
async with self.mysql.Session() as session: async with self.mysql.Session() as session:

View File

@ -1,7 +1,6 @@
import asyncio
from typing import List from typing import List
import ujson
from .cache import QuizCache from .cache import QuizCache
from .models import Question, Answer from .models import Question, Answer
from .repositories import QuizRepository from .repositories import QuizRepository
@ -11,6 +10,7 @@ class QuizService:
def __init__(self, repository: QuizRepository, cache: QuizCache): def __init__(self, repository: QuizRepository, cache: QuizCache):
self._repository = repository self._repository = repository
self._cache = cache self._cache = cache
self.lock = asyncio.Lock()
async def get_quiz_form_database(self) -> List[Question]: async def get_quiz_form_database(self) -> List[Question]:
"""从数据库获取问题列表 """从数据库获取问题列表
@ -21,7 +21,6 @@ class QuizService:
for question in question_list: for question in question_list:
question_id = question.id question_id = question.id
answers = await self._repository.get_answers_form_question_id(question_id) answers = await self._repository.get_answers_form_question_id(question_id)
question.answers = answers
data = Question.de_database_data(question) data = Question.de_database_data(question)
data.answers = [Answer.de_database_data(a) for a in answers] data.answers = [Answer.de_database_data(a) for a in answers]
temp.append(data) temp.append(data)
@ -33,20 +32,30 @@ class QuizService:
await self._repository.add_answer(answers.to_database_data()) await self._repository.add_answer(answers.to_database_data())
async def refresh_quiz(self) -> int: async def refresh_quiz(self) -> int:
question_list = await self.get_quiz_form_database() """从数据库刷新问题到Redis缓存 线程安全
await self._cache.del_all_question() :return: 已经缓存问题的数量
question_count = await self._cache.add_question(question_list) """
await self._cache.del_all_answer() # 只允许一个线程访问该区域 让数据被安全有效的访问
for question in question_list: async with self.lock:
await self._cache.add_answer(question.answers) question_list = await self.get_quiz_form_database()
return question_count await self._cache.del_all_question()
question_count = await self._cache.add_question(question_list)
await self._cache.del_all_answer()
for question in question_list:
await self._cache.add_answer(question.answers)
return question_count
async def get_question_id_list(self) -> List[int]: async def get_question_id_list(self) -> List[int]:
return [int(question_id) for question_id in await self._cache.get_all_question_id_list()] return [int(question_id) for question_id in await self._cache.get_all_question_id_list()]
async def get_answer(self, answer_id: int): async def get_answer(self, answer_id: int) -> Answer:
data = await self._cache.get_one_answer(answer_id) return await self._cache.get_one_answer(answer_id)
return ujson.loads(data)
async def get_question(self, question_id: int) -> Question: async def get_question(self, question_id: int) -> Question:
return await self._cache.get_one_question(question_id) return await self._cache.get_one_question(question_id)
async def delete_question_by_id(self, question_id: int):
return await self._repository.delete_question_by_id(question_id)
async def delete_answer_by_id(self, answer_id: int):
return await self._repository.delete_answer_by_id(answer_id)

View File

@ -1,4 +1,6 @@
import asyncio
import datetime import datetime
from typing import List, Tuple, Callable
from telegram import Update, ReplyKeyboardRemove from telegram import Update, ReplyKeyboardRemove
from telegram.error import BadRequest from telegram.error import BadRequest
@ -55,6 +57,12 @@ class NewChatMembersHandler:
@inject @inject
def __init__(self, bot_admin_service: BotAdminService): def __init__(self, bot_admin_service: BotAdminService):
self.bot_admin_service = bot_admin_service self.bot_admin_service = bot_admin_service
self.callback: List[Tuple[Callable, int]] = []
def add_callback(self, callback, chat_id: int):
if chat_id >= 0:
raise ValueError
self.callback.append((callback, chat_id))
async def new_member(self, update: Update, context: CallbackContext) -> None: async def new_member(self, update: Update, context: CallbackContext) -> None:
message = update.message message = update.message
@ -79,4 +87,11 @@ class NewChatMembersHandler:
Log.warning("不是管理员邀请!退出群聊。") Log.warning("不是管理员邀请!退出群聊。")
await context.bot.send_message(message.chat_id, "派蒙不想进去!不是旅行者的邀请!") await context.bot.send_message(message.chat_id, "派蒙不想进去!不是旅行者的邀请!")
await context.bot.leave_chat(chat.id) await context.bot.leave_chat(chat.id)
else:
tasks = []
for callback, chat_id in self.callback:
if chat.id == chat_id:
task = asyncio.create_task(callback(update, context))
tasks.append(task)
if len(tasks) >= 1:
await asyncio.gather(*tasks)

291
plugins/genshin/quiz.py Normal file
View File

@ -0,0 +1,291 @@
import random
import re
from typing import List, Optional
from redis import DataError, ResponseError
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, Poll, \
ReplyKeyboardRemove, Message
from telegram.constants import ChatAction
from telegram.ext import CallbackContext, ConversationHandler, CommandHandler, MessageHandler, filters
from telegram.helpers import escape_markdown
from core.admin import BotAdminService
from core.quiz import QuizService
from core.quiz.models import Answer, Question
from logger import Log
from plugins.base import BasePlugins
from utils.decorators.restricts import restricts
from utils.plugins.manager import listener_plugins_class
from utils.random import MT19937_Random
class QuizCommandData:
question_id: int = -1
new_question: str = ""
new_correct_answer: str = ""
new_wrong_answer: List[str] = []
status: int = 0
@listener_plugins_class()
class QuizPlugin(BasePlugins):
"""派蒙的十万个为什么
合并了问题修改/添加/删除
"""
CHECK_COMMAND, VIEW_COMMAND, CHECK_QUESTION, \
GET_NEW_QUESTION, GET_NEW_CORRECT_ANSWER, GET_NEW_WRONG_ANSWER, \
QUESTION_EDIT, SAVE_QUESTION = range(10300, 10308)
def __init__(self, quiz_service: QuizService = None, bot_admin_service: BotAdminService = None):
self.bot_admin_service = bot_admin_service
self.user_time = {}
self.quiz_service = quiz_service
self.time_out = 120
self.random = MT19937_Random()
@classmethod
def create_handlers(cls):
quiz = cls()
quiz_handler = ConversationHandler(
entry_points=[CommandHandler('quiz', quiz.command_start, block=True)],
states={
quiz.CHECK_COMMAND: [MessageHandler(filters.TEXT & ~filters.COMMAND,
quiz.check_command, block=True)],
quiz.CHECK_QUESTION: [MessageHandler(filters.TEXT & ~filters.COMMAND,
quiz.check_question, block=True)],
quiz.GET_NEW_QUESTION: [MessageHandler(filters.TEXT & ~filters.COMMAND,
quiz.get_new_question, block=True)],
quiz.GET_NEW_CORRECT_ANSWER: [MessageHandler(filters.TEXT & ~filters.COMMAND,
quiz.get_new_correct_answer, block=True)],
quiz.GET_NEW_WRONG_ANSWER: [MessageHandler(filters.TEXT & ~filters.COMMAND,
quiz.get_new_wrong_answer, block=True),
CommandHandler("finish", quiz.finish_edit)],
quiz.SAVE_QUESTION: [MessageHandler(filters.TEXT & ~filters.COMMAND,
quiz.save_question, block=True)],
},
fallbacks=[CommandHandler('cancel', quiz.cancel, block=True)]
)
return [quiz_handler]
async def send_poll(self, update: Update) -> Optional[Message]:
chat = update.message.chat
user = update.effective_user
question_id_list = await self.quiz_service.get_question_id_list()
if filters.ChatType.GROUPS.filter(update.message):
Log.info(f"用户 {user.full_name}[{user.id}] 在群 {chat.title}[{chat.id}] 发送挑战问题命令请求")
if len(question_id_list) == 0:
return None
if len(question_id_list) == 0:
return None
index = self.random.random(0, len(question_id_list))
question = await self.quiz_service.get_question(question_id_list[index])
_options = []
correct_option = None
for answer in question.answers:
_options.append(answer.text)
if answer.is_correct:
correct_option = answer.text
if correct_option is None:
question_id = question["question_id"]
Log.warning(f"Quiz模块 correct_option 异常 question_id[{question_id}] ")
return None
random.shuffle(_options)
index = _options.index(correct_option)
return await update.effective_message.reply_poll(question.text, _options,
correct_option_id=index, is_anonymous=False,
open_period=self.time_out, type=Poll.QUIZ)
@restricts(filters.ChatType.GROUPS, ConversationHandler.END, restricts_time=20, try_delete_message=True)
@restricts(filters.ChatType.PRIVATE, ConversationHandler.END)
async def command_start(self, update: Update, context: CallbackContext) -> int:
user = update.effective_user
message = update.message
if filters.ChatType.PRIVATE.filter(message):
Log.info(f"用户 {user.full_name}[{user.id}] quiz命令请求")
admin_list = await self.bot_admin_service.get_admin_list()
if user.id in admin_list:
quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data")
if quiz_command_data is None:
quiz_command_data = QuizCommandData()
context.chat_data["quiz_command_data"] = quiz_command_data
text = f'你好 {user.mention_markdown_v2()} {escape_markdown("!请选择你的操作!")}'
reply_keyboard = [
["查看问题", "添加问题"],
["重载问题"],
["退出"]
]
await message.reply_markdown_v2(text, reply_markup=ReplyKeyboardMarkup(reply_keyboard,
one_time_keyboard=True))
return self.CHECK_COMMAND
else:
await self.send_poll(update)
elif filters.ChatType.GROUPS.filter(update.message):
await update.message.reply_chat_action(ChatAction.TYPING)
poll_message = await self.send_poll(update)
if poll_message is None:
return ConversationHandler.END
self._add_delete_message_job(context, update.message.chat_id, update.message.message_id, 300)
self._add_delete_message_job(context, poll_message.chat_id, poll_message.message_id, 300)
return ConversationHandler.END
async def view_command(self, update: Update, _: CallbackContext) -> int:
keyboard = [
[
InlineKeyboardButton(text="选择问题", switch_inline_query_current_chat="查看问题 ")
]
]
await update.message.reply_text("请回复你要查看的问题",
reply_markup=InlineKeyboardMarkup(keyboard))
return self.CHECK_COMMAND
async def check_question(self, update: Update, _: CallbackContext) -> int:
reply_keyboard = [
["删除问题"],
["退出"]
]
await update.message.reply_text("请选择你的操作", reply_markup=ReplyKeyboardMarkup(reply_keyboard))
return self.CHECK_COMMAND
async def check_command(self, update: Update, context: CallbackContext) -> int:
quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data")
if update.message.text == "退出":
await update.message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
elif update.message.text == "查看问题":
return await self.view_command(update, context)
elif update.message.text == "添加问题":
return await self.add_question(update, context)
elif update.message.text == "删除问题":
return await self.delete_question(update, context)
# elif update.message.text == "修改问题":
# return await self.edit_question(update, context)
elif update.message.text == "重载问题":
return await self.refresh_question(update, context)
else:
result = re.findall(r"问题ID (\d+)", update.message.text)
if len(result) == 1:
try:
question_id = int(result[0])
except ValueError:
await update.message.reply_text("获取问题ID失败")
return ConversationHandler.END
quiz_command_data.question_id = question_id
await update.message.reply_text("获取问题ID成功")
return await self.check_question(update, context)
await update.message.reply_text("命令错误", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
async def refresh_question(self, update: Update, _: CallbackContext) -> int:
try:
await self.quiz_service.refresh_quiz()
except DataError:
await update.message.reply_text("Redis数据错误重载失败", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
except ResponseError as error:
Log.error("重载问题失败", error)
await update.message.reply_text("重载问题失败异常抛出Redis请求错误异常详情错误请看日记",
reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
await update.message.reply_text("重载成功", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
async def add_question(self, update: Update, context: CallbackContext) -> int:
quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data")
quiz_command_data.new_wrong_answer = []
quiz_command_data.new_question = ""
quiz_command_data.new_correct_answer = ""
quiz_command_data.status = 1
await update.message.reply_text("请回复你要添加的问题,或发送 /cancel 取消操作", reply_markup=ReplyKeyboardRemove())
return self.GET_NEW_QUESTION
async def get_new_question(self, update: Update, context: CallbackContext) -> int:
quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data")
reply_text = f"问题:`{escape_markdown(update.message.text, version=2)}`\n" \
f"请填写正确答案:"
quiz_command_data.new_question = update.message.text
await update.message.reply_markdown_v2(reply_text)
return self.GET_NEW_CORRECT_ANSWER
async def get_new_correct_answer(self, update: Update, context: CallbackContext) -> int:
quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data")
reply_text = f"正确答案:`{escape_markdown(update.message.text, version=2)}`\n" \
f"请填写错误答案:"
await update.message.reply_markdown_v2(reply_text)
quiz_command_data.new_correct_answer = update.message.text
return self.GET_NEW_WRONG_ANSWER
async def get_new_wrong_answer(self, update: Update, context: CallbackContext) -> int:
quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data")
reply_text = f"错误答案:`{escape_markdown(update.message.text, version=2)}`\n" \
f"可继续填写,并使用 {escape_markdown('/finish', version=2)} 结束。"
await update.message.reply_markdown_v2(reply_text)
quiz_command_data.new_wrong_answer.append(update.message.text)
return self.GET_NEW_WRONG_ANSWER
async def finish_edit(self, update: Update, context: CallbackContext):
quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data")
reply_text = f"问题:`{escape_markdown(quiz_command_data.new_question, version=2)}`\n" \
f"正确答案:`{escape_markdown(quiz_command_data.new_correct_answer, version=2)}`\n" \
f"错误答案:`{escape_markdown(' '.join(quiz_command_data.new_wrong_answer), version=2)}`"
await update.message.reply_markdown_v2(reply_text)
reply_keyboard = [["保存并重载配置", "抛弃修改并退出"]]
await update.message.reply_text("请核对问题,并选择下一步操作。", reply_markup=ReplyKeyboardMarkup(reply_keyboard))
return self.SAVE_QUESTION
async def save_question(self, update: Update, context: CallbackContext):
quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data")
if update.message.text == "抛弃修改并退出":
await update.message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
elif update.message.text == "保存并重载配置":
if quiz_command_data.status == 1:
answer = [
Answer(text=wrong_answer, is_correct=False) for wrong_answer in
quiz_command_data.new_wrong_answer
]
answer.append(Answer(text=quiz_command_data.new_correct_answer, is_correct=True))
await self.quiz_service.save_quiz(
Question(text=quiz_command_data.new_question))
await update.message.reply_text("保存成功", reply_markup=ReplyKeyboardRemove())
try:
await self.quiz_service.refresh_quiz()
except ResponseError as error:
Log.error("重载问题失败", error)
await update.message.reply_text("重载问题失败异常抛出Redis请求错误异常详情错误请看日记",
reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
await update.message.reply_text("重载配置成功", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
else:
await update.message.reply_text("回复错误,请重新选择")
return self.SAVE_QUESTION
async def edit_question(self, update: Update, context: CallbackContext) -> int:
quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data")
quiz_command_data.new_wrong_answer = []
quiz_command_data.new_question = ""
quiz_command_data.new_correct_answer = ""
quiz_command_data.status = 2
await update.message.reply_text("请回复你要修改的问题", reply_markup=ReplyKeyboardRemove())
return self.GET_NEW_QUESTION
async def delete_question(self, update: Update, context: CallbackContext) -> int:
quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data")
# 再问题重载Redis 以免redis数据为空时出现奔溃
try:
await self.quiz_service.refresh_quiz()
question = await self.quiz_service.get_question(quiz_command_data.question_id)
# 因为外键的存在,先删除答案
for answer in question.answers:
await self.quiz_service.delete_question_by_id(answer.answer_id)
await self.quiz_service.delete_question_by_id(question.question_id)
await update.message.reply_text("删除问题成功", reply_markup=ReplyKeyboardRemove())
await self.quiz_service.refresh_quiz()
except ResponseError as error:
Log.error("重载问题失败", error)
await update.message.reply_text("重载问题失败异常抛出Redis请求错误异常详情错误请看日记",
reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END
await update.message.reply_text("重载配置成功", reply_markup=ReplyKeyboardRemove())
return ConversationHandler.END

290
plugins/system/auth.py Normal file
View File

@ -0,0 +1,290 @@
import asyncio
import random
import time
from typing import Tuple, Union, Dict
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ChatPermissions, ChatMember
from telegram.constants import ParseMode
from telegram.error import BadRequest
from telegram.ext import CallbackContext
from telegram.helpers import escape_markdown
from core.quiz import QuizService
from logger import Log
from utils.random import MT19937_Random
from utils.service.inject import inject
FullChatPermissions = ChatPermissions(
can_send_messages=True,
can_send_media_messages=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,
)
class GroupJoiningVerification:
"""群验证模块"""
@inject
def __init__(self, quiz_service: QuizService = None):
self.quiz_service = quiz_service
self.time_out = 120
self.kick_time = 120
self.random = MT19937_Random()
self.lock = asyncio.Lock()
self.chat_administrators_cache: Dict[Union[str, int], Tuple[float, list[ChatMember]]] = {}
self.is_refresh_quiz = False
async def refresh_quiz(self):
async with self.lock:
if not self.is_refresh_quiz:
await self.quiz_service.refresh_quiz()
self.is_refresh_quiz = True
async def get_chat_administrators(self, context: CallbackContext, chat_id: Union[str, int]) -> list[ChatMember]:
async with self.lock:
cache_data = self.chat_administrators_cache.get(f"{chat_id}")
if cache_data is not None:
cache_time, chat_administrators = cache_data
if time.time() >= cache_time + 360:
return chat_administrators
chat_administrators = await context.bot.get_chat_administrators(chat_id)
self.chat_administrators_cache[f"{chat_id}"] = (time.time(), chat_administrators)
return chat_administrators
@staticmethod
def is_admin(chat_administrators: list[ChatMember], user_id: int) -> bool:
return any(admin.user.id == user_id for admin in chat_administrators)
async def kick_member_job(self, context: CallbackContext):
job = context.job
Log.debug(f"踢出用户 user_id[{job.user_id}] 在 chat_id[{job.chat_id}]")
try:
await context.bot.ban_chat_member(chat_id=job.chat_id, user_id=job.user_id,
until_date=int(time.time()) + self.kick_time)
except BadRequest as error:
Log.error(f"Auth模块在 chat_id[{job.chat_id}] user_id[{job.user_id}] 执行kick失败", error)
@staticmethod
async def clean_message_job(context: CallbackContext):
job = context.job
Log.debug(f"删除消息 chat_id[{job.chat_id}] 的 message_id[{job.data}]")
try:
await context.bot.delete_message(chat_id=job.chat_id, message_id=job.data)
except BadRequest as error:
if "not found" in str(error):
Log.warning(f"Auth模块删除消息 chat_id[{job.chat_id}] message_id[{job.data}]失败 消息不存在")
elif "Message can't be deleted" in str(error):
Log.warning(
f"Auth模块删除消息 chat_id[{job.chat_id}] message_id[{job.data}]失败 消息无法删除 可能是没有授权")
else:
Log.error(f"Auth模块删除消息 chat_id[{job.chat_id}] message_id[{job.data}]失败", error)
@staticmethod
async def restore_member(context: CallbackContext, chat_id: int, user_id: int):
Log.debug(f"重置用户权限 user_id[{user_id}] 在 chat_id[{chat_id}]")
try:
await context.bot.restrict_chat_member(chat_id=chat_id, user_id=user_id, permissions=FullChatPermissions)
except BadRequest as error:
Log.error(f"Auth模块在 chat_id[{chat_id}] user_id[{user_id}] 执行restore失败", error)
async def admin(self, update: Update, context: CallbackContext) -> None:
async def admin_callback(callback_query_data: str) -> Tuple[str, int]:
_data = callback_query_data.split("|")
_result = _data[1]
_user_id = int(_data[2])
Log.debug(f"admin_callback函数返回 result[{_result}] user_id[{_user_id}]")
return _result, _user_id
callback_query = update.callback_query
user = callback_query.from_user
message = callback_query.message
chat = message.chat
Log.info(f"用户 {user.full_name}[{user.id}] 在群 {chat.title}[{chat.id}] 点击Auth管理员命令")
chat_administrators = await self.get_chat_administrators(context, chat_id=chat.id)
if not self.is_admin(chat_administrators, user.id):
Log.debug(f"用户 {user.full_name}[{user.id}] 在群 {chat.title}[{chat.id}] 非群管理")
await callback_query.answer(text="你不是管理!\n"
"再乱点我叫西风骑士团、千岩军和天领奉行了!", show_alert=True)
return
result, user_id = await admin_callback(callback_query.data)
try:
member_info = await context.bot.get_chat_member(chat.id, user_id)
except BadRequest as error:
Log.warning(f"获取用户 {user_id} 在群 {chat.title}[{chat.id}] 信息失败 \n", error)
user_info = f"{user_id}"
else:
user_info = member_info.user.mention_markdown_v2()
if result == "pass":
await callback_query.answer(text="放行", show_alert=False)
await self.restore_member(context, chat.id, user_id)
if schedule := context.job_queue.scheduler.get_job(f"{chat.id}|{user_id}|clean_join"):
schedule.remove()
await message.edit_text(f"{user_info}{user.mention_markdown_v2()} 放行",
parse_mode=ParseMode.MARKDOWN_V2)
Log.info(f"用户 user_id[{user_id}] 在群 {chat.title}[{chat.id}] 被管理放行")
elif result == "kick":
await callback_query.answer(text="驱离", show_alert=False)
await context.bot.ban_chat_member(chat.id, user_id)
await message.edit_text(f"{user_info}{user.mention_markdown_v2()} 驱离",
parse_mode=ParseMode.MARKDOWN_V2)
Log.info(f"用户 user_id[{user_id}] 在群 {chat.title}[{chat.id}] 被管理踢出")
elif result == "unban":
await callback_query.answer(text="解除驱离", show_alert=False)
await self.restore_member(context, chat.id, user_id)
if schedule := context.job_queue.scheduler.get_job(f"{chat.id}|{user_id}|clean_join"):
schedule.remove()
await message.edit_text(f"{user_info}{user.mention_markdown_v2()} 解除驱离",
parse_mode=ParseMode.MARKDOWN_V2)
Log.info(f"用户 user_id[{user_id}] 在群 {chat.title}[{chat.id}] 被管理解除封禁")
else:
Log.warning(f"auth 模块 admin 函数 发现未知命令 result[{result}]")
await context.bot.send_message(chat.id, "派蒙这边收到了错误的消息!请检查详细日记!")
if schedule := context.job_queue.scheduler.get_job(f"{chat.id}|{user_id}|auth_kick"):
schedule.remove()
async def query(self, update: Update, context: CallbackContext) -> None:
async def query_callback(callback_query_data: str) -> Tuple[int, bool, str, str]:
_data = callback_query_data.split("|")
_user_id = int(_data[1])
_question_id = int(_data[2])
_answer_id = int(_data[3])
_answer = await self.quiz_service.get_answer(_answer_id)
_question = await self.quiz_service.get_question(_question_id)
_result = _answer.is_correct
_answer_encode = _answer.text
_question_encode = _question.text
Log.debug(f"query_callback函数返回 user_id[{_user_id}] result[{_result}] \n"
f"question_encode[{_question_encode}] answer_encode[{_answer_encode}]")
return _user_id, _result, _question_encode, _answer_encode
callback_query = update.callback_query
user = callback_query.from_user
message = callback_query.message
chat = message.chat
user_id, result, question, answer = await query_callback(callback_query.data)
Log.info(f"用户 {user.full_name}[{user.id}] 在群 {chat.title}[{chat.id}] 点击Auth认证命令 ")
if user.id != user_id:
await callback_query.answer(text="这不是你的验证!\n"
"再乱点再按我叫西风骑士团、千岩军和天领奉行了!", show_alert=True)
return
Log.info(f"用户 {user.full_name}[{user.id}] 在群 {chat.title}[{chat.id}] 认证结果为 {'通过' if result else '失败'}")
if result:
buttons = [[InlineKeyboardButton("驱离", callback_data=f"auth_admin|kick|{user.id}")]]
await callback_query.answer(text="验证成功", show_alert=False)
await self.restore_member(context, chat.id, user_id)
if schedule := context.job_queue.scheduler.get_job(f"{chat.id}|{user.id}|clean_join"):
schedule.remove()
text = f"{user.mention_markdown_v2()} 验证成功,向着星辰与深渊!\n" \
f"问题:{escape_markdown(question, version=2)} \n" \
f"回答:{escape_markdown(answer, version=2)}"
Log.info(f"用户 user_id[{user_id}] 在群 {chat.title}[{chat.id}] 验证成功")
else:
buttons = [[InlineKeyboardButton("驱离", callback_data=f"auth_admin|kick|{user.id}"),
InlineKeyboardButton("撤回驱离", callback_data=f"auth_admin|unban|{user.id}")]]
await callback_query.answer(text=f"验证失败,请在 {self.time_out} 秒后重试", show_alert=True)
await context.bot.ban_chat_member(chat_id=chat.id, user_id=user_id,
until_date=int(time.time()) + self.kick_time)
text = f"{user.mention_markdown_v2()} 验证失败,已经赶出提瓦特大陆!\n" \
f"问题:{escape_markdown(question, version=2)} \n" \
f"回答:{escape_markdown(answer, version=2)}"
Log.info(f"用户 user_id[{user_id}] 在群 {chat.title}[{chat.id}] 验证失败")
try:
await message.edit_text(text, reply_markup=InlineKeyboardMarkup(buttons), parse_mode=ParseMode.MARKDOWN_V2)
except BadRequest as exc:
if 'are exactly the same as ' in str(exc):
Log.warning("编辑消息发生异常,可能为用户点按多次键盘导致")
else:
raise exc
if schedule := context.job_queue.scheduler.get_job(f"{chat.id}|{user.id}|auth_kick"):
schedule.remove()
async def new_mem(self, update: Update, context: CallbackContext) -> None:
await self.refresh_quiz()
message = update.message
chat = message.chat
for user in message.new_chat_members:
if user.id == context.bot.id:
return
Log.info(f"用户 {user.full_name}[{user.id}] 尝试加入群 {chat.title}[{chat.id}]")
not_enough_rights = context.chat_data.get("not_enough_rights", False)
if not_enough_rights:
return
chat_administrators = await self.get_chat_administrators(context, chat_id=chat.id)
if self.is_admin(chat_administrators, message.from_user.id):
await message.reply_text("派蒙检测到管理员邀请,自动放行了!")
return
for user in message.new_chat_members:
if user.is_bot:
continue
question_id_list = await self.quiz_service.get_question_id_list()
if len(question_id_list) == 0:
await message.reply_text("旅行者!!!派蒙的问题清单你还没给我!!快去私聊我给我问题!")
return
try:
await context.bot.restrict_chat_member(chat_id=message.chat.id, user_id=user.id,
permissions=ChatPermissions(can_send_messages=False))
except BadRequest as err:
if "Not enough rights" in str(err):
Log.warning(f"权限不够 chat_id[{message.chat_id}]")
# reply_message = await message.reply_markdown_v2(f"派蒙无法修改 {user.mention_markdown_v2()} 的权限!"
# f"请检查是否给派蒙授权管理了")
context.chat_data["not_enough_rights"] = True
# await context.bot.delete_message(chat.id, reply_message.message_id)
return
else:
raise err
index = self.random.random(0, len(question_id_list))
question = await self.quiz_service.get_question(question_id_list[index])
buttons = [
[
InlineKeyboardButton(
answer.text,
callback_data=f"auth_challenge|{user.id}|{question['question_id']}|{answer['answer_id']}",
)
]
for answer in question.answers
]
random.shuffle(buttons)
buttons.append(
[
InlineKeyboardButton(
"放行",
callback_data=f"auth_admin|pass|{user.id}",
),
InlineKeyboardButton(
"驱离",
callback_data=f"auth_admin|kick|{user.id}",
),
]
)
reply_message = f"*欢迎来到「提瓦特」世界!* \n" \
f"问题: {escape_markdown(question.text, version=2)} \n" \
f"请在 {self.time_out}S 内回答问题"
Log.debug(f"发送入群验证问题 question_id[{question.question_id}] question[{question.text}] \n"
f"{user.full_name}[{user.id}] 在 {chat.title}[{chat.id}]")
try:
question_message = await message.reply_markdown_v2(reply_message,
reply_markup=InlineKeyboardMarkup(buttons))
except BadRequest as error:
await message.reply_text("派蒙分心了一下,不小心忘记你了,你只能先退出群再重新进来吧。")
raise error
context.job_queue.run_once(callback=self.kick_member_job, when=self.time_out,
name=f"{chat.id}|{user.id}|auth_kick", chat_id=chat.id, user_id=user.id,
job_kwargs={"replace_existing": True})
context.job_queue.run_once(callback=self.clean_message_job, when=self.time_out, data=message.message_id,
name=f"{chat.id}|{user.id}|auth_clean_join_message",
chat_id=chat.id, user_id=user.id,
job_kwargs={"replace_existing": True})
context.job_queue.run_once(callback=self.clean_message_job, when=self.time_out,
data=question_message.message_id,
name=f"{chat.id}|{user.id}|auth_clean_question_message",
chat_id=chat.id, user_id=user.id,
job_kwargs={"replace_existing": True})

View File

@ -3,8 +3,10 @@ from typing import Optional
from telegram.ext import CommandHandler, MessageHandler, filters, CallbackQueryHandler, Application, InlineQueryHandler from telegram.ext import CommandHandler, MessageHandler, filters, CallbackQueryHandler, Application, InlineQueryHandler
from config import config
from logger import Log from logger import Log
from plugins.base import NewChatMembersHandler from plugins.base import NewChatMembersHandler
from plugins.system.auth import GroupJoiningVerification
from plugins.system.errorhandler import error_handler from plugins.system.errorhandler import error_handler
from plugins.system.inline import Inline from plugins.system.inline import Inline
from plugins.system.start import start, ping, reply_keyboard_remove, unknown_command from plugins.system.start import start, ping, reply_keyboard_remove, unknown_command
@ -32,10 +34,12 @@ def register_plugin_handlers(application: Application):
# 初始化 # 初始化
Log.info("正在加载动态插件管理器") Log.info("正在加载动态插件管理器")
plugins_manager = PluginsManager() plugins_manager = PluginsManager()
plugins_manager.add_exclude(["start", "auth", "inline", "errorhandler"]) # 忽略内置模块 plugins_manager.add_exclude(["start", "auth", "inline", "errorhandler"]) # 忽略内置模块
Log.info("正在加载插件") Log.info("正在加载插件")
plugins_manager.refresh_list("plugins/genshin/*") plugins_manager.refresh_list("plugins/genshin/*")
plugins_manager.refresh_list("plugins/system/*") plugins_manager.refresh_list("plugins/system/*")
Log.info("加载插件管理器正在加载插件") Log.info("加载插件管理器正在加载插件")
plugins_manager.import_module() plugins_manager.import_module()
plugins_manager.add_handler(application) plugins_manager.add_handler(application)
@ -44,6 +48,13 @@ def register_plugin_handlers(application: Application):
inline = Inline() inline = Inline()
new_chat_members_handler = NewChatMembersHandler() new_chat_members_handler = NewChatMembersHandler()
if len(config.joining_verification_groups) >= 1:
auth = GroupJoiningVerification()
for chat_id in config.joining_verification_groups:
new_chat_members_handler.add_callback(auth.new_mem, chat_id)
add_handler(auth.query, query=r"^auth_challenge\|")
add_handler(auth.admin, query=r"^auth_admin\|")
add_handler(start, command="start") add_handler(start, command="start")
add_handler(ping, command="ping") add_handler(ping, command="ping")
# 调试功能 # 调试功能
@ -55,6 +66,6 @@ def register_plugin_handlers(application: Application):
application.add_handler(MessageHandler(filters.COMMAND & filters.ChatType.PRIVATE, unknown_command)) application.add_handler(MessageHandler(filters.COMMAND & filters.ChatType.PRIVATE, unknown_command))
application.add_error_handler(error_handler, block=False) application.add_error_handler(error_handler, block=False)
import_module(f"plugins.system.admin") import_module("plugins.system.admin")
Log.info("插件加载成功") Log.info("插件加载成功")