about summary refs log tree commit diff
path: root/app/models
diff options
context:
space:
mode:
authorDavid Yip <yipdw@member.fsf.org>2017-09-09 14:27:47 -0500
committerDavid Yip <yipdw@member.fsf.org>2017-09-09 14:27:47 -0500
commitb9f7bc149b2a6abfbdaee83e6992b617b8bdb18e (patch)
tree355225f4424a6ea1b40c66c5540ccab42096e3bf /app/models
parente18ed4bbc7ab4e258d05a3e2a5db0790f67a8f37 (diff)
parent5d170587e3b6c1a3b3ebe0910b62a4c526e2900d (diff)
Merge branch 'origin/master' into sync/upstream
 Conflicts:
	app/javascript/mastodon/components/status_list.js
	app/javascript/mastodon/features/notifications/index.js
	app/javascript/mastodon/features/ui/components/modal_root.js
	app/javascript/mastodon/features/ui/components/onboarding_modal.js
	app/javascript/mastodon/features/ui/index.js
	app/javascript/styles/about.scss
	app/javascript/styles/accounts.scss
	app/javascript/styles/components.scss
	app/presenters/instance_presenter.rb
	app/services/post_status_service.rb
	app/services/reblog_service.rb
	app/views/about/more.html.haml
	app/views/about/show.html.haml
	app/views/accounts/_header.html.haml
	config/webpack/loaders/babel.js
	spec/controllers/api/v1/accounts/credentials_controller_spec.rb
Diffstat (limited to 'app/models')
-rw-r--r--app/models/account.rb17
-rw-r--r--app/models/concerns/account_avatar.rb2
-rw-r--r--app/models/concerns/account_header.rb2
-rw-r--r--app/models/concerns/account_interactions.rb4
-rw-r--r--app/models/concerns/remotable.rb2
-rw-r--r--app/models/import.rb1
-rw-r--r--app/models/media_attachment.rb3
-rw-r--r--app/models/preview_card.rb31
-rw-r--r--app/models/remote_follow.rb2
-rw-r--r--app/models/session_activation.rb1
-rw-r--r--app/models/status.rb20
-rw-r--r--app/models/status_pin.rb18
-rw-r--r--app/models/subscription.rb1
-rw-r--r--app/models/user.rb15
-rw-r--r--app/models/web/push_subscription.rb160
15 files changed, 121 insertions, 158 deletions
diff --git a/app/models/account.rb b/app/models/account.rb
index e217733f5..d0ebf5a5e 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -77,6 +77,10 @@ class Account < ApplicationRecord
   has_many :mentions, inverse_of: :account, dependent: :destroy
   has_many :notifications, inverse_of: :account, dependent: :destroy
 
+  # Pinned statuses
+  has_many :status_pins, inverse_of: :account, dependent: :destroy
+  has_many :pinned_statuses, -> { reorder('status_pins.created_at DESC') }, through: :status_pins, class_name: 'Status', source: :status
+
   # Media
   has_many :media_attachments, dependent: :destroy
 
@@ -91,7 +95,7 @@ class Account < ApplicationRecord
   scope :local, -> { where(domain: nil) }
   scope :without_followers, -> { where(followers_count: 0) }
   scope :with_followers, -> { where('followers_count > 0') }
-  scope :expiring, ->(time) { where(subscription_expires_at: nil).or(where('subscription_expires_at < ?', time)).remote.with_followers }
+  scope :expiring, ->(time) { remote.where.not(subscription_expires_at: nil).where('subscription_expires_at < ?', time) }
   scope :partitioned, -> { order('row_number() over (partition by domain)') }
   scope :silenced, -> { where(silenced: true) }
   scope :suspended, -> { where(suspended: true) }
@@ -105,6 +109,7 @@ class Account < ApplicationRecord
            :current_sign_in_ip,
            :current_sign_in_at,
            :confirmed?,
+           :admin?,
            :locale,
            to: :user,
            prefix: true,
@@ -133,11 +138,11 @@ class Account < ApplicationRecord
   end
 
   def keypair
-    OpenSSL::PKey::RSA.new(private_key || public_key)
+    @keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
   end
 
   def subscription(webhook_url)
-    OStatus2::Subscription.new(remote_url, secret: secret, lease_seconds: 30.days.seconds, webhook: webhook_url, hub: hub_url)
+    @subscription ||= OStatus2::Subscription.new(remote_url, secret: secret, webhook: webhook_url, hub: hub_url)
   end
 
   def save_with_optional_media!
@@ -171,6 +176,10 @@ class Account < ApplicationRecord
       reorder(nil).pluck('distinct accounts.domain')
     end
 
