vines 0.1.1 → 0.2.0

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 (83) hide show
  1. data/README +2 -2
  2. data/Rakefile +63 -8
  3. data/bin/vines +0 -1
  4. data/conf/config.rb +16 -7
  5. data/lib/vines.rb +21 -16
  6. data/lib/vines/command/init.rb +5 -3
  7. data/lib/vines/config.rb +34 -0
  8. data/lib/vines/contact.rb +14 -0
  9. data/lib/vines/stanza.rb +26 -0
  10. data/lib/vines/stanza/iq.rb +1 -1
  11. data/lib/vines/stanza/iq/disco_info.rb +3 -0
  12. data/lib/vines/stanza/iq/private_storage.rb +83 -0
  13. data/lib/vines/stanza/iq/roster.rb +26 -30
  14. data/lib/vines/stanza/presence.rb +0 -12
  15. data/lib/vines/stanza/presence/subscribe.rb +3 -20
  16. data/lib/vines/stanza/presence/subscribed.rb +9 -10
  17. data/lib/vines/stanza/presence/unsubscribe.rb +8 -15
  18. data/lib/vines/stanza/presence/unsubscribed.rb +8 -8
  19. data/lib/vines/storage.rb +28 -0
  20. data/lib/vines/storage/couchdb.rb +29 -0
  21. data/lib/vines/storage/local.rb +22 -0
  22. data/lib/vines/storage/redis.rb +26 -0
  23. data/lib/vines/storage/sql.rb +48 -5
  24. data/lib/vines/stream/client.rb +6 -8
  25. data/lib/vines/stream/http.rb +23 -21
  26. data/lib/vines/stream/http/auth.rb +1 -1
  27. data/lib/vines/stream/http/bind.rb +1 -1
  28. data/lib/vines/stream/http/bind_restart.rb +4 -3
  29. data/lib/vines/stream/http/ready.rb +1 -1
  30. data/lib/vines/stream/http/request.rb +94 -5
  31. data/lib/vines/stream/http/session.rb +8 -6
  32. data/lib/vines/version.rb +1 -1
  33. data/test/config_test.rb +12 -0
  34. data/test/contact_test.rb +40 -0
  35. data/test/rake_test_loader.rb +11 -3
  36. data/test/stanza/iq/private_storage_test.rb +177 -0
  37. data/test/stanza/iq/roster_test.rb +1 -1
  38. data/test/stanza/iq_test.rb +63 -0
  39. data/test/storage/couchdb_test.rb +7 -1
  40. data/test/storage/local_test.rb +8 -2
  41. data/test/storage/redis_test.rb +16 -7
  42. data/test/storage/sql_test.rb +8 -1
  43. data/test/storage/storage_tests.rb +50 -0
  44. data/test/stream/http/auth_test.rb +3 -0
  45. data/test/stream/http/ready_test.rb +3 -0
  46. data/test/stream/http/request_test.rb +86 -0
  47. data/test/stream/parser_test.rb +2 -0
  48. data/web/404.html +43 -0
  49. data/web/apple-touch-icon.png +0 -0
  50. data/web/chat/coffeescripts/chat.coffee +385 -0
  51. data/web/chat/coffeescripts/init.coffee +15 -0
  52. data/web/chat/coffeescripts/logout.coffee +5 -0
  53. data/web/chat/index.html +17 -0
  54. data/web/chat/javascripts/app.js +1 -0
  55. data/web/chat/javascripts/chat.js +436 -0
  56. data/web/chat/javascripts/init.js +21 -0
  57. data/web/chat/javascripts/logout.js +11 -0
  58. data/web/chat/stylesheets/chat.css +290 -0
  59. data/web/favicon.png +0 -0
  60. data/web/lib/coffeescripts/contact.coffee +32 -0
  61. data/web/lib/coffeescripts/layout.coffee +30 -0
  62. data/web/lib/coffeescripts/login.coffee +52 -0
  63. data/web/lib/coffeescripts/navbar.coffee +84 -0
  64. data/web/lib/coffeescripts/router.coffee +40 -0
  65. data/web/lib/coffeescripts/session.coffee +211 -0
  66. data/web/lib/images/default-user.png +0 -0
  67. data/web/lib/images/logo-large.png +0 -0
  68. data/web/lib/images/logo-small.png +0 -0
  69. data/web/lib/javascripts/base.js +9 -0
  70. data/web/lib/javascripts/contact.js +94 -0
  71. data/web/lib/javascripts/icons.js +101 -0
  72. data/web/lib/javascripts/jquery.cookie.js +91 -0
  73. data/web/lib/javascripts/jquery.js +18 -0
  74. data/web/lib/javascripts/layout.js +48 -0
  75. data/web/lib/javascripts/login.js +61 -0
  76. data/web/lib/javascripts/navbar.js +69 -0
  77. data/web/lib/javascripts/raphael.js +8 -0
  78. data/web/lib/javascripts/router.js +105 -0
  79. data/web/lib/javascripts/session.js +322 -0
  80. data/web/lib/javascripts/strophe.js +1 -0
  81. data/web/lib/stylesheets/base.css +223 -0
  82. data/web/lib/stylesheets/login.css +63 -0
  83. metadata +51 -9
