summaryrefslogtreecommitdiff
path: root/src/discord
diff options
context:
space:
mode:
Diffstat (limited to 'src/discord')
-rw-r--r--src/discord/commands.ts59
-rw-r--r--src/discord/embeds.ts90
-rw-r--r--src/discord/interfaces.ts69
-rw-r--r--src/discord/responses.ts15
-rw-r--r--src/discord/types.ts1
-rw-r--r--src/discord/verification.ts24
6 files changed, 258 insertions, 0 deletions
diff --git a/src/discord/commands.ts b/src/discord/commands.ts
new file mode 100644
index 0000000..dec18e6
--- /dev/null
+++ b/src/discord/commands.ts
@@ -0,0 +1,59 @@
+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',
+ },
+ ],
+ },
+ ],
+};
diff --git a/src/discord/embeds.ts b/src/discord/embeds.ts
new file mode 100644
index 0000000..f88cec2
--- /dev/null
+++ b/src/discord/embeds.ts
@@ -0,0 +1,90 @@
+import type { DiscordEmbed } from './interfaces.ts';
+import type { RedditPost } from '../reddit.ts';
+
+function 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 function 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;
+}
diff --git a/src/discord/interfaces.ts b/src/discord/interfaces.ts
new file mode 100644
index 0000000..3eb81eb
--- /dev/null
+++ b/src/discord/interfaces.ts
@@ -0,0 +1,69 @@
+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;
+ };
+}
+
+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 };
+}
+
+export interface DiscordResponse {
+ type: number;
+ data?: {
+ content?: string;
+ embeds?: DiscordEmbed[];
+ flags?: number;
+ };
+}
+
+export interface DiscordCommand {
+ name: string;
+ description: string;
+ options?: DiscordCommandOption[];
+}
+
+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
new file mode 100644
index 0000000..da72967
--- /dev/null
+++ b/src/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/src/discord/types.ts b/src/discord/types.ts
new file mode 100644
index 0000000..9b1d6c5
--- /dev/null
+++ b/src/discord/types.ts
@@ -0,0 +1 @@
+export type TimePeriod = 'hour' | 'day' | 'week' | 'month' | 'year' | 'all';
diff --git a/src/discord/verification.ts b/src/discord/verification.ts
new file mode 100644
index 0000000..c60c70e
--- /dev/null
+++ b/src/discord/verification.ts
@@ -0,0 +1,24 @@
+import { verifyKey } from 'discord-interactions';
+import type { Environment, DiscordInteraction } from './interfaces.ts';
+
+export async function verifyDiscordRequest(
+ 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 };
+}