vinesmod 0.4.5
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/Gemfile +3 -0
- data/LICENSE +19 -0
- data/README.md +43 -0
- data/Rakefile +57 -0
- data/bin/vines +93 -0
- data/conf/certs/README +39 -0
- data/conf/certs/ca-bundle.crt +3366 -0
- data/conf/config.rb +149 -0
- data/lib/vines.rb +197 -0
- data/lib/vines/cluster.rb +246 -0
- data/lib/vines/cluster/connection.rb +26 -0
- data/lib/vines/cluster/publisher.rb +55 -0
- data/lib/vines/cluster/pubsub.rb +92 -0
- data/lib/vines/cluster/sessions.rb +125 -0
- data/lib/vines/cluster/subscriber.rb +108 -0
- data/lib/vines/command/bcrypt.rb +12 -0
- data/lib/vines/command/cert.rb +50 -0
- data/lib/vines/command/init.rb +68 -0
- data/lib/vines/command/register.rb +27 -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/command/unregister.rb +27 -0
- data/lib/vines/config.rb +213 -0
- data/lib/vines/config/host.rb +119 -0
- data/lib/vines/config/port.rb +132 -0
- data/lib/vines/config/pubsub.rb +108 -0
- data/lib/vines/contact.rb +111 -0
- data/lib/vines/daemon.rb +78 -0
- data/lib/vines/error.rb +150 -0
- data/lib/vines/jid.rb +95 -0
- data/lib/vines/kit.rb +35 -0
- data/lib/vines/log.rb +24 -0
- data/lib/vines/router.rb +179 -0
- data/lib/vines/stanza.rb +175 -0
- data/lib/vines/stanza/iq.rb +48 -0
- data/lib/vines/stanza/iq/auth.rb +18 -0
- data/lib/vines/stanza/iq/disco_info.rb +45 -0
- data/lib/vines/stanza/iq/disco_items.rb +29 -0
- data/lib/vines/stanza/iq/error.rb +16 -0
- data/lib/vines/stanza/iq/ping.rb +16 -0
- data/lib/vines/stanza/iq/private_storage.rb +83 -0
- data/lib/vines/stanza/iq/query.rb +10 -0
- data/lib/vines/stanza/iq/register.rb +42 -0
- data/lib/vines/stanza/iq/result.rb +16 -0
- data/lib/vines/stanza/iq/roster.rb +140 -0
- data/lib/vines/stanza/iq/session.rb +17 -0
- data/lib/vines/stanza/iq/vcard.rb +56 -0
- data/lib/vines/stanza/iq/version.rb +25 -0
- data/lib/vines/stanza/message.rb +43 -0
- data/lib/vines/stanza/presence.rb +156 -0
- data/lib/vines/stanza/presence/error.rb +23 -0
- data/lib/vines/stanza/presence/probe.rb +37 -0
- data/lib/vines/stanza/presence/subscribe.rb +42 -0
- data/lib/vines/stanza/presence/subscribed.rb +51 -0
- data/lib/vines/stanza/presence/unavailable.rb +15 -0
- data/lib/vines/stanza/presence/unsubscribe.rb +38 -0
- data/lib/vines/stanza/presence/unsubscribed.rb +38 -0
- data/lib/vines/stanza/pubsub.rb +22 -0
- data/lib/vines/stanza/pubsub/create.rb +39 -0
- data/lib/vines/stanza/pubsub/delete.rb +41 -0
- data/lib/vines/stanza/pubsub/publish.rb +66 -0
- data/lib/vines/stanza/pubsub/subscribe.rb +44 -0
- data/lib/vines/stanza/pubsub/unsubscribe.rb +30 -0
- data/lib/vines/storage.rb +188 -0
- data/lib/vines/storage/local.rb +165 -0
- data/lib/vines/storage/null.rb +39 -0
- data/lib/vines/storage/sql.rb +260 -0
- data/lib/vines/store.rb +94 -0
- data/lib/vines/stream.rb +247 -0
- data/lib/vines/stream/client.rb +84 -0
- data/lib/vines/stream/client/auth.rb +74 -0
- data/lib/vines/stream/client/auth_restart.rb +29 -0
- data/lib/vines/stream/client/bind.rb +72 -0
- data/lib/vines/stream/client/bind_restart.rb +24 -0
- data/lib/vines/stream/client/closed.rb +13 -0
- data/lib/vines/stream/client/ready.rb +17 -0
- data/lib/vines/stream/client/session.rb +210 -0
- data/lib/vines/stream/client/start.rb +27 -0
- data/lib/vines/stream/client/tls.rb +38 -0
- data/lib/vines/stream/component.rb +58 -0
- data/lib/vines/stream/component/handshake.rb +26 -0
- data/lib/vines/stream/component/ready.rb +23 -0
- data/lib/vines/stream/component/start.rb +19 -0
- data/lib/vines/stream/http.rb +157 -0
- data/lib/vines/stream/http/auth.rb +22 -0
- data/lib/vines/stream/http/bind.rb +32 -0
- data/lib/vines/stream/http/bind_restart.rb +37 -0
- data/lib/vines/stream/http/ready.rb +29 -0
- data/lib/vines/stream/http/request.rb +172 -0
- data/lib/vines/stream/http/session.rb +120 -0
- data/lib/vines/stream/http/sessions.rb +65 -0
- data/lib/vines/stream/http/start.rb +23 -0
- data/lib/vines/stream/parser.rb +78 -0
- data/lib/vines/stream/sasl.rb +92 -0
- data/lib/vines/stream/server.rb +150 -0
- data/lib/vines/stream/server/auth.rb +13 -0
- data/lib/vines/stream/server/auth_restart.rb +13 -0
- data/lib/vines/stream/server/final_restart.rb +21 -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 +32 -0
- data/lib/vines/stream/server/outbound/final_features.rb +28 -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 +34 -0
- data/lib/vines/stream/server/ready.rb +24 -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 +60 -0
- data/lib/vines/token_bucket.rb +55 -0
- data/lib/vines/user.rb +123 -0
- data/lib/vines/version.rb +5 -0
- data/lib/vines/xmpp_server.rb +43 -0
- data/vines.gemspec +36 -0
- data/web/404.html +51 -0
- data/web/apple-touch-icon.png +0 -0
- data/web/chat/coffeescripts/chat.coffee +362 -0
- data/web/chat/coffeescripts/init.coffee +15 -0
- data/web/chat/index.html +16 -0
- data/web/chat/javascripts/app.js +1 -0
- data/web/chat/stylesheets/chat.css +144 -0
- data/web/favicon.png +0 -0
- data/web/lib/coffeescripts/button.coffee +25 -0
- data/web/lib/coffeescripts/contact.coffee +32 -0
- data/web/lib/coffeescripts/filter.coffee +49 -0
- data/web/lib/coffeescripts/layout.coffee +30 -0
- data/web/lib/coffeescripts/login.coffee +68 -0
- data/web/lib/coffeescripts/logout.coffee +5 -0
- data/web/lib/coffeescripts/navbar.coffee +84 -0
- data/web/lib/coffeescripts/notification.coffee +14 -0
- data/web/lib/coffeescripts/router.coffee +40 -0
- data/web/lib/coffeescripts/session.coffee +229 -0
- data/web/lib/coffeescripts/transfer.coffee +106 -0
- data/web/lib/images/dark-gray.png +0 -0
- data/web/lib/images/default-user.png +0 -0
- data/web/lib/images/light-gray.png +0 -0
- data/web/lib/images/logo-large.png +0 -0
- data/web/lib/images/logo-small.png +0 -0
- data/web/lib/images/white.png +0 -0
- data/web/lib/javascripts/base.js +12 -0
- data/web/lib/javascripts/icons.js +110 -0
- data/web/lib/javascripts/jquery.cookie.js +91 -0
- data/web/lib/javascripts/jquery.js +4 -0
- data/web/lib/javascripts/raphael.js +6 -0
- data/web/lib/javascripts/strophe.js +1 -0
- data/web/lib/stylesheets/base.css +385 -0
- data/web/lib/stylesheets/login.css +68 -0
- metadata +423 -0
data/lib/vines/user.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
class User
|
5
|
+
include Comparable
|
6
|
+
|
7
|
+
attr_accessor :name, :password, :roster
|
8
|
+
attr_reader :jid
|
9
|
+
|
10
|
+
def initialize(args={})
|
11
|
+
@jid = JID.new(args[:jid])
|
12
|
+
raise ArgumentError, 'invalid jid' if @jid.empty?
|
13
|
+
|
14
|
+
@name = args[:name]
|
15
|
+
@password = args[:password]
|
16
|
+
@roster = args[:roster] || []
|
17
|
+
end
|
18
|
+
|
19
|
+
def <=>(user)
|
20
|
+
user.is_a?(User) ? self.jid.to_s <=> user.jid.to_s : nil
|
21
|
+
end
|
22
|
+
|
23
|
+
alias :eql? :==
|
24
|
+
|
25
|
+
def hash
|
26
|
+
jid.to_s.hash
|
27
|
+
end
|
28
|
+
|
29
|
+
# Update this user's information from the given user object.
|
30
|
+
def update_from(user)
|
31
|
+
@name = user.name
|
32
|
+
@password = user.password
|
33
|
+
@roster = user.roster.map {|c| c.clone }
|
34
|
+
end
|
35
|
+
|
36
|
+
# Return true if the jid is on this user's roster.
|
37
|
+
def contact?(jid)
|
38
|
+
!contact(jid).nil?
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns the contact with this jid or nil if not found.
|
42
|
+
def contact(jid)
|
43
|
+
bare = JID.new(jid).bare
|
44
|
+
@roster.find {|c| c.jid.bare == bare }
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns true if the user is subscribed to this contact's
|
48
|
+
# presence updates.
|
49
|
+
def subscribed_to?(jid)
|
50
|
+
contact = contact(jid)
|
51
|
+
contact && contact.subscribed_to?
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns true if the user has a presence subscription from this contact.
|
55
|
+
# The contact is subscribed to this user's presence.
|
56
|
+
def subscribed_from?(jid)
|
57
|
+
contact = contact(jid)
|
58
|
+
contact && contact.subscribed_from?
|
59
|
+
end
|
60
|
+
|
61
|
+
# Removes the contact with this jid from the user's roster.
|
62
|
+
def remove_contact(jid)
|
63
|
+
bare = JID.new(jid).bare
|
64
|
+
@roster.reject! {|c| c.jid.bare == bare }
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns a list of the contacts to which this user has
|
68
|
+
# successfully subscribed.
|
69
|
+
def subscribed_to_contacts
|
70
|
+
@roster.select {|c| c.subscribed_to? }
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns a list of the contacts that are subscribed to this user's
|
74
|
+
# presence updates.
|
75
|
+
def subscribed_from_contacts
|
76
|
+
@roster.select {|c| c.subscribed_from? }
|
77
|
+
end
|
78
|
+
|
79
|
+
# Update the contact's jid on this user's roster to signal that this user
|
80
|
+
# has requested the contact's permission to receive their presence updates.
|
81
|
+
def request_subscription(jid)
|
82
|
+
unless contact = contact(jid)
|
83
|
+
contact = Contact.new(:jid => jid)
|
84
|
+
@roster << contact
|
85
|
+
end
|
86
|
+
contact.ask = 'subscribe' if %w[none from].include?(contact.subscription)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Add the user's jid to this contact's roster with a subscription state of
|
90
|
+
# 'from.' This signals that this contact has approved a user's subscription.
|
91
|
+
def add_subscription_from(jid)
|
92
|
+
unless contact = contact(jid)
|
93
|
+
contact = Contact.new(:jid => jid)
|
94
|
+
@roster << contact
|
95
|
+
end
|
96
|
+
contact.subscribe_from
|
97
|
+
end
|
98
|
+
|
99
|
+
def remove_subscription_to(jid)
|
100
|
+
if contact = contact(jid)
|
101
|
+
contact.unsubscribe_to
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def remove_subscription_from(jid)
|
106
|
+
if contact = contact(jid)
|
107
|
+
contact.unsubscribe_from
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns this user's roster contacts as an iq query element.
|
112
|
+
def to_roster_xml(id)
|
113
|
+
doc = Nokogiri::XML::Document.new
|
114
|
+
doc.create_element('iq', 'id' => id, 'type' => 'result') do |el|
|
115
|
+
el << doc.create_element('query', 'xmlns' => 'jabber:iq:roster') do |query|
|
116
|
+
@roster.sort!.each do |contact|
|
117
|
+
query << contact.to_roster_xml
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Vines
|
4
|
+
|
5
|
+
# The main starting point for the XMPP server process. Starts the
|
6
|
+
# EventMachine processing loop and registers the XMPP protocol handler
|
7
|
+
# with the ports defined in the server configuration file.
|
8
|
+
class XmppServer
|
9
|
+
include Vines::Log
|
10
|
+
|
11
|
+
def initialize(config)
|
12
|
+
@config = config
|
13
|
+
end
|
14
|
+
|
15
|
+
def start
|
16
|
+
log.info('XMPP server started')
|
17
|
+
at_exit { log.fatal('XMPP server stopped') }
|
18
|
+
EM.epoll
|
19
|
+
EM.kqueue
|
20
|
+
|
21
|
+
u = UPnP::UPnP.new
|
22
|
+
log.info('UPnP started')
|
23
|
+
|
24
|
+
EM.run do
|
25
|
+
@config.ports.each do |port|
|
26
|
+
forwarded = true
|
27
|
+
|
28
|
+
begin
|
29
|
+
u.addPortMapping(port.settings[:port], port.settings[:port],
|
30
|
+
"TCP", port.stream.to_s, Kit.local_ip)
|
31
|
+
rescue UPnP::UPnPException
|
32
|
+
log.warn("Cannot forward port #{port.settings[:port]}")
|
33
|
+
forwarded = false
|
34
|
+
end
|
35
|
+
|
36
|
+
log.info("Forwarded port #{port.settings[:port]}") if forwarded
|
37
|
+
|
38
|
+
port.start
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/vines.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require './lib/vines/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'vinesmod'
|
5
|
+
s.version = Vines::VERSION
|
6
|
+
s.summary = %q[Vines is an XMPP chat server that's easy to install and run.]
|
7
|
+
s.description = %q[Vines is an XMPP chat server that supports thousands of simultaneous connections, using EventMachine and Nokogiri.]
|
8
|
+
|
9
|
+
s.authors = ['David Graham', 'Damian Lesiuk']
|
10
|
+
s.email = %w[ja@lesiuk.net]
|
11
|
+
s.homepage = ''
|
12
|
+
s.license = 'MIT'
|
13
|
+
|
14
|
+
s.files = Dir['[A-Z]*', 'vines.gemspec', '{bin,lib,conf,web}/**/*'] - ['Gemfile.lock']
|
15
|
+
s.test_files = Dir['test/**/*']
|
16
|
+
s.executables = %w[vines]
|
17
|
+
s.require_path = 'lib'
|
18
|
+
|
19
|
+
s.add_dependency 'activerecord', '~> 3.2.8'
|
20
|
+
s.add_dependency 'bcrypt-ruby', '~> 3.0.1'
|
21
|
+
s.add_dependency 'em-http-request', '~> 1.0.3'
|
22
|
+
s.add_dependency 'eventmachine', '~> 1.0.0'
|
23
|
+
s.add_dependency 'http_parser.rb', '~> 0.5.3'
|
24
|
+
s.add_dependency 'bson_ext', '~> 1.5.2'
|
25
|
+
s.add_dependency 'nokogiri', '~> 1.5.5'
|
26
|
+
s.add_dependency 'mupnp', '~> 0.2.0'
|
27
|
+
|
28
|
+
s.add_development_dependency 'minitest', '~> 3.4.0'
|
29
|
+
s.add_development_dependency 'coffee-script', '~> 2.2.0'
|
30
|
+
s.add_development_dependency 'coffee-script-source', '~> 1.3.3'
|
31
|
+
s.add_development_dependency 'uglifier', '~> 1.3.0'
|
32
|
+
s.add_development_dependency 'rake'
|
33
|
+
s.add_development_dependency 'sqlite3'
|
34
|
+
|
35
|
+
s.required_ruby_version = '>= 1.9.3'
|
36
|
+
end
|
data/web/404.html
ADDED
@@ -0,0 +1,51 @@
|
|
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(rgba(26, 55, 98, 0.8), rgba(12, 26, 45, 0.8)), url(/lib/images/dark-gray.png);
|
12
|
+
background: -ms-radial-gradient(center, 500px 500px, rgba(26, 55, 98, 0.8), rgba(12, 26, 45, 0.8)), url(/lib/images/dark-gray.png);
|
13
|
+
background: -o-radial-gradient(rgba(26, 55, 98, 0.8), rgba(12, 26, 45, 0.8)), url(/lib/images/dark-gray.png);
|
14
|
+
background: -webkit-radial-gradient(center, 500px 500px, rgba(26, 55, 98, 0.8), rgba(12, 26, 45, 0.8)), url(/lib/images/dark-gray.png);
|
15
|
+
display: table;
|
16
|
+
text-align: center;
|
17
|
+
width: 100%;
|
18
|
+
}
|
19
|
+
header {
|
20
|
+
display: table-cell;
|
21
|
+
vertical-align: middle;
|
22
|
+
width: 100%;
|
23
|
+
}
|
24
|
+
h1 {
|
25
|
+
background: url(/lib/images/logo-large.png) no-repeat center;
|
26
|
+
color: transparent;
|
27
|
+
height: 82px;
|
28
|
+
text-shadow: none;
|
29
|
+
width: 100%;
|
30
|
+
}
|
31
|
+
p {
|
32
|
+
color: rgba(255, 255, 255, 0.8);
|
33
|
+
font-size: 11pt;
|
34
|
+
margin: 20px auto;
|
35
|
+
width: 400px;
|
36
|
+
}
|
37
|
+
a {
|
38
|
+
color: inherit;
|
39
|
+
}
|
40
|
+
</style>
|
41
|
+
</head>
|
42
|
+
<body>
|
43
|
+
<header>
|
44
|
+
<h1>Page not found</h1>
|
45
|
+
<p>
|
46
|
+
This is not the page you're looking for. You probably wanted the
|
47
|
+
<a href="/chat/">chat</a> application.
|
48
|
+
</p>
|
49
|
+
</header>
|
50
|
+
</body>
|
51
|
+
</html>
|
Binary file
|
@@ -0,0 +1,362 @@
|
|
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
|
+
@layout = null
|
10
|
+
|
11
|
+
datef: (millis) ->
|
12
|
+
d = new Date(millis)
|
13
|
+
meridian = if d.getHours() >= 12 then ' pm' else ' am'
|
14
|
+
hour = if d.getHours() > 12 then d.getHours() - 12 else d.getHours()
|
15
|
+
hour = 12 if hour == 0
|
16
|
+
minutes = d.getMinutes() + ''
|
17
|
+
minutes = '0' + minutes if minutes.length == 1
|
18
|
+
hour + ':' + minutes + meridian
|
19
|
+
|
20
|
+
card: (card) ->
|
21
|
+
this.eachContact card.jid, (node) =>
|
22
|
+
$('.vcard-img', node).attr 'src', @session.avatar card.jid
|
23
|
+
|
24
|
+
roster: ->
|
25
|
+
roster = $('#roster')
|
26
|
+
|
27
|
+
$('li', roster).each (ix, node) =>
|
28
|
+
jid = $(node).attr('data-jid')
|
29
|
+
$(node).remove() unless @session.roster[jid]
|
30
|
+
|
31
|
+
setName = (node, contact) ->
|
32
|
+
$('.text', node).text contact.name || contact.jid
|
33
|
+
node.attr 'data-name', contact.name || ''
|
34
|
+
|
35
|
+
for jid, contact of @session.roster
|
36
|
+
found = $("#roster li[data-jid='#{jid}']")
|
37
|
+
setName(found, contact)
|
38
|
+
if found.length == 0
|
39
|
+
node = $("""
|
40
|
+
<li data-jid="#{jid}" data-name="" class="offline">
|
41
|
+
<span class="text"></span>
|
42
|
+
<span class="status-msg">Offline</span>
|
43
|
+
<span class="unread" style="display:none;"></span>
|
44
|
+
<img class="vcard-img" alt="#{jid}" src="#{@session.avatar jid}"/>
|
45
|
+
</li>
|
46
|
+
""").appendTo roster
|
47
|
+
setName(node, contact)
|
48
|
+
node.click (event) => this.selectContact(event)
|
49
|
+
|
50
|
+
message: (message) ->
|
51
|
+
return unless message.type == 'chat' && message.text
|
52
|
+
this.queueMessage message
|
53
|
+
me = message.from == @session.jid()
|
54
|
+
from = message.from.split('/')[0]
|
55
|
+
|
56
|
+
if me || from == @currentContact
|
57
|
+
bottom = this.atBottom()
|
58
|
+
this.appendMessage message
|
59
|
+
this.scroll() if bottom
|
60
|
+
else
|
61
|
+
chat = this.chat message.from
|
62
|
+
chat.unread++
|
63
|
+
this.eachContact from, (node) ->
|
64
|
+
$('.unread', node).text(chat.unread).show()
|
65
|
+
|
66
|
+
eachContact: (jid, callback) ->
|
67
|
+
for node in $("#roster li[data-jid='#{jid}']").get()
|
68
|
+
callback $(node)
|
69
|
+
|
70
|
+
appendMessage: (message) ->
|
71
|
+
from = message.from.split('/')[0]
|
72
|
+
contact = @session.roster[from]
|
73
|
+
name = if contact then (contact.name || from) else from
|
74
|
+
name = 'Me' if message.from == @session.jid()
|
75
|
+
node = $("""
|
76
|
+
<li data-jid="#{from}" style="display:none;">
|
77
|
+
<p></p>
|
78
|
+
<img alt="#{from}" src="#{@session.avatar from}"/>
|
79
|
+
<footer>
|
80
|
+
<span class="author"></span>
|
81
|
+
<span class="time">#{this.datef message.received}</span>
|
82
|
+
</footer>
|
83
|
+
</li>
|
84
|
+
""").appendTo '#messages'
|
85
|
+
|
86
|
+
$('p', node).text message.text
|
87
|
+
$('.author', node).text name
|
88
|
+
node.fadeIn 200
|
89
|
+
|
90
|
+
queueMessage: (message) ->
|
91
|
+
me = message.from == @session.jid()
|
92
|
+
full = message[if me then 'to' else 'from']
|
93
|
+
chat = this.chat full
|
94
|
+
chat.jid = full
|
95
|
+
chat.messages.push message
|
96
|
+
|
97
|
+
chat: (jid) ->
|
98
|
+
bare = jid.split('/')[0]
|
99
|
+
chat = @chats[bare]
|
100
|
+
unless chat
|
101
|
+
chat = jid: jid, messages: [], unread: 0
|
102
|
+
@chats[bare] = chat
|
103
|
+
chat
|
104
|
+
|
105
|
+
presence: (presence) ->
|
106
|
+
from = presence.from.split('/')[0]
|
107
|
+
return if from == @session.bareJid()
|
108
|
+
if !presence.type || presence.offline
|
109
|
+
contact = @session.roster[from]
|
110
|
+
this.eachContact from, (node) ->
|
111
|
+
$('.status-msg', node).text contact.status()
|
112
|
+
if contact.offline()
|
113
|
+
node.addClass 'offline'
|
114
|
+
else
|
115
|
+
node.removeClass 'offline'
|
116
|
+
|
117
|
+
if presence.offline
|
118
|
+
this.chat(from).jid = from
|
119
|
+
|
120
|
+
if presence.type == 'subscribe'
|
121
|
+
node = $("""
|
122
|
+
<li data-jid="#{presence.from}" style="display:none;">
|
123
|
+
<form class="inset">
|
124
|
+
<h2>Buddy Approval</h2>
|
125
|
+
<p>#{presence.from} wants to add you as a buddy.</p>
|
126
|
+
<fieldset class="buttons">
|
127
|
+
<input type="button" value="Decline"/>
|
128
|
+
<input type="submit" value="Accept"/>
|
129
|
+
</fieldset>
|
130
|
+
</form>
|
131
|
+
</li>
|
132
|
+
""").appendTo '#notifications'
|
133
|
+
node.fadeIn 200
|
134
|
+
$('form', node).submit => this.acceptContact node, presence.from
|
135
|
+
$('input[type="button"]', node).click => this.rejectContact node, presence.from
|
136
|
+
|
137
|
+
acceptContact: (node, jid) ->
|
138
|
+
node.fadeOut 200, -> node.remove()
|
139
|
+
@session.sendSubscribed jid
|
140
|
+
@session.sendSubscribe jid
|
141
|
+
false
|
142
|
+
|
143
|
+
rejectContact: (node, jid) ->
|
144
|
+
node.fadeOut 200, -> node.remove()
|
145
|
+
@session.sendUnsubscribed jid
|
146
|
+
|
147
|
+
selectContact: (event) ->
|
148
|
+
jid = $(event.currentTarget).attr 'data-jid'
|
149
|
+
contact = @session.roster[jid]
|
150
|
+
return if @currentContact == jid
|
151
|
+
@currentContact = jid
|
152
|
+
|
153
|
+
$('#roster li').removeClass 'selected'
|
154
|
+
$(event.currentTarget).addClass 'selected'
|
155
|
+
$('#chat-title').text('Chat with ' + (contact.name || contact.jid))
|
156
|
+
$('#messages').empty()
|
157
|
+
|
158
|
+
chat = @chats[jid]
|
159
|
+
messages = []
|
160
|
+
if chat
|
161
|
+
messages = chat.messages
|
162
|
+
chat.unread = 0
|
163
|
+
this.eachContact jid, (node) ->
|
164
|
+
$('.unread', node).text('').hide()
|
165
|
+
|
166
|
+
this.appendMessage msg for msg in messages
|
167
|
+
this.scroll()
|
168
|
+
|
169
|
+
$('#remove-contact-msg').html "Are you sure you want to remove " +
|
170
|
+
"<strong>#{@currentContact}</strong> from your buddy list?"
|
171
|
+
$('#remove-contact-form .buttons').fadeIn 200
|
172
|
+
|
173
|
+
$('#edit-contact-jid').text @currentContact
|
174
|
+
$('#edit-contact-name').val @session.roster[@currentContact].name
|
175
|
+
$('#edit-contact-form input').fadeIn 200
|
176
|
+
$('#edit-contact-form .buttons').fadeIn 200
|
177
|
+
|
178
|
+
scroll: ->
|
179
|
+
msgs = $ '#messages'
|
180
|
+
msgs.animate(scrollTop: msgs.prop('scrollHeight'), 400)
|
181
|
+
|
182
|
+
atBottom: ->
|
183
|
+
msgs = $('#messages')
|
184
|
+
bottom = msgs.prop('scrollHeight') - msgs.outerHeight()
|
185
|
+
msgs.scrollTop() >= bottom
|
186
|
+
|
187
|
+
send: ->
|
188
|
+
return false unless @currentContact
|
189
|
+
input = $('#message')
|
190
|
+
text = input.val().trim()
|
191
|
+
if text
|
192
|
+
chat = @chats[@currentContact]
|
193
|
+
jid = if chat then chat.jid else @currentContact
|
194
|
+
this.message
|
195
|
+
from: @session.jid()
|
196
|
+
text: text
|
197
|
+
to: jid
|
198
|
+
type: 'chat'
|
199
|
+
received: new Date()
|
200
|
+
@session.sendMessage jid, text
|
201
|
+
input.val ''
|
202
|
+
false
|
203
|
+
|
204
|
+
addContact: ->
|
205
|
+
this.toggleForm '#add-contact-form'
|
206
|
+
contact =
|
207
|
+
jid: $('#add-contact-jid').val()
|
208
|
+
name: $('#add-contact-name').val()
|
209
|
+
groups: ['Buddies']
|
210
|
+
@session.updateContact contact, true if contact.jid
|
211
|
+
false
|
212
|
+
|
213
|
+
removeContact: ->
|
214
|
+
this.toggleForm '#remove-contact-form'
|
215
|
+
@session.removeContact @currentContact
|
216
|
+
@currentContact = null
|
217
|
+
|
218
|
+
$('#chat-title').text 'Select a buddy to chat'
|
219
|
+
$('#messages').empty()
|
220
|
+
|
221
|
+
$('#remove-contact-msg').html "Select a buddy in the list above to remove."
|
222
|
+
$('#remove-contact-form .buttons').hide()
|
223
|
+
|
224
|
+
$('#edit-contact-jid').text "Select a buddy in the list above to update."
|
225
|
+
$('#edit-contact-name').val ''
|
226
|
+
$('#edit-contact-form input').hide()
|
227
|
+
$('#edit-contact-form .buttons').hide()
|
228
|
+
false
|
229
|
+
|
230
|
+
updateContact: ->
|
231
|
+
this.toggleForm '#edit-contact-form'
|
232
|
+
contact =
|
233
|
+
jid: @currentContact
|
234
|
+
name: $('#edit-contact-name').val()
|
235
|
+
groups: @session.roster[@currentContact].groups
|
236
|
+
@session.updateContact contact
|
237
|
+
false
|
238
|
+
|
239
|
+
toggleForm: (form, fn) ->
|
240
|
+
form = $(form)
|
241
|
+
$('form.overlay').each ->
|
242
|
+
$(this).hide() unless this.id == form.attr 'id'
|
243
|
+
if form.is ':hidden'
|
244
|
+
fn() if fn
|
245
|
+
form.fadeIn 100
|
246
|
+
else
|
247
|
+
form.fadeOut 100, =>
|
248
|
+
form[0].reset()
|
249
|
+
@layout.resize()
|
250
|
+
fn() if fn
|
251
|
+
|
252
|
+
draw: ->
|
253
|
+
unless @session.connected()
|
254
|
+
window.location.hash = ''
|
255
|
+
return
|
256
|
+
|
257
|
+
$('body').attr 'id', 'chat-page'
|
258
|
+
$('#container').hide().empty()
|
259
|
+
$("""
|
260
|
+
<div id="alpha" class="sidebar column y-fill">
|
261
|
+
<h2>Buddies <div id="search-roster-icon"></div></h2>
|
262
|
+
<div id="search-roster-form"></div>
|
263
|
+
<ul id="roster" class="selectable scroll y-fill"></ul>
|
264
|
+
<div id="alpha-controls" class="controls">
|
265
|
+
<div id="add-contact"></div>
|
266
|
+
<div id="remove-contact"></div>
|
267
|
+
<div id="edit-contact"></div>
|
268
|
+
</div>
|
269
|
+
<form id="add-contact-form" class="overlay" style="display:none;">
|
270
|
+
<h2>Add Buddy</h2>
|
271
|
+
<input id="add-contact-jid" type="email" maxlength="1024" placeholder="Account name"/>
|
272
|
+
<input id="add-contact-name" type="text" maxlength="1024" placeholder="Real name"/>
|
273
|
+
<fieldset class="buttons">
|
274
|
+
<input id="add-contact-cancel" type="button" value="Cancel"/>
|
275
|
+
<input id="add-contact-ok" type="submit" value="Add"/>
|
276
|
+
</fieldset>
|
277
|
+
</form>
|
278
|
+
<form id="remove-contact-form" class="overlay" style="display:none;">
|
279
|
+
<h2>Remove Buddy</h2>
|
280
|
+
<p id="remove-contact-msg">Select a buddy in the list above to remove.</p>
|
281
|
+
<fieldset class="buttons" style="display:none;">
|
282
|
+
<input id="remove-contact-cancel" type="button" value="Cancel"/>
|
283
|
+
<input id="remove-contact-ok" type="submit" value="Remove"/>
|
284
|
+
</fieldset>
|
285
|
+
</form>
|
286
|
+
<form id="edit-contact-form" class="overlay" style="display:none;">
|
287
|
+
<h2>Update Profile</h2>
|
288
|
+
<p id="edit-contact-jid">Select a buddy in the list above to update.</p>
|
289
|
+
<input id="edit-contact-name" type="text" maxlength="1024" placeholder="Real name" style="display:none;"/>
|
290
|
+
<fieldset class="buttons" style="display:none;">
|
291
|
+
<input id="edit-contact-cancel" type="button" value="Cancel"/>
|
292
|
+
<input id="edit-contact-ok" type="submit" value="Save"/>
|
293
|
+
</fieldset>
|
294
|
+
</form>
|
295
|
+
</div>
|
296
|
+
<div id="beta" class="primary column x-fill y-fill">
|
297
|
+
<h2 id="chat-title">Select a buddy to chat</h2>
|
298
|
+
<ul id="messages" class="scroll y-fill"></ul>
|
299
|
+
<form id="message-form">
|
300
|
+
<input id="message" name="message" type="text" maxlength="1024" placeholder="Type a message and press enter to send"/>
|
301
|
+
</form>
|
302
|
+
</div>
|
303
|
+
<div id="charlie" class="sidebar column y-fill">
|
304
|
+
<h2>Notifications</h2>
|
305
|
+
<ul id="notifications" class="scroll y-fill"></ul>
|
306
|
+
<div id="charlie-controls" class="controls">
|
307
|
+
<div id="clear-notices"></div>
|
308
|
+
</div>
|
309
|
+
</div>
|
310
|
+
""").appendTo '#container'
|
311
|
+
|
312
|
+
this.roster()
|
313
|
+
|
314
|
+
new Button '#clear-notices', ICONS.no
|
315
|
+
new Button '#add-contact', ICONS.plus
|
316
|
+
new Button '#remove-contact', ICONS.minus
|
317
|
+
new Button '#edit-contact', ICONS.user
|
318
|
+
|
319
|
+
$('#message').focus -> $('form.overlay').fadeOut()
|
320
|
+
$('#message-form').submit => this.send()
|
321
|
+
|
322
|
+
$('#clear-notices').click -> $('#notifications li').fadeOut 200
|
323
|
+
|
324
|
+
$('#add-contact').click => this.toggleForm '#add-contact-form'
|
325
|
+
$('#remove-contact').click => this.toggleForm '#remove-contact-form'
|
326
|
+
$('#edit-contact').click => this.toggleForm '#edit-contact-form', =>
|
327
|
+
if @currentContact
|
328
|
+
$('#edit-contact-jid').text @currentContact
|
329
|
+
$('#edit-contact-name').val @session.roster[@currentContact].name
|
330
|
+
|
331
|
+
$('#add-contact-cancel').click => this.toggleForm '#add-contact-form'
|
332
|
+
$('#remove-contact-cancel').click => this.toggleForm '#remove-contact-form'
|
333
|
+
$('#edit-contact-cancel').click => this.toggleForm '#edit-contact-form'
|
334
|
+
|
335
|
+
$('#add-contact-form').submit => this.addContact()
|
336
|
+
$('#remove-contact-form').submit => this.removeContact()
|
337
|
+
$('#edit-contact-form').submit => this.updateContact()
|
338
|
+
|
339
|
+
$('#container').fadeIn 200
|
340
|
+
@layout = this.resize()
|
341
|
+
|
342
|
+
fn = =>
|
343
|
+
@layout.resize()
|
344
|
+
@layout.resize() # not sure why two are needed
|
345
|
+
|
346
|
+
new Filter
|
347
|
+
list: '#roster'
|
348
|
+
icon: '#search-roster-icon'
|
349
|
+
form: '#search-roster-form'
|
350
|
+
attrs: ['data-jid', 'data-name']
|
351
|
+
open: fn
|
352
|
+
close: fn
|
353
|
+
|
354
|
+
resize: ->
|
355
|
+
a = $ '#alpha'
|
356
|
+
b = $ '#beta'
|
357
|
+
c = $ '#charlie'
|
358
|
+
msg = $ '#message'
|
359
|
+
form = $ '#message-form'
|
360
|
+
new Layout ->
|
361
|
+
c.css 'left', a.width() + b.width()
|
362
|
+
msg.width form.width() - 32
|