no-name-here commited on
Commit
511fc24
Β·
verified Β·
1 Parent(s): 86ada0f

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +28 -0
  2. config.env +13 -0
  3. fileManager.py +108 -0
  4. main.py +309 -0
  5. requirements.txt +6 -0
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim-bookworm
2
+
3
+ ENV PYTHONUNBUFFERED=1
4
+ ENV TZ=Asia/Kolkata
5
+ ARG DEBIAN_FRONTEND=noninteractive
6
+
7
+ RUN apt-get update && \
8
+ apt-get install -y --no-install-recommends \
9
+ build-essential tzdata libssl-dev libffi-dev && \
10
+ ln -snf "/usr/share/zoneinfo/$TZ" /etc/localtime && \
11
+ echo "$TZ" > /etc/timezone && \
12
+ rm -rf /var/lib/apt/lists/*
13
+
14
+ RUN pip install --no-cache-dir -U pip
15
+
16
+ WORKDIR /app
17
+
18
+ COPY requirements.txt .
19
+ RUN pip install -U -r requirements.txt
20
+
21
+ COPY main.py .
22
+ COPY fileManager.py .
23
+
24
+ # Storage dirs
25
+ RUN mkdir -p fl
26
+
27
+ EXPOSE 7860
28
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
config.env ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Telegram API credentials β€” https://my.telegram.org
2
+ API_ID=
3
+ API_HASH=
4
+
5
+ # Bot token from @BotFather
6
+ BOT_TOKEN=
7
+
8
+ # Your HuggingFace Space URL (this space itself)
9
+ # e.g. https://yourname-linkgenbot.hf.space
10
+ SPACE_URL=
11
+
12
+ # How long files are kept in minutes (0 = keep forever)
13
+ FILE_EXPIRE_MINUTES=0
fileManager.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from threading import Thread
2
+ from copy import deepcopy
3
+ from uuid import uuid4
4
+ from time import sleep
5
+ import json
6
+ import time
7
+ import re
8
+ import os
9
+
10
+
11
+ def syncmethod(func):
12
+ def function(*args, **kwargs):
13
+ th = Thread(target=func, args=args, kwargs=kwargs)
14
+ th.start()
15
+ return function
16
+
17
+
18
+ @syncmethod
19
+ def interval(fc, t):
20
+ while True:
21
+ sleep(t)
22
+ fc()
23
+
24
+
25
+ class FileManager:
26
+ def __init__(self, base_url: str, expires_minutes: int = 60):
27
+ self.data_file = "collector_data.json"
28
+ self.base_url = base_url.rstrip("/")
29
+ self.expires_minutes = expires_minutes
30
+ os.makedirs("fl", exist_ok=True)
31
+ if not os.path.exists(self.data_file):
32
+ self._write_json()
33
+ else:
34
+ self.check_files()
35
+
36
+ # ------------------------------------------------------------------
37
+ def _format_path(self, path: str) -> str:
38
+ p = re.sub(r'\s+', '-', path)
39
+ p = re.sub(r'[!?#*ÇçΓͺwrong_char]', '', p)
40
+ return p
41
+
42
+ def _read_json(self) -> list:
43
+ with open(self.data_file, "r") as f:
44
+ return json.load(f)
45
+
46
+ def _write_json(self, data: list = None):
47
+ with open(self.data_file, "w") as f:
48
+ json.dump(data or [], f, indent=2)
49
+
50
+ # ------------------------------------------------------------------
51
+ def start(self, gap_minutes: int = 5):
52
+ interval(self.check_files, gap_minutes * 60)
53
+
54
+ def check_files(self):
55
+ data = self._read_json()
56
+ now = time.time()
57
+ keep = []
58
+ for entry in data:
59
+ if entry["expiresAt"] < now:
60
+ try:
61
+ os.remove(entry["path"])
62
+ except Exception as e:
63
+ print(f"[cleanup] {e}")
64
+ else:
65
+ keep.append(entry)
66
+ self._write_json(keep)
67
+
68
+ # ------------------------------------------------------------------
69
+ def _revalidate(self, path: str):
70
+ data = self._read_json()
71
+ for entry in data:
72
+ if entry["path"] == path:
73
+ entry["expiresAt"] = time.time() + self.expires_minutes * 60
74
+ self._write_json(data)
75
+
76
+ def _add_path(self, path: str) -> dict:
77
+ new_path = self._format_path(path)
78
+ if path != new_path:
79
+ os.rename(path, new_path)
80
+ data = self._read_json()
81
+ data = [e for e in data if e["path"] != new_path]
82
+ entry = {
83
+ "path": new_path,
84
+ "filename": new_path.split("/")[-1],
85
+ "id": str(uuid4()),
86
+ "expiresAt": time.time() + self.expires_minutes * 60,
87
+ }
88
+ data.append(entry)
89
+ self._write_json(data)
90
+ return entry
91
+
92
+ # ------------------------------------------------------------------
93
+ def save_file(self, data: bytes, filename: str) -> dict:
94
+ filename = self._format_path(filename)
95
+ file_path = f"fl/{uuid4()}_{filename}"
96
+ with open(file_path, "wb") as f:
97
+ f.write(data)
98
+ return self._add_path(file_path)
99
+
100
+ def get_url(self, entry: dict) -> str:
101
+ return f"{self.base_url}/file=./{entry['path']}"
102
+
103
+ def get_entry_by_id(self, uid: str) -> dict | None:
104
+ for entry in self._read_json():
105
+ if entry["id"] == uid:
106
+ self._revalidate(entry["path"])
107
+ return entry
108
+ return None
main.py ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Link Generator Bot
2
+ # Webhook-only | HuggingFace Spaces compatible
3
+ #
4
+ # Flow:
5
+ # User sends any file / photo / video / audio / document to the bot
6
+ # β†’ Bot downloads it via Pyrogram MTProto (no Bot API HTTP)
7
+ # β†’ Bot saves it to local HF Space storage
8
+ # β†’ Bot replies with a permanent public https://...hf.space/file=./... link
9
+
10
+ import os
11
+ import sys
12
+ import logging
13
+ import asyncio
14
+ import traceback
15
+ from contextlib import asynccontextmanager
16
+ from logging.handlers import RotatingFileHandler
17
+
18
+ from dotenv import load_dotenv
19
+ from fastapi import FastAPI, Request
20
+ from fastapi.responses import JSONResponse
21
+ from pyrogram import Client
22
+ from pyrogram.enums import ParseMode
23
+ from pyrogram.errors import FloodWait
24
+
25
+ from fileManager import FileManager
26
+
27
+ # ============================================================
28
+ # Logging
29
+ # ============================================================
30
+ try:
31
+ os.remove("logs.txt")
32
+ except Exception:
33
+ pass
34
+
35
+ logging.basicConfig(
36
+ level=logging.INFO,
37
+ format="[%(asctime)s - %(levelname)s] - %(name)s - %(message)s",
38
+ datefmt="%d-%b-%y %I:%M:%S %p",
39
+ handlers=[
40
+ RotatingFileHandler("logs.txt", mode="w+", maxBytes=5_000_000, backupCount=10),
41
+ logging.StreamHandler(),
42
+ ],
43
+ )
44
+ logging.getLogger("pyrogram").setLevel(logging.ERROR)
45
+
46
+ def LOGGER(name: str) -> logging.Logger:
47
+ return logging.getLogger(name)
48
+
49
+ # ============================================================
50
+ # Config
51
+ # ============================================================
52
+ try:
53
+ load_dotenv("config.env.local")
54
+ load_dotenv("config.env")
55
+ except Exception:
56
+ pass
57
+
58
+ _required = ["BOT_TOKEN", "API_ID", "API_HASH", "SPACE_URL"]
59
+ _missing = [v for v in _required if not os.getenv(v)]
60
+ if _missing:
61
+ print(f"ERROR: Missing required env vars: {', '.join(_missing)}")
62
+ sys.exit(1)
63
+
64
+ API_ID = int(os.getenv("API_ID"))
65
+ API_HASH = os.getenv("API_HASH")
66
+ BOT_TOKEN = os.getenv("BOT_TOKEN")
67
+ SPACE_URL = os.getenv("SPACE_URL") # e.g. https://yourname-spacename.hf.space
68
+ FILE_EXPIRE_MINUTES = int(os.getenv("FILE_EXPIRE_MINUTES", "0")) # 0 = never expire
69
+
70
+ # ============================================================
71
+ # FileManager
72
+ # ============================================================
73
+ os.makedirs("fl", exist_ok=True)
74
+ fm = FileManager(base_url=SPACE_URL, expires_minutes=FILE_EXPIRE_MINUTES or 999_999)
75
+ fm.start()
76
+
77
+ # ============================================================
78
+ # Pyrogram bot client (MTProto only β€” no Bot API HTTP)
79
+ # ============================================================
80
+ bot = Client(
81
+ "link_gen_bot",
82
+ api_id=API_ID,
83
+ api_hash=API_HASH,
84
+ bot_token=BOT_TOKEN,
85
+ workers=50,
86
+ parse_mode=ParseMode.MARKDOWN,
87
+ max_concurrent_transmissions=4,
88
+ sleep_threshold=30,
89
+ )
90
+
91
+ # ============================================================
92
+ # Global state
93
+ # ============================================================
94
+ RUNNING_TASKS: set = set()
95
+
96
+ def track_task(coro):
97
+ task = asyncio.create_task(coro)
98
+ RUNNING_TASKS.add(task)
99
+ task.add_done_callback(RUNNING_TASKS.discard)
100
+ return task
101
+
102
+ # ============================================================
103
+ # Webhook response helpers
104
+ # ============================================================
105
+ def _msg(chat_id: int, text: str, parse_mode: str = "Markdown",
106
+ reply_markup: dict = None, disable_web_page_preview: bool = False) -> dict:
107
+ payload = {"method": "sendMessage", "chat_id": chat_id,
108
+ "text": text, "parse_mode": parse_mode}
109
+ if reply_markup:
110
+ payload["reply_markup"] = reply_markup
111
+ if disable_web_page_preview:
112
+ payload["disable_web_page_preview"] = True
113
+ return payload
114
+
115
+ # ============================================================
116
+ # Instant command handlers
117
+ # ============================================================
118
+ def handle_start(chat_id: int) -> dict:
119
+ return _msg(chat_id,
120
+ "πŸ‘‹ **Welcome to Link Generator Bot!**\n\n"
121
+ "Just send me any file, photo, video, audio or document\n"
122
+ "and I'll give you a **public shareable link** for it.\n\n"
123
+ "πŸ“Ž Supports: documents, photos, videos, audio, voice, stickers\n"
124
+ "πŸ”— Links are permanent and accessible by anyone.\n\n"
125
+ "Just send a file to get started!")
126
+
127
+ def handle_help(chat_id: int) -> dict:
128
+ return _msg(chat_id,
129
+ "πŸ’‘ **Link Generator Bot Help**\n\n"
130
+ "➀ Send any file/media β†’ get a public link\n"
131
+ "➀ `/start` – welcome message\n"
132
+ "➀ `/help` – this message\n"
133
+ "➀ `/stats` – bot stats\n\n"
134
+ "**Supported types:**\n"
135
+ "β€’ Documents, PDFs, ZIPs, etc.\n"
136
+ "β€’ Photos\n"
137
+ "β€’ Videos\n"
138
+ "β€’ Audio / Voice messages\n"
139
+ "β€’ Stickers\n"
140
+ "β€’ Any other file type\n\n"
141
+ "Just send the file β€” no commands needed!")
142
+
143
+ def handle_stats(chat_id: int) -> dict:
144
+ import shutil, psutil
145
+ total, used, free = shutil.disk_usage(".")
146
+ proc = psutil.Process(os.getpid())
147
+
148
+ def fmt(b):
149
+ for u in ["B","KB","MB","GB"]:
150
+ if b < 1024: return f"{b:.1f} {u}"
151
+ b /= 1024
152
+ return f"{b:.1f} TB"
153
+
154
+ fl_count = len(os.listdir("fl")) if os.path.isdir("fl") else 0
155
+ return _msg(chat_id,
156
+ "**πŸ“Š Bot Stats**\n\n"
157
+ f"**➜ Disk:** `{fmt(used)}` used / `{fmt(total)}` total\n"
158
+ f"**➜ Free:** `{fmt(free)}`\n"
159
+ f"**➜ Memory:** `{round(proc.memory_info()[0]/1024**2)} MiB`\n"
160
+ f"**➜ CPU:** `{psutil.cpu_percent(interval=0.2)}%`\n"
161
+ f"**➜ Stored files:** `{fl_count}`")
162
+
163
+ # ============================================================
164
+ # Detect media in a raw Telegram message dict
165
+ # ============================================================
166
+ MEDIA_TYPES = [
167
+ "document", "video", "audio", "voice",
168
+ "photo", "animation", "sticker", "video_note"
169
+ ]
170
+
171
+ def get_media_info(msg: dict) -> tuple[str, str] | tuple[None, None]:
172
+ """Returns (media_type, file_name) or (None, None) if no media."""
173
+ for mtype in MEDIA_TYPES:
174
+ if mtype not in msg:
175
+ continue
176
+ obj = msg[mtype]
177
+ if mtype == "photo":
178
+ # photo is a list β€” pick largest
179
+ obj = msg["photo"][-1] if isinstance(msg["photo"], list) else msg["photo"]
180
+ return mtype, f"{obj.get('file_unique_id', 'photo')}.jpg"
181
+ fname = (
182
+ obj.get("file_name")
183
+ or obj.get("file_unique_id", mtype)
184
+ )
185
+ # ensure extension
186
+ if "." not in fname.split("/")[-1]:
187
+ ext_map = {
188
+ "video": "mp4", "audio": "mp3", "voice": "ogg",
189
+ "animation": "gif", "sticker": "webp", "video_note": "mp4"
190
+ }
191
+ fname = f"{fname}.{ext_map.get(mtype, 'bin')}"
192
+ return mtype, fname
193
+ return None, None
194
+
195
+ # ============================================================
196
+ # Async media handler
197
+ # ============================================================
198
+ async def handle_media(chat_id: int, msg_id: int, tg_msg_id: int, filename: str):
199
+ """Download file via Pyrogram MTProto, save to HF space, reply with link."""
200
+ progress_msg = await bot.send_message(
201
+ chat_id, "⏳ **Processing your file...**",
202
+ reply_to_message_id=tg_msg_id)
203
+ try:
204
+ # Get the actual Pyrogram message object
205
+ pyro_msg = await bot.get_messages(chat_id, tg_msg_id)
206
+ if not pyro_msg or not pyro_msg.media:
207
+ await progress_msg.edit("❌ **Could not read the file. Please try again.**")
208
+ return
209
+
210
+ # Download to memory-mapped temp path
211
+ tmp_path = f"fl/tmp_{tg_msg_id}_{filename}"
212
+ for attempt in range(2):
213
+ try:
214
+ await pyro_msg.download(file_name=tmp_path)
215
+ break
216
+ except FloodWait as e:
217
+ wait_s = int(getattr(e, "value", 0) or 0)
218
+ LOGGER(__name__).warning(f"FloodWait: {wait_s}s")
219
+ if attempt == 0 and wait_s > 0:
220
+ await asyncio.sleep(wait_s + 1)
221
+ continue
222
+ raise
223
+
224
+ if not os.path.exists(tmp_path) or os.path.getsize(tmp_path) == 0:
225
+ await progress_msg.edit("❌ **Download failed. Please try again.**")
226
+ try: os.remove(tmp_path)
227
+ except: pass
228
+ return
229
+
230
+ # Read and save via FileManager
231
+ with open(tmp_path, "rb") as f:
232
+ file_data = f.read()
233
+ os.remove(tmp_path)
234
+
235
+ entry = fm.save_file(file_data, filename)
236
+ link = fm.get_url(entry)
237
+
238
+ file_size = len(file_data)
239
+ size_str = f"{file_size/1024/1024:.2f} MB" if file_size > 1024*1024 else f"{file_size/1024:.1f} KB"
240
+
241
+ await progress_msg.edit(
242
+ f"βœ… **Link Generated!**\n\n"
243
+ f"πŸ”— **Link:** `{link}`\n\n"
244
+ f"πŸ“„ **File:** `{entry['filename']}`\n"
245
+ f"πŸ“¦ **Size:** `{size_str}`\n\n"
246
+ f"_Link is public and accessible by anyone._"
247
+ )
248
+ LOGGER(__name__).info(f"Generated link for {filename}: {link}")
249
+
250
+ except Exception as e:
251
+ LOGGER(__name__).error(traceback.format_exc())
252
+ try:
253
+ await progress_msg.edit(f"❌ **Error:** `{str(e)}`")
254
+ except Exception:
255
+ pass
256
+
257
+ # ============================================================
258
+ # FastAPI lifespan
259
+ # ============================================================
260
+ @asynccontextmanager
261
+ async def lifespan(app: FastAPI):
262
+ LOGGER(__name__).info("Starting bot client…")
263
+ await bot.start()
264
+ LOGGER(__name__).info("Bot ready β€” waiting for webhook updates.")
265
+ yield
266
+ LOGGER(__name__).info("Shutting down…")
267
+ await bot.stop()
268
+
269
+ app = FastAPI(lifespan=lifespan)
270
+
271
+ # ============================================================
272
+ # Webhook endpoint
273
+ # ============================================================
274
+ @app.post("/webhook")
275
+ async def telegram_webhook(request: Request):
276
+ update = await request.json()
277
+ LOGGER(__name__).debug(f"Update: {update}")
278
+
279
+ if "message" not in update:
280
+ return JSONResponse({"status": "ok"})
281
+
282
+ msg = update["message"]
283
+ chat_id = msg["chat"]["id"]
284
+ text = msg.get("text", "").strip()
285
+ msg_id = msg["message_id"]
286
+
287
+ # ── commands ──────────────────────────────────────────
288
+ if text.startswith("/start"):
289
+ return JSONResponse(handle_start(chat_id))
290
+
291
+ if text.startswith("/help"):
292
+ return JSONResponse(handle_help(chat_id))
293
+
294
+ if text.startswith("/stats"):
295
+ return JSONResponse(handle_stats(chat_id))
296
+
297
+ # ── media ─────────────────────────────────────────────
298
+ media_type, filename = get_media_info(msg)
299
+ if media_type:
300
+ track_task(handle_media(chat_id, msg_id, msg_id, filename))
301
+ return JSONResponse({"status": "ok"})
302
+
303
+ # ── unknown ───────────────────────────────────────────
304
+ if text and not text.startswith("/"):
305
+ return JSONResponse(_msg(chat_id,
306
+ "πŸ“Ž **Please send a file, photo, video or audio.**\n"
307
+ "I'll generate a public link for it!"))
308
+
309
+ return JSONResponse({"status": "ok"})
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ pyrofork
2
+ tgcrypto
3
+ python-dotenv
4
+ fastapi
5
+ uvicorn[standard]
6
+ psutil