diff --git a/bot.py b/bot.py index 24bf6f7..900e1ad 100644 --- a/bot.py +++ b/bot.py @@ -8,11 +8,10 @@ log = logging.getLogger(__name__) TOKEN = os.environ.get('BOT_TOKEN', '') ADMIN_ID = int(os.environ.get('ADMIN_ID', '0')) -DATA_FILE = os.environ.get('DATA_FILE', '/opt/sub-bot/data.json') +DATA_FILE = '/opt/sub-bot/data.json' PROTOS = ['ss://', 'vmess://', 'vless://', 'trojan://', 'hysteria2://', 'hy2://', 'tuic://'] AUTO_DEL = 60 -SUB_SECRET = os.environ.get('SUB_SECRET', 'change_me_to_random_string') -SUB_HOST = os.environ.get('SUB_HOST', '0.0.0.0:18888') +SUB_SECRET = os.environ.get('SUB_SECRET', 'changeme') WAITING_ADD = set() # user_ids waiting to add sub def load_data(): @@ -131,6 +130,7 @@ async def cb_menu(update: Update, context: ContextTypes.DEFAULT_TYPE): InlineKeyboardButton(f"๐Ÿ”— {t} ({cnt})", callback_data=f'get_{t}_raw'), InlineKeyboardButton(f"๐Ÿ“„ {t} Base64", callback_data=f'get_{t}_b64')]) buttons.append([InlineKeyboardButton("โšก Clash Meta ่ฎข้˜…", callback_data='get_all_clash')]) + buttons.append([InlineKeyboardButton("๐ŸŽฏ ่‡ช้€‰่Š‚็‚น", callback_data='menu_pick_multi')]) await send_del(q.message, '้€‰ๆ‹ฉๆ ผๅผ:', reply_markup=InlineKeyboardMarkup(buttons)) elif action == 'menu_del': @@ -163,6 +163,15 @@ async def cb_menu(update: Update, context: ContextTypes.DEFAULT_TYPE): except: pass await send_del(q.message, '๐Ÿ“Š *ๆฃ€ๆต‹็ป“ๆžœ*\n\n' + '\n'.join(results), parse_mode='Markdown') + elif action == 'menu_pick_multi': + alive = [s for s in data['subs'] if s.get('alive', True)] + if not alive: + return await send_del(q.message, '๐Ÿ“ญ ๆš‚ๆ— ๅฏ็”จ่ฎข้˜…') + context.user_data['multi_sel'] = set() + buttons = [[InlineKeyboardButton(f"โ˜ {s['name']}", callback_data=f'msel_{i}')] for i, s in enumerate(alive)] + buttons.append([InlineKeyboardButton("โœ… ็กฎ่ฎค", callback_data='msel_done')]) + await send_del(q.message, '๐ŸŽฏ ็‚นๅ‡ป้€‰ๆ‹ฉ่Š‚็‚น:', reply_markup=InlineKeyboardMarkup(buttons), delay=120) + elif action == 'menu_setgroup': if q.from_user.id != ADMIN_ID: return await send_del(q.message, 'โ›” ไป…็ฎก็†ๅ‘˜ๅฏ็”จ') @@ -185,7 +194,8 @@ async def cb_getsub(update: Update, context: ContextTypes.DEFAULT_TYPE): if idx < len(alive): buttons = [ [InlineKeyboardButton("๐Ÿ”— ๅŽŸๅง‹้“พๆŽฅ", callback_data=f'fmt_{idx}_raw')], - [InlineKeyboardButton("โšก Clash Meta ่ฎข้˜…", callback_data=f'fmt_{idx}_clash')], + [InlineKeyboardButton("โšก Clash Meta", callback_data=f'fmt_{idx}_clash')], + [InlineKeyboardButton("๐Ÿ“Ž Clash Meta ่ฎข้˜…URL", callback_data=f'fmt_{idx}_url')], ] await send_del(q.message, f'๐Ÿ“ {alive[idx]["name"]}\n้€‰ๆ‹ฉ่พ“ๅ‡บๆ ผๅผ:', reply_markup=InlineKeyboardMarkup(buttons)) @@ -198,10 +208,12 @@ async def cb_getsub(update: Update, context: ContextTypes.DEFAULT_TYPE): s = alive[idx] if fmt == 'raw': await send_del(q.message, s["link"]) - else: - # single node clash meta - generate inline + elif fmt == 'clash': cm = gen_clash_meta([s]) await send_del(q.message, f'```yaml\n{cm}\n```', parse_mode='Markdown') + elif fmt == 'url': + url = f'http://YOUR_SERVER:18888/{SUB_SECRET}/download?target=ClashMeta' + await send_del(q.message, f'๐Ÿ“Ž ่ฎข้˜…URL:\n{url}') return if action.startswith('send_'): @@ -223,7 +235,7 @@ async def cb_getsub(update: Update, context: ContextTypes.DEFAULT_TYPE): proto, fmt = parts[1], parts[2] subs = alive if proto == 'all' else [s for s in alive if s['type'] == proto] if fmt == 'clash': - url = f'http://{SUB_HOST}/{SUB_SECRET}/download?target=ClashMeta' + url = f'http://YOUR_SERVER:18888/{SUB_SECRET}/download?target=ClashMeta' if proto != 'all': url += f'&type={proto}' return await send_del(q.message, f'๐Ÿ“Ž Clash Meta ่ฎข้˜…้“พๆŽฅ:\n{url}') links = '\n'.join(s['link'] for s in subs) @@ -237,7 +249,7 @@ async def cb_getsub(update: Update, context: ContextTypes.DEFAULT_TYPE): return if action == 'get_all_clash': - url = f'http://{SUB_HOST}/{SUB_SECRET}/download?target=ClashMeta' + url = 'http://YOUR_SERVER:18888/{SUB_SECRET}/download?target=ClashMeta' await send_del(q.message, f'๐Ÿ“Ž Clash Meta ่ฎข้˜…้“พๆŽฅ:\n{url}') return @@ -270,6 +282,71 @@ async def cb_getsub(update: Update, context: ContextTypes.DEFAULT_TYPE): else: await send_del(q.message, links) +async def cb_multisel(update: Update, context: ContextTypes.DEFAULT_TYPE): + q = update.callback_query + await q.answer() + data = load_data() + alive = [s for s in data['subs'] if s.get('alive', True)] + sel = context.user_data.get('multi_sel', set()) + action = q.data + + if action == 'msel_done': + if not sel: + return await send_del(q.message, 'โš ๏ธ ๆœช้€‰ๆ‹ฉไปปไฝ•่Š‚็‚น') + chosen = [alive[i] for i in sorted(sel) if i < len(alive)] + if not chosen: + return await send_del(q.message, 'โš ๏ธ ้€‰ๆ‹ฉๅทฒๅคฑๆ•ˆ') + context.user_data['multi_sel'] = set() + names = ', '.join(s['name'] for s in chosen) + buttons = [ + [InlineKeyboardButton("๐Ÿ”— ๅŽŸๅง‹้“พๆŽฅ", callback_data='mout_raw')], + [InlineKeyboardButton("โšก Clash Meta", callback_data='mout_clash')], + [InlineKeyboardButton("๐Ÿ“Ž ่ฎข้˜…URL", callback_data='mout_url')], + [InlineKeyboardButton("๐Ÿ“„ Base64", callback_data='mout_b64')], + ] + context.user_data['multi_chosen'] = chosen + await send_del(q.message, f'ๅทฒ้€‰ {len(chosen)} ไธช: {names}\n้€‰ๆ‹ฉ่พ“ๅ‡บๆ ผๅผ:', + reply_markup=InlineKeyboardMarkup(buttons)) + return + + idx = int(action.split('_')[1]) + if idx in sel: + sel.discard(idx) + else: + sel.add(idx) + context.user_data['multi_sel'] = sel + + buttons = [] + for i, s in enumerate(alive): + mark = 'โ˜‘' if i in sel else 'โ˜' + buttons.append([InlineKeyboardButton(f"{mark} {s['name']}", callback_data=f'msel_{i}')]) + buttons.append([InlineKeyboardButton(f"โœ… ็กฎ่ฎค ({len(sel)})", callback_data='msel_done')]) + try: + await q.message.edit_text('๐ŸŽฏ ็‚นๅ‡ป้€‰ๆ‹ฉ่Š‚็‚น:', reply_markup=InlineKeyboardMarkup(buttons)) + except: pass + +async def cb_multiout(update: Update, context: ContextTypes.DEFAULT_TYPE): + q = update.callback_query + await q.answer() + chosen = context.user_data.get('multi_chosen', []) + if not chosen: + return await send_del(q.message, 'โš ๏ธ ้€‰ๆ‹ฉๅทฒ่ฟ‡ๆœŸ๏ผŒ่ฏท้‡ๆ–ฐๆ“ไฝœ') + fmt = q.data.split('_')[1] + if fmt == 'raw': + links = '\n'.join(s['link'] for s in chosen) + await send_del(q.message, links) + elif fmt == 'clash': + cm = gen_clash_meta(chosen) + await send_del(q.message, f'```yaml\n{cm}\n```', parse_mode='Markdown') + elif fmt == 'url': + names = ','.join(urllib.request.quote(s['name']) for s in chosen) + url = f'http://YOUR_SERVER:18888/{SUB_SECRET}/download?target=ClashMeta&name={names}' + await send_del(q.message, f'๐Ÿ“Ž ่ฎข้˜…URL:\n{url}') + elif fmt == 'b64': + links = '\n'.join(s['link'] for s in chosen) + await send_del(q.message, f'```\n{base64.b64encode(links.encode()).decode()}\n```', parse_mode='Markdown') + context.user_data.pop('multi_chosen', None) + async def cb_delsub(update: Update, context: ContextTypes.DEFAULT_TYPE): q = update.callback_query await q.answer() @@ -336,13 +413,25 @@ async def check_node(link, proto): def parse_sp(link, proto): try: - if proto == 'ss': + if proto == "ss": raw = link[5:] - if '#' in raw: raw = raw.split('#')[0] - decoded = base64.b64decode(raw + '==').decode() - _, rest = decoded.split(':', 1) - _, sp = rest.rsplit('@', 1) - return sp.rsplit(':', 1) + if "#" in raw: raw = raw.split("#")[0] + if "@" in raw: + sp = raw.split("@")[-1] + return sp.rsplit(":", 1) + try: + decoded = base64.b64decode(raw + "==").decode() + _, rest = decoded.split(":", 1) + _, sp = rest.rsplit("@", 1) + return sp.rsplit(":", 1) + except: pass + + + + + + + m = re.search(r'@([^:/?#]+):(\d+)', link) if m: return m.group(1), m.group(2) except: pass @@ -354,10 +443,20 @@ def parse_to_clash_proxy(s): raw = s['link'][5:] name = '' if '#' in raw: raw, name = raw.rsplit('#', 1); name = urllib.request.unquote(name) - decoded = base64.b64decode(raw + '==').decode() - method, rest = decoded.split(':', 1) - pwd, sp = rest.rsplit('@', 1) - server, port = sp.rsplit(':', 1) + if "@" in raw: + userinfo, sp = raw.rsplit("@", 1) + server, port = sp.rsplit(":", 1) + decoded = base64.b64decode(userinfo + "==").decode() + method, pwd = decoded.split(":", 1) + else: + decoded = base64.b64decode(raw + "==").decode() + method, rest = decoded.split(":", 1) + pwd, sp = rest.rsplit("@", 1) + server, port = sp.rsplit(":", 1) + + + + return {'name': name or server, 'type': 'ss', 'server': server, 'port': int(port), 'cipher': method, 'password': pwd} except: pass @@ -385,6 +484,10 @@ class SubHandler(BaseHTTPRequestHandler): qs = dict(x.split('=',1) for x in self.path.split('?',1)[1].split('&') if '=' in x) ftype = qs.get('type', '') if ftype: alive = [s for s in alive if s['type'] == ftype] + fname = qs.get('name', '') + if fname: + names = [urllib.request.unquote(n) for n in fname.split(',')] + alive = [s for s in alive if s['name'] in names] target = qs.get('target', 'ClashMeta') body = gen_clash_meta(alive) if target == 'ClashMeta' else '\n'.join(s['link'] for s in alive) self.send_response(200) @@ -445,6 +548,8 @@ def main(): app.add_handler(CommandHandler('setgroup', cmd_setgroup)) app.add_handler(CallbackQueryHandler(cb_menu, pattern='^menu_')) app.add_handler(CallbackQueryHandler(cb_delsub, pattern='^del_')) + app.add_handler(CallbackQueryHandler(cb_multisel, pattern='^msel_')) + app.add_handler(CallbackQueryHandler(cb_multiout, pattern='^mout_')) app.add_handler(CallbackQueryHandler(cb_getsub, pattern='^(get_|pick_|send_|fmt_|out_)')) app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, auto_detect)) log.info('Sub Bot starting polling...')