feat: human-readable date format with timezone awareness

- Add format_due_date() function that formats dates as '4 April 2026'
  or '4 April 2026 14:30 (Asia/Jakarta)' with timezone support
- Update format_bounty() to use timezone-aware date formatting
- Update cmd_bounty, cmd_my, cmd_add to pass room_id for timezone
- Dates now display in room's configured timezone
- Fixes #54
This commit is contained in:
shokollm
2026-04-04 08:15:15 +00:00
parent e3b813661d
commit 9cc9a6bf2f

View File

@@ -1,8 +1,10 @@
"""Telegram command handlers for JIGAIDO - Thin wrappers around core services.""" """Telegram command handlers for JIGAIDO - Thin wrappers around core services."""
import time import time
from datetime import datetime
from functools import wraps from functools import wraps
from typing import Optional from typing import Optional
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
import dateparser import dateparser
from telegram import Update from telegram import Update
@@ -19,6 +21,33 @@ TRACKING_SERVICE = TrackingService(TRACKING_STORAGE, ROOM_STORAGE)
TELEGRAM_BOT_USERNAME = "your_bot_username" 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]: def extract_args(text: str) -> list[str]:
if not text: if not text:
@@ -85,7 +114,9 @@ def parse_args(
return text, link, due_date_ts, clear_link, clear_date 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 = [] parts = []
if show_id: if show_id:
parts.append(f"[#{b.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: if b.link:
parts.append(f"🔗 {b.link}") parts.append(f"🔗 {b.link}")
if b.due_date_ts: 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 days_left = (b.due_date_ts - int(time.time())) // 86400
if days_left < 0: if days_left < 0:
parts.append(f"{due_str} (OVERDUE)") parts.append(f"{due_str} (OVERDUE)")
@@ -185,7 +220,9 @@ async def cmd_bounty(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
slice_length = 0 slice_length = 0
for b in displayed_bounties: 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) 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) await update.message.reply_text(msg)
return 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) 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 = "" due_str = ""
if due_date_ts: if due_date_ts:
due_str = "" timezone_str = BOUNTY_SERVICE.get_timezone(room_id)
if due_date_ts: due_str = f" | Due: {format_due_date(due_date_ts, timezone_str)}"
due_str = f" | Due: {time.strftime("%Y-%m-%d", time.localtime(due_date_ts))}"
await update.message.reply_text( await update.message.reply_text(
f"✅ Bounty added (#{bounty.id}){due_str}", f"✅ Bounty added (#{bounty.id}){due_str}",
disable_web_page_preview=True, 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}") lines.append(f"🔗 {bounty.link}")
if bounty.due_date_ts: if bounty.due_date_ts:
due_str = time.strftime("%d %B %Y %H:%M", time.localtime(bounty.due_date_ts)) due_str = format_due_date(bounty.due_date_ts, timezone)
lines.append(f"📅 {due_str} ({timezone})") lines.append(f"📅 {due_str}")
username = bounty.created_by_username or f"user#{bounty.created_by_user_id}" username = bounty.created_by_username or f"user#{bounty.created_by_user_id}"
lines.append(f"👤 @{username}") lines.append(f"👤 @{username}")
@@ -476,5 +512,3 @@ async def cmd_help(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
"/help — this message", "/help — this message",
disable_web_page_preview=True, disable_web_page_preview=True,
) )