diff options
| author | Fuwn <[email protected]> | 2025-09-13 15:56:50 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-09-13 16:05:18 -0700 |
| commit | 3e442642316c6246b3d488d6fb58dfa00d3bce95 (patch) | |
| tree | 030022dff3039c480790a9fdc610226a908c142d | |
| parent | fix(server): Replace complaint channel ID (diff) | |
| download | umabotdiscord-3e442642316c6246b3d488d6fb58dfa00d3bce95.tar.xz umabotdiscord-3e442642316c6246b3d488d6fb58dfa00d3bce95.zip | |
feat: Colour role distribution command
| -rw-r--r-- | src/discord/commands.ts | 5 | ||||
| -rw-r--r-- | src/register.ts | 2 | ||||
| -rw-r--r-- | src/server.ts | 226 |
3 files changed, 233 insertions, 0 deletions
diff --git a/src/discord/commands.ts b/src/discord/commands.ts index bcf8cba..85ff7c0 100644 --- a/src/discord/commands.ts +++ b/src/discord/commands.ts @@ -70,3 +70,8 @@ export const COMPLAIN_COMMAND: DiscordCommand = { }, ], }; + +export const COLOURS_COMMAND: DiscordCommand = { + name: "colours", + description: "Show the distribution of colour roles in the server", +}; diff --git a/src/register.ts b/src/register.ts index 8e0202f..c7bd8f5 100644 --- a/src/register.ts +++ b/src/register.ts @@ -4,6 +4,7 @@ import { ROLEPLAY_COMMAND, TOP_COMMAND, COMPLAIN_COMMAND, + COLOURS_COMMAND, type DiscordCommand, } from "./discord/commands.ts"; import dotenv from "dotenv"; @@ -30,6 +31,7 @@ const commands: DiscordCommand[] = [ NSFW_COMMAND, TOP_COMMAND, COMPLAIN_COMMAND, + COLOURS_COMMAND, ]; const response = await fetch(url, { diff --git a/src/server.ts b/src/server.ts index 5192cc9..ce7e8c7 100644 --- a/src/server.ts +++ b/src/server.ts @@ -6,6 +6,7 @@ import { NSFW_COMMAND, TOP_COMMAND, COMPLAIN_COMMAND, + COLOURS_COMMAND, } from "./discord/commands.ts"; import { getCutePost, @@ -21,6 +22,33 @@ import { verifyDiscordRequest } from "./discord/verification.ts"; const router = AutoRouter(); const COMPLAINT_CHANNEL_ID = "1415868433714778204"; +const GUILD_ID = "1406422617724026901"; +const COLOR_ROLE_IDS = [ + "1407075059830624406", // Nice Nature Red + "1407075160250650664", // Taiki Shuttle Green + "1407075256904187997", // Mejiro McQueen Purple + "1407075372427640952", // Gold Ship Grey + "1407075670177091664", // Grass Wonder Gold + "1407078154555752589", // Agnes Tachyon Dark Purple + "1407345006108475476", // Special Week Salmon + "1408246546708959403", // Biwahaya Hide Linen + "1408247166413176943", // Symboli Rudolf Celeste + "1411128003924332764", // King Halo Dark Blue + "1413582797284708474", // Matikanetannhauser Lemon + "1414435043761324042", // Silence Suzuka Sea Green + "1414454914138116158", // Haru Urara Pink + "1414455824524247161", // TM Opera O Orange + "1414456352167825490", // Oguri Cap Buttermilk + "1414541675396862012", // Kitasan Black Sable + "1415083621152460832", // Tokai Teio Royal Blue + "1415539544232824913", // Aston Machan Sienna + "1415567915578818723", // Super Creek Baby Blue + "1415592658906124338", // Sakura Bakushin O Lilac + "1415593126273224795", // El Condor Pasa Biscotti + "1415797242845200475", // Still in Love Crimson + "1415868433714778204", // Mayano Top Gun Navy Blue + "1415868433714778205", // Mr. C.B. Forest Green +]; const sendComplaintToChannel = async ( environment: Environment, @@ -48,6 +76,141 @@ const sendComplaintToChannel = async ( } }; +const fetchRoleDistribution = async ( + environment: Environment, + guildID: string, +): Promise<Array<{ name: string; count: number }>> => { + const roleData: Array<{ name: string; count: number }> = []; + + try { + const guildResponse = await fetch( + `https://discord.com/api/v10/guilds/${guildID}`, + { + headers: { + Authorization: `Bot ${environment.DISCORD_TOKEN}`, + }, + }, + ); + + if (!guildResponse.ok) { + console.error( + "Failed to fetch guild data:", + guildResponse.status, + guildResponse.statusText, + ); + + const errorText = await guildResponse.text(); + + console.error("Error details:", errorText); + + return roleData; + } + + const guild = await guildResponse.json(); + + for (const roleID of COLOR_ROLE_IDS) { + const role = guild.roles?.find((r: any) => r.id === roleID); + + if (role) { + roleData.push({ + name: role.name, + count: 0, + }); + } else { + console.log(`Role not found: ${roleID}`); + } + } + + let after = ""; + let hasMore = true; + let batchCount = 0; + const maxBatches = 10; + + while (hasMore && batchCount < maxBatches) { + const membersResponse = await fetch( + `https://discord.com/api/v10/guilds/${guildID}/members?limit=1000${after ? `&after=${after}` : ""}`, + { + headers: { + Authorization: `Bot ${environment.DISCORD_TOKEN}`, + }, + }, + ); + + if (membersResponse.status === 429) { + const retryAfter = membersResponse.headers.get("Retry-After"); + const resetAfter = membersResponse.headers.get( + "X-RateLimit-Reset-After", + ); + const scope = membersResponse.headers.get("X-RateLimit-Scope"); + + console.log( + `Rate limited! Scope: ${scope}, Retry-After: ${retryAfter}, Reset-After: ${resetAfter}`, + ); + + const delayMs = Math.max( + retryAfter ? parseFloat(retryAfter) * 1000 : 0, + resetAfter ? parseFloat(resetAfter) * 1000 : 0, + ); + + if (delayMs > 0) { + console.log(`Waiting ${delayMs}ms before retry ...`); + + await new Promise((resolve) => setTimeout(resolve, delayMs)); + + continue; + } + } + + if (!membersResponse.ok) { + console.error( + "Failed to fetch members:", + membersResponse.status, + membersResponse.statusText, + ); + + const errorText = await membersResponse.text(); + + console.error("Members error details:", errorText); + + break; + } + + const remaining = membersResponse.headers.get("X-RateLimit-Remaining"); + const resetAfter = membersResponse.headers.get("X-RateLimit-Reset-After"); + + if (remaining === "0" && resetAfter) { + console.log(`Rate limit bucket empty, waiting ${resetAfter}s...`); + + await new Promise((resolve) => + setTimeout(resolve, parseFloat(resetAfter) * 1000), + ); + } + + const members = await membersResponse.json(); + + for (const member of members) + for (const roleId of member.roles || []) { + const roleIndex = COLOR_ROLE_IDS.indexOf(roleId); + + if (roleIndex !== -1) roleData[roleIndex].count++; + } + + hasMore = members.length === 1000; + + if (hasMore && members.length > 0) + after = members[members.length - 1].user.id; + + batchCount += 1; + } + + roleData.sort((a, b) => b.count - a.count); + } catch (error) { + console.error("Error fetching role distribution:", error); + } + + return roleData; +}; + router.get("/", (_request: Request, environment: Environment) => { return new Response(`👋 ${environment.DISCORD_APPLICATION_ID}`); }); @@ -243,6 +406,69 @@ router.post("/", async (request: Request, environment: Environment) => { } } + case COLOURS_COMMAND.name.toLowerCase(): { + try { + if (!interaction.guild_id) + return new JSONResponse({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "❌ This command can only be used in server channels.", + flags: 64, + }, + }); + + const roleDistribution = await fetchRoleDistribution( + environment, + GUILD_ID, + ); + + if (roleDistribution.length === 0) + return new JSONResponse({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: + "❌ Unable to fetch role distribution data. The bot may not have permission to read member lists or the server may not be accessible.", + flags: 64, + }, + }); + + const totalMembers = roleDistribution.reduce( + (sum, role) => sum + role.count, + 0, + ); + const embed: DiscordEmbed = { + title: "🎨 Colour Role Distribution", + description: `Total members with colour roles: **${totalMembers}**`, + color: 0x5865F2, + fields: roleDistribution.map((role) => ({ + name: role.name, + value: `${role.count} member${role.count !== 1 ? "s" : ""}`, + inline: true, + })), + footer: { + text: "Sorted by member count (highest to lowest)", + }, + }; + + return new JSONResponse({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + embeds: [embed], + }, + }); + } catch (error) { + console.error("Error in colours command:", error); + + return new JSONResponse({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "❌ An error occurred while fetching role distribution.", + flags: 64, + }, + }); + } + } + default: return new JSONResponse({ error: "Unknown Type" }, { status: 400 }); } |