rack-push-notification 0.0.1 → 0.1.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.
- data/Gemfile.lock +40 -1
- data/README.md +22 -5
- data/lib/rack/push-notification.rb +30 -108
- data/lib/rack/push-notification/admin.rb +129 -0
- data/lib/rack/push-notification/assets/images/wallpaper-clown-fish.jpg +0 -0
- data/lib/rack/push-notification/assets/javascripts/application.coffee +28 -0
- data/lib/rack/push-notification/assets/javascripts/collections/devices.coffee +28 -0
- data/lib/rack/push-notification/assets/javascripts/models/device.coffee +2 -0
- data/lib/rack/push-notification/assets/javascripts/routers/root.coffee +30 -0
- data/lib/rack/push-notification/assets/javascripts/rpn.coffee +14 -0
- data/lib/rack/push-notification/assets/javascripts/templates/_devices.jst.eco +23 -0
- data/lib/rack/push-notification/assets/javascripts/templates/_preview.jst.eco +24 -0
- data/lib/rack/push-notification/assets/javascripts/templates/compose.jst.eco +46 -0
- data/lib/rack/push-notification/assets/javascripts/templates/devices.jst.eco +12 -0
- data/lib/rack/push-notification/assets/javascripts/templates/pagination.jst.eco +12 -0
- data/lib/rack/push-notification/assets/javascripts/vendor/backbone.js +1431 -0
- data/lib/rack/push-notification/assets/javascripts/vendor/backbone.paginator.js +833 -0
- data/lib/rack/push-notification/assets/javascripts/vendor/codemirror.javascript.js +411 -0
- data/lib/rack/push-notification/assets/javascripts/vendor/codemirror.js +3047 -0
- data/lib/rack/push-notification/assets/javascripts/vendor/date.js +104 -0
- data/lib/rack/push-notification/assets/javascripts/vendor/jquery.js +9404 -0
- data/lib/rack/push-notification/assets/javascripts/vendor/underscore.js +1059 -0
- data/lib/rack/push-notification/assets/javascripts/views/compose.coffee +119 -0
- data/lib/rack/push-notification/assets/javascripts/views/devices.coffee +23 -0
- data/lib/rack/push-notification/assets/javascripts/views/pagination.coffee +29 -0
- data/lib/rack/push-notification/assets/stylesheets/_codemirror.sass +219 -0
- data/lib/rack/push-notification/assets/stylesheets/_preview.sass +148 -0
- data/lib/rack/push-notification/assets/stylesheets/screen.sass +110 -0
- data/lib/rack/push-notification/assets/views/index.haml +26 -0
- data/lib/rack/push-notification/device.rb +33 -0
- data/lib/rack/push-notification/migrations/001_base_schema.rb +26 -0
- data/lib/rack/push-notification/migrations/002_add_full_text_search.rb +23 -0
- data/rack-push-notification.gemspec +9 -1
- metadata +164 -8
- data/lib/rack/push-notification/version.rb +0 -5
data/Gemfile.lock
CHANGED
@@ -1,16 +1,46 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
rack-push-notification (0.0
|
4
|
+
rack-push-notification (0.1.0)
|
5
|
+
bootstrap-sass (~> 2.1.1)
|
6
|
+
coffee-script (~> 2.2.0)
|
7
|
+
eco (~> 1.0.0)
|
8
|
+
haml (~> 3.1.7)
|
9
|
+
houston (~> 0.1.1)
|
5
10
|
rack (~> 1.4)
|
6
11
|
rack-contrib (~> 1.1.0)
|
12
|
+
sass (~> 3.2.3)
|
7
13
|
sequel (~> 3.37.0)
|
8
14
|
sinatra (~> 1.3.2)
|
9
15
|
sinatra-param (~> 0.1.1)
|
16
|
+
sprockets (~> 2.8.1)
|
17
|
+
sprockets-sass (~> 0.9.1)
|
10
18
|
|
11
19
|
GEM
|
12
20
|
remote: http://rubygems.org/
|
13
21
|
specs:
|
22
|
+
bootstrap-sass (2.1.1.0)
|
23
|
+
coffee-script (2.2.0)
|
24
|
+
coffee-script-source
|
25
|
+
execjs
|
26
|
+
coffee-script-source (1.4.0)
|
27
|
+
commander (4.1.2)
|
28
|
+
highline (~> 1.6.11)
|
29
|
+
eco (1.0.0)
|
30
|
+
coffee-script
|
31
|
+
eco-source
|
32
|
+
execjs
|
33
|
+
eco-source (1.1.0.rc.1)
|
34
|
+
execjs (1.4.0)
|
35
|
+
multi_json (~> 1.0)
|
36
|
+
haml (3.1.7)
|
37
|
+
highline (1.6.15)
|
38
|
+
hike (1.2.1)
|
39
|
+
houston (0.1.1)
|
40
|
+
commander (~> 4.1.2)
|
41
|
+
json (~> 1.7.3)
|
42
|
+
json (1.7.5)
|
43
|
+
multi_json (1.3.7)
|
14
44
|
rack (1.4.1)
|
15
45
|
rack-contrib (1.1.0)
|
16
46
|
rack (>= 0.9.1)
|
@@ -18,6 +48,7 @@ GEM
|
|
18
48
|
rack
|
19
49
|
rake (0.9.2.2)
|
20
50
|
rspec (0.6.4)
|
51
|
+
sass (3.2.3)
|
21
52
|
sequel (3.37.0)
|
22
53
|
sinatra (1.3.3)
|
23
54
|
rack (~> 1.3, >= 1.3.6)
|
@@ -25,6 +56,14 @@ GEM
|
|
25
56
|
tilt (~> 1.3, >= 1.3.3)
|
26
57
|
sinatra-param (0.1.1)
|
27
58
|
sinatra (~> 1.3)
|
59
|
+
sprockets (2.8.1)
|
60
|
+
hike (~> 1.2)
|
61
|
+
multi_json (~> 1.0)
|
62
|
+
rack (~> 1.0)
|
63
|
+
tilt (~> 1.1, != 1.3.0)
|
64
|
+
sprockets-sass (0.9.1)
|
65
|
+
sprockets (~> 2.0)
|
66
|
+
tilt (~> 1.1)
|
28
67
|
tilt (1.3.3)
|
29
68
|
|
30
69
|
PLATFORMS
|
data/README.md
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
Rack::PushNotification
|
2
2
|
======================
|
3
|
-
**
|
3
|
+
**A Rack-mountable webservice for managing push notifications**
|
4
4
|
|
5
|
-
|
5
|
+
> This is still in early stages of development, so proceed with caution when using this in a production application. Any bug reports, feature requests, or general feedback at this point would be greatly appreciated.
|
6
6
|
|
7
|
-
|
7
|
+
`Rack::PushNotification` generates API endpoints that can be consumed by iOS apps to register and unregister for push notifications. Along with the registration API, `Rack::PushNotification` spawns an admin console that gives you a convenient interface to manage device tokens and compose targeted push notification messages.
|
8
|
+
|
9
|
+

|
10
|
+

|
8
11
|
|
9
12
|
## Example Record
|
10
13
|
|
@@ -25,7 +28,7 @@ Each device has a `token`, which uniquely identifies the app installation on a p
|
|
25
28
|
|
26
29
|
A device's `locale` & `language` can be used to localize outgoing communications to that particular user. Having `timezone` information gives you the ability to schedule messages for an exact time of day, to ensure maximum impact (and minimum annoyance). `ip_address` as well as `lat` and `lng` allows you to specifically target users according to their geographic location.
|
27
30
|
|
28
|
-
|
31
|
+
**It is strongly recommended that you use `Rack::PushNotification` in conjunction with some sort of Rack authentication middleware, so that the registration endpoints are not accessible without some form of credentials.**
|
29
32
|
|
30
33
|
## Example Usage
|
31
34
|
|
@@ -37,8 +40,12 @@ Rack::PushNotification can be run as Rack middleware or as a single web applicat
|
|
37
40
|
require 'bundler'
|
38
41
|
Bundler.require
|
39
42
|
|
40
|
-
|
43
|
+
Rack::PushNotification::Admin.use Rack::Auth::Basic do |username, password|
|
44
|
+
[username, password] == ['admin', ENV['ADMIN_CONSOLE_PASSWORD'] || ""]
|
45
|
+
end
|
41
46
|
|
47
|
+
use Rack::PushNotification::Admin, certificate: "/path/to/apn_certificate.pem",
|
48
|
+
environment: :production
|
42
49
|
run Rack::PushNotification
|
43
50
|
```
|
44
51
|
|
@@ -64,6 +71,16 @@ didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
|
|
64
71
|
}
|
65
72
|
```
|
66
73
|
|
74
|
+
## Deployment
|
75
|
+
|
76
|
+
`Rack::PushNotification` can be deployed to Heroku with the following commands:
|
77
|
+
|
78
|
+
```
|
79
|
+
$ heroku create
|
80
|
+
$ heroku credentials:add ADMIN_CONSOLE_PASSWORD="YourPa55wordG0esH3r3"
|
81
|
+
$ git push heroku master
|
82
|
+
```
|
83
|
+
|
67
84
|
## Contact
|
68
85
|
|
69
86
|
Mattt Thompson
|
@@ -6,129 +6,51 @@ require 'sinatra/param'
|
|
6
6
|
|
7
7
|
require 'sequel'
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
Sequel.extension(:pg_array)
|
12
|
-
|
13
|
-
module Rack::PushNotification
|
14
|
-
end
|
9
|
+
Sequel.extension(:pg_array, :migration)
|
15
10
|
|
16
11
|
module Rack
|
17
|
-
|
18
|
-
|
19
|
-
klass.dataset = :devices
|
20
|
-
|
21
|
-
klass.class_eval do
|
22
|
-
self.strict_param_setting = false
|
23
|
-
self.raise_on_save_failure = false
|
12
|
+
class PushNotification < Sinatra::Base
|
13
|
+
VERSION = '0.1.0'
|
24
14
|
|
25
|
-
|
26
|
-
|
27
|
-
plugin :timestamps, force: true
|
28
|
-
plugin :schema
|
15
|
+
use Rack::PostBodyContentTypeParser
|
16
|
+
helpers Sinatra::Param
|
29
17
|
|
30
|
-
|
31
|
-
primary_key :id
|
32
|
-
|
33
|
-
column :token, :varchar, null: false, unique: true
|
34
|
-
column :alias, :varchar
|
35
|
-
column :badge, :int4, null: false, default: 0
|
36
|
-
column :locale, :varchar
|
37
|
-
column :language, :varchar
|
38
|
-
column :timezone, :varchar, null: false, default: 'UTC'
|
39
|
-
column :ip_address, :inet
|
40
|
-
column :lat, :float8
|
41
|
-
column :lng, :float8
|
42
|
-
column :tags, :'text[]'
|
43
|
-
|
44
|
-
index :token
|
45
|
-
index :alias
|
46
|
-
index [:lat, :lng]
|
47
|
-
end
|
18
|
+
disable :raise_errors, :show_exceptions
|
48
19
|
|
49
|
-
|
50
|
-
|
51
|
-
def before_validation
|
52
|
-
normalize_token!
|
53
|
-
end
|
54
|
-
|
55
|
-
private
|
56
|
-
|
57
|
-
def normalize_token!
|
58
|
-
self.token = self.token.strip.gsub(/[<\s>]/, '')
|
59
|
-
end
|
20
|
+
before do
|
21
|
+
content_type :json
|
60
22
|
end
|
61
23
|
|
62
|
-
|
63
|
-
|
64
|
-
|
24
|
+
put '/devices/:token/?' do
|
25
|
+
param :languages, Array
|
26
|
+
param :tags, Array
|
65
27
|
|
66
|
-
|
28
|
+
@record = Device.find(token: params[:token]) || Device.new
|
29
|
+
@record.set(params)
|
67
30
|
|
68
|
-
|
69
|
-
content_type :json
|
70
|
-
end
|
31
|
+
code = @record.new? ? 201 : 200
|
71
32
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
@devices = @devices.filter(attribute => params[attribute]) if params[attribute]
|
79
|
-
end
|
80
|
-
|
81
|
-
@devices.to_json
|
82
|
-
end
|
83
|
-
|
84
|
-
put '/devices/:token/?' do
|
85
|
-
param :languages, Array
|
86
|
-
param :tags, Array
|
87
|
-
|
88
|
-
@record = klass.new(params)
|
89
|
-
@record.tags = nil
|
90
|
-
if @record.save
|
91
|
-
status 201
|
92
|
-
@record.to_json
|
93
|
-
else
|
94
|
-
status 406
|
95
|
-
{errors: @record.errors}.to_json
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
get '/devices/:token/?' do
|
100
|
-
@record = klass.find(token: params[:token])
|
101
|
-
if @record
|
102
|
-
@record.to_json
|
103
|
-
else
|
104
|
-
status 404
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
delete '/devices/:token/?' do
|
109
|
-
@record = klass.find(token: params[:token]) or halt 404
|
110
|
-
if @record.destroy
|
111
|
-
status 200
|
112
|
-
else
|
113
|
-
status 406
|
114
|
-
{errors: record.errors}.to_json
|
115
|
-
end
|
33
|
+
if @record.save
|
34
|
+
status code
|
35
|
+
@record.to_json
|
36
|
+
else
|
37
|
+
status 406
|
38
|
+
{errors: @record.errors}.to_json
|
116
39
|
end
|
117
40
|
end
|
118
41
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
module PushNotification
|
123
|
-
class << self
|
124
|
-
def new(options = {})
|
125
|
-
@app ||= ::Rack::PushNotification()
|
126
|
-
end
|
42
|
+
delete '/devices/:token/?' do
|
43
|
+
@record = Device.find(token: params[:token]) or halt 404
|
127
44
|
|
128
|
-
|
129
|
-
|
130
|
-
|
45
|
+
if @record.destroy
|
46
|
+
status 200
|
47
|
+
else
|
48
|
+
status 406
|
49
|
+
{errors: record.errors}.to_json
|
131
50
|
end
|
132
51
|
end
|
133
52
|
end
|
134
53
|
end
|
54
|
+
|
55
|
+
require 'rack/push-notification/device'
|
56
|
+
require 'rack/push-notification/admin'
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'coffee-script'
|
2
|
+
require 'eco'
|
3
|
+
require 'sass'
|
4
|
+
require 'compass'
|
5
|
+
require 'bootstrap-sass'
|
6
|
+
require 'sprockets'
|
7
|
+
require 'sprockets-sass'
|
8
|
+
require 'houston'
|
9
|
+
|
10
|
+
module Rack
|
11
|
+
class PushNotification::Admin < Sinatra::Base
|
12
|
+
use Rack::Static, urls: ['/images'], root: ::File.join(root, "assets")
|
13
|
+
use Rack::PostBodyContentTypeParser
|
14
|
+
|
15
|
+
helpers Sinatra::Param
|
16
|
+
|
17
|
+
set :root, ::File.dirname(__FILE__)
|
18
|
+
set :views, Proc.new { ::File.join(root, "assets/views") }
|
19
|
+
|
20
|
+
set :assets, Sprockets::Environment.new(::File.join(settings.root, "assets"))
|
21
|
+
settings.assets.append_path "javascripts"
|
22
|
+
settings.assets.append_path "stylesheets"
|
23
|
+
|
24
|
+
def initialize(app = nil, options = {})
|
25
|
+
super(app)
|
26
|
+
|
27
|
+
self.class.set :apn_certificate, options.delete(:certificate)
|
28
|
+
self.class.set :apn_environment, options.delete(:environment)
|
29
|
+
end
|
30
|
+
|
31
|
+
before do
|
32
|
+
content_type :json
|
33
|
+
end
|
34
|
+
|
35
|
+
get '/devices/?' do
|
36
|
+
param :q, String
|
37
|
+
param :offset, Integer, default: 0
|
38
|
+
param :limit, Integer, max: 100, min: 1, default: 25
|
39
|
+
|
40
|
+
@devices = ::Rack::PushNotification::Device.dataset
|
41
|
+
@devices = @devices.filter("tsv @@ to_tsquery('english', ?)", "#{params[:q]}:*") if params[:q] and not params[:q].empty?
|
42
|
+
|
43
|
+
{
|
44
|
+
devices: @devices.limit(params[:limit], params[:offset]),
|
45
|
+
total: @devices.count
|
46
|
+
}.to_json
|
47
|
+
end
|
48
|
+
|
49
|
+
get '/devices/:token/?' do
|
50
|
+
@record = ::Rack::PushNotification::Device.find(token: params[:token])
|
51
|
+
|
52
|
+
if @record
|
53
|
+
@record.to_json
|
54
|
+
else
|
55
|
+
status 404
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
head '/message' do
|
60
|
+
status 503 and return unless client
|
61
|
+
|
62
|
+
status 204
|
63
|
+
end
|
64
|
+
|
65
|
+
post '/message' do
|
66
|
+
status 503 and return unless client
|
67
|
+
|
68
|
+
param :payload, String, empty: false
|
69
|
+
param :tokens, Array, empty: false
|
70
|
+
|
71
|
+
tokens = params[:tokens] || ::Rack::PushNotification::Device.all.collect(&:token)
|
72
|
+
|
73
|
+
options = JSON.parse(params[:payload])
|
74
|
+
options[:alert] = options["aps"]["alert"]
|
75
|
+
options[:badge] = options["aps"]["badge"]
|
76
|
+
options[:sound] = options["aps"]["sound"]
|
77
|
+
options.delete("aps")
|
78
|
+
|
79
|
+
begin
|
80
|
+
notifications = tokens.collect{|token| Houston::Notification.new(options.update({device: token}))}
|
81
|
+
client.push(*notifications)
|
82
|
+
|
83
|
+
status 204
|
84
|
+
rescue => error
|
85
|
+
status 500
|
86
|
+
|
87
|
+
{error: error}.to_json
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
get "/javascripts/:file.js" do
|
92
|
+
content_type "application/javascript"
|
93
|
+
|
94
|
+
settings.assets["#{params[:file]}.js"]
|
95
|
+
end
|
96
|
+
|
97
|
+
get "/stylesheets/:file.css" do
|
98
|
+
content_type "text/css"
|
99
|
+
|
100
|
+
settings.assets["#{params[:file]}.css"]
|
101
|
+
end
|
102
|
+
|
103
|
+
get '*' do
|
104
|
+
content_type :html
|
105
|
+
|
106
|
+
haml :index
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def client
|
112
|
+
begin
|
113
|
+
return nil unless settings.apn_certificate and ::File.exist?(settings.apn_certificate)
|
114
|
+
|
115
|
+
client = case settings.apn_environment.to_sym
|
116
|
+
when :development
|
117
|
+
Houston::Client.development
|
118
|
+
when :production
|
119
|
+
Houston::Client.production
|
120
|
+
end
|
121
|
+
client.certificate = ::File.read(settings.apn_certificate)
|
122
|
+
|
123
|
+
return client
|
124
|
+
rescue
|
125
|
+
return nil
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
Binary file
|
@@ -0,0 +1,28 @@
|
|
1
|
+
#= require ./vendor/date
|
2
|
+
#= require ./vendor/jquery
|
3
|
+
#= require ./vendor/underscore
|
4
|
+
#= require ./vendor/backbone
|
5
|
+
#= require ./vendor/backbone.paginator
|
6
|
+
#= require ./vendor/codemirror
|
7
|
+
#= require ./vendor/codemirror.javascript
|
8
|
+
|
9
|
+
#= require ./rpn
|
10
|
+
#= require_tree ./models
|
11
|
+
#= require_tree ./collections
|
12
|
+
#= require_tree ./templates
|
13
|
+
#= require_tree ./views
|
14
|
+
#= require_tree ./routers
|
15
|
+
|
16
|
+
$ ->
|
17
|
+
$('a').live 'click', (event) ->
|
18
|
+
href = $(this).attr('href')
|
19
|
+
event.preventDefault()
|
20
|
+
window.app.navigate(href, {trigger: true})
|
21
|
+
|
22
|
+
$('.iphone .slider input').live 'change', (event) ->
|
23
|
+
$(this).siblings("span").css(opacity: (100 - $(this).val()) / 100.0)
|
24
|
+
$('.alert button.close').live 'click', (event) ->
|
25
|
+
$(this).parents(".alert").remove()
|
26
|
+
|
27
|
+
RPN.devices = new RPN.Collections.Devices
|
28
|
+
RPN.devices.fetch(success: RPN.initialize)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class RPN.Collections.Devices extends Backbone.Paginator.requestPager
|
2
|
+
model: RPN.Models.Device
|
3
|
+
|
4
|
+
paginator_core:
|
5
|
+
type: 'GET'
|
6
|
+
dataType: 'json'
|
7
|
+
url: '/devices?'
|
8
|
+
|
9
|
+
paginator_ui:
|
10
|
+
firstPage: 1,
|
11
|
+
currentPage: 1,
|
12
|
+
perPage: 20
|
13
|
+
|
14
|
+
server_api:
|
15
|
+
'q': ->
|
16
|
+
@query || ""
|
17
|
+
'limit': ->
|
18
|
+
@perPage
|
19
|
+
'offset': ->
|
20
|
+
(@currentPage - 1) * @perPage
|
21
|
+
|
22
|
+
parse: (response) ->
|
23
|
+
@total = response.total
|
24
|
+
@totalPages = Math.ceil(@total / @perPage)
|
25
|
+
response.devices
|
26
|
+
|
27
|
+
comparator: (database) ->
|
28
|
+
database.get('token')
|