@@ -77,13 +77,8 @@ module Vines
77
77
  raise StanzaErrors::ItemNotFound.new(self, 'modify') unless contact
78
78
  if router.local_jid?(contact.jid)
79
79
  user = storage(contact.jid.domain).find_user(contact.jid)
80
- remove(contact, user)
81
- else
82
- remove(contact, nil)
83
80
  end
84
- end
85
81
 
86
- def remove(contact, user=nil)
87
82
  if user && user.contact(stream.user.jid)
88
83
  user.contact(stream.user.jid).subscription = 'none'
89
84
  user.contact(stream.user.jid).ask = nil
@@ -93,52 +88,53 @@ module Vines
93
88
  storage(save.jid.domain).save_user(save)
94
89
  stream.update_user_streams(save)
95
90
  end
91
+
96
92
  send_result_iq
97
93
  push_roster_updates(stream.user.jid, Contact.new(
98
94
  :jid => contact.jid,
99
95
  :subscription => 'remove'))
100
96
 
101
- presence = [%w[to unsubscribe], %w[from unsubscribed]].map do |meth, type|
102
- if contact.send("subscribed_#{meth}?")
103
- doc = Document.new
104
- doc.create_element('presence',
105
- 'from' => stream.user.jid.bare.to_s,
106
- 'to' => contact.jid.to_s,
107
- 'type' => type)
97
+ if router.local_jid?(contact.jid)
98
+ send_unavailable(stream.user.jid, contact.jid) if contact.subscribed_from?
99
+ send_unsubscribe(contact)
100
+ if user.contact(stream.user.jid)
101
+ push_roster_updates(contact.jid, user.contact(stream.user.jid))
108
102
  end
103
+ else
104
+ send_unsubscribe(contact)
105
+ end
106
+ end
107
+
108
+ # Notify the contact that it's been removed from the user's roster
109
+ # and no longer has any presence relationship with the user.
110
+ def send_unsubscribe(contact)
111
+ presence = [%w[to unsubscribe], %w[from unsubscribed]].map do |meth, type|
112
+ presence(contact.jid, type) if contact.send("subscribed_#{meth}?")
109
113
  end.compact
110
114
 
111
115
  if router.local_jid?(contact.jid)
112
116
  router.interested_resources(contact.jid).each do |recipient|
113
117
  presence.each {|el| recipient.write(el) }
114
- doc = Document.new
115
- el = doc.create_element('presence',
116
- 'from' => stream.user.jid.bare.to_s,
117
- 'to' => recipient.user.jid.to_s,
118
- 'type' => 'unavailable')
119
- recipient.write(el)
120
118
  end
121
- push_roster_updates(contact.jid, Contact.new(
122
- :jid => stream.user.jid,
123
- :subscription => 'none'))
124
119
  else
125
120
  presence.each {|el| router.route(el) }
126
121
  end
127
122
  end
128
123
 
124
+ def presence(to, type)
125
+ doc = Document.new
126
+ doc.create_element('presence',
127
+ 'from' => stream.user.jid.bare.to_s,
128
+ 'id' => Kit.uuid,
129
+ 'to' => to.to_s,
130
+ 'type' => type)
131
+ end
132
+
129
133
  # Send an iq set stanza to the user's interested resources, letting them
130
134
  # know their roster has been updated.
131
135
  def push_roster_updates(to, contact)
132
- doc = Document.new
133
- el = doc.create_element('iq', 'type' => 'set') do |node|
134
- node << doc.create_element('query', 'xmlns' => NAMESPACES[:roster]) do |query|
135
- query << contact.to_roster_xml
136
- end
137
- end
138
136
  router.interested_resources(to).each do |recipient|
