!7 cmodule TomiiBoiAnswers > DynPrintLogAndEnabled { switchable int port = 8080; switchable S frontendName = "tomiiBoiDiscordBot"; transient CRUD categoriesCRUD; transient CRUD serversCRUD; transient CRUD channelsCRUD; transient CRUD serverToCategoryCRUD; start { init(); categoriesCRUD = new CRUD(Category); serversCRUD = new CRUD(Server); serverToCategoryCRUD = new CRUD(ServerToCategory); channelsCRUD = new CRUD(Channel); thread { dm_serveHttpFromFunction(port, lambda2 html); print("Admin live at: http://localhost:" + port); dm_registerAs('tomiiBoiQA); } } visualize { JComponent c = super.visualize(); addComponents(buttons, jbutton("Open admin in browser", rThread { openURLInBrowser("http://localhost:" + port) }), jPopDownButton_noText( "Import data...", rThreadEnter importData)); c = jtabs( "Main", c, "Categories", categoriesCRUD.visualize(), "Servers", serversCRUD.visualize(), "Server-to-category", serverToCategoryCRUD.visualize(), "Channels", channelsCRUD.visualize()); channelsCRUD.addButton("Update list", rThreadEnter grabChannels); ret c; } void importData { selectFile("Tomii Brain File", voidfunc(File f) enter { replaceConceptsWithTextFileOnNextStart(f); dm_reloadModule(); }); } void grabChannels { dm_call(frontendName, 'grabChannels); } // API Server addServer(S serverID, S name) { ret csetAndReturn(uniq Server(+serverID), +name); } Channel addChannel(Server server, S channelID, S name) { ret csetAndReturn(uniq Channel(+server, +channelID), +name); } Channel channelForID(S channelID) { ret conceptWhere Channel(+channelID); } } //sS mainBotID = #1026411; static int maxTypos = 1; sS adminTitle = "Tomii Boi Chat Bot Admin"; sS shortLink; // catchy link to admin if available sS embedderLink; // where chat bot will go sS goDaddyEmbedCode; static Cache> consistencyCheckResult = new(lambda0 consistencyCheck); concept Server { S serverID, name; toString { ret name; } } concept ServerToCategory { Server server; Category category; bool enabled; sS _fieldOrder = "server category enabled"; } concept Channel { Server server; S channelID; S name; bool botEnabled = true; toString { ret name; } sS _fieldOrder = "botEnabled name server"; } concept Category { int index; S name; bool onByDefault = true; toString { ret name; } } concept Language { int index; S code; } concept QA { int index; S questions; // line by line S patterns; // to be determined S answers; // one answer, or "random { answers separated by empty lines }" S category; // "business", "smalltalk" etc. transient MMOPattern parsedPattern; sS _fieldOrder = "category index questions patterns answers"; void change { parsedPattern = null; super.change(); } MMOPattern parsedPattern() { if (parsedPattern == null) parsedPattern = mmo_parsePattern(patterns); ret parsedPattern; } } // keep these because otherwise deserialization breaks concept Settings {} concept Defaults {} svoid init { processConceptsOverwriteFile(); //print("fieldOrder", getFieldOrder(QA)); dbIndexing(Category, 'index, Channel, 'channelID); print("QA count: " + countConcepts(QA)); // categories, default category int i = 0; for (S category : ll("business", "smalltalk", "btd")) cset(uniq(Category, name := category), index := i++); cset(conceptsWhere QA(category := null), category := "business"); onConceptsChange(r { consistencyCheckResult.clear(); }); reindexQAs(); } html { try { print(+uri); // force auth /*try answer callHtmlMethod(getBot(mainBotID), "/auth-only", mapPlus( params, uri := rawLink(uri)));*/ if (eq(uri, "/download")) ret serveText(conceptsStructure()); HCRUD_Concepts data = new HCRUD_Concepts(QA) { S itemName() { ret "question"; } S fieldHelp(S field) { if (eq(field, "index")) ret "lower index = higher matching precedence"; if (eq(field, "patterns")) ret [[Phrases to match. Use "+" as "and" operator, commas as "or" operator. Round brackets for grouping. Quotes for special characters. Put exclamation mark after phrase to disable typo correction.]]; if (eq(field, "answers")) ret "Text of answer. Use random { ... } for multiple answers (separated from each other by an empty line)"; null; } Renderer getRenderer(S field) { if (eqOneOf(field, 'questions, 'answers)) ret new TextArea(80, 10); if (eq(field, 'patterns)) ret new TextField(80); if (eq(field, 'category)) ret new ComboBox(collect name(conceptsSortedByField(Category, 'index))); ret super.getRenderer(field); } // sort for display L defaultSort(L l) { ret sortedByCalculatedField(l, q -> pair(lower(q.category), q.index)); } }; data.onCreateOrUpdate.add(qa -> reindexQAs()); HCRUD crud = new(rawLink(), data) { S frame(S title, S contents) { title = ahref(or2(shortLink, rawLink("")), adminTitle) + " | " + title; ret hhtml(hhead_title_decode(title) + hbody(h2(title) + p(joinWithVBar( targetBlank(rawLink("download"), "export brain"), )) + contents)); } S renderTable(bool withCmds) { Scorer scorer = consistencyCheckResult!; ret (empty(scorer.errors) ? p("Pattern analysis: No problems found. " + nEntries(countConcepts(QA)) + ".") : joinMap(scorer.errors, lambda1 renderErrorAsParagraph)) + super.renderTable(withCmds); } S renderErrorAsParagraph(ConsistencyError error) { S fix = error.renderFix(); ret p("Error: " + htmlEncode2(error.msg) + " " + joinMap(error.items, qa -> ahref(editLink(qa.id), "[item]")) + (empty(fix) ? "" : " " + fix)); } S renderValue(S field, O value) { S html = super.renderValue(field, value); if (eq(field, "questions")) html = b(html); ret html; } }; // actions if (eqGet(params, action := 'reindex)) { long id = parseLong(params.get('id)); int newIndex = parseInt(params.get('newIndex)); QA qa = getConcept(QA, id); if (qa == null) ret crud.refreshWithMsgs("Item not found"); cset(qa, index := newIndex); reindexQAs(); ret crud.refreshWithMsgs("Index of item " + qa.id + " changed"); } crud.tableClass = "responstable"; ret hsansserif() + hcss_responstable() + crud.renderPage(params); } catch print e { ret "ERROR."; } } // main API function for other bots sS answer(S s, O... _) { optPar Server server; QA qa = findMatchingQA(s, qasForServer(server)); if (qa == null) null; S raw = qa.answers; S contents = extractKeywordPlusBracketed_keepComments("random", raw); if (contents != null) ret random(splitAtEmptyLines(contents)); ret raw; } static L qasForServer(Server server) { Set categories = asSet(categoriesForServer(server)); ret sortQAs(filter(list(QA), qa -> contains(categories, qa.category))); } static QA findQAWithTypos(S s, Cl qas) { Lowest qa = findClosestQA(s, qas); if (!qa.has()) null; if (qa.score() > maxTypos) { print("Rejecting " + nTypos((int) qa.score()) + ": " + s + " / " + qa->patterns); null; } print("Accepting " + nTypos((int) qa.score()) + ": " + s + " / " + qa->patterns); ret qa!; } static QA findMatchingQA(S s, Cl qas, bool allowTypos) { try object QA qa = findMatchingQA(s, qas); if (allowTypos) try object QA qa = findQAWithTypos(s, qas); null; } static Lowest findClosestQA(S s, Cl qas) { time "findClosestQA" { new Lowest qa; findClosestQA(s, qa, qas); } ret qa; } static L sortQAs(Cl qas) { Map categoryToIndex = fieldToFieldIndex('name, 'index, list(Category)); ret sortedByCalculatedField(qas, q -> pair(categoryToIndex.get(q.category), q.index)); } svoid reindexQAs() { int index = 1; for (QA qa : sortQAs(list(QA))) cset(qa, index := index++); } static QA findMatchingQA(S s, Cl qas) { for (QA qa : sortQAs(qas)) if (mmo_match_parsedPattern(qa.parsedPattern(), s)) ret qa; null; } svoid findClosestQA(S s, Lowest best, Cl qas) { for (QA qa : sortQAs(qas)) { Int score = mmo_levenWithSwapsScore_parsedPattern(qa.parsedPattern(), s); if (score != null) best.put(qa, score); } } sclass ConsistencyError { S msg; L items; *(S *msg, QA... items) { this.items = asList(items); } S renderFix() { null; } } svoid checkLocalConsistency(QA qa, Scorer scorer) { LS questions = tlft(qa.questions); for (S q : questions) if (mmo_match_parsedPattern(qa.parsedPattern(), q)) scorer.ok(); else scorer.error(ConsistencyError("Question " + quote(q) + " not matched by patterns " + quote(qa.patterns), qa)); } svoid checkGlobalConsistency(QA qa, Cl qas, Scorer scorer) { for (S q : tlft(qa.questions)) { QA found = findMatchingQA(q, qas); if (found != null && found != qa) scorer.error(new ConsistencyError("Question " + quote(q) + " (item " + qa.id + ") shadowed by patterns " + quote(found.patterns) + " (item " + found.id + ")", qa, found) { S renderFix() { ret ahref(rawLink("?action=reindex&id=" + qa.id + "&newIndex=" + (found.index-1)), "[fix by changing index]"); } }); else scorer.ok(); } } static Scorer consistencyCheck() { new Scorer scorer; scorer.collectErrors(); for (QA qa) checkLocalConsistency(qa, scorer); for (Server server) { L qas = qasForServer(server); for (QA qa : qas) checkGlobalConsistency(qa, qas, scorer); } ret scorer; } // server can be null static Cl categoriesForServer(Server server) { Set set = asSet(conceptsWhere Category(onByDefault := true)); if (server != null) for (ServerToCategory link : conceptsWhere(ServerToCategory, +server)) addOrRemove(set, link.category, link.enabled); ret collectAsSet name(set); } // API svoid importQA(virtual QA qa_external) { QA qa = shallowCloneToUnlistedConcept QA(qa_external); uniq QA(allConceptFieldsAsParams(qa)); } sS rawLink() { ret "/"; } sS rawLink(S uri) { ret addSlashPrefix(uri) ; }