Last active
December 16, 2020 13:13
-
-
Save kellysutton/7609078 to your computer and use it in GitHub Desktop.
Web Push Package in Rails.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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