feat: mentorship improvements and name mention notifications (#14)
Node.js CI / CI (push) Successful in 36s
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m27s

## Summary

- **Name/title mention notifications**: Amari now notifies Naomi when a message contains her name, common nicknames (`nomi`, `nao`, `nae`, `naonao`), or honorifics (`goddess`, `queen`, `mistress`, `your/her majesty`, `your/her highness`). Uses the same cooldown logic as mention forwarding.
- **Simplified mentee onboarding**: Replaced the lengthy welcome message with a concise prompt asking the new mentee to ping Naomi with their GitHub username and name.
- **Removed offboard notification**: `logMenteeLeave` now only logs a metric silently — no more "user must be offboarded" messages in the channel.
- **Deduplicated welcome messages**: Welcomed mentee IDs are persisted to `data/welcomed.txt` so the onboarding message is only ever sent once, even if the role is re-assigned.

## Test plan

- [ ] Assign mentorship role to a user and confirm the new onboarding message appears
- [ ] Re-assign the role to the same user and confirm no duplicate message is sent
- [ ] Remove a mentee from the server and confirm no offboard message is posted
- [ ] Send a message containing a matched name/honorific and confirm Naomi receives a DM forwarding it

 This PR was created with help from Hikari~ 🌸

Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Reviewed-on: #14
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #14.
This commit is contained in:
2026-03-02 16:03:14 -08:00
committed by Naomi Carrigan
parent 5c39d3d9ce
commit 5e149a29e4
9 changed files with 265 additions and 33 deletions
+2
View File
@@ -5,6 +5,7 @@
*/
import { ids } from "../config/ids.js";
import { notifyNameMention } from "../modules/notifyNameMention.js";
import { respondToMention } from "../modules/respondToMention.js";
import { updateMentorshipThread } from "../modules/updateMentorshipThread.js";
import type { Amari } from "../interfaces/amari.js";
@@ -29,4 +30,5 @@ export const handleMessageCreate = async(
}
await updateMentorshipThread(amari, message);
await respondToMention(amari, message);
await notifyNameMention(amari, message);
};
+1 -1
View File
@@ -159,7 +159,7 @@ amari.discord.on(Events.GuildMemberAdd, (member) => {
});
amari.discord.on(Events.GuildMemberRemove, (member) => {
void logMenteeLeave(amari, member);
void logMenteeLeave(member);
});
await amari.discord.login(process.env.BOT_TOKEN);
+1 -23
View File
@@ -6,40 +6,18 @@
import { ids } from "../config/ids.js";
import { logger } from "../utils/logger.js";
import type { Amari } from "../interfaces/amari.js";
import type { GuildMember, PartialGuildMember } from "discord.js";
/**
* Run when a guild member leaves. If the member had the mentorship role,
* notify Naomi.
* @param amari - Amari's instance.
* log the metric.
* @param member - The member payload from Discord.
*/
export const logMenteeLeave = async(
amari: Amari,
member: GuildMember | PartialGuildMember,
): Promise<void> => {
if (!member.roles.cache.has(ids.roles.mentorship)) {
return;
}
const channel = amari.discord.channels.cache.get(ids.channels.menteeChat)
?? await amari.discord.channels.fetch(ids.channels.menteeChat);
if (channel?.isSendable() !== true) {
await logger.log(
"warn",
"Mentee Chat channel does not exist or is not sendable.",
);
return;
}
await channel.send({
content: `Hey <@${ids.users.naomi}>~!
<@${member.id}> (${member.user.displayName} - ${member.id}) has left the server.
It seems they were part of the mentorship programme, so you may need to offboard them.`,
});
await logger.metric("processed_mentee_leave", 1, { user: member.id });
};
+68
View File
@@ -0,0 +1,68 @@
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { MessageFlags, type Message } from "discord.js";
import { ids } from "../config/ids.js";
import { getComponentsForNaomi } from "../utils/getComponentsForNaomi.js";
import { logger } from "../utils/logger.js";
import type { Amari } from "../interfaces/amari.js";
const nameMentionPatterns = [
/n\s+a\s+o\s+m\s+i/i,
/\bgoddess\b/i,
/\bqueen\b/i,
/\bmistress\b/i,
/\bnaonao\b/i,
/\bnao\b/i,
/\bnomi\b/i,
/\bnae\b/i,
/\byour majesty\b/i,
/\bher majesty\b/i,
/\byour highness\b/i,
/\bher highness\b/i,
];
/**
* Checks if a message contains a nickname or indirect reference to Naomi.
* If so, forwards the message to Naomi via DM.
* @param amari -- Amari's instance.
* @param message -- The guild message payload from Discord.
*/
export const notifyNameMention = async(
amari: Amari,
message: Message<true>,
): Promise<void> => {
try {
const { content, author, url, channel } = message;
if (author.bot || author.id === ids.users.naomi) {
return;
}
if (amari.recentlyActiveChannels.has(channel.id)) {
return;
}
const matchedPattern = nameMentionPatterns.find((pattern) => {
return pattern.test(content);
});
if (matchedPattern === undefined) {
return;
}
const matchedText = content.match(matchedPattern)?.[0] ?? "";
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 logger.metric("processed_name_mention", 1, {
match: matchedText,
user: author.id,
});
} catch (error) {
if (error instanceof Error) {
await logger.error("notify name mention module", error);
}
}
};
+14 -8
View File
@@ -6,6 +6,10 @@
import { ids } from "../config/ids.js";
import { logger } from "../utils/logger.js";
import {
addWelcomedMentee,
welcomedMentees,
} from "../utils/welcomedMentees.js";
import type { Amari } from "../interfaces/amari.js";
import type { GuildMember, PartialGuildMember } from "discord.js";
@@ -25,6 +29,7 @@ export const processMentorshipRole = async(
if (
oldMember.roles.cache.has(ids.roles.mentorship)
|| !updatedMember.roles.cache.has(ids.roles.mentorship)
|| welcomedMentees.has(updatedMember.id)
) {
return;
}
@@ -42,16 +47,17 @@ export const processMentorshipRole = async(
}
await channel.send({
content: `Hey <@${updatedMember.id}>~!
content: `Hey <@${updatedMember.id}>~! Welcome to the mentorship programme!
Welcome to our mentorship programme! We are excited to have you here and help you grow and reach success.
To get started, please ping Naomi with your GitHub username and your first and last name. She will invite you to your dedicated repository in the [NHCarrigan Mentorship organsation](<https://github.com/nhcarrigan-mentorship>) where you can work on your flagship project.
Once you have done this, your next step is to read our [wiki](<https://docs.nhcarrigan.com/mentorship/00-faq/>). Then, create your goal-setting post in <#1400629118110011526> and your project-planning post in <#1400616702265266186> as outlined in the wiki.
If at any time you need some guidance, support, or review, please ping Naomi! She's always happy to help you succeed in your mentorship! Best of luck on your journey~! <a:love:1364089736557494353>`,
Please ping (mention, tag) Naomi in this channel with the following template to get started:
\`\`\`
GitHub username:
First name:
Last name:
\`\`\`
Then read our [mentorship wiki](<https://docs.nhcarrigan.com/mentorship/00-faq/>) for the next steps!`,
});
addWelcomedMentee(updatedMember.id);
await logger.metric("processed_mentorship_role", 1, {
user: updatedMember.id,
});
+30
View File
@@ -0,0 +1,30 @@
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { appendFileSync, existsSync, readFileSync } from "node:fs";
const filePath = "data/welcomed.txt";
const loadWelcomedMentees = (): Set<string> => {
if (!existsSync(filePath)) {
return new Set();
}
const contents = readFileSync(filePath, "utf-8");
return new Set(contents.split("\n").filter(Boolean));
};
const welcomedMentees = loadWelcomedMentees();
/**
* Appends a mentee's ID to the welcomed set and persists it to disk.
* @param id - The Discord user ID to record.
*/
const addWelcomedMentee = (id: string): void => {
welcomedMentees.add(id);
appendFileSync(filePath, `${id}\n`);
};
export { addWelcomedMentee, welcomedMentees };