File size: 8,940 Bytes
d9e283e
 
 
0ef2ff7
d9e283e
0ef2ff7
 
 
 
d9e283e
 
0ef2ff7
c82caa3
d9e283e
 
0ef2ff7
 
26f5df0
 
d9e283e
 
c82caa3
0ef2ff7
9c13ab4
d9e283e
9c13ab4
0ef2ff7
d9e283e
0ef2ff7
9c13ab4
0ef2ff7
9c13ab4
 
0ef2ff7
 
d9e283e
0ef2ff7
9c13ab4
d9e283e
 
9c13ab4
 
 
 
 
55581e2
9c13ab4
0ef2ff7
c82caa3
 
 
 
 
 
 
9c13ab4
 
 
 
 
 
 
 
0ef2ff7
d9e283e
9c13ab4
 
 
0ef2ff7
9c13ab4
 
d9e283e
 
 
9c13ab4
 
 
 
 
 
 
0ef2ff7
9c13ab4
 
 
 
 
 
 
 
 
 
 
0ef2ff7
 
9c13ab4
 
 
 
 
 
 
 
0ef2ff7
9c13ab4
 
 
 
 
0ef2ff7
d9e283e
 
 
9c13ab4
 
0ef2ff7
9c13ab4
 
d9e283e
0ef2ff7
d9e283e
9c13ab4
 
 
 
 
0ef2ff7
9c13ab4
 
 
c82caa3
 
9c13ab4
0ef2ff7
9c13ab4
c82caa3
9c13ab4
c82caa3
9c13ab4
 
0ef2ff7
9c13ab4
0ef2ff7
9c13ab4
 
c82caa3
0ef2ff7
9c13ab4
c82caa3
0ef2ff7
 
c82caa3
 
9c13ab4
 
0ef2ff7
9c13ab4
 
0ef2ff7
9c13ab4
0ef2ff7
 
c82caa3
 
 
0ef2ff7
 
 
 
 
ea3bc58
0ef2ff7
 
 
 
 
 
55581e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ea3bc58
55581e2
0ef2ff7
55581e2
 
 
0ef2ff7
 
 
 
 
 
 
 
 
d9e283e
0ef2ff7
 
 
9c13ab4
0ef2ff7
 
 
 
 
 
 
9c13ab4
0ef2ff7
 
 
55581e2
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
import os
import uuid
import time
import asyncio
import fitz  # PyMuPDF
import logging
from contextlib import asynccontextmanager

from fastapi import FastAPI, Request, Response
from fastapi.responses import FileResponse
import uvicorn

from telegram import Update, ReplyKeyboardMarkup, KeyboardButton
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes

# --- تنظیمات لاگ و متغیرها ---
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)
logger = logging.getLogger(__name__)

TOKEN = os.environ.get("TOKEN")
DOWNLOAD_DIR = "downloads"
SPACE_URL = "https://barnamemaker-pdfmeger.hf.space"
MERGED_FILE_TTL = 3600  # ۱ ساعت

os.makedirs(DOWNLOAD_DIR, exist_ok=True)
user_files: dict[int, list[str]] = {}
user_locks: dict[int, asyncio.Lock] = {}

def get_user_lock(user_id: int) -> asyncio.Lock:
    if user_id not in user_locks:
        user_locks[user_id] = asyncio.Lock()
    return user_locks[user_id]

# --- تسک پاکسازی در پس‌زمینه ---
async def cleanup_task():
    while True:
        await asyncio.sleep(600)
        now = time.time()
        for f in os.listdir(DOWNLOAD_DIR):
            path = os.path.join(DOWNLOAD_DIR, f)
            try:
                if os.path.isfile(path) and (now - os.path.getmtime(path) > MERGED_FILE_TTL):
                    os.remove(path)
                    logger.info(f"Cleaned up old file: {f}")
            except Exception as e:
                pass

# --- منوی دکمه‌ها ---
def get_menu():
    keyboard = [
        [KeyboardButton("📄 ادغام فایل‌ها")],
        [KeyboardButton("🗑 پاک کردن صف"), KeyboardButton("❓ راهنما")]
    ]
    return ReplyKeyboardMarkup(keyboard, resize_keyboard=True, one_time_keyboard=False)

HELP_TEXT = (
    "📖 راهنمای استفاده:\n\n"
    "۱. فایل‌های PDF خود را به هر تعدادی که می‌خواهید یکی‌یکی بفرستید.\n"
    "۲. روی «📄 ادغام فایل‌ها» کلیک کنید.\n"
    "۳. لینک دانلود فایل ادغام‌شده را دریافت کنید.\n\n"
    "برای پاک کردن صف: «🗑 پاک کردن صف»\n"
)

# --- منطق ربات (Handlers) ---
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    logger.info(f"User {user_id} started the bot.")
    await update.message.reply_text(
        "سلام! 👋\nفایل‌های PDF خود را بفرستید و سپس «📄 ادغام فایل‌ها» را بزنید.",
        reply_markup=get_menu()
    )

