From cece2bb957238abf649036cb0eae8fb6480f1495 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 00:30:07 +0000 Subject: [PATCH 1/5] docs: document events feature and expanded permission nodes Updated README.md to include: - A new section for the Events Calendar feature, detailing its lifecycle and features. - Expanded permissions table with missing nodes for events, voting, punishments, and audits. - Detailed documentation for Support Ticket management, including participant logic and category-specific permissions. Co-authored-by: benrobson <15405528+benrobson@users.noreply.github.com> --- README.md | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e26f7d99..05ee5e39 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,13 @@ All permission nodes follow dot-notation and are managed via LuckPerms. Wildcard | `zander.web.scheduler` | Schedule announcements/messages | | `zander.web.vault` | Access vault management | | `zander.web.bridge` | Manage bridge/integrations | +| `zander.web.voting` | Manage voting sites and reward templates | | `zander.web.punishment.view` | View the global punishments list | | `zander.web.punishments` | View punishments on user profiles | +| `zander.web.punishment.manage` | Issue and lift web-based punishments | +| `zander.web.reports` | View player reports on user profiles | +| `zander.web.events` | Access the events dashboard and management | +| `zander.web.events.review` | Access the event approval queue | | `zander.web.audit` | Run audit commands in Discord | | `zander.web.nicknamecheck` | Run nickname check commands | @@ -47,7 +52,7 @@ All permission nodes follow dot-notation and are managed via LuckPerms. Wildcard | `zander.forums.discussion.archive` | Archive forum discussions | | `zander.forums.category.manage` | Manage forum categories (admin dashboard) | -### Discord Punishments +### Discord | Permission Node | Description | |---|---| @@ -56,6 +61,7 @@ All permission nodes follow dot-notation and are managed via LuckPerms. Wildcard | `zander.discord.punish.ban` | Ban/unban users from the Discord server | | `zander.discord.punish.mute` | Mute/unmute users in Discord | | `zander.discord.punish.history` | View punishment history for users | +| `zander.discord.lpaudit` | Run LuckPerms audit commands in Discord | ### Watch / Creator Content @@ -65,6 +71,38 @@ All permission nodes follow dot-notation and are managed via LuckPerms. Wildcard --- +## Events Calendar + +The Events Calendar is a centralized system for scheduling and managing community events across the network and Discord. + +### Event Lifecycle +Events follow a strict state machine to ensure quality and visibility: +1. **Draft**: The initial creation state. Only visible to staff. +2. **Pending Review**: Submitted for approval. Visible in the Review Queue for administrators. +3. **Approved**: Reviewed and ready for publication. +4. **Published**: Visible to the community on the public calendar. Triggers downstream actions. + +### Features +* **Templates**: Define recurring event structures (e.g., Weekly Survival Night) to quickly generate new drafts. +* **Discord Integration**: Publishing an event can automatically create a Discord Guild Event and post a notification message to configured channels. +* **Audit Logging**: Every transition and update is recorded for administrative oversight. + +--- + +## Support Tickets + +The Support Ticket system provides a unified interface for community assistance, integrated with Discord. + +### Managing Participants +Access to a ticket is granted to the **Ticket Owner** and any **Staff** with appropriate category permissions. Additional users or groups can be added to a ticket: +* **Add User**: Grants an individual registered user access to the ticket on the web and the linked Discord channel. +* **Add Group**: Grants all users with a specific LuckPerms rank access to the ticket. +* **Permission logic**: The ability to add/remove participants is available to the Ticket Owner (for non-appeal tickets), Staff members, and existing ticket participants. + +Permissions are automatically synchronized to the linked Discord channel's overwrites whenever participants are modified. + +--- + ## Creator Content Integration (Twitch & YouTube) The `/watch` page displays live streams and recent videos from community members who have linked their Twitch or YouTube accounts and hold the `zander.watch.creator` permission node. Content is only surfaced if it contains a CFC marker (e.g. `#cfc` in a stream title/tag or video tag/description). @@ -158,4 +196,3 @@ Run `migration/v1.11.0_v1.12.0.sql` against your database to create the four tab | `creator_watch_settings` | Per-user toggles controlling listing visibility and Discord notification preferences | | `creator_content_items` | Cached CFC-eligible content items fetched by the cron jobs | | `creator_content_notifications` | Deduplication log preventing repeat Discord notifications for the same content | - From 382b22da6e1b81fdc99237bf0b51c824efee6a2c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 01:17:46 +0000 Subject: [PATCH 2/5] docs: document events and permissions; feat: add ticket participant management permission Updated README.md: - Added "Events Calendar" section detailing lifecycle and features. - Expanded "Permissions" table with missing nodes (events, voting, punishment, reports, Discord audit). - Updated "Support Tickets" section with participant management logic. Code changes in routes/support.js: - Implemented `zander.web.tickets.manageparticipants` permission node. - Users with this node can now view any ticket and manage (add/remove) users and groups. - This permission overrides standard 'Appeal' ticket restrictions for authorized staff/roles. Co-authored-by: benrobson <15405528+benrobson@users.noreply.github.com> --- README.md | 3 ++- routes/support.js | 20 +++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 05ee5e39..e2a897c4 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ All permission nodes follow dot-notation and are managed via LuckPerms. Wildcard | `zander.web.tickets.{slug}` | Access a specific ticket category (dynamic, based on category slug) | | `zander.web.tickets.*` | Access all ticket categories | | `zander.web.ticket.escalate` | Escalate support tickets | +| `zander.web.tickets.manageparticipants` | Add or remove users and groups from any support ticket | ### Forums @@ -97,7 +98,7 @@ The Support Ticket system provides a unified interface for community assistance, Access to a ticket is granted to the **Ticket Owner** and any **Staff** with appropriate category permissions. Additional users or groups can be added to a ticket: * **Add User**: Grants an individual registered user access to the ticket on the web and the linked Discord channel. * **Add Group**: Grants all users with a specific LuckPerms rank access to the ticket. -* **Permission logic**: The ability to add/remove participants is available to the Ticket Owner (for non-appeal tickets), Staff members, and existing ticket participants. +* **Permission logic**: The ability to add/remove participants is available to the Ticket Owner (for non-appeal tickets), Staff members, existing ticket participants, or any user with the `zander.web.tickets.manageparticipants` permission node. Permissions are automatically synchronized to the linked Discord channel's overwrites whenever participants are modified. diff --git a/routes/support.js b/routes/support.js index f7c8f21c..3abbee65 100644 --- a/routes/support.js +++ b/routes/support.js @@ -330,8 +330,9 @@ export default function supportRoutes( participants.groups.some((group) => group.rankSlug === slug) ); const isAppeal = /Appeal #/i.test(ticket.title || ""); - const canManageParticipants = isStaff || !isAppeal; - const canManageTicket = isOwner || isStaff || isParticipantUser || isParticipantRank; + const hasParticipantManagePermission = userHasPermissionNode(permissions, "zander.web.tickets.manageparticipants"); + const canManageParticipants = isStaff || hasParticipantManagePermission || !isAppeal; + const canManageTicket = isOwner || isStaff || isParticipantUser || isParticipantRank || hasParticipantManagePermission; if (!canManageTicket) { return res.redirect("/support"); @@ -412,8 +413,9 @@ export default function supportRoutes( const isParticipantRank = userRankSlugs.some((slug) => participants.groups.some((group) => group.rankSlug === slug) ); + const hasParticipantManagePermission = userHasPermissionNode(permissions, "zander.web.tickets.manageparticipants"); - if (!isOwner && !isStaff && !isParticipantUser && !isParticipantRank) { + if (!isOwner && !isStaff && !isParticipantUser && !isParticipantRank && !hasParticipantManagePermission) { return res.redirect("/support"); } @@ -821,8 +823,9 @@ export default function supportRoutes( const isParticipantRank = userRankSlugs.some((slug) => participants.groups.some((group) => group.rankSlug === slug) ); + const hasParticipantManagePermission = userHasPermissionNode(permissions, "zander.web.tickets.manageparticipants"); - if (!isOwner && !isStaff && !isParticipantUser && !isParticipantRank) { + if (!isOwner && !isStaff && !isParticipantUser && !isParticipantRank && !hasParticipantManagePermission) { return res.redirect("/support"); } @@ -891,8 +894,9 @@ export default function supportRoutes( const isParticipantRank = userRankSlugs.some((slug) => participants.groups.some((group) => group.rankSlug === slug) ); + const hasParticipantManagePermission = userHasPermissionNode(permissions, "zander.web.tickets.manageparticipants"); - if (!isOwner && !isStaff && !isParticipantUser && !isParticipantRank) { + if (!isOwner && !isStaff && !isParticipantUser && !isParticipantRank && !hasParticipantManagePermission) { return res.redirect("/support"); } @@ -956,8 +960,9 @@ export default function supportRoutes( const isParticipantRank = userRankSlugs.some((slug) => participants.groups.some((group) => group.rankSlug === slug) ); + const hasParticipantManagePermission = userHasPermissionNode(permissions, "zander.web.tickets.manageparticipants"); - if (!isOwner && !isStaff && !isParticipantUser && !isParticipantRank) { + if (!isOwner && !isStaff && !isParticipantUser && !isParticipantRank && !hasParticipantManagePermission) { return res.redirect("/support"); } @@ -1034,8 +1039,9 @@ export default function supportRoutes( const isParticipantRank = userRankSlugs.some((slug) => participants.groups.some((group) => group.rankSlug === slug) ); + const hasParticipantManagePermission = userHasPermissionNode(permissions, "zander.web.tickets.manageparticipants"); - if (!isOwner && !isStaff && !isParticipantUser && !isParticipantRank) { + if (!isOwner && !isStaff && !isParticipantUser && !isParticipantRank && !hasParticipantManagePermission) { return res.redirect("/support"); } From f2bf06a1045788dd32bc11cb3506fa39dd974f4d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:27:29 +0000 Subject: [PATCH 3/5] docs: document events and permissions; feat: support ticket manageparticipants node Updates README.md: - Added "Events Calendar" section detailing lifecycle and features. - Expanded "Permissions" table with missing nodes (events, voting, punishment, reports, Discord audit). - Updated "Support Tickets" section with participant management logic. Functional Changes: - Implemented `zander.web.tickets.manageparticipants` permission node in web routes and Discord commands. - Updated `getUserPermissions` in `userController.js` to support lookups via `discordId`, enabling permission checks in Discord. - Refactored `commands/support.mjs` to respect LuckPerms permissions for `add`, `remove`, `status`, `close`, and `manual` subcommands. - Users with `zander.web.tickets.manageparticipants` can now manage ticket participants on both the web and Discord, overriding standard restrictions. Co-authored-by: benrobson <15405528+benrobson@users.noreply.github.com> --- commands/support.mjs | 32 ++++++++++++++++++++++++++------ controllers/userController.js | 19 ++++++++++++++++++- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/commands/support.mjs b/commands/support.mjs index e6bf72af..911dcd17 100644 --- a/commands/support.mjs +++ b/commands/support.mjs @@ -12,6 +12,8 @@ import { EmbedBuilder, } from "discord.js"; import { startTicketFlow } from "../lib/discord/ticketFlow.mjs"; +import { getUserPermissions } from "../controllers/userController.js"; +import { hasPermission as hasLuckPermsPermission } from "../lib/discord/permissions.mjs"; import { addTicketGroupParticipant, addTicketUserParticipant, @@ -209,9 +211,12 @@ export class SupportCommand extends Command { const categoryStaffRoles = await getCategoryPermissions(ticketDetails.categoryId); const member = await interaction.guild.members.fetch(interaction.user.id); + const userLPPermissions = await getUserPermissions({ discordId: interaction.user.id }); + const hasPermission = member.permissions.has(PermissionFlagsBits.ManageChannels) || - member.roles.cache.some((role) => categoryStaffRoles.includes(role.id)); + member.roles.cache.some((role) => categoryStaffRoles.includes(role.id)) || + hasLuckPermsPermission(userLPPermissions, "zander.web.tickets.manageparticipants"); if (!hasPermission) { return interaction.reply({ @@ -345,9 +350,12 @@ export class SupportCommand extends Command { const categoryStaffRoles = await getCategoryPermissions(ticketDetails.categoryId); const member = await interaction.guild.members.fetch(interaction.user.id); + const userLPPermissions = await getUserPermissions({ discordId: interaction.user.id }); + const hasPermission = member.permissions.has(PermissionFlagsBits.ManageChannels) || - member.roles.cache.some((role) => categoryStaffRoles.includes(role.id)); + member.roles.cache.some((role) => categoryStaffRoles.includes(role.id)) || + hasLuckPermsPermission(userLPPermissions, "zander.web.tickets.manageparticipants"); if (!hasPermission) { return interaction.reply({ @@ -447,9 +455,13 @@ export class SupportCommand extends Command { const categoryStaffRoles = await getCategoryPermissions(ticketDetails.categoryId); const member = await interaction.guild.members.fetch(interaction.user.id); + const userLPPermissions = await getUserPermissions({ discordId: interaction.user.id }); + const hasPermission = member.permissions.has(PermissionFlagsBits.ManageChannels) || - member.roles.cache.some((role) => categoryStaffRoles.includes(role.id)); + member.roles.cache.some((role) => categoryStaffRoles.includes(role.id)) || + hasLuckPermsPermission(userLPPermissions, "zander.web.tickets.manageparticipants") || + hasLuckPermsPermission(userLPPermissions, "zander.web.tickets"); if (!hasPermission) { return interaction.reply({ @@ -528,9 +540,12 @@ export class SupportCommand extends Command { const categoryStaffRoles = await getCategoryPermissions(ticketDetails.categoryId); const member = await interaction.guild.members.fetch(interaction.user.id); + const userLPPermissions = await getUserPermissions({ discordId: interaction.user.id }); const isStaff = member.permissions.has(PermissionFlagsBits.ManageChannels) || - member.roles.cache.some((role) => categoryStaffRoles.includes(role.id)); + member.roles.cache.some((role) => categoryStaffRoles.includes(role.id)) || + hasLuckPermsPermission(userLPPermissions, "zander.web.tickets.manageparticipants") || + hasLuckPermsPermission(userLPPermissions, "zander.web.tickets"); const isOwner = ticketDetails.discordId && ticketDetails.discordId === interaction.user.id; if (!isStaff && !isOwner) { @@ -583,9 +598,14 @@ export class SupportCommand extends Command { } if (subcommand === "manual") { - if (!interaction.memberPermissions.has(PermissionFlagsBits.ManageChannels)) { + const userLPPermissions = await getUserPermissions({ discordId: interaction.user.id }); + const hasManualPermission = + interaction.memberPermissions.has(PermissionFlagsBits.ManageChannels) || + hasLuckPermsPermission(userLPPermissions, "zander.web.tickets"); + + if (!hasManualPermission) { return interaction.reply({ - content: "You need Manage Channels permission to create a manual ticket.", + content: "You need Manage Channels or support staff permissions to create a manual ticket.", ephemeral: true, }); } diff --git a/controllers/userController.js b/controllers/userController.js index f62ac2ac..0d02104a 100644 --- a/controllers/userController.js +++ b/controllers/userController.js @@ -558,7 +558,8 @@ export async function getUserPermissions(userData = {}) { const queuedRanks = []; const queuedRankSet = new Set(); - const userId = userData?.userId || null; + let userId = userData?.userId || null; + const discordId = userData?.discordId || null; // rawUuid is the LP-native UUID string (VARCHAR with dashes, e.g. "550e8400-e29b-41d4-a716-446655440000"). // LuckPerms MySQL stores uuid as VARCHAR(36) with dashes, so LP queries must use this value // directly rather than UNHEX(hex-without-dashes), which would produce binary that never matches. @@ -573,6 +574,22 @@ export async function getUserPermissions(userData = {}) { return; } + if (discordId) { + const rows = await runQuery( + `SELECT userId, uuid FROM users WHERE discordId = ? LIMIT 1`, + [discordId] + ); + + if (rows.length) { + userId = rows[0].userId; + if (rows[0].uuid) { + rawUuid = rows[0].uuid; + uuidHex = normaliseUuid(rows[0].uuid); + return; + } + } + } + if (userId) { const rows = await runQuery( `SELECT uuid FROM users WHERE userId = ? LIMIT 1`, From 037ead491d823db36f22a76d49865c63b9135c60 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:50:02 +0000 Subject: [PATCH 4/5] docs: document events/permissions; feat: ticket management node; chore: cleanup logging Documentation: - Added "Events Calendar" section to README.md describing lifecycle and features. - Expanded permissions table with missing administrative nodes. - Documented Support Ticket participant management logic. Functional Changes: - Implemented `zander.web.tickets.manageparticipants` permission node. - Updated `getUserPermissions` to support lookups via `discordId`, enabling permission enforcement in Discord. - Refactored `/ticket` Discord command to use LuckPerms authorization. - Users with the management node can now add/remove users and roles on any ticket across web and Discord. Cleanup: - Removed verbose `console.log` statements from `watchController.js` to reduce noise in cron logs. Co-authored-by: benrobson <15405528+benrobson@users.noreply.github.com> --- controllers/watchController.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/controllers/watchController.js b/controllers/watchController.js index 0a5e997d..49b497b5 100644 --- a/controllers/watchController.js +++ b/controllers/watchController.js @@ -335,7 +335,6 @@ async function hasCreatorPermission(uuid) { [dashedUuid, CREATOR_PERMISSION_NODE] ); if (direct.length > 0) { - console.log(`[Watch] uuid=${dashedUuid}: direct permission grant found.`); return true; } } catch (err) { @@ -352,7 +351,6 @@ async function hasCreatorPermission(uuid) { [dashedUuid] ); groups = groupRows.map((r) => r.grp); - console.log(`[Watch] uuid=${dashedUuid}: LP groups=[${groups.join(", ")}]`); } catch (err) { console.warn(`[Watch] uuid=${dashedUuid}: group-membership LP query failed —`, err.message); } @@ -366,7 +364,6 @@ async function hasCreatorPermission(uuid) { if (playerRow.length > 0 && playerRow[0].primary_group) { const pg = playerRow[0].primary_group; if (!groups.includes(pg)) groups.push(pg); - console.log(`[Watch] uuid=${dashedUuid}: primary_group="${pg}"`); } } catch (err) { console.warn(`[Watch] uuid=${dashedUuid}: luckperms_players query failed —`, err.message); @@ -385,7 +382,6 @@ async function hasCreatorPermission(uuid) { [...groups, CREATOR_PERMISSION_NODE] ); if (groupPerm.length > 0) { - console.log(`[Watch] uuid=${dashedUuid}: permission found on group "${groupPerm[0].name}".`); return true; } } catch (err) { @@ -403,14 +399,12 @@ async function hasCreatorPermission(uuid) { ); const parentGroups = [...new Set(parentRows.map((r) => r.parent))]; if (parentGroups.length > 0) { - console.log(`[Watch] uuid=${dashedUuid}: inherited parent groups=[${parentGroups.join(", ")}]`); const ph2 = parentGroups.map(() => "?").join(", "); const inheritedPerm = await runLpQuery( `SELECT name FROM luckperms_group_permissions WHERE name IN (${ph2}) AND permission=? AND value=1 LIMIT 1`, [...parentGroups, CREATOR_PERMISSION_NODE] ); if (inheritedPerm.length > 0) { - console.log(`[Watch] uuid=${dashedUuid}: permission found on inherited group "${inheritedPerm[0].name}".`); return true; } } @@ -434,15 +428,10 @@ export async function getEligibleCreators(platform) { [platform] ); - console.log(`[Watch] getEligibleCreators(${platform}): ${rows.length} active connection(s) found.`); - const eligible = []; for (const row of rows) { try { - console.log(`[Watch] userId=${row.userId} (${row.username}): uuid=${row.uuid || "(none)"}`); - const hasCreatorPerm = await hasCreatorPermission(row.uuid); - console.log(`[Watch] userId=${row.userId} (${row.username}): ${hasCreatorPerm ? "ELIGIBLE" : "not eligible"} for ${CREATOR_PERMISSION_NODE}`); if (hasCreatorPerm) { eligible.push(row); } @@ -451,7 +440,6 @@ export async function getEligibleCreators(platform) { } } - console.log(`[Watch] getEligibleCreators(${platform}): ${eligible.length}/${rows.length} creator(s) eligible.`); return eligible; } From dec4ad0ca245acbac4bb53aa4b3ec892c734c67c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:40:39 +0000 Subject: [PATCH 5/5] docs: document events and permissions; feat: implement ticket participant management; fix: discord interaction timeouts and permission resolution Documentation: - Added comprehensive documentation for the Events Calendar and Support Ticket systems in README.md. - Expanded the permissions reference table with missing administrative nodes. Functional Improvements: - Implemented the `zander.web.tickets.manageparticipants` permission node across web and Discord. - Updated `getUserPermissions` to support lookups via `discordId`, enabling permission enforcement in Discord slash commands. - Refactored Discord support commands to immediately defer interactions, preventing "Unknown interaction" errors during async operations. - Resolved deprecation warnings by migrating `ephemeral: true` to `flags: [MessageFlags.Ephemeral]`. Cleanup: - Removed verbose `console.log` statements from the watch feature to reduce cron log noise. Co-authored-by: benrobson <15405528+benrobson@users.noreply.github.com> --- commands/support.mjs | 58 ++++++++++++++--------------------- controllers/userController.js | 1 - lib/discord/ticketFlow.mjs | 19 ++++++------ 3 files changed, 33 insertions(+), 45 deletions(-) diff --git a/commands/support.mjs b/commands/support.mjs index 911dcd17..eacce0fe 100644 --- a/commands/support.mjs +++ b/commands/support.mjs @@ -10,6 +10,7 @@ import { ActionRowBuilder, ChannelType, EmbedBuilder, + MessageFlags, } from "discord.js"; import { startTicketFlow } from "../lib/discord/ticketFlow.mjs"; import { getUserPermissions } from "../controllers/userController.js"; @@ -190,22 +191,22 @@ export class SupportCommand extends Command { } if (subcommand === "add") { + await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); + const userOption = interaction.options.getUser("user"); const roleOption = interaction.options.getRole("role"); if (!userOption && !roleOption) { - return interaction.reply({ + return interaction.editReply({ content: "Provide a user or role to add to this ticket.", - ephemeral: true, }); } const ticketDetails = await getTicketDetailsByChannel(interaction.channel.id); if (!ticketDetails) { - return interaction.reply({ + return interaction.editReply({ content: "This channel is not linked to a ticket.", - ephemeral: true, }); } @@ -219,14 +220,11 @@ export class SupportCommand extends Command { hasLuckPermsPermission(userLPPermissions, "zander.web.tickets.manageparticipants"); if (!hasPermission) { - return interaction.reply({ + return interaction.editReply({ content: "You need support staff permissions to update ticket access.", - ephemeral: true, }); } - await interaction.deferReply({ ephemeral: true }); - const additions = []; const staffUserId = await getUserIdByDiscordId(interaction.user.id); @@ -329,22 +327,22 @@ export class SupportCommand extends Command { } if (subcommand === "remove") { + await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); + const userOption = interaction.options.getUser("user"); const roleOption = interaction.options.getRole("role"); if (!userOption && !roleOption) { - return interaction.reply({ + return interaction.editReply({ content: "Provide a user or role to remove from this ticket.", - ephemeral: true, }); } const ticketDetails = await getTicketDetailsByChannel(interaction.channel.id); if (!ticketDetails) { - return interaction.reply({ + return interaction.editReply({ content: "This channel is not linked to a ticket.", - ephemeral: true, }); } @@ -358,14 +356,11 @@ export class SupportCommand extends Command { hasLuckPermsPermission(userLPPermissions, "zander.web.tickets.manageparticipants"); if (!hasPermission) { - return interaction.reply({ + return interaction.editReply({ content: "You need support staff permissions to update ticket access.", - ephemeral: true, }); } - await interaction.deferReply({ ephemeral: true }); - const removals = []; const staffUserId = await getUserIdByDiscordId(interaction.user.id); @@ -442,14 +437,14 @@ export class SupportCommand extends Command { } if (subcommand === "status") { + await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); const state = interaction.options.getString("state", true); const ticketDetails = await getTicketDetailsByChannel(interaction.channel.id); if (!ticketDetails) { - return interaction.reply({ + return interaction.editReply({ content: "This channel is not linked to a ticket.", - ephemeral: true, }); } @@ -464,14 +459,11 @@ export class SupportCommand extends Command { hasLuckPermsPermission(userLPPermissions, "zander.web.tickets"); if (!hasPermission) { - return interaction.reply({ + return interaction.editReply({ content: "You need support staff permissions to update ticket status.", - ephemeral: true, }); } - await interaction.deferReply({ ephemeral: true }); - const username = interaction.user.tag; const staffUserId = await getUserIdByDiscordId(interaction.user.id); const statusMessages = { @@ -529,12 +521,13 @@ export class SupportCommand extends Command { } if (subcommand === "close") { + await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); + const ticketDetails = await getTicketDetailsByChannel(interaction.channel.id); if (!ticketDetails) { - return interaction.reply({ + return interaction.editReply({ content: "This channel is not linked to a ticket.", - ephemeral: true, }); } @@ -549,14 +542,11 @@ export class SupportCommand extends Command { const isOwner = ticketDetails.discordId && ticketDetails.discordId === interaction.user.id; if (!isStaff && !isOwner) { - return interaction.reply({ + return interaction.editReply({ content: "Only ticket staff or the ticket owner can close this ticket.", - ephemeral: true, }); } - await interaction.deferReply({ ephemeral: true }); - const username = interaction.user.tag; const actorUserId = await getUserIdByDiscordId(interaction.user.id); @@ -598,15 +588,16 @@ export class SupportCommand extends Command { } if (subcommand === "manual") { + await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); + const userLPPermissions = await getUserPermissions({ discordId: interaction.user.id }); const hasManualPermission = interaction.memberPermissions.has(PermissionFlagsBits.ManageChannels) || hasLuckPermsPermission(userLPPermissions, "zander.web.tickets"); if (!hasManualPermission) { - return interaction.reply({ + return interaction.editReply({ content: "You need Manage Channels or support staff permissions to create a manual ticket.", - ephemeral: true, }); } @@ -631,14 +622,11 @@ export class SupportCommand extends Command { } if (!ownerUserId) { - return interaction.reply({ + return interaction.editReply({ content: "Unable to link your account to a ticket record. Please try again.", - ephemeral: true, }); } - await interaction.deferReply({ ephemeral: true }); - let targetUserId = await getUserIdByDiscordId(targetUser.id); if (!targetUserId) { @@ -855,7 +843,7 @@ export class SupportCommand extends Command { content: `Posted a Create Ticket panel in ${targetChannel} using the ${ ticketCategory ? `\`${ticketCategory.name}\`` : "default" } ticket category.`, - ephemeral: true, + flags: [MessageFlags.Ephemeral], }); } } diff --git a/controllers/userController.js b/controllers/userController.js index 0d02104a..98d81714 100644 --- a/controllers/userController.js +++ b/controllers/userController.js @@ -585,7 +585,6 @@ export async function getUserPermissions(userData = {}) { if (rows[0].uuid) { rawUuid = rows[0].uuid; uuidHex = normaliseUuid(rows[0].uuid); - return; } } } diff --git a/lib/discord/ticketFlow.mjs b/lib/discord/ticketFlow.mjs index 5f051544..dd6934f2 100644 --- a/lib/discord/ticketFlow.mjs +++ b/lib/discord/ticketFlow.mjs @@ -7,6 +7,7 @@ import { ButtonStyle, ComponentType, EmbedBuilder, + MessageFlags, ModalBuilder, PermissionFlagsBits, StringSelectMenuBuilder, @@ -36,7 +37,7 @@ export async function startTicketFlow(interaction, { parentCategoryId = null } = : null; if (!interaction.deferred && !interaction.replied) { - await interaction.deferReply({ ephemeral: true }); + await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); } const categories = await getSupportCategories(); @@ -149,11 +150,11 @@ export async function startTicketFlow(interaction, { parentCategoryId = null } = time: 60000, }); } catch { - return selection.followUp({ content: "Ticket creation timed out.", ephemeral: true }); + return selection.followUp({ content: "Ticket creation timed out.", flags: [MessageFlags.Ephemeral] }); } if (!modalInteraction.deferred && !modalInteraction.replied) { - await modalInteraction.deferReply({ ephemeral: true }); + await modalInteraction.deferReply({ flags: [MessageFlags.Ephemeral] }); } const subject = modalInteraction.fields.getTextInputValue("subject"); @@ -243,7 +244,7 @@ export async function handleTicketClose(interaction) { if (!ticketDetails) { return interaction.reply({ content: "This channel is not linked to a ticket.", - ephemeral: true, + flags: [MessageFlags.Ephemeral], }); } @@ -257,7 +258,7 @@ export async function handleTicketClose(interaction) { if (!isStaff && !isOwner) { return interaction.reply({ content: "Only ticket staff or the ticket owner can close this ticket.", - ephemeral: true, + flags: [MessageFlags.Ephemeral], }); } @@ -276,7 +277,7 @@ export async function handleTicketClose(interaction) { content: "Are you sure you want to close this ticket? The channel will lock and delete 5 seconds after confirmation.", components: [confirmRow], - ephemeral: true, + flags: [MessageFlags.Ephemeral], }); } @@ -286,7 +287,7 @@ export async function handleTicketCloseConfirmation(interaction) { if (channelId && channelId !== interaction.channel.id) { return interaction.reply({ content: "This close confirmation does not match this channel.", - ephemeral: true, + flags: [MessageFlags.Ephemeral], }); } @@ -325,7 +326,7 @@ async function performTicketClose(interaction) { if (!ticketDetails) { return interaction.followUp({ content: "This channel is not linked to a ticket.", - ephemeral: true, + flags: [MessageFlags.Ephemeral], }); } @@ -339,7 +340,7 @@ async function performTicketClose(interaction) { if (!isStaff && !isOwner) { return interaction.followUp({ content: "Only ticket staff or the ticket owner can close this ticket.", - ephemeral: true, + flags: [MessageFlags.Ephemeral], }); }