139
- el['id'] = Kit.uuid
140
- el['to'] = recipient.user.jid.to_s
141
- recipient.write(el)
137
+ contact.send_roster_push(recipient)
142
138
  end
143
139
  end
144
140
 
@@ -81,18 +81,6 @@ module Vines
81
81
  router.route(probe)
82
82
  end
83
83
 
84
- def send_subscribed_roster_push(recipient, jid, state)
85
- doc = Document.new
86
- node = doc.create_element('iq',
87
- 'id' => Kit.uuid,
88
- 'to' => recipient.user.jid.to_s,
89
- 'type' => 'set')
90
- node << doc.create_element('query', 'xmlns' => NAMESPACES[:roster]) do |query|
91
- query << doc.create_element('item', 'jid' => jid.to_s, 'subscription' => state)
92
- end
93
- recipient.write(node)
94
- end
95
-
96
84
  def auto_reply_to_subscription_request(from, type)
97
85
  doc = Document.new
98
86
  node = doc.create_element('presence') do |el|
@@ -13,16 +13,15 @@ module Vines
13
13
  def process_outbound
14
14
  self['from'] = stream.user.jid.bare.to_s
15
15
  to = stamp_to
16
- route unless local?
16
+ local? ? process_inbound : route
17
17
 
18
18
  stream.user.request_subscription(to)
19
19
  storage.save_user(stream.user)
20
20
  stream.update_user_streams(stream.user)
21
21
 
22
- process_inbound if local?
23
-
22
+ contact = stream.user.contact(to)
24
23
  router.interested_resources(stream.user.jid).each do |recipient|
25
- send_subscribe_roster_push(recipient, stream.user.contact(to))
24
+ contact.send_roster_push(recipient)
26
25
  end
27
26
  end
28
27
 
@@ -44,22 +43,6 @@ module Vines
44
43
  end
45
44
  end
46
45
  end
47
-
48
- private
49
-
50
- def send_subscribe_roster_push(recipient, contact)
51
- doc = Document.new
52
- node = doc.create_element('iq') do |el|
53
- el['id'] = Kit.uuid
54
- el['to'] = recipient.user.jid.to_s
55
- el['type'] = 'set'
56
- el << doc.create_element('query') do |query|
57
- query.default_namespace = NAMESPACES[:roster]
58
- query << contact.to_roster_xml
59
- end
60
- end
61
- recipient.write(node)
62
- end
63
46
  end
64
47
  end
65
48
  end
@@ -13,22 +13,23 @@ module Vines
13
13
  def process_outbound
14
14
  self['from'] = stream.user.jid.bare.to_s
15
15
  to = stamp_to
16
- route unless local?
16
+ local? ? process_inbound : route
17
17
 
18
18
  stream.user.add_subscription_from(to)
19
19
  storage.save_user(stream.user)
20
20
  stream.update_user_streams(stream.user)
21
21
 
22
+ contact = stream.user.contact(to)
22
23
  router.interested_resources(stream.user.jid).each do |recipient|
23
- send_subscribed_roster_push(recipient, to, stream.user.contact(to).subscription)
24
+ contact.send_roster_push(recipient)
24
25
  end
25
26
 
26
27
  presences = router.available_resources(stream.user.jid).map do |c|
27
- doc = Document.new
28
- doc.create_element('presence',
29
- 'from' => c.user.jid.to_s,
30
- 'id' => Kit.uuid,
31
- 'to' => to.to_s)
28
+ c.last_broadcast_presence.clone.tap do |node|
29
+ node['from'] = c.user.jid.to_s
30
+ node['id'] = Kit.uuid
31
+ node['to'] = to.to_s
32
+ end
32
33
  end
33
34
 
34
35
  if local?
@@ -38,8 +39,6 @@ module Vines
38
39
  else
39
40
  presences.each {|el| router.route(el) }
40
41
  end
41
-
42
- process_inbound if local?
43
42
  end
44
43
 
45
44
  def process_inbound
@@ -55,7 +54,7 @@ module Vines
55
54
 
56
55
  router.interested_resources(to).each do |recipient|
57
56
  recipient.write(@node)
58
- send_subscribed_roster_push(recipient, stream.user.jid.bare, contact.subscription)
57
+ contact.send_roster_push(recipient)
59
58
  end
60
59
  end
61
60
  end
@@ -13,18 +13,17 @@ module Vines
13
13
  def process_outbound
