diff options
Diffstat (limited to 'src/server.ts')
| -rw-r--r-- | src/server.ts | 741 |
1 files changed, 0 insertions, 741 deletions
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; |