Skip to content

Instantly share code, notes, and snippets.

@kellysutton
Last active December 16, 2020 13:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kellysutton/7609078 to your computer and use it in GitHub Desktop.
Save kellysutton/7609078 to your computer and use it in GitHub Desktop.
Web Push Package in Rails.
# The rubygems.org grocer-pushpackager needs a bump. Ride that <del>dragon</del> master.
gem 'grocer-pushpackager', git: 'git@github.com:layervault/grocer-pushpackager.git'
# Houston will help us send the notifications themselves.
gem 'houston'
# You're going to want to subclass this base class and override
# #payload, #custom_data, and #user.
#
# We have something called a PushNotificationService::RepliedComment for
# example, which handles sending out the push notification when someone
# replies to a comment on Designer News.
module PushNotificationService
class Base
def send!
validate_payload!(payload)
validate_custom_data!(custom_data)
user.device_tokens.map do |device_token|
notification = Houston::Notification.new(device: device_token.token)
notification.alert = payload
notification.custom_data = custom_data
houston.push(notification)
notification
end
end
# { "title"=>"Hello, Doge!", "body"=>"Welcome to Designer News!", "action"=>"See More" }
def payload
raise "Attempted to call an abstract method!"
end
# { "aps" => { "url-args" => [@action, @object_id] } }
def custom_data
raise "Attempted to call an abstract method!"
end
def user
raise "Attempted to call an abstract method!"
end
private
def houston
return @apn if @apn
# So the annoying thing is, you'll need a .p12 certificate and a
# .pem-style certificate with my setup. If you're down with the OpenSSL,
# you can get things down to a single certificate.
@apn ||= Houston::Client.production
@apn.certificate = File.read("/home/user/cert.pem")
@apn.passphrase = "hellapassword!"
@apn
end
def validate_payload!(payload)
raise "'title' is required" unless payload['title']
raise "'body' is required" unless payload['body']
end
def validate_custom_data!(custom_data)
raise "['aps'] is required" unless custom_data['aps']
raise "['aps']['url-args'] is required" unless custom_data['aps']['url-args']
raise "url-ags must be of size 2" unless custom_data['aps']['url-args'].size == 2
raise "url-args can only be strings" unless custom_data['aps']['url-args'][0].is_a?(String) && custom_data['aps']['url-args'][1].is_a?(String)
end
end
end
class PushNotificationsController < ApplicationController
respond_to :html
def validate
respond_to do |format|
format.html { send_data builder.buffer }
end
end
def log
logger.info params[:logs]
respond_to do |format|
format.html { head :ok }
end
end
def register_device
@user = aps_user || not_found
unless DeviceToken.where(user_id: @user.id, token: params[:token]).exists?
DeviceToken.create({
user: @user,
token: params[:token]
})
end
respond_to do |format|
format.html { head :ok }
end
end
def unregister_device
DeviceToken.where(user_id: aps_user.id, token: params[:token]).destroy
respond_to do |format|
format.html { head :ok }
end
end
private
def builder
test_icon16 = File.open(File.join(Rails.root, 'app/assets/images/push_notification_badges/16.png'))
test_icon16r = File.open(File.join(Rails.root, 'app/assets/images/push_notification_badges/32.png'))
test_icon32 = File.open(File.join(Rails.root, 'app/assets/images/push_notification_badges/32.png'))
test_icon32r = File.open(File.join(Rails.root, 'app/assets/images/push_notification_badges/64.png'))
test_icon128 = File.open(File.join(Rails.root, 'app/assets/images/push_notification_badges/128.png'))
test_icon128r = File.open(File.join(Rails.root, 'app/assets/images/push_notification_badges/256.png'))
@user = User.first
@p12 = OpenSSL::PKCS12.new File.open(File.join(Rails.root, 'lib/designer_news.p12'), 'rb').read, "passwordyeahyeah"
builder = Grocer::Pushpackager::Package.new({
websiteName: 'Designer News',
websitePushID: Rails.application.config.website_push_id,
allowedDomains: ["https://news.layervault.com"],
urlFormatString: "https://news.layervault.com/push_link/%@/%@",
authenticationToken: user.push_notification_auth_token,
webServiceURL: "https://news.layervault.com",
certificate: @p12.certificate,
key: @p12.key,
iconSet: {
:'16x16' => test_icon16,
:'16x16@2x' => test_icon16r,
:'32x32' => test_icon32,
:'32x32@2x' => test_icon32r,
:'128x128' => test_icon128,
:'128x128@2x' => test_icon128r
}
})
end
# This is pulling the user_id out of the user_info submitted with your
# window.safari.pushNotification.requestPermission call. I use the following dictionary
# on the client-side: { "user_id": "1" }
#
# NOTE: Apple hates integers, so you should .toString() things in JS and .to_s things
# in Rubby.
def user
User.find JSON.parse(request.body.read)['user_id']
end
# Apple includes an Authorization header that looks like this in their requests:
# Authorization: ApplePushNotifications <16-char auth_token>
def aps_user
User.find_by_cached_push_notification_auth_token request.headers["Authorization"].split(' ')[1]
end
end
# Apple requires your endpoint to be /v1/...
scope controller: 'push_notifications', path: 'v1', format: :html, constraints: { website_push_id: /\w+\.\w+\.\w+\.\w+/ } do
post 'pushPackages/:website_push_id', action: :validate
post 'log', action: :log
post 'devices/:token/registrations/:website_push_id', action: :register_device
delete 'devices/:token/registrations/:website_push_id', action: :unregister_device
end
class User
def push_notification_auth_token
return cached_push_notification_auth_token if cached_push_notification_auth_token
# Apple requires auth tokens be at least 16-characters
self.cached_push_notification_auth_token = SecureRandom.hex(16)
save!
self.cached_push_notification_auth_token
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment