Ahmad3g commited on
Commit
fae6cf3
Β·
1 Parent(s): 60b7790
Files changed (2) hide show
  1. config.py +13 -17
  2. main.py +96 -103
config.py CHANGED
@@ -1,49 +1,45 @@
1
  """
2
  config.py β€” Centralized configuration loader.
3
- Reads environment variables from the system (Hugging Face Secrets in production)
4
- or from a .env file (local development via python-dotenv).
5
  """
6
 
7
  import os
8
  from dotenv import load_dotenv
9
 
10
- # Load .env file if it exists (for local dev). In production (HF Spaces),
11
- # this does nothing β€” secrets are already in the environment.
12
  load_dotenv()
13
 
14
 
15
  class Config:
16
- """Holds all configuration values for the application."""
17
-
18
- # --- Telegram ---
19
- BOT_TOKEN: str = os.getenv("BOT_TOKEN", "")
20
  OWNER_USERNAME: str = os.getenv("OWNER_USERNAME", "Ahmad_Gebril")
21
 
22
- # --- Database ---
23
- DATABASE_URL: str = os.getenv("DATABASE_URL", "")
 
 
 
 
24
 
25
- # --- Web Server ---
26
- # Hugging Face Spaces require the app to listen on port 7860
27
  WEB_HOST: str = "0.0.0.0"
28
  WEB_PORT: int = 7860
29
 
30
- # --- Pagination ---
31
  ITEMS_PER_PAGE: int = 10
32
 
33
  @classmethod
34
  def validate(cls):
35
- """Check that all critical config values are present at startup."""
36
  missing = []
37
  if not cls.BOT_TOKEN:
38
  missing.append("BOT_TOKEN")
39
  if not cls.DATABASE_URL:
40
  missing.append("DATABASE_URL")
 
 
41
  if missing:
42
  raise EnvironmentError(
43
- f"❌ Missing critical environment variables: {', '.join(missing)}\n"
44
- f"Please check your .env file or Hugging Face Secrets."
45
  )
46
 
47
 
48
- # Instantiate config for easy importing
49
  config = Config()
 
1
  """
2
  config.py β€” Centralized configuration loader.
 
 
3
  """
4
 
5
  import os
6
  from dotenv import load_dotenv
7
 
 
 
8
  load_dotenv()
9
 
10
 
11
  class Config:
12
+ # Telegram
13
+ BOT_TOKEN: str = os.getenv("BOT_TOKEN", "")
 
 
14
  OWNER_USERNAME: str = os.getenv("OWNER_USERNAME", "Ahmad_Gebril")
15
 
16
+ # Database
17
+ DATABASE_URL: str = os.getenv("DATABASE_URL", "")
18
+
19
+ # Webhook β€” your HF Space public URL (no trailing slash)
20
+ # Example: https://ahmad3g-telegram-bot.hf.space
21
+ WEBHOOK_URL: str = os.getenv("WEBHOOK_URL", "")
22
 
23
+ # Web server
 
24
  WEB_HOST: str = "0.0.0.0"
25
  WEB_PORT: int = 7860
26
 
27
+ # Pagination
28
  ITEMS_PER_PAGE: int = 10
29
 
30
  @classmethod
31
  def validate(cls):
 
32
  missing = []
33
  if not cls.BOT_TOKEN:
34
  missing.append("BOT_TOKEN")
35
  if not cls.DATABASE_URL:
36
  missing.append("DATABASE_URL")
37
+ if not cls.WEBHOOK_URL:
38
+ missing.append("WEBHOOK_URL")
39
  if missing:
40
  raise EnvironmentError(
41
+ f"❌ Missing environment variables: {', '.join(missing)}"
 
42
  )
43
 
44
 
 
45
  config = Config()
main.py CHANGED
@@ -1,13 +1,22 @@
1
  """
2
- main.py β€” Application entry point.
3
-
4
- Runs TWO concurrent async services:
5
- 1. An aiohttp web server on port 7860 (Hugging Face keepalive β€” starts FIRST)
6
- 2. The aiogram Telegram bot (long polling)
7
-
8
- FIX: Removed bot["view_states"] assignment β€” aiogram v3 Bot object does NOT
9
- support item assignment. VIEW_STATES is a plain module-level dict in
10
- handlers/admin.py and is imported directly by any handler that needs it.
 
 
 
 
 
 
 
 
 
11
  """
12
 
13
  import asyncio
@@ -20,12 +29,16 @@ from aiogram import Bot, Dispatcher
20
  from aiogram.client.default import DefaultBotProperties
21
  from aiogram.enums import ParseMode
22
  from aiogram.fsm.storage.memory import MemoryStorage
 
 
 
 
23
 
24
  from config import config
25
  from database.db import init_db
