vines 0.1.0

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