回调功能(高级)
FullTclash回调功能
FullTclash拥有回调函数的抽象接口,可以让使用者对bot二次开发出拓展功能。
什么是回调函数?
在计算机程序设计中,回调函数,或简称回调(Callback 即call then back 被主函数调用运算后会返回主函数),是指通过参数将函数传递到其它代码的,某一块可执行代码的引用。这一设计允许了底层代码调用在高层定义的子程序。
实现细节
本项目回调功能的实现基于Python的装饰器语法糖,我们的抽象接口为:
@AccessCallback()
具体实现,请前往项目的源代码文件 ./botmodule/cfilter.py 查看。
在代码实现中,会依次执行 ./addons/callback 文件夹下所有符合接入条件的函数,通过判断返回布尔值的真假,进行取消执行或者继续执行原本功能。
目前,我们的回调函数设计仅支持 阻塞式回调。阻塞式回调里,回调函数的调用发生在原始函数调用返回之前。
第一个回调函数
首先请在 ./addons/callback/ 文件夹下,新建一个 .py 后缀文件,比如 mycallback.py
1、导入必要库
from pyrogram.types import Message
from pyrogram import Client
2、定义合法的回调函数。 我们约定,所有的回调函数名都为 callback ,并且是协程函数(async开头)。
async def callback(app: Client, message: Message) -> bool:
"""
app 参数为 Bot的客户端主程序
message 参数为 触发回调的消息对象
返回值一定为布尔值
"""
try:
await message.reply("回调函数调用成功!")
return True
# 异常检测,固定格式,防止抛异常未返回bool值
except Exception as e:
print(e)
return True
以上就是一个简单的回调例子,接下来我们加点料。 这里其实回调可以分为两种使用场景:
1、新添功能
2、拒绝服务
两者的区别主要是一个始终最后返回True,另外一个满足某个条件判断返回False。
使用案例
让bot离开群组
想使用一个新的指令让bot离开群组,比如 /leave 首先需要在配置文件写入一个配置让bot能识别leave指令,不加该配置,无法在群组使用。
bot:
command:
- leave
leave.py
from pyrogram.enums import ChatType
from pyrogram.types import Message
from pyrogram import Client
from loguru import logger
from botmodule.init_bot import admin
from utils.cleaner import ArgCleaner
from utils.check import get_telegram_id_from_message
async def callback(app: Client, message: Message) -> bool:
try:
tgargs = ArgCleaner().getall(str(message.text))
if tgargs[0].startswith('/leave'):
assert message.chat.type in (ChatType.GROUP, ChatType.SUPERGROUP)
ID = get_telegram_id_from_message(message)
if ID in admin:
await message.chat.leave()
logger.info(f"{app.username} 已离开 {message.chat.id}:{message.chat.title}")
return True
except Exception as e:
print(e)
return True
拒绝滥用bot的坏蛋
假设有一个坏蛋故意往你的bot丢一个节点池测速,或者你单纯讨厌TA,不想给TA使用,那么有以下例子。
block.py
from pyrogram.types import Message
from pyrogram import Client
from utils.check import get_telegram_id_from_message # 项目封装的方法,用于获取用户id
block_id_list = [] # 你要禁止的TG用户id
async def callback(app: Client, message: Message) -> bool:
"""
app 参数为 Bot的客户端主程序
message 参数为 触发回调的消息对象
返回值一定为布尔值
"""
try:
# get_telegram_id_from_message 为项目封装好的函数,获取干净的id
userid = get_telegram_id_from_message(message)
if userid in block_id_list:
await message.reply("你已被ban,无法使用!")
# 因为这个回调属于拒绝服务类型,所以返回False就是告诉bot程序不再往下运行。
return False
else:
return True
except Exception as e:
print(e)
return True
全局测试订阅链接黑名单
总是有人喜欢那公益的节点订阅进行测试,本来就慢,这下更慢了,所以可以加个禁止测试某些订阅的黑名单。
from urllib.parse import urlparse # url解析
from pyrogram.filters import private_filter
from pyrogram.types import Message
from pyrogram import Client
from loguru import logger # 日志记录
from utils.cleaner import ArgCleaner, geturl # 项目定义的成员变量、方法
from botmodule.init_bot import admin # 管理员名单
domain_blacklist = [] # 全局域名黑名单,针对机场和订阅转换。
url_blacklist = ["https://baidu.com"] # 全局url黑名单,针对单个订阅。
async def callback(_: Client, message: Message) -> bool:
"""
invite指令临时邀请测试的人判定url黑名单
"""
try:
# 不是私聊的,跳过,不走这个回调。交给其他回调
if not await private_filter(_, _, message):
return True
# 消息不是http开头说明不是订阅链接,跳过,不走这个回调。
tgargs = ArgCleaner().getall(str(message.text))
if not tgargs[0].startswith("http"):
return True
# 用 geturl 匹配订阅链接
suburl = geturl(str(message.text))
# 没匹配成功,跳过
if suburl is None:
return True
# 管理员怎么能受限制呢,权限肯定拉满!
if message.from_user.id in admin:
await message.reply("您是至高的管理员,黑名单对您无效☺️")
return True
# 匹配到对应的域名或者URL,拒绝测试!
elif is_in_blacklist(suburl):
await message.reply("❌此订阅拒绝服务~")
return False
else:
return True
except Exception as e:
logger.info(str(e))
return True
def is_in_blacklist(url: str) -> bool:
if url in url_blacklist:
logger.warning(f"检测到url黑名单: {url}")
return True
domain = urlparse(url).netloc
logger.info("invite指令解析的域名: "+domain)
if domain in domain_blacklist:
logger.warning(f"检测到域名黑名单: {domain}\n来源于此订阅: {url}")
return True
else:
return False
权限回调,让游客权限在特定群组使用invite
import time
from pyrogram.types import Message
from pyrogram.errors import UserNotParticipant
from pyrogram import Client
# from botmodule import invite # 3.5.9(包括)以前的invite接口
from botmodule import task_handler # 3.6.0以上新版invite接口,低于此版本请勿启用
from botmodule.init_bot import reloadUser # 拿到用户名单
from utils import message_delete_queue as mdq # 消息定时删除队列,调用 mdq.put(message, 5)将在5秒后删除消息
from utils.cleaner import ArgCleaner
from utils.check import get_telegram_id_from_message
group_whitelist = [-1001542230803] # 群组id白名单,只有在名单的群组 /invite指令才有效
cooling_queue = {}
cooling_interval = 1 # 调用invite的冷却时间,单位秒
GROUP_VERIFY = True # 需要目标在群里,有些是通过频道关联群组发言,并未加群。若为True则此类情况将拒绝测试。
async def user_is_in_group(message: Message) -> bool:
ID2 = get_telegram_id_from_message(message)
if str(ID2).startswith("-100") or message.sender_chat is not None:
await message.reply("❌匿名身份无法邀请测试", disable_notification=True)
return False
if GROUP_VERIFY:
try:
await message.chat.get_member(message.from_user.id)
except UserNotParticipant:
await message.reply("❌目标非群组成员,请稍后再试", disable_notification=True)
return False
return True
async def callback(bot: Client, message: Message) -> bool:
try:
tgargs = ArgCleaner().getall(str(message.text))
# 匹配invite指令,不是的话不走这个回调
if tgargs[0].startswith('/invite'):
user = reloadUser()
print("invite指令触发的群组id:", message.chat.id)
ID = get_telegram_id_from_message(message)
# invite指令需要回复一个目标
if message.reply_to_message is None:
backmsg = await message.reply("请先回复一个目标")
mdq.put_nowait((backmsg.chat.id, backmsg.id, 5))
return False
r_msg = message.reply_to_message
# 判断是否已入群
if not await user_is_in_group(r_msg):
return False
# 用户权限就直接返回了,因为自己本身有权限。
if ID in user:
return True
if message.chat.id in group_whitelist:
if ID in cooling_queue:
pre_time = cooling_queue[ID]
if time.time() - pre_time < cooling_interval:
backmsg = await message.reply(f"❌您在{cooling_interval}秒内发起过测试,请稍后再试~")
mdq.put_nowait((backmsg.chat.id, backmsg.id, 5)) # 旧版本写法,为了兼容3.5.3。用mdq.put(不支持3.5.3)也可以
return False
else:
cooling_queue[ID] = time.time()
await task_handler(bot, message, page=1) # 3.6.0新版回调,低于此版本请勿启用
# await invite(bot, message)
# 这里返回False是因为我们上面已经发起了一个invite,我们不再需要走原来的invite逻辑了,返回True就会触发两个,不信你试试。
return False
else:
cooling_queue[ID] = time.time()
await task_handler(bot, message, page=1) # 3.6.0新版回调,低于此版本请勿启用
# await invite(bot, message)
return False
# 匹配invite指令,不是的话不走这个回调
return True
except Exception as e:
print(e)
return True
项目中所定义的变量、方法预览
你可以导入这些方法和变量,就不需要自己造太多轮子了
1、消息删除队列
bot进行发送消息后,你可以设定若干秒数后删除某个消息
from utils.check import message_delete_queue as mdq
# 使用方法
# 10秒后删除(默认值)
mdq.put(message) # 此方法在3.5.3不适用
# 5秒后删除
mdq.put(message, 5)
# 3.5.3 请使用这个:
mdq.put_nowait((message.chat.id, message.id, 10))
2、拿到用户名单、管理员名单
from botmodule.init_bot import admin, USER_TARGET
# 或者
from botmodule.init_bot import reloadUser
#区别在于 reloadUser会进行重载操作,配置文件会重新加载然后读取用户列表返回,USER_TARGET是启动bot加载好的,出于安全考虑,admin名单列表无重载,只会在启动时加载。
3、拿到配置文件中的所有值
我们定义了一个存放着配置文件的全局变量,它属于 cleaner.ConfigManager() 类
from botmodule.init_bot import config
# 当然 utils.cleaner下也有一个,你用哪个都没问题,和bot有关的配置最好用上面这个
# from utils.cleaner import config
myconfig = config.config # 第一个config是实例,第二个config才是成员变量,它是yaml反序列化而来的一个字典。
4、消息编辑队列 这个用得不是很多。主要是针对编辑同一条消息可能会出现顺序不一致的问题而设计。
from utils import message_edit_queue as meq
# 使用方法:meq.put(chat.id, message.id, text, seconds, reply_markup)
5、正则匹配url地址
from utils.cleaner import geturl
url = geturl("http字符串")
if url is not None:
print(f"匹配成功的URL: {url}")
6、切片tg消息中的指令参数,返回命令参数列表
from utils.cleaner import ArgCleaner
tgargs = ArgCleaner().getall(message.text)
print(tgargs)
# 或者:
tgargs = ArgCleaner.getarg(message.text)
# 源码展示:
@staticmethod
def getarg(string: str, sep: str = ' ') -> list[str]:
"""
对字符串使用特定字符进行切片
Args:
string: 要切片的字符串
sep: 指定用来切片的字符依据,默认为空格
Returns: 返回一个切好的字符串列表
"""
return [x for x in string.strip().split(sep) if x != ''] if string is not None else []
更多的方法和变量,就自己阅读源码或者手搓吧
Last updated