Add ReactionRoles plugin
This commit is contained in:
parent
92e5282b70
commit
c0614f2470
6 changed files with 271 additions and 1 deletions
18
migrations/20180729142200_create_reaction_roles_table.js
Normal file
18
migrations/20180729142200_create_reaction_roles_table.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
exports.up = async function(knex, Promise) {
|
||||
if (! await knex.schema.hasTable('reaction_roles')) {
|
||||
await knex.schema.createTable('reaction_roles', table => {
|
||||
table.string('guild_id', 20).notNullable();
|
||||
table.string('channel_id', 20).notNullable();
|
||||
table.string('message_id', 20).notNullable();
|
||||
table.string('emoji', 20).notNullable();
|
||||
table.string('role_id', 20).notNullable();
|
||||
|
||||
table.primary(['guild_id', 'channel_id', 'message_id', 'emoji']);
|
||||
table.index(['message_id', 'emoji']);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async function(knex, Promise) {
|
||||
await knex.schema.dropTableIfExists('reaction_roles');
|
||||
};
|
59
src/data/GuildReactionRoles.ts
Normal file
59
src/data/GuildReactionRoles.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import knex from "../knex";
|
||||
import ReactionRole from "../models/ReactionRole";
|
||||
|
||||
export class GuildReactionRoles {
|
||||
protected guildId: string;
|
||||
|
||||
constructor(guildId) {
|
||||
this.guildId = guildId;
|
||||
}
|
||||
|
||||
async all(): Promise<ReactionRole[]> {
|
||||
const results = await knex("reaction_roles")
|
||||
.where("guild_id", this.guildId)
|
||||
.select();
|
||||
|
||||
return results.map(r => new ReactionRole(r));
|
||||
}
|
||||
|
||||
async getForMessage(messageId: string): Promise<ReactionRole[]> {
|
||||
const results = await knex("reaction_roles")
|
||||
.where("guild_id", this.guildId)
|
||||
.where("message_id", messageId)
|
||||
.select();
|
||||
|
||||
return results.map(r => new ReactionRole(r));
|
||||
}
|
||||
|
||||
async getByMessageAndEmoji(messageId: string, emoji: string): Promise<ReactionRole> {
|
||||
const result = await knex("reaction_roles")
|
||||
.where("guild_id", this.guildId)
|
||||
.where("message_id", messageId)
|
||||
.where("emoji", emoji)
|
||||
.first();
|
||||
|
||||
return result ? new ReactionRole(result) : null;
|
||||
}
|
||||
|
||||
async removeFromMessage(messageId: string, emoji: string = null) {
|
||||
let query = knex("reaction_roles")
|
||||
.where("guild_id", this.guildId)
|
||||
.where("message_id", messageId);
|
||||
|
||||
if (emoji) {
|
||||
query = query.where("emoji", emoji);
|
||||
}
|
||||
|
||||
await query.delete();
|
||||
}
|
||||
|
||||
async add(channelId: string, messageId: string, emoji: string, roleId: string) {
|
||||
await knex("reaction_roles").insert({
|
||||
guild_id: this.guildId,
|
||||
channel_id: channelId,
|
||||
message_id: messageId,
|
||||
emoji,
|
||||
role_id: roleId
|
||||
});
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ import { ModActionsPlugin } from "./plugins/ModActions";
|
|||
import { UtilityPlugin } from "./plugins/Utility";
|
||||
import { LogsPlugin } from "./plugins/Logs";
|
||||
import { PostPlugin } from "./plugins/Post";
|
||||
import { ReactionRolesPlugin } from "./plugins/ReactionRoles";
|
||||
import knex from "./knex";
|
||||
|
||||
// Run latest database migrations
|
||||
|
@ -30,7 +31,8 @@ knex.migrate.latest().then(() => {
|
|||
utility: UtilityPlugin,
|
||||
mod_actions: ModActionsPlugin,
|
||||
logs: LogsPlugin,
|
||||
post: PostPlugin
|
||||
post: PostPlugin,
|
||||
reaction_roles: ReactionRolesPlugin
|
||||
},
|
||||
globalPlugins: {
|
||||
bot_control: BotControlPlugin
|
||||
|
|
9
src/models/ReactionRole.ts
Normal file
9
src/models/ReactionRole.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import Model from "./Model";
|
||||
|
||||
export default class ReactionRole extends Model {
|
||||
public guild_id: string;
|
||||
public channel_id: string;
|
||||
public message_id: string;
|
||||
public emoji: string;
|
||||
public role_id: string;
|
||||
}
|
178
src/plugins/ReactionRoles.ts
Normal file
178
src/plugins/ReactionRoles.ts
Normal file
|
@ -0,0 +1,178 @@
|
|||
import { Plugin, decorators as d } from "knub";
|
||||
import { errorMessage, isSnowflake } from "../utils";
|
||||
import { GuildReactionRoles } from "../data/GuildReactionRoles";
|
||||
import { Channel, Emoji, Message, TextChannel } from "eris";
|
||||
|
||||
type ReactionRolePair = string[];
|
||||
|
||||
type CustomEmoji = {
|
||||
id: string;
|
||||
} & Emoji;
|
||||
|
||||
export class ReactionRolesPlugin extends Plugin {
|
||||
protected reactionRoles: GuildReactionRoles;
|
||||
|
||||
getDefaultOptions() {
|
||||
return {
|
||||
permissions: {
|
||||
manage: false
|
||||
},
|
||||
|
||||
overrides: [
|
||||
{
|
||||
level: ">=100",
|
||||
permissions: {
|
||||
manage: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
async onLoad() {
|
||||
this.reactionRoles = new GuildReactionRoles(this.guildId);
|
||||
return;
|
||||
|
||||
// Pre-fetch all messages with reaction roles so we get their events
|
||||
const reactionRoles = await this.reactionRoles.all();
|
||||
|
||||
const channelMessages: Map<string, Set<string>> = reactionRoles.reduce(
|
||||
(map: Map<string, Set<string>>, row) => {
|
||||
if (!map.has(row.channel_id)) map.set(row.channel_id, new Set());
|
||||
map.get(row.channel_id).add(row.message_id);
|
||||
return map;
|
||||
},
|
||||
new Map()
|
||||
);
|
||||
|
||||
const msgLoadPromises = [];
|
||||
|
||||
for (const [channelId, messageIdSet] of channelMessages.entries()) {
|
||||
const messageIds = Array.from(messageIdSet.values());
|
||||
const channel = (await this.guild.channels.get(channelId)) as TextChannel;
|
||||
if (!channel) continue;
|
||||
|
||||
for (const messageId of messageIds) {
|
||||
msgLoadPromises.push(channel.getMessage(messageId));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(msgLoadPromises);
|
||||
}
|
||||
|
||||
@d.command("reaction_roles", "<channel:channel> <messageId:string> <reactionRolePairs:string$>")
|
||||
@d.permission("manage")
|
||||
async reactionRolesCmd(
|
||||
msg: Message,
|
||||
args: { channel: Channel; messageId: string; reactionRolePairs: string }
|
||||
) {
|
||||
if (!(args.channel instanceof TextChannel)) {
|
||||
msg.channel.createMessage(errorMessage("Channel must be a text channel!"));
|
||||
return;
|
||||
}
|
||||
|
||||
const targetMessage = await args.channel.getMessage(args.messageId);
|
||||
if (!targetMessage) {
|
||||
args.channel.createMessage(errorMessage("Unknown message!"));
|
||||
return;
|
||||
}
|
||||
|
||||
const guildEmojis = this.guild.emojis as CustomEmoji[];
|
||||
const guildEmojiIds = guildEmojis.map(e => e.id);
|
||||
|
||||
// Turn "emoji = role" pairs into an array of tuples of the form [emoji, roleId]
|
||||
// Emoji is either a unicode emoji or the snowflake of a custom emoji
|
||||
const newRolePairs: ReactionRolePair[] = args.reactionRolePairs
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map(v => v.split("=").map(v => v.trim()))
|
||||
.map(pair => {
|
||||
const customEmojiMatch = pair[0].match(/^<:(?:.*?):(\d+)>$/);
|
||||
if (customEmojiMatch) {
|
||||
return [customEmojiMatch[1], pair[1]];
|
||||
} else {
|
||||
return pair;
|
||||
}
|
||||
});
|
||||
|
||||
// Verify the specified emojis and roles are valid
|
||||
for (const pair of newRolePairs) {
|
||||
if (isSnowflake(pair[0]) && !guildEmojiIds.includes(pair[0])) {
|
||||
msg.channel.createMessage(
|
||||
errorMessage("I can only use regular emojis and custom emojis from this server")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.guild.roles.has(pair[1])) {
|
||||
msg.channel.createMessage(errorMessage(`Unknown role ${pair[1]}`));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const oldReactionRoles = await this.reactionRoles.getForMessage(targetMessage.id);
|
||||
const oldRolePairs: ReactionRolePair[] = oldReactionRoles.map(r => [r.emoji, r.role_id]);
|
||||
|
||||
// Remove old reaction/role pairs that weren't included in the new pairs or were changed in some way
|
||||
const toRemove = oldRolePairs.filter(
|
||||
pair => !newRolePairs.find(oldPair => oldPair[0] === pair[0] && oldPair[1] === pair[1])
|
||||
);
|
||||
for (const rolePair of toRemove) {
|
||||
await this.reactionRoles.removeFromMessage(targetMessage.id, rolePair[0]);
|
||||
|
||||
for (const reaction of targetMessage.reactions.values()) {
|
||||
if (reaction.emoji.id === rolePair[0] || reaction.emoji.name === rolePair[0]) {
|
||||
reaction.remove(this.bot.user.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add new/changed reaction/role pairs
|
||||
const toAdd = newRolePairs.filter(
|
||||
pair => !oldRolePairs.find(oldPair => oldPair[0] === pair[0] && oldPair[1] === pair[1])
|
||||
);
|
||||
for (const rolePair of toAdd) {
|
||||
let emoji;
|
||||
|
||||
if (isSnowflake(rolePair[0])) {
|
||||
// Custom emoji
|
||||
const guildEmoji = guildEmojis.find(e => e.id === emoji.id);
|
||||
emoji = `${guildEmoji.name}:${guildEmoji.id}`;
|
||||
} else {
|
||||
// Unicode emoji
|
||||
emoji = rolePair[0];
|
||||
}
|
||||
|
||||
await targetMessage.addReaction(emoji);
|
||||
await this.reactionRoles.add(args.channel.id, targetMessage.id, rolePair[0], rolePair[1]);
|
||||
}
|
||||
}
|
||||
|
||||
@d.event("messageReactionAdd")
|
||||
async onAddReaction(msg: Message, emoji: CustomEmoji, userId: string) {
|
||||
const matchingReactionRole = await this.reactionRoles.getByMessageAndEmoji(
|
||||
msg.id,
|
||||
emoji.id || emoji.name
|
||||
);
|
||||
if (!matchingReactionRole) return;
|
||||
|
||||
const member = this.guild.members.get(userId);
|
||||
if (!member) return;
|
||||
|
||||
member.addRole(matchingReactionRole.role_id);
|
||||
}
|
||||
|
||||
@d.event("messageReactionRemove")
|
||||
async onRemoveReaction(msg: Message, emoji: CustomEmoji, userId: string) {
|
||||
const matchingReactionRole = await this.reactionRoles.getByMessageAndEmoji(
|
||||
msg.id,
|
||||
emoji.id || emoji.name
|
||||
);
|
||||
if (!matchingReactionRole) return;
|
||||
|
||||
const member = this.guild.members.get(userId);
|
||||
if (!member) return;
|
||||
|
||||
member.removeRole(matchingReactionRole.role_id);
|
||||
}
|
||||
}
|
|
@ -74,3 +74,7 @@ export function formatTemplateString(str: string, values) {
|
|||
return (at(values, val)[0] as string) || "";
|
||||
});
|
||||
}
|
||||
|
||||
export function isSnowflake(v: string): boolean {
|
||||
return /^\d{17,20}$/.test(v);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue