vines 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +2 -2
- data/Rakefile +63 -8
- data/bin/vines +0 -1
- data/conf/config.rb +16 -7
- data/lib/vines.rb +21 -16
- data/lib/vines/command/init.rb +5 -3
- data/lib/vines/config.rb +34 -0
- data/lib/vines/contact.rb +14 -0
- data/lib/vines/stanza.rb +26 -0
- data/lib/vines/stanza/iq.rb +1 -1
- data/lib/vines/stanza/iq/disco_info.rb +3 -0
- data/lib/vines/stanza/iq/private_storage.rb +83 -0
- data/lib/vines/stanza/iq/roster.rb +26 -30
- data/lib/vines/stanza/presence.rb +0 -12
- data/lib/vines/stanza/presence/subscribe.rb +3 -20
- data/lib/vines/stanza/presence/subscribed.rb +9 -10
- data/lib/vines/stanza/presence/unsubscribe.rb +8 -15
- data/lib/vines/stanza/presence/unsubscribed.rb +8 -8
- data/lib/vines/storage.rb +28 -0
- data/lib/vines/storage/couchdb.rb +29 -0
- data/lib/vines/storage/local.rb +22 -0
- data/lib/vines/storage/redis.rb +26 -0
- data/lib/vines/storage/sql.rb +48 -5
- data/lib/vines/stream/client.rb +6 -8
- data/lib/vines/stream/http.rb +23 -21
- data/lib/vines/stream/http/auth.rb +1 -1
- data/lib/vines/stream/http/bind.rb +1 -1
- data/lib/vines/stream/http/bind_restart.rb +4 -3
- data/lib/vines/stream/http/ready.rb +1 -1
- data/lib/vines/stream/http/request.rb +94 -5
- data/lib/vines/stream/http/session.rb +8 -6
- data/lib/vines/version.rb +1 -1
- data/test/config_test.rb +12 -0
- data/test/contact_test.rb +40 -0
- data/test/rake_test_loader.rb +11 -3
- data/test/stanza/iq/private_storage_test.rb +177 -0
- data/test/stanza/iq/roster_test.rb +1 -1
- data/test/stanza/iq_test.rb +63 -0
- data/test/storage/couchdb_test.rb +7 -1
- data/test/storage/local_test.rb +8 -2
- data/test/storage/redis_test.rb +16 -7
- data/test/storage/sql_test.rb +8 -1
- data/test/storage/storage_tests.rb +50 -0
- data/test/stream/http/auth_test.rb +3 -0
- data/test/stream/http/ready_test.rb +3 -0
- data/test/stream/http/request_test.rb +86 -0
- data/test/stream/parser_test.rb +2 -0
- data/web/404.html +43 -0
- data/web/apple-touch-icon.png +0 -0
- data/web/chat/coffeescripts/chat.coffee +385 -0
- data/web/chat/coffeescripts/init.coffee +15 -0
- data/web/chat/coffeescripts/logout.coffee +5 -0
- data/web/chat/index.html +17 -0
- data/web/chat/javascripts/app.js +1 -0
- data/web/chat/javascripts/chat.js +436 -0
- data/web/chat/javascripts/init.js +21 -0
- data/web/chat/javascripts/logout.js +11 -0
- data/web/chat/stylesheets/chat.css +290 -0
- data/web/favicon.png +0 -0
- data/web/lib/coffeescripts/contact.coffee +32 -0
- data/web/lib/coffeescripts/layout.coffee +30 -0
- data/web/lib/coffeescripts/login.coffee +52 -0
- data/web/lib/coffeescripts/navbar.coffee +84 -0
- data/web/lib/coffeescripts/router.coffee +40 -0
- data/web/lib/coffeescripts/session.coffee +211 -0
- data/web/lib/images/default-user.png +0 -0
- data/web/lib/images/logo-large.png +0 -0
- data/web/lib/images/logo-small.png +0 -0
- data/web/lib/javascripts/base.js +9 -0
- data/web/lib/javascripts/contact.js +94 -0
- data/web/lib/javascripts/icons.js +101 -0
- data/web/lib/javascripts/jquery.cookie.js +91 -0
- data/web/lib/javascripts/jquery.js +18 -0
- data/web/lib/javascripts/layout.js +48 -0
- data/web/lib/javascripts/login.js +61 -0
- data/web/lib/javascripts/navbar.js +69 -0
- data/web/lib/javascripts/raphael.js +8 -0
- data/web/lib/javascripts/router.js +105 -0
- data/web/lib/javascripts/session.js +322 -0
- data/web/lib/javascripts/strophe.js +1 -0
- data/web/lib/stylesheets/base.css +223 -0
- data/web/lib/stylesheets/login.css +63 -0
- metadata +51 -9
@@ -177,7 +177,7 @@ class RosterTest < MiniTest::Unit::TestCase
|
|
177
177
|
assert router.verify
|
178
178
|
|
179
179
|
expected = node(%q{
|
180
|
-
<iq
|
180
|
+
<iq to="alice@wonderland.lit/tea" type="set">
|
181
181
|
<query xmlns="jabber:iq:roster">
|
182
182
|
<item jid="hatter@wonderland.lit" name="Mad Hatter" subscription="none">
|
183
183
|
<group>Friends</group>
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'vines'
|
4
|
+
require 'ext/nokogiri'
|
5
|
+
require 'minitest/autorun'
|
6
|
+
|
7
|
+
class IqTest < MiniTest::Unit::TestCase
|
8
|
+
def setup
|
9
|
+
@stream = MiniTest::Mock.new
|
10
|
+
@stream.expect(:domain, 'wonderland.lit')
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_allow_other_iq_to_route
|
14
|
+
alice = Vines::User.new(:jid => 'alice@wonderland.lit/tea')
|
15
|
+
hatter = Vines::User.new(:jid => 'hatter@wonderland.lit/crumpets')
|
16
|
+
node = node(%q{
|
17
|
+
<iq id="42" type="set" to="alice@wonderland.lit/tea" from="hatter@wonderland.lit/crumpets">
|
18
|
+
<si xmlns="http://jabber.org/protocol/si" id="42_si" profile="http://jabber.org/protocol/si/profile/file-transfer">
|
19
|
+
<file xmlns="http://jabber.org/protocol/si/profile/file-transfer" name="file" size="1"/>
|
20
|
+
<feature xmlns="http://jabber.org/protocol/feature-neg">
|
21
|
+
<x xmlns="jabber:x:data" type="form">
|
22
|
+
<field var="stream-method" type="list-single">
|
23
|
+
<option>
|
24
|
+
<value>http://jabber.org/protocol/bytestreams</value>
|
25
|
+
</option>
|
26
|
+
<option>
|
27
|
+
<value>http://jabber.org/protocol/ibb</value>
|
28
|
+
</option>
|
29
|
+
</field>
|
30
|
+
</x>
|
31
|
+
</feature>
|
32
|
+
</si>
|
33
|
+
</iq>}.strip.gsub(/\n|\s{2,}/, ''))
|
34
|
+
|
35
|
+
recipient = MiniTest::Mock.new
|
36
|
+
recipient.expect(:write, nil, [node])
|
37
|
+
|
38
|
+
router = MiniTest::Mock.new
|
39
|
+
router.expect(:available_resources, [recipient], [alice.jid])
|
40
|
+
router.expect(:local?, true, [node])
|
41
|
+
|
42
|
+
@stream.expect(:user, hatter)
|
43
|
+
@stream.expect(:router, router)
|
44
|
+
@stream.expect(:domain, 'wonderland.lit')
|
45
|
+
|
46
|
+
stanza = Vines::Stanza::Iq.new(node, @stream)
|
47
|
+
stanza.process
|
48
|
+
assert @stream.verify
|
49
|
+
assert recipient.verify
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_feature_not_implemented
|
53
|
+
node = node('<iq type="set" id="42">')
|
54
|
+
stanza = Vines::Stanza::Iq.new(node, @stream)
|
55
|
+
assert_raises(Vines::StanzaErrors::FeatureNotImplemented) { stanza.process }
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def node(xml)
|
61
|
+
Nokogiri::XML(xml).root
|
62
|
+
end
|
63
|
+
end
|
@@ -49,7 +49,13 @@ class CouchDBTest < MiniTest::Unit::TestCase
|
|
49
49
|
save_doc({
|
50
50
|
'_id' => 'vcard:full@wonderland.lit',
|
51
51
|
'type' => 'Vcard',
|
52
|
-
'card' =>
|
52
|
+
'card' => VCARD.to_xml
|
53
|
+
})
|
54
|
+
|
55
|
+
save_doc({
|
56
|
+
'_id' => "fragment:full@wonderland.lit:#{FRAGMENT_ID}",
|
57
|
+
'type' => 'Fragment',
|
58
|
+
'xml' => FRAGMENT.to_xml
|
53
59
|
})
|
54
60
|
end
|
55
61
|
end
|
data/test/storage/local_test.rb
CHANGED
@@ -14,7 +14,8 @@ class LocalTest < MiniTest::Unit::TestCase
|
|
14
14
|
:clear_pass => './clear_password@wonderland.lit.user',
|
15
15
|
:bcrypt => './bcrypt_password@wonderland.lit.user',
|
16
16
|
:full => './full@wonderland.lit.user',
|
17
|
-
:vcard => './full@wonderland.lit.vcard'
|
17
|
+
:vcard => './full@wonderland.lit.vcard',
|
18
|
+
:fragment => "./full@wonderland.lit-#{FRAGMENT_ID}.fragment"
|
18
19
|
}
|
19
20
|
File.open(@files[:empty], 'w') {|f| f.write('') }
|
20
21
|
File.open(@files[:no_pass], 'w') {|f| f.write('foo: bar') }
|
@@ -32,11 +33,16 @@ class LocalTest < MiniTest::Unit::TestCase
|
|
32
33
|
f.puts(" groups: [Group3, Group4]")
|
33
34
|
end
|
34
35
|
File.open(@files[:vcard], 'w') {|f| f.write(StorageTests::VCARD.to_xml) }
|
36
|
+
File.open(@files[:fragment], 'w') {|f| f.write(StorageTests::FRAGMENT.to_xml) }
|
35
37
|
end
|
36
38
|
|
37
39
|
def teardown
|
38
40
|
misc = %w[user vcard].map {|ext| "./save_user@domain.tld.#{ext}" }
|
39
|
-
[
|
41
|
+
[
|
42
|
+
"./save_user@domain.tld-#{FRAGMENT_ID}.fragment",
|
43
|
+
*misc,
|
44
|
+
*@files.values
|
45
|
+
].each do |f|
|
40
46
|
File.delete(f) if File.exist?(f)
|
41
47
|
end
|
42
48
|
end
|
data/test/storage/redis_test.rb
CHANGED
@@ -11,24 +11,32 @@ class RedisTest < MiniTest::Unit::TestCase
|
|
11
11
|
def initialize
|
12
12
|
@db = {}
|
13
13
|
end
|
14
|
+
def del(key)
|
15
|
+
@db.delete(key)
|
16
|
+
EM.next_tick { yield if block_given? }
|
17
|
+
end
|
14
18
|
def get(key)
|
15
19
|
EM.next_tick { yield @db[key] }
|
16
20
|
end
|
21
|
+
def set(key, value)
|
22
|
+
@db[key] = value
|
23
|
+
EM.next_tick { yield if block_given? }
|
24
|
+
end
|
25
|
+
def hget(key, field)
|
26
|
+
EM.next_tick { yield @db[key][field] rescue nil }
|
27
|
+
end
|
17
28
|
def hgetall(key)
|
18
29
|
EM.next_tick { yield @db[key] || {} }
|
19
30
|
end
|
20
|
-
def
|
21
|
-
@db[key]
|
31
|
+
def hset(key, field, value)
|
32
|
+
@db[key] ||= {}
|
33
|
+
@db[key][field] = value
|
22
34
|
EM.next_tick { yield if block_given? }
|
23
35
|
end
|
24
36
|
def hmset(key, *args)
|
25
37
|
@db[key] = Hash[*args]
|
26
38
|
EM.next_tick { yield if block_given? }
|
27
39
|
end
|
28
|
-
def del(key)
|
29
|
-
@db.delete(key)
|
30
|
-
EM.next_tick { yield if block_given? }
|
31
|
-
end
|
32
40
|
def flushdb
|
33
41
|
@db.clear
|
34
42
|
EM.next_tick { yield if block_given? }
|
@@ -51,7 +59,8 @@ class RedisTest < MiniTest::Unit::TestCase
|
|
51
59
|
{'name' => 'Contact1', 'groups' => %w[Group1 Group2]}.to_json,
|
52
60
|
'contact2@wonderland.lit',
|
53
61
|
{'name' => 'Contact2', 'groups' => %w[Group3 Group4]}.to_json)
|
54
|
-
db.set('vcard:full@wonderland.lit', {'card' =>
|
62
|
+
db.set('vcard:full@wonderland.lit', {'card' => VCARD.to_xml}.to_json)
|
63
|
+
db.hset('fragments:full@wonderland.lit', FRAGMENT_ID, {'xml' => FRAGMENT.to_xml}.to_json)
|
55
64
|
end
|
56
65
|
end
|
57
66
|
|
data/test/storage/sql_test.rb
CHANGED
@@ -25,7 +25,7 @@ class SqlTest < MiniTest::Unit::TestCase
|
|
25
25
|
:jid => 'full@wonderland.lit',
|
26
26
|
:name => 'Tester',
|
27
27
|
:password => BCrypt::Password.create('secret'),
|
28
|
-
:vcard =>
|
28
|
+
:vcard => VCARD.to_xml)
|
29
29
|
full.contacts << Vines::Storage::Sql::Contact.new(
|
30
30
|
:jid => 'contact1@wonderland.lit',
|
31
31
|
:name => 'Contact1',
|
@@ -37,6 +37,13 @@ class SqlTest < MiniTest::Unit::TestCase
|
|
37
37
|
:groups => groups[2, 2],
|
38
38
|
:subscription => 'both')
|
39
39
|
full.save
|
40
|
+
|
41
|
+
fragment = Vines::Storage::Sql::Fragment.new(
|
42
|
+
:user => full,
|
43
|
+
:root => 'characters',
|
44
|
+
:namespace => 'urn:wonderland',
|
45
|
+
:xml => FRAGMENT.to_xml)
|
46
|
+
fragment.save
|
40
47
|
end
|
41
48
|
|
42
49
|
def teardown
|
@@ -8,6 +8,14 @@ require 'minitest/autorun'
|
|
8
8
|
# tests are the same regardless of implementation so share those methods
|
9
9
|
# here.
|
10
10
|
module StorageTests
|
11
|
+
FRAGMENT_ID = Digest::SHA1.hexdigest("characters:urn:wonderland")
|
12
|
+
|
13
|
+
FRAGMENT = Nokogiri::XML(%q{
|
14
|
+
<characters xmlns="urn:wonderland">
|
15
|
+
<character>Alice</character>
|
16
|
+
</characters>
|
17
|
+
}.strip).root
|
18
|
+
|
11
19
|
VCARD = Nokogiri::XML(%q{
|
12
20
|
<vCard xmlns="vcard-temp">
|
13
21
|
<FN>Alice in Wonderland</FN>
|
@@ -131,4 +139,46 @@ module StorageTests
|
|
131
139
|
assert_equal VCARD, card
|
132
140
|
end
|
133
141
|
end
|
142
|
+
|
143
|
+
def test_find_fragment
|
144
|
+
EMLoop.new do
|
145
|
+
db = storage
|
146
|
+
root = Nokogiri::XML(%q{<characters xmlns="urn:wonderland"/>}).root
|
147
|
+
bad_name = Nokogiri::XML(%q{<not_characters xmlns="urn:wonderland"/>}).root
|
148
|
+
bad_ns = Nokogiri::XML(%q{<characters xmlns="not:wonderland"/>}).root
|
149
|
+
|
150
|
+
node = db.find_fragment(nil, nil)
|
151
|
+
assert_nil node
|
152
|
+
|
153
|
+
node = db.find_fragment('full@wonderland.lit', bad_name)
|
154
|
+
assert_nil node
|
155
|
+
|
156
|
+
node = db.find_fragment('full@wonderland.lit', bad_ns)
|
157
|
+
assert_nil node
|
158
|
+
|
159
|
+
node = db.find_fragment('full@wonderland.lit', root)
|
160
|
+
refute_nil node
|
161
|
+
assert_equal FRAGMENT, node
|
162
|
+
|
163
|
+
node = db.find_fragment(Vines::JID.new('full@wonderland.lit'), root)
|
164
|
+
refute_nil node
|
165
|
+
assert_equal FRAGMENT, node
|
166
|
+
|
167
|
+
node = db.find_fragment(Vines::JID.new('full@wonderland.lit/resource'), root)
|
168
|
+
refute_nil node
|
169
|
+
assert_equal FRAGMENT, node
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def test_save_fragment
|
174
|
+
EMLoop.new do
|
175
|
+
db = storage
|
176
|
+
root = Nokogiri::XML(%q{<characters xmlns="urn:wonderland"/>}).root
|
177
|
+
db.save_user(Vines::User.new(:jid => 'save_user@domain.tld'))
|
178
|
+
db.save_fragment('save_user@domain.tld/resource1', FRAGMENT)
|
179
|
+
node = db.find_fragment('save_user@domain.tld', root)
|
180
|
+
refute_nil node
|
181
|
+
assert_equal FRAGMENT, node
|
182
|
+
end
|
183
|
+
end
|
134
184
|
end
|
@@ -11,16 +11,19 @@ class HttpAuthTest < MiniTest::Unit::TestCase
|
|
11
11
|
|
12
12
|
def test_missing_body_raises_error
|
13
13
|
node = node('<presence type="unavailable"/>')
|
14
|
+
@stream.expect(:valid_session?, true, [nil])
|
14
15
|
assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
|
15
16
|
end
|
16
17
|
|
17
18
|
def test_body_with_missing_namespace_raises_error
|
18
19
|
node = node('<body rid="42" sid="12"/>')
|
20
|
+
@stream.expect(:valid_session?, true, ['12'])
|
19
21
|
assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
|
20
22
|
end
|
21
23
|
|
22
24
|
def test_missing_rid_raises_error
|
23
25
|
node = node('<body xmlns="http://jabber.org/protocol/httpbind" sid="12"/>')
|
26
|
+
@stream.expect(:valid_session?, true, ['12'])
|
24
27
|
assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
|
25
28
|
end
|
26
29
|
|
@@ -11,16 +11,19 @@ class HttpReadyTest < MiniTest::Unit::TestCase
|
|
11
11
|
|
12
12
|
def test_missing_body_raises_error
|
13
13
|
node = node('<presence type="unavailable"/>')
|
14
|
+
@stream.expect(:valid_session?, true, [nil])
|
14
15
|
assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
|
15
16
|
end
|
16
17
|
|
17
18
|
def test_body_with_missing_namespace_raises_error
|
18
19
|
node = node('<body rid="42" sid="12"/>')
|
20
|
+
@stream.expect(:valid_session?, true, ['12'])
|
19
21
|
assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
|
20
22
|
end
|
21
23
|
|
22
24
|
def test_missing_rid_raises_error
|
23
25
|
node = node('<body xmlns="http://jabber.org/protocol/httpbind" sid="12"/>')
|
26
|
+
@stream.expect(:valid_session?, true, ['12'])
|
24
27
|
assert_raises(Vines::StreamErrors::NotAuthorized) { @state.node(node) }
|
25
28
|
end
|
26
29
|
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'vines'
|
4
|
+
require 'minitest/autorun'
|
5
|
+
|
6
|
+
class RequestTest < MiniTest::Unit::TestCase
|
7
|
+
PASSWORD = File.expand_path('../passwords')
|
8
|
+
INDEX = File.expand_path('index.html')
|
9
|
+
|
10
|
+
def setup
|
11
|
+
File.open(PASSWORD, 'w') {|f| f.puts '/etc/passwd contents' }
|
12
|
+
File.open(INDEX, 'w') {|f| f.puts 'index.html contents' }
|
13
|
+
|
14
|
+
@stream = MiniTest::Mock.new
|
15
|
+
@parser = MiniTest::Mock.new
|
16
|
+
@parser.expect(:headers, {'Content-Type' => 'text/html'})
|
17
|
+
@parser.expect(:http_method, 'GET')
|
18
|
+
@parser.expect(:request_path, '/blogs/12')
|
19
|
+
@parser.expect(:request_url, '/blogs/12?ok=true')
|
20
|
+
@parser.expect(:query_string, 'ok=true')
|
21
|
+
end
|
22
|
+
|
23
|
+
def teardown
|
24
|
+
File.delete(PASSWORD)
|
25
|
+
File.delete(INDEX)
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_copies_request_info_from_parser
|
29
|
+
request = Vines::Stream::Http::Request.new(@stream, @parser, '<html></html>')
|
30
|
+
assert_equal request.headers, {'Content-Type' => 'text/html'}
|
31
|
+
assert_equal request.method, 'GET'
|
32
|
+
assert_equal request.path, '/blogs/12'
|
33
|
+
assert_equal request.url, '/blogs/12?ok=true'
|
34
|
+
assert_equal request.query, 'ok=true'
|
35
|
+
assert_equal request.body, '<html></html>'
|
36
|
+
assert @stream.verify
|
37
|
+
assert @parser.verify
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_reply_with_file_404
|
41
|
+
request = Vines::Stream::Http::Request.new(@stream, @parser, '<html></html>')
|
42
|
+
|
43
|
+
expected = "HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n"
|
44
|
+
@stream.expect(:stream_write, nil, [expected])
|
45
|
+
@stream.expect(:close_connection_after_writing, nil)
|
46
|
+
|
47
|
+
request.reply_with_file(Dir.pwd)
|
48
|
+
assert @stream.verify
|
49
|
+
assert @parser.verify
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_reply_with_file_directory_traversal
|
53
|
+
@parser.expect(:request_path, '../passwords')
|
54
|
+
request = Vines::Stream::Http::Request.new(@stream, @parser, '<html></html>')
|
55
|
+
|
56
|
+
expected = "HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n"
|
57
|
+
@stream.expect(:stream_write, nil, [expected])
|
58
|
+
@stream.expect(:close_connection_after_writing, nil)
|
59
|
+
|
60
|
+
request.reply_with_file(Dir.pwd)
|
61
|
+
assert @stream.verify
|
62
|
+
assert @parser.verify
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_reply_with_file_for_directory_serves_index_html
|
66
|
+
@parser.expect(:request_path, '/')
|
67
|
+
request = Vines::Stream::Http::Request.new(@stream, @parser, '<html></html>')
|
68
|
+
|
69
|
+
mtime = File.mtime(INDEX).utc.strftime('%a, %d %b %Y %H:%M:%S GMT')
|
70
|
+
headers = [
|
71
|
+
"HTTP/1.1 200 OK",
|
72
|
+
"Connection: close",
|
73
|
+
'Content-Type: text/html; charset="utf-8"',
|
74
|
+
"Content-Length: 20",
|
75
|
+
"Last-Modified: #{mtime}"
|
76
|
+
].join("\r\n")
|
77
|
+
|
78
|
+
@stream.expect(:stream_write, nil, ["#{headers}\r\n\r\n"])
|
79
|
+
@stream.expect(:stream_write, nil, ["index.html contents\n"])
|
80
|
+
@stream.expect(:close_connection_after_writing, nil)
|
81
|
+
|
82
|
+
request.reply_with_file(Dir.pwd)
|
83
|
+
assert @stream.verify
|
84
|
+
assert @parser.verify
|
85
|
+
end
|
86
|
+
end
|
data/test/stream/parser_test.rb
CHANGED
@@ -31,6 +31,8 @@ class ParserTest < MiniTest::Unit::TestCase
|
|
31
31
|
['<iq id="42" type="get"><query xmlns="http://jabber.org/protocol/disco#info"></query></iq>', Vines::Stanza::Iq::Query::DiscoInfo],
|
32
32
|
['<iq id="42" type="get"><query xmlns="http://jabber.org/protocol/disco#items"></query></iq>', Vines::Stanza::Iq::Query::DiscoItems],
|
33
33
|
['<iq id="42" type="error"></iq>', Vines::Stanza::Iq::Error],
|
34
|
+
['<iq id="42" type="get"><query xmlns="jabber:iq:private"/></iq>', Vines::Stanza::Iq::PrivateStorage],
|
35
|
+
['<iq id="42" type="set"><query xmlns="jabber:iq:private"/></iq>', Vines::Stanza::Iq::PrivateStorage],
|
34
36
|
['<iq id="42" type="get"><ping xmlns="urn:xmpp:ping"/></iq>', Vines::Stanza::Iq::Ping],
|
35
37
|
['<iq id="42" type="result"></iq>', Vines::Stanza::Iq::Result],
|
36
38
|
['<iq id="42" type="get"><query xmlns="jabber:iq:roster"/></iq>', Vines::Stanza::Iq::Query::Roster],
|
data/web/404.html
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8"/>
|
5
|
+
<meta name="apple-mobile-web-app-capable" content="yes"/>
|
6
|
+
<title>Vines</title>
|
7
|
+
<link rel="shortcut icon" type="image/png" href="/favicon.png"/>
|
8
|
+
<link rel="stylesheet" href="/lib/stylesheets/base.css"/>
|
9
|
+
<style type="text/css">
|
10
|
+
body {
|
11
|
+
background: -moz-radial-gradient(#1a3762, #0c1a2d);
|
12
|
+
background: -webkit-gradient(radial, 50% 50%, 0, 50% 50%, 500, from(#1a3762), to(#0c1a2d));
|
13
|
+
display: table;
|
14
|
+
text-align: center;
|
15
|
+
width: 100%;
|
16
|
+
}
|
17
|
+
header {
|
18
|
+
display: table-cell;
|
19
|
+
vertical-align: middle;
|
20
|
+
width: 100%;
|
21
|
+
}
|
22
|
+
h1 {
|
23
|
+
background: url(/lib/images/logo-large.png) no-repeat center;
|
24
|
+
color: transparent;
|
25
|
+
height: 64px;
|
26
|
+
text-shadow: none;
|
27
|
+
width: 100%;
|
28
|
+
}
|
29
|
+
p {
|
30
|
+
color: #fff;
|
31
|
+
font-size: 11pt;
|
32
|
+
margin: 20px auto;
|
33
|
+
text-shadow: 0 1px 1px #000;
|
34
|
+
}
|
35
|
+
</style>
|
36
|
+
</head>
|
37
|
+
<body>
|
38
|
+
<header>
|
39
|
+
<h1>Page not found</h1>
|
40
|
+
<p>This is not the page you're looking for.</p>
|
41
|
+
</header>
|
42
|
+
</body>
|
43
|
+
</html>
|
Binary file
|
@@ -0,0 +1,385 @@
|
|
1
|
+
class ChatPage
|
2
|
+
constructor: (@session) ->
|
3
|
+
@session.onRoster ( ) => this.roster()
|
4
|
+
@session.onCard (c) => this.card(c)
|
5
|
+
@session.onMessage (m) => this.message(m)
|
6
|
+
@session.onPresence (p) => this.presence(p)
|
7
|
+
@chats = {}
|
8
|
+
@currentContact = null
|
9
|
+
|
10
|
+
datef: (millis) ->
|
11
|
+
d = new Date(millis)
|
12
|
+
meridian = if d.getHours() >= 12 then ' pm' else ' am'
|
13
|
+
hour = if d.getHours() > 12 then d.getHours() - 12 else d.getHours()
|
14
|
+
hour = 12 if hour == 0
|
15
|
+
minutes = d.getMinutes() + ''
|
16
|
+
minutes = '0' + minutes if minutes.length == 1
|
17
|
+
hour + ':' + minutes + meridian
|
18
|
+
|
19
|
+
card: (card) ->
|
20
|
+
this.eachContact card.jid, (node) =>
|
21
|
+
$('.vcard-img', node).attr 'src', @session.avatar card.jid
|
22
|
+
|
23
|
+
roster: ->
|
24
|
+
roster = $('#roster')
|
25
|
+
|
26
|
+
$('li', roster).each (ix, node) =>
|
27
|
+
jid = $(node).attr('data-jid')
|
28
|
+
$(node).remove() unless @session.roster[jid]
|
29
|
+
|
30
|
+
setName = (node, contact) ->
|
31
|
+
$('.text', node).text contact.name || contact.jid
|
32
|
+
node.attr 'data-name', contact.name || ''
|
33
|
+
|
34
|
+
for jid, contact of @session.roster
|
35
|
+
found = $("#roster li[data-jid='#{jid}']")
|
36
|
+
setName(found, contact)
|
37
|
+
if found.length == 0
|
38
|
+
node = $("""
|
39
|
+
<li data-jid="#{jid}" data-name="" class="offline">
|
40
|
+
<span class="text"></span>
|
41
|
+
<span class="status-msg">Offline</span>
|
42
|
+
<span class="unread" style="display:none;"></span>
|
43
|
+
<img class="vcard-img" alt="#{jid}" src="#{@session.avatar jid}"/>
|
44
|
+
</li>
|
45
|
+
""").appendTo roster
|
46
|
+
setName(node, contact)
|
47
|
+
node.click (event) => this.selectContact(event)
|
48
|
+
|
49
|
+
message: (message) ->
|
50
|
+
this.queueMessage message
|
51
|
+
me = message.from == @session.jid()
|
52
|
+
from = message.from.split('/')[0]
|
53
|
+
|
54
|
+
if me || from == @currentContact
|
55
|
+
bottom = this.atBottom()
|
56
|
+
this.appendMessage message
|
57
|
+
this.scroll() if bottom
|
58
|
+
else
|
59
|
+
chat = this.chat message.from
|
60
|
+
chat.unread++
|
61
|
+
this.eachContact from, (node) ->
|
62
|
+
$('.unread', node).text(chat.unread).show()
|
63
|
+
|
64
|
+
eachContact: (jid, callback) ->
|
65
|
+
for node in $("#roster li[data-jid='#{jid}']").get()
|
66
|
+
callback $(node)
|
67
|
+
|
68
|
+
appendMessage: (message) ->
|
69
|
+
from = message.from.split('/')[0]
|
70
|
+
contact = @session.roster[from]
|
71
|
+
name = if contact then (contact.name || from) else from
|
72
|
+
name = 'Me' if message.from == @session.jid()
|
73
|
+
node = $("""
|
74
|
+
<li data-jid="#{from}" style="display:none;">
|
75
|
+
<p></p>
|
76
|
+
<img alt="#{from}" src="#{@session.avatar from}"/>
|
77
|
+
<footer>
|
78
|
+
<span class="author"></span>
|
79
|
+
<span class="time">#{this.datef message.received}</span>
|
80
|
+
</footer>
|
81
|
+
</li>
|
82
|
+
""").appendTo '#messages'
|
83
|
+
|
84
|
+
$('p', node).text message.text
|
85
|
+
$('.author', node).text name
|
86
|
+
node.fadeIn 200
|
87
|
+
|
88
|
+
queueMessage: (message) ->
|
89
|
+
me = message.from == @session.jid()
|
90
|
+
full = message[if me then 'to' else 'from']
|
91
|
+
chat = this.chat full
|
92
|
+
chat.jid = full
|
93
|
+
chat.messages.push message
|
94
|
+
|
95
|
+
chat: (jid) ->
|
96
|
+
bare = jid.split('/')[0]
|
97
|
+
chat = @chats[bare]
|
98
|
+
unless chat
|
99
|
+
chat = jid: jid, messages: [], unread: 0
|
100
|
+
@chats[bare] = chat
|
101
|
+
chat
|
102
|
+
|
103
|
+
presence: (presence) ->
|
104
|
+
from = presence.from.split('/')[0]
|
105
|
+
return if from == @session.bareJid()
|
106
|
+
if !presence.type || presence.offline
|
107
|
+
contact = @session.roster[from]
|
108
|
+
this.eachContact from, (node) ->
|
109
|
+
$('.status-msg', node).text contact.status()
|
110
|
+
if contact.offline()
|
111
|
+
node.addClass 'offline'
|
112
|
+
else
|
113
|
+
node.removeClass 'offline'
|
114
|
+
|
115
|
+
if presence.offline
|
116
|
+
this.chat(from).jid = from
|
117
|
+
|
118
|
+
if presence.type == 'subscribe'
|
119
|
+
node = $("""
|
120
|
+
<li data-jid="#{presence.from}" style="display:none;">
|
121
|
+
<form class="notify-form">
|
122
|
+
<h2>Buddy Approval</h2>
|
123
|
+
<p>#{presence.from} wants to add you as a buddy.</p>
|
124
|
+
<fieldset class="buttons">
|
125
|
+
<input type="button" value="Decline"/>
|
126
|
+
<input type="submit" value="Accept"/>
|
127
|
+
</fieldset>
|
128
|
+
</form>
|
129
|
+
</li>
|
130
|
+
""").appendTo '#notifications'
|
131
|
+
node.fadeIn 200
|
132
|
+
$('form', node).submit => this.acceptContact node, presence.from
|
133
|
+
$('input[type="button"]', node).click => this.rejectContact node, presence.from
|
134
|
+
|
135
|
+
acceptContact: (node, jid) ->
|
136
|
+
node.fadeOut 200, -> node.remove()
|
137
|
+
@session.sendSubscribed jid
|
138
|
+
@session.sendSubscribe jid
|
139
|
+
false
|
140
|
+
|
141
|
+
rejectContact: (node, jid) ->
|
142
|
+
node.fadeOut 200, -> node.remove()
|
143
|
+
@session.sendUnsubscribed jid
|
144
|
+
|
145
|
+
selectContact: (event) ->
|
146
|
+
jid = $(event.currentTarget).attr 'data-jid'
|
147
|
+
contact = @session.roster[jid]
|
148
|
+
return if @currentContact == jid
|
149
|
+
@currentContact = jid
|
150
|
+
|
151
|
+
$('#roster li').removeClass 'selected'
|
152
|
+
$(event.currentTarget).addClass 'selected'
|
153
|
+
$('#chat-title').text('Chat with ' + (contact.name || contact.jid))
|
154
|
+
$('#messages').empty()
|
155
|
+
|
156
|
+
chat = @chats[jid]
|
157
|
+
messages = []
|
158
|
+
if chat
|
159
|
+
messages = chat.messages
|
160
|
+
chat.unread = 0
|
161
|
+
this.eachContact jid, (node) ->
|
162
|
+
$('.unread', node).text('').hide()
|
163
|
+
|
164
|
+
this.appendMessage msg for msg in messages
|
165
|
+
this.scroll()
|
166
|
+
|
167
|
+
$('#remove-contact-msg').html "Are you sure you want to remove " +
|
168
|
+
"<strong>#{@currentContact}</strong> from your buddy list?"
|
169
|
+
$('#remove-contact-form .buttons').fadeIn 200
|
170
|
+
|
171
|
+
$('#edit-contact-jid').text @currentContact
|
172
|
+
$('#edit-contact-name').val @session.roster[@currentContact].name
|
173
|
+
$('#edit-contact-form input').fadeIn 200
|
174
|
+
$('#edit-contact-form .buttons').fadeIn 200
|
175
|
+
|
176
|
+
scroll: ->
|
177
|
+
msgs = $ '#messages'
|
178
|
+
msgs.animate(scrollTop: msgs.prop('scrollHeight'), 400)
|
179
|
+
|
180
|
+
atBottom: ->
|
181
|
+
msgs = $('#messages')
|
182
|
+
bottom = msgs.prop('scrollHeight') - msgs.height()
|
183
|
+
msgs.scrollTop() == bottom
|
184
|
+
|
185
|
+
send: ->
|
186
|
+
return false unless @currentContact
|
187
|
+
input = $('#message')
|
188
|
+
text = input.val().trim()
|
189
|
+
if text
|
190
|
+
chat = @chats[@currentContact]
|
191
|
+
jid = if chat then chat.jid else @currentContact
|
192
|
+
this.message
|
193
|
+
from: @session.jid()
|
194
|
+
text: text
|
195
|
+
to: jid
|
196
|
+
received: new Date()
|
197
|
+
@session.sendMessage jid, text
|
198
|
+
input.val ''
|
199
|
+
false
|
200
|
+
|
201
|
+
addContact: ->
|
202
|
+
this.toggleForm '#add-contact-form'
|
203
|
+
contact =
|
204
|
+
jid: $('#add-contact-jid').val()
|
205
|
+
name: $('#add-contact-name').val()
|
206
|
+
groups: ['Buddies']
|
207
|
+
@session.updateContact contact, true if contact.jid
|
208
|
+
false
|
209
|
+
|
210
|
+
removeContact: ->
|
211
|
+
this.toggleForm '#remove-contact-form'
|
212
|
+
@session.removeContact @currentContact
|
213
|
+
@currentContact = null
|
214
|
+
|
215
|
+
$('#chat-title').text 'Select a buddy to chat'
|
216
|
+
$('#messages').empty()
|
217
|
+
|
218
|
+
$('#remove-contact-msg').html "Select a buddy in the list above to remove."
|
219
|
+
$('#remove-contact-form .buttons').hide()
|
220
|
+
|
221
|
+
$('#edit-contact-jid').text "Select a buddy in the list above to update."
|
222
|
+
$('#edit-contact-name').val ''
|
223
|
+
$('#edit-contact-form input').hide()
|
224
|
+
$('#edit-contact-form .buttons').hide()
|
225
|
+
false
|
226
|
+
|
227
|
+
updateContact: ->
|
228
|
+
this.toggleForm '#edit-contact-form'
|
229
|
+
contact =
|
230
|
+
jid: @currentContact
|
231
|
+
name: $('#edit-contact-name').val()
|
232
|
+
groups: @session.roster[@currentContact].groups
|
233
|
+
@session.updateContact contact
|
234
|
+
false
|
235
|
+
|
236
|
+
toggleForm: (form, fn) ->
|
237
|
+
form = $(form)
|
238
|
+
$('.contact-form').each ->
|
239
|
+
$(this).hide() unless this.id == form.attr 'id'
|
240
|
+
if form.is ':hidden'
|
241
|
+
fn() if fn
|
242
|
+
form.fadeIn 100
|
243
|
+
else
|
244
|
+
form.fadeOut 100, ->
|
245
|
+
form[0].reset()
|
246
|
+
fn() if fn
|
247
|
+
|
248
|
+
filterRoster: ->
|
249
|
+
text = $('#search-roster-text').val().toLowerCase()
|
250
|
+
if text == ''
|
251
|
+
$('#roster li').show()
|
252
|
+
return
|
253
|
+
|
254
|
+
$('#roster li').each ->
|
255
|
+
node = $(this)
|
256
|
+
jid = (node.attr('data-jid') || '').toLowerCase()
|
257
|
+
name = (node.attr('data-name') || '').toLowerCase()
|
258
|
+
match = jid.indexOf(text) != -1 || name.indexOf(text) != -1
|
259
|
+
if match then node.show() else node.hide()
|
260
|
+
|
261
|
+
draw: ->
|
262
|
+
unless @session.connected()
|
263
|
+
window.location.hash = ''
|
264
|
+
return
|
265
|
+
|
266
|
+
$('body').attr 'id', 'chat-page'
|
267
|
+
$('#container').hide().empty()
|
268
|
+
$("""
|
269
|
+
<div id="alpha" class="y-fill">
|
270
|
+
<h2>Buddies <div id="search-roster"></div></h2>
|
271
|
+
<form id="search-roster-form" style="display:none;">
|
272
|
+
<input id="search-roster-text" type="search" placeholder="Filter" results="5"/>
|
273
|
+
</form>
|
274
|
+
<ul id="roster" class="y-fill"></ul>
|
275
|
+
<div id="roster-controls">
|
276
|
+
<div id="add-contact"></div>
|
277
|
+
<div id="remove-contact"></div>
|
278
|
+
<div id="edit-contact"></div>
|
279
|
+
</div>
|
280
|
+
<form id="add-contact-form" class="contact-form" style="display:none;">
|
281
|
+
<h2>Add Buddy</h2>
|
282
|
+
<input id="add-contact-jid" type="email" maxlength="1024" placeholder="Account name"/>
|
283
|
+
<input id="add-contact-name" type="text" maxlength="1024" placeholder="Real name"/>
|
284
|
+
<fieldset class="buttons">
|
285
|
+
<input id="add-contact-cancel" type="button" value="Cancel"/>
|
286
|
+
<input id="add-contact-ok" type="submit" value="Add"/>
|
287
|
+
</fieldset>
|
288
|
+
</form>
|
289
|
+
<form id="remove-contact-form" class="contact-form" style="display:none;">
|
290
|
+
<h2>Remove Buddy</h2>
|
291
|
+
<p id="remove-contact-msg">Select a buddy in the list above to remove.</p>
|
292
|
+
<fieldset class="buttons" style="display:none;">
|
293
|
+
<input id="remove-contact-cancel" type="button" value="Cancel"/>
|
294
|
+
<input id="remove-contact-ok" type="submit" value="Remove"/>
|
295
|
+
</fieldset>
|
296
|
+
</form>
|
297
|
+
<form id="edit-contact-form" class="contact-form" style="display:none;">
|
298
|
+
<h2>Update Profile</h2>
|
299
|
+
<p id="edit-contact-jid">Select a buddy in the list above to update.</p>
|
300
|
+
<input id="edit-contact-name" type="text" maxlength="1024" placeholder="Real name" style="display:none;"/>
|
301
|
+
<fieldset class="buttons" style="display:none;">
|
302
|
+
<input id="edit-contact-cancel" type="button" value="Cancel"/>
|
303
|
+
<input id="edit-contact-ok" type="submit" value="Save"/>
|
304
|
+
</fieldset>
|
305
|
+
</form>
|
306
|
+
</div>
|
307
|
+
<div id="beta" class="x-fill y-fill">
|
308
|
+
<h2 id="chat-title">Select a buddy to chat</h2>
|
309
|
+
<ul id="messages" class="y-fill"></ul>
|
310
|
+
<form id="message-form">
|
311
|
+
<input id="message" name="message" type="text" maxlength="1024" placeholder="Type a message and press enter to send"/>
|
312
|
+
</form>
|
313
|
+
</div>
|
314
|
+
<div id="charlie" class="y-fill">
|
315
|
+
<h2>Notifications</h2>
|
316
|
+
<ul id="notifications" class="y-fill"></ul>
|
317
|
+
<div id="notification-controls">
|
318
|
+
<div id="clear-notices"></div>
|
319
|
+
</div>
|
320
|
+
</div>
|
321
|
+
""").appendTo '#container'
|
322
|
+
|
323
|
+
this.roster()
|
324
|
+
this.button 'clear-notices', ICONS.no
|
325
|
+
this.button 'add-contact', ICONS.plus
|
326
|
+
this.button 'remove-contact', ICONS.minus
|
327
|
+
this.button 'edit-contact', ICONS.user
|
328
|
+
this.button 'search-roster', ICONS.search, scale: 0.5, translation: '-8 -8'
|
329
|
+
|
330
|
+
$('#message').focus -> $('.contact-form').fadeOut()
|
331
|
+
$('#message-form').submit => this.send()
|
332
|
+
|
333
|
+
$('#clear-notices').click -> $('#notifications li').fadeOut 200
|
334
|
+
|
335
|
+
$('#add-contact').click => this.toggleForm '#add-contact-form'
|
336
|
+
$('#remove-contact').click => this.toggleForm '#remove-contact-form'
|
337
|
+
$('#edit-contact').click => this.toggleForm '#edit-contact-form', =>
|
338
|
+
if @currentContact
|
339
|
+
$('#edit-contact-jid').text @currentContact
|
340
|
+
$('#edit-contact-name').val @session.roster[@currentContact].name
|
341
|
+
|
342
|
+
$('#add-contact-cancel').click => this.toggleForm '#add-contact-form'
|
343
|
+
$('#remove-contact-cancel').click => this.toggleForm '#remove-contact-form'
|
344
|
+
$('#edit-contact-cancel').click => this.toggleForm '#edit-contact-form'
|
345
|
+
|
346
|
+
$('#add-contact-form').submit => this.addContact()
|
347
|
+
$('#remove-contact-form').submit => this.removeContact()
|
348
|
+
$('#edit-contact-form').submit => this.updateContact()
|
349
|
+
$('#search-roster-form').submit -> false
|
350
|
+
|
351
|
+
$('#search-roster-text').keyup => this.filterRoster()
|
352
|
+
$('#search-roster-text').change => this.filterRoster()
|
353
|
+
$('#search-roster-text').click => this.filterRoster()
|
354
|
+
$('#search-roster').click =>
|
355
|
+
this.toggleForm '#search-roster-form', => this.filterRoster()
|
356
|
+
|
357
|
+
$('#container').fadeIn 200
|
358
|
+
this.resize()
|
359
|
+
|
360
|
+
resize: ->
|
361
|
+
a = $ '#alpha'
|
362
|
+
b = $ '#beta'
|
363
|
+
c = $ '#charlie'
|
364
|
+
msg = $ '#message'
|
365
|
+
form = $ '#message-form'
|
366
|
+
new Layout ->
|
367
|
+
c.css 'left', a.width() + b.width()
|
368
|
+
msg.width form.width() - 32
|
369
|
+
|
370
|
+
button: (id, path, options) ->
|
371
|
+
options ||= {}
|
372
|
+
paper = Raphael(id)
|
373
|
+
icon = paper.path(path).attr
|
374
|
+
fill: '#000'
|
375
|
+
stroke: '#fff'
|
376
|
+
'stroke-width': 0.3
|
377
|
+
opacity: 0.6
|
378
|
+
scale: options.scale || 0.85
|
379
|
+
translation: options.translation || ''
|
380
|
+
|
381
|
+
node = $('#' + id)
|
382
|
+
node.hover(
|
383
|
+
-> icon.animate(opacity: 1.0, 200),
|
384
|
+
-> icon.animate(opacity: 0.6, 200))
|
385
|
+
node.get 0
|