14
14
  self['from'] = stream.user.jid.bare.to_s
15
15
  to = stamp_to
16
- route unless local?
16
+
17
+ return unless stream.user.subscribed_to?(to)
18
+ local? ? process_inbound : route
17
19
 
18
20
  stream.user.remove_subscription_to(to)
19
21
  storage.save_user(stream.user)
20
22
  stream.update_user_streams(stream.user)
21
23
 
22
- process_inbound if local?
23
-
24
- if contact = stream.user.contact(to)
25
- router.interested_resources(stream.user.jid).each do |recipient|
26
- send_subscribed_roster_push(recipient, to, contact.subscription)
27
- end
24
+ contact = stream.user.contact(to)
25
+ router.interested_resources(stream.user.jid).each do |recipient|
26
+ contact.send_roster_push(recipient)
28
27
  end
29
28
  end
30
29
 
@@ -41,15 +40,9 @@ module Vines
41
40
 
42
41
  router.interested_resources(to).each do |recipient|
43
42
  recipient.write(@node)
44
- send_subscribed_roster_push(recipient, stream.user.jid.bare, contact.subscription)
45
- doc = Document.new
46
- el = doc.create_element('presence',
47
- 'from' => stream.user.jid.bare.to_s,
48
- 'id' => Kit.uuid,
49
- 'to' => recipient.user.jid.to_s,
50
- 'type' => 'unavailable')
51
- recipient.write(el)
43
+ contact.send_roster_push(recipient)
52
44
  end
45
+ send_unavailable(to, stream.user.jid.bare)
53
46
  end
54
47
  end
55
48
  end
@@ -13,19 +13,19 @@ module Vines
13
13
  def process_outbound
14
14
  self['from'] = stream.user.jid.bare.to_s
15
15
  to = stamp_to
16
- route unless local?
16
+
17
+ return unless stream.user.subscribed_from?(to)
18
+ send_unavailable(stream.user.jid, to)
19
+ local? ? process_inbound : route
17
20
 
18
21
  stream.user.remove_subscription_from(to)
19
22
  storage.save_user(stream.user)
20
23
  stream.update_user_streams(stream.user)
21
24
 
22
- if contact = stream.user.contact(to)
23
- router.interested_resources(stream.user.jid).each do |recipient|
24
- send_subscribed_roster_push(recipient, to, contact.subscription)
25
- end
25
+ contact = stream.user.contact(to)
26
+ router.interested_resources(stream.user.jid).each do |recipient|
27
+ contact.send_roster_push(recipient)
26
28
  end
27
-
28
- process_inbound if local?
29
29
  end
30
30
 
31
31
  def process_inbound
@@ -41,7 +41,7 @@ module Vines
41
41
 
42
42
  router.interested_resources(to).each do |recipient|
43
43
  recipient.write(@node)
44
- send_subscribed_roster_push(recipient, stream.user.jid.bare, contact.subscription)
44
+ contact.send_roster_push(recipient)
45
45
  end
46
46
  end
47
47
  end
@@ -168,6 +168,34 @@ module Vines
168
168
  raise 'subclass must implement'
169
169
  end
170
170
 
171
+ # Return the Nokogiri::XML::Node for the XML fragment stored for this JID.
172
+ # Return nil if the fragment could not be found. JID may be +nil+, a
173
+ # +String+, or a +Vines::JID+ object. It may be a bare JID or a full JID.
174
+ # Implementations of this method must convert the JID to a bare JID before
175
+ # searching for the fragment in the database.
176
+ #
177
+ # Private XML storage uniquely identifies fragments by JID, root element name,
178
+ # and root element namespace.
179
+ #
180
+ # root = Nokogiri::XML('<custom xmlns="urn:custom:ns"/>').root
181
+ # fragment = storage.find_fragment('alice@wonderland.lit', root)
182
+ # puts fragment.nil?
183
+ def find_fragment(jid, node)
184
+ raise 'subclass must implement'
185
+ end
186
+
187
+ # Save the XML fragment to the database and return when the save is complete.
188
+ # JID may be a +String+ or a +Vines::JID+ object. It may be a bare JID or a
189
+ # full JID. Implementations of this method must convert the JID to a bare
190
+ # JID before saving the fragment. Fragment is a +Nokogiri::XML::Node+ object.
191
+ #
192
+ # fragment = Nokogiri::XML('<custom xmlns="urn:custom:ns">some data</custom>').root
193
+ # storage.save_fragment('alice@wonderland.lit', fragment)
194
+ # puts 'saved'
195
+ def save_fragment(jid, fragment)
196
+ raise 'subclass must implement'
197
+ end
198
+
171
199
  private
