Refactor Web::PushSubscription, remove welcome message (#4524)

* Refactor Web::PushSubscription, remove welcome message

* Add missing helper

* Use locale of the receiver on push notifications (#4519)

* Remove unused translations

* Fix dir on notifications
This commit is contained in:
Eugen Rochko 2017-09-01 13:35:23 +02:00 committed by GitHub
parent 504737e860
commit 74437c6bff
13 changed files with 205 additions and 252 deletions

View File

@ -31,8 +31,8 @@ const notify = options =>
const group = cloneNotification(notifications[0]); const group = cloneNotification(notifications[0]);
group.title = formatGroupTitle(group.data.message, group.data.count + 1); group.title = formatGroupTitle(group.data.message, group.data.count + 1);
group.body = `${options.title}\n${group.body}`; group.body = `${options.title}\n${group.body}`;
group.data = { ...group.data, count: group.data.count + 1 }; group.data = { ...group.data, count: group.data.count + 1 };
return self.registration.showNotification(group.title, group); return self.registration.showNotification(group.title, group);
} }
@ -43,18 +43,18 @@ const notify = options =>
const handlePush = (event) => { const handlePush = (event) => {
const options = event.data.json(); const options = event.data.json();
options.body = options.data.nsfw || options.data.content; options.body = options.data.nsfw || options.data.content;
options.image = options.image || undefined; // Null results in a network request (404) 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); options.timestamp = options.timestamp && new Date(options.timestamp);
const expandAction = options.data.actions.find(action => action.todo === 'expand'); const expandAction = options.data.actions.find(action => action.todo === 'expand');
if (expandAction) { if (expandAction) {
options.actions = [expandAction]; options.actions = [expandAction];
options.hiddenActions = options.data.actions.filter(action => action !== expandAction); options.hiddenActions = options.data.actions.filter(action => action !== expandAction);
options.data.hiddenImage = options.image; options.data.hiddenImage = options.image;
options.image = undefined; options.image = undefined;
} else { } else {
options.actions = options.data.actions; options.actions = options.data.actions;
} }
@ -75,8 +75,8 @@ const cloneNotification = (notification) => {
const expandNotification = (notification) => { const expandNotification = (notification) => {
const nextNotification = cloneNotification(notification); const nextNotification = cloneNotification(notification);
nextNotification.body = notification.data.content; nextNotification.body = notification.data.content;
nextNotification.image = notification.data.hiddenImage; nextNotification.image = notification.data.hiddenImage;
nextNotification.actions = notification.data.actions.filter(action => action.todo !== 'expand'); nextNotification.actions = notification.data.actions.filter(action => action.todo !== 'expand');
return self.registration.showNotification(nextNotification.title, nextNotification); return self.registration.showNotification(nextNotification.title, nextNotification);
@ -105,8 +105,7 @@ const openUrl = url =>
const webClients = clientList.filter(client => /\/web\//.test(client.url)); const webClients = clientList.filter(client => /\/web\//.test(client.url));
if (webClients.length !== 0) { if (webClients.length !== 0) {
const client = findBestClient(webClients); const client = findBestClient(webClients);
const { pathname } = new URL(url); const { pathname } = new URL(url);
if (pathname.startsWith('/web/')) { if (pathname.startsWith('/web/')) {
@ -126,8 +125,7 @@ const openUrl = url =>
}); });
const removeActionFromNotification = (notification, action) => { 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); const nextNotification = cloneNotification(notification);
nextNotification.actions = actions; nextNotification.actions = actions;

View File

@ -25,6 +25,7 @@
# #
class SessionActivation < ApplicationRecord class SessionActivation < ApplicationRecord
belongs_to :user, inverse_of: :session_activations, required: true
belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', dependent: :destroy belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', dependent: :destroy
belongs_to :web_push_subscription, class_name: 'Web::PushSubscription', dependent: :destroy belongs_to :web_push_subscription, class_name: 'Web::PushSubscription', dependent: :destroy

View File

@ -13,59 +13,14 @@
# #
require 'webpush' require 'webpush'
require_relative '../../models/setting'
class Web::PushSubscription < ApplicationRecord class Web::PushSubscription < ApplicationRecord
include RoutingHelper
include StreamEntriesHelper
include ActionView::Helpers::TranslationHelper
include ActionView::Helpers::SanitizeHelper
has_one :session_activation has_one :session_activation
before_create :send_welcome_notification
def push(notification) def push(notification)
name = display_name notification.from_account I18n.with_locale(session_activation.user.locale || I18n.default_locale) do
title = title_str(name, notification) push_payload(message_from(notification), 48.hours.seconds)
body = body_str notification end
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
)
end end
def pushable?(notification) def pushable?(notification)
@ -73,120 +28,47 @@ class Web::PushSubscription < ApplicationRecord
end end
def as_payload def as_payload
payload = { payload = { id: id, endpoint: endpoint }
id: id,
endpoint: endpoint,
}
payload[:alerts] = data['alerts'] if data && data.key?('alerts') payload[:alerts] = data['alerts'] if data && data.key?('alerts')
payload payload
end end
def access_token
find_or_create_access_token.token
end
private private
def title_str(name, notification) def push_payload(message, ttl = 5.minutes.seconds)
case notification.type # TODO: Make sure that the payload does not
when :mention then translate('push_notifications.mention.title', name: name) # exceed 4KB - Webpush::PayloadTooLarge
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
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
def dir_str(body)
rtl?(body) ? 'rtl' : 'ltr'
end
def send_welcome_notification
Webpush.payload_send( Webpush.payload_send(
message: JSON.generate( message: Oj.dump(message),
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
}
),
endpoint: endpoint, endpoint: endpoint,
p256dh: key_p256dh, p256dh: key_p256dh,
auth: key_auth, auth: key_auth,
ttl: ttl,
vapid: { vapid: {
subject: "mailto:#{Setting.site_contact_email}", subject: "mailto:#{Setting.site_contact_email}",
private_key: Rails.configuration.x.vapid_private_key, private_key: Rails.configuration.x.vapid_private_key,
public_key: Rails.configuration.x.vapid_public_key, public_key: Rails.configuration.x.vapid_public_key,
}, }
ttl: 5 * 60 # 5 minutes
) )
end 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::AccessToken.find_or_create_for(
Doorkeeper::Application.find_by(superapp: true), Doorkeeper::Application.find_by(superapp: true),
notification.account.user.id, session_activation.user_id,
Doorkeeper::OAuth::Scopes.from_string('read write follow'), Doorkeeper::OAuth::Scopes.from_string('read write follow'),
Doorkeeper.configuration.access_token_expires_in, Doorkeeper.configuration.access_token_expires_in,
Doorkeeper.configuration.refresh_token_enabled? Doorkeeper.configuration.refresh_token_enabled?
) )
end end
def decoder
@decoder ||= HTMLEntities.new
end
end end

View File

@ -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

File diff suppressed because one or more lines are too long

View File

@ -382,9 +382,6 @@ en:
title: "%{name} mentioned you" title: "%{name} mentioned you"
reblog: reblog:
title: "%{name} boosted your status" title: "%{name} boosted your status"
subscribed:
body: You can now receive push notifications.
title: Subscription registered!
remote_follow: remote_follow:
acct: Enter your username@domain you want to follow from acct: Enter your username@domain you want to follow from
missing_resource: Could not find the required redirect URL for your account missing_resource: Could not find the required redirect URL for your account

View File

@ -357,9 +357,6 @@ fa:
title: "%{name} از شما نام برد" title: "%{name} از شما نام برد"
reblog: reblog:
title: "%{name} نوشتهٔ شما را بازبوقید" title: "%{name} نوشتهٔ شما را بازبوقید"
subscribed:
body: از این به بعد سرور می‌تواندبه شما اعلان‌های تازه بفرستد .
title: عضویت ثبت شد!
remote_follow: remote_follow:
acct: نشانی حساب username@domain خود را این‌جا بنویسید acct: نشانی حساب username@domain خود را این‌جا بنویسید
missing_resource: نشانی اینترنتی برای رسیدن به حساب شما پیدا نشد missing_resource: نشانی اینترنتی برای رسیدن به حساب شما پیدا نشد

View File

@ -353,9 +353,6 @@ fr:
title: "%{name} vous a mentionné·e" title: "%{name} vous a mentionné·e"
reblog: reblog:
title: "%{name} a partagé⋅e votre statut" title: "%{name} a partagé⋅e votre statut"
subscribed:
body: Vous pouvez désormais recevoir des notifications push.
title: Abonnements aux notifications push
remote_follow: remote_follow:
acct: Entrez votre pseudo@instance depuis lequel vous voulez suivre ce⋅tte utilisateur⋅rice acct: Entrez votre pseudo@instance depuis lequel vous voulez suivre ce⋅tte utilisateur⋅rice
missing_resource: LURL de redirection na pas pu être trouvée missing_resource: LURL de redirection na pas pu être trouvée

View File

@ -381,9 +381,6 @@ ja:
title: "%{name} さんから返信がありました" title: "%{name} さんから返信がありました"
reblog: reblog:
title: あなたのトゥートが %{name} さんにブーストされました title: あなたのトゥートが %{name} さんにブーストされました
subscribed:
body: あなたはプッシュ通知を受け取ることが出来ます
title: Subscription が登録されました
remote_follow: remote_follow:
acct: あなたの ユーザー名@ドメイン を入力してください acct: あなたの ユーザー名@ドメイン を入力してください
missing_resource: リダイレクト先が見つかりませんでした missing_resource: リダイレクト先が見つかりませんでした

View File

@ -353,9 +353,6 @@ nl:
title: "%{name} vermeldde jou" title: "%{name} vermeldde jou"
reblog: reblog:
title: "%{name} boostte jouw toot" title: "%{name} boostte jouw toot"
subscribed:
body: Je kan nu pushmeldingen ontvangen.
title: Aanmelding bevestigd!
remote_follow: remote_follow:
acct: Geef jouw account@domein.tld op waarvandaan je wilt volgen acct: Geef jouw account@domein.tld op waarvandaan je wilt volgen
missing_resource: Kon vereiste doorverwijzings-URL voor jouw account niet vinden missing_resource: Kon vereiste doorverwijzings-URL voor jouw account niet vinden

View File

@ -448,9 +448,6 @@ oc:
title: "%{name} vos a mencionat" title: "%{name} vos a mencionat"
reblog: reblog:
title: "%{name} a partejat vòstre estatut" title: "%{name} a partejat vòstre estatut"
subscribed:
body: Podètz ara recebre las notificacions push.
title: Abonament enregistrat!
remote_follow: remote_follow:
acct: Picatz vòstre utilizaire@instància que cal utilizar per sègre aqueste utilizaire acct: Picatz vòstre utilizaire@instància que cal utilizar per sègre aqueste utilizaire
missing_resource: URL de redireccion pas trobada missing_resource: URL de redireccion pas trobada

View File

@ -224,8 +224,6 @@ pl:
settings: 'Zmień ustawienia powiadamiania: %{link}' settings: 'Zmień ustawienia powiadamiania: %{link}'
signature: Powiadomienie Mastodona z instancji %{instance} signature: Powiadomienie Mastodona z instancji %{instance}
view: 'Zobacz:' view: 'Zobacz:'
applications:
invalid_url: Ten URL jest nieprawidłowy
applications: applications:
created: Pomyślnie utworzono aplikację created: Pomyślnie utworzono aplikację
destroyed: Pomyślnie usunięto aplikację destroyed: Pomyślnie usunięto aplikację
@ -375,9 +373,6 @@ pl:
title: "%{name} wspomniał o Tobie" title: "%{name} wspomniał o Tobie"
reblog: reblog:
title: "%{name} podbił Twój status" title: "%{name} podbił Twój status"
subscribed:
body: Otrzymujesz teraz powiadomienia push.
title: Zarejestrowano subskrypcję!
remote_follow: remote_follow:
acct: Podaj swój adres (nazwa@domena), z którego chcesz śledzić acct: Podaj swój adres (nazwa@domena), z którego chcesz śledzić
missing_resource: Nie udało się znaleźć adresu przekierowania z Twojej domeny missing_resource: Nie udało się znaleźć adresu przekierowania z Twojej domeny

View File

@ -278,9 +278,6 @@ ru:
title: Вас упомянул(а) %{name} title: Вас упомянул(а) %{name}
reblog: reblog:
title: "%{name} продвинул(а) Ваш статус" title: "%{name} продвинул(а) Ваш статус"
subscribed:
body: Теперь Вы можете получать push-уведомления.
title: Подписка зарегистрирована!
remote_follow: remote_follow:
acct: Введите username@domain, откуда Вы хотите подписаться acct: Введите username@domain, откуда Вы хотите подписаться
missing_resource: Поиск требуемого перенаправления URL для Вашего аккаунта завершился неудачей missing_resource: Поиск требуемого перенаправления URL для Вашего аккаунта завершился неудачей