diff options
| author | Fuwn <[email protected]> | 2025-09-09 18:05:15 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-09-09 18:05:15 -0700 |
| commit | 9678e4e1e87a5d73c47683fe85848888ca8e857b (patch) | |
| tree | 42235ab613eba920ef46ceaba946f2fdc6362427 /src/discord | |
| parent | fix: Properly handle videos (diff) | |
| download | umabotdiscord-9678e4e1e87a5d73c47683fe85848888ca8e857b.tar.xz umabotdiscord-9678e4e1e87a5d73c47683fe85848888ca8e857b.zip | |
refactor: Move Discord APIs to Discord module
Diffstat (limited to 'src/discord')
| -rw-r--r-- | src/discord/commands.ts | 59 | ||||
| -rw-r--r-- | src/discord/embeds.ts | 90 | ||||
| -rw-r--r-- | src/discord/interfaces.ts | 69 | ||||
| -rw-r--r-- | src/discord/responses.ts | 15 | ||||
| -rw-r--r-- | src/discord/types.ts | 1 | ||||
| -rw-r--r-- | src/discord/verification.ts | 24 |
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(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(///g, '/') + .replace(/`/g, '`') + .replace(/=/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 }; +} |