Browse Source

Add support for preview cards for local posts/accounts

Claire 2 months ago
parent
commit
61bd11bacf

+ 156 - 0
app/models/local_preview_card.rb

@@ -0,0 +1,156 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: local_preview_cards
+#
+#  id                :bigint(8)        not null, primary key
+#  status_id         :bigint(8)        not null
+#  target_status_id  :bigint(8)
+#  target_account_id :bigint(8)
+#  created_at        :datetime         not null
+#  updated_at        :datetime         not null
+#
+class LocalPreviewCard < ApplicationRecord
+  include ActionView::Helpers::NumberHelper
+  include InstanceHelper
+  include AccountsHelper
+  include StatusesHelper
+
+  belongs_to :status
+  belongs_to :target_status, class_name: 'Status', optional: true
+  belongs_to :target_account, class_name: 'Account', optional: true
+
+  def url
+    ActivityPub::TagManager.instance.url_for(object)
+  end
+
+  def embed_url
+    '' # TODO: audio/video uploads?
+  end
+
+  alias original_url url
+
+  def title
+    account = object.is_a?(Account) ? object : object.account
+    "#{display_name(account)} (#{acct(account)})"
+  end
+
+  def provider_name
+    site_title
+  end
+
+  def provider_url
+    ''
+  end
+
+  def author_name
+    ''
+  end
+
+  def author_url
+    ''
+  end
+
+  def description
+    if object.is_a?(Account)
+      account_description(object)
+    elsif object.is_a?(Status)
+      status_description(object)
+    end
+  end
+
+  def type
+    'link'
+  end
+
+  def link_type
+    object.is_a?(Status) ? 'article' : 'unknown'
+  end
+
+  def html
+    ''
+  end
+
+  def published_at
+    nil
+  end
+
+  def max_score
+    nil
+  end
+
+  def max_score_at
+    nil
+  end
+
+  def trendable
+    false
+  end
+
+  def image_description
+    if object.is_a?(Account)
+      ''
+    elsif object.is_a?(Status)
+      status_media&.description.presence || ''
+    end
+  end
+
+  def width
+    if object.is_a?(Account)
+      400
+    elsif object.is_a?(Status)
+      if status_media&.image? && status_media.file.meta.present?
+        status_media.file.meta.dig('original', 'width')
+      else
+        0 # TODO
+      end
+    end
+  end
+
+  def height
+    if object.is_a?(Account)
+      400
+    elsif object.is_a?(Status)
+      if status_media&.image? && status_media.file.meta.present?
+        status_media.file.meta.dig('original', 'height')
+      else
+        0 # TODO
+      end
+    end
+  end
+
+  def blurhash
+    if object.is_a?(Account)
+      nil # TODO
+    elsif object.is_a?(Status)
+      status_media&.blurhash
+    end
+  end
+
+  def image
+    if object.is_a?(Account)
+      object.avatar
+    elsif object.is_a?(Status)
+      status_media&.thumbnail
+    end
+  end
+
+  def image?
+    image.present?
+  end
+
+  def language
+    nil # TODO
+  end
+
+  private
+
+  def object
+    target_status || target_account
+  end
+
+  def status_media
+    object.ordered_media_attachments.first
+  end
+end

+ 6 - 2
app/models/status.rb

@@ -85,6 +85,7 @@ class Status < ApplicationRecord
   has_and_belongs_to_many :tags
 
   has_one :preview_cards_status, inverse_of: :status, dependent: :delete
+  has_one :local_preview_card, inverse_of: :status, dependent: :delete
 
   has_one :notification, as: :activity, dependent: :destroy
   has_one :status_stat, inverse_of: :status, dependent: nil
@@ -152,6 +153,7 @@ class Status < ApplicationRecord
                    :status_stat,
                    :tags,
                    :preloadable_poll,
+                   :local_preview_card,
                    preview_cards_status: [:preview_card],
                    account: [:account_stat, user: :role],
                    active_mentions: { account: :account_stat },
@@ -162,6 +164,7 @@ class Status < ApplicationRecord
                      :conversation,
                      :status_stat,
                      :preloadable_poll,
+                     :local_preview_card,
                      preview_cards_status: [:preview_card],
                      account: [:account_stat, user: :role],
                      active_mentions: { account: :account_stat },
@@ -229,10 +232,11 @@ class Status < ApplicationRecord
   end
 
   def preview_card
-    preview_cards_status&.preview_card&.tap { |x| x.original_url = preview_cards_status.url }
+    local_preview_card || preview_cards_status&.preview_card&.tap { |x| x.original_url = preview_cards_status.url }
   end
 
   def reset_preview_card!
