mirror of
https://github.com/discourse/discourse.git
synced 2024-11-30 08:24:05 +08:00
6ec8728ebf
This change is mainly a refactor of the desktop notifications service to improve readability and have standardised values for tracking state for current user in regards to the Notification API and Push API. Also improves readability when handling push notification jobs, especially in scenarios where the push_notification_time_window_mins site setting is set to 0, which will allow sending push notifications instantly.
188 lines
5.8 KiB
Ruby
188 lines
5.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] || title(payload),
|
|
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],
|
|
}
|
|
|
|
subscriptions(user).each { |subscription| send_notification(user, subscription, message) }
|
|
end
|
|
|
|
message
|
|
end
|
|
|
|
def self.title(payload)
|
|
translation_key =
|
|
case payload[:notification_type]
|
|
when Notification.types[:watching_category_or_tag]
|
|
# For watching_category_or_tag, the notification could be for either a new post or new topic.
|
|
# Instead of duplicating translations, we can rely on 'watching_first_post' for new topics,
|
|
# and 'posted' for new posts.
|
|
type = payload[:post_number] == 1 ? "watching_first_post" : "posted"
|
|
"discourse_push_notifications.popup.#{type}"
|
|
else
|
|
"discourse_push_notifications.popup.#{Notification.types[payload[:notification_type]]}"
|
|
end
|
|
|
|
# Payload modifier used to adjust arguments to the translation
|
|
payload =
|
|
DiscoursePluginRegistry.apply_modifier(:push_notification_pusher_title_payload, payload)
|
|
|
|
I18n.t(
|
|
translation_key,
|
|
site_title: SiteSetting.title,
|
|
topic: payload[:topic_title],
|
|
username: payload[:username],
|
|
group_name: payload[:group_name],
|
|
)
|
|
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 ||= 86_400 # 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
|
|
|
|
DiscourseEvent.trigger(:push_notification_sent, user, message)
|
|
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)
|
|
rescue OpenSSL::SSL::SSLError => e
|
|
handle_generic_error(subscription, e, user, endpoint, message)
|
|
end
|
|
end
|
|
|
|
private_class_method :send_notification
|
|
private_class_method :handle_generic_error
|
|
end
|