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:
@@ -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.")
|
||||
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)
|
||||
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 success:
|
||||
await update.message.reply_text(f"✅ Bounty #{bounty_id} updated.")
|
||||
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)
|
||||
|
||||
await update.message.reply_text(f"✅ Bounty #{bounty_id} updated.")
|
||||
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."
|
||||
)
|
||||
return
|
||||
storage.delete_group_bounty(group_id, bounty_id)
|
||||
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
|
||||
|
||||
if success:
|
||||
await update.message.reply_text(f"✅ Bounty #{bounty_id} deleted.")
|
||||
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)
|
||||
|
||||
await update.message.reply_text(f"✅ Bounty #{bounty_id} deleted.")
|
||||
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):
|
||||
await update.message.reply_text(f"✅ Untracked bounty #{bounty_id}.")
|
||||
else:
|
||||
await update.message.reply_text(f"Not tracking bounty #{bounty_id}.")
|
||||
if TRACKING_SERVICE.untrack_bounty(room_id, user_id, bounty_id):
|
||||
await update.message.reply_text(f"✅ Untracked 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,
|
||||
|
||||
Reference in New Issue
Block a user