thunderer 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/.travis.yml +9 -0
- data/Gemfile +16 -0
- data/Guardfile +17 -0
- data/LICENSE.txt +22 -0
- data/README.md +32 -0
- data/Rakefile +12 -0
- data/app/assets/javascripts/thunderer.js +93 -0
- data/app/assets/javascripts/thunderer_angular.js +43 -0
- data/lib/generators/thunderer/install_generator.rb +19 -0
- data/lib/generators/thunderer/templates/thunderer.ru +10 -0
- data/lib/generators/thunderer/templates/thunderer.yml +10 -0
- data/lib/thunderer/controller_additions.rb +38 -0
- data/lib/thunderer/engine.rb +20 -0
- data/lib/thunderer/faye_extension.rb +40 -0
- data/lib/thunderer/messanger.rb +50 -0
- data/lib/thunderer/parser.rb +22 -0
- data/lib/thunderer/publish_changes.rb +51 -0
- data/lib/thunderer/railtie.rb +15 -0
- data/lib/thunderer/version.rb +3 -0
- data/lib/thunderer/view_helpers.rb +22 -0
- data/lib/thunderer.rb +66 -0
- data/spec/fixtures/thunderer.yml +8 -0
- data/spec/javascripts/helpers/.gitkeep +0 -0
- data/spec/javascripts/helpers/jasmine_examples/SpecHelper.js +15 -0
- data/spec/javascripts/support/jasmine.yml +123 -0
- data/spec/javascripts/support/jasmine_helper.rb +11 -0
- data/spec/javascripts/thunderer_interceptor_spec.js +0 -0
- data/spec/javascripts/thunderer_spec.js +118 -0
- data/spec/javascripts/thunderer_subscription_service_spec.js +0 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/controller_macros.rb +5 -0
- data/spec/support/matchers/have_filter.rb +6 -0
- data/spec/thunderer/controller_aditions_spec.rb +48 -0
- data/spec/thunderer/messanger_spec.rb +46 -0
- data/spec/thunderer/parser_spec.rb +35 -0
- data/spec/thunderer/publish_changes_spec.rb +0 -0
- data/spec/thunderer_spec.rb +108 -0
- data/thunderer.gemspec +30 -0
- metadata +198 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: eed2e6107ca366aa72173a03e02aac6315d21a98
|
4
|
+
data.tar.gz: d1700a9f0bf3511b4be6ba4b5d5e8d3411e10e8a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 74e671d789053ceb11bfa8e579978d2c38c3d9fad54d889e312885f8324056c247f560bbcabd6d9a1400bc4fcacf46649abab795d7095a9ec8e07033c92d5454
|
7
|
+
data.tar.gz: 1a8414e53918afbb5e9f83f36f99b979c573418064ca962db96d631c09527fe21bfc20c69d2dccf13c607959ee5840b92e7c553d8636fee67011bd749174fd26
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 1.9.3
|
4
|
+
- 2.0.0
|
5
|
+
- 2.1.0
|
6
|
+
addons:
|
7
|
+
code_climate:
|
8
|
+
repo_token:
|
9
|
+
secure: kQfmRpllfvpVjss75SRQKDJ70j72doORRsr8EQyY3cJsoAnwfu8E25/JZ+ZoG25AS+Z6dNYpSH0unrsqzbaSGucFfnQD0TiGINJhs36svHhtrP+JDSxwE38PyKu3tX07TMDxfZ0ig8OfuEuB3RB+E/1/1fy4RrrDn+ZIQN0dIHs=
|
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in thunderer.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
group :development do
|
7
|
+
gem 'guard-rspec', require: false
|
8
|
+
gem 'guard-jasmine'
|
9
|
+
gem 'rsense'
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
group :test do
|
14
|
+
gem 'rspec'
|
15
|
+
gem 'codeclimate-test-reporter', require: nil
|
16
|
+
end
|
data/Guardfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'rspec' , cmd: 'bundle exec rspec'do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
7
|
+
watch('spec/spec_helper.rb') { 'spec' }
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
# guard :jasmine do
|
13
|
+
# watch(%r{spec/javascripts/spec\.(js\.coffee|js|coffee)$}) { 'spec/javascripts' }
|
14
|
+
# watch(%r{spec/javascripts/.+_spec\.(js\.coffee|js|coffee)$})
|
15
|
+
# watch(%r{spec/javascripts/fixtures/.+$})
|
16
|
+
# watch(%r{app/assets/javascripts/(.+?)\.(js\.coffee|js|coffee)(?:\.\w+)*$}) { |m| "spec/javascripts/#{ m[1] }_spec.#{ m[2] }" }
|
17
|
+
# end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Nick Chubarov
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
[](https://travis-ci.org/chubarovNick/thunderer)
|
2
|
+
[](https://codeclimate.com/github/chubarovNick/thunderer)
|
3
|
+
[](https://gemnasium.com/chubarovNick/thunderer)
|
4
|
+
# Thunderer
|
5
|
+
|
6
|
+
Thunderer is gem for publishing messages through [Faye](http://faye.jcoglan.com/). It allows you to easily provide real-time updates through an open socket without tying
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
gem 'thunderer'
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
|
20
|
+
$ gem install thunderer
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
TODO: Write usage instructions here
|
25
|
+
|
26
|
+
## Contributing
|
27
|
+
|
28
|
+
1. Fork it ( http://github.com/<my-github-username>/thunderer/fork )
|
29
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
30
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
31
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
32
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
var Thunderer = (function (doc) {
|
2
|
+
var handleResponse, connectToFaye, self;
|
3
|
+
|
4
|
+
self = {
|
5
|
+
connecting: false,
|
6
|
+
fayeClient: null,
|
7
|
+
fayeCallbacks: [],
|
8
|
+
subscriptions: {},
|
9
|
+
subscriptionObjects: {},
|
10
|
+
subscriptionCallbacks: {}
|
11
|
+
};
|
12
|
+
|
13
|
+
handleResponse = function(message) {
|
14
|
+
if (callback = self.subscriptionCallbacks[message.channel]) {
|
15
|
+
callback(message.data, message.channel);
|
16
|
+
}
|
17
|
+
};
|
18
|
+
|
19
|
+
connectToFaye = function () {
|
20
|
+
self.fayeClient = new Faye.Client(self.subscriptions.server);
|
21
|
+
self.fayeClient.addExtension(self.fayeExtension);
|
22
|
+
for (var i=0; i < self.fayeCallbacks.length; i++) {
|
23
|
+
self.fayeCallbacks[i](self.fayeClient);
|
24
|
+
};
|
25
|
+
};
|
26
|
+
|
27
|
+
self.sign = function(options) {
|
28
|
+
if (!self.subscriptions.server) {
|
29
|
+
self.subscriptions.server = options.server;
|
30
|
+
}
|
31
|
+
self.subscriptions[options.channel] = options;
|
32
|
+
self.faye(function(faye) {
|
33
|
+
var sub = faye.subscribe(options.channel, handleResponse);
|
34
|
+
self.subscriptionObjects[options.channel] = sub;
|
35
|
+
if (options.subscription) {
|
36
|
+
options.subscription(sub);
|
37
|
+
}
|
38
|
+
});
|
39
|
+
};
|
40
|
+
|
41
|
+
self.subscribe = function (channel, callback) {
|
42
|
+
self.subscriptionCallbacks[channel] = callback;
|
43
|
+
};
|
44
|
+
|
45
|
+
self.faye = function (callback) {
|
46
|
+
if (self.fayeClient) {
|
47
|
+
callback(self.fayeClient);
|
48
|
+
} else {
|
49
|
+
self.fayeCallbacks.push(callback);
|
50
|
+
if (self.subscriptions.server && !self.connecting) {
|
51
|
+
self.connecting = true;
|
52
|
+
if (typeof Faye === 'undefined') {
|
53
|
+
console.log('Faye is undefined, you should require faye.js before using Thunderer')
|
54
|
+
} else {
|
55
|
+
connectToFaye();
|
56
|
+
}
|
57
|
+
}
|
58
|
+
}
|
59
|
+
};
|
60
|
+
|
61
|
+
self.fayeExtension ={
|
62
|
+
outgoing: function(message, callback) {
|
63
|
+
if (message.channel == "/meta/subscribe") {
|
64
|
+
// Attach the signature and timestamp to subscription messages
|
65
|
+
var subscription = self.subscriptions[message.subscription];
|
66
|
+
if (!message.ext) message.ext = {};
|
67
|
+
message.ext.thunderer_signature = subscription.signature;
|
68
|
+
message.ext.thunderer_timestamp = subscription.timestamp;
|
69
|
+
}
|
70
|
+
callback(message);
|
71
|
+
}
|
72
|
+
};
|
73
|
+
|
74
|
+
self.subscription = function(channel) {
|
75
|
+
return self.subscriptionObjects[channel];
|
76
|
+
};
|
77
|
+
self.unsubscribe = function (channel) {
|
78
|
+
var sub = self.subscription(channel);
|
79
|
+
if (sub) {
|
80
|
+
sub.cancel();
|
81
|
+
delete self.subscriptionObjects[channel];
|
82
|
+
}
|
83
|
+
};
|
84
|
+
|
85
|
+
self.unsubscribeAll = function () {
|
86
|
+
for (var i in self.subscriptionObjects) {
|
87
|
+
if ( self.subscriptionObjects.hasOwnProperty(i) ) {
|
88
|
+
self.unsubscribe(i);
|
89
|
+
}
|
90
|
+
}
|
91
|
+
};
|
92
|
+
return self;
|
93
|
+
}(document));
|
@@ -0,0 +1,43 @@
|
|
1
|
+
(function () {
|
2
|
+
'use strict';
|
3
|
+
|
4
|
+
angular.module('Thunderer', [])
|
5
|
+
|
6
|
+
.factory('ThundererInterceptor', function ($q) {
|
7
|
+
var self = {
|
8
|
+
response: function (response) {
|
9
|
+
var rawChanels = response.headers().channels;
|
10
|
+
if (rawChanels) {
|
11
|
+
var channels = JSON.parse(rawChanels);
|
12
|
+
for (var i = 0; i < channels.length; i++) {
|
13
|
+
Thunderer.sign(channels[i]);
|
14
|
+
}
|
15
|
+
}
|
16
|
+
return response;
|
17
|
+
},
|
18
|
+
responseError: function (rejection) {
|
19
|
+
$q.reject(rejection);
|
20
|
+
}
|
21
|
+
};
|
22
|
+
|
23
|
+
return self;
|
24
|
+
|
25
|
+
})
|
26
|
+
|
27
|
+
.service('$thunderer', function () {
|
28
|
+
|
29
|
+
var self = {
|
30
|
+
addListener: function (channel, callback) {
|
31
|
+
Thunderer.subscribe(channel, callback);
|
32
|
+
},
|
33
|
+
removeListener: function (channel) {
|
34
|
+
Thunderer.unsubscribe(channel)
|
35
|
+
},
|
36
|
+
removeAllListners: function () {
|
37
|
+
Thunderer.unsubscribeAll()
|
38
|
+
}
|
39
|
+
}
|
40
|
+
return self;
|
41
|
+
})
|
42
|
+
|
43
|
+
}());
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Thunderer
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
def self.source_root
|
5
|
+
File.dirname(__FILE__) + '/templates'
|
6
|
+
end
|
7
|
+
|
8
|
+
def copy_files
|
9
|
+
template 'thunderer.yml', 'config/thunderer.yml'
|
10
|
+
if ::Rails.version < '3.1'
|
11
|
+
copy_file '../../../../app/assets/javascripts/thunderer.js', 'public/javascripts/thunderer.js'
|
12
|
+
copy_file '../../../../app/assets/javascripts/thunderer_interceptor.js', 'public/javascripts/thunderer_interceptor.js'
|
13
|
+
copy_file '../../../../app/assets/javascripts/thunderer_subscription_service.js', 'public/javascripts/thunderer_subscription_service.js'
|
14
|
+
end
|
15
|
+
copy_file 'thunderer.ru', 'thunderer.ru'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# Run with: rackup thunderer.ru -s thin -E production
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'yaml'
|
4
|
+
require 'faye'
|
5
|
+
require 'thunderer'
|
6
|
+
|
7
|
+
Faye::WebSocket.load_adapter('thin')
|
8
|
+
|
9
|
+
Thunderer.load_config(File.expand_path('../config/thunderer.yml', __FILE__), ENV['RAILS_ENV'] || 'development')
|
10
|
+
run Thunderer.faye_app
|
@@ -0,0 +1,10 @@
|
|
1
|
+
development:
|
2
|
+
server: "http://localhost:9292/faye"
|
3
|
+
secret_token: "secret"
|
4
|
+
test:
|
5
|
+
server: "http://localhost:9292/faye"
|
6
|
+
secret_token: "secret"
|
7
|
+
production:
|
8
|
+
server: "http://example.com/faye"
|
9
|
+
secret_token: "<%= defined?(SecureRandom) ? SecureRandom.hex(32) : ActiveSupport::SecureRandom.hex(32) %>"
|
10
|
+
signature_expiration: 3600 # one hour
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
module Thunderer
|
3
|
+
module ControllerAdditions
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
cattr_accessor :channels, :interpolation_object
|
8
|
+
|
9
|
+
before_action :add_channels_header,only: [:index]
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def add_channels_header
|
14
|
+
headers['channels'] = (self.class.channels || []).map do |channel|
|
15
|
+
new_str = if self.class.interpolation_object && channel
|
16
|
+
object = send(self.class.interpolation_object)
|
17
|
+
Thunderer::Parser.interpolate_channel channel, object
|
18
|
+
else
|
19
|
+
channel
|
20
|
+
end
|
21
|
+
Thunderer.subscription(channel: new_str)
|
22
|
+
end.to_json
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module ClassMethods
|
27
|
+
|
28
|
+
def thunderer_channels(*args)
|
29
|
+
options = args.extract_options!
|
30
|
+
options.assert_valid_keys(:object)
|
31
|
+
self.interpolation_object = options[:object]
|
32
|
+
self.channels = Array.wrap(args)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'thunderer/view_helpers'
|
2
|
+
module Thunderer
|
3
|
+
class Engine < ::Rails::Engine
|
4
|
+
|
5
|
+
initializer "thunderer.config" do
|
6
|
+
path = Rails.root.join("config/thunderer.yml")
|
7
|
+
Thunderer.load_config(path, Rails.env) if path.exist?
|
8
|
+
end
|
9
|
+
|
10
|
+
initializer :assets do |config|
|
11
|
+
Rails.application.config.assets.paths << File.join(
|
12
|
+
Gem.loaded_specs['faye'].full_gem_path, 'lib')
|
13
|
+
end
|
14
|
+
|
15
|
+
initializer "thunderer.view_helpers" do
|
16
|
+
ActionView::Base.send :include, ViewHelpers
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Thunderer
|
2
|
+
# This class is an extension for the Faye::RackAdapter.
|
3
|
+
# It is used inside of Thunderer.faye_app.
|
4
|
+
class FayeExtension
|
5
|
+
# Callback to handle incoming Faye messages. This authenticates both
|
6
|
+
# subscribe and publish calls.
|
7
|
+
def incoming(message, callback)
|
8
|
+
if message["channel"] == "/meta/subscribe"
|
9
|
+
authenticate_subscribe(message)
|
10
|
+
elsif message["channel"] !~ %r{^/meta/}
|
11
|
+
authenticate_publish(message)
|
12
|
+
end
|
13
|
+
callback.call(message)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# Ensure the subscription signature is correct and that it has not expired.
|
19
|
+
def authenticate_subscribe(message)
|
20
|
+
subscription = Thunderer.subscription(:channel => message["subscription"], :timestamp => message["ext"]["thunderer_timestamp"])
|
21
|
+
if message["ext"]["thunderer_signature"] != subscription[:signature]
|
22
|
+
message["error"] = "Incorrect signature."
|
23
|
+
elsif Thunderer.signature_expired? message["ext"]["thunderer_timestamp"].to_i
|
24
|
+
message["error"] = "Signature has expired."
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Ensures the secret token is correct before publishing.
|
29
|
+
def authenticate_publish(message)
|
30
|
+
if Thunderer.config[:secret_token].nil?
|
31
|
+
raise Error, "No secret_token config set, ensure thunderer.yml is loaded properly."
|
32
|
+
elsif message["ext"]["thunderer_secret_token"] != Thunderer.config[:secret_token]
|
33
|
+
|
34
|
+
message["error"] = "Incorrect token."
|
35
|
+
else
|
36
|
+
message["ext"]["thunderer_secret_token"] = nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Thunderer
|
2
|
+
module Messanger
|
3
|
+
class ConfigurationError < StandardError; end
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_reader :config
|
7
|
+
|
8
|
+
def reset_config
|
9
|
+
@config = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def configure url
|
13
|
+
reset_config
|
14
|
+
uri = URI.parse(url)
|
15
|
+
@config['uri'] = uri
|
16
|
+
@config['use_ssl'] = uri.scheme == 'https'
|
17
|
+
end
|
18
|
+
|
19
|
+
def post message
|
20
|
+
raise ConfigurationError if not_configured?
|
21
|
+
|
22
|
+
form = build_form
|
23
|
+
form.set_form_data(:message => message.to_json)
|
24
|
+
protocol.start {|h| h.request(form)}
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def build_form
|
30
|
+
uri = @config['uri']
|
31
|
+
Net::HTTP::Post.new(uri.path.empty? ? '/' : uri.path)
|
32
|
+
end
|
33
|
+
|
34
|
+
def protocol
|
35
|
+
uri = @config['uri']
|
36
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
37
|
+
http.use_ssl = @config['use_ssl']
|
38
|
+
if @config['use_ssl']
|
39
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
40
|
+
end
|
41
|
+
http
|
42
|
+
end
|
43
|
+
|
44
|
+
def not_configured?
|
45
|
+
@config == {}
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Thunderer
|
2
|
+
module Parser
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def interpolate_channel channel, object
|
6
|
+
channel.gsub(/:\w*\b/, interpolation_hash(channel, object))
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def interpolation_hash channel, object
|
12
|
+
{}.tap do |result|
|
13
|
+
channel.scan(/:\w*\b/).map do |interpolation_key|
|
14
|
+
object_method = interpolation_key.gsub(':', '')
|
15
|
+
replaced_string = object.send(object_method).to_s
|
16
|
+
result[interpolation_key] = replaced_string
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
module Thunderer
|
3
|
+
module PublishChanges
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
after_save :publish_changes
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :channels, :options, :block
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def publish_changes
|
16
|
+
(self.class.channels || []).each do |channel|
|
17
|
+
rooted_message = if message_root?
|
18
|
+
Hash[message_root, notification_message]
|
19
|
+
else
|
20
|
+
notification_message
|
21
|
+
end
|
22
|
+
Thunderer.publish_to Thunderer::Parser.interpolate_channel(channel, self), rooted_message
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def notification_message
|
27
|
+
block = self.class.block
|
28
|
+
block ? block.call(self) : self
|
29
|
+
end
|
30
|
+
|
31
|
+
def message_root
|
32
|
+
self.class.options[:message_root]
|
33
|
+
end
|
34
|
+
|
35
|
+
def message_root?
|
36
|
+
message_root.present?
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
module ClassMethods
|
42
|
+
def notify_client_to_channels *args, &block
|
43
|
+
@options = args.extract_options!
|
44
|
+
@options.assert_valid_keys(:message_root)
|
45
|
+
@channels = args
|
46
|
+
@block = block
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Thunderer
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
initializer 'thunderer.controller' do
|
4
|
+
ActiveSupport.on_load(:action_controller) do
|
5
|
+
include Thunderer::ControllerAdditions
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
initializer 'thunderer.active_record' do
|
10
|
+
ActiveSupport.on_load(:active_record) do
|
11
|
+
include Thunderer::PublishChanges
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Thunderer
|
2
|
+
module ViewHelpers
|
3
|
+
# Publish the given data or block to the client by sending
|
4
|
+
# a Net::HTTP POST request to the Faye server. If a block
|
5
|
+
# or string is passed in, it is evaluated as JavaScript
|
6
|
+
# on the client. Otherwise it will be converted to JSON
|
7
|
+
# for use in a JavaScript callback.
|
8
|
+
def publish_to(channel, data = nil, &block)
|
9
|
+
Thunderer.publish_to(channel, data || capture(&block))
|
10
|
+
end
|
11
|
+
|
12
|
+
# Subscribe the client to the given channel. This generates
|
13
|
+
# some JavaScript calling PrivatePub.sign with the subscription
|
14
|
+
# options.
|
15
|
+
def subscribe_to(channel)
|
16
|
+
subscription = Thunderer.subscription(:channel => channel)
|
17
|
+
content_tag "script", :type => "text/javascript" do
|
18
|
+
raw("Thunderer.sign(#{subscription.to_json});")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/thunderer.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require "thunderer/version"
|
2
|
+
require 'thunderer/parser'
|
3
|
+
require 'thunderer/messanger'
|
4
|
+
require 'thunderer/publish_changes'
|
5
|
+
require 'thunderer/faye_extension'
|
6
|
+
require "digest/sha1"
|
7
|
+
require "net/http"
|
8
|
+
require "net/https"
|
9
|
+
require 'yaml'
|
10
|
+
require "thunderer/engine" if defined? Rails
|
11
|
+
|
12
|
+
module Thunderer
|
13
|
+
class Error < StandardError; end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_reader :config
|
17
|
+
attr_reader :messanger
|
18
|
+
|
19
|
+
def reset_config
|
20
|
+
@config = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def load_config filename, environment
|
24
|
+
reset_config
|
25
|
+
config_yaml = YAML.load_file(filename)[environment]
|
26
|
+
raise ArgumentError, "The #{environment} environment dose not exist" unless config_yaml
|
27
|
+
config_yaml.each { |k,v| config[k.to_sym] = v }
|
28
|
+
Thunderer::Messanger.configure(config[:server])
|
29
|
+
@messanger = Thunderer::Messanger
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
def publish_to channel, data
|
34
|
+
publish_message(message(channel, data))
|
35
|
+
end
|
36
|
+
|
37
|
+
def publish_message(message)
|
38
|
+
raise Error, "No server specified, ensure private_pub.yml was loaded properly." unless config[:server]
|
39
|
+
url = URI.parse(config[:server])
|
40
|
+
messanger.post(message)
|
41
|
+
end
|
42
|
+
|
43
|
+
def message(channel, data)
|
44
|
+
{:channel => channel,
|
45
|
+
:data => {
|
46
|
+
:channel => channel,
|
47
|
+
:data => data},
|
48
|
+
:ext => {:thunderer_secret_token => config[:secret_token]}}
|
49
|
+
end
|
50
|
+
|
51
|
+
def subscription(options = {})
|
52
|
+
sub = {:server => config[:server], :timestamp => (Time.now.to_f * 1000).round}.merge(options)
|
53
|
+
sub[:signature] = Digest::SHA1.hexdigest([config[:secret_token], sub[:channel], sub[:timestamp]].join)
|
54
|
+
sub
|
55
|
+
end
|
56
|
+
|
57
|
+
def signature_expired?(timestamp)
|
58
|
+
timestamp < ((Time.now.to_f - config[:signature_expiration])*1000).round if config[:signature_expiration]
|
59
|
+
end
|
60
|
+
|
61
|
+
def faye_app(options = {})
|
62
|
+
options = {mount: '/faye', timeout: 45, extensions: [FayeExtension.new] }.merge(options)
|
63
|
+
Faye::RackAdapter.new(options)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
File without changes
|