725 lines
23 KiB
JavaScript
725 lines
23 KiB
JavaScript
const crypto = require("crypto");
|
|
const path = require("path");
|
|
const discord = require("discord.js");
|
|
const { Permissions } = discord;
|
|
const { ensureUserForIdentity } = require("../../src/services/users");
|
|
const { log } = require("../../src/services/logger");
|
|
|
|
const PLUGIN_ID = "welcome_messages";
|
|
const CONFIG_KEY = "config";
|
|
const ALLOWED_PLACEHOLDERS = new Set([
|
|
"username",
|
|
"displayname",
|
|
"pronoun",
|
|
"guildmembers"
|
|
]);
|
|
const LISTENER_KEY = Symbol.for("lumi.welcome_messages.guildMemberAdd");
|
|
|
|
const DEFAULT_WELCOME_MESSAGES = [
|
|
"Welcome {displayname}! Everyone say hi - {pronoun} just became member #{guildmembers}.",
|
|
"A wild {displayname} appeared. Welcome to the server!",
|
|
"Glad you made it, {displayname}. Make yourself at home!",
|
|
"Welcome in, {displayname}! The server now has {guildmembers} members."
|
|
];
|
|
|
|
const DEFAULT_WELCOME_BACK_MESSAGES = [
|
|
"Welcome back, {displayname}! Good to see {pronoun} again.",
|
|
"Look who's back: {displayname}! Hope you've been well.",
|
|
"Welcome back to the server, {displayname}. We kept the lights on for you.",
|
|
"Hey {displayname}, welcome back!"
|
|
];
|
|
|
|
const PRONOUN_SETS = [
|
|
{ value: "they/them", label: "they/them", subject: "they" },
|
|
{ value: "he/him", label: "he/him", subject: "he" },
|
|
{ value: "she/her", label: "she/her", subject: "she" },
|
|
{ value: "it/its", label: "it/its", subject: "it" },
|
|
{ value: "he/they", label: "he/they", subject: "he" },
|
|
{ value: "she/they", label: "she/they", subject: "she" },
|
|
{ value: "they/he", label: "they/he", subject: "they" },
|
|
{ value: "they/she", label: "they/she", subject: "they" },
|
|
{ value: "any/all", label: "any/all", subject: "they" },
|
|
{ value: "use name", label: "use name", subject: "they" }
|
|
];
|
|
|
|
module.exports = {
|
|
id: PLUGIN_ID,
|
|
init({ web, db, discordClient, settings }) {
|
|
ensureTables(db);
|
|
ensureConfig(db);
|
|
log("info", "Welcome Messages plugin initialized");
|
|
|
|
const router = web.createRouter();
|
|
|
|
router.get("/", async (req, res) => {
|
|
const user = req.session.user || null;
|
|
if (!canModerate(user)) {
|
|
return renderDenied(res);
|
|
}
|
|
const config = getConfig(db);
|
|
const diagnostics = await buildDiagnostics(discordClient, config.channelId);
|
|
res.render(path.join(__dirname, "views", "settings.ejs"), {
|
|
title: "Welcome Messages",
|
|
config,
|
|
diagnostics,
|
|
textChannels: getTextChannels(discordClient),
|
|
isAdmin: Boolean(user?.isAdmin),
|
|
canModerate: true,
|
|
allowedPlaceholders: Array.from(ALLOWED_PLACEHOLDERS),
|
|
pronounSets: PRONOUN_SETS,
|
|
previewMessage
|
|
});
|
|
});
|
|
|
|
router.post("/settings", async (req, res) => {
|
|
if (!req.session.user?.isAdmin) {
|
|
return renderDenied(res);
|
|
}
|
|
const config = getConfig(db);
|
|
const channelId = (req.body.channel_id || "").trim();
|
|
if (channelId) {
|
|
const validation = await validateTextChannel(discordClient, channelId);
|
|
if (!validation.valid) {
|
|
req.session.flash = { type: "error", message: validation.message };
|
|
return res.redirect(`/plugins/${PLUGIN_ID}`);
|
|
}
|
|
}
|
|
config.enabled = req.body.enabled === "on";
|
|
config.welcomeBackEnabled = req.body.welcome_back_enabled === "on";
|
|
config.channelId = channelId;
|
|
saveConfig(db, config);
|
|
req.session.flash = { type: "success", message: "Welcome settings saved." };
|
|
res.redirect(`/plugins/${PLUGIN_ID}`);
|
|
});
|
|
|
|
router.post("/messages/create", (req, res) => {
|
|
if (!canModerate(req.session.user)) {
|
|
return renderDenied(res);
|
|
}
|
|
const pool = normalizePool(req.body.pool);
|
|
const text = (req.body.text || "").trim();
|
|
const error = validateTemplate(text);
|
|
if (error) {
|
|
req.session.flash = { type: "error", message: error };
|
|
return res.redirect(`/plugins/${PLUGIN_ID}`);
|
|
}
|
|
const config = getConfig(db);
|
|
config[pool].push(newMessage(text, req.body.enabled !== "off"));
|
|
saveConfig(db, config);
|
|
req.session.flash = { type: "success", message: "Message added." };
|
|
res.redirect(`/plugins/${PLUGIN_ID}`);
|
|
});
|
|
|
|
router.post("/messages/:id/update", (req, res) => {
|
|
if (!canModerate(req.session.user)) {
|
|
return renderDenied(res);
|
|
}
|
|
const pool = normalizePool(req.body.pool);
|
|
const text = (req.body.text || "").trim();
|
|
const error = validateTemplate(text);
|
|
if (error) {
|
|
req.session.flash = { type: "error", message: error };
|
|
return res.redirect(`/plugins/${PLUGIN_ID}`);
|
|
}
|
|
const config = getConfig(db);
|
|
const message = config[pool].find((item) => item.id === req.params.id);
|
|
if (!message) {
|
|
req.session.flash = { type: "error", message: "Message not found." };
|
|
return res.redirect(`/plugins/${PLUGIN_ID}`);
|
|
}
|
|
message.text = text;
|
|
message.enabled = req.body.enabled === "on";
|
|
message.updatedAt = Date.now();
|
|
saveConfig(db, config);
|
|
req.session.flash = { type: "success", message: "Message updated." };
|
|
res.redirect(`/plugins/${PLUGIN_ID}`);
|
|
});
|
|
|
|
router.post("/messages/:id/duplicate", (req, res) => {
|
|
if (!canModerate(req.session.user)) {
|
|
return renderDenied(res);
|
|
}
|
|
const pool = normalizePool(req.body.pool);
|
|
const config = getConfig(db);
|
|
const source = config[pool].find((item) => item.id === req.params.id);
|
|
if (!source) {
|
|
req.session.flash = { type: "error", message: "Message not found." };
|
|
return res.redirect(`/plugins/${PLUGIN_ID}`);
|
|
}
|
|
config[pool].push(newMessage(source.text, source.enabled));
|
|
saveConfig(db, config);
|
|
req.session.flash = { type: "success", message: "Message duplicated." };
|
|
res.redirect(`/plugins/${PLUGIN_ID}`);
|
|
});
|
|
|
|
router.post("/messages/:id/archive", (req, res) => {
|
|
if (!canModerate(req.session.user)) {
|
|
return renderDenied(res);
|
|
}
|
|
const pool = normalizePool(req.body.pool);
|
|
const config = getConfig(db);
|
|
const message = config[pool].find((item) => item.id === req.params.id);
|
|
if (message) {
|
|
message.archived = true;
|
|
message.enabled = false;
|
|
message.updatedAt = Date.now();
|
|
saveConfig(db, config);
|
|
}
|
|
req.session.flash = { type: "success", message: "Message archived." };
|
|
res.redirect(`/plugins/${PLUGIN_ID}`);
|
|
});
|
|
|
|
router.post("/messages/:id/restore", (req, res) => {
|
|
if (!canModerate(req.session.user)) {
|
|
return renderDenied(res);
|
|
}
|
|
const pool = normalizePool(req.body.pool);
|
|
const config = getConfig(db);
|
|
const message = config[pool].find((item) => item.id === req.params.id);
|
|
if (message) {
|
|
message.archived = false;
|
|
message.updatedAt = Date.now();
|
|
saveConfig(db, config);
|
|
}
|
|
req.session.flash = { type: "success", message: "Message restored." };
|
|
res.redirect(`/plugins/${PLUGIN_ID}`);
|
|
});
|
|
|
|
router.post("/profile/pronouns", (req, res) => {
|
|
const user = req.session.user || null;
|
|
if (!user?.id) {
|
|
return renderDenied(res);
|
|
}
|
|
const pronouns = normalizePronounSet(req.body.pronoun_set);
|
|
savePronouns(db, user.id, pronouns);
|
|
req.session.flash = { type: "success", message: "Pronouns saved." };
|
|
res.redirect("/profile");
|
|
});
|
|
|
|
web.mount(`/plugins/${PLUGIN_ID}`, router, {
|
|
label: "Welcome Messages",
|
|
role: "mod",
|
|
section: "plugins"
|
|
});
|
|
ensureSidebarNavItem(settings);
|
|
|
|
if (typeof web.addProfileSection === "function") {
|
|
web.addProfileSection({
|
|
id: PLUGIN_ID,
|
|
label: "Pronouns",
|
|
view: path.join(__dirname, "views", "profile-pronouns.ejs"),
|
|
order: 40,
|
|
locals: {
|
|
pronounSets: PRONOUN_SETS,
|
|
getPronouns: (userId) => getStoredPronouns(db, userId)
|
|
}
|
|
});
|
|
}
|
|
|
|
attachDiscordListener(discordClient, db);
|
|
logStartupDiagnostics(discordClient);
|
|
}
|
|
};
|
|
|
|
function ensureTables(db) {
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS welcome_message_pronouns (
|
|
user_id TEXT PRIMARY KEY,
|
|
pronoun_set TEXT NOT NULL,
|
|
subject_pronoun TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
`);
|
|
}
|
|
|
|
function ensureConfig(db) {
|
|
const existing = db
|
|
.prepare("SELECT value FROM plugin_settings WHERE plugin_id = ? AND key = ?")
|
|
.get(PLUGIN_ID, CONFIG_KEY);
|
|
if (!existing) {
|
|
saveConfig(db, normalizeConfig({}));
|
|
}
|
|
}
|
|
|
|
function getConfig(db) {
|
|
const row = db
|
|
.prepare("SELECT value FROM plugin_settings WHERE plugin_id = ? AND key = ?")
|
|
.get(PLUGIN_ID, CONFIG_KEY);
|
|
if (!row) {
|
|
return normalizeConfig({});
|
|
}
|
|
try {
|
|
return normalizeConfig(JSON.parse(row.value));
|
|
} catch {
|
|
return normalizeConfig({});
|
|
}
|
|
}
|
|
|
|
function saveConfig(db, config) {
|
|
db.prepare(
|
|
"INSERT INTO plugin_settings (plugin_id, key, value, updated_at) VALUES (?, ?, ?, ?) " +
|
|
"ON CONFLICT(plugin_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at"
|
|
).run(PLUGIN_ID, CONFIG_KEY, JSON.stringify(normalizeConfig(config)), Date.now());
|
|
}
|
|
|
|
function normalizeConfig(config) {
|
|
return {
|
|
enabled: config?.enabled !== false,
|
|
channelId: (config?.channelId || "").toString().trim(),
|
|
welcomeBackEnabled: Boolean(config?.welcomeBackEnabled),
|
|
welcomeMessages: normalizeMessages(config?.welcomeMessages, DEFAULT_WELCOME_MESSAGES),
|
|
welcomeBackMessages: normalizeMessages(
|
|
config?.welcomeBackMessages,
|
|
DEFAULT_WELCOME_BACK_MESSAGES
|
|
)
|
|
};
|
|
}
|
|
|
|
function normalizeMessages(messages, defaults) {
|
|
const source = Array.isArray(messages) && messages.length
|
|
? messages
|
|
: defaults.map((text) => ({ text, enabled: true }));
|
|
return source
|
|
.map((item) => {
|
|
const text = typeof item === "string" ? item : item?.text;
|
|
const now = Date.now();
|
|
return {
|
|
id: item?.id || crypto.randomUUID(),
|
|
text: (text || "").toString(),
|
|
enabled: item?.enabled !== false,
|
|
archived: Boolean(item?.archived),
|
|
createdAt: Number(item?.createdAt) || now,
|
|
updatedAt: Number(item?.updatedAt) || now
|
|
};
|
|
})
|
|
.filter((item) => item.text.trim());
|
|
}
|
|
|
|
function newMessage(text, enabled) {
|
|
const now = Date.now();
|
|
return {
|
|
id: crypto.randomUUID(),
|
|
text,
|
|
enabled: Boolean(enabled),
|
|
archived: false,
|
|
createdAt: now,
|
|
updatedAt: now
|
|
};
|
|
}
|
|
|
|
function normalizePool(pool) {
|
|
return pool === "welcomeBackMessages" ? "welcomeBackMessages" : "welcomeMessages";
|
|
}
|
|
|
|
function validateTemplate(text) {
|
|
if (!text) {
|
|
return "Message text is required.";
|
|
}
|
|
if (text.length > 1800) {
|
|
return "Message text is too long.";
|
|
}
|
|
const matches = text.matchAll(/\{([^{}]+)\}/g);
|
|
for (const match of matches) {
|
|
const name = match[1].trim().toLowerCase();
|
|
if (!ALLOWED_PLACEHOLDERS.has(name)) {
|
|
return `Unknown placeholder: {${match[1]}}.`;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function canModerate(user) {
|
|
return Boolean(user?.isAdmin || user?.isMod);
|
|
}
|
|
|
|
function renderDenied(res) {
|
|
return res.status(403).render("error", {
|
|
title: "Access denied",
|
|
message: "You do not have access to that page."
|
|
});
|
|
}
|
|
|
|
function ensureSidebarNavItem(settings) {
|
|
if (!settings?.getSetting || !settings?.setSetting) {
|
|
return;
|
|
}
|
|
const navId = "plugins_welcome_messages";
|
|
const raw = settings.getSetting("nav_structure", null);
|
|
if (!raw) {
|
|
return;
|
|
}
|
|
let structure = raw;
|
|
if (typeof structure === "string") {
|
|
try {
|
|
structure = JSON.parse(structure);
|
|
} catch {
|
|
return;
|
|
}
|
|
}
|
|
if (!structure?.enabled || !Array.isArray(structure.sections)) {
|
|
return;
|
|
}
|
|
if (structure.sections.some((section) => Array.isArray(section.items) && section.items.includes(navId))) {
|
|
return;
|
|
}
|
|
let pluginsSection = structure.sections.find((section) => section.id === "plugins");
|
|
if (!pluginsSection) {
|
|
pluginsSection = {
|
|
id: "plugins",
|
|
label: "Plugins",
|
|
icon: "blocks",
|
|
items: []
|
|
};
|
|
structure.sections.push(pluginsSection);
|
|
}
|
|
pluginsSection.items = Array.isArray(pluginsSection.items)
|
|
? pluginsSection.items.concat(navId)
|
|
: [navId];
|
|
settings.setSetting("nav_structure", structure);
|
|
}
|
|
|
|
function attachDiscordListener(discordClient, db) {
|
|
if (!discordClient || discordClient[LISTENER_KEY]) {
|
|
if (!discordClient) {
|
|
log("warn", "Welcome Messages listener not attached", {
|
|
reason: "Discord client is unavailable"
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
discordClient[LISTENER_KEY] = true;
|
|
discordClient.on("guildMemberAdd", (member) => {
|
|
handleMemberJoin(member, db).catch((error) => {
|
|
log("error", "Welcome Messages join handler failed", error);
|
|
});
|
|
});
|
|
log("info", "Welcome Messages listener attached", {
|
|
event: "guildMemberAdd",
|
|
memberIntent: detectGuildMembersIntent(discordClient)
|
|
});
|
|
}
|
|
|
|
async function handleMemberJoin(member, db) {
|
|
const config = getConfig(db);
|
|
if (!config.enabled || !config.channelId) {
|
|
log("debug", "Welcome Messages skipped join", {
|
|
reason: !config.enabled ? "Plugin posting disabled" : "No welcome channel configured",
|
|
guildId: member?.guild?.id || null,
|
|
userId: member?.user?.id || member?.id || null
|
|
});
|
|
return;
|
|
}
|
|
const discordId = member?.user?.id || member?.id;
|
|
if (!discordId) {
|
|
log("warn", "Welcome Messages skipped join", {
|
|
reason: "Missing Discord user id",
|
|
guildId: member?.guild?.id || null
|
|
});
|
|
return;
|
|
}
|
|
const existingIdentity = db
|
|
.prepare(
|
|
"SELECT user_id FROM user_identities WHERE provider = 'discord' AND provider_user_id = ?"
|
|
)
|
|
.get(discordId);
|
|
const returning = Boolean(existingIdentity?.user_id);
|
|
const profile = ensureDiscordIdentity(member);
|
|
const channelResult = await validateTextChannel(member.client, config.channelId);
|
|
if (!channelResult.valid || !channelResult.channel) {
|
|
log("warn", "Welcome Messages skipped join", {
|
|
reason: channelResult.message,
|
|
guildId: member?.guild?.id || null,
|
|
userId: discordId,
|
|
channelId: config.channelId
|
|
});
|
|
return;
|
|
}
|
|
const pool =
|
|
returning && config.welcomeBackEnabled
|
|
? config.welcomeBackMessages
|
|
: config.welcomeMessages;
|
|
const message = chooseMessage(pool) || chooseMessage(config.welcomeMessages);
|
|
if (!message) {
|
|
log("warn", "Welcome Messages skipped join", {
|
|
reason: "No enabled message templates",
|
|
guildId: member?.guild?.id || null,
|
|
userId: discordId
|
|
});
|
|
return;
|
|
}
|
|
const pronoun = getSubjectPronoun(db, profile?.id);
|
|
const rendered = renderTemplate(message.text, member, pronoun);
|
|
try {
|
|
await channelResult.channel.send({
|
|
content: rendered,
|
|
allowedMentions: { parse: [] }
|
|
});
|
|
log("info", "Welcome Messages sent welcome message", {
|
|
guildId: member?.guild?.id || null,
|
|
userId: discordId,
|
|
channelId: config.channelId,
|
|
returning,
|
|
pool: returning && config.welcomeBackEnabled ? "welcomeBackMessages" : "welcomeMessages",
|
|
messageId: message.id
|
|
});
|
|
} catch (error) {
|
|
log("error", "Welcome Messages failed to send message", {
|
|
guildId: member?.guild?.id || null,
|
|
userId: discordId,
|
|
channelId: config.channelId,
|
|
error: error?.stack || error?.message || String(error)
|
|
});
|
|
}
|
|
}
|
|
|
|
function ensureDiscordIdentity(member) {
|
|
const user = member.user || {};
|
|
const displayName =
|
|
member.displayName || user.globalName || user.username || user.tag || "Discord User";
|
|
const avatar =
|
|
typeof user.displayAvatarURL === "function"
|
|
? user.displayAvatarURL({ format: "png", size: 128 })
|
|
: null;
|
|
return ensureUserForIdentity({
|
|
provider: "discord",
|
|
providerUserId: user.id || member.id,
|
|
displayName,
|
|
avatar,
|
|
fallbackName: user.username || user.tag || "discord-user"
|
|
});
|
|
}
|
|
|
|
function chooseMessage(messages) {
|
|
const active = (messages || []).filter((item) => item.enabled && !item.archived);
|
|
if (!active.length) {
|
|
return null;
|
|
}
|
|
return active[Math.floor(Math.random() * active.length)];
|
|
}
|
|
|
|
function renderTemplate(template, member, pronoun) {
|
|
const user = member.user || {};
|
|
const username = sanitizeMentionText(user.username || user.tag || "Discord User");
|
|
const displayname = sanitizeMentionText(
|
|
member.displayName || user.globalName || user.username || user.tag || "Discord User"
|
|
);
|
|
const guildmembers =
|
|
member.guild?.memberCount || member.guild?.members?.cache?.size || "unknown";
|
|
const values = { username, displayname, pronoun, guildmembers: String(guildmembers) };
|
|
return template.replace(/\{([^{}]+)\}/g, (full, key) => {
|
|
const normalized = key.trim().toLowerCase();
|
|
return Object.prototype.hasOwnProperty.call(values, normalized)
|
|
? values[normalized]
|
|
: full;
|
|
});
|
|
}
|
|
|
|
function previewMessage(text) {
|
|
return (text || "").replace(/\{([^{}]+)\}/g, (full, key) => {
|
|
const values = {
|
|
username: "lumi_user",
|
|
displayname: "Lumi User",
|
|
pronoun: "they",
|
|
guildmembers: "123"
|
|
};
|
|
return values[key.trim().toLowerCase()] || full;
|
|
});
|
|
}
|
|
|
|
function sanitizeMentionText(value) {
|
|
return (value || "")
|
|
.toString()
|
|
.replace(/@everyone/gi, "@\u200beveryone")
|
|
.replace(/@here/gi, "@\u200bhere")
|
|
.replace(/<@&?\d+>/g, "[mention]")
|
|
.replace(/<#\d+>/g, "[channel]");
|
|
}
|
|
|
|
function getTextChannels(discordClient) {
|
|
const channels = [];
|
|
if (!discordClient?.guilds?.cache) {
|
|
return channels;
|
|
}
|
|
for (const guild of discordClient.guilds.cache.values()) {
|
|
guild.channels?.cache?.forEach((channel) => {
|
|
if (isRegularTextChannel(channel)) {
|
|
channels.push({
|
|
id: channel.id,
|
|
label: `${guild.name} - ${channel.name}`
|
|
});
|
|
}
|
|
});
|
|
}
|
|
return channels.sort((a, b) => a.label.localeCompare(b.label));
|
|
}
|
|
|
|
async function validateTextChannel(discordClient, channelId) {
|
|
if (!discordClient) {
|
|
return { valid: false, message: "Discord client is unavailable." };
|
|
}
|
|
const channel = await resolveChannel(discordClient, channelId);
|
|
if (!channel) {
|
|
return { valid: false, message: "Configured channel was not found." };
|
|
}
|
|
if (!isRegularTextChannel(channel)) {
|
|
return { valid: false, message: "Welcome channel must be a regular text channel." };
|
|
}
|
|
const botMember = await resolveBotMember(channel.guild, discordClient);
|
|
const permissions = botMember ? channel.permissionsFor(botMember) : null;
|
|
const canView = hasPermission(permissions, "VIEW_CHANNEL");
|
|
const canSend = hasPermission(permissions, "SEND_MESSAGES");
|
|
if (permissions && !canView) {
|
|
return { valid: false, message: "Bot cannot view the configured channel.", channel };
|
|
}
|
|
if (permissions && !canSend) {
|
|
return { valid: false, message: "Bot cannot send messages in the configured channel.", channel };
|
|
}
|
|
return { valid: true, message: "Channel is valid.", channel, canView, canSend };
|
|
}
|
|
|
|
function isRegularTextChannel(channel) {
|
|
if (!channel || channel.isThread?.()) {
|
|
return false;
|
|
}
|
|
const type = channel.type;
|
|
return type === "GUILD_TEXT" || type === 0;
|
|
}
|
|
|
|
async function resolveChannel(discordClient, channelId) {
|
|
if (!channelId) {
|
|
return null;
|
|
}
|
|
const cached = discordClient.channels?.cache?.get(channelId) || null;
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
if (typeof discordClient.channels?.fetch === "function") {
|
|
return discordClient.channels.fetch(channelId).catch(() => null);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function resolveBotMember(guild, discordClient) {
|
|
if (!guild || !discordClient?.user?.id) {
|
|
return null;
|
|
}
|
|
return (
|
|
guild.members?.cache?.get(discordClient.user.id) ||
|
|
(typeof guild.members?.fetch === "function"
|
|
? await guild.members.fetch(discordClient.user.id).catch(() => null)
|
|
: null)
|
|
);
|
|
}
|
|
|
|
function hasPermission(permissions, flag) {
|
|
if (!permissions) {
|
|
return false;
|
|
}
|
|
const value = Permissions?.FLAGS?.[flag] || flag;
|
|
try {
|
|
return Boolean(permissions.has(value));
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function buildDiagnostics(discordClient, channelId) {
|
|
const channelValidation = channelId
|
|
? await validateTextChannel(discordClient, channelId)
|
|
: { valid: false, message: "No welcome channel configured." };
|
|
return {
|
|
clientAvailable: Boolean(discordClient),
|
|
ready: Boolean(discordClient?.readyAt),
|
|
memberIntent: detectGuildMembersIntent(discordClient),
|
|
channel: channelValidation
|
|
};
|
|
}
|
|
|
|
function logStartupDiagnostics(discordClient) {
|
|
const memberIntent = detectGuildMembersIntent(discordClient);
|
|
const details = {
|
|
discordClientAvailable: Boolean(discordClient),
|
|
discordClientReady: Boolean(discordClient?.readyAt),
|
|
memberIntent
|
|
};
|
|
if (memberIntent === "configured") {
|
|
log("info", "Welcome Messages diagnostics passed", details);
|
|
return;
|
|
}
|
|
log("warn", "Welcome Messages diagnostics warning", {
|
|
...details,
|
|
message:
|
|
"Guild member join events require the runtime Discord client to start with GuildMembers/GUILD_MEMBERS and the Discord Developer Portal Server Members Intent enabled."
|
|
});
|
|
}
|
|
|
|
function detectGuildMembersIntent(discordClient) {
|
|
if (!discordClient) {
|
|
return "unknown";
|
|
}
|
|
const bitfield = resolveIntentBitfield(discordClient.options?.intents);
|
|
const guildMembersFlag = resolveGuildMembersFlag();
|
|
if (bitfield === null || guildMembersFlag === null) {
|
|
return "unknown";
|
|
}
|
|
return (bitfield & guildMembersFlag) !== 0n ? "configured" : "missing";
|
|
}
|
|
|
|
function resolveIntentBitfield(intents) {
|
|
if (intents === undefined || intents === null) {
|
|
return null;
|
|
}
|
|
if (typeof intents === "number" || typeof intents === "bigint") {
|
|
return BigInt(intents);
|
|
}
|
|
if (Array.isArray(intents)) {
|
|
return intents.reduce((acc, value) => {
|
|
const resolved = resolveIntentBitfield(value);
|
|
return resolved === null ? acc : acc | resolved;
|
|
}, 0n);
|
|
}
|
|
if (intents.bitfield !== undefined && intents.bitfield !== null) {
|
|
return resolveIntentBitfield(intents.bitfield);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function resolveGuildMembersFlag() {
|
|
const value =
|
|
discord.GatewayIntentBits?.GuildMembers ||
|
|
discord.IntentsBitField?.Flags?.GuildMembers ||
|
|
discord.Intents?.FLAGS?.GUILD_MEMBERS;
|
|
return value === undefined || value === null ? null : BigInt(value);
|
|
}
|
|
|
|
function normalizePronounSet(input) {
|
|
const raw = (input || "").toString().trim().toLowerCase();
|
|
const normalized = raw
|
|
.replace(/\s+/g, " ")
|
|
.replace(/\s*\/\s*/g, "/");
|
|
return PRONOUN_SETS.find((item) => item.value === normalized) || PRONOUN_SETS[0];
|
|
}
|
|
|
|
function savePronouns(db, userId, pronouns) {
|
|
const now = Date.now();
|
|
db.prepare(
|
|
"INSERT INTO welcome_message_pronouns (user_id, pronoun_set, subject_pronoun, created_at, updated_at) VALUES (?, ?, ?, ?, ?) " +
|
|
"ON CONFLICT(user_id) DO UPDATE SET pronoun_set = excluded.pronoun_set, subject_pronoun = excluded.subject_pronoun, updated_at = excluded.updated_at"
|
|
).run(userId, pronouns.value, pronouns.subject, now, now);
|
|
}
|
|
|
|
function getStoredPronouns(db, userId) {
|
|
const row = userId
|
|
? db.prepare("SELECT pronoun_set, subject_pronoun FROM welcome_message_pronouns WHERE user_id = ?").get(userId)
|
|
: null;
|
|
if (!row) {
|
|
return PRONOUN_SETS[0];
|
|
}
|
|
return normalizePronounSet(row.pronoun_set);
|
|
}
|
|
|
|
function getSubjectPronoun(db, userId) {
|
|
return getStoredPronouns(db, userId).subject || "they";
|
|
}
|