Compare commits

...

11 Commits

Author SHA1 Message Date
Jonny Saunders
f59954827e
Merge 22485b9422 into 94bceb8683 2025-07-11 14:05:42 +00:00
Echo
94bceb8683
Expose enabled features to the frontend (#35348)
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
Chromatic / Run Chromatic (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 / ImageMagick tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (3.2) (push) Blocked by required conditions
Ruby Testing / ImageMagick 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-07-11 13:15:22 +00:00
Claire
88b0f3a172
Simplify DatabaseViewRecord.refresh (#35252) 2025-07-11 08:36:05 +00:00
github-actions[bot]
b69b5ba775
New Crowdin Translations (automated) (#35344)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-07-11 08:14:39 +00:00
sneakers-the-rat
22485b9422
class attr access 2025-05-05 13:44:41 -07:00
sneakers-the-rat
f9600fcbd3
bump limits 2025-05-05 13:44:41 -07:00
sneakers-the-rat
849c0500b0
filter for only public and unlisted posts 2025-05-05 13:44:41 -07:00
sneakers-the-rat
e489b9e34d
port account backfill implementation from #32634 2025-05-05 13:44:41 -07:00
sneakers-the-rat
e5e83ebaed
apply changes from #34610 2025-05-05 13:39:49 -07:00
sneakers-the-rat
48ed1a38a1
oh right ruby uses double && for logical and 2025-05-05 13:36:49 -07:00
sneakers-the-rat
ccb2f9a210
consolidate collection handling in jsonld helper 2025-05-05 13:36:46 -07:00
22 changed files with 341 additions and 130 deletions

View File

@ -109,3 +109,17 @@ FETCH_REPLIES_MAX_SINGLE=500
# Max number of replies Collection pages to fetch - total # Max number of replies Collection pages to fetch - total
FETCH_REPLIES_MAX_PAGES=500 FETCH_REPLIES_MAX_PAGES=500
# Account Backfill Behavior
# --------------------------
# When the first person from your instance follows a remote account,
# backfill their most recent n statuses.
# (default: true if unset, set explicitly to ``false`` to disable)
ACCOUNT_BACKFILL_ENABLED=true
# Max statuses to fetch when backfilling a new account
ACCOUNT_BACKFILL_MAX_STATUSES=1000
# Max number of replies Collection pages to fetch
ACCOUNT_BACKFILL_MAX_PAGES=200

View File

@ -226,6 +226,72 @@ module JsonLdHelper
end end
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) def valid_activitypub_content_type?(response)
return true if response.mime_type == 'application/activity+json' return true if response.mime_type == 'application/activity+json'

View File

@ -1,6 +1,5 @@
// @ts-check // @ts-check
/** /**
* @typedef {[code: string, name: string, localName: string]} InitialStateLanguage * @typedef {[code: string, name: string, localName: string]} InitialStateLanguage
*/ */
@ -64,6 +63,7 @@
* @property {boolean=} critical_updates_pending * @property {boolean=} critical_updates_pending
* @property {InitialStateMeta} meta * @property {InitialStateMeta} meta
* @property {Role?} role * @property {Role?} role
* @property {string[]} features
*/ */
const element = document.getElementById('initial-state'); const element = document.getElementById('initial-state');
@ -140,4 +140,12 @@ export function getAccessToken() {
return getMeta('access_token'); return getMeta('access_token');
} }
/**
* @param {string} feature
* @returns {boolean}
*/
export function isFeatureEnabled(feature) {
return initialState?.features?.includes(feature) || false;
}
export default initialState; export default initialState;

View File

@ -219,6 +219,9 @@
"confirmations.delete_list.confirm": "Elimina", "confirmations.delete_list.confirm": "Elimina",
"confirmations.delete_list.message": "Segur que vols suprimir permanentment aquesta llista?", "confirmations.delete_list.message": "Segur que vols suprimir permanentment aquesta llista?",
"confirmations.delete_list.title": "Eliminar la llista?", "confirmations.delete_list.title": "Eliminar la llista?",
"confirmations.discard_draft.confirm": "Descarta i continua",
"confirmations.discard_draft.edit.cancel": "Continua l'edició",
"confirmations.discard_draft.post.cancel": "Reprendre l'esborrany",
"confirmations.discard_edit_media.confirm": "Descarta", "confirmations.discard_edit_media.confirm": "Descarta",
"confirmations.discard_edit_media.message": "Tens canvis no desats en la descripció del contingut o en la previsualització, els vols descartar?", "confirmations.discard_edit_media.message": "Tens canvis no desats en la descripció del contingut o en la previsualització, els vols descartar?",
"confirmations.follow_to_list.confirm": "Seguir i afegir a una llista", "confirmations.follow_to_list.confirm": "Seguir i afegir a una llista",
@ -792,6 +795,7 @@
"report_notification.categories.violation": "Violació de norma", "report_notification.categories.violation": "Violació de norma",
"report_notification.categories.violation_sentence": "violació de normes", "report_notification.categories.violation_sentence": "violació de normes",
"report_notification.open": "Obre l'informe", "report_notification.open": "Obre l'informe",
"search.clear": "Esborra la cerca",
"search.no_recent_searches": "No hi ha cerques recents", "search.no_recent_searches": "No hi ha cerques recents",
"search.placeholder": "Cerca", "search.placeholder": "Cerca",
"search.quick_action.account_search": "Perfils coincidint amb {x}", "search.quick_action.account_search": "Perfils coincidint amb {x}",

View File

@ -572,7 +572,7 @@
"navigation_bar.mutes": "Skjulte brugere", "navigation_bar.mutes": "Skjulte brugere",
"navigation_bar.opened_in_classic_interface": "Indlæg, konti og visse andre sider åbnes som standard i den klassiske webgrænseflade.", "navigation_bar.opened_in_classic_interface": "Indlæg, konti og visse andre sider åbnes som standard i den klassiske webgrænseflade.",
"navigation_bar.preferences": "Præferencer", "navigation_bar.preferences": "Præferencer",
"navigation_bar.privacy_and_reach": "Fortrolighed og udbredelse", "navigation_bar.privacy_and_reach": "Fortrolighed og rækkevidde",
"navigation_bar.search": "Søg", "navigation_bar.search": "Søg",
"navigation_bar.search_trends": "Søg/Trender", "navigation_bar.search_trends": "Søg/Trender",
"navigation_panel.collapse_followed_tags": "Sammenfold menuen Fulgte hashtags", "navigation_panel.collapse_followed_tags": "Sammenfold menuen Fulgte hashtags",

View File

@ -10,12 +10,6 @@ module DatabaseViewRecord
concurrently: true, concurrently: true,
cascade: false cascade: false
) )
rescue ActiveRecord::StatementInvalid
Scenic.database.refresh_materialized_view(
table_name,
concurrently: false,
cascade: false
)
end end
end end

