From 0b1381e7b331ab63d826aec9b42e9d7336217b58 Mon Sep 17 00:00:00 2001 From: roflmaoqwerty Date: Thu, 9 Jan 2020 00:23:41 +1100 Subject: [PATCH 1/3] created new plugin --- .gitignore | 1 + backend/src/plugins/Roles.ts | 92 +++++++++++++++++++++++++ backend/src/plugins/availablePlugins.ts | 2 + 3 files changed, 95 insertions(+) create mode 100644 backend/src/plugins/Roles.ts diff --git a/.gitignore b/.gitignore index d38433b6..efa6d897 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,4 @@ desktop.ini .cache npm-ls.txt npm-audit.txt +.vscode/launch.json diff --git a/backend/src/plugins/Roles.ts b/backend/src/plugins/Roles.ts new file mode 100644 index 00000000..fd49e1ee --- /dev/null +++ b/backend/src/plugins/Roles.ts @@ -0,0 +1,92 @@ +import { ZeppelinPlugin, trimPluginDescription } from "./ZeppelinPlugin"; +import * as t from "io-ts"; +import { tNullable } from "src/utils"; +import { decorators as d, IPluginOptions, logger, waitForReaction, waitForReply } from "knub"; +import { Attachment, Constants as ErisConstants, Guild, Member, Message, TextChannel, User } from "eris"; +import { GuildLogs } from "src/data/GuildLogs"; + +const ConfigSchema = t.type({ + can_assign: t.boolean, + assignable_roles: tNullable(t.array(t.string)) + }); +type TConfigSchema = t.TypeOf; + +enum RoleActions{ + Add, + Remove +}; + +export class RolesPlugin extends ZeppelinPlugin { + public static pluginName = "roles"; + public static configSchema = ConfigSchema; + + public static pluginInfo = { + prettyName: "Roles", + description: trimPluginDescription(` + Enables authorised users to add and remove whitelisted roles with a command. + `), + }; + protected logs: GuildLogs; + + onLoad(){ + this.logs = new GuildLogs(this.guildId); + } + + public static getStaticDefaultOptions(): IPluginOptions { + return { + config: { + can_assign: false, + assignable_roles: null + }, + overrides: [ + { + level: ">=50", + config: { + can_assign: true, + }, + }, + ], + }; + } + + + @d.command("role", " ",{ + extra: { + info: { + description: "Assign a permitted role to a user", + }, + }, + }) + @d.permission("can_assign") + async assignRole(msg, args: {action: string; user: string; role: string}){ + const user = await this.resolveUser(args.user); + console.log(user); + if (!user) { + return this.sendErrorMessage(msg.channel, `User not found`); + } + + //if the role doesnt exist, we can exit + let roleIds = (msg.channel as TextChannel).guild.roles.map(x => x.id) + if(!(roleIds.includes(args.role))){ + return this.sendErrorMessage(msg.channel, `Role not found`); + } + + // If the user exists as a guild member, make sure we can act on them first + const member = await this.getMember(user.id); + if (member && !this.canActOn(msg.member, member)) { + this.sendErrorMessage(msg.channel, "Cannot add or remove roles on this user: insufficient permissions"); + return; + } + + const action: string = args.action[0].toUpperCase() + args.action.slice(1).toLowerCase(); + if(!RoleActions[action]){ + this.sendErrorMessage(msg.channel, "Cannot add or remove roles on this user: invalid action"); + } + + //check if the role is allowed to be applied + + + console.log("exited at the end"); + } + +} \ No newline at end of file diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts index f202ea76..8e6d3374 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -27,6 +27,7 @@ import { LocatePlugin } from "./LocateUser"; import { GuildConfigReloader } from "./GuildConfigReloader"; import { ChannelArchiverPlugin } from "./ChannelArchiver"; import { AutomodPlugin } from "./Automod"; +import { RolesPlugin} from "./Roles"; /** * Plugins available to be loaded for individual guilds @@ -58,6 +59,7 @@ export const availablePlugins = [ CompanionChannelPlugin, LocatePlugin, ChannelArchiverPlugin, + RolesPlugin, ]; /** From fa1e8b78f54df03d01d64cc514794611d3253e7f Mon Sep 17 00:00:00 2001 From: roflmaoqwerty Date: Thu, 9 Jan 2020 01:32:12 +1100 Subject: [PATCH 2/3] POC done --- backend/src/plugins/Roles.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/backend/src/plugins/Roles.ts b/backend/src/plugins/Roles.ts index fd49e1ee..08734dc0 100644 --- a/backend/src/plugins/Roles.ts +++ b/backend/src/plugins/Roles.ts @@ -1,9 +1,9 @@ import { ZeppelinPlugin, trimPluginDescription } from "./ZeppelinPlugin"; import * as t from "io-ts"; -import { tNullable } from "src/utils"; +import { tNullable } from "../utils"; import { decorators as d, IPluginOptions, logger, waitForReaction, waitForReply } from "knub"; import { Attachment, Constants as ErisConstants, Guild, Member, Message, TextChannel, User } from "eris"; -import { GuildLogs } from "src/data/GuildLogs"; +import { GuildLogs } from "../data/GuildLogs"; const ConfigSchema = t.type({ can_assign: t.boolean, @@ -12,7 +12,7 @@ const ConfigSchema = t.type({ type TConfigSchema = t.TypeOf; enum RoleActions{ - Add, + Add = 1, Remove }; @@ -58,10 +58,10 @@ export class RolesPlugin extends ZeppelinPlugin { }, }) @d.permission("can_assign") - async assignRole(msg, args: {action: string; user: string; role: string}){ + async assignRole(msg: Message, args: {action: string; user: string; role: string}){ const user = await this.resolveUser(args.user); console.log(user); - if (!user) { + if (user.discriminator == "0000") { return this.sendErrorMessage(msg.channel, `User not found`); } @@ -81,10 +81,17 @@ export class RolesPlugin extends ZeppelinPlugin { const action: string = args.action[0].toUpperCase() + args.action.slice(1).toLowerCase(); if(!RoleActions[action]){ this.sendErrorMessage(msg.channel, "Cannot add or remove roles on this user: invalid action"); + return; } //check if the role is allowed to be applied - + let config = this.getConfigForMsg(msg) + if(!config.assignable_roles || !config.assignable_roles.includes(args.role)){ + this.sendErrorMessage(msg.channel, "You do not have access to the specified role"); + return; + } + //at this point, everything has been verified, so apply the role + await this.bot.addGuildMemberRole(this.guildId, user.id, args.role); console.log("exited at the end"); } From d16a67bca3563da0bfd443952f03678621d2cf76 Mon Sep 17 00:00:00 2001 From: roflmaoqwerty Date: Fri, 10 Jan 2020 01:04:58 +1100 Subject: [PATCH 3/3] added match by role name functionality to roles plugin --- backend/src/plugins/Roles.ts | 37 +++++++++++++++++++-------- backend/src/plugins/ZeppelinPlugin.ts | 11 ++++++++ backend/src/utils.ts | 26 +++++++++++++++++++ 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/backend/src/plugins/Roles.ts b/backend/src/plugins/Roles.ts index 08734dc0..1355333e 100644 --- a/backend/src/plugins/Roles.ts +++ b/backend/src/plugins/Roles.ts @@ -50,7 +50,7 @@ export class RolesPlugin extends ZeppelinPlugin { } - @d.command("role", " ",{ + @d.command("role", " [role:string$]",{ extra: { info: { description: "Assign a permitted role to a user", @@ -60,20 +60,20 @@ export class RolesPlugin extends ZeppelinPlugin { @d.permission("can_assign") async assignRole(msg: Message, args: {action: string; user: string; role: string}){ const user = await this.resolveUser(args.user); - console.log(user); + const roleId = await this.resolveRoleId(args.role); if (user.discriminator == "0000") { return this.sendErrorMessage(msg.channel, `User not found`); } //if the role doesnt exist, we can exit let roleIds = (msg.channel as TextChannel).guild.roles.map(x => x.id) - if(!(roleIds.includes(args.role))){ + if(!(roleIds.includes(roleId))){ return this.sendErrorMessage(msg.channel, `Role not found`); } // If the user exists as a guild member, make sure we can act on them first - const member = await this.getMember(user.id); - if (member && !this.canActOn(msg.member, member)) { + const targetMember = await this.getMember(user.id); + if (targetMember && !this.canActOn(msg.member, targetMember)) { this.sendErrorMessage(msg.channel, "Cannot add or remove roles on this user: insufficient permissions"); return; } @@ -86,14 +86,31 @@ export class RolesPlugin extends ZeppelinPlugin { //check if the role is allowed to be applied let config = this.getConfigForMsg(msg) - if(!config.assignable_roles || !config.assignable_roles.includes(args.role)){ + if(!config.assignable_roles || !config.assignable_roles.includes(roleId)){ this.sendErrorMessage(msg.channel, "You do not have access to the specified role"); return; } - //at this point, everything has been verified, so apply the role - await this.bot.addGuildMemberRole(this.guildId, user.id, args.role); - - console.log("exited at the end"); + //at this point, everything has been verified, so it's ACTION TIME + switch(RoleActions[action]){ + case RoleActions.Add: + if(targetMember.roles.includes(roleId)){ + this.sendErrorMessage(msg.channel, "Role already applied to user"); + return; + } + await this.bot.addGuildMemberRole(this.guildId, user.id, roleId); + this.sendSuccessMessage(msg.channel, `Role added to user!`); + break; + case RoleActions.Remove: + if(!targetMember.roles.includes(roleId)){ + this.sendErrorMessage(msg.channel, "User does not have role"); + return; + } + await this.bot.removeGuildMemberRole(this.guildId, user.id, roleId); + this.sendSuccessMessage(msg.channel, `Role removed from user!`); + break; + default: + break; + } } } \ No newline at end of file diff --git a/backend/src/plugins/ZeppelinPlugin.ts b/backend/src/plugins/ZeppelinPlugin.ts index ce593a0b..65eecbaa 100644 --- a/backend/src/plugins/ZeppelinPlugin.ts +++ b/backend/src/plugins/ZeppelinPlugin.ts @@ -16,6 +16,7 @@ import { trimEmptyStartEndLines, trimIndents, UnknownUser, + resolveRoleId, } from "../utils"; import { Invite, Member, User } from "eris"; import DiscordRESTError from "eris/lib/errors/DiscordRESTError"; // tslint:disable-line @@ -237,6 +238,16 @@ export class ZeppelinPlugin extends Plug return user; } + /** + * Resolves a role from the passed string. The passed string can be a role ID, a role mention or a role name. + * In the event of duplicate role names, this function will return the first one it comes across. + * @param roleResolvable + */ + async resolveRoleId(roleResolvable: string): Promise { + const roleId = await resolveRoleId(this.bot, this.guildId, roleResolvable); + return roleId; + } + /** * Resolves a member from the passed string. The passed string can be a user id, a user mention, a full username (with discrim), etc. * If the member is not found in the cache, it's fetched from the API. diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 9542c48b..f12a04e1 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -956,6 +956,32 @@ export async function resolveMember(bot: Client, guild: Guild, value: string): P return null; } +export async function resolveRoleId(bot: Client, guildId: string, value: string){ + if(value == null){ + return null; + } + + //role mention + const mentionMatch = value.match(/^<@&?(\d+)>$/); + if(mentionMatch){ + return mentionMatch[1]; + } + + //role name + let roleList = await bot.getRESTGuildRoles(guildId); + let role = roleList.filter(x => x.name.toLocaleLowerCase() == value.toLocaleLowerCase()); + if(role[0]){ + return role[0].id; + } + + //role ID + const idMatch = value.match(/^\d+$/); + if (idMatch) { + return value; + } + return null; +} + export type StrictMessageContent = { content?: string; tts?: boolean; disableEveryone?: boolean; embed?: EmbedOptions }; export async function confirm(bot: Client, channel: TextableChannel, userId: string, content: MessageContent) {