!7 sS chatBotID = #1026221; static long longPollMaxWait = 30*1000; // must match value in chatBotID static long clientLatency = 10*1000; // how long we allow a client to take for reconnecting srecord BotConversationID(S dbID, long convoID) { toString { ret dbID + "/" + convoID; } } concept Customer { S cookie, ip; long lastPing; bool active; BotConversationID botConversationID; bool active() { _setField(active := elapsedMS_timestamp(lastPing) <= longPollMaxWait+clientLatency); ret active; } Message lastMessage() { ret highestByField created(conceptsWhere(Message, customer := this)); } } concept Message { Customer customer; S text; bool unread; } concept IncomingMessage > Message {} concept OutgoingMessage > Message {} cmodule MultiComm { transient ConceptTable ct; start { dbIndexing(Customer, 'cookie, Customer, 'active, Message, 'customer); dm_vmBus_onMessage_q chatBot_userPolling(voidfunc(O mc, virtual Conversation conv) { Customer c = customerFromConv(mc, conv); if (c == null) ret; }); dm_vmBus_onMessage_q chatBot_messageAdded(voidfunc(O mc, virtual Conversation conv, virtual Msg msg) { Customer c = customerFromConv(mc, conv); if (c == null) ret; S text = getString text(msg); bool fromUser = getBool fromUser(msg); if (fromUser) cnew(IncomingMessage, customer := c, +text, unread := true); else cnew(OutgoingMessage, customer := c, +text); }); } visualize { ct = new ConceptTable(Customer); ct.filter = c -> c.active(); IF1 renderer = defaultConceptRendererForTable(Customer); ct.renderer = c -> { Map map = renderer.get(c); map.remove("lastPing"); transformValueInPlace str(map, "botConversationID"); map.put("Last activity", iround(toSeconds(elapsedMS_timestamp(c.lastPing)))); Message msg = c.lastMessage(); map.put("Last message", msg == null ? "" : msg.text); ret map; }; awtEvery(ct.getTable(), 5.0, r { ct.update() }); ret jhsplit( jCenteredSection("Active Customers", ct.getTable()), jCenteredSection("Messages", makeConceptsTable(Message))); } Customer customerFromConv(O mc, virtual Conversation conv) { S cookie = getString cookie(conv); if (empty(cookie)) null; Customer c = uniq(Customer, +cookie); cset(c, ip := getString ip(conv)); cset(c, lastPing := now(), botConversationID := BotConversationID(programID(mc), getLong id(conv)); ret c; } // API L activeCustomers() { ret cloneList(conceptsWhere Customer(active := true)); } }