├── .gitignore
├── Gemfile
├── Gemfile.lock
├── MIT-LICENSE
├── README.md
├── Rakefile
├── alondra.gemspec
├── app
├── assets
│ ├── javascripts
│ │ ├── alondra-client.js.coffee.erb
│ │ ├── moz_websocket.js
│ │ └── vendor
│ │ │ ├── jquery.json-2.2.js
│ │ │ ├── json2.js
│ │ │ ├── swfobject.js
│ │ │ └── web_socket.js
│ └── swf
│ │ └── WebSocketMain.swf
└── helpers
│ └── alondra_helper.rb
├── bin
└── alondra
├── lib
├── alondra.rb
├── alondra
│ ├── changes_callbacks.rb
│ ├── changes_push.rb
│ ├── channel.rb
│ ├── command.rb
│ ├── command_dispatcher.rb
│ ├── connection.rb
│ ├── event.rb
│ ├── event_listener.rb
│ ├── event_router.rb
│ ├── listener_callback.rb
│ ├── log.rb
│ ├── message.rb
│ ├── message_queue.rb
│ ├── message_queue_client.rb
│ ├── push_controller.rb
│ ├── pushing.rb
│ ├── server.rb
│ ├── session_parser.rb
│ └── version.rb
├── generators
│ └── alondra
│ │ ├── USAGE
│ │ ├── alondra_generator.rb
│ │ └── templates
│ │ └── alondra
└── tasks
│ └── alondra_tasks.rake
├── script
└── rails
└── test
├── dummy
├── Rakefile
├── app
│ ├── assets
│ │ ├── javascripts
│ │ │ └── application.js
│ │ └── stylesheets
│ │ │ └── application.css
│ ├── controllers
│ │ ├── application_controller.rb
│ │ ├── chats_controller.rb
│ │ ├── messages_controller.rb
│ │ ├── sessions_controller.rb
│ │ └── users_controller.rb
│ ├── helpers
│ │ ├── application_helper.rb
│ │ ├── chats_helper.rb
│ │ ├── error_messages_helper.rb
│ │ └── layout_helper.rb
│ ├── mailers
│ │ └── .gitkeep
│ ├── models
│ │ ├── .gitkeep
│ │ ├── chat.rb
│ │ ├── message.rb
│ │ └── user.rb
│ └── views
│ │ ├── chats
│ │ ├── _form.html.erb
│ │ ├── edit.html.erb
│ │ ├── index.html.erb
│ │ ├── new.html.erb
│ │ └── show.html.erb
│ │ ├── layouts
│ │ └── application.html.erb
│ │ ├── sessions
│ │ └── new.html.erb
│ │ ├── shared
│ │ └── _message.js.erb
│ │ └── users
│ │ ├── _form.html.erb
│ │ ├── edit.html.erb
│ │ ├── index.html.erb
│ │ ├── new.html.erb
│ │ └── show.html.erb
├── config.ru
├── config
│ ├── application.rb
│ ├── boot.rb
│ ├── database.yml
│ ├── environment.rb
│ ├── environments
│ │ ├── development.rb
│ │ ├── production.rb
│ │ └── test.rb
│ ├── initializers
│ │ ├── backtrace_silencers.rb
│ │ ├── inflections.rb
│ │ ├── mime_types.rb
│ │ ├── secret_token.rb
│ │ ├── session_store.rb
│ │ └── wrap_parameters.rb
│ ├── locales
│ │ └── en.yml
│ └── routes.rb
├── db
│ ├── migrate
│ │ ├── 20110719090458_create_chats.rb
│ │ ├── 20110719090538_create_messages.rb
│ │ └── 20110720193249_create_users.rb
│ └── schema.rb
├── lib
│ └── controller_authentication.rb
├── log
│ └── .gitkeep
├── public
│ ├── 404.html
│ ├── 422.html
│ ├── 500.html
│ ├── favicon.ico
│ └── stylesheets
│ │ └── application.css
└── script
│ └── rails
├── integration
├── push_changes_test.rb
└── push_messages_test.rb
├── models
├── channel_test.rb
├── command_test.rb
├── configuration_test.rb
├── connection_test.rb
├── event_listener_test.rb
├── event_router_test.rb
├── message_queue_client_test.rb
├── message_queue_test.rb
└── pushing_test.rb
├── performance
└── message_queue_performance.rb
├── support
├── factories.rb
├── integration_helper.rb
├── integration_test.rb
└── mocks
│ ├── bogus_event.rb
│ ├── mock_connection.rb
│ ├── mock_event_router.rb
│ └── mock_listener.rb
└── test_helper.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .rvmrc
3 | .bundle/
4 | log/*.log
5 | pkg/
6 | test/dummy/db/*.sqlite3
7 | test/dummy/log/*.log
8 | test/dummy/tmp/
9 |
10 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | # Pusher server
4 | gemspec
5 |
6 | # rails dependecies
7 | gem "mysql2"
8 |
9 | # Rails 3.1 - Asset Pipeline
10 | gem 'json'
11 | gem 'sass-rails'
12 | gem 'coffee-script'
13 | gem 'uglifier'
14 |
15 | # Rails 3.1 - JavaScript
16 | gem 'jquery-rails'
17 |
18 | gem 'capybara'
19 | gem 'capybara-webkit'
20 | gem 'launchy'
21 | gem 'factory_girl'
22 |
23 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | alondra (0.1.1)
5 | daemons
6 | em-websocket
7 | em-zeromq (>= 0.3.1)
8 | rails (>= 3.1.0)
9 | uuidtools
10 |
11 | GEM
12 | remote: http://rubygems.org/
13 | specs:
14 | actionmailer (3.2.8)
15 | actionpack (= 3.2.8)
16 | mail (~> 2.4.4)
17 | actionpack (3.2.8)
18 | activemodel (= 3.2.8)
19 | activesupport (= 3.2.8)
20 | builder (~> 3.0.0)
21 | erubis (~> 2.7.0)
22 | journey (~> 1.0.4)
23 | rack (~> 1.4.0)
24 | rack-cache (~> 1.2)
25 | rack-test (~> 0.6.1)
26 | sprockets (~> 2.1.3)
27 | activemodel (3.2.8)
28 | activesupport (= 3.2.8)
29 | builder (~> 3.0.0)
30 | activerecord (3.2.8)
31 | activemodel (= 3.2.8)
32 | activesupport (= 3.2.8)
33 | arel (~> 3.0.2)
34 | tzinfo (~> 0.3.29)
35 | activeresource (3.2.8)
36 | activemodel (= 3.2.8)
37 | activesupport (= 3.2.8)
38 | activesupport (3.2.8)
39 | i18n (~> 0.6)
40 | multi_json (~> 1.0)
41 | addressable (2.3.2)
42 | arel (3.0.2)
43 | builder (3.0.0)
44 | capybara (1.1.2)
45 | mime-types (>= 1.16)
46 | nokogiri (>= 1.3.3)
47 | rack (>= 1.0.0)
48 | rack-test (>= 0.5.4)
49 | selenium-webdriver (~> 2.0)
50 | xpath (~> 0.1.4)
51 | capybara-webkit (0.12.1)
52 | capybara (>= 1.0.0, < 1.2)
53 | json
54 | childprocess (0.3.5)
55 | ffi (~> 1.0, >= 1.0.6)
56 | coffee-script (2.2.0)
57 | coffee-script-source
58 | execjs
59 | coffee-script-source (1.3.3)
60 | daemons (1.1.9)
61 | em-websocket (0.3.8)
62 | addressable (>= 2.1.1)
63 | eventmachine (>= 0.12.9)
64 | em-zeromq (0.3.1)
65 | eventmachine (= 1.0.0.beta.4)
66 | ffi (>= 1.0.0)
67 | ffi-rzmq (= 0.9.3)
68 | erubis (2.7.0)
69 | eventmachine (1.0.0.beta.4)
70 | execjs (1.4.0)
71 | multi_json (~> 1.0)
72 | factory_girl (4.0.0)
73 | activesupport (>= 3.0.0)
74 | ffi (1.1.5)
75 | ffi-rzmq (0.9.3)
76 | ffi
77 | hike (1.2.1)
78 | i18n (0.6.0)
79 | journey (1.0.4)
80 | jquery-rails (2.0.2)
81 | railties (>= 3.2.0, < 5.0)
82 | thor (~> 0.14)
83 | json (1.7.4)
84 | launchy (2.1.2)
85 | addressable (~> 2.3)
86 | libwebsocket (0.1.5)
87 | addressable
88 | mail (2.4.4)
89 | i18n (>= 0.4.0)
90 | mime-types (~> 1.16)
91 | treetop (~> 1.4.8)
92 | mime-types (1.19)
93 | multi_json (1.3.6)
94 | mysql2 (0.3.11)
95 | nokogiri (1.5.5)
96 | polyglot (0.3.3)
97 | rack (1.4.1)
98 | rack-cache (1.2)
99 | rack (>= 0.4)
100 | rack-ssl (1.3.2)
101 | rack
102 | rack-test (0.6.1)
103 | rack (>= 1.0)
104 | rails (3.2.8)
105 | actionmailer (= 3.2.8)
106 | actionpack (= 3.2.8)
107 | activerecord (= 3.2.8)
108 | activeresource (= 3.2.8)
109 | activesupport (= 3.2.8)
110 | bundler (~> 1.0)
111 | railties (= 3.2.8)
112 | railties (3.2.8)
113 | actionpack (= 3.2.8)
114 | activesupport (= 3.2.8)
115 | rack-ssl (~> 1.3.2)
116 | rake (>= 0.8.7)
117 | rdoc (~> 3.4)
118 | thor (>= 0.14.6, < 2.0)
119 | rake (0.9.2.2)
120 | rdoc (3.12)
121 | json (~> 1.4)
122 | rubyzip (0.9.9)
123 | sass (3.2.0)
124 | sass-rails (3.2.5)
125 | railties (~> 3.2.0)
126 | sass (>= 3.1.10)
127 | tilt (~> 1.3)
128 | selenium-webdriver (2.25.0)
129 | childprocess (>= 0.2.5)
130 | libwebsocket (~> 0.1.3)
131 | multi_json (~> 1.0)
132 | rubyzip
133 | sprockets (2.1.3)
134 | hike (~> 1.2)
135 | rack (~> 1.0)
136 | tilt (~> 1.1, != 1.3.0)
137 | thor (0.15.4)
138 | tilt (1.3.3)
139 | treetop (1.4.10)
140 | polyglot
141 | polyglot (>= 0.3.1)
142 | tzinfo (0.3.33)
143 | uglifier (1.2.7)
144 | execjs (>= 0.3.0)
145 | multi_json (~> 1.3)
146 | uuidtools (2.1.3)
147 | xpath (0.1.4)
148 | nokogiri (~> 1.3)
149 |
150 | PLATFORMS
151 | ruby
152 |
153 | DEPENDENCIES
154 | alondra!
155 | capybara
156 | capybara-webkit
157 | coffee-script
158 | factory_girl
159 | jquery-rails
160 | json
161 | launchy
162 | mysql2
163 | sass-rails
164 | uglifier
165 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2011 YOURNAME
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Alondra
2 |
3 | Alondra is a push server and framework that adds real time capabilities to
4 | your rails applications.
5 |
6 | ## What can I do with Alondra?
7 |
8 | ### Subscribe clients to channels
9 |
10 | Alondra allows browsers to subscribe to channels. Any Ruby process that loads
11 | your Rails environment will be able to push messages to those channels.
12 |
13 | To subscribe to a channel you can use the built in helper:
14 |
15 | ```
16 | <%= alondra_client @chat %>
17 | ```
18 |
19 | Alondra uses [conventions to map records and classes to channel names](https://github.com/afcapel/alondra/wiki/Event-conventions).
20 | The last example will subscribe the browser to a channel named '/chats/:chat_id'.
21 | Then, the Alondra client will render any message pushed to that channel.
22 |
23 | If you don't want to use Alondra conventions, you can always provide your own
24 | channel names:
25 |
26 | ```
27 | <%= alondra_client ['my custom channel', 'another channel'] %>
28 | ```
29 |
30 | ### Sending push notifications
31 |
32 | Since Alondra is all Ruby and integrates with your Rails environment, you can
33 | use your Rails models and views to render push messages. For example, sending
34 | a push notification from your controller action is as simple as this:
35 |
36 | ```ruby
37 | def create
38 | @chat = Chat.find(params[:chat_id])
39 | @message = @chat.messages.build(params[:message])
40 |
41 | if @message.save
42 | push '/messages/create', :to => @chat
43 | end
44 |
45 | respond_with @message
46 | end
47 | ```
48 |
49 | This will render the '/messages/create' view and send the results to all
50 | clients subscribed to the chat channel.
51 |
52 | You can send push notifications from any process that loads your Rails
53 | environment and from any class that includes the Alondra::Pushing module.
54 | When rendering a push message the local context (that is, the instance
55 | variables of the caller object) will be available in the view.
56 |
57 | ### Listening to events
58 |
59 | Alondra comes bundled with an EventListener class that allows you to react to
60 | events such as when a client subscribes to a channel.
61 |
62 | ```ruby
63 | # A ChatListener will by default listen to events
64 | # sent to any channel whose name begins with '/chat'
65 | class ChatListener < Alondra::EventListener
66 |
67 | # If you want to listen to other channels than the default ones
68 | # you can specify other patterns with the listen_to method, like
69 | #
70 | # listen_to /tion$/
71 | #
72 | # That would make your listener receive events from any channel whose
73 | # name ends in 'ion'
74 |
75 |
76 | # This will be fired any time a client subscribes to
77 | # any of the observed channels
78 | on :subscribed, :to => :member do
79 |
80 | # If you use Cookie Based Session Store,
81 | # you can access the Rails session from the listener
82 | @user = User.find(session[:user_id])
83 |
84 | # Push notifications from listener
85 | push '/users/user', :to => channel_name
86 | end
87 | end
88 | ```
89 |
90 | You can also listen to :unsubscribe, :created, :updated, :destroyed or any
91 | custom event in the observed channels.
92 |
93 | ### Push record changes to the client
94 |
95 | Sometimes you are just interested in pushing record updates to subscribed
96 | clients. You can do that annotating your model:
97 |
98 | ```ruby
99 | class Presence < ActiveRecord::Base
100 | belongs_to :user
101 | belongs_to :chat
102 |
103 | push :changes, :to => :chat
104 | end
105 | ```
106 |
107 | This will push an event (:created, :upated or :destroyed) to the chat channel
108 | each time a Message instance changes.
109 |
110 | In the client you can listen to these events using the JavaScript API:
111 |
112 | ```javascript
113 |
114 | var alondraClient = new AlondraClient('localhost', 12345, ['/chat_rooms/1']);
115 |
116 | // render user name when presence is created
117 |
118 | $(alondraClient).bind("created.Presence", function(event, resource){
119 | if( $('#user_'+resource.user_id).length == 0 ){
120 | $('#users').append("
" + resource.username + "
");
121 | }
122 | });
123 |
124 | // remove user name when presence is destroyed
125 |
126 | $(alondraClient).bind("destroyed.Presence", function(event, resource){
127 | $('#user_'+resource.user_id).remove();
128 | });
129 |
130 | ```
131 |
132 | This technique is especially useful if you use something like Backbone.js
133 | to render your app frontend.
134 |
135 |
136 | ## Example application
137 |
138 | You can check the [example application](http://github.com/afcapel/alondra-example)
139 | to see how some of the features are used.
140 |
141 | ## Installation
142 |
143 | Currently Alondra depends on Rails 3.1 and Ruby 1.9. It also uses ZeroMQ for
144 | interprocess communication, so you need to install the library first. If
145 | you are using Homebrew on Mac OS X, just type
146 |
147 |
148 | brew install zeromq
149 |
150 |
151 | When ZeroMQ is installed, add the Alondra gem to your Gemfile.
152 |
153 |
154 | gem "alondra"
155 |
156 |
157 | You also will need to install the server initialization script into your app.
158 | In the shell execute the generator.
159 |
160 |
161 | $ rails g alondra install
162 |
163 |
164 | To run the Alondra server, just call the provided executable in your project directory
165 |
166 |
167 | $ bundle exec alondra
168 |
169 |
170 | In development mode you can also run the Alondra server in its own thread.
171 | See the [initializer in the example application](https://github.com/afcapel/alondra-example/blob/master/config/initializers/alondra_server.rb)
172 | for how to do it.
173 |
174 | ## Contributors
175 |
176 | - [Ryan LeCompte](http://github.com/ryanlecompte)
177 | - [Jaime Iniesta](http://github.com/jaimeiniesta)
178 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | begin
3 | require 'bundler/setup'
4 | require "bundler/gem_tasks"
5 | require 'rake/testtask'
6 | rescue LoadError
7 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
8 | end
9 |
10 | Rake::TestTask.new(:test) do |t|
11 | t.libs << 'lib'
12 | t.libs << 'test'
13 | t.pattern = 'test/**/*_test.rb'
14 | t.verbose = false
15 | end
16 |
17 | task :default => :test
18 |
--------------------------------------------------------------------------------
/alondra.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | require File.expand_path('../lib/alondra/version', __FILE__)
3 |
4 | Gem::Specification.new do |s|
5 | s.name = "alondra"
6 | s.summary = "Add real time capabilities to your rails app"
7 | s.description = "Add real time capabilities to your rails app"
8 | s.version = Alondra::VERSION
9 | s.authors = ['Alberto F. Capel', 'Ryan LeCompte']
10 |
11 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12 | s.files = `git ls-files`.split("\n")
13 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
15 | s.require_paths = ["lib"]
16 |
17 | # Dependencies
18 | s.add_dependency('daemons')
19 | s.add_dependency('uuidtools')
20 | s.add_dependency('rails', '>= 3.1.0')
21 | s.add_dependency('em-websocket')
22 | s.add_dependency('em-zeromq', '>= 0.3.1')
23 | end
24 |
--------------------------------------------------------------------------------
/app/assets/javascripts/alondra-client.js.coffee.erb:
--------------------------------------------------------------------------------
1 | #= require "moz_websocket"
2 | #= require "vendor/jquery.json-2.2"
3 | #= require "vendor/swfobject"
4 | #= require "vendor/web_socket"
5 | //= provide "../swf"
6 |
7 | window.WEB_SOCKET_SWF_LOCATION = "<%= asset_path 'WebSocketMain.swf' %>"
8 |
9 | class @AlondraClient
10 | constructor: (@server, @channels=[], @token = null, @retry = 10000) ->
11 |
12 | @channels = [@channels] unless @channels instanceof Array
13 |
14 | @url = "ws://#{@server}"
15 | @url += "?token=#{@token}" if @token
16 |
17 | @connect()
18 |
19 | subscribe: (channel) =>
20 | @channels.push(channel)
21 |
22 | if @socket.readyState == 0
23 | # Socket is connecting
24 | # Schedule for later subscription
25 |
26 | return
27 |
28 | subscription =
29 | command: 'subscribe'
30 | channel: channel
31 |
32 | @socket.send $.toJSON(subscription)
33 | @
34 |
35 | unsubscribe: (channel) =>
36 | channelIndex = @channels.indexOf(channel)
37 | @channels.splice(channelIndex, 1) if channelIndex >= 0
38 |
39 | if @socket.readyState == 1
40 | unsubscription =
41 | command: 'unsubscribe'
42 | channel: channel
43 |
44 | @socket.send $.toJSON(unsubscription)
45 |
46 | return this
47 |
48 | opened: () =>
49 | if @reconnectInterval
50 | clearInterval(@reconnectInterval)
51 | @reconnectInterval = null
52 |
53 | @subscribe(channel) for channel in @channels
54 | $(this).trigger('connected')
55 | true
56 |
57 | connect: =>
58 | @socket = new WebSocket(@url)
59 |
60 | @socket.onopen = @opened
61 |
62 | @socket.onclose = () =>
63 | this.reconnect()
64 | $(this).trigger('disconnected')
65 |
66 | @socket.onmessage = (message) =>
67 | msg = $.parseJSON(message.data)
68 | if msg.event
69 | @process(msg)
70 | else
71 | @execute(msg)
72 |
73 |
74 | @socket.onerror = (error) =>
75 | @reconnect()
76 | $(this).trigger('error', error)
77 |
78 | @
79 |
80 | process: (serverEvent) ->
81 | eventName = serverEvent.event
82 | resourceType = serverEvent.resource_type
83 | resource = serverEvent.resource
84 |
85 | $(@).trigger("#{eventName}.#{resourceType}", resource)
86 |
87 | execute: (message) ->
88 | eval(message.message)
89 |
90 | reconnect: ->
91 | return if !@retry || @reconnectInterval
92 |
93 | @reconnectInterval = setInterval =>
94 | this.connect()
95 | ,@retry
96 |
97 |
--------------------------------------------------------------------------------
/app/assets/javascripts/moz_websocket.js:
--------------------------------------------------------------------------------
1 | /* In Firefox 6 WebSocket has been renamed to MozWebSocket */
2 |
3 | if( window.WebSocket == null && window.MozWebSocket ){
4 | window.WebSocket = window.MozWebSocket;
5 | }
--------------------------------------------------------------------------------
/app/assets/javascripts/vendor/jquery.json-2.2.js:
--------------------------------------------------------------------------------
1 | /*
2 | * jQuery JSON Plugin
3 | * version: 2.1 (2009-08-14)
4 | *
5 | * This document is licensed as free software under the terms of the
6 | * MIT License: http://www.opensource.org/licenses/mit-license.php
7 | *
8 | * Brantley Harris wrote this plugin. It is based somewhat on the JSON.org
9 | * website's http://www.json.org/json2.js, which proclaims:
10 | * "NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.", a sentiment that
11 | * I uphold.
12 | *
13 | * It is also influenced heavily by MochiKit's serializeJSON, which is
14 | * copyrighted 2005 by Bob Ippolito.
15 | */
16 |
17 | (function($) {
18 | /** jQuery.toJSON( json-serializble )
19 | Converts the given argument into a JSON respresentation.
20 |
21 | If an object has a "toJSON" function, that will be used to get the representation.
22 | Non-integer/string keys are skipped in the object, as are keys that point to a function.
23 |
24 | json-serializble:
25 | The *thing* to be converted.
26 | **/
27 | $.toJSON = function(o)
28 | {
29 | if (typeof(JSON) == 'object' && JSON.stringify)
30 | return JSON.stringify(o);
31 |
32 | var type = typeof(o);
33 |
34 | if (o === null)
35 | return "null";
36 |
37 | if (type == "undefined")
38 | return undefined;
39 |
40 | if (type == "number" || type == "boolean")
41 | return o + "";
42 |
43 | if (type == "string")
44 | return $.quoteString(o);
45 |
46 | if (type == 'object')
47 | {
48 | if (typeof o.toJSON == "function")
49 | return $.toJSON( o.toJSON() );
50 |
51 | if (o.constructor === Date)
52 | {
53 | var month = o.getUTCMonth() + 1;
54 | if (month < 10) month = '0' + month;
55 |
56 | var day = o.getUTCDate();
57 | if (day < 10) day = '0' + day;
58 |
59 | var year = o.getUTCFullYear();
60 |
61 | var hours = o.getUTCHours();
62 | if (hours < 10) hours = '0' + hours;
63 |
64 | var minutes = o.getUTCMinutes();
65 | if (minutes < 10) minutes = '0' + minutes;
66 |
67 | var seconds = o.getUTCSeconds();
68 | if (seconds < 10) seconds = '0' + seconds;
69 |
70 | var milli = o.getUTCMilliseconds();
71 | if (milli < 100) milli = '0' + milli;
72 | if (milli < 10) milli = '0' + milli;
73 |
74 | return '"' + year + '-' + month + '-' + day + 'T' +
75 | hours + ':' + minutes + ':' + seconds +
76 | '.' + milli + 'Z"';
77 | }
78 |
79 | if (o.constructor === Array)
80 | {
81 | var ret = [];
82 | for (var i = 0; i < o.length; i++)
83 | ret.push( $.toJSON(o[i]) || "null" );
84 |
85 | return "[" + ret.join(",") + "]";
86 | }
87 |
88 | var pairs = [];
89 | for (var k in o) {
90 | var name;
91 | var type = typeof k;
92 |
93 | if (type == "number")
94 | name = '"' + k + '"';
95 | else if (type == "string")
96 | name = $.quoteString(k);
97 | else
98 | continue; //skip non-string or number keys
99 |
100 | if (typeof o[k] == "function")
101 | continue; //skip pairs where the value is a function.
102 |
103 | var val = $.toJSON(o[k]);
104 |
105 | pairs.push(name + ":" + val);
106 | }
107 |
108 | return "{" + pairs.join(", ") + "}";
109 | }
110 | };
111 |
112 | /** jQuery.evalJSON(src)
113 | Evaluates a given piece of json source.
114 | **/
115 | $.evalJSON = function(src)
116 | {
117 | if (typeof(JSON) == 'object' && JSON.parse)
118 | return JSON.parse(src);
119 | return eval("(" + src + ")");
120 | };
121 |
122 | /** jQuery.secureEvalJSON(src)
123 | Evals JSON in a way that is *more* secure.
124 | **/
125 | $.secureEvalJSON = function(src)
126 | {
127 | if (typeof(JSON) == 'object' && JSON.parse)
128 | return JSON.parse(src);
129 |
130 | var filtered = src;
131 | filtered = filtered.replace(/\\["\\\/bfnrtu]/g, '@');
132 | filtered = filtered.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']');
133 | filtered = filtered.replace(/(?:^|:|,)(?:\s*\[)+/g, '');
134 |
135 | if (/^[\],:{}\s]*$/.test(filtered))
136 | return eval("(" + src + ")");
137 | else
138 | throw new SyntaxError("Error parsing JSON, source is not valid.");
139 | };
140 |
141 | /** jQuery.quoteString(string)
142 | Returns a string-repr of a string, escaping quotes intelligently.
143 | Mostly a support function for toJSON.
144 |
145 | Examples:
146 | >>> jQuery.quoteString("apple")
147 | "apple"
148 |
149 | >>> jQuery.quoteString('"Where are we going?", she asked.')
150 | "\"Where are we going?\", she asked."
151 | **/
152 | $.quoteString = function(string)
153 | {
154 | if (string.match(_escapeable))
155 | {
156 | return '"' + string.replace(_escapeable, function (a)
157 | {
158 | var c = _meta[a];
159 | if (typeof c === 'string') return c;
160 | c = a.charCodeAt();
161 | return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16);
162 | }) + '"';
163 | }
164 | return '"' + string + '"';
165 | };
166 |
167 | var _escapeable = /["\\\x00-\x1f\x7f-\x9f]/g;
168 |
169 | var _meta = {
170 | '\b': '\\b',
171 | '\t': '\\t',
172 | '\n': '\\n',
173 | '\f': '\\f',
174 | '\r': '\\r',
175 | '"' : '\\"',
176 | '\\': '\\\\'
177 | };
178 | })(jQuery);
179 |
--------------------------------------------------------------------------------
/app/assets/javascripts/vendor/json2.js:
--------------------------------------------------------------------------------
1 | /*
2 | http://www.JSON.org/json2.js
3 | 2011-02-23
4 |
5 | Public Domain.
6 |
7 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
8 |
9 | See http://www.JSON.org/js.html
10 |
11 |
12 | This code should be minified before deployment.
13 | See http://javascript.crockford.com/jsmin.html
14 |
15 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
16 | NOT CONTROL.
17 |
18 |
19 | This file creates a global JSON object containing two methods: stringify
20 | and parse.
21 |
22 | JSON.stringify(value, replacer, space)
23 | value any JavaScript value, usually an object or array.
24 |
25 | replacer an optional parameter that determines how object
26 | values are stringified for objects. It can be a
27 | function or an array of strings.
28 |
29 | space an optional parameter that specifies the indentation
30 | of nested structures. If it is omitted, the text will
31 | be packed without extra whitespace. If it is a number,
32 | it will specify the number of spaces to indent at each
33 | level. If it is a string (such as '\t' or ' '),
34 | it contains the characters used to indent at each level.
35 |
36 | This method produces a JSON text from a JavaScript value.
37 |
38 | When an object value is found, if the object contains a toJSON
39 | method, its toJSON method will be called and the result will be
40 | stringified. A toJSON method does not serialize: it returns the
41 | value represented by the name/value pair that should be serialized,
42 | or undefined if nothing should be serialized. The toJSON method
43 | will be passed the key associated with the value, and this will be
44 | bound to the value
45 |
46 | For example, this would serialize Dates as ISO strings.
47 |
48 | Date.prototype.toJSON = function (key) {
49 | function f(n) {
50 | // Format integers to have at least two digits.
51 | return n < 10 ? '0' + n : n;
52 | }
53 |
54 | return this.getUTCFullYear() + '-' +
55 | f(this.getUTCMonth() + 1) + '-' +
56 | f(this.getUTCDate()) + 'T' +
57 | f(this.getUTCHours()) + ':' +
58 | f(this.getUTCMinutes()) + ':' +
59 | f(this.getUTCSeconds()) + 'Z';
60 | };
61 |
62 | You can provide an optional replacer method. It will be passed the
63 | key and value of each member, with this bound to the containing
64 | object. The value that is returned from your method will be
65 | serialized. If your method returns undefined, then the member will
66 | be excluded from the serialization.
67 |
68 | If the replacer parameter is an array of strings, then it will be
69 | used to select the members to be serialized. It filters the results
70 | such that only members with keys listed in the replacer array are
71 | stringified.
72 |
73 | Values that do not have JSON representations, such as undefined or
74 | functions, will not be serialized. Such values in objects will be
75 | dropped; in arrays they will be replaced with null. You can use
76 | a replacer function to replace those with JSON values.
77 | JSON.stringify(undefined) returns undefined.
78 |
79 | The optional space parameter produces a stringification of the
80 | value that is filled with line breaks and indentation to make it
81 | easier to read.
82 |
83 | If the space parameter is a non-empty string, then that string will
84 | be used for indentation. If the space parameter is a number, then
85 | the indentation will be that many spaces.
86 |
87 | Example:
88 |
89 | text = JSON.stringify(['e', {pluribus: 'unum'}]);
90 | // text is '["e",{"pluribus":"unum"}]'
91 |
92 |
93 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
94 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
95 |
96 | text = JSON.stringify([new Date()], function (key, value) {
97 | return this[key] instanceof Date ?
98 | 'Date(' + this[key] + ')' : value;
99 | });
100 | // text is '["Date(---current time---)"]'
101 |
102 |
103 | JSON.parse(text, reviver)
104 | This method parses a JSON text to produce an object or array.
105 | It can throw a SyntaxError exception.
106 |
107 | The optional reviver parameter is a function that can filter and
108 | transform the results. It receives each of the keys and values,
109 | and its return value is used instead of the original value.
110 | If it returns what it received, then the structure is not modified.
111 | If it returns undefined then the member is deleted.
112 |
113 | Example:
114 |
115 | // Parse the text. Values that look like ISO date strings will
116 | // be converted to Date objects.
117 |
118 | myData = JSON.parse(text, function (key, value) {
119 | var a;
120 | if (typeof value === 'string') {
121 | a =
122 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
123 | if (a) {
124 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
125 | +a[5], +a[6]));
126 | }
127 | }
128 | return value;
129 | });
130 |
131 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
132 | var d;
133 | if (typeof value === 'string' &&
134 | value.slice(0, 5) === 'Date(' &&
135 | value.slice(-1) === ')') {
136 | d = new Date(value.slice(5, -1));
137 | if (d) {
138 | return d;
139 | }
140 | }
141 | return value;
142 | });
143 |
144 |
145 | This is a reference implementation. You are free to copy, modify, or
146 | redistribute.
147 | */
148 |
149 | /*jslint evil: true, strict: false, regexp: false */
150 |
151 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
152 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
153 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
154 | lastIndex, length, parse, prototype, push, replace, slice, stringify,
155 | test, toJSON, toString, valueOf
156 | */
157 |
158 |
159 | // Create a JSON object only if one does not already exist. We create the
160 | // methods in a closure to avoid creating global variables.
161 |
162 | var JSON;
163 | if (!JSON) {
164 | JSON = {};
165 | }
166 |
167 | (function () {
168 | "use strict";
169 |
170 | function f(n) {
171 | // Format integers to have at least two digits.
172 | return n < 10 ? '0' + n : n;
173 | }
174 |
175 | if (typeof Date.prototype.toJSON !== 'function') {
176 |
177 | Date.prototype.toJSON = function (key) {
178 |
179 | return isFinite(this.valueOf()) ?
180 | this.getUTCFullYear() + '-' +
181 | f(this.getUTCMonth() + 1) + '-' +
182 | f(this.getUTCDate()) + 'T' +
183 | f(this.getUTCHours()) + ':' +
184 | f(this.getUTCMinutes()) + ':' +
185 | f(this.getUTCSeconds()) + 'Z' : null;
186 | };
187 |
188 | String.prototype.toJSON =
189 | Number.prototype.toJSON =
190 | Boolean.prototype.toJSON = function (key) {
191 | return this.valueOf();
192 | };
193 | }
194 |
195 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
196 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
197 | gap,
198 | indent,
199 | meta = { // table of character substitutions
200 | '\b': '\\b',
201 | '\t': '\\t',
202 | '\n': '\\n',
203 | '\f': '\\f',
204 | '\r': '\\r',
205 | '"' : '\\"',
206 | '\\': '\\\\'
207 | },
208 | rep;
209 |
210 |
211 | function quote(string) {
212 |
213 | // If the string contains no control characters, no quote characters, and no
214 | // backslash characters, then we can safely slap some quotes around it.
215 | // Otherwise we must also replace the offending characters with safe escape
216 | // sequences.
217 |
218 | escapable.lastIndex = 0;
219 | return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
220 | var c = meta[a];
221 | return typeof c === 'string' ? c :
222 | '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
223 | }) + '"' : '"' + string + '"';
224 | }
225 |
226 |
227 | function str(key, holder) {
228 |
229 | // Produce a string from holder[key].
230 |
231 | var i, // The loop counter.
232 | k, // The member key.
233 | v, // The member value.
234 | length,
235 | mind = gap,
236 | partial,
237 | value = holder[key];
238 |
239 | // If the value has a toJSON method, call it to obtain a replacement value.
240 |
241 | if (value && typeof value === 'object' &&
242 | typeof value.toJSON === 'function') {
243 | value = value.toJSON(key);
244 | }
245 |
246 | // If we were called with a replacer function, then call the replacer to
247 | // obtain a replacement value.
248 |
249 | if (typeof rep === 'function') {
250 | value = rep.call(holder, key, value);
251 | }
252 |
253 | // What happens next depends on the value's type.
254 |
255 | switch (typeof value) {
256 | case 'string':
257 | return quote(value);
258 |
259 | case 'number':
260 |
261 | // JSON numbers must be finite. Encode non-finite numbers as null.
262 |
263 | return isFinite(value) ? String(value) : 'null';
264 |
265 | case 'boolean':
266 | case 'null':
267 |
268 | // If the value is a boolean or null, convert it to a string. Note:
269 | // typeof null does not produce 'null'. The case is included here in
270 | // the remote chance that this gets fixed someday.
271 |
272 | return String(value);
273 |
274 | // If the type is 'object', we might be dealing with an object or an array or
275 | // null.
276 |
277 | case 'object':
278 |
279 | // Due to a specification blunder in ECMAScript, typeof null is 'object',
280 | // so watch out for that case.
281 |
282 | if (!value) {
283 | return 'null';
284 | }
285 |
286 | // Make an array to hold the partial results of stringifying this object value.
287 |
288 | gap += indent;
289 | partial = [];
290 |
291 | // Is the value an array?
292 |
293 | if (Object.prototype.toString.apply(value) === '[object Array]') {
294 |
295 | // The value is an array. Stringify every element. Use null as a placeholder
296 | // for non-JSON values.
297 |
298 | length = value.length;
299 | for (i = 0; i < length; i += 1) {
300 | partial[i] = str(i, value) || 'null';
301 | }
302 |
303 | // Join all of the elements together, separated with commas, and wrap them in
304 | // brackets.
305 |
306 | v = partial.length === 0 ? '[]' : gap ?
307 | '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' :
308 | '[' + partial.join(',') + ']';
309 | gap = mind;
310 | return v;
311 | }
312 |
313 | // If the replacer is an array, use it to select the members to be stringified.
314 |
315 | if (rep && typeof rep === 'object') {
316 | length = rep.length;
317 | for (i = 0; i < length; i += 1) {
318 | if (typeof rep[i] === 'string') {
319 | k = rep[i];
320 | v = str(k, value);
321 | if (v) {
322 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
323 | }
324 | }
325 | }
326 | } else {
327 |
328 | // Otherwise, iterate through all of the keys in the object.
329 |
330 | for (k in value) {
331 | if (Object.prototype.hasOwnProperty.call(value, k)) {
332 | v = str(k, value);
333 | if (v) {
334 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
335 | }
336 | }
337 | }
338 | }
339 |
340 | // Join all of the member texts together, separated with commas,
341 | // and wrap them in braces.
342 |
343 | v = partial.length === 0 ? '{}' : gap ?
344 | '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' :
345 | '{' + partial.join(',') + '}';
346 | gap = mind;
347 | return v;
348 | }
349 | }
350 |
351 | // If the JSON object does not yet have a stringify method, give it one.
352 |
353 | if (typeof JSON.stringify !== 'function') {
354 | JSON.stringify = function (value, replacer, space) {
355 |
356 | // The stringify method takes a value and an optional replacer, and an optional
357 | // space parameter, and returns a JSON text. The replacer can be a function
358 | // that can replace values, or an array of strings that will select the keys.
359 | // A default replacer method can be provided. Use of the space parameter can
360 | // produce text that is more easily readable.
361 |
362 | var i;
363 | gap = '';
364 | indent = '';
365 |
366 | // If the space parameter is a number, make an indent string containing that
367 | // many spaces.
368 |
369 | if (typeof space === 'number') {
370 | for (i = 0; i < space; i += 1) {
371 | indent += ' ';
372 | }
373 |
374 | // If the space parameter is a string, it will be used as the indent string.
375 |
376 | } else if (typeof space === 'string') {
377 | indent = space;
378 | }
379 |
380 | // If there is a replacer, it must be a function or an array.
381 | // Otherwise, throw an error.
382 |
383 | rep = replacer;
384 | if (replacer && typeof replacer !== 'function' &&
385 | (typeof replacer !== 'object' ||
386 | typeof replacer.length !== 'number')) {
387 | throw new Error('JSON.stringify');
388 | }
389 |
390 | // Make a fake root object containing our value under the key of ''.
391 | // Return the result of stringifying the value.
392 |
393 | return str('', {'': value});
394 | };
395 | }
396 |
397 |
398 | // If the JSON object does not yet have a parse method, give it one.
399 |
400 | if (typeof JSON.parse !== 'function') {
401 | JSON.parse = function (text, reviver) {
402 |
403 | // The parse method takes a text and an optional reviver function, and returns
404 | // a JavaScript value if the text is a valid JSON text.
405 |
406 | var j;
407 |
408 | function walk(holder, key) {
409 |
410 | // The walk method is used to recursively walk the resulting structure so
411 | // that modifications can be made.
412 |
413 | var k, v, value = holder[key];
414 | if (value && typeof value === 'object') {
415 | for (k in value) {
416 | if (Object.prototype.hasOwnProperty.call(value, k)) {
417 | v = walk(value, k);
418 | if (v !== undefined) {
419 | value[k] = v;
420 | } else {
421 | delete value[k];
422 | }
423 | }
424 | }
425 | }
426 | return reviver.call(holder, key, value);
427 | }
428 |
429 |
430 | // Parsing happens in four stages. In the first stage, we replace certain
431 | // Unicode characters with escape sequences. JavaScript handles many characters
432 | // incorrectly, either silently deleting them, or treating them as line endings.
433 |
434 | text = String(text);
435 | cx.lastIndex = 0;
436 | if (cx.test(text)) {
437 | text = text.replace(cx, function (a) {
438 | return '\\u' +
439 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
440 | });
441 | }
442 |
443 | // In the second stage, we run the text against regular expressions that look
444 | // for non-JSON patterns. We are especially concerned with '()' and 'new'
445 | // because they can cause invocation, and '=' because it can cause mutation.
446 | // But just to be safe, we want to reject all unexpected forms.
447 |
448 | // We split the second stage into 4 regexp operations in order to work around
449 | // crippling inefficiencies in IE's and Safari's regexp engines. First we
450 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
451 | // replace all simple value tokens with ']' characters. Third, we delete all
452 | // open brackets that follow a colon or comma or that begin the text. Finally,
453 | // we look to see that the remaining characters are only whitespace or ']' or
454 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
455 |
456 | if (/^[\],:{}\s]*$/
457 | .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
458 | .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
459 | .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
460 |
461 | // In the third stage we use the eval function to compile the text into a
462 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
463 | // in JavaScript: it can begin a block or an object literal. We wrap the text
464 | // in parens to eliminate the ambiguity.
465 |
466 | j = eval('(' + text + ')');
467 |
468 | // In the optional fourth stage, we recursively walk the new structure, passing
469 | // each name/value pair to a reviver function for possible transformation.
470 |
471 | return typeof reviver === 'function' ?
472 | walk({'': j}, '') : j;
473 | }
474 |
475 | // If the text is not JSON parseable, then a SyntaxError is thrown.
476 |
477 | throw new SyntaxError('JSON.parse');
478 | };
479 | }
480 | }());
481 |
--------------------------------------------------------------------------------
/app/assets/javascripts/vendor/swfobject.js:
--------------------------------------------------------------------------------
1 | /* SWFObject v2.2
2 | is released under the MIT License
3 | */
4 | var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O.ActiveXObject!=D){try{var ad=new ActiveXObject(W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y0){for(var af=0;af0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad'}}aa.outerHTML='";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab
2 | // License: New BSD License
3 | // Reference: http://dev.w3.org/html5/websockets/
4 | // Reference: http://tools.ietf.org/html/rfc6455
5 |
6 | (function() {
7 |
8 | if (window.WEB_SOCKET_FORCE_FLASH) {
9 | // Keeps going.
10 | } else if (window.WebSocket) {
11 | return;
12 | } else if (window.MozWebSocket) {
13 | // Firefox.
14 | window.WebSocket = MozWebSocket;
15 | return;
16 | }
17 |
18 | var logger;
19 | if (window.WEB_SOCKET_LOGGER) {
20 | logger = WEB_SOCKET_LOGGER;
21 | } else if (window.console && window.console.log && window.console.error) {
22 | // In some environment, console is defined but console.log or console.error is missing.
23 | logger = window.console;
24 | } else {
25 | logger = {log: function(){ }, error: function(){ }};
26 | }
27 |
28 | // swfobject.hasFlashPlayerVersion("10.0.0") doesn't work with Gnash.
29 | if (swfobject.getFlashPlayerVersion().major < 10) {
30 | logger.error("Flash Player >= 10.0.0 is required.");
31 | return;
32 | }
33 | if (location.protocol == "file:") {
34 | logger.error(
35 | "WARNING: web-socket-js doesn't work in file:///... URL " +
36 | "unless you set Flash Security Settings properly. " +
37 | "Open the page via Web server i.e. http://...");
38 | }
39 |
40 | /**
41 | * Our own implementation of WebSocket class using Flash.
42 | * @param {string} url
43 | * @param {array or string} protocols
44 | * @param {string} proxyHost
45 | * @param {int} proxyPort
46 | * @param {string} headers
47 | */
48 | window.WebSocket = function(url, protocols, proxyHost, proxyPort, headers) {
49 | var self = this;
50 | self.__id = WebSocket.__nextId++;
51 | WebSocket.__instances[self.__id] = self;
52 | self.readyState = WebSocket.CONNECTING;
53 | self.bufferedAmount = 0;
54 | self.__events = {};
55 | if (!protocols) {
56 | protocols = [];
57 | } else if (typeof protocols == "string") {
58 | protocols = [protocols];
59 | }
60 | // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc.
61 | // Otherwise, when onopen fires immediately, onopen is called before it is set.
62 | self.__createTask = setTimeout(function() {
63 | WebSocket.__addTask(function() {
64 | self.__createTask = null;
65 | WebSocket.__flash.create(
66 | self.__id, url, protocols, proxyHost || null, proxyPort || 0, headers || null);
67 | });
68 | }, 0);
69 | };
70 |
71 | /**
72 | * Send data to the web socket.
73 | * @param {string} data The data to send to the socket.
74 | * @return {boolean} True for success, false for failure.
75 | */
76 | WebSocket.prototype.send = function(data) {
77 | if (this.readyState == WebSocket.CONNECTING) {
78 | throw "INVALID_STATE_ERR: Web Socket connection has not been established";
79 | }
80 | // We use encodeURIComponent() here, because FABridge doesn't work if
81 | // the argument includes some characters. We don't use escape() here
82 | // because of this:
83 | // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions
84 | // But it looks decodeURIComponent(encodeURIComponent(s)) doesn't
85 | // preserve all Unicode characters either e.g. "\uffff" in Firefox.
86 | // Note by wtritch: Hopefully this will not be necessary using ExternalInterface. Will require
87 | // additional testing.
88 | var result = WebSocket.__flash.send(this.__id, encodeURIComponent(data));
89 | if (result < 0) { // success
90 | return true;
91 | } else {
92 | this.bufferedAmount += result;
93 | return false;
94 | }
95 | };
96 |
97 | /**
98 | * Close this web socket gracefully.
99 | */
100 | WebSocket.prototype.close = function() {
101 | if (this.__createTask) {
102 | clearTimeout(this.__createTask);
103 | this.__createTask = null;
104 | this.readyState = WebSocket.CLOSED;
105 | return;
106 | }
107 | if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) {
108 | return;
109 | }
110 | this.readyState = WebSocket.CLOSING;
111 | WebSocket.__flash.close(this.__id);
112 | };
113 |
114 | /**
115 | * Implementation of {@link DOM 2 EventTarget Interface}
116 | *
117 | * @param {string} type
118 | * @param {function} listener
119 | * @param {boolean} useCapture
120 | * @return void
121 | */
122 | WebSocket.prototype.addEventListener = function(type, listener, useCapture) {
123 | if (!(type in this.__events)) {
124 | this.__events[type] = [];
125 | }
126 | this.__events[type].push(listener);
127 | };
128 |
129 | /**
130 | * Implementation of {@link DOM 2 EventTarget Interface}
131 | *
132 | * @param {string} type
133 | * @param {function} listener
134 | * @param {boolean} useCapture
135 | * @return void
136 | */
137 | WebSocket.prototype.removeEventListener = function(type, listener, useCapture) {
138 | if (!(type in this.__events)) return;
139 | var events = this.__events[type];
140 | for (var i = events.length - 1; i >= 0; --i) {
141 | if (events[i] === listener) {
142 | events.splice(i, 1);
143 | break;
144 | }
145 | }
146 | };
147 |
148 | /**
149 | * Implementation of {@link DOM 2 EventTarget Interface}
150 | *
151 | * @param {Event} event
152 | * @return void
153 | */
154 | WebSocket.prototype.dispatchEvent = function(event) {
155 | var events = this.__events[event.type] || [];
156 | for (var i = 0; i < events.length; ++i) {
157 | events[i](event);
158 | }
159 | var handler = this["on" + event.type];
160 | if (handler) handler.apply(this, [event]);
161 | };
162 |
163 | /**
164 | * Handles an event from Flash.
165 | * @param {Object} flashEvent
166 | */
167 | WebSocket.prototype.__handleEvent = function(flashEvent) {
168 |
169 | if ("readyState" in flashEvent) {
170 | this.readyState = flashEvent.readyState;
171 | }
172 | if ("protocol" in flashEvent) {
173 | this.protocol = flashEvent.protocol;
174 | }
175 |
176 | var jsEvent;
177 | if (flashEvent.type == "open" || flashEvent.type == "error") {
178 | jsEvent = this.__createSimpleEvent(flashEvent.type);
179 | } else if (flashEvent.type == "close") {
180 | jsEvent = this.__createSimpleEvent("close");
181 | jsEvent.wasClean = flashEvent.wasClean ? true : false;
182 | jsEvent.code = flashEvent.code;
183 | jsEvent.reason = flashEvent.reason;
184 | } else if (flashEvent.type == "message") {
185 | var data = decodeURIComponent(flashEvent.message);
186 | jsEvent = this.__createMessageEvent("message", data);
187 | } else {
188 | throw "unknown event type: " + flashEvent.type;
189 | }
190 |
191 | this.dispatchEvent(jsEvent);
192 |
193 | };
194 |
195 | WebSocket.prototype.__createSimpleEvent = function(type) {
196 | if (document.createEvent && window.Event) {
197 | var event = document.createEvent("Event");
198 | event.initEvent(type, false, false);
199 | return event;
200 | } else {
201 | return {type: type, bubbles: false, cancelable: false};
202 | }
203 | };
204 |
205 | WebSocket.prototype.__createMessageEvent = function(type, data) {
206 | if (document.createEvent && window.MessageEvent && !window.opera) {
207 | var event = document.createEvent("MessageEvent");
208 | event.initMessageEvent("message", false, false, data, null, null, window, null);
209 | return event;
210 | } else {
211 | // IE and Opera, the latter one truncates the data parameter after any 0x00 bytes.
212 | return {type: type, data: data, bubbles: false, cancelable: false};
213 | }
214 | };
215 |
216 | /**
217 | * Define the WebSocket readyState enumeration.
218 | */
219 | WebSocket.CONNECTING = 0;
220 | WebSocket.OPEN = 1;
221 | WebSocket.CLOSING = 2;
222 | WebSocket.CLOSED = 3;
223 |
224 | WebSocket.__initialized = false;
225 | WebSocket.__flash = null;
226 | WebSocket.__instances = {};
227 | WebSocket.__tasks = [];
228 | WebSocket.__nextId = 0;
229 |
230 | /**
231 | * Load a new flash security policy file.
232 | * @param {string} url
233 | */
234 | WebSocket.loadFlashPolicyFile = function(url){
235 | WebSocket.__addTask(function() {
236 | WebSocket.__flash.loadManualPolicyFile(url);
237 | });
238 | };
239 |
240 | /**
241 | * Loads WebSocketMain.swf and creates WebSocketMain object in Flash.
242 | */
243 | WebSocket.__initialize = function() {
244 |
245 | if (WebSocket.__initialized) return;
246 | WebSocket.__initialized = true;
247 |
248 | if (WebSocket.__swfLocation) {
249 | // For backword compatibility.
250 | window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation;
251 | }
252 | if (!window.WEB_SOCKET_SWF_LOCATION) {
253 | logger.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf");
254 | return;
255 | }
256 | if (!window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR &&
257 | !WEB_SOCKET_SWF_LOCATION.match(/(^|\/)WebSocketMainInsecure\.swf(\?.*)?$/) &&
258 | WEB_SOCKET_SWF_LOCATION.match(/^\w+:\/\/([^\/]+)/)) {
259 | var swfHost = RegExp.$1;
260 | if (location.host != swfHost) {
261 | logger.error(
262 | "[WebSocket] You must host HTML and WebSocketMain.swf in the same host " +
263 | "('" + location.host + "' != '" + swfHost + "'). " +
264 | "See also 'How to host HTML file and SWF file in different domains' section " +
265 | "in README.md. If you use WebSocketMainInsecure.swf, you can suppress this message " +
266 | "by WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true;");
267 | }
268 | }
269 | var container = document.createElement("div");
270 | container.id = "webSocketContainer";
271 | // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents
272 | // Flash from loading at least in IE. So we move it out of the screen at (-100, -100).
273 | // But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash
274 | // Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is
275 | // the best we can do as far as we know now.
276 | container.style.position = "absolute";
277 | if (WebSocket.__isFlashLite()) {
278 | container.style.left = "0px";
279 | container.style.top = "0px";
280 | } else {
281 | container.style.left = "-100px";
282 | container.style.top = "-100px";
283 | }
284 | var holder = document.createElement("div");
285 | holder.id = "webSocketFlash";
286 | container.appendChild(holder);
287 | document.body.appendChild(container);
288 | // See this article for hasPriority:
289 | // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html
290 | swfobject.embedSWF(
291 | WEB_SOCKET_SWF_LOCATION,
292 | "webSocketFlash",
293 | "1" /* width */,
294 | "1" /* height */,
295 | "10.0.0" /* SWF version */,
296 | null,
297 | null,
298 | {hasPriority: true, swliveconnect : true, allowScriptAccess: "always"},
299 | null,
300 | function(e) {
301 | if (!e.success) {
302 | logger.error("[WebSocket] swfobject.embedSWF failed");
303 | }
304 | }
305 | );
306 |
307 | };
308 |
309 | /**
310 | * Called by Flash to notify JS that it's fully loaded and ready
311 | * for communication.
312 | */
313 | WebSocket.__onFlashInitialized = function() {
314 | // We need to set a timeout here to avoid round-trip calls
315 | // to flash during the initialization process.
316 | setTimeout(function() {
317 | WebSocket.__flash = document.getElementById("webSocketFlash");
318 | WebSocket.__flash.setCallerUrl(location.href);
319 | WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG);
320 | for (var i = 0; i < WebSocket.__tasks.length; ++i) {
321 | WebSocket.__tasks[i]();
322 | }
323 | WebSocket.__tasks = [];
324 | }, 0);
325 | };
326 |
327 | /**
328 | * Called by Flash to notify WebSockets events are fired.
329 | */
330 | WebSocket.__onFlashEvent = function() {
331 | setTimeout(function() {
332 | try {
333 | // Gets events using receiveEvents() instead of getting it from event object
334 | // of Flash event. This is to make sure to keep message order.
335 | // It seems sometimes Flash events don't arrive in the same order as they are sent.
336 | var events = WebSocket.__flash.receiveEvents();
337 | for (var i = 0; i < events.length; ++i) {
338 | WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]);
339 | }
340 | } catch (e) {
341 | logger.error(e);
342 | }
343 | }, 0);
344 | return true;
345 | };
346 |
347 | // Called by Flash.
348 | WebSocket.__log = function(message) {
349 | logger.log(decodeURIComponent(message));
350 | };
351 |
352 | // Called by Flash.
353 | WebSocket.__error = function(message) {
354 | logger.error(decodeURIComponent(message));
355 | };
356 |
357 | WebSocket.__addTask = function(task) {
358 | if (WebSocket.__flash) {
359 | task();
360 | } else {
361 | WebSocket.__tasks.push(task);
362 | }
363 | };
364 |
365 | /**
366 | * Test if the browser is running flash lite.
367 | * @return {boolean} True if flash lite is running, false otherwise.
368 | */
369 | WebSocket.__isFlashLite = function() {
370 | if (!window.navigator || !window.navigator.mimeTypes) {
371 | return false;
372 | }
373 | var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"];
374 | if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) {
375 | return false;
376 | }
377 | return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false;
378 | };
379 |
380 | if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) {
381 | // NOTE:
382 | // This fires immediately if web_socket.js is dynamically loaded after
383 | // the document is loaded.
384 | swfobject.addDomLoadEvent(function() {
385 | WebSocket.__initialize();
386 | });
387 | }
388 |
389 | })();
390 |
--------------------------------------------------------------------------------
/app/assets/swf/WebSocketMain.swf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afcapel/alondra/071ec2fc677846cdb1fc3a964211ecdbf6c55d69/app/assets/swf/WebSocketMain.swf
--------------------------------------------------------------------------------
/app/helpers/alondra_helper.rb:
--------------------------------------------------------------------------------
1 | module AlondraHelper
2 |
3 | def alondra_subscribe_tag(resources)
4 | javascript_tag do
5 | %Q{
6 | $(function(){
7 | #{alondra_subscribe(resources)}
8 | });
9 | }
10 | end
11 | end
12 |
13 | def alondra_subscribe(resources)
14 | resources = [resources] unless Enumerable === resources
15 | resources_paths = resources.collect { |r| "'#{polymorphic_path(r)}'" }.join(', ')
16 |
17 | "new AlondraClient('#{Alondra::Alondra.config.host}', #{Alondra::Alondra.config.port}, [#{resources_paths}]);"
18 | end
19 |
20 | def encrypted_token
21 | token = {:user_id => current_user.id, :valid_until => 5.minutes.from_now}.to_json
22 | Alondra::SessionParser.verifier.generate(token)
23 | end
24 | end
--------------------------------------------------------------------------------
/bin/alondra:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | options = {}
4 |
5 | parser = OptionParser.new do |opts|
6 | opts.on "-p", "--port PORT", Integer,
7 | "Define what port TCP port to bind to (default: 3000)" do |arg|
8 | options[:port] = arg
9 | end
10 |
11 | opts.on "-a", "--address HOST",
12 | "bind to HOST address (default: 0.0.0.0)" do |arg|
13 | options[:host] = arg
14 | end
15 |
16 | opts.on "-s", "--queue-socket PATH", "Socket for IPC communication" do
17 | options[:quiet] = true
18 | end
19 |
20 | opts.on "-e", "--environment ENVIRONMENT",
21 | "The environment to run the Rails app on (default: development)" do |arg|
22 | ENV['RAILS_ENV'] ||= arg
23 | end
24 | end
25 |
26 | parser.banner = "alondra "
27 |
28 | parser.on_tail "-h", "--help", "Show help" do
29 | puts parser
30 | exit 1
31 | end
32 |
33 | parser.parse(ARGV)
34 |
35 | ENV['ALONDRA_SERVER'] ||= 'true'
36 | ENV['RAILS_ENV'] ||= 'development'
37 |
38 | require './config/environment'
39 | require 'alondra'
40 |
41 |
42 | Alondra::Alondra.start_with_options(options)
43 |
44 | puts "Alondra server started at port #{Alondra::Alondra.config.port}"
45 |
46 | sleep
--------------------------------------------------------------------------------
/lib/alondra.rb:
--------------------------------------------------------------------------------
1 | require_relative 'alondra/log'
2 | require_relative 'alondra/message'
3 | require_relative 'alondra/event'
4 | require_relative 'alondra/connection'
5 | require_relative 'alondra/channel'
6 | require_relative 'alondra/command'
7 | require_relative 'alondra/command_dispatcher'
8 | require_relative 'alondra/event_router'
9 | require_relative 'alondra/message_queue_client'
10 | require_relative 'alondra/message_queue'
11 | require_relative 'alondra/pushing'
12 | require_relative 'alondra/event_listener'
13 | require_relative 'alondra/session_parser'
14 | require_relative 'alondra/listener_callback'
15 | require_relative 'alondra/push_controller'
16 | require_relative 'alondra/changes_callbacks'
17 | require_relative 'alondra/changes_push'
18 | require_relative 'alondra/server'
19 |
20 | module Alondra
21 | ActiveRecord::Base.extend ChangesPush
22 | ActionController::Base.send :include, Pushing
23 |
24 | class Alondra < Rails::Engine
25 |
26 | # Setting default configuration values
27 | config.port = Rails.env == 'test' ? 12346 : 12345
28 | config.host = '0.0.0.0'
29 | config.queue_socket = 'ipc:///tmp/alondra.ipc'
30 |
31 | initializer "configure EM thread pool" do
32 | # If we have more threads than db connections we will exhaust the conn pool
33 | threadpool_size = ActiveRecord::Base.connection_pool.instance_variable_get :@size
34 | threadpool_size -= 2 if threadpool_size > 2
35 | EM.threadpool_size = threadpool_size
36 | end
37 |
38 | initializer "enable sessions for flash websockets" do
39 | Rails.application.config.session_store :cookie_store, httponly: false
40 | end
41 |
42 | initializer "load listeners" do
43 | listeners_dir = File.join(Rails.root, 'app', 'listeners')
44 |
45 | Log.info "Loading event listeners in #{listeners_dir}"
46 | Dir[File.join(listeners_dir, '*.rb')].each { |file| require_dependency file }
47 | end
48 |
49 | config.after_initialize do
50 | PushController.send :include, Rails.application.routes.url_helpers
51 | end
52 |
53 | def self.start_with_options(options)
54 | options.each do |k, v|
55 | config.send "#{k}=", v
56 | end
57 | start_server_in_new_thread!
58 | end
59 |
60 | def self.start_server_in_new_thread!
61 | Thread.new do
62 | start_server!
63 | end
64 | end
65 |
66 | def self.start_server!
67 | start_server_proc = Proc.new do
68 | MessageQueue.instance.start_listening
69 | Server.run
70 | end
71 |
72 | if EM.reactor_running?
73 | EM.schedule(start_server_proc)
74 | else
75 | Log.info "starting EM reactor"
76 | EM.run(start_server_proc)
77 | end
78 | end
79 |
80 | def self.stop_server!
81 | EM.stop
82 | end
83 | end
84 | end
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/lib/alondra/changes_callbacks.rb:
--------------------------------------------------------------------------------
1 | module Alondra
2 | module ChangesCallbacks
3 | extend self
4 |
5 | def push_updates(klass, options)
6 | klass.class_eval do
7 | after_update do |record|
8 | ChangesCallbacks.push_event :updated, record, options
9 | end
10 | end
11 | end
12 |
13 | def push_creations(klass, options)
14 | klass.class_eval do
15 | after_create do |record|
16 | ChangesCallbacks.push_event :created, record, options
17 | end
18 | end
19 | end
20 |
21 | def push_destroys(klass, options)
22 | klass.class_eval do
23 | after_destroy do |record|
24 | ChangesCallbacks.push_event :destroyed, record, options
25 | end
26 | end
27 | end
28 |
29 | def push_event(type, record, options)
30 | channels = channel_names_from(type, record, options)
31 |
32 | channels.each do |channel|
33 | event = Event.new(:event => type, :resource => record,
34 | :resource_type => record.class.name, :channel => channel)
35 |
36 | MessageQueueClient.push event
37 | end
38 | end
39 |
40 | def channel_names_from(type, record, options)
41 | case options[:to]
42 | when String then
43 | [options[:to]]
44 | when Symbol then
45 | records = record.send options[:to]
46 | Channel.names_for(records)
47 | else
48 | [Channel.default_name_for(record)]
49 | end
50 | end
51 | end
52 | end
--------------------------------------------------------------------------------
/lib/alondra/changes_push.rb:
--------------------------------------------------------------------------------
1 | module Alondra
2 | module ChangesPush
3 | def push(*args)
4 | last_arg = args.last
5 | options = Hash === last_arg ? args.delete(last_arg) : {}
6 |
7 | args.each do |event_type|
8 | case event_type
9 | when :changes then
10 | ChangesCallbacks.push_updates(self, options)
11 | ChangesCallbacks.push_creations(self, options)
12 | ChangesCallbacks.push_destroys(self, options)
13 | when :updates then
14 | ChangesCallbacks.push_updates(self, options)
15 | when :creations then
16 | ChangesCallbacks.push_creations(self, options)
17 | when :destroys then
18 | ChangesCallbacks.push_destroys(self, options)
19 | end
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/alondra/channel.rb:
--------------------------------------------------------------------------------
1 | module Alondra
2 | class Channel
3 | attr_reader :name
4 | attr_reader :em_channel
5 | attr_reader :connections
6 |
7 | class << self
8 | def list
9 | @channel_list ||= {}
10 | end
11 |
12 | def [](name)
13 | list[name] ||= Channel.new(name)
14 | end
15 |
16 | def for(records)
17 | names_for(records).collect { |name| Channel[name] }
18 | end
19 |
20 | def names_for(records)
21 | case records
22 | when String
23 | [records]
24 | when Enumerable then
25 | records.collect { |r| Channel.default_name_for(r) }
26 | else
27 | [Channel.default_name_for(records)]
28 | end
29 | end
30 |
31 | def default_name_for(resource_or_class, type = :member)
32 |
33 | if resource_or_class.kind_of?(Class)
34 | resource_name = resource_or_class.name.pluralize.underscore
35 | else
36 | resource = resource_or_class
37 | resource_name = resource.class.name.pluralize.underscore
38 | end
39 |
40 | case type
41 | when :member then
42 | "/#{resource_name}/#{resource.id}"
43 | when :collection then
44 | "/#{resource_name}/"
45 | end
46 | end
47 | end
48 |
49 | def initialize(name)
50 | @name = name
51 | @em_channel = EM::Channel.new
52 | @connections = {}
53 | end
54 |
55 | def subscribe(connection)
56 | sid = em_channel.subscribe do |event_or_message|
57 | connection.receive event_or_message
58 | end
59 |
60 | connection.channels << self
61 | connections[connection] = sid
62 | end
63 |
64 | def unsubscribe(connection)
65 | em_channel.unsubscribe connections[connection]
66 |
67 | connection.channels.delete self
68 | connections.delete connection
69 |
70 | event_hash = { :event => :unsubscribed,
71 | :resource => connection.session,
72 | :resource_type => connection.session.class.name,
73 | :channel => name }
74 |
75 |
76 | event = Event.new(event_hash, nil, connection)
77 | event.fire!
78 | end
79 |
80 | def receive(event_or_message)
81 | em_channel << event_or_message
82 | end
83 | end
84 | end
--------------------------------------------------------------------------------
/lib/alondra/command.rb:
--------------------------------------------------------------------------------
1 | module Alondra
2 | class Command
3 | attr_reader :name
4 | attr_reader :connection
5 | attr_reader :channel_name
6 |
7 | def initialize(connection, command_hash)
8 | @connection = connection
9 |
10 | @name = command_hash[:command].to_sym
11 | @channel_name = command_hash[:channel]
12 | end
13 |
14 | def channel
15 | @channel ||= Channel[channel_name]
16 | end
17 |
18 | def execute!
19 | case name
20 | when :subscribe then
21 | channel.subscribe @connection
22 | fire_event :subscribed
23 | when :unsubscribe then
24 | # :unsubscribed event will be fired in Channel#unsubscribe
25 | channel.unsubscribe @connection
26 | end
27 | end
28 |
29 | def fire_event(event_type)
30 | event_hash = {
31 | :event => event_type,
32 | :resource => @connection.session,
33 | :resource_type => @connection.session.class.name,
34 | :channel => @channel_name
35 | }
36 |
37 | Event.new(event_hash, nil, connection).fire!
38 | end
39 | end
40 | end
--------------------------------------------------------------------------------
/lib/alondra/command_dispatcher.rb:
--------------------------------------------------------------------------------
1 | module Alondra
2 |
3 | class NotRecognizedCommand < StandardError; end
4 |
5 | module CommandDispatcher
6 | extend self
7 |
8 | def dispatch(input, connection)
9 | msg = parse(input)
10 |
11 | unless msg.kind_of?(Hash) && msg[:command].present?
12 | raise NotRecognizedCommand.new("Unrecognized command: #{input}")
13 | end
14 |
15 | Command.new(connection, msg).execute!
16 | end
17 |
18 | def parse(string)
19 | msg = ActiveSupport::JSON.decode(string).symbolize_keys
20 | end
21 | end
22 | end
--------------------------------------------------------------------------------
/lib/alondra/connection.rb:
--------------------------------------------------------------------------------
1 | require 'uuidtools'
2 |
3 | module Alondra
4 | module Connections
5 | extend self
6 |
7 | def connections
8 | @connections ||= {}
9 | end
10 |
11 | def [](websocket)
12 | connections[websocket]
13 | end
14 |
15 | def []=(websocket, connection)
16 | connections[websocket] = connection
17 | end
18 |
19 | def delete(websocket)
20 | connections.delete websocket
21 | end
22 | end
23 |
24 | class Connection
25 | attr_reader :uuid
26 | attr_reader :websocket
27 | attr_reader :session
28 | attr_reader :channels
29 |
30 | def initialize(websocket, session = {})
31 | @session = session.symbolize_keys
32 | @websocket = websocket
33 | @uuid = UUIDTools::UUID.random_create
34 |
35 | Connections[websocket] = self
36 | end
37 |
38 | def channels
39 | @channels ||= []
40 | end
41 |
42 | def receive(event_or_message)
43 | Log.info "sending: #{event_or_message.to_json}"
44 | websocket.send event_or_message.to_json
45 | end
46 |
47 | def destroy!
48 | channels.each { |c| c.unsubscribe self }
49 | Connections.delete self.websocket
50 | end
51 | end
52 | end
--------------------------------------------------------------------------------
/lib/alondra/event.rb:
--------------------------------------------------------------------------------
1 | module Alondra
2 | class Event
3 | attr_reader :channel_name
4 | attr_reader :type
5 | attr_reader :resource
6 | attr_reader :resource_type
7 | attr_reader :connection
8 |
9 | def initialize(event_hash, from_json = nil, connection = nil)
10 | @connection = connection
11 | @type = event_hash[:event].to_sym
12 | @json_encoded = from_json
13 |
14 | set_resource_from(event_hash)
15 | set_channel_from(event_hash)
16 | end
17 |
18 | def channel
19 | @channel ||= Channel[channel_name]
20 | end
21 |
22 | def fire!
23 | if connection
24 | # We are inside the Alondra Server
25 | EM.schedule do
26 | MessageQueue.instance.receive self
27 | end
28 | else
29 | MessageQueueClient.push self
30 | end
31 | end
32 |
33 | def as_json
34 | {
35 | :event => type,
36 | :resource_type => resource_type,
37 | :resource => resource.as_json,
38 | :channel => channel_name
39 | }
40 | end
41 |
42 | def to_json
43 | @json_encoded ||= ActiveSupport::JSON.encode(as_json)
44 | end
45 |
46 | private
47 |
48 | def fetch(resource_type_name, attributes)
49 | attributes.symbolize_keys!
50 | resource_class = Kernel.const_get(resource_type_name)
51 |
52 | return attributes unless resource_class < ActiveRecord::Base
53 |
54 | resource = resource_class.new
55 |
56 | filtered_attributes = attributes.delete_if { |k,v| !resource.has_attribute?(k) }
57 |
58 | resource.assign_attributes(filtered_attributes, :without_protection => true)
59 | resource
60 | end
61 |
62 | def set_resource_from(event_hash)
63 | if Hash === event_hash[:resource]
64 | @resource = fetch(event_hash[:resource_type], event_hash[:resource])
65 | else
66 | @resource = event_hash[:resource]
67 | end
68 |
69 | @resource_type = event_hash[:resource_type] || resource.class.name
70 | end
71 |
72 | def set_channel_from(event_hash)
73 | if event_hash[:channel].present?
74 | @channel_name = event_hash[:channel]
75 | else
76 | channel_type = type == :updated ? :member : :collection
77 | Channel.default_name_for(resource, channel_type)
78 | end
79 | end
80 | end
81 | end
--------------------------------------------------------------------------------
/lib/alondra/event_listener.rb:
--------------------------------------------------------------------------------
1 | module Alondra
2 | class EventListener
3 | include Pushing
4 |
5 | attr_accessor :event
6 | attr_accessor :resource
7 | attr_accessor :channel_name
8 |
9 | class << self
10 | def listened_patterns
11 | @listened_patterns ||= [default_listened_pattern]
12 | end
13 |
14 | def listen_to?(channel_name)
15 | listened_patterns.any? { |p| p =~ channel_name }
16 | end
17 |
18 | def listen_to(channel_name)
19 | unless @custom_pattern_provided
20 | listened_patterns.clear
21 | @custom_pattern_provided = true
22 | end
23 |
24 | if Regexp === channel_name
25 | listened_patterns << channel_name
26 | else
27 | escaped_pattern = Regexp.escape(channel_name)
28 | listened_patterns << Regexp.new("^#{escaped_pattern}")
29 | end
30 | end
31 |
32 | def on(event_type, options = {}, &block)
33 | callbacks << ListenerCallback.new(event_type, options, block)
34 | end
35 |
36 | def callbacks
37 | @callbacks ||= []
38 | end
39 |
40 | def default_listened_pattern
41 | word = self.name.demodulize
42 | word.gsub!(/Listener$/, '')
43 | word.gsub!(/::/, '/')
44 | word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1\/\2')
45 | word.gsub!(/([a-z\d])([A-Z])/,'\1/\2')
46 | word.downcase!
47 | Regexp.new("^/#{word}")
48 | end
49 |
50 | def inherited(subclass)
51 | # In development mode Rails will load the same class many times
52 | # Delete it first if we already have parsed it
53 | EventRouter.listeners.delete_if { |l| l.name == subclass.name }
54 | EventRouter.listeners << subclass
55 | end
56 |
57 | def matching_callbacks_for(event)
58 | callbacks.find_all { |c| c.matches?(event) }
59 | end
60 |
61 | def process(event)
62 | matching_callbacks_for(event).each do |callback|
63 | new_instance = new(event)
64 | begin
65 | new_instance.instance_exec(event, &callback.proc)
66 | rescue Exception => ex
67 | Log.error 'Error while processing event listener callback'
68 | Log.error ex.message
69 | Log.error ex.backtrace.join("\n")
70 | end
71 | end
72 | end
73 | end
74 |
75 | def session
76 | @connection.session
77 | end
78 |
79 | def initialize(event)
80 | @event = event
81 | @resource = event.resource
82 | @channel_name = event.channel_name
83 | @connection = event.connection
84 | end
85 | end
86 | end
--------------------------------------------------------------------------------
/lib/alondra/event_router.rb:
--------------------------------------------------------------------------------
1 | module Alondra
2 | class EventRouter
3 |
4 | def self.listeners
5 | @listeners ||= []
6 | end
7 |
8 | def process(event)
9 | event.channel.receive(event)
10 |
11 | # Event listeners callback can manipulate AR objects and so can potentially
12 | # block the EM reactor thread. To avoid that, we defer them to another thread.
13 | EM.defer do
14 |
15 | # Ensure the connection associated with the thread is checked in
16 | # after the callbacks are processed
17 | ActiveRecord::Base.connection_pool.with_connection do
18 | listening_classes = EventRouter.listeners.select do |ob|
19 | ob.listen_to?(event.channel_name)
20 | end
21 |
22 | listening_classes.each { |listening_class| listening_class.process(event) }
23 | end
24 | end
25 | end
26 | end
27 | end
--------------------------------------------------------------------------------
/lib/alondra/listener_callback.rb:
--------------------------------------------------------------------------------
1 | module Alondra
2 | class ListenerCallback
3 | attr_reader :event_type
4 | attr_reader :options
5 | attr_reader :proc
6 |
7 | CHANNEL_NAME_PATTERN = %r{\d+$}
8 |
9 | def initialize(event_type, options = {}, proc)
10 | @event_type = event_type
11 | @options = options
12 | @proc = proc
13 | end
14 |
15 | def matches?(event)
16 | return false unless event.type == event_type
17 |
18 | case options[:to]
19 | when nil then true
20 | when :member then
21 | member_channel? event.channel_name
22 | when :collection then
23 | !member_channel?(event.channel_name)
24 | end
25 | end
26 |
27 | def to_proc
28 | proc
29 | end
30 |
31 | private
32 |
33 | def member_channel?(channel_name)
34 | channel_name =~ CHANNEL_NAME_PATTERN
35 | end
36 | end
37 | end
--------------------------------------------------------------------------------
/lib/alondra/log.rb:
--------------------------------------------------------------------------------
1 | module Alondra
2 | module Log
3 | extend self
4 |
5 | NUMBER_TO_NAME_MAP = {0=>'DEBUG', 1=>'INFO', 2=>'WARN', 3=>'ERROR', 4=>'FATAL', 5=>'UNKNOWN'}
6 | NUMBER_TO_COLOR_MAP = {0=>'0;37', 1=>'32', 2=>'33', 3=>'31', 4=>'31', 5=>'37'}
7 |
8 |
9 | def debug(message)
10 | add(ActiveSupport::BufferedLogger::Severity::DEBUG, message)
11 | end
12 |
13 | def info(message)
14 | add(ActiveSupport::BufferedLogger::Severity::INFO, message)
15 | end
16 |
17 | def warn(message)
18 | add(ActiveSupport::BufferedLogger::Severity::WARN, message)
19 | end
20 |
21 | def error(message)
22 | add(ActiveSupport::BufferedLogger::Severity::ERROR, message)
23 | end
24 |
25 | def fatal(message)
26 | add(ActiveSupport::BufferedLogger::Severity::FATAL, message)
27 | end
28 |
29 | def unkwon(message)
30 | add(ActiveSupport::BufferedLogger::Severity::UNKNOWN, message)
31 | end
32 |
33 | private
34 |
35 | def add(severity, message = nil, progname = 'ALONDRA')
36 | sevstring = NUMBER_TO_NAME_MAP[severity]
37 | color = NUMBER_TO_COLOR_MAP[severity]
38 |
39 | message = "\n\033[35m#{progname}:\033[0m[\033[#{color}m#{sevstring}\033[0m] #{message.strip}\033\n"
40 | Rails.logger.add(severity, message, progname)
41 | end
42 | end
43 | end
--------------------------------------------------------------------------------
/lib/alondra/message.rb:
--------------------------------------------------------------------------------
1 | module Alondra
2 | class Message
3 | attr_reader :content
4 | attr_reader :channel_names
5 |
6 | def initialize(content, channel_names)
7 | @content = content
8 | @channel_names = channel_names
9 | end
10 |
11 | def enqueue
12 | MessageQueueClient.push self
13 | end
14 |
15 | def send_to_channels
16 | channels.each do |channel|
17 | channel.receive self
18 | end
19 | end
20 |
21 | def as_json
22 | {:message => content, :channel_names => channel_names}
23 | end
24 |
25 | def to_json
26 | ActiveSupport::JSON.encode(as_json)
27 | end
28 |
29 | private
30 |
31 | def channels
32 | channel_names.collect { |name| Channel[name] }
33 | end
34 | end
35 | end
--------------------------------------------------------------------------------
/lib/alondra/message_queue.rb:
--------------------------------------------------------------------------------
1 | require 'singleton'
2 | require 'ffi'
3 | require 'em-zeromq'
4 |
5 | module Alondra
6 | class MessageQueue
7 | include Singleton
8 |
9 | def start_listening
10 | Log.info "Starting message queue"
11 |
12 | if @pull_socket || @push_socket
13 | Log.warn 'Connections to message queue started twice'
14 | reset!
15 | end
16 |
17 | push_socket
18 | pull_socket
19 |
20 | self
21 | end
22 |
23 | def on_readable(socket, messages)
24 | messages.each do |received|
25 | begin
26 | parse received.copy_out_string
27 | rescue Exception => ex
28 | Log.error "Error raised while processing message"
29 | Log.error "#{ex.class}: #{ex.message}"
30 | Log.error ex.backtrace.join("\n") if ex.respond_to? :backtrace
31 | end
32 | end
33 | end
34 |
35 | def parse(received_string)
36 | received_hash = ActiveSupport::JSON.decode(received_string).symbolize_keys
37 |
38 | if received_hash[:event]
39 | event = Event.new(received_hash, received_string)
40 | receive(event)
41 | elsif received_hash[:message]
42 | message = Message.new(received_hash[:message], received_hash[:channel_names])
43 | message.send_to_channels
44 | else
45 | Log.warn "Unrecognized message type #{received_string}"
46 | end
47 | end
48 |
49 | def receive(event)
50 | event_router.process(event)
51 | end
52 |
53 | def push_socket
54 | @push_socket ||= begin
55 | push_socket = context.socket(ZMQ::PUSH)
56 | push_socket.connect(Alondra.config.queue_socket)
57 | push_socket
58 | end
59 | end
60 |
61 | def pull_socket
62 | @pull_socket ||= begin
63 | pull_socket = context.socket(ZMQ::PULL, self)
64 | pull_socket.bind(Alondra.config.queue_socket)
65 | pull_socket
66 | end
67 | end
68 |
69 | def reset!
70 | @push_socket.unbind
71 | @pull_socket.unbind
72 |
73 | @context = nil
74 | @push_socket = nil
75 | @pull_socket = nil
76 | end
77 |
78 | private
79 |
80 | def event_router
81 | @event_router ||= EventRouter.new
82 | end
83 |
84 | def context
85 | @context ||= EM::ZeroMQ::Context.new(1)
86 | end
87 | end
88 | end
89 |
90 |
--------------------------------------------------------------------------------
/lib/alondra/message_queue_client.rb:
--------------------------------------------------------------------------------
1 | require 'singleton'
2 | require 'ffi-rzmq'
3 | require 'em-zeromq'
4 |
5 | module Alondra
6 | class MessageQueueClient
7 |
8 | def self.push(message)
9 | instance.send_message(message)
10 | end
11 |
12 | def self.instance
13 | if EM.reactor_running?
14 | async_instance
15 | else
16 | sync_instance
17 | end
18 | end
19 |
20 | def self.async_instance
21 | @async_instance ||= AsyncMessageQueueClient.new
22 | end
23 |
24 | def self.sync_instance
25 | @sync_instance ||= SyncMessageQueueClient.new
26 | end
27 | end
28 |
29 | class AsyncMessageQueueClient < MessageQueueClient
30 | def send_message(message)
31 | EM.schedule do
32 | begin
33 | push_socket.send_msg(message.to_json)
34 | rescue Exception => ex
35 | Log.error "Exception while sending message to message queue: #{ex.message}"
36 | end
37 | end
38 | end
39 |
40 | def push_socket
41 | @push_socket ||= begin
42 | push_socket = context.socket(ZMQ::PUSH)
43 | push_socket.connect(Alondra.config.queue_socket)
44 | push_socket
45 | end
46 | end
47 |
48 | def context
49 | @context ||= EM::ZeroMQ::Context.new(1)
50 | end
51 | end
52 |
53 | class SyncMessageQueueClient < MessageQueueClient
54 |
55 | def send_message(message)
56 | begin
57 | push_socket.send_string(message.to_json)
58 | rescue Exception => ex
59 | Log.error "Exception while sending message to message queue: #{ex.message}"
60 | end
61 | end
62 |
63 | def push_socket
64 | @push_socket ||= begin
65 | socket = context.socket(ZMQ::PUSH)
66 | socket.connect(Alondra.config.queue_socket)
67 | socket
68 | end
69 | end
70 |
71 | def context
72 | @context ||= ZMQ::Context.new(1)
73 | end
74 | end
75 | end
--------------------------------------------------------------------------------
/lib/alondra/push_controller.rb:
--------------------------------------------------------------------------------
1 | module Alondra
2 | class PushController
3 | include ActiveSupport::Configurable
4 | include AbstractController::Logger
5 | include AbstractController::Rendering
6 | include AbstractController::Helpers
7 | include AbstractController::Translation
8 | include AbstractController::AssetPaths
9 |
10 | helper_method :protect_against_forgery?
11 |
12 | attr_accessor :channel_names
13 | attr_accessor :request
14 |
15 | def initialize(context, to, request = nil)
16 | @channel_names = Channel.names_for(to)
17 | @request = request
18 |
19 | self.class.view_paths = ActionController::Base.view_paths
20 | copy_instance_variables_from(context)
21 | end
22 |
23 | def render_push(options)
24 | if EM.reactor_thread?
25 | Log.warn 'You are rendering a view from the Event Machine reactor thread'
26 | Log.warn 'Rendering a view is a possibly blocking operation, so be careful'
27 | end
28 |
29 | message_content = render_to_string(*options)
30 | msg = Message.new(message_content, channel_names)
31 | msg.enqueue
32 | end
33 |
34 | def _prefixes
35 | ['application']
36 | end
37 |
38 | def view_paths
39 | @view_paths ||= ApplicationController.send '_view_paths'
40 | end
41 |
42 | def action_name
43 | 'push'
44 | end
45 |
46 | def protect_against_forgery?
47 | false
48 | end
49 |
50 | def self.protect_against_forgery?
51 | false
52 | end
53 |
54 | private
55 |
56 | def copy_instance_variables_from(context)
57 | context.instance_variables.each do |var|
58 | value = context.instance_variable_get(var)
59 | instance_variable_set(var, value)
60 | end
61 | end
62 | end
63 | end
--------------------------------------------------------------------------------
/lib/alondra/pushing.rb:
--------------------------------------------------------------------------------
1 | module Alondra
2 |
3 | class PushingException < StandardError; end
4 |
5 | module Pushing
6 | def push(*args)
7 | raise PushingException.new('You need to specify the channel to push') unless args.last[:to].present?
8 |
9 | to = args.last.delete(:to)
10 |
11 | # If we are called in the context of a request we save this information
12 | # so we can create proper routes
13 | caller_request = self.respond_to?(:request) ? request : nil
14 |
15 | controller = PushController.new(self, to, caller_request)
16 | controller.render_push(args)
17 | end
18 | end
19 | end
--------------------------------------------------------------------------------
/lib/alondra/server.rb:
--------------------------------------------------------------------------------
1 | require 'em-websocket'
2 |
3 | module Alondra
4 | module Server
5 | extend self
6 |
7 | def run
8 | Log.info "Server starting on port #{Alondra.config.port}"
9 |
10 | EM::WebSocket.start(:host => '0.0.0.0', :port => Alondra.config.port) do |websocket|
11 |
12 | websocket.onopen do
13 | session = SessionParser.parse(websocket)
14 |
15 | Log.info "client connected."
16 | Connection.new(websocket, session)
17 | end
18 |
19 | websocket.onclose do
20 | Log.info "Connection closed"
21 | Connections[websocket].destroy! if Connections[websocket].present?
22 | end
23 |
24 | websocket.onerror do |ex|
25 | puts "Error: #{ex.message}"
26 | Log.error "Error: #{ex.message}"
27 | Log.error ex.backtrace.join("\n")
28 | Connections[websocket].destroy! if Connections[websocket]
29 | end
30 |
31 | websocket.onmessage do |msg|
32 | Log.info "received: #{msg}"
33 | CommandDispatcher.dispatch(msg, Connections[websocket])
34 | end
35 | end
36 |
37 | EM.error_handler do |error|
38 | puts "Error raised during event loop: #{error.message}"
39 | Log.error "Error raised during event loop: #{error.message}"
40 | Log.error error.backtrace.join("\n")
41 | end
42 | end
43 | end
44 | end
--------------------------------------------------------------------------------
/lib/alondra/session_parser.rb:
--------------------------------------------------------------------------------
1 | require 'cgi'
2 |
3 | module Alondra
4 | module SessionParser
5 | extend self
6 |
7 | def verifier
8 | @verifier ||= ActiveSupport::MessageVerifier.new(Rails.application.config.secret_token)
9 | end
10 |
11 | def parse(websocket)
12 | cookie = websocket.request['cookie'] || websocket.request['Cookie']
13 | token = websocket.request['query']['token']
14 |
15 | if token.present?
16 | SessionParser.parse_token(token)
17 | elsif cookie.present?
18 | SessionParser.parse_cookie(cookie)
19 | else
20 | Hash.new
21 | end
22 | end
23 |
24 | def parse_cookie(cookie)
25 | begin
26 | cookies = cookie.split(';')
27 | session_key = Rails.application.config.session_options[:key]
28 |
29 | encoded_session = cookies.detect{|c| c.include?(session_key)}.gsub("#{session_key}=",'').strip
30 | verifier.verify(CGI.unescape(encoded_session))
31 | rescue ActiveSupport::MessageVerifier::InvalidSignature => ex
32 | Log.error "invalid session cookie: #{cookie}"
33 | Hash.new
34 | rescue Exception => ex
35 | Log.error "Exception parsing session from cookie: #{ex.message}"
36 | end
37 | end
38 |
39 | def parse_token(token)
40 | begin
41 | decoded_token = verifier.verify(token)
42 | ActiveSupport::JSON.decode(decoded_token)
43 | rescue ActiveSupport::MessageVerifier::InvalidSignature => ex
44 | Log.error "invalid session token: #{token}"
45 | Hash.new
46 | end
47 | end
48 |
49 | def session_key
50 | Rails.application.config.session_options.key
51 | end
52 |
53 | def marshall
54 | Rails.application.config.session_options[:coder]
55 | end
56 | end
57 | end
--------------------------------------------------------------------------------
/lib/alondra/version.rb:
--------------------------------------------------------------------------------
1 | module Alondra
2 | VERSION = '0.1.1'
3 | end
--------------------------------------------------------------------------------
/lib/generators/alondra/USAGE:
--------------------------------------------------------------------------------
1 | Description:
2 | Explain the generator
3 |
4 | Example:
5 | rails generate alondra Thing
6 |
7 | This will create:
8 | what/will/it/create
9 |
--------------------------------------------------------------------------------
/lib/generators/alondra/alondra_generator.rb:
--------------------------------------------------------------------------------
1 | class AlondraGenerator < Rails::Generators::NamedBase
2 | source_root File.expand_path('../templates', __FILE__)
3 |
4 | def generate_install
5 | copy_file "alondra", "script/alondra"
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/generators/alondra/templates/alondra:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # Set up gems listed in the Gemfile.
3 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', File.dirname(__FILE__))
4 |
5 | require 'rubygems'
6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
7 | require 'daemons'
8 |
9 | options = {
10 | :app_name => 'alondra',
11 | :dir_mode => :script,
12 | :dir => 'tmp/pids'
13 | }
14 |
15 | Daemons.run_proc 'alondra', options do
16 |
17 | ENV["ALONDRA_SERVER"] = 'true'
18 |
19 | require_relative File.join('..', 'config', 'environment')
20 |
21 | Rails.logger.info "Started alondra server on port #{Alondra::Alondra.config.port}... #{EM.reactor_running?}"
22 |
23 | Alondra::Alondra.start_server_in_new_thread!.join
24 |
25 | Rails.logger.info 'Alondra server terminated'
26 | end
--------------------------------------------------------------------------------
/lib/tasks/alondra_tasks.rake:
--------------------------------------------------------------------------------
1 | # desc "Explaining what the task does"
2 | # task :alondra do
3 | # # Task goes here
4 | # end
5 |
--------------------------------------------------------------------------------
/script/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | #!/usr/bin/env ruby
3 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
4 |
5 | ENGINE_PATH = File.expand_path('../..', __FILE__)
6 | load File.expand_path('../../test/dummy/script/rails', __FILE__)
7 |
--------------------------------------------------------------------------------
/test/dummy/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | # Add your own tasks in files placed in lib/tasks ending in .rake,
3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
4 |
5 | require File.expand_path('../config/application', __FILE__)
6 |
7 | Dummy::Application.load_tasks
8 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into including all the files listed below.
2 | // Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
3 | // be included in the compiled file accessible from http://example.com/assets/application.js
4 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
5 | // the compiled file.
6 | //
7 | //= require jquery
8 | //= require jquery_ujs
9 | //= require_tree .
10 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll automatically include all the stylesheets available in this directory
3 | * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
4 | * the top of the compiled file, but it's generally better to create a new file per style scope.
5 | *= require_self
6 | *= require_tree .
7 | */
--------------------------------------------------------------------------------
/test/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | include ControllerAuthentication
3 | protect_from_forgery
4 |
5 | def current_user
6 | @current_user ||= find_current_user
7 | end
8 |
9 | private
10 |
11 | def find_current_user
12 | User.find(session[:user_id]) if session[:user_id].present?
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/chats_controller.rb:
--------------------------------------------------------------------------------
1 | class ChatsController < ApplicationController
2 | before_filter :login_required
3 |
4 | # GET /chats
5 | # GET /chats.json
6 | def index
7 | @chats = Chat.all
8 |
9 | respond_to do |format|
10 | format.html # index.html.erb
11 | format.json { render json: @chats }
12 | end
13 | end
14 |
15 | # GET /chats/1
16 | # GET /chats/1.json
17 | def show
18 | @chat = Chat.find(params[:id])
19 |
20 | respond_to do |format|
21 | format.html # show.html.erb
22 | format.json { render json: @chat }
23 | end
24 | end
25 |
26 | # GET /chats/new
27 | # GET /chats/new.json
28 | def new
29 | @chat = Chat.new
30 |
31 | respond_to do |format|
32 | format.html # new.html.erb
33 | format.json { render json: @chat }
34 | end
35 | end
36 |
37 | # GET /chats/1/edit
38 | def edit
39 | @chat = Chat.find(params[:id])
40 | end
41 |
42 | # POST /chats
43 | # POST /chats.json
44 | def create
45 | @chat = Chat.new(params[:chat])
46 |
47 | respond_to do |format|
48 | if @chat.save
49 | format.html { redirect_to @chat, notice: 'Chat was successfully created.' }
50 | format.json { render json: @chat, status: :created, location: @chat }
51 | else
52 | format.html { render action: "new" }
53 | format.json { render json: @chat.errors, status: :unprocessable_entity }
54 | end
55 | end
56 | end
57 |
58 | # PUT /chats/1
59 | # PUT /chats/1.json
60 | def update
61 | @chat = Chat.find(params[:id])
62 |
63 | respond_to do |format|
64 | if @chat.update_attributes(params[:chat])
65 | format.html { redirect_to @chat, notice: 'Chat was successfully updated.' }
66 | format.json { head :ok }
67 | else
68 | format.html { render action: "edit" }
69 | format.json { render json: @chat.errors, status: :unprocessable_entity }
70 | end
71 | end
72 | end
73 |
74 | # DELETE /chats/1
75 | # DELETE /chats/1.json
76 | def destroy
77 | @chat = Chat.find(params[:id])
78 | @chat.destroy
79 |
80 | respond_to do |format|
81 | format.html { redirect_to chats_url }
82 | format.json { head :ok }
83 | end
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/messages_controller.rb:
--------------------------------------------------------------------------------
1 | class MessagesController < ApplicationController
2 | respond_to :html, :js, :json
3 |
4 | def create
5 | @chat = Chat.find(params[:chat_id])
6 | @message = @chat.messages.build(params[:message])
7 |
8 | @message.save
9 |
10 | respond_with @message, :location => @chat
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/sessions_controller.rb:
--------------------------------------------------------------------------------
1 | class SessionsController < ApplicationController
2 | def new
3 | end
4 |
5 | def create
6 | user = User.authenticate(params[:login], params[:password])
7 | if user
8 | session[:user_id] = user.id
9 | redirect_to_target_or_default root_url, :notice => "Logged in successfully."
10 | else
11 | flash.now[:alert] = "Invalid login or password."
12 | render :action => 'new'
13 | end
14 | end
15 |
16 | def destroy
17 | session[:user_id] = nil
18 | redirect_to root_url, :notice => "You have been logged out."
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/users_controller.rb:
--------------------------------------------------------------------------------
1 | class UsersController < ApplicationController
2 | before_filter :login_required, :except => [:new, :create]
3 |
4 | def new
5 | @user = User.new
6 | end
7 |
8 | def create
9 | @user = User.new(params[:user])
10 | if @user.save
11 | session[:user_id] = @user.id
12 | redirect_to root_url, :notice => "Thank you for signing up! You are now logged in."
13 | else
14 | render :action => 'new'
15 | end
16 | end
17 |
18 | def edit
19 | @user = current_user
20 | end
21 |
22 | def update
23 | @user = current_user
24 | if @user.update_attributes(params[:user])
25 | redirect_to root_url, :notice => "Your profile has been updated."
26 | else
27 | render :action => 'edit'
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/test/dummy/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/helpers/chats_helper.rb:
--------------------------------------------------------------------------------
1 | module ChatsHelper
2 |
3 | def present_users
4 | @present_users = [] #||= Alondra::Channel['/messages/'].users
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/test/dummy/app/helpers/error_messages_helper.rb:
--------------------------------------------------------------------------------
1 | module ErrorMessagesHelper
2 | # Render error messages for the given objects. The :message and :header_message options are allowed.
3 | def error_messages_for(*objects)
4 | options = objects.extract_options!
5 | options[:header_message] ||= I18n.t(:"activerecord.errors.header", :default => "Invalid Fields")
6 | options[:message] ||= I18n.t(:"activerecord.errors.message", :default => "Correct the following errors and try again.")
7 | messages = objects.compact.map { |o| o.errors.full_messages }.flatten
8 | unless messages.empty?
9 | content_tag(:div, :class => "error_messages") do
10 | list_items = messages.map { |msg| content_tag(:li, msg) }
11 | content_tag(:h2, options[:header_message]) + content_tag(:p, options[:message]) + content_tag(:ul, list_items.join.html_safe)
12 | end
13 | end
14 | end
15 |
16 | module FormBuilderAdditions
17 | def error_messages(options = {})
18 | @template.error_messages_for(@object, options)
19 | end
20 | end
21 | end
22 |
23 | ActionView::Helpers::FormBuilder.send(:include, ErrorMessagesHelper::FormBuilderAdditions)
24 |
--------------------------------------------------------------------------------
/test/dummy/app/helpers/layout_helper.rb:
--------------------------------------------------------------------------------
1 | # These helper methods can be called in your template to set variables to be used in the layout
2 | # This module should be included in all views globally,
3 | # to do so you may need to add this line to your ApplicationController
4 | # helper :layout
5 | module LayoutHelper
6 | def title(page_title, show_title = true)
7 | content_for(:title) { h(page_title.to_s) }
8 | @show_title = show_title
9 | end
10 |
11 | def show_title?
12 | @show_title
13 | end
14 |
15 | def stylesheet(*args)
16 | content_for(:head) { stylesheet_link_tag(*args) }
17 | end
18 |
19 | def javascript(*args)
20 | content_for(:head) { javascript_include_tag(*args) }
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/test/dummy/app/mailers/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afcapel/alondra/071ec2fc677846cdb1fc3a964211ecdbf6c55d69/test/dummy/app/mailers/.gitkeep
--------------------------------------------------------------------------------
/test/dummy/app/models/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afcapel/alondra/071ec2fc677846cdb1fc3a964211ecdbf6c55d69/test/dummy/app/models/.gitkeep
--------------------------------------------------------------------------------
/test/dummy/app/models/chat.rb:
--------------------------------------------------------------------------------
1 | class Chat < ActiveRecord::Base
2 | has_many :messages
3 | push :changes
4 |
5 | end
6 |
--------------------------------------------------------------------------------
/test/dummy/app/models/message.rb:
--------------------------------------------------------------------------------
1 | class Message < ActiveRecord::Base
2 | belongs_to :chat
3 | push :changes, :to => :chat
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/app/models/user.rb:
--------------------------------------------------------------------------------
1 | require 'digest'
2 | require 'securerandom'
3 |
4 | class User < ActiveRecord::Base
5 | # new columns need to be added here to be writable through mass assignment
6 | attr_accessible :username, :email, :password, :password_confirmation
7 |
8 | attr_accessor :password
9 | before_save :prepare_password
10 |
11 | validates_presence_of :username
12 | validates_uniqueness_of :username, :email, :allow_blank => true
13 | validates_format_of :username, :with => /^[-\w\._@]+$/i, :allow_blank => true, :message => "should only contain letters, numbers, or .-_@"
14 | validates_format_of :email, :with => /^[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
15 | validates_presence_of :password, :on => :create
16 | validates_confirmation_of :password
17 | validates_length_of :password, :minimum => 4, :allow_blank => true
18 |
19 | # login can be either username or email address
20 | def self.authenticate(login, pass)
21 | user = find_by_username(login) || find_by_email(login)
22 | return user if user && user.password_hash == user.encrypt_password(pass)
23 | end
24 |
25 | def encrypt_password(pass)
26 | Digest::SHA2.hexdigest(pass + password_salt)
27 | end
28 |
29 | private
30 |
31 | def prepare_password
32 | unless password.blank?
33 | self.password_salt = SecureRandom.hex(16)
34 | self.password_hash = encrypt_password(password)
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/test/dummy/app/views/chats/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_for(@chat) do |f| %>
2 | <% if @chat.errors.any? %>
3 |
4 |
<%= pluralize(@chat.errors.count, "error") %> prohibited this chat from being saved:
5 |
6 |
7 | <% @chat.errors.full_messages.each do |msg| %>
8 |
12 |
13 |
14 | <%= link_to 'Edit', edit_user_path(@user) %> |
15 | <%= link_to 'Back', users_path %>
16 |
--------------------------------------------------------------------------------
/test/dummy/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run Dummy::Application
5 |
--------------------------------------------------------------------------------
/test/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require 'rails/all'
4 |
5 | Bundler.require
6 | require "alondra"
7 |
8 | module Dummy
9 | class Application < Rails::Application
10 | config.autoload_paths << "#{config.root}/lib" # Settings in config/environments/* take precedence over those specified here.
11 | # Application configuration should go into files in config/initializers
12 | # -- all .rb files in that directory are automatically loaded.
13 |
14 | # Custom directories with classes and modules you want to be autoloadable.
15 | # config.autoload_paths += %W(#{config.root}/extras)
16 |
17 | # Only load the plugins named here, in the order given (default is alphabetical).
18 | # :all can be used as a placeholder for all plugins not explicitly named.
19 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
20 |
21 | # Activate listeners that should always be running.
22 | # config.active_record.listeners = :cacher, :garbage_collector, :forum_observer
23 |
24 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
25 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
26 | # config.time_zone = 'Central Time (US & Canada)'
27 |
28 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
29 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
30 | # config.i18n.default_locale = :de
31 |
32 | # Configure the default encoding used in templates for Ruby 1.9.
33 | config.encoding = "utf-8"
34 |
35 | # Configure sensitive parameters which will be filtered from the log file.
36 | config.filter_parameters += [:password]
37 |
38 | # Enable the asset pipeline
39 | config.assets.enabled = true
40 | end
41 | end
42 |
43 |
--------------------------------------------------------------------------------
/test/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | gemfile = File.expand_path('../../../../Gemfile', __FILE__)
3 |
4 | if File.exist?(gemfile)
5 | ENV['BUNDLE_GEMFILE'] = gemfile
6 | require 'bundler'
7 | Bundler.setup
8 | end
9 |
10 | $:.unshift File.expand_path('../../../../lib', __FILE__)
--------------------------------------------------------------------------------
/test/dummy/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite version 3.x
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | development:
7 | adapter: mysql2
8 | host: localhost
9 | encoding: utf8
10 | database: alondra_development
11 | pool: 5
12 | timeout: 5000
13 |
14 | # Warning: The database defined as "test" will be erased and
15 | # re-generated from your development database when you run "rake".
16 | # Do not set this db to the same as development or production.
17 | test:
18 | adapter: mysql2
19 | host: localhost
20 | encoding: utf8
21 | database: alondra_test
22 | pool: 5
23 | timeout: 5000
24 |
25 | production:
26 | adapter: mysql2
27 | host: localhost
28 | encoding: utf8
29 | database: alondra_production
30 | pool: 5
31 | timeout: 5000
--------------------------------------------------------------------------------
/test/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the rails application
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the rails application
5 | Dummy::Application.initialize!
6 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Log error messages when you accidentally call methods on nil.
10 | config.whiny_nils = true
11 |
12 | # Show full error reports and disable caching
13 | config.consider_all_requests_local = true
14 | config.action_controller.perform_caching = false
15 |
16 | # Don't care if the mailer can't send
17 | config.action_mailer.raise_delivery_errors = false
18 |
19 | # Print deprecation notices to the Rails logger
20 | config.active_support.deprecation = :log
21 |
22 | # Only use best-standards-support built into browsers
23 | config.action_dispatch.best_standards_support = :builtin
24 |
25 | # Do not compress assets
26 | config.assets.compress = false
27 | end
28 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # Code is not reloaded between requests
5 | config.cache_classes = true
6 |
7 | # Full error reports are disabled and caching is turned on
8 | config.consider_all_requests_local = false
9 | config.action_controller.perform_caching = true
10 |
11 | # Disable Rails's static asset server (Apache or nginx will already do this)
12 | config.serve_static_assets = false
13 |
14 | # Compress JavaScripts and CSS
15 | config.assets.compress = true
16 |
17 | # Specify the default JavaScript compressor
18 | config.assets.js_compressor = :uglifier
19 |
20 | # Specifies the header that your server uses for sending files
21 | # (comment out if your front-end server doesn't support this)
22 | config.action_dispatch.x_sendfile_header = "X-Sendfile" # Use 'X-Accel-Redirect' for nginx
23 |
24 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
25 | # config.force_ssl = true
26 |
27 | # See everything in the log (default is :info)
28 | # config.log_level = :debug
29 |
30 | # Use a different logger for distributed setups
31 | # config.logger = SyslogLogger.new
32 |
33 | # Use a different cache store in production
34 | # config.cache_store = :mem_cache_store
35 |
36 | # Enable serving of images, stylesheets, and JavaScripts from an asset server
37 | # config.action_controller.asset_host = "http://assets.example.com"
38 |
39 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
40 | # config.assets.precompile += %w( search.js )
41 |
42 | # Disable delivery errors, bad email addresses will be ignored
43 | # config.action_mailer.raise_delivery_errors = false
44 |
45 | # Enable threaded mode
46 | # config.threadsafe!
47 |
48 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
49 | # the I18n.default_locale when a translation can not be found)
50 | config.i18n.fallbacks = true
51 |
52 | # Send deprecation notices to registered listeners
53 | config.active_support.deprecation = :notify
54 | end
55 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Configure static asset server for tests with Cache-Control for performance
11 | config.serve_static_assets = true
12 | config.static_cache_control = "public, max-age=3600"
13 |
14 | # Log error messages when you accidentally call methods on nil
15 | config.whiny_nils = true
16 |
17 | # Show full error reports and disable caching
18 | config.consider_all_requests_local = true
19 | config.action_controller.perform_caching = false
20 |
21 | # Raise exceptions instead of rendering exception templates
22 | config.action_dispatch.show_exceptions = false
23 |
24 | # Disable request forgery protection in test environment
25 | config.action_controller.allow_forgery_protection = false
26 |
27 | # Tell Action Mailer not to deliver emails to the real world.
28 | # The :test delivery method accumulates sent emails in the
29 | # ActionMailer::Base.deliveries array.
30 | config.action_mailer.delivery_method = :test
31 |
32 | # Use SQL instead of Active Record's schema dumper when creating the test database.
33 | # This is necessary if your schema can't be completely dumped by the schema dumper,
34 | # like if you have constraints or database-specific column types
35 | # config.active_record.schema_format = :sql
36 |
37 | # Print deprecation notices to the stderr
38 | config.active_support.deprecation = :stderr
39 | end
40 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format
4 | # (all these examples are active by default):
5 | # ActiveSupport::Inflector.inflections do |inflect|
6 | # inflect.plural /^(ox)$/i, '\1en'
7 | # inflect.singular /^(ox)en/i, '\1'
8 | # inflect.irregular 'person', 'people'
9 | # inflect.uncountable %w( fish sheep )
10 | # end
11 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 | # Mime::Type.register_alias "text/html", :iphone
6 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/secret_token.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 | # Make sure the secret is at least 30 characters and all random,
6 | # no regular words or you'll be exposed to dictionary attacks.
7 | Dummy::Application.config.secret_token = 'baf3fe98ca3a7b2b5d5cf116cc3b9c2264db831b2bd91d1b056d5b81757455c3d6efcbdb07039ca75821229c32fde58677ac37861071fc2aa6b61a59a8dcda28'
8 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Dummy::Application.config.session_store :cookie_store, key: '_dummy_session'
4 | Dummy::Application.config.session_store :cookie_store, httponly: false
5 |
6 | # Use the database for sessions instead of the cookie-based default,
7 | # which shouldn't be used to store highly confidential information
8 | # (create the session table with "rails generate session_migration")
9 | # Dummy::Application.config.session_store :active_record_store
10 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 | #
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActionController::Base.wrap_parameters format: [:json]
8 |
9 | # Disable root element in JSON by default.
10 | if defined?(ActiveRecord)
11 | ActiveRecord::Base.include_root_in_json = false
12 | end
13 |
--------------------------------------------------------------------------------
/test/dummy/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Sample localization file for English. Add more files in this directory for other locales.
2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3 |
4 | en:
5 | hello: "Hello world"
6 |
--------------------------------------------------------------------------------
/test/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.routes.draw do
2 | match 'user/edit' => 'users#edit', :as => :edit_current_user
3 |
4 | match 'signup' => 'users#new', :as => :signup
5 |
6 | match 'logout' => 'sessions#destroy', :as => :logout
7 |
8 | match 'login' => 'sessions#new', :as => :login
9 |
10 | resources :sessions
11 |
12 | resources :users
13 |
14 | resources :chats do
15 | resources :messages
16 | end
17 |
18 | root :to => 'chats#index'
19 | end
20 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20110719090458_create_chats.rb:
--------------------------------------------------------------------------------
1 | class CreateChats < ActiveRecord::Migration
2 | def change
3 | create_table :chats do |t|
4 | t.string :name
5 |
6 | t.timestamps
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20110719090538_create_messages.rb:
--------------------------------------------------------------------------------
1 | class CreateMessages < ActiveRecord::Migration
2 | def change
3 | create_table :messages do |t|
4 | t.integer :chat_id
5 | t.string :text
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20110720193249_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration
2 | def self.up
3 | create_table :users do |t|
4 | t.string :username
5 | t.string :email
6 | t.string :password_hash
7 | t.string :password_salt
8 | t.string :access_token
9 | t.timestamps
10 | end
11 | end
12 |
13 | def self.down
14 | drop_table :users
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test/dummy/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # Note that this schema.rb definition is the authoritative source for your
6 | # database schema. If you need to create the application database on another
7 | # system, you should be using db:schema:load, not running all the migrations
8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9 | # you'll amass, the slower it'll run and the greater likelihood for issues).
10 | #
11 | # It's strongly recommended to check this file into your version control system.
12 |
13 | ActiveRecord::Schema.define(:version => 20110720193249) do
14 |
15 | create_table "chats", :force => true do |t|
16 | t.string "name"
17 | t.datetime "created_at"
18 | t.datetime "updated_at"
19 | end
20 |
21 | create_table "messages", :force => true do |t|
22 | t.integer "chat_id"
23 | t.string "text"
24 | t.datetime "created_at"
25 | t.datetime "updated_at"
26 | end
27 |
28 | create_table "users", :force => true do |t|
29 | t.string "username"
30 | t.string "email"
31 | t.string "password_hash"
32 | t.string "password_salt"
33 | t.string "access_token"
34 | t.datetime "created_at"
35 | t.datetime "updated_at"
36 | end
37 |
38 | end
39 |
--------------------------------------------------------------------------------
/test/dummy/lib/controller_authentication.rb:
--------------------------------------------------------------------------------
1 | # This module is included in your application controller which makes
2 | # several methods available to all controllers and views. Here's a
3 | # common example you might add to your application layout file.
4 | #
5 | # <% if logged_in? %>
6 | # Welcome <%= current_user.username %>.
7 | # <%= link_to "Edit profile", edit_current_user_path %> or
8 | # <%= link_to "Log out", logout_path %>
9 | # <% else %>
10 | # <%= link_to "Sign up", signup_path %> or
11 | # <%= link_to "log in", login_path %>.
12 | # <% end %>
13 | #
14 | # You can also restrict unregistered users from accessing a controller using
15 | # a before filter. For example.
16 | #
17 | # before_filter :login_required, :except => [:index, :show]
18 | module ControllerAuthentication
19 | def self.included(controller)
20 | controller.send :helper_method, :current_user, :logged_in?, :redirect_to_target_or_default
21 | end
22 |
23 | def current_user
24 | @current_user ||= User.find(session[:user_id]) if session[:user_id]
25 | end
26 |
27 | def logged_in?
28 | current_user
29 | end
30 |
31 | def login_required
32 | unless logged_in?
33 | store_target_location
34 | redirect_to login_url, :alert => "You must first log in or sign up before accessing this page."
35 | end
36 | end
37 |
38 | def redirect_to_target_or_default(default, *args)
39 | redirect_to(session[:return_to] || default, *args)
40 | session[:return_to] = nil
41 | end
42 |
43 | private
44 |
45 | def store_target_location
46 | session[:return_to] = request.url
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/test/dummy/log/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afcapel/alondra/071ec2fc677846cdb1fc3a964211ecdbf6c55d69/test/dummy/log/.gitkeep
--------------------------------------------------------------------------------
/test/dummy/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The page you were looking for doesn't exist.
23 |
You may have mistyped the address or the page may have moved.