import { isAuthorisedAsync } from "../util"; import { ICommand } from "./command"; import { Message } from "discord.js"; import { getRepository } from "typeorm"; import { Guide, GuideType, GuideKeyword } from "@db/entity/Guide"; const makePattern = /^make (\w+)\s+name:(.+)\s*keywords:(.+)\s*contents:((.*[\n\r]*)+)$/i; const deletePattern = /^delete (\w+)\s+(.+)$/i; interface IGuide { name: string, displayName: string, content: string }; async function matchGuide(keywords: string[]) { let 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 function listGuides(msg: Message, guideType: string, message: string) { let repo = getRepository(Guide); let 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(let 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); } } export default { onDirectMention: async (actionsDone, msg, content) => { if (actionsDone) return false; if(msg.attachments.size > 0 || content.length == 0) return false; let parts = content.split(" ").map(s => s.trim()).filter(s => s.length != 0); let guide = await matchGuide(parts); if (guide) { msg.channel.send(guide.content); return true; } return false; }, commands: [ { pattern: makePattern, action: async (msg, s, match) => { if (!await isAuthorisedAsync(msg.member)) return; let type = match[1].toLowerCase(); let name = match[2].trim(); let keywords = match[3].toLowerCase().split(" ").map(s => s.trim()).filter(s => s.length != 0); let contents = match[4].trim(); if(contents.length == 0){ msg.channel.send( `${msg.author.toString()} The guide must have some content!` ); return; } if(!Object.values(GuideType).includes(type)){ msg.channel.send( `${msg.author.toString()} The type ${type} is not a valid guide type!` ); return; } let repo = getRepository(GuideKeyword); let guideRepo = getRepository(Guide); let existingKeywords = await repo.find({ where: [ ...keywords.map(k => ({ keyword: k })) ] }); let existingGuide = await matchGuide(keywords); let addGuide = async () => { let newKeywords = new Set(); let knownKeywords = new Set(existingKeywords.map(e => e.keyword)); for(let word of keywords) { if(!knownKeywords.has(word)) newKeywords.add(word); } let addedKeywords = await repo.save([...newKeywords].map(k => repo.create({ keyword: k }))); await guideRepo.save(guideRepo.create({ content: contents, displayName: name, keywords: [...existingKeywords, ...addedKeywords], type: type as GuideType })); }; if(existingGuide) { let 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: contents }); else await addGuide(); } else await addGuide(); msg.channel.send( `${msg.author.toString()} Added/updated "${name}" (keywords \`${keywords.join(" ")}\`)!` ); } }, { pattern: deletePattern, action: async (msg, s, match) => { if (!await isAuthorisedAsync(msg.member)) return; let type = match[1]; let keywords = match[2].toLowerCase().split(" ").map(s => s.trim()).filter(s => s.length != 0); if(!Object.values(GuideType).includes(type)){ await msg.channel.send( `${msg.author.toString()} The type ${type} is not a valid guide type!` ); return; } let dedupedKeywords = [...new Set(keywords)]; let repo = getRepository(GuideKeyword); let guideRepo = getRepository(Guide); let existingGuide = await matchGuide(keywords); if(existingGuide) { let 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 msg.channel.send(`${msg.author.toString()} Removed ${type} "${keywords.join(" ")}"!`); return; } } await msg.channel.send(`${msg.author.toString()} No such ${type} with keywords \`${keywords.join(" ")}\`! Did you forget to specify all keywords?`); } }, { pattern: "guides", action: async msg => await listGuides(msg, "guide", "Here are the guides I have:") }, { pattern: "memes", action: async msg => await listGuides(msg, "meme", "Here are some random memes I have:") }, { pattern: "misc", action: async msg => await listGuides(msg, "misc", "These are some misc stuff I can also do:") }, ], documentation: { "make name: keywords: contents: ": { auth: true, description: "Creates a new guide of the specified type, the specified keywords and content." }, "delete ": { auth: true, description: "Deletes a guide of the specified type." }, "guides": { auth: false, description: "Lists all guides and keywords that trigger them." }, "memes": { auth: false, description: "Lists all memes and keywords that trigger them." }, "miscs": { auth: false, description: "Lists all additional keywords the bot reacts to." } } } as ICommand;