├── .gitignore ├── Dockerfile ├── README.md ├── bot.py ├── config.yaml ├── db └── cert │ └── cetrificates.here ├── docker-compose.yaml ├── link_manager.py ├── main.py ├── requirements.txt ├── scheduler.py ├── scripts ├── base.json ├── count.py ├── generate_urls.py ├── get_username.py ├── nginx.conf └── traffic.sh ├── sqlitedb.py ├── subscribe.py ├── traffic_manager.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | db/* 2 | config.yaml 3 | *.code-workspace 4 | *.pyc 5 | *.db 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | COPY requirements.txt . 3 | RUN pip3 install -r requirements.txt 4 | WORKDIR /workspace 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xray-Bot 2 | 3 | ## Getting Started 4 | 5 | These instructions will guide you through the setup process of Xray-Bot. 6 | 7 | ### Prerequisites 8 | 9 | ### Steps 10 | 0. Install docker by: 11 | `curl -fsSL get.docker.com -o get-docker.sh && sh get-docker.sh` 12 | 13 | 1. Generate 3000 UUIDs by running the following command: 14 | `cd scripts & python3 generate_urls.py` 15 | 2. customize your `config.yaml` file. 16 | 3. Run the Xray-Bot container using docker-compose: 17 | `docker-compose up --build -d` 18 | 19 | Note: Make sure your firewall is set up properly and your port (default 443) is open. -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import logging 4 | import requests 5 | from telegram import __version__ as TG_VER 6 | from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update, WebAppInfo 7 | from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes, MessageHandler, filters 8 | import yaml 9 | from utils import load_config 10 | import argparse 11 | import datetime 12 | import random 13 | import asyncio 14 | 15 | from link_manager import LinkManager 16 | from utils import get_outline_key 17 | 18 | 19 | # Enable logging 20 | logging.basicConfig( 21 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO) 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | async def instructions(update: Update, context: ContextTypes.DEFAULT_TYPE): 26 | await context.bot.send_message(chat_id=update.effective_chat.id, text="""Download the applications below and import the generated link. 27 | Android: V2rayNG 28 | IOS: wingsX 29 | Windows: v2rayN 30 | MacOS: V2RayXS """, parse_mode="HTML") 31 | 32 | 33 | async def gen_link(update: Update, context: ContextTypes.DEFAULT_TYPE, type): 34 | maintenance = await is_maintenance(update, context) 35 | if maintenance: 36 | return 37 | 38 | if not update.effective_user.username: 39 | await context.bot.send_message(chat_id=update.effective_chat.id, text="Please set a username for your telegram account.") 40 | return 41 | 42 | if not await is_member(update, context, send_thank_you=False): 43 | return 44 | 45 | if type == 'reality': 46 | ret, urls = link_manager.get_link_reality( 47 | str(update.effective_user.id), str(update.effective_user.username)) 48 | logger.info(f'REALITY, Gave link to @{update.effective_user.username}') 49 | 50 | if ret: 51 | for (link_type, url) in urls.items(): 52 | if url: 53 | print(url) 54 | await context.bot.send_message(chat_id=update.effective_chat.id, text=link_type) 55 | 56 | text = "`" + '\n'.join(url) + "`" 57 | await context.bot.send_message(chat_id=update.effective_chat.id, text=text, parse_mode="MarkdownV2") 58 | else: 59 | await context.bot.send_message(chat_id=update.effective_chat.id, text=urls[0]) 60 | 61 | 62 | async def is_maintenance(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool: 63 | config = load_config(config_path) 64 | if config['maintenance']: 65 | await context.bot.send_message(chat_id=update.effective_chat.id, text="Sorry. The bot is under maintanance right now.") 66 | await context.bot.send_message(chat_id=update.effective_chat.id, text=".در حال ارتقا ربات هستیم. ربات بصورت موقتی غیرفعال است.") 67 | # await context.bot.send_message(chat_id=update.effective_chat.id, text="در حال حاضر سرور پر شده است. ") 68 | 69 | return config['maintenance'] 70 | 71 | 72 | async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: 73 | """Sends a message with three inline buttons attached.""" 74 | maintenance = await is_maintenance(update, context) 75 | if maintenance: 76 | return 77 | 78 | keyboard = [[ InlineKeyboardButton("REALITY", callback_data="gen_reality")], 79 | [InlineKeyboardButton("گزارش استفاده", callback_data="usage")], 80 | [InlineKeyboardButton( 81 | "Outline VPN گرفتن لینک", callback_data="gen_outline")], 82 | [InlineKeyboardButton( 83 | "رو چه نرم افزاری کار میکنه؟", callback_data="instructions")], 84 | [InlineKeyboardButton( 85 | "می خواهم کمک کنم", url="https://t.me/+0l8_7FaM-UkyNzIx", callback_data="contribute")], 86 | [InlineKeyboardButton( 87 | "لینک کانال", url="https://t.me/WomanLifeFreedomVPN", callback_data="contact_support")], 88 | [InlineKeyboardButton("تست سرعت", web_app=WebAppInfo(url="https://pcmag.speedtestcustom.com"))]] 89 | reply_markup = InlineKeyboardMarkup(keyboard) 90 | 91 | await update.message.reply_text("Please choose one of the following options:", reply_markup=reply_markup) 92 | 93 | 94 | async def gen_report(update: Update, context: ContextTypes.DEFAULT_TYPE): 95 | usage = link_manager.get_usage(update.effective_user.id) 96 | await context.bot.send_message(chat_id=update.effective_chat.id, text=usage) 97 | 98 | async def get_sub(update: Update, context: ContextTypes.DEFAULT_TYPE): 99 | maintenance = await is_maintenance(update, context) 100 | if maintenance: 101 | return 102 | 103 | if not await is_member(update, context): 104 | return 105 | 106 | if not update.effective_user.username: 107 | await context.bot.send_message(chat_id=update.effective_chat.id, text="Please set a username for your telegram account.") 108 | return 109 | 110 | logging.info( 111 | f'Subscription, Gave sub to @{update.effective_user.username}') 112 | await context.bot.send_message(chat_id=update.effective_chat.id, text="لینک سابسکریپشن اگه اولی نشد دومی رو بزنید. هر دو یکین نیاز نیست هر دو رو بزنید.") 113 | link = '`'+link_manager.get_sub(str(update.effective_user.id),str(update.effective_user.username))[0]+'`' 114 | await context.bot.send_message(chat_id=update.effective_chat.id, text=link, parse_mode="MarkdownV2") 115 | link = '`'+link_manager.get_sub(str(update.effective_user.id),str(update.effective_user.username))[1]+'`' 116 | await context.bot.send_message(chat_id=update.effective_chat.id, text=link, parse_mode="MarkdownV2") 117 | 118 | 119 | async def gen_outline(update: Update, context: ContextTypes.DEFAULT_TYPE): 120 | if not await is_member(update, context): 121 | return 122 | maintenance = await is_maintenance(update, context) 123 | if maintenance: 124 | return 125 | 126 | await context.bot.send_message(chat_id=update.effective_chat.id, text="درحال برقراری ارتباط با سرور. لطفا چند دقیقه صبر کنید...") 127 | email = str(update.effective_user.id)+"@telegram.ca" 128 | logging.info(f'OUTLINE, Gave link to @{update.effective_user.username}') 129 | 130 | status, url = await get_outline_key(email) 131 | if status == 200: 132 | text = "`" + url + "`" 133 | await context.bot.send_message(chat_id=update.effective_chat.id, text=text, parse_mode="MarkdownV2") 134 | await context.bot.send_message(chat_id=update.effective_chat.id, text="حجم 4 گیگابایت. انقضا یک هفته.") 135 | await context.bot.send_message(chat_id=update.effective_chat.id, text="برای اطلاعات بیشتر به https://instagram.com/getoutlinevpnkey مراجعه کنید.") 136 | 137 | elif status == 226: 138 | await context.bot.send_message(chat_id=update.effective_chat.id, text="شما قبلا درخواست داده اید. لطفا یک هفته از درخواست قبلی صبر کنید.") 139 | 140 | elif status == 408: 141 | await context.bot.send_message(chat_id=update.effective_chat.id, text="ارتباط با سرور اوتلاین برقرار نشد. احتمالا سرور در حال تعمیر میباشد. برای اطلاعات بیشتر به https://instagram.com/getoutlinevpnkey مراجه کنید.") 142 | 143 | 144 | async def button(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: 145 | """Parses the CallbackQuery and updates the message text.""" 146 | query = update.callback_query 147 | 148 | # CallbackQueries need to be answered, even if no notification to the user is needed 149 | # Some clients may have trouble otherwise. See https://core.telegram.org/bots/api#callbackquery 150 | await query.answer() 151 | if query.data == "instructions": 152 | await instructions(update, context) 153 | elif query.data == "gen_trojan": 154 | await gen_link(update, context, 'trojan') 155 | elif query.data == "gen_vless": 156 | await gen_link(update, context, 'vless') 157 | elif query.data == "gen_reality": 158 | await gen_link(update, context, 'reality') 159 | elif query.data == "gen_vmess": 160 | await gen_link(update, context, 'vmess') 161 | elif query.data == "get_sub": 162 | await get_sub(update, context) 163 | elif query.data == "usage": 164 | await gen_report(update, context) 165 | elif query.data == "gen_outline": 166 | await gen_outline(update, context) 167 | 168 | 169 | async def is_member(update: Update, context: ContextTypes.DEFAULT_TYPE, send_thank_you=True): 170 | config = load_config(config_path) 171 | # Check if the client is a member of the specified channel 172 | user_id = update.effective_user.id # update.message.from_user.id # Get the client's user ID 173 | try: 174 | chat_member = await context.bot.get_chat_member(chat_id=config["telegram_channel_id"], user_id=user_id) 175 | if chat_member.status in ["member", "creator"]: 176 | if send_thank_you: 177 | await context.bot.send_message(chat_id=update.effective_chat.id, text="Thank you for subscribing to our channel!") 178 | else: 179 | await context.bot.send_message(chat_id=update.effective_chat.id, text=f"Please subscribe to our channel {config['telegram_channel_id']}.") 180 | await context.bot.send_message(chat_id=update.effective_chat.id, text="لطفا ابتدا عضو کانال شوید. این وی پی ان محدود به اعضای کانال می باشد.") 181 | return chat_member.status in ["member", "creator"] 182 | except: 183 | logger.error(f"Error in checking the members of the channel. Please make sure robot is admin to your channel {config['telegram_channel_id']}") 184 | return False 185 | 186 | def main() -> None: 187 | # Parse the config file path from the command line arguments 188 | parser = argparse.ArgumentParser() 189 | parser.add_argument( 190 | '--config_path', help='Path to the config file', default='config.yaml') 191 | args = parser.parse_args() 192 | global config_path 193 | config_path = args.config_path 194 | 195 | # Load the config file 196 | config = load_config(config_path) 197 | 198 | global link_manager 199 | link_manager = LinkManager(config) 200 | 201 | """Run the bot.""" 202 | # Create the Application and pass it your bot's token. 203 | application = Application.builder().token( 204 | config['telegram_bot_token']).build() 205 | 206 | application.add_handler(CommandHandler("start", start)) 207 | application.add_handler(CallbackQueryHandler(button)) 208 | 209 | # Run the bot until the user presses Ctrl-C 210 | application.run_polling() 211 | 212 | 213 | if __name__ == "__main__": 214 | main() 215 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | telegram_bot_token: "5737744160:AAHWNc3rnHF_uoOIdo5sNff-eUGhdk-tsvE" #change it 2 | maintenance: false #turn it to true to force the bot to go into maintanance mode. 3 | db_path: "db/database.db" 4 | max_users: 3000 5 | traffic_limit: 8716192768 #5 GB 6 | outline_ip: "100.25.47.59" 7 | telegram_channel_id: "@WomanLifeFreedomVPN" 8 | domain: "135.181.35.207" 9 | ip: "135.181.35.207" -------------------------------------------------------------------------------- /db/cert/cetrificates.here: -------------------------------------------------------------------------------- 1 | make a folder and put your certificates here. -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | core: 4 | image: teddysun/xray 5 | volumes: 6 | - $PWD/db/:/etc/xray 7 | - /dev/shm/:/dev/shm/ 8 | restart: always 9 | network_mode: host 10 | 11 | scheduler: 12 | build: . 13 | volumes: 14 | - $PWD:/workspace 15 | restart: always 16 | network_mode: host 17 | command: python scheduler.py 18 | 19 | bot: 20 | build: . 21 | volumes: 22 | - $PWD:/workspace 23 | command: python bot.py 24 | restart: always 25 | network_mode: host -------------------------------------------------------------------------------- /link_manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import logging 4 | import requests 5 | import json 6 | import base64 7 | 8 | from sqlitedb import SQLiteDB 9 | from utils import get_cf_ip, get_daily_number 10 | 11 | # Enable logging 12 | logging.basicConfig( 13 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO) 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class LinkManager: 18 | def __init__(self, config): 19 | self.db = SQLiteDB(config['db_path']) 20 | self.config = config 21 | self.domain = config['domain'] 22 | 23 | def register_id(self, telegram_id, telegram_username): 24 | if self.db.get_uuid(telegram_id): 25 | return True 26 | else: 27 | if int(self.db.count_users_with_telegram_id()) > self.config['max_users']: 28 | return False 29 | 30 | self.db.register_telegram_id( 31 | telegram_id=telegram_id, telegram_username=telegram_username, traffic_limit=self.config['traffic_limit']) 32 | return True 33 | 34 | def get_link_trojan(self, telegram_id, telegram_username): 35 | if not self.register_id(telegram_id, telegram_username): 36 | return False, ["Server is full."] 37 | 38 | user_uuid = self.db.get_uuid(telegram_id) 39 | if user_uuid is None: 40 | return False, ["Error connetion to server database."] 41 | 42 | # sni_id = telegram_id 43 | sni_id = get_daily_number() 44 | 45 | urls = {} 46 | urls['Trojan-WS'] = self.trojan_ws_cdn(user_uuid, sni_id) 47 | urls['Trojan-gRPC'] = self.trojan_grpc_cdn(user_uuid, sni_id) 48 | return True, urls 49 | 50 | def get_link_reality(self,telegram_id,telegram_username): 51 | if not self.register_id(telegram_id, telegram_username): 52 | return False, ["Server is full."] 53 | 54 | user_uuid = self.db.get_uuid(telegram_id) 55 | if user_uuid is None: 56 | return False, ["Error connetion to server database."] 57 | 58 | urls = {} 59 | sni_id = 0 60 | urls['VLESS-REALITY'] = [self.vless_xtls_reality(user_uuid, sni_id)] 61 | return True, urls 62 | 63 | def get_link_vless(self, telegram_id, telegram_username): 64 | if not self.register_id(telegram_id, telegram_username): 65 | return False, ["Server is full."] 66 | 67 | user_uuid = self.db.get_uuid(telegram_id) 68 | if user_uuid is None: 69 | return False, ["Error connetion to server database."] 70 | 71 | # sni_id = telegram_id 72 | sni_id = get_daily_number() 73 | 74 | urls = {} 75 | urls['VLESS-WS'] = self.vless_ws_cdn(user_uuid, sni_id) 76 | urls['VLESS-gRPC'] = self.vless_grpc_cdn(user_uuid, sni_id) 77 | return True, urls 78 | 79 | def get_link_vmess(self, telegram_id, telegram_username): 80 | if not self.register_id(telegram_id, telegram_username): 81 | return False, ["Server is full."] 82 | 83 | user_uuid = self.db.get_uuid(telegram_id) 84 | if user_uuid is None: 85 | return False, ["Error connetion to server database."] 86 | 87 | # sni_id = telegram_id 88 | sni_id = get_daily_number() 89 | 90 | urls = {} 91 | urls['VMess-WS'] = self.vmess_ws_cdn(user_uuid, sni_id) 92 | # urls['VMess-gRPC'] = self.vmess_grpc_cdn(user_uuid, sni_id) 93 | return True, urls 94 | 95 | def trojan_grpc_cdn(self, uuid, sni_id): 96 | url = f"trojan://{uuid}@{self.subdomain_preffix}{sni_id}.{self.domain}:443?security=tls&type=grpc&serviceName=trgrpc&mode=gun&alpn=h2&sni={self.subdomain_preffix}{sni_id}.{self.domain}#@WLF-TgRPC" 97 | urls = LinkManager.alternate_vless_trojan(url) 98 | return urls 99 | 100 | def trojan_ws_cdn(self, uuid, sni_id): 101 | url = f"trojan://{uuid}@{self.subdomain_preffix}{sni_id}.{self.domain}:443?security=tls&alpn=http%2F1.1&type=ws&path=%2Ftrojanws%3Fed%3D2048#@WLF-Tws" 102 | urls = LinkManager.alternate_vless_trojan(url) 103 | return urls 104 | 105 | def vless_grpc_cdn(self, uuid, sni_id): 106 | url = f"vless://{uuid}@{self.subdomain_preffix}{sni_id}.{self.domain}:443?encryption=none&security=tls&type=grpc&alpn=h2&sni={self.subdomain_preffix}{sni_id}.{self.domain}&serviceName=vlgrpc&mode=gun#@WLF-VLESSgRPC" 107 | 108 | urls = LinkManager.alternate_vless_trojan(url) 109 | return urls 110 | 111 | def vless_ws_cdn(self, uuid, sni_id): 112 | url = f"vless://{uuid}@{self.subdomain_preffix}{sni_id}.{self.domain}:443?encryption=none&alpn=http%2F1.1&security=tls&type=ws&path=%2Fvlws%3Fed%3D2048#@WLF-VLESSws" 113 | urls = LinkManager.alternate_vless_trojan(url) 114 | return urls 115 | 116 | def vless_xtls_reality(self, uuid, sni_id): 117 | import random 118 | import string 119 | spx = ''.join(random.choices(string.ascii_lowercase, k=7))+'/'+''.join(random.choices(string.ascii_lowercase, k=5)) 120 | url = f"vless://{uuid}@{self.domain}:443?encryption=none&flow=xtls-rprx-vision&security=reality&sni=www.yahoo.com&fp=chrome&pbk=GAUjNf1wQnDm5ziCGqRmb3yVfU9bg_UPwZ2_QU4JWRU&sid=6bb85179e30d4fc2&spx=%2F{spx}&type=tcp&headerType=none#@WomanLifeFreedomVPN" 121 | return url 122 | 123 | def vmess_ws_cdn(self, uuid, sni_id): 124 | data = {"v": "2", "ps": "@WLF-VMessws", "add": f"{self.subdomain_preffix}{sni_id}.{self.domain}", "port": "443", "id": uuid, "aid": "0", "scy": "none", 125 | "net": "ws", "type": "none", "host": "", "path": "/vmws", "tls": "tls", "sni": "", "alpn": "http/1.1"} 126 | 127 | json_string = json.dumps(data) 128 | base64_encoded_string = base64.b64encode(json_string.encode()).decode() 129 | url = f"vmess://{base64_encoded_string}" 130 | urls = LinkManager.alternate_vmess( 131 | url, use_cf_ip=self.config['use_cf_ip']) 132 | return urls 133 | 134 | def vmess_grpc_cdn(self, uuid, sni_id): 135 | data = {"v": "2", "ps": "@WLF-VMessgRPC", "add": f"{self.subdomain_preffix}{sni_id}.{self.domain}", "port": "443", "id": uuid, "aid": "0", "scy": "none", 136 | "net": "grpc", "type": "http", "host": "", "path": "vmgrpc", "tls": "tls", "sni": f"{self.subdomain_preffix}{sni_id}.{self.domain}", "alpn": "h2"} 137 | json_string = json.dumps(data) 138 | base64_encoded_string = base64.b64encode(json_string.encode()).decode() 139 | url = f"vmess://{base64_encoded_string}" 140 | urls = LinkManager.alternate_vmess( 141 | url, use_cf_ip=self.config['use_cf_ip']) 142 | return urls 143 | 144 | @staticmethod 145 | def alternate_vless_trojan(url, use_cf_ip=True): 146 | import re 147 | 148 | # get address 149 | addr_match = re.search(r'@([^:]+):', url) 150 | if addr_match: 151 | address = addr_match.group(1) 152 | else: 153 | return 154 | # get host and add it 155 | host = None 156 | match_host = re.search(r'host=([^&#]+)', url) 157 | if match_host: 158 | host = match_host.group(1) 159 | else: 160 | host = address 161 | url_parts = url.split("#") 162 | url = url_parts[0] + f"&host={host}#" + url_parts[1] 163 | 164 | # get sni and add it 165 | match_sni = re.search(r'sni=([^&#]+)', url) 166 | if not match_sni: 167 | url_parts = url.split("#") 168 | url = url_parts[0] + f"&sni={host}#" + url_parts[1] 169 | 170 | cf_ips = [{"NAME": "iranserver.com", "IP": "iranserver.com", "TIME": None, "DESC": "2"}, {"NAME": "45.85.118.88", "IP": "45.85.118.88", "TIME": None, "DESC": "Irancell"}, { 171 | "NAME": "45.85.118.48", "IP": "45.85.118.48", "TIME": None, "DESC": "Irancell2"}, {"NAME": "104.16.219.215", "IP": "104.16.219.215", "TIME": None, "DESC": "Irancell3"}] 172 | 173 | if use_cf_ip: 174 | cf_good_ips = get_cf_ip() 175 | cf_ips += cf_good_ips 176 | 177 | urls = [url] 178 | for cf_ip in cf_ips: 179 | # if cf_ip["NAME"] in ["IRC"]: 180 | # continue 181 | if cf_ip["IP"]: 182 | new_url = url.replace(address, cf_ip["IP"], 1) 183 | url_parts = new_url.split("#") 184 | new_url += f'-{cf_ip["DESC"]}' 185 | urls.append(new_url) 186 | return urls 187 | 188 | @staticmethod 189 | def alternate_vmess(url, use_cf_ip=True): 190 | cf_ips = [] 191 | cf_ips = [{"NAME": "iranserver.com", 192 | "IP": "iranserver.com", "TIME": None, "DESC": "2"}] 193 | if use_cf_ip: 194 | cf_good_ips = get_cf_ip() 195 | cf_ips += cf_good_ips 196 | urls = [url] 197 | url = url[8:] 198 | json_string = base64.b64decode(url).decode() 199 | data = json.loads(json_string) 200 | for cf_ip in cf_ips: 201 | if cf_ip["NAME"] not in ["MCI", "IRC"]: 202 | continue 203 | 204 | data_new = dict(data) 205 | data_new['sni'] = data['add'] 206 | data_new['host'] = data['add'] 207 | 208 | if cf_ip["IP"]: 209 | data_new['add'] = cf_ip["IP"] 210 | data_new['ps'] += f'-{cf_ip["DESC"]}' 211 | json_string = json.dumps(data_new) 212 | base64_encoded_string = base64.b64encode( 213 | json_string.encode()).decode() 214 | new_url = f'vmess://{base64_encoded_string}' 215 | urls.append(new_url) 216 | return urls 217 | 218 | def get_sub(self, telegram_id, telegram_username): 219 | """ 220 | Returns the link to your subscription. 221 | """ 222 | if not self.register_id(telegram_id, telegram_username): 223 | return ["Server is full.", "Server is full."] 224 | 225 | user_uuid = self.db.get_uuid(telegram_id) 226 | if user_uuid is None or user_uuid == "None": 227 | return [f"Bad request. Please Contact admin and provide this error: {user_uuid}", f"Bad request. Please Contact admin and provide this error: {user_uuid}"] 228 | 229 | url = [f"https://{self.config['subscription']['domain']}:{self.config['subscription']['https_port']}/subscriptions?token={user_uuid}", 230 | f"http://{self.config['subscription']['domain']}:{self.config['subscription']['http_port']}/subscriptions?token={user_uuid}"] 231 | return url 232 | 233 | def get_usage(self, telegram_id): 234 | usage = self.db.get_usage(telegram_id) 235 | return f"Used {round(usage/1024**3,2)}/{round(self.config['traffic_limit']/1024**3,2)} GB" 236 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | from utils import load_config 5 | 6 | config = load_config('config.yaml') 7 | byobu_command = f"byobu new-session -d -s mysession 'python3 bot.py' \\; new-window -t mysession 'python3 scheduler.py' \\; new-window -t mysession 'uvicorn subscribe:app --reload --host {config['subscription']['ip']} --ssl-keyfile db/cert/wlfvip.au1.store/sub-private.key --ssl-certfile /root/xray-bot/db/cert/wlfvip.au1.store/sub-cert.crt --port {config['subscription']['https_port']}'\\; new-window -t mysession 'uvicorn subscribe:app --reload --host {config['subscription']['ip']} --port {config['subscription']['http_port']}'" 8 | 9 | os.system(byobu_command) 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tqdm 2 | schedule 3 | xtlsapi 4 | pyyaml 5 | requests 6 | python-telegram-bot -------------------------------------------------------------------------------- /scheduler.py: -------------------------------------------------------------------------------- 1 | from utils import add_cloudflare_record, load_config 2 | import schedule 3 | import time 4 | import logging 5 | import datetime 6 | 7 | from utils import get_daily_number 8 | from traffic_manager import TrafficManager 9 | # Summary: 10 | # This code sets up a scheduler to run every day at midnight and 1 minute after midnight. 11 | # It calls the add_cloudflare_record function with config from config.yaml file and today_number or tomorrow_number as arguments. 12 | # The scheduler runs indefinitely until stopped with run_pending and sleep statements. 13 | # Logging is also enabled to log information during execution. 14 | 15 | 16 | # Enable logging 17 | logging.basicConfig( 18 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO) 19 | logger = logging.getLogger(__name__) 20 | 21 | def next_monday(): 22 | today = datetime.datetime.today() 23 | next_monday = today + datetime.timedelta((7 - today.weekday()) % 7) 24 | delta = next_monday - today 25 | return delta.total_seconds() 26 | 27 | def update_cf(cf_config): 28 | add_cloudflare_record(config=cf_config, number_str=get_daily_number(delta=0)) 29 | add_cloudflare_record(config=cf_config, number_str=get_daily_number(delta=1)) 30 | 31 | config = load_config('config.yaml') 32 | logger.info("Scheduler started") 33 | today_number = get_daily_number() 34 | tomorrow_number = get_daily_number(delta=1) 35 | traffic_manager = TrafficManager(config) 36 | # schedule.every().day.at("00:00").do(update_cf, cf_config=config['cloudflare']) 37 | 38 | schedule.every(1).minutes.do(traffic_manager.run) 39 | schedule.every().monday.at("00:00").do(traffic_manager.reset_traffic) 40 | 41 | for job in schedule.jobs: 42 | logger.info(" - " + str(job)) 43 | 44 | traffic_manager.run() 45 | # update_cf(config['cloudflare']) 46 | 47 | while True: 48 | schedule.run_pending() 49 | time.sleep(1) 50 | -------------------------------------------------------------------------------- /scripts/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "log":{ 3 | "access": "/etc/xray/access.log", 4 | "loglevel":"debug" 5 | }, 6 | "inbounds":[ 7 | { 8 | "listen": "0.0.0.0", 9 | "tag": "Vless-XTLS-reality", 10 | "port": 443, 11 | "protocol": "vless", 12 | "settings": { 13 | "clients": [ 14 | { 15 | "id" : "general" , 16 | "flow": "xtls-rprx-vision" 17 | } 18 | ], 19 | "decryption": "none" 20 | }, 21 | "streamSettings": { 22 | "network": "tcp", 23 | "security": "reality", 24 | "realitySettings": { 25 | "show": true, 26 | "dest": "www.yahoo.com:443", 27 | "xver": 0, 28 | "maxTimeDiff": 0, 29 | "minClientVer": "1.8.0", 30 | "serverNames": [ 31 | "www.yahoo.com", 32 | "www.varzesh3.com", 33 | "" 34 | ], 35 | "privateKey": "qM0mqldIHJIooZOs6YyrLFbLtb3bz-lLPRdJ5oa9CVs", 36 | "pubKey": "GAUjNf1wQnDm5ziCGqRmb3yVfU9bg_UPwZ2_QU4JWRU", 37 | "shortIds": [ 38 | "6bb85179e30d4fc2" 39 | ] 40 | } 41 | } 42 | }, 43 | 44 | { 45 | "listen":"127.0.0.1", 46 | "port":62789, 47 | "protocol":"dokodemo-door", 48 | "settings":{ 49 | "address":"127.0.0.1" 50 | }, 51 | "tag":"api", 52 | "sniffing":null 53 | } 54 | ], 55 | "routing":{ 56 | "domainStrategy":"IPIfNonMatch", 57 | "rules":[ 58 | { 59 | "inboundTag":[ 60 | "api" 61 | ], 62 | "outboundTag":"api", 63 | "type":"field" 64 | }, 65 | { 66 | "type":"field", 67 | "ip":[ 68 | "geoip:ir", 69 | "geoip:private" 70 | ], 71 | "outboundTag":"block" 72 | }, 73 | { 74 | "type":"field", 75 | "outboundTag":"block", 76 | "domain":[ 77 | "geosite:category-ir", 78 | "geosite:private", 79 | "domain:intrack.ir", 80 | "domain:divar.ir", 81 | "domain:irancell.ir", 82 | "domain:yooz.ir", 83 | "domain:iran-cell.com", 84 | "domain:irancell.i-r", 85 | "domain:shaparak.ir", 86 | "domain:learnit.ir", 87 | "domain:yooz.ir", 88 | "domain:baadesaba.ir", 89 | "domain:webgozar.ir" 90 | ] 91 | } 92 | ] 93 | }, 94 | "outbounds":[ 95 | { 96 | "protocol":"freedom", 97 | "tag":"direct" 98 | }, 99 | { 100 | "protocol":"blackhole", 101 | "tag":"block" 102 | } 103 | ], 104 | "api":{ 105 | "services":[ 106 | "HandlerService", 107 | "LoggerService", 108 | "StatsService" 109 | ], 110 | "tag":"api" 111 | }, 112 | "stats":{ 113 | 114 | }, 115 | "policy":{ 116 | "levels":{ 117 | "0":{ 118 | "statsUserUplink":true, 119 | "statsUserDownlink":true 120 | } 121 | }, 122 | "system":{ 123 | "statsInboundUplink":true, 124 | "statsInboundDownlink":true, 125 | "statsOutboundUplink":true, 126 | "statsOutboundDownlink":true 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /scripts/count.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import os 4 | repo_folder = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 | sys.path.append(repo_folder) 6 | 7 | from sqlitedb import SQLiteDB 8 | 9 | 10 | db = SQLiteDB(f'{repo_folder}/db/database.db') 11 | print(db.count_users_with_telegram_id()) -------------------------------------------------------------------------------- /scripts/generate_urls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import json 3 | import os 4 | import sys 5 | from tqdm import tqdm 6 | import uuid 7 | repo_folder = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 8 | sys.path.append(repo_folder) 9 | 10 | from sqlitedb import SQLiteDB 11 | 12 | """ 13 | This scripts generates a 10000 uuid connections for tags. 14 | """ 15 | ############################################################################# 16 | tags = ['Vless-XTLS-reality'] #,'vmess-grpc' 17 | quantity = 7000 18 | db_path = f'{repo_folder}/db/database.db' 19 | config_path = f"{repo_folder}/db/config.json" 20 | domain = 'cdir-*.wlfvip.shop' 21 | port = 443 22 | ############################################################################## 23 | 24 | # Open the file 25 | with open(f'{repo_folder}/scripts/base.json', 'r') as file: 26 | # Load the JSON data from the file 27 | data = json.load(file) 28 | db = SQLiteDB(db_path) 29 | 30 | for i in tqdm(range(quantity)): 31 | random_uuid = str(uuid.uuid4()) 32 | client = {'email':f'{random_uuid}', 'id': f'{random_uuid}' ,'password': f'{random_uuid}',"flow": "xtls-rprx-vision", 'level': 0} 33 | added = False 34 | for inbound in data['inbounds']: 35 | if 'listen' in inbound.keys() and inbound['listen'] in tags or 'tag' in inbound.keys() and inbound['tag'] in tags: 36 | inbound['settings']['clients'].append(client) 37 | added =True 38 | if added: 39 | db.add_entry(None, None, 1, random_uuid, domain, port, 0, None) 40 | 41 | with open(config_path, "w") as outfile: 42 | json.dump(data, outfile, indent=4) -------------------------------------------------------------------------------- /scripts/get_username.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import json 3 | import os 4 | import sys 5 | from tqdm import tqdm 6 | import uuid 7 | repo_folder = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 8 | sys.path.append(repo_folder) 9 | 10 | uuid = '' 11 | from sqlitedb import SQLiteDB 12 | db = SQLiteDB('/root/xray-bot/db/database.db') 13 | 14 | print(db.get_username(uuid)) -------------------------------------------------------------------------------- /scripts/nginx.conf: -------------------------------------------------------------------------------- 1 | # Restrict access to the website by IP or wrong domain name) and return 400 2 | server { 3 | listen unix:/dev/shm/h1.sock proxy_protocol default_server; 4 | listen unix:/dev/shm/h2c.sock http2 proxy_protocol default_server; 5 | set_real_ip_from unix:; 6 | real_ip_header proxy_protocol; 7 | server_name _; 8 | return 400; 9 | } 10 | 11 | 12 | # HTTP1 UDS listener 13 | server { 14 | listen unix:/dev/shm/h1.sock proxy_protocol; # HTTP/1.1 server monitor process and enable PROXY protocol reception 15 | set_real_ip_from unix:; 16 | real_ip_header proxy_protocol; 17 | 18 | server_name "~^cdir-.*\.wlfvip\.com"; 19 | 20 | location / { 21 | add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; # enable HSTS 22 | root /var/www/html; # Modify to the path of the WEB file stored by yourself (check the permissions) 23 | index index.html index2.html; 24 | } 25 | 26 | 27 | } 28 | # HTTP2 UDS listener 29 | server { 30 | listen unix:/dev/shm/h2c.sock http2 proxy_protocol ; # H2C server monitor process and enable PROXY protocol reception 31 | set_real_ip_from unix:; 32 | real_ip_header proxy_protocol; 33 | 34 | # server_name "~^cdir-.*\.wlfvip\.com|^wlfvip\.au1\.store"; 35 | server_name "~^cdir-.*\.wlfvip\.com"; 36 | 37 | 38 | # grpc settings 39 | grpc_read_timeout 1h; 40 | grpc_send_timeout 1h; 41 | grpc_set_header X-Real-IP $remote_addr; 42 | 43 | # Decoy website 44 | location / { 45 | add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; # enable HSTS 46 | root /var/www/html; # Modify to the path of the WEB file stored by yourself (check the permissions) 47 | index index.html index2.html; 48 | } 49 | 50 | location /trgrpc { #corresponds to serviceName in trojan-grpc config of xray 51 | # POST returns 404 when negotiation fails 52 | if ($request_method != "POST") { 53 | return 404; 54 | } 55 | client_body_buffer_size 1m; 56 | client_body_timeout 1h; 57 | client_max_body_size 0; 58 | grpc_pass grpc://127.0.0.1:3001; 59 | 60 | } 61 | 62 | location /vlgrpc { # corresponds to serviceName in vless-grpc config of xray 63 | # return 404 if HTTP Method is not POST 64 | if ($request_method != "POST") { 65 | return 404; 66 | } 67 | client_body_buffer_size 1m; 68 | client_body_timeout 1h; 69 | client_max_body_size 0; 70 | grpc_pass grpc://127.0.0.1:3002; 71 | 72 | } 73 | 74 | location /vmgrpc { # corresponds to serviceName in vmess-grpc config of xray 75 | # return 404 if HTTP Method is not POST 76 | if ($request_method != "POST") { 77 | return 404; 78 | } 79 | client_body_buffer_size 1m; 80 | client_body_timeout 1h; 81 | client_max_body_size 0; 82 | grpc_pass grpc://127.0.0.1:3003; 83 | 84 | } 85 | 86 | location /ssgrpc { # corresponds to serviceName in shadowsocks-grpc config of xray 87 | # return 404 if HTTP Method is not POST 88 | if ($request_method != "POST") { 89 | return 404; 90 | } 91 | client_body_buffer_size 1m; 92 | client_body_timeout 1h; 93 | client_max_body_size 0; 94 | grpc_pass grpc://127.0.0.1:3004; 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /scripts/traffic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | _APISERVER=127.0.0.1:62789 4 | _XRAY=xray 5 | 6 | apidata () { 7 | local ARGS= 8 | if [[ $1 == "reset" ]]; then 9 | ARGS="-reset=true" 10 | fi 11 | $_XRAY api statsquery --server=$_APISERVER "${ARGS}" \ 12 | | awk '{ 13 | if (match($1, /"name":/)) { 14 | f=1; gsub(/^"|link"|,$/, "", $2); 15 | split($2, p, ">>>"); 16 | printf "%s:%s->%s\t", p[1],p[2],p[4]; 17 | } 18 | else if (match($1, /"value":/) && f){ 19 | f = 0; 20 | gsub(/"/, "", $2); 21 | printf "%.0f\n", $2; 22 | } 23 | else if (match($0, /}/) && f) { f = 0; print 0; } 24 | }' 25 | } 26 | 27 | print_sum() { 28 | local DATA="$1" 29 | local PREFIX="$2" 30 | local SORTED=$(echo "$DATA" | grep "^${PREFIX}" | sort -r) 31 | local SUM=$(echo "$SORTED" | awk ' 32 | /->up/{us+=$2} 33 | /->down/{ds+=$2} 34 | END{ 35 | printf "SUM->up:\t%.0f\nSUM->down:\t%.0f\nSUM->TOTAL:\t%.0f\n", us, ds, us+ds; 36 | }') 37 | echo -e "${SORTED}\n${SUM}" \ 38 | | numfmt --field=2 --suffix=B --to=iec \ 39 | | column -t 40 | } 41 | 42 | DATA=$(apidata $1) 43 | echo "------------Inbound----------" 44 | print_sum "$DATA" "inbound" 45 | echo "-----------------------------" 46 | echo "------------Outbound----------" 47 | print_sum "$DATA" "outbound" 48 | echo "-----------------------------" 49 | echo 50 | echo "-------------User------------" 51 | print_sum "$DATA" "user" 52 | echo "-----------------------------" -------------------------------------------------------------------------------- /sqlitedb.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from datetime import datetime 3 | 4 | class SQLiteDB: 5 | """ 6 | SQLiteDB is a class that allows you to interact with a SQLite database using Python. 7 | It creates a table called 'users' with the following columns: 8 | telegram_id, telegram_username, enabled, date_created, uuid, server, port, usage, traffic_limit 9 | """ 10 | 11 | def __init__(self, db_file): 12 | """ 13 | Initializes the SQLiteDB class and creates the 'users' table if it does not already exist. 14 | 15 | Args: 16 | db_file (str): The file path for the SQLite database. 17 | """ 18 | self.conn = sqlite3.connect(db_file) 19 | c = self.conn.cursor() 20 | c.execute('''CREATE TABLE IF NOT EXISTS users ( 21 | telegram_id INTEGER, 22 | telegram_username TEXT, 23 | enabled INTEGER, 24 | date_created DATETIME, 25 | uuid TEXT, 26 | server TEXT, 27 | port INTEGER, 28 | usage INTEGER, 29 | traffic_limit INTEGER)''') 30 | self.conn.commit() 31 | 32 | def count_users_with_telegram_id(self): 33 | """ 34 | Counts the number of entries in the 'users' table where telegram_id is not NULL. 35 | """ 36 | c = self.conn.cursor() 37 | c.execute("SELECT COUNT(*) FROM users WHERE telegram_id IS NOT NULL") 38 | count = c.fetchone()[0] 39 | return count 40 | 41 | def add_entry(self, telegram_id, telegram_username, enabled, uuid, server, port, usage, traffic_limit): 42 | """ 43 | Adds a new entry to the 'users' table. 44 | 45 | Args: 46 | telegram_id (int): The Telegram ID of the user. 47 | telegram_username (str): The Telegram username of the user. 48 | enabled (int): A flag indicating whether the user is enabled (1) or disabled (0). 49 | uuid (str): A unique identifier for the user. 50 | server (str): The server that the user is connected to. 51 | port (int): The port that the user is connected to. 52 | usage (int): The usage of the user. 53 | traffic_limit (int): The traffic_limit of the user. 54 | """ 55 | c = self.conn.cursor() 56 | current_date = datetime.now().strftime('%Y-%m-%d') 57 | c.execute("INSERT INTO users VALUES (?,?,?,?,?,?,?,?,?)", (telegram_id, telegram_username, enabled, current_date, uuid, server, port, usage, traffic_limit)) 58 | self.conn.commit() 59 | 60 | def remove_entry(self, uuid): 61 | """ 62 | Removes an entry from the 'users' table based on the provided uuid. 63 | 64 | Args: 65 | uuid (str): The unique identifier of the user to be removed. 66 | """ 67 | c = self.conn.cursor() 68 | with self.conn: 69 | c.execute("DELETE FROM users WHERE uuid=?", (uuid,)) 70 | self.conn.commit() 71 | 72 | def register_telegram_id(self, telegram_id, telegram_username, traffic_limit): 73 | """ 74 | Registers a Telegram ID and username for a user, and updates the date_created and bandwidth_limit in the table. 75 | 76 | Args: 77 | telegram_id (int): The Telegram ID of the user. 78 | telegram_username (str): The Telegram username of the user. 79 | bandwidth_limit (int): The bandwidth limit of the user. 80 | """ 81 | 82 | c = self.conn.cursor() 83 | current_date = datetime.now().strftime('%Y-%m-%d') 84 | with self.conn: 85 | c.execute("UPDATE users SET telegram_id = ?, telegram_username = ?, date_created = ?, traffic_limit = ? WHERE telegram_id is NULL LIMIT 1", (telegram_id, telegram_username, current_date, traffic_limit)) 86 | 87 | def get_uuid(self, telegram_id): 88 | """ 89 | Retrieves the uuid of a user based on the provided Telegram ID. 90 | 91 | Args: 92 | telegram_id (int): The Telegram ID of the user. 93 | 94 | Returns: 95 | str: The uuid of the user, or None if no user was found with the provided Telegram ID. 96 | """ 97 | c = self.conn.cursor() 98 | c.execute("SELECT uuid FROM users WHERE telegram_id=?", (telegram_id,)) 99 | result = c.fetchone() 100 | if result: 101 | return result[0] 102 | else: 103 | return None 104 | 105 | def get_username(self, uuid): 106 | """ 107 | Retrieves the telegram_username of a user based on the provided uuid. 108 | 109 | Args: 110 | uuid (str): The uuid of the user. 111 | 112 | Returns: 113 | str: The telegram username of the user, or None if no user was found with the provided Telegram ID. 114 | """ 115 | c = self.conn.cursor() 116 | c.execute("SELECT telegram_id, telegram_username FROM users WHERE uuid=?", (uuid,)) 117 | result = c.fetchone() 118 | if result: 119 | return result 120 | else: 121 | return None,None 122 | 123 | def get_usage(self, telegram_id): 124 | """ 125 | Retrieves the usage of a user based on the provided telegram_id. 126 | 127 | Args: 128 | telegram_id (str): The telegram_id of the user. 129 | 130 | Returns: 131 | int: The usage of the user, or None if no user was found with the provided Telegram ID. 132 | """ 133 | c = self.conn.cursor() 134 | c.execute("SELECT usage FROM users WHERE telegram_id=?", (telegram_id,)) 135 | result = c.fetchone() 136 | if result: 137 | return result[0] 138 | else: 139 | return 0 140 | 141 | def add_usage(self, uuid, amount): 142 | c = self.conn.cursor() 143 | with self.conn: 144 | c.execute("UPDATE users SET usage = usage + ? WHERE uuid = ?", (amount, uuid)) 145 | 146 | def get_uuid_by_usage(self, traffic_limit): 147 | c = self.conn.cursor() 148 | c.execute("SELECT uuid FROM users WHERE usage > ?", (traffic_limit,)) 149 | result = c.fetchall() 150 | return result 151 | def get_all_uuid(self): 152 | c = self.conn.cursor() 153 | c.execute("SELECT uuid FROM users") 154 | result = c.fetchall() 155 | return result 156 | 157 | def reset_usage(self): 158 | c = self.conn.cursor() 159 | with self.conn: 160 | c.execute("UPDATE users SET usage = 0") 161 | 162 | if __name__=="__main__": 163 | table = SQLiteDB('database_test.db') 164 | for i in range(10): 165 | table.add_entry(None, None, 1, str(i), 'server1', 443, 0, None) 166 | 167 | table.register_telegram_id(98765, 'new_username',9999) 168 | table.register_telegram_id(9000, 'new_username2',9999) 169 | table.register_telegram_id(10200, 'new_username3',9999) 170 | 171 | 172 | uuid = table.get_uuid(12345) 173 | print(uuid) 174 | uuid = table.get_uuid(9000) 175 | print(uuid) 176 | print('count:',table.count_users_with_telegram_id()) -------------------------------------------------------------------------------- /subscribe.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, HTTPException 2 | from link_manager import LinkManager 3 | from utils import load_config 4 | import base64 5 | from fastapi.responses import PlainTextResponse 6 | from utils import get_daily_number, is_valid_uuid 7 | import random 8 | from sqlitedb import SQLiteDB 9 | 10 | 11 | # Create a FastAPI application object 12 | app = FastAPI() 13 | 14 | # Load the configuration from 'config.yaml' file 15 | config = load_config('config.yaml') 16 | 17 | # Create a LinkManager object using the loaded configuration 18 | link_manager = LinkManager(config) 19 | 20 | # A fake items database used for demonstration purposes 21 | db = SQLiteDB("db/database.db") 22 | 23 | 24 | # Define the API endpoint to retrieve subscription links 25 | @app.get("/subscriptions", response_class=PlainTextResponse) 26 | async def get_subscription(token: str = 0, s: int = 2): 27 | if not is_valid_uuid(token): 28 | return random.choice(["Dahanet service! :))", "Ey baba! Ajab giri kardima", "Khodaei chetor root mishe inkaro koni!", " Dada hichi in posht nist! talash nakn alaki!"]) 29 | # Store the 's' parameter as 'sni' variable 30 | 31 | if db.get_username(token)[0] is None: 32 | raise HTTPException(status_code=404, detail="Link Jadid Begir") 33 | sni = get_daily_number() 34 | 35 | # Create an empty list to store links 36 | links = [] 37 | 38 | # Get the links using the various methods provided by the LinkManager object 39 | links += link_manager.trojan_grpc_cdn(token, sni) 40 | links += link_manager.vless_grpc_cdn(token, sni) 41 | # links += link_manager.vmess_grpc_cdn(token, sni) 42 | links += link_manager.trojan_ws_cdn(token, sni) 43 | links += link_manager.vless_ws_cdn(token, sni) 44 | links += link_manager.vmess_ws_cdn(token, sni) 45 | 46 | # Encode the links into base64 format 47 | sub = base64.b64encode('\n'.join(links).encode("utf-8")) 48 | 49 | # Return the encoded links 50 | return sub -------------------------------------------------------------------------------- /traffic_manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import logging 4 | import os 5 | 6 | from sqlitedb import SQLiteDB 7 | from utils import get_cf_ip, get_daily_number 8 | from xtlsapi import XrayClient, utils, exceptions 9 | 10 | # Enable logging 11 | logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO) 12 | 13 | class TrafficManager: 14 | def __init__(self, config): 15 | self.logger = logging.getLogger("TrafficManager") 16 | 17 | self.db = SQLiteDB(config['db_path']) 18 | self.config = config 19 | self.xray_client = XrayClient('127.0.0.1', 62789) 20 | self.removed = [] 21 | 22 | def update_traffic(self): 23 | from collections import defaultdict 24 | usage = defaultdict(int) 25 | uuids = self.db.get_all_uuid() 26 | for uuid in uuids: 27 | uuid = uuid[0] 28 | dl = self.xray_client.get_client_download_traffic(uuid,reset=True) 29 | if dl is None: 30 | dl = 0 31 | upload = self.xray_client.get_client_upload_traffic(uuid,reset=True) 32 | if upload is None: 33 | upload = 0 34 | usage = dl + upload 35 | self.db.add_usage(uuid, usage) 36 | self.logger.info("Updated traffic.") 37 | 38 | def remove_users_higher(self): 39 | inbounds = ["Vless-XTLS-reality"] 40 | for inbound in inbounds: 41 | emails = self.db.get_uuid_by_usage(self.config['traffic_limit']) 42 | for email in emails: 43 | if email[0] in self.removed: 44 | continue 45 | try: 46 | self.removed.append(email[0]) 47 | self.xray_client.remove_client(inbound,email=email[0]) 48 | except Exception as e: 49 | self.logger.error(e) 50 | self.logger.info(f"{len(emails)} users used all traffic. {str([self.db.get_username(x)[1] for x in self.removed])}") 51 | 52 | def reset_traffic(self): 53 | try: 54 | self.db.reset_usage() 55 | self.xray_client.restart_logger() 56 | self.removed = [] 57 | os.system('docker restart xray-bot') 58 | self.logger.info("Traffic was reset.") 59 | 60 | 61 | except Exception as e: 62 | self.logger.error(e) 63 | 64 | def run(self): 65 | self.update_traffic() 66 | self.remove_users_higher() -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import logging 3 | # Enable logging 4 | logging.basicConfig( 5 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO) 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | def load_config(path='config.yaml'): 10 | """Load the configuration from the specified yaml file. 11 | 12 | Args: 13 | path (str, optional): The path of the configuration file. Defaults to 'config.yaml'. 14 | 15 | Returns: 16 | dict: The configuration data. 17 | """ 18 | with open(path, 'r') as stream: 19 | config = yaml.safe_load(stream) 20 | return config 21 | 22 | def get_cf_ip(url = "http://bot.sudoer.net/best.cf.iran"): 23 | """Get the list of nonblocked cloudflare internet service providers in Iran. 24 | 25 | Returns: 26 | list: A list of dictionaries containing provider name, IP, time and description. 27 | """ 28 | from collections import defaultdict 29 | import requests 30 | 31 | data = [] 32 | try: 33 | response = requests.get(url,timeout=1) 34 | data = response.text.strip().split('\n') 35 | except: 36 | pass 37 | net = defaultdict(str, {"MCI": "HamrahAval", "RTL": "Rightel", "AST": "Asiatek", "IRC": "Irancel", 38 | "SHT": "Shatel", "MKB": "Mokhaberat", "MBT": "Mobinnet", "ZTL": "Zitel", "PRS": "ParsOnline"}) 39 | data_list = [] 40 | for i in data: 41 | parts = i.split() 42 | if parts[0] not in net.keys(): 43 | continue 44 | if len(parts) >= 3: 45 | data_dict = {'NAME': parts[0], 'IP': parts[1], 46 | 'Time': parts[2], "DESC": net[parts[0]]} 47 | else: 48 | data_dict = {'NAME': parts[0], 49 | 'IP': None, 'Time': None, "DESC": None} 50 | data_list.append(data_dict) 51 | return data_list 52 | 53 | 54 | async def get_outline_key(user_id): 55 | """ 56 | This function retrieves the Outline key for a user. 57 | 58 | :param user_id: ID of the user. 59 | :type user_id: str 60 | :return: A tuple of HTTP status code and Outline key. 61 | :rtype: tuple (int, str) status code and the url 62 | """ 63 | config = load_config('config.yaml') 64 | import requests 65 | url = f"http://{config['outline_ip']}/get_outline_key?user_id={user_id}" 66 | try: 67 | response = requests.get(url, timeout = 15) 68 | except: 69 | return 408, "" 70 | 71 | return response.status_code, str(response.content).split('"')[1] 72 | 73 | 74 | def add_cloudflare_record(config, number_str): 75 | """ 76 | This function adds a record in Cloudflare. 77 | 78 | :param config: Configuration object containing the necessary authentication information. 79 | :type config: dict 80 | :param number_str: The string representation of a number to be added to the record name. 81 | :type number_str: str 82 | :return: None 83 | :rtype: None 84 | """ 85 | import requests 86 | import time 87 | 88 | # code for adding the record goes here 89 | auth_email = config['auth_email'] 90 | auth_key = config['auth_key'] 91 | zone_id = config['zone_id'] 92 | record_name = config['prefix_subdomain_name'] + number_str 93 | record_content = config['ip'] 94 | record_type = "A" 95 | 96 | headers = { 97 | "X-Auth-Email": auth_email, 98 | "X-Auth-Key": auth_key, 99 | "Content-Type": "application/json" 100 | } 101 | 102 | data = { 103 | "name": record_name, 104 | "type": record_type, 105 | "content": record_content, 106 | "proxied": True 107 | } 108 | 109 | url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records" 110 | 111 | response = requests.get(url, headers=headers) 112 | records = response.json()["result"] 113 | record_exists = False 114 | for record in records: 115 | if record["name"].split('.')[0] == record_name and record["type"] == record_type: 116 | record_exists = True 117 | break 118 | 119 | if record_exists: 120 | logging.info(f"Record A {record_name} already exists.") 121 | 122 | return 123 | 124 | retry = 0 125 | record_added = False 126 | 127 | while not record_added and retry < 300: 128 | response = requests.post(url, headers=headers, json=data) 129 | if response.status_code == 200: 130 | logging.info( 131 | f"Record A {record_name} successfully added to Cloudflare") 132 | record_added = True 133 | else: 134 | logging.info(f"Failed to add record A {record_name} to Cloudflare") 135 | time.sleep(1) 136 | retry += 1 137 | 138 | 139 | def get_daily_number(delta=0): 140 | import datetime 141 | number = datetime.datetime.now().date() + datetime.timedelta(days=delta) 142 | return number.strftime("%m%d") 143 | 144 | 145 | if __name__ == "__main__": 146 | config = load_config('config.yaml') 147 | tomorrow = get_daily_number(1) 148 | today = get_daily_number(0) 149 | add_cloudflare_record(config['cloudflare'], today) 150 | add_cloudflare_record(config['cloudflare'], tomorrow) 151 | 152 | 153 | def is_valid_uuid(val): 154 | import uuid 155 | try: 156 | uuid.UUID(str(val)) 157 | return True 158 | except ValueError: 159 | return False --------------------------------------------------------------------------------