Merge upstream (#81)
This commit is contained in:
		@@ -47,6 +47,7 @@ class Account < ApplicationRecord
 | 
			
		||||
  include AccountInteractions
 | 
			
		||||
  include Attachmentable
 | 
			
		||||
  include Remotable
 | 
			
		||||
  include EmojiHelper
 | 
			
		||||
 | 
			
		||||
  # Local users
 | 
			
		||||
  has_one :user, inverse_of: :account
 | 
			
		||||
@@ -129,7 +130,7 @@ class Account < ApplicationRecord
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def subscription(webhook_url)
 | 
			
		||||
    OStatus2::Subscription.new(remote_url, secret: secret, lease_seconds: 86_400 * 30, webhook: webhook_url, hub: hub_url)
 | 
			
		||||
    OStatus2::Subscription.new(remote_url, secret: secret, lease_seconds: 30.days.seconds, webhook: webhook_url, hub: hub_url)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def save_with_optional_media!
 | 
			
		||||
@@ -240,9 +241,18 @@ class Account < ApplicationRecord
 | 
			
		||||
 | 
			
		||||
  before_create :generate_keys
 | 
			
		||||
  before_validation :normalize_domain
 | 
			
		||||
  before_validation :prepare_contents, if: :local?
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def prepare_contents
 | 
			
		||||
    display_name&.strip!
 | 
			
		||||
    note&.strip!
 | 
			
		||||
 | 
			
		||||
    self.display_name = emojify(display_name)
 | 
			
		||||
    self.note         = emojify(note)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def generate_keys
 | 
			
		||||
    return unless local?
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
module Remotable
 | 
			
		||||
  include HttpHelper
 | 
			
		||||
  extend ActiveSupport::Concern
 | 
			
		||||
 | 
			
		||||
  included do
 | 
			
		||||
@@ -20,7 +19,7 @@ module Remotable
 | 
			
		||||
        return if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty? || self[attribute_name] == url
 | 
			
		||||
 | 
			
		||||
        begin
 | 
			
		||||
          response = http_client.get(url)
 | 
			
		||||
          response = Request.new(:get, url).perform
 | 
			
		||||
 | 
			
		||||
          return if response.code != 200
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
#  created_at   :datetime         not null
 | 
			
		||||
#  updated_at   :datetime         not null
 | 
			
		||||
#  severity     :integer          default("silence")
 | 
			
		||||
#  reject_media :boolean
 | 
			
		||||
#  reject_media :boolean          default(FALSE), not null
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
class DomainBlock < ApplicationRecord
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
#  id                :integer          not null, primary key
 | 
			
		||||
#  account_id        :integer          not null
 | 
			
		||||
#  type              :integer          not null
 | 
			
		||||
#  approved          :boolean
 | 
			
		||||
#  approved          :boolean          default(FALSE), not null
 | 
			
		||||
#  created_at        :datetime         not null
 | 
			
		||||
#  updated_at        :datetime         not null
 | 
			
		||||
#  data_file_name    :string
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,17 @@
 | 
			
		||||
#
 | 
			
		||||
# Table name: session_activations
 | 
			
		||||
#
 | 
			
		||||
#  id                       :integer          not null, primary key
 | 
			
		||||
#  user_id                  :integer          not null
 | 
			
		||||
#  session_id               :string           not null
 | 
			
		||||
#  created_at               :datetime         not null
 | 
			
		||||
#  updated_at               :datetime         not null
 | 
			
		||||
#  user_agent               :string           default(""), not null
 | 
			
		||||
#  ip                       :inet
 | 
			
		||||
#  access_token_id          :integer
 | 
			
		||||
#  web_push_subscription_id :integer
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
#  id              :integer          not null, primary key
 | 
			
		||||
#  user_id         :integer          not null
 | 
			
		||||
#  session_id      :string           not null
 | 
			
		||||
@@ -15,6 +26,7 @@
 | 
			
		||||
 | 
			
		||||
class SessionActivation < ApplicationRecord
 | 
			
		||||
  belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', dependent: :destroy
 | 
			
		||||
  belongs_to :web_push_subscription, class_name: 'Web::PushSubscription', dependent: :destroy
 | 
			
		||||
 | 
			
		||||
  delegate :token,
 | 
			
		||||
           to: :access_token,
 | 
			
		||||
 
 | 
			
		||||
@@ -12,12 +12,12 @@
 | 
			
		||||
#  in_reply_to_id         :integer
 | 
			
		||||
#  reblog_of_id           :integer
 | 
			
		||||
#  url                    :string
 | 
			
		||||
#  sensitive              :boolean          default(FALSE)
 | 
			
		||||
#  sensitive              :boolean          default(FALSE), not null
 | 
			
		||||
#  visibility             :integer          default("public"), not null
 | 
			
		||||
#  in_reply_to_account_id :integer
 | 
			
		||||
#  application_id         :integer
 | 
			
		||||
#  spoiler_text           :text             default(""), not null
 | 
			
		||||
#  reply                  :boolean          default(FALSE)
 | 
			
		||||
#  reply                  :boolean          default(FALSE), not null
 | 
			
		||||
#  favourites_count       :integer          default(0), not null
 | 
			
		||||
#  reblogs_count          :integer          default(0), not null
 | 
			
		||||
#  language               :string
 | 
			
		||||
@@ -29,6 +29,7 @@ class Status < ApplicationRecord
 | 
			
		||||
  include Streamable
 | 
			
		||||
  include Cacheable
 | 
			
		||||
  include StatusThreadingConcern
 | 
			
		||||
  include EmojiHelper
 | 
			
		||||
 | 
			
		||||
  enum visibility: [:public, :unlisted, :private, :direct], _suffix: :visibility
 | 
			
		||||
 | 
			
		||||
@@ -120,10 +121,11 @@ class Status < ApplicationRecord
 | 
			
		||||
    !sensitive? && media_attachments.any?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  before_validation :prepare_contents
 | 
			
		||||
  before_validation :prepare_contents, if: :local?
 | 
			
		||||
  before_validation :set_reblog
 | 
			
		||||
  before_validation :set_visibility
 | 
			
		||||
  before_validation :set_conversation
 | 
			
		||||
  before_validation :set_sensitivity
 | 
			
		||||
 | 
			
		||||
  class << self
 | 
			
		||||
    def not_in_filtered_languages(account)
 | 
			
		||||
@@ -240,6 +242,9 @@ class Status < ApplicationRecord
 | 
			
		||||
  def prepare_contents
 | 
			
		||||
    text&.strip!
 | 
			
		||||
    spoiler_text&.strip!
 | 
			
		||||
 | 
			
		||||
    self.text         = emojify(text)
 | 
			
		||||
    self.spoiler_text = emojify(spoiler_text)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def set_reblog
 | 
			
		||||
@@ -248,6 +253,11 @@ class Status < ApplicationRecord
 | 
			
		||||
 | 
			
		||||
  def set_visibility
 | 
			
		||||
    self.visibility = (account.locked? ? :private : :public) if visibility.nil?
 | 
			
		||||
    self.sensitive  = false if sensitive.nil?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def set_sensitivity
 | 
			
		||||
    self.sensitive = sensitive || spoiler_text.present?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def set_conversation
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
# == Schema Information
 | 
			
		||||
#
 | 
			
		||||
# Table name: subscriptions
 | 
			
		||||
@@ -13,11 +12,12 @@
 | 
			
		||||
#  created_at                  :datetime         not null
 | 
			
		||||
#  updated_at                  :datetime         not null
 | 
			
		||||
#  last_successful_delivery_at :datetime
 | 
			
		||||
#  domain                      :string
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
class Subscription < ApplicationRecord
 | 
			
		||||
  MIN_EXPIRATION = 7.days.seconds.to_i
 | 
			
		||||
  MAX_EXPIRATION = 30.days.seconds.to_i
 | 
			
		||||
  MIN_EXPIRATION = 1.day.to_i
 | 
			
		||||
  MAX_EXPIRATION = 30.days.to_i
 | 
			
		||||
 | 
			
		||||
  belongs_to :account, required: true
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,9 +12,10 @@
 | 
			
		||||
class Tag < ApplicationRecord
 | 
			
		||||
  has_and_belongs_to_many :statuses
 | 
			
		||||
 | 
			
		||||
  HASHTAG_RE = /(?:^|[^\/\)\w])#([[:word:]_]*[[:alpha:]_][[:word:]_]*)/i
 | 
			
		||||
  HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_][[:word:]_]*'
 | 
			
		||||
  HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i
 | 
			
		||||
 | 
			
		||||
  validates :name, presence: true, uniqueness: true
 | 
			
		||||
  validates :name, presence: true, uniqueness: true, format: { with: /\A#{HASHTAG_NAME_RE}\z/i }
 | 
			
		||||
 | 
			
		||||
  def to_param
 | 
			
		||||
    name
 | 
			
		||||
@@ -23,7 +24,7 @@ class Tag < ApplicationRecord
 | 
			
		||||
  class << self
 | 
			
		||||
    def search_for(term, limit = 5)
 | 
			
		||||
      pattern = sanitize_sql_like(term) + '%'
 | 
			
		||||
      Tag.where('name like ?', pattern).order(:name).limit(limit)
 | 
			
		||||
      Tag.where('lower(name) like lower(?)', pattern).order(:name).limit(limit)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@
 | 
			
		||||
#  last_sign_in_at           :datetime
 | 
			
		||||
#  current_sign_in_ip        :inet
 | 
			
		||||
#  last_sign_in_ip           :inet
 | 
			
		||||
#  admin                     :boolean          default(FALSE)
 | 
			
		||||
#  admin                     :boolean          default(FALSE), not null
 | 
			
		||||
#  confirmation_token        :string
 | 
			
		||||
#  confirmed_at              :datetime
 | 
			
		||||
#  confirmation_sent_at      :datetime
 | 
			
		||||
@@ -27,7 +27,7 @@
 | 
			
		||||
#  encrypted_otp_secret_iv   :string
 | 
			
		||||
#  encrypted_otp_secret_salt :string
 | 
			
		||||
#  consumed_timestep         :integer
 | 
			
		||||
#  otp_required_for_login    :boolean
 | 
			
		||||
#  otp_required_for_login    :boolean          default(FALSE), not null
 | 
			
		||||
#  last_emailed_at           :datetime
 | 
			
		||||
#  otp_backup_codes          :string           is an Array
 | 
			
		||||
#  filtered_languages        :string           default([]), not null, is an Array
 | 
			
		||||
@@ -99,6 +99,10 @@ class User < ApplicationRecord
 | 
			
		||||
    settings.system_font_ui
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def setting_noindex
 | 
			
		||||
    settings.noindex
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def activate_session(request)
 | 
			
		||||
    session_activations.activate(session_id: SecureRandom.hex,
 | 
			
		||||
                                 user_agent: request.user_agent,
 | 
			
		||||
@@ -113,6 +117,10 @@ class User < ApplicationRecord
 | 
			
		||||
    session_activations.active? id
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def web_push_subscription(session)
 | 
			
		||||
    session.web_push_subscription.nil? ? nil : session.web_push_subscription.as_payload
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  protected
 | 
			
		||||
 | 
			
		||||
  def send_devise_notification(notification, *args)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										190
									
								
								app/models/web/push_subscription.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								app/models/web/push_subscription.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,190 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
# == Schema Information
 | 
			
		||||
#
 | 
			
		||||
# Table name: web_push_subscriptions
 | 
			
		||||
#
 | 
			
		||||
#  id         :integer          not null, primary key
 | 
			
		||||
#  endpoint   :string           not null
 | 
			
		||||
#  key_p256dh :string           not null
 | 
			
		||||
#  key_auth   :string           not null
 | 
			
		||||
#  data       :json
 | 
			
		||||
#  created_at :datetime         not null
 | 
			
		||||
#  updated_at :datetime         not null
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
    return unless pushable? 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
 | 
			
		||||
    # TODO: Queue the requests - Webpush::TooManyRequests
 | 
			
		||||
    Webpush.payload_send(
 | 
			
		||||
      message: JSON.generate(
 | 
			
		||||
        title: title,
 | 
			
		||||
        dir: dir,
 | 
			
		||||
        image: image,
 | 
			
		||||
        badge: full_asset_url('badge.png'),
 | 
			
		||||
        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,
 | 
			
		||||
        }
 | 
			
		||||
      ),
 | 
			
		||||
      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
 | 
			
		||||
 | 
			
		||||
  def as_payload
 | 
			
		||||
    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
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def actions_arr(notification)
 | 
			
		||||
    actions =
 | 
			
		||||
      case notification.type
 | 
			
		||||
      when :mention then [
 | 
			
		||||
        {
 | 
			
		||||
          title: translate('push_notifications.mention.action_favourite'),
 | 
			
		||||
          icon: full_asset_url('emoji/2764.png'),
 | 
			
		||||
          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('emoji/1f441.png'), todo: 'expand', action: 'expand')
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    if can_boost
 | 
			
		||||
      actions << { title: translate('push_notifications.mention.action_boost'), icon: full_asset_url('emoji/1f504.png'), 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 pushable?(notification)
 | 
			
		||||
    data && data.key?('alerts') && data['alerts'][notification.type.to_s]
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def send_welcome_notification
 | 
			
		||||
    Webpush.payload_send(
 | 
			
		||||
      message: JSON.generate(
 | 
			
		||||
        title: translate('push_notifications.subscribed.title'),
 | 
			
		||||
        icon: full_asset_url('android-chrome-192x192.png'),
 | 
			
		||||
        badge: full_asset_url('badge.png'),
 | 
			
		||||
        data: {
 | 
			
		||||
          content: translate('push_notifications.subscribed.body'),
 | 
			
		||||
          actions: [],
 | 
			
		||||
          url: web_url('notifications'),
 | 
			
		||||
        }
 | 
			
		||||
      ),
 | 
			
		||||
      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: 5 * 60 # 5 minutes
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def find_or_create_access_token(notification)
 | 
			
		||||
    Doorkeeper::AccessToken.find_or_create_for(
 | 
			
		||||
      Doorkeeper::Application.find_by(superapp: true),
 | 
			
		||||
      notification.account.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
 | 
			
		||||
		Reference in New Issue
	
	Block a user