!759 sbool bottomRight = false; sS templateID = bottomRight ? #1008787 : /*#1009000*/#1009081; sS heading = "BotCompany | Stefan's Chat"; sS roomName = "main"; // internal static int maxMessages = 40; static int longPollTick = 100; static int longPollMaxWait = 1000*60; static new L pagePostProcessors; concept Extension { S code; double priority; } concept User { S ipAddress, cookie; } concept ByCookie { S cookie; S avatarID; } sclass Msg { S globalID = aGlobalIDUnlessLoading(); long time; User user; S text; L buttons; *() {} *(S ipAddress, S cookie, S *text) { user = uniq_sync(User, +ipAddress, +cookie); time = now(); } } concept Conversation { S cookie; // TODO: use synchro lists new LL oldDialogs; new L spam; new L msgs; void add(Msg m) { if (empty(oldDialogs)) oldDialogs.add(new L); if (l(msgs) >= maxMessages) last(oldDialogs).add(popFirst(msgs)); msgs.add(m); change(); pcall { processMsgCommands(m); } } void moveToSpam(Msg m, S toID) { int i = msgs.indexOf(m); if (i < 0) ret; while (i < l(msgs) && neq(toID, msgs.get(i).globalID)) { spam.add(msgs.get(i)); msgs.remove(i); change(); if (empty(toID)) break; } moveMsgsUp(); } // move msgs from archive back to main dialog void moveMsgsUp { int delta = maxMessages-l(msgs); print("moveMsgsUp delta=" + delta); if (delta <= 0) ret; L old = last(oldDialogs); if (old == null) ret; L l = takeLast(delta, old); print("moveMsgsUp l=" + l(l)); if (empty(l)) ret; msgs.addAll(0, l); removeLast(old, delta); change(); } int allCount() { ret archiveSize() + l(msgs); } int archiveSize() { ret lengthLevel2(oldDialogs) + l(spam); } } p { dbIndexing(ByCookie, 'cookie); //db(); for (Extension e : sortByField('priority, list(Extension))) pcall { evalJava(e.code); } } /*synchronized static void sayAsync(S session, S user, S cookie, S text) { Conversation conv = getConv(session); conv.add(new Msg(user, cookie, text)); print("sayAsync " + session + ", new size=" + conv.allCount()); }*/ html { _registerThread(); registerVisitor(); //fS cookie = cookieSent(); try { Conversation conv; { lock dbLock(); if (eq(uri, "/stats")) ret "Threads: " + ul_htmlEncode(getThreadNames(registeredThreads())); if (eq(uri, "/spam")) ret h3AndTitle("Spam Log") + ul(reversedList(map htmlencode(scanLog("spam.log")))); if (eq(uri, "/logs")) ret withDBLock(func -> S { L msgs = sortByFieldDesc(allMsgs(), 'time); new L l; for (Msg m : msgs) l.add(formatMsgForLog(m) + "
"); ret h3_htitle("Chat Logs") + p(join(l)); }); conv = getConv(roomName); S inspam = params.get("inspam"); S to = params.get("to"); if (possibleGlobalID(inspam) && webAuthed()) { Msg m = findWhere(conv.msgs, globalID := inspam); if (m == null) ret "Msg not found"; conv.moveToSpam(m, to); ret "OK, moved to spam"; } S message = trim(params.get("btn")); if (empty(message)) message = trim(params.get("message")); //print("Have " + l(conv.msgs) + " msgs in conversation " + conv.cookie); if (match("clear", message)) { print("Clearing."); conv.oldDialogs.add(conv.msgs); cset(conv, msgs := new L); conv.change(); message = null; } if (nempty(message) /*&& !lastUserMessageWas(conv, message)*/) { if (isSpam(message)) print(logStructureWithDate("spam.log", "Ignoring spam message: " + quote(message))); else { conv.add(new Msg(clientIP(), cookieConcept(), message)); print("Have " + l(conv.msgs) + " msgs in conversation " + conv.cookie + " after add"); } } } // synchronized block if (eq(uri, "/msg")) ret "OK"; if (eq(uri, "/n")) ret str(conv.allCount()); if (eq(uri, "/lastmsg")) ret struct(msgsToJSON(takeLast(1, conv.msgs))); if (eq(uri, "/incremental")) { int a = parseInt(params.get("a")); bool json = nempty(params.get('json)); // Funny thing is, JSON isn't even JSON. It's struct() long start = sysNow(); L msgs; bool first = true; while (licensed() && sysNow() < start+longPollMaxWait) { int as = conv.archiveSize(); msgs = subList(conv.msgs, a-as); if (empty(msgs)) { if (first) { print("Long poll starting on " + roomName + ", " + a); first = false; } sleep(longPollTick); } else { int ac = conv.allCount(); if (first) print("Long poll ended. a=" + a + ", as=" + as + ", msgs=" + l(msgs) + ", ac=" + ac); if (json) ret struct/*jsonEncode*/( litorderedmap(n := conv.allCount(), msgs := msgsToJSON(msgs))); new StringBuilder buf; renderMessages(buf, msgs); ret "\n" + buf; } } ret ""; } lock dbLock(); S html = loadSnippet(templateID); // TODO: cache new StringBuilder buf; renderMessages(buf, conv.msgs); html = html.replace("#N#", str(conv.allCount())); html = html.replace("#INCREMENTALURL#", rawBotLink(programID(), "incremental?a="); html = html.replace("#MSGURL#", rawBotLink(programID(), "msg?message=")); S more = p(targetBlank(selfLink("logs"), "Full Chat Logs")) + h3("Download!") + p(b(ahref("http://tinybrain.de/x30.jar", "The Software.")) + " (Windows/Linux/Mac OS.) Just double-click to run. " + targetBlank("http://java.com/", "Install Java if needed.") + " " + ahref("mailto:info@ai1.lol", "Mail.")) + tag('table, tr( td(youtubeEmbed("dVonuKKGL-M")) + td(youtubeEmbed("452YhuPh0gM"), style := "padding-left: 10px")) + tr( td(youtubeEmbed("XW9v3q5N_Bk") + td(youtubeEmbed("bgq7JaFOp2w"), style := "padding-left: 10px")) )); html = html.replace("$HEADING", heading); html = html.replace("", str(buf)); html = html.replace("", more); html = hreplaceTitle(html, heading); html = hmobilefix(html); for (O f : pagePostProcessors) pcall { html = or((S) callF(f, html), html); } ret html; } finally { _unregisterThread(); } } svoid renderMessages(StringBuilder buf, L msgs) { for (Msg m : msgs) { new Matches mm; S html; if (startsWith(m.text, "[IMAGE] ", mm)) { S url = trim(mm.get(0)); html = targetBlank(url, himg(url, width := 200, title := "User-submitted image", style := "display: block; margin-left: auto; margin-right: auto")); } else { dynamize_linkParams.set(new O[] { target := "_blank" }); html = dynamize(m.text); } S name = "?"; if (m.user != null) 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) ? " / " + ahref("http://ai1.lol/" + m.user.cookie, m.user.cookie, style := "color: white", title := "User's Cookie") : "") + " | " + span(m.globalID, title := "Message ID"); if (webAuthed()) name += " " + targetBlank(botLink(programID()) + "?inspam=" + m.globalID, "[spam]"); renderMessage(buf, name, formatTime(m.time), html, /*!m.fromUser*/false, m.globalID, m.user == null ? null : m.user.cookie); } if (empty(msgs)) ret; L buttons = last(msgs).buttons; if (nempty(buttons)) appendButtons(buf, buttons); } static O msgsToJSON(L msgs) { ret map(msgs, func(Msg m) { litorderedmap( time := m.time, text := m.text, ip := m.user.ipAddress, cookie := m.user.cookie, buttons := m.buttons) }); } svoid renderMessage(StringBuilder buf, S name, S time, S text, bool bot, S id, S cookie) { ByCookie bc = findConcept(ByCookie, +cookie); S imgID = #1008359; if (bc != null) imgID = bc.avatarID; S imgLink = snippetImgLink(imgID); if (nempty(id)) buf.append(hcomment("Msg ID: " + id)); buf.append([[
$NAME
message user image
$TEXT
$TIME
]].replace("$IMG", imgLink) .replace("$NAME", name) .replace("$TIME", time) .replace("$TEXT", text)); } svoid appendButtons(StringBuilder buf, L buttons) { S buttonsHtml = lines(map(buttons, func(S text) { hsubmit(text, name := "btn") })); buf.append([[
$BUTTONS
]].replace("$BUTTONS", buttonsHtml); } svoid appendDate(StringBuilder buf, S date) { buf.append([[
DATE
]].replace("DATE", date)); } sS formatTime(long time) { ret formatGMTWithOptionalDate_24(time); } sS formatMsgForLog(Msg m) { ret htmlencode(formatDateAndTime(m.time)) + " - " + htmlencode(m.user.ipAddress + ": " + m.text); } static Conversation getConv(fS cookie) { ret uniq_sync(Conversation, +cookie); } static L allMsgs() { new L l; for (Conversation c) { l.addAll(c.msgs); for (L msgs : c.oldDialogs) l.addAll(msgs); } ret l; } svoid processMsgCommands(Msg msg) { new Matches m; if (swic(msg.text, "avatar ", m)) { S avatarID = fsI(trim($1)); BufferedImage img = loadImage2(avatarID); if (img.getWidth() <= 400 && img.getHeight() <= 400) cset(uniq(ByCookie, cookie := msg.user.cookie), +avatarID); else fail("Avatar too big: " + avatarID); } } // super-simple spam tester sbool isSpam(S text) { ret text.contains("://") && cicOneOf(text, "Viagra", "Zoloft", "Cialis", "azithromycin", "prednisolone"); }