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