feat: initial project prototype
Node.js CI / Lint and Test (push) Successful in 1m27s

This commit is contained in:
2025-11-12 19:32:40 -08:00
parent 3c853c9a31
commit 13c12f3465
20 changed files with 5746 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
name: Node.js CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
name: Lint and Test
steps:
- name: Checkout Source Files
uses: actions/checkout@v4
- name: Use Node.js v22
uses: actions/setup-node@v4
with:
node-version: 22
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 9
- name: Install Dependencies
run: pnpm install
- name: Lint Source Files
run: pnpm run lint
- name: Verify Build
run: pnpm run build
- name: Run Tests
run: pnpm run test
+2
View File
@@ -0,0 +1,2 @@
node_modules
prod
+6
View File
@@ -0,0 +1,6 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": ["typescript"]
}
+3
View File
@@ -0,0 +1,3 @@
import NaomisConfig from "@nhcarrigan/eslint-config";
export default NaomisConfig;
+33
View File
@@ -0,0 +1,33 @@
{
"name": "saisoku",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
"build": "tsc",
"lint": "eslint src --max-warnings 0",
"start": "op run --env-file=prod.env -- node prod/index.js",
"test": "echo \"Error: no test specified\" && exit 0"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.20.0",
"devDependencies": {
"@nhcarrigan/eslint-config": "5.2.0",
"@nhcarrigan/typescript-config": "4.0.0",
"@types/node": "24.10.1",
"@types/node-schedule": "2.1.8",
"eslint": "9.39.1",
"prisma": "6.19.0",
"typescript": "5.9.3"
},
"dependencies": {
"@nhcarrigan/discord-analytics": "0.0.6",
"@nhcarrigan/logger": "1.1.1",
"@prisma/client": "6.19.0",
"discord.js": "14.24.2",
"fastify": "5.6.2",
"node-schedule": "2.1.1"
}
}
+4914
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("MONGO_URI")
}
model Reminders {
id String @id @default(auto()) @map("_id") @db.ObjectId
userId String
reminder String
isHarsh Boolean
}
+3
View File
@@ -0,0 +1,3 @@
BOT_TOKEN="op://Environment Variables - Naomi/Saisoku/bot token"
LOG_TOKEN="op://Environment Variables - Naomi/Alert Server/api_auth"
MONGO_URI="op://Environment Variables - Naomi/Saisoku/mongo"
+39
View File
@@ -0,0 +1,39 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import {
ApplicationIntegrationType,
InteractionContextType,
SlashCommandBuilder,
} from "discord.js";
const reminder = new SlashCommandBuilder().
setName("reminder").
setIntegrationTypes(ApplicationIntegrationType.UserInstall).
setContexts(
InteractionContextType.BotDM,
InteractionContextType.Guild,
InteractionContextType.PrivateChannel,
).
setDescription("Ask the bot to remind you hourly to complete a task.").
addStringOption((option) => {
return option.
setName("task").
setDescription("The task to remind you about.").
setMaxLength(50).
setRequired(true);
}).
addBooleanOption((option) => {
return option.
setName("bully").
setDescription(
"Whether the bot should bully you instead of encouraging you.",
).
setRequired(true);
});
// eslint-disable-next-line no-console -- We only use this to generate the JSON payload for the API.
console.log(JSON.stringify(reminder.toJSON(), null, 2));
+12
View File
@@ -0,0 +1,12 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export const entitledUserIds: Array<string> = [
// Naomi
"465650873650118659",
// Hana (from FCC) - free access because she wants it
"693913517040074792",
];
+288
View File
@@ -0,0 +1,288 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
const encouragingPhrases = [
"Just a gentle reminder to start {{ task name }}.",
"When you have a moment, give {{ task name }} some love.",
"Ready? Let's take a look at {{ task name }}.",
"It will feel so good to have {{ task name }} done.",
"Be kind to your future self and handle {{ task name }} now.",
"A little progress on {{ task name }} goes a long way.",
"Let's simply start {{ task name }} and see where it goes.",
"Take a deep breath, then tackle {{ task name }}.",
"No pressure, but {{ task name }} is ready for you.",
"Ease into it: let's do {{ task name }}.",
"Sending you good vibes for {{ task name }}.",
"You deserve the peace of mind of finishing {{ task name }}.",
"Let's clear {{ task name }} off your plate.",
"Soft reminder: {{ task name }} is waiting for you.",
"Make space in your day for {{ task name }}.",
"You are going to crush {{ task name }}!",
"Let's destroy {{ task name }} like a boss!",
"Unleash your potential on {{ task name }}.",
"It's time to show {{ task name }} who is in charge!",
"Get excited! It's time for {{ task name }}.",
"You're a machine! Go handle {{ task name }}.",
"Knock out {{ task name }} and celebrate!",
"Power through {{ task name }}—you've got this!",
"Nothing can stop you from finishing {{ task name }}.",
"Let's make {{ task name }} happen right now!",
"Attack {{ task name }} with enthusiasm!",
"You're unstoppable! Time for {{ task name }}.",
"Let's get a big win by finishing {{ task name }}.",
"Supercharge your day by completing {{ task name }}.",
"Show {{ task name }} what you are made of!",
"Focus mode: engaged. Target: {{ task name }}.",
"Let's get into the flow with {{ task name }}.",
"One thing at a time. Right now, it's {{ task name }}.",
"Clear your mind and focus on {{ task name }}.",
"Let's be productive and knock out {{ task name }}.",
"Zone in on {{ task name }} for a few minutes.",
"Prioritize {{ task name }} and watch the stress vanish.",
"Let's turn that 'to-do' into 'done' for {{ task name }}.",
"Sharpen your focus: {{ task name }} needs you.",
"Dedicate this moment to {{ task name }}.",
"Let's check the box for {{ task name }}.",
"Efficient and effective—that's you doing {{ task name }}.",
"Momentum is building. Next up: {{ task name }}.",
"Stay on track: {{ task name }} is next.",
"Let's get organized and finish {{ task name }}.",
"I believe in your ability to finish {{ task name }}.",
"You are perfectly capable of handling {{ task name }}.",
"You've done harder things than {{ task name }}.",
"Trust your process and start {{ task name }}.",
"You have all the skills needed for {{ task name }}.",
"Don't doubt yourself; just start {{ task name }}.",
"You are stronger than {{ task name }}.",
"Conquer {{ task name }} with confidence.",
"You're doing great. Next stop: {{ task name }}.",
"Your best effort on {{ task name }} is enough.",
"Smart, capable, and ready for {{ task name }}.",
"You've got the magic touch for {{ task name }}.",
"Prove to yourself that you can do {{ task name }}.",
"Channel your inner strength for {{ task name }}.",
"You are a finisher! Go finish {{ task name }}.",
"Five minutes is all you need to start {{ task name }}.",
"Don't overthink it, just start {{ task name }}.",
"The hardest part is starting. Let's begin {{ task name }}.",
"Just one small step towards {{ task name }}.",
"Only you can do {{ task name }}. Why not now?",
"Let's stop planning and start doing {{ task name }}.",
"Beat procrastination by starting {{ task name }} now.",
"Action is the antidote to anxiety regarding {{ task name }}.",
"Dive right into {{ task name }}.",
"Don't wait for the perfect time to do {{ task name }}.",
"Let's rip the band-aid off and do {{ task name }}.",
"Start {{ task name }} and the motivation will follow.",
"Imperfect action is better than no action on {{ task name }}.",
"Let's get the ball rolling on {{ task name }}.",
"Today is a great day to start {{ task name }}.",
"Imagine the relief when {{ task name }} is done!",
"Treat yourself after you finish {{ task name }}.",
"Earn your relaxation by doing {{ task name }}.",
"The finish line for {{ task name }} is so close.",
"Think of how proud you'll be to finish {{ task name }}.",
"Free up your brain space by finishing {{ task name }}.",
"Do {{ task name }} now so you can relax later.",
"Give yourself the gift of a completed {{ task name }}.",
"Let's get {{ task name }} done so we can chill.",
"Finish {{ task name }} and enjoy the rest of your day.",
"Success tastes sweet, especially after {{ task name }}.",
"Happiness is a completed {{ task name }}.",
"You're working towards a goal. {{ task name }} helps.",
"Invest in your peace of mind: do {{ task name }}.",
"Let's wrap up {{ task name }} and call it a win.",
"Time for {{ task name }}!",
"Let's go: {{ task name }}.",
"Up next: {{ task name }}.",
"You + {{ task name }} = Done.",
"It's {{ task name }} time.",
"Handle {{ task name }}.",
"Go get {{ task name }}.",
"Start {{ task name }}.",
"Finish {{ task name }}.",
"Crush {{ task name }}.",
"Do {{ task name }}.",
"Tackle {{ task name }}.",
"Complete {{ task name }}.",
"Regarding {{ task name }}: Go!",
"Attention: {{ task name }}.",
];
const bullyingPhrases = [
"Oh, look who finally decided to think about {{ task name }}.",
"I'd be shocked if you actually did {{ task name }} today.",
"Wow, avoiding {{ task name }}? Groundbreaking.",
"Is {{ task name }} invisible? Or are you just pretending?",
"Congratulations on your award for 'Best at Ignoring {{ task name }}'.",
"I love how you just stare at {{ task name }} without doing it.",
"It must be exhausting avoiding {{ task name }} this hard.",
"Oh, you're busy? Too busy for {{ task name }}? Sure you are.",
"Take a picture of {{ task name }}, it'll last longer.",
"I'm sure procrastination is a great strategy for {{ task name }}.",
"Did you forget {{ task name }}, or are you just ignoring me?",
"I bet you have a great excuse for not doing {{ task name }}.",
"Don't worry, {{ task name }} will just do itself... oh wait, it won't.",
"So, are we doing {{ task name }} or just looking at it?",
"Keep scrolling. {{ task name }} loves being neglected.",
"I'm ageing rapidly waiting for you to start {{ task name }}.",
"We are all waiting for you to do {{ task name }}.",
"Tick tock. {{ task name }} isn't getting younger.",
"Can we please just get {{ task name }} over with?",
"I'm going to sleep. Wake me when you do {{ task name }}.",
"Do {{ task name }} before I turn into a skeleton.",
"Any century now for {{ task name }}...",
"My battery is dying and you still haven't done {{ task name }}.",
"Hurry up. {{ task name }} is getting stale.",
"I've seen glaciers move faster than you on {{ task name }}.",
"Are you waiting for a written invitation to do {{ task name }}?",
"Less thinking, more doing {{ task name }}.",
"Stop stalling. {{ task name }} is right there.",
"I don't have all day, and neither does {{ task name }}.",
"Bored now. Do {{ task name }}.",
"Is {{ task name }} too hard for you? Aww.",
"I thought you were capable, but then I saw {{ task name }} sitting there.",
"Do you need a tutorial on how to do {{ task name }}?",
"Even a toddler could have finished {{ task name }} by now.",
"Is the concept of {{ task name }} confusing you?",
"Are you scared of {{ task name }}? It's okay to admit it.",
"I'd help you with {{ task name }}, but I don't want to.",
"Trying to figure out if you're lazy or just bad at {{ task name }}.",
"Do you need me to hold your hand for {{ task name }}?",
"Maybe {{ task name }} is just above your skill level.",
"It's not rocket science, it's just {{ task name }}.",
"Are you allergic to productivity? Do {{ task name }}.",
"I honestly didn't think {{ task name }} would stump you this bad.",
"You're struggling with {{ task name }}? Really?",
"Prove me wrong and actually finish {{ task name }}.",
"Your future self already hates you for leaving {{ task name }}.",
"You're going to regret not doing {{ task name }} right now.",
"Imagine how pathetic it will feel to do {{ task name }} at midnight.",
"Disappointing me is one thing, but ignoring {{ task name }}?",
"Every second you ignore {{ task name }}, it gets stronger.",
"Do {{ task name }} or bring shame upon your family.",
"Why do you torture yourself by putting off {{ task name }}?",
"It's sad that {{ task name }} is still on your list.",
"You're ruining your own day by not doing {{ task name }}.",
"Do you enjoy the stress of an unfinished {{ task name }}?",
"Cry about it, then do {{ task name }}.",
"{{ task name }} is judging you. I can feel it.",
"You promised you'd do {{ task name }}. Liar.",
"Don't come crying to me when {{ task name }} isn't done.",
"Be a functional adult and do {{ task name }}.",
"Stop being a baby and do {{ task name }}.",
"Get off your butt. {{ task name }}. Now.",
"Put the phone down and handle {{ task name }}.",
"Quit whining and start {{ task name }}.",
"Do {{ task name }} or I delete your cookies.",
"Stop making excuses. {{ task name }}.",
"Focus, you goldfish. {{ task name }}.",
"Get it together. {{ task name }} needs doing.",
"Suck it up and finish {{ task name }}.",
"Stop doom-scrolling and look at {{ task name }}.",
"Snap out of it! {{ task name }} time!",
"Don't make me tell you to do {{ task name }} again.",
"JUST DO {{ task name }}!",
"Move it! {{ task name }} isn't going to wait.",
"Shut up and do {{ task name }}.",
"{{ task name }} is starting to smell.",
"{{ task name }} is collecting dust while you procrastinate.",
"If {{ task name }} was food, it would be mouldy by now.",
"{{ task name }} is literally rotting on your to-do list.",
"Cobwebs are forming on {{ task name }}.",
"I think something crawled into {{ task name }} and died.",
"{{ task name }} is festering.",
"Clean up your act and clean up {{ task name }}.",
"It's getting gross how long you've ignored {{ task name }}.",
"{{ task name }} is fossilizing.",
"You look like someone who ignores {{ task name }}.",
// eslint-disable-next-line stylistic/max-len -- long string
"I bet you tell people you're 'busy' but you're just avoiding {{ task name }}.",
"Your procrastination on {{ task name }} is not a personality trait.",
"You are the reason {{ task name }} isn't done.",
"Look in the mirror. Now look at {{ task name }}. Fix it.",
"You're better than this. Well, maybe not at {{ task name }}.",
"Are you always this slow with {{ task name }}?",
"I'd ask how you are, but you haven't done {{ task name }} yet.",
"You have the attention span of a gnat. Do {{ task name }}.",
"Don't be that person who complains about {{ task name }}.",
"You're acting like a brat about {{ task name }}.",
"Drama queen. Just do {{ task name }}.",
"I didn't realize {{ task name }} was your kryptonite.",
"You're making {{ task name }} awkward.",
"Fix your life. Start with {{ task name }}.",
];
const encouragingCompletion = [
"Way to go!",
"You crushed it!",
"Another one bites the dust.",
"Look at you go!",
"Productivity looks good on you.",
"Great job finishing that up.",
"You are on fire today!",
"One step closer to your goals.",
"Give yourself a pat on the back.",
"That wasn't so bad, was it?",
"You are unstoppable.",
"Victory is yours!",
"Excellent work.",
"Check! Done and done.",
"So proud of you.",
"You made that look easy.",
"Keep up the amazing momentum!",
"Tick that box with pride.",
"Your future self thanks you.",
"Outstanding effort.",
"You are making serious progress.",
"Doesn't it feel good to be done?",
"Mission accomplished.",
"You handled that like a pro.",
"Take a bow, you earned it.",
"Clearing the list, one item at a time.",
"You are a productivity machine.",
"That's a win in my book.",
"Fantastic work!",
"Sending you a virtual high-five.",
];
const bullyingCompletion = [
"Finally. I was getting worried.",
"About time.",
"Wow, you actually did it. I'm shocked.",
"Do you want a cookie or something?",
"Don't expect a parade, it was just one task.",
"Miracles really do happen.",
"Took you long enough.",
"See? Was that really so hard?",
"You survived. Barely.",
"Don't stop now, you have more to do.",
"I guess you can be useful sometimes.",
"Oh, look who decided to be productive.",
"Congratulations on doing the bare minimum.",
"I was about to call search and rescue.",
"Finally checked that off? Cute.",
"Don't break an arm patting yourself on the back.",
"Okay, now do the next one.",
"You finished it! Now stop stalling.",
"I'd clap, but I'm not that impressed.",
"Welcome to the world of functioning adults.",
"Check mark! Finally.",
"You did it! Now get back to work.",
"Alert the media, you actually finished a task.",
"I was starting to think you'd never finish that.",
"Less celebrating, more doing.",
"Adequate performance. Barely.",
"Don't let it go to your head.",
"You're slow, but you get there eventually.",
"One down, a million to go.",
"Fine. Good job. Whatever.",
];
export { encouragingPhrases,
bullyingPhrases,
encouragingCompletion,
bullyingCompletion };
+41
View File
@@ -0,0 +1,41 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { DiscordAnalytics } from "@nhcarrigan/discord-analytics";
import { PrismaClient } from "@prisma/client";
import { Client, Events } from "discord.js";
import { scheduleJob } from "node-schedule";
import { completeTask } from "./modules/completeTask.js";
import { createReminder } from "./modules/createReminder.js";
import { sendReminders } from "./modules/sendReminders.js";
import { logger } from "./utils/logger.js";
const bot = new Client({
intents: [],
});
const database = new PrismaClient();
const analytics = new DiscordAnalytics(bot, logger);
bot.once(Events.ClientReady, () => {
void logger.log("debug", "Client is ready!");
analytics.startCron();
const hourly = "32 * * * *";
scheduleJob("reminders", hourly, () => {
void sendReminders(bot, database);
});
});
bot.on(Events.InteractionCreate, (interaction) => {
if (interaction.isButton()) {
void completeTask(bot, interaction, database);
}
if (interaction.isChatInputCommand()) {
void createReminder(bot, interaction, database);
}
});
await bot.login(process.env.BOT_TOKEN ?? "");
+68
View File
@@ -0,0 +1,68 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
type ButtonInteraction,
type Client,
} from "discord.js";
import {
bullyingCompletion,
encouragingCompletion,
} from "../config/phrases.js";
import { getRandomValue } from "../utils/getRandomValue.js";
import { isEntitled } from "./isEntitled.js";
import type { PrismaClient } from "@prisma/client";
/**
* Completes a task.
* @param bot - The Discord client.
* @param interaction - The interaction payload from Discord.
* @param database - The database client.
*/
export const completeTask = async(
bot: Client,
interaction: ButtonInteraction,
database: PrismaClient,
): Promise<void> => {
await interaction.deferReply();
const entitled = await isEntitled(bot, interaction.user.id);
if (!entitled) {
await interaction.editReply({
components: [
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().
setStyle(ButtonStyle.Premium).
setSKUId("1438369555956105382"),
),
],
content: `You must have a subscription to use this bot. Please note that if your subscription lapses, you will stop receiving reminders until you restore it.`,
});
return;
}
const task = await database.reminders.findUnique({
where: {
id: interaction.customId,
},
});
if (!task) {
await interaction.editReply({ content: "Task not found" });
return;
}
await database.reminders.delete({
where: {
id: task.id,
},
});
const phrase = getRandomValue(
task.isHarsh
? bullyingCompletion
: encouragingCompletion,
);
await interaction.editReply({ content: phrase });
};
+66
View File
@@ -0,0 +1,66 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
type ChatInputCommandInteraction,
type Client,
} from "discord.js";
import { isEntitled } from "./isEntitled.js";
import type { PrismaClient } from "@prisma/client";
/**
* Creates a reminder.
* @param bot - The Discord client.
* @param interaction - The interaction payload from Discord.
* @param database - The database client.
*/
export const createReminder = async(
bot: Client,
interaction: ChatInputCommandInteraction,
database: PrismaClient,
): Promise<void> => {
await interaction.deferReply({ ephemeral: true });
const entitled = await isEntitled(bot, interaction.user.id);
if (!entitled) {
await interaction.editReply({
components: [
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().
setStyle(ButtonStyle.Premium).
setSKUId("1438369555956105382"),
),
],
content: `You must have a subscription to use this bot. Please note that if your subscription lapses, you will stop receiving reminders until you restore it.`,
});
return;
}
const existingUserReminders = await database.reminders.findMany({
where: {
userId: interaction.user.id,
},
});
if (existingUserReminders.length >= 5) {
await interaction.editReply({
content: "You can only have 5 reminders at a time.",
});
return;
}
const task = interaction.options.getString("task", true);
const bully = interaction.options.getBoolean("bully", true);
await database.reminders.create({
data: {
isHarsh: bully,
reminder: task,
userId: interaction.user.id,
},
});
await interaction.editReply({
content: `I will remind you about **${task}** every hour until you complete it.`,
});
};
+33
View File
@@ -0,0 +1,33 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { entitledUserIds } from "../config/entitlements.js";
import type { Client } from "discord.js";
/**
* Checks if a user is entitled to use the bot.
* @param bot - The Discord client.
* @param userId - The ID of the user to check.
* @returns True if the user is entitled, false otherwise.
*/
export const isEntitled = async(
bot: Client,
userId: string,
): Promise<boolean> => {
if (entitledUserIds.includes(userId)) {
return true;
}
const hasEntitlement = await bot.application?.entitlements.
fetch({ excludeDeleted: true, excludeEnded: true, user: userId }).
catch(() => {
return null;
});
return (
hasEntitlement !== null
&& hasEntitlement?.size !== undefined
&& hasEntitlement.size > 0
);
};
+69
View File
@@ -0,0 +1,69 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
type Client,
} from "discord.js";
import { bullyingPhrases, encouragingPhrases } from "../config/phrases.js";
import { getRandomValue } from "../utils/getRandomValue.js";
import { isEntitled } from "./isEntitled.js";
import type { PrismaClient } from "@prisma/client";
/**
* Fetches all active reminders, sends them to the users.
* @param bot - The Discord client.
* @param database - The database client.
*/
export const sendReminders = async(
bot: Client,
database: PrismaClient,
): Promise<void> => {
const reminders = await database.reminders.findMany();
await Promise.all(
reminders.map(async(reminder) => {
const user
= bot.users.cache.get(reminder.userId)
?? await bot.users.fetch(reminder.userId).catch(() => {
return null;
});
if (user === null) {
await database.reminders.delete({
where: {
id: reminder.id,
},
});
return;
}
const entitled = await isEntitled(bot, user.id);
if (!entitled) {
return;
}
const phrase = getRandomValue(
reminder.isHarsh
? bullyingPhrases
: encouragingPhrases,
);
await user.send({
components: [
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().
setCustomId(reminder.id).
setLabel("Complete").
setStyle(ButtonStyle.Success),
new ButtonBuilder().
setStyle(ButtonStyle.Link).
setURL("https://chat.nhcarrigan.com").
setLabel("Need help?"),
),
],
content: phrase.replace("{{ task name }}", `**${reminder.reminder}**`),
});
}),
);
};
+79
View File
@@ -0,0 +1,79 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import fastify from "fastify";
import { logger } from "../utils/logger.js";
const html = `<!DOCTYPE html>
<html>
<head>
<title>Saisoku</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="A Discord bot that encourages (or bullies) you to be more productive." />
<script src="https://cdn.nhcarrigan.com/headers/index.js" async defer></script>
</head>
<body>
<main>
<h1>Saisoku</h1>
<img src="https://cdn.nhcarrigan.com/new-avatars/saisoku-full.png" width="250" alt="Saisoku" />
<section>
<p>A Discord bot that encourages (or bullies) you to be more productive.</p>
<a href="https://discord.com/oauth2/authorize?client_id=1438325099345346723" class="social-button discord-button" style="display: inline-block; background-color: #5865F2; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px; margin: 5px;">
<i class="fab fa-discord"></i> Add to Discord
</a>
</section>
<section>
<h2>Links</h2>
<p>
<a href="https://git.nhcarrigan.com/nhcarrigan/saisoku">
<i class="fa-solid fa-code"></i> Source Code
</a>
</p>
<p>
<a href="https://docs.nhcarrigan.com/">
<i class="fa-solid fa-book"></i> Documentation
</a>
</p>
<p>
<a href="https://chat.nhcarrigan.com">
<i class="fa-solid fa-circle-info"></i> Support
</a>
</p>
</section>
</main>
</body>
</html>`;
/**
* Starts up a web server for health monitoring.
*/
export const instantiateServer = (): void => {
try {
const server = fastify({
logger: false,
});
server.get("/", (_request, response) => {
response.header("Content-Type", "text/html");
response.send(html);
});
server.listen({ port: 9100 }, (error) => {
if (error) {
void logger.error("instantiate server", error);
return;
}
void logger.log("debug", "Server listening on port 9100.");
});
} catch (error) {
if (error instanceof Error) {
void logger.error("instantiate server", error);
return;
}
void logger.error("instantiate server", new Error(String(error)));
}
};
+15
View File
@@ -0,0 +1,15 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/**
* Gets a random value from an array.
* @param array - The array to get a random value from.
* @returns A random value from the array.
*/
export const getRandomValue = <T>(array: Array<T>): T => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- we know the type is correct
return array[Math.floor(Math.random() * array.length)] as T;
};
+12
View File
@@ -0,0 +1,12 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { Logger } from "@nhcarrigan/logger";
export const logger = new Logger(
"Saisoku",
process.env.LOG_TOKEN ?? "",
);
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "@nhcarrigan/typescript-config",
"compilerOptions": {
"outDir": "./prod",
"rootDir": "./src"
}
}