| 1 | local verse = require "verse"; |
| 2 | local bare_jid = require "prosody.util.jid".bare; |
| 3 | |
| 4 | local xmlns_roster = "jabber:iq:roster"; |
| 5 | local xmlns_rosterver = "urn:xmpp:features:rosterver"; |
| 6 | local t_insert = table.insert; |
| 7 | |
| 8 | function verse.plugins.roster(stream) |
| 9 | local ver_supported = false; |
| 10 | local roster = { |
| 11 | items = {}; |
| 12 | ver = ""; |
| 13 | -- TODO: |
| 14 | -- groups = {}; |
| 15 | }; |
| 16 | stream.roster = roster; |
| 17 | |
| 18 | stream:hook("stream-features", function(features_stanza) |
| 19 | if features_stanza:get_child("ver", xmlns_rosterver) then |
| 20 | ver_supported = true; |
| 21 | end |
| 22 | end); |
| 23 | |
| 24 | local function item_lua2xml(item_table) |
| 25 | local xml_item = verse.stanza("item", { xmlns = xmlns_roster }); |
| 26 | for k, v in pairs(item_table) do |
| 27 | if k ~= "groups" then |
| 28 | xml_item.attr[k] = v; |
| 29 | else |
| 30 | for i = 1,#v do |
| 31 | xml_item:tag("group"):text(v[i]):up(); |
| 32 | end |
| 33 | end |
| 34 | end |
| 35 | return xml_item; |
| 36 | end |
| 37 | |
| 38 | local function item_xml2lua(xml_item) |
| 39 | local item_table = { }; |
| 40 | local groups = {}; |
| 41 | item_table.groups = groups; |
| 42 | |
| 43 | for k, v in pairs(xml_item.attr) do |
| 44 | if k ~= "xmlns" then |
| 45 | item_table[k] = v |
| 46 | end |
| 47 | end |
| 48 | |
| 49 | for group in xml_item:childtags("group") do |
| 50 | t_insert(groups, group:get_text()) |
| 51 | end |
| 52 | return item_table; |
| 53 | end |
| 54 | |
| 55 | function roster:load(r) |
| 56 | roster.ver, roster.items = r.ver, r.items; |
| 57 | end |
| 58 | |
| 59 | function roster:dump() |
| 60 | return { |
| 61 | ver = roster.ver, |
| 62 | items = roster.items, |
| 63 | }; |
| 64 | end |
| 65 | |
| 66 | -- should this be add_contact(item, callback) instead? |
| 67 | function roster:add_contact(jid, name, groups, callback) |
| 68 | local item = { jid = jid, name = name, groups = groups }; |
| 69 | local stanza = verse.iq({ type = "set" }) |
| 70 | :tag("query", { xmlns = xmlns_roster }) |
| 71 | :add_child(item_lua2xml(item)); |
| 72 | stream:send_iq(stanza, function (reply) |
| 73 | if not callback then return end |
| 74 | if reply.attr.type == "result" then |
| 75 | callback(true); |
| 76 | else |
| 77 | callback(nil, reply); |
| 78 | end |
| 79 | end); |
| 80 | end |
| 81 | -- What about subscriptions? |
| 82 | |
| 83 | function roster:delete_contact(jid, callback) |
| 84 | jid = (type(jid) == "table" and jid.jid) or jid; |
| 85 | local item = { jid = jid, subscription = "remove" } |
| 86 | if not roster.items[jid] then return false, "item-not-found"; end |
| 87 | stream:send_iq(verse.iq({ type = "set" }) |
| 88 | :tag("query", { xmlns = xmlns_roster }) |
| 89 | :add_child(item_lua2xml(item)), |
| 90 | function (reply) |
| 91 | if not callback then return end |
| 92 | if reply.attr.type == "result" then |
| 93 | callback(true); |
| 94 | else |
| 95 | callback(nil, reply); |
| 96 | end |
| 97 | end); |
| 98 | end |
| 99 | |
| 100 | local function add_item(item) -- Takes one roster <item/> |
| 101 | local roster_item = item_xml2lua(item); |
| 102 | roster.items[roster_item.jid] = roster_item; |
| 103 | end |
| 104 | |
| 105 | -- Private low level |
| 106 | local function delete_item(jid) |
| 107 | local deleted_item = roster.items[jid]; |
| 108 | roster.items[jid] = nil; |
| 109 | return deleted_item; |
| 110 | end |
| 111 | |
| 112 | function roster:fetch(callback) |
| 113 | stream:send_iq(verse.iq({type="get"}):tag("query", { xmlns = xmlns_roster, ver = ver_supported and roster.ver or nil }), |
| 114 | function (result) |
| 115 | if result.attr.type == "result" then |
| 116 | local query = result:get_child("query", xmlns_roster); |
| 117 | if query then |
| 118 | roster.items = {}; |
| 119 | for item in query:childtags("item") do |
| 120 | add_item(item) |
| 121 | end |
| 122 | roster.ver = query.attr.ver or ""; |
| 123 | end |
| 124 | callback(roster); |
| 125 | else |
| 126 | callback(nil, result); |
| 127 | end |
| 128 | end); |
| 129 | end |
| 130 | |
| 131 | stream:hook("iq/"..xmlns_roster, function(stanza) |
| 132 | local type, from = stanza.attr.type, stanza.attr.from; |
| 133 | if type == "set" and (not from or from == bare_jid(stream.jid)) then |
| 134 | local query = stanza:get_child("query", xmlns_roster); |
| 135 | local item = query and query:get_child("item"); |
| 136 | if item then |
| 137 | local event, target; |
| 138 | local jid = item.attr.jid; |
| 139 | if item.attr.subscription == "remove" then |
| 140 | event = "removed" |
| 141 | target = delete_item(jid); |
| 142 | else |
| 143 | event = roster.items[jid] and "changed" or "added"; |
| 144 | add_item(item) |
| 145 | target = roster.items[jid]; |
| 146 | end |
| 147 | roster.ver = query.attr.ver; |
| 148 | if target then |
| 149 | stream:event("roster/item-"..event, target); |
| 150 | end |
| 151 | -- TODO else return error? Events? |
| 152 | end |
| 153 | stream:send(verse.reply(stanza)) |
| 154 | return true; |
| 155 | end |
| 156 | end); |
| 157 | end |