summaryrefslogtreecommitdiff
path: root/packages/interactions/discord
diff options
context:
space:
mode:
Diffstat (limited to 'packages/interactions/discord')
-rw-r--r--packages/interactions/discord/commands.ts125
-rw-r--r--packages/interactions/discord/embeds.ts181
-rw-r--r--packages/interactions/discord/interfaces.ts86
-rw-r--r--packages/interactions/discord/responses.ts15
-rw-r--r--packages/interactions/discord/types.ts1
-rw-r--r--packages/interactions/discord/verification.ts24
6 files changed, 432 insertions, 0 deletions
diff --git a/packages/interactions/discord/commands.ts b/packages/interactions/discord/commands.ts
new file mode 100644
index 0000000..601591b
--- /dev/null
+++ b/packages/interactions/discord/commands.ts
@@ -0,0 +1,125 @@
+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/packages/interactions/discord/embeds.ts b/packages/interactions/discord/embeds.ts
new file mode 100644
index 0000000..3f7c344
--- /dev/null
+++ b/packages/interactions/discord/embeds.ts
@@ -0,0 +1,181 @@
+import type { DiscordEmbed } from "./interfaces.ts";
+import type { RedditPost } from "../reddit.ts";
+
+const decodeHtmlEntities = (str: string): string => {
+ return str
+ .replace(/&/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/packages/interactions/discord/interfaces.ts b/packages/interactions/discord/interfaces.ts
new file mode 100644
index 0000000..bc8683c
--- /dev/null
+++ b/packages/interactions/discord/interfaces.ts
@@ -0,0 +1,86 @@
+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/packages/interactions/discord/responses.ts b/packages/interactions/discord/responses.ts
new file mode 100644
index 0000000..4dcc777
--- /dev/null
+++ b/packages/interactions/discord/responses.ts
@@ -0,0 +1,15 @@
+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/packages/interactions/discord/types.ts b/packages/interactions/discord/types.ts
new file mode 100644
index 0000000..4f6e85e
--- /dev/null
+++ b/packages/interactions/discord/types.ts
@@ -0,0 +1 @@
+export type TimePeriod = "hour" | "day" | "week" | "month" | "year" | "all";
diff --git a/packages/interactions/discord/verification.ts b/packages/interactions/discord/verification.ts
new file mode 100644
index 0000000..89d26db
--- /dev/null
+++ b/packages/interactions/discord/verification.ts
@@ -0,0 +1,24 @@
+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 };
+};