mirror of
https://github.com/mastodon/mastodon.git
synced 2024-12-22 23:20:44 +01:00
Add support for preview cards for local posts/accounts
This commit is contained in:
parent
7f84bbfd92
commit
61bd11bacf
5 changed files with 215 additions and 4 deletions
156
app/models/local_preview_card.rb
Normal file
156
app/models/local_preview_card.rb
Normal file
|
@ -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
|
|
@ -85,6 +85,7 @@ class Status < ApplicationRecord
|
||||||
has_and_belongs_to_many :tags
|
has_and_belongs_to_many :tags
|
||||||
|
|
||||||
has_one :preview_cards_status, inverse_of: :status, dependent: :delete
|
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 :notification, as: :activity, dependent: :destroy
|
||||||
has_one :status_stat, inverse_of: :status, dependent: nil
|
has_one :status_stat, inverse_of: :status, dependent: nil
|
||||||
|
@ -152,6 +153,7 @@ class Status < ApplicationRecord
|
||||||
:status_stat,
|
:status_stat,
|
||||||
:tags,
|
:tags,
|
||||||
:preloadable_poll,
|
:preloadable_poll,
|
||||||
|
:local_preview_card,
|
||||||
preview_cards_status: [:preview_card],
|
preview_cards_status: [:preview_card],
|
||||||
account: [:account_stat, user: :role],
|
account: [:account_stat, user: :role],
|
||||||
active_mentions: { account: :account_stat },
|
active_mentions: { account: :account_stat },
|
||||||
|
@ -162,6 +164,7 @@ class Status < ApplicationRecord
|
||||||
:conversation,
|
:conversation,
|
||||||
:status_stat,
|
:status_stat,
|
||||||
:preloadable_poll,
|
:preloadable_poll,
|
||||||
|
:local_preview_card,
|
||||||
preview_cards_status: [:preview_card],
|
preview_cards_status: [:preview_card],
|
||||||
account: [:account_stat, user: :role],
|
account: [:account_stat, user: :role],
|
||||||
active_mentions: { account: :account_stat },
|
active_mentions: { account: :account_stat },
|
||||||
|
@ -229,10 +232,11 @@ class Status < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def preview_card
|
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
|
end
|
||||||
|
|
||||||
def reset_preview_card!
|
def reset_preview_card!
|
||||||
|
LocalPreviewCard.where(status_id: id).delete_all
|
||||||
PreviewCardsStatus.where(status_id: id).delete_all
|
PreviewCardsStatus.where(status_id: id).delete_all
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -251,7 +255,7 @@ class Status < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_preview_card?
|
def with_preview_card?
|
||||||
preview_cards_status.present?
|
local_preview_card.present? || preview_cards_status.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_poll?
|
def with_poll?
|
||||||
|
|
|
@ -23,6 +23,8 @@ class FetchLinkCardService < BaseService
|
||||||
|
|
||||||
@url = @original_url.to_s
|
@url = @original_url.to_s
|
||||||
|
|
||||||
|
return process_local_url if TagManager.instance.local_url?(@url)
|
||||||
|
|
||||||
with_redis_lock("fetch:#{@original_url}") do
|
with_redis_lock("fetch:#{@original_url}") do
|
||||||
@card = PreviewCard.find_by(url: @url)
|
@card = PreviewCard.find_by(url: @url)
|
||||||
process_url if @card.nil? || @card.updated_at <= 2.weeks.ago || @card.missing_image?
|
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
|
attempt_oembed || attempt_opengraph
|
||||||
end
|
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
|
def html
|
||||||
return @html if defined?(@html)
|
return @html if defined?(@html)
|
||||||
|
|
||||||
|
@ -85,7 +105,14 @@ class FetchLinkCardService < BaseService
|
||||||
|
|
||||||
def bad_url?(uri)
|
def bad_url?(uri)
|
||||||
# Avoid local instance URLs and invalid URLs
|
# 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
|
end
|
||||||
|
|
||||||
def mention_link?(anchor)
|
def mention_link?(anchor)
|
||||||
|
|
10
db/migrate/20240229163603_create_local_preview_cards.rb
Normal file
10
db/migrate/20240229163603_create_local_preview_cards.rb
Normal file
|
@ -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
|
16
db/schema.rb
16
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
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"
|
t.index ["account_id"], name: "index_lists_on_account_id"
|
||||||
end
|
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|
|
create_table "login_activities", force: :cascade do |t|
|
||||||
t.bigint "user_id", null: false
|
t.bigint "user_id", null: false
|
||||||
t.string "authentication_method"
|
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", "follows", on_delete: :cascade
|
||||||
add_foreign_key "list_accounts", "lists", on_delete: :cascade
|
add_foreign_key "list_accounts", "lists", on_delete: :cascade
|
||||||
add_foreign_key "lists", "accounts", 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 "login_activities", "users", on_delete: :cascade
|
||||||
add_foreign_key "markers", "users", on_delete: :cascade
|
add_foreign_key "markers", "users", on_delete: :cascade
|
||||||
add_foreign_key "media_attachments", "accounts", name: "fk_96dd81e81b", on_delete: :nullify
|
add_foreign_key "media_attachments", "accounts", name: "fk_96dd81e81b", on_delete: :nullify
|
||||||
|
|
Loading…
Reference in a new issue