guide.ts 8.5 KB

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