# HG changeset patch # User Kim Alvefur # Date 1521725626 -3600 # Node ID a57ed544fece10bd125a78a396c8c99ec1aaa81c # Parent 0167a102ed35eb2e01efd8d892027016fe2198d9 mod_minimix: Experiment in account-based MUC joins diff -r 0167a102ed35 -r a57ed544fece mod_minimix/README.markdown --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_minimix/README.markdown Thu Mar 22 14:33:46 2018 +0100 @@ -0,0 +1,43 @@ +Account based MUC joining +========================= + +Normally when joing a MUC groupchat, it is each individual client that +joins. This means their presence in the group is tied to the session, +which can be short-lived or unstable, especially in the case of mobile +clients. + +This has a few problems. For one, for every message to the groupchat, a +copy is sent to each joined client. This means that at the account +level, each message would pass by once for each client that is joined, +making it difficult to archive these messages in the users personal +archive. + +A potentially better approach would be that the user account itself is +the entity that joins the groupchat. Since the account is an entity that +lives in the server itself, and the server tends to be online on a good +connection most of the time, this may improve the experience and +simplify some problems. + +This is one of the essential changes in the MIX architecture, which is +being designed to replace MUC. + +`mod_minimix` is an experiment meant to determine if things can be +improved without replacing the entire MUC standard. It works by +pretending to each client that nothing is different and that they are +joining MUCs directly, but behind the scenes, it arranges it such that +only the account itself joins each groupchat. Which sessions have joined +which groups are kept track of. Groupchat messages are then forked to +those sessions, similar to how normal chat messages work. + +## Known issues + +- You can never leave. +- You will never see anyone leave. + +## Unknown issues + +- Probably many. + +# Compatibility + +Briefly tested with Prosody trunk (as of this writing). diff -r 0167a102ed35 -r a57ed544fece mod_minimix/mod_minimix.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_minimix/mod_minimix.lua Thu Mar 22 14:33:46 2018 +0100 @@ -0,0 +1,116 @@ +-- mod_minimix +-- +-- Rewrite MUC stanzas suich that the account / bare JID joins rooms instead of clients / full JIDs +-- +local jid_split, jid_join, jid_node, jid_bare = import("util.jid", "split", "join", "node", "bare"); +local st = require "util.stanza"; + +local users = prosody.hosts[module.host].sessions; + +local joined_rooms = module:open_store("joined_rooms", "map"); -- TODO cache? +local room_state = module:open_store("joined_rooms_state", "map"); +local all_room_state = module:open_store("joined_rooms_state"); + +-- FIXME You can join but you can never leave. + +module:hook("pre-presence/full", function (event) + local origin, stanza = event.origin, event.stanza; + + local room_node, room_host, nickname = jid_split(stanza.attr.to); + local room_jid = jid_join(room_node, room_host); + local username = origin.username; + + if stanza.attr.type == nil and stanza:get_child("x", "http://jabber.org/protocol/muc") then + module:log("debug", "Joining %s as %s", room_jid, nickname); + + -- TODO Should this be kept track of before the *initial* join has been confirmed or? + if origin.joined_rooms then + origin.joined_rooms[room_jid] = nickname; + else + origin.joined_rooms = { [room_jid] = nickname }; + end + + if joined_rooms:get(username, room_jid) then + module:log("debug", "Already joined to %s as %s", room_jid, nickname); + local state = assert(all_room_state:get(username)); + for jid, stanza in pairs(state) do + if jid ~= room_jid and jid ~= stanza.attr.to then + origin.send(st.clone(st.deserialize(stanza))); + end + end + origin.send(st.deserialize(state[stanza.attr.to])); + origin.send(st.message({type="groupchat",to=origin.full_jid,from=room_jid}):tag("subject"):text(state[room_jid])); + -- Send on-join stanzas from local state, somehow + -- Maybe tell them their nickname was changed if it doesn't match the account one + return true; + end + + joined_rooms:set(username, room_jid, nickname); + + local account_join = st.clone(stanza); + account_join.attr.from = jid_join(origin.username, origin.host); + module:send(account_join); + + return true; + elseif stanza.attr.type == "unavailable" and joined_rooms:get(username, room_jid) then + origin.send(st.reply(stanza)); + return true; + end +end); + +module:hook("pre-message/bare", function (event) + local origin, stanza = event.origin, event.stanza; + local username = origin.username; + local room_jid = jid_bare(stanza.attr.to); + + module:log("info", "%s", stanza) + if joined_rooms:get(username, room_jid) then + local from_account = st.clone(stanza); + from_account.attr.from = jid_join(origin.username, origin.host); + module:log("debug", "Sending:\n%s\nInstead of:\n%s", from_account, stanza); + module:send(from_account, origin); + return true; + end +end); + +local function handle_to_bare_jid(event) + local origin, stanza = event.origin, event.stanza; + local username = jid_node(stanza.attr.to); + local room_jid = jid_bare(stanza.attr.from); + + if joined_rooms:get(username, room_jid) then + module:log("debug", "handle_to_bare_jid %q, %s", room_jid, stanza); + -- Broadcast to clients + + if stanza.name == "message" and stanza.attr.type == "groupchat" + and not stanza:get_child("body") and stanza:get_child("subject") then + room_state:set(username, room_jid, stanza:get_child_text("subject")); + elseif stanza.name == "presence" then + if stanza.attr.type == nil then + room_state:set(username, stanza.attr.from, st.preserialize(stanza)); + elseif stanza.attr.type == "unavailable" then + room_state:set(username, stanza.attr.from, nil); + end + end + + if users[username] then + module:log("debug", "%s has sessions", username); + for _, session in pairs(users[username].sessions) do + module:log("debug", "Session: %s", jid_join(session.username, session.host, session.resource)); + if session.joined_rooms and session.joined_rooms[room_jid] then + module:log("debug", "Is joined"); + local s = st.clone(stanza); + s.attr.to = session.full_jid; + session.send(s); + else + module:log("debug", "session.joined_rooms = %s", session.joined_rooms); + end + end + end + + return true; + end +end + +module:hook("presence/bare", handle_to_bare_jid); +module:hook("message/bare", handle_to_bare_jid);