import { Message } from "discord.js"; import { getRepository } from "typeorm"; import { Guide, GuideType, GuideKeyword } from "@shared/db/entity/Guide"; import { Event, BotEventData, Command, ICommandData, Plugin } from "src/model/plugin"; @Plugin export class GuideCommands { async matchGuide(keywords: string[]): Promise { const a = await getRepository(Guide).query( `select guide.* from guide inner join (select gk."guideId", count("guideKeywordId") as gc from guide_keywords_guide_keyword as gk where gk."guideKeywordId" in (select id from guide_keyword where guide_keyword.keyword in (${keywords.map((v, i) => `$${i + 1}`).join(",")})) group by gk."guideId") as gks on gks."guideId" = guide.id order by gc desc limit 1`, keywords ) as Guide[]; if (a.length == 0) return null; return a[0]; } async listGuides(msg: Message, guideType: string, message: string): Promise { const repo = getRepository(Guide); const allGuides = await repo.createQueryBuilder("guide") .select(["guide.displayName"]) .leftJoinAndSelect("guide.keywords", "keyword") .where("guide.type = :type", { type: guideType }) .getMany(); const MAX_GUIDES_PER_MSG = 30; let guides = `${msg.author.toString()} ${message}\n\`\`\``; let guideNum = 0; for (const guide of allGuides) { guides += `${guide.displayName} -- ${guide.keywords.map(k => k.keyword).join(" ")}\n`; guideNum++; if (guideNum == MAX_GUIDES_PER_MSG) { guides += "```"; await msg.channel.send(guides); guides = "```\n"; guideNum = 0; } } if (guideNum != 0) { guides += "```\n\nTo display the guides, ping me with one or more keywords, like `@NoctBot sybaris com`"; await msg.channel.send(guides); } } @Event("directMention") async displayGuide(data: BotEventData, msg: Message, lowerCaseContent: string): Promise { if (data.actionsDone) return; if (msg.attachments.size > 0 || lowerCaseContent.length == 0) return; const parts = lowerCaseContent.split(" ").map(s => s.trim()).filter(s => s.length != 0); const guide = await this.matchGuide(parts); if (guide) { msg.channel.send(guide.content); data.actionsDone = true; } } @Command({ type: "mention", pattern: /^make (\w+)\s+name:(.+)\s*keywords:(.+)\s*contents:((.*[\n\r]*)+)$/i, auth: true, documentation: { description: "Creates a new guide of the specified type, the specified keywords and content.", example: "make name: keywords: contents: " } }) async makeGuide({ message, contents }: ICommandData): Promise { const match = contents as RegExpMatchArray; const type = match[1].toLowerCase(); const name = match[2].trim(); const keywords = match[3].toLowerCase().split(" ").map(s => s.trim()).filter(s => s.length != 0); const msgContents = match[4].trim(); if (msgContents.length == 0) { message.reply("the guide must have some content!"); return; } if (!(Object.values(GuideType)).includes(type)) { message.reply(`the type ${type} is not a valid guide type!`); return; } const repo = getRepository(GuideKeyword); const guideRepo = getRepository(Guide); const existingKeywords = await repo.find({ where: [ ...keywords.map(k => ({ keyword: k })) ] }); const existingGuide = await this.matchGuide(keywords); const addGuide = async () => { const newKeywords = new Set(); const knownKeywords = new Set(existingKeywords.map(e => e.keyword)); for (const word of keywords) { if (!knownKeywords.has(word)) newKeywords.add(word); } const addedKeywords = await repo.save([...newKeywords].map(k => repo.create({ keyword: k }))); await guideRepo.save(guideRepo.create({ content: msgContents, displayName: name, keywords: [...existingKeywords, ...addedKeywords], type: type as GuideType })); }; if (existingGuide) { const guideKeywordsCount = await repo .createQueryBuilder("keywords") .leftJoinAndSelect("keywords.relatedGuides", "guide") .where("guide.id = :id", { id: existingGuide.id }) .getCount(); if (guideKeywordsCount == existingKeywords.length) await guideRepo.update({ id: existingGuide.id }, { displayName: name, content: msgContents }); else await addGuide(); } else await addGuide(); message.reply( `Added/updated "${name}" (keywords \`${keywords.join(" ")}\`)!` ); } @Command({ type: "mention", pattern: /^delete (\w+)\s+(.+)$/i, auth: true, documentation: { example: "delete ", description: "Deletes a guide with the specified keywords" } }) async deleteGuide({ message, contents }: ICommandData): Promise { const match = contents as RegExpMatchArray; const type = match[1]; const keywords = match[2].toLowerCase().split(" ").map(s => s.trim()).filter(s => s.length != 0); if (!(Object.values(GuideType)).includes(type)) { await message.reply( `The type ${type} is not a valid guide type!` ); return; } const dedupedKeywords = [...new Set(keywords)]; const repo = getRepository(GuideKeyword); const guideRepo = getRepository(Guide); const existingGuide = await this.matchGuide(keywords); if (existingGuide) { const guideKeywordsCount = await repo .createQueryBuilder("keywords") .leftJoinAndSelect("keywords.relatedGuides", "guide") .where("guide.id = :id", { id: existingGuide.id }) .getCount(); if (guideKeywordsCount == dedupedKeywords.length) { await guideRepo.delete({ id: existingGuide.id }); await message.reply(`removed ${type} "${keywords.join(" ")}"!`); return; } } await message.reply(`no such ${type} with keywords \`${keywords.join(" ")}\`! Did you forget to specify all keywords?`); } @Command({ type: "mention", pattern: "guides", documentation: { description: "Lists all guides and keywords that trigger them.", example: "guides" } }) async showGuides({ message }: ICommandData): Promise { await this.listGuides(message, "guide", "Here are the guides I have:"); } @Command({ type: "mention", pattern: "memes", documentation: { description: "Lists all memes and keywords that trigger them.", example: "memes" } }) async showMemes({ message }: ICommandData): Promise { await this.listGuides(message, "meme", "Here are some random memes I have:"); } @Command({ type: "mention", pattern: "misc", documentation: { description: "Lists all additional keywords the bot reacts to.", example: "misc" } }) async showMisc({ message }: ICommandData): Promise { await this.listGuides(message, "misc", "These are some misc stuff I can also do:"); } }