Libraryless. Click here for Pure Java version (16847L/123K).
1 | !include once #1027630 // Msg + Conversation |
2 | |
3 | sclass WebChatBot { |
4 | O thoughtBot; |
5 | |
6 | int longPollTick = 100; |
7 | int longPollMaxWait = 1000*30; // lowered to 30 seconds |
8 | |
9 | S templateID = #1027638; |
10 | S botName = "Chat Bot"; |
11 | S heading = "Chat Bot"; |
12 | S afterHeading; |
13 | S botImageID = #1102802; |
14 | S userImageID = #1102803; |
15 | S chatHeaderImageID = #1102802; |
16 | S cssID = #1026266; |
17 | S baseLink; |
18 | S jsOnMsgHTML; // operates on variable "src" |
19 | bool botOnRight = true; |
20 | S timeZone = germanTimeZone_string(); |
21 | S moreStuff; // JS code to execute after init is done |
22 | S onBotShown; |
23 | bool forceCookie; |
24 | |
25 | Set<S> specialButtons = litciset("Cancel", "Back", "Abbrechen", "Zurück"); |
26 | |
27 | void start { |
28 | db(); |
29 | Class envType = fieldType(thoughtBot, "env"); |
30 | if (envType != null) |
31 | setOpt(thoughtBot, "env", proxy(envType, (O) mc())); |
32 | } |
33 | |
34 | void sayAsync(S session, S text) { |
35 | lock dbLock(); |
36 | Conversation conv = getConv(session); |
37 | conv.add(new Msg(false, text)); |
38 | print("sayAsync " + session + ", new size=" + conv.allCount()); |
39 | } |
40 | |
41 | Request newRequest(S uri, SS params) { ret new Request(uri, params); } |
42 | |
43 | O html(S uri, SS params, O... _) { |
44 | ret newRequest(uri, params).html(_); |
45 | } |
46 | |
47 | class Request { |
48 | S uri; |
49 | SS params; |
50 | S cookie; |
51 | S clientIP; |
52 | |
53 | *(S *uri, SS *params) {} |
54 | |
55 | O html(O... _) { |
56 | temp tempRegisterThread(); |
57 | uri = dropTrailingSlashIfNemptyAfterwards(uri); |
58 | if (cookie == null) cookie = params.get("cookie"); |
59 | Conversation conv = nempty(cookie) ? getConv(cookie) : null; |
60 | if (conv != null) { |
61 | if (clientIP == null) clientIP = clientIP(); |
62 | cset(conv, ip := clientIP); |
63 | } |
64 | print("URI: " + uri + ", cookie: " + cookie + (conv == null ? "" : ", msgs: " + l(conv.msgs))); |
65 | |
66 | S pw = trim(params.get('pw)); |
67 | if (nempty(pw)) { |
68 | S realPW = trim(loadSecretTextFile("password.txt")); |
69 | if (empty(realPW)) ret errorMsg("Administrator has not set a password"); |
70 | if (neq(pw, realPW)) |
71 | ret errorMsg("Bad password, please try again"); |
72 | cset(conv, authed := true); |
73 | if (nempty(params.get('redirect))) |
74 | ret hrefresh(params.get('redirect)); |
75 | } |
76 | |
77 | if (eq(uri, "/stats")) { |
78 | if (!conv.authed) ret serveAuthForm(rawLink(uri)); |
79 | ret "Threads: " + ul_htmlEncode(getThreadNames(registeredThreads())); |
80 | } |
81 | |
82 | if (eq(uri, "/logs")) { |
83 | if (!conv.authed) ret serveAuthForm(rawLink(uri)); |
84 | ret webChatBotLogsHTML2(rawLink(uri), params); |
85 | } |
86 | |
87 | if (eq(uri, "/auth-only")) { |
88 | if (eq(params.get('logout), "1")) |
89 | cset(conv, authed := false); |
90 | if (!conv.authed) ret serveAuthForm(params.get('uri)); |
91 | ret ""; |
92 | } |
93 | |
94 | if (conv != null) { |
95 | lock dbLock(); |
96 | |
97 | S message = trim(params.get("btn")); |
98 | if (empty(message)) message = trim(params.get("message")); |
99 | message = preprocess(message); |
100 | |
101 | if (match_vbar("new dialog|new dialogue", message)) { |
102 | conv.oldDialogs.add(conv.msgs); |
103 | cset(conv, msgs := new L); |
104 | conv.change(); |
105 | callOpt(thoughtBot, "clearSession", conv.cookie); |
106 | vmBus_send chatBot_clearedSession(mc(), conv); |
107 | message = null; |
108 | } |
109 | |
110 | callOpt(thoughtBot, "setSession", cookie, params); |
111 | |
112 | if (empty(conv.msgs)) |
113 | addReplyToConvo(conv, lambda0 initialMessage); |
114 | |
115 | if (nempty(message) && !lastUserMessageWas(conv, message)) { |
116 | conv.add(new Msg(true, message)); |
117 | print("Added message: " + message + " to " + conv.id + ", l=" + l(conv.msgs)); |
118 | } |
119 | |
120 | if (nempty(conv.msgs) && last(conv.msgs).fromUser) |
121 | addReplyToConvo(conv, () -> makeReply(last(conv.msgs).text)); |
122 | } // locked |
123 | |
124 | if (eq(uri, "/msg")) ret withHeader("OK"); |
125 | |
126 | if (eq(uri, "/grabFullLog")) { |
127 | if (conv == null) ret "No conversation for " + cookie; |
128 | L<Msg> msgs = conv.allMsgs(); |
129 | ret jsonEncode(map(msgs, msg -> |
130 | litorderedmap( |
131 | fromUser := msg.fromUser, |
132 | text := msg.text, |
133 | time := msg.time))); |
134 | } |
135 | |
136 | if (eq(uri, "/incremental")) { |
137 | vmBus_send chatBot_userPolling(mc(), conv); |
138 | int a = parseInt(params.get("a")); |
139 | |
140 | print("a=" + a + ", as=" + conv.archiveSize()); |
141 | |
142 | long start = sysNow(); |
143 | L msgs; |
144 | bool first = true; |
145 | while (licensed() && sysNow() < start+longPollMaxWait) { |
146 | int as = conv.archiveSize(); |
147 | msgs = cloneSubList(conv.msgs, a-as); |
148 | bool newDialog = a <= as; |
149 | if (empty(msgs)) { |
150 | if (first) { |
151 | print("Long poll starting on " + cookie + ", " + a + "/" + a + ", id=" + conv.id); |
152 | first = false; |
153 | } |
154 | sleep(longPollTick); |
155 | } else { |
156 | if (first) print("Long poll ended."); |
157 | new StringBuilder buf; |
158 | renderMessages(buf, msgs); |
159 | ret withHeader("<!-- " + conv.allCount() + " " + (newDialog ? "NEW DIALOG " : "") + "-->\n" + buf); |
160 | } |
161 | } |
162 | ret withHeader(""); |
163 | } |
164 | |
165 | if (eq(uri, "/n")) ret str(conv.allCount()); |
166 | |
167 | { |
168 | lock dbLock(); |
169 | S html = templateHTML(); |
170 | new StringBuilder buf; |
171 | |
172 | // incremental only |
173 | //renderMessages(buf, conv.msgs); |
174 | |
175 | S langlinks = "<!-- langlinks here -->"; |
176 | if (html.contains(langlinks)) |
177 | html = html.replace(langlinks, |
178 | ahref(rawLink("eng"), "English") + " | " + ahref(rawLink("deu"), "German")); |
179 | |
180 | if (forceCookie && nempty(cookie)) |
181 | html = html.replace([[localStorage.getItem('cookie')]], jsQuote(cookie)); |
182 | html = html.replace("#BOTIMG#", imageSnippetURLOrEmptyGIF(chatHeaderImageID)); |
183 | html = html.replace("#N#", "0" /*str(conv.allCount())*/); |
184 | html = html.replace("#INCREMENTALURL#", baseLink + "/incremental?a="); |
185 | html = html.replace("#MSGURL#", baseLink + "/msg?message="); |
186 | html = html.replace("#CSS_ID#", psI_str(cssID)); |
187 | if (nempty(params.get("debug"))) |
188 | html = html.replace("var showActions = false;", "var showActions = true;"); |
189 | |
190 | html = html.replace("#AUTOOPEN#", jsBool(botAutoOpen()); |
191 | html = html.replace("#BOT_ON#", jsBool(botOn()); |
192 | html = html.replace("$HEADING", joinNemptiesWithSpace(heading, afterHeading)); |
193 | html = html.replace("#ONMSGHTML#", unnull(jsOnMsgHTML)); |
194 | html = html.replace("<!-- MSGS HERE -->", str(buf)); |
195 | html = html.replace("//MORESTUFF//", unnull(moreStuff)); |
196 | html = html.replace("//chatBotShown//", unnull(onBotShown)); |
197 | html = hreplaceTitle(html, heading); |
198 | |
199 | if (eqGet(params, "_botDemo", "1")) |
200 | ret hhtml(hhead( |
201 | htitle(heading) |
202 | + loadJQuery() |
203 | ) + hbody(hjavascript(html))); |
204 | else { |
205 | optPar bool returnJS; |
206 | ret returnJS ? html : withHeader(serveJavaScript(html)); |
207 | } |
208 | } |
209 | } |
210 | } // end of class Request |
211 | |
212 | void addReplyToConvo(Conversation conv, IF0<S> think) { |
213 | S reply = ""; |
214 | MsgOut out = null; |
215 | pcall { |
216 | reply = trim(think!); |
217 | out = (MsgOut) quickImport(getThreadLocal(thoughtBot, "out")); |
218 | } |
219 | |
220 | // if no answer and no buttons, don't add message |
221 | if (empty(reply) && (out == null || out.isEmpty())) ret; |
222 | |
223 | Msg msg = new Msg(false, reply); |
224 | msg.out = out; |
225 | conv.add(msg); |
226 | } |
227 | |
228 | O withHeader(S html) { |
229 | ret withHeader(noCacheHeaders(serveHTML(html))); |
230 | } |
231 | |
232 | O withHeader(O response) { |
233 | call(response, 'addHeader, "Access-Control-Allow-Origin", "*"); |
234 | ret response; |
235 | } |
236 | |
237 | S renderMessageText(S text) { |
238 | ret replace(htmlEncode2_nlToBr(trim(text)), |
239 | ":wave:", html_wavingHand()); |
240 | } |
241 | |
242 | void renderMessages(StringBuilder buf, L<Msg> msgs) { |
243 | new Set<S> buttonsToSkip; |
244 | new LS buttonsHtml; |
245 | for (Msg m : msgs) { |
246 | if (!m.fromUser && eq(m.text, "-")) continue; |
247 | S html = renderMessageText(m.text); |
248 | // pull back & cancel buttons to beginning of msg |
249 | if (m == last(msgs) && m.out != null) { |
250 | fOr (S btn : m.out.buttons) |
251 | if (specialButtons.contains(btn)) { |
252 | buttonsToSkip.add(btn); |
253 | buttonsHtml.add(renderButtons(ll(btn))); |
254 | } |
255 | } |
256 | if (nempty(buttonsHtml) && l(m.out.buttons) == l(buttonsToSkip)) |
257 | html += " " + hspan(" ", class := "chat-button-span") + lines(buttonsHtml); |
258 | else |
259 | buttonsToSkip.clear(); |
260 | appendMsg(buf, m.fromUser ? defaultUserName() : botName, formatTime(m.time), html, !m.fromUser); |
261 | } |
262 | |
263 | appendButtons(buf, last(msgs).out, buttonsToSkip); |
264 | } |
265 | |
266 | void appendMsg(StringBuilder buf, S name, S time, S text, bool bot) { |
267 | // img now before text because of position: absolute |
268 | // removed from last div: <span class="direct-chat-timestamp pull-$LR">$TIME</span> |
269 | buf.append(([[<div class="direct-chat-msg doted-border">]]); |
270 | |
271 | S imgID = bot ? botImageID : userImageID; |
272 | new Matches m; |
273 | bool dontSay = bot && startsWith(text, "[don't say]", m); |
274 | if (dontSay) text = m.rest(); |
275 | if (nempty(imgID)) |
276 | buf.append([[<img alt="message user image" src="$IMG" class="direct-chat-img">]] |
277 | .replace("$IMG", snippetImgLink(imgID))); |
278 | |
279 | buf.append(([[ |
280 | <div class="direct-chat-info clearfix"> |
281 | <div style="float: right" class="direct-chat-timestamp">$TIME</div> |
282 | <span class="direct-chat-name pull-left">$NAME</span> |
283 | </div> |
284 | <div class="direct-chat-text pull-$LR clearfix]] + (bot ? " bot-utterance" + (dontSay ? " dont-say" : "") : "") + [[">$TEXT</div> |
285 | <div class="direct-chat-info clearfix"></div> |
286 | </div> |
287 | ]]).replace("$LR", bot != botOnRight ? "left" : "right") |
288 | .replace("$NAME", name) |
289 | .replace("$TIME", time) |
290 | .replace("$TEXT", text)); |
291 | } |
292 | |
293 | S replaceButtonText(S s) { |
294 | if (eqicOneOf(s, "back", "zurück")) ret unicode_undoArrow(); |
295 | if (eqicOneOf(s, "cancel", "Abbrechen")) ret unicode_crossProduct(); |
296 | ret s; |
297 | } |
298 | |
299 | S renderButtons(LS buttons) { |
300 | new LS out; |
301 | for i over buttons: { |
302 | S code = buttons.get(i); |
303 | S text = replaceButtonText(code); |
304 | out.add(hbuttonOnClick_returnFalse(text, "submitAMsg(" + jsQuote(text) + ")", class := "chatbot-choice-button", title := eq(code, text) ? null : code)); |
305 | if (!specialButtons.contains(code) |
306 | && i+1 < l(buttons) && specialButtons.contains(buttons.get(i+1))) |
307 | out.add(" "); |
308 | } |
309 | ret lines(out); |
310 | } |
311 | |
312 | void appendButtons(StringBuilder buf, MsgOut out, Set<S> buttonsToSkip) { |
313 | S placeholder = out == null ? "" : unnull(out.placeholder); |
314 | S defaultInput = out == null ? "" : unnull(out.defaultInput); |
315 | buf.append(hscript("chatBot_setInput(" + jsQuote(defaultInput) + ", " + jsQuote(placeholder) + ");")); |
316 | if (out == null) ret; |
317 | LS buttons = listMinusSet(out.buttons, buttonsToSkip); |
318 | if (empty(buttons)) ret; |
319 | S buttonsHtml = renderButtons(buttons); |
320 | buf.append([[<div class="direct-chat-msg doted-border"> |
321 | <div class="direct-chat-buttons"> |
322 | $BUTTONS |
323 | </div> |
324 | </div> |
325 | ]].replace("$BUTTONS", htmlEncode2_nlToBr(appendNewLineIfNempty(trim(out.buttonsIntro))) + buttonsHtml)); |
326 | } |
327 | |
328 | void appendDate(StringBuilder buf, S date) { |
329 | buf.append([[ |
330 | <div class="chat-box-single-line"> |
331 | <abbr class="timestamp">DATE</abbr> |
332 | </div>]].replace("DATE", date)); |
333 | } |
334 | |
335 | S initialMessage() { |
336 | ret (S) call(thoughtBot, "initialMessage"); |
337 | } |
338 | |
339 | bool lastUserMessageWas(Conversation conv, S message) { |
340 | Msg m = last(conv.msgs); |
341 | ret m != null && m.fromUser && eq(m.text, message); |
342 | } |
343 | |
344 | S makeReply(S message) { |
345 | ret callStaticAnswerMethod(thoughtBot, message); |
346 | } |
347 | |
348 | S formatTime(long time) { |
349 | ret timeInTimeZoneWithOptionalDate_24(timeZone, time); |
350 | } |
351 | |
352 | S formatDialog(S id, L<Msg> msgs) { |
353 | new L<S> lc; |
354 | for (Msg m : msgs) |
355 | lc.add(htmlencode((m.fromUser ? "> " : "< ") + m.text)); |
356 | ret id + ul(lc); |
357 | } |
358 | |
359 | Conversation getConv(fS cookie) { |
360 | ret uniq Conversation(+cookie); |
361 | } |
362 | |
363 | S serveAuthForm(S redirect) { |
364 | ret hhtml(hhead(htitle("Authorization required")) + hbody(hfullcenter( |
365 | h3_htmlEncode(heading + " Admin") |
366 | + hpostform( |
367 | hhidden(+redirect) |
368 | + "Password: " + hpassword('pw) + "<br><br>" + hsubmit())))); |
369 | } |
370 | |
371 | S errorMsg(S msg) { |
372 | ret hhtml(hhead_title("Error") + hbody(hfullcenter(msg + "<br><br>" + ahref(jsBackLink(), "Back")))); |
373 | } |
374 | |
375 | S defaultUserName() { |
376 | ret de() ? "Sie" : "You"; |
377 | } |
378 | |
379 | bool de() { |
380 | ret isTrue(callOpt(thoughtBot, "de")); |
381 | } |
382 | |
383 | bool botOn() { true; } |
384 | |
385 | bool botAutoOpen() { |
386 | ret !isFalse(callOpt(thoughtBot, "botAutoOpen")); |
387 | } |
388 | |
389 | swappable S templateHTML() { |
390 | ret loadSnippet(templateID); // TODO: cache |
391 | } |
392 | |
393 | swappable S preprocess(S message) { ret message; } |
394 | } |
Began life as a copy of #1026250
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: | #1027629 |
Snippet name: | WebChatBot [LIVE] |
Eternal ID of this version: | #1027629/59 |
Text MD5: | fd0c6c243f0aecf02fa385248bc2a7c7 |
Transpilation MD5: | 53770747c4cea9ac616813ab4608bf17 |
Author: | stefan |
Category: | javax |
Type: | JavaX fragment (include) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2020-07-14 13:59:56 |
Source code size: | 13455 bytes / 394 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 361 / 971 |
Version history: | 58 change(s) |
Referenced in: | [show references] |