discourse/app/services/push_notification_pusher.rb
Sam 7b4e338c0e
FIX: fallback to default push notification icon if none exists (#16961)
Due to some changes we started notifying via push notifications on other
families of notifications. There are a total of about 30 or so possible
notification you could get, some can be pushed.

This fallback means that if for any reason we are unable to find an icon
for a push notification we just fallback to the Discourse logo.

Also go with a simple reply icon for watching first post.

Note, that in production `image_url` can return an exception if an image is
missing. This is not the case in test / development.
2022-06-01 12:00:05 +10:00

160 lines
4.8 KiB
Ruby

# frozen_string_literal: true
class PushNotificationPusher
TOKEN_VALID_FOR_SECONDS ||= 5 * 60
CONNECTION_TIMEOUT_SECONDS = 5
def self.push(user, payload)
message = nil
I18n.with_locale(user.effective_locale) do
notification_icon_name = Notification.types[payload[:notification_type]]
if !File.exist?(File.expand_path("../../app/assets/images/push-notifications/#{notification_icon_name}.png", __dir__))
notification_icon_name = "discourse"
end
notification_icon = ActionController::Base.helpers.image_url("push-notifications/#{notification_icon_name}.png")
message = {
title: payload[:translated_title] || I18n.t(
"discourse_push_notifications.popup.#{Notification.types[payload[:notification_type]]}",
site_title: SiteSetting.title,
topic: payload[:topic_title],
username: payload[:username]
),
body: payload[:excerpt],
badge: get_badge,
icon: notification_icon,
tag: payload[:tag] || "#{Discourse.current_hostname}-#{payload[:topic_id]}",
base_url: Discourse.base_url,
url: payload[:post_url],
hide_when_active: true
}
subscriptions(user).each do |subscription|
send_notification(user, subscription, message)
end
end
message
end
def self.subscriptions(user)
user.push_subscriptions
end
def self.clear_subscriptions(user)
user.push_subscriptions.clear
end
def self.subscribe(user, push_params, send_confirmation)
data = push_params.to_json
subscriptions = PushSubscription.where(user: user, data: data)
subscriptions_count = subscriptions.count
new_subscription = if subscriptions_count > 1
subscriptions.destroy_all
PushSubscription.create!(user: user, data: data)
elsif subscriptions_count == 0
PushSubscription.create!(user: user, data: data)
end
if send_confirmation == "true"
message = {
title: I18n.t("discourse_push_notifications.popup.confirm_title",
site_title: SiteSetting.title),
body: I18n.t("discourse_push_notifications.popup.confirm_body"),
icon: ActionController::Base.helpers.image_url("push-notifications/check.png"),
badge: get_badge,
tag: "#{Discourse.current_hostname}-subscription"
}
send_notification(user, new_subscription, message)
end
end
def self.unsubscribe(user, subscription)
PushSubscription.find_by(user: user, data: subscription.to_json)&.destroy!
end
def self.get_badge
if (url = SiteSetting.site_push_notifications_icon_url).present?
url
else
ActionController::Base.helpers.image_url("push-notifications/discourse.png")
end
end
MAX_ERRORS ||= 3
MIN_ERROR_DURATION ||= 86400 # 1 day
def self.handle_generic_error(subscription, error, user, endpoint, message)
subscription.error_count += 1
subscription.first_error_at ||= Time.zone.now
delta = Time.zone.now - subscription.first_error_at
if subscription.error_count >= MAX_ERRORS && delta > MIN_ERROR_DURATION
subscription.destroy!
else
subscription.save!
end
Discourse.warn_exception(
error,
message: "Failed to send push notification",
env: {
user_id: user.id,
endpoint: endpoint,
message: message.to_json
}
)
end
def self.send_notification(user, subscription, message)
parsed_data = subscription.parsed_data
endpoint = parsed_data["endpoint"]
p256dh = parsed_data.dig("keys", "p256dh")
auth = parsed_data.dig("keys", "auth")
if (endpoint.blank? || p256dh.blank? || auth.blank?)
subscription.destroy!
return
end
begin
Webpush.payload_send(
endpoint: endpoint,
message: message.to_json,
p256dh: p256dh,
auth: auth,
vapid: {
subject: Discourse.base_url,
public_key: SiteSetting.vapid_public_key,
private_key: SiteSetting.vapid_private_key,
expiration: TOKEN_VALID_FOR_SECONDS
},
open_timeout: CONNECTION_TIMEOUT_SECONDS,
read_timeout: CONNECTION_TIMEOUT_SECONDS,
ssl_timeout: CONNECTION_TIMEOUT_SECONDS
)
if subscription.first_error_at || subscription.error_count != 0
subscription.update_columns(error_count: 0, first_error_at: nil)
end
rescue Webpush::ExpiredSubscription
subscription.destroy!
rescue Webpush::ResponseError => e
if e.response.message == "MismatchSenderId"
subscription.destroy!
else
handle_generic_error(subscription, e, user, endpoint, message)
end
rescue Timeout::Error => e
handle_generic_error(subscription, e, user, endpoint, message)
end
end
private_class_method :send_notification
private_class_method :handle_generic_error
end