refactor(commands): use core services instead of storage module

Refactor commands.py to be thin Telegram wrappers around core services.

Changes:
- Replace 'import storage' with imports from core.services and adapters.storage
- Create module-level service instances (BountyService, TrackingService)
- Update format_bounty() to work with Bounty dataclass instead of dict
- Add get_room_id() helper for unified group/DM handling
- Each command handler is now a thin wrapper that:
  1. Extracts Telegram types (update, user_id, room_id)
  2. Calls appropriate core service
  3. Formats and sends response

Kept from original:
- parse_args()
- format_bounty()
- extract_args()

Commands now use services:
- cmd_bounty: BOUNTY_SERVICE.list_bounties()
- cmd_my: BOUNTY_SERVICE.list_bounties() or TRACKING_SERVICE.get_tracked_bounties()
- cmd_add: BOUNTY_SERVICE.add_bounty()
- cmd_update: BOUNTY_SERVICE.update_bounty()
- cmd_delete: BOUNTY_SERVICE.delete_bounty()
- cmd_track: TRACKING_SERVICE.track_bounty() (groups only)
- cmd_untrack: TRACKING_SERVICE.untrack_bounty() (groups only)

Fixes #13
This commit is contained in:
shokollm
2026-04-03 12:39:23 +00:00
parent edbc924b98
commit 5b1634ebca

View File

