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