View File

@ -32,6 +32,7 @@ class FollowRequest < ApplicationRecord
validates :languages, language: true validates :languages, language: true
def authorize! def authorize!
is_first_follow = first_follow?
follow = account.follow!(target_account, reblogs: show_reblogs, notify: notify, languages: languages, uri: uri, bypass_limit: true) follow = account.follow!(target_account, reblogs: show_reblogs, notify: notify, languages: languages, uri: uri, bypass_limit: true)
if account.local? if account.local?
@ -40,6 +41,7 @@ class FollowRequest < ApplicationRecord
MergeWorker.push_bulk(List.where(account: account).joins(:list_accounts).where(list_accounts: { account_id: target_account.id }).pluck(:id)) do |list_id| MergeWorker.push_bulk(List.where(account: account).joins(:list_accounts).where(list_accounts: { account_id: target_account.id }).pluck(:id)) do |list_id|
[target_account.id, list_id, 'list'] [target_account.id, list_id, 'list']
end end
ActivityPub::AccountBackfillWorker.perform_async(target_account.id) if is_first_follow & ActivityPub::AccountBackfillService::ENABLED
end end
destroy! destroy!
@ -51,6 +53,10 @@ class FollowRequest < ApplicationRecord
false # Force uri_for to use uri attribute false # Force uri_for to use uri attribute
end end
def first_follow?
!target_account.followers.local.exists?
end
before_validation :set_uri, only: :create before_validation :set_uri, only: :create
after_commit :invalidate_follow_recommendations_cache after_commit :invalidate_follow_recommendations_cache

View File

