Streamixph05/Rstream
0
1# app.py (THE REAL, FINAL, CLEAN, EASY-TO-READ FULL CODE)2 3import os4import asyncio5import secrets6import traceback7import uvicorn8import re9import logging10from contextlib import asynccontextmanager11 12from pyrogram import Client, filters, enums13from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, ChatMemberUpdated14from pyrogram.errors import FloodWait, UserNotParticipant15from fastapi import FastAPI, Request, HTTPException16from fastapi.middleware.cors import CORSMiddleware17from fastapi.responses import JSONResponse, StreamingResponse18from pyrogram.file_id import FileId19from pyrogram import raw20from pyrogram.session import Session, Auth21from fastapi.responses import HTMLResponse22from fastapi.templating import Jinja2Templates23import math24 25# Project ki dusri files se important cheezein import karo26from config import Config27from database import db28 29# =====================================================================================30# --- SETUP: BOT, WEB SERVER, AUR LOGGING ---31# =====================================================================================32 33@asynccontextmanager34async def lifespan(app: FastAPI):35 """36 Yeh function bot ko web server ke saath start aur stop karta hai.37 """38 print("--- Lifespan: Server chalu ho raha hai... ---")39 40 await db.connect()41 42 try:43 print("Starting main Pyrogram bot...")44 await bot.start()45 46 me = await bot.get_me()47 Config.BOT_USERNAME = me.username48 print(f"✅ Main Bot [@{Config.BOT_USERNAME}] safaltapoorvak start ho gaya.")49 50 # --- MULTI-CLIENT STARTUP ---51 multi_clients[0] = bot52 work_loads[0] = 053 await initialize_clients()54 55 print(f"Verifying storage channel ({Config.STORAGE_CHANNEL})...")56 await bot.get_chat(Config.STORAGE_CHANNEL)57 print("✅ Storage channel accessible hai.")58 59 if Config.FORCE_SUB_CHANNEL:60 try:61 print(f"Verifying force sub channel ({Config.FORCE_SUB_CHANNEL})...")62 await bot.get_chat(Config.FORCE_SUB_CHANNEL)63 print("✅ Force Sub channel accessible hai.")64 except Exception as e:65 print(f"!!! WARNING: Bot, Force Sub channel mein admin nahi hai. Error: {e}")66 67 try:68 await cleanup_channel(bot)69 except Exception as e:70 print(f"Warning: Channel cleanup fail ho gaya. Error: {e}")71 72 print("--- Lifespan: Startup safaltapoorvak poora hua. ---")73 74 except Exception as e:75 print(f"!!! FATAL ERROR: Bot startup ke dauraan error aa gaya: {traceback.format_exc()}")76 77 yield78 79 print("--- Lifespan: Server band ho raha hai... ---")80 if bot.is_initialized:81 await bot.stop()82 print("--- Lifespan: Shutdown poora hua. ---")83 84app = FastAPI(lifespan=lifespan)85templates = Jinja2Templates(directory="templates")86app.add_middleware(87 CORSMiddleware,88 allow_origins=["*"],89 allow_credentials=True,90 allow_methods=["*"],91 allow_headers=["*"],92)93 94# --- LOG FILTER: YEH SIRF /dl/ WALE LOGS KO CHUPAYEGA ---95class HideDLFilter(logging.Filter):96 def filter(self, record: logging.LogRecord) -> bool:97 # Agar log message mein "GET /dl/" hai, toh usse mat dikhao98 return "GET /dl/" not in record.getMessage()99 100# Uvicorn ke 'access' logger par filter lagao101logging.getLogger("uvicorn.access").addFilter(HideDLFilter())102# --- FIX KHATAM ---103 104bot = Client("SimpleStreamBot", api_id=Config.API_ID, api_hash=Config.API_HASH, bot_token=Config.BOT_TOKEN, in_memory=True)105multi_clients = {}; work_loads = {}; class_cache = {}106 107# =====================================================================================108# --- MULTI-CLIENT LOGIC ---109# =====================================================================================110 111class TokenParser:112 """ Environment variables se MULTI_TOKENs ko parse karta hai. """113 @staticmethod114 def parse_from_env():115 return {116 c + 1: t117 for c, (_, t) in enumerate(118 filter(lambda n: n[0].startswith("MULTI_TOKEN"), sorted(os.environ.items()))119 )120 }121 122async def start_client(client_id, bot_token):123 """ Ek naye client bot ko start karta hai. """124 try:125 print(f"Attempting to start Client: {client_id}")126 client = await Client(127 name=str(client_id), 128 api_id=Config.API_ID, 129 api_hash=Config.API_HASH,130 bot_token=bot_token, 131 no_updates=True, 132 in_memory=True133 ).start()134 work_loads[client_id] = 0135 multi_clients[client_id] = client136 print(f"✅ Client {client_id} started successfully.")137 except Exception as e:138 print(f"!!! CRITICAL ERROR: Failed to start Client {client_id} - Error: {e}")139 140async def initialize_clients():141 """ Saare additional clients ko initialize karta hai. """142 all_tokens = TokenParser.parse_from_env()143 if not all_tokens:144 print("No additional clients found. Using default bot only.")145 return146 147 print(f"Found {len(all_tokens)} extra clients. Starting them...")148 tasks = [start_client(i, token) for i, token in all_tokens.items()]149 await asyncio.gather(*tasks)150 151 if len(multi_clients) > 1:152 print(f"✅ Multi-Client Mode Enabled. Total Clients: {len(multi_clients)}")153 154# =====================================================================================155# --- HELPER FUNCTIONS ---156# =====================================================================================157 158def get_readable_file_size(size_in_bytes):159 if not size_in_bytes:160 return '0B'161 power = 1024162 n = 0163 power_labels = {0: 'B', 1: 'KB', 2: 'MB', 3: 'GB'}164 while size_in_bytes >= power and n < len(power_labels) - 1:165 size_in_bytes /= power166 n += 1167 return f"{size_in_bytes:.2f} {power_labels[n]}"168 169def mask_filename(name: str):170 if not name:171 return "Protected File"172 base, ext = os.path.splitext(name)173 metadata_pattern = re.compile(174 r'((19|20)\d{2}|4k|2160p|1080p|720p|480p|360p|HEVC|x265|BluRay|WEB-DL|HDRip)',175 re.IGNORECASE176 )177 match = metadata_pattern.search(base)178 if match:179 title_part = base[:match.start()].strip(' .-_')180 metadata_part = base[match.start():]181 else:182 title_part = base183 metadata_part = ""184 masked_title = ''.join(c if (i % 3 == 0 and c.isalnum()) else ('*' if c.isalnum() else c) for i, c in enumerate(title_part))185 return f"{masked_title} {metadata_part}{ext}".strip()186 187# =====================================================================================188# --- PYROGRAM BOT HANDLERS ---189# =====================================================================================190 191@bot.on_message(filters.command("start") & filters.private)192async def start_command(client: Client, message: Message):193 user_id = message.from_user.id194 user_name = message.from_user.first_name195 196 if len(message.command) > 1 and message.command[1].startswith("verify_"):197 unique_id = message.command[1].split("_", 1)[1]198 199 if Config.FORCE_SUB_CHANNEL:200 try:201 await client.get_chat_member(Config.FORCE_SUB_CHANNEL, user_id)202 except UserNotParticipant:203 channel_username = str(Config.FORCE_SUB_CHANNEL).replace('@', '')204 channel_link = f"https://t.me/{channel_username}"205 join_button = InlineKeyboardButton("📢 Join Channel", url=channel_link)206 retry_button = InlineKeyboardButton("✅ Joined", url=f"https://t.me/{Config.BOT_USERNAME}?start={message.command[1]}")207 keyboard = InlineKeyboardMarkup([[join_button], [retry_button]])208 await message.reply_text(209 "**You Must Join Our Channel To Get The Link!**\n\n"210 "__Join Channel & Click '✅ Joined'.__",211 reply_markup=keyboard, quote=True212 )213 return214 215 final_link = f"{Config.BASE_URL}/show/{unique_id}"216 reply_text = f"__✅ Verification Successful!\n\nCopy Link:__ `{final_link}`"217 button = InlineKeyboardMarkup([[InlineKeyboardButton("Open Link", url=final_link)]])218 await message.reply_text(reply_text, reply_markup=button, quote=True, disable_web_page_preview=True)219 220 else:221 reply_text = f"""222👋 **Hello, {user_name}!**223 224__Welcome To Sharing Box Bot. I Can Help You Create Permanent, Shareable Links For Your Files.__225 226**How To Use Me:**227 228__Just Send Or Forward Any File To Me And I will instantly give you a special link that you can share with anyone!__229"""230 await message.reply_text(reply_text)231 232async def handle_file_upload(message: Message, user_id: int):233 try:234 sent_message = await message.copy(chat_id=Config.STORAGE_CHANNEL)235 unique_id = secrets.token_urlsafe(8)236 await db.save_link(unique_id, sent_message.id)237 238 verify_link = f"https://t.me/{Config.BOT_USERNAME}?start=verify_{unique_id}"239 button = InlineKeyboardMarkup([[InlineKeyboardButton("Get Link Now", url=verify_link)]])240 241 await message.reply_text("__✅ File Uploaded!__", reply_markup=button, quote=True)242 except Exception as e:243 print(f"!!! ERROR: {traceback.format_exc()}"); await message.reply_text("Sorry, something went wrong.")244 245@bot.on_message(filters.private & (filters.document | filters.video | filters.audio))246async def file_handler(_, message: Message):247 await handle_file_upload(message, message.from_user.id)248 249@bot.on_chat_member_updated(filters.chat(Config.STORAGE_CHANNEL))250async def simple_gatekeeper(c: Client, m_update: ChatMemberUpdated):251 try:252 if(m_update.new_chat_member and m_update.new_chat_member.status==enums.ChatMemberStatus.MEMBER):253 u=m_update.new_chat_member.user254 if u.id==Config.OWNER_ID or u.is_self: return255 print(f"Gatekeeper: Kicking {u.id}"); await c.ban_chat_member(Config.STORAGE_CHANNEL,u.id); await c.unban_chat_member(Config.STORAGE_CHANNEL,u.id)256 except Exception as e: print(f"Gatekeeper Error: {e}")257 258async def cleanup_channel(c: Client):259 print("Gatekeeper: Running cleanup..."); allowed={Config.OWNER_ID,c.me.id}260 try:261 async for m in c.get_chat_members(Config.STORAGE_CHANNEL):262 if m.user.id in allowed: continue263 if m.status in [enums.ChatMemberStatus.ADMINISTRATOR,enums.ChatMemberStatus.OWNER]: continue264 try: print(f"Cleanup: Kicking {m.user.id}"); await c.ban_chat_member(Config.STORAGE_CHANNEL,m.user.id); await asyncio.sleep(1)265 except FloodWait as e: await asyncio.sleep(e.value)266 except Exception as e: print(f"Cleanup Error: {e}")267 except Exception as e: print(f"Cleanup Error: {e}")268 269# =====================================================================================270# --- FASTAPI WEB SERVER ---271# =====================================================================================272 273@app.get("/")274async def health_check():275 """276 This route provides a 200 OK response for uptime monitors.277 """278 return {"status": "ok", "message": "Server is healthy and running!"}279 280@app.get("/show/{unique_id}", response_class=HTMLResponse)281async def show_page(request: Request, unique_id: str):282 return templates.TemplateResponse(283 "show.html",284 {"request": request}285 )286 287@app.get("/api/file/{unique_id}", response_class=JSONResponse)288async def get_file_details_api(request: Request, unique_id: str):289 message_id = await db.get_link(unique_id)290 if not message_id:291 raise HTTPException(status_code=404, detail="Link expired or invalid.")292 main_bot = multi_clients.get(0)293 if not main_bot:294 raise HTTPException(status_code=503, detail="Bot is not ready.")295 try:296 message = await main_bot.get_messages(Config.STORAGE_CHANNEL, message_id)297 except Exception:298 raise HTTPException(status_code=404, detail="File not found on Telegram.")299 media = message.document or message.video or message.audio300 if not media:301 raise HTTPException(status_code=404, detail="Media not found in the message.")302 file_name = media.file_name or "file"303 safe_file_name = "".join(c for c in file_name if c.isalnum() or c in (' ', '.', '_', '-')).rstrip()304 mime_type = media.mime_type or "application/octet-stream"305 response_data = {306 "file_name": mask_filename(file_name),307 "file_size": get_readable_file_size(media.file_size),308 "is_media": mime_type.startswith(("video", "audio")),309 "direct_dl_link": f"{Config.BASE_URL}/dl/{message_id}/{safe_file_name}",310 "mx_player_link": f"intent:{Config.BASE_URL}/dl/{message_id}/{safe_file_name}#Intent;action=android.intent.action.VIEW;type={mime_type};end",311 "vlc_player_link": f"intent:{Config.BASE_URL}/dl/{message_id}/{safe_file_name}#Intent;action=android.intent.action.VIEW;type={mime_type};package=org.videolan.vlc;end"312 }313 return response_data314 315class ByteStreamer:316 def __init__(self,c:Client):self.client=c317 @staticmethod318 async def get_location(f:FileId): return raw.types.InputDocumentFileLocation(id=f.media_id,access_hash=f.access_hash,file_reference=f.file_reference,thumb_size=f.thumbnail_size)319 async def yield_file(self,f:FileId,i:int,o:int,fc:int,lc:int,pc:int,cs:int):320 c=self.client;work_loads[i]+=1;ms=c.media_sessions.get(f.dc_id)321 if ms is None:322 if f.dc_id!=await c.storage.dc_id():323 ak=await Auth(c,f.dc_id,await c.storage.test_mode()).create();ms=Session(c,f.dc_id,ak,await c.storage.test_mode(),is_media=True);await ms.start();ea=await c.invoke(raw.functions.auth.ExportAuthorization(dc_id=f.dc_id));await ms.invoke(raw.functions.auth.ImportAuthorization(id=ea.id,bytes=ea.bytes))324 else:ms=c.session325 c.media_sessions[f.dc_id]=ms326 loc=await self.get_location(f);cp=1327 try:328 while cp<=pc:329 r=await ms.invoke(raw.functions.upload.GetFile(location=loc,offset=o,limit=cs),retries=0)330 if isinstance(r,raw.types.upload.File):331 chk=r.bytes332 if not chk:break333 if pc==1:yield chk[fc:lc]334 elif cp==1:yield chk[fc:]335 elif cp==pc:yield chk[:lc]336 else:yield chk337 cp+=1;o+=cs338 else:break339 finally:work_loads[i]-=1340 341@app.get("/dl/{mid}/{fname}")342async def stream_media(r:Request,mid:int,fname:str):343 if not work_loads: raise HTTPException(503)344 client_id = min(work_loads, key=work_loads.get)345 c = multi_clients.get(client_id)346 if not c: raise HTTPException(503)347 348 tc=class_cache.get(c) or ByteStreamer(c);class_cache[c]=tc349 try:350 msg=await c.get_messages(Config.STORAGE_CHANNEL,mid);m=msg.document or msg.video or msg.audio351 if not m or msg.empty:raise FileNotFoundError352 fid=FileId.decode(m.file_id);fsize=m.file_size;rh=r.headers.get("Range","");fb,ub=0,fsize-1353 if rh:354 rps=rh.replace("bytes=","").split("-");fb=int(rps[0])355 if len(rps)>1 and rps[1]:ub=int(rps[1])356 if(ub>=fsize)or(fb<0):raise HTTPException(416)357 rl=ub-fb+1;cs=1024*1024;off=(fb//cs)*cs;fc=fb-off;lc=(ub%cs)+1;pc=math.ceil(rl/cs)358 body=tc.yield_file(fid,client_id,off,fc,lc,pc,cs);sc=206 if rh else 200359 hdrs={"Content-Type":m.mime_type or "application/octet-stream","Accept-Ranges":"bytes","Content-Disposition":f'inline; filename="{m.file_name}"',"Content-Length":str(rl)}360 if rh:hdrs["Content-Range"]=f"bytes {fb}-{ub}/{fsize}"361 return StreamingResponse(body,status_code=sc,headers=hdrs)362 except FileNotFoundError:raise HTTPException(404)363 except Exception:print(traceback.format_exc());raise HTTPException(500)364 365# =====================================================================================366# --- MAIN EXECUTION BLOCK ---367# =====================================================================================368 369if __name__ == "__main__":370 port = int(os.environ.get("PORT", 8000))371 # Log level ko "info" rakho taaki hamara filter kaam kar sake372 uvicorn.run("app:app", host="0.0.0.0", port=port, log_level="info")373 