2019-03-04 21:44:04 +02:00
|
|
|
import { decorators as d, IPluginOptions, logger } from "knub";
|
2020-04-10 11:30:37 +03:00
|
|
|
import { CustomEmoji, errorMessage, isSnowflake, noop, sleep } from "../utils";
|
2018-07-29 15:18:26 +03:00
|
|
|
import { GuildReactionRoles } from "../data/GuildReactionRoles";
|
2019-02-17 16:01:38 +02:00
|
|
|
import { Message, TextChannel } from "eris";
|
2019-02-09 14:36:03 +02:00
|
|
|
import { ZeppelinPlugin } from "./ZeppelinPlugin";
|
|
|
|
import { GuildSavedMessages } from "../data/GuildSavedMessages";
|
2019-02-16 12:01:31 +02:00
|
|
|
import { Queue } from "../Queue";
|
2019-02-23 21:21:05 +02:00
|
|
|
import { ReactionRole } from "../data/entities/ReactionRole";
|
2019-04-30 06:58:38 +03:00
|
|
|
import DiscordRESTError from "eris/lib/errors/DiscordRESTError"; // tslint:disable-line
|
2019-07-21 21:15:52 +03:00
|
|
|
import * as t from "io-ts";
|
2020-04-10 11:30:37 +03:00
|
|
|
import { ERRORS, RecoverablePluginError } from "../RecoverablePluginError";
|
|
|
|
import Timeout = NodeJS.Timeout;
|
2019-07-21 21:15:52 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Either of:
|
|
|
|
* [emojiId, roleId]
|
|
|
|
* [emojiId, roleId, emojiName]
|
|
|
|
* Where emojiId is either the snowflake of a custom emoji, or the actual unicode emoji
|
|
|
|
*/
|
|
|
|
const ReactionRolePair = t.union([t.tuple([t.string, t.string, t.string]), t.tuple([t.string, t.string])]);
|
|
|
|
type TReactionRolePair = t.TypeOf<typeof ReactionRolePair>;
|
|
|
|
|
|
|
|
const ConfigSchema = t.type({
|
|
|
|
auto_refresh_interval: t.number,
|
|
|
|
can_manage: t.boolean,
|
|
|
|
});
|
|
|
|
type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
|
2018-07-29 15:18:26 +03:00
|
|
|
|
2019-02-06 20:07:53 +02:00
|
|
|
type ReactionRolePair = [string, string, string?];
|
2018-07-29 15:18:26 +03:00
|
|
|
|
2019-02-18 01:58:21 +02:00
|
|
|
const MIN_AUTO_REFRESH = 1000 * 60 * 15; // 15min minimum, let's not abuse the API
|
2019-02-23 21:21:05 +02:00
|
|
|
const CLEAR_ROLES_EMOJI = "❌";
|
|
|
|
const ROLE_CHANGE_BATCH_DEBOUNCE_TIME = 1500;
|
|
|
|
|
|
|
|
type RoleChangeMode = "+" | "-";
|
|
|
|
|
|
|
|
type PendingMemberRoleChanges = {
|
|
|
|
timeout: Timeout;
|
|
|
|
applyFn: () => void;
|
|
|
|
changes: Array<{
|
|
|
|
mode: RoleChangeMode;
|
|
|
|
roleId: string;
|
|
|
|
}>;
|
|
|
|
};
|
2019-02-18 01:58:21 +02:00
|
|
|
|
2019-07-21 21:15:52 +03:00
|
|
|
export class ReactionRolesPlugin extends ZeppelinPlugin<TConfigSchema> {
|
2019-01-12 13:42:11 +02:00
|
|
|
public static pluginName = "reaction_roles";
|
2019-08-22 02:58:32 +03:00
|
|
|
public static configSchema = ConfigSchema;
|
|
|
|
|
|
|
|
public static pluginInfo = {
|
|
|
|
prettyName: "Reaction roles",
|
|
|
|
};
|
2019-01-03 06:15:28 +02:00
|
|
|
|
2018-07-29 15:18:26 +03:00
|
|
|
protected reactionRoles: GuildReactionRoles;
|
2019-02-09 14:36:03 +02:00
|
|
|
protected savedMessages: GuildSavedMessages;
|
2018-07-29 15:18:26 +03:00
|
|
|
|
2019-02-16 12:01:31 +02:00
|
|
|
protected reactionRemoveQueue: Queue;
|
2019-02-23 21:21:05 +02:00
|
|
|
protected pendingRoleChanges: Map<string, PendingMemberRoleChanges>;
|
2019-02-18 01:58:21 +02:00
|
|
|
protected pendingRefreshes: Set<string>;
|
|
|
|
|
|
|
|
private autoRefreshTimeout;
|
2019-02-16 12:01:31 +02:00
|
|
|
|
2019-08-22 01:22:26 +03:00
|
|
|
public static getStaticDefaultOptions(): IPluginOptions<TConfigSchema> {
|
2018-07-29 15:18:26 +03:00
|
|
|
return {
|
2019-02-18 01:58:21 +02:00
|
|
|
config: {
|
2019-07-21 21:20:57 +03:00
|
|
|
auto_refresh_interval: MIN_AUTO_REFRESH,
|
2019-02-18 01:58:21 +02:00
|
|
|
|
2019-04-13 01:44:18 +03:00
|
|
|
can_manage: false,
|
2018-07-29 15:18:26 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
overrides: [
|
|
|
|
{
|
|
|
|
level: ">=100",
|
2019-04-13 01:44:18 +03:00
|
|
|
config: {
|
|
|
|
can_manage: true,
|
2019-02-09 14:36:03 +02:00
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
2018-07-29 15:18:26 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
async onLoad() {
|
2019-05-25 21:25:34 +03:00
|
|
|
this.reactionRoles = GuildReactionRoles.getGuildInstance(this.guildId);
|
|
|
|
this.savedMessages = GuildSavedMessages.getGuildInstance(this.guildId);
|
2019-02-24 01:06:40 +02:00
|
|
|
this.reactionRemoveQueue = new Queue();
|
2019-02-23 21:21:05 +02:00
|
|
|
this.pendingRoleChanges = new Map();
|
2019-02-18 01:58:21 +02:00
|
|
|
this.pendingRefreshes = new Set();
|
|
|
|
|
2019-03-04 21:44:04 +02:00
|
|
|
let autoRefreshInterval = this.getConfig().auto_refresh_interval;
|
2019-02-18 01:58:21 +02:00
|
|
|
if (autoRefreshInterval != null) {
|
|
|
|
autoRefreshInterval = Math.max(MIN_AUTO_REFRESH, autoRefreshInterval);
|
|
|
|
this.autoRefreshLoop(autoRefreshInterval);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async onUnload() {
|
|
|
|
if (this.autoRefreshTimeout) {
|
|
|
|
clearTimeout(this.autoRefreshTimeout);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async autoRefreshLoop(interval: number) {
|
|
|
|
this.autoRefreshTimeout = setTimeout(async () => {
|
2019-02-23 21:21:05 +02:00
|
|
|
await this.runAutoRefresh();
|
2019-02-18 01:58:21 +02:00
|
|
|
this.autoRefreshLoop(interval);
|
|
|
|
}, interval);
|
|
|
|
}
|
|
|
|
|
2019-02-23 21:21:05 +02:00
|
|
|
async runAutoRefresh() {
|
|
|
|
// Refresh reaction roles on all reaction role messages
|
|
|
|
const reactionRoles = await this.reactionRoles.all();
|
|
|
|
const idPairs = new Set(reactionRoles.map(r => `${r.channel_id}-${r.message_id}`));
|
|
|
|
for (const pair of idPairs) {
|
|
|
|
const [channelId, messageId] = pair.split("-");
|
|
|
|
await this.refreshReactionRoles(channelId, messageId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Refreshes the reaction roles in a message. Basically just calls applyReactionRoleReactionsToMessage().
|
|
|
|
*/
|
2019-02-18 01:58:21 +02:00
|
|
|
async refreshReactionRoles(channelId: string, messageId: string) {
|
|
|
|
const pendingKey = `${channelId}-${messageId}`;
|
|
|
|
if (this.pendingRefreshes.has(pendingKey)) return;
|
|
|
|
this.pendingRefreshes.add(pendingKey);
|
|
|
|
|
|
|
|
try {
|
|
|
|
const reactionRoles = await this.reactionRoles.getForMessage(messageId);
|
2019-02-23 21:21:05 +02:00
|
|
|
await this.applyReactionRoleReactionsToMessage(channelId, messageId, reactionRoles);
|
|
|
|
} finally {
|
|
|
|
this.pendingRefreshes.delete(pendingKey);
|
|
|
|
}
|
|
|
|
}
|
2019-02-18 01:58:21 +02:00
|
|
|
|
2019-02-23 21:21:05 +02:00
|
|
|
/**
|
|
|
|
* Applies the reactions from the specified reaction roles to a message
|
|
|
|
*/
|
|
|
|
async applyReactionRoleReactionsToMessage(channelId: string, messageId: string, reactionRoles: ReactionRole[]) {
|
|
|
|
const channel = this.guild.channels.get(channelId) as TextChannel;
|
2019-06-11 10:24:50 +03:00
|
|
|
if (!channel) return;
|
2019-04-30 06:58:38 +03:00
|
|
|
|
|
|
|
let targetMessage;
|
|
|
|
try {
|
|
|
|
targetMessage = await channel.getMessage(messageId);
|
|
|
|
} catch (e) {
|
|
|
|
if (e instanceof DiscordRESTError) {
|
2020-01-12 17:02:14 +02:00
|
|
|
if (e.code === 10008) {
|
|
|
|
// Unknown message, remove reaction roles from the message
|
|
|
|
logger.warn(
|
|
|
|
`Removed reaction roles from unknown message ${channelId}/${messageId} in guild ${this.guild.name} (${this.guildId})`,
|
|
|
|
);
|
|
|
|
await this.reactionRoles.removeFromMessage(messageId);
|
|
|
|
} else {
|
|
|
|
logger.warn(
|
|
|
|
`Error when applying reaction roles to message ${channelId}/${messageId} in guild ${this.guild.name} (${this.guildId}), error code ${e.code}`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-04-30 06:58:38 +03:00
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
2019-02-18 01:58:21 +02:00
|
|
|
|
2019-02-23 21:21:05 +02:00
|
|
|
// Remove old reactions, if any
|
|
|
|
const removeSleep = sleep(1250);
|
|
|
|
await targetMessage.removeReactions();
|
|
|
|
await removeSleep;
|
2019-02-18 01:58:21 +02:00
|
|
|
|
2019-02-23 21:21:05 +02:00
|
|
|
// Add reaction role reactions
|
|
|
|
for (const rr of reactionRoles) {
|
|
|
|
const emoji = isSnowflake(rr.emoji) ? `foo:${rr.emoji}` : rr.emoji;
|
2019-02-18 01:58:21 +02:00
|
|
|
|
2019-02-23 21:21:05 +02:00
|
|
|
const sleepTime = sleep(1250); // Make sure we only add 1 reaction per ~second so as not to hit rate limits
|
|
|
|
await targetMessage.addReaction(emoji);
|
|
|
|
await sleepTime;
|
2019-02-18 01:58:21 +02:00
|
|
|
}
|
2019-02-23 21:21:05 +02:00
|
|
|
|
|
|
|
// Add the "clear reactions" button
|
|
|
|
await targetMessage.addReaction(CLEAR_ROLES_EMOJI);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds a pending role change for a member. After a delay, all pending role changes for a member are applied at once.
|
|
|
|
* This delay is refreshed any time new pending changes are added (i.e. "debounced").
|
|
|
|
*/
|
|
|
|
async addMemberPendingRoleChange(memberId: string, mode: RoleChangeMode, roleId: string) {
|
|
|
|
if (!this.pendingRoleChanges.has(memberId)) {
|
|
|
|
const newPendingRoleChangeObj: PendingMemberRoleChanges = {
|
|
|
|
timeout: null,
|
|
|
|
changes: [],
|
|
|
|
applyFn: async () => {
|
2019-05-02 08:21:11 +03:00
|
|
|
const member = await this.getMember(memberId);
|
2019-02-23 21:21:05 +02:00
|
|
|
if (member) {
|
|
|
|
const newRoleIds = new Set(member.roles);
|
|
|
|
for (const change of newPendingRoleChangeObj.changes) {
|
|
|
|
if (change.mode === "+") newRoleIds.add(change.roleId);
|
|
|
|
else newRoleIds.delete(change.roleId);
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
await member.edit({
|
|
|
|
roles: Array.from(newRoleIds.values()),
|
|
|
|
});
|
|
|
|
} catch (e) {
|
|
|
|
logger.warn(
|
2019-11-02 22:11:26 +02:00
|
|
|
`Failed to apply role changes to ${member.username}#${member.discriminator} (${member.id}): ${e.message}`,
|
2019-02-23 21:21:05 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.pendingRoleChanges.delete(memberId);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
this.pendingRoleChanges.set(memberId, newPendingRoleChangeObj);
|
|
|
|
}
|
|
|
|
|
|
|
|
const pendingRoleChangeObj = this.pendingRoleChanges.get(memberId);
|
|
|
|
pendingRoleChangeObj.changes.push({ mode, roleId });
|
|
|
|
|
|
|
|
if (pendingRoleChangeObj.timeout) clearTimeout(pendingRoleChangeObj.timeout);
|
|
|
|
setTimeout(() => pendingRoleChangeObj.applyFn(), ROLE_CHANGE_BATCH_DEBOUNCE_TIME);
|
2019-02-09 14:36:03 +02:00
|
|
|
}
|
2018-07-29 15:18:26 +03:00
|
|
|
|
2019-02-10 22:32:27 +02:00
|
|
|
/**
|
|
|
|
* COMMAND: Clear reaction roles from the specified message
|
|
|
|
*/
|
2019-02-09 14:36:03 +02:00
|
|
|
@d.command("reaction_roles clear", "<messageId:string>")
|
2019-04-13 01:44:18 +03:00
|
|
|
@d.permission("can_manage")
|
2019-02-09 14:36:03 +02:00
|
|
|
async clearReactionRolesCmd(msg: Message, args: { messageId: string }) {
|
|
|
|
const savedMessage = await this.savedMessages.find(args.messageId);
|
|
|
|
if (!savedMessage) {
|
|
|
|
msg.channel.createMessage(errorMessage("Unknown message"));
|
|
|
|
return;
|
|
|
|
}
|
2018-07-29 15:18:26 +03:00
|
|
|
|
2019-02-09 14:36:03 +02:00
|
|
|
const existingReactionRoles = this.reactionRoles.getForMessage(args.messageId);
|
|
|
|
if (!existingReactionRoles) {
|
|
|
|
msg.channel.createMessage(errorMessage("Message doesn't have reaction roles on it"));
|
|
|
|
return;
|
|
|
|
}
|
2018-07-29 15:18:26 +03:00
|
|
|
|
2019-02-09 14:36:03 +02:00
|
|
|
this.reactionRoles.removeFromMessage(args.messageId);
|
2018-07-29 15:18:26 +03:00
|
|
|
|
2019-02-09 14:36:03 +02:00
|
|
|
const channel = this.guild.channels.get(savedMessage.channel_id) as TextChannel;
|
|
|
|
const targetMessage = await channel.getMessage(savedMessage.id);
|
|
|
|
await targetMessage.removeReactions();
|
2018-07-29 15:18:26 +03:00
|
|
|
|
2020-01-11 00:39:32 +11:00
|
|
|
this.sendSuccessMessage(msg.channel, "Reaction roles cleared");
|
2019-02-10 22:32:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-02-23 21:21:05 +02:00
|
|
|
* COMMAND: Refresh reaction roles in the specified message by removing all reactions and re-adding them
|
2019-02-10 22:32:27 +02:00
|
|
|
*/
|
|
|
|
@d.command("reaction_roles refresh", "<messageId:string>")
|
2019-04-13 01:44:18 +03:00
|
|
|
@d.permission("can_manage")
|
2019-02-10 22:32:27 +02:00
|
|
|
async refreshReactionRolesCmd(msg: Message, args: { messageId: string }) {
|
|
|
|
const savedMessage = await this.savedMessages.find(args.messageId);
|
|
|
|
if (!savedMessage) {
|
|
|
|
msg.channel.createMessage(errorMessage("Unknown message"));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-02-18 01:58:21 +02:00
|
|
|
if (this.pendingRefreshes.has(`${savedMessage.channel_id}-${savedMessage.id}`)) {
|
|
|
|
msg.channel.createMessage(errorMessage("Another refresh in progress"));
|
|
|
|
return;
|
2019-02-10 22:32:27 +02:00
|
|
|
}
|
|
|
|
|
2019-02-18 01:58:21 +02:00
|
|
|
await this.refreshReactionRoles(savedMessage.channel_id, savedMessage.id);
|
|
|
|
|
2020-01-11 00:39:32 +11:00
|
|
|
this.sendSuccessMessage(msg.channel, "Reaction roles refreshed");
|
2018-07-29 15:18:26 +03:00
|
|
|
}
|
|
|
|
|
2019-02-23 21:21:05 +02:00
|
|
|
/**
|
|
|
|
* COMMAND: Initialize reaction roles on a message.
|
|
|
|
* The second parameter, reactionRolePairs, is a list of emoji/role pairs separated by a newline. For example:
|
|
|
|
* :zep_twitch: = 473086848831455234
|
|
|
|
* :zep_ps4: = 543184300250759188
|
|
|
|
*/
|
2019-11-30 23:39:29 +02:00
|
|
|
@d.command("reaction_roles", "<messageId:string> <reactionRolePairs:string$>", {
|
|
|
|
options: [
|
|
|
|
{
|
|
|
|
name: "exclusive",
|
|
|
|
shortcut: "e",
|
|
|
|
isSwitch: true,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
})
|
2019-04-13 01:44:18 +03:00
|
|
|
@d.permission("can_manage")
|
2019-11-30 23:39:29 +02:00
|
|
|
async reactionRolesCmd(msg: Message, args: { messageId: string; reactionRolePairs: string; exclusive?: boolean }) {
|
2019-02-09 14:36:03 +02:00
|
|
|
const savedMessage = await this.savedMessages.find(args.messageId);
|
|
|
|
if (!savedMessage) {
|
|
|
|
msg.channel.createMessage(errorMessage("Unknown message"));
|
2018-07-29 15:18:26 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-02-09 14:36:03 +02:00
|
|
|
const channel = await this.guild.channels.get(savedMessage.channel_id);
|
|
|
|
if (!channel || !(channel instanceof TextChannel)) {
|
|
|
|
msg.channel.createMessage(errorMessage("Channel no longer exists"));
|
2018-07-29 15:18:26 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-02-09 14:36:03 +02:00
|
|
|
const targetMessage = await channel.getMessage(args.messageId);
|
|
|
|
if (!targetMessage) {
|
|
|
|
msg.channel.createMessage(errorMessage("Unknown message (2)"));
|
|
|
|
return;
|
|
|
|
}
|
2018-07-29 15:18:26 +03:00
|
|
|
|
2019-02-23 21:21:05 +02:00
|
|
|
// Clear old reaction roles for the message from the DB
|
|
|
|
await this.reactionRoles.removeFromMessage(targetMessage.id);
|
|
|
|
|
2018-07-29 15:18:26 +03:00
|
|
|
// 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
|
2019-07-21 21:15:52 +03:00
|
|
|
const emojiRolePairs: TReactionRolePair[] = args.reactionRolePairs
|
2018-07-29 15:18:26 +03:00
|
|
|
.trim()
|
|
|
|
.split("\n")
|
2019-01-12 13:42:11 +02:00
|
|
|
.map(v => v.split("=").map(v => v.trim())) // tslint:disable-line
|
2018-08-03 19:26:27 +03:00
|
|
|
.map(
|
2019-07-21 21:15:52 +03:00
|
|
|
(pair): TReactionRolePair => {
|
2019-02-06 20:07:53 +02:00
|
|
|
const customEmojiMatch = pair[0].match(/^<a?:(.*?):(\d+)>$/);
|
2018-08-03 19:26:27 +03:00
|
|
|
if (customEmojiMatch) {
|
2019-02-06 20:07:53 +02:00
|
|
|
return [customEmojiMatch[2], pair[1], customEmojiMatch[1]];
|
2018-08-03 19:26:27 +03:00
|
|
|
} else {
|
2019-07-21 21:15:52 +03:00
|
|
|
return pair as TReactionRolePair;
|
2018-08-03 19:26:27 +03:00
|
|
|
}
|
2019-02-09 14:36:03 +02:00
|
|
|
},
|
2018-08-03 19:26:27 +03:00
|
|
|
);
|
2018-07-29 15:18:26 +03:00
|
|
|
|
2019-02-23 21:21:05 +02:00
|
|
|
// Verify the specified emojis and roles are valid and usable
|
|
|
|
for (const pair of emojiRolePairs) {
|
|
|
|
if (pair[0] === CLEAR_ROLES_EMOJI) {
|
|
|
|
msg.channel.createMessage(
|
|
|
|
errorMessage(`The emoji for clearing roles (${CLEAR_ROLES_EMOJI}) is reserved and cannot be used`),
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-04-10 11:30:37 +03:00
|
|
|
try {
|
|
|
|
if (!this.canUseEmoji(pair[0])) {
|
|
|
|
msg.channel.createMessage(
|
|
|
|
errorMessage("I can only use regular emojis and custom emojis from servers I'm on"),
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
if (e instanceof RecoverablePluginError && e.code === ERRORS.INVALID_EMOJI) {
|
|
|
|
msg.channel.createMessage(errorMessage(`Invalid emoji: ${pair[0]}`));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
throw e;
|
2018-07-29 15:18:26 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.guild.roles.has(pair[1])) {
|
|
|
|
msg.channel.createMessage(errorMessage(`Unknown role ${pair[1]}`));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-23 21:21:05 +02:00
|
|
|
// Save the new reaction roles to the database
|
|
|
|
for (const pair of emojiRolePairs) {
|
2019-11-30 23:39:29 +02:00
|
|
|
await this.reactionRoles.add(channel.id, targetMessage.id, pair[0], pair[1], args.exclusive);
|
2018-07-29 15:18:26 +03:00
|
|
|
}
|
|
|
|
|
2019-02-23 21:21:05 +02:00
|
|
|
// Apply the reactions themselves
|
|
|
|
const reactionRoles = await this.reactionRoles.getForMessage(targetMessage.id);
|
|
|
|
await this.applyReactionRoleReactionsToMessage(targetMessage.channel.id, targetMessage.id, reactionRoles);
|
2018-07-29 15:18:26 +03:00
|
|
|
|
2020-01-11 00:39:32 +11:00
|
|
|
this.sendSuccessMessage(msg.channel, "Reaction roles added");
|
2018-07-29 15:18:26 +03:00
|
|
|
}
|
|
|
|
|
2019-02-23 21:21:05 +02:00
|
|
|
/**
|
|
|
|
* When a reaction is added to a message with reaction roles, see which role that reaction matches (if any) and queue
|
|
|
|
* those role changes for the member. Multiple role changes in rapid succession are batched and applied at once.
|
|
|
|
* Reacting with CLEAR_ROLES_EMOJI will queue a removal of all roles granted by this message's reaction roles.
|
|
|
|
*/
|
2018-07-29 15:18:26 +03:00
|
|
|
@d.event("messageReactionAdd")
|
|
|
|
async onAddReaction(msg: Message, emoji: CustomEmoji, userId: string) {
|
2019-02-23 21:21:05 +02:00
|
|
|
// Make sure this message has reaction roles on it
|
|
|
|
const reactionRoles = await this.reactionRoles.getForMessage(msg.id);
|
|
|
|
if (reactionRoles.length === 0) return;
|
2018-07-29 15:18:26 +03:00
|
|
|
|
2019-05-02 08:21:11 +03:00
|
|
|
const member = await this.getMember(userId);
|
2018-07-29 15:18:26 +03:00
|
|
|
if (!member) return;
|
|
|
|
|
2019-02-23 21:21:05 +02:00
|
|
|
if (emoji.name === CLEAR_ROLES_EMOJI) {
|
|
|
|
// User reacted with "clear roles" emoji -> clear their roles
|
|
|
|
const reactionRoleRoleIds = reactionRoles.map(rr => rr.role_id);
|
|
|
|
for (const roleId of reactionRoleRoleIds) {
|
|
|
|
this.addMemberPendingRoleChange(userId, "-", roleId);
|
|
|
|
}
|
2019-02-16 16:03:20 +02:00
|
|
|
|
2019-02-23 21:21:05 +02:00
|
|
|
this.reactionRemoveQueue.add(async () => {
|
|
|
|
await msg.channel.removeMessageReaction(msg.id, CLEAR_ROLES_EMOJI, userId);
|
2019-02-16 11:58:11 +02:00
|
|
|
});
|
|
|
|
} else {
|
2019-02-23 21:21:05 +02:00
|
|
|
// User reacted with a reaction role emoji -> add the role
|
|
|
|
const matchingReactionRole = await this.reactionRoles.getByMessageAndEmoji(msg.id, emoji.id || emoji.name);
|
|
|
|
if (!matchingReactionRole) return;
|
|
|
|
|
2019-11-30 23:39:29 +02:00
|
|
|
// If the reaction role is exclusive, remove any other roles in the message first
|
|
|
|
if (matchingReactionRole.is_exclusive) {
|
|
|
|
const messageReactionRoles = await this.reactionRoles.getForMessage(msg.id);
|
|
|
|
for (const reactionRole of messageReactionRoles) {
|
|
|
|
this.addMemberPendingRoleChange(userId, "-", reactionRole.role_id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-23 21:21:05 +02:00
|
|
|
this.addMemberPendingRoleChange(userId, "+", matchingReactionRole.role_id);
|
2019-02-16 11:58:11 +02:00
|
|
|
}
|
2018-07-29 15:18:26 +03:00
|
|
|
|
2019-02-16 14:40:44 +02:00
|
|
|
// Remove the reaction after a small delay
|
|
|
|
setTimeout(() => {
|
|
|
|
this.reactionRemoveQueue.add(async () => {
|
|
|
|
const reaction = emoji.id ? `${emoji.name}:${emoji.id}` : emoji.name;
|
2019-02-23 23:39:12 +02:00
|
|
|
const wait = sleep(1500);
|
2019-02-16 14:40:44 +02:00
|
|
|
await msg.channel.removeMessageReaction(msg.id, reaction, userId).catch(noop);
|
2019-02-23 21:21:05 +02:00
|
|
|
await wait;
|
2019-02-16 14:40:44 +02:00
|
|
|
});
|
2019-02-23 21:21:05 +02:00
|
|
|
}, 1500);
|
2018-07-29 15:18:26 +03:00
|
|
|
}
|
|
|
|
}
|