@ -5,7 +5,7 @@ class InitialStateSerializer < ActiveModel::Serializer
attributes :meta, :compose, :accounts, attributes :meta, :compose, :accounts,
:media_attachments, :settings, :media_attachments, :settings,
:languages :languages, :features
attribute :critical_updates_pending, if: -> { object&.role&.can?(:view_devops) && SoftwareUpdate.check_enabled? } attribute :critical_updates_pending, if: -> { object&.role&.can?(:view_devops) && SoftwareUpdate.check_enabled? }
@ -85,6 +85,10 @@ class InitialStateSerializer < ActiveModel::Serializer
LanguagesHelper::SUPPORTED_LOCALES.map { |(key, value)| [key, value[0], value[1]] } LanguagesHelper::SUPPORTED_LOCALES.map { |(key, value)| [key, value[0], value[1]] }
end end
def features
Mastodon::Feature.enabled_features
end
private private
def default_meta_store def default_meta_store

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
class ActivityPub::AccountBackfillService < BaseService
include JsonLdHelper
ENABLED = ENV['ACCOUNT_BACKFILL_ENABLED'].nil? || ENV['ACCOUNT_BACKFILL_ENABLED'] == 'true'
MAX_STATUSES = (ENV['ACCOUNT_BACKFILL_MAX_STATUSES'] || 1000).to_i
MAX_PAGES = (ENV['ACCOUNT_BACKFILL_MAX_PAGES'] || 200).to_i
def call(account, on_behalf_of: nil, request_id: nil)
return unless ENABLED
@account = account
return if @account.nil? || @account.outbox_url.nil?
@items, = collection_items(@account.outbox_url, max_items: MAX_STATUSES, max_pages: MAX_PAGES, on_behalf_of: on_behalf_of)
@items = filter_items(@items)
return if @items.nil?
on_behalf_of_id = on_behalf_of&.id
FetchReplyWorker.push_bulk(@items) do |status_uri_or_body|
if status_uri_or_body.is_a?(Hash) && status_uri_or_body.key?('object') && status_uri_or_body.key?('id')
# Re-add the minimally-acceptable @context, which gets stripped because this object comes inside a collection
status_uri_or_body['@context'] = ActivityPub::TagManager::CONTEXT unless status_uri_or_body.key?('@context')
[status_uri_or_body['id'], { prefetched_body: status_uri_or_body, request_id: request_id, on_behalf_of: on_behalf_of_id }]
else
[status_uri_or_body, { request_id: request_id, on_behalf_of: on_behalf_of_id }]
end
end
@items
end
private
# Reject any non-public statuses.
# Since our request may have been signed on behalf of the follower,
# we may have received followers-only statuses.
#
# Formally, a followers-only status is addressed to the account's followers collection.
# We were not in that collection at the time that the post was made,
# so followers-only statuses fetched by backfilling are not addressed to us.
# Public and unlisted statuses are send to the activitystreams "Public" entity.
# We are part of the public, so those posts *are* addressed to us.
#
# @param items [Array<Hash>]
# @return [Array<Hash>]
def filter_items(items)
allowed = [:public, :unlisted]
items.filter { |item| item.is_a?(String) || allowed.include?(ActivityPub::Parser::StatusParser.new(item).visibility) }
end
end

View File

@ -11,30 +11,12 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService
@json = fetch_collection(options[:collection].presence || @account.featured_collection_url) @json = fetch_collection(options[:collection].presence || @account.featured_collection_url)
return if @json.blank? return if @json.blank?
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 end
private 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) def process_items(items)
return if items.nil? return if items.nil?

View File

@ -11,43 +11,12 @@ class ActivityPub::FetchFeaturedTagsCollectionService < BaseService
return unless supported_context?(@json) 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 end
private 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) def process_items(items)
names = items.filter_map { |item| item['type'] == 'Hashtag' && item['name']&.delete_prefix('#') }.take(FeaturedTag::LIMIT) names = items.filter_map { |item| item['type'] == 'Hashtag' && item['name']&.delete_prefix('#') }.take(FeaturedTag::LIMIT)
tags = names.index_by { |name| HashtagNormalizer.new.normalize(name) } tags = names.index_by { |name| HashtagNormalizer.new.normalize(name) }

View File

@ -11,6 +11,9 @@ class ActivityPub::FetchRemoteStatusService < BaseService
def call(uri, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil) def call(uri, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil)
return if domain_not_allowed?(uri) return if domain_not_allowed?(uri)
# load the account if given as an ID
on_behalf_of = Account.find(on_behalf_of) unless on_behalf_of.nil? || on_behalf_of.is_a?(Account)
@request_id = request_id || "#{Time.now.utc.to_i}-status-#{uri}" @request_id = request_id || "#{Time.now.utc.to_i}-status-#{uri}"
@json = if prefetched_body.nil? @json = if prefetched_body.nil?
fetch_status(uri, true, on_behalf_of) fetch_status(uri, true, on_behalf_of)

View File

@ -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) def call(reference_uri, collection_or_uri, max_pages: 1, allow_synchronous_requests: true, request_id: nil)
@reference_uri = reference_uri @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? return if @items.nil?
@items = filter_replies(@items) @items = filter_replies(@items)
@ -21,45 +25,6 @@ class ActivityPub::FetchRepliesService < BaseService
private 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) def filter_replies(items)
# Only fetch replies to the same server as the original status to avoid # Only fetch replies to the same server as the original status to avoid
# amplification attacks. # amplification attacks.

View File

