Compare commits

..

1 Commits

Author SHA1 Message Date
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
3 changed files with 69 additions and 68 deletions

View File

@@ -13,6 +13,7 @@ from commands import (
cmd_edit,
cmd_help,
cmd_my,
cmd_show,
cmd_start,
cmd_track,
cmd_untrack,
@@ -41,6 +42,7 @@ def build_app() -> Application:
app.add_handler(CommandHandler("delete", cmd_delete))
app.add_handler(CommandHandler("track", cmd_track))
app.add_handler(CommandHandler("untrack", cmd_untrack))
app.add_handler(CommandHandler("show", cmd_show))
app.add_handler(MessageHandler(filters.COMMAND, cmd_help))
@@ -56,6 +58,7 @@ async def post_init(app: Application) -> None:
("edit", "Edit a bounty"),
("track", "Track a bounty"),
("untrack", "Stop tracking"),
("show", "Show bounty details"),
("help", "Show help"),
]
)

View File

@@ -32,52 +32,22 @@ def parse_args(args: list[str]) -> tuple[Optional[str], Optional[str], Optional[
due_date_ts = None
remaining = []
i = 0
while i < len(args):
arg = args[i]
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())
if i + 1 < len(args) and _is_time_format(args[i + 1]):
time_str = args[i + 1]
hour, minute = map(int, time_str.split(":"))
due_date_ts = _set_time_on_timestamp(due_date_ts, hour, minute)
i += 1
else:
remaining.append(arg)
else:
remaining.append(arg)
i += 1
text = " ".join(remaining) if remaining else None
return text, link, due_date_ts
def _is_time_format(s: str) -> bool:
"""Check if string matches HH:MM format."""
if not s or len(s) != 5:
return False
if s[2] != ":":
return False
try:
h, m = map(int, s.split(":"))
return 0 <= h <= 23 and 0 <= m <= 59
except ValueError:
return False
def _set_time_on_timestamp(ts: int, hour: int, minute: int) -> int:
"""Set time (hour:minute) on a Unix timestamp, keeping the date."""
import datetime
dt = datetime.datetime.fromtimestamp(ts)
dt = dt.replace(hour=hour, minute=minute, second=0, microsecond=0)
return int(dt.timestamp())
def format_bounty(b, show_id: bool = True) -> str:
parts = []
if show_id:
@@ -162,8 +132,8 @@ 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 <text> [link] [date] [time]\n"
"Example: /add Fix bug https://github.com/foo/bar april 15 14:30"
"Usage: /add <text> [link] [due_date]\n"
"Example: /add Fix the bug https://github.com/foo/bar tomorrow"
)
return
@@ -175,7 +145,6 @@ async def cmd_add(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
user_id = get_user_id(update)
room_id = get_room_id(update)
try:
bounty = BOUNTY_SERVICE.add_bounty(
room_id=room_id,
user_id=user_id,
@@ -183,12 +152,6 @@ async def cmd_add(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
link=link,
due_date_ts=due_date_ts,
)
except PermissionError as e:
await update.message.reply_text(f"{e}")
return
except ValueError as e:
await update.message.reply_text(f"{e}")
return
due_str = ""
if due_date_ts:
@@ -348,16 +311,59 @@ async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
)
async def cmd_show(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
args = extract_args(update.message.text)
if not args:
await update.message.reply_text("Usage: /show <bounty_id>")
return
try:
bounty_id = int(args[0])
except ValueError:
await update.message.reply_text("Invalid bounty ID.")
return
room_id = get_room_id(update)
bounty = BOUNTY_SERVICE.get_bounty(room_id, bounty_id)
if not bounty:
await update.message.reply_text("Bounty not found.")
return
timezone = BOUNTY_SERVICE.get_timezone(room_id)
lines = []
title = bounty.text or "(no text)"
lines.append(f"[#{bounty.id}] {title}")
if bounty.link:
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})")
username = bounty.created_by_username or f"user#{bounty.created_by_user_id}"
lines.append(f"👤 @{username}")
created_str = time.strftime("%Y-%m-%d %H:%M", time.localtime(bounty.created_at))
lines.append(f"📌 Created: {created_str}")
await update.message.reply_text("\n".join(lines), disable_web_page_preview=True)
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 <text> [link] [date] [time] — add bounty (admin only)\n"
"/update <id> [text] [link] [due] — update bounty (admin only)\n"
"/delete <id> — delete bounty (admin only)\n"
"/add <text> [link] [due] — add bounty\n"
"/update <id> [text> [link] [due] — update bounty\n"
"/delete <id> — delete bounty\n"
"/track <id> — track a bounty (groups only)\n"
"/untrack <id> — stop tracking (groups only)\n"
"/show <id> — show bounty details\n"
"/start — re-initialize\n"
"/help — this message",
disable_web_page_preview=True,

View File

@@ -96,24 +96,21 @@ class BountyService:
def check_link_unique(
self, room_id: int, link: str | None, exclude_bounty_id: int | None = None
) -> int | None:
"""Check if a link is unique within a room (not used by another bounty).
Returns the conflicting bounty ID if found, or None if unique/allowed.
"""
) -> bool:
"""Check if a link is unique within a room (not used by another bounty)."""
if not link:
return None
return True
room_data = self._storage.load(room_id)
if room_data is None:
return None
return True
for bounty in room_data.bounties:
if bounty.deleted_at is not None:
continue
if bounty.link == link and bounty.id != exclude_bounty_id:
return bounty.id
return None
return False
return True
def add_bounty(
self,
@@ -127,11 +124,8 @@ class BountyService:
if not self.is_admin(room_id, user_id):
raise PermissionError("Only admins can add bounties.")
conflicting_id = self.check_link_unique(room_id, link)
if conflicting_id is not None:
raise ValueError(
f"A bounty with this link already exists: #{conflicting_id}"
)
if not self.check_link_unique(room_id, link):
raise ValueError("A bounty with this link already exists in this room.")
room_data = self._storage.load(room_id)
if room_data is None:
@@ -184,10 +178,8 @@ class BountyService:
if not self.is_admin(room_id, user_id):
raise PermissionError("Only admins can edit bounties.")
if (
link
and self.check_link_unique(room_id, link, exclude_bounty_id=bounty_id)
is not None
if link and not self.check_link_unique(
room_id, link, exclude_bounty_id=bounty_id
):
raise ValueError("A bounty with this link already exists in this room.")