barnamemaker commited on
Commit
0ef2ff7
·
verified ·
1 Parent(s): 9c13ab4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +82 -138
app.py CHANGED
@@ -1,66 +1,40 @@
1
  import os
2
  import uuid
3
- import threading
4
  import time
 
5
  import fitz # PyMuPDF
6
- from fastapi import FastAPI
 
 
 
7
  from fastapi.responses import FileResponse
8
  import uvicorn
9
- import logging
10
  from telegram import Update, ReplyKeyboardMarkup, KeyboardButton
11
  from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
12
 
13
- # --- فعال‌سازی سیستم گزارش‌گیری ---
14
- logging.basicConfig(
15
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
16
- level=logging.INFO
17
- )
18
  logger = logging.getLogger(__name__)
19
 
20
  TOKEN = os.environ.get("TOKEN")
21
  DOWNLOAD_DIR = "downloads"
22
  SPACE_URL = "https://barnamemaker-pdfmeger.hf.space"
23
- MERGED_FILE_TTL = 3600 # مدت نگهداری فایل‌های ادغام‌شده (ثانیه)
24
 
25
  os.makedirs(DOWNLOAD_DIR, exist_ok=True)
26
-
27
- # user_id -> list of file paths
28
  user_files: dict[int, list[str]] = {}
29
- # قفل برای جلوگیری از race condition
30
- user_locks: dict[int, threading.Lock] = {}
31
 
32
- app_web = FastAPI()
33
-
34
- def get_user_lock(user_id: int) -> threading.Lock:
35
  if user_id not in user_locks:
36
- user_locks[user_id] = threading.Lock()
37
  return user_locks[user_id]
38
 
39
- # ───────────────────────── Web server ─────────────────────────
40
-
41
- @app_web.get("/")
42
- async def root():
43
- return {"message": "Bot is running!"}
44
-
45
- @app_web.get("/download/{filename}")
46
- async def get_file(filename: str):
47
- # جلوگیری از path traversal
48
- if ".." in filename or "/" in filename:
49
- return {"error": "Invalid filename"}
50
- file_path = os.path.join(DOWNLOAD_DIR, filename)
51
- if os.path.exists(file_path):
52
- return FileResponse(file_path, media_type="application/pdf")
53
- return {"error": "File not found"}
54
-
55
- def run_web():
56
- uvicorn.run(app_web, host="0.0.0.0", port=7860)
57
-
58
- # ───────────────────────── Cleanup ─────────────────────────
59
-
60
- def cleanup_files():
61
- """هر ساعت فایل‌های قدیمی‌تر از TTL را پاک می‌کند."""
62
  while True:
63
- time.sleep(600) # هر ۱۰ دقیقه چک کن
64
  now = time.time()
65
  for f in os.listdir(DOWNLOAD_DIR):
66
  path = os.path.join(DOWNLOAD_DIR, f)
@@ -71,8 +45,7 @@ def cleanup_files():
71
  except Exception as e:
72
  logger.warning(f"Could not remove {f}: {e}")
73
 
74
- # ───────────────────────── UI helpers ─────────────────────────
75
-
76
  def get_menu():
77
  keyboard = [
78
  [KeyboardButton("📄 ادغام فایل‌ها")],
@@ -86,17 +59,14 @@ HELP_TEXT = (
86
  "۲. روی «📄 ادغام فایل‌ها» کلیک کنید.\n"
87
  "۳. لینک دانلود فایل ادغام‌شده را دریافت کنید.\n\n"
88
  "برای پاک کردن صف: «🗑 پاک کردن صف»\n"
89
- "فایل‌ها به ترتیب ارسال ادغام می‌شوند."
90
  )
91
 
92
- # ───────────────────────── Handlers ─────────────────────────
93
-
94
  async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
95
  user_id = update.effective_user.id
96
  logger.info(f"User {user_id} started the bot.")
97
  await update.message.reply_text(
98
- "سلام! 👋\nفایل‌های PDF خود را بفرستید و سپس «📄 ادغام فایل‌ها» را بزنید.\n\n"
99
- "برای راهنما: ❓ راهنما",
100
  reply_markup=get_menu()
101
  )
102
 
@@ -104,13 +74,12 @@ async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE):
104
  user_id = update.effective_user.id
