* Refactor Web::PushSubscription, remove welcome message * Add missing helper * Use locale of the receiver on push notifications (#4519) * Remove unused translations * Fix dir on notificationsmaster
@@ -31,8 +31,8 @@ const notify = options => | |||
const group = cloneNotification(notifications[0]); | |||
group.title = formatGroupTitle(group.data.message, group.data.count + 1); | |||
group.body = `${options.title}\n${group.body}`; | |||
group.data = { ...group.data, count: group.data.count + 1 }; | |||
group.body = `${options.title}\n${group.body}`; | |||
group.data = { ...group.data, count: group.data.count + 1 }; | |||
return self.registration.showNotification(group.title, group); | |||
} | |||
@@ -43,18 +43,18 @@ const notify = options => | |||
const handlePush = (event) => { | |||
const options = event.data.json(); | |||
options.body = options.data.nsfw || options.data.content; | |||
options.image = options.image || undefined; // Null results in a network request (404) | |||
options.body = options.data.nsfw || options.data.content; | |||
options.dir = options.data.dir; | |||
options.image = options.image || undefined; // Null results in a network request (404) | |||
options.timestamp = options.timestamp && new Date(options.timestamp); | |||
const expandAction = options.data.actions.find(action => action.todo === 'expand'); | |||
if (expandAction) { | |||
options.actions = [expandAction]; | |||
options.hiddenActions = options.data.actions.filter(action => action !== expandAction); | |||
options.actions = [expandAction]; | |||
options.hiddenActions = options.data.actions.filter(action => action !== expandAction); | |||
options.data.hiddenImage = options.image; | |||
options.image = undefined; | |||
options.image = undefined; | |||
} else { | |||
options.actions = options.data.actions; | |||
} | |||
@@ -75,8 +75,8 @@ const cloneNotification = (notification) => { | |||
const expandNotification = (notification) => { | |||
const nextNotification = cloneNotification(notification); | |||
nextNotification.body = notification.data.content; | |||
nextNotification.image = notification.data.hiddenImage; | |||
nextNotification.body = notification.data.content; | |||
nextNotification.image = notification.data.hiddenImage; | |||
nextNotification.actions = notification.data.actions.filter(action => action.todo !== 'expand'); | |||
return self.registration.showNotification(nextNotification.title, nextNotification); | |||
@@ -105,8 +105,7 @@ const openUrl = url => | |||
const webClients = clientList.filter(client => /\/web\//.test(client.url)); | |||
if (webClients.length !== 0) { | |||
const client = findBestClient(webClients); | |||
const client = findBestClient(webClients); | |||
const { pathname } = new URL(url); | |||
if (pathname.startsWith('/web/')) { | |||
@@ -126,8 +125,7 @@ const openUrl = url => | |||
}); | |||
const removeActionFromNotification = (notification, action) => { | |||
const actions = notification.actions.filter(act => act.action !== action.action); | |||
const actions = notification.actions.filter(act => act.action !== action.action); | |||
const nextNotification = cloneNotification(notification); | |||
nextNotification.actions = actions; | |||
@@ -25,6 +25,7 @@ | |||
# | |||
class SessionActivation < ApplicationRecord | |||
belongs_to :user, inverse_of: :session_activations, required: true | |||
belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', dependent: :destroy | |||
belongs_to :web_push_subscription, class_name: 'Web::PushSubscription', dependent: :destroy | |||
@@ -13,59 +13,14 @@ | |||
# | |||
require 'webpush' | |||
require_relative '../../models/setting' | |||
class Web::PushSubscription < ApplicationRecord | |||
include RoutingHelper | |||
include StreamEntriesHelper | |||
include ActionView::Helpers::TranslationHelper | |||
include ActionView::Helpers::SanitizeHelper | |||
has_one :session_activation | |||
before_create :send_welcome_notification | |||
def push(notification) | |||
name = display_name notification.from_account | |||
title = title_str(name, notification) | |||
body = body_str notification | |||
dir = dir_str body | |||
url = url_str notification | |||
image = image_str notification | |||
actions = actions_arr notification | |||
access_token = actions.empty? ? nil : find_or_create_access_token(notification).token | |||
nsfw = notification.target_status.nil? || notification.target_status.spoiler_text.empty? ? nil : notification.target_status.spoiler_text | |||
# TODO: Make sure that the payload does not exceed 4KB - Webpush::PayloadTooLarge | |||
Webpush.payload_send( | |||
message: JSON.generate( | |||
title: title, | |||
dir: dir, | |||
image: image, | |||
badge: full_asset_url('badge.png', skip_pipeline: true), | |||
tag: notification.id, | |||
timestamp: notification.created_at, | |||
icon: notification.from_account.avatar_static_url, | |||
data: { | |||
content: decoder.decode(strip_tags(body)), | |||
nsfw: nsfw.nil? ? nil : decoder.decode(strip_tags(nsfw)), | |||
url: url, | |||
actions: actions, | |||
access_token: access_token, | |||
message: translate('push_notifications.group.title'), # Do not pass count, will be formatted in the ServiceWorker | |||
} | |||
), | |||
endpoint: endpoint, | |||
p256dh: key_p256dh, | |||
auth: key_auth, | |||
vapid: { | |||
subject: "mailto:#{Setting.site_contact_email}", | |||
private_key: Rails.configuration.x.vapid_private_key, | |||
public_key: Rails.configuration.x.vapid_public_key, | |||
}, | |||
ttl: 40 * 60 * 60 # 48 hours | |||
) | |||
I18n.with_locale(session_activation.user.locale || I18n.default_locale) do | |||
push_payload(message_from(notification), 48.hours.seconds) | |||
end | |||
end | |||
def pushable?(notification) | |||
@@ -73,120 +28,47 @@ class Web::PushSubscription < ApplicationRecord | |||
end | |||
def as_payload | |||
payload = { | |||
id: id, | |||
endpoint: endpoint, | |||
} | |||
payload = { id: id, endpoint: endpoint } | |||
payload[:alerts] = data['alerts'] if data && data.key?('alerts') | |||
payload | |||
end | |||
private | |||
def title_str(name, notification) | |||
case notification.type | |||
when :mention then translate('push_notifications.mention.title', name: name) | |||
when :follow then translate('push_notifications.follow.title', name: name) | |||
when :favourite then translate('push_notifications.favourite.title', name: name) | |||
when :reblog then translate('push_notifications.reblog.title', name: name) | |||
end | |||
end | |||
def body_str(notification) | |||
case notification.type | |||
when :mention then notification.target_status.text | |||
when :follow then notification.from_account.note | |||
when :favourite then notification.target_status.text | |||
when :reblog then notification.target_status.text | |||
end | |||
end | |||
def url_str(notification) | |||
case notification.type | |||
when :mention then web_url("statuses/#{notification.target_status.id}") | |||
when :follow then web_url("accounts/#{notification.from_account.id}") | |||
when :favourite then web_url("statuses/#{notification.target_status.id}") | |||
when :reblog then web_url("statuses/#{notification.target_status.id}") | |||
end | |||
def access_token | |||
find_or_create_access_token.token | |||
end | |||
def actions_arr(notification) | |||
actions = | |||
case notification.type | |||
when :mention then [ | |||
{ | |||
title: translate('push_notifications.mention.action_favourite'), | |||
icon: full_asset_url('web-push-icon_favourite.png', skip_pipeline: true), | |||
todo: 'request', | |||
method: 'POST', | |||
action: "/api/v1/statuses/#{notification.target_status.id}/favourite", | |||
}, | |||
] | |||
else [] | |||
end | |||
should_hide = notification.type.equal?(:mention) && !notification.target_status.nil? && (notification.target_status.sensitive || !notification.target_status.spoiler_text.empty?) | |||
can_boost = notification.type.equal?(:mention) && !notification.target_status.nil? && !notification.target_status.hidden? | |||
if should_hide | |||
actions.insert(0, title: translate('push_notifications.mention.action_expand'), icon: full_asset_url('web-push-icon_expand.png', skip_pipeline: true), todo: 'expand', action: 'expand') | |||
end | |||
if can_boost | |||
actions << { title: translate('push_notifications.mention.action_boost'), icon: full_asset_url('web-push-icon_reblog.png', skip_pipeline: true), todo: 'request', method: 'POST', action: "/api/v1/statuses/#{notification.target_status.id}/reblog" } | |||
end | |||
actions | |||
end | |||
def image_str(notification) | |||
return nil if notification.target_status.nil? || notification.target_status.media_attachments.empty? | |||
full_asset_url(notification.target_status.media_attachments.first.file.url(:small)) | |||
end | |||
private | |||
def dir_str(body) | |||
rtl?(body) ? 'rtl' : 'ltr' | |||
end | |||
def push_payload(message, ttl = 5.minutes.seconds) | |||
# TODO: Make sure that the payload does not | |||
# exceed 4KB - Webpush::PayloadTooLarge | |||
def send_welcome_notification | |||
Webpush.payload_send( | |||
message: JSON.generate( | |||
title: translate('push_notifications.subscribed.title'), | |||
icon: full_asset_url('android-chrome-192x192.png', skip_pipeline: true), | |||
badge: full_asset_url('badge.png', skip_pipeline: true), | |||
data: { | |||
content: translate('push_notifications.subscribed.body'), | |||
actions: [], | |||
url: web_url('notifications'), | |||
message: translate('push_notifications.group.title'), # Do not pass count, will be formatted in the ServiceWorker | |||
} | |||
), | |||
message: Oj.dump(message), | |||
endpoint: endpoint, | |||
p256dh: key_p256dh, | |||
auth: key_auth, | |||
ttl: ttl, | |||
vapid: { | |||
subject: "mailto:#{Setting.site_contact_email}", | |||
private_key: Rails.configuration.x.vapid_private_key, | |||
public_key: Rails.configuration.x.vapid_public_key, | |||
}, | |||
ttl: 5 * 60 # 5 minutes | |||
} | |||
) | |||
end | |||
def find_or_create_access_token(notification) | |||
def message_from(notification) | |||
serializable_resource = ActiveModelSerializers::SerializableResource.new(notification, serializer: Web::NotificationSerializer, scope: self, scope_name: :current_push_subscription) | |||
serializable_resource.as_json | |||
end | |||
def find_or_create_access_token | |||
Doorkeeper::AccessToken.find_or_create_for( | |||
Doorkeeper::Application.find_by(superapp: true), | |||
notification.account.user.id, | |||
session_activation.user_id, | |||
Doorkeeper::OAuth::Scopes.from_string('read write follow'), | |||
Doorkeeper.configuration.access_token_expires_in, | |||
Doorkeeper.configuration.refresh_token_enabled? | |||
) | |||
end | |||
def decoder | |||
@decoder ||= HTMLEntities.new | |||
end | |||
end |
@@ -0,0 +1,168 @@ | |||
# frozen_string_literal: true | |||
class Web::NotificationSerializer < ActiveModel::Serializer | |||
include StreamEntriesHelper | |||
class DataSerializer < ActiveModel::Serializer | |||
include RoutingHelper | |||
include StreamEntriesHelper | |||
include ActionView::Helpers::SanitizeHelper | |||
attributes :content, :nsfw, :url, :actions, | |||
:access_token, :message | |||
def content | |||
decoder.decode(strip_tags(body)) | |||
end | |||
def dir | |||
rtl?(body) ? 'rtl' : 'ltr' | |||
end | |||
def nsfw | |||
return if object.target_status.nil? | |||
object.target_status.spoiler_text.presence | |||
end | |||
def url | |||
case object.type | |||
when :mention | |||
web_url("statuses/#{object.target_status.id}") | |||
when :follow | |||
web_url("accounts/#{object.from_account.id}") | |||
when :favourite | |||
web_url("statuses/#{object.target_status.id}") | |||
when :reblog | |||
web_url("statuses/#{object.target_status.id}") | |||
end | |||
end | |||
def actions | |||
return @actions if defined?(@actions) | |||
@actions = [] | |||
if object.type == :mention | |||
@actions << expand_action if collapsed? | |||
@actions << favourite_action | |||
@actions << reblog_action if rebloggable? | |||
end | |||
@actions | |||
end | |||
def access_token | |||
return if actions.empty? | |||
current_push_subscription.access_token | |||
end | |||
def message | |||
I18n.t('push_notifications.group.title') | |||
end | |||
private | |||
def body | |||
case object.type | |||
when :mention | |||
object.target_status.text | |||
when :follow | |||
object.from_account.note | |||
when :favourite | |||
object.target_status.text | |||
when :reblog | |||
object.target_status.text | |||
end | |||
end | |||
def decoder | |||
@decoder ||= HTMLEntities.new | |||
end | |||
def expand_action | |||
{ | |||
title: I18n.t('push_notifications.mention.action_expand'), | |||
icon: full_asset_url('web-push-icon_expand.png', skip_pipeline: true), | |||
todo: 'expand', | |||
action: 'expand', | |||
} | |||
end | |||
def favourite_action | |||
{ | |||
title: I18n.t('push_notifications.mention.action_favourite'), | |||
icon: full_asset_url('web-push-icon_favourite.png', skip_pipeline: true), | |||
todo: 'request', | |||
method: 'POST', | |||
action: "/api/v1/statuses/#{object.target_status.id}/favourite", | |||
} | |||
end | |||
def reblog_action | |||
{ | |||
title: I18n.t('push_notifications.mention.action_boost'), | |||
icon: full_asset_url('web-push-icon_reblog.png', skip_pipeline: true), | |||
todo: 'request', | |||
method: 'POST', | |||
action: "/api/v1/statuses/#{object.target_status.id}/reblog", | |||
} | |||
end | |||
def collapsed? | |||
!object.target_status.nil? && (object.target_status.sensitive? || object.target_status.spoiler_text.present?) | |||
end | |||
def rebloggable? | |||
!object.target_status.nil? && !object.target_status.hidden? | |||
end | |||
end | |||
attributes :title, :dir, :image, :badge, :tag, | |||
:timestamp, :icon | |||
has_one :data | |||
def title | |||
case object.type | |||
when :mention | |||
I18n.t('push_notifications.mention.title', name: name) | |||
when :follow | |||
I18n.t('push_notifications.follow.title', name: name) | |||
when :favourite | |||
I18n.t('push_notifications.favourite.title', name: name) | |||
when :reblog | |||
I18n.t('push_notifications.reblog.title', name: name) | |||
end | |||
end | |||
def image | |||
return if object.target_status.nil? || object.target_status.media_attachments.empty? | |||
full_asset_url(object.target_status.media_attachments.first.file.url(:small)) | |||
end | |||
def badge | |||
full_asset_url('badge.png', skip_pipeline: true) | |||
end | |||
def tag | |||
object.id | |||
end | |||
def timestamp | |||
object.created_at | |||
end | |||
def icon | |||
object.from_account.avatar_static_url | |||
end | |||
def data | |||
object | |||
end | |||
private | |||
def name | |||
display_name(object.from_account) | |||
end | |||
end |
@@ -382,9 +382,6 @@ en: | |||
title: "%{name} mentioned you" | |||
reblog: | |||
title: "%{name} boosted your status" | |||
subscribed: | |||
body: You can now receive push notifications. | |||
title: Subscription registered! | |||
remote_follow: | |||
acct: Enter your username@domain you want to follow from | |||
missing_resource: Could not find the required redirect URL for your account | |||
@@ -357,9 +357,6 @@ fa: | |||
title: "%{name} از شما نام برد" | |||
reblog: | |||
title: "%{name} نوشتهٔ شما را بازبوقید" | |||
subscribed: | |||
body: از این به بعد سرور میتواندبه شما اعلانهای تازه بفرستد . | |||
title: عضویت ثبت شد! | |||
remote_follow: | |||
acct: نشانی حساب username@domain خود را اینجا بنویسید | |||
missing_resource: نشانی اینترنتی برای رسیدن به حساب شما پیدا نشد | |||
@@ -353,9 +353,6 @@ fr: | |||
title: "%{name} vous a mentionné·e" | |||
reblog: | |||
title: "%{name} a partagé⋅e votre statut" | |||
subscribed: | |||
body: Vous pouvez désormais recevoir des notifications push. | |||
title: Abonnements aux notifications push | |||
remote_follow: | |||
acct: Entrez votre pseudo@instance depuis lequel vous voulez suivre ce⋅tte utilisateur⋅rice | |||
missing_resource: L’URL de redirection n’a pas pu être trouvée | |||
@@ -381,9 +381,6 @@ ja: | |||
title: "%{name} さんから返信がありました" | |||
reblog: | |||
title: あなたのトゥートが %{name} さんにブーストされました | |||
subscribed: | |||
body: あなたはプッシュ通知を受け取ることが出来ます | |||
title: Subscription が登録されました | |||
remote_follow: | |||
acct: あなたの ユーザー名@ドメイン を入力してください | |||
missing_resource: リダイレクト先が見つかりませんでした | |||
@@ -353,9 +353,6 @@ nl: | |||
title: "%{name} vermeldde jou" | |||
reblog: | |||
title: "%{name} boostte jouw toot" | |||
subscribed: | |||
body: Je kan nu pushmeldingen ontvangen. | |||
title: Aanmelding bevestigd! | |||
remote_follow: | |||
acct: Geef jouw account@domein.tld op waarvandaan je wilt volgen | |||
missing_resource: Kon vereiste doorverwijzings-URL voor jouw account niet vinden | |||
@@ -448,9 +448,6 @@ oc: | |||
title: "%{name} vos a mencionat" | |||
reblog: | |||
title: "%{name} a partejat vòstre estatut" | |||
subscribed: | |||
body: Podètz ara recebre las notificacions push. | |||
title: Abonament enregistrat ! | |||
remote_follow: | |||
acct: Picatz vòstre utilizaire@instància que cal utilizar per sègre aqueste utilizaire | |||
missing_resource: URL de redireccion pas trobada | |||
@@ -225,8 +225,6 @@ pl: | |||
signature: Powiadomienie Mastodona z instancji %{instance} | |||
view: 'Zobacz:' | |||
applications: | |||
invalid_url: Ten URL jest nieprawidłowy | |||
applications: | |||
created: Pomyślnie utworzono aplikację | |||
destroyed: Pomyślnie usunięto aplikację | |||
invalid_url: Wprowadzony adres URL jest nieprawidłowy | |||
@@ -375,9 +373,6 @@ pl: | |||
title: "%{name} wspomniał o Tobie" | |||
reblog: | |||
title: "%{name} podbił Twój status" | |||
subscribed: | |||
body: Otrzymujesz teraz powiadomienia push. | |||
title: Zarejestrowano subskrypcję! | |||
remote_follow: | |||
acct: Podaj swój adres (nazwa@domena), z którego chcesz śledzić | |||
missing_resource: Nie udało się znaleźć adresu przekierowania z Twojej domeny | |||
@@ -278,9 +278,6 @@ ru: | |||
title: Вас упомянул(а) %{name} | |||
reblog: | |||
title: "%{name} продвинул(а) Ваш статус" | |||
subscribed: | |||
body: Теперь Вы можете получать push-уведомления. | |||
title: Подписка зарегистрирована! | |||
remote_follow: | |||
acct: Введите username@domain, откуда Вы хотите подписаться | |||
missing_resource: Поиск требуемого перенаправления URL для Вашего аккаунта завершился неудачей | |||