guide.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. import { Message, TextBasedChannels } from "discord.js";
  2. import { getRepository } from "typeorm";
  3. import { Guide, GuideType, GuideKeyword } from "@shared/db/entity/Guide";
  4. import { Event, BotEventData, Command, ICommandData, Plugin } from "src/model/plugin";
  5. import { tryDo } from "../../../shared/lib/src/common/async_utils";
  6. @Plugin
  7. export class GuideCommands {
  8. async matchGuide(keywords: string[]): Promise<Guide | null> {
  9. const a = await getRepository(Guide).query(
  10. `select guide.*
  11. from guide
  12. inner join (select gk."guideId", count("guideKeywordId") as gc
  13. from guide_keywords_guide_keyword as gk
  14. where
  15. gk."guideKeywordId" in (select id
  16. from guide_keyword
  17. where
  18. guide_keyword.keyword in (${keywords.map((v, i) => `$${i + 1}`).join(",")}))
  19. group by gk."guideId") as gks
  20. on gks."guideId" = guide.id
  21. order by gc desc
  22. limit 1`,
  23. keywords
  24. ) as Guide[];
  25. if (a.length == 0)
  26. return null;
  27. return a[0];
  28. }
  29. async listGuides(msg: Message, guideType: string, message: string): Promise<void> {
  30. const repo = getRepository(Guide);
  31. const allGuides = await repo.createQueryBuilder("guide")
  32. .select(["guide.displayName"])
  33. .leftJoinAndSelect("guide.keywords", "keyword")
  34. .where("guide.type = :type", { type: guideType })
  35. .getMany();
  36. const dmChannelResult = await tryDo(msg.author.createDM());
  37. let channel: TextBasedChannels;
  38. if (dmChannelResult.ok){
  39. await tryDo(msg.delete());
  40. channel = dmChannelResult.result;
  41. } else {
  42. channel = msg.channel;
  43. }
  44. const send = async (content: string) => {
  45. const result = await tryDo(channel.send(content));
  46. if (result.ok)
  47. return;
  48. channel = msg.channel;
  49. await tryDo(channel.send(content));
  50. };
  51. const MAX_GUIDES_PER_MSG = 30;
  52. let guides = `${msg.author.toString()} ${message}\n\`\`\``;
  53. let guideNum = 0;
  54. for (const guide of allGuides) {
  55. guides += `${guide.displayName} -- ${guide.keywords.map(k => k.keyword).join(" ")}\n`;
  56. guideNum++;
  57. if (guideNum == MAX_GUIDES_PER_MSG) {
  58. guides += "```";
  59. await send(guides);
  60. guides = "```\n";
  61. guideNum = 0;
  62. }
  63. }
  64. if (guideNum != 0) {
  65. guides += "```\n\nTo display the guides, ping me with one or more keywords, like `@NoctBot sybaris com`";
  66. await send(guides);
  67. }
  68. }
  69. @Event("directMention")
  70. async displayGuide(data: BotEventData, msg: Message, lowerCaseContent: string): Promise<void> {
  71. if (data.actionsDone)
  72. return;
  73. if (msg.attachments.size > 0 || lowerCaseContent.length == 0)
  74. return;
  75. const parts = lowerCaseContent.split(" ").map(s => s.trim()).filter(s => s.length != 0);
  76. const guide = await this.matchGuide(parts);
  77. if (guide) {
  78. await tryDo(msg.channel.send(guide.content));
  79. data.actionsDone = true;
  80. }
  81. }
  82. @Command({
  83. type: "mention",
  84. pattern: /^make (\w+)\s+name:(.+)\s*keywords:(.+)\s*contents:((.*[\n\r]*)+)$/i,
  85. auth: true,
  86. documentation: {
  87. description: "Creates a new guide of the specified type, the specified keywords and content.",
  88. example: "make <GUIDE TYPE> <NEWLINE>name: <NAME> <NEWLINE> keywords: <KEYWORDS> <NEWLINE> contents: <CONTENTS>"
  89. },
  90. allowDM: true,
  91. })
  92. async makeGuide({ message, contents }: ICommandData): Promise<void> {
  93. const match = contents as RegExpMatchArray;
  94. const type = match[1].toLowerCase();
  95. const name = match[2].trim();
  96. const keywords = match[3].toLowerCase().split(" ").map(s => s.trim()).filter(s => s.length != 0);
  97. const msgContents = match[4].trim();
  98. if (msgContents.length == 0) {
  99. message.reply("the guide must have some content!");
  100. return;
  101. }
  102. if (!(<string[]>Object.values(GuideType)).includes(type)) {
  103. message.reply(`the type ${type} is not a valid guide type!`);
  104. return;
  105. }
  106. const repo = getRepository(GuideKeyword);
  107. const guideRepo = getRepository(Guide);
  108. const existingKeywords = await repo.find({
  109. where: [
  110. ...keywords.map(k => ({ keyword: k }))
  111. ]
  112. });
  113. const existingGuide = await this.matchGuide(keywords);
  114. const addGuide = async () => {
  115. const newKeywords = new Set<string>();
  116. const knownKeywords = new Set(existingKeywords.map(e => e.keyword));
  117. for (const word of keywords) {
  118. if (!knownKeywords.has(word))
  119. newKeywords.add(word);
  120. }
  121. const addedKeywords = await repo.save([...newKeywords].map(k => repo.create({
  122. keyword: k
  123. })));
  124. await guideRepo.save(guideRepo.create({
  125. content: msgContents,
  126. displayName: name,
  127. keywords: [...existingKeywords, ...addedKeywords],
  128. type: type as GuideType
  129. }));
  130. };
  131. if (existingGuide) {
  132. const guideKeywordsCount = await repo
  133. .createQueryBuilder("keywords")
  134. .leftJoinAndSelect("keywords.relatedGuides", "guide")
  135. .where("guide.id = :id", { id: existingGuide.id })
  136. .getCount();
  137. if (guideKeywordsCount == existingKeywords.length)
  138. await guideRepo.update({ id: existingGuide.id }, {
  139. displayName: name,
  140. content: msgContents
  141. });
  142. else
  143. await addGuide();
  144. } else
  145. await addGuide();
  146. message.reply(
  147. `Added/updated "${name}" (keywords \`${keywords.join(" ")}\`)!`
  148. );
  149. }
  150. @Command({
  151. type: "mention",
  152. pattern: /^delete (\w+)\s+(.+)$/i,
  153. auth: true,
  154. documentation: {
  155. example: "delete <guidetype> <keywords>",
  156. description: "Deletes a guide with the specified keywords"
  157. },
  158. allowDM: true,
  159. })
  160. async deleteGuide({ message, contents }: ICommandData): Promise<void> {
  161. const match = contents as RegExpMatchArray;
  162. const type = match[1];
  163. const keywords = match[2].toLowerCase().split(" ").map(s => s.trim()).filter(s => s.length != 0);
  164. if (!(<string[]>Object.values(GuideType)).includes(type)) {
  165. await message.reply(
  166. `The type ${type} is not a valid guide type!`
  167. );
  168. return;
  169. }
  170. const dedupedKeywords = [...new Set(keywords)];
  171. const repo = getRepository(GuideKeyword);
  172. const guideRepo = getRepository(Guide);
  173. const existingGuide = await this.matchGuide(keywords);
  174. if (existingGuide) {
  175. const guideKeywordsCount = await repo
  176. .createQueryBuilder("keywords")
  177. .leftJoinAndSelect("keywords.relatedGuides", "guide")
  178. .where("guide.id = :id", { id: existingGuide.id })
  179. .getCount();
  180. if (guideKeywordsCount == dedupedKeywords.length) {
  181. await guideRepo.delete({ id: existingGuide.id });
  182. await message.reply(`removed ${type} "${keywords.join(" ")}"!`);
  183. return;
  184. }
  185. }
  186. await message.reply(`no such ${type} with keywords \`${keywords.join(" ")}\`! Did you forget to specify all keywords?`);
  187. }
  188. @Command({
  189. type: "mention",
  190. pattern: "guides",
  191. documentation: {
  192. description: "Lists all guides and keywords that trigger them.",
  193. example: "guides"
  194. },
  195. allowDM: true,
  196. })
  197. async showGuides({ message }: ICommandData): Promise<void> {
  198. await this.listGuides(message, "guide", "Here are the guides I have:");
  199. }
  200. @Command({
  201. type: "mention",
  202. pattern: "memes",
  203. documentation: {
  204. description: "Lists all memes and keywords that trigger them.",
  205. example: "memes"
  206. },
  207. allowDM: true,
  208. })
  209. async showMemes({ message }: ICommandData): Promise<void> {
  210. await this.listGuides(message, "meme", "Here are some random memes I have:");
  211. }
  212. @Command({
  213. type: "mention",
  214. pattern: "misc",
  215. documentation: {
  216. description: "Lists all additional keywords the bot reacts to.",
  217. example: "misc"
  218. },
  219. allowDM: true,
  220. })
  221. async showMisc({ message }: ICommandData): Promise<void> {
  222. await this.listGuides(message, "misc", "These are some misc stuff I can also do:");
  223. }
  224. }