26
  from handlers import admin, owner, user
27
 
28
- # ─────────────────────────── LOGGING ───────────────────────────
29
 
30
  logging.basicConfig(
31
  level=logging.INFO,
@@ -35,19 +48,15 @@ logging.basicConfig(
35
  )
36
  logger = logging.getLogger(__name__)
37
 
38
- # Track bot health for the status endpoint
39
  BOT_STATUS = {"running": False, "started_at": None, "error": None}
40
 
 
 
41
 
42
- # ══════════════════════════════════════════════════════════════
43
- # WEB SERVER (must respond on :7860 for Hugging Face)
44
- # ══════════════════════════════════════════════════════════════
45
 
46
  async def handle_root(request: web.Request) -> web.Response:
47
- """
48
- Root health-check endpoint.
49
- UptimeRobot pings this every 5 min to keep the Space awake.
50
- """
51
  status = "βœ… running" if BOT_STATUS["running"] else "⏳ starting"
52
  error = f"\n❌ Last error: {BOT_STATUS['error']}" if BOT_STATUS["error"] else ""
53
  body = (
@@ -60,7 +69,6 @@ async def handle_root(request: web.Request) -> web.Response:
60
 
61
 
62
  async def handle_health(request: web.Request) -> web.Response:
63
- """JSON health endpoint β€” handy for monitoring dashboards."""
64
  import json
65
  payload = {
66
  "status" : "ok" if BOT_STATUS["running"] else "starting",
@@ -75,36 +83,9 @@ async def handle_health(request: web.Request) -> web.Response:
75
  )
76
 
77
 
78
- async def run_web_server() -> None:
79
- """
80
- Starts aiohttp on 0.0.0.0:7860.
81
- This coroutine NEVER exits β€” keeps the HF Space alive.
82
- """
83
- app = web.Application()
84
- app.router.add_get("/", handle_root)
85
- app.router.add_get("/health", handle_health)
86
-
87
- runner = web.AppRunner(app)
88
- await runner.setup()
89
- site = web.TCPSite(runner, host=config.WEB_HOST, port=config.WEB_PORT)
90
- await site.start()
91
-
92
- logger.info(f"βœ… Web server listening on http://{config.WEB_HOST}:{config.WEB_PORT}/")
93
-
94
- # Keep this coroutine alive forever
95
- while True:
96
- await asyncio.sleep(3600)
97
-
98
-
99
- # ══════════════════════════════════════════════════════════════
100
- # TELEGRAM BOT
101
- # ══════════════════════════════════════════════════════════════
102
 
103
  def build_dispatcher() -> Dispatcher:
104
- """
105
- Creates the Dispatcher and registers all routers.
106
- Order: owner > admin > user β€” privileged handlers intercept first.
107
- """
108
  dp = Dispatcher(storage=MemoryStorage())
109
  dp.include_router(owner.router)
110
  dp.include_router(admin.router)
@@ -112,92 +93,104 @@ def build_dispatcher() -> Dispatcher:
112
  return dp
113
 
114
 
115
- async def run_bot() -> None:
116
- """
117
- Initialises the database, then starts the Telegram bot.
118
- Retries the DB connection up to 5 times before giving up.
119
- The web server keeps running even if the bot fails to start.
120
- """
121
- # ── 1. Config validation ──────────────────────────────────
 
122
  try:
123
  config.validate()
124
  except EnvironmentError as e:
125
  logger.critical(str(e))
126
  BOT_STATUS["error"] = str(e)
127
- return
128
-
129
- # ── 2. DB initialisation with retry ──────────────────────
130
- MAX_RETRIES = 5
131
- RETRY_DELAY = 5 # seconds
132
-
133
- for attempt in range(1, MAX_RETRIES + 1):
 
 
 
 
134
  try:
135
- logger.info(f"πŸ”Œ Connecting to database (attempt {attempt}/{MAX_RETRIES})…")
136
  await init_db()
137
  logger.info("βœ… Database ready.")
138
  break
139
  except Exception as exc:
140
  BOT_STATUS["error"] = str(exc)
141
- logger.error(f"❌ DB connection failed: {exc}")
142
- if attempt < MAX_RETRIES:
143
- logger.info(f"⏳ Retrying in {RETRY_DELAY}s…")
144
- await asyncio.sleep(RETRY_DELAY)
145
  else:
146
- logger.critical("πŸ’₯ Could not connect to database after all retries.")
147
- return
 
148
 
149
- # ── 3. Create Bot instance ────────────────────────────────
150
  bot = Bot(
151
  token=config.BOT_TOKEN,
152
  default=DefaultBotProperties(parse_mode=ParseMode.HTML),
153
  )
154
 
155
- # NOTE: VIEW_STATES is a plain module-level dict in handlers/admin.py.
156
- # Any handler that needs it imports it directly:
157
- # from handlers.admin import VIEW_STATES
158
- # No need to attach anything to the Bot object.
159
-
160
  dp = build_dispatcher()
161
 
162
- # ── 4. Start polling ──────────────────────────────────────
163
- BOT_STATUS["running"] = True
164
- BOT_STATUS["started_at"] = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
165
- BOT_STATUS["error"] = None
166
- logger.info("πŸš€ Bot is now polling for updates…")
167
 
 
 
 
 
 
168
  try:
169
  await bot.delete_webhook(drop_pending_updates=True)
170
- await dp.start_polling(
171
- bot,
172
  allowed_updates=dp.resolve_used_update_types(),
 
173
  )
 
 
 
 
174
  except Exception as exc:
175
- BOT_STATUS["running"] = False
176
- BOT_STATUS["error"] = str(exc)
177
- logger.critical(f"πŸ’₯ Bot polling crashed: {exc}")
178
- finally:
179
- await bot.session.close()
180
- logger.info("πŸ›‘ Bot session closed.")
 
181
 
 
 
182
 
183
- # ══════════════════════════════════════════════════════════════
184
- # ENTRY POINT
185
- # ══════════════════════════════════════════════════════════════
186
 
187
- async def main() -> None:
188
- """
189
- Runs the web server and the bot concurrently via asyncio.gather.
190
- The web server binds port 7860 within milliseconds.
191
- The bot's DB retry loop runs in the background without blocking it.
192
- """
193
- print(f"\n{'='*50}")
194
- print(f" Application Startup at {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC")
195
- print(f"{'='*50}\n")
196
 
197
- await asyncio.gather(
198
- run_web_server(),
199
- run_bot(),
200
- )
 
 
 
 
 
 
 
201
 
202
 
203
  if __name__ == "__main__":
 
1
  """
2
+ main.py β€” Webhook mode for Hugging Face Spaces.
3
+
4
+ WHY WEBHOOK INSTEAD OF POLLING:
5
+ ─────────────────────────────────────────────────────────
6
+ Hugging Face blocks outbound connections from Docker containers.
7
+ Polling requires the bot to connect OUT to api.telegram.org β€” blocked.
8
+ Webhook is the opposite: Telegram connects IN to our server β€” allowed.
9
+
10
+ HOW IT WORKS:
11
+ ─────────────────────────────────────────────────────────
12
+ 1. We tell Telegram: "Send all updates to https://OUR_HF_URL/webhook"
13
+ 2. Telegram POSTs each update to our aiohttp server
14
+ 3. aiogram processes it normally β€” no outbound polling needed
15
+
16
+ REQUIRED ENVIRONMENT VARIABLE (add to HF Secrets):
17
+ ─────────────────────────────────────────────────────────
18
+ WEBHOOK_URL = https://YOUR_USERNAME-YOUR_SPACE_NAME.hf.space
19
+ Example : https://ahmad3g-telegram-bot.hf.space
20
  """
21
 
22
  import asyncio
 
29
  from aiogram.client.default import DefaultBotProperties
30
  from aiogram.enums import ParseMode
31
  from aiogram.fsm.storage.memory import MemoryStorage
32
+ from aiogram.webhook.aiohttp_server import (
33
+ SimpleRequestHandler,
34
+ setup_application,
35
+ )
36
 
37
  from config import config
38
  from database.db import init_db
39
  from handlers import admin, owner, user
40
 
41
+ # ─────────────────────────── LOGGING ─────────────────────────────────────
42
 
43
  logging.basicConfig(
44
  level=logging.INFO,
 
48
  )
49
  logger = logging.getLogger(__name__)
50
 
 
51
  BOT_STATUS = {"running": False, "started_at": None, "error": None}
52
 
53
+ # Webhook path β€” Telegram will POST updates here
54
+ WEBHOOK_PATH = "/webhook"
55
 
56
+
57
+ # ─────────────────────────── HEALTH ENDPOINTS ────────────────────────────
 
58
 
59
  async def handle_root(request: web.Request) -> web.Response:
 
 
 
 
60
  status = "βœ… running" if BOT_STATUS["running"] else "⏳ starting"
61
  error = f"\n❌ Last error: {BOT_STATUS['error']}" if BOT_STATUS["error"] else ""
62
  body = (
 
69
 
70
 
71
  async def handle_health(request: web.Request) -> web.Response:
 
72
  import json
73
  payload = {
74
  "status" : "ok" if BOT_STATUS["running"] else "starting",
 
83
  )
84
 
85
 
86
+ # ─────────────────────────── DISPATCHER ──────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
  def build_dispatcher() -> Dispatcher:
 
 
 
 
89
  dp = Dispatcher(storage=MemoryStorage())
90
  dp.include_router(owner.router)
91
  dp.include_router(admin.router)
 
93
  return dp
94
 
95
 
96
+ # ─────────────────────────── MAIN ────────────────────────────────────────
97
+
98
+ async def main() -> None:
99
+ print(f"\n{'='*50}")
100
+ print(f" Startup at {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC")
101
+ print(f"{'='*50}\n")
102
+
103
+ # ── 1. Validate config ────────────────────────────────────
104
  try:
105
  config.validate()
106
  except EnvironmentError as e:
107
  logger.critical(str(e))
108
  BOT_STATUS["error"] = str(e)
109
+ # Still start the web server so HF Space shows something
110
+ app = web.Application()
111
+ app.router.add_get("/", handle_root)
112
+ runner = web.AppRunner(app)
113
+ await runner.setup()
114
+ await web.TCPSite(runner, config.WEB_HOST, config.WEB_PORT).start()
115
+ while True:
116
+ await asyncio.sleep(3600)
117
+
118
+ # ── 2. Database with retry ────────────────────────────────
119
+ for attempt in range(1, 6):
120
  try:
121
+ logger.info(f"πŸ”Œ DB connect attempt {attempt}/5…")
122
  await init_db()
123
  logger.info("βœ… Database ready.")
124
  break
125
  except Exception as exc:
126
  BOT_STATUS["error"] = str(exc)
127
+ logger.error(f"❌ DB failed: {exc}")
128
+ if attempt < 5:
129
+ await asyncio.sleep(5)
 
130
  else:
131
+ logger.critical("πŸ’₯ DB unavailable β€” bot will not start.")
132
+ # Web server still starts below
133
+ break
134
 
135
+ # ── 3. Bot + webhook setup ────────────────────────────────
136
  bot = Bot(
137
  token=config.BOT_TOKEN,
138
  default=DefaultBotProperties(parse_mode=ParseMode.HTML),
139
  )
140
 
 
 
 
 
 
141
  dp = build_dispatcher()
142
 
143
+ # Build the full webhook URL
144
+ webhook_url = f"{config.WEBHOOK_URL.rstrip('/')}{WEBHOOK_PATH}"
145
+ logger.info(f"πŸ”— Setting webhook: {webhook_url}")
 
 
146
 
147
+ # Register webhook with Telegram
148
+ # (This is an OUTBOUND call β€” but it only happens ONCE at startup,
149
+ # before HF fully locks down the container network in some cases.
150
+ # If it fails, set the webhook manually via browser:
151
+ # https://api.telegram.org/botTOKEN/setWebhook?url=WEBHOOK_URL)
152
  try:
153
  await bot.delete_webhook(drop_pending_updates=True)
154
+ await bot.set_webhook(
155
+ url=webhook_url,
156
  allowed_updates=dp.resolve_used_update_types(),
157
+ drop_pending_updates=True,
158
  )
159
+ logger.info("βœ… Webhook registered with Telegram.")
160
+ BOT_STATUS["running"] = True
161
+ BOT_STATUS["started_at"] = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
162
+ BOT_STATUS["error"] = None
163
  except Exception as exc:
164
+ logger.error(f"⚠️ Could not set webhook automatically: {exc}")
165
+ logger.info(
166
+ f"πŸ‘‰ Set it manually in browser:\n"
167
+ f" https://api.telegram.org/bot{config.BOT_TOKEN}"
168
+ f"/setWebhook?url={webhook_url}"
169
+ )
170
+ BOT_STATUS["error"] = f"Webhook not set: {exc}"
171
 
172
+ # ── 4. aiohttp app with webhook handler ───────────────────
173
+ app = web.Application()
174
 
175
+ # Health endpoints
176
+ app.router.add_get("/", handle_root)
177
+ app.router.add_get("/health", handle_health)
178
 
179
+ # aiogram webhook handler β€” Telegram POSTs updates here
180
+ SimpleRequestHandler(dispatcher=dp, bot=bot).register(app, path=WEBHOOK_PATH)
181
+ setup_application(app, dp, bot=bot)
 
 
 
 
 
 
182
 
183
+ # ── 5. Start server ───────────────────────────────────────
184
+ runner = web.AppRunner(app)
185
+ await runner.setup()
186
+ site = web.TCPSite(runner, host=config.WEB_HOST, port=config.WEB_PORT)
187
+ await site.start()
188
+ logger.info(f"βœ… Server on http://{config.WEB_HOST}:{config.WEB_PORT}/")
189
+ logger.info(f"βœ… Webhook endpoint: {WEBHOOK_PATH}")
190
+
191
+ # Keep running forever
192
+ while True:
193
+ await asyncio.sleep(3600)
194
 
195
 
196
  if __name__ == "__main__":