Not logged in.  Login/Logout/Register | List snippets | | Create snippet | Upload image | Upload data

602
LINES

< > BotCompany Repo | #1022488 // Discord Gazelle [v13, LIVE]

JavaX source code (Dynamic Module) [tags: use-pretranspiled] - run with: Stefan's OS

Uses 11217K of libraries. Click here for Pure Java version (30435L/185K).

!7

set flag JDA40.
set flag DynModule.

import java.util.function.Consumer;
import net.dv8tion.jda.api.requests.restaction.*;

// Note: Bot is always started & logs lines,
// but doesn't respond when enabled is false.

cm DiscordBot > DynPrintLogAndEnabled {
  transient JDA bot;
  transient new Set<Long> msgsReactedTo;
  transient double deleteDelay = 60.0;
  transient bool doDelete;
  transient int maxMonologue = 5;
  transient bool selfTalk = false;
  transient Color imageEmbedMysteriousLineColor = colorFromHex("36393f");

  transient new InheritableThreadLocal<MessageChannel> currentChannel;
  transient new InheritableThreadLocal<Message> respondingTo;
  
  transient new L<Long> lastPosted;
  transient GazelleContextCache_v2 contextCache;
  
  transient Set<S> acceptablePurposes = litciset("");
  
  switchable S gazoogleDefaultCountry = "us";
  
  switchable S powerWordURL = "https://gazelle.rocks/beaHTML/274191";
  
  transient LS voice9speakers = tok_splitAtComma("p225, p226, p227, p228, p229, p230, p231, p232, p233, p234, p236, p237, p238, p239, p240, p241, p243, p244, p245, p246, p247, p248, p249, p250, p251, p252, p253, p254, p255, p256, p257, p258, p259, p260, p261, p262, p263, p264, p265, p266, p267, p268, p269, p270, p271, p272, p273, p274, p275, p276, p277, p278, p279, p280, p281, p282, p283, p284, p285, p286, p287, p288, p292, p293, p294, p295, p297, p298, p299, p300, p301, p302, p303, p304, p305, p306, p307, p308, p310, p311, p312, p313, p314, p316, p317, p318, p323, p326, p329, p330, p333, p334, p335, p336, p339, p340, p341, p343, p345, p347, p351, p360, p361, p362, p363, p364, p374, p376");
  
  transient Consumer<Throwable> onQueueError = error -> {
    temp enter();
    printStackTrace(error);
  };
  
  // map user command message id to bot message id
  // (for editing)
  Map<Long> cmdToResponse = syncMap();

  start-thread {
    vm_cleanPrints();
    logModuleOutput();
    dm_useLocalMechListCopies();
    
    print("Making cache");
    time "Made cache" {
      contextCache = new GazelleContextCache_v2(me());
      ownResource(contextCache.listenToMessages());
      contextCache.fill();
    }
    
    // TODO: log JDA output
    bot = discordBot40(new ListenerAdapter {
      public void onMessageUpdate(MessageUpdateEvent e) pcall {
        temp enter();
        ret if !enabled || !licensed();
        
        long msgID = e.getMessage().getIdLong();
        S content = e.getMessage().getContentRaw();
        print(content := quote(content));
        O lineConcept = dm_discord_lineForMsgID_unimported(msgID);
        S rendered = msgID + ": " + content;
        if (lineConcept == null)
          ret with print("Weird: Message author, but not found: " + rendered);
        call(lineConcept, '_setField, editedText := content);
        print("Message edited: " + rendered);
        
        handleMessage(e, e.getAuthor(), e.getMember(), e.getMessage(), false);
      }
      
      public void onMessageReceived(MessageReceivedEvent e) pcall {
        temp enter();
        ret if !licensed();
        
        handleMessage(e, e.getAuthor(), e.getMember(), e.getMessage(), true);
      }
      
      void handleMessage(GenericMessageEvent e, User author, Member member, Message message, bool newMsg) {
        bool bot = author.isBot();
        long msgID = message.getIdLong();
        long userID = author.getIdLong();
        S userName = member == null ? null : member.getNickname(); // the changeable nick name - null sometimes?
        if (userName == null && member != null) userName = member.getEffectiveName();
        if (userName == null) userName = author.getName();
        
        final Message msg = message;
        
        print("Channel type: " + e.getChannelType());
        bool isPrivate = e.getChannelType() == ChannelType.PRIVATE;
        S content = trim(backtickUnquote(trim(msg.getContentRaw())));
        print("Msg from " + userName + ": " + content);
        if (empty(content)) ret;
        fS originalContent = content;

        O user = userConcept(author);
        
        if (newMsg) {
          S crud = dm_gazelle_linesCRUD();
          O channel = dm_call(crud, 'uniqChannel, msg.getChannel().getIdLong());
          dm_call(crud, 'cset, channel, litobjectarray(name := msg.getChannel().getName()));
          
          O lineConcept = dm_call(crud, 'uniqConcept, litObjectArrayAsObject(+msgID));
          dm_call(crud, 'cset, lineConcept, litobjectarray(
            text := content,
            +bot, +isPrivate,
            author := user, +channel));
          vmBus_send('newDiscordLine, lineConcept);
        }
        
        ret if !enabled;
        
        final MessageChannel ch = e.getChannel();
        long channelID = ch.getIdLong();
        L<GazelleLine> linesInChannel = dm_discord_linesInChannel(channelID);
        
        // monologue prevention
        
        if (bot) {
          int monologueLength = cast dm_call(dm_gazelle_linesCRUD(), 'monologueLength, channelID);
          if (monologueLength >= maxMonologue) ret;
          //Long last = get(lastPosted, l(lastPosted)-3);
          //if (last != null && now() < last+10000) ret;
        }
        
        temp tempSetTL(currentChannel, ch);
        temp tempSetTL(respondingTo, msg);
        
        // module meta ("gazelle stop" etc.)
        
        S userForModulesManager = "discord user " + userID;
        
        if (!bot) {
          for (T3S t : dm_gazelle_allRulesWithPurpose("moduleMeta")) {
            PairS p = splitAtDoubleArrow_pair(t.a);
            if (eqic(p.b, "stop module") && nempty(p.a) && matchX2(p.a, content)) {
              print("Stopping modules");
              postText((S) dm_call(dm_gazelle_modulesManager(), 'deleteAllModulesForUser, userForModulesManager, silentIfEmpty := true));
            }
          }
        }

        new Matches m;
        
        try {
          content = fixNewLines(content);
          content = replacePrefix("!eval\n", "!eval ", content);
          
          content = regexpReplace_direct(content, "^=" + regexpLookahead("[\\.\\w0-9\\s\\]\\{]"), "!eval ");
          
          // g me
          if (eqic(content, "g me"))
            ret with postInChannel(ch, newMsg, msgID, author.getId());
          
          // G command - power word search
          
          if (regexpFindIC("^g\\b", content)) {
            S answer = loadPageWithParams(powerWordURL, text := content, user := userName);
            if (nempty(answer))
              ret with postInChannel(ch, newMsg, msgID, answer);
          }
          
          // GAZOOGLE command - TODO: find out when it's just a sentence starting with "gazoogle"
          
          S googleQuery = regexpFirstGroupIC("^gazoogle\\b[,:.!]*\\s*(.*)", content);
          if (nempty(googleQuery)) {
            /*if (newMsg)*/ sendTyping();
            MapSO urlParams = litmap(gl := gazoogleDefaultCountry);
            ret with postInChannel(ch, newMsg, msgID, or2_rev("Gazoogle doesn't know :(", lines(map(dm_webSearch_all(googleQuery, urlParams),
              result -> result! + " <" + result.url() + ">"))));
          }
          
          // GAZTUBE/GAZELLETUBE command (YouTube search)
          
          S ytQuery = regexpFirstGroupIC("^gaz\\w*tube\\b[,:.!]*\\s*(.*)", content);
          if (nempty(ytQuery)) {
            /*if (newMsg)*/ sendTyping();
            ret with postInChannel(ch, newMsg, msgID, 
              or2_rev("GazelleTube is showing a black screen :(",
                mapSingle(dm_youTubeSearch(ytQuery),
                  result -> result.url())));
          }
          
          // GPIC/GAZPIC command (Pixabay search)
          
          S picQuery = regexpFirstGroupIC("(?:gpic|gazpic)\\b[,:.!]*\\s*(.*)", content);
          if (nempty(picQuery)) {
            sendTyping();
            S imgURL = cast mapGet(first(pixabaySearch(picQuery)), "webformatURL");
            ret with postInChannel(ch, newMsg, msgID, 
              or2(imgURL, "No image found on Pixabay"));
          }

          // gsay / gazsay / gazellesay / !say
          S toSay = regexpFirstGroupIC("(?:!say|gsay|gazsay|gazellesay)\\b[,:.!]*\\s*(.*)", content);
          if (nempty(toSay)) {
            sendTyping();
            S fileName = "speech.mp3";
            
            toSay = shorten(trim(toSay), 500);
            if (eqic(toSay, "voices"))
              ret with postInChannel(ch, newMsg, msgID, "Voice list: https://mouth.gazelle.rocks/beaHTML/22");
              
            LS groups = regexpFirstGroupsIC("voice\\s*(\\d+)\\b[,:.!]*\\s*(.*)", toSay);
            S voiceNr = null, speaker = null;
            if (groups != null) {
              voiceNr = first(groups);
              toSay = second(groups);
              
              groups = regexpFirstGroupsIC("speaker\\s*(\\S+)\\b[,:.!]*\\s*(.*)", toSay);
              if (groups != null) {
                speaker = first(groups);
                if (eqic(speaker, "random")) {
                  speaker = random(voice9speakers);
                  fileName = "voice-9-" + speaker + ".mp3";
                }
                toSay = second(groups);
              }
            }
            
            File f = programFile(fileName);
            postBinaryPageToFile(f, "https://mouth.gazelle.rocks/beaHTML/16",
              page := 0, +voiceNr, +speaker, text := toSay);
            ret with uploadFileInChannel(f);
          }
          
          S evalHelp = regexpFirstGroupIC("^\\!eval[\\s:\\-]*help\\s+(.*)", content);
          if (evalHelp != null)
            ret with postInChannel(ch, newMsg, msgID,
              htmlDemo(javaxSourceToHTML(evalHelp)));

          if (swicOneOf(content, "!eval ", "!fresh ", "!real-eval ", "!safe-eval ") || eqic(content, "!fresh")) {
            O safetyCheck = null;
            bool authed = dm_discord_userCanEval(userID);
            if (swic_trim(content, "!safe-eval ", m)) {
              content = "!eval " + m.rest();
              authed = false;
            }
            
            if (regexpMatches("![a-z\\-]+\\s+again", content)) {
              GazelleLine last = dm_discord_nextToLastEvalByUser(userID);
              if (last == null) ret with postInChannel(ch, newMsg, msgID, "No last eval found");
              content = replaceSuffix("again", afterSpace(last.text), content);
            }
            
            if (!authed)
              safetyCheck = botEval_strictSafetyCheck();
            
            /*if (newMsg)*/ sendTyping();
            
            var post = voidfunc(S s) {
              if (s != null)
                postInChannel(ch, newMsg, msgID, shorten(ifEmpty(s, [[""]]), 1000))
            };
            
            if (startsWith(content, "!eval ", m))
              ret with dm_bot_execFreshRealEval(post, m.rest(), +safetyCheck);
            else
              ret with dm_bot_execEvalCmd(post, content, +safetyCheck, alwaysFresh := true);
          }
          
          if (eqic(content, "!rule")) {
            LS lines = linesInChannelBy(channelID, userID);
            if (contains(nextToLast(lines), "=>"))
              content = "!rule " + nextToLast(lines);
            else {
              if (l(lines) < 3) fail("Too few lines");
              content = "!rule " + nextToNextToLast(lines) + " => " + nextToLast(lines);
            }
          }
          
          if (swicOneOf(content, m, "!rule ", "!rule\n")) {
            S s = trim(m.rest());
            print("Processing rule: " + s);
            S opt = leadingSquareBracketStuff(s);
            s = dropActuallyLeadingSquareBracketStuff(s);
            if (startsWith(s, "=>"))
              s = assertNotNull("No last line in channel", nextToLast(linesInChannelBy(channelID, userID))) + "\n" + s;
            LS comments = ll("made by user", "discord", "tokenize out with javaTok");
            if (cic(pairB(tok_splitAtDoubleArrow_pair(s)), userName)) {
              s = optCurly(userName) + " says: " + s;
              comments.add("with user name");
            }
            
            gazelle_parsePublicRuleOptions(opt, comments);
            s = replace(s, " + ", "\n+ ");
            s = jreplace1(s, "=>", "\n=>");
            s = gazelle_processSquareBracketAnnotations(s, comments);
            temp tempSetTL(dm_gazelle_addRuleWithComment_renderWithoutComments, true);
            dm_gazelle_addRuleWithComment(s, lines_rtrim(comments));
            ret with postText(dm_gazelle_addRuleWithComment_msg!);
          }
          
          if (swic_trim(content, "!phrase ", m)) {
            gazelle_createRuleForPhrase(m.rest());
            ret with postText(dm_gazelle_addRuleWithComment_msg!);
          }
          
          // Go into normal reasoning
          
          if (bot && !selfTalk) ret;
          
          S purpose = null;
          bool debug = false, skipBad = true;
          Set<S> restrictToRules = null;
            
          bool change;
          do {
            change = false;
            if (swic_trim(content, "!withBad ", m)) {
              skipBad = false;
              content = m.rest(); set change;
            }
            
            if (swic_trim(content, "!preprocess ", m)) {
              purpose = 'preprocess;
              content = m.rest(); set change;
            }
          
            if (swic_trim(content, "!debug ", m)) {
              set debug;
              content = m.rest(); set change;
            }
            
            if (swic(content, "!using[", m)) {
              int i = indexOf(m.rest(), ']');
              restrictToRules = litset(substring(m.rest(), 0, i));
              content = trimSubstring(m.rest(), i+1);
            }
          } while (change);
          
          print("debug=" + debug + ", content=" + content);
          
          LS preContext = takeLast(2, dropLast(collect text(linesInChannel)));
          print("preContext=" + preContext);
          
          GazelleContextCache_v2 contextCache = DiscordBot.this.contextCache;
          if (restrictToRules != null) {
            contextCache = gazelle_cloneContextCache(contextCache);
            Set<S> _restrictToRules = restrictToRules;
            print("restrictToRules=" + restrictToRules);
            contextCache.cachedCtx.engine.dropRulesWhere(r -> !contains(_restrictToRules, r.globalID));
            print("Using rules: " + collect globalID(contextCache.cachedCtx.engine.rules));
          }
          
          O[] params = litmapparams(+userName, +skipBad, +preContext,
            badComments := mechCISet("Knock-out rule comments"),
            acceptablePurposes := nempty(purpose)
              ? litciset(purpose)
              : acceptablePurposes,
            respondingToHuman := !bot,
            +debug,
            +userID,
            contextMaker := contextCache,
            debugPreprocessing := true,
            sendToModules := func(S input) -> L<GazelleTree> {
              gazelle_wrapLinesAsTree(gazelle_answersFromModules(userForModulesManager, input))
      });
            
          if (eq(purpose, 'preprocess))
            ret with postText(lines_rtrim(takeFirst(10, gazelle_preprocess(content, params))));

          L<GazelleTree> l;
          if (nempty(purpose))
            l = gazelle_postprocess(dm_gazelle_reasonAboutChatInput_v2(userName, content, params));
          else
            l = gazelle_reason_repeat(content, params);

          int idx = 0;
          for (final GazelleTree t : l) {
            final int _idx = idx++;
            
            if (eqic(t.lineType, "temporary fact")) {
              if (dm_gazelle_hasTempFact(t.line)) continue;
              print("Saving temp fact: " + t.line); 
              dm_gazelle_addTempFact(t.line, "discord msg " + msgID);
            }
            
            if (eqic(t.lineType, "eval")) {
              S code = t.rule().out;
              if (!mechSet("Gazelle | Allowed Evals").contains(code))
                print("Eval not allowed: " + code);
              else {
                /*if (newMsg)*/ sendTyping();
                S out = strOrNull(dm_javaEvalOrInterpret(code));
                if (nempty(out))
                  postInChannelWithDelete(ch, out, voidfunc(Message msg2) {
                    Gazelle_ReasoningForLine reasoning = nu(Gazelle_ReasoningForLine,
                      outMsgID := msg2.getIdLong(),
                      outText := out,
                      inMsgID := msg.getIdLong(),
                      inUserID := userID,
                      inChannelID := channelID,
                      inText := originalContent,
                      tree := t,
                      treeIndex := _idx);
                    dm_gazelle_saveReasoningForLine(reasoning);
                  });
              }
              continue;
            }
            
            // Now that we actually post, store recently used mapping
            gazelle_storeRecentlyUsedMappings(ll(t),
              context := "discord channel " + channelID);
              
            fS out = tok_dropCurlyBrackets(t.line);
            print(">> " + t);
            print("POSTING: " + out);
            
            postInChannelWithDelete(ch, out, voidfunc(Message msg2) {
              Gazelle_ReasoningForLine reasoning = nu(Gazelle_ReasoningForLine,
                outMsgID := msg2.getIdLong(),
                outText := out,
                inMsgID := msg.getIdLong(),
                inUserID := userID,
                inChannelID := channelID,
                inText := originalContent,
                tree := t,
                treeIndex := _idx);
              dm_gazelle_saveReasoningForLine(reasoning);
            });
          }
        } catch print error {
          postInChannel(ch, newMsg, msgID, exceptionToStringShort(error));
        }
      }
      
      public void onMessageReactionAdd(MessageReactionAddEvent e) pcall {
        temp enter();
        ret if !enabled || !licensed();
        MessageReaction r = e.getReaction();
        bool bot = e.getUser().isBot();
        long msgID = r.getMessageIdLong();
        add(msgsReactedTo, msgID);
        print("User " + e.getUser() + (bot ? " (bot)" : "") + " reacted to message " + msgID + " with " + r.getReactionEmote());
        if (bot) ret;

        S crud = dm_gazelle_linesCRUD();
        O lineConcept = dm_call(crud, 'uniqConcept, litObjectArrayAsObject(+msgID));
        L reactions = cast get(lineConcept, 'reactions);
        print("lineConcept=" + lineConcept);
        print("reactions=" + reactions);
        O reaction = dm_call(crud, 'nuReaction, litObjectArrayAsObject(
          user := userConcept(e.getUser()),
          emoji := r.getReactionEmote().getName(),
          created := now()));
        print("reaction=" + reaction);
          
        dm_call(crud, 'cset, lineConcept, litobjectarray(
          reactions := listPlus(reactions, reaction)));
          
        dm_discord_gatherFeedbackFromLine(dm_discord_importLine(lineConcept));
      }
    });
    
    dm_registerAs('discordBot);
    print("Started bot");
  }
  
  void cleanMeUp {
    if (bot != null) pcall {
      print("Shutting down bot");
      bot.shutdown();
      print("Bot shut down");
    }
    bot = null;
  }
  
  O userConcept(User user) {
    S crud = dm_gazelle_linesCRUD();
    O userConcept = dm_call(crud, 'uniqUser, user.getIdLong());
    dm_call(crud, 'cset, userConcept, litobjectarray(name := user.getName()));
    ret userConcept;
  }
  
  // API
  
  MessageChannel getChannel(long channelID) {
    ret bot.getTextChannelById(channelID);
  }
  
  /*void postInChannel(long channelID, S msg) {
    postInChannel(getChannel(channelID), msg);
  }*/
  
  void postInChannel(MessageChannel channel, S msg) {
    postInChannel(channel, true, 0, msg);
  }
  
  void postInChannel(MessageChannel channel, bool newMsg, long userMsgID, S msg) {
    if (!newMsg) {
      Long responseID = cmdToResponse.get(userMsgID);
      if (responseID != null) {
        editMessage(channel.getIdLong(), responseID, msg);
        ret;
      }
    }
    
    S postID = cast dm_call(gazelle_postedLinesCRUD(), 'postingLine, channel.getId(), msg);
    channel.sendMessage(msg).queue(m -> {
      dm_call(gazelle_postedLinesCRUD(), 'donePosting, postID, "discord msg " + m.getId());
      cmdToResponse.put(userMsgID, m.getIdLong());
      change();
    });
    if (l(lastPosted) > 5) popFirst(lastPosted);
    lastPosted.add(now());
  }
  
  /*void postInChannel(S channel, S msg) {
    long id = dm_discord_channelID(channel);
    if (id == 0) fail("Channel not found: " + channel);
    postInChannel(id, msg);
  }*/
  
  void postInChannelWithDelete(MessageChannel channel, S msg, VF1<Message> onPost) {
    S postID = cast dm_call(gazelle_postedLinesCRUD(), 'postingLine, channel.getId(), msg);
    channel.sendMessage(msg).queue(msg2 -> {
      dm_pcall(gazelle_postedLinesCRUD(), 'donePosting, postID, "discord msg " + msg2.getId());
      pcallF(onPost, msg2);
      final long msgId = msg2.getIdLong();
      print("I sent msg: " + msgId);
      if (doDelete) doLater(deleteDelay, r {
        ret if contains(msgsReactedTo, msgId);
        print("Deleting msg " + msgId);
        msg2.delete().queue();
      });
    }, error -> _handleException(error));
  }
  
  void postText(S text) {
    postInChannel(currentChannel!, text);
  }
  
  void postImage(S url) {
    postImage(currentChannel!, url);
  }
  
  void postImage(S url, S title) {
    postImage(currentChannel!, url, title);
  }
  
  void postImage(MessageChannel channel, S url) {
    postImage(channel, url, "");
  }
  
  void postImage(MessageChannel channel, S url, S description) {
    channel.sendMessage(
      new EmbedBuilder()
        .setImage(absoluteURL(url))
        //.setTitle(unnull(title))
        .setDescription(unnull(description))
        .setColor(imageEmbedMysteriousLineColor)
        .build()).queue(); // TODO: posted lines
  }
  
  LS linesInChannelBy(long channelID, long userID) {
    ret collect text(dm_discord_linesInChannelBy(channelID, userID));
  }
  
  MessageChannel currentChannel() { ret currentChannel!; }
  Message respondingTo() { ret respondingTo!; }
  
  void sendTyping() {
    currentChannel().sendTyping().queue();
  }
  
  // for testing, outdated
  L<GazelleTree> reasonAbout(S line, O... _) {
    ret gazelle_reasonAbout(line, paramsPlus(_, ctx := contextCache!));
  }
  
  void rebuildCache {
    contextCache.fill();
  }
  
  void editMessage(long channelID, long msgID, S text) {
    getChannel(channelID).editMessageById(str(msgID), text).queue();
  }
  
  S respondingToUserID() {
    ret respondingTo! == null ? null : "discord user " + respondingTo->getAuthor().getIdLong();
  }
  
  void uploadFileInChannel(File file) {
    uploadFileInChannel(file, "", null);
  }
  
  void uploadFileInChannel(File file, S msg, IVF1<Message> onPost) {
    if (!isFile(file)) fail("Not a file: " + file);
    
    var channel = currentChannel!;
    S fileName = fileName(file);
    MessageAction a = empty(msg)
      ? channel.sendFile(file, fileName)
      : channel.sendMessage(msg).addFile(file, fileName);
    a.queue(onPost == null ?: m -> onPost.get(m), onQueueError);
  }
}

Author comment

Began life as a copy of #1022193

download  show line numbers  debug dex  old transpilations   

Travelled to 7 computer(s): bhatertpkbcr, cfunsshuasjs, mqqgnosmbjvj, pyentgdyhuwx, pzhvpgtvlbxg, tvejysmllsmz, vouqrxazstgt

No comments. add comment

Snippet ID: #1022488
Snippet name: Discord Gazelle [v13, LIVE]
Eternal ID of this version: #1022488/86
Text MD5: a28f0c0a35fa58bcc4b2bec42eb22242
Transpilation MD5: 80ab60721d94bd0f686bba617bfdb592
Author: stefan
Category: javax / discord
Type: JavaX source code (Dynamic Module)
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2021-10-18 08:31:12
Source code size: 23860 bytes / 602 lines
Pitched / IR pitched: No / No
Views / Downloads: 554 / 26158
Version history: 85 change(s)
Referenced in: [show references]