From e937cc85b9004aa29f81d6465e6cbfefe0524ecf Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Sat, 4 Apr 2026 13:12:13 +0000 Subject: [PATCH] feat: implement /recover command for listing and recovering soft-deleted bounties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add recover_bounty method to BountyService (admin-only) - Add cmd_recover handler for listing and recovering deleted bounties - Register /recover command in bot.py - Add /recover to bot command list /recover - list all recoverable bounties (sorted by deleted_at desc) /recover - recover specific bounty(ies) Output formats: List: [#1] Deleted bounty | 🗑️ Deleted 2 Apr 2026 Recover: ✅ Recovered bounty #1. or ⛔ Bounty #5 not found or not deleted. Fixes #49 --- apps/telegram-bot/bot.py | 3 ++ apps/telegram-bot/commands.py | 62 +++++++++++++++++++++++++++++++++++ core/services.py | 27 +++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/apps/telegram-bot/bot.py b/apps/telegram-bot/bot.py index a8a1f38..3037453 100644 --- a/apps/telegram-bot/bot.py +++ b/apps/telegram-bot/bot.py @@ -14,6 +14,7 @@ from commands import ( cmd_edit, cmd_help, cmd_my, + cmd_recover, cmd_show, cmd_start, cmd_timezone, @@ -47,6 +48,7 @@ def build_app() -> Application: app.add_handler(CommandHandler("show", cmd_show)) app.add_handler(CommandHandler("timezone", cmd_timezone)) app.add_handler(CommandHandler("admin", cmd_admin)) + app.add_handler(CommandHandler("recover", cmd_recover)) app.add_handler(MessageHandler(filters.COMMAND, cmd_help)) @@ -65,6 +67,7 @@ async def post_init(app: Application) -> None: ("show", "Show bounty details"), ("timezone", "Get/set room timezone"), ("admin", "Manage admins"), + ("recover", "Recover deleted bounties"), ("help", "Show help"), ] ) diff --git a/apps/telegram-bot/commands.py b/apps/telegram-bot/commands.py index 609a921..d554788 100644 --- a/apps/telegram-bot/commands.py +++ b/apps/telegram-bot/commands.py @@ -346,6 +346,68 @@ async def cmd_delete(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: await update.message.reply_text("Bounty not found.") +async def cmd_recover(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) + + try: + if not BOUNTY_SERVICE.is_admin(room_id, user_id): + await update.message.reply_text("⛔ Only admins can recover bounties.") + return + except PermissionError as e: + await update.message.reply_text(f"⛔ {e}") + return + + if not args: + deleted_bounties = BOUNTY_SERVICE.list_deleted_bounties(room_id) + if not deleted_bounties: + await update.message.reply_text("No recoverable bounties.") + return + + deleted_bounties.sort(key=lambda b: b.deleted_at or 0, reverse=True) + + lines = ["Recoverable bounties:"] + for b in deleted_bounties: + deleted_str = time.strftime("%d %b %Y", time.localtime(b.deleted_at)) + text_part = ( + b.text[:40] + "..." + if b.text and len(b.text) > 40 + else (b.text or "(no text)") + ) + lines.append(f"[#{b.id}] {text_part} | 🗑️ Deleted {deleted_str}") + + await update.message.reply_text("\n".join(lines), disable_web_page_preview=True) + return + + bounty_ids = [] + for arg in args: + try: + bounty_ids.append(int(arg)) + except ValueError: + await update.message.reply_text(f"Invalid bounty ID: {arg}") + return + + results = [] + for bounty_id in bounty_ids: + try: + success, message = BOUNTY_SERVICE.recover_bounty( + room_id=room_id, + bounty_id=bounty_id, + user_id=user_id, + ) + except PermissionError as e: + results.append(f"⛔ {e}") + continue + + if success: + results.append(f"✅ {message}") + else: + results.append(f"⛔ {message}") + + await update.message.reply_text("\n".join(results)) + + async def cmd_track(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: if not is_group(update): await update.message.reply_text("⛔ /track is only available in groups.") diff --git a/core/services.py b/core/services.py index 2f727b4..cf45abd 100644 --- a/core/services.py +++ b/core/services.py @@ -210,6 +210,33 @@ class BountyService: self._storage.update_bounty(room_id, bounty) return True + def recover_bounty( + self, room_id: int, bounty_id: int, user_id: int + ) -> tuple[bool, str]: + """Recover a soft-deleted bounty. Only admins can recover. + + Returns (success, message) tuple. + """ + if not self.is_admin(room_id, user_id): + raise PermissionError("Only admins can recover bounties.") + + all_bounties = self._storage.list_all_bounties(room_id, include_deleted=True) + bounty = None + for b in all_bounties: + if b.id == bounty_id: + bounty = b + break + + if not bounty: + return False, f"Bounty #{bounty_id} not found." + + if bounty.deleted_at is None: + return False, f"Bounty #{bounty_id} is not deleted." + + bounty.deleted_at = None + self._storage.update_bounty(room_id, bounty) + return True, f"Recovered bounty #{bounty_id}." + class TrackingService: """Service for tracking bounty operations."""