mix: Initial commit
This commit is contained in:
commit
881085e767
428
mod_mix/mod_mix.lua
Normal file
428
mod_mix/mod_mix.lua
Normal file
@ -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);
|
Loading…
Reference in New Issue
Block a user