172
200
 
173
201
  # Return true if any of the arguments are nil or empty strings.
@@ -84,8 +84,37 @@ module Vines
84
84
  end
85
85
  fiber :save_vcard
86
86
 
87
+ def find_fragment(jid, node)
88
+ jid = JID.new(jid || '').bare.to_s
89
+ if jid.empty? then yield; return end
90
+ get(fragment_id(jid, node)) do |doc|
91
+ fragment = if doc && doc['type'] == 'Fragment'
92
+ Nokogiri::XML(doc['xml']).root rescue nil
93
+ end
94
+ yield fragment
95
+ end
96
+ end
97
+ fiber :find_fragment
98
+
99
+ def save_fragment(jid, node, &callback)
100
+ jid = JID.new(jid).bare.to_s
101
+ id = fragment_id(jid, node)
102
+ get(id) do |doc|
103
+ doc ||= {'_id' => id}
104
+ doc['type'] = 'Fragment'
105
+ doc['xml'] = node.to_xml
106
+ save_doc(doc, &callback)
107
+ end
108
+ end
109
+ fiber :save_fragment
110
+
87
111
  private
88
112
 
113
+ def fragment_id(jid, node)
114
+ id = Digest::SHA1.hexdigest("#{node.name}:#{node.namespace.href}")
115
+ "fragment:#{jid}:#{id}"
116
+ end
117
+
89
118
  def url(config)
90
119
  scheme = config[:tls] ? 'https' : 'http'
91
120
  user, password = config.values_at(:username, :password)
@@ -61,6 +61,28 @@ module Vines
61
61
  f.write(card.to_xml)
62
62
  end
63
63
  end
64
+
65
+ def find_fragment(jid, node)
66
+ jid = JID.new(jid || '').bare.to_s
67
+ return if jid.empty?
68
+ file = File.join(@dir, fragment_id(jid, node))
69
+ Nokogiri::XML(File.read(file)).root rescue nil
70
+ end
71
+
72
+ def save_fragment(jid, node)
73
+ jid = JID.new(jid).bare.to_s
74
+ file = File.join(@dir, fragment_id(jid, node))
75
+ File.open(file, 'w') do |f|
76
+ f.write(node.to_xml)
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def fragment_id(jid, node)
83
+ id = Digest::SHA1.hexdigest("#{node.name}:#{node.namespace.href}")
84
+ "#{jid}-#{id}.fragment"
85
+ end
64
86
  end
65
87
  end
66
88
  end
@@ -74,8 +74,34 @@ module Vines
74
74
  end
75
75
  fiber :save_vcard
76
76
 
77
+ def find_fragment(jid, node)
78
+ jid = JID.new(jid || '').bare.to_s
79
+ if jid.empty? then yield; return end
80
+ redis.hget("fragments:#{jid}", fragment_id(node)) do |response|
81
+ fragment = if response
82
+ doc = JSON.parse(response) rescue nil
83
+ Nokogiri::XML(doc['xml']).root rescue nil
84
+ end
85
+ yield fragment
86
+ end
87
+ end
88
+ fiber :find_fragment
89
+
90
+ def save_fragment(jid, node)
91
+ jid = JID.new(jid).bare.to_s
92
+ doc = {:xml => node.to_xml}
93
+ redis.hset("fragments:#{jid}", fragment_id(node), doc.to_json) do
94
+ yield
95
+ end
96
+ end
97
+ fiber :save_fragment
98
+
77
99
  private
78
100
 
101
+ def fragment_id(node)
102
+ Digest::SHA1.hexdigest("#{node.name}:#{node.namespace.href}")
103
+ end
104
+
79
105
  # Retrieve the hash stored at roster:jid and yield an array of
80
106
  # +Vines::Contact+ objects to the provided block.
81
107
  #
@@ -8,9 +8,13 @@ module Vines
8
8
  class Contact < ActiveRecord::Base
9
9
  belongs_to :user
10
10
  end
11
+ class Fragment < ActiveRecord::Base
12
+ belongs_to :user
13
+ end
11
14
  class Group < ActiveRecord::Base; end
12
15
  class User < ActiveRecord::Base
13
16
  has_many :contacts
17
+ has_many :fragments
14
18
  end
15
19
 
