From 3685cc4df866ecf295659a92dcf22707485ba14b Mon Sep 17 00:00:00 2001 From: Dark <7890309+DarkView@users.noreply.github.com> Date: Thu, 23 Jul 2020 00:55:12 +0200 Subject: [PATCH] Migrate Roles to new Plugin structure --- backend/src/plugins/Roles/RolesPlugin.ts | 49 ++++++++++ .../src/plugins/Roles/commands/AddRoleCmd.ts | 61 ++++++++++++ .../plugins/Roles/commands/MassAddRoleCmd.ts | 98 +++++++++++++++++++ .../Roles/commands/MassRemoveRoleCmd.ts | 98 +++++++++++++++++++ .../plugins/Roles/commands/RemoveRoleCmd.ts | 61 ++++++++++++ backend/src/plugins/Roles/types.ts | 19 ++++ backend/src/plugins/availablePlugins.ts | 2 + 7 files changed, 388 insertions(+) create mode 100644 backend/src/plugins/Roles/RolesPlugin.ts create mode 100644 backend/src/plugins/Roles/commands/AddRoleCmd.ts create mode 100644 backend/src/plugins/Roles/commands/MassAddRoleCmd.ts create mode 100644 backend/src/plugins/Roles/commands/MassRemoveRoleCmd.ts create mode 100644 backend/src/plugins/Roles/commands/RemoveRoleCmd.ts create mode 100644 backend/src/plugins/Roles/types.ts diff --git a/backend/src/plugins/Roles/RolesPlugin.ts b/backend/src/plugins/Roles/RolesPlugin.ts new file mode 100644 index 00000000..565e34c6 --- /dev/null +++ b/backend/src/plugins/Roles/RolesPlugin.ts @@ -0,0 +1,49 @@ +import { zeppelinPlugin } from "../ZeppelinPluginBlueprint"; +import { PluginOptions } from "knub"; +import { ConfigSchema, RolesPluginType } from "./types"; +import { GuildLogs } from "src/data/GuildLogs"; +import { AddRoleCmd } from "./commands/AddRoleCmd"; +import { RemoveRoleCmd } from "./commands/RemoveRoleCmd"; +import { MassAddRoleCmd } from "./commands/MassAddRoleCmd"; +import { MassRemoveRoleCmd } from "./commands/MassRemoveRoleCmd"; + +const defaultOptions: PluginOptions = { + config: { + can_assign: false, + can_mass_assign: false, + assignable_roles: ["558037973581430785"], + }, + overrides: [ + { + level: ">=50", + config: { + can_assign: true, + }, + }, + { + level: ">=100", + config: { + can_mass_assign: true, + }, + }, + ], +}; + +export const RolesPlugin = zeppelinPlugin()("roles", { + configSchema: ConfigSchema, + defaultOptions, + + // prettier-ignore + commands: [ + AddRoleCmd, + RemoveRoleCmd, + MassAddRoleCmd, + MassRemoveRoleCmd, + ], + + onLoad(pluginData) { + const { state, guild } = pluginData; + + state.logs = new GuildLogs(guild.id); + }, +}); diff --git a/backend/src/plugins/Roles/commands/AddRoleCmd.ts b/backend/src/plugins/Roles/commands/AddRoleCmd.ts new file mode 100644 index 00000000..c9969a89 --- /dev/null +++ b/backend/src/plugins/Roles/commands/AddRoleCmd.ts @@ -0,0 +1,61 @@ +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { sendErrorMessage, sendSuccessMessage, canActOn } from "src/pluginUtils"; +import { rolesCmd } from "../types"; +import { resolveRoleId, stripObjectToScalars, verboseUserMention } from "src/utils"; +import { LogType } from "src/data/LogType"; +import { GuildChannel } from "eris"; + +export const AddRoleCmd = rolesCmd({ + trigger: "addrole", + permission: "can_assign", + + signature: { + member: ct.resolvedMember(), + role: ct.string({ catchAll: true }), + }, + + async run({ message: msg, args, pluginData }) { + if (!canActOn(pluginData, msg.member, args.member, true)) { + return sendErrorMessage(pluginData, msg.channel, "Cannot add roles to this user: insufficient permissions"); + } + + const roleId = await resolveRoleId(pluginData.client, pluginData.guild.id, args.role); + if (!roleId) { + return sendErrorMessage(pluginData, msg.channel, "Invalid role id"); + } + + const config = pluginData.config.getForMessage(msg); + if (!config.assignable_roles.includes(roleId)) { + return sendErrorMessage(pluginData, msg.channel, "You cannot assign that role"); + } + + // Sanity check: make sure the role is configured properly + const role = (msg.channel as GuildChannel).guild.roles.get(roleId); + if (!role) { + pluginData.state.logs.log(LogType.BOT_ALERT, { + body: `Unknown role configured for 'roles' plugin: ${roleId}`, + }); + return sendErrorMessage(pluginData, msg.channel, "You cannot assign that role"); + } + + if (args.member.roles.includes(roleId)) { + return sendErrorMessage(pluginData, msg.channel, "Member already has that role"); + } + + pluginData.state.logs.ignoreLog(LogType.MEMBER_ROLE_ADD, args.member.id); + + await args.member.addRole(roleId); + + pluginData.state.logs.log(LogType.MEMBER_ROLE_ADD, { + member: stripObjectToScalars(args.member, ["user", "roles"]), + roles: role.name, + mod: stripObjectToScalars(msg.author), + }); + + sendSuccessMessage( + pluginData, + msg.channel, + `Added role **${role.name}** to ${verboseUserMention(args.member.user)}!`, + ); + }, +}); diff --git a/backend/src/plugins/Roles/commands/MassAddRoleCmd.ts b/backend/src/plugins/Roles/commands/MassAddRoleCmd.ts new file mode 100644 index 00000000..d916ea10 --- /dev/null +++ b/backend/src/plugins/Roles/commands/MassAddRoleCmd.ts @@ -0,0 +1,98 @@ +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { sendErrorMessage, canActOn } from "src/pluginUtils"; +import { rolesCmd } from "../types"; +import { resolveMember, resolveRoleId, stripObjectToScalars, successMessage } from "src/utils"; +import { LogType } from "src/data/LogType"; +import { logger } from "src/logger"; + +export const MassAddRoleCmd = rolesCmd({ + trigger: "massaddrole", + permission: "can_mass_assign", + + signature: { + role: ct.string(), + members: ct.string({ rest: true }), + }, + + async run({ message: msg, args, pluginData }) { + msg.channel.createMessage(`Resolving members...`); + + const members = []; + const unknownMembers = []; + for (const memberId of args.members) { + const member = await resolveMember(pluginData.client, pluginData.guild, memberId); + if (member) members.push(member); + else unknownMembers.push(memberId); + } + + for (const member of members) { + if (!canActOn(pluginData, msg.member, member, true)) { + return sendErrorMessage( + pluginData, + msg.channel, + "Cannot add roles to 1 or more specified members: insufficient permissions", + ); + } + } + + const roleId = await resolveRoleId(pluginData.client, pluginData.guild.id, args.role); + if (!roleId) { + return sendErrorMessage(pluginData, msg.channel, "Invalid role id"); + } + + const config = pluginData.config.getForMessage(msg); + if (!config.assignable_roles.includes(roleId)) { + return sendErrorMessage(pluginData, msg.channel, "You cannot assign that role"); + } + + const role = pluginData.guild.roles.get(roleId); + if (!role) { + pluginData.state.logs.log(LogType.BOT_ALERT, { + body: `Unknown role configured for 'roles' plugin: ${roleId}`, + }); + return sendErrorMessage(pluginData, msg.channel, "You cannot assign that role"); + } + + const membersWithoutTheRole = members.filter(m => !m.roles.includes(roleId)); + let assigned = 0; + const failed = []; + const alreadyHadRole = members.length - membersWithoutTheRole.length; + + msg.channel.createMessage( + `Adding role **${role.name}** to ${membersWithoutTheRole.length} ${ + membersWithoutTheRole.length === 1 ? "member" : "members" + }...`, + ); + + for (const member of membersWithoutTheRole) { + try { + pluginData.state.logs.ignoreLog(LogType.MEMBER_ROLE_ADD, member.id); + await member.addRole(roleId); + pluginData.state.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.push(member.id); + } + } + + let resultMessage = `Added role **${role.name}** to ${assigned} ${assigned === 1 ? "member" : "members"}!`; + if (alreadyHadRole) { + resultMessage += ` ${alreadyHadRole} ${alreadyHadRole === 1 ? "member" : "members"} already had the role.`; + } + + if (failed.length) { + resultMessage += `\nFailed to add the role to the following members: ${failed.join(", ")}`; + } + + if (unknownMembers.length) { + resultMessage += `\nUnknown members: ${unknownMembers.join(", ")}`; + } + + msg.channel.createMessage(successMessage(resultMessage)); + }, +}); diff --git a/backend/src/plugins/Roles/commands/MassRemoveRoleCmd.ts b/backend/src/plugins/Roles/commands/MassRemoveRoleCmd.ts new file mode 100644 index 00000000..e88115ae --- /dev/null +++ b/backend/src/plugins/Roles/commands/MassRemoveRoleCmd.ts @@ -0,0 +1,98 @@ +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { sendErrorMessage, canActOn } from "src/pluginUtils"; +import { rolesCmd } from "../types"; +import { resolveMember, stripObjectToScalars, successMessage, resolveRoleId } from "src/utils"; +import { LogType } from "src/data/LogType"; +import { logger } from "src/logger"; + +export const MassRemoveRoleCmd = rolesCmd({ + trigger: "massremoverole", + permission: "can_mass_assign", + + signature: { + role: ct.string(), + members: ct.string({ rest: true }), + }, + + async run({ message: msg, args, pluginData }) { + msg.channel.createMessage(`Resolving members...`); + + const members = []; + const unknownMembers = []; + for (const memberId of args.members) { + const member = await resolveMember(pluginData.client, pluginData.guild, memberId); + if (member) members.push(member); + else unknownMembers.push(memberId); + } + + for (const member of members) { + if (!canActOn(pluginData, msg.member, member, true)) { + return sendErrorMessage( + pluginData, + msg.channel, + "Cannot add roles to 1 or more specified members: insufficient permissions", + ); + } + } + + const roleId = await resolveRoleId(pluginData.client, pluginData.guild.id, args.role); + if (!roleId) { + return sendErrorMessage(pluginData, msg.channel, "Invalid role id"); + } + + const config = pluginData.config.getForMessage(msg); + if (!config.assignable_roles.includes(roleId)) { + return sendErrorMessage(pluginData, msg.channel, "You cannot remove that role"); + } + + const role = pluginData.guild.roles.get(roleId); + if (!role) { + pluginData.state.logs.log(LogType.BOT_ALERT, { + body: `Unknown role configured for 'roles' plugin: ${roleId}`, + }); + return sendErrorMessage(pluginData, msg.channel, "You cannot remove that role"); + } + + const membersWithTheRole = members.filter(m => m.roles.includes(roleId)); + let assigned = 0; + const failed = []; + const didNotHaveRole = members.length - membersWithTheRole.length; + + msg.channel.createMessage( + `Removing role **${role.name}** from ${membersWithTheRole.length} ${ + membersWithTheRole.length === 1 ? "member" : "members" + }...`, + ); + + for (const member of membersWithTheRole) { + try { + pluginData.state.logs.ignoreLog(LogType.MEMBER_ROLE_REMOVE, member.id); + await member.removeRole(roleId); + pluginData.state.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.push(member.id); + } + } + + let resultMessage = `Removed role **${role.name}** from ${assigned} ${assigned === 1 ? "member" : "members"}!`; + if (didNotHaveRole) { + resultMessage += ` ${didNotHaveRole} ${didNotHaveRole === 1 ? "member" : "members"} didn't have the role.`; + } + + if (failed.length) { + resultMessage += `\nFailed to remove the role from the following members: ${failed.join(", ")}`; + } + + if (unknownMembers.length) { + resultMessage += `\nUnknown members: ${unknownMembers.join(", ")}`; + } + + msg.channel.createMessage(successMessage(resultMessage)); + }, +}); diff --git a/backend/src/plugins/Roles/commands/RemoveRoleCmd.ts b/backend/src/plugins/Roles/commands/RemoveRoleCmd.ts new file mode 100644 index 00000000..c02b1a2d --- /dev/null +++ b/backend/src/plugins/Roles/commands/RemoveRoleCmd.ts @@ -0,0 +1,61 @@ +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { sendErrorMessage, sendSuccessMessage, canActOn } from "src/pluginUtils"; +import { rolesCmd } from "../types"; +import { GuildChannel } from "eris"; +import { LogType } from "src/data/LogType"; +import { stripObjectToScalars, verboseUserMention, resolveRoleId } from "src/utils"; + +export const RemoveRoleCmd = rolesCmd({ + trigger: "removerole", + permission: "can_assign", + + signature: { + member: ct.resolvedMember(), + role: ct.string({ catchAll: true }), + }, + + async run({ message: msg, args, pluginData }) { + if (!canActOn(pluginData, msg.member, args.member, true)) { + return sendErrorMessage(pluginData, msg.channel, "Cannot remove roles from this user: insufficient permissions"); + } + + const roleId = await resolveRoleId(pluginData.client, pluginData.guild.id, args.role); + if (!roleId) { + return sendErrorMessage(pluginData, msg.channel, "Invalid role id"); + } + + const config = pluginData.config.getForMessage(msg); + if (!config.assignable_roles.includes(roleId)) { + return sendErrorMessage(pluginData, msg.channel, "You cannot remove that role"); + } + + // Sanity check: make sure the role is configured properly + const role = (msg.channel as GuildChannel).guild.roles.get(roleId); + if (!role) { + pluginData.state.logs.log(LogType.BOT_ALERT, { + body: `Unknown role configured for 'roles' plugin: ${roleId}`, + }); + return sendErrorMessage(pluginData, msg.channel, "You cannot remove that role"); + } + + if (!args.member.roles.includes(roleId)) { + return sendErrorMessage(pluginData, msg.channel, "Member doesn't have that role"); + } + + pluginData.state.logs.ignoreLog(LogType.MEMBER_ROLE_REMOVE, args.member.id); + + await args.member.removeRole(roleId); + + pluginData.state.logs.log(LogType.MEMBER_ROLE_REMOVE, { + member: stripObjectToScalars(args.member, ["user", "roles"]), + roles: role.name, + mod: stripObjectToScalars(msg.author), + }); + + sendSuccessMessage( + pluginData, + msg.channel, + `Removed role **${role.name}** removed from ${verboseUserMention(args.member.user)}!`, + ); + }, +}); diff --git a/backend/src/plugins/Roles/types.ts b/backend/src/plugins/Roles/types.ts new file mode 100644 index 00000000..fa86518f --- /dev/null +++ b/backend/src/plugins/Roles/types.ts @@ -0,0 +1,19 @@ +import * as t from "io-ts"; +import { BasePluginType, eventListener, command } from "knub"; +import { GuildLogs } from "src/data/GuildLogs"; + +export const ConfigSchema = t.type({ + can_assign: t.boolean, + can_mass_assign: t.boolean, + assignable_roles: t.array(t.string), +}); +export type TConfigSchema = t.TypeOf; + +export interface RolesPluginType extends BasePluginType { + config: TConfigSchema; + state: { + logs: GuildLogs; + }; +} + +export const rolesCmd = command(); diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts index 32825725..aad4e1d6 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -13,6 +13,7 @@ import { GuildConfigReloaderPlugin } from "./GuildConfigReloader/GuildConfigRelo import { CasesPlugin } from "./Cases/CasesPlugin"; import { MutesPlugin } from "./Mutes/MutesPlugin"; import { TagsPlugin } from "./Tags/TagsPlugin"; +import { RolesPlugin } from "./Roles/RolesPlugin"; // prettier-ignore export const guildPlugins: Array> = [ @@ -23,6 +24,7 @@ export const guildPlugins: Array> = [ MessageSaverPlugin, NameHistoryPlugin, RemindersPlugin, + RolesPlugin, TagsPlugin, UsernameSaverPlugin, UtilityPlugin,