guide.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import { isAuthorisedAsync } from "../util";
  2. import { Message } from "discord.js";
  3. import { getRepository } from "typeorm";
  4. import { Guide, GuideType, GuideKeyword } from "@shared/db/entity/Guide";
  5. import { CommandSet, Action, ActionType, Command } from "src/model/command";
  6. @CommandSet
  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 MAX_GUIDES_PER_MSG = 30;
  37. let guides = `${msg.author.toString()} ${message}\n\`\`\``;
  38. let guideNum = 0;
  39. for (const guide of allGuides) {
  40. guides += `${guide.displayName} -- ${guide.keywords.map(k => k.keyword).join(" ")}\n`;
  41. guideNum++;
  42. if (guideNum == MAX_GUIDES_PER_MSG) {
  43. guides += "```";
  44. await msg.channel.send(guides);
  45. guides = "```\n";
  46. guideNum = 0;
  47. }
  48. }
  49. if (guideNum != 0) {
  50. guides += "```\n\nTo display the guides, ping me with one or more keywords, like `@NoctBot sybaris com`";
  51. await msg.channel.send(guides);
  52. }
  53. }
  54. @Action(ActionType.DIRECT_MENTION)
  55. async displayGuide(actionsDone: boolean, msg: Message, content: string): Promise<boolean> {
  56. if (actionsDone)
  57. return false;
  58. if (msg.attachments.size > 0 || content.length == 0)
  59. return false;
  60. const parts = content.split(" ").map(s => s.trim()).filter(s => s.length != 0);
  61. const guide = await this.matchGuide(parts);
  62. if (guide) {
  63. msg.channel.send(guide.content);
  64. return true;
  65. }
  66. return false;
  67. }
  68. @Command({
  69. pattern: /^make (\w+)\s+name:(.+)\s*keywords:(.+)\s*contents:((.*[\n\r]*)+)$/i,
  70. auth: true,
  71. documentation: {
  72. description: "Creates a new guide of the specified type, the specified keywords and content.",
  73. example: "make <GUIDE TYPE> <NEWLINE>name: <NAME> <NEWLINE> keywords: <KEYWORDS> <NEWLINE> contents: <CONTENTS>"
  74. }
  75. })
  76. async makeGuide(msg: Message, content: string, match: RegExpMatchArray): Promise<void> {
  77. if (!await isAuthorisedAsync(msg.member)) return;
  78. const type = match[1].toLowerCase();
  79. const name = match[2].trim();
  80. const keywords = match[3].toLowerCase().split(" ").map(s => s.trim()).filter(s => s.length != 0);
  81. const contents = match[4].trim();
  82. if (contents.length == 0) {
  83. msg.channel.send(
  84. `${msg.author.toString()} The guide must have some content!`
  85. );
  86. return;
  87. }
  88. if (!(<string[]>Object.values(GuideType)).includes(type)) {
  89. msg.channel.send(
  90. `${msg.author.toString()} The type ${type} is not a valid guide type!`
  91. );
  92. return;
  93. }
  94. const repo = getRepository(GuideKeyword);
  95. const guideRepo = getRepository(Guide);
  96. const existingKeywords = await repo.find({
  97. where: [
  98. ...keywords.map(k => ({ keyword: k }))
  99. ]
  100. });
  101. const existingGuide = await this.matchGuide(keywords);
  102. const addGuide = async () => {
  103. const newKeywords = new Set<string>();
  104. const knownKeywords = new Set(existingKeywords.map(e => e.keyword));
  105. for (const word of keywords) {
  106. if (!knownKeywords.has(word))
  107. newKeywords.add(word);
  108. }
  109. const addedKeywords = await repo.save([...newKeywords].map(k => repo.create({
  110. keyword: k
  111. })));
  112. await guideRepo.save(guideRepo.create({
  113. content: contents,
  114. displayName: name,
  115. keywords: [...existingKeywords, ...addedKeywords],
  116. type: type as GuideType
  117. }));
  118. };
  119. if (existingGuide) {
  120. const guideKeywordsCount = await repo
  121. .createQueryBuilder("keywords")
  122. .leftJoinAndSelect("keywords.relatedGuides", "guide")
  123. .where("guide.id = :id", { id: existingGuide.id })
  124. .getCount();
  125. if (guideKeywordsCount == existingKeywords.length)
  126. await guideRepo.update({ id: existingGuide.id }, {
  127. displayName: name,
  128. content: contents
  129. });
  130. else
  131. await addGuide();
  132. } else
  133. await addGuide();
  134. msg.channel.send(
  135. `${msg.author.toString()} Added/updated "${name}" (keywords \`${keywords.join(" ")}\`)!`
  136. );
  137. }
  138. @Command({
  139. pattern: /^delete (\w+)\s+(.+)$/i,
  140. auth: true,
  141. documentation: {
  142. example: "delete <guidetype> <keywords>",
  143. description: "Deletes a guide with the specified keywords"
  144. }
  145. })
  146. async deleteGuide(msg: Message, content: string, match: RegExpMatchArray): Promise<void> {
  147. if (!await isAuthorisedAsync(msg.member)) return;
  148. const type = match[1];
  149. const keywords = match[2].toLowerCase().split(" ").map(s => s.trim()).filter(s => s.length != 0);
  150. if (!(<string[]>Object.values(GuideType)).includes(type)) {
  151. await msg.channel.send(
  152. `${msg.author.toString()} The type ${type} is not a valid guide type!`
  153. );
  154. return;
  155. }
  156. const dedupedKeywords = [...new Set(keywords)];
  157. const repo = getRepository(GuideKeyword);
  158. const guideRepo = getRepository(Guide);
  159. const existingGuide = await this.matchGuide(keywords);
  160. if (existingGuide) {
  161. const guideKeywordsCount = await repo
  162. .createQueryBuilder("keywords")
  163. .leftJoinAndSelect("keywords.relatedGuides", "guide")
  164. .where("guide.id = :id", { id: existingGuide.id })
  165. .getCount();
  166. if (guideKeywordsCount == dedupedKeywords.length) {
  167. await guideRepo.delete({ id: existingGuide.id });
  168. await msg.channel.send(`${msg.author.toString()} Removed ${type} "${keywords.join(" ")}"!`);
  169. return;
  170. }
  171. }
  172. await msg.channel.send(`${msg.author.toString()} No such ${type} with keywords \`${keywords.join(" ")}\`! Did you forget to specify all keywords?`);
  173. }
  174. @Command({ pattern: "guides", documentation: { description: "Lists all guides and keywords that trigger them.", example: "guides" } })
  175. async showGuides(msg: Message): Promise<void> {
  176. await this.listGuides(msg, "guide", "Here are the guides I have:");
  177. }
  178. @Command({ pattern: "memes", documentation: {description: "Lists all memes and keywords that trigger them.", example: "memes"} })
  179. async showMemes(msg: Message): Promise<void> {
  180. await this.listGuides(msg, "meme", "Here are some random memes I have:");
  181. }
  182. @Command({ pattern: "misc", documentation: {description: "Lists all additional keywords the bot reacts to.", example: "misc"} })
  183. async showMisc(msg: Message): Promise<void> {
  184. await this.listGuides(msg, "misc", "These are some misc stuff I can also do:");
  185. }
  186. }