1 | // a human who answers to clients |
2 | concept Worker { |
3 | S loginName, displayName; |
4 | //S password; |
5 | bool available; |
6 | long lastOnline; // recent timestamp if online |
7 | |
8 | sS _fieldOrder = "loginName displayName"; |
9 | |
10 | S renderAsHTML() { |
11 | ret htmlEncode2(loginName + " (display name: " + displayName + ")"); |
12 | } |
13 | } |
14 | |
15 | sclass WorkerChat { |
16 | int workerLongPollTick = 200; |
17 | int workerLongPollMaxWait = 1000*30; |
18 | long lastWorkerRequested; // timestamp for notification sound |
19 | |
20 | O html(S uri, SS params, Conversation conv, AuthedDialogID auth) null { |
21 | S uri2 = appendSlash(uri); |
22 | bool requestAuthed = auth != null; |
23 | |
24 | if (startsWith(uri2, "/workers-admin/")) { |
25 | if (!requestAuthed) ret serveAuthForm(params.get('uri)); |
26 | ret serveWorkersAdmin(uri, params); |
27 | } |
28 | |
29 | if (startsWith(uri2, "/worker/")) { |
30 | if (!subBot_isHttps()) |
31 | ret subBot_serveRedirect("https://" + domain() + fullSelfLink(params)); |
32 | |
33 | if (!requestAuthed) ret serveAuthForm(params.get('uri)); |
34 | |
35 | if (nempty(params.get("turnBotOn"))) |
36 | conv.turnBotOn(); |
37 | |
38 | ret serveWorkerPage(auth, conv, uri, params); |
39 | } |
40 | } |
41 | |
42 | // TODO: handle deletion properly |
43 | S serveWorkersAdmin(S uri, SS params) { |
44 | S nav = p(ahref(rawBotLink(dbBotID), "Main admin") + " | " + ahref(baseLink + "/workers-admin", "Workers admin")); |
45 | |
46 | if (eq(uri, "/workers-admin/change-image")) { |
47 | long id = parseLong(params.get("id")); |
48 | Worker worker = getConcept Worker(id); |
49 | File f = workerImageFile(id); |
50 | S content; |
51 | |
52 | S b64 = params.get("base64"); |
53 | if (nempty(b64)) |
54 | saveFile(f, decodeBASE64(b64)); |
55 | |
56 | if (worker == null) content = "Worker not found"; |
57 | else |
58 | content = |
59 | nav + |
60 | hscript([[ |
61 | function submitIt() { |
62 | var file = $('#myUpload')[0].files[0]; |
63 | var reader = new FileReader(); |
64 | |
65 | reader.onloadend = function () { |
66 | var b64 = reader.result.replace(/^data:.+;base64,/, ''); |
67 | $("#base64").val(b64); |
68 | console.log("Got base64 data: " + b64.length); |
69 | $("#submitForm").submit(); |
70 | }; |
71 | |
72 | reader.readAsDataURL(file); |
73 | return false; |
74 | } |
75 | ]]) + |
76 | h2("Worker image for: " + worker.renderAsHTML()) |
77 | + p(!fileExists(f) ? "No image set" : himgsrc(rawLink("worker-image/" + id))) |
78 | + hpostform( |
79 | hhiddenWithIDAndName("base64") |
80 | + "Choose image: " + hfileupload("accept", "image/png,image/jpeg,image/gif", id := "myUpload") + "<br><br>" |
81 | + "Note: Image should be square. Will be scaled to 40x40px in the chat" |
82 | + hhidden(+id), action := rawLink("/workers-admin/change-image"), id := "submitForm") |
83 | + hbuttonOnClick_returnFalse("Upload", "submitIt()"); |
84 | |
85 | ret hhtml(hhead_title("Change worker image") |
86 | + hsansserif() |
87 | + hbody(loadJQuery() |
88 | + content)); |
89 | } |
90 | |
91 | HCRUD_Concepts<Worker> data = new HCRUD_Concepts<Worker>(Worker); |
92 | |
93 | HCRUD crud = new(rawLink("workers-admin"), data) { |
94 | S frame(S title, S contents) { |
95 | ret hhtml(hhead_title_htmldecode(title) + hbody( |
96 | nav + h1(title) + contents)); |
97 | } |
98 | }; |
99 | crud.unshownFields = litset("available", "lastOnline", "away"); |
100 | |
101 | crud.postProcessTableRow = (item, rendered) -> { |
102 | printStruct(+item); |
103 | long id = parseLong(item.get("id")); |
104 | File f = workerImageFile(id); |
105 | ret mapPlus(rendered, "Image" := |
106 | f == null ? "???" : !fileExists(f) ? "-" : himgsrc(rawLink("worker-image/" + id))); |
107 | }; |
108 | crud.renderCmds = item -> crud.renderCmds_base(item) + " | " + ahref(rawLink("workers-admin/change-image" + hquery(id := item.get("id"))), "Change image..."); |
109 | crud.tableClass = "responstable"; |
110 | ret hsansserif() + hcss_responstable() |
111 | + crud.renderPage(params); |
112 | } |
113 | |
114 | // auth is tested before we get here |
115 | S serveWorkerPage(AuthedDialogID auth, Conversation conv, S uri, SS params) { |
116 | S cookie = conv.cookie; |
117 | S uri2 = afterLastSlash(uri); |
118 | |
119 | if (eq(uri2, "availableWorkers")) |
120 | ret "Available workers: " + or2(joinWithComma(map(workersAvailable(), w -> w.renderAsHTML())), "-"); |
121 | |
122 | if (nempty(params.get('workerLogOut))) |
123 | cset(auth, loggedIn := null); |
124 | |
125 | if (auth.loggedIn != null && nempty(params.get('workerAvailableBox))) |
126 | if (cset_trueIfChanged(auth.loggedIn, available := nempty(params.get('workerAvailable)))) |
127 | noteConversationChange(); // update list of available workers |
128 | |
129 | if (nempty(params.get("acceptConversation"))) { |
130 | if (conv.worker == null) { // only if not accepted by anyone |
131 | cset(conv, worker := auth.loggedIn); |
132 | conv.turnBotOff(); |
133 | } |
134 | } |
135 | |
136 | S loginID = params.get('workerLogIn); |
137 | if (nempty(loginID)) |
138 | cset(auth, loggedIn := getConcept Worker(parseLong(loginID))); |
139 | |
140 | Map map = prependEmptyOptionForHSelect(mapToOrderedMap(conceptsSortedByFieldCI(Worker, 'loginName), |
141 | w -> pair(w.id, w.loginName))); |
142 | if (auth.loggedIn == null) |
143 | ret hsansserif() + p("You are not logged in as a worker") |
144 | + hpostform( |
145 | "Log in as: " + hselect("workerLogIn", map, conceptID(auth.loggedIn)) + " " + hsubmit("OK"), action := rawLink("worker")); |
146 | |
147 | // We are logged in |
148 | |
149 | if (eq(uri2, "conversation")) { |
150 | if (conv == null) ret "Conversation not found"; |
151 | // Serve the checkbox & the JavaScript |
152 | S onOffURL = rawLink("worker/botOnOff" + hquery(+cookie) + "&on="); |
153 | ret |
154 | hsansserif() + loadJQuery() |
155 | + hhidden(cookie := conv.cookie) // for other frame |
156 | + hpostform( |
157 | hhidden(+cookie) + |
158 | p(renderBotStatus(conv)) |
159 | + p(conv.botOn |
160 | ? hsubmit("Accept conversation", name := "acceptConversation") |
161 | : hsubmit("Turn bot back on", name := "turnBotOn")) |
162 | , action := rawLink("worker/innerFrameSet"), target := "innerFrameSet") |
163 | |
164 | // include bot |
165 | + hscriptsrc(rawLink(hquery(workerMode := 1, cookie := conv.cookie))); |
166 | } |
167 | |
168 | if (eq(uri2, "conversations")) { |
169 | cset(auth.loggedIn, lastOnline := now()); |
170 | bool poll = eq("1", params.get("poll")); // poll mode? |
171 | S content = ""; |
172 | |
173 | if (poll) { |
174 | long seenChange = parseLong(params.get("lastChange")); |
175 | vmBus_send chatBot_startingWorkerPoll(mc(), conv); |
176 | |
177 | long start = sysNow(); |
178 | L msgs; |
179 | bool first = true; |
180 | while (licensed() && sysNow() < start+workerLongPollMaxWait |
181 | && lastConversationChange == seenChange) |
182 | sleep(workerLongPollTick); |
183 | |
184 | printVars_str(+lastWorkerRequested, +seenChange); |
185 | if (lastWorkerRequested > seenChange) |
186 | content = hscript([[ |
187 | window.parent.parent.frames[0].sendDesktopNotification("A worker is requested!", { action: function() { window.focus(); } }); |
188 | window.parent.parent.frames[0].playWorkerRequestedSound(); |
189 | ]]); |
190 | |
191 | // if poll times out, send update anyway to update time calculations |
192 | } |
193 | |
194 | long pingThreshold = now()-activeConversationTimeout(); |
195 | L<Conversation> convos = sortByCalculatedFieldDesc(c -> c.lastMsgTime(), |
196 | conceptsWithFieldGreaterThan Conversation(lastPing := pingThreshold)); |
197 | |
198 | content += |
199 | hhiddenWithID(+lastConversationChange) + tag table( |
200 | hsimpletableheader("IP", "Country", "Bot/worker status", "Last change", "Last messages") |
201 | + mapToLines(convos, c -> { |
202 | L<Msg> lastMsgs = lastTwo(c.msgs); |
203 | S style = c == conv ? "background: #90EE90" : null; |
204 | S convLink = rawLink("worker/innerFrameSet" + hquery(cookie := c.cookie)); |
205 | ret tag tr( |
206 | td(ahref(convLink, c.ip, target := "innerFrameSet")) + |
207 | td(getCountry(c)) + |
208 | td(renderBotStatus(c)) + |
209 | td(renderHowLongAgo(c.lastMsgTime())) + |
210 | td(ahref(convLink, hparagraphs(lambdaMap renderMsgForWorkerChat(lastMsgs)), target := "innerFrameSet", style := "text-decoration: none")), +style, /*class := "clickable-row", "data-href" := convLink*/); |
211 | }), class := "responstable"); |
212 | |
213 | if (poll) ret content; |
214 | |
215 | S incrementalURL = rawLink("worker/conversations?poll=1&lastChange="); |
216 | |
217 | ret hhtml( |
218 | hhead(hsansserif() + loadJQuery() |
219 | + hscript_clickableRows()) |
220 | + hbody(h3(botName) |
221 | + hpostform( |
222 | "Logged in as " |
223 | + htmlEncode2(auth.loggedIn.loginName) |
224 | + " (display name: " + htmlEncode2(auth.loggedIn.displayName) + ")" |
225 | + hhidden(workerAvailableBox := 1) |
226 | + " " |
227 | + hcheckboxWithText("workerAvailable", "I am available", auth.loggedIn.available, onclick := "form.submit()") |
228 | + " " |
229 | + hsubmit("Log out", name := "workerLogOut"), |
230 | target := "innerFrameSet", action := rawLink("worker/innerFrameSet")) |
231 | + p("Available workers: " + b(or2(joinWithComma( |
232 | map(workersAvailable(), w -> w.displayName)), "none"))) |
233 | + h3("Active conversations") |
234 | + hcss_responstable() |
235 | + hdivWithID("contentArea", content) |
236 | + hscript([[ |
237 | function poll_start() { |
238 | var lastChange = $("#lastConversationChange").val(); |
239 | if (!lastChange) |
240 | setTimeout(poll_start, 1000); |
241 | else { |
242 | var url = "#INCREMENTALURL#" + lastChange; |
243 | console.log("Loading " + url); |
244 | $.get(url, function(src) { |
245 | if (src.match(/^ERROR/)) console.log(src); |
246 | else { |
247 | console.log("Loaded " + src.length + " chars"); |
248 | $("#contentArea").html(src); |
249 | } |
250 | setTimeout(poll_start, 1000); |
251 | }, 'text') |
252 | .fail(function() { |
253 | console.log("Rescheduling after fail"); |
254 | setTimeout(poll_start, 1000); |
255 | }); |
256 | } |
257 | } |
258 | poll_start(); |
259 | ]].replace("#INCREMENTALURL#", incrementalURL) |
260 | )); |
261 | } // end of worker/conversations part |
262 | |
263 | if (eq(uri2, "notificationArea")) |
264 | ret hhtml( |
265 | hhead(hsansserif() + loadJQuery()) |
266 | + hbody(hdesktopNotifications() |
267 | + div(small( |
268 | span(hbutton("CLICK HERE to enable notification sounds!"), id := "enableSoundsBtn") |
269 | + " | " |
270 | + span("", id := "notiStatus")), style := "float: right") |
271 | + hscript([[ |
272 | function enableSounds() { |
273 | document.removeEventListener('click', enableSounds); |
274 | $("#enableSoundsBtn").html("Notification sounds enabled"); |
275 | } |
276 | document.addEventListener('click', enableSounds); |
277 | |
278 | if (window.workerRequestedSound == null) { |
279 | console.log("Loading worker requested sound"); |
280 | window.workerRequestedSound = new Audio("https://botcompany.de/files/1400404/worker-requested.mp3"); |
281 | } |
282 | |
283 | function playWorkerRequestedSound() { |
284 | console.log("Playing worker requested sound"); |
285 | window.workerRequestedSound.play(); |
286 | } |
287 | window.playWorkerRequestedSound = playWorkerRequestedSound; |
288 | |
289 | ]]))); |
290 | |
291 | if (eq(uri2, "innerFrameSet")) |
292 | // serve frame set 2 |
293 | ret hhtml(hhead_title("Worker Chat [" + auth.loggedIn.loginName + "]") |
294 | + hframeset_cols("*,*", |
295 | tag frame("", name := "conversations", src := rawLink("worker/conversations" + hquery(+cookie))) + |
296 | tag frame("", name := "conversation", src := conv == null ? null : rawLink("worker/conversation" + hquery(+cookie))))); |
297 | |
298 | // serve frame set 1 |
299 | ret hhtml(hhead_title("Worker Chat [" + auth.loggedIn.loginName + "]") |
300 | + hframeset_rows("50,*", |
301 | tag frame("", name := "notificationArea", src := rawLink("worker/notificationArea")) + |
302 | tag frame("", name := "innerFrameSet", src := conv == null ? null : rawLink("worker/innerFrameSet" + hquery(+cookie))))); |
303 | |
304 | } |
305 | |
306 | S renderMsgForWorkerChat(Msg msg) { |
307 | ret (msg.fromWorker != null ? htmlEncode2(msg.fromWorker.displayName) : msg.fromUser ? "User" : "Bot") + ": " + b(htmlEncode2If(shouldHtmlEncodeMsg(msg), msg.text)); |
308 | } |
309 | |
310 | Cl<Worker> workersAvailable() { |
311 | long timestamp = now()-workerLongPollMaxWait-10000; |
312 | ret filter(list(Worker), w -> w.available && w.lastOnline >= timestamp); |
313 | } |
314 | |
315 | bool anyWorkersAvailable() { |
316 | ret nempty(workersAvailable()); |
317 | } |
318 | |
319 | S renderBotStatus(Conversation conv) { |
320 | ret "Bot is " + b(conv.botOn ? "on" : "off") + "<br>" |
321 | + "Assigned worker: " + b(conv.worker == null ? "none" : conv.worker.displayName); |
322 | } |
323 | } |
download show line numbers debug dex old transpilations
Travelled to 7 computer(s): bhatertpkbcr, mqqgnosmbjvj, pyentgdyhuwx, pzhvpgtvlbxg, tvejysmllsmz, vouqrxazstgt, xrpafgyirdlv
No comments. add comment
Snippet ID: | #1028434 |
Snippet name: | WorkerChat + concept Worker [Include for web chat bots] |
Eternal ID of this version: | #1028434/67 |
Text MD5: | 5a6f64bc62bd8cbbfc8c46f660765760 |
Author: | stefan |
Category: | javax / web chat bots |
Type: | JavaX fragment (include) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2020-07-13 21:00:33 |
Source code size: | 13125 bytes / 323 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 304 / 1752 |
Version history: | 66 change(s) |
Referenced in: | [show references] |