barnamemaker commited on
Commit
9c13ab4
·
verified ·
1 Parent(s): 26f5df0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +190 -35
app.py CHANGED
@@ -20,32 +20,58 @@ logger = logging.getLogger(__name__)
20
  TOKEN = os.environ.get("TOKEN")
21
  DOWNLOAD_DIR = "downloads"
22
  SPACE_URL = "https://barnamemaker-pdfmeger.hf.space"
 
 
23
  os.makedirs(DOWNLOAD_DIR, exist_ok=True)
24
- user_files = {}
 
 
 
 
25
 
26
  app_web = FastAPI()
27
 
 
 
 
 
 
 
 
28
  @app_web.get("/")
29
  async def root():
30
  return {"message": "Bot is running!"}
31
 
32
  @app_web.get("/download/{filename}")
33
  async def get_file(filename: str):
34
- file_path = f"{DOWNLOAD_DIR}/{filename}"
 
 
 
35
  if os.path.exists(file_path):
36
- return FileResponse(file_path)
37
  return {"error": "File not found"}
38
 
39
  def run_web():
40
  uvicorn.run(app_web, host="0.0.0.0", port=7860)
41
 
 
 
42
  def cleanup_files():
 
43
  while True:
44
- time.sleep(3600)
 
45
  for f in os.listdir(DOWNLOAD_DIR):
46
  path = os.path.join(DOWNLOAD_DIR, f)
47
- if os.path.exists(path) and (time.time() - os.path.getmtime(path) > 3600):
48
- os.remove(path)
 
 
 
 
 
 
49
 
50
  def get_menu():
51
  keyboard = [
@@ -54,62 +80,191 @@ def get_menu():
54
  ]
55
  return ReplyKeyboardMarkup(keyboard, resize_keyboard=True, one_time_keyboard=False)
56
 
 
 
 
 
 
 
 
 
 
 
 
57
  async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
58
- logger.info(f"User {update.effective_user.id} started the bot.")
59
- text = "سلام! فایل‌های خود را بفرستید و سپس روی «📄 ادغام فایل‌ها» کلیک کنید."
60
- await update.message.reply_text(text, reply_markup=get_menu())
 
 
 
 
61
 
62
  async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE):
63
  user_id = update.effective_user.id
64
- logger.info(f"User {user_id} sent a document.")
65
- file = await update.message.document.get_file()
66
- file_id = f"{user_id}_{uuid.uuid4().hex[:6]}.pdf"
67
- file_path = f"{DOWNLOAD_DIR}/{file_id}"
68
- await file.download_to_drive(file_path)
69
- if user_id not in user_files: user_files[user_id] = []
70
- user_files[user_id].append(file_path)
71
- await update.message.reply_text(f"✅ فایل {len(user_files[user_id])} دریافت شد.", reply_markup=get_menu())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  async def merge_pdfs(update: Update, context: ContextTypes.DEFAULT_TYPE):
74
  user_id = update.effective_user.id
75
- files = user_files.get(user_id, [])
 
 
 
 
76
  if not files:
77
- await update.message.reply_text("⚠️ صف شما خالی است!", reply_markup=get_menu())
78
  return
79
- await update.message.reply_text("⏳ در حال ادغام...")
80
- output_filename = f"merged_{user_id}_{uuid.uuid4().hex[:6]}.pdf"
81
- output_path = f"{DOWNLOAD_DIR}/{output_filename}"
 
 
 
 
 
 
 
82
  try:
83
  result = fitz.open()
84
- for pdf in files:
85
- with fitz.open(pdf) as doc:
 
 
 
86
  result.insert_pdf(doc)
 
 
 
 
 
 
87
  result.save(output_path)
 
 
 
 
 
 
 
 
 
 
 
 
88
  link = f"{SPACE_URL}/download/{output_filename}"
89
- await update.message.reply_text(f"🎉 فایل آماده است:\n📥 {link}", reply_markup=get_menu())
90
- user_files[user_id] = []
 
 
 
 
 
 
 
91
  except Exception as e:
92
- logger.error(f"Error merging PDFs: {e}")
93
- await update.message.reply_text("❌ خطایی در پردازش رخ داد.", reply_markup=get_menu())
 
 
 
 
 
 
 
 
 
94
 
95
  async def clear_queue(update: Update, context: ContextTypes.DEFAULT_TYPE):
96
- user_files[update.effective_user.id] = []
97
- await update.message.reply_text("🗑 صف پاک شد.", reply_markup=get_menu())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
  async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
100
  text = update.message.text
101
- if text == "📄 ادغام فایل‌ها": await merge_pdfs(update, context)
102
- elif text == "🗑 پاک کردن صف": await clear_queue(update, context)
103
- else: await update.message.reply_text("لطفاً از منو استفاده کنید.", reply_markup=get_menu())
 
 
 
 
 
 
 
 
 
 
 
104
 
105
  if __name__ == "__main__":
 
 
 
106
  threading.Thread(target=run_web, daemon=True).start()
107
  threading.Thread(target=cleanup_files, daemon=True).start()
108
-
109
  app = Application.builder().token(TOKEN).build()
110
  app.add_handler(CommandHandler("start", start))
111
  app.add_handler(MessageHandler(filters.Document.PDF, handle_document))
112
  app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text))
113
-
114
  logger.info("Bot is polling...")
115
  app.run_polling(drop_pending_updates=True)
 
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)
67
+ try:
68
+ if os.path.isfile(path) and (now - os.path.getmtime(path) > MERGED_FILE_TTL):
69
+ os.remove(path)
70
+ logger.info(f"Cleaned up old file: {f}")
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 = [
 
80
  ]
81
  return ReplyKeyboardMarkup(keyboard, resize_keyboard=True, one_time_keyboard=False)
82
 
83
+ HELP_TEXT = (
84
+ "📖 راهنمای استفاده:\n\n"
85
+ "۱. فایل‌های PDF خود را به هر تعدادی که می‌خواهید یکی‌یکی بفرستید.\n"
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
 
103
  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
+
117
+ logger.info(f"User {user_id} sent a document: {doc.file_name}")
118
+ file = await doc.get_file()
119
+ file_id = f"{user_id}_{uuid.uuid4().hex[:8]}.pdf"
120
+ file_path = os.path.join(DOWNLOAD_DIR, file_id)
121
+
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)