mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-03-15 05:41:51 +00:00
Add mass role add/remove commands. Fix UnknownUser mentions in logs. Fix unnecessary audit log reads when adding/removing roles and the log type is ignored.
This commit is contained in:
parent
9aeae4f89e
commit
820c9b466e
5 changed files with 220 additions and 59 deletions
|
@ -69,4 +69,7 @@ export enum LogType {
|
|||
SET_ANTIRAID_AUTO,
|
||||
|
||||
AUTOMOD_SPAM_NEW,
|
||||
|
||||
MASS_ASSIGN_ROLES,
|
||||
MASS_UNASSIGN_ROLES,
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import { decorators as d, IPluginOptions, logger } from "knub";
|
||||
import { GuildLogs } from "../data/GuildLogs";
|
||||
import { LogType } from "../data/LogType";
|
||||
import { Attachment, Channel, Constants as ErisConstants, Embed, Guild, Member, TextChannel, User } from "eris";
|
||||
import { Attachment, Channel, Constants as ErisConstants, Embed, Member, TextChannel, User } from "eris";
|
||||
import DiscordRESTError from "eris/lib/errors/DiscordRESTError"; // tslint:disable-line
|
||||
import {
|
||||
createChunkedMessage,
|
||||
disableCodeBlocks,
|
||||
disableLinkPreviews,
|
||||
findRelevantAuditLogEntry,
|
||||
messageSummary,
|
||||
noop,
|
||||
|
@ -26,7 +24,7 @@ import { GuildSavedMessages } from "../data/GuildSavedMessages";
|
|||
import { SavedMessage } from "../data/entities/SavedMessage";
|
||||
import { GuildArchives } from "../data/GuildArchives";
|
||||
import { GuildCases } from "../data/GuildCases";
|
||||
import { trimPluginDescription, ZeppelinPlugin } from "./ZeppelinPlugin";
|
||||
import { ZeppelinPlugin } from "./ZeppelinPlugin";
|
||||
import { renderTemplate, TemplateParseError } from "../templateFormatter";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import * as t from "io-ts";
|
||||
|
@ -376,59 +374,74 @@ export class LogsPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
if (!isEqual(oldMember.roles, member.roles)) {
|
||||
const addedRoles = diff(member.roles, oldMember.roles);
|
||||
const removedRoles = diff(oldMember.roles, member.roles);
|
||||
let skip = false;
|
||||
|
||||
const relevantAuditLogEntry = await this.findRelevantAuditLogEntry(
|
||||
ErisConstants.AuditLogActions.MEMBER_ROLE_UPDATE,
|
||||
member.id,
|
||||
);
|
||||
const mod = relevantAuditLogEntry ? relevantAuditLogEntry.user : new UnknownUser();
|
||||
if (
|
||||
addedRoles.length &&
|
||||
removedRoles.length &&
|
||||
this.guildLogs.isLogIgnored(LogType.MEMBER_ROLE_CHANGES, member.id)
|
||||
) {
|
||||
skip = true;
|
||||
} else if (addedRoles.length && this.guildLogs.isLogIgnored(LogType.MEMBER_ROLE_ADD, member.id)) {
|
||||
skip = true;
|
||||
} else if (removedRoles.length && this.guildLogs.isLogIgnored(LogType.MEMBER_ROLE_REMOVE, member.id)) {
|
||||
skip = true;
|
||||
}
|
||||
|
||||
if (addedRoles.length && removedRoles.length) {
|
||||
// Roles added *and* removed
|
||||
this.guildLogs.log(
|
||||
LogType.MEMBER_ROLE_CHANGES,
|
||||
{
|
||||
member: logMember,
|
||||
addedRoles: addedRoles
|
||||
.map(roleId => this.guild.roles.get(roleId) || { id: roleId, name: `Unknown (${roleId})` })
|
||||
.map(r => r.name)
|
||||
.join(", "),
|
||||
removedRoles: removedRoles
|
||||
.map(roleId => this.guild.roles.get(roleId) || { id: roleId, name: `Unknown (${roleId})` })
|
||||
.map(r => r.name)
|
||||
.join(", "),
|
||||
mod: stripObjectToScalars(mod),
|
||||
},
|
||||
member.id,
|
||||
);
|
||||
} else if (addedRoles.length) {
|
||||
// Roles added
|
||||
this.guildLogs.log(
|
||||
LogType.MEMBER_ROLE_ADD,
|
||||
{
|
||||
member: logMember,
|
||||
roles: addedRoles
|
||||
.map(roleId => this.guild.roles.get(roleId) || { id: roleId, name: `Unknown (${roleId})` })
|
||||
.map(r => r.name)
|
||||
.join(", "),
|
||||
mod: stripObjectToScalars(mod),
|
||||
},
|
||||
member.id,
|
||||
);
|
||||
} else if (removedRoles.length && !addedRoles.length) {
|
||||
// Roles removed
|
||||
this.guildLogs.log(
|
||||
LogType.MEMBER_ROLE_REMOVE,
|
||||
{
|
||||
member: logMember,
|
||||
roles: removedRoles
|
||||
.map(roleId => this.guild.roles.get(roleId) || { id: roleId, name: `Unknown (${roleId})` })
|
||||
.map(r => r.name)
|
||||
.join(", "),
|
||||
mod: stripObjectToScalars(mod),
|
||||
},
|
||||
if (!skip) {
|
||||
const relevantAuditLogEntry = await this.findRelevantAuditLogEntry(
|
||||
ErisConstants.AuditLogActions.MEMBER_ROLE_UPDATE,
|
||||
member.id,
|
||||
);
|
||||
const mod = relevantAuditLogEntry ? relevantAuditLogEntry.user : new UnknownUser();
|
||||
|
||||
if (addedRoles.length && removedRoles.length) {
|
||||
// Roles added *and* removed
|
||||
this.guildLogs.log(
|
||||
LogType.MEMBER_ROLE_CHANGES,
|
||||
{
|
||||
member: logMember,
|
||||
addedRoles: addedRoles
|
||||
.map(roleId => this.guild.roles.get(roleId) || { id: roleId, name: `Unknown (${roleId})` })
|
||||
.map(r => r.name)
|
||||
.join(", "),
|
||||
removedRoles: removedRoles
|
||||
.map(roleId => this.guild.roles.get(roleId) || { id: roleId, name: `Unknown (${roleId})` })
|
||||
.map(r => r.name)
|
||||
.join(", "),
|
||||
mod: stripObjectToScalars(mod),
|
||||
},
|
||||
member.id,
|
||||
);
|
||||
} else if (addedRoles.length) {
|
||||
// Roles added
|
||||
this.guildLogs.log(
|
||||
LogType.MEMBER_ROLE_ADD,
|
||||
{
|
||||
member: logMember,
|
||||
roles: addedRoles
|
||||
.map(roleId => this.guild.roles.get(roleId) || { id: roleId, name: `Unknown (${roleId})` })
|
||||
.map(r => r.name)
|
||||
.join(", "),
|
||||
mod: stripObjectToScalars(mod),
|
||||
},
|
||||
member.id,
|
||||
);
|
||||
} else if (removedRoles.length && !addedRoles.length) {
|
||||
// Roles removed
|
||||
this.guildLogs.log(
|
||||
LogType.MEMBER_ROLE_REMOVE,
|
||||
{
|
||||
member: logMember,
|
||||
roles: removedRoles
|
||||
.map(roleId => this.guild.roles.get(roleId) || { id: roleId, name: `Unknown (${roleId})` })
|
||||
.map(r => r.name)
|
||||
.join(", "),
|
||||
mod: stripObjectToScalars(mod),
|
||||
},
|
||||
member.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
disableUserNotificationStrings,
|
||||
errorMessage,
|
||||
findRelevantAuditLogEntry,
|
||||
MINUTES,
|
||||
multiSorter,
|
||||
notifyUser,
|
||||
stripObjectToScalars,
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { ZeppelinPlugin, trimPluginDescription } from "./ZeppelinPlugin";
|
||||
import { trimPluginDescription, ZeppelinPlugin } from "./ZeppelinPlugin";
|
||||
import * as t from "io-ts";
|
||||
import { stripObjectToScalars, tNullable } from "../utils";
|
||||
import { decorators as d, IPluginOptions, logger, waitForReaction, waitForReply } from "knub";
|
||||
import { Attachment, Constants as ErisConstants, Guild, GuildChannel, Member, Message, TextChannel, User } from "eris";
|
||||
import { stripObjectToScalars, successMessage } from "../utils";
|
||||
import { decorators as d, IPluginOptions, logger } from "knub";
|
||||
import { GuildChannel, Member, Message } from "eris";
|
||||
import { GuildLogs } from "../data/GuildLogs";
|
||||
import { LogType } from "../data/LogType";
|
||||
|
||||
const ConfigSchema = t.type({
|
||||
can_assign: t.boolean,
|
||||
can_mass_assign: t.boolean,
|
||||
assignable_roles: t.array(t.string),
|
||||
});
|
||||
type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
|
||||
|
@ -33,6 +34,7 @@ export class RolesPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
return {
|
||||
config: {
|
||||
can_assign: false,
|
||||
can_mass_assign: false,
|
||||
assignable_roles: [],
|
||||
},
|
||||
overrides: [
|
||||
|
@ -42,11 +44,17 @@ export class RolesPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
can_assign: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
level: ">=100",
|
||||
config: {
|
||||
can_mass_assign: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@d.command("addrole", "<member:member> [role:string$]", {
|
||||
@d.command("addrole", "<member:member> <role:string$>", {
|
||||
extra: {
|
||||
info: {
|
||||
description: "Add a role to the specified member",
|
||||
|
@ -95,7 +103,64 @@ export class RolesPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
this.sendSuccessMessage(msg.channel, "Role added to user!");
|
||||
}
|
||||
|
||||
@d.command("removerole", "<member:member> [role:string$]", {
|
||||
@d.command("massaddrole", "<role:string> <members:member...>")
|
||||
@d.permission("can_mass_assign")
|
||||
async massAddRoleCmd(msg: Message, args: { role: string; members: Member[] }) {
|
||||
for (const member of args.members) {
|
||||
if (!this.canActOn(msg.member, member, true)) {
|
||||
return this.sendErrorMessage(
|
||||
msg.channel,
|
||||
"Cannot add roles to 1 or more specified members: insufficient permissions",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const roleId = await this.resolveRoleId(args.role);
|
||||
if (!roleId) {
|
||||
return this.sendErrorMessage(msg.channel, "Invalid role id");
|
||||
}
|
||||
const role = this.guild.roles.get(roleId);
|
||||
|
||||
const config = this.getConfigForMsg(msg);
|
||||
if (!config.assignable_roles.includes(roleId)) {
|
||||
return this.sendErrorMessage(msg.channel, "You cannot assign that role");
|
||||
}
|
||||
|
||||
const membersWithoutTheRole = args.members.filter(m => !m.roles.includes(roleId));
|
||||
let assigned = 0;
|
||||
let failed = 0;
|
||||
const alreadyHadRole = args.members.length - membersWithoutTheRole.length;
|
||||
|
||||
msg.channel.createMessage(`Adding role to specified members...`);
|
||||
|
||||
for (const member of membersWithoutTheRole) {
|
||||
try {
|
||||
this.logs.ignoreLog(LogType.MEMBER_ROLE_ADD, member.id);
|
||||
await member.addRole(roleId);
|
||||
this.logs.log(LogType.MEMBER_ROLE_ADD, {
|
||||
member: stripObjectToScalars(member, ["user", "roles"]),
|
||||
roles: role.name,
|
||||
mod: stripObjectToScalars(msg.author),
|
||||
});
|
||||
assigned++;
|
||||
} catch (e) {
|
||||
logger.warn(`Error when adding role via !massaddrole: ${e.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
let resultMessage = `Role added to ${assigned} ${assigned === 1 ? "member" : "members"}!`;
|
||||
if (failed > 0) {
|
||||
resultMessage += ` Failed to add the role to ${failed} ${failed === 1 ? "member" : "members"}.`;
|
||||
}
|
||||
if (alreadyHadRole) {
|
||||
resultMessage += ` ${alreadyHadRole} ${alreadyHadRole === 1 ? "member" : "members"} already had the role.`;
|
||||
}
|
||||
|
||||
msg.channel.createMessage(successMessage(resultMessage));
|
||||
}
|
||||
|
||||
@d.command("removerole", "<member:member> <role:string$>", {
|
||||
extra: {
|
||||
info: {
|
||||
description: "Remove a role from the specified member",
|
||||
|
@ -143,4 +208,61 @@ export class RolesPlugin extends ZeppelinPlugin<TConfigSchema> {
|
|||
|
||||
this.sendSuccessMessage(msg.channel, "Role removed from user!");
|
||||
}
|
||||
|
||||
@d.command("massremoverole", "<role:string> <members:member...>")
|
||||
@d.permission("can_mass_assign")
|
||||
async massRemoveRoleCmd(msg: Message, args: { role: string; members: Member[] }) {
|
||||
for (const member of args.members) {
|
||||
if (!this.canActOn(msg.member, member, true)) {
|
||||
return this.sendErrorMessage(
|
||||
msg.channel,
|
||||
"Cannot add roles to 1 or more specified members: insufficient permissions",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const roleId = await this.resolveRoleId(args.role);
|
||||
if (!roleId) {
|
||||
return this.sendErrorMessage(msg.channel, "Invalid role id");
|
||||
}
|
||||
const role = this.guild.roles.get(roleId);
|
||||
|
||||
const config = this.getConfigForMsg(msg);
|
||||
if (!config.assignable_roles.includes(roleId)) {
|
||||
return this.sendErrorMessage(msg.channel, "You cannot remove that role");
|
||||
}
|
||||
|
||||
const membersWithTheRole = args.members.filter(m => m.roles.includes(roleId));
|
||||
let assigned = 0;
|
||||
let failed = 0;
|
||||
const didNotHaveRole = args.members.length - membersWithTheRole.length;
|
||||
|
||||
msg.channel.createMessage(`Removing role from specified members...`);
|
||||
|
||||
for (const member of membersWithTheRole) {
|
||||
try {
|
||||
this.logs.ignoreLog(LogType.MEMBER_ROLE_REMOVE, member.id);
|
||||
await member.removeRole(roleId);
|
||||
this.logs.log(LogType.MEMBER_ROLE_REMOVE, {
|
||||
member: stripObjectToScalars(member, ["user", "roles"]),
|
||||
roles: role.name,
|
||||
mod: stripObjectToScalars(msg.author),
|
||||
});
|
||||
assigned++;
|
||||
} catch (e) {
|
||||
logger.warn(`Error when removing role via !massremoverole: ${e.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
let resultMessage = `Role removed from ${assigned} ${assigned === 1 ? "member" : "members"}!`;
|
||||
if (failed > 0) {
|
||||
resultMessage += ` Failed to remove the role from ${failed} ${failed === 1 ? "member" : "members"}.`;
|
||||
}
|
||||
if (didNotHaveRole) {
|
||||
resultMessage += ` ${didNotHaveRole} ${didNotHaveRole === 1 ? "member" : "members"} didn't have the role.`;
|
||||
}
|
||||
|
||||
msg.channel.createMessage(successMessage(resultMessage));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
User,
|
||||
} from "eris";
|
||||
import DiscordHTTPError from "eris/lib/errors/DiscordHTTPError"; // tslint:disable-line
|
||||
import DiscordRESTError from "eris/lib/errors/DiscordRESTError"; // tslint:disable-line
|
||||
import url from "url";
|
||||
import tlds from "tlds";
|
||||
import emojiRegex from "emoji-regex";
|
||||
|
@ -332,6 +333,9 @@ export function sleep(ms: number): Promise<void> {
|
|||
/**
|
||||
* Attempts to find a relevant audit log entry for the given user and action
|
||||
*/
|
||||
const auditLogNextAttemptAfterFail: Map<string, number> = new Map();
|
||||
const AUDIT_LOG_FAIL_COOLDOWN = 2 * MINUTES;
|
||||
|
||||
export async function findRelevantAuditLogEntry(
|
||||
guild: Guild,
|
||||
actionType: number,
|
||||
|
@ -339,10 +343,20 @@ export async function findRelevantAuditLogEntry(
|
|||
attempts: number = 3,
|
||||
attemptDelay: number = 3000,
|
||||
): Promise<GuildAuditLogEntry> {
|
||||
if (auditLogNextAttemptAfterFail.has(guild.id) && auditLogNextAttemptAfterFail.get(guild.id) > Date.now()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let auditLogs: GuildAuditLog;
|
||||
try {
|
||||
auditLogs = await guild.getAuditLogs(5, null, actionType);
|
||||
} catch (e) {
|
||||
// If we don't have permission to read audit log, set audit log requests on cooldown
|
||||
if (e instanceof DiscordRESTError && e.code === 50013) {
|
||||
auditLogNextAttemptAfterFail.set(guild.id, Date.now() + AUDIT_LOG_FAIL_COOLDOWN);
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Ignore internal server errors which seem to be pretty common with audit log requests
|
||||
if (!(e instanceof DiscordHTTPError) || e.code !== 500) {
|
||||
throw e;
|
||||
|
@ -1015,10 +1029,18 @@ export function messageSummary(msg: SavedMessage) {
|
|||
}
|
||||
|
||||
export function verboseUserMention(user: User | UnknownUser): string {
|
||||
if (user.id == null) {
|
||||
return `**${user.username}#${user.discriminator}**`;
|
||||
}
|
||||
|
||||
return `<@!${user.id}> (**${user.username}#${user.discriminator}**, \`${user.id}\`)`;
|
||||
}
|
||||
|
||||
export function verboseUserName(user: User | UnknownUser): string {
|
||||
if (user.id == null) {
|
||||
return `**${user.username}#${user.discriminator}**`;
|
||||
}
|
||||
|
||||
return `**${user.username}#${user.discriminator}** (\`${user.id}\`)`;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue