vines 0.1.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 (119) hide show
  1. data/LICENSE +19 -0
  2. data/README +34 -0
  3. data/Rakefile +55 -0
  4. data/bin/vines +95 -0
  5. data/conf/certs/README +32 -0
  6. data/conf/certs/ca-bundle.crt +3987 -0
  7. data/conf/config.rb +114 -0
  8. data/lib/vines.rb +155 -0
  9. data/lib/vines/command/bcrypt.rb +12 -0
  10. data/lib/vines/command/cert.rb +49 -0
  11. data/lib/vines/command/init.rb +58 -0
  12. data/lib/vines/command/ldap.rb +35 -0
  13. data/lib/vines/command/restart.rb +12 -0
  14. data/lib/vines/command/schema.rb +24 -0
  15. data/lib/vines/command/start.rb +28 -0
  16. data/lib/vines/command/stop.rb +18 -0
  17. data/lib/vines/config.rb +191 -0
  18. data/lib/vines/contact.rb +99 -0
  19. data/lib/vines/daemon.rb +78 -0
  20. data/lib/vines/error.rb +150 -0
  21. data/lib/vines/jid.rb +56 -0
  22. data/lib/vines/kit.rb +23 -0
  23. data/lib/vines/router.rb +125 -0
  24. data/lib/vines/stanza.rb +55 -0
  25. data/lib/vines/stanza/iq.rb +50 -0
  26. data/lib/vines/stanza/iq/auth.rb +18 -0
  27. data/lib/vines/stanza/iq/disco_info.rb +25 -0
  28. data/lib/vines/stanza/iq/disco_items.rb +23 -0
  29. data/lib/vines/stanza/iq/error.rb +16 -0
  30. data/lib/vines/stanza/iq/ping.rb +16 -0
  31. data/lib/vines/stanza/iq/query.rb +10 -0
  32. data/lib/vines/stanza/iq/result.rb +16 -0
  33. data/lib/vines/stanza/iq/roster.rb +153 -0
  34. data/lib/vines/stanza/iq/session.rb +22 -0
  35. data/lib/vines/stanza/iq/vcard.rb +58 -0
  36. data/lib/vines/stanza/message.rb +41 -0
  37. data/lib/vines/stanza/presence.rb +119 -0
  38. data/lib/vines/stanza/presence/error.rb +23 -0
  39. data/lib/vines/stanza/presence/probe.rb +38 -0
  40. data/lib/vines/stanza/presence/subscribe.rb +66 -0
  41. data/lib/vines/stanza/presence/subscribed.rb +64 -0
  42. data/lib/vines/stanza/presence/unavailable.rb +15 -0
  43. data/lib/vines/stanza/presence/unsubscribe.rb +57 -0
  44. data/lib/vines/stanza/presence/unsubscribed.rb +50 -0
  45. data/lib/vines/storage.rb +216 -0
  46. data/lib/vines/storage/couchdb.rb +119 -0
  47. data/lib/vines/storage/ldap.rb +59 -0
  48. data/lib/vines/storage/local.rb +66 -0
  49. data/lib/vines/storage/redis.rb +108 -0
  50. data/lib/vines/storage/sql.rb +174 -0
  51. data/lib/vines/store.rb +51 -0
  52. data/lib/vines/stream.rb +198 -0
  53. data/lib/vines/stream/client.rb +131 -0
  54. data/lib/vines/stream/client/auth.rb +94 -0
  55. data/lib/vines/stream/client/auth_restart.rb +33 -0
  56. data/lib/vines/stream/client/bind.rb +58 -0
  57. data/lib/vines/stream/client/bind_restart.rb +25 -0
  58. data/lib/vines/stream/client/closed.rb +13 -0
  59. data/lib/vines/stream/client/ready.rb +15 -0
  60. data/lib/vines/stream/client/start.rb +27 -0
  61. data/lib/vines/stream/client/tls.rb +37 -0
  62. data/lib/vines/stream/component.rb +53 -0
  63. data/lib/vines/stream/component/handshake.rb +25 -0
  64. data/lib/vines/stream/component/ready.rb +24 -0
  65. data/lib/vines/stream/component/start.rb +19 -0
  66. data/lib/vines/stream/http.rb +111 -0
  67. data/lib/vines/stream/http/http_request.rb +22 -0
  68. data/lib/vines/stream/http/http_state.rb +139 -0
  69. data/lib/vines/stream/http/http_states.rb +53 -0
  70. data/lib/vines/stream/parser.rb +78 -0
  71. data/lib/vines/stream/server.rb +126 -0
  72. data/lib/vines/stream/server/auth.rb +13 -0
  73. data/lib/vines/stream/server/auth_restart.rb +19 -0
  74. data/lib/vines/stream/server/final_restart.rb +20 -0
  75. data/lib/vines/stream/server/outbound/auth.rb +31 -0
  76. data/lib/vines/stream/server/outbound/auth_restart.rb +20 -0
  77. data/lib/vines/stream/server/outbound/auth_result.rb +28 -0
  78. data/lib/vines/stream/server/outbound/final_features.rb +27 -0
  79. data/lib/vines/stream/server/outbound/final_restart.rb +20 -0
  80. data/lib/vines/stream/server/outbound/start.rb +20 -0
  81. data/lib/vines/stream/server/outbound/tls.rb +30 -0
  82. data/lib/vines/stream/server/outbound/tls_result.rb +31 -0
  83. data/lib/vines/stream/server/ready.rb +20 -0
  84. data/lib/vines/stream/server/start.rb +13 -0
  85. data/lib/vines/stream/server/tls.rb +13 -0
  86. data/lib/vines/stream/state.rb +55 -0
  87. data/lib/vines/token_bucket.rb +46 -0
  88. data/lib/vines/user.rb +124 -0
  89. data/lib/vines/version.rb +5 -0
  90. data/lib/vines/xmpp_server.rb +25 -0
  91. data/test/config_test.rb +396 -0
  92. data/test/error_test.rb +59 -0
  93. data/test/ext/nokogiri.rb +14 -0
  94. data/test/jid_test.rb +71 -0
  95. data/test/kit_test.rb +21 -0
  96. data/test/router_test.rb +60 -0
  97. data/test/stanza/iq/roster_test.rb +198 -0
  98. data/test/stanza/iq/session_test.rb +30 -0
  99. data/test/stanza/iq/vcard_test.rb +159 -0
  100. data/test/stanza/message_test.rb +124 -0
  101. data/test/stanza/presence/subscribe_test.rb +75 -0
  102. data/test/storage/couchdb_test.rb +102 -0
  103. data/test/storage/ldap_test.rb +207 -0
  104. data/test/storage/local_test.rb +54 -0
  105. data/test/storage/redis_test.rb +75 -0
  106. data/test/storage/sql_test.rb +55 -0
  107. data/test/storage/storage_tests.rb +134 -0
  108. data/test/storage_test.rb +90 -0
  109. data/test/stream/client/auth_test.rb +127 -0
  110. data/test/stream/client/ready_test.rb +47 -0
  111. data/test/stream/component/handshake_test.rb +46 -0
  112. data/test/stream/component/ready_test.rb +105 -0
  113. data/test/stream/component/start_test.rb +41 -0
  114. data/test/stream/parser_test.rb +121 -0
  115. data/test/stream/server/outbound/auth_test.rb +77 -0
  116. data/test/stream/server/ready_test.rb +100 -0
  117. data/test/token_bucket_test.rb +24 -0
  118. data/test/user_test.rb +64 -0
  119. metadata +318 -0
