summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFuwn <[email protected]>2025-09-24 18:14:30 -0700
committerFuwn <[email protected]>2025-09-24 18:14:30 -0700
commit2d987046d094cf5eb784c8d79d678bd3efa5eaf9 (patch)
treedd37d395961d9a68e3e1293a89fb46992aab88d1 /src
parentstyle: Lint (diff)
downloadumabotdiscord-2d987046d094cf5eb784c8d79d678bd3efa5eaf9.tar.xz
umabotdiscord-2d987046d094cf5eb784c8d79d678bd3efa5eaf9.zip
refactor: Move interactions client to packages directory
Diffstat (limited to 'src')
-rw-r--r--src/discord/commands.ts125
-rw-r--r--src/discord/embeds.ts181
-rw-r--r--src/discord/interfaces.ts86
-rw-r--r--src/discord/responses.ts15
-rw-r--r--src/discord/types.ts1
-rw-r--r--src/discord/verification.ts24
-rw-r--r--src/reddit.ts208
-rw-r--r--src/register.ts70
-rw-r--r--src/server.ts741
9 files changed, 0 insertions, 1451 deletions
diff --git a/src/discord/commands.ts b/src/discord/commands.ts
deleted file mode 100644
index 601591b..0000000
--- a/src/discord/commands.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-import type { DiscordCommand } from "./interfaces.ts";
-
-export type { DiscordCommand };
-
-export const HOT_COMMAND: DiscordCommand = {
- name: "hot",
- description: "Fetch a random hot post from r/okbuddyumamusume",
-};
-
-export const ROLEPLAY_COMMAND: DiscordCommand = {
- name: "roleplay",
- description: "Fetch a random hot roleplay post from r/okbuddyumamusume",
-};
-
-export const NSFW_COMMAND: DiscordCommand = {
- name: "nsfw",
- description:
- "Fetch a random NSFW post from r/okbuddyumamusume (NSFW channels only)",
-};
-
-export const TOP_COMMAND: DiscordCommand = {
- name: "top",
- description:
- "Fetch a random top post from r/okbuddyumamusume (defaults to today)",
- options: [
- {
- type: 3,
- name: "time",
- description: "Time period for top posts (defaults to today)",
- required: false,
- choices: [
- {
- name: "Now",
- value: "hour",
- },
- {
- name: "Today",
- value: "day",
- },
- {
- name: "This Week",
- value: "week",
- },
- {
- name: "This Month",
- value: "month",
- },
- {
- name: "This Year",
- value: "year",
- },
- {
- name: "All Time",
- value: "all",
- },
- ],
- },
- ],
-};
-
-export const COMPLAIN_COMMAND: DiscordCommand = {
- name: "complain",
- description: "Submit a complaint to the moderators",
- contexts: [0],
- options: [
- {
- type: 3,
- name: "message",
- description: "Your complaint message",
- required: true,
- },
- ],
-};
-
-export const APPEAL_COMMAND: DiscordCommand = {
- name: "appeal",
- description: "Submit an appeal to the moderators",
- contexts: [0],
- options: [
- {
- type: 3,
- name: "message",
- description: "Your appeal message",
- required: true,
- },
- ],
-};
-
-export const COLOURS_COMMAND: DiscordCommand = {
- name: "colours",
- description: "Show the distribution of colour roles in the server",
-};
-
-export const ROLEPLAY_SERIOUS_COMMAND: DiscordCommand = {
- name: "roleplay-serious",
- description: "Manage the serious roleplay role (Admin/Roleplay Curator only)",
- options: [
- {
- type: 3,
- name: "action",
- description: "Action to perform on the role",
- required: true,
- choices: [
- {
- name: "Add Role",
- value: "add",
- },
- {
- name: "Remove Role",
- value: "remove",
- },
- {
- name: "Toggle Role",
- value: "toggle",
- },
- ],
- },
- {
- type: 6,
- name: "user",
- description: "User to perform the action on",
- required: true,
- },
- ],
-};
diff --git a/src/discord/embeds.ts b/src/discord/embeds.ts
deleted file mode 100644
index 3f7c344..0000000
--- a/src/discord/embeds.ts
+++ /dev/null
@@ -1,181 +0,0 @@
-import type { DiscordEmbed } from "./interfaces.ts";
-import type { RedditPost } from "../reddit.ts";
-
-const decodeHtmlEntities = (str: string): string => {
- return str
- .replace(/&amp;/g, "&")
- .replace(/&lt;/g, "<")
- .replace(/&gt;/g, ">")
- .replace(/&quot;/g, '"')
- .replace(/&#x27;/g, "'")
- .replace(/&#x2F;/g, "/")
- .replace(/&#x60;/g, "`")
- .replace(/&#x3D;/g, "=");
-};
-
-export const createPostEmbed = (post: RedditPost): DiscordEmbed => {
- const mediaUrl =
- post.media?.reddit_video?.fallback_url ||
- post.secure_media?.reddit_video?.fallback_url ||
- post.url;
-
- let description = post.selftext || "";
-
- if (description.length > 1000)
- description = description.substring(0, 997).trim() + " ...";
-
- const embed: DiscordEmbed = {
- title: post.title,
- description: description,
- url: `https://reddit.com${post.permalink}`,
- color: 0xff4500,
- author: {
- name: `u/${post.author}`,
- url: `https://reddit.com/u/${post.author}`,
- },
- fields: [
- {
- name: "Score",
- value: `${post.score} ā¬†ļø`,
- inline: true,
- },
- {
- name: "Comments",
- value: `${post.num_comments} šŸ’¬`,
- inline: true,
- },
- ],
- timestamp: new Date(post.created_utc * 1000).toISOString(),
- footer: {
- text: "r/okbuddyumamusume",
- },
- };
-
- if (mediaUrl)
- if (post.media?.reddit_video || post.secure_media?.reddit_video) {
- if (!description) description = "";
-
- description +=
- "\n\nšŸ“¹ **This post contains a video** - [Click here to view](" +
- mediaUrl +
- ")";
- embed.description = description;
-
- if (post.preview?.images?.[0]?.source?.url) {
- const decodedURL = decodeHtmlEntities(
- post.preview.images[0].source.url,
- );
-
- console.log("Using preview image:", decodedURL);
-
- embed.image = { url: decodedURL };
- } else if (
- post.thumbnail &&
- post.thumbnail !== "self" &&
- post.thumbnail !== "default"
- ) {
- const decodedThumbnail = decodeHtmlEntities(post.thumbnail);
-
- console.log("Using thumbnail:", decodedThumbnail);
-
- embed.image = { url: decodedThumbnail };
- } else {
- console.log("No suitable thumbnail found for video post");
- }
- } else {
- embed.image = { url: mediaUrl };
- }
-
- return embed;
-};
-
-export const createRoleDistributionEmbed = (
- roleDistribution: Array<{ name: string; count: number }>,
-): DiscordEmbed => {
- const totalMembers = roleDistribution.reduce(
- (sum, role) => sum + role.count,
- 0,
- );
-
- return {
- 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)",
- },
- };
-};
-
-export const createComplaintEmbed = (
- complaintContent: string,
- complainant: { username: string; id: string; avatar?: string },
- timestamp: number,
- isDM: boolean = true,
-): DiscordEmbed => {
- return {
- title: "🚨 New Complaint",
- description: complaintContent,
- color: 0xff6b6b,
- fields: [
- {
- name: "Complainant",
- value: `${complainant.username} (${complainant.id})`,
- inline: true,
- },
- {
- name: "Timestamp",
- value: `<t:${Math.floor(timestamp / 1000)}:F>`,
- inline: true,
- },
- ],
- thumbnail: complainant.avatar
- ? {
- url: `https://cdn.discordapp.com/avatars/${complainant.id}/${complainant.avatar}.png?size=256`,
- }
- : undefined,
- footer: {
- text: isDM
- ? "Complaint submitted via DM"
- : "Complaint submitted from server",
- },
- };
-};
-
-export const createAppealEmbed = (
- appealContent: string,
- appellant: { username: string; id: string; avatar?: string },
- timestamp: number,
- isDM: boolean = true,
-): DiscordEmbed => {
- return {
- title: "šŸ“‹ New Appeal",
- description: appealContent,
- color: 0x5865f2,
- fields: [
- {
- name: "Appellant",
- value: `${appellant.username} (${appellant.id})`,
- inline: true,
- },
- {
- name: "Timestamp",
- value: `<t:${Math.floor(timestamp / 1000)}:F>`,
- inline: true,
- },
- ],
- thumbnail: appellant.avatar
- ? {
- url: `https://cdn.discordapp.com/avatars/${appellant.id}/${appellant.avatar}.png?size=256`,
- }
- : undefined,
- footer: {
- text: isDM ? "Appeal submitted via DM" : "Appeal submitted from server",
- },
- };
-};
diff --git a/src/discord/interfaces.ts b/src/discord/interfaces.ts
deleted file mode 100644
index bc8683c..0000000
--- a/src/discord/interfaces.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-export interface Environment {
- DISCORD_APPLICATION_ID: string;
- DISCORD_PUBLIC_KEY: string;
- DISCORD_TOKEN: string;
-}
-
-export interface DiscordInteraction {
- type: number;
- data: {
- name: string;
- options?: Array<{
- name: string;
- value: string;
- }>;
- };
- channel_id?: string;
- channel?: {
- nsfw: boolean;
- };
- guild_id?: string;
- user?: {
- id: string;
- username: string;
- avatar?: string;
- };
- member?: {
- user?: {
- id: string;
- username: string;
- avatar?: string;
- };
- roles?: string[];
- permissions?: string;
- };
-}
-
-export interface DiscordEmbed {
- title: string;
- description: string;
- url?: string;
- color: number;
- author?: {
- name: string;
- url: string;
- };
- fields?: Array<{
- name: string;
- value: string;
- inline: boolean;
- }>;
- timestamp?: string;
- footer?: {
- text: string;
- };
- image?: { url: string };
- thumbnail?: { url: string };
-}
-
-export interface DiscordResponse {
- type: number;
- data?: {
- content?: string;
- embeds?: DiscordEmbed[];
- flags?: number;
- };
-}
-
-export interface DiscordCommand {
- name: string;
- description: string;
- options?: DiscordCommandOption[];
- contexts?: number[];
-}
-
-export interface DiscordCommandOption {
- type: number;
- name: string;
- description: string;
- required?: boolean;
- choices?: DiscordCommandChoice[];
-}
-
-export interface DiscordCommandChoice {
- name: string;
- value: string;
-}
diff --git a/src/discord/responses.ts b/src/discord/responses.ts
deleted file mode 100644
index 4dcc777..0000000
--- a/src/discord/responses.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { DiscordResponse } from "./interfaces.ts";
-
-export class JSONResponse extends Response {
- constructor(body: DiscordResponse | { error: string }, init?: ResponseInit) {
- const jsonBody = JSON.stringify(body);
-
- init = init || {
- headers: {
- "content-type": "application/json;charset=UTF-8",
- },
- };
-
- super(jsonBody, init);
- }
-}
diff --git a/src/discord/types.ts b/src/discord/types.ts
deleted file mode 100644
index 4f6e85e..0000000
--- a/src/discord/types.ts
+++ /dev/null
@@ -1 +0,0 @@
-export type TimePeriod = "hour" | "day" | "week" | "month" | "year" | "all";
diff --git a/src/discord/verification.ts b/src/discord/verification.ts
deleted file mode 100644
index 89d26db..0000000
--- a/src/discord/verification.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { verifyKey } from "discord-interactions";
-import type { Environment, DiscordInteraction } from "./interfaces.ts";
-
-export const verifyDiscordRequest = async (
- request: Request,
- environment: Environment,
-): Promise<{ isValid: boolean; interaction?: DiscordInteraction }> => {
- const signature = request.headers.get("x-signature-ed25519");
- const timestamp = request.headers.get("x-signature-timestamp");
- const body = await request.text();
- const isValidRequest =
- signature &&
- timestamp &&
- (await verifyKey(
- body,
- signature,
- timestamp,
- environment.DISCORD_PUBLIC_KEY,
- ));
-
- if (!isValidRequest) return { isValid: false };
-
- return { interaction: JSON.parse(body) as DiscordInteraction, isValid: true };
-};
diff --git a/src/reddit.ts b/src/reddit.ts
deleted file mode 100644
index 5b4ded7..0000000
--- a/src/reddit.ts
+++ /dev/null
@@ -1,208 +0,0 @@
-import type { TimePeriod } from "./discord/types.ts";
-
-export interface RedditPost {
- id: string;
- title: string;
- author: string;
- score: number;
- num_comments: number;
- created_utc: number;
- permalink: string;
- url: string;
- selftext: string;
- is_gallery?: boolean;
- over_18: boolean;
- link_flair_text?: string;
- thumbnail?: string;
- preview?: {
- images: Array<{
- source: {
- url: string;
- width: number;
- height: number;
- };
- resolutions: Array<{
- url: string;
- width: number;
- height: number;
- }>;
- }>;
- enabled: boolean;
- };
- media?: {
- reddit_video?: {
- fallback_url: string;
- };
- };
- secure_media?: {
- reddit_video?: {
- fallback_url: string;
- };
- };
-}
-
-export interface RedditResponse {
- data: {
- children: Array<{
- data: RedditPost;
- }>;
- };
-}
-
-type SortType = "hot" | "top";
-
-const fetchWithRetry = async (
- url: string,
- maxRetries: number = 3,
-): Promise<Response> => {
- for (let attempt = 0; attempt < maxRetries; attempt++)
- try {
- await new Promise((resolve) =>
- setTimeout(resolve, Math.random() * 1000 + 500),
- );
-
- const response = await fetch(url, {
- headers: {
- "User-Agent":
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
- Accept:
- "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
- "Accept-Language": "en-US,en;q=0.5",
- "Accept-Encoding": "gzip, deflate, br",
- DNT: "1",
- Connection: "keep-alive",
- "Upgrade-Insecure-Requests": "1",
- },
- });
-
- return response;
- } catch (error) {
- if (attempt === maxRetries - 1) throw error;
-
- const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
-
- console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms ...`);
-
- await new Promise((resolve) => setTimeout(resolve, delay));
- }
-
- throw new Error("Max retries exceeded");
-};
-
-export const fetchRedditPosts = async (
- sort: SortType = "hot",
- time: TimePeriod = "day",
-): Promise<RedditPost[]> => {
- const url = `https://www.reddit.com/r/okbuddyumamusume/${sort}.json${sort === "top" ? `?t=${time}` : ""}`;
- const response = await fetchWithRetry(url);
-
- if (!response.ok) {
- let errorText = `Error fetching ${response.url}: ${response.status} ${response.statusText}`;
-
- try {
- const error = await response.text();
-
- if (
- error.includes("You've been blocked by network security") ||
- error.includes("blocked by network security")
- )
- throw new Error(
- "Reddit is blocking requests due to network security. This may be due to rate limiting or bot detection. Please try again later.",
- );
-
- if (error) errorText = `${errorText} \n\n ${error}`;
- } catch (err) {
- if (
- err instanceof Error &&
- err.message.includes("blocked by network security")
- )
- throw err;
- }
-
- throw new Error(errorText);
- }
-
- const data: RedditResponse = await response.json();
-
- return data.data.children.map((post) => post.data);
-};
-
-export const filterPostsByFlair = (
- posts: RedditPost[],
- excludedFlairs: string[] = [],
- includedFlairs: string[] = [],
-): RedditPost[] => {
- return posts.filter((post) => {
- if (post.is_gallery) return false;
-
- const hasMedia =
- post.media?.reddit_video?.fallback_url ||
- post.secure_media?.reddit_video?.fallback_url ||
- post.url;
-
- if (!hasMedia) return false;
-
- const postFlair = post.link_flair_text?.toLowerCase() || "";
- const isNSFW = post.over_18 || postFlair.includes("nsfw");
-
- if (
- includedFlairs.length > 0 &&
- includedFlairs.some((flair) => flair.toLowerCase() === "nsfw")
- )
- if (includedFlairs.some((flair) => flair.toLowerCase() === "nsfw"))
- return isNSFW;
-
- if (isNSFW) return false;
-
- if (includedFlairs.length > 0)
- return includedFlairs.some((flair) =>
- postFlair.includes(flair.toLowerCase()),
- );
-
- if (excludedFlairs.length > 0)
- return !excludedFlairs.some((flair) =>
- postFlair.includes(flair.toLowerCase()),
- );
-
- return true;
- });
-};
-
-const getRandomPost = (posts: RedditPost[]): RedditPost => {
- if (posts.length === 0)
- throw new Error("No posts found matching the criteria");
-
- const randomIndex = Math.floor(Math.random() * posts.length);
-
- return posts[randomIndex];
-};
-
-export const getCutePost = async (): Promise<RedditPost> => {
- const posts = await fetchRedditPosts("hot");
- const filteredPosts = filterPostsByFlair(posts, ["roleplay", "announcement"]);
-
- return getRandomPost(filteredPosts);
-};
-
-export const getRoleplayPost = async (): Promise<RedditPost> => {
- const posts = await fetchRedditPosts("hot");
- const filteredPosts = filterPostsByFlair(posts, [], ["roleplay"]);
-
- return getRandomPost(filteredPosts);
-};
-
-export const getNSFWPost = async (): Promise<RedditPost> => {
- const posts = await fetchRedditPosts("hot");
- const filteredPosts = filterPostsByFlair(posts, [], ["nsfw"]);
-
- return getRandomPost(filteredPosts);
-};
-
-export const getTopPost = async (
- time: TimePeriod = "day",
-): Promise<RedditPost> => {
- const posts = await fetchRedditPosts("top", time);
- const filteredPosts = filterPostsByFlair(posts, ["roleplay", "announcement"]);
-
- return getRandomPost(filteredPosts);
-};
diff --git a/src/register.ts b/src/register.ts
deleted file mode 100644
index a23c9c8..0000000
--- a/src/register.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import {
- HOT_COMMAND,
- NSFW_COMMAND,
- ROLEPLAY_COMMAND,
- TOP_COMMAND,
- COMPLAIN_COMMAND,
- APPEAL_COMMAND,
- COLOURS_COMMAND,
- ROLEPLAY_SERIOUS_COMMAND,
- type DiscordCommand,
-} from "./discord/commands.ts";
-import dotenv from "dotenv";
-import process from "node:process";
-
-dotenv.config({ path: ".dev.vars" });
-
-const token = process.env.DISCORD_TOKEN;
-const applicationID = process.env.DISCORD_APPLICATION_ID;
-
-if (!token)
- throw new Error("The DISCORD_TOKEN environment variable is required.");
-
-if (!applicationID)
- throw new Error(
- "The DISCORD_APPLICATION_ID environment variable is required.",
- );
-
-const url = `https://discord.com/api/v10/applications/${applicationID}/commands`;
-
-const commands: DiscordCommand[] = [
- HOT_COMMAND,
- ROLEPLAY_COMMAND,
- NSFW_COMMAND,
- TOP_COMMAND,
- COMPLAIN_COMMAND,
- APPEAL_COMMAND,
- COLOURS_COMMAND,
- ROLEPLAY_SERIOUS_COMMAND,
-];
-
-const response = await fetch(url, {
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bot ${token}`,
- },
- method: "PUT",
- body: JSON.stringify(commands),
-});
-
-if (response.ok) {
- console.log("Registered all commands");
-
- const data = await response.json();
-
- console.log(JSON.stringify(data, null, 2));
-} else {
- console.error("Error registering commands");
-
- let errorText = `Error registering commands \n ${response.url}: ${response.status} ${response.statusText}`;
-
- try {
- const error = await response.text();
-
- if (error) errorText = `${errorText} \n\n ${error}`;
- } catch (error) {
- console.error("Error reading body from request:", error);
- }
-
- console.error(errorText);
-}
diff --git a/src/server.ts b/src/server.ts
deleted file mode 100644
index a2618fe..0000000
--- a/src/server.ts
+++ /dev/null
@@ -1,741 +0,0 @@
-import { AutoRouter } from "itty-router";
-import { InteractionResponseType, InteractionType } from "discord-interactions";
-import {
- HOT_COMMAND,
- ROLEPLAY_COMMAND,
- NSFW_COMMAND,
- TOP_COMMAND,
- COMPLAIN_COMMAND,
- APPEAL_COMMAND,
- COLOURS_COMMAND,
- ROLEPLAY_SERIOUS_COMMAND,
-} from "./discord/commands.ts";
-import {
- getCutePost,
- getRoleplayPost,
- getNSFWPost,
- getTopPost,
-} from "./reddit.ts";
-import type { TimePeriod } from "./discord/types.ts";
-import type { Environment, DiscordEmbed } from "./discord/interfaces.ts";
-import {
- createPostEmbed,
- createComplaintEmbed,
- createAppealEmbed,
- createRoleDistributionEmbed,
-} from "./discord/embeds.ts";
-import { JSONResponse } from "./discord/responses.ts";
-import { verifyDiscordRequest } from "./discord/verification.ts";
-
-const router = AutoRouter();
-const COMPLAINT_CHANNEL_ID = "1415868433714778204";
-const APPEAL_CHANNEL_ID = "1420340807931531385";
-const SERIOUS_ROLEPLAY_ROLE_ID = "1418311833303122021";
-const ROLE_MANAGER_ROLE_ID = "1410993207608873070";
-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
- "1415520343690575883", // Aston Machan Sienna
- "1415539100315942962", // Super Creek Baby Blue
- "1415539544232824913", // Sakura Bakushin O Lilac
- "1415567915578818723", // El Condor Pasa Biscotti
- "1415592658906124338", // Still in Love Crimson
- "1415593126273224795", // Mayano Top Gun Navy Blue
- "1415797242845200475", // Mr. C.B. Forest Green
- "1416583306698297354", // Seuin Sky Mint
- "1416583690217328660", // Neo Universe Pastel Yellow
- "1416595046249267364", // Manhattan Cafe Jet Black
-];
-
-const sendComplaintToChannel = async (
- environment: Environment,
- embed: DiscordEmbed,
-): Promise<boolean> => {
- const url = `https://discord.com/api/v10/channels/${COMPLAINT_CHANNEL_ID}/messages`;
-
- try {
- const response = await fetch(url, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bot ${environment.DISCORD_TOKEN}`,
- },
- body: JSON.stringify({
- embeds: [embed],
- }),
- });
-
- return response.ok;
- } catch (error) {
- console.error("Error sending complaint to channel:", error);
-
- return false;
- }
-};
-
-const sendAppealToChannel = async (
- environment: Environment,
- embed: DiscordEmbed,
-): Promise<boolean> => {
- const url = `https://discord.com/api/v10/channels/${APPEAL_CHANNEL_ID}/messages`;
-
- try {
- const response = await fetch(url, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bot ${environment.DISCORD_TOKEN}`,
- },
- body: JSON.stringify({
- embeds: [embed],
- }),
- });
-
- return response.ok;
- } catch (error) {
- console.error("Error sending appeal to channel:", error);
-
- return false;
- }
-};
-
-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}`);
-});
-
-router.post("/", async (request: Request, environment: Environment) => {
- const { isValid, interaction } = await server.verifyDiscordRequest(
- request,
- environment,
- );
-
- if (!isValid || !interaction)
- return new Response("Bad request signature.", { status: 401 });
-
- if (interaction.type === InteractionType.PING)
- return new JSONResponse({
- type: InteractionResponseType.PONG,
- });
-
- if (interaction.type === InteractionType.APPLICATION_COMMAND) {
- switch (interaction.data.name.toLowerCase()) {
- case HOT_COMMAND.name.toLowerCase(): {
- try {
- const post = await getCutePost();
- const embed = createPostEmbed(post);
-
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- embeds: [embed],
- },
- });
- } catch (error) {
- console.error("Error in hot command:", error);
-
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content: "āŒ No posts found. Try again later!",
- flags: 64,
- },
- });
- }
- }
-
- case ROLEPLAY_COMMAND.name.toLowerCase(): {
- try {
- const post = await getRoleplayPost();
- const embed = createPostEmbed(post);
-
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- embeds: [embed],
- },
- });
- } catch (error) {
- console.error("Error in roleplay command:", error);
-
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content: "āŒ No roleplay posts found. Try again later!",
- flags: 64,
- },
- });
- }
- }
-
- case NSFW_COMMAND.name.toLowerCase(): {
- if (!interaction.channel_id || !interaction.channel?.nsfw) {
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content: "āŒ This command can only be used in NSFW channels.",
- flags: 64,
- },
- });
- }
-
- try {
- const post = await getNSFWPost();
- const embed = createPostEmbed(post);
-
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- embeds: [embed],
- },
- });
- } catch (error) {
- console.error("Error in NSFW command:", error);
-
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content: "āŒ No NSFW posts found. Try again later!",
- flags: 64,
- },
- });
- }
- }
-
- case TOP_COMMAND.name.toLowerCase(): {
- try {
- const time =
- (interaction.data.options?.[0]?.value as TimePeriod) || "day";
- const post = await getTopPost(time);
- const embed = createPostEmbed(post);
-
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- embeds: [embed],
- },
- });
- } catch (error) {
- console.error("Error in top command:", error);
-
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content: "āŒ No top posts found. Try again later!",
- flags: 64,
- },
- });
- }
- }
-
- case COMPLAIN_COMMAND.name.toLowerCase(): {
- try {
- const complaintMessage = interaction.data.options?.[0]
- ?.value as string;
-
- if (!complaintMessage)
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content: "āŒ Please provide a message for your complaint.",
- flags: 64,
- },
- });
-
- const complainant = {
- username:
- interaction.member?.user?.username ||
- interaction.user?.username ||
- "Unknown",
- id:
- interaction.member?.user?.id || interaction.user?.id || "Unknown",
- avatar:
- interaction.member?.user?.avatar || interaction.user?.avatar,
- };
- const isDM = !interaction.guild_id;
- const complaintEmbed = createComplaintEmbed(
- complaintMessage,
- complainant,
- Date.now(),
- isDM,
- );
- const success = await sendComplaintToChannel(
- environment,
- complaintEmbed,
- );
-
- if (success) {
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content: "āœ… Your complaint has been submitted successfully!",
- flags: 64,
- },
- });
- } else {
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content:
- "āŒ Failed to submit your complaint. Please try again later.",
- flags: 64,
- },
- });
- }
- } catch (error) {
- console.error("Error in complain command:", error);
-
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content: "āŒ An error occurred while processing your complaint.",
- flags: 64,
- },
- });
- }
- }
-
- case APPEAL_COMMAND.name.toLowerCase(): {
- try {
- const appealMessage = interaction.data.options?.[0]?.value as string;
-
- if (!appealMessage)
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content: "āŒ Please provide a message for your appeal.",
- flags: 64,
- },
- });
-
- const appellant = {
- username:
- interaction.member?.user?.username ||
- interaction.user?.username ||
- "Unknown",
- id:
- interaction.member?.user?.id || interaction.user?.id || "Unknown",
- avatar:
- interaction.member?.user?.avatar || interaction.user?.avatar,
- };
- const isDM = !interaction.guild_id;
- const appealEmbed = createAppealEmbed(
- appealMessage,
- appellant,
- Date.now(),
- isDM,
- );
- const success = await sendAppealToChannel(environment, appealEmbed);
-
- if (success) {
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content:
- "āœ… Your appeal has been submitted successfully! A moderator will follow up with you soon.",
- flags: 64,
- },
- });
- } else {
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content:
- "āŒ Failed to submit your appeal. Please try again later.",
- flags: 64,
- },
- });
- }
- } catch (error) {
- console.error("Error in appeal command:", error);
-
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content: "āŒ An error occurred while processing your appeal.",
- flags: 64,
- },
- });
- }
- }
-
- 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 embed = createRoleDistributionEmbed(roleDistribution);
-
- 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,
- },
- });
- }
- }
-
- case ROLEPLAY_SERIOUS_COMMAND.name.toLowerCase(): {
- try {
- const member = interaction.member;
- const hasAdminPermission =
- member?.permissions && (parseInt(member.permissions) & 0x8) === 0x8;
- const hasManagerRole = member?.roles?.includes(ROLE_MANAGER_ROLE_ID);
-
- if (!hasAdminPermission && !hasManagerRole)
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content:
- "āŒ You don't have permission to use this command. Only administrators and role managers can use this command.",
- flags: 64,
- },
- });
-
- const action = interaction.data.options?.[0]?.value as string;
- const targetUserID = interaction.data.options?.[1]?.value as string;
-
- if (!action || !targetUserID)
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content:
- "āŒ Missing required parameters. Please provide both action and user.",
- flags: 64,
- },
- });
-
- const guild = await fetch(
- `https://discord.com/api/v10/guilds/${GUILD_ID}/members/${targetUserID}`,
- {
- headers: {
- Authorization: `Bot ${environment.DISCORD_TOKEN}`,
- },
- },
- );
-
- if (!guild.ok)
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content:
- "āŒ Unable to fetch user information. The user may not be in this server.",
- flags: 64,
- },
- });
-
- const targetMember = await guild.json();
- const currentRoles = targetMember.roles || [];
- const hasRole = currentRoles.includes(SERIOUS_ROLEPLAY_ROLE_ID);
- let newRoles = [...currentRoles];
- let actionTaken = "";
-
- switch (action) {
- case "add":
- if (hasRole)
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content:
- "āŒ The user already has the serious roleplay role.",
- flags: 64,
- },
- });
-
- newRoles.push(SERIOUS_ROLEPLAY_ROLE_ID);
-
- actionTaken = "added";
-
- break;
-
- case "remove":
- if (!hasRole)
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content:
- "āŒ The user doesn't have the serious roleplay role.",
- flags: 64,
- },
- });
-
- newRoles = newRoles.filter(
- (roleId) => roleId !== SERIOUS_ROLEPLAY_ROLE_ID,
- );
- actionTaken = "removed";
-
- break;
-
- case "toggle":
- if (hasRole) {
- newRoles = newRoles.filter(
- (roleId) => roleId !== SERIOUS_ROLEPLAY_ROLE_ID,
- );
- actionTaken = "removed";
- } else {
- newRoles.push(SERIOUS_ROLEPLAY_ROLE_ID);
-
- actionTaken = "added";
- }
-
- break;
-
- default:
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content:
- "āŒ Invalid action. Please use 'add', 'remove', or 'toggle'.",
- flags: 64,
- },
- });
- }
-
- const updateResponse = await fetch(
- `https://discord.com/api/v10/guilds/${GUILD_ID}/members/${targetUserID}`,
- {
- method: "PATCH",
- headers: {
- Authorization: `Bot ${environment.DISCORD_TOKEN}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- roles: newRoles,
- }),
- },
- );
-
- if (!updateResponse.ok) {
- console.error(
- "Failed to update user roles:",
- await updateResponse.text(),
- );
-
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content:
- "āŒ Failed to update user roles. The bot may not have sufficient permissions.",
- flags: 64,
- },
- });
- }
-
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content: `āœ… Successfully ${actionTaken} the serious roleplay role for <@${targetUserID}>.`,
- },
- });
- } catch (error) {
- console.error("Error in roleplay-serious command:", error);
-
- return new JSONResponse({
- type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
- data: {
- content: "āŒ An error occurred while managing the role.",
- flags: 64,
- },
- });
- }
- }
-
- default:
- return new JSONResponse({ error: "Unknown Type" }, { status: 400 });
- }
- }
-
- console.error("Unknown Type");
-
- return new JSONResponse({ error: "Unknown Type" }, { status: 400 });
-});
-
-router.all("*", () => new Response("Not Found.", { status: 404 }));
-
-const server = {
- verifyDiscordRequest,
- fetch: router.fetch,
-};
-
-export default server;