feat: implement lootdrop management endpoints and fix class api types

This commit is contained in:
syntaxbullet
2026-02-08 16:56:34 +01:00
parent 4232674494
commit 073348fa55
3 changed files with 154 additions and 4 deletions

View File

@@ -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`

View File

@@ -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<boolean> {
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();

View File

@@ -951,7 +951,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
const { classService } = await import("@shared/modules/class/class.service");
const data = await req.json() as Record<string, any>;
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<Web
}
}
// =====================================
// Lootdrops API
// =====================================
// GET /api/lootdrops - List lootdrops
if (url.pathname === "/api/lootdrops" && req.method === "GET") {
try {
const { lootdrops } = await import("../../shared/db/schema");
const { DrizzleClient } = await import("@shared/db/DrizzleClient");
const { desc } = await import("drizzle-orm");
const limit = url.searchParams.get("limit") ? parseInt(url.searchParams.get("limit")!) : 50;
const result = await DrizzleClient.select()
.from(lootdrops)
.orderBy(desc(lootdrops.createdAt))
.limit(limit);
const { jsonReplacer } = await import("@shared/lib/utils");
return new Response(JSON.stringify({ lootdrops: result }, jsonReplacer), {
headers: { "Content-Type": "application/json" }
});
} catch (error) {
logger.error("web", "Error fetching lootdrops", error);
return Response.json(
{ error: "Failed to fetch lootdrops", details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}
}
// POST /api/lootdrops - Spawn lootdrop
if (url.pathname === "/api/lootdrops" && req.method === "POST") {
try {
const { lootdropService } = await import("@shared/modules/economy/lootdrop.service");
const { AuroraClient } = await import("../../bot/lib/BotClient");
const { TextChannel } = await import("discord.js");
const data = await req.json() as Record<string, any>;
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 });
},