@@ -0,0 +1,119 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Storage
5
+ class CouchDB < Storage
6
+ register :couchdb
7
+
8
+ %w[host port database tls username password].each do |name|
9
+ define_method(name) do |*args|
10
+ if args.first
11
+ @config[name.to_sym] = args.first
12
+ else
13
+ @config[name.to_sym]
14
+ end
15
+ end
16
+ end
17
+
18
+ def initialize(&block)
19
+ @config = {}
20
+ instance_eval(&block)
21
+ [:host, :port, :database].each {|key| raise "Must provide #{key}" unless @config[key] }
22
+ @url = url(@config)
23
+ end
24
+
25
+ def find_user(jid)
26
+ jid = JID.new(jid || '').bare.to_s
27
+ if jid.empty? then yield; return end
28
+ get("user:#{jid}") do |doc|
29
+ user = if doc && doc['type'] == 'User'
30
+ User.new(:jid => jid).tap do |user|
31
+ user.name, user.password = doc.values_at('name', 'password')
32
+ (doc['roster'] || {}).each_pair do |jid, props|
33
+ user.roster << Contact.new(
34
+ :jid => jid,
35
+ :name => props['name'],
36
+ :subscription => props['subscription'],
37
+ :ask => props['ask'],
38
+ :groups => props['groups'] || [])
39
+ end
40
+ end
41
+ end
42
+ yield user
43
+ end
44
+ end
45
+ fiber :find_user
46
+
47
+ def save_user(user, &callback)
48
+ id = "user:#{user.jid.bare}"
49
+ get(id) do |doc|
50
+ doc ||= {'_id' => id}
51
+ doc['type'] = 'User'
52
+ doc['name'] = user.name
53
+ doc['password'] = user.password
54
+ doc['roster'] = {}
55
+ user.roster.each do |contact|
56
+ doc['roster'][contact.jid.bare.to_s] = contact.to_h
57
+ end
58
+ save_doc(doc, &callback)
59
+ end
60
+ end
61
+ fiber :save_user
62
+
63
+ def find_vcard(jid)
64
+ jid = JID.new(jid || '').bare.to_s
65
+ if jid.empty? then yield; return end
66
+ get("vcard:#{jid}") do |doc|
67
+ card = if doc && doc['type'] == 'Vcard'
68
+ Nokogiri::XML(doc['card']).root rescue nil
69
+ end
70
+ yield card
71
+ end
72
+ end
73
+ fiber :find_vcard
74
+
75
+ def save_vcard(jid, card, &callback)
76
+ jid = JID.new(jid).bare.to_s
77
+ id = "vcard:#{jid}"
78
+ get(id) do |doc|
79
+ doc ||= {'_id' => id}
80
+ doc['type'] = 'Vcard'
81
+ doc['card'] = card.to_xml
82
+ save_doc(doc, &callback)
83
+ end
84
+ end
85
+ fiber :save_vcard
86
+
87
+ private
88
+
89
+ def url(config)
90
+ scheme = config[:tls] ? 'https' : 'http'
91
+ user, password = config.values_at(:username, :password)
92
+ credentials = empty?(user, password) ? '' : "%s:%s@" % [user, password]
93
+ "%s://%s%s:%s/%s" % [scheme, credentials, *config.values_at(:host, :port, :database)]
94
+ end
95
+
96
+ def get(jid)
97
+ http = EM::HttpRequest.new("#{@url}/#{escape(jid)}").get
98
+ http.errback { yield }
99
+ http.callback do
100
+ doc = if http.response_header.status == 200
101
+ JSON.parse(http.response) rescue nil
102
+ end
103
+ yield doc
104
+ end
105
+ end
106
+
107
+ def save_doc(doc, &callback)
108
+ http = EM::HttpRequest.new(@url).post(
109
+ :head => {'Content-Type' => 'application/json'}, :body => doc.to_json)
110
+ http.callback(&callback) if callback
111
+ http.errback(&callback) if callback
112
+ end
113
+
114
+ def escape(jid)
115
+ URI.escape(jid, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,59 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Storage
5
+
6
+ # Authenticates usernames and passwords against an LDAP directory. This can
7
+ # provide authentication logic for the other, full-featured Storage
8
+ # implementations while they store and retrieve the rest of the user
9
+ # information.
10
+ class Ldap
11
+ @@required = [:host, :port]
12
+ %w[tls dn password basedn object_class user_attr name_attr].each do |name|
13
+ @@required << name.to_sym
14
+ define_method name do |*args|
15
+ @config[name.to_sym] = args.first
16
+ end
17
+ end
18
+
19
+ def initialize(host='localhost', port=636, &block)
20
+ @config = {:host => host, :port => port}
21
+ instance_eval(&block)
22
+ @@required.each {|key| raise "Must provide #{key}" if @config[key].nil? }
23
+ end
24
+
25
+ # Validates a username and password by binding to the LDAP instance with
26
+ # those credentials. If the bind succeeds, the user's attributes are
27
+ # retrieved.
28
+ def authenticate(username, password)
29
+ return if [username, password].any? {|arg| (arg || '').strip.empty? }
30
+
31
+ clas = Net::LDAP::Filter.eq('objectClass', @config[:object_class])
32
+ uid = Net::LDAP::Filter.eq(@config[:user_attr], username)
33
+ filter = clas & uid
34
+ attrs = [@config[:name_attr], 'mail']
35
+
36
+ ldap = connect(@config[:dn], @config[:password])
37
+ entries = ldap.search(:attributes => attrs, :filter => filter)
38
+ return unless entries && entries.size == 1
39
+
40
+ user = if connect(entries.first.dn, password).bind
41
+ name = entries.first[@config[:name_attr]].first
42
+ User.new(:jid => username, :name => name.to_s, :roster => [])
43
+ end
44
+ user
45
+ end
46
+
47
+ private
48
+
49
+ def connect(dn, password)
50
+ options = [:host, :port, :base].zip(
51
+ @config.values_at(:host, :port, :basedn))
52
+ Net::LDAP.new(Hash[options]).tap do |ldap|
53
+ ldap.encryption(:simple_tls) if @config[:tls]
54
+ ldap.auth(dn, password)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,66 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Storage
5
+
6
+ # A storage implementation that persists data to YAML files on the
7
+ # local file system.
8
+ class Local < Storage
9
+ register :fs
10
+
11
+ def initialize(&block)
12
+ instance_eval(&block)
13
+ unless @dir && File.directory?(@dir) && File.writable?(@dir)
14
+ raise 'Must provide a writable storage directory'
15
+ end
16
+ end
17
+
18
+ def dir(dir=nil)
19
+ dir ? @dir = File.expand_path(dir) : @dir
20
+ end
21
+
22
+ def find_user(jid)
23
+ jid = JID.new(jid || '').bare.to_s
24
+ file = File.join(@dir, "#{jid}.user") unless jid.empty?
25
+ record = YAML.load_file(file) rescue nil
26
+ return User.new(:jid => jid).tap do |user|
27
+ user.name, user.password = record.values_at('name', 'password')
28
+ (record['roster'] || {}).each_pair do |jid, props|
29
+ user.roster << Contact.new(
30
+ :jid => jid,
31
+ :name => props['name'],
32
+ :subscription => props['subscription'],
33
+ :ask => props['ask'],
34
+ :groups => props['groups'] || [])
35
+ end
36
+ end if record
37
+ end
38
+
39
+ def save_user(user)
40
+ record = {'name' => user.name, 'password' => user.password, 'roster' => {}}
41
+ user.roster.each do |contact|
42
+ record['roster'][contact.jid.bare.to_s] = contact.to_h
43
+ end
44
+ file = File.join(@dir, "#{user.jid.bare.to_s}.user")
45
+ File.open(file, 'w') do |f|
46
+ YAML.dump(record, f)
47
+ end
48
+ end
49
+
50
+ def find_vcard(jid)
51
+ jid = JID.new(jid || '').bare.to_s
52
+ return if jid.empty?
53
+ file = File.join(@dir, "#{jid}.vcard")
54
+ Nokogiri::XML(File.read(file)).root rescue nil
55
+ end
56
+
57
+ def save_vcard(jid, card)
58
+ jid = JID.new(jid).bare.to_s
59
+ file = File.join(@dir, "#{jid}.vcard")
60
+ File.open(file, 'w') do |f|
61
+ f.write(card.to_xml)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,108 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Storage
5
+ class Redis < Storage
6
+ register :redis
7
+
8
+ %w[host port database password].each do |name|
9
+ define_method(name) do |*args|
10
+ if args.first
11
+ @config[name.to_sym] = args.first
12
+ else
13
+ @config[name.to_sym]
14
+ end
15
+ end
16
+ end
17
+
18
+ def initialize(&block)
19
+ @config = {}
20
+ instance_eval(&block)
21
+ @config[:db] = @config.delete(:database) if @config.key?(:database)
22
+ end
23
+
24
+ def find_user(jid)
25
+ jid = JID.new(jid || '').bare.to_s
26
+ if jid.empty? then yield; return end
27
+ find_roster(jid) do |contacts|
28
+ redis.get("user:#{jid}") do |response|
29
+ user = if response
30
+ doc = JSON.parse(response) rescue nil
31
+ User.new(:jid => jid).tap do |user|
32
+ user.name, user.password = doc.values_at('name', 'password')
33
+ user.roster = contacts
34
+ end if doc
35
+ end
36
+ yield user
37
+ end
38
+ end
39
+ end
40
+ fiber :find_user
41
+
42
+ def save_user(user)
43
+ doc = {:name => user.name, :password => user.password}
44
+ contacts = user.roster.map {|c| [c.jid.to_s, c.to_h.to_json] }.flatten
45
+ roster = "roster:#{user.jid.bare}"
46
+
47
+ redis.set("user:#{user.jid.bare}", doc.to_json) do
48
+ redis.del(roster) do
49
+ contacts.empty? ? yield : redis.hmset(roster, *contacts) { yield }
50
+ end
51
+ end
52
+ end
53
+ fiber :save_user
54
+
55
+ def find_vcard(jid)
56
+ jid = JID.new(jid || '').bare.to_s
57
+ if jid.empty? then yield; return end
58
+ redis.get("vcard:#{jid}") do |response|
59
+ card = if response
60
+ doc = JSON.parse(response) rescue nil
61
+ Nokogiri::XML(doc['card']).root rescue nil
62
+ end
63
+ yield card
64
+ end
65
+ end
66
+ fiber :find_vcard
67
+
68
+ def save_vcard(jid, card)
69
+ jid = JID.new(jid).bare.to_s
70
+ doc = {:card => card.to_xml}
71
+ redis.set("vcard:#{jid}", doc.to_json) do
72
+ yield
73
+ end
74
+ end
75
+ fiber :save_vcard
76
+
77
+ private
78
+
79
+ # Retrieve the hash stored at roster:jid and yield an array of
80
+ # +Vines::Contact+ objects to the provided block.
81
+ #
82
+ # find_roster('alice@wonderland.lit') do |contacts|
83
+ # puts contacts.size
84
+ # end
85
+ def find_roster(jid)
86
+ jid = JID.new(jid).bare
87
+ redis.hgetall("roster:#{jid}") do |roster|
88
+ contacts = roster.map do |jid, json|
89
+ contact = JSON.parse(json) rescue nil
90
+ Contact.new(
91
+ :jid => jid,
92
+ :name => contact['name'],
93
+ :subscription => contact['subscription'],
94
+ :ask => contact['ask'],
95
+ :groups => contact['groups'] || []) if contact
96
+ end.compact
97
+ yield contacts
98
+ end
99
+ end
100
+
101
+ # Cache and return a redis connection object. The em-redis gem reconnects
102
+ # in unbind so only create one connection.
103
+ def redis
104
+ @redis ||= EM::Protocols::Redis.connect(@config)
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,174 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ class Storage
5
+ class Sql < Storage
6
+ register :sql
7
+
8
+ class Contact < ActiveRecord::Base
9
+ belongs_to :user
10
+ end
11
+ class Group < ActiveRecord::Base; end
12
+ class User < ActiveRecord::Base
13
+ has_many :contacts
14
+ end
15
+
16
+ %w[adapter host port database username password pool].each do |name|
17
+ define_method(name) do |*args|
18
+ if args.first
19
+ @config[name.to_sym] = args.first
20
+ else
21
+ @config[name.to_sym]
22
+ end
23
+ end
24
+ end
25
+
26
+ def initialize(&block)
27
+ @config = {}
28
+ instance_eval(&block)
29
+ required = [:adapter, :database]
30
+ required << [:host, :port] unless @config[:adapter] == 'sqlite3'
31
+ required.flatten.each {|key| raise "Must provide #{key}" unless @config[key] }
32
+ [:username, :password].each {|key| @config.delete(key) if empty?(@config[key]) }
33
+ establish_connection
34
+ end
35
+
36
+ def find_user(jid)
37
+ ActiveRecord::Base.clear_reloadable_connections!
38
+
39
+ jid = JID.new(jid || '').bare.to_s
40
+ return if jid.empty?
41
+ xuser = by_jid(jid)
42
+ return Vines::User.new(:jid => jid).tap do |user|
43
+ user.name, user.password = xuser.name, xuser.password
44
+ xuser.contacts.each do |contact|
45
+ groups = contact.groups.map {|group| group.name }
46
+ user.roster << Vines::Contact.new(
47
+ :jid => contact.jid,
48
+ :name => contact.name,
49
+ :subscription => contact.subscription,
50
+ :ask => contact.ask,
51
+ :groups => groups)
52
+ end
53
+ end if xuser
54
+ end
55
+ defer :find_user
56
+
57
+ def save_user(user)
58
+ ActiveRecord::Base.clear_reloadable_connections!
59
+
60
+ xuser = by_jid(user.jid) || Sql::User.new(:jid => user.jid.bare.to_s)
61
+ xuser.name = user.name
62
+ xuser.password = user.password
63
+
64
+ # remove deleted contacts from roster
65
+ xuser.contacts.delete(xuser.contacts.select do |contact|
66
+ !user.contact?(contact.jid)
67
+ end)
68
+
69
+ # update contacts
70
+ xuser.contacts.each do |contact|
71
+ fresh = user.contact(contact.jid)
72
+ contact.update_attributes(
73
+ :name => fresh.name,
74
+ :ask => fresh.ask,
75
+ :subscription => fresh.subscription,
76
+ :groups => groups(fresh))
77
+ end
78
+
79
+ # add new contacts to roster
80
+ jids = xuser.contacts.map {|c| c.jid }
81
+ user.roster.select {|contact| !jids.include?(contact.jid.bare.to_s) }
82
+ .each do |contact|
83
+ xuser.contacts.build(
84
+ :user => xuser,
85
+ :jid => contact.jid.bare.to_s,
86
+ :name => contact.name,
87
+ :ask => contact.ask,
88
+ :subscription => contact.subscription,
89
+ :groups => groups(contact))
90
+ end
91
+ xuser.save
92
+ end
93
+ defer :save_user
94
+
95
+ def find_vcard(jid)
96
+ ActiveRecord::Base.clear_reloadable_connections!
97
+
98
+ jid = JID.new(jid || '').bare.to_s
99
+ return if jid.empty?
100
+ if xuser = by_jid(jid)
101
+ Nokogiri::XML(xuser.vcard).root rescue nil
102
+ end
103
+ end
104
+ defer :find_vcard
105
+
106
+ def save_vcard(jid, card)
107
+ ActiveRecord::Base.clear_reloadable_connections!
108
+
109
+ xuser = by_jid(jid)
110
+ if xuser
111
+ xuser.vcard = card.to_xml
112
+ xuser.save
113
+ end
114
+ end
115
+ defer :save_vcard
116
+
117
+ # Create the tables and indexes used by this storage engine.
118
+ def create_schema(args={})
119
+ ActiveRecord::Base.clear_reloadable_connections!
120
+
121
+ args[:force] ||= false
122
+
123
+ ActiveRecord::Schema.define do
124
+ create_table :users, :force => args[:force] do |t|
125
+ t.string :jid, :limit => 1000, :null => false
126
+ t.string :name, :limit => 1000, :null => true
127
+ t.string :password, :limit => 1000, :null => true
128
+ t.text :vcard, :null => true
129
+ end
130
+ add_index :users, :jid, :unique => true
131
+
132
+ create_table :contacts, :force => args[:force] do |t|
133
+ t.integer :user_id, :null => false
134
+ t.string :jid, :limit => 1000, :null => false
135
+ t.string :name, :limit => 1000, :null => true
136
+ t.string :ask, :limit => 1000, :null => true
137
+ t.string :subscription, :limit => 1000, :null => false
138
+ end
139
+ add_index :contacts, [:user_id, :jid], :unique => true
140
+
141
+ create_table :groups, :force => args[:force] do |t|
142
+ t.string :name, :limit => 1000, :null => false
143
+ end
144
+ add_index :groups, :name, :unique => true
145
+
146
+ create_table :contacts_groups, :id => false, :force => args[:force] do |t|
147
+ t.integer :contact_id, :null => false
148
+ t.integer :group_id, :null => false
149
+ end
150
+ add_index :contacts_groups, [:contact_id, :group_id], :unique => true
151
+ end
152
+ end
153
+
154
+ private
155
+
156
+ def establish_connection
157
+ ActiveRecord::Base.establish_connection(@config)
158
+ # has_and_belongs_to_many requires a connection so configure the
159
+ # associations here rather than in the class definitions above.
160
+ Sql::Contact.has_and_belongs_to_many :groups
161
+ Sql::Group.has_and_belongs_to_many :contacts
162
+ end
163
+
164
+ def by_jid(jid)
165
+ jid = JID.new(jid).bare.to_s
166
+ Sql::User.find_by_jid(jid, :include => {:contacts => :groups})
167
+ end
168
+
169
+ def groups(contact)
170
+ contact.groups.map {|name| Sql::Group.find_or_create_by_name(name.strip) }
171
+ end
172
+ end
173
+ end
174
+ end