suj-pusher 0.1.5 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -40,24 +40,36 @@ pusher start|stop|restart|status <options>
40
40
  ```
41
41
 
42
42
  options:
43
- - -r <REDIS>: The redis server used to receive push notification messages. The default is localhost:6379.
43
+ - -H <redis host>: The redis server host or IP address where push messages are stored. Defaults to localhost.
44
+ - -P <redis port>: Port number of the redis server where push messages are stored. Defaults to 6379.
45
+ - -b <redis db>: The database number on the redis server to connect to. Defaults to 0.
46
+ - -n <redis namespace>: A namespace separate pusher queues from others in redis. Defaults to pusher.
47
+ - -l <logdir>: The directory where logfiles are stored. Defaults to $PWD/logs.
48
+ - -p <piddir>: The directory where pid files are stored. Defaults to $PWD/pids.
49
+ - -c <certs>: The directory where we temporarily store APN certs. Defaults to $PWD/certs.
44
50
 
45
- Note that the daemon will run on your current folder and under the current user. Make sure you cd to a folder (e.g. /var/run/pusher) and that your current user has permissions to create folders/files inside that directory:
51
+ The pusher daemon runs under the current user and current folder. If you specify the logid, piddir and certs folders make sure these exists and that the current user can create/write files inside those folder.
52
+
53
+ To start the server run:
54
+
55
+ ```
56
+ /path/to/bin/pusher start -H localhost -P 6379 -b 0 -n pusher -p /var/run/pids
57
+ ```
58
+
59
+ To stop the server run:
46
60
 
47
61
  ```
48
- mkdir /var/run/pusher
49
- chown pusher_user:pusher_group /var/run/pusher
50
- cd /var/run/pusher
51
- /usr/bin/pusher start -r redis://192.168.x.x:6379/namespace
62
+ /path/to/bin/pusher start -H localhost -P 6379 -b 0 -n pusher -p /var/run/pids
52
63
  ```
53
64
 
54
- The pusher daemon creates a logs, tmp and certs directory in the current path. To check the state of the daemon you may check the logs. We store the APN certs inside the certs directory temporarily during the connection to APN and delete them when the connection is closed. Still make sure this folder is only readable/writeable by the user under which the pusher process is running.
65
+ If you set a piddir (using -p option) when starting the server then you must supply the same option when stopping or restarting the server.
66
+
67
+ The certs option (-c) is to temporarily store APN certificates. These are deleted when the APN connection is closed.
55
68
 
56
69
  ## Sending Notifications
57
70
 
58
71
  Once the pusher daemon is running and connected to your redis server you can push notifications by publishing messages to redis. The message format is a simple JSON string.
59
72
 
60
-
61
73
  ### JSON string format
62
74
 
63
75
  Example JSON message:
@@ -107,6 +119,39 @@ Normally you would send messages to either Android or iOS indenpendently. But th
107
119
 
108
120
  If your data hash is compatible with the APN standard as described above and you specify a APN cert, a GCM api_key, a list of apn_ids and a list of gcm_ids then the message will be delivered via push notification to all the devices in those lists. Apple will display the notifications using their own mechanisms and for Android you will receive the data hash in a Bundle object as usual. Is your responsibility to extract the data from that bundle and display/use it as you please.
109
121
 
122
+
123
+
124
+ #### Sending one message to WNS
125
+ message hash: {
126
+ wnstype: "type" ,
127
+ wnsrequeststatus: true,
128
+ wnsids: ["xxx"],
129
+ secret: "app-secret",
130
+ development: false,
131
+ sid: "secret-id",
132
+ data: "notif"}
133
+ wnstype: type of the notification to send, posibles values are: "wns/badge", "wns/tile", "wns/toast", "wns/raw"
134
+ data: a string with de notifiaction to send, please use the xml templates provided by Microsoft(http://msdn.microsoft.com/en-us/library/windows/apps/hh779725.aspx) for each wnstype listed above.
135
+ secret and sid: App identification credentials provided by microsoft when registering a new application to use wns services.
136
+ wnsrequeststatus: boolean, if true, the response from wns server will have aditional information
137
+ wnsids: jsonArray of target devices.
138
+
139
+
140
+ #### Sending one message to WPNS
141
+ message hash: { wptype: "type",
142
+ wpids: ["xxx"],
143
+ secret: "unicId",
144
+ development: false,
145
+ wpnotificationclass: number,
146
+ data: notif}
147
+
148
+ wptype: ype of the notification to send, posible values are: "toast" or "badge", if this parameter is not present, a "raw" type notifications will be sent.
149
+ secret: a unic hash to identify a conection, internal use, each notification sent must have a diferent id
150
+ wpids: jsonArray of ids for target devices
151
+ data: notification data to send, please use de xsml template provided by Microsoft(http://msdn.microsoft.com/en-us/library/windowsphone/develop/hh202945(v=vs.105).aspx) for each wptype listed above
152
+
153
+
154
+
110
155
  ## Examples
111
156
 
112
157
  A simple example using ruby code to send a push notification to iOS devices.
@@ -134,20 +179,26 @@ msg_json = MultiJson.dump(msg)
134
179
  redis = Redis.new({ host: "localhost", port: 6379})
135
180
 
136
181
  # Push the message to the *suj_pusher_queue* in the redis server:
137
- redis.lpush "suj_pusher_msgs", msg_json
182
+ redis.lpush "pusher:suj_pusher_msgs", msg_json
138
183
 
139
184
  # Notify workers there is a new message
140
- redis.publish "suj_pusher_queue", "PUSH_MSG"
185
+ redis.publish "pusher:suj_pusher_queue", "PUSH_MSG"
141
186
  ```
