diff --git a/src/data/GuildPingableRoles.ts b/src/data/GuildPingableRoles.ts new file mode 100644 index 00000000..80382274 --- /dev/null +++ b/src/data/GuildPingableRoles.ts @@ -0,0 +1,55 @@ +import { BaseRepository } from "./BaseRepository"; +import { getRepository, Repository } from "typeorm"; +import { PingableRole } from "./entities/PingableRole"; + +export class GuildPingableRoles extends BaseRepository { + private pingableRoles: Repository; + + constructor(guildId) { + super(guildId); + this.pingableRoles = getRepository(PingableRole); + } + + async all(): Promise { + return this.pingableRoles.find({ + where: { + guild_id: this.guildId + } + }); + } + + async getForChannel(channelId: string): Promise { + return this.pingableRoles.find({ + where: { + guild_id: this.guildId, + channel_id: channelId + } + }); + } + + async getByChannelAndRoleId(channelId: string, roleId: string): Promise { + return this.pingableRoles.findOne({ + where: { + guild_id: this.guildId, + channel_id: channelId, + role_id: roleId + } + }); + } + + async delete(channelId: string, roleId: string) { + await this.pingableRoles.delete({ + guild_id: this.guildId, + channel_id: channelId, + role_id: roleId + }); + } + + async add(channelId: string, roleId: string) { + await this.pingableRoles.insert({ + guild_id: this.guildId, + channel_id: channelId, + role_id: roleId + }); + } +} diff --git a/src/data/entities/PingableRole.ts b/src/data/entities/PingableRole.ts new file mode 100644 index 00000000..8d7264a4 --- /dev/null +++ b/src/data/entities/PingableRole.ts @@ -0,0 +1,15 @@ +import { Entity, Column, PrimaryColumn } from "typeorm"; +import { ISavedMessageData } from "./SavedMessage"; + +@Entity("pingable_roles") +export class PingableRole { + @Column() + @PrimaryColumn() + id: number; + + @Column() guild_id: string; + + @Column() channel_id: string; + + @Column() role_id: string; +} diff --git a/src/index.ts b/src/index.ts index d251d304..0fd6394a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -63,6 +63,7 @@ import { SlowmodePlugin } from "./plugins/Slowmode"; import { StarboardPlugin } from "./plugins/Starboard"; import { NameHistoryPlugin } from "./plugins/NameHistory"; import { AutoReactions } from "./plugins/AutoReactions"; +import { PingableRoles } from "./plugins/PingableRoles"; // Run latest database migrations logger.info("Running database migrations"); @@ -96,7 +97,8 @@ connect().then(async conn => { TagsPlugin, SlowmodePlugin, StarboardPlugin, - AutoReactions + AutoReactions, + PingableRoles ], globalPlugins: [BotControlPlugin, LogServerPlugin], diff --git a/src/migrations/1547293464842-CreatePingableRolesTable.ts b/src/migrations/1547293464842-CreatePingableRolesTable.ts new file mode 100644 index 00000000..fe952463 --- /dev/null +++ b/src/migrations/1547293464842-CreatePingableRolesTable.ts @@ -0,0 +1,49 @@ +import { MigrationInterface, QueryRunner, Table } from "typeorm"; + +export class CreatePingableRolesTable1547293464842 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: "pingable_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 + } + ], + indices: [ + { + columnNames: ["guild_id", "channel_id"] + }, + { + columnNames: ["guild_id", "channel_id", "role_id"], + isUnique: true + } + ] + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable("pingable_roles", true); + } +} diff --git a/src/plugins/PingableRoles.ts b/src/plugins/PingableRoles.ts new file mode 100644 index 00000000..a25516af --- /dev/null +++ b/src/plugins/PingableRoles.ts @@ -0,0 +1,135 @@ +import { Plugin, decorators as d } from "knub"; +import { SavedMessage } from "../data/entities/SavedMessage"; +import { Message, Role, TextableChannel, User } from "eris"; +import { GuildPingableRoles } from "../data/GuildPingableRoles"; +import { PingableRole } from "../data/entities/PingableRole"; +import { errorMessage, successMessage } from "../utils"; + +const TIMEOUT = 10 * 1000; + +export class PingableRoles extends Plugin { + public static pluginName = "pingable_roles"; + + protected pingableRoles: GuildPingableRoles; + protected cache: Map; + protected timeouts: Map; + + getDefaultOptions() { + return { + permissions: { + use: false + }, + + overrides: [ + { + level: ">=100", + permissions: { + use: true + } + } + ] + }; + } + + onLoad() { + this.pingableRoles = GuildPingableRoles.getInstance(this.guildId); + + this.cache = new Map(); + this.timeouts = new Map(); + } + + protected async getPingableRolesForChannel(channelId: string): Promise { + if (!this.cache.has(channelId)) { + this.cache.set(channelId, await this.pingableRoles.getForChannel(channelId)); + } + + return this.cache.get(channelId); + } + + @d.command("pingable_role disable", " ") + @d.permission("use") + async disablePingableRoleCmd(msg: Message, args: { channelId: string; role: Role }) { + const pingableRole = await this.pingableRoles.getByChannelAndRoleId(args.channelId, args.role.id); + if (!pingableRole) { + msg.channel.createMessage(errorMessage(`**${args.role.name}** is not set as pingable in <#${args.channelId}>`)); + return; + } + + await this.pingableRoles.delete(args.channelId, args.role.id); + msg.channel.createMessage( + successMessage(`**${args.role.name}** is no longer set as pingable in <#${args.channelId}>`) + ); + } + + @d.command("pingable_role", " ") + @d.permission("use") + async setPingableRoleCmd(msg: Message, args: { channelId: string; role: Role }) { + const existingPingableRole = await this.pingableRoles.getByChannelAndRoleId(args.channelId, args.role.id); + if (existingPingableRole) { + msg.channel.createMessage( + errorMessage(`**${args.role.name}** is already set as pingable in <#${args.channelId}>`) + ); + return; + } + + await this.pingableRoles.add(args.channelId, args.role.id); + msg.channel.createMessage(successMessage(`**${args.role.name}** has been set as pingable in <#${args.channelId}>`)); + } + + @d.event("typingStart") + async onTypingStart(channel: TextableChannel, user: User) { + const pingableRoles = await this.getPingableRolesForChannel(channel.id); + if (pingableRoles.length === 0) return; + + if (this.timeouts.has(channel.id)) { + clearTimeout(this.timeouts.get(channel.id)); + } + + this.enablePingableRoles(pingableRoles); + + const timeout = setTimeout(() => { + this.disablePingableRoles(pingableRoles); + }, TIMEOUT); + this.timeouts.set(channel.id, timeout); + } + + @d.event("messageCreate") + async onMessageCreate(msg: Message) { + const pingableRoles = await this.getPingableRolesForChannel(msg.channel.id); + if (pingableRoles.length === 0) return; + + if (this.timeouts.has(msg.channel.id)) { + clearTimeout(this.timeouts.get(msg.channel.id)); + } + + this.disablePingableRoles(pingableRoles); + } + + protected enablePingableRoles(pingableRoles: PingableRole[]) { + for (const pingableRole of pingableRoles) { + const role = this.guild.roles.get(pingableRole.role_id); + if (!role) continue; + + role.edit( + { + mentionable: true + }, + "Enable pingable role" + ); + } + } + + protected disablePingableRoles(pingableRoles: PingableRole[]) { + for (const pingableRole of pingableRoles) { + const role = this.guild.roles.get(pingableRole.role_id); + if (!role) continue; + + role.edit( + { + mentionable: false + }, + "Disable pingable role" + ); + } + } +}