about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/javascript/mastodon/service_worker/web_push_notifications.js26
-rw-r--r--app/models/session_activation.rb1
-rw-r--r--app/models/web/push_subscription.rb158
-rw-r--r--app/serializers/web/notification_serializer.rb168
4 files changed, 201 insertions, 152 deletions
diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js
index acb85f626..f63cff335 100644
--- a/app/javascript/mastodon/service_worker/web_push_notifications.js
+++ b/app/javascript/mastodon/service_worker/web_push_notifications.js
@@ -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;
diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb
index 7eb16af8f..c1645223b 100644
--- a/app/models/session_activation.rb
+++ b/app/models/session_activation.rb
@@ -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
 
diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb
index e76f61278..79f782114 100644
--- a/app/models/web/push_subscription.rb
+++ b/app/models/web/push_subscription.rb
@@ -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
diff --git a/app/serializers/web/notification_serializer.rb b/app/serializers/web/notification_serializer.rb
new file mode 100644
index 000000000..0fe75f3a8
--- /dev/null
+++ b/app/serializers/web/notification_serializer.rb
@@ -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