diff options
| author | Fuwn <[email protected]> | 2025-09-07 02:28:34 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-09-07 02:28:34 -0700 |
| commit | 188c714f43635fb57eac70b167dba682d6b93a2f (patch) | |
| tree | 28a5bc64a6a8efd78c19cdaa666b98e42d3b90b5 /src/server.ts | |
| parent | feat: Add top command (diff) | |
| download | umabotdiscord-188c714f43635fb57eac70b167dba682d6b93a2f.tar.xz umabotdiscord-188c714f43635fb57eac70b167dba682d6b93a2f.zip | |
build: Switch to TypeScript
Diffstat (limited to 'src/server.ts')
| -rw-r--r-- | src/server.ts | 298 |
1 files changed, 298 insertions, 0 deletions
diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..3f54f63 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,298 @@ +import { AutoRouter } from 'itty-router'; +import { + InteractionResponseType, + InteractionType, + verifyKey, +} from 'discord-interactions'; +import { + HOT_COMMAND, + ROLEPLAY_COMMAND, + NSFW_COMMAND, + TOP_COMMAND, +} from './commands.js'; +import { + getCutePost, + getRoleplayPost, + getNSFWPost, + getTopPost, + type RedditPost, +} from './reddit.js'; +import type { TimePeriod } from './commands.js'; + +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; + }; + video?: { url: 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); + } +} + +const router = AutoRouter(); + +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) { + embed.video = { url: mediaUrl }; + } else { + embed.image = { url: mediaUrl }; + } + } + + return embed; +} + +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 { + 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 { + 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 { + 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 { + return new JSONResponse({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: '❌ No top posts found. Try again later!', + 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 })); + +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, +}; + +export default server; |