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