From 2fe71720026e36cadd538ecd3c06e6a2f1024789 Mon Sep 17 00:00:00 2001 From: Michael Stanclift Date: Wed, 12 Mar 2025 03:32:07 -0500 Subject: [PATCH 01/12] Dockerfile: Limit Yarn copy operations to reduce cache impact (#34094) --- Dockerfile | 58 +++++++++++++++++++++--------------------------------- 1 file changed, 22 insertions(+), 36 deletions(-) diff --git a/Dockerfile b/Dockerfile index b9f2bada8f..f33e136928 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,12 +14,12 @@ ARG BASE_REGISTRY="docker.io" # Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"] # renovate: datasource=docker depName=docker.io/ruby ARG RUBY_VERSION="3.4.2" -# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] +# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] # renovate: datasource=node-version depName=node ARG NODE_MAJOR_VERSION="22" # Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"] ARG DEBIAN_VERSION="bookworm" -# Node image to use for base image based on combined variables (ex: 20-bookworm-slim) +# Node.js image to use for base image based on combined variables (ex: 20-bookworm-slim) FROM ${BASE_REGISTRY}/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node # Ruby image to use for base image based on combined variables (ex: 3.4.x-slim-bookworm) FROM ${BASE_REGISTRY}/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby @@ -61,7 +61,7 @@ ENV \ ENV \ # Configure the IP to bind Mastodon to when serving traffic BIND="0.0.0.0" \ - # Use production settings for Yarn, Node and related nodejs based tools + # Use production settings for Yarn, Node.js and related tools NODE_ENV="production" \ # Use production settings for Ruby on Rails RAILS_ENV="production" \ @@ -128,13 +128,6 @@ RUN \ # Create temporary build layer from base image FROM ruby AS build -# Copy Node package configuration files into working directory -COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/ -COPY .yarn /opt/mastodon/.yarn - -COPY --from=node /usr/local/bin /usr/local/bin -COPY --from=node /usr/local/lib /usr/local/lib - ARG TARGETPLATFORM # hadolint ignore=DL3008 @@ -188,12 +181,6 @@ RUN \ libx265-dev \ ; -RUN \ - # Configure Corepack - rm /usr/local/bin/yarn*; \ - corepack enable; \ - corepack prepare --activate; - # Create temporary libvips specific build layer from build layer FROM build AS libvips @@ -284,38 +271,37 @@ RUN \ # Download and install required Gems bundle install -j"$(nproc)"; -# Create temporary node specific build layer from build layer -FROM build AS yarn +# Create temporary assets build layer from build layer +FROM build AS precompiler ARG TARGETPLATFORM -# Copy Node package configuration files into working directory -COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/ -COPY streaming/package.json /opt/mastodon/streaming/ -COPY .yarn /opt/mastodon/.yarn +# Copy Mastodon sources into layer +COPY . /opt/mastodon/ + +# Copy Node.js binaries/libraries into layer +COPY --from=node /usr/local/bin /usr/local/bin +COPY --from=node /usr/local/lib /usr/local/lib + +RUN \ + # Configure Corepack + rm /usr/local/bin/yarn*; \ + corepack enable; \ + corepack prepare --activate; # hadolint ignore=DL3008 RUN \ --mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \ --mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \ - # Install Node packages + # Install Node.js packages yarn workspaces focus --production @mastodon/mastodon; -# Create temporary assets build layer from build layer -FROM build AS precompiler - -# Copy Mastodon sources into precompiler layer -COPY . /opt/mastodon/ - -# Copy bundler and node packages from build layer to container -COPY --from=yarn /opt/mastodon /opt/mastodon/ -COPY --from=bundler /opt/mastodon /opt/mastodon/ -COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/ -# Copy libvips components to layer for precompiler +# Copy libvips components into layer for precompiler COPY --from=libvips /usr/local/libvips/bin /usr/local/bin COPY --from=libvips /usr/local/libvips/lib /usr/local/lib - -ARG TARGETPLATFORM +# Copy bundler packages into layer for precompiler +COPY --from=bundler /opt/mastodon /opt/mastodon/ +COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/ RUN \ ldconfig; \ From 46e13dd81c84ca952fb3f01e1ce389f473160fc5 Mon Sep 17 00:00:00 2001 From: Jonny Saunders Date: Wed, 12 Mar 2025 02:03:01 -0700 Subject: [PATCH 02/12] Add Fetch All Replies Part 1: Backend (#32615) Signed-off-by: sneakers-the-rat Co-authored-by: jonny Co-authored-by: Claire Co-authored-by: Kouhai <66407198+kouhaidev@users.noreply.github.com> --- .env.production.sample | 21 ++ app/controllers/api/v1/statuses_controller.rb | 2 + app/helpers/json_ld_helper.rb | 35 ++- .../concerns/status/fetch_replies_concern.rb | 53 ++++ app/models/status.rb | 2 + .../activitypub/fetch_all_replies_service.rb | 68 +++++ .../fetch_featured_collection_service.rb | 2 +- .../fetch_featured_tags_collection_service.rb | 2 +- .../fetch_remote_status_service.rb | 18 +- .../activitypub/fetch_replies_service.rb | 54 +++- .../synchronize_followers_service.rb | 2 +- .../activitypub/fetch_all_replies_worker.rb | 77 +++++ ...233930_add_fetched_replies_at_to_status.rb | 7 + db/schema.rb | 1 + .../status/fetch_replies_concern_spec.rb | 132 +++++++++ .../fetch_all_replies_service_spec.rb | 90 ++++++ .../fetch_remote_status_service_spec.rb | 53 +++- .../fetch_all_replies_worker_spec.rb | 280 ++++++++++++++++++ 18 files changed, 874 insertions(+), 25 deletions(-) create mode 100644 app/models/concerns/status/fetch_replies_concern.rb create mode 100644 app/services/activitypub/fetch_all_replies_service.rb create mode 100644 app/workers/activitypub/fetch_all_replies_worker.rb create mode 100644 db/migrate/20240918233930_add_fetched_replies_at_to_status.rb create mode 100644 spec/models/concerns/status/fetch_replies_concern_spec.rb create mode 100644 spec/services/activitypub/fetch_all_replies_service_spec.rb create mode 100644 spec/workers/activitypub/fetch_all_replies_worker_spec.rb diff --git a/.env.production.sample b/.env.production.sample index 1faaf5b57c..61bad7609c 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -86,3 +86,24 @@ S3_ALIAS_HOST=files.example.com # ----------------------- IP_RETENTION_PERIOD=31556952 SESSION_RETENTION_PERIOD=31556952 + +# Fetch All Replies Behavior +# -------------------------- +# When a user expands a post (DetailedStatus view), fetch all of its replies +# (default: true if unset, set explicitly to ``false`` to disable) +FETCH_REPLIES_ENABLED=true + +# Period to wait between fetching replies (in minutes) +FETCH_REPLIES_COOLDOWN_MINUTES=15 + +# Period to wait after a post is first created before fetching its replies (in minutes) +FETCH_REPLIES_INITIAL_WAIT_MINUTES=5 + +# Max number of replies to fetch - total, recursively through a whole reply tree +FETCH_REPLIES_MAX_GLOBAL=1000 + +# Max number of replies to fetch - for a single post +FETCH_REPLIES_MAX_SINGLE=500 + +# Max number of replies Collection pages to fetch - total +FETCH_REPLIES_MAX_PAGES=500 diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 2027bc6016..d3b0e89e97 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -58,6 +58,8 @@ class Api::V1::StatusesController < Api::BaseController statuses = [@status] + @context.ancestors + @context.descendants render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id) + + ActivityPub::FetchAllRepliesWorker.perform_async(@status.id) if !current_account.nil? && @status.should_fetch_replies? end def create diff --git a/app/helpers/json_ld_helper.rb b/app/helpers/json_ld_helper.rb index ba096427cf..ccdefb35d8 100644 --- a/app/helpers/json_ld_helper.rb +++ b/app/helpers/json_ld_helper.rb @@ -155,24 +155,49 @@ module JsonLdHelper end end - def fetch_resource(uri, id_is_known, on_behalf_of = nil, request_options: {}) + # Fetch the resource given by uri. + # @param uri [String] + # @param id_is_known [Boolean] + # @param on_behalf_of [nil, Account] + # @param raise_on_error [Boolean, Symbol<:all, :temporary>] See {#fetch_resource_without_id_validation} for possible values + def fetch_resource(uri, id_is_known, on_behalf_of = nil, raise_on_error: false, request_options: {}) unless id_is_known - json = fetch_resource_without_id_validation(uri, on_behalf_of) + json = fetch_resource_without_id_validation(uri, on_behalf_of, raise_on_error: raise_on_error) return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id']) uri = json['id'] end - json = fetch_resource_without_id_validation(uri, on_behalf_of, request_options: request_options) + json = fetch_resource_without_id_validation(uri, on_behalf_of, raise_on_error: raise_on_error, request_options: request_options) json.present? && json['id'] == uri ? json : nil end - def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {}) + # Fetch the resource given by uri + # + # If an error is raised, it contains the response and can be captured for handling like + # + # begin + # fetch_resource_without_id_validation(uri, nil, true) + # rescue Mastodon::UnexpectedResponseError => e + # e.response + # end + # + # @param uri [String] + # @param on_behalf_of [nil, Account] + # @param raise_on_error [Boolean, Symbol<:all, :temporary>] + # - +true+, +:all+ - raise if response code is not in the 2** range + # - +:temporary+ - raise if the response code is not an "unsalvageable error" like a 404 + # (see {#response_error_unsalvageable} ) + # - +false+ - do not raise, return +nil+ + def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_error: false, request_options: {}) on_behalf_of ||= Account.representative build_request(uri, on_behalf_of, options: request_options).perform do |response| - raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error + raise Mastodon::UnexpectedResponseError, response if !response_successful?(response) && ( + [true, :all].include?(raise_on_error) || + (!response_error_unsalvageable?(response) && raise_on_error == :temporary) + ) body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response) end diff --git a/app/models/concerns/status/fetch_replies_concern.rb b/app/models/concerns/status/fetch_replies_concern.rb new file mode 100644 index 0000000000..f34bce59b4 --- /dev/null +++ b/app/models/concerns/status/fetch_replies_concern.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Status::FetchRepliesConcern + extend ActiveSupport::Concern + + # enable/disable fetching all replies + FETCH_REPLIES_ENABLED = ENV.key?('FETCH_REPLIES_ENABLED') ? ENV['FETCH_REPLIES_ENABLED'] == 'true' : true + + # debounce fetching all replies to minimize DoS + FETCH_REPLIES_COOLDOWN_MINUTES = (ENV['FETCH_REPLIES_COOLDOWN_MINUTES'] || 15).to_i.minutes + FETCH_REPLIES_INITIAL_WAIT_MINUTES = (ENV['FETCH_REPLIES_INITIAL_WAIT_MINUTES'] || 5).to_i.minutes + + included do + scope :created_recently, -> { where(created_at: FETCH_REPLIES_INITIAL_WAIT_MINUTES.ago..) } + scope :not_created_recently, -> { where(created_at: ..FETCH_REPLIES_INITIAL_WAIT_MINUTES.ago) } + scope :fetched_recently, -> { where(fetched_replies_at: FETCH_REPLIES_COOLDOWN_MINUTES.ago..) } + scope :not_fetched_recently, -> { where(fetched_replies_at: [nil, ..FETCH_REPLIES_COOLDOWN_MINUTES.ago]) } + + scope :should_not_fetch_replies, -> { local.or(created_recently.or(fetched_recently)) } + scope :should_fetch_replies, -> { remote.not_created_recently.not_fetched_recently } + + # statuses for which we won't receive update or deletion actions, + # and should update when fetching replies + # Status from an account which either + # a) has only remote followers + # b) has local follows that were created after the last update time, or + # c) has no known followers + scope :unsubscribed, lambda { + remote.merge( + Status.left_outer_joins(account: :followers).where.not(followers_accounts: { domain: nil }) + .or(where.not('follows.created_at < statuses.updated_at')) + .or(where(follows: { id: nil })) + ) + } + end + + def should_fetch_replies? + # we aren't brand new, and we haven't fetched replies since the debounce window + FETCH_REPLIES_ENABLED && !local? && created_at <= FETCH_REPLIES_INITIAL_WAIT_MINUTES.ago && ( + fetched_replies_at.nil? || fetched_replies_at <= FETCH_REPLIES_COOLDOWN_MINUTES.ago + ) + end + + def unsubscribed? + return false if local? + + !Follow.joins(:account).exists?( + target_account: account.id, + account: { domain: nil }, + created_at: ..updated_at + ) + end +end diff --git a/app/models/status.rb b/app/models/status.rb index c012b1ddfa..746fcde395 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -27,6 +27,7 @@ # edited_at :datetime # trendable :boolean # ordered_media_attachment_ids :bigint(8) is an Array +# fetched_replies_at :datetime # class Status < ApplicationRecord @@ -34,6 +35,7 @@ class Status < ApplicationRecord include Discard::Model include Paginable include RateLimitable + include Status::FetchRepliesConcern include Status::SafeReblogInsert include Status::SearchConcern include Status::SnapshotConcern diff --git a/app/services/activitypub/fetch_all_replies_service.rb b/app/services/activitypub/fetch_all_replies_service.rb new file mode 100644 index 0000000000..9f92b7efee --- /dev/null +++ b/app/services/activitypub/fetch_all_replies_service.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +class ActivityPub::FetchAllRepliesService < ActivityPub::FetchRepliesService + include JsonLdHelper + + # Limit of replies to fetch per status + MAX_REPLIES = (ENV['FETCH_REPLIES_MAX_SINGLE'] || 500).to_i + + def call(collection_or_uri, status_uri, max_pages = nil, request_id: nil) + @allow_synchronous_requests = true + @collection_or_uri = collection_or_uri + @status_uri = status_uri + + @items, n_pages = collection_items(collection_or_uri, max_pages) + @items = filtered_replies + return if @items.nil? + + FetchReplyWorker.push_bulk(@items) { |reply_uri| [reply_uri, { 'request_id' => request_id }] } + + [@items, n_pages] + end + + private + + def filtered_replies + return if @items.nil? + + # Find all statuses that we *shouldn't* update the replies for, and use that as a filter. + # We don't assume that we have the statuses before they're created, + # hence the negative filter - + # "keep all these uris except the ones we already have" + # instead of + # "keep all these uris that match some conditions on existing Status objects" + # + # Typically we assume the number of replies we *shouldn't* fetch is smaller than the + # replies we *should* fetch, so we also minimize the number of uris we should load here. + uris = @items.map { |item| value_or_id(item) } + + # Expand collection to get replies in the DB that were + # - not included in the collection, + # - that we have locally + # - but we have no local followers and thus don't get updates/deletes for + parent_id = Status.where(uri: @status_uri).pick(:id) + unless parent_id.nil? + unsubscribed_replies = Status + .where.not(uri: uris) + .where(in_reply_to_id: parent_id) + .unsubscribed + .pluck(:uri) + uris.concat(unsubscribed_replies) + end + + dont_update = Status.where(uri: uris).should_not_fetch_replies.pluck(:uri) + + # touch all statuses that already exist and that we're about to update + Status.where(uri: uris).should_fetch_replies.touch_all(:fetched_replies_at) + + # Reject all statuses that we already have in the db + uris = (uris - dont_update).take(MAX_REPLIES) + + Rails.logger.debug { "FetchAllRepliesService - #{@collection_or_uri}: Fetching filtered statuses: #{uris}" } + uris + end + + def filter_by_host? + false + end +end diff --git a/app/services/activitypub/fetch_featured_collection_service.rb b/app/services/activitypub/fetch_featured_collection_service.rb index 89c3a1b6c0..25c62f3be6 100644 --- a/app/services/activitypub/fetch_featured_collection_service.rb +++ b/app/services/activitypub/fetch_featured_collection_service.rb @@ -33,7 +33,7 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService 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, true) + fetch_resource_without_id_validation(collection_or_uri, local_follower, raise_on_error: :temporary) end def process_items(items) diff --git a/app/services/activitypub/fetch_featured_tags_collection_service.rb b/app/services/activitypub/fetch_featured_tags_collection_service.rb index a0b3c6036b..ec2422a075 100644 --- a/app/services/activitypub/fetch_featured_tags_collection_service.rb +++ b/app/services/activitypub/fetch_featured_tags_collection_service.rb @@ -45,7 +45,7 @@ class ActivityPub::FetchFeaturedTagsCollectionService < BaseService 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, true) + fetch_resource_without_id_validation(collection_or_uri, local_follower, raise_on_error: :temporary) end def process_items(items) diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index 6f8882378f..dc74de32f1 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -13,7 +13,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService @request_id = request_id || "#{Time.now.utc.to_i}-status-#{uri}" @json = if prefetched_body.nil? - fetch_resource(uri, true, on_behalf_of) + fetch_status(uri, true, on_behalf_of) else body_to_json(prefetched_body, compare_id: uri) end @@ -80,4 +80,20 @@ class ActivityPub::FetchRemoteStatusService < BaseService def expected_object_type? equals_or_includes_any?(@json['type'], ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES) end + + def fetch_status(uri, id_is_known, on_behalf_of = nil) + begin + fetch_resource(uri, id_is_known, on_behalf_of, raise_on_error: true) + rescue Mastodon::UnexpectedResponseError => e + return unless e.response.code == 404 + + # If this is a 404 from a status from an account that has no local followers, delete it + existing_status = Status.find_by(uri: uri) + if !existing_status.nil? && existing_status.unsubscribed? && existing_status.distributable? + Rails.logger.debug { "FetchRemoteStatusService - Got 404 for orphaned status with URI #{uri}, deleting" } + Tombstone.find_or_create_by(uri: uri, account: existing_status.account) + RemoveStatusService.new.call(existing_status, redraft: false) + end + end + end end diff --git a/app/services/activitypub/fetch_replies_service.rb b/app/services/activitypub/fetch_replies_service.rb index 46cab6caf9..72b9c0f5a6 100644 --- a/app/services/activitypub/fetch_replies_service.rb +++ b/app/services/activitypub/fetch_replies_service.rb @@ -3,11 +3,14 @@ class ActivityPub::FetchRepliesService < BaseService include JsonLdHelper + # Limit of fetched replies + MAX_REPLIES = 5 + def call(parent_status, collection_or_uri, allow_synchronous_requests: true, request_id: nil) @account = parent_status.account @allow_synchronous_requests = allow_synchronous_requests - @items = collection_items(collection_or_uri) + @items, = collection_items(collection_or_uri) return if @items.nil? FetchReplyWorker.push_bulk(filtered_replies) { |reply_uri| [reply_uri, { 'request_id' => request_id }] } @@ -17,25 +20,39 @@ class ActivityPub::FetchRepliesService < BaseService private - def collection_items(collection_or_uri) + def collection_items(collection_or_uri, max_pages = nil) 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) - case collection['type'] - when 'Collection', 'CollectionPage' - as_array(collection['items']) - when 'OrderedCollection', 'OrderedCollectionPage' - as_array(collection['orderedItems']) + all_items = [] + n_pages = 1 + while collection.is_a?(Hash) + items = case collection['type'] + when 'Collection', 'CollectionPage' + collection['items'] + when 'OrderedCollection', 'OrderedCollectionPage' + collection['orderedItems'] + end + + all_items.concat(as_array(items)) + + break if all_items.size >= MAX_REPLIES + break if !max_pages.nil? && n_pages >= max_pages + + collection = collection['next'].present? ? fetch_collection(collection['next']) : nil + n_pages += 1 end + + [all_items, n_pages] 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?(@account.uri, collection_or_uri) + return if filter_by_host? && non_matching_uri_hosts?(@account.uri, collection_or_uri) # NOTE: For backward compatibility reasons, Mastodon signs outgoing # queries incorrectly by default. @@ -45,19 +62,28 @@ class ActivityPub::FetchRepliesService < BaseService # # Therefore, retry with correct signatures if this fails. begin - fetch_resource_without_id_validation(collection_or_uri, nil, true) + fetch_resource_without_id_validation(collection_or_uri, nil, raise_on_error: :temporary) rescue Mastodon::UnexpectedResponseError => e raise unless e.response && e.response.code == 401 && Addressable::URI.parse(collection_or_uri).query.present? - fetch_resource_without_id_validation(collection_or_uri, nil, true, request_options: { omit_query_string: false }) + fetch_resource_without_id_validation(collection_or_uri, nil, raise_on_error: :temporary, request_options: { omit_query_string: false }) end end def filtered_replies - # Only fetch replies to the same server as the original status to avoid - # amplification attacks. + if filter_by_host? + # Only fetch replies to the same server as the original status to avoid + # amplification attacks. - # Also limit to 5 fetched replies to limit potential for DoS. - @items.map { |item| value_or_id(item) }.reject { |uri| non_matching_uri_hosts?(@account.uri, uri) }.take(5) + # Also limit to 5 fetched replies to limit potential for DoS. + @items.map { |item| value_or_id(item) }.reject { |uri| non_matching_uri_hosts?(@account.uri, uri) }.take(MAX_REPLIES) + else + @items.map { |item| value_or_id(item) }.take(MAX_REPLIES) + end + end + + # Whether replies with a different domain than the replied_to post should be rejected + def filter_by_host? + true end end diff --git a/app/services/activitypub/synchronize_followers_service.rb b/app/services/activitypub/synchronize_followers_service.rb index f51d671a00..b01974dcc6 100644 --- a/app/services/activitypub/synchronize_followers_service.rb +++ b/app/services/activitypub/synchronize_followers_service.rb @@ -69,6 +69,6 @@ class ActivityPub::SynchronizeFollowersService < BaseService 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, true) + fetch_resource_without_id_validation(collection_or_uri, nil, raise_on_error: :temporary) end end diff --git a/app/workers/activitypub/fetch_all_replies_worker.rb b/app/workers/activitypub/fetch_all_replies_worker.rb new file mode 100644 index 0000000000..87eac321fa --- /dev/null +++ b/app/workers/activitypub/fetch_all_replies_worker.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +# Fetch all replies to a status, querying recursively through +# ActivityPub replies collections, fetching any statuses that +# we either don't already have or we haven't checked for new replies +# in the Status::FETCH_REPLIES_COOLDOWN_MINUTES interval +class ActivityPub::FetchAllRepliesWorker + include Sidekiq::Worker + include ExponentialBackoff + include JsonLdHelper + + sidekiq_options queue: 'pull', retry: 3 + + # Global max replies to fetch per request (all replies, recursively) + MAX_REPLIES = (ENV['FETCH_REPLIES_MAX_GLOBAL'] || 1000).to_i + MAX_PAGES = (ENV['FETCH_REPLIES_MAX_PAGES'] || 500).to_i + + def perform(parent_status_id, options = {}) + @parent_status = Status.find(parent_status_id) + return unless @parent_status.should_fetch_replies? + + @parent_status.touch(:fetched_replies_at) + Rails.logger.debug { "FetchAllRepliesWorker - #{@parent_status.uri}: Fetching all replies for status: #{@parent_status}" } + + uris_to_fetch, n_pages = get_replies(@parent_status.uri, MAX_PAGES, options) + return if uris_to_fetch.nil? + + fetched_uris = uris_to_fetch.clone.to_set + + until uris_to_fetch.empty? || fetched_uris.length >= MAX_REPLIES || n_pages >= MAX_PAGES + next_reply = uris_to_fetch.pop + next if next_reply.nil? + + new_reply_uris, new_n_pages = get_replies(next_reply, MAX_PAGES - n_pages, options) + next if new_reply_uris.nil? + + new_reply_uris = new_reply_uris.reject { |uri| fetched_uris.include?(uri) } + + uris_to_fetch.concat(new_reply_uris) + fetched_uris = fetched_uris.merge(new_reply_uris) + n_pages += new_n_pages + end + + Rails.logger.debug { "FetchAllRepliesWorker - #{parent_status_id}: fetched #{fetched_uris.length} replies" } + fetched_uris + end + + private + + def get_replies(status_uri, max_pages, options = {}) + replies_collection_or_uri = get_replies_uri(status_uri) + return if replies_collection_or_uri.nil? + + ActivityPub::FetchAllRepliesService.new.call(replies_collection_or_uri, status_uri, max_pages, **options.deep_symbolize_keys) + end + + def get_replies_uri(parent_status_uri) + begin + json_status = fetch_resource(parent_status_uri, true) + if json_status.nil? + Rails.logger.debug { "FetchAllRepliesWorker - #{@parent_status.uri}: Could not get replies URI for #{parent_status_uri}, returned nil" } + nil + elsif !json_status.key?('replies') + Rails.logger.debug { "FetchAllRepliesWorker - #{@parent_status.uri}: No replies collection found in ActivityPub object: #{json_status}" } + nil + else + json_status['replies'] + end + rescue => e + Rails.logger.error { "FetchAllRepliesWorker - #{@parent_status.uri}: Caught exception while resolving replies URI #{parent_status_uri}: #{e} - #{e.message}" } + # Raise if we can't get the collection for top-level status to trigger retry + raise e if parent_status_uri == @parent_status.uri + + nil + end + end +end diff --git a/db/migrate/20240918233930_add_fetched_replies_at_to_status.rb b/db/migrate/20240918233930_add_fetched_replies_at_to_status.rb new file mode 100644 index 0000000000..229e43d978 --- /dev/null +++ b/db/migrate/20240918233930_add_fetched_replies_at_to_status.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddFetchedRepliesAtToStatus < ActiveRecord::Migration[7.1] + def change + add_column :statuses, :fetched_replies_at, :datetime, null: true + end +end diff --git a/db/schema.rb b/db/schema.rb index ce7e358f4f..66c63e53f5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1053,6 +1053,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_05_074104) do t.datetime "edited_at", precision: nil t.boolean "trendable" t.bigint "ordered_media_attachment_ids", array: true + t.datetime "fetched_replies_at" t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)" t.index ["account_id"], name: "index_statuses_on_account_id" t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)" diff --git a/spec/models/concerns/status/fetch_replies_concern_spec.rb b/spec/models/concerns/status/fetch_replies_concern_spec.rb new file mode 100644 index 0000000000..f152cf234a --- /dev/null +++ b/spec/models/concerns/status/fetch_replies_concern_spec.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Status::FetchRepliesConcern do + ActiveRecord.verbose_query_logs = true + + let!(:alice) { Fabricate(:account, username: 'alice') } + let!(:bob) { Fabricate(:account, username: 'bob', domain: 'other.com') } + + let!(:account) { alice } + let!(:status_old) { Fabricate(:status, account: account, fetched_replies_at: 1.year.ago, created_at: 1.year.ago) } + let!(:status_fetched_recently) { Fabricate(:status, account: account, fetched_replies_at: 1.second.ago, created_at: 1.year.ago) } + let!(:status_created_recently) { Fabricate(:status, account: account, created_at: 1.second.ago) } + let!(:status_never_fetched) { Fabricate(:status, account: account, created_at: 1.year.ago) } + + describe 'should_fetch_replies' do + let!(:statuses) { Status.should_fetch_replies.all } + + context 'with a local status' do + it 'never fetches local replies' do + expect(statuses).to eq([]) + end + end + + context 'with a remote status' do + let(:account) { bob } + + it 'fetches old statuses' do + expect(statuses).to include(status_old) + end + + it 'fetches statuses that have never been fetched and weren\'t created recently' do + expect(statuses).to include(status_never_fetched) + end + + it 'does not fetch statuses that were fetched recently' do + expect(statuses).to_not include(status_fetched_recently) + end + + it 'does not fetch statuses that were created recently' do + expect(statuses).to_not include(status_created_recently) + end + end + end + + describe 'should_not_fetch_replies' do + let!(:statuses) { Status.should_not_fetch_replies.all } + + context 'with a local status' do + it 'does not fetch local statuses' do + expect(statuses).to include(status_old, status_never_fetched, status_fetched_recently, status_never_fetched) + end + end + + context 'with a remote status' do + let(:account) { bob } + + it 'fetches old statuses' do + expect(statuses).to_not include(status_old) + end + + it 'fetches statuses that have never been fetched and weren\'t created recently' do + expect(statuses).to_not include(status_never_fetched) + end + + it 'does not fetch statuses that were fetched recently' do + expect(statuses).to include(status_fetched_recently) + end + + it 'does not fetch statuses that were created recently' do + expect(statuses).to include(status_created_recently) + end + end + end + + describe 'unsubscribed' do + let!(:spike) { Fabricate(:account, username: 'spike', domain: 'other.com') } + let!(:status) { Fabricate(:status, account: bob, updated_at: 1.day.ago) } + + context 'when the status is from an account with only remote followers after last update' do + before do + Fabricate(:follow, account: spike, target_account: bob) + end + + it 'shows the status as unsubscribed' do + expect(Status.unsubscribed).to eq([status]) + expect(status.unsubscribed?).to be(true) + end + end + + context 'when the status is from an account with only remote followers before last update' do + before do + Fabricate(:follow, account: spike, target_account: bob, created_at: 2.days.ago) + end + + it 'shows the status as unsubscribed' do + expect(Status.unsubscribed).to eq([status]) + expect(status.unsubscribed?).to be(true) + end + end + + context 'when status is from account with local followers after last update' do + before do + Fabricate(:follow, account: alice, target_account: bob) + end + + it 'shows the status as unsubscribed' do + expect(Status.unsubscribed).to eq([status]) + expect(status.unsubscribed?).to be(true) + end + end + + context 'when status is from account with local followers before last update' do + before do + Fabricate(:follow, account: alice, target_account: bob, created_at: 2.days.ago) + end + + it 'does not show the status as unsubscribed' do + expect(Status.unsubscribed).to eq([]) + expect(status.unsubscribed?).to be(false) + end + end + + context 'when the status has no followers' do + it 'shows the status as unsubscribed' do + expect(Status.unsubscribed).to eq([status]) + expect(status.unsubscribed?).to be(true) + end + end + end +end diff --git a/spec/services/activitypub/fetch_all_replies_service_spec.rb b/spec/services/activitypub/fetch_all_replies_service_spec.rb new file mode 100644 index 0000000000..eadd5b10fa --- /dev/null +++ b/spec/services/activitypub/fetch_all_replies_service_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::FetchAllRepliesService do + subject { described_class.new } + + let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') } + let(:status) { Fabricate(:status, account: actor) } + let(:collection_uri) { 'http://example.com/replies/1' } + + let(:items) do + [ + 'http://example.com/self-reply-1', + 'http://example.com/self-reply-2', + 'http://example.com/self-reply-3', + 'http://other.com/other-reply-1', + 'http://other.com/other-reply-2', + 'http://other.com/other-reply-3', + 'http://example.com/self-reply-4', + 'http://example.com/self-reply-5', + 'http://example.com/self-reply-6', + ] + end + + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Collection', + id: collection_uri, + items: items, + }.with_indifferent_access + end + + describe '#call' do + it 'fetches more than the default maximum and from multiple domains' do + allow(FetchReplyWorker).to receive(:push_bulk) + + subject.call(payload, status.uri) + + expect(FetchReplyWorker).to have_received(:push_bulk).with(%w(http://example.com/self-reply-1 http://example.com/self-reply-2 http://example.com/self-reply-3 http://other.com/other-reply-1 http://other.com/other-reply-2 http://other.com/other-reply-3 http://example.com/self-reply-4 + http://example.com/self-reply-5 http://example.com/self-reply-6)) + end + + context 'with a recent status' do + before do + Fabricate(:status, uri: 'http://example.com/self-reply-2', fetched_replies_at: 1.second.ago, local: false) + end + + it 'skips statuses that have been updated recently' do + allow(FetchReplyWorker).to receive(:push_bulk) + + subject.call(payload, status.uri) + + expect(FetchReplyWorker).to have_received(:push_bulk).with(%w(http://example.com/self-reply-1 http://example.com/self-reply-3 http://other.com/other-reply-1 http://other.com/other-reply-2 http://other.com/other-reply-3 http://example.com/self-reply-4 http://example.com/self-reply-5 http://example.com/self-reply-6)) + end + end + + context 'with an old status' do + before do + Fabricate(:status, uri: 'http://other.com/other-reply-1', fetched_replies_at: 1.year.ago, created_at: 1.year.ago, account: actor) + end + + it 'updates the time that fetched statuses were last fetched' do + allow(FetchReplyWorker).to receive(:push_bulk) + + subject.call(payload, status.uri) + + expect(Status.find_by(uri: 'http://other.com/other-reply-1').fetched_replies_at).to be >= 1.minute.ago + end + end + + context 'with unsubscribed replies' do + before do + remote_actor = Fabricate(:account, domain: 'other.com', uri: 'http://other.com/account') + # reply not in the collection from the remote instance, but we know about anyway without anyone following the account + Fabricate(:status, account: remote_actor, in_reply_to_id: status.id, uri: 'http://other.com/account/unsubscribed', fetched_replies_at: 1.year.ago, created_at: 1.year.ago) + end + + it 'updates the unsubscribed replies' do + allow(FetchReplyWorker).to receive(:push_bulk) + + subject.call(payload, status.uri) + + expect(FetchReplyWorker).to have_received(:push_bulk).with(%w(http://example.com/self-reply-1 http://example.com/self-reply-2 http://example.com/self-reply-3 http://other.com/other-reply-1 http://other.com/other-reply-2 http://other.com/other-reply-3 http://example.com/self-reply-4 + http://example.com/self-reply-5 http://example.com/self-reply-6 http://other.com/account/unsubscribed)) + end + end + end +end diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb index 9d8c6e0e0a..847affd307 100644 --- a/spec/services/activitypub/fetch_remote_status_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb @@ -9,6 +9,9 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do let!(:sender) { Fabricate(:account, domain: 'foo.bar', uri: 'https://foo.bar') } + let(:follower) { Fabricate(:account, username: 'alice') } + let(:follow) { nil } + let(:response) { { body: Oj.dump(object), headers: { 'content-type': 'application/activity+json' } } } let(:existing_status) { nil } let(:note) do @@ -23,13 +26,14 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do before do stub_request(:get, 'https://foo.bar/watch?v=12345').to_return(status: 404, body: '') - stub_request(:get, object[:id]).to_return(body: Oj.dump(object)) + stub_request(:get, object[:id]).to_return(**response) end describe '#call' do before do + follow existing_status - subject.call(object[:id], prefetched_body: Oj.dump(object)) + subject.call(object[:id]) end context 'with Note object' do @@ -254,6 +258,51 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do expect(existing_status.text).to eq 'Lorem ipsum' expect(existing_status.edits).to_not be_empty end + + context 'when the status appears to have been deleted at source' do + let(:response) { { status: 404, body: '' } } + + shared_examples 'no delete' do + it 'does not delete the status' do + existing_status.reload + expect(existing_status.text).to eq 'Foo' + expect(existing_status.edits).to be_empty + end + end + + context 'when the status is orphaned/unsubscribed' do + it 'deletes the orphaned status' do + expect { existing_status.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'when the status is from an account with only remote followers' do + let(:follower) { Fabricate(:account, username: 'alice', domain: 'foo.bar') } + let(:follow) { Fabricate(:follow, account: follower, target_account: sender, created_at: 2.days.ago) } + + it 'deletes the orphaned status' do + expect { existing_status.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + context 'when the status is private' do + let(:existing_status) { Fabricate(:status, account: sender, text: 'Foo', uri: note[:id], visibility: :private) } + + it_behaves_like 'no delete' + end + + context 'when the status is direct' do + let(:existing_status) { Fabricate(:status, account: sender, text: 'Foo', uri: note[:id], visibility: :direct) } + + it_behaves_like 'no delete' + end + end + + context 'when the status is from an account with local followers' do + let(:follow) { Fabricate(:follow, account: follower, target_account: sender, created_at: 2.days.ago) } + + it_behaves_like 'no delete' + end + end end context 'with a Create activity' do diff --git a/spec/workers/activitypub/fetch_all_replies_worker_spec.rb b/spec/workers/activitypub/fetch_all_replies_worker_spec.rb new file mode 100644 index 0000000000..2b291e9624 --- /dev/null +++ b/spec/workers/activitypub/fetch_all_replies_worker_spec.rb @@ -0,0 +1,280 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::FetchAllRepliesWorker do + subject { described_class.new } + + let(:top_items) do + [ + 'http://example.com/self-reply-1', + 'http://other.com/other-reply-2', + 'http://example.com/self-reply-3', + ] + end + + let(:top_items_paged) do + [ + 'http://example.com/self-reply-4', + 'http://other.com/other-reply-5', + 'http://example.com/self-reply-6', + ] + end + + let(:nested_items) do + [ + 'http://example.com/nested-self-reply-1', + 'http://other.com/nested-other-reply-2', + 'http://example.com/nested-self-reply-3', + ] + end + + let(:nested_items_paged) do + [ + 'http://example.com/nested-self-reply-4', + 'http://other.com/nested-other-reply-5', + 'http://example.com/nested-self-reply-6', + ] + end + + let(:all_items) do + top_items + top_items_paged + nested_items + nested_items_paged + end + + let(:top_note_uri) do + 'http://example.com/top-post' + end + + let(:top_collection_uri) do + 'http://example.com/top-post/replies' + end + + # The reply uri that has the nested replies under it + let(:reply_note_uri) do + 'http://other.com/other-reply-2' + end + + # The collection uri of nested replies + let(:reply_collection_uri) do + 'http://other.com/other-reply-2/replies' + end + + let(:replies_top) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: top_collection_uri, + type: 'Collection', + items: top_items + top_items_paged, + } + end + + let(:replies_nested) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: reply_collection_uri, + type: 'Collection', + items: nested_items + nested_items_paged, + } + end + + # The status resource for the top post + let(:top_object) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: top_note_uri, + type: 'Note', + content: 'Lorem ipsum', + replies: replies_top, + attributedTo: 'https://example.com', + } + end + + # The status resource that has the uri to the replies collection + let(:reply_object) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: reply_note_uri, + type: 'Note', + content: 'Lorem ipsum', + replies: replies_nested, + attributedTo: 'https://other.com', + } + end + + let(:empty_object) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'https://example.com/empty', + type: 'Note', + content: 'Lorem ipsum', + replies: [], + attributedTo: 'https://example.com', + } + end + + let(:account) { Fabricate(:account, domain: 'example.com') } + let(:status) do + Fabricate( + :status, + account: account, + uri: top_note_uri, + created_at: 1.day.ago - Status::FetchRepliesConcern::FETCH_REPLIES_INITIAL_WAIT_MINUTES + ) + end + + before do + allow(FetchReplyWorker).to receive(:push_bulk) + all_items.each do |item| + next if [top_note_uri, reply_note_uri].include? item + + stub_request(:get, item).to_return(status: 200, body: Oj.dump(empty_object), headers: { 'Content-Type': 'application/activity+json' }) + end + + stub_request(:get, top_note_uri).to_return(status: 200, body: Oj.dump(top_object), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, reply_note_uri).to_return(status: 200, body: Oj.dump(reply_object), headers: { 'Content-Type': 'application/activity+json' }) + end + + shared_examples 'fetches all replies' do + it 'fetches statuses recursively' do + got_uris = subject.perform(status.id) + expect(got_uris).to match_array(all_items) + end + + it 'respects the maximum limits set by not recursing after the max is reached' do + stub_const('ActivityPub::FetchAllRepliesWorker::MAX_REPLIES', 5) + got_uris = subject.perform(status.id) + expect(got_uris).to match_array(top_items + top_items_paged) + end + end + + describe 'perform' do + context 'when the payload is a Note with replies as a Collection of inlined replies' do + it_behaves_like 'fetches all replies' + end + + context 'when the payload is a Note with replies as a URI to a Collection' do + let(:top_object) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: top_note_uri, + type: 'Note', + content: 'Lorem ipsum', + replies: top_collection_uri, + attributedTo: 'https://example.com', + } + end + let(:reply_object) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: reply_note_uri, + type: 'Note', + content: 'Lorem ipsum', + replies: reply_collection_uri, + attributedTo: 'https://other.com', + } + end + + before do + stub_request(:get, top_collection_uri).to_return(status: 200, body: Oj.dump(replies_top), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, reply_collection_uri).to_return(status: 200, body: Oj.dump(replies_nested), headers: { 'Content-Type': 'application/activity+json' }) + end + + it_behaves_like 'fetches all replies' + end + + context 'when the payload is a Note with replies as a paginated collection' do + let(:top_page_2_uri) do + "#{top_collection_uri}/2" + end + + let(:reply_page_2_uri) do + "#{reply_collection_uri}/2" + end + + let(:top_object) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: top_note_uri, + type: 'Note', + content: 'Lorem ipsum', + replies: { + type: 'Collection', + id: top_collection_uri, + first: { + type: 'CollectionPage', + partOf: top_collection_uri, + items: top_items, + next: top_page_2_uri, + }, + }, + attributedTo: 'https://example.com', + } + end + let(:reply_object) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: reply_note_uri, + type: 'Note', + content: 'Lorem ipsum', + replies: { + type: 'Collection', + id: reply_collection_uri, + first: { + type: 'CollectionPage', + partOf: reply_collection_uri, + items: nested_items, + next: reply_page_2_uri, + }, + }, + attributedTo: 'https://other.com', + } + end + + let(:top_page_two) do + { + type: 'CollectionPage', + id: top_page_2_uri, + partOf: top_collection_uri, + items: top_items_paged, + } + end + + let(:reply_page_two) do + { + type: 'CollectionPage', + id: reply_page_2_uri, + partOf: reply_collection_uri, + items: nested_items_paged, + } + end + + before do + stub_request(:get, top_page_2_uri).to_return(status: 200, body: Oj.dump(top_page_two), headers: { 'Content-Type': 'application/activity+json' }) + stub_request(:get, reply_page_2_uri).to_return(status: 200, body: Oj.dump(reply_page_two), headers: { 'Content-Type': 'application/activity+json' }) + end + + it_behaves_like 'fetches all replies' + + it 'limits by max pages' do + stub_const('ActivityPub::FetchAllRepliesWorker::MAX_PAGES', 3) + got_uris = subject.perform(status.id) + expect(got_uris).to match_array(top_items + top_items_paged + nested_items) + end + end + + context 'when replies should not be fetched' do + # ensure that we should not fetch by setting the status to be created in the debounce window + let(:status) { Fabricate(:status, account: account, uri: top_note_uri, created_at: DateTime.now) } + + before do + stub_const('Status::FetchRepliesConcern::FETCH_REPLIES_INITIAL_WAIT_MINUTES', 1.week) + end + + it 'returns nil without fetching' do + got_uris = subject.perform(status.id) + expect(got_uris).to be_nil + assert_not_requested :get, top_note_uri + end + end + end +end From 9db26db4958e47bb49f3724bdc550a72684079fe Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 12 Mar 2025 11:28:06 +0100 Subject: [PATCH 03/12] Refactor reply-fetching code and disable it by default (#34147) --- .env.production.sample | 4 ++-- app/helpers/json_ld_helper.rb | 14 +++++++------- .../concerns/status/fetch_replies_concern.rb | 12 +----------- .../activitypub/fetch_remote_status_service.rb | 8 ++++---- .../concerns/status/fetch_replies_concern_spec.rb | 5 ----- .../fetch_remote_status_service_spec.rb | 6 ------ .../activitypub/fetch_all_replies_worker_spec.rb | 1 + 7 files changed, 15 insertions(+), 35 deletions(-) diff --git a/.env.production.sample b/.env.production.sample index 61bad7609c..12ab2b6dcb 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -90,8 +90,8 @@ SESSION_RETENTION_PERIOD=31556952 # Fetch All Replies Behavior # -------------------------- # When a user expands a post (DetailedStatus view), fetch all of its replies -# (default: true if unset, set explicitly to ``false`` to disable) -FETCH_REPLIES_ENABLED=true +# (default: false) +FETCH_REPLIES_ENABLED=false # Period to wait between fetching replies (in minutes) FETCH_REPLIES_COOLDOWN_MINUTES=15 diff --git a/app/helpers/json_ld_helper.rb b/app/helpers/json_ld_helper.rb index ccdefb35d8..65daaa5302 100644 --- a/app/helpers/json_ld_helper.rb +++ b/app/helpers/json_ld_helper.rb @@ -159,8 +159,8 @@ module JsonLdHelper # @param uri [String] # @param id_is_known [Boolean] # @param on_behalf_of [nil, Account] - # @param raise_on_error [Boolean, Symbol<:all, :temporary>] See {#fetch_resource_without_id_validation} for possible values - def fetch_resource(uri, id_is_known, on_behalf_of = nil, raise_on_error: false, request_options: {}) + # @param raise_on_error [Symbol<:all, :temporary, :none>] See {#fetch_resource_without_id_validation} for possible values + def fetch_resource(uri, id_is_known, on_behalf_of = nil, raise_on_error: :none, request_options: {}) unless id_is_known json = fetch_resource_without_id_validation(uri, on_behalf_of, raise_on_error: raise_on_error) @@ -185,17 +185,17 @@ module JsonLdHelper # # @param uri [String] # @param on_behalf_of [nil, Account] - # @param raise_on_error [Boolean, Symbol<:all, :temporary>] - # - +true+, +:all+ - raise if response code is not in the 2** range + # @param raise_on_error [Symbol<:all, :temporary, :none>] + # - +:all+ - raise if response code is not in the 2xx range # - +:temporary+ - raise if the response code is not an "unsalvageable error" like a 404 # (see {#response_error_unsalvageable} ) - # - +false+ - do not raise, return +nil+ - def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_error: false, request_options: {}) + # - +:none+ - do not raise, return +nil+ + def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_error: :none, request_options: {}) on_behalf_of ||= Account.representative build_request(uri, on_behalf_of, options: request_options).perform do |response| raise Mastodon::UnexpectedResponseError, response if !response_successful?(response) && ( - [true, :all].include?(raise_on_error) || + raise_on_error == :all || (!response_error_unsalvageable?(response) && raise_on_error == :temporary) ) diff --git a/app/models/concerns/status/fetch_replies_concern.rb b/app/models/concerns/status/fetch_replies_concern.rb index f34bce59b4..fd9929aba4 100644 --- a/app/models/concerns/status/fetch_replies_concern.rb +++ b/app/models/concerns/status/fetch_replies_concern.rb @@ -4,7 +4,7 @@ module Status::FetchRepliesConcern extend ActiveSupport::Concern # enable/disable fetching all replies - FETCH_REPLIES_ENABLED = ENV.key?('FETCH_REPLIES_ENABLED') ? ENV['FETCH_REPLIES_ENABLED'] == 'true' : true + FETCH_REPLIES_ENABLED = ENV['FETCH_REPLIES_ENABLED'] == 'true' # debounce fetching all replies to minimize DoS FETCH_REPLIES_COOLDOWN_MINUTES = (ENV['FETCH_REPLIES_COOLDOWN_MINUTES'] || 15).to_i.minutes @@ -40,14 +40,4 @@ module Status::FetchRepliesConcern fetched_replies_at.nil? || fetched_replies_at <= FETCH_REPLIES_COOLDOWN_MINUTES.ago ) end - - def unsubscribed? - return false if local? - - !Follow.joins(:account).exists?( - target_account: account.id, - account: { domain: nil }, - created_at: ..updated_at - ) - end end diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index dc74de32f1..7173746f2d 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -83,13 +83,13 @@ class ActivityPub::FetchRemoteStatusService < BaseService def fetch_status(uri, id_is_known, on_behalf_of = nil) begin - fetch_resource(uri, id_is_known, on_behalf_of, raise_on_error: true) + fetch_resource(uri, id_is_known, on_behalf_of, raise_on_error: :all) rescue Mastodon::UnexpectedResponseError => e return unless e.response.code == 404 - # If this is a 404 from a status from an account that has no local followers, delete it - existing_status = Status.find_by(uri: uri) - if !existing_status.nil? && existing_status.unsubscribed? && existing_status.distributable? + # If this is a 404 from a public status from a remote account, delete it + existing_status = Status.remote.find_by(uri: uri) + if existing_status&.distributable? Rails.logger.debug { "FetchRemoteStatusService - Got 404 for orphaned status with URI #{uri}, deleting" } Tombstone.find_or_create_by(uri: uri, account: existing_status.account) RemoveStatusService.new.call(existing_status, redraft: false) diff --git a/spec/models/concerns/status/fetch_replies_concern_spec.rb b/spec/models/concerns/status/fetch_replies_concern_spec.rb index f152cf234a..e9c81d43b1 100644 --- a/spec/models/concerns/status/fetch_replies_concern_spec.rb +++ b/spec/models/concerns/status/fetch_replies_concern_spec.rb @@ -85,7 +85,6 @@ RSpec.describe Status::FetchRepliesConcern do it 'shows the status as unsubscribed' do expect(Status.unsubscribed).to eq([status]) - expect(status.unsubscribed?).to be(true) end end @@ -96,7 +95,6 @@ RSpec.describe Status::FetchRepliesConcern do it 'shows the status as unsubscribed' do expect(Status.unsubscribed).to eq([status]) - expect(status.unsubscribed?).to be(true) end end @@ -107,7 +105,6 @@ RSpec.describe Status::FetchRepliesConcern do it 'shows the status as unsubscribed' do expect(Status.unsubscribed).to eq([status]) - expect(status.unsubscribed?).to be(true) end end @@ -118,14 +115,12 @@ RSpec.describe Status::FetchRepliesConcern do it 'does not show the status as unsubscribed' do expect(Status.unsubscribed).to eq([]) - expect(status.unsubscribed?).to be(false) end end context 'when the status has no followers' do it 'shows the status as unsubscribed' do expect(Status.unsubscribed).to eq([status]) - expect(status.unsubscribed?).to be(true) end end end diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb index 847affd307..2503a58ac2 100644 --- a/spec/services/activitypub/fetch_remote_status_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb @@ -296,12 +296,6 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do it_behaves_like 'no delete' end end - - context 'when the status is from an account with local followers' do - let(:follow) { Fabricate(:follow, account: follower, target_account: sender, created_at: 2.days.ago) } - - it_behaves_like 'no delete' - end end end diff --git a/spec/workers/activitypub/fetch_all_replies_worker_spec.rb b/spec/workers/activitypub/fetch_all_replies_worker_spec.rb index 2b291e9624..4746d742d0 100644 --- a/spec/workers/activitypub/fetch_all_replies_worker_spec.rb +++ b/spec/workers/activitypub/fetch_all_replies_worker_spec.rb @@ -123,6 +123,7 @@ RSpec.describe ActivityPub::FetchAllRepliesWorker do end before do + stub_const('Status::FetchRepliesConcern::FETCH_REPLIES_ENABLED', true) allow(FetchReplyWorker).to receive(:push_bulk) all_items.each do |item| next if [top_note_uri, reply_note_uri].include? item From 966b8163829c367bf301fb1391a00dd75a539992 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 12 Mar 2025 12:52:38 +0100 Subject: [PATCH 04/12] Refactor `ActivityPub::FetchRepliesService` and `ActivityPub::FetchAllRepliesService` (#34149) --- app/lib/activitypub/activity/create.rb | 2 +- .../activitypub/fetch_all_replies_service.rb | 22 +----- .../activitypub/fetch_replies_service.rb | 60 +++++++-------- .../activitypub/fetch_all_replies_worker.rb | 2 +- .../activitypub/fetch_replies_worker.rb | 2 +- .../fetch_all_replies_service_spec.rb | 74 ++++++++++++++----- .../activitypub/fetch_replies_service_spec.rb | 14 ++-- 7 files changed, 95 insertions(+), 81 deletions(-) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index a756912592..f54e64ad7e 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -338,7 +338,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity collection = @object['replies'] return if collection.blank? - replies = ActivityPub::FetchRepliesService.new.call(status, collection, allow_synchronous_requests: false, request_id: @options[:request_id]) + replies = ActivityPub::FetchRepliesService.new.call(status.account.uri, collection, allow_synchronous_requests: false, request_id: @options[:request_id]) return unless replies.nil? uri = value_or_id(collection) diff --git a/app/services/activitypub/fetch_all_replies_service.rb b/app/services/activitypub/fetch_all_replies_service.rb index 9f92b7efee..765e5c8ae8 100644 --- a/app/services/activitypub/fetch_all_replies_service.rb +++ b/app/services/activitypub/fetch_all_replies_service.rb @@ -6,25 +6,15 @@ class ActivityPub::FetchAllRepliesService < ActivityPub::FetchRepliesService # Limit of replies to fetch per status MAX_REPLIES = (ENV['FETCH_REPLIES_MAX_SINGLE'] || 500).to_i - def call(collection_or_uri, status_uri, max_pages = nil, request_id: nil) - @allow_synchronous_requests = true - @collection_or_uri = collection_or_uri + def call(status_uri, collection_or_uri, max_pages: 1, request_id: nil) @status_uri = status_uri - @items, n_pages = collection_items(collection_or_uri, max_pages) - @items = filtered_replies - return if @items.nil? - - FetchReplyWorker.push_bulk(@items) { |reply_uri| [reply_uri, { 'request_id' => request_id }] } - - [@items, n_pages] + super end private - def filtered_replies - return if @items.nil? - + def filter_replies(items) # Find all statuses that we *shouldn't* update the replies for, and use that as a filter. # We don't assume that we have the statuses before they're created, # hence the negative filter - @@ -34,7 +24,7 @@ class ActivityPub::FetchAllRepliesService < ActivityPub::FetchRepliesService # # Typically we assume the number of replies we *shouldn't* fetch is smaller than the # replies we *should* fetch, so we also minimize the number of uris we should load here. - uris = @items.map { |item| value_or_id(item) } + uris = items.map { |item| value_or_id(item) } # Expand collection to get replies in the DB that were # - not included in the collection, @@ -61,8 +51,4 @@ class ActivityPub::FetchAllRepliesService < ActivityPub::FetchRepliesService Rails.logger.debug { "FetchAllRepliesService - #{@collection_or_uri}: Fetching filtered statuses: #{uris}" } uris end - - def filter_by_host? - false - end end diff --git a/app/services/activitypub/fetch_replies_service.rb b/app/services/activitypub/fetch_replies_service.rb index 72b9c0f5a6..f2e4f45104 100644 --- a/app/services/activitypub/fetch_replies_service.rb +++ b/app/services/activitypub/fetch_replies_service.rb @@ -6,53 +6,56 @@ class ActivityPub::FetchRepliesService < BaseService # Limit of fetched replies MAX_REPLIES = 5 - def call(parent_status, collection_or_uri, allow_synchronous_requests: true, request_id: nil) - @account = parent_status.account + 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 - @items, = collection_items(collection_or_uri) + @items, n_pages = collection_items(collection_or_uri, max_pages: max_pages) return if @items.nil? - FetchReplyWorker.push_bulk(filtered_replies) { |reply_uri| [reply_uri, { 'request_id' => request_id }] } + @items = filter_replies(@items) + FetchReplyWorker.push_bulk(@items) { |reply_uri| [reply_uri, { 'request_id' => request_id }] } - @items + [@items, n_pages] end private - def collection_items(collection_or_uri, max_pages = nil) + 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) - all_items = [] + items = [] n_pages = 1 while collection.is_a?(Hash) - items = case collection['type'] - when 'Collection', 'CollectionPage' - collection['items'] - when 'OrderedCollection', 'OrderedCollectionPage' - collection['orderedItems'] - end + items.concat(as_array(collection_page_items(collection))) - all_items.concat(as_array(items)) - - break if all_items.size >= MAX_REPLIES - break if !max_pages.nil? && n_pages >= max_pages + 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 - [all_items, n_pages] + [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 filter_by_host? && non_matching_uri_hosts?(@account.uri, collection_or_uri) + return if non_matching_uri_hosts?(@reference_uri, collection_or_uri) # NOTE: For backward compatibility reasons, Mastodon signs outgoing # queries incorrectly by default. @@ -70,20 +73,11 @@ class ActivityPub::FetchRepliesService < BaseService end end - def filtered_replies - if filter_by_host? - # Only fetch replies to the same server as the original status to avoid - # amplification attacks. + def filter_replies(items) + # Only fetch replies to the same server as the original status to avoid + # amplification attacks. - # Also limit to 5 fetched replies to limit potential for DoS. - @items.map { |item| value_or_id(item) }.reject { |uri| non_matching_uri_hosts?(@account.uri, uri) }.take(MAX_REPLIES) - else - @items.map { |item| value_or_id(item) }.take(MAX_REPLIES) - end - end - - # Whether replies with a different domain than the replied_to post should be rejected - def filter_by_host? - true + # Also limit to 5 fetched replies to limit potential for DoS. + items.map { |item| value_or_id(item) }.reject { |uri| non_matching_uri_hosts?(@reference_uri, uri) }.take(MAX_REPLIES) end end diff --git a/app/workers/activitypub/fetch_all_replies_worker.rb b/app/workers/activitypub/fetch_all_replies_worker.rb index 87eac321fa..e31ff17c23 100644 --- a/app/workers/activitypub/fetch_all_replies_worker.rb +++ b/app/workers/activitypub/fetch_all_replies_worker.rb @@ -51,7 +51,7 @@ class ActivityPub::FetchAllRepliesWorker replies_collection_or_uri = get_replies_uri(status_uri) return if replies_collection_or_uri.nil? - ActivityPub::FetchAllRepliesService.new.call(replies_collection_or_uri, status_uri, max_pages, **options.deep_symbolize_keys) + ActivityPub::FetchAllRepliesService.new.call(status_uri, replies_collection_or_uri, max_pages: max_pages, **options.deep_symbolize_keys) end def get_replies_uri(parent_status_uri) diff --git a/app/workers/activitypub/fetch_replies_worker.rb b/app/workers/activitypub/fetch_replies_worker.rb index d72bad7452..f9b3dbf171 100644 --- a/app/workers/activitypub/fetch_replies_worker.rb +++ b/app/workers/activitypub/fetch_replies_worker.rb @@ -7,7 +7,7 @@ class ActivityPub::FetchRepliesWorker sidekiq_options queue: 'pull', retry: 3 def perform(parent_status_id, replies_uri, options = {}) - ActivityPub::FetchRepliesService.new.call(Status.find(parent_status_id), replies_uri, **options.deep_symbolize_keys) + ActivityPub::FetchRepliesService.new.call(Status.find(parent_status_id).account.uri, replies_uri, **options.deep_symbolize_keys) rescue ActiveRecord::RecordNotFound true end diff --git a/spec/services/activitypub/fetch_all_replies_service_spec.rb b/spec/services/activitypub/fetch_all_replies_service_spec.rb index eadd5b10fa..241c1a8464 100644 --- a/spec/services/activitypub/fetch_all_replies_service_spec.rb +++ b/spec/services/activitypub/fetch_all_replies_service_spec.rb @@ -10,17 +10,17 @@ RSpec.describe ActivityPub::FetchAllRepliesService do let(:collection_uri) { 'http://example.com/replies/1' } let(:items) do - [ - 'http://example.com/self-reply-1', - 'http://example.com/self-reply-2', - 'http://example.com/self-reply-3', - 'http://other.com/other-reply-1', - 'http://other.com/other-reply-2', - 'http://other.com/other-reply-3', - 'http://example.com/self-reply-4', - 'http://example.com/self-reply-5', - 'http://example.com/self-reply-6', - ] + %w( + http://example.com/self-reply-1 + http://example.com/self-reply-2 + http://example.com/self-reply-3 + http://other.com/other-reply-1 + http://other.com/other-reply-2 + http://other.com/other-reply-3 + http://example.com/self-reply-4 + http://example.com/self-reply-5 + http://example.com/self-reply-6 + ) end let(:payload) do @@ -36,10 +36,21 @@ RSpec.describe ActivityPub::FetchAllRepliesService do it 'fetches more than the default maximum and from multiple domains' do allow(FetchReplyWorker).to receive(:push_bulk) - subject.call(payload, status.uri) + subject.call(status.uri, payload) - expect(FetchReplyWorker).to have_received(:push_bulk).with(%w(http://example.com/self-reply-1 http://example.com/self-reply-2 http://example.com/self-reply-3 http://other.com/other-reply-1 http://other.com/other-reply-2 http://other.com/other-reply-3 http://example.com/self-reply-4 - http://example.com/self-reply-5 http://example.com/self-reply-6)) + expect(FetchReplyWorker).to have_received(:push_bulk).with( + %w( + http://example.com/self-reply-1 + http://example.com/self-reply-2 + http://example.com/self-reply-3 + http://other.com/other-reply-1 + http://other.com/other-reply-2 + http://other.com/other-reply-3 + http://example.com/self-reply-4 + http://example.com/self-reply-5 + http://example.com/self-reply-6 + ) + ) end context 'with a recent status' do @@ -50,9 +61,20 @@ RSpec.describe ActivityPub::FetchAllRepliesService do it 'skips statuses that have been updated recently' do allow(FetchReplyWorker).to receive(:push_bulk) - subject.call(payload, status.uri) + subject.call(status.uri, payload) - expect(FetchReplyWorker).to have_received(:push_bulk).with(%w(http://example.com/self-reply-1 http://example.com/self-reply-3 http://other.com/other-reply-1 http://other.com/other-reply-2 http://other.com/other-reply-3 http://example.com/self-reply-4 http://example.com/self-reply-5 http://example.com/self-reply-6)) + expect(FetchReplyWorker).to have_received(:push_bulk).with( + %w( + http://example.com/self-reply-1 + http://example.com/self-reply-3 + http://other.com/other-reply-1 + http://other.com/other-reply-2 + http://other.com/other-reply-3 + http://example.com/self-reply-4 + http://example.com/self-reply-5 + http://example.com/self-reply-6 + ) + ) end end @@ -64,7 +86,7 @@ RSpec.describe ActivityPub::FetchAllRepliesService do it 'updates the time that fetched statuses were last fetched' do allow(FetchReplyWorker).to receive(:push_bulk) - subject.call(payload, status.uri) + subject.call(status.uri, payload) expect(Status.find_by(uri: 'http://other.com/other-reply-1').fetched_replies_at).to be >= 1.minute.ago end @@ -80,10 +102,22 @@ RSpec.describe ActivityPub::FetchAllRepliesService do it 'updates the unsubscribed replies' do allow(FetchReplyWorker).to receive(:push_bulk) - subject.call(payload, status.uri) + subject.call(status.uri, payload) - expect(FetchReplyWorker).to have_received(:push_bulk).with(%w(http://example.com/self-reply-1 http://example.com/self-reply-2 http://example.com/self-reply-3 http://other.com/other-reply-1 http://other.com/other-reply-2 http://other.com/other-reply-3 http://example.com/self-reply-4 - http://example.com/self-reply-5 http://example.com/self-reply-6 http://other.com/account/unsubscribed)) + expect(FetchReplyWorker).to have_received(:push_bulk).with( + %w( + http://example.com/self-reply-1 + http://example.com/self-reply-2 + http://example.com/self-reply-3 + http://other.com/other-reply-1 + http://other.com/other-reply-2 + http://other.com/other-reply-3 + http://example.com/self-reply-4 + http://example.com/self-reply-5 + http://example.com/self-reply-6 + http://other.com/account/unsubscribed + ) + ) end end end diff --git a/spec/services/activitypub/fetch_replies_service_spec.rb b/spec/services/activitypub/fetch_replies_service_spec.rb index e7d8d3528a..36159309f1 100644 --- a/spec/services/activitypub/fetch_replies_service_spec.rb +++ b/spec/services/activitypub/fetch_replies_service_spec.rb @@ -40,7 +40,7 @@ RSpec.describe ActivityPub::FetchRepliesService do it 'queues the expected worker' do allow(FetchReplyWorker).to receive(:push_bulk) - subject.call(status, payload) + subject.call(status.account.uri, payload) expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1']) end @@ -50,7 +50,7 @@ RSpec.describe ActivityPub::FetchRepliesService do it 'spawns workers for up to 5 replies on the same server' do allow(FetchReplyWorker).to receive(:push_bulk) - subject.call(status, payload) + subject.call(status.account.uri, payload) expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) end @@ -64,7 +64,7 @@ RSpec.describe ActivityPub::FetchRepliesService do it 'spawns workers for up to 5 replies on the same server' do allow(FetchReplyWorker).to receive(:push_bulk) - subject.call(status, collection_uri) + subject.call(status.account.uri, collection_uri) expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) end @@ -85,7 +85,7 @@ RSpec.describe ActivityPub::FetchRepliesService do it 'spawns workers for up to 5 replies on the same server' do allow(FetchReplyWorker).to receive(:push_bulk) - subject.call(status, payload) + subject.call(status.account.uri, payload) expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) end @@ -99,7 +99,7 @@ RSpec.describe ActivityPub::FetchRepliesService do it 'spawns workers for up to 5 replies on the same server' do allow(FetchReplyWorker).to receive(:push_bulk) - subject.call(status, collection_uri) + subject.call(status.account.uri, collection_uri) expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) end @@ -124,7 +124,7 @@ RSpec.describe ActivityPub::FetchRepliesService do it 'spawns workers for up to 5 replies on the same server' do allow(FetchReplyWorker).to receive(:push_bulk) - subject.call(status, payload) + subject.call(status.account.uri, payload) expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) end @@ -138,7 +138,7 @@ RSpec.describe ActivityPub::FetchRepliesService do it 'spawns workers for up to 5 replies on the same server' do allow(FetchReplyWorker).to receive(:push_bulk) - subject.call(status, collection_uri) + subject.call(status.account.uri, collection_uri) expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5']) end From fef446d22c96b3ef77cd9660c83328b9be9b2a51 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:51:16 +0100 Subject: [PATCH 05/12] New Crowdin Translations (automated) (#34136) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/da.json | 2 +- app/javascript/mastodon/locales/sl.json | 245 +++++++++++++++++------- app/javascript/mastodon/locales/tt.json | 4 + app/javascript/mastodon/locales/uk.json | 1 + config/locales/activerecord.el.yml | 4 + config/locales/activerecord.nl.yml | 2 +- config/locales/activerecord.sl.yml | 9 + config/locales/activerecord.tt.yml | 43 ++++- config/locales/devise.tt.yml | 12 ++ config/locales/doorkeeper.sl.yml | 2 + config/locales/el.yml | 1 + config/locales/et.yml | 2 + config/locales/nl.yml | 10 +- config/locales/simple_form.is.yml | 5 + config/locales/simple_form.lv.yml | 5 + config/locales/simple_form.nl.yml | 10 +- config/locales/simple_form.sl.yml | 23 +++ config/locales/sl.yml | 102 ++++++++++ config/locales/uk.yml | 3 +- 19 files changed, 399 insertions(+), 86 deletions(-) diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 91ae5804f8..91ec0b742e 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -153,7 +153,7 @@ "column.domain_blocks": "Blokerede domæner", "column.edit_list": "Redigér liste", "column.favourites": "Favoritter", - "column.firehose": "Realtids-strømme", + "column.firehose": "Aktuelt", "column.follow_requests": "Følgeanmodninger", "column.home": "Hjem", "column.list_members": "Håndtér listemedlemmer", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index f2ead1e117..9af93e3f7f 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -1,10 +1,10 @@ { "about.blocks": "Moderirani strežniki", "about.contact": "Stik:", - "about.disclaimer": "Mastodon je prosto, odprto-kodno programje in blagovna znamka Mastodon gGmbH.", + "about.disclaimer": "Mastodon je prosto, odprtokodno programje in blagovna znamka podjetja Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "Razlog ni na voljo", - "about.domain_blocks.preamble": "Mastodon vam splošno omogoča ogled vsebin in interakcijo z uporabniki iz vseh drugih strežnikov v fediverzumu. To so izjeme, opravljene na tem strežniku.", - "about.domain_blocks.silenced.explanation": "V splošnem ne boste videli profilov in vsebin s tega strežnika, če jih eksplicino ne poiščete ali nanje naročite s sledenjem.", + "about.domain_blocks.preamble": "Mastodon vam na splošno omogoča ogled vsebin in interakcijo z uporabniki z vseh drugih strežnikov v fediverzumu. Tu so navedene izjeme, ki jih postavlja ta strežnik.", + "about.domain_blocks.silenced.explanation": "V splošnem ne boste videli profilov in vsebin s tega strežnika, razen če jih izrecno poiščete ali jim začnete slediti.", "about.domain_blocks.silenced.title": "Omejeno", "about.domain_blocks.suspended.explanation": "Nobeni podatki s tega strežnika ne bodo obdelani, shranjeni ali izmenjani, zaradi česar je nemogoča kakršna koli interakcija ali komunikacija z uporabniki s tega strežnika.", "about.domain_blocks.suspended.title": "Suspendiran", @@ -29,11 +29,11 @@ "account.endorse": "Izpostavi v profilu", "account.featured_tags.last_status_at": "Zadnja objava {date}", "account.featured_tags.last_status_never": "Ni objav", - "account.featured_tags.title": "Izpostavljeni ključniki {name}", + "account.featured_tags.title": "Izpostavljeni ključniki osebe {name}", "account.follow": "Sledi", "account.follow_back": "Sledi nazaj", "account.followers": "Sledilci", - "account.followers.empty": "Nihče ne sledi temu uporabniku.", + "account.followers.empty": "Nihče še ne sledi temu uporabniku.", "account.followers_counter": "{count, plural, one {{counter} sledilec} two {{counter} sledilca} few {{counter} sledilci} other {{counter} sledilcev}}", "account.following": "Sledim", "account.following_counter": "{count, plural, one {{counter} sleden} two {{counter} sledena} few {{counter} sledeni} other {{counter} sledenih}}", @@ -45,9 +45,9 @@ "account.languages": "Spremeni naročene jezike", "account.link_verified_on": "Lastništvo te povezave je bilo preverjeno {date}", "account.locked_info": "Stanje zasebnosti računa je nastavljeno na zaklenjeno. Lastnik ročno pregleda, kdo ga lahko spremlja.", - "account.media": "Mediji", + "account.media": "Predstavnosti", "account.mention": "Omeni @{name}", - "account.moved_to": "{name} nakazuje, da ima zdaj nov račun:", + "account.moved_to": "{name} sporoča, da ima zdaj nov račun:", "account.mute": "Utišaj @{name}", "account.mute_notifications_short": "Utišaj obvestila", "account.mute_short": "Utišaj", @@ -68,14 +68,14 @@ "account.unblock_short": "Odblokiraj", "account.unendorse": "Ne vključi v profil", "account.unfollow": "Ne sledi več", - "account.unmute": "Odtišaj @{name}", + "account.unmute": "Povrni glas @{name}", "account.unmute_notifications_short": "Izklopi utišanje obvestil", - "account.unmute_short": "Odtišaj", - "account_note.placeholder": "Kliknite za dodajanje opombe", + "account.unmute_short": "Povrni glas", + "account_note.placeholder": "Kliknite, da dodate opombo", "admin.dashboard.daily_retention": "Mera ohranjanja uporabnikov po dnevih od registracije", "admin.dashboard.monthly_retention": "Mera ohranjanja uporabnikov po mesecih od registracije", "admin.dashboard.retention.average": "Povprečje", - "admin.dashboard.retention.cohort": "Mesec prijave", + "admin.dashboard.retention.cohort": "Mesec registracije", "admin.dashboard.retention.cohort_size": "Novi uporabniki", "admin.impact_report.instance_accounts": "Profili računov, ki bi jih s tem izbrisali", "admin.impact_report.instance_followers": "Sledilci, ki bi jih izgubili naši uporabniki", @@ -87,44 +87,61 @@ "alert.unexpected.title": "Ojoj!", "alt_text_badge.title": "Nadomestno besedilo", "alt_text_modal.add_alt_text": "Dodaj nadomestno besedilo", + "alt_text_modal.add_text_from_image": "Dodaj besedilo iz slike", "alt_text_modal.cancel": "Prekliči", "alt_text_modal.change_thumbnail": "Spremeni sličico", + "alt_text_modal.describe_for_people_with_hearing_impairments": "Podaj opis za ljudi s težavami s sluhom ...", + "alt_text_modal.describe_for_people_with_visual_impairments": "Podaj opis za slabovidne ...", "alt_text_modal.done": "Opravljeno", - "announcement.announcement": "Obvestilo", + "announcement.announcement": "Oznanilo", + "annual_report.summary.archetype.booster": "Lovec na trende", + "annual_report.summary.archetype.lurker": "Tiholazec", + "annual_report.summary.archetype.oracle": "Orakelj", + "annual_report.summary.archetype.pollster": "Anketar", + "annual_report.summary.archetype.replier": "Priljudnež", "annual_report.summary.followers.followers": "sledilcev", - "annual_report.summary.followers.total": "", + "annual_report.summary.followers.total": "skupaj {count}", + "annual_report.summary.here_it_is": "Tule je povzetek vašega leta {year}:", "annual_report.summary.highlighted_post.by_favourites": "- najpriljubljenejša objava", + "annual_report.summary.highlighted_post.by_reblogs": "- največkrat izpostavljena objava", + "annual_report.summary.highlighted_post.by_replies": "- objava z največ odgovori", + "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.summary.most_used_app.most_used_app": "najpogosteje uporabljena aplikacija", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "največkrat uporabljen ključnik", "annual_report.summary.most_used_hashtag.none": "Brez", "annual_report.summary.new_posts.new_posts": "nove objave", + "annual_report.summary.percentile.text": "S tem ste se uvrstili med zgornjih uporabnikov domene {domain}.", + "annual_report.summary.percentile.we_wont_tell_bernie": "Živi duši ne bomo povedali.", "annual_report.summary.thanks": "Hvala, ker ste del Mastodona!", "attachments_list.unprocessed": "(neobdelano)", "audio.hide": "Skrij zvok", - "block_modal.remote_users_caveat": "Od strežnika {domain} bomo zahtevali, da spoštuje vašo odločitev. Izpolnjevanje zahteve ni zagotovljeno, ker nekateri strežniki blokiranja obravnavajo drugače. Javne objave bodo morda še vedno vidne neprijavljenim uporabnikom.", + "block_modal.remote_users_caveat": "Strežnik {domain} bomo pozvali, naj spoštuje vašo odločitev. Kljub temu pa ni gotovo, da bo strežnik prošnjo upošteval, saj nekateri strežniki blokiranja obravnavajo drugače. Javne objave bodo morda še vedno vidne neprijavljenim uporabnikom.", "block_modal.show_less": "Pokaži manj", "block_modal.show_more": "Pokaži več", - "block_modal.they_cant_mention": "Ne morejo vas omenjati ali vam slediti.", - "block_modal.they_cant_see_posts": "Ne vidijo vaših objav, vi pa ne njihovih.", - "block_modal.they_will_know": "Ne morejo videti, da so blokirani.", - "block_modal.title": "Blokiraj uporabnika?", - "block_modal.you_wont_see_mentions": "Objav, ki jih omenjajo, ne boste videli.", - "boost_modal.combo": "Če želite preskočiti to, lahko pritisnete {combo}", - "boost_modal.reblog": "Izpostavi objavo?", + "block_modal.they_cant_mention": "Ne more vas omenjati ali vam slediti.", + "block_modal.they_cant_see_posts": "Ne vidi vaših objav, vi pa ne njegovih.", + "block_modal.they_will_know": "Ne more videti, da je blokiran.", + "block_modal.title": "Blokiram uporabnika?", + "block_modal.you_wont_see_mentions": "Objav, ki ga omenjajo, ne boste videli.", + "boost_modal.combo": "Če želite naslednjič to preskočiti, lahko pritisnete {combo}", + "boost_modal.reblog": "Izpostavim objavo?", "boost_modal.undo_reblog": "Ali želite preklicati izpostavitev objave?", "bundle_column_error.copy_stacktrace": "Kopiraj poročilo o napaki", "bundle_column_error.error.body": "Zahtevane strani ni mogoče upodobiti. Vzrok težave je morda hrošč v naši kodi ali pa nezdružljivost z brskalnikom.", "bundle_column_error.error.title": "Oh, ne!", "bundle_column_error.network.body": "Pri poskusu nalaganja te strani je prišlo do napake. Vzrok je lahko začasna težava z vašo internetno povezavo ali s tem strežnikom.", - "bundle_column_error.network.title": "Napaka v omrežju", + "bundle_column_error.network.title": "Omrežna napaka", "bundle_column_error.retry": "Poskusi znova", "bundle_column_error.return": "Nazaj domov", "bundle_column_error.routing.body": "Zahtevane strani ni mogoče najti. Ali ste prepričani, da je naslov URL v naslovni vrstici pravilen?", "bundle_column_error.routing.title": "404", "bundle_modal_error.close": "Zapri", + "bundle_modal_error.message": "Med nalaganjem prikaza je prišlo do napake.", "bundle_modal_error.retry": "Poskusi znova", "closed_registrations.other_server_instructions": "Ker je Mastodon decentraliziran, lahko ustvarite račun na drugem strežniku in ste še vedno v interakciji s tem.", - "closed_registrations_modal.description": "Odpiranje računa na {domain} trenutno ni možno, upoštevajte pa, da ne potrebujete računa prav na {domain}, da bi uporabljali Mastodon.", + "closed_registrations_modal.description": "Odpiranje računa na domeni {domain} trenutno ni možno, upoštevajte pa, da ne potrebujete računa prav na domeni {domain}, da bi uporabljali Mastodon.", "closed_registrations_modal.find_another_server": "Najdi drug strežnik", - "closed_registrations_modal.preamble": "Mastodon je decentraliziran, kar pomeni, da ni pomembno, kje ustvarite svoj račun; od koder koli je omogočeno sledenje in interakcija z vsemi s tega strežnika. Strežnik lahko gostite tudi sami!", + "closed_registrations_modal.preamble": "Mastodon je decentraliziran, kar pomeni, da ni pomembno, kje ustvarite svoj račun; od koder koli je mogoče slediti in komunicirati z vsemi s tega strežnika. Strežnik lahko gostite tudi sami!", "closed_registrations_modal.title": "Registracija v Mastodon", "column.about": "O programu", "column.blocks": "Blokirani uporabniki", @@ -137,7 +154,7 @@ "column.edit_list": "Uredi seznam", "column.favourites": "Priljubljeni", "column.firehose": "Viri v živo", - "column.follow_requests": "Sledi prošnjam", + "column.follow_requests": "Prošnje za sledenje", "column.home": "Domov", "column.list_members": "Upravljaj člane seznama", "column.lists": "Seznami", @@ -155,25 +172,25 @@ "column_search.cancel": "Prekliči", "column_subheading.settings": "Nastavitve", "community.column_settings.local_only": "Samo krajevno", - "community.column_settings.media_only": "Samo mediji", + "community.column_settings.media_only": "Samo predstavnosti", "community.column_settings.remote_only": "Samo oddaljeno", "compose.language.change": "Spremeni jezik", - "compose.language.search": "Poišči jezik ...", + "compose.language.search": "Poišči jezike ...", "compose.published.body": "Objavljeno.", "compose.published.open": "Odpri", "compose.saved.body": "Objava shranjena.", - "compose_form.direct_message_warning_learn_more": "Izvej več", - "compose_form.encryption_warning": "Objave na Mastodonu niso šifrirane od kraja do kraja. Prek Mastodona ne delite nobenih občutljivih informacij.", + "compose_form.direct_message_warning_learn_more": "Več o tem", + "compose_form.encryption_warning": "Objave na Mastodonu niso šifrirane od konca do konca. Prek Mastodona ne delite nobenih občutljivih informacij.", "compose_form.hashtag_warning": "Ta objava ne bo navedena pod nobenim ključnikom, ker ni javna. Samo javne objave lahko iščete s ključniki.", "compose_form.lock_disclaimer": "Vaš račun ni {locked}. Vsakdo vam lahko sledi in si ogleda objave, ki so namenjene samo sledilcem.", "compose_form.lock_disclaimer.lock": "zaklenjen", "compose_form.placeholder": "O čem razmišljate?", "compose_form.poll.duration": "Trajanje ankete", - "compose_form.poll.multiple": "Več možnosti", + "compose_form.poll.multiple": "Izbira več možnosti", "compose_form.poll.option_placeholder": "Možnost {number}", - "compose_form.poll.single": "Ena izbira", - "compose_form.poll.switch_to_multiple": "Spremenite anketo, da omogočite več izbir", - "compose_form.poll.switch_to_single": "Spremenite anketo, da omogočite eno izbiro", + "compose_form.poll.single": "Izbira ene možnosti", + "compose_form.poll.switch_to_multiple": "Spremenite anketo, da omogočite izbiro več možnosti", + "compose_form.poll.switch_to_single": "Spremenite anketo, da omogočite izbiro ene možnosti", "compose_form.poll.type": "Slog", "compose_form.publish": "Objavi", "compose_form.publish_form": "Objavi", @@ -191,17 +208,24 @@ "confirmations.delete_list.message": "Ali ste prepričani, da želite trajno izbrisati ta seznam?", "confirmations.delete_list.title": "Želite izbrisati seznam?", "confirmations.discard_edit_media.confirm": "Opusti", - "confirmations.discard_edit_media.message": "Imate ne shranjene spremembe za medijski opis ali predogled; jih želite kljub temu opustiti?", + "confirmations.discard_edit_media.message": "Spremenjenega opisa predstavnosti ali predogleda niste shranili. Želite spremembe kljub temu opustiti?", "confirmations.edit.confirm": "Uredi", "confirmations.edit.message": "Urejanje bo prepisalo sporočilo, ki ga trenutno sestavljate. Ali ste prepričani, da želite nadaljevati?", "confirmations.edit.title": "Želite prepisati objavo?", + "confirmations.follow_to_list.confirm": "Sledi in dodaj na seznam", + "confirmations.follow_to_list.message": "Osebi {name} morate slediti, preden jo lahko dodate na seznam.", + "confirmations.follow_to_list.title": "Naj sledim uporabniku?", "confirmations.logout.confirm": "Odjava", "confirmations.logout.message": "Ali ste prepričani, da se želite odjaviti?", "confirmations.logout.title": "Se želite odjaviti?", - "confirmations.mute.confirm": "Utišanje", + "confirmations.missing_alt_text.confirm": "Dodaj nadomestno besedilo", + "confirmations.missing_alt_text.message": "Vaša objava vsebuje predstavnosti brez nadomestnega besedila. Če jih dodatno opišete, bodo dostopne večji množici ljudi.", + "confirmations.missing_alt_text.secondary": "Vseeno objavi", + "confirmations.missing_alt_text.title": "Dodam nadomestno besedilo?", + "confirmations.mute.confirm": "Utišaj", "confirmations.redraft.confirm": "Izbriši in preoblikuj", - "confirmations.redraft.message": "Ali ste prepričani, da želite izbrisati ta status in ga preoblikovati? Vzljubi in izpostavitve bodo izgubljeni, odgovori na izvirno objavo pa bodo osiroteli.", - "confirmations.redraft.title": "Želite izbrisati in predelati objavo?", + "confirmations.redraft.message": "Ali ste prepričani, da želite izbrisati to objavo in jo preoblikovati? Izkazi priljubljenosti in izpostavitve bodo izgubljeni, odgovori na izvirno objavo pa bodo osiroteli.", + "confirmations.redraft.title": "Želite izbrisati in preoblikovati objavo?", "confirmations.reply.confirm": "Odgovori", "confirmations.reply.message": "Odgovarjanje bo prepisalo sporočilo, ki ga trenutno sestavljate. Ali ste prepričani, da želite nadaljevati?", "confirmations.reply.title": "Želite prepisati objavo?", @@ -217,7 +241,7 @@ "conversation.with": "Z {names}", "copy_icon_button.copied": "Kopirano v odložišče", "copypaste.copied": "Kopirano", - "copypaste.copy_to_clipboard": "Kopiraj na odložišče", + "copypaste.copy_to_clipboard": "Kopiraj v odložišče", "directory.federated": "Iz znanega fediverzuma", "directory.local": "Samo iz {domain}", "directory.new_arrivals": "Novi prišleki", @@ -226,28 +250,34 @@ "disabled_account_banner.text": "Vaš račun {disabledAccount} je trenutno onemogočen.", "dismissable_banner.community_timeline": "To so najnovejše javne objave oseb, katerih računi gostujejo na {domain}.", "dismissable_banner.dismiss": "Opusti", + "dismissable_banner.explore_links": "Danes po fediverzumu najbolj odmevajo te novice. Višje na seznamu so novejše vesti bolj raznolikih objaviteljev.", + "dismissable_banner.explore_statuses": "Danes so po fediverzumu pozornost pritegnile te objave. Višje na seznamu so novejše, bolj izpostavljene in bolj priljubljene objave.", + "dismissable_banner.explore_tags": "Danes se po fediverzumu najpogosteje uporabljajo ti ključniki. Višje na seznamu so ključniki, ki jih uporablja več različnih ljudi.", + "dismissable_banner.public_timeline": "To so najnovejše javne objave ljudi s fediverzuma, ki jim sledijo ljudje na domeni {domain}.", "domain_block_modal.block": "Blokiraj strežnik", "domain_block_modal.block_account_instead": "Namesto tega blokiraj @{name}", "domain_block_modal.they_can_interact_with_old_posts": "Osebe s tega strežnika se lahko odzivajo na vaše stare objave.", "domain_block_modal.they_cant_follow": "Nihče s tega strežnika vam ne more slediti.", "domain_block_modal.they_wont_know": "Ne bodo vedeli, da so blokirani.", "domain_block_modal.title": "Blokiraj domeno?", + "domain_block_modal.you_will_lose_num_followers": "Izgubili boste {followersCount, plural, one {{followersCountDisplay} sledilca} two {{followersCountDisplay} sledilca} few {{followersCountDisplay} sledilce} other {{followersCountDisplay} sledilcev}} in {followingCount, plural, one {{followingCountDisplay} osebo, ki ji sledite} two {{followingCountDisplay} osebi, ki jima sledite} few {{followingCountDisplay} osebe, ki jim sledite} other {{followingCountDisplay} oseb, ki jim sledite}}.", + "domain_block_modal.you_will_lose_relationships": "Izgubili boste vse sledilce in ljudi, ki jim sledite na tem strežniku.", "domain_block_modal.you_wont_see_posts": "Objav ali obvestil uporabnikov s tega strežnika ne boste videli.", - "domain_pill.activitypub_lets_connect": "Omogoča vam povezovanje in interakcijo z ljudmi, ki niso samo na Mastodonu, ampak tudi na drugih družabnih platformah.", - "domain_pill.activitypub_like_language": "Protokol ActivityPub je kot jezik, s katerim se Mastodon pogovarja z drugimi družabnimi omrežji.", + "domain_pill.activitypub_lets_connect": "Omogoča vam povezovanje in interakcijo z ljudmi, ki niso samo na Mastodonu, ampak tudi na drugih družbenih platformah.", + "domain_pill.activitypub_like_language": "Protokol ActivityPub je kot jezik, v katerem se Mastodon pogovarja z drugimi družabnimi omrežji.", "domain_pill.server": "Strežnik", - "domain_pill.their_handle": "Njihova ročica:", - "domain_pill.their_server": "Njihovo digitalno domovanje, kjer bivajo vse njihove objave.", - "domain_pill.their_username": "Njihov edinstveni identifikator na njihovem strežniku. Uporabnike z istim uporabniškim imenom lahko najdete na različnih strežnikih.", + "domain_pill.their_handle": "Njegova/njena ročica:", + "domain_pill.their_server": "Njegovo/njeno digitalno domovanje, kjer bivajo vse njegove/njene objave.", + "domain_pill.their_username": "Njegov/njen edinstveni identifikator na njegovem/njenem strežniku. Uporabnike z istim uporabniškim imenom lahko najdete na različnih strežnikih.", "domain_pill.username": "Uporabniško ime", "domain_pill.whats_in_a_handle": "Kaj je v ročici?", - "domain_pill.who_they_are": "Ker ročice povedo, kdo je kdo in kje so, ste lahko z osebami v interakciji prek družabnega spleta .", - "domain_pill.who_you_are": "Ker ročice povedo, kdo ste in kje ste, ste lahko z osebami v interakciji prek družabnega spleta .", + "domain_pill.who_they_are": "Ker ročice povedo, kdo je kdo in kje je, lahko komunicirate z ljudmi po vsem spletu družbenih .", + "domain_pill.who_you_are": "Ker ročice povedo, kdo ste in kje ste, lahko komunicirate z ljudmi po vsem spletu družbenih .", "domain_pill.your_handle": "Vaša ročica:", - "domain_pill.your_server": "Vaše digitalno domovanje, kjer bivajo vse vaše objave. Vam ta ni všeč? Prenesite ga med strežniki kadar koli in z njim tudi svoje sledilce.", + "domain_pill.your_server": "Vaše digitalno domovanje, kjer bivajo vse vaše objave. Vam ni všeč? Kadar koli ga prenesite med strežniki in z njim tudi svoje sledilce.", "domain_pill.your_username": "Vaš edinstveni identifikator na tem strežniku. Uporabnike z istim uporabniškim imenom je možno najti na različnih strežnikih.", "embed.instructions": "Vstavite to objavo na svojo spletno stran tako, da kopirate spodnjo kodo.", - "embed.preview": "Tako bo izgledalo:", + "embed.preview": "Takole bo videti:", "emoji_button.activity": "Dejavnost", "emoji_button.clear": "Počisti", "emoji_button.custom": "Po meri", @@ -263,32 +293,32 @@ "emoji_button.search_results": "Rezultati iskanja", "emoji_button.symbols": "Simboli", "emoji_button.travel": "Potovanja in kraji", - "empty_column.account_hides_collections": "Ta uporabnik se je odločil, da te informacije ne bo dal na voljo", + "empty_column.account_hides_collections": "Ta uporabnik se je odločil, da te informacije ne bo delil", "empty_column.account_suspended": "Račun je suspendiran", "empty_column.account_timeline": "Tukaj ni objav!", "empty_column.account_unavailable": "Profil ni na voljo", "empty_column.blocks": "Niste še blokirali nobenega uporabnika.", "empty_column.bookmarked_statuses": "Zaenkrat še nimate zaznamovanih objav. Ko objavo zaznamujete, se pojavi tukaj.", - "empty_column.community": "Krajevna časovnica je prazna. Napišite nekaj javnega, da se bo snežna kepa zakotalila!", + "empty_column.community": "Krajevna časovnica je prazna. Napišite nekaj javnega, da se začne polniti!", "empty_column.direct": "Nimate še nobenih zasebnih omemb. Ko jih boste poslali ali prejeli, se bodo prikazale tukaj.", "empty_column.domain_blocks": "Zaenkrat ni blokiranih domen.", - "empty_column.explore_statuses": "Trenutno ni nič v trendu. Preverite znova kasneje!", - "empty_column.favourited_statuses": "Nimate priljubljenih objav. Ko boste vzljubili kakšno, bo prikazana tukaj.", - "empty_column.favourites": "Nihče še ni vzljubil te objave. Ko jo bo nekdo, se bo pojavila tukaj.", + "empty_column.explore_statuses": "Trenutno ni novih trendov. Preverite znova kasneje!", + "empty_column.favourited_statuses": "Nimate priljubljenih objav. Ko boste kakšno dodali med priljubljene, bo prikazana tukaj.", + "empty_column.favourites": "Nihče še ni vzljubil te objave. Ko jo bo nekdo, bo naveden tukaj.", "empty_column.follow_requests": "Nimate prošenj za sledenje. Ko boste prejeli kakšno, se bo prikazala tukaj.", - "empty_column.followed_tags": "Zaenkrat ne sledite še nobenemu ključniku. Ko boste, se bodo pojavili tukaj.", - "empty_column.hashtag": "V tem ključniku še ni nič.", - "empty_column.home": "Vaša domača časovnica je prazna! Sledite več osebam, da jo zapolnite. {suggestions}", - "empty_column.list": "Na tem seznamu ni ničesar. Ko bodo člani tega seznama objavili nove statuse, se bodo pojavili tukaj.", + "empty_column.followed_tags": "Zaenkrat ne sledite še nobenemu ključniku. Ko boste, se bo pojavil tukaj.", + "empty_column.hashtag": "V tem ključniku ni še nič.", + "empty_column.home": "Vaša domača časovnica je prazna! Sledite več osebam, da jo zapolnite.", + "empty_column.list": "Na tem seznamu ni ničesar. Ko bodo člani tega seznama kaj objavili, se bodo te objave pojavile tukaj.", "empty_column.mutes": "Niste utišali še nobenega uporabnika.", "empty_column.notification_requests": "Vse prebrano! Tu ni ničesar več. Ko prejmete nova obvestila, se bodo pojavila tu glede na vaše nastavitve.", "empty_column.notifications": "Nimate še nobenih obvestil. Povežite se z drugimi, da začnete pogovor.", - "empty_column.public": "Tukaj ni ničesar! Da ga napolnite, napišite nekaj javnega ali pa ročno sledite uporabnikom iz drugih strežnikov", + "empty_column.public": "Tukaj ni ničesar! Napišite nekaj javnega ali pa ročno sledite uporabnikom iz drugih strežnikov, da se bo napolnilo", "error.unexpected_crash.explanation": "Zaradi hrošča v naši kodi ali težave z združljivostjo brskalnika te strani ni mogoče ustrezno prikazati.", - "error.unexpected_crash.explanation_addons": "Te strani ni mogoče ustrezno prikazati. To napako najverjetneje povzroča dodatek briskalnika ali samodejna orodja za prevajanje.", + "error.unexpected_crash.explanation_addons": "Te strani ni mogoče ustrezno prikazati. To napako najverjetneje povzroča dodatek brskalnika ali samodejna orodja za prevajanje.", "error.unexpected_crash.next_steps": "Poskusite osvežiti stran. Če to ne pomaga, boste morda še vedno lahko uporabljali Mastodon prek drugega brskalnika ali z domorodno aplikacijo.", "error.unexpected_crash.next_steps_addons": "Poskusite jih onemogočiti in osvežiti stran. Če to ne pomaga, boste morda še vedno lahko uporabljali Mastodon prek drugega brskalnika ali z domorodno aplikacijo.", - "errors.unexpected_crash.copy_stacktrace": "Kopiraj sledenje skladu na odložišče", + "errors.unexpected_crash.copy_stacktrace": "Kopiraj sled sklada na odložišče", "errors.unexpected_crash.report_issue": "Prijavi težavo", "explore.suggested_follows": "Ljudje", "explore.title": "Razišči", @@ -297,7 +327,7 @@ "explore.trending_tags": "Ključniki", "filter_modal.added.context_mismatch_explanation": "Ta kategorija filtra ne velja za kontekst, v katerem ste dostopali do te objave. Če želite, da je objava filtrirana tudi v tem kontekstu, morate urediti filter.", "filter_modal.added.context_mismatch_title": "Neujemanje konteksta!", - "filter_modal.added.expired_explanation": "Ta kategorija filtra je pretekla, morali boste spremeniti datum veljavnosti, da bo veljal še naprej.", + "filter_modal.added.expired_explanation": "Ta kategorija filtra je pretekla. Morali boste spremeniti datum veljavnosti, da bo veljal še naprej.", "filter_modal.added.expired_title": "Filter je pretekel!", "filter_modal.added.review_and_configure": "Če želite pregledati in nadalje prilagoditi kategorijo filtra, obiščite {settings_link}.", "filter_modal.added.review_and_configure_title": "Nastavitve filtra", @@ -312,6 +342,7 @@ "filter_modal.select_filter.title": "Filtriraj to objavo", "filter_modal.title.status": "Filtrirajte objavo", "filter_warning.matches_filter": "Se ujema s filtrom »{title}«", + "filtered_notifications_banner.pending_requests": "Od {count, plural, =0 {nikogar, ki bi ga poznali} one {nekoga, ki ga morda poznate} two {dveh ljudi, ki ju morda poznate} other {ljudi, ki jih morda poznate}}", "filtered_notifications_banner.title": "Filtrirana obvestila", "firehose.all": "Vse", "firehose.local": "Ta strežnik", @@ -360,9 +391,12 @@ "hashtag.counter_by_uses_today": "{count, plural, one {{counter} objava} two {{counter} objavi} few {{counter} objav} other {{counter} objav}}", "hashtag.follow": "Sledi ključniku", "hashtag.unfollow": "Nehaj slediti ključniku", - "hashtags.and_other": "…in še {count, plural, other {#}}", + "hashtags.and_other": "… in še {count, plural, other {#}}", + "hints.profiles.followers_may_be_missing": "Sledilci za ta profil morda manjkajo.", + "hints.profiles.follows_may_be_missing": "Osebe, ki jim ta profil sledi, morda manjkajo.", "hints.profiles.posts_may_be_missing": "Nekatere objave s tega profila morda manjkajo.", "hints.profiles.see_more_followers": "Pokaži več sledilcev na {domain}", + "hints.profiles.see_more_follows": "Pokaži več sledenih ljudi na zbirališču {domain}", "hints.profiles.see_more_posts": "Pokaži več objav na {domain}", "hints.threads.replies_may_be_missing": "Odgovori z drugih strežnikov morda manjkajo.", "hints.threads.see_more": "Pokaži več odgovorov na {domain}", @@ -373,9 +407,25 @@ "home.pending_critical_update.link": "Glejte posodobitve", "home.pending_critical_update.title": "Na voljo je kritična varnostna posodobbitev!", "home.show_announcements": "Pokaži obvestila", + "ignore_notifications_modal.disclaimer": "Mastodon ne more obveščati uporabnikov, da ste prezrli njihova obvestila. Tudi če jih prezrete, jih lahko uporabniki še vedno pošiljajo.", + "ignore_notifications_modal.filter_instead": "Raje filtriraj", + "ignore_notifications_modal.filter_to_act_users": "Še vedno boste lahko sprejeli, zavrnili ali prijavili uporabnike", "ignore_notifications_modal.filter_to_avoid_confusion": "Filtriranje pomaga pri izogibanju morebitni zmedi", "ignore_notifications_modal.filter_to_review_separately": "Filtrirana obvestila lahko pregledate ločeno", "ignore_notifications_modal.ignore": "Prezri obvestila", + "ignore_notifications_modal.limited_accounts_title": "Naj prezrem obvestila moderiranih računov?", + "ignore_notifications_modal.new_accounts_title": "Naj prezrem obvestila novih računov?", + "ignore_notifications_modal.not_followers_title": "Naj prezrem obvestila ljudi, ki vam ne sledijo?", + "ignore_notifications_modal.not_following_title": "Naj prezrem obvestila ljudi, ki jim ne sledite?", + "ignore_notifications_modal.private_mentions_title": "Naj prezrem obvestila od nezaželenih zasebnih omemb?", + "info_button.label": "Pomoč", + "info_button.what_is_alt_text": "

Kaj je nadomestno besedilo?

Z nadomestnim besedilom dodatno opišemo sliko in tako pomagamo slabovidnim, ljudem s slabo internetno povezavo in tistim, ki jim manjka kontekst.

Vaša objava bo veliko bolj dostopna in razumljiva, če boste napisali jasno, jedrnato in nepristransko nadomestno besedilo.

  • Izpostavite pomembne elemente.
  • Povzemite besedilo v slikah.
  • Pišite v celih stavkih.
  • Zajemite bistvo, ne dolgovezite.
  • Opišite težnje in ključna odkritja, ki ste jih razbrali iz zapletenih grafik (npr. diagramov ali zemljevidov).
", + "interaction_modal.action.favourite": "Med priljubljene lahko dodate, ko se vpišete v svoj račun.", + "interaction_modal.action.follow": "Sledite lahko šele, ko se vpišete v svoj račun.", + "interaction_modal.action.reblog": "Izpostavite lahko šele, ko se vpišete v svoj račun.", + "interaction_modal.action.reply": "Odgovorite lahko šele, ko se vpišete v svoj račun.", + "interaction_modal.action.vote": "Glasujete lahko šele, ko se vpišete v svoj račun.", + "interaction_modal.go": "Naprej", "interaction_modal.no_account_yet": "Še nimate računa?", "interaction_modal.on_another_server": "Na drugem strežniku", "interaction_modal.on_this_server": "Na tem strežniku", @@ -383,6 +433,8 @@ "interaction_modal.title.follow": "Sledi {name}", "interaction_modal.title.reblog": "Izpostavi objavo {name}", "interaction_modal.title.reply": "Odgovori na objavo {name}", + "interaction_modal.title.vote": "Izpolni anketo uporabnika/ce {name}", + "interaction_modal.username_prompt": "Npr. {example}", "intervals.full.days": "{number, plural, one {# dan} two {# dni} few {# dni} other {# dni}}", "intervals.full.hours": "{number, plural, one {# ura} two {# uri} few {# ure} other {# ur}}", "intervals.full.minutes": "{number, plural, one {# minuta} two {# minuti} few {# minute} other {# minut}}", @@ -407,7 +459,7 @@ "keyboard_shortcuts.muted": "Odpri seznam utišanih uporabnikov", "keyboard_shortcuts.my_profile": "Odprite svoj profil", "keyboard_shortcuts.notifications": "Odpri stolpec z obvestili", - "keyboard_shortcuts.open_media": "Odpri medij", + "keyboard_shortcuts.open_media": "Odpri predstavnost", "keyboard_shortcuts.pinned": "Odpri seznam pripetih objav", "keyboard_shortcuts.profile": "Odpri avtorjev profil", "keyboard_shortcuts.reply": "Odgovori na objavo", @@ -416,26 +468,35 @@ "keyboard_shortcuts.spoilers": "Pokaži/skrij polje CW", "keyboard_shortcuts.start": "Odpri stolpec \"začni\"", "keyboard_shortcuts.toggle_hidden": "Pokaži/skrij besedilo za CW", - "keyboard_shortcuts.toggle_sensitivity": "Pokaži/skrij medije", + "keyboard_shortcuts.toggle_sensitivity": "Pokaži/skrij predstavnosti", "keyboard_shortcuts.toot": "Začni povsem novo objavo", + "keyboard_shortcuts.translate": "za prevod objave", "keyboard_shortcuts.unfocus": "Odstrani pozornost z območja za sestavljanje besedila/iskanje", "keyboard_shortcuts.up": "Premakni navzgor po seznamu", "lightbox.close": "Zapri", "lightbox.next": "Naslednji", "lightbox.previous": "Prejšnji", + "lightbox.zoom_in": "Približaj na dejansko velikost", + "lightbox.zoom_out": "Čez cel prikaz", "limited_account_hint.action": "Vseeno pokaži profil", "limited_account_hint.title": "Profil so moderatorji strežnika {domain} skrili.", - "link_preview.author": "Avtor_ica {name}", + "link_preview.author": "Avtor/ica {name}", "link_preview.more_from_author": "Več od {name}", "link_preview.shares": "{count, plural, one {{counter} objava} two {{counter} objavi} few {{counter} objave} other {{counter} objav}}", "lists.add_member": "Dodaj", "lists.add_to_list": "Dodaj na seznam", + "lists.add_to_lists": "Dodaj {name} na sezname", "lists.create": "Ustvari", + "lists.create_a_list_to_organize": "Uredite si domači vir z novim seznamom", "lists.create_list": "Ustvari seznam", "lists.delete": "Izbriši seznam", "lists.done": "Opravljeno", "lists.edit": "Uredi seznam", + "lists.exclusive": "Skrij člane v domovanju", + "lists.exclusive_hint": "Objave vseh, ki so na tem seznamu, se ne pokažejo v vašem domačem viru. Tako se izognete podvojenim objavam.", "lists.find_users_to_add": "Poišči člane za dodajanje", + "lists.list_members": "Člani seznama", + "lists.list_members_count": "{count, plural, one {# član} two {# člana} few {# člani} other {# članov}}", "lists.list_name": "Ime seznama", "lists.new_list_name": "Novo ime seznama", "lists.no_lists_yet": "Ni seznamov.", @@ -446,6 +507,8 @@ "lists.replies_policy.list": "Članom seznama", "lists.replies_policy.none": "Nikomur", "lists.save": "Shrani", + "lists.search": "Iskanje", + "lists.show_replies_to": "Vključi odgovore, katerih pošiljatelji so člani seznama in prejemniki", "load_pending": "{count, plural, one {# nov element} two {# nova elementa} few {# novi elementi} other {# novih elementov}}", "loading_indicator.label": "Nalaganje …", "media_gallery.hide": "Skrij", @@ -493,9 +556,17 @@ "notification.admin.report_statuses": "{name} je prijavil/a {target} zaradi {category}", "notification.admin.report_statuses_other": "{name} je prijavil/a {target}", "notification.admin.sign_up": "{name} se je vpisal/a", + "notification.admin.sign_up.name_and_others": "Prijavili so se {name} in {count, plural, one {# druga oseba} two {# drugi osebi} few {# druge osebe} other {# drugih oseb}}", + "notification.annual_report.message": "Čaka vas vaš #Wrapstodon {year}! Razkrijte svoje letošnje nepozabne trenutke na Mastodonu!", + "notification.annual_report.view": "Pokaži #Wrapstodon", "notification.favourite": "{name} je vzljubil/a vašo objavo", + "notification.favourite.name_and_others_with_link": "{name} in {count, plural, one {# druga oseba} two {# drugi osebi} few {# druge osebe} other {# drugih oseb}} je dodalo vašo objavo med priljubljene", + "notification.favourite_pm": "{name} je dodalo vašo zasebno omembo med priljubljene", + "notification.favourite_pm.name_and_others_with_link": "{name} in {count, plural, one {# druga oseba} two {# drugi osebi} few {# druge osebe} other {# drugih oseb}} je dodalo vašo zasebno omembo med priljubljene", "notification.follow": "{name} vam sledi", + "notification.follow.name_and_others": "{name} in {count, plural, one {# druga oseba sta ti sledila} two {# drugi osebi so ti sledili} few {# druge osebe so ti sledili} other {# drugih oseb ti je sledilo}}", "notification.follow_request": "{name} vam želi slediti", + "notification.follow_request.name_and_others": "{name} in {count, plural, one {# druga oseba bi ti rada sledila} two {# drugi osebi bi ti radi sledili} few {# druge osebe bi ti radi sledili} other {# drugih oseb bi ti radi sledili}}", "notification.label.mention": "Omemba", "notification.label.private_mention": "Zasebna omemba", "notification.label.private_reply": "Zasebni odgovor", @@ -514,6 +585,7 @@ "notification.own_poll": "Vaša anketa je zaključena", "notification.poll": "Anketa, v kateri ste sodelovali, je zaključena", "notification.reblog": "{name} je izpostavila/a vašo objavo", + "notification.reblog.name_and_others_with_link": "{name} in {count, plural, one {# druga oseba sta izpostavila tvojo objavo} two {# drugi osebi so izpostavili tvojo objavo} few {# druge osebe so izpostavili tvojo objavo} other {# drugih oseb so izpostavili tvojo objavo}}", "notification.relationships_severance_event": "Povezave z {name} prekinjene", "notification.relationships_severance_event.account_suspension": "Skrbnik na {from} je suspendiral račun {target}, kar pomeni, da od računa ne morete več prejemati posodobitev ali imeti z njim interakcij.", "notification.relationships_severance_event.domain_block": "Skrbnik na {from} je blokiral domeno {target}, vključno z vašimi sledilci ({followersCount}) in {followingCount, plural, one {# računom, ki mu sledite} two {# računoma, ki jima sledite} few {# računi, ki jim sledite} other {# računi, ki jim sledite}}.", @@ -522,12 +594,21 @@ "notification.status": "{name} je pravkar objavil/a", "notification.update": "{name} je uredil(a) objavo", "notification_requests.accept": "Sprejmi", + "notification_requests.accept_multiple": "{count, plural, one {Sprejmi # prošnjo …} two {Sprejmi # prošnji …} few {Sprejmi # prošnje …} other {Sprejmi # prošenj …}}", + "notification_requests.confirm_accept_multiple.button": "{count, plural, one {Sprejmi prošnjo} two {Sprejmi prošnji} other {Sprejmi prošnje}}", + "notification_requests.confirm_accept_multiple.message": "Sprejeti nameravate {count, plural, one {eno prošnjo za obvestila} two {dve prošnji za obvestila} few {# prošnje za obvestila} other {# prošenj za obvestila}}. Ali ste prepričani?", "notification_requests.confirm_accept_multiple.title": "Ali želite sprejeti zahteve za obvestila?", + "notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Zavrni prošnjo} two {Zavrni prošnji} other {Zavrni prošnje}}", + "notification_requests.confirm_dismiss_multiple.message": "Zavrniti nameravate {count, plural, one {eno prošnjo za obvestila} two {dve prošnji za obvestila} few {# prošnje za obvestila} other {# prošenj za obvestila}}. Do {count, plural, one {nje} two {njiju} other {njih}} ne boste več mogli dostopati. Ali ste prepričani?", "notification_requests.confirm_dismiss_multiple.title": "Želite opustiti zahteve za obvestila?", "notification_requests.dismiss": "Zavrni", + "notification_requests.dismiss_multiple": "{count, plural, one {Zavrni # prošnjo …} two {Zavrni # prošnji …} few {Zavrni # prošnje …} other {Zavrni # prošenj …}}", "notification_requests.edit_selection": "Uredi", "notification_requests.exit_selection": "Opravljeno", + "notification_requests.explainer_for_limited_account": "Obvestila za ta račun so bila filtrirana, ker je ta račun omejil moderator.", + "notification_requests.explainer_for_limited_remote_account": "Obvestila za ta račun so bila filtrirana, ker je račun ali njegov strežnik omejil moderator.", "notification_requests.maximize": "Maksimiraj", + "notification_requests.minimize_banner": "Zloži pasico filtriranih obvestil", "notification_requests.notifications_from": "Obvestila od {name}", "notification_requests.title": "Filtrirana obvestila", "notification_requests.view": "Pokaži obvestila", @@ -542,6 +623,7 @@ "notifications.column_settings.filter_bar.category": "Vrstica za hitro filtriranje", "notifications.column_settings.follow": "Novi sledilci:", "notifications.column_settings.follow_request": "Nove prošnje za sledenje:", + "notifications.column_settings.group": "Združi", "notifications.column_settings.mention": "Omembe:", "notifications.column_settings.poll": "Rezultati ankete:", "notifications.column_settings.push": "Potisna obvestila", @@ -568,6 +650,9 @@ "notifications.policy.accept": "Sprejmi", "notifications.policy.accept_hint": "Pokaži med obvestili", "notifications.policy.drop": "Prezri", + "notifications.policy.drop_hint": "Pošlji v pozabo, od koder se nikdar nič ne vrne", + "notifications.policy.filter": "Filtriraj", + "notifications.policy.filter_hint": "Pošlji med filtrirana prejeta obvestila", "notifications.policy.filter_limited_accounts_hint": "Omejeno s strani moderatorjev strežnika", "notifications.policy.filter_limited_accounts_title": "Moderirani računi", "notifications.policy.filter_new_accounts.hint": "Ustvarjen v {days, plural, one {zadnjem # dnevu} two {zadnjih # dnevih} few {zadnjih # dnevih} other {zadnjih # dnevih}}", @@ -585,12 +670,14 @@ "onboarding.follows.back": "Nazaj", "onboarding.follows.done": "Opravljeno", "onboarding.follows.empty": "Žal trenutno ni mogoče prikazati nobenih rezultatov. Lahko poskusite z iskanjem ali brskanjem po strani za raziskovanje, da poiščete osebe, ki jim želite slediti, ali poskusite znova pozneje.", + "onboarding.follows.search": "Išči", + "onboarding.follows.title": "Vaš prvi korak je, da sledite ljudem", "onboarding.profile.discoverable": "Naj bo moj profil mogoče najti", "onboarding.profile.discoverable_hint": "Ko se odločite za razkrivanje na Mastodonu, se lahko vaše objave pojavijo v rezultatih iskanja in trendih, vaš profil pa se lahko predlaga ljudem, ki imajo podobne interese kot vi.", "onboarding.profile.display_name": "Pojavno ime", "onboarding.profile.display_name_hint": "Vaše polno ime ali lažno ime ...", "onboarding.profile.note": "Biografija", - "onboarding.profile.note_hint": "Druge osebe lahko @omenite ali #ključite ...", + "onboarding.profile.note_hint": "Lahko @omenite druge osebe ali dodate #ključnike ...", "onboarding.profile.save_and_continue": "Shrani in nadaljuj", "onboarding.profile.title": "Nastavitev profila", "onboarding.profile.upload_avatar": "Naloži sliko profila", @@ -610,6 +697,7 @@ "poll_button.remove_poll": "Odstrani anketo", "privacy.change": "Spremeni zasebnost objave", "privacy.direct.long": "Vsem omenjenim v objavi", + "privacy.direct.short": "Zasebna omemba", "privacy.private.long": "Samo vašim sledilcem", "privacy.private.short": "Sledilcem", "privacy.public.long": "Vsem, ki so ali niso na Mastodonu", @@ -621,6 +709,8 @@ "privacy_policy.title": "Pravilnik o zasebnosti", "recommended": "Priporočeno", "refresh": "Osveži", + "regeneration_indicator.please_stand_by": "Prosimo, počakajte.", + "regeneration_indicator.preparing_your_home_feed": "Pripravljamo vaš domači vir …", "relative_time.days": "{number} d", "relative_time.full.days": "{number, plural, one {pred # dnem} two {pred # dnevoma} few {pred # dnevi} other {pred # dnevi}}", "relative_time.full.hours": "{number, plural, one {pred # uro} two {pred # urama} few {pred # urami} other {pred # urami}}", @@ -656,7 +746,7 @@ "report.reasons.dislike": "Ni mi všeč", "report.reasons.dislike_description": "To ni tisto, kar želim videti", "report.reasons.legal": "To ni legalno", - "report.reasons.legal_description": "Ste mnenja, da krši zakonodajo vaše države ali države strežnika", + "report.reasons.legal_description": "Sem mnenja, da krši zakonodajo moje države ali države strežnika", "report.reasons.other": "Gre za nekaj drugega", "report.reasons.other_description": "Težava ne sodi v druge kategorije", "report.reasons.spam": "To je neželena vsebina", @@ -668,10 +758,10 @@ "report.statuses.subtitle": "Izberite vse, kar ustreza", "report.statuses.title": "Ali so kakšne objave, ki dokazujejo trditve iz te prijave?", "report.submit": "Pošlji", - "report.target": "Prijavi {target}", + "report.target": "Prijavljate {target}", "report.thanks.take_action": "Tukaj so vaše možnosti za nadzor tistega, kar vidite na Mastodonu:", "report.thanks.take_action_actionable": "Medtem, ko to pregledujemo, lahko proti @{name} ukrepate:", - "report.thanks.title": "Ali ne želite tega videti?", + "report.thanks.title": "Ali ne želite videti tega?", "report.thanks.title_actionable": "Hvala za prijavo, bomo preverili.", "report.unfollow": "Ne sledi več @{name}", "report.unfollow_explanation": "Temu računu sledite. Da ne boste več videli njegovih objav v svojem domačem viru, mu prenehajte slediti.", @@ -695,7 +785,7 @@ "search.search_or_paste": "Iščite ali prilepite URL", "search_popout.full_text_search_disabled_message": "Ni dostopno na {domain}.", "search_popout.full_text_search_logged_out_message": "Na voljo le, če ste prijavljeni.", - "search_popout.language_code": "Koda ISO jezika", + "search_popout.language_code": "Jezikovna koda ISO", "search_popout.options": "Možnosti iskanja", "search_popout.quick_actions": "Hitra dejanja", "search_popout.recent": "Nedavna iskanja", @@ -705,8 +795,10 @@ "search_results.all": "Vse", "search_results.hashtags": "Ključniki", "search_results.no_results": "Ni rezultatov.", + "search_results.no_search_yet": "Pobrskajte med objavami, profili in ključniki.", "search_results.see_all": "Poglej vse", "search_results.statuses": "Objave", + "search_results.title": "Zadetki za \"{q}\"", "server_banner.about_active_users": "Osebe, ki so uporabljale ta strežnik zadnjih 30 dni (dejavni uporabniki meseca)", "server_banner.active_users": "dejavnih uporabnikov", "server_banner.administered_by": "Upravlja:", @@ -724,6 +816,7 @@ "status.bookmark": "Dodaj med zaznamke", "status.cancel_reblog_private": "Prekliči izpostavitev", "status.cannot_reblog": "Te objave ni mogoče izpostaviti", + "status.continued_thread": "Nadaljevanje niti", "status.copy": "Kopiraj povezavo do objave", "status.delete": "Izbriši", "status.detailed_status": "Podroben pogled pogovora", @@ -733,7 +826,7 @@ "status.edited": "Zadnje urejanje {date}", "status.edited_x_times": "Urejeno {count, plural, one {#-krat} two {#-krat} few {#-krat} other {#-krat}}", "status.embed": "Pridobite kodo za vgradnjo", - "status.favourite": "Priljubljen_a", + "status.favourite": "Priljubljen/a", "status.favourites": "{count, plural, one {priljubitev} two {priljubitvi} few {priljubitve} other {priljubitev}}", "status.filter": "Filtriraj to objavo", "status.history.created": "{name}: ustvarjeno {date}", @@ -741,7 +834,7 @@ "status.load_more": "Naloži več", "status.media.open": "Kliknite za odpiranje", "status.media.show": "Kliknite za prikaz", - "status.media_hidden": "Mediji so skriti", + "status.media_hidden": "Predstavnosti so skrite", "status.mention": "Omeni @{name}", "status.more": "Več", "status.mute": "Utišaj @{name}", @@ -758,6 +851,7 @@ "status.redraft": "Izbriši in preoblikuj", "status.remove_bookmark": "Odstrani zaznamek", "status.remove_favourite": "Odstrani iz priljubljenih", + "status.replied_in_thread": "Odgovor iz niti", "status.replied_to": "Odgovoril/a {name}", "status.reply": "Odgovori", "status.replyAll": "Odgovori na nit", @@ -767,7 +861,7 @@ "status.show_less_all": "Prikaži manj za vse", "status.show_more_all": "Pokaži več za vse", "status.show_original": "Pokaži izvirnik", - "status.title.with_attachments": "{user} je objavil_a {attachmentCount, plural, one {{attachmentCount} priponko} two {{attachmentCount} priponki} few {{attachmentCount} priponke} other {{attachmentCount} priponk}}", + "status.title.with_attachments": "{user} je objavil/a {attachmentCount, plural, one {{attachmentCount} priponko} two {{attachmentCount} priponki} few {{attachmentCount} priponke} other {{attachmentCount} priponk}}", "status.translate": "Prevedi", "status.translated_from_with": "Prevedeno iz {lang} s pomočjo {provider}", "status.uncached_media_warning": "Predogled ni na voljo", @@ -778,7 +872,9 @@ "subscribed_languages.target": "Spremeni naročene jezike za {target}", "tabs_bar.home": "Domov", "tabs_bar.notifications": "Obvestila", + "terms_of_service.effective_as_of": "Veljavno od {date}", "terms_of_service.title": "Pogoji uporabe", + "terms_of_service.upcoming_changes_on": "Spremembe začnejo veljati {date}", "time_remaining.days": "{number, plural, one {preostaja # dan} two {preostajata # dneva} few {preostajajo # dnevi} other {preostaja # dni}}", "time_remaining.hours": "{number, plural, one {# ura} other {# ur}} je ostalo", "time_remaining.minutes": "{number, plural, one {# minuta} other {# minut}} je ostalo", @@ -794,6 +890,11 @@ "upload_button.label": "Dodajte slike, video ali zvočno datoteko", "upload_error.limit": "Omejitev prenosa datoteke je presežena.", "upload_error.poll": "Prenos datoteke z anketami ni dovoljen.", + "upload_form.drag_and_drop.instructions": "Predstavnostno priponko lahko poberete tako, da pritisnete preslednico ali vnašalko. S puščicami na tipkovnici premikate priponko v posamezno smer. Priponko lahko odložite na novem položaju s ponovnim pritiskom na preslednico ali vnašalko ali pa dejanje prekličete s tipko ubežnica.", + "upload_form.drag_and_drop.on_drag_cancel": "Premikanje priponke je preklicano. Predstavnostna priponka {item} je padla nazaj na prejšnje mesto.", + "upload_form.drag_and_drop.on_drag_end": "Predstavnostna priponka {item} je padla nazaj.", + "upload_form.drag_and_drop.on_drag_over": "Priponka {item} je bila premaknjena.", + "upload_form.drag_and_drop.on_drag_start": "Pobrana priponka {item}.", "upload_form.edit": "Uredi", "upload_progress.label": "Pošiljanje ...", "upload_progress.processing": "Obdelovanje …", diff --git a/app/javascript/mastodon/locales/tt.json b/app/javascript/mastodon/locales/tt.json index c545711052..8fef9ee40f 100644 --- a/app/javascript/mastodon/locales/tt.json +++ b/app/javascript/mastodon/locales/tt.json @@ -19,6 +19,7 @@ "account.block_short": "Блокла", "account.blocked": "Блокланган", "account.cancel_follow_request": "Киләсе сорау", + "account.copy": "Профиль сылтамасын күчереп ал", "account.disable_notifications": "@{name} язулары өчен белдерүләр сүндерү", "account.domain_blocked": "Домен блокланган", "account.edit_profile": "Профильне үзгәртү", @@ -43,6 +44,8 @@ "account.mention": "@{name} искәртү", "account.moved_to": "{name} аларның яңа счеты хәзер күрсәтте:", "account.mute": "@{name} кулланучыга әһәмият бирмәү", + "account.mute_notifications_short": "Искәртүләрне сүндер", + "account.mute_short": "Тавышсыз", "account.muted": "Әһәмият бирмәнгән", "account.open_original_page": "Чыганак битен ачу", "account.posts": "Язма", @@ -58,6 +61,7 @@ "account.unendorse": "Профильдә тәкъдим итмәү", "account.unfollow": "Язылуны туктату", "account.unmute": "Kабызыгыз @{name}", + "account.unmute_notifications_short": "Искәртүләрне кабыз", "account.unmute_short": "Kабызыгыз", "account_note.placeholder": "Click to add a note", "admin.dashboard.daily_retention": "Теркәлгәннән соң икенче көнне кулланучыларны тоту коэффициенты", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 76408a22d7..bc4fb7573b 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -874,6 +874,7 @@ "tabs_bar.notifications": "Сповіщення", "terms_of_service.effective_as_of": "Ефективний на {date}", "terms_of_service.title": "Умови використання", + "terms_of_service.upcoming_changes_on": "Майбутні зміни {date}", "time_remaining.days": "{number, plural, one {# день} few {# дні} other {# днів}}", "time_remaining.hours": "{number, plural, one {# година} few {# години} other {# годин}}", "time_remaining.minutes": "{number, plural, one {# хвилина} few {# хвилини} other {# хвилин}}", diff --git a/config/locales/activerecord.el.yml b/config/locales/activerecord.el.yml index 9b17b75939..58e7a7df3f 100644 --- a/config/locales/activerecord.el.yml +++ b/config/locales/activerecord.el.yml @@ -49,6 +49,10 @@ el: attributes: reblog: taken: της ανάρτησης υπάρχει ήδη + terms_of_service: + attributes: + effective_date: + too_soon: είναι πολύ σύντομα, πρέπει να είναι μετά από %{date} user: attributes: email: diff --git a/config/locales/activerecord.nl.yml b/config/locales/activerecord.nl.yml index 3bed4e1095..5e5424902e 100644 --- a/config/locales/activerecord.nl.yml +++ b/config/locales/activerecord.nl.yml @@ -52,7 +52,7 @@ nl: terms_of_service: attributes: effective_date: - too_soon: is te vroeg, moet later zijn dan %{date} + too_soon: is te vroeg, moet na %{date} zijn user: attributes: email: diff --git a/config/locales/activerecord.sl.yml b/config/locales/activerecord.sl.yml index bba6ce6aad..8b05d5d2cd 100644 --- a/config/locales/activerecord.sl.yml +++ b/config/locales/activerecord.sl.yml @@ -18,9 +18,13 @@ sl: attributes: domain: invalid: ni veljavno ime domene + messages: + invalid_domain_on_line: "%{value} ni veljavno ime domene" models: account: attributes: + fields: + fields_with_values_missing_labels: vsebuje vrednosti, ki niso kategorizirane username: invalid: samo črke, številke in podčrtaji reserved: je rezerviran @@ -40,10 +44,15 @@ sl: attributes: account_id: taken: je že na seznamu + must_be_following: mora biti račun, ki mu sledite status: attributes: reblog: taken: od objave že obstajajo + terms_of_service: + attributes: + effective_date: + too_soon: je prekmalu, naj bo kasneje od %{date} user: attributes: email: diff --git a/config/locales/activerecord.tt.yml b/config/locales/activerecord.tt.yml index e53c2341e4..87c6592b6e 100644 --- a/config/locales/activerecord.tt.yml +++ b/config/locales/activerecord.tt.yml @@ -5,10 +5,51 @@ tt: poll: options: Сайлаулар user: - email: Почта адресы + email: Эл. почта адресы locale: Тел password: Серсүз user/account: username: Кулланучы исеме user/invite_request: text: Сәбәп + errors: + attributes: + domain: + invalid: бу домен исеме гамәлдә түгел + messages: + invalid_domain_on_line: "%{value} дөрес домен исеме түгел" + models: + account: + attributes: + username: + invalid: хәрефләр, цифрлар һәм ассызыклау билгеләре генә ярый + admin/webhook: + attributes: + url: + invalid: рөхсәт ителгән URL түгел + doorkeeper/application: + attributes: + website: + invalid: рөхсәт ителгән URL түгел + import: + attributes: + data: + malformed: формат дөрес түгел + list_account: + attributes: + account_id: + taken: инде исемлектә + status: + attributes: + reblog: + taken: язма инде бар + user: + attributes: + email: + blocked: ярамаган эл. почта провайдерын куллана + role_id: + elevated: сезнең хәзерге ролегездән югарырак була алмый + user_role: + attributes: + position: + elevated: сезнең хәзерге ролегездән югарырак була алмый diff --git a/config/locales/devise.tt.yml b/config/locales/devise.tt.yml index b9ac2e09f6..608d7dbcc9 100644 --- a/config/locales/devise.tt.yml +++ b/config/locales/devise.tt.yml @@ -3,7 +3,19 @@ tt: devise: confirmations: confirmed: Сезнең э. почта адресыгыз уңышлы расланган. + failure: + already_authenticated: Сез кердегез инде. + inactive: Сезнең аккаунтыгыз әле активламаган. + invalid: "%{authentication_keys} яки серсүз дөрес кертелмәгән." + locked: Сезнең хисапъязмагыз блокланган. + not_found_in_database: "%{authentication_keys} яки серсүз дөрес кертелмәгән." mailer: + confirmation_instructions: + action: Email адресын расла + action_with_app: Расла һәм %{app} эченә кайт + title: Email адресын раслагыз + email_changed: + explanation: 'Сезнең аккаунтыгызның email адресы моңа үзгәртеләчәк:' reset_password_instructions: action: Серсүзне үзгәртү title: Серсүзне алыштыру diff --git a/config/locales/doorkeeper.sl.yml b/config/locales/doorkeeper.sl.yml index 3f36c73756..8b28d1532a 100644 --- a/config/locales/doorkeeper.sl.yml +++ b/config/locales/doorkeeper.sl.yml @@ -60,6 +60,7 @@ sl: error: title: Prišlo je do napake new: + prompt_html: "%{client_name} želi dostopati do vašega računa. To prošnjo odobrite le, če tega odjemalca prepoznate in mu zaupate." review_permissions: Preglej dovoljenja title: Potrebna je odobritev show: @@ -82,6 +83,7 @@ sl: access_denied: Lastnik virov ali odobritveni strežnik je zavrnil zahtevo. credential_flow_not_configured: Pretok geselskih pooblastil lastnika virov ni uspel, ker Doorkeeper.configure.resource_owner_from_credentials ni nastavljen. invalid_client: Odobritev odjemalca ni uspela zaradi neznanega odjemalca, zaradi nevključitve odobritve odjemalca ali zaradi nepodprte metode odobritve. + invalid_code_challenge_method: Metoda za kodo mora biti S256, čistopis ni podprt. invalid_grant: Predložena odobritev je neveljavna, je potekla, je preklicana, se ne ujema z URI-jem za preusmeritev uporabljenim v zahtevi za odobritev, ali pa je bila izdana drugemu odjemalcu. invalid_redirect_uri: URI za preusmeritev ni veljaven. invalid_request: diff --git a/config/locales/el.yml b/config/locales/el.yml index d42791a65a..2c6d91298e 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -309,6 +309,7 @@ el: title: Αρχείο ελέγχου unavailable_instance: "(μη διαθέσιμο όνομα τομέα)" announcements: + back: Επιστροφή στις ανακοινώσεις destroyed_msg: Επιτυχής διαγραφή ανακοίνωσης! edit: title: Ενημέρωση ανακοίνωσης diff --git a/config/locales/et.yml b/config/locales/et.yml index 5d83333da8..8339ed087f 100644 --- a/config/locales/et.yml +++ b/config/locales/et.yml @@ -1159,6 +1159,7 @@ et: set_new_password: Uue salasõna määramine setup: email_below_hint_html: Kontrolli rämpsposti kausta või taotle uut. Saad oma e-posti aadressi parandada, kui see on vale. + email_settings_hint_html: Klõpsa aadressile %{email} saadetud linki, et alustada Mastodoni kasutamist. Me oleme ootel. link_not_received: Kas ei saanud linki? new_confirmation_instructions_sent: Saad mõne minuti pärast uue kinnituslingiga e-kirja! title: Kontrolli sisendkasti @@ -1167,6 +1168,7 @@ et: title: Logi sisse kohta %{domain} sign_up: manual_review: Liitumised kohas %{domain} vaadatakse meie moderaatorite poolt käsitsi läbi. Aitamaks meil sinu taotlust läbi vaadata, kirjuta palun natuke endast ja miks soovid kontot kohas %{domain}. + preamble: Selle Mastodoni serveri kontoga saad jälgida mistahes teist isikut fediversumis, sõltumata sellest, kus ta konto on majutatud. title: Loo konto serverisse %{domain}. status: account_status: Konto olek diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 1d7d22ff2d..81261e2f99 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -320,7 +320,7 @@ nl: title: Nieuwe mededeling preview: explanation_html: 'De e-mail wordt verzonden naar %{display_count} gebruikers. De volgende tekst wordt in het bericht opgenomen:' - title: Voorbeeld aankondiging notificatie + title: Voorbeeld van mededeling publish: Inschakelen published_msg: Publiceren van mededeling geslaagd! scheduled_for: Ingepland voor %{time} @@ -943,7 +943,7 @@ nl: chance_to_review_html: "De gegenereerde gebruiksvoorwaarden worden niet automatisch gepubliceerd. Je krijgt de gelegenheid om de resultaten eerst te bekijken. Vul de benodigde gegevens in om verder te gaan." explanation_html: Het sjabloon voor de gebruiksvoorwaarden is uitsluitend bedoeld voor informatieve doeleinden en mag niet worden opgevat als juridisch advies over welk onderwerp dan ook. Raadpleeg een eigen juridisch adviseur over jouw situatie en voor specifieke juridische vragen. title: Gebruiksvoorwaarden instellen - going_live_on_html: Actueel, met ingang van %{date} + going_live_on_html: Actueel met ingang van %{date} history: Geschiedenis live: Actueel no_history: Er zijn nog geen opgeslagen wijzigingen van de gebruiksvoorwaarden. @@ -1911,7 +1911,7 @@ nl: user_mailer: announcement_published: description: 'De beheerders van %{domain} doen een mededeling:' - subject: Service aankondiging + subject: Service-aankondiging title: "%{domain} service aankondiging" appeal_approved: action: Accountinstellingen @@ -1945,8 +1945,8 @@ nl: terms_of_service_changed: agreement: Door %{domain} te blijven gebruiken, ga je akkoord met deze voorwaarden. Als je het niet eens bent met de bijgewerkte voorwaarden, kun je je overeenkomst met %{domain} op elk gewenst moment beëindigen door je account te verwijderen. changelog: 'In een oogopslag betekent deze update voor jou:' - description: 'Je ontvangt dit bericht, omdat we enkele wijzigingen aanbrengen in onze gebruiksvoorwaarden bij %{domain}. Deze aanpassingen komen van kracht op %{date}. We raden je aan om de bijgewerkte voorwaarden hier volledig te bekijken:' - description_html: Je ontvangt dit bericht, omdat we enkele wijzigingen aanbrengen in onze gebruiksvoorwaarden bij %{domain}. Deze aanpassingen komen van kracht op %{date}. We raden je aan om de bijgewerkte voorwaarden hier volledig te bestuderen. + description: 'Je ontvangt dit bericht, omdat we enkele wijzigingen aanbrengen in onze gebruiksvoorwaarden op %{domain}. Deze aanpassingen worden van kracht op %{date}. We raden je aan om de bijgewerkte voorwaarden hier volledig te bekijken:' + description_html: Je ontvangt dit bericht, omdat we enkele wijzigingen aanbrengen in onze gebruiksvoorwaarden op %{domain}. Deze aanpassingen worden van kracht op %{date}. We raden je aan om de bijgewerkte voorwaarden hier volledig te bestuderen. sign_off: Het %{domain}-team subject: Onze bijgewerkte gebruiksvoorwaarden subtitle: De gebruiksvoorwaarden van %{domain} veranderen diff --git a/config/locales/simple_form.is.yml b/config/locales/simple_form.is.yml index 121560046a..e190198f89 100644 --- a/config/locales/simple_form.is.yml +++ b/config/locales/simple_form.is.yml @@ -136,10 +136,14 @@ is: text: Er hægt að sníða með Markdown-málskipan. terms_of_service_generator: admin_email: Löglegar tilkynningar ná yfir andsvör, dómsúrskurði, lokunarbeiðnir og beiðnir frá lögregluembættum. + arbitration_address: Má vera það sama og raunverulegt heimilisfang eða “N/A” ef tölvupóstur er notaður. + arbitration_website: Má vera innfyllingarform á vefsíðu eða “N/A” ef tölvupóstur er notaður. choice_of_law: Sveitarfélög, héruð eða ríki þar sem ríkjandi lög og reglugerðir skulu stýra meðhöndlun á öllum kröfum. dmca_address: Fyrir rekstraraðila í BNA ætti að nota heimilisfang sem skráð er í DMCA Designated Agent Directory. Hægt er að verða sér úti um A P.O. pósthólfsskráningu með beinni beiðni; notaðu DMCA Designated Agent Post Office Box Waiver Request til að senda tölvupóst á Copyright Office og lýstu því yfir að þú sért heimavinnandi efnismiðlari (home-based content moderator) sem átt á hættu refsingar eða hefndir vegna þess sem þú miðlar og þurfir því á slíku pósthólfi að halda svo þitt eigið heimilisfang sé ekki gert opinbert. + dmca_email: Má vera sama tölvupóstfang og það sem notað er í “Tölvupóstfang vegna löglegra tilkynninga” hér að ofan. domain: Einstakt auðkenni á netþjónustunni sem þú býður. jurisdiction: Settu inn landið þar sem sá býr sem borgar reikningana. Ef það er fyrirtæki eða samtök, skaltu hafa það landið þar sem lögheimili þess er, auk borgar, héraðs, svæðis eða fylkis eins og við á. + min_age: Ætti ekki að vera lægri en sá lágmarksaldur sek kveðið er á um í lögum þíns lögsagnarumdæmis. user: chosen_languages: Þegar merkt er við þetta, birtast einungis færslur á völdum tungumálum á opinberum tímalínum role: Hlutverk stýrir hvaða heimildir notandinn hefur. @@ -343,6 +347,7 @@ is: dmca_email: Tölvupóstfang tilkynninga vegna DMCA/höfundaréttar domain: Lén jurisdiction: Lögsagnarumdæmi + min_age: Lágmarksaldur user: role: Hlutverk time_zone: Tímabelti diff --git a/config/locales/simple_form.lv.yml b/config/locales/simple_form.lv.yml index 2500c0fc5c..d9fe043952 100644 --- a/config/locales/simple_form.lv.yml +++ b/config/locales/simple_form.lv.yml @@ -132,8 +132,11 @@ lv: name: Tu vari mainīt tikai burtu lielumu, piemēram, lai tie būtu vieglāk lasāmi terms_of_service: changelog: Var veidot ar Markdown pierakstu. + effective_date: Saprātīgs laika logs var būt no 10 līdz 30 dienām no dienas, kad lietotāji tiek apziņoti. text: Var veidot ar Markdown pierakstu. terms_of_service_generator: + arbitration_address: Var būt tāda pati kā augstāk esošā fiziskā adrese vai "N/A", ja tiek izmantota e-pasta adrese. + arbitration_website: Var būt tīmekļa veidlapa vai "N/A", ja tiek izmantots e-pasts. domain: Sniegtā tiešsaistas pakalpojuma neatkārtojama identifikācija. user: chosen_languages: Ja ieķeksēts, publiskos laika grafikos tiks parādītas tikai ziņas noteiktajās valodās @@ -327,11 +330,13 @@ lv: usable: Ļaut ierakstos vietēji izmantot šo tēmturi terms_of_service: changelog: Kas ir mainījies? + effective_date: Spēkā stāšanās datums text: Pakalpojuma izmantošanas nosacījumi terms_of_service_generator: admin_email: E-pasta adrese juridiskiem paziņojumiem choice_of_law: Likuma izvēle domain: Domēna vārds + min_age: Mazākais pieļaujamais vecums user: role: Loma time_zone: Laika josla diff --git a/config/locales/simple_form.nl.yml b/config/locales/simple_form.nl.yml index 4b7a0aa50f..b31ae4b5ea 100644 --- a/config/locales/simple_form.nl.yml +++ b/config/locales/simple_form.nl.yml @@ -132,18 +132,18 @@ nl: name: Je kunt elk woord met een hoofdletter beginnen, om zo bijvoorbeeld de tekst leesbaarder te maken terms_of_service: changelog: Kan worden opgemaakt met Markdown. - effective_date: Een redelijke periode kan variëren van 10 tot 30 dagen vanaf de datum waarop u uw gebruikers op de hoogte stelt. + effective_date: Een redelijke periode kan variëren van 10 tot 30 dagen vanaf de datum waarop je jouw gebruikers op de hoogte stelt. text: Kan worden opgemaakt met Markdown. terms_of_service_generator: admin_email: Juridische mededelingen zijn o. a. counter-notices, gerechterlijke bevelen, takedown-requests en handhavingsverzoeken. arbitration_address: Kan hetzelfde zijn als bovenstaand vestigingsadres of "N/A" bij gebruik van e-mail. - arbitration_website: Kan een webformulier zijn, of "N/A" als e-mail wordt gebruikt. - choice_of_law: Stad, regio, grondgebied of staat waar de interne grondwetten van toepassing zijn op alle claims. + arbitration_website: Kan een webformulier zijn, of "N/A" wanneer e-mail wordt gebruikt. + choice_of_law: Stad, regio, grondgebied of staat waar de interne materiële wetten van toepassing zijn op alle aanspraken. dmca_address: 'Gebruik voor beheerders in de VS: het adres dat is geregistreerd in de DMCA Designated Agent Directory. Op verzoek is er een postbuslijst beschikbaar. Gebruik het DMCA Designated Agent Post Office Box Waiver Request om het Copyright Office te e-mailen en te beschrijven dat je een vanaf huis opererende inhoudsmoderator bent, die wraak of vergelding vreest voor je moderator-acties en daarom een postbus moet gebruiken om jouw huisadres uit het publieke domein te houden.' dmca_email: Kan hetzelfde e-mailadres zijn dat gebruikt wordt voor "E-mailadres voor juridische berichten" hierboven. domain: Een unieke identificatie van de online dienst die je levert. jurisdiction: Vermeld het land waar de persoon woont die de rekeningen betaalt. Is het een bedrijf of iets dergelijks, vermeld dan het land waar het ingeschreven staat en de stad, de regio, het grondgebied of de staat, voor zover van toepassing. - min_age: Mag niet lager zijn dan de minimale vereiste leeftijd volgens de wetten van uw jurisdictie. + min_age: Mag niet lager zijn dan de minimale vereiste leeftijd volgens de wetten van jouw jurisdictie. user: chosen_languages: Alleen berichten in de aangevinkte talen worden op de openbare tijdlijnen getoond role: De rol bepaalt welke rechten de gebruiker heeft. @@ -342,7 +342,7 @@ nl: admin_email: E-mailadres voor juridische meldingen arbitration_address: Vestigingsadres voor arbitrage-mededelingen arbitration_website: Website voor het indienen van arbitrage-mededelingen - choice_of_law: Keuze van recht + choice_of_law: Keuze van rechtsgebied dmca_address: Vestigingsadres voor DMCA/auteursrecht-mededelingen dmca_email: E-mailadres voor DMCA/auteursrecht-mededelingen domain: Domein diff --git a/config/locales/simple_form.sl.yml b/config/locales/simple_form.sl.yml index 1f1867854e..d43ea5eb9f 100644 --- a/config/locales/simple_form.sl.yml +++ b/config/locales/simple_form.sl.yml @@ -3,12 +3,14 @@ sl: simple_form: hints: account: + attribution_domains: Ena na vrstico. Ščiti pred napačno navedbo avtorstva. discoverable: Vaše javne objave in profil so lahko predstavljeni ali priporočeni v različnih delih Mastodona, vaš profil pa je lahko predlagan drugim uporabnikom. display_name: Vaše polno ime ali lažno ime. fields: Vaša domača stran, starost, kar koli. indexable: Vaše javne objave se lahko pojavijo v rezultatih iskanja na Mastodonu. Ljudje, ki so bili v interakciji z vašimi objavami, jih bodo lahko iskali ne glede na to. note: 'Druge osebe lahko @omenite ali #ključnite.' show_collections: Ljudje bodo lahko brskali po vaših sledilcih in sledenih. Ljudje, ki jim sledite, bodo videli, da jim sledite ne glede na to. + unlocked: Ljudje vam bodo lahko sledili, ne da bi zahtevali odobritev. Ne potrdite, če želite pregledati prošnje za sledenje in izbrati, ali želite nove sledilce sprejeti ali zavrniti. account_alias: acct: Določite uporabniškoime@domena računa, od katerega se želite preseliti account_migration: @@ -58,6 +60,7 @@ sl: setting_display_media_default: Skrij medij, ki je označen kot občutljiv setting_display_media_hide_all: Vedno skrij vse medije setting_display_media_show_all: Vedno pokaži medij, ki je označen kot občutljiv + setting_system_scrollbars_ui: Velja zgolj za namizne brskalnike, ki temeljijo na Safariju in Chromeu setting_use_blurhash: Prelivi temeljijo na barvah skrite vizualne slike, vendar zakrivajo vse podrobnosti setting_use_pending_items: Skrij posodobitev časovnice za klikom namesto samodejnega posodabljanja username: Uporabite lahko črke, števke in podčrtaje. @@ -127,6 +130,14 @@ sl: show_application: Ne glede na to boste vedno lahko pogledali, katera aplikacija je objavila vašo objavo. tag: name: Spremenite lahko le npr. velikost črk (velike/male), da je bolj berljivo + terms_of_service: + changelog: Uporabite lahko oblikovanje z Markdown. + effective_date: Razumen čas do uveljavitve je navadno nekje med 10 in 30 dni od datuma, ko ste obvestili svoje uporabnike. + text: Uporabite lahko oblikovanje z Markdown. + terms_of_service_generator: + admin_email: Pravna obvestila vključujejo odgovore na obvestila, sodne naloge, zahteve za odstranitev in zahteve organov pregona. + arbitration_address: Lahko je enak kot fizični naslov zgoraj ali „N/A“, če uporabljate e-pošto. + arbitration_website: Lahko je spletni obrazec ali „N/A“, če uporabljate e-pošto. user: chosen_languages: Ko je označeno, bodo v javnih časovnicah prikazane samo objave v izbranih jezikih user_role: @@ -313,6 +324,18 @@ sl: name: Ključnik trendable: Dovoli, da se ta ključnik pojavi med trendi usable: Dovoli, da objave krajevno uporabljajo ta ključnik + terms_of_service: + changelog: Kaj je novega? + effective_date: Datum začetka veljavnosti + text: Pogoji uporabe + terms_of_service_generator: + admin_email: E-poštni naslov za pravna obvestila + arbitration_address: Fizični naslov za arbitražna obvestila + arbitration_website: Spletišče za vložitev arbitražnih obvestil + dmca_address: Fizični naslov za obvestila DMCA ali o avtorskih pravicah + dmca_email: E-poštni naslov za obvestila DMCA ali o avtorskih pravicah + domain: Domena + min_age: Najmanjša starost user: role: Vloga time_zone: Časovni pas diff --git a/config/locales/sl.yml b/config/locales/sl.yml index e6a6c4a08b..d1bdc8983b 100644 --- a/config/locales/sl.yml +++ b/config/locales/sl.yml @@ -25,9 +25,12 @@ sl: other: Objav two: Objavi posts_tab_heading: Objave + self_follow_error: Ni dovoljeno slediti lastnemu računu admin: account_actions: action: Izvedi dejanje + already_silenced: Ta račun je že omejen. + already_suspended: Ta račun je že suspendiran. title: Izvedi moderirano dejanje za %{acct} account_moderation_notes: create: Pusti opombo @@ -49,6 +52,7 @@ sl: title: Spremeni e-naslov za %{username} change_role: changed_msg: Vloga uspešno spremenjena! + edit_roles: Upravljaj z uporabniškimi vlogami label: Spremeni vlogo no_role: Brez vloge title: Spremeni vlogo za %{username} @@ -189,6 +193,7 @@ sl: create_domain_block: Ustvari blokado domene create_email_domain_block: Ustvari blokado domene e-pošte create_ip_block: Ustvari pravilo IP + create_relay: Ustvari rele create_unavailable_domain: Ustvari domeno, ki ni na voljo create_user_role: Ustvari vlogo demote_user: Ponižaj uporabnika @@ -200,18 +205,22 @@ sl: destroy_email_domain_block: Izbriši blokado domene e-pošte destroy_instance: Očisti domeno destroy_ip_block: Izbriši pravilo IP + destroy_relay: Izbriši rele destroy_status: Izbriši objavo destroy_unavailable_domain: Izbriši nedosegljivo domeno destroy_user_role: Uniči vlogo disable_2fa_user: Onemogoči disable_custom_emoji: Onemogoči emotikon po meri + disable_relay: Onemogoči rele disable_sign_in_token_auth_user: Onemogoči overjanje z žetonom po e-pošti za uporabnika disable_user: Onemogoči uporabnika enable_custom_emoji: Omogoči emotikon po meri + enable_relay: Omogoči rele enable_sign_in_token_auth_user: Omogoči overjanje z žetonom po e-pošti za uporabnika enable_user: Omogoči uporabnika memorialize_account: Spomenificiraj račun promote_user: Povišaj uporabnika + publish_terms_of_service: Objavi pogoje uporabe reject_appeal: Zavrni pritožbo reject_user: Zavrni uporabnika remove_avatar_user: Odstrani avatar @@ -249,6 +258,7 @@ sl: create_domain_block_html: "%{name} je blokiral/a domeno %{target}" create_email_domain_block_html: "%{name} je dal/a na črni seznam e-pošto domene %{target}" create_ip_block_html: "%{name} je ustvaril/a pravilo za IP %{target}" + create_relay_html: "%{name} je ustvaril/a rele %{target}" create_unavailable_domain_html: "%{name} je prekinil/a dostavo v domeno %{target}" create_user_role_html: "%{name} je ustvaril/a vlogo %{target}" demote_user_html: "%{name} je ponižal/a uporabnika %{target}" @@ -260,18 +270,22 @@ sl: destroy_email_domain_block_html: "%{name} je odblokiral/a e-pošto domene %{target}" destroy_instance_html: "%{name} je očistil/a domeno %{target}" destroy_ip_block_html: "%{name} je izbrisal/a pravilo za IP %{target}" + destroy_relay_html: "%{name} je izbrisal/a rele %{target}" destroy_status_html: "%{name} je odstranil/a objavo uporabnika %{target}" destroy_unavailable_domain_html: "%{name} je nadaljeval/a dostav v domeno %{target}" destroy_user_role_html: "%{name} je izbrisal/a vlogo %{target}" disable_2fa_user_html: "%{name} je onemogočil/a dvofaktorsko zahtevo za uporabnika %{target}" disable_custom_emoji_html: "%{name} je onemogočil/a emotikone %{target}" + disable_relay_html: "%{name} je onemogočil/a rele %{target}" disable_sign_in_token_auth_user_html: "%{name} je onemogočil/a overjanje z žetonom po e-pošti za uporabnika %{target}" disable_user_html: "%{name} je onemogočil/a prijavo za uporabnika %{target}" enable_custom_emoji_html: "%{name} je omogočil/a emotikone %{target}" + enable_relay_html: "%{name} je omogočil rele %{target}" enable_sign_in_token_auth_user_html: "%{name} je omogočil/a overjanje z žetonom po e-pošti za uporabnika %{target}" enable_user_html: "%{name} je omogočil/a prijavo za uporabnika %{target}" memorialize_account_html: "%{name} je spremenil/a račun uporabnika %{target} v spominsko stran" promote_user_html: "%{name} je povišal/a uporabnika %{target}" + publish_terms_of_service_html: "%{name} je posodobil/a pogoje uporabe" reject_appeal_html: "%{name} je zavrnil/a pritožbo uporabnika %{target} na moderatorsko odločitev" reject_user_html: "%{name} je zavrnil/a registracijo iz %{target}" remove_avatar_user_html: "%{name} je odstranil podobo (avatar) uporabnika %{target}" @@ -301,6 +315,7 @@ sl: title: Dnevnik revizije unavailable_instance: "(ime domene ni na voljo)" announcements: + back: Nazaj na oznanila destroyed_msg: Obvestilo je bilo uspešno izbrisano! edit: title: Uredi obvestilo @@ -309,6 +324,9 @@ sl: new: create: Ustvari obvestilo title: Novo obvestilo + preview: + explanation_html: 'E-poštno sporočilo bo poslano %{display_count} uporabnikom. Priloženo bo naslednje besedilo:' + title: Pokaži predogled oznanila publish: Objavi published_msg: Obvestilo je bilo uspešno objavljeno! scheduled_for: Načrtovano ob %{time} @@ -486,6 +504,9 @@ sl: title: Sledi priporočilom unsuppress: Obnovi sledenje priporočilom instances: + audit_log: + title: Nedavni revizijski zapisi + view_all: Prikaži ves revizijski dnevnik availability: description_html: few: Če dostava v domeno spodleti %{count} različne dni brez uspeha, ne bo nadaljnjih poskusov dostopa, razen če je prejeta dostava iz domene. @@ -622,6 +643,7 @@ sl: suspend_description_html: Račun in vsa njegova vsebina ne bo dostopna in bo postopoma izbrisana, interakcija z njim pa ne bo več možna. Dejanje je moč povrniti v roku 30 dni. Zaključi vse prijave zoper ta račun. actions_description_html: Odločite se, katere ukrepe boste sprejeli za rešitev te prijave. Če sprejmete kazenski ukrep proti prijavljenemu računu, mu bo poslano e-poštno obvestilo, razen če je izbrana kategorija Neželena pošta. actions_description_remote_html: Odločite se za dejanje, ki bo odločilo o tej prijavi. To bo vplivalo le na to, kako vaš strežnik komunicira s tem oddaljenim računom in obravnava njegovo vsebino. + actions_no_posts: To poročilo ni vezano na nobene objave, ki bi jih lahko izbrisali add_to_report: Dodaj več v prijavo already_suspended_badges: local: Že suspendiran na tem strežniku @@ -838,6 +860,7 @@ sl: back_to_account: Nazaj na stran računa back_to_report: Nazaj na stran prijave batch: + add_to_report: 'Dodaj poročilu #%{id}' remove_from_report: Odstrani iz prijave report: Poročaj contents: Vsebina @@ -849,12 +872,17 @@ sl: media: title: Mediji metadata: Metapodatki + no_history: Ta objava ni bila spremenjena no_status_selected: Nobena objava ni bila spremenjena, ker ni bila nobena izbrana open: Odpri objavo original_status: Izvorna objava reblogs: Ponovljeni blogi + replied_to_html: V odgovor %{acct_link} status_changed: Objava spremenjena + status_title: Avtor/ica objave @%{name} + title: Objave računa - @%{name} trending: V trendu + view_publicly: Prikaži javno visibility: Vidnost with_media: Z mediji strikes: @@ -896,6 +924,9 @@ sl: message_html: Nobenih pravil strežnika niste določili. sidekiq_process_check: message_html: Noben proces Sidekiq ne poteka za %{value} vrst. Preglejte svojo prilagoditev Sidekiq + software_version_check: + action: Oglejte si razpoložljive posodobitve + message_html: Na voljo je posodobitev Mastodona. software_version_critical_check: action: Glejte razpoložljive posodobitve message_html: Na voljo je kritična posodobitev Mastodona. Posodobite čim prej. @@ -922,16 +953,42 @@ sl: name: Ime newest: Najnovejše oldest: Najstarejše + open: Prikaži javno reset: Ponastavi review: Stanje pregleda search: Išči title: Ključniki updated_msg: Nastavitve ključnikov uspešno posodobljene terms_of_service: + back: Nazaj na pogoje uporabe + changelog: Kaj je novega + create: Uporabi svoje + current: Trenutni draft: Osnutek generate: Uporabi predlogo + generates: + action: Generiraj + chance_to_review_html: "Generirani pogoji uporabe ne bodo objavljeni samodejno, tako da jih boste imeli čas preveriti. Vnesite vse potrebne podatke." + explanation_html: Predloga pogojev uporabe je informativne narave in naj ne služi kot pravno vodilo. O pravnih vprašanjih in specifikah se posvetujte s pravnim strokovnjakom. + title: Postavitev pogojev uporabe + going_live_on_html: Objavljeni, začnejo veljati %{date} history: Zgodovina + live: Objavljeni + no_history: V pogojih uporabe še ni zabeleženih sprememb. + no_terms_of_service_html: Trenutno nimate nastavljenih pogojev uporabe. Ti naj bi razjasnili pravno razmerje in dodeljevanje odgovornosti med vami in vašimi uporabniki v primeru spora. + notified_on_html: Uporabniki so obveščeni %{date} + notify_users: Obvesti uporabnike + preview: + explanation_html: 'E-poštno sporočilo bo poslano %{display_count} uporabnikom, ki so se registrirali pred %{date}. Priloženo bo naslednje besedilo:' + send_preview: Pošlji predogled na %{email} + send_to_all: + few: Pošlji %{display_count} e-poštna sporočila + one: Pošlji %{display_count} e-poštno sporočilo + other: Pošlji %{display_count} e-poštnih sporočil + two: Pošlji %{display_count} e-poštni sporočili + title: Prikaži predogled obvestila pogojev uporabe publish: Objavi + published_on_html: Objavljeno %{date} save_draft: Shrani osnutek title: Pogoji uporabe title: Upravljanje @@ -1173,6 +1230,7 @@ sl: set_new_password: Nastavi novo geslo setup: email_below_hint_html: Poglejte v mapo neželene pošte ali zaprosite za novega. Če ste podali napačen e-naslov, ga lahko popravite. + email_settings_hint_html: Kliknite na povezavo, ki smo vam jo poslali na %{email}, pa boste lahko začeli uporabljati Mastodon. Tukajle bomo počakali. link_not_received: Ali ste prejeli povezavo? new_confirmation_instructions_sent: Čez nekaj minut boste prejeli novo e-sporočilo s potrditveno povezavo! title: Preverite svojo dohodno e-pošto @@ -1181,6 +1239,7 @@ sl: title: Vpiši se v %{domain} sign_up: manual_review: Registracije na %{domain} ročno pregledajo naši moderatorji. Da nam olajšate obdelavo vaše prijave, zapišite kaj o sebi in zakaj si želite račun na %{domain}. + preamble: Če ustvarite račun na tem strežniku Mastodona, boste lahko sledili komur koli v fediverzumu, ne glede na to, kje gostuje njegov/njen račun. title: Naj vas namestimo na %{domain}. status: account_status: Stanje računa @@ -1192,8 +1251,16 @@ sl: view_strikes: Pokaži pretekle ukrepe proti mojemu računu too_fast: Obrazec oddan prehitro, poskusite znova. use_security_key: Uporabi varnostni ključ + user_agreement_html: Prebral/a sem pogoje uporabe in politiko zasebnosti in z obojim soglašam + user_privacy_agreement_html: Prebral sem politiko zasebnosti in soglašam z njo author_attribution: example_title: Vzorčno besedilo + hint_html: Ali pišete novičke ali spletni dnevnik kje drugje poleg Mastodona? Poskrbite, da bo vaše avtorstvo pravilno navedeno, ko bo kdo delil vaše delo na Mastodonu. + instructions: 'Poskrbite, da bo v dokumentu HTML vašega prispevka naslednja koda:' + more_from_html: Več od %{name} + s_blog: Spletni dnevnik %{name} + then_instructions: Nato dodajte ime domene, kamor objavljate, v spodnje polje. + title: Priznanje avtorstva challenge: confirm: Nadaljuj hint_html: "Namig: naslednjo uro vas ne bomo več vprašali po vašem geslu." @@ -1404,6 +1471,27 @@ sl: merge_long: Ohrani obstoječe zapise in dodaj nove overwrite: Prepiši overwrite_long: Zamenjaj trenutne zapise z novimi + overwrite_preambles: + blocking_html: + few: Kaže, da želite zamenjati svoj seznam blokiranih z do %{count} računi iz %{filename}. + one: Kaže, da želite zamenjati svoj seznam blokiranih z do %{count} računom iz %{filename}. + other: Kaže, da želite zamenjati svoj seznam blokiranih z do %{count} računi iz %{filename}. + two: Kaže, da želite zamenjati svoj seznam blokiranih z do %{count} računoma iz %{filename}. + bookmarks_html: + few: Kaže, da želite zamenjati svoje zaznamke z do %{count} objavami iz %{filename}. + one: Kaže, da želite zamenjati svoje zaznamke z do %{count} objavo iz %{filename}. + other: Kaže, da želite zamenjati svoje zaznamke z do %{count} objavami iz %{filename}. + two: Kaže, da želite zamenjati svoje zaznamke z do %{count} objavama iz %{filename}. + domain_blocking_html: + few: Kaže, da želite zamenjati svoj seznam blokiranih domen z do %{count} domenami iz %{filename}. + one: Kaže, da želite zamenjati svoj seznam blokiranih domen z do %{count} domeno iz %{filename}. + other: Kaže, da želite zamenjati svoj seznam blokiranih domen z do %{count} domenami iz %{filename}. + two: Kaže, da želite zamenjati svoj seznam blokiranih domen z do %{count} domenama iz %{filename}. + following_html: + few: Kaže, da želite slediti do %{count} računom iz %{filename} in nehati slediti komur koli drugemu. + one: Kaže, da želite slediti do %{count} računu iz %{filename} in nehati slediti komur koli drugemu. + other: Kaže, da želite slediti do %{count} računom iz %{filename} in nehati slediti komur koli drugemu. + two: Kaže, da želite slediti do %{count} računoma iz %{filename} in nehati slediti komur koli drugemu. preface: Podatke, ki ste jih izvozili iz drugega strežnika, lahko uvozite. Na primer seznam oseb, ki jih spremljate ali blokirate. recent_imports: Nedavni uvozi states: @@ -1490,6 +1578,7 @@ sl: media_attachments: validations: images_and_video: Videoposnetka ni mogoče priložiti objavi, ki že vsebuje slike + not_found: Predstavnosti %{ids} ne najdem ali pa je že pripeta k drugi objavi not_ready: Datotek, katerih obdelava ni dokončana, ni mogoče pripeti. Poskusite znova kmalu! too_many: Ni možno priložiti več kot 4 datoteke migrations: @@ -1661,6 +1750,7 @@ sl: scheduled_statuses: over_daily_limit: Za ta dan ste presegli omejitev %{limit} načrtovanih objav over_total_limit: Presegli ste omejitev %{limit} načrtovanih objav + too_soon: datum mora biti v prihodnosti self_destruct: lead_html: Na žalost se %{domain} za vedno zapira. Če ste tu imeli svoj račun, ga v prihodnje ne boste mogli več uporabljati. Zahtevate lahko kopijo svojih podatkov. title: Ta strežnik se zapira @@ -1864,6 +1954,10 @@ sl: recovery_instructions_html: Če kdaj izgubite dostop do telefona, lahko uporabite eno od spodnjih obnovitvenih kod, da ponovno pridobite dostop do svojega računa. Shranite obnovitvene kode. Lahko jih natisnete in shranite z drugimi pomembnimi dokumenti. webauthn: Varnostni ključi user_mailer: + announcement_published: + description: 'Skrbniki domene %{domain} oznanjajo:' + subject: Storitveno oznanilo + title: Storitveno oznanilo %{domain} appeal_approved: action: Nastavitve računa explanation: Pritožbi na ukrep proti vašemu računu z dne %{strike_date}, ki ste jo oddali dne %{appeal_date}, je bilo ugodeno. Vaš račun je znova nesporen. @@ -1894,6 +1988,13 @@ sl: subject: Do vašega računa je bil opravljen dostop z novega naslova IP title: Nova prijava terms_of_service_changed: + agreement: Če boste še naprej uporabljali %{domain}, se strinjate s temi pogoji. Če se ne, lahko kadarkoli odstopite od dogovora z domeno %{domain} tako, da izbrišete svoj račun. + changelog: 'Kratek povzetek tega, kaj ta posodobitev pomeni za vas:' + description: 'To e-poštno sporočilo ste prejeli, ker smo spremenili pogoje uporabe v domeni %{domain}. Posodobitve bodo začele veljati %{date}. Vabimo vas, da si posodobljene pogoje preberete tukaj:' + description_html: To e-poštno sporočilo ste prejeli, ker smo spremenili pogoje uporabe v domeni %{domain}. Posodobitve bodo začele veljati %{date}. Vabimo vas, da si posodobljene pogoje preberete na tej povezavi. + sign_off: Ekipa %{domain} + subject: Posodobitve naših pogojev uporabe + subtitle: Spreminjajo se pogoji uporabe domene %{domain} title: Pomembna posodobitev warning: appeal: Pošlji pritožbo @@ -1984,6 +2085,7 @@ sl: instructions_html: Spodnjo kodo kopirajte in prilepite v HTML svojega spletnega mesta. Nato dodajte naslov svoje spletne strani v eno od dodatnih polj v svojem profilu v zavihku »Uredi profil« in shranite spremembe. verification: Potrditev verified_links: Vaše preverjene povezave + website_verification: Overitev spletišča webauthn_credentials: add: Dodaj nov varnostni ključ create: diff --git a/config/locales/uk.yml b/config/locales/uk.yml index ff4e77e529..788c84fb27 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -325,7 +325,8 @@ uk: create: Створити оголошення title: Нове оголошення preview: - title: Попередній перегляд повідомлення + explanation_html: 'Електронний лист буде надіслано %{display_count} користувачам. До електронного листа буде включено такий текст:' + title: Попередній перегляд сповіщення publish: Опублікувати published_msg: Оголошення успішно опубліковано! scheduled_for: Заплановано на %{time} From f71a855e2d53e07076363df56ba02600ec316f16 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 12 Mar 2025 10:29:19 -0400 Subject: [PATCH 06/12] Add coverage for `standard` params on push subs create (#34092) --- .../web/push_subscriptions_controller_spec.rb | 25 +++++++++++++++---- .../api/v1/push/subscriptions_spec.rb | 14 +++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/spec/controllers/api/web/push_subscriptions_controller_spec.rb b/spec/controllers/api/web/push_subscriptions_controller_spec.rb index acc0312113..1e01709262 100644 --- a/spec/controllers/api/web/push_subscriptions_controller_spec.rb +++ b/spec/controllers/api/web/push_subscriptions_controller_spec.rb @@ -15,6 +15,7 @@ RSpec.describe Api::Web::PushSubscriptionsController do p256dh: 'BEm_a0bdPDhf0SOsrnB2-ategf1hHoCnpXgQsFj5JCkcoMrMt2WHoPfEYOYPzOIs9mZE8ZUaD7VA5vouy0kEkr8=', auth: 'eH_C8rq2raXqlcBVDa1gLg==', }, + standard: standard, }, } end @@ -36,6 +37,7 @@ RSpec.describe Api::Web::PushSubscriptionsController do }, } end + let(:standard) { '1' } before do sign_in(user) @@ -51,14 +53,27 @@ RSpec.describe Api::Web::PushSubscriptionsController do user.reload - expect(created_push_subscription).to have_attributes( - endpoint: eq(create_payload[:subscription][:endpoint]), - key_p256dh: eq(create_payload[:subscription][:keys][:p256dh]), - key_auth: eq(create_payload[:subscription][:keys][:auth]) - ) + expect(created_push_subscription) + .to have_attributes( + endpoint: eq(create_payload[:subscription][:endpoint]), + key_p256dh: eq(create_payload[:subscription][:keys][:p256dh]), + key_auth: eq(create_payload[:subscription][:keys][:auth]) + ) + .and be_standard expect(user.session_activations.first.web_push_subscription).to eq(created_push_subscription) end + context 'when standard is provided as false value' do + let(:standard) { '0' } + + it 'saves push subscription with standard as false' do + post :create, format: :json, params: create_payload + + expect(created_push_subscription) + .to_not be_standard + end + end + context 'with a user who has a session with a prior subscription' do let!(:prior_subscription) { Fabricate(:web_push_subscription, session_activation: user.session_activations.last) } diff --git a/spec/requests/api/v1/push/subscriptions_spec.rb b/spec/requests/api/v1/push/subscriptions_spec.rb index f2b457705e..69adeb9b6f 100644 --- a/spec/requests/api/v1/push/subscriptions_spec.rb +++ b/spec/requests/api/v1/push/subscriptions_spec.rb @@ -16,6 +16,7 @@ RSpec.describe 'API V1 Push Subscriptions' do subscription: { endpoint: endpoint, keys: keys, + standard: standard, }, }.with_indifferent_access end @@ -36,6 +37,7 @@ RSpec.describe 'API V1 Push Subscriptions' do }, }.with_indifferent_access end + let(:standard) { '1' } let(:scopes) { 'push' } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } @@ -66,6 +68,7 @@ RSpec.describe 'API V1 Push Subscriptions' do user_id: eq(user.id), access_token_id: eq(token.id) ) + .and be_standard expect(response.parsed_body.with_indifferent_access) .to include( @@ -73,6 +76,17 @@ RSpec.describe 'API V1 Push Subscriptions' do ) end + context 'when standard is provided as false value' do + let(:standard) { '0' } + + it 'saves push subscription with standard as false' do + subject + + expect(endpoint_push_subscription) + .to_not be_standard + end + end + it 'replaces old subscription on repeat calls' do 2.times { subject } From a704e1991cb63aa56f4328404761477cf728ed67 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 12 Mar 2025 15:52:10 +0100 Subject: [PATCH 07/12] Further refactor reply fetching code (#34151) --- .../activitypub/fetch_all_replies_worker.rb | 39 +++++++------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/app/workers/activitypub/fetch_all_replies_worker.rb b/app/workers/activitypub/fetch_all_replies_worker.rb index e31ff17c23..849a06d0fa 100644 --- a/app/workers/activitypub/fetch_all_replies_worker.rb +++ b/app/workers/activitypub/fetch_all_replies_worker.rb @@ -15,14 +15,14 @@ class ActivityPub::FetchAllRepliesWorker MAX_REPLIES = (ENV['FETCH_REPLIES_MAX_GLOBAL'] || 1000).to_i MAX_PAGES = (ENV['FETCH_REPLIES_MAX_PAGES'] || 500).to_i - def perform(parent_status_id, options = {}) - @parent_status = Status.find(parent_status_id) - return unless @parent_status.should_fetch_replies? + def perform(root_status_id, options = {}) + @root_status = Status.remote.find_by(id: root_status_id) + return unless @root_status&.should_fetch_replies? - @parent_status.touch(:fetched_replies_at) - Rails.logger.debug { "FetchAllRepliesWorker - #{@parent_status.uri}: Fetching all replies for status: #{@parent_status}" } + @root_status.touch(:fetched_replies_at) + Rails.logger.debug { "FetchAllRepliesWorker - #{@root_status.uri}: Fetching all replies for status: #{@root_status}" } - uris_to_fetch, n_pages = get_replies(@parent_status.uri, MAX_PAGES, options) + uris_to_fetch, n_pages = get_replies(@root_status.uri, MAX_PAGES, options) return if uris_to_fetch.nil? fetched_uris = uris_to_fetch.clone.to_set @@ -41,7 +41,9 @@ class ActivityPub::FetchAllRepliesWorker n_pages += new_n_pages end - Rails.logger.debug { "FetchAllRepliesWorker - #{parent_status_id}: fetched #{fetched_uris.length} replies" } + Rails.logger.debug { "FetchAllRepliesWorker - #{@root_status.uri}: fetched #{fetched_uris.length} replies" } + + # Workers shouldn't be returning anything, but this is used in tests fetched_uris end @@ -55,23 +57,12 @@ class ActivityPub::FetchAllRepliesWorker end def get_replies_uri(parent_status_uri) - begin - json_status = fetch_resource(parent_status_uri, true) - if json_status.nil? - Rails.logger.debug { "FetchAllRepliesWorker - #{@parent_status.uri}: Could not get replies URI for #{parent_status_uri}, returned nil" } - nil - elsif !json_status.key?('replies') - Rails.logger.debug { "FetchAllRepliesWorker - #{@parent_status.uri}: No replies collection found in ActivityPub object: #{json_status}" } - nil - else - json_status['replies'] - end - rescue => e - Rails.logger.error { "FetchAllRepliesWorker - #{@parent_status.uri}: Caught exception while resolving replies URI #{parent_status_uri}: #{e} - #{e.message}" } - # Raise if we can't get the collection for top-level status to trigger retry - raise e if parent_status_uri == @parent_status.uri + fetch_resource(parent_status_uri, true)&.fetch('replies', nil) + rescue => e + Rails.logger.info { "FetchAllRepliesWorker - #{@root_status.uri}: Caught exception while resolving replies URI #{parent_status_uri}: #{e} - #{e.message}" } + # Raise if we can't get the collection for top-level status to trigger retry + raise e if parent_status_uri == @root_status.uri - nil - end + nil end end From 41e8eaa872b1391041a113973c1d38324720cf17 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:10:37 +0100 Subject: [PATCH 08/12] Update babel monorepo to v7.26.10 (#34144) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 100 ++++++++++++++++++++++++------------------------------ 1 file changed, 44 insertions(+), 56 deletions(-) diff --git a/yarn.lock b/yarn.lock index efebd64bbd..5c29e2d5e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -74,38 +74,38 @@ __metadata: linkType: hard "@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.22.1, @babel/core@npm:^7.24.4, @babel/core@npm:^7.25.0": - version: 7.26.9 - resolution: "@babel/core@npm:7.26.9" + version: 7.26.10 + resolution: "@babel/core@npm:7.26.10" dependencies: "@ampproject/remapping": "npm:^2.2.0" "@babel/code-frame": "npm:^7.26.2" - "@babel/generator": "npm:^7.26.9" + "@babel/generator": "npm:^7.26.10" "@babel/helper-compilation-targets": "npm:^7.26.5" "@babel/helper-module-transforms": "npm:^7.26.0" - "@babel/helpers": "npm:^7.26.9" - "@babel/parser": "npm:^7.26.9" + "@babel/helpers": "npm:^7.26.10" + "@babel/parser": "npm:^7.26.10" "@babel/template": "npm:^7.26.9" - "@babel/traverse": "npm:^7.26.9" - "@babel/types": "npm:^7.26.9" + "@babel/traverse": "npm:^7.26.10" + "@babel/types": "npm:^7.26.10" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 10c0/ed7212ff42a9453765787019b7d191b167afcacd4bd8fec10b055344ef53fa0cc648c9a80159ae4ecf870016a6318731e087042dcb68d1a2a9d34eb290dc014b + checksum: 10c0/e046e0e988ab53841b512ee9d263ca409f6c46e2a999fe53024688b92db394346fa3aeae5ea0866331f62133982eee05a675d22922a4603c3f603aa09a581d62 languageName: node linkType: hard -"@babel/generator@npm:^7.26.9, @babel/generator@npm:^7.7.2": - version: 7.26.9 - resolution: "@babel/generator@npm:7.26.9" +"@babel/generator@npm:^7.26.10, @babel/generator@npm:^7.7.2": + version: 7.26.10 + resolution: "@babel/generator@npm:7.26.10" dependencies: - "@babel/parser": "npm:^7.26.9" - "@babel/types": "npm:^7.26.9" + "@babel/parser": "npm:^7.26.10" + "@babel/types": "npm:^7.26.10" "@jridgewell/gen-mapping": "npm:^0.3.5" "@jridgewell/trace-mapping": "npm:^0.3.25" jsesc: "npm:^3.0.2" - checksum: 10c0/6b78872128205224a9a9761b9ea7543a9a7902a04b82fc2f6801ead4de8f59056bab3fd17b1f834ca7b049555fc4c79234b9a6230dd9531a06525306050becad + checksum: 10c0/88b3b3ea80592fc89349c4e1a145e1386e4042866d2507298adf452bf972f68d13bf699a845e6ab8c028bd52c2247013eb1221b86e1db5c9779faacba9c4b10e languageName: node linkType: hard @@ -171,7 +171,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-define-polyfill-provider@npm:^0.6.1, @babel/helper-define-polyfill-provider@npm:^0.6.2, @babel/helper-define-polyfill-provider@npm:^0.6.3": +"@babel/helper-define-polyfill-provider@npm:^0.6.1, @babel/helper-define-polyfill-provider@npm:^0.6.3": version: 0.6.3 resolution: "@babel/helper-define-polyfill-provider@npm:0.6.3" dependencies: @@ -303,24 +303,24 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.26.9": - version: 7.26.9 - resolution: "@babel/helpers@npm:7.26.9" +"@babel/helpers@npm:^7.26.10": + version: 7.26.10 + resolution: "@babel/helpers@npm:7.26.10" dependencies: "@babel/template": "npm:^7.26.9" - "@babel/types": "npm:^7.26.9" - checksum: 10c0/3d4dbc4a33fe4181ed810cac52318b578294745ceaec07e2f6ecccf6cda55d25e4bfcea8f085f333bf911c9e1fc13320248dd1d5315ab47ad82ce1077410df05 + "@babel/types": "npm:^7.26.10" + checksum: 10c0/f99e1836bcffce96db43158518bb4a24cf266820021f6461092a776cba2dc01d9fc8b1b90979d7643c5c2ab7facc438149064463a52dd528b21c6ab32509784f languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.26.9": - version: 7.26.9 - resolution: "@babel/parser@npm:7.26.9" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.26.10, @babel/parser@npm:^7.26.9": + version: 7.26.10 + resolution: "@babel/parser@npm:7.26.10" dependencies: - "@babel/types": "npm:^7.26.9" + "@babel/types": "npm:^7.26.10" bin: parser: ./bin/babel-parser.js - checksum: 10c0/4b9ef3c9a0d4c328e5e5544f50fe8932c36f8a2c851e7f14a85401487cd3da75cad72c2e1bcec1eac55599a6bbb2fdc091f274c4fcafa6bdd112d4915ff087fc + checksum: 10c0/c47f5c0f63cd12a663e9dc94a635f9efbb5059d98086a92286d7764357c66bceba18ccbe79333e01e9be3bfb8caba34b3aaebfd8e62c3d5921c8cf907267be75 languageName: node linkType: hard @@ -1137,18 +1137,18 @@ __metadata: linkType: hard "@babel/plugin-transform-runtime@npm:^7.22.4": - version: 7.26.9 - resolution: "@babel/plugin-transform-runtime@npm:7.26.9" + version: 7.26.10 + resolution: "@babel/plugin-transform-runtime@npm:7.26.10" dependencies: "@babel/helper-module-imports": "npm:^7.25.9" "@babel/helper-plugin-utils": "npm:^7.26.5" babel-plugin-polyfill-corejs2: "npm:^0.4.10" - babel-plugin-polyfill-corejs3: "npm:^0.10.6" + babel-plugin-polyfill-corejs3: "npm:^0.11.0" babel-plugin-polyfill-regenerator: "npm:^0.6.1" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10c0/2c4d77d0671badc7fd53dcd7015df5db892712436c7e9740ffb2f5b85e8591e5bfe208f78dff402b4ee2d55d0f7a3c0a1102c683f333f4ee0cfa62f68ea68842 + checksum: 10c0/4b70a63b904a3f7faa6ca95f9034d2f29330764820b06cf1814dda4ab0482b233a28241e98d8497bc1690dd31972e72861d8534ae0e37f26e04637e7d615e43d languageName: node linkType: hard @@ -1403,11 +1403,11 @@ __metadata: linkType: hard "@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.22.3, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": - version: 7.26.9 - resolution: "@babel/runtime@npm:7.26.9" + version: 7.26.10 + resolution: "@babel/runtime@npm:7.26.10" dependencies: regenerator-runtime: "npm:^0.14.0" - checksum: 10c0/e8517131110a6ec3a7360881438b85060e49824e007f4a64b5dfa9192cf2bb5c01e84bfc109f02d822c7edb0db926928dd6b991e3ee460b483fb0fac43152d9b + checksum: 10c0/6dc6d88c7908f505c4f7770fb4677dfa61f68f659b943c2be1f2a99cb6680343462867abf2d49822adc435932919b36c77ac60125793e719ea8745f2073d3745 languageName: node linkType: hard @@ -1422,28 +1422,28 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.25.0, @babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.26.8, @babel/traverse@npm:^7.26.9": - version: 7.26.9 - resolution: "@babel/traverse@npm:7.26.9" +"@babel/traverse@npm:^7.25.0, @babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.26.10, @babel/traverse@npm:^7.26.8": + version: 7.26.10 + resolution: "@babel/traverse@npm:7.26.10" dependencies: "@babel/code-frame": "npm:^7.26.2" - "@babel/generator": "npm:^7.26.9" - "@babel/parser": "npm:^7.26.9" + "@babel/generator": "npm:^7.26.10" + "@babel/parser": "npm:^7.26.10" "@babel/template": "npm:^7.26.9" - "@babel/types": "npm:^7.26.9" + "@babel/types": "npm:^7.26.10" debug: "npm:^4.3.1" globals: "npm:^11.1.0" - checksum: 10c0/51dd57fa39ea34d04816806bfead04c74f37301269d24c192d1406dc6e244fea99713b3b9c5f3e926d9ef6aa9cd5c062ad4f2fc1caa9cf843d5e864484ac955e + checksum: 10c0/4e86bb4e3c30a6162bb91df86329df79d96566c3e2d9ccba04f108c30473a3a4fd360d9990531493d90f6a12004f10f616bf9b9229ca30c816b708615e9de2ac languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.0.0-beta.49, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.9, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": - version: 7.26.9 - resolution: "@babel/types@npm:7.26.9" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.0.0-beta.49, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.10, @babel/types@npm:^7.26.9, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": + version: 7.26.10 + resolution: "@babel/types@npm:7.26.10" dependencies: "@babel/helper-string-parser": "npm:^7.25.9" "@babel/helper-validator-identifier": "npm:^7.25.9" - checksum: 10c0/999c56269ba00e5c57aa711fbe7ff071cd6990bafd1b978341ea7572cc78919986e2aa6ee51dacf4b6a7a6fa63ba4eb3f1a03cf55eee31b896a56d068b895964 + checksum: 10c0/7a7f83f568bfc3dfabfaf9ae3a97ab5c061726c0afa7dcd94226d4f84a81559da368ed79671e3a8039d16f12476cf110381a377ebdea07587925f69628200dac languageName: node linkType: hard @@ -5377,18 +5377,6 @@ __metadata: languageName: node linkType: hard -"babel-plugin-polyfill-corejs3@npm:^0.10.6": - version: 0.10.6 - resolution: "babel-plugin-polyfill-corejs3@npm:0.10.6" - dependencies: - "@babel/helper-define-polyfill-provider": "npm:^0.6.2" - core-js-compat: "npm:^3.38.0" - peerDependencies: - "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10c0/3a69220471b07722c2ae6537310bf26b772514e12b601398082965459c838be70a0ca70b0662f0737070654ff6207673391221d48599abb4a2b27765206d9f79 - languageName: node - linkType: hard - "babel-plugin-polyfill-corejs3@npm:^0.11.0": version: 0.11.1 resolution: "babel-plugin-polyfill-corejs3@npm:0.11.1" @@ -6467,7 +6455,7 @@ __metadata: languageName: node linkType: hard -"core-js-compat@npm:^3.38.0, core-js-compat@npm:^3.40.0": +"core-js-compat@npm:^3.40.0": version: 3.40.0 resolution: "core-js-compat@npm:3.40.0" dependencies: From 2454a81e7129a133afb7054e6985d10a12cc1675 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:51:43 +0100 Subject: [PATCH 09/12] Update dependency axios to v1.8.3 (#34146) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5c29e2d5e5..befacca128 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5234,13 +5234,13 @@ __metadata: linkType: hard "axios@npm:^1.4.0": - version: 1.8.2 - resolution: "axios@npm:1.8.2" + version: 1.8.3 + resolution: "axios@npm:1.8.3" dependencies: follow-redirects: "npm:^1.15.6" form-data: "npm:^4.0.0" proxy-from-env: "npm:^1.1.0" - checksum: 10c0/d8c2969e4642dc6d39555ac58effe06c051ba7aac2bd40cad7a9011c019fb2f16ee011c5a6906cb25b8a4f87258c359314eb981f852e60ad445ecaeb793c7aa2 + checksum: 10c0/de75da9859adf0a6481d4af2b687db357a054d20f0d69b99d502b71dae3578326b1fdc0951dabaef769827484941cda93d3f89150bf9e04f05f6615fb8316780 languageName: node linkType: hard From c59890cda3847f689725acacab70403385d6765c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:52:09 +0100 Subject: [PATCH 10/12] Update dependency rails to v8.0.2 (#34145) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 106 +++++++++++++++++++++++++-------------------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 812a1e0146..9fda991bef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,29 +10,29 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (8.0.1) - actionpack (= 8.0.1) - activesupport (= 8.0.1) + actioncable (8.0.2) + actionpack (= 8.0.2) + activesupport (= 8.0.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.0.1) - actionpack (= 8.0.1) - activejob (= 8.0.1) - activerecord (= 8.0.1) - activestorage (= 8.0.1) - activesupport (= 8.0.1) + actionmailbox (8.0.2) + actionpack (= 8.0.2) + activejob (= 8.0.2) + activerecord (= 8.0.2) + activestorage (= 8.0.2) + activesupport (= 8.0.2) mail (>= 2.8.0) - actionmailer (8.0.1) - actionpack (= 8.0.1) - actionview (= 8.0.1) - activejob (= 8.0.1) - activesupport (= 8.0.1) + actionmailer (8.0.2) + actionpack (= 8.0.2) + actionview (= 8.0.2) + activejob (= 8.0.2) + activesupport (= 8.0.2) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.0.1) - actionview (= 8.0.1) - activesupport (= 8.0.1) + actionpack (8.0.2) + actionview (= 8.0.2) + activesupport (= 8.0.2) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -40,15 +40,15 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.0.1) - actionpack (= 8.0.1) - activerecord (= 8.0.1) - activestorage (= 8.0.1) - activesupport (= 8.0.1) + actiontext (8.0.2) + actionpack (= 8.0.2) + activerecord (= 8.0.2) + activestorage (= 8.0.2) + activesupport (= 8.0.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.0.1) - activesupport (= 8.0.1) + actionview (8.0.2) + activesupport (= 8.0.2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) @@ -58,22 +58,22 @@ GEM activemodel (>= 4.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (8.0.1) - activesupport (= 8.0.1) + activejob (8.0.2) + activesupport (= 8.0.2) globalid (>= 0.3.6) - activemodel (8.0.1) - activesupport (= 8.0.1) - activerecord (8.0.1) - activemodel (= 8.0.1) - activesupport (= 8.0.1) + activemodel (8.0.2) + activesupport (= 8.0.2) + activerecord (8.0.2) + activemodel (= 8.0.2) + activesupport (= 8.0.2) timeout (>= 0.4.0) - activestorage (8.0.1) - actionpack (= 8.0.1) - activejob (= 8.0.1) - activerecord (= 8.0.1) - activesupport (= 8.0.1) + activestorage (8.0.2) + actionpack (= 8.0.2) + activejob (= 8.0.2) + activerecord (= 8.0.2) + activesupport (= 8.0.2) marcel (~> 1.0) - activesupport (8.0.1) + activesupport (8.0.2) base64 benchmark (>= 0.3) bigdecimal @@ -637,20 +637,20 @@ GEM rackup (1.0.1) rack (< 3) webrick - rails (8.0.1) - actioncable (= 8.0.1) - actionmailbox (= 8.0.1) - actionmailer (= 8.0.1) - actionpack (= 8.0.1) - actiontext (= 8.0.1) - actionview (= 8.0.1) - activejob (= 8.0.1) - activemodel (= 8.0.1) - activerecord (= 8.0.1) - activestorage (= 8.0.1) - activesupport (= 8.0.1) + rails (8.0.2) + actioncable (= 8.0.2) + actionmailbox (= 8.0.2) + actionmailer (= 8.0.2) + actionpack (= 8.0.2) + actiontext (= 8.0.2) + actionview (= 8.0.2) + activejob (= 8.0.2) + activemodel (= 8.0.2) + activerecord (= 8.0.2) + activestorage (= 8.0.2) + activesupport (= 8.0.2) bundler (>= 1.15.0) - railties (= 8.0.1) + railties (= 8.0.2) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -661,9 +661,9 @@ GEM rails-i18n (8.0.1) i18n (>= 0.7, < 2) railties (>= 8.0.0, < 9) - railties (8.0.1) - actionpack (= 8.0.1) - activesupport (= 8.0.1) + railties (8.0.2) + actionpack (= 8.0.2) + activesupport (= 8.0.2) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) From 98d703ac91d0053e852ff0abd3164a349f33bb30 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 17:25:27 +0100 Subject: [PATCH 11/12] Update dependency pg to v8.14.0 (#34141) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/yarn.lock b/yarn.lock index befacca128..283e1a2b55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13146,19 +13146,19 @@ __metadata: languageName: node linkType: hard -"pg-pool@npm:^3.7.1": - version: 3.7.1 - resolution: "pg-pool@npm:3.7.1" +"pg-pool@npm:^3.8.0": + version: 3.8.0 + resolution: "pg-pool@npm:3.8.0" peerDependencies: pg: ">=8.0" - checksum: 10c0/65bff013102684774f4cfdffbfe0a2b0614252234561d391608f7fd915477e44f5fd0e1968ecc2f42032dcf8bac241d2f724c4f3df90384567d22df37dd3b6ba + checksum: 10c0/c05287b0caafeab43807e6ad22d153c09c473dbeb5b2cea13b83102376e9a56f46b91fa9adf9d53885ce198280c6a95555390987c42b3858d1936d3e0cdc83aa languageName: node linkType: hard -"pg-protocol@npm:*, pg-protocol@npm:^1.7.1": - version: 1.7.1 - resolution: "pg-protocol@npm:1.7.1" - checksum: 10c0/3168d407ddc4c0fa2403eb9b49205399d4bc53dadbafdfcc5d25fa61b860a31c25df25704cf14c8140c80f0a41061d586e5fd5ce9bf800dfb91e9ce810bc2c37 +"pg-protocol@npm:*, pg-protocol@npm:^1.8.0": + version: 1.8.0 + resolution: "pg-protocol@npm:1.8.0" + checksum: 10c0/2be784955599d84b564795952cee52cc2b8eab0be43f74fc1061506353801e282c1d52c9e0691a9b72092c1f3fde370e9b181e80fef6bb82a9b8d1618bfa91e6 languageName: node linkType: hard @@ -13191,13 +13191,13 @@ __metadata: linkType: hard "pg@npm:^8.5.0": - version: 8.13.3 - resolution: "pg@npm:8.13.3" + version: 8.14.0 + resolution: "pg@npm:8.14.0" dependencies: pg-cloudflare: "npm:^1.1.1" pg-connection-string: "npm:^2.7.0" - pg-pool: "npm:^3.7.1" - pg-protocol: "npm:^1.7.1" + pg-pool: "npm:^3.8.0" + pg-protocol: "npm:^1.8.0" pg-types: "npm:^2.1.0" pgpass: "npm:1.x" peerDependencies: @@ -13208,7 +13208,7 @@ __metadata: peerDependenciesMeta: pg-native: optional: true - checksum: 10c0/7296f0e5930b35faef471be2673210cda553b30f1b8e9d176fcc286aa43248e17e09336032bf5a6bba55d2cc2d03afb8a407b5a6e6bc56ebb331c02d1a7ccc05 + checksum: 10c0/14d9fe726189107b028d5603b299776d039e36ed657c99057bcc1c125f889cb46536e0c48c6d98952231733c788f98c631bf74d5f8c9cbf85c4ac7c0a119b8b4 languageName: node linkType: hard From aff51823752b809c3349698b6be02bfdda7a9010 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 18:52:24 +0100 Subject: [PATCH 12/12] Update dependency rubocop-capybara to v2.22.1 (#34153) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9fda991bef..e0ec65a322 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -333,7 +333,7 @@ GEM azure-blob (~> 0.5.2) hashie (~> 5.0) jmespath (1.6.2) - json (2.10.1) + json (2.10.2) json-canonicalization (1.0.0) json-jwt (1.16.7) activesupport (>= 4.2) @@ -742,7 +742,7 @@ GEM unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.38.1) parser (>= 3.3.1.0) - rubocop-capybara (2.22.0) + rubocop-capybara (2.22.1) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) rubocop-i18n (3.2.3)