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,15 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Presence
6
+ class Unavailable < Presence
7
+ register "/presence[@type='unavailable']"
8
+
9
+ def process
10
+ inbound? ? inbound_broadcast_presence : outbound_broadcast_presence
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,38 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Presence
6
+ class Unsubscribe < Presence
7
+ register "/presence[@type='unsubscribe']"
8
+
9
+ def process
10
+ stamp_from
11
+ inbound? ? process_inbound : process_outbound
12
+ end
13
+
14
+ def process_outbound
15
+ to = stamp_to
16
+ return unless stream.user.subscribed_to?(to)
17
+ stream.user.remove_subscription_to(to)
18
+ storage.save_user(stream.user)
19
+ stream.update_user_streams(stream.user)
20
+ local? ? process_inbound : route
21
+ send_roster_push(to)
22
+ end
23
+
24
+ def process_inbound
25
+ to = stamp_to
26
+ user = storage(to.domain).find_user(to)
27
+ return unless user && user.subscribed_from?(stream.user.jid)
28
+ contact = user.contact(stream.user.jid)
29
+ contact.unsubscribe_from
30
+ storage(to.domain).save_user(user)
31
+ stream.update_user_streams(user)
32
+ broadcast_subscription_change(contact)
33
+ send_unavailable(to, stream.user.jid.bare)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,38 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class Presence
6
+ class Unsubscribed < Presence
7
+ register "/presence[@type='unsubscribed']"
8
+
9
+ def process
10
+ stamp_from
11
+ inbound? ? process_inbound : process_outbound
12
+ end
13
+
14
+ def process_outbound
15
+ to = stamp_to
16
+ return unless stream.user.subscribed_from?(to)
17
+ send_unavailable(stream.user.jid, to)
18
+ stream.user.remove_subscription_from(to)
19
+ storage.save_user(stream.user)
20
+ stream.update_user_streams(stream.user)
21
+ local? ? process_inbound : route
22
+ send_roster_push(to)
23
+ end
24
+
25
+ def process_inbound
26
+ to = stamp_to
27
+ user = storage(to.domain).find_user(to)
28
+ return unless user && user.subscribed_to?(stream.user.jid)
29
+ contact = user.contact(stream.user.jid)
30
+ contact.unsubscribe_to
31
+ storage(to.domain).save_user(user)
32
+ stream.update_user_streams(user)
33
+ broadcast_subscription_change(contact)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class PubSub < Iq
6
+
7
+ private
8
+
9
+ # Return the Config::PubSub system for the domain to which this stanza is
10
+ # addressed or nil if it's not to a pubsub subdomain.
11
+ def pubsub
12
+ stream.config.pubsub(validate_to)
13
+ end
14
+
15
+ # Raise feature-not-implemented if this stanza is addressed to the chat
16
+ # server itself, rather than a pubsub subdomain.
17
+ def validate_to_address
18
+ raise StanzaErrors::FeatureNotImplemented.new(self, 'cancel') unless pubsub
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,39 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class PubSub
6
+ class Create < PubSub
7
+ NS = NAMESPACES[:pubsub]
8
+
9
+ register "/iq[@id and @type='set']/ns:pubsub/ns:create", 'ns' => NS
10
+
11
+ def process
12
+ return if route_iq || !allowed?
13
+ validate_to_address
14
+
15
+ node = self.xpath('ns:pubsub/ns:create', 'ns' => NS)
16
+ raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1
17
+ node = node.first
18
+
19
+ id = (node['node'] || '').strip
20
+ id = Kit.uuid if id.empty?
21
+ raise StanzaErrors::Conflict.new(self, 'cancel') if pubsub.node?(id)
22
+ pubsub.add_node(id)
23
+ send_result_iq(id)
24
+ end
25
+
26
+ private
27
+
28
+ def send_result_iq(id)
29
+ el = to_result
30
+ el << el.document.create_element('pubsub') do |node|
31
+ node.default_namespace = NS
32
+ node << el.document.create_element('create', 'node' => id)
33
+ end
34
+ stream.write(el)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,41 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class PubSub
6
+ class Delete < PubSub
7
+ NS = NAMESPACES[:pubsub]
8
+
9
+ register "/iq[@id and @type='set']/ns:pubsub/ns:delete", 'ns' => NS
10
+
11
+ def process
12
+ return if route_iq || !allowed?
13
+ validate_to_address
14
+
15
+ node = self.xpath('ns:pubsub/ns:delete', 'ns' => NS)
16
+ raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1
17
+ node = node.first
18
+
19
+ id = node['node']
20
+ raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless pubsub.node?(id)
21
+
22
+ pubsub.publish(id, message(id))
23
+ pubsub.delete_node(id)
24
+ stream.write(to_result)
25
+ end
26
+
27
+ private
28
+
29
+ def message(id)
30
+ doc = Document.new
31
+ doc.create_element('message') do |node|
32
+ node << node.document.create_element('event') do |event|
33
+ event.default_namespace = NAMESPACES[:pubsub_event]
34
+ event << node.document.create_element('delete', 'node' => id)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,66 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class PubSub
6
+ class Publish < PubSub
7
+ NS = NAMESPACES[:pubsub]
8
+
9
+ register "/iq[@id and @type='set']/ns:pubsub/ns:publish", 'ns' => NS
10
+
11
+ def process
12
+ return if route_iq || !allowed?
13
+ validate_to_address
14
+
15
+ node = self.xpath('ns:pubsub/ns:publish', 'ns' => NS)
16
+ raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1
17
+ node = node.first
18
+ id = node['node']
19
+
20
+ raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless pubsub.node?(id)
21
+
22
+ item = node.xpath('ns:item', 'ns' => NS)
23
+ raise StanzaErrors::BadRequest.new(self, 'modify') unless item.size == 1
24
+ item = item.first
25
+ unless item['id']
26
+ item['id'] = Kit.uuid
27
+ include_item = true
28
+ end
29
+
30
+ raise StanzaErrors::BadRequest.new(self, 'modify') unless item.elements.size == 1
31
+ pubsub.publish(id, message(id, item))
32
+ send_result_iq(id, include_item ? item : nil)
33
+ end
34
+
35
+ private
36
+
37
+ def message(node, item)
38
+ doc = Document.new
39
+ doc.create_element('message') do |message|
40
+ message << doc.create_element('event') do |event|
41
+ event.default_namespace = NAMESPACES[:pubsub_event]
42
+ event << doc.create_element('items', 'node' => node) do |items|
43
+ items << doc.create_element('item', 'id' => item['id'], 'publisher' => stream.user.jid.to_s) do |el|
44
+ el << item.elements.first
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def send_result_iq(node, item)
52
+ result = to_result
53
+ if item
54
+ result << result.document.create_element('pubsub') do |pubsub|
55
+ pubsub.default_namespace = NS
56
+ pubsub << result.document.create_element('publish', 'node' => node) do |publish|
57
+ publish << result.document.create_element('item', 'id' => item['id'])
58
+ end
59
+ end
60
+ end
61
+ stream.write(result)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,44 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class PubSub
6
+ class Subscribe < PubSub
7
+ NS = NAMESPACES[:pubsub]
8
+
9
+ register "/iq[@id and @type='set']/ns:pubsub/ns:subscribe", 'ns' => NS
10
+
11
+ def process
12
+ return if route_iq || !allowed?
13
+ validate_to_address
14
+
15
+ node = self.xpath('ns:pubsub/ns:subscribe', 'ns' => NS)
16
+ raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1
17
+ node = node.first
18
+ id, jid = node['node'], JID.new(node['jid'])
19
+
20
+ raise StanzaErrors::BadRequest.new(self, 'modify') unless stream.user.jid.bare == jid.bare
21
+ raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless pubsub.node?(id)
22
+ raise StanzaErrors::PolicyViolation.new(self, 'wait') if pubsub.subscribed?(id, jid)
23
+
24
+ pubsub.subscribe(id, jid)
25
+ send_result_iq(id, jid)
26
+ end
27
+
28
+ private
29
+
30
+ def send_result_iq(id, jid)
31
+ result = to_result
32
+ result << result.document.create_element('pubsub') do |node|
33
+ node.default_namespace = NS
34
+ node << result.document.create_element('subscription',
35
+ 'node' => id,
36
+ 'jid' => jid.to_s,
37
+ 'subscription' => 'subscribed')
38
+ end
39
+ stream.write(result)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Stanza
5
+ class PubSub
6
+ class Unsubscribe < PubSub
7
+ NS = NAMESPACES[:pubsub]
8
+
9
+ register "/iq[@id and @type='set']/ns:pubsub/ns:unsubscribe", 'ns' => NS
10
+
11
+ def process
12
+ return if route_iq || !allowed?
13
+ validate_to_address
14
+
15
+ node = self.xpath('ns:pubsub/ns:unsubscribe', 'ns' => NS)
16
+ raise StanzaErrors::BadRequest.new(self, 'modify') if node.size != 1
17
+ node = node.first
18
+ id, jid = node['node'], JID.new(node['jid'])
19
+
20
+ raise StanzaErrors::Forbidden.new(self, 'auth') unless stream.user.jid.bare == jid.bare
21
+ raise StanzaErrors::ItemNotFound.new(self, 'cancel') unless pubsub.node?(id)
22
+ raise StanzaErrors::UnexpectedRequest.new(self, 'cancel') unless pubsub.subscribed?(id, jid)
23
+
24
+ pubsub.unsubscribe(id, jid)
25
+ stream.write(to_result)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,188 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Storage
5
+ include Vines::Log
6
+
7
+ @@nicks = {}
8
+
9
+ # Register a nickname that can be used in the config file to specify this
10
+ # storage implementation.
11
+ def self.register(name)
12
+ @@nicks[name.to_sym] = self
13
+ end
14
+
15
+ def self.from_name(name, &block)
16
+ klass = @@nicks[name.to_sym]
17
+ raise "#{name} storage class not found" unless klass
18
+ klass.new(&block)
19
+ end
20
+
21
+ # Wrap a blocking IO method in a new method that pushes the original method
22
+ # onto EventMachine's thread pool using EM#defer. Storage classes implemented
23
+ # with blocking IO don't need to worry about threading or blocking the
24
+ # EventMachine reactor thread if they wrap their methods with this one.
25
+ #
26
+ # For example:
27
+ # def find_user(jid)
28
+ # some_blocking_lookup(jid)
29
+ # end
30
+ # defer :find_user
31
+ #
32
+ # Storage classes that use asynchronous IO (through an EventMachine
33
+ # enabled library like em-http-request or em-redis) don't need any special
34
+ # consideration and must not use this method.
35
+ def self.defer(method)
36
+ old = instance_method(method)
37
+ define_method method do |*args|
38
+ fiber = Fiber.current
39
+ op = operation { old.bind(self).call(*args) }
40
+ cb = proc {|result| fiber.resume(result) }
41
+ EM.defer(op, cb)
42
+ Fiber.yield
43
+ end
44
+ end
45
+
46
+ # Wrap a method with Fiber yield and resume logic. The method must yield
47
+ # its result to a block. This makes it easier to write asynchronous
48
+ # implementations of +authenticate+, +find_user+, and +save_user+ that
49
+ # block and return a result rather than yielding.
50
+ #
51
+ # For example:
52
+ # def find_user(jid)
53
+ # http = EM::HttpRequest.new(url).get
54
+ # http.callback { yield build_user_from_http_response(http) }
55
+ # end
56
+ # fiber :find_user
57
+ #
58
+ # Because +find_user+ has been wrapped in Fiber logic, we can call it
59
+ # synchronously even though it uses asynchronous EventMachine calls.
60
+ #
61
+ # user = storage.find_user('alice@wonderland.lit')
62
+ # puts user.nil?
63
+ def self.fiber(method)
64
+ old = instance_method(method)
65
+ define_method method do |*args|
66
+ fiber, yielding = Fiber.current, true
67
+ old.bind(self).call(*args) do |user|
68
+ fiber.resume(user) rescue yielding = false
69
+ end
70
+ Fiber.yield if yielding
71
+ end
72
+ end
73
+
74
+ # Validate the username and password pair and return a +Vines::User+ object
75
+ # on success. Return +nil+ on failure.
76
+ #
77
+ # For example:
78
+ # user = storage.authenticate('alice@wonderland.lit', 'secr3t')
79
+ # puts user.nil?
80
+ #
81
+ # This default implementation validates the password against a bcrypt hash
82
+ # of the password stored in the database. Sub-classes not using bcrypt
83
+ # passwords must override this method.
84
+ def authenticate(username, password)
85
+ user = find_user(username)
86
+ hash = BCrypt::Password.new(user.password) rescue nil
87
+ (hash && hash == password) ? user : nil
88
+ end
89
+
90
+ # Return the +Vines::User+ associated with the JID. Return +nil+ if the user
91
+ # could not be found. JID may be +nil+, a +String+, or a +Vines::JID+
92
+ # object. It may be a bare JID or a full JID. Implementations of this method
93
+ # must convert the JID to a bare JID before searching for the user in the
94
+ # database.
95
+ #
96
+ # user = storage.find_user('alice@wonderland.lit')
97
+ # puts user.nil?
98
+ def find_user(jid)
99
+ raise 'subclass must implement'
100
+ end
101
+
102
+ # Persist the +Vines::User+ object to the database and return when the save
103
+ # is complete.
104
+ #
105
+ # alice = Vines::User.new(:jid => 'alice@wonderland.lit')
106
+ # storage.save_user(alice)
107
+ # puts 'saved'
108
+ def save_user(user)
109
+ raise 'subclass must implement'
110
+ end
111
+
112
+ # Return the Nokogiri::XML::Node for the vcard stored for this JID. Return
113
+ # nil if the vcard could not be found. JID may be +nil+, a +String+, or a
114
+ # +Vines::JID+ object. It may be a bare JID or a full JID. Implementations
115
+ # of this method must convert the JID to a bare JID before searching for the
116
+ # vcard in the database.
117
+ #
118
+ # card = storage.find_vcard('alice@wonderland.lit')
119
+ # puts card.nil?
120
+ def find_vcard(jid)
121
+ raise 'subclass must implement'
122
+ end
123
+
124
+ # Save the vcard to the database and return when the save is complete. JID
125
+ # may be a +String+ or a +Vines::JID+ object. It may be a bare JID or a
126
+ # full JID. Implementations of this method must convert the JID to a bare
127
+ # JID before saving the vcard. Card is a +Nokogiri::XML::Node+ object.
128
+ #
129
+ # card = Nokogiri::XML('<vCard>...</vCard>').root
130
+ # storage.save_vcard('alice@wonderland.lit', card)
131
+ # puts 'saved'
132
+ def save_vcard(jid, card)
133
+ raise 'subclass must implement'
134
+ end
135
+
136
+ # Return the Nokogiri::XML::Node for the XML fragment stored for this JID.
137
+ # Return nil if the fragment could not be found. JID may be +nil+, a
138
+ # +String+, or a +Vines::JID+ object. It may be a bare JID or a full JID.
139
+ # Implementations of this method must convert the JID to a bare JID before
140
+ # searching for the fragment in the database.
141
+ #
142
+ # Private XML storage uniquely identifies fragments by JID, root element name,
143
+ # and root element namespace.
144
+ #
145
+ # root = Nokogiri::XML('<custom xmlns="urn:custom:ns"/>').root
146
+ # fragment = storage.find_fragment('alice@wonderland.lit', root)
147
+ # puts fragment.nil?
148
+ def find_fragment(jid, node)
149
+ raise 'subclass must implement'
150
+ end
151
+
152
+ # Save the XML fragment to the database and return when the save is complete.
153
+ # JID may be a +String+ or a +Vines::JID+ object. It may be a bare JID or a
154
+ # full JID. Implementations of this method must convert the JID to a bare
155
+ # JID before saving the fragment. Fragment is a +Nokogiri::XML::Node+ object.
156
+ #
157
+ # fragment = Nokogiri::XML('<custom xmlns="urn:custom:ns">some data</custom>').root
158
+ # storage.save_fragment('alice@wonderland.lit', fragment)
159
+ # puts 'saved'
160
+ def save_fragment(jid, fragment)
161
+ raise 'subclass must implement'
162
+ end
163
+
164
+ private
165
+
166
+ # Return true if any of the arguments are nil or empty strings.
167
+ # For example:
168
+ # username, password = 'alice@wonderland.lit', ''
169
+ # empty?(username, password) #=> true
170
+ def empty?(*args)
171
+ args.flatten.any? {|arg| (arg || '').strip.empty? }
172
+ end
173
+
174
+ # Return a +proc+ suitable for running on the +EM.defer+ thread pool that traps
175
+ # and logs any errors thrown by the provided block.
176
+ def operation
177
+ proc do
178
+ begin
179
+ yield
180
+ rescue => e
181
+ log.error("Thread pool operation failed: #{e.message}")
182
+ nil
183
+ end
184
+ end
185
+ end
186
+
187
+ end
188
+ end