From 5c999e0949b22ebd2138b4b0df3007f6d760b2be Mon Sep 17 00:00:00 2001
From: Sam <100821827+01101sam@users.noreply.github.com>
Date: Wed, 6 Jul 2022 03:28:39 +0800
Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20PMCaptcha=20-=20v2.2=20(#44)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
---
list.json | 6 +-
pmcaptcha/main.py | 975 ++++++++++++----------------------------------
2 files changed, 249 insertions(+), 732 deletions(-)
diff --git a/list.json b/list.json
index ba6c803..0baab32 100644
--- a/list.json
+++ b/list.json
@@ -72,13 +72,13 @@
},
{
"name": "pmcaptcha",
- "version": "2.1919810",
+ "version": "2.2",
"section": "chat",
"maintainer": "cloudreflection,01101sam",
- "size": "20 kb",
+ "size": "2.1k line",
"supported": true,
"des-short": "私聊人机验证插件",
- "des": "私聊人机验证,支持自定义关键词黑白名单,通过后欢迎语,验证超时时间,禁止陌生人私聊,对premium特殊操作"
+ "des": "私聊人机验证,助您远离广告烦恼"
},
{
"name": "bc",
diff --git a/pmcaptcha/main.py b/pmcaptcha/main.py
index 4f273ac..dbeeca8 100644
--- a/pmcaptcha/main.py
+++ b/pmcaptcha/main.py
@@ -1,19 +1,16 @@
# -*- coding: utf-8 -*-
-"""PMCaptcha v2 - A PagerMaid-Pyro plugin
+"""PMCaptcha v2 - An plugin a day keeps the ads away
v1 by xtaodata and cloudreflection
v2 by Sam
"""
-import re
-import time
-import html
-import asyncio
-import inspect
-import traceback
+import re, gc, time, html, asyncio, inspect, traceback
from dataclasses import dataclass, field
+from random import randint
from typing import Optional, Callable, Union, List, Any, Dict, Coroutine
-from pyrogram.errors import FloodWait, AutoarchiveNotAvailable, ChannelsAdminPublicTooMuch
+from pyrogram.errors import (FloodWait, AutoarchiveNotAvailable, ChannelsAdminPublicTooMuch,
+ BotResponseTimeout, PeerIdInvalid)
from pyrogram.raw.functions.channels import UpdateUsername
from pyrogram.raw.types import GlobalPrivacySettings
from pyrogram.raw.functions.account import SetGlobalPrivacySettings, GetGlobalPrivacySettings
@@ -31,14 +28,14 @@ from pagermaid.single_utils import sqlite
cmd_name = "pmcaptcha"
-log_collect_bot = "CloudreflectionPmcaptchabot"
+log_collect_bot = "PagerMaid_Sam_Bot"
img_captcha_bot = "PagerMaid_Sam_Bot"
# Get alias for user command
user_cmd_name = alias_command(cmd_name)
-def sort_line_number(m):
+def _sort_line_number(m):
try:
func = getattr(m[1], "__func__", m[1])
return func.__code__.co_firstlineno
@@ -57,7 +54,7 @@ def get_version():
from json import load
with open(f"{working_dir}{sep}plugins{sep}version.json", 'r', encoding="utf-8") as f:
version_json = load(f)
- return version_json.get(cmd_name, lang('unknown_version'))
+ return version_json.get(cmd_name, "unknown")
# region Text Formatting
@@ -87,595 +84,37 @@ def str_timestamp(unix_ts: int) -> str:
# endregion
-lang_dict = {
- # region General
- "settings_lists": [
- "All Settings (Please refer to docs to learn more about):\n%s",
- "所有设置(具体数值含义请自行查看文档):\n%s"
- ],
- "cmd_err_run": [
- f"Error occurred when running command: {code('%s')}: {code('%s')}\n{code('%s')}",
- f"运行指令 {code('%s')} 时发生错误: {code('%s')}\n{code('%s')}",
- ],
- "no_cmd_given": [
- "Please use this command in private chat, or add parameters to execute.",
- "请在私聊时使用此命令,或添加参数执行。"
- ],
- "invalid_user_id": [
- "Invalid User ID",
- "未知用户或无效的用户 ID"
- ],
- "invalid_param": [
- "Invalid Parameter",
- "无效的参数"
- ],
- "enabled": [
- "Enabled",
- "开启"
- ],
- "disabled": [
- "Disabled",
- "关闭"
- ],
- "none": [
- "None",
- "无"
- ],
- "tip_edit": [
- f"You can edit this by using {code('%s')}",
- f"如需编辑,请使用 {code('%s')}"
- ],
- "tip_run_in_pm": [
- "You can only run this command in private chat, or by adding parameters.",
- "请在私聊使用此命令,或添加参数执行。"
- ],
- # endregion
- # region Plugin
- "plugin_desc": [
- "Captcha for PM",
- "私聊人机验证插件"
- ],
- "check_usage": [
- "Please use %s to see available commands.",
- "请使用 %s 查看可用命令"
- ],
- "curr_version": [
- f"Current {code('PMCaptcha')} Version: %s",
- f"{code('PMCaptcha')} 当前版本:%s"
- ],
- "unknown_version": [
- italic("Unknown"),
- italic("未知")
- ],
- # endregion
+def get_lang_list(): # Yes, blocking
+ from httpx import Client
+ endpoint = f"https://raw.githubusercontent.com/TeamPGM/PMCaptcha-i18n/main/v{get_version()}.py"
+ for _ in range(3):
+ try:
+ with Client() as client:
+ response = client.get(endpoint)
+ if response.status_code == 200:
+ return eval(f"lambda: {response.text}")()
+ except Exception as e:
+ console.error(f"Failed to get language file: {e}\n{traceback.format_exc()}")
+ exit(1)
- # region Vocabs
- "vocab_msg": [
- "Message",
- "消息"
- ],
- "vocab_array": [
- "List",
- "列表"
- ],
- "vocab_bool": [
- "Boolean",
- "y / n"
- ],
- "vocab_int": [
- "Integer",
- "整数"
- ],
- "vocab_cmd": [
- "Command",
- "指令"
- ],
- "vocab_rule": [
- "Rule",
- "规则"
- ],
- "vocab_action": [
- "Action",
- "操作"
- ],
- # endregion
- # region Captcha Challenge
- "verify_verified": [
- "Verified user",
- "已验证用户"
- ],
- "verify_unverified": [
- "Unverified user",
- "未验证用户"
- ],
- "verify_blocked": [
- "You were blocked.",
- "您已被封禁"
- ],
- "verify_log_punished": [
- "User %s has been %s.",
- "已对用户 %s 执行`%s`操作"
- ],
- "verify_challenge": [
- "Please answer this question to prove you are human (1 chance)",
- "请回答这个问题证明您不是机器人 (一次机会)"
- ],
- "verify_challenge_timed": [
- "You have %i seconds.",
- "您有 %i 秒来回答这个问题"
- ],
- "verify_passed": [
- "Verification passed.",
- "验证通过"
- ],
- "verify_failed": [
- "Verification failed.",
- "验证失败"
- ],
- # Sticker
- "verify_send_sticker": [
- "Please send a sticker to me.",
- "请发送一个贴纸给我"
- ],
- # endregion
-
- # region Help
- "cmd_param": [
- "Parameter",
- "参数"
- ],
- "cmd_param_optional": [
- "Optional",
- "可选"
- ],
- "cmd_alias": [
- "Alias",
- "别名/快捷命令"
- ],
- "cmd_detail": [
- f"Do {code(f',{user_cmd_name} h ')}[command ] for details",
- f"详细指令请输入 {code(f',{user_cmd_name} h ')}[指令名称 ]",
- ],
- "cmd_not_found": [
- "Command Not Found",
- "指令不存在"
- ],
- "cmd_list": [
- "Command List",
- "指令列表"
- ],
- "priority": [
- "Priority",
- "优先级"
- ],
- "cmd_search_result": [
- f"Search Result for `%s`",
- f"`%s` 的搜索结果"
- ],
- "cmd_search_docs": [
- "Documentation",
- "文档"
- ],
- "cmd_search_cmds": [
- "Commands",
- "指令"
- ],
- "cmd_search_none": [
- "No result found.",
- "未找到结果"
- ],
- # endregion
-
- # region Check
- "user_verified": [
- f"User {code('%i')} {italic('verified')}",
- f"用户 {code('%i')} {italic('已验证')}"
- ],
- "user_unverified": [
- f"User {code('%i')} {bold('unverified')}",
- f"用户 {code('%i')} {bold('未验证')}"
- ],
- # endregion
-
- # region Add / Delete
- "add_whitelist_success": [
- f"User {code('%i')} added to whitelist",
- f"用户 {code('%i')} 已添加到白名单"
- ],
- "add_whitelist_failed": [
- f"Failed to add iser {code('%i')} to whitelist",
- f"无法添加用户 {code('%i')} 到白名单"
- ],
- "remove_verify_log_success": [
- f"Removed User {code('%i')}'s verify record",
- f"已删除用户 {code('%i')} 的验证记录"
- ],
- "remove_verify_log_failed": [
- f"Failed to remove User {code('%i')}'s verify record.",
- f"删除用户 {code('%i')} 的验证记录失败"
- ],
- "remove_verify_log_not_found": [
- f"Verify record not found for User {code('%i')}",
- f"未找到用户 {code('%i')} 的验证记录"
- ],
- # endregion
-
- # region Unstuck
- "unstuck_success": [
- f"User {code('%i')} has removed from challenge mode",
- f"用户 {code('%i')} 已解除验证状态"
- ],
- "not_stuck": [
- f"User {code('%i')} is not stuck",
- f"用户 {code('%i')} 未在验证状态"
- ],
- # endregion
-
- # region Welcome
- "welcome_curr_rule": [
- "Current welcome rule",
- "当前验证通过时消息规则"
- ],
- "welcome_set": [
- "Welcome message set.",
- "已设置验证通过消息"
- ],
- "welcome_reset": [
- "Welcome message reset.",
- "已重置验证通过消息"
- ],
- # endregion
-
- # region Whitelist
- "whitelist_curr_rule": [
- "Current whitelist rule",
- "当前白名单规则"
- ],
- "whitelist_set": [
- "Keywords whitelist set.",
- "已设置关键词白名单"
- ],
- "whitelist_reset": [
- "Keywords whitelist reset.",
- "已重置关键词白名单"
- ],
- # endregion
-
- # region Blacklist
- "blacklist_curr_rule": [
- "Current blacklist rule",
- "当前黑名单规则"
- ],
- "blacklist_set": [
- "Keywords blacklist set.",
- "已设置关键词黑名单"
- ],
- "blacklist_reset": [
- "Keywords blacklist reset.",
- "已重置关键词黑名单"
- ],
- "blacklist_triggered": [
- "Blacklist rule triggered",
- "您触发了黑名单规则"
- ],
- # endregion
-
- # region Timeout
- "timeout_curr_rule": [
- "Current timeout: %i second(s)",
- "当前超时时间: %i 秒"
- ],
- "timeout_set": [
- "Verification timeout has been set to %i seconds.",
- "已设置验证超时时间为 %i 秒"
- ],
- "timeout_off": [
- "Verification timeout disabled.",
- "已关闭验证超时时间"
- ],
- "timeout_exceeded": [
- "Verification timeout.",
- "验证超时"
- ],
- # endregion
-
- # region Disable PM
- "disable_pm_curr_rule": [
- "Current disable PM status: %s",
- "当前禁止私聊状态: 已%s"
- ],
- "disable_pm_tip_exception": [
- "This feature will automatically allow contents and whitelist users.",
- "此功能会自动放行联系人与白名单用户"
- ],
- "disable_set": [
- f"Disable private chat has been set to {bold('%s')}.",
- f"已设置禁止私聊为{bold('%s')}"
- ],
- "disable_pm_enabled": [
- "Owner has private chat disabled.",
- "对方已禁止私聊。"
- ],
- # endregion
-
- # region Stats
- "stats_display": [
- "has verified %i users in total.\nSuccess: %i\nBlocked: %i",
- "已进行验证 %i 次\n验证通过: %i 次\n拦截: %i 次"
- ],
- "stats_reset": [
- "Statistics has been reset.",
- "已重置统计"
- ],
- # endregion
-
- # region Action
- "action_curr_rule": [
- "Current action rule",
- "当前验证失败规则"
- ],
- "action_set": [
- f"Action has been set to {bold('%s')}.",
- f"验证失败后将执行{bold('%s')}操作"
- ],
- "action_set_none": [
- "Action has been set to none.",
- "验证失败后将不执行任何操作"
- ],
- "action_ban": [
- "Ban",
- "封禁"
- ],
- "action_delete": [
- "Ban and delete",
- "封禁并删除对话"
- ],
- "action_archive": [
- "Ban and archive",
- "封禁并归档"
- ],
- # endregion
-
- # region Report
- "report_curr_rule": [
- "Current report state: %s",
- "当前举报状态为: %s"
- ],
- "report_set": [
- f"Report has been set to {bold('%s')}.",
- f"已设置举报状态为{bold('%s')}"
- ],
- # endregion
-
- # region Premium
- "premium_curr_rule": [
- "Current premium user rule",
- "当前 Premium 用户规则"
- ],
- "premium_set_allow": [
- f"Telegram Premium users will be allowed to {bold('bypass')} the captcha.",
- f"将{bold('不对')} Telegram Premium 用户{bold('发起验证')}"
- ],
- "premium_set_ban": [
- f"Telegram Premium users will be {bold('banned')} from private chat.",
- f"将{bold('禁止')} Telegram Premium 用户私聊"
- ],
- "premium_set_only": [
- f"{bold('Only allowed')} Telegram Premium users to private chat.",
- f"将{bold('仅允许')} Telegram Premium 用户私聊"
- ],
- "premium_set_none": [
- "Nothing will do to Telegram Premium",
- "将不对 Telegram Premium 用户执行额外操作"
- ],
- "premium_only": [
- "Owner only allows Telegram Premium users to private chat.",
- "对方只允许 Telegram Premium 用户私聊"
- ],
- "premium_ban": [
- "Owner bans Telegram Premium users from private chat.",
- "对方禁止 Telegram Premium 用户私聊"
- ],
- # endregion
-
- # region Groups In Common
- "groups_in_common_curr_rule": [
- "Current groups in common rule",
- "当前共同群规则"
- ],
- "groups_in_common_set": [
- f"Groups in common larger than {bold('%i')} will be whitelisted.",
- f"共同群数量大于 {bold('%i')} 时将自动添加到白名单"
- ],
- "groups_in_common_disabled": [
- "Group in command is not enabled",
- "未开启共同群数量检测"
- ],
- "groups_in_common_disable": [
- "Groups in common disabled.",
- "已关闭共同群检查"
- ],
- # endregion
-
- # region Chat History
- "chat_history_curr_rule": [
- f"Chat history equal or larger than {bold('%i')} will be whitelisted.",
- f"聊天记录数量大于 {bold('%i')} 时将自动添加到白名单"
- ],
- "chat_history_disabled": [
- "Chat history check is not enabled",
- "未开启聊天记录数量检测"
- ],
- # endregion
-
- # region Initiative
- "initiative_curr_rule": [
- "Current initiative status: %s",
- "当前对主动进行对话的用户添加白名单状态为: %s"
- ],
- "initiative_set": [
- f"Initiative has been set to {bold('%s')}.",
- f"已设置对主动进行对话的用户添加白名单状态为{bold('%s')}"
- ],
- # endregion
-
- # region Silent
- "silent_curr_rule": [
- "Current silent status: %s",
- "当前静音状态: 已%s"
- ],
- "silent_set": [
- f"Silent has been set to {bold('%s')}.",
- f"已设置静音模式为{bold('%s')}"
- ],
- # endregion
-
- # region Flood
- "flood_curr_rule": [
- "Current flood detect limit was set to %i user(s)",
- "当前轰炸人数已设置为 %i 人"
- ],
- # Username
- "flood_username_curr_rule": [
- "Current flood username option was set to %s",
- "当前轰炸时切换用户名选项已设置为 %s"
- ],
- "flood_username_set_confirm": [
- (f"The feature may lose your username, are you sure you want to enable this feature?\n"
- f"Please enter {code(f',{cmd_name} flood_username y')} again to confirm."),
- f"此功能有可能会导致您的用户名丢失,您是否确定要开启此功能?\n请再次输入 {code(f',{cmd_name} flood_username y')} 来确认"
- ],
- "flood_username_set": [
- f"Change username in flood preiod has been %s.",
- f"轰炸时切换用户名已%s"
- ],
- "flood_channel_desc": [
- ("This channel is a placeholder of username, which the owner is being flooded.\n"
- "Please content him later after this channel is gone."),
- "这是一个用于临时设置用户名的频道,该群主正在被私聊轰炸\n请在此频道消失后再联系他。"
- ],
- # Action
- "flood_act_curr_rule": [
- "Current flood action was set to %s",
- "当前轰炸操作已设置为 %s"
- ],
- "flood_act_set_asis": [
- f"All users in flood period will be {bold('treat as verify failed')}.",
- f"所有在轰炸期间的用户将会{bold('与验证失败的处理方式一致')}"
- ],
- "flood_act_set_captcha": [
- f"All users in flood period will be {bold('asked for captcha')}.",
- f"所有在轰炸期间的用户将会{bold('进行验证码挑战')}"
- ],
- "flood_act_set_none": [
- "Nothing will do to users in flood period.",
- "所有在轰炸期间的用户将不会被进行任何处理"
- ],
- # endregion
-
- # region Custom Rule
- "custom_rule_curr_rule": [
- "Current custom rule",
- "当前自定义规则"
- ],
- "custom_rule_set": [
- f"Custom rule has been set to\n{code('%s')}.",
- f"已设置自定义规则为\n{code('%s')}"
- ],
- "custom_rule_reset": [
- "Custom rule has been deleted.",
- "已删除自定义规则"
- ],
- "custom_rule_exec_err": [
- "Error occurred when executing custom rule",
- "执行自定义规则时发生错误"
- ],
- # endregion
-
- # region Collect Logs
- "collect_logs_curr_rule": [
- "Current collect logs status: %s",
- "当前收集日志状态: 已%s"
- ],
- "collect_logs_note": [
- ("This feature will only collect user information and chat logs of non-verifiers "
- f"via @{log_collect_bot} , and is not provided to third parties (except @LivegramBot ).\n"
- "Information collected will be used for PMCaptcha improvements, "
- "toggling this feature does not affect the use of PMCaptcha."),
- (f"此功能仅会通过 @{log_collect_bot} 收集未通过验证者的用户信息以及验证未通过的聊天记录;"
- "且不会提供给第三方(@LivegramBot 除外)。\n收集的信息将用于 PMCaptcha 改进,开启或关闭此功能不影响 PMCaptcha 的使用。")
- ],
- "collect_logs_set": [
- "Collect logs has been set to %s.",
- "已设置收集日志为 %s"
- ],
- # endregion
-
- # region Captcha Type
- "type_curr_rule": [
- "Current captcha type: %s",
- "当前验证码类型: %s"
- ],
- "type_set": [
- f"Captcha type has been set to {bold('%s')}.",
- f"已设置验证码类型为 {bold('%s')}"
- ],
- "type_param_name": [
- "Type",
- "类型"
- ],
- "type_captcha_img": [
- "Image",
- "图像辨识"
- ],
- "type_captcha_math": [
- "Math",
- "计算"
- ],
- "type_captcha_sticker": [
- "Sticker",
- "贴纸"
- ],
- # endregion
-
- # region Image Captcha Type
- "img_captcha_curr_rule": [
- "Current image captcha type: %s",
- "当前图像验证类型: %s"
- ],
- "img_captcha_type_func": [
- "funCaptcha",
- "funCaptcha",
- ],
- "img_captcha_type_github": [
- "GitHub",
- "GitHub",
- ],
- "img_captcha_type_rec": [
- "reCaptcha",
- "reCaptcha"
- ],
- "img_captcha_retry_curr_rule": [
- "Current max retry for image captcha: %s",
- "当前图像验证码最大重试次数: %s"
- ],
- "img_captcha_retry_set": [
- "Max retry for image captcha has been set to %s.",
- "已设置图像验证码最大重试次数为 %s"
- ],
- # endregion
-}
+# Language file
+lang_dict = get_lang_list()
def lang(lang_id: str, lang_code: str = Config.LANGUAGE or "en") -> str:
lang_code = lang_code or "en"
- return lang_dict.get(lang_id)[1 if lang_code.startswith("zh") else 0]
+ return (lang_dict.get(lang_id, [f"Get lang failed[{code('lang_id')}]", f"获取语言失败[{code('lang_id')}]"])
+ [1 if lang_code.startswith("zh") else 0])
def lang_full(lang_id: str, *format_args):
- return "\n".join(lang_str % format_args for lang_str in lang_dict[lang_id])
+ return "\n".join(
+ lang_str % format_args
+ for lang_str in lang_dict.get(
+ lang_id, [f"Get lang failed[{code('lang_id')}]", f"获取语言失败[{code('lang_id')}]"])
+ )
async def exec_api(coro_func: Coroutine):
@@ -683,10 +122,10 @@ async def exec_api(coro_func: Coroutine):
try:
return await coro_func
except FloodWait as e:
- console.debug(f"Flood triggered, waiting for {e.value} seconds")
+ console.debug(f"API Flood triggered, waiting for {e.value} seconds")
await asyncio.sleep(e.value)
except Exception as e:
- console.error(f"API Call Failed: {e}\n{traceback.format_exc()}")
+ console.error(f"Function Call Failed: {e}\n{traceback.format_exc()}")
return None
@@ -803,10 +242,12 @@ class Command:
# region Helpers (Formatting, User ID)
- def _generate_markdown(self):
- result = []
+ @classmethod
+ def _generate_markdown(cls):
+ self = cls(None, None) # noqa
+ result = [f"版本: v{get_version()}", ""]
members = inspect.getmembers(self, inspect.iscoroutinefunction)
- members.sort(key=sort_line_number)
+ members.sort(key=_sort_line_number)
for name, func in members:
if name.startswith("_"):
continue
@@ -873,22 +314,36 @@ class Command:
len(extras) and extras.insert(0, "")
cmd_display = code(f",{cmd_name} {self._get_cmd_with_param(subcmd_name)}".strip())
if markdown:
- result = ["",
- f"{self._get_cmd_with_param(subcmd_name) or cmd_name} · {re.search(r'(.+)', self[subcmd_name].__doc__ or '')[1].strip()}
",
- "\n>\n", f"用法:{cmd_display}", "",
- re.sub(r" {4,}", "", text).replace("{cmd_name}", cmd_name).strip().replace("\n", "\n\n"),
- "\n\n".join(extras)]
- result.extend(("", "---", " "))
+ result = [
+ "",
+ f"{self._get_cmd_with_param(subcmd_name, markdown) or code(cmd_name)} · {re.search(r'(.+)', self[subcmd_name].__doc__ or '')[1].strip()}
",
+ "\n>\n",
+ f"用法:{cmd_display}",
+ "",
+ re.sub(r" {4,}", "", text)
+ .replace("{cmd_name}", cmd_name)
+ .strip()
+ .replace("\n", "\n\n"),
+ "\n\n".join(extras),
+ "",
+ "---",
+ " ",
+ ]
+
return "\n".join(result)
return "\n".join([cmd_display, re.sub(r" {4,}", "", text).replace("{cmd_name}", cmd_name).strip()] + extras)
- def _get_cmd_with_param(self, subcmd_name: str) -> str:
+ def _get_cmd_with_param(self, subcmd_name: str, markdown: bool = False) -> str:
if subcmd_name == cmd_name:
return ""
msg = subcmd_name
if result := re.search(self.param_rgx, getattr(self, msg).__doc__ or ''):
+ if markdown:
+ msg = f"{msg}
"
param = result[2].lstrip("_")
msg += f" [{param}]" if result[1] else html.escape(f" <{param}>")
+ elif markdown:
+ msg = f"{msg}
"
return msg
def _get_mapped_alias(self, alias_name: str, ret_type: str):
@@ -972,7 +427,7 @@ class Command:
await self.msg.safe_delete()
async def version(self):
- """查看 PMCaptcha 当前版本
+ """查看 PMCaptcha
当前版本
:alias: v, ver
"""
@@ -1036,7 +491,7 @@ class Command:
self._edit(self._extract_docs(func.__name__, func.__doc__ or ''))
if func else self._edit(f"{lang('cmd_not_found')}: {code(command)}"))
members = inspect.getmembers(self, inspect.iscoroutinefunction)
- members.sort(key=sort_line_number)
+ members.sort(key=_sort_line_number)
for name, func in members:
if name.startswith("_"):
continue
@@ -1048,7 +503,7 @@ class Command:
# region Checking User / Manual update
async def check(self, _id: Optional[str]):
- """查询指定用户验证状态,如未指定为当前私聊用户 ID
+ """查询指定用户验证状态,对该信息回复或者输入用户 ID,如未指定为当前私聊用户 ID
:param opt _id: 用户 ID
"""
@@ -1057,7 +512,7 @@ class Command:
await self._edit(lang(f"user_{'' if setting.is_verified(user_id) else 'un'}verified") % user_id)
async def add(self, _id: Optional[str]):
- """将 ID 加入已验证,如未指定为当前私聊用户 ID
+ """将 ID 加入已验证,对该信息回复或者输入用户 ID,如未指定为当前私聊用户 ID
:param opt _id: 用户 ID
"""
@@ -1074,7 +529,7 @@ class Command:
await self._edit(lang(f"add_whitelist_{'success' if result else 'failed'}") % user_id)
async def delete(self, _id: Optional[str]):
- """移除 ID 验证记录,如未指定为当前私聊用户 ID
+ """移除 ID 验证记录,对该信息回复或者输入用户 ID,如未指定为当前私聊用户 ID
:param opt _id: 用户 ID
:alias: del
@@ -1088,6 +543,7 @@ class Command:
async def unstuck(self, _id: Optional[str]):
"""解除一个用户的验证状态,通常用于解除卡死的验证状态
+ 使用:对该信息回复或者输入用户 ID,如未指定为当前私聊用户 ID
:param opt _id: 用户 ID
"""
@@ -1188,7 +644,7 @@ class Command:
await self._edit(lang('timeout_set') % seconds)
async def disable_pm(self, toggle: Optional[str]):
- """启用 / 禁止陌生人私聊,默认为 N
(允许私聊)
+ """启用 / 禁止陌生人私聊,默认为 关闭
(允许私聊)
此功能会放行联系人和白名单(已通过验证)用户
您可以使用 ,{cmd_name} add
将用户加入白名单
@@ -1209,15 +665,22 @@ class Command:
:param opt arg: 参数 (reset)
"""
if not arg:
- data = (setting.get('pass', 0) + setting.get('banned', 0), setting.get('pass', 0), setting.get('banned', 0))
- await self.msg.edit_text(f"{code('PMCaptcha')} {lang('stats_display') % data}", parse_mode=ParseMode.HTML)
+ data = (setting.get('pass', 0) + setting.get('banned', 0),
+ setting.get('pass', 0), setting.get('banned', 0),
+ len(curr_captcha) + len(setting.pending_challenge_list.get_subs()),
+ len(setting.pending_ban_list.get_subs()),
+ setting.get('flooded', 0))
+ text = f"{code('PMCaptcha')} {lang('stats_display') % data}"
+ if the_world_eye.triggered:
+ text += f"\n\n{lang('stats_flooding') % len(the_world_eye.user_ids)}"
+ await self.msg.edit_text(text, parse_mode=ParseMode.HTML)
return
if arg.startswith("-c"):
- setting.delete('pass').delete('banned')
+ setting.delete('pass').delete('banned').delete('flooded')
return await self.msg.edit(lang('stats_reset'), parse_mode=ParseMode.HTML)
async def action(self, action: Optional[str]):
- """选择验证失败的处理方式,默认为 none
+ """选择验证失败的处理方式,默认为 封禁
处理方式如下:
- ban
| 封禁
- delete
| 封禁并删除对话
@@ -1227,7 +690,7 @@ class Command:
:alias: act
"""
if not action:
- action = setting.get("action", "none")
+ action = setting.get("action", "ban")
return await self._display_value(
key="action",
display_text=lang(f"action_{action == 'none' and 'set_none' or action}"),
@@ -1235,12 +698,13 @@ class Command:
value_type="vocab_action")
if action not in ("ban", "delete", "none"):
return await self.help("act")
- if (action == "none" and setting.delete("action") or setting.set("action", action)) == action:
+ if (action == "ban" and setting.delete("action") or setting.set("action", action)) == action:
+ if action == "none":
+ return await self._edit(lang('action_set_none'))
return await self._edit(lang('action_set') % lang(f'action_{action}'))
- await self._edit(lang('action_set_none'))
async def report(self, toggle: Optional[str]):
- """选择验证失败后是否举报该用户,默认为 N
+ """选择验证失败后是否举报该用户,默认为 开启
:param opt toggle: 开关 (y / n)
"""
@@ -1249,10 +713,10 @@ class Command:
display_text=lang('report_curr_rule') % lang('enabled' if setting.get('report') else 'disabled'),
sub_cmd="report",
value_type="vocab_bool")
- await self._set_toggle("report", toggle)
+ await self._set_toggle("report", toggle, reverse=True)
async def premium(self, action: Optional[str]):
- """选择对 Premium 用户的操作,默认为 none
+ """选择对 Premium 用户的操作,默认为 不执行任何操作
处理方式如下:
- allow
| 白名单
- ban
| 封禁
@@ -1301,7 +765,7 @@ class Command:
"""设置对拥有一定数量的聊天记录的用户添加白名单(触发验证的信息不计算在内)
使用 ,{cmd_name} his -1
重置设置
- 请注意,由于 Telegram 内部限制,信息获取有可能会不完整,请不要设置过大的数值
+ 请注意,由于 Telegram
内部限制,数值过大会导致程序缓慢,请不要设置过大的数值
:param opt count: 聊天记录数量
:alias: his, history
@@ -1315,11 +779,11 @@ class Command:
display_text=text,
sub_cmd="his",
value_type="vocab_bool")
- setting.set('history_count', count)
+ count < 0 and setting.delete('history_count') or setting.set('history_count', count)
await self._edit(lang('chat_history_curr_rule') % count)
async def initiative(self, toggle: Optional[str]):
- """设置对主动进行对话的用户添加白名单,默认为 N
+ """设置对主动进行对话的用户添加白名单,默认为 关闭
:param opt toggle: 开关 (y / n)
"""
@@ -1332,7 +796,7 @@ class Command:
await self._set_toggle("initiative", toggle)
async def silent(self, toggle: Optional[str]):
- """减少信息发送,默认为 N
+ """减少信息发送,默认为 关闭
开启后,封禁、验证成功提示(包括欢迎信息)信息将不会发送
(并不会影响到 log
发送)
@@ -1347,7 +811,7 @@ class Command:
await self._set_toggle("silent", toggle)
async def flood(self, limit: Optional[int]):
- """设置一分钟内超过 n
人开启轰炸检测机制,默认为 50
人
+ """设置一分钟内超过 n
人开启轰炸检测机制,默认为 5
人
此机制会在用户被轰炸时启用,持续 5
分钟,假如有用户继续进行私聊计时将会重置
当轰炸开始时,PMCaptcha
将会启动以下一系列机制
@@ -1357,7 +821,8 @@ class Command:
- (用户可选)创建临时频道,并把用户名转移到创建的频道上 【默认关闭】
轰炸结束后,如果用户名已转移到频道上,将恢复用户名,并删除频道
- 并对记录收集机器人发送轰炸的用户数量
、轰炸开始时间
、轰炸结束时间
、轰炸时长
(由于不存在隐私问题,此操作为强制性)
+ 并对记录收集机器人发送轰炸的用户数量
、轰炸开始时间
、轰炸结束时间
、轰炸时长
+ (由于不存在隐私问题,此操作为强制性)
请参阅 ,{cmd_name} h flood_username
了解更多有关创建临时频道的机制
请参阅 ,{cmd_name} h flood_act
查看轰炸结束后的处理方式
@@ -1376,7 +841,7 @@ class Command:
async def flood_username(self, toggle: Optional[str]):
"""设置是否在轰炸时启用“转移用户名到临时频道”机制(如有用户名)
将此机制分开出来的原因是此功能有可能会被抢注用户名(虽然经测试并不会出现此问题)
- 但为了万一依然分开出来作为一个选项了
+ 但为了以防万一依然分开出来作为一个选项了
启用后,在轰炸机制开启时,会进行以下操作
- 创建临时频道
@@ -1384,6 +849,7 @@ class Command:
- (如设置失败)恢复用户名,删除频道
注意:请预留足够的公开群用户名设置额度,否则将不会设置成功,但同时用户名也不会被清空
+ (操作失败虽然会有 log 提醒,但请不要过度依赖 log)
:param opt toggle: 开关 (y / n)
:alias: boom_username
@@ -1402,9 +868,10 @@ class Command:
await self._set_toggle("flood_username", toggle)
async def flood_act(self, action: Optional[str]):
- """设置轰炸结束后进行的处理方式,默认为 none
+ """设置轰炸结束后进行的处理方式,默认为 删除并举报所有轰炸的用户
可用的处理方式如下:
- - asis
| 与验证失败的处理方式一致,但不会进行验证失败通知以及发送log记录
+ - asis
| 与验证失败的处理方式一致,但不会进行验证失败通知以及发送log
记录
+ - delete
| 删除并举报所有轰炸的用户(速度最快)
- captcha
| 对每个用户进行 CAPTCHA
挑战
- none
| 不进行任何操作
@@ -1413,16 +880,16 @@ class Command:
"""
if not action:
return await self._display_value(
- display_text=lang('flood_act_curr_rule') % lang(f"flood_act_set_{setting.get('flood_act', 'none')}"),
+ display_text=lang('flood_act_curr_rule') % lang(f"flood_act_set_{setting.get('flood_act', 'delete')}"),
sub_cmd="flood_act",
value_type="vocab_action")
- if action not in ("asis", "captcha", "none"):
+ if action not in ("asis", "captcha", "none", "delete"):
return await self.help("flood_act")
action == "none" and setting.delete("flood_act") or setting.set("flood_act", action)
await self._edit(lang(f'flood_act_set_{action}'))
async def custom_rule(self, *rule: Optional[str]):
- """用户自定义过滤规则,返回True
为白名单,否则继续执行下面的规则
+ """用户自定义过滤规则,规则返回True
为白名单,否则继续执行下面的规则
使用 ,{cmd_name} custom_rule -c
可删除规则
注意事项:
@@ -1435,12 +902,14 @@ class Command:
- text
| 触发验证的信息的文本,永远不为None
- user
| 用户
- me
| 机器人用户(自己)
+ - global 数值 (例如: curr_captcha
, the_order
等)
+ - 注意,可以调用 await 函数
范例:
- text == "BYPASS"
+ text == "BYPASS"
解释:
- 当文字为“BYPASS”时,认为是白名单用户,不继续执行规则
+ 当对方发送的文字为“BYPASS”时,不继续执行规则
:param rule: 规则
"""
@@ -1459,7 +928,7 @@ class Command:
async def collect_logs(self, toggle: Optional[str]):
"""查看或设置是否允许 PMCaptcha
收集验证错误相关信息以帮助改进
- 默认为 Y
,收集的信息包括被验证者的信息以及未通过验证的信息记录
+ 默认为 开启
,收集的信息包括被验证者的信息以及未通过验证的信息记录
:param opt toggle: 开关 (y / n)
:alias: collect, log
@@ -1473,11 +942,11 @@ class Command:
await self._set_toggle("collect_logs", toggle, reverse=True)
async def change_type(self, _type: Optional[str]):
- """切换验证码类型,默认为 math
+ """切换验证码类型,默认为 计算验证
验证码类型如下:
- math
| 计算验证
- img
| 图像辨识验证
- - sticker
| 贴纸验证(发送贴纸即可)
+ - sticker
| 贴纸验证
注意:如果图像验证不能使用将回退到计算验证
@@ -1506,8 +975,8 @@ class Command:
("blacklist", text_none),
("timeout", 300 if setting.get("type") == "img" else 30),
("disable_pm", bold(lang('disabled'))),
- ("action", bold(lang("action_set_none"))),
- ("report", bold(lang('disabled'))),
+ ("action", bold(lang("action_ban"))),
+ ("report", bold(lang('enabled'))),
("premium", bold(lang('premium_set_none'))),
("groups_in_common", text_none),
("chat_history", -1),
@@ -1515,7 +984,7 @@ class Command:
("silent", bold(lang("disabled"))),
("flood", 5),
("flood_username", bold(lang("disabled"))),
- ("flood_act", bold(lang("flood_act_set_none"))),
+ ("flood_act", bold(lang("flood_act_set_delete"))),
("collect_logs", bold(lang("enabled"))),
("type", bold(lang("type_captcha_math"))),
("img_captcha", bold(lang("img_captcha_type_func"))),
@@ -1554,7 +1023,7 @@ class Command:
key = "img_max_retry"
value = setting.get(key, default)
if isinstance(value, bool):
- value = bold(lang(f'enabled' if value else 'disabled'))
+ value = bold(lang('enabled' if value else 'disabled'))
elif key == "premium":
value = "\n" + value
if lang_text.find("%") != -1:
@@ -1567,7 +1036,7 @@ class Command:
# Image Captcha
async def change_img_type(self, _type: Optional[str]):
- """切换图像辨识使用接口,默认为 func
+ """切换图像辨识使用接口,默认为 funCaptcha
目前可用的接口:
- func
(ArkLabs funCaptcha )
- github
(GitHub 螺旋星系 )
@@ -1613,7 +1082,6 @@ class TheOrder:
"""Worker of blocking user (Punishment)"""
queue = asyncio.Queue()
task: Optional[asyncio.Task] = None
- flood_text = "[The Order] Flood Triggered: wait %is, Command: %s, Target: %s"
def __post_init__(self):
if pending := setting.pending_ban_list.get_subs():
@@ -1629,17 +1097,21 @@ class TheOrder:
target = None
try:
target, skip_log = await self.queue.get()
- action = setting.get("action", "none")
+ action = setting.get("action", "ban")
if action in ("ban", "delete"):
if not await exec_api(bot.block_user(user_id=target)):
console.debug(f"Failed to block user {target}")
- if action == "delete":
- if not await exec_api(bot.invoke(messages.DeleteHistory(
+ if action == "delete" and not await exec_api(
+ bot.invoke(
+ messages.DeleteHistory(
just_clear=False,
revoke=False,
peer=await bot.resolve_peer(target),
- max_id=0))):
- console.debug(f"Failed to delete user chat {target}")
+ max_id=0,
+ )
+ )
+ ):
+ console.debug(f"Failed to delete user chat {target}")
setting.pending_ban_list.del_id(target)
setting.get_challenge_state(target) and setting.del_challenge_state(target)
setting.set("banned", setting.get("banned", 0) + 1)
@@ -1647,7 +1119,8 @@ class TheOrder:
text = f"[PMCaptcha - The Order] {lang('verify_log_punished')} (Punishment)"
(not skip_log and action not in ("none", "archive") and
await log(text % (chat_link, lang(f'action_{action}')), True))
- skip_log and console.debug(text % (chat_link, lang(f'action_{action}')))
+ (skip_log and
+ console.debug(text % (chat_link, lang(f'action_{action == "none" and "set_none" or action}'))))
except asyncio.CancelledError:
break
except Exception as e:
@@ -1678,6 +1151,8 @@ class TheWorldEye:
- sophitia -> Watcher
- synchronize -> flood_triggered
- overload -> flood_ended
+
+ Naming inspired by a anime game
"""
queue = asyncio.Queue()
watcher: Optional[asyncio.Task] = None
@@ -1709,7 +1184,7 @@ class TheWorldEye:
self.update = state.get("update")
self.user_ids = state.get("user_ids")
self.auto_archive_enabled_default = state.get("auto_archive_enabled_default")
- self.reset_timer(300 - (now - self.start))
+ self.reset_timer(360 - (now - self.start))
console.debug("PMCaptcha restarted, flood state resume")
self.watcher = asyncio.create_task(self.sophitia())
@@ -1720,12 +1195,11 @@ class TheWorldEye:
await asyncio.sleep(interval)
except asyncio.CancelledError:
return
- console.debug("Flood ends")
- self.triggered = False
+ console.debug("Flood timer ended")
self.end = int(time.time())
await self.overload()
- def reset_timer(self, interval: int = 300):
+ def reset_timer(self, interval: int = 270 + randint(30, 60)):
if self.timer_task:
self.timer_task.cancel()
self.update = int(time.time())
@@ -1832,7 +1306,7 @@ class TheWorldEye:
elif not self.last_challenge_time or now - self.last_challenge_time > 60:
self.level = 1
self.last_challenge_time = now
- if self.level >= setting.get("flood_limit", 50):
+ if self.level >= setting.get("flood_limit", 5):
console.warn(f"Flooding detected: {self.level} reached in 1 min")
self.triggered = True
self.start = self.update = now
@@ -1874,18 +1348,33 @@ class TheWorldEye:
async def overload(self):
"""Executed after flood ends (Nine has performed load action)"""
console.info(f"Flood ended, {len(self.user_ids)} users were affected, duration: {self.end - self.start}s")
+ flood_act = setting.get("flood_act", "delete")
+ user_ids, start, end, duration = self.user_ids.copy(), self.start, self.end, self.update - self.start
+ # Reset now so it can handle next flood in time
+ rule_lock.locked() and rule_lock.release()
+ await rule_lock.acquire() # Don't process rule until flood state is reset
+ self.triggered = False
+ self.user_ids.clear()
+ self.del_state()
+ self.start = self.end = self.update = None
if self.channel_id or self.username:
console.debug("Changing back username")
await self._restore_username()
try:
- await bot.send_message(log_collect_bot, "\n".join((
- f"💣 检测到私聊轰炸",
- f"设置限制: {code(setting.get('flood_limit', 50))}",
- f"用户数量: {code(str(len(self.user_ids)))}",
- f"开始时间: {code(str_timestamp(self.start))}",
- f"结束时间: {code(str_timestamp(self.end))}",
- f"轰炸时长: {code(str(self.update - self.start))} 秒",
- )))
+ await bot.send_message(
+ log_collect_bot,
+ "\n".join(
+ (
+ "💣 检测到私聊轰炸",
+ f"设置限制: {code(setting.get('flood_limit', 5))}",
+ f"用户数量: {code(str(len(user_ids)))}",
+ f"开始时间: {code(str_timestamp(start))}",
+ f"结束时间: {code(str_timestamp(end))}",
+ f"轰炸时长: {code(str(duration))} 秒",
+ )
+ ),
+ )
+
except Exception as e:
console.debug(f"Failed to send flood log: {e}\n{traceback.format_exc()}")
if not self.auto_archive_enabled_default: # Restore auto archive setting
@@ -1896,27 +1385,35 @@ class TheWorldEye:
console.debug("Auto archive disabled")
except Exception as e:
console.debug(f"Failed to disable auto archive: {e}\n{traceback.format_exc()}")
- flood_act = setting.get("flood_act")
+ rule_lock.release() # Let rule processing continue
+ setting.set("banned", setting.get("banned", 0) + len(user_ids))
+ setting.set('flooded', setting.get('flooded', 0) + 1)
+ console.debug(f"Doing flood action: {flood_act}")
if flood_act == "asis":
if not the_order.task or the_order.task.done():
the_order.task = asyncio.create_task(the_order.worker())
- for user_id in self.user_ids:
+ for user_id in user_ids:
setting.pending_ban_list.add_id(user_id)
- for user_id in self.user_ids:
+ for user_id in user_ids:
await the_order.queue.put((user_id, True))
elif flood_act == "captcha":
if not captcha_task.task or captcha_task.task.done():
captcha_task.task = asyncio.create_task(captcha_task.worker())
- for user_id in self.user_ids:
- if (setting.pending_challenge_list.check_id(user_id) or curr_captcha.get(user_id) or
- setting.get_challenge_state(user_id)):
+ for user_id in user_ids:
+ setting.pending_challenge_list.add_id(user_id)
+ for user_id in user_ids:
+ if curr_captcha.get(user_id) or setting.get_challenge_state(user_id):
continue
await self.queue.put((user_id, None, None, None))
- setting.pending_challenge_list.add_id(user_id)
console.debug(f"User {user_id} added to challenge queue")
- self.user_ids.clear()
- self.start = self.end = self.update = self.auto_archive_enabled_default = None
- self.del_state()
+ elif flood_act == "delete":
+ from pyrogram.raw.functions import messages
+ console.debug(f"Delete and reporting {len(user_ids)} users")
+ for user_id in user_ids:
+ peer = await bot.resolve_peer(user_id)
+ await exec_api(bot.invoke(messages.ReportSpam(peer=peer)))
+ await exec_api(bot.invoke(messages.DeleteHistory(just_clear=False, revoke=False, peer=peer, max_id=0)))
+ console.debug("Flood action done")
@dataclass
@@ -1928,7 +1425,6 @@ class CaptchaTask:
"""
queue = asyncio.Queue()
task: Optional[asyncio.Task] = None
- flood_text = "[CaptchaTask] Flood Triggered: wait %is, Command: %s, Target: %s"
def __post_init__(self):
if pending := setting.pending_challenge_list.get_subs():
@@ -2002,7 +1498,7 @@ class CaptchaTask:
if not (setting.pending_challenge_list.check_id(user_id) or curr_captcha.get(user_id) or
setting.get_challenge_state(user_id)):
setting.pending_challenge_list.add_id(user_id)
- await self.queue.put((user_id, msg, can_report, auto_archived))
+ self.queue.put_nowait((user_id, msg, can_report, auto_archived))
console.debug(f"User {user_id} added to challenge queue")
@@ -2020,7 +1516,7 @@ class CaptchaChallenge:
# Post Init Value
captcha_start: int = 0
captcha_end: int = 0
- challenge_msg_id: Optional[int] = None
+ challenge_msg_ids: Optional[List[int]] = field(default_factory=list)
timer_task: Optional[asyncio.Task] = None
# region Logging
@@ -2056,7 +1552,7 @@ class CaptchaChallenge:
user.language_code and caption.append(f"Language: {code(user.language_code)}")
user.dc_id and caption.append(f"DC: {code(str(user.dc_id))}")
user.phone_number and caption.append(f"Phone: {code(user.phone_number)}")
- self.type and caption.append(f"Captcha Type: {code(self.type)}")
+ self.type and caption.append(f"Type: {code(self.type)}")
self.can_report and setting.get("report") and caption.append(f"Spam Reported: {code('Yes')}")
ban_code and caption.append(f"Block Reason: {code(ban_code)}")
self.captcha_start and caption.append(f"Start: {code(str(self.captcha_start))}")
@@ -2080,7 +1576,7 @@ class CaptchaChallenge:
"type": self.type,
"start": self.captcha_start,
"logs": self.logs,
- "msg_id": self.challenge_msg_id,
+ "msg_ids": self.challenge_msg_ids,
"report": self.can_report
}
extra and data.update(extra)
@@ -2098,46 +1594,52 @@ class CaptchaChallenge:
# region Verify Result
- async def _verify_success(self):
+ async def _del_challenge_msgs(self):
+ for challenge_msg_id in self.challenge_msg_ids:
+ try:
+ await bot.delete_messages(self.user.id, challenge_msg_id)
+ except Exception as e:
+ console.error(f"Failed to delete challenge message: {e}\n{traceback.format_exc()}")
+
+ async def _verify_success(self, _):
setting.whitelist.add_id(self.user.id)
setting.set("pass", setting.get("pass", 0) + 1)
+ chat_link = gen_link(str(self.user.id), f"tg://user?id={self.user.id}")
+ await log(lang("verify_log_passed") % (chat_link, lang(f"type_captcha_{self.type}")))
success_msg = setting.get("welcome") or lang_full("verify_passed")
welcome_msg: Optional[Message] = None
if setting.get("silent"):
- try:
- self.challenge_msg_id and await bot.delete_messages(self.user.id, self.challenge_msg_id)
- except Exception as e:
- console.error(f"Failed to delete challenge message: {e}\n{traceback.format_exc()}")
+ await self._del_challenge_msgs()
return await CaptchaTask.archive(self.user.id, un_archive=True)
try:
- if self.challenge_msg_id:
- welcome_msg = await bot.edit_message_text(self.user.id, self.challenge_msg_id, success_msg)
+ self.challenge_msg_ids and await bot.edit_message_text(self.user.id, self.challenge_msg_ids[0], success_msg)
except Exception as e:
console.error(f"Failed to edit welcome message: {e}\n{traceback.format_exc()}")
try:
welcome_msg = await bot.send_message(self.user.id, success_msg)
- self.challenge_msg_id = welcome_msg.id
except Exception as e:
console.error(f"Failed to send welcome message: {e}\n{traceback.format_exc()}")
- await asyncio.sleep(3)
- welcome_msg and await welcome_msg.safe_delete()
+ await asyncio.sleep(setting.get("welcome") and 5 or 3)
+ await self._del_challenge_msgs()
+ welcome_msg and await welcome_msg.delete()
await CaptchaTask.archive(self.user.id, un_archive=True)
- async def _verify_failed(self):
+ async def _verify_failed(self, reason_code: str):
try:
- self.challenge_msg_id and await bot.delete_messages(self.user.id, self.challenge_msg_id)
+ for challenge_msg_id in self.challenge_msg_ids:
+ await bot.delete_messages(self.user.id, challenge_msg_id)
(self.can_report and setting.get("report") and
await bot.invoke(messages.ReportSpam(peer=await bot.resolve_peer(self.user.id))))
except Exception as e:
console.debug(f"Error occurred when executing verify failed function: {e}\n{traceback.format_exc()}")
await the_order.active(self.user.id, "verify_failed")
- await self.send_log("verify_failed")
+ await self.send_log(reason_code)
- async def action(self, success: bool):
+ async def action(self, success: bool, reason_code="verify_failed"):
self.captcha_end = int(time.time())
self.del_state()
self.remove_timer()
- await getattr(self, f"_verify_{'success' if success else 'failed'}")()
+ await getattr(self, f"_verify_{'success' if success else 'failed'}")(reason_code)
console.debug(f"User {self.user.id} verify {'success' if success else 'failed'}")
# endregion
@@ -2153,7 +1655,7 @@ class CaptchaChallenge:
console.error(f"Error occurred when running challenge timer: {e}\n{traceback.format_exc()}")
async with self.captcha_write_lock:
console.debug(f"User {self.user.id} verification timed out")
- await self.action(False)
+ await self.action(False, "verify_timeout")
if curr_captcha.get(self.user.id):
del curr_captcha[self.user.id]
@@ -2192,7 +1694,7 @@ class MathChallenge(CaptchaChallenge):
captcha = cls(user, state.get("report", True))
captcha.captcha_start = state['start']
captcha.logs = state['logs']
- captcha.challenge_msg_id = state['msg_id']
+ captcha.challenge_msg_ids = state.get("msg_id") and [state["msg_id"]] or state.get("msg_ids", [])
captcha.answer = state['answer']
if (timeout := setting.get("timeout", 30)) > 0:
time_passed = int(time.time()) - int(state['start'])
@@ -2221,7 +1723,7 @@ class MathChallenge(CaptchaChallenge):
)), parse_mode=ParseMode.HTML))
if not challenge_msg:
return await log(f"Failed to send math captcha challenge to {self.user.id}")
- self.challenge_msg_id = challenge_msg.id
+ self.challenge_msg_ids = [challenge_msg.id]
self.answer = eval(expression)
self.save_state({"answer": self.answer})
self.reset_timer(timeout)
@@ -2253,7 +1755,7 @@ class ImageChallenge(CaptchaChallenge):
captcha = cls(user, state['report'])
captcha.captcha_start = state['start']
captcha.logs = state['logs']
- captcha.challenge_msg_id = state['msg_id']
+ captcha.challenge_msg_ids = state.get("msg_id") and [state["msg_id"]] or state.get("msg_ids", [])
captcha.try_count = state['try_count']
if captcha.try_count >= setting.get("img_max_retry", 3):
return await captcha.action(False)
@@ -2279,18 +1781,25 @@ class ImageChallenge(CaptchaChallenge):
console.debug(f"Failed to get captcha results from {img_captcha_bot}, fallback")
break # Fallback
# From now on, wait for bot result
+ timeout = setting.get("timeout", 300)
+ challenge_msg = await exec_api(bot.send_message(self.user.id, "\n".join((
+ lang_full('verify_challenge'),
+ "", code(lang_full("verify_complete_image")), "",
+ lang_full('verify_challenge_timed', timeout if timeout > 0 else "")
+ )), parse_mode=ParseMode.HTML))
updates = await bot.send_inline_bot_result(self.user.id, result.query_id, result.results[0].id)
for update in updates.updates:
if isinstance(update, UpdateMessageID):
- self.challenge_msg_id = update.id
+ self.challenge_msg_ids = [challenge_msg.id, update.id]
self.save_state({"try_count": self.try_count, "last_active": int(time.time())})
await bot.block_user(self.user.id)
self.reset_timer()
await super(ImageChallenge, self).start()
return
console.debug(f"Failed to send image captcha challenge to {self.user.id}, fallback")
+ challenge_msg and await challenge_msg.safe_delete()
break
- except TimeoutError:
+ except (TimeoutError, BotResponseTimeout):
console.debug(f"Image captcha bot timeout for {self.user.id}, fallback")
break # Fallback
except FloodWait as e:
@@ -2328,7 +1837,7 @@ class StickerChallenge(CaptchaChallenge):
captcha = cls(user, state['report'])
captcha.captcha_start = state['start']
captcha.logs = state['logs']
- captcha.challenge_msg_id = state['msg_id']
+ captcha.challenge_msg_ids = state.get("msg_id") and [state["msg_id"]] or state.get("msg_ids", [])
if (timeout := setting.get("timeout", 30)) > 0:
time_passed = int(time.time()) - int(state['start'])
if time_passed > timeout:
@@ -2352,7 +1861,7 @@ class StickerChallenge(CaptchaChallenge):
)), parse_mode=ParseMode.HTML))
if not challenge_msg:
return await log(f"Failed to send sticker captcha challenge to {self.user.id}")
- self.challenge_msg_id = challenge_msg.id
+ self.challenge_msg_ids = [challenge_msg.id]
self.save_state()
self.reset_timer(timeout)
await super(StickerChallenge, self).start()
@@ -2397,24 +1906,25 @@ class Rule:
if not outgoing and self._precondition():
return
members = inspect.getmembers(self, inspect.iscoroutinefunction)
- members.sort(key=sort_line_number)
- for name, func in members:
- docs = func.__doc__ or ""
- try:
- if (not name.startswith("_") and (
- "outgoing" in docs and outgoing and await func() or
- "outgoing" not in docs and not self.user.is_self and await func()
- )):
- console.debug(f"Rule triggered: `{name}` (user: {self.user.id} chat: {self.msg.chat.id})")
- break
- except Exception as e:
- console.error(f"Failed to run rule `{name}`: {e}\n{traceback.format_exc()}")
+ members.sort(key=_sort_line_number)
+ async with rule_lock:
+ for name, func in members:
+ docs = func.__doc__ or ""
+ try:
+ if (not name.startswith("_") and (
+ "outgoing" in docs and outgoing and await func() or
+ "outgoing" not in docs and not self.user.is_self and await func()
+ )):
+ console.debug(f"Rule triggered: `{name}` (user: {self.user.id} chat: {self.msg.chat.id})")
+ break
+ except Exception as e:
+ console.error(f"Failed to run rule `{name}`: {e}\n{traceback.format_exc()}")
@staticmethod
def _get_rules_priority() -> tuple:
prio_list = []
members = inspect.getmembers(Rule, inspect.iscoroutinefunction)
- members.sort(key=sort_line_number)
+ members.sort(key=_sort_line_number)
for name, func in members:
if name.startswith("_"):
continue
@@ -2433,7 +1943,6 @@ class Rule:
async def user_defined(self) -> bool:
if custom_rule := setting.get("custom_rule"):
- pass
try:
exec(f"async def _(msg, text, user, me):\n return {custom_rule}")
return bool(await locals()["_"](self.msg, self._get_text(), self.user, bot.me))
@@ -2450,15 +1959,19 @@ class Rule:
return the_world_eye.triggered
async def disable_pm(self) -> bool:
- disabled = setting.get('disable', False)
- disabled and await the_order.active(self.user.id, "disable_pm_enabled")
+ if disabled := setting.get('disable', False):
+ await the_order.active(self.user.id, "disable_pm_enabled")
+ captcha = CaptchaChallenge("none", self.user, False)
+ captcha.log_msg(self.msg.text or self.msg.caption or "")
+ await captcha.send_log("pm_disabled")
return disabled
async def chat_history(self) -> bool:
- if (history_count := setting.get("history_count")) is not None:
+ if (history_count := setting.get("history_count", -1)) > 0:
count = 0
- async for _ in bot.get_chat_history(self.user.id, offset_id=self.msg.id, offset=-history_count):
- count += 1
+ async for msg in bot.get_chat_history(self.user.id, limit=history_count + 1):
+ if msg.id != self.msg.id:
+ count += 1
if count >= history_count:
setting.whitelist.add_id(self.user.id)
return True
@@ -2468,7 +1981,7 @@ class Rule:
from pyrogram.raw.functions.users import GetFullUser
if (common_groups := setting.get("groups_in_common")) is not None:
if user_full := await exec_api(bot.invoke(GetFullUser(id=await bot.resolve_peer(self.user.id)))):
- if user_full.common_chats_count >= common_groups:
+ if user_full.full_user.common_chats_count >= common_groups:
setting.whitelist.add_id(self.user.id)
return True
else:
@@ -2477,12 +1990,16 @@ class Rule:
async def premium(self) -> bool:
if premium := setting.get("premium", False):
+ captcha = CaptchaChallenge("disable_pm", self.user, False)
+ captcha.log_msg(self.msg.text or self.msg.caption or "")
if premium == "only" and not self.msg.from_user.is_premium:
await the_order.active(self.user.id, "premium_only")
+ await captcha.send_log("premium_only")
elif not self.msg.from_user.is_premium:
return False
elif premium == "ban":
await the_order.active(self.user.id, "premium_ban")
+ await captcha.send_log("premium_ban")
return premium
# Whitelist / Blacklist
@@ -2505,7 +2022,7 @@ class Rule:
await the_order.active(self.user.id, reason_code)
# Collect logs
can_report, _ = await self._get_user_settings()
- captcha = CaptchaChallenge("", self.user, False, can_report)
+ captcha = CaptchaChallenge("blacklist", self.user, False, can_report)
captcha.log_msg(text)
await captcha.send_log(reason_code)
return True
@@ -2533,7 +2050,6 @@ class Rule:
async def verify_sticker_response(self) -> bool:
"""no_priority"""
- print("")
if (captcha := curr_captcha.get(user_id := self.user.id)) and captcha.input and captcha.type == "sticker":
captcha.log_msg(self._get_text())
await captcha.verify(self.msg.sticker) and await self.msg.safe_delete()
@@ -2599,12 +2115,6 @@ async def chat_listener(_, msg: Message):
description=f"{lang('plugin_desc')}\n{(lang('check_usage') % code(f',{user_cmd_name} h'))}")
async def cmd_entry(_, msg: Message):
cmd = Command(msg.from_user, msg)
- if len(msg.parameter) > 0 and msg.parameter[0] == "markdown":
- from io import BytesIO
- file = BytesIO(cmd._generate_markdown().encode())
- file.name = "commands.md"
- await bot.send_document(msg.chat.id, file, caption="PMCaptcha commands markdown")
- return await msg.delete()
result, err_code, extra = await cmd._run_command()
if not result:
if err_code == "NOT_FOUND":
@@ -2624,21 +2134,24 @@ async def resume_states():
try:
user = await bot.get_users(user_id)
await challenge.resume(user=user, state=value)
+ except (KeyError, PeerIdInvalid):
+ del sqlite[key]
+ console.debug(f"User {user_id} not found, deleted challenge state")
except Exception as e:
console.error(f"Error occurred when resuming captcha state: {e}\n{traceback.format_exc()}")
console.debug("Captcha State Resume Completed")
if __name__ == "plugins.pmcaptcha":
- import gc
-
# Force disabled for old PMCaptcha
globals().get("SubCommand") and exit(0)
# Flood Username confirm
user_want_set_flood_username = None
# Logger
console = logs.getChild(cmd_name)
- globals().get("console") is None and exit(0)
+ globals().get("console") is None and exit(0) # Old version
+ # Rule lock
+ rule_lock = asyncio.Lock()
captcha_challenges = {
"math": MathChallenge,
"img": ImageChallenge,
@@ -2676,3 +2189,7 @@ if __name__ == "plugins.pmcaptcha":
_cancel_task(resume_task)
resume_task = asyncio.create_task(resume_states())
gc.collect()
+elif __name__ == '__main__':
+ with open("command_list.md", "wb") as f:
+ f.write(Command._generate_markdown().encode())
+ print("MarkDown Generated.")