+    LocalPreviewCard.where(status_id: id).delete_all
     PreviewCardsStatus.where(status_id: id).delete_all
   end
 
@@ -251,7 +255,7 @@ class Status < ApplicationRecord
   end
 
   def with_preview_card?
-    preview_cards_status.present?
+    local_preview_card.present? || preview_cards_status.present?
   end
 
   def with_poll?

+ 28 - 1
app/services/fetch_link_card_service.rb

@@ -23,6 +23,8 @@ class FetchLinkCardService < BaseService
 
     @url = @original_url.to_s
 
+    return process_local_url if TagManager.instance.local_url?(@url)
+
     with_redis_lock("fetch:#{@original_url}") do
       @card = PreviewCard.find_by(url: @url)
       process_url if @card.nil? || @card.updated_at <= 2.weeks.ago || @card.missing_image?
@@ -42,6 +44,24 @@ class FetchLinkCardService < BaseService
     attempt_oembed || attempt_opengraph
   end
 
+  def process_local_url
+    recognized_params = Rails.application.routes.recognize_path(@url)
+    return unless recognized_params[:action] == 'show'
+
+    @card = nil
+
+    case recognized_params[:controller]
+    when 'statuses'
+      status = Status.where(visibility: [:public, :unlisted]).find_by(id: recognized_params[:id])
+      @card = LocalPreviewCard.create(status: @status, target_status: status)
+    when 'accounts'
+      account = Account.find_local(recognized_params[:username])
+      @card = LocalPreviewCard.create(status: @status, target_account: account)
+    end
+
+    Rails.cache.delete(@status) if @card.present?
+  end
+
   def html
     return @html if defined?(@html)
 
@@ -85,7 +105,14 @@ class FetchLinkCardService < BaseService
 
   def bad_url?(uri)
     # Avoid local instance URLs and invalid URLs
-    uri.host.blank? || TagManager.instance.local_url?(uri.to_s) || !%w(http https).include?(uri.scheme)
+    uri.host.blank? || bad_local_url?(uri.to_s) || !%w(http https).include?(uri.scheme)
+  end
+
+  def bad_local_url?(uri)
+    return false unless TagManager.instance.local_url?(uri.to_s)
+
+    recognized_params = Rails.application.routes.recognize_path(uri)
+    recognized_params[:action] != 'show' || %w(accounts statuses).exclude?(recognized_params[:controller])
   end
 
   def mention_link?(anchor)

+ 10 - 0
db/migrate/20240229163603_create_local_preview_cards.rb

@@ -0,0 +1,10 @@
+class CreateLocalPreviewCards < ActiveRecord::Migration[7.1]
+  def change
+    create_table :local_preview_cards do |t|
+      t.belongs_to :status, foreign_key: { on_delete: :cascade }, null: false
+      t.belongs_to :target_status, foreign_key: { on_delete: :cascade, to_table: :statuses }, null: true
+      t.belongs_to :target_account, foreign_key: { on_delete: :cascade, to_table: :accounts }, null: true
+      t.timestamps
+    end
+  end
+end

+ 15 - 1
db/schema.rb

@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema[7.1].define(version: 2024_01_11_033014) do
+ActiveRecord::Schema[7.1].define(version: 2024_02_29_163603) do
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
 
@@ -594,6 +594,17 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_11_033014) do
     t.index ["account_id"], name: "index_lists_on_account_id"
   end
 
+  create_table "local_preview_cards", force: :cascade do |t|
+    t.bigint "status_id", null: false
+    t.bigint "target_status_id"
+    t.bigint "target_account_id"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["status_id"], name: "index_local_preview_cards_on_status_id"
+    t.index ["target_account_id"], name: "index_local_preview_cards_on_target_account_id"
+    t.index ["target_status_id"], name: "index_local_preview_cards_on_target_status_id"
+  end
+
   create_table "login_activities", force: :cascade do |t|
     t.bigint "user_id", null: false
     t.string "authentication_method"
@@ -1246,6 +1257,9 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_11_033014) do
   add_foreign_key "list_accounts", "follows", on_delete: :cascade
   add_foreign_key "list_accounts", "lists", on_delete: :cascade
   add_foreign_key "lists", "accounts", on_delete: :cascade
+  add_foreign_key "local_preview_cards", "accounts", column: "target_account_id", on_delete: :cascade
+  add_foreign_key "local_preview_cards", "statuses", column: "target_status_id", on_delete: :cascade
+  add_foreign_key "local_preview_cards", "statuses", on_delete: :cascade
   add_foreign_key "login_activities", "users", on_delete: :cascade
   add_foreign_key "markers", "users", on_delete: :cascade
   add_foreign_key "media_attachments", "accounts", name: "fk_96dd81e81b", on_delete: :nullify