105
  doc = update.message.document
106
 
107
- # بررسی نوع فایل
108
  if doc.mime_type != "application/pdf":
109
  await update.message.reply_text("⚠️ فقط فایل PDF قبول می‌شود.", reply_markup=get_menu())
110
  return
111
 
112
  lock = get_user_lock(user_id)
113
- with lock:
114
  if user_id not in user_files:
115
  user_files[user_id] = []
116
 
@@ -122,149 +91,124 @@ async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE):
122
  try:
123
  await file.download_to_drive(file_path)
124
  except Exception as e:
125
- logger.error(f"Download error for user {user_id}: {e}")
126
- await update.message.reply_text("❌ خطا در دریافت فایل. دوباره امتحان کنید.", reply_markup=get_menu())
127
  return
128
 
129
- # اعتبارسنجی PDF (جلوگیری از فایل‌های خراب)
130
  try:
131
  with fitz.open(file_path) as test_doc:
132
  if test_doc.page_count == 0:
133
  raise ValueError("Empty PDF")
134
  except Exception:
135
  os.remove(file_path)
136
- await update.message.reply_text("⚠️ فایل PDF معتبر نیست یا خراب است.", reply_markup=get_menu())
137
  return
138
 
139
  user_files[user_id].append(file_path)
140
  count = len(user_files[user_id])
141
 
142
- await update.message.reply_text(
143
- f"✅ فایل {count} دریافت شد: {doc.file_name}",
144
- reply_markup=get_menu()
145
- )
146
 
147
  async def merge_pdfs(update: Update, context: ContextTypes.DEFAULT_TYPE):
148
  user_id = update.effective_user.id
149
  lock = get_user_lock(user_id)
150
 
151
- with lock:
152
  files = list(user_files.get(user_id, []))
153
 
154
  if not files:
155
- await update.message.reply_text("⚠️ صف شما خالی است! ابتدا فایل‌های PDF بفرستید.", reply_markup=get_menu())
156
  return
157
 
158
  if len(files) == 1:
159
  await update.message.reply_text("⚠️ حداقل ۲ فایل برای ادغام لازم است.", reply_markup=get_menu())
160
  return
161
 
162
- await update.message.reply_text(f"⏳ در حال ادغام {len(files)} فایل...", reply_markup=get_menu())
163
-
164
  output_filename = f"merged_{user_id}_{uuid.uuid4().hex[:8]}.pdf"
165
  output_path = os.path.join(DOWNLOAD_DIR, output_filename)
166
 
167
  try:
168
  result = fitz.open()
169
  for pdf_path in files:
170
- if not os.path.exists(pdf_path):
171
- logger.warning(f"Missing file during merge: {pdf_path}")
172
- continue
173
  with fitz.open(pdf_path) as doc:
174
  result.insert_pdf(doc)
175
 
176
- if result.page_count == 0:
177
- result.close()
178
- await update.message.reply_text("❌ هیچ صفحه‌ای برای ادغام یافت نشد.", reply_markup=get_menu())
179
- return
180
-
181
  result.save(output_path)
182
  result.close()
183
 
184
- # پاک کردن فایل‌های موقت کاربر
185
- with lock:
186
  for f in user_files.get(user_id, []):
187
- try:
188
- if os.path.exists(f):
189
- os.remove(f)
190
- except Exception as e:
191
- logger.warning(f"Could not delete temp file {f}: {e}")
192
  user_files[user_id] = []
193
 
194
  link = f"{SPACE_URL}/download/{output_filename}"
