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.
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')