Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@

+ `/help` now has a **Privacy policy** button, and its footer says that problems and data requests go through a
Support ticket.
+ `/help` only lists the commands you can run.
+ Bot-admin commands (all of Management, `/links` and `/whois`) only show for members with a bot-admin role.
+ Commands that need a Discord permission only show for members who have it.
+ Every page says so: "Missing a command? You only see the ones your permissions in this server allow."

#### Documentation

Expand Down
1 change: 1 addition & 0 deletions DISCORD_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ bullets, `> ` quotes. Tables, `+` bullets and links with titles do not render.
- The bridge has its own `/link` again, separate from the community bot's. If you were linked, your link was copied over, so you don't need to do anything.
- Feature requests (`/request`) are on the community bot.
- `/help` links our new privacy policy. Open a Support ticket to report a problem or ask for your data to be removed.
- `/help` now only shows the commands you can use. Missing one? You don't have the permission for it.
- `/link` now tells you what linking means: your messages here go to guild chat, and a global profile change can show them under another linked member's name.

### Moderation
Expand Down
3 changes: 3 additions & 0 deletions docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,9 @@ chat.
everything the bot is responsible for.
- **`/help`** — every command, one page per category, with ◀ / ▶ buttons. The categories are the folder names
under `src/commands/`, so adding a folder adds a page. Commands needing a Discord permission are marked ⛔.
It only lists what the member can run: commands marked `adminOnly` show only to bot-admin roles, and
`permissionsRequired` must be met (Administrator passes). Every page ends with "Missing a command? You only
see the ones your permissions in this server allow."
The buttons stop working after five minutes. Every page links the
[privacy policy](https://thehypixelguardians.mintlify.site/privacy) and says that problems and data requests
go through a Support ticket: Discord requires an app to tell users where to report problems and ask for
Expand Down
84 changes: 71 additions & 13 deletions src/commands/information/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ const {
} = require("discord.js");
const path = require("path");
const getAllFiles = require("../../utils/getAllFiles");
const { isAdmin } = require("../../utils/adminRoles");

/** The privacy policy for both THG bots, linked from every help page. */
const PRIVACY_URL = "https://thehypixelguardians.mintlify.site/privacy";

/** Shown on every page, since /help leaves out what the member can't run. */
const MISSING_COMMAND_HINT =
"-# Missing a command? You only see the ones your permissions in this server allow.";

/**
* Converts a camelCase folder name into a readable title.
* e.g. "chatScore" → "Chat Score", "admin" → "Admin"
Expand All @@ -34,9 +39,36 @@ function requiresAdminPermission(command) {
return command.permissionsRequired.some((perm) => adminPerms.includes(perm));
}

/**
* Whether a viewer may see a command in /help: bot-admin-only commands need a
* bot-admin role, and `permissionsRequired` must all be held (Administrator
* passes; outside a server, `permissions` is null and only commands without
* any show). Mirrors the checks the commands themselves run.
*
* @param {{ adminOnly?: boolean, permissionsRequired?: bigint[] }} command
* @param {{ botAdmin: boolean, permissions: { has(p: bigint): boolean } | null }} viewer
*/
function canSeeCommand(command, viewer) {
if (command.adminOnly && !viewer.botAdmin) return false;
if (!command.permissionsRequired?.length) return true;
if (!viewer.permissions) return false;
if (viewer.permissions.has(PermissionFlagsBits.Administrator)) return true;
return command.permissionsRequired.every((perm) =>
viewer.permissions.has(perm),
);
}

/** What decides which commands a member sees in /help. */
async function viewerOf(interaction) {
return {
botAdmin: await isAdmin(interaction.member),
permissions: interaction.memberPermissions ?? null,
};
}

/**
* Builds an array of page objects, one per command category.
* Each page has { category: string, lines: string[] }.
* Each page has { category: string, entries: { line, adminOnly, permissionsRequired }[] }.
* Result is cached so the filesystem is only scanned once per process.
*/
let cachedPages = null;
Expand All @@ -52,13 +84,17 @@ function buildHelpPages() {
const categoryName = path.basename(categoryPath);
const readableName = parseCategoryName(categoryName);
const commandFiles = getAllFiles(categoryPath).sort();
const lines = [];
const entries = [];

for (const commandFile of commandFiles) {
const command = require(commandFile);
if (command.deleted) continue;

const adminEmoji = requiresAdminPermission(command) ? " ⛔" : "";
const access = {
adminOnly: Boolean(command.adminOnly),
permissionsRequired: command.permissionsRequired,
};
const subcommands = (command.options || []).filter(
(opt) =>
opt.type === ApplicationCommandOptionType.Subcommand ||
Expand All @@ -67,36 +103,55 @@ function buildHelpPages() {

if (subcommands.length > 0) {
for (const sub of subcommands) {
lines.push(
`\`/${command.name} ${sub.name}\` —${adminEmoji} ${sub.description}`,
);
entries.push({
...access,
line: `\`/${command.name} ${sub.name}\` —${adminEmoji} ${sub.description}`,
});
}
} else {
lines.push(
`\`/${command.name}\` —${adminEmoji} ${command.description}`,
);
entries.push({
...access,
line: `\`/${command.name}\` —${adminEmoji} ${command.description}`,
});
}
}

if (lines.length > 0) {
pages.push({ category: readableName, lines });
if (entries.length > 0) {
pages.push({ category: readableName, entries });
}
}

cachedPages = pages;
return cachedPages;
}

/**
* The pages one viewer gets: { category, lines }, without the commands they
* can't see and without categories left empty.
*/
function helpPagesFor(viewer, pages = buildHelpPages()) {
return pages
.map((page) => ({
category: page.category,
lines: page.entries
.filter((entry) => canSeeCommand(entry, viewer))
.map((entry) => entry.line),
}))
.filter((page) => page.lines.length > 0);
}

/**
* Constructs the embed for the given page index.
*/
function buildEmbed(pages, pageIndex) {
const page = pages[pageIndex];

const hint = `\n\n${MISSING_COMMAND_HINT}`;
let description = page.lines.join("\n");
if (description.length > 4096) {
description = description.substring(0, 4093) + "...";
if (description.length + hint.length > 4096) {
description = description.substring(0, 4093 - hint.length) + "...";
}
description += hint;

return new EmbedBuilder()
.setTitle(`📖 Help — ${page.category}`)
Expand Down Expand Up @@ -138,7 +193,7 @@ module.exports = {
description: "Browse all available commands by category.",

callback: async (client, interaction) => {
const pages = buildHelpPages();
const pages = helpPagesFor(await viewerOf(interaction));

if (pages.length === 0) {
return interaction.reply({
Expand Down Expand Up @@ -172,6 +227,9 @@ module.exports = {
},

buildHelpPages,
helpPagesFor,
viewerOf,
canSeeCommand,
buildEmbed,
buildButtons,
PRIVACY_URL,
Expand Down
2 changes: 2 additions & 0 deletions src/commands/linking/links.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const { getAllLinks } = require("../../utils/linkedAccounts");
module.exports = {
name: "links",
description: "List every Minecraft account linked on the bridge.",
// Bot-admin roles only (checked in the callback); /help lists it only for them.
adminOnly: true,

callback: async (client, interaction) => {
if (!(await isAdmin(interaction.member))) {
Expand Down
2 changes: 2 additions & 0 deletions src/commands/linking/whois.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ module.exports = {
name: "whois",
description:
"Look up a bridge account link by Discord user or Minecraft username.",
// Bot-admin roles only (checked in the callback); /help lists it only for them.
adminOnly: true,
options: [
{
name: "user",
Expand Down
2 changes: 2 additions & 0 deletions src/commands/management/adminpanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const { mainView } = require("../../utils/adminPanel");
module.exports = {
name: "adminpanel",
description: "Open the admin panel to run and configure admin functions.",
// Bot-admin roles only (checked in the callback); /help lists it only for them.
adminOnly: true,

callback: async (client, interaction) => {
if (!(await isAdmin(interaction.member))) {
Expand Down
2 changes: 2 additions & 0 deletions src/commands/management/auditchannel.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const SCOPED_GUILD_OPTION = {
module.exports = {
name: "auditchannel",
description: "Configure the channel admin panel actions are recorded in.",
// Bot-admin roles only (checked in the callback); /help lists it only for them.
adminOnly: true,
options: [
{
name: "set",
Expand Down
2 changes: 2 additions & 0 deletions src/commands/management/blacklist.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ module.exports = {
name: "blacklist",
description:
"Block a member from both bots' commands and from the chat bridge.",
// Bot-admin roles only (checked in the callback); /help lists it only for them.
adminOnly: true,
options: [
{
name: "add",
Expand Down
2 changes: 2 additions & 0 deletions src/commands/management/demote.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const {
module.exports = {
name: "demote",
description: "Demote a member in a Hypixel guild.",
// Bot-admin roles only (checked in the callback); /help lists it only for them.
adminOnly: true,
options: [
{
name: "username",
Expand Down
2 changes: 2 additions & 0 deletions src/commands/management/guilds.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ function beginSignIn(guildKey, interaction) {
module.exports = {
name: "guilds",
description: "Manage the Hypixel guilds this bot bridges.",
// Bot-admin roles only (checked in the callback); /help lists it only for them.
adminOnly: true,
options: [
{
name: "list",
Expand Down
2 changes: 2 additions & 0 deletions src/commands/management/invite.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const {
module.exports = {
name: "invite",
description: "Invite a player to a Hypixel guild.",
// Bot-admin roles only (checked in the callback); /help lists it only for them.
adminOnly: true,
options: [
{
name: "username",
Expand Down
2 changes: 2 additions & 0 deletions src/commands/management/kick.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const {
module.exports = {
name: "kick",
description: "Kick a member from a Hypixel guild.",
// Bot-admin roles only (checked in the callback); /help lists it only for them.
adminOnly: true,
options: [
{
name: "username",
Expand Down
2 changes: 2 additions & 0 deletions src/commands/management/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ async function waitForSpawn(record) {
module.exports = {
name: "login",
description: "Connect the bot to Hypixel.",
// Bot-admin roles only (checked in the callback); /help lists it only for them.
adminOnly: true,
options: [
{
...GUILD_OPTION,
Expand Down
2 changes: 2 additions & 0 deletions src/commands/management/mute.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const {
module.exports = {
name: "mute",
description: "Mute a member in a Hypixel guild's chat.",
// Bot-admin roles only (checked in the callback); /help lists it only for them.
adminOnly: true,
options: [
{
name: "username",
Expand Down
2 changes: 2 additions & 0 deletions src/commands/management/promote.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const {
module.exports = {
name: "promote",
description: "Give a member a rank, in-game and on Discord.",
// Bot-admin roles only (checked in the callback); /help lists it only for them.
adminOnly: true,
options: [
{
name: "user",
Expand Down
2 changes: 2 additions & 0 deletions src/commands/management/send.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const {
module.exports = {
name: "send",
description: "Send a command or message to the Minecraft server.",
// Bot-admin roles only (checked in the callback); /help lists it only for them.
adminOnly: true,
options: [
{
name: "message",
Expand Down
2 changes: 2 additions & 0 deletions src/commands/management/unmute.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const {
module.exports = {
name: "unmute",
description: "Unmute a member in a Hypixel guild's chat.",
// Bot-admin roles only (checked in the callback); /help lists it only for them.
adminOnly: true,
options: [
{
name: "username",
Expand Down
8 changes: 6 additions & 2 deletions src/events/discord/interactionCreate/handleHelpButtons.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const {
buildHelpPages,
helpPagesFor,
viewerOf,
buildEmbed,
buildButtons,
} = require("../../../commands/information/help");
Expand Down Expand Up @@ -48,7 +49,10 @@ module.exports = async (client, interaction) => {
});
}

const pages = buildHelpPages();
const pages = helpPagesFor(await viewerOf(interaction));
if (pages.length === 0) {
return interaction.update({ components: [] });
}
const newPage = Math.max(
0,
Math.min(
Expand Down
Loading
Loading