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: | 580 / 912 |
| Version history: | 99 change(s) |
| Referenced in: | [show references] |