Compare commits

...

5 Commits

Author SHA1 Message Date
Eugen Rochko
2cfc3d915d
Merge ec6d1f678f into fbe9728f36 2025-05-06 15:05:46 +00:00
Claire
fbe9728f36
Bump version to v4.3.8 (#34626)
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / Libvips tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.2) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
2025-05-06 14:17:07 +00:00
Claire
3bbf3e9709
Fix code style issue (#34624) 2025-05-06 13:35:54 +00:00
Claire
79931bf3ae
Merge commit from fork
* Check scheme in account and post links

* Harden media attachments

* Client-side mitigation

* Client-side mitigation for media attachments
2025-05-06 15:02:13 +02:00
Eugen Rochko
ec6d1f678f Add color and blurhash extraction for profile pictures 2025-05-02 18:27:15 +02:00
26 changed files with 219 additions and 94 deletions

View File

@ -2,9 +2,34 @@
All notable changes to this project will be documented in this file.
## [4.3.8] - 2025-05-06
### Security
- Update dependencies
- Check scheme on account, profile, and media URLs ([GHSA-x2rc-v5wx-g3m5](https://github.com/mastodon/mastodon/security/advisories/GHSA-x2rc-v5wx-g3m5))
### Added
- Add warning for REDIS_NAMESPACE deprecation at startup (#34581 by @ClearlyClaire)
- Add built-in context for interaction policies (#34574 by @ClearlyClaire)
### Changed
- Change activity distribution error handling to skip retrying for deleted accounts (#33617 by @ClearlyClaire)
### Removed
- Remove double-query for signed query strings (#34610 by @ClearlyClaire)
### Fixed
- Fix incorrect redirect in response to unauthenticated API requests in limited federation mode (#34549 by @ClearlyClaire)
- Fix sign-up e-mail confirmation page reloading on error or redirect (#34548 by @ClearlyClaire)
## [4.3.7] - 2025-04-02
### Add
### Added
- Add delay to profile updates to debounce them (#34137 by @ClearlyClaire)
- Add support for paginating partial collections in `SynchronizeFollowersService` (#34272 and #34277 by @ClearlyClaire)

View File

@ -77,6 +77,17 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
if (normalStatus.url && !(normalStatus.url.startsWith('http://') || normalStatus.url.startsWith('https://'))) {
normalStatus.url = null;
}
normalStatus.url ||= normalStatus.uri;
normalStatus.media_attachments.forEach(item => {
if (item.remote_url && !(item.remote_url.startsWith('http://') || item.remote_url.startsWith('https://')))
item.remote_url = null;
});
}
if (normalOldStatus) {

View File

@ -12,6 +12,14 @@ export interface ApiAccountRoleJSON {
name: string;
}
export interface ApiMetaJSON {
colors?: {
background: string;
foreground: string;
accent: string;
};
}
// See app/serializers/rest/account_serializer.rb
export interface BaseApiAccountJSON {
acct: string;
@ -44,6 +52,8 @@ export interface BaseApiAccountJSON {
limited?: boolean;
memorial?: boolean;
hide_collections: boolean;
blurhash?: string;
meta?: ApiMetaJSON;
}
// See app/serializers/rest/muted_account_serializer.rb

View File

@ -16,7 +16,9 @@ exports[`<AvatarOverlay renders a overlay avatar 1`] = `
className="account__avatar-overlay-base"
>
<div
className="account__avatar"
className="account__avatar account__avatar--loading"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
style={
{
"height": "36px",
@ -25,7 +27,9 @@ exports[`<AvatarOverlay renders a overlay avatar 1`] = `
}
>
<img
alt="alice"
alt=""
onError={[Function]}
onLoad={[Function]}
src="/static/alice.jpg"
/>
</div>
@ -34,7 +38,9 @@ exports[`<AvatarOverlay renders a overlay avatar 1`] = `
className="account__avatar-overlay-overlay"
>
<div
className="account__avatar"
className="account__avatar account__avatar--loading"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
style={
{
"height": "24px",
@ -43,7 +49,9 @@ exports[`<AvatarOverlay renders a overlay avatar 1`] = `
}
>
<img
alt="eve@blackhat.lair"
alt=""
onError={[Function]}
onLoad={[Function]}
src="/static/eve.jpg"
/>
</div>

View File

@ -2,8 +2,9 @@ import { useState, useCallback } from 'react';
import classNames from 'classnames';
import { Blurhash } from 'mastodon/components/blurhash';
import { useHovering } from 'mastodon/hooks/useHovering';
import { autoPlayGif } from 'mastodon/initial_state';
import { autoPlayGif, useBlurhash } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account';
interface Props {
@ -58,6 +59,14 @@ export const Avatar: React.FC<Props> = ({
onMouseLeave={handleMouseLeave}
style={style}
>
{(loading || error) && account?.blurhash && (
<Blurhash
hash={account.blurhash}
className='account__avatar__preview'
dummy={!useBlurhash}
/>
)}
{src && !error && (
<img src={src} alt='' onLoad={handleLoad} onError={handleError} />
)}

View File

@ -1,3 +1,4 @@
import { Avatar } from 'mastodon/components/avatar';
import { useHovering } from 'mastodon/hooks/useHovering';
import { autoPlayGif } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account';
@ -19,12 +20,6 @@ export const AvatarOverlay: React.FC<Props> = ({
}) => {
const { hovering, handleMouseEnter, handleMouseLeave } =
useHovering(autoPlayGif);
const accountSrc = hovering
? account?.get('avatar')
: account?.get('avatar_static');
const friendSrc = hovering
? friend?.get('avatar')
: friend?.get('avatar_static');
return (
<div
@ -34,20 +29,19 @@ export const AvatarOverlay: React.FC<Props> = ({
onMouseLeave={handleMouseLeave}
>
<div className='account__avatar-overlay-base'>
<div
className='account__avatar'
style={{ width: `${baseSize}px`, height: `${baseSize}px` }}
>
{accountSrc && <img src={accountSrc} alt={account?.get('acct')} />}
</div>
<Avatar
account={account}
size={baseSize}
animate={hovering || autoPlayGif}
/>
</div>
<div className='account__avatar-overlay-overlay'>
<div
className='account__avatar'
style={{ width: `${overlaySize}px`, height: `${overlaySize}px` }}
>
{friendSrc && <img src={friendSrc} alt={friend?.get('acct')} />}
</div>
<Avatar
account={friend}
size={overlaySize}
animate={hovering || autoPlayGif}
/>
</div>
</div>
);

View File

@ -74,9 +74,9 @@ export default class MediaAttachments extends ImmutablePureComponent {
width={width}
height={height}
poster={audio.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={audio.getIn(['meta', 'colors', 'background'])}
foregroundColor={audio.getIn(['meta', 'colors', 'foreground'])}
accentColor={audio.getIn(['meta', 'colors', 'accent'])}
backgroundColor={audio.getIn(['meta', 'colors', 'background']) ?? status.getIn(['account', 'meta', 'colors', 'background'])}
foregroundColor={audio.getIn(['meta', 'colors', 'foreground']) ?? status.getIn(['account', 'meta', 'colors', 'foreground'])}
accentColor={audio.getIn(['meta', 'colors', 'accent']) ?? status.getIn(['account', 'meta', 'colors', 'accent'])}
duration={audio.getIn(['meta', 'original', 'duration'], 0)}
/>
)}

View File

@ -480,9 +480,9 @@ class Status extends ImmutablePureComponent {
alt={description}
lang={language}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background']) ?? status.getIn(['account', 'meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground']) ?? status.getIn(['account', 'meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent']) ?? status.getIn(['account', 'meta', 'colors', 'accent'])}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
sensitive={status.get('sensitive')}

View File

@ -218,12 +218,19 @@ const Preview: React.FC<{
}
duration={media.getIn(['meta', 'original', 'duration'], 0) as number}
backgroundColor={
media.getIn(['meta', 'colors', 'background']) as string
(media.getIn(['meta', 'colors', 'background']) as
| string
| undefined) ?? account?.meta.colors?.background
}
foregroundColor={
media.getIn(['meta', 'colors', 'foreground']) as string
(media.getIn(['meta', 'colors', 'foreground']) as
| string
| undefined) ?? account?.meta.colors?.foreground
}
accentColor={
(media.getIn(['meta', 'colors', 'accent']) as string | undefined) ??
account?.meta.colors?.accent
}
accentColor={media.getIn(['meta', 'colors', 'accent']) as string}
editable
/>
);

View File

@ -193,9 +193,18 @@ export const DetailedStatus: React.FC<{
status.getIn(['account', 'avatar_static'])
}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
backgroundColor={
attachment.getIn(['meta', 'colors', 'background']) ??
status.getIn(['account', 'meta', 'colors', 'background'])
}
foregroundColor={
attachment.getIn(['meta', 'colors', 'foreground']) ??
status.getIn(['account', 'meta', 'colors', 'foreground'])
}
accentColor={
attachment.getIn(['meta', 'colors', 'accent']) ??
status.getIn(['account', 'meta', 'colors', 'accent'])
}
sensitive={status.get('sensitive')}
visible={showMedia}
blurhash={attachment.get('blurhash')}

View File

@ -18,8 +18,8 @@ const AudioModal: React.FC<{
}> = ({ media, statusId, options, onClose, onChangeBackgroundColor }) => {
const status = useAppSelector((state) => state.statuses.get(statusId));
const accountId = status?.get('account') as string | undefined;
const accountStaticAvatar = useAppSelector((state) =>
accountId ? state.accounts.get(accountId)?.avatar_static : undefined,
const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined,
);
useEffect(() => {
@ -47,16 +47,24 @@ const AudioModal: React.FC<{
alt={description}
lang={language}
poster={
(media.get('preview_url') as string | null) ?? accountStaticAvatar
(media.get('preview_url') as string | null) ??
account?.avatar_static
}
duration={media.getIn(['meta', 'original', 'duration'], 0) as number}
backgroundColor={
media.getIn(['meta', 'colors', 'background']) as string
(media.getIn(['meta', 'colors', 'background']) as
| string
| undefined) ?? account?.meta.colors?.background
}
foregroundColor={
media.getIn(['meta', 'colors', 'foreground']) as string
(media.getIn(['meta', 'colors', 'foreground']) as
| string
| undefined) ?? account?.meta.colors?.foreground
}
accentColor={
(media.getIn(['meta', 'colors', 'accent']) as string | undefined) ??
account?.meta.colors?.accent
}
accentColor={media.getIn(['meta', 'colors', 'accent']) as string}
startPlaying={options.autoPlay}
/>
</div>

View File

@ -94,6 +94,8 @@ export const accountDefaultValues: AccountShape = {
limited: false,
moved: null,
hide_collections: false,
blurhash: '',
meta: {},
// This comes from `ApiMutedAccountJSON`, but we should eventually
// store that in a different object.
mute_expires_at: null,
@ -144,5 +146,10 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
),
note_emojified: emojify(accountJSON.note, emojiMap),
note_plain: unescapeHTML(accountJSON.note),
url:
accountJSON.url.startsWith('http://') ||
accountJSON.url.startsWith('https://')
? accountJSON.url
: accountJSON.uri,
});
}

View File

@ -2100,6 +2100,16 @@ body > [data-popper-placement] {
display: inline-block; // to not show broken images
}
&__preview {
position: absolute;
top: 0;
inset-inline-start: 0;
width: 100%;
height: 100%;
border-radius: var(--avatar-border-radius);
object-fit: cover;
}
&--loading {
background-color: var(--surface-background-color);
}

View File

@ -15,13 +15,15 @@ class ActivityPub::Parser::MediaAttachmentParser
end
def remote_url
Addressable::URI.parse(@json['url'])&.normalize&.to_s
url = Addressable::URI.parse(@json['url'])&.normalize&.to_s
url unless unsupported_uri_scheme?(url)
rescue Addressable::URI::InvalidURIError
nil
end
def thumbnail_remote_url
Addressable::URI.parse(@json['icon'].is_a?(Hash) ? @json['icon']['url'] : @json['icon'])&.normalize&.to_s
url = Addressable::URI.parse(@json['icon'].is_a?(Hash) ? @json['icon']['url'] : @json['icon'])&.normalize&.to_s
url unless unsupported_uri_scheme?(url)
rescue Addressable::URI::InvalidURIError
nil
end

View File

@ -29,7 +29,10 @@ class ActivityPub::Parser::StatusParser
end
def url
url_to_href(@object['url'], 'text/html') if @object['url'].present?
return if @object['url'].blank?
url = url_to_href(@object['url'], 'text/html')
url unless unsupported_uri_scheme?(url)
end
def text

View File

@ -4,6 +4,7 @@ require 'singleton'
class ActivityPub::TagManager
include Singleton
include JsonLdHelper
include RoutingHelper
CONTEXT = 'https://www.w3.org/ns/activitystreams'
@ -17,7 +18,7 @@ class ActivityPub::TagManager
end
def url_for(target)
return target.url if target.respond_to?(:local?) && !target.local?
return unsupported_uri_scheme?(target.url) ? nil : target.url if target.respond_to?(:local?) && !target.local?
return unless target.respond_to?(:object_type)

View File

@ -5,52 +5,54 @@
# Table name: accounts
#
# id :bigint(8) not null, primary key
# username :string default(""), not null
# domain :string
# private_key :text
# public_key :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# note :text default(""), not null
# display_name :string default(""), not null
# uri :string default(""), not null
# url :string
# avatar_file_name :string
# actor_type :string
# also_known_as :string is an Array
# attribution_domains :string default([]), is an Array
# avatar_content_type :string
# avatar_file_name :string
# avatar_file_size :integer
# avatar_updated_at :datetime
# header_file_name :string
# header_content_type :string
# header_file_size :integer
# header_updated_at :datetime
# avatar_remote_url :string
# locked :boolean default(FALSE), not null
# header_remote_url :string default(""), not null
# last_webfingered_at :datetime
# inbox_url :string default(""), not null
# outbox_url :string default(""), not null
# shared_inbox_url :string default(""), not null
# followers_url :string default(""), not null
# protocol :integer default("ostatus"), not null
# memorial :boolean default(FALSE), not null
# moved_to_account_id :bigint(8)
# avatar_storage_schema_version :integer
# avatar_updated_at :datetime
# blurhash :string
# discoverable :boolean
# display_name :string default(""), not null
# domain :string
# featured_collection_url :string
# fields :jsonb
# actor_type :string
# discoverable :boolean
# also_known_as :string is an Array
# followers_url :string default(""), not null
# header_content_type :string
# header_file_name :string
# header_file_size :integer
# header_remote_url :string default(""), not null
# header_storage_schema_version :integer
# header_updated_at :datetime
# hide_collections :boolean
# inbox_url :string default(""), not null
# indexable :boolean default(FALSE), not null
# last_webfingered_at :datetime
# locked :boolean default(FALSE), not null
# memorial :boolean default(FALSE), not null
# meta :json
# note :text default(""), not null
# outbox_url :string default(""), not null
# private_key :text
# protocol :integer default("ostatus"), not null
# public_key :text default(""), not null
# requested_review_at :datetime
# reviewed_at :datetime
# sensitized_at :datetime
# shared_inbox_url :string default(""), not null
# silenced_at :datetime
# suspended_at :datetime
# hide_collections :boolean
# avatar_storage_schema_version :integer
# header_storage_schema_version :integer
# suspension_origin :integer
# sensitized_at :datetime
# trendable :boolean
# reviewed_at :datetime
# requested_review_at :datetime
# indexable :boolean default(FALSE), not null
# attribution_domains :string default([]), is an Array
# uri :string default(""), not null
# url :string
# username :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# moved_to_account_id :bigint(8)
#
class Account < ApplicationRecord

View File

@ -11,7 +11,13 @@ module Account::Avatar
class_methods do
def avatar_styles(file)
styles = { original: { geometry: "#{AVATAR_GEOMETRY}#", file_geometry_parser: FastGeometryParser } }
styles[:static] = { geometry: "#{AVATAR_GEOMETRY}#", format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser } if file.content_type == 'image/gif'
if file.content_type == 'image/gif'
styles[:static] = { geometry: "#{AVATAR_GEOMETRY}#", format: 'png', convert_options: '-coalesce', file_geometry_parser: FastGeometryParser, blurhash: { x_comp: 3, y_comp: 3 }, extract_colors: { meta_attribute_name: :meta } }
else
styles[:original].merge!(blurhash: { x_comp: 3, y_comp: 3 }, extract_colors: { meta_attribute_name: :meta })
end
styles
end
@ -20,7 +26,7 @@ module Account::Avatar
included do
# Avatar upload
has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, convert_options: { all: '+profile "!icc,*" +set date:modify +set date:create +set date:timestamp' }, processors: [:lazy_thumbnail]
has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, convert_options: { all: '+profile "!icc,*" +set date:modify +set date:create +set date:timestamp' }, processors: [:lazy_thumbnail, :blurhash_transcoder, :color_extractor]
validates_attachment_content_type :avatar, content_type: AVATAR_IMAGE_MIME_TYPES
validates_attachment_size :avatar, less_than: AVATAR_LIMIT
remotable_attachment :avatar, AVATAR_LIMIT, suppress_errors: false

View File

@ -165,7 +165,7 @@ class MediaAttachment < ApplicationRecord
}.freeze
THUMBNAIL_STYLES = {
original: IMAGE_STYLES[:small].freeze,
original: IMAGE_STYLES[:small].merge(extract_colors: { meta_attribute_name: :file_meta }.freeze).freeze,
}.freeze
DEFAULT_STYLES = [:original].freeze

View File

@ -8,7 +8,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
attributes :id, :username, :acct, :display_name, :locked, :bot, :discoverable, :indexable, :group, :created_at,
:note, :url, :uri, :avatar, :avatar_static, :header, :header_static,
:followers_count, :following_count, :statuses_count, :last_status_at, :hide_collections
:followers_count, :following_count, :statuses_count, :last_status_at, :hide_collections, :meta, :blurhash
has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
class AddAvatarMetaToAccounts < ActiveRecord::Migration[8.0]
def change
safety_assured { add_column :accounts, :meta, :json }
add_column :accounts, :blurhash, :string
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_04_28_095029) do
ActiveRecord::Schema[8.0].define(version: 2025_04_30_151215) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
@ -198,6 +198,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_28_095029) do
t.datetime "requested_review_at", precision: nil
t.boolean "indexable", default: false, null: false
t.string "attribution_domains", default: [], array: true
t.json "meta"
t.string "blurhash"
t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true
t.index ["domain", "id"], name: "index_accounts_on_domain_and_id"

View File

@ -59,7 +59,7 @@ services:
web:
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
# build: .
image: ghcr.io/mastodon/mastodon:v4.3.7
image: ghcr.io/mastodon/mastodon:v4.3.8
restart: always
env_file: .env.production
command: bundle exec puma -C config/puma.rb
@ -83,7 +83,7 @@ services:
# build:
# dockerfile: ./streaming/Dockerfile
# context: .
image: ghcr.io/mastodon/mastodon-streaming:v4.3.7
image: ghcr.io/mastodon/mastodon-streaming:v4.3.8
restart: always
env_file: .env.production
command: node ./streaming/index.js
@ -102,7 +102,7 @@ services:
sidekiq:
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
# build: .
image: ghcr.io/mastodon/mastodon:v4.3.7
image: ghcr.io/mastodon/mastodon:v4.3.8
restart: always
env_file: .env.production
command: bundle exec sidekiq

View File

@ -17,7 +17,7 @@ module Mastodon
end
def default_prerelease
'alpha.4'
'alpha.5'
end
def prerelease

View File

@ -3,7 +3,7 @@
module Paperclip
class BlurhashTranscoder < Paperclip::Processor
def make
return @file unless options[:style] == :small || options[:blurhash]
return @file unless options[:blurhash]
width, height, data = blurhash_params
# Guard against segfaults if data has unexpected size

View File

@ -10,6 +10,8 @@ module Paperclip
BINS = 10
def make
return @file unless options.key?(:extract_colors)
background_palette, foreground_palette = Rails.configuration.x.use_vips ? palettes_from_libvips : palettes_from_imagemagick
background_color = background_palette.first || foreground_palette.first
@ -66,7 +68,8 @@ module Paperclip
},
}
attachment.instance.file.instance_write(:meta, (attachment.instance.file.instance_read(:meta) || {}).merge(meta))
meta_attribute_name = options[:extract_colors][:meta_attribute_name] || "#{attachment.name}_meta"
attachment.instance.public_send("#{meta_attribute_name}=", (attachment.instance.public_send(meta_attribute_name) || {}).merge(meta))
@file
rescue Vips::Error => e