mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-03-16 22:21:51 +00:00
feat: first batch of emojis 🎉
This commit is contained in:
parent
dfb0e2c19d
commit
a4c4b17a14
91 changed files with 3659 additions and 2032 deletions
30
backend/package-lock.json
generated
30
backend/package-lock.json
generated
|
@ -5520,9 +5520,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/knub": {
|
"node_modules/knub": {
|
||||||
"version": "32.0.0-next.16",
|
"version": "32.0.0-next.17",
|
||||||
"resolved": "https://registry.npmjs.org/knub/-/knub-32.0.0-next.16.tgz",
|
"resolved": "https://registry.npmjs.org/knub/-/knub-32.0.0-next.17.tgz",
|
||||||
"integrity": "sha512-lmjbLusvinWCoyo0T3dtWy6PEuqysIcqQvg85W85th59ubHasnTc+KGR+4o6EgLCzuDUtc4dP1XQk7XAIKnkYQ==",
|
"integrity": "sha512-q8PeIJuOYUQySOmVuXF56ykklKRnnJmAMpfSN1+vWCNhdMsDxIPZAkRpvekQD6oBkYoL7WlzEsMoUsECGZp+QQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"discord.js": "^14.11.0",
|
"discord.js": "^14.11.0",
|
||||||
"knub-command-manager": "^9.1.0",
|
"knub-command-manager": "^9.1.0",
|
||||||
|
@ -9228,11 +9228,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ts-essentials": {
|
"node_modules/ts-essentials": {
|
||||||
"version": "9.3.2",
|
"version": "9.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-9.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-9.4.1.tgz",
|
||||||
"integrity": "sha512-JxKJzuWqH1MmH4ZFHtJzGEhkfN3QvVR3C3w+4BIoWeoY68UVVoA2Np/Bca9z0IPSErVCWhv439aT0We4Dks8kQ==",
|
"integrity": "sha512-oke0rI2EN9pzHsesdmrOrnqv1eQODmJpd/noJjwj2ZPC3Z4N2wbjrOEqnsEgmvlO2+4fBb0a794DCna2elEVIQ==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": ">=4.1.0"
|
"typescript": ">=4.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ts-mixer": {
|
"node_modules/ts-mixer": {
|
||||||
|
@ -9645,19 +9650,6 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
|
||||||
"version": "5.1.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
|
|
||||||
"integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
|
||||||
"tsc": "bin/tsc",
|
|
||||||
"tsserver": "bin/tsserver"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.17"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/uid2": {
|
"node_modules/uid2": {
|
||||||
"version": "0.0.4",
|
"version": "0.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { In, InsertResult, Repository } from "typeorm";
|
import { In, InsertResult, Repository } from "typeorm";
|
||||||
|
import { FindOptionsWhere } from "typeorm/find-options/FindOptionsWhere";
|
||||||
import { Queue } from "../Queue";
|
import { Queue } from "../Queue";
|
||||||
import { chunkArray } from "../utils";
|
import { chunkArray } from "../utils";
|
||||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||||
|
@ -73,12 +74,16 @@ 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({
|
return this.cases.find({
|
||||||
relations: this.getRelations(),
|
relations: this.getRelations(),
|
||||||
where: {
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
|
...filters,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -98,24 +103,40 @@ export class GuildCases extends BaseGuildRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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({
|
return this.cases.count({
|
||||||
where: {
|
where: {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
mod_id: modId,
|
mod_id: modId,
|
||||||
is_hidden: false,
|
is_hidden: false,
|
||||||
|
...filters,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRecentByModId(modId: string, count: number, skip = 0): Promise<Case[]> {
|
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({
|
return this.cases.find({
|
||||||
relations: this.getRelations(),
|
relations: this.getRelations(),
|
||||||
where: {
|
where,
|
||||||
guild_id: this.guildId,
|
|
||||||
mod_id: modId,
|
|
||||||
is_hidden: false,
|
|
||||||
},
|
|
||||||
skip,
|
skip,
|
||||||
take: count,
|
take: count,
|
||||||
order: {
|
order: {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
ModalSubmitInteraction,
|
ModalSubmitInteraction,
|
||||||
PermissionsBitField,
|
PermissionsBitField,
|
||||||
TextBasedChannel,
|
TextBasedChannel,
|
||||||
|
User,
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import * as t from "io-ts";
|
import * as t from "io-ts";
|
||||||
import {
|
import {
|
||||||
|
@ -101,19 +102,32 @@ export function makeIoTsConfigParser<Schema extends t.Type<any>>(schema: Schema)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isContextInteraction(
|
export function isContextInteraction(
|
||||||
context: TextBasedChannel | ChatInputCommandInteraction,
|
context: TextBasedChannel | User | ChatInputCommandInteraction,
|
||||||
): context is ChatInputCommandInteraction {
|
): context is ChatInputCommandInteraction {
|
||||||
return "commandId" in context && !!context.commandId;
|
return "commandId" in context && !!context.commandId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sendContextResponse(
|
||||||
|
context: TextBasedChannel | User | ChatInputCommandInteraction,
|
||||||
|
response: string | Omit<MessageCreateOptions, "flags">,
|
||||||
|
): Promise<Message> {
|
||||||
|
if (isContextInteraction(context)) {
|
||||||
|
const options = { ...(typeof response === "string" ? { content: response } : response), fetchReply: true };
|
||||||
|
|
||||||
|
return (context.replied ? context.followUp(options) : context.reply(options)) as Promise<Message>;
|
||||||
|
} else {
|
||||||
|
return context.send(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function sendSuccessMessage(
|
export async function sendSuccessMessage(
|
||||||
pluginData: AnyPluginData<any>,
|
pluginData: AnyPluginData<any>,
|
||||||
context: TextBasedChannel | ChatInputCommandInteraction,
|
context: TextBasedChannel | User | ChatInputCommandInteraction,
|
||||||
body: string,
|
body: string,
|
||||||
allowedMentions?: MessageMentionOptions,
|
allowedMentions?: MessageMentionOptions,
|
||||||
responseInteraction?: ModalSubmitInteraction,
|
responseInteraction?: ModalSubmitInteraction,
|
||||||
ephemeral = false,
|
ephemeral = true,
|
||||||
): Promise<Message | undefined> {
|
): Promise<Message | undefined> {
|
||||||
const emoji = pluginData.fullConfig.success_emoji || undefined;
|
const emoji = pluginData.fullConfig.success_emoji || undefined;
|
||||||
const formattedBody = successMessage(body, emoji);
|
const formattedBody = successMessage(body, emoji);
|
||||||
|
@ -153,12 +167,12 @@ export async function sendSuccessMessage(
|
||||||
logger.error(`Context reply failed: ${err}`);
|
logger.error(`Context reply failed: ${err}`);
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
}) as Promise<Message>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendErrorMessage(
|
export async function sendErrorMessage(
|
||||||
pluginData: AnyPluginData<any>,
|
pluginData: AnyPluginData<any>,
|
||||||
context: TextBasedChannel | ChatInputCommandInteraction,
|
context: TextBasedChannel | User | ChatInputCommandInteraction,
|
||||||
body: string,
|
body: string,
|
||||||
allowedMentions?: MessageMentionOptions,
|
allowedMentions?: MessageMentionOptions,
|
||||||
responseInteraction?: ModalSubmitInteraction,
|
responseInteraction?: ModalSubmitInteraction,
|
||||||
|
@ -201,7 +215,7 @@ export async function sendErrorMessage(
|
||||||
logger.error(`Context reply failed: ${err}`);
|
logger.error(`Context reply failed: ${err}`);
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
}) as Promise<Message>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBaseUrl(pluginData: AnyPluginData<any>) {
|
export function getBaseUrl(pluginData: AnyPluginData<any>) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { GuildPluginData } from "knub";
|
import { GuildPluginData } from "knub";
|
||||||
|
import { FindOptionsWhere } from "typeorm/find-options/FindOptionsWhere";
|
||||||
import { Case } from "../../../data/entities/Case";
|
import { Case } from "../../../data/entities/Case";
|
||||||
import { CasesPluginType } from "../types";
|
import { CasesPluginType } from "../types";
|
||||||
|
|
||||||
|
@ -7,6 +8,7 @@ export function getRecentCasesByMod(
|
||||||
modId: string,
|
modId: string,
|
||||||
count: number,
|
count: number,
|
||||||
skip = 0,
|
skip = 0,
|
||||||
|
filters: Omit<FindOptionsWhere<Case>, "guild_id" | "mod_id" | "is_hidden"> = {},
|
||||||
): Promise<Case[]> {
|
): Promise<Case[]> {
|
||||||
return pluginData.state.cases.getRecentByModId(modId, count, skip);
|
return pluginData.state.cases.getRecentByModId(modId, count, skip, filters);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import { GuildPluginData } from "knub";
|
import { GuildPluginData } from "knub";
|
||||||
|
import { FindOptionsWhere } from "typeorm/find-options/FindOptionsWhere";
|
||||||
|
import { Case } from "../../../data/entities/Case";
|
||||||
import { CasesPluginType } from "../types";
|
import { CasesPluginType } from "../types";
|
||||||
|
|
||||||
export function getTotalCasesByMod(pluginData: GuildPluginData<CasesPluginType>, modId: string): Promise<number> {
|
export function getTotalCasesByMod(
|
||||||
return pluginData.state.cases.getTotalCasesByModId(modId);
|
pluginData: GuildPluginData<CasesPluginType>,
|
||||||
|
modId: string,
|
||||||
|
filters: Omit<FindOptionsWhere<Case>, "guild_id" | "mod_id" | "is_hidden"> = {},
|
||||||
|
): Promise<number> {
|
||||||
|
return pluginData.state.cases.getTotalCasesByModId(modId, filters);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,29 +13,47 @@ import { LogsPlugin } from "../Logs/LogsPlugin";
|
||||||
import { MutesPlugin } from "../Mutes/MutesPlugin";
|
import { MutesPlugin } from "../Mutes/MutesPlugin";
|
||||||
import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
|
import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
|
||||||
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
||||||
import { AddCaseCmd } from "./commands/AddCaseCmd";
|
import { AddCaseMsgCmd } from "./commands/addcase/AddCaseMsgCmd";
|
||||||
import { BanCmd } from "./commands/BanCmd";
|
import { AddCaseSlashCmd } from "./commands/addcase/AddCaseSlashCmd";
|
||||||
import { CaseCmd } from "./commands/CaseCmd";
|
import { BanMsgCmd } from "./commands/ban/BanMsgCmd";
|
||||||
import { CasesModCmd } from "./commands/CasesModCmd";
|
import { BanSlashCmd } from "./commands/ban/BanSlashCmd";
|
||||||
import { CasesUserCmd } from "./commands/CasesUserCmd";
|
import { CaseMsgCmd } from "./commands/case/CaseMsgCmd";
|
||||||
import { DeleteCaseCmd } from "./commands/DeleteCaseCmd";
|
import { CaseSlashCmd } from "./commands/case/CaseSlashCmd";
|
||||||
import { ForcebanCmd } from "./commands/ForcebanCmd";
|
import { CasesModMsgCmd } from "./commands/cases/CasesModMsgCmd";
|
||||||
import { ForcemuteCmd } from "./commands/ForcemuteCmd";
|
import { CasesSlashCmd } from "./commands/cases/CasesSlashCmd";
|
||||||
import { ForceUnmuteCmd } from "./commands/ForceunmuteCmd";
|
import { CasesUserMsgCmd } from "./commands/cases/CasesUserMsgCmd";
|
||||||
import { HideCaseCmd } from "./commands/HideCaseCmd";
|
import { DeleteCaseMsgCmd } from "./commands/deletecase/DeleteCaseMsgCmd";
|
||||||
import { KickCmd } from "./commands/KickCmd";
|
import { DeleteCaseSlashCmd } from "./commands/deletecase/DeleteCaseSlashCmd";
|
||||||
import { MassbanCmd } from "./commands/MassBanCmd";
|
import { ForceBanMsgCmd } from "./commands/forceban/ForceBanMsgCmd";
|
||||||
import { MassunbanCmd } from "./commands/MassUnbanCmd";
|
import { ForceBanSlashCmd } from "./commands/forceban/ForceBanSlashCmd";
|
||||||
import { MassmuteCmd } from "./commands/MassmuteCmd";
|
import { ForceMuteMsgCmd } from "./commands/forcemute/ForceMuteMsgCmd";
|
||||||
import { MuteCmd } from "./commands/MuteCmd";
|
import { ForceMuteSlashCmd } from "./commands/forcemute/ForceMuteSlashCmd";
|
||||||
import { SoftbanCmd } from "./commands/SoftbanCommand";
|
import { ForceUnmuteMsgCmd } from "./commands/forceunmute/ForceUnmuteMsgCmd";
|
||||||
import { UnbanCmd } from "./commands/UnbanCmd";
|
import { ForceUnmuteSlashCmd } from "./commands/forceunmute/ForceUnmuteSlashCmd";
|
||||||
import { UnhideCaseCmd } from "./commands/UnhideCaseCmd";
|
import { HideCaseMsgCmd } from "./commands/hidecase/HideCaseMsgCmd";
|
||||||
import { UnmuteCmd } from "./commands/UnmuteCmd";
|
import { HideCaseSlashCmd } from "./commands/hidecase/HideCaseSlashCmd";
|
||||||
import { UpdateCmd } from "./commands/UpdateCmd";
|
import { KickMsgCmd } from "./commands/kick/KickMsgCmd";
|
||||||
import { WarnCmd } from "./commands/WarnCmd";
|
import { KickSlashCmd } from "./commands/kick/KickSlashCmd";
|
||||||
|
import { MassBanMsgCmd } from "./commands/massban/MassBanMsgCmd";
|
||||||
|
import { MassBanSlashCmd } from "./commands/massban/MassBanSlashCmd";
|
||||||
|
import { MassMuteMsgCmd } from "./commands/massmute/MassMuteMsgCmd";
|
||||||
|
import { MassMuteSlashSlashCmd } from "./commands/massmute/MassMuteSlashCmd";
|
||||||
|
import { MassUnbanMsgCmd } from "./commands/massunban/MassUnbanMsgCmd";
|
||||||
|
import { MassUnbanSlashCmd } from "./commands/massunban/MassUnbanSlashCmd";
|
||||||
|
import { MuteMsgCmd } from "./commands/mute/MuteMsgCmd";
|
||||||
|
import { MuteSlashCmd } from "./commands/mute/MuteSlashCmd";
|
||||||
import { NoteMsgCmd } from "./commands/note/NoteMsgCmd";
|
import { NoteMsgCmd } from "./commands/note/NoteMsgCmd";
|
||||||
import { NoteSlashCmd } from "./commands/note/NoteSlashCmd";
|
import { NoteSlashCmd } from "./commands/note/NoteSlashCmd";
|
||||||
|
import { UnbanMsgCmd } from "./commands/unban/UnbanMsgCmd";
|
||||||
|
import { UnbanSlashCmd } from "./commands/unban/UnbanSlashCmd";
|
||||||
|
import { UnhideCaseMsgCmd } from "./commands/unhidecase/UnhideCaseMsgCmd";
|
||||||
|
import { UnhideCaseSlashCmd } from "./commands/unhidecase/UnhideCaseSlashCmd";
|
||||||
|
import { UnmuteMsgCmd } from "./commands/unmute/UnmuteMsgCmd";
|
||||||
|
import { UnmuteSlashCmd } from "./commands/unmute/UnmuteSlashCmd";
|
||||||
|
import { UpdateMsgCmd } from "./commands/update/UpdateMsgCmd";
|
||||||
|
import { UpdateSlashCmd } from "./commands/update/UpdateSlashCmd";
|
||||||
|
import { WarnMsgCmd } from "./commands/warn/WarnMsgCmd";
|
||||||
|
import { WarnSlashCmd } from "./commands/warn/WarnSlashCmd";
|
||||||
import { AuditLogEvents } from "./events/AuditLogEvents";
|
import { AuditLogEvents } from "./events/AuditLogEvents";
|
||||||
import { CreateBanCaseOnManualBanEvt } from "./events/CreateBanCaseOnManualBanEvt";
|
import { CreateBanCaseOnManualBanEvt } from "./events/CreateBanCaseOnManualBanEvt";
|
||||||
import { CreateUnbanCaseOnManualUnbanEvt } from "./events/CreateUnbanCaseOnManualUnbanEvt";
|
import { CreateUnbanCaseOnManualUnbanEvt } from "./events/CreateUnbanCaseOnManualUnbanEvt";
|
||||||
|
@ -148,33 +166,53 @@ export const ModActionsPlugin = zeppelinGuildPlugin<ModActionsPluginType>()({
|
||||||
name: "mod",
|
name: "mod",
|
||||||
description: "Moderation actions",
|
description: "Moderation actions",
|
||||||
defaultMemberPermissions: "0",
|
defaultMemberPermissions: "0",
|
||||||
subcommands: [{ type: "slash", ...NoteSlashCmd }],
|
subcommands: [
|
||||||
|
{ type: "slash", ...AddCaseSlashCmd },
|
||||||
|
{ type: "slash", ...BanSlashCmd },
|
||||||
|
{ type: "slash", ...CaseSlashCmd },
|
||||||
|
{ type: "slash", ...CasesSlashCmd },
|
||||||
|
{ type: "slash", ...DeleteCaseSlashCmd },
|
||||||
|
{ type: "slash", ...ForceBanSlashCmd },
|
||||||
|
{ type: "slash", ...ForceMuteSlashCmd },
|
||||||
|
{ type: "slash", ...ForceUnmuteSlashCmd },
|
||||||
|
{ type: "slash", ...HideCaseSlashCmd },
|
||||||
|
{ type: "slash", ...KickSlashCmd },
|
||||||
|
{ type: "slash", ...MassBanSlashCmd },
|
||||||
|
{ type: "slash", ...MassMuteSlashSlashCmd },
|
||||||
|
{ type: "slash", ...MassUnbanSlashCmd },
|
||||||
|
{ type: "slash", ...MuteSlashCmd },
|
||||||
|
{ type: "slash", ...NoteSlashCmd },
|
||||||
|
{ type: "slash", ...UnbanSlashCmd },
|
||||||
|
{ type: "slash", ...UnhideCaseSlashCmd },
|
||||||
|
{ type: "slash", ...UnmuteSlashCmd },
|
||||||
|
{ type: "slash", ...UpdateSlashCmd },
|
||||||
|
{ type: "slash", ...WarnSlashCmd },
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|
||||||
messageCommands: [
|
messageCommands: [
|
||||||
UpdateCmd,
|
UpdateMsgCmd,
|
||||||
NoteMsgCmd,
|
NoteMsgCmd,
|
||||||
WarnCmd,
|
WarnMsgCmd,
|
||||||
MuteCmd,
|
MuteMsgCmd,
|
||||||
ForcemuteCmd,
|
ForceMuteMsgCmd,
|
||||||
UnmuteCmd,
|
UnmuteMsgCmd,
|
||||||
ForceUnmuteCmd,
|
ForceUnmuteMsgCmd,
|
||||||
KickCmd,
|
KickMsgCmd,
|
||||||
SoftbanCmd,
|
BanMsgCmd,
|
||||||
BanCmd,
|
UnbanMsgCmd,
|
||||||
UnbanCmd,
|
ForceBanMsgCmd,
|
||||||
ForcebanCmd,
|
MassBanMsgCmd,
|
||||||
MassbanCmd,
|
MassMuteMsgCmd,
|
||||||
MassmuteCmd,
|
MassUnbanMsgCmd,
|
||||||
MassunbanCmd,
|
AddCaseMsgCmd,
|
||||||
AddCaseCmd,
|
CaseMsgCmd,
|
||||||
CaseCmd,
|
CasesUserMsgCmd,
|
||||||
CasesUserCmd,
|
CasesModMsgCmd,
|
||||||
CasesModCmd,
|
HideCaseMsgCmd,
|
||||||
HideCaseCmd,
|
UnhideCaseMsgCmd,
|
||||||
UnhideCaseCmd,
|
DeleteCaseMsgCmd,
|
||||||
DeleteCaseCmd,
|
|
||||||
],
|
],
|
||||||
|
|
||||||
public: {
|
public: {
|
||||||
|
@ -198,7 +236,7 @@ export const ModActionsPlugin = zeppelinGuildPlugin<ModActionsPluginType>()({
|
||||||
|
|
||||||
updateCase(pluginData) {
|
updateCase(pluginData) {
|
||||||
return (msg: Message, caseNumber: number | null, note: string) => {
|
return (msg: Message, caseNumber: number | null, note: string) => {
|
||||||
updateCase(pluginData, msg, { caseNumber, note });
|
updateCase(pluginData, msg.channel, msg.author, caseNumber ?? undefined, note, [...msg.attachments.values()]);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
|
||||||
import { CaseTypes } from "../../../data/CaseTypes";
|
|
||||||
import { Case } from "../../../data/entities/Case";
|
|
||||||
import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin";
|
|
||||||
import { canActOn, hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
|
||||||
import { renderUserUsername, resolveMember, resolveUser } from "../../../utils";
|
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
|
||||||
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments";
|
|
||||||
import { modActionsMsgCmd } from "../types";
|
|
||||||
|
|
||||||
const opts = {
|
|
||||||
mod: ct.member({ option: true }),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AddCaseCmd = modActionsMsgCmd({
|
|
||||||
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 **${renderUserUsername(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,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,225 +0,0 @@
|
||||||
import humanizeDuration from "humanize-duration";
|
|
||||||
import { getMemberLevel } from "knub/helpers";
|
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
|
||||||
import { CaseTypes } from "../../../data/CaseTypes";
|
|
||||||
import { clearExpiringTempban, registerExpiringTempban } from "../../../data/loops/expiringTempbansLoop";
|
|
||||||
import { canActOn, hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
|
||||||
import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin";
|
|
||||||
import { renderUserUsername, resolveMember, resolveUser } from "../../../utils";
|
|
||||||
import { banLock } from "../../../utils/lockNameHelpers";
|
|
||||||
import { waitForButtonConfirm } from "../../../utils/waitForInteraction";
|
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
|
||||||
import { banUserId } from "../functions/banUserId";
|
|
||||||
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments";
|
|
||||||
import { isBanned } from "../functions/isBanned";
|
|
||||||
import { readContactMethodsFromArgs } from "../functions/readContactMethodsFromArgs";
|
|
||||||
import { modActionsMsgCmd } from "../types";
|
|
||||||
|
|
||||||
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 = modActionsMsgCmd({
|
|
||||||
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 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const time = args["time"] ? args["time"] : null;
|
|
||||||
const reason = formatReasonWithAttachments(args.reason, [...msg.attachments.values()]);
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 **${renderUserUsername(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);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
|
||||||
import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin";
|
|
||||||
import { sendErrorMessage } from "../../../pluginUtils";
|
|
||||||
import { modActionsMsgCmd } from "../types";
|
|
||||||
|
|
||||||
export const CaseCmd = modActionsMsgCmd({
|
|
||||||
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);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,81 +0,0 @@
|
||||||
import { APIEmbed, User } from "discord.js";
|
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
|
||||||
import { sendErrorMessage } from "../../../pluginUtils";
|
|
||||||
import { emptyEmbedValue, resolveUser, trimLines } from "../../../utils";
|
|
||||||
import { asyncMap } from "../../../utils/async";
|
|
||||||
import { createPaginatedMessage } from "../../../utils/createPaginatedMessage";
|
|
||||||
import { getChunkedEmbedFields } from "../../../utils/getChunkedEmbedFields";
|
|
||||||
import { getGuildPrefix } from "../../../utils/getGuildPrefix";
|
|
||||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
|
||||||
import { modActionsMsgCmd } from "../types";
|
|
||||||
|
|
||||||
const opts = {
|
|
||||||
mod: ct.userId({ option: true }),
|
|
||||||
};
|
|
||||||
|
|
||||||
const casesPerPage = 5;
|
|
||||||
|
|
||||||
export const CasesModCmd = modActionsMsgCmd({
|
|
||||||
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 resolveUser(pluginData.client, modId);
|
|
||||||
const modName = mod instanceof User ? mod.tag : modId;
|
|
||||||
|
|
||||||
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 firstCaseNum = (page - 1) * casesPerPage + 1;
|
|
||||||
const lastCaseNum = page * casesPerPage;
|
|
||||||
const title = `Most recent cases ${firstCaseNum}-${lastCaseNum} of ${totalCases} by ${modName}`;
|
|
||||||
|
|
||||||
const embed = {
|
|
||||||
author: {
|
|
||||||
name: title,
|
|
||||||
icon_url: mod instanceof User ? mod.displayAvatarURL() : undefined,
|
|
||||||
},
|
|
||||||
fields: [
|
|
||||||
...getChunkedEmbedFields(emptyEmbedValue, lines.join("\n")),
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,139 +0,0 @@
|
||||||
import { APIEmbed, User } from "discord.js";
|
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
|
||||||
import { CaseTypes } from "../../../data/CaseTypes";
|
|
||||||
import { sendErrorMessage } from "../../../pluginUtils";
|
|
||||||
import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin";
|
|
||||||
import { UnknownUser, chunkArray, emptyEmbedValue, renderUserUsername, resolveUser, trimLines } from "../../../utils";
|
|
||||||
import { asyncMap } from "../../../utils/async";
|
|
||||||
import { getChunkedEmbedFields } from "../../../utils/getChunkedEmbedFields";
|
|
||||||
import { getGuildPrefix } from "../../../utils/getGuildPrefix";
|
|
||||||
import { modActionsMsgCmd } from "../types";
|
|
||||||
|
|
||||||
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" }),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CasesUserCmd = modActionsMsgCmd({
|
|
||||||
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 resolveUser(pluginData.client, args.user);
|
|
||||||
if (!user.id) {
|
|
||||||
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 : renderUserUsername(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 lines = await asyncMap(casesToDisplay, (c) => casesPlugin.getCaseSummary(c, true, msg.author.id));
|
|
||||||
|
|
||||||
const prefix = getGuildPrefix(pluginData);
|
|
||||||
const linesPerChunk = 10;
|
|
||||||
const lineChunks = chunkArray(lines, linesPerChunk);
|
|
||||||
|
|
||||||
const footerField = {
|
|
||||||
name: emptyEmbedValue,
|
|
||||||
value: trimLines(`
|
|
||||||
Use \`${prefix}case <num>\` to see more information about an individual case
|
|
||||||
`),
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [i, linesInChunk] of lineChunks.entries()) {
|
|
||||||
const isLastChunk = i === lineChunks.length - 1;
|
|
||||||
|
|
||||||
if (isLastChunk && !args.hidden && hiddenCases.length) {
|
|
||||||
if (hiddenCases.length === 1) {
|
|
||||||
linesInChunk.push(`*+${hiddenCases.length} hidden case, use "-hidden" to show it*`);
|
|
||||||
} else {
|
|
||||||
linesInChunk.push(`*+${hiddenCases.length} hidden cases, use "-hidden" to show them*`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const chunkStart = i * linesPerChunk + 1;
|
|
||||||
const chunkEnd = Math.min((i + 1) * linesPerChunk, lines.length);
|
|
||||||
|
|
||||||
const embed = {
|
|
||||||
author: {
|
|
||||||
name:
|
|
||||||
lineChunks.length === 1
|
|
||||||
? `Cases for ${userName} (${lines.length} total)`
|
|
||||||
: `Cases ${chunkStart}–${chunkEnd} of ${lines.length} for ${userName}`,
|
|
||||||
icon_url: user instanceof User ? user.displayAvatarURL() : undefined,
|
|
||||||
},
|
|
||||||
fields: [
|
|
||||||
...getChunkedEmbedFields(emptyEmbedValue, linesInChunk.join("\n")),
|
|
||||||
...(isLastChunk ? [footerField] : []),
|
|
||||||
],
|
|
||||||
} satisfies APIEmbed;
|
|
||||||
|
|
||||||
msg.channel.send({ embeds: [embed] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,98 +0,0 @@
|
||||||
import { helpers } from "knub";
|
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
|
||||||
import { Case } from "../../../data/entities/Case";
|
|
||||||
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
|
||||||
import { SECONDS, trimLines } from "../../../utils";
|
|
||||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
|
||||||
import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
|
|
||||||
import { modActionsMsgCmd } from "../types";
|
|
||||||
|
|
||||||
export const DeleteCaseCmd = modActionsMsgCmd({
|
|
||||||
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 = message.author.tag;
|
|
||||||
|
|
||||||
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}`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,103 +0,0 @@
|
||||||
import { Snowflake } from "discord.js";
|
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
|
||||||
import { CaseTypes } from "../../../data/CaseTypes";
|
|
||||||
import { LogType } from "../../../data/LogType";
|
|
||||||
import { canActOn, hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
|
||||||
import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin";
|
|
||||||
import { DAYS, MINUTES, resolveMember, resolveUser } from "../../../utils";
|
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
|
||||||
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments";
|
|
||||||
import { ignoreEvent } from "../functions/ignoreEvent";
|
|
||||||
import { isBanned } from "../functions/isBanned";
|
|
||||||
import { IgnoredEventType, modActionsMsgCmd } from "../types";
|
|
||||||
|
|
||||||
const opts = {
|
|
||||||
mod: ct.member({ option: true }),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ForcebanCmd = modActionsMsgCmd({
|
|
||||||
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);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,51 +0,0 @@
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
|
||||||
import { canActOn, sendErrorMessage } from "../../../pluginUtils";
|
|
||||||
import { resolveMember, resolveUser } from "../../../utils";
|
|
||||||
import { actualMuteUserCmd } from "../functions/actualMuteUserCmd";
|
|
||||||
import { modActionsMsgCmd } from "../types";
|
|
||||||
|
|
||||||
const opts = {
|
|
||||||
mod: ct.member({ option: true }),
|
|
||||||
notify: ct.string({ option: true }),
|
|
||||||
"notify-channel": ct.textChannel({ option: true }),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ForcemuteCmd = modActionsMsgCmd({
|
|
||||||
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" });
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,45 +0,0 @@
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
|
||||||
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
|
||||||
import { modActionsMsgCmd } from "../types";
|
|
||||||
|
|
||||||
export const HideCaseCmd = modActionsMsgCmd({
|
|
||||||
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}`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
|
||||||
import { actualKickMemberCmd } from "../functions/actualKickMemberCmd";
|
|
||||||
import { modActionsMsgCmd } from "../types";
|
|
||||||
|
|
||||||
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 = modActionsMsgCmd({
|
|
||||||
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);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,156 +0,0 @@
|
||||||
import { Snowflake } from "discord.js";
|
|
||||||
import { waitForReply } from "knub/helpers";
|
|
||||||
import { performance } from "perf_hooks";
|
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
|
||||||
import { CaseTypes } from "../../../data/CaseTypes";
|
|
||||||
import { LogType } from "../../../data/LogType";
|
|
||||||
import { humanizeDurationShort } from "../../../humanizeDurationShort";
|
|
||||||
import { canActOn, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
|
||||||
import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin";
|
|
||||||
import { DAYS, MINUTES, SECONDS, noop } from "../../../utils";
|
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
|
||||||
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments";
|
|
||||||
import { ignoreEvent } from "../functions/ignoreEvent";
|
|
||||||
import { IgnoredEventType, modActionsMsgCmd } from "../types";
|
|
||||||
|
|
||||||
export const MassbanCmd = modActionsMsgCmd({
|
|
||||||
trigger: "massban",
|
|
||||||
permission: "can_massban",
|
|
||||||
description: "Mass-ban a list of user IDs",
|
|
||||||
|
|
||||||
signature: [
|
|
||||||
{
|
|
||||||
userIds: ct.string({ rest: true }),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
async run({ pluginData, message: msg, args }) {
|
|
||||||
// Limit to 100 users at once (arbitrary?)
|
|
||||||
if (args.userIds.length > 100) {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, `Can only massban max 100 users at once`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ask for ban reason (cleaner this way instead of trying to cram it into the args)
|
|
||||||
msg.channel.send("Ban reason? `cancel` to cancel");
|
|
||||||
const banReasonReply = await waitForReply(pluginData.client, msg.channel, msg.author.id);
|
|
||||||
if (!banReasonReply || !banReasonReply.content || banReasonReply.content.toLowerCase().trim() === "cancel") {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "Cancelled");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const banReason = formatReasonWithAttachments(banReasonReply.content, [...msg.attachments.values()]);
|
|
||||||
|
|
||||||
// Verify we can act on each of the users specified
|
|
||||||
for (const userId of args.userIds) {
|
|
||||||
const member = pluginData.guild.members.cache.get(userId as Snowflake); // TODO: Get members on demand?
|
|
||||||
if (member && !canActOn(pluginData, msg.member, member)) {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "Cannot massban one or more users: insufficient permissions");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show a loading indicator since this can take a while
|
|
||||||
const maxWaitTime = pluginData.state.massbanQueue.timeout * pluginData.state.massbanQueue.length;
|
|
||||||
const maxWaitTimeFormatted = humanizeDurationShort(maxWaitTime, { round: true });
|
|
||||||
const initialLoadingText =
|
|
||||||
pluginData.state.massbanQueue.length === 0
|
|
||||||
? "Banning..."
|
|
||||||
: `Massban queued. Waiting for previous massban to finish (max wait ${maxWaitTimeFormatted}).`;
|
|
||||||
const loadingMsg = await msg.channel.send(initialLoadingText);
|
|
||||||
|
|
||||||
const waitTimeStart = performance.now();
|
|
||||||
const waitingInterval = setInterval(() => {
|
|
||||||
const waitTime = humanizeDurationShort(performance.now() - waitTimeStart, { round: true });
|
|
||||||
loadingMsg
|
|
||||||
.edit(`Massban queued. Still waiting for previous massban to finish (waited ${waitTime}).`)
|
|
||||||
.catch(() => clearInterval(waitingInterval));
|
|
||||||
}, 1 * MINUTES);
|
|
||||||
|
|
||||||
pluginData.state.massbanQueue.add(async () => {
|
|
||||||
clearInterval(waitingInterval);
|
|
||||||
|
|
||||||
if (pluginData.state.unloaded) {
|
|
||||||
void loadingMsg.delete().catch(noop);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
void loadingMsg.edit("Banning...").catch(noop);
|
|
||||||
|
|
||||||
// Ban each user and count failed bans (if any)
|
|
||||||
const startTime = performance.now();
|
|
||||||
const failedBans: string[] = [];
|
|
||||||
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
|
||||||
const deleteDays = (await pluginData.config.getForMessage(msg)).ban_delete_message_days;
|
|
||||||
for (const [i, userId] of args.userIds.entries()) {
|
|
||||||
if (pluginData.state.unloaded) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Ignore automatic ban cases and logs
|
|
||||||
// We create our own cases below and post a single "mass banned" log instead
|
|
||||||
ignoreEvent(pluginData, IgnoredEventType.Ban, userId, 30 * MINUTES);
|
|
||||||
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_BAN, userId, 30 * MINUTES);
|
|
||||||
|
|
||||||
await pluginData.guild.bans.create(userId as Snowflake, {
|
|
||||||
deleteMessageSeconds: (deleteDays * DAYS) / SECONDS,
|
|
||||||
reason: banReason,
|
|
||||||
});
|
|
||||||
|
|
||||||
await casesPlugin.createCase({
|
|
||||||
userId,
|
|
||||||
modId: msg.author.id,
|
|
||||||
type: CaseTypes.Ban,
|
|
||||||
reason: `Mass ban: ${banReason}`,
|
|
||||||
postInCaseLogOverride: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
pluginData.state.events.emit("ban", userId, banReason);
|
|
||||||
} catch {
|
|
||||||
failedBans.push(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send a status update every 10 bans
|
|
||||||
if ((i + 1) % 10 === 0) {
|
|
||||||
loadingMsg.edit(`Banning... ${i + 1}/${args.userIds.length}`).catch(noop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalTime = performance.now() - startTime;
|
|
||||||
const formattedTimeTaken = humanizeDurationShort(totalTime, { round: true });
|
|
||||||
|
|
||||||
// Clear loading indicator
|
|
||||||
loadingMsg.delete().catch(noop);
|
|
||||||
|
|
||||||
const successfulBanCount = args.userIds.length - failedBans.length;
|
|
||||||
if (successfulBanCount === 0) {
|
|
||||||
// All bans failed - don't create a log entry and notify the user
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "All bans failed. Make sure the IDs are valid.");
|
|
||||||
} else {
|
|
||||||
// Some or all bans were successful. Create a log entry for the mass ban and notify the user.
|
|
||||||
pluginData.getPlugin(LogsPlugin).logMassBan({
|
|
||||||
mod: msg.author,
|
|
||||||
count: successfulBanCount,
|
|
||||||
reason: banReason,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (failedBans.length) {
|
|
||||||
sendSuccessMessage(
|
|
||||||
pluginData,
|
|
||||||
msg.channel,
|
|
||||||
`Banned ${successfulBanCount} users in ${formattedTimeTaken}, ${
|
|
||||||
failedBans.length
|
|
||||||
} failed: ${failedBans.join(" ")}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
sendSuccessMessage(
|
|
||||||
pluginData,
|
|
||||||
msg.channel,
|
|
||||||
`Banned ${successfulBanCount} users successfully in ${formattedTimeTaken}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,125 +0,0 @@
|
||||||
import { Snowflake } from "discord.js";
|
|
||||||
import { waitForReply } from "knub/helpers";
|
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
|
||||||
import { CaseTypes } from "../../../data/CaseTypes";
|
|
||||||
import { LogType } from "../../../data/LogType";
|
|
||||||
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
|
||||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
|
||||||
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments";
|
|
||||||
import { ignoreEvent } from "../functions/ignoreEvent";
|
|
||||||
import { isBanned } from "../functions/isBanned";
|
|
||||||
import { IgnoredEventType, modActionsMsgCmd } from "../types";
|
|
||||||
|
|
||||||
export const MassunbanCmd = modActionsMsgCmd({
|
|
||||||
trigger: "massunban",
|
|
||||||
permission: "can_massunban",
|
|
||||||
description: "Mass-unban a list of user IDs",
|
|
||||||
|
|
||||||
signature: [
|
|
||||||
{
|
|
||||||
userIds: ct.string({ rest: true }),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
async run({ pluginData, message: msg, args }) {
|
|
||||||
// Limit to 100 users at once (arbitrary?)
|
|
||||||
if (args.userIds.length > 100) {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, `Can only mass-unban max 100 users at once`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ask for unban reason (cleaner this way instead of trying to cram it into the args)
|
|
||||||
msg.channel.send("Unban reason? `cancel` to cancel");
|
|
||||||
const unbanReasonReply = await waitForReply(pluginData.client, msg.channel, msg.author.id);
|
|
||||||
if (!unbanReasonReply || !unbanReasonReply.content || unbanReasonReply.content.toLowerCase().trim() === "cancel") {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "Cancelled");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const unbanReason = formatReasonWithAttachments(unbanReasonReply.content, [...msg.attachments.values()]);
|
|
||||||
|
|
||||||
// Ignore automatic unban cases and logs for these users
|
|
||||||
// We'll create our own cases below and post a single "mass unbanned" log instead
|
|
||||||
args.userIds.forEach((userId) => {
|
|
||||||
// Use longer timeouts since this can take a while
|
|
||||||
ignoreEvent(pluginData, IgnoredEventType.Unban, userId, 120 * 1000);
|
|
||||||
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, userId, 120 * 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show a loading indicator since this can take a while
|
|
||||||
const loadingMsg = await msg.channel.send("Unbanning...");
|
|
||||||
|
|
||||||
// Unban each user and count failed unbans (if any)
|
|
||||||
const failedUnbans: Array<{ userId: string; reason: UnbanFailReasons }> = [];
|
|
||||||
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
|
||||||
for (const userId of args.userIds) {
|
|
||||||
if (!(await isBanned(pluginData, userId))) {
|
|
||||||
failedUnbans.push({ userId, reason: UnbanFailReasons.NOT_BANNED });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await pluginData.guild.bans.remove(userId as Snowflake, unbanReason ?? undefined);
|
|
||||||
|
|
||||||
await casesPlugin.createCase({
|
|
||||||
userId,
|
|
||||||
modId: msg.author.id,
|
|
||||||
type: CaseTypes.Unban,
|
|
||||||
reason: `Mass unban: ${unbanReason}`,
|
|
||||||
postInCaseLogOverride: false,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
failedUnbans.push({ userId, reason: UnbanFailReasons.UNBAN_FAILED });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear loading indicator
|
|
||||||
loadingMsg.delete();
|
|
||||||
|
|
||||||
const successfulUnbanCount = args.userIds.length - failedUnbans.length;
|
|
||||||
if (successfulUnbanCount === 0) {
|
|
||||||
// All unbans failed - don't create a log entry and notify the user
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "All unbans failed. Make sure the IDs are valid and banned.");
|
|
||||||
} else {
|
|
||||||
// Some or all unbans were successful. Create a log entry for the mass unban and notify the user.
|
|
||||||
pluginData.getPlugin(LogsPlugin).logMassUnban({
|
|
||||||
mod: msg.author,
|
|
||||||
count: successfulUnbanCount,
|
|
||||||
reason: unbanReason,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (failedUnbans.length) {
|
|
||||||
const notBanned = failedUnbans.filter((x) => x.reason === UnbanFailReasons.NOT_BANNED);
|
|
||||||
const unbanFailed = failedUnbans.filter((x) => x.reason === UnbanFailReasons.UNBAN_FAILED);
|
|
||||||
|
|
||||||
let failedMsg = "";
|
|
||||||
if (notBanned.length > 0) {
|
|
||||||
failedMsg += `${notBanned.length}x ${UnbanFailReasons.NOT_BANNED}:`;
|
|
||||||
notBanned.forEach((fail) => {
|
|
||||||
failedMsg += " " + fail.userId;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (unbanFailed.length > 0) {
|
|
||||||
failedMsg += `\n${unbanFailed.length}x ${UnbanFailReasons.UNBAN_FAILED}:`;
|
|
||||||
unbanFailed.forEach((fail) => {
|
|
||||||
failedMsg += " " + fail.userId;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sendSuccessMessage(
|
|
||||||
pluginData,
|
|
||||||
msg.channel,
|
|
||||||
`Unbanned ${successfulUnbanCount} users, ${failedUnbans.length} failed:\n${failedMsg}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
sendSuccessMessage(pluginData, msg.channel, `Unbanned ${successfulUnbanCount} users successfully`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
enum UnbanFailReasons {
|
|
||||||
NOT_BANNED = "Not banned",
|
|
||||||
UNBAN_FAILED = "Unban failed",
|
|
||||||
}
|
|
|
@ -1,105 +0,0 @@
|
||||||
import { Snowflake } from "discord.js";
|
|
||||||
import { waitForReply } from "knub/helpers";
|
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
|
||||||
import { LogType } from "../../../data/LogType";
|
|
||||||
import { logger } from "../../../logger";
|
|
||||||
import { canActOn, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
|
||||||
import { MutesPlugin } from "../../../plugins/Mutes/MutesPlugin";
|
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
|
||||||
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments";
|
|
||||||
import { modActionsMsgCmd } from "../types";
|
|
||||||
|
|
||||||
export const MassmuteCmd = modActionsMsgCmd({
|
|
||||||
trigger: "massmute",
|
|
||||||
permission: "can_massmute",
|
|
||||||
description: "Mass-mute a list of user IDs",
|
|
||||||
|
|
||||||
signature: [
|
|
||||||
{
|
|
||||||
userIds: ct.string({ rest: true }),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
async run({ pluginData, message: msg, args }) {
|
|
||||||
// Limit to 100 users at once (arbitrary?)
|
|
||||||
if (args.userIds.length > 100) {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, `Can only massmute max 100 users at once`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ask for mute reason
|
|
||||||
msg.channel.send("Mute reason? `cancel` to cancel");
|
|
||||||
const muteReasonReceived = await waitForReply(pluginData.client, msg.channel, msg.author.id);
|
|
||||||
if (
|
|
||||||
!muteReasonReceived ||
|
|
||||||
!muteReasonReceived.content ||
|
|
||||||
muteReasonReceived.content.toLowerCase().trim() === "cancel"
|
|
||||||
) {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "Cancelled");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const muteReason = formatReasonWithAttachments(muteReasonReceived.content, [...msg.attachments.values()]);
|
|
||||||
|
|
||||||
// Verify we can act upon all users
|
|
||||||
for (const userId of args.userIds) {
|
|
||||||
const member = pluginData.guild.members.cache.get(userId as Snowflake);
|
|
||||||
if (member && !canActOn(pluginData, msg.member, member)) {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "Cannot massmute one or more users: insufficient permissions");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore automatic mute cases and logs for these users
|
|
||||||
// We'll create our own cases below and post a single "mass muted" log instead
|
|
||||||
args.userIds.forEach((userId) => {
|
|
||||||
// Use longer timeouts since this can take a while
|
|
||||||
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_MUTE, userId, 120 * 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show loading indicator
|
|
||||||
const loadingMsg = await msg.channel.send("Muting...");
|
|
||||||
|
|
||||||
// Mute everyone and count fails
|
|
||||||
const modId = msg.author.id;
|
|
||||||
const failedMutes: string[] = [];
|
|
||||||
const mutesPlugin = pluginData.getPlugin(MutesPlugin);
|
|
||||||
for (const userId of args.userIds) {
|
|
||||||
try {
|
|
||||||
await mutesPlugin.muteUser(userId, 0, `Mass mute: ${muteReason}`, {
|
|
||||||
caseArgs: {
|
|
||||||
modId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.info(e);
|
|
||||||
failedMutes.push(userId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear loading indicator
|
|
||||||
loadingMsg.delete();
|
|
||||||
|
|
||||||
const successfulMuteCount = args.userIds.length - failedMutes.length;
|
|
||||||
if (successfulMuteCount === 0) {
|
|
||||||
// All mutes failed
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "All mutes failed. Make sure the IDs are valid.");
|
|
||||||
} else {
|
|
||||||
// Success on all or some mutes
|
|
||||||
pluginData.getPlugin(LogsPlugin).logMassMute({
|
|
||||||
mod: msg.author,
|
|
||||||
count: successfulMuteCount,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (failedMutes.length) {
|
|
||||||
sendSuccessMessage(
|
|
||||||
pluginData,
|
|
||||||
msg.channel,
|
|
||||||
`Muted ${successfulMuteCount} users, ${failedMutes.length} failed: ${failedMutes.join(" ")}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
sendSuccessMessage(pluginData, msg.channel, `Muted ${successfulMuteCount} users successfully`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
|
||||||
import { trimPluginDescription } from "../../../utils";
|
|
||||||
import { actualKickMemberCmd } from "../functions/actualKickMemberCmd";
|
|
||||||
import { modActionsMsgCmd } from "../types";
|
|
||||||
|
|
||||||
const opts = {
|
|
||||||
mod: ct.member({ option: true }),
|
|
||||||
notify: ct.string({ option: true }),
|
|
||||||
"notify-channel": ct.textChannel({ option: true }),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SoftbanCmd = modActionsMsgCmd({
|
|
||||||
trigger: "softban",
|
|
||||||
permission: "can_kick",
|
|
||||||
description: trimPluginDescription(`
|
|
||||||
"Softban" the specified user by banning and immediately unbanning them. Effectively a kick with message deletions.
|
|
||||||
This command will be removed in the future, please use kick with the \`- clean\` argument instead
|
|
||||||
`),
|
|
||||||
|
|
||||||
signature: [
|
|
||||||
{
|
|
||||||
user: ct.string(),
|
|
||||||
reason: ct.string({ required: false, catchAll: true }),
|
|
||||||
|
|
||||||
...opts,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
async run({ pluginData, message: msg, args }) {
|
|
||||||
await actualKickMemberCmd(pluginData, msg, { clean: true, ...args });
|
|
||||||
await msg.channel.send(
|
|
||||||
"Softban will be removed in the future - please use the kick command with the `-clean` argument instead!",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,90 +0,0 @@
|
||||||
import { Snowflake } from "discord.js";
|
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
|
||||||
import { CaseTypes } from "../../../data/CaseTypes";
|
|
||||||
import { LogType } from "../../../data/LogType";
|
|
||||||
import { clearExpiringTempban } from "../../../data/loops/expiringTempbansLoop";
|
|
||||||
import { hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
|
||||||
import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin";
|
|
||||||
import { resolveUser } from "../../../utils";
|
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
|
||||||
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments";
|
|
||||||
import { ignoreEvent } from "../functions/ignoreEvent";
|
|
||||||
import { IgnoredEventType, modActionsMsgCmd } from "../types";
|
|
||||||
|
|
||||||
const opts = {
|
|
||||||
mod: ct.member({ option: true }),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UnbanCmd = modActionsMsgCmd({
|
|
||||||
trigger: "unban",
|
|
||||||
permission: "can_unban",
|
|
||||||
description: "Unban the specified member",
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, user.id);
|
|
||||||
const reason = formatReasonWithAttachments(args.reason, [...msg.attachments.values()]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
ignoreEvent(pluginData, IgnoredEventType.Unban, user.id);
|
|
||||||
await pluginData.guild.bans.remove(user.id as Snowflake, reason ?? undefined);
|
|
||||||
} catch {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "Failed to unban member; are you sure they're banned?");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a case
|
|
||||||
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
|
||||||
const createdCase = await casesPlugin.createCase({
|
|
||||||
userId: user.id,
|
|
||||||
modId: mod.id,
|
|
||||||
type: CaseTypes.Unban,
|
|
||||||
reason,
|
|
||||||
ppId: mod.id !== msg.author.id ? msg.author.id : undefined,
|
|
||||||
});
|
|
||||||
// Delete the tempban, if one exists
|
|
||||||
const tempban = await pluginData.state.tempbans.findExistingTempbanForUserId(user.id);
|
|
||||||
if (tempban) {
|
|
||||||
clearExpiringTempban(tempban);
|
|
||||||
await pluginData.state.tempbans.clear(user.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm the action
|
|
||||||
sendSuccessMessage(pluginData, msg.channel, `Member unbanned (Case #${createdCase.case_number})`);
|
|
||||||
|
|
||||||
// Log the action
|
|
||||||
pluginData.getPlugin(LogsPlugin).logMemberUnban({
|
|
||||||
mod: mod.user,
|
|
||||||
userId: user.id,
|
|
||||||
caseNumber: createdCase.case_number,
|
|
||||||
reason: reason ?? "",
|
|
||||||
});
|
|
||||||
|
|
||||||
pluginData.state.events.emit("unban", user.id);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,45 +0,0 @@
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
|
||||||
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
|
||||||
import { modActionsMsgCmd } from "../types";
|
|
||||||
|
|
||||||
export const UnhideCaseCmd = modActionsMsgCmd({
|
|
||||||
trigger: ["unhide", "unhidecase", "unhide_case"],
|
|
||||||
permission: "can_hidecase",
|
|
||||||
description: "Un-hide the specified case, making it appear in !cases and !info again",
|
|
||||||
|
|
||||||
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, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
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"} no longer hidden!${failedAddendum}`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,112 +0,0 @@
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
|
||||||
import { CaseTypes } from "../../../data/CaseTypes";
|
|
||||||
import { canActOn, hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
|
||||||
import { errorMessage, renderUserUsername, resolveMember, resolveUser } from "../../../utils";
|
|
||||||
import { waitForButtonConfirm } from "../../../utils/waitForInteraction";
|
|
||||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
|
||||||
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments";
|
|
||||||
import { isBanned } from "../functions/isBanned";
|
|
||||||
import { readContactMethodsFromArgs } from "../functions/readContactMethodsFromArgs";
|
|
||||||
import { warnMember } from "../functions/warnMember";
|
|
||||||
import { modActionsMsgCmd } from "../types";
|
|
||||||
|
|
||||||
export const WarnCmd = modActionsMsgCmd({
|
|
||||||
trigger: "warn",
|
|
||||||
permission: "can_warn",
|
|
||||||
description: "Send a warning to the specified user",
|
|
||||||
|
|
||||||
signature: {
|
|
||||||
user: ct.string(),
|
|
||||||
reason: ct.string({ catchAll: true }),
|
|
||||||
|
|
||||||
mod: ct.member({ option: true }),
|
|
||||||
notify: ct.string({ option: true }),
|
|
||||||
"notify-channel": ct.textChannel({ option: true }),
|
|
||||||
},
|
|
||||||
|
|
||||||
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 memberToWarn = await resolveMember(pluginData.client, pluginData.guild, user.id);
|
|
||||||
|
|
||||||
if (!memberToWarn) {
|
|
||||||
const _isBanned = await isBanned(pluginData, user.id);
|
|
||||||
if (_isBanned) {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, `User is banned`);
|
|
||||||
} else {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, `User not found on the server`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure we're allowed to warn this member
|
|
||||||
if (!canActOn(pluginData, msg.member, memberToWarn)) {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "Cannot warn: 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 }))) {
|
|
||||||
msg.channel.send(errorMessage("You don't have permission to use -mod"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mod = args.mod;
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = pluginData.config.get();
|
|
||||||
const reason = formatReasonWithAttachments(args.reason, [...msg.attachments.values()]);
|
|
||||||
|
|
||||||
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
|
||||||
const priorWarnAmount = await casesPlugin.getCaseTypeAmountForUserId(memberToWarn.id, CaseTypes.Warn);
|
|
||||||
if (config.warn_notify_enabled && priorWarnAmount >= config.warn_notify_threshold) {
|
|
||||||
const reply = await waitForButtonConfirm(
|
|
||||||
msg.channel,
|
|
||||||
{ content: config.warn_notify_message.replace("{priorWarnings}", `${priorWarnAmount}`) },
|
|
||||||
{ confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id },
|
|
||||||
);
|
|
||||||
if (!reply) {
|
|
||||||
msg.channel.send(errorMessage("Warn cancelled by moderator"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let contactMethods;
|
|
||||||
try {
|
|
||||||
contactMethods = readContactMethodsFromArgs(args);
|
|
||||||
} catch (e) {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, e.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const warnResult = await warnMember(pluginData, memberToWarn, reason, {
|
|
||||||
contactMethods,
|
|
||||||
caseArgs: {
|
|
||||||
modId: mod.id,
|
|
||||||
ppId: mod.id !== msg.author.id ? msg.author.id : undefined,
|
|
||||||
reason,
|
|
||||||
},
|
|
||||||
retryPromptChannel: msg.channel,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (warnResult.status === "failed") {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "Failed to warn user");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageResultText = warnResult.notifyResult.text ? ` (${warnResult.notifyResult.text})` : "";
|
|
||||||
|
|
||||||
sendSuccessMessage(
|
|
||||||
pluginData,
|
|
||||||
msg.channel,
|
|
||||||
`Warned **${renderUserUsername(memberToWarn.user)}** (Case #${warnResult.case.case_number})${messageResultText}`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
|
import { CaseTypes } from "../../../../data/CaseTypes";
|
||||||
|
import { hasPermission, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import { resolveUser } from "../../../../utils";
|
||||||
|
import { actualAddCaseCmd } from "../../functions/actualCommands/actualAddCaseCmd";
|
||||||
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
mod: ct.member({ option: true }),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddCaseMsgCmd = modActionsMsgCmd({
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualAddCaseCmd(
|
||||||
|
pluginData,
|
||||||
|
msg.channel,
|
||||||
|
msg.member,
|
||||||
|
mod,
|
||||||
|
[...msg.attachments.values()],
|
||||||
|
user,
|
||||||
|
type as keyof CaseTypes,
|
||||||
|
args.reason || "",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { slashOptions } from "knub";
|
||||||
|
import { CaseTypes } from "../../../../data/CaseTypes";
|
||||||
|
import { hasPermission, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import { generateAttachmentSlashOptions, retrieveMultipleOptions } from "../../../../utils/multipleSlashOptions";
|
||||||
|
import { actualAddCaseCmd } from "../../functions/actualCommands/actualAddCaseCmd";
|
||||||
|
import { NUMBER_ATTACHMENTS_CASE_CREATION } from "../constants";
|
||||||
|
|
||||||
|
const opts = [
|
||||||
|
slashOptions.string({ name: "reason", description: "The reason", required: false }),
|
||||||
|
slashOptions.user({ name: "mod", description: "The moderator to add this case as", required: false }),
|
||||||
|
...generateAttachmentSlashOptions(NUMBER_ATTACHMENTS_CASE_CREATION, {
|
||||||
|
name: "attachment",
|
||||||
|
description: "An attachment to add to the reason of the case",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AddCaseSlashCmd = {
|
||||||
|
name: "addcase",
|
||||||
|
configPermission: "can_addcase",
|
||||||
|
description: "Add an arbitrary case to the specified user without taking any action",
|
||||||
|
allowDms: false,
|
||||||
|
|
||||||
|
signature: [
|
||||||
|
slashOptions.string({
|
||||||
|
name: "type",
|
||||||
|
description: "The type of case to add",
|
||||||
|
required: true,
|
||||||
|
choices: Object.keys(CaseTypes).map((type) => ({ name: type, value: type })),
|
||||||
|
}),
|
||||||
|
slashOptions.user({ name: "user", description: "The user to add a case to", required: true }),
|
||||||
|
|
||||||
|
...opts,
|
||||||
|
],
|
||||||
|
|
||||||
|
async run({ interaction, options, pluginData }) {
|
||||||
|
const attachments = retrieveMultipleOptions(NUMBER_ATTACHMENTS_CASE_CREATION, options, "attachment");
|
||||||
|
|
||||||
|
// The moderator who did the action is the message author or, if used, the specified -mod
|
||||||
|
let mod = interaction.member;
|
||||||
|
const canActAsOther = await hasPermission(pluginData, "can_act_as_other", {
|
||||||
|
channel: interaction.channel,
|
||||||
|
member: interaction.member,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.mod) {
|
||||||
|
if (!canActAsOther) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "You don't have permission to act as another moderator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod = options.mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualAddCaseCmd(
|
||||||
|
pluginData,
|
||||||
|
interaction,
|
||||||
|
interaction.member,
|
||||||
|
mod,
|
||||||
|
attachments,
|
||||||
|
options.user,
|
||||||
|
options.type as keyof CaseTypes,
|
||||||
|
options.reason || "",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
75
backend/src/plugins/ModActions/commands/ban/BanMsgCmd.ts
Normal file
75
backend/src/plugins/ModActions/commands/ban/BanMsgCmd.ts
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
|
import { hasPermission, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import { UserNotificationMethod, resolveUser } from "../../../../utils";
|
||||||
|
import { actualBanCmd } from "../../functions/actualCommands/actualBanCmd";
|
||||||
|
import { readContactMethodsFromArgs } from "../../functions/readContactMethodsFromArgs";
|
||||||
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
|
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 BanMsgCmd = modActionsMsgCmd({
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contactMethods: UserNotificationMethod[] | undefined;
|
||||||
|
try {
|
||||||
|
contactMethods = readContactMethodsFromArgs(args) ?? undefined;
|
||||||
|
} catch (e) {
|
||||||
|
sendErrorMessage(pluginData, msg.channel, e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualBanCmd(
|
||||||
|
pluginData,
|
||||||
|
msg.channel,
|
||||||
|
user,
|
||||||
|
args["time"] ? args["time"] : null,
|
||||||
|
args.reason || "",
|
||||||
|
[...msg.attachments.values()],
|
||||||
|
msg.member,
|
||||||
|
mod,
|
||||||
|
contactMethods,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
98
backend/src/plugins/ModActions/commands/ban/BanSlashCmd.ts
Normal file
98
backend/src/plugins/ModActions/commands/ban/BanSlashCmd.ts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import { ChannelType } from "discord.js";
|
||||||
|
import { slashOptions } from "knub";
|
||||||
|
import { hasPermission, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import { UserNotificationMethod, convertDelayStringToMS } from "../../../../utils";
|
||||||
|
import { generateAttachmentSlashOptions, retrieveMultipleOptions } from "../../../../utils/multipleSlashOptions";
|
||||||
|
import { actualBanCmd } from "../../functions/actualCommands/actualBanCmd";
|
||||||
|
import { readContactMethodsFromArgs } from "../../functions/readContactMethodsFromArgs";
|
||||||
|
import { NUMBER_ATTACHMENTS_CASE_CREATION } from "../constants";
|
||||||
|
|
||||||
|
const opts = [
|
||||||
|
slashOptions.string({ name: "time", description: "The duration of the ban", required: false }),
|
||||||
|
slashOptions.string({ name: "reason", description: "The reason", required: false }),
|
||||||
|
slashOptions.user({ name: "mod", description: "The moderator to ban as", required: false }),
|
||||||
|
slashOptions.string({
|
||||||
|
name: "notify",
|
||||||
|
description: "How to notify",
|
||||||
|
required: false,
|
||||||
|
choices: [
|
||||||
|
{ name: "DM", value: "dm" },
|
||||||
|
{ name: "Channel", value: "channel" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
slashOptions.channel({
|
||||||
|
name: "notify-channel",
|
||||||
|
description: "The channel to notify in",
|
||||||
|
channelTypes: [ChannelType.GuildText, ChannelType.PrivateThread, ChannelType.PublicThread],
|
||||||
|
required: false,
|
||||||
|
}),
|
||||||
|
slashOptions.number({
|
||||||
|
name: "delete-days",
|
||||||
|
description: "The number of days of messages to delete",
|
||||||
|
required: false,
|
||||||
|
}),
|
||||||
|
...generateAttachmentSlashOptions(NUMBER_ATTACHMENTS_CASE_CREATION, {
|
||||||
|
name: "attachment",
|
||||||
|
description: "An attachment to add to the reason of the ban",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const BanSlashCmd = {
|
||||||
|
name: "ban",
|
||||||
|
configPermission: "can_ban",
|
||||||
|
description: "Ban or Tempban the specified member",
|
||||||
|
allowDms: false,
|
||||||
|
|
||||||
|
signature: [slashOptions.user({ name: "user", description: "The user to ban", required: true }), ...opts],
|
||||||
|
|
||||||
|
async run({ interaction, options, pluginData }) {
|
||||||
|
const attachments = retrieveMultipleOptions(NUMBER_ATTACHMENTS_CASE_CREATION, options, "attachment");
|
||||||
|
|
||||||
|
if ((!options.reason || options.reason.trim() === "") && attachments.length < 1) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "Text or attachment required", undefined, undefined, true);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mod = interaction.member;
|
||||||
|
const canActAsOther = await hasPermission(pluginData, "can_act_as_other", {
|
||||||
|
channel: interaction.channel,
|
||||||
|
member: interaction.member,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.mod) {
|
||||||
|
if (!canActAsOther) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "You don't have permission to act as another moderator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod = options.mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contactMethods: UserNotificationMethod[] | undefined;
|
||||||
|
try {
|
||||||
|
contactMethods = readContactMethodsFromArgs(options) ?? undefined;
|
||||||
|
} catch (e) {
|
||||||
|
sendErrorMessage(pluginData, interaction, e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const convertedTime = options.time ? convertDelayStringToMS(options.time) : null;
|
||||||
|
if (options.time && !convertedTime) {
|
||||||
|
sendErrorMessage(pluginData, interaction, `Could not convert ${options.time} to a delay`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualBanCmd(
|
||||||
|
pluginData,
|
||||||
|
interaction,
|
||||||
|
options.user,
|
||||||
|
convertedTime,
|
||||||
|
options.reason || "",
|
||||||
|
attachments,
|
||||||
|
interaction.member,
|
||||||
|
mod,
|
||||||
|
contactMethods,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
19
backend/src/plugins/ModActions/commands/case/CaseMsgCmd.ts
Normal file
19
backend/src/plugins/ModActions/commands/case/CaseMsgCmd.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
|
import { actualCaseCmd } from "../../functions/actualCommands/actualCaseCmd";
|
||||||
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
|
export const CaseMsgCmd = modActionsMsgCmd({
|
||||||
|
trigger: "case",
|
||||||
|
permission: "can_view",
|
||||||
|
description: "Show information about a specific case",
|
||||||
|
|
||||||
|
signature: [
|
||||||
|
{
|
||||||
|
caseNumber: ct.number(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run({ pluginData, message: msg, args }) {
|
||||||
|
actualCaseCmd(pluginData, msg.channel, msg.author.id, args.caseNumber);
|
||||||
|
},
|
||||||
|
});
|
17
backend/src/plugins/ModActions/commands/case/CaseSlashCmd.ts
Normal file
17
backend/src/plugins/ModActions/commands/case/CaseSlashCmd.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { slashOptions } from "knub";
|
||||||
|
import { actualCaseCmd } from "../../functions/actualCommands/actualCaseCmd";
|
||||||
|
|
||||||
|
export const CaseSlashCmd = {
|
||||||
|
name: "case",
|
||||||
|
configPermission: "can_view",
|
||||||
|
description: "Show information about a specific case",
|
||||||
|
allowDms: false,
|
||||||
|
|
||||||
|
signature: [
|
||||||
|
slashOptions.number({ name: "case-number", description: "The number of the case to show", required: true }),
|
||||||
|
],
|
||||||
|
|
||||||
|
async run({ interaction, options, pluginData }) {
|
||||||
|
actualCaseCmd(pluginData, interaction, interaction.user.id, options["case-number"]);
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
|
import { actualCasesCmd } from "../../functions/actualCommands/actualCasesCmd";
|
||||||
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
mod: ct.userId({ option: true }),
|
||||||
|
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" }),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CasesModMsgCmd = modActionsMsgCmd({
|
||||||
|
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 }) {
|
||||||
|
return actualCasesCmd(
|
||||||
|
pluginData,
|
||||||
|
msg.channel,
|
||||||
|
args.mod,
|
||||||
|
null,
|
||||||
|
msg.author,
|
||||||
|
args.notes,
|
||||||
|
args.warns,
|
||||||
|
args.mutes,
|
||||||
|
args.unmutes,
|
||||||
|
args.bans,
|
||||||
|
args.unbans,
|
||||||
|
args.reverseFilters,
|
||||||
|
args.hidden,
|
||||||
|
args.expand,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { slashOptions } from "knub";
|
||||||
|
import { actualCasesCmd } from "../../functions/actualCommands/actualCasesCmd";
|
||||||
|
|
||||||
|
const opts = [
|
||||||
|
slashOptions.user({ name: "user", description: "The user to show cases for", required: false }),
|
||||||
|
slashOptions.user({ name: "mod", description: "The mod to filter cases by", required: false }),
|
||||||
|
slashOptions.boolean({ name: "expand", description: "Show each case individually", required: false }),
|
||||||
|
slashOptions.boolean({ name: "hidden", description: "Whether or not to show hidden cases", required: false }),
|
||||||
|
slashOptions.boolean({
|
||||||
|
name: "reverse-filters",
|
||||||
|
description: "To treat case type filters as exclusive instead of inclusive",
|
||||||
|
required: false,
|
||||||
|
}),
|
||||||
|
slashOptions.boolean({ name: "notes", description: "To filter notes", required: false }),
|
||||||
|
slashOptions.boolean({ name: "warns", description: "To filter warns", required: false }),
|
||||||
|
slashOptions.boolean({ name: "mutes", description: "To filter mutes", required: false }),
|
||||||
|
slashOptions.boolean({ name: "unmutes", description: "To filter unmutes", required: false }),
|
||||||
|
slashOptions.boolean({ name: "bans", description: "To filter bans", required: false }),
|
||||||
|
slashOptions.boolean({ name: "unbans", description: "To filter unbans", required: false }),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CasesSlashCmd = {
|
||||||
|
name: "cases",
|
||||||
|
configPermission: "can_view",
|
||||||
|
description: "Show a list of cases the specified user has or the specified mod made",
|
||||||
|
allowDms: false,
|
||||||
|
|
||||||
|
signature: [...opts],
|
||||||
|
|
||||||
|
async run({ interaction, options, pluginData }) {
|
||||||
|
return actualCasesCmd(
|
||||||
|
pluginData,
|
||||||
|
interaction,
|
||||||
|
options.mod,
|
||||||
|
options.user,
|
||||||
|
interaction.user,
|
||||||
|
options.notes,
|
||||||
|
options.warns,
|
||||||
|
options.mutes,
|
||||||
|
options.unmutes,
|
||||||
|
options.bans,
|
||||||
|
options.unbans,
|
||||||
|
options["reverse-filters"],
|
||||||
|
options.hidden,
|
||||||
|
options.expand,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
|
import { sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import { resolveUser } from "../../../../utils";
|
||||||
|
import { actualCasesCmd } from "../../functions/actualCommands/actualCasesCmd";
|
||||||
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
mod: ct.userId({ option: true }),
|
||||||
|
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" }),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CasesUserMsgCmd = modActionsMsgCmd({
|
||||||
|
trigger: ["cases", "modlogs", "infractions"],
|
||||||
|
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 resolveUser(pluginData.client, args.user);
|
||||||
|
if (!user.id) {
|
||||||
|
sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return actualCasesCmd(
|
||||||
|
pluginData,
|
||||||
|
msg.channel,
|
||||||
|
args.mod,
|
||||||
|
user,
|
||||||
|
msg.author,
|
||||||
|
args.notes,
|
||||||
|
args.warns,
|
||||||
|
args.mutes,
|
||||||
|
args.unmutes,
|
||||||
|
args.bans,
|
||||||
|
args.unbans,
|
||||||
|
args.reverseFilters,
|
||||||
|
args.hidden,
|
||||||
|
args.expand,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
2
backend/src/plugins/ModActions/commands/constants.ts
Normal file
2
backend/src/plugins/ModActions/commands/constants.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export const NUMBER_ATTACHMENTS_CASE_CREATION = 1;
|
||||||
|
export const NUMBER_ATTACHMENTS_CASE_UPDATE = 3;
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
|
import { trimLines } from "../../../../utils";
|
||||||
|
import { actualDeleteCaseCmd } from "../../functions/actualCommands/actualDeleteCaseCmd";
|
||||||
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
|
export const DeleteCaseMsgCmd = modActionsMsgCmd({
|
||||||
|
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 }) {
|
||||||
|
actualDeleteCaseCmd(pluginData, message.channel, message.member, args.caseNumber, args.force);
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { slashOptions } from "knub";
|
||||||
|
import { actualDeleteCaseCmd } from "../../functions/actualCommands/actualDeleteCaseCmd";
|
||||||
|
|
||||||
|
const opts = [slashOptions.boolean({ name: "force", description: "Whether or not to force delete", required: false })];
|
||||||
|
|
||||||
|
export const DeleteCaseSlashCmd = {
|
||||||
|
name: "deletecase",
|
||||||
|
configPermission: "can_deletecase",
|
||||||
|
description: "Delete the specified case. This operation can *not* be reversed.",
|
||||||
|
allowDms: false,
|
||||||
|
|
||||||
|
signature: [
|
||||||
|
slashOptions.string({ name: "case-number", description: "The number of the case to delete", required: true }),
|
||||||
|
|
||||||
|
...opts,
|
||||||
|
],
|
||||||
|
|
||||||
|
async run({ interaction, options, pluginData }) {
|
||||||
|
actualDeleteCaseCmd(
|
||||||
|
pluginData,
|
||||||
|
interaction,
|
||||||
|
interaction.member,
|
||||||
|
options["case-number"].split(/[\s,]+/),
|
||||||
|
!!options.force,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
|
import { canActOn, hasPermission, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import { resolveMember, resolveUser } from "../../../../utils";
|
||||||
|
import { actualForceBanCmd } from "../../functions/actualCommands/actualForceBanCmd";
|
||||||
|
import { isBanned } from "../../functions/isBanned";
|
||||||
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
mod: ct.member({ option: true }),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ForceBanMsgCmd = modActionsMsgCmd({
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualForceBanCmd(pluginData, msg.channel, msg.author.id, user, args.reason, [...msg.attachments.values()], mod);
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { slashOptions } from "knub";
|
||||||
|
import { hasPermission, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import { convertDelayStringToMS } from "../../../../utils";
|
||||||
|
import { generateAttachmentSlashOptions, retrieveMultipleOptions } from "../../../../utils/multipleSlashOptions";
|
||||||
|
import { actualForceBanCmd } from "../../functions/actualCommands/actualForceBanCmd";
|
||||||
|
import { NUMBER_ATTACHMENTS_CASE_CREATION } from "../constants";
|
||||||
|
|
||||||
|
const opts = [
|
||||||
|
slashOptions.string({ name: "reason", description: "The reason", required: false }),
|
||||||
|
slashOptions.user({ name: "mod", description: "The moderator to ban as", required: false }),
|
||||||
|
...generateAttachmentSlashOptions(NUMBER_ATTACHMENTS_CASE_CREATION, {
|
||||||
|
name: "attachment",
|
||||||
|
description: "An attachment to add to the reason of the ban",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ForceBanSlashCmd = {
|
||||||
|
name: "forceban",
|
||||||
|
configPermission: "can_ban",
|
||||||
|
description: "Force-ban the specified user, even if they aren't on the server",
|
||||||
|
allowDms: false,
|
||||||
|
|
||||||
|
signature: [slashOptions.user({ name: "user", description: "The user to ban", required: true }), ...opts],
|
||||||
|
|
||||||
|
async run({ interaction, options, pluginData }) {
|
||||||
|
const attachments = retrieveMultipleOptions(NUMBER_ATTACHMENTS_CASE_CREATION, options, "attachment");
|
||||||
|
|
||||||
|
if ((!options.reason || options.reason.trim() === "") && attachments.length < 1) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "Text or attachment required", undefined, undefined, true);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mod = interaction.member;
|
||||||
|
const canActAsOther = await hasPermission(pluginData, "can_act_as_other", {
|
||||||
|
channel: interaction.channel,
|
||||||
|
member: interaction.member,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.mod) {
|
||||||
|
if (!canActAsOther) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "You don't have permission to act as another moderator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod = options.mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
const convertedTime = options.time ? convertDelayStringToMS(options.time) : null;
|
||||||
|
if (options.time && !convertedTime) {
|
||||||
|
sendErrorMessage(pluginData, interaction, `Could not convert ${options.time} to a delay`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualForceBanCmd(pluginData, interaction, interaction.user.id, options.user, options.reason, attachments, mod);
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
|
import { canActOn, hasPermission, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import { resolveMember, resolveUser } from "../../../../utils";
|
||||||
|
import { actualMuteCmd } from "../../functions/actualCommands/actualMuteCmd";
|
||||||
|
import { readContactMethodsFromArgs } from "../../functions/readContactMethodsFromArgs";
|
||||||
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
mod: ct.member({ option: true }),
|
||||||
|
notify: ct.string({ option: true }),
|
||||||
|
"notify-channel": ct.textChannel({ option: true }),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ForceMuteMsgCmd = modActionsMsgCmd({
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The moderator who did the action is the message author or, if used, the specified -mod
|
||||||
|
let mod = msg.member;
|
||||||
|
let ppId: string | undefined;
|
||||||
|
|
||||||
|
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;
|
||||||
|
ppId = msg.author.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contactMethods;
|
||||||
|
try {
|
||||||
|
contactMethods = readContactMethodsFromArgs(args);
|
||||||
|
} catch (e) {
|
||||||
|
sendErrorMessage(pluginData, msg.channel, e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualMuteCmd(
|
||||||
|
pluginData,
|
||||||
|
msg.channel,
|
||||||
|
user,
|
||||||
|
[...msg.attachments.values()],
|
||||||
|
mod,
|
||||||
|
ppId,
|
||||||
|
"time" in args ? args.time ?? undefined : undefined,
|
||||||
|
args.reason,
|
||||||
|
contactMethods,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { ChannelType } from "discord.js";
|
||||||
|
import { slashOptions } from "knub";
|
||||||
|
import { hasPermission, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import { UserNotificationMethod, convertDelayStringToMS } from "../../../../utils";
|
||||||
|
import { generateAttachmentSlashOptions, retrieveMultipleOptions } from "../../../../utils/multipleSlashOptions";
|
||||||
|
import { actualMuteCmd } from "../../functions/actualCommands/actualMuteCmd";
|
||||||
|
import { readContactMethodsFromArgs } from "../../functions/readContactMethodsFromArgs";
|
||||||
|
import { NUMBER_ATTACHMENTS_CASE_CREATION } from "../constants";
|
||||||
|
|
||||||
|
const opts = [
|
||||||
|
slashOptions.string({ name: "time", description: "The duration of the mute", required: false }),
|
||||||
|
slashOptions.string({ name: "reason", description: "The reason", required: false }),
|
||||||
|
slashOptions.user({ name: "mod", description: "The moderator to mute as", required: false }),
|
||||||
|
slashOptions.string({
|
||||||
|
name: "notify",
|
||||||
|
description: "How to notify",
|
||||||
|
required: false,
|
||||||
|
choices: [
|
||||||
|
{ name: "DM", value: "dm" },
|
||||||
|
{ name: "Channel", value: "channel" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
slashOptions.channel({
|
||||||
|
name: "notify-channel",
|
||||||
|
description: "The channel to notify in",
|
||||||
|
channelTypes: [ChannelType.GuildText, ChannelType.PrivateThread, ChannelType.PublicThread],
|
||||||
|
required: false,
|
||||||
|
}),
|
||||||
|
...generateAttachmentSlashOptions(NUMBER_ATTACHMENTS_CASE_CREATION, {
|
||||||
|
name: "attachment",
|
||||||
|
description: "An attachment to add to the reason of the mute",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ForceMuteSlashCmd = {
|
||||||
|
name: "forcemute",
|
||||||
|
configPermission: "can_mute",
|
||||||
|
description: "Force-mute the specified user, even if they're not on the server",
|
||||||
|
allowDms: false,
|
||||||
|
|
||||||
|
signature: [slashOptions.user({ name: "user", description: "The user to mute", required: true }), ...opts],
|
||||||
|
|
||||||
|
async run({ interaction, options, pluginData }) {
|
||||||
|
const attachments = retrieveMultipleOptions(NUMBER_ATTACHMENTS_CASE_CREATION, options, "attachment");
|
||||||
|
|
||||||
|
if ((!options.reason || options.reason.trim() === "") && attachments.length < 1) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "Text or attachment required", undefined, undefined, true);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mod = interaction.member;
|
||||||
|
let ppId: string | undefined;
|
||||||
|
const canActAsOther = await hasPermission(pluginData, "can_act_as_other", {
|
||||||
|
channel: interaction.channel,
|
||||||
|
member: interaction.member,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.mod) {
|
||||||
|
if (!canActAsOther) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "You don't have permission to act as another moderator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod = options.mod;
|
||||||
|
ppId = interaction.user.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const convertedTime = options.time ? convertDelayStringToMS(options.time) : null;
|
||||||
|
if (options.time && !convertedTime) {
|
||||||
|
sendErrorMessage(pluginData, interaction, `Could not convert ${options.time} to a delay`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contactMethods: UserNotificationMethod[] | undefined;
|
||||||
|
try {
|
||||||
|
contactMethods = readContactMethodsFromArgs(options) ?? undefined;
|
||||||
|
} catch (e) {
|
||||||
|
sendErrorMessage(pluginData, interaction, e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualMuteCmd(
|
||||||
|
pluginData,
|
||||||
|
interaction,
|
||||||
|
options.user,
|
||||||
|
attachments,
|
||||||
|
mod,
|
||||||
|
ppId,
|
||||||
|
options.time,
|
||||||
|
options.reason,
|
||||||
|
contactMethods,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,14 +1,14 @@
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
import { canActOn, sendErrorMessage } from "../../../pluginUtils";
|
import { canActOn, hasPermission, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
import { resolveMember, resolveUser } from "../../../utils";
|
import { resolveMember, resolveUser } from "../../../../utils";
|
||||||
import { actualUnmuteCmd } from "../functions/actualUnmuteUserCmd";
|
import { actualUnmuteCmd } from "../../functions/actualCommands/actualUnmuteCmd";
|
||||||
import { modActionsMsgCmd } from "../types";
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
mod: ct.member({ option: true }),
|
mod: ct.member({ option: true }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ForceUnmuteCmd = modActionsMsgCmd({
|
export const ForceUnmuteMsgCmd = modActionsMsgCmd({
|
||||||
trigger: "forceunmute",
|
trigger: "forceunmute",
|
||||||
permission: "can_mute",
|
permission: "can_mute",
|
||||||
description: "Force-unmute the specified user, even if they're not on the server",
|
description: "Force-unmute the specified user, even if they're not on the server",
|
||||||
|
@ -51,6 +51,29 @@ export const ForceUnmuteCmd = modActionsMsgCmd({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
actualUnmuteCmd(pluginData, user, msg, args);
|
// The moderator who did the action is the message author or, if used, the specified -mod
|
||||||
|
let mod = msg.member;
|
||||||
|
let ppId: string | undefined;
|
||||||
|
|
||||||
|
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;
|
||||||
|
ppId = msg.author.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualUnmuteCmd(
|
||||||
|
pluginData,
|
||||||
|
msg.channel,
|
||||||
|
user,
|
||||||
|
[...msg.attachments.values()],
|
||||||
|
mod,
|
||||||
|
ppId,
|
||||||
|
"time" in args ? args.time ?? undefined : undefined,
|
||||||
|
args.reason,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { slashOptions } from "knub";
|
||||||
|
import { hasPermission, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import { convertDelayStringToMS } from "../../../../utils";
|
||||||
|
import { generateAttachmentSlashOptions, retrieveMultipleOptions } from "../../../../utils/multipleSlashOptions";
|
||||||
|
import { actualUnmuteCmd } from "../../functions/actualCommands/actualUnmuteCmd";
|
||||||
|
import { NUMBER_ATTACHMENTS_CASE_CREATION } from "../constants";
|
||||||
|
|
||||||
|
const opts = [
|
||||||
|
slashOptions.string({ name: "time", description: "The duration of the unmute", required: false }),
|
||||||
|
slashOptions.string({ name: "reason", description: "The reason", required: false }),
|
||||||
|
slashOptions.user({ name: "mod", description: "The moderator to unmute as", required: false }),
|
||||||
|
...generateAttachmentSlashOptions(NUMBER_ATTACHMENTS_CASE_CREATION, {
|
||||||
|
name: "attachment",
|
||||||
|
description: "An attachment to add to the reason of the unmute",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ForceUnmuteSlashCmd = {
|
||||||
|
name: "forceunmute",
|
||||||
|
configPermission: "can_mute",
|
||||||
|
description: "Force-unmute the specified user, even if they're not on the server",
|
||||||
|
allowDms: false,
|
||||||
|
|
||||||
|
signature: [slashOptions.user({ name: "user", description: "The user to unmute", required: true }), ...opts],
|
||||||
|
|
||||||
|
async run({ interaction, options, pluginData }) {
|
||||||
|
const attachments = retrieveMultipleOptions(NUMBER_ATTACHMENTS_CASE_CREATION, options, "attachment");
|
||||||
|
|
||||||
|
if ((!options.reason || options.reason.trim() === "") && attachments.length < 1) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "Text or attachment required", undefined, undefined, true);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mod = interaction.member;
|
||||||
|
let ppId: string | undefined;
|
||||||
|
const canActAsOther = await hasPermission(pluginData, "can_act_as_other", {
|
||||||
|
channel: interaction.channel,
|
||||||
|
member: interaction.member,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.mod) {
|
||||||
|
if (!canActAsOther) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "You don't have permission to act as another moderator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod = options.mod;
|
||||||
|
ppId = interaction.user.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const convertedTime = options.time ? convertDelayStringToMS(options.time) : null;
|
||||||
|
if (options.time && !convertedTime) {
|
||||||
|
sendErrorMessage(pluginData, interaction, `Could not convert ${options.time} to a delay`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualUnmuteCmd(pluginData, interaction, options.user, attachments, mod, ppId, options.time, options.reason);
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
|
import { actualHideCaseCmd } from "../../functions/actualCommands/actualHideCaseCmd";
|
||||||
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
|
export const HideCaseMsgCmd = modActionsMsgCmd({
|
||||||
|
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 }) {
|
||||||
|
actualHideCaseCmd(pluginData, msg.channel, args.caseNum);
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { slashOptions } from "knub";
|
||||||
|
import { actualHideCaseCmd } from "../../functions/actualCommands/actualHideCaseCmd";
|
||||||
|
|
||||||
|
export const HideCaseSlashCmd = {
|
||||||
|
name: "hidecase",
|
||||||
|
configPermission: "can_hidecase",
|
||||||
|
description: "Hide the specified case so it doesn't appear in !cases or !info",
|
||||||
|
allowDms: false,
|
||||||
|
|
||||||
|
signature: [
|
||||||
|
slashOptions.string({ name: "case-number", description: "The number of the case to hide", required: true }),
|
||||||
|
],
|
||||||
|
|
||||||
|
async run({ interaction, options, pluginData }) {
|
||||||
|
actualHideCaseCmd(pluginData, interaction, options["case-number"].split(/[\s,]+/).map(Number));
|
||||||
|
},
|
||||||
|
};
|
68
backend/src/plugins/ModActions/commands/kick/KickMsgCmd.ts
Normal file
68
backend/src/plugins/ModActions/commands/kick/KickMsgCmd.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import { hasPermission } from "knub/helpers";
|
||||||
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
|
import { sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import { resolveUser } from "../../../../utils";
|
||||||
|
import { actualKickCmd } from "../../functions/actualCommands/actualKickCmd";
|
||||||
|
import { readContactMethodsFromArgs } from "../../functions/readContactMethodsFromArgs";
|
||||||
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
|
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 KickMsgCmd = modActionsMsgCmd({
|
||||||
|
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 }) {
|
||||||
|
const user = await resolveUser(pluginData.client, args.user);
|
||||||
|
if (!user.id) {
|
||||||
|
sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||||
|
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(await pluginData.config.getForMessage(msg), "can_act_as_other"))) {
|
||||||
|
sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod = args.mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contactMethods;
|
||||||
|
try {
|
||||||
|
contactMethods = readContactMethodsFromArgs(args);
|
||||||
|
} catch (e) {
|
||||||
|
sendErrorMessage(pluginData, msg.channel, e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualKickCmd(
|
||||||
|
pluginData,
|
||||||
|
msg.channel,
|
||||||
|
msg.member,
|
||||||
|
user,
|
||||||
|
args.reason,
|
||||||
|
[...msg.attachments.values()],
|
||||||
|
mod,
|
||||||
|
contactMethods,
|
||||||
|
args.clean,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
91
backend/src/plugins/ModActions/commands/kick/KickSlashCmd.ts
Normal file
91
backend/src/plugins/ModActions/commands/kick/KickSlashCmd.ts
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import { ChannelType } from "discord.js";
|
||||||
|
import { slashOptions } from "knub";
|
||||||
|
import { hasPermission, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import { UserNotificationMethod } from "../../../../utils";
|
||||||
|
import { generateAttachmentSlashOptions, retrieveMultipleOptions } from "../../../../utils/multipleSlashOptions";
|
||||||
|
import { actualKickCmd } from "../../functions/actualCommands/actualKickCmd";
|
||||||
|
import { readContactMethodsFromArgs } from "../../functions/readContactMethodsFromArgs";
|
||||||
|
import { NUMBER_ATTACHMENTS_CASE_CREATION } from "../constants";
|
||||||
|
|
||||||
|
const opts = [
|
||||||
|
slashOptions.string({ name: "reason", description: "The reason", required: false }),
|
||||||
|
slashOptions.user({ name: "mod", description: "The moderator to kick as", required: false }),
|
||||||
|
slashOptions.string({
|
||||||
|
name: "notify",
|
||||||
|
description: "How to notify",
|
||||||
|
required: false,
|
||||||
|
choices: [
|
||||||
|
{ name: "DM", value: "dm" },
|
||||||
|
{ name: "Channel", value: "channel" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
slashOptions.channel({
|
||||||
|
name: "notify-channel",
|
||||||
|
description: "The channel to notify in",
|
||||||
|
channelTypes: [ChannelType.GuildText, ChannelType.PrivateThread, ChannelType.PublicThread],
|
||||||
|
required: false,
|
||||||
|
}),
|
||||||
|
slashOptions.boolean({
|
||||||
|
name: "clean",
|
||||||
|
description: "Whether or not to delete the member's last messages",
|
||||||
|
required: false,
|
||||||
|
}),
|
||||||
|
...generateAttachmentSlashOptions(NUMBER_ATTACHMENTS_CASE_CREATION, {
|
||||||
|
name: "attachment",
|
||||||
|
description: "An attachment to add to the reason of the kick",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const KickSlashCmd = {
|
||||||
|
name: "kick",
|
||||||
|
configPermission: "can_kick",
|
||||||
|
description: "Kick the specified member",
|
||||||
|
allowDms: false,
|
||||||
|
|
||||||
|
signature: [slashOptions.user({ name: "user", description: "The user to kick", required: true }), ...opts],
|
||||||
|
|
||||||
|
async run({ interaction, options, pluginData }) {
|
||||||
|
const attachments = retrieveMultipleOptions(NUMBER_ATTACHMENTS_CASE_CREATION, options, "attachment");
|
||||||
|
|
||||||
|
if ((!options.reason || options.reason.trim() === "") && attachments.length < 1) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "Text or attachment required", undefined, undefined, true);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mod = interaction.member;
|
||||||
|
const canActAsOther = await hasPermission(pluginData, "can_act_as_other", {
|
||||||
|
channel: interaction.channel,
|
||||||
|
member: interaction.member,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.mod) {
|
||||||
|
if (!canActAsOther) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "You don't have permission to act as another moderator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod = options.mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contactMethods: UserNotificationMethod[] | undefined;
|
||||||
|
try {
|
||||||
|
contactMethods = readContactMethodsFromArgs(options) ?? undefined;
|
||||||
|
} catch (e) {
|
||||||
|
sendErrorMessage(pluginData, interaction, e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualKickCmd(
|
||||||
|
pluginData,
|
||||||
|
interaction,
|
||||||
|
interaction.member,
|
||||||
|
options.user,
|
||||||
|
options.reason || "",
|
||||||
|
attachments,
|
||||||
|
mod,
|
||||||
|
contactMethods,
|
||||||
|
options.clean,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
|
import { actualMassBanCmd } from "../../functions/actualCommands/actualMassBanCmd";
|
||||||
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
|
export const MassBanMsgCmd = modActionsMsgCmd({
|
||||||
|
trigger: "massban",
|
||||||
|
permission: "can_massban",
|
||||||
|
description: "Mass-ban a list of user IDs",
|
||||||
|
|
||||||
|
signature: [
|
||||||
|
{
|
||||||
|
userIds: ct.string({ rest: true }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run({ pluginData, message: msg, args }) {
|
||||||
|
actualMassBanCmd(pluginData, msg.channel, args.userIds, msg.member);
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { slashOptions } from "knub";
|
||||||
|
import { actualMassBanCmd } from "../../functions/actualCommands/actualMassBanCmd";
|
||||||
|
|
||||||
|
export const MassBanSlashCmd = {
|
||||||
|
name: "massban",
|
||||||
|
configPermission: "can_massban",
|
||||||
|
description: "Mass-ban a list of user IDs",
|
||||||
|
allowDms: false,
|
||||||
|
|
||||||
|
signature: [slashOptions.string({ name: "user-ids", description: "The list of user IDs to ban", required: true })],
|
||||||
|
|
||||||
|
async run({ interaction, options, pluginData }) {
|
||||||
|
actualMassBanCmd(pluginData, interaction, options["user-ids"].split(/[\s,\r\n]+/), interaction.member);
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
|
import { actualMassMuteCmd } from "../../functions/actualCommands/actualMassMuteCmd";
|
||||||
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
|
export const MassMuteMsgCmd = modActionsMsgCmd({
|
||||||
|
trigger: "massmute",
|
||||||
|
permission: "can_massmute",
|
||||||
|
description: "Mass-mute a list of user IDs",
|
||||||
|
|
||||||
|
signature: [
|
||||||
|
{
|
||||||
|
userIds: ct.string({ rest: true }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run({ pluginData, message: msg, args }) {
|
||||||
|
actualMassMuteCmd(pluginData, msg.channel, args.userIds, msg.member);
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { slashOptions } from "knub";
|
||||||
|
import { actualMassMuteCmd } from "../../functions/actualCommands/actualMassMuteCmd";
|
||||||
|
|
||||||
|
export const MassMuteSlashSlashCmd = {
|
||||||
|
name: "massmute",
|
||||||
|
configPermission: "can_massmute",
|
||||||
|
description: "Mass-mute a list of user IDs",
|
||||||
|
allowDms: false,
|
||||||
|
|
||||||
|
signature: [slashOptions.string({ name: "user-ids", description: "The list of user IDs to mute", required: true })],
|
||||||
|
|
||||||
|
async run({ interaction, options, pluginData }) {
|
||||||
|
actualMassMuteCmd(pluginData, interaction, options["user-ids"].split(/[\s,\r\n]+/), interaction.member);
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
|
import { actualMassBanCmd } from "../../functions/actualCommands/actualMassBanCmd";
|
||||||
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
|
export const MassUnbanMsgCmd = modActionsMsgCmd({
|
||||||
|
trigger: "massunban",
|
||||||
|
permission: "can_massunban",
|
||||||
|
description: "Mass-unban a list of user IDs",
|
||||||
|
|
||||||
|
signature: [
|
||||||
|
{
|
||||||
|
userIds: ct.string({ rest: true }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run({ pluginData, message: msg, args }) {
|
||||||
|
actualMassBanCmd(pluginData, msg.channel, args.userIds, msg.member);
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { slashOptions } from "knub";
|
||||||
|
import { actualMassUnbanCmd } from "../../functions/actualCommands/actualMassUnbanCmd";
|
||||||
|
|
||||||
|
export const MassUnbanSlashCmd = {
|
||||||
|
name: "massunban",
|
||||||
|
configPermission: "can_massunban",
|
||||||
|
description: "Mass-unban a list of user IDs",
|
||||||
|
allowDms: false,
|
||||||
|
|
||||||
|
signature: [slashOptions.string({ name: "user-ids", description: "The list of user IDs to unban", required: true })],
|
||||||
|
|
||||||
|
async run({ interaction, options, pluginData }) {
|
||||||
|
actualMassUnbanCmd(pluginData, interaction, options["user-ids"].split(/[\s,\r\n]+/), interaction.member);
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,10 +1,11 @@
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
import { canActOn, sendErrorMessage } from "../../../pluginUtils";
|
import { canActOn, hasPermission, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
import { resolveMember, resolveUser } from "../../../utils";
|
import { resolveMember, resolveUser } from "../../../../utils";
|
||||||
import { waitForButtonConfirm } from "../../../utils/waitForInteraction";
|
import { waitForButtonConfirm } from "../../../../utils/waitForInteraction";
|
||||||
import { actualMuteUserCmd } from "../functions/actualMuteUserCmd";
|
import { actualMuteCmd } from "../../functions/actualCommands/actualMuteCmd";
|
||||||
import { isBanned } from "../functions/isBanned";
|
import { isBanned } from "../../functions/isBanned";
|
||||||
import { modActionsMsgCmd } from "../types";
|
import { readContactMethodsFromArgs } from "../../functions/readContactMethodsFromArgs";
|
||||||
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
mod: ct.member({ option: true }),
|
mod: ct.member({ option: true }),
|
||||||
|
@ -12,7 +13,7 @@ const opts = {
|
||||||
"notify-channel": ct.textChannel({ option: true }),
|
"notify-channel": ct.textChannel({ option: true }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MuteCmd = modActionsMsgCmd({
|
export const MuteMsgCmd = modActionsMsgCmd({
|
||||||
trigger: "mute",
|
trigger: "mute",
|
||||||
permission: "can_mute",
|
permission: "can_mute",
|
||||||
description: "Mute the specified member",
|
description: "Mute the specified member",
|
||||||
|
@ -73,6 +74,38 @@ export const MuteCmd = modActionsMsgCmd({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
actualMuteUserCmd(pluginData, user, msg, args);
|
// The moderator who did the action is the message author or, if used, the specified -mod
|
||||||
|
let mod = msg.member;
|
||||||
|
let ppId: string | undefined;
|
||||||
|
|
||||||
|
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;
|
||||||
|
ppId = msg.author.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contactMethods;
|
||||||
|
try {
|
||||||
|
contactMethods = readContactMethodsFromArgs(args);
|
||||||
|
} catch (e) {
|
||||||
|
sendErrorMessage(pluginData, msg.channel, e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualMuteCmd(
|
||||||
|
pluginData,
|
||||||
|
msg.channel,
|
||||||
|
user,
|
||||||
|
[...msg.attachments.values()],
|
||||||
|
mod,
|
||||||
|
ppId,
|
||||||
|
"time" in args ? args.time ?? undefined : undefined,
|
||||||
|
args.reason,
|
||||||
|
contactMethods,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
130
backend/src/plugins/ModActions/commands/mute/MuteSlashCmd.ts
Normal file
130
backend/src/plugins/ModActions/commands/mute/MuteSlashCmd.ts
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
import { ChannelType } from "discord.js";
|
||||||
|
import { slashOptions } from "knub";
|
||||||
|
import { canActOn, hasPermission, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import { UserNotificationMethod, convertDelayStringToMS, resolveMember } from "../../../../utils";
|
||||||
|
import { generateAttachmentSlashOptions, retrieveMultipleOptions } from "../../../../utils/multipleSlashOptions";
|
||||||
|
import { waitForButtonConfirm } from "../../../../utils/waitForInteraction";
|
||||||
|
import { actualMuteCmd } from "../../functions/actualCommands/actualMuteCmd";
|
||||||
|
import { isBanned } from "../../functions/isBanned";
|
||||||
|
import { readContactMethodsFromArgs } from "../../functions/readContactMethodsFromArgs";
|
||||||
|
import { NUMBER_ATTACHMENTS_CASE_CREATION } from "../constants";
|
||||||
|
|
||||||
|
const opts = [
|
||||||
|
slashOptions.string({ name: "time", description: "The duration of the mute", required: false }),
|
||||||
|
slashOptions.string({ name: "reason", description: "The reason", required: false }),
|
||||||
|
slashOptions.user({ name: "mod", description: "The moderator to mute as", required: false }),
|
||||||
|
slashOptions.string({
|
||||||
|
name: "notify",
|
||||||
|
description: "How to notify",
|
||||||
|
required: false,
|
||||||
|
choices: [
|
||||||
|
{ name: "DM", value: "dm" },
|
||||||
|
{ name: "Channel", value: "channel" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
slashOptions.channel({
|
||||||
|
name: "notify-channel",
|
||||||
|
description: "The channel to notify in",
|
||||||
|
channelTypes: [ChannelType.GuildText, ChannelType.PrivateThread, ChannelType.PublicThread],
|
||||||
|
required: false,
|
||||||
|
}),
|
||||||
|
...generateAttachmentSlashOptions(NUMBER_ATTACHMENTS_CASE_CREATION, {
|
||||||
|
name: "attachment",
|
||||||
|
description: "An attachment to add to the reason of the mute",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MuteSlashCmd = {
|
||||||
|
name: "mute",
|
||||||
|
configPermission: "can_mute",
|
||||||
|
description: "Mute the specified member",
|
||||||
|
allowDms: false,
|
||||||
|
|
||||||
|
signature: [slashOptions.user({ name: "user", description: "The user to mute", required: true }), ...opts],
|
||||||
|
|
||||||
|
async run({ interaction, options, pluginData }) {
|
||||||
|
const attachments = retrieveMultipleOptions(NUMBER_ATTACHMENTS_CASE_CREATION, options, "attachment");
|
||||||
|
|
||||||
|
if ((!options.reason || options.reason.trim() === "") && attachments.length < 1) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "Text or attachment required", undefined, undefined, true);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const memberToMute = await resolveMember(pluginData.client, pluginData.guild, options.user.id);
|
||||||
|
|
||||||
|
if (!memberToMute) {
|
||||||
|
const _isBanned = await isBanned(pluginData, options.user.id);
|
||||||
|
const prefix = pluginData.fullConfig.prefix;
|
||||||
|
if (_isBanned) {
|
||||||
|
sendErrorMessage(
|
||||||
|
pluginData,
|
||||||
|
interaction,
|
||||||
|
`User is banned. Use \`${prefix}forcemute\` if you want to mute them anyway.`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Ask the mod if we should upgrade to a forcemute as the user is not on the server
|
||||||
|
const reply = await waitForButtonConfirm(
|
||||||
|
interaction,
|
||||||
|
{ content: "User not found on the server, forcemute instead?" },
|
||||||
|
{ confirmText: "Yes", cancelText: "No", restrictToId: interaction.member.id },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!reply) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "User not on server, mute cancelled by moderator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we're allowed to mute this member
|
||||||
|
if (memberToMute && !canActOn(pluginData, interaction.member, memberToMute)) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "Cannot mute: insufficient permissions");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mod = interaction.member;
|
||||||
|
let ppId: string | undefined;
|
||||||
|
const canActAsOther = await hasPermission(pluginData, "can_act_as_other", {
|
||||||
|
channel: interaction.channel,
|
||||||
|
member: interaction.member,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.mod) {
|
||||||
|
if (!canActAsOther) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "You don't have permission to act as another moderator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod = options.mod;
|
||||||
|
ppId = interaction.user.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const convertedTime = options.time ? convertDelayStringToMS(options.time) : null;
|
||||||
|
if (options.time && !convertedTime) {
|
||||||
|
sendErrorMessage(pluginData, interaction, `Could not convert ${options.time} to a delay`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contactMethods: UserNotificationMethod[] | undefined;
|
||||||
|
try {
|
||||||
|
contactMethods = readContactMethodsFromArgs(options) ?? undefined;
|
||||||
|
} catch (e) {
|
||||||
|
sendErrorMessage(pluginData, interaction, e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualMuteCmd(
|
||||||
|
pluginData,
|
||||||
|
interaction,
|
||||||
|
options.user,
|
||||||
|
attachments,
|
||||||
|
mod,
|
||||||
|
ppId,
|
||||||
|
options.time,
|
||||||
|
options.reason,
|
||||||
|
contactMethods,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,7 +1,7 @@
|
||||||
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
import { sendErrorMessage } from "../../../../pluginUtils";
|
import { sendErrorMessage } from "../../../../pluginUtils";
|
||||||
import { resolveUser } from "../../../../utils";
|
import { resolveUser } from "../../../../utils";
|
||||||
import { actualNoteCmd } from "../../functions/actualNoteCmd";
|
import { actualNoteCmd } from "../../functions/actualCommands/actualNoteCmd";
|
||||||
import { modActionsMsgCmd } from "../../types";
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
export const NoteMsgCmd = modActionsMsgCmd({
|
export const NoteMsgCmd = modActionsMsgCmd({
|
||||||
|
|
|
@ -1,38 +1,27 @@
|
||||||
import { ApplicationCommandOptionType, ChatInputCommandInteraction } from "discord.js";
|
|
||||||
import { slashOptions } from "knub";
|
import { slashOptions } from "knub";
|
||||||
import { sendErrorMessage } from "../../../../pluginUtils";
|
import { sendErrorMessage } from "../../../../pluginUtils";
|
||||||
import { actualNoteCmd } from "../../functions/actualNoteCmd";
|
import { generateAttachmentSlashOptions, retrieveMultipleOptions } from "../../../../utils/multipleSlashOptions";
|
||||||
|
import { actualNoteCmd } from "../../functions/actualCommands/actualNoteCmd";
|
||||||
|
import { NUMBER_ATTACHMENTS_CASE_CREATION } from "../constants";
|
||||||
|
|
||||||
|
const opts = [
|
||||||
|
slashOptions.string({ name: "note", description: "The note to add to the user", required: false }),
|
||||||
|
...generateAttachmentSlashOptions(NUMBER_ATTACHMENTS_CASE_CREATION, {
|
||||||
|
name: "attachment",
|
||||||
|
description: "An attachment to add to the note",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
export const NoteSlashCmd = {
|
export const NoteSlashCmd = {
|
||||||
name: "note",
|
name: "note",
|
||||||
|
configPermission: "can_note",
|
||||||
description: "Add a note to the specified user",
|
description: "Add a note to the specified user",
|
||||||
allowDms: false,
|
allowDms: false,
|
||||||
configPermission: "can_note",
|
|
||||||
|
|
||||||
signature: [
|
signature: [slashOptions.user({ name: "user", description: "The user to add a note to", required: true }), ...opts],
|
||||||
slashOptions.user({ name: "user", description: "The user to add a note to", required: true }),
|
|
||||||
slashOptions.string({ name: "note", description: "The note to add to the user", required: false }),
|
|
||||||
...new Array(10).fill(0).map((_, i) => {
|
|
||||||
return {
|
|
||||||
name: `attachment${i + 1}`,
|
|
||||||
description: "An attachment to add to the note",
|
|
||||||
type: ApplicationCommandOptionType.Attachment,
|
|
||||||
required: false,
|
|
||||||
resolveValue: (interaction: ChatInputCommandInteraction) => {
|
|
||||||
return interaction.options.getAttachment(`attachment${i + 1}`);
|
|
||||||
},
|
|
||||||
getExtraAPIProps: () => ({}),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
|
|
||||||
async run({ interaction, options, pluginData }) {
|
async run({ interaction, options, pluginData }) {
|
||||||
const attachments = new Array(10)
|
const attachments = retrieveMultipleOptions(NUMBER_ATTACHMENTS_CASE_CREATION, options, "attachment");
|
||||||
.fill(0)
|
|
||||||
.map((_, i) => {
|
|
||||||
return options[`attachment${i + 1}`];
|
|
||||||
})
|
|
||||||
.filter((a) => a);
|
|
||||||
|
|
||||||
if ((!options.note || options.note.trim() === "") && attachments.length < 1) {
|
if ((!options.note || options.note.trim() === "") && attachments.length < 1) {
|
||||||
sendErrorMessage(pluginData, interaction, "Text or attachment required", undefined, undefined, true);
|
sendErrorMessage(pluginData, interaction, "Text or attachment required", undefined, undefined, true);
|
||||||
|
|
45
backend/src/plugins/ModActions/commands/unban/UnbanMsgCmd.ts
Normal file
45
backend/src/plugins/ModActions/commands/unban/UnbanMsgCmd.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
|
import { hasPermission, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import { resolveUser } from "../../../../utils";
|
||||||
|
import { actualUnbanCmd } from "../../functions/actualCommands/actualUnbanCmd";
|
||||||
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
mod: ct.member({ option: true }),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UnbanMsgCmd = modActionsMsgCmd({
|
||||||
|
trigger: "unban",
|
||||||
|
permission: "can_unban",
|
||||||
|
description: "Unban the specified member",
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualUnbanCmd(pluginData, msg.channel, msg.author.id, user, args.reason, [...msg.attachments.values()], mod);
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { slashOptions } from "knub";
|
||||||
|
import { hasPermission, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import { generateAttachmentSlashOptions, retrieveMultipleOptions } from "../../../../utils/multipleSlashOptions";
|
||||||
|
import { actualUnbanCmd } from "../../functions/actualCommands/actualUnbanCmd";
|
||||||
|
import { NUMBER_ATTACHMENTS_CASE_CREATION } from "../constants";
|
||||||
|
|
||||||
|
const opts = [
|
||||||
|
slashOptions.string({ name: "reason", description: "The reason", required: false }),
|
||||||
|
slashOptions.user({ name: "mod", description: "The moderator to unban as", required: false }),
|
||||||
|
...generateAttachmentSlashOptions(NUMBER_ATTACHMENTS_CASE_CREATION, {
|
||||||
|
name: "attachment",
|
||||||
|
description: "An attachment to add to the reason of the unban",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const UnbanSlashCmd = {
|
||||||
|
name: "unban",
|
||||||
|
configPermission: "can_unban",
|
||||||
|
description: "Unban the specified member",
|
||||||
|
allowDms: false,
|
||||||
|
|
||||||
|
signature: [slashOptions.user({ name: "user", description: "The user to unban", required: true }), ...opts],
|
||||||
|
|
||||||
|
async run({ interaction, options, pluginData }) {
|
||||||
|
const attachments = retrieveMultipleOptions(NUMBER_ATTACHMENTS_CASE_CREATION, options, "attachment");
|
||||||
|
|
||||||
|
if ((!options.reason || options.reason.trim() === "") && attachments.length < 1) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "Text or attachment required", undefined, undefined, true);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mod = interaction.member;
|
||||||
|
const canActAsOther = await hasPermission(pluginData, "can_act_as_other", {
|
||||||
|
channel: interaction.channel,
|
||||||
|
member: interaction.member,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.mod) {
|
||||||
|
if (!canActAsOther) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "You don't have permission to act as another moderator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod = options.mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualUnbanCmd(pluginData, interaction, interaction.user.id, options.user, options.reason, attachments, mod);
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
|
import { actualHideCaseCmd } from "../../functions/actualCommands/actualHideCaseCmd";
|
||||||
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
|
export const UnhideCaseMsgCmd = modActionsMsgCmd({
|
||||||
|
trigger: ["unhide", "unhidecase", "unhide_case"],
|
||||||
|
permission: "can_hidecase",
|
||||||
|
description: "Un-hide the specified case, making it appear in !cases and !info again",
|
||||||
|
|
||||||
|
signature: [
|
||||||
|
{
|
||||||
|
caseNum: ct.number({ rest: true }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
async run({ pluginData, message: msg, args }) {
|
||||||
|
actualHideCaseCmd(pluginData, msg.channel, args.caseNum);
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { slashOptions } from "knub";
|
||||||
|
import { actualUnhideCaseCmd } from "../../functions/actualCommands/actualUnhideCaseCmd";
|
||||||
|
|
||||||
|
export const UnhideCaseSlashCmd = {
|
||||||
|
name: "unhidecase",
|
||||||
|
configPermission: "can_hidecase",
|
||||||
|
description: "Un-hide the specified case",
|
||||||
|
allowDms: false,
|
||||||
|
|
||||||
|
signature: [
|
||||||
|
slashOptions.string({ name: "case-number", description: "The number of the case to unhide", required: true }),
|
||||||
|
],
|
||||||
|
|
||||||
|
async run({ interaction, options, pluginData }) {
|
||||||
|
actualUnhideCaseCmd(pluginData, interaction, options["case-number"].split(/[\s,]+/).map(Number));
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,17 +1,17 @@
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
import { MutesPlugin } from "../../../plugins/Mutes/MutesPlugin";
|
import { canActOn, hasPermission, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
import { canActOn, sendErrorMessage } from "../../../pluginUtils";
|
import { resolveMember, resolveUser } from "../../../../utils";
|
||||||
import { resolveMember, resolveUser } from "../../../utils";
|
import { waitForButtonConfirm } from "../../../../utils/waitForInteraction";
|
||||||
import { waitForButtonConfirm } from "../../../utils/waitForInteraction";
|
import { MutesPlugin } from "../../../Mutes/MutesPlugin";
|
||||||
import { actualUnmuteCmd } from "../functions/actualUnmuteUserCmd";
|
import { actualUnmuteCmd } from "../../functions/actualCommands/actualUnmuteCmd";
|
||||||
import { isBanned } from "../functions/isBanned";
|
import { isBanned } from "../../functions/isBanned";
|
||||||
import { modActionsMsgCmd } from "../types";
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
mod: ct.member({ option: true }),
|
mod: ct.member({ option: true }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UnmuteCmd = modActionsMsgCmd({
|
export const UnmuteMsgCmd = modActionsMsgCmd({
|
||||||
trigger: "unmute",
|
trigger: "unmute",
|
||||||
permission: "can_mute",
|
permission: "can_mute",
|
||||||
description: "Unmute the specified member",
|
description: "Unmute the specified member",
|
||||||
|
@ -84,6 +84,29 @@ export const UnmuteCmd = modActionsMsgCmd({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
actualUnmuteCmd(pluginData, user, msg, args);
|
// The moderator who did the action is the message author or, if used, the specified -mod
|
||||||
|
let mod = msg.member;
|
||||||
|
let ppId: string | undefined;
|
||||||
|
|
||||||
|
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;
|
||||||
|
ppId = msg.author.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualUnmuteCmd(
|
||||||
|
pluginData,
|
||||||
|
msg.channel,
|
||||||
|
user,
|
||||||
|
[...msg.attachments.values()],
|
||||||
|
mod,
|
||||||
|
ppId,
|
||||||
|
"time" in args ? args.time ?? undefined : undefined,
|
||||||
|
args.reason,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
108
backend/src/plugins/ModActions/commands/unmute/UnmuteSlashCmd.ts
Normal file
108
backend/src/plugins/ModActions/commands/unmute/UnmuteSlashCmd.ts
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
import { slashOptions } from "knub";
|
||||||
|
import { canActOn, hasPermission, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import { convertDelayStringToMS, resolveMember } from "../../../../utils";
|
||||||
|
import { generateAttachmentSlashOptions, retrieveMultipleOptions } from "../../../../utils/multipleSlashOptions";
|
||||||
|
import { waitForButtonConfirm } from "../../../../utils/waitForInteraction";
|
||||||
|
import { MutesPlugin } from "../../../Mutes/MutesPlugin";
|
||||||
|
import { actualUnmuteCmd } from "../../functions/actualCommands/actualUnmuteCmd";
|
||||||
|
import { isBanned } from "../../functions/isBanned";
|
||||||
|
import { NUMBER_ATTACHMENTS_CASE_CREATION } from "../constants";
|
||||||
|
|
||||||
|
const opts = [
|
||||||
|
slashOptions.string({ name: "time", description: "The duration of the unmute", required: false }),
|
||||||
|
slashOptions.string({ name: "reason", description: "The reason", required: false }),
|
||||||
|
slashOptions.user({ name: "mod", description: "The moderator to unmute as", required: false }),
|
||||||
|
...generateAttachmentSlashOptions(NUMBER_ATTACHMENTS_CASE_CREATION, {
|
||||||
|
name: "attachment",
|
||||||
|
description: "An attachment to add to the reason of the unmute",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const UnmuteSlashCmd = {
|
||||||
|
name: "unmute",
|
||||||
|
configPermission: "can_mute",
|
||||||
|
description: "Unmute the specified member",
|
||||||
|
allowDms: false,
|
||||||
|
|
||||||
|
signature: [slashOptions.user({ name: "user", description: "The user to unmute", required: true }), ...opts],
|
||||||
|
|
||||||
|
async run({ interaction, options, pluginData }) {
|
||||||
|
const attachments = retrieveMultipleOptions(NUMBER_ATTACHMENTS_CASE_CREATION, options, "attachment");
|
||||||
|
|
||||||
|
if ((!options.reason || options.reason.trim() === "") && attachments.length < 1) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "Text or attachment required", undefined, undefined, true);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const memberToUnmute = await resolveMember(pluginData.client, pluginData.guild, options.user.id);
|
||||||
|
const mutesPlugin = pluginData.getPlugin(MutesPlugin);
|
||||||
|
const hasMuteRole = memberToUnmute && mutesPlugin.hasMutedRole(memberToUnmute);
|
||||||
|
|
||||||
|
// Check if they're muted in the first place
|
||||||
|
if (
|
||||||
|
!(await pluginData.state.mutes.isMuted(options.user.id)) &&
|
||||||
|
!hasMuteRole &&
|
||||||
|
!memberToUnmute?.isCommunicationDisabled()
|
||||||
|
) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "Cannot unmute: member is not muted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!memberToUnmute) {
|
||||||
|
const banned = await isBanned(pluginData, options.user.id);
|
||||||
|
const prefix = pluginData.fullConfig.prefix;
|
||||||
|
if (banned) {
|
||||||
|
sendErrorMessage(
|
||||||
|
pluginData,
|
||||||
|
interaction,
|
||||||
|
`User is banned. Use \`${prefix}forceunmute\` to unmute them anyway.`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Ask the mod if we should upgrade to a forceunmute as the user is not on the server
|
||||||
|
const reply = await waitForButtonConfirm(
|
||||||
|
interaction,
|
||||||
|
{ content: "User not on server, forceunmute instead?" },
|
||||||
|
{ confirmText: "Yes", cancelText: "No", restrictToId: interaction.user.id },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!reply) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "User not on server, unmute cancelled by moderator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we're allowed to unmute this member
|
||||||
|
if (memberToUnmute && !canActOn(pluginData, interaction.member, memberToUnmute)) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "Cannot unmute: insufficient permissions");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mod = interaction.member;
|
||||||
|
let ppId: string | undefined;
|
||||||
|
const canActAsOther = await hasPermission(pluginData, "can_act_as_other", {
|
||||||
|
channel: interaction.channel,
|
||||||
|
member: interaction.member,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.mod) {
|
||||||
|
if (!canActAsOther) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "You don't have permission to act as another moderator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod = options.mod;
|
||||||
|
ppId = interaction.user.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const convertedTime = options.time ? convertDelayStringToMS(options.time) : null;
|
||||||
|
if (options.time && !convertedTime) {
|
||||||
|
sendErrorMessage(pluginData, interaction, `Could not convert ${options.time} to a delay`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualUnmuteCmd(pluginData, interaction, options.user, attachments, mod, ppId, options.time, options.reason);
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,8 +1,8 @@
|
||||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
import { updateCase } from "../functions/updateCase";
|
import { updateCase } from "../../functions/updateCase";
|
||||||
import { modActionsMsgCmd } from "../types";
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
export const UpdateCmd = modActionsMsgCmd({
|
export const UpdateMsgCmd = modActionsMsgCmd({
|
||||||
trigger: ["update", "reason"],
|
trigger: ["update", "reason"],
|
||||||
permission: "can_note",
|
permission: "can_note",
|
||||||
description:
|
description:
|
||||||
|
@ -19,6 +19,6 @@ export const UpdateCmd = modActionsMsgCmd({
|
||||||
],
|
],
|
||||||
|
|
||||||
async run({ pluginData, message: msg, args }) {
|
async run({ pluginData, message: msg, args }) {
|
||||||
await updateCase(pluginData, msg, args);
|
await updateCase(pluginData, msg.channel, msg.author, args.caseNumber, args.note, [...msg.attachments.values()]);
|
||||||
},
|
},
|
||||||
});
|
});
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { slashOptions } from "knub";
|
||||||
|
import { generateAttachmentSlashOptions, retrieveMultipleOptions } from "../../../../utils/multipleSlashOptions";
|
||||||
|
import { updateCase } from "../../functions/updateCase";
|
||||||
|
import { NUMBER_ATTACHMENTS_CASE_UPDATE } from "../constants";
|
||||||
|
|
||||||
|
const opts = [
|
||||||
|
slashOptions.string({ name: "case-number", description: "The number of the case to update", required: false }),
|
||||||
|
slashOptions.string({ name: "reason", description: "The note to add to the case", required: false }),
|
||||||
|
...generateAttachmentSlashOptions(NUMBER_ATTACHMENTS_CASE_UPDATE, {
|
||||||
|
name: "attachment",
|
||||||
|
description: "An attachment to add to the update",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const UpdateSlashCmd = {
|
||||||
|
name: "update",
|
||||||
|
configPermission: "can_note",
|
||||||
|
description: "Update the specified case (or your latest case) by adding more notes to it",
|
||||||
|
allowDms: false,
|
||||||
|
|
||||||
|
signature: [...opts],
|
||||||
|
|
||||||
|
async run({ interaction, options, pluginData }) {
|
||||||
|
await updateCase(
|
||||||
|
pluginData,
|
||||||
|
interaction,
|
||||||
|
interaction.user,
|
||||||
|
options.caseNumber,
|
||||||
|
options.note,
|
||||||
|
retrieveMultipleOptions(NUMBER_ATTACHMENTS_CASE_UPDATE, options, "attachment"),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
79
backend/src/plugins/ModActions/commands/warn/WarnMsgCmd.ts
Normal file
79
backend/src/plugins/ModActions/commands/warn/WarnMsgCmd.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import { commandTypeHelpers as ct } from "../../../../commandTypes";
|
||||||
|
import { canActOn, hasPermission, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import { errorMessage, resolveMember, resolveUser } from "../../../../utils";
|
||||||
|
import { actualWarnCmd } from "../../functions/actualCommands/actualWarnCmd";
|
||||||
|
import { isBanned } from "../../functions/isBanned";
|
||||||
|
import { readContactMethodsFromArgs } from "../../functions/readContactMethodsFromArgs";
|
||||||
|
import { modActionsMsgCmd } from "../../types";
|
||||||
|
|
||||||
|
export const WarnMsgCmd = modActionsMsgCmd({
|
||||||
|
trigger: "warn",
|
||||||
|
permission: "can_warn",
|
||||||
|
description: "Send a warning to the specified user",
|
||||||
|
|
||||||
|
signature: {
|
||||||
|
user: ct.string(),
|
||||||
|
reason: ct.string({ catchAll: true }),
|
||||||
|
|
||||||
|
mod: ct.member({ option: true }),
|
||||||
|
notify: ct.string({ option: true }),
|
||||||
|
"notify-channel": ct.textChannel({ option: true }),
|
||||||
|
},
|
||||||
|
|
||||||
|
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 memberToWarn = await resolveMember(pluginData.client, pluginData.guild, user.id);
|
||||||
|
|
||||||
|
if (!memberToWarn) {
|
||||||
|
const _isBanned = await isBanned(pluginData, user.id);
|
||||||
|
if (_isBanned) {
|
||||||
|
sendErrorMessage(pluginData, msg.channel, `User is banned`);
|
||||||
|
} else {
|
||||||
|
sendErrorMessage(pluginData, msg.channel, `User not found on the server`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we're allowed to warn this member
|
||||||
|
if (!canActOn(pluginData, msg.member, memberToWarn)) {
|
||||||
|
sendErrorMessage(pluginData, msg.channel, "Cannot warn: 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 }))) {
|
||||||
|
msg.channel.send(errorMessage("You don't have permission to use -mod"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod = args.mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contactMethods;
|
||||||
|
try {
|
||||||
|
contactMethods = readContactMethodsFromArgs(args);
|
||||||
|
} catch (e) {
|
||||||
|
sendErrorMessage(pluginData, msg.channel, e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualWarnCmd(
|
||||||
|
pluginData,
|
||||||
|
msg.channel,
|
||||||
|
msg.author.id,
|
||||||
|
mod,
|
||||||
|
memberToWarn,
|
||||||
|
args.reason,
|
||||||
|
[...msg.attachments.values()],
|
||||||
|
contactMethods,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
105
backend/src/plugins/ModActions/commands/warn/WarnSlashCmd.ts
Normal file
105
backend/src/plugins/ModActions/commands/warn/WarnSlashCmd.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import { ChannelType } from "discord.js";
|
||||||
|
import { slashOptions } from "knub";
|
||||||
|
import { canActOn, hasPermission, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import { UserNotificationMethod, resolveMember } from "../../../../utils";
|
||||||
|
import { generateAttachmentSlashOptions, retrieveMultipleOptions } from "../../../../utils/multipleSlashOptions";
|
||||||
|
import { actualWarnCmd } from "../../functions/actualCommands/actualWarnCmd";
|
||||||
|
import { isBanned } from "../../functions/isBanned";
|
||||||
|
import { readContactMethodsFromArgs } from "../../functions/readContactMethodsFromArgs";
|
||||||
|
import { NUMBER_ATTACHMENTS_CASE_CREATION } from "../constants";
|
||||||
|
|
||||||
|
const opts = [
|
||||||
|
slashOptions.string({ name: "reason", description: "The reason", required: false }),
|
||||||
|
slashOptions.user({ name: "mod", description: "The moderator to warn as", required: false }),
|
||||||
|
slashOptions.string({
|
||||||
|
name: "notify",
|
||||||
|
description: "How to notify",
|
||||||
|
required: false,
|
||||||
|
choices: [
|
||||||
|
{ name: "DM", value: "dm" },
|
||||||
|
{ name: "Channel", value: "channel" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
slashOptions.channel({
|
||||||
|
name: "notify-channel",
|
||||||
|
description: "The channel to notify in",
|
||||||
|
channelTypes: [ChannelType.GuildText, ChannelType.PrivateThread, ChannelType.PublicThread],
|
||||||
|
required: false,
|
||||||
|
}),
|
||||||
|
...generateAttachmentSlashOptions(NUMBER_ATTACHMENTS_CASE_CREATION, {
|
||||||
|
name: "attachment",
|
||||||
|
description: "An attachment to add to the reason of the warn",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const WarnSlashCmd = {
|
||||||
|
name: "warn",
|
||||||
|
configPermission: "can_warn",
|
||||||
|
description: "Send a warning to the specified user",
|
||||||
|
allowDms: false,
|
||||||
|
|
||||||
|
signature: [slashOptions.user({ name: "user", description: "The user to warn", required: true }), ...opts],
|
||||||
|
|
||||||
|
async run({ interaction, options, pluginData }) {
|
||||||
|
const attachments = retrieveMultipleOptions(NUMBER_ATTACHMENTS_CASE_CREATION, options, "attachment");
|
||||||
|
|
||||||
|
if ((!options.reason || options.reason.trim() === "") && attachments.length < 1) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "Text or attachment required", undefined, undefined, true);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const memberToWarn = await resolveMember(pluginData.client, pluginData.guild, options.user.id);
|
||||||
|
|
||||||
|
if (!memberToWarn) {
|
||||||
|
const _isBanned = await isBanned(pluginData, options.user.id);
|
||||||
|
if (_isBanned) {
|
||||||
|
sendErrorMessage(pluginData, interaction, `User is banned`);
|
||||||
|
} else {
|
||||||
|
sendErrorMessage(pluginData, interaction, `User not found on the server`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we're allowed to warn this member
|
||||||
|
if (!canActOn(pluginData, interaction.member, memberToWarn)) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "Cannot warn: insufficient permissions");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mod = interaction.member;
|
||||||
|
const canActAsOther = await hasPermission(pluginData, "can_act_as_other", {
|
||||||
|
channel: interaction.channel,
|
||||||
|
member: interaction.member,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.mod) {
|
||||||
|
if (!canActAsOther) {
|
||||||
|
sendErrorMessage(pluginData, interaction, "You don't have permission to act as another moderator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod = options.mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contactMethods: UserNotificationMethod[] | undefined;
|
||||||
|
try {
|
||||||
|
contactMethods = readContactMethodsFromArgs(options) ?? undefined;
|
||||||
|
} catch (e) {
|
||||||
|
sendErrorMessage(pluginData, interaction, e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualWarnCmd(
|
||||||
|
pluginData,
|
||||||
|
interaction,
|
||||||
|
interaction.user.id,
|
||||||
|
mod,
|
||||||
|
memberToWarn,
|
||||||
|
options.reason ?? "",
|
||||||
|
attachments,
|
||||||
|
contactMethods,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Attachment, ChatInputCommandInteraction, GuildMember, TextBasedChannel, User } from "discord.js";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
|
import { CaseTypes } from "../../../../data/CaseTypes";
|
||||||
|
import { Case } from "../../../../data/entities/Case";
|
||||||
|
import { canActOn, sendErrorMessage, sendSuccessMessage } from "../../../../pluginUtils";
|
||||||
|
import { UnknownUser, renderUserUsername, resolveMember } from "../../../../utils";
|
||||||
|
import { CasesPlugin } from "../../../Cases/CasesPlugin";
|
||||||
|
import { LogsPlugin } from "../../../Logs/LogsPlugin";
|
||||||
|
import { ModActionsPluginType } from "../../types";
|
||||||
|
import { formatReasonWithAttachments } from "../formatReasonWithAttachments";
|
||||||
|
|
||||||
|
export async function actualAddCaseCmd(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
|
author: GuildMember,
|
||||||
|
mod: GuildMember,
|
||||||
|
attachments: Array<Attachment>,
|
||||||
|
user: User | UnknownUser,
|
||||||
|
type: keyof CaseTypes,
|
||||||
|
reason: string,
|
||||||
|
) {
|
||||||
|
// 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, author, member)) {
|
||||||
|
sendErrorMessage(pluginData, context, "Cannot add case on this user: insufficient permissions");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedReason = formatReasonWithAttachments(reason, attachments);
|
||||||
|
|
||||||
|
// Create the case
|
||||||
|
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||||
|
const theCase: Case = await casesPlugin.createCase({
|
||||||
|
userId: user.id,
|
||||||
|
modId: mod.id,
|
||||||
|
type: CaseTypes[type],
|
||||||
|
reason: formattedReason,
|
||||||
|
ppId: mod.id !== author.id ? author.id : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
sendSuccessMessage(pluginData, context, `Case #${theCase.case_number} created for **${renderUserUsername(user)}**`);
|
||||||
|
} else {
|
||||||
|
sendSuccessMessage(pluginData, context, `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: formattedReason,
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,182 @@
|
||||||
|
import { Attachment, ChatInputCommandInteraction, GuildMember, TextBasedChannel, User } from "discord.js";
|
||||||
|
import humanizeDuration from "humanize-duration";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
|
import { getMemberLevel } from "knub/helpers";
|
||||||
|
import { CaseTypes } from "../../../../data/CaseTypes";
|
||||||
|
import { clearExpiringTempban, registerExpiringTempban } from "../../../../data/loops/expiringTempbansLoop";
|
||||||
|
import { canActOn, isContextInteraction, sendErrorMessage, sendSuccessMessage } from "../../../../pluginUtils";
|
||||||
|
import { UnknownUser, UserNotificationMethod, renderUserUsername, resolveMember } from "../../../../utils";
|
||||||
|
import { banLock } from "../../../../utils/lockNameHelpers";
|
||||||
|
import { waitForButtonConfirm } from "../../../../utils/waitForInteraction";
|
||||||
|
import { CasesPlugin } from "../../../Cases/CasesPlugin";
|
||||||
|
import { LogsPlugin } from "../../../Logs/LogsPlugin";
|
||||||
|
import { ModActionsPluginType } from "../../types";
|
||||||
|
import { banUserId } from "../banUserId";
|
||||||
|
import { formatReasonWithAttachments } from "../formatReasonWithAttachments";
|
||||||
|
import { isBanned } from "../isBanned";
|
||||||
|
|
||||||
|
export async function actualBanCmd(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
|
user: User | UnknownUser,
|
||||||
|
time: number | null,
|
||||||
|
reason: string,
|
||||||
|
attachments: Attachment[],
|
||||||
|
author: GuildMember,
|
||||||
|
mod: GuildMember,
|
||||||
|
contactMethods?: UserNotificationMethod[],
|
||||||
|
deleteDays?: number,
|
||||||
|
) {
|
||||||
|
const memberToBan = await resolveMember(pluginData.client, pluginData.guild, user.id);
|
||||||
|
const formattedReason = formatReasonWithAttachments(reason, attachments);
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
// Ask the mod if we should upgrade to a forceban as the user is not on the server
|
||||||
|
const reply = await waitForButtonConfirm(
|
||||||
|
context,
|
||||||
|
{ content: "User not on server, forceban instead?" },
|
||||||
|
{ confirmText: "Yes", cancelText: "No", restrictToId: author.id },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!reply) {
|
||||||
|
sendErrorMessage(pluginData, context, "User not on server, ban cancelled by moderator");
|
||||||
|
lock.unlock();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
forceban = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abort if trying to ban user indefinitely if they are already banned indefinitely
|
||||||
|
if (!existingTempban && !time) {
|
||||||
|
sendErrorMessage(pluginData, context, `User is already banned indefinitely.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask the mod if we should update the existing ban
|
||||||
|
const reply = await waitForButtonConfirm(
|
||||||
|
context,
|
||||||
|
{ content: "Failed to message the user. Log the warning anyway?" },
|
||||||
|
{ confirmText: "Yes", cancelText: "No", restrictToId: author.id },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!reply) {
|
||||||
|
sendErrorMessage(pluginData, context, "User already banned, update cancelled by moderator");
|
||||||
|
lock.unlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: formattedReason,
|
||||||
|
noteDetails: [`Ban updated to ${time ? humanizeDuration(time) : "indefinite"}`],
|
||||||
|
});
|
||||||
|
if (time) {
|
||||||
|
pluginData.getPlugin(LogsPlugin).logMemberTimedBan({
|
||||||
|
mod: mod.user,
|
||||||
|
user,
|
||||||
|
caseNumber: createdCase.case_number,
|
||||||
|
reason: formattedReason,
|
||||||
|
banTime: humanizeDuration(time),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pluginData.getPlugin(LogsPlugin).logMemberBan({
|
||||||
|
mod: mod.user,
|
||||||
|
user,
|
||||||
|
caseNumber: createdCase.case_number,
|
||||||
|
reason: formattedReason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendSuccessMessage(
|
||||||
|
pluginData,
|
||||||
|
context,
|
||||||
|
`Ban updated to ${time ? "expire in " + humanizeDuration(time) + " from now" : "indefinite"}`,
|
||||||
|
);
|
||||||
|
lock.unlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we're allowed to ban this member if they are on the server
|
||||||
|
if (!forceban && !canActOn(pluginData, author, memberToBan!)) {
|
||||||
|
const ourLevel = getMemberLevel(pluginData, author);
|
||||||
|
const targetLevel = getMemberLevel(pluginData, memberToBan!);
|
||||||
|
sendErrorMessage(
|
||||||
|
pluginData,
|
||||||
|
context,
|
||||||
|
`Cannot ban: target permission level is equal or higher to yours, ${targetLevel} >= ${ourLevel}`,
|
||||||
|
);
|
||||||
|
lock.unlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingConfig = await pluginData.config.getMatchingConfig({
|
||||||
|
member: author,
|
||||||
|
channel: isContextInteraction(context) ? context.channel : context,
|
||||||
|
});
|
||||||
|
const deleteMessageDays = deleteDays ?? matchingConfig.ban_delete_message_days;
|
||||||
|
const banResult = await banUserId(
|
||||||
|
pluginData,
|
||||||
|
user.id,
|
||||||
|
formattedReason,
|
||||||
|
{
|
||||||
|
contactMethods,
|
||||||
|
caseArgs: {
|
||||||
|
modId: mod.id,
|
||||||
|
ppId: mod.id !== author.id ? author.id : undefined,
|
||||||
|
},
|
||||||
|
deleteMessageDays,
|
||||||
|
modId: mod.id,
|
||||||
|
},
|
||||||
|
time ?? undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (banResult.status === "failed") {
|
||||||
|
sendErrorMessage(pluginData, context, `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 **${renderUserUsername(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, context, response);
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { ChatInputCommandInteraction, TextBasedChannel } from "discord.js";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
|
import { sendContextResponse, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import { CasesPlugin } from "../../../Cases/CasesPlugin";
|
||||||
|
import { ModActionsPluginType } from "../../types";
|
||||||
|
|
||||||
|
export async function actualCaseCmd(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
|
authorId: string,
|
||||||
|
caseNumber: number,
|
||||||
|
) {
|
||||||
|
const theCase = await pluginData.state.cases.findByCaseNumber(caseNumber);
|
||||||
|
|
||||||
|
if (!theCase) {
|
||||||
|
sendErrorMessage(pluginData, context, "Case not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||||
|
const embed = await casesPlugin.getCaseEmbed(theCase.id, authorId);
|
||||||
|
|
||||||
|
sendContextResponse(context, embed);
|
||||||
|
}
|
|
@ -0,0 +1,258 @@
|
||||||
|
import { APIEmbed, ChatInputCommandInteraction, TextBasedChannel, User } from "discord.js";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
|
import { In } from "typeorm";
|
||||||
|
import { FindOptionsWhere } from "typeorm/find-options/FindOptionsWhere";
|
||||||
|
import { CaseTypes } from "../../../../data/CaseTypes";
|
||||||
|
import { Case } from "../../../../data/entities/Case";
|
||||||
|
import { sendContextResponse, sendErrorMessage } from "../../../../pluginUtils";
|
||||||
|
import {
|
||||||
|
UnknownUser,
|
||||||
|
chunkArray,
|
||||||
|
emptyEmbedValue,
|
||||||
|
renderUserUsername,
|
||||||
|
resolveUser,
|
||||||
|
trimLines,
|
||||||
|
} from "../../../../utils";
|
||||||
|
import { asyncMap } from "../../../../utils/async";
|
||||||
|
import { createPaginatedMessage } from "../../../../utils/createPaginatedMessage";
|
||||||
|
import { getChunkedEmbedFields } from "../../../../utils/getChunkedEmbedFields";
|
||||||
|
import { getGuildPrefix } from "../../../../utils/getGuildPrefix";
|
||||||
|
import { CasesPlugin } from "../../../Cases/CasesPlugin";
|
||||||
|
import { ModActionsPluginType } from "../../types";
|
||||||
|
|
||||||
|
const casesPerPage = 5;
|
||||||
|
const maxExpandedCases = 8;
|
||||||
|
|
||||||
|
async function sendExpandedCases(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
|
casesCount: number,
|
||||||
|
cases: Case[],
|
||||||
|
) {
|
||||||
|
if (casesCount > maxExpandedCases) {
|
||||||
|
await sendContextResponse(context, "Too many cases for expanded view. Please use compact view instead.");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||||
|
|
||||||
|
for (const theCase of cases) {
|
||||||
|
const embed = await casesPlugin.getCaseEmbed(theCase.id);
|
||||||
|
await sendContextResponse(context, embed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function casesUserCmd(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
|
author: User,
|
||||||
|
modId: string | null,
|
||||||
|
user: User | UnknownUser,
|
||||||
|
modName: string,
|
||||||
|
typesToShow: CaseTypes[],
|
||||||
|
hidden: boolean | null,
|
||||||
|
expand: boolean | null,
|
||||||
|
) {
|
||||||
|
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||||
|
const casesFilters: Omit<FindOptionsWhere<Case>, "guild_id" | "user_id"> = { type: In(typesToShow) };
|
||||||
|
|
||||||
|
if (modId) {
|
||||||
|
casesFilters.mod_id = modId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cases = await pluginData.state.cases.with("notes").getByUserId(user.id, casesFilters);
|
||||||
|
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 : renderUserUsername(user);
|
||||||
|
|
||||||
|
if (cases.length === 0) {
|
||||||
|
await sendContextResponse(context, `No cases found for **${userName}**${modId ? ` by ${modName}` : ""}.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const casesToDisplay = hidden ? cases : normalCases;
|
||||||
|
|
||||||
|
if (!casesToDisplay.length) {
|
||||||
|
await sendContextResponse(
|
||||||
|
context,
|
||||||
|
`No normal cases found for **${userName}**. Use "-hidden" to show ${cases.length} hidden cases.`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expand) {
|
||||||
|
sendExpandedCases(pluginData, context, casesToDisplay.length, casesToDisplay);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compact view (= regular message with a preview of each case)
|
||||||
|
const lines = await asyncMap(casesToDisplay, (c) => casesPlugin.getCaseSummary(c, true, author.id));
|
||||||
|
|
||||||
|
const prefix = getGuildPrefix(pluginData);
|
||||||
|
const linesPerChunk = 10;
|
||||||
|
const lineChunks = chunkArray(lines, linesPerChunk);
|
||||||
|
|
||||||
|
const footerField = {
|
||||||
|
name: emptyEmbedValue,
|
||||||
|
value: trimLines(`
|
||||||
|
Use \`${prefix}case <num>\` to see more information about an individual case
|
||||||
|
`),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [i, linesInChunk] of lineChunks.entries()) {
|
||||||
|
const isLastChunk = i === lineChunks.length - 1;
|
||||||
|
|
||||||
|
if (isLastChunk && !hidden && hiddenCases.length) {
|
||||||
|
if (hiddenCases.length === 1) {
|
||||||
|
linesInChunk.push(`*+${hiddenCases.length} hidden case, use "-hidden" to show it*`);
|
||||||
|
} else {
|
||||||
|
linesInChunk.push(`*+${hiddenCases.length} hidden cases, use "-hidden" to show them*`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunkStart = i * linesPerChunk + 1;
|
||||||
|
const chunkEnd = Math.min((i + 1) * linesPerChunk, lines.length);
|
||||||
|
|
||||||
|
const embed = {
|
||||||
|
author: {
|
||||||
|
name:
|
||||||
|
lineChunks.length === 1
|
||||||
|
? `Cases for ${userName}${modId ? ` by ${modName}` : ""} (${lines.length} total)`
|
||||||
|
: `Cases ${chunkStart}–${chunkEnd} of ${lines.length} for ${userName}`,
|
||||||
|
icon_url: user instanceof User ? user.displayAvatarURL() : undefined,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
...getChunkedEmbedFields(emptyEmbedValue, linesInChunk.join("\n")),
|
||||||
|
...(isLastChunk ? [footerField] : []),
|
||||||
|
],
|
||||||
|
} satisfies APIEmbed;
|
||||||
|
|
||||||
|
sendContextResponse(context, { embeds: [embed] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function casesModCmd(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
|
author: User,
|
||||||
|
modId: string | null,
|
||||||
|
mod: User | UnknownUser,
|
||||||
|
modName: string,
|
||||||
|
typesToShow: CaseTypes[],
|
||||||
|
hidden: boolean | null,
|
||||||
|
expand: boolean | null,
|
||||||
|
) {
|
||||||
|
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||||
|
const caseFilters = { type: In(typesToShow), is_hidden: !!hidden };
|
||||||
|
|
||||||
|
const totalCases = await casesPlugin.getTotalCasesByMod(modId ?? author.id, caseFilters);
|
||||||
|
|
||||||
|
if (totalCases === 0) {
|
||||||
|
sendErrorMessage(pluginData, context, `No cases by **${modName}**`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalPages = Math.max(Math.ceil(totalCases / casesPerPage), 1);
|
||||||
|
const prefix = getGuildPrefix(pluginData);
|
||||||
|
|
||||||
|
if (expand) {
|
||||||
|
// Expanded view (= individual case embeds)
|
||||||
|
const cases = totalCases > 8 ? [] : await casesPlugin.getRecentCasesByMod(modId ?? author.id, 8, 0, caseFilters);
|
||||||
|
|
||||||
|
sendExpandedCases(pluginData, context, totalCases, cases);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
createPaginatedMessage(
|
||||||
|
pluginData.client,
|
||||||
|
context,
|
||||||
|
totalPages,
|
||||||
|
async (page) => {
|
||||||
|
const cases = await casesPlugin.getRecentCasesByMod(
|
||||||
|
modId ?? author.id,
|
||||||
|
casesPerPage,
|
||||||
|
(page - 1) * casesPerPage,
|
||||||
|
caseFilters,
|
||||||
|
);
|
||||||
|
|
||||||
|
const lines = await asyncMap(cases, (c) => casesPlugin.getCaseSummary(c, true, author.id));
|
||||||
|
const firstCaseNum = (page - 1) * casesPerPage + 1;
|
||||||
|
const lastCaseNum = firstCaseNum - 1 + Math.min(cases.length, casesPerPage);
|
||||||
|
const title = `Most recent cases ${firstCaseNum}-${lastCaseNum} of ${totalCases} by ${modName}`;
|
||||||
|
|
||||||
|
const embed = {
|
||||||
|
author: {
|
||||||
|
name: title,
|
||||||
|
icon_url: mod instanceof User ? mod.displayAvatarURL() : undefined,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
...getChunkedEmbedFields(emptyEmbedValue, lines.join("\n")),
|
||||||
|
{
|
||||||
|
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: author.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function actualCasesCmd(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
|
modId: string | null,
|
||||||
|
user: User | UnknownUser | null,
|
||||||
|
author: User,
|
||||||
|
notes: boolean | null,
|
||||||
|
warns: boolean | null,
|
||||||
|
mutes: boolean | null,
|
||||||
|
unmutes: boolean | null,
|
||||||
|
bans: boolean | null,
|
||||||
|
unbans: boolean | null,
|
||||||
|
reverseFilters: boolean | null,
|
||||||
|
hidden: boolean | null,
|
||||||
|
expand: boolean | null,
|
||||||
|
) {
|
||||||
|
const mod = modId ? await resolveUser(pluginData.client, modId) : null;
|
||||||
|
const modName = modId ? (mod instanceof User ? renderUserUsername(mod) : modId) : renderUserUsername(author);
|
||||||
|
|
||||||
|
let typesToShow: CaseTypes[] = [];
|
||||||
|
|
||||||
|
if (notes) typesToShow.push(CaseTypes.Note);
|
||||||
|
if (warns) typesToShow.push(CaseTypes.Warn);
|
||||||
|
if (mutes) typesToShow.push(CaseTypes.Mute);
|
||||||
|
if (unmutes) typesToShow.push(CaseTypes.Unmute);
|
||||||
|
if (bans) typesToShow.push(CaseTypes.Ban);
|
||||||
|
if (unbans) typesToShow.push(CaseTypes.Unban);
|
||||||
|
|
||||||
|
if (typesToShow.length === 0) {
|
||||||
|
typesToShow = [CaseTypes.Note, CaseTypes.Warn, CaseTypes.Mute, CaseTypes.Unmute, CaseTypes.Ban, CaseTypes.Unban];
|
||||||
|
} else {
|
||||||
|
if (reverseFilters) {
|
||||||
|
typesToShow = [
|
||||||
|
CaseTypes.Note,
|
||||||
|
CaseTypes.Warn,
|
||||||
|
CaseTypes.Mute,
|
||||||
|
CaseTypes.Unmute,
|
||||||
|
CaseTypes.Ban,
|
||||||
|
CaseTypes.Unban,
|
||||||
|
].filter((t) => !typesToShow.includes(t));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user
|
||||||
|
? casesUserCmd(pluginData, context, author, modId!, user, modName, typesToShow, hidden, expand)
|
||||||
|
: casesModCmd(pluginData, context, author, modId!, mod!, modName, typesToShow, hidden, expand);
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { ChatInputCommandInteraction, GuildMember, TextBasedChannel } from "discord.js";
|
||||||
|
import { GuildPluginData, helpers } from "knub";
|
||||||
|
import { Case } from "../../../../data/entities/Case";
|
||||||
|
import {
|
||||||
|
isContextInteraction,
|
||||||
|
sendContextResponse,
|
||||||
|
sendErrorMessage,
|
||||||
|
sendSuccessMessage,
|
||||||
|
} from "../../../../pluginUtils";
|
||||||
|
import { SECONDS } from "../../../../utils";
|
||||||
|
import { CasesPlugin } from "../../../Cases/CasesPlugin";
|
||||||
|
import { LogsPlugin } from "../../../Logs/LogsPlugin";
|
||||||
|
import { TimeAndDatePlugin } from "../../../TimeAndDate/TimeAndDatePlugin";
|
||||||
|
import { ModActionsPluginType } from "../../types";
|
||||||
|
|
||||||
|
export async function actualDeleteCaseCmd(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
|
author: GuildMember,
|
||||||
|
caseNumbers: number[],
|
||||||
|
force: boolean,
|
||||||
|
) {
|
||||||
|
const failed: number[] = [];
|
||||||
|
const validCases: Case[] = [];
|
||||||
|
let cancelled = 0;
|
||||||
|
|
||||||
|
for (const num of caseNumbers) {
|
||||||
|
const theCase = await pluginData.state.cases.findByCaseNumber(num);
|
||||||
|
if (!theCase) {
|
||||||
|
failed.push(num);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
validCases.push(theCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed.length === caseNumbers.length) {
|
||||||
|
sendErrorMessage(pluginData, context, "None of the cases were found!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const theCase of validCases) {
|
||||||
|
if (!force) {
|
||||||
|
const cases = pluginData.getPlugin(CasesPlugin);
|
||||||
|
const embedContent = await cases.getCaseEmbed(theCase);
|
||||||
|
sendContextResponse(context, {
|
||||||
|
...embedContent,
|
||||||
|
content: "Delete the following case? Answer 'Yes' to continue, 'No' to cancel.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const reply = await helpers.waitForReply(
|
||||||
|
pluginData.client,
|
||||||
|
isContextInteraction(context) ? context.channel! : context,
|
||||||
|
author.id,
|
||||||
|
15 * SECONDS,
|
||||||
|
);
|
||||||
|
const normalizedReply = (reply?.content || "").toLowerCase().trim();
|
||||||
|
if (normalizedReply !== "yes" && normalizedReply !== "y") {
|
||||||
|
sendContextResponse(context, "Cancelled. Case was not deleted.");
|
||||||
|
cancelled++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedByName = author.user.tag;
|
||||||
|
|
||||||
|
const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin);
|
||||||
|
const deletedAt = timeAndDate.inGuildTz().format(timeAndDate.getDateFormat("pretty_datetime"));
|
||||||
|
|
||||||
|
await pluginData.state.cases.softDelete(
|
||||||
|
theCase.id,
|
||||||
|
author.id,
|
||||||
|
deletedByName,
|
||||||
|
`Case deleted by **${deletedByName}** (\`${author.id}\`) on ${deletedAt}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const logs = pluginData.getPlugin(LogsPlugin);
|
||||||
|
logs.logCaseDelete({
|
||||||
|
mod: author,
|
||||||
|
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, context, "All deletions were cancelled, no cases were deleted.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendSuccessMessage(pluginData, context, `${amt} case${amt === 1 ? " was" : "s were"} deleted!${failedAddendum}`);
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { Attachment, ChatInputCommandInteraction, GuildMember, Snowflake, TextBasedChannel, User } from "discord.js";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
|
import { CaseTypes } from "../../../../data/CaseTypes";
|
||||||
|
import { LogType } from "../../../../data/LogType";
|
||||||
|
import { sendErrorMessage, sendSuccessMessage } from "../../../../pluginUtils";
|
||||||
|
import { DAYS, MINUTES, UnknownUser } from "../../../../utils";
|
||||||
|
import { CasesPlugin } from "../../../Cases/CasesPlugin";
|
||||||
|
import { LogsPlugin } from "../../../Logs/LogsPlugin";
|
||||||
|
import { IgnoredEventType, ModActionsPluginType } from "../../types";
|
||||||
|
import { formatReasonWithAttachments } from "../formatReasonWithAttachments";
|
||||||
|
import { ignoreEvent } from "../ignoreEvent";
|
||||||
|
|
||||||
|
export async function actualForceBanCmd(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
|
authorId: string,
|
||||||
|
user: User | UnknownUser,
|
||||||
|
reason: string,
|
||||||
|
attachments: Array<Attachment>,
|
||||||
|
mod: GuildMember,
|
||||||
|
) {
|
||||||
|
const formattedReason = formatReasonWithAttachments(reason, attachments);
|
||||||
|
|
||||||
|
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: formattedReason ?? undefined,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
sendErrorMessage(pluginData, context, "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: formattedReason,
|
||||||
|
ppId: mod.id !== authorId ? authorId : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Confirm the action
|
||||||
|
sendSuccessMessage(pluginData, context, `Member forcebanned (Case #${createdCase.case_number})`);
|
||||||
|
|
||||||
|
// Log the action
|
||||||
|
pluginData.getPlugin(LogsPlugin).logMemberForceban({
|
||||||
|
mod,
|
||||||
|
userId: user.id,
|
||||||
|
caseNumber: createdCase.case_number,
|
||||||
|
reason: formattedReason,
|
||||||
|
});
|
||||||
|
|
||||||
|
pluginData.state.events.emit("ban", user.id, formattedReason);
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { ChatInputCommandInteraction, TextBasedChannel } from "discord.js";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
|
import { sendErrorMessage, sendSuccessMessage } from "../../../../pluginUtils";
|
||||||
|
import { ModActionsPluginType } from "../../types";
|
||||||
|
|
||||||
|
export async function actualHideCaseCmd(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
|
caseNumbers: number[],
|
||||||
|
) {
|
||||||
|
const failed: number[] = [];
|
||||||
|
|
||||||
|
for (const num of caseNumbers) {
|
||||||
|
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 === caseNumbers.length) {
|
||||||
|
sendErrorMessage(pluginData, context, "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 = caseNumbers.length - failed.length;
|
||||||
|
sendSuccessMessage(
|
||||||
|
pluginData,
|
||||||
|
context,
|
||||||
|
`${amt} case${amt === 1 ? " is" : "s are"} now hidden! Use \`unhidecase\` to unhide them.${failedAddendum}`,
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { Attachment, ChatInputCommandInteraction, GuildMember, TextBasedChannel, User } from "discord.js";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
|
import { LogType } from "../../../../data/LogType";
|
||||||
|
import { canActOn, sendErrorMessage, sendSuccessMessage } from "../../../../pluginUtils";
|
||||||
|
import {
|
||||||
|
DAYS,
|
||||||
|
SECONDS,
|
||||||
|
UnknownUser,
|
||||||
|
UserNotificationMethod,
|
||||||
|
renderUserUsername,
|
||||||
|
resolveMember,
|
||||||
|
} from "../../../../utils";
|
||||||
|
import { IgnoredEventType, ModActionsPluginType } from "../../types";
|
||||||
|
import { formatReasonWithAttachments } from "../formatReasonWithAttachments";
|
||||||
|
import { ignoreEvent } from "../ignoreEvent";
|
||||||
|
import { isBanned } from "../isBanned";
|
||||||
|
import { kickMember } from "../kickMember";
|
||||||
|
|
||||||
|
export async function actualKickCmd(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
|
author: GuildMember,
|
||||||
|
user: User | UnknownUser,
|
||||||
|
reason: string,
|
||||||
|
attachments: Attachment[],
|
||||||
|
mod: GuildMember,
|
||||||
|
contactMethods?: UserNotificationMethod[],
|
||||||
|
clean?: boolean,
|
||||||
|
) {
|
||||||
|
const memberToKick = await resolveMember(pluginData.client, pluginData.guild, user.id);
|
||||||
|
|
||||||
|
if (!memberToKick) {
|
||||||
|
const banned = await isBanned(pluginData, user.id);
|
||||||
|
if (banned) {
|
||||||
|
sendErrorMessage(pluginData, context, `User is banned`);
|
||||||
|
} else {
|
||||||
|
sendErrorMessage(pluginData, context, `User not found on the server`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we're allowed to kick this member
|
||||||
|
if (!canActOn(pluginData, author, memberToKick)) {
|
||||||
|
sendErrorMessage(pluginData, context, "Cannot kick: insufficient permissions");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedReason = formatReasonWithAttachments(reason, attachments);
|
||||||
|
|
||||||
|
const kickResult = await kickMember(pluginData, memberToKick, formattedReason, {
|
||||||
|
contactMethods,
|
||||||
|
caseArgs: {
|
||||||
|
modId: mod.id,
|
||||||
|
ppId: mod.id !== author.id ? author.id : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (clean) {
|
||||||
|
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_BAN, memberToKick.id);
|
||||||
|
ignoreEvent(pluginData, IgnoredEventType.Ban, memberToKick.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await memberToKick.ban({ deleteMessageSeconds: (1 * DAYS) / SECONDS, reason: "kick -clean" });
|
||||||
|
} catch {
|
||||||
|
sendErrorMessage(pluginData, context, "Failed to ban the user to clean messages (-clean)");
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, memberToKick.id);
|
||||||
|
ignoreEvent(pluginData, IgnoredEventType.Unban, memberToKick.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pluginData.guild.bans.remove(memberToKick.id, "kick -clean");
|
||||||
|
} catch {
|
||||||
|
sendErrorMessage(pluginData, context, "Failed to unban the user after banning them (-clean)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kickResult.status === "failed") {
|
||||||
|
sendErrorMessage(pluginData, context, `Failed to kick user`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm the action to the moderator
|
||||||
|
let response = `Kicked **${renderUserUsername(memberToKick.user)}** (Case #${kickResult.case.case_number})`;
|
||||||
|
|
||||||
|
if (kickResult.notifyResult.text) response += ` (${kickResult.notifyResult.text})`;
|
||||||
|
sendSuccessMessage(pluginData, context, response);
|
||||||
|
}
|
|
@ -0,0 +1,163 @@
|
||||||
|
import { ChatInputCommandInteraction, GuildMember, Snowflake, TextBasedChannel } from "discord.js";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
|
import { waitForReply } from "knub/helpers";
|
||||||
|
import { CaseTypes } from "../../../../data/CaseTypes";
|
||||||
|
import { LogType } from "../../../../data/LogType";
|
||||||
|
import { humanizeDurationShort } from "../../../../humanizeDurationShort";
|
||||||
|
import {
|
||||||
|
canActOn,
|
||||||
|
isContextInteraction,
|
||||||
|
sendContextResponse,
|
||||||
|
sendErrorMessage,
|
||||||
|
sendSuccessMessage,
|
||||||
|
} from "../../../../pluginUtils";
|
||||||
|
import { DAYS, MINUTES, SECONDS, noop } from "../../../../utils";
|
||||||
|
import { CasesPlugin } from "../../../Cases/CasesPlugin";
|
||||||
|
import { LogsPlugin } from "../../../Logs/LogsPlugin";
|
||||||
|
import { IgnoredEventType, ModActionsPluginType } from "../../types";
|
||||||
|
import { formatReasonWithAttachments } from "../formatReasonWithAttachments";
|
||||||
|
import { ignoreEvent } from "../ignoreEvent";
|
||||||
|
|
||||||
|
export async function actualMassBanCmd(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
|
userIds: string[],
|
||||||
|
author: GuildMember,
|
||||||
|
) {
|
||||||
|
// Limit to 100 users at once (arbitrary?)
|
||||||
|
if (userIds.length > 100) {
|
||||||
|
sendErrorMessage(pluginData, context, `Can only massban max 100 users at once`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask for ban reason (cleaner this way instead of trying to cram it into the args)
|
||||||
|
sendContextResponse(context, "Ban reason? `cancel` to cancel");
|
||||||
|
const banReasonReply = await waitForReply(
|
||||||
|
pluginData.client,
|
||||||
|
isContextInteraction(context) ? context.channel! : context,
|
||||||
|
author.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!banReasonReply || !banReasonReply.content || banReasonReply.content.toLowerCase().trim() === "cancel") {
|
||||||
|
sendErrorMessage(pluginData, context, "Cancelled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const banReason = formatReasonWithAttachments(banReasonReply.content, [...banReasonReply.attachments.values()]);
|
||||||
|
|
||||||
|
// Verify we can act on each of the users specified
|
||||||
|
for (const userId of userIds) {
|
||||||
|
const member = pluginData.guild.members.cache.get(userId as Snowflake); // TODO: Get members on demand?
|
||||||
|
if (member && !canActOn(pluginData, author, member)) {
|
||||||
|
sendErrorMessage(pluginData, context, "Cannot massban one or more users: insufficient permissions");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show a loading indicator since this can take a while
|
||||||
|
const maxWaitTime = pluginData.state.massbanQueue.timeout * pluginData.state.massbanQueue.length;
|
||||||
|
const maxWaitTimeFormatted = humanizeDurationShort(maxWaitTime, { round: true });
|
||||||
|
const initialLoadingText =
|
||||||
|
pluginData.state.massbanQueue.length === 0
|
||||||
|
? "Banning..."
|
||||||
|
: `Massban queued. Waiting for previous massban to finish (max wait ${maxWaitTimeFormatted}).`;
|
||||||
|
const loadingMsg = await sendContextResponse(context, initialLoadingText);
|
||||||
|
|
||||||
|
const waitTimeStart = performance.now();
|
||||||
|
const waitingInterval = setInterval(() => {
|
||||||
|
const waitTime = humanizeDurationShort(performance.now() - waitTimeStart, { round: true });
|
||||||
|
loadingMsg
|
||||||
|
.edit(`Massban queued. Still waiting for previous massban to finish (waited ${waitTime}).`)
|
||||||
|
.catch(() => clearInterval(waitingInterval));
|
||||||
|
}, 1 * MINUTES);
|
||||||
|
|
||||||
|
pluginData.state.massbanQueue.add(async () => {
|
||||||
|
clearInterval(waitingInterval);
|
||||||
|
|
||||||
|
if (pluginData.state.unloaded) {
|
||||||
|
void loadingMsg.delete().catch(noop);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadingMsg.edit("Banning...").catch(noop);
|
||||||
|
|
||||||
|
// Ban each user and count failed bans (if any)
|
||||||
|
const startTime = performance.now();
|
||||||
|
const failedBans: string[] = [];
|
||||||
|
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||||
|
const messageConfig = isContextInteraction(context)
|
||||||
|
? await pluginData.config.getForInteraction(context)
|
||||||
|
: await pluginData.config.getForChannel(context);
|
||||||
|
const deleteDays = messageConfig.ban_delete_message_days;
|
||||||
|
|
||||||
|
for (const [i, userId] of userIds.entries()) {
|
||||||
|
if (pluginData.state.unloaded) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ignore automatic ban cases and logs
|
||||||
|
// We create our own cases below and post a single "mass banned" log instead
|
||||||
|
ignoreEvent(pluginData, IgnoredEventType.Ban, userId, 30 * MINUTES);
|
||||||
|
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_BAN, userId, 30 * MINUTES);
|
||||||
|
|
||||||
|
await pluginData.guild.bans.create(userId as Snowflake, {
|
||||||
|
deleteMessageSeconds: (deleteDays * DAYS) / SECONDS,
|
||||||
|
reason: banReason,
|
||||||
|
});
|
||||||
|
|
||||||
|
await casesPlugin.createCase({
|
||||||
|
userId,
|
||||||
|
modId: author.id,
|
||||||
|
type: CaseTypes.Ban,
|
||||||
|
reason: `Mass ban: ${banReason}`,
|
||||||
|
postInCaseLogOverride: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
pluginData.state.events.emit("ban", userId, banReason);
|
||||||
|
} catch {
|
||||||
|
failedBans.push(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a status update every 10 bans
|
||||||
|
if ((i + 1) % 10 === 0) {
|
||||||
|
loadingMsg.edit(`Banning... ${i + 1}/${userIds.length}`).catch(noop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalTime = performance.now() - startTime;
|
||||||
|
const formattedTimeTaken = humanizeDurationShort(totalTime, { round: true });
|
||||||
|
|
||||||
|
// Clear loading indicator
|
||||||
|
loadingMsg.delete().catch(noop);
|
||||||
|
|
||||||
|
const successfulBanCount = userIds.length - failedBans.length;
|
||||||
|
if (successfulBanCount === 0) {
|
||||||
|
// All bans failed - don't create a log entry and notify the user
|
||||||
|
sendErrorMessage(pluginData, context, "All bans failed. Make sure the IDs are valid.");
|
||||||
|
} else {
|
||||||
|
// Some or all bans were successful. Create a log entry for the mass ban and notify the user.
|
||||||
|
pluginData.getPlugin(LogsPlugin).logMassBan({
|
||||||
|
mod: author.user,
|
||||||
|
count: successfulBanCount,
|
||||||
|
reason: banReason,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (failedBans.length) {
|
||||||
|
sendSuccessMessage(
|
||||||
|
pluginData,
|
||||||
|
context,
|
||||||
|
`Banned ${successfulBanCount} users in ${formattedTimeTaken}, ${failedBans.length} failed: ${failedBans.join(
|
||||||
|
" ",
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
sendSuccessMessage(
|
||||||
|
pluginData,
|
||||||
|
context,
|
||||||
|
`Banned ${successfulBanCount} users successfully in ${formattedTimeTaken}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
import { ChatInputCommandInteraction, GuildMember, Snowflake, TextBasedChannel } from "discord.js";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
|
import { waitForReply } from "knub/helpers";
|
||||||
|
import { LogType } from "../../../../data/LogType";
|
||||||
|
import { logger } from "../../../../logger";
|
||||||
|
import {
|
||||||
|
canActOn,
|
||||||
|
isContextInteraction,
|
||||||
|
sendContextResponse,
|
||||||
|
sendErrorMessage,
|
||||||
|
sendSuccessMessage,
|
||||||
|
} from "../../../../pluginUtils";
|
||||||
|
import { LogsPlugin } from "../../../Logs/LogsPlugin";
|
||||||
|
import { MutesPlugin } from "../../../Mutes/MutesPlugin";
|
||||||
|
import { ModActionsPluginType } from "../../types";
|
||||||
|
import { formatReasonWithAttachments } from "../formatReasonWithAttachments";
|
||||||
|
|
||||||
|
export async function actualMassMuteCmd(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
|
userIds: string[],
|
||||||
|
author: GuildMember,
|
||||||
|
) {
|
||||||
|
// Limit to 100 users at once (arbitrary?)
|
||||||
|
if (userIds.length > 100) {
|
||||||
|
sendErrorMessage(pluginData, context, `Can only massmute max 100 users at once`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask for mute reason
|
||||||
|
sendContextResponse(context, "Mute reason? `cancel` to cancel");
|
||||||
|
const muteReasonReceived = await waitForReply(
|
||||||
|
pluginData.client,
|
||||||
|
isContextInteraction(context) ? context.channel! : context,
|
||||||
|
author.id,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!muteReasonReceived ||
|
||||||
|
!muteReasonReceived.content ||
|
||||||
|
muteReasonReceived.content.toLowerCase().trim() === "cancel"
|
||||||
|
) {
|
||||||
|
sendErrorMessage(pluginData, context, "Cancelled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const muteReason = formatReasonWithAttachments(muteReasonReceived.content, [
|
||||||
|
...muteReasonReceived.attachments.values(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify we can act upon all users
|
||||||
|
for (const userId of userIds) {
|
||||||
|
const member = pluginData.guild.members.cache.get(userId as Snowflake);
|
||||||
|
if (member && !canActOn(pluginData, author, member)) {
|
||||||
|
sendErrorMessage(pluginData, context, "Cannot massmute one or more users: insufficient permissions");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore automatic mute cases and logs for these users
|
||||||
|
// We'll create our own cases below and post a single "mass muted" log instead
|
||||||
|
userIds.forEach((userId) => {
|
||||||
|
// Use longer timeouts since this can take a while
|
||||||
|
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_MUTE, userId, 120 * 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show loading indicator
|
||||||
|
const loadingMsg = await sendContextResponse(context, "Muting...");
|
||||||
|
|
||||||
|
// Mute everyone and count fails
|
||||||
|
const modId = author.id;
|
||||||
|
const failedMutes: string[] = [];
|
||||||
|
const mutesPlugin = pluginData.getPlugin(MutesPlugin);
|
||||||
|
for (const userId of userIds) {
|
||||||
|
try {
|
||||||
|
await mutesPlugin.muteUser(userId, 0, `Mass mute: ${muteReason}`, {
|
||||||
|
caseArgs: {
|
||||||
|
modId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.info(e);
|
||||||
|
failedMutes.push(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear loading indicator
|
||||||
|
loadingMsg.delete();
|
||||||
|
|
||||||
|
const successfulMuteCount = userIds.length - failedMutes.length;
|
||||||
|
if (successfulMuteCount === 0) {
|
||||||
|
// All mutes failed
|
||||||
|
sendErrorMessage(pluginData, context, "All mutes failed. Make sure the IDs are valid.");
|
||||||
|
} else {
|
||||||
|
// Success on all or some mutes
|
||||||
|
pluginData.getPlugin(LogsPlugin).logMassMute({
|
||||||
|
mod: author.user,
|
||||||
|
count: successfulMuteCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (failedMutes.length) {
|
||||||
|
sendSuccessMessage(
|
||||||
|
pluginData,
|
||||||
|
context,
|
||||||
|
`Muted ${successfulMuteCount} users, ${failedMutes.length} failed: ${failedMutes.join(" ")}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
sendSuccessMessage(pluginData, context, `Muted ${successfulMuteCount} users successfully`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { ChatInputCommandInteraction, GuildMember, Snowflake, TextBasedChannel } from "discord.js";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
|
import { waitForReply } from "knub/helpers";
|
||||||
|
import { CaseTypes } from "../../../../data/CaseTypes";
|
||||||
|
import { LogType } from "../../../../data/LogType";
|
||||||
|
import {
|
||||||
|
isContextInteraction,
|
||||||
|
sendContextResponse,
|
||||||
|
sendErrorMessage,
|
||||||
|
sendSuccessMessage,
|
||||||
|
} from "../../../../pluginUtils";
|
||||||
|
import { CasesPlugin } from "../../../Cases/CasesPlugin";
|
||||||
|
import { LogsPlugin } from "../../../Logs/LogsPlugin";
|
||||||
|
import { IgnoredEventType, ModActionsPluginType } from "../../types";
|
||||||
|
import { formatReasonWithAttachments } from "../formatReasonWithAttachments";
|
||||||
|
import { ignoreEvent } from "../ignoreEvent";
|
||||||
|
import { isBanned } from "../isBanned";
|
||||||
|
|
||||||
|
export async function actualMassUnbanCmd(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
|
userIds: string[],
|
||||||
|
author: GuildMember,
|
||||||
|
) {
|
||||||
|
// Limit to 100 users at once (arbitrary?)
|
||||||
|
if (userIds.length > 100) {
|
||||||
|
sendErrorMessage(pluginData, context, `Can only mass-unban max 100 users at once`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask for unban reason (cleaner this way instead of trying to cram it into the args)
|
||||||
|
sendContextResponse(context, "Unban reason? `cancel` to cancel");
|
||||||
|
const unbanReasonReply = await waitForReply(
|
||||||
|
pluginData.client,
|
||||||
|
isContextInteraction(context) ? context.channel! : context,
|
||||||
|
author.id,
|
||||||
|
);
|
||||||
|
if (!unbanReasonReply || !unbanReasonReply.content || unbanReasonReply.content.toLowerCase().trim() === "cancel") {
|
||||||
|
sendErrorMessage(pluginData, context, "Cancelled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unbanReason = formatReasonWithAttachments(unbanReasonReply.content, [...unbanReasonReply.attachments.values()]);
|
||||||
|
|
||||||
|
// Ignore automatic unban cases and logs for these users
|
||||||
|
// We'll create our own cases below and post a single "mass unbanned" log instead
|
||||||
|
userIds.forEach((userId) => {
|
||||||
|
// Use longer timeouts since this can take a while
|
||||||
|
ignoreEvent(pluginData, IgnoredEventType.Unban, userId, 120 * 1000);
|
||||||
|
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, userId, 120 * 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show a loading indicator since this can take a while
|
||||||
|
const loadingMsg = await sendContextResponse(context, "Unbanning...");
|
||||||
|
|
||||||
|
// Unban each user and count failed unbans (if any)
|
||||||
|
const failedUnbans: Array<{ userId: string; reason: UnbanFailReasons }> = [];
|
||||||
|
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||||
|
for (const userId of userIds) {
|
||||||
|
if (!(await isBanned(pluginData, userId))) {
|
||||||
|
failedUnbans.push({ userId, reason: UnbanFailReasons.NOT_BANNED });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pluginData.guild.bans.remove(userId as Snowflake, unbanReason ?? undefined);
|
||||||
|
|
||||||
|
await casesPlugin.createCase({
|
||||||
|
userId,
|
||||||
|
modId: author.id,
|
||||||
|
type: CaseTypes.Unban,
|
||||||
|
reason: `Mass unban: ${unbanReason}`,
|
||||||
|
postInCaseLogOverride: false,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
failedUnbans.push({ userId, reason: UnbanFailReasons.UNBAN_FAILED });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear loading indicator
|
||||||
|
loadingMsg.delete();
|
||||||
|
|
||||||
|
const successfulUnbanCount = userIds.length - failedUnbans.length;
|
||||||
|
if (successfulUnbanCount === 0) {
|
||||||
|
// All unbans failed - don't create a log entry and notify the user
|
||||||
|
sendErrorMessage(pluginData, context, "All unbans failed. Make sure the IDs are valid and banned.");
|
||||||
|
} else {
|
||||||
|
// Some or all unbans were successful. Create a log entry for the mass unban and notify the user.
|
||||||
|
pluginData.getPlugin(LogsPlugin).logMassUnban({
|
||||||
|
mod: author.user,
|
||||||
|
count: successfulUnbanCount,
|
||||||
|
reason: unbanReason,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (failedUnbans.length) {
|
||||||
|
const notBanned = failedUnbans.filter((x) => x.reason === UnbanFailReasons.NOT_BANNED);
|
||||||
|
const unbanFailed = failedUnbans.filter((x) => x.reason === UnbanFailReasons.UNBAN_FAILED);
|
||||||
|
|
||||||
|
let failedMsg = "";
|
||||||
|
if (notBanned.length > 0) {
|
||||||
|
failedMsg += `${notBanned.length}x ${UnbanFailReasons.NOT_BANNED}:`;
|
||||||
|
notBanned.forEach((fail) => {
|
||||||
|
failedMsg += " " + fail.userId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (unbanFailed.length > 0) {
|
||||||
|
failedMsg += `\n${unbanFailed.length}x ${UnbanFailReasons.UNBAN_FAILED}:`;
|
||||||
|
unbanFailed.forEach((fail) => {
|
||||||
|
failedMsg += " " + fail.userId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendSuccessMessage(
|
||||||
|
pluginData,
|
||||||
|
context,
|
||||||
|
`Unbanned ${successfulUnbanCount} users, ${failedUnbans.length} failed:\n${failedMsg}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
sendSuccessMessage(pluginData, context, `Unbanned ${successfulUnbanCount} users successfully`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UnbanFailReasons {
|
||||||
|
NOT_BANNED = "Not banned",
|
||||||
|
UNBAN_FAILED = "Unban failed",
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { Attachment, ChatInputCommandInteraction, GuildMember, TextBasedChannel, User } from "discord.js";
|
||||||
|
import humanizeDuration from "humanize-duration";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
|
import { ERRORS, RecoverablePluginError } from "../../../../RecoverablePluginError";
|
||||||
|
import { logger } from "../../../../logger";
|
||||||
|
import { sendErrorMessage, sendSuccessMessage } from "../../../../pluginUtils";
|
||||||
|
import {
|
||||||
|
UnknownUser,
|
||||||
|
UserNotificationMethod,
|
||||||
|
asSingleLine,
|
||||||
|
isDiscordAPIError,
|
||||||
|
renderUserUsername,
|
||||||
|
} from "../../../../utils";
|
||||||
|
import { MutesPlugin } from "../../../Mutes/MutesPlugin";
|
||||||
|
import { MuteResult } from "../../../Mutes/types";
|
||||||
|
import { ModActionsPluginType } from "../../types";
|
||||||
|
import { formatReasonWithAttachments } from "../formatReasonWithAttachments";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The actual function run by both !mute and !forcemute.
|
||||||
|
* The only difference between the two commands is in target member validation.
|
||||||
|
*/
|
||||||
|
export async function actualMuteCmd(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
|
user: User | UnknownUser,
|
||||||
|
attachments: Array<Attachment>,
|
||||||
|
mod: GuildMember,
|
||||||
|
ppId?: string,
|
||||||
|
time?: number,
|
||||||
|
reason?: string,
|
||||||
|
contactMethods?: UserNotificationMethod[],
|
||||||
|
) {
|
||||||
|
const timeUntilUnmute = time && humanizeDuration(time);
|
||||||
|
const formattedReason = reason ? formatReasonWithAttachments(reason, attachments) : undefined;
|
||||||
|
|
||||||
|
let muteResult: MuteResult;
|
||||||
|
const mutesPlugin = pluginData.getPlugin(MutesPlugin);
|
||||||
|
|
||||||
|
try {
|
||||||
|
muteResult = await mutesPlugin.muteUser(user.id, time, formattedReason, {
|
||||||
|
contactMethods,
|
||||||
|
caseArgs: {
|
||||||
|
modId: mod.id,
|
||||||
|
ppId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) {
|
||||||
|
sendErrorMessage(pluginData, context, "Could not mute the user: no mute role set in config");
|
||||||
|
} else if (isDiscordAPIError(e) && e.code === 10007) {
|
||||||
|
sendErrorMessage(pluginData, context, "Could not mute the user: unknown member");
|
||||||
|
} else {
|
||||||
|
logger.error(`Failed to mute user ${user.id}: ${e.stack}`);
|
||||||
|
if (user.id == null) {
|
||||||
|
// FIXME: Debug
|
||||||
|
// tslint:disable-next-line:no-console
|
||||||
|
console.trace("[DEBUG] Null user.id for mute");
|
||||||
|
}
|
||||||
|
sendErrorMessage(pluginData, context, "Could not mute the user");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm the action to the moderator
|
||||||
|
let response: string;
|
||||||
|
if (time) {
|
||||||
|
if (muteResult.updatedExistingMute) {
|
||||||
|
response = asSingleLine(`
|
||||||
|
Updated **${renderUserUsername(user)}**'s
|
||||||
|
mute to ${timeUntilUnmute} (Case #${muteResult.case.case_number})
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
response = asSingleLine(`
|
||||||
|
Muted **${renderUserUsername(user)}**
|
||||||
|
for ${timeUntilUnmute} (Case #${muteResult.case.case_number})
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (muteResult.updatedExistingMute) {
|
||||||
|
response = asSingleLine(`
|
||||||
|
Updated **${renderUserUsername(user)}**'s
|
||||||
|
mute to indefinite (Case #${muteResult.case.case_number})
|
||||||
|
`);
|
||||||
|
} else {
|
||||||
|
response = asSingleLine(`
|
||||||
|
Muted **${renderUserUsername(user)}**
|
||||||
|
indefinitely (Case #${muteResult.case.case_number})
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (muteResult.notifyResult.text) response += ` (${muteResult.notifyResult.text})`;
|
||||||
|
sendSuccessMessage(pluginData, context, response);
|
||||||
|
}
|
|
@ -1,12 +1,12 @@
|
||||||
import { Attachment, ChatInputCommandInteraction, TextBasedChannel, User } from "discord.js";
|
import { Attachment, ChatInputCommandInteraction, TextBasedChannel, User } from "discord.js";
|
||||||
import { GuildPluginData } from "knub";
|
import { GuildPluginData } from "knub";
|
||||||
import { CaseTypes } from "../../../data/CaseTypes";
|
import { CaseTypes } from "../../../../data/CaseTypes";
|
||||||
import { sendSuccessMessage } from "../../../pluginUtils";
|
import { sendSuccessMessage } from "../../../../pluginUtils";
|
||||||
import { UnknownUser, renderUserUsername } from "../../../utils";
|
import { UnknownUser, renderUserUsername } from "../../../../utils";
|
||||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
import { CasesPlugin } from "../../../Cases/CasesPlugin";
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
import { LogsPlugin } from "../../../Logs/LogsPlugin";
|
||||||
import { ModActionsPluginType } from "../types";
|
import { ModActionsPluginType } from "../../types";
|
||||||
import { formatReasonWithAttachments } from "./formatReasonWithAttachments";
|
import { formatReasonWithAttachments } from "../formatReasonWithAttachments";
|
||||||
|
|
||||||
export async function actualNoteCmd(
|
export async function actualNoteCmd(
|
||||||
pluginData: GuildPluginData<ModActionsPluginType>,
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { Attachment, ChatInputCommandInteraction, GuildMember, Snowflake, TextBasedChannel, User } from "discord.js";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
|
import { CaseTypes } from "../../../../data/CaseTypes";
|
||||||
|
import { LogType } from "../../../../data/LogType";
|
||||||
|
import { clearExpiringTempban } from "../../../../data/loops/expiringTempbansLoop";
|
||||||
|
import { sendErrorMessage, sendSuccessMessage } from "../../../../pluginUtils";
|
||||||
|
import { UnknownUser } from "../../../../utils";
|
||||||
|
import { CasesPlugin } from "../../../Cases/CasesPlugin";
|
||||||
|
import { LogsPlugin } from "../../../Logs/LogsPlugin";
|
||||||
|
import { IgnoredEventType, ModActionsPluginType } from "../../types";
|
||||||
|
import { formatReasonWithAttachments } from "../formatReasonWithAttachments";
|
||||||
|
import { ignoreEvent } from "../ignoreEvent";
|
||||||
|
|
||||||
|
export async function actualUnbanCmd(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
|
authorId: string,
|
||||||
|
user: User | UnknownUser,
|
||||||
|
reason: string,
|
||||||
|
attachments: Array<Attachment>,
|
||||||
|
mod: GuildMember,
|
||||||
|
) {
|
||||||
|
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, user.id);
|
||||||
|
const formattedReason = formatReasonWithAttachments(reason, attachments);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ignoreEvent(pluginData, IgnoredEventType.Unban, user.id);
|
||||||
|
await pluginData.guild.bans.remove(user.id as Snowflake, formattedReason ?? undefined);
|
||||||
|
} catch {
|
||||||
|
sendErrorMessage(pluginData, context, "Failed to unban member; are you sure they're banned?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a case
|
||||||
|
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||||
|
const createdCase = await casesPlugin.createCase({
|
||||||
|
userId: user.id,
|
||||||
|
modId: mod.id,
|
||||||
|
type: CaseTypes.Unban,
|
||||||
|
reason: formattedReason,
|
||||||
|
ppId: mod.id !== authorId ? authorId : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete the tempban, if one exists
|
||||||
|
const tempban = await pluginData.state.tempbans.findExistingTempbanForUserId(user.id);
|
||||||
|
if (tempban) {
|
||||||
|
clearExpiringTempban(tempban);
|
||||||
|
await pluginData.state.tempbans.clear(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm the action
|
||||||
|
sendSuccessMessage(pluginData, context, `Member unbanned (Case #${createdCase.case_number})`);
|
||||||
|
|
||||||
|
// Log the action
|
||||||
|
pluginData.getPlugin(LogsPlugin).logMemberUnban({
|
||||||
|
mod: mod.user,
|
||||||
|
userId: user.id,
|
||||||
|
caseNumber: createdCase.case_number,
|
||||||
|
reason: formattedReason ?? "",
|
||||||
|
});
|
||||||
|
|
||||||
|
pluginData.state.events.emit("unban", user.id);
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { ChatInputCommandInteraction, TextBasedChannel } from "discord.js";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
|
import { sendErrorMessage, sendSuccessMessage } from "../../../../pluginUtils";
|
||||||
|
import { ModActionsPluginType } from "../../types";
|
||||||
|
|
||||||
|
export async function actualUnhideCaseCmd(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
|
caseNumbers: number[],
|
||||||
|
) {
|
||||||
|
const failed: number[] = [];
|
||||||
|
|
||||||
|
for (const num of caseNumbers) {
|
||||||
|
const theCase = await pluginData.state.cases.findByCaseNumber(num);
|
||||||
|
if (!theCase) {
|
||||||
|
failed.push(num);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await pluginData.state.cases.setHidden(theCase.id, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed.length === caseNumbers.length) {
|
||||||
|
sendErrorMessage(pluginData, context, "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 = caseNumbers.length - failed.length;
|
||||||
|
sendSuccessMessage(
|
||||||
|
pluginData,
|
||||||
|
context,
|
||||||
|
`${amt} case${amt === 1 ? " is" : "s are"} no longer hidden!${failedAddendum}`,
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Attachment, ChatInputCommandInteraction, GuildMember, TextBasedChannel, User } from "discord.js";
|
||||||
|
import humanizeDuration from "humanize-duration";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
|
import { sendErrorMessage, sendSuccessMessage } from "../../../../pluginUtils";
|
||||||
|
import { UnknownUser, asSingleLine, renderUserUsername } from "../../../../utils";
|
||||||
|
import { MutesPlugin } from "../../../Mutes/MutesPlugin";
|
||||||
|
import { ModActionsPluginType } from "../../types";
|
||||||
|
import { formatReasonWithAttachments } from "../formatReasonWithAttachments";
|
||||||
|
|
||||||
|
export async function actualUnmuteCmd(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
|
user: User | UnknownUser,
|
||||||
|
attachments: Array<Attachment>,
|
||||||
|
mod: GuildMember,
|
||||||
|
ppId?: string,
|
||||||
|
time?: number,
|
||||||
|
reason?: string,
|
||||||
|
) {
|
||||||
|
const parsedReason = reason ? formatReasonWithAttachments(reason, attachments) : undefined;
|
||||||
|
|
||||||
|
const mutesPlugin = pluginData.getPlugin(MutesPlugin);
|
||||||
|
const result = await mutesPlugin.unmuteUser(user.id, time, {
|
||||||
|
modId: mod.id,
|
||||||
|
ppId: ppId ?? undefined,
|
||||||
|
reason: parsedReason,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
sendErrorMessage(pluginData, context, "User is not muted!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm the action to the moderator
|
||||||
|
if (time) {
|
||||||
|
const timeUntilUnmute = time && humanizeDuration(time);
|
||||||
|
sendSuccessMessage(
|
||||||
|
pluginData,
|
||||||
|
context,
|
||||||
|
asSingleLine(`
|
||||||
|
Unmuting **${renderUserUsername(user)}**
|
||||||
|
in ${timeUntilUnmute} (Case #${result.case.case_number})
|
||||||
|
`),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
sendSuccessMessage(
|
||||||
|
pluginData,
|
||||||
|
context,
|
||||||
|
asSingleLine(`
|
||||||
|
Unmuted **${renderUserUsername(user)}**
|
||||||
|
(Case #${result.case.case_number})
|
||||||
|
`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { Attachment, ChatInputCommandInteraction, GuildMember, TextBasedChannel } from "discord.js";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
|
import { CaseTypes } from "../../../../data/CaseTypes";
|
||||||
|
import { sendErrorMessage, sendSuccessMessage } from "../../../../pluginUtils";
|
||||||
|
import { UserNotificationMethod, renderUserUsername } from "../../../../utils";
|
||||||
|
import { waitForButtonConfirm } from "../../../../utils/waitForInteraction";
|
||||||
|
import { CasesPlugin } from "../../../Cases/CasesPlugin";
|
||||||
|
import { ModActionsPluginType } from "../../types";
|
||||||
|
import { formatReasonWithAttachments } from "../formatReasonWithAttachments";
|
||||||
|
import { warnMember } from "../warnMember";
|
||||||
|
|
||||||
|
export async function actualWarnCmd(
|
||||||
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
|
authorId: string,
|
||||||
|
mod: GuildMember,
|
||||||
|
memberToWarn: GuildMember,
|
||||||
|
reason: string,
|
||||||
|
attachments: Attachment[],
|
||||||
|
contactMethods?: UserNotificationMethod[],
|
||||||
|
) {
|
||||||
|
const config = pluginData.config.get();
|
||||||
|
const formattedReason = formatReasonWithAttachments(reason, attachments);
|
||||||
|
|
||||||
|
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||||
|
const priorWarnAmount = await casesPlugin.getCaseTypeAmountForUserId(memberToWarn.id, CaseTypes.Warn);
|
||||||
|
if (config.warn_notify_enabled && priorWarnAmount >= config.warn_notify_threshold) {
|
||||||
|
const reply = await waitForButtonConfirm(
|
||||||
|
context,
|
||||||
|
{ content: config.warn_notify_message.replace("{priorWarnings}", `${priorWarnAmount}`) },
|
||||||
|
{ confirmText: "Yes", cancelText: "No", restrictToId: authorId },
|
||||||
|
);
|
||||||
|
if (!reply) {
|
||||||
|
sendErrorMessage(pluginData, context, "Warn cancelled by moderator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const warnResult = await warnMember(pluginData, memberToWarn, formattedReason, {
|
||||||
|
contactMethods,
|
||||||
|
caseArgs: {
|
||||||
|
modId: mod.id,
|
||||||
|
ppId: mod.id !== authorId ? authorId : undefined,
|
||||||
|
reason: formattedReason,
|
||||||
|
},
|
||||||
|
retryPromptContext: context,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (warnResult.status === "failed") {
|
||||||
|
sendErrorMessage(pluginData, context, "Failed to warn user");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageResultText = warnResult.notifyResult.text ? ` (${warnResult.notifyResult.text})` : "";
|
||||||
|
|
||||||
|
sendSuccessMessage(
|
||||||
|
pluginData,
|
||||||
|
context,
|
||||||
|
`Warned **${renderUserUsername(memberToWarn.user)}** (Case #${warnResult.case.case_number})${messageResultText}`,
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,110 +0,0 @@
|
||||||
import { GuildMember, GuildTextBasedChannel } from "discord.js";
|
|
||||||
import { GuildPluginData } from "knub";
|
|
||||||
import { hasPermission } from "knub/helpers";
|
|
||||||
import { LogType } from "../../../data/LogType";
|
|
||||||
import { canActOn, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
|
||||||
import { DAYS, SECONDS, errorMessage, renderUserUsername, resolveMember, resolveUser } from "../../../utils";
|
|
||||||
import { IgnoredEventType, ModActionsPluginType } from "../types";
|
|
||||||
import { formatReasonWithAttachments } from "./formatReasonWithAttachments";
|
|
||||||
import { ignoreEvent } from "./ignoreEvent";
|
|
||||||
import { isBanned } from "./isBanned";
|
|
||||||
import { kickMember } from "./kickMember";
|
|
||||||
import { readContactMethodsFromArgs } from "./readContactMethodsFromArgs";
|
|
||||||
|
|
||||||
export async function actualKickMemberCmd(
|
|
||||||
pluginData: GuildPluginData<ModActionsPluginType>,
|
|
||||||
msg,
|
|
||||||
args: {
|
|
||||||
user: string;
|
|
||||||
reason: string;
|
|
||||||
mod: GuildMember;
|
|
||||||
notify?: string;
|
|
||||||
"notify-channel"?: GuildTextBasedChannel;
|
|
||||||
clean?: boolean;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const user = await resolveUser(pluginData.client, args.user);
|
|
||||||
if (!user.id) {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, `User not found`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const memberToKick = await resolveMember(pluginData.client, pluginData.guild, user.id);
|
|
||||||
|
|
||||||
if (!memberToKick) {
|
|
||||||
const banned = await isBanned(pluginData, user.id);
|
|
||||||
if (banned) {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, `User is banned`);
|
|
||||||
} else {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, `User not found on the server`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure we're allowed to kick this member
|
|
||||||
if (!canActOn(pluginData, msg.member, memberToKick)) {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "Cannot kick: 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(await pluginData.config.getForMessage(msg), "can_act_as_other"))) {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "You don't have permission to use -mod");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mod = args.mod;
|
|
||||||
}
|
|
||||||
|
|
||||||
let contactMethods;
|
|
||||||
try {
|
|
||||||
contactMethods = readContactMethodsFromArgs(args);
|
|
||||||
} catch (e) {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, e.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reason = formatReasonWithAttachments(args.reason, msg.attachments);
|
|
||||||
|
|
||||||
const kickResult = await kickMember(pluginData, memberToKick, reason, {
|
|
||||||
contactMethods,
|
|
||||||
caseArgs: {
|
|
||||||
modId: mod.id,
|
|
||||||
ppId: mod.id !== msg.author.id ? msg.author.id : null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (args.clean) {
|
|
||||||
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_BAN, memberToKick.id);
|
|
||||||
ignoreEvent(pluginData, IgnoredEventType.Ban, memberToKick.id);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await memberToKick.ban({ deleteMessageSeconds: (1 * DAYS) / SECONDS, reason: "kick -clean" });
|
|
||||||
} catch {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "Failed to ban the user to clean messages (-clean)");
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, memberToKick.id);
|
|
||||||
ignoreEvent(pluginData, IgnoredEventType.Unban, memberToKick.id);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await pluginData.guild.bans.remove(memberToKick.id, "kick -clean");
|
|
||||||
} catch {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "Failed to unban the user after banning them (-clean)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (kickResult.status === "failed") {
|
|
||||||
msg.channel.send(errorMessage(`Failed to kick user`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm the action to the moderator
|
|
||||||
let response = `Kicked **${renderUserUsername(memberToKick.user)}** (Case #${kickResult.case.case_number})`;
|
|
||||||
|
|
||||||
if (kickResult.notifyResult.text) response += ` (${kickResult.notifyResult.text})`;
|
|
||||||
sendSuccessMessage(pluginData, msg.channel, response);
|
|
||||||
}
|
|
|
@ -1,114 +0,0 @@
|
||||||
import { GuildMember, GuildTextBasedChannel, Message, User } from "discord.js";
|
|
||||||
import humanizeDuration from "humanize-duration";
|
|
||||||
import { GuildPluginData } from "knub";
|
|
||||||
import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError";
|
|
||||||
import { logger } from "../../../logger";
|
|
||||||
import { hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
|
||||||
import { UnknownUser, asSingleLine, isDiscordAPIError, renderUserUsername } from "../../../utils";
|
|
||||||
import { MutesPlugin } from "../../Mutes/MutesPlugin";
|
|
||||||
import { MuteResult } from "../../Mutes/types";
|
|
||||||
import { ModActionsPluginType } from "../types";
|
|
||||||
import { formatReasonWithAttachments } from "./formatReasonWithAttachments";
|
|
||||||
import { readContactMethodsFromArgs } from "./readContactMethodsFromArgs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The actual function run by both !mute and !forcemute.
|
|
||||||
* The only difference between the two commands is in target member validation.
|
|
||||||
*/
|
|
||||||
export async function actualMuteUserCmd(
|
|
||||||
pluginData: GuildPluginData<ModActionsPluginType>,
|
|
||||||
user: User | UnknownUser,
|
|
||||||
msg: Message,
|
|
||||||
args: {
|
|
||||||
time?: number;
|
|
||||||
reason?: string;
|
|
||||||
mod: GuildMember;
|
|
||||||
notify?: string;
|
|
||||||
"notify-channel"?: GuildTextBasedChannel;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
// The moderator who did the action is the message author or, if used, the specified -mod
|
|
||||||
let mod: GuildMember = msg.member!;
|
|
||||||
let pp: User | null = null;
|
|
||||||
|
|
||||||
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;
|
|
||||||
pp = msg.author;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeUntilUnmute = args.time && humanizeDuration(args.time);
|
|
||||||
const reason = args.reason ? formatReasonWithAttachments(args.reason, [...msg.attachments.values()]) : undefined;
|
|
||||||
|
|
||||||
let muteResult: MuteResult;
|
|
||||||
const mutesPlugin = pluginData.getPlugin(MutesPlugin);
|
|
||||||
|
|
||||||
let contactMethods;
|
|
||||||
try {
|
|
||||||
contactMethods = readContactMethodsFromArgs(args);
|
|
||||||
} catch (e) {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, e.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
muteResult = await mutesPlugin.muteUser(user.id, args.time, reason, {
|
|
||||||
contactMethods,
|
|
||||||
caseArgs: {
|
|
||||||
modId: mod.id,
|
|
||||||
ppId: pp ? pp.id : undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "Could not mute the user: no mute role set in config");
|
|
||||||
} else if (isDiscordAPIError(e) && e.code === 10007) {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "Could not mute the user: unknown member");
|
|
||||||
} else {
|
|
||||||
logger.error(`Failed to mute user ${user.id}: ${e.stack}`);
|
|
||||||
if (user.id == null) {
|
|
||||||
// FIXME: Debug
|
|
||||||
// tslint:disable-next-line:no-console
|
|
||||||
console.trace("[DEBUG] Null user.id for mute");
|
|
||||||
}
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "Could not mute the user");
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm the action to the moderator
|
|
||||||
let response: string;
|
|
||||||
if (args.time) {
|
|
||||||
if (muteResult.updatedExistingMute) {
|
|
||||||
response = asSingleLine(`
|
|
||||||
Updated **${renderUserUsername(user)}**'s
|
|
||||||
mute to ${timeUntilUnmute} (Case #${muteResult.case.case_number})
|
|
||||||
`);
|
|
||||||
} else {
|
|
||||||
response = asSingleLine(`
|
|
||||||
Muted **${renderUserUsername(user)}**
|
|
||||||
for ${timeUntilUnmute} (Case #${muteResult.case.case_number})
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (muteResult.updatedExistingMute) {
|
|
||||||
response = asSingleLine(`
|
|
||||||
Updated **${renderUserUsername(user)}**'s
|
|
||||||
mute to indefinite (Case #${muteResult.case.case_number})
|
|
||||||
`);
|
|
||||||
} else {
|
|
||||||
response = asSingleLine(`
|
|
||||||
Muted **${renderUserUsername(user)}**
|
|
||||||
indefinitely (Case #${muteResult.case.case_number})
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (muteResult.notifyResult.text) response += ` (${muteResult.notifyResult.text})`;
|
|
||||||
sendSuccessMessage(pluginData, msg.channel, response);
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
import { GuildMember, Message, User } from "discord.js";
|
|
||||||
import humanizeDuration from "humanize-duration";
|
|
||||||
import { GuildPluginData } from "knub";
|
|
||||||
import { hasPermission, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
|
||||||
import { MutesPlugin } from "../../../plugins/Mutes/MutesPlugin";
|
|
||||||
import { UnknownUser, asSingleLine, renderUserUsername } from "../../../utils";
|
|
||||||
import { ModActionsPluginType } from "../types";
|
|
||||||
import { formatReasonWithAttachments } from "./formatReasonWithAttachments";
|
|
||||||
|
|
||||||
export async function actualUnmuteCmd(
|
|
||||||
pluginData: GuildPluginData<ModActionsPluginType>,
|
|
||||||
user: User | UnknownUser,
|
|
||||||
msg: Message,
|
|
||||||
args: { time?: number; reason?: string; mod?: GuildMember },
|
|
||||||
) {
|
|
||||||
// The moderator who did the action is the message author or, if used, the specified -mod
|
|
||||||
let mod = msg.author;
|
|
||||||
let pp: User | null = null;
|
|
||||||
|
|
||||||
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.user;
|
|
||||||
pp = msg.author;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reason = args.reason ? formatReasonWithAttachments(args.reason, [...msg.attachments.values()]) : undefined;
|
|
||||||
|
|
||||||
const mutesPlugin = pluginData.getPlugin(MutesPlugin);
|
|
||||||
const result = await mutesPlugin.unmuteUser(user.id, args.time, {
|
|
||||||
modId: mod.id,
|
|
||||||
ppId: pp ? pp.id : undefined,
|
|
||||||
reason,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
sendErrorMessage(pluginData, msg.channel, "User is not muted!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm the action to the moderator
|
|
||||||
if (args.time) {
|
|
||||||
const timeUntilUnmute = args.time && humanizeDuration(args.time);
|
|
||||||
sendSuccessMessage(
|
|
||||||
pluginData,
|
|
||||||
msg.channel,
|
|
||||||
asSingleLine(`
|
|
||||||
Unmuting **${renderUserUsername(user)}**
|
|
||||||
in ${timeUntilUnmute} (Case #${result.case.case_number})
|
|
||||||
`),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
sendSuccessMessage(
|
|
||||||
pluginData,
|
|
||||||
msg.channel,
|
|
||||||
asSingleLine(`
|
|
||||||
Unmuted **${renderUserUsername(user)}**
|
|
||||||
(Case #${result.case.case_number})
|
|
||||||
`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +1,53 @@
|
||||||
import { Message } from "discord.js";
|
import { Attachment, ChatInputCommandInteraction, TextBasedChannel, User } from "discord.js";
|
||||||
|
import { GuildPluginData } from "knub";
|
||||||
import { CaseTypes } from "../../../data/CaseTypes";
|
import { CaseTypes } from "../../../data/CaseTypes";
|
||||||
import { Case } from "../../../data/entities/Case";
|
import { Case } from "../../../data/entities/Case";
|
||||||
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
||||||
import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin";
|
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||||
|
import { ModActionsPluginType } from "../types";
|
||||||
import { formatReasonWithAttachments } from "./formatReasonWithAttachments";
|
import { formatReasonWithAttachments } from "./formatReasonWithAttachments";
|
||||||
|
|
||||||
export async function updateCase(pluginData, msg: Message, args) {
|
export async function updateCase(
|
||||||
let theCase: Case | undefined;
|
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||||
if (args.caseNumber != null) {
|
context: TextBasedChannel | ChatInputCommandInteraction,
|
||||||
theCase = await pluginData.state.cases.findByCaseNumber(args.caseNumber);
|
author: User,
|
||||||
|
caseNumber?: number,
|
||||||
|
note?: string,
|
||||||
|
attachments: Attachment[] = [],
|
||||||
|
) {
|
||||||
|
let theCase: Case | null;
|
||||||
|
if (caseNumber != null) {
|
||||||
|
theCase = await pluginData.state.cases.findByCaseNumber(caseNumber);
|
||||||
} else {
|
} else {
|
||||||
theCase = await pluginData.state.cases.findLatestByModId(msg.author.id);
|
theCase = await pluginData.state.cases.findLatestByModId(author.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!theCase) {
|
if (!theCase) {
|
||||||
sendErrorMessage(pluginData, msg.channel, "Case not found");
|
sendErrorMessage(pluginData, context, "Case not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!args.note && msg.attachments.size === 0) {
|
if (!note && attachments.length === 0) {
|
||||||
sendErrorMessage(pluginData, msg.channel, "Text or attachment required");
|
sendErrorMessage(pluginData, context, "Text or attachment required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const note = formatReasonWithAttachments(args.note, [...msg.attachments.values()]);
|
const formattedNote = formatReasonWithAttachments(note ?? "", attachments);
|
||||||
|
|
||||||
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||||
await casesPlugin.createCaseNote({
|
await casesPlugin.createCaseNote({
|
||||||
caseId: theCase.id,
|
caseId: theCase.id,
|
||||||
modId: msg.author.id,
|
modId: author.id,
|
||||||
body: note,
|
body: formattedNote,
|
||||||
});
|
});
|
||||||
|
|
||||||
pluginData.getPlugin(LogsPlugin).logCaseUpdate({
|
pluginData.getPlugin(LogsPlugin).logCaseUpdate({
|
||||||
mod: msg.author,
|
mod: author,
|
||||||
caseNumber: theCase.case_number,
|
caseNumber: theCase.case_number,
|
||||||
caseType: CaseTypes[theCase.type],
|
caseType: CaseTypes[theCase.type],
|
||||||
note,
|
note: formattedNote,
|
||||||
});
|
});
|
||||||
|
|
||||||
sendSuccessMessage(pluginData, msg.channel, `Case \`#${theCase.case_number}\` updated`);
|
sendSuccessMessage(pluginData, context, `Case \`#${theCase.case_number}\` updated`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { GuildMember, Snowflake } from "discord.js";
|
import { GuildMember, Snowflake } from "discord.js";
|
||||||
import { GuildPluginData } from "knub";
|
import { GuildPluginData } from "knub";
|
||||||
import { CaseTypes } from "../../../data/CaseTypes";
|
import { CaseTypes } from "../../../data/CaseTypes";
|
||||||
|
import { isContextInteraction } from "../../../pluginUtils";
|
||||||
import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
|
import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
|
||||||
import { UserNotificationResult, createUserNotificationError, notifyUser, resolveUser, ucfirst } from "../../../utils";
|
import { UserNotificationResult, createUserNotificationError, notifyUser, resolveUser, ucfirst } from "../../../utils";
|
||||||
import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects";
|
import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects";
|
||||||
|
@ -39,20 +40,23 @@ export async function warnMember(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!notifyResult.success) {
|
if (!notifyResult.success) {
|
||||||
if (warnOptions.retryPromptChannel && pluginData.guild.channels.resolve(warnOptions.retryPromptChannel.id)) {
|
const contextIsChannel = warnOptions.retryPromptContext && !isContextInteraction(warnOptions.retryPromptContext);
|
||||||
const reply = await waitForButtonConfirm(
|
const isValidChannel = contextIsChannel && pluginData.guild.channels.resolve(warnOptions.retryPromptContext!.id);
|
||||||
warnOptions.retryPromptChannel,
|
|
||||||
{ content: "Failed to message the user. Log the warning anyway?" },
|
|
||||||
{ confirmText: "Yes", cancelText: "No", restrictToId: warnOptions.caseArgs?.modId },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!reply) {
|
if (!warnOptions.retryPromptContext || !isValidChannel) {
|
||||||
return {
|
return {
|
||||||
status: "failed",
|
status: "failed",
|
||||||
error: "Failed to message user",
|
error: "Failed to message user",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
const reply = await waitForButtonConfirm(
|
||||||
|
warnOptions.retryPromptContext,
|
||||||
|
{ content: "Failed to message the user. Log the warning anyway?" },
|
||||||
|
{ confirmText: "Yes", cancelText: "No", restrictToId: warnOptions.caseArgs?.modId },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!reply) {
|
||||||
return {
|
return {
|
||||||
status: "failed",
|
status: "failed",
|
||||||
error: "Failed to message user",
|
error: "Failed to message user",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { GuildTextBasedChannel } from "discord.js";
|
import { ChatInputCommandInteraction, TextBasedChannel } from "discord.js";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import * as t from "io-ts";
|
import * as t from "io-ts";
|
||||||
import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand, guildPluginSlashGroup } from "knub";
|
import { BasePluginType, guildPluginEventListener, guildPluginMessageCommand, guildPluginSlashGroup } from "knub";
|
||||||
|
@ -127,7 +127,7 @@ export type WarnMemberNotifyRetryCallback = () => boolean | Promise<boolean>;
|
||||||
export interface WarnOptions {
|
export interface WarnOptions {
|
||||||
caseArgs?: Partial<CaseArgs> | null;
|
caseArgs?: Partial<CaseArgs> | null;
|
||||||
contactMethods?: UserNotificationMethod[] | null;
|
contactMethods?: UserNotificationMethod[] | null;
|
||||||
retryPromptChannel?: GuildTextBasedChannel | null;
|
retryPromptContext?: TextBasedChannel | ChatInputCommandInteraction | null;
|
||||||
isAutomodAction?: boolean;
|
isAutomodAction?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
ChatInputCommandInteraction,
|
||||||
Client,
|
Client,
|
||||||
Message,
|
Message,
|
||||||
MessageCreateOptions,
|
MessageCreateOptions,
|
||||||
|
@ -9,6 +10,7 @@ import {
|
||||||
TextBasedChannel,
|
TextBasedChannel,
|
||||||
User,
|
User,
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
|
import { sendContextResponse } from "../pluginUtils";
|
||||||
import { MINUTES, noop } from "../utils";
|
import { MINUTES, noop } from "../utils";
|
||||||
import { Awaitable } from "./typeUtils";
|
import { Awaitable } from "./typeUtils";
|
||||||
import Timeout = NodeJS.Timeout;
|
import Timeout = NodeJS.Timeout;
|
||||||
|
@ -27,14 +29,14 @@ const defaultOpts: PaginateMessageOpts = {
|
||||||
|
|
||||||
export async function createPaginatedMessage(
|
export async function createPaginatedMessage(
|
||||||
client: Client,
|
client: Client,
|
||||||
channel: TextBasedChannel | User,
|
context: TextBasedChannel | User | ChatInputCommandInteraction,
|
||||||
totalPages: number,
|
totalPages: number,
|
||||||
loadPageFn: LoadPageFn,
|
loadPageFn: LoadPageFn,
|
||||||
opts: Partial<PaginateMessageOpts> = {},
|
opts: Partial<PaginateMessageOpts> = {},
|
||||||
): Promise<Message> {
|
): Promise<Message> {
|
||||||
const fullOpts = { ...defaultOpts, ...opts } as PaginateMessageOpts;
|
const fullOpts = { ...defaultOpts, ...opts } as PaginateMessageOpts;
|
||||||
const firstPageContent = await loadPageFn(1);
|
const firstPageContent = await loadPageFn(1);
|
||||||
const message = await channel.send(firstPageContent);
|
const message = await sendContextResponse(context, firstPageContent);
|
||||||
|
|
||||||
let page = 1;
|
let page = 1;
|
||||||
let pageLoadId = 0; // Used to avoid race conditions when rapidly switching pages
|
let pageLoadId = 0; // Used to avoid race conditions when rapidly switching pages
|
||||||
|
|
20
backend/src/utils/multipleSlashOptions.ts
Normal file
20
backend/src/utils/multipleSlashOptions.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { AttachmentSlashCommandOption, slashOptions } from "knub";
|
||||||
|
|
||||||
|
type AttachmentSlashOptions = Omit<AttachmentSlashCommandOption, "type" | "resolveValue" | "getExtraAPIProps">;
|
||||||
|
|
||||||
|
export function generateAttachmentSlashOptions(amount: number, options: AttachmentSlashOptions) {
|
||||||
|
return new Array(amount).fill(0).map((_, i) => {
|
||||||
|
return slashOptions.attachment({
|
||||||
|
name: amount > 1 ? `${options.name}${i + 1}` : options.name,
|
||||||
|
description: options.description,
|
||||||
|
required: options.required ?? false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function retrieveMultipleOptions(amount: number, options: any, name: string) {
|
||||||
|
return new Array(amount)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => options[amount > 1 ? `${name}${i + 1}` : name])
|
||||||
|
.filter((a) => a);
|
||||||
|
}
|
|
@ -2,22 +2,26 @@ import {
|
||||||
ActionRowBuilder,
|
ActionRowBuilder,
|
||||||
ButtonBuilder,
|
ButtonBuilder,
|
||||||
ButtonStyle,
|
ButtonStyle,
|
||||||
GuildTextBasedChannel,
|
ChatInputCommandInteraction,
|
||||||
MessageActionRowComponentBuilder,
|
MessageActionRowComponentBuilder,
|
||||||
MessageComponentInteraction,
|
MessageComponentInteraction,
|
||||||
MessageCreateOptions,
|
MessageCreateOptions,
|
||||||
|
TextBasedChannel,
|
||||||
|
User,
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { isContextInteraction } from "../pluginUtils";
|
||||||
import { noop } from "../utils";
|
import { noop } from "../utils";
|
||||||
|
|
||||||
export async function waitForButtonConfirm(
|
export async function waitForButtonConfirm(
|
||||||
channel: GuildTextBasedChannel,
|
context: TextBasedChannel | User | ChatInputCommandInteraction,
|
||||||
toPost: MessageCreateOptions,
|
toPost: MessageCreateOptions,
|
||||||
options?: WaitForOptions,
|
options?: WaitForOptions,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
const idMod = `${channel.guild.id}-${moment.utc().valueOf()}`;
|
const contextIsInteraction = isContextInteraction(context);
|
||||||
|
const idMod = `${context.id}-${moment.utc().valueOf()}`;
|
||||||
const row = new ActionRowBuilder<MessageActionRowComponentBuilder>().addComponents([
|
const row = new ActionRowBuilder<MessageActionRowComponentBuilder>().addComponents([
|
||||||
new ButtonBuilder()
|
new ButtonBuilder()
|
||||||
.setStyle(ButtonStyle.Success)
|
.setStyle(ButtonStyle.Success)
|
||||||
|
@ -29,7 +33,9 @@ export async function waitForButtonConfirm(
|
||||||
.setLabel(options?.cancelText || "Cancel")
|
.setLabel(options?.cancelText || "Cancel")
|
||||||
.setCustomId(`cancelButton:${idMod}:${uuidv4()}`),
|
.setCustomId(`cancelButton:${idMod}:${uuidv4()}`),
|
||||||
]);
|
]);
|
||||||
const message = await channel.send({ ...toPost, components: [row] });
|
const sendMethod = contextIsInteraction ? (context.replied ? "followUp" : "reply") : "send";
|
||||||
|
const extraParameters = contextIsInteraction ? { fetchReply: true } : {};
|
||||||
|
const message = await context[sendMethod]({ ...toPost, components: [row], ...extraParameters });
|
||||||
|
|
||||||
const collector = message.createMessageComponentCollector({ time: 10000 });
|
const collector = message.createMessageComponentCollector({ time: 10000 });
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue