b7c04163a6552b4db887da88c367feea844a2317
[prosody.git] / plugins / mod_pubsub.lua
1 local pubsub = require "util.pubsub";
2 local st = require "util.stanza";
3 local jid_bare = require "util.jid".bare;
4 local uuid_generate = require "util.uuid".generate;
5
6 require "core.modulemanager".load(module.host, "iq");
7
8 local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
9 local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors";
10 local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event";
11
12 local service;
13
14 local handlers = {};
15
16 function handle_pubsub_iq(event)
17         local origin, stanza = event.origin, event.stanza;
18         local pubsub = stanza.tags[1];
19         local action = pubsub.tags[1];
20         local handler = handlers[stanza.attr.type.."_"..action.name];
21         if handler then
22                 handler(origin, stanza, action);
23                 return true;
24         end
25 end
26
27 local pubsub_errors = {
28         ["conflict"] = { "cancel", "conflict" };
29         ["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" };
30         ["item-not-found"] = { "cancel", "item-not-found" };
31         ["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" };
32         ["forbidden"] = { "cancel", "forbidden" };
33 };
34 function pubsub_error_reply(stanza, error)
35         local e = pubsub_errors[error];
36         local reply = st.error_reply(stanza, unpack(e, 1, 3));
37         if e[4] then
38                 reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up();
39         end
40         return reply;
41 end
42
43 function handlers.get_items(origin, stanza, items)
44         local node = items.attr.node;
45         local item = items:get_child("item");
46         local id = item and item.attr.id;
47         
48         local ok, results = service:get_items(node, stanza.attr.from, id);
49         if not ok then
50                 return origin.send(pubsub_error_reply(stanza, results));
51         end
52         
53         local data = st.stanza("items", { node = node });
54         for _, entry in pairs(results) do
55                 data:add_child(entry);
56         end
57         if data then
58                 reply = st.reply(stanza)
59                         :tag("pubsub", { xmlns = xmlns_pubsub })
60                                 :add_child(data);
61         else
62                 reply = pubsub_error_reply(stanza, "item-not-found");
63         end
64         return origin.send(reply);
65 end
66
67 function handlers.set_create(origin, stanza, create)
68         local node = create.attr.node;
69         local ok, ret, reply;
70         if node then
71                 ok, ret = service:create(node, stanza.attr.from);
72                 if ok then
73                         reply = st.reply(stanza);
74                 else
75                         reply = pubsub_error_reply(stanza, ret);
76                 end
77         else
78                 repeat
79                         node = uuid_generate();
80                         ok, ret = service:create(node, stanza.attr.from);
81                 until ok or ret ~= "conflict";
82                 if ok then
83                         reply = st.reply(stanza)
84                                 :tag("pubsub", { xmlns = xmlns_pubsub })
85                                         :tag("create", { node = node });
86                 else
87                         reply = pubsub_error_reply(stanza, ret);
88                 end
89         end
90         return origin.send(reply);
91 end
92
93 function handlers.set_subscribe(origin, stanza, subscribe)
94         local node, jid = subscribe.attr.node, subscribe.attr.jid;
95         if jid_bare(jid) ~= jid_bare(stanza.attr.from) then
96                 return origin.send(pubsub_error_reply(stanza, "invalid-jid"));
97         end
98         local ok, ret = service:add_subscription(node, stanza.attr.from, jid);
99         local reply;
100         if ok then
101                 reply = st.reply(stanza)
102                         :tag("pubsub", { xmlns = xmlns_pubsub })
103                                 :tag("subscription", {
104                                         node = node,
105                                         jid = jid,
106                                         subscription = "subscribed"
107                                 });
108         else
109                 reply = pubsub_error_reply(stanza, ret);
110         end
111         return origin.send(reply);
112 end
113
114 function handlers.set_unsubscribe(origin, stanza, unsubscribe)
115         local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid;
116         if jid_bare(jid) ~= jid_bare(stanza.attr.from) then
117                 return origin.send(pubsub_error_reply(stanza, "invalid-jid"));
118         end
119         local ok, ret = service:remove_subscription(node, stanza.attr.from, jid);
120         local reply;
121         if ok then
122                 reply = st.reply(stanza);
123         else
124                 reply = pubsub_error_reply(stanza, ret);
125         end
126         return origin.send(reply);
127 end
128
129 function handlers.set_publish(origin, stanza, publish)
130         local node = publish.attr.node;
131         local item = publish:get_child("item");
132         local id = (item and item.attr.id) or uuid_generate();
133         local ok, ret = service:publish(node, stanza.attr.from, id, item);
134         local reply;
135         if ok then
136                 reply = st.reply(stanza)
137                         :tag("pubsub", { xmlns = xmlns_pubsub })
138                                 :tag("publish", { node = node })
139                                         :tag("item", { id = id });
140         else
141                 reply = pubsub_error_reply(stanza, ret);
142         end
143         return origin.send(reply);
144 end
145
146 function handlers.set_retract(origin, stanza, retract)
147         local node, notify = retract.attr.node, retract.attr.notify;
148         notify = (notify == "1") or (notify == "true");
149         local item = retract:get_child("item");
150         local id = item and item.attr.id
151         local reply, notifier;
152         if notify then
153                 notifier = st.stanza("retract", { id = id });
154         end
155         local ok, ret = service:retract(node, stanza.attr.from, id, notifier);
156         if ok then
157                 reply = st.reply(stanza);
158         else
159                 reply = pubsub_error_reply(stanza, ret);
160         end
161         return origin.send(reply);
162 end
163
164 function simple_broadcast(node, jids, item)
165         item = st.clone(item);
166         item.attr.xmlns = nil; -- Clear the pubsub namespace
167         local message = st.message({ from = module.host, type = "headline" })
168                 :tag("event", { xmlns = xmlns_pubsub_event })
169                         :tag("items", { node = node })
170                                 :add_child(item);
171         for jid in pairs(jids) do
172                 module:log("debug", "Sending notification to %s", jid);
173                 message.attr.to = jid;
174                 core_post_stanza(hosts[module.host], message);
175         end
176 end
177
178 module:hook("iq/host/http://jabber.org/protocol/pubsub:pubsub", handle_pubsub_iq);
179
180 local disco_info = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" })
181         :tag("identity", { category = "pubsub", type = "service" }):up()
182         :tag("feature", { var = "http://jabber.org/protocol/pubsub" }):up();
183
184 module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function (event)
185         event.origin.send(st.reply(event.stanza):add_child(disco_info));
186         return true;
187 end);
188
189 module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function (event)
190         local ok, ret = service:get_nodes(event.stanza.attr.from);
191         if not ok then
192                 event.origin.send(pubsub_error_reply(stanza, ret));
193         else
194                 local reply = st.reply(event.stanza)
195                         :tag("query", { xmlns = "http://jabber.org/protocol/disco#items" });
196                 for node, node_obj in pairs(ret) do
197                         reply:tag("item", { jid = module.host, node = node, name = node_obj.config.name }):up();
198                 end
199                 event.origin.send(reply);
200         end
201         return true;
202 end);
203
204 local admin_aff = module:get_option_string("default_admin_affiliation", "owner");
205 local function get_affiliation(jid)
206         if usermanager.is_admin(jid, module.host) then
207                 return admin_aff;
208         end
209 end
210
211 service = pubsub.new({
212         capabilities = {
213                 none = {
214                         create = false;
215                         publish = false;
216                         retract = false;
217                         get_nodes = true;
218                         
219                         subscribe = true;
220                         unsubscribe = true;
221                         get_subscription = true;
222                         --get_items = true;
223                         
224                         subscribe_other = false;
225                         unsubscribe_other = false;
226                         get_subscription_other = false;
227                         
228                         be_subscribed = true;
229                         be_unsubscribed = true;
230                         
231                         set_affiliation = false;
232                 };
233                 owner = {
234                         create = true;
235                         publish = true;
236                         retract = true;
237                         get_nodes = true;
238                         
239                         subscribe = true;
240                         unsubscribe = true;
241                         get_subscription = true;
242                         --get_items = true;
243                         
244                         
245                         subscribe_other = true;
246                         unsubscribe_other = true;
247                         get_subscription_other = true;
248                         
249                         be_subscribed = true;
250                         be_unsubscribed = true;
251                         
252                         set_affiliation = true;
253                 };
254                 admin = { get_items = true };
255         };
256         
257         autocreate_on_publish = module:get_option_boolean("autocreate_on_publish");
258         autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe");
259         
260         broadcaster = simple_broadcast;
261         get_affiliation = get_affiliation;
262         jids_equal = function (jid1, jid2)
263                 return jid_bare(jid1) == jid_bare(jid2);
264         end;
265 });
266 module.environment.service = service;
267
268 function module.save()
269         return { service = service };
270 end
271
272 function module.restore(data)
273         service = data.service;
274         module.environment.service = service;
275 end