vines 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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