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.
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