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.
Files changed (35) hide show
  1. data/Gemfile.lock +40 -1
  2. data/README.md +22 -5
  3. data/lib/rack/push-notification.rb +30 -108
  4. data/lib/rack/push-notification/admin.rb +129 -0
  5. data/lib/rack/push-notification/assets/images/wallpaper-clown-fish.jpg +0 -0
  6. data/lib/rack/push-notification/assets/javascripts/application.coffee +28 -0
  7. data/lib/rack/push-notification/assets/javascripts/collections/devices.coffee +28 -0
  8. data/lib/rack/push-notification/assets/javascripts/models/device.coffee +2 -0
  9. data/lib/rack/push-notification/assets/javascripts/routers/root.coffee +30 -0
  10. data/lib/rack/push-notification/assets/javascripts/rpn.coffee +14 -0
  11. data/lib/rack/push-notification/assets/javascripts/templates/_devices.jst.eco +23 -0
  12. data/lib/rack/push-notification/assets/javascripts/templates/_preview.jst.eco +24 -0
  13. data/lib/rack/push-notification/assets/javascripts/templates/compose.jst.eco +46 -0
  14. data/lib/rack/push-notification/assets/javascripts/templates/devices.jst.eco +12 -0
  15. data/lib/rack/push-notification/assets/javascripts/templates/pagination.jst.eco +12 -0
  16. data/lib/rack/push-notification/assets/javascripts/vendor/backbone.js +1431 -0
  17. data/lib/rack/push-notification/assets/javascripts/vendor/backbone.paginator.js +833 -0
  18. data/lib/rack/push-notification/assets/javascripts/vendor/codemirror.javascript.js +411 -0
  19. data/lib/rack/push-notification/assets/javascripts/vendor/codemirror.js +3047 -0
  20. data/lib/rack/push-notification/assets/javascripts/vendor/date.js +104 -0
  21. data/lib/rack/push-notification/assets/javascripts/vendor/jquery.js +9404 -0
  22. data/lib/rack/push-notification/assets/javascripts/vendor/underscore.js +1059 -0
  23. data/lib/rack/push-notification/assets/javascripts/views/compose.coffee +119 -0
  24. data/lib/rack/push-notification/assets/javascripts/views/devices.coffee +23 -0
  25. data/lib/rack/push-notification/assets/javascripts/views/pagination.coffee +29 -0
  26. data/lib/rack/push-notification/assets/stylesheets/_codemirror.sass +219 -0
  27. data/lib/rack/push-notification/assets/stylesheets/_preview.sass +148 -0
  28. data/lib/rack/push-notification/assets/stylesheets/screen.sass +110 -0
  29. data/lib/rack/push-notification/assets/views/index.haml +26 -0
  30. data/lib/rack/push-notification/device.rb +33 -0
  31. data/lib/rack/push-notification/migrations/001_base_schema.rb +26 -0
  32. data/lib/rack/push-notification/migrations/002_add_full_text_search.rb +23 -0
  33. data/rack-push-notification.gemspec +9 -1
  34. metadata +164 -8
  35. 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.1)
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
- **An extensible, Rack-mountable webservice for managing push notification information**
3
+ **A Rack-mountable webservice for managing push notifications**
4
4
 
5
- There is misconception that managing push notification information is a difficult problem. It's really not.
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
- This library is generates a `/devices` API endpoint, that can be used by iOS apps to register and unregister for push notifications.
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
- > It is 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.
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
- DB = Sequel.connect(ENV['DATABASE_URL'])
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
- require 'rack/push-notification/version'
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
- def self.PushNotification(options = {})
18
- klass = Rack::PushNotification.const_set("Device", Class.new(Sequel::Model))
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
- plugin :json_serializer, naked: true, except: :id
26
- plugin :validation_helpers
27
- plugin :timestamps, force: true
28
- plugin :schema
15
+ use Rack::PostBodyContentTypeParser
16
+ helpers Sinatra::Param
29
17
 
30
- set_schema do
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
- create_table unless table_exists?
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
- app = Class.new(Sinatra::Base) do
63
- use Rack::PostBodyContentTypeParser
64
- helpers Sinatra::Param
24
+ put '/devices/:token/?' do
25
+ param :languages, Array
26
+ param :tags, Array
65
27
 
66
- disable :raise_errors, :show_exceptions
28
+ @record = Device.find(token: params[:token]) || Device.new
29
+ @record.set(params)
67
30
 
68
- before do
69
- content_type :json
70
- end
31
+ code = @record.new? ? 201 : 200
71
32
 
72
- get '/devices/?' do
73
- param :languages, Array
74
- param :tags, Array
75
-
76
- @devices = klass.dataset
77
- [:alias, :badge, :locale, :languages, :timezone, :tags].each do |attribute|
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
- return app
120
- end
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
- def call(*args)
129
- @app ||= ::Rack::PushNotification()
130
- @app.call(*args)
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
@@ -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')