From 881085e767e7faf5a6a61e5949ecaa8d5f0d0b43 Mon Sep 17 00:00:00 2001 From: Alexander PapaTutuWawa Date: Tue, 27 Oct 2020 18:05:41 +0100 Subject: [PATCH] mix: Initial commit --- mod_mix/mod_mix.lua | 428 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 428 insertions(+) create mode 100644 mod_mix/mod_mix.lua diff --git a/mod_mix/mod_mix.lua b/mod_mix/mod_mix.lua new file mode 100644 index 0000000..1487848 --- /dev/null +++ b/mod_mix/mod_mix.lua @@ -0,0 +1,428 @@ +local host = module:get_host(); +if module:get_host_type() ~= "component" then + error("MIX should be loaded as a component", 0); +end + +local st = require("util.stanza"); +local jid = require("util.jid"); +local uuid = require("util.uuid"); +local id = require("util.id"); +local datetime = require("util.datetime"); +local time = require("util.time"); +local serialization = require("util.serialization"); +local pep = module:depends("pep"); + +local persistent_channels = module:open_store("mix_channels", "keyval"); +local persistent_channel_data = module:open_store("mix_data", "keyval"); + +-- Default values +local default_channel_description = module:get_option("default_description", "A MIX channel for chatting"); +local default_channel_name = module:get_option("default_name", "MIX channel"); + +module:depends("disco"); +module:add_identity("conference", "mix", module:get_option("name", "Prosody MIX service"); +module:add_feature("http://jabber.org/protocol/disco#info"); +module:add_feature("urn:xmpp:mix:core:1"); + +Participant = {}; +Participant.__index = Participant; +function Participant:new(jid, nick) + return setmetatable({ + jid = jid, + nick = nick, + }, Participant); +end + +Channel = {} +Channel.__index = Channel; +function Channel:new(jid, name, description, participants, subscriptions, spid, contacts, adhoc) + return setmetatable({ + jid = jid, + name = name, + description = description, + participants = participants, + subscriptions = subscriptions, + spid = spid, + contacts = contacts, + adhoc = adhoc, + }, Channel); +end +function Channel:from(config) + return setmetatable(config, Channel); +end + +function Channel:get_spid(jid) + return self.spid[jid]; +end + +function Channel:debug_print() + module:log("debug", "Channel %s (%s)", self.jid, self.name); + module:log("debug", "'%s'", self.description); + for _, p in pairs(self.participants) do + module:log("debug", "=> %s (%s)", p.jid, p.nick); + end + + module:log("debug", "Contacts:"); + for _, c in pairs(self.contacts) do + module:log("debug", "=> %s", c); + end + + if self.subscriptions then + module:log("debug", "Subscriptions:"); + for user, subs in pairs(self.subscriptions) do + module:log("debug", "[%s]", user); + for _, sub in pairs(self.subscriptions[user]) do + module:log("debug", "=> %s", sub); + end + end + end +end + +local channels = {}; + +function get_channel(jid) + for i, channel in pairs(channels) do + if channel.jid == jid then + return i, channel + end + end +end + +function publish_participant(service, spid, participant) + service:publish("urn:xmpp:mix:nodes:participants", + true, + spid, + st.stanza("item", { id = spid, xmlns = "http://jabber.org/protocol/pubsub" }) + :tag("participant", { xmlns = "urn:xmpp:mix:core:1" }) + :tag("nick"):text(participant["nick"]):up() + :tag("jid"):text(participant["jid"])); +end + +function save_channels() + module:log("debug", "Saving channel list..."); + local channel_list = {}; + for _, channel in pairs(channels) do + table.insert(channel_list, channel.jid); + + persistent_channel_data:set(channel.jid, channel); + end + + persistent_channels:set("channels", channel_list); + module:log("debug", "Saving channel list done."); +end + +function Channel:save_state() + -- Saving the entire state everything one channel seems stupid, + -- so we just save the changed channel + module:log("debug", "Saving state of channel %s...", self.jid); + persistent_channel_data:set(self.jid, self); + module:log("debug", "Saving state done.", self.jid); +end + +function module.load() + module:log("info", "Loading MIX channels..."); + + local channel_list = persistent_channels:get("channels"); + if channel_list then + for _, channel in pairs(channel_list) do + table.insert(channels, Channel:from(persistent_channel_data:get(channel))); + module:log("debug", "MIX channel %s loaded", channel); + end + else + module:log("debug", "No MIX channels found."); + end + module:log("debug", "Loading MIX channels done."); +end + +module:hook("host-disco-items", function(event) + module:log("debug", "host-disco-items called"); + local reply = event.reply; + for _, channel in pairs(channels) do + -- Adhoc channels are supposed to be invisible + if not channel.adhoc then + reply:tag("item", { jid = channel.jid }):up(); + end + end +end); + +module:hook("iq/bare/http://jabber.org/protocol/disco#items:query", function(event) + module:log("debug", "IQ-GET disco#items"); + + local origin, stanza = event.origin, event.stanza; + local _, channel = get_channel(staza.attr.to); + if not channel then + -- TODO: Send error message + end + local reply = st.reply(stanza):tag("query", { xmlns = "http://jabber.org/protocol/disco#items", node = "mix" }); + for _, node in pairs({"urn:xmpp:mix:nodes:messages", "urn:xmpp:mix:nodes:participants", "urn:xmpp:mix:nodes:info"}) do + reply:tag("item", { jid = channel.jid, node = node }):up(); + end + + origin.send(reply); + return true; +end); + +module:hook("iq/bare/http://jabber.org/protocol/disco#info:query", function(event) + module:log("debug", "IQ-GET disco#info"); + + local origin, stanza = event.origin, event.stanza; + local _, channel = get_channel(stanza.attr.to); + if not channel then + -- TODO: Send error message + end + local reply = st.reply(stanza):tag("query", { xmlns = "http://jabber.org/protocol/disco#info" }); + reply:tag("identity", { category = "conference", name = channel.name, type = "mix" }):up(); + reply:tag("feature", { var = "urn:xmpp:mix:core:1" }):up(); + -- TODO: Do something here + reply:tag("feature", { var = "urn:xmpp:mix:core:1#create-channel" }):up(); + reply:tag("feature", { var = "http://jabber.org/protocol/disco#info" }):up(); + origin.send(reply); + return true; +end); + +function find_participant(table, jid) + for i, v in pairs(table) do + if v.jid == jid then + return i + end + end + + return -1 +end + +module:hook("iq-set/bare/urn:xmpp:mix:core:1:leave", function(event) + module:log("debug", "MIX leave received"); + local origin, stanza = event.origin, event.stanza; + local from = jid.bare(stanza.attr.from); + local i, channel = get_channel(stanza.attr.to); + if not channel then + -- TODO: Return error + end + + local participant_index = find_participant(channel.participants, from); + if participant_index == -1 then + channel:debug_print(); + module:log("debug", "%s is not a participant in %s", from, channel.jid); + return; + end + + -- Remove the user as a participant by... + -- Unsubscribing + local srv = pep.get_pep_service(channel.jid); + for _, node in pairs(channel.subscriptions[from]) do + srv:remove_subscription(node, true, from); + module:log("debug", "Unsubscribed %s from %s on %s", from, node, channel.jid); + end + channels[i].subscriptions[from] = nil; + -- Retracting the participation + srv:retract("urn:xmpp:mix:nodes:participants", true, channel.spid[from], true); + -- Removing the user + table.remove(channels[i].participants, participant_index); + channel:save_state(); + + origin.send(st.reply(stanza):tag("leave", { xmlns = "urn:xmpp:mix:core:1" })); + return true; +end); + +module:hook("iq-set/bare/urn:xmpp:mix:core:1:join", function(event) + module:log("debug", "MIX join received"); + + local origin, stanza = event.origin, event.stanza; + local i, channel = get_channel(stanza.attr.to); + if not channel then + -- TODO: Return error + return + end + local from = jid.bare(stanza.attr.from); + local spid = channel.spid[from] or uuid.generate(); -- Stable Participant ID + local reply = st.reply(stanza) + :tag("join", { xmlns = "urn:xmpp:mix:core:1", id = spid }); + local srv = pep.get_pep_service(jid.node(stanza.attr.to)); + local join = stanza:get_child("join", "urn:xmpp:mix:core:1"); + local nick = join:get_child("nick"); + module:log("debug", "User joining as nick %s", nick:get_text()); + + local nodes = {}; + -- TODO: The spec says that an error sould be returned when no nodes can be subscribed to + for subscribe in join:childtags("subscribe") do + module:log("debug", "Subscribing user to node %s", subscribe.attr.node); + local ok, err = srv:add_subscription(subscribe.attr.node, true, from); + if not ok then + module:log("debug", "Error during subscription: %s", err); + else + table.insert(nodes, subscribe.attr.node); + reply:tag("subscribe", { node = subscribe.attr.node }):up(); + end + end + channels[i].subscriptions[from] = nodes; + + local participant = Participant:new(jid.bare(from), nick:get_text()) + table.insert(channels[i].participants, participant) + channels[i].spid[jid.bare(stanza.attr.from)] = spid; + publish_participant(srv, spid, participant); + channels[i]:save_state(); + + reply:add_child(nick); + origin.send(reply); + return true +end); + +module:hook("iq-set/bare/urn:xmpp:mix:core:1:setnick", function(event) + module:log("debug", "MIX setnick received"); + local origin, stanza = event.origin, event.stanza; + local from = jid.bare(stanza.attr.from); + local i, channel = get_channel(stanza.attr.to); + if not channel then + -- TODO: Return error + end + + local participant_index = find_participant(channel.participants, from); + if participant_index == -1 then + channel:debug_print(); + module:log("debug", "%s is not a participant in %s", from, channel.jid); + return; + end + + local setnick = stanza:get_child("setnick", "urn:xmpp:mix:core:1"); + -- TODO: Error handling + local nick = setnick:get_child("nick"):get_text(); + + -- Change the nick + channels[i].participants[participant_index].nick = nick; + -- Inform all other members + local srv = pep.get_pep_service(channel.jid); + local participant = channel.participants[participant_index]; + publish_participant(srv, channel:get_spid(participant.jid), participant); + + origin.send(st.reply(stanza) + :tag("setnick", { xmlns = "urn:xmpp:mix:core:1" }) + :tag("nick"):text(nick)); + channel:save_state(); + return true; +end); + +function Channel:publish_info(srv) + srv:publish("urn:xmpp:mix:nodes:info", true, timestamp, + st.stanza("item", { id = timestamp, xmlns = "http://jabber.org/protocol/pubsub" }) + :tag("x", { xmlns = "jabber:x:data", type = "result" }) + :tag("field", { var = "FORM_TYPE", type = "hidden" }) + :tag("value"):text("urn:xmpp:mix:core:1"):up():up() + :tag("field", { var = "Name" }) + :tag("value"):text(self.name):up() + :tag("field", { var = "Contact" }) + :tag("value"):text(self.contacts)); +end + +function create_channel(node, creator, adhoc) + local channel = Channel:new(string.format("%s@%s", node, host), + default_channel_name, + default_channel_description, + {}, + {}, + {}, + { creator }, + adhoc); + -- Create the PEP nodes + local srv = pep.get_pep_service(node); + local timestamp = datetime.datetime(time.now()); + srv:create("urn:xmpp:mix:nodes:info", true, { ["access_model"] = "open" }); + channel:publish_info(srv); + -- TODO: This seems bad + srv:create("urn:xmpp:mix:nodes:participants", true, { ["access_model"] = "open"}); + table.insert(channels, channel); +end + +module:hook("iq-set/host/urn:xmpp:mix:core:1:create", function(event) + module:log("debug", "MIX create received"); + local origin, stanza = event.origin, event.stanza; + local from = jid.bare(stanza.attr.from); + + local create = stanza:get_child("create", "urn:xmpp:mix:core:1"); + local node; + if create.attr.channel ~= nil then + -- Create non-adhoc channel + node = create.attr.channel; + local _, channel = get_channel(stanza.attr.to); + if channel then + -- TODO: Return error + end + + create_channel(create.attr.channel, from, false); + else + -- Create adhoc channel + -- TODO: Check for a collision + node = id.short(); + create_channel(node, from, true); + end + module:log("debug", "Channel %s created with %s as owner", node, from); + + origin.send(st.reply(stanza) + :tag("create", { xmlns = "urn:xmpp:mix:core:1", channel = node })); + save_channels(); + return true; +end); + +module:hook("iq-set/host/urn:xmpp:mix:core:1:destroy", function(event) + module:log("debug", "MIX destroy received"); + local origin, stanza = event.origin, event.stanza; + local from = jid.bare(stanza.attr.from); + + local destroy = stanza:get_child("create", "urn:xmpp:mix:core:1"); + local node = destory.attr.channel; + local node_jid = string.format("%s@%s", node, host); + local i, channel = get_channel(node_jid); + if not channel then + -- TODO: Error + end + -- TODO: Check permissions + + -- Remove all registered nodes + local srv = pep.get_pep_service(node); + for _, pep_node in pairs({ "urn:xmpp:mix:nodes:participants", "urn:xmpp:mix:nodes:info" }) do + srv:delete(pep_node, true); + end + table.remove(channels, i); + + module:log("debug", "Channel %s destroyed", node); + + origin.send(st.reply(stanza)); + save_channels(); + return true; +end); + +module:hook("message/bare", function(event) + module:log("debug", "MIX message detected"); + local stanza = event.stanza; + + if stanza.attr.type ~= "groupchat" then + -- TODO: Error handling + end + + local from = jid.bare(stanza.attr.from); + local i, channel = get_channel(stanza.attr.to); + if not channel then + -- TODO: Error handlung + end + + local participant_index = find_participant(channel.participants, from); + if participant_index == -1 then + -- TODO: Error not in channel + return; + end + local participant = channel.participants[participant_index]; + + local msg = st.clone(stanza); + msg:add_child(st.stanza("mix", { xmlns = "urn:xmpp:mix:core:1" }) + :tag("nick"):text(participant.nick):up() + :tag("jid"):text(participant.jid):up()); + msg.attr.from = channel.jid.."/"..channel.spid[from]; + for _, p in pairs(channel.participants) do + if p.jid ~= participant.jid then + msg.attr.to = p.jid; + module:send(msg); + module:log("debug", msg:pretty_print()); + module:log("debug", "Message from %s sent to %s", participant.jid, p.jid); + end + end + return true; +end);