Add REST API for Web Push Notifications subscriptions (#7445)
- POST /api/v1/push/subscription - PUT /api/v1/push/subscription - DELETE /api/v1/push/subscription - New OAuth scope: "push" (required for the above methods)
This commit is contained in:
parent
9a794067f7
commit
b4fb766b23
20 changed files with 258 additions and 81 deletions
50
app/controllers/api/v1/push/subscriptions_controller.rb
Normal file
50
app/controllers/api/v1/push/subscriptions_controller.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Push::SubscriptionsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :push }
|
||||
before_action :require_user!
|
||||
before_action :set_web_push_subscription
|
||||
|
||||
def create
|
||||
@web_subscription&.destroy!
|
||||
|
||||
@web_subscription = ::Web::PushSubscription.create!(
|
||||
endpoint: subscription_params[:endpoint],
|
||||
key_p256dh: subscription_params[:keys][:p256dh],
|
||||
key_auth: subscription_params[:keys][:auth],
|
||||
data: data_params,
|
||||
user_id: current_user.id,
|
||||
access_token_id: doorkeeper_token.id
|
||||
)
|
||||
|
||||
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
end
|
||||
|
||||
def update
|
||||
raise ActiveRecord::RecordNotFound if @web_subscription.nil?
|
||||
|
||||
@web_subscription.update!(data: data_params)
|
||||
|
||||
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
end
|
||||
|
||||
def destroy
|
||||
@web_subscription&.destroy!
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_web_push_subscription
|
||||
@web_subscription = ::Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id)
|
||||
end
|
||||
|
||||
def subscription_params
|
||||
params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
|
||||
end
|
||||
|
||||
def data_params
|
||||
return {} if params[:data].blank?
|
||||
params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention])
|
||||
end
|
||||
end
|
|
@ -31,22 +31,23 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||
endpoint: subscription_params[:endpoint],
|
||||
key_p256dh: subscription_params[:keys][:p256dh],
|
||||
key_auth: subscription_params[:keys][:auth],
|
||||
data: data
|
||||
data: data,
|
||||
user_id: active_session.user_id,
|
||||
access_token_id: active_session.access_token_id
|
||||
)
|
||||
|
||||
active_session.update!(web_push_subscription: web_subscription)
|
||||
|
||||
render json: web_subscription.as_payload
|
||||
render json: web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
end
|
||||
|
||||
def update
|
||||
params.require([:id])
|
||||
|
||||
web_subscription = ::Web::PushSubscription.find(params[:id])
|
||||
|
||||
web_subscription.update!(data: data_params)
|
||||
|
||||
render json: web_subscription.as_payload
|
||||
render json: web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -56,6 +57,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||
end
|
||||
|
||||
def data_params
|
||||
@data_params ||= params.require(:data).permit(:alerts)
|
||||
@data_params ||= params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,6 +15,7 @@ class SharesController < ApplicationController
|
|||
|
||||
def initial_state_params
|
||||
text = [params[:title], params[:text], params[:url]].compact.join(' ')
|
||||
|
||||
{
|
||||
settings: Web::Setting.find_by(user: current_user)&.data || {},
|
||||
push_subscription: current_account.user.web_push_subscription(current_session),
|
||||
|
|
|
@ -245,7 +245,7 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def web_push_subscription(session)
|
||||
session.web_push_subscription.nil? ? nil : session.web_push_subscription.as_payload
|
||||
session.web_push_subscription.nil? ? nil : session.web_push_subscription
|
||||
end
|
||||
|
||||
def invite_code=(code)
|
||||
|
|
|
@ -3,38 +3,51 @@
|
|||
#
|
||||
# Table name: web_push_subscriptions
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# endpoint :string not null
|
||||
# key_p256dh :string not null
|
||||
# key_auth :string not null
|
||||
# data :json
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# id :bigint(8) not null, primary key
|
||||
# endpoint :string not null
|
||||
# key_p256dh :string not null
|
||||
# key_auth :string not null
|
||||
# data :json
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# access_token_id :bigint(8)
|
||||
# user_id :bigint(8)
|
||||
#
|
||||
|
||||
require 'webpush'
|
||||
|
||||
class Web::PushSubscription < ApplicationRecord
|
||||
belongs_to :user, optional: true
|
||||
belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', optional: true
|
||||
|
||||
has_one :session_activation
|
||||
|
||||
def push(notification)
|
||||
I18n.with_locale(session_activation.user.locale || I18n.default_locale) do
|
||||
I18n.with_locale(associated_user.locale || I18n.default_locale) do
|
||||
push_payload(message_from(notification), 48.hours.seconds)
|
||||
end
|
||||
end
|
||||
|
||||
def pushable?(notification)
|
||||
data&.key?('alerts') && data['alerts'][notification.type.to_s]
|
||||
data&.key?('alerts') && ActiveModel::Type::Boolean.new.cast(data['alerts'][notification.type.to_s])
|
||||
end
|
||||
|
||||
def as_payload
|
||||
payload = { id: id, endpoint: endpoint }
|
||||
payload[:alerts] = data['alerts'] if data&.key?('alerts')
|
||||
payload
|
||||
def associated_user
|
||||
return @associated_user if defined?(@associated_user)
|
||||
|
||||
@associated_user = if user_id.nil?
|
||||
session_activation.user
|
||||
else
|
||||
user
|
||||
end
|
||||
end
|
||||
|
||||
def access_token
|
||||
find_or_create_access_token.token
|
||||
def associated_access_token
|
||||
return @associated_access_token if defined?(@associated_access_token)
|
||||
|
||||
@associated_access_token = if access_token_id.nil?
|
||||
find_or_create_access_token.token
|
||||
else
|
||||
access_token
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
class InitialStateSerializer < ActiveModel::Serializer
|
||||
attributes :meta, :compose, :accounts,
|
||||
:media_attachments, :settings, :push_subscription
|
||||
:media_attachments, :settings
|
||||
|
||||
has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
|
||||
def meta
|
||||
store = {
|
||||
|
|
13
app/serializers/rest/web_push_subscription_serializer.rb
Normal file
13
app/serializers/rest/web_push_subscription_serializer.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::WebPushSubscriptionSerializer < ActiveModel::Serializer
|
||||
attributes :id, :endpoint, :alerts, :server_key
|
||||
|
||||
def alerts
|
||||
object.data&.dig('alerts') || {}
|
||||
end
|
||||
|
||||
def server_key
|
||||
Rails.configuration.x.vapid_public_key
|
||||
end
|
||||
end
|
|
@ -54,7 +54,7 @@ class Web::NotificationSerializer < ActiveModel::Serializer
|
|||
|
||||
def access_token
|
||||
return if actions.empty?
|
||||
current_push_subscription.access_token
|
||||
current_push_subscription.associated_access_token
|
||||
end
|
||||
|
||||
def message
|
||||
|
|
|
@ -9,6 +9,7 @@ class NotifyService < BaseService
|
|||
return if recipient.user.nil? || blocked?
|
||||
|
||||
create_notification
|
||||
push_notification if @notification.browserable?
|
||||
send_email if email_enabled?
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
return
|
||||
|
@ -101,25 +102,27 @@ class NotifyService < BaseService
|
|||
|
||||
def create_notification
|
||||
@notification.save!
|
||||
return unless @notification.browserable?
|
||||
end
|
||||
|
||||
def push_notification
|
||||
return if @notification.activity.nil?
|
||||
|
||||
Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification)))
|
||||
send_push_notifications
|
||||
end
|
||||
|
||||
def send_push_notifications
|
||||
# HACK: Can be caused by quickly unfavouriting a status, since creating
|
||||
# a favourite and creating a notification are not wrapped in a transaction.
|
||||
return if @notification.activity.nil?
|
||||
subscriptions_ids = ::Web::PushSubscription.where(user_id: @recipient.user.id)
|
||||
.select { |subscription| subscription.pushable?(@notification) }
|
||||
.map(&:id)
|
||||
|
||||
sessions_with_subscriptions = @recipient.user.session_activations.where.not(web_push_subscription: nil)
|
||||
sessions_with_subscriptions_ids = sessions_with_subscriptions.select { |session| session.web_push_subscription.pushable? @notification }.map(&:id)
|
||||
|
||||
WebPushNotificationWorker.push_bulk(sessions_with_subscriptions_ids) do |session_activation_id|
|
||||
[session_activation_id, @notification.id]
|
||||
::Web::PushNotificationWorker.push_bulk(subscriptions_ids) do |subscription_id|
|
||||
[subscription_id, @notification.id]
|
||||
end
|
||||
end
|
||||
|
||||
def send_email
|
||||
return if @notification.activity.nil?
|
||||
NotificationMailer.public_send(@notification.type, @recipient, @notification).deliver_later
|
||||
end
|
||||
|
||||
|
|
18
app/workers/web/push_notification_worker.rb
Normal file
18
app/workers/web/push_notification_worker.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Web::PushNotificationWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options backtrace: true
|
||||
|
||||
def perform(subscription_id, notification_id)
|
||||
subscription = ::Web::PushSubscription.find(subscription_id)
|
||||
notification = Notification.find(notification_id)
|
||||
|
||||
subscription.push(notification) unless notification.activity.nil?
|
||||
rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription
|
||||
subscription.destroy!
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
|
@ -1,25 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class WebPushNotificationWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options backtrace: true
|
||||
|
||||
def perform(session_activation_id, notification_id)
|
||||
session_activation = SessionActivation.find(session_activation_id)
|
||||
notification = Notification.find(notification_id)
|
||||
|
||||
return if session_activation.web_push_subscription.nil? || notification.activity.nil?
|
||||
|
||||
session_activation.web_push_subscription.push(notification)
|
||||
rescue Webpush::InvalidSubscription, Webpush::ExpiredSubscription
|
||||
# Subscription expiration is not currently implemented in any browser
|
||||
|
||||
session_activation.web_push_subscription.destroy!
|
||||
session_activation.update!(web_push_subscription: nil)
|
||||
|
||||
true
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue