Add welcome messages plugin and update controls
This commit is contained in:
parent
f877e4f084
commit
6af21664e8
2
.gitignore
vendored
2
.gitignore
vendored
@ -10,3 +10,5 @@ updates/
|
|||||||
*.sqlite
|
*.sqlite
|
||||||
*.sqlite-*
|
*.sqlite-*
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
security-audit-*.json
|
||||||
|
security-audit-*.md
|
||||||
|
|||||||
22
plugins/welcome_messages/README.md
Normal file
22
plugins/welcome_messages/README.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# Welcome Messages
|
||||||
|
|
||||||
|
Standalone Lumi plugin for randomized Discord welcome and welcome-back messages.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Upload `lumi-plugin-welcome_messages-v0.1.0.zip` through **Admin -> Plugins** or
|
||||||
|
**Admin -> Updates**. The zip root must contain `plugin.json` and `index.js`.
|
||||||
|
|
||||||
|
## Discord intent requirement
|
||||||
|
|
||||||
|
This plugin listens for `guildMemberAdd`. Lumi's Discord client must start with
|
||||||
|
the Discord Server Members intent (`GuildMembers` / `GUILD_MEMBERS`), and the
|
||||||
|
Server Members Intent must be enabled in the Discord Developer Portal. If that
|
||||||
|
intent is missing, the plugin loads and the WebUI shows diagnostics, but join
|
||||||
|
events will not fire.
|
||||||
|
|
||||||
|
## Placeholders
|
||||||
|
|
||||||
|
Templates may use only `{username}`, `{displayname}`, `{pronoun}`, and
|
||||||
|
`{guildmembers}`. User-controlled names are sanitized and messages are sent with
|
||||||
|
mentions disabled.
|
||||||
724
plugins/welcome_messages/index.js
Normal file
724
plugins/welcome_messages/index.js
Normal file
@ -0,0 +1,724 @@
|
|||||||
|
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";
|
||||||
|
}
|
||||||
7
plugins/welcome_messages/plugin.json
Normal file
7
plugins/welcome_messages/plugin.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"id": "welcome_messages",
|
||||||
|
"name": "Welcome Messages",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Randomized Discord welcome and welcome-back messages with safe pronoun preferences.",
|
||||||
|
"main": "index.js"
|
||||||
|
}
|
||||||
73
plugins/welcome_messages/views/message-section.ejs
Normal file
73
plugins/welcome_messages/views/message-section.ejs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<section class="card">
|
||||||
|
<h2><%= title %></h2>
|
||||||
|
<form method="post" action="/plugins/welcome_messages/messages/create" class="form-grid">
|
||||||
|
<input type="hidden" name="pool" value="<%= pool %>" />
|
||||||
|
<div class="field full">
|
||||||
|
<label>New message</label>
|
||||||
|
<textarea name="text" rows="3" placeholder="Welcome {displayname}!"></textarea>
|
||||||
|
<p class="hint">Preview uses sample values after saving.</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Enabled</label>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" class="switch-input" name="enabled" checked />
|
||||||
|
<span class="switch-track" aria-hidden="true"></span>
|
||||||
|
<span class="switch-text">On</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field full">
|
||||||
|
<button type="submit" class="button">Add message</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<% if (!messages.length) { %>
|
||||||
|
<p>No messages configured.</p>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="form-grid">
|
||||||
|
<% messages.forEach((message) => { %>
|
||||||
|
<div class="card">
|
||||||
|
<form method="post" action="/plugins/welcome_messages/messages/<%= message.id %>/update" class="form-grid">
|
||||||
|
<input type="hidden" name="pool" value="<%= pool %>" />
|
||||||
|
<div class="field full">
|
||||||
|
<label>Message</label>
|
||||||
|
<textarea name="text" rows="3"><%= message.text %></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Status</label>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" class="switch-input" name="enabled" <%= message.enabled && !message.archived ? "checked" : "" %> <%= message.archived ? "disabled" : "" %> />
|
||||||
|
<span class="switch-track" aria-hidden="true"></span>
|
||||||
|
<span class="switch-text"><%= message.archived ? "Archived" : message.enabled ? "On" : "Off" %></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field full">
|
||||||
|
<p class="hint">Preview: <%= previewMessage(message.text) %></p>
|
||||||
|
</div>
|
||||||
|
<div class="field full">
|
||||||
|
<% if (!message.archived) { %>
|
||||||
|
<button type="submit" class="button">Save</button>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="inline-actions">
|
||||||
|
<form method="post" action="/plugins/welcome_messages/messages/<%= message.id %>/duplicate" class="inline-form">
|
||||||
|
<input type="hidden" name="pool" value="<%= pool %>" />
|
||||||
|
<button type="submit" class="button subtle">Duplicate</button>
|
||||||
|
</form>
|
||||||
|
<% if (message.archived) { %>
|
||||||
|
<form method="post" action="/plugins/welcome_messages/messages/<%= message.id %>/restore" class="inline-form">
|
||||||
|
<input type="hidden" name="pool" value="<%= pool %>" />
|
||||||
|
<button type="submit" class="button">Restore</button>
|
||||||
|
</form>
|
||||||
|
<% } else { %>
|
||||||
|
<form method="post" action="/plugins/welcome_messages/messages/<%= message.id %>/archive" class="inline-form">
|
||||||
|
<input type="hidden" name="pool" value="<%= pool %>" />
|
||||||
|
<button type="submit" class="button danger">Archive</button>
|
||||||
|
</form>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</section>
|
||||||
15
plugins/welcome_messages/views/profile-pronouns.ejs
Normal file
15
plugins/welcome_messages/views/profile-pronouns.ejs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<% const currentPronouns = getPronouns(user.id); %>
|
||||||
|
<form method="post" action="/plugins/welcome_messages/profile/pronouns" class="form-grid">
|
||||||
|
<div class="field">
|
||||||
|
<label>Pronouns for welcome messages</label>
|
||||||
|
<select name="pronoun_set">
|
||||||
|
<% pronounSets.forEach((item) => { %>
|
||||||
|
<option value="<%= item.value %>" <%= item.value === currentPronouns.value ? "selected" : "" %>><%= item.label %></option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
|
<p class="hint">Unsafe or unknown values are normalized to they/them.</p>
|
||||||
|
</div>
|
||||||
|
<div class="field full">
|
||||||
|
<button type="submit" class="button subtle">Save pronouns</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
101
plugins/welcome_messages/views/settings.ejs
Normal file
101
plugins/welcome_messages/views/settings.ejs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<%- include("../../../src/web/views/partials/layout-top", { title }) %>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<div class="section-header">
|
||||||
|
<div>
|
||||||
|
<h1>Welcome Messages</h1>
|
||||||
|
<p class="command-subtitle">Send randomized Discord welcome messages when members join.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="callout">
|
||||||
|
<strong>Allowed placeholders</strong>
|
||||||
|
<p>
|
||||||
|
<% allowedPlaceholders.forEach((name) => { %>
|
||||||
|
<code>{<%= name %>}</code>
|
||||||
|
<% }) %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Diagnostics</h2>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>Discord client</th>
|
||||||
|
<td><%= diagnostics.clientAvailable ? (diagnostics.ready ? "Ready" : "Available, not ready") : "Unavailable" %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Guild members intent</th>
|
||||||
|
<td><%= diagnostics.memberIntent %><% if (diagnostics.memberIntent !== "configured") { %> - join events may not fire<% } %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Welcome channel</th>
|
||||||
|
<td><%= diagnostics.channel.message %></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Delivery</h2>
|
||||||
|
<% if (isAdmin) { %>
|
||||||
|
<form method="post" action="/plugins/welcome_messages/settings" class="form-grid">
|
||||||
|
<div class="field">
|
||||||
|
<label>Posting enabled</label>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" class="switch-input" name="enabled" <%= config.enabled ? "checked" : "" %> />
|
||||||
|
<span class="switch-track" aria-hidden="true"></span>
|
||||||
|
<span class="switch-text"><%= config.enabled ? "On" : "Off" %></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Welcome-back messages</label>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" class="switch-input" name="welcome_back_enabled" <%= config.welcomeBackEnabled ? "checked" : "" %> />
|
||||||
|
<span class="switch-track" aria-hidden="true"></span>
|
||||||
|
<span class="switch-text"><%= config.welcomeBackEnabled ? "On" : "Off" %></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field full">
|
||||||
|
<label>Welcome channel</label>
|
||||||
|
<% if (textChannels.length) { %>
|
||||||
|
<select name="channel_id">
|
||||||
|
<option value="">Select a Discord text channel</option>
|
||||||
|
<% textChannels.forEach((channel) => { %>
|
||||||
|
<option value="<%= channel.id %>" <%= channel.id === config.channelId ? "selected" : "" %>><%= channel.label %></option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
|
<% } else { %>
|
||||||
|
<input name="channel_id" value="<%= config.channelId %>" placeholder="Discord text channel ID" />
|
||||||
|
<% } %>
|
||||||
|
<p class="hint">Only regular text channels are accepted.</p>
|
||||||
|
</div>
|
||||||
|
<div class="field full">
|
||||||
|
<button type="submit" class="button">Save delivery settings</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<% } else { %>
|
||||||
|
<p>Posting is <strong><%= config.enabled ? "enabled" : "disabled" %></strong>.</p>
|
||||||
|
<p>Welcome-back messages are <strong><%= config.welcomeBackEnabled ? "enabled" : "disabled" %></strong>.</p>
|
||||||
|
<p class="hint">Only admins can change delivery settings or the welcome channel.</p>
|
||||||
|
<% } %>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<%- include("./message-section", {
|
||||||
|
title: "Welcome messages",
|
||||||
|
pool: "welcomeMessages",
|
||||||
|
messages: config.welcomeMessages,
|
||||||
|
previewMessage
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
<%- include("./message-section", {
|
||||||
|
title: "Welcome-back messages",
|
||||||
|
pool: "welcomeBackMessages",
|
||||||
|
messages: config.welcomeBackMessages,
|
||||||
|
previewMessage
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
<%- include("../../../src/web/views/partials/layout-bottom") %>
|
||||||
@ -19,6 +19,7 @@ async function startBot({ commandRouter } = {}) {
|
|||||||
const intents = [
|
const intents = [
|
||||||
resolveIntent("Guilds", "GUILDS"),
|
resolveIntent("Guilds", "GUILDS"),
|
||||||
resolveIntent("GuildMessages", "GUILD_MESSAGES"),
|
resolveIntent("GuildMessages", "GUILD_MESSAGES"),
|
||||||
|
resolveIntent("GuildMembers", "GUILD_MEMBERS"),
|
||||||
resolveIntent("MessageContent", "MESSAGE_CONTENT"),
|
resolveIntent("MessageContent", "MESSAGE_CONTENT"),
|
||||||
resolveIntent("GuildVoiceStates", "GUILD_VOICE_STATES"),
|
resolveIntent("GuildVoiceStates", "GUILD_VOICE_STATES"),
|
||||||
resolveIntent("GuildPresences", "GUILD_PRESENCES")
|
resolveIntent("GuildPresences", "GUILD_PRESENCES")
|
||||||
@ -28,6 +29,10 @@ async function startBot({ commandRouter } = {}) {
|
|||||||
if (intents.length) {
|
if (intents.length) {
|
||||||
options.intents = intents;
|
options.intents = intents;
|
||||||
}
|
}
|
||||||
|
console.log("Discord bot starting with intents", {
|
||||||
|
intents,
|
||||||
|
guildMembers: Boolean(resolveIntent("GuildMembers", "GUILD_MEMBERS"))
|
||||||
|
});
|
||||||
if (Partials?.Channel) {
|
if (Partials?.Channel) {
|
||||||
options.partials = [Partials.Channel];
|
options.partials = [Partials.Channel];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,28 @@
|
|||||||
<label>Git branch</label>
|
<label>Git branch</label>
|
||||||
<input name="git_branch" value="<%= settings.git_branch || 'main' %>" />
|
<input name="git_branch" value="<%= settings.git_branch || 'main' %>" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field full">
|
||||||
|
<div class="inline-actions">
|
||||||
|
<button type="submit" class="button">Save settings</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="button subtle"
|
||||||
|
formaction="/admin/check-update"
|
||||||
|
formmethod="post"
|
||||||
|
>
|
||||||
|
Check for updates
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="button subtle"
|
||||||
|
formaction="/admin/update"
|
||||||
|
formmethod="post"
|
||||||
|
>
|
||||||
|
Update from git
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="hint">Git update checks use the configured remote and branch.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field full">
|
<div class="field full">
|
||||||
<h2>Platform Integration</h2>
|
<h2>Platform Integration</h2>
|
||||||
|
|||||||
@ -5,6 +5,19 @@
|
|||||||
<p class="hint">Rollback is handled from Safe Mode if something breaks.</p>
|
<p class="hint">Rollback is handled from Safe Mode if something breaks.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Git updates</h2>
|
||||||
|
<p>Check or pull updates from the remote and branch configured in Settings.</p>
|
||||||
|
<div class="inline-actions">
|
||||||
|
<form method="post" action="/admin/check-update" class="inline-form">
|
||||||
|
<button type="submit" class="button subtle">Check for updates</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/admin/update" class="inline-form">
|
||||||
|
<button type="submit" class="button">Update from git</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2>Upload bot update</h2>
|
<h2>Upload bot update</h2>
|
||||||
<form method="post" action="/admin/updates/bot" enctype="multipart/form-data" class="form-grid">
|
<form method="post" action="/admin/updates/bot" enctype="multipart/form-data" class="form-grid">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user