195
- size_kb = os.path.getsize(output_path) // 1024
196
- await update.message.reply_text(
197
- f"🎉 ادغام موفق!\n"
198
- f"📄 تعداد فایل‌ها: {len(files)}\n"
199
- f"📦 حجم خروجی: {size_kb} KB\n\n"
200
- f"📥 لینک دانلود:\n{link}",
201
- reply_markup=get_menu()
202
- )
203
 
204
  except Exception as e:
205
- logger.error(f"Error merging PDFs for user {user_id}: {e}")
206
- # پاک کردن فایل ناقص
207
- if os.path.exists(output_path):
208
- try:
209
- os.remove(output_path)
210
- except Exception:
211
- pass
212
- await update.message.reply_text(
213
- "❌ خطایی در پردازش رخ داد. لطفاً دوباره امتحان کنید.",
214
- reply_markup=get_menu()
215
- )
216
 
217
  async def clear_queue(update: Update, context: ContextTypes.DEFAULT_TYPE):
218
  user_id = update.effective_user.id
219
  lock = get_user_lock(user_id)
220
-
221
- with lock:
222
  files = list(user_files.get(user_id, []))
223
  user_files[user_id] = []
224
-
225
- # پاک کردن فایل‌های دانلودشده از دیسک
226
- removed = 0
227
  for f in files:
228
- try:
229
- if os.path.exists(f):
230
- os.remove(f)
231
- removed += 1
232
- except Exception as e:
233
- logger.warning(f"Could not delete {f}: {e}")
234
-
235
- await update.message.reply_text(
236
- f"🗑 صف پاک شد. ({removed} فایل حذف شد)",
237
- reply_markup=get_menu()
238
- )
239
 
240
  async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
241
  text = update.message.text
242
- if text == "📄 ادغام فایل‌ها":
243
- await merge_pdfs(update, context)
244
- elif text == "🗑 پاک کردن صف":
245
- await clear_queue(update, context)
246
- elif text == "❓ راهنما":
247
- await update.message.reply_text(HELP_TEXT, reply_markup=get_menu())
248
- else:
249
- count = len(user_files.get(update.effective_user.id, []))
250
- await update.message.reply_text(
251
- f"لطفاً از منو استفاده کنید.\n📂 فایل‌های در صف: {count}",
252
- reply_markup=get_menu()
253
- )
254
-
255
- # ───────────────────────── Main ─────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
- if __name__ == "__main__":
258
- if not TOKEN:
259
- raise RuntimeError("متغیر محیطی TOKEN تنظیم نشده است!")
260
-
261
- threading.Thread(target=run_web, daemon=True).start()
262
- threading.Thread(target=cleanup_files, daemon=True).start()
263
 
264
- app = Application.builder().token(TOKEN).build()
265
- app.add_handler(CommandHandler("start", start))
266
- app.add_handler(MessageHandler(filters.Document.PDF, handle_document))
267
- app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text))
 
 
 
268
 
269
- logger.info("Bot is polling...")
270
- app.run_polling(drop_pending_updates=True)
 
 
 
 
1
  import os
2
  import uuid
 
3
  import time
4
+ import asyncio
5
  import fitz # PyMuPDF
6
+ import logging
7
+ from contextlib import asynccontextmanager
8
+
9
+ from fastapi import FastAPI, Request, Response
10
  from fastapi.responses import FileResponse
11
  import uvicorn
12
+
13
  from telegram import Update, ReplyKeyboardMarkup, KeyboardButton
14
  from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
15
 
16
+ # --- تنظیمات لاگ و متغیرها ---
17
+ logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)
 
 
 
18
  logger = logging.getLogger(__name__)
19
 
20
  TOKEN = os.environ.get("TOKEN")
21
  DOWNLOAD_DIR = "downloads"
22
  SPACE_URL = "https://barnamemaker-pdfmeger.hf.space"
23
+ MERGED_FILE_TTL = 3600 # ۱ ساعت
24
 
25
  os.makedirs(DOWNLOAD_DIR, exist_ok=True)
 
 
26
  user_files: dict[int, list[str]] = {}