142
187
 
143
- You must push the messages to the *suj_pusher_queue* queue that is the one the Pusher daemon is listening to. Also make sure your message follows the format described on the previous sections.
188
+ First you must push your messages to the *suj_pusher_msgs* queue and then publish a *PUSH_MSG* to the *suj_pusher_queue*. The first is a simple queue that stores the messages and the second is a PUB/SUB queue that tells the push daemons that a new message is available. Also make sure to set the same namespace (e.g. pusher) to the queue names that you set on the pusher daemons.
144
189
 
145
190
  ## Issues
146
191
 
147
192
  - We have no feedback mechanism. This is a fire and forget daemon that does not tell us if the message was sent or not.
148
193
  - This daemon has no security at all. Anyone that can push to your redis server can use this daemon to spam your users. Make sure your redis server is only accessible to you and the pusher daemon.
149
194
 
195
+ ## Troubleshooting
196
+
197
+ - You get errors like "Encryption not available on this event-machine", ensure that the eventmachine gem was installed with ssl support. If you are not sure uninstall the gem, install libssl-dev package and re-install the gem again.
198
+ - If you get 401 Unauthorized error on the log when sending notifications via GCM, ensure the key you use has configured the global IP addresses of the pusher daemons as allowed.
199
+
150
200
  ## TODO
151
201
 
152
202
  - Implement a feedback mechanism that exposes a simple API to allow users check if some tokens, ids, certs, or api_keys are no longer valid so they can take proper action.
153
203
  - Find a way to register certificates and api_key only once so we do not need to send them for every request. Maybe add a cert/key registration api.
204
+ - For MPNS add ssl service authentification for unlimited push notifications.
data/bin/pusher CHANGED
@@ -37,7 +37,7 @@ ARGV.options do |opts|
37
37
  opts.on('-l LOGS', '--logdir LOGS', String, 'Logs destination directory') { |l| logdir = l }
38
38
  opts.on('-p PIDS', '--piddir PIDS', String, 'Pids destination diercoty') { |pid| piddir = pid }
39
39
  opts.on('-c CERTS', '--cerdir CERTS', String, 'Directory to store certificates') { |cert| cerdir = cert }
40
- opts.on('-v', '--version', 'Print this version of pusher daemon.') { puts "rapns #{Suj::Pusher::VERSION}"; exit }
40
+ opts.on('-v', '--version', 'Print this version of pusher daemon.') { puts "Pusher #{Suj::Pusher::VERSION}"; exit }
41
41
  opts.on('-h', '--help', 'You\'re looking at it.') { puts opts; exit }