@ -63,10 +63,10 @@ class ActivityPub::SynchronizeFollowersService < BaseService
# Only returns true if the whole collection has been processed # Only returns true if the whole collection has been processed
def process_collection!(collection_uri, max_pages: MAX_COLLECTION_PAGES) 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) 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) while collection.is_a?(Hash)
process_page!(as_array(collection_page_items(collection))) process_page!(as_array(collection_page_items(collection)))
@ -81,20 +81,4 @@ class ActivityPub::SynchronizeFollowersService < BaseService
false false
end 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 end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class ActivityPub::AccountBackfillWorker
include Sidekiq::Worker
include ExponentialBackoff
def perform(account_id, options = {})
account = Account.find(account_id)
return if account.local?
ActivityPub::AccountBackfillService.new.call(account, **options.deep_symbolize_keys)
end
end

View File

@ -578,6 +578,11 @@ ca:
all: Totes all: Totes
limited: Limitades limited: Limitades
title: Moderació title: Moderació
moderation_notes:
create: Afegeix una nota de moderació
created_msg: S'ha creat la nota de moderació d'instància.
destroyed_msg: S'ha esborrat la nota de moderació d'instància.
title: Notes de moderació
private_comment: Comentari privat private_comment: Comentari privat
public_comment: Comentari públic public_comment: Comentari públic
purge: Purga purge: Purga
@ -1339,6 +1344,10 @@ ca:
basic_information: Informació bàsica basic_information: Informació bàsica
hint_html: "<strong>Personalitza el que la gent veu en el teu perfil públic i a prop dels teus tuts..</strong> És més probable que altres persones et segueixin i interaccionin amb tu quan tens emplenat el teu perfil i amb la teva imatge." hint_html: "<strong>Personalitza el que la gent veu en el teu perfil públic i a prop dels teus tuts..</strong> És més probable que altres persones et segueixin i interaccionin amb tu quan tens emplenat el teu perfil i amb la teva imatge."
other: Altres other: Altres
emoji_styles:
auto: Automàtic
native: Nadiu
twemoji: Twemoji
errors: errors:
'400': La sol·licitud que vas emetre no era vàlida o no era correcta. '400': La sol·licitud que vas emetre no era vàlida o no era correcta.
'403': No tens permís per a veure aquesta pàgina. '403': No tens permís per a veure aquesta pàgina.

View File

