diff options
| author | Fuwn <[email protected]> | 2025-09-06 16:51:26 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-09-06 16:51:26 -0700 |
| commit | b625aff7160c593646efaf080163f96f69aa6391 (patch) | |
| tree | 163d5096e3145bcb0b0bf8feba5ab35ef12c9f62 /src | |
| download | umabotdiscord-b625aff7160c593646efaf080163f96f69aa6391.tar.xz umabotdiscord-b625aff7160c593646efaf080163f96f69aa6391.zip | |
feat: Initial commit
Diffstat (limited to 'src')
| -rw-r--r-- | src/commands.js | 9 | ||||
| -rw-r--r-- | src/reddit.js | 77 | ||||
| -rw-r--r-- | src/register.js | 49 | ||||
| -rw-r--r-- | src/server.js | 152 |
4 files changed, 287 insertions, 0 deletions
diff --git a/src/commands.js b/src/commands.js new file mode 100644 index 0000000..5445644 --- /dev/null +++ b/src/commands.js @@ -0,0 +1,9 @@ +export const HOT_COMMAND = { + name: 'hot', + description: 'Fetch a random hot post from r/okbuddyumamusume', +}; + +export const ROLEPLAY_COMMAND = { + name: 'roleplay', + description: 'Fetch a random hot roleplay post from r/okbuddyumamusume', +}; diff --git a/src/reddit.js b/src/reddit.js new file mode 100644 index 0000000..36cb154 --- /dev/null +++ b/src/reddit.js @@ -0,0 +1,77 @@ +async function fetchRedditPosts() { + const response = await fetch(redditURL, { + headers: { + 'User-Agent': 'UmaBot/0.1.0', + }, + }); + + if (!response.ok) { + let errorText = `Error fetching ${response.url}: ${response.status} ${response.statusText}`; + + try { + const error = await response.text(); + + if (error) errorText = `${errorText} \n\n ${error}`; + } catch { + // + } + + throw new Error(errorText); + } + + const data = await response.json(); + + return data.data.children.map((post) => post.data); +} + +function filterPostsByFlair(posts, excludedFlairs = [], includedFlairs = []) { + return posts.filter((post) => { + if (post.is_gallery) return false; + + const hasMedia = + post.media?.reddit_video?.fallback_url || + post.secure_media?.reddit_video?.fallback_url || + post.url; + + if (!hasMedia) return false; + + if (post.over_18 || post.link_flair_text?.toLowerCase().includes('nsfw')) + return false; + + const postFlair = post.link_flair_text?.toLowerCase() || ''; + + if (includedFlairs.length > 0) + return includedFlairs.some((flair) => + postFlair.includes(flair.toLowerCase()), + ); + + if (excludedFlairs.length > 0) + return !excludedFlairs.some((flair) => + postFlair.includes(flair.toLowerCase()), + ); + + return true; + }); +} + +function getRandomPost(posts) { + const randomIndex = Math.floor(Math.random() * posts.length); + + return posts[randomIndex]; +} + +export async function getCutePost() { + const posts = await fetchRedditPosts(); + const filteredPosts = filterPostsByFlair(posts, ['roleplay', 'announcement']); + + return getRandomPost(filteredPosts); +} + +export async function getRoleplayPost() { + const posts = await fetchRedditPosts(); + const filteredPosts = filterPostsByFlair(posts, [], ['roleplay']); + + return getRandomPost(filteredPosts); +} + +export const redditURL = 'https://www.reddit.com/r/okbuddyumamusume/hot.json'; diff --git a/src/register.js b/src/register.js new file mode 100644 index 0000000..a9a5deb --- /dev/null +++ b/src/register.js @@ -0,0 +1,49 @@ +import { HOT_COMMAND, ROLEPLAY_COMMAND } from './commands.js'; +import dotenv from 'dotenv'; +import process from 'node:process'; + +dotenv.config({ path: '.dev.vars' }); + +const token = process.env.DISCORD_TOKEN; +const applicationID = process.env.DISCORD_APPLICATION_ID; + +if (!token) + throw new Error('The DISCORD_TOKEN environment variable is required.'); + +if (!applicationID) + throw new Error( + 'The DISCORD_APPLICATION_ID environment variable is required.', + ); + +const url = `https://discord.com/api/v10/applications/${applicationID}/commands`; + +const response = await fetch(url, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bot ${token}`, + }, + method: 'PUT', + body: JSON.stringify([HOT_COMMAND, ROLEPLAY_COMMAND]), +}); + +if (response.ok) { + console.log('Registered all commands'); + + const data = await response.json(); + + console.log(JSON.stringify(data, null, 2)); +} else { + console.error('Error registering commands'); + + let errorText = `Error registering commands \n ${response.url}: ${response.status} ${response.statusText}`; + + try { + const error = await response.text(); + + if (error) errorText = `${errorText} \n\n ${error}`; + } catch (error) { + console.error('Error reading body from request:', error); + } + + console.error(errorText); +} diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..b505f31 --- /dev/null +++ b/src/server.js @@ -0,0 +1,152 @@ +import { AutoRouter } from 'itty-router'; +import { + InteractionResponseType, + InteractionType, + verifyKey, +} from 'discord-interactions'; +import { HOT_COMMAND, ROLEPLAY_COMMAND } from './commands.js'; +import { getCutePost, getRoleplayPost } from './reddit.js'; + +class JSONResponse extends Response { + constructor(body, init) { + const jsonBody = JSON.stringify(body); + + init = init || { + headers: { + 'content-type': 'application/json;charset=UTF-8', + }, + }; + + super(jsonBody, init); + } +} + +const router = AutoRouter(); + +function createPostEmbed(post) { + 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 = { + 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, environment) => { + return new Response(`👋 ${environment.DISCORD_APPLICATION_ID}`); +}); + +router.post('/', async (request, 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(): { + const post = await getCutePost(); + const embed = createPostEmbed(post); + + return new JSONResponse({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + embeds: [embed], + }, + }); + } + + case ROLEPLAY_COMMAND.name.toLowerCase(): { + const post = await getRoleplayPost(); + const embed = createPostEmbed(post); + + return new JSONResponse({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + embeds: [embed], + }, + }); + } + + 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, environment) { + 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), isValid: true }; +} + +const server = { + verifyDiscordRequest, + fetch: router.fetch, +}; + +export default server; |