vinesmod 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. data/Gemfile +3 -0
  2. data/LICENSE +19 -0
  3. data/README.md +43 -0
  4. data/Rakefile +57 -0
  5. data/bin/vines +93 -0
  6. data/conf/certs/README +39 -0
  7. data/conf/certs/ca-bundle.crt +3366 -0
  8. data/conf/config.rb +149 -0
  9. data/lib/vines.rb +197 -0
  10. data/lib/vines/cluster.rb +246 -0
  11. data/lib/vines/cluster/connection.rb +26 -0
  12. data/lib/vines/cluster/publisher.rb +55 -0
  13. data/lib/vines/cluster/pubsub.rb +92 -0
  14. data/lib/vines/cluster/sessions.rb +125 -0
  15. data/lib/vines/cluster/subscriber.rb +108 -0
  16. data/lib/vines/command/bcrypt.rb +12 -0
  17. data/lib/vines/command/cert.rb +50 -0
  18. data/lib/vines/command/init.rb +68 -0
  19. data/lib/vines/command/register.rb +27 -0
  20. data/lib/vines/command/restart.rb +12 -0
  21. data/lib/vines/command/schema.rb +24 -0
  22. data/lib/vines/command/start.rb +28 -0
  23. data/lib/vines/command/stop.rb +18 -0
  24. data/lib/vines/command/unregister.rb +27 -0
  25. data/lib/vines/config.rb +213 -0
  26. data/lib/vines/config/host.rb +119 -0
  27. data/lib/vines/config/port.rb +132 -0
  28. data/lib/vines/config/pubsub.rb +108 -0
  29. data/lib/vines/contact.rb +111 -0
  30. data/lib/vines/daemon.rb +78 -0
  31. data/lib/vines/error.rb +150 -0
  32. data/lib/vines/jid.rb +95 -0
  33. data/lib/vines/kit.rb +35 -0
  34. data/lib/vines/log.rb +24 -0
  35. data/lib/vines/router.rb +179 -0
  36. data/lib/vines/stanza.rb +175 -0
  37. data/lib/vines/stanza/iq.rb +48 -0
  38. data/lib/vines/stanza/iq/auth.rb +18 -0
  39. data/lib/vines/stanza/iq/disco_info.rb +45 -0
  40. data/lib/vines/stanza/iq/disco_items.rb +29 -0
  41. data/lib/vines/stanza/iq/error.rb +16 -0
  42. data/lib/vines/stanza/iq/ping.rb +16 -0
  43. data/lib/vines/stanza/iq/private_storage.rb +83 -0
  44. data/lib/vines/stanza/iq/query.rb +10 -0
  45. data/lib/vines/stanza/iq/register.rb +42 -0
  46. data/lib/vines/stanza/iq/result.rb +16 -0
  47. data/lib/vines/stanza/iq/roster.rb +140 -0
  48. data/lib/vines/stanza/iq/session.rb +17 -0
  49. data/lib/vines/stanza/iq/vcard.rb +56 -0
  50. data/lib/vines/stanza/iq/version.rb +25 -0
  51. data/lib/vines/stanza/message.rb +43 -0
  52. data/lib/vines/stanza/presence.rb +156 -0
  53. data/lib/vines/stanza/presence/error.rb +23 -0
  54. data/lib/vines/stanza/presence/probe.rb +37 -0
  55. data/lib/vines/stanza/presence/subscribe.rb +42 -0
  56. data/lib/vines/stanza/presence/subscribed.rb +51 -0
  57. data/lib/vines/stanza/presence/unavailable.rb +15 -0
  58. data/lib/vines/stanza/presence/unsubscribe.rb +38 -0
  59. data/lib/vines/stanza/presence/unsubscribed.rb +38 -0
  60. data/lib/vines/stanza/pubsub.rb +22 -0
  61. data/lib/vines/stanza/pubsub/create.rb +39 -0
  62. data/lib/vines/stanza/pubsub/delete.rb +41 -0
  63. data/lib/vines/stanza/pubsub/publish.rb +66 -0
  64. data/lib/vines/stanza/pubsub/subscribe.rb +44 -0
  65. data/lib/vines/stanza/pubsub/unsubscribe.rb +30 -0
  66. data/lib/vines/storage.rb +188 -0
  67. data/lib/vines/storage/local.rb +165 -0
  68. data/lib/vines/storage/null.rb +39 -0
  69. data/lib/vines/storage/sql.rb +260 -0
  70. data/lib/vines/store.rb +94 -0
  71. data/lib/vines/stream.rb +247 -0
  72. data/lib/vines/stream/client.rb +84 -0
  73. data/lib/vines/stream/client/auth.rb +74 -0
  74. data/lib/vines/stream/client/auth_restart.rb +29 -0
  75. data/lib/vines/stream/client/bind.rb +72 -0
  76. data/lib/vines/stream/client/bind_restart.rb +24 -0
  77. data/lib/vines/stream/client/closed.rb +13 -0
  78. data/lib/vines/stream/client/ready.rb +17 -0
  79. data/lib/vines/stream/client/session.rb +210 -0
  80. data/lib/vines/stream/client/start.rb +27 -0
  81. data/lib/vines/stream/client/tls.rb +38 -0
  82. data/lib/vines/stream/component.rb +58 -0
  83. data/lib/vines/stream/component/handshake.rb +26 -0
  84. data/lib/vines/stream/component/ready.rb +23 -0
  85. data/lib/vines/stream/component/start.rb +19 -0
  86. data/lib/vines/stream/http.rb +157 -0
  87. data/lib/vines/stream/http/auth.rb +22 -0
  88. data/lib/vines/stream/http/bind.rb +32 -0
  89. data/lib/vines/stream/http/bind_restart.rb +37 -0
  90. data/lib/vines/stream/http/ready.rb +29 -0
  91. data/lib/vines/stream/http/request.rb +172 -0
  92. data/lib/vines/stream/http/session.rb +120 -0
  93. data/lib/vines/stream/http/sessions.rb +65 -0
  94. data/lib/vines/stream/http/start.rb +23 -0
  95. data/lib/vines/stream/parser.rb +78 -0
  96. data/lib/vines/stream/sasl.rb +92 -0
  97. data/lib/vines/stream/server.rb +150 -0
  98. data/lib/vines/stream/server/auth.rb +13 -0
  99. data/lib/vines/stream/server/auth_restart.rb +13 -0
  100. data/lib/vines/stream/server/final_restart.rb +21 -0
  101. data/lib/vines/stream/server/outbound/auth.rb +31 -0
  102. data/lib/vines/stream/server/outbound/auth_restart.rb +20 -0
  103. data/lib/vines/stream/server/outbound/auth_result.rb +32 -0
  104. data/lib/vines/stream/server/outbound/final_features.rb +28 -0
  105. data/lib/vines/stream/server/outbound/final_restart.rb +20 -0
  106. data/lib/vines/stream/server/outbound/start.rb +20 -0
  107. data/lib/vines/stream/server/outbound/tls.rb +30 -0
  108. data/lib/vines/stream/server/outbound/tls_result.rb +34 -0
  109. data/lib/vines/stream/server/ready.rb +24 -0
  110. data/lib/vines/stream/server/start.rb +13 -0
  111. data/lib/vines/stream/server/tls.rb +13 -0
  112. data/lib/vines/stream/state.rb +60 -0
  113. data/lib/vines/token_bucket.rb +55 -0
  114. data/lib/vines/user.rb +123 -0
  115. data/lib/vines/version.rb +5 -0
  116. data/lib/vines/xmpp_server.rb +43 -0
  117. data/vines.gemspec +36 -0
  118. data/web/404.html +51 -0
  119. data/web/apple-touch-icon.png +0 -0
  120. data/web/chat/coffeescripts/chat.coffee +362 -0
  121. data/web/chat/coffeescripts/init.coffee +15 -0
  122. data/web/chat/index.html +16 -0
  123. data/web/chat/javascripts/app.js +1 -0
  124. data/web/chat/stylesheets/chat.css +144 -0
  125. data/web/favicon.png +0 -0
  126. data/web/lib/coffeescripts/button.coffee +25 -0
  127. data/web/lib/coffeescripts/contact.coffee +32 -0
  128. data/web/lib/coffeescripts/filter.coffee +49 -0
  129. data/web/lib/coffeescripts/layout.coffee +30 -0
  130. data/web/lib/coffeescripts/login.coffee +68 -0
  131. data/web/lib/coffeescripts/logout.coffee +5 -0
  132. data/web/lib/coffeescripts/navbar.coffee +84 -0
  133. data/web/lib/coffeescripts/notification.coffee +14 -0
  134. data/web/lib/coffeescripts/router.coffee +40 -0
  135. data/web/lib/coffeescripts/session.coffee +229 -0
  136. data/web/lib/coffeescripts/transfer.coffee +106 -0
  137. data/web/lib/images/dark-gray.png +0 -0
  138. data/web/lib/images/default-user.png +0 -0
  139. data/web/lib/images/light-gray.png +0 -0
  140. data/web/lib/images/logo-large.png +0 -0
  141. data/web/lib/images/logo-small.png +0 -0
  142. data/web/lib/images/white.png +0 -0
  143. data/web/lib/javascripts/base.js +12 -0
  144. data/web/lib/javascripts/icons.js +110 -0
  145. data/web/lib/javascripts/jquery.cookie.js +91 -0
  146. data/web/lib/javascripts/jquery.js +4 -0
  147. data/web/lib/javascripts/raphael.js +6 -0
  148. data/web/lib/javascripts/strophe.js +1 -0
  149. data/web/lib/stylesheets/base.css +385 -0
  150. data/web/lib/stylesheets/login.css +68 -0
  151. metadata +423 -0
