vines 0.1.1 → 0.2.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/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
|