27
+ user_locks: dict[int, asyncio.Lock] = {}
 
28
 
29
+ def get_user_lock(user_id: int) -> asyncio.Lock:
 
 
30
  if user_id not in user_locks:
31
+ user_locks[user_id] = asyncio.Lock()
32
  return user_locks[user_id]
33
 
34
+ # --- تسک پاکسازی در پس‌زمینه ---
35
+ async def cleanup_task():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  while True:
37
+ await asyncio.sleep(600)
38
  now = time.time()
39
  for f in os.listdir(DOWNLOAD_DIR):
40
  path = os.path.join(DOWNLOAD_DIR, f)
 
45
  except Exception as e:
46
  logger.warning(f"Could not remove {f}: {e}")
47
 
48
+ # --- منوی دکمه‌ها ---
 
49
  def get_menu():
50
  keyboard = [
51
  [KeyboardButton("📄 ادغام فایل‌ها")],
 
59
  "۲. روی «📄 ادغام فایل‌ها» کلیک کنید.\n"
60
  "۳. لینک دانلود فایل ادغام‌شده را دریافت کنید.\n\n"
61
  "برای پاک کردن صف: «🗑 پاک کردن صف»\n"
 
62
  )
63
 
64
+ # --- منطق ربات (Handlers) ---
 
65
  async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
66
  user_id = update.effective_user.id
67
  logger.info(f"User {user_id} started the bot.")
68
  await update.message.reply_text(
69
+ "سلام! 👋\nفایل‌های PDF خود را بفرستید و سپس «📄 ادغام فایل‌ها» را بزنید.",
 
70
  reply_markup=get_menu()
71
  )
72
 
 
74
  user_id = update.effective_user.id
75
  doc = update.message.document
76
 
 
77
  if doc.mime_type != "application/pdf":
78
  await update.message.reply_text("⚠️ فقط فایل PDF قبول می‌شود.", reply_markup=get_menu())
79
  return
80
 
81
  lock = get_user_lock(user_id)
82
+ async with lock:
83
  if user_id not in user_files:
84
  user_files[user_id] = []
85
 
 
91
  try:
92
  await file.download_to_drive(file_path)
93
  except Exception as e:
94
+ logger.error(f"Download error: {e}")
95
+ await update.message.reply_text("❌ خطا در دریافت فایل.", reply_markup=get_menu())
96
  return
97
 
 
98
  try:
99
  with fitz.open(file_path) as test_doc:
100
  if test_doc.page_count == 0:
101
  raise ValueError("Empty PDF")
102
  except Exception:
103
  os.remove(file_path)
104
+ await update.message.reply_text("⚠️ فایل PDF معتبر نیست.", reply_markup=get_menu())
105
  return
106
 
107
  user_files[user_id].append(file_path)
108
  count = len(user_files[user_id])
109
 
110
+ await update.message.reply_text(f"✅ فایل {count} دریافت شد: {doc.file_name}", reply_markup=get_menu())
 
 
 
111
 
112
  async def merge_pdfs(update: Update, context: ContextTypes.DEFAULT_TYPE):
113
  user_id = update.effective_user.id
114
  lock = get_user_lock(user_id)
115
 
116
+ async with lock:
117
  files = list(user_files.get(user_id, []))
118
 
119
  if not files:
120
+ await update.message.reply_text("⚠️ صف شما خالی است!", reply_markup=get_menu())
121
  return
122
 
123
  if len(files) == 1:
124
  await update.message.reply_text("⚠️ حداقل ۲ فایل برای ادغام لازم است.", reply_markup=get_menu())
125
  return
126
 
127
+ msg = await update.message.reply_text(f"⏳ در حال ادغام {len(files)} فایل...")
 
128
  output_filename = f"merged_{user_id}_{uuid.uuid4().hex[:8]}.pdf"
129
  output_path = os.path.join(DOWNLOAD_DIR, output_filename)
130
 
131
  try:
132
  result = fitz.open()
133
  for pdf_path in files:
