mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-07-08 03:27:20 +00:00
feat: update to djs 14.19.3, node 22, zod 4
This commit is contained in:
parent
595e1a0556
commit
09eb8e92f2
189 changed files with 1244 additions and 900 deletions
|
@ -15,7 +15,7 @@ import { AboutCmd } from "./commands/AboutCmd.js";
|
|||
import { AvatarCmd } from "./commands/AvatarCmd.js";
|
||||
import { BanSearchCmd } from "./commands/BanSearchCmd.js";
|
||||
import { ChannelInfoCmd } from "./commands/ChannelInfoCmd.js";
|
||||
import { CleanCmd, cleanCmd } from "./commands/CleanCmd.js";
|
||||
import { CleanCmd } from "./commands/CleanCmd.js";
|
||||
import { ContextCmd } from "./commands/ContextCmd.js";
|
||||
import { EmojiInfoCmd } from "./commands/EmojiInfoCmd.js";
|
||||
import { HelpCmd } from "./commands/HelpCmd.js";
|
||||
|
@ -43,6 +43,8 @@ import { hasPermission } from "./functions/hasPermission.js";
|
|||
import { activeReloads } from "./guildReloads.js";
|
||||
import { refreshMembersIfNeeded } from "./refreshMembers.js";
|
||||
import { UtilityPluginType, zUtilityConfig } from "./types.js";
|
||||
import { cleanMessages } from "./functions/cleanMessages.js";
|
||||
import { fetchChannelMessagesToClean } from "./functions/fetchChannelMessagesToClean.js";
|
||||
|
||||
const defaultOptions: PluginOptions<UtilityPluginType> = {
|
||||
config: {
|
||||
|
@ -158,7 +160,8 @@ export const UtilityPlugin = guildPlugin<UtilityPluginType>()({
|
|||
|
||||
public(pluginData) {
|
||||
return {
|
||||
clean: makePublicFn(pluginData, cleanCmd),
|
||||
fetchChannelMessagesToClean: makePublicFn(pluginData, fetchChannelMessagesToClean),
|
||||
cleanMessages: makePublicFn(pluginData, cleanMessages),
|
||||
userInfo: (userId: Snowflake) => getUserInfoEmbed(pluginData, userId, false),
|
||||
hasPermission: makePublicFn(pluginData, hasPermission),
|
||||
};
|
||||
|
|
|
@ -1,62 +1,14 @@
|
|||
import { Message, ModalSubmitInteraction, Snowflake, TextChannel, User } from "discord.js";
|
||||
import { GuildPluginData } from "knub";
|
||||
import { allowTimeout } from "../../../RegExpRunner.js";
|
||||
import { Message, Snowflake } from "discord.js";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
||||
import { LogType } from "../../../data/LogType.js";
|
||||
import { SavedMessage } from "../../../data/entities/SavedMessage.js";
|
||||
import { humanizeDurationShort } from "../../../humanizeDuration.js";
|
||||
import { getBaseUrl } from "../../../pluginUtils.js";
|
||||
import { ContextResponse, deleteContextResponse } from "../../../pluginUtils.js";
|
||||
import { ModActionsPlugin } from "../../../plugins/ModActions/ModActionsPlugin.js";
|
||||
import { DAYS, SECONDS, chunkArray, getInviteCodesInString, noop } from "../../../utils.js";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
|
||||
import { UtilityPluginType, utilityCmd } from "../types.js";
|
||||
import { SECONDS, noop } from "../../../utils.js";
|
||||
import { cleanMessages } from "../functions/cleanMessages.js";
|
||||
import { fetchChannelMessagesToClean } from "../functions/fetchChannelMessagesToClean.js";
|
||||
import { utilityCmd } from "../types.js";
|
||||
|
||||
const MAX_CLEAN_COUNT = 300;
|
||||
const MAX_CLEAN_TIME = 1 * DAYS;
|
||||
const MAX_CLEAN_API_REQUESTS = 20;
|
||||
const CLEAN_COMMAND_DELETE_DELAY = 10 * SECONDS;
|
||||
|
||||
export async function cleanMessages(
|
||||
pluginData: GuildPluginData<UtilityPluginType>,
|
||||
channel: TextChannel,
|
||||
savedMessages: SavedMessage[],
|
||||
mod: User,
|
||||
) {
|
||||
pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE, savedMessages[0].id);
|
||||
pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE_BULK, savedMessages[0].id);
|
||||
|
||||
// Delete & archive in ID order
|
||||
savedMessages = Array.from(savedMessages).sort((a, b) => (a.id > b.id ? 1 : -1));
|
||||
const idsToDelete = savedMessages.map((m) => m.id) as Snowflake[];
|
||||
|
||||
// Make sure the deletions aren't double logged
|
||||
idsToDelete.forEach((id) => pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE, id));
|
||||
pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE_BULK, idsToDelete[0]);
|
||||
|
||||
// Actually delete the messages (in chunks of 100)
|
||||
|
||||
const chunks = chunkArray(idsToDelete, 100);
|
||||
await Promise.all(
|
||||
chunks.map((chunk) =>
|
||||
Promise.all([channel.bulkDelete(chunk), pluginData.state.savedMessages.markBulkAsDeleted(chunk)]),
|
||||
),
|
||||
);
|
||||
|
||||
// Create an archive
|
||||
const archiveId = await pluginData.state.archives.createFromSavedMessages(savedMessages, pluginData.guild);
|
||||
const baseUrl = getBaseUrl(pluginData);
|
||||
const archiveUrl = pluginData.state.archives.getUrl(baseUrl, archiveId);
|
||||
|
||||
pluginData.getPlugin(LogsPlugin).logClean({
|
||||
mod,
|
||||
channel,
|
||||
count: savedMessages.length,
|
||||
archiveUrl,
|
||||
});
|
||||
|
||||
return { archiveUrl };
|
||||
}
|
||||
|
||||
const opts = {
|
||||
user: ct.userId({ option: true, shortcut: "u" }),
|
||||
channel: ct.channelId({ option: true, shortcut: "c" }),
|
||||
|
@ -67,188 +19,6 @@ const opts = {
|
|||
"to-id": ct.anyId({ option: true, shortcut: "id" }),
|
||||
};
|
||||
|
||||
export interface CleanArgs {
|
||||
count: number;
|
||||
update?: boolean;
|
||||
user?: string;
|
||||
channel?: string;
|
||||
bots?: boolean;
|
||||
"delete-pins"?: boolean;
|
||||
"has-invites"?: boolean;
|
||||
match?: RegExp;
|
||||
"to-id"?: string;
|
||||
"response-interaction"?: ModalSubmitInteraction;
|
||||
}
|
||||
|
||||
export async function cleanCmd(pluginData: GuildPluginData<UtilityPluginType>, args: CleanArgs | any, msg) {
|
||||
if (args.count > MAX_CLEAN_COUNT || args.count <= 0) {
|
||||
void pluginData.state.common.sendErrorMessage(
|
||||
msg,
|
||||
`Clean count must be between 1 and ${MAX_CLEAN_COUNT}`,
|
||||
undefined,
|
||||
args["response-interaction"],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetChannel = args.channel ? pluginData.guild.channels.cache.get(args.channel as Snowflake) : msg.channel;
|
||||
if (!targetChannel?.isTextBased()) {
|
||||
void pluginData.state.common.sendErrorMessage(
|
||||
msg,
|
||||
`Invalid channel specified`,
|
||||
undefined,
|
||||
args["response-interaction"],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetChannel.id !== msg.channel.id) {
|
||||
const configForTargetChannel = await pluginData.config.getMatchingConfig({
|
||||
userId: msg.author.id,
|
||||
member: msg.member,
|
||||
channelId: targetChannel.id,
|
||||
categoryId: targetChannel.parentId,
|
||||
});
|
||||
if (configForTargetChannel.can_clean !== true) {
|
||||
void pluginData.state.common.sendErrorMessage(
|
||||
msg,
|
||||
`Missing permissions to use clean on that channel`,
|
||||
undefined,
|
||||
args["response-interaction"],
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let cleaningMessage: Message | undefined = undefined;
|
||||
if (!args["response-interaction"]) {
|
||||
cleaningMessage = await msg.channel.send("Cleaning...");
|
||||
}
|
||||
|
||||
const messagesToClean: Message[] = [];
|
||||
let beforeId = msg.id;
|
||||
const timeCutoff = msg.createdTimestamp - MAX_CLEAN_TIME;
|
||||
const upToMsgId = args["to-id"];
|
||||
let foundId = false;
|
||||
|
||||
const deletePins = args["delete-pins"] != null ? args["delete-pins"] : false;
|
||||
let pinIds: Set<Snowflake> = new Set();
|
||||
if (!deletePins) {
|
||||
pinIds = new Set((await msg.channel.messages.fetchPinned()).keys());
|
||||
}
|
||||
|
||||
let note: string | null = null;
|
||||
let requests = 0;
|
||||
while (messagesToClean.length < args.count) {
|
||||
const potentialMessages = await targetChannel.messages.fetch({
|
||||
before: beforeId,
|
||||
limit: 100,
|
||||
});
|
||||
if (potentialMessages.size === 0) break;
|
||||
|
||||
requests++;
|
||||
|
||||
const filtered: Message[] = [];
|
||||
for (const message of potentialMessages.values()) {
|
||||
const contentString = message.content || "";
|
||||
if (args.user && message.author.id !== args.user) continue;
|
||||
if (args.bots && !message.author.bot) continue;
|
||||
if (!deletePins && pinIds.has(message.id)) continue;
|
||||
if (args["has-invites"] && getInviteCodesInString(contentString).length === 0) continue;
|
||||
if (upToMsgId != null && message.id < upToMsgId) {
|
||||
foundId = true;
|
||||
break;
|
||||
}
|
||||
if (message.createdTimestamp < timeCutoff) continue;
|
||||
if (args.match && !(await pluginData.state.regexRunner.exec(args.match, contentString).catch(allowTimeout))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
filtered.push(message);
|
||||
}
|
||||
const remaining = args.count - messagesToClean.length;
|
||||
const withoutOverflow = filtered.slice(0, remaining);
|
||||
messagesToClean.push(...withoutOverflow);
|
||||
|
||||
beforeId = potentialMessages.lastKey()!;
|
||||
|
||||
if (foundId) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (messagesToClean.length < args.count) {
|
||||
if (potentialMessages.last()!.createdTimestamp < timeCutoff) {
|
||||
note = `stopped looking after reaching ${humanizeDurationShort(MAX_CLEAN_TIME)} old messages`;
|
||||
break;
|
||||
}
|
||||
|
||||
if (requests >= MAX_CLEAN_API_REQUESTS) {
|
||||
note = `stopped looking after ${requests * 100} messages`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let responseMsg: Message | undefined;
|
||||
if (messagesToClean.length > 0) {
|
||||
// Save to-be-deleted messages that were missing from the database
|
||||
const existingStored = await pluginData.state.savedMessages.getMultiple(messagesToClean.map((m) => m.id));
|
||||
const alreadyStored = existingStored.map((stored) => stored.id);
|
||||
const messagesToStore = messagesToClean.filter((potentialMsg) => !alreadyStored.includes(potentialMsg.id));
|
||||
await pluginData.state.savedMessages.createFromMessages(messagesToStore);
|
||||
|
||||
const savedMessagesToClean = await pluginData.state.savedMessages.getMultiple(messagesToClean.map((m) => m.id));
|
||||
const cleanResult = await cleanMessages(pluginData, targetChannel, savedMessagesToClean, msg.author);
|
||||
|
||||
let responseText = `Cleaned ${messagesToClean.length} ${messagesToClean.length === 1 ? "message" : "messages"}`;
|
||||
if (note) {
|
||||
responseText += ` (${note})`;
|
||||
}
|
||||
if (targetChannel.id !== msg.channel.id) {
|
||||
responseText += ` in <#${targetChannel.id}>: ${cleanResult.archiveUrl}`;
|
||||
}
|
||||
|
||||
if (args.update) {
|
||||
const modActions = pluginData.getPlugin(ModActionsPlugin);
|
||||
const channelId = targetChannel.id !== msg.channel.id ? targetChannel.id : msg.channel.id;
|
||||
const updateMessage = `Cleaned ${messagesToClean.length} ${
|
||||
messagesToClean.length === 1 ? "message" : "messages"
|
||||
} in <#${channelId}>: ${cleanResult.archiveUrl}`;
|
||||
if (typeof args.update === "number") {
|
||||
modActions.updateCase(msg, args.update, updateMessage);
|
||||
} else {
|
||||
modActions.updateCase(msg, null, updateMessage);
|
||||
}
|
||||
}
|
||||
|
||||
responseMsg = await pluginData.state.common.sendSuccessMessage(
|
||||
msg,
|
||||
responseText,
|
||||
undefined,
|
||||
args["response-interaction"],
|
||||
);
|
||||
} else {
|
||||
const responseText = `Found no messages to clean${note ? ` (${note})` : ""}!`;
|
||||
responseMsg = await pluginData.state.common.sendErrorMessage(
|
||||
msg,
|
||||
responseText,
|
||||
undefined,
|
||||
args["response-interaction"],
|
||||
);
|
||||
}
|
||||
|
||||
cleaningMessage?.delete();
|
||||
|
||||
if (targetChannel.id === msg.channel.id) {
|
||||
// Delete the !clean command and the bot response if a different channel wasn't specified
|
||||
// (so as not to spam the cleaned channel with the command itself)
|
||||
msg.delete().catch(noop);
|
||||
setTimeout(() => {
|
||||
responseMsg?.delete().catch(noop);
|
||||
}, CLEAN_COMMAND_DELETE_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
export const CleanCmd = utilityCmd({
|
||||
trigger: ["clean", "clear"],
|
||||
description: "Remove a number of recent messages",
|
||||
|
@ -271,6 +41,108 @@ export const CleanCmd = utilityCmd({
|
|||
],
|
||||
|
||||
async run({ message: msg, args, pluginData }) {
|
||||
cleanCmd(pluginData, args, msg);
|
||||
const targetChannel = args.channel ? pluginData.guild.channels.cache.get(args.channel as Snowflake) : msg.channel;
|
||||
if (!targetChannel?.isTextBased()) {
|
||||
void pluginData.state.common.sendErrorMessage(
|
||||
msg,
|
||||
`Invalid channel specified`,
|
||||
undefined,
|
||||
args["response-interaction"],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetChannel.id !== msg.channel.id) {
|
||||
const configForTargetChannel = await pluginData.config.getMatchingConfig({
|
||||
userId: msg.author.id,
|
||||
member: msg.member,
|
||||
channelId: targetChannel.id,
|
||||
categoryId: targetChannel.parentId,
|
||||
});
|
||||
if (configForTargetChannel.can_clean !== true) {
|
||||
void pluginData.state.common.sendErrorMessage(
|
||||
msg,
|
||||
`Missing permissions to use clean on that channel`,
|
||||
undefined,
|
||||
args["response-interaction"],
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let cleaningMessage: Message | undefined = undefined;
|
||||
if (!args["response-interaction"]) {
|
||||
cleaningMessage = await msg.channel.send("Cleaning...");
|
||||
}
|
||||
|
||||
const fetchMessagesResult = await fetchChannelMessagesToClean(pluginData, targetChannel, {
|
||||
beforeId: msg.id,
|
||||
count: args.count,
|
||||
authorId: args.user,
|
||||
includePins: args["delete-pins"],
|
||||
onlyBotMessages: args.bots,
|
||||
onlyWithInvites: args["has-invites"],
|
||||
upToId: args["to-id"],
|
||||
matchContent: args.match,
|
||||
});
|
||||
if ("error" in fetchMessagesResult) {
|
||||
void pluginData.state.common.sendErrorMessage(msg, fetchMessagesResult.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const { messages: messagesToClean, note } = fetchMessagesResult;
|
||||
|
||||
let responseMsg: ContextResponse | null = null;
|
||||
if (messagesToClean.length > 0) {
|
||||
const cleanResult = await cleanMessages(pluginData, targetChannel, messagesToClean, msg.author);
|
||||
|
||||
let responseText = `Cleaned ${messagesToClean.length} ${messagesToClean.length === 1 ? "message" : "messages"}`;
|
||||
if (note) {
|
||||
responseText += ` (${note})`;
|
||||
}
|
||||
if (targetChannel.id !== msg.channel.id) {
|
||||
responseText += ` in <#${targetChannel.id}>: ${cleanResult.archiveUrl}`;
|
||||
}
|
||||
|
||||
if (args.update) {
|
||||
const modActions = pluginData.getPlugin(ModActionsPlugin);
|
||||
const channelId = targetChannel.id !== msg.channel.id ? targetChannel.id : msg.channel.id;
|
||||
const updateMessage = `Cleaned ${messagesToClean.length} ${
|
||||
messagesToClean.length === 1 ? "message" : "messages"
|
||||
} in <#${channelId}>: ${cleanResult.archiveUrl}`;
|
||||
if (typeof args.update === "number") {
|
||||
modActions.updateCase(msg, args.update, updateMessage);
|
||||
} else {
|
||||
modActions.updateCase(msg, null, updateMessage);
|
||||
}
|
||||
}
|
||||
|
||||
responseMsg = await pluginData.state.common.sendSuccessMessage(
|
||||
msg,
|
||||
responseText,
|
||||
undefined,
|
||||
args["response-interaction"],
|
||||
);
|
||||
} else {
|
||||
const responseText = `Found no messages to clean${note ? ` (${note})` : ""}!`;
|
||||
responseMsg = await pluginData.state.common.sendErrorMessage(
|
||||
msg,
|
||||
responseText,
|
||||
undefined,
|
||||
args["response-interaction"],
|
||||
);
|
||||
}
|
||||
|
||||
cleaningMessage?.delete();
|
||||
|
||||
if (targetChannel.id === msg.channel.id) {
|
||||
// Delete the !clean command and the bot response if a different channel wasn't specified
|
||||
// (so as not to spam the cleaned channel with the command itself)
|
||||
msg.delete().catch(noop);
|
||||
setTimeout(() => {
|
||||
deleteContextResponse(responseMsg).catch(noop);
|
||||
responseMsg?.delete().catch(noop);
|
||||
}, CLEAN_COMMAND_DELETE_DELAY);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -3,6 +3,7 @@ import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
|||
import { messageLink } from "../../../utils.js";
|
||||
import { canReadChannel } from "../../../utils/canReadChannel.js";
|
||||
import { utilityCmd } from "../types.js";
|
||||
import { resolveMessageMember } from "../../../pluginUtils.js";
|
||||
|
||||
export const ContextCmd = utilityCmd({
|
||||
trigger: "context",
|
||||
|
@ -29,7 +30,8 @@ export const ContextCmd = utilityCmd({
|
|||
const channel = args.channel ?? args.message.channel;
|
||||
const messageId = args.messageId ?? args.message.messageId;
|
||||
|
||||
if (!canReadChannel(channel, msg.member)) {
|
||||
const authorMember = await resolveMessageMember(msg);
|
||||
if (!canReadChannel(channel, authorMember)) {
|
||||
void pluginData.state.common.sendErrorMessage(msg, "Message context not found");
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import { getServerInfoEmbed } from "../functions/getServerInfoEmbed.js";
|
|||
import { getSnowflakeInfoEmbed } from "../functions/getSnowflakeInfoEmbed.js";
|
||||
import { getUserInfoEmbed } from "../functions/getUserInfoEmbed.js";
|
||||
import { utilityCmd } from "../types.js";
|
||||
import { resolveMessageMember } from "../../../pluginUtils.js";
|
||||
|
||||
export const InfoCmd = utilityCmd({
|
||||
trigger: "info",
|
||||
|
@ -77,7 +78,8 @@ export const InfoCmd = utilityCmd({
|
|||
if (userCfg.can_messageinfo) {
|
||||
const messageTarget = await resolveMessageTarget(pluginData, value);
|
||||
if (messageTarget) {
|
||||
if (canReadChannel(messageTarget.channel, message.member)) {
|
||||
const authorMember = await resolveMessageMember(message);
|
||||
if (canReadChannel(messageTarget.channel, authorMember)) {
|
||||
const embed = await getMessageInfoEmbed(pluginData, messageTarget.channel.id, messageTarget.messageId);
|
||||
if (embed) {
|
||||
message.channel.send({ embeds: [embed] });
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
||||
import { resolveMessageMember } from "../../../pluginUtils.js";
|
||||
import { canReadChannel } from "../../../utils/canReadChannel.js";
|
||||
import { getMessageInfoEmbed } from "../functions/getMessageInfoEmbed.js";
|
||||
import { utilityCmd } from "../types.js";
|
||||
|
@ -14,7 +15,8 @@ export const MessageInfoCmd = utilityCmd({
|
|||
},
|
||||
|
||||
async run({ message, args, pluginData }) {
|
||||
if (!canReadChannel(args.message.channel, message.member)) {
|
||||
const messageMember = await resolveMessageMember(message);
|
||||
if (!canReadChannel(args.message.channel, messageMember)) {
|
||||
void pluginData.state.common.sendErrorMessage(message, "Unknown message");
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { escapeBold } from "discord.js";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
||||
import { canActOn } from "../../../pluginUtils.js";
|
||||
import { canActOn, resolveMessageMember } from "../../../pluginUtils.js";
|
||||
import { errorMessage } from "../../../utils.js";
|
||||
import { utilityCmd } from "../types.js";
|
||||
|
||||
|
@ -25,7 +25,8 @@ export const NicknameCmd = utilityCmd({
|
|||
return;
|
||||
}
|
||||
|
||||
if (msg.member.id !== args.member.id && !canActOn(pluginData, msg.member, args.member)) {
|
||||
const authorMember = await resolveMessageMember(msg);
|
||||
if (msg.author.id !== args.member.id && !canActOn(pluginData, authorMember, args.member)) {
|
||||
msg.channel.send(errorMessage("Cannot change nickname: insufficient permissions"));
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
||||
import { canActOn } from "../../../pluginUtils.js";
|
||||
import { canActOn, resolveMessageMember } from "../../../pluginUtils.js";
|
||||
import { errorMessage } from "../../../utils.js";
|
||||
import { utilityCmd } from "../types.js";
|
||||
|
||||
|
@ -14,7 +14,8 @@ export const NicknameResetCmd = utilityCmd({
|
|||
},
|
||||
|
||||
async run({ message: msg, args, pluginData }) {
|
||||
if (msg.member.id !== args.member.id && !canActOn(pluginData, msg.member, args.member)) {
|
||||
const authorMember = await resolveMessageMember(msg);
|
||||
if (msg.author.id !== args.member.id && !canActOn(pluginData, authorMember, args.member)) {
|
||||
msg.channel.send(errorMessage("Cannot reset nickname: insufficient permissions"));
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import moment from "moment-timezone";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
||||
import { getBaseUrl } from "../../../pluginUtils.js";
|
||||
import { getBaseUrl, resolveMessageMember } from "../../../pluginUtils.js";
|
||||
import { canReadChannel } from "../../../utils/canReadChannel.js";
|
||||
import { utilityCmd } from "../types.js";
|
||||
|
||||
|
@ -15,7 +15,8 @@ export const SourceCmd = utilityCmd({
|
|||
},
|
||||
|
||||
async run({ message: cmdMessage, args, pluginData }) {
|
||||
if (!canReadChannel(args.message.channel, cmdMessage.member)) {
|
||||
const cmdAuthorMember = await resolveMessageMember(cmdMessage);
|
||||
if (!canReadChannel(args.message.channel, cmdAuthorMember)) {
|
||||
void pluginData.state.common.sendErrorMessage(cmdMessage, "Unknown message");
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { VoiceChannel } from "discord.js";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
||||
import { canActOn } from "../../../pluginUtils.js";
|
||||
import { canActOn, resolveMessageMember } from "../../../pluginUtils.js";
|
||||
import { renderUsername } from "../../../utils.js";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
|
||||
import { utilityCmd } from "../types.js";
|
||||
|
@ -16,7 +16,8 @@ export const VcdisconnectCmd = utilityCmd({
|
|||
},
|
||||
|
||||
async run({ message: msg, args, pluginData }) {
|
||||
if (!canActOn(pluginData, msg.member, args.member)) {
|
||||
const authorMember = await resolveMessageMember(msg);
|
||||
if (!canActOn(pluginData, authorMember, args.member)) {
|
||||
void pluginData.state.common.sendErrorMessage(msg, "Cannot move: insufficient permissions");
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ChannelType, Snowflake, VoiceChannel } from "discord.js";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
|
||||
import { canActOn } from "../../../pluginUtils.js";
|
||||
import { canActOn, resolveMessageMember } from "../../../pluginUtils.js";
|
||||
import { channelMentionRegex, isSnowflake, renderUsername, simpleClosestStringMatch } from "../../../utils.js";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
|
||||
import { utilityCmd } from "../types.js";
|
||||
|
@ -144,15 +144,17 @@ export const VcmoveAllCmd = utilityCmd({
|
|||
return;
|
||||
}
|
||||
|
||||
const authorMember = await resolveMessageMember(msg);
|
||||
|
||||
// Cant leave null, otherwise we get an assignment error in the catch
|
||||
let currMember = msg.member;
|
||||
let currMember = authorMember;
|
||||
const moveAmt = args.oldChannel.members.size;
|
||||
let errAmt = 0;
|
||||
for (const memberWithId of args.oldChannel.members) {
|
||||
currMember = memberWithId[1];
|
||||
|
||||
// Check for permissions but allow self-moves
|
||||
if (currMember.id !== msg.member.id && !canActOn(pluginData, msg.member, currMember)) {
|
||||
if (currMember.id !== authorMember.id && !canActOn(pluginData, authorMember, currMember)) {
|
||||
void pluginData.state.common.sendErrorMessage(
|
||||
msg,
|
||||
`Failed to move ${renderUsername(currMember)} (${currMember.id}): You cannot act on this member`,
|
||||
|
@ -166,7 +168,7 @@ export const VcmoveAllCmd = utilityCmd({
|
|||
channel: channel.id,
|
||||
});
|
||||
} catch {
|
||||
if (msg.member.id === currMember.id) {
|
||||
if (authorMember.id === currMember.id) {
|
||||
void pluginData.state.common.sendErrorMessage(msg, "Unknown error when trying to move members");
|
||||
return;
|
||||
}
|
||||
|
|
49
backend/src/plugins/Utility/functions/cleanMessages.ts
Normal file
49
backend/src/plugins/Utility/functions/cleanMessages.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { UtilityPluginType } from "../types.js";
|
||||
import { GuildBasedChannel, Snowflake, TextBasedChannel, User } from "discord.js";
|
||||
import { SavedMessage } from "../../../data/entities/SavedMessage.js";
|
||||
import { LogType } from "../../../data/LogType.js";
|
||||
import { chunkArray } from "../../../utils.js";
|
||||
import { getBaseUrl } from "../../../pluginUtils.js";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
|
||||
|
||||
export async function cleanMessages(
|
||||
pluginData: GuildPluginData<UtilityPluginType>,
|
||||
channel: GuildBasedChannel & TextBasedChannel,
|
||||
savedMessages: SavedMessage[],
|
||||
mod: User,
|
||||
) {
|
||||
pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE, savedMessages[0].id);
|
||||
pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE_BULK, savedMessages[0].id);
|
||||
|
||||
// Delete & archive in ID order
|
||||
savedMessages = Array.from(savedMessages).sort((a, b) => (a.id > b.id ? 1 : -1));
|
||||
const idsToDelete = savedMessages.map((m) => m.id) as Snowflake[];
|
||||
|
||||
// Make sure the deletions aren't double logged
|
||||
idsToDelete.forEach((id) => pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE, id));
|
||||
pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE_BULK, idsToDelete[0]);
|
||||
|
||||
// Actually delete the messages (in chunks of 100)
|
||||
|
||||
const chunks = chunkArray(idsToDelete, 100);
|
||||
await Promise.all(
|
||||
chunks.map((chunk) =>
|
||||
Promise.all([channel.bulkDelete(chunk), pluginData.state.savedMessages.markBulkAsDeleted(chunk)]),
|
||||
),
|
||||
);
|
||||
|
||||
// Create an archive
|
||||
const archiveId = await pluginData.state.archives.createFromSavedMessages(savedMessages, pluginData.guild);
|
||||
const baseUrl = getBaseUrl(pluginData);
|
||||
const archiveUrl = pluginData.state.archives.getUrl(baseUrl, archiveId);
|
||||
|
||||
pluginData.getPlugin(LogsPlugin).logClean({
|
||||
mod,
|
||||
channel,
|
||||
count: savedMessages.length,
|
||||
archiveUrl,
|
||||
});
|
||||
|
||||
return { archiveUrl };
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
import { GuildBasedChannel, Message, OmitPartialGroupDMChannel, Snowflake, TextBasedChannel } from "discord.js";
|
||||
import { DAYS, getInviteCodesInString } from "../../../utils.js";
|
||||
import { GuildPluginData } from "knub";
|
||||
import { UtilityPluginType } from "../types.js";
|
||||
import { snowflakeToTimestamp } from "../../../utils/snowflakeToTimestamp.js";
|
||||
import { humanizeDurationShort } from "../../../humanizeDuration.js";
|
||||
import { allowTimeout } from "../../../RegExpRunner.js";
|
||||
import { SavedMessage } from "../../../data/entities/SavedMessage.js";
|
||||
|
||||
const MAX_CLEAN_COUNT = 300;
|
||||
const MAX_CLEAN_TIME = 1 * DAYS;
|
||||
const MAX_CLEAN_API_REQUESTS = 20;
|
||||
|
||||
export interface FetchChannelMessagesToCleanOpts {
|
||||
count: number;
|
||||
beforeId: string;
|
||||
upToId?: string;
|
||||
authorId?: string;
|
||||
includePins?: boolean;
|
||||
onlyBotMessages?: boolean;
|
||||
onlyWithInvites?: boolean;
|
||||
matchContent?: RegExp;
|
||||
}
|
||||
|
||||
export interface SuccessResult {
|
||||
messages: SavedMessage[];
|
||||
note: string;
|
||||
}
|
||||
|
||||
export interface ErrorResult {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export type FetchChannelMessagesToCleanResult = SuccessResult | ErrorResult;
|
||||
|
||||
export async function fetchChannelMessagesToClean(pluginData: GuildPluginData<UtilityPluginType>, targetChannel: GuildBasedChannel & TextBasedChannel, opts: FetchChannelMessagesToCleanOpts): Promise<FetchChannelMessagesToCleanResult> {
|
||||
if (opts.count > MAX_CLEAN_COUNT || opts.count <= 0) {
|
||||
return { error: `Clean count must be between 1 and ${MAX_CLEAN_COUNT}` };
|
||||
}
|
||||
|
||||
const result: FetchChannelMessagesToCleanResult = {
|
||||
messages: [],
|
||||
note: "",
|
||||
};
|
||||
|
||||
const timestampCutoff = snowflakeToTimestamp(opts.beforeId) - MAX_CLEAN_TIME;
|
||||
let foundId = false;
|
||||
|
||||
let pinIds: Set<Snowflake> = new Set();
|
||||
if (!opts.includePins) {
|
||||
pinIds = new Set((await targetChannel.messages.fetchPinned()).keys());
|
||||
}
|
||||
|
||||
let rawMessagesToClean: Array<OmitPartialGroupDMChannel<Message<true>>> = [];
|
||||
let beforeId = opts.beforeId;
|
||||
let requests = 0;
|
||||
while (rawMessagesToClean.length < opts.count) {
|
||||
const potentialMessages = await targetChannel.messages.fetch({
|
||||
before: beforeId,
|
||||
limit: 100,
|
||||
});
|
||||
if (potentialMessages.size === 0) break;
|
||||
|
||||
requests++;
|
||||
|
||||
const filtered: Array<OmitPartialGroupDMChannel<Message<true>>> = [];
|
||||
for (const message of potentialMessages.values()) {
|
||||
const contentString = message.content || "";
|
||||
if (opts.authorId && message.author.id !== opts.authorId) continue;
|
||||
if (opts.onlyBotMessages && !message.author.bot) continue;
|
||||
if (pinIds.has(message.id)) continue;
|
||||
if (opts.onlyWithInvites && getInviteCodesInString(contentString).length === 0) continue;
|
||||
if (opts.upToId && message.id < opts.upToId) {
|
||||
foundId = true;
|
||||
break;
|
||||
}
|
||||
if (message.createdTimestamp < timestampCutoff) continue;
|
||||
if (opts.matchContent && !(await pluginData.state.regexRunner.exec(opts.matchContent, contentString).catch(allowTimeout))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
filtered.push(message);
|
||||
}
|
||||
const remaining = opts.count - rawMessagesToClean.length;
|
||||
const withoutOverflow = filtered.slice(0, remaining);
|
||||
rawMessagesToClean.push(...withoutOverflow);
|
||||
|
||||
beforeId = potentialMessages.lastKey()!;
|
||||
|
||||
if (foundId) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (rawMessagesToClean.length < opts.count) {
|
||||
if (potentialMessages.last()!.createdTimestamp < timestampCutoff) {
|
||||
result.note = `stopped looking after reaching ${humanizeDurationShort(MAX_CLEAN_TIME)} old messages`;
|
||||
break;
|
||||
}
|
||||
|
||||
if (requests >= MAX_CLEAN_API_REQUESTS) {
|
||||
result.note = `stopped looking after ${requests * 100} messages`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Discord messages -> SavedMessages
|
||||
const existingStored = await pluginData.state.savedMessages.getMultiple(rawMessagesToClean.map((m) => m.id));
|
||||
const alreadyStored = existingStored.map((stored) => stored.id);
|
||||
const messagesToStore = rawMessagesToClean.filter((potentialMsg) => !alreadyStored.includes(potentialMsg.id));
|
||||
await pluginData.state.savedMessages.createFromMessages(messagesToStore);
|
||||
|
||||
result.messages = await pluginData.state.savedMessages.getMultiple(rawMessagesToClean.map((m) => m.id));
|
||||
|
||||
return result;
|
||||
}
|
|
@ -100,9 +100,8 @@ export async function getInviteInfoEmbed(
|
|||
fields: [],
|
||||
};
|
||||
|
||||
invite = invite as GroupDMInvite;
|
||||
embed.author = {
|
||||
name: invite.channel!.name ? `Group DM invite: ${invite.channel!.name}` : `Group DM invite`,
|
||||
name: invite.channel.name ? `Group DM invite: ${invite.channel.name}` : `Group DM invite`,
|
||||
url: `https://discord.gg/${invite.code}`,
|
||||
}; // FIXME pending invite re-think
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
GuildMember,
|
||||
Message,
|
||||
MessageComponentInteraction,
|
||||
OmitPartialGroupDMChannel,
|
||||
PermissionsBitField,
|
||||
Snowflake,
|
||||
User,
|
||||
|
@ -73,22 +74,22 @@ export async function displaySearch(
|
|||
pluginData: GuildPluginData<UtilityPluginType>,
|
||||
args: MemberSearchParams,
|
||||
searchType: SearchType.MemberSearch,
|
||||
msg: Message,
|
||||
msg: OmitPartialGroupDMChannel<Message>,
|
||||
);
|
||||
export async function displaySearch(
|
||||
pluginData: GuildPluginData<UtilityPluginType>,
|
||||
args: BanSearchParams,
|
||||
searchType: SearchType.BanSearch,
|
||||
msg: Message,
|
||||
msg: OmitPartialGroupDMChannel<Message>,
|
||||
);
|
||||
export async function displaySearch(
|
||||
pluginData: GuildPluginData<UtilityPluginType>,
|
||||
args: MemberSearchParams | BanSearchParams,
|
||||
searchType: SearchType,
|
||||
msg: Message,
|
||||
msg: OmitPartialGroupDMChannel<Message>,
|
||||
) {
|
||||
// If we're not exporting, load 1 page of search results at a time and allow the user to switch pages with reactions
|
||||
let originalSearchMsg: Message;
|
||||
let originalSearchMsg: OmitPartialGroupDMChannel<Message>;
|
||||
let searching = false;
|
||||
let currentPage = args.page || 1;
|
||||
let stopCollectionFn: () => void;
|
||||
|
@ -107,7 +108,7 @@ export async function displaySearch(
|
|||
searchMsgPromise = originalSearchMsg.edit("Searching...");
|
||||
} else {
|
||||
searchMsgPromise = msg.channel.send("Searching...");
|
||||
searchMsgPromise.then((m) => (originalSearchMsg = m));
|
||||
searchMsgPromise.then((m) => (originalSearchMsg = m as OmitPartialGroupDMChannel<Message>));
|
||||
}
|
||||
|
||||
let searchResult;
|
||||
|
@ -240,19 +241,19 @@ export async function archiveSearch(
|
|||
pluginData: GuildPluginData<UtilityPluginType>,
|
||||
args: MemberSearchParams,
|
||||
searchType: SearchType.MemberSearch,
|
||||
msg: Message,
|
||||
msg: OmitPartialGroupDMChannel<Message>,
|
||||
);
|
||||
export async function archiveSearch(
|
||||
pluginData: GuildPluginData<UtilityPluginType>,
|
||||
args: BanSearchParams,
|
||||
searchType: SearchType.BanSearch,
|
||||
msg: Message,
|
||||
msg: OmitPartialGroupDMChannel<Message>,
|
||||
);
|
||||
export async function archiveSearch(
|
||||
pluginData: GuildPluginData<UtilityPluginType>,
|
||||
args: MemberSearchParams | BanSearchParams,
|
||||
searchType: SearchType,
|
||||
msg: Message,
|
||||
msg: OmitPartialGroupDMChannel<Message>,
|
||||
) {
|
||||
let results;
|
||||
try {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand, pluginUtils } from "knub";
|
||||
import z from "zod";
|
||||
import z from "zod/v4";
|
||||
import { RegExpRunner } from "../../RegExpRunner.js";
|
||||
import { GuildArchives } from "../../data/GuildArchives.js";
|
||||
import { GuildCases } from "../../data/GuildCases.js";
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue