From 61bd11bacf1f254e20ba095331ca0ca19cf3a43a Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 28 Feb 2024 17:18:31 +0100 Subject: [PATCH] Add support for preview cards for local posts/accounts --- app/models/local_preview_card.rb | 156 ++++++++++++++++++ app/models/status.rb | 8 +- app/services/fetch_link_card_service.rb | 29 +++- ...240229163603_create_local_preview_cards.rb | 10 ++ db/schema.rb | 16 +- 5 files changed, 215 insertions(+), 4 deletions(-) create mode 100644 app/models/local_preview_card.rb create mode 100644 db/migrate/20240229163603_create_local_preview_cards.rb diff --git a/app/models/local_preview_card.rb b/app/models/local_preview_card.rb new file mode 100644 index 0000000000..e33789aed0 --- /dev/null +++ b/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 diff --git a/app/models/status.rb b/app/models/status.rb index 0ec69c8dd1..f855b6a400 100644 --- a/app/models/status.rb +++ b/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? diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index c6b600dd7c..8e401e3041 100644 --- a/app/services/fetch_link_card_service.rb +++ b/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) diff --git a/db/migrate/20240229163603_create_local_preview_cards.rb b/db/migrate/20240229163603_create_local_preview_cards.rb new file mode 100644 index 0000000000..2e2b99e8fd --- /dev/null +++ b/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 diff --git a/db/schema.rb b/db/schema.rb index 50f4e7189d..af82639a98 100644 --- a/db/schema.rb +++ b/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