42
42
  opts.parse!
43
43
  end
@@ -7,6 +7,10 @@ require 'suj/pusher/apn_feedback_connection'
7
7
  require 'suj/pusher/apn_connection'
8
8
  require 'suj/pusher/gcm_connection'
9
9
  require 'suj/pusher/apn_notification'
10
+ require 'suj/pusher/wns_connection'
11
+ require 'suj/pusher/wns_notification'
12
+ require 'suj/pusher/wpns_connection'
13
+ require 'suj/pusher/wpns_notification'
10
14
  require 'suj/pusher/daemon'
11
15
 
12
16
  require 'logger'
@@ -48,6 +48,30 @@ module Suj
48
48
  @pool[api_key] ||= Suj::Pusher::GCMConnection.new(self, api_key, options)
49
49
  end
50
50
 
51
+ def apn_connection(options = {})
52
+ cert = Digest::SHA1.hexdigest options[:cert]
53
+ info "APN connection #{cert}"
54
+ @pool[cert] ||= EM.connect(APN_GATEWAY, APN_PORT, APNConnection, self, options)
55
+ end
56
+
57
+ def wns_connection(options = {})
58
+ cert = Digest::SHA1.hexdigest options[:secret]
59
+ info "WNS connection #{cert}"
60
+ info "WNS Options #{options}"
61
+ @pool[cert] ||= Suj::Pusher::WNSConnection.new(self,options)
62
+ end
63
+
64
+ def wpns_connection(options = {})
65
+ cert = Digest::SHA1.hexdigest options[:secret]
66
+ info "WPNS connection #{cert}"
67
+ info "WPNS Options #{options}"
68
+ @pool[cert] ||= Suj::Pusher::WPNSConnection.new(self,options)
69
+ return @pool[cert]
70
+ end
71
+
72
+
73
+
74
+
51
75
  def remove_connection(key)
52
76
  info "Removing connection #{key}"
53
77
  info "Connection not found" unless @pool.delete(key)
@@ -81,6 +81,12 @@ module Suj
81
81
  end
82
82
  elsif msg.has_key?(:api_key)
83
83
  send_gcm_notification(msg)
84
+ elsif msg.has_key?(:secret) && msg.has_key?(:sid)
85
+ #wns push notification
86
+ send_wns_notification(msg)
87
+ elsif msg.has_key?(:wpnotificationclass)
88
+ #send wpns push notification
89
+ send_wpns_notification(msg)
84
90
  else
85
91
  warn "Could not determine push notification service."
86
92
  end
@@ -132,6 +138,20 @@ module Suj
132
138
  conn.deliver(msg)
133
139
  end
134
140
 
141
+ def send_wns_notification(msg)
142
+ info "Sending WNS notification via connection #{Digest::SHA1.hexdigest(msg[:secret])}"
143
+ conn = pool.wns_connection(msg)
144
+ conn.deliver(msg)
145
+ end
146
+
147
+ def send_wpns_notification(msg)
148
+ info "Sending WPNS notification via connection #{Digest::SHA1.hexdigest(msg[:secret])}"
149
+ conn = pool.wpns_connection(msg)
150
+ conn.deliver(msg)
151
+ end
152
+
153
+
154
+
135
155
  def redis_url
136
156
  @redis_url ||= "redis://#{Suj::Pusher.config.redis_host}:#{Suj::Pusher.config.redis_port}/#{Suj::Pusher.config.redis_db}"
137
157
  end
@@ -1,5 +1,5 @@
1
1
  module Suj
2
2
  module Pusher
3
- VERSION = '0.1.5'
3
+ VERSION = '0.2.0'
4
4
  end
5
5
  end
