mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-05-10 12:25:02 +00:00
248 lines
8.7 KiB
TypeScript
248 lines
8.7 KiB
TypeScript
import { GuildMember, MessageActionRow, MessageButton, MessageComponentInteraction, Snowflake } from "discord.js";
|
||
import moment from "moment-timezone";
|
||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||
import { humanizeDurationShort } from "../../../humanizeDurationShort";
|
||
import { getBaseUrl } from "../../../pluginUtils";
|
||
import { DBDateFormat, MINUTES, resolveMember } from "../../../utils";
|
||
import { IMuteWithDetails, mutesCmd } from "../types";
|
||
|
||
export const MutesCmd = mutesCmd({
|
||
trigger: "mutes",
|
||
permission: "can_view_list",
|
||
|
||
signature: {
|
||
age: ct.delay({
|
||
option: true,
|
||
shortcut: "a",
|
||
}),
|
||
|
||
left: ct.switchOption({ def: false, shortcut: "l" }),
|
||
manual: ct.switchOption({ def: false, shortcut: "m" }),
|
||
export: ct.switchOption({ def: false, shortcut: "e" }),
|
||
},
|
||
|
||
async run({ pluginData, message: msg, args }) {
|
||
const listMessagePromise = msg.channel.send("Loading mutes...");
|
||
const mutesPerPage = 10;
|
||
let totalMutes = 0;
|
||
let hasFilters = false;
|
||
|
||
let stopCollectionFn = () => {
|
||
return;
|
||
};
|
||
let stopCollectionTimeout: NodeJS.Timeout;
|
||
const stopCollectionDebounce = 5 * MINUTES;
|
||
|
||
const bumpCollectionTimeout = () => {
|
||
clearTimeout(stopCollectionTimeout);
|
||
stopCollectionTimeout = setTimeout(stopCollectionFn, stopCollectionDebounce);
|
||
};
|
||
|
||
let lines: string[] = [];
|
||
|
||
// Active, logged mutes
|
||
const activeMutes = await pluginData.state.mutes.getActiveMutes();
|
||
activeMutes.sort((a, b) => {
|
||
if (a.expires_at == null && b.expires_at != null) return 1;
|
||
if (b.expires_at == null && a.expires_at != null) return -1;
|
||
if (a.expires_at == null && b.expires_at == null) {
|
||
return a.created_at > b.created_at ? -1 : 1;
|
||
}
|
||
return a.expires_at! > b.expires_at! ? 1 : -1;
|
||
});
|
||
|
||
if (args.manual) {
|
||
// Show only manual mutes (i.e. "Muted" role added without a logged mute)
|
||
const muteUserIds = new Set(activeMutes.map(m => m.user_id));
|
||
const manuallyMutedMembers: GuildMember[] = [];
|
||
const muteRole = pluginData.config.get().mute_role;
|
||
|
||
if (muteRole) {
|
||
pluginData.guild.members.cache.forEach(member => {
|
||
if (muteUserIds.has(member.id)) return;
|
||
if (member.roles.cache.has(muteRole as Snowflake)) manuallyMutedMembers.push(member);
|
||
});
|
||
}
|
||
|
||
totalMutes = manuallyMutedMembers.length;
|
||
|
||
lines = manuallyMutedMembers.map(member => {
|
||
return `<@!${member.id}> (**${member.user.username}#${member.user.discriminator}**, \`${member.id}\`) 🔧 Manual mute`;
|
||
});
|
||
} else {
|
||
// Show filtered active mutes (but not manual mutes)
|
||
let filteredMutes: IMuteWithDetails[] = activeMutes;
|
||
let bannedIds: string[] | null = null;
|
||
|
||
// Filter: mute age
|
||
if (args.age) {
|
||
const cutoff = moment
|
||
.utc()
|
||
.subtract(args.age, "ms")
|
||
.format(DBDateFormat);
|
||
filteredMutes = filteredMutes.filter(m => m.created_at <= cutoff);
|
||
hasFilters = true;
|
||
}
|
||
|
||
// Fetch some extra details for each mute: the muted member, and whether they've been banned
|
||
for (const [index, mute] of filteredMutes.entries()) {
|
||
const muteWithDetails = { ...mute };
|
||
|
||
const member = await resolveMember(pluginData.client, pluginData.guild, mute.user_id);
|
||
|
||
if (!member) {
|
||
if (!bannedIds) {
|
||
const bans = await pluginData.guild.bans.fetch({ cache: true });
|
||
bannedIds = bans.map(u => u.user.id);
|
||
}
|
||
|
||
muteWithDetails.banned = bannedIds.includes(mute.user_id);
|
||
} else {
|
||
muteWithDetails.member = member;
|
||
}
|
||
|
||
filteredMutes[index] = muteWithDetails;
|
||
}
|
||
|
||
// Filter: left the server
|
||
if (args.left != null) {
|
||
filteredMutes = filteredMutes.filter(m => (args.left && !m.member) || (!args.left && m.member));
|
||
hasFilters = true;
|
||
}
|
||
|
||
totalMutes = filteredMutes.length;
|
||
|
||
// Create a message line for each mute
|
||
const caseIds = filteredMutes.map(m => m.case_id).filter(v => !!v);
|
||
const muteCases = caseIds.length ? await pluginData.state.cases.get(caseIds) : [];
|
||
const muteCasesById = muteCases.reduce((map, c) => map.set(c.id, c), new Map());
|
||
|
||
lines = filteredMutes.map(mute => {
|
||
const user = pluginData.client.users.resolve(mute.user_id as Snowflake);
|
||
const username = user ? `${user.username}#${user.discriminator}` : "Unknown#0000";
|
||
const theCase = muteCasesById.get(mute.case_id);
|
||
const caseName = theCase ? `Case #${theCase.case_number}` : "No case";
|
||
|
||
let line = `<@!${mute.user_id}> (**${username}**, \`${mute.user_id}\`) 📋 ${caseName}`;
|
||
|
||
if (mute.expires_at) {
|
||
const timeUntilExpiry = moment.utc().diff(moment.utc(mute.expires_at, DBDateFormat));
|
||
const humanizedTime = humanizeDurationShort(timeUntilExpiry, { largest: 2, round: true });
|
||
line += ` ⏰ Expires in ${humanizedTime}`;
|
||
} else {
|
||
line += ` ⏰ Indefinite`;
|
||
}
|
||
|
||
const timeFromMute = moment.utc(mute.created_at, DBDateFormat).diff(moment.utc());
|
||
const humanizedTimeFromMute = humanizeDurationShort(timeFromMute, { largest: 2, round: true });
|
||
line += ` 🕒 Muted ${humanizedTimeFromMute} ago`;
|
||
|
||
if (mute.banned) {
|
||
line += ` 🔨 Banned`;
|
||
} else if (!mute.member) {
|
||
line += ` ❌ Left server`;
|
||
}
|
||
|
||
return line;
|
||
});
|
||
}
|
||
|
||
const listMessage = await listMessagePromise;
|
||
|
||
let currentPage = 1;
|
||
const totalPages = Math.ceil(lines.length / mutesPerPage);
|
||
|
||
const drawListPage = async page => {
|
||
page = Math.max(1, Math.min(totalPages, page));
|
||
currentPage = page;
|
||
|
||
const pageStart = (page - 1) * mutesPerPage;
|
||
const pageLines = lines.slice(pageStart, pageStart + mutesPerPage);
|
||
|
||
const pageRangeText = `${pageStart + 1}–${pageStart + pageLines.length} of ${totalMutes}`;
|
||
|
||
let message;
|
||
if (args.manual) {
|
||
message = `Showing manual mutes ${pageRangeText}:`;
|
||
} else if (hasFilters) {
|
||
message = `Showing filtered active mutes ${pageRangeText}:`;
|
||
} else {
|
||
message = `Showing active mutes ${pageRangeText}:`;
|
||
}
|
||
|
||
message += "\n\n" + pageLines.join("\n");
|
||
|
||
listMessage.edit(message);
|
||
bumpCollectionTimeout();
|
||
};
|
||
|
||
if (totalMutes === 0) {
|
||
if (args.manual) {
|
||
listMessage.edit("No manual mutes found!");
|
||
} else if (hasFilters) {
|
||
listMessage.edit("No mutes found with the specified filters!");
|
||
} else {
|
||
listMessage.edit("No active mutes!");
|
||
}
|
||
} else if (args.export) {
|
||
const archiveId = await pluginData.state.archives.create(lines.join("\n"), moment.utc().add(1, "hour"));
|
||
const baseUrl = getBaseUrl(pluginData);
|
||
const url = await pluginData.state.archives.getUrl(baseUrl, archiveId);
|
||
|
||
await listMessage.edit(`Exported mutes: ${url}`);
|
||
} else {
|
||
drawListPage(1);
|
||
|
||
if (totalPages > 1) {
|
||
const idMod = `${listMessage.id}:muteList`;
|
||
const buttons: MessageButton[] = [];
|
||
|
||
buttons.push(
|
||
new MessageButton()
|
||
.setStyle("SECONDARY")
|
||
.setEmoji("⬅")
|
||
.setCustomId(`previousButton:${idMod}`),
|
||
);
|
||
|
||
buttons.push(
|
||
new MessageButton()
|
||
.setStyle("SECONDARY")
|
||
.setEmoji("➡")
|
||
.setCustomId(`nextButton:${idMod}`),
|
||
);
|
||
|
||
const row = new MessageActionRow().addComponents(buttons);
|
||
await listMessage.edit({ components: [row] });
|
||
|
||
const filter = (iac: MessageComponentInteraction) => iac.message.id === listMessage.id;
|
||
const collector = listMessage.createMessageComponentCollector({
|
||
filter,
|
||
time: stopCollectionDebounce,
|
||
});
|
||
|
||
collector.on("collect", async (interaction: MessageComponentInteraction) => {
|
||
if (msg.author.id !== interaction.user.id) {
|
||
interaction.reply({ content: `You are not permitted to use these buttons.`, ephemeral: true });
|
||
} else {
|
||
collector.resetTimer();
|
||
if (interaction.customId === `previousButton:${idMod}` && currentPage > 1) {
|
||
await interaction.deferUpdate();
|
||
await drawListPage(currentPage - 1);
|
||
} else if (interaction.customId === `nextButton:${idMod}` && currentPage < totalPages) {
|
||
await interaction.deferUpdate();
|
||
await drawListPage(currentPage + 1);
|
||
} else {
|
||
await interaction.deferUpdate();
|
||
}
|
||
}
|
||
});
|
||
|
||
stopCollectionFn = async () => {
|
||
collector.stop();
|
||
await listMessage.edit({ content: listMessage.content, components: [] });
|
||
};
|
||
bumpCollectionTimeout();
|
||
}
|
||
}
|
||
},
|
||
});
|