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.
- data/LICENSE +19 -0
- data/README +34 -0
- data/Rakefile +55 -0
- data/bin/vines +95 -0
- data/conf/certs/README +32 -0
- data/conf/certs/ca-bundle.crt +3987 -0
- data/conf/config.rb +114 -0
- data/lib/vines.rb +155 -0
- data/lib/vines/command/bcrypt.rb +12 -0
- data/lib/vines/command/cert.rb +49 -0
- data/lib/vines/command/init.rb +58 -0
- data/lib/vines/command/ldap.rb +35 -0
- data/lib/vines/command/restart.rb +12 -0
- data/lib/vines/command/schema.rb +24 -0
- data/lib/vines/command/start.rb +28 -0
- data/lib/vines/command/stop.rb +18 -0
- data/lib/vines/config.rb +191 -0
- data/lib/vines/contact.rb +99 -0
- data/lib/vines/daemon.rb +78 -0
- data/lib/vines/error.rb +150 -0
- data/lib/vines/jid.rb +56 -0
- data/lib/vines/kit.rb +23 -0
- data/lib/vines/router.rb +125 -0
- data/lib/vines/stanza.rb +55 -0
- data/lib/vines/stanza/iq.rb +50 -0
- data/lib/vines/stanza/iq/auth.rb +18 -0
- data/lib/vines/stanza/iq/disco_info.rb +25 -0
- data/lib/vines/stanza/iq/disco_items.rb +23 -0
- data/lib/vines/stanza/iq/error.rb +16 -0
- data/lib/vines/stanza/iq/ping.rb +16 -0
- data/lib/vines/stanza/iq/query.rb +10 -0
- data/lib/vines/stanza/iq/result.rb +16 -0
- data/lib/vines/stanza/iq/roster.rb +153 -0
- data/lib/vines/stanza/iq/session.rb +22 -0
- data/lib/vines/stanza/iq/vcard.rb +58 -0
- data/lib/vines/stanza/message.rb +41 -0
- data/lib/vines/stanza/presence.rb +119 -0
- data/lib/vines/stanza/presence/error.rb +23 -0
- data/lib/vines/stanza/presence/probe.rb +38 -0
- data/lib/vines/stanza/presence/subscribe.rb +66 -0
- data/lib/vines/stanza/presence/subscribed.rb +64 -0
- data/lib/vines/stanza/presence/unavailable.rb +15 -0
- data/lib/vines/stanza/presence/unsubscribe.rb +57 -0
- data/lib/vines/stanza/presence/unsubscribed.rb +50 -0
- data/lib/vines/storage.rb +216 -0
- data/lib/vines/storage/couchdb.rb +119 -0
- data/lib/vines/storage/ldap.rb +59 -0
- data/lib/vines/storage/local.rb +66 -0
- data/lib/vines/storage/redis.rb +108 -0
- data/lib/vines/storage/sql.rb +174 -0
- data/lib/vines/store.rb +51 -0
- data/lib/vines/stream.rb +198 -0
- data/lib/vines/stream/client.rb +131 -0
- data/lib/vines/stream/client/auth.rb +94 -0
- data/lib/vines/stream/client/auth_restart.rb +33 -0
- data/lib/vines/stream/client/bind.rb +58 -0
- data/lib/vines/stream/client/bind_restart.rb +25 -0
- data/lib/vines/stream/client/closed.rb +13 -0
- data/lib/vines/stream/client/ready.rb +15 -0
- data/lib/vines/stream/client/start.rb +27 -0
- data/lib/vines/stream/client/tls.rb +37 -0
- data/lib/vines/stream/component.rb +53 -0
- data/lib/vines/stream/component/handshake.rb +25 -0
- data/lib/vines/stream/component/ready.rb +24 -0
- data/lib/vines/stream/component/start.rb +19 -0
- data/lib/vines/stream/http.rb +111 -0
- data/lib/vines/stream/http/http_request.rb +22 -0
- data/lib/vines/stream/http/http_state.rb +139 -0
- data/lib/vines/stream/http/http_states.rb +53 -0
- data/lib/vines/stream/parser.rb +78 -0
- data/lib/vines/stream/server.rb +126 -0
- data/lib/vines/stream/server/auth.rb +13 -0
- data/lib/vines/stream/server/auth_restart.rb +19 -0
- data/lib/vines/stream/server/final_restart.rb +20 -0
- data/lib/vines/stream/server/outbound/auth.rb +31 -0
- data/lib/vines/stream/server/outbound/auth_restart.rb +20 -0
- data/lib/vines/stream/server/outbound/auth_result.rb +28 -0
- data/lib/vines/stream/server/outbound/final_features.rb +27 -0
- data/lib/vines/stream/server/outbound/final_restart.rb +20 -0
- data/lib/vines/stream/server/outbound/start.rb +20 -0
- data/lib/vines/stream/server/outbound/tls.rb +30 -0
- data/lib/vines/stream/server/outbound/tls_result.rb +31 -0
- data/lib/vines/stream/server/ready.rb +20 -0
- data/lib/vines/stream/server/start.rb +13 -0
- data/lib/vines/stream/server/tls.rb +13 -0
- data/lib/vines/stream/state.rb +55 -0
- data/lib/vines/token_bucket.rb +46 -0
- data/lib/vines/user.rb +124 -0
- data/lib/vines/version.rb +5 -0
- data/lib/vines/xmpp_server.rb +25 -0
- data/test/config_test.rb +396 -0
- data/test/error_test.rb +59 -0
- data/test/ext/nokogiri.rb +14 -0
- data/test/jid_test.rb +71 -0
- data/test/kit_test.rb +21 -0
- data/test/router_test.rb +60 -0
- data/test/stanza/iq/roster_test.rb +198 -0
- data/test/stanza/iq/session_test.rb +30 -0
- data/test/stanza/iq/vcard_test.rb +159 -0
- data/test/stanza/message_test.rb +124 -0
- data/test/stanza/presence/subscribe_test.rb +75 -0
- data/test/storage/couchdb_test.rb +102 -0
- data/test/storage/ldap_test.rb +207 -0
- data/test/storage/local_test.rb +54 -0
- data/test/storage/redis_test.rb +75 -0
- data/test/storage/sql_test.rb +55 -0
- data/test/storage/storage_tests.rb +134 -0
- data/test/storage_test.rb +90 -0
- data/test/stream/client/auth_test.rb +127 -0
- data/test/stream/client/ready_test.rb +47 -0
- data/test/stream/component/handshake_test.rb +46 -0
- data/test/stream/component/ready_test.rb +105 -0
- data/test/stream/component/start_test.rb +41 -0
- data/test/stream/parser_test.rb +121 -0
- data/test/stream/server/outbound/auth_test.rb +77 -0
- data/test/stream/server/ready_test.rb +100 -0
- data/test/token_bucket_test.rb +24 -0
- data/test/user_test.rb +64 -0
- 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
|