| """ |
| Deploys two Cloudflare Workers: |
| 1. Keep-alive cron worker β pings the HF Space every 4 minutes |
| 2. Telegram proxy worker β forwards Telegram API calls |
| """ |
| import os, json, requests, time |
|
|
| ACCOUNT_ID = os.environ.get("CLOUDFLARE_ACCOUNT_ID") |
| CF_TOKEN = os.environ.get("CLOUDFLARE_WORKERS_TOKEN") |
| SPACE_URL = os.environ.get("SPACE_URL", "https://your-space.hf.space") |
| BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "") |
| GATEWAY_TOKEN = os.environ.get("GATEWAY_TOKEN", "changeme") |
|
|
| AUTH_HEADERS = {"Authorization": f"Bearer {CF_TOKEN}"} |
|
|
| |
| KEEPALIVE_SCRIPT = f""" |
| addEventListener('scheduled', event => {{ |
| event.waitUntil(handleScheduled()); |
| }}); |
| |
| async function handleScheduled() {{ |
| const url = '{SPACE_URL}/health'; |
| try {{ |
| const res = await fetch(url, {{ method: 'GET' }}); |
| console.log('[keep-alive]', new Date().toISOString(), res.status); |
| }} catch (e) {{ |
| console.error('[keep-alive] failed:', e.message); |
| }} |
| }} |
| """ |
|
|
| |
| TELEGRAM_PROXY_SCRIPT = """ |
| addEventListener('fetch', event => { |
| event.respondWith(handleRequest(event.request)); |
| }); |
| |
| async function handleRequest(request) { |
| const url = new URL(request.url); |
| const target = 'https://api.telegram.org' + url.pathname + url.search; |
| |
| const newReq = new Request(target, { |
| method: request.method, |
| headers: request.headers, |
| body: request.method !== 'GET' && request.method !== 'HEAD' |
| ? request.body : undefined |
| }); |
| |
| const response = await fetch(newReq); |
| |
| // Pass response back with CORS headers |
| const newHeaders = new Headers(response.headers); |
| newHeaders.set('Access-Control-Allow-Origin', '*'); |
| |
| return new Response(response.body, { |
| status: response.status, |
| headers: newHeaders |
| }); |
| } |
| """ |
|
|
|
|
| def get_subdomain(): |
| """Get the workers.dev subdomain for this account.""" |
| resp = requests.get( |
| f"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/workers/subdomain", |
| headers=AUTH_HEADERS |
| ) |
| data = resp.json() |
| if data.get("success"): |
| return data["result"]["subdomain"] |
| return None |
|
|
|
|
| def deploy_worker(name, script): |
| """ |
| Deploy a Worker script using the correct multipart format. |
| Cloudflare requires TWO parts: |
| - 'metadata' : JSON describing the script |
| - 'script' : The actual JS content |
| """ |
| url = f"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/workers/scripts/{name}" |
|
|
| |
| metadata = { |
| "body_part" : "script", |
| "bindings" : [], |
| "compatibility_date": "2024-01-01" |
| } |
|
|
| resp = requests.put( |
| url, |
| headers=AUTH_HEADERS, |
| files={ |
| "metadata": ( |
| "metadata.json", |
| json.dumps(metadata), |
| "application/json" |
| ), |
| "script": ( |
| "worker.js", |
| script, |
| "application/javascript" |
| ) |
| } |
| ) |
|
|
| result = resp.json() |
|
|
| if result.get("success"): |
| print(f"β
Worker '{name}' deployed successfully") |
| return True |
| else: |
| print(f"β Failed to deploy '{name}': {result.get('errors')}") |
| return False |
|
|
|
|
| def set_cron(name, cron_expression): |
| """Attach a cron trigger to an existing worker.""" |
| url = f"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/workers/scripts/{name}/schedules" |
|
|
| resp = requests.put( |
| url, |
| headers={**AUTH_HEADERS, "Content-Type": "application/json"}, |
| json=[{"cron": cron_expression}] |
| ) |
| result = resp.json() |
|
|
| if result.get("success"): |
| print(f" β° Cron trigger set: {cron_expression}") |
| return True |
| else: |
| print(f" β οΈ Cron setup failed: {result.get('errors')}") |
| return False |
|
|
|
|
| def enable_workers_dev_route(name): |
| """Enable the workers.dev route so the worker has a public URL.""" |
| url = f"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/workers/scripts/{name}/subdomain" |
|
|
| resp = requests.post( |
| url, |
| headers={**AUTH_HEADERS, "Content-Type": "application/json"}, |
| json={"enabled": True} |
| ) |
| result = resp.json() |
|
|
| if result.get("success"): |
| print(f" π workers.dev route enabled") |
| return True |
| else: |
| |
| print(f" β οΈ Could not enable workers.dev route: {result.get('errors')}") |
| return False |
|
|
|
|
| def set_telegram_webhook(proxy_url): |
| """Point Telegram webhook at our HF Space β via Cloudflare proxy.""" |
| if not BOT_TOKEN: |
| print(" β οΈ TELEGRAM_BOT_TOKEN not set β skipping webhook") |
| return |
|
|
| if not proxy_url: |
| print(" β οΈ No proxy_url provided β skipping webhook") |
| return |
|
|
| webhook_url = f"{SPACE_URL}/telegram" |
|
|
| |
| |
| resp = requests.get( |
| f"{proxy_url}/bot{BOT_TOKEN}/setWebhook", |
| params={"url": webhook_url}, |
| timeout=30 |
| ) |
|
|
| try: |
| data = resp.json() |
| if data.get("ok"): |
| print(f" π‘ Telegram webhook set β {webhook_url}") |
| else: |
| print(f" β οΈ Webhook set failed: {data}") |
| except Exception as e: |
| print(f" β Failed to parse webhook response: {e}") |
| print(f" Raw response: {resp.text}") |
|
|
|
|
| if __name__ == "__main__": |
| if not ACCOUNT_ID or not CF_TOKEN: |
| print("β οΈ CLOUDFLARE_ACCOUNT_ID or CLOUDFLARE_WORKERS_TOKEN not set β skipping") |
| exit(0) |
|
|
| print("βοΈ Deploying Cloudflare Workers...") |
|
|
| |
| if deploy_worker("llm-space-keepalive", KEEPALIVE_SCRIPT): |
| set_cron("llm-space-keepalive", "*/4 * * * *") |
|
|
| |
| if deploy_worker("llm-space-telegram-proxy", TELEGRAM_PROXY_SCRIPT): |
| enable_workers_dev_route("llm-space-telegram-proxy") |
|
|
| |
| subdomain = get_subdomain() |
| if subdomain: |
| proxy_url = f"https://llm-space-telegram-proxy.{subdomain}.workers.dev" |
| print(f"\n β
Telegram Proxy URL: {proxy_url}") |
| print(f" π Add this to HF Space Secrets:") |
| print(f" CLOUDFLARE_TELEGRAM_PROXY_URL = {proxy_url}") |
|
|
| time.sleep(3) |
| set_telegram_webhook(proxy_url) |
| else: |
| print(" β οΈ Could not determine workers.dev subdomain") |
| else: |
| print(" β Telegram proxy worker deployment failed β skipping webhook setup") |
|
|
| print("\nβοΈ Cloudflare setup complete!") |