@ -653,7 +653,7 @@ da:
mark_as_sensitive_description_html: Medierne i det anmeldte indlæg markeres som sensitive, og en advarsel (strike) registreres mhp. eskalering ved evt. fremtidige overtrædelser fra samme konto. mark_as_sensitive_description_html: Medierne i det anmeldte indlæg markeres som sensitive, og en advarsel (strike) registreres mhp. eskalering ved evt. fremtidige overtrædelser fra samme konto.
other_description_html: Se flere muligheder for at kontrollere kontoens adfærd og tilpasse kommunikationen til den anmeldte konto. other_description_html: Se flere muligheder for at kontrollere kontoens adfærd og tilpasse kommunikationen til den anmeldte konto.
resolve_description_html: Ingen foranstaltninger træffes mod den anmeldte konto, ingen advarsel (strike) registreres og anmeldelsen lukkes. resolve_description_html: Ingen foranstaltninger træffes mod den anmeldte konto, ingen advarsel (strike) registreres og anmeldelsen lukkes.
silence_description_html: Kontoen vil kun være synlig for følgerene eller dem, som manuelt slå den op, hvilket markant begrænser dens udbredelse. Kan altid omgøres. Lukker alle indrapporteringer af kontoen. silence_description_html: Kontoen vil kun være synlig for dem, der allerede følger den eller manuelt slår den op, hvilket alvorligt begrænser dens rækkevidde. Kan altid omgøres. Lukker alle indrapporteringer af denne konto.
suspend_description_html: Kontoen inkl. alt indhold utilgængeliggøres og interaktion umuliggøres, og den slettes på et tidspunkt. Kan omgøres inden for 30 dage. Lukker alle indrapporteringer af kontoen. suspend_description_html: Kontoen inkl. alt indhold utilgængeliggøres og interaktion umuliggøres, og den slettes på et tidspunkt. Kan omgøres inden for 30 dage. Lukker alle indrapporteringer af kontoen.
actions_description_html: Afgør, hvilke foranstaltning, der skal træffes for at løse denne anmeldelse. Ved en straffende foranstaltning mod den anmeldte konto, fremsendes en e-mailnotifikation, undtagen når kategorien <strong>Spam</strong> er valgt. actions_description_html: Afgør, hvilke foranstaltning, der skal træffes for at løse denne anmeldelse. Ved en straffende foranstaltning mod den anmeldte konto, fremsendes en e-mailnotifikation, undtagen når kategorien <strong>Spam</strong> er valgt.
actions_description_remote_html: Fastslå en nødvendig handling mhp. at løse denne anmeldelse. Dette vil kun påvirke <strong>din</strong> servers kommunikation med, og indholdshåndtering for, fjernkontoen. actions_description_remote_html: Fastslå en nødvendig handling mhp. at løse denne anmeldelse. Dette vil kun påvirke <strong>din</strong> servers kommunikation med, og indholdshåndtering for, fjernkontoen.
@ -1266,8 +1266,8 @@ da:
user_privacy_agreement_html: Jeg accepterer <a href="%{privacy_policy_path}" target="_blank">fortrolighedspolitikken</a> user_privacy_agreement_html: Jeg accepterer <a href="%{privacy_policy_path}" target="_blank">fortrolighedspolitikken</a>
author_attribution: author_attribution:
example_title: Eksempeltekst example_title: Eksempeltekst
hint_html: Skriver du nyheder eller blogartikler uden for Mastodon? Styr, hvordan man bliver krediteret, når disse deles på Mastodon. hint_html: Skriver du nyheder eller blogartikler uden for Mastodon? Styr, hvordan du bliver krediteret, når de bliver delt på Mastodon.
instructions: 'Sørg for, at denne kode er i artikelens HTML:' instructions: 'Sørg for, at denne kode er i din artikels HTML:'
more_from_html: Flere fra %{name} more_from_html: Flere fra %{name}
s_blog: "%{name}s blog" s_blog: "%{name}s blog"
then_instructions: Tilføj dernæst publikationsdomænenavnet i feltet nedenfor. then_instructions: Tilføj dernæst publikationsdomænenavnet i feltet nedenfor.
@ -1718,11 +1718,11 @@ da:
hint_html: "<strong>Tilpas hvordan din profil og dine indlæg kan findes.</strong> En række funktioner i Mastodon kan hjælpe dig med at nå ud til et bredere publikum, hvis du aktiverer dem. Tjek indstillingerne herunder for at sikre, at de passer til dit brugsscenarie." hint_html: "<strong>Tilpas hvordan din profil og dine indlæg kan findes.</strong> En række funktioner i Mastodon kan hjælpe dig med at nå ud til et bredere publikum, hvis du aktiverer dem. Tjek indstillingerne herunder for at sikre, at de passer til dit brugsscenarie."
privacy: Privatliv privacy: Privatliv
privacy_hint_html: Styr, hvor meget der ønskes synliggjort til gavn for andre. Folk finder interessante profiler og apps ved at tjekke andres følgere ud, samt se hvilke apps de sender fra, men dine præferencer ønskes muligvis ikke synliggjort. privacy_hint_html: Styr, hvor meget der ønskes synliggjort til gavn for andre. Folk finder interessante profiler og apps ved at tjekke andres følgere ud, samt se hvilke apps de sender fra, men dine præferencer ønskes muligvis ikke synliggjort.
reach: Udbredelse reach: Rækkevidde
reach_hint_html: Indstil om du vil blive opdaget og fulgt af nye mennesker. Ønsker du, at dine indlæg skal vises på Udforsk-siden? Ønsker du, at andre skal se dig i deres følg-anbefalinger? Ønsker du at acceptere alle nye følgere automatisk, eller vil du have detaljeret kontrol over hver og en? reach_hint_html: Indstil om du vil blive opdaget og fulgt af nye mennesker. Ønsker du, at dine indlæg skal vises på Udforsk-siden? Ønsker du, at andre skal se dig i deres følg-anbefalinger? Ønsker du at acceptere alle nye følgere automatisk, eller vil du have detaljeret kontrol over hver og en?
search: Søg search: Søgning
search_hint_html: Indstil hvordan du vil findes. Ønsker du, at folk skal finde dig gennem hvad du har skrevet offentligt? Vil du have folk udenfor Mastodon til at finde din profil, når de søger på nettet? Vær opmærksom på, at det ikke kan garanteres at dine offentlige indlæg er udelukket fra alle søgemaskiner. search_hint_html: Indstil hvordan du vil findes. Ønsker du, at folk skal finde dig gennem hvad du har skrevet offentligt? Vil du have folk udenfor Mastodon til at finde din profil, når de søger på nettet? Vær opmærksom på, at det ikke kan garanteres at dine offentlige indlæg er udelukket fra alle søgemaskiner.
title: Fortrolighed og udbredelse title: Fortrolighed og rækkevidde
privacy_policy: privacy_policy:
title: Privatlivspolitik title: Privatlivspolitik
reactions: reactions:
@ -1923,7 +1923,7 @@ da:
'7889238': 3 måneder '7889238': 3 måneder
min_age_label: Alderstærskel min_age_label: Alderstærskel
min_favs: Behold indlæg favoritmarkeret mindst min_favs: Behold indlæg favoritmarkeret mindst
min_favs_hint: Sletter ingen dine egne indlæg, som har modtaget minimum dette antal favoritmarkeringer. Lad stå tomt for at slette indlæg uanset antal favoritmarkeringer min_favs_hint: Sletter ingen af dine egne indlæg, som har modtaget minimum dette antal favoritmarkeringer. Lad stå tom for at slette indlæg uanset antal favoritmarkeringer
min_reblogs: Behold indlæg fremhævet mindst min_reblogs: Behold indlæg fremhævet mindst
min_reblogs_hint: Sletter ingen af dine egne indlæg, som er fremhævet flere end dette antal gange. Lad stå tom for at slette indlæg uanset antallet af fremhævelser min_reblogs_hint: Sletter ingen af dine egne indlæg, som er fremhævet flere end dette antal gange. Lad stå tom for at slette indlæg uanset antallet af fremhævelser
stream_entries: stream_entries:
@ -2095,7 +2095,7 @@ da:
verification: verification:
extra_instructions_html: <strong>Tip:</strong> Linket på din hjemmeside kan være usynligt. Den vigtige del er <code>rel="me"</code> , som forhindrer impersonation på websteder med brugergenereret indhold. Du kan endda bruge et <code>link</code> tag i overskriften på siden i stedet for <code>a</code>, men HTML skal være tilgængelig uden at udføre JavaScript. extra_instructions_html: <strong>Tip:</strong> Linket på din hjemmeside kan være usynligt. Den vigtige del er <code>rel="me"</code> , som forhindrer impersonation på websteder med brugergenereret indhold. Du kan endda bruge et <code>link</code> tag i overskriften på siden i stedet for <code>a</code>, men HTML skal være tilgængelig uden at udføre JavaScript.
here_is_how: Sådan gør du here_is_how: Sådan gør du
hint_html: "<strong>Bekræftelse af din identitet på Mastodon er for alle.</strong> Baseret på åbne webstandarder, nu og for evigt gratis. Alt du behøver er en personlig hjemmeside, som folk genkende dig ved. Når du linker til denne hjemmeside fra din profil, vi vil kontrollere, at hjemmesiden linker tilbage til din profil og vise en visuel indikator på det." hint_html: "<strong>Verificering af din identitet på Mastodon er for alle.</strong> Baseret på åbne webstandarder, nu og for altid gratis. Alt, hvad du behøver, er en personlig hjemmeside, som folk kender dig fra. Når du linker til denne hjemmeside fra din profil, kontrollerer vi, at hjemmesiden linker tilbage til din profil, og viser en visuel indikator på den."
instructions_html: Kopier og indsæt koden nedenfor i HTML på din hjemmeside. Tilføj derefter adressen på din hjemmeside i et af de ekstra felter på din profil på fanen "Redigér profil" og gem ændringer. instructions_html: Kopier og indsæt koden nedenfor i HTML på din hjemmeside. Tilføj derefter adressen på din hjemmeside i et af de ekstra felter på din profil på fanen "Redigér profil" og gem ændringer.
verification: Bekræftelse verification: Bekræftelse
verified_links: Dine bekræftede links verified_links: Dine bekræftede links

