# encoding: UTF-8 module Vines class Stanza class Iq class Roster < Query NS = NAMESPACES[:roster] register "/iq[@id and (@type='get' or @type='set')]/ns:query", 'ns' => NS def process validate_to_address get? ? roster_query : update_roster end private # Send an iq result stanza containing roster items to the user in # response to their roster get request. Requesting the roster makes # this stream an "interested resource" that can now receive roster # updates. def roster_query stream.requested_roster! stream.write(stream.user.to_roster_xml(self['id'])) end # Roster sets must have no 'to' address or be addressed to the same # JID that sent the stanza. RFC 6121 sections 2.1.5 and 2.3.3. def validate_to_address to = validate_to unless to.nil? || to.bare == stream.user.jid.bare raise StanzaErrors::Forbidden.new(self, 'auth') end end # Add, update, or delete the roster item contained in the iq set # stanza received from the client. RFC 6121 sections 2.3, 2.4, 2.5. def update_roster items = self.xpath('ns:query/ns:item', 'ns' => NS) raise StanzaErrors::BadRequest.new(self, 'modify') if items.size != 1 item = items.first jid = JID.new(item['jid']) rescue (raise StanzaErrors::JidMalformed.new(self, 'modify')) raise StanzaErrors::BadRequest.new(self, 'modify') if jid.empty? || !jid.bare? if item['subscription'] == 'remove' remove_contact(jid) return end raise StanzaErrors::NotAllowed.new(self, 'modify') if jid == stream.user.jid.bare groups = item.xpath('ns:group', 'ns' => NS).map {|g| g.text.strip } raise StanzaErrors::BadRequest.new(self, 'modify') if groups.uniq! raise StanzaErrors::NotAcceptable.new(self, 'modify') if groups.include?('') contact = stream.user.contact(jid) unless contact contact = Contact.new(jid: jid) stream.user.roster << contact end contact.name = item['name'] contact.groups = groups storage.save_user(stream.user) stream.update_user_streams(stream.user) send_result_iq push_roster_updates(stream.user.jid, contact) end # Remove the contact with this JID from the user's roster and send # roster pushes to the user's interested resources. This is triggered # by receiving an iq set with an item element like # . RFC 6121 # section 2.5. def remove_contact(jid) contact = stream.user.contact(jid) raise StanzaErrors::ItemNotFound.new(self, 'modify') unless contact if local_jid?(contact.jid) user = storage(contact.jid.domain).find_user(contact.jid) end if user && user.contact(stream.user.jid) user.contact(stream.user.jid).subscription = 'none' user.contact(stream.user.jid).ask = nil end stream.user.remove_contact(contact.jid) [user, stream.user].compact.each do |save| storage(save.jid.domain).save_user(save) stream.update_user_streams(save) end send_result_iq push_roster_updates(stream.user.jid, Contact.new(jid: contact.jid, subscription: 'remove')) if local_jid?(contact.jid) send_unavailable(stream.user.jid, contact.jid) if contact.subscribed_from? send_unsubscribe(contact) if user && user.contact(stream.user.jid) push_roster_updates(contact.jid, user.contact(stream.user.jid)) end else send_unsubscribe(contact) end end # Notify the contact that it's been removed from the user's roster # and no longer has any presence relationship with the user. def send_unsubscribe(contact) presence = [%w[to unsubscribe], %w[from unsubscribed]].map do |meth, type| presence(contact.jid, type) if contact.send("subscribed_#{meth}?") end.compact broadcast_to_interested_resources(presence, contact.jid) end def presence(to, type) doc = Document.new doc.create_element('presence', 'from' => stream.user.jid.bare.to_s, 'id' => Kit.uuid, 'to' => to.to_s, 'type' => type) end # Send an iq set stanza to the user's interested resources, letting them # know their roster has been updated. def push_roster_updates(to, contact) stream.interested_resources(to).each do |recipient| contact.send_roster_push(recipient) end end def send_result_iq node = to_result node.remove_attribute('from') stream.write(node) end end end end end