Context Menu Actions v1, clean and mute support with full options
This commit is contained in:
parent
2281dbcfef
commit
ff774aa5f6
19 changed files with 657 additions and 152 deletions
|
@ -13,7 +13,7 @@ import { AboutCmd } from "./commands/AboutCmd";
|
|||
import { AvatarCmd } from "./commands/AvatarCmd";
|
||||
import { BanSearchCmd } from "./commands/BanSearchCmd";
|
||||
import { ChannelInfoCmd } from "./commands/ChannelInfoCmd";
|
||||
import { CleanCmd } from "./commands/CleanCmd";
|
||||
import { CleanArgs, cleanCmd, CleanCmd } from "./commands/CleanCmd";
|
||||
import { ContextCmd } from "./commands/ContextCmd";
|
||||
import { EmojiInfoCmd } from "./commands/EmojiInfoCmd";
|
||||
import { HelpCmd } from "./commands/HelpCmd";
|
||||
|
@ -156,6 +156,14 @@ export const UtilityPlugin = zeppelinGuildPlugin<UtilityPluginType>()({
|
|||
AutoJoinThreadSyncEvt,
|
||||
],
|
||||
|
||||
public: {
|
||||
clean(pluginData) {
|
||||
return (args: CleanArgs, msg) => {
|
||||
cleanCmd(pluginData, args, msg);
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
beforeLoad(pluginData) {
|
||||
const { state, guild } = pluginData;
|
||||
|
||||
|
|
|
@ -10,12 +10,13 @@ import { getBaseUrl, sendErrorMessage, sendSuccessMessage } from "../../../plugi
|
|||
import { allowTimeout } from "../../../RegExpRunner";
|
||||
import { DAYS, getInviteCodesInString, noop, SECONDS } from "../../../utils";
|
||||
import { utilityCmd, UtilityPluginType } from "../types";
|
||||
import { boolean, number } from "io-ts";
|
||||
|
||||
const MAX_CLEAN_COUNT = 150;
|
||||
const MAX_CLEAN_TIME = 1 * DAYS;
|
||||
const CLEAN_COMMAND_DELETE_DELAY = 5 * SECONDS;
|
||||
|
||||
async function cleanMessages(
|
||||
export async function cleanMessages(
|
||||
pluginData: GuildPluginData<UtilityPluginType>,
|
||||
channel: TextChannel,
|
||||
savedMessages: SavedMessage[],
|
||||
|
@ -61,6 +62,142 @@ 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;
|
||||
}
|
||||
|
||||
export async function cleanCmd(pluginData: GuildPluginData<UtilityPluginType>, args: CleanArgs | any, msg) {
|
||||
if (args.count > MAX_CLEAN_COUNT || args.count <= 0) {
|
||||
sendErrorMessage(pluginData, msg.channel, `Clean count must be between 1 and ${MAX_CLEAN_COUNT}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetChannel = args.channel ? pluginData.guild.channels.cache.get(args.channel as Snowflake) : msg.channel;
|
||||
if (!targetChannel || !(targetChannel instanceof TextChannel)) {
|
||||
sendErrorMessage(pluginData, msg.channel, `Invalid channel specified`);
|
||||
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) {
|
||||
sendErrorMessage(pluginData, msg.channel, `Missing permissions to use clean on that channel`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const cleaningMessage = msg.channel.send("Cleaning...");
|
||||
|
||||
const messagesToClean: SavedMessage[] = [];
|
||||
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 pins: Message[] = [];
|
||||
if (!deletePins) {
|
||||
pins = [...(await msg.channel.messages.fetchPinned().catch(() => [])).values()];
|
||||
}
|
||||
|
||||
while (messagesToClean.length < args.count) {
|
||||
const potentialMessages = await targetChannel.messages.fetch({
|
||||
before: beforeId,
|
||||
limit: args.count,
|
||||
});
|
||||
if (potentialMessages.size === 0) break;
|
||||
|
||||
const existingStored = await pluginData.state.savedMessages.getMultiple([...potentialMessages.keys()]);
|
||||
const alreadyStored = existingStored.map(stored => stored.id);
|
||||
const messagesToStore = [
|
||||
...potentialMessages.filter(potentialMsg => !alreadyStored.includes(potentialMsg.id)).values(),
|
||||
];
|
||||
await pluginData.state.savedMessages.createFromMessages(messagesToStore);
|
||||
|
||||
const potentialMessagesToClean = await pluginData.state.savedMessages.getMultiple([...potentialMessages.keys()]);
|
||||
if (potentialMessagesToClean.length === 0) break;
|
||||
|
||||
const filtered: SavedMessage[] = [];
|
||||
for (const message of potentialMessagesToClean) {
|
||||
const contentString = message.data.content || "";
|
||||
if (args.user && message.user_id !== args.user) continue;
|
||||
if (args.bots && !message.is_bot) continue;
|
||||
if (!deletePins && pins.find(x => x.id === message.id) != null) continue;
|
||||
if (args["has-invites"] && getInviteCodesInString(contentString).length === 0) continue;
|
||||
if (upToMsgId != null && message.id < upToMsgId) {
|
||||
foundId = true;
|
||||
break;
|
||||
}
|
||||
if (moment.utc(message.posted_at).valueOf() < 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 || moment.utc(potentialMessages.last()!.createdTimestamp).valueOf() < timeCutoff) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let responseMsg: Message | undefined;
|
||||
if (messagesToClean.length > 0) {
|
||||
const cleanResult = await cleanMessages(pluginData, targetChannel, messagesToClean, msg.author);
|
||||
|
||||
let responseText = `Cleaned ${messagesToClean.length} ${messagesToClean.length === 1 ? "message" : "messages"}`;
|
||||
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 sendSuccessMessage(pluginData, msg.channel, responseText);
|
||||
} else {
|
||||
responseMsg = await sendErrorMessage(pluginData, msg.channel, `Found no messages to clean!`);
|
||||
}
|
||||
|
||||
await (await 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)
|
||||
setTimeout(() => {
|
||||
msg.delete().catch(noop);
|
||||
responseMsg?.delete().catch(noop);
|
||||
}, CLEAN_COMMAND_DELETE_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
export const CleanCmd = utilityCmd({
|
||||
trigger: ["clean", "clear"],
|
||||
description: "Remove a number of recent messages",
|
||||
|
@ -83,126 +220,6 @@ export const CleanCmd = utilityCmd({
|
|||
],
|
||||
|
||||
async run({ message: msg, args, pluginData }) {
|
||||
if (args.count > MAX_CLEAN_COUNT || args.count <= 0) {
|
||||
sendErrorMessage(pluginData, msg.channel, `Clean count must be between 1 and ${MAX_CLEAN_COUNT}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetChannel = args.channel ? pluginData.guild.channels.cache.get(args.channel as Snowflake) : msg.channel;
|
||||
if (!targetChannel || !(targetChannel instanceof TextChannel)) {
|
||||
sendErrorMessage(pluginData, msg.channel, `Invalid channel specified`);
|
||||
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) {
|
||||
sendErrorMessage(pluginData, msg.channel, `Missing permissions to use clean on that channel`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const cleaningMessage = msg.channel.send("Cleaning...");
|
||||
|
||||
const messagesToClean: SavedMessage[] = [];
|
||||
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 pins: Message[] = [];
|
||||
if (!deletePins) {
|
||||
pins = [...(await msg.channel.messages.fetchPinned().catch(() => [])).values()];
|
||||
}
|
||||
|
||||
while (messagesToClean.length < args.count) {
|
||||
const potentialMessages = await targetChannel.messages.fetch({
|
||||
before: beforeId,
|
||||
limit: args.count,
|
||||
});
|
||||
if (potentialMessages.size === 0) break;
|
||||
|
||||
const existingStored = await pluginData.state.savedMessages.getMultiple([...potentialMessages.keys()]);
|
||||
const alreadyStored = existingStored.map(stored => stored.id);
|
||||
const messagesToStore = [
|
||||
...potentialMessages.filter(potentialMsg => !alreadyStored.includes(potentialMsg.id)).values(),
|
||||
];
|
||||
await pluginData.state.savedMessages.createFromMessages(messagesToStore);
|
||||
|
||||
const potentialMessagesToClean = await pluginData.state.savedMessages.getMultiple([...potentialMessages.keys()]);
|
||||
if (potentialMessagesToClean.length === 0) break;
|
||||
|
||||
const filtered: SavedMessage[] = [];
|
||||
for (const message of potentialMessagesToClean) {
|
||||
const contentString = message.data.content || "";
|
||||
if (args.user && message.user_id !== args.user) continue;
|
||||
if (args.bots && !message.is_bot) continue;
|
||||
if (!deletePins && pins.find(x => x.id === message.id) != null) continue;
|
||||
if (args["has-invites"] && getInviteCodesInString(contentString).length === 0) continue;
|
||||
if (upToMsgId != null && message.id < upToMsgId) {
|
||||
foundId = true;
|
||||
break;
|
||||
}
|
||||
if (moment.utc(message.posted_at).valueOf() < 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 || moment.utc(potentialMessages.last()!.createdTimestamp).valueOf() < timeCutoff) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let responseMsg: Message | undefined;
|
||||
if (messagesToClean.length > 0) {
|
||||
const cleanResult = await cleanMessages(pluginData, targetChannel, messagesToClean, msg.author);
|
||||
|
||||
let responseText = `Cleaned ${messagesToClean.length} ${messagesToClean.length === 1 ? "message" : "messages"}`;
|
||||
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 sendSuccessMessage(pluginData, msg.channel, responseText);
|
||||
} else {
|
||||
responseMsg = await sendErrorMessage(pluginData, msg.channel, `Found no messages to clean!`);
|
||||
}
|
||||
|
||||
await (await 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)
|
||||
setTimeout(() => {
|
||||
msg.delete().catch(noop);
|
||||
responseMsg?.delete().catch(noop);
|
||||
}, CLEAN_COMMAND_DELETE_DELAY);
|
||||
}
|
||||
cleanCmd(pluginData, args, msg);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -134,9 +134,8 @@ export async function getChannelInfoEmbed(
|
|||
const memberCount = thread.memberCount ?? thread.members.cache.size;
|
||||
const owner = await thread.fetchOwner().catch(() => null);
|
||||
const ownerMention = owner?.user ? verboseUserMention(owner.user) : "Unknown#0000";
|
||||
const humanizedArchiveTime = `Archive duration: **${humanizeDuration(
|
||||
(thread.autoArchiveDuration ?? 0) * MINUTES,
|
||||
)}**`;
|
||||
const autoArchiveDuration = thread.autoArchiveDuration === "MAX" ? 10080 : thread.autoArchiveDuration; // TODO: Boost level check
|
||||
const humanizedArchiveTime = `Archive duration: **${humanizeDuration((autoArchiveDuration ?? 0) * MINUTES)}**`;
|
||||
|
||||
embed.fields.push({
|
||||
name: preEmbedPadding + "Thread information",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue