guide.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. import { isAuthorisedAsync } from "../util";
  2. import { ICommand } from "./command";
  3. import { Message } from "discord.js";
  4. import { getRepository } from "typeorm";
  5. import { Guide, GuideType, GuideKeyword } from "@db/entity/Guide";
  6. const makePattern = /^make (\w+)\s+name:(.+)\s*keywords:(.+)\s*contents:((.*[\n\r]*)+)$/i;
  7. const deletePattern = /^delete (\w+)\s+(.+)$/i;
  8. interface IGuide {
  9. name: string,
  10. displayName: string,
  11. content: string
  12. };
  13. async function matchGuide(keywords: string[]) {
  14. let a = await getRepository(Guide).query(
  15. `select guide.*
  16. from guide
  17. inner join (select gk."guideId", count("guideKeywordId") as gc
  18. from guide_keywords_guide_keyword as gk
  19. where
  20. gk."guideKeywordId" in (select id
  21. from guide_keyword
  22. where
  23. guide_keyword.keyword in (${keywords.map((v, i) => `$${i + 1}`).join(",")}))
  24. group by gk."guideId") as gks
  25. on gks."guideId" = guide.id
  26. order by gc desc
  27. limit 1`,
  28. keywords
  29. ) as Guide[];
  30. if(a.length == 0)
  31. return null;
  32. return a[0];
  33. }
  34. async function listGuides(msg: Message, guideType: string, message: string) {
  35. let repo = getRepository(Guide);
  36. let allGuides = await repo.createQueryBuilder("guide")
  37. .select(["guide.displayName"])
  38. .leftJoinAndSelect("guide.keywords", "keyword")
  39. .where("guide.type = :type", { type: guideType })
  40. .getMany();
  41. const MAX_GUIDES_PER_MSG = 30;
  42. let guides = `${msg.author.toString()} ${message}\n\`\`\``;
  43. let guideNum = 0;
  44. for(let guide of allGuides) {
  45. guides += `${guide.displayName} -- ${guide.keywords.map(k => k.keyword).join(" ")}\n`;
  46. guideNum++;
  47. if(guideNum == MAX_GUIDES_PER_MSG) {
  48. guides += "```";
  49. await msg.channel.send(guides);
  50. guides = "```\n";
  51. guideNum = 0;
  52. }
  53. }
  54. if(guideNum != 0) {
  55. guides += "```\n\nTo display the guides, ping me with one or more keywords, like `@NoctBot sybaris com`";
  56. await msg.channel.send(guides);
  57. }
  58. }
  59. export default {
  60. onDirectMention: async (actionsDone, msg, content) => {
  61. if (actionsDone)
  62. return false;
  63. if(msg.attachments.size > 0 || content.length == 0)
  64. return false;
  65. let parts = content.split(" ").map(s => s.trim()).filter(s => s.length != 0);
  66. let guide = await matchGuide(parts);
  67. if (guide) {
  68. msg.channel.send(guide.content);
  69. return true;
  70. }
  71. return false;
  72. },
  73. commands: [
  74. {
  75. pattern: makePattern,
  76. action: async (msg, s, match) => {
  77. if (!await isAuthorisedAsync(msg.member)) return;
  78. let type = match[1].toLowerCase();
  79. let name = match[2].trim();
  80. let keywords = match[3].toLowerCase().split(" ").map(s => s.trim()).filter(s => s.length != 0);
  81. let 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(!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. let repo = getRepository(GuideKeyword);
  95. let guideRepo = getRepository(Guide);
  96. let existingKeywords = await repo.find({
  97. where: [
  98. ...keywords.map(k => ({ keyword: k }))
  99. ]
  100. });
  101. let existingGuide = await matchGuide(keywords);
  102. let addGuide = async () => {
  103. let newKeywords = new Set<string>();
  104. let knownKeywords = new Set(existingKeywords.map(e => e.keyword));
  105. for(let word of keywords) {
  106. if(!knownKeywords.has(word))
  107. newKeywords.add(word);
  108. }
  109. let 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. let 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. },
  139. {
  140. pattern: deletePattern,
  141. action: async (msg, s, match) => {
  142. if (!await isAuthorisedAsync(msg.member)) return;
  143. let type = match[1];
  144. let keywords = match[2].toLowerCase().split(" ").map(s => s.trim()).filter(s => s.length != 0);
  145. if(!Object.values(GuideType).includes(type)){
  146. await msg.channel.send(
  147. `${msg.author.toString()} The type ${type} is not a valid guide type!`
  148. );
  149. return;
  150. }
  151. let dedupedKeywords = [...new Set(keywords)];
  152. let repo = getRepository(GuideKeyword);
  153. let guideRepo = getRepository(Guide);
  154. let existingGuide = await matchGuide(keywords);
  155. if(existingGuide) {
  156. let guideKeywordsCount = await repo
  157. .createQueryBuilder("keywords")
  158. .leftJoinAndSelect("keywords.relatedGuides", "guide")
  159. .where("guide.id = :id", {id: existingGuide.id })
  160. .getCount();
  161. if(guideKeywordsCount == dedupedKeywords.length) {
  162. await guideRepo.delete({ id: existingGuide.id });
  163. await msg.channel.send(`${msg.author.toString()} Removed ${type} "${keywords.join(" ")}"!`);
  164. return;
  165. }
  166. }
  167. await msg.channel.send(`${msg.author.toString()} No such ${type} with keywords \`${keywords.join(" ")}\`! Did you forget to specify all keywords?`);
  168. }
  169. },
  170. { pattern: "guides", action: async msg => await listGuides(msg, "guide", "Here are the guides I have:") },
  171. { pattern: "memes", action: async msg => await listGuides(msg, "meme", "Here are some random memes I have:") },
  172. { pattern: "misc", action: async msg => await listGuides(msg, "misc", "These are some misc stuff I can also do:") },
  173. ],
  174. documentation: {
  175. "make <guidetype> <NEWLINE>name: <name> <NEWLINE>keywords: <keywords> <NEWLINE>contents: <content>": {
  176. auth: true,
  177. description: "Creates a new guide of the specified type, the specified keywords and content."
  178. },
  179. "delete <guidetype> <keywords>": {
  180. auth: true,
  181. description: "Deletes a guide of the specified type."
  182. },
  183. "guides": {
  184. auth: false,
  185. description: "Lists all guides and keywords that trigger them."
  186. },
  187. "memes": {
  188. auth: false,
  189. description: "Lists all memes and keywords that trigger them."
  190. },
  191. "miscs": {
  192. auth: false,
  193. description: "Lists all additional keywords the bot reacts to."
  194. }
  195. }
  196. } as ICommand;