feat: add /trip slash commands + add trips & itinerary system
New /trips router with full CRUD, attendee management, and nested events. Events validate date is within parent trip range and inherit trip location when not explicitly set. Leaving a trip cascades removal from all its events. New TripCommands cog with /trip create, list, view, delete, join, leave and /trip event add, remove, join, leave. Event autocomplete is scoped to the selected trip. Enforces must-be-on-trip rule for event joins with a clear error message.
This commit is contained in:
@@ -68,5 +68,137 @@ class C2Client:
|
||||
return node
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Trips
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
async def get_trips(self) -> list:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
r = await client.get(f"{self.base}/trips", headers=self._headers())
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
logger.error(f"C2 get_trips failed: {e}")
|
||||
return []
|
||||
|
||||
async def get_trip(self, trip_id: str) -> Optional[dict]:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
r = await client.get(f"{self.base}/trips/{trip_id}", headers=self._headers())
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
logger.error(f"C2 get_trip failed: {e}")
|
||||
return None
|
||||
|
||||
async def create_trip(self, payload: dict) -> Optional[dict]:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
r = await client.post(f"{self.base}/trips", json=payload, headers=self._headers())
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
logger.error(f"C2 create_trip failed: {e}")
|
||||
return None
|
||||
|
||||
async def delete_trip(self, trip_id: str) -> bool:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
r = await client.delete(f"{self.base}/trips/{trip_id}", headers=self._headers())
|
||||
r.raise_for_status()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"C2 delete_trip failed: {e}")
|
||||
return False
|
||||
|
||||
async def join_trip(self, trip_id: str, user_id: str, username: str) -> bool:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
r = await client.post(
|
||||
f"{self.base}/trips/{trip_id}/join",
|
||||
json={"discord_user_id": user_id, "discord_username": username},
|
||||
headers=self._headers(),
|
||||
)
|
||||
r.raise_for_status()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"C2 join_trip failed: {e}")
|
||||
return False
|
||||
|
||||
async def leave_trip(self, trip_id: str, user_id: str) -> bool:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
r = await client.post(
|
||||
f"{self.base}/trips/{trip_id}/leave",
|
||||
json={"discord_user_id": user_id},
|
||||
headers=self._headers(),
|
||||
)
|
||||
r.raise_for_status()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"C2 leave_trip failed: {e}")
|
||||
return False
|
||||
|
||||
async def create_trip_event(self, trip_id: str, payload: dict) -> Optional[dict]:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
r = await client.post(
|
||||
f"{self.base}/trips/{trip_id}/events",
|
||||
json=payload,
|
||||
headers=self._headers(),
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
logger.error(f"C2 create_trip_event failed: {e}")
|
||||
return None
|
||||
|
||||
async def delete_trip_event(self, trip_id: str, event_id: str) -> bool:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
r = await client.delete(
|
||||
f"{self.base}/trips/{trip_id}/events/{event_id}",
|
||||
headers=self._headers(),
|
||||
)
|
||||
r.raise_for_status()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"C2 delete_trip_event failed: {e}")
|
||||
return False
|
||||
|
||||
async def join_trip_event(
|
||||
self, trip_id: str, event_id: str, user_id: str, username: str
|
||||
) -> bool | str:
|
||||
"""Returns True on success, 'not_on_trip' on 403, False on other errors."""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
r = await client.post(
|
||||
f"{self.base}/trips/{trip_id}/events/{event_id}/join",
|
||||
json={"discord_user_id": user_id, "discord_username": username},
|
||||
headers=self._headers(),
|
||||
)
|
||||
if r.status_code == 403:
|
||||
return "not_on_trip"
|
||||
r.raise_for_status()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"C2 join_trip_event failed: {e}")
|
||||
return False
|
||||
|
||||
async def leave_trip_event(self, trip_id: str, event_id: str, user_id: str) -> bool:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
r = await client.post(
|
||||
f"{self.base}/trips/{trip_id}/events/{event_id}/leave",
|
||||
json={"discord_user_id": user_id},
|
||||
headers=self._headers(),
|
||||
)
|
||||
r.raise_for_status()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"C2 leave_trip_event failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
c2 = C2Client()
|
||||
|
||||
Reference in New Issue
Block a user