summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/discord/commands.ts5
-rw-r--r--src/register.ts2
-rw-r--r--src/server.ts226
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 });
}