@@ -0,0 +1,48 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Iq < Stanza
6
+ register "/iq"
7
+
8
+ VALID_TYPES = %w[get set result error].freeze
9
+
10
+ VALID_TYPES.each do |type|
11
+ define_method "#{type}?" do
12
+ self['type'] == type
13
+ end
14
+ end
15
+
16
+ def process
17
+ if self['id'] && VALID_TYPES.include?(self['type'])
18
+ route_iq or raise StanzaErrors::FeatureNotImplemented.new(@node, 'cancel')
19
+ else
20
+ raise StanzaErrors::BadRequest.new(@node, 'modify')
21
+ end
22
+ end
23
+
24
+ def to_result
25
+ doc = Document.new
26
+ doc.create_element('iq',
27
+ 'from' => validate_to || stream.domain,
28
+ 'id' => self['id'],
29
+ 'to' => stream.user.jid,
30
+ 'type' => 'result')
31
+ end
32
+
33
+ private
34
+
35
+ # Return false if this IQ stanza is addressed to the server, or a pubsub
36
+ # service hosted here, and must be handled locally. Return true if the
37
+ # stanza must not be handled locally and has been routed to the appropriate
38
+ # component, s2s, or c2s stream.
39
+ def route_iq
40
+ to = validate_to
41
+ return false if to.nil? || stream.config.vhost?(to) || to_pubsub_domain?
42
+ self['from'] = stream.user.jid.to_s
43
+ local? ? broadcast(stream.connected_resources(to)) : route
44
+ true
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Iq
6
+ class Auth < Query
7
+ register "/iq[@id and @type='get']/ns:query", 'ns' => NAMESPACES[:non_sasl]
8
+
9
+ def process
10
+ # XEP-0078 says we MUST send a service-unavailable error
11
+ # here, but Adium 1.4.1 won't login if we do that, so just
12
+ # swallow this stanza.
13
+ # raise StanzaErrors::ServiceUnavailable.new(@node, 'cancel')
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,45 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Iq
6
+ class DiscoInfo < Query
7
+ NS = NAMESPACES[:disco_info]
8
+
9
+ register "/iq[@id and @type='get']/ns:query", 'ns' => NS
10
+
11
+ def process
12
+ return if route_iq || !allowed?
13
+ result = to_result.tap do |el|
14
+ el << el.document.create_element('query') do |query|
15
+ query.default_namespace = NS
16
+ if to_pubsub_domain?
17
+ identity(query, 'pubsub', 'service')
18
+ pubsub = [:pubsub_create, :pubsub_delete, :pubsub_instant, :pubsub_item_ids, :pubsub_publish, :pubsub_subscribe]
19
+ features(query, :disco_info, :ping, :pubsub, *pubsub)
20
+ else
21
+ identity(query, 'server', 'im')
22
+ features = [:disco_info, :disco_items, :ping, :vcard, :version]
23
+ features << :storage if stream.config.private_storage?(validate_to || stream.domain)
24
+ features(query, features)
25
+ end
26
+ end
27
+ end
28
+ stream.write(result)
29
+ end
30
+
31
+ private
32
+
33
+ def identity(query, category, type)
34
+ query << query.document.create_element('identity', 'category' => category, 'type' => type)
35
+ end
36
+
37
+ def features(query, *features)
38
+ features.flatten.each do |feature|
39
+ query << query.document.create_element('feature', 'var' => NAMESPACES[feature])
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,29 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Iq
6
+ class DiscoItems < Query
7
+ NS = NAMESPACES[:disco_items]
8
+
9
+ register "/iq[@id and @type='get']/ns:query", 'ns' => NS
10
+
11
+ def process
12
+ return if route_iq || !allowed?
13
+ result = to_result.tap do |el|
14
+ el << el.document.create_element('query') do |query|
15
+ query.default_namespace = NS
16
+ unless to_pubsub_domain?
17
+ to = (validate_to || stream.domain).to_s
18
+ stream.config.vhost(to).disco_items.each do |domain|
19
+ query << el.document.create_element('item', 'jid' => domain)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ stream.write(result)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,16 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Iq
6
+ class Error < Iq
7
+ register "/iq[@id and @type='error']"
8
+
9
+ def process
10
+ return if route_iq
11
+ # do nothing
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Iq
6
+ class Ping < Iq
7
+ register "/iq[@id and @type='get']/ns:ping", 'ns' => NAMESPACES[:ping]
8
+
9
+ def process
10
+ return if route_iq || !allowed?
11
+ stream.write(to_result)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,83 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Iq
6
+ # Implements the Private Storage feature defined in XEP-0049. Clients are
7
+ # allowed to save arbitrary XML documents on the server, identified by
8
+ # element name and namespace.
9
+ class PrivateStorage < Query
10
+ NS = NAMESPACES[:storage]
11
+
12
+ register "/iq[@id and (@type='get' or @type='set')]/ns:query", 'ns' => NS
13
+
14
+ def process
15
+ validate_to_address
16
+ validate_storage_enabled
17
+ validate_children_size
18
+ validate_namespaces
19
+ get? ? retrieve_fragment : update_fragment
20
+ end
21
+
22
+ private
23
+
24
+ def retrieve_fragment
25
+ found = storage.find_fragment(stream.user.jid, elements.first.elements.first)
26
+ raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless found
27
+
28
+ result = to_result do |node|
29
+ node << node.document.create_element('query') do |query|
30
+ query.default_namespace = NS
31
+ query << found
32
+ end
33
+ end
34
+ stream.write(result)
35
+ end
36
+
37
+ def update_fragment
38
+ elements.first.elements.each do |node|
39
+ storage.save_fragment(stream.user.jid, node)
40
+ end
41
+ stream.write(to_result)
42
+ end
43
+
44
+ private
45
+
46
+ def to_result
47
+ super.tap do |node|
48
+ node['from'] = stream.user.jid.to_s
49
+ yield node if block_given?
50
+ end
51
+ end
52
+
53
+ def validate_children_size
54
+ size = elements.first.elements.size
55
+ if (get? && size != 1) || (set? && size == 0)
56
+ raise StanzaErrors::NotAcceptable.new(self, 'modify')
57
+ end
58
+ end
59
+
60
+ def validate_to_address
61
+ to = validate_to
62
+ unless to.nil? || to == stream.user.jid.bare
63
+ raise StanzaErrors::Forbidden.new(self, 'cancel')
64
+ end
65
+ end
66
+
67
+ def validate_storage_enabled
68
+ unless stream.config.private_storage?(stream.domain)
69
+ raise StanzaErrors::ServiceUnavailable.new(self, 'cancel')
70
+ end
71
+ end
72
+
73
+ def validate_namespaces
74
+ elements.first.elements.each do |node|
75
+ if node.namespace.nil? || NAMESPACES.values.include?(node.namespace.href)
76
+ raise StanzaErrors::NotAcceptable.new(self, 'modify')
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,10 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Iq
6
+ class Query < Iq
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,42 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Iq
6
+ class Register < Query
7
+ NS = NAMESPACES[:register]
8
+
9
+ register "/iq[@id and @type='set']/ns:query", 'ns' => NS
10
+
11
+ def process
12
+ if is_stream_owner
13
+ current_user = storage(stream.domain).find_user(stream.user.jid)
14
+ password = @node.xpath("//iq/jir:query//jir:password", {"jir"=>"jabber:iq:register"}).text
15
+ unless password.nil?
16
+ current_user.password = BCrypt::Password.create(password.to_s)
17
+ storage.save_user(current_user)
18
+ stream.write(to_result)
19
+ else
20
+ raise StanzaErrors::NotAcceptable.new(self, 'cancel')
21
+ end
22
+ else
23
+
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def is_stream_owner
30
+ stream.user.jid.bare == jid_from_username.bare
31
+ end
32
+
33
+ def jid_from_username
34
+ username = @node.xpath("//iq/jir:query//jir:username", {"jir"=>"jabber:iq:register"}).text
35
+ dom = @node.attributes["to"].text
36
+ JID.new(username, dom)
37
+ end
38
+
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,16 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Iq
6
+ class Result < Iq
7
+ register "/iq[@id and @type='result']"
8
+
9
+ def process
10
+ return if route_iq
11
+ # do nothing
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,140 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Iq
6
+ class Roster < Query
7
+ NS = NAMESPACES[:roster]
8
+
9
+ register "/iq[@id and (@type='get' or @type='set')]/ns:query", 'ns' => NS
10
+
11
+ def process
12
+ validate_to_address
13
+ get? ? roster_query : update_roster
14
+ end
15
+
16
+ private
17
+
18
+ # Send an iq result stanza containing roster items to the user in
19
+ # response to their roster get request. Requesting the roster makes
20
+ # this stream an "interested resource" that can now receive roster
21
+ # updates.
22
+ def roster_query
23
+ stream.requested_roster!
24
+ stream.write(stream.user.to_roster_xml(self['id']))
25
+ end
26
+
27
+ # Roster sets must have no 'to' address or be addressed to the same
28
+ # JID that sent the stanza. RFC 6121 sections 2.1.5 and 2.3.3.
29
+ def validate_to_address
30
+ to = validate_to
31
+ unless to.nil? || to.bare == stream.user.jid.bare
32
+ raise StanzaErrors::Forbidden.new(self, 'auth')
33
+ end
34
+ end
35
+
36
+ # Add, update, or delete the roster item contained in the iq set
37
+ # stanza received from the client. RFC 6121 sections 2.3, 2.4, 2.5.
38
+ def update_roster
39
+ items = self.xpath('ns:query/ns:item', 'ns' => NS)
40
+ raise StanzaErrors::BadRequest.new(self, 'modify') if items.size != 1
41
+ item = items.first
42
+
43
+ jid = JID.new(item['jid']) rescue (raise StanzaErrors::JidMalformed.new(self, 'modify'))
44
+ raise StanzaErrors::BadRequest.new(self, 'modify') if jid.empty? || !jid.bare?
45
+
46
+ if item['subscription'] == 'remove'
47
+ remove_contact(jid)
48
+ return
49
+ end
50
+
51
+ raise StanzaErrors::NotAllowed.new(self, 'modify') if jid == stream.user.jid.bare
52
+ groups = item.xpath('ns:group', 'ns' => NS).map {|g| g.text.strip }
53
+ raise StanzaErrors::BadRequest.new(self, 'modify') if groups.uniq!
54
+ raise StanzaErrors::NotAcceptable.new(self, 'modify') if groups.include?('')
55
+
56
+ contact = stream.user.contact(jid)
57
+ unless contact
58
+ contact = Contact.new(jid: jid)
59
+ stream.user.roster << contact
60
+ end
61
+ contact.name = item['name']
62
+ contact.groups = groups
63
+ storage.save_user(stream.user)
64
+ stream.update_user_streams(stream.user)
65
+ send_result_iq
66
+ push_roster_updates(stream.user.jid, contact)
67
+ end
68
+
69
+ # Remove the contact with this JID from the user's roster and send
70
+ # roster pushes to the user's interested resources. This is triggered
71
+ # by receiving an iq set with an item element like
72
+ # <item jid="alice@wonderland.lit" subscription="remove"/>. RFC 6121
73
+ # section 2.5.
74
+ def remove_contact(jid)
75
+ contact = stream.user.contact(jid)
76
+ raise StanzaErrors::ItemNotFound.new(self, 'modify') unless contact
77
+ if local_jid?(contact.jid)
78
+ user = storage(contact.jid.domain).find_user(contact.jid)
79
+ end
80
+
81
+ if user && user.contact(stream.user.jid)
82
+ user.contact(stream.user.jid).subscription = 'none'
83
+ user.contact(stream.user.jid).ask = nil
84
+ end
85
+ stream.user.remove_contact(contact.jid)
86
+ [user, stream.user].compact.each do |save|
87
+ storage(save.jid.domain).save_user(save)
88
+ stream.update_user_streams(save)
89
+ end
90
+
91
+ send_result_iq
92
+ push_roster_updates(stream.user.jid,
93
+ Contact.new(jid: contact.jid, subscription: 'remove'))
94
+
95
+ if local_jid?(contact.jid)
96
+ send_unavailable(stream.user.jid, contact.jid) if contact.subscribed_from?
97
+ send_unsubscribe(contact)
98
+ if user && user.contact(stream.user.jid)
99
+ push_roster_updates(contact.jid, user.contact(stream.user.jid))
100
+ end
101
+ else
102
+ send_unsubscribe(contact)
103
+ end
104
+ end
105
+
106
+ # Notify the contact that it's been removed from the user's roster
107
+ # and no longer has any presence relationship with the user.
108
+ def send_unsubscribe(contact)
109
+ presence = [%w[to unsubscribe], %w[from unsubscribed]].map do |meth, type|
110
+ presence(contact.jid, type) if contact.send("subscribed_#{meth}?")
111
+ end.compact
112
+ broadcast_to_interested_resources(presence, contact.jid)
113
+ end
114
+
115
+ def presence(to, type)
116
+ doc = Document.new
117
+ doc.create_element('presence',
118
+ 'from' => stream.user.jid.bare.to_s,
119
+ 'id' => Kit.uuid,
120
+ 'to' => to.to_s,
121
+ 'type' => type)
122
+ end
123
+
124
+ # Send an iq set stanza to the user's interested resources, letting them
125
+ # know their roster has been updated.
126
+ def push_roster_updates(to, contact)
127
+ stream.interested_resources(to).each do |recipient|
128
+ contact.send_roster_push(recipient)
129
+ end
130
+ end
131
+
132
+ def send_result_iq
133
+ node = to_result
134
+ node.remove_attribute('from')
135
+ stream.write(node)
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end