From 073348fa55822fe9e167b593818c5db5a301ec74 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Sun, 8 Feb 2026 16:56:34 +0100 Subject: [PATCH] feat: implement lootdrop management endpoints and fix class api types --- docs/api.md | 30 +++++++ shared/modules/economy/lootdrop.service.ts | 36 ++++++++- web/src/server.ts | 92 +++++++++++++++++++++- 3 files changed, 154 insertions(+), 4 deletions(-) diff --git a/docs/api.md b/docs/api.md index 3927527..aa02226 100644 --- a/docs/api.md +++ b/docs/api.md @@ -350,6 +350,36 @@ List economy transactions. --- +--- + +## Lootdrops + +### `GET /api/lootdrops` +List lootdrops (default limit 50, sorted by newest). + +| Query Param | Type | Description | +|-------------|------|-------------| +| `limit` | number | Max results (default: 50) | + +**Response:** `{ "lootdrops": [...] }` + +### `POST /api/lootdrops` +Spawn a lootdrop in a channel. + +**Body:** +```json +{ + "channelId": "1234567890", + "amount": 100, + "currency": "Gold" +} +``` + +### `DELETE /api/lootdrops/:messageId` +Cancel and delete a lootdrop. + +--- + ## Quests ### `GET /api/quests` diff --git a/shared/modules/economy/lootdrop.service.ts b/shared/modules/economy/lootdrop.service.ts index 960fd37..0364a9f 100644 --- a/shared/modules/economy/lootdrop.service.ts +++ b/shared/modules/economy/lootdrop.service.ts @@ -93,11 +93,11 @@ class LootdropService { } } - private async spawnLootdrop(channel: TextChannel) { + public async spawnLootdrop(channel: TextChannel, overrideReward?: number, overrideCurrency?: string) { const min = config.lootdrop.reward.min; const max = config.lootdrop.reward.max; - const reward = Math.floor(Math.random() * (max - min + 1)) + min; - const currency = config.lootdrop.reward.currency; + const reward = overrideReward ?? (Math.floor(Math.random() * (max - min + 1)) + min); + const currency = overrideCurrency ?? config.lootdrop.reward.currency; const { content, files, components } = await getLootdropMessage(reward, currency); @@ -205,6 +205,36 @@ class LootdropService { this.channelCooldowns.clear(); console.log("[LootdropService] Caches cleared via administrative action."); } + public async deleteLootdrop(messageId: string): Promise { + try { + // First fetch it to get channel info so we can delete the message + const drop = await DrizzleClient.query.lootdrops.findFirst({ + where: eq(lootdrops.messageId, messageId) + }); + + if (!drop) return false; + + // Delete from DB + await DrizzleClient.delete(lootdrops).where(eq(lootdrops.messageId, messageId)); + + // Try to delete from Discord + try { + const { AuroraClient } = await import("../../../bot/lib/BotClient"); + const channel = await AuroraClient.channels.fetch(drop.channelId) as TextChannel; + if (channel) { + const message = await channel.messages.fetch(messageId); + if (message) await message.delete(); + } + } catch (e) { + console.warn("Could not delete lootdrop message from Discord:", e); + } + + return true; + } catch (error) { + console.error("Error deleting lootdrop:", error); + return false; + } + } } export const lootdropService = new LootdropService(); diff --git a/web/src/server.ts b/web/src/server.ts index 883cfbf..df1fd4a 100644 --- a/web/src/server.ts +++ b/web/src/server.ts @@ -951,7 +951,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise; - if (!data.id || !data.name) { + if (!data.id || !data.name || typeof data.name !== 'string') { return Response.json( { error: "Missing required fields: id and name are required" }, { status: 400 } @@ -1206,6 +1206,96 @@ export async function createWebServer(config: WebServerConfig = {}): Promise; + + if (!data.channelId) { + return Response.json( + { error: "Missing required field: channelId" }, + { status: 400 } + ); + } + + const channel = await AuroraClient.channels.fetch(data.channelId); + + if (!channel || !(channel instanceof TextChannel)) { + return Response.json( + { error: "Invalid channel. Must be a TextChannel." }, + { status: 400 } + ); + } + + await lootdropService.spawnLootdrop(channel, data.amount, data.currency); + + return Response.json({ success: true }, { status: 201 }); + } catch (error) { + logger.error("web", "Error spawning lootdrop", error); + return Response.json( + { error: "Failed to spawn lootdrop", details: error instanceof Error ? error.message : String(error) }, + { status: 500 } + ); + } + } + + // DELETE /api/lootdrops/:id - Cancel/Delete lootdrop + if (url.pathname.match(/^\/api\/lootdrops\/[^\/]+$/) && req.method === "DELETE") { + const messageId = url.pathname.split("/").pop()!; + + try { + const { lootdropService } = await import("@shared/modules/economy/lootdrop.service"); + const success = await lootdropService.deleteLootdrop(messageId); + + if (!success) { + return Response.json({ error: "Lootdrop not found" }, { status: 404 }); + } + + return new Response(null, { status: 204 }); + } catch (error) { + logger.error("web", "Error deleting lootdrop", error); + return Response.json( + { error: "Failed to delete lootdrop", details: error instanceof Error ? error.message : String(error) }, + { status: 500 } + ); + } + } + // No frontend - return 404 for unknown routes return new Response("Not Found", { status: 404 }); },