View File

@ -1349,6 +1349,10 @@ hu:
basic_information: Általános információk basic_information: Általános információk
hint_html: "<strong>Tedd egyedivé, mi látnak mások a profilodon és a bejegyzéseid mellett.</strong> Mások nagyobb eséllyel követnek vissza és lépnek veled kapcsolatba, ha van kitöltött profilod és profilképed." hint_html: "<strong>Tedd egyedivé, mi látnak mások a profilodon és a bejegyzéseid mellett.</strong> Mások nagyobb eséllyel követnek vissza és lépnek veled kapcsolatba, ha van kitöltött profilod és profilképed."
other: Egyéb other: Egyéb
emoji_styles:
auto: Automatikus
native: Natív
twemoji: Twemoji
errors: errors:
'400': A küldött kérés érvénytelen vagy hibás volt. '400': A küldött kérés érvénytelen vagy hibás volt.
'403': Nincs jogosultságod az oldal megtekintéséhez. '403': Nincs jogosultságod az oldal megtekintéséhez.

View File

@ -61,6 +61,7 @@ ca:
setting_display_media_default: Amaga el contingut gràfic marcat com a sensible setting_display_media_default: Amaga el contingut gràfic marcat com a sensible
setting_display_media_hide_all: Oculta sempre tot el contingut multimèdia setting_display_media_hide_all: Oculta sempre tot el contingut multimèdia
setting_display_media_show_all: Mostra sempre el contingut gràfic setting_display_media_show_all: Mostra sempre el contingut gràfic
setting_emoji_style: Com mostrar els emojis. "Automàtic" provarà de fer servir els emojis nadius, però revertirà a twemojis en els navegadors antics.
setting_system_scrollbars_ui: S'aplica només als navegadors d'escriptori basats en Safari i Chrome setting_system_scrollbars_ui: S'aplica només als navegadors d'escriptori basats en Safari i Chrome
setting_use_blurhash: Els degradats es basen en els colors de les imatges ocultes, però n'enfosqueixen els detalls setting_use_blurhash: Els degradats es basen en els colors de les imatges ocultes, però n'enfosqueixen els detalls
setting_use_pending_items: Amaga les actualitzacions de la línia de temps després de fer un clic, en lloc de desplaçar-les automàticament setting_use_pending_items: Amaga les actualitzacions de la línia de temps després de fer un clic, en lloc de desplaçar-les automàticament
@ -240,6 +241,7 @@ ca:
setting_display_media_default: Per defecte setting_display_media_default: Per defecte
setting_display_media_hide_all: Amaga-ho tot setting_display_media_hide_all: Amaga-ho tot
setting_display_media_show_all: Mostra-ho tot setting_display_media_show_all: Mostra-ho tot
setting_emoji_style: Estil d'emojis
setting_expand_spoilers: Desplega sempre els tuts marcats amb advertències de contingut setting_expand_spoilers: Desplega sempre els tuts marcats amb advertències de contingut
setting_hide_network: Amaga la teva xarxa setting_hide_network: Amaga la teva xarxa
setting_missing_alt_text_modal: Mostra un diàleg de confirmació abans de publicar contingut sense text alternatiu setting_missing_alt_text_modal: Mostra un diàleg de confirmació abans de publicar contingut sense text alternatiu

