3
0
Fork 0
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:
Dragory 2025-05-22 22:35:48 +00:00
parent 595e1a0556
commit 09eb8e92f2
No known key found for this signature in database
189 changed files with 1244 additions and 900 deletions

View file

@ -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),
};

View file

@ -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);
}
},
});

View file

@ -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;
}

View file

@ -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] });

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View 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 };
}

View file

@ -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;
}

View file

@ -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

View file

@ -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 {

View file

@ -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";