CoolFace
Apppublic

Fyfgfgfg/Web-Project-screenshot

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
app.py723 linesDownload Raw Back to root
1import os2import json3import logging4import asyncio5import re6import socket7import gc8from io import BytesIO9from datetime import datetime, date10from contextlib import asynccontextmanager11 12from PIL import Image13import gradio as gr14from fastapi import FastAPI, Request15from fastapi.responses import JSONResponse, PlainTextResponse16import uvicorn17import aiohttp18 19from telegram import Update20from telegram.ext import (21    Application as TGApp,22    CommandHandler, MessageHandler, filters, ContextTypes23)24from telegram.constants import ChatAction25from telegram.request import HTTPXRequest26from playwright.async_api import async_playwright27 28from amazon_aod import capture_aod_screenshot29 30# =====================================================31# === LOGGING ===32# =====================================================33logging.basicConfig(34    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',35    level=logging.INFO36)37logger = logging.getLogger(__name__)38 39# =====================================================40# === CONFIGURATION ===41# =====================================================42SERVICE_TOKEN = os.getenv('SERVICE_TOKEN')43PORT = int(os.getenv('PORT', 7860))44 45SCREENSHOT_WIDTH = 124046SCREENSHOT_HEIGHT = 64947SCREENSHOT_TIMEOUT = 6048SCREENSHOT_MAX_RETRIES = 249 50# =====================================================51# === GLOBAL STATE ===52# =====================================================53browser = None54browser_context = None55playwright_instance = None56msg_app = None57 58# =====================================================59# === ACCESS CONTROL SYSTEM ===60# =====================================================61authorized_users = {}62admin_ids = []63contact_username = "admin"64 65 66def load_users():67    global authorized_users, admin_ids, contact_username68    possible_paths = [69        os.path.join(os.path.dirname(os.path.abspath(__file__)), 'users.json'),70        '/app/users.json',71        'users.json'72    ]73    for filepath in possible_paths:74        try:75            with open(filepath, 'r') as f:76                data = json.load(f)77            authorized_users = data.get('authorized_users', {})78            admin_ids = data.get('admin_ids', [])79            contact_username = data.get('contact_username', 'admin')80            logger.info(f"Loaded {len(authorized_users)} users from {filepath}")81            return True82        except FileNotFoundError:83            continue84        except Exception as e:85            logger.error(f"Error loading users from {filepath}: {e}")86            continue87    logger.error("users.json not found!")88    return False89 90 91def is_user_authorized(user_id: int) -> dict:92    user_id_str = str(user_id)93    if user_id_str not in authorized_users:94        return {'authorized': False, 'reason': 'not_registered', 'info': None}95    user_info = authorized_users[user_id_str]96    expiry_str = user_info.get('expiry', '2000-01-01')97    try:98        expiry_date = datetime.strptime(expiry_str, '%Y-%m-%d').date()99    except ValueError:100        return {'authorized': False, 'reason': 'invalid_expiry', 'info': user_info}101    today = date.today()102    if today > expiry_date:103        return {104            'authorized': False, 'reason': 'expired', 'info': user_info,105            'expiry_date': expiry_str, 'days_expired': (today - expiry_date).days106        }107    return {108        'authorized': True, 'reason': 'active', 'info': user_info,109        'expiry_date': expiry_str, 'days_remaining': (expiry_date - today).days110    }111 112 113def is_admin(user_id: int) -> bool:114    return user_id in admin_ids115 116 117def get_denial_message(auth_result: dict) -> str:118    reason = auth_result.get('reason', 'unknown')119    if reason == 'not_registered':120        return (121            "๐Ÿšซ *Access Denied*\n\n"122            "You don't have access to this bot.\n\n"123            "This is a premium bot available to paid subscribers only.\n\n"124            f"๐Ÿ“ฉ Contact @{contact_username} to get access.\n\n"125            "๐Ÿ’Ž *Plans Available:*\nโ€ข Monthly subscription\nโ€ข Lifetime access\n\n"126            "Send your payment and get instant activation!"127        )128    elif reason == 'expired':129        expiry = auth_result.get('expiry_date', 'Unknown')130        days = auth_result.get('days_expired', 0)131        username = auth_result.get('info', {}).get('username', 'User')132        return (133            "โฐ *Subscription Expired*\n\n"134            f"Hey @{username}, your subscription expired on *{expiry}* "135            f"({days} day{'s' if days != 1 else ''} ago).\n\n"136            f"๐Ÿ“ฉ Contact @{contact_username} to renew.\n\n"137            "Renew now to continue! ๐Ÿ”„"138        )139    elif reason == 'invalid_expiry':140        return f"โš ๏ธ *Account Error*\n\nPlease contact @{contact_username} to fix this."141    else:142        return f"๐Ÿšซ *Access Denied*\n\n๐Ÿ“ฉ Contact @{contact_username} for access."143 144 145load_users()146 147# =====================================================148# === BROWSER MANAGEMENT ===149# =====================================================150async def init_browser():151    global browser, browser_context, playwright_instance152    for attempt in range(3):153        try:154            logger.info(f"Initializing browser (attempt {attempt + 1}/3)...")155            if not playwright_instance:156                playwright_instance = await async_playwright().start()157            browser = await playwright_instance.chromium.launch(158                headless=True,159                args=[160                    '--no-sandbox', '--disable-setuid-sandbox',161                    '--disable-dev-shm-usage', '--disable-gpu',162                    '--disable-software-rasterizer', '--disable-extensions',163                    '--disable-blink-features=AutomationControlled',164                    '--disable-web-security',165                    '--disable-features=IsolateOrigins,site-per-process',166                    '--no-first-run', '--no-zygote',167                    '--force-color-profile=srgb', '--disable-lcd-text',168                ]169            )170            browser_context = await browser.new_context(171                viewport={'width': SCREENSHOT_WIDTH, 'height': SCREENSHOT_HEIGHT + 250},172                user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',173                ignore_https_errors=True174            )175            test_page = await browser_context.new_page()176            await test_page.goto('about:blank', timeout=5000)177            await test_page.close()178            logger.info("Browser initialized successfully!")179            return True180        except Exception as e:181            logger.error(f"Browser init failed (attempt {attempt + 1}/3): {e}")182            for obj, name in [(browser_context, 'ctx'), (browser, 'browser')]:183                if obj:184                    try: await obj.close()185                    except: pass186            browser_context = None187            browser = None188            if attempt < 2:189                await asyncio.sleep(2)190    return False191 192 193async def close_browser():194    global browser, browser_context, playwright_instance195    for obj in [browser_context, browser]:196        if obj:197            try: await obj.close()198            except: pass199    if playwright_instance:200        try: await playwright_instance.stop()201        except: pass202    logger.info("Browser cleanup completed")203 204 205# =====================================================206# === SCREENSHOT LOGIC ===207# =====================================================208def extract_urls(text):209    return re.findall(r'https?://[^\s<>"{}|\\^`\[\]]+', text)210 211 212def get_url_type(url):213    url_lower = url.lower()214    if any(d in url_lower for d in ['fkrt.cc', 'fkrt.to', 'fkrt.site', 'fkrt.co']):215        return 'flipkart'216    elif any(d in url_lower for d in ['amazon.in', 'amazon.com', 'amzn.to', 'amzn.in', 'a.co', 'amzn.eu', 'amzn.asia']):217        return 'amazon'218    return 'default'219 220 221async def capture_screenshot(url, timeout=SCREENSHOT_TIMEOUT, max_retries=SCREENSHOT_MAX_RETRIES):222    global browser_context223    if not browser_context:224        return None225 226    url_type = get_url_type(url)227    logger.info(f"URL type: {url_type} for {url}")228 229    # Amazon AOD primary method230    if url_type == 'amazon' and browser:231        try:232            aod_bytes = await asyncio.wait_for(233                capture_aod_screenshot(url, browser), timeout=25234            )235            if aod_bytes is not None:236                logger.info("Amazon AOD screenshot success")237                return aod_bytes238            logger.info("Amazon AOD returned None, falling back")239        except asyncio.TimeoutError:240            logger.warning("Amazon AOD timed out, falling back")241        except Exception as e:242            logger.warning(f"Amazon AOD error: {e}, falling back")243 244    # Standard screenshot245    for attempt in range(max_retries):246        page = None247        try:248            page = await browser_context.new_page()249            await page.set_extra_http_headers({250                'Accept-Language': 'en-US,en;q=0.9',251                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'252            })253 254            async def route_handler(route):255                if route.request.resource_type in ["font", "media"]:256                    await route.abort()257                else:258                    await route.continue_()259 260            await page.route("**/*", route_handler)261 262            for strategy in ['commit', 'domcontentloaded', 'load']:263                try:264                    await page.goto(url, wait_until=strategy, timeout=timeout * 1000)265                    break266                except Exception as e:267                    if strategy == 'load':268                        raise269                    continue270 271            await page.wait_for_timeout(2000)272            await page.unroute("**/*")273            await page.wait_for_timeout(1500)274 275            if url_type == 'default':276                try:277                    await page.evaluate("window.scrollTo(0, document.body.scrollHeight / 2)")278                    await page.wait_for_timeout(1000)279                    await page.evaluate("window.scrollTo(0, 0)")280                    await page.wait_for_timeout(500)281                except: pass282            elif url_type == 'amazon':283                try:284                    await page.evaluate("window.scrollTo(0, 300)")285                    await page.wait_for_timeout(1500)286                    await page.evaluate("window.scrollTo(0, 0)")287                    await page.wait_for_timeout(800)288                except: pass289 290            if url_type == 'flipkart':291                screenshot_bytes = await page.screenshot(292                    full_page=False, type='jpeg', quality=85, animations='disabled',293                    clip={'x': 0, 'y': 100, 'width': SCREENSHOT_WIDTH, 'height': 540}294                )295            elif url_type == 'amazon':296                screenshot_bytes = await page.screenshot(297                    full_page=False, type='jpeg', quality=85, animations='disabled',298                    clip={'x': 0, 'y': 250, 'width': SCREENSHOT_WIDTH, 'height': SCREENSHOT_HEIGHT}299                )300            else:301                screenshot_bytes = await page.screenshot(302                    full_page=False, type='jpeg', quality=85, animations='disabled'303                )304 305            logger.info(f"Screenshot captured for: {url}")306            return screenshot_bytes307 308        except Exception as e:309            logger.error(f"Screenshot attempt {attempt + 1}/{max_retries} failed: {e}")310            if attempt < max_retries - 1:311                await asyncio.sleep(2)312            else:313                return None314        finally:315            if page:316                try: await page.close()317                except: pass318    return None319 320 321# =====================================================322# === TELEGRAM COMMAND HANDLERS ===323# =====================================================324async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):325    user_id = update.effective_user.id326    auth = is_user_authorized(user_id)327    if not auth['authorized']:328        await update.message.reply_text(get_denial_message(auth), parse_mode='Markdown')329        return330 331    days_info = ""332    if auth.get('days_remaining') is not None:333        days = auth['days_remaining']334        plan = auth.get('info', {}).get('plan', 'unknown')335        if plan == 'lifetime':336            days_info = "โ™พ๏ธ Lifetime Access"337        elif days <= 7:338            days_info = f"โš ๏ธ {days} day{'s' if days != 1 else ''} remaining!"339        else:340            days_info = f"โœ… {days} days remaining"341 342    await update.message.reply_text(343        f"๐ŸŸข *Bot is Active!*\n๐Ÿ“Š *Your Status:* {days_info}\n\n"344        "๐Ÿ‘‹ Welcome! Send me any message containing URLs, "345        "and I'll send you screenshots!\n\n"346        "*Features:*\nโ€ข Extract links from any message\nโ€ข Works with forwarded messages\n"347        "โ€ข Smart cropping for Amazon & Flipkart\nโ€ข Amazon AOD panel screenshots\n\n"348        "Just send or forward any message with links! ๐Ÿš€",349        parse_mode='Markdown'350    )351 352 353async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE):354    auth = is_user_authorized(update.effective_user.id)355    if not auth['authorized']:356        await update.message.reply_text(get_denial_message(auth), parse_mode='Markdown')357        return358    await update.message.reply_text(359        "๐Ÿ“– *How to use:*\n\n"360        "1. Send any message containing URLs\n"361        "2. I'll extract the links automatically\n"362        "3. Wait for screenshots (30-60 seconds)\n\n"363        "*Smart Cropping:*\n"364        "โ€ข ๐Ÿ›’ Amazon: AOD panel (576ร—239, primary)\n"365        "โ€ข ๐Ÿ“ฆ Amazon fallback: 1240ร—649\n"366        "โ€ข ๐Ÿ›๏ธ Flipkart: 1240ร—540\n"367        "โ€ข ๐ŸŒ Other sites: 1240ร—649\n\n"368        "*Commands:*\n/start /help /status /myaccount",369        parse_mode='Markdown'370    )371 372 373async def cmd_myaccount(update: Update, context: ContextTypes.DEFAULT_TYPE):374    user_id = update.effective_user.id375    auth = is_user_authorized(user_id)376    if not auth['authorized']:377        await update.message.reply_text(get_denial_message(auth), parse_mode='Markdown')378        return379    info = auth.get('info', {})380    plan = info.get('plan', 'Unknown')381    expiry = auth.get('expiry_date', 'Unknown')382    added = info.get('added_on', 'Unknown')383    days = auth.get('days_remaining', 0)384    username = info.get('username', 'Unknown')385 386    if plan == 'lifetime': status_emoji, days_text = "๐Ÿ‘‘", "Lifetime - Never expires"387    elif days <= 3: status_emoji, days_text = "๐Ÿ”ด", f"{days}d remaining - RENEW SOON!"388    elif days <= 7: status_emoji, days_text = "๐ŸŸก", f"{days}d remaining"389    else: status_emoji, days_text = "๐ŸŸข", f"{days}d remaining"390 391    await update.message.reply_text(392        f"๐Ÿ‘ค *My Account*\n\n๐Ÿ“› @{username}\n๐Ÿ†” `{user_id}`\n"393        f"๐Ÿ“‹ *{plan.title()}*\n๐Ÿ“… Since: {added}\nโฐ Expires: {expiry}\n"394        f"{status_emoji} {days_text}\n\nRenew? Contact @{contact_username}",395        parse_mode='Markdown'396    )397 398 399async def cmd_admin_users(update: Update, context: ContextTypes.DEFAULT_TYPE):400    if not is_admin(update.effective_user.id):401        await update.message.reply_text("๐Ÿšซ Admins only.")402        return403    if not authorized_users:404        await update.message.reply_text("๐Ÿ“ญ No users.")405        return406    today = date.today()407    msg = "๐Ÿ‘ฅ *Users:*\n\n"408    for uid, info in authorized_users.items():409        username = info.get('username', '?')410        plan = info.get('plan', '?')411        expiry_str = info.get('expiry', '?')412        try:413            ed = datetime.strptime(expiry_str, '%Y-%m-%d').date()414            dl = (ed - today).days415            if dl < 0: s = f"๐Ÿ”ด Exp {abs(dl)}d"416            elif plan == 'lifetime': s = "๐Ÿ‘‘"417            else: s = f"๐ŸŸข {dl}d"418        except: s = "โš ๏ธ"419        msg += f"โ€ข `{uid}` @{username} {plan} {s}\n"420    msg += f"\nTotal: {len(authorized_users)}"421    await update.message.reply_text(msg, parse_mode='Markdown')422 423 424async def cmd_status(update: Update, context: ContextTypes.DEFAULT_TYPE):425    auth = is_user_authorized(update.effective_user.id)426    if not auth['authorized']:427        await update.message.reply_text(get_denial_message(auth), parse_mode='Markdown')428        return429    if browser and browser_context:430        await update.message.reply_text(431            "๐ŸŸข Active\n๐Ÿ“ธ Browser: Ready\n"432            "โ€ข Amazon AOD: 576ร—239\nโ€ข Amazon fallback: 1240ร—649\n"433            "โ€ข Flipkart: 1240ร—540\nโ€ข Default: 1240ร—649"434        )435    else:436        await update.message.reply_text("๐ŸŸก Browser initializing, try again shortly.")437 438 439async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):440    user_id = update.effective_user.id441    auth = is_user_authorized(user_id)442    if not auth['authorized']:443        await update.message.reply_text(get_denial_message(auth), parse_mode='Markdown')444        return445    if not browser or not browser_context:446        await update.message.reply_text("โณ Service initializing. Try again shortly.")447        return448 449    message_text = update.message.text or update.message.caption or ""450    urls = extract_urls(message_text)451    if not urls:452        await update.message.reply_text("๐Ÿ” No links found. Send a message with URLs.")453        return454 455    confirm_msg = await update.message.reply_text(456        f"๐Ÿ”— Found {len(urls)} link(s)! Generating screenshots..."457    )458 459    successful = 0460    failed = 0461    for idx, url in enumerate(urls, 1):462        try:463            await context.bot.send_chat_action(chat_id=update.effective_chat.id, action=ChatAction.TYPING)464 465            if len(urls) > 1:466                try:467                    await confirm_msg.edit_text(f"โณ Processing {idx}/{len(urls)}...")468                except: pass469 470            screenshot_bytes = await capture_screenshot(url)471            if not screenshot_bytes:472                failed += 1473                await update.message.reply_text(f"โŒ Failed for:\n{url}")474                continue475 476            caption = message_text[:1024] if len(urls) == 1 else f"๐Ÿ“ธ {idx}/{len(urls)}\n๐Ÿ”— {url[:900]}"477            await context.bot.send_chat_action(chat_id=update.effective_chat.id, action=ChatAction.UPLOAD_PHOTO)478            await update.message.reply_photo(photo=BytesIO(screenshot_bytes), caption=caption)479            successful += 1480 481            if idx < len(urls):482                await asyncio.sleep(1)483        except Exception as e:484            failed += 1485            logger.error(f"Error URL {idx}: {e}", exc_info=True)486            try:487                await update.message.reply_text(f"โŒ Error for link {idx}: {str(e)[:100]}")488            except: pass489 490    try: await confirm_msg.delete()491    except: pass492 493    summary = f"โœ… Done! Success: {successful}"494    if failed > 0:495        summary += f", Failed: {failed}"496    try: await update.message.reply_text(summary)497    except: pass498 499 500async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):501    logger.error(f"Update error: {context.error}", exc_info=context.error)502 503 504# =====================================================505# === GRADIO WEB UI ===506# =====================================================507async def gradio_screenshot(url_text):508    if not url_text or not url_text.strip():509        return None, "Please enter a URL"510    if not browser or not browser_context:511        return None, "Browser still initializing..."512    try:513        url = url_text.strip()514        if not url.startswith(('http://', 'https://')):515            url = 'https://' + url516        img_bytes = await capture_screenshot(url)517        if img_bytes:518            return Image.open(BytesIO(img_bytes)), "Screenshot captured!"519        return None, "Failed to capture. Site may be blocking access."520    except Exception as e:521        return None, f"Error: {str(e)[:200]}"522 523 524async def gradio_get_status():525    b = "Ready" if (browser and browser_context) else "Starting..."526    m = "Connected" if msg_app else "Connecting..."527    return f"Browser: {b}\nMessaging: {m}\nUsers: {len(authorized_users)}"528 529 530with gr.Blocks(title="URL Preview Generator", theme=gr.themes.Soft()) as demo:531    gr.Markdown("# ๐Ÿ“ธ URL Preview Generator\nCapture screenshots of any webpage.")532 533    with gr.Row():534        url_input = gr.Textbox(label="URL", placeholder="https://example.com", lines=1, scale=4)535        capture_btn = gr.Button("๐Ÿ“ธ Capture", variant="primary", scale=1)536 537    output_image = gr.Image(label="Result", type="pil", height=400)538    result_text = gr.Textbox(label="Status", interactive=False, lines=1)539 540    with gr.Accordion("Service Status", open=False):541        status_box = gr.Textbox(label="Status", interactive=False, lines=3)542        gr.Button("Refresh").click(fn=gradio_get_status, outputs=[status_box])543 544    capture_btn.click(fn=gradio_screenshot, inputs=[url_input], outputs=[output_image, result_text])545    url_input.submit(fn=gradio_screenshot, inputs=[url_input], outputs=[output_image, result_text])546    demo.load(fn=gradio_get_status, outputs=[status_box])547 548 549# =====================================================550# === MESSAGING INIT (background) ===551# =====================================================552async def _init_bot_background():553    global msg_app554 555    if not SERVICE_TOKEN:556        logger.warning("No SERVICE_TOKEN set")557        return558 559    logger.info("Waiting for network...")560    await asyncio.sleep(8)561 562    # DNS563    for attempt in range(1, 31):564        try:565            socket.getaddrinfo("api.telegram.org", 443)566            logger.info(f"DNS ready on attempt {attempt}")567            break568        except socket.gaierror:569            logger.warning(f"DNS not ready ({attempt}/30)")570            await asyncio.sleep(10)571    else:572        logger.error("DNS never resolved")573        return574 575    # Connectivity test576    for attempt in range(1, 6):577        try:578            logger.info(f"Testing API (attempt {attempt}/5)...")579            async with aiohttp.ClientSession() as session:580                async with session.get(581                    f"https://api.telegram.org/bot{SERVICE_TOKEN}/getMe",582                    timeout=aiohttp.ClientTimeout(total=120)583                ) as resp:584                    data = await resp.json()585                    if data.get("ok"):586                        logger.info("API reachable!")587                        break588        except Exception as e:589            logger.warning(f"API test failed: {e}")590            await asyncio.sleep(10)591    else:592        logger.error("Cannot reach API")593        return594 595    # Build app596    for attempt in range(1, 6):597        try:598            logger.info(f"Initializing app (attempt {attempt}/5)...")599            req = HTTPXRequest(600                connect_timeout=120.0, read_timeout=120.0,601                write_timeout=120.0, pool_timeout=120.0,602                connection_pool_size=8,603            )604            msg_app = (605                TGApp.builder()606                .token(SERVICE_TOKEN)607                .request(req)608                .get_updates_request(HTTPXRequest(609                    connect_timeout=120.0, read_timeout=120.0,610                    write_timeout=120.0, pool_timeout=120.0,611                    connection_pool_size=8,612                ))613                .build()614            )615 616            msg_app.add_handler(CommandHandler("start", cmd_start))617            msg_app.add_handler(CommandHandler("help", cmd_help))618            msg_app.add_handler(CommandHandler("status", cmd_status))619            msg_app.add_handler(CommandHandler("myaccount", cmd_myaccount))620            msg_app.add_handler(CommandHandler("users", cmd_admin_users))621            msg_app.add_handler(MessageHandler(622                (filters.TEXT | filters.CAPTION) & ~filters.COMMAND, handle_message623            ))624            msg_app.add_error_handler(error_handler)625 626            await asyncio.wait_for(msg_app.initialize(), timeout=180)627            logger.info("App initialized")628            await asyncio.wait_for(msg_app.start(), timeout=60)629            logger.info("App started")630 631            # Set callback632            cb_url = os.getenv('RENDER_EXTERNAL_URL')633            if not cb_url:634                host = os.getenv('SPACE_HOST')635                if host:636                    cb_url = f"https://{host}"637            if cb_url:638                full = f"{cb_url}/callback/{SERVICE_TOKEN}"639                await asyncio.wait_for(640                    msg_app.bot.set_webhook(url=full, allowed_updates=Update.ALL_TYPES, drop_pending_updates=True),641                    timeout=120642                )643                logger.info(f"Callback set: {full}")644            else:645                logger.warning("No callback URL found")646 647            logger.info("Messaging service ready!")648            return649 650        except asyncio.TimeoutError:651            logger.error(f"Attempt {attempt}/5 timed out")652        except Exception as e:653            logger.error(f"Attempt {attempt}/5 failed: {e}")654 655        if msg_app:656            try: await msg_app.shutdown()657            except: pass658        msg_app = None659        if attempt < 5:660            await asyncio.sleep(15 * attempt)661 662    logger.error("Messaging init failed after all attempts")663 664 665# =====================================================666# === FASTAPI APP ===667# =====================================================668@asynccontextmanager669async def lifespan(app):670    print("=" * 50)671    print("  URL Preview Generator")672    print("=" * 50)673 674    logger.info("Initializing browser...")675    await init_browser()676    asyncio.create_task(_init_bot_background())677    print("Server starting...")678 679    yield680 681    logger.info("Shutting down...")682    await close_browser()683    if msg_app:684        try:685            await msg_app.stop()686            await msg_app.shutdown()687        except: pass688 689 690fastapi_app = FastAPI(title="URL Preview Generator", lifespan=lifespan)691 692 693@fastapi_app.get("/health")694async def health():695    return PlainTextResponse(696        f"OK - Browser:{'Ready' if browser else 'Starting'} Service:{'Active' if msg_app else 'Starting'}"697    )698 699 700@fastapi_app.post("/callback/{token}")701async def callback(token: str, request: Request):702    if token != SERVICE_TOKEN:703        return JSONResponse(status_code=403, content={"error": "forbidden"})704    if not msg_app:705        return JSONResponse(status_code=503, content={"error": "starting"})706    try:707        data = await request.json()708        update = Update.de_json(data, msg_app.bot)709        await msg_app.process_update(update)710        return JSONResponse(status_code=200, content={"ok": True})711    except Exception as e:712        logger.error(f"Callback error: {e}")713        return JSONResponse(status_code=500, content={"error": str(e)})714 715 716# Mount Gradio717app = gr.mount_gradio_app(fastapi_app, demo, path="/")718 719# =====================================================720# === MAIN ===721# =====================================================722if __name__ == '__main__':723    uvicorn.run(app, host='0.0.0.0', port=PORT)