Compare commits

..

23 Commits

Author SHA1 Message Date
Claire
b60edf59d8 Merge pull request #3337 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to ef4d722d6a to stable-4.4
2026-01-07 15:22:45 +01:00
Claire
3a87a63abf Merge commit 'ef4d722d6af8e0e9a25e30bb6c33aa0e14f6951f' into glitch-soc/merge-4.4 2026-01-07 14:46:45 +01:00
Claire
ef4d722d6a Bump version to v4.4.11 (#37410) 2026-01-07 14:45:02 +01:00
Claire
68e30985ca Merge commit from fork 2026-01-07 14:15:13 +01:00
Claire
1702290786 Merge commit from fork 2026-01-07 14:14:42 +01:00
Claire
abb3a02ce2 Merge pull request #3325 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to f5890040e1
2025-12-28 19:47:33 +01:00
Claire
25c79f526a Merge commit 'f5890040e12169e2ec27be68033ab0f1ad744c6e' into glitch-soc/merge-4.4 2025-12-28 11:19:25 +01:00
Claire
f5890040e1 Fix mentions of domain-blocked users being processed (#37257) 2025-12-19 11:00:14 +01:00
Claire
740f262e38 Change HTTP Signature verification status from 401 to 503 on temporary failure to get remote actor (#37221) 2025-12-19 11:00:14 +01:00
Claire
4d2b6795a4 Fix stable-4.4 branches being built with the latest tag (#37170) 2025-12-08 18:30:42 +01:00
Claire
6558a1e07a Merge pull request #3309 from ClearlyClaire/glitch-soc/merge-4.4
Merge upstream changes up to d5f12debe0 into stable-4.4
2025-12-08 17:29:58 +01:00
Echo
b64101ee64 [Glitch] Fixes YouTube embeds
Port 9bc9ebc59e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-08 16:56:28 +01:00
Bruno Viveiros
0bde273b3d [Glitch] fix: YouTube iframe being able to start at a defined time
Port bdff970a5e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-08 16:56:20 +01:00
Claire
22fe977ffe [Glitch] Fix error handling when re-fetching already-known statuses
Port edfbcfb3f5 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-08 16:55:59 +01:00
Claire
8e945bef2a Merge commit 'd5f12debe0fdd9033f3ef7ee84f9ddf366ffc2e3' into glitch-soc/merge-4.4 2025-12-08 16:54:12 +01:00
Claire
d5f12debe0 Bump version to v4.4.10 (#37162) 2025-12-08 16:20:18 +01:00
Claire
5d0ec718fd Merge commit from fork 2025-12-08 15:44:08 +01:00
Echo
c7aa312307 Fixes YouTube embeds (#37126) 2025-12-05 11:15:08 +01:00
Bruno Viveiros
dc1d4eda7c fix: YouTube iframe being able to start at a defined time (#26584) 2025-12-05 11:15:08 +01:00
Claire
931a29b4f3 Fix streamed quoted polls not being hydrated correctly (#37118) 2025-12-05 11:15:08 +01:00
Claire
99b2307350 Fix error handling when re-fetching already-known statuses (#37077) 2025-12-05 11:15:08 +01:00
Claire
375f2e6ebf Increase HTTP read timeout for expensive S3 batch delete operation (#37004) 2025-12-05 11:15:08 +01:00
Matt Jankowski
f0a1da78ba Suggest ES image version 7.17.29 in docker compose (#36972) 2025-12-05 11:15:08 +01:00
34 changed files with 220 additions and 76 deletions

View File

@@ -20,7 +20,7 @@ jobs:
# Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release
flavor: |
latest=${{ startsWith(github.ref, 'refs/tags/v4.4.') }}
latest=false
tags: |
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
@@ -37,7 +37,7 @@ jobs:
# Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release
flavor: |
latest=${{ startsWith(github.ref, 'refs/tags/v4.4.') }}
latest=false
tags: |
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}

View File

@@ -2,6 +2,35 @@
All notable changes to this project will be documented in this file.
## [4.4.11] - 2026-01-07
### Security
- Fix SSRF protection bypass ([GHSA](https://github.com/mastodon/mastodon/security/advisories/GHSA-xfrj-c749-jxxq))
- Fix missing ownership check in severed relationships controller ([GHSA](https://github.com/mastodon/mastodon/security/advisories/GHSA-ww85-x9cp-5v24))
### Changed
- Change HTTP Signature verification status from 401 to 503 on temporary failure to get remote actor (#37221 by @ClearlyClaire)
### Fixed
- Fix mentions of domain-blocked users being processed (#37257 by @ClearlyClaire)
## [4.4.10] - 2025-12-08
### Security
- Fix inconsistent error handling leaking information on existence of private posts ([GHSA-gwhw-gcjx-72v8](https://github.com/mastodon/mastodon/security/advisories/GHSA-gwhw-gcjx-72v8))
### Fixed
- Fix YouTube embeds by sending referer (#37126 by @ChaosExAnima)
- Fix YouTube iframe not being able to start at a defined time (#26584 by @BrunoViveiros)
- Fix streamed quoted polls not being hydrated correctly (#37118 by @ClearlyClaire)
- Fix error handling when re-fetching already-known statuses (#37077 by @ClearlyClaire)
- Fix known expensive S3 batch delete operation failing because of short timeouts (#37004 by @ClearlyClaire)
## [4.4.9] - 2025-11-20
### Fixed

View File

@@ -22,7 +22,7 @@ class ActivityPub::LikesController < ActivityPub::BaseController
def set_status
@status = @account.statuses.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end

View File

@@ -25,7 +25,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
def set_status
@status = @account.statuses.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end

View File

@@ -22,7 +22,7 @@ class ActivityPub::SharesController < ActivityPub::BaseController
def set_status
@status = @account.statuses.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end

View File

@@ -17,7 +17,7 @@ class Api::V1::Polls::VotesController < Api::BaseController
def set_poll
@poll = Poll.find(params[:poll_id])
authorize @poll.status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end

View File

@@ -17,7 +17,7 @@ class Api::V1::PollsController < Api::BaseController
def set_poll
@poll = Poll.find(params[:id])
authorize @poll.status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end

View File

@@ -10,7 +10,7 @@ class Api::V1::Statuses::BaseController < Api::BaseController
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end
end

View File

@@ -23,7 +23,7 @@ class Api::V1::Statuses::BookmarksController < Api::V1::Statuses::BaseController
bookmark&.destroy!
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, bookmarks_map: { @status.id => false })
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end
end

View File

@@ -25,7 +25,7 @@ class Api::V1::Statuses::FavouritesController < Api::V1::Statuses::BaseControlle
relationships = StatusRelationshipsPresenter.new([@status], current_account.id, favourites_map: { @status.id => false }, attributes_map: { @status.id => { favourites_count: count } })
render json: @status, serializer: REST::StatusSerializer, relationships: relationships
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end
end

View File

@@ -36,7 +36,7 @@ class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
relationships = StatusRelationshipsPresenter.new([@status], current_account.id, reblogs_map: { @reblog.id => false }, attributes_map: { @reblog.id => { reblogs_count: count } })
render json: @reblog, serializer: REST::StatusSerializer, relationships: relationships
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end
@@ -45,7 +45,7 @@ class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
def set_reblog
@reblog = Status.find(params[:status_id])
authorize @reblog, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end

View File

@@ -129,7 +129,7 @@ class Api::V1::StatusesController < Api::BaseController
def set_status
@status = Status.find(params[:id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end

View File

@@ -30,7 +30,7 @@ class Api::Web::EmbedsController < Api::Web::BaseController
def set_status
@status = Status.find(params[:id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end
end

View File

@@ -21,7 +21,7 @@ class AuthorizeInteractionsController < ApplicationController
def set_resource
@resource = located_resource
authorize(@resource, :show?) if @resource.is_a?(Status)
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end

View File

@@ -70,10 +70,13 @@ module SignatureVerification
rescue Mastodon::SignatureVerificationError => e
fail_with! e.message
rescue *Mastodon::HTTP_CONNECTION_ERRORS => e
@signature_verification_failure_code ||= 503
fail_with! "Failed to fetch remote data: #{e.message}"
rescue Mastodon::UnexpectedResponseError
@signature_verification_failure_code ||= 503
fail_with! 'Failed to fetch remote data (got unexpected reply from server)'
rescue Stoplight::Error::RedLight
@signature_verification_failure_code ||= 503
fail_with! 'Fetching attempt skipped because of recent connection failure'
end

View File

@@ -34,7 +34,7 @@ class MediaController < ApplicationController
def verify_permitted_status!
authorize @media_attachment.status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end

View File

@@ -26,7 +26,7 @@ class SeveredRelationshipsController < ApplicationController
private
def set_event
@event = AccountRelationshipSeveranceEvent.find(params[:id])
@event = AccountRelationshipSeveranceEvent.where(account: current_account).find(params[:id])
end
def following_data

View File

@@ -59,7 +59,7 @@ class StatusesController < ApplicationController
def set_status
@status = @account.statuses.find(params[:id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end

View File

@@ -78,6 +78,8 @@ export function fetchStatus(id, {
dispatch(fetchStatusSuccess(skipLoading));
}).catch(error => {
dispatch(fetchStatusFail(id, error, skipLoading, parentQuotePostId));
if (error.status === 404)
dispatch(deleteFromTimelines(id));
});
};
}

View File

@@ -26,18 +26,23 @@ const getHostname = url => {
const domParser = new DOMParser();
const addAutoPlay = html => {
const handleIframeUrl = (html, url, providerName) => {
const document = domParser.parseFromString(html, 'text/html').documentElement;
const iframe = document.querySelector('iframe');
const startTime = new URL(url).searchParams.get('t')
if (iframe) {
if (iframe.src.indexOf('?') !== -1) {
iframe.src += '&';
} else {
iframe.src += '?';
const iframeUrl = new URL(iframe.src)
iframeUrl.searchParams.set('autoplay', 1)
iframeUrl.searchParams.set('auto_play', 1)
if (providerName === 'YouTube') {
iframeUrl.searchParams.set('start', startTime || '');
iframe.referrerPolicy = 'strict-origin-when-cross-origin';
}
iframe.src += 'autoplay=1&auto_play=1';
iframe.src = iframeUrl.href
// DOM parser creates html/body elements around original HTML fragment,
// so we need to get innerHTML out of the body and not the entire document
@@ -103,7 +108,7 @@ export default class Card extends PureComponent {
renderVideo () {
const { card } = this.props;
const content = { __html: addAutoPlay(card.get('html')) };
const content = { __html: handleIframeUrl(card.get('html'), card.get('url'), card.get('provider_name')) };
return (
<div

View File

@@ -64,6 +64,10 @@ const statusTranslateUndo = (state, id) => {
});
};
const removeStatusStub = (state, id) => {
return state.getIn([id, 'id']) ? state.deleteIn([id, 'isLoading']) : state.delete(id);
}
/** @type {ImmutableMap<string, import('flavours/glitch/models/status').Status>} */
const initialState = ImmutableMap();
@@ -75,11 +79,10 @@ export default function statuses(state = initialState, action) {
return state.setIn([action.id, 'isLoading'], true);
case STATUS_FETCH_FAIL: {
if (action.parentQuotePostId && action.error.status === 404) {
return state
.delete(action.id)
return removeStatusStub(state, action.id)
.setIn([action.parentQuotePostId, 'quote', 'state'], 'deleted')
} else {
return state.delete(action.id);
return removeStatusStub(state, action.id);
}
}
case STATUS_IMPORT:

View File

@@ -78,6 +78,8 @@ export function fetchStatus(id, {
dispatch(fetchStatusSuccess(skipLoading));
}).catch(error => {
dispatch(fetchStatusFail(id, error, skipLoading, parentQuotePostId));
if (error.status === 404)
dispatch(deleteFromTimelines(id));
});
};
}

View File

@@ -37,18 +37,23 @@ const getHostname = url => {
const domParser = new DOMParser();
const addAutoPlay = html => {
const handleIframeUrl = (html, url, providerName) => {
const document = domParser.parseFromString(html, 'text/html').documentElement;
const iframe = document.querySelector('iframe');
const startTime = new URL(url).searchParams.get('t')
if (iframe) {
if (iframe.src.indexOf('?') !== -1) {
iframe.src += '&';
} else {
iframe.src += '?';
const iframeUrl = new URL(iframe.src)
iframeUrl.searchParams.set('autoplay', 1)
iframeUrl.searchParams.set('auto_play', 1)
if (providerName === 'YouTube') {
iframeUrl.searchParams.set('start', startTime || '');
iframe.referrerPolicy = 'strict-origin-when-cross-origin';
}
iframe.src += 'autoplay=1&auto_play=1';
iframe.src = iframeUrl.href
// DOM parser creates html/body elements around original HTML fragment,
// so we need to get innerHTML out of the body and not the entire document
@@ -114,7 +119,7 @@ export default class Card extends PureComponent {
renderVideo () {
const { card } = this.props;
const content = { __html: addAutoPlay(card.get('html')) };
const content = { __html: handleIframeUrl(card.get('html'), card.get('url'), card.get('provider_name')) };
return (
<div

View File

@@ -64,6 +64,10 @@ const statusTranslateUndo = (state, id) => {
});
};
const removeStatusStub = (state, id) => {
return state.getIn([id, 'id']) ? state.deleteIn([id, 'isLoading']) : state.delete(id);
}
/** @type {ImmutableMap<string, import('mastodon/models/status').Status>} */
const initialState = ImmutableMap();
@@ -75,11 +79,10 @@ export default function statuses(state = initialState, action) {
return state.setIn([action.id, 'isLoading'], true);
case STATUS_FETCH_FAIL: {
if (action.parentQuotePostId && action.error.status === 404) {
return state
.delete(action.id)
return removeStatusStub(state, action.id)
.setIn([action.parentQuotePostId, 'quote', 'state'], 'deleted')
} else {
return state.delete(action.id);
return removeStatusStub(state, action.id);
}
}
case STATUS_IMPORT:

View File

@@ -112,10 +112,12 @@ class AttachmentBatch
keys.each_slice(LIMIT) do |keys_slice|
logger.debug { "Deleting #{keys_slice.size} objects" }
bucket.delete_objects(delete: {
objects: keys_slice.map { |key| { key: key } },
quiet: true,
})
with_overridden_timeout(bucket.client, 120) do
bucket.delete_objects(delete: {
objects: keys_slice.map { |key| { key: key } },
quiet: true,
})
end
rescue => e
retries += 1
@@ -134,6 +136,20 @@ class AttachmentBatch
@bucket ||= records.first.public_send(@attachment_names.first).s3_bucket
end
# Currently, the aws-sdk-s3 gem does not offer a way to cleanly override the timeout
# per-request. So we change the client's config instead. As this client will likely
# be re-used for other jobs, restore its original configuration in an `ensure` block.
def with_overridden_timeout(s3_client, longer_read_timeout)
original_timeout = s3_client.config.http_read_timeout
s3_client.config.http_read_timeout = [original_timeout, longer_read_timeout].max
begin
yield
ensure
s3_client.config.http_read_timeout = original_timeout
end
end
def nullified_attributes
@attachment_names.flat_map { |attachment_name| NULLABLE_ATTRIBUTES.map { |attribute| "#{attachment_name}_#{attribute}" } & klass.column_names }.index_with(nil)
end

View File

@@ -1,9 +1,7 @@
# frozen_string_literal: true
module PrivateAddressCheck
module_function
CIDR_LIST = [
IP4_CIDR_LIST = [
IPAddr.new('0.0.0.0/8'), # Current network (only valid as source address)
IPAddr.new('100.64.0.0/10'), # Shared Address Space
IPAddr.new('172.16.0.0/12'), # Private network
@@ -16,6 +14,9 @@ module PrivateAddressCheck
IPAddr.new('224.0.0.0/4'), # IP multicast (former Class D network)
IPAddr.new('240.0.0.0/4'), # Reserved (former Class E network)
IPAddr.new('255.255.255.255'), # Broadcast
].freeze
CIDR_LIST = (IP4_CIDR_LIST + IP4_CIDR_LIST.map(&:ipv4_mapped) + [
IPAddr.new('64:ff9b::/96'), # IPv4/IPv6 translation (RFC 6052)
IPAddr.new('100::/64'), # Discard prefix (RFC 6666)
IPAddr.new('2001::/32'), # Teredo tunneling
@@ -25,7 +26,9 @@ module PrivateAddressCheck
IPAddr.new('2002::/16'), # 6to4
IPAddr.new('fc00::/7'), # Unique local address
IPAddr.new('ff00::/8'), # Multicast
].freeze
]).freeze
module_function
def private_address?(address)
address.private? || address.loopback? || address.link_local? || CIDR_LIST.any? { |cidr| cidr.include?(address) }

View File

@@ -26,12 +26,7 @@ class StatusCacheHydrator
def hydrate_non_reblog_payload(empty_payload, account_id, nested: false)
empty_payload.tap do |payload|
fill_status_payload(payload, @status, account_id, nested:)
if payload[:poll]
payload[:poll][:voted] = @status.account_id == account_id
payload[:poll][:own_votes] = []
end
fill_status_payload(payload, @status, account_id, fresh: !nested, nested:)
end
end
@@ -45,18 +40,7 @@ class StatusCacheHydrator
# used to create the status, we need to hydrate it here too
payload[:reblog][:application] = payload_reblog_application if payload[:reblog][:application].nil? && @status.reblog.account_id == account_id
fill_status_payload(payload[:reblog], @status.reblog, account_id, nested:)
if payload[:reblog][:poll]
if @status.reblog.account_id == account_id
payload[:reblog][:poll][:voted] = true
payload[:reblog][:poll][:own_votes] = []
else
own_votes = PollVote.where(poll_id: @status.reblog.poll_id, account_id: account_id).pluck(:choice)
payload[:reblog][:poll][:voted] = !own_votes.empty?
payload[:reblog][:poll][:own_votes] = own_votes
end
end
fill_status_payload(payload[:reblog], @status.reblog, account_id, fresh: false, nested:)
payload[:filtered] = payload[:reblog][:filtered]
payload[:favourited] = payload[:reblog][:favourited]
@@ -64,7 +48,7 @@ class StatusCacheHydrator
end
end
def fill_status_payload(payload, status, account_id, nested: false)
def fill_status_payload(payload, status, account_id, nested: false, fresh: true)
payload[:favourited] = Favourite.exists?(account_id: account_id, status_id: status.id)
payload[:reblogged] = Status.exists?(account_id: account_id, reblog_of_id: status.id)
payload[:muted] = ConversationMute.exists?(account_id: account_id, conversation_id: status.conversation_id)
@@ -72,6 +56,30 @@ class StatusCacheHydrator
payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: status.id) if status.account_id == account_id
payload[:filtered] = mapped_applied_custom_filter(account_id, status)
payload[:quote] = hydrate_quote_payload(payload[:quote], status.quote, account_id, nested:) if payload[:quote]
if payload[:poll]
if fresh
# If the status is brand new, we don't need to look up votes in database
payload[:poll][:voted] = status.account_id == account_id
payload[:poll][:own_votes] = []
elsif status.account_id == account_id
payload[:poll][:voted] = true
payload[:poll][:own_votes] = []
else
own_votes = PollVote.where(poll_id: status.poll_id, account_id: account_id).pluck(:choice)
payload[:poll][:voted] = !own_votes.empty?
payload[:poll][:own_votes] = own_votes
end
end
# Nested statuses are more likely to have a stale cache
fill_status_stats(payload, status) if nested
end
def fill_status_stats(payload, status)
payload[:replies_count] = status.replies_count
payload[:reblogs_count] = status.untrusted_reblogs_count || status.reblogs_count
payload[:favourites_count] = status.untrusted_favourites_count || status.favourites_count
end
def hydrate_quote_payload(empty_payload, quote, account_id, nested: false)

View File

@@ -71,7 +71,7 @@ class ProcessMentionsService < BaseService
# Make sure we never mention blocked accounts
unless @current_mentions.empty?
mentioned_domains = @current_mentions.filter_map { |m| m.account.domain }.uniq
blocked_domains = Set.new(mentioned_domains.empty? ? [] : AccountDomainBlock.where(account_id: @status.account_id, domain: mentioned_domains))
blocked_domains = Set.new(mentioned_domains.empty? ? [] : AccountDomainBlock.where(account_id: @status.account_id, domain: mentioned_domains).pluck(:domain))
mentioned_account_ids = @current_mentions.map(&:account_id)
blocked_account_ids = Set.new(@status.account.block_relationships.where(target_account_id: mentioned_account_ids).pluck(:target_account_id))

View File

@@ -27,7 +27,7 @@ services:
# es:
# restart: always
# image: docker.elastic.co/elasticsearch/elasticsearch:7.17.4
# image: docker.elastic.co/elasticsearch/elasticsearch:7.17.29
# environment:
# - "ES_JAVA_OPTS=-Xms512m -Xmx512m -Des.enforce.bootstrap.checks=true"
# - "xpack.license.self_generated.type=basic"
@@ -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/glitch-soc/mastodon:v4.4.9
image: ghcr.io/glitch-soc/mastodon:v4.4.11
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/glitch-soc/mastodon-streaming:v4.4.9
image: ghcr.io/glitch-soc/mastodon-streaming:v4.4.11
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/glitch-soc/mastodon:v4.4.9
image: ghcr.io/glitch-soc/mastodon:v4.4.11
restart: always
env_file: .env.production
command: bundle exec sidekiq

View File

@@ -13,7 +13,7 @@ module Mastodon
end
def patch
9
11
end
def default_prerelease

View File

@@ -0,0 +1,20 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe PrivateAddressCheck do
describe 'private_address?' do
it 'returns true for private addresses' do
# rubocop:disable RSpec/ExpectActual
expect(
[
'192.168.1.7',
'0.0.0.0',
'127.0.0.1',
'::ffff:0.0.0.1',
]
).to all satisfy('return true') { |addr| described_class.private_address?(IPAddr.new(addr)) }
# rubocop:enable RSpec/ExpectActual
end
end
end

View File

@@ -164,6 +164,28 @@ RSpec.describe StatusCacheHydrator do
end
end
context 'when the quoted post has a poll authored by the user' do
let(:poll) { Fabricate(:poll, account: account) }
let(:quoted_status) { Fabricate(:status, poll: poll, account: account) }
it 'renders the same attributes as a full render' do
expect(subject).to eql(compare_to_hash)
end
end
context 'when the quoted post has been voted in' do
let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) }
let(:quoted_status) { Fabricate(:status, poll: poll) }
before do
VoteService.new.call(account, poll, [0])
end
it 'renders the same attributes as a full render' do
expect(subject).to eql(compare_to_hash)
end
end
context 'when the quoted post matches account filters' do
let(:quoted_status) { Fabricate(:status, text: 'this toot is about that banned word') }

View File

@@ -3,9 +3,10 @@
require 'rails_helper'
RSpec.describe 'Severed Relationships' do
let(:account_rs_event) { Fabricate :account_relationship_severance_event }
let(:account_rs_event) { Fabricate(:account_relationship_severance_event) }
let(:user) { account_rs_event.account.user }
before { sign_in Fabricate(:user) }
before { sign_in user }
describe 'GET /severed_relationships/:id/following' do
it 'returns a CSV file with correct data' do
@@ -22,6 +23,17 @@ RSpec.describe 'Severed Relationships' do
expect(response.body)
.to include('Account address')
end
context 'when the user is not the subject of the event' do
let(:user) { Fabricate(:user) }
it 'returns a 404' do
get following_severed_relationship_path(account_rs_event, format: :csv)
expect(response)
.to have_http_status(404)
end
end
end
describe 'GET /severed_relationships/:id/followers' do
@@ -39,5 +51,16 @@ RSpec.describe 'Severed Relationships' do
expect(response.body)
.to include('Account address')
end
context 'when the user is not the subject of the event' do
let(:user) { Fabricate(:user) }
it 'returns a 404' do
get followers_severed_relationship_path(account_rs_event, format: :csv)
expect(response)
.to have_http_status(404)
end
end
end
end

View File

@@ -8,9 +8,9 @@ RSpec.describe ProcessMentionsService do
let(:account) { Fabricate(:account, username: 'alice') }
context 'when mentions contain blocked accounts' do
let(:non_blocked_account) { Fabricate(:account) }
let(:individually_blocked_account) { Fabricate(:account) }
let(:domain_blocked_account) { Fabricate(:account, domain: 'evil.com') }
let!(:non_blocked_account) { Fabricate(:account) }
let!(:individually_blocked_account) { Fabricate(:account) }
let!(:domain_blocked_account) { Fabricate(:account, domain: 'evil.com', protocol: :activitypub) }
let(:status) { Fabricate(:status, account: account, text: "Hello @#{non_blocked_account.acct} @#{individually_blocked_account.acct} @#{domain_blocked_account.acct}", visibility: :public) }
before do