summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2025-09-09 18:05:15 -0700
committerFuwn <[email protected]>2025-09-09 18:05:15 -0700
commit9678e4e1e87a5d73c47683fe85848888ca8e857b (patch)
tree42235ab613eba920ef46ceaba946f2fdc6362427
parentfix: Properly handle videos (diff)
downloadumabotdiscord-9678e4e1e87a5d73c47683fe85848888ca8e857b.tar.xz
umabotdiscord-9678e4e1e87a5d73c47683fe85848888ca8e857b.zip
refactor: Move Discord APIs to Discord module
-rw-r--r--src/discord/commands.ts (renamed from src/commands.ts)21
-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
-rw-r--r--src/reddit.ts2
-rw-r--r--src/register.ts2
-rw-r--r--src/server.ts190
9 files changed, 210 insertions, 204 deletions
diff --git a/src/commands.ts b/src/discord/commands.ts
index 56d4321..dec18e6 100644
--- a/src/commands.ts
+++ b/src/discord/commands.ts
@@ -1,23 +1,6 @@
-export interface DiscordCommand {
- name: string;
- description: string;
- options?: DiscordCommandOption[];
-}
+import type { DiscordCommand } from './interfaces.ts';
-export interface DiscordCommandOption {
- type: number;
- name: string;
- description: string;
- required?: boolean;
- choices?: DiscordCommandChoice[];
-}
-
-export interface DiscordCommandChoice {
- name: string;
- value: string;
-}
-
-export type TimePeriod = 'hour' | 'day' | 'week' | 'month' | 'year' | 'all';
+export type { DiscordCommand };
export const HOT_COMMAND: DiscordCommand = {
name: 'hot',
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(/&amp;/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 };
+}
diff --git a/src/reddit.ts b/src/reddit.ts
index 6fe576e..0c12739 100644
--- a/src/reddit.ts
+++ b/src/reddit.ts
@@ -1,4 +1,4 @@
-import type { TimePeriod } from './commands.ts';
+import type { TimePeriod } from './discord/types.ts';
export interface RedditPost {
id: string;
diff --git a/src/register.ts b/src/register.ts
index 94bb420..319b054 100644
--- a/src/register.ts
+++ b/src/register.ts
@@ -4,7 +4,7 @@ import {
ROLEPLAY_COMMAND,
TOP_COMMAND,
type DiscordCommand,
-} from './commands.ts';
+} from './discord/commands.ts';
import dotenv from 'dotenv';
import process from 'node:process';
diff --git a/src/server.ts b/src/server.ts
index baeb607..5f43763 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -1,179 +1,25 @@
import { AutoRouter } from 'itty-router';
-import {
- InteractionResponseType,
- InteractionType,
- verifyKey,
-} from 'discord-interactions';
+import { InteractionResponseType, InteractionType } from 'discord-interactions';
import {
HOT_COMMAND,
ROLEPLAY_COMMAND,
NSFW_COMMAND,
TOP_COMMAND,
-} from './commands.ts';
+} from './discord/commands.ts';
import {
getCutePost,
getRoleplayPost,
getNSFWPost,
getTopPost,
- type RedditPost,
} from './reddit.ts';
-import type { TimePeriod } from './commands.ts';
-
-interface Environment {
- DISCORD_APPLICATION_ID: string;
- DISCORD_PUBLIC_KEY: string;
- DISCORD_TOKEN: string;
-}
-
-interface DiscordInteraction {
- type: number;
- data: {
- name: string;
- options?: Array<{
- name: string;
- value: string;
- }>;
- };
- channel_id?: string;
- channel?: {
- nsfw: boolean;
- };
-}
-
-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 };
-}
-
-interface DiscordResponse {
- type: number;
- data?: {
- content?: string;
- embeds?: DiscordEmbed[];
- flags?: number;
- };
-}
-
-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);
- }
-}
+import type { TimePeriod } from './discord/types.ts';
+import type { Environment } from './discord/interfaces.ts';
+import { createPostEmbed } from './discord/embeds.ts';
+import { JSONResponse } from './discord/responses.ts';
+import { verifyDiscordRequest } from './discord/verification.ts';
const router = AutoRouter();
-function 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, '=');
-}
-
-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;
-}
-
router.get('/', (_request: Request, environment: Environment) => {
return new Response(`šŸ‘‹ ${environment.DISCORD_APPLICATION_ID}`);
});
@@ -314,28 +160,6 @@ router.post('/', async (request: Request, environment: Environment) => {
router.all('*', () => new Response('Not Found.', { status: 404 }));
-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 };
-}
-
const server = {
verifyDiscordRequest,
fetch: router.fetch,