#!/usr/bin/env python3 """ Unified Telegram Bot - Habit, Diet, URL Summarizer Features: - YouTube/Blog/News summarization (EN/KO) - Habit logging - Diet/food logging with photo analysis - Morning briefing - Night debrief + motivation """ import os import sys import json import re import datetime from typing import Optional, Dict, List from dataclasses import dataclass, field from enum import Enum # Try to import telegram, handle if not available try: from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, ContextTypes TELEGRAM_AVAILABLE = True except ImportError: TELEGRAM_AVAILABLE = False # Configuration TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', '8325588419:AAGghb0nosWG8g6QtYeghqUs0RHug06uG74') OBSIDIAN_PATH = os.environ.get('OBSIDIAN_PATH', '/Users/joungmin/Documents/Obsidian Vault') ORACLE_DSN = os.environ.get('ORACLE_DSN', 'h8i4i0g8cxtd2lpf_high') ORACLE_USER = os.environ.get('ORACLE_USER', 'admin') ORACLE_PASSWORD = os.environ.get('ORACLE_PASSWORD', 'Carter55@26@1') ORACLE_WALLET = os.environ.get('ORACLE_WALLET', '/Users/joungmin/devkit/db_conn/Wallet_H8I4I0G8CXTD2LPF') # In-memory storage (replace with Oracle later) DATA_DIR = '/tmp/habit_bot' os.makedirs(DATA_DIR, exist_ok=True) def load_json(f): if os.path.exists(f): with open(f, 'r') as file: return json.load(file) return {} def save_json(f, data): with open(f, 'w') as file: json.dump(data, file, indent=2, default=str) HABITS_FILE = os.path.join(DATA_DIR, 'habits.json') HABIT_LOGS_FILE = os.path.join(DATA_DIR, 'habit_logs.json') FOOD_LOGS_FILE = os.path.join(DATA_DIR, 'food_logs.json') USER_DATA_FILE = os.path.join(DATA_DIR, 'users.json') # Motivational quotes MOTIVATIONAL_QUOTES = [ "The only bad workout is the one that didn't happen. ๐Ÿ’ช", "Every expert was once a beginner. Keep going! ๐ŸŒŸ", "Success is the sum of small efforts repeated day in and day out. ๐Ÿ“ˆ", "You don't have to be great to start, but you have to start to be great. ๐Ÿš€", "The body achieves what the mind believes. ๐Ÿง ", "Discipline is doing what needs to be done, even if you don't want to do it. ๐Ÿ”ฅ", "Your future is created by what you do today, not tomorrow. โฐ", "Small steps add up to big changes. Keep walking! ๐Ÿ‘ฃ", ] class UserData: def __init__(self): self.habits = load_json(HABITS_FILE) self.habit_logs = load_json(HABIT_LOGS_FILE) self.food_logs = load_json(FOOD_LOGS_FILE) self.users = load_json(USER_DATA_FILE) def save(self): save_json(HABITS_FILE, self.habits) save_json(HABIT_LOGS_FILE, self.habit_logs) save_json(FOOD_LOGS_FILE, self.food_logs) save_json(USER_DATA_FILE, self.users) def get_daily_totals(self, user_id: str, date: str = None) -> Dict: """Get daily nutrition totals for a user""" if date is None: date = datetime.datetime.now().strftime('%Y-%m-%d') totals = {'calories': 0, 'carbs': 0, 'protein': 0, 'fat': 0} if user_id in self.food_logs and date in self.food_logs[user_id]: for log in self.food_logs[user_id][date]: totals['calories'] += log.get('calories', 0) totals['carbs'] += log.get('carbs', 0) totals['protein'] += log.get('protein', 0) totals['fat'] += log.get('fat', 0) return totals data = UserData() # URL Patterns URL_PATTERNS = { 'youtube': r'(?:youtube\.com|youtu\.be)', 'blog': r'blog\.|medium\.com|substack\.com', 'news': r'news\.|cnn\.com|bbc\.com|nytimes\.com|reuters\.com', } @dataclass class Habit: name: str description: str = '' frequency: str = 'daily' streak: int = 0 is_active: bool = True @dataclass class HabitLog: habit_name: str date: str status: str # completed, skipped notes: str = '' timestamp: str = '' @dataclass class FoodLog: date: str meal_type: str food_name: str photo_url: str = '' calories: int = 0 carbs: float = 0 protein: float = 0 fat: float = 0 analysis: str = '' # ============== Telegram Handlers ============== async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """Welcome message""" welcome = """ ๐Ÿ”ฎ **Welcome to Your Life Assistant Bot!** I can help you with: ๐Ÿ“š **Content Summarization** - Send me a YouTube/Blog/News URL - I'll summarize in English & Korean โœ… **Habit Tracking** - `/habit add ` - Add new habit - `/habit log [notes]` - Log completion - `/habit list` - Show all habits - `/habit streak ` - Show streak ๐Ÿฝ๏ธ **Diet Logging** - Send a photo of your meal - Or text: "had chicken breast 200g" - I'll analyze nutrition ๐Ÿ“Š **Daily Status** - `/morning` - Morning briefing - `/debrief` - Night summary + motivation - `/status` - Today's progress What would you like to do? """ await update.message.reply_text(welcome, parse_mode='Markdown') async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """Help message""" help_text = """ ๐Ÿ”ฎ **Available Commands** **Habit Management** - `/habit add ` - Add new habit - `/habit log [notes]` - Log completion - `/habit list` - Show all habits - `/habit streak ` - Show streak - `/habit delete ` - Remove habit **Food/Diet Logging** - Send meal photo - AI nutrition analysis - Text: "breakfast eggs 2" - Quick log - `/food today` - Today's meals - `/food stats` - Nutrition summary **Daily Briefings** - `/morning` - Morning briefing - `/debrief` - Night summary + motivation - `/status` - Current progress **Content** - Send URL - Summarize (EN/KO) """ await update.message.reply_text(help_text, parse_mode='Markdown') # ============== Habit Commands ============== async def habit_add(update: Update, context: ContextTypes.DEFAULT_TYPE): """Add new habit""" if not context.args: await update.message.reply_text("Usage: `/habit add `") return habit_name = ' '.join(context.args).strip().lower() user_id = str(update.message.from_user.id) if user_id not in data.habits: data.habits[user_id] = {} if habit_name in data.habits[user_id]: await update.message.reply_text(f"โœ… Habit '{habit_name}' already exists!") return data.habits[user_id][habit_name] = { 'name': habit_name, 'streak': 0, 'created_at': datetime.datetime.now().isoformat(), 'is_active': True } data.save() await update.message.reply_text(f"โœ… Added habit: *{habit_name}*", parse_mode='Markdown') async def habit_list(update: Update, context: ContextTypes.DEFAULT_TYPE): """List all habits""" user_id = str(update.message.from_user.id) if user_id not in data.habits or not data.habits[user_id]: await update.message.reply_text("No habits yet. Add one with `/habit add `") return today = datetime.datetime.now().strftime('%Y-%m-%d') completed_today = set() if user_id in data.habit_logs and today in data.habit_logs[user_id]: for log in data.habit_logs[user_id][today]: if log.get('status') == 'completed': completed_today.add(log.get('habit_name', '')) text = "๐Ÿ“‹ **Your Habits:**\n\n" for name, info in data.habits[user_id].items(): if info.get('is_active', True): streak = info.get('streak', 0) status = "โœ…" if name in completed_today else "โฌœ" text += f"{status} *{name}* (streak: {streak}๐Ÿ”ฅ)\n" await update.message.reply_text(text, parse_mode='Markdown') async def habit_log(update: Update, context: ContextTypes.DEFAULT_TYPE): """Log habit completion""" if not context.args: await update.message.reply_text("Usage: `/habit log [notes]`") return user_id = str(update.message.from_user.id) today = datetime.datetime.now().strftime('%Y-%m-%d') # Parse args args_text = ' '.join(context.args) if ' ' in args_text: habit_name, notes = args_text.split(' ', 1) else: habit_name = args_text notes = '' habit_name = habit_name.strip().lower() # Verify habit exists if user_id not in data.habits or habit_name not in data.habits[user_id]: await update.message.reply_text(f"โŒ Habit '{habit_name}' not found!") return # Log it if user_id not in data.habit_logs: data.habit_logs[user_id] = {} if today not in data.habit_logs[user_id]: data.habit_logs[user_id][today] = [] data.habit_logs[user_id][today].append({ 'habit_name': habit_name, 'status': 'completed', 'notes': notes, 'timestamp': datetime.datetime.now().isoformat() }) # Update streak prev_streak = data.habits[user_id][habit_name].get('streak', 0) data.habits[user_id][habit_name]['streak'] = prev_streak + 1 data.save() # Motivational response quote = MOTIVATIONAL_QUOTES[datetime.datetime.now().second % len(MOTIVATIONAL_QUOTES)] await update.message.reply_text( f"โœ… *{habit_name}* completed! Streak: {prev_streak + 1}๐Ÿ”ฅ\n\n{quote}", parse_mode='Markdown' ) async def habit_streak(update: Update, context: ContextTypes.DEFAULT_TYPE): """Show habit streak""" if not context.args: await update.message.reply_text("Usage: `/habit streak `") return user_id = str(update.message.from_user.id) habit_name = ' '.join(context.args).strip().lower() if user_id not in data.habits or habit_name not in data.habits[user_id]: await update.message.reply_text(f"โŒ Habit '{habit_name}' not found!") return streak = data.habits[user_id][habit_name].get('streak', 0) await update.message.reply_text( f"๐Ÿ”ฅ *{habit_name}* streak: {streak} days", parse_mode='Markdown' ) # ============== Food/Diet Commands ============== async def food_log(update: Update, context: ContextTypes.DEFAULT_TYPE): """Log food/meal""" user_id = str(update.message.from_user.id) today = datetime.datetime.now().strftime('%Y-%m-%d') now = datetime.datetime.now().strftime('%H:%M') # Determine meal type hour = datetime.datetime.now().hour if 5 <= hour < 11: meal_type = 'breakfast' elif 11 <= hour < 14: meal_type = 'lunch' elif 14 <= hour < 17: meal_type = 'snack' else: meal_type = 'dinner' text = ' '.join(context.args) if context.args else '' # Simple food analysis (placeholder - would use MiniMax/vision API) food_info = analyze_food_text(text) if user_id not in data.food_logs: data.food_logs[user_id] = {} if today not in data.food_logs[user_id]: data.food_logs[user_id][today] = [] data.food_logs[user_id][today].append({ 'meal_type': meal_type, 'food_name': text or 'Photo', 'time': now, 'calories': food_info['calories'], 'carbs': food_info['carbs'], 'protein': food_info['protein'], 'fat': food_info['fat'], 'timestamp': datetime.datetime.now().isoformat() }) data.save() # Keto guidance remaining = 2000 - food_info['calories'] # Simplified await update.message.reply_text( f"๐Ÿฝ๏ธ Logged: *{text or 'Photo'}*\n" f"๐Ÿ“Š {food_info['calories']}kcal | " f" carbs: {food_info['carbs']}g | " f"protein: {food_info['protein']}g | " f"fat: {food_info['fat']}g\n" f"\n๐Ÿ’ช Keep going! {remaining}kcal remaining today.", parse_mode='Markdown' ) def analyze_food_text(text: str) -> Dict: """Simple food analysis (placeholder)""" # This would use MiniMax/vision API in production # For now, return placeholder data # Simple keyword matching calories = 0 carbs = 0 protein = 0 fat = 0 food_database = { 'chicken': {'cal': 165, 'carb': 0, 'pro': 31, 'fat': 3.6}, 'egg': {'cal': 78, 'carb': 0.6, 'pro': 6, 'fat': 5}, 'rice': {'cal': 130, 'carb': 28, 'pro': 2.7, 'fat': 0.3}, 'beef': {'cal': 250, 'carb': 0, 'pro': 26, 'fat': 15}, 'salad': {'cal': 50, 'carb': 5, 'pro': 2, 'fat': 3}, 'bread': {'cal': 265, 'carb': 49, 'pro': 9, 'fat': 3.2}, 'apple': {'cal': 95, 'carb': 25, 'pro': 0.5, 'fat': 0.3}, 'banana': {'cal': 105, 'carb': 27, 'pro': 1.3, 'fat': 0.4}, } text_lower = text.lower() for food, info in food_database.items(): if food in text_lower: # Check for quantity numbers = re.findall(r'\d+', text) qty = int(numbers[0]) if numbers else 1 calories += info['cal'] * qty carbs += info['carb'] * qty protein += info['pro'] * qty fat += info['fat'] * qty # Default if no match if calories == 0: calories, carbs, protein, fat = 300, 20, 15, 12 return {'calories': calories, 'carbs': carbs, 'protein': protein, 'fat': fat} # ============== MiniMax Vision API ============== MINIMAX_API_URL = "https://api.minimax.chat/v1/text/chatcompletion_v2" MINIMAX_API_KEY = os.environ.get('MINIMAX_API_KEY', '') async def analyze_food_photo(file_path: str) -> Dict: """ Analyze food photo using MiniMax Vision API Returns: Dict with calories, carbs, protein, fat estimation """ if not MINIMAX_API_KEY: # Fallback to placeholder if no API key return { 'calories': 400, 'carbs': 25, 'protein': 30, 'fat': 20, 'detected_foods': ['food (placeholder - add MiniMax API key)'], 'confidence': 0.5 } try: import base64 # Read and encode image with open(file_path, 'rb') as f: image_b64 = base64.b64encode(f.read()).decode('utf-8') # Prepare vision prompt prompt = """Analyze this food image and estimate nutrition: 1. What foods are in the image? 2. Estimate: calories, carbs (g), protein (g), fat (g) 3. Keto-friendly? (yes/no) Return JSON format: { "foods": ["item1", "item2"], "calories": number, "carbs": number, "protein": number, "fat": number, "keto_friendly": boolean, "confidence": 0.0-1.0 }""" # Call MiniMax API headers = { "Authorization": f"Bearer {MINIMAX_API_KEY}", "Content-Type": "application/json" } payload = { "model": "MiniMax-Vision-01", "messages": [ { "role": "user", "content": [ {"type": "text", "text": prompt}, {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_b64}"}} ] } ], "max_tokens": 500, "temperature": 0.3 } import httpx async with httpx.AsyncClient() as client: response = await client.post( MINIMAX_API_URL, headers=headers, json=payload, timeout=30.0 ) if response.status_code == 200: result = response.json() # Parse JSON from response content = result.get('choices', [{}])[0].get('message', {}).get('content', '{}') # Extract JSON import json as json_module try: # Try to parse the response as JSON nutrition = json_module.loads(content) return { 'calories': nutrition.get('calories', 400), 'carbs': nutrition.get('carbs', 25), 'protein': nutrition.get('protein', 30), 'fat': nutrition.get('fat', 20), 'detected_foods': nutrition.get('foods', ['unknown']), 'confidence': nutrition.get('confidence', 0.8), 'keto_friendly': nutrition.get('keto_friendly', True) } except json_module.JSONDecodeError: # Fallback if JSON parsing fails return { 'calories': 400, 'carbs': 25, 'protein': 30, 'fat': 20, 'detected_foods': ['analyzed via MiniMax'], 'confidence': 0.7 } else: print(f"MiniMax API error: {response.status_code}") return { 'calories': 400, 'carbs': 25, 'protein': 30, 'fat': 20, 'detected_foods': ['analysis failed - using defaults'], 'confidence': 0.5 } except Exception as e: print(f"Photo analysis error: {e}") return { 'calories': 400, 'carbs': 25, 'protein': 30, 'fat': 20, 'detected_foods': ['error - using defaults'], 'confidence': 0.5 } async def food_photo(update: Update, context: ContextTypes.DEFAULT_TYPE): """Handle food photo upload and analysis""" user_id = str(update.message.from_user.id) today = datetime.datetime.now().strftime('%Y-%m-%d') now = datetime.datetime.now().strftime('%H:%M') # Determine meal type hour = datetime.datetime.now().hour if 5 <= hour < 11: meal_type = 'breakfast' elif 11 <= hour < 14: meal_type = 'lunch' elif 14 <= hour < 17: meal_type = 'snack' else: meal_type = 'dinner' # Get photo photo = update.message.photo[-1] if update.message.photo else None if not photo: await update.message.reply_text("โŒ No photo found! Please send a food photo.") return await update.message.reply_text("๐Ÿ“ธ Analyzing food photo...") try: # Download photo file = await context.bot.get_file(photo.file_id) file_path = f"/tmp/food_{user_id}_{today}.jpg" await file.download_to_drive(file_path) # Analyze with MiniMax Vision API nutrition = await analyze_food_photo(file_path) # Log the food if user_id not in data.food_logs: data.food_logs[user_id] = {} if today not in data.food_logs[user_id]: data.food_logs[user_id][today] = [] data.food_logs[user_id][today].append({ 'meal_type': meal_type, 'food_name': ', '.join(nutrition.get('detected_foods', ['food'])), 'time': now, 'calories': nutrition['calories'], 'carbs': nutrition['carbs'], 'protein': nutrition['protein'], 'fat': nutrition['fat'], 'source': 'photo', 'confidence': nutrition.get('confidence', 0.8), 'timestamp': datetime.datetime.now().isoformat() }) data.save() # Build response emoji = "โœ…" if nutrition.get('keto_friendly', True) else "โš ๏ธ" confidence_pct = int(nutrition.get('confidence', 0.8) * 100) text = f"๐Ÿฝ๏ธ **Food Analyzed**\n\n" text += f"Detected: {', '.join(nutrition.get('detected_foods', ['food']))}\n" text += f"Confidence: {confidence_pct}%\n\n" text += f"๐Ÿ“Š **Nutrition:**\n" text += f"๐Ÿ”ฅ Calories: {nutrition['calories']}kcal\n" text += f"๐Ÿฅฆ Carbs: {nutrition['carbs']}g\n" text += f"๐Ÿ’ช Protein: {nutrition['protein']}g\n" text += f"๐Ÿฅ‘ Fat: {nutrition['fat']}g\n\n" text += f"{emoji} Keto-friendly: {'Yes' if nutrition.get('keto_friendly', True) else 'No'}\n" # Keto check if nutrition['carbs'] > 25: text += "\nโš ๏ธ Carbs exceed keto limit (25g)!" # Daily total total = data.get_daily_totals(user_id, today) text += f"\n๐Ÿ“ˆ **Today's Total:** {total['calories']}kcal" text += f"\n๐Ÿ’ช {2000 - total['calories']}kcal remaining" await update.message.reply_text(text, parse_mode='Markdown') # Clean up import os if os.path.exists(file_path): os.remove(file_path) except Exception as e: await update.message.reply_text(f"โŒ Error analyzing photo: {str(e)}") async def food_today(update: Update, context: ContextTypes.DEFAULT_TYPE): """Show today's food log""" user_id = str(update.message.from_user.id) today = datetime.datetime.now().strftime('%Y-%m-%d') if user_id not in data.food_logs or today not in data.food_logs[user_id]: await update.message.reply_text("No food logged today yet!") return logs = data.food_logs[user_id][today] total_cal = sum(l.get('calories', 0) for l in logs) total_carb = sum(l.get('carbs', 0) for l in logs) total_pro = sum(l.get('protein', 0) for l in logs) total_fat = sum(l.get('fat', 0) for l in logs) text = f"๐Ÿฝ๏ธ **Today's Meals:**\n\n" for log in logs: text += f"- {log.get('meal_type', '')}: {log.get('food_name', '')} ({log.get('calories', 0)}kcal)\n" text += f"\n๐Ÿ“Š **Total:** {total_cal}kcal | {total_carb}g carbs | {total_pro}g protein | {total_fat}g fat" remaining = 2000 - total_cal if remaining > 0: text += f"\n๐Ÿ’ช {remaining}kcal remaining for today!" else: text += f"\nโš ๏ธ Over by {abs(remaining)}kcal" await update.message.reply_text(text, parse_mode='Markdown') # ============== Daily Briefings ============== async def morning_briefing(update: Update, context: ContextTypes.DEFAULT_TYPE): """Morning briefing with habits to do""" user_id = str(update.message.from_user.id) today = datetime.datetime.now().strftime('%Y-%m-%d') if user_id not in data.habits or not data.habits[user_id]: await update.message.reply_text("โ˜€๏ธ Good morning! No habits set yet. Add some with `/habit add `") return # Check yesterday's uncompleted habits yesterday = (datetime.datetime.now() - datetime.timedelta(days=1)).strftime('%Y-%m-%d') uncompleted = [] if user_id in data.habit_logs and yesterday in data.habit_logs[user_id]: completed = set(l.get('habit_name', '') for l in data.habit_logs[user_id][yesterday] if l.get('status') == 'completed') for name in data.habits[user_id]: if name not in completed and data.habits[user_id][name].get('is_active', True): uncompleted.append(name) text = "โ˜€๏ธ **Good Morning!** Here's your plan:\n\n" # Today's habits text += "*Today's Habits:*\n" for name, info in data.habits[user_id].items(): if info.get('is_active', True): streak = info.get('streak', 0) text += f"โฌœ {name} (๐Ÿ”ฅ {streak})\n" if uncompleted: text += f"\n*Yesterday's unfinished:*\n" for name in uncompleted: text += f"โš ๏ธ {name}\n" text += "\n๐Ÿ’ช Let's make today count!" await update.message.reply_text(text, parse_mode='Markdown') async def debrief(update: Update, context: ContextTypes.DEFAULT_TYPE): """Night debrief with progress and motivation""" user_id = str(update.message.from_user.id) today = datetime.datetime.now().strftime('%Y-%m-%d') # Count today's achievements habits_completed = 0 if user_id in data.habit_logs and today in data.habit_logs[user_id]: habits_completed = len([l for l in data.habit_logs[user_id][today] if l.get('status') == 'completed']) total_habits = len([h for h in data.habits.get(user_id, {}).values() if h.get('is_active', True)]) # Food stats total_cal = 0 if user_id in data.food_logs and today in data.food_logs[user_id]: total_cal = sum(l.get('calories', 0) for l in data.food_logs[user_id][today]) quote = MOTIVATIONAL_QUOTES[datetime.datetime.now().second % len(MOTIVATIONAL_QUOTES)] text = f"๐ŸŒ™ **Night Debrief**\n\n" text += f"๐Ÿ“‹ *Habits:* {habits_completed}/{total_habits} completed\n" text += f"๐Ÿฝ๏ธ *Calories:* {total_cal} consumed\n" if habits_completed >= total_habits: text += f"\n๐ŸŽ‰ Amazing day! You crushed all your habits!" elif habits_completed > 0: text += f"\n๐Ÿ‘ Good effort! {total_habits - habits_completed} habits left for tomorrow." else: text += f"\n๐Ÿ’ช Tomorrow is a new chance. You've got this!" text += f"\n\n{quote}" await update.message.reply_text(text, parse_mode='Markdown') # ============== URL Summarization ============== async def handle_url(update: Update, context: ContextTypes.DEFAULT_TYPE): """Handle URL messages - summarize content""" url = update.message.text.strip() # Check if it's a URL if not url.startswith(('http://', 'https://')): return # Determine type url_type = 'general' for t, pattern in URL_PATTERNS.items(): if re.search(pattern, url, re.IGNORECASE): url_type = t break await update.message.reply_text(f"๐Ÿ”„ Processing {url_type} URL...") # TODO: Use MiniMax API to summarize # For now, return placeholder summary_en = f""" **English Summary ({url_type})** Title: [Would extract from page] Key Points: 1. [Main point 1] 2. [Main point 2] 3. [Main point 3] Tags: #summary """.strip() summary_ko = f""" **ํ•œ๊ตญ์–ด ์š”์•ฝ ({url_type})** ์ œ๋ชฉ: [Would extract from page] ์ฃผ์š” ํฌ์ธํŠธ: 1. [๋ฉ”์ธ ํฌ์ธํŠธ 1] 2. [๋ฉ”์ธ ํฌ์ธํŠธ 2] 3. [๋ฉ”์ธ ํฌ์ธํŠธ 3] ํƒœ๊ทธ: #์š”์•ฝ """.strip() # Save to Obsidian save_to_obsidian(url, summary_en, summary_ko, url_type) # Send response text = f"**๐Ÿ“š Summary saved to Obsidian**\n\n{summary_en}\n\n---\n\n{summary_ko}" await update.message.reply_text(text, parse_mode='Markdown') def save_to_obsidian(url: str, summary_en: str, summary_ko: str, url_type: str): """Save summary to Obsidian""" date = datetime.datetime.now().strftime('%Y-%m-%d') filename = f"URL Summary - {date}.md" filepath = os.path.join(OBSIDIAN_PATH, 'URL Summaries', filename) os.makedirs(os.path.dirname(filepath), exist_ok=True) content = f"""# URL Summary - {date} **Source:** {url} **Type:** {url_type} **Date:** {date} --- ## English Summary {summary_en} --- ## ํ•œ๊ตญ์–ด ์š”์•ฝ {summary_ko} --- *Generated by OpenClaw* """ with open(filepath, 'w', encoding='utf-8') as f: f.write(content) # ============== Main ============== def main(): """Run the bot""" if not TELEGRAM_AVAILABLE: print("โš ๏ธ Telegram library not installed. Run: pip install python-telegram-bot") print("Bot code is ready but cannot run without the library.") return app = Application.builder().token(TELEGRAM_BOT_TOKEN).build() # Commands app.add_handler(CommandHandler('start', start_command)) app.add_handler(CommandHandler('help', help_command)) app.add_handler(CommandHandler('habit', habit_list)) # Default handler app.add_handler(CommandHandler('habit_add', habit_add)) app.add_handler(CommandHandler('habit_list', habit_list)) app.add_handler(CommandHandler('habit_log', habit_log)) app.add_handler(CommandHandler('habit_streak', habit_streak)) app.add_handler(CommandHandler('food', food_log)) app.add_handler(CommandHandler('food_today', food_today)) app.add_handler(CommandHandler('food_photo', food_photo)) app.add_handler(CommandHandler('morning', morning_briefing)) app.add_handler(CommandHandler('debrief', debrief)) app.add_handler(CommandHandler('status', lambda u, c: food_today(u, c))) # Alias # Photo handler (for food photos) from telegram.ext import.filters app.add_handler(MessageHandler(filters.PHOTO, food_photo)) # URL handler app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_url)) print("๐Ÿ”ฎ Starting Habit & Diet Bot...") app.run_polling() if __name__ == '__main__': main()