16
20
  %w[adapter host port database username password pool].each do |name|
@@ -38,7 +42,7 @@ module Vines
38
42
 
39
43
  jid = JID.new(jid || '').bare.to_s
40
44
  return if jid.empty?
41
- xuser = by_jid(jid)
45
+ xuser = user_by_jid(jid)
42
46
  return Vines::User.new(:jid => jid).tap do |user|
43
47
  user.name, user.password = xuser.name, xuser.password
44
48
  xuser.contacts.each do |contact|
@@ -57,7 +61,7 @@ module Vines
57
61
  def save_user(user)
58
62
  ActiveRecord::Base.clear_reloadable_connections!
59
63
 
60
- xuser = by_jid(user.jid) || Sql::User.new(:jid => user.jid.bare.to_s)
64
+ xuser = user_by_jid(user.jid) || Sql::User.new(:jid => user.jid.bare.to_s)
61
65
  xuser.name = user.name
62
66
  xuser.password = user.password
63
67
 
@@ -97,7 +101,7 @@ module Vines
97
101
 
98
102
  jid = JID.new(jid || '').bare.to_s
99
103
  return if jid.empty?
100
- if xuser = by_jid(jid)
104
+ if xuser = user_by_jid(jid)
101
105
  Nokogiri::XML(xuser.vcard).root rescue nil
102
106
  end
103
107
  end
@@ -106,7 +110,7 @@ module Vines
106
110
  def save_vcard(jid, card)
107
111
  ActiveRecord::Base.clear_reloadable_connections!
108
112
 
109
- xuser = by_jid(jid)
113
+ xuser = user_by_jid(jid)
110
114
  if xuser
111
115
  xuser.vcard = card.to_xml
112
116
  xuser.save
@@ -114,6 +118,31 @@ module Vines
114
118
  end
115
119
  defer :save_vcard
116
120
 
121
+ def find_fragment(jid, node)
122
+ ActiveRecord::Base.clear_reloadable_connections!
123
+
124
+ jid = JID.new(jid || '').bare.to_s
125
+ return if jid.empty?
126
+ if fragment = fragment_by_jid(jid, node)
127
+ Nokogiri::XML(fragment.xml).root rescue nil
128
+ end
129
+ end
130
+ defer :find_fragment
131
+
132
+ def save_fragment(jid, node)
133
+ ActiveRecord::Base.clear_reloadable_connections!
134
+
135
+ jid = JID.new(jid).bare.to_s
136
+ fragment = fragment_by_jid(jid, node) ||
137
+ Sql::Fragment.new(
138
+ :user => user_by_jid(jid),
139
+ :root => node.name,
140
+ :namespace => node.namespace.href)
141
+ fragment.xml = node.to_xml
142
+ fragment.save
143
+ end
144
+ defer :save_fragment
145
+
117
146
  # Create the tables and indexes used by this storage engine.
118
147
  def create_schema(args={})
119
148
  ActiveRecord::Base.clear_reloadable_connections!
@@ -148,6 +177,14 @@ module Vines
148
177
  t.integer :group_id, :null => false
149
178
  end
150
179
  add_index :contacts_groups, [:contact_id, :group_id], :unique => true
180
+
181
+ create_table :fragments, :force => args[:force] do |t|
182
+ t.integer :user_id, :null => false
183
+ t.string :root, :limit => 1000, :null => false
184
+ t.string :namespace, :limit => 1000, :null => false
185
+ t.text :xml, :null => false
186
+ end
187
+ add_index :fragments, [:user_id, :root, :namespace], :unique => true
151
188
  end
152
189
  end
153
190
 
@@ -161,11 +198,17 @@ module Vines
161
198
  Sql::Group.has_and_belongs_to_many :contacts
162
199
  end
163
200
 
164
- def by_jid(jid)
201
+ def user_by_jid(jid)
165
202
  jid = JID.new(jid).bare.to_s
166
203
  Sql::User.find_by_jid(jid, :include => {:contacts => :groups})
167
204
  end
168
205
 
206
+ def fragment_by_jid(jid, node)
207
+ jid = JID.new(jid).bare.to_s
208
+ clause = 'user_id=(select id from users where jid=?) and root=? and namespace=?'
209
+ Sql::Fragment.where(clause, jid, node.name, node.namespace.href).first
210
+ end
211
+
169
212
  def groups(contact)
170
213
  contact.groups.map {|name| Sql::Group.find_or_create_by_name(name.strip) }
171
214
  end