generated from nhcarrigan/template
feat: mentorship improvements and name mention notifications (#14)
## 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:
@@ -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
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user