class WorkerChat {
int workerLongPollTick = 200;
int workerLongPollMaxWait = 1000*30;
long lastWorkerRequested; // timestamp for notification sound
S mainAdminLink = "/";
O html(S uri, SS params, Conversation conv, AuthedDialogID auth) null {
S uri2 = appendSlash(uri);
bool requestAuthed = auth != null;
if (startsWith(uri2, "/workers-admin/")) {
if (!requestAuthed) ret serveAuthForm(params.get('uri));
ret serveWorkersAdmin(uri, params);
}
if (startsWith(uri2, "/worker/")) {
if (!subBot_isHttps())
ret subBot_serveRedirect("https://" + domain() + fullSelfLink(params));
if (!requestAuthed) ret serveAuthForm(params.get('uri));
if (nempty(params.get("turnBotOn")))
conv.turnBotOn();
ret serveWorkerPage(auth, conv, uri, params);
}
}
// TODO: handle deletion properly
S serveWorkersAdmin(S uri, SS params) {
S nav = p(ahref(mainAdminLink, "Main admin") + " | " + ahref(baseLink + "/workers-admin", "Workers admin"));
if (eq(uri, "/workers-admin/change-image")) {
long id = parseLong(params.get("id"));
Worker worker = getConcept Worker(id);
File f = workerImageFile(id);
S content;
S b64 = params.get("base64");
if (nempty(b64))
saveFile(f, decodeBASE64(b64));
if (worker == null) content = "Worker not found";
else
content =
nav +
hscript([[
function submitIt() {
var file = $('#myUpload')[0].files[0];
var reader = new FileReader();
reader.onloadend = function () {
var b64 = reader.result.replace(/^data:.+;base64,/, '');
$("#base64").val(b64);
console.log("Got base64 data: " + b64.length);
$("#submitForm").submit();
};
reader.readAsDataURL(file);
return false;
}
]]) +
h2("Worker image for: " + worker.renderAsHTML())
+ p(!fileExists(f) ? "No image set" : himgsrc(rawLink("worker-image/" + id)))
+ hpostform(
hhiddenWithIDAndName("base64")
+ "Choose image: " + hfileupload("accept", "image/png,image/jpeg,image/gif", id := "myUpload") + "
"
+ "Note: Image should be square. Will be scaled to 40x40px in the chat"
+ hhidden(+id), action := rawLink("/workers-admin/change-image"), id := "submitForm")
+ hbuttonOnClick_returnFalse("Upload", "submitIt()");
ret hhtml(hhead_title("Change worker image")
+ hsansserif()
+ hbody(loadJQuery()
+ content));
}
HCRUD_Concepts data = new HCRUD_Concepts(Worker);
HCRUD crud = new(rawLink("workers-admin"), data) {
S frame(S title, S contents) {
ret hhtml(hhead_title_htmldecode(title) + hbody(
nav + h1(title) + contents));
}
};
crud.unshownFields = litset("available", "lastOnline", "away");
crud.postProcessTableRow = (item, rendered) -> {
printStruct(+item);
long id = parseLong(item.get("id"));
File f = workerImageFile(id);
ret mapPlus(rendered, "Image" :=
f == null ? "???" : !fileExists(f) ? "-" : himgsrc(rawLink("worker-image/" + id)));
};
crud.renderCmds = item -> crud.renderCmds_base(item) + " | " + ahref(rawLink("workers-admin/change-image" + hquery(id := item.get("id"))), "Change image...");
crud.tableClass = "responstable";
ret hsansserif() + hcss_responstable()
+ crud.renderPage(params);
}
// auth is tested before we get here
S serveWorkerPage(AuthedDialogID auth, Conversation conv, S uri, SS 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 := rawLink("worker"));
// We are logged in
if (eq(uri2, "conversation")) {
if (conv == null) ret "Conversation not found";
// Serve the checkbox & the JavaScript
S onOffURL = rawLink("worker/botOnOff" + hquery(+cookie) + "&on=");
ret
hsansserif() + loadJQuery()
+ hhidden(cookie := conv.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 := rawLink("worker/innerFrameSet"), target := "innerFrameSet")
// include bot
+ hscriptsrc(rawLink(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 = rawLink("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 = rawLink("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 := rawLink("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 := rawLink("worker/conversations" + hquery(+cookie))) +
tag frame("", name := "conversation", src := conv == null ? null : rawLink("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 := rawLink("worker/notificationArea")) +
tag frame("", name := "innerFrameSet", src := conv == null ? null : rawLink("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);
}
}