mirror of
https://github.com/mastodon/mastodon.git
synced 2025-07-15 16:58:14 +00:00
Compare commits
7 Commits
6d1c1fd684
...
bc3737f0c3
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bc3737f0c3 | ||
![]() |
03bbb74b0c | ||
![]() |
836c0477ac | ||
![]() |
b15a3614dc | ||
![]() |
ff08d99d4d | ||
![]() |
42adb6eaee | ||
![]() |
096057b845 |
|
@ -76,8 +76,8 @@ export function importFetchedStatuses(statuses) {
|
|||
pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id])));
|
||||
}
|
||||
|
||||
if (status.card?.author_account) {
|
||||
pushUnique(accounts, status.card.author_account);
|
||||
if (status.card) {
|
||||
status.card.authors.forEach(author => author.account && pushUnique(accounts, author.account));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,8 +36,15 @@ export function normalizeStatus(status, normalOldStatus) {
|
|||
normalStatus.poll = status.poll.id;
|
||||
}
|
||||
|
||||
if (status.card?.author_account) {
|
||||
normalStatus.card = { ...status.card, author_account: status.card.author_account.id };
|
||||
if (status.card) {
|
||||
normalStatus.card = {
|
||||
...status.card,
|
||||
authors: status.card.authors.map(author => ({
|
||||
...author,
|
||||
accountId: author.account?.id,
|
||||
account: undefined,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
if (status.filtered) {
|
||||
|
|
|
@ -51,7 +51,7 @@ export const fetchTrendingLinks = () => (dispatch) => {
|
|||
api()
|
||||
.get('/api/v1/trends/links', { params: { limit: 20 } })
|
||||
.then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data.map(link => link.author_account).filter(account => !!account)));
|
||||
dispatch(importFetchedAccounts(data.flatMap(link => link.authors.map(author => author.account)).filter(account => !!account)));
|
||||
dispatch(fetchTrendingLinksSuccess(data));
|
||||
})
|
||||
.catch(err => dispatch(fetchTrendingLinksFail(err)));
|
||||
|
|
|
@ -30,6 +30,12 @@ export interface ApiMentionJSON {
|
|||
acct: string;
|
||||
}
|
||||
|
||||
export interface ApiPreviewCardAuthorJSON {
|
||||
name: string;
|
||||
url: string;
|
||||
account?: ApiAccountJSON;
|
||||
}
|
||||
|
||||
export interface ApiPreviewCardJSON {
|
||||
url: string;
|
||||
title: string;
|
||||
|
@ -48,6 +54,7 @@ export interface ApiPreviewCardJSON {
|
|||
embed_url: string;
|
||||
blurhash: string;
|
||||
published_at: string;
|
||||
authors: ApiPreviewCardAuthorJSON[];
|
||||
}
|
||||
|
||||
export interface ApiStatusJSON {
|
||||
|
|
|
@ -8,6 +8,10 @@ import { useAppSelector } from 'mastodon/store';
|
|||
export const AuthorLink = ({ accountId }) => {
|
||||
const account = useAppSelector(state => state.getIn(['accounts', accountId]));
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link to={`/@${account.get('acct')}`} className='story__details__shared__author-link' data-hover-card-account={accountId}>
|
||||
<Avatar account={account} size={16} />
|
||||
|
|
|
@ -75,7 +75,7 @@ class Links extends PureComponent {
|
|||
publisher={link.get('provider_name')}
|
||||
publishedAt={link.get('published_at')}
|
||||
author={link.get('author_name')}
|
||||
authorAccount={link.getIn(['author_account', 'id'])}
|
||||
authorAccount={link.getIn(['authors', 0, 'account', 'id'])}
|
||||
sharedTimes={link.getIn(['history', 0, 'accounts']) * 1 + link.getIn(['history', 1, 'accounts']) * 1}
|
||||
thumbnail={link.get('image')}
|
||||
thumbnailDescription={link.get('image_description')}
|
||||
|
|
|
@ -138,7 +138,7 @@ export default class Card extends PureComponent {
|
|||
const interactive = card.get('type') === 'video';
|
||||
const language = card.get('language') || '';
|
||||
const largeImage = (card.get('image')?.length > 0 && card.get('width') > card.get('height')) || interactive;
|
||||
const showAuthor = !!card.get('author_account');
|
||||
const showAuthor = !!card.getIn(['authors', 0, 'accountId']);
|
||||
|
||||
const description = (
|
||||
<div className='status-card__content'>
|
||||
|
@ -244,7 +244,7 @@ export default class Card extends PureComponent {
|
|||
{description}
|
||||
</a>
|
||||
|
||||
{showAuthor && <MoreFromAuthor accountId={card.get('author_account')} />}
|
||||
{showAuthor && <MoreFromAuthor accountId={card.getIn(['authors', 0, 'accountId'])} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -128,6 +128,22 @@ class PreviewCard < ApplicationRecord
|
|||
@history ||= Trends::History.new('links', id)
|
||||
end
|
||||
|
||||
def authors
|
||||
@authors ||= [PreviewCard::Author.new(self)]
|
||||
end
|
||||
|
||||
class Author < ActiveModelSerializers::Model
|
||||
attributes :name, :url, :account
|
||||
|
||||
def initialize(preview_card)
|
||||
super(
|
||||
name: preview_card.author_name,
|
||||
url: preview_card.author_url,
|
||||
account: preview_card.author_account,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
private
|
||||
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::PreviewCardSerializer < ActiveModel::Serializer
|
||||
class AuthorSerializer < ActiveModel::Serializer
|
||||
attributes :name, :url
|
||||
has_one :account, serializer: REST::AccountSerializer
|
||||
end
|
||||
|
||||
include RoutingHelper
|
||||
|
||||
attributes :url, :title, :description, :language, :type,
|
||||
|
@ -8,7 +13,7 @@ class REST::PreviewCardSerializer < ActiveModel::Serializer
|
|||
:provider_url, :html, :width, :height,
|
||||
:image, :image_description, :embed_url, :blurhash, :published_at
|
||||
|
||||
has_one :author_account, serializer: REST::AccountSerializer, if: -> { object.author_account.present? }
|
||||
has_many :authors, serializer: AuthorSerializer
|
||||
|
||||
def url
|
||||
object.original_url.presence || object.url
|
||||
|
|
|
@ -15,6 +15,9 @@ class FetchLinkCardService < BaseService
|
|||
)
|
||||
}iox
|
||||
|
||||
# URL size limit to safely store in PosgreSQL's unique indexes
|
||||
BYTESIZE_LIMIT = 2692
|
||||
|
||||
def call(status)
|
||||
@status = status
|
||||
@original_url = parse_urls
|
||||
|
@ -29,7 +32,7 @@ class FetchLinkCardService < BaseService
|
|||
end
|
||||
|
||||
attach_card if @card&.persisted?
|
||||
rescue HTTP::Error, OpenSSL::SSL::SSLError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e
|
||||
rescue HTTP::Error, OpenSSL::SSL::SSLError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError, Encoding::UndefinedConversionError => e
|
||||
Rails.logger.debug { "Error fetching link #{@original_url}: #{e}" }
|
||||
nil
|
||||
end
|
||||
|
@ -85,7 +88,7 @@ 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? || TagManager.instance.local_url?(uri.to_s) || !%w(http https).include?(uri.scheme) || uri.to_s.bytesize > BYTESIZE_LIMIT
|
||||
end
|
||||
|
||||
def mention_link?(anchor)
|
||||
|
|
|
@ -5,7 +5,11 @@ if Rails.configuration.x.use_vips
|
|||
|
||||
require 'vips'
|
||||
|
||||
abort('Incompatible libvips version, please install libvips >= 8.13') unless Vips.at_least_libvips?(8, 13)
|
||||
unless Vips.at_least_libvips?(8, 13)
|
||||
abort <<~ERROR.squish
|
||||
Incompatible libvips version (#{Vips.version_string}), please install libvips >= 8.13
|
||||
ERROR
|
||||
end
|
||||
|
||||
Vips.block('VipsForeign', true)
|
||||
|
||||
|
@ -25,3 +29,11 @@ if Rails.configuration.x.use_vips
|
|||
|
||||
Vips.block_untrusted(true)
|
||||
end
|
||||
|
||||
# In some places of the code, we rescue this exception, but we don't always
|
||||
# load libvips, so it may be an undefined constant:
|
||||
unless defined?(Vips)
|
||||
module Vips
|
||||
class Error < StandardError; end
|
||||
end
|
||||
end
|
||||
|
|
5
spec/fixtures/requests/redirect_with_utf8_url.txt
vendored
Normal file
5
spec/fixtures/requests/redirect_with_utf8_url.txt
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
HTTP/1.1 301 Moved Permanently
|
||||
server: nginx
|
||||
date: Thu, 27 Jun 2024 11:04:53 GMT
|
||||
content-type: text/html; charset=UTF-8
|
||||
location: http://example.com/ärgerliche-umlaute.html
|
|
@ -210,10 +210,14 @@ RSpec.describe MediaAttachment, :paperclip_processing do
|
|||
expect(media.file.meta['original']['duration']).to be_within(0.05).of(0.235102)
|
||||
expect(media.thumbnail.present?).to be true
|
||||
|
||||
# NOTE: Our libvips and ImageMagick implementations currently have different results
|
||||
expect(media.file.meta['colors']['background']).to eq(ENV['MASTODON_USE_LIBVIPS'] ? '#268cd9' : '#3088d4')
|
||||
expect(media.file.meta['colors']['background']).to eq(expected_background_color)
|
||||
expect(media.file_file_name).to_not eq 'boop.ogg'
|
||||
end
|
||||
|
||||
def expected_background_color
|
||||
# The libvips and ImageMagick implementations produce different results
|
||||
Rails.configuration.x.use_vips ? '#268cd9' : '#3088d4'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'mp3 with large cover art' do
|
||||
|
|
|
@ -27,6 +27,7 @@ RSpec.describe FetchLinkCardService do
|
|||
stub_request(:get, 'http://example.com/koi8-r').to_return(request_fixture('koi8-r.txt'))
|
||||
stub_request(:get, 'http://example.com/windows-1251').to_return(request_fixture('windows-1251.txt'))
|
||||
stub_request(:get, 'http://example.com/low_confidence_latin1').to_return(request_fixture('low_confidence_latin1.txt'))
|
||||
stub_request(:get, 'http://example.com/aergerliche-umlaute').to_return(request_fixture('redirect_with_utf8_url.txt'))
|
||||
|
||||
Rails.cache.write('oembed_endpoint:example.com', oembed_cache) if oembed_cache
|
||||
|
||||
|
@ -101,6 +102,14 @@ RSpec.describe FetchLinkCardService do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with a redirect URL with faulty encoding' do
|
||||
let(:status) { Fabricate(:status, text: 'http://example.com/aergerliche-umlaute') }
|
||||
|
||||
it 'does not create a preview card' do
|
||||
expect(status.preview_card).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a 404 URL' do
|
||||
let(:status) { Fabricate(:status, text: 'http://example.com/not-found') }
|
||||
|
||||
|
@ -193,6 +202,19 @@ RSpec.describe FetchLinkCardService do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with an URL too long for PostgreSQL unique indexes' do
|
||||
let(:url) { "http://example.com/#{'a' * 2674}" }
|
||||
let(:status) { Fabricate(:status, text: url) }
|
||||
|
||||
it 'does not fetch the URL' do
|
||||
expect(a_request(:get, url)).to_not have_been_made
|
||||
end
|
||||
|
||||
it 'does not create a preview card' do
|
||||
expect(status.preview_card).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a URL of a page with oEmbed support' do
|
||||
let(:html) { '<!doctype html><title>Hello world</title><link rel="alternate" type="application/json+oembed" href="http://example.com/oembed?url=http://example.com/html">' }
|
||||
let(:status) { Fabricate(:status, text: 'http://example.com/html') }
|
||||
|
|
|
@ -14099,12 +14099,12 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"prom-client@npm:^15.0.0":
|
||||
version: 15.1.2
|
||||
resolution: "prom-client@npm:15.1.2"
|
||||
version: 15.1.3
|
||||
resolution: "prom-client@npm:15.1.3"
|
||||
dependencies:
|
||||
"@opentelemetry/api": "npm:^1.4.0"
|
||||
tdigest: "npm:^0.1.1"
|
||||
checksum: 10c0/a221db148fa64e29dfd4c6cdcaaae14635495a4272b68917e2b44fcfd988bc57027d275b04489ceeea4d0c4d64d058af842c1300966d2c1ffa255f1fa6af1277
|
||||
checksum: 10c0/816525572e5799a2d1d45af78512fb47d073c842dc899c446e94d17cfc343d04282a1627c488c7ca1bcd47f766446d3e49365ab7249f6d9c22c7664a5bce7021
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user