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

394
LINES

< > BotCompany Repo | #1027629 // WebChatBot [LIVE]

JavaX fragment (include) [tags: use-pretranspiled]

Libraryless. Click here for Pure Java version (16847L/123K).

!include once #1027630 // Msg + Conversation

sclass WebChatBot {
  O thoughtBot;

  int longPollTick = 100;
  int longPollMaxWait = 1000*30; // lowered to 30 seconds
  
  S templateID = #1027638;
  S botName = "Chat Bot";
  S heading = "Chat Bot";
  S afterHeading;
  S botImageID = #1102802;
  S userImageID = #1102803;
  S chatHeaderImageID = #1102802;
  S cssID = #1026266;
  S baseLink;
  S jsOnMsgHTML; // operates on variable "src"
  bool botOnRight = true;
  S timeZone = germanTimeZone_string();
  S moreStuff; // JS code to execute after init is done
  S onBotShown;
  bool forceCookie;
  
  Set<S> specialButtons = litciset("Cancel", "Back", "Abbrechen", "Zurück");
  
  void start {
    db();
    Class envType = fieldType(thoughtBot, "env");
    if (envType != null)
      setOpt(thoughtBot, "env", proxy(envType, (O) mc()));
  }
  
  void sayAsync(S session, S text) {
    lock dbLock();
    Conversation conv = getConv(session);
    conv.add(new Msg(false, text));
    print("sayAsync " + session + ", new size=" + conv.allCount());
  }
  
  Request newRequest(S uri, SS params) { ret new Request(uri, params); }
  
  O html(S uri, SS params, O... _) {
    ret newRequest(uri, params).html(_);
  }
  
  class Request {
    S uri;
    SS params;
    S cookie;
    S clientIP;
    
    *(S *uri, SS *params) {}

    O html(O... _) {
      temp tempRegisterThread();
      uri = dropTrailingSlashIfNemptyAfterwards(uri);
      if (cookie == null) cookie = params.get("cookie");
      Conversation conv = nempty(cookie) ? getConv(cookie) : null;
      if (conv != null) {
        if (clientIP == null) clientIP = clientIP();
        cset(conv, ip := clientIP);
      }
      print("URI: " + uri + ", cookie: " + cookie + (conv == null ? "" : ", msgs: " + l(conv.msgs)));
      
      S pw = trim(params.get('pw));
      if (nempty(pw)) {
        S realPW = trim(loadSecretTextFile("password.txt"));
        if (empty(realPW)) ret errorMsg("Administrator has not set a password");
        if (neq(pw, realPW))
          ret errorMsg("Bad password, please try again");
        cset(conv, authed := true);
        if (nempty(params.get('redirect)))
          ret hrefresh(params.get('redirect));
      }
      
      if (eq(uri, "/stats")) {
        if (!conv.authed) ret serveAuthForm(rawLink(uri));
        ret "Threads: " + ul_htmlEncode(getThreadNames(registeredThreads()));
      }
          
      if (eq(uri, "/logs")) {
        if (!conv.authed) ret serveAuthForm(rawLink(uri));
        ret webChatBotLogsHTML2(rawLink(uri), params);
      }
    
      if (eq(uri, "/auth-only")) {
        if (eq(params.get('logout), "1"))
          cset(conv, authed := false);
        if (!conv.authed) ret serveAuthForm(params.get('uri));
        ret "";
      }
    
      if (conv != null) {
        lock dbLock();
        
        S message = trim(params.get("btn"));
        if (empty(message)) message = trim(params.get("message"));
        message = preprocess(message);
        
        if (match_vbar("new dialog|new dialogue", message)) {
          conv.oldDialogs.add(conv.msgs);
          cset(conv, msgs := new L);
          conv.change();
          callOpt(thoughtBot, "clearSession", conv.cookie);
          vmBus_send chatBot_clearedSession(mc(), conv);
          message = null;
        }
        
        callOpt(thoughtBot, "setSession", cookie, params);
        
        if (empty(conv.msgs))
          addReplyToConvo(conv, lambda0 initialMessage);
          
        if (nempty(message) && !lastUserMessageWas(conv, message)) {
          conv.add(new Msg(true, message));
          print("Added message: " + message + " to " + conv.id + ", l=" + l(conv.msgs));
        }
      
        if (nempty(conv.msgs) && last(conv.msgs).fromUser)
          addReplyToConvo(conv, () -> makeReply(last(conv.msgs).text));
      } // locked
      
      if (eq(uri, "/msg")) ret withHeader("OK");
      
      if (eq(uri, "/grabFullLog")) {
        if (conv == null) ret "No conversation for " + cookie;
        L<Msg> msgs = conv.allMsgs();
        ret jsonEncode(map(msgs, msg -> 
          litorderedmap(
            fromUser := msg.fromUser,
            text := msg.text,
            time := msg.time)));
      }
      
      if (eq(uri, "/incremental")) {
        vmBus_send chatBot_userPolling(mc(), conv);
        int a = parseInt(params.get("a"));
        
        print("a=" + a + ", as=" + conv.archiveSize());

        long start = sysNow();
        L msgs;
        bool first = true;
        while (licensed() && sysNow() < start+longPollMaxWait) {
          int as = conv.archiveSize();
          msgs = cloneSubList(conv.msgs, a-as);
          bool newDialog = a <= as;
          if (empty(msgs)) {
            if (first) {
              print("Long poll starting on " + cookie + ", " + a + "/" + a + ", id=" + conv.id);
              first = false;
            }
            sleep(longPollTick);
          } else {
            if (first) print("Long poll ended.");
            new StringBuilder buf;
            renderMessages(buf, msgs);
            ret withHeader("<!-- " + conv.allCount() + " " + (newDialog ? "NEW DIALOG " : "") + "-->\n"  + buf);
          }
        }
        ret withHeader("");
      }
      
      if (eq(uri, "/n")) ret str(conv.allCount());
      
      {
        lock dbLock();
        S html = templateHTML();
        new StringBuilder buf;
    
        // incremental only    
        //renderMessages(buf, conv.msgs);
      
        S langlinks = "<!-- langlinks here -->";
        if (html.contains(langlinks))
          html = html.replace(langlinks,
            ahref(rawLink("eng"), "English") + " | " + ahref(rawLink("deu"), "German"));
        
        if (forceCookie && nempty(cookie))
          html = html.replace([[localStorage.getItem('cookie')]], jsQuote(cookie));
        html = html.replace("#BOTIMG#", imageSnippetURLOrEmptyGIF(chatHeaderImageID));
        html = html.replace("#N#", "0" /*str(conv.allCount())*/);
        html = html.replace("#INCREMENTALURL#", baseLink + "/incremental?a=");
        html = html.replace("#MSGURL#", baseLink + "/msg?message=");
        html = html.replace("#CSS_ID#", psI_str(cssID));
        if (nempty(params.get("debug")))
          html = html.replace("var showActions = false;", "var showActions = true;");
      
        html = html.replace("#AUTOOPEN#", jsBool(botAutoOpen());
        html = html.replace("#BOT_ON#", jsBool(botOn());
        html = html.replace("$HEADING", joinNemptiesWithSpace(heading, afterHeading));
        html = html.replace("#ONMSGHTML#", unnull(jsOnMsgHTML));
        html = html.replace("<!-- MSGS HERE -->", str(buf));
        html = html.replace("//MORESTUFF//", unnull(moreStuff));
        html = html.replace("//chatBotShown//", unnull(onBotShown));
        html = hreplaceTitle(html, heading);
        
        if (eqGet(params, "_botDemo", "1"))
          ret hhtml(hhead(
            htitle(heading)
            + loadJQuery()
          ) + hbody(hjavascript(html)));
        else {
          optPar bool returnJS;
          ret returnJS ? html : withHeader(serveJavaScript(html));
        }
      }
    }
  } // end of class Request
  
  void addReplyToConvo(Conversation conv, IF0<S> think) {
    S reply = "";
    MsgOut out = null;
    pcall {
      reply = trim(think!);
      out = (MsgOut) quickImport(getThreadLocal(thoughtBot, "out"));
    }
    
    // if no answer and no buttons, don't add message
    if (empty(reply) && (out == null || out.isEmpty())) ret;
    
    Msg msg = new Msg(false, reply);
    msg.out = out;
    conv.add(msg);
  }
  
  O withHeader(S html) {
    ret withHeader(noCacheHeaders(serveHTML(html)));
  }
  
  O withHeader(O response) {
    call(response, 'addHeader, "Access-Control-Allow-Origin", "*");
    ret response;
  }
  
  S renderMessageText(S text) {
    ret replace(htmlEncode2_nlToBr(trim(text)),
      ":wave:", html_wavingHand());
  }
  
  void renderMessages(StringBuilder buf, L<Msg> msgs) {
    new Set<S> buttonsToSkip;
    new LS buttonsHtml;
    for (Msg m : msgs) {
      if (!m.fromUser && eq(m.text, "-")) continue;
      S html = renderMessageText(m.text);
      // pull back & cancel buttons to beginning of msg
      if (m == last(msgs) && m.out != null) {
        fOr (S btn : m.out.buttons)
          if (specialButtons.contains(btn)) {
            buttonsToSkip.add(btn);
            buttonsHtml.add(renderButtons(ll(btn)));
          }
      }
      if (nempty(buttonsHtml) && l(m.out.buttons) == l(buttonsToSkip))
        html += " " + hspan("&nbsp;&nbsp;", class := "chat-button-span") + lines(buttonsHtml);
      else
        buttonsToSkip.clear();
      appendMsg(buf, m.fromUser ? defaultUserName() : botName, formatTime(m.time), html, !m.fromUser);
    }
        
    appendButtons(buf, last(msgs).out, buttonsToSkip);
  }
  
  void appendMsg(StringBuilder buf, S name, S time, S text, bool bot) {
    // img now before text because of position: absolute
    // removed from last div: <span class="direct-chat-timestamp pull-$LR">$TIME</span>
    buf.append(([[<div class="direct-chat-msg doted-border">]]);
    
    S imgID = bot ? botImageID : userImageID;
    new Matches m;
    bool dontSay = bot && startsWith(text, "[don't say]", m);
    if (dontSay) text = m.rest();
    if (nempty(imgID))
      buf.append([[<img alt="message user image" src="$IMG" class="direct-chat-img">]]
        .replace("$IMG", snippetImgLink(imgID)));
    
    buf.append(([[
      <div class="direct-chat-info clearfix">
      <div style="float: right" class="direct-chat-timestamp">$TIME</div>
      <span class="direct-chat-name pull-left">$NAME</span>
      </div>
      <div class="direct-chat-text pull-$LR clearfix]] + (bot ? " bot-utterance" + (dontSay ? " dont-say" : "") : "") + [[">$TEXT</div>
      <div class="direct-chat-info clearfix"></div>
    </div>
    ]]).replace("$LR", bot != botOnRight ? "left" : "right")
       .replace("$NAME", name)
       .replace("$TIME", time)
       .replace("$TEXT", text));
  }
  
  S replaceButtonText(S s) {
    if (eqicOneOf(s, "back", "zurück")) ret unicode_undoArrow();
    if (eqicOneOf(s, "cancel", "Abbrechen")) ret unicode_crossProduct();
    ret s;
  }
  
  S renderButtons(LS buttons) {
    new LS out;
    for i over buttons: {
      S code = buttons.get(i);
      S text = replaceButtonText(code);
      out.add(hbuttonOnClick_returnFalse(text, "submitAMsg(" + jsQuote(text) + ")", class := "chatbot-choice-button", title := eq(code, text) ? null : code));
      if (!specialButtons.contains(code)
        && i+1 < l(buttons) && specialButtons.contains(buttons.get(i+1)))
        out.add("&nbsp;&nbsp;");
    }
    ret lines(out);
  }
  
  void appendButtons(StringBuilder buf, MsgOut out, Set<S> buttonsToSkip) {
    S placeholder = out == null ? "" : unnull(out.placeholder);
    S defaultInput = out == null ? "" : unnull(out.defaultInput);
    buf.append(hscript("chatBot_setInput(" + jsQuote(defaultInput) + ", " + jsQuote(placeholder) + ");"));
    if (out == null) ret;
    LS buttons = listMinusSet(out.buttons, buttonsToSkip);
    if (empty(buttons)) ret;
    S buttonsHtml = renderButtons(buttons);
    buf.append([[<div class="direct-chat-msg doted-border">
      <div class="direct-chat-buttons">
        $BUTTONS
      </div>
    </div>
      ]].replace("$BUTTONS", htmlEncode2_nlToBr(appendNewLineIfNempty(trim(out.buttonsIntro))) + buttonsHtml));
  }
  
  void appendDate(StringBuilder buf, S date) {
    buf.append([[
    <div class="chat-box-single-line">
      <abbr class="timestamp">DATE</abbr>
    </div>]].replace("DATE", date));
  }
  
  S initialMessage() {
    ret (S) call(thoughtBot, "initialMessage"); 
  }
  
  bool lastUserMessageWas(Conversation conv, S message) {
    Msg m = last(conv.msgs);
    ret m != null && m.fromUser && eq(m.text, message);
  }
  
  S makeReply(S message) {
    ret callStaticAnswerMethod(thoughtBot, message);
  }
  
  S formatTime(long time) {
    ret timeInTimeZoneWithOptionalDate_24(timeZone, time);
  }
  
  S formatDialog(S id, L<Msg> msgs) {
    new L<S> lc;
    for (Msg m : msgs)
      lc.add(htmlencode((m.fromUser ? "> " : "< ") + m.text));
    ret id + ul(lc);
  }
  
  Conversation getConv(fS cookie) {
    ret uniq Conversation(+cookie);
  }
  
  S serveAuthForm(S redirect) {
    ret hhtml(hhead(htitle("Authorization required")) + hbody(hfullcenter(
      h3_htmlEncode(heading + " Admin")
      + hpostform(
        hhidden(+redirect)
      + "Password: " + hpassword('pw) + "<br><br>" + hsubmit()))));
  }
  
  S errorMsg(S msg) {
    ret hhtml(hhead_title("Error") + hbody(hfullcenter(msg + "<br><br>" + ahref(jsBackLink(), "Back"))));
  }
  
  S defaultUserName() {
    ret de() ? "Sie" : "You";
  }
  
  bool de() {
    ret isTrue(callOpt(thoughtBot, "de"));
  }
  
  bool botOn() { true; }

  bool botAutoOpen() {
    ret !isFalse(callOpt(thoughtBot, "botAutoOpen"));
  }
  
  swappable S templateHTML() {
    ret loadSnippet(templateID); // TODO: cache
  }
  
  swappable S preprocess(S message) { ret message; }
}

Author comment

Began life as a copy of #1026250

download  show line numbers  debug dex  old transpilations   

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

No comments. add comment

Snippet ID: #1027629
Snippet name: WebChatBot [LIVE]
Eternal ID of this version: #1027629/59
Text MD5: fd0c6c243f0aecf02fa385248bc2a7c7
Transpilation MD5: 53770747c4cea9ac616813ab4608bf17
Author: stefan
Category: javax
Type: JavaX fragment (include)
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2020-07-14 13:59:56
Source code size: 13455 bytes / 394 lines
Pitched / IR pitched: No / No
Views / Downloads: 359 / 968
Version history: 58 change(s)
Referenced in: [show references]