mirror of
https://github.com/mastodon/mastodon.git
synced 2025-05-07 20:26:15 +00:00
Compare commits
7 Commits
1dd82671dd
...
2608bc6d71
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2608bc6d71 | ||
![]() |
fbe9728f36 | ||
![]() |
3bbf3e9709 | ||
![]() |
79931bf3ae | ||
![]() |
e5e83ebaed | ||
![]() |
48ed1a38a1 | ||
![]() |
ccb2f9a210 |
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -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)
|
||||
|
|
|
@ -215,6 +215,72 @@ module JsonLdHelper
|
|||
end
|
||||
end
|
||||
|
||||
# Iterate through the pages of an activitypub collection,
|
||||
# returning the collected items and the number of pages that were fetched.
|
||||
#
|
||||
# @param collection_or_uri [String, Hash]
|
||||
# either the URI or an already-fetched AP object
|
||||
# @param max_pages [Integer, nil]
|
||||
# Max pages to fetch, if nil, fetch until no more pages
|
||||
# @param max_items [Integer, nil]
|
||||
# Max items to fetch, if nil, fetch until no more items
|
||||
# @param reference_uri [String, nil]
|
||||
# If not nil, a URI to compare to the collection URI.
|
||||
# If the host of the collection URI does not match the reference URI,
|
||||
# do not fetch the collection page.
|
||||
# @param on_behalf_of [Account, nil]
|
||||
# Sign the request on behalf of the Account, if not nil
|
||||
# @return [Array<Array<Hash>, Integer>, nil]
|
||||
# The collection items and the number of pages fetched
|
||||
def collection_items(collection_or_uri, max_pages: 1, max_items: nil, reference_uri: nil, on_behalf_of: nil)
|
||||
collection = fetch_collection(collection_or_uri, reference_uri: reference_uri, on_behalf_of: on_behalf_of)
|
||||
return unless collection.is_a?(Hash)
|
||||
|
||||
collection = fetch_collection(collection['first'], reference_uri: reference_uri, on_behalf_of: on_behalf_of) if collection['first'].present?
|
||||
return unless collection.is_a?(Hash)
|
||||
|
||||
items = []
|
||||
n_pages = 1
|
||||
while collection.is_a?(Hash)
|
||||
items.concat(as_array(collection_page_items(collection)))
|
||||
|
||||
break if !max_items.nil? && items.size >= max_items
|
||||
break if !max_pages.nil? && n_pages >= max_pages
|
||||
|
||||
collection = collection['next'].present? ? fetch_collection(collection['next'], reference_uri: reference_uri, on_behalf_of: on_behalf_of) : nil
|
||||
n_pages += 1
|
||||
end
|
||||
|
||||
[items, n_pages]
|
||||
end
|
||||
|
||||
def collection_page_items(collection)
|
||||
case collection['type']
|
||||
when 'Collection', 'CollectionPage'
|
||||
collection['items']
|
||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||
collection['orderedItems']
|
||||
end
|
||||
end
|
||||
|
||||
# Fetch a single collection page
|
||||
# To get the whole collection, use collection_items
|
||||
#
|
||||
# @param collection_or_uri [String, Hash]
|
||||
# @param reference_uri [String, nil]
|
||||
# If not nil, a URI to compare to the collection URI.
|
||||
# If the host of the collection URI does not match the reference URI,
|
||||
# do not fetch the collection page.
|
||||
# @param on_behalf_of [Account, nil]
|
||||
# Sign the request on behalf of the Account, if not nil
|
||||
# @return [Hash, nil]
|
||||
def fetch_collection(collection_or_uri, reference_uri: nil, on_behalf_of: nil)
|
||||
return collection_or_uri if collection_or_uri.is_a?(Hash)
|
||||
return if !reference_uri.nil? && non_matching_uri_hosts?(reference_uri, collection_or_uri)
|
||||
|
||||
fetch_resource_without_id_validation(collection_or_uri, on_behalf_of, raise_on_error: :temporary)
|
||||
end
|
||||
|
||||
def valid_activitypub_content_type?(response)
|
||||
return true if response.mime_type == 'application/activity+json'
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -144,5 +144,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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -12,30 +12,12 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService
|
|||
|
||||
return unless supported_context?(@json)
|
||||
|
||||
process_items(collection_items(@json))
|
||||
@items, = collection_items(@json, max_pages: 1, reference_uri: @account.uri, on_behalf_of: local_follower)
|
||||
process_items(@items)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def collection_items(collection)
|
||||
collection = fetch_collection(collection['first']) if collection['first'].present?
|
||||
return unless collection.is_a?(Hash)
|
||||
|
||||
case collection['type']
|
||||
when 'Collection', 'CollectionPage'
|
||||
as_array(collection['items'])
|
||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||
as_array(collection['orderedItems'])
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_collection(collection_or_uri)
|
||||
return collection_or_uri if collection_or_uri.is_a?(Hash)
|
||||
return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
|
||||
|
||||
fetch_resource_without_id_validation(collection_or_uri, local_follower, raise_on_error: :temporary)
|
||||
end
|
||||
|
||||
def process_items(items)
|
||||
return if items.nil?
|
||||
|
||||
|
|
|
@ -11,43 +11,12 @@ class ActivityPub::FetchFeaturedTagsCollectionService < BaseService
|
|||
|
||||
return unless supported_context?(@json)
|
||||
|
||||
process_items(collection_items(@json))
|
||||
@items, = collection_items(@json, max_items: FeaturedTag::LIMIT, reference_uri: @account.uri, on_behalf_of: local_follower)
|
||||
process_items(@items)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def collection_items(collection)
|
||||
all_items = []
|
||||
|
||||
collection = fetch_collection(collection['first']) if collection['first'].present?
|
||||
|
||||
while collection.is_a?(Hash)
|
||||
items = case collection['type']
|
||||
when 'Collection', 'CollectionPage'
|
||||
collection['items']
|
||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||
collection['orderedItems']
|
||||
end
|
||||
|
||||
break if items.blank?
|
||||
|
||||
all_items.concat(items)
|
||||
|
||||
break if all_items.size >= FeaturedTag::LIMIT
|
||||
|
||||
collection = collection['next'].present? ? fetch_collection(collection['next']) : nil
|
||||
end
|
||||
|
||||
all_items
|
||||
end
|
||||
|
||||
def fetch_collection(collection_or_uri)
|
||||
return collection_or_uri if collection_or_uri.is_a?(Hash)
|
||||
return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
|
||||
|
||||
fetch_resource_without_id_validation(collection_or_uri, local_follower, raise_on_error: :temporary)
|
||||
end
|
||||
|
||||
def process_items(items)
|
||||
names = items.filter_map { |item| item['type'] == 'Hashtag' && item['name']&.delete_prefix('#') }.take(FeaturedTag::LIMIT)
|
||||
tags = names.index_by { |name| HashtagNormalizer.new.normalize(name) }
|
||||
|
|
|
@ -8,9 +8,13 @@ class ActivityPub::FetchRepliesService < BaseService
|
|||
|
||||
def call(reference_uri, collection_or_uri, max_pages: 1, allow_synchronous_requests: true, request_id: nil)
|
||||
@reference_uri = reference_uri
|
||||
@allow_synchronous_requests = allow_synchronous_requests
|
||||
return if !allow_synchronous_requests && !collection_or_uri.is_a?(Hash)
|
||||
|
||||
@items, n_pages = collection_items(collection_or_uri, max_pages: max_pages)
|
||||
# if given a prefetched collection while forbidding synchronous requests,
|
||||
# process it and return without fetching additional pages
|
||||
max_pages = 1 if !allow_synchronous_requests && collection_or_uri.is_a?(Hash)
|
||||
|
||||
@items, n_pages = collection_items(collection_or_uri, max_pages: max_pages, max_items: MAX_REPLIES, reference_uri: @reference_uri)
|
||||
return if @items.nil?
|
||||
|
||||
@items = filter_replies(@items)
|
||||
|
@ -21,45 +25,6 @@ class ActivityPub::FetchRepliesService < BaseService
|
|||
|
||||
private
|
||||
|
||||
def collection_items(collection_or_uri, max_pages: 1)
|
||||
collection = fetch_collection(collection_or_uri)
|
||||
return unless collection.is_a?(Hash)
|
||||
|
||||
collection = fetch_collection(collection['first']) if collection['first'].present?
|
||||
return unless collection.is_a?(Hash)
|
||||
|
||||
items = []
|
||||
n_pages = 1
|
||||
while collection.is_a?(Hash)
|
||||
items.concat(as_array(collection_page_items(collection)))
|
||||
|
||||
break if items.size >= MAX_REPLIES
|
||||
break if n_pages >= max_pages
|
||||
|
||||
collection = collection['next'].present? ? fetch_collection(collection['next']) : nil
|
||||
n_pages += 1
|
||||
end
|
||||
|
||||
[items, n_pages]
|
||||
end
|
||||
|
||||
def collection_page_items(collection)
|
||||
case collection['type']
|
||||
when 'Collection', 'CollectionPage'
|
||||
collection['items']
|
||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||
collection['orderedItems']
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_collection(collection_or_uri)
|
||||
return collection_or_uri if collection_or_uri.is_a?(Hash)
|
||||
return unless @allow_synchronous_requests
|
||||
return if non_matching_uri_hosts?(@reference_uri, collection_or_uri)
|
||||
|
||||
fetch_resource_without_id_validation(collection_or_uri, nil, raise_on_error: :temporary)
|
||||
end
|
||||
|
||||
def filter_replies(items)
|
||||
# Only fetch replies to the same server as the original status to avoid
|
||||
# amplification attacks.
|
||||
|
|
|
@ -63,10 +63,10 @@ class ActivityPub::SynchronizeFollowersService < BaseService
|
|||
|
||||
# Only returns true if the whole collection has been processed
|
||||
def process_collection!(collection_uri, max_pages: MAX_COLLECTION_PAGES)
|
||||
collection = fetch_collection(collection_uri)
|
||||
collection = fetch_collection(collection_uri, reference_uri: @account.uri)
|
||||
return false unless collection.is_a?(Hash)
|
||||
|
||||
collection = fetch_collection(collection['first']) if collection['first'].present?
|
||||
collection = fetch_collection(collection['first'], reference_uri: @account.uri) if collection['first'].present?
|
||||
|
||||
while collection.is_a?(Hash)
|
||||
process_page!(as_array(collection_page_items(collection)))
|
||||
|
@ -81,20 +81,4 @@ class ActivityPub::SynchronizeFollowersService < BaseService
|
|||
|
||||
false
|
||||
end
|
||||
|
||||
def collection_page_items(collection)
|
||||
case collection['type']
|
||||
when 'Collection', 'CollectionPage'
|
||||
collection['items']
|
||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||
collection['orderedItems']
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_collection(collection_or_uri)
|
||||
return collection_or_uri if collection_or_uri.is_a?(Hash)
|
||||
return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
|
||||
|
||||
fetch_resource_without_id_validation(collection_or_uri, nil, raise_on_error: :temporary)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -17,7 +17,7 @@ module Mastodon
|
|||
end
|
||||
|
||||
def default_prerelease
|
||||
'alpha.4'
|
||||
'alpha.5'
|
||||
end
|
||||
|
||||
def prerelease
|
||||
|
|
Loading…
Reference in New Issue
Block a user