1 | !7 |
2 | |
3 | set flag NoNanoHTTPD. |
4 | |
5 | sbool bottomRight = false; |
6 | sS templateID = bottomRight ? #1008787 : /*#1009000*/#1009081; |
7 | sS heading = "BotCompany | Stefan's Chat"; |
8 | sS roomName = "main"; // internal |
9 | static int maxMessages = 40; |
10 | static int visitors; |
11 | static int messageLimit = stefansChat_messageLimit(); |
12 | sbool postingDisabled = false; |
13 | static SS lastPosted = new ExpiringHashMap(2000); // ip -> message |
14 | static volatile bool started; |
15 | |
16 | static int longPollTick = 100; |
17 | static int longPollMaxWait = 1000*60; |
18 | |
19 | static new L pagePostProcessors; |
20 | |
21 | concept WeirdIPs { |
22 | Set<S> ips = setInConcept(this, new TreeSet); |
23 | } |
24 | |
25 | concept Extension { |
26 | S code; |
27 | double priority; |
28 | } |
29 | |
30 | concept User { |
31 | S ipAddress, cookie; |
32 | } |
33 | |
34 | concept ByCookie { |
35 | S cookie; |
36 | S avatarID; |
37 | } |
38 | |
39 | sclass Msg { |
40 | S globalID = aGlobalIDUnlessLoading(); |
41 | long time; |
42 | User user; |
43 | S text; |
44 | L<S> buttons; |
45 | bool botMark, auth; |
46 | int nr; |
47 | |
48 | *() {} |
49 | *(S ipAddress, S cookie, S *text) { |
50 | user = uniq_sync(User, +ipAddress, +cookie); |
51 | time = now(); |
52 | } |
53 | } |
54 | |
55 | concept Conversation { |
56 | S cookie; |
57 | |
58 | // TODO: use synchro lists |
59 | new LL<Msg> oldDialogs; |
60 | new L<Msg> spam; |
61 | new L<Msg> msgs; |
62 | int counter; |
63 | |
64 | void add(Msg m) { |
65 | if (empty(oldDialogs)) oldDialogs.add(new L); |
66 | if (l(msgs) >= maxMessages) |
67 | last(oldDialogs).add(popFirst(msgs)); |
68 | msgs.add(m); |
69 | change(); |
70 | pcall { processMsgCommands(m); } |
71 | } |
72 | |
73 | // TODO |
74 | /*void correctSanity() { |
75 | if (empty(msgs)) ret; |
76 | int nr = last(msgs).nr; |
77 | int c = allCount(); |
78 | while (nr > c) { msgs.add(Msg("null", "", "filler")); ++c; } |
79 | while (nr > c) |
80 | }*/ |
81 | |
82 | void moveToSpam(Msg m, S toID) { |
83 | int i = msgs.indexOf(m); |
84 | if (i < 0) ret; |
85 | while (i < l(msgs) && neq(toID, msgs.get(i).globalID)) { |
86 | spam.add(msgs.get(i)); |
87 | msgs.remove(i); |
88 | change(); |
89 | if (empty(toID)) break; |
90 | } |
91 | moveMsgsUp(); |
92 | } |
93 | |
94 | // move msgs from archive back to main dialog |
95 | void moveMsgsUp { |
96 | int delta = maxMessages-l(msgs); |
97 | print("moveMsgsUp delta=" + delta); |
98 | if (delta <= 0) ret; |
99 | L<Msg> old = last(oldDialogs); |
100 | if (old == null) ret; |
101 | L<Msg> l = takeLast(delta, old); |
102 | print("moveMsgsUp l=" + l(l)); |
103 | if (empty(l)) ret; |
104 | msgs.addAll(0, l); |
105 | removeLast(old, delta); |
106 | change(); |
107 | } |
108 | |
109 | int allCount() { ret archiveSize() + l(msgs); } |
110 | int archiveSize() { ret lengthLevel2(oldDialogs) + l(spam); } |
111 | } |
112 | |
113 | p { |
114 | loadPage_forcedTimeout = 10000; |
115 | dbIndexing(ByCookie, 'cookie); |
116 | started = true; |
117 | |
118 | for (Conversation conv) |
119 | print("room=" + conv.cookie + ": allCount=" + conv.allCount() + " old=" + l(conv.oldDialogs)); |
120 | |
121 | L<Conversation> l = findConcepts(Conversation, cookie := roomName); |
122 | if (l(l) > 1) { |
123 | print("FIXING"); |
124 | deleteConcept(smallestByMethod(l, 'allCount)); |
125 | } |
126 | |
127 | for (Extension e : sortByField('priority, list(Extension))) |
128 | pcall { evalJava(e.code); } |
129 | } |
130 | |
131 | html { |
132 | _registerThread(); |
133 | while (!started) sleep(100); |
134 | |
135 | try { |
136 | S _vis = registerVisitor(); |
137 | int visitorsToday = parseFirstInt(_vis); |
138 | main.visitors = visitorsToday; |
139 | //fS cookie = cookieSent(); |
140 | |
141 | if (eq(uri, "/thoughts")) try { |
142 | loadPage_extraHeaders.set(litmap("X-Forwarded-For", clientIP())); |
143 | ret hrefresh(10) + loadPage(smartBotURL() + "/thoughts" |
144 | + (webAuthed() ? "?_pretendAuthed=1" : "")); |
145 | } catch { |
146 | ret hrefresh(10) + hGoogleFontOswald() + hfullcenter("Bot loading..."); |
147 | } |
148 | |
149 | new Matches mm; |
150 | Conversation conv; |
151 | { |
152 | lock dbLock(); |
153 | |
154 | if (eq(uri, "/testauth")) ret "Authed: " + yn(webAuthed()); |
155 | |
156 | if (swic(uri, "/setSmartBotURL/", mm)) { |
157 | if (!webAuthed()) ret "Not authed"; |
158 | setSmartBotURL(urldecode(mm.rest())); |
159 | ret "OK"; |
160 | } |
161 | |
162 | if (eq(uri, "/stats")) |
163 | ret "Threads: " + ul_htmlEncode(getThreadNames(registeredThreads())); |
164 | |
165 | if (eq(uri, "/spam")) |
166 | ret h3AndTitle("Spam Log") + ul(reversedList(map htmlencode(scanLog("spam.log")))); |
167 | |
168 | if (eq(uri, "/logs")) |
169 | ret withDBLock(func -> S { |
170 | int step = 100, n = toInt(params.get("n")); |
171 | L<Msg> msgs = reversed(allMsgs()); // TODO: optimize |
172 | new L<S> l; |
173 | int count = l(msgs); |
174 | msgs = subList(msgs, n, n+step); |
175 | for (Msg m : msgs) |
176 | l.add(formatMsgForLog(m) + "<br>"); |
177 | ret h3_htitle("Chat Logs") |
178 | + pageNav2("/logs", count, n, step, 'n) |
179 | + p(join(reversed(l))); |
180 | }); |
181 | |
182 | conv = getConv(roomName); |
183 | |
184 | if (eq(uri, "/sanity")) { |
185 | if (conv == null) ret "No conv"; |
186 | Msg m = last(conv.msgs); |
187 | int c = conv.allCount(); |
188 | if (m == null) ret "Msgs: " + l(conv.msgs) + " vs " + c; |
189 | ret m.nr + " vs " + c; |
190 | } |
191 | |
192 | S weirdip = params.get("weirdip"); |
193 | if (nempty(weirdip) && webAuthed()) { |
194 | uniq(WeirdIPs).ips.add(weirdip); |
195 | ret "OK"; |
196 | } |
197 | |
198 | S inspam = params.get("inspam"); |
199 | S to = params.get("to"); |
200 | if (possibleGlobalID(inspam) && webAuthed()) { |
201 | Msg m = findWhere(conv.msgs, globalID := inspam); |
202 | if (m == null) ret "Msg not found"; |
203 | conv.moveToSpam(m, to); |
204 | ret "OK, moved to spam"; |
205 | } |
206 | |
207 | S message = trim(params.get("btn")); |
208 | if (empty(message)) message = trim(params.get("message")); |
209 | bool botMark = nempty(params.get("botmark")); |
210 | //print("Have " + l(conv.msgs) + " msgs in conversation " + conv.cookie); |
211 | |
212 | if (match("clear", message)) { |
213 | print("Clearing."); |
214 | conv.oldDialogs.add(conv.msgs); |
215 | cset(conv, msgs := new L); |
216 | conv.change(); |
217 | message = null; |
218 | } |
219 | |
220 | if (nempty(message) /*&& !lastUserMessageWas(conv, message)*/) |
221 | postMessage(conv, message, botMark); |
222 | } // synchronized block |
223 | |
224 | if (eq(uri, "/msg")) ret "OK"; |
225 | |
226 | if (eq(uri, "/n")) ret str(conv.allCount()); |
227 | |
228 | if (eq(uri, "/lastmsg")) ret struct(msgsToJSON(takeLast(1, conv.msgs))); |
229 | |
230 | if (eq(uri, "/msgs-from-to")) { |
231 | int as = conv.archiveSize(); |
232 | int a = parseInt(params.get('a))-as; |
233 | int b = parseInt(params.get('b))-as; |
234 | ret serveText(struct(msgsToJSON(subList(conv.msgs, a, b)))); |
235 | } |
236 | |
237 | if (eq(uri, "/archive-from-to")) { |
238 | L<Msg> l = allMsgs(); |
239 | int a = parseInt(params.get('a))-l(conv.spam); |
240 | int b = parseInt(params.get('b))-l(conv.spam); |
241 | ret serveText(struct(msgsToJSON(subList(l, a, b)))); |
242 | } |
243 | |
244 | if (eq(uri, "/incremental")) { |
245 | if (uniq(WeirdIPs).ips.contains(clientIP())) { |
246 | print("Weird IP."); |
247 | sleepSeconds(10); |
248 | } |
249 | |
250 | int a = parseIntOpt(params.get("a")); |
251 | int vis = parseIntOpt(params.get("v")); |
252 | bool json = nempty(params.get('json)); // Funny thing is, JSON isn't even JSON. It's struct() |
253 | |
254 | long start = sysNow(); |
255 | L msgs; |
256 | bool first = true; |
257 | while (licensed() && sysNow() < start+longPollMaxWait) { |
258 | int as = conv.archiveSize(); |
259 | msgs = cloneSubList(conv.msgs, a-as); |
260 | //int visitors = main.visitors+random(2); |
261 | if (empty(msgs) && (vis == 0 || vis == visitors)) { |
262 | if (first) { |
263 | print("Long poll starting on " + roomName + ", " + a); |
264 | first = false; |
265 | } |
266 | sleep(longPollTick); |
267 | } else { |
268 | int ac = conv.allCount(); |
269 | if (first) print("Long poll ended. a=" + a + ", as=" + as + ", msgs=" + l(msgs) + ", ac=" + ac); |
270 | if (json) ret struct/*jsonEncode*/( |
271 | litorderedmap(n := conv.allCount(), |
272 | msgs := msgsToJSON(msgs))); |
273 | new StringBuilder buf; |
274 | renderMessages(buf, msgs); |
275 | ret "<!-- " + ac + "-->\n" |
276 | + (vis == visitors ? "" : "<!-- vis " + visitors + " -->") |
277 | + buf; |
278 | } |
279 | } |
280 | ret ""; |
281 | } |
282 | |
283 | lock dbLock(); |
284 | |
285 | S html = loadSnippet_simpleCache(templateID); |
286 | new StringBuilder buf; |
287 | |
288 | print("Have " + n(conv.msgs, "msg")); |
289 | renderMessages(buf, conv.msgs); |
290 | |
291 | html = html.replace("#RELATIVESTYLES#", loadSnippet_simpleCache(#1010888)); |
292 | html = html.replace("#N#", str(conv.allCount())); |
293 | html = html.replace("#VISITORS#", str(visitorsToday)); |
294 | html = html.replace("#INCREMENTALURL#", relativeRawBotLink(programID(), "incremental"); |
295 | html = html.replace("#MSGURL#", rawBotLink_rel(programID(), "msg?message=")); |
296 | |
297 | // debug mode |
298 | html = html.replace("var showActions = false;", "var showActions = true;"); |
299 | |
300 | int yw = 150, yh = 100; |
301 | |
302 | // MAKE MORE |
303 | |
304 | S more = |
305 | h3("We make custom chat bots.") |
306 | + p(b(ahref("http://tinybrain.de/x30.jar", "The Software.")) |
307 | + " (Windows/Linux/Mac OS.) Just double-click to run. " + targetBlank("http://java.com/", "Install Java if needed.") + "<br>" |
308 | + ahref("mailto:info@ai1.lol", "Mail.") + " " |
309 | + "Visitors today: " + span(visitorsToday, id := "visnum") + "." + "<br>" |
310 | + "Feel free to talk to " + b("Smart Bot") + " in this chat!" + " - " + targetBlank("http://tinybrain.de/1010745", "Smart Bot's Source Code.") + " " |
311 | + targetBlank("http://javax.tinybrain.de/", "JavaX.") + "<br>" |
312 | + htmlencode(unicode_rightPointingTriangle()) + " " + targetBlank("http://web-woody-lab.de/", "German smalltalk bot.") + " " |
313 | + htmlencode(unicode_rightPointingTriangle()) + " " + targetBlank("http://ai1.lol/1008316/raw", "Simple English product bot.")) |
314 | + tag('table, tr( |
315 | td(youtubeEmbed("xeguVLToOwU", yw, yh)) |
316 | + td(youtubeEmbed("z51GRYMSeaI", yw, yh), style := "padding-left: 10px") |
317 | + td(youtubeEmbed("jRb7UpwvJV8", yw, yh), style := "padding-left: 10px") |
318 | + td(youtubeEmbed("gOTPwVVyY-4", yw, yh), style := "padding-left: 10px") |
319 | )) |
320 | + h3("Bot's Thoughts " + small("(Type " + tt("!word ...") + " to change)")) |
321 | + tag('iframe, "", src := relativeRawBotLink(programID(), "thoughts"), width:= 700, height := 300) |
322 | ; |
323 | |
324 | html = html.replace("$HEADING", htmlencode(heading) + " | " + targetBlank(selfLink("logs"), "Log")); |
325 | html = html.replace("<!-- MSGS HERE -->", str(buf)); |
326 | html = html.replace("<!--MORE STUFF HERE-->", more); |
327 | html = hreplaceTitle(html, heading); |
328 | html = hmobilefix(html); |
329 | for (O f : pagePostProcessors) pcall { html = or((S) callF(f, html), html); } |
330 | ret html; |
331 | } finally { |
332 | _unregisterThread(); |
333 | } |
334 | } |
335 | |
336 | svoid renderMessages(StringBuilder buf, L<Msg> msgs) { |
337 | for (Msg m : msgs) { |
338 | new Matches mm; |
339 | S html; |
340 | if (startsWith(m.text, "[IMAGE] ", mm)) { |
341 | // IMAGE POST |
342 | S url = trim(mm.get(0)); |
343 | html = targetBlank(url, himg(url, width := 200, title := "User-submitted image", style := "display: block; margin-left: auto; margin-right: auto")); |
344 | } else { |
345 | // TEXT POST |
346 | //dynamize_linkParams.set(new O[] { target := "_blank" }); |
347 | //html = dynamize(m.text); |
348 | html = html_linkURLs_targetBlank(htmlEncode_nlToBr(m.text)); |
349 | } |
350 | S name = "?"; |
351 | if (m.user != null) |
352 | name = targetBlank("http://ai1.lol/1008750/?ip=" + urlencode(m.user.ipAddress), m.user.ipAddress, style := "color: white", title := "User's IP Address") + (nempty(m.user.cookie) ? " / " + targetBlank("http://ai1.lol/" + m.user.cookie, m.user.cookie, style := "color: white", title := "User's Cookie") : "") |
353 | + " | " + targetBlank(/*"http://ai1.lol/" + m.globalID*/encyclopediaLink("Chat line " + m.nr), |
354 | /*m.globalID*/m.nr, style := "color: white", title := "Message ID"); |
355 | if (webAuthed()) name += " " + targetBlank(botLink(programID()) + "?inspam=" + m.globalID, "[spam]"); |
356 | renderMessage(buf, name, formatTime(m.time), html, m.globalID, m.user == null ? null : m.user.cookie, m.botMark, m.nr); |
357 | } |
358 | |
359 | if (empty(msgs)) ret; |
360 | L<S> buttons = last(msgs).buttons; |
361 | if (nempty(buttons)) |
362 | appendButtons(buf, buttons); |
363 | } |
364 | |
365 | static O msgsToJSON(L<Msg> msgs) { |
366 | ret map(msgs, func(Msg m) { litorderedmap( |
367 | time := m.time, |
368 | text := m.text, |
369 | ip := m.user.ipAddress, |
370 | cookie := m.user.cookie, |
371 | buttons := m.buttons, |
372 | auth := trueOrNull(m.auth), |
373 | botMark := trueOrNull(m.botMark), |
374 | nr := m.nr, |
375 | globalID := m.globalID |
376 | ) }); |
377 | } |
378 | |
379 | svoid renderMessage(StringBuilder buf, S name, S time, S html, S id, S cookie, bool botMark, int nr) { |
380 | ByCookie bc = findConcept(ByCookie, +cookie); |
381 | S imgID = botMark ? #1008323 : #1008359; |
382 | if (nempty(cookie) && !botMark /*XXX*/ && bc != null) imgID = or(bc.avatarID, imgID); |
383 | S imgLink = snippetImgLink(imgID); |
384 | //if (nempty(id)) buf.append(hcomment("Msg ID: " + id)); |
385 | buf.append([[<div class="direct-chat-msg doted-border" id="msg_#ID#"> |
386 | <div class="direct-chat-info clearfix"> |
387 | <span class="direct-chat-name pull-left">$NAME</span> |
388 | </div> |
389 | <img alt="message user image" src="$IMG" class="direct-chat-img"> |
390 | <div class="direct-chat-text" $STYLE> |
391 | $TEXT |
392 | </div> |
393 | <div class="direct-chat-info clearfix"> |
394 | <span class="direct-chat-timestamp pull-right">$TIME</span> |
395 | </div> |
396 | </div> |
397 | ]].replace("$IMG", imgLink) |
398 | .replace("$NAME", name) |
399 | .replace("$TIME", time) |
400 | .replace("#ID#", id) |
401 | .replace("$STYLE", botMark |
402 | //? "style='text-align: right'" |
403 | //? "style='font-style: italic'" |
404 | ? "style='font-family: Pacifico'" |
405 | : "") |
406 | .replace("$TEXT", html)); |
407 | } |
408 | |
409 | svoid appendButtons(StringBuilder buf, L<S> buttons) { |
410 | S buttonsHtml = lines(map(buttons, func(S text) { |
411 | hsubmit(text, name := "btn") |
412 | })); |
413 | buf.append([[<div class="direct-chat-msg doted-border"> |
414 | <div class="direct-chat-buttons"> |
415 | $BUTTONS |
416 | </div> |
417 | </div> |
418 | ]].replace("$BUTTONS", buttonsHtml); |
419 | } |
420 | |
421 | svoid appendDate(StringBuilder buf, S date) { |
422 | buf.append([[ |
423 | <div class="chat-box-single-line"> |
424 | <abbr class="timestamp">DATE</abbr> |
425 | </div>]].replace("DATE", date)); |
426 | } |
427 | |
428 | sS formatTime(long time) { |
429 | ret formatGMTWithOptionalDate_24(time); |
430 | } |
431 | |
432 | sS formatMsgForLog(Msg m) { |
433 | ret htmlencode(m.nr + " " + formatDateAndTime(m.time)) + " - " + htmlencode(m.user.ipAddress + ": " + m.text); |
434 | } |
435 | |
436 | static Conversation getConv(fS cookie) { |
437 | ret uniq_sync(Conversation, +cookie); |
438 | } |
439 | |
440 | static L<Msg> allMsgs() { |
441 | new L<Msg> l; |
442 | for (Conversation c) { |
443 | for (L<Msg> msgs : c.oldDialogs) l.addAll(msgs); |
444 | l.addAll(c.msgs); |
445 | } |
446 | ret l; |
447 | } |
448 | |
449 | svoid processMsgCommands(Msg msg) { |
450 | new Matches m; |
451 | if (swic(msg.text, "avatar ", m)) { |
452 | S avatarID = fsI(trim($1)); |
453 | BufferedImage img = loadImage2(avatarID); |
454 | if (img.getWidth() <= 400 && img.getHeight() <= 400) |
455 | cset(uniq(ByCookie, cookie := msg.user.cookie), +avatarID); |
456 | else fail("Avatar too big: " + avatarID); |
457 | } |
458 | } |
459 | |
460 | svoid postMessage(Conversation conv, S message, bool botMark) { |
461 | message = shorten(message, messageLimit, " [...]"); |
462 | S ip = clientIP(); |
463 | print("Have message from " + ip + " at " + gmtWithSeconds() + " in thread " + currentThread() + ": " + quote(shorten(message, 80))); |
464 | |
465 | if (startsWith(ip, "66.249.65.")) |
466 | if (matchOneOf(message, "web * is invalid", "web * is correct")) |
467 | ret; |
468 | |
469 | // anti hammer mechanism |
470 | S key = ip + " " + botMark; |
471 | if (eq(lastPosted.get(key), message)) { |
472 | print("Skipping same."); |
473 | ret; |
474 | } |
475 | |
476 | lastPosted.put(key, message); |
477 | |
478 | if (postingDisabled) ret; |
479 | |
480 | if (superSimpleSpamTester(message)) { |
481 | print(logStructureWithDate("spam.log", "Ignoring spam message: " + quote(message))); |
482 | ret; |
483 | } |
484 | |
485 | // ADD NOT SPAM MESSAGE |
486 | Msg msg = new Msg(clientIP(), cookieConcept(), message); |
487 | msg.botMark = botMark; |
488 | msg.auth = webAuthed(); |
489 | //msg.nr = toInt(getOpt(last(conv.msgs), 'nr))+1; |
490 | if (conv.counter == 0) cset(conv, counter := conv.allCount()); |
491 | cset(conv, counter := conv.counter+1); |
492 | msg.nr = conv.counter; |
493 | conv.add(msg); |
494 | print("Have " + l(conv.msgs) + " msgs in conversation " + conv.cookie + " after add"); |
495 | } |
Began life as a copy of #1008998
download show line numbers debug dex old transpilations
Travelled to 13 computer(s): aoiabmzegqzx, bhatertpkbcr, cbybwowwnfue, cfunsshuasjs, gwrvuhgaqvyk, ishqpsrjomds, lpdgvwnxivlt, mqqgnosmbjvj, pyentgdyhuwx, pzhvpgtvlbxg, tslmcundralx, tvejysmllsmz, vouqrxazstgt
No comments. add comment
Snippet ID: | #1012236 |
Snippet name: | Stefan's Chat - pre parsing display |
Eternal ID of this version: | #1012236/1 |
Text MD5: | edb7f1ba11c2585fb5788906fe0badfb |
Author: | stefan |
Category: | javax / a.i. |
Type: | JavaX source code (desktop) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2017-11-26 11:27:19 |
Source code size: | 15876 bytes / 495 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 591 / 744 |
Referenced in: | [show references] |