diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/commands.ts (renamed from src/commands.js) | 29 | ||||
| -rw-r--r-- | src/reddit.ts (renamed from src/reddit.js) | 62 | ||||
| -rw-r--r-- | src/register.ts (renamed from src/register.js) | 15 | ||||
| -rw-r--r-- | src/server.ts (renamed from src/server.js) | 83 |
4 files changed, 160 insertions, 29 deletions
diff --git a/src/commands.js b/src/commands.ts index c2faac5..56d4321 100644 --- a/src/commands.js +++ b/src/commands.ts @@ -1,20 +1,41 @@ -export const HOT_COMMAND = { +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; +} + +export type TimePeriod = 'hour' | 'day' | 'week' | 'month' | 'year' | 'all'; + +export const HOT_COMMAND: DiscordCommand = { name: 'hot', description: 'Fetch a random hot post from r/okbuddyumamusume', }; -export const ROLEPLAY_COMMAND = { +export const ROLEPLAY_COMMAND: DiscordCommand = { name: 'roleplay', description: 'Fetch a random hot roleplay post from r/okbuddyumamusume', }; -export const NSFW_COMMAND = { +export const NSFW_COMMAND: DiscordCommand = { name: 'nsfw', description: 'Fetch a random NSFW post from r/okbuddyumamusume (NSFW channels only)', }; -export const TOP_COMMAND = { +export const TOP_COMMAND: DiscordCommand = { name: 'top', description: 'Fetch a random top post from r/okbuddyumamusume (defaults to today)', diff --git a/src/reddit.js b/src/reddit.ts index 543e6ff..79475b1 100644 --- a/src/reddit.js +++ b/src/reddit.ts @@ -1,4 +1,44 @@ -async function fetchRedditPosts(sort = 'hot', time = 'day') { +import type { TimePeriod } from './commands.js'; + +export interface RedditPost { + id: string; + title: string; + author: string; + score: number; + num_comments: number; + created_utc: number; + permalink: string; + url: string; + selftext: string; + is_gallery?: boolean; + over_18: boolean; + link_flair_text?: string; + media?: { + reddit_video?: { + fallback_url: string; + }; + }; + secure_media?: { + reddit_video?: { + fallback_url: string; + }; + }; +} + +export interface RedditResponse { + data: { + children: Array<{ + data: RedditPost; + }>; + }; +} + +type SortType = 'hot' | 'top'; + +async function fetchRedditPosts( + sort: SortType = 'hot', + time: TimePeriod = 'day', +): Promise<RedditPost[]> { const url = `https://www.reddit.com/r/okbuddyumamusume/${sort}.json${sort === 'top' ? `?t=${time}` : ''}`; const response = await fetch(url, { headers: { @@ -20,12 +60,16 @@ async function fetchRedditPosts(sort = 'hot', time = 'day') { throw new Error(errorText); } - const data = await response.json(); + const data: RedditResponse = await response.json(); return data.data.children.map((post) => post.data); } -function filterPostsByFlair(posts, excludedFlairs = [], includedFlairs = []) { +function filterPostsByFlair( + posts: RedditPost[], + excludedFlairs: string[] = [], + includedFlairs: string[] = [], +): RedditPost[] { return posts.filter((post) => { if (post.is_gallery) return false; @@ -62,7 +106,7 @@ function filterPostsByFlair(posts, excludedFlairs = [], includedFlairs = []) { }); } -function getRandomPost(posts) { +function getRandomPost(posts: RedditPost[]): RedditPost { if (posts.length === 0) throw new Error('No posts found matching the criteria'); @@ -71,28 +115,30 @@ function getRandomPost(posts) { return posts[randomIndex]; } -export async function getCutePost() { +export async function getCutePost(): Promise<RedditPost> { const posts = await fetchRedditPosts('hot'); const filteredPosts = filterPostsByFlair(posts, ['roleplay', 'announcement']); return getRandomPost(filteredPosts); } -export async function getRoleplayPost() { +export async function getRoleplayPost(): Promise<RedditPost> { const posts = await fetchRedditPosts('hot'); const filteredPosts = filterPostsByFlair(posts, [], ['roleplay']); return getRandomPost(filteredPosts); } -export async function getNSFWPost() { +export async function getNSFWPost(): Promise<RedditPost> { const posts = await fetchRedditPosts('hot'); const filteredPosts = filterPostsByFlair(posts, [], ['nsfw']); return getRandomPost(filteredPosts); } -export async function getTopPost(time = 'day') { +export async function getTopPost( + time: TimePeriod = 'day', +): Promise<RedditPost> { const posts = await fetchRedditPosts('top', time); const filteredPosts = filterPostsByFlair(posts, ['roleplay', 'announcement']); diff --git a/src/register.js b/src/register.ts index 5b374d1..632b2b8 100644 --- a/src/register.js +++ b/src/register.ts @@ -3,6 +3,7 @@ import { NSFW_COMMAND, ROLEPLAY_COMMAND, TOP_COMMAND, + type DiscordCommand, } from './commands.js'; import dotenv from 'dotenv'; import process from 'node:process'; @@ -22,18 +23,20 @@ if (!applicationID) const url = `https://discord.com/api/v10/applications/${applicationID}/commands`; +const commands: DiscordCommand[] = [ + HOT_COMMAND, + ROLEPLAY_COMMAND, + NSFW_COMMAND, + TOP_COMMAND, +]; + const response = await fetch(url, { headers: { 'Content-Type': 'application/json', Authorization: `Bot ${token}`, }, method: 'PUT', - body: JSON.stringify([ - HOT_COMMAND, - ROLEPLAY_COMMAND, - NSFW_COMMAND, - TOP_COMMAND, - ]), + body: JSON.stringify(commands), }); if (response.ok) { diff --git a/src/server.js b/src/server.ts index 22b754c..3f54f63 100644 --- a/src/server.js +++ b/src/server.ts @@ -15,10 +15,64 @@ import { 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, init) { + constructor(body: DiscordResponse | { error: string }, init?: ResponseInit) { const jsonBody = JSON.stringify(body); init = init || { @@ -33,7 +87,7 @@ class JSONResponse extends Response { const router = AutoRouter(); -function createPostEmbed(post) { +function createPostEmbed(post: RedditPost): DiscordEmbed { const mediaUrl = post.media?.reddit_video?.fallback_url || post.secure_media?.reddit_video?.fallback_url || @@ -44,7 +98,7 @@ function createPostEmbed(post) { if (description.length > 1000) description = description.substring(0, 997).trim() + ' ...'; - const embed = { + const embed: DiscordEmbed = { title: post.title, description: description, url: `https://reddit.com${post.permalink}`, @@ -71,19 +125,22 @@ function createPostEmbed(post) { }, }; - if (mediaUrl) - if (post.media?.reddit_video || post.secure_media?.reddit_video) + if (mediaUrl) { + if (post.media?.reddit_video || post.secure_media?.reddit_video) { embed.video = { url: mediaUrl }; - else embed.image = { url: mediaUrl }; + } else { + embed.image = { url: mediaUrl }; + } + } return embed; } -router.get('/', (_request, environment) => { +router.get('/', (_request: Request, environment: Environment) => { return new Response(`👋 ${environment.DISCORD_APPLICATION_ID}`); }); -router.post('/', async (request, environment) => { +router.post('/', async (request: Request, environment: Environment) => { const { isValid, interaction } = await server.verifyDiscordRequest( request, environment, @@ -177,7 +234,8 @@ router.post('/', async (request, environment) => { case TOP_COMMAND.name.toLowerCase(): { try { - const time = interaction.data.options?.[0]?.value || 'day'; + const time = + (interaction.data.options?.[0]?.value as TimePeriod) || 'day'; const post = await getTopPost(time); const embed = createPostEmbed(post); @@ -210,7 +268,10 @@ router.post('/', async (request, environment) => { router.all('*', () => new Response('Not Found.', { status: 404 })); -async function verifyDiscordRequest(request, environment) { +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(); @@ -226,7 +287,7 @@ async function verifyDiscordRequest(request, environment) { if (!isValidRequest) return { isValid: false }; - return { interaction: JSON.parse(body), isValid: true }; + return { interaction: JSON.parse(body) as DiscordInteraction, isValid: true }; } const server = { |