diff --git a/apps/telegram-bot/commands.py b/apps/telegram-bot/commands.py index 243d9d1..a125b8f 100644 --- a/apps/telegram-bot/commands.py +++ b/apps/telegram-bot/commands.py @@ -1,8 +1,10 @@ """Telegram command handlers for JIGAIDO - Thin wrappers around core services.""" import time +from datetime import datetime from functools import wraps from typing import Optional +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError import dateparser from telegram import Update @@ -19,6 +21,33 @@ TRACKING_SERVICE = TrackingService(TRACKING_STORAGE, ROOM_STORAGE) TELEGRAM_BOT_USERNAME = "your_bot_username" +def format_due_date(due_date_ts: int | None, timezone_str: str) -> str: + """Format due date as human-readable with timezone. + + Examples: + No due date: (none shown) + Date only: 4 April 2026 + Date + time: 4 April 2026 14:30 + With timezone: 4 April 2026 14:30 (Asia/Jakarta) + """ + if not due_date_ts: + return "" + + try: + tz = ZoneInfo(timezone_str) + except (KeyError, ZoneInfoNotFoundError): + tz = ZoneInfo("UTC") + + dt = datetime.fromtimestamp(due_date_ts, tz=tz) + + date_str = dt.strftime("%-d %B %Y") + + if dt.hour != 0 or dt.minute != 0: + date_str += f" {dt.strftime('%H:%M')}" + date_str += f" ({timezone_str})" + + return date_str + def extract_args(text: str) -> list[str]: if not text: @@ -85,7 +114,9 @@ def parse_args( return text, link, due_date_ts, clear_link, clear_date -def format_bounty(b, show_id: bool = True, slice_length: int = 0) -> str: +def format_bounty( + b, show_id: bool = True, slice_length: int = 0, room_id: int | None = None +) -> str: parts = [] if show_id: parts.append(f"[#{b.id}]") @@ -97,7 +128,11 @@ def format_bounty(b, show_id: bool = True, slice_length: int = 0) -> str: if b.link: parts.append(f"🔗 {b.link}") if b.due_date_ts: - due_str = time.strftime("%d %b %Y", time.localtime(b.due_date_ts)) + timezone_str = "UTC" + if room_id is not None: + timezone_str = BOUNTY_SERVICE.get_timezone(room_id) + + due_str = format_due_date(b.due_date_ts, timezone_str) days_left = (b.due_date_ts - int(time.time())) // 86400 if days_left < 0: parts.append(f"⏰ {due_str} (OVERDUE)") @@ -185,7 +220,9 @@ async def cmd_bounty(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: slice_length = 0 for b in displayed_bounties: - lines.append(format_bounty(b, show_id=True, slice_length=slice_length)) + lines.append( + format_bounty(b, show_id=True, slice_length=slice_length, room_id=room_id) + ) await update.message.reply_text("\n".join(lines), disable_web_page_preview=True) @@ -210,7 +247,7 @@ async def cmd_my(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: await update.message.reply_text(msg) return - lines = [format_bounty(b, show_id=True) for b in bounties] + lines = [format_bounty(b, show_id=True, room_id=room_id) for b in bounties] await update.message.reply_text("\n".join(lines), disable_web_page_preview=True) @@ -241,9 +278,8 @@ async def cmd_add(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: due_str = "" if due_date_ts: - due_str = "" - if due_date_ts: - due_str = f" | Due: {time.strftime("%Y-%m-%d", time.localtime(due_date_ts))}" + timezone_str = BOUNTY_SERVICE.get_timezone(room_id) + due_str = f" | Due: {format_due_date(due_date_ts, timezone_str)}" await update.message.reply_text( f"✅ Bounty added (#{bounty.id}){due_str}", disable_web_page_preview=True, @@ -446,8 +482,8 @@ async def cmd_show(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: lines.append(f"🔗 {bounty.link}") if bounty.due_date_ts: - due_str = time.strftime("%d %B %Y %H:%M", time.localtime(bounty.due_date_ts)) - lines.append(f"📅 {due_str} ({timezone})") + due_str = format_due_date(bounty.due_date_ts, timezone) + lines.append(f"📅 {due_str}") username = bounty.created_by_username or f"user#{bounty.created_by_user_id}" lines.append(f"👤 @{username}") @@ -476,5 +512,3 @@ async def cmd_help(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: "/help — this message", disable_web_page_preview=True, ) - -