"""Telegram command handlers for JIGAIDO.""" import json import os import re import time from functools import wraps from typing import Optional import dateparser from telegram import Update from telegram.ext import ContextTypes import storage TELEGRAM_BOT_USERNAME = "your_bot_username" def extract_args(text: str) -> list[str]: if not text: return [] tokens = text.strip().split() return tokens[1:] if len(tokens) > 1 else [] def parse_args(args: list[str]) -> tuple[Optional[str], Optional[str], Optional[int]]: text = None link = None due_date_ts = None remaining = [] for arg in args: if not link and (arg.startswith("http://") or arg.startswith("https://")): link = arg elif due_date_ts is None: parsed = dateparser.parse(arg) if parsed: due_date_ts = int(parsed.timestamp()) else: remaining.append(arg) else: remaining.append(arg) text = " ".join(remaining) if remaining else None return text, link, due_date_ts def format_bounty(b: dict, show_id: bool = True) -> str: parts = [] if show_id: parts.append(f"[#{b['id']}]") if b.get("text"): parts.append(b["text"]) if b.get("link"): parts.append(f"🔗 {b['link']}") if b.get("due_date_ts"): due_str = time.strftime("%Y-%m-%d", time.localtime(b["due_date_ts"])) days_left = (b["due_date_ts"] - int(time.time())) // 86400 if days_left < 0: parts.append(f"⏰ {due_str} (OVERDUE)") elif days_left == 0: parts.append(f"⏰ {due_str} (TODAY)") else: parts.append(f"⏰ {due_str} ({days_left}d)") if b.get("created_by_user_id"): parts.append(f"by {b['created_by_user_id']}") return " | ".join(parts) def is_group(update: Update) -> bool: return update.effective_chat.type != "private" def get_group_id(update: Update) -> int: return update.effective_chat.id def get_user_id(update: Update) -> int: return update.effective_user.id async def cmd_bounty(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: if is_group(update): data = storage.load_group_bounties(get_group_id(update)) bounties = data.get("bounties", []) else: data = storage.load_user_personal(get_user_id(update)) bounties = data.get("bounties", []) if not bounties: await update.message.reply_text("No bounties yet.") return lines = [format_bounty(dict(b), show_id=True) for b in bounties] await update.message.reply_text("\n".join(lines), disable_web_page_preview=True) async def cmd_my(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: user_id = get_user_id(update) if is_group(update): group_id = get_group_id(update) tracking = storage.load_user_tracking(group_id, user_id) tracked = tracking.get("tracked", []) else: data = storage.load_user_personal(user_id) bounties = data.get("bounties", []) lines = [format_bounty(dict(b), show_id=True) for b in bounties] await update.message.reply_text( "\n".join(lines) if lines else "No personal bounties.", disable_web_page_preview=True, ) return if not tracked: await update.message.reply_text("You are not tracking any bounties.") return group_data = storage.load_group_bounties(group_id) bounty_map = {b["id"]: b for b in group_data.get("bounties", [])} bounty_lines = [] for t in tracked: bounty = bounty_map.get(t["bounty_id"]) if bounty: bounty_lines.append(format_bounty(bounty, show_id=True)) if not bounty_lines: await update.message.reply_text("You are not tracking any bounties.") return await update.message.reply_text( "\n".join(bounty_lines), disable_web_page_preview=True ) async def cmd_add(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: args = extract_args(update.message.text) if not args: await update.message.reply_text( "Usage: /add [link] [due_date]\n" "Example: /add Fix the bug https://github.com/foo/bar tomorrow" ) return text, link, due_date_ts = parse_args(args) if not text and not link: await update.message.reply_text("A bounty needs at least text or a link.") return user_id = get_user_id(update) if is_group(update): group_id = get_group_id(update) bounty = storage.add_group_bounty(group_id, user_id, text, link, due_date_ts) else: bounty = storage.add_personal_bounty(user_id, text, link, due_date_ts) due_str = "" if due_date_ts: due_str = f" | Due: {time.strftime('%Y-%m-%d', time.localtime(due_date_ts))}" await update.message.reply_text( f"✅ Bounty added (#{bounty['id']}){due_str}", disable_web_page_preview=True, ) async def cmd_update(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: args = extract_args(update.message.text) if len(args) < 1: await update.message.reply_text( "Usage: /update [text] [link] [due_date]" ) return try: bounty_id = int(args[0]) except ValueError: await update.message.reply_text("Invalid bounty ID.") return text, link, due_date_ts = parse_args(args[1:]) if not text and not link and due_date_ts is None: await update.message.reply_text("Nothing to update.") return user_id = get_user_id(update) if is_group(update): group_id = get_group_id(update) bounty = storage.get_group_bounty(group_id, bounty_id) if not bounty: await update.message.reply_text("Bounty not found.") return if bounty["created_by_user_id"] != user_id: await update.message.reply_text("⛔ Only the creator can edit this bounty.") return storage.update_group_bounty(group_id, bounty_id, text, link, due_date_ts) else: bounty = storage.get_personal_bounty(user_id, bounty_id) if not bounty: await update.message.reply_text("Bounty not found.") return storage.update_personal_bounty(user_id, bounty_id, text, link, due_date_ts) await update.message.reply_text(f"✅ Bounty #{bounty_id} updated.") async def cmd_delete(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: args = extract_args(update.message.text) if not args: await update.message.reply_text("Usage: /delete ") return try: bounty_id = int(args[0]) except ValueError: await update.message.reply_text("Invalid bounty ID.") return user_id = get_user_id(update) if is_group(update): group_id = get_group_id(update) bounty = storage.get_group_bounty(group_id, bounty_id) if not bounty: await update.message.reply_text("Bounty not found.") return if bounty["created_by_user_id"] != user_id: await update.message.reply_text( "⛔ Only the creator can delete this bounty." ) return storage.delete_group_bounty(group_id, bounty_id) else: bounty = storage.get_personal_bounty(user_id, bounty_id) if not bounty: await update.message.reply_text("Bounty not found.") return storage.delete_personal_bounty(user_id, bounty_id) await update.message.reply_text(f"✅ Bounty #{bounty_id} deleted.") async def cmd_track(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: args = extract_args(update.message.text) if not args: await update.message.reply_text("Usage: /track ") return try: bounty_id = int(args[0]) except ValueError: await update.message.reply_text("Invalid bounty ID.") return user_id = get_user_id(update) if is_group(update): group_id = get_group_id(update) bounty = storage.get_group_bounty(group_id, bounty_id) if not bounty: await update.message.reply_text("Bounty not found.") return if storage.track_bounty(group_id, user_id, bounty_id): await update.message.reply_text(f"✅ Now tracking bounty #{bounty_id}.") else: await update.message.reply_text(f"Already tracking bounty #{bounty_id}.") else: if storage.track_bounty(user_id, user_id, bounty_id): await update.message.reply_text(f"✅ Now tracking bounty #{bounty_id}.") else: await update.message.reply_text(f"Already tracking bounty #{bounty_id}.") async def cmd_untrack(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: args = extract_args(update.message.text) if not args: await update.message.reply_text("Usage: /untrack ") return try: bounty_id = int(args[0]) except ValueError: await update.message.reply_text("Invalid bounty ID.") return user_id = get_user_id(update) if is_group(update): group_id = get_group_id(update) if storage.untrack_bounty(group_id, user_id, bounty_id): await update.message.reply_text(f"✅ Untracked bounty #{bounty_id}.") else: await update.message.reply_text(f"Not tracking bounty #{bounty_id}.") else: if storage.untrack_bounty(user_id, user_id, bounty_id): await update.message.reply_text(f"✅ Untracked bounty #{bounty_id}.") else: await update.message.reply_text(f"Not tracking bounty #{bounty_id}.") async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: if is_group(update): await update.message.reply_text( "👻 JIGAIDO is watching.\n\n" "This group's bounty list is now active.\n" "/bounty — list bounties\n" "/add — create a bounty\n" "/track — track a bounty\n" "/my — your tracked bounties" ) else: await update.message.reply_text( "👻 JIGAIDO activated.\n\n" "Personal bounty list ready.\n" "/bounty — list your bounties\n" "/add — create a bounty\n" "/my — your tracked bounties" ) async def cmd_help(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: await update.message.reply_text( "👻 JIGAIDO Commands:\n\n" "/bounty — list all bounties\n" "/my — bounties you're tracking\n" "/add [link] [due] — add bounty\n" "/update [text] [link] [due] — update bounty\n" "/delete — delete bounty\n" "/track — track a bounty\n" "/untrack — stop tracking\n" "/start — re-initialize\n" "/help — this message", disable_web_page_preview=True, )