!7 module NLRulesBot extends DynTalkBot2<.ByServer> { void init { super.init(); makeByServer = () -> new ByServer; useAGIBlueForDropPunctuation = false; preprocessAtSelfToMyName = false; dropPunctuation = false; print("Total rules in all guilds: " + totalRuleCount()); } sclass Rule { GlobalID globalID = aGlobalIDObj(); S text, comment; *() {} *(S *text) {} } S renderRule(Rule r) { ret r == null ? null : appendNewLineIfNempty(r.comment) + "Rule " + r.globalID + ": " + r.text; } class ByServer extends DynTalkBot2.ByServer { new L rules; new Set priorityChannels; // where we always respond bool enableMagicQuotes = true; new Map topicByChannel; new SimpleFactStore factStore; new StringClustersWithIDs synonyms; delegate Cluster to StringClustersWithIDs. void _doneLoading { synonyms.onChange(r change); } S processSimplifiedLine(S s, O... _) { try answer super.processSimplifiedLine(s, _); ret fullTrim(processSimplifiedLine2(s, _)); } S processSimplifiedLine2(S s, O... _) { lock myLock(); try answer factStore_cmds(factStore, s, authed(_)); try answer philosophyBotWithFactStore_discordAnswer( ai_weightChangeBot_theory(), factStore, s, _); long channelID = longPar channelID(_); new Matches m; S s2 = dropMyPrefixOrNull(s); if (s2 != null) s = s2; else if (!contains(priorityChannels, channelID)) null; optPar Message msg; Message.Attachment attachment = msg == null ? null : first(msg.getAttachments()); if (attachment != null) { File file = prepareCacheProgramFile(guildID + "/" + attachment.getFileName()); print("Downloading attachment: " + file); deleteFile(file); if (!attachment.download(file)) ret "Couldn't download attachment"; LS lines = tlft(loadTextFile(file)); print("First line: " + first(lines)); if (!swic(first(lines), "rule ")) print("File does not contain rules"); else { new L toImport; // (globalID, text, comment) new LS comment; for (S line : lines) { LS l = regexpICFullMatch_groups("Rule ([a-z]+):(.*)$", line); if (nempty(l)) { toImport.add(t3(first(l), second(l), lines_rtrim(comment)); comment.clear(); } else comment.add(line); } S found = "Found " + nRules(toImport) + " in attachment"; int oldCount = l(rules); Map existing = indexByField globalID(rules); int updated = 0, added = 0; for (T3S rule : toImport) { GlobalID id = GlobalID(rule.a); S text = trim(rule.b); Rule r = existing.get(id); if (r == null) { r = new Rule(text); r.globalID = id; r.comment = rule.c; rules.add(r); existing.put(id, r); ++added; } else if (neq(r.text, text)) { r.comment = rule.c; r.text = text; ++updated; } } if (added+updated > 0) change(); ret found + ". Had: " + oldCount + ". Added: " + added + ". Updated: " + updated + ". New count: " + l(rules); } } // duplicated patterns go here if "on * say *|on * or * say *|on keyword * and * say *|on keyword * and keyword * say *|on keyword * or * say *|on keyword * say *|on *, image-google X|on *, google X|when <*> comes online, say *|on bot keyword * and keyword *, say *|on conversation keyword * and *, say *|on topic * and *, say *|on topic * and keyword *, say *" { Rule r = firstWhereIC(rules, text := s); if (r != null) ret "Rule exists: " + r.globalID; rules.add(r = new Rule(s)); change(); ret "Rule added with ID " + r.globalID; } if "rules|rules as file" { if (empty(rules)) ret "No rules defined yet"; uploadFileInChannel(channelID, toUtf8(lines(map(r -> renderRule(r), rules))), genericTextFileName(), null, null); null; } if "delete rule *|delete rule with id *" { try answer checkAuth(_); Rule r = firstWhere(rules, globalID := GlobalID($1)); if (r == null) ret "Rule " + $1 + " not found"; rules.remove(r); change(); ret "Rule deleted, " + nRule(rules) + " remain"; } if "delete all rules" { appendToTextFile(programFile("backups.txt"), guildStructure()); try answer checkAuth(_); rules.clear(); change(); ret "All rules deleted"; } if "priority channel on" { try answer checkAuth(_); priorityChannels.add(channelID); change(); ret "OK"; } if "priority channel off" { try answer checkAuth(_); priorityChannels.remove(channelID); change(); ret "OK"; } if "disable magic quotes" { try answer checkAuth(_); enableMagicQuotes = false; change(); ret "OK"; } if "add synonym ...|add synonyms ..." { LS tok = unquoteAll(wordTokC(m.rest())); printStruct(+tok); if (l(tok) <= 1) ret "Give me at least 2 synonyms, dude"; for (int i = 1; i < l(tok); i++) synonyms.addPair(first(tok), tok.get(i)); ret "OK: " + synonyms.clusterWith(first(tok)).synonyms; } if "delete synonym cluster *" { Cluster c = synonyms.clusterWith($1); if (c == null) ret "Cluster not found"; synonyms.remove(c); ret "Synonym cluster deleted: " + c.synonyms; } if "delete synonym * from cluster *" { Cluster c = synonyms.clusterWith($1); if (c == null) ret "Cluster not found"; if (!c.synonyms.remove($1)) ret format("Synonym * not found in " + c.synonyms); change(); if (l(c.synonyms) <= 1) synonyms.remove(c); ret "OK"; } if "list synonyms" ret or2(lines(map(synonyms.clusters, c -> join(" = ", c.synonyms))), "No synonyms defined"); if "list synonyms for *" { Cl l = synonyms.searchForCluster($1); ret or2(lines(map(l, c -> join(" = ", c.synonyms))), format("No synonyms found for *", $1)); } if "print log" { try answer checkAuth(_); ret str(_actualPrintLog()); } if "lottery print log" { try answer checkAuth(_); S mod = dm_findModule("#1025913/LotteryBot"); if (mod == null) ret "Not loaded"; ret mod + "\n" + str(dm_call(mod, '_actualPrintLog)); } if "lottery enabled" { try answer checkAuth(_); ret str(dm_get(dm_findModule("#1025913/LotteryBot"), 'enabled)); } if "lottery last error" { try answer checkAuth(_); ret str(dm_get(dm_findModule("#1025913/LotteryBot"), '_error)); } if "lottery reload" { try answer checkAuth(_); dm_reloadModule(dm_findModule("#1025913/LotteryBot")); ret "OK (maybe)"; } if (eqic(s, "help")) ret ltrim([[ I execute simple English rules. Stuff to say. @me `on "hello bot" say "who are you"` @me `on keyword "time" say "it is always time for a tea"` @me `on "what is X" google X` @me `on "show me X" image-google X` @me `on bot keyword "game" and keyword "sure" say "let's play tic tac toe"` @me `on topic "weather" and "nice", say "so the sun is shining?"` @me `when @user comes online, say "hello friend"` @me `rules` / `rules as file` - list rules @me `delete rule abcdefghijkl` - delete rule with ID @me `add synonyms thx "thank you" thanks` - add synonyms @me `delete synonym cluster thx` - delete all synonyms of thx @me `delete synonym thx from cluster thanks` - delete a single synonym @me `list synonyms` / `list synonyms for "thank you"` @me `topic` - show current topic for channel (see: conversation keyword) @me `forget the topic` / `forget ` - forget current topic @me `priority channel on` - respond to every msg @me `priority channel off` - respond to msgs with my name only @me `masters` / `add master ` / `delete master ` @me `help` / `support` / `source code` @me `download` - download my brain for this guild To fill me with rules, just upload the file you got from `rules as file` to the channel. [Bot made by https://BotCompany.de] ]]).replace("@me", atSelf()); // find rule to execute new LinkedHashSet outs; new LinkedHashSet outs2; // higher priority (two matches) for (Rule r : rules) pcall { // IMPORTANT: patterns are duplicated above if (matchX2("on conversation keyword * and *, say *|on topic * and *, say *|on topic * and keyword *, say *", r.text, m)) { S in1 = $1, in2 = $2, out = $3; if (findWithSynonyms(in1, s)) if (put_trueIfChanged(topicByChannel, channelID, in1)) change(); if (synonyms.eqicOrInSameCluster(topicByChannel.get(channelID), in1) && findWithSynonyms(in2, s)) outs2.add(rewrite(out, _)); } if (match("on * say *", r.text, m)) { S in = $1, out = $2; if (matchWithSynonyms(in, s)) outs.add(rewrite(out, _)); } if (match("on * or * say *", r.text, m)) { S in1 = $1, in2 = $2, out = $3; if (matchWithSynonyms(in1, s) || matchWithSynonyms(in2, s)) outs.add(rewrite(out, _)); } if (match_vbar("on keyword * and * say *|on keyword * and keyword * say *", r.text, m)) { S in1 = $1, in2 = $2, out = $3; if (findWithSynonyms(in1, s) && findWithSynonyms(in2, s)) outs2.add(rewrite(out, _)); } if (match("on keyword * or * say *", r.text, m)) { S in1 = $1, in2 = $2, out = $3; if (findWithSynonyms(in1, s) || findWithSynonyms(in2, s)) outs2.add(rewrite(out, _)); } if (match("on bot keyword * and keyword *, say *", r.text, m)) { ISaid lastSaid = lastSaidInChannel.get(channelID); if (lastSaid != null) { S in1 = $1, in2 = $2, out = $3; if (findWithSynonyms(in1, lastSaid.text) && findWithSynonyms(in2, s)) outs2.add(rewrite(out, _)); } } if (match("on keyword * say *", r.text, m)) { S in = $1, out = $2; if (findWithSynonyms(in, s)) outs.add(rewrite(out, _)); } if (match("on *, image-google X", r.text, m)) if (flexMatchIC(replaceWord($1, "X", "*"), s, m)) { bool nsfw = discord_isNSFWChannel_gen(optPar channel(_)); print(+nsfw); BufferedImage img = nsfw ? quickVisualize_nsfw($1) : quickVisualize($1); if (img == null) ret "No image found, sorry!"; postImage(paramsToMap(_), uploadToImageServer(img, $1)); // null; } if (match("on *, google X", r.text, m)) if (flexMatchIC(replaceWord($1, "X", "*"), s, m)) { bool nsfw = discord_isNSFWChannel_gen(optPar channel(_)); print(+nsfw); ret discord_google($1, results := 1, safeSearch := !nsfw); } } if "topic|conversation keyword" { S topic = topicByChannel.get(channelID); ret nempty(topic) ? "Current topic in this channel is: " + topic : "No topic set in this channel"; } if "forget topic|forget the topic" ret forgetTopic(channelID); if "forget ..." if (synonyms.eqicOrInSameCluster($1, topicByChannel.get(channelID))) ret forgetTopic(channelID); if "download" { optPar Guild guild; optPar MessageChannel channel; ret serveGuildBackup(channel, guild, structure_nullingInstancesOfClass(_getClass(module()), ByServer.this)); } if "stats" ret "I have " + nRules(rules) + " and know " + n2(synonyms.totalCount(), "synonym"); if "masters" ret renderMasters(); try answer random(outs2); try answer random(outs); if (enableMagicQuotes) try answer answer_magicQuoteInput(s, useTranspiled := true); null; } S rewrite(S answer, O... _) { if (containsIC(answer, "")) { optPar S name; if (empty(name)) name = discord_optParUserName(_); if (nempty(name)) answer = replaceIC(answer, "", name); } ret answer; } void onOnlineStatusChange(UserUpdateOnlineStatusEvent e) { User user = e.getMember().getUser(); print("User status change: " + user + " " + e.getOldValue() + " -> " + e.getNewValue()); // only react to user coming online if (e.getOldValue() != OnlineStatus.OFFLINE) ret; long id = user.getIdLong(); new LinkedHashSet outs; new Matches m; for (Rule r : rules) pcall { // IMPORTANT: patterns are duplicated above if (match("when <*> comes online, say *", r.text, m)) { long _id = parseLongOpt($1); S out = $2; if (id == _id) outs.add(rewrite(out, name := user.getName())); } } postInChannel(preferredChannelID, random(outs)); } bool findWithSynonyms(S pat, S s) { for (S pat2 : splitAtComma(pat)) if (!findWithSynonyms2(pat2, s)) false; true; } bool findWithSynonyms2(S pat2, S s) { for (S pat3 : synonyms.extend(pat2)) if (find3(pat3, s)) true; false; } bool matchWithSynonyms(S pat, S s) { for (S pat2 : synonyms.extend(pat)) if (match(pat2, s)) true; false; } S forgetTopic(long channelID) { topicByChannel.remove(channelID); ret "OK, let's talk about something else"; } } long totalRuleCount() { ret intSumAsLong(map(syncCloneValues(dataByServer), bs -> l(bs.rules))); } S renderMasters() { ret empty(authorizedUsers) ? "I have no masters." : "My masters are: " + ai_renderAndList(map discordAt(authorizedUsers)); } LS splitIntoLines(S s) { ret tlft_honoringTokens(s); } }