sO thoughtBot; static int longPollTick = 100; static int longPollMaxWait = 1000*30; // lowered to 30 seconds sS botImageID = #1102802; sS userImageID = #1102803; sS chatHeaderImageID = #1102802; sS cssID = #1026220; sS baseLink; sbool botOnRight = true; sS timeZone = germanTimeZone_string(); static Set 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, ip; new LL oldDialogs; new L msgs; void add(Msg m) { syncAdd(msgs, m); change(); vmBus_send chatBot_messageAdded(mc(), this, m); } int allCount() { ret lengthLevel2(oldDialogs) + l(msgs); } int archiveSize() { ret lengthLevel2(oldDialogs); } } svoid pWebChatBot { db(); if (thoughtBotID != null) 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; if (conv != null) cset(conv, ip := subBot_clientIP()); 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); vmBus_send chatBot_clearedSession(mc(), conv); 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")) { vmBus_send chatBot_userPolling(mc(), conv); 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("\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 = ""; 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="); 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", heading); html = html.replace("", str(buf)); html = hreplaceTitle(html, heading); if (eqGet(params, "_botDemo", "1")) ret hhtml(hhead( htitle(heading) + loadJQuery() ) + hbody(hjavascript(html))); else ret withHeader(subBot_serveJavaScript(html)); } } svoid addReplyToConvo(Conversation conv, IF0 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_noCacheHeaders(subBot_serveHTML(html))); } sO withHeader(O response) { call(response, 'addHeader, "Access-Control-Allow-Origin", "*"); ret response; } sS renderMessageText(S text) { ret replace(htmlEncode2_nlToBr(trim(text)), ":wave:", html_wavingHand()); } svoid 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); } svoid 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; 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)); } 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("  "); } ret lines(out); } svoid appendButtons(StringBuilder buf, Out 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)); } svoid appendDate(StringBuilder buf, S date) { buf.append([[
DATE
]].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 msgs) { new L 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) + "

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

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