abstract sclass ThoughtBot { } static ThoughtBot thoughtBot; static int longPollTick = 100; static int longPollMaxWait = 1000*60; sclass Msg { long time; bool fromUser; S text; L buttons; *() {} *(bool *fromUser, S *text) { time = now(); } } concept Conversation { S cookie; new LL oldDialogs; new L msgs; void add(Msg m) { msgs.add(m); change(); } int allCount() { ret lengthLevel2(oldDialogs) + l(msgs); } int archiveSize() { ret lengthLevel2(oldDialogs); } } svoid pWebChatBot { db(); } 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 { _registerThread(); registerVisitor(); fS cookie = cookieSent(); try { Conversation conv; { lock dbLock(); if (eq(uri, "/stats")) ret "Threads: " + ul_htmlEncode(getThreadNames(registeredThreads())); if (eq(uri, "/logs")) ret webChatBotLogsHTML(); S message = trim(params.get("btn")); if (empty(message)) message = trim(params.get("message")); conv = getConv(cookie); if (match("clear", message)) { conv.oldDialogs.add(conv.msgs); cset(conv, msgs := new L); conv.change(); thoughtBot.clearSession(conv.cookie); message = null; } thoughtBot.setSession(cookieSent()); if (empty(conv.msgs)) conv.add(new Msg(false, initialMessage())); if (nempty(message) && !lastUserMessageWas(conv, message)) conv.add(new Msg(true, message)); if (nempty(conv.msgs) && last(conv.msgs).fromUser) { S reply = "I'm sorry, I don't understand"; L buttons = null; pcall { reply = or2(makeReply(last(conv.msgs).text), reply); buttons = (L) getThreadLocal(thoughtBot, "buttons"); } Msg msg = new Msg(false, reply); msg.buttons = buttons; conv.add(msg); } } // locked if (eq(uri, "/msg")) ret "OK"; if (eq(uri, "/incremental")) { int a = parseInt(params.get("a")), origA = a; //int b = parseInt(params.get("b")); int as = conv.archiveSize(); a -= as; //b -= as; long start = sysNow(); L msgs; bool first = true; while (licensed() && sysNow() < start+longPollMaxWait) { msgs = subList(conv.msgs, a); if (empty(msgs)) { if (first) { print("Long poll starting on " + cookie + ", " + origA + "/" + a); first = false; } sleep(longPollTick); } else { if (first) print("Long poll ended."); new StringBuilder buf; renderMessages(buf, msgs); ret "\n" + buf; } } ret ""; } { lock dbLock(); S html = loadSnippet(templateID); // TODO: cache new StringBuilder buf; renderMessages(buf, conv.msgs); html = html.replace("#N#", str(conv.allCount())); html = html.replace("#INCREMENTALURL#", relativeRawBotLink(programID(), "incremental?a="); html = html.replace("#MSGURL#", relativeRawBotLink(programID(), "msg?message=")); if (nempty(params.get("debug"))) html = html.replace("var showActions = false;", "var showActions = true;"); html = html.replace("$HEADING", heading); html = html.replace("", str(buf)); html = hreplaceTitle(html, heading); html = hmobilefix(html); ret html; } } finally { _unregisterThread(); } } svoid renderMessages(StringBuilder buf, L msgs) { for (Msg m : msgs) if (m.fromUser || neq(m.text, "-")) appendMsg(buf, m.fromUser ? "Esteemed Customer" : botName, formatTime(m.time), htmlencode(m.text), !m.fromUser); L buttons = last(msgs).buttons; if (nempty(buttons)) appendButtons(buf, buttons); } svoid appendMsg(StringBuilder buf, S name, S time, S text, bool bot) { S imgLink = snippetImgLink(bot ? #1008323 : #1008359); buf.append([[
$NAME
message user image
$TEXT
$TIME
]].replace("$IMG", imgLink) .replace("$NAME", name) .replace("$TIME", time) .replace("$TEXT", text)); } svoid appendButtons(StringBuilder buf, L buttons) { S buttonsHtml = lines(map(buttons, func(S text) { hsubmit(text, name := "btn") })); buf.append([[
$BUTTONS
]].replace("$BUTTONS", buttonsHtml); } svoid appendDate(StringBuilder buf, S date) { buf.append([[
DATE
]].replace("DATE", date)); } sS initialMessage() { ret 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 formatGMT_24(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) }); }