feat(core): implement services for issue #8 #25

Closed
shoko wants to merge 2 commits from feat/issue-8-services into main
Showing only changes of commit 932bbc5b63 - Show all commits

View File

@@ -4,6 +4,9 @@ These services orchestrate operations through storage ports.
No implementation details - pure orchestration layer. No implementation details - pure orchestration layer.
""" """
import time
from typing import Optional
from core.models import Bounty, RoomData, TrackingData, TrackedBounty from core.models import Bounty, RoomData, TrackingData, TrackedBounty
from core.ports import RoomStorage, TrackingStorage from core.ports import RoomStorage, TrackingStorage
@@ -12,20 +15,26 @@ class RoomBountyService:
"""Service for managing bounties within a room. """Service for managing bounties within a room.
Orchestrates bounty operations through a RoomStorage port. Orchestrates bounty operations through a RoomStorage port.
A room is identified by room_id:
- Negative room_id: Telegram group
- Positive room_id: DM/personal context
This single service handles both group and personal bounties.
""" """
def __init__(self, storage: RoomStorage): def __init__(self, storage: RoomStorage):
self._storage = storage self._storage = storage
def create_bounty( def add_bounty(
self, self,
room_id: int, room_id: int,
text: str | None, user_id: int,
link: str | None, text: Optional[str] = None,
due_date_ts: int | None, link: Optional[str] = None,
created_by_user_id: int, due_date_ts: Optional[int] = None,
) -> Bounty: ) -> Bounty:
"""Create a new bounty in a room. Returns the created bounty.""" """Add a new bounty to a room. Returns the created bounty."""
room_data = self._storage.load(room_id) room_data = self._storage.load(room_id)
if room_data is None: if room_data is None:
room_data = RoomData(room_id=room_id, bounties=[], next_id=1) room_data = RoomData(room_id=room_id, bounties=[], next_id=1)
@@ -35,8 +44,8 @@ class RoomBountyService:
text=text, text=text,
link=link, link=link,
due_date_ts=due_date_ts, due_date_ts=due_date_ts,
created_at=0, created_at=int(time.time()),
created_by_user_id=created_by_user_id, created_by_user_id=user_id,
) )
room_data.bounties.append(bounty) room_data.bounties.append(bounty)
@@ -60,20 +69,28 @@ class RoomBountyService:
self, self,
room_id: int, room_id: int,
bounty_id: int, bounty_id: int,
text: str | None = None, user_id: int,
link: str | None = None, text: Optional[str] = None,
due_date_ts: int | None = None, link: Optional[str] = None,
due_date_ts: Optional[int] = None,
clear_link: bool = False,
clear_due: bool = False,
) -> bool: ) -> bool:
"""Update a bounty. Returns False if not found.""" """Update a bounty. Only creator can update. Raises PermissionError if not creator."""
bounty = self._storage.get_bounty(room_id, bounty_id) bounty = self._storage.get_bounty(room_id, bounty_id)
if bounty is None: if bounty is None:
return False return False
if bounty.created_by_user_id != user_id:
raise PermissionError("Only the creator can edit this bounty.")
updated = Bounty( updated = Bounty(
id=bounty.id, id=bounty.id,
text=text if text is not None else bounty.text, text=text if text is not None else bounty.text,
link=link if link is not None else bounty.link, link=None if clear_link else (link if link is not None else bounty.link),
due_date_ts=due_date_ts if due_date_ts is not None else bounty.due_date_ts, due_date_ts=None
if clear_due
else (due_date_ts if due_date_ts is not None else bounty.due_date_ts),
created_at=bounty.created_at, created_at=bounty.created_at,
created_by_user_id=bounty.created_by_user_id, created_by_user_id=bounty.created_by_user_id,
) )
@@ -81,12 +98,15 @@ class RoomBountyService:
self._storage.update_bounty(room_id, updated) self._storage.update_bounty(room_id, updated)
return True return True
def delete_bounty(self, room_id: int, bounty_id: int) -> bool: def delete_bounty(self, room_id: int, bounty_id: int, user_id: int) -> bool:
"""Delete a bounty. Returns False if not found.""" """Delete a bounty. Only creator can delete. Raises PermissionError if not creator."""
bounty = self._storage.get_bounty(room_id, bounty_id) bounty = self._storage.get_bounty(room_id, bounty_id)
if bounty is None: if bounty is None:
return False return False
if bounty.created_by_user_id != user_id:
raise PermissionError("Only the creator can delete this bounty.")
self._storage.delete_bounty(room_id, bounty_id) self._storage.delete_bounty(room_id, bounty_id)
return True return True
@@ -97,11 +117,16 @@ class TrackingService:
Orchestrates tracking operations through a TrackingStorage port. Orchestrates tracking operations through a TrackingStorage port.
""" """
def __init__(self, storage: TrackingStorage): def __init__(self, storage: TrackingStorage, room_storage: RoomStorage):
self._storage = storage self._storage = storage
self._room_storage = room_storage
def track_bounty(self, room_id: int, user_id: int, bounty_id: int) -> bool: def track_bounty(self, room_id: int, user_id: int, bounty_id: int) -> bool:
"""Start tracking a bounty. Returns False if already tracking.""" """Start tracking a bounty. Verifies bounty exists first. Raises ValueError if not found."""
bounty = self._room_storage.get_bounty(room_id, bounty_id)
if bounty is None:
raise ValueError("Bounty not found.")
tracking_data = self._storage.load(room_id, user_id) tracking_data = self._storage.load(room_id, user_id)
if tracking_data is None: if tracking_data is None:
tracking_data = TrackingData(room_id=room_id, user_id=user_id, tracked=[]) tracking_data = TrackingData(room_id=room_id, user_id=user_id, tracked=[])
@@ -110,12 +135,12 @@ class TrackingService:
if tracked.bounty_id == bounty_id: if tracked.bounty_id == bounty_id:
return False return False
tracked = TrackedBounty(bounty_id=bounty_id, created_at=0) tracked = TrackedBounty(bounty_id=bounty_id, created_at=int(time.time()))
self._storage.track_bounty(room_id, user_id, tracked) self._storage.track_bounty(room_id, user_id, tracked)
return True return True
def untrack_bounty(self, room_id: int, user_id: int, bounty_id: int) -> bool: def untrack_bounty(self, room_id: int, user_id: int, bounty_id: int) -> bool:
"""Stop tracking a bounty. Returns False if not tracking.""" """Stop tracking a bounty. Returns True if was tracking, False if not found."""
tracking_data = self._storage.load(room_id, user_id) tracking_data = self._storage.load(room_id, user_id)
if tracking_data is None: if tracking_data is None:
return False return False