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

362
LINES

< > BotCompany Repo | #1025984 // Web Chat Bot Include v2 [used by MMO]

JavaX fragment (include)

sO thoughtBot;

static int longPollTick = 100;
static int longPollMaxWait = 1000*60;

sS botImageID = #1102802;
sS userImageID = #1102803;
sS chatHeaderImageID = #1102802;
sS baseLink;
sbool botOnRight = true;
sS timeZone = germanTimeZone_string();

static Set<S> specialButtons = litciset("Cancel", "Back", "Abbrechen", "Zurück");

sclass Out extends DynamicObject {
  S buttonsIntro;
  LS buttons;
  S placeholder;
  S defaultInput;
}

sclass Msg extends DynamicObject {
  long time;
  bool fromUser;
  S text;
  Out out;
  
  *() {}
  *(bool *fromUser, S *text) { time = now(); }
}

concept Conversation {
  bool authed;
  S cookie;
  new LL<Msg> oldDialogs;
  new L<Msg> msgs;

  void add(Msg m) {
    syncAdd(msgs, m);
    change();
  }
  
  int allCount() { ret lengthLevel2(oldDialogs) + l(msgs); }
  int archiveSize() { ret lengthLevel2(oldDialogs); }
}

svoid pWebChatBot {
  db();
  thoughtBot = runDependent(thoughtBotID);
  Class envType = fieldType(thoughtBot, "env");
  if (envType != null)
    setOpt(thoughtBot, "env", proxy(envType, (O) mc()));
}

static 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());
}

html {
  temp tempRegisterThread();
  S cookie = params.get('cookie);
  if (empty(cookie)) {
    registerVisitor();
    cookie = cookieSent();
  }
  Conversation conv = nempty(cookie) ? getConv(cookie) : null;
  print("URI: " + uri + ", cookie: " + cookie + ", 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 "";
  }

  {
    lock dbLock();
    
    S message = trim(params.get("btn"));
    if (empty(message)) message = trim(params.get("message"));
    
    if (match("new dialog", message)) {
      conv.oldDialogs.add(conv.msgs);
      cset(conv, msgs := new L);
      conv.change();
      callOpt(thoughtBot, "clearSession", conv.cookie);
      message = null;
    }
    
    call(thoughtBot, "setSession", cookie, params);
    
    if (empty(conv.msgs))
      addReplyToConvo(conv, lambda0 initialMessage);
      
    if (nempty(message) && !lastUserMessageWas(conv, message)) {
      print("Adding message: " + message);
      conv.add(new Msg(true, message));
    }
  
    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, "/incremental")) {
    int a = parseInt(params.get("a"));

    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);
          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("");
  }
  
  {
    lock dbLock();
    processParams(params);
    S html = loadSnippet(templateID); // TODO: cache
    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"));
    
    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=");
    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", heading);
    html = html.replace("<!-- MSGS HERE -->", str(buf));
    html = hreplaceTitle(html, heading);
    
    if (eqGet(params, "_botDemo", "1"))
      ret hhtml(hhead(
        htitle("Bot Demo")
        + loadJQuery()
      ) + hbody(hjavascript(html)));
    else
      ret withHeader(subBot_serveJavaScript(html));
  }
}

svoid addReplyToConvo(Conversation conv, IF0<S> think) {
  S reply = "";
  Out out = null;
  pcall {
    reply = think!;
    out = (Out) quickImport(getThreadLocal(thoughtBot, "out"));
  }
  Msg msg = new Msg(false, reply);
  msg.out = out;
  conv.add(msg);
}

sO withHeader(S html) {
  ret withHeader(subBot_serveHTML(html));
}

sO withHeader(O response) {
  call(response, 'addHeader, "Access-Control-Allow-Origin", "*");
  ret response;
}

svoid 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 = htmlEncode2_nlToBr(trim(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);
}

svoid 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;
  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">
      $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));
}

sS replaceButtonText(S s) {
  if (eqicOneOf(s, "back", "zurück")) ret unicode_undoArrow();
  if (eqicOneOf(s, "cancel", "Abbrechen")) ret unicode_crossProduct();
  ret s;
}

sS 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);
}

svoid appendButtons(StringBuilder buf, Out 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));
}

svoid appendDate(StringBuilder buf, S date) {
  buf.append([[
  <div class="chat-box-single-line">
    <abbr class="timestamp">DATE</abbr>
  </div>]].replace("DATE", date));
}

sS initialMessage() {
  ret (S) call(thoughtBot, "initialMessage"); 
}

static bool lastUserMessageWas(Conversation conv, S message) {
  Msg m = last(conv.msgs);
  ret m != null && m.fromUser && eq(m.text, message);
}

sS makeReply(S message) {
  ret callStaticAnswerMethod(thoughtBot, message);
}

sS formatTime(long time) {
  ret timeInTimeZoneWithOptionalDate_24(timeZone, time);
}

sS 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);
}

static Conversation getConv(fS cookie) {
  ret withDBLock(func -> Conversation {
    uniq(Conversation, +cookie)
  });
}

sS serveAuthForm(S redirect) {
  ret hhtml(hhead(htitle("Authorization required")) + hbody(hfullcenter(
    h3_htmlEncode(heading + " Admin")
    + hpostform(
      hhidden(+redirect)
    + "Password: " + hpassword('pw) + "<br><br>" + hsubmit()))));
}

sS errorMsg(S msg) {
  ret hhtml(hhead_title("Error") + hbody(hfullcenter(msg + "<br><br>" + ahref(jsBackLink(), "Back"))));
}

sS defaultUserName() {
  ret de() ? "Sie" : "You";
}

sbool de() {
  ret isTrue(callOpt(thoughtBot, "de"));
}

sbool botOn() {
  ret isTrue(callOpt(thoughtBot, "botOn"));
}

sbool botAutoOpen() {
  ret !isFalse(callOpt(thoughtBot, "botAutoOpen"));
}

Author comment

Began life as a copy of #1008764

download  show line numbers  debug dex  old transpilations   

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

No comments. add comment

Snippet ID: #1025984
Snippet name: Web Chat Bot Include v2 [used by MMO]
Eternal ID of this version: #1025984/100
Text MD5: 70b726b84e05d5918e8ad9d68774bb09
Author: stefan
Category: javax / a.i.
Type: JavaX fragment (include)
Public (visible to everyone): Yes
Archived (hidden from active list): No
Created/modified: 2019-12-07 20:49:36
Source code size: 10781 bytes / 362 lines
Pitched / IR pitched: No / No
Views / Downloads: 358 / 690
Version history: 99 change(s)
Referenced in: [show references]