@@ -1,8 +1,5 @@
"""Telegram command handlers for JIGAIDO.""" """Telegram command handlers for JIGAIDO - Thin wrappers around core services."""
import json
import os
import re
import time import time
from functools import wraps from functools import wraps
from typing import Optional from typing import Optional
@@ -11,7 +8,13 @@ import dateparser
from telegram import Update from telegram import Update
from telegram.ext import ContextTypes from telegram.ext import ContextTypes
import storage from adapters.storage.json_file import JsonFileRoomStorage, JsonFileTrackingStorage
from core.services import BountyService, TrackingService
ROOM_STORAGE = JsonFileRoomStorage()
TRACKING_STORAGE = JsonFileTrackingStorage()
BOUNTY_SERVICE = BountyService(ROOM_STORAGE)
TRACKING_SERVICE = TrackingService(TRACKING_STORAGE, ROOM_STORAGE)
TELEGRAM_BOT_USERNAME = "your_bot_username" TELEGRAM_BOT_USERNAME = "your_bot_username"
@@ -45,25 +48,25 @@ def parse_args(args: list[str]) -> tuple[Optional[str], Optional[str], Optional[
return text, link, due_date_ts return text, link, due_date_ts
def format_bounty(b: dict, show_id: bool = True) -> 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}]")
if b.get("text"): if b.text:
parts.append(b["text"]) parts.append(b.text)
if b.get("link"): if b.link:
parts.append(f"🔗 {b['link']}") parts.append(f"🔗 {b.link}")
if b.get("due_date_ts"): if b.due_date_ts:
due_str = time.strftime("%Y-%m-%d", time.localtime(b["due_date_ts"])) due_str = time.strftime("%Y-%m-%d", time.localtime(b.due_date_ts))
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)")
elif days_left == 0: elif days_left == 0:
parts.append(f"{due_str} (TODAY)") parts.append(f"{due_str} (TODAY)")
else: else:
parts.append(f"{due_str} ({days_left}d)") parts.append(f"{due_str} ({days_left}d)")
if b.get("created_by_user_id"): if b.created_by_user_id:
parts.append(f"by {b['created_by_user_id']}") parts.append(f"by {b.created_by_user_id}")
return " | ".join(parts) return " | ".join(parts)
@@ -79,19 +82,26 @@ def get_user_id(update: Update) -> int:
return update.effective_user.id return update.effective_user.id
async def cmd_bounty(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: def get_room_id(update: Update) -> int:
"""Get room_id for the current context.
For groups: negative group_id
For DMs: positive user_id
"""
if is_group(update): if is_group(update):
data = storage.load_group_bounties(get_group_id(update)) return get_group_id(update)
bounties = data.get("bounties", []) return get_user_id(update)
else:
data = storage.load_user_personal(get_user_id(update))
bounties = data.get("bounties", []) async def cmd_bounty(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
room_id = get_room_id(update)
bounties = BOUNTY_SERVICE.list_bounties(room_id)
if not bounties: if not bounties:
await update.message.reply_text("No bounties yet.") await update.message.reply_text("No bounties yet.")
return return
lines = [format_bounty(dict(b), show_id=True) 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)
@@ -100,38 +110,18 @@ async def cmd_my(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
if is_group(update): if is_group(update):
group_id = get_group_id(update) group_id = get_group_id(update)
tracking = storage.load_user_tracking(group_id, user_id) bounties = TRACKING_SERVICE.get_tracked_bounties(group_id, user_id)
tracked = tracking.get("tracked", [])
else: else:
data = storage.load_user_personal(user_id) room_id = get_room_id(update)
bounties = data.get("bounties", []) bounties = BOUNTY_SERVICE.list_bounties(room_id)
lines = [format_bounty(dict(b), show_id=True) for b in bounties]
await update.message.reply_text( if not bounties:
"\n".join(lines) if lines else "No personal bounties.", msg = "You are not tracking any bounties." if is_group(update) else "No personal bounties."
disable_web_page_preview=True, await update.message.reply_text(msg)
)
return return
if not tracked: lines = [format_bounty(b, show_id=True) for b in bounties]
await update.message.reply_text("You are not tracking any bounties.") await update.message.reply_text("\n".join(lines), disable_web_page_preview=True)
return
group_data = storage.load_group_bounties(group_id)
bounty_map = {b["id"]: b for b in group_data.get("bounties", [])}
bounty_lines = []
for t in tracked:
bounty = bounty_map.get(t["bounty_id"])
if bounty:
bounty_lines.append(format_bounty(bounty, show_id=True))
if not bounty_lines:
await update.message.reply_text("You are not tracking any bounties.")
return
await update.message.reply_text(
"\n".join(bounty_lines), disable_web_page_preview=True
)
async def cmd_add(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: async def cmd_add(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
@@ -149,19 +139,22 @@ async def cmd_add(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
return return
user_id = get_user_id(update) user_id = get_user_id(update)
room_id = get_room_id(update)
if is_group(update): bounty = BOUNTY_SERVICE.add_bounty(
group_id = get_group_id(update) room_id=room_id,
bounty = storage.add_group_bounty(group_id, user_id, text, link, due_date_ts) user_id=user_id,
else: text=text,
bounty = storage.add_personal_bounty(user_id, text, link, due_date_ts) link=link,
due_date_ts=due_date_ts,
)
due_str = "" due_str = ""
if due_date_ts: if due_date_ts:
due_str = f" | Due: {time.strftime('%Y-%m-%d', time.localtime(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,
) )
@@ -186,25 +179,25 @@ async def cmd_update(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
return return
user_id = get_user_id(update) user_id = get_user_id(update)
room_id = get_room_id(update)
if is_group(update): try:
group_id = get_group_id(update) success = BOUNTY_SERVICE.update_bounty(
bounty = storage.get_group_bounty(group_id, bounty_id) room_id=room_id,
if not bounty: bounty_id=bounty_id,
await update.message.reply_text("Bounty not found.") user_id=user_id,
return text=text,
if bounty["created_by_user_id"] != user_id: link=link,
await update.message.reply_text("⛔ Only the creator can edit this bounty.") due_date_ts=due_date_ts,
return )
storage.update_group_bounty(group_id, bounty_id, text, link, due_date_ts) except PermissionError as e:
await update.message.reply_text(f"{e}")
return
if success:
await update.message.reply_text(f"✅ Bounty #{bounty_id} updated.")
else: else:
bounty = storage.get_personal_bounty(user_id, bounty_id) await update.message.reply_text("Bounty not found.")
if not bounty:
await update.message.reply_text("Bounty not found.")
return
storage.update_personal_bounty(user_id, bounty_id, text, link, due_date_ts)
await update.message.reply_text(f"✅ Bounty #{bounty_id} updated.")
async def cmd_delete(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: async def cmd_delete(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
@@ -220,30 +213,29 @@ async def cmd_delete(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
return return
user_id = get_user_id(update) user_id = get_user_id(update)
room_id = get_room_id(update)
if is_group(update): try:
group_id = get_group_id(update) success = BOUNTY_SERVICE.delete_bounty(
bounty = storage.get_group_bounty(group_id, bounty_id) room_id=room_id,
if not bounty: bounty_id=bounty_id,
await update.message.reply_text("Bounty not found.") user_id=user_id,
return )
if bounty["created_by_user_id"] != user_id: except PermissionError as e:
await update.message.reply_text( await update.message.reply_text(f"{e}")
"⛔ Only the creator can delete this bounty." return
)
return if success:
storage.delete_group_bounty(group_id, bounty_id) await update.message.reply_text(f"✅ Bounty #{bounty_id} deleted.")
else: else:
bounty = storage.get_personal_bounty(user_id, bounty_id) await update.message.reply_text("Bounty not found.")
if not bounty:
await update.message.reply_text("Bounty not found.")
return
storage.delete_personal_bounty(user_id, bounty_id)
await update.message.reply_text(f"✅ Bounty #{bounty_id} deleted.")
async def cmd_track(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: 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.")
return
args = extract_args(update.message.text) args = extract_args(update.message.text)
if not args: if not args:
await update.message.reply_text("Usage: /track <bounty_id>") await update.message.reply_text("Usage: /track <bounty_id>")
@@ -256,25 +248,22 @@ async def cmd_track(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
return return
user_id = get_user_id(update) user_id = get_user_id(update)
room_id = get_room_id(update)
if is_group(update): try:
group_id = get_group_id(update) if TRACKING_SERVICE.track_bounty(room_id, user_id, bounty_id):
bounty = storage.get_group_bounty(group_id, bounty_id)
if not bounty:
await update.message.reply_text("Bounty not found.")
return
if storage.track_bounty(group_id, user_id, bounty_id):
await update.message.reply_text(f"✅ Now tracking bounty #{bounty_id}.")
else:
await update.message.reply_text(f"Already tracking bounty #{bounty_id}.")
else:
if storage.track_bounty(user_id, user_id, bounty_id):
await update.message.reply_text(f"✅ Now tracking bounty #{bounty_id}.") await update.message.reply_text(f"✅ Now tracking bounty #{bounty_id}.")
else: else:
await update.message.reply_text(f"Already tracking bounty #{bounty_id}.") await update.message.reply_text(f"Already tracking bounty #{bounty_id}.")
except ValueError as e:
await update.message.reply_text(str(e))
async def cmd_untrack(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: async def cmd_untrack(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
if not is_group(update):
await update.message.reply_text("⛔ /untrack is only available in groups.")
return
args = extract_args(update.message.text) args = extract_args(update.message.text)
if not args: if not args:
await update.message.reply_text("Usage: /untrack <bounty_id>") await update.message.reply_text("Usage: /untrack <bounty_id>")
@@ -287,18 +276,12 @@ async def cmd_untrack(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
return return
user_id = get_user_id(update) user_id = get_user_id(update)
room_id = get_room_id(update)
if is_group(update): if TRACKING_SERVICE.untrack_bounty(room_id, user_id, bounty_id):
group_id = get_group_id(update) await update.message.reply_text(f"✅ Untracked bounty #{bounty_id}.")
if storage.untrack_bounty(group_id, user_id, bounty_id):
await update.message.reply_text(f"✅ Untracked bounty #{bounty_id}.")
else:
await update.message.reply_text(f"Not tracking bounty #{bounty_id}.")
else: else:
if storage.untrack_bounty(user_id, user_id, bounty_id): await update.message.reply_text("Not tracking bounty #{bounty_id}.")
await update.message.reply_text(f"✅ Untracked bounty #{bounty_id}.")
else:
await update.message.reply_text(f"Not tracking bounty #{bounty_id}.")
async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
@@ -327,10 +310,10 @@ async def cmd_help(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
"/bounty — list all bounties\n" "/bounty — list all bounties\n"
"/my — bounties you're tracking\n" "/my — bounties you're tracking\n"
"/add <text> [link] [due] — add bounty\n" "/add <text> [link] [due] — add bounty\n"
"/update <id> [text] [link] [due] — update bounty\n" "/update <id> [text> [link] [due] — update bounty\n"
"/delete <id> — delete bounty\n" "/delete <id> — delete bounty\n"
"/track <id> — track a bounty\n" "/track <id> — track a bounty (groups only)\n"
"/untrack <id> — stop tracking\n" "/untrack <id> — stop tracking (groups only)\n"
"/start — re-initialize\n" "/start — re-initialize\n"
"/help — this message", "/help — this message",
disable_web_page_preview=True, disable_web_page_preview=True,