mirror of
https://github.com/discourse/discourse.git
synced 2024-11-23 06:49:14 +08:00
7b4e338c0e
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.
160 lines
4.8 KiB
Ruby
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
|