Spaces:
Running on Zero
Running on Zero
Download main.py from Catalna/ginchiee-bot: direct link, hf CLI and curl.
- Browser
- Download file 11.1 kB
-
https://huggingface.co/spaces/Catalna/ginchiee-bot/resolve/main/main.py
- Command line
-
hf download hf://spaces/Catalna/ginchiee-bot/main.py
-
curl -L -o main.py https://huggingface.co/spaces/Catalna/ginchiee-bot/resolve/main/main.py
11.1 kB
| """ | |
| main.py | |
| Entry point for Ginchiee Bot β AI Diet Companion. | |
| Startup sequence: | |
| 1. Load config | |
| 2. Init database & run migrations | |
| 3. Init AI service | |
| 4. Register Telegram handlers | |
| 5. Start APScheduler | |
| 6. Start polling | |
| """ | |
| import logging | |
| import os | |
| import sys | |
| import threading | |
| import warnings | |
| from http.server import BaseHTTPRequestHandler, HTTPServer | |
| # Suppress known non-critical library warnings | |
| warnings.filterwarnings("ignore", category=FutureWarning, module="google.api_core") | |
| try: | |
| from telegram.warnings import PTBUserWarning | |
| warnings.filterwarnings("ignore", category=PTBUserWarning) | |
| except ImportError: | |
| pass | |
| from telegram import BotCommand, Update | |
| from telegram.error import NetworkError, TimedOut | |
| from telegram.ext import ( | |
| Application, | |
| ApplicationHandlerStop, | |
| CallbackQueryHandler, | |
| ContextTypes, | |
| MessageHandler, | |
| TypeHandler, | |
| filters, | |
| ) | |
| from telegram.request import HTTPXRequest | |
| from config.settings import ( | |
| ALLOWED_USER_IDS, | |
| GEMINI_MODEL, | |
| LOG_LEVEL, | |
| TELEGRAM_BOT_TOKEN, | |
| ) | |
| from database.connection import close_db, init_db | |
| from database.migrations import run_migrations | |
| from services.ai_service import AIService | |
| from services.scheduler_service import start_scheduler, stop_scheduler | |
| # ββ Health Check HTTP Server for Cloud Hosting (HF Spaces / Render) βββββββββββ | |
| class HealthHandler(BaseHTTPRequestHandler): | |
| def do_GET(self): | |
| self.send_response(200) | |
| self.send_header("Content-type", "text/plain; charset=utf-8") | |
| self.end_headers() | |
| self.wfile.write("Ginchiee Bot is running 24/7! πΈ\n".encode("utf-8")) | |
| def log_message(self, format, *args): | |
| pass # Suppress health check access logs | |
| def start_health_server(port: int = 7860) -> None: | |
| """Run lightweight HTTP server in a background daemon thread.""" | |
| try: | |
| server = HTTPServer(("0.0.0.0", port), HealthHandler) | |
| thread = threading.Thread(target=server.serve_forever, daemon=True) | |
| thread.start() | |
| logging.getLogger(__name__).info(f"Health check HTTP server started on port {port}") | |
| except Exception as e: | |
| logging.getLogger(__name__).warning(f"Could not start health check server: {e}") | |
| # ββ Handlers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| from bot.conversations.onboarding import build_onboarding_conversation | |
| from bot.handlers.activity_handler import build_activity_handlers | |
| from bot.handlers.food_handler import build_food_handlers | |
| from bot.handlers.help_handler import build_help_handlers | |
| from bot.handlers.message_handler import ( | |
| handle_confirm_callback, | |
| handle_done_callback, | |
| handle_message, | |
| handle_undone_callback, | |
| ) | |
| from bot.handlers.photo_handler import build_photo_handlers | |
| from bot.handlers.profile_handler import build_profile_handlers | |
| from bot.handlers.schedule_handler import build_schedule_handlers | |
| from bot.handlers.today_handler import build_today_handlers | |
| # ββ Logging βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| logging.basicConfig( | |
| level=getattr(logging, LOG_LEVEL, logging.INFO), | |
| format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", | |
| datefmt="%Y-%m-%d %H:%M:%S", | |
| handlers=[ | |
| logging.StreamHandler(sys.stdout), | |
| ], | |
| ) | |
| # Silence httpx and httpcore logs to avoid exposing Telegram bot token in request URLs | |
| logging.getLogger("httpx").setLevel(logging.WARNING) | |
| logging.getLogger("httpcore").setLevel(logging.WARNING) | |
| logger = logging.getLogger(__name__) | |
| # ββ Bot Commands Menu βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| BOT_COMMANDS = [ | |
| BotCommand("start", "Mulai & setup profil"), | |
| BotCommand("agenda", "Lihat kegiatan & jadwal kamu"), | |
| BotCommand("tambahkegiatan", "Tambah kegiatan baru"), | |
| BotCommand("hapuskegiatan", "Hapus kegiatan yang tersimpan"), | |
| BotCommand("profile", "Lihat profil & target nutrisimu"), | |
| BotCommand("setprofile", "Perbarui profil"), | |
| BotCommand("today", "Progress nutrisi hari ini"), | |
| BotCommand("log", "Catat makanan yang dimakan"), | |
| BotCommand("food", "Alias untuk /log"), | |
| BotCommand("schedule", "Atur jadwal makan & reminder"), | |
| BotCommand("reminders", "Lihat jadwal makanmu"), | |
| BotCommand("help", "Bantuan"), | |
| ] | |
| # ββ Post-init βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def post_init(application: Application) -> None: | |
| """Runs after application initializes, before polling starts.""" | |
| try: | |
| logger.info("Initializing database...") | |
| await init_db() | |
| await run_migrations() | |
| logger.info("Database initialized.") | |
| except Exception as e: | |
| logger.critical(f"FATAL: Database initialization failed: {e}", exc_info=True) | |
| raise | |
| try: | |
| # Init AI service | |
| ai_service = AIService() | |
| application.bot_data["ai_service"] = ai_service | |
| logger.info( | |
| "AI service initialized (model=%s, temp=%.2f, max_tokens=%d).", | |
| GEMINI_MODEL, ai_service._generation_config.temperature, | |
| ai_service._generation_config.max_output_tokens, | |
| ) | |
| except Exception as e: | |
| logger.critical(f"FATAL: AI service initialization failed: {e}", exc_info=True) | |
| raise | |
| try: | |
| # Set bot command menu | |
| await application.bot.set_my_commands(BOT_COMMANDS) | |
| logger.info("Bot commands menu set.") | |
| except Exception as e: | |
| logger.warning(f"Could not set bot commands: {e}") | |
| # Start scheduler | |
| start_scheduler(bot=application.bot, ai_service=ai_service) | |
| logger.info("Scheduler started.") | |
| async def post_shutdown(application: Application) -> None: | |
| """Cleanup on shutdown.""" | |
| stop_scheduler() | |
| await close_db() | |
| logger.info("Shutdown complete.") | |
| async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None: | |
| """Log errors caused by Updates.""" | |
| if isinstance(context.error, (TimedOut, NetworkError)): | |
| logger.warning(f"Telegram network issue (timeout/network error): {context.error}") | |
| else: | |
| logger.error("Exception while handling an update:", exc_info=context.error) | |
| # ββ Main ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def is_allowed(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool: | |
| """Gate every update behind the optional ALLOWED_USER_IDS allow-list. | |
| When the list is empty (the default) everybody is allowed, which keeps local | |
| development friction-free. Set ALLOWED_USER_IDS to a comma-separated list of | |
| Telegram user ids to restrict the bot to just those people. | |
| """ | |
| if not ALLOWED_USER_IDS: | |
| return True | |
| user = update.effective_user | |
| if user is None: | |
| return False | |
| if user.id in ALLOWED_USER_IDS: | |
| return True | |
| logger.warning( | |
| "Blocked update from unauthorised user %s (@%s)", | |
| user.id, user.username or "?", | |
| ) | |
| try: | |
| await update.effective_message.reply_text( | |
| "maaf, bot ini belum terbuka untuk publik ya" | |
| ) | |
| except Exception: | |
| pass | |
| return False | |
| # Backwards-compatible helper for callers that want to gate a single handler. | |
| async def guarded(handler): | |
| async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE): | |
| if await is_allowed(update, context): | |
| await handler(update, context) | |
| return wrapper | |
| async def _allow_list_gate(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: | |
| """Reject updates from users outside ALLOWED_USER_IDS. | |
| Registered with `TypeHandler` at group -1 so it runs before every real | |
| handler; raising ApplicationHandlerStop stops all further processing. | |
| """ | |
| if not await is_allowed(update, context): | |
| raise ApplicationHandlerStop() | |
| def build_app() -> Application: | |
| """Build and configure the Telegram Application with all handlers.""" | |
| request = HTTPXRequest( | |
| connect_timeout=20.0, | |
| read_timeout=20.0, | |
| write_timeout=20.0, | |
| pool_timeout=20.0, | |
| ) | |
| app = ( | |
| Application.builder() | |
| .token(TELEGRAM_BOT_TOKEN) | |
| .request(request) | |
| .post_init(post_init) | |
| .post_shutdown(post_shutdown) | |
| .build() | |
| ) | |
| app.add_error_handler(error_handler) | |
| # Allow-list first, before any real handler (no-op when the list is empty). | |
| app.add_handler(TypeHandler(Update, _allow_list_gate), group=-1) | |
| # ββ Register handlers (order matters) βββββββββββββββββββββββββββββββββββββ | |
| app.add_handler(build_onboarding_conversation()) | |
| for handler in build_schedule_handlers(): | |
| app.add_handler(handler) | |
| for handler in build_profile_handlers(): | |
| app.add_handler(handler) | |
| for handler in build_food_handlers(): | |
| app.add_handler(handler) | |
| for handler in build_today_handlers(): | |
| app.add_handler(handler) | |
| for handler in build_activity_handlers(): | |
| app.add_handler(handler) | |
| for handler in build_help_handlers(): | |
| app.add_handler(handler) | |
| for handler in build_photo_handlers(): | |
| app.add_handler(handler) | |
| # Inline buttons: confirmations for data-changing actions, plus the | |
| # per-activity done/delete buttons. | |
| app.add_handler(CallbackQueryHandler(handle_confirm_callback, pattern=r"^confirm:")) | |
| app.add_handler(CallbackQueryHandler(handle_done_callback, pattern=r"^done:")) | |
| app.add_handler(CallbackQueryHandler(handle_undone_callback, pattern=r"^undone:")) | |
| # Free-form text is handled by the intent router. | |
| app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message)) | |
| return app | |
| def main() -> None: | |
| """CLI entry point (also used by the Docker image).""" | |
| logger.info("Starting Ginchiee Bot...") | |
| # The health endpoint makes the container pass platform liveness checks | |
| # (HF Spaces / Render / plain `docker run`). | |
| try: | |
| port = int(os.getenv("PORT", "7860")) | |
| except ValueError: | |
| port = 7860 | |
| start_health_server(port) | |
| app = build_app() | |
| logger.info("Bot is running! Press Ctrl+C to stop.") | |
| app.run_polling(drop_pending_updates=True) | |
| if __name__ == "__main__": | |
| main() | |