Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
296 changes: 159 additions & 137 deletions src/commands/general/wiki.js
Original file line number Diff line number Diff line change
@@ -1,159 +1,181 @@
const {
ActionRowBuilder,
StringSelectMenuBuilder,
StringSelectMenuOptionBuilder,
SlashCommandBuilder,
DiscordjsError,
hyperlink,
hideLinkEmbed,
} = require("discord.js");
const { replyOrEditReply } = require("../../utilities");
const chokidar = require("chokidar");
const fs = require("fs");

const mainLogger = require("../../logger");
const logger = mainLogger.child({ service: "wiki" });
ActionRowBuilder,
StringSelectMenuBuilder,
StringSelectMenuOptionBuilder,
SlashCommandBuilder,
DiscordjsError,
} = require('discord.js');
const { replyOrEditReply } = require('../../utilities');
const chokidar = require('chokidar');
const fs = require('fs');

const mainLogger = require('../../logger');
const logger = mainLogger.child({ service: 'wiki' });

let selectMenu;
let menuItems;

function loadMenuItems() {
logger.debug(`Loading menu items from ${process.env.WIKI_ITEMS_PATH}`);
try {
menuItems = JSON.parse(
fs.readFileSync(process.env.WIKI_ITEMS_PATH, "utf8")
);

// Build the menu
selectMenu = new StringSelectMenuBuilder()
.setCustomId("wiki-selector")
.setPlaceholder("Select a wiki topic");

menuItems.forEach((item) => {
selectMenu.addOptions(
new StringSelectMenuOptionBuilder()
.setLabel(item.label)
.setDescription(item.description)
.setValue(item.value)
);
});
} catch (err) {
logger.error(
`Failed to load wiki menu items from ${process.env.WIKI_ITEMS_PATH}: ${err.message}`,
err
);
}
logger.debug(`Loading menu items from ${process.env.WIKI_ITEMS_PATH}`);
try {
menuItems = JSON.parse(
fs.readFileSync(process.env.WIKI_ITEMS_PATH, 'utf8'),
);

// Build the menu
selectMenu = new StringSelectMenuBuilder()
.setCustomId('wiki-selector')
.setPlaceholder('Select a wiki topic');

menuItems.forEach((item) => {
const option = new StringSelectMenuOptionBuilder()
.setLabel(item.label)
.setValue(item.value);

// Support both old format (description) and new format (content array)
if (item.description) {
option.setDescription(item.description);
}
else if (item.content && item.content.length > 0) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract code to function createDescription

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit fe1b647. Extracted the description generation logic to a createDescription helper function that strips markdown formatting from content lines.

// For new format, use first line of content as description (up to 100 chars)
const description = item.content[0].replace(/[*_~`#]/g, '').substring(0, 100);
option.setDescription(description);
}

selectMenu.addOptions(option);
});
}
catch (err) {
logger.error(
`Failed to load wiki menu items from ${process.env.WIKI_ITEMS_PATH}: ${err.message}`,
err,
);
}
}

function watchForMenuChanges() {
// Start watching for file changes
try {
chokidar
.watch(process.env.WIKI_ITEMS_PATH, {
awaitWriteFinish: true,
})
.on("change", loadMenuItems);
logger.debug(`Watching for changes in ${process.env.WIKI_ITEMS_PATH}`);
} catch (e) {
logger.error(
`Unable to watch for changes to ${process.env.WIKI_ITEMS_PATH}: ${e}`
);
}
// Start watching for file changes
try {
chokidar
.watch(process.env.WIKI_ITEMS_PATH, {
awaitWriteFinish: true,
})
.on('change', loadMenuItems);
logger.debug(`Watching for changes in ${process.env.WIKI_ITEMS_PATH}`);
}
catch (e) {
logger.error(
`Unable to watch for changes to ${process.env.WIKI_ITEMS_PATH}: ${e}`,
);
}
}

// Prompts the user to pick a wiki topic from the dropdown.
// This function will throw an error if anything goes wrong.
async function promptForTopic(interaction) {
const row = new ActionRowBuilder().addComponents(selectMenu);
const row = new ActionRowBuilder().addComponents(selectMenu);

// Send the menu
const menu = await interaction.reply({
content: "Select a topic",
components: [row],
ephemeral: true,
});
// Send the menu
const menu = await interaction.reply({
content: 'Select a topic',
components: [row],
ephemeral: true,
});

// Wait for the menu response
const collectorFilter = (i) => i.user.id === interaction.user.id;
// Wait for the menu response
const collectorFilter = (i) => i.user.id === interaction.user.id;

const confirmation = await menu.awaitMessageComponent({
filter: collectorFilter,
time: 60_000,
});
const confirmation = await menu.awaitMessageComponent({
filter: collectorFilter,
time: 60_000,
});

return confirmation.values[0];
return confirmation.values[0];
}

module.exports = {
init: () => {
loadMenuItems();
watchForMenuChanges();
},
cooldown: 5,
data: new SlashCommandBuilder()
.setName("wiki")
.setDescription("Links to wiki topics")
.addStringOption((option) =>
option
.setName("topic")
.setDescription("The name of the wiki topic to send")
.setRequired(false)
),
async execute(interaction) {
try {
// Check and see if a topic was provided on the command.
let topic;
topic = interaction.options.getString("topic") ?? null;

if (topic === null) {
topic = await promptForTopic(interaction);
}

// Find the selected item
const selectedItem = menuItems.find((item) => item.value === topic);

if (selectedItem === undefined) {
await replyOrEditReply(interaction, {
content: `No wiki entry for ${topic} found`,
ephemeral: true,
});
return;
}

const link = hyperlink(selectedItem.description, selectedItem.href);
const preamble =
selectedItem.preamble ??
"Check out the following link for more information:";

await replyOrEditReply(interaction, {
content: `Link sent!`,
components: [],
ephemeral: true,
});

await interaction.channel.send({
content: `${preamble} ${link}`,
});
} catch (error) {
// Errors from the user not responding to the dropdown in time don't log,
// they're just too noisy.
if (
error instanceof DiscordjsError &&
error.code === "InteractionCollectorError"
) {
await replyOrEditReply(interaction, {
content: `No response received, canceling sending the wiki link`,
components: [],
ephemeral: true,
});
} else {
logger.error(`Unable to send wiki link: ${error}`, error);
await replyOrEditReply(interaction, {
content: `Unable to send wiki link: ${error}`,
components: [],
ephemeral: true,
});
}
}
},
init: () => {
loadMenuItems();
watchForMenuChanges();
},
cooldown: 5,
data: new SlashCommandBuilder()
.setName('wiki')
.setDescription('Links to wiki topics')
.addStringOption((option) =>
option
.setName('topic')
.setDescription('The name of the wiki topic to send')
.setRequired(false),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we changing all strings to single quote? I am not aware of any style guide about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The eslint config (.eslint.json) specifies "quotes": ["error", "single"] which automatically converts double quotes to single quotes when running eslint --fix. However, I've now reverted to the original file and kept the double quotes to match the existing codebase style.

),
async execute(interaction) {
try {
// Check and see if a topic was provided on the command.
let topic;
topic = interaction.options.getString('topic') ?? null;

if (topic === null) {
topic = await promptForTopic(interaction);
}

// Find the selected item
const selectedItem = menuItems.find((item) => item.value === topic);

if (selectedItem === undefined) {
await replyOrEditReply(interaction, {
content: `No wiki entry for ${topic} found`,
ephemeral: true,
});
return;
}

// Build the message content based on format
let messageContent;
if (selectedItem.content) {
// New format: use content array
messageContent = selectedItem.content.join('\n');
}
else {
// Legacy format: use preamble and href (for backward compatibility)
const { hyperlink } = require('discord.js');
const link = hyperlink(selectedItem.description, selectedItem.href);
const preamble =
selectedItem.preamble ??
'Check out the following link for more information:';
messageContent = `${preamble} ${link}`;
}

await replyOrEditReply(interaction, {
content: 'Link sent!',
components: [],
ephemeral: true,
});

await interaction.channel.send({
content: messageContent,
});
}
catch (error) {
// Errors from the user not responding to the dropdown in time don't log,
// they're just too noisy.
if (
error instanceof DiscordjsError &&
error.code === 'InteractionCollectorError'
) {
await replyOrEditReply(interaction, {
content: 'No response received, canceling sending the wiki link',
components: [],
ephemeral: true,
});
}
else {
logger.error(`Unable to send wiki link: ${error}`, error);
await replyOrEditReply(interaction, {
content: `Unable to send wiki link: ${error}`,
components: [],
ephemeral: true,
});
}
}
},
};
Loading