Skip to content

Commit dd17695

Browse files
authored
Add wrapped (#560)
1 parent c54a6a4 commit dd17695

File tree

2 files changed

+605
-0
lines changed

2 files changed

+605
-0
lines changed

src/commands/wrapped.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
//
2+
// !CAUTION! this file is entirely vibe-coded and only used for dönerjesus-wrapped
3+
// !CAUTION! It will be deleted in 2026-01-01
4+
//
5+
6+
import {
7+
ActionRowBuilder,
8+
ButtonBuilder,
9+
ButtonStyle,
10+
type CommandInteraction,
11+
ComponentType,
12+
ContainerBuilder,
13+
MessageFlags,
14+
SlashCommandBuilder,
15+
type User,
16+
} from "discord.js";
17+
18+
import type { BotContext } from "#context.ts";
19+
import type { ApplicationCommand } from "#commands/command.ts";
20+
import { ensureChatInputCommand } from "#utils/interactionUtils.ts";
21+
import * as statsService from "#service/stats.ts";
22+
import log from "#log";
23+
24+
export default class WrappedCommand implements ApplicationCommand {
25+
name = "wrapped";
26+
description = "Zeigt deine Jahresstatistiken";
27+
28+
applicationCommand = new SlashCommandBuilder()
29+
.setName(this.name)
30+
.setDescription(this.description);
31+
32+
async handleInteraction(interaction: CommandInteraction, context: BotContext) {
33+
const cmd = ensureChatInputCommand(interaction);
34+
const user = cmd.user;
35+
36+
await this.#createWrappedView(context, interaction, user);
37+
}
38+
39+
async #createWrappedView(context: BotContext, interaction: CommandInteraction, user: User) {
40+
// Load all stats in parallel
41+
const [pollStats, inventoryStats, honorStats, penisStats, boobsStats, topEmotes] =
42+
await Promise.all([
43+
statsService.getPollStats(user.id),
44+
statsService.getInventoryStats(user.id),
45+
statsService.getHonorStats(user.id),
46+
statsService.getPenisStats(user.id),
47+
statsService.getBoobsStats(user.id),
48+
statsService.getMostFrequentEmote(5),
49+
]);
50+
51+
const totalPages = 5;
52+
53+
function buildMessageData(pageIndex: number) {
54+
const container = new ContainerBuilder().addTextDisplayComponents(t =>
55+
t.setContent(`## 📊 ${user}'s Wrapped ${new Date().getUTCFullYear()}`),
56+
);
57+
58+
switch (pageIndex) {
59+
case 0: {
60+
container.addTextDisplayComponents(
61+
t => t.setContent(`### 📋 Umfragen\n`),
62+
t =>
63+
t.setContent(
64+
`Du hast **${pollStats.userPolls ?? 0}** Umfragen erstellt\n` +
65+
`und bei **${pollStats.userVotes ?? 0}** Umfragen abgestimmt\n\n` +
66+
`Insgesamt gibt es **${pollStats.totalPolls}** Umfragen\n` +
67+
`mit **${pollStats.totalVotes}** Stimmen.`,
68+
),
69+
);
70+
break;
71+
}
72+
73+
case 1: {
74+
container.addTextDisplayComponents(
75+
t => t.setContent(`### 🎁 Inventar\n`),
76+
t =>
77+
t.setContent(
78+
`Du besitzt **${inventoryStats.itemCount}** Items\n\n` +
79+
`Du bist besser als **${inventoryStats.percentile.toFixed(1)}%** ` +
80+
`aller anderen Nutzer! 🎉`,
81+
),
82+
);
83+
break;
84+
}
85+
86+
case 2: {
87+
container.addTextDisplayComponents(
88+
t => t.setContent(`### ⭐ Ehre\n`),
89+
t =>
90+
t.setContent(
91+
`Du hast **${honorStats.collectedPoints.toFixed(1)}** Ehre-Punkte gesammelt\n\n` +
92+
`Du hast **${honorStats.votesGiven}** mal Ehre vergeben\n` +
93+
`und dabei etwa **${honorStats.awardedPoints}** Punkte verteilt.`,
94+
),
95+
);
96+
break;
97+
}
98+
99+
case 3: {
100+
container.addTextDisplayComponents(t => t.setContent(`### 📏 Messungen\n`));
101+
102+
let penisContent: string;
103+
if (penisStats.userSize !== undefined) {
104+
penisContent =
105+
`**Penis:** ${penisStats.userSize}cm (${penisStats.userRadius}cm Radius)\n` +
106+
`Du bist größer als **${penisStats.userSizePercentile?.toFixed(1) ?? 0}%** der anderen\n\n` +
107+
`*Durchschnitt: ${penisStats.averageSize.toFixed(1)}cm (${penisStats.averageRadius.toFixed(1)}cm Radius)*\n` +
108+
`*Min: ${penisStats.minSize}cm | Max: ${penisStats.maxSize}cm*\n\n`;
109+
} else {
110+
penisContent = `**Penis:** Noch nicht gemessen\n\n`;
111+
}
112+
113+
let boobsContent: string;
114+
if (boobsStats.userSize !== undefined) {
115+
boobsContent =
116+
`**Boobs:** Größe ${boobsStats.userSize}\n` +
117+
`Du bist größer als **${boobsStats.userSizePercentile?.toFixed(1) ?? 0}%** der anderen\n\n` +
118+
`*Durchschnitt: ${boobsStats.averageSize.toFixed(1)}*\n` +
119+
`*Min: ${boobsStats.minSize} | Max: ${boobsStats.maxSize}*`;
120+
} else {
121+
boobsContent = `**Boobs:** Noch nicht gemessen`;
122+
}
123+
124+
container.addTextDisplayComponents(t =>
125+
t.setContent(penisContent + boobsContent),
126+
);
127+
break;
128+
}
129+
130+
case 4: {
131+
container.addTextDisplayComponents(t => t.setContent(`### 😀 Emotes\n`));
132+
133+
if (topEmotes.length > 0) {
134+
const emoteList = topEmotes
135+
.map((emote, idx) => {
136+
const emoji = context.client.emojis.cache.get(emote.emoteId);
137+
const display = emoji ? `${emoji}` : emote.emoteName;
138+
return `${idx + 1}. ${display} - **${emote.usageCount}** mal verwendet`;
139+
})
140+
.join("\n");
141+
142+
container.addTextDisplayComponents(t =>
143+
t.setContent(`Top 5 häufigste Emotes auf dem Server:\n\n${emoteList}`),
144+
);
145+
} else {
146+
container.addTextDisplayComponents(t =>
147+
t.setContent("Keine Emote-Statistiken verfügbar."),
148+
);
149+
}
150+
break;
151+
}
152+
153+
default: {
154+
container.addTextDisplayComponents(t => t.setContent("Ungültige Seite."));
155+
}
156+
}
157+
158+
return {
159+
components: [
160+
container,
161+
new ActionRowBuilder<ButtonBuilder>().addComponents(
162+
new ButtonBuilder()
163+
.setCustomId("wrapped-prev")
164+
.setLabel("← Zurück")
165+
.setStyle(ButtonStyle.Secondary)
166+
.setDisabled(pageIndex <= 0),
167+
new ButtonBuilder()
168+
.setCustomId("wrapped-next")
169+
.setLabel("Weiter →")
170+
.setStyle(ButtonStyle.Secondary)
171+
.setDisabled(pageIndex >= totalPages - 1),
172+
),
173+
],
174+
} as const;
175+
}
176+
177+
let pageIndex = 0;
178+
179+
const callbackResponse = await interaction.reply({
180+
...buildMessageData(pageIndex),
181+
flags: MessageFlags.IsComponentsV2,
182+
withResponse: true,
183+
tts: false,
184+
});
185+
186+
const message = callbackResponse.resource?.message;
187+
if (message === null || message === undefined) {
188+
throw new Error("Expected message to be present.");
189+
}
190+
191+
const collector = message.createMessageComponentCollector({
192+
componentType: ComponentType.Button,
193+
filter: i => i.user.id === interaction.user.id,
194+
time: 300_000, // 5 minutes
195+
});
196+
197+
collector.on("collect", async i => {
198+
i.deferUpdate();
199+
switch (i.customId) {
200+
case "wrapped-prev":
201+
pageIndex = Math.max(0, pageIndex - 1);
202+
break;
203+
case "wrapped-next":
204+
pageIndex = Math.min(totalPages - 1, pageIndex + 1);
205+
break;
206+
default:
207+
log.warn(`Unknown customId: "${i.customId}"`);
208+
return;
209+
}
210+
211+
await message.edit({
212+
...buildMessageData(pageIndex),
213+
});
214+
});
215+
216+
collector.on("end", async () => {
217+
await message.edit({
218+
components: [],
219+
});
220+
});
221+
}
222+
}

0 commit comments

Comments
 (0)