Not logged in.  Login/Logout/Register | List snippets | | Create snippet | Upload image | Upload data

312
LINES

< > TinyBrain | #1008998 - Stefan's Chat [LIVE]

JavaX source code (desktop) [tags: use-pretranspiled] - run with: x30.jar - homepage

Libraryless. Click here for Pure Java version (10823L/75K).

!759

sbool bottomRight = false;
sS templateID = bottomRight ? #1008787 : /*#1009000*/#1009081;
sS heading = "Stefan's Chat";
sS roomName = "main"; // internal
static int maxMessages = 100;

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<S> 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<Msg> oldDialogs;
  new L<Msg> spam;
  new L<Msg> 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();
    }
  }
  
  void moveToSpam(Msg m) {
    if (msgs.contains(m)) {
      msgs.remove(m);
      spam.add(m);
      change();
    }
  }
  
  int allCount() { ret archiveSize() + l(msgs); }
  int archiveSize() { ret lengthLevel2(oldDialogs) + l(spam); }
}

p {
  dbIndexing(ByCookie, 'cookie);
  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, "/logs"))
      ret withDBLock(func -> S {
        L<Msg> msgs = sortByFieldDesc(allMsgs(), 'time);
        new L<S> l;
        for (Msg m : msgs)
          l.add(formatMsgForLog(m) + "<br>");
        ret h3_htitle("Chat Logs") + p(join(l));
      });
    
    conv = getConv(roomName);
    
    S inspam = params.get("inspam");
    S to = params.get("to");
    if (possibleGlobalID(inspam)) {
      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)*/) {
      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) {
      msgs = subList(conv.msgs, a-conv.archiveSize());
      if (empty(msgs)) {
        if (first) {
          print("Long poll starting on " + roomName + ", " + a);
          first = false;
        }
        sleep(longPollTick);
      } else {
        if (first) print("Long poll ended.");
        if (json) ret struct/*jsonEncode*/(
          litorderedmap(n := conv.allCount(),
          msgs := msgsToJSON(msgs)));
        new StringBuilder buf;
        renderMessages(buf, msgs);
        ret "<!-- " + conv.allCount() + "-->\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"))
    + tag('table, tr(
      td(youtubeEmbed("0YXhKgbo3co"))
    + td(youtubeEmbed("o0Pi7QhC61Q"), style := "padding-left: 10px")) + tr(
      td(youtubeEmbed("9ajGg9eRs_A")
    + td(youtubeEmbed("https://youtu.be/Jk8eXDuTH6U"), style := "padding-left: 10px"))
    ));

  html = html.replace("$HEADING", heading);
  html = html.replace("<!-- MSGS HERE -->", str(buf));
  html = html.replace("<!--MORE STUFF HERE-->", 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<Msg> 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);
    }
    renderMessage(buf, m.user == null ? "?" 
      : targetBlank("http://ai1.lol/1008750/?ip=" + urlencode(m.user.ipAddress), m.user.ipAddress, style := "color: white") + (nempty(m.user.cookie) ? " / " + ahref("http://ai1.lol/" + m.user.cookie, m.user.cookie, style := "color: white") : "")
        + " | " + m.globalID, formatTime(m.time), html, /*!m.fromUser*/false, m.globalID, m.user == null ? null : m.user.cookie);
  }
      
  if (empty(msgs)) ret;
  L<S> buttons = last(msgs).buttons;
  if (nempty(buttons))
    appendButtons(buf, buttons);
}

static O msgsToJSON(L<Msg> 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([[<div class="direct-chat-msg doted-border">
    <div class="direct-chat-info clearfix">
    <span class="direct-chat-name pull-left">$NAME</span>
    </div>
    <img alt="message user image" src="$IMG" class="direct-chat-img">
    <div class="direct-chat-text">
      $TEXT
    </div>
    <div class="direct-chat-info clearfix">
      <span class="direct-chat-timestamp pull-right">$TIME</span>
    </div>
  </div>
    ]].replace("$IMG", imgLink)
      .replace("$NAME", name)
      .replace("$TIME", time)
      .replace("$TEXT", text));
}

svoid appendButtons(StringBuilder buf, L<S> buttons) {
  S buttonsHtml = lines(map(buttons, func(S text) {
    hsubmit(text, name := "btn")
  }));
	buf.append([[<div class="direct-chat-msg doted-border">
    <div class="direct-chat-buttons">
      $BUTTONS
    </div>
  </div>
    ]].replace("$BUTTONS", buttonsHtml);
}

svoid appendDate(StringBuilder buf, S date) {
  buf.append([[
  <div class="chat-box-single-line">
		<abbr class="timestamp">DATE</abbr>
	</div>]].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<Msg> allMsgs() {
  new L<Msg> l;
  for (Conversation c) {
    l.addAll(c.msgs);
    for (L<Msg> 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);
  }
}

Author comment

Began life as a copy of #1008764

download  show line numbers  debug dex   

Travelled to 3 computer(s): cfunsshuasjs, onxytkatvevr, tvejysmllsmz

No comments. add comment

Snippet ID: #1008998
Snippet name: Stefan's Chat [LIVE]
Eternal ID of this version: #1008998/90
Text MD5: a29758c43c03a806951fdf205aa93feb
Transpilation MD5: c55f26824da8629cc654f6f0089279a3
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-07-27 18:35:22
Source code size: 9025 bytes / 312 lines
Pitched / IR pitched: No / No
Views / Downloads: 252 / 639
Version history: 89 change(s)
Referenced in: [show]