Add ability to manage which websites can credit you in link previews (#31819)
This commit is contained in:
parent
3929e3c6d2
commit
e0c27a5047
92 changed files with 381 additions and 160 deletions
|
@ -2,14 +2,30 @@
|
|||
|
||||
class Settings::VerificationsController < Settings::BaseController
|
||||
before_action :set_account
|
||||
before_action :set_verified_links
|
||||
|
||||
def show
|
||||
@verified_links = @account.fields.select(&:verified?)
|
||||
def show; end
|
||||
|
||||
def update
|
||||
if UpdateAccountService.new.call(@account, account_params)
|
||||
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
||||
redirect_to settings_verification_path, notice: I18n.t('generic.changes_saved_msg')
|
||||
else
|
||||
render :show
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def account_params
|
||||
params.require(:account).permit(:attribution_domains_as_text)
|
||||
end
|
||||
|
||||
def set_account
|
||||
@account = current_account
|
||||
end
|
||||
|
||||
def set_verified_links
|
||||
@verified_links = @account.fields.select(&:verified?)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -41,6 +41,7 @@ module ContextHelper
|
|||
'cipherText' => 'toot:cipherText',
|
||||
},
|
||||
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
||||
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
|
||||
}.freeze
|
||||
|
||||
def full_context
|
||||
|
|
|
@ -12,6 +12,41 @@ code {
|
|||
margin: 50px auto;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
border-radius: 8px;
|
||||
background: var(--surface-background-color);
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.fade-out-top {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 160px;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--surface-background-color),
|
||||
transparent
|
||||
);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
& > div {
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.indicator-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
# reviewed_at :datetime
|
||||
# requested_review_at :datetime
|
||||
# indexable :boolean default(FALSE), not null
|
||||
# attribution_domains :string default([]), is an Array
|
||||
#
|
||||
|
||||
class Account < ApplicationRecord
|
||||
|
@ -88,6 +89,7 @@ class Account < ApplicationRecord
|
|||
include Account::Merging
|
||||
include Account::Search
|
||||
include Account::StatusesSearch
|
||||
include Account::AttributionDomains
|
||||
include DomainMaterializable
|
||||
include DomainNormalizable
|
||||
include Paginable
|
||||
|
|
25
app/models/concerns/account/attribution_domains.rb
Normal file
25
app/models/concerns/account/attribution_domains.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Account::AttributionDomains
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
validates :attribution_domains_as_text, domain: { multiline: true }, lines: { maximum: 100 }, if: -> { local? && will_save_change_to_attribution_domains? }
|
||||
end
|
||||
|
||||
def attribution_domains_as_text
|
||||
self[:attribution_domains].join("\n")
|
||||
end
|
||||
|
||||
def attribution_domains_as_text=(str)
|
||||
self[:attribution_domains] = str.split.filter_map do |line|
|
||||
line.strip.delete_prefix('*.')
|
||||
end
|
||||
end
|
||||
|
||||
def can_be_attributed_from?(domain)
|
||||
segments = domain.split('.')
|
||||
variants = segments.map.with_index { |_, i| segments[i..].join('.') }.to_set
|
||||
self[:attribution_domains].to_set.intersect?(variants)
|
||||
end
|
||||
end
|
|
@ -8,7 +8,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
|||
|
||||
context_extensions :manually_approves_followers, :featured, :also_known_as,
|
||||
:moved_to, :property_value, :discoverable, :olm, :suspended,
|
||||
:memorial, :indexable
|
||||
:memorial, :indexable, :attribution_domains
|
||||
|
||||
attributes :id, :type, :following, :followers,
|
||||
:inbox, :outbox, :featured, :featured_tags,
|
||||
|
@ -25,6 +25,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
|||
attribute :moved_to, if: :moved?
|
||||
attribute :also_known_as, if: :also_known_as?
|
||||
attribute :suspended, if: :suspended?
|
||||
attribute :attribution_domains, if: -> { object.attribution_domains.any? }
|
||||
|
||||
class EndpointsSerializer < ActivityPub::Serializer
|
||||
include RoutingHelper
|
||||
|
|
|
@ -117,6 +117,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
@account.discoverable = @json['discoverable'] || false
|
||||
@account.indexable = @json['indexable'] || false
|
||||
@account.memorial = @json['memorial'] || false
|
||||
@account.attribution_domains = as_array(@json['attributionDomains'] || []).map { |item| value_or_id(item) }
|
||||
end
|
||||
|
||||
def set_fetchable_key!
|
||||
|
|
|
@ -153,12 +153,13 @@ class FetchLinkCardService < BaseService
|
|||
return if html.nil?
|
||||
|
||||
link_details_extractor = LinkDetailsExtractor.new(@url, @html, @html_charset)
|
||||
provider = PreviewCardProvider.matching_domain(Addressable::URI.parse(link_details_extractor.canonical_url).normalized_host)
|
||||
linked_account = ResolveAccountService.new.call(link_details_extractor.author_account, suppress_errors: true) if link_details_extractor.author_account.present? && provider&.trendable?
|
||||
domain = Addressable::URI.parse(link_details_extractor.canonical_url).normalized_host
|
||||
provider = PreviewCardProvider.matching_domain(domain)
|
||||
linked_account = ResolveAccountService.new.call(link_details_extractor.author_account, suppress_errors: true) if link_details_extractor.author_account.present?
|
||||
|
||||
@card = PreviewCard.find_or_initialize_by(url: link_details_extractor.canonical_url) if link_details_extractor.canonical_url != @card.url
|
||||
@card.assign_attributes(link_details_extractor.to_preview_card_attributes)
|
||||
@card.author_account = linked_account
|
||||
@card.author_account = linked_account if linked_account&.can_be_attributed_from?(domain) || provider&.trendable?
|
||||
@card.save_with_optional_image! unless @card.title.blank? && @card.html.blank?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,22 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DomainValidator < ActiveModel::EachValidator
|
||||
MAX_DOMAIN_LENGTH = 256
|
||||
MIN_LABEL_LENGTH = 1
|
||||
MAX_LABEL_LENGTH = 63
|
||||
ALLOWED_CHARACTERS_RE = /^[a-z0-9\-]+$/i
|
||||
|
||||
def validate_each(record, attribute, value)
|
||||
return if value.blank?
|
||||
|
||||
domain = if options[:acct]
|
||||
value.split('@').last
|
||||
else
|
||||
value
|
||||
end
|
||||
(options[:multiline] ? value.split : [value]).each do |domain|
|
||||
_, domain = domain.split('@') if options[:acct]
|
||||
|
||||
record.errors.add(attribute, I18n.t('domain_validator.invalid_domain')) unless compliant?(domain)
|
||||
next if domain.blank?
|
||||
|
||||
record.errors.add(attribute, options[:multiline] ? :invalid_domain_on_line : :invalid, value: domain) unless compliant?(domain)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def compliant?(value)
|
||||
Addressable::URI.new.tap { |uri| uri.host = value }
|
||||
uri = Addressable::URI.new
|
||||
uri.host = value
|
||||
uri.normalized_host.size < MAX_DOMAIN_LENGTH && uri.normalized_host.split('.').all? { |label| label.size.between?(MIN_LABEL_LENGTH, MAX_LABEL_LENGTH) && label =~ ALLOWED_CHARACTERS_RE }
|
||||
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
|
||||
false
|
||||
end
|
||||
|
|
9
app/validators/lines_validator.rb
Normal file
9
app/validators/lines_validator.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class LinesValidator < ActiveModel::EachValidator
|
||||
def validate_each(record, attribute, value)
|
||||
return if value.blank?
|
||||
|
||||
record.errors.add(attribute, :too_many_lines, limit: options[:maximum]) if options[:maximum].present? && value.split.size > options[:maximum]
|
||||
end
|
||||
end
|
|
@ -5,7 +5,9 @@
|
|||
%h2= t('settings.profile')
|
||||
= render partial: 'settings/shared/profile_navigation'
|
||||
|
||||
.simple_form
|
||||
.simple_form.form-section
|
||||
%h3= t('verification.website_verification')
|
||||
|
||||
%p.lead= t('verification.hint_html')
|
||||
|
||||
%h4= t('verification.here_is_how')
|
||||
|
@ -28,3 +30,33 @@
|
|||
%span.verified-badge
|
||||
= material_symbol 'check', class: 'verified-badge__mark'
|
||||
%span= field.value
|
||||
|
||||
= simple_form_for @account, url: settings_verification_path, html: { method: :put, class: 'form-section' } do |f|
|
||||
= render 'shared/error_messages', object: @account
|
||||
|
||||
%h3= t('author_attribution.title')
|
||||
|
||||
%p.lead= t('author_attribution.hint_html')
|
||||
|
||||
.fields-row
|
||||
.fields-row__column.fields-row__column-6
|
||||
.fields-group
|
||||
= f.input :attribution_domains_as_text, as: :text, wrapper: :with_block_label, input_html: { placeholder: "example1.com\nexample2.com\nexample3.com", rows: 4 }
|
||||
.fields-row__column.fields-row__column-6
|
||||
.fields-group.fade-out-top
|
||||
%div
|
||||
.status-card.expanded.bottomless
|
||||
.status-card__image
|
||||
= image_tag frontend_asset_url('images/preview.png'), alt: '', class: 'status-card__image-image'
|
||||
.status-card__content
|
||||
%span.status-card__host
|
||||
%span= t('author_attribution.s_blog', name: @account.username)
|
||||
·
|
||||
%time.time-ago{ datetime: 1.year.ago.to_date.iso8601 }
|
||||
%strong.status-card__title= t('author_attribution.example_title')
|
||||
.more-from-author
|
||||
= logo_as_symbol(:icon)
|
||||
= t('author_attribution.more_from_html', name: link_to(root_url, class: 'story__details__shared__author-link') { image_tag(@account.avatar.url, class: 'account__avatar', width: 16, height: 16, alt: '') + content_tag(:bdi, display_name(@account)) })
|
||||
|
||||
.actions
|
||||
= f.button :button, t('generic.save_changes'), type: :submit
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue