Compare commits

..

3 Commits

Author SHA1 Message Date
bdb0f3cd8b Merge pull request 'feat: implement /show command to display full bounty details' (#59) from fix/issue-44 into main 2026-04-04 10:09:28 +02:00
shokollm
649b1ffbd3 revert: remove timezone command and revert date format to simple YYYY-MM-DD
This reverts:
- cmd_timezone function (issue #67)
- format_due_date with human-readable dates (issue #68)
- Reverts date display back to time.strftime("%Y-%m-%d")
- Keeps /edit command with -link/-date flags (issue #46)
2026-04-04 15:05:29 +07:00
shokollm
780cba6301 feat: implement /show command to display full bounty details
- Add cmd_show function to display bounty details including:
  - ID and full text (not sliced)
  - Link if exists
  - Due date formatted with room timezone
  - Created by username
  - Created at timestamp
- Register show command handler in bot.py
- Add show command to help text and bot command list
- Fixes #44
2026-04-04 05:43:50 +00:00
2 changed files with 14 additions and 79 deletions

View File

@@ -15,7 +15,6 @@ from commands import (
cmd_my, cmd_my,
cmd_show, cmd_show,
cmd_start, cmd_start,
cmd_timezone,
cmd_track, cmd_track,
cmd_untrack, cmd_untrack,
cmd_update, cmd_update,
@@ -43,7 +42,6 @@ def build_app() -> Application:
app.add_handler(CommandHandler("delete", cmd_delete)) app.add_handler(CommandHandler("delete", cmd_delete))
app.add_handler(CommandHandler("track", cmd_track)) app.add_handler(CommandHandler("track", cmd_track))
app.add_handler(CommandHandler("untrack", cmd_untrack)) app.add_handler(CommandHandler("untrack", cmd_untrack))
app.add_handler(CommandHandler("timezone", cmd_timezone))
app.add_handler(CommandHandler("show", cmd_show)) app.add_handler(CommandHandler("show", cmd_show))
app.add_handler(MessageHandler(filters.COMMAND, cmd_help)) app.add_handler(MessageHandler(filters.COMMAND, cmd_help))
@@ -60,7 +58,6 @@ async def post_init(app: Application) -> None:
("edit", "Edit a bounty"), ("edit", "Edit a bounty"),
("track", "Track a bounty"), ("track", "Track a bounty"),
("untrack", "Stop tracking"), ("untrack", "Stop tracking"),
("timezone", "Get/set room timezone"),
("show", "Show bounty details"), ("show", "Show bounty details"),
("help", "Show help"), ("help", "Show help"),
] ]

View File

@@ -1,10 +1,8 @@
"""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
@@ -21,33 +19,6 @@ 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:
@@ -114,7 +85,7 @@ 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, room_id: int | None = None) -> str: def format_bounty(b, show_id: bool = True) -> str:
parts = [] parts = []
if show_id: if show_id:
parts.append(f"[#{b.id}]") parts.append(f"[#{b.id}]")
@@ -123,11 +94,7 @@ def format_bounty(b, show_id: bool = True, room_id: int | None = None) -> 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:
timezone_str = "UTC" due_str = time.strftime("%Y-%m-%d", time.localtime(b.due_date_ts))
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)")
@@ -171,7 +138,7 @@ async def cmd_bounty(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
await update.message.reply_text("No bounties yet.") await update.message.reply_text("No bounties yet.")
return return
lines = [format_bounty(b, show_id=True, room_id=room_id) for b in bounties] lines = [format_bounty(b, show_id=True) 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)
@@ -195,7 +162,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, room_id=room_id) for b in bounties] lines = [format_bounty(b, show_id=True) 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)
@@ -226,9 +193,9 @@ async def cmd_add(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
due_str = "" due_str = ""
if due_date_ts: if due_date_ts:
timezone_str = BOUNTY_SERVICE.get_timezone(room_id) due_str = ""
due_str = f" | Due: {format_due_date(due_date_ts, timezone_str)}" if due_date_ts:
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,
@@ -427,19 +394,18 @@ async def cmd_show(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
title = bounty.text or "(no text)" title = bounty.text or "(no text)"
lines.append(f"[#{bounty.id}] {title}") lines.append(f"[#{bounty.id}] {title}")
due_parts = [] if 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 = time.strftime("%d %B %Y %H:%M", time.localtime(bounty.due_date_ts))
due_parts.append(f"Due: {due_str} ({timezone})") lines.append(f"📅 {due_str} ({timezone})")
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}"
if bounty.link: lines.append(f"👤 @{username}")
due_parts.append(f"{bounty.link} by @{username}")
else:
due_parts.append(f"by @{username}")
if due_parts: created_str = time.strftime("%Y-%m-%d %H:%M", time.localtime(bounty.created_at))
lines.extend(due_parts) lines.append(f"📌 Created: {created_str}")
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)
@@ -457,7 +423,6 @@ async def cmd_help(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
"/delete <id> — delete bounty (admin only)\n" "/delete <id> — delete bounty (admin only)\n"
"/track <id> — track a bounty (groups only)\n" "/track <id> — track a bounty (groups only)\n"
"/untrack <id> — stop tracking (groups only)\n" "/untrack <id> — stop tracking (groups only)\n"
"/timezone [tz] — get/set room timezone (admin only)\n"
"/show <id> — show bounty details\n" "/show <id> — show bounty details\n"
"/start — re-initialize\n" "/start — re-initialize\n"
"/help — this message", "/help — this message",
@@ -465,30 +430,3 @@ async def cmd_help(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
) )
async def cmd_timezone(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
args = extract_args(update.message.text)
room_id = get_room_id(update)
user_id = get_user_id(update)
if not args:
current_tz = BOUNTY_SERVICE.get_timezone(room_id)
await update.message.reply_text(f"Current timezone: {current_tz}")
return
timezone_str = args[0]
try:
ZoneInfo(timezone_str)
except (KeyError, ZoneInfoNotFoundError):
await update.message.reply_text(
"⛔ Invalid timezone. Use IANA format (e.g., Asia/Jakarta)"
)
return
try:
BOUNTY_SERVICE.set_timezone(room_id, timezone_str, user_id)
except PermissionError as e:
await update.message.reply_text(f"{e}")
return
await update.message.reply_text(f"✅ Timezone set to {timezone_str}.")