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
from functools import wraps
from typing import Optional
@@ -11,7 +8,13 @@ import dateparser
from telegram import Update
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"
@@ -45,25 +48,25 @@ def parse_args(args: list[str]) -> tuple[Optional[str], Optional[str], Optional[
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 = []
if show_id:
parts.append(f"[#{b['id']}]")
if b.get("text"):
parts.append(b["text"])
if b.get("link"):
parts.append(f"🔗 {b['link']}")
if b.get("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
parts.append(f"[#{b.id}]")
if b.text:
parts.append(b.text)
if b.link:
parts.append(f"🔗 {b.link}")
if 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
if days_left < 0:
parts.append(f"{due_str} (OVERDUE)")
elif days_left == 0:
parts.append(f"{due_str} (TODAY)")
else:
parts.append(f"{due_str} ({days_left}d)")
if b.get("created_by_user_id"):
parts.append(f"by {b['created_by_user_id']}")
if b.created_by_user_id:
parts.append(f"by {b.created_by_user_id}")
return " | ".join(parts)
@@ -79,19 +82,26 @@ def get_user_id(update: Update) -> int:
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):
data = storage.load_group_bounties(get_group_id(update))
bounties = data.get("bounties", [])
else:
data = storage.load_user_personal(get_user_id(update))
bounties = data.get("bounties", [])
return get_group_id(update)
return get_user_id(update)
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:
await update.message.reply_text("No bounties yet.")
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)
@@ -100,38 +110,18 @@ async def cmd_my(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
if is_group(update):
group_id = get_group_id(update)
tracking = storage.load_user_tracking(group_id, user_id)
tracked = tracking.get("tracked", [])
bounties = TRACKING_SERVICE.get_tracked_bounties(group_id, user_id)
else:
data = storage.load_user_personal(user_id)
bounties = data.get("bounties", [])
lines = [format_bounty(dict(b), show_id=True) for b in bounties]
await update.message.reply_text(
"\n".join(lines) if lines else "No personal bounties.",
disable_web_page_preview=True,
)
room_id = get_room_id(update)
bounties = BOUNTY_SERVICE.list_bounties(room_id)
if not bounties:
msg = "You are not tracking any bounties." if is_group(update) else "No personal bounties."
await update.message.reply_text(msg)
return
if not tracked:
await update.message.reply_text("You are not tracking any bounties.")
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
)
lines = [format_bounty(b, show_id=True) for b in bounties]
await update.message.reply_text("\n".join(lines), disable_web_page_preview=True)
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
user_id = get_user_id(update)
room_id = get_room_id(update)
if is_group(update):
group_id = get_group_id(update)
bounty = storage.add_group_bounty(group_id, user_id, text, link, due_date_ts)
else:
bounty = storage.add_personal_bounty(user_id, text, link, due_date_ts)
bounty = BOUNTY_SERVICE.add_bounty(
room_id=room_id,
user_id=user_id,
text=text,
link=link,
due_date_ts=due_date_ts,
)
due_str = ""
if due_date_ts:
due_str = f" | Due: {time.strftime('%Y-%m-%d', time.localtime(due_date_ts))}"
await update.message.reply_text(
f"✅ Bounty added (#{bounty['id']}){due_str}",
f"✅ Bounty added (#{bounty.id}){due_str}",
disable_web_page_preview=True,
)
@@ -186,25 +179,25 @@ async def cmd_update(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
return
user_id = get_user_id(update)
room_id = get_room_id(update)
if is_group(update):
group_id = get_group_id(update)
bounty = storage.get_group_bounty(group_id, bounty_id)
if not bounty:
await update.message.reply_text("Bounty not found.")
try:
success = BOUNTY_SERVICE.update_bounty(
room_id=room_id,
bounty_id=bounty_id,
user_id=user_id,
text=text,
link=link,
due_date_ts=due_date_ts,
)
except PermissionError as e:
await update.message.reply_text(f"{e}")
return
if bounty["created_by_user_id"] != user_id:
await update.message.reply_text("⛔ Only the creator can edit this bounty.")
return
storage.update_group_bounty(group_id, bounty_id, text, link, due_date_ts)
else:
bounty = storage.get_personal_bounty(user_id, bounty_id)
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)
if success:
await update.message.reply_text(f"✅ Bounty #{bounty_id} updated.")
else:
await update.message.reply_text("Bounty not found.")
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
user_id = get_user_id(update)
room_id = get_room_id(update)
if is_group(update):
group_id = get_group_id(update)
bounty = storage.get_group_bounty(group_id, bounty_id)
if not bounty:
await update.message.reply_text("Bounty not found.")
return
if bounty["created_by_user_id"] != user_id:
await update.message.reply_text(
"⛔ Only the creator can delete this bounty."
try:
success = BOUNTY_SERVICE.delete_bounty(
room_id=room_id,
bounty_id=bounty_id,
user_id=user_id,
)
except PermissionError as e:
await update.message.reply_text(f"{e}")
return
storage.delete_group_bounty(group_id, bounty_id)
else:
bounty = storage.get_personal_bounty(user_id, bounty_id)
if not bounty:
await update.message.reply_text("Bounty not found.")
return
storage.delete_personal_bounty(user_id, bounty_id)
if success:
await update.message.reply_text(f"✅ Bounty #{bounty_id} deleted.")
else:
await update.message.reply_text("Bounty not found.")
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)
if not args:
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
user_id = get_user_id(update)
room_id = get_room_id(update)
if is_group(update):
group_id = get_group_id(update)
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):
try:
if TRACKING_SERVICE.track_bounty(room_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}.")
except ValueError as e:
await update.message.reply_text(str(e))
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)
if not args:
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
user_id = get_user_id(update)
room_id = get_room_id(update)
if is_group(update):
group_id = get_group_id(update)
if storage.untrack_bounty(group_id, user_id, bounty_id):
if TRACKING_SERVICE.untrack_bounty(room_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:
if storage.untrack_bounty(user_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}.")
await update.message.reply_text("Not tracking bounty #{bounty_id}.")
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"
"/my — bounties you're tracking\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"
"/track <id> — track a bounty\n"
"/untrack <id> — stop tracking\n"
"/track <id> — track a bounty (groups only)\n"
"/untrack <id> — stop tracking (groups only)\n"
"/start — re-initialize\n"
"/help — this message",
disable_web_page_preview=True,