Skip to content

Commit

Permalink
Merge pull request #151 from AirportR/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
AirportR authored Jan 23, 2024
2 parents 552e55b + fc54f47 commit 15acd11
Show file tree
Hide file tree
Showing 30 changed files with 688 additions and 913 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ name: ci

on:
push:
paths-ignore:
- "resources/**"
- "README.md"
- "README-EN.md"
- ".github/ISSUE_TEMPLATE/**"
branches:
- "dev"
- "master"

jobs:
docker:
Expand Down
56 changes: 33 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

## 基本介绍

FullTClash bot 是承载其测试任务的Telegram 机器人(以下简称bot)。\
FullTClash名字来源于 Full Test base on Clash 。后端部分使用[Clash项目](https://github.com/Dreamacro/clash)(现在亦可称之为[mihomo](https://github.com/MetaCubeX/mihomo))相关代码作为出站代理,前端部分使用Telegram API作为交互界面,需配合Telegram使用,即为一个Telegram机器人(bot), FullTClash bot 是承载其测试任务的Telegram 机器人(以下简称bot)。\
目前支持以Clash配置文件为载体的**批量**连通性测试,支持以下测试条目:
1. Netflix
2. Youtube
Expand All @@ -43,7 +43,7 @@ FullTClash bot 是承载其测试任务的Telegram 机器人(以下简称bot

## 支持协议

| 客户端上游分支 | Clash | Clash.Meta |
| 出站协议 | Clash | Clash.Meta |
|----------------|-------|------------|
| SOCKS (4/4a/5) |||
| HTTP(S) |||
Expand All @@ -52,7 +52,7 @@ FullTClash bot 是承载其测试任务的Telegram 机器人(以下简称bot
| Trojan |||
| Snell |||
| VLESS | ||
| Tuic | ||
| TUIC | ||
| Hysteria | ||
| Hysteria2 | ||
| Wireguard | ||
Expand All @@ -76,7 +76,7 @@ FullTClash bot 是承载其测试任务的Telegram 机器人(以下简称bot

要成功运行该Telegram 机器人,首先需要准备以下信息:

- Telegram 的api_id 、api_hash [获取地址](https://my.telegram.org/apps) 不会请Google。(部分TG账号已被拉黑,无法正常使用)
- Telegram 的api_id 、api_hash [获取地址](https://my.telegram.org/apps) 不会请Google。(部分IP已被拉黑,无法正常申请成功,请尝试更换干净IP)

-[@BotFather](https://t.me/BotFather) 那里创建一个机器人,获得该机器人的bot_token,应形如:

Expand Down Expand Up @@ -157,7 +157,7 @@ apt install -y git && git clone https://github.com/AirportR/FullTclash.git && cd
```
### 获取session文件(可选)
您需要在项目文件目录下,放置一个已经登陆好的.session后缀文件,这个文件是程序生成的,形如: my_bot.session
您需要在项目文件目录下,放置一个已经登陆好的.session后缀文件,这个文件是程序生成的,是Telegram的登录凭据,形如: my_bot.session
>方法1:可以直接在配置文件config.yaml中配置,这样程序启动后会自动读取配置文件里面的值来生成session文件(要求一定要正确)。
```yaml
#配置文件示例,注意缩进要正确
Expand Down Expand Up @@ -213,33 +213,43 @@ fulltclash-windows-amd64 为 Windows-amd64 所支持的
### Docker启动
[./docker/ 目录](https://github.com/AirportR/FullTclash/tree/dev/docker)
### 为程序设置进程守护(Linux)
由于Linux系统特性,关闭ssh连接后,前台程序会被关闭。您需要设置进程守护,才能在后台不间断地运行程序。具体方法Google搜索即可。
由于Linux系统特性,关闭ssh连接后,前台程序会被关闭。您需要设置进程守护,才能在后台不间断地持久化运行程序。具体方法Google搜索即可。
## 交流探讨
我们欢迎各方朋友提出针对性的反馈:
- [TG更新发布频道](https://t.me/FullTClash)
- 在项目页面提出issue
## 致谢

- [流媒体解锁思路](https://github.com/lmc999/RegionRestrictionCheck)
- [Clash](https://github.com/Dreamacro/clash)
- [aiohttp](https://github.com/aio-libs/aiohttp)
- [pyrogram](https://github.com/pyrogram/pyrogram)
- [async-timeout](https://github.com/aio-libs/async-timeout)
- [Pillow](https://github.com/python-pillow/Pillow)
- [pilmoji](https://github.com/jay3332/pilmoji)
- [pyyaml](https://github.com/yaml/pyyaml)
- [requests](https://github.com/psf/requests)
- 在项目页面提出issue

## 如何给本项目做贡献:
1、在本项目的主GitHub仓库进行fork,你可以只fork dev的分支。 \
2、在你的计算机上使用git clone来下载你fork后的仓库。 \
3、在下载后的本地仓库进行修改。\
4、执行git add .(请不要忘记句号!!!)\
5、执行git commit,并输入你做出的更改。\
6、回到你的仓库,发起pr请求,等待下一步(通过/驳回/修改)。
6、回到你的仓库,发起pr请求到dev分支,等待下一步(通过/驳回/修改)。

## 答疑
1. FullTClash测试原理\
原理是在后台启动一个代理客户端,然后开启多个socks5入站端口,通过配置里的配置信息匹配代理客户端出站协议类型进行测试。代理客户端是基于上游的Clash项目改动得到的专属客户端,并将其命名为FullTCore。
2. 为什么不使用原版的Clash客户端二进制\
自从FullTclash的3.5.8版本起,支持前后端模式,我们把后端部分单独分离,使之可以让前端的bot运行环境与后端运行的环境不在同一台机器上,在当时Clash并没有提供符合本项目的特性,再加上FullTClash仅仅只需要其中出站功能,所以不得已进行一些改动。事实上,FullTClash的old分支是依靠Clash提供的Restful API运行的,现在已不再维护。
3. 什么是Telegram UID\
Telegram官方并没有承认UID的说法,但确实存在于Telegram中。每一个TG用户都存在一个唯一的身份ID,这个在官方的TG客户端是查询不到的。Bot依靠UID确定管理员身份,至于如何获取Google搜索即可。
4. 是否有一键部署脚本\
目前只有Docker部署脚本,期待你的贡献!

如果不这样做可能会:
## 致谢

1、仓库维护者看到的是一片绿色加号,根本不知道你改了什么。\
2、你的操作会很麻烦,可能还会改错文件。\
3、维护者很难看懂你都干了些什么。
- [流媒体解锁思路](https://github.com/lmc999/RegionRestrictionCheck)
- [Clash](https://github.com/Dreamacro/clash) ==> [mihomo](https://github.com/MetaCubeX/mihomo) [GPLv3]
- [aiohttp](https://github.com/aio-libs/aiohttp) [Apache2]
- [pyrogram](https://github.com/pyrogram/pyrogram) [LGPLv3]
- [async-timeout](https://github.com/aio-libs/async-timeout) [Apache2]
- [Pillow](https://github.com/python-pillow/Pillow) [HPND]
- [pilmoji](https://github.com/jay3332/pilmoji) [MIT]
- [pyyaml](https://github.com/yaml/pyyaml) [MIT]
- [APScheduler](https://github.com/agronholm/apscheduler) [MIT]
- [loguru](https://github.com/Delgan/loguru) [MIT]
- [geoip2](https://github.com/maxmind/GeoIP2-python) [Apache2]
- [cryptography](https://github.com/pyca/cryptography) [Apache2] [BSD3]
- [google-re2](https://github.com/google/re2) [BSD3]
- [aiohttp_socks](https://github.com/romis2012/aiohttp-socks) [Apache2]
3 changes: 2 additions & 1 deletion botmodule/cfilter.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ async def func(_, __, message: Message) -> bool:
string = str(message.text)
arg = string.strip().split(' ')
arg = [x for x in arg if x != '']
if arg[0].endswith("url") or 'invite' in arg[0]:
cmd = arg[0].split('@')[0]
if cmd.endswith("url") or 'invite' in arg[0]:
return True
if check.check_sub_name(arg[1]):
return True
Expand Down
6 changes: 4 additions & 2 deletions botmodule/command/authority.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
b1 = InlineKeyboardMarkup(
[
[ # 第一行
InlineKeyboardButton("📺 联通性测试", callback_data='test', url='')
InlineKeyboardButton("📺 连通性测试", callback_data='test', url='')
],
[ # 第二行
InlineKeyboardButton("🔗 链路拓扑测试", callback_data='analyze')
Expand Down Expand Up @@ -182,8 +182,10 @@ async def invite_pass(client: Client, message: Message):
await asyncio.sleep(3)
await bot_mes.delete()
test_item = test_type_select
initiator = str(message.from_user.id) if message.from_user else ''
await bot_put(client, mes, task_type_select, test_items=test_item,
include_text=in_text, exclude_text=ex_text, url=suburl)
include_text=in_text, exclude_text=ex_text, url=suburl,
name="邀请测试", initiator=initiator)
else:
invite_list.pop(key2, '')
else:
Expand Down
138 changes: 74 additions & 64 deletions botmodule/command/setting.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

dsc = default_slave_comment = config.getSlaveconfig().get('default-slave', {}).get('comment', "本地后端")
dsi = default_slave_id = config.getSlaveconfig().get('default-slave', {}).get('username', "local")
ds_shadow = bool(config.getSlaveconfig().get('default-slave', {}).get('shadow', False)) # 是否隐藏默认后端
ds_shadow = bool(config.getSlaveconfig().get('default-slave', {}).get('hidden', False)) # 是否隐藏默认后端
dbtn = default_button = {
1: IKB("✅Netflix", callback_data='✅Netflix'),
2: IKB("✅Youtube", callback_data='✅Youtube'),
Expand All @@ -41,7 +41,7 @@
'b_okpage': IKB("🔒完成本页选择", callback_data="ok_p"),
'b_all': IKB("全测", callback_data="全测"),
'b_origin': IKB("♾️订阅原序", callback_data="sort:订阅原序"),
'b_rhttp': IKB("⬇️HTTP倒序", callback_data="sort:HTTP倒序"),
'b_rhttp': IKB("⬇️HTTP降序", callback_data="sort:HTTP降序"),
'b_http': IKB("⬆️HTTP升序", callback_data="sort:HTTP升序"),
'b_aspeed': IKB("⬆️平均速度升序", callback_data="sort:平均速度升序"),
'b_arspeed': IKB("⬇️平均速度降序", callback_data="sort:平均速度降序"),
Expand Down Expand Up @@ -155,14 +155,13 @@ async def test_setting(client: Client, callback_query: CallbackQuery, row=3, **k
mess_id = callback_query.message.id
chat_id = callback_query.message.chat.id
origin_message = callback_query.message.reply_to_message
if origin_message is None:
logger.warning("⚠️无法获取发起该任务的源消息")
# await edit_mess.edit_text("⚠️无法获取发起该任务的源消息")
return test_items, origin_message, message, ''
inline_keyboard = callback_query.message.reply_markup.inline_keyboard

if origin_message is None:
return test_items, origin_message, message, ''
with contextlib.suppress(IndexError, ValueError):
test_type = origin_message.text.split(" ", maxsplit=1)[0].split("@", maxsplit=1)[0]
test_type = origin_message.text.split(" ", maxsplit=1)[0].split("@", maxsplit=1)[0] \
if origin_message is not None else ''

try:
if "✅" == callback_data[0]:
Expand Down Expand Up @@ -198,7 +197,7 @@ async def test_setting(client: Client, callback_query: CallbackQuery, row=3, **k
q = receiver[bot_key]
try:
if isinstance(q, asyncio.Queue):
q.put_nowait(test_items)
q.put_nowait("*")
else:
await edit_mess.reply("运行发现逻辑错误,请联系管理员~")
except asyncio.queues.QueueFull:
Expand Down Expand Up @@ -482,7 +481,11 @@ async def task_handler(app: Client, message: Message, **kwargs):
async def select_task(app: Client, originmsg: Message, slaveid: str, sort: str, script: list = None):
if originmsg.text.startswith('/invite'):
comment = config.getSlavecomment(slaveid)
scripttext = ",".join(script) if script is not None else ""
if script is not None:
tmp_script = deepcopy(script)[::-1]
scripttext = ",".join(tmp_script[:10]) + f"...共{len(script)}个脚本" if len(script) > 10 else ",".join(script)
else:
scripttext = ''
invite_help_text = f"🤖选中后端: {comment}\n⛓️选中排序: {sort}\n🧵选中脚本: {scripttext}\n\n"
botmsg = await originmsg.reply(invite_help_text)
key = genkey(8)
Expand Down Expand Up @@ -528,6 +531,61 @@ async def select_slave_only_1(_: Client, call: Union[CallbackQuery, Message], **
return await target.reply(f"请选择测试后端:\n", quote=True, reply_markup=IKM)


async def select_slave_only(app: Client, call: Union[CallbackQuery, Message], timeout=60, **kwargs) -> tuple[str, str]:
"""
高层级的选择后端api
return: (slaveid, comment)
"""
if isinstance(call, Message):
botmsg = await select_slave_only_1(app, call, timeout=timeout, **kwargs)

recvkey = gen_msg_key(botmsg)
q = asyncio.Queue(1)
receiver[recvkey] = q

try:
async with async_timeout.timeout(timeout):
comment = await q.get()
slaveconfig = config.getSlaveconfig()
slaveid = ''

for k, v in slaveconfig.items():
if v.get('comment', '') == comment:
if str(k) == "default-slave":
slaveid = 'local'
break
slaveid = str(k)
break
if not slaveid and comment == "本地后端":
slaveid = "local"
if slaveid and comment:
return str(slaveid), comment
else:
await botmsg.delete()
return '', ''

except asyncio.exceptions.TimeoutError:
print("获取超时")
return '', ''
finally:
receiver.pop(recvkey, None)
await botmsg.delete(revoke=True)
else:
api_route = '/api/getSlaveId'
le = len(api_route) + len("?comment=")
key = gen_msg_key(call.message)
if key in receiver:
q = receiver[key]
try:
if isinstance(q, asyncio.Queue):
q.put_nowait(str(call.data)[le:])
except asyncio.queues.QueueFull:
pass
else:
await call.answer("❌无法找到该消息与之对应的队列")


async def select_script_only(_: "Client", call: Union["CallbackQuery", "Message"],
timeout: int = 120) -> Union[List[str], None]:
"""
Expand Down Expand Up @@ -561,6 +619,13 @@ async def select_script_only(_: "Client", call: Union["CallbackQuery", "Message"
script_list = await q.get()
if isinstance(script_list, list):
return script_list
elif isinstance(script_list, str):
if script_list == "全测" or script_list == "all" or script_list == "*":
script = addon.global_test_item(True)
else:
new_script = [s for s in addon.global_test_item(True) if script_list in s]
script = new_script
return script
else:
await botmsg.reply("❌数据类型接收错误")
return None
Expand Down Expand Up @@ -666,61 +731,6 @@ async def select_sort_only(_: "Client", call: Union["CallbackQuery", "Message"],
await call.answer("❌无法找到该消息与之对应的队列")


async def select_slave_only(app: Client, call: Union[CallbackQuery, Message], timeout=60, **kwargs) -> tuple[str, str]:
"""
高层级的选择后端api
return: (slaveid, comment)
"""
if isinstance(call, Message):
botmsg = await select_slave_only_1(app, call, timeout=timeout, **kwargs)

recvkey = gen_msg_key(botmsg)
q = asyncio.Queue(1)
receiver[recvkey] = q

try:
async with async_timeout.timeout(timeout):
comment = await q.get()
slaveconfig = config.getSlaveconfig()
slaveid = ''

for k, v in slaveconfig.items():
if v.get('comment', '') == comment:
if str(k) == "default-slave":
slaveid = 'local'
break
slaveid = str(k)
break
if not slaveid and comment == "本地后端":
slaveid = "local"
if slaveid and comment:
return str(slaveid), comment
else:
await botmsg.delete()
return '', ''

except asyncio.exceptions.TimeoutError:
print("获取超时")
return '', ''
finally:
receiver.pop(recvkey, None)
await botmsg.delete(revoke=True)
else:
api_route = '/api/getSlaveId'
le = len(api_route) + len("?comment=")
key = gen_msg_key(call.message)
if key in receiver:
q = receiver[key]
try:
if isinstance(q, asyncio.Queue):
q.put_nowait(str(call.data)[le:])
except asyncio.queues.QueueFull:
pass
else:
await call.answer("❌无法找到该消息与之对应的队列")


async def select_slave(app: Client, call: CallbackQuery):
"""
内置的旧版选择后端回调查询
Expand Down
Loading

0 comments on commit 15acd11

Please sign in to comment.