@@ -0,0 +1,142 @@
1
+ require "eventmachine"
2
+ require "em-http"
3
+ require "base64"
4
+ require 'uri'
5
+ module Suj
6
+ module Pusher
7
+ class WNSConnection
8
+ include Suj::Pusher::Logger
9
+
10
+ WPN_OAUTH_SERVER = "https://login.live.com/accesstoken.srf"
11
+
12
+ ERRORS = {
13
+ 0 => "No errors encountered",
14
+ 1 => "Processing error",
15
+ 2 => "Missing device token",
16
+ 3 => "Missing topic",
17
+ 4 => "Missing payload",
18
+ 5 => "Invalid token size",
19
+ 6 => "Invalid topic size",
20
+ 7 => "Invalid payload size",
21
+ 8 => "Invalid token",
22
+ 255 => "Unknown error"
23
+ }
24
+
25
+ def initialize(pool,options = {})
26
+ @pool = pool
27
+ @options = options
28
+ #info "options #{options}"
29
+ @cert_key = Digest::SHA1.hexdigest(@options[:secret])
30
+ @cert_file = File.join(Suj::Pusher.config.certs_path, @cert_key)
31
+ end
32
+
33
+
34
+
35
+ def deliver(options)
36
+
37
+ begin
38
+ @body = "grant_type=client_credentials&client_id=#{CGI.escape(@options[:sid])}&client_secret=#{CGI.escape(@options[:secret])}&scope=notify.windows.com"
39
+ @header = {"Content-Type" => "application/x-www-form-urlencoded"}
40
+ http = EventMachine::HttpRequest.new(WPN_OAUTH_SERVER).post( :head => @header, :body =>@body )
41
+ info http.inspect
42
+ http.errback do
43
+ error "WNS network error"
44
+ @pool.remove_connection(@cert_key)
45
+ end
46
+ http.callback do
47
+ if http.response_header.status != 200
48
+ error "WNS push error #{http.response_header.status}"
49
+ error http.response
50
+ else
51
+
52
+
53
+
54
+ token = JSON.parse(http.response)
55
+ @options = options
56
+ @notification = Suj::Pusher::WnsNotification.new(@options)
57
+ info "WNS delivering data"
58
+ @Authorization = "Bearer #{token["access_token"]}"
59
+
60
+ @Content_type = ""
61
+ if(@options[:wnstype].eql?("wns/raw"))
62
+
63
+ @Content_type = "application/octet-stream"
64
+ else
65
+
66
+ @Content_type = "text/xml"
67
+ end
68
+ @Content_length = @notification.data.size
69
+ @X_WNS_Type = @options[:wnstype]
70
+ @X_WNS_Cache_Policy = "no-cache"
71
+ if(@options.has_key?("wnscache"))
72
+
73
+ @X_WNS_Cache_Policy = @options[:wnscache]
74
+ end
75
+ @X_WNS_RequestForStatus = true
76
+ if(@options.has_key?("wnsrequeststatus"))
77
+
78
+ @X_WNS_RequestForStatus = @options[:wnsrequeststatus]
79
+ end
80
+ @X_WNS_Tag = nil
81
+ if(@options.has_key?("wnstag"))
82
+
83
+ @X_WNS_Tag = @options[:wnstag]
84
+ end
85
+ @X_WNS_TTL = nil
86
+ if(@options.has_key?("time_to_live"))
87
+
88
+ @X_WNS_Cache_Policy = @options[:time_to_live]
89
+ end
90
+ @options[:wnsids].each do |id|
91
+ info "atempting send notification to Id: #{id}"
92
+ @header = Hash.new
93
+ @header['Authorization'] = @Authorization
94
+ @header['Content-Type'] = @Content_type
95
+ @header['Content-Length'] = @Content_length
96
+ @header['X-WNS-Type'] = @X_WNS_Type
97
+ @header['X-WNS-Cache-Policy'] = @X_WNS_Cache_Policy
98
+ @header['X-WNS-RequestForStatus'] = @X_WNS_RequestForStatus
99
+ if(@X_WNS_Tag!= nil)
100
+ @header['X-WNS-Tag'] = @X_WNS_Tag
101
+ end
102
+ if(@X_WNS_TTL!= nil)
103
+ @header['X-WNS-TTL'] = @X_WNS_TTL
104
+ end
105
+ http2 = EventMachine::HttpRequest.new(id).post( :head => @header, :body =>@options[:data]) #@notification.data )
106
+ http2.errback do
107
+ error "WNS-sending network error"
108
+ end
109
+ http2.callback do
110
+ if http2.response_header.status != 200
111
+ error "WNS-sending push error #{http2.response_header.status}"
112
+ error http.response
113
+ else
114
+ info "WNS-push notification sent"
115
+ info http2.response_header
116
+ end
117
+ end
118
+
119
+ end
120
+ end #nbersano
121
+ end #nbersano
122
+ @notification = nil
123
+ info "WNS delivered data"
124
+ rescue => ex
125
+ unbind
126
+ error "WNS notification error : #{ex}"
127
+ end
128
+ unbind
129
+ end
130
+
131
+
132
+
133
+
134
+ def unbind
135
+ info "WNS Connection closed..."
136
+ @disconnected = true
137
+ FileUtils.rm_f(@cert_file)
138
+ @pool.remove_connection(@cert_key)
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,34 @@
1
+ module Suj
2
+ module Pusher
3
+ class WnsNotification
4
+ include Suj::Pusher::Logger
5
+
6
+
7
+ def initialize(options = {})
8
+ @options = options
9
+ end
10
+
11
+ def payload
12
+ @payload ||= MultiJson.dump(@options[:data] || {})
13
+ end
14
+
15
+ def data
16
+ @data ||= encode_data
17
+ end
18
+
19
+ private
20
+
21
+ def get_expiry
22
+ if @ttl.to_i == 0
23
+ return 0
24
+ else
25
+ return Time.now.to_i + @ttl.to_i
26
+ end
27
+ end
28
+
29
+ def encode_data
30
+ Base64.encode64(payload)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,98 @@
1
+ require "eventmachine"
2
+ require "em-http"
3
+ require "base64"
4
+ require 'uri'
5
+ module Suj
6
+ module Pusher
7
+ class WPNSConnection
8
+ include Suj::Pusher::Logger
9
+
10
+
11
+ ERRORS = {
12
+ 0 => "No errors encountered",
13
+ 1 => "Processing error",
14
+ 2 => "Missing device token",
15
+ 3 => "Missing topic",
16
+ 4 => "Missing payload",
17
+ 5 => "Invalid token size",
18
+ 6 => "Invalid topic size",
19
+ 7 => "Invalid payload size",
20
+ 8 => "Invalid token",
21
+ 255 => "Unknown error"
22
+ }
23
+
24
+ def initialize(pool,options = {})
25
+ @pool = pool
26
+ @options = options
27
+ @cert_key = Digest::SHA1.hexdigest(@options[:secret])
28
+ @cert_file = File.join(Suj::Pusher.config.certs_path, @cert_key)
29
+ info "initialazing wns connection"
30
+ end
31
+
32
+
33
+ def deliver(options)
34
+ begin
35
+ @options = options
36
+ @notification = Suj::Pusher::WnsNotification.new(@options)
37
+ info "WPNS delivering data"
38
+ # @Authorization = "Bearer #{token["access_token"]}"
39
+ @Content_type = "text/xml; charset=utf-8"
40
+ @Content_length = @options[:data].size #@notification.data.size
41
+ @X_MessageID = nil
42
+ @X_MessageID = @options[:wpmid]
43
+ @X_WindowsPhone_Target = nil
44
+ @X_WindowsPhone_Target = @options[:wptype]
45
+ @X_NotificationClass = @options[:wpnotificationclass]
46
+ @options[:wpids].each do |id|
47
+ info "atempting send notification to Id: #{id}"
48
+ @header = Hash.new
49
+ @header['Content-Type'] = @Content_type
50
+ @header['Accept'] = "application/*"
51
+ # @header['Content-Length'] = @Content_length
52
+ @header['X-NotificationClass'] = @X_NotificationClass
53
+ if (@X_WindowsPhone_Target != nil)
54
+ @header['X-WindowsPhone-Target'] = @X_WindowsPhone_Target
55
+ else
56
+ end
57
+ if( @X_MessageID != nil)
58
+ @header['X-MessageID'] = @X_MessageID
59
+ else
60
+ end
61
+ http = EventMachine::HttpRequest.new(id).post( :head => @header, :body => @options[:data])# @notification.data )
62
+ info http.inspect
63
+ http.errback do
64
+ error "WPNS-sending network error"
65
+ end
66
+ http.callback do
67
+ if http.response_header.status != 200
68
+ error "WPNS-sending push error #{http.response_header.status}"
69
+ error http.response
70
+ error http.response_header
71
+ else
72
+ info "WPNS-push notification sent"
73
+ info http.response_header
74
+ end
75
+ end
76
+
77
+ end
78
+
79
+ @notification = nil
80
+ info "WPNS delivered data"
81
+ rescue => ex
82
+ unbind
83
+ error "WPNS notification error : #{ex}"
84
+ end
85
+ unbind
86
+ end
87
+
88
+
89
+
90
+
91
+ def unbind
92
+ info "WPNS Connection closed..."
93
+ FileUtils.rm_f(@cert_file)
94
+ @pool.remove_connection(@cert_key)
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,34 @@
1
+ module Suj
2
+ module Pusher
3
+ class WnpsNotification
4
+ include Suj::Pusher::Logger
5
+
6
+
7
+ def initialize(options = {})
8
+ @options = options
9
+ end
10
+
11
+ def payload
12
+ @payload ||= MultiJson.dump(@options[:data] || {})
13
+ end
14
+
15
+ def data
16
+ @data ||= encode_data
17
+ end
18
+
19
+ private
20
+
21
+ def get_expiry
22
+ if @ttl.to_i == 0
23
+ return 0
24
+ else
25
+ return Time.now.to_i + @ttl.to_i
26
+ end
27
+ end
28
+
29
+ def encode_data
30
+ Base64.encode64(payload)
31
+ end
32
+ end
33
+ end
34
+ end
metadata CHANGED
@@ -1,19 +1,19 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: suj-pusher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
8
- - Horacio Sanson
8
+ - Horacio Sanson / Fernando Wong / Nicolas Bersano(WNS and MPNS support)
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-10-11 00:00:00.000000000 Z
12
+ date: 2013-10-31 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: em-http-request
16
- requirement: &18258240 !ruby/object:Gem::Requirement
16
+ requirement: &16244860 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *18258240
24
+ version_requirements: *16244860
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: em-hiredis
27
- requirement: &18257120 !ruby/object:Gem::Requirement
27
+ requirement: &16244440 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *18257120
35
+ version_requirements: *16244440
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: multi_json
38
- requirement: &18255620 !ruby/object:Gem::Requirement
38
+ requirement: &16244020 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *18255620
46
+ version_requirements: *16244020
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: daemon-spawn
49
- requirement: &18254140 !ruby/object:Gem::Requirement
49
+ requirement: &16243600 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0'
55
55
  type: :runtime
56
56
  prerelease: false
57
- version_requirements: *18254140
58
- description: Stand alone push notification server for APN and GCM.
57
+ version_requirements: *16243600
58
+ description: Stand alone push notification server for APN, GCM, WNS and MPNS.
59
59
  email:
60
- - rd@skillupjapan.co.jp
60
+ - rd@skillupjapan.co.jp, n.bersano@skillupchile.cl
61
61
  executables:
62
62
  - pusher
63
63
  extensions: []
@@ -76,6 +76,10 @@ files:
76
76
  - lib/suj/pusher/logger.rb
77
77
  - lib/suj/pusher/monkey/hash.rb
78
78
  - lib/suj/pusher/version.rb
79
+ - lib/suj/pusher/wns_connection.rb
80
+ - lib/suj/pusher/wns_notification.rb
81
+ - lib/suj/pusher/wpns_connection.rb
82
+ - lib/suj/pusher/wpns_notification.rb
79
83
  - bin/pusher
80
84
  homepage: https://github.com/sujrd/suj-pusher
81
85
  licenses: []