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