134
+ if not os.path.exists(pdf_path): continue
 
 
135
  with fitz.open(pdf_path) as doc:
136
  result.insert_pdf(doc)
137
 
 
 
 
 
 
138
  result.save(output_path)
139
  result.close()
140
 
141
+ async with lock:
 
142
  for f in user_files.get(user_id, []):
143
+ if os.path.exists(f): os.remove(f)
 
 
 
 
144
  user_files[user_id] = []
145
 
146
  link = f"{SPACE_URL}/download/{output_filename}"
147
+ await msg.edit_text(f"🎉 ادغام موفق!\n📥 لینک دانلود:\n{link}")
 
 
 
 
 
 
 
148
 
149
  except Exception as e:
150
+ logger.error(f"Error merging: {e}")
151
+ await msg.edit_text("❌ خطایی در پردازش رخ داد.")
 
 
 
 
 
 
 
 
 
152
 
153
  async def clear_queue(update: Update, context: ContextTypes.DEFAULT_TYPE):
154
  user_id = update.effective_user.id
155
  lock = get_user_lock(user_id)
156
+ async with lock:
 
157
  files = list(user_files.get(user_id, []))
158
  user_files[user_id] = []
159
+
 
 
160
  for f in files:
161
+ if os.path.exists(f): os.remove(f)
162
+ await update.message.reply_text("🗑 صف پاک شد.", reply_markup=get_menu())
 
 
 
 
 
 
 
 
 
163
 
164
  async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
165
  text = update.message.text
166
+ if text == "📄 ادغام فایل‌ها": await merge_pdfs(update, context)
167
+ elif text == "🗑 پاک کردن صف": await clear_queue(update, context)
168
+ elif text == " راهنما": await update.message.reply_text(HELP_TEXT, reply_markup=get_menu())
169
+
170
+ # --- تنظیمات اتصال اصلی (Webhook) ---
171
+ ptb_app = Application.builder().token(TOKEN).build()
172
+ ptb_app.add_handler(CommandHandler("start", start))
173
+ ptb_app.add_handler(MessageHandler(filters.Document.PDF, handle_document))
174
+ ptb_app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text))
175
+
176
+ @asynccontextmanager
177
+ async def lifespan(app: FastAPI):
178
+ # تنظیم Webhook در هنگام روشن شدن سرور
179
+ await ptb_app.bot.set_webhook(url=f"{SPACE_URL}/webhook", drop_pending_updates=True)
180
+ async with ptb_app:
181
+ await ptb_app.start()
182
+ task = asyncio.create_task(cleanup_task())
183
+ yield
184
+ # خاموش کردن امن
185
+ task.cancel()
186
+ await ptb_app.stop()
187
+
188
+ app_web = FastAPI(lifespan=lifespan)
189
+
190
+ # مسیری که تلگرام پیام‌ها را به آن می‌فرستد
191
+ @app_web.post("/webhook")
192
+ async def telegram_webhook(request: Request):
193
+ req_json = await request.json()
194
+ update = Update.de_json(req_json, ptb_app.bot)
195
+ await ptb_app.process_update(update)
196
+ return Response(status_code=200)
197
 
198
+ @app_web.get("/")
199
+ async def root():
200
+ return {"message": "Bot Webhook is Active!"}
 
 
 
201
 
202
+ @app_web.get("/download/{filename}")
203
+ async def get_file(filename: str):
204
+ if ".." in filename or "/" in filename: return {"error": "Invalid"}
205
+ file_path = os.path.join(DOWNLOAD_DIR, filename)
206
+ if os.path.exists(file_path):
207
+ return FileResponse(file_path, media_type="application/pdf")
208
+ return {"error": "Not found"}
209
 
210
+ if __name__ == "__main__":
211
+ if not TOKEN:
212
+ raise RuntimeError("TOKEN is missing!")
213
+ # اجرای سرور واحد وب
214
+ uvicorn.run(app_web, host="0.0.0.0", port=7860)