pusher-client-nc 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "libwebsocket", "~>0.1.3"
4
+
5
+ group :development do
6
+ gem "json"
7
+ gem "ruby-hmac"
8
+ gem "bacon", ">= 0"
9
+ gem "bundler", "~> 1.0.0"
10
+ gem "jeweler", "~> 1.5.2"
11
+ gem "rcov", ">= 0"
12
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,31 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ addressable (2.2.8)
5
+ bacon (1.1.0)
6
+ git (1.2.5)
7
+ jeweler (1.5.2)
8
+ bundler (~> 1.0.0)
9
+ git (>= 1.2.5)
10
+ rake
11
+ json (1.4.6)
12
+ json (1.4.6-java)
13
+ libwebsocket (0.1.3)
14
+ addressable
15
+ rake (0.8.7)
16
+ rcov (0.9.9)
17
+ rcov (0.9.9-java)
18
+ ruby-hmac (0.4.0)
19
+
20
+ PLATFORMS
21
+ java
22
+ ruby
23
+
24
+ DEPENDENCIES
25
+ bacon
26
+ bundler (~> 1.0.0)
27
+ jeweler (~> 1.5.2)
28
+ json
29
+ libwebsocket (~> 0.1.3)
30
+ rcov
31
+ ruby-hmac
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Logan Koester
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.
data/README.rdoc ADDED
@@ -0,0 +1,103 @@
1
+ = pusher-client (Ruby)
2
+
3
+ pusher-client is a ruby gem for consuming WebSockets from the Pusher[http://pusherapp.com] web service.
4
+
5
+ The connection to Pusher can optionally be maintained in its own thread (see Asynchronous Usage).
6
+
7
+ This gem is compatible with jruby since 0.2.
8
+
9
+ == Installation
10
+
11
+ gem install pusher-client
12
+
13
+ == Single-Threaded Usage
14
+
15
+ The application will pause at socket.connect and handle events from Pusher as they happen.
16
+
17
+ require 'pusher-client'
18
+
19
+ PusherClient.logger = Logger.new(STDOUT)
20
+ options = {:secret => YOUR_APP_SECRET}
21
+ socket = PusherClient::Socket.new(YOUR_APP_KEY, options)
22
+
23
+ # Subscribe to a public channel
24
+ socket.subscribe('channel')
25
+
26
+ # Subscribe to an authenticated channel
27
+ socket.subscribe('presence-channel', 'user_id')
28
+
29
+ # Subscribe to an authenticated channel with optional :user_info
30
+ socket.subscribe('presence-channel', 'user_id', { :name => 'name' })
31
+
32
+ # Subscribe to array of channels
33
+ ['channel1', 'channel2'].each do |c|
34
+ socket.subscribe("presence-#{c}", 'user_id')
35
+ end
36
+
37
+ # Bind to global events (a catch-all for any 'event' across subscribed channels)
38
+ socket.bind('event') do |data|
39
+ puts data
40
+ end
41
+
42
+ # Bind to events that occur on a specific channel
43
+ socket['channel'].bind('event') do |data|
44
+ puts data
45
+ end
46
+
47
+ socket.connect
48
+
49
+ == Asynchronous Usage
50
+
51
+ The socket will remain open in the background as long as your main application thread is running,
52
+ and you can continue to subscribe/unsubscribe to channels and bind new events.
53
+
54
+ require 'pusher-client'
55
+
56
+ PusherClient.logger = Logger.new(STDOUT)
57
+ options = {:secret => YOUR_APP_SECRET}
58
+ socket = PusherClient::Socket.new(YOUR_APP_KEY, options)
59
+ socket.connect(true) # Connect asynchronously
60
+
61
+ # Subscribe to a channel
62
+ socket.subscribe('channel')
63
+
64
+ # Bind to a global event
65
+ socket.bind('event') do |data|
66
+ puts data
67
+ end
68
+
69
+ loop do
70
+ sleep(1) # Keep your main thread running
71
+ end
72
+
73
+ For further documentation, read the source & test suite. Some features of the JavaScript client
74
+ are not yet implemented.
75
+
76
+ == Gotchas
77
+
78
+ When binding to a global event, note that you still must be subscribed to the channels the event
79
+ may be sent on. You can't just bind a global event without subscribing to any channels and call it a day.
80
+
81
+ == Contributing to pusher-client
82
+
83
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
84
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
85
+ * Fork the project
86
+ * Start a feature/bugfix branch
87
+ * Commit and push until you are happy with your contribution
88
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
89
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
90
+
91
+ == TODOs
92
+
93
+ * Implement Channel instances associated with Subscription instances.
94
+ * Remove Subscriptions#find_for_bind workaround for lack of Channel instances
95
+ * Remove the duplication between Socket#subscribe and #subscribe_existing
96
+ * Test Socket.rb bind('pusher:connection_disconnected')
97
+ * Identify subscriptions on :channel and :user_id (rather than :user_data).
98
+
99
+ == Copyright
100
+
101
+ Copyright (c) 2010 Logan Koester. See LICENSE.txt for
102
+ further details.
103
+
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "pusher-client-nc"
16
+ gem.homepage = "http://github.com/logankoester/pusher-client"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{Ruby client for consuming WebSockets from http://pusherapp.com}
19
+ gem.description = %Q{Ruby client for consuming WebSockets from http://pusherapp.com}
20
+ gem.email = "support@nur.ph"
21
+ gem.authors = ["Logan Koester, Neil Cauldwell"]
22
+ end
23
+ Jeweler::RubygemsDotOrgTasks.new
24
+
25
+ require 'rake/testtask'
26
+ Rake::TestTask.new(:test) do |test|
27
+ test.libs << 'lib' << 'test'
28
+ test.pattern = 'test/**/*_test.rb'
29
+ test.verbose = true
30
+ end
31
+
32
+ require 'rcov/rcovtask'
33
+ Rcov::RcovTask.new do |test|
34
+ test.libs << 'test'
35
+ test.pattern = 'test/**/*_test.rb'
36
+ test.verbose = true
37
+ end
38
+
39
+ task :default => :test
40
+
41
+ require 'rake/rdoctask'
42
+ Rake::RDocTask.new do |rdoc|
43
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
44
+
45
+ rdoc.rdoc_dir = 'rdoc'
46
+ rdoc.title = "pusher-client #{version}"
47
+ rdoc.rdoc_files.include('README*')
48
+ rdoc.rdoc_files.include('lib/**/*.rb')
49
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.1
@@ -0,0 +1,31 @@
1
+ require 'pusher-client'
2
+
3
+ PusherClient.logger = Logger.new(STDOUT)
4
+ options = {:secret => YOUR_APP_SECRET}
5
+ socket = PusherClient::Socket.new(YOUR_APP_KEY, options)
6
+
7
+ # Subscribe to a public channel
8
+ socket.subscribe('channel')
9
+
10
+ # Subscribe to an authenticated channel (presence or private)
11
+ socket.subscribe('presence-channel', 'user_id')
12
+
13
+ # Subscribe to an authenticated channel with optional :user_info
14
+ socket.subscribe('presence-channel', 'user_id', { :name => 'name' })
15
+
16
+ # Subscribe to array of channels
17
+ ['channel1', 'channel2'].each do |c|
18
+ socket.subscribe("presence-#{c}", 'user_id')
19
+ end
20
+
21
+ # Bind to global events (a catch-all for any 'event' across subscribed channels)
22
+ socket.bind('event') do |data|
23
+ puts data
24
+ end
25
+
26
+ # Bind to events that occur on a specific channel
27
+ socket['channel'].bind('event') do |data|
28
+ puts data
29
+ end
30
+
31
+ socket.connect
@@ -0,0 +1,18 @@
1
+ require 'pusher-client'
2
+
3
+ PusherClient.logger = Logger.new(STDOUT)
4
+ options = {:secret => YOUR_APP_SECRET}
5
+ socket = PusherClient::Socket.new(YOUR_APP_KEY, options)
6
+ socket.connect(true) # Connect asynchronously
7
+
8
+ # Subscribe to a channel
9
+ socket.subscribe('channel')
10
+
11
+ # Bind to a global event
12
+ socket.bind('event') do |data|
13
+ puts data
14
+ end
15
+
16
+ loop do
17
+ sleep(1) # Keep your main thread running
18
+ end
@@ -0,0 +1,26 @@
1
+ autoload :Logger, 'logger'
2
+
3
+ module PusherClient
4
+ HOST = 'ws.pusherapp.com'
5
+ WS_PORT = 80
6
+ WSS_PORT = 443
7
+
8
+ @logger = Logger.new(STDOUT)
9
+
10
+ def self.logger
11
+ @logger
12
+ end
13
+
14
+ def self.logger=(logger)
15
+ @logger = logger
16
+ end
17
+ end
18
+
19
+ Thread.abort_on_exception = true
20
+
21
+ require File.dirname(__FILE__) + '/pusher-client/websocket.rb'
22
+ require File.dirname(__FILE__) + '/pusher-client/socket.rb'
23
+ require File.dirname(__FILE__) + '/pusher-client/channel.rb'
24
+ require File.dirname(__FILE__) + '/pusher-client/channels.rb'
25
+ require File.dirname(__FILE__) + '/pusher-client/subscription.rb'
26
+ require File.dirname(__FILE__) + '/pusher-client/subscriptions.rb'
@@ -0,0 +1,53 @@
1
+ module PusherClient
2
+ class Channel
3
+ attr_accessor :global, :subscribed
4
+ attr_reader :name, :callbacks, :global_callbacks
5
+
6
+ def initialize(channel_name)
7
+ @name = channel_name
8
+ @global = false
9
+ @callbacks = {}
10
+ @global_callbacks = {}
11
+ @subscribed = false
12
+ end
13
+
14
+ def bind(event_name, &callback)
15
+ @callbacks[event_name] = callbacks[event_name] || []
16
+ @callbacks[event_name] << callback
17
+ return self
18
+ end
19
+
20
+ def dispatch_with_all(event_name, data)
21
+ dispatch(event_name, data)
22
+ dispatch_global_callbacks(event_name, data)
23
+ end
24
+
25
+ def dispatch(event_name, data)
26
+ PusherClient.logger.debug "Dispatching callbacks for #{event_name}"
27
+ if @callbacks[event_name]
28
+ @callbacks[event_name].each do |callback|
29
+ callback.call(data)
30
+ end
31
+ else
32
+ PusherClient.logger.debug "No callbacks to dispatch for #{event_name}"
33
+ end
34
+ end
35
+
36
+ def dispatch_global_callbacks(event_name, data)
37
+ if @global_callbacks[event_name]
38
+ PusherClient.logger.debug "Dispatching global callbacks for #{event_name}"
39
+ @global_callbacks[event_name].each do |callback|
40
+ callback.call(data)
41
+ end
42
+ else
43
+ PusherClient.logger.debug "No global callbacks to dispatch for #{event_name}"
44
+ end
45
+ end
46
+
47
+ def acknowledge_subscription(data)
48
+ @subscribed = true
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -0,0 +1,37 @@
1
+ module PusherClient
2
+ class Channels
3
+ attr_reader :channels
4
+
5
+ def initialize
6
+ @channels = {}
7
+ end
8
+
9
+ def add(channel_name)
10
+ unless @channels[channel_name]
11
+ @channels[channel_name] = Channel.new(channel_name)
12
+ end
13
+ @channels[channel_name]
14
+ end
15
+
16
+ def find(channel_name)
17
+ @channels[channel_name]
18
+ end
19
+
20
+ def remove(channel_name)
21
+ @channels.delete(channel_name)
22
+ @channels
23
+ end
24
+
25
+ def empty?
26
+ @channels.empty?
27
+ end
28
+
29
+ def size
30
+ @channels.size
31
+ end
32
+
33
+ alias :<< :add
34
+ alias :[] :find
35
+
36
+ end
37
+ end
@@ -0,0 +1,202 @@
1
+ require 'json'
2
+ require 'hmac-sha2'
3
+ require 'digest/md5'
4
+
5
+ module PusherClient
6
+ class Socket
7
+
8
+ # Mimick the JavaScript client
9
+ CLIENT_ID = 'js'
10
+ VERSION = '1.7.1'
11
+
12
+ attr_accessor :encrypted, :secure
13
+ attr_reader :path, :connected, :subscriptions, :global_channel, :socket_id
14
+
15
+ def initialize(application_key, options={})
16
+ raise ArgumentError if (!application_key.is_a?(String) || application_key.size < 1)
17
+
18
+ @path = "/app/#{application_key}?client=#{CLIENT_ID}&version=#{VERSION}"
19
+ @key = application_key
20
+ @secret = options[:secret]
21
+ @socket_id = nil
22
+ @subscriptions = Subscriptions.new
23
+ @global_channel = Channel.new('pusher_global_channel')
24
+ @global_channel.global = true
25
+ @secure = false
26
+ @connected = false
27
+ @encrypted = options[:encrypted] || false
28
+
29
+ bind('pusher:connection_established') do |data|
30
+ socket = JSON.parse(data)
31
+ @connected = true
32
+ @socket_id = socket['socket_id']
33
+ subscribe_all
34
+ end
35
+
36
+ bind('pusher:connection_disconnected') do |data|
37
+ @subscriptions.subscriptions.each { |s| s.disconnect }
38
+ end
39
+
40
+ bind('pusher:error') do |data|
41
+ PusherClient.logger.fatal("Pusher : error : #{data.inspect}")
42
+ end
43
+ end
44
+
45
+ def connect(async = false)
46
+ if @encrypted || @secure
47
+ url = "wss://#{HOST}:#{WSS_PORT}#{@path}"
48
+ else
49
+ url = "ws://#{HOST}:#{WS_PORT}#{@path}"
50
+ end
51
+ PusherClient.logger.debug("Pusher : connecting : #{url}")
52
+
53
+ @connection_thread = Thread.new {
54
+ @connection = WebSocket.new(url)
55
+ PusherClient.logger.debug "Websocket connected"
56
+ loop do
57
+ msg = @connection.receive[0]
58
+ params = parser(msg)
59
+ next if (params['socket_id'] && params['socket_id'] == self.socket_id)
60
+ event_name = params['event']
61
+ event_data = params['data']
62
+ channel_name = params['channel']
63
+ send_local_event(event_name, event_data, channel_name)
64
+ end
65
+ }
66
+
67
+ @connection_thread.run
68
+ @connection_thread.join unless async
69
+ return self
70
+ end
71
+
72
+ def disconnect
73
+ if @connected
74
+ PusherClient.logger.debug "Pusher : disconnecting"
75
+ @connection.close
76
+ @connection_thread.kill if @connection_thread
77
+ @connected = false
78
+ else
79
+ PusherClient.logger.warn "Disconnect attempted... not connected"
80
+ end
81
+ end
82
+
83
+ def subscribe(channel_name, user_id = nil, options={})
84
+ if user_id
85
+ user_data = {:user_id => user_id, :user_info => options}.to_json
86
+ else
87
+ user_data = {:user_id => '', :user_info => ''}.to_json
88
+ end
89
+
90
+ subscription = @subscriptions.add(channel_name, user_data)
91
+ if @connected
92
+ authorize(subscription, method(:authorize_callback))
93
+ end
94
+ return subscription
95
+ end
96
+
97
+ def subscribe_existing(subscription)
98
+ if @connected
99
+ authorize(subscription, method(:authorize_callback))
100
+ end
101
+ return subscription
102
+ end
103
+
104
+ def subscribe_all
105
+ @subscriptions.subscriptions.each{ |s| subscribe_existing(s) }
106
+ end
107
+
108
+ def unsubscribe(channel_name, user_data)
109
+ subscription = @subscriptions.remove(channel_name, user_data)
110
+ if @connected
111
+ send_event('pusher:unsubscribe', {
112
+ 'channel' => channel_name
113
+ })
114
+ end
115
+ return subscription
116
+ end
117
+
118
+ def bind(event_name, &callback)
119
+ @global_channel.bind(event_name, &callback)
120
+ return self
121
+ end
122
+
123
+ def [](channel_name)
124
+ if @subscriptions[channel_name]
125
+ @subscriptions[channel_name]
126
+ else
127
+ @subscriptions << channel_name
128
+ end
129
+ end
130
+
131
+ def authorize(subscription, callback)
132
+ if is_private_channel(subscription)
133
+ auth_data = get_private_auth(subscription)
134
+ elsif is_presence_channel(subscription)
135
+ auth_data = get_presence_auth(subscription)
136
+ end
137
+ callback.call(subscription, auth_data)
138
+ end
139
+
140
+ def authorize_callback(subscription, auth_data)
141
+ send_event('pusher:subscribe', {
142
+ 'channel' => subscription.channel,
143
+ 'auth' => auth_data,
144
+ 'channel_data' => subscription.user_data
145
+ })
146
+ subscription.acknowledge_subscription(nil)
147
+ end
148
+
149
+ def is_private_channel(subscription)
150
+ subscription.channel.match(/^private-/)
151
+ end
152
+
153
+ def is_presence_channel(subscription)
154
+ subscription.channel.match(/^presence-/)
155
+ end
156
+
157
+ def get_private_auth(subscription)
158
+ string_to_sign = @socket_id + ':' + subscription.channel + ':' + subscription.user_data
159
+ signature = HMAC::SHA256.hexdigest(@secret, string_to_sign)
160
+ return "#{@key}:#{signature}"
161
+ end
162
+
163
+ def get_presence_auth(subscription)
164
+ string_to_sign = @socket_id + ':' + subscription.channel + ':' + subscription.user_data
165
+ signature = HMAC::SHA256.hexdigest(@secret, string_to_sign)
166
+ return "#{@key}:#{signature}"
167
+ end
168
+
169
+ # For compatibility with JavaScript client API
170
+ alias :subscribeAll :subscribe_all
171
+
172
+ def send_event(event_name, data)
173
+ payload = {'event' => event_name, 'data' => data}.to_json
174
+ @connection.send(payload)
175
+ PusherClient.logger.debug("Pusher : sending event : #{payload}")
176
+ end
177
+
178
+ protected
179
+
180
+ def send_local_event(event_name, event_data, channel_name)
181
+ if (channel_name)
182
+ subs = @subscriptions.find_all(channel_name)
183
+ if (subs)
184
+ subs.each {|s| s.dispatch_with_all(event_name, event_data)}
185
+ end
186
+ end
187
+
188
+ @global_channel.dispatch_with_all(event_name, event_data)
189
+ PusherClient.logger.debug("Pusher : event received : channel: #{channel_name}; event: #{event_name}")
190
+ end
191
+
192
+ def parser(data)
193
+ begin
194
+ return JSON.parse(data)
195
+ rescue => err
196
+ PusherClient.logger.warn(err)
197
+ PusherClient.logger.warn("Pusher : data attribute not valid JSON - you may wish to implement your own Pusher::Client.parser")
198
+ return data
199
+ end
200
+ end
201
+ end
202
+ end