+    def inboxes
+      reorder(nil).where(protocol: :activitypub).pluck("distinct coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url)")
+    end
+
     def triadic_closures(account, limit: 5, offset: 0)
       sql = <<-SQL.squish
         WITH first_degree AS (
@@ -263,7 +272,7 @@ class Account < ApplicationRecord
   def generate_keys
     return unless local?
 
-    keypair = OpenSSL::PKey::RSA.new(Rails.env.test? ? 1024 : 2048)
+    keypair = OpenSSL::PKey::RSA.new(Rails.env.test? ? 512 : 2048)
     self.private_key = keypair.to_pem
     self.public_key  = keypair.public_key.to_pem
   end
diff --git a/app/models/concerns/account_avatar.rb b/app/models/concerns/account_avatar.rb
index b0ec689a7..8a5c9a22c 100644
--- a/app/models/concerns/account_avatar.rb
+++ b/app/models/concerns/account_avatar.rb
@@ -8,7 +8,7 @@ module AccountAvatar
   class_methods do
     def avatar_styles(file)
       styles = { original: '120x120#' }
-      styles[:static] = { animated: false } if file.content_type == 'image/gif'
+      styles[:static] = { format: 'png', convert_options: '-coalesce' } if file.content_type == 'image/gif'
       styles
     end
 
diff --git a/app/models/concerns/account_header.rb b/app/models/concerns/account_header.rb
index 542e25abe..aff2aa3f9 100644
--- a/app/models/concerns/account_header.rb
+++ b/app/models/concerns/account_header.rb
@@ -8,7 +8,7 @@ module AccountHeader
   class_methods do
     def header_styles(file)
       styles = { original: '700x335#' }
-      styles[:static] = { animated: false } if file.content_type == 'image/gif'
+      styles[:static] = { format: 'png', convert_options: '-coalesce' } if file.content_type == 'image/gif'
       styles
     end
 
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index 9ffed2910..b26520f5b 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -138,4 +138,8 @@ module AccountInteractions
   def reblogged?(status)
     status.proper.reblogs.where(account: self).exists?
   end
+
+  def pinned?(status)
+    status_pins.where(status: status).exists?
+  end
 end
diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb
index 1bd87a642..270043a9e 100644
--- a/app/models/concerns/remotable.rb
+++ b/app/models/concerns/remotable.rb
@@ -10,6 +10,8 @@ module Remotable
       alt_method_name = "reset_#{attachment_name}!".to_sym
 
       define_method method_name do |url|
+        return if url.blank?
+
         begin
           parsed_url = Addressable::URI.parse(url).normalize
         rescue Addressable::URI::InvalidURIError
diff --git a/app/models/import.rb b/app/models/import.rb
index 815e02589..4656c3af6 100644
--- a/app/models/import.rb
+++ b/app/models/import.rb
@@ -28,4 +28,5 @@ class Import < ApplicationRecord
 
   has_attached_file :data, url: '/system/:hash.:extension', hash_secret: ENV['PAPERCLIP_SECRET']
   validates_attachment_content_type :data, content_type: FILE_TYPES
+  validates_attachment_presence :data
 end
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index 1e8c6d00a..d83ca44f1 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -142,9 +142,11 @@ class MediaAttachment < ApplicationRecord
 
   def populate_meta
     meta = {}
+
     file.queued_for_write.each do |style, file|
       begin
         geo = Paperclip::Geometry.from_file file
+
         meta[style] = {
           width: geo.width.to_i,
           height: geo.height.to_i,
@@ -155,6 +157,7 @@ class MediaAttachment < ApplicationRecord
         meta[style] = {}
       end
     end
+
     meta
   end
 
diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb
index c334c48aa..b7efac354 100644
--- a/app/models/preview_card.rb
+++ b/app/models/preview_card.rb
@@ -4,16 +4,13 @@
 # Table name: preview_cards
 #
 #  id                 :integer          not null, primary key
-#  status_id          :integer
 #  url                :string           default(""), not null
-#  title              :string
-#  description        :string
+#  title              :string           default(""), not null
+#  description        :string           default(""), not null
 #  image_file_name    :string
 #  image_content_type :string
 #  image_file_size    :integer
 #  image_updated_at   :datetime
-#  created_at         :datetime         not null
-#  updated_at         :datetime         not null
 #  type               :integer          default("link"), not null
 #  html               :text             default(""), not null
 #  author_name        :string           default(""), not null
@@ -22,6 +19,8 @@
 #  provider_url       :string           default(""), not null
 #  width              :integer          default(0), not null
 #  height             :integer          default(0), not null
+#  created_at         :datetime         not null
+#  updated_at         :datetime         not null
 #
 
 class PreviewCard < ApplicationRecord
@@ -31,21 +30,37 @@ class PreviewCard < ApplicationRecord
 
   enum type: [:link, :photo, :video, :rich]
 
-  belongs_to :status
+  has_and_belongs_to_many :statuses
 
-  has_attached_file :image, styles: { original: '120x120#' }, convert_options: { all: '-quality 80 -strip' }
+  has_attached_file :image, styles: { original: '280x120>' }, convert_options: { all: '-quality 80 -strip' }
 
   include Attachmentable
   include Remotable
 
-  validates :url, presence: true
+  validates :url, presence: true, uniqueness: true
   validates_attachment_content_type :image, content_type: IMAGE_MIME_TYPES
   validates_attachment_size :image, less_than: 1.megabytes
 
+  before_save :extract_dimensions, if: :link?
+
   def save_with_optional_image!
     save!
   rescue ActiveRecord::RecordInvalid
     self.image = nil
     save!
   end
+
+  private
+
+  def extract_dimensions
+    file = image.queued_for_write[:original]
+
+    return if file.nil?
+
+    geo         = Paperclip::Geometry.from_file(file)
+    self.width  = geo.width.to_i
+    self.height = geo.height.to_i
+  rescue Paperclip::Errors::NotIdentifiedByImageMagickError
+    nil
+  end
 end
diff --git a/app/models/remote_follow.rb b/app/models/remote_follow.rb
index 8366d43c5..c3f867743 100644
--- a/app/models/remote_follow.rb
+++ b/app/models/remote_follow.rb
@@ -42,7 +42,7 @@ class RemoteFollow
 
   def acct_resource
     @_acct_resource ||= Goldfinger.finger("acct:#{acct}")
-  rescue Goldfinger::Error
+  rescue Goldfinger::Error, HTTP::ConnectionError
     nil
   end
 
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/status.rb b/app/models/status.rb
index 24eaf7071..f44f79aaf 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -47,10 +47,12 @@ class Status < ApplicationRecord
   has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :thread
   has_many :mentions, dependent: :destroy
   has_many :media_attachments, dependent: :destroy
+
   has_and_belongs_to_many :tags
+  has_and_belongs_to_many :preview_cards
 
   has_one :notification, as: :activity, dependent: :destroy
-  has_one :preview_card, dependent: :destroy
+  has_one :stream_entry, as: :activity, inverse_of: :status
 
   validates :uri, uniqueness: true, unless: :local?
   validates :text, presence: true, unless: :reblog?
@@ -90,7 +92,11 @@ class Status < ApplicationRecord
   end
 
   def verb
-    reblog? ? :share : :post
+    if destroyed?
+      :delete
+    else
+      reblog? ? :share : :post
+    end
   end
 
   def object_type
@@ -110,7 +116,11 @@ class Status < ApplicationRecord
   end
 
   def title
-    reblog? ? "#{account.acct} shared a status by #{reblog.account.acct}" : "New status by #{account.acct}"
+    if destroyed?
+      "#{account.acct} deleted status"
+    else
+      reblog? ? "#{account.acct} shared a status by #{reblog.account.acct}" : "New status by #{account.acct}"
+    end
   end
 
   def hidden?
@@ -164,6 +174,10 @@ class Status < ApplicationRecord
       ConversationMute.select('conversation_id').where(conversation_id: conversation_ids).where(account_id: account_id).map { |m| [m.conversation_id, true] }.to_h
     end
 
+    def pins_map(status_ids, account_id)
+      StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).map { |p| [p.status_id, true] }.to_h
+    end
+
     def reload_stale_associations!(cached_items)
       account_ids = []
 
diff --git a/app/models/status_pin.rb b/app/models/status_pin.rb
new file mode 100644
index 000000000..a72c19750
--- /dev/null
+++ b/app/models/status_pin.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: status_pins
+#
+#  id         :integer          not null, primary key
+#  account_id :integer          not null
+#  status_id  :integer          not null
+#  created_at :datetime         not null
+#  updated_at :datetime         not null
+#
+
+class StatusPin < ApplicationRecord
+  belongs_to :account, required: true
+  belongs_to :status, required: true
+
+  validates_with StatusPinValidator
+end
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
index bf643c1f9..14f1a140c 100644
--- a/app/models/subscription.rb
+++ b/app/models/subscription.rb
@@ -26,6 +26,7 @@ class Subscription < ApplicationRecord
 
   scope :confirmed, -> { where(confirmed: true) }
   scope :future_expiration, -> { where(arel_table[:expires_at].gt(Time.now.utc)) }
+  scope :expired, -> { where(arel_table[:expires_at].lt(Time.now.utc)) }
   scope :active, -> { confirmed.future_expiration }
 
   def lease_seconds=(value)
diff --git a/app/models/user.rb b/app/models/user.rb
index 96a2d09b7..5e548c1ef 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -46,6 +46,8 @@ class User < ApplicationRecord
   belongs_to :account, inverse_of: :user, required: true
   accepts_nested_attributes_for :account
 
+  has_many :applications, class_name: 'Doorkeeper::Application', as: :owner
+
   validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
   validates_with BlacklistedEmailValidator, if: :email_changed?
 
@@ -108,10 +110,21 @@ class User < ApplicationRecord
     settings.noindex
   end
 
+  def token_for_app(a)
+    return nil if a.nil? || a.owner != self
+    Doorkeeper::AccessToken
+      .find_or_create_by(application_id: a.id, resource_owner_id: id) do |t|
+
+      t.scopes = a.scopes
+      t.expires_in = Doorkeeper.configuration.access_token_expires_in
+      t.use_refresh_token = Doorkeeper.configuration.refresh_token_enabled?
+    end
+  end
+
   def activate_session(request)
     session_activations.activate(session_id: SecureRandom.hex,
                                  user_agent: request.user_agent,
-                                 ip: request.ip).session_id
+                                 ip: request.remote_ip).session_id
   end
 
   def exclusive_session(id)
diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb
index e76f61278..cb15dfa37 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
+  def access_token
+    find_or_create_access_token.token
   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
+  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}",
+        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