vinesmod 0.4.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (151) hide show
  1. data/Gemfile +3 -0
  2. data/LICENSE +19 -0
  3. data/README.md +43 -0
  4. data/Rakefile +57 -0
  5. data/bin/vines +93 -0
  6. data/conf/certs/README +39 -0
  7. data/conf/certs/ca-bundle.crt +3366 -0
  8. data/conf/config.rb +149 -0
  9. data/lib/vines.rb +197 -0
  10. data/lib/vines/cluster.rb +246 -0
  11. data/lib/vines/cluster/connection.rb +26 -0
  12. data/lib/vines/cluster/publisher.rb +55 -0
  13. data/lib/vines/cluster/pubsub.rb +92 -0
  14. data/lib/vines/cluster/sessions.rb +125 -0
  15. data/lib/vines/cluster/subscriber.rb +108 -0
  16. data/lib/vines/command/bcrypt.rb +12 -0
  17. data/lib/vines/command/cert.rb +50 -0
  18. data/lib/vines/command/init.rb +68 -0
  19. data/lib/vines/command/register.rb +27 -0
  20. data/lib/vines/command/restart.rb +12 -0
  21. data/lib/vines/command/schema.rb +24 -0
  22. data/lib/vines/command/start.rb +28 -0
  23. data/lib/vines/command/stop.rb +18 -0
  24. data/lib/vines/command/unregister.rb +27 -0
  25. data/lib/vines/config.rb +213 -0
  26. data/lib/vines/config/host.rb +119 -0
  27. data/lib/vines/config/port.rb +132 -0
  28. data/lib/vines/config/pubsub.rb +108 -0
  29. data/lib/vines/contact.rb +111 -0
  30. data/lib/vines/daemon.rb +78 -0
  31. data/lib/vines/error.rb +150 -0
  32. data/lib/vines/jid.rb +95 -0
  33. data/lib/vines/kit.rb +35 -0
  34. data/lib/vines/log.rb +24 -0
  35. data/lib/vines/router.rb +179 -0
  36. data/lib/vines/stanza.rb +175 -0
  37. data/lib/vines/stanza/iq.rb +48 -0
  38. data/lib/vines/stanza/iq/auth.rb +18 -0
  39. data/lib/vines/stanza/iq/disco_info.rb +45 -0
  40. data/lib/vines/stanza/iq/disco_items.rb +29 -0
  41. data/lib/vines/stanza/iq/error.rb +16 -0
  42. data/lib/vines/stanza/iq/ping.rb +16 -0
  43. data/lib/vines/stanza/iq/private_storage.rb +83 -0
  44. data/lib/vines/stanza/iq/query.rb +10 -0
  45. data/lib/vines/stanza/iq/register.rb +42 -0
  46. data/lib/vines/stanza/iq/result.rb +16 -0
  47. data/lib/vines/stanza/iq/roster.rb +140 -0
  48. data/lib/vines/stanza/iq/session.rb +17 -0
  49. data/lib/vines/stanza/iq/vcard.rb +56 -0
  50. data/lib/vines/stanza/iq/version.rb +25 -0
  51. data/lib/vines/stanza/message.rb +43 -0
  52. data/lib/vines/stanza/presence.rb +156 -0
  53. data/lib/vines/stanza/presence/error.rb +23 -0
  54. data/lib/vines/stanza/presence/probe.rb +37 -0
  55. data/lib/vines/stanza/presence/subscribe.rb +42 -0
  56. data/lib/vines/stanza/presence/subscribed.rb +51 -0
  57. data/lib/vines/stanza/presence/unavailable.rb +15 -0
  58. data/lib/vines/stanza/presence/unsubscribe.rb +38 -0
  59. data/lib/vines/stanza/presence/unsubscribed.rb +38 -0
  60. data/lib/vines/stanza/pubsub.rb +22 -0
  61. data/lib/vines/stanza/pubsub/create.rb +39 -0
  62. data/lib/vines/stanza/pubsub/delete.rb +41 -0
  63. data/lib/vines/stanza/pubsub/publish.rb +66 -0
  64. data/lib/vines/stanza/pubsub/subscribe.rb +44 -0
  65. data/lib/vines/stanza/pubsub/unsubscribe.rb +30 -0
  66. data/lib/vines/storage.rb +188 -0
  67. data/lib/vines/storage/local.rb +165 -0
  68. data/lib/vines/storage/null.rb +39 -0
  69. data/lib/vines/storage/sql.rb +260 -0
  70. data/lib/vines/store.rb +94 -0
  71. data/lib/vines/stream.rb +247 -0
  72. data/lib/vines/stream/client.rb +84 -0
  73. data/lib/vines/stream/client/auth.rb +74 -0
  74. data/lib/vines/stream/client/auth_restart.rb +29 -0
  75. data/lib/vines/stream/client/bind.rb +72 -0
  76. data/lib/vines/stream/client/bind_restart.rb +24 -0
  77. data/lib/vines/stream/client/closed.rb +13 -0
  78. data/lib/vines/stream/client/ready.rb +17 -0
  79. data/lib/vines/stream/client/session.rb +210 -0
  80. data/lib/vines/stream/client/start.rb +27 -0
  81. data/lib/vines/stream/client/tls.rb +38 -0
  82. data/lib/vines/stream/component.rb +58 -0
  83. data/lib/vines/stream/component/handshake.rb +26 -0
  84. data/lib/vines/stream/component/ready.rb +23 -0
  85. data/lib/vines/stream/component/start.rb +19 -0
  86. data/lib/vines/stream/http.rb +157 -0
  87. data/lib/vines/stream/http/auth.rb +22 -0
  88. data/lib/vines/stream/http/bind.rb +32 -0
  89. data/lib/vines/stream/http/bind_restart.rb +37 -0
  90. data/lib/vines/stream/http/ready.rb +29 -0
  91. data/lib/vines/stream/http/request.rb +172 -0
  92. data/lib/vines/stream/http/session.rb +120 -0
  93. data/lib/vines/stream/http/sessions.rb +65 -0
  94. data/lib/vines/stream/http/start.rb +23 -0
  95. data/lib/vines/stream/parser.rb +78 -0
  96. data/lib/vines/stream/sasl.rb +92 -0
  97. data/lib/vines/stream/server.rb +150 -0
  98. data/lib/vines/stream/server/auth.rb +13 -0
  99. data/lib/vines/stream/server/auth_restart.rb +13 -0
  100. data/lib/vines/stream/server/final_restart.rb +21 -0
  101. data/lib/vines/stream/server/outbound/auth.rb +31 -0
  102. data/lib/vines/stream/server/outbound/auth_restart.rb +20 -0
  103. data/lib/vines/stream/server/outbound/auth_result.rb +32 -0
  104. data/lib/vines/stream/server/outbound/final_features.rb +28 -0
  105. data/lib/vines/stream/server/outbound/final_restart.rb +20 -0
  106. data/lib/vines/stream/server/outbound/start.rb +20 -0
  107. data/lib/vines/stream/server/outbound/tls.rb +30 -0
  108. data/lib/vines/stream/server/outbound/tls_result.rb +34 -0
  109. data/lib/vines/stream/server/ready.rb +24 -0
  110. data/lib/vines/stream/server/start.rb +13 -0
  111. data/lib/vines/stream/server/tls.rb +13 -0
  112. data/lib/vines/stream/state.rb +60 -0
  113. data/lib/vines/token_bucket.rb +55 -0
  114. data/lib/vines/user.rb +123 -0
  115. data/lib/vines/version.rb +5 -0
  116. data/lib/vines/xmpp_server.rb +43 -0
  117. data/vines.gemspec +36 -0
  118. data/web/404.html +51 -0
  119. data/web/apple-touch-icon.png +0 -0
  120. data/web/chat/coffeescripts/chat.coffee +362 -0
  121. data/web/chat/coffeescripts/init.coffee +15 -0
  122. data/web/chat/index.html +16 -0
  123. data/web/chat/javascripts/app.js +1 -0
  124. data/web/chat/stylesheets/chat.css +144 -0
  125. data/web/favicon.png +0 -0
  126. data/web/lib/coffeescripts/button.coffee +25 -0
  127. data/web/lib/coffeescripts/contact.coffee +32 -0
  128. data/web/lib/coffeescripts/filter.coffee +49 -0
  129. data/web/lib/coffeescripts/layout.coffee +30 -0
  130. data/web/lib/coffeescripts/login.coffee +68 -0
  131. data/web/lib/coffeescripts/logout.coffee +5 -0
  132. data/web/lib/coffeescripts/navbar.coffee +84 -0
  133. data/web/lib/coffeescripts/notification.coffee +14 -0
  134. data/web/lib/coffeescripts/router.coffee +40 -0
  135. data/web/lib/coffeescripts/session.coffee +229 -0
  136. data/web/lib/coffeescripts/transfer.coffee +106 -0
  137. data/web/lib/images/dark-gray.png +0 -0
  138. data/web/lib/images/default-user.png +0 -0
  139. data/web/lib/images/light-gray.png +0 -0
  140. data/web/lib/images/logo-large.png +0 -0
  141. data/web/lib/images/logo-small.png +0 -0
  142. data/web/lib/images/white.png +0 -0
  143. data/web/lib/javascripts/base.js +12 -0
  144. data/web/lib/javascripts/icons.js +110 -0
  145. data/web/lib/javascripts/jquery.cookie.js +91 -0
  146. data/web/lib/javascripts/jquery.js +4 -0
  147. data/web/lib/javascripts/raphael.js +6 -0
  148. data/web/lib/javascripts/strophe.js +1 -0
  149. data/web/lib/stylesheets/base.css +385 -0
  150. data/web/lib/stylesheets/login.css +68 -0
  151. 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,5 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ VERSION = '0.4.5'
5
+ 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