3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-03-15 05:41:51 +00:00

Merge branch '240811_application_commands_merge_2' into next

This commit is contained in:
Dragory 2024-08-11 22:28:41 +03:00
commit 43b8017985
No known key found for this signature in database
279 changed files with 6192 additions and 3044 deletions

View file

@ -1,6 +1,6 @@
import { ApiPermissions } from "@zeppelinbot/shared/apiPermissions.js";
import express, { Request, Response } from "express";
import jsYaml from "js-yaml";
import { YAMLException } from "js-yaml";
import moment from "moment-timezone";
import { Queue } from "../../Queue.js";
import { validateGuildConfig } from "../../configValidator.js";
@ -15,8 +15,6 @@ import { ObjectAliasError } from "../../utils/validateNoObjectAliases.js";
import { hasGuildPermission, requireGuildPermission } from "../permissions.js";
import { clientError, ok, serverError, unauthorized } from "../responses.js";
const YAMLException = jsYaml.YAMLException;
const apiPermissionAssignments = new ApiPermissionAssignments();
const auditLog = new ApiAuditLog();

View file

@ -28,10 +28,10 @@ app.use(multer().none());
const rootRouter = express.Router();
initAuth(app);
initGuildsAPI(app);
initArchives(app);
initDocs(app);
initAuth(rootRouter);
initGuildsAPI(rootRouter);
initArchives(rootRouter);
initDocs(rootRouter);
// Default route
rootRouter.get("/", (req, res) => {

View file

@ -1,8 +1,7 @@
import { ConfigValidationError, GuildPluginBlueprint, PluginConfigManager } from "knub";
import moment from "moment-timezone";
import { BaseConfig, ConfigValidationError, GuildPluginBlueprint, PluginConfigManager } from "knub";
import { ZodError } from "zod";
import { availableGuildPlugins } from "./plugins/availablePlugins.js";
import { ZeppelinGuildConfig, zZeppelinGuildConfig } from "./types.js";
import { zZeppelinGuildConfig } from "./types.js";
import { formatZodIssue } from "./utils/formatZodIssue.js";
const pluginNameToPlugin = new Map<string, GuildPluginBlueprint<any, any>>();
@ -16,14 +15,7 @@ export async function validateGuildConfig(config: any): Promise<string | null> {
return validationResult.error.issues.map(formatZodIssue).join("\n");
}
const guildConfig = config as ZeppelinGuildConfig;
if (guildConfig.timezone) {
const validTimezones = moment.tz.names();
if (!validTimezones.includes(guildConfig.timezone)) {
return `Invalid timezone: ${guildConfig.timezone}`;
}
}
const guildConfig = config as BaseConfig;
if (guildConfig.plugins) {
for (const [pluginName, pluginOptions] of Object.entries(guildConfig.plugins)) {

View file

@ -1,4 +1,5 @@
import { In, InsertResult, Repository } from "typeorm";
import { FindOptionsWhere } from "typeorm/find-options/FindOptionsWhere";
import { Queue } from "../Queue.js";
import { chunkArray } from "../utils.js";
import { BaseGuildRepository } from "./BaseGuildRepository.js";
@ -73,34 +74,69 @@ export class GuildCases extends BaseGuildRepository {
});
}
async getByUserId(userId: string): Promise<Case[]> {
async getByUserId(
userId: string,
filters: Omit<FindOptionsWhere<Case>, "guild_id" | "user_id"> = {},
): Promise<Case[]> {
return this.cases.find({
relations: this.getRelations(),
where: {
guild_id: this.guildId,
user_id: userId,
...filters,
},
});
}
async getRecentByUserId(userId: string, count: number, skip = 0): Promise<Case[]> {
return this.cases.find({
relations: this.getRelations(),
where: {
guild_id: this.guildId,
user_id: userId,
},
skip,
take: count,
order: {
case_number: "DESC",
},
});
}
async getTotalCasesByModId(modId: string): Promise<number> {
async getTotalCasesByModId(
modId: string,
filters: Omit<FindOptionsWhere<Case>, "guild_id" | "mod_id" | "is_hidden"> = {},
): Promise<number> {
return this.cases.count({
where: {
guild_id: this.guildId,
mod_id: modId,
is_hidden: false,
...filters,
},
});
}
async getRecentByModId(modId: string, count: number, skip = 0): Promise<Case[]> {
return this.cases.find({
relations: this.getRelations(),
where: {
async getRecentByModId(
modId: string,
count: number,
skip = 0,
filters: Omit<FindOptionsWhere<Case>, "guild_id" | "mod_id"> = {},
): Promise<Case[]> {
const where: FindOptionsWhere<Case> = {
guild_id: this.guildId,
mod_id: modId,
is_hidden: false,
},
...filters,
};
if (where.is_hidden === true) {
delete where.is_hidden;
}
return this.cases.find({
relations: this.getRelations(),
where,
skip,
take: count,
order: {

View file

@ -44,15 +44,7 @@ import { availableGlobalPlugins, availableGuildPlugins } from "./plugins/availab
import { setProfiler } from "./profiler.js";
import { logRateLimit } from "./rateLimitStats.js";
import { startUptimeCounter } from "./uptime.js";
import {
MINUTES,
SECONDS,
errorMessage,
isDiscordAPIError,
isDiscordHTTPError,
sleep,
successMessage,
} from "./utils.js";
import { MINUTES, SECONDS, errorMessage, isDiscordAPIError, isDiscordHTTPError, sleep, successMessage } from "./utils.js";
import { DecayingCounter } from "./utils/DecayingCounter.js";
import { enableProfiling } from "./utils/easyProfiler.js";
import { loadYamlSafely } from "./utils/loadYamlSafely.js";
@ -324,9 +316,27 @@ connect().then(async () => {
if (row) {
try {
const loaded = loadYamlSafely(row.config);
if (loaded.success_emoji || loaded.error_emoji) {
const deprecatedKeys = [] as string[];
const exampleConfig = `plugins:\n common:\n config:\n success_emoji: "👍"\n error_emoji: "👎"`;
if (loaded.success_emoji) {
deprecatedKeys.push("success_emoji");
}
if (loaded.error_emoji) {
deprecatedKeys.push("error_emoji");
}
logger.warn(`Deprecated config properties found in "${key}": ${deprecatedKeys.join(", ")}`);
logger.warn(`You can now configure those emojis in the "common" plugin config\n${exampleConfig}`);
}
// Remove deprecated properties some may still have in their config
delete loaded.success_emoji;
delete loaded.error_emoji;
return loaded;
} catch (err) {
logger.error(`Error while loading config "${key}": ${err.message}`);

View file

@ -3,18 +3,18 @@
*/
import {
ChatInputCommandInteraction,
GuildMember,
InteractionReplyOptions,
Message,
MessageCreateOptions,
MessageMentionOptions,
PermissionsBitField,
TextBasedChannel,
User,
} from "discord.js";
import { AnyPluginData, BasePluginData, CommandContext, ExtendedMatchParams, GuildPluginData, helpers } from "knub";
import { logger } from "./logger.js";
import { isStaff } from "./staff.js";
import { TZeppelinKnub } from "./types.js";
import { errorMessage, successMessage } from "./utils.js";
import { Tail } from "./utils/typeUtils.js";
const { getMemberLevel } = helpers;
@ -49,46 +49,57 @@ export async function hasPermission(
return helpers.hasPermission(config, permission);
}
export async function sendSuccessMessage(
pluginData: AnyPluginData<any>,
channel: TextBasedChannel,
body: string,
allowedMentions?: MessageMentionOptions,
): Promise<Message | undefined> {
const emoji = pluginData.fullConfig.success_emoji || undefined;
const formattedBody = successMessage(body, emoji);
const content: MessageCreateOptions = allowedMentions
? { content: formattedBody, allowedMentions }
: { content: formattedBody };
return channel
.send({ ...content }) // Force line break
.catch((err) => {
const channelInfo = "guild" in channel ? `${channel.id} (${channel.guild.id})` : channel.id;
logger.warn(`Failed to send success message to ${channelInfo}): ${err.code} ${err.message}`);
return undefined;
});
export function isContextInteraction(
context: TextBasedChannel | Message | User | ChatInputCommandInteraction,
): context is ChatInputCommandInteraction {
return "commandId" in context && !!context.commandId;
}
export async function sendErrorMessage(
pluginData: AnyPluginData<any>,
channel: TextBasedChannel,
body: string,
allowedMentions?: MessageMentionOptions,
): Promise<Message | undefined> {
const emoji = pluginData.fullConfig.error_emoji || undefined;
const formattedBody = errorMessage(body, emoji);
const content: MessageCreateOptions = allowedMentions
? { content: formattedBody, allowedMentions }
: { content: formattedBody };
export function isContextMessage(
context: TextBasedChannel | Message | User | ChatInputCommandInteraction,
): context is Message {
return "content" in context || "embeds" in context;
}
return channel
.send({ ...content }) // Force line break
.catch((err) => {
const channelInfo = "guild" in channel ? `${channel.id} (${channel.guild.id})` : channel.id;
logger.warn(`Failed to send error message to ${channelInfo}): ${err.code} ${err.message}`);
return undefined;
});
export async function getContextChannel(
context: TextBasedChannel | Message | User | ChatInputCommandInteraction,
): Promise<TextBasedChannel> {
if (isContextInteraction(context)) {
// context is ChatInputCommandInteraction
return context.channel!;
} else if ("username" in context) {
// context is User
return await (context as User).createDM();
} else if ("send" in context) {
// context is TextBaseChannel
return context as TextBasedChannel;
} else {
// context is Message
return context.channel;
}
}
export async function sendContextResponse(
context: TextBasedChannel | Message | User | ChatInputCommandInteraction,
response: string | Omit<MessageCreateOptions, "flags"> | InteractionReplyOptions,
): Promise<Message> {
if (isContextInteraction(context)) {
const options = { ...(typeof response === "string" ? { content: response } : response), fetchReply: true };
return (
context.replied
? context.followUp(options)
: context.deferred
? context.editReply(options)
: context.reply(options)
) as Promise<Message>;
}
if (typeof response !== "string" && "ephemeral" in response) {
delete response.ephemeral;
}
return (await getContextChannel(context)).send(response as string | Omit<MessageCreateOptions, "flags">);
}
export function getBaseUrl(pluginData: AnyPluginData<any>) {

View file

@ -1,6 +1,7 @@
import { PluginOptions, guildPlugin } from "knub";
import { GuildAutoReactions } from "../../data/GuildAutoReactions.js";
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
import { CommonPlugin } from "../Common/CommonPlugin.js";
import { LogsPlugin } from "../Logs/LogsPlugin.js";
import { DisableAutoReactionsCmd } from "./commands/DisableAutoReactionsCmd.js";
import { NewAutoReactionsCmd } from "./commands/NewAutoReactionsCmd.js";
@ -50,4 +51,8 @@ export const AutoReactionsPlugin = guildPlugin<AutoReactionsPluginType>()({
state.autoReactions = GuildAutoReactions.getGuildInstance(guild.id);
state.cache = new Map();
},
beforeStart(pluginData) {
pluginData.state.common = pluginData.getPlugin(CommonPlugin);
},
});

View file

@ -1,5 +1,4 @@
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { autoReactionsCmd } from "../types.js";
export const DisableAutoReactionsCmd = autoReactionsCmd({
@ -14,12 +13,12 @@ export const DisableAutoReactionsCmd = autoReactionsCmd({
async run({ message: msg, args, pluginData }) {
const autoReaction = await pluginData.state.autoReactions.getForChannel(args.channelId);
if (!autoReaction) {
sendErrorMessage(pluginData, msg.channel, `Auto-reactions aren't enabled in <#${args.channelId}>`);
void pluginData.state.common.sendErrorMessage(msg, `Auto-reactions aren't enabled in <#${args.channelId}>`);
return;
}
await pluginData.state.autoReactions.removeFromChannel(args.channelId);
pluginData.state.cache.delete(args.channelId);
sendSuccessMessage(pluginData, msg.channel, `Auto-reactions disabled in <#${args.channelId}>`);
void pluginData.state.common.sendSuccessMessage(msg, `Auto-reactions disabled in <#${args.channelId}>`);
},
});

View file

@ -1,6 +1,5 @@
import { PermissionsBitField } from "discord.js";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { canUseEmoji, customEmojiRegex, isEmoji } from "../../../utils.js";
import { getMissingChannelPermissions } from "../../../utils/getMissingChannelPermissions.js";
import { missingPermissionError } from "../../../utils/missingPermissionError.js";
@ -25,9 +24,8 @@ export const NewAutoReactionsCmd = autoReactionsCmd({
const me = pluginData.guild.members.cache.get(pluginData.client.user!.id)!;
const missingPermissions = getMissingChannelPermissions(me, args.channel, requiredPermissions);
if (missingPermissions) {
sendErrorMessage(
pluginData,
msg.channel,
pluginData.state.common.sendErrorMessage(
msg,
`Cannot set auto-reactions for that channel. ${missingPermissionError(missingPermissions)}`,
);
return;
@ -35,7 +33,7 @@ export const NewAutoReactionsCmd = autoReactionsCmd({
for (const reaction of args.reactions) {
if (!isEmoji(reaction)) {
sendErrorMessage(pluginData, msg.channel, "One or more of the specified reactions were invalid!");
void pluginData.state.common.sendErrorMessage(msg, "One or more of the specified reactions were invalid!");
return;
}
@ -45,7 +43,10 @@ export const NewAutoReactionsCmd = autoReactionsCmd({
if (customEmojiMatch) {
// Custom emoji
if (!canUseEmoji(pluginData.client, customEmojiMatch[2])) {
sendErrorMessage(pluginData, msg.channel, "I can only use regular emojis and custom emojis from this server");
pluginData.state.common.sendErrorMessage(
msg,
"I can only use regular emojis and custom emojis from this server",
);
return;
}
@ -60,6 +61,6 @@ export const NewAutoReactionsCmd = autoReactionsCmd({
await pluginData.state.autoReactions.set(args.channel.id, finalReactions);
pluginData.state.cache.delete(args.channel.id);
sendSuccessMessage(pluginData, msg.channel, `Auto-reactions set for <#${args.channel.id}>`);
void pluginData.state.common.sendSuccessMessage(msg, `Auto-reactions set for <#${args.channel.id}>`);
},
});

View file

@ -1,9 +1,10 @@
import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub";
import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand, pluginUtils } from "knub";
import z from "zod";
import { GuildAutoReactions } from "../../data/GuildAutoReactions.js";
import { GuildLogs } from "../../data/GuildLogs.js";
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
import { AutoReaction } from "../../data/entities/AutoReaction.js";
import { CommonPlugin } from "../Common/CommonPlugin.js";
export const zAutoReactionsConfig = z.strictObject({
can_manage: z.boolean(),
@ -16,6 +17,7 @@ export interface AutoReactionsPluginType extends BasePluginType {
savedMessages: GuildSavedMessages;
autoReactions: GuildAutoReactions;
cache: Map<string, AutoReaction | null>;
common: pluginUtils.PluginPublicInterface<typeof CommonPlugin>;
};
}

View file

@ -8,6 +8,7 @@ import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners.js";
import { MINUTES, SECONDS } from "../../utils.js";
import { registerEventListenersFromMap } from "../../utils/registerEventListenersFromMap.js";
import { unregisterEventListenersFromMap } from "../../utils/unregisterEventListenersFromMap.js";
import { CommonPlugin } from "../Common/CommonPlugin.js";
import { CountersPlugin } from "../Counters/CountersPlugin.js";
import { InternalPosterPlugin } from "../InternalPoster/InternalPosterPlugin.js";
import { LogsPlugin } from "../Logs/LogsPlugin.js";
@ -117,6 +118,10 @@ export const AutomodPlugin = guildPlugin<AutomodPluginType>()({
state.cachedAntiraidLevel = await state.antiraidLevels.get();
},
beforeStart(pluginData) {
pluginData.state.common = pluginData.getPlugin(CommonPlugin);
},
async afterLoad(pluginData) {
const { state } = pluginData;

View file

@ -47,6 +47,7 @@ export const BanAction = automodAction({
await modActions.banUserId(
userId,
reason,
reason,
{
contactMethods,
caseArgs,

View file

@ -33,7 +33,7 @@ export const KickAction = automodAction({
const modActions = pluginData.getPlugin(ModActionsPlugin);
for (const member of membersToKick) {
if (!member) continue;
await modActions.kickMember(member, reason, { contactMethods, caseArgs, isAutomodAction: true });
await modActions.kickMember(member, reason, reason, { contactMethods, caseArgs, isAutomodAction: true });
}
},
});

View file

@ -57,6 +57,7 @@ export const MuteAction = automodAction({
userId,
duration,
reason,
reason,
{ contactMethods, caseArgs, isAutomodAction: true },
rolesToRemove,
rolesToRestore,

View file

@ -33,7 +33,7 @@ export const WarnAction = automodAction({
const modActions = pluginData.getPlugin(ModActionsPlugin);
for (const member of membersToWarn) {
if (!member) continue;
await modActions.warnMember(member, reason, { contactMethods, caseArgs, isAutomodAction: true });
await modActions.warnMember(member, reason, reason, { contactMethods, caseArgs, isAutomodAction: true });
}
},
});

View file

@ -1,5 +1,4 @@
import { guildPluginMessageCommand } from "knub";
import { sendSuccessMessage } from "../../../pluginUtils.js";
import { setAntiraidLevel } from "../functions/setAntiraidLevel.js";
import { AutomodPluginType } from "../types.js";
@ -9,6 +8,6 @@ export const AntiraidClearCmd = guildPluginMessageCommand<AutomodPluginType>()({
async run({ pluginData, message }) {
await setAntiraidLevel(pluginData, null, message.author);
sendSuccessMessage(pluginData, message.channel, "Anti-raid turned **off**");
void pluginData.state.common.sendSuccessMessage(message, "Anti-raid turned **off**");
},
});

View file

@ -1,6 +1,5 @@
import { guildPluginMessageCommand } from "knub";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { setAntiraidLevel } from "../functions/setAntiraidLevel.js";
import { AutomodPluginType } from "../types.js";
@ -15,11 +14,11 @@ export const SetAntiraidCmd = guildPluginMessageCommand<AutomodPluginType>()({
async run({ pluginData, message, args }) {
const config = pluginData.config.get();
if (!config.antiraid_levels.includes(args.level)) {
sendErrorMessage(pluginData, message.channel, "Unknown anti-raid level");
pluginData.state.common.sendErrorMessage(message, "Unknown anti-raid level");
return;
}
await setAntiraidLevel(pluginData, args.level, message.author);
sendSuccessMessage(pluginData, message.channel, `Anti-raid level set to **${args.level}**`);
pluginData.state.common.sendSuccessMessage(message, `Anti-raid level set to **${args.level}**`);
},
});

View file

@ -1,6 +1,6 @@
import { guildPluginEventListener } from "knub";
import diff from "lodash/difference.js";
import isEqual from "lodash/isEqual.js";
import diff from "lodash.difference";
import isEqual from "lodash.isequal";
import { runAutomod } from "../functions/runAutomod.js";
import { AutomodContext, AutomodPluginType } from "../types.js";

View file

@ -1,5 +1,5 @@
import { GuildMember, GuildTextBasedChannel, PartialGuildMember, ThreadChannel, User } from "discord.js";
import { BasePluginType, CooldownManager } from "knub";
import { BasePluginType, CooldownManager, pluginUtils } from "knub";
import z from "zod";
import { Queue } from "../../Queue.js";
import { RegExpRunner } from "../../RegExpRunner.js";
@ -9,6 +9,7 @@ import { GuildLogs } from "../../data/GuildLogs.js";
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
import { SavedMessage } from "../../data/entities/SavedMessage.js";
import { entries, zBoundedRecord, zDelayString } from "../../utils.js";
import { CommonPlugin } from "../Common/CommonPlugin.js";
import { CounterEvents } from "../Counters/types.js";
import { ModActionType, ModActionsEvents } from "../ModActions/types.js";
import { MutesEvents } from "../Mutes/types.js";
@ -140,6 +141,8 @@ export interface AutomodPluginType extends BasePluginType {
modActionsListeners: Map<keyof ModActionsEvents, any>;
mutesListeners: Map<keyof MutesEvents, any>;
common: pluginUtils.PluginPublicInterface<typeof CommonPlugin>;
};
}

View file

@ -4,7 +4,6 @@ import { AllowedGuilds } from "../../data/AllowedGuilds.js";
import { ApiPermissionAssignments } from "../../data/ApiPermissionAssignments.js";
import { Configs } from "../../data/Configs.js";
import { GuildArchives } from "../../data/GuildArchives.js";
import { sendSuccessMessage } from "../../pluginUtils.js";
import { getActiveReload, resetActiveReload } from "./activeReload.js";
import { AddDashboardUserCmd } from "./commands/AddDashboardUserCmd.js";
import { AddServerFromInviteCmd } from "./commands/AddServerFromInviteCmd.js";
@ -77,7 +76,7 @@ export const BotControlPlugin = globalPlugin<BotControlPluginType>()({
if (guild) {
const channel = guild.channels.cache.get(channelId as Snowflake);
if (channel instanceof TextChannel) {
sendSuccessMessage(pluginData, channel, "Global plugins reloaded!");
void channel.send("Global plugins reloaded!");
}
}
}

View file

@ -1,6 +1,6 @@
import { ApiPermissions } from "@zeppelinbot/shared/apiPermissions.js";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { isStaffPreFilter, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { isStaffPreFilter } from "../../../pluginUtils.js";
import { renderUsername } from "../../../utils.js";
import { botControlCmd } from "../types.js";
@ -19,7 +19,7 @@ export const AddDashboardUserCmd = botControlCmd({
async run({ pluginData, message: msg, args }) {
const guild = await pluginData.state.allowedGuilds.find(args.guildId);
if (!guild) {
sendErrorMessage(pluginData, msg.channel, "Server is not using Zeppelin");
void msg.channel.send("Server is not using Zeppelin");
return;
}
@ -36,10 +36,7 @@ export const AddDashboardUserCmd = botControlCmd({
}
const userNameList = args.users.map((user) => `<@!${user.id}> (**${renderUsername(user)}**, \`${user.id}\`)`);
sendSuccessMessage(
pluginData,
msg.channel,
`The following users were given dashboard access for **${guild.name}**:\n\n${userNameList}`,
);
msg.channel.send(`The following users were given dashboard access for **${guild.name}**:\n\n${userNameList}`);
},
});

View file

@ -1,7 +1,6 @@
import { ApiPermissions } from "@zeppelinbot/shared/apiPermissions.js";
import moment from "moment-timezone";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { DBDateFormat, isGuildInvite, resolveInvite } from "../../../utils.js";
import { isEligible } from "../functions/isEligible.js";
import { botControlCmd } from "../types.js";
@ -18,19 +17,19 @@ export const AddServerFromInviteCmd = botControlCmd({
async run({ pluginData, message: msg, args }) {
const invite = await resolveInvite(pluginData.client, args.inviteCode, true);
if (!invite || !isGuildInvite(invite)) {
sendErrorMessage(pluginData, msg.channel, "Could not resolve invite"); // :D
void msg.channel.send("Could not resolve invite"); // :D
return;
}
const existing = await pluginData.state.allowedGuilds.find(invite.guild.id);
if (existing) {
sendErrorMessage(pluginData, msg.channel, "Server is already allowed!");
void msg.channel.send("Server is already allowed!");
return;
}
const { result, explanation } = await isEligible(pluginData, args.user, invite);
if (!result) {
sendErrorMessage(pluginData, msg.channel, `Could not add server because it's not eligible: ${explanation}`);
msg.channel.send(`Could not add server because it's not eligible: ${explanation}`);
return;
}
@ -51,6 +50,6 @@ export const AddServerFromInviteCmd = botControlCmd({
);
}
sendSuccessMessage(pluginData, msg.channel, "Server was eligible and is now allowed to use Zeppelin!");
msg.channel.send("Server was eligible and is now allowed to use Zeppelin!");
},
});

View file

@ -1,7 +1,7 @@
import { ApiPermissions } from "@zeppelinbot/shared/apiPermissions.js";
import moment from "moment-timezone";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { isStaffPreFilter, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { isStaffPreFilter } from "../../../pluginUtils.js";
import { DBDateFormat, isSnowflake } from "../../../utils.js";
import { botControlCmd } from "../types.js";
@ -20,17 +20,17 @@ export const AllowServerCmd = botControlCmd({
async run({ pluginData, message: msg, args }) {
const existing = await pluginData.state.allowedGuilds.find(args.guildId);
if (existing) {
sendErrorMessage(pluginData, msg.channel, "Server is already allowed!");
void msg.channel.send("Server is already allowed!");
return;
}
if (!isSnowflake(args.guildId)) {
sendErrorMessage(pluginData, msg.channel, "Invalid server ID!");
void msg.channel.send("Invalid server ID!");
return;
}
if (args.userId && !isSnowflake(args.userId)) {
sendErrorMessage(pluginData, msg.channel, "Invalid user ID!");
void msg.channel.send("Invalid user ID!");
return;
}
@ -51,6 +51,6 @@ export const AllowServerCmd = botControlCmd({
);
}
sendSuccessMessage(pluginData, msg.channel, "Server is now allowed to use Zeppelin!");
void msg.channel.send("Server is now allowed to use Zeppelin!");
},
});

View file

@ -1,5 +1,5 @@
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { isStaffPreFilter, sendErrorMessage } from "../../../pluginUtils.js";
import { isStaffPreFilter } from "../../../pluginUtils.js";
import { botControlCmd } from "../types.js";
export const ChannelToServerCmd = botControlCmd({
@ -16,7 +16,7 @@ export const ChannelToServerCmd = botControlCmd({
async run({ pluginData, message: msg, args }) {
const channel = pluginData.client.channels.cache.get(args.channelId);
if (!channel) {
sendErrorMessage(pluginData, msg.channel, "Channel not found in cache!");
void msg.channel.send("Channel not found in cache!");
return;
}

View file

@ -1,6 +1,6 @@
import { Snowflake } from "discord.js";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { isStaffPreFilter, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { isStaffPreFilter } from "../../../pluginUtils.js";
import { noop } from "../../../utils.js";
import { botControlCmd } from "../types.js";
@ -18,7 +18,7 @@ export const DisallowServerCmd = botControlCmd({
async run({ pluginData, message: msg, args }) {
const existing = await pluginData.state.allowedGuilds.find(args.guildId);
if (!existing) {
sendErrorMessage(pluginData, msg.channel, "That server is not allowed in the first place!");
void msg.channel.send("That server is not allowed in the first place!");
return;
}
@ -27,6 +27,6 @@ export const DisallowServerCmd = botControlCmd({
.get(args.guildId as Snowflake)
?.leave()
.catch(noop);
sendSuccessMessage(pluginData, msg.channel, "Server removed!");
void msg.channel.send("Server removed!");
},
});

View file

@ -1,5 +1,4 @@
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { isGuildInvite, resolveInvite } from "../../../utils.js";
import { isEligible } from "../functions/isEligible.js";
import { botControlCmd } from "../types.js";
@ -16,17 +15,17 @@ export const EligibleCmd = botControlCmd({
async run({ pluginData, message: msg, args }) {
const invite = await resolveInvite(pluginData.client, args.inviteCode, true);
if (!invite || !isGuildInvite(invite)) {
sendErrorMessage(pluginData, msg.channel, "Could not resolve invite");
void msg.channel.send("Could not resolve invite");
return;
}
const { result, explanation } = await isEligible(pluginData, args.user, invite);
if (result) {
sendSuccessMessage(pluginData, msg.channel, `Server is eligible: ${explanation}`);
void msg.channel.send(`Server is eligible: ${explanation}`);
return;
}
sendErrorMessage(pluginData, msg.channel, `Server is **NOT** eligible: ${explanation}`);
void msg.channel.send(`Server is **NOT** eligible: ${explanation}`);
},
});

View file

@ -1,6 +1,6 @@
import { Snowflake } from "discord.js";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { isStaffPreFilter, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { isStaffPreFilter } from "../../../pluginUtils.js";
import { botControlCmd } from "../types.js";
export const LeaveServerCmd = botControlCmd({
@ -16,7 +16,7 @@ export const LeaveServerCmd = botControlCmd({
async run({ pluginData, message: msg, args }) {
if (!pluginData.client.guilds.cache.has(args.guildId as Snowflake)) {
sendErrorMessage(pluginData, msg.channel, "I am not in that guild");
void msg.channel.send("I am not in that guild");
return;
}
@ -26,10 +26,10 @@ export const LeaveServerCmd = botControlCmd({
try {
await pluginData.client.guilds.cache.get(args.guildId as Snowflake)?.leave();
} catch (e) {
sendErrorMessage(pluginData, msg.channel, `Failed to leave guild: ${e.message}`);
void msg.channel.send(`Failed to leave guild: ${e.message}`);
return;
}
sendSuccessMessage(pluginData, msg.channel, `Left guild **${guildName}**`);
void msg.channel.send(`Left guild **${guildName}**`);
},
});

View file

@ -1,7 +1,6 @@
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { AllowedGuild } from "../../../data/entities/AllowedGuild.js";
import { ApiPermissionAssignment } from "../../../data/entities/ApiPermissionAssignment.js";
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { renderUsername, resolveUser } from "../../../utils.js";
import { botControlCmd } from "../types.js";
@ -16,7 +15,7 @@ export const ListDashboardPermsCmd = botControlCmd({
async run({ pluginData, message: msg, args }) {
if (!args.user && !args.guildId) {
sendErrorMessage(pluginData, msg.channel, "Must specify at least guildId, user, or both.");
void msg.channel.send("Must specify at least guildId, user, or both.");
return;
}
@ -24,7 +23,7 @@ export const ListDashboardPermsCmd = botControlCmd({
if (args.guildId) {
guild = await pluginData.state.allowedGuilds.find(args.guildId);
if (!guild) {
sendErrorMessage(pluginData, msg.channel, "Server is not using Zeppelin");
void msg.channel.send("Server is not using Zeppelin");
return;
}
}
@ -33,7 +32,7 @@ export const ListDashboardPermsCmd = botControlCmd({
if (args.user) {
existingUserAssignment = await pluginData.state.apiPermissionAssignments.getByUserId(args.user.id);
if (existingUserAssignment.length === 0) {
sendErrorMessage(pluginData, msg.channel, "The user has no assigned permissions.");
void msg.channel.send("The user has no assigned permissions.");
return;
}
}
@ -54,11 +53,7 @@ export const ListDashboardPermsCmd = botControlCmd({
}
if (finalMessage === "") {
sendErrorMessage(
pluginData,
msg.channel,
`The user ${userInfo} has no assigned permissions on the specified server.`,
);
msg.channel.send(`The user ${userInfo} has no assigned permissions on the specified server.`);
return;
}
// Else display all users that have permissions on the specified guild
@ -67,7 +62,7 @@ export const ListDashboardPermsCmd = botControlCmd({
const existingGuildAssignment = await pluginData.state.apiPermissionAssignments.getByGuildId(guild.id);
if (existingGuildAssignment.length === 0) {
sendErrorMessage(pluginData, msg.channel, `The server ${guildInfo} has no assigned permissions.`);
msg.channel.send(`The server ${guildInfo} has no assigned permissions.`);
return;
}
@ -80,6 +75,9 @@ export const ListDashboardPermsCmd = botControlCmd({
}
}
await sendSuccessMessage(pluginData, msg.channel, finalMessage.trim(), {});
await msg.channel.send({
content: finalMessage.trim(),
allowedMentions: {},
});
},
});

View file

@ -1,5 +1,4 @@
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { renderUsername, resolveUser } from "../../../utils.js";
import { botControlCmd } from "../types.js";
@ -14,7 +13,7 @@ export const ListDashboardUsersCmd = botControlCmd({
async run({ pluginData, message: msg, args }) {
const guild = await pluginData.state.allowedGuilds.find(args.guildId);
if (!guild) {
sendErrorMessage(pluginData, msg.channel, "Server is not using Zeppelin");
void msg.channel.send("Server is not using Zeppelin");
return;
}
@ -30,11 +29,9 @@ export const ListDashboardUsersCmd = botControlCmd({
`<@!${user.id}> (**${renderUsername(user)}**, \`${user.id}\`): ${permission.permissions.join(", ")}`,
);
sendSuccessMessage(
pluginData,
msg.channel,
`The following users have dashboard access for **${guild.name}**:\n\n${userNameList.join("\n")}`,
{},
);
msg.channel.send({
content: `The following users have dashboard access for **${guild.name}**:\n\n${userNameList.join("\n")}`,
allowedMentions: {},
});
},
});

View file

@ -1,6 +1,6 @@
import moment from "moment-timezone";
import { GuildArchives } from "../../../data/GuildArchives.js";
import { getBaseUrl, sendSuccessMessage } from "../../../pluginUtils.js";
import { getBaseUrl } from "../../../pluginUtils.js";
import { getRateLimitStats } from "../../../rateLimitStats.js";
import { botControlCmd } from "../types.js";
@ -13,7 +13,7 @@ export const RateLimitPerformanceCmd = botControlCmd({
async run({ pluginData, message: msg }) {
const logItems = getRateLimitStats();
if (logItems.length === 0) {
sendSuccessMessage(pluginData, msg.channel, `No rate limits hit`);
void msg.channel.send(`No rate limits hit`);
return;
}

View file

@ -1,4 +1,4 @@
import { isStaffPreFilter, sendErrorMessage } from "../../../pluginUtils.js";
import { isStaffPreFilter } from "../../../pluginUtils.js";
import { getActiveReload, setActiveReload } from "../activeReload.js";
import { botControlCmd } from "../types.js";
@ -14,7 +14,7 @@ export const ReloadGlobalPluginsCmd = botControlCmd({
const guildId = "guild" in message.channel ? message.channel.guild.id : null;
if (!guildId) {
sendErrorMessage(pluginData, message.channel, "This command can only be used in a server");
void message.channel.send("This command can only be used in a server");
return;
}

View file

@ -1,6 +1,6 @@
import { Snowflake } from "discord.js";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { isStaffPreFilter, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { isStaffPreFilter } from "../../../pluginUtils.js";
import { botControlCmd } from "../types.js";
export const ReloadServerCmd = botControlCmd({
@ -16,18 +16,18 @@ export const ReloadServerCmd = botControlCmd({
async run({ pluginData, message: msg, args }) {
if (!pluginData.client.guilds.cache.has(args.guildId as Snowflake)) {
sendErrorMessage(pluginData, msg.channel, "I am not in that guild");
void msg.channel.send("I am not in that guild");
return;
}
try {
await pluginData.getKnubInstance().reloadGuild(args.guildId);
} catch (e) {
sendErrorMessage(pluginData, msg.channel, `Failed to reload guild: ${e.message}`);
void msg.channel.send(`Failed to reload guild: ${e.message}`);
return;
}
const guild = await pluginData.client.guilds.fetch(args.guildId as Snowflake);
sendSuccessMessage(pluginData, msg.channel, `Reloaded guild **${guild?.name || "???"}**`);
void msg.channel.send(`Reloaded guild **${guild?.name || "???"}**`);
},
});

View file

@ -1,5 +1,5 @@
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { isStaffPreFilter, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { isStaffPreFilter } from "../../../pluginUtils.js";
import { renderUsername } from "../../../utils.js";
import { botControlCmd } from "../types.js";
@ -18,7 +18,7 @@ export const RemoveDashboardUserCmd = botControlCmd({
async run({ pluginData, message: msg, args }) {
const guild = await pluginData.state.allowedGuilds.find(args.guildId);
if (!guild) {
sendErrorMessage(pluginData, msg.channel, "Server is not using Zeppelin");
void msg.channel.send("Server is not using Zeppelin");
return;
}
@ -35,10 +35,7 @@ export const RemoveDashboardUserCmd = botControlCmd({
}
const userNameList = args.users.map((user) => `<@!${user.id}> (**${renderUsername(user)}**, \`${user.id}\`)`);
sendSuccessMessage(
pluginData,
msg.channel,
`The following users were removed from the dashboard for **${guild.name}**:\n\n${userNameList}`,
);
msg.channel.send(`The following users were removed from the dashboard for **${guild.name}**:\n\n${userNameList}`);
},
});

View file

@ -1,4 +1,5 @@
import { GuildPluginData } from "knub";
import { FindOptionsWhere } from "typeorm/find-options/FindOptionsWhere";
import { Case } from "../../../data/entities/Case.js";
import { CasesPluginType } from "../types.js";
@ -7,6 +8,7 @@ export function getRecentCasesByMod(
modId: string,
count: number,
skip = 0,
filters: Omit<FindOptionsWhere<Case>, "guild_id" | "mod_id" | "is_hidden"> = {},
): Promise<Case[]> {
return pluginData.state.cases.getRecentByModId(modId, count, skip);
return pluginData.state.cases.getRecentByModId(modId, count, skip, filters);
}

View file

@ -1,6 +1,12 @@
import { GuildPluginData } from "knub";
import { FindOptionsWhere } from "typeorm/find-options/FindOptionsWhere";
import { Case } from "../../../data/entities/Case.js";
import { CasesPluginType } from "../types.js";
export function getTotalCasesByMod(pluginData: GuildPluginData<CasesPluginType>, modId: string): Promise<number> {
return pluginData.state.cases.getTotalCasesByModId(modId);
export function getTotalCasesByMod(
pluginData: GuildPluginData<CasesPluginType>,
modId: string,
filters: Omit<FindOptionsWhere<Case>, "guild_id" | "mod_id" | "is_hidden"> = {},
): Promise<number> {
return pluginData.state.cases.getTotalCasesByModId(modId, filters);
}

View file

@ -1,17 +1,11 @@
import { Invite } from "discord.js";
import escapeStringRegexp from "escape-string-regexp";
import { GuildPluginData } from "knub";
import cloneDeep from "lodash/cloneDeep.js";
import cloneDeep from "lodash.clonedeep";
import { allowTimeout } from "../../../RegExpRunner.js";
import { ZalgoRegex } from "../../../data/Zalgo.js";
import { ISavedMessageEmbedData, SavedMessage } from "../../../data/entities/SavedMessage.js";
import {
getInviteCodesInString,
getUrlsInString,
isGuildInvite,
resolveInvite,
resolveMember,
} from "../../../utils.js";
import { getInviteCodesInString, getUrlsInString, isGuildInvite, resolveInvite, resolveMember } from "../../../utils.js";
import { CensorPluginType } from "../types.js";
import { censorMessage } from "./censorMessage.js";

View file

@ -1,5 +1,6 @@
import { guildPlugin } from "knub";
import z from "zod";
import { CommonPlugin } from "../Common/CommonPlugin.js";
import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin.js";
import { ArchiveChannelCmd } from "./commands/ArchiveChannelCmd.js";
import { ChannelArchiverPluginType } from "./types.js";
@ -14,4 +15,8 @@ export const ChannelArchiverPlugin = guildPlugin<ChannelArchiverPluginType>()({
messageCommands: [
ArchiveChannelCmd,
],
beforeStart(pluginData) {
pluginData.state.common = pluginData.getPlugin(CommonPlugin);
},
});

View file

@ -1,7 +1,7 @@
import { Snowflake } from "discord.js";
import moment from "moment-timezone";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { isOwner, sendErrorMessage } from "../../../pluginUtils.js";
import { isOwner } from "../../../pluginUtils.js";
import { SECONDS, confirm, noop, renderUsername } from "../../../utils.js";
import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin.js";
import { rehostAttachment } from "../rehostAttachment.js";
@ -32,12 +32,12 @@ export const ArchiveChannelCmd = channelArchiverCmd({
async run({ message: msg, args, pluginData }) {
if (!args["attachment-channel"]) {
const confirmed = await confirm(msg.channel, msg.author.id, {
const confirmed = await confirm(msg, msg.author.id, {
content:
"No `-attachment-channel` specified. Continue? Attachments will not be available in the log if their message is deleted.",
});
if (!confirmed) {
sendErrorMessage(pluginData, msg.channel, "Canceled");
void pluginData.state.common.sendErrorMessage(msg, "Canceled");
return;
}
}

View file

@ -1,5 +1,10 @@
import { BasePluginType, guildPluginMessageCommand } from "knub";
import { BasePluginType, guildPluginMessageCommand, pluginUtils } from "knub";
import { CommonPlugin } from "../Common/CommonPlugin.js";
export interface ChannelArchiverPluginType extends BasePluginType {}
export interface ChannelArchiverPluginType extends BasePluginType {
state: {
common: pluginUtils.PluginPublicInterface<typeof CommonPlugin>;
};
}
export const channelArchiverCmd = guildPluginMessageCommand<ChannelArchiverPluginType>();

View file

@ -0,0 +1,153 @@
import {
Attachment,
ChatInputCommandInteraction,
Message,
MessageCreateOptions,
MessageMentionOptions,
ModalSubmitInteraction,
TextBasedChannel,
User,
} from "discord.js";
import { PluginOptions, guildPlugin } from "knub";
import { logger } from "../../logger.js";
import { isContextInteraction, sendContextResponse } from "../../pluginUtils.js";
import { errorMessage, successMessage } from "../../utils.js";
import { getErrorEmoji, getSuccessEmoji } from "./functions/getEmoji.js";
import { CommonPluginType, zCommonConfig } from "./types.js";
const defaultOptions: PluginOptions<CommonPluginType> = {
config: {
success_emoji: "✅",
error_emoji: "❌",
attachment_storing_channel: null,
},
};
export const CommonPlugin = guildPlugin<CommonPluginType>()({
name: "common",
dependencies: () => [],
configParser: (input) => zCommonConfig.parse(input),
defaultOptions,
public(pluginData) {
return {
getSuccessEmoji,
getErrorEmoji,
sendSuccessMessage: async (
context: TextBasedChannel | Message | User | ChatInputCommandInteraction,
body: string,
allowedMentions?: MessageMentionOptions,
responseInteraction?: ModalSubmitInteraction,
ephemeral = true,
): Promise<Message | undefined> => {
const emoji = getSuccessEmoji(pluginData);
const formattedBody = successMessage(body, emoji);
const content: MessageCreateOptions = allowedMentions
? { content: formattedBody, allowedMentions }
: { content: formattedBody };
if (responseInteraction) {
await responseInteraction
.editReply({ content: formattedBody, embeds: [], components: [] })
.catch((err) => logger.error(`Interaction reply failed: ${err}`));
return;
}
if (!isContextInteraction(context)) {
// noinspection TypeScriptValidateJSTypes
return sendContextResponse(context, { ...content }) // Force line break
.catch((err) => {
const channelInfo =
"guild" in context && context.guild ? `${context.id} (${context.guild.id})` : context.id;
logger.warn(`Failed to send success message to ${channelInfo}): ${err.code} ${err.message}`);
return undefined;
});
}
const replyMethod = context.replied || context.deferred ? "editReply" : "reply";
return context[replyMethod]({
content: formattedBody,
embeds: [],
components: [],
fetchReply: true,
ephemeral,
}).catch((err) => {
logger.error(`Context reply failed: ${err}`);
return undefined;
}) as Promise<Message>;
},
sendErrorMessage: async (
context: TextBasedChannel | Message | User | ChatInputCommandInteraction,
body: string,
allowedMentions?: MessageMentionOptions,
responseInteraction?: ModalSubmitInteraction,
ephemeral = true,
): Promise<Message | undefined> => {
const emoji = getErrorEmoji(pluginData);
const formattedBody = errorMessage(body, emoji);
const content: MessageCreateOptions = allowedMentions
? { content: formattedBody, allowedMentions }
: { content: formattedBody };
if (responseInteraction) {
await responseInteraction
.editReply({ content: formattedBody, embeds: [], components: [] })
.catch((err) => logger.error(`Interaction reply failed: ${err}`));
return;
}
if (!isContextInteraction(context)) {
// noinspection TypeScriptValidateJSTypes
return sendContextResponse(context, { ...content }) // Force line break
.catch((err) => {
const channelInfo =
"guild" in context && context.guild ? `${context.id} (${context.guild.id})` : context.id;
logger.warn(`Failed to send error message to ${channelInfo}): ${err.code} ${err.message}`);
return undefined;
});
}
const replyMethod = context.replied || context.deferred ? "editReply" : "reply";
return context[replyMethod]({
content: formattedBody,
embeds: [],
components: [],
fetchReply: true,
ephemeral,
}).catch((err) => {
logger.error(`Context reply failed: ${err}`);
return undefined;
}) as Promise<Message>;
},
storeAttachmentsAsMessage: async (attachments: Attachment[], backupChannel?: TextBasedChannel | null) => {
const attachmentChannelId = pluginData.config.get().attachment_storing_channel;
const channel = attachmentChannelId
? (pluginData.guild.channels.cache.get(attachmentChannelId) as TextBasedChannel) ?? backupChannel
: backupChannel;
if (!channel) {
throw new Error(
"Cannot store attachments: no attachment storing channel configured, and no backup channel passed",
);
}
return channel!.send({
content: `Storing ${attachments.length} attachment${attachments.length === 1 ? "" : "s"}`,
files: attachments.map((a) => a.url),
});
},
};
},
});

View file

@ -0,0 +1,9 @@
import { ZeppelinPluginDocs } from "../../types.js";
import { zCommonConfig } from "./types.js";
export const commonPluginDocs: ZeppelinPluginDocs = {
type: "internal",
configSchema: zCommonConfig,
prettyName: "Common",
};

View file

@ -0,0 +1,10 @@
import { GuildPluginData } from "knub";
import { CommonPluginType } from "../types.js";
export function getSuccessEmoji(pluginData: GuildPluginData<CommonPluginType>) {
return pluginData.config.get().success_emoji ?? "✅";
}
export function getErrorEmoji(pluginData: GuildPluginData<CommonPluginType>) {
return pluginData.config.get().error_emoji ?? "❌";
}

View file

@ -0,0 +1,12 @@
import { BasePluginType } from "knub";
import z from "zod";
export const zCommonConfig = z.strictObject({
success_emoji: z.string(),
error_emoji: z.string(),
attachment_storing_channel: z.nullable(z.string()),
});
export interface CommonPluginType extends BasePluginType {
config: z.output<typeof zCommonConfig>;
}

View file

@ -1,30 +1,31 @@
import { PluginOptions, guildPlugin } from "knub";
import { GuildContextMenuLinks } from "../../data/GuildContextMenuLinks.js";
import { GuildCases } from "../../data/GuildCases.js";
import { CasesPlugin } from "../Cases/CasesPlugin.js";
import { LogsPlugin } from "../Logs/LogsPlugin.js";
import { ModActionsPlugin } from "../ModActions/ModActionsPlugin.js";
import { MutesPlugin } from "../Mutes/MutesPlugin.js";
import { UtilityPlugin } from "../Utility/UtilityPlugin.js";
import { ContextClickedEvt } from "./events/ContextClickedEvt.js";
import { BanCmd } from "./commands/BanUserCtxCmd.js";
import { CleanCmd } from "./commands/CleanMessageCtxCmd.js";
import { ModMenuCmd } from "./commands/ModMenuUserCtxCmd.js";
import { MuteCmd } from "./commands/MuteUserCtxCmd.js";
import { NoteCmd } from "./commands/NoteUserCtxCmd.js";
import { WarnCmd } from "./commands/WarnUserCtxCmd.js";
import { ContextMenuPluginType, zContextMenusConfig } from "./types.js";
import { loadAllCommands } from "./utils/loadAllCommands.js";
const defaultOptions: PluginOptions<ContextMenuPluginType> = {
config: {
can_use: false,
user_muteindef: false,
user_mute1d: false,
user_mute1h: false,
user_info: false,
message_clean10: false,
message_clean25: false,
message_clean50: false,
can_open_mod_menu: false,
},
overrides: [
{
level: ">=50",
config: {
can_use: true,
can_open_mod_menu: true,
},
},
],
@ -33,22 +34,15 @@ const defaultOptions: PluginOptions<ContextMenuPluginType> = {
export const ContextMenuPlugin = guildPlugin<ContextMenuPluginType>()({
name: "context_menu",
dependencies: () => [MutesPlugin, LogsPlugin, UtilityPlugin],
dependencies: () => [CasesPlugin, MutesPlugin, ModActionsPlugin, LogsPlugin, UtilityPlugin],
configParser: (input) => zContextMenusConfig.parse(input),
defaultOptions,
// prettier-ignore
events: [
ContextClickedEvt,
],
contextMenuCommands: [ModMenuCmd, NoteCmd, WarnCmd, MuteCmd, BanCmd, CleanCmd],
beforeLoad(pluginData) {
const { state, guild } = pluginData;
state.contextMenuLinks = new GuildContextMenuLinks(guild.id);
},
afterLoad(pluginData) {
loadAllCommands(pluginData);
state.cases = GuildCases.getGuildInstance(guild.id);
},
});

View file

@ -0,0 +1,116 @@
import {
ActionRowBuilder,
ButtonInteraction,
ContextMenuCommandInteraction,
ModalBuilder,
ModalSubmitInteraction,
TextInputBuilder,
TextInputStyle,
} from "discord.js";
import humanizeDuration from "humanize-duration";
import { GuildPluginData } from "knub";
import { logger } from "../../../logger.js";
import { canActOn } from "../../../pluginUtils.js";
import { convertDelayStringToMS, renderUserUsername } from "../../../utils.js";
import { CaseArgs } from "../../Cases/types.js";
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin.js";
import { MODAL_TIMEOUT } from "../commands/ModMenuUserCtxCmd.js";
import { ContextMenuPluginType, ModMenuActionType } from "../types.js";
import { updateAction } from "./update.js";
async function banAction(
pluginData: GuildPluginData<ContextMenuPluginType>,
duration: string | undefined,
reason: string | undefined,
evidence: string | undefined,
target: string,
interaction: ButtonInteraction | ContextMenuCommandInteraction,
submitInteraction: ModalSubmitInteraction,
) {
const interactionToReply = interaction.isButton() ? interaction : submitInteraction;
const executingMember = await pluginData.guild.members.fetch(interaction.user.id);
const userCfg = await pluginData.config.getMatchingConfig({
channelId: interaction.channelId,
member: executingMember,
});
const modactions = pluginData.getPlugin(ModActionsPlugin);
if (!userCfg.can_use || !(await modactions.hasBanPermission(executingMember, interaction.channelId))) {
await interactionToReply.editReply({ content: "Cannot ban: insufficient permissions", embeds: [], components: [] });
return;
}
const targetMember = await pluginData.guild.members.fetch(target);
if (!canActOn(pluginData, executingMember, targetMember)) {
await interactionToReply.editReply({ content: "Cannot ban: insufficient permissions", embeds: [], components: [] });
return;
}
const caseArgs: Partial<CaseArgs> = {
modId: executingMember.id,
};
const durationMs = duration ? convertDelayStringToMS(duration)! : undefined;
const result = await modactions.banUserId(target, reason, reason, { caseArgs }, durationMs);
if (result.status === "failed") {
await interactionToReply.editReply({ content: "Error: Failed to ban user", embeds: [], components: [] });
return;
}
const userName = renderUserUsername(targetMember.user);
const messageResultText = result.notifyResult.text ? ` (${result.notifyResult.text})` : "";
const banMessage = `Banned **${userName}** ${
durationMs ? `for ${humanizeDuration(durationMs)}` : "indefinitely"
} (Case #${result.case.case_number})${messageResultText}`;
if (evidence) {
await updateAction(pluginData, executingMember, result.case, evidence);
}
await interactionToReply.editReply({ content: banMessage, embeds: [], components: [] });
}
export async function launchBanActionModal(
pluginData: GuildPluginData<ContextMenuPluginType>,
interaction: ButtonInteraction | ContextMenuCommandInteraction,
target: string,
) {
const modalId = `${ModMenuActionType.BAN}:${interaction.id}`;
const modal = new ModalBuilder().setCustomId(modalId).setTitle("Ban");
const durationIn = new TextInputBuilder()
.setCustomId("duration")
.setLabel("Duration (Optional)")
.setRequired(false)
.setStyle(TextInputStyle.Short);
const reasonIn = new TextInputBuilder()
.setCustomId("reason")
.setLabel("Reason (Optional)")
.setRequired(false)
.setStyle(TextInputStyle.Paragraph);
const evidenceIn = new TextInputBuilder()
.setCustomId("evidence")
.setLabel("Evidence (Optional)")
.setRequired(false)
.setStyle(TextInputStyle.Paragraph);
const durationRow = new ActionRowBuilder<TextInputBuilder>().addComponents(durationIn);
const reasonRow = new ActionRowBuilder<TextInputBuilder>().addComponents(reasonIn);
const evidenceRow = new ActionRowBuilder<TextInputBuilder>().addComponents(evidenceIn);
modal.addComponents(durationRow, reasonRow, evidenceRow);
await interaction.showModal(modal);
await interaction
.awaitModalSubmit({ time: MODAL_TIMEOUT, filter: (i) => i.customId == modalId })
.then(async (submitted) => {
if (interaction.isButton()) {
await submitted.deferUpdate().catch((err) => logger.error(`Ban interaction defer failed: ${err}`));
} else if (interaction.isContextMenuCommand()) {
await submitted.deferReply({ ephemeral: true });
}
const duration = submitted.fields.getTextInputValue("duration");
const reason = submitted.fields.getTextInputValue("reason");
const evidence = submitted.fields.getTextInputValue("evidence");
await banAction(pluginData, duration, reason, evidence, target, interaction, submitted);
});
}

View file

@ -1,16 +1,26 @@
import { ContextMenuCommandInteraction, TextChannel } from "discord.js";
import {
ActionRowBuilder,
Message,
MessageContextMenuCommandInteraction,
ModalBuilder,
ModalSubmitInteraction,
TextInputBuilder,
TextInputStyle,
} from "discord.js";
import { GuildPluginData } from "knub";
import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError.js";
import { logger } from "../../../logger.js";
import { UtilityPlugin } from "../../../plugins/Utility/UtilityPlugin.js";
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
import { ContextMenuPluginType } from "../types.js";
import { MODAL_TIMEOUT } from "../commands/ModMenuUserCtxCmd.js";
import { ContextMenuPluginType, ModMenuActionType } from "../types.js";
export async function cleanAction(
pluginData: GuildPluginData<ContextMenuPluginType>,
amount: number,
interaction: ContextMenuCommandInteraction,
target: string,
targetMessage: Message,
targetChannel: string,
interaction: ModalSubmitInteraction,
) {
await interaction.deferReply({ ephemeral: true });
const executingMember = await pluginData.guild.members.fetch(interaction.user.id);
const userCfg = await pluginData.config.getMatchingConfig({
channelId: interaction.channelId,
@ -18,33 +28,54 @@ export async function cleanAction(
});
const utility = pluginData.getPlugin(UtilityPlugin);
if (!userCfg.can_use || !(await utility.hasPermission(executingMember, interaction.channelId, "can_clean"))) {
await interaction.followUp({ content: "Cannot clean: insufficient permissions" });
if (!userCfg.can_use || !(await utility.hasPermission(executingMember, targetChannel, "can_clean"))) {
await interaction
.editReply({ content: "Cannot clean: insufficient permissions", embeds: [], components: [] })
.catch((err) => logger.error(`Clean interaction reply failed: ${err}`));
return;
}
const targetMessage = interaction.channel
? await interaction.channel.messages.fetch(interaction.targetId)
: await (pluginData.guild.channels.resolve(interaction.channelId) as TextChannel).messages.fetch(
interaction.targetId,
);
await interaction
.editReply({
content: `Cleaning ${amount} messages from ${target}...`,
embeds: [],
components: [],
})
.catch((err) => logger.error(`Clean interaction reply failed: ${err}`));
const targetUserOnly = false;
const deletePins = false;
const user = undefined;
try {
await interaction.followUp(`Cleaning... Amount: ${amount}, User Only: ${targetUserOnly}, Pins: ${deletePins}`);
utility.clean({ count: amount, user, channel: targetMessage.channel.id, "delete-pins": deletePins }, targetMessage);
} catch (e) {
await interaction.followUp({ ephemeral: true, content: "Plugin error, please check your BOT_ALERTs" });
if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) {
pluginData.getPlugin(LogsPlugin).logBotAlert({
body: `Failed to clean in <#${interaction.channelId}> in ContextMenu action \`clean\`:_ ${e}`,
});
} else {
throw e;
}
}
await utility.clean({ count: amount, channel: targetChannel, "response-interaction": interaction }, targetMessage);
}
export async function launchCleanActionModal(
pluginData: GuildPluginData<ContextMenuPluginType>,
interaction: MessageContextMenuCommandInteraction,
target: string,
) {
const modalId = `${ModMenuActionType.CLEAN}:${interaction.id}`;
const modal = new ModalBuilder().setCustomId(modalId).setTitle("Clean");
const amountIn = new TextInputBuilder().setCustomId("amount").setLabel("Amount").setStyle(TextInputStyle.Short);
const amountRow = new ActionRowBuilder<TextInputBuilder>().addComponents(amountIn);
modal.addComponents(amountRow);
await interaction.showModal(modal);
await interaction
.awaitModalSubmit({ time: MODAL_TIMEOUT, filter: (i) => i.customId == modalId })
.then(async (submitted) => {
await submitted.deferReply({ ephemeral: true });
const amount = submitted.fields.getTextInputValue("amount");
if (isNaN(Number(amount))) {
interaction.editReply({ content: `Error: Amount '${amount}' is invalid`, embeds: [], components: [] });
return;
}
await cleanAction(
pluginData,
Number(amount),
target,
interaction.targetMessage,
interaction.channelId,
submitted,
);
});
}

View file

@ -1,21 +1,36 @@
import { ContextMenuCommandInteraction } from "discord.js";
import {
ActionRowBuilder,
ButtonInteraction,
ContextMenuCommandInteraction,
ModalBuilder,
ModalSubmitInteraction,
TextInputBuilder,
TextInputStyle,
} from "discord.js";
import humanizeDuration from "humanize-duration";
import { GuildPluginData } from "knub";
import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError.js";
import { logger } from "../../../logger.js";
import { canActOn } from "../../../pluginUtils.js";
import { convertDelayStringToMS } from "../../../utils.js";
import { CaseArgs } from "../../Cases/types.js";
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin.js";
import { MutesPlugin } from "../../Mutes/MutesPlugin.js";
import { ContextMenuPluginType } from "../types.js";
import { MODAL_TIMEOUT } from "../commands/ModMenuUserCtxCmd.js";
import { ContextMenuPluginType, ModMenuActionType } from "../types.js";
import { updateAction } from "./update.js";
export async function muteAction(
async function muteAction(
pluginData: GuildPluginData<ContextMenuPluginType>,
duration: string | undefined,
interaction: ContextMenuCommandInteraction,
reason: string | undefined,
evidence: string | undefined,
target: string,
interaction: ButtonInteraction | ContextMenuCommandInteraction,
submitInteraction: ModalSubmitInteraction,
) {
await interaction.deferReply({ ephemeral: true });
const interactionToReply = interaction.isButton() ? interaction : submitInteraction;
const executingMember = await pluginData.guild.members.fetch(interaction.user.id);
const userCfg = await pluginData.config.getMatchingConfig({
channelId: interaction.channelId,
@ -24,43 +39,100 @@ export async function muteAction(
const modactions = pluginData.getPlugin(ModActionsPlugin);
if (!userCfg.can_use || !(await modactions.hasMutePermission(executingMember, interaction.channelId))) {
await interaction.followUp({ content: "Cannot mute: insufficient permissions" });
await interactionToReply.editReply({
content: "Cannot mute: insufficient permissions",
embeds: [],
components: [],
});
return;
}
const durationMs = duration ? convertDelayStringToMS(duration)! : undefined;
const mutes = pluginData.getPlugin(MutesPlugin);
const userId = interaction.targetId;
const targetMember = await pluginData.guild.members.fetch(interaction.targetId);
const targetMember = await pluginData.guild.members.fetch(target);
if (!canActOn(pluginData, executingMember, targetMember)) {
await interaction.followUp({ ephemeral: true, content: "Cannot mute: insufficient permissions" });
await interactionToReply.editReply({
content: "Cannot mute: insufficient permissions",
embeds: [],
components: [],
});
return;
}
const caseArgs: Partial<CaseArgs> = {
modId: executingMember.id,
};
const mutes = pluginData.getPlugin(MutesPlugin);
const durationMs = duration ? convertDelayStringToMS(duration)! : undefined;
try {
const result = await mutes.muteUser(userId, durationMs, "Context Menu Action", { caseArgs });
const result = await mutes.muteUser(target, durationMs, reason, reason, { caseArgs });
const messageResultText = result.notifyResult.text ? ` (${result.notifyResult.text})` : "";
const muteMessage = `Muted **${result.case!.user_name}** ${
durationMs ? `for ${humanizeDuration(durationMs)}` : "indefinitely"
} (Case #${result.case!.case_number}) (user notified via ${
result.notifyResult.method ?? "dm"
})\nPlease update the new case with the \`update\` command`;
} (Case #${result.case!.case_number})${messageResultText}`;
await interaction.followUp({ ephemeral: true, content: muteMessage });
if (evidence) {
await updateAction(pluginData, executingMember, result.case!, evidence);
}
await interactionToReply.editReply({ content: muteMessage, embeds: [], components: [] });
} catch (e) {
await interaction.followUp({ ephemeral: true, content: "Plugin error, please check your BOT_ALERTs" });
await interactionToReply.editReply({
content: "Plugin error, please check your BOT_ALERTs",
embeds: [],
components: [],
});
if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) {
pluginData.getPlugin(LogsPlugin).logBotAlert({
body: `Failed to mute <@!${userId}> in ContextMenu action \`mute\` because a mute role has not been specified in server config`,
body: `Failed to mute <@!${target}> in ContextMenu action \`mute\` because a mute role has not been specified in server config`,
});
} else {
throw e;
}
}
}
export async function launchMuteActionModal(
pluginData: GuildPluginData<ContextMenuPluginType>,
interaction: ButtonInteraction | ContextMenuCommandInteraction,
target: string,
) {
const modalId = `${ModMenuActionType.MUTE}:${interaction.id}`;
const modal = new ModalBuilder().setCustomId(modalId).setTitle("Mute");
const durationIn = new TextInputBuilder()
.setCustomId("duration")
.setLabel("Duration (Optional)")
.setRequired(false)
.setStyle(TextInputStyle.Short);
const reasonIn = new TextInputBuilder()
.setCustomId("reason")
.setLabel("Reason (Optional)")
.setRequired(false)
.setStyle(TextInputStyle.Paragraph);
const evidenceIn = new TextInputBuilder()
.setCustomId("evidence")
.setLabel("Evidence (Optional)")
.setRequired(false)
.setStyle(TextInputStyle.Paragraph);
const durationRow = new ActionRowBuilder<TextInputBuilder>().addComponents(durationIn);
const reasonRow = new ActionRowBuilder<TextInputBuilder>().addComponents(reasonIn);
const evidenceRow = new ActionRowBuilder<TextInputBuilder>().addComponents(evidenceIn);
modal.addComponents(durationRow, reasonRow, evidenceRow);
await interaction.showModal(modal);
await interaction
.awaitModalSubmit({ time: MODAL_TIMEOUT, filter: (i) => i.customId == modalId })
.then(async (submitted) => {
if (interaction.isButton()) {
await submitted.deferUpdate().catch((err) => logger.error(`Mute interaction defer failed: ${err}`));
} else if (interaction.isContextMenuCommand()) {
await submitted.deferReply({ ephemeral: true });
}
const duration = submitted.fields.getTextInputValue("duration");
const reason = submitted.fields.getTextInputValue("reason");
const evidence = submitted.fields.getTextInputValue("evidence");
await muteAction(pluginData, duration, reason, evidence, target, interaction, submitted);
});
}

View file

@ -0,0 +1,103 @@
import {
ActionRowBuilder,
ButtonInteraction,
ContextMenuCommandInteraction,
ModalBuilder,
ModalSubmitInteraction,
TextInputBuilder,
TextInputStyle,
} from "discord.js";
import { GuildPluginData } from "knub";
import { CaseTypes } from "../../../data/CaseTypes.js";
import { logger } from "../../../logger.js";
import { canActOn } from "../../../pluginUtils.js";
import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin.js";
import { renderUserUsername } from "../../../utils.js";
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin.js";
import { MODAL_TIMEOUT } from "../commands/ModMenuUserCtxCmd.js";
import { ContextMenuPluginType, ModMenuActionType } from "../types.js";
async function noteAction(
pluginData: GuildPluginData<ContextMenuPluginType>,
reason: string,
target: string,
interaction: ButtonInteraction | ContextMenuCommandInteraction,
submitInteraction: ModalSubmitInteraction,
) {
const interactionToReply = interaction.isButton() ? interaction : submitInteraction;
const executingMember = await pluginData.guild.members.fetch(interaction.user.id);
const userCfg = await pluginData.config.getMatchingConfig({
channelId: interaction.channelId,
member: executingMember,
});
const modactions = pluginData.getPlugin(ModActionsPlugin);
if (!userCfg.can_use || !(await modactions.hasNotePermission(executingMember, interaction.channelId))) {
await interactionToReply.editReply({
content: "Cannot note: insufficient permissions",
embeds: [],
components: [],
});
return;
}
const targetMember = await pluginData.guild.members.fetch(target);
if (!canActOn(pluginData, executingMember, targetMember)) {
await interactionToReply.editReply({
content: "Cannot note: insufficient permissions",
embeds: [],
components: [],
});
return;
}
const casesPlugin = pluginData.getPlugin(CasesPlugin);
const createdCase = await casesPlugin.createCase({
userId: target,
modId: executingMember.id,
type: CaseTypes.Note,
reason,
});
pluginData.getPlugin(LogsPlugin).logMemberNote({
mod: interaction.user,
user: targetMember.user,
caseNumber: createdCase.case_number,
reason,
});
const userName = renderUserUsername(targetMember.user);
await interactionToReply.editReply({
content: `Note added on **${userName}** (Case #${createdCase.case_number})`,
embeds: [],
components: [],
});
}
export async function launchNoteActionModal(
pluginData: GuildPluginData<ContextMenuPluginType>,
interaction: ButtonInteraction | ContextMenuCommandInteraction,
target: string,
) {
const modalId = `${ModMenuActionType.NOTE}:${interaction.id}`;
const modal = new ModalBuilder().setCustomId(modalId).setTitle("Note");
const reasonIn = new TextInputBuilder().setCustomId("reason").setLabel("Note").setStyle(TextInputStyle.Paragraph);
const reasonRow = new ActionRowBuilder<TextInputBuilder>().addComponents(reasonIn);
modal.addComponents(reasonRow);
await interaction.showModal(modal);
await interaction
.awaitModalSubmit({ time: MODAL_TIMEOUT, filter: (i) => i.customId == modalId })
.then(async (submitted) => {
if (interaction.isButton()) {
await submitted.deferUpdate().catch((err) => logger.error(`Note interaction defer failed: ${err}`));
} else if (interaction.isContextMenuCommand()) {
await submitted.deferReply({ ephemeral: true });
}
const reason = submitted.fields.getTextInputValue("reason");
await noteAction(pluginData, reason, target, interaction, submitted);
});
}

View file

@ -0,0 +1,28 @@
import { GuildMember } from "discord.js";
import { GuildPluginData } from "knub";
import { CaseTypes } from "../../../data/CaseTypes.js";
import { Case } from "../../../data/entities/Case.js";
import { CasesPlugin } from "../../Cases/CasesPlugin.js";
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
import { ContextMenuPluginType } from "../types.js";
export async function updateAction(
pluginData: GuildPluginData<ContextMenuPluginType>,
executingMember: GuildMember,
theCase: Case,
value: string,
) {
const casesPlugin = pluginData.getPlugin(CasesPlugin);
await casesPlugin.createCaseNote({
caseId: theCase.case_number,
modId: executingMember.id,
body: value,
});
void pluginData.getPlugin(LogsPlugin).logCaseUpdate({
mod: executingMember.user,
caseNumber: theCase.case_number,
caseType: CaseTypes[theCase.type],
note: value,
});
}

View file

@ -1,28 +0,0 @@
import { ContextMenuCommandInteraction } from "discord.js";
import { GuildPluginData } from "knub";
import { UtilityPlugin } from "../../../plugins/Utility/UtilityPlugin.js";
import { ContextMenuPluginType } from "../types.js";
export async function userInfoAction(
pluginData: GuildPluginData<ContextMenuPluginType>,
interaction: ContextMenuCommandInteraction,
) {
await interaction.deferReply({ ephemeral: true });
const executingMember = await pluginData.guild.members.fetch(interaction.user.id);
const userCfg = await pluginData.config.getMatchingConfig({
channelId: interaction.channelId,
member: executingMember,
});
const utility = pluginData.getPlugin(UtilityPlugin);
if (userCfg.can_use && (await utility.hasPermission(executingMember, interaction.channelId, "can_userinfo"))) {
const embed = await utility.userInfo(interaction.targetId);
if (!embed) {
await interaction.followUp({ content: "Cannot info: internal error" });
return;
}
await interaction.followUp({ embeds: [embed] });
} else {
await interaction.followUp({ content: "Cannot info: insufficient permissions" });
}
}

View file

@ -0,0 +1,108 @@
import {
ActionRowBuilder,
ButtonInteraction,
ContextMenuCommandInteraction,
ModalBuilder,
ModalSubmitInteraction,
TextInputBuilder,
TextInputStyle,
} from "discord.js";
import { GuildPluginData } from "knub";
import { logger } from "../../../logger.js";
import { canActOn } from "../../../pluginUtils.js";
import { renderUserUsername } from "../../../utils.js";
import { CaseArgs } from "../../Cases/types.js";
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin.js";
import { MODAL_TIMEOUT } from "../commands/ModMenuUserCtxCmd.js";
import { ContextMenuPluginType, ModMenuActionType } from "../types.js";
import { updateAction } from "./update.js";
async function warnAction(
pluginData: GuildPluginData<ContextMenuPluginType>,
reason: string,
evidence: string | undefined,
target: string,
interaction: ButtonInteraction | ContextMenuCommandInteraction,
submitInteraction: ModalSubmitInteraction,
) {
const interactionToReply = interaction.isButton() ? interaction : submitInteraction;
const executingMember = await pluginData.guild.members.fetch(interaction.user.id);
const userCfg = await pluginData.config.getMatchingConfig({
channelId: interaction.channelId,
member: executingMember,
});
const modactions = pluginData.getPlugin(ModActionsPlugin);
if (!userCfg.can_use || !(await modactions.hasWarnPermission(executingMember, interaction.channelId))) {
await interactionToReply.editReply({
content: "Cannot warn: insufficient permissions",
embeds: [],
components: [],
});
return;
}
const targetMember = await pluginData.guild.members.fetch(target);
if (!canActOn(pluginData, executingMember, targetMember)) {
await interactionToReply.editReply({
content: "Cannot warn: insufficient permissions",
embeds: [],
components: [],
});
return;
}
const caseArgs: Partial<CaseArgs> = {
modId: executingMember.id,
};
const result = await modactions.warnMember(targetMember, reason, reason, { caseArgs });
if (result.status === "failed") {
await interactionToReply.editReply({ content: "Error: Failed to warn user", embeds: [], components: [] });
return;
}
const userName = renderUserUsername(targetMember.user);
const messageResultText = result.notifyResult.text ? ` (${result.notifyResult.text})` : "";
const muteMessage = `Warned **${userName}** (Case #${result.case.case_number})${messageResultText}`;
if (evidence) {
await updateAction(pluginData, executingMember, result.case, evidence);
}
await interactionToReply.editReply({ content: muteMessage, embeds: [], components: [] });
}
export async function launchWarnActionModal(
pluginData: GuildPluginData<ContextMenuPluginType>,
interaction: ButtonInteraction | ContextMenuCommandInteraction,
target: string,
) {
const modalId = `${ModMenuActionType.WARN}:${interaction.id}`;
const modal = new ModalBuilder().setCustomId(modalId).setTitle("Warn");
const reasonIn = new TextInputBuilder().setCustomId("reason").setLabel("Reason").setStyle(TextInputStyle.Paragraph);
const evidenceIn = new TextInputBuilder()
.setCustomId("evidence")
.setLabel("Evidence (Optional)")
.setRequired(false)
.setStyle(TextInputStyle.Paragraph);
const reasonRow = new ActionRowBuilder<TextInputBuilder>().addComponents(reasonIn);
const evidenceRow = new ActionRowBuilder<TextInputBuilder>().addComponents(evidenceIn);
modal.addComponents(reasonRow, evidenceRow);
await interaction.showModal(modal);
await interaction
.awaitModalSubmit({ time: MODAL_TIMEOUT, filter: (i) => i.customId == modalId })
.then(async (submitted) => {
if (interaction.isButton()) {
await submitted.deferUpdate().catch((err) => logger.error(`Warn interaction defer failed: ${err}`));
} else if (interaction.isContextMenuCommand()) {
await submitted.deferReply({ ephemeral: true });
}
const reason = submitted.fields.getTextInputValue("reason");
const evidence = submitted.fields.getTextInputValue("evidence");
await warnAction(pluginData, reason, evidence, target, interaction, submitted);
});
}

View file

@ -0,0 +1,11 @@
import { PermissionFlagsBits } from "discord.js";
import { guildPluginUserContextMenuCommand } from "knub";
import { launchBanActionModal } from "../actions/ban.js";
export const BanCmd = guildPluginUserContextMenuCommand({
name: "Ban",
defaultMemberPermissions: PermissionFlagsBits.BanMembers.toString(),
async run({ pluginData, interaction }) {
await launchBanActionModal(pluginData, interaction, interaction.targetId);
},
});

View file

@ -0,0 +1,11 @@
import { PermissionFlagsBits } from "discord.js";
import { guildPluginMessageContextMenuCommand } from "knub";
import { launchCleanActionModal } from "../actions/clean.js";
export const CleanCmd = guildPluginMessageContextMenuCommand({
name: "Clean",
defaultMemberPermissions: PermissionFlagsBits.ManageMessages.toString(),
async run({ pluginData, interaction }) {
await launchCleanActionModal(pluginData, interaction, interaction.targetId);
},
});

View file

@ -0,0 +1,328 @@
import {
APIEmbed,
ActionRowBuilder,
ButtonBuilder,
ButtonInteraction,
ButtonStyle,
ContextMenuCommandInteraction,
GuildMember,
PermissionFlagsBits,
User,
} from "discord.js";
import { GuildPluginData, guildPluginUserContextMenuCommand } from "knub";
import { Case } from "../../../data/entities/Case.js";
import { logger } from "../../../logger.js";
import { SECONDS, UnknownUser, emptyEmbedValue, renderUserUsername, resolveUser, trimLines } from "../../../utils.js";
import { asyncMap } from "../../../utils/async.js";
import { getChunkedEmbedFields } from "../../../utils/getChunkedEmbedFields.js";
import { getGuildPrefix } from "../../../utils/getGuildPrefix.js";
import { CasesPlugin } from "../../Cases/CasesPlugin.js";
import { ModActionsPlugin } from "../../ModActions/ModActionsPlugin.js";
import { getUserInfoEmbed } from "../../Utility/functions/getUserInfoEmbed.js";
import { launchBanActionModal } from "../actions/ban.js";
import { launchMuteActionModal } from "../actions/mute.js";
import { launchNoteActionModal } from "../actions/note.js";
import { launchWarnActionModal } from "../actions/warn.js";
import {
ContextMenuPluginType,
LoadModMenuPageFn,
ModMenuActionOpts,
ModMenuActionType,
ModMenuNavigationType,
} from "../types.js";
export const MODAL_TIMEOUT = 60 * SECONDS;
const MOD_MENU_TIMEOUT = 60 * SECONDS;
const CASES_PER_PAGE = 10;
export const ModMenuCmd = guildPluginUserContextMenuCommand({
name: "Mod Menu",
defaultMemberPermissions: PermissionFlagsBits.ViewAuditLog.toString(),
async run({ pluginData, interaction }) {
await interaction.deferReply({ ephemeral: true });
// Run permission checks for executing user.
const executingMember = await pluginData.guild.members.fetch(interaction.user.id);
const userCfg = await pluginData.config.getMatchingConfig({
channelId: interaction.channelId,
member: executingMember,
});
if (!userCfg.can_use || !userCfg.can_open_mod_menu) {
await interaction.followUp({ content: "Error: Insufficient Permissions" });
return;
}
const user = await resolveUser(pluginData.client, interaction.targetId);
if (!user.id) {
await interaction.followUp("Error: User not found");
return;
}
// Load cases and display mod menu
const cases: Case[] = await pluginData.state.cases.with("notes").getByUserId(user.id);
const userName =
user instanceof UnknownUser && cases.length ? cases[cases.length - 1].user_name : renderUserUsername(user);
const casesPlugin = pluginData.getPlugin(CasesPlugin);
const totalCases = cases.length;
const totalPages: number = Math.max(Math.ceil(totalCases / CASES_PER_PAGE), 1);
const prefix = getGuildPrefix(pluginData);
const infoEmbed = await getUserInfoEmbed(pluginData, user.id, false);
displayModMenu(
pluginData,
interaction,
totalPages,
async (page) => {
const pageCases: Case[] = await pluginData.state.cases
.with("notes")
.getRecentByUserId(user.id, CASES_PER_PAGE, (page - 1) * CASES_PER_PAGE);
const lines = await asyncMap(pageCases, (c) => casesPlugin.getCaseSummary(c, true, interaction.targetId));
const firstCaseNum = (page - 1) * CASES_PER_PAGE + 1;
const lastCaseNum = Math.min(page * CASES_PER_PAGE, totalCases);
const title =
lines.length == 0
? `${userName}`
: `Most recent cases for ${userName} | ${firstCaseNum}-${lastCaseNum} of ${totalCases}`;
const embed = {
author: {
name: title,
icon_url: user instanceof User ? user.displayAvatarURL() : undefined,
},
fields: [
...getChunkedEmbedFields(
emptyEmbedValue,
lines.length == 0 ? `No cases found for **${userName}**` : lines.join("\n"),
),
{
name: emptyEmbedValue,
value: trimLines(
lines.length == 0 ? "" : `Use \`${prefix}case <num>\` to see more information about an individual case`,
),
},
],
footer: { text: `Page ${page}/${totalPages}` },
} satisfies APIEmbed;
return embed;
},
infoEmbed,
executingMember,
);
},
});
async function displayModMenu(
pluginData: GuildPluginData<ContextMenuPluginType>,
interaction: ContextMenuCommandInteraction,
totalPages: number,
loadPage: LoadModMenuPageFn,
infoEmbed: APIEmbed | null,
executingMember: GuildMember,
) {
if (interaction.deferred == false) {
await interaction.deferReply().catch((err) => logger.error(`Mod menu interaction defer failed: ${err}`));
}
const firstButton = new ButtonBuilder()
.setStyle(ButtonStyle.Secondary)
.setEmoji("⏪")
.setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.FIRST }))
.setDisabled(true);
const prevButton = new ButtonBuilder()
.setStyle(ButtonStyle.Secondary)
.setEmoji("⬅")
.setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.PREV }))
.setDisabled(true);
const infoButton = new ButtonBuilder()
.setStyle(ButtonStyle.Primary)
.setLabel("Info")
.setEmoji("")
.setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.INFO }))
.setDisabled(infoEmbed != null ? false : true);
const nextButton = new ButtonBuilder()
.setStyle(ButtonStyle.Secondary)
.setEmoji("➡")
.setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.NEXT }))
.setDisabled(totalPages > 1 ? false : true);
const lastButton = new ButtonBuilder()
.setStyle(ButtonStyle.Secondary)
.setEmoji("⏩")
.setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.LAST }))
.setDisabled(totalPages > 1 ? false : true);
const navigationButtons = [firstButton, prevButton, infoButton, nextButton, lastButton] satisfies ButtonBuilder[];
const modactions = pluginData.getPlugin(ModActionsPlugin);
const moderationButtons = [
new ButtonBuilder()
.setStyle(ButtonStyle.Primary)
.setLabel("Note")
.setEmoji("📝")
.setDisabled(!(await modactions.hasNotePermission(executingMember, interaction.channelId)))
.setCustomId(serializeCustomId({ action: ModMenuActionType.NOTE, target: interaction.targetId })),
new ButtonBuilder()
.setStyle(ButtonStyle.Primary)
.setLabel("Warn")
.setEmoji("⚠️")
.setDisabled(!(await modactions.hasWarnPermission(executingMember, interaction.channelId)))
.setCustomId(serializeCustomId({ action: ModMenuActionType.WARN, target: interaction.targetId })),
new ButtonBuilder()
.setStyle(ButtonStyle.Primary)
.setLabel("Mute")
.setEmoji("🔇")
.setDisabled(!(await modactions.hasMutePermission(executingMember, interaction.channelId)))
.setCustomId(serializeCustomId({ action: ModMenuActionType.MUTE, target: interaction.targetId })),
new ButtonBuilder()
.setStyle(ButtonStyle.Primary)
.setLabel("Ban")
.setEmoji("🚫")
.setDisabled(!(await modactions.hasBanPermission(executingMember, interaction.channelId)))
.setCustomId(serializeCustomId({ action: ModMenuActionType.BAN, target: interaction.targetId })),
] satisfies ButtonBuilder[];
const navigationRow = new ActionRowBuilder<ButtonBuilder>().addComponents(navigationButtons);
const moderationRow = new ActionRowBuilder<ButtonBuilder>().addComponents(moderationButtons);
let page = 1;
await interaction
.editReply({
embeds: [await loadPage(page)],
components: [navigationRow, moderationRow],
})
.then(async (currentPage) => {
const collector = await currentPage.createMessageComponentCollector({
time: MOD_MENU_TIMEOUT,
});
collector.on("collect", async (i) => {
const opts = deserializeCustomId(i.customId);
if (opts.action == ModMenuActionType.PAGE) {
await i.deferUpdate().catch((err) => logger.error(`Mod menu defer failed: ${err}`));
}
// Update displayed embed if any navigation buttons were used
if (opts.action == ModMenuActionType.PAGE && opts.target == ModMenuNavigationType.INFO && infoEmbed != null) {
infoButton
.setLabel("Cases")
.setEmoji("📋")
.setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.CASES }));
firstButton.setDisabled(true);
prevButton.setDisabled(true);
nextButton.setDisabled(true);
lastButton.setDisabled(true);
await i
.editReply({
embeds: [infoEmbed],
components: [navigationRow, moderationRow],
})
.catch((err) => logger.error(`Mod menu info view failed: ${err}`));
} else if (opts.action == ModMenuActionType.PAGE && opts.target == ModMenuNavigationType.CASES) {
infoButton
.setLabel("Info")
.setEmoji("")
.setCustomId(serializeCustomId({ action: ModMenuActionType.PAGE, target: ModMenuNavigationType.INFO }));
updateNavButtonState(firstButton, prevButton, nextButton, lastButton, page, totalPages);
await i
.editReply({
embeds: [await loadPage(page)],
components: [navigationRow, moderationRow],
})
.catch((err) => logger.error(`Mod menu cases view failed: ${err}`));
} else if (opts.action == ModMenuActionType.PAGE) {
let pageDelta = 0;
switch (opts.target) {
case ModMenuNavigationType.PREV:
pageDelta = -1;
break;
case ModMenuNavigationType.NEXT:
pageDelta = 1;
break;
}
let newPage = 1;
if (opts.target == ModMenuNavigationType.PREV || opts.target == ModMenuNavigationType.NEXT) {
newPage = Math.max(Math.min(page + pageDelta, totalPages), 1);
} else if (opts.target == ModMenuNavigationType.FIRST) {
newPage = 1;
} else if (opts.target == ModMenuNavigationType.LAST) {
newPage = totalPages;
}
if (newPage != page) {
updateNavButtonState(firstButton, prevButton, nextButton, lastButton, newPage, totalPages);
await i
.editReply({
embeds: [await loadPage(newPage)],
components: [navigationRow, moderationRow],
})
.catch((err) => logger.error(`Mod menu navigation failed: ${err}`));
page = newPage;
}
} else if (opts.action == ModMenuActionType.NOTE) {
await launchNoteActionModal(pluginData, i as ButtonInteraction, opts.target);
} else if (opts.action == ModMenuActionType.WARN) {
await launchWarnActionModal(pluginData, i as ButtonInteraction, opts.target);
} else if (opts.action == ModMenuActionType.MUTE) {
await launchMuteActionModal(pluginData, i as ButtonInteraction, opts.target);
} else if (opts.action == ModMenuActionType.BAN) {
await launchBanActionModal(pluginData, i as ButtonInteraction, opts.target);
}
collector.resetTimer();
});
// Remove components on timeout.
collector.on("end", async (_, reason) => {
if (reason !== "messageDelete") {
await interaction
.editReply({
components: [],
})
.catch((err) => logger.error(`Mod menu timeout failed: ${err}`));
}
});
})
.catch((err) => logger.error(`Mod menu setup failed: ${err}`));
}
function serializeCustomId(opts: ModMenuActionOpts) {
return `${opts.action}:${opts.target}`;
}
function deserializeCustomId(customId: string): ModMenuActionOpts {
const opts: ModMenuActionOpts = {
action: customId.split(":")[0] as ModMenuActionType,
target: customId.split(":")[1],
};
return opts;
}
function updateNavButtonState(
firstButton: ButtonBuilder,
prevButton: ButtonBuilder,
nextButton: ButtonBuilder,
lastButton: ButtonBuilder,
currentPage: number,
totalPages: number,
) {
if (currentPage > 1) {
firstButton.setDisabled(false);
prevButton.setDisabled(false);
} else {
firstButton.setDisabled(true);
prevButton.setDisabled(true);
}
if (currentPage == totalPages) {
nextButton.setDisabled(true);
lastButton.setDisabled(true);
} else {
nextButton.setDisabled(false);
lastButton.setDisabled(false);
}
}

View file

@ -0,0 +1,11 @@
import { PermissionFlagsBits } from "discord.js";
import { guildPluginUserContextMenuCommand } from "knub";
import { launchMuteActionModal } from "../actions/mute.js";
export const MuteCmd = guildPluginUserContextMenuCommand({
name: "Mute",
defaultMemberPermissions: PermissionFlagsBits.ModerateMembers.toString(),
async run({ pluginData, interaction }) {
await launchMuteActionModal(pluginData, interaction, interaction.targetId);
},
});

View file

@ -0,0 +1,11 @@
import { PermissionFlagsBits } from "discord.js";
import { guildPluginUserContextMenuCommand } from "knub";
import { launchNoteActionModal } from "../actions/note.js";
export const NoteCmd = guildPluginUserContextMenuCommand({
name: "Note",
defaultMemberPermissions: PermissionFlagsBits.ManageMessages.toString(),
async run({ pluginData, interaction }) {
await launchNoteActionModal(pluginData, interaction, interaction.targetId);
},
});

View file

@ -0,0 +1,11 @@
import { PermissionFlagsBits } from "discord.js";
import { guildPluginUserContextMenuCommand } from "knub";
import { launchWarnActionModal } from "../actions/warn.js";
export const WarnCmd = guildPluginUserContextMenuCommand({
name: "Warn",
defaultMemberPermissions: PermissionFlagsBits.ManageMessages.toString(),
async run({ pluginData, interaction }) {
await launchWarnActionModal(pluginData, interaction, interaction.targetId);
},
});

View file

@ -1,12 +0,0 @@
import { contextMenuEvt } from "../types.js";
import { routeContextAction } from "../utils/contextRouter.js";
export const ContextClickedEvt = contextMenuEvt({
event: "interactionCreate",
async listener(meta) {
if (!meta.args.interaction.isContextMenuCommand()) return;
const inter = meta.args.interaction;
await routeContextAction(meta.pluginData, inter);
},
});

View file

@ -1,23 +1,41 @@
import { BasePluginType, guildPluginEventListener } from "knub";
import { APIEmbed, Awaitable } from "discord.js";
import { BasePluginType } from "knub";
import z from "zod";
import { GuildContextMenuLinks } from "../../data/GuildContextMenuLinks.js";
import { GuildCases } from "../../data/GuildCases.js";
export const zContextMenusConfig = z.strictObject({
can_use: z.boolean(),
user_muteindef: z.boolean(),
user_mute1d: z.boolean(),
user_mute1h: z.boolean(),
user_info: z.boolean(),
message_clean10: z.boolean(),
message_clean25: z.boolean(),
message_clean50: z.boolean(),
can_open_mod_menu: z.boolean(),
});
export interface ContextMenuPluginType extends BasePluginType {
config: z.infer<typeof zContextMenusConfig>;
state: {
contextMenuLinks: GuildContextMenuLinks;
cases: GuildCases;
};
}
export const contextMenuEvt = guildPluginEventListener<ContextMenuPluginType>();
export const enum ModMenuActionType {
PAGE = "page",
NOTE = "note",
WARN = "warn",
CLEAN = "clean",
MUTE = "mute",
BAN = "ban",
}
export const enum ModMenuNavigationType {
FIRST = "first",
PREV = "prev",
NEXT = "next",
LAST = "last",
INFO = "info",
CASES = "cases",
}
export interface ModMenuActionOpts {
action: ModMenuActionType;
target: string;
}
export type LoadModMenuPageFn = (page: number) => Awaitable<APIEmbed>;

View file

@ -1,13 +0,0 @@
import { ContextMenuCommandInteraction } from "discord.js";
import { GuildPluginData } from "knub";
import { ContextMenuPluginType } from "../types.js";
import { hardcodedActions } from "./hardcodedContextOptions.js";
export async function routeContextAction(
pluginData: GuildPluginData<ContextMenuPluginType>,
interaction: ContextMenuCommandInteraction,
) {
const contextLink = await pluginData.state.contextMenuLinks.get(interaction.commandId);
if (!contextLink) return;
hardcodedActions[contextLink.action_name](pluginData, interaction);
}

View file

@ -1,23 +0,0 @@
import { cleanAction } from "../actions/clean.js";
import { muteAction } from "../actions/mute.js";
import { userInfoAction } from "../actions/userInfo.js";
export const hardcodedContext: Record<string, string> = {
user_muteindef: "Mute Indefinitely",
user_mute1d: "Mute for 1 day",
user_mute1h: "Mute for 1 hour",
user_info: "Get Info",
message_clean10: "Clean 10 messages",
message_clean25: "Clean 25 messages",
message_clean50: "Clean 50 messages",
};
export const hardcodedActions = {
user_muteindef: (pluginData, interaction) => muteAction(pluginData, undefined, interaction),
user_mute1d: (pluginData, interaction) => muteAction(pluginData, "1d", interaction),
user_mute1h: (pluginData, interaction) => muteAction(pluginData, "1h", interaction),
user_info: (pluginData, interaction) => userInfoAction(pluginData, interaction),
message_clean10: (pluginData, interaction) => cleanAction(pluginData, 10, interaction),
message_clean25: (pluginData, interaction) => cleanAction(pluginData, 25, interaction),
message_clean50: (pluginData, interaction) => cleanAction(pluginData, 50, interaction),
};

View file

@ -1,39 +0,0 @@
import { ApplicationCommandData, ApplicationCommandType } from "discord.js";
import { GuildPluginData } from "knub";
import { LogsPlugin } from "../../../plugins/Logs/LogsPlugin.js";
import { ContextMenuPluginType } from "../types.js";
import { hardcodedContext } from "./hardcodedContextOptions.js";
export async function loadAllCommands(pluginData: GuildPluginData<ContextMenuPluginType>) {
const comms = await pluginData.client.application!.commands;
const cfg = pluginData.config.get();
const newCommands: ApplicationCommandData[] = [];
const addedNames: string[] = [];
for (const [name, label] of Object.entries(hardcodedContext)) {
if (!cfg[name]) continue;
const type = name.startsWith("user") ? ApplicationCommandType.User : ApplicationCommandType.Message;
const data: ApplicationCommandData = {
type,
name: label,
};
addedNames.push(name);
newCommands.push(data);
}
const setCommands = await comms.set(newCommands, pluginData.guild.id).catch((e) => {
pluginData.getPlugin(LogsPlugin).logBotAlert({ body: `Unable to overwrite context menus: ${e}` });
return undefined;
});
if (!setCommands) return;
const setCommandsArray = [...setCommands.values()];
await pluginData.state.contextMenuLinks.deleteAll();
for (let i = 0; i < setCommandsArray.length; i++) {
const command = setCommandsArray[i];
pluginData.state.contextMenuLinks.create(command.id, addedNames[i]);
}
}

View file

@ -4,6 +4,7 @@ import { GuildCounters } from "../../data/GuildCounters.js";
import { CounterTrigger, parseCounterConditionString } from "../../data/entities/CounterTrigger.js";
import { makePublicFn } from "../../pluginUtils.js";
import { MINUTES, convertDelayStringToMS, values } from "../../utils.js";
import { CommonPlugin } from "../Common/CommonPlugin.js";
import { AddCounterCmd } from "./commands/AddCounterCmd.js";
import { CountersListCmd } from "./commands/CountersListCmd.js";
import { ResetAllCounterValuesCmd } from "./commands/ResetAllCounterValuesCmd.js";
@ -127,6 +128,10 @@ export const CountersPlugin = guildPlugin<CountersPluginType>()({
await state.counters.markUnusedTriggersToBeDeleted(activeTriggerIds);
},
beforeStart(pluginData) {
pluginData.state.common = pluginData.getPlugin(CommonPlugin);
},
async afterLoad(pluginData) {
const { state } = pluginData;

View file

@ -2,7 +2,6 @@ import { Snowflake, TextChannel } from "discord.js";
import { guildPluginMessageCommand } from "knub";
import { waitForReply } from "knub/helpers";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { sendErrorMessage } from "../../../pluginUtils.js";
import { UnknownUser, resolveUser } from "../../../utils.js";
import { changeCounterValue } from "../functions/changeCounterValue.js";
import { CountersPluginType } from "../types.js";
@ -45,22 +44,22 @@ export const AddCounterCmd = guildPluginMessageCommand<CountersPluginType>()({
const counter = config.counters[args.counterName];
const counterId = pluginData.state.counterIds[args.counterName];
if (!counter || !counterId) {
sendErrorMessage(pluginData, message.channel, `Unknown counter: ${args.counterName}`);
void pluginData.state.common.sendErrorMessage(message, `Unknown counter: ${args.counterName}`);
return;
}
if (counter.can_edit === false) {
sendErrorMessage(pluginData, message.channel, `Missing permissions to edit this counter's value`);
void pluginData.state.common.sendErrorMessage(message, `Missing permissions to edit this counter's value`);
return;
}
if (args.channel && !counter.per_channel) {
sendErrorMessage(pluginData, message.channel, `This counter is not per-channel`);
void pluginData.state.common.sendErrorMessage(message, `This counter is not per-channel`);
return;
}
if (args.user && !counter.per_user) {
sendErrorMessage(pluginData, message.channel, `This counter is not per-user`);
void pluginData.state.common.sendErrorMessage(message, `This counter is not per-user`);
return;
}
@ -69,13 +68,13 @@ export const AddCounterCmd = guildPluginMessageCommand<CountersPluginType>()({
message.channel.send(`Which channel's counter value would you like to add to?`);
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
if (!reply || !reply.content) {
sendErrorMessage(pluginData, message.channel, "Cancelling");
void pluginData.state.common.sendErrorMessage(message, "Cancelling");
return;
}
const potentialChannel = pluginData.guild.channels.resolve(reply.content as Snowflake);
if (!potentialChannel || !(potentialChannel instanceof TextChannel)) {
sendErrorMessage(pluginData, message.channel, "Channel is not a text channel, cancelling");
void pluginData.state.common.sendErrorMessage(message, "Channel is not a text channel, cancelling");
return;
}
@ -87,13 +86,13 @@ export const AddCounterCmd = guildPluginMessageCommand<CountersPluginType>()({
message.channel.send(`Which user's counter value would you like to add to?`);
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
if (!reply || !reply.content) {
sendErrorMessage(pluginData, message.channel, "Cancelling");
void pluginData.state.common.sendErrorMessage(message, "Cancelling");
return;
}
const potentialUser = await resolveUser(pluginData.client, reply.content);
if (!potentialUser || potentialUser instanceof UnknownUser) {
sendErrorMessage(pluginData, message.channel, "Unknown user, cancelling");
void pluginData.state.common.sendErrorMessage(message, "Unknown user, cancelling");
return;
}
@ -105,13 +104,13 @@ export const AddCounterCmd = guildPluginMessageCommand<CountersPluginType>()({
message.channel.send("How much would you like to add to the counter's value?");
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
if (!reply || !reply.content) {
sendErrorMessage(pluginData, message.channel, "Cancelling");
void pluginData.state.common.sendErrorMessage(message, "Cancelling");
return;
}
const potentialAmount = parseInt(reply.content, 10);
if (!potentialAmount) {
sendErrorMessage(pluginData, message.channel, "Not a number, cancelling");
void pluginData.state.common.sendErrorMessage(message, "Not a number, cancelling");
return;
}

View file

@ -1,5 +1,4 @@
import { guildPluginMessageCommand } from "knub";
import { sendErrorMessage } from "../../../pluginUtils.js";
import { trimMultilineString, ucfirst } from "../../../utils.js";
import { getGuildPrefix } from "../../../utils/getGuildPrefix.js";
import { CountersPluginType } from "../types.js";
@ -15,7 +14,7 @@ export const CountersListCmd = guildPluginMessageCommand<CountersPluginType>()({
const countersToShow = Array.from(Object.values(config.counters)).filter((c) => c.can_view !== false);
if (!countersToShow.length) {
sendErrorMessage(pluginData, message.channel, "No counters are configured for this server");
void pluginData.state.common.sendErrorMessage(message, "No counters are configured for this server");
return;
}

View file

@ -1,6 +1,5 @@
import { guildPluginMessageCommand } from "knub";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { confirm, noop, trimMultilineString } from "../../../utils.js";
import { resetAllCounterValues } from "../functions/resetAllCounterValues.js";
import { CountersPluginType } from "../types.js";
@ -18,17 +17,20 @@ export const ResetAllCounterValuesCmd = guildPluginMessageCommand<CountersPlugin
const counter = config.counters[args.counterName];
const counterId = pluginData.state.counterIds[args.counterName];
if (!counter || !counterId) {
sendErrorMessage(pluginData, message.channel, `Unknown counter: ${args.counterName}`);
void pluginData.state.common.sendErrorMessage(message, `Unknown counter: ${args.counterName}`);
return;
}
if (counter.can_reset_all === false) {
sendErrorMessage(pluginData, message.channel, `Missing permissions to reset all of this counter's values`);
void pluginData.state.common.sendErrorMessage(
message,
`Missing permissions to reset all of this counter's values`,
);
return;
}
const counterName = counter.name || args.counterName;
const confirmed = await confirm(message.channel, message.author.id, {
const confirmed = await confirm(message, message.author.id, {
content: trimMultilineString(`
Do you want to reset **ALL** values for counter **${counterName}**?
This will reset the counter for **all** users and channels.
@ -36,7 +38,7 @@ export const ResetAllCounterValuesCmd = guildPluginMessageCommand<CountersPlugin
`),
});
if (!confirmed) {
sendErrorMessage(pluginData, message.channel, "Cancelled");
void pluginData.state.common.sendErrorMessage(message, "Cancelled");
return;
}
@ -47,7 +49,10 @@ export const ResetAllCounterValuesCmd = guildPluginMessageCommand<CountersPlugin
await resetAllCounterValues(pluginData, args.counterName);
loadingMessage?.delete().catch(noop);
sendSuccessMessage(pluginData, message.channel, `All counter values for **${counterName}** have been reset`);
void pluginData.state.common.sendSuccessMessage(
message,
`All counter values for **${counterName}** have been reset`,
);
pluginData.getKnubInstance().reloadGuild(pluginData.guild.id);
},

View file

@ -2,7 +2,6 @@ import { Snowflake, TextChannel } from "discord.js";
import { guildPluginMessageCommand } from "knub";
import { waitForReply } from "knub/helpers";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { sendErrorMessage } from "../../../pluginUtils.js";
import { UnknownUser, resolveUser } from "../../../utils.js";
import { setCounterValue } from "../functions/setCounterValue.js";
import { CountersPluginType } from "../types.js";
@ -40,22 +39,22 @@ export const ResetCounterCmd = guildPluginMessageCommand<CountersPluginType>()({
const counter = config.counters[args.counterName];
const counterId = pluginData.state.counterIds[args.counterName];
if (!counter || !counterId) {
sendErrorMessage(pluginData, message.channel, `Unknown counter: ${args.counterName}`);
void pluginData.state.common.sendErrorMessage(message, `Unknown counter: ${args.counterName}`);
return;
}
if (counter.can_edit === false) {
sendErrorMessage(pluginData, message.channel, `Missing permissions to reset this counter's value`);
void pluginData.state.common.sendErrorMessage(message, `Missing permissions to reset this counter's value`);
return;
}
if (args.channel && !counter.per_channel) {
sendErrorMessage(pluginData, message.channel, `This counter is not per-channel`);
void pluginData.state.common.sendErrorMessage(message, `This counter is not per-channel`);
return;
}
if (args.user && !counter.per_user) {
sendErrorMessage(pluginData, message.channel, `This counter is not per-user`);
void pluginData.state.common.sendErrorMessage(message, `This counter is not per-user`);
return;
}
@ -64,13 +63,13 @@ export const ResetCounterCmd = guildPluginMessageCommand<CountersPluginType>()({
message.channel.send(`Which channel's counter value would you like to reset?`);
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
if (!reply || !reply.content) {
sendErrorMessage(pluginData, message.channel, "Cancelling");
void pluginData.state.common.sendErrorMessage(message, "Cancelling");
return;
}
const potentialChannel = pluginData.guild.channels.resolve(reply.content as Snowflake);
if (!potentialChannel || !(potentialChannel instanceof TextChannel)) {
sendErrorMessage(pluginData, message.channel, "Channel is not a text channel, cancelling");
void pluginData.state.common.sendErrorMessage(message, "Channel is not a text channel, cancelling");
return;
}
@ -82,13 +81,13 @@ export const ResetCounterCmd = guildPluginMessageCommand<CountersPluginType>()({
message.channel.send(`Which user's counter value would you like to reset?`);
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
if (!reply || !reply.content) {
sendErrorMessage(pluginData, message.channel, "Cancelling");
void pluginData.state.common.sendErrorMessage(message, "Cancelling");
return;
}
const potentialUser = await resolveUser(pluginData.client, reply.content);
if (!potentialUser || potentialUser instanceof UnknownUser) {
sendErrorMessage(pluginData, message.channel, "Unknown user, cancelling");
void pluginData.state.common.sendErrorMessage(message, "Unknown user, cancelling");
return;
}

View file

@ -2,7 +2,6 @@ import { Snowflake, TextChannel } from "discord.js";
import { guildPluginMessageCommand } from "knub";
import { waitForReply } from "knub/helpers";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { sendErrorMessage } from "../../../pluginUtils.js";
import { UnknownUser, resolveUser } from "../../../utils.js";
import { setCounterValue } from "../functions/setCounterValue.js";
import { CountersPluginType } from "../types.js";
@ -45,22 +44,22 @@ export const SetCounterCmd = guildPluginMessageCommand<CountersPluginType>()({
const counter = config.counters[args.counterName];
const counterId = pluginData.state.counterIds[args.counterName];
if (!counter || !counterId) {
sendErrorMessage(pluginData, message.channel, `Unknown counter: ${args.counterName}`);
void pluginData.state.common.sendErrorMessage(message, `Unknown counter: ${args.counterName}`);
return;
}
if (counter.can_edit === false) {
sendErrorMessage(pluginData, message.channel, `Missing permissions to edit this counter's value`);
void pluginData.state.common.sendErrorMessage(message, `Missing permissions to edit this counter's value`);
return;
}
if (args.channel && !counter.per_channel) {
sendErrorMessage(pluginData, message.channel, `This counter is not per-channel`);
void pluginData.state.common.sendErrorMessage(message, `This counter is not per-channel`);
return;
}
if (args.user && !counter.per_user) {
sendErrorMessage(pluginData, message.channel, `This counter is not per-user`);
void pluginData.state.common.sendErrorMessage(message, `This counter is not per-user`);
return;
}
@ -69,13 +68,13 @@ export const SetCounterCmd = guildPluginMessageCommand<CountersPluginType>()({
message.channel.send(`Which channel's counter value would you like to change?`);
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
if (!reply || !reply.content) {
sendErrorMessage(pluginData, message.channel, "Cancelling");
void pluginData.state.common.sendErrorMessage(message, "Cancelling");
return;
}
const potentialChannel = pluginData.guild.channels.resolve(reply.content as Snowflake);
if (!potentialChannel || !(potentialChannel instanceof TextChannel)) {
sendErrorMessage(pluginData, message.channel, "Channel is not a text channel, cancelling");
void pluginData.state.common.sendErrorMessage(message, "Channel is not a text channel, cancelling");
return;
}
@ -87,13 +86,13 @@ export const SetCounterCmd = guildPluginMessageCommand<CountersPluginType>()({
message.channel.send(`Which user's counter value would you like to change?`);
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
if (!reply || !reply.content) {
sendErrorMessage(pluginData, message.channel, "Cancelling");
void pluginData.state.common.sendErrorMessage(message, "Cancelling");
return;
}
const potentialUser = await resolveUser(pluginData.client, reply.content);
if (!potentialUser || potentialUser instanceof UnknownUser) {
sendErrorMessage(pluginData, message.channel, "Unknown user, cancelling");
void pluginData.state.common.sendErrorMessage(message, "Unknown user, cancelling");
return;
}
@ -105,13 +104,13 @@ export const SetCounterCmd = guildPluginMessageCommand<CountersPluginType>()({
message.channel.send("What would you like to set the counter's value to?");
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
if (!reply || !reply.content) {
sendErrorMessage(pluginData, message.channel, "Cancelling");
void pluginData.state.common.sendErrorMessage(message, "Cancelling");
return;
}
const potentialValue = parseInt(reply.content, 10);
if (Number.isNaN(potentialValue)) {
sendErrorMessage(pluginData, message.channel, "Not a number, cancelling");
void pluginData.state.common.sendErrorMessage(message, "Not a number, cancelling");
return;
}
@ -119,7 +118,7 @@ export const SetCounterCmd = guildPluginMessageCommand<CountersPluginType>()({
}
if (value < 0) {
sendErrorMessage(pluginData, message.channel, "Cannot set counter value below 0");
void pluginData.state.common.sendErrorMessage(message, "Cannot set counter value below 0");
return;
}

View file

@ -2,7 +2,6 @@ import { Snowflake } from "discord.js";
import { guildPluginMessageCommand } from "knub";
import { waitForReply } from "knub/helpers";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { sendErrorMessage } from "../../../pluginUtils.js";
import { resolveUser, UnknownUser } from "../../../utils.js";
import { CountersPluginType } from "../types.js";
@ -39,22 +38,22 @@ export const ViewCounterCmd = guildPluginMessageCommand<CountersPluginType>()({
const counter = config.counters[args.counterName];
const counterId = pluginData.state.counterIds[args.counterName];
if (!counter || !counterId) {
sendErrorMessage(pluginData, message.channel, `Unknown counter: ${args.counterName}`);
void pluginData.state.common.sendErrorMessage(message, `Unknown counter: ${args.counterName}`);
return;
}
if (counter.can_view === false) {
sendErrorMessage(pluginData, message.channel, `Missing permissions to view this counter's value`);
void pluginData.state.common.sendErrorMessage(message, `Missing permissions to view this counter's value`);
return;
}
if (args.channel && !counter.per_channel) {
sendErrorMessage(pluginData, message.channel, `This counter is not per-channel`);
void pluginData.state.common.sendErrorMessage(message, `This counter is not per-channel`);
return;
}
if (args.user && !counter.per_user) {
sendErrorMessage(pluginData, message.channel, `This counter is not per-user`);
void pluginData.state.common.sendErrorMessage(message, `This counter is not per-user`);
return;
}
@ -63,13 +62,13 @@ export const ViewCounterCmd = guildPluginMessageCommand<CountersPluginType>()({
message.channel.send(`Which channel's counter value would you like to view?`);
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
if (!reply || !reply.content) {
sendErrorMessage(pluginData, message.channel, "Cancelling");
void pluginData.state.common.sendErrorMessage(message, "Cancelling");
return;
}
const potentialChannel = pluginData.guild.channels.resolve(reply.content as Snowflake);
if (!potentialChannel?.isTextBased()) {
sendErrorMessage(pluginData, message.channel, "Channel is not a text channel, cancelling");
void pluginData.state.common.sendErrorMessage(message, "Channel is not a text channel, cancelling");
return;
}
@ -81,13 +80,13 @@ export const ViewCounterCmd = guildPluginMessageCommand<CountersPluginType>()({
message.channel.send(`Which user's counter value would you like to view?`);
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
if (!reply || !reply.content) {
sendErrorMessage(pluginData, message.channel, "Cancelling");
void pluginData.state.common.sendErrorMessage(message, "Cancelling");
return;
}
const potentialUser = await resolveUser(pluginData.client, reply.content);
if (!potentialUser || potentialUser instanceof UnknownUser) {
sendErrorMessage(pluginData, message.channel, "Unknown user, cancelling");
void pluginData.state.common.sendErrorMessage(message, "Unknown user, cancelling");
return;
}

View file

@ -1,5 +1,5 @@
import { EventEmitter } from "events";
import { BasePluginType } from "knub";
import { BasePluginType, pluginUtils } from "knub";
import z from "zod";
import { GuildCounters, MAX_COUNTER_VALUE, MIN_COUNTER_VALUE } from "../../data/GuildCounters.js";
import {
@ -9,6 +9,7 @@ import {
parseCounterConditionString,
} from "../../data/entities/CounterTrigger.js";
import { zBoundedCharacters, zBoundedRecord, zDelayString } from "../../utils.js";
import { CommonPlugin } from "../Common/CommonPlugin.js";
import Timeout = NodeJS.Timeout;
const MAX_COUNTERS = 5;
@ -132,5 +133,6 @@ export interface CountersPluginType extends BasePluginType {
decayTimers: Timeout[];
events: CounterEventEmitter;
counterTriggersByCounterId: Map<number, CounterTrigger[]>;
common: pluginUtils.PluginPublicInterface<typeof CommonPlugin>;
};
}

View file

@ -11,6 +11,7 @@ import {
messageToTemplateSafeMessage,
userToTemplateSafeUser,
} from "../../utils/templateSafeObjects.js";
import { CommonPlugin } from "../Common/CommonPlugin.js";
import { LogsPlugin } from "../Logs/LogsPlugin.js";
import { runEvent } from "./functions/runEvent.js";
import { CustomEventsPluginType, zCustomEventsConfig } from "./types.js";
@ -28,6 +29,10 @@ export const CustomEventsPlugin = guildPlugin<CustomEventsPluginType>()({
configParser: (input) => zCustomEventsConfig.parse(input),
defaultOptions,
beforeStart(pluginData) {
pluginData.state.common = pluginData.getPlugin(CommonPlugin);
},
afterLoad(pluginData) {
const config = pluginData.config.get();
for (const [key, event] of Object.entries(config.events)) {

View file

@ -1,6 +1,5 @@
import { Message } from "discord.js";
import { GuildPluginData } from "knub";
import { sendErrorMessage } from "../../../pluginUtils.js";
import { TemplateSafeValueContainer } from "../../../templateFormatter.js";
import { ActionError } from "../ActionError.js";
import { addRoleAction } from "../actions/addRoleAction.js";
@ -39,7 +38,7 @@ export async function runEvent(
} catch (e) {
if (e instanceof ActionError) {
if (event.trigger.type === "command") {
sendErrorMessage(pluginData, (eventData.msg as Message).channel, e.message);
void pluginData.state.common.sendErrorMessage((eventData.msg as Message).channel, e.message);
} else {
// TODO: Where to log action errors from other kinds of triggers?
}

View file

@ -1,6 +1,7 @@
import { BasePluginType } from "knub";
import { BasePluginType, pluginUtils } from "knub";
import z from "zod";
import { zBoundedCharacters, zBoundedRecord } from "../../utils.js";
import { CommonPlugin } from "../Common/CommonPlugin.js";
import { zAddRoleAction } from "./actions/addRoleAction.js";
import { zCreateCaseAction } from "./actions/createCaseAction.js";
import { zMakeRoleMentionableAction } from "./actions/makeRoleMentionableAction.js";
@ -43,5 +44,6 @@ export interface CustomEventsPluginType extends BasePluginType {
config: z.infer<typeof zCustomEventsConfig>;
state: {
clearTriggers: () => void;
common: pluginUtils.PluginPublicInterface<typeof CommonPlugin>;
};
}

View file

@ -1,6 +1,7 @@
import { PluginOptions, guildPlugin } from "knub";
import { onGuildEvent } from "../../data/GuildEvents.js";
import { GuildVCAlerts } from "../../data/GuildVCAlerts.js";
import { CommonPlugin } from "../Common/CommonPlugin.js";
import { FollowCmd } from "./commands/FollowCmd.js";
import { DeleteFollowCmd, ListFollowCmd } from "./commands/ListFollowCmd.js";
import { WhereCmd } from "./commands/WhereCmd.js";
@ -53,6 +54,10 @@ export const LocateUserPlugin = guildPlugin<LocateUserPluginType>()({
state.usersWithAlerts = [];
},
beforeStart(pluginData) {
pluginData.state.common = pluginData.getPlugin(CommonPlugin);
},
afterLoad(pluginData) {
const { state, guild } = pluginData;

View file

@ -2,7 +2,6 @@ import humanizeDuration from "humanize-duration";
import moment from "moment-timezone";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { registerExpiringVCAlert } from "../../../data/loops/expiringVCAlertsLoop.js";
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { MINUTES, SECONDS } from "../../../utils.js";
import { locateUserCmd } from "../types.js";
@ -27,7 +26,7 @@ export const FollowCmd = locateUserCmd({
const active = args.active || false;
if (time < 30 * SECONDS) {
sendErrorMessage(pluginData, msg.channel, "Sorry, but the minimum duration for an alert is 30 seconds!");
void pluginData.state.common.sendErrorMessage(msg, "Sorry, but the minimum duration for an alert is 30 seconds!");
return;
}
@ -46,17 +45,15 @@ export const FollowCmd = locateUserCmd({
}
if (active) {
sendSuccessMessage(
pluginData,
msg.channel,
void pluginData.state.common.sendSuccessMessage(
msg,
`Every time <@${args.member.id}> joins or switches VC in the next ${humanizeDuration(
time,
)} i will notify and move you.\nPlease make sure to be in a voice channel, otherwise i cannot move you!`,
);
} else {
sendSuccessMessage(
pluginData,
msg.channel,
void pluginData.state.common.sendSuccessMessage(
msg,
`Every time <@${args.member.id}> joins or switches VC in the next ${humanizeDuration(time)} i will notify you`,
);
}

View file

@ -1,6 +1,5 @@
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { clearExpiringVCAlert } from "../../../data/loops/expiringVCAlertsLoop.js";
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { createChunkedMessage, sorter } from "../../../utils.js";
import { locateUserCmd } from "../types.js";
@ -13,7 +12,7 @@ export const ListFollowCmd = locateUserCmd({
async run({ message: msg, pluginData }) {
const alerts = await pluginData.state.alerts.getAlertsByRequestorId(msg.member.id);
if (alerts.length === 0) {
sendErrorMessage(pluginData, msg.channel, "You have no active alerts!");
void pluginData.state.common.sendErrorMessage(msg, "You have no active alerts!");
return;
}
@ -46,7 +45,7 @@ export const DeleteFollowCmd = locateUserCmd({
alerts.sort(sorter("expires_at"));
if (args.num > alerts.length || args.num <= 0) {
sendErrorMessage(pluginData, msg.channel, "Unknown alert!");
void pluginData.state.common.sendErrorMessage(msg, "Unknown alert!");
return;
}
@ -54,6 +53,6 @@ export const DeleteFollowCmd = locateUserCmd({
clearExpiringVCAlert(toDelete);
await pluginData.state.alerts.delete(toDelete.id);
sendSuccessMessage(pluginData, msg.channel, "Alert deleted");
void pluginData.state.common.sendSuccessMessage(msg, "Alert deleted");
},
});

View file

@ -1,6 +1,7 @@
import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub";
import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand, pluginUtils } from "knub";
import z from "zod";
import { GuildVCAlerts } from "../../data/GuildVCAlerts.js";
import { CommonPlugin } from "../Common/CommonPlugin.js";
export const zLocateUserConfig = z.strictObject({
can_where: z.boolean(),
@ -13,6 +14,7 @@ export interface LocateUserPluginType extends BasePluginType {
alerts: GuildVCAlerts;
usersWithAlerts: string[];
unregisterGuildEventListener: () => void;
common: pluginUtils.PluginPublicInterface<typeof CommonPlugin>;
};
}

View file

@ -1,6 +1,5 @@
import { GuildMember, GuildTextBasedChannel, Snowflake } from "discord.js";
import { GuildPluginData } from "knub";
import { sendErrorMessage } from "../../../pluginUtils.js";
import { LocateUserPluginType } from "../types.js";
export async function moveMember(
@ -16,10 +15,10 @@ export async function moveMember(
channel: target.voice.channelId,
});
} catch {
sendErrorMessage(pluginData, errorChannel, "Failed to move you. Are you in a voice channel?");
void pluginData.state.common.sendErrorMessage(errorChannel, "Failed to move you. Are you in a voice channel?");
return;
}
} else {
sendErrorMessage(pluginData, errorChannel, "Failed to move you. Are you in a voice channel?");
void pluginData.state.common.sendErrorMessage(errorChannel, "Failed to move you. Are you in a voice channel?");
}
}

View file

@ -1,7 +1,6 @@
import { GuildMember, GuildTextBasedChannel, Invite, VoiceChannel } from "discord.js";
import { GuildPluginData } from "knub";
import { getInviteLink } from "knub/helpers";
import { sendErrorMessage } from "../../../pluginUtils.js";
import { LocateUserPluginType } from "../types.js";
import { createOrReuseInvite } from "./createOrReuseInvite.js";
@ -22,7 +21,7 @@ export async function sendWhere(
try {
invite = await createOrReuseInvite(voice);
} catch {
sendErrorMessage(pluginData, channel, "Cannot create an invite to that channel!");
void pluginData.state.common.sendErrorMessage(channel, "Cannot create an invite to that channel!");
return;
}
channel.send({

View file

@ -30,7 +30,7 @@ import {
import { LogsThreadCreateEvt, LogsThreadDeleteEvt, LogsThreadUpdateEvt } from "./events/LogsThreadModifyEvts.js";
import { LogsGuildMemberUpdateEvt } from "./events/LogsUserUpdateEvts.js";
import { LogsVoiceStateUpdateEvt } from "./events/LogsVoiceChannelEvts.js";
import { LogsPluginType, zLogsConfig } from "./types.js";
import { FORMAT_NO_TIMESTAMP, LogsPluginType, zLogsConfig } from "./types.js";
import { getLogMessage } from "./util/getLogMessage.js";
import { log } from "./util/log.js";
import { onMessageDelete } from "./util/onMessageDelete.js";

View file

@ -12,7 +12,7 @@ import {
userToTemplateSafeUser,
} from "../../../utils/templateSafeObjects.js";
import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin.js";
import { LogsPluginType } from "../types.js";
import { FORMAT_NO_TIMESTAMP, LogsPluginType } from "../types.js";
import { log } from "../util/log.js";
export interface LogMessageDeleteData {

View file

@ -6,7 +6,7 @@ import { GuildCases } from "../../data/GuildCases.js";
import { GuildLogs } from "../../data/GuildLogs.js";
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
import { LogType } from "../../data/LogType.js";
import { zBoundedCharacters, zMessageContent, zRegex, zSnowflake } from "../../utils.js";
import { keys, zBoundedCharacters, zMessageContent, zRegex, zSnowflake } from "../../utils.js";
import { MessageBuffer } from "../../utils/MessageBuffer.js";
import {
TemplateSafeCase,

View file

@ -25,7 +25,7 @@ import {
TemplateSafeUser,
} from "../../../utils/templateSafeObjects.js";
import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin.js";
import { ILogTypeData, LogsPluginType, TLogChannel } from "../types.js";
import { FORMAT_NO_TIMESTAMP, ILogTypeData, LogsPluginType, TLogChannel } from "../types.js";
export async function getLogMessage<TLogType extends keyof ILogTypeData>(
pluginData: GuildPluginData<LogsPluginType>,

View file

@ -1,6 +1,6 @@
import { EmbedData, GuildTextBasedChannel, Snowflake } from "discord.js";
import { GuildPluginData } from "knub";
import cloneDeep from "lodash/cloneDeep.js";
import cloneDeep from "lodash.clonedeep";
import { SavedMessage } from "../../../data/entities/SavedMessage.js";
import { resolveUser } from "../../../utils.js";
import { logMessageEdit } from "../logFunctions/logMessageEdit.js";

View file

@ -1,13 +1,9 @@
import { PluginOptions, guildPlugin } from "knub";
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
import { CommonPlugin } from "../Common/CommonPlugin.js";
import { SaveMessagesToDBCmd } from "./commands/SaveMessagesToDB.js";
import { SavePinsToDBCmd } from "./commands/SavePinsToDB.js";
import {
MessageCreateEvt,
MessageDeleteBulkEvt,
MessageDeleteEvt,
MessageUpdateEvt,
} from "./events/SaveMessagesEvts.js";
import { MessageCreateEvt, MessageDeleteBulkEvt, MessageDeleteEvt, MessageUpdateEvt } from "./events/SaveMessagesEvts.js";
import { MessageSaverPluginType, zMessageSaverConfig } from "./types.js";
const defaultOptions: PluginOptions<MessageSaverPluginType> = {
@ -48,4 +44,8 @@ export const MessageSaverPlugin = guildPlugin<MessageSaverPluginType>()({
const { state, guild } = pluginData;
state.savedMessages = GuildSavedMessages.getGuildInstance(guild.id);
},
beforeStart(pluginData) {
pluginData.state.common = pluginData.getPlugin(CommonPlugin);
},
});

View file

@ -1,5 +1,4 @@
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { sendSuccessMessage } from "../../../pluginUtils.js";
import { saveMessagesToDB } from "../saveMessagesToDB.js";
import { messageSaverCmd } from "../types.js";
@ -18,13 +17,12 @@ export const SaveMessagesToDBCmd = messageSaverCmd({
const { savedCount, failed } = await saveMessagesToDB(pluginData, args.channel, args.ids.trim().split(" "));
if (failed.length) {
sendSuccessMessage(
pluginData,
msg.channel,
void pluginData.state.common.sendSuccessMessage(
msg,
`Saved ${savedCount} messages. The following messages could not be saved: ${failed.join(", ")}`,
);
} else {
sendSuccessMessage(pluginData, msg.channel, `Saved ${savedCount} messages!`);
void pluginData.state.common.sendSuccessMessage(msg, `Saved ${savedCount} messages!`);
}
},
});

View file

@ -1,5 +1,4 @@
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { sendSuccessMessage } from "../../../pluginUtils.js";
import { saveMessagesToDB } from "../saveMessagesToDB.js";
import { messageSaverCmd } from "../types.js";
@ -19,13 +18,12 @@ export const SavePinsToDBCmd = messageSaverCmd({
const { savedCount, failed } = await saveMessagesToDB(pluginData, args.channel, [...pins.keys()]);
if (failed.length) {
sendSuccessMessage(
pluginData,
msg.channel,
void pluginData.state.common.sendSuccessMessage(
msg,
`Saved ${savedCount} messages. The following messages could not be saved: ${failed.join(", ")}`,
);
} else {
sendSuccessMessage(pluginData, msg.channel, `Saved ${savedCount} messages!`);
void pluginData.state.common.sendSuccessMessage(msg, `Saved ${savedCount} messages!`);
}
},
});

View file

@ -1,6 +1,7 @@
import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand } from "knub";
import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand, pluginUtils } from "knub";
import z from "zod";
import { GuildSavedMessages } from "../../data/GuildSavedMessages.js";
import { CommonPlugin } from "../Common/CommonPlugin.js";
export const zMessageSaverConfig = z.strictObject({
can_manage: z.boolean(),
@ -10,6 +11,7 @@ export interface MessageSaverPluginType extends BasePluginType {
config: z.infer<typeof zMessageSaverConfig>;
state: {
savedMessages: GuildSavedMessages;
common: pluginUtils.PluginPublicInterface<typeof CommonPlugin>;
};
}

View file

@ -10,44 +10,69 @@ import { GuildTempbans } from "../../data/GuildTempbans.js";
import { makePublicFn, mapToPublicFn } from "../../pluginUtils.js";
import { MINUTES } from "../../utils.js";
import { CasesPlugin } from "../Cases/CasesPlugin.js";
import { CommonPlugin } from "../Common/CommonPlugin.js";
import { LogsPlugin } from "../Logs/LogsPlugin.js";
import { MutesPlugin } from "../Mutes/MutesPlugin.js";
import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin.js";
import { AddCaseCmd } from "./commands/AddCaseCmd.js";
import { BanCmd } from "./commands/BanCmd.js";
import { CaseCmd } from "./commands/CaseCmd.js";
import { CasesModCmd } from "./commands/CasesModCmd.js";
import { CasesUserCmd } from "./commands/CasesUserCmd.js";
import { DeleteCaseCmd } from "./commands/DeleteCaseCmd.js";
import { ForcebanCmd } from "./commands/ForcebanCmd.js";
import { ForcemuteCmd } from "./commands/ForcemuteCmd.js";
import { ForceUnmuteCmd } from "./commands/ForceunmuteCmd.js";
import { HideCaseCmd } from "./commands/HideCaseCmd.js";
import { KickCmd } from "./commands/KickCmd.js";
import { MassbanCmd } from "./commands/MassBanCmd.js";
import { MassunbanCmd } from "./commands/MassUnbanCmd.js";
import { MassmuteCmd } from "./commands/MassmuteCmd.js";
import { MuteCmd } from "./commands/MuteCmd.js";
import { NoteCmd } from "./commands/NoteCmd.js";
import { SoftbanCmd } from "./commands/SoftbanCommand.js";
import { UnbanCmd } from "./commands/UnbanCmd.js";
import { UnhideCaseCmd } from "./commands/UnhideCaseCmd.js";
import { UnmuteCmd } from "./commands/UnmuteCmd.js";
import { UpdateCmd } from "./commands/UpdateCmd.js";
import { WarnCmd } from "./commands/WarnCmd.js";
import { AddCaseMsgCmd } from "./commands/addcase/AddCaseMsgCmd.js";
import { AddCaseSlashCmd } from "./commands/addcase/AddCaseSlashCmd.js";
import { BanMsgCmd } from "./commands/ban/BanMsgCmd.js";
import { BanSlashCmd } from "./commands/ban/BanSlashCmd.js";
import { CaseMsgCmd } from "./commands/case/CaseMsgCmd.js";
import { CaseSlashCmd } from "./commands/case/CaseSlashCmd.js";
import { CasesModMsgCmd } from "./commands/cases/CasesModMsgCmd.js";
import { CasesSlashCmd } from "./commands/cases/CasesSlashCmd.js";
import { CasesUserMsgCmd } from "./commands/cases/CasesUserMsgCmd.js";
import { DeleteCaseMsgCmd } from "./commands/deletecase/DeleteCaseMsgCmd.js";
import { DeleteCaseSlashCmd } from "./commands/deletecase/DeleteCaseSlashCmd.js";
import { ForceBanMsgCmd } from "./commands/forceban/ForceBanMsgCmd.js";
import { ForceBanSlashCmd } from "./commands/forceban/ForceBanSlashCmd.js";
import { ForceMuteMsgCmd } from "./commands/forcemute/ForceMuteMsgCmd.js";
import { ForceMuteSlashCmd } from "./commands/forcemute/ForceMuteSlashCmd.js";
import { ForceUnmuteMsgCmd } from "./commands/forceunmute/ForceUnmuteMsgCmd.js";
import { ForceUnmuteSlashCmd } from "./commands/forceunmute/ForceUnmuteSlashCmd.js";
import { HideCaseMsgCmd } from "./commands/hidecase/HideCaseMsgCmd.js";
import { HideCaseSlashCmd } from "./commands/hidecase/HideCaseSlashCmd.js";
import { KickMsgCmd } from "./commands/kick/KickMsgCmd.js";
import { KickSlashCmd } from "./commands/kick/KickSlashCmd.js";
import { MassBanMsgCmd } from "./commands/massban/MassBanMsgCmd.js";
import { MassBanSlashCmd } from "./commands/massban/MassBanSlashCmd.js";
import { MassMuteMsgCmd } from "./commands/massmute/MassMuteMsgCmd.js";
import { MassMuteSlashSlashCmd } from "./commands/massmute/MassMuteSlashCmd.js";
import { MassUnbanMsgCmd } from "./commands/massunban/MassUnbanMsgCmd.js";
import { MassUnbanSlashCmd } from "./commands/massunban/MassUnbanSlashCmd.js";
import { MuteMsgCmd } from "./commands/mute/MuteMsgCmd.js";
import { MuteSlashCmd } from "./commands/mute/MuteSlashCmd.js";
import { NoteMsgCmd } from "./commands/note/NoteMsgCmd.js";
import { NoteSlashCmd } from "./commands/note/NoteSlashCmd.js";
import { UnbanMsgCmd } from "./commands/unban/UnbanMsgCmd.js";
import { UnbanSlashCmd } from "./commands/unban/UnbanSlashCmd.js";
import { UnhideCaseMsgCmd } from "./commands/unhidecase/UnhideCaseMsgCmd.js";
import { UnhideCaseSlashCmd } from "./commands/unhidecase/UnhideCaseSlashCmd.js";
import { UnmuteMsgCmd } from "./commands/unmute/UnmuteMsgCmd.js";
import { UnmuteSlashCmd } from "./commands/unmute/UnmuteSlashCmd.js";
import { UpdateMsgCmd } from "./commands/update/UpdateMsgCmd.js";
import { UpdateSlashCmd } from "./commands/update/UpdateSlashCmd.js";
import { WarnMsgCmd } from "./commands/warn/WarnMsgCmd.js";
import { WarnSlashCmd } from "./commands/warn/WarnSlashCmd.js";
import { AuditLogEvents } from "./events/AuditLogEvents.js";
import { CreateBanCaseOnManualBanEvt } from "./events/CreateBanCaseOnManualBanEvt.js";
import { CreateUnbanCaseOnManualUnbanEvt } from "./events/CreateUnbanCaseOnManualUnbanEvt.js";
import { PostAlertOnMemberJoinEvt } from "./events/PostAlertOnMemberJoinEvt.js";
import { banUserId } from "./functions/banUserId.js";
import { clearTempban } from "./functions/clearTempban.js";
import { hasMutePermission } from "./functions/hasMutePerm.js";
import {
hasBanPermission,
hasMutePermission,
hasNotePermission,
hasWarnPermission,
} from "./functions/hasModActionPerm.js";
import { kickMember } from "./functions/kickMember.js";
import { offModActionsEvent } from "./functions/offModActionsEvent.js";
import { onModActionsEvent } from "./functions/onModActionsEvent.js";
import { updateCase } from "./functions/updateCase.js";
import { warnMember } from "./functions/warnMember.js";
import { ModActionsPluginType, zModActionsConfig } from "./types.js";
import { AttachmentLinkReactionType, ModActionsPluginType, modActionsSlashGroup, zModActionsConfig } from "./types.js";
const defaultOptions = {
config: {
@ -69,6 +94,7 @@ const defaultOptions = {
warn_notify_message:
"The user already has **{priorWarnings}** warnings!\n Please check their prior cases and assess whether or not to warn anyways.\n Proceed with the warning?",
ban_delete_message_days: 1,
attachment_link_reaction: "warn" as AttachmentLinkReactionType,
can_note: false,
can_warn: false,
@ -122,29 +148,58 @@ export const ModActionsPlugin = guildPlugin<ModActionsPluginType>()({
events: [CreateBanCaseOnManualBanEvt, CreateUnbanCaseOnManualUnbanEvt, PostAlertOnMemberJoinEvt, AuditLogEvents],
slashCommands: [
modActionsSlashGroup({
name: "mod",
description: "Moderation actions",
defaultMemberPermissions: "0",
subcommands: [
AddCaseSlashCmd,
BanSlashCmd,
CaseSlashCmd,
CasesSlashCmd,
DeleteCaseSlashCmd,
ForceBanSlashCmd,
ForceMuteSlashCmd,
ForceUnmuteSlashCmd,
HideCaseSlashCmd,
KickSlashCmd,
MassBanSlashCmd,
MassMuteSlashSlashCmd,
MassUnbanSlashCmd,
MuteSlashCmd,
NoteSlashCmd,
UnbanSlashCmd,
UnhideCaseSlashCmd,
UnmuteSlashCmd,
UpdateSlashCmd,
WarnSlashCmd,
],
}),
],
messageCommands: [
UpdateCmd,
NoteCmd,
WarnCmd,
MuteCmd,
ForcemuteCmd,
UnmuteCmd,
ForceUnmuteCmd,
KickCmd,
SoftbanCmd,
BanCmd,
UnbanCmd,
ForcebanCmd,
MassbanCmd,
MassmuteCmd,
MassunbanCmd,
AddCaseCmd,
CaseCmd,
CasesUserCmd,
CasesModCmd,
HideCaseCmd,
UnhideCaseCmd,
DeleteCaseCmd,
UpdateMsgCmd,
NoteMsgCmd,
WarnMsgCmd,
MuteMsgCmd,
ForceMuteMsgCmd,
UnmuteMsgCmd,
ForceUnmuteMsgCmd,
KickMsgCmd,
BanMsgCmd,
UnbanMsgCmd,
ForceBanMsgCmd,
MassBanMsgCmd,
MassMuteMsgCmd,
MassUnbanMsgCmd,
AddCaseMsgCmd,
CaseMsgCmd,
CasesUserMsgCmd,
CasesModMsgCmd,
HideCaseMsgCmd,
UnhideCaseMsgCmd,
DeleteCaseMsgCmd,
],
public(pluginData) {
@ -153,8 +208,11 @@ export const ModActionsPlugin = guildPlugin<ModActionsPluginType>()({
kickMember: makePublicFn(pluginData, kickMember),
banUserId: makePublicFn(pluginData, banUserId),
updateCase: (msg: Message, caseNumber: number | null, note: string) =>
updateCase(pluginData, msg, { caseNumber, note }),
updateCase(pluginData, msg, msg.author, caseNumber ?? undefined, note, [...msg.attachments.values()]),
hasNotePermission: makePublicFn(pluginData, hasNotePermission),
hasWarnPermission: makePublicFn(pluginData, hasWarnPermission),
hasMutePermission: makePublicFn(pluginData, hasMutePermission),
hasBanPermission: makePublicFn(pluginData, hasBanPermission),
on: mapToPublicFn(onModActionsEvent),
off: mapToPublicFn(offModActionsEvent),
getEventEmitter: () => pluginData.state.events,
@ -178,6 +236,10 @@ export const ModActionsPlugin = guildPlugin<ModActionsPluginType>()({
state.events = new EventEmitter();
},
beforeStart(pluginData) {
pluginData.state.common = pluginData.getPlugin(CommonPlugin);
},
afterLoad(pluginData) {
const { state, guild } = pluginData;

View file

@ -1,93 +0,0 @@
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { CaseTypes } from "../../../data/CaseTypes.js";
import { Case } from "../../../data/entities/Case.js";
import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin.js";
import { canActOn, hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { renderUsername, resolveMember, resolveUser } from "../../../utils.js";
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments.js";
import { modActionsCmd } from "../types.js";
const opts = {
mod: ct.member({ option: true }),
};
export const AddCaseCmd = modActionsCmd({
trigger: "addcase",
permission: "can_addcase",
description: "Add an arbitrary case to the specified user without taking any action",
signature: [
{
type: ct.string(),
user: ct.string(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
],
async run({ pluginData, message: msg, args }) {
const user = await resolveUser(pluginData.client, args.user);
if (!user.id) {
sendErrorMessage(pluginData, msg.channel, `User not found`);
return;
}
// If the user exists as a guild member, make sure we can act on them first
const member = await resolveMember(pluginData.client, pluginData.guild, user.id);
if (member && !canActOn(pluginData, msg.member, member)) {
sendErrorMessage(pluginData, msg.channel, "Cannot add case on this user: insufficient permissions");
return;
}
// The moderator who did the action is the message author or, if used, the specified -mod
let mod = msg.member;
if (args.mod) {
if (!(await hasPermission(pluginData, "can_act_as_other", { message: msg }))) {
sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod");
return;
}
mod = args.mod;
}
// Verify the case type is valid
const type: string = args.type[0].toUpperCase() + args.type.slice(1).toLowerCase();
if (!CaseTypes[type]) {
sendErrorMessage(pluginData, msg.channel, "Cannot add case: invalid case type");
return;
}
const reason = formatReasonWithAttachments(args.reason, [...msg.attachments.values()]);
// Create the case
const casesPlugin = pluginData.getPlugin(CasesPlugin);
const theCase: Case = await casesPlugin.createCase({
userId: user.id,
modId: mod.id,
type: CaseTypes[type],
reason,
ppId: mod.id !== msg.author.id ? msg.author.id : undefined,
});
if (user) {
sendSuccessMessage(
pluginData,
msg.channel,
`Case #${theCase.case_number} created for **${renderUsername(user)}**`,
);
} else {
sendSuccessMessage(pluginData, msg.channel, `Case #${theCase.case_number} created`);
}
// Log the action
pluginData.getPlugin(LogsPlugin).logCaseCreate({
mod: mod.user,
userId: user.id,
caseNum: theCase.case_number,
caseType: type.toUpperCase(),
reason,
});
},
});

View file

@ -1,219 +0,0 @@
import humanizeDuration from "humanize-duration";
import { getMemberLevel } from "knub/helpers";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { CaseTypes } from "../../../data/CaseTypes.js";
import { clearExpiringTempban, registerExpiringTempban } from "../../../data/loops/expiringTempbansLoop.js";
import { canActOn, hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin.js";
import { renderUsername, resolveMember, resolveUser } from "../../../utils.js";
import { banLock } from "../../../utils/lockNameHelpers.js";
import { waitForButtonConfirm } from "../../../utils/waitForInteraction.js";
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
import { banUserId } from "../functions/banUserId.js";
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments.js";
import { isBanned } from "../functions/isBanned.js";
import { readContactMethodsFromArgs } from "../functions/readContactMethodsFromArgs.js";
import { modActionsCmd } from "../types.js";
const opts = {
mod: ct.member({ option: true }),
notify: ct.string({ option: true }),
"notify-channel": ct.textChannel({ option: true }),
"delete-days": ct.number({ option: true, shortcut: "d" }),
};
export const BanCmd = modActionsCmd({
trigger: "ban",
permission: "can_ban",
description: "Ban or Tempban the specified member",
signature: [
{
user: ct.string(),
time: ct.delay(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
{
user: ct.string(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
],
async run({ pluginData, message: msg, args }) {
const user = await resolveUser(pluginData.client, args.user);
if (!user.id) {
sendErrorMessage(pluginData, msg.channel, `User not found`);
return;
}
const time = args["time"] ? args["time"] : null;
const reason = formatReasonWithAttachments(args.reason, [...msg.attachments.values()]);
const memberToBan = await resolveMember(pluginData.client, pluginData.guild, user.id);
// The moderator who did the action is the message author or, if used, the specified -mod
let mod = msg.member;
if (args.mod) {
if (!(await hasPermission(pluginData, "can_act_as_other", { message: msg, channelId: msg.channel.id }))) {
sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod");
return;
}
mod = args.mod;
}
// acquire a lock because of the needed user-inputs below (if banned/not on server)
const lock = await pluginData.locks.acquire(banLock(user));
let forceban = false;
const existingTempban = await pluginData.state.tempbans.findExistingTempbanForUserId(user.id);
if (!memberToBan) {
const banned = await isBanned(pluginData, user.id);
if (banned) {
// Abort if trying to ban user indefinitely if they are already banned indefinitely
if (!existingTempban && !time) {
sendErrorMessage(pluginData, msg.channel, `User is already banned indefinitely.`);
return;
}
// Ask the mod if we should update the existing ban
const reply = await waitForButtonConfirm(
msg.channel,
{ content: "Failed to message the user. Log the warning anyway?" },
{ confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id },
);
if (!reply) {
sendErrorMessage(pluginData, msg.channel, "User already banned, update cancelled by moderator");
lock.unlock();
return;
} else {
// Update or add new tempban / remove old tempban
if (time && time > 0) {
if (existingTempban) {
await pluginData.state.tempbans.updateExpiryTime(user.id, time, mod.id);
} else {
await pluginData.state.tempbans.addTempban(user.id, time, mod.id);
}
const tempban = (await pluginData.state.tempbans.findExistingTempbanForUserId(user.id))!;
registerExpiringTempban(tempban);
} else if (existingTempban) {
clearExpiringTempban(existingTempban);
pluginData.state.tempbans.clear(user.id);
}
// Create a new case for the updated ban since we never stored the old case id and log the action
const casesPlugin = pluginData.getPlugin(CasesPlugin);
const createdCase = await casesPlugin.createCase({
modId: mod.id,
type: CaseTypes.Ban,
userId: user.id,
reason,
noteDetails: [`Ban updated to ${time ? humanizeDuration(time) : "indefinite"}`],
});
if (time) {
pluginData.getPlugin(LogsPlugin).logMemberTimedBan({
mod: mod.user,
user,
caseNumber: createdCase.case_number,
reason,
banTime: humanizeDuration(time),
});
} else {
pluginData.getPlugin(LogsPlugin).logMemberBan({
mod: mod.user,
user,
caseNumber: createdCase.case_number,
reason,
});
}
sendSuccessMessage(
pluginData,
msg.channel,
`Ban updated to ${time ? "expire in " + humanizeDuration(time) + " from now" : "indefinite"}`,
);
lock.unlock();
return;
}
} else {
// Ask the mod if we should upgrade to a forceban as the user is not on the server
const reply = await waitForButtonConfirm(
msg.channel,
{ content: "User not on server, forceban instead?" },
{ confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id },
);
if (!reply) {
sendErrorMessage(pluginData, msg.channel, "User not on server, ban cancelled by moderator");
lock.unlock();
return;
} else {
forceban = true;
}
}
}
// Make sure we're allowed to ban this member if they are on the server
if (!forceban && !canActOn(pluginData, msg.member, memberToBan!)) {
const ourLevel = getMemberLevel(pluginData, msg.member);
const targetLevel = getMemberLevel(pluginData, memberToBan!);
sendErrorMessage(
pluginData,
msg.channel,
`Cannot ban: target permission level is equal or higher to yours, ${targetLevel} >= ${ourLevel}`,
);
lock.unlock();
return;
}
let contactMethods;
try {
contactMethods = readContactMethodsFromArgs(args);
} catch (e) {
sendErrorMessage(pluginData, msg.channel, e.message);
lock.unlock();
return;
}
const deleteMessageDays =
args["delete-days"] ?? (await pluginData.config.getForMessage(msg)).ban_delete_message_days;
const banResult = await banUserId(
pluginData,
user.id,
reason,
{
contactMethods,
caseArgs: {
modId: mod.id,
ppId: mod.id !== msg.author.id ? msg.author.id : undefined,
},
deleteMessageDays,
modId: mod.id,
},
time,
);
if (banResult.status === "failed") {
sendErrorMessage(pluginData, msg.channel, `Failed to ban member: ${banResult.error}`);
lock.unlock();
return;
}
let forTime = "";
if (time && time > 0) {
forTime = `for ${humanizeDuration(time)} `;
}
// Confirm the action to the moderator
let response = "";
if (!forceban) {
response = `Banned **${renderUsername(user)}** ${forTime}(Case #${banResult.case.case_number})`;
if (banResult.notifyResult.text) response += ` (${banResult.notifyResult.text})`;
} else {
response = `Member forcebanned ${forTime}(Case #${banResult.case.case_number})`;
}
lock.unlock();
sendSuccessMessage(pluginData, msg.channel, response);
},
});

View file

@ -1,29 +0,0 @@
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin.js";
import { sendErrorMessage } from "../../../pluginUtils.js";
import { modActionsCmd } from "../types.js";
export const CaseCmd = modActionsCmd({
trigger: "case",
permission: "can_view",
description: "Show information about a specific case",
signature: [
{
caseNumber: ct.number(),
},
],
async run({ pluginData, message: msg, args }) {
const theCase = await pluginData.state.cases.findByCaseNumber(args.caseNumber);
if (!theCase) {
sendErrorMessage(pluginData, msg.channel, "Case not found");
return;
}
const casesPlugin = pluginData.getPlugin(CasesPlugin);
const embed = await casesPlugin.getCaseEmbed(theCase.id, msg.author.id);
msg.channel.send(embed);
},
});

View file

@ -1,83 +0,0 @@
import { APIEmbed } from "discord.js";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { sendErrorMessage } from "../../../pluginUtils.js";
import { UnknownUser, emptyEmbedValue, renderUsername, resolveMember, resolveUser, trimLines } from "../../../utils.js";
import { asyncMap } from "../../../utils/async.js";
import { createPaginatedMessage } from "../../../utils/createPaginatedMessage.js";
import { getGuildPrefix } from "../../../utils/getGuildPrefix.js";
import { CasesPlugin } from "../../Cases/CasesPlugin.js";
import { modActionsCmd } from "../types.js";
const opts = {
mod: ct.userId({ option: true }),
};
const casesPerPage = 5;
export const CasesModCmd = modActionsCmd({
trigger: ["cases", "modlogs", "infractions"],
permission: "can_view",
description: "Show the most recent 5 cases by the specified -mod",
signature: [
{
...opts,
},
],
async run({ pluginData, message: msg, args }) {
const modId = args.mod || msg.author.id;
const mod =
(await resolveMember(pluginData.client, pluginData.guild, modId)) ||
(await resolveUser(pluginData.client, modId));
const modName = mod instanceof UnknownUser ? modId : renderUsername(mod);
const casesPlugin = pluginData.getPlugin(CasesPlugin);
const totalCases = await casesPlugin.getTotalCasesByMod(modId);
if (totalCases === 0) {
sendErrorMessage(pluginData, msg.channel, `No cases by **${modName}**`);
return;
}
const totalPages = Math.max(Math.ceil(totalCases / casesPerPage), 1);
const prefix = getGuildPrefix(pluginData);
createPaginatedMessage(
pluginData.client,
msg.channel,
totalPages,
async (page) => {
const cases = await casesPlugin.getRecentCasesByMod(modId, casesPerPage, (page - 1) * casesPerPage);
const lines = await asyncMap(cases, (c) => casesPlugin.getCaseSummary(c, true, msg.author.id));
const isLastPage = page === totalPages;
const firstCaseNum = (page - 1) * casesPerPage + 1;
const lastCaseNum = isLastPage ? totalCases : page * casesPerPage;
const title = `Most recent cases ${firstCaseNum}-${lastCaseNum} of ${totalCases} by ${modName}`;
const embed = {
author: {
name: title,
icon_url: mod instanceof UnknownUser ? undefined : mod.displayAvatarURL(),
},
description: lines.join("\n"),
fields: [
{
name: emptyEmbedValue,
value: trimLines(`
Use \`${prefix}case <num>\` to see more information about an individual case
Use \`${prefix}cases <user>\` to see a specific user's cases
`),
},
],
} satisfies APIEmbed;
return { embeds: [embed] };
},
{
limitToUserId: msg.author.id,
},
);
},
});

View file

@ -1,156 +0,0 @@
import { APIEmbed, User } from "discord.js";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { CaseTypes } from "../../../data/CaseTypes.js";
import { sendErrorMessage } from "../../../pluginUtils.js";
import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin.js";
import {
UnknownUser,
chunkArray,
emptyEmbedValue,
renderUsername,
resolveMember,
resolveUser,
} from "../../../utils.js";
import { asyncMap } from "../../../utils/async.js";
import { createPaginatedMessage } from "../../../utils/createPaginatedMessage.js";
import { getGuildPrefix } from "../../../utils/getGuildPrefix.js";
import { modActionsCmd } from "../types.js";
const opts = {
expand: ct.bool({ option: true, isSwitch: true, shortcut: "e" }),
hidden: ct.bool({ option: true, isSwitch: true, shortcut: "h" }),
reverseFilters: ct.switchOption({ def: false, shortcut: "r" }),
notes: ct.switchOption({ def: false, shortcut: "n" }),
warns: ct.switchOption({ def: false, shortcut: "w" }),
mutes: ct.switchOption({ def: false, shortcut: "m" }),
unmutes: ct.switchOption({ def: false, shortcut: "um" }),
bans: ct.switchOption({ def: false, shortcut: "b" }),
unbans: ct.switchOption({ def: false, shortcut: "ub" }),
};
const casesPerPage = 5;
export const CasesUserCmd = modActionsCmd({
trigger: ["cases", "modlogs"],
permission: "can_view",
description: "Show a list of cases the specified user has",
signature: [
{
user: ct.string(),
...opts,
},
],
async run({ pluginData, message: msg, args }) {
const user =
(await resolveMember(pluginData.client, pluginData.guild, args.user)) ||
(await resolveUser(pluginData.client, args.user));
if (user instanceof UnknownUser) {
sendErrorMessage(pluginData, msg.channel, `User not found`);
return;
}
let cases = await pluginData.state.cases.with("notes").getByUserId(user.id);
const typesToShow: CaseTypes[] = [];
if (args.notes) typesToShow.push(CaseTypes.Note);
if (args.warns) typesToShow.push(CaseTypes.Warn);
if (args.mutes) typesToShow.push(CaseTypes.Mute);
if (args.unmutes) typesToShow.push(CaseTypes.Unmute);
if (args.bans) typesToShow.push(CaseTypes.Ban);
if (args.unbans) typesToShow.push(CaseTypes.Unban);
if (typesToShow.length > 0) {
// Reversed: Hide specified types
if (args.reverseFilters) cases = cases.filter((c) => !typesToShow.includes(c.type));
// Normal: Show only specified types
else cases = cases.filter((c) => typesToShow.includes(c.type));
}
const normalCases = cases.filter((c) => !c.is_hidden);
const hiddenCases = cases.filter((c) => c.is_hidden);
const userName =
user instanceof UnknownUser && cases.length ? cases[cases.length - 1].user_name : renderUsername(user);
if (cases.length === 0) {
msg.channel.send(`No cases found for **${userName}**`);
} else {
const casesToDisplay = args.hidden ? cases : normalCases;
if (!casesToDisplay.length) {
msg.channel.send(
`No normal cases found for **${userName}**. Use "-hidden" to show ${cases.length} hidden cases.`,
);
return;
}
if (args.expand) {
if (casesToDisplay.length > 8) {
msg.channel.send("Too many cases for expanded view. Please use compact view instead.");
return;
}
// Expanded view (= individual case embeds)
const casesPlugin = pluginData.getPlugin(CasesPlugin);
for (const theCase of casesToDisplay) {
const embed = await casesPlugin.getCaseEmbed(theCase.id);
msg.channel.send(embed);
}
} else {
// Compact view (= regular message with a preview of each case)
const casesPlugin = pluginData.getPlugin(CasesPlugin);
const totalPages = Math.max(Math.ceil(casesToDisplay.length / casesPerPage), 1);
const prefix = getGuildPrefix(pluginData);
createPaginatedMessage(
pluginData.client,
msg.channel,
totalPages,
async (page) => {
const chunkedCases = chunkArray(casesToDisplay, casesPerPage)[page - 1];
const lines = await asyncMap(chunkedCases, (c) => casesPlugin.getCaseSummary(c, true, msg.author.id));
const isLastPage = page === totalPages;
const firstCaseNum = (page - 1) * casesPerPage + 1;
const lastCaseNum = isLastPage ? casesToDisplay.length : page * casesPerPage;
const title =
totalPages === 1
? `Cases for ${userName} (${lines.length} total)`
: `Most recent cases ${firstCaseNum}-${lastCaseNum} of ${casesToDisplay.length} for ${userName}`;
const embed = {
author: {
name: title,
icon_url: user instanceof User ? user.displayAvatarURL() : undefined,
},
description: lines.join("\n"),
fields: [
{
name: emptyEmbedValue,
value: `Use \`${prefix}case <num>\` to see more information about an individual case`,
},
],
} satisfies APIEmbed;
if (isLastPage && !args.hidden && hiddenCases.length)
embed.fields.push({
name: emptyEmbedValue,
value:
hiddenCases.length === 1
? `*+${hiddenCases.length} hidden case, use "-hidden" to show it*`
: `*+${hiddenCases.length} hidden cases, use "-hidden" to show them*`,
});
return { embeds: [embed] };
},
{
limitToUserId: msg.author.id,
},
);
}
}
},
});

View file

@ -1,98 +0,0 @@
import { helpers } from "knub";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { Case } from "../../../data/entities/Case.js";
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { SECONDS, renderUsername, trimLines } from "../../../utils.js";
import { CasesPlugin } from "../../Cases/CasesPlugin.js";
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin.js";
import { modActionsCmd } from "../types.js";
export const DeleteCaseCmd = modActionsCmd({
trigger: ["delete_case", "deletecase"],
permission: "can_deletecase",
description: trimLines(`
Delete the specified case. This operation can *not* be reversed.
It is generally recommended to use \`!hidecase\` instead when possible.
`),
signature: {
caseNumber: ct.number({ rest: true }),
force: ct.switchOption({ def: false, shortcut: "f" }),
},
async run({ pluginData, message, args }) {
const failed: number[] = [];
const validCases: Case[] = [];
let cancelled = 0;
for (const num of args.caseNumber) {
const theCase = await pluginData.state.cases.findByCaseNumber(num);
if (!theCase) {
failed.push(num);
continue;
}
validCases.push(theCase);
}
if (failed.length === args.caseNumber.length) {
sendErrorMessage(pluginData, message.channel, "None of the cases were found!");
return;
}
for (const theCase of validCases) {
if (!args.force) {
const cases = pluginData.getPlugin(CasesPlugin);
const embedContent = await cases.getCaseEmbed(theCase);
message.channel.send({
...embedContent,
content: "Delete the following case? Answer 'Yes' to continue, 'No' to cancel.",
});
const reply = await helpers.waitForReply(pluginData.client, message.channel, message.author.id, 15 * SECONDS);
const normalizedReply = (reply?.content || "").toLowerCase().trim();
if (normalizedReply !== "yes" && normalizedReply !== "y") {
message.channel.send("Cancelled. Case was not deleted.");
cancelled++;
continue;
}
}
const deletedByName = renderUsername(message.author);
const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
const deletedAt = timeAndDate.inGuildTz().format(timeAndDate.getDateFormat("pretty_datetime"));
await pluginData.state.cases.softDelete(
theCase.id,
message.author.id,
deletedByName,
`Case deleted by **${deletedByName}** (\`${message.author.id}\`) on ${deletedAt}`,
);
const logs = pluginData.getPlugin(LogsPlugin);
logs.logCaseDelete({
mod: message.member,
case: theCase,
});
}
const failedAddendum =
failed.length > 0
? `\nThe following cases were not found: ${failed.toString().replace(new RegExp(",", "g"), ", ")}`
: "";
const amt = validCases.length - cancelled;
if (amt === 0) {
sendErrorMessage(pluginData, message.channel, "All deletions were cancelled, no cases were deleted.");
return;
}
sendSuccessMessage(
pluginData,
message.channel,
`${amt} case${amt === 1 ? " was" : "s were"} deleted!${failedAddendum}`,
);
},
});

View file

@ -1,103 +0,0 @@
import { Snowflake } from "discord.js";
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { CaseTypes } from "../../../data/CaseTypes.js";
import { LogType } from "../../../data/LogType.js";
import { canActOn, hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin.js";
import { DAYS, MINUTES, resolveMember, resolveUser } from "../../../utils.js";
import { LogsPlugin } from "../../Logs/LogsPlugin.js";
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments.js";
import { ignoreEvent } from "../functions/ignoreEvent.js";
import { isBanned } from "../functions/isBanned.js";
import { IgnoredEventType, modActionsCmd } from "../types.js";
const opts = {
mod: ct.member({ option: true }),
};
export const ForcebanCmd = modActionsCmd({
trigger: "forceban",
permission: "can_ban",
description: "Force-ban the specified user, even if they aren't on the server",
signature: [
{
user: ct.string(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
],
async run({ pluginData, message: msg, args }) {
const user = await resolveUser(pluginData.client, args.user);
if (!user.id) {
sendErrorMessage(pluginData, msg.channel, `User not found`);
return;
}
// If the user exists as a guild member, make sure we can act on them first
const member = await resolveMember(pluginData.client, pluginData.guild, user.id);
if (member && !canActOn(pluginData, msg.member, member)) {
sendErrorMessage(pluginData, msg.channel, "Cannot forceban this user: insufficient permissions");
return;
}
// Make sure the user isn't already banned
const banned = await isBanned(pluginData, user.id);
if (banned) {
sendErrorMessage(pluginData, msg.channel, `User is already banned`);
return;
}
// The moderator who did the action is the message author or, if used, the specified -mod
let mod = msg.member;
if (args.mod) {
if (!(await hasPermission(pluginData, "can_act_as_other", { message: msg }))) {
sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod");
return;
}
mod = args.mod;
}
const reason = formatReasonWithAttachments(args.reason, [...msg.attachments.values()]);
ignoreEvent(pluginData, IgnoredEventType.Ban, user.id);
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_BAN, user.id);
try {
// FIXME: Use banUserId()?
await pluginData.guild.bans.create(user.id as Snowflake, {
deleteMessageSeconds: (1 * DAYS) / MINUTES,
reason: reason ?? undefined,
});
} catch {
sendErrorMessage(pluginData, msg.channel, "Failed to forceban member");
return;
}
// Create a case
const casesPlugin = pluginData.getPlugin(CasesPlugin);
const createdCase = await casesPlugin.createCase({
userId: user.id,
modId: mod.id,
type: CaseTypes.Ban,
reason,
ppId: mod.id !== msg.author.id ? msg.author.id : undefined,
});
// Confirm the action
sendSuccessMessage(pluginData, msg.channel, `Member forcebanned (Case #${createdCase.case_number})`);
// Log the action
pluginData.getPlugin(LogsPlugin).logMemberForceban({
mod,
userId: user.id,
caseNumber: createdCase.case_number,
reason,
});
pluginData.state.events.emit("ban", user.id, reason);
},
});

View file

@ -1,51 +0,0 @@
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { canActOn, sendErrorMessage } from "../../../pluginUtils.js";
import { resolveMember, resolveUser } from "../../../utils.js";
import { actualMuteUserCmd } from "../functions/actualMuteUserCmd.js";
import { modActionsCmd } from "../types.js";
const opts = {
mod: ct.member({ option: true }),
notify: ct.string({ option: true }),
"notify-channel": ct.textChannel({ option: true }),
};
export const ForcemuteCmd = modActionsCmd({
trigger: "forcemute",
permission: "can_mute",
description: "Force-mute the specified user, even if they're not on the server",
signature: [
{
user: ct.string(),
time: ct.delay(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
{
user: ct.string(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
],
async run({ pluginData, message: msg, args }) {
const user = await resolveUser(pluginData.client, args.user);
if (!user.id) {
sendErrorMessage(pluginData, msg.channel, `User not found`);
return;
}
const memberToMute = await resolveMember(pluginData.client, pluginData.guild, user.id);
// Make sure we're allowed to mute this user
if (memberToMute && !canActOn(pluginData, msg.member, memberToMute)) {
sendErrorMessage(pluginData, msg.channel, "Cannot mute: insufficient permissions");
return;
}
actualMuteUserCmd(pluginData, user, msg, { ...args, notify: "none" });
},
});

View file

@ -1,56 +0,0 @@
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { canActOn, sendErrorMessage } from "../../../pluginUtils.js";
import { resolveMember, resolveUser } from "../../../utils.js";
import { actualUnmuteCmd } from "../functions/actualUnmuteUserCmd.js";
import { modActionsCmd } from "../types.js";
const opts = {
mod: ct.member({ option: true }),
};
export const ForceUnmuteCmd = modActionsCmd({
trigger: "forceunmute",
permission: "can_mute",
description: "Force-unmute the specified user, even if they're not on the server",
signature: [
{
user: ct.string(),
time: ct.delay(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
{
user: ct.string(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
],
async run({ pluginData, message: msg, args }) {
const user = await resolveUser(pluginData.client, args.user);
if (!user.id) {
sendErrorMessage(pluginData, msg.channel, `User not found`);
return;
}
// Check if they're muted in the first place
if (!(await pluginData.state.mutes.isMuted(user.id))) {
sendErrorMessage(pluginData, msg.channel, "Cannot unmute: member is not muted");
return;
}
// Find the server member to unmute
const memberToUnmute = await resolveMember(pluginData.client, pluginData.guild, user.id);
// Make sure we're allowed to unmute this member
if (memberToUnmute && !canActOn(pluginData, msg.member, memberToUnmute)) {
sendErrorMessage(pluginData, msg.channel, "Cannot unmute: insufficient permissions");
return;
}
actualUnmuteCmd(pluginData, user, msg, args);
},
});

View file

@ -1,45 +0,0 @@
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils.js";
import { modActionsCmd } from "../types.js";
export const HideCaseCmd = modActionsCmd({
trigger: ["hide", "hidecase", "hide_case"],
permission: "can_hidecase",
description: "Hide the specified case so it doesn't appear in !cases or !info",
signature: [
{
caseNum: ct.number({ rest: true }),
},
],
async run({ pluginData, message: msg, args }) {
const failed: number[] = [];
for (const num of args.caseNum) {
const theCase = await pluginData.state.cases.findByCaseNumber(num);
if (!theCase) {
failed.push(num);
continue;
}
await pluginData.state.cases.setHidden(theCase.id, true);
}
if (failed.length === args.caseNum.length) {
sendErrorMessage(pluginData, msg.channel, "None of the cases were found!");
return;
}
const failedAddendum =
failed.length > 0
? `\nThe following cases were not found: ${failed.toString().replace(new RegExp(",", "g"), ", ")}`
: "";
const amt = args.caseNum.length - failed.length;
sendSuccessMessage(
pluginData,
msg.channel,
`${amt} case${amt === 1 ? " is" : "s are"} now hidden! Use \`unhidecase\` to unhide them.${failedAddendum}`,
);
},
});

View file

@ -1,29 +0,0 @@
import { commandTypeHelpers as ct } from "../../../commandTypes.js";
import { actualKickMemberCmd } from "../functions/actualKickMemberCmd.js";
import { modActionsCmd } from "../types.js";
const opts = {
mod: ct.member({ option: true }),
notify: ct.string({ option: true }),
"notify-channel": ct.textChannel({ option: true }),
clean: ct.bool({ option: true, isSwitch: true }),
};
export const KickCmd = modActionsCmd({
trigger: "kick",
permission: "can_kick",
description: "Kick the specified member",
signature: [
{
user: ct.string(),
reason: ct.string({ required: false, catchAll: true }),
...opts,
},
],
async run({ pluginData, message: msg, args }) {
actualKickMemberCmd(pluginData, msg, args);
},
});

Some files were not shown because too many files have changed in this diff Show more