class WorkerChat { int workerLongPollTick = 200; int workerLongPollMaxWait = 1000*30; long lastWorkerRequested; // timestamp for notification sound S mainAdminLink = "/"; O html(DynNewBot2.Req req) null { SS params = req.params; S uri = req.uri, uri2 = appendSlash(uri); bool requestAuthed = req.auth != null; Conversation conv = req.conv; if (startsWith(uri2, "/worker/")) { if (!req.webRequest.isHttps()) ret subBot_serveRedirect("https://" + req.webRequest.domain() + req.uri + htmlQuery(req.params)); if (!requestAuthed) ret serveAuthForm(params.get('uri)); if (nempty(params.get("turnBotOn"))) conv.turnBotOn(); ret serveWorkerPage(req); } } // auth is tested before we get here S serveWorkerPage(DynNewBot2.Req req) { AuthedDialogID auth = req.auth; Conversation conv = req.conv; S uri = req.uri; SS params = req.params; S cookie = conv.cookie; S uri2 = afterLastSlash(uri); if (eq(uri2, "availableWorkers")) ret "Available workers: " + or2(joinWithComma(map(workersAvailable(), w -> w.renderAsHTML())), "-"); if (nempty(params.get('workerLogOut))) cset(auth, loggedIn := null); if (auth.loggedIn != null && nempty(params.get('workerAvailableBox))) if (cset_trueIfChanged(auth.loggedIn, available := nempty(params.get('workerAvailable)))) noteConversationChange(); // update list of available workers if (nempty(params.get("acceptConversation"))) { if (conv.worker == null) { // only if not accepted by anyone cset(conv, worker := auth.loggedIn); conv.turnBotOff(); } } S loginID = params.get('workerLogIn); if (nempty(loginID)) cset(auth, loggedIn := getConcept Worker(parseLong(loginID))); Map map = prependEmptyOptionForHSelect(mapToOrderedMap(conceptsSortedByFieldCI(Worker, 'loginName), w -> pair(w.id, w.loginName))); if (auth.loggedIn == null) ret hsansserif() + p("You are not logged in as a worker") + hpostform( "Log in as: " + hselect("workerLogIn", map, conceptID(auth.loggedIn)) + " " + hsubmit("OK"), action := botMod().baseLink + "/worker"); // We are logged in if (eq(uri2, "conversation")) { if (conv == null) ret "Conversation not found"; // Serve the checkbox & the JavaScript S onOffURL = botMod().baseLink + "/worker/botOnOff" + hquery(+cookie) + "&on="; ret hsansserif() + loadJQuery() + hhidden(+cookie) // for other frame + hpostform( hhidden(+cookie) + p(renderBotStatus(conv)) + p(conv.botOn ? hsubmit("Accept conversation", name := "acceptConversation") : hsubmit("Turn bot back on", name := "turnBotOn")) , action := botMod().baseLink + "/worker/innerFrameSet", target := "innerFrameSet") // include bot + hscriptsrc(botMod().baseLink + "/script" + hquery(workerMode := 1, cookie := conv.cookie)); } if (eq(uri2, "conversations")) { cset(auth.loggedIn, lastOnline := now()); bool poll = eq("1", params.get("poll")); // poll mode? S content = ""; if (poll) { long seenChange = parseLong(params.get("lastChange")); vmBus_send chatBot_startingWorkerPoll(mc(), conv); long start = sysNow(); L msgs; bool first = true; while (licensed() && sysNow() < start+workerLongPollMaxWait && lastConversationChange == seenChange) sleep(workerLongPollTick); printVars_str(+lastWorkerRequested, +seenChange); if (lastWorkerRequested > seenChange) content = hscript([[ window.parent.parent.frames[0].sendDesktopNotification("A worker is requested!", { action: function() { window.focus(); } }); window.parent.parent.frames[0].playWorkerRequestedSound(); ]]); // if poll times out, send update anyway to update time calculations } long pingThreshold = now()-activeConversationTimeout(); L convos = sortByCalculatedFieldDesc(c -> c.lastMsgTime(), conceptsWithFieldGreaterThan Conversation(lastPing := pingThreshold)); content += hhiddenWithID(+lastConversationChange) + tag table( hsimpletableheader("IP", "Country", "Bot/worker status", "Last change", "Last messages") + mapToLines(convos, c -> { L lastMsgs = lastTwo(c.msgs); S style = c == conv ? "background: #90EE90" : null; S convLink = botMod().baseLink + "/worker/innerFrameSet" + hquery(cookie := c.cookie); ret tag tr( td(ahref(convLink, c.ip, target := "innerFrameSet")) + td(getCountry(c)) + td(renderBotStatus(c)) + td(renderHowLongAgo(c.lastMsgTime())) + td(ahref(convLink, hparagraphs(lambdaMap renderMsgForWorkerChat(lastMsgs)), target := "innerFrameSet", style := "text-decoration: none")), +style, /*class := "clickable-row", "data-href" := convLink*/); }), class := "responstable"); if (poll) ret content; S incrementalURL = botMod().baseLink + "/worker/conversations?poll=1&lastChange="; ret hhtml( hhead(hsansserif() + loadJQuery() + hscript_clickableRows()) + hbody(h3(botName) + hpostform( "Logged in as " + htmlEncode2(auth.loggedIn.loginName) + " (display name: " + htmlEncode2(auth.loggedIn.displayName) + ")" + hhidden(workerAvailableBox := 1) + "   " + hcheckboxWithText("workerAvailable", "I am available", auth.loggedIn.available, onclick := "form.submit()") + "   " + hsubmit("Log out", name := "workerLogOut"), target := "innerFrameSet", action := botMod().baseLink + "/worker/innerFrameSet") + p("Available workers: " + b(or2(joinWithComma( map(workersAvailable(), w -> w.displayName)), "none"))) + h3("Active conversations") + hcss_responstable() + hdivWithID("contentArea", content) + hscript([[ function poll_start() { var lastChange = $("#lastConversationChange").val(); if (!lastChange) setTimeout(poll_start, 1000); else { var url = "#INCREMENTALURL#" + lastChange; console.log("Loading " + url); $.get(url, function(src) { if (src.match(/^ERROR/)) console.log(src); else { console.log("Loaded " + src.length + " chars"); $("#contentArea").html(src); } setTimeout(poll_start, 1000); }, 'text') .fail(function() { console.log("Rescheduling after fail"); setTimeout(poll_start, 1000); }); } } poll_start(); ]].replace("#INCREMENTALURL#", incrementalURL) )); } // end of worker/conversations part if (eq(uri2, "notificationArea")) ret hhtml( hhead(hsansserif() + loadJQuery()) + hbody(hdesktopNotifications() + div(small( span(hbutton("CLICK HERE to enable notification sounds!"), id := "enableSoundsBtn") + " | " + span("", id := "notiStatus")), style := "float: right") + hscript([[ function enableSounds() { document.removeEventListener('click', enableSounds); $("#enableSoundsBtn").html("Notification sounds enabled"); } document.addEventListener('click', enableSounds); if (window.workerRequestedSound == null) { console.log("Loading worker requested sound"); window.workerRequestedSound = new Audio("https://botcompany.de/files/1400404/worker-requested.mp3"); } function playWorkerRequestedSound() { console.log("Playing worker requested sound"); window.workerRequestedSound.play(); } window.playWorkerRequestedSound = playWorkerRequestedSound; ]]))); if (eq(uri2, "innerFrameSet")) // serve frame set 2 ret hhtml(hhead_title("Worker Chat [" + auth.loggedIn.loginName + "]") + hframeset_cols("*,*", tag frame("", name := "conversations", src := botMod().baseLink + "/worker/conversations" + hquery(+cookie)) + tag frame("", name := "conversation", src := conv == null ? null : botMod().baseLink + "/worker/conversation" + hquery(+cookie)))); // serve frame set 1 ret hhtml(hhead_title("Worker Chat [" + auth.loggedIn.loginName + "]") + hframeset_rows("50,*", tag frame("", name := "notificationArea", src := botMod().baseLink + "/worker/notificationArea") + tag frame("", name := "innerFrameSet", src := conv == null ? null : botMod().baseLink + "/worker/innerFrameSet" + hquery(+cookie)))); } S renderMsgForWorkerChat(Msg msg) { ret (msg.fromWorker != null ? htmlEncode2(msg.fromWorker.displayName) : msg.fromUser ? "User" : "Bot") + ": " + b(htmlEncode2If(shouldHtmlEncodeMsg(msg), msg.text)); } Cl workersAvailable() { long timestamp = now()-workerLongPollMaxWait-10000; ret filter(list(Worker), w -> w.available && w.lastOnline >= timestamp); } bool anyWorkersAvailable() { ret nempty(workersAvailable()); } S renderBotStatus(Conversation conv) { ret "Bot is " + b(conv.botOn ? "on" : "off") + "
" + "Assigned worker: " + b(conv.worker == null ? "none" : conv.worker.displayName); } }