import { client } from "../client"; import { getRepository } from "typeorm"; import { KnownUser } from "@shared/db/entity/KnownUser"; import { ReactionType, ReactionEmote } from "@shared/db/entity/ReactionEmote"; import { Message, User, PartialUser, MessageReaction as DiscordMessageReaction } from "discord.js"; import { logger } from "src/logging"; import { Command, ICommandData, Event, BotEventData, Plugin } from "src/model/plugin"; import { MessageReaction } from "@shared/db/entity/MessageReaction"; import { tryDo } from "@shared/common/async_utils"; const pattern = /^react to\s+"([^"]+)"\s+with\s+<:[^:]+:([^>]+)>$/i; async function getRandomEmotes(allowedTypes: ReactionType[], limit: number) { const reactionEmotesRepo = getRepository(ReactionEmote); const a = await reactionEmotesRepo.query(` select distinct on (type) type, "reactionId" from ( select * from reaction_emote where type in (${allowedTypes.map((s, i) => `$${i + 1}`).join(",")}) order by random(), random() limit ${limit} ) as sub`, allowedTypes) as ReactionEmote[]; return a; } @Plugin export class ReactCommands { private REACT_PROBABILITY = 0.3; @Event("messageReactionAdd") async randomReact(data: BotEventData, reaction: DiscordMessageReaction, user: User | PartialUser): Promise { if (Math.random() <= this.REACT_PROBABILITY && !user.bot) { const msg = reaction.message; logger.verbose(`Reacting to message ${msg.id} because user ${user.tag} reacted to it`); const result = await tryDo(msg.react(reaction.emoji)); if (!result.ok) { logger.error("Failed to react to user %s#%s (%s): %s", msg.author?.username, msg.author?.discriminator, msg.author?.id, result.error); } } } @Command({ type: "mention", pattern: "react to", auth: true, documentation: { description: "React to with .", example: "react to \"\" with " } }) async addReaction({ message, contents }: ICommandData): Promise { const reactContents = pattern.exec(contents as string); if (reactContents != null) { const reactable = reactContents[1].trim().toLowerCase(); const reactionEmoji = reactContents[2]; if (!client.bot.emojis.cache.has(reactionEmoji)) { message.reply("I cannot react with this emoji :("); return; } const repo = getRepository(MessageReaction); const msgReaction = repo.create({ message: reactable, reactionEmoteId: reactionEmoji }); await repo.save(msgReaction); message.reply("added reaction!"); } } @Command({ type: "mention", pattern: "remove reaction to", auth: true, documentation: { description: "Stops reacting to .", example: "remove reaction to " } }) async removeReaction({message, contents}: ICommandData): Promise { const content = (contents as string).substring("remove reaction to ".length).trim().toLowerCase(); const repo = getRepository(MessageReaction); const result = await repo.delete({ message: content }); if (result.affected == 0) { message.reply("no such reaction available!"); return; } message.reply("removed reaction!"); } @Command({ type: "mention", pattern: "reactions", documentation: { description: "Lists all known messages this bot can react to.", example: "reactions" } }) async listReactions({ message }: ICommandData): Promise { const reactionsRepo = getRepository(MessageReaction); const messages = await reactionsRepo.find({ select: ["message"] }); const reactions = messages.reduce((p, c) => `${p}\n${c.message}`, ""); message.reply(`I'll react to the following messages:\n\`\`\`${reactions}\n\`\`\``); } @Event("message") async reactToMentions(data: BotEventData, msg: Message): Promise { if (data.actionsDone) return; const content = msg.cleanContent.trim(); const lowerContent = content.toLowerCase(); const reactionRepo = getRepository(MessageReaction); const usersRepo = getRepository(KnownUser); const message = await reactionRepo.findOne({ message: lowerContent }); if (message) { const emoji = client.bot.emojis.resolve(message.reactionEmoteId); if (emoji) { const result = await tryDo(msg.react(emoji)); if (!result.ok) { logger.error("Failed to react to user %s#%s (%s): %s", msg.author.username, msg.author.discriminator, msg.author.id, result.error); } } data.actionsDone = true; return; } if (msg.mentions.users.size == 0) return; const knownUsers = await usersRepo.find({ select: ["mentionReactionType"], where: [...msg.mentions.users.map(u => ({ userID: u.id }))] }); if (knownUsers.length == 0) return; const reactionEmoteTypes = new Set(); for (const user of knownUsers) { if (user.mentionReactionType == ReactionType.NONE) continue; reactionEmoteTypes.add(user.mentionReactionType); } if(reactionEmoteTypes.size == 0) return; const randomEmotes = await getRandomEmotes([...reactionEmoteTypes], 5); if (randomEmotes.length == 0) return; for (const emote of randomEmotes) { const emoji = client.bot.emojis.resolve(emote.reactionId); if(emoji) { const result = await tryDo(msg.react(emoji)); if (!result.ok) { logger.error("Failed to react to user %s#%s (%s): %s", msg.author.username, msg.author.discriminator, msg.author.id, result.error); } } } data.actionsDone = true; } @Event("indirectMention") async reactToPing(data: BotEventData, msg: Message): Promise { if (data.actionsDone) return; let emoteType = ReactionType.ANGERY; const repo = getRepository(KnownUser); const knownUser = await repo.findOne({ select: ["replyReactionType"], where: [{ userID: msg.author.id }] }); if (knownUser) { if (knownUser.replyReactionType == ReactionType.NONE) return; emoteType = knownUser.replyReactionType; } const emotes = await getRandomEmotes([emoteType], 1); if (emotes.length != 1) return; const emote = client.bot.emojis.resolve(emotes[0].reactionId); if (!emote) { logger.warn(`Emote ${emotes[0]} no longer is valid. Deleting invalid emojis from the list...`); const emotesRepo = getRepository(ReactionEmote); await emotesRepo.delete({ reactionId: emotes[0].reactionId }); return; } msg.channel.send(emote.toString()); data.actionsDone = true; } }