├── README.md ├── bot.py ├── example.config.yaml ├── readme.config.yaml └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # 订阅转换Bot 2 | 3 | 订阅转换Bot是一个基于 `Telegram Api` 的订阅转换前端,可以方便地选择转换目标类型和规则来生成订阅转换链接。 4 | 5 | ## 关于数据安全 6 | 7 | 订阅转换 Bot 处理的所有的数据都不会以任何方式保存,订阅链接仅会进行编码和拼接,不会保存 8 | 9 | ## 机器人安装方法 10 | 11 | ### 搭建 12 | 13 | > 这里以 `Debian` 系统为例 14 | 15 | 你首先可能需要安装软件包 16 | 17 | ```bash 18 | apt install git python3-pip -y 19 | ``` 20 | 21 | 然后拉取项目 22 | 23 | ```bash 24 | git clone https://github.com/cpploveme/ConvertBot.git 25 | ``` 26 | 27 | 安装依赖 28 | 29 | ```bash 30 | pip install re pyyaml telebot 31 | ``` 32 | 33 | > 对于高版本且非虚拟环境搭建的Bot你可能需要加上 `--break-system-packages` 34 | 35 | 关于如何让 Bot 持久化运行这里不会详细说,你可以参考 `screen`, `pm2`, `systemd` 等方法,这里提供一个使用 `screen` 的简单进程守护方式。 36 | 37 | ```bash 38 | cd ConvertBot 39 | apt install screen -y 40 | screen -R convertbot 41 | python3 bot.py 42 | ``` 43 | 44 | ### 机器人配置说明 45 | 46 | #### 必要配置 47 | 48 | `token`:不填入则 Bot 无法启动,`Bot Token` 可以在 `@BotFather` 获取。 49 | 50 | `admin`:不填入则 Bot 无法由您拉入群聊内,但仍仅可在私聊内使用以防暴露订阅链接。 51 | 52 | `items`:不填入则 Bot 按钮无法生成。 53 | 54 | #### 可选配置 55 | 56 | 建议参看 `readme.config.yaml` 内的详细说明,也可以使用 `example.config.yaml` 的样例配置。 57 | 58 | 需要注意的是,`规则名称` 和 `平台名称` 不要包含空格,也不要太长,因为要作为按钮的文本和回调数据,过长可能会导致按钮无法生成。 59 | 60 | `airport` 配置如若不填或者直接删去即为不限制订阅转换的链接。 61 | 62 | ## 机器人使用方法 63 | 64 | > 请注意 Bot 命令仅可在私聊中使用 65 | 66 | #### /help 67 | 68 | 获取帮助菜单 69 | 70 | #### /convert <订阅链接> 71 | 72 | 发送命令转换订阅链接后,选择需要转换的类型和订阅规则即可生成订阅链接。 -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | import re 2 | import yaml 3 | import urllib 4 | import telebot 5 | 6 | def load_admin() : 7 | try : 8 | with open('./config.yaml', 'r', encoding='utf-8') as f: 9 | data = yaml.load(stream=f, Loader=yaml.FullLoader) 10 | return data['admin'] 11 | except : 12 | pass 13 | 14 | def load_token() : 15 | try : 16 | with open('./config.yaml', 'r', encoding='utf-8') as f: 17 | data = yaml.load(stream=f, Loader=yaml.FullLoader) 18 | return data['token'] 19 | except : 20 | pass 21 | 22 | def load_items() : 23 | try : 24 | with open('./config.yaml', 'r', encoding='utf-8') as f: 25 | data = yaml.load(stream=f, Loader=yaml.FullLoader) 26 | return int(data['items']) 27 | except : 28 | pass 29 | 30 | def remove_convert(url): 31 | if "sub?target=" in url: 32 | pattern = r"url=([^&]*)" 33 | match = re.search(pattern, url) 34 | if match: 35 | encoded_url = match.group(1) 36 | decoded_url = urllib.parse.unquote(encoded_url) 37 | return decoded_url 38 | else: 39 | return url 40 | return url 41 | 42 | def get_link(message): 43 | url_list = re.findall("http[s]?://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]", message.text) 44 | temp_list = [] 45 | for url in url_list: 46 | temp_list.append(remove_convert(url)) 47 | temp_list = list(set(temp_list)) 48 | return temp_list 49 | 50 | admin_id = load_admin() 51 | 52 | items_per_page = load_items() 53 | 54 | bot = telebot.TeleBot(load_token()) 55 | bot_name = "" 56 | 57 | @bot.message_handler(func=lambda m: True, content_types=['new_chat_members']) 58 | def auto_leave(message): 59 | if not message.json['new_chat_participant']['username'] in bot_name : 60 | return 61 | try : 62 | if not str(message.from_user.id) in admin_id: 63 | bot.reply_to(message, "❌ 机器人已启动防拉群模式 请勿拉群呢 ~") 64 | bot.leave_chat(message.chat.id) 65 | except : 66 | pass 67 | 68 | @bot.message_handler(commands=['start']) 69 | def start_bot(message): 70 | try: 71 | bot.reply_to(message, "欢迎使用订阅转换机器人\n\n发送 `/help` 获取帮助\n\n发送 `/convert <订阅链接>` 开始转换操作", parse_mode='Markdown') 72 | except: 73 | bot.reply_to(message, "❌ 出现异常错误", parse_mode='Markdown') 74 | 75 | @bot.message_handler(commands=['help']) 76 | def start_bot(message): 77 | try: 78 | bot.reply_to(message, "发送 `/convert <订阅链接>` 开始转换\n\n发送命令后 选择订阅链接转换后的 `平台 / 格式` 并点击按钮\n\n然后选择 `分流规则` 最后复制 `订阅链接`", parse_mode='Markdown') 79 | except: 80 | bot.reply_to(message, "❌ 出现异常错误", parse_mode='Markdown') 81 | 82 | @bot.message_handler(commands=['convert']) 83 | def convert_sub(message): 84 | try: 85 | if not message.chat.type == "private" : 86 | bot.reply_to(message, f"❌ 请私聊使用本机器人呢 ~", parse_mode='Markdown') 87 | return 88 | with open('./config.yaml', 'r', encoding='utf-8') as f: 89 | data = yaml.load(stream=f, Loader=yaml.FullLoader) 90 | 91 | url_list = get_link(message) 92 | if len(url_list) == 0: 93 | bot.reply_to(message, "您转换的内容不包含 `订阅链接` 呢 ~", parse_mode='Markdown') 94 | return 95 | try: 96 | if data.get("airport", None) is not None: 97 | for url in url_list: 98 | flag = False 99 | for airport in data["airport"]: 100 | if urllib.parse.urlparse(url).netloc == airport: 101 | flag = True 102 | if urllib.parse.urlparse(url).netloc == urllib.parse.urlparse(airport).netloc: 103 | flag = True 104 | if flag == False: 105 | bot.reply_to(message, f"❌ 不支持转换订阅域名 `{urllib.parse.urlparse(url).netloc}` 呢 ~", parse_mode='Markdown') 106 | return 107 | except: 108 | pass 109 | keyboard = [] 110 | i = 0 111 | page_buttons = [] 112 | if len(data['platform']) % items_per_page > 0: 113 | pages = len(data['platform']) // items_per_page + 1 114 | else: 115 | pages = len(data['platform']) // items_per_page 116 | for platform in data['platform']: 117 | if i > items_per_page: 118 | break 119 | i = i + 1 120 | if i % 2 == 1: 121 | page_buttons = [] 122 | temp_button = telebot.types.InlineKeyboardButton(f'{platform}', callback_data=f'platform {platform}') 123 | page_buttons.append(temp_button) 124 | else : 125 | temp_button = telebot.types.InlineKeyboardButton(f'{platform}', callback_data=f'platform {platform}') 126 | page_buttons.append(temp_button) 127 | keyboard.append(page_buttons) 128 | page_info = f'{1} / {pages}' 129 | prev_button = telebot.types.InlineKeyboardButton(' ', callback_data='blank') 130 | if pages == 1 : 131 | next_button = telebot.types.InlineKeyboardButton(' ', callback_data='blank') 132 | else : 133 | next_button = telebot.types.InlineKeyboardButton('下一页', callback_data='platform next 1') 134 | page_button = telebot.types.InlineKeyboardButton(page_info, callback_data=f'page_info {1} {pages}') 135 | page_buttons = [prev_button, page_button, next_button] 136 | keyboard.append(page_buttons) 137 | keyboard.append([telebot.types.InlineKeyboardButton('关闭', callback_data='close')]) 138 | reply_markup = telebot.types.InlineKeyboardMarkup(keyboard) 139 | bot.reply_to(message, "请选择 `生成类型` :", parse_mode='Markdown', reply_markup=reply_markup) 140 | except: 141 | bot.reply_to(message, "❌ 出现异常错误", parse_mode='Markdown') 142 | 143 | def botinit(): 144 | global bot_name 145 | bot_name = '@' + bot.get_me().username 146 | bot.delete_my_commands(scope=None, language_code=None) 147 | bot.set_my_commands(commands=[telebot.types.BotCommand("help", "帮助菜单"), telebot.types.BotCommand("convert", "订阅转换")]) 148 | 149 | @bot.callback_query_handler(func=lambda call: True) 150 | def callback_inline(call): 151 | try: 152 | command_type = call.data.split()[0] 153 | if command_type == "platform": 154 | op = call.data.split()[1] 155 | if op == "next": 156 | current_page = int(call.data.split()[2]) + 1 157 | 158 | with open('./config.yaml', 'r', encoding='utf-8') as f: 159 | data = yaml.load(stream=f, Loader=yaml.FullLoader) 160 | keyboard = [] 161 | i = 0 162 | page_buttons = [] 163 | if len(data['platform']) % items_per_page > 0: 164 | pages = len(data['platform']) // items_per_page + 1 165 | else: 166 | pages = len(data['platform']) // items_per_page 167 | for platform in data['platform']: 168 | if i < (current_page - 1) * items_per_page: 169 | i = i + 1 170 | continue 171 | if i > items_per_page * current_page: 172 | break 173 | i = i + 1 174 | if i % 2 == 1: 175 | page_buttons = [] 176 | temp_button = telebot.types.InlineKeyboardButton(f'{platform}', callback_data=f'platform {platform}') 177 | page_buttons.append(temp_button) 178 | else : 179 | temp_button = telebot.types.InlineKeyboardButton(f'{platform}', callback_data=f'platform {platform}') 180 | page_buttons.append(temp_button) 181 | keyboard.append(page_buttons) 182 | page_info = f'{current_page} / {pages}' 183 | if current_page == 1: 184 | prev_button = telebot.types.InlineKeyboardButton(' ', callback_data='blank') 185 | else: 186 | prev_button = telebot.types.InlineKeyboardButton('上一页', callback_data=f'platform prev {current_page}') 187 | if current_page == pages : 188 | next_button = telebot.types.InlineKeyboardButton(' ', callback_data='blank') 189 | else : 190 | next_button = telebot.types.InlineKeyboardButton('下一页', callback_data=f'platform next {current_page}') 191 | page_button = telebot.types.InlineKeyboardButton(page_info, callback_data=f'page_info {current_page} {pages}') 192 | page_buttons = [prev_button, page_button, next_button] 193 | keyboard.append(page_buttons) 194 | keyboard.append([telebot.types.InlineKeyboardButton('关闭', callback_data='close')]) 195 | reply_markup = telebot.types.InlineKeyboardMarkup(keyboard) 196 | bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text="请选择 `生成类型` :", parse_mode='Markdown', reply_markup=reply_markup) 197 | elif op == "prev": 198 | current_page = int(call.data.split()[2]) - 1 199 | 200 | with open('./config.yaml', 'r', encoding='utf-8') as f: 201 | data = yaml.load(stream=f, Loader=yaml.FullLoader) 202 | keyboard = [] 203 | i = 0 204 | page_buttons = [] 205 | if len(data['platform']) % items_per_page > 0: 206 | pages = len(data['platform']) // items_per_page + 1 207 | else: 208 | pages = len(data['platform']) // items_per_page 209 | for platform in data['platform']: 210 | if i < (current_page - 1) * items_per_page: 211 | i = i + 1 212 | continue 213 | if i > items_per_page * current_page: 214 | break 215 | i = i + 1 216 | if i % 2 == 1: 217 | page_buttons = [] 218 | temp_button = telebot.types.InlineKeyboardButton(f'{platform}', callback_data=f'platform {platform}') 219 | page_buttons.append(temp_button) 220 | else : 221 | temp_button = telebot.types.InlineKeyboardButton(f'{platform}', callback_data=f'platform {platform}') 222 | page_buttons.append(temp_button) 223 | keyboard.append(page_buttons) 224 | page_info = f'{current_page} / {pages}' 225 | if current_page == 1: 226 | prev_button = telebot.types.InlineKeyboardButton(' ', callback_data='blank') 227 | else: 228 | prev_button = telebot.types.InlineKeyboardButton('上一页', callback_data=f'platform prev {current_page}') 229 | if current_page == pages : 230 | next_button = telebot.types.InlineKeyboardButton(' ', callback_data='blank') 231 | else : 232 | next_button = telebot.types.InlineKeyboardButton('下一页', callback_data=f'platform next {current_page}') 233 | page_button = telebot.types.InlineKeyboardButton(page_info, callback_data=f'page_info {current_page} {pages}') 234 | page_buttons = [prev_button, page_button, next_button] 235 | keyboard.append(page_buttons) 236 | keyboard.append([telebot.types.InlineKeyboardButton('关闭', callback_data='close')]) 237 | reply_markup = telebot.types.InlineKeyboardMarkup(keyboard) 238 | bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text="请选择 `生成类型` :", parse_mode='Markdown', reply_markup=reply_markup) 239 | else : 240 | with open('./config.yaml', 'r', encoding='utf-8') as f: 241 | data = yaml.load(stream=f, Loader=yaml.FullLoader) 242 | keyboard = [] 243 | i = 0 244 | page_buttons = [] 245 | if len(data['rule']) % items_per_page > 0: 246 | pages = len(data['rule']) // items_per_page + 1 247 | else: 248 | pages = len(data['rule']) // items_per_page 249 | ptf = call.data.split()[1] 250 | for rule in data['rule']: 251 | if i > items_per_page: 252 | break 253 | i = i + 1 254 | if i % 2 == 1: 255 | page_buttons = [] 256 | temp_button = telebot.types.InlineKeyboardButton(f'{rule}', callback_data=f'rule {rule} {ptf}') 257 | page_buttons.append(temp_button) 258 | else : 259 | temp_button = telebot.types.InlineKeyboardButton(f'{rule}', callback_data=f'rule {rule} {ptf}') 260 | page_buttons.append(temp_button) 261 | keyboard.append(page_buttons) 262 | page_info = f'{1} / {pages}' 263 | prev_button = telebot.types.InlineKeyboardButton(' ', callback_data='blank') 264 | if pages == 1 : 265 | next_button = telebot.types.InlineKeyboardButton(' ', callback_data='blank') 266 | else : 267 | next_button = telebot.types.InlineKeyboardButton('下一页', callback_data=f'rule next 1 {ptf}') 268 | page_button = telebot.types.InlineKeyboardButton(page_info, callback_data=f'page_info {1} {pages}') 269 | page_buttons = [prev_button, page_button, next_button] 270 | keyboard.append(page_buttons) 271 | keyboard.append([telebot.types.InlineKeyboardButton('关闭', callback_data='close')]) 272 | reply_markup = telebot.types.InlineKeyboardMarkup(keyboard) 273 | bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text="请选择 `规则` :", parse_mode='Markdown', reply_markup=reply_markup) 274 | elif command_type == "rule": 275 | op = call.data.split()[1] 276 | if op == "next": 277 | current_page = int(call.data.split()[2]) + 1 278 | 279 | with open('./config.yaml', 'r', encoding='utf-8') as f: 280 | data = yaml.load(stream=f, Loader=yaml.FullLoader) 281 | keyboard = [] 282 | i = 0 283 | page_buttons = [] 284 | if len(data['rule']) % items_per_page > 0: 285 | pages = len(data['rule']) // items_per_page + 1 286 | else: 287 | pages = len(data['rule']) // items_per_page 288 | for rule in data['rule']: 289 | if i < (current_page - 1) * items_per_page: 290 | i = i + 1 291 | continue 292 | if i > items_per_page * current_page: 293 | break 294 | i = i + 1 295 | if i % 2 == 1: 296 | page_buttons = [] 297 | temp_button = telebot.types.InlineKeyboardButton(f'{rule}', callback_data=f'rule {rule} {call.data.split()[3]}') 298 | page_buttons.append(temp_button) 299 | else : 300 | temp_button = telebot.types.InlineKeyboardButton(f'{rule}', callback_data=f'rule {rule} {call.data.split()[3]}') 301 | page_buttons.append(temp_button) 302 | keyboard.append(page_buttons) 303 | page_info = f'{current_page} / {pages}' 304 | if current_page == 1: 305 | prev_button = telebot.types.InlineKeyboardButton(' ', callback_data='blank') 306 | else: 307 | prev_button = telebot.types.InlineKeyboardButton('上一页', callback_data=f'rule prev {current_page} {call.data.split()[3]}') 308 | if current_page == pages : 309 | next_button = telebot.types.InlineKeyboardButton(' ', callback_data='blank') 310 | else : 311 | next_button = telebot.types.InlineKeyboardButton('下一页', callback_data=f'rule next {current_page} {call.data.split()[3]}') 312 | page_button = telebot.types.InlineKeyboardButton(page_info, callback_data=f'page_info {current_page} {pages} {call.data.split()[3]}') 313 | page_buttons = [prev_button, page_button, next_button] 314 | keyboard.append(page_buttons) 315 | keyboard.append([telebot.types.InlineKeyboardButton('关闭', callback_data='close')]) 316 | reply_markup = telebot.types.InlineKeyboardMarkup(keyboard) 317 | bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text="请选择 `规则` :", parse_mode='Markdown', reply_markup=reply_markup) 318 | elif op == "prev": 319 | current_page = int(call.data.split()[2]) - 1 320 | 321 | with open('./config.yaml', 'r', encoding='utf-8') as f: 322 | data = yaml.load(stream=f, Loader=yaml.FullLoader) 323 | keyboard = [] 324 | i = 0 325 | page_buttons = [] 326 | if len(data['rule']) % items_per_page > 0: 327 | pages = len(data['rule']) // items_per_page + 1 328 | else: 329 | pages = len(data['rule']) // items_per_page 330 | for rule in data['rule']: 331 | if i < (current_page - 1) * items_per_page: 332 | i = i + 1 333 | continue 334 | if i > items_per_page * current_page: 335 | break 336 | i = i + 1 337 | if i % 2 == 1: 338 | page_buttons = [] 339 | temp_button = telebot.types.InlineKeyboardButton(f'{rule}', callback_data=f'rule {rule} {call.data.split()[3]}') 340 | page_buttons.append(temp_button) 341 | else : 342 | temp_button = telebot.types.InlineKeyboardButton(f'{rule}', callback_data=f'rule {rule} {call.data.split()[3]}') 343 | page_buttons.append(temp_button) 344 | keyboard.append(page_buttons) 345 | page_info = f'{current_page} / {pages}' 346 | if current_page == 1: 347 | prev_button = telebot.types.InlineKeyboardButton(' ', callback_data='blank') 348 | else: 349 | prev_button = telebot.types.InlineKeyboardButton('上一页', callback_data=f'rule prev {current_page} {call.data.split()[3]}') 350 | if current_page == pages : 351 | next_button = telebot.types.InlineKeyboardButton(' ', callback_data='blank') 352 | else : 353 | next_button = telebot.types.InlineKeyboardButton('下一页', callback_data=f'rule next {current_page} {call.data.split()[3]}') 354 | page_button = telebot.types.InlineKeyboardButton(page_info, callback_data=f'page_info {current_page} {pages} {call.data.split()[3]}') 355 | page_buttons = [prev_button, page_button, next_button] 356 | keyboard.append(page_buttons) 357 | keyboard.append([telebot.types.InlineKeyboardButton('关闭', callback_data='close')]) 358 | reply_markup = telebot.types.InlineKeyboardMarkup(keyboard) 359 | bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text="请选择 `规则` :", parse_mode='Markdown', reply_markup=reply_markup) 360 | else : 361 | with open('./config.yaml', 'r', encoding='utf-8') as f: 362 | data = yaml.load(stream=f, Loader=yaml.FullLoader) 363 | url_list = get_link(call.message.reply_to_message) 364 | backend = data['backend'] 365 | if not "http" in backend: 366 | backend = "https://" + backend 367 | backend = urllib.parse.urlparse(backend).netloc 368 | result = "https://"+ backend + "/sub?target=" + str(data['platform'][str(call.data.split()[2])]) + "&url=" + urllib.parse.quote_plus(url_list[0]) 369 | for url in url_list[1:]: 370 | if len(url) != 0: 371 | result = result + "|" + urllib.parse.quote_plus(url) 372 | result = result + "&config=" + str(data['rule'][str(call.data.split()[1])]) + str(data['parameter']) 373 | keyboard = [] 374 | reply_markup = telebot.types.InlineKeyboardMarkup(keyboard) 375 | bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text=f"点击以下链接复制到软件内订阅即可 ~\n\n`{result}`", parse_mode='Markdown', reply_markup=reply_markup) 376 | elif command_type == "page_info": 377 | bot.answer_callback_query(call.id, f"第 {call.data.split()[1]} 页 共 {call.data.split()[2]} 页", show_alert=True) 378 | elif command_type == "close": 379 | bot.delete_message(call.message.chat.id, call.message.message_id) 380 | except: 381 | keyboard = [] 382 | reply_markup = telebot.types.InlineKeyboardMarkup(keyboard) 383 | bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text="❌ 出现异常错误", parse_mode='Markdown', reply_markup=reply_markup) 384 | 385 | if __name__ == '__main__': 386 | print('[程序已启动]') 387 | botinit() 388 | while True: 389 | try: 390 | bot.polling(none_stop=True) 391 | except Exception as e: 392 | pass 393 | -------------------------------------------------------------------------------- /example.config.yaml: -------------------------------------------------------------------------------- 1 | token: 你的 Bot Token 2 | admin: 3 | - 你的id 4 | 5 | 6 | items: 10 7 | backend: api.nexconvert.com 8 | platform: 9 | Clash: clash 10 | ClashR: clashr 11 | Base64: mixed 12 | 混合订阅: mixed 13 | Surge2: surge&ver=2 14 | Surge3: surge&ver=3 15 | Surge3: surge&ver=4 16 | Quantumult: quan 17 | QuantumultX: quanx 18 | Loon: loon 19 | Mellow: mellow 20 | SurfBoard: surfboard 21 | Shadowsocks: ss 22 | ShadowsocksAndroid: sssub 23 | ShadowsocksR: ssr 24 | ShadowsocksD: ssd 25 | 26 | # 就填一下下面这个就行 parameter是附加参数 现在这个就挺好 上面的platform你可以自己加 注意surge那个有版本区别 27 | parameter: "&emoji=true&list=false&tfo=false&expand=true&scv=true&fdn=false" 28 | rule: 29 | 精简: https://cdn.staticaly.com/gh/fengguowudi/ini/main/jess-sjgz.ini 30 | 默认: default.ini 31 | -------------------------------------------------------------------------------- /readme.config.yaml: -------------------------------------------------------------------------------- 1 | token: # bot token 2 | admin: 3 | - 你的id 4 | 5 | items: 10 # 一页按钮的数量 最好是偶数 6 | backend: # 订阅转换后端域名 7 | airport: # 限制订阅链接域名 只能转换以下域名的订阅 8 | - a.b.c 9 | parameter: "" # 订阅转换的参数 10 | rule: # 规则 格式为 "规则名称: 规则地址" 名称和地址不要包含空格 11 | 默认: default.ini 12 | platform: # 平台 格式为 "平台名称: 转换后目标类型" 平台和类型不要包含空格 13 | Clash: clash -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | re 2 | pyyaml 3 | telebot --------------------------------------------------------------------------------