Spaces:
Running
Running
feat: add HF Spaces deployment (Dockerfile + /health endpoint for UptimeRobot)
Browse files- Dockerfile +46 -0
- README.md +45 -0
- main.py +114 -1
- 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(
|
| 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">✓ 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
|