File size: 6,155 Bytes
9b7a707
 
 
b351d03
 
b6a9b42
9b7a707
 
 
 
 
 
 
 
b351d03
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219e24c
9b7a707
219e24c
 
 
 
 
 
b351d03
219e24c
9b7a707
b351d03
9b7a707
 
 
 
 
 
b6a9b42
b351d03
 
9b7a707
 
8919e46
 
b351d03
 
8919e46
b351d03
 
 
 
 
 
 
 
 
 
 
b6a9b42
8919e46
 
b351d03
 
8919e46
 
9b7a707
b6a9b42
9b7a707
b351d03
9b7a707
 
b351d03
9b7a707
219e24c
 
 
 
 
 
 
b351d03
219e24c
b351d03
219e24c
b351d03
9b7a707
 
 
b351d03
9b7a707
 
 
219e24c
 
 
 
9b7a707
 
 
b351d03
9b7a707
 
 
b351d03
9b7a707
 
 
219e24c
9b7a707
 
 
 
 
 
 
 
 
b351d03
 
 
 
 
9b7a707
b351d03
 
9b7a707
b351d03
9b7a707
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import os
import math
import logging
import socket
import requests  # Make sure 'requests' is in requirements.txt
from contextlib import asynccontextmanager
from pyrogram import Client
from pyrogram.session import Session, Auth
from pyrogram import raw
from pyrogram.file_id import FileId
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import StreamingResponse
import uvicorn

# --- 1. GLOBAL IPv4 PATCH (The "Nuclear Option") ---
# This forces the entire Python script to ignore IPv6.
# If this doesn't fix it, your Space IP is blacklisted.
def force_ipv4():
    old_getaddrinfo = socket.getaddrinfo
    def new_getaddrinfo(*args, **kwargs):
        responses = old_getaddrinfo(*args, **kwargs)
        return [response for response in responses if response[0] == socket.AF_INET]
    socket.getaddrinfo = new_getaddrinfo

force_ipv4()

# --- 2. HTTP CONNECTIVITY CHECK ---
def check_telegram_api():
    print("πŸ“‘ TEST: Checking connection to Telegram API via HTTP...")
    try:
        # We try to reach the standard HTTP API. 
        # If this fails, the NETWORK is blocked.
        r = requests.get(f"https://api.telegram.org/bot{os.environ.get('BOT_TOKEN')}/getMe", timeout=10)
        if r.status_code == 200:
            print("βœ… HTTP TEST PASSED: Network is fine. Bot Token is valid.")
            return True
        else:
            print(f"⚠️ HTTP TEST FAILED: Status {r.status_code} - {r.text}")
            return False
    except Exception as e:
        print(f"❌ HTTP TEST FAILED: Could not reach Telegram. The IP is likely blocked. Error: {e}")
        return False

# --- CONFIGURATION ---
try:
    API_ID = int(os.environ.get("API_ID"))
    API_HASH = os.environ.get("API_HASH")
    BOT_TOKEN = os.environ.get("BOT_TOKEN")
    STORAGE_CHANNEL = int(os.environ.get("STORAGE_CHANNEL"))
except ValueError:
    print("❌ ERROR: Env vars are missing or invalid!")
    exit(1)

# --- BOT CLIENT ---
client = Client(
    "worker_bot",
    api_id=API_ID,
    api_hash=API_HASH,
    bot_token=BOT_TOKEN,
    in_memory=True,
    no_updates=True,
    ipv6=False,
    workdir="/tmp"
)

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Run the diagnostic
    network_ok = check_telegram_api()
    
    if network_ok:
        print("⏳ Connecting via Pyrogram (MTProto)...")
        try:
            await client.start()
            me = await client.get_me()
            print(f"βœ… Worker Started as: {me.first_name} (@{me.username})")
        except Exception as e:
            print(f"❌ PYROGRAM ERROR: HTTP worked, but MTProto failed. {e}")
    else:
        print("πŸ›‘ ABORTING: Cannot reach Telegram API.")

    yield
    
    try:
        if client.is_connected: await client.stop()
    except: pass

app = FastAPI(lifespan=lifespan)

# --- STREAMING LOGIC ---
class ByteStreamer:
    def __init__(self, client: Client): self.client = client
    async def yield_file(self, file_id, offset, first_part_cut, last_part_cut, part_count, chunk_size):
        client = self.client
        if not client.is_connected: return
        
        try:
            ms = client.media_sessions.get(file_id.dc_id)
            if not ms:
                if file_id.dc_id != await client.storage.dc_id():
                    auth_key = await Auth(client, file_id.dc_id, await client.storage.test_mode()).create()
                    ms = Session(client, file_id.dc_id, auth_key, await client.storage.test_mode(), is_media=True)
                    await ms.start()
                else: ms = client.session
                client.media_sessions[file_id.dc_id] = ms
        except: return

        loc = raw.types.InputDocumentFileLocation(id=file_id.media_id, access_hash=file_id.access_hash, file_reference=file_id.file_reference, thumb_size=file_id.thumbnail_size)
        curr = 1
        while curr <= part_count:
            try:
                r = await ms.invoke(raw.functions.upload.GetFile(location=loc, offset=offset, limit=chunk_size), retries=3)
                if isinstance(r, raw.types.upload.File):
                    chunk = r.bytes
                    if not chunk: break
                    if part_count == 1: yield chunk[first_part_cut:last_part_cut]
                    elif curr == 1: yield chunk[first_part_cut:]
                    elif curr == part_count: yield chunk[:last_part_cut]
                    else: yield chunk
                    curr += 1
                    offset += chunk_size
                else: break
            except: break

@app.get("/stream/{message_id}/{filename}")
async def stream_handler(req: Request, message_id: int, filename: str):
    if not client.is_connected: raise HTTPException(503, "Bot not connected to Telegram")
    try:
        msg = await client.get_messages(STORAGE_CHANNEL, message_id)
        media = msg.document or msg.video or msg.audio
        if not media: raise HTTPException(404, "File not found")
        file_id = FileId.decode(media.file_id)
        file_size = media.file_size
        range_header = req.headers.get("Range", 0)
        from_bytes, until_bytes = 0, file_size - 1
        if range_header:
            s = range_header.replace("bytes=", "").split("-")
            from_bytes = int(s[0])
            if s[1]: until_bytes = int(s[1])
        req_len = until_bytes - from_bytes + 1
        chunk = 1048576 
        offset = (from_bytes // chunk) * chunk
        first_cut = from_bytes - offset
        last_cut = (until_bytes % chunk) + 1
        parts = math.ceil(req_len / chunk)
        streamer = ByteStreamer(client)
        body = streamer.yield_file(file_id, offset, first_cut, last_cut, parts, chunk)
        headers = {"Content-Type": media.mime_type or "application/octet-stream", "Content-Disposition": f'inline; filename="{media.file_name}"', "Accept-Ranges": "bytes", "Content-Range": f"bytes {from_bytes}-{until_bytes}/{file_size}", "Content-Length": str(req_len)}
        return StreamingResponse(body, status_code=206 if range_header else 200, headers=headers)
    except: raise HTTPException(404, "File Not Found")

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=7860)