summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFuwn <[email protected]>2025-09-06 16:51:26 -0700
committerFuwn <[email protected]>2025-09-06 16:51:26 -0700
commitb625aff7160c593646efaf080163f96f69aa6391 (patch)
tree163d5096e3145bcb0b0bf8feba5ab35ef12c9f62 /src
downloadumabotdiscord-b625aff7160c593646efaf080163f96f69aa6391.tar.xz
umabotdiscord-b625aff7160c593646efaf080163f96f69aa6391.zip
feat: Initial commit
Diffstat (limited to 'src')
-rw-r--r--src/commands.js9
-rw-r--r--src/reddit.js77
-rw-r--r--src/register.js49
-rw-r--r--src/server.js152
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;