!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 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 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("\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 = ""; 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("", 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 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 msgs) { new Set 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("  ", 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: $TIME buf.append(([[
]]); 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([[message user image]] .replace("$IMG", snippetImgLink(imgID))); buf.append(([[
$TIME
$NAME
$TEXT
]]).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("  "); } ret lines(out); } void appendButtons(StringBuilder buf, MsgOut out, Set 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([[
$BUTTONS
]].replace("$BUTTONS", htmlEncode2_nlToBr(appendNewLineIfNempty(trim(out.buttonsIntro))) + buttonsHtml)); } void appendDate(StringBuilder buf, S date) { buf.append([[
DATE
]].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 msgs) { new L 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) + "

" + hsubmit())))); } S errorMsg(S msg) { ret hhtml(hhead_title("Error") + hbody(hfullcenter(msg + "

" + 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; } }