├── example ├── Procfile ├── config.ru ├── Gemfile ├── README.md └── Gemfile.lock ├── Gemfile ├── lib └── rack │ ├── push-notification │ ├── version.rb │ ├── migrations │ │ ├── 002_add_full_text_search.rb │ │ └── 001_base_schema.rb │ └── models │ │ └── device.rb │ └── push-notification.rb ├── Rakefile ├── LICENSE.md ├── Gemfile.lock ├── rack-push-notification.gemspec └── README.md /example/Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec thin start -p $PORT 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /example/config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler' 4 | Bundler.require 5 | 6 | run Rack::PushNotification 7 | -------------------------------------------------------------------------------- /lib/rack/push-notification/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rack 4 | class PushNotification 5 | VERSION = '0.6.0' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'rack-push-notification', require: 'rack/push-notification', path: File.join(__FILE__, '../..') 6 | 7 | gem 'pg' 8 | gem 'sinatra' 9 | gem 'thin' 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler' 4 | Bundler.setup 5 | 6 | gemspec = eval(File.read('rack-push-notification.gemspec')) 7 | 8 | task build: "#{gemspec.full_name}.gem" 9 | 10 | file "#{gemspec.full_name}.gem" => gemspec.files + ['rack-push-notification.gemspec'] do 11 | system 'gem build rack-push-notification.gemspec' 12 | end 13 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Rack::PushNotification Example 2 | 3 | ## Requirements 4 | 5 | - Postgres 9.1 or above running locally (see [Postgres.app](http://postgresapp.com) for an easy way to get set up on a Mac) 6 | - [Heroku Toolbelt](https://toolbelt.heroku.com) 7 | 8 | ## Instructions 9 | 10 | To run the example application, run the following commands: 11 | 12 | ```sh 13 | $ cd example 14 | $ psql -c "CREATE DATABASE rack_push_notification;" 15 | $ echo "DATABASE_URL=postgres://localhost:5432/rack_push_notification" > .env 16 | $ bundle 17 | $ foreman start 18 | ``` 19 | -------------------------------------------------------------------------------- /lib/rack/push-notification/migrations/002_add_full_text_search.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Sequel.migration do 4 | up do 5 | add_column :push_notification_devices, :tsv, 'TSVector' 6 | add_index :push_notification_devices, :tsv, type: 'GIN' 7 | create_trigger :push_notification_devices, :tsv, :tsvector_update_trigger, 8 | args: %i[tsv pg_catalog.english token alias locale timezone], 9 | events: %i[insert update], 10 | each_row: true 11 | end 12 | 13 | down do 14 | drop_column :push_notification_devices, :tsv 15 | drop_index :push_notification_devices, :tsv 16 | drop_trigger :push_notification_devices, :tsv 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/rack/push-notification/migrations/001_base_schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Sequel.migration do 4 | up do 5 | create_table :push_notification_devices do 6 | primary_key :id 7 | 8 | column :token, :varchar, empty: false, unique: true 9 | column :alias, :varchar 10 | column :badge, :int4, null: false, default: 0 11 | column :locale, :varchar 12 | column :language, :varchar 13 | column :timezone, :varchar, empty: false, default: 'UTC' 14 | column :ip_address, :inet 15 | column :lat, :float8 16 | column :lng, :float8 17 | column :tags, :'text[]' 18 | 19 | index :token 20 | index :alias 21 | index %i[lat lng] 22 | end 23 | end 24 | 25 | down do 26 | drop_table :push_notification_devices 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/rack/push-notification/models/device.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rack 4 | class PushNotification::Device < Sequel::Model 5 | plugin :json_serializer, naked: true, except: :id 6 | plugin :validation_helpers 7 | plugin :timestamps, force: true, update_on_create: true 8 | plugin :schema 9 | 10 | self.dataset = :push_notification_devices 11 | self.strict_param_setting = false 12 | self.raise_on_save_failure = false 13 | 14 | def before_validation 15 | normalize_token! 16 | end 17 | 18 | def validate 19 | super 20 | 21 | validates_presence :token 22 | validates_unique :token 23 | validates_format /[[:xdigit:]]{40}/, :token 24 | end 25 | 26 | private 27 | 28 | def normalize_token! 29 | self.token = token.strip.gsub(/[<\s>]/, '') 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /example/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: /Users/mattt/Code/rack-push-notification 3 | specs: 4 | rack-push-notification (0.5.3) 5 | rack (~> 1.4) 6 | rack-contrib (~> 1.1) 7 | sequel (>= 3.0) 8 | sinatra (~> 1.3) 9 | sinatra-param (~> 0.1) 10 | 11 | GEM 12 | remote: https://rubygems.org/ 13 | specs: 14 | daemons (1.3.1) 15 | eventmachine (1.2.7) 16 | pg (1.1.4) 17 | rack (1.6.11) 18 | rack-contrib (1.8.0) 19 | rack (~> 1.4) 20 | rack-protection (1.5.5) 21 | rack 22 | sequel (5.22.0) 23 | sinatra (1.4.8) 24 | rack (~> 1.5) 25 | rack-protection (~> 1.4) 26 | tilt (>= 1.3, < 3) 27 | sinatra-param (0.1.3) 28 | sinatra (~> 1.3) 29 | thin (1.7.2) 30 | daemons (~> 1.0, >= 1.0.9) 31 | eventmachine (~> 1.0, >= 1.0.4) 32 | rack (>= 1, < 3) 33 | tilt (2.0.9) 34 | 35 | PLATFORMS 36 | ruby 37 | 38 | DEPENDENCIES 39 | pg 40 | rack-push-notification! 41 | sinatra 42 | thin 43 | 44 | BUNDLED WITH 45 | 2.0.1 46 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 – 2019 Mattt (https://mat.tt/) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | rack-push-notification (0.6.0) 5 | rack (~> 1.4) 6 | rack-contrib (~> 1.1) 7 | sequel (>= 3.0) 8 | sinatra (~> 1.3) 9 | sinatra-param (~> 0.1) 10 | 11 | GEM 12 | remote: https://rubygems.org/ 13 | specs: 14 | diff-lcs (1.3) 15 | rack (1.6.11) 16 | rack-contrib (1.8.0) 17 | rack (~> 1.4) 18 | rack-protection (1.5.5) 19 | rack 20 | rake (12.3.2) 21 | rspec (3.8.0) 22 | rspec-core (~> 3.8.0) 23 | rspec-expectations (~> 3.8.0) 24 | rspec-mocks (~> 3.8.0) 25 | rspec-core (3.8.2) 26 | rspec-support (~> 3.8.0) 27 | rspec-expectations (3.8.4) 28 | diff-lcs (>= 1.2.0, < 2.0) 29 | rspec-support (~> 3.8.0) 30 | rspec-mocks (3.8.1) 31 | diff-lcs (>= 1.2.0, < 2.0) 32 | rspec-support (~> 3.8.0) 33 | rspec-support (3.8.2) 34 | sequel (5.22.0) 35 | sinatra (1.4.8) 36 | rack (~> 1.5) 37 | rack-protection (~> 1.4) 38 | tilt (>= 1.3, < 3) 39 | sinatra-param (0.1.3) 40 | sinatra (~> 1.3) 41 | tilt (2.0.9) 42 | 43 | PLATFORMS 44 | ruby 45 | 46 | DEPENDENCIES 47 | rack-push-notification! 48 | rake 49 | rspec 50 | 51 | BUNDLED WITH 52 | 2.0.1 53 | -------------------------------------------------------------------------------- /rack-push-notification.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.push File.expand_path('lib', __dir__) 4 | require 'rack/push-notification/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'rack-push-notification' 8 | s.authors = ['Mattt'] 9 | s.email = 'mattt@me.com' 10 | s.homepage = 'https://mat.tt' 11 | s.version = Rack::PushNotification::VERSION 12 | s.licenses = 'MIT' 13 | s.platform = Gem::Platform::RUBY 14 | s.summary = 'Rack::PushNotification' 15 | s.description = 'Generate a REST API for registering and querying push notification device tokens.' 16 | 17 | s.add_dependency 'rack', '~> 1.4' 18 | s.add_dependency 'rack-contrib', '~> 1.1' 19 | s.add_dependency 'sequel', '>= 3.0' 20 | s.add_dependency 'sinatra', '~> 1.3' 21 | s.add_dependency 'sinatra-param', '~> 0.1' 22 | 23 | s.add_development_dependency 'rake' 24 | s.add_development_dependency 'rspec' 25 | 26 | s.files = Dir['./**/*'].reject { |file| file =~ %r{\./(bin|example|log|pkg|script|spec|test|vendor)} } 27 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 28 | s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } 29 | s.require_paths = ['lib'] 30 | end 31 | -------------------------------------------------------------------------------- /lib/rack/push-notification.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rack' 4 | require 'rack/contrib' 5 | 6 | require 'sinatra/base' 7 | require 'sinatra/param' 8 | 9 | require 'sequel' 10 | 11 | module Rack 12 | class PushNotification < Sinatra::Base 13 | use Rack::PostBodyContentTypeParser 14 | helpers Sinatra::Param 15 | 16 | disable :raise_errors, :show_exceptions 17 | 18 | autoload :Device, 'rack/push-notification/models/device' 19 | 20 | configure do 21 | if ENV['DATABASE_URL'] 22 | Sequel.extension :pg_inet, :migration 23 | 24 | DB = Sequel.connect(ENV['DATABASE_URL']) 25 | Sequel::Model.db.extension :pg_array 26 | DB.extend Sequel::Postgres::PGArray::DatabaseMethods 27 | Sequel::Migrator.run(DB, ::File.join(::File.dirname(__FILE__), 'push-notification/migrations'), table: 'push_notification_schema_info') 28 | end 29 | end 30 | 31 | before do 32 | content_type :json 33 | end 34 | 35 | put '/devices/:token/?' do 36 | param :languages, Array 37 | param :tags, Array 38 | 39 | record = Device.find(token: params[:token]) || Device.new 40 | record.set(params.update(ip_address: request.ip)) 41 | 42 | code = record.new? ? 201 : 200 43 | 44 | if record.save 45 | status code 46 | { device: record }.to_json 47 | else 48 | status 400 49 | { errors: record.errors }.to_json 50 | end 51 | end 52 | 53 | delete '/devices/:token/?' do 54 | (record = Device.find(token: params[:token])) || halt(404) 55 | 56 | if record.destroy 57 | status 200 58 | else 59 | status 400 60 | { errors: record.errors }.to_json 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rack::PushNotification 2 | 3 | **A Rack-mountable web service for managing push notifications** 4 | 5 | > This project is no longer maintained. 6 | 7 | `Rack::PushNotification` is Rack middleware that 8 | generates API endpoints that can be consumed by iOS apps 9 | to register and unregister for push notifications. 10 | 11 | **Example Record** 12 | 13 | | Field | Value | 14 | | ------------ | --------------------------------------------------------------------------- | 15 | | `token` | `"ce8be627 2e43e855 16033e24 b4c28922 0eeda487 9c477160 b2545e95 b68b5969"` | 16 | | `alias` | `mattt@heroku.com` | 17 | | `badge` | `0` | 18 | | `locale` | `en_US` | 19 | | `language` | `en` | 20 | | `timezone` | `America/Los_Angeles` | 21 | | `ip_address` | `0.0.0.0` | 22 | | `lat` | `37.7716` | 23 | | `lng` | `-122.4137` | 24 | | `tags` | `["iPhone OS 6.0", "v1.0", "iPhone"]` | 25 | 26 | - Each device has a `token`, 27 | which uniquely identifies the app installation on a particular device. 28 | - This token can be associated with an `alias`, 29 | which can be a domain-specific piece of identifying information, 30 | such as a username or e-mail address. 31 | - A running `badge` count keeps track of the badge count to show on the app icon. 32 | - A device's `locale` & `language` can be used to 33 | localize outgoing communications to that particular user. 34 | - Having `timezone` information gives you the ability to 35 | schedule messages for an exact time of day and to 36 | ensure maximum impact (and minimum annoyance). 37 | - An `ip_address` --- along with `lat` and `lng` --- 38 | lets you to specifically target users according to their geographic location. 39 | 40 | > **Important** 41 | > Use `Rack::PushNotification` in conjunction with some kind of authentication, 42 | > so that the administration endpoints aren't publicly accessible. 43 | 44 | ## Usage 45 | 46 | Rack::PushNotification can be run as Rack middleware or as a single web application. 47 | All that's required is a connection to a Postgres database. 48 | Define this with the environment variable `DATABASE_URL`. 49 | 50 | > For rails, use the 51 | > [`rails-database-url`](https://github.com/glenngillen/rails-database-url) gem 52 | > to define this from the `database.yml`. 53 | 54 | An example application can be found in the `/example` directory of this repository. 55 | 56 | ### config.ru 57 | 58 | ```ruby 59 | require 'bundler' 60 | Bundler.require 61 | 62 | run Rack::PushNotification 63 | ``` 64 | 65 | ## Deployment 66 | 67 | `Rack::PushNotification` can be deployed to Heroku with the following commands: 68 | 69 | ``` 70 | $ heroku create 71 | $ git push heroku master 72 | ``` 73 | 74 | ## Contact 75 | 76 | [Mattt](https://twitter.com/mattt) 77 | 78 | ## License 79 | 80 | Rack::PushNotification is available under the MIT license. 81 | See the LICENSE file for more info. 82 | --------------------------------------------------------------------------------