summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFuwn <[email protected]>2025-09-07 02:28:34 -0700
committerFuwn <[email protected]>2025-09-07 02:28:34 -0700
commit188c714f43635fb57eac70b167dba682d6b93a2f (patch)
tree28a5bc64a6a8efd78c19cdaa666b98e42d3b90b5 /src
parentfeat: Add top command (diff)
downloadumabotdiscord-188c714f43635fb57eac70b167dba682d6b93a2f.tar.xz
umabotdiscord-188c714f43635fb57eac70b167dba682d6b93a2f.zip
build: Switch to TypeScript
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 = {