diff --git a/src/data/GuildSelfGrantableRoles.ts b/src/data/GuildSelfGrantableRoles.ts new file mode 100644 index 00000000..1b7a58f6 --- /dev/null +++ b/src/data/GuildSelfGrantableRoles.ts @@ -0,0 +1,38 @@ +import { BaseRepository } from "./BaseRepository"; +import { getRepository, Repository } from "typeorm"; +import { SelfGrantableRole } from "./entities/SelfGrantableRole"; + +export class GuildSelfGrantableRoles extends BaseRepository { + private selfGrantableRoles: Repository; + + constructor(guildId) { + super(guildId); + this.selfGrantableRoles = getRepository(SelfGrantableRole); + } + + async getForChannel(channelId: string): Promise { + return this.selfGrantableRoles.find({ + where: { + guild_id: this.guildId, + channel_id: channelId, + }, + }); + } + + async delete(channelId: string, roleId: string) { + await this.selfGrantableRoles.delete({ + guild_id: this.guildId, + channel_id: channelId, + role_id: roleId, + }); + } + + async add(channelId: string, roleId: string, aliases: string[]) { + await this.selfGrantableRoles.insert({ + guild_id: this.guildId, + channel_id: channelId, + role_id: roleId, + aliases, + }); + } +} diff --git a/src/data/entities/SelfGrantableRole.ts b/src/data/entities/SelfGrantableRole.ts new file mode 100644 index 00000000..a3a310e8 --- /dev/null +++ b/src/data/entities/SelfGrantableRole.ts @@ -0,0 +1,16 @@ +import { Entity, Column, PrimaryColumn } from "typeorm"; + +@Entity("self_grantable_roles") +export class SelfGrantableRole { + @Column() + @PrimaryColumn() + id: number; + + @Column() guild_id: string; + + @Column() channel_id: string; + + @Column() role_id: string; + + @Column("simple-array") aliases: string[]; +} diff --git a/src/index.ts b/src/index.ts index 3f94e418..b82c5e2a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -72,6 +72,7 @@ import { StarboardPlugin } from "./plugins/Starboard"; import { NameHistoryPlugin } from "./plugins/NameHistory"; import { AutoReactions } from "./plugins/AutoReactions"; import { PingableRoles } from "./plugins/PingableRoles"; +import { SelfGrantableRoles } from "./plugins/SelfGrantableRoles"; // Run latest database migrations logger.info("Running database migrations"); @@ -79,7 +80,7 @@ connect().then(async conn => { await conn.runMigrations(); const client = new Client(process.env.TOKEN, { - getAllUsers: true + getAllUsers: true, }); client.setMaxListeners(100); @@ -112,7 +113,8 @@ connect().then(async conn => { SlowmodePlugin, StarboardPlugin, AutoReactions, - PingableRoles + PingableRoles, + SelfGrantableRoles, ], globalPlugins: [BotControlPlugin, LogServerPlugin], @@ -149,9 +151,9 @@ connect().then(async conn => { performanceDebug: { enabled: true, size: 30, - threshold: 200 - } - } + threshold: 200, + }, + }, }); logger.info("Starting the bot"); diff --git a/src/migrations/1550521627877-CreateSelfGrantableRolesTable.ts b/src/migrations/1550521627877-CreateSelfGrantableRolesTable.ts new file mode 100644 index 00000000..0cdb23b0 --- /dev/null +++ b/src/migrations/1550521627877-CreateSelfGrantableRolesTable.ts @@ -0,0 +1,51 @@ +import { MigrationInterface, QueryRunner, Table } from "typeorm"; + +export class CreateSelfGrantableRolesTable1550521627877 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: "self_grantable_roles", + columns: [ + { + name: "id", + type: "int", + unsigned: true, + isGenerated: true, + generationStrategy: "increment", + isPrimary: true, + }, + { + name: "guild_id", + type: "bigint", + unsigned: true, + }, + { + name: "channel_id", + type: "bigint", + unsigned: true, + }, + { + name: "role_id", + type: "bigint", + unsigned: true, + }, + { + name: "aliases", + type: "varchar", + length: "255", + }, + ], + indices: [ + { + columnNames: ["guild_id", "channel_id", "role_id"], + isUnique: true, + }, + ], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable("self_grantable_roles", true); + } +} diff --git a/src/plugins/ReactionRoles.ts b/src/plugins/ReactionRoles.ts index e2794874..86c43a6e 100644 --- a/src/plugins/ReactionRoles.ts +++ b/src/plugins/ReactionRoles.ts @@ -30,6 +30,7 @@ export class ReactionRolesPlugin extends ZeppelinPlugin { permissions: { manage: false, + fallback_command: false, }, overrides: [ diff --git a/src/plugins/SelfGrantableRoles.ts b/src/plugins/SelfGrantableRoles.ts new file mode 100644 index 00000000..698ae73b --- /dev/null +++ b/src/plugins/SelfGrantableRoles.ts @@ -0,0 +1,255 @@ +import { Plugin, decorators as d } from "knub"; +import { GuildSelfGrantableRoles } from "../data/GuildSelfGrantableRoles"; +import { GuildChannel, Message, Role, TextChannel } from "eris"; +import { chunkArray, errorMessage, sorter, successMessage } from "../utils"; + +export class SelfGrantableRoles extends Plugin { + public static pluginName = "self_grantable_roles"; + + protected selfGrantableRoles: GuildSelfGrantableRoles; + + getDefaultOptions() { + return { + permissions: { + manage: false, + use: false, + ignore_cooldown: false, + }, + + overrides: [ + { + level: ">=50", + permissions: { + ignore_cooldown: true, + }, + }, + { + level: ">=100", + permissions: { + manage: true, + }, + }, + ], + }; + } + + onLoad() { + this.selfGrantableRoles = GuildSelfGrantableRoles.getInstance(this.guildId); + } + + @d.command("role remove", "") + @d.permission("use") + @d.cooldown(2500, "ignore_cooldown") + async roleRemoveCmd(msg: Message, args: { roleNames: string[] }) { + const lock = await this.locks.acquire(`grantableRoles:${msg.author.id}`); + + const channelGrantableRoles = await this.selfGrantableRoles.getForChannel(msg.channel.id); + if (channelGrantableRoles.length === 0) { + lock.unlock(); + return; + } + + const nonMatchingRoleNames: string[] = []; + const rolesToRemove: Set = new Set(); + + // Match given role names with actual grantable roles + for (const roleName of args.roleNames) { + const normalized = roleName.toLowerCase(); + let matched = false; + + for (const grantableRole of channelGrantableRoles) { + if (grantableRole.aliases.includes(normalized)) { + if (this.guild.roles.has(grantableRole.role_id)) { + rolesToRemove.add(this.guild.roles.get(grantableRole.role_id)); + matched = true; + break; + } + } + } + + if (!matched) { + nonMatchingRoleNames.push(roleName); + } + } + + // Remove the roles + if (rolesToRemove.size) { + const rolesToRemoveArr = Array.from(rolesToRemove.values()); + const roleIdsToRemove = rolesToRemoveArr.map(r => r.id); + const newRoleIds = msg.member.roles.filter(roleId => !roleIdsToRemove.includes(roleId)); + + try { + await msg.member.edit({ + roles: newRoleIds, + }); + + const removedRolesStr = rolesToRemoveArr.map(r => `**${r.name}**`); + const removedRolesWord = rolesToRemoveArr.length === 1 ? "role" : "roles"; + + if (nonMatchingRoleNames.length) { + msg.channel.createMessage( + successMessage( + `<@!${msg.author.id}> Removed ${removedRolesStr.join(", ")} ${removedRolesWord};` + + ` couldn't recognize the other roles you mentioned`, + ), + ); + } else { + msg.channel.createMessage( + successMessage(`<@!${msg.author.id}> Removed ${removedRolesStr.join(", ")} ${removedRolesWord}`), + ); + } + } catch (e) { + msg.channel.createMessage(errorMessage(`<@!${msg.author.id}> Got an error while trying to remove the roles`)); + } + } else { + msg.channel.createMessage( + errorMessage(`<@!${msg.author.id}> Unknown ${args.roleNames.length === 1 ? "role" : "roles"}`), + ); + } + + lock.unlock(); + } + + @d.command("role", "") + @d.permission("use") + @d.cooldown(2500, "ignore_cooldown") + async roleCmd(msg: Message, args: { roleNames: string[] }) { + const lock = await this.locks.acquire(`grantableRoles:${msg.author.id}`); + + const channelGrantableRoles = await this.selfGrantableRoles.getForChannel(msg.channel.id); + if (channelGrantableRoles.length === 0) { + lock.unlock(); + return; + } + + const nonMatchingRoleNames: string[] = []; + const rolesToGrant: Set = new Set(); + + // Match given role names with actual grantable roles + for (const roleName of args.roleNames) { + const normalized = roleName.toLowerCase(); + let matched = false; + + for (const grantableRole of channelGrantableRoles) { + if (grantableRole.aliases.includes(normalized)) { + if (this.guild.roles.has(grantableRole.role_id)) { + rolesToGrant.add(this.guild.roles.get(grantableRole.role_id)); + matched = true; + break; + } + } + } + + if (!matched) { + nonMatchingRoleNames.push(roleName); + } + } + + // Grant the roles + if (rolesToGrant.size) { + const rolesToGrantArr = Array.from(rolesToGrant.values()); + const roleIdsToGrant = rolesToGrantArr.map(r => r.id); + const newRoleIds = Array.from(new Set(msg.member.roles.concat(roleIdsToGrant)).values()); + try { + await msg.member.edit({ + roles: newRoleIds, + }); + + const grantedRolesStr = rolesToGrantArr.map(r => `**${r.name}**`); + const grantedRolesWord = rolesToGrantArr.length === 1 ? "role" : "roles"; + + if (nonMatchingRoleNames.length) { + msg.channel.createMessage( + successMessage( + `<@!${msg.author.id}> Granted you the ${grantedRolesStr.join(", ")} ${grantedRolesWord};` + + ` couldn't recognize the other roles you mentioned`, + ), + ); + } else { + msg.channel.createMessage( + successMessage(`<@!${msg.author.id}> Granted you the ${grantedRolesStr.join(", ")} ${grantedRolesWord}`), + ); + } + } catch (e) { + msg.channel.createMessage( + errorMessage(`<@!${msg.author.id}> Got an error while trying to grant you the roles`), + ); + } + } else { + msg.channel.createMessage( + errorMessage(`<@!${msg.author.id}> Unknown ${args.roleNames.length === 1 ? "role" : "roles"}`), + ); + } + + lock.unlock(); + } + + @d.command("self_grantable_roles add", " [aliases:string...]") + @d.permission("manage") + async addSelfGrantableRoleCmd(msg: Message, args: { channel: GuildChannel; roleId: string; aliases?: string[] }) { + if (!(args.channel instanceof TextChannel)) { + msg.channel.createMessage(errorMessage("Invalid channel (must be a text channel)")); + return; + } + + const role = this.guild.roles.get(args.roleId); + if (!role) { + msg.channel.createMessage(errorMessage("Unknown role")); + return; + } + + const aliases = [role.name].concat(args.aliases || []); + const normalizedAliases = aliases.map(a => a.toLowerCase()); + const uniqueAliases = Array.from(new Set(normalizedAliases).values()); + + // Remove existing self grantable role on that channel, if one exists + await this.selfGrantableRoles.delete(args.channel.id, role.id); + + // Add new one + await this.selfGrantableRoles.add(args.channel.id, role.id, uniqueAliases); + + msg.channel.createMessage( + successMessage(`Self-grantable role **${role.name}** added to **#${args.channel.name}**`), + ); + } + + @d.command("self_grantable_roles delete", " ") + @d.permission("manage") + async deleteSelfGrantableRoleCmd(msg: Message, args: { channel: GuildChannel; roleId: string }) { + await this.selfGrantableRoles.delete(args.channel.id, args.roleId); + + const roleName = this.guild.roles.has(args.roleId) ? this.guild.roles.get(args.roleId).name : args.roleId; + + msg.channel.createMessage( + successMessage(`Self-grantable role **${roleName}** removed from **#${args.channel.name}**`), + ); + } + + @d.command("self_grantable_roles", "") + @d.permission("manage") + async selfGrantableRolesCmd(msg: Message, args: { channel: GuildChannel }) { + if (!(args.channel instanceof TextChannel)) { + msg.channel.createMessage(errorMessage("Invalid channel (must be a text channel)")); + return; + } + + const channelGrantableRoles = await this.selfGrantableRoles.getForChannel(args.channel.id); + if (channelGrantableRoles.length === 0) { + msg.channel.createMessage(errorMessage(`No self-grantable roles on **#${args.channel.name}**`)); + return; + } + + channelGrantableRoles.sort(sorter(gr => gr.aliases.join(", "))); + + const longestId = channelGrantableRoles.reduce((longest, gr) => Math.max(longest, gr.role_id.length), 0); + const lines = channelGrantableRoles.map(gr => { + const paddedId = gr.role_id.padEnd(longestId, " "); + return `${paddedId} ${gr.aliases.join(", ")}`; + }); + + const batches = chunkArray(lines, 20); + for (const batch of batches) { + await msg.channel.createMessage(`\`\`\`js\n${batch.join("\n")}\n\`\`\``); + } + } +}