generated from nhcarrigan/template
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
prod
|
||||
@@ -0,0 +1,5 @@
|
||||
import NaomisConfig from "@nhcarrigan/eslint-config";
|
||||
|
||||
export default [
|
||||
...NaomisConfig
|
||||
];
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "amari",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "op run --env-file=prod.env -- node prod/index.js",
|
||||
"lint": "eslint src --max-warnings 0",
|
||||
"test": "echo \"Error: no test specified\" && exit 0"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"packageManager": "pnpm@10.15.0",
|
||||
"devDependencies": {
|
||||
"@nhcarrigan/eslint-config": "5.2.0",
|
||||
"@nhcarrigan/typescript-config": "4.0.0",
|
||||
"@types/node": "24.3.0",
|
||||
"eslint": "9.33.0",
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nhcarrigan/logger": "1.0.0",
|
||||
"discord.js": "14.21.0",
|
||||
"fastify": "5.5.0"
|
||||
}
|
||||
}
|
||||
Generated
+4633
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
LOG_TOKEN="op://Environment Variables - Naomi/Alert Server/api_auth"
|
||||
BOT_TOKEN="op://Environment Variables - Naomi/Amari/bot token"
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
export const ids = {
|
||||
channels: {
|
||||
mentorshipGoalForum: "1400629118110011526",
|
||||
mentorshipProjectForum: "1400616702265266186",
|
||||
},
|
||||
roles: {
|
||||
nhcarrigan: "1355033209037127771",
|
||||
},
|
||||
tags: {
|
||||
goal: {
|
||||
member: "1406355263811752147",
|
||||
naomi: "1406355221847871740",
|
||||
},
|
||||
project: {
|
||||
member: "1406355801983025183",
|
||||
naomi: "1406355770336870420",
|
||||
},
|
||||
},
|
||||
users: {
|
||||
amari: "1406431359345496255",
|
||||
naomi: "465650873650118659",
|
||||
nhcarrigan: "1382837581649150104",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
/* eslint-disable stylistic/max-len -- Lots of big strings here. */
|
||||
|
||||
export const responses = {
|
||||
dm: "Hiya! I am Naomi's personal assistant, so I am afraid I cannot help you. But I have forwarded your message to Naomi!",
|
||||
naomiMentioned: "Hello~! It looks like you mentioned Naomi. I have notified her, and she will respond as soon as she can.",
|
||||
teamMentioned: "Hello~! It looks like you mentioned our team. I have notified Naomi, and she will respond as soon as she can.",
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { respondToMention } from "../modules/respondToMention.js";
|
||||
import { updateMentorshipThread } from "../modules/updateMentorshipThread.js";
|
||||
import type { Amari } from "../interfaces/amari.js";
|
||||
import type { Message } from "discord.js";
|
||||
|
||||
/**
|
||||
* Handles the message create event from Discord.
|
||||
* Bootstraps all of our custom logic modules.
|
||||
* @param amari -- Amari's instance.
|
||||
* @param message -- The guild message payload from Discord.
|
||||
*/
|
||||
export const handleMessageCreate = async(
|
||||
amari: Amari,
|
||||
message: Message<true>,
|
||||
): Promise<void> => {
|
||||
if (message.author.bot || message.system) {
|
||||
return;
|
||||
}
|
||||
await updateMentorshipThread(amari, message);
|
||||
await respondToMention(amari, message);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { Client, GatewayIntentBits, Events, Partials } from "discord.js";
|
||||
import { handleMessageCreate } from "./events/handleMessageCreate.js";
|
||||
import { respondToDm } from "./modules/respondToDm.js";
|
||||
import { instantiateServer } from "./server/serve.js";
|
||||
import { logger } from "./utils/logger.js";
|
||||
import type { Amari } from "./interfaces/amari.js";
|
||||
|
||||
const amari: Amari = {
|
||||
discord: new Client({ intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
GatewayIntentBits.DirectMessages,
|
||||
],
|
||||
partials: [ Partials.Channel ] }),
|
||||
};
|
||||
|
||||
amari.discord.once(Events.ClientReady, () => {
|
||||
void logger.log("debug",
|
||||
`Authenticated to Discord as ${amari.discord.user?.username ?? "unknown"}`);
|
||||
});
|
||||
|
||||
amari.discord.on(Events.MessageCreate, (message) => {
|
||||
if (!message.inGuild()) {
|
||||
void respondToDm(amari, message);
|
||||
return;
|
||||
}
|
||||
void handleMessageCreate(amari, message);
|
||||
});
|
||||
|
||||
await amari.discord.login(process.env.BOT_TOKEN);
|
||||
instantiateServer();
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import type { Client } from "discord.js";
|
||||
|
||||
export interface Amari {
|
||||
discord: Client;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { MessageFlags, type Message } from "discord.js";
|
||||
import { ids } from "../config/ids.js";
|
||||
import { responses } from "../config/responses.js";
|
||||
import { getComponentsForNaomi } from "../utils/getComponentsForNaomi.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import type { Amari } from "../interfaces/amari.js";
|
||||
|
||||
/**
|
||||
* Responds to a DM.
|
||||
* @param amari -- Amari's instance.
|
||||
* @param message -- The DM message payload from Discord.
|
||||
*/
|
||||
export const respondToDm = async(
|
||||
amari: Amari,
|
||||
message: Message,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const { author, content, system, url } = message;
|
||||
if (author.bot || system || author.id === ids.users.naomi) {
|
||||
return;
|
||||
}
|
||||
const naomi = amari.discord.users.cache.get(ids.users.naomi)
|
||||
?? await amari.discord.users.fetch(ids.users.naomi);
|
||||
await naomi.send({
|
||||
components: getComponentsForNaomi(author, content, url),
|
||||
flags: [ MessageFlags.IsComponentsV2 ],
|
||||
});
|
||||
await message.reply({
|
||||
content: responses.dm,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
await logger.error("respond to DM module", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { MessageFlags, type Message } from "discord.js";
|
||||
import { ids } from "../config/ids.js";
|
||||
import { responses } from "../config/responses.js";
|
||||
import { getComponentsForNaomi } from "../utils/getComponentsForNaomi.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import type { Amari } from "../interfaces/amari.js";
|
||||
|
||||
/**
|
||||
* Checks if a message mentions Naomi or our team role.
|
||||
* If so, responds.
|
||||
* @param amari -- Amari's instance.
|
||||
* @param message -- The guild message payload from Discord.
|
||||
*/
|
||||
// eslint-disable-next-line max-lines-per-function, complexity -- Mainly those reply options...
|
||||
export const respondToMention = async(
|
||||
amari: Amari,
|
||||
message: Message<true>,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const naomi = amari.discord.users.cache.get(ids.users.naomi)
|
||||
?? await amari.discord.users.fetch(ids.users.naomi);
|
||||
const { mentions, content, author, url } = message;
|
||||
if (author.bot || author.id === ids.users.naomi) {
|
||||
return;
|
||||
}
|
||||
if (mentions.has(ids.users.naomi, {
|
||||
ignoreEveryone: true,
|
||||
ignoreRepliedUser: true,
|
||||
ignoreRoles: true,
|
||||
}) || /naomi/i.test(content)) {
|
||||
await message.reply({
|
||||
allowedMentions: {
|
||||
repliedUser: false,
|
||||
},
|
||||
content: responses.naomiMentioned,
|
||||
});
|
||||
await naomi.send(
|
||||
{
|
||||
components: getComponentsForNaomi(author, content, url),
|
||||
flags: [ MessageFlags.IsComponentsV2 ],
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (mentions.has(ids.roles.nhcarrigan, {
|
||||
ignoreEveryone: true,
|
||||
ignoreRepliedUser: true,
|
||||
}) || mentions.has(ids.users.nhcarrigan, {
|
||||
ignoreEveryone: true,
|
||||
ignoreRepliedUser: true,
|
||||
ignoreRoles: true,
|
||||
}) || /nhcarrigan/i.test(content)) {
|
||||
await message.reply({
|
||||
allowedMentions: {
|
||||
repliedUser: false,
|
||||
},
|
||||
content: responses.naomiMentioned,
|
||||
});
|
||||
await naomi.send(
|
||||
{
|
||||
components: getComponentsForNaomi(author, content, url),
|
||||
flags: [ MessageFlags.IsComponentsV2 ],
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
await logger.error("respond to mention module", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { ids } from "../config/ids.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import type { Amari } from "../interfaces/amari.js";
|
||||
import type { Message } from "discord.js";
|
||||
|
||||
/**
|
||||
* Processes a message in a mentorship thread. Applies either
|
||||
* the `waiting on member` tag or the `waiting on naomi` tag,
|
||||
* depending on who sent the message.
|
||||
* @param _amari -- Amari's instance.
|
||||
* @param message -- The guild message payload from Discord.
|
||||
*/
|
||||
export const updateMentorshipThread = async(
|
||||
_amari: Amari,
|
||||
message: Message<true>,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const { channel, author } = message;
|
||||
if (!channel.isThread() || channel.parent?.isThreadOnly() !== true) {
|
||||
return;
|
||||
}
|
||||
const { mentorshipGoalForum, mentorshipProjectForum } = ids.channels;
|
||||
if (![
|
||||
mentorshipGoalForum,
|
||||
mentorshipProjectForum,
|
||||
].includes(channel.parent.id)) {
|
||||
return;
|
||||
}
|
||||
const memberTag = channel.parentId === mentorshipGoalForum
|
||||
? ids.tags.goal.member
|
||||
: ids.tags.project.member;
|
||||
const naomiTag = channel.parentId === mentorshipGoalForum
|
||||
? ids.tags.goal.naomi
|
||||
: ids.tags.project.naomi;
|
||||
if (author.id === ids.users.naomi) {
|
||||
await channel.setAppliedTags([ ...channel.appliedTags.filter((tag) => {
|
||||
return tag !== naomiTag;
|
||||
}), memberTag ]);
|
||||
return;
|
||||
}
|
||||
await channel.setAppliedTags([ ...channel.appliedTags.filter((tag) => {
|
||||
return tag !== memberTag;
|
||||
}), naomiTag ]);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
await logger.error("update mentorship thread module", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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>Amari</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Naomi's virtual personal assistant on Discord." />
|
||||
<script src="https://cdn.nhcarrigan.com/headers/index.js" async defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Amari/h1>
|
||||
<img src="https://cdn.nhcarrigan.com/new-avatars/amari.png" width="250" alt="Amari" />
|
||||
<section>
|
||||
<p>Naomi's Virtual Personal Assistant on Discord.</p>
|
||||
<a href="https://chat.nhcarrigan.com" 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> Join Discord
|
||||
</a>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Links</h2>
|
||||
<p>
|
||||
<a href="https://git.nhcarrigan.com/nhcarrigan/amari">
|
||||
<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: 5044 }, (error) => {
|
||||
if (error) {
|
||||
void logger.error("instantiate server", error);
|
||||
return;
|
||||
}
|
||||
void logger.log("debug", "Server listening on port 5044.");
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
void logger.error("instantiate server", error);
|
||||
return;
|
||||
}
|
||||
void logger.error("instantiate server", new Error(String(error)));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import type { User, APIMessageTopLevelComponent } from "discord.js";
|
||||
|
||||
/**
|
||||
* Generates the components to forward a message to Naomi.
|
||||
* @param author -- The user object that created the message.
|
||||
* @param content -- The text content of the message.
|
||||
* @param url -- The link to the message.
|
||||
* @returns An array of message component objects.
|
||||
*/
|
||||
export const getComponentsForNaomi
|
||||
= (
|
||||
author: User,
|
||||
content: string,
|
||||
url: string,
|
||||
): Array<APIMessageTopLevelComponent> => {
|
||||
return [
|
||||
{
|
||||
components: [
|
||||
{
|
||||
accessory: {
|
||||
description: null,
|
||||
media: {
|
||||
url: author.displayAvatarURL(),
|
||||
},
|
||||
spoiler: false,
|
||||
type: 11,
|
||||
},
|
||||
components: [
|
||||
{
|
||||
content: `# Message from ${author.displayName}!`,
|
||||
type: 10,
|
||||
},
|
||||
{
|
||||
content: content,
|
||||
type: 10,
|
||||
},
|
||||
],
|
||||
type: 9,
|
||||
},
|
||||
],
|
||||
spoiler: false,
|
||||
type: 17,
|
||||
},
|
||||
{
|
||||
components: [
|
||||
{
|
||||
disabled: false,
|
||||
label: "View Message",
|
||||
style: 5,
|
||||
type: 2,
|
||||
url: url,
|
||||
},
|
||||
],
|
||||
type: 1,
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { Logger } from "@nhcarrigan/logger";
|
||||
|
||||
export const logger = new Logger(
|
||||
"Amari",
|
||||
process.env.LOG_TOKEN ?? "",
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "@nhcarrigan/typescript-config",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./prod"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user