thehnx commited on
Commit
e2c6c50
·
1 Parent(s): da49547

feat: add HF Spaces deployment (Dockerfile + /health endpoint for UptimeRobot)

Browse files
Files changed (4) hide show
  1. Dockerfile +46 -0
  2. README.md +45 -0
  3. main.py +114 -1
  4. requirements.txt +1 -0
Dockerfile ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ─────────────────────────────────────────────────────────────────────────────
2
+ # PepBielsa Monitor Bot — Dockerfile for Hugging Face Spaces
3
+ # ─────────────────────────────────────────────────────────────────────────────
4
+ # HF Spaces runs Docker containers and exposes port 7860 to the web.
5
+ # UptimeRobot pings https://<space>.hf.space/health every 5 min to keep it alive.
6
+ # ─────────────────────────────────────────────────────────────────────────────
7
+
8
+ FROM python:3.11-slim
9
+
10
+ # Prevents Python from writing .pyc files and enables stdout/stderr logging.
11
+ ENV PYTHONDONTWRITEBYTECODE=1 \
12
+ PYTHONUNBUFFERED=1 \
13
+ PORT=7860
14
+
15
+ WORKDIR /app
16
+
17
+ # Install OS-level dependencies first (cached layer).
18
+ RUN apt-get update && apt-get install -y --no-install-recommends \
19
+ gcc \
20
+ libffi-dev \
21
+ && rm -rf /var/lib/apt/lists/*
22
+
23
+ # Install Python dependencies (cached unless requirements.txt changes).
24
+ COPY requirements.txt .
25
+ RUN pip install --no-cache-dir -r requirements.txt
26
+
27
+ # Copy application source.
28
+ COPY . .
29
+
30
+ # Create directories the bot writes to at runtime.
31
+ RUN mkdir -p temp_media
32
+
33
+ # HF Spaces runs containers as a non-root user by default.
34
+ # Create a dedicated user for security best-practices.
35
+ RUN useradd -m -u 1000 botuser \
36
+ && chown -R botuser:botuser /app
37
+ USER botuser
38
+
39
+ # HF Spaces expects the container to listen on port 7860.
40
+ EXPOSE 7860
41
+
42
+ # Health-check so Docker/HF knows the service is alive.
43
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
44
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:7860/health')" || exit 1
45
+
46
+ CMD ["python", "main.py"]
README.md ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: PepBielsa Monitor Bot
3
+ emoji: ⚽
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ license: mit
10
+ ---
11
+
12
+ # ⚽ PepBielsa Monitor Bot
13
+
14
+ A Telegram channel monitor and forwarder with AI-powered deduplication (Groq llama-3.1-8b-instant).
15
+
16
+ ## Health Check
17
+
18
+ A lightweight web server runs alongside the bot at `/health` — used by UptimeRobot to prevent the Space from sleeping.
19
+
20
+ ```
21
+ GET https://<your-space>.hf.space/health
22
+ ```
23
+
24
+ ## Environment Variables
25
+
26
+ Set these as **Secrets** in your HF Space settings (`Settings → Repository secrets`):
27
+
28
+ | Variable | Description |
29
+ |----------|-------------|
30
+ | `API_ID` | Telegram API ID |
31
+ | `API_HASH` | Telegram API Hash |
32
+ | `PHONE_NUMBER` | Your Telegram phone number |
33
+ | `SESSION_STRING` | Generated by `session_export.py` |
34
+ | `SOURCE_CHANNELS` | Comma-separated channel usernames |
35
+ | `DESTINATION_CHANNEL` | Target channel username |
36
+ | `GROQ_API_KEY` | Groq API key (free at console.groq.com) |
37
+ | `HF_TOKEN` | HF write token (for persistent store) |
38
+ | `HF_DATASET_REPO` | e.g. `yourname/pepbielsa-processed` |
39
+
40
+ ## UptimeRobot Setup
41
+
42
+ 1. Create a **HTTP(s)** monitor
43
+ 2. URL: `https://<your-space-name>.hf.space/health`
44
+ 3. Monitoring interval: **5 minutes**
45
+ 4. Keyword monitor (optional): set keyword to `Online`
main.py CHANGED
@@ -44,6 +44,12 @@ import time
44
  from pathlib import Path
45
  from typing import Dict, List, Optional, Tuple
46
 
 
 
 
 
 
 
47
  from telethon import TelegramClient, events
48
  from telethon.errors import (
49
  ChannelPrivateError,
@@ -840,6 +846,94 @@ class TelegramMonitor:
840
  # DB is closed only on final shutdown (not between reconnects).
841
 
842
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
843
  # ═════════════════════════════════════════════════════════════════════════════
844
  # Entry-point
845
  # ═════════════════════════════════════════════════════════════════════════════
@@ -869,8 +963,27 @@ def main() -> None:
869
 
870
  monitor = TelegramMonitor(cfg)
871
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
872
  try:
873
- asyncio.run(monitor.run_forever())
874
  except KeyboardInterrupt:
875
  pass
876
  finally:
 
44
  from pathlib import Path
45
  from typing import Dict, List, Optional, Tuple
46
 
47
+ try:
48
+ from aiohttp import web as aiohttp_web
49
+ _AIOHTTP_AVAILABLE = True
50
+ except ImportError:
51
+ _AIOHTTP_AVAILABLE = False
52
+
53
  from telethon import TelegramClient, events
54
  from telethon.errors import (
55
  ChannelPrivateError,
 
846
  # DB is closed only on final shutdown (not between reconnects).
847
 
848
 
849
+ # ═════════════════════════════════════════════════════════════════════════════
850
+ # Health-check web server (for UptimeRobot / HF Spaces keep-alive)
851
+ # ═════════════════════════════════════════════════════════════════════════════
852
+
853
+ _HEALTH_HTML = """\
854
+ <!DOCTYPE html>
855
+ <html lang="en">
856
+ <head>
857
+ <meta charset="UTF-8">
858
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
859
+ <title>PepBielsa Bot — Status</title>
860
+ <style>
861
+ *{{margin:0;padding:0;box-sizing:border-box}}
862
+ body{{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
863
+ background:#0f172a;color:#e2e8f0;display:flex;align-items:center;
864
+ justify-content:center;min-height:100vh}}
865
+ .card{{background:#1e293b;border:1px solid #334155;border-radius:16px;
866
+ padding:40px 48px;text-align:center;max-width:420px;width:90%}}
867
+ .dot{{width:14px;height:14px;background:#22c55e;border-radius:50%;
868
+ display:inline-block;margin-right:8px;
869
+ box-shadow:0 0 0 4px rgba(34,197,94,.25);animation:pulse 2s infinite}}
870
+ @keyframes pulse{{0%,100%{{box-shadow:0 0 0 4px rgba(34,197,94,.25)}}
871
+ 50%{{box-shadow:0 0 0 8px rgba(34,197,94,.1)}}}}
872
+ h1{{font-size:1.6rem;font-weight:700;margin-bottom:8px}}
873
+ p{{color:#94a3b8;font-size:.9rem;margin-top:12px}}
874
+ .badge{{display:inline-block;background:#22c55e22;color:#4ade80;
875
+ border:1px solid #22c55e55;border-radius:999px;
876
+ padding:4px 14px;font-size:.8rem;margin-top:20px}}
877
+ </style>
878
+ </head>
879
+ <body>
880
+ <div class="card">
881
+ <h1><span class="dot"></span>PepBielsa Bot</h1>
882
+ <p>Telegram channel monitor is active and forwarding.</p>
883
+ <div class="badge">&#x2713;&nbsp;Online</div>
884
+ </div>
885
+ </body>
886
+ </html>
887
+ """
888
+
889
+
890
+ async def _health_handler(request) -> "aiohttp_web.Response":
891
+ """Return an HTML health page that satisfies UptimeRobot keyword monitors."""
892
+ return aiohttp_web.Response(
893
+ text=_HEALTH_HTML,
894
+ content_type="text/html",
895
+ charset="utf-8",
896
+ headers={
897
+ # Mimic a real browser server so UptimeRobot treats us as a web page.
898
+ "Server": "nginx/1.25.3",
899
+ "X-Content-Type-Options": "nosniff",
900
+ "Cache-Control": "no-cache, no-store, must-revalidate",
901
+ },
902
+ )
903
+
904
+
905
+ async def run_health_server(port: int = 7860) -> None:
906
+ """Start the aiohttp health-check server on *port*.
907
+
908
+ HF Spaces expects the app to listen on port 7860 by default.
909
+ Override via the PORT environment variable.
910
+ """
911
+ if not _AIOHTTP_AVAILABLE:
912
+ log.warning(
913
+ "aiohttp not installed — health-check server disabled. "
914
+ "Add 'aiohttp>=3.9.0' to requirements.txt."
915
+ )
916
+ return
917
+
918
+ app = aiohttp_web.Application()
919
+ app.router.add_get("/", _health_handler)
920
+ app.router.add_get("/health", _health_handler)
921
+
922
+ runner = aiohttp_web.AppRunner(app)
923
+ await runner.setup()
924
+ site = aiohttp_web.TCPSite(runner, "0.0.0.0", port)
925
+ await site.start()
926
+ log.info("Health-check server listening on http://0.0.0.0:%d/health", port)
927
+
928
+ # Keep running forever (cancelled when the event loop shuts down).
929
+ try:
930
+ await asyncio.Event().wait()
931
+ except asyncio.CancelledError:
932
+ pass
933
+ finally:
934
+ await runner.cleanup()
935
+
936
+
937
  # ═════════════════════════════════════════════════════════════════════════════
938
  # Entry-point
939
  # ═════════════════════════════════════════════════════════════════════════════
 
963
 
964
  monitor = TelegramMonitor(cfg)
965
 
966
+ # Determine health-server port (HF Spaces default: 7860).
967
+ health_port = int(os.getenv("PORT", "7860"))
968
+
969
+ async def _run_all() -> None:
970
+ """Run the bot and the health-check server concurrently."""
971
+ bot_task = asyncio.create_task(monitor.run_forever())
972
+ health_task = asyncio.create_task(run_health_server(health_port))
973
+ try:
974
+ await asyncio.gather(bot_task, health_task)
975
+ except KeyboardInterrupt:
976
+ pass
977
+ finally:
978
+ bot_task.cancel()
979
+ health_task.cancel()
980
+ with asyncio.suppress(asyncio.CancelledError):
981
+ await bot_task
982
+ with asyncio.suppress(asyncio.CancelledError):
983
+ await health_task
984
+
985
  try:
986
+ asyncio.run(_run_all())
987
  except KeyboardInterrupt:
988
  pass
989
  finally:
requirements.txt CHANGED
@@ -1,5 +1,6 @@
1
  telethon>=1.36.0
2
  python-dotenv>=1.0.0
3
  aiofiles>=23.2.1
 
4
  groq>=0.9.0
5
  huggingface_hub>=0.23.0
 
1
  telethon>=1.36.0
2
  python-dotenv>=1.0.0
3
  aiofiles>=23.2.1
4
+ aiohttp>=3.9.0
5
  groq>=0.9.0
6
  huggingface_hub>=0.23.0