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..1355333e --- /dev/null +++ b/backend/src/plugins/Roles.ts @@ -0,0 +1,116 @@ +import { ZeppelinPlugin, trimPluginDescription } from "./ZeppelinPlugin"; +import * as t from "io-ts"; +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 "../data/GuildLogs"; + +const ConfigSchema = t.type({ + can_assign: t.boolean, + assignable_roles: tNullable(t.array(t.string)) + }); +type TConfigSchema = t.TypeOf; + +enum RoleActions{ + Add = 1, + 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", " [role:string$]",{ + extra: { + info: { + description: "Assign a permitted role to a user", + }, + }, + }) + @d.permission("can_assign") + async assignRole(msg: Message, args: {action: string; user: string; role: string}){ + const user = await this.resolveUser(args.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(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 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; + } + + 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(roleId)){ + this.sendErrorMessage(msg.channel, "You do not have access to the specified role"); + return; + } + //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/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, ]; /** 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) {