react.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import { client } from "../client";
  2. import { getRepository } from "typeorm";
  3. import { KnownUser } from "@shared/db/entity/KnownUser";
  4. import { ReactionType, ReactionEmote } from "@shared/db/entity/ReactionEmote";
  5. import { Message, User, PartialUser, MessageReaction as DiscordMessageReaction } from "discord.js";
  6. import { logger } from "src/logging";
  7. import { Command, ICommandData, Event, BotEventData, Plugin } from "src/model/plugin";
  8. import { MessageReaction } from "@shared/db/entity/MessageReaction";
  9. import { tryDo } from "@shared/common/async_utils";
  10. const pattern = /^react to\s+"([^"]+)"\s+with\s+<:[^:]+:([^>]+)>$/i;
  11. async function getRandomEmotes(allowedTypes: ReactionType[], limit: number) {
  12. const reactionEmotesRepo = getRepository(ReactionEmote);
  13. const a = await reactionEmotesRepo.query(`
  14. select distinct on (type) type, "reactionId"
  15. from
  16. ( select *
  17. from reaction_emote
  18. where type in (${allowedTypes.map((s, i) => `$${i + 1}`).join(",")})
  19. order by random(), random()
  20. limit ${limit}
  21. ) as sub`, allowedTypes) as ReactionEmote[];
  22. return a;
  23. }
  24. @Plugin
  25. export class ReactCommands {
  26. private REACT_PROBABILITY = 0.3;
  27. @Event("messageReactionAdd")
  28. async randomReact(data: BotEventData, reaction: DiscordMessageReaction, user: User | PartialUser): Promise<void> {
  29. if (Math.random() <= this.REACT_PROBABILITY && !user.bot) {
  30. const msg = reaction.message;
  31. logger.verbose(`Reacting to message ${msg.id} because user ${user.tag} reacted to it`);
  32. const result = await tryDo(msg.react(reaction.emoji));
  33. if (!result.ok) {
  34. logger.error("Failed to react to user %s#%s (%s): %s", msg.author?.username, msg.author?.discriminator, msg.author?.id, result.error);
  35. }
  36. }
  37. }
  38. @Command({
  39. type: "mention",
  40. pattern: "react to",
  41. auth: true,
  42. documentation: {
  43. description: "React to <message> with <emote>.",
  44. example: "react to \"<message>\" with <emote>"
  45. }
  46. })
  47. async addReaction({ message, contents }: ICommandData): Promise<void> {
  48. const reactContents = pattern.exec(contents as string);
  49. if (reactContents != null) {
  50. const reactable = reactContents[1].trim().toLowerCase();
  51. const reactionEmoji = reactContents[2];
  52. if (!client.bot.emojis.cache.has(reactionEmoji)) {
  53. message.reply("I cannot react with this emoji :(");
  54. return;
  55. }
  56. const repo = getRepository(MessageReaction);
  57. const msgReaction = repo.create({
  58. message: reactable,
  59. reactionEmoteId: reactionEmoji
  60. });
  61. await repo.save(msgReaction);
  62. message.reply("added reaction!");
  63. }
  64. }
  65. @Command({
  66. type: "mention",
  67. pattern: "remove reaction to",
  68. auth: true,
  69. documentation: {
  70. description: "Stops reacting to <message>.",
  71. example: "remove reaction to <message>"
  72. }
  73. })
  74. async removeReaction({message, contents}: ICommandData): Promise<void> {
  75. const content = (contents as string).substring("remove reaction to ".length).trim().toLowerCase();
  76. const repo = getRepository(MessageReaction);
  77. const result = await repo.delete({ message: content });
  78. if (result.affected == 0) {
  79. message.reply("no such reaction available!");
  80. return;
  81. }
  82. message.reply("removed reaction!");
  83. }
  84. @Command({
  85. type: "mention",
  86. pattern: "reactions",
  87. documentation: {
  88. description: "Lists all known messages this bot can react to.",
  89. example: "reactions"
  90. }
  91. })
  92. async listReactions({ message }: ICommandData): Promise<void> {
  93. const reactionsRepo = getRepository(MessageReaction);
  94. const messages = await reactionsRepo.find({
  95. select: ["message"]
  96. });
  97. const reactions = messages.reduce((p, c) => `${p}\n${c.message}`, "");
  98. message.reply(`I'll react to the following messages:\n\`\`\`${reactions}\n\`\`\``);
  99. }
  100. @Event("message")
  101. async reactToMentions(data: BotEventData, msg: Message): Promise<void> {
  102. if (data.actionsDone)
  103. return;
  104. const content = msg.cleanContent.trim();
  105. const lowerContent = content.toLowerCase();
  106. const reactionRepo = getRepository(MessageReaction);
  107. const usersRepo = getRepository(KnownUser);
  108. const message = await reactionRepo.findOne({ message: lowerContent });
  109. if (message) {
  110. const emoji = client.bot.emojis.resolve(message.reactionEmoteId);
  111. if (emoji) {
  112. const result = await tryDo(msg.react(emoji));
  113. if (!result.ok) {
  114. logger.error("Failed to react to user %s#%s (%s): %s", msg.author.username, msg.author.discriminator, msg.author.id, result.error);
  115. }
  116. }
  117. data.actionsDone = true;
  118. return;
  119. }
  120. if (msg.mentions.users.size == 0)
  121. return;
  122. const knownUsers = await usersRepo.find({
  123. select: ["mentionReactionType"],
  124. where: [...msg.mentions.users.map(u => ({ userID: u.id }))]
  125. });
  126. if (knownUsers.length == 0)
  127. return;
  128. const reactionEmoteTypes = new Set<ReactionType>();
  129. for (const user of knownUsers) {
  130. if (user.mentionReactionType == ReactionType.NONE)
  131. continue;
  132. reactionEmoteTypes.add(user.mentionReactionType);
  133. }
  134. if(reactionEmoteTypes.size == 0)
  135. return;
  136. const randomEmotes = await getRandomEmotes([...reactionEmoteTypes], 5);
  137. if (randomEmotes.length == 0)
  138. return;
  139. for (const emote of randomEmotes) {
  140. const emoji = client.bot.emojis.resolve(emote.reactionId);
  141. if(emoji) {
  142. const result = await tryDo(msg.react(emoji));
  143. if (!result.ok) {
  144. logger.error("Failed to react to user %s#%s (%s): %s", msg.author.username, msg.author.discriminator, msg.author.id, result.error);
  145. }
  146. }
  147. }
  148. data.actionsDone = true;
  149. }
  150. @Event("indirectMention")
  151. async reactToPing(data: BotEventData, msg: Message): Promise<void> {
  152. if (data.actionsDone)
  153. return;
  154. let emoteType = ReactionType.ANGERY;
  155. const repo = getRepository(KnownUser);
  156. const knownUser = await repo.findOne({
  157. select: ["replyReactionType"],
  158. where: [{
  159. userID: msg.author.id
  160. }]
  161. });
  162. if (knownUser) {
  163. if (knownUser.replyReactionType == ReactionType.NONE)
  164. return;
  165. emoteType = knownUser.replyReactionType;
  166. }
  167. const emotes = await getRandomEmotes([emoteType], 1);
  168. if (emotes.length != 1)
  169. return;
  170. const emote = client.bot.emojis.resolve(emotes[0].reactionId);
  171. if (!emote) {
  172. logger.warn(`Emote ${emotes[0]} no longer is valid. Deleting invalid emojis from the list...`);
  173. const emotesRepo = getRepository(ReactionEmote);
  174. await emotesRepo.delete({ reactionId: emotes[0].reactionId });
  175. return;
  176. }
  177. msg.channel.send(emote.toString());
  178. data.actionsDone = true;
  179. }
  180. }