async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    doc = update.message.document

    if doc.mime_type != "application/pdf":
        await update.message.reply_text("⚠️ فقط فایل PDF قبول می‌شود.", reply_markup=get_menu())
        return

    lock = get_user_lock(user_id)
    async with lock:
        if user_id not in user_files:
            user_files[user_id] = []

        logger.info(f"User {user_id} sent a document: {doc.file_name}")
        file = await doc.get_file()
        file_id = f"{user_id}_{uuid.uuid4().hex[:8]}.pdf"
        file_path = os.path.join(DOWNLOAD_DIR, file_id)

        try:
            await file.download_to_drive(file_path)
        except Exception as e:
            logger.error(f"Download error: {e}")
            await update.message.reply_text("❌ خطا در دریافت فایل.", reply_markup=get_menu())
            return

        try:
            with fitz.open(file_path) as test_doc:
                if test_doc.page_count == 0:
                    raise ValueError("Empty PDF")
        except Exception:
            os.remove(file_path)
            await update.message.reply_text("⚠️ فایل PDF معتبر نیست.", reply_markup=get_menu())
            return

        user_files[user_id].append(file_path)
        count = len(user_files[user_id])

    await update.message.reply_text(f"✅ فایل {count} دریافت شد: {doc.file_name}", reply_markup=get_menu())

async def merge_pdfs(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    lock = get_user_lock(user_id)

    async with lock:
        files = list(user_files.get(user_id, []))

    if not files:
        await update.message.reply_text("⚠️ صف شما خالی است!", reply_markup=get_menu())
        return

    if len(files) == 1:
        await update.message.reply_text("⚠️ حداقل ۲ فایل برای ادغام لازم است.", reply_markup=get_menu())
        return

    msg = await update.message.reply_text(f"⏳ در حال ادغام {len(files)} فایل...")
    output_filename = f"merged_{user_id}_{uuid.uuid4().hex[:8]}.pdf"
    output_path = os.path.join(DOWNLOAD_DIR, output_filename)

    try:
        result = fitz.open()
        for pdf_path in files:
            if not os.path.exists(pdf_path): continue
            with fitz.open(pdf_path) as doc:
                result.insert_pdf(doc)

        result.save(output_path)
        result.close()

        async with lock:
            for f in user_files.get(user_id, []):
                if os.path.exists(f): os.remove(f)
            user_files[user_id] = []

        link = f"{SPACE_URL}/download/{output_filename}"
        await msg.edit_text(f"🎉 ادغام موفق!\n📥 لینک دانلود:\n{link}")

    except Exception as e:
        logger.error(f"Error merging: {e}")
        await msg.edit_text("❌ خطایی در پردازش رخ داد.")

async def clear_queue(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    lock = get_user_lock(user_id)
    async with lock:
        files = list(user_files.get(user_id, []))
        user_files[user_id] = []
    
    for f in files:
        if os.path.exists(f): os.remove(f)
    await update.message.reply_text("🗑 صف پاک شد.", reply_markup=get_menu())

async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
    text = update.message.text
    if text == "📄 ادغام فایل‌ها": await merge_pdfs(update, context)
    elif text == "🗑 پاک کردن صف": await clear_queue(update, context)
    elif text == "❓ راهنما": await update.message.reply_text(HELP_TEXT, reply_markup=get_menu())

# --- تنظیمات اتصال اصلی (Webhook) ---
ptb_app = Application.builder().token(TOKEN).connect_timeout(40.0).read_timeout(40.0).build()
ptb_app.add_handler(CommandHandler("start", start))
ptb_app.add_handler(MessageHandler(filters.Document.PDF, handle_document))
ptb_app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text))

@asynccontextmanager
async def lifespan(app: FastAPI):
    # این تابع در پس‌زمینه اجرا می‌شود تا سرور وب منتظر نماند
    async def setup_telegram():
        await asyncio.sleep(3)  # صبر می‌کنیم تا سرور وب کاملاً بالا بیاید
        try:
            logger.info("⏳ در حال راه‌اندازی ربات در پس‌زمینه...")
            await ptb_app.initialize()
            await ptb_app.bot.set_webhook(
                url=f"{SPACE_URL}/webhook", 
                drop_pending_updates=True,
                connect_timeout=60.0,
                read_timeout=60.0
            )
            await ptb_app.start()
            logger.info("✅ Webhook با موفقیت ثبت شد و ربات آماده است!")
            asyncio.create_task(cleanup_task())
        except Exception as e:
            logger.error(f"❌ خطا در تنظیم Webhook: {e}")

    # استارت تسک در پس‌زمینه
    setup_task = asyncio.create_task(setup_telegram())
    
    yield
    
    # خاموش کردن امن
    try:
        setup_task.cancel()
        await ptb_app.stop()
        await ptb_app.shutdown()
    except Exception:
        pass

app_web = FastAPI(lifespan=lifespan)

@app_web.post("/webhook")
async def telegram_webhook(request: Request):
    req_json = await request.json()
    update = Update.de_json(req_json, ptb_app.bot)
    await ptb_app.process_update(update)
    return Response(status_code=200)

@app_web.get("/")
async def root():
    return {"message": "Bot Webhook is Active!"}

@app_web.get("/download/{filename}")
async def get_file(filename: str):
    if ".." in filename or "/" in filename: return {"error": "Invalid"}
    file_path = os.path.join(DOWNLOAD_DIR, filename)
    if os.path.exists(file_path):
        return FileResponse(file_path, media_type="application/pdf")
    return {"error": "Not found"}

if __name__ == "__main__":
    if not TOKEN:
        raise RuntimeError("TOKEN is missing!")
    uvicorn.run("app:app_web", host="0.0.0.0", port=7860)