View File

@ -61,6 +61,7 @@ hu:
setting_display_media_default: Kényes tartalomnak jelölt média elrejtése setting_display_media_default: Kényes tartalomnak jelölt média elrejtése
setting_display_media_hide_all: Média elrejtése mindig setting_display_media_hide_all: Média elrejtése mindig
setting_display_media_show_all: Média megjelenítése mindig setting_display_media_show_all: Média megjelenítése mindig
setting_emoji_style: Az emodzsik megjelenítési módja. Az „Automatikus” megpróbálja a natív emodzsikat használni, de az örökölt böngészők esetén a Twemojira vált vissza.
setting_system_scrollbars_ui: Csak Chrome és Safari alapú asztali böngészőkre vonatkozik setting_system_scrollbars_ui: Csak Chrome és Safari alapú asztali böngészőkre vonatkozik
setting_use_blurhash: A kihomályosítás az eredeti képből történik, de minden részletet elrejt setting_use_blurhash: A kihomályosítás az eredeti képből történik, de minden részletet elrejt
setting_use_pending_items: Idővonal frissítése csak kattintásra automatikus görgetés helyett setting_use_pending_items: Idővonal frissítése csak kattintásra automatikus görgetés helyett
@ -241,6 +242,7 @@ hu:
setting_display_media_default: Alapértelmezés setting_display_media_default: Alapértelmezés
setting_display_media_hide_all: Mindent elrejt setting_display_media_hide_all: Mindent elrejt
setting_display_media_show_all: Mindent mutat setting_display_media_show_all: Mindent mutat
setting_emoji_style: Emodzsistílus
setting_expand_spoilers: Tartalmi figyelmeztetéssel ellátott bejegyzések automatikus kinyitása setting_expand_spoilers: Tartalmi figyelmeztetéssel ellátott bejegyzések automatikus kinyitása
setting_hide_network: Hálózatod elrejtése setting_hide_network: Hálózatod elrejtése
setting_missing_alt_text_modal: Megerősítési párbeszédablak megjelenítése a helyettesítő szöveg nélküli média közzététele előtt setting_missing_alt_text_modal: Megerősítési párbeszédablak megjelenítése a helyettesítő szöveg nélküli média közzététele előtt

View File

