feat: summarise ban status in user embed
vi v@vt3e.cat
Wed, 04 Mar 2026 21:10:13 +0000
3 files changed,
81 insertions(+),
18 deletions(-)
M
src/interactions/commands/roblox/getBanHistory.ts
→
src/interactions/commands/roblox/getBanHistory.ts
@@ -1,4 +1,5 @@
import { + type ButtonInteraction, type ChatInputCommandInteraction, type Client, type GuildMember,@@ -11,9 +12,9 @@ import type { RobloxUserId } from "@/roblox/types";
import { hasManagerPermissions } from "@/utils/permissions"; import { constructBansContainer, + constructBansSummaryContainer, constructUserContainer, } from "@/utils/profile"; -import { processRobloxUserInteraction } from "@/utils/robloxUser"; const commandData = new SlashCommandBuilder() .setName("ban-history")@@ -185,7 +186,24 @@ );
} } +async function buttonExecute(_client: Client, interaction: ButtonInteraction) { + const [, action, userId] = interaction.customId.split(":"); + + const isModerator = await hasManagerPermissions( + interaction.member as GuildMember, + ); + const { container } = await constructBansSummaryContainer(userId, { + showPrivate: isModerator, + }); + + await interaction.reply({ + components: [container], + flags: ["IsComponentsV2"], + }); +} + export default { data: commandData, + buttonExecute, execute, };
M
src/utils/profile.ts
→
src/utils/profile.ts
@@ -1,4 +1,4 @@
-import config from "@/config"; +import config, { type Project } from "@/config"; import { roblox } from "@/roblox/client"; import type { GetUserResponse,@@ -135,6 +135,7 @@ startMs: number;
endMs: number; nowActive: boolean; universeId?: string; + project?: Project; }; type UserRestrictionLogWithUid = UserRestrictionLog & {@@ -154,6 +155,9 @@ startMs,
endMs, nowActive, ...(log.universeId ? { universeId: log.universeId } : {}), + project: Object.values(config.projects).find( + (p) => p.universe === log.universeId, + ), }; }); }@@ -166,11 +170,7 @@ includeProjectLabel?: string | null;
includeStatus?: boolean; } = {}, ) { - const { - showPrivate = false, - includeProjectLabel = null, - includeStatus = false, - } = opts; + const { showPrivate = false, includeProjectLabel = null } = opts; const reason = log.displayReason || "no public reason"; const moderator = await moderatorMention(log.moderator);@@ -194,9 +194,6 @@
const lines = [ includeProjectLabel ? `**${includeProjectLabel}**` : undefined, `> Banned **${bannedFor}**, ${startRel}, by **${moderator}**`, - includeStatus - ? `> **Status:** ${log.active ? "Active" : "Expired"}` - : undefined, `> **Reason:** ${reason}`, showPrivate ? `> **Private Reason:** ${log.privateReason || "no private reason"}`@@ -257,6 +254,31 @@ );
return { container, total: bans.length }; } +export async function getBans(userId: RobloxUserId) { + const universeIds = Object.values(config.projects) + .map((p) => p.universe) + .filter((id): id is string => Boolean(id)); + + const perUniverseLogs = await Promise.all( + universeIds.map(async (universeId) => { + const [logs] = await roblox.getUserRestrictionLogs(universeId, userId); + const arr = logs ?? []; + return arr.map( + (log) => + ({ + ...log, + universeId, + project: Object.values(config.projects).find( + (p) => p.universe === universeId, + ), + }) as EnrichedLog, + ); + }), + ); + + return perUniverseLogs; +} + export async function constructBansSummaryContainer( userId: string, opts: { showPrivate?: boolean } = {},@@ -318,11 +340,24 @@ // ---- user -------------------------------------------------------------------
export async function constructUserContainer( profile: GetUserResponse, extras?: { - bans?: UserRestrictionLog[] | null; + bans?: EnrichedLog[] | null; thumbnailUri?: string | null; showPrivate?: boolean; + originCommand?: string; }, ) { + const lastBans = extras?.bans + ?.sort( + (a, b) => + new Date(b.createTime).getTime() - new Date(a.createTime).getTime(), + ) + .slice(0, 3); + + const banSummary = + lastBans && lastBans.length > 0 + ? `Currently banned in ${[...new Set(lastBans.map((b) => b.project?.displayName))].join(", ")}` + : "No recent bans"; + const createdAt = new Date(profile.createTime); const createdAtMs = Math.round(createdAt.getTime() / 1000); const createdAtString = `<t:${createdAtMs}> (<t:${createdAtMs}:R>)`;@@ -340,6 +375,7 @@ ["### About", profile.about ? profile.about : "-# No bio provided"].join(
"\n", ), ); + const banText = text([`-# ${banSummary}`].join("\n")); const section = new SectionBuilder().addTextDisplayComponents(bodyText); const icon = thumbnail(extras?.thumbnailUri ?? undefined);@@ -348,15 +384,23 @@
const mainContainer = new ContainerBuilder() .addSectionComponents(section) .addSeparatorComponents(new SeparatorBuilder()) - .addTextDisplayComponents(aboutText); + .addTextDisplayComponents(aboutText) + .addTextDisplayComponents(banText); const profileButton = new ButtonBuilder() .setLabel("View Profile") .setStyle(ButtonStyle.Link) .setURL(profileUrl); + const bansButton = new ButtonBuilder() + .setLabel("View Bans") + .setStyle(ButtonStyle.Primary) + .setCustomId(`ban-history:summary:${profile.id}`); mainContainer.addActionRowComponents( - new ActionRowBuilder<ButtonBuilder>().addComponents(profileButton), + new ActionRowBuilder<ButtonBuilder>().addComponents( + profileButton, + bansButton, + ), ); return { mainContainer, buttons: [profileButton] };
M
src/utils/robloxUser.ts
→
src/utils/robloxUser.ts
@@ -3,6 +3,7 @@ import type { RobloxUserId } from "@/roblox/types";
import { constructBansSummaryContainer, constructUserContainer, + getBans, } from "@/utils/profile"; import type { ChatInputCommandInteraction,@@ -49,19 +50,19 @@ const message =
randomMessages[Math.floor(Math.random() * randomMessages.length)]; await interaction.editReply(message); + const bans = await getBans(id); + const { mainContainer } = await constructUserContainer(user, { thumbnailUri, - showPrivate: isModerator, - }); - - const banSummary = await constructBansSummaryContainer(id, { + bans: bans.flat(), showPrivate: isModerator, + originCommand: interaction.commandName, }); await interaction.editReply(user.id); await interaction.followUp({ flags: ["IsComponentsV2"], - components: [mainContainer, banSummary.container], + components: [mainContainer], }); } catch (err) { console.error(err);