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