Allow certain or all roles to be removed upon mute and readded on unmute (#140)
This commit is contained in:
parent
8e812aab2f
commit
a13b0b6fda
12 changed files with 135 additions and 27 deletions
|
@ -34,7 +34,7 @@ export class GuildMutes extends BaseGuildRepository {
|
||||||
return mute != null;
|
return mute != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addMute(userId, expiryTime): Promise<Mute> {
|
async addMute(userId, expiryTime, rolesToRestore?: string[]): Promise<Mute> {
|
||||||
const expiresAt = expiryTime
|
const expiresAt = expiryTime
|
||||||
? moment
|
? moment
|
||||||
.utc()
|
.utc()
|
||||||
|
@ -46,12 +46,13 @@ export class GuildMutes extends BaseGuildRepository {
|
||||||
guild_id: this.guildId,
|
guild_id: this.guildId,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
expires_at: expiresAt,
|
expires_at: expiresAt,
|
||||||
|
roles_to_restore: rolesToRestore ?? [],
|
||||||
});
|
});
|
||||||
|
|
||||||
return (await this.mutes.findOne({ where: result.identifiers[0] }))!;
|
return (await this.mutes.findOne({ where: result.identifiers[0] }))!;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateExpiryTime(userId, newExpiryTime) {
|
async updateExpiryTime(userId, newExpiryTime, rolesToRestore?: string[]) {
|
||||||
const expiresAt = newExpiryTime
|
const expiresAt = newExpiryTime
|
||||||
? moment
|
? moment
|
||||||
.utc()
|
.utc()
|
||||||
|
@ -59,15 +60,28 @@ export class GuildMutes extends BaseGuildRepository {
|
||||||
.format("YYYY-MM-DD HH:mm:ss")
|
.format("YYYY-MM-DD HH:mm:ss")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return this.mutes.update(
|
if (rolesToRestore && rolesToRestore.length) {
|
||||||
{
|
return this.mutes.update(
|
||||||
guild_id: this.guildId,
|
{
|
||||||
user_id: userId,
|
guild_id: this.guildId,
|
||||||
},
|
user_id: userId,
|
||||||
{
|
},
|
||||||
expires_at: expiresAt,
|
{
|
||||||
},
|
expires_at: expiresAt,
|
||||||
);
|
roles_to_restore: rolesToRestore,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return this.mutes.update(
|
||||||
|
{
|
||||||
|
guild_id: this.guildId,
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expires_at: expiresAt,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getActiveMutes(): Promise<Mute[]> {
|
async getActiveMutes(): Promise<Mute[]> {
|
||||||
|
|
|
@ -15,4 +15,6 @@ export class Mute {
|
||||||
@Column({ type: String, nullable: true }) expires_at: string | null;
|
@Column({ type: String, nullable: true }) expires_at: string | null;
|
||||||
|
|
||||||
@Column() case_id: number;
|
@Column() case_id: number;
|
||||||
|
|
||||||
|
@Column("simple-array") roles_to_restore: string[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
|
||||||
|
|
||||||
|
export class CreateRestoredRolesColumn1608608903570 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.addColumn(
|
||||||
|
"mutes",
|
||||||
|
new TableColumn({
|
||||||
|
name: "roles_to_restore",
|
||||||
|
type: "text",
|
||||||
|
isNullable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.dropColumn("mutes", "roles_to_restore");
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,8 @@ export const MuteAction = automodAction({
|
||||||
duration: tNullable(tDelayString),
|
duration: tNullable(tDelayString),
|
||||||
notify: tNullable(t.string),
|
notify: tNullable(t.string),
|
||||||
notifyChannel: tNullable(t.string),
|
notifyChannel: tNullable(t.string),
|
||||||
|
remove_roles_on_mute: tNullable(t.union([t.boolean, t.array(t.string)])),
|
||||||
|
restore_roles_on_mute: tNullable(t.union([t.boolean, t.array(t.string)])),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
defaultConfig: {
|
defaultConfig: {
|
||||||
|
@ -32,6 +34,8 @@ export const MuteAction = automodAction({
|
||||||
const duration = actionConfig.duration ? convertDelayStringToMS(actionConfig.duration)! : undefined;
|
const duration = actionConfig.duration ? convertDelayStringToMS(actionConfig.duration)! : undefined;
|
||||||
const reason = actionConfig.reason || "Muted automatically";
|
const reason = actionConfig.reason || "Muted automatically";
|
||||||
const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined;
|
const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined;
|
||||||
|
const rolesToRemove = actionConfig.remove_roles_on_mute;
|
||||||
|
const rolesToRestore = actionConfig.restore_roles_on_mute;
|
||||||
|
|
||||||
const caseArgs = {
|
const caseArgs = {
|
||||||
modId: pluginData.client.user.id,
|
modId: pluginData.client.user.id,
|
||||||
|
@ -43,7 +47,7 @@ export const MuteAction = automodAction({
|
||||||
const mutes = pluginData.getPlugin(MutesPlugin);
|
const mutes = pluginData.getPlugin(MutesPlugin);
|
||||||
for (const userId of userIdsToMute) {
|
for (const userId of userIdsToMute) {
|
||||||
try {
|
try {
|
||||||
await mutes.muteUser(userId, duration, reason, { contactMethods, caseArgs });
|
await mutes.muteUser(userId, duration, reason, { contactMethods, caseArgs }, rolesToRemove, rolesToRestore);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) {
|
if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) {
|
||||||
pluginData.getPlugin(LogsPlugin).log(LogType.BOT_ALERT, {
|
pluginData.getPlugin(LogsPlugin).log(LogType.BOT_ALERT, {
|
||||||
|
|
|
@ -13,7 +13,6 @@ import { ClearMutesWithoutRoleCmd } from "./commands/ClearMutesWithoutRoleCmd";
|
||||||
import { ClearMutesCmd } from "./commands/ClearMutesCmd";
|
import { ClearMutesCmd } from "./commands/ClearMutesCmd";
|
||||||
import { muteUser } from "./functions/muteUser";
|
import { muteUser } from "./functions/muteUser";
|
||||||
import { unmuteUser } from "./functions/unmuteUser";
|
import { unmuteUser } from "./functions/unmuteUser";
|
||||||
import { CaseArgs } from "../Cases/types";
|
|
||||||
import { Member } from "eris";
|
import { Member } from "eris";
|
||||||
import { ClearActiveMuteOnMemberBanEvt } from "./events/ClearActiveMuteOnMemberBanEvt";
|
import { ClearActiveMuteOnMemberBanEvt } from "./events/ClearActiveMuteOnMemberBanEvt";
|
||||||
import { ReapplyActiveMuteOnJoinEvt } from "./events/ReapplyActiveMuteOnJoinEvt";
|
import { ReapplyActiveMuteOnJoinEvt } from "./events/ReapplyActiveMuteOnJoinEvt";
|
||||||
|
@ -32,6 +31,8 @@ const defaultOptions = {
|
||||||
mute_message: "You have been muted on the {guildName} server. Reason given: {reason}",
|
mute_message: "You have been muted on the {guildName} server. Reason given: {reason}",
|
||||||
timed_mute_message: "You have been muted on the {guildName} server for {time}. Reason given: {reason}",
|
timed_mute_message: "You have been muted on the {guildName} server for {time}. Reason given: {reason}",
|
||||||
update_mute_message: "Your mute on the {guildName} server has been updated to {time}.",
|
update_mute_message: "Your mute on the {guildName} server has been updated to {time}.",
|
||||||
|
remove_roles_on_mute: false,
|
||||||
|
restore_roles_on_mute: false,
|
||||||
|
|
||||||
can_view_list: false,
|
can_view_list: false,
|
||||||
can_cleanup: false,
|
can_cleanup: false,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { GuildPluginData } from "knub";
|
||||||
import { MutesPluginType } from "../types";
|
import { MutesPluginType } from "../types";
|
||||||
import { LogType } from "../../../data/LogType";
|
import { LogType } from "../../../data/LogType";
|
||||||
import { resolveMember, stripObjectToScalars, UnknownUser } from "../../../utils";
|
import { resolveMember, stripObjectToScalars, UnknownUser } from "../../../utils";
|
||||||
|
import { MemberOptions } from "eris";
|
||||||
|
|
||||||
export async function clearExpiredMutes(pluginData: GuildPluginData<MutesPluginType>) {
|
export async function clearExpiredMutes(pluginData: GuildPluginData<MutesPluginType>) {
|
||||||
const expiredMutes = await pluginData.state.mutes.getExpiredMutes();
|
const expiredMutes = await pluginData.state.mutes.getExpiredMutes();
|
||||||
|
@ -14,6 +15,14 @@ export async function clearExpiredMutes(pluginData: GuildPluginData<MutesPluginT
|
||||||
if (muteRole) {
|
if (muteRole) {
|
||||||
await member.removeRole(muteRole);
|
await member.removeRole(muteRole);
|
||||||
}
|
}
|
||||||
|
if (mute.roles_to_restore) {
|
||||||
|
const memberOptions: MemberOptions = {};
|
||||||
|
const guildRoles = pluginData.guild.roles;
|
||||||
|
memberOptions.roles = Array.from(
|
||||||
|
new Set([...mute.roles_to_restore, ...member.roles.filter(x => x !== muteRole && guildRoles.has(x))]),
|
||||||
|
);
|
||||||
|
member.edit(memberOptions);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
pluginData.state.serverLogs.log(LogType.BOT_ALERT, {
|
pluginData.state.serverLogs.log(LogType.BOT_ALERT, {
|
||||||
body: `Failed to remove mute role from {userMention(member)}`,
|
body: `Failed to remove mute role from {userMention(member)}`,
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
UserNotificationMethod,
|
UserNotificationMethod,
|
||||||
} from "../../../utils";
|
} from "../../../utils";
|
||||||
import { renderTemplate } from "../../../templateFormatter";
|
import { renderTemplate } from "../../../templateFormatter";
|
||||||
import { TextChannel, User } from "eris";
|
import { MemberOptions, TextChannel, User } from "eris";
|
||||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||||
import { CaseTypes } from "../../../data/CaseTypes";
|
import { CaseTypes } from "../../../data/CaseTypes";
|
||||||
import { LogType } from "../../../data/LogType";
|
import { LogType } from "../../../data/LogType";
|
||||||
|
@ -26,6 +26,8 @@ export async function muteUser(
|
||||||
muteTime?: number,
|
muteTime?: number,
|
||||||
reason?: string,
|
reason?: string,
|
||||||
muteOptions: MuteOptions = {},
|
muteOptions: MuteOptions = {},
|
||||||
|
removeRolesOnMuteOverride: boolean | string[] | null = null,
|
||||||
|
restoreRolesOnMuteOverride: boolean | string[] | null = null,
|
||||||
) {
|
) {
|
||||||
const lock = await pluginData.locks.acquire(`mute-${userId}`);
|
const lock = await pluginData.locks.acquire(`mute-${userId}`);
|
||||||
|
|
||||||
|
@ -52,8 +54,37 @@ export async function muteUser(
|
||||||
const member = await resolveMember(pluginData.client, pluginData.guild, user.id, true); // Grab the fresh member so we don't have stale role info
|
const member = await resolveMember(pluginData.client, pluginData.guild, user.id, true); // Grab the fresh member so we don't have stale role info
|
||||||
const config = pluginData.config.getMatchingConfig({ member, userId });
|
const config = pluginData.config.getMatchingConfig({ member, userId });
|
||||||
|
|
||||||
|
let rolesToRestore: string[] = [];
|
||||||
if (member) {
|
if (member) {
|
||||||
const logs = pluginData.getPlugin(LogsPlugin);
|
const logs = pluginData.getPlugin(LogsPlugin);
|
||||||
|
// remove and store any roles to be removed/restored
|
||||||
|
const currentUserRoles = member.roles;
|
||||||
|
const memberOptions: MemberOptions = {};
|
||||||
|
const removeRoles = removeRolesOnMuteOverride ?? config.remove_roles_on_mute;
|
||||||
|
const restoreRoles = restoreRolesOnMuteOverride ?? config.restore_roles_on_mute;
|
||||||
|
|
||||||
|
// remove roles
|
||||||
|
if (!Array.isArray(removeRoles)) {
|
||||||
|
if (removeRoles) {
|
||||||
|
// exclude managed roles from being removed
|
||||||
|
const managedRoles = pluginData.guild.roles.filter(x => x.managed).map(y => y.id);
|
||||||
|
memberOptions.roles = managedRoles.filter(x => member.roles.includes(x));
|
||||||
|
await member.edit(memberOptions);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
memberOptions.roles = currentUserRoles.filter(x => !(<string[]>removeRoles).includes(x));
|
||||||
|
await member.edit(memberOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// set roles to be restored
|
||||||
|
if (!Array.isArray(restoreRoles)) {
|
||||||
|
if (restoreRoles) {
|
||||||
|
rolesToRestore = currentUserRoles;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rolesToRestore = currentUserRoles.filter(x => (<string[]>restoreRoles).includes(x));
|
||||||
|
}
|
||||||
|
|
||||||
// Apply mute role if it's missing
|
// Apply mute role if it's missing
|
||||||
if (!member.roles.includes(muteRole)) {
|
if (!member.roles.includes(muteRole)) {
|
||||||
try {
|
try {
|
||||||
|
@ -103,9 +134,12 @@ export async function muteUser(
|
||||||
let notifyResult: UserNotificationResult = { method: null, success: true };
|
let notifyResult: UserNotificationResult = { method: null, success: true };
|
||||||
|
|
||||||
if (existingMute) {
|
if (existingMute) {
|
||||||
await pluginData.state.mutes.updateExpiryTime(user.id, muteTime);
|
if (existingMute.roles_to_restore?.length || rolesToRestore?.length) {
|
||||||
|
rolesToRestore = Array.from(new Set([...existingMute.roles_to_restore, ...rolesToRestore]));
|
||||||
|
}
|
||||||
|
await pluginData.state.mutes.updateExpiryTime(user.id, muteTime, rolesToRestore);
|
||||||
} else {
|
} else {
|
||||||
await pluginData.state.mutes.addMute(user.id, muteTime);
|
await pluginData.state.mutes.addMute(user.id, muteTime, rolesToRestore);
|
||||||
}
|
}
|
||||||
|
|
||||||
const template = existingMute
|
const template = existingMute
|
||||||
|
|
|
@ -7,7 +7,7 @@ import humanizeDuration from "humanize-duration";
|
||||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||||
import { CaseTypes } from "../../../data/CaseTypes";
|
import { CaseTypes } from "../../../data/CaseTypes";
|
||||||
import { LogType } from "../../../data/LogType";
|
import { LogType } from "../../../data/LogType";
|
||||||
import { WithRequiredProps } from "../../../utils/typeUtils";
|
import { MemberOptions } from "eris";
|
||||||
|
|
||||||
export async function unmuteUser(
|
export async function unmuteUser(
|
||||||
pluginData: GuildPluginData<MutesPluginType>,
|
pluginData: GuildPluginData<MutesPluginType>,
|
||||||
|
@ -36,6 +36,14 @@ export async function unmuteUser(
|
||||||
if (muteRole && member.roles.includes(muteRole)) {
|
if (muteRole && member.roles.includes(muteRole)) {
|
||||||
await member.removeRole(muteRole);
|
await member.removeRole(muteRole);
|
||||||
}
|
}
|
||||||
|
if (existingMute?.roles_to_restore) {
|
||||||
|
const memberOptions: MemberOptions = {};
|
||||||
|
const guildRoles = pluginData.guild.roles;
|
||||||
|
memberOptions.roles = Array.from(
|
||||||
|
new Set([...existingMute.roles_to_restore, ...member.roles.filter(x => x !== muteRole && guildRoles.has(x))]),
|
||||||
|
);
|
||||||
|
member.edit(memberOptions);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Member ${userId} not found in guild ${pluginData.guild.name} (${pluginData.guild.id}) when attempting to unmute`,
|
`Member ${userId} not found in guild ${pluginData.guild.name} (${pluginData.guild.id}) when attempting to unmute`,
|
||||||
|
|
|
@ -23,6 +23,8 @@ export const ConfigSchema = t.type({
|
||||||
mute_message: tNullable(t.string),
|
mute_message: tNullable(t.string),
|
||||||
timed_mute_message: tNullable(t.string),
|
timed_mute_message: tNullable(t.string),
|
||||||
update_mute_message: tNullable(t.string),
|
update_mute_message: tNullable(t.string),
|
||||||
|
remove_roles_on_mute: t.union([t.boolean, t.array(t.string)]),
|
||||||
|
restore_roles_on_mute: t.union([t.boolean, t.array(t.string)]),
|
||||||
|
|
||||||
can_view_list: t.boolean,
|
can_view_list: t.boolean,
|
||||||
can_cleanup: t.boolean,
|
can_cleanup: t.boolean,
|
||||||
|
|
|
@ -11,6 +11,8 @@ const BaseSingleSpamConfig = t.type({
|
||||||
count: t.number,
|
count: t.number,
|
||||||
mute: tNullable(t.boolean),
|
mute: tNullable(t.boolean),
|
||||||
mute_time: tNullable(t.number),
|
mute_time: tNullable(t.number),
|
||||||
|
remove_roles_on_mute: tNullable(t.union([t.boolean, t.array(t.string)])),
|
||||||
|
restore_roles_on_mute: tNullable(t.union([t.boolean, t.array(t.string)])),
|
||||||
clean: tNullable(t.boolean),
|
clean: tNullable(t.boolean),
|
||||||
});
|
});
|
||||||
export type TBaseSingleSpamConfig = t.TypeOf<typeof BaseSingleSpamConfig>;
|
export type TBaseSingleSpamConfig = t.TypeOf<typeof BaseSingleSpamConfig>;
|
||||||
|
|
|
@ -82,12 +82,19 @@ export async function logAndDetectMessageSpam(
|
||||||
(spamConfig.mute_time && convertDelayStringToMS(spamConfig.mute_time.toString())) ?? 120 * 1000;
|
(spamConfig.mute_time && convertDelayStringToMS(spamConfig.mute_time.toString())) ?? 120 * 1000;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
muteResult = await mutesPlugin.muteUser(member.id, muteTime, "Automatic spam detection", {
|
muteResult = await mutesPlugin.muteUser(
|
||||||
caseArgs: {
|
member.id,
|
||||||
modId: pluginData.client.user.id,
|
muteTime,
|
||||||
postInCaseLogOverride: false,
|
"Automatic spam detection",
|
||||||
|
{
|
||||||
|
caseArgs: {
|
||||||
|
modId: pluginData.client.user.id,
|
||||||
|
postInCaseLogOverride: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
spamConfig.remove_roles_on_mute,
|
||||||
|
spamConfig.restore_roles_on_mute,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) {
|
if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) {
|
||||||
logs.log(LogType.BOT_ALERT, {
|
logs.log(LogType.BOT_ALERT, {
|
||||||
|
|
|
@ -41,12 +41,19 @@ export async function logAndDetectOtherSpam(
|
||||||
(spamConfig.mute_time && convertDelayStringToMS(spamConfig.mute_time.toString())) ?? 120 * 1000;
|
(spamConfig.mute_time && convertDelayStringToMS(spamConfig.mute_time.toString())) ?? 120 * 1000;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await mutesPlugin.muteUser(member.id, muteTime, "Automatic spam detection", {
|
await mutesPlugin.muteUser(
|
||||||
caseArgs: {
|
member.id,
|
||||||
modId: pluginData.client.user.id,
|
muteTime,
|
||||||
extraNotes: [`Details: ${details}`],
|
"Automatic spam detection",
|
||||||
|
{
|
||||||
|
caseArgs: {
|
||||||
|
modId: pluginData.client.user.id,
|
||||||
|
extraNotes: [`Details: ${details}`],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
spamConfig.remove_roles_on_mute,
|
||||||
|
spamConfig.restore_roles_on_mute,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) {
|
if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) {
|
||||||
logs.log(LogType.BOT_ALERT, {
|
logs.log(LogType.BOT_ALERT, {
|
||||||
|
|
Loading…
Add table
Reference in a new issue