rack-push-notification 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![Devices Screenshot](https://raw.github.com/mattt/rack-push-notification/screenshots/rack-push-notifications-screenshot-devices.png)
|
10
|
+
![Compose Screenshot](https://raw.github.com/mattt/rack-push-notification/screenshots/rack-push-notifications-screenshot-compose.png)
|
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')
|