refactor(commands): use core services instead of storage module #33
@@ -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,
|
||||||
|
text=text,
|
||||||
|
link=link,
|
||||||
|
due_date_ts=due_date_ts,
|
||||||
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
await update.message.reply_text(f"⛔ {e}")
|
||||||
return
|
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.")
|
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:
|
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:
|
|
||||||
await update.message.reply_text(
|
|
||||||
"⛔ Only the creator can delete this bounty."
|
|
||||||
)
|
)
|
||||||
|
except PermissionError as e:
|
||||||
|
await update.message.reply_text(f"⛔ {e}")
|
||||||
return
|
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.")
|
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:
|
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)
|
|
||||||
if storage.untrack_bounty(group_id, user_id, bounty_id):
|
|
||||||
await update.message.reply_text(f"✅ Untracked bounty #{bounty_id}.")
|
await update.message.reply_text(f"✅ Untracked bounty #{bounty_id}.")
|
||||||
else:
|
else:
|
||||||
await update.message.reply_text(f"Not tracking bounty #{bounty_id}.")
|
await update.message.reply_text("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}.")
|
|
||||||
|
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
Reference in New Issue
Block a user