@ -20,17 +20,19 @@ RSpec.describe FollowRequest do
end end
end end
it 'calls Account#follow!, MergeWorker.perform_async, and #destroy!' do it 'calls Account#follow!, MergeWorker.perform_async, ActivityPub::AccountBackfillWorker, and #destroy!' do
allow(account).to receive(:follow!) do allow(account).to receive(:follow!) do
account.active_relationships.create!(target_account: target_account) account.active_relationships.create!(target_account: target_account)
end end
allow(MergeWorker).to receive(:perform_async) allow(MergeWorker).to receive(:perform_async)
allow(ActivityPub::AccountBackfillWorker).to receive(:perform_async)
allow(follow_request).to receive(:destroy!) allow(follow_request).to receive(:destroy!)
follow_request.authorize! follow_request.authorize!
expect(account).to have_received(:follow!).with(target_account, reblogs: true, notify: false, uri: follow_request.uri, languages: nil, bypass_limit: true) expect(account).to have_received(:follow!).with(target_account, reblogs: true, notify: false, uri: follow_request.uri, languages: nil, bypass_limit: true)
expect(MergeWorker).to have_received(:perform_async).with(target_account.id, account.id, 'home') expect(MergeWorker).to have_received(:perform_async).with(target_account.id, account.id, 'home')
expect(ActivityPub::AccountBackfillWorker).to have_received(:perform_async).with(target_account.id)
expect(follow_request).to have_received(:destroy!) expect(follow_request).to have_received(:destroy!)
end end
@ -47,6 +49,21 @@ RSpec.describe FollowRequest do
target = follow_request.target_account target = follow_request.target_account
expect(follow_request.account.muting_reblogs?(target)).to be true expect(follow_request.account.muting_reblogs?(target)).to be true
end end
context 'when subsequent follow requests are made' do
before do
second_account = Fabricate(:account)
second_account.follow!(target_account)
end
it 'doesnt call ActivityPub::AccountBackfillWorker' do
allow(ActivityPub::AccountBackfillWorker).to receive(:perform_async)
follow_request.authorize!
expect(ActivityPub::AccountBackfillWorker).to_not have_received(:perform_async)
end
end
end end
describe '#reject!' do describe '#reject!' do

View File

@ -0,0 +1,112 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe ActivityPub::AccountBackfillService do
subject { described_class.new }
before do
stub_const('ActivityPub::AccountBackfillService::ENABLED', true)
end
let!(:account) { Fabricate(:account, domain: 'other.com', outbox_url: 'http://other.com/alice/outbox') }
let!(:outbox) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'http://other.com/alice/outbox',
type: 'OrderedCollection',
first: 'http://other.com/alice/outbox?page=true',
}.with_indifferent_access
end
let!(:items) do
[
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'https://other.com/alice/1234',
to: ['https://www.w3.org/ns/activitystreams#Public'],
cc: ['https://other.com/alice/followers'],
type: 'Note',
content: 'Lorem ipsum',
attributedTo: 'http://other.com/alice',
},
'https://other.com/alice/5678',
]
end
let!(:outbox_page) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'http://example.com/alice/outbox?page=true',
type: 'OrderedCollectionPage',
orderedItems: items,
}
end
describe '#call' do
before do
stub_request(:get, 'http://other.com/alice/outbox').to_return(status: 200, body: Oj.dump(outbox), headers: { 'Content-Type': 'application/activity+json' })
stub_request(:get, 'http://other.com/alice/outbox?page=true').to_return(status: 200, body: Oj.dump(outbox_page), headers: { 'Content-Type': 'application/activity+json' })
end
it 'fetches the items in the outbox' do
allow(FetchReplyWorker).to receive(:push_bulk)
got_items = subject.call(account)
expect(got_items[0].deep_symbolize_keys).to eq(items[0])
expect(got_items[1]).to eq(items[1])
expect(FetchReplyWorker).to have_received(:push_bulk).with([items[0].stringify_keys, items[1]])
end
context 'with followers-only and private statuses' do
let!(:items) do
[
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'https://other.com/alice/public',
type: 'Note',
to: ['https://www.w3.org/ns/activitystreams#Public'],
cc: ['https://other.com/alice/followers'],
content: 'Lorem ipsum',
attributedTo: 'http://other.com/alice',
},
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'https://other.com/alice/unlisted',
to: ['https://other.com/alice/followers'],
cc: ['https://www.w3.org/ns/activitystreams#Public'],
type: 'Note',
content: 'Lorem ipsum',
attributedTo: 'http://other.com/alice',
},
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'https://other.com/alice/followers-only',
to: ['https://other.com/alice/followers'],
type: 'Note',
content: 'Lorem ipsum',
attributedTo: 'http://other.com/alice',
},
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'https://other.com/alice/dm',
to: ['https://other.com/alice/followers'],
type: 'Note',
content: 'Lorem ipsum',
attributedTo: 'http://other.com/alice',
},
]
end
it 'only processes public and unlisted statuses' do
allow(FetchReplyWorker).to receive(:push_bulk)
got_items = subject.call(account)
expect(got_items.length).to eq(2)
expect(got_items[0].deep_symbolize_keys).to eq(items[0])
expect(got_items[1].deep_symbolize_keys).to eq(items[1])
expect(FetchReplyWorker).to have_received(:push_bulk).with([items[0].stringify_keys, items[1].stringify_keys])
end
end
end
end