From 7202eeb1d2f35fe34dedb608ce7d66b613155815 Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Fri, 3 Apr 2026 12:37:55 +0000 Subject: [PATCH] feat(cli): implement CLI for jigaido bounty tracker Create cli/main.py with the following commands: - add [--link url] [--due date] - Add a new bounty - list - List all bounties in room - my - List tracked bounties for user - update [text] [--link url] [--due date] [--clear-link] [--clear-due] - delete - Delete a bounty - track - Track a bounty - untrack - Untrack a bounty Context flags: - --group-id - Group context - --user-id - User context Requires either --group-id or --user-id for all commands. Fixes #11 --- cli/__init__.py | 0 cli/main.py | 251 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 cli/__init__.py create mode 100644 cli/main.py diff --git a/cli/__init__.py b/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/main.py b/cli/main.py new file mode 100644 index 0000000..06faaba --- /dev/null +++ b/cli/main.py @@ -0,0 +1,251 @@ +"""JIGAIDO CLI - Command line interface for bounty tracking.""" + +import argparse +import sys +from pathlib import Path + +import dateparser + +from adapters.storage.json_file import JsonFileRoomStorage, JsonFileTrackingStorage +from config import Config +from core.services import BountyService, TrackingService + + +def parse_due_date(due_str: str | None) -> int | None: + """Parse due date string to Unix timestamp.""" + if not due_str: + return None + parsed = dateparser.parse(due_str) + if parsed: + return int(parsed.timestamp()) + return None + + +def create_services(): + """Create service instances with storage.""" + config = Config() + config.ensure_data_dir() + room_storage = JsonFileRoomStorage(config.data_dir / "room") + tracking_storage = JsonFileTrackingStorage(config.data_dir / "tracking") + bounty_service = BountyService(room_storage) + tracking_service = TrackingService(tracking_storage, room_storage) + return bounty_service, tracking_service + + +def cmd_add(args): + """Add a new bounty.""" + bounty_service, _ = create_services() + due_ts = parse_due_date(args.due) + bounty = bounty_service.add_bounty( + room_id=args.group_id or args.user_id, + user_id=args.user_id or 0, + text=args.text, + link=args.link, + due_date_ts=due_ts, + ) + print(f"Added bounty #{bounty.id}") + + +def cmd_list(args): + """List all bounties in a room.""" + bounty_service, _ = create_services() + bounties = bounty_service.list_bounties(room_id=args.group_id or args.user_id) + if not bounties: + print("No bounties") + return + for b in bounties: + due_str = ( + f", due: {dateparser.parse(str(b.due_date_ts)).strftime('%Y-%m-%d')}" + if b.due_date_ts + else "" + ) + link_str = f", link: {b.link}" if b.link else "" + print(f"#{b.id}: {b.text or '(no text)'}{link_str}{due_str}") + + +def cmd_my(args): + """List tracked bounties for a user.""" + _, tracking_service = create_services() + room_id = args.group_id or args.user_id + tracked = tracking_service.get_tracked_bounties( + room_id=room_id, user_id=args.user_id + ) + if not tracked: + print("Not tracking any bounties") + return + for b in tracked: + due_str = ( + f", due: {dateparser.parse(str(b.due_date_ts)).strftime('%Y-%m-%d')}" + if b.due_date_ts + else "" + ) + link_str = f", link: {b.link}" if b.link else "" + print(f"#{b.id}: {b.text or '(no text)'}{link_str}{due_str}") + + +def cmd_update(args): + """Update a bounty.""" + bounty_service, _ = create_services() + due_ts = parse_due_date(args.due) + try: + success = bounty_service.update_bounty( + room_id=args.group_id or args.user_id, + bounty_id=args.bounty_id, + user_id=args.user_id or 0, + text=args.text, + link=args.link, + due_date_ts=due_ts, + clear_link=args.clear_link, + clear_due=args.clear_due, + ) + if success: + print(f"Updated bounty #{args.bounty_id}") + else: + print(f"Bounty #{args.bounty_id} not found", file=sys.stderr) + sys.exit(1) + except PermissionError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_delete(args): + """Delete a bounty.""" + bounty_service, _ = create_services() + try: + success = bounty_service.delete_bounty( + room_id=args.group_id or args.user_id, + bounty_id=args.bounty_id, + user_id=args.user_id or 0, + ) + if success: + print(f"Deleted bounty #{args.bounty_id}") + else: + print(f"Bounty #{args.bounty_id} not found", file=sys.stderr) + sys.exit(1) + except PermissionError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_track(args): + """Track a bounty.""" + _, tracking_service = create_services() + try: + success = tracking_service.track_bounty( + room_id=args.group_id, + user_id=args.user_id, + bounty_id=args.bounty_id, + ) + if success: + print(f"Tracking bounty #{args.bounty_id}") + else: + print(f"Already tracking bounty #{args.bounty_id}") + except ValueError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def cmd_untrack(args): + """Untrack a bounty.""" + _, tracking_service = create_services() + success = tracking_service.untrack_bounty( + room_id=args.group_id, + user_id=args.user_id, + bounty_id=args.bounty_id, + ) + if success: + print(f"Untracked bounty #{args.bounty_id}") + else: + print(f"Not tracking bounty #{args.bounty_id}", file=sys.stderr) + sys.exit(1) + + +def main(): + parser = argparse.ArgumentParser( + prog="jigaido-cli", description="JIGAIDO bounty tracker CLI" + ) + subparsers = parser.add_subparsers(dest="command", help="Commands") + + parser_add = subparsers.add_parser("add", help="Add a new bounty") + parser_add.add_argument("text", help="Bounty description") + parser_add.add_argument("--link", help="Optional link") + parser_add.add_argument( + "--due", help="Optional due date (e.g., 'tomorrow', 'in 3 days')" + ) + + parser_list = subparsers.add_parser("list", help="List all bounties") + + parser_my = subparsers.add_parser("my", help="List tracked bounties") + + parser_update = subparsers.add_parser("update", help="Update a bounty") + parser_update.add_argument("bounty_id", type=int, help="Bounty ID to update") + parser_update.add_argument("text", nargs="?", help="New description") + parser_update.add_argument("--link", help="New link") + parser_update.add_argument("--due", help="New due date") + parser_update.add_argument("--clear-link", action="store_true", help="Clear link") + parser_update.add_argument( + "--clear-due", action="store_true", help="Clear due date" + ) + + parser_delete = subparsers.add_parser("delete", help="Delete a bounty") + parser_delete.add_argument("bounty_id", type=int, help="Bounty ID to delete") + + parser_track = subparsers.add_parser("track", help="Track a bounty") + parser_track.add_argument("bounty_id", type=int, help="Bounty ID to track") + + parser_untrack = subparsers.add_parser("untrack", help="Untrack a bounty") + parser_untrack.add_argument("bounty_id", type=int, help="Bounty ID to untrack") + + for sp in [ + parser_add, + parser_list, + parser_my, + parser_update, + parser_delete, + parser_track, + parser_untrack, + ]: + sp.add_argument( + "--group-id", type=int, help="Group context (use group room ID)" + ) + sp.add_argument( + "--user-id", type=int, help="User context (use Telegram user ID)" + ) + + args = parser.parse_args() + + if not args.command: + parser.print_help() + sys.exit(1) + + if not args.group_id and not args.user_id: + print("Error: either --group-id or --user-id is required", file=sys.stderr) + sys.exit(1) + + if args.command in ("add", "list", "my", "update", "delete"): + if not (args.group_id or args.user_id): + print("Error: --group-id or --user-id required", file=sys.stderr) + sys.exit(1) + if args.command == "add" and not args.text: + print("Error: text is required for add", file=sys.stderr) + sys.exit(1) + + command_map = { + "add": cmd_add, + "list": cmd_list, + "my": cmd_my, + "update": cmd_update, + "delete": cmd_delete, + "track": cmd_track, + "untrack": cmd_untrack, + } + + if args.command in command_map: + command_map[args.command](args) + else: + print(f"Unknown command: {args.command}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main()