From 23aeef52cc4540b4514e9f3b935b21f0530a3746 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 6 Jul 2019 23:26:16 +0200 Subject: [PATCH 01/71] Remove Salmon and PubSubHubbub (#11205) * Remove Salmon and PubSubHubbub endpoints * Add error when trying to follow OStatus accounts * Fix new accounts not being created in ResolveAccountService --- .../activitypub/inboxes_controller.rb | 1 - app/controllers/admin/accounts_controller.rb | 16 +- app/controllers/api/push_controller.rb | 73 ----- app/controllers/api/salmon_controller.rb | 37 --- .../api/subscriptions_controller.rb | 51 ---- app/controllers/api/v1/follows_controller.rb | 31 --- app/lib/ostatus/activity/base.rb | 71 ----- app/lib/ostatus/activity/creation.rb | 219 --------------- app/lib/ostatus/activity/deletion.rb | 16 -- app/lib/ostatus/activity/general.rb | 20 -- app/lib/ostatus/activity/post.rb | 23 -- app/lib/ostatus/activity/remote.rb | 11 - app/lib/ostatus/activity/share.rb | 26 -- app/lib/ostatus/atom_serializer.rb | 2 - app/models/account.rb | 3 +- app/serializers/webfinger_serializer.rb | 1 - app/services/authorize_follow_service.rb | 12 +- app/services/batched_remove_status_service.rb | 34 +-- app/services/block_domain_service.rb | 1 - app/services/block_service.rb | 12 +- app/services/concerns/author_extractor.rb | 23 -- .../concerns/stream_entry_renderer.rb | 7 - app/services/favourite_service.rb | 6 - app/services/fetch_remote_account_service.rb | 28 -- app/services/fetch_remote_status_service.rb | 28 -- app/services/follow_service.rb | 24 +- app/services/post_status_service.rb | 1 - app/services/process_feed_service.rb | 31 --- app/services/process_interaction_service.rb | 151 ----------- app/services/process_mentions_service.rb | 7 - .../pubsubhubbub/subscribe_service.rb | 53 ---- .../pubsubhubbub/unsubscribe_service.rb | 31 --- app/services/reblog_service.rb | 4 - app/services/reject_follow_service.rb | 12 +- app/services/remove_status_service.rb | 13 - app/services/resolve_account_service.rb | 133 +-------- app/services/send_interaction_service.rb | 39 --- app/services/subscribe_service.rb | 58 ---- app/services/unblock_service.rb | 12 +- app/services/unfavourite_service.rb | 13 +- app/services/unfollow_service.rb | 16 +- app/services/unsubscribe_service.rb | 36 --- app/services/update_remote_profile_service.rb | 66 ----- app/services/verify_salmon_service.rb | 26 -- app/views/accounts/show.html.haml | 1 - .../subscriptions/_subscription.html.haml | 18 -- app/views/admin/subscriptions/index.html.haml | 16 -- app/views/well_known/webfinger/show.xml.ruby | 5 - .../after_remote_follow_request_worker.rb | 24 +- app/workers/after_remote_follow_worker.rb | 24 +- app/workers/notification_worker.rb | 4 +- app/workers/processing_worker.rb | 4 +- .../pubsubhubbub/confirmation_worker.rb | 75 +----- app/workers/pubsubhubbub/delivery_worker.rb | 74 +---- .../pubsubhubbub/distribution_worker.rb | 25 +- .../pubsubhubbub/raw_distribution_worker.rb | 15 +- app/workers/pubsubhubbub/subscribe_worker.rb | 27 +- .../pubsubhubbub/unsubscribe_worker.rb | 8 +- app/workers/remote_profile_update_worker.rb | 6 +- app/workers/salmon_worker.rb | 6 +- .../scheduler/subscriptions_scheduler.rb | 10 +- config/locales/en.yml | 7 - config/navigation.rb | 1 - config/routes.rb | 14 - config/sidekiq.yml | 3 - .../admin/accounts_controller_spec.rb | 38 --- .../admin/subscriptions_controller_spec.rb | 32 --- spec/controllers/api/push_controller_spec.rb | 59 ---- .../controllers/api/salmon_controller_spec.rb | 65 ----- .../api/subscriptions_controller_spec.rb | 68 ----- .../api/v1/follows_controller_spec.rb | 51 ---- spec/fixtures/requests/webfinger.txt | 2 +- spec/lib/ostatus/atom_serializer_spec.rb | 145 ---------- .../services/authorize_follow_service_spec.rb | 7 - .../batched_remove_status_service_spec.rb | 13 - spec/services/block_service_spec.rb | 7 - spec/services/favourite_service_spec.rb | 7 - .../fetch_remote_account_service_spec.rb | 40 --- spec/services/follow_service_spec.rb | 68 ----- spec/services/import_service_spec.rb | 30 ++- spec/services/post_status_service_spec.rb | 2 - spec/services/process_feed_service_spec.rb | 252 ------------------ .../process_interaction_service_spec.rb | 151 ----------- .../services/process_mentions_service_spec.rb | 4 - .../pubsubhubbub/subscribe_service_spec.rb | 71 ----- .../pubsubhubbub/unsubscribe_service_spec.rb | 46 ---- spec/services/reblog_service_spec.rb | 4 - spec/services/reject_follow_service_spec.rb | 7 - spec/services/remove_status_service_spec.rb | 13 - spec/services/resolve_account_service_spec.rb | 88 +----- .../services/send_interaction_service_spec.rb | 7 - spec/services/subscribe_service_spec.rb | 43 --- spec/services/unblock_service_spec.rb | 7 - spec/services/unfollow_service_spec.rb | 7 - spec/services/unsubscribe_service_spec.rb | 37 --- .../update_remote_profile_service_spec.rb | 84 ------ ...after_remote_follow_request_worker_spec.rb | 59 ---- .../after_remote_follow_worker_spec.rb | 59 ---- .../pubsubhubbub/confirmation_worker_spec.rb | 88 ------ .../pubsubhubbub/delivery_worker_spec.rb | 68 ----- .../pubsubhubbub/distribution_worker_spec.rb | 46 ---- .../scheduler/subscriptions_scheduler_spec.rb | 19 -- 102 files changed, 70 insertions(+), 3569 deletions(-) delete mode 100644 app/controllers/api/push_controller.rb delete mode 100644 app/controllers/api/salmon_controller.rb delete mode 100644 app/controllers/api/subscriptions_controller.rb delete mode 100644 app/controllers/api/v1/follows_controller.rb delete mode 100644 app/lib/ostatus/activity/base.rb delete mode 100644 app/lib/ostatus/activity/creation.rb delete mode 100644 app/lib/ostatus/activity/deletion.rb delete mode 100644 app/lib/ostatus/activity/general.rb delete mode 100644 app/lib/ostatus/activity/post.rb delete mode 100644 app/lib/ostatus/activity/remote.rb delete mode 100644 app/lib/ostatus/activity/share.rb delete mode 100644 app/services/concerns/author_extractor.rb delete mode 100644 app/services/concerns/stream_entry_renderer.rb delete mode 100644 app/services/process_feed_service.rb delete mode 100644 app/services/process_interaction_service.rb delete mode 100644 app/services/pubsubhubbub/subscribe_service.rb delete mode 100644 app/services/pubsubhubbub/unsubscribe_service.rb delete mode 100644 app/services/send_interaction_service.rb delete mode 100644 app/services/subscribe_service.rb delete mode 100644 app/services/unsubscribe_service.rb delete mode 100644 app/services/update_remote_profile_service.rb delete mode 100644 app/services/verify_salmon_service.rb delete mode 100644 app/views/admin/subscriptions/_subscription.html.haml delete mode 100644 app/views/admin/subscriptions/index.html.haml delete mode 100644 spec/controllers/admin/subscriptions_controller_spec.rb delete mode 100644 spec/controllers/api/push_controller_spec.rb delete mode 100644 spec/controllers/api/salmon_controller_spec.rb delete mode 100644 spec/controllers/api/subscriptions_controller_spec.rb delete mode 100644 spec/controllers/api/v1/follows_controller_spec.rb delete mode 100644 spec/services/process_feed_service_spec.rb delete mode 100644 spec/services/process_interaction_service_spec.rb delete mode 100644 spec/services/pubsubhubbub/subscribe_service_spec.rb delete mode 100644 spec/services/pubsubhubbub/unsubscribe_service_spec.rb delete mode 100644 spec/services/send_interaction_service_spec.rb delete mode 100644 spec/services/subscribe_service_spec.rb delete mode 100644 spec/services/unsubscribe_service_spec.rb delete mode 100644 spec/services/update_remote_profile_service_spec.rb delete mode 100644 spec/workers/after_remote_follow_request_worker_spec.rb delete mode 100644 spec/workers/after_remote_follow_worker_spec.rb delete mode 100644 spec/workers/pubsubhubbub/confirmation_worker_spec.rb delete mode 100644 spec/workers/pubsubhubbub/delivery_worker_spec.rb delete mode 100644 spec/workers/pubsubhubbub/distribution_worker_spec.rb delete mode 100644 spec/workers/scheduler/subscriptions_scheduler_spec.rb diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index a0b7532c2e..e2cd8eaedb 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -44,7 +44,6 @@ class ActivityPub::InboxesController < Api::BaseController ResolveAccountWorker.perform_async(signed_request_account.acct) end - Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed? DeliveryFailureTracker.track_inverse_success!(signed_request_account) end diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 0c7760d779..2fa1dfe5fc 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -2,8 +2,8 @@ module Admin class AccountsController < BaseController - before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject] - before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload] + before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject] + before_action :require_remote_account!, only: [:redownload] before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject] def index @@ -19,18 +19,6 @@ module Admin @warnings = @account.targeted_account_warnings.latest.custom end - def subscribe - authorize @account, :subscribe? - Pubsubhubbub::SubscribeWorker.perform_async(@account.id) - redirect_to admin_account_path(@account.id) - end - - def unsubscribe - authorize @account, :unsubscribe? - Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id) - redirect_to admin_account_path(@account.id) - end - def memorialize authorize @account, :memorialize? @account.memorialize! diff --git a/app/controllers/api/push_controller.rb b/app/controllers/api/push_controller.rb deleted file mode 100644 index e04d19125b..0000000000 --- a/app/controllers/api/push_controller.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -class Api::PushController < Api::BaseController - include SignatureVerification - - def update - response, status = process_push_request - render plain: response, status: status - end - - private - - def process_push_request - case hub_mode - when 'subscribe' - Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds, verified_domain) - when 'unsubscribe' - Pubsubhubbub::UnsubscribeService.new.call(account_from_topic, hub_callback) - else - ["Unknown mode: #{hub_mode}", 422] - end - end - - def hub_mode - params['hub.mode'] - end - - def hub_topic - params['hub.topic'] - end - - def hub_callback - params['hub.callback'] - end - - def hub_lease_seconds - params['hub.lease_seconds'] - end - - def hub_secret - params['hub.secret'] - end - - def account_from_topic - if hub_topic.present? && local_domain? && account_feed_path? - Account.find_local(hub_topic_params[:username]) - end - end - - def hub_topic_params - @_hub_topic_params ||= Rails.application.routes.recognize_path(hub_topic_uri.path) - end - - def hub_topic_uri - @_hub_topic_uri ||= Addressable::URI.parse(hub_topic).normalize - end - - def local_domain? - TagManager.instance.web_domain?(hub_topic_domain) - end - - def verified_domain - return signed_request_account.domain if signed_request_account - end - - def hub_topic_domain - hub_topic_uri.host + (hub_topic_uri.port ? ":#{hub_topic_uri.port}" : '') - end - - def account_feed_path? - hub_topic_params[:controller] == 'accounts' && hub_topic_params[:action] == 'show' && hub_topic_params[:format] == 'atom' - end -end diff --git a/app/controllers/api/salmon_controller.rb b/app/controllers/api/salmon_controller.rb deleted file mode 100644 index ac5f3268d8..0000000000 --- a/app/controllers/api/salmon_controller.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -class Api::SalmonController < Api::BaseController - include SignatureVerification - - before_action :set_account - respond_to :txt - - def update - if verify_payload? - process_salmon - head 202 - elsif payload.present? - render plain: signature_verification_failure_reason, status: 401 - else - head 400 - end - end - - private - - def set_account - @account = Account.find(params[:id]) - end - - def payload - @_payload ||= request.body.read - end - - def verify_payload? - payload.present? && VerifySalmonService.new.call(payload) - end - - def process_salmon - SalmonWorker.perform_async(@account.id, payload.force_encoding('UTF-8')) - end -end diff --git a/app/controllers/api/subscriptions_controller.rb b/app/controllers/api/subscriptions_controller.rb deleted file mode 100644 index 89007f3d6e..0000000000 --- a/app/controllers/api/subscriptions_controller.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -class Api::SubscriptionsController < Api::BaseController - before_action :set_account - respond_to :txt - - def show - if subscription.valid?(params['hub.topic']) - @account.update(subscription_expires_at: future_expires) - render plain: encoded_challenge, status: 200 - else - head 404 - end - end - - def update - if subscription.verify(body, request.headers['HTTP_X_HUB_SIGNATURE']) - ProcessingWorker.perform_async(@account.id, body.force_encoding('UTF-8')) - end - - head 200 - end - - private - - def subscription - @_subscription ||= @account.subscription( - api_subscription_url(@account.id) - ) - end - - def body - @_body ||= request.body.read - end - - def encoded_challenge - HTMLEntities.new.encode(params['hub.challenge']) - end - - def future_expires - Time.now.utc + lease_seconds_or_default - end - - def lease_seconds_or_default - (params['hub.lease_seconds'] || 1.day).to_i.seconds - end - - def set_account - @account = Account.find(params[:id]) - end -end diff --git a/app/controllers/api/v1/follows_controller.rb b/app/controllers/api/v1/follows_controller.rb deleted file mode 100644 index 5420c05336..0000000000 --- a/app/controllers/api/v1/follows_controller.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::FollowsController < Api::BaseController - before_action -> { doorkeeper_authorize! :follow, :'write:follows' } - before_action :require_user! - - respond_to :json - - def create - raise ActiveRecord::RecordNotFound if follow_params[:uri].blank? - - @account = FollowService.new.call(current_user.account, target_uri).try(:target_account) - - if @account.nil? - username, domain = target_uri.split('@') - @account = Account.find_remote!(username, domain) - end - - render json: @account, serializer: REST::AccountSerializer - end - - private - - def target_uri - follow_params[:uri].strip.gsub(/\A@/, '') - end - - def follow_params - params.permit(:uri) - end -end diff --git a/app/lib/ostatus/activity/base.rb b/app/lib/ostatus/activity/base.rb deleted file mode 100644 index db70f19980..0000000000 --- a/app/lib/ostatus/activity/base.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::Base - include Redisable - - def initialize(xml, account = nil, **options) - @xml = xml - @account = account - @options = options - end - - def status? - [:activity, :note, :comment].include?(type) - end - - def verb - raw = @xml.at_xpath('./activity:verb', activity: OStatus::TagManager::AS_XMLNS).content - OStatus::TagManager::VERBS.key(raw) - rescue - :post - end - - def type - raw = @xml.at_xpath('./activity:object-type', activity: OStatus::TagManager::AS_XMLNS).content - OStatus::TagManager::TYPES.key(raw) - rescue - :activity - end - - def id - @xml.at_xpath('./xmlns:id', xmlns: OStatus::TagManager::XMLNS).content - end - - def url - link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: OStatus::TagManager::XMLNS).find { |link_candidate| link_candidate['type'] == 'text/html' } - link.nil? ? nil : link['href'] - end - - def activitypub_uri - link = @xml.xpath('./xmlns:link[@rel="alternate"]', xmlns: OStatus::TagManager::XMLNS).find { |link_candidate| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link_candidate['type']) } - link.nil? ? nil : link['href'] - end - - def activitypub_uri? - activitypub_uri.present? - end - - private - - def find_status(uri) - if OStatus::TagManager.instance.local_id?(uri) - local_id = OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Status') - return Status.find_by(id: local_id) - elsif ActivityPub::TagManager.instance.local_uri?(uri) - local_id = ActivityPub::TagManager.instance.uri_to_local_id(uri) - return Status.find_by(id: local_id) - end - - Status.find_by(uri: uri) - end - - def find_activitypub_status(uri, href) - tag_matches = /tag:([^,:]+)[^:]*:objectId=([\d]+)/.match(uri) - href_matches = %r{/users/([^/]+)}.match(href) - - unless tag_matches.nil? || href_matches.nil? - uri = "https://#{tag_matches[1]}/users/#{href_matches[1]}/statuses/#{tag_matches[2]}" - Status.find_by(uri: uri) - end - end -end diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb deleted file mode 100644 index 60de712db8..0000000000 --- a/app/lib/ostatus/activity/creation.rb +++ /dev/null @@ -1,219 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::Creation < OStatus::Activity::Base - def perform - if redis.exists("delete_upon_arrival:#{@account.id}:#{id}") - Rails.logger.debug "Delete for status #{id} was queued, ignoring" - return [nil, false] - end - - return [nil, false] if @account.suspended? || invalid_origin? - - RedisLock.acquire(lock_options) do |lock| - if lock.acquired? - # Return early if status already exists in db - @status = find_status(id) - return [@status, false] unless @status.nil? - @status = process_status - else - raise Mastodon::RaceConditionError - end - end - - [@status, true] - end - - def process_status - Rails.logger.debug "Creating remote status #{id}" - cached_reblog = reblog - status = nil - - # Skip if the reblogged status is not public - return if cached_reblog && !(cached_reblog.public_visibility? || cached_reblog.unlisted_visibility?) - - media_attachments = save_media.take(4) - - ApplicationRecord.transaction do - status = Status.create!( - uri: id, - url: url, - account: @account, - reblog: cached_reblog, - text: content, - spoiler_text: content_warning, - created_at: published, - override_timestamps: @options[:override_timestamps], - reply: thread?, - language: content_language, - visibility: visibility_scope, - conversation: find_or_create_conversation, - thread: thread? ? find_status(thread.first) || find_activitypub_status(thread.first, thread.second) : nil, - media_attachment_ids: media_attachments.map(&:id), - sensitive: sensitive? - ) - - save_mentions(status) - save_hashtags(status) - save_emojis(status) - end - - if thread? && status.thread.nil? && Request.valid_url?(thread.second) - Rails.logger.debug "Trying to attach #{status.id} (#{id}) to #{thread.first}" - ThreadResolveWorker.perform_async(status.id, thread.second) - end - - Rails.logger.debug "Queuing remote status #{status.id} (#{id}) for distribution" - - LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text? - - # Only continue if the status is supposed to have arrived in real-time. - # Note that if @options[:override_timestamps] isn't set, the status - # may have a lower snowflake id than other existing statuses, potentially - # "hiding" it from paginated API calls - return status unless @options[:override_timestamps] || status.within_realtime_window? - - DistributionWorker.perform_async(status.id) - - status - end - - def content - @xml.at_xpath('./xmlns:content', xmlns: OStatus::TagManager::XMLNS).content - end - - def content_language - @xml.at_xpath('./xmlns:content', xmlns: OStatus::TagManager::XMLNS)['xml:lang']&.presence || 'en' - end - - def content_warning - @xml.at_xpath('./xmlns:summary', xmlns: OStatus::TagManager::XMLNS)&.content || '' - end - - def visibility_scope - @xml.at_xpath('./mastodon:scope', mastodon: OStatus::TagManager::MTDN_XMLNS)&.content&.to_sym || :public - end - - def published - @xml.at_xpath('./xmlns:published', xmlns: OStatus::TagManager::XMLNS).content - end - - def thread? - !@xml.at_xpath('./thr:in-reply-to', thr: OStatus::TagManager::THR_XMLNS).nil? - end - - def thread - thr = @xml.at_xpath('./thr:in-reply-to', thr: OStatus::TagManager::THR_XMLNS) - [thr['ref'], thr['href']] - end - - private - - def sensitive? - # OStatus-specific convention (not standard) - @xml.xpath('./xmlns:category', xmlns: OStatus::TagManager::XMLNS).any? { |category| category['term'] == 'nsfw' } - end - - def find_or_create_conversation - uri = @xml.at_xpath('./ostatus:conversation', ostatus: OStatus::TagManager::OS_XMLNS)&.attribute('ref')&.content - return if uri.nil? - - if OStatus::TagManager.instance.local_id?(uri) - local_id = OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation') - return Conversation.find_by(id: local_id) - end - - Conversation.find_by(uri: uri) || Conversation.create!(uri: uri) - end - - def save_mentions(parent) - processed_account_ids = [] - - @xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: OStatus::TagManager::XMLNS).each do |link| - next if [OStatus::TagManager::TYPES[:group], OStatus::TagManager::TYPES[:collection]].include? link['ostatus:object-type'] - - mentioned_account = account_from_href(link['href']) - - next if mentioned_account.nil? || processed_account_ids.include?(mentioned_account.id) - - mentioned_account.mentions.where(status: parent).first_or_create(status: parent) - - # So we can skip duplicate mentions - processed_account_ids << mentioned_account.id - end - end - - def save_hashtags(parent) - tags = @xml.xpath('./xmlns:category', xmlns: OStatus::TagManager::XMLNS).map { |category| category['term'] }.select(&:present?) - ProcessHashtagsService.new.call(parent, tags) - end - - def save_media - do_not_download = DomainBlock.reject_media?(@account.domain) - media_attachments = [] - - @xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: OStatus::TagManager::XMLNS).each do |link| - next unless link['href'] - - media = MediaAttachment.where(status: nil, remote_url: link['href']).first_or_initialize(account: @account, status: nil, remote_url: link['href']) - parsed_url = Addressable::URI.parse(link['href']).normalize - - next if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty? - - media.save - media_attachments << media - - next if do_not_download - - begin - media.file_remote_url = link['href'] - media.save! - rescue ActiveRecord::RecordInvalid - next - end - end - - media_attachments - end - - def save_emojis(parent) - do_not_download = DomainBlock.reject_media?(parent.account.domain) - - return if do_not_download - - @xml.xpath('./xmlns:link[@rel="emoji"]', xmlns: OStatus::TagManager::XMLNS).each do |link| - next unless link['href'] && link['name'] - - shortcode = link['name'].delete(':') - emoji = CustomEmoji.find_by(shortcode: shortcode, domain: parent.account.domain) - - next unless emoji.nil? - - emoji = CustomEmoji.new(shortcode: shortcode, domain: parent.account.domain) - emoji.image_remote_url = link['href'] - emoji.save - end - end - - def account_from_href(href) - url = Addressable::URI.parse(href).normalize - - if TagManager.instance.web_domain?(url.host) - Account.find_local(url.path.gsub('/users/', '')) - else - Account.where(uri: href).or(Account.where(url: href)).first || FetchRemoteAccountService.new.call(href) - end - end - - def invalid_origin? - return false unless id.start_with?('http') # Legacy IDs cannot be checked - - needle = Addressable::URI.parse(id).normalized_host - - !(needle.casecmp(@account.domain).zero? || - needle.casecmp(Addressable::URI.parse(@account.remote_url.presence || @account.uri).normalized_host).zero?) - end - - def lock_options - { redis: Redis.current, key: "create:#{id}" } - end -end diff --git a/app/lib/ostatus/activity/deletion.rb b/app/lib/ostatus/activity/deletion.rb deleted file mode 100644 index c98f5ee0ad..0000000000 --- a/app/lib/ostatus/activity/deletion.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::Deletion < OStatus::Activity::Base - def perform - Rails.logger.debug "Deleting remote status #{id}" - - status = Status.find_by(uri: id, account: @account) - status ||= Status.find_by(uri: activitypub_uri, account: @account) if activitypub_uri? - - if status.nil? - redis.setex("delete_upon_arrival:#{@account.id}:#{id}", 6 * 3_600, id) - else - RemoveStatusService.new.call(status) - end - end -end diff --git a/app/lib/ostatus/activity/general.rb b/app/lib/ostatus/activity/general.rb deleted file mode 100644 index 8a6aabc337..0000000000 --- a/app/lib/ostatus/activity/general.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::General < OStatus::Activity::Base - def specialize - special_class&.new(@xml, @account, @options) - end - - private - - def special_class - case verb - when :post - OStatus::Activity::Post - when :share - OStatus::Activity::Share - when :delete - OStatus::Activity::Deletion - end - end -end diff --git a/app/lib/ostatus/activity/post.rb b/app/lib/ostatus/activity/post.rb deleted file mode 100644 index 755ed86563..0000000000 --- a/app/lib/ostatus/activity/post.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::Post < OStatus::Activity::Creation - def perform - status, just_created = super - - if just_created - status.mentions.includes(:account).each do |mention| - mentioned_account = mention.account - next unless mentioned_account.local? - NotifyService.new.call(mentioned_account, mention) - end - end - - status - end - - private - - def reblog - nil - end -end diff --git a/app/lib/ostatus/activity/remote.rb b/app/lib/ostatus/activity/remote.rb deleted file mode 100644 index 5b204b6d8f..0000000000 --- a/app/lib/ostatus/activity/remote.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::Remote < OStatus::Activity::Base - def perform - if activitypub_uri? - find_status(activitypub_uri) || FetchRemoteStatusService.new.call(url) - else - find_status(id) || FetchRemoteStatusService.new.call(url) - end - end -end diff --git a/app/lib/ostatus/activity/share.rb b/app/lib/ostatus/activity/share.rb deleted file mode 100644 index 5ca6014154..0000000000 --- a/app/lib/ostatus/activity/share.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -class OStatus::Activity::Share < OStatus::Activity::Creation - def perform - return if reblog.nil? - - status, just_created = super - NotifyService.new.call(reblog.account, status) if reblog.account.local? && just_created - status - end - - def object - @xml.at_xpath('.//activity:object', activity: OStatus::TagManager::AS_XMLNS) - end - - private - - def reblog - return @reblog if defined? @reblog - - original_status = OStatus::Activity::Remote.new(object).perform - return if original_status.nil? - - @reblog = original_status.reblog? ? original_status.reblog : original_status - end -end diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb index 9a05d96cf9..f5c0e85cae 100644 --- a/app/lib/ostatus/atom_serializer.rb +++ b/app/lib/ostatus/atom_serializer.rb @@ -53,8 +53,6 @@ class OStatus::AtomSerializer append_element(feed, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(account)) append_element(feed, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_url(account, format: 'atom')) append_element(feed, 'link', nil, rel: :next, type: 'application/atom+xml', href: account_url(account, format: 'atom', max_id: stream_entries.last.id)) if stream_entries.size == 20 - append_element(feed, 'link', nil, rel: :hub, href: api_push_url) - append_element(feed, 'link', nil, rel: :salmon, href: api_salmon_url(account.id)) stream_entries.each do |stream_entry| feed << entry(stream_entry) diff --git a/app/models/account.rb b/app/models/account.rb index c588451fce..d6772eb982 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -164,8 +164,7 @@ class Account < ApplicationRecord end def refresh! - return if local? - ResolveAccountService.new.call(acct) + ResolveAccountService.new.call(acct) unless local? end def silenced? diff --git a/app/serializers/webfinger_serializer.rb b/app/serializers/webfinger_serializer.rb index 8c0b077020..4220f697e6 100644 --- a/app/serializers/webfinger_serializer.rb +++ b/app/serializers/webfinger_serializer.rb @@ -18,7 +18,6 @@ class WebfingerSerializer < ActiveModel::Serializer { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) }, { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(object, format: 'atom') }, { rel: 'self', type: 'application/activity+json', href: account_url(object) }, - { rel: 'salmon', href: api_salmon_url(object.id) }, { rel: 'magic-public-key', href: "data:application/magic-public-key,#{object.magic_key}" }, { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" }, ] diff --git a/app/services/authorize_follow_service.rb b/app/services/authorize_follow_service.rb index 29b8700c7c..49bef727e6 100644 --- a/app/services/authorize_follow_service.rb +++ b/app/services/authorize_follow_service.rb @@ -11,25 +11,17 @@ class AuthorizeFollowService < BaseService follow_request.authorize! end - create_notification(follow_request) unless source_account.local? + create_notification(follow_request) if !source_account.local? && source_account.activitypub? follow_request end private def create_notification(follow_request) - if follow_request.account.ostatus? - NotificationWorker.perform_async(build_xml(follow_request), follow_request.target_account_id, follow_request.account_id) - elsif follow_request.account.activitypub? - ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url) - end + ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url) end def build_json(follow_request) Oj.dump(serialize_payload(follow_request, ActivityPub::AcceptFollowSerializer)) end - - def build_xml(follow_request) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request)) - end end diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index e328b17391..cb66debc8c 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class BatchedRemoveStatusService < BaseService - include StreamEntryRenderer include Redisable # Delete given statuses and reblogs of them @@ -18,10 +17,7 @@ class BatchedRemoveStatusService < BaseService @mentions = statuses.each_with_object({}) { |s, h| h[s.id] = s.active_mentions.includes(:account).to_a } @tags = statuses.each_with_object({}) { |s, h| h[s.id] = s.tags.pluck(:name) } - @stream_entry_batches = [] - @salmon_batches = [] - @json_payloads = statuses.each_with_object({}) { |s, h| h[s.id] = Oj.dump(event: :delete, payload: s.id.to_s) } - @activity_xml = {} + @json_payloads = statuses.each_with_object({}) { |s, h| h[s.id] = Oj.dump(event: :delete, payload: s.id.to_s) } # Ensure that rendered XML reflects destroyed state statuses.each do |status| @@ -39,28 +35,16 @@ class BatchedRemoveStatusService < BaseService unpush_from_home_timelines(account, account_statuses) unpush_from_list_timelines(account, account_statuses) - - batch_stream_entries(account, account_statuses) if account.local? end # Cannot be batched statuses.each do |status| unpush_from_public_timelines(status) - batch_salmon_slaps(status) if status.local? end - - Pubsubhubbub::RawDistributionWorker.push_bulk(@stream_entry_batches) { |batch| batch } - NotificationWorker.push_bulk(@salmon_batches) { |batch| batch } end private - def batch_stream_entries(account, statuses) - statuses.each do |status| - @stream_entry_batches << [build_xml(status.stream_entry), account.id] - end - end - def unpush_from_home_timelines(account, statuses) recipients = account.followers_for_local_distribution.to_a @@ -101,20 +85,4 @@ class BatchedRemoveStatusService < BaseService end end end - - def batch_salmon_slaps(status) - return if @mentions[status.id].empty? - - recipients = @mentions[status.id].map(&:account).reject(&:local?).select(&:ostatus?).uniq(&:domain).map(&:id) - - recipients.each do |recipient_id| - @salmon_batches << [build_xml(status.stream_entry), status.account_id, recipient_id] - end - end - - def build_xml(stream_entry) - return @activity_xml[stream_entry.id] if @activity_xml.key?(stream_entry.id) - - @activity_xml[stream_entry.id] = stream_entry_to_xml(stream_entry) - end end diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb index c6eef04d42..c5e5e57613 100644 --- a/app/services/block_domain_service.rb +++ b/app/services/block_domain_service.rb @@ -44,7 +44,6 @@ class BlockDomainService < BaseService def suspend_accounts! blocked_domain_accounts.without_suspended.reorder(nil).find_each do |account| - UnsubscribeService.new.call(account) if account.subscribed? SuspendAccountService.new.call(account, suspended_at: @domain_block.created_at) end end diff --git a/app/services/block_service.rb b/app/services/block_service.rb index 9050a48585..da06361c20 100644 --- a/app/services/block_service.rb +++ b/app/services/block_service.rb @@ -13,25 +13,17 @@ class BlockService < BaseService block = account.block!(target_account) BlockWorker.perform_async(account.id, target_account.id) - create_notification(block) unless target_account.local? + create_notification(block) if !target_account.local? && target_account.activitypub? block end private def create_notification(block) - if block.target_account.ostatus? - NotificationWorker.perform_async(build_xml(block), block.account_id, block.target_account_id) - elsif block.target_account.activitypub? - ActivityPub::DeliveryWorker.perform_async(build_json(block), block.account_id, block.target_account.inbox_url) - end + ActivityPub::DeliveryWorker.perform_async(build_json(block), block.account_id, block.target_account.inbox_url) end def build_json(block) Oj.dump(serialize_payload(block, ActivityPub::BlockSerializer)) end - - def build_xml(block) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.block_salmon(block)) - end end diff --git a/app/services/concerns/author_extractor.rb b/app/services/concerns/author_extractor.rb deleted file mode 100644 index c2419e9ecb..0000000000 --- a/app/services/concerns/author_extractor.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module AuthorExtractor - def author_from_xml(xml, update_profile = true) - return nil if xml.nil? - - # Try for acct - acct = xml.at_xpath('./xmlns:author/xmlns:email', xmlns: OStatus::TagManager::XMLNS)&.content - - # Try + - if acct.blank? - username = xml.at_xpath('./xmlns:author/xmlns:name', xmlns: OStatus::TagManager::XMLNS)&.content - uri = xml.at_xpath('./xmlns:author/xmlns:uri', xmlns: OStatus::TagManager::XMLNS)&.content - - return nil if username.blank? || uri.blank? - - domain = Addressable::URI.parse(uri).normalized_host - acct = "#{username}@#{domain}" - end - - ResolveAccountService.new.call(acct, update_profile: update_profile) - end -end diff --git a/app/services/concerns/stream_entry_renderer.rb b/app/services/concerns/stream_entry_renderer.rb deleted file mode 100644 index 9f6c8a082a..0000000000 --- a/app/services/concerns/stream_entry_renderer.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -module StreamEntryRenderer - def stream_entry_to_xml(stream_entry) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(stream_entry, true)) - end -end diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb index 128a24ad61..02b26458a6 100644 --- a/app/services/favourite_service.rb +++ b/app/services/favourite_service.rb @@ -30,8 +30,6 @@ class FavouriteService < BaseService if status.account.local? NotifyService.new.call(status.account, favourite) - elsif status.account.ostatus? - NotificationWorker.perform_async(build_xml(favourite), favourite.account_id, status.account_id) elsif status.account.activitypub? ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url) end @@ -46,8 +44,4 @@ class FavouriteService < BaseService def build_json(favourite) Oj.dump(serialize_payload(favourite, ActivityPub::LikeSerializer)) end - - def build_xml(favourite) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.favourite_salmon(favourite)) - end end diff --git a/app/services/fetch_remote_account_service.rb b/app/services/fetch_remote_account_service.rb index cfc560022f..a7f95603d9 100644 --- a/app/services/fetch_remote_account_service.rb +++ b/app/services/fetch_remote_account_service.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class FetchRemoteAccountService < BaseService - include AuthorExtractor - def call(url, prefetched_body = nil, protocol = :ostatus) if prefetched_body.nil? resource_url, resource_options, protocol = FetchAtomService.new.call(url) @@ -12,34 +10,8 @@ class FetchRemoteAccountService < BaseService end case protocol - when :ostatus - process_atom(resource_url, **resource_options) when :activitypub ActivityPub::FetchRemoteAccountService.new.call(resource_url, **resource_options) end end - - private - - def process_atom(url, prefetched_body:) - xml = Nokogiri::XML(prefetched_body) - xml.encoding = 'utf-8' - - account = author_from_xml(xml.at_xpath('/xmlns:feed', xmlns: OStatus::TagManager::XMLNS), false) - - UpdateRemoteProfileService.new.call(xml, account) if account.present? && trusted_domain?(url, account) - - account - rescue TypeError - Rails.logger.debug "Unparseable URL given: #{url}" - nil - rescue Nokogiri::XML::XPath::SyntaxError - Rails.logger.debug 'Invalid XML or missing namespace' - nil - end - - def trusted_domain?(url, account) - domain = Addressable::URI.parse(url).normalized_host - domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url.presence || account.uri).normalized_host).zero? - end end diff --git a/app/services/fetch_remote_status_service.rb b/app/services/fetch_remote_status_service.rb index 9c3008035d..aac39dfd53 100644 --- a/app/services/fetch_remote_status_service.rb +++ b/app/services/fetch_remote_status_service.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class FetchRemoteStatusService < BaseService - include AuthorExtractor - def call(url, prefetched_body = nil, protocol = :ostatus) if prefetched_body.nil? resource_url, resource_options, protocol = FetchAtomService.new.call(url) @@ -12,34 +10,8 @@ class FetchRemoteStatusService < BaseService end case protocol - when :ostatus - process_atom(resource_url, **resource_options) when :activitypub ActivityPub::FetchRemoteStatusService.new.call(resource_url, **resource_options) end end - - private - - def process_atom(url, prefetched_body:) - Rails.logger.debug "Processing Atom for remote status at #{url}" - - xml = Nokogiri::XML(prefetched_body) - xml.encoding = 'utf-8' - - account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: OStatus::TagManager::XMLNS)) - domain = Addressable::URI.parse(url).normalized_host - - return nil unless !account.nil? && confirmed_domain?(domain, account) - - statuses = ProcessFeedService.new.call(prefetched_body, account) - statuses.first - rescue Nokogiri::XML::XPath::SyntaxError - Rails.logger.debug 'Invalid XML or missing namespace' - nil - end - - def confirmed_domain?(domain, account) - account.domain.nil? || domain.casecmp(account.domain).zero? || domain.casecmp(Addressable::URI.parse(account.remote_url.presence || account.uri).normalized_host).zero? - end end diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 0305e2d621..8e118f5d34 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -13,7 +13,7 @@ class FollowService < BaseService target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true) raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended? - raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) || target_account.moved? + raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) || target_account.moved? || (!target_account.local? && target_account.ostatus?) if source_account.following?(target_account) # We're already following this account, but we'll call follow! again to @@ -32,7 +32,7 @@ class FollowService < BaseService if target_account.locked? || target_account.activitypub? request_follow(source_account, target_account, reblogs: reblogs) - else + elsif target_account.local? direct_follow(source_account, target_account, reblogs: reblogs) end end @@ -44,9 +44,6 @@ class FollowService < BaseService if target_account.local? LocalNotificationWorker.perform_async(target_account.id, follow_request.id, follow_request.class.name) - elsif target_account.ostatus? - NotificationWorker.perform_async(build_follow_request_xml(follow_request), source_account.id, target_account.id) - AfterRemoteFollowRequestWorker.perform_async(follow_request.id) elsif target_account.activitypub? ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), source_account.id, target_account.inbox_url) end @@ -57,27 +54,12 @@ class FollowService < BaseService def direct_follow(source_account, target_account, reblogs: true) follow = source_account.follow!(target_account, reblogs: reblogs) - if target_account.local? - LocalNotificationWorker.perform_async(target_account.id, follow.id, follow.class.name) - else - Pubsubhubbub::SubscribeWorker.perform_async(target_account.id) unless target_account.subscribed? - NotificationWorker.perform_async(build_follow_xml(follow), source_account.id, target_account.id) - AfterRemoteFollowWorker.perform_async(follow.id) - end - + LocalNotificationWorker.perform_async(target_account.id, follow.id, follow.class.name) MergeWorker.perform_async(target_account.id, source_account.id) follow end - def build_follow_request_xml(follow_request) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_request_salmon(follow_request)) - end - - def build_follow_xml(follow) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_salmon(follow)) - end - def build_json(follow_request) Oj.dump(serialize_payload(follow_request, ActivityPub::FollowSerializer)) end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 7830aee118..34ec6d5048 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -88,7 +88,6 @@ class PostStatusService < BaseService def postprocess_status! LinkCrawlWorker.perform_async(@status.id) unless @status.spoiler_text? DistributionWorker.perform_async(@status.id) - Pubsubhubbub::DistributionWorker.perform_async(@status.stream_entry.id) ActivityPub::DistributionWorker.perform_async(@status.id) PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll end diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb deleted file mode 100644 index 30a9dd85eb..0000000000 --- a/app/services/process_feed_service.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -class ProcessFeedService < BaseService - def call(body, account, **options) - @options = options - - xml = Nokogiri::XML(body) - xml.encoding = 'utf-8' - - update_author(body, account) - process_entries(xml, account) - end - - private - - def update_author(body, account) - RemoteProfileUpdateWorker.perform_async(account.id, body.force_encoding('UTF-8'), true) - end - - def process_entries(xml, account) - xml.xpath('//xmlns:entry', xmlns: OStatus::TagManager::XMLNS).reverse_each.map { |entry| process_entry(entry, account) }.compact - end - - def process_entry(xml, account) - activity = OStatus::Activity::General.new(xml, account, @options) - activity.specialize&.perform if activity.status? - rescue ActiveRecord::RecordInvalid => e - Rails.logger.debug "Nothing was saved for #{activity.id} because: #{e}" - nil - end -end diff --git a/app/services/process_interaction_service.rb b/app/services/process_interaction_service.rb deleted file mode 100644 index 1fca3832b7..0000000000 --- a/app/services/process_interaction_service.rb +++ /dev/null @@ -1,151 +0,0 @@ -# frozen_string_literal: true - -class ProcessInteractionService < BaseService - include AuthorExtractor - include Authorization - - # Record locally the remote interaction with our user - # @param [String] envelope Salmon envelope - # @param [Account] target_account Account the Salmon was addressed to - def call(envelope, target_account) - body = salmon.unpack(envelope) - - xml = Nokogiri::XML(body) - xml.encoding = 'utf-8' - - account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: OStatus::TagManager::XMLNS)) - - return if account.nil? || account.suspended? - - if salmon.verify(envelope, account.keypair) - RemoteProfileUpdateWorker.perform_async(account.id, body.force_encoding('UTF-8'), true) - - case verb(xml) - when :follow - follow!(account, target_account) unless target_account.locked? || target_account.blocking?(account) || target_account.domain_blocking?(account.domain) - when :request_friend - follow_request!(account, target_account) unless !target_account.locked? || target_account.blocking?(account) || target_account.domain_blocking?(account.domain) - when :authorize - authorize_follow_request!(account, target_account) - when :reject - reject_follow_request!(account, target_account) - when :unfollow - unfollow!(account, target_account) - when :favorite - favourite!(xml, account) - when :unfavorite - unfavourite!(xml, account) - when :post - add_post!(body, account) if mentions_account?(xml, target_account) - when :share - add_post!(body, account) unless status(xml).nil? - when :delete - delete_post!(xml, account) - when :block - reflect_block!(account, target_account) - when :unblock - reflect_unblock!(account, target_account) - end - end - rescue HTTP::Error, OStatus2::BadSalmonError, Mastodon::NotPermittedError - nil - end - - private - - def mentions_account?(xml, account) - xml.xpath('/xmlns:entry/xmlns:link[@rel="mentioned"]', xmlns: OStatus::TagManager::XMLNS).each { |mention_link| return true if [OStatus::TagManager.instance.uri_for(account), OStatus::TagManager.instance.url_for(account)].include?(mention_link.attribute('href').value) } - false - end - - def verb(xml) - raw = xml.at_xpath('//activity:verb', activity: OStatus::TagManager::AS_XMLNS).content - OStatus::TagManager::VERBS.key(raw) - rescue - :post - end - - def follow!(account, target_account) - follow = account.follow!(target_account) - FollowRequest.find_by(account: account, target_account: target_account)&.destroy - NotifyService.new.call(target_account, follow) - end - - def follow_request!(account, target_account) - return if account.requested?(target_account) - - follow_request = FollowRequest.create!(account: account, target_account: target_account) - NotifyService.new.call(target_account, follow_request) - end - - def authorize_follow_request!(account, target_account) - follow_request = FollowRequest.find_by(account: target_account, target_account: account) - follow_request&.authorize! - Pubsubhubbub::SubscribeWorker.perform_async(account.id) unless account.subscribed? - end - - def reject_follow_request!(account, target_account) - follow_request = FollowRequest.find_by(account: target_account, target_account: account) - follow_request&.reject! - end - - def unfollow!(account, target_account) - account.unfollow!(target_account) - FollowRequest.find_by(account: account, target_account: target_account)&.destroy - end - - def reflect_block!(account, target_account) - UnfollowService.new.call(target_account, account) if target_account.following?(account) - account.block!(target_account) - end - - def reflect_unblock!(account, target_account) - UnblockService.new.call(account, target_account) - end - - def delete_post!(xml, account) - status = Status.find(xml.at_xpath('//xmlns:id', xmlns: OStatus::TagManager::XMLNS).content) - - return if status.nil? - - authorize_with account, status, :destroy? - - RemovalWorker.perform_async(status.id) - end - - def favourite!(xml, from_account) - current_status = status(xml) - - return if current_status.nil? - - favourite = current_status.favourites.where(account: from_account).first_or_create!(account: from_account) - NotifyService.new.call(current_status.account, favourite) - end - - def unfavourite!(xml, from_account) - current_status = status(xml) - - return if current_status.nil? - - favourite = current_status.favourites.where(account: from_account).first - favourite&.destroy - end - - def add_post!(body, account) - ProcessingWorker.perform_async(account.id, body.force_encoding('UTF-8')) - end - - def status(xml) - uri = activity_id(xml) - return nil unless OStatus::TagManager.instance.local_id?(uri) - Status.find(OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Status')) - end - - def activity_id(xml) - xml.at_xpath('//activity:object', activity: OStatus::TagManager::AS_XMLNS).at_xpath('./xmlns:id', xmlns: OStatus::TagManager::XMLNS).content - end - - def salmon - @salmon ||= OStatus2::Salmon.new - end -end diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index bc607dff39..da52bff6a5 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ProcessMentionsService < BaseService - include StreamEntryRenderer include Payloadable # Scan status for mentions and fetch remote mentioned users, create @@ -49,17 +48,11 @@ class ProcessMentionsService < BaseService if mentioned_account.local? LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name) - elsif mentioned_account.ostatus? && !@status.stream_entry.hidden? - NotificationWorker.perform_async(ostatus_xml, @status.account_id, mentioned_account.id) elsif mentioned_account.activitypub? ActivityPub::DeliveryWorker.perform_async(activitypub_json, mention.status.account_id, mentioned_account.inbox_url) end end - def ostatus_xml - @ostatus_xml ||= stream_entry_to_xml(@status.stream_entry) - end - def activitypub_json return @activitypub_json if defined?(@activitypub_json) @activitypub_json = Oj.dump(serialize_payload(@status, ActivityPub::ActivitySerializer, signer: @status.account)) diff --git a/app/services/pubsubhubbub/subscribe_service.rb b/app/services/pubsubhubbub/subscribe_service.rb deleted file mode 100644 index 550da63281..0000000000 --- a/app/services/pubsubhubbub/subscribe_service.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -class Pubsubhubbub::SubscribeService < BaseService - URL_PATTERN = /\A#{URI.regexp(%w(http https))}\z/ - - attr_reader :account, :callback, :secret, - :lease_seconds, :domain - - def call(account, callback, secret, lease_seconds, verified_domain = nil) - @account = account - @callback = Addressable::URI.parse(callback).normalize.to_s - @secret = secret - @lease_seconds = lease_seconds - @domain = verified_domain - - process_subscribe - end - - private - - def process_subscribe - if account.nil? - ['Invalid topic URL', 422] - elsif !valid_callback? - ['Invalid callback URL', 422] - elsif blocked_domain? - ['Callback URL not allowed', 403] - else - confirm_subscription - ['', 202] - end - end - - def confirm_subscription - subscription = locate_subscription - Pubsubhubbub::ConfirmationWorker.perform_async(subscription.id, 'subscribe', secret, lease_seconds) - end - - def valid_callback? - callback.present? && callback =~ URL_PATTERN - end - - def blocked_domain? - DomainBlock.blocked? Addressable::URI.parse(callback).host - end - - def locate_subscription - subscription = Subscription.find_or_initialize_by(account: account, callback_url: callback) - subscription.domain = domain - subscription.save! - subscription - end -end diff --git a/app/services/pubsubhubbub/unsubscribe_service.rb b/app/services/pubsubhubbub/unsubscribe_service.rb deleted file mode 100644 index 646150f7bb..0000000000 --- a/app/services/pubsubhubbub/unsubscribe_service.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -class Pubsubhubbub::UnsubscribeService < BaseService - attr_reader :account, :callback - - def call(account, callback) - @account = account - @callback = Addressable::URI.parse(callback).normalize.to_s - - process_unsubscribe - end - - private - - def process_unsubscribe - if account.nil? - ['Invalid topic URL', 422] - else - confirm_unsubscribe unless subscription.nil? - ['', 202] - end - end - - def confirm_unsubscribe - Pubsubhubbub::ConfirmationWorker.perform_async(subscription.id, 'unsubscribe') - end - - def subscription - @_subscription ||= Subscription.find_by(account: account, callback_url: callback) - end -end diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index 9cf4bc128b..3bb460fcaf 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -2,7 +2,6 @@ class ReblogService < BaseService include Authorization - include StreamEntryRenderer include Payloadable # Reblog a status and notify its remote author @@ -24,7 +23,6 @@ class ReblogService < BaseService reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility) DistributionWorker.perform_async(reblog.id) - Pubsubhubbub::DistributionWorker.perform_async(reblog.stream_entry.id) ActivityPub::DistributionWorker.perform_async(reblog.id) create_notification(reblog) @@ -40,8 +38,6 @@ class ReblogService < BaseService if reblogged_status.account.local? LocalNotificationWorker.perform_async(reblogged_status.account_id, reblog.id, reblog.class.name) - elsif reblogged_status.account.ostatus? - NotificationWorker.perform_async(stream_entry_to_xml(reblog.stream_entry), reblog.account_id, reblogged_status.account_id) elsif reblogged_status.account.activitypub? && !reblogged_status.account.following?(reblog.account) ActivityPub::DeliveryWorker.perform_async(build_json(reblog), reblog.account_id, reblogged_status.account.inbox_url) end diff --git a/app/services/reject_follow_service.rb b/app/services/reject_follow_service.rb index f87d0ba914..bc0000c8c8 100644 --- a/app/services/reject_follow_service.rb +++ b/app/services/reject_follow_service.rb @@ -6,25 +6,17 @@ class RejectFollowService < BaseService def call(source_account, target_account) follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account) follow_request.reject! - create_notification(follow_request) unless source_account.local? + create_notification(follow_request) if !source_account.local? && source_account.activitypub? follow_request end private def create_notification(follow_request) - if follow_request.account.ostatus? - NotificationWorker.perform_async(build_xml(follow_request), follow_request.target_account_id, follow_request.account_id) - elsif follow_request.account.activitypub? - ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url) - end + ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), follow_request.target_account_id, follow_request.account.inbox_url) end def build_json(follow_request) Oj.dump(serialize_payload(follow_request, ActivityPub::RejectFollowSerializer)) end - - def build_xml(follow_request) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request)) - end end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 81adc5aae5..a8c9100b33 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class RemoveStatusService < BaseService - include StreamEntryRenderer include Redisable include Payloadable @@ -78,11 +77,6 @@ class RemoveStatusService < BaseService target_accounts << @status.reblog.account if @status.reblog? && !@status.reblog.account.local? target_accounts.uniq!(&:id) - # Ostatus - NotificationWorker.push_bulk(target_accounts.select(&:ostatus?).uniq(&:domain)) do |target_account| - [salmon_xml, @account.id, target_account.id] - end - # ActivityPub ActivityPub::DeliveryWorker.push_bulk(target_accounts.select(&:activitypub?).uniq(&:preferred_inbox_url)) do |target_account| [signed_activity_json, @account.id, target_account.preferred_inbox_url] @@ -90,9 +84,6 @@ class RemoveStatusService < BaseService end def remove_from_remote_followers - # OStatus - Pubsubhubbub::RawDistributionWorker.perform_async(salmon_xml, @account.id) - # ActivityPub ActivityPub::DeliveryWorker.push_bulk(@account.followers.inboxes) do |inbox_url| [signed_activity_json, @account.id, inbox_url] @@ -111,10 +102,6 @@ class RemoveStatusService < BaseService end end - def salmon_xml - @salmon_xml ||= stream_entry_to_xml(@stream_entry) - end - def signed_activity_json @signed_activity_json ||= Oj.dump(serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer, signer: @account)) end diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb index e557706da5..0ea31a0d85 100644 --- a/app/services/resolve_account_service.rb +++ b/app/services/resolve_account_service.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true -class ResolveAccountService < BaseService - include OStatus2::MagicKey - include JsonLdHelper +require_relative '../models/account' - DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0' +class ResolveAccountService < BaseService + include JsonLdHelper # Find or create a local account for a remote user. # When creating, look up the user's webfinger and fetch all @@ -48,18 +47,16 @@ class ResolveAccountService < BaseService return end - return if links_missing? || auto_suspend? return Account.find_local(@username) if TagManager.instance.local_domain?(@domain) + return unless activitypub_ready? RedisLock.acquire(lock_options) do |lock| if lock.acquired? @account = Account.find_remote(@username, @domain) - if activitypub_ready? || @account&.activitypub? - handle_activitypub - else - handle_ostatus - end + next unless @account.nil? || @account.activitypub? + + handle_activitypub else raise Mastodon::RaceConditionError end @@ -73,38 +70,12 @@ class ResolveAccountService < BaseService private - def links_missing? - !(activitypub_ready? || ostatus_ready?) - end - - def ostatus_ready? - !(@webfinger.link('http://schemas.google.com/g/2010#updates-from').nil? || - @webfinger.link('salmon').nil? || - @webfinger.link('http://webfinger.net/rel/profile-page').nil? || - @webfinger.link('magic-public-key').nil? || - canonical_uri.nil? || - hub_url.nil?) - end - def webfinger_update_due? @account.nil? || ((!@options[:skip_webfinger] || @account.ostatus?) && @account.possibly_stale?) end def activitypub_ready? - !@webfinger.link('self').nil? && - ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self').type) && - !actor_json.nil? && - actor_json['inbox'].present? - end - - def handle_ostatus - create_account if @account.nil? - update_account - update_account_profile if update_profile? - end - - def update_profile? - @options[:update_profile] + !@webfinger.link('self').nil? && ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self').type) end def handle_activitypub @@ -115,89 +86,10 @@ class ResolveAccountService < BaseService nil end - def create_account - Rails.logger.debug "Creating new remote account for #{@username}@#{@domain}" - - @account = Account.new(username: @username, domain: @domain) - @account.suspended_at = domain_block.created_at if auto_suspend? - @account.silenced_at = domain_block.created_at if auto_silence? - @account.private_key = nil - end - - def update_account - @account.last_webfingered_at = Time.now.utc - @account.protocol = :ostatus - @account.remote_url = atom_url - @account.salmon_url = salmon_url - @account.url = url - @account.public_key = public_key - @account.uri = canonical_uri - @account.hub_url = hub_url - @account.save! - end - - def auto_suspend? - domain_block&.suspend? - end - - def auto_silence? - domain_block&.silence? - end - - def domain_block - return @domain_block if defined?(@domain_block) - @domain_block = DomainBlock.rule_for(@domain) - end - - def atom_url - @atom_url ||= @webfinger.link('http://schemas.google.com/g/2010#updates-from').href - end - - def salmon_url - @salmon_url ||= @webfinger.link('salmon').href - end - def actor_url @actor_url ||= @webfinger.link('self').href end - def url - @url ||= @webfinger.link('http://webfinger.net/rel/profile-page').href - end - - def public_key - @public_key ||= magic_key_to_pem(@webfinger.link('magic-public-key').href) - end - - def canonical_uri - return @canonical_uri if defined?(@canonical_uri) - - author_uri = atom.at_xpath('/xmlns:feed/xmlns:author/xmlns:uri') - - if author_uri.nil? - owner = atom.at_xpath('/xmlns:feed').at_xpath('./dfrn:owner', dfrn: DFRN_NS) - author_uri = owner.at_xpath('./xmlns:uri') unless owner.nil? - end - - @canonical_uri = author_uri.nil? ? nil : author_uri.content - end - - def hub_url - return @hub_url if defined?(@hub_url) - - hubs = atom.xpath('//xmlns:link[@rel="hub"]') - @hub_url = hubs.empty? || hubs.first['href'].nil? ? nil : hubs.first['href'] - end - - def atom_body - return @atom_body if defined?(@atom_body) - - @atom_body = Request.new(:get, atom_url).perform do |response| - raise Mastodon::UnexpectedResponseError, response unless response.code == 200 - response.body_with_limit - end - end - def actor_json return @actor_json if defined?(@actor_json) @@ -205,15 +97,6 @@ class ResolveAccountService < BaseService @actor_json = supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) ? json : nil end - def atom - return @atom if defined?(@atom) - @atom = Nokogiri::XML(atom_body) - end - - def update_account_profile - RemoteProfileUpdateWorker.perform_async(@account.id, atom_body.force_encoding('UTF-8'), false) - end - def lock_options { redis: Redis.current, key: "resolve:#{@username}@#{@domain}" } end diff --git a/app/services/send_interaction_service.rb b/app/services/send_interaction_service.rb deleted file mode 100644 index 3419043e56..0000000000 --- a/app/services/send_interaction_service.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -class SendInteractionService < BaseService - # Send an Atom representation of an interaction to a remote Salmon endpoint - # @param [String] Entry XML - # @param [Account] source_account - # @param [Account] target_account - def call(xml, source_account, target_account) - @xml = xml - @source_account = source_account - @target_account = target_account - - return if !target_account.ostatus? || block_notification? - - build_request.perform do |delivery| - raise Mastodon::UnexpectedResponseError, delivery unless delivery.code > 199 && delivery.code < 300 - end - end - - private - - def build_request - request = Request.new(:post, @target_account.salmon_url, body: envelope) - request.add_headers('Content-Type' => 'application/magic-envelope+xml') - request - end - - def envelope - salmon.pack(@xml, @source_account.keypair) - end - - def block_notification? - DomainBlock.blocked?(@target_account.domain) - end - - def salmon - @salmon ||= OStatus2::Salmon.new - end -end diff --git a/app/services/subscribe_service.rb b/app/services/subscribe_service.rb deleted file mode 100644 index 83fd64396a..0000000000 --- a/app/services/subscribe_service.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -class SubscribeService < BaseService - def call(account) - return if account.hub_url.blank? - - @account = account - @account.secret = SecureRandom.hex - - build_request.perform do |response| - if response_failed_permanently? response - # We're not allowed to subscribe. Fail and move on. - @account.secret = '' - @account.save! - elsif response_successful? response - # The subscription will be confirmed asynchronously. - @account.save! - else - # The response was either a 429 rate limit, or a 5xx error. - # We need to retry at a later time. Fail loudly! - raise Mastodon::UnexpectedResponseError, response - end - end - end - - private - - def build_request - request = Request.new(:post, @account.hub_url, form: subscription_params) - request.on_behalf_of(some_local_account) if some_local_account - request - end - - def subscription_params - { - 'hub.topic': @account.remote_url, - 'hub.mode': 'subscribe', - 'hub.callback': api_subscription_url(@account.id), - 'hub.verify': 'async', - 'hub.secret': @account.secret, - 'hub.lease_seconds': 7.days.seconds, - } - end - - def some_local_account - @some_local_account ||= Account.local.without_suspended.first - end - - # Any response in the 3xx or 4xx range, except for 429 (rate limit) - def response_failed_permanently?(response) - (response.status.redirect? || response.status.client_error?) && !response.status.too_many_requests? - end - - # Any response in the 2xx range - def response_successful?(response) - response.status.success? - end -end diff --git a/app/services/unblock_service.rb b/app/services/unblock_service.rb index 95a858e9f5..c263ac8afe 100644 --- a/app/services/unblock_service.rb +++ b/app/services/unblock_service.rb @@ -7,25 +7,17 @@ class UnblockService < BaseService return unless account.blocking?(target_account) unblock = account.unblock!(target_account) - create_notification(unblock) unless target_account.local? + create_notification(unblock) if !target_account.local? && target_account.activitypub? unblock end private def create_notification(unblock) - if unblock.target_account.ostatus? - NotificationWorker.perform_async(build_xml(unblock), unblock.account_id, unblock.target_account_id) - elsif unblock.target_account.activitypub? - ActivityPub::DeliveryWorker.perform_async(build_json(unblock), unblock.account_id, unblock.target_account.inbox_url) - end + ActivityPub::DeliveryWorker.perform_async(build_json(unblock), unblock.account_id, unblock.target_account.inbox_url) end def build_json(unblock) Oj.dump(serialize_payload(unblock, ActivityPub::UndoBlockSerializer)) end - - def build_xml(block) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unblock_salmon(block)) - end end diff --git a/app/services/unfavourite_service.rb b/app/services/unfavourite_service.rb index dcc890b7de..37917a64f1 100644 --- a/app/services/unfavourite_service.rb +++ b/app/services/unfavourite_service.rb @@ -6,7 +6,7 @@ class UnfavouriteService < BaseService def call(account, status) favourite = Favourite.find_by!(account: account, status: status) favourite.destroy! - create_notification(favourite) unless status.local? + create_notification(favourite) if !status.account.local? && status.account.activitypub? favourite end @@ -14,19 +14,10 @@ class UnfavouriteService < BaseService def create_notification(favourite) status = favourite.status - - if status.account.ostatus? - NotificationWorker.perform_async(build_xml(favourite), favourite.account_id, status.account_id) - elsif status.account.activitypub? - ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url) - end + ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url) end def build_json(favourite) Oj.dump(serialize_payload(favourite, ActivityPub::UndoLikeSerializer)) end - - def build_xml(favourite) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfavourite_salmon(favourite)) - end end diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb index 17dc29735f..b7033d7ebe 100644 --- a/app/services/unfollow_service.rb +++ b/app/services/unfollow_service.rb @@ -21,8 +21,8 @@ class UnfollowService < BaseService return unless follow follow.destroy! - create_notification(follow) unless @target_account.local? - create_reject_notification(follow) if @target_account.local? && !@source_account.local? + create_notification(follow) if !@target_account.local? && @target_account.activitypub? + create_reject_notification(follow) if @target_account.local? && !@source_account.local? && @source_account.activitypub? UnmergeWorker.perform_async(@target_account.id, @source_account.id) follow end @@ -38,16 +38,10 @@ class UnfollowService < BaseService end def create_notification(follow) - if follow.target_account.ostatus? - NotificationWorker.perform_async(build_xml(follow), follow.account_id, follow.target_account_id) - elsif follow.target_account.activitypub? - ActivityPub::DeliveryWorker.perform_async(build_json(follow), follow.account_id, follow.target_account.inbox_url) - end + ActivityPub::DeliveryWorker.perform_async(build_json(follow), follow.account_id, follow.target_account.inbox_url) end def create_reject_notification(follow) - # Rejecting an already-existing follow request - return unless follow.account.activitypub? ActivityPub::DeliveryWorker.perform_async(build_reject_json(follow), follow.target_account_id, follow.account.inbox_url) end @@ -58,8 +52,4 @@ class UnfollowService < BaseService def build_reject_json(follow) Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)) end - - def build_xml(follow) - OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.unfollow_salmon(follow)) - end end diff --git a/app/services/unsubscribe_service.rb b/app/services/unsubscribe_service.rb deleted file mode 100644 index 95c1fb4fc0..0000000000 --- a/app/services/unsubscribe_service.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -class UnsubscribeService < BaseService - def call(account) - return if account.hub_url.blank? - - @account = account - - begin - build_request.perform do |response| - Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{response.status}" unless response.status.success? - end - rescue HTTP::Error, OpenSSL::SSL::SSLError => e - Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{e}" - end - - @account.secret = '' - @account.subscription_expires_at = nil - @account.save! - end - - private - - def build_request - Request.new(:post, @account.hub_url, form: subscription_params) - end - - def subscription_params - { - 'hub.topic': @account.remote_url, - 'hub.mode': 'unsubscribe', - 'hub.callback': api_subscription_url(@account.id), - 'hub.verify': 'async', - } - end -end diff --git a/app/services/update_remote_profile_service.rb b/app/services/update_remote_profile_service.rb deleted file mode 100644 index 403395a0dd..0000000000 --- a/app/services/update_remote_profile_service.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -class UpdateRemoteProfileService < BaseService - attr_reader :account, :remote_profile - - def call(body, account, resubscribe = false) - @account = account - @remote_profile = RemoteProfile.new(body) - - return if remote_profile.root.nil? - - update_account unless remote_profile.author.nil? - - old_hub_url = account.hub_url - account.hub_url = remote_profile.hub_link if remote_profile.hub_link.present? && remote_profile.hub_link != old_hub_url - - account.save_with_optional_media! - - Pubsubhubbub::SubscribeWorker.perform_async(account.id) if resubscribe && account.hub_url != old_hub_url - end - - private - - def update_account - account.display_name = remote_profile.display_name || '' - account.note = remote_profile.note || '' - account.locked = remote_profile.locked? - - if !account.suspended? && !DomainBlock.reject_media?(account.domain) - if remote_profile.avatar.present? - account.avatar_remote_url = remote_profile.avatar - else - account.avatar_remote_url = '' - account.avatar.destroy - end - - if remote_profile.header.present? - account.header_remote_url = remote_profile.header - else - account.header_remote_url = '' - account.header.destroy - end - - save_emojis if remote_profile.emojis.present? - end - end - - def save_emojis - do_not_download = DomainBlock.reject_media?(account.domain) - - return if do_not_download - - remote_profile.emojis.each do |link| - next unless link['href'] && link['name'] - - shortcode = link['name'].delete(':') - emoji = CustomEmoji.find_by(shortcode: shortcode, domain: account.domain) - - next unless emoji.nil? - - emoji = CustomEmoji.new(shortcode: shortcode, domain: account.domain) - emoji.image_remote_url = link['href'] - emoji.save - end - end -end diff --git a/app/services/verify_salmon_service.rb b/app/services/verify_salmon_service.rb deleted file mode 100644 index 205b35d8b1..0000000000 --- a/app/services/verify_salmon_service.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -class VerifySalmonService < BaseService - include AuthorExtractor - - def call(payload) - body = salmon.unpack(payload) - - xml = Nokogiri::XML(body) - xml.encoding = 'utf-8' - - account = author_from_xml(xml.at_xpath('/xmlns:entry', xmlns: OStatus::TagManager::XMLNS)) - - if account.nil? - false - else - salmon.verify(payload, account.keypair) - end - end - - private - - def salmon - @salmon ||= OStatus2::Salmon.new - end -end diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index 950e618477..de7d2a8ba3 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -7,7 +7,6 @@ - if @account.user&.setting_noindex %meta{ name: 'robots', content: 'noindex' }/ - %link{ rel: 'salmon', href: api_salmon_url(@account.id) }/ %link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/ %link{ rel: 'alternate', type: 'application/rss+xml', href: account_url(@account, format: 'rss') }/ %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/ diff --git a/app/views/admin/subscriptions/_subscription.html.haml b/app/views/admin/subscriptions/_subscription.html.haml deleted file mode 100644 index 1dec8e3962..0000000000 --- a/app/views/admin/subscriptions/_subscription.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -%tr - %td - %samp= subscription.account.acct - %td - %samp= subscription.callback_url - %td - - if subscription.confirmed? - %i.fa.fa-check - %td{ style: "color: #{subscription.expired? ? 'red' : 'inherit'};" } - %time.time-ago{ datetime: subscription.expires_at.iso8601, title: l(subscription.expires_at) } - = precede subscription.expired? ? '-' : '' do - = time_ago_in_words(subscription.expires_at) - %td - - if subscription.last_successful_delivery_at? - %time.formatted{ datetime: subscription.last_successful_delivery_at.iso8601, title: l(subscription.last_successful_delivery_at) } - = l subscription.last_successful_delivery_at - - else - %i.fa.fa-times diff --git a/app/views/admin/subscriptions/index.html.haml b/app/views/admin/subscriptions/index.html.haml deleted file mode 100644 index 83704c8ee5..0000000000 --- a/app/views/admin/subscriptions/index.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -- content_for :page_title do - = t('admin.subscriptions.title') - -.table-wrapper - %table.table - %thead - %tr - %th= t('admin.subscriptions.topic') - %th= t('admin.subscriptions.callback_url') - %th= t('admin.subscriptions.confirmed') - %th= t('admin.subscriptions.expires_in') - %th= t('admin.subscriptions.last_delivery') - %tbody - = render @subscriptions - -= paginate @subscriptions diff --git a/app/views/well_known/webfinger/show.xml.ruby b/app/views/well_known/webfinger/show.xml.ruby index 968c8c1380..c82cdb7b3d 100644 --- a/app/views/well_known/webfinger/show.xml.ruby +++ b/app/views/well_known/webfinger/show.xml.ruby @@ -25,11 +25,6 @@ doc << Ox::Element.new('XRD').tap do |xrd| link['href'] = account_url(@account) end - xrd << Ox::Element.new('Link').tap do |link| - link['rel'] = 'salmon' - link['href'] = api_salmon_url(@account.id) - end - xrd << Ox::Element.new('Link').tap do |link| link['rel'] = 'magic-public-key' link['href'] = "data:application/magic-public-key,#{@account.magic_key}" diff --git a/app/workers/after_remote_follow_request_worker.rb b/app/workers/after_remote_follow_request_worker.rb index 84eb6ade23..ce9c65834c 100644 --- a/app/workers/after_remote_follow_request_worker.rb +++ b/app/workers/after_remote_follow_request_worker.rb @@ -5,27 +5,5 @@ class AfterRemoteFollowRequestWorker sidekiq_options queue: 'pull', retry: 5 - attr_reader :follow_request - - def perform(follow_request_id) - @follow_request = FollowRequest.find(follow_request_id) - process_follow_service if processing_required? - rescue ActiveRecord::RecordNotFound - true - end - - private - - def process_follow_service - follow_request.destroy - FollowService.new.call(follow_request.account, updated_account.acct) - end - - def processing_required? - !updated_account.nil? && !updated_account.locked? - end - - def updated_account - @_updated_account ||= FetchRemoteAccountService.new.call(follow_request.target_account.remote_url) - end + def perform(follow_request_id); end end diff --git a/app/workers/after_remote_follow_worker.rb b/app/workers/after_remote_follow_worker.rb index edab83f853..d9719f2bf8 100644 --- a/app/workers/after_remote_follow_worker.rb +++ b/app/workers/after_remote_follow_worker.rb @@ -5,27 +5,5 @@ class AfterRemoteFollowWorker sidekiq_options queue: 'pull', retry: 5 - attr_reader :follow - - def perform(follow_id) - @follow = Follow.find(follow_id) - process_follow_service if processing_required? - rescue ActiveRecord::RecordNotFound - true - end - - private - - def process_follow_service - follow.destroy - FollowService.new.call(follow.account, updated_account.acct) - end - - def updated_account - @_updated_account ||= FetchRemoteAccountService.new.call(follow.target_account.remote_url) - end - - def processing_required? - !updated_account.nil? && updated_account.locked? - end + def perform(follow_id); end end diff --git a/app/workers/notification_worker.rb b/app/workers/notification_worker.rb index da1d6ab455..1c0f001cf2 100644 --- a/app/workers/notification_worker.rb +++ b/app/workers/notification_worker.rb @@ -5,7 +5,5 @@ class NotificationWorker sidekiq_options queue: 'push', retry: 5 - def perform(xml, source_account_id, target_account_id) - SendInteractionService.new.call(xml, Account.find(source_account_id), Account.find(target_account_id)) - end + def perform(xml, source_account_id, target_account_id); end end diff --git a/app/workers/processing_worker.rb b/app/workers/processing_worker.rb index 978c3aba26..cf3bd83979 100644 --- a/app/workers/processing_worker.rb +++ b/app/workers/processing_worker.rb @@ -5,7 +5,5 @@ class ProcessingWorker sidekiq_options backtrace: true - def perform(account_id, body) - ProcessFeedService.new.call(body, Account.find(account_id), override_timestamps: true) - end + def perform(account_id, body); end end diff --git a/app/workers/pubsubhubbub/confirmation_worker.rb b/app/workers/pubsubhubbub/confirmation_worker.rb index c0e7b677e4..783a8c95fb 100644 --- a/app/workers/pubsubhubbub/confirmation_worker.rb +++ b/app/workers/pubsubhubbub/confirmation_worker.rb @@ -2,81 +2,8 @@ class Pubsubhubbub::ConfirmationWorker include Sidekiq::Worker - include RoutingHelper sidekiq_options queue: 'push', retry: false - attr_reader :subscription, :mode, :secret, :lease_seconds - - def perform(subscription_id, mode, secret = nil, lease_seconds = nil) - @subscription = Subscription.find(subscription_id) - @mode = mode - @secret = secret - @lease_seconds = lease_seconds - process_confirmation - end - - private - - def process_confirmation - prepare_subscription - - callback_get_with_params - logger.debug "Confirming PuSH subscription for #{subscription.callback_url} with challenge #{challenge}: #{@callback_response_body}" - - update_subscription - end - - def update_subscription - if successful_subscribe? - subscription.save! - elsif successful_unsubscribe? - subscription.destroy! - end - end - - def successful_subscribe? - subscribing? && response_matches_challenge? - end - - def successful_unsubscribe? - (unsubscribing? && response_matches_challenge?) || !subscription.confirmed? - end - - def response_matches_challenge? - @callback_response_body == challenge - end - - def subscribing? - mode == 'subscribe' - end - - def unsubscribing? - mode == 'unsubscribe' - end - - def callback_get_with_params - Request.new(:get, subscription.callback_url, params: callback_params).perform do |response| - @callback_response_body = response.body_with_limit - end - end - - def callback_params - { - 'hub.topic': account_url(subscription.account, format: :atom), - 'hub.mode': mode, - 'hub.challenge': challenge, - 'hub.lease_seconds': subscription.lease_seconds, - } - end - - def prepare_subscription - subscription.secret = secret - subscription.lease_seconds = lease_seconds - subscription.confirmed = true - end - - def challenge - @_challenge ||= SecureRandom.hex - end + def perform(subscription_id, mode, secret = nil, lease_seconds = nil); end end diff --git a/app/workers/pubsubhubbub/delivery_worker.rb b/app/workers/pubsubhubbub/delivery_worker.rb index 619bfa48aa..1260060bd5 100644 --- a/app/workers/pubsubhubbub/delivery_worker.rb +++ b/app/workers/pubsubhubbub/delivery_worker.rb @@ -2,80 +2,8 @@ class Pubsubhubbub::DeliveryWorker include Sidekiq::Worker - include RoutingHelper sidekiq_options queue: 'push', retry: 3, dead: false - sidekiq_retry_in do |count| - 5 * (count + 1) - end - - attr_reader :subscription, :payload - - def perform(subscription_id, payload) - @subscription = Subscription.find(subscription_id) - @payload = payload - process_delivery unless blocked_domain? - rescue => e - raise e.class, "Delivery failed for #{subscription&.callback_url}: #{e.message}", e.backtrace[0] - end - - private - - def process_delivery - callback_post_payload do |payload_delivery| - raise Mastodon::UnexpectedResponseError, payload_delivery unless response_successful? payload_delivery - end - - subscription.touch(:last_successful_delivery_at) - end - - def callback_post_payload(&block) - request = Request.new(:post, subscription.callback_url, body: payload) - request.add_headers(headers) - request.perform(&block) - end - - def blocked_domain? - DomainBlock.blocked?(host) - end - - def host - Addressable::URI.parse(subscription.callback_url).normalized_host - end - - def headers - { - 'Content-Type' => 'application/atom+xml', - 'Link' => link_header, - }.merge(signature_headers.to_h) - end - - def link_header - LinkHeader.new([hub_link_header, self_link_header]).to_s - end - - def hub_link_header - [api_push_url, [%w(rel hub)]] - end - - def self_link_header - [account_url(subscription.account, format: :atom), [%w(rel self)]] - end - - def signature_headers - { 'X-Hub-Signature' => payload_signature } if subscription.secret? - end - - def payload_signature - "sha1=#{hmac_payload_digest}" - end - - def hmac_payload_digest - OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), subscription.secret, payload) - end - - def response_successful?(payload_delivery) - payload_delivery.code > 199 && payload_delivery.code < 300 - end + def perform(subscription_id, payload); end end diff --git a/app/workers/pubsubhubbub/distribution_worker.rb b/app/workers/pubsubhubbub/distribution_worker.rb index fed5e917d3..75bac5d6fa 100644 --- a/app/workers/pubsubhubbub/distribution_worker.rb +++ b/app/workers/pubsubhubbub/distribution_worker.rb @@ -5,28 +5,5 @@ class Pubsubhubbub::DistributionWorker sidekiq_options queue: 'push' - def perform(stream_entry_ids) - stream_entries = StreamEntry.where(id: stream_entry_ids).includes(:status).reject { |e| e.status.nil? || e.status.hidden? } - - return if stream_entries.empty? - - @account = stream_entries.first.account - @subscriptions = active_subscriptions.to_a - - distribute_public!(stream_entries) - end - - private - - def distribute_public!(stream_entries) - @payload = OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, stream_entries)) - - Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions) do |subscription_id| - [subscription_id, @payload] - end - end - - def active_subscriptions - Subscription.where(account: @account).active.pluck(:id) - end + def perform(stream_entry_ids); end end diff --git a/app/workers/pubsubhubbub/raw_distribution_worker.rb b/app/workers/pubsubhubbub/raw_distribution_worker.rb index 16962a623c..ece9c80ac5 100644 --- a/app/workers/pubsubhubbub/raw_distribution_worker.rb +++ b/app/workers/pubsubhubbub/raw_distribution_worker.rb @@ -5,18 +5,5 @@ class Pubsubhubbub::RawDistributionWorker sidekiq_options queue: 'push' - def perform(xml, source_account_id) - @account = Account.find(source_account_id) - @subscriptions = active_subscriptions.to_a - - Pubsubhubbub::DeliveryWorker.push_bulk(@subscriptions) do |subscription| - [subscription.id, xml] - end - end - - private - - def active_subscriptions - Subscription.where(account: @account).active.select('id, callback_url, domain') - end + def perform(xml, source_account_id); end end diff --git a/app/workers/pubsubhubbub/subscribe_worker.rb b/app/workers/pubsubhubbub/subscribe_worker.rb index 2e176d1c10..b861b5e67a 100644 --- a/app/workers/pubsubhubbub/subscribe_worker.rb +++ b/app/workers/pubsubhubbub/subscribe_worker.rb @@ -5,30 +5,5 @@ class Pubsubhubbub::SubscribeWorker sidekiq_options queue: 'push', retry: 10, unique: :until_executed, dead: false - sidekiq_retry_in do |count| - case count - when 0 - 30.minutes.seconds - when 1 - 2.hours.seconds - when 2 - 12.hours.seconds - else - 24.hours.seconds * (count - 2) - end - end - - sidekiq_retries_exhausted do |msg, _e| - account = Account.find(msg['args'].first) - Sidekiq.logger.error "PuSH subscription attempts for #{account.acct} exhausted. Unsubscribing" - ::UnsubscribeService.new.call(account) - end - - def perform(account_id) - account = Account.find(account_id) - logger.debug "PuSH re-subscribing to #{account.acct}" - ::SubscribeService.new.call(account) - rescue => e - raise e.class, "Subscribe failed for #{account&.acct}: #{e.message}", e.backtrace[0] - end + def perform(account_id); end end diff --git a/app/workers/pubsubhubbub/unsubscribe_worker.rb b/app/workers/pubsubhubbub/unsubscribe_worker.rb index a271715b7a..0c1c263f6e 100644 --- a/app/workers/pubsubhubbub/unsubscribe_worker.rb +++ b/app/workers/pubsubhubbub/unsubscribe_worker.rb @@ -5,11 +5,5 @@ class Pubsubhubbub::UnsubscribeWorker sidekiq_options queue: 'push', retry: false, unique: :until_executed, dead: false - def perform(account_id) - account = Account.find(account_id) - logger.debug "PuSH unsubscribing from #{account.acct}" - ::UnsubscribeService.new.call(account) - rescue ActiveRecord::RecordNotFound - true - end + def perform(account_id); end end diff --git a/app/workers/remote_profile_update_worker.rb b/app/workers/remote_profile_update_worker.rb index 03585ad2d6..01e8daf8f2 100644 --- a/app/workers/remote_profile_update_worker.rb +++ b/app/workers/remote_profile_update_worker.rb @@ -5,9 +5,5 @@ class RemoteProfileUpdateWorker sidekiq_options queue: 'pull' - def perform(account_id, body, resubscribe) - UpdateRemoteProfileService.new.call(body, Account.find(account_id), resubscribe) - rescue ActiveRecord::RecordNotFound - true - end + def perform(account_id, body, resubscribe); end end diff --git a/app/workers/salmon_worker.rb b/app/workers/salmon_worker.rb index d37d404323..10200b06cf 100644 --- a/app/workers/salmon_worker.rb +++ b/app/workers/salmon_worker.rb @@ -5,9 +5,5 @@ class SalmonWorker sidekiq_options backtrace: true - def perform(account_id, body) - ProcessInteractionService.new.call(body, Account.find(account_id)) - rescue Nokogiri::XML::XPath::SyntaxError, ActiveRecord::RecordNotFound - true - end + def perform(account_id, body); end end diff --git a/app/workers/scheduler/subscriptions_scheduler.rb b/app/workers/scheduler/subscriptions_scheduler.rb index d5873bccb0..6903cadc76 100644 --- a/app/workers/scheduler/subscriptions_scheduler.rb +++ b/app/workers/scheduler/subscriptions_scheduler.rb @@ -5,13 +5,5 @@ class Scheduler::SubscriptionsScheduler sidekiq_options unique: :until_executed, retry: 0 - def perform - Pubsubhubbub::SubscribeWorker.push_bulk(expiring_accounts.pluck(:id)) - end - - private - - def expiring_accounts - Account.expiring(1.day.from_now).partitioned - end + def perform; end end diff --git a/config/locales/en.yml b/config/locales/en.yml index d4f1855aaa..611f36fdd1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -469,13 +469,6 @@ en: no_status_selected: No statuses were changed as none were selected title: Account statuses with_media: With media - subscriptions: - callback_url: Callback URL - confirmed: Confirmed - expires_in: Expires in - last_delivery: Last delivery - title: WebSub - topic: Topic tags: accounts: Accounts hidden: Hidden diff --git a/config/navigation.rb b/config/navigation.rb index df10241892..ef845d1fc5 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -48,7 +48,6 @@ SimpleNavigation::Configuration.run do |navigation| s.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url, if: -> { current_user.admin? }, highlights_on: %r{/admin/settings} s.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_url, highlights_on: %r{/admin/custom_emojis} s.item :relays, safe_join([fa_icon('exchange fw'), t('admin.relays.title')]), admin_relays_url, if: -> { current_user.admin? }, highlights_on: %r{/admin/relays} - s.item :subscriptions, safe_join([fa_icon('paper-plane-o fw'), t('admin.subscriptions.title')]), admin_subscriptions_url, if: -> { current_user.admin? } s.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }, if: -> { current_user.admin? } s.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }, if: -> { current_user.admin? } end diff --git a/config/routes.rb b/config/routes.rb index 9ab5ba7f07..4b6d464c6d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -154,7 +154,6 @@ Rails.application.routes.draw do namespace :admin do get '/dashboard', to: 'dashboard#index' - resources :subscriptions, only: [:index] resources :domain_blocks, only: [:new, :create, :show, :destroy] resources :email_domain_blocks, only: [:index, :new, :create, :destroy] resources :action_logs, only: [:index] @@ -191,8 +190,6 @@ Rails.application.routes.draw do resources :accounts, only: [:index, :show] do member do - post :subscribe - post :unsubscribe post :enable post :unsilence post :unsuspend @@ -257,16 +254,6 @@ Rails.application.routes.draw do get '/admin', to: redirect('/admin/dashboard', status: 302) namespace :api do - # PubSubHubbub outgoing subscriptions - resources :subscriptions, only: [:show] - post '/subscriptions/:id', to: 'subscriptions#update' - - # PubSubHubbub incoming subscriptions - post '/push', to: 'push#update', as: :push - - # Salmon - post '/salmon/:id', to: 'salmon#update', as: :salmon - # OEmbed get '/oembed', to: 'oembed#show', as: :oembed @@ -318,7 +305,6 @@ Rails.application.routes.draw do get '/search', to: 'search#index', as: :search - resources :follows, only: [:create] resources :media, only: [:create, :update] resources :blocks, only: [:index] resources :mutes, only: [:index] diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 0ec1742abf..a16dea9679 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -9,9 +9,6 @@ scheduled_statuses_scheduler: every: '5m' class: Scheduler::ScheduledStatusesScheduler - subscriptions_scheduler: - cron: '<%= Random.rand(0..59) %> <%= Random.rand(4..6) %> * * *' - class: Scheduler::SubscriptionsScheduler media_cleanup_scheduler: cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *' class: Scheduler::MediaCleanupScheduler diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb index a348ab3d75..608606ff90 100644 --- a/spec/controllers/admin/accounts_controller_spec.rb +++ b/spec/controllers/admin/accounts_controller_spec.rb @@ -75,44 +75,6 @@ RSpec.describe Admin::AccountsController, type: :controller do end end - describe 'POST #subscribe' do - subject { post :subscribe, params: { id: account.id } } - - let(:current_user) { Fabricate(:user, admin: admin) } - let(:account) { Fabricate(:account) } - - context 'when user is admin' do - let(:admin) { true } - - it { is_expected.to redirect_to admin_account_path(account.id) } - end - - context 'when user is not admin' do - let(:admin) { false } - - it { is_expected.to have_http_status :forbidden } - end - end - - describe 'POST #unsubscribe' do - subject { post :unsubscribe, params: { id: account.id } } - - let(:current_user) { Fabricate(:user, admin: admin) } - let(:account) { Fabricate(:account) } - - context 'when user is admin' do - let(:admin) { true } - - it { is_expected.to redirect_to admin_account_path(account.id) } - end - - context 'when user is not admin' do - let(:admin) { false } - - it { is_expected.to have_http_status :forbidden } - end - end - describe 'POST #memorialize' do subject { post :memorialize, params: { id: account.id } } diff --git a/spec/controllers/admin/subscriptions_controller_spec.rb b/spec/controllers/admin/subscriptions_controller_spec.rb deleted file mode 100644 index 967152abe3..0000000000 --- a/spec/controllers/admin/subscriptions_controller_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true -require 'rails_helper' - -RSpec.describe Admin::SubscriptionsController, type: :controller do - render_views - - describe 'GET #index' do - around do |example| - default_per_page = Subscription.default_per_page - Subscription.paginates_per 1 - example.run - Subscription.paginates_per default_per_page - end - - before do - sign_in Fabricate(:user, admin: true), scope: :user - end - - it 'renders subscriptions' do - Fabricate(:subscription) - specified = Fabricate(:subscription) - - get :index - - subscriptions = assigns(:subscriptions) - expect(subscriptions.count).to eq 1 - expect(subscriptions[0]).to eq specified - - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/controllers/api/push_controller_spec.rb b/spec/controllers/api/push_controller_spec.rb deleted file mode 100644 index d769d8554f..0000000000 --- a/spec/controllers/api/push_controller_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'rails_helper' - -RSpec.describe Api::PushController, type: :controller do - describe 'POST #update' do - context 'with hub.mode=subscribe' do - it 'creates a subscription' do - service = double(call: ['', 202]) - allow(Pubsubhubbub::SubscribeService).to receive(:new).and_return(service) - account = Fabricate(:account) - account_topic_url = "https://#{Rails.configuration.x.local_domain}/users/#{account.username}.atom" - post :update, params: { - 'hub.mode' => 'subscribe', - 'hub.topic' => account_topic_url, - 'hub.callback' => 'https://callback.host/api', - 'hub.lease_seconds' => '3600', - 'hub.secret' => 'as1234df', - } - - expect(service).to have_received(:call).with( - account, - 'https://callback.host/api', - 'as1234df', - '3600', - nil - ) - expect(response).to have_http_status(202) - end - end - - context 'with hub.mode=unsubscribe' do - it 'unsubscribes the account' do - service = double(call: ['', 202]) - allow(Pubsubhubbub::UnsubscribeService).to receive(:new).and_return(service) - account = Fabricate(:account) - account_topic_url = "https://#{Rails.configuration.x.local_domain}/users/#{account.username}.atom" - post :update, params: { - 'hub.mode' => 'unsubscribe', - 'hub.topic' => account_topic_url, - 'hub.callback' => 'https://callback.host/api', - } - - expect(service).to have_received(:call).with( - account, - 'https://callback.host/api', - ) - expect(response).to have_http_status(202) - end - end - - context 'with unknown mode' do - it 'returns an unknown mode error' do - post :update, params: { 'hub.mode' => 'fake' } - - expect(response).to have_http_status(422) - expect(response.body).to match(/Unknown mode/) - end - end - end -end diff --git a/spec/controllers/api/salmon_controller_spec.rb b/spec/controllers/api/salmon_controller_spec.rb deleted file mode 100644 index 235a29af00..0000000000 --- a/spec/controllers/api/salmon_controller_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -require 'rails_helper' - -RSpec.describe Api::SalmonController, type: :controller do - render_views - - let(:account) { Fabricate(:user, account: Fabricate(:account, username: 'catsrgr8')).account } - - before do - stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt')) - stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt')) - stub_request(:get, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) - stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt')) - end - - describe 'POST #update' do - context 'with valid post data' do - before do - post :update, params: { id: account.id }, body: File.read(Rails.root.join('spec', 'fixtures', 'salmon', 'mention.xml')) - end - - it 'contains XML in the request body' do - expect(request.body.read).to be_a String - end - - it 'returns http success' do - expect(response).to have_http_status(202) - end - - it 'creates remote account' do - expect(Account.find_by(username: 'gargron', domain: 'quitter.no')).to_not be_nil - end - - it 'creates status' do - expect(Status.find_by(uri: 'tag:quitter.no,2016-03-20:noticeId=1276923:objectType=note')).to_not be_nil - end - - it 'creates mention for target account' do - expect(account.mentions.count).to eq 1 - end - end - - context 'with empty post data' do - before do - post :update, params: { id: account.id }, body: '' - end - - it 'returns http client error' do - expect(response).to have_http_status(400) - end - end - - context 'with invalid post data' do - before do - service = double(call: false) - allow(VerifySalmonService).to receive(:new).and_return(service) - - post :update, params: { id: account.id }, body: File.read(Rails.root.join('spec', 'fixtures', 'salmon', 'mention.xml')) - end - - it 'returns http client error' do - expect(response).to have_http_status(401) - end - end - end -end diff --git a/spec/controllers/api/subscriptions_controller_spec.rb b/spec/controllers/api/subscriptions_controller_spec.rb deleted file mode 100644 index 7a4252fe67..0000000000 --- a/spec/controllers/api/subscriptions_controller_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'rails_helper' - -RSpec.describe Api::SubscriptionsController, type: :controller do - render_views - - let(:account) { Fabricate(:account, username: 'gargron', domain: 'quitter.no', remote_url: 'topic_url', secret: 'abc') } - - describe 'GET #show' do - context 'with valid subscription' do - before do - get :show, params: { :id => account.id, 'hub.topic' => 'topic_url', 'hub.challenge' => '456', 'hub.lease_seconds' => "#{86400 * 30}" } - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'echoes back the challenge' do - expect(response.body).to match '456' - end - end - - context 'with invalid subscription' do - before do - expect_any_instance_of(Account).to receive_message_chain(:subscription, :valid?).and_return(false) - get :show, params: { :id => account.id } - end - - it 'returns http success' do - expect(response).to have_http_status(404) - end - end - end - - describe 'POST #update' do - let(:feed) { File.read(Rails.root.join('spec', 'fixtures', 'push', 'feed.atom')) } - - before do - stub_request(:post, "https://quitter.no/main/push/hub").to_return(:status => 200, :body => "", :headers => {}) - stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt')) - stub_request(:get, "https://quitter.no/notice/1269244").to_return(status: 404) - stub_request(:get, "https://quitter.no/notice/1265331").to_return(status: 404) - stub_request(:get, "https://community.highlandarrow.com/notice/54411").to_return(status: 404) - stub_request(:get, "https://community.highlandarrow.com/notice/53857").to_return(status: 404) - stub_request(:get, "https://community.highlandarrow.com/notice/51852").to_return(status: 404) - stub_request(:get, "https://social.umeahackerspace.se/notice/424348").to_return(status: 404) - stub_request(:get, "https://community.highlandarrow.com/notice/50467").to_return(status: 404) - stub_request(:get, "https://quitter.no/notice/1243309").to_return(status: 404) - stub_request(:get, "https://quitter.no/user/7477").to_return(status: 404) - stub_request(:any, "https://community.highlandarrow.com/user/1").to_return(status: 404) - stub_request(:any, "https://social.umeahackerspace.se/user/2").to_return(status: 404) - stub_request(:any, "https://gs.kawa-kun.com/user/2").to_return(status: 404) - stub_request(:any, "https://mastodon.social/users/Gargron").to_return(status: 404) - - request.env['HTTP_X_HUB_SIGNATURE'] = "sha1=#{OpenSSL::HMAC.hexdigest('sha1', 'abc', feed)}" - - post :update, params: { id: account.id }, body: feed - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'creates statuses for feed' do - expect(account.statuses.count).to_not eq 0 - end - end -end diff --git a/spec/controllers/api/v1/follows_controller_spec.rb b/spec/controllers/api/v1/follows_controller_spec.rb deleted file mode 100644 index 089e0fe5ec..0000000000 --- a/spec/controllers/api/v1/follows_controller_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'rails_helper' - -RSpec.describe Api::V1::FollowsController, type: :controller do - render_views - - let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write:follows') } - - before do - allow(controller).to receive(:doorkeeper_token) { token } - end - - describe 'POST #create' do - before do - stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt')) - stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt')) - stub_request(:head, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(:status => 405, :body => "", :headers => {}) - stub_request(:get, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) - stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt')) - stub_request(:post, "https://quitter.no/main/push/hub").to_return(:status => 200, :body => "", :headers => {}) - stub_request(:post, "https://quitter.no/main/salmon/user/7477").to_return(:status => 200, :body => "", :headers => {}) - - post :create, params: { uri: 'gargron@quitter.no' } - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - it 'creates account for remote user' do - expect(Account.find_by(username: 'gargron', domain: 'quitter.no')).to_not be_nil - end - - it 'creates a follow relation between user and remote user' do - expect(user.account.following?(Account.find_by(username: 'gargron', domain: 'quitter.no'))).to be true - end - - it 'sends a salmon slap to the remote user' do - expect(a_request(:post, "https://quitter.no/main/salmon/user/7477")).to have_been_made - end - - it 'subscribes to remote hub' do - expect(a_request(:post, "https://quitter.no/main/push/hub")).to have_been_made - end - - it 'returns http success if already following, too' do - post :create, params: { uri: 'gargron@quitter.no' } - expect(response).to have_http_status(200) - end - end -end diff --git a/spec/fixtures/requests/webfinger.txt b/spec/fixtures/requests/webfinger.txt index edb8a2dbb5..f337ecae6f 100644 --- a/spec/fixtures/requests/webfinger.txt +++ b/spec/fixtures/requests/webfinger.txt @@ -8,4 +8,4 @@ Access-Control-Allow-Origin: * Vary: Accept-Encoding,Cookie Strict-Transport-Security: max-age=31536000; includeSubdomains; -{"subject":"acct:gargron@quitter.no","aliases":["https:\/\/quitter.no\/user\/7477","https:\/\/quitter.no\/gargron","https:\/\/quitter.no\/index.php\/user\/7477","https:\/\/quitter.no\/index.php\/gargron"],"links":[{"rel":"http:\/\/webfinger.net\/rel\/profile-page","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/gmpg.org\/xfn\/11","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"describedby","type":"application\/rdf+xml","href":"https:\/\/quitter.no\/gargron\/foaf"},{"rel":"http:\/\/apinamespace.org\/atom","type":"application\/atomsvc+xml","href":"https:\/\/quitter.no\/api\/statusnet\/app\/service\/gargron.xml"},{"rel":"http:\/\/apinamespace.org\/twitter","href":"https:\/\/quitter.no\/api\/"},{"rel":"http:\/\/specs.openid.net\/auth\/2.0\/provider","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/schemas.google.com\/g\/2010#updates-from","type":"application\/atom+xml","href":"https:\/\/quitter.no\/api\/statuses\/user_timeline\/7477.atom"},{"rel":"magic-public-key","href":"data:application\/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB"},{"rel":"salmon","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-replies","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-mention","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/ostatus.org\/schema\/1.0\/subscribe","template":"https:\/\/quitter.no\/main\/ostatussub?profile={uri}"}]} \ No newline at end of file +{"subject":"acct:gargron@quitter.no","aliases":["https:\/\/quitter.no\/user\/7477","https:\/\/quitter.no\/gargron","https:\/\/quitter.no\/index.php\/user\/7477","https:\/\/quitter.no\/index.php\/gargron"],"links":[{"rel":"http:\/\/webfinger.net\/rel\/profile-page","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/gmpg.org\/xfn\/11","type":"text\/html","href":"https:\/\/quitter.no\/gargron"},{"rel":"describedby","type":"application\/rdf+xml","href":"https:\/\/quitter.no\/gargron\/foaf"},{"rel":"http:\/\/apinamespace.org\/atom","type":"application\/atomsvc+xml","href":"https:\/\/quitter.no\/api\/statusnet\/app\/service\/gargron.xml"},{"rel":"http:\/\/apinamespace.org\/twitter","href":"https:\/\/quitter.no\/api\/"},{"rel":"http:\/\/specs.openid.net\/auth\/2.0\/provider","href":"https:\/\/quitter.no\/gargron"},{"rel":"http:\/\/schemas.google.com\/g\/2010#updates-from","type":"application\/atom+xml","href":"https:\/\/quitter.no\/api\/statuses\/user_timeline\/7477.atom"},{"rel":"magic-public-key","href":"data:application\/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB"},{"rel":"salmon","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-replies","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/salmon-protocol.org\/ns\/salmon-mention","href":"https:\/\/quitter.no\/main\/salmon\/user\/7477"},{"rel":"http:\/\/ostatus.org\/schema\/1.0\/subscribe","template":"https:\/\/quitter.no\/main\/ostatussub?profile={uri}"}]} diff --git a/spec/lib/ostatus/atom_serializer_spec.rb b/spec/lib/ostatus/atom_serializer_spec.rb index 891871c1c7..74ab7576f2 100644 --- a/spec/lib/ostatus/atom_serializer_spec.rb +++ b/spec/lib/ostatus/atom_serializer_spec.rb @@ -406,28 +406,6 @@ RSpec.describe OStatus::AtomSerializer do scope = entry.nodes.find { |node| node.name == 'mastodon:scope' } expect(scope.text).to eq 'public' end - - it 'returns element whose rendered view triggers creation when processed' do - remote_account = Account.create!(username: 'username') - remote_status = Fabricate(:status, account: remote_account, created_at: '2000-01-01T00:00:00Z') - - entry = OStatus::AtomSerializer.new.entry(remote_status.stream_entry, true) - entry.nodes.delete_if { |node| node[:type] == 'application/activity+json' } # Remove ActivityPub link to simplify test - xml = OStatus::AtomSerializer.render(entry).gsub('cb6e6126.ngrok.io', 'remote.test') - - remote_status.destroy! - remote_account.destroy! - - account = Account.create!( - domain: 'remote.test', - username: 'username', - last_webfingered_at: Time.now.utc - ) - - ProcessFeedService.new.call(xml, account) - - expect(Status.find_by(uri: "https://remote.test/users/#{remote_status.account.to_param}/statuses/#{remote_status.id}")).to be_instance_of Status - end end context 'if status is not present' do @@ -683,24 +661,6 @@ RSpec.describe OStatus::AtomSerializer do end end - it 'appends link element for hub' do - account = Fabricate(:account, username: 'username') - - feed = OStatus::AtomSerializer.new.feed(account, []) - - link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'hub' } - expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/api/push' - end - - it 'appends link element for Salmon' do - account = Fabricate(:account, username: 'username') - - feed = OStatus::AtomSerializer.new.feed(account, []) - - link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'salmon' } - expect(link[:href]).to start_with 'https://cb6e6126.ngrok.io/api/salmon/' - end - it 'appends stream entries' do account = Fabricate(:account, username: 'username') status = Fabricate(:status, account: account) @@ -784,18 +744,6 @@ RSpec.describe OStatus::AtomSerializer do object = block_salmon.nodes.find { |node| node.name == 'activity:object' } expect(object.id.text).to eq 'https://domain.test/id' end - - it 'returns element whose rendered view triggers block when processed' do - block = Fabricate(:block) - block_salmon = OStatus::AtomSerializer.new.block_salmon(block) - xml = OStatus::AtomSerializer.render(block_salmon) - envelope = OStatus2::Salmon.new.pack(xml, block.account.keypair) - block.destroy! - - ProcessInteractionService.new.call(envelope, block.target_account) - - expect(block.account.blocking?(block.target_account)).to be true - end end describe '#unblock_salmon' do @@ -871,17 +819,6 @@ RSpec.describe OStatus::AtomSerializer do object = unblock_salmon.nodes.find { |node| node.name == 'activity:object' } expect(object.id.text).to eq 'https://domain.test/id' end - - it 'returns element whose rendered view triggers block when processed' do - block = Fabricate(:block) - unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) - xml = OStatus::AtomSerializer.render(unblock_salmon) - envelope = OStatus2::Salmon.new.pack(xml, block.account.keypair) - - ProcessInteractionService.new.call(envelope, block.target_account) - - expect { block.reload }.to raise_error ActiveRecord::RecordNotFound - end end describe '#favourite_salmon' do @@ -964,17 +901,6 @@ RSpec.describe OStatus::AtomSerializer do expect(favourite_salmon.title.text).to eq 'account favourited a status by status_account@remote' expect(favourite_salmon.content.text).to eq 'account favourited a status by status_account@remote' end - - it 'returns element whose rendered view triggers favourite when processed' do - favourite = Fabricate(:favourite) - favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) - xml = OStatus::AtomSerializer.render(favourite_salmon) - envelope = OStatus2::Salmon.new.pack(xml, favourite.account.keypair) - favourite.destroy! - - ProcessInteractionService.new.call(envelope, favourite.status.account) - expect(favourite.account.favourited?(favourite.status)).to be true - end end describe '#unfavourite_salmon' do @@ -1064,16 +990,6 @@ RSpec.describe OStatus::AtomSerializer do expect(unfavourite_salmon.title.text).to eq 'account no longer favourites a status by status_account@remote' expect(unfavourite_salmon.content.text).to eq 'account no longer favourites a status by status_account@remote' end - - it 'returns element whose rendered view triggers unfavourite when processed' do - favourite = Fabricate(:favourite) - unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - xml = OStatus::AtomSerializer.render(unfavourite_salmon) - envelope = OStatus2::Salmon.new.pack(xml, favourite.account.keypair) - - ProcessInteractionService.new.call(envelope, favourite.status.account) - expect { favourite.reload }.to raise_error ActiveRecord::RecordNotFound - end end describe '#follow_salmon' do @@ -1143,18 +1059,6 @@ RSpec.describe OStatus::AtomSerializer do expect(follow_salmon.title.text).to eq 'account started following target_account@remote' expect(follow_salmon.content.text).to eq 'account started following target_account@remote' end - - it 'returns element whose rendered view triggers follow when processed' do - follow = Fabricate(:follow) - follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) - xml = OStatus::AtomSerializer.render(follow_salmon) - follow.destroy! - envelope = OStatus2::Salmon.new.pack(xml, follow.account.keypair) - - ProcessInteractionService.new.call(envelope, follow.target_account) - - expect(follow.account.following?(follow.target_account)).to be true - end end describe '#unfollow_salmon' do @@ -1251,19 +1155,6 @@ RSpec.describe OStatus::AtomSerializer do object = unfollow_salmon.nodes.find { |node| node.name == 'activity:object' } expect(object.id.text).to eq 'https://domain.test/id' end - - it 'returns element whose rendered view triggers unfollow when processed' do - follow = Fabricate(:follow) - follow.destroy! - unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - xml = OStatus::AtomSerializer.render(unfollow_salmon) - follow.account.follow!(follow.target_account) - envelope = OStatus2::Salmon.new.pack(xml, follow.account.keypair) - - ProcessInteractionService.new.call(envelope, follow.target_account) - - expect(follow.account.following?(follow.target_account)).to be false - end end describe '#follow_request_salmon' do @@ -1294,18 +1185,6 @@ RSpec.describe OStatus::AtomSerializer do follow_request_salmon = serialize(follow_request) expect(follow_request_salmon.title.text).to eq 'account requested to follow target_account@remote' end - - it 'returns element whose rendered view triggers follow request when processed' do - follow_request = Fabricate(:follow_request) - follow_request_salmon = serialize(follow_request) - xml = OStatus::AtomSerializer.render(follow_request_salmon) - envelope = OStatus2::Salmon.new.pack(xml, follow_request.account.keypair) - follow_request.destroy! - - ProcessInteractionService.new.call(envelope, follow_request.target_account) - - expect(follow_request.account.requested?(follow_request.target_account)).to eq true - end end end @@ -1364,18 +1243,6 @@ RSpec.describe OStatus::AtomSerializer do verb = authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' } expect(verb.text).to eq OStatus::TagManager::VERBS[:authorize] end - - it 'returns element whose rendered view creates follow from follow request when processed' do - follow_request = Fabricate(:follow_request) - authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) - xml = OStatus::AtomSerializer.render(authorize_follow_request_salmon) - envelope = OStatus2::Salmon.new.pack(xml, follow_request.target_account.keypair) - - ProcessInteractionService.new.call(envelope, follow_request.account) - - expect(follow_request.account.following?(follow_request.target_account)).to eq true - expect { follow_request.reload }.to raise_error ActiveRecord::RecordNotFound - end end describe '#reject_follow_request_salmon' do @@ -1427,18 +1294,6 @@ RSpec.describe OStatus::AtomSerializer do verb = reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' } expect(verb.text).to eq OStatus::TagManager::VERBS[:reject] end - - it 'returns element whose rendered view deletes follow request when processed' do - follow_request = Fabricate(:follow_request) - reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) - xml = OStatus::AtomSerializer.render(reject_follow_request_salmon) - envelope = OStatus2::Salmon.new.pack(xml, follow_request.target_account.keypair) - - ProcessInteractionService.new.call(envelope, follow_request.account) - - expect(follow_request.account.following?(follow_request.target_account)).to eq false - expect { follow_request.reload }.to raise_error ActiveRecord::RecordNotFound - end end describe '#object' do diff --git a/spec/services/authorize_follow_service_spec.rb b/spec/services/authorize_follow_service_spec.rb index 562ef00412..ce56d57a62 100644 --- a/spec/services/authorize_follow_service_spec.rb +++ b/spec/services/authorize_follow_service_spec.rb @@ -38,13 +38,6 @@ RSpec.describe AuthorizeFollowService, type: :service do it 'creates follow relation' do expect(bob.following?(sender)).to be true end - - it 'sends a follow request authorization salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:authorize]) - }).to have_been_made.once - end end describe 'remote ActivityPub' do diff --git a/spec/services/batched_remove_status_service_spec.rb b/spec/services/batched_remove_status_service_spec.rb index e53623449f..d52e7f4841 100644 --- a/spec/services/batched_remove_status_service_spec.rb +++ b/spec/services/batched_remove_status_service_spec.rb @@ -49,19 +49,6 @@ RSpec.describe BatchedRemoveStatusService, type: :service do expect(Redis.current).to have_received(:publish).with('timeline:public', any_args).at_least(:once) end - it 'sends PuSH update to PuSH subscribers' do - expect(a_request(:post, 'http://example.com/push').with { |req| - matches = req.body.match(OStatus::TagManager::VERBS[:delete]) - }).to have_been_made.at_least_once - end - - it 'sends Salmon slap to previously mentioned users' do - expect(a_request(:post, "http://example.com/salmon").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:delete]) - }).to have_been_made.once - end - it 'sends delete activity to followers' do expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.at_least_once end diff --git a/spec/services/block_service_spec.rb b/spec/services/block_service_spec.rb index 6584bb90ed..de20dd0265 100644 --- a/spec/services/block_service_spec.rb +++ b/spec/services/block_service_spec.rb @@ -28,13 +28,6 @@ RSpec.describe BlockService, type: :service do it 'creates a blocking relation' do expect(sender.blocking?(bob)).to be true end - - it 'sends a block salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:block]) - }).to have_been_made.once - end end describe 'remote ActivityPub' do diff --git a/spec/services/favourite_service_spec.rb b/spec/services/favourite_service_spec.rb index 0a20ccf6e4..4c29ea77b7 100644 --- a/spec/services/favourite_service_spec.rb +++ b/spec/services/favourite_service_spec.rb @@ -30,13 +30,6 @@ RSpec.describe FavouriteService, type: :service do it 'creates a favourite' do expect(status.favourites.first).to_not be_nil end - - it 'sends a salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:favorite]) - }).to have_been_made.once - end end describe 'remote ActivityPub' do diff --git a/spec/services/fetch_remote_account_service_spec.rb b/spec/services/fetch_remote_account_service_spec.rb index 3cd86708be..37e9910d4b 100644 --- a/spec/services/fetch_remote_account_service_spec.rb +++ b/spec/services/fetch_remote_account_service_spec.rb @@ -36,36 +36,6 @@ RSpec.describe FetchRemoteAccountService, type: :service do include_examples 'return Account' end - context 'protocol is :ostatus' do - let(:prefetched_body) { xml } - let(:protocol) { :ostatus } - - before do - stub_request(:get, "https://kickass.zone/.well-known/webfinger?resource=acct:localhost@kickass.zone").to_return(request_fixture('webfinger-hacker3.txt')) - stub_request(:get, "https://kickass.zone/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) - end - - include_examples 'return Account' - - it 'does not update account information if XML comes from an unverified domain' do - feed_xml = <<-XML.squish - - - - http://activitystrea.ms/schema/1.0/person - http://kickass.zone/users/localhost - localhost - localhost - Villain!!! - - - XML - - returned_account = described_class.new.call('https://real-fake-domains.com/alice', feed_xml, :ostatus) - expect(returned_account.display_name).to_not eq 'Villain!!!' - end - end - context 'when prefetched_body is nil' do context 'protocol is :activitypub' do before do @@ -75,15 +45,5 @@ RSpec.describe FetchRemoteAccountService, type: :service do include_examples 'return Account' end - - context 'protocol is :ostatus' do - before do - stub_request(:get, url).to_return(status: 200, body: xml, headers: { 'Content-Type' => 'application/atom+xml' }) - stub_request(:get, "https://kickass.zone/.well-known/webfinger?resource=acct:localhost@kickass.zone").to_return(request_fixture('webfinger-hacker3.txt')) - stub_request(:get, "https://kickass.zone/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) - end - - include_examples 'return Account' - end end end diff --git a/spec/services/follow_service_spec.rb b/spec/services/follow_service_spec.rb index 3c4ec59be0..86c85293e8 100644 --- a/spec/services/follow_service_spec.rb +++ b/spec/services/follow_service_spec.rb @@ -96,74 +96,6 @@ RSpec.describe FollowService, type: :service do end end - context 'remote OStatus account' do - describe 'locked account' do - let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, locked: true, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account } - - before do - stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {}) - subject.call(sender, bob.acct) - end - - it 'creates a follow request' do - expect(FollowRequest.find_by(account: sender, target_account: bob)).to_not be_nil - end - - it 'sends a follow request salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:request_friend]) - }).to have_been_made.once - end - end - - describe 'unlocked account' do - let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account } - - before do - stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {}) - stub_request(:post, "http://hub.example.com/").to_return(status: 202) - subject.call(sender, bob.acct) - end - - it 'creates a following relation' do - expect(sender.following?(bob)).to be true - end - - it 'sends a follow salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:follow]) - }).to have_been_made.once - end - - it 'subscribes to PuSH' do - expect(a_request(:post, "http://hub.example.com/")).to have_been_made.once - end - end - - describe 'already followed account' do - let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, protocol: :ostatus, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com', hub_url: 'http://hub.example.com')).account } - - before do - sender.follow!(bob) - subject.call(sender, bob.acct) - end - - it 'keeps a following relation' do - expect(sender.following?(bob)).to be true - end - - it 'does not send a follow salmon slap' do - expect(a_request(:post, "http://salmon.example.com/")).not_to have_been_made - end - - it 'does not subscribe to PuSH' do - expect(a_request(:post, "http://hub.example.com/")).not_to have_been_made - end - end - end - context 'remote ActivityPub account' do let(:bob) { Fabricate(:user, account: Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox')).account } diff --git a/spec/services/import_service_spec.rb b/spec/services/import_service_spec.rb index 5cf2dadf06..5355133f40 100644 --- a/spec/services/import_service_spec.rb +++ b/spec/services/import_service_spec.rb @@ -3,7 +3,11 @@ require 'rails_helper' RSpec.describe ImportService, type: :service do let!(:account) { Fabricate(:account, locked: false) } let!(:bob) { Fabricate(:account, username: 'bob', locked: false) } - let!(:eve) { Fabricate(:account, username: 'eve', domain: 'example.com', locked: false) } + let!(:eve) { Fabricate(:account, username: 'eve', domain: 'example.com', locked: false, protocol: :activitypub, inbox_url: 'https://example.com/inbox') } + + before do + stub_request(:post, "https://example.com/inbox").to_return(status: 200) + end context 'import old-style list of muted users' do subject { ImportService.new } @@ -95,7 +99,8 @@ RSpec.describe ImportService, type: :service do let(:import) { Import.create(account: account, type: 'following', data: csv) } it 'follows the listed accounts, including boosts' do subject.call(import) - expect(account.following.count).to eq 2 + expect(account.following.count).to eq 1 + expect(account.follow_requests.count).to eq 1 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true end end @@ -106,7 +111,8 @@ RSpec.describe ImportService, type: :service do it 'follows the listed accounts, including notifications' do account.follow!(bob, reblogs: false) subject.call(import) - expect(account.following.count).to eq 2 + expect(account.following.count).to eq 1 + expect(account.follow_requests.count).to eq 1 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true end end @@ -117,7 +123,8 @@ RSpec.describe ImportService, type: :service do it 'mutes the listed accounts, including notifications' do account.follow!(bob, reblogs: false) subject.call(import) - expect(account.following.count).to eq 2 + expect(account.following.count).to eq 1 + expect(account.follow_requests.count).to eq 1 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true end end @@ -136,9 +143,10 @@ RSpec.describe ImportService, type: :service do let(:import) { Import.create(account: account, type: 'following', data: csv) } it 'follows the listed accounts, respecting boosts' do subject.call(import) - expect(account.following.count).to eq 2 + expect(account.following.count).to eq 1 + expect(account.follow_requests.count).to eq 1 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true - expect(Follow.find_by(account: account, target_account: eve).show_reblogs).to be false + expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false end end @@ -148,9 +156,10 @@ RSpec.describe ImportService, type: :service do it 'mutes the listed accounts, respecting notifications' do account.follow!(bob, reblogs: true) subject.call(import) - expect(account.following.count).to eq 2 + expect(account.following.count).to eq 1 + expect(account.follow_requests.count).to eq 1 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true - expect(Follow.find_by(account: account, target_account: eve).show_reblogs).to be false + expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false end end @@ -160,9 +169,10 @@ RSpec.describe ImportService, type: :service do it 'mutes the listed accounts, respecting notifications' do account.follow!(bob, reblogs: true) subject.call(import) - expect(account.following.count).to eq 2 + expect(account.following.count).to eq 1 + expect(account.follow_requests.count).to eq 1 expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true - expect(Follow.find_by(account: account, target_account: eve).show_reblogs).to be false + expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false end end end diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb index facbe977f2..bf06f50e9b 100644 --- a/spec/services/post_status_service_spec.rb +++ b/spec/services/post_status_service_spec.rb @@ -144,7 +144,6 @@ RSpec.describe PostStatusService, type: :service do it 'gets distributed' do allow(DistributionWorker).to receive(:perform_async) - allow(Pubsubhubbub::DistributionWorker).to receive(:perform_async) allow(ActivityPub::DistributionWorker).to receive(:perform_async) account = Fabricate(:account) @@ -152,7 +151,6 @@ RSpec.describe PostStatusService, type: :service do status = subject.call(account, text: "test status update") expect(DistributionWorker).to have_received(:perform_async).with(status.id) - expect(Pubsubhubbub::DistributionWorker).to have_received(:perform_async).with(status.stream_entry.id) expect(ActivityPub::DistributionWorker).to have_received(:perform_async).with(status.id) end diff --git a/spec/services/process_feed_service_spec.rb b/spec/services/process_feed_service_spec.rb deleted file mode 100644 index 9d3465f3f8..0000000000 --- a/spec/services/process_feed_service_spec.rb +++ /dev/null @@ -1,252 +0,0 @@ -require 'rails_helper' - -RSpec.describe ProcessFeedService, type: :service do - subject { ProcessFeedService.new } - - describe 'processing a feed' do - let(:body) { File.read(Rails.root.join('spec', 'fixtures', 'xml', 'mastodon.atom')) } - let(:account) { Fabricate(:account, username: 'localhost', domain: 'kickass.zone') } - - before do - stub_request(:post, "https://pubsubhubbub.superfeedr.com/").to_return(:status => 200, :body => "", :headers => {}) - stub_request(:head, "http://kickass.zone/media/2").to_return(:status => 404) - stub_request(:head, "http://kickass.zone/media/3").to_return(:status => 404) - stub_request(:get, "http://kickass.zone/system/accounts/avatars/000/000/001/large/eris.png").to_return(request_fixture('avatar.txt')) - stub_request(:get, "http://kickass.zone/system/media_attachments/files/000/000/002/original/morpheus_linux.jpg?1476059910").to_return(request_fixture('attachment1.txt')) - stub_request(:get, "http://kickass.zone/system/media_attachments/files/000/000/003/original/gizmo.jpg?1476060065").to_return(request_fixture('attachment2.txt')) - end - - context 'when domain does not reject media' do - before do - subject.call(body, account) - end - - it 'updates remote user\'s account information' do - account.reload - expect(account.display_name).to eq '::1' - expect(account).to have_attached_file(:avatar) - expect(account.avatar_file_name).not_to be_nil - end - - it 'creates posts' do - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=1:objectType=Status')).to_not be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status')).to_not be_nil - end - - it 'marks replies as replies' do - status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status') - expect(status.reply?).to be true - end - - it 'sets account being replied to when possible' do - status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status') - expect(status.in_reply_to_account_id).to eq status.account_id - end - - it 'ignores delete statuses unless they existed before' do - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=3:objectType=Status')).to be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=12:objectType=Status')).to be_nil - end - - it 'does not create statuses for follows' do - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=1:objectType=Follow')).to be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Follow')).to be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=4:objectType=Follow')).to be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=7:objectType=Follow')).to be_nil - end - - it 'does not create statuses for favourites' do - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Favourite')).to be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=3:objectType=Favourite')).to be_nil - end - - it 'creates posts with media' do - status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=14:objectType=Status') - - expect(status).to_not be_nil - expect(status.media_attachments.first).to have_attached_file(:file) - expect(status.media_attachments.first.image?).to be true - expect(status.media_attachments.first.file_file_name).not_to be_nil - end - end - - context 'when domain is set to reject media' do - let!(:domain_block) { Fabricate(:domain_block, domain: 'kickass.zone', reject_media: true) } - - before do - subject.call(body, account) - end - - it 'updates remote user\'s account information' do - account.reload - expect(account.display_name).to eq '::1' - end - - it 'rejects remote user\'s avatar' do - account.reload - expect(account.display_name).to eq '::1' - expect(account.avatar_file_name).to be_nil - end - - it 'creates posts' do - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=1:objectType=Status')).to_not be_nil - expect(Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=2:objectType=Status')).to_not be_nil - end - - it 'creates posts with remote-only media' do - status = Status.find_by(uri: 'tag:kickass.zone,2016-10-10:objectId=14:objectType=Status') - - expect(status).to_not be_nil - expect(status.media_attachments.first.file_file_name).to be_nil - expect(status.media_attachments.first.unknown?).to be true - end - end - end - - it 'does not accept tampered reblogs' do - good_actor = Fabricate(:account, username: 'tracer', domain: 'overwatch.com') - - real_body = < - - tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status - 2017-04-27T13:49:25Z - 2017-04-27T13:49:25Z - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - https://overwatch.com/users/tracer - http://activitystrea.ms/schema/1.0/person - https://overwatch.com/users/tracer - tracer - - Overwatch rocks - -XML - - stub_request(:get, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 200, body: real_body, headers: { 'Content-Type' => 'application/atom+xml' }) - - bad_actor = Fabricate(:account, username: 'sombra', domain: 'talon.xyz') - - body = < - - tag:talon.xyz,2017-04-27:objectId=4467137:objectType=Status - 2017-04-27T13:49:25Z - 2017-04-27T13:49:25Z - - https://talon.xyz/users/sombra - http://activitystrea.ms/schema/1.0/person - https://talon.xyz/users/sombra - sombra - - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - Overwatch SUCKS AHAHA - - tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - https://overwatch.com/users/tracer - http://activitystrea.ms/schema/1.0/person - https://overwatch.com/users/tracer - tracer - - Overwatch SUCKS AHAHA - - - -XML - created_statuses = subject.call(body, bad_actor) - - expect(created_statuses.first.reblog?).to be true - expect(created_statuses.first.account_id).to eq bad_actor.id - expect(created_statuses.first.reblog.account_id).to eq good_actor.id - expect(created_statuses.first.reblog.text).to eq 'Overwatch rocks' - end - - it 'ignores reblogs if it failed to retrieve reblogged statuses' do - stub_request(:get, 'https://overwatch.com/users/tracer/updates/1').to_return(status: 404) - - actor = Fabricate(:account, username: 'tracer', domain: 'overwatch.com') - - body = < - - tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status - 2017-04-27T13:49:25Z - 2017-04-27T13:49:25Z - - https://overwatch.com/users/tracer - http://activitystrea.ms/schema/1.0/person - https://overwatch.com/users/tracer - tracer - - http://activitystrea.ms/schema/1.0/activity - http://activitystrea.ms/schema/1.0/share - Overwatch rocks - - tag:overwatch.com,2017-04-27:objectId=4467137:objectType=Status - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - https://overwatch.com/users/tracer - http://activitystrea.ms/schema/1.0/person - https://overwatch.com/users/tracer - tracer - - Overwatch rocks - - -XML - - created_statuses = subject.call(body, actor) - - expect(created_statuses).to eq [] - end - - it 'ignores statuses with an out-of-order delete' do - sender = Fabricate(:account, username: 'tracer', domain: 'overwatch.com') - - delete_body = < - - tag:overwatch.com,2017-04-27:objectId=4487555:objectType=Status - 2017-04-27T13:49:25Z - 2017-04-27T13:49:25Z - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/delete - - https://overwatch.com/users/tracer - http://activitystrea.ms/schema/1.0/person - https://overwatch.com/users/tracer - tracer - - -XML - - status_body = < - - tag:overwatch.com,2017-04-27:objectId=4487555:objectType=Status - 2017-04-27T13:49:25Z - 2017-04-27T13:49:25Z - http://activitystrea.ms/schema/1.0/note - http://activitystrea.ms/schema/1.0/post - - https://overwatch.com/users/tracer - http://activitystrea.ms/schema/1.0/person - https://overwatch.com/users/tracer - tracer - - Overwatch rocks - -XML - - subject.call(delete_body, sender) - created_statuses = subject.call(status_body, sender) - - expect(created_statuses).to be_empty - end -end diff --git a/spec/services/process_interaction_service_spec.rb b/spec/services/process_interaction_service_spec.rb deleted file mode 100644 index b858c19d0f..0000000000 --- a/spec/services/process_interaction_service_spec.rb +++ /dev/null @@ -1,151 +0,0 @@ -require 'rails_helper' - -RSpec.describe ProcessInteractionService, type: :service do - let(:receiver) { Fabricate(:user, email: 'alice@example.com', account: Fabricate(:account, username: 'alice')).account } - let(:sender) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } - let(:remote_sender) { Fabricate(:account, username: 'carol', domain: 'localdomain.com', uri: 'https://webdomain.com/users/carol') } - - subject { ProcessInteractionService.new } - - describe 'status delete slap' do - let(:remote_status) { Fabricate(:status, account: remote_sender) } - let(:envelope) { OStatus2::Salmon.new.pack(payload, sender.keypair) } - let(:payload) { - <<~XML - - - carol@localdomain.com - carol - https://webdomain.com/users/carol - - - #{remote_status.id} - http://activitystrea.ms/schema/1.0/delete - - XML - } - - before do - receiver.update(locked: true) - remote_sender.update(private_key: sender.private_key, public_key: remote_sender.public_key) - end - - it 'deletes a record' do - expect(RemovalWorker).to receive(:perform_async).with(remote_status.id) - subject.call(envelope, receiver) - end - end - - describe 'follow request slap' do - before do - receiver.update(locked: true) - - payload = < - - bob - https://cb6e6126.ngrok.io/users/bob - - - someIdHere - http://activitystrea.ms/schema/1.0/request-friend - -XML - - envelope = OStatus2::Salmon.new.pack(payload, sender.keypair) - subject.call(envelope, receiver) - end - - it 'creates a record' do - expect(FollowRequest.find_by(account: sender, target_account: receiver)).to_not be_nil - end - end - - describe 'follow request slap from known remote user identified by email' do - before do - receiver.update(locked: true) - # Copy already-generated key - remote_sender.update(private_key: sender.private_key, public_key: remote_sender.public_key) - - payload = < - - carol@localdomain.com - carol - https://webdomain.com/users/carol - - - someIdHere - http://activitystrea.ms/schema/1.0/request-friend - -XML - - envelope = OStatus2::Salmon.new.pack(payload, remote_sender.keypair) - subject.call(envelope, receiver) - end - - it 'creates a record' do - expect(FollowRequest.find_by(account: remote_sender, target_account: receiver)).to_not be_nil - end - end - - describe 'follow request authorization slap' do - before do - receiver.update(locked: true) - FollowRequest.create(account: sender, target_account: receiver) - - payload = < - - alice - https://cb6e6126.ngrok.io/users/alice - - - someIdHere - http://activitystrea.ms/schema/1.0/authorize - -XML - - envelope = OStatus2::Salmon.new.pack(payload, receiver.keypair) - subject.call(envelope, sender) - end - - it 'creates a follow relationship' do - expect(Follow.find_by(account: sender, target_account: receiver)).to_not be_nil - end - - it 'removes the follow request' do - expect(FollowRequest.find_by(account: sender, target_account: receiver)).to be_nil - end - end - - describe 'follow request rejection slap' do - before do - receiver.update(locked: true) - FollowRequest.create(account: sender, target_account: receiver) - - payload = < - - alice - https://cb6e6126.ngrok.io/users/alice - - - someIdHere - http://activitystrea.ms/schema/1.0/reject - -XML - - envelope = OStatus2::Salmon.new.pack(payload, receiver.keypair) - subject.call(envelope, sender) - end - - it 'does not create a follow relationship' do - expect(Follow.find_by(account: sender, target_account: receiver)).to be_nil - end - - it 'removes the follow request' do - expect(FollowRequest.find_by(account: sender, target_account: receiver)).to be_nil - end - end -end diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb index 8a6bb44aca..35a804f2b9 100644 --- a/spec/services/process_mentions_service_spec.rb +++ b/spec/services/process_mentions_service_spec.rb @@ -18,10 +18,6 @@ RSpec.describe ProcessMentionsService, type: :service do it 'creates a mention' do expect(remote_user.mentions.where(status: status).count).to eq 1 end - - it 'posts to remote user\'s Salmon end point' do - expect(a_request(:post, remote_user.salmon_url)).to have_been_made.once - end end context 'OStatus with private toot' do diff --git a/spec/services/pubsubhubbub/subscribe_service_spec.rb b/spec/services/pubsubhubbub/subscribe_service_spec.rb deleted file mode 100644 index 01c956230a..0000000000 --- a/spec/services/pubsubhubbub/subscribe_service_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Pubsubhubbub::SubscribeService, type: :service do - describe '#call' do - subject { described_class.new } - let(:user_account) { Fabricate(:account) } - - context 'with a nil account' do - it 'returns the invalid topic status results' do - result = service_call(account: nil) - - expect(result).to eq invalid_topic_status - end - end - - context 'with an invalid callback url' do - it 'returns invalid callback status when callback is blank' do - result = service_call(callback: '') - - expect(result).to eq invalid_callback_status - end - it 'returns invalid callback status when callback is not a URI' do - result = service_call(callback: 'invalid-hostname') - - expect(result).to eq invalid_callback_status - end - end - - context 'with a blocked domain in the callback' do - it 'returns callback not allowed' do - Fabricate(:domain_block, domain: 'test.host', severity: :suspend) - result = service_call(callback: 'https://test.host/api') - - expect(result).to eq not_allowed_callback_status - end - end - - context 'with a valid account and callback' do - it 'returns success status and confirms subscription' do - allow(Pubsubhubbub::ConfirmationWorker).to receive(:perform_async).and_return(nil) - subscription = Fabricate(:subscription, account: user_account) - - result = service_call(callback: subscription.callback_url) - expect(result).to eq success_status - expect(Pubsubhubbub::ConfirmationWorker).to have_received(:perform_async).with(subscription.id, 'subscribe', 'asdf', 3600) - end - end - end - - def service_call(account: user_account, callback: 'https://callback.host', secret: 'asdf', lease_seconds: 3600) - subject.call(account, callback, secret, lease_seconds) - end - - def invalid_topic_status - ['Invalid topic URL', 422] - end - - def invalid_callback_status - ['Invalid callback URL', 422] - end - - def not_allowed_callback_status - ['Callback URL not allowed', 403] - end - - def success_status - ['', 202] - end -end diff --git a/spec/services/pubsubhubbub/unsubscribe_service_spec.rb b/spec/services/pubsubhubbub/unsubscribe_service_spec.rb deleted file mode 100644 index 7ed9fc5af5..0000000000 --- a/spec/services/pubsubhubbub/unsubscribe_service_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Pubsubhubbub::UnsubscribeService, type: :service do - describe '#call' do - subject { described_class.new } - - context 'with a nil account' do - it 'returns an invalid topic status' do - result = subject.call(nil, 'callback.host') - - expect(result).to eq invalid_topic_status - end - end - - context 'with a valid account' do - let(:account) { Fabricate(:account) } - - it 'returns a valid topic status and does not run confirm when no subscription' do - allow(Pubsubhubbub::ConfirmationWorker).to receive(:perform_async).and_return(nil) - result = subject.call(account, 'callback.host') - - expect(result).to eq valid_topic_status - expect(Pubsubhubbub::ConfirmationWorker).not_to have_received(:perform_async) - end - - it 'returns a valid topic status and does run confirm when there is a subscription' do - subscription = Fabricate(:subscription, account: account, callback_url: 'callback.host') - allow(Pubsubhubbub::ConfirmationWorker).to receive(:perform_async).and_return(nil) - result = subject.call(account, 'callback.host') - - expect(result).to eq valid_topic_status - expect(Pubsubhubbub::ConfirmationWorker).to have_received(:perform_async).with(subscription.id, 'unsubscribe') - end - end - - def invalid_topic_status - ['Invalid topic URL', 422] - end - - def valid_topic_status - ['', 202] - end - end -end diff --git a/spec/services/reblog_service_spec.rb b/spec/services/reblog_service_spec.rb index 9d84c41d5e..58fb46f0ff 100644 --- a/spec/services/reblog_service_spec.rb +++ b/spec/services/reblog_service_spec.rb @@ -46,10 +46,6 @@ RSpec.describe ReblogService, type: :service do it 'creates a reblog' do expect(status.reblogs.count).to eq 1 end - - it 'sends a Salmon slap for a remote reblog' do - expect(a_request(:post, 'http://salmon.example.com')).to have_been_made - end end context 'ActivityPub' do diff --git a/spec/services/reject_follow_service_spec.rb b/spec/services/reject_follow_service_spec.rb index e5ac37ed90..1aec060db7 100644 --- a/spec/services/reject_follow_service_spec.rb +++ b/spec/services/reject_follow_service_spec.rb @@ -38,13 +38,6 @@ RSpec.describe RejectFollowService, type: :service do it 'does not create follow relation' do expect(bob.following?(sender)).to be false end - - it 'sends a follow request rejection salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:reject]) - }).to have_been_made.once - end end describe 'remote ActivityPub' do diff --git a/spec/services/remove_status_service_spec.rb b/spec/services/remove_status_service_spec.rb index 7bba83a602..48191d47c4 100644 --- a/spec/services/remove_status_service_spec.rb +++ b/spec/services/remove_status_service_spec.rb @@ -32,23 +32,10 @@ RSpec.describe RemoveStatusService, type: :service do expect(HomeFeed.new(jeff).get(10)).to_not include(@status.id) end - it 'sends PuSH update to PuSH subscribers' do - expect(a_request(:post, 'http://example.com/push').with { |req| - req.body.match(OStatus::TagManager::VERBS[:delete]) - }).to have_been_made - end - it 'sends delete activity to followers' do expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.twice end - it 'sends Salmon slap to previously mentioned users' do - expect(a_request(:post, "http://example.com/salmon").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:delete]) - }).to have_been_made.once - end - it 'sends delete activity to rebloggers' do expect(a_request(:post, 'http://example2.com/inbox')).to have_been_made end diff --git a/spec/services/resolve_account_service_spec.rb b/spec/services/resolve_account_service_spec.rb index 27a85af7c9..7a64f41618 100644 --- a/spec/services/resolve_account_service_spec.rb +++ b/spec/services/resolve_account_service_spec.rb @@ -6,19 +6,13 @@ RSpec.describe ResolveAccountService, type: :service do before do stub_request(:get, "https://quitter.no/.well-known/host-meta").to_return(request_fixture('.host-meta.txt')) stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:catsrgr8@example.com").to_return(status: 404) - stub_request(:get, "https://redirected.com/.well-known/host-meta").to_return(request_fixture('redirected.host-meta.txt')) stub_request(:get, "https://example.com/.well-known/host-meta").to_return(status: 404) - stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no").to_return(request_fixture('webfinger.txt')) - stub_request(:get, "https://redirected.com/.well-known/webfinger?resource=acct:gargron@redirected.com").to_return(request_fixture('webfinger.txt')) - stub_request(:get, "https://redirected.com/.well-known/webfinger?resource=acct:hacker1@redirected.com").to_return(request_fixture('webfinger-hacker1.txt')) - stub_request(:get, "https://redirected.com/.well-known/webfinger?resource=acct:hacker2@redirected.com").to_return(request_fixture('webfinger-hacker2.txt')) - stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:catsrgr8@quitter.no").to_return(status: 404) - stub_request(:get, "https://quitter.no/api/statuses/user_timeline/7477.atom").to_return(request_fixture('feed.txt')) stub_request(:get, "https://quitter.no/avatar/7477-300-20160211190340.png").to_return(request_fixture('avatar.txt')) - stub_request(:get, "https://localdomain.com/.well-known/host-meta").to_return(request_fixture('localdomain-hostmeta.txt')) - stub_request(:get, "https://localdomain.com/.well-known/webfinger?resource=acct:foo@localdomain.com").to_return(status: 404) - stub_request(:get, "https://webdomain.com/.well-known/webfinger?resource=acct:foo@localdomain.com").to_return(request_fixture('localdomain-webfinger.txt')) - stub_request(:get, "https://webdomain.com/users/foo.atom").to_return(request_fixture('localdomain-feed.txt')) + stub_request(:get, "https://quitter.no/.well-known/webfinger?resource=acct:catsrgr8@quitter.no").to_return(status: 404) + stub_request(:get, "https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com").to_return(request_fixture('activitypub-webfinger.txt')) + stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor.txt')) + stub_request(:get, "https://ap.example.com/users/foo.atom").to_return(request_fixture('activitypub-feed.txt')) + stub_request(:get, %r{https://ap.example.com/users/foo/\w+}).to_return(status: 404) end it 'raises error if no such user can be resolved via webfinger' do @@ -29,74 +23,7 @@ RSpec.describe ResolveAccountService, type: :service do expect(subject.call('catsrgr8@example.com')).to be_nil end - it 'prevents hijacking existing accounts' do - account = subject.call('hacker1@redirected.com') - expect(account.salmon_url).to_not eq 'https://hacker.com/main/salmon/user/7477' - end - - it 'prevents hijacking inexisting accounts' do - expect(subject.call('hacker2@redirected.com')).to be_nil - end - - context 'with an OStatus account' do - it 'returns an already existing remote account' do - old_account = Fabricate(:account, username: 'gargron', domain: 'quitter.no') - returned_account = subject.call('gargron@quitter.no') - - expect(old_account.id).to eq returned_account.id - end - - it 'returns a new remote account' do - account = subject.call('gargron@quitter.no') - - expect(account.username).to eq 'gargron' - expect(account.domain).to eq 'quitter.no' - expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom' - end - - it 'follows a legitimate account redirection' do - account = subject.call('gargron@redirected.com') - - expect(account.username).to eq 'gargron' - expect(account.domain).to eq 'quitter.no' - expect(account.remote_url).to eq 'https://quitter.no/api/statuses/user_timeline/7477.atom' - end - - it 'returns a new remote account' do - account = subject.call('foo@localdomain.com') - - expect(account.username).to eq 'foo' - expect(account.domain).to eq 'localdomain.com' - expect(account.remote_url).to eq 'https://webdomain.com/users/foo.atom' - end - end - context 'with an ActivityPub account' do - before do - stub_request(:get, "https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com").to_return(request_fixture('activitypub-webfinger.txt')) - stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor.txt')) - stub_request(:get, "https://ap.example.com/users/foo.atom").to_return(request_fixture('activitypub-feed.txt')) - stub_request(:get, %r{https://ap.example.com/users/foo/\w+}).to_return(status: 404) - end - - it 'fallback to OStatus if actor json could not be fetched' do - stub_request(:get, "https://ap.example.com/users/foo").to_return(status: 404) - - account = subject.call('foo@ap.example.com') - - expect(account.ostatus?).to eq true - expect(account.remote_url).to eq 'https://ap.example.com/users/foo.atom' - end - - it 'fallback to OStatus if actor json did not have inbox_url' do - stub_request(:get, "https://ap.example.com/users/foo").to_return(request_fixture('activitypub-actor-noinbox.txt')) - - account = subject.call('foo@ap.example.com') - - expect(account.ostatus?).to eq true - expect(account.remote_url).to eq 'https://ap.example.com/users/foo.atom' - end - it 'returns new remote account' do account = subject.call('foo@ap.example.com') @@ -124,13 +51,14 @@ RSpec.describe ResolveAccountService, type: :service do it 'processes one remote account at a time using locks' do wait_for_start = true fail_occurred = false - return_values = [] + return_values = Concurrent::Array.new threads = Array.new(5) do Thread.new do true while wait_for_start + begin - return_values << described_class.new.call('foo@localdomain.com') + return_values << described_class.new.call('foo@ap.example.com') rescue ActiveRecord::RecordNotUnique fail_occurred = true end diff --git a/spec/services/send_interaction_service_spec.rb b/spec/services/send_interaction_service_spec.rb deleted file mode 100644 index 710d8184cf..0000000000 --- a/spec/services/send_interaction_service_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'rails_helper' - -RSpec.describe SendInteractionService, type: :service do - subject { SendInteractionService.new } - - it 'sends an XML envelope to the Salmon end point of remote user' -end diff --git a/spec/services/subscribe_service_spec.rb b/spec/services/subscribe_service_spec.rb deleted file mode 100644 index 10bdb1ba8f..0000000000 --- a/spec/services/subscribe_service_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'rails_helper' - -RSpec.describe SubscribeService, type: :service do - let(:account) { Fabricate(:account, username: 'bob', domain: 'example.com', hub_url: 'http://hub.example.com') } - subject { SubscribeService.new } - - it 'sends subscription request to PuSH hub' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 202) - subject.call(account) - expect(a_request(:post, 'http://hub.example.com/')).to have_been_made.once - end - - it 'generates and keeps PuSH secret on successful call' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 202) - subject.call(account) - expect(account.secret).to_not be_blank - end - - it 'fails silently if PuSH hub forbids subscription' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 403) - subject.call(account) - end - - it 'fails silently if PuSH hub is not found' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 404) - subject.call(account) - end - - it 'fails loudly if there is a network error' do - stub_request(:post, 'http://hub.example.com/').to_raise(HTTP::Error) - expect { subject.call(account) }.to raise_error HTTP::Error - end - - it 'fails loudly if PuSH hub is unavailable' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 503) - expect { subject.call(account) }.to raise_error Mastodon::UnexpectedResponseError - end - - it 'fails loudly if rate limited' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 429) - expect { subject.call(account) }.to raise_error Mastodon::UnexpectedResponseError - end -end diff --git a/spec/services/unblock_service_spec.rb b/spec/services/unblock_service_spec.rb index 5835b912ba..6350c68344 100644 --- a/spec/services/unblock_service_spec.rb +++ b/spec/services/unblock_service_spec.rb @@ -30,13 +30,6 @@ RSpec.describe UnblockService, type: :service do it 'destroys the blocking relation' do expect(sender.blocking?(bob)).to be false end - - it 'sends an unblock salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:unblock]) - }).to have_been_made.once - end end describe 'remote ActivityPub' do diff --git a/spec/services/unfollow_service_spec.rb b/spec/services/unfollow_service_spec.rb index 8a2881ab17..84b5dafbc4 100644 --- a/spec/services/unfollow_service_spec.rb +++ b/spec/services/unfollow_service_spec.rb @@ -30,13 +30,6 @@ RSpec.describe UnfollowService, type: :service do it 'destroys the following relation' do expect(sender.following?(bob)).to be false end - - it 'sends an unfollow salmon slap' do - expect(a_request(:post, "http://salmon.example.com/").with { |req| - xml = OStatus2::Salmon.new.unpack(req.body) - xml.match(OStatus::TagManager::VERBS[:unfollow]) - }).to have_been_made.once - end end describe 'remote ActivityPub' do diff --git a/spec/services/unsubscribe_service_spec.rb b/spec/services/unsubscribe_service_spec.rb deleted file mode 100644 index 54d4b1b53c..0000000000 --- a/spec/services/unsubscribe_service_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'rails_helper' - -RSpec.describe UnsubscribeService, type: :service do - let(:account) { Fabricate(:account, username: 'bob', domain: 'example.com', hub_url: 'http://hub.example.com') } - subject { UnsubscribeService.new } - - it 'removes the secret and resets expiration on account' do - stub_request(:post, 'http://hub.example.com/').to_return(status: 204) - subject.call(account) - account.reload - - expect(account.secret).to be_blank - expect(account.subscription_expires_at).to be_blank - end - - it 'logs error on subscription failure' do - logger = stub_logger - stub_request(:post, 'http://hub.example.com/').to_return(status: 404) - subject.call(account) - - expect(logger).to have_received(:debug).with(/unsubscribe for bob@example.com failed/) - end - - it 'logs error on connection failure' do - logger = stub_logger - stub_request(:post, 'http://hub.example.com/').to_raise(HTTP::Error) - subject.call(account) - - expect(logger).to have_received(:debug).with(/unsubscribe for bob@example.com failed/) - end - - def stub_logger - double(debug: nil).tap do |logger| - allow(Rails).to receive(:logger).and_return(logger) - end - end -end diff --git a/spec/services/update_remote_profile_service_spec.rb b/spec/services/update_remote_profile_service_spec.rb deleted file mode 100644 index f3ea70b801..0000000000 --- a/spec/services/update_remote_profile_service_spec.rb +++ /dev/null @@ -1,84 +0,0 @@ -require 'rails_helper' - -RSpec.describe UpdateRemoteProfileService, type: :service do - let(:xml) { File.read(Rails.root.join('spec', 'fixtures', 'push', 'feed.atom')) } - - subject { UpdateRemoteProfileService.new } - - before do - stub_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png').to_return(request_fixture('avatar.txt')) - end - - context 'with updated details' do - let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com') } - - before do - subject.call(xml, remote_account) - end - - it 'downloads new avatar' do - expect(a_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png')).to have_been_made - end - - it 'sets the avatar remote url' do - expect(remote_account.reload.avatar_remote_url).to eq 'https://quitter.no/avatar/7477-300-20160211190340.png' - end - - it 'sets display name' do - expect(remote_account.reload.display_name).to eq 'DIGITAL CAT' - end - - it 'sets note' do - expect(remote_account.reload.note).to eq 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes' - end - end - - context 'with unchanged details' do - let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com', display_name: 'DIGITAL CAT', note: 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes', avatar_remote_url: 'https://quitter.no/avatar/7477-300-20160211190340.png') } - - before do - subject.call(xml, remote_account) - end - - it 'does not re-download avatar' do - expect(a_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png')).to have_been_made.once - end - - it 'sets the avatar remote url' do - expect(remote_account.reload.avatar_remote_url).to eq 'https://quitter.no/avatar/7477-300-20160211190340.png' - end - - it 'sets display name' do - expect(remote_account.reload.display_name).to eq 'DIGITAL CAT' - end - - it 'sets note' do - expect(remote_account.reload.note).to eq 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes' - end - end - - context 'with updated details from a domain set to reject media' do - let(:remote_account) { Fabricate(:account, username: 'bob', domain: 'example.com') } - let!(:domain_block) { Fabricate(:domain_block, domain: 'example.com', reject_media: true) } - - before do - subject.call(xml, remote_account) - end - - it 'does not the avatar remote url' do - expect(remote_account.reload.avatar_remote_url).to be_nil - end - - it 'sets display name' do - expect(remote_account.reload.display_name).to eq 'DIGITAL CAT' - end - - it 'sets note' do - expect(remote_account.reload.note).to eq 'Software engineer, free time musician and DIGITAL SPORTS enthusiast. Likes cats. Warning: May contain memes' - end - - it 'does not set store the avatar' do - expect(remote_account.reload.avatar_file_name).to be_nil - end - end -end diff --git a/spec/workers/after_remote_follow_request_worker_spec.rb b/spec/workers/after_remote_follow_request_worker_spec.rb deleted file mode 100644 index bd623cca50..0000000000 --- a/spec/workers/after_remote_follow_request_worker_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe AfterRemoteFollowRequestWorker do - subject { described_class.new } - let(:follow_request) { Fabricate(:follow_request) } - describe 'perform' do - context 'when the follow_request does not exist' do - it 'catches a raise and returns true' do - allow(FollowService).to receive(:new) - result = subject.perform('aaa') - - expect(result).to eq(true) - expect(FollowService).not_to have_received(:new) - end - end - - context 'when the account cannot be updated' do - it 'returns nil and does not call service when account is nil' do - allow(FollowService).to receive(:new) - service = double(call: nil) - allow(FetchRemoteAccountService).to receive(:new).and_return(service) - - result = subject.perform(follow_request.id) - - expect(result).to be_nil - expect(FollowService).not_to have_received(:new) - end - - it 'returns nil and does not call service when account is locked' do - allow(FollowService).to receive(:new) - service = double(call: double(locked?: true)) - allow(FetchRemoteAccountService).to receive(:new).and_return(service) - - result = subject.perform(follow_request.id) - - expect(result).to be_nil - expect(FollowService).not_to have_received(:new) - end - end - - context 'when the account is updated' do - it 'calls the follow service and destroys the follow' do - follow_service = double(call: nil) - allow(FollowService).to receive(:new).and_return(follow_service) - account = Fabricate(:account, locked: false) - service = double(call: account) - allow(FetchRemoteAccountService).to receive(:new).and_return(service) - - result = subject.perform(follow_request.id) - - expect(result).to be_nil - expect(follow_service).to have_received(:call).with(follow_request.account, account.acct) - expect { follow_request.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end - end -end diff --git a/spec/workers/after_remote_follow_worker_spec.rb b/spec/workers/after_remote_follow_worker_spec.rb deleted file mode 100644 index d93c469f9d..0000000000 --- a/spec/workers/after_remote_follow_worker_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe AfterRemoteFollowWorker do - subject { described_class.new } - let(:follow) { Fabricate(:follow) } - describe 'perform' do - context 'when the follow does not exist' do - it 'catches a raise and returns true' do - allow(FollowService).to receive(:new) - result = subject.perform('aaa') - - expect(result).to eq(true) - expect(FollowService).not_to have_received(:new) - end - end - - context 'when the account cannot be updated' do - it 'returns nil and does not call service when account is nil' do - allow(FollowService).to receive(:new) - service = double(call: nil) - allow(FetchRemoteAccountService).to receive(:new).and_return(service) - - result = subject.perform(follow.id) - - expect(result).to be_nil - expect(FollowService).not_to have_received(:new) - end - - it 'returns nil and does not call service when account is not locked' do - allow(FollowService).to receive(:new) - service = double(call: double(locked?: false)) - allow(FetchRemoteAccountService).to receive(:new).and_return(service) - - result = subject.perform(follow.id) - - expect(result).to be_nil - expect(FollowService).not_to have_received(:new) - end - end - - context 'when the account is updated' do - it 'calls the follow service and destroys the follow' do - follow_service = double(call: nil) - allow(FollowService).to receive(:new).and_return(follow_service) - account = Fabricate(:account, locked: true) - service = double(call: account) - allow(FetchRemoteAccountService).to receive(:new).and_return(service) - - result = subject.perform(follow.id) - - expect(result).to be_nil - expect(follow_service).to have_received(:call).with(follow.account, account.acct) - expect { follow.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - end - end -end diff --git a/spec/workers/pubsubhubbub/confirmation_worker_spec.rb b/spec/workers/pubsubhubbub/confirmation_worker_spec.rb deleted file mode 100644 index 1eecdd2b5b..0000000000 --- a/spec/workers/pubsubhubbub/confirmation_worker_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Pubsubhubbub::ConfirmationWorker do - include RoutingHelper - - subject { described_class.new } - - let!(:alice) { Fabricate(:account, username: 'alice') } - let!(:subscription) { Fabricate(:subscription, account: alice, callback_url: 'http://example.com/api', confirmed: false, expires_at: 3.days.from_now, secret: nil) } - - describe 'perform' do - describe 'with subscribe mode' do - it 'confirms and updates subscription when challenge matches' do - stub_random_value - stub_request(:get, url_for_mode('subscribe')) - .with(headers: http_headers) - .to_return(status: 200, body: challenge_value, headers: {}) - - seconds = 10.days.seconds.to_i - subject.perform(subscription.id, 'subscribe', 'asdf', seconds) - - subscription.reload - expect(subscription.secret).to eq 'asdf' - expect(subscription.confirmed).to eq true - expect(subscription.expires_at).to be_within(5).of(10.days.from_now) - end - - it 'does not update subscription when challenge does not match' do - stub_random_value - stub_request(:get, url_for_mode('subscribe')) - .with(headers: http_headers) - .to_return(status: 200, body: 'wrong value', headers: {}) - - seconds = 10.days.seconds.to_i - subject.perform(subscription.id, 'subscribe', 'asdf', seconds) - - subscription.reload - expect(subscription.secret).to be_blank - expect(subscription.confirmed).to eq false - expect(subscription.expires_at).to be_within(5).of(3.days.from_now) - end - end - - describe 'with unsubscribe mode' do - it 'confirms and destroys subscription when challenge matches' do - stub_random_value - stub_request(:get, url_for_mode('unsubscribe')) - .with(headers: http_headers) - .to_return(status: 200, body: challenge_value, headers: {}) - - seconds = 10.days.seconds.to_i - subject.perform(subscription.id, 'unsubscribe', 'asdf', seconds) - - expect { subscription.reload }.to raise_error(ActiveRecord::RecordNotFound) - end - - it 'does not destroy subscription when challenge does not match' do - stub_random_value - stub_request(:get, url_for_mode('unsubscribe')) - .with(headers: http_headers) - .to_return(status: 200, body: 'wrong value', headers: {}) - - seconds = 10.days.seconds.to_i - subject.perform(subscription.id, 'unsubscribe', 'asdf', seconds) - - expect { subscription.reload }.not_to raise_error - end - end - end - - def url_for_mode(mode) - "http://example.com/api?hub.challenge=#{challenge_value}&hub.lease_seconds=863999&hub.mode=#{mode}&hub.topic=https://#{Rails.configuration.x.local_domain}/users/alice.atom" - end - - def stub_random_value - allow(SecureRandom).to receive(:hex).and_return(challenge_value) - end - - def challenge_value - '1a2s3d4f' - end - - def http_headers - { 'Connection' => 'close', 'Host' => 'example.com' } - end -end diff --git a/spec/workers/pubsubhubbub/delivery_worker_spec.rb b/spec/workers/pubsubhubbub/delivery_worker_spec.rb deleted file mode 100644 index c0e0d51866..0000000000 --- a/spec/workers/pubsubhubbub/delivery_worker_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Pubsubhubbub::DeliveryWorker do - include RoutingHelper - subject { described_class.new } - - let(:payload) { 'test' } - - describe 'perform' do - it 'raises when subscription does not exist' do - expect { subject.perform 123, payload }.to raise_error(ActiveRecord::RecordNotFound) - end - - it 'does not attempt to deliver when domain blocked' do - _domain_block = Fabricate(:domain_block, domain: 'example.com', severity: :suspend) - subscription = Fabricate(:subscription, callback_url: 'https://example.com/api', last_successful_delivery_at: 2.days.ago) - - subject.perform(subscription.id, payload) - - expect(subscription.reload.last_successful_delivery_at).to be_within(2).of(2.days.ago) - end - - it 'raises when request fails' do - subscription = Fabricate(:subscription) - - stub_request_to_respond_with(subscription, 500) - expect { subject.perform(subscription.id, payload) }.to raise_error Mastodon::UnexpectedResponseError - end - - it 'updates subscriptions when delivery succeeds' do - subscription = Fabricate(:subscription) - - stub_request_to_respond_with(subscription, 200) - subject.perform(subscription.id, payload) - - expect(subscription.reload.last_successful_delivery_at).to be_within(2).of(Time.now.utc) - end - - it 'updates subscription without a secret when delivery succeeds' do - subscription = Fabricate(:subscription, secret: nil) - - stub_request_to_respond_with(subscription, 200) - subject.perform(subscription.id, payload) - - expect(subscription.reload.last_successful_delivery_at).to be_within(2).of(Time.now.utc) - end - - def stub_request_to_respond_with(subscription, code) - stub_request(:post, 'http://example.com/callback') - .with(body: payload, headers: expected_headers(subscription)) - .to_return(status: code, body: '', headers: {}) - end - - def expected_headers(subscription) - { - 'Connection' => 'close', - 'Content-Type' => 'application/atom+xml', - 'Host' => 'example.com', - 'Link' => "; rel=\"hub\", ; rel=\"self\"", - }.tap do |basic| - known_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), subscription.secret.to_s, payload) - basic.merge('X-Hub-Signature' => "sha1=#{known_digest}") if subscription.secret? - end - end - end -end diff --git a/spec/workers/pubsubhubbub/distribution_worker_spec.rb b/spec/workers/pubsubhubbub/distribution_worker_spec.rb deleted file mode 100644 index 5844850795..0000000000 --- a/spec/workers/pubsubhubbub/distribution_worker_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'rails_helper' - -describe Pubsubhubbub::DistributionWorker do - subject { Pubsubhubbub::DistributionWorker.new } - - let!(:alice) { Fabricate(:account, username: 'alice') } - let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example2.com') } - let!(:anonymous_subscription) { Fabricate(:subscription, account: alice, callback_url: 'http://example1.com', confirmed: true, lease_seconds: 3600) } - let!(:subscription_with_follower) { Fabricate(:subscription, account: alice, callback_url: 'http://example2.com', confirmed: true, lease_seconds: 3600) } - - before do - bob.follow!(alice) - end - - describe 'with public status' do - let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :public) } - - it 'delivers payload to all subscriptions' do - allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk) - subject.perform(status.stream_entry.id) - expect(Pubsubhubbub::DeliveryWorker).to have_received(:push_bulk).with([anonymous_subscription.id, subscription_with_follower.id]) - end - end - - context 'when OStatus privacy is not used' do - describe 'with private status' do - let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :private) } - - it 'does not deliver anything' do - allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk) - subject.perform(status.stream_entry.id) - expect(Pubsubhubbub::DeliveryWorker).to_not have_received(:push_bulk) - end - end - - describe 'with direct status' do - let(:status) { Fabricate(:status, account: alice, text: 'Hello', visibility: :direct) } - - it 'does not deliver payload' do - allow(Pubsubhubbub::DeliveryWorker).to receive(:push_bulk) - subject.perform(status.stream_entry.id) - expect(Pubsubhubbub::DeliveryWorker).to_not have_received(:push_bulk) - end - end - end -end diff --git a/spec/workers/scheduler/subscriptions_scheduler_spec.rb b/spec/workers/scheduler/subscriptions_scheduler_spec.rb deleted file mode 100644 index a7d1046de6..0000000000 --- a/spec/workers/scheduler/subscriptions_scheduler_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'rails_helper' - -describe Scheduler::SubscriptionsScheduler do - subject { Scheduler::SubscriptionsScheduler.new } - - let!(:expiring_account1) { Fabricate(:account, subscription_expires_at: 20.minutes.from_now, domain: 'example.com', followers_count: 1, hub_url: 'http://hub.example.com') } - let!(:expiring_account2) { Fabricate(:account, subscription_expires_at: 4.hours.from_now, domain: 'example.org', followers_count: 1, hub_url: 'http://hub.example.org') } - - before do - stub_request(:post, 'http://hub.example.com/').to_return(status: 202) - stub_request(:post, 'http://hub.example.org/').to_return(status: 202) - end - - it 're-subscribes for all expiring accounts' do - subject.perform - expect(a_request(:post, 'http://hub.example.com/')).to have_been_made.once - expect(a_request(:post, 'http://hub.example.org/')).to have_been_made.once - end -end From 58276715be8a7e6b518ebd33cd2d4fd82ae81b2c Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 7 Jul 2019 02:05:38 +0200 Subject: [PATCH 02/71] Fix support for HTTP proxies (#11245) * Disable incorrect check for hidden services in Socket Hidden services can only be accessed with an HTTP proxy, in which case the host seen by the Socket class will be the proxy, not the target host. Hidden services are already filtered in `Request#initialize`. * Use our Socket class to connect to HTTP proxies Avoid the timeout logic being bypassed * Add support for IP addresses in Request::Socket * Refactor a bit, no need to keep the DNS resolver around --- app/lib/request.rb | 83 +++++++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 35 deletions(-) diff --git a/app/lib/request.rb b/app/lib/request.rb index e25b9026c7..5f7075a3c9 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -30,7 +30,8 @@ class Request @verb = verb @url = Addressable::URI.parse(url).normalize @http_client = options.delete(:http_client) - @options = options.merge(use_proxy? ? Rails.configuration.x.http_client_proxy : { socket_class: Socket }) + @options = options.merge(socket_class: use_proxy? ? ProxySocket : Socket) + @options = @options.merge(Rails.configuration.x.http_client_proxy) if use_proxy? @headers = {} raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if block_hidden_service? @@ -177,47 +178,49 @@ class Request class Socket < TCPSocket class << self def open(host, *args) - return super(host, *args) if thru_hidden_service?(host) - outer_e = nil port = args.first - Resolv::DNS.open do |dns| - dns.timeouts = 5 + addresses = [] + begin + addresses = [IPAddr.new(host)] + rescue IPAddr::InvalidAddressError + Resolv::DNS.open do |dns| + dns.timeouts = 5 + addresses = dns.getaddresses(host).take(2) + end + end - addresses = dns.getaddresses(host).take(2) + addresses.each do |address| + begin + check_private_address(address) + + sock = ::Socket.new(address.is_a?(Resolv::IPv6) ? ::Socket::AF_INET6 : ::Socket::AF_INET, ::Socket::SOCK_STREAM, 0) + sockaddr = ::Socket.pack_sockaddr_in(port, address.to_s) + + sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1) - addresses.each do |address| begin - raise Mastodon::HostValidationError if PrivateAddressCheck.private_address?(IPAddr.new(address.to_s)) - - sock = ::Socket.new(address.is_a?(Resolv::IPv6) ? ::Socket::AF_INET6 : ::Socket::AF_INET, ::Socket::SOCK_STREAM, 0) - sockaddr = ::Socket.pack_sockaddr_in(port, address.to_s) - - sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1) - - begin - sock.connect_nonblock(sockaddr) - rescue IO::WaitWritable - if IO.select(nil, [sock], nil, Request::TIMEOUT[:connect]) - begin - sock.connect_nonblock(sockaddr) - rescue Errno::EISCONN - # Yippee! - rescue - sock.close - raise - end - else + sock.connect_nonblock(sockaddr) + rescue IO::WaitWritable + if IO.select(nil, [sock], nil, Request::TIMEOUT[:connect]) + begin + sock.connect_nonblock(sockaddr) + rescue Errno::EISCONN + # Yippee! + rescue sock.close - raise HTTP::TimeoutError, "Connect timed out after #{Request::TIMEOUT[:connect]} seconds" + raise end + else + sock.close + raise HTTP::TimeoutError, "Connect timed out after #{Request::TIMEOUT[:connect]} seconds" end - - return sock - rescue => e - outer_e = e end + + return sock + rescue => e + outer_e = e end end @@ -230,11 +233,21 @@ class Request alias new open - def thru_hidden_service?(host) - Rails.configuration.x.access_to_hidden_service && /\.(onion|i2p)$/.match(host) + def check_private_address(address) + raise Mastodon::HostValidationError if PrivateAddressCheck.private_address?(IPAddr.new(address.to_s)) end end end - private_constant :ClientLimit, :Socket + class ProxySocket < Socket + class << self + def check_private_address(_address) + # Accept connections to private addresses as HTTP proxies will usually + # be on local addresses + nil + end + end + end + + private_constant :ClientLimit, :Socket, :ProxySocket end From 406b46395d6f79e87b286585f6b6867374d198c1 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 7 Jul 2019 03:37:01 +0200 Subject: [PATCH 03/71] Fix URLs appearing twice in errors of ActivityPub::DeliveryWorker (#11231) --- app/lib/request.rb | 2 +- app/workers/activitypub/delivery_worker.rb | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/lib/request.rb b/app/lib/request.rb index 5f7075a3c9..322457ad7e 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -59,7 +59,7 @@ class Request begin response = http_client.public_send(@verb, @url.to_s, @options.merge(headers: headers)) rescue => e - raise e.class, "#{e.message} on #{@url}", e.backtrace[0] + raise e.class, "#{e.message} on #{@url}", e.backtrace end begin diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb index 818fd8f5d9..8b52b8e490 100644 --- a/app/workers/activitypub/delivery_worker.rb +++ b/app/workers/activitypub/delivery_worker.rb @@ -18,13 +18,15 @@ class ActivityPub::DeliveryWorker @source_account = Account.find(source_account_id) @inbox_url = inbox_url @host = Addressable::URI.parse(inbox_url).normalized_site + @performed = false perform_request - - failure_tracker.track_success! - rescue => e - failure_tracker.track_failure! - raise e.class, "Delivery failed for #{inbox_url}: #{e.message}", e.backtrace[0] + ensure + if @performed + failure_tracker.track_success! + else + failure_tracker.track_failure! + end end private @@ -40,6 +42,8 @@ class ActivityPub::DeliveryWorker request_pool.with(@host) do |http_client| build_request(http_client).perform do |response| raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) + + @performed = true end end end From b8514561394767a10d3cf40132ada24d938c1680 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 7 Jul 2019 16:16:51 +0200 Subject: [PATCH 04/71] Remove Atom feeds and old URLs in the form of `GET /:username/updates/:id` (#11247) --- app/controllers/accounts_controller.rb | 7 - app/controllers/statuses_controller.rb | 36 +- app/controllers/stream_entries_controller.rb | 64 - app/helpers/admin/action_logs_helper.rb | 2 +- app/helpers/home_helper.rb | 2 +- ...m_entries_helper.rb => statuses_helper.rb} | 4 +- app/javascript/styles/application.scss | 2 +- .../{stream_entries.scss => statuses.scss} | 0 app/lib/formatter.rb | 2 +- app/lib/ostatus/atom_serializer.rb | 376 ----- app/lib/status_finder.rb | 2 - app/lib/tag_manager.rb | 11 - app/mailers/admin_mailer.rb | 2 +- app/mailers/notification_mailer.rb | 2 +- app/models/concerns/account_associations.rb | 1 - app/models/concerns/streamable.rb | 43 - app/models/remote_profile.rb | 57 - app/models/status.rb | 4 - app/models/stream_entry.rb | 59 - app/serializers/rest/account_serializer.rb | 2 +- app/serializers/rest/status_serializer.rb | 6 +- app/serializers/rss/account_serializer.rb | 6 +- app/serializers/rss/tag_serializer.rb | 4 +- app/services/batched_remove_status_service.rb | 2 +- app/services/fetch_link_card_service.rb | 2 +- app/services/process_mentions_service.rb | 2 +- app/services/remove_status_service.rb | 15 +- app/services/resolve_url_service.rb | 5 +- app/services/suspend_account_service.rb | 1 - app/views/accounts/_moved.html.haml | 4 +- app/views/accounts/show.html.haml | 4 +- app/views/admin/accounts/_account.html.haml | 2 +- app/views/admin/reports/_status.html.haml | 2 +- app/views/application/_card.html.haml | 2 +- .../_post_follow_actions.html.haml | 2 +- app/views/remote_interaction/new.html.haml | 2 +- app/views/remote_unfollows/_card.html.haml | 2 +- .../_post_follow_actions.html.haml | 2 +- .../_attachment_list.html.haml | 0 .../_detailed_status.html.haml | 10 +- .../_og_description.html.haml | 0 .../_og_image.html.haml | 0 .../_poll.html.haml | 0 .../_simple_status.html.haml | 10 +- .../_status.html.haml | 12 +- app/views/statuses/embed.html.haml | 3 + app/views/statuses/show.html.haml | 24 + app/views/stream_entries/embed.html.haml | 3 - app/views/stream_entries/show.html.haml | 25 - config/routes.rb | 6 - .../20190706233204_drop_stream_entries.rb | 13 + db/schema.rb | 14 +- spec/controllers/accounts_controller_spec.rb | 31 - .../controllers/api/oembed_controller_spec.rb | 2 +- .../application_controller_spec.rb | 4 - spec/controllers/statuses_controller_spec.rb | 16 +- .../stream_entries_controller_spec.rb | 95 -- spec/fabricators/stream_entry_fabricator.rb | 5 - .../account_moderation_notes_helper_spec.rb | 2 +- ...helper_spec.rb => statuses_helper_spec.rb} | 10 +- spec/lib/activitypub/tag_manager_spec.rb | 6 - spec/lib/ostatus/atom_serializer_spec.rb | 1415 ----------------- spec/lib/status_finder_spec.rb | 9 - spec/lib/tag_manager_spec.rb | 42 - spec/models/concerns/streamable_spec.rb | 63 - spec/models/remote_profile_spec.rb | 143 -- spec/models/stream_entry_spec.rb | 192 --- .../services/process_mentions_service_spec.rb | 4 +- spec/services/suspend_account_service_spec.rb | 6 +- .../show.html.haml_spec.rb | 13 +- 70 files changed, 130 insertions(+), 2791 deletions(-) delete mode 100644 app/controllers/stream_entries_controller.rb rename app/helpers/{stream_entries_helper.rb => statuses_helper.rb} (99%) rename app/javascript/styles/mastodon/{stream_entries.scss => statuses.scss} (100%) delete mode 100644 app/lib/ostatus/atom_serializer.rb delete mode 100644 app/models/concerns/streamable.rb delete mode 100644 app/models/remote_profile.rb delete mode 100644 app/models/stream_entry.rb rename app/views/{stream_entries => statuses}/_attachment_list.html.haml (100%) rename app/views/{stream_entries => statuses}/_detailed_status.html.haml (87%) rename app/views/{stream_entries => statuses}/_og_description.html.haml (100%) rename app/views/{stream_entries => statuses}/_og_image.html.haml (100%) rename app/views/{stream_entries => statuses}/_poll.html.haml (100%) rename app/views/{stream_entries => statuses}/_simple_status.html.haml (86%) rename app/views/{stream_entries => statuses}/_status.html.haml (74%) create mode 100644 app/views/statuses/embed.html.haml create mode 100644 app/views/statuses/show.html.haml delete mode 100644 app/views/stream_entries/embed.html.haml delete mode 100644 app/views/stream_entries/show.html.haml create mode 100644 db/post_migrate/20190706233204_drop_stream_entries.rb delete mode 100644 spec/controllers/stream_entries_controller_spec.rb delete mode 100644 spec/fabricators/stream_entry_fabricator.rb rename spec/helpers/{stream_entries_helper_spec.rb => statuses_helper_spec.rb} (94%) delete mode 100644 spec/lib/ostatus/atom_serializer_spec.rb delete mode 100644 spec/models/concerns/streamable_spec.rb delete mode 100644 spec/models/remote_profile_spec.rb delete mode 100644 spec/models/stream_entry_spec.rb rename spec/views/{stream_entries => statuses}/show.html.haml_spec.rb (83%) diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 73a4b1859c..0657073780 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -31,13 +31,6 @@ class AccountsController < ApplicationController end end - format.atom do - mark_cacheable! - - @entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]) - render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? })) - end - format.rss do mark_cacheable! diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index ef26691b29..776099ca87 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -33,12 +33,10 @@ class StatusesController < ApplicationController set_ancestors set_descendants - - render 'stream_entries/show' end format.json do - render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do + render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: @status.distributable?) do ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter) end end @@ -46,7 +44,7 @@ class StatusesController < ApplicationController end def activity - render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do + render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: @status.distributable?) do ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter) end end @@ -58,7 +56,7 @@ class StatusesController < ApplicationController response.headers['X-Frame-Options'] = 'ALLOWALL' @autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay]) - render 'stream_entries/embed', layout: 'embedded' + render layout: 'embedded' end def replies @@ -92,11 +90,20 @@ class StatusesController < ApplicationController def create_descendant_thread(starting_depth, statuses) depth = starting_depth + statuses.size + if depth < DESCENDANTS_DEPTH_LIMIT - { statuses: statuses, starting_depth: starting_depth } + { + statuses: statuses, + starting_depth: starting_depth, + } else next_status = statuses.pop - { statuses: statuses, starting_depth: starting_depth, next_status: next_status } + + { + statuses: statuses, + starting_depth: starting_depth, + next_status: next_status, + } end end @@ -164,22 +171,13 @@ class StatusesController < ApplicationController end def set_link_headers - response.headers['Link'] = LinkHeader.new( - [ - [account_stream_entry_url(@account, @status.stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]], - [ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]], - ] - ) + response.headers['Link'] = LinkHeader.new([[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]]) end def set_status - @status = @account.statuses.find(params[:id]) - @stream_entry = @status.stream_entry - @type = @stream_entry.activity_type.downcase - + @status = @account.statuses.find(params[:id]) authorize @status, :show? rescue Mastodon::NotPermittedError - # Reraise in order to get a 404 raise ActiveRecord::RecordNotFound end @@ -192,7 +190,7 @@ class StatusesController < ApplicationController end def redirect_to_original - redirect_to ::TagManager.instance.url_for(@status.reblog) if @status.reblog? + redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog? end def set_referrer_policy_header diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb deleted file mode 100644 index 0f7e9e0f56..0000000000 --- a/app/controllers/stream_entries_controller.rb +++ /dev/null @@ -1,64 +0,0 @@ -# frozen_string_literal: true - -class StreamEntriesController < ApplicationController - include Authorization - include SignatureVerification - - layout 'public' - - before_action :set_account - before_action :set_stream_entry - before_action :set_link_headers - before_action :check_account_suspension - before_action :set_cache_headers - - def show - respond_to do |format| - format.html do - expires_in 5.minutes, public: true unless @stream_entry.hidden? - - redirect_to short_account_status_url(params[:account_username], @stream_entry.activity) - end - - format.atom do - expires_in 3.minutes, public: true unless @stream_entry.hidden? - - render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true)) - end - end - end - - def embed - redirect_to embed_short_account_status_url(@account, @stream_entry.activity), status: 301 - end - - private - - def set_account - @account = Account.find_local!(params[:account_username]) - end - - def set_link_headers - response.headers['Link'] = LinkHeader.new( - [ - [account_stream_entry_url(@account, @stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]], - [ActivityPub::TagManager.instance.uri_for(@stream_entry.activity), [%w(rel alternate), %w(type application/activity+json)]], - ] - ) - end - - def set_stream_entry - @stream_entry = @account.stream_entries.where(activity_type: 'Status').find(params[:id]) - @type = 'status' - - raise ActiveRecord::RecordNotFound if @stream_entry.activity.nil? - authorize @stream_entry.activity, :show? if @stream_entry.hidden? - rescue Mastodon::NotPermittedError - # Reraise in order to get a 404 - raise ActiveRecord::RecordNotFound - end - - def check_account_suspension - gone if @account.suspended? - end -end diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index e5fbb1500e..1daa607745 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -89,7 +89,7 @@ module Admin::ActionLogsHelper when 'DomainBlock', 'EmailDomainBlock' link_to record.domain, "https://#{record.domain}" when 'Status' - link_to record.account.acct, TagManager.instance.url_for(record) + link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record) when 'AccountWarning' link_to record.target_account.acct, admin_account_path(record.target_account_id) end diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index df60b7dd7a..b66e827fee 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -21,7 +21,7 @@ module HomeHelper end end else - link_to(path || TagManager.instance.url_for(account), class: 'account__display-name') do + link_to(path || ActivityPub::TagManager.instance.url_for(account), class: 'account__display-name') do content_tag(:div, class: 'account__avatar-wrapper') do content_tag(:div, '', class: 'account__avatar', style: "width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px; background-image: url(#{full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url)})") end + diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/statuses_helper.rb similarity index 99% rename from app/helpers/stream_entries_helper.rb rename to app/helpers/statuses_helper.rb index 02a860a748..e067380f6e 100644 --- a/app/helpers/stream_entries_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module StreamEntriesHelper +module StatusesHelper EMBEDDED_CONTROLLER = 'statuses' EMBEDDED_ACTION = 'embed' @@ -109,11 +109,13 @@ module StreamEntriesHelper def status_text_summary(status) return if status.spoiler_text.blank? + I18n.t('statuses.content_warning', warning: status.spoiler_text) end def poll_summary(status) return unless status.preloadable_poll + status.preloadable_poll.options.map { |o| "[ ] #{o}" }.join("\n") end diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index 6db3bc3dc9..8ebc45b62d 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -13,7 +13,7 @@ @import 'mastodon/widgets'; @import 'mastodon/forms'; @import 'mastodon/accounts'; -@import 'mastodon/stream_entries'; +@import 'mastodon/statuses'; @import 'mastodon/boost'; @import 'mastodon/components'; @import 'mastodon/polls'; diff --git a/app/javascript/styles/mastodon/stream_entries.scss b/app/javascript/styles/mastodon/statuses.scss similarity index 100% rename from app/javascript/styles/mastodon/stream_entries.scss rename to app/javascript/styles/mastodon/statuses.scss diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 8a1aad41a7..6c12399637 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -295,6 +295,6 @@ class Formatter end def mention_html(account) - "@#{encode(account.username)}" + "@#{encode(account.username)}" end end diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb deleted file mode 100644 index f5c0e85cae..0000000000 --- a/app/lib/ostatus/atom_serializer.rb +++ /dev/null @@ -1,376 +0,0 @@ -# frozen_string_literal: true - -class OStatus::AtomSerializer - include RoutingHelper - include ActionView::Helpers::SanitizeHelper - - class << self - def render(element) - document = Ox::Document.new(version: '1.0') - document << element - ('' + Ox.dump(element, effort: :tolerant)).force_encoding('UTF-8') - end - end - - def author(account) - author = Ox::Element.new('author') - - uri = OStatus::TagManager.instance.uri_for(account) - - append_element(author, 'id', uri) - append_element(author, 'activity:object-type', OStatus::TagManager::TYPES[:person]) - append_element(author, 'uri', uri) - append_element(author, 'name', account.username) - append_element(author, 'email', account.local? ? account.local_username_and_domain : account.acct) - append_element(author, 'summary', Formatter.instance.simplified_format(account).to_str, type: :html) if account.note? - append_element(author, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(account)) - append_element(author, 'link', nil, rel: :avatar, type: account.avatar_content_type, 'media:width': 120, 'media:height': 120, href: full_asset_url(account.avatar.url(:original))) if account.avatar? - append_element(author, 'link', nil, rel: :header, type: account.header_content_type, 'media:width': 700, 'media:height': 335, href: full_asset_url(account.header.url(:original))) if account.header? - account.emojis.each do |emoji| - append_element(author, 'link', nil, rel: :emoji, href: full_asset_url(emoji.image.url), name: emoji.shortcode) - end - append_element(author, 'poco:preferredUsername', account.username) - append_element(author, 'poco:displayName', account.display_name) if account.display_name? - append_element(author, 'poco:note', account.local? ? account.note : strip_tags(account.note)) if account.note? - append_element(author, 'mastodon:scope', account.locked? ? :private : :public) - - author - end - - def feed(account, stream_entries) - feed = Ox::Element.new('feed') - - add_namespaces(feed) - - append_element(feed, 'id', account_url(account, format: 'atom')) - append_element(feed, 'title', account.display_name.presence || account.username) - append_element(feed, 'subtitle', account.note) - append_element(feed, 'updated', account.updated_at.iso8601) - append_element(feed, 'logo', full_asset_url(account.avatar.url(:original))) - - feed << author(account) - - append_element(feed, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(account)) - append_element(feed, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_url(account, format: 'atom')) - append_element(feed, 'link', nil, rel: :next, type: 'application/atom+xml', href: account_url(account, format: 'atom', max_id: stream_entries.last.id)) if stream_entries.size == 20 - - stream_entries.each do |stream_entry| - feed << entry(stream_entry) - end - - feed - end - - def entry(stream_entry, root = false) - entry = Ox::Element.new('entry') - - add_namespaces(entry) if root - - append_element(entry, 'id', OStatus::TagManager.instance.uri_for(stream_entry.status)) - append_element(entry, 'published', stream_entry.created_at.iso8601) - append_element(entry, 'updated', stream_entry.updated_at.iso8601) - append_element(entry, 'title', stream_entry&.status&.title || "#{stream_entry.account.acct} deleted status") - - entry << author(stream_entry.account) if root - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[stream_entry.object_type]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[stream_entry.verb]) - - entry << object(stream_entry.target) if stream_entry.targeted? - - if stream_entry.status.nil? - append_element(entry, 'content', 'Deleted status') - elsif stream_entry.status.destroyed? - append_element(entry, 'content', 'Deleted status') - append_element(entry, 'link', nil, rel: :alternate, type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(stream_entry.status)) if stream_entry.account.local? - else - serialize_status_attributes(entry, stream_entry.status) - end - - append_element(entry, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(stream_entry.status)) - append_element(entry, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_stream_entry_url(stream_entry.account, stream_entry, format: 'atom')) - append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(stream_entry.thread), href: ::TagManager.instance.url_for(stream_entry.thread)) if stream_entry.threaded? - append_element(entry, 'ostatus:conversation', nil, ref: conversation_uri(stream_entry.status.conversation)) unless stream_entry&.status&.conversation_id.nil? - - entry - end - - def object(status) - object = Ox::Element.new('activity:object') - - append_element(object, 'id', OStatus::TagManager.instance.uri_for(status)) - append_element(object, 'published', status.created_at.iso8601) - append_element(object, 'updated', status.updated_at.iso8601) - append_element(object, 'title', status.title) - - object << author(status.account) - - append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[status.object_type]) - append_element(object, 'activity:verb', OStatus::TagManager::VERBS[status.verb]) - - serialize_status_attributes(object, status) - - append_element(object, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(status)) - append_element(object, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(status.thread), href: ::TagManager.instance.url_for(status.thread)) unless status.thread.nil? - append_element(object, 'ostatus:conversation', nil, ref: conversation_uri(status.conversation)) unless status.conversation_id.nil? - - object - end - - def follow_salmon(follow) - entry = Ox::Element.new('entry') - add_namespaces(entry) - - description = "#{follow.account.acct} started following #{follow.target_account.acct}" - - append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(follow.created_at, follow.id, 'Follow')) - append_element(entry, 'title', description) - append_element(entry, 'content', description, type: :html) - - entry << author(follow.account) - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:follow]) - - object = author(follow.target_account) - object.value = 'activity:object' - - entry << object - entry - end - - def follow_request_salmon(follow_request) - entry = Ox::Element.new('entry') - add_namespaces(entry) - - append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(follow_request.created_at, follow_request.id, 'FollowRequest')) - append_element(entry, 'title', "#{follow_request.account.acct} requested to follow #{follow_request.target_account.acct}") - - entry << author(follow_request.account) - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:request_friend]) - - object = author(follow_request.target_account) - object.value = 'activity:object' - - entry << object - entry - end - - def authorize_follow_request_salmon(follow_request) - entry = Ox::Element.new('entry') - add_namespaces(entry) - - append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest')) - append_element(entry, 'title', "#{follow_request.target_account.acct} authorizes follow request by #{follow_request.account.acct}") - - entry << author(follow_request.target_account) - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:authorize]) - - object = Ox::Element.new('activity:object') - object << author(follow_request.account) - - append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(object, 'activity:verb', OStatus::TagManager::VERBS[:request_friend]) - - inner_object = author(follow_request.target_account) - inner_object.value = 'activity:object' - - object << inner_object - entry << object - entry - end - - def reject_follow_request_salmon(follow_request) - entry = Ox::Element.new('entry') - add_namespaces(entry) - - append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, follow_request.id, 'FollowRequest')) - append_element(entry, 'title', "#{follow_request.target_account.acct} rejects follow request by #{follow_request.account.acct}") - - entry << author(follow_request.target_account) - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:reject]) - - object = Ox::Element.new('activity:object') - object << author(follow_request.account) - - append_element(object, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(object, 'activity:verb', OStatus::TagManager::VERBS[:request_friend]) - - inner_object = author(follow_request.target_account) - inner_object.value = 'activity:object' - - object << inner_object - entry << object - entry - end - - def unfollow_salmon(follow) - entry = Ox::Element.new('entry') - add_namespaces(entry) - - description = "#{follow.account.acct} is no longer following #{follow.target_account.acct}" - - append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, follow.id, 'Follow')) - append_element(entry, 'title', description) - append_element(entry, 'content', description, type: :html) - - entry << author(follow.account) - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unfollow]) - - object = author(follow.target_account) - object.value = 'activity:object' - - entry << object - entry - end - - def block_salmon(block) - entry = Ox::Element.new('entry') - add_namespaces(entry) - - description = "#{block.account.acct} no longer wishes to interact with #{block.target_account.acct}" - - append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block')) - append_element(entry, 'title', description) - - entry << author(block.account) - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:block]) - - object = author(block.target_account) - object.value = 'activity:object' - - entry << object - entry - end - - def unblock_salmon(block) - entry = Ox::Element.new('entry') - add_namespaces(entry) - - description = "#{block.account.acct} no longer blocks #{block.target_account.acct}" - - append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, block.id, 'Block')) - append_element(entry, 'title', description) - - entry << author(block.account) - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unblock]) - - object = author(block.target_account) - object.value = 'activity:object' - - entry << object - entry - end - - def favourite_salmon(favourite) - entry = Ox::Element.new('entry') - add_namespaces(entry) - - description = "#{favourite.account.acct} favourited a status by #{favourite.status.account.acct}" - - append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(favourite.created_at, favourite.id, 'Favourite')) - append_element(entry, 'title', description) - append_element(entry, 'content', description, type: :html) - - entry << author(favourite.account) - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:favorite]) - - entry << object(favourite.status) - - append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(favourite.status), href: ::TagManager.instance.url_for(favourite.status)) - - entry - end - - def unfavourite_salmon(favourite) - entry = Ox::Element.new('entry') - add_namespaces(entry) - - description = "#{favourite.account.acct} no longer favourites a status by #{favourite.status.account.acct}" - - append_element(entry, 'id', OStatus::TagManager.instance.unique_tag(Time.now.utc, favourite.id, 'Favourite')) - append_element(entry, 'title', description) - append_element(entry, 'content', description, type: :html) - - entry << author(favourite.account) - - append_element(entry, 'activity:object-type', OStatus::TagManager::TYPES[:activity]) - append_element(entry, 'activity:verb', OStatus::TagManager::VERBS[:unfavorite]) - - entry << object(favourite.status) - - append_element(entry, 'thr:in-reply-to', nil, ref: OStatus::TagManager.instance.uri_for(favourite.status), href: ::TagManager.instance.url_for(favourite.status)) - - entry - end - - private - - def append_element(parent, name, content = nil, **attributes) - element = Ox::Element.new(name) - attributes.each { |k, v| element[k] = sanitize_str(v) } - element << sanitize_str(content) unless content.nil? - parent << element - end - - def sanitize_str(raw_str) - raw_str.to_s - end - - def conversation_uri(conversation) - return conversation.uri if conversation.uri? - OStatus::TagManager.instance.unique_tag(conversation.created_at, conversation.id, 'Conversation') - end - - def add_namespaces(parent) - parent['xmlns'] = OStatus::TagManager::XMLNS - parent['xmlns:thr'] = OStatus::TagManager::THR_XMLNS - parent['xmlns:activity'] = OStatus::TagManager::AS_XMLNS - parent['xmlns:poco'] = OStatus::TagManager::POCO_XMLNS - parent['xmlns:media'] = OStatus::TagManager::MEDIA_XMLNS - parent['xmlns:ostatus'] = OStatus::TagManager::OS_XMLNS - parent['xmlns:mastodon'] = OStatus::TagManager::MTDN_XMLNS - end - - def serialize_status_attributes(entry, status) - append_element(entry, 'link', nil, rel: :alternate, type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(status)) if status.account.local? - - append_element(entry, 'summary', status.spoiler_text, 'xml:lang': status.language) if status.spoiler_text? - append_element(entry, 'content', Formatter.instance.format(status, inline_poll_options: true).to_str || '.', type: 'html', 'xml:lang': status.language) - - status.active_mentions.sort_by(&:id).each do |mentioned| - append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:person], href: OStatus::TagManager.instance.uri_for(mentioned.account)) - end - - append_element(entry, 'link', nil, rel: :mentioned, 'ostatus:object-type': OStatus::TagManager::TYPES[:collection], href: OStatus::TagManager::COLLECTIONS[:public]) if status.public_visibility? - - status.tags.each do |tag| - append_element(entry, 'category', nil, term: tag.name) - end - - status.media_attachments.each do |media| - append_element(entry, 'link', nil, rel: :enclosure, type: media.file_content_type, length: media.file_file_size, href: full_asset_url(media.file.url(:original, false))) - end - - append_element(entry, 'category', nil, term: 'nsfw') if status.sensitive? && status.media_attachments.any? - append_element(entry, 'mastodon:scope', status.visibility) - - status.emojis.each do |emoji| - append_element(entry, 'link', nil, rel: :emoji, href: full_asset_url(emoji.image.url), name: emoji.shortcode) - end - end -end diff --git a/app/lib/status_finder.rb b/app/lib/status_finder.rb index 4d1aed2979..22ced8bf82 100644 --- a/app/lib/status_finder.rb +++ b/app/lib/status_finder.rb @@ -13,8 +13,6 @@ class StatusFinder raise ActiveRecord::RecordNotFound unless TagManager.instance.local_url?(url) case recognized_params[:controller] - when 'stream_entries' - StreamEntry.find(recognized_params[:id]).status when 'statuses' Status.find(recognized_params[:id]) else diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb index fb364cb98a..daf4f556b3 100644 --- a/app/lib/tag_manager.rb +++ b/app/lib/tag_manager.rb @@ -33,15 +33,4 @@ class TagManager domain = uri.host + (uri.port ? ":#{uri.port}" : '') TagManager.instance.web_domain?(domain) end - - def url_for(target) - return target.url if target.respond_to?(:local?) && !target.local? - - case target.object_type - when :person - short_account_url(target) - when :note, :comment, :activity - short_account_status_url(target.account, target) - end - end end diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index db154cad53..9ab3e2bbdc 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -3,7 +3,7 @@ class AdminMailer < ApplicationMailer layout 'plain_mailer' - helper :stream_entries + helper :statuses def new_report(recipient, report) @report = report diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 66fa337c1f..723d901fc6 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class NotificationMailer < ApplicationMailer - helper :stream_entries + helper :statuses add_template_helper RoutingHelper diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index 70855e0543..0921e32522 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -11,7 +11,6 @@ module AccountAssociations has_many :identity_proofs, class_name: 'AccountIdentityProof', dependent: :destroy, inverse_of: :account # Timelines - has_many :stream_entries, inverse_of: :account, dependent: :destroy has_many :statuses, inverse_of: :account, dependent: :destroy has_many :favourites, inverse_of: :account, dependent: :destroy has_many :mentions, inverse_of: :account, dependent: :destroy diff --git a/app/models/concerns/streamable.rb b/app/models/concerns/streamable.rb deleted file mode 100644 index 7c9edb8ef8..0000000000 --- a/app/models/concerns/streamable.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -module Streamable - extend ActiveSupport::Concern - - included do - has_one :stream_entry, as: :activity - - after_create do - account.stream_entries.create!(activity: self, hidden: hidden?) if needs_stream_entry? - end - end - - def title - super - end - - def content - title - end - - def target - super - end - - def object_type - :activity - end - - def thread - super - end - - def hidden? - false - end - - private - - def needs_stream_entry? - account.local? - end -end diff --git a/app/models/remote_profile.rb b/app/models/remote_profile.rb deleted file mode 100644 index 742d2b56f4..0000000000 --- a/app/models/remote_profile.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -class RemoteProfile - include ActiveModel::Model - - attr_reader :document - - def initialize(body) - @document = Nokogiri::XML.parse(body, nil, 'utf-8') - end - - def root - @root ||= document.at_xpath('/atom:feed|/atom:entry', atom: OStatus::TagManager::XMLNS) - end - - def author - @author ||= root.at_xpath('./atom:author|./dfrn:owner', atom: OStatus::TagManager::XMLNS, dfrn: OStatus::TagManager::DFRN_XMLNS) - end - - def hub_link - @hub_link ||= link_href_from_xml(root, 'hub') - end - - def display_name - @display_name ||= author.at_xpath('./poco:displayName', poco: OStatus::TagManager::POCO_XMLNS)&.content - end - - def note - @note ||= author.at_xpath('./atom:summary|./poco:note', atom: OStatus::TagManager::XMLNS, poco: OStatus::TagManager::POCO_XMLNS)&.content - end - - def scope - @scope ||= author.at_xpath('./mastodon:scope', mastodon: OStatus::TagManager::MTDN_XMLNS)&.content - end - - def avatar - @avatar ||= link_href_from_xml(author, 'avatar') - end - - def header - @header ||= link_href_from_xml(author, 'header') - end - - def emojis - @emojis ||= author.xpath('./xmlns:link[@rel="emoji"]', xmlns: OStatus::TagManager::XMLNS) - end - - def locked? - scope == 'private' - end - - private - - def link_href_from_xml(xml, type) - xml.at_xpath(%(./atom:link[@rel="#{type}"]/@href), atom: OStatus::TagManager::XMLNS)&.content - end -end diff --git a/app/models/status.rb b/app/models/status.rb index 2258e2d074..906756e850 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -28,7 +28,6 @@ class Status < ApplicationRecord before_destroy :unlink_from_conversations include Paginable - include Streamable include Cacheable include StatusThreadingConcern @@ -61,7 +60,6 @@ class Status < ApplicationRecord has_and_belongs_to_many :preview_cards has_one :notification, as: :activity, dependent: :destroy - has_one :stream_entry, as: :activity, inverse_of: :status has_one :status_stat, inverse_of: :status has_one :poll, inverse_of: :status, dependent: :destroy @@ -106,13 +104,11 @@ class Status < ApplicationRecord :status_stat, :tags, :preview_cards, - :stream_entry, :preloadable_poll, account: :account_stat, active_mentions: { account: :account_stat }, reblog: [ :application, - :stream_entry, :tags, :preview_cards, :media_attachments, diff --git a/app/models/stream_entry.rb b/app/models/stream_entry.rb deleted file mode 100644 index 1a9afc5c7b..0000000000 --- a/app/models/stream_entry.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true -# == Schema Information -# -# Table name: stream_entries -# -# id :bigint(8) not null, primary key -# activity_id :bigint(8) -# activity_type :string -# created_at :datetime not null -# updated_at :datetime not null -# hidden :boolean default(FALSE), not null -# account_id :bigint(8) -# - -class StreamEntry < ApplicationRecord - include Paginable - - belongs_to :account, inverse_of: :stream_entries - belongs_to :activity, polymorphic: true - belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id', inverse_of: :stream_entry - - validates :account, :activity, presence: true - - STATUS_INCLUDES = [:account, :stream_entry, :conversation, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, :conversation, :media_attachments, :tags, mentions: :account], thread: [:stream_entry, :account]].freeze - - default_scope { where(activity_type: 'Status') } - scope :recent, -> { reorder(id: :desc) } - scope :with_includes, -> { includes(:account, status: STATUS_INCLUDES) } - - delegate :target, :title, :content, :thread, - to: :status, - allow_nil: true - - def object_type - orphaned? || targeted? ? :activity : status.object_type - end - - def verb - orphaned? ? :delete : status.verb - end - - def targeted? - [:follow, :request_friend, :authorize, :reject, :unfollow, :block, :unblock, :share, :favorite].include? verb - end - - def threaded? - (verb == :favorite || object_type == :comment) && !thread.nil? - end - - def mentions - orphaned? ? [] : status.active_mentions.map(&:account) - end - - private - - def orphaned? - status.nil? - end -end diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 12adc971cb..272e3eb9c8 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -29,7 +29,7 @@ class REST::AccountSerializer < ActiveModel::Serializer end def url - TagManager.instance.url_for(object) + ActivityPub::TagManager.instance.url_for(object) end def avatar diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index c9b76cb162..2dc4a1b61c 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -58,7 +58,7 @@ class REST::StatusSerializer < ActiveModel::Serializer end def uri - OStatus::TagManager.instance.uri_for(object) + ActivityPub::TagManager.instance.uri_for(object) end def content @@ -66,7 +66,7 @@ class REST::StatusSerializer < ActiveModel::Serializer end def url - TagManager.instance.url_for(object) + ActivityPub::TagManager.instance.url_for(object) end def favourited @@ -132,7 +132,7 @@ class REST::StatusSerializer < ActiveModel::Serializer end def url - TagManager.instance.url_for(object.account) + ActivityPub::TagManager.instance.url_for(object.account) end def acct diff --git a/app/serializers/rss/account_serializer.rb b/app/serializers/rss/account_serializer.rb index 88eca79ed2..278affe136 100644 --- a/app/serializers/rss/account_serializer.rb +++ b/app/serializers/rss/account_serializer.rb @@ -2,7 +2,7 @@ class RSS::AccountSerializer include ActionView::Helpers::NumberHelper - include StreamEntriesHelper + include StatusesHelper include RoutingHelper def render(account, statuses) @@ -10,7 +10,7 @@ class RSS::AccountSerializer builder.title("#{display_name(account)} (@#{account.local_username_and_domain})") .description(account_description(account)) - .link(TagManager.instance.url_for(account)) + .link(ActivityPub::TagManager.instance.url_for(account)) .logo(full_pack_url('media/images/logo.svg')) .accent_color('2b90d9') @@ -20,7 +20,7 @@ class RSS::AccountSerializer statuses.each do |status| builder.item do |item| item.title(status.title) - .link(TagManager.instance.url_for(status)) + .link(ActivityPub::TagManager.instance.url_for(status)) .pub_date(status.created_at) .description(status.spoiler_text.presence || Formatter.instance.format(status, inline_poll_options: true).to_str) diff --git a/app/serializers/rss/tag_serializer.rb b/app/serializers/rss/tag_serializer.rb index 644380149b..e8562ee87d 100644 --- a/app/serializers/rss/tag_serializer.rb +++ b/app/serializers/rss/tag_serializer.rb @@ -3,7 +3,7 @@ class RSS::TagSerializer include ActionView::Helpers::NumberHelper include ActionView::Helpers::SanitizeHelper - include StreamEntriesHelper + include StatusesHelper include RoutingHelper def render(tag, statuses) @@ -18,7 +18,7 @@ class RSS::TagSerializer statuses.each do |status| builder.item do |item| item.title(status.title) - .link(TagManager.instance.url_for(status)) + .link(ActivityPub::TagManager.instance.url_for(status)) .pub_date(status.created_at) .description(status.spoiler_text.presence || Formatter.instance.format(status).to_str) diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index cb66debc8c..27dc460a6b 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -12,7 +12,7 @@ class BatchedRemoveStatusService < BaseService # @param [Hash] options # @option [Boolean] :skip_side_effects def call(statuses, **options) - statuses = Status.where(id: statuses.map(&:id)).includes(:account, :stream_entry).flat_map { |status| [status] + status.reblogs.includes(:account, :stream_entry).to_a } + statuses = Status.where(id: statuses.map(&:id)).includes(:account).flat_map { |status| [status] + status.reblogs.includes(:account).to_a } @mentions = statuses.each_with_object({}) { |s, h| h[s.id] = s.active_mentions.includes(:account).to_a } @tags = statuses.each_with_object({}) { |s, h| h[s.id] = s.tags.pluck(:name) } diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 494aaed759..75fbd0e8c6 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -84,7 +84,7 @@ class FetchLinkCardService < BaseService def mention_link?(a) @status.mentions.any? do |mention| - a['href'] == TagManager.instance.url_for(mention.account) + a['href'] == ActivityPub::TagManager.instance.url_for(mention.account) end end diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index da52bff6a5..90dca9740e 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -40,7 +40,7 @@ class ProcessMentionsService < BaseService private def mention_undeliverable?(mentioned_account) - mentioned_account.nil? || (!mentioned_account.local? && mentioned_account.ostatus? && @status.stream_entry.hidden?) + mentioned_account.nil? || (!mentioned_account.local? && mentioned_account.ostatus?) end def create_notification(mention) diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index a8c9100b33..6311971ffc 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -5,14 +5,13 @@ class RemoveStatusService < BaseService include Payloadable def call(status, **options) - @payload = Oj.dump(event: :delete, payload: status.id.to_s) - @status = status - @account = status.account - @tags = status.tags.pluck(:name).to_a - @mentions = status.active_mentions.includes(:account).to_a - @reblogs = status.reblogs.includes(:account).to_a - @stream_entry = status.stream_entry - @options = options + @payload = Oj.dump(event: :delete, payload: status.id.to_s) + @status = status + @account = status.account + @tags = status.tags.pluck(:name).to_a + @mentions = status.active_mentions.includes(:account).to_a + @reblogs = status.reblogs.includes(:account).to_a + @options = options RedisLock.acquire(lock_options) do |lock| if lock.acquired? diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb index bbdc0a5955..f941b489a5 100644 --- a/app/services/resolve_url_service.rb +++ b/app/services/resolve_url_service.rb @@ -73,10 +73,7 @@ class ResolveURLService < BaseService return unless recognized_params[:action] == 'show' - if recognized_params[:controller] == 'stream_entries' - status = StreamEntry.find_by(id: recognized_params[:id])&.status - check_local_status(status) - elsif recognized_params[:controller] == 'statuses' + if recognized_params[:controller] == 'statuses' status = Status.find_by(id: recognized_params[:id]) check_local_status(status) elsif recognized_params[:controller] == 'accounts' diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index a5ce3dbd9b..0ebe0b562d 100644 --- a/app/services/suspend_account_service.rb +++ b/app/services/suspend_account_service.rb @@ -24,7 +24,6 @@ class SuspendAccountService < BaseService report_notes scheduled_statuses status_pins - stream_entries subscriptions ).freeze diff --git a/app/views/accounts/_moved.html.haml b/app/views/accounts/_moved.html.haml index 7a777bfea8..02fd7bf429 100644 --- a/app/views/accounts/_moved.html.haml +++ b/app/views/accounts/_moved.html.haml @@ -3,10 +3,10 @@ .moved-account-widget .moved-account-widget__message = fa_icon 'suitcase' - = t('accounts.moved_html', name: content_tag(:bdi, content_tag(:strong, display_name(account, custom_emojify: true), class: :emojify)), new_profile_link: link_to(content_tag(:strong, safe_join(['@', content_tag(:span, moved_to_account.acct)])), TagManager.instance.url_for(moved_to_account), class: 'mention')) + = t('accounts.moved_html', name: content_tag(:bdi, content_tag(:strong, display_name(account, custom_emojify: true), class: :emojify)), new_profile_link: link_to(content_tag(:strong, safe_join(['@', content_tag(:span, moved_to_account.acct)])), ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'mention')) .moved-account-widget__card - = link_to TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'me noopener' do + = link_to ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'me noopener' do .detailed-status__display-avatar .account__avatar-overlay .account__avatar-overlay-base{ style: "background-image: url('#{moved_to_account.avatar.url(:original)}')" } diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index de7d2a8ba3..0dc984dcc2 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -39,12 +39,12 @@ - else .activity-stream.activity-stream--under-tabs - if params[:page].to_i.zero? - = render partial: 'stream_entries/status', collection: @pinned_statuses, as: :status, locals: { pinned: true } + = render partial: 'statuses/status', collection: @pinned_statuses, as: :status, locals: { pinned: true } - if @newer_url .entry= link_to_more @newer_url - = render partial: 'stream_entries/status', collection: @statuses, as: :status + = render partial: 'statuses/status', collection: @statuses, as: :status - if @older_url .entry= link_to_more @older_url diff --git a/app/views/admin/accounts/_account.html.haml b/app/views/admin/accounts/_account.html.haml index eba3ad8041..b057d3e42e 100644 --- a/app/views/admin/accounts/_account.html.haml +++ b/app/views/admin/accounts/_account.html.haml @@ -19,4 +19,4 @@ = table_link_to 'times', t('admin.accounts.reject'), reject_admin_account_path(account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:reject, account.user) - else = table_link_to 'circle', t('admin.accounts.web'), web_path("accounts/#{account.id}") - = table_link_to 'globe', t('admin.accounts.public'), TagManager.instance.url_for(account) + = table_link_to 'globe', t('admin.accounts.public'), ActivityPub::TagManager.instance.url_for(account) diff --git a/app/views/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml index b3c145120a..9376db7ffe 100644 --- a/app/views/admin/reports/_status.html.haml +++ b/app/views/admin/reports/_status.html.haml @@ -19,7 +19,7 @@ = react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } .detailed-status__meta - = link_to TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener' do + = link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener' do %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) · - if status.reblog? diff --git a/app/views/application/_card.html.haml b/app/views/application/_card.html.haml index e6059b0350..00254c40c7 100644 --- a/app/views/application/_card.html.haml +++ b/app/views/application/_card.html.haml @@ -1,4 +1,4 @@ -- account_url = local_assigns[:admin] ? admin_account_path(account.id) : TagManager.instance.url_for(account) +- account_url = local_assigns[:admin] ? admin_account_path(account.id) : ActivityPub::TagManager.instance.url_for(account) .card.h-card = link_to account_url, target: '_blank', rel: 'noopener' do diff --git a/app/views/authorize_interactions/_post_follow_actions.html.haml b/app/views/authorize_interactions/_post_follow_actions.html.haml index 561c60137f..dd71160e2d 100644 --- a/app/views/authorize_interactions/_post_follow_actions.html.haml +++ b/app/views/authorize_interactions/_post_follow_actions.html.haml @@ -1,4 +1,4 @@ .post-follow-actions %div= link_to t('authorize_follow.post_follow.web'), web_url("accounts/#{@resource.id}"), class: 'button button--block' - %div= link_to t('authorize_follow.post_follow.return'), TagManager.instance.url_for(@resource), class: 'button button--block' + %div= link_to t('authorize_follow.post_follow.return'), ActivityPub::TagManager.instance.url_for(@resource), class: 'button button--block' %div= t('authorize_follow.post_follow.close') diff --git a/app/views/remote_interaction/new.html.haml b/app/views/remote_interaction/new.html.haml index c8c08991f0..2cc0fcb93d 100644 --- a/app/views/remote_interaction/new.html.haml +++ b/app/views/remote_interaction/new.html.haml @@ -7,7 +7,7 @@ .public-layout .activity-stream.activity-stream--highlighted - = render 'stream_entries/status', status: @status + = render 'statuses/status', status: @status = simple_form_for @remote_follow, as: :remote_follow, url: remote_interaction_path(@status) do |f| = render 'shared/error_messages', object: @remote_follow diff --git a/app/views/remote_unfollows/_card.html.haml b/app/views/remote_unfollows/_card.html.haml index 9abcfd37e1..80ad3bae2d 100644 --- a/app/views/remote_unfollows/_card.html.haml +++ b/app/views/remote_unfollows/_card.html.haml @@ -4,7 +4,7 @@ = image_tag account.avatar.url(:original), alt: '', width: 48, height: 48, class: 'avatar' %span.display-name - - account_url = local_assigns[:admin] ? admin_account_path(account.id) : TagManager.instance.url_for(account) + - account_url = local_assigns[:admin] ? admin_account_path(account.id) : ActivityPub::TagManager.instance.url_for(account) = link_to account_url, class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do %strong.emojify= display_name(account, custom_emojify: true) %span @#{account.acct} diff --git a/app/views/remote_unfollows/_post_follow_actions.html.haml b/app/views/remote_unfollows/_post_follow_actions.html.haml index 2a9c062e9c..328f7c833b 100644 --- a/app/views/remote_unfollows/_post_follow_actions.html.haml +++ b/app/views/remote_unfollows/_post_follow_actions.html.haml @@ -1,4 +1,4 @@ .post-follow-actions %div= link_to t('authorize_follow.post_follow.web'), web_url("accounts/#{@account.id}"), class: 'button button--block' - %div= link_to t('authorize_follow.post_follow.return'), TagManager.instance.url_for(@account), class: 'button button--block' + %div= link_to t('authorize_follow.post_follow.return'), ActivityPub::TagManager.instance.url_for(@account), class: 'button button--block' %div= t('authorize_follow.post_follow.close') diff --git a/app/views/stream_entries/_attachment_list.html.haml b/app/views/statuses/_attachment_list.html.haml similarity index 100% rename from app/views/stream_entries/_attachment_list.html.haml rename to app/views/statuses/_attachment_list.html.haml diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml similarity index 87% rename from app/views/stream_entries/_detailed_status.html.haml rename to app/views/statuses/_detailed_status.html.haml index 069d0053fc..8686c20335 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/statuses/_detailed_status.html.haml @@ -1,6 +1,6 @@ .detailed-status.detailed-status--flex .p-author.h-card - = link_to TagManager.instance.url_for(status.account), class: 'detailed-status__display-name u-url', target: stream_link_target, rel: 'noopener' do + = link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'detailed-status__display-name u-url', target: stream_link_target, rel: 'noopener' do .detailed-status__display-avatar - if current_account&.user&.setting_auto_play_gif || autoplay = image_tag status.account.avatar_original_url, width: 48, height: 48, alt: '', class: 'account__avatar u-photo' @@ -24,23 +24,23 @@ = Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay) - if status.preloadable_poll = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do - = render partial: 'stream_entries/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay } + = render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay } - if !status.media_attachments.empty? - if status.media_attachments.first.audio_or_video? - video = status.media_attachments.first = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do - = render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments } + = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - else = react_component :media_gallery, height: 380, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do - = render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments } + = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - elsif status.preview_card = react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json .detailed-status__meta %data.dt-published{ value: status.created_at.to_time.iso8601 } - = link_to TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener' do + = link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener' do %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) · - if status.application && @account.user&.setting_show_application diff --git a/app/views/stream_entries/_og_description.html.haml b/app/views/statuses/_og_description.html.haml similarity index 100% rename from app/views/stream_entries/_og_description.html.haml rename to app/views/statuses/_og_description.html.haml diff --git a/app/views/stream_entries/_og_image.html.haml b/app/views/statuses/_og_image.html.haml similarity index 100% rename from app/views/stream_entries/_og_image.html.haml rename to app/views/statuses/_og_image.html.haml diff --git a/app/views/stream_entries/_poll.html.haml b/app/views/statuses/_poll.html.haml similarity index 100% rename from app/views/stream_entries/_poll.html.haml rename to app/views/statuses/_poll.html.haml diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml similarity index 86% rename from app/views/stream_entries/_simple_status.html.haml rename to app/views/statuses/_simple_status.html.haml index 0b924f72fb..11220dfcb1 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/statuses/_simple_status.html.haml @@ -1,11 +1,11 @@ .status .status__info - = link_to TagManager.instance.url_for(status), class: 'status__relative-time u-url u-uid', target: stream_link_target, rel: 'noopener' do + = link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__relative-time u-url u-uid', target: stream_link_target, rel: 'noopener' do %time.time-ago{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) %data.dt-published{ value: status.created_at.to_time.iso8601 } .p-author.h-card - = link_to TagManager.instance.url_for(status.account), class: 'status__display-name u-url', target: stream_link_target, rel: 'noopener' do + = link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'status__display-name u-url', target: stream_link_target, rel: 'noopener' do .status__avatar %div - if current_account&.user&.setting_auto_play_gif || autoplay @@ -28,16 +28,16 @@ = Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay) - if status.preloadable_poll = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do - = render partial: 'stream_entries/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay } + = render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay } - if !status.media_attachments.empty? - if status.media_attachments.first.audio_or_video? - video = status.media_attachments.first = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description do - = render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments } + = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - else = react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do - = render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments } + = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments } - elsif status.preview_card = react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json diff --git a/app/views/stream_entries/_status.html.haml b/app/views/statuses/_status.html.haml similarity index 74% rename from app/views/stream_entries/_status.html.haml rename to app/views/statuses/_status.html.haml index 83887cd874..0e36525030 100644 --- a/app/views/stream_entries/_status.html.haml +++ b/app/views/statuses/_status.html.haml @@ -17,9 +17,9 @@ - if status.reply? && include_threads - if @next_ancestor .entry{ class: entry_classes } - = link_to_more TagManager.instance.url_for(@next_ancestor) + = link_to_more ActivityPub::TagManager.instance.url_for(@next_ancestor) - = render partial: 'stream_entries/status', collection: @ancestors, as: :status, locals: { is_predecessor: true, direct_reply_id: status.in_reply_to_id }, autoplay: autoplay + = render partial: 'statuses/status', collection: @ancestors, as: :status, locals: { is_predecessor: true, direct_reply_id: status.in_reply_to_id }, autoplay: autoplay .entry{ class: entry_classes } @@ -28,7 +28,7 @@ .status__prepend-icon-wrapper %i.status__prepend-icon.fa.fa-fw.fa-retweet %span - = link_to TagManager.instance.url_for(status.account), class: 'status__display-name muted' do + = link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'status__display-name muted' do %bdi %strong.emojify= display_name(status.account, custom_emojify: true) = t('stream_entries.reblogged') @@ -39,18 +39,18 @@ %span = t('stream_entries.pinned') - = render (centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status'), status: status.proper, autoplay: autoplay + = render (centered ? 'statuses/detailed_status' : 'statuses/simple_status'), status: status.proper, autoplay: autoplay - if include_threads - if @since_descendant_thread_id .entry{ class: entry_classes } = link_to_more short_account_status_url(status.account.username, status, max_descendant_thread_id: @since_descendant_thread_id + 1) - @descendant_threads.each do |thread| - = render partial: 'stream_entries/status', collection: thread[:statuses], as: :status, locals: { is_successor: true, parent_id: status.id }, autoplay: autoplay + = render partial: 'statuses/status', collection: thread[:statuses], as: :status, locals: { is_successor: true, parent_id: status.id }, autoplay: autoplay - if thread[:next_status] .entry{ class: entry_classes } - = link_to_more TagManager.instance.url_for(thread[:next_status]) + = link_to_more ActivityPub::TagManager.instance.url_for(thread[:next_status]) - if @next_descendant_thread .entry{ class: entry_classes } = link_to_more short_account_status_url(status.account.username, status, since_descendant_thread_id: @max_descendant_thread_id - 1) diff --git a/app/views/statuses/embed.html.haml b/app/views/statuses/embed.html.haml new file mode 100644 index 0000000000..6f2ec646fe --- /dev/null +++ b/app/views/statuses/embed.html.haml @@ -0,0 +1,3 @@ +- cache @status do + .activity-stream.activity-stream--headless + = render 'status', status: @status, centered: true, autoplay: @autoplay diff --git a/app/views/statuses/show.html.haml b/app/views/statuses/show.html.haml new file mode 100644 index 0000000000..704e37a3df --- /dev/null +++ b/app/views/statuses/show.html.haml @@ -0,0 +1,24 @@ +- content_for :page_title do + = t('statuses.title', name: display_name(@account), quote: truncate(@status.spoiler_text.presence || @status.text, length: 50, omission: '…', escape: false)) + +- content_for :header_tags do + - if @account.user&.setting_noindex + %meta{ name: 'robots', content: 'noindex' }/ + + %link{ rel: 'alternate', type: 'application/json+oembed', href: api_oembed_url(url: short_account_status_url(@account, @status), format: 'json') }/ + %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@status) }/ + + = opengraph 'og:site_name', site_title + = opengraph 'og:type', 'article' + = opengraph 'og:title', "#{display_name(@account)} (@#{@account.local_username_and_domain})" + = opengraph 'og:url', short_account_status_url(@account, @status) + + = render 'og_description', activity: @status + = render 'og_image', activity: @status, account: @account + +.grid + .column-0 + .activity-stream.h-entry + = render partial: 'status', locals: { status: @status, include_threads: true } + .column-1 + = render 'application/sidebar' diff --git a/app/views/stream_entries/embed.html.haml b/app/views/stream_entries/embed.html.haml deleted file mode 100644 index 4871c101e1..0000000000 --- a/app/views/stream_entries/embed.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -- cache @stream_entry.activity do - .activity-stream.activity-stream--headless - = render "stream_entries/#{@type}", @type.to_sym => @stream_entry.activity, centered: true, autoplay: @autoplay diff --git a/app/views/stream_entries/show.html.haml b/app/views/stream_entries/show.html.haml deleted file mode 100644 index 0e81c4f685..0000000000 --- a/app/views/stream_entries/show.html.haml +++ /dev/null @@ -1,25 +0,0 @@ -- content_for :page_title do - = t('statuses.title', name: display_name(@account), quote: truncate(@stream_entry.activity.spoiler_text.presence || @stream_entry.activity.text, length: 50, omission: '…', escape: false)) - -- content_for :header_tags do - - if @account.user&.setting_noindex - %meta{ name: 'robots', content: 'noindex' }/ - - %link{ rel: 'alternate', type: 'application/atom+xml', href: account_stream_entry_url(@account, @stream_entry, format: 'atom') }/ - %link{ rel: 'alternate', type: 'application/json+oembed', href: api_oembed_url(url: account_stream_entry_url(@account, @stream_entry), format: 'json') }/ - %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@stream_entry.activity) }/ - - = opengraph 'og:site_name', site_title - = opengraph 'og:type', 'article' - = opengraph 'og:title', "#{display_name(@account)} (@#{@account.local_username_and_domain})" - = opengraph 'og:url', short_account_status_url(@account, @stream_entry.activity) - - = render 'stream_entries/og_description', activity: @stream_entry.activity - = render 'stream_entries/og_image', activity: @stream_entry.activity, account: @account - -.grid - .column-0 - .activity-stream.h-entry - = render partial: "stream_entries/#{@type}", locals: { @type.to_sym => @stream_entry.activity, include_threads: true } - .column-1 - = render 'application/sidebar' diff --git a/config/routes.rb b/config/routes.rb index 4b6d464c6d..69b495a96d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -45,12 +45,6 @@ Rails.application.routes.draw do get '/authorize_follow', to: redirect { |_, request| "/authorize_interaction?#{request.params.to_query}" } resources :accounts, path: 'users', only: [:show], param: :username do - resources :stream_entries, path: 'updates', only: [:show] do - member do - get :embed - end - end - get :remote_follow, to: 'remote_follow#new' post :remote_follow, to: 'remote_follow#create' diff --git a/db/post_migrate/20190706233204_drop_stream_entries.rb b/db/post_migrate/20190706233204_drop_stream_entries.rb new file mode 100644 index 0000000000..1fecece055 --- /dev/null +++ b/db/post_migrate/20190706233204_drop_stream_entries.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class DropStreamEntries < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def up + drop_table :stream_entries + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/schema.rb b/db/schema.rb index 09e6c9faeb..2e38fb1f26 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_06_27_222826) do +ActiveRecord::Schema.define(version: 2019_07_06_233204) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -647,17 +647,6 @@ ActiveRecord::Schema.define(version: 2019_06_27_222826) do t.index ["tag_id", "status_id"], name: "index_statuses_tags_on_tag_id_and_status_id", unique: true end - create_table "stream_entries", force: :cascade do |t| - t.bigint "activity_id" - t.string "activity_type" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "hidden", default: false, null: false - t.bigint "account_id" - t.index ["account_id", "activity_type", "id"], name: "index_stream_entries_on_account_id_and_activity_type_and_id" - t.index ["activity_id", "activity_type"], name: "index_stream_entries_on_activity_id_and_activity_type" - end - create_table "subscriptions", force: :cascade do |t| t.string "callback_url", default: "", null: false t.string "secret" @@ -831,7 +820,6 @@ ActiveRecord::Schema.define(version: 2019_06_27_222826) do add_foreign_key "statuses", "statuses", column: "reblog_of_id", on_delete: :cascade add_foreign_key "statuses_tags", "statuses", on_delete: :cascade add_foreign_key "statuses_tags", "tags", name: "fk_3081861e21", on_delete: :cascade - add_foreign_key "stream_entries", "accounts", name: "fk_5659b17554", on_delete: :cascade add_foreign_key "subscriptions", "accounts", name: "fk_9847d1cbb5", on_delete: :cascade add_foreign_key "tombstones", "accounts", on_delete: :cascade add_foreign_key "user_invite_requests", "users", on_delete: :cascade diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index b728d719f9..3d2a0665da 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -48,37 +48,6 @@ RSpec.describe AccountsController, type: :controller do end end - context 'atom' do - let(:format) { 'atom' } - let(:content_type) { 'application/atom+xml' } - - shared_examples 'responsed streams' do - it 'assigns @entries' do - entries = assigns(:entries).to_a - expect(entries.size).to eq expected_statuses.size - entries.each.zip(expected_statuses.each) do |entry, expected_status| - expect(entry.status).to eq expected_status - end - end - end - - include_examples 'responses' - - context 'without max_id nor since_id' do - let(:expected_statuses) { [status7, status6, status5, status4, status3, status2, status1] } - - include_examples 'responsed streams' - end - - context 'with max_id and since_id' do - let(:max_id) { status4.stream_entry.id } - let(:since_id) { status1.stream_entry.id } - let(:expected_statuses) { [status3, status2] } - - include_examples 'responsed streams' - end - end - context 'activitystreams2' do let(:format) { 'json' } let(:content_type) { 'application/activity+json' } diff --git a/spec/controllers/api/oembed_controller_spec.rb b/spec/controllers/api/oembed_controller_spec.rb index 7fee15a353..b9082bde1e 100644 --- a/spec/controllers/api/oembed_controller_spec.rb +++ b/spec/controllers/api/oembed_controller_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Api::OEmbedController, type: :controller do describe 'GET #show' do before do request.host = Rails.configuration.x.local_domain - get :show, params: { url: account_stream_entry_url(alice, status.stream_entry) }, format: :json + get :show, params: { url: short_account_status_url(alice, status) }, format: :json end it 'returns http success' do diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 33cc710871..27946b60f9 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -360,9 +360,5 @@ describe ApplicationController, type: :controller do context 'Status' do include_examples 'cacheable', :status, Status end - - context 'StreamEntry' do - include_examples 'receives :with_includes', :stream_entry, StreamEntry - end end end diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb index 1bb6636c60..95e5c363c9 100644 --- a/spec/controllers/statuses_controller_spec.rb +++ b/spec/controllers/statuses_controller_spec.rb @@ -55,18 +55,6 @@ describe StatusesController do expect(assigns(:status)).to eq status end - it 'assigns @stream_entry' do - status = Fabricate(:status) - get :show, params: { account_username: status.account.username, id: status.id } - expect(assigns(:stream_entry)).to eq status.stream_entry - end - - it 'assigns @type' do - status = Fabricate(:status) - get :show, params: { account_username: status.account.username, id: status.id } - expect(assigns(:type)).to eq 'status' - end - it 'assigns @ancestors for ancestors of the status if it is a reply' do ancestor = Fabricate(:status) status = Fabricate(:status, in_reply_to_id: ancestor.id) @@ -135,10 +123,10 @@ describe StatusesController do expect(response).to have_http_status(200) end - it 'renders stream_entries/show' do + it 'renders statuses/show' do status = Fabricate(:status) get :show, params: { account_username: status.account.username, id: status.id } - expect(response).to render_template 'stream_entries/show' + expect(response).to render_template 'statuses/show' end end end diff --git a/spec/controllers/stream_entries_controller_spec.rb b/spec/controllers/stream_entries_controller_spec.rb deleted file mode 100644 index eb7fdf9d78..0000000000 --- a/spec/controllers/stream_entries_controller_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -require 'rails_helper' - -RSpec.describe StreamEntriesController, type: :controller do - render_views - - shared_examples 'before_action' do |route| - context 'when account is not suspended and stream_entry is available' do - it 'assigns instance variables' do - status = Fabricate(:status) - - get route, params: { account_username: status.account.username, id: status.stream_entry.id } - - expect(assigns(:account)).to eq status.account - expect(assigns(:stream_entry)).to eq status.stream_entry - expect(assigns(:type)).to eq 'status' - end - - it 'sets Link headers' do - alice = Fabricate(:account, username: 'alice') - status = Fabricate(:status, account: alice) - - get route, params: { account_username: alice.username, id: status.stream_entry.id } - - expect(response.headers['Link'].to_s).to eq "; rel=\"alternate\"; type=\"application/atom+xml\", ; rel=\"alternate\"; type=\"application/activity+json\"" - end - end - - context 'when account is suspended' do - it 'returns http status 410' do - account = Fabricate(:account, suspended: true) - status = Fabricate(:status, account: account) - - get route, params: { account_username: account.username, id: status.stream_entry.id } - - expect(response).to have_http_status(410) - end - end - - context 'when activity is nil' do - it 'raises ActiveRecord::RecordNotFound' do - account = Fabricate(:account) - stream_entry = Fabricate.build(:stream_entry, account: account, activity: nil, activity_type: 'Status') - stream_entry.save!(validate: false) - - get route, params: { account_username: account.username, id: stream_entry.id } - - expect(response).to have_http_status(404) - end - end - - context 'when it is hidden and it is not permitted' do - it 'raises ActiveRecord::RecordNotFound' do - status = Fabricate(:status) - user = Fabricate(:user) - status.account.block!(user.account) - status.stream_entry.update!(hidden: true) - - sign_in(user) - get route, params: { account_username: status.account.username, id: status.stream_entry.id } - - expect(response).to have_http_status(404) - end - end - end - - describe 'GET #show' do - include_examples 'before_action', :show - - it 'redirects to status page' do - status = Fabricate(:status) - - get :show, params: { account_username: status.account.username, id: status.stream_entry.id } - - expect(response).to redirect_to(short_account_status_url(status.account, status)) - end - - it 'returns http success with Atom' do - status = Fabricate(:status) - get :show, params: { account_username: status.account.username, id: status.stream_entry.id }, format: 'atom' - expect(response).to have_http_status(200) - end - end - - describe 'GET #embed' do - include_examples 'before_action', :embed - - it 'redirects to new embed page' do - status = Fabricate(:status) - - get :embed, params: { account_username: status.account.username, id: status.stream_entry.id } - - expect(response).to redirect_to(embed_short_account_status_url(status.account, status)) - end - end -end diff --git a/spec/fabricators/stream_entry_fabricator.rb b/spec/fabricators/stream_entry_fabricator.rb deleted file mode 100644 index f33822c7c5..0000000000 --- a/spec/fabricators/stream_entry_fabricator.rb +++ /dev/null @@ -1,5 +0,0 @@ -Fabricator(:stream_entry) do - account - activity { Fabricate(:status) } - hidden { [true, false].sample } -end diff --git a/spec/helpers/admin/account_moderation_notes_helper_spec.rb b/spec/helpers/admin/account_moderation_notes_helper_spec.rb index c07f6c4b88..ddfe8b46f9 100644 --- a/spec/helpers/admin/account_moderation_notes_helper_spec.rb +++ b/spec/helpers/admin/account_moderation_notes_helper_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe Admin::AccountModerationNotesHelper, type: :helper do - include StreamEntriesHelper + include StatusesHelper describe '#admin_account_link_to' do context 'account is nil' do diff --git a/spec/helpers/stream_entries_helper_spec.rb b/spec/helpers/statuses_helper_spec.rb similarity index 94% rename from spec/helpers/stream_entries_helper_spec.rb rename to spec/helpers/statuses_helper_spec.rb index 845b9974ea..510955a2ff 100644 --- a/spec/helpers/stream_entries_helper_spec.rb +++ b/spec/helpers/statuses_helper_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe StreamEntriesHelper, type: :helper do +RSpec.describe StatusesHelper, type: :helper do describe '#display_name' do it 'uses the display name when it exists' do account = Account.new(display_name: "Display", username: "Username") @@ -70,13 +70,13 @@ RSpec.describe StreamEntriesHelper, type: :helper do end def set_not_embedded_view - params[:controller] = "not_#{StreamEntriesHelper::EMBEDDED_CONTROLLER}" - params[:action] = "not_#{StreamEntriesHelper::EMBEDDED_ACTION}" + params[:controller] = "not_#{StatusesHelper::EMBEDDED_CONTROLLER}" + params[:action] = "not_#{StatusesHelper::EMBEDDED_ACTION}" end def set_embedded_view - params[:controller] = StreamEntriesHelper::EMBEDDED_CONTROLLER - params[:action] = StreamEntriesHelper::EMBEDDED_ACTION + params[:controller] = StatusesHelper::EMBEDDED_CONTROLLER + params[:action] = StatusesHelper::EMBEDDED_ACTION end describe '#style_classes' do diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb index 6d246629e7..1c5c6f0edd 100644 --- a/spec/lib/activitypub/tag_manager_spec.rb +++ b/spec/lib/activitypub/tag_manager_spec.rb @@ -143,12 +143,6 @@ RSpec.describe ActivityPub::TagManager do expect(subject.uri_to_resource(OStatus::TagManager.instance.uri_for(status), Status)).to eq status end - it 'returns the local status for OStatus StreamEntry URL' do - status = Fabricate(:status) - stream_entry_url = account_stream_entry_url(status.account, status.stream_entry) - expect(subject.uri_to_resource(stream_entry_url, Status)).to eq status - end - it 'returns the remote status by matching URI without fragment part' do status = Fabricate(:status, uri: 'https://example.com/123') expect(subject.uri_to_resource('https://example.com/123#456', Status)).to eq status diff --git a/spec/lib/ostatus/atom_serializer_spec.rb b/spec/lib/ostatus/atom_serializer_spec.rb deleted file mode 100644 index 74ab7576f2..0000000000 --- a/spec/lib/ostatus/atom_serializer_spec.rb +++ /dev/null @@ -1,1415 +0,0 @@ -require 'rails_helper' - -RSpec.describe OStatus::AtomSerializer do - shared_examples 'follow request salmon' do - it 'appends author element with account' do - account = Fabricate(:account, domain: nil, username: 'username') - follow_request = Fabricate(:follow_request, account: account) - - follow_request_salmon = serialize(follow_request) - - expect(follow_request_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' - end - - it 'appends activity:object-type element with activity type' do - follow_request = Fabricate(:follow_request) - - follow_request_salmon = serialize(follow_request) - - object_type = follow_request_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] - end - - it 'appends activity:verb element with request_friend type' do - follow_request = Fabricate(:follow_request) - - follow_request_salmon = serialize(follow_request) - - verb = follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq OStatus::TagManager::VERBS[:request_friend] - end - - it 'appends activity:object with target account' do - target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id') - follow_request = Fabricate(:follow_request, target_account: target_account) - - follow_request_salmon = serialize(follow_request) - - object = follow_request_salmon.nodes.find { |node| node.name == 'activity:object' } - expect(object.id.text).to eq 'https://domain.test/id' - end - end - - shared_examples 'namespaces' do - it 'adds namespaces' do - element = serialize - - expect(element['xmlns']).to eq OStatus::TagManager::XMLNS - expect(element['xmlns:thr']).to eq OStatus::TagManager::THR_XMLNS - expect(element['xmlns:activity']).to eq OStatus::TagManager::AS_XMLNS - expect(element['xmlns:poco']).to eq OStatus::TagManager::POCO_XMLNS - expect(element['xmlns:media']).to eq OStatus::TagManager::MEDIA_XMLNS - expect(element['xmlns:ostatus']).to eq OStatus::TagManager::OS_XMLNS - expect(element['xmlns:mastodon']).to eq OStatus::TagManager::MTDN_XMLNS - end - end - - shared_examples 'no namespaces' do - it 'does not add namespaces' do - expect(serialize['xmlns']).to eq nil - end - end - - shared_examples 'status attributes' do - it 'appends summary element with spoiler text if present' do - status = Fabricate(:status, language: :ca, spoiler_text: 'spoiler text') - - element = serialize(status) - - summary = element.summary - expect(summary['xml:lang']).to eq 'ca' - expect(summary.text).to eq 'spoiler text' - end - - it 'does not append summary element with spoiler text if not present' do - status = Fabricate(:status, spoiler_text: '') - element = serialize(status) - element.nodes.each { |node| expect(node.name).not_to eq 'summary' } - end - - it 'appends content element with formatted status' do - status = Fabricate(:status, language: :ca, text: 'text') - - element = serialize(status) - - content = element.content - expect(content[:type]).to eq 'html' - expect(content['xml:lang']).to eq 'ca' - expect(content.text).to eq '

text

' - end - - it 'appends link elements for mentioned accounts' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status) - Fabricate(:mention, account: account, status: status) - - element = serialize(status) - - mentioned = element.nodes.find do |node| - node.name == 'link' && - node[:rel] == 'mentioned' && - node['ostatus:object-type'] == OStatus::TagManager::TYPES[:person] - end - - expect(mentioned[:href]).to eq 'https://cb6e6126.ngrok.io/users/username' - end - - it 'appends link elements for emojis' do - Fabricate(:custom_emoji) - - status = Fabricate(:status, text: ':coolcat:') - element = serialize(status) - emoji = element.nodes.find { |node| node.name == 'link' && node[:rel] == 'emoji' } - - expect(emoji[:name]).to eq 'coolcat' - expect(emoji[:href]).to_not be_blank - end - end - - describe 'render' do - it 'returns XML with emojis' do - element = Ox::Element.new('tag') - element << '💩' - xml = OStatus::AtomSerializer.render(element) - - expect(xml).to eq "\n💩\n" - end - - it 'returns XML, stripping invalid characters like \b and \v' do - element = Ox::Element.new('tag') - element << "im l33t\b haxo\b\vr" - xml = OStatus::AtomSerializer.render(element) - - expect(xml).to eq "\nim l33t haxor\n" - end - end - - describe '#author' do - context 'when note is present' do - it 'appends poco:note element with note for local account' do - account = Fabricate(:account, domain: nil, note: '

note

') - - author = OStatus::AtomSerializer.new.author(account) - - note = author.nodes.find { |node| node.name == 'poco:note' } - expect(note.text).to eq '

note

' - end - - it 'appends poco:note element with tags-stripped note for remote account' do - account = Fabricate(:account, domain: 'remote', note: '

note

') - - author = OStatus::AtomSerializer.new.author(account) - - note = author.nodes.find { |node| node.name == 'poco:note' } - expect(note.text).to eq 'note' - end - - it 'appends summary element with type attribute and simplified note if present' do - account = Fabricate(:account, note: 'note') - author = OStatus::AtomSerializer.new.author(account) - expect(author.summary.text).to eq '

note

' - expect(author.summary[:type]).to eq 'html' - end - end - - context 'when note is not present' do - it 'does not append poco:note element' do - account = Fabricate(:account, note: '') - author = OStatus::AtomSerializer.new.author(account) - author.nodes.each { |node| expect(node.name).not_to eq 'poco:note' } - end - - it 'does not append summary element' do - account = Fabricate(:account, note: '') - author = OStatus::AtomSerializer.new.author(account) - author.nodes.each { |node| expect(node.name).not_to eq 'summary' } - end - end - - it 'returns author element' do - account = Fabricate(:account) - author = OStatus::AtomSerializer.new.author(account) - expect(author.name).to eq 'author' - end - - it 'appends activity:object-type element with person type' do - account = Fabricate(:account, domain: nil, username: 'username') - - author = OStatus::AtomSerializer.new.author(account) - - object_type = author.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:person] - end - - it 'appends email element with username and domain for local account' do - account = Fabricate(:account, username: 'username') - author = OStatus::AtomSerializer.new.author(account) - expect(author.email.text).to eq 'username@cb6e6126.ngrok.io' - end - - it 'appends email element with username and domain for remote user' do - account = Fabricate(:account, domain: 'domain', username: 'username') - author = OStatus::AtomSerializer.new.author(account) - expect(author.email.text).to eq 'username@domain' - end - - it 'appends link element for an alternative' do - account = Fabricate(:account, domain: nil, username: 'username') - - author = OStatus::AtomSerializer.new.author(account) - - link = author.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' } - expect(link[:type]).to eq 'text/html' - expect(link[:rel]).to eq 'alternate' - expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/@username' - end - - it 'has link element for avatar if present' do - account = Fabricate(:account, avatar: attachment_fixture('avatar.gif')) - - author = OStatus::AtomSerializer.new.author(account) - - link = author.nodes.find { |node| node.name == 'link' && node[:rel] == 'avatar' } - expect(link[:type]).to eq 'image/gif' - expect(link['media:width']).to eq '120' - expect(link['media:height']).to eq '120' - expect(link[:href]).to match /^https:\/\/cb6e6126.ngrok.io\/system\/accounts\/avatars\/.+\/original\/avatar.gif/ - end - - it 'does not have link element for avatar if not present' do - account = Fabricate(:account, avatar: nil) - - author = OStatus::AtomSerializer.new.author(account) - - author.nodes.each do |node| - expect(node[:rel]).not_to eq 'avatar' if node.name == 'link' - end - end - - it 'appends link element for header if present' do - account = Fabricate(:account, header: attachment_fixture('avatar.gif')) - - author = OStatus::AtomSerializer.new.author(account) - - link = author.nodes.find { |node| node.name == 'link' && node[:rel] == 'header' } - expect(link[:type]).to eq 'image/gif' - expect(link['media:width']).to eq '700' - expect(link['media:height']).to eq '335' - expect(link[:href]).to match /^https:\/\/cb6e6126.ngrok.io\/system\/accounts\/headers\/.+\/original\/avatar.gif/ - end - - it 'does not append link element for header if not present' do - account = Fabricate(:account, header: nil) - - author = OStatus::AtomSerializer.new.author(account) - - author.nodes.each do |node| - expect(node[:rel]).not_to eq 'header' if node.name == 'link' - end - end - - it 'appends poco:displayName element with display name if present' do - account = Fabricate(:account, display_name: 'display name') - - author = OStatus::AtomSerializer.new.author(account) - - display_name = author.nodes.find { |node| node.name == 'poco:displayName' } - expect(display_name.text).to eq 'display name' - end - - it 'does not append poco:displayName element with display name if not present' do - account = Fabricate(:account, display_name: '') - author = OStatus::AtomSerializer.new.author(account) - author.nodes.each { |node| expect(node.name).not_to eq 'poco:displayName' } - end - - it "appends mastodon:scope element with 'private' if locked" do - account = Fabricate(:account, locked: true) - - author = OStatus::AtomSerializer.new.author(account) - - scope = author.nodes.find { |node| node.name == 'mastodon:scope' } - expect(scope.text).to eq 'private' - end - - it "appends mastodon:scope element with 'public' if unlocked" do - account = Fabricate(:account, locked: false) - - author = OStatus::AtomSerializer.new.author(account) - - scope = author.nodes.find { |node| node.name == 'mastodon:scope' } - expect(scope.text).to eq 'public' - end - - it 'includes URI' do - account = Fabricate(:account, domain: nil, username: 'username') - - author = OStatus::AtomSerializer.new.author(account) - - expect(author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' - expect(author.uri.text).to eq 'https://cb6e6126.ngrok.io/users/username' - end - - it 'includes username' do - account = Fabricate(:account, username: 'username') - - author = OStatus::AtomSerializer.new.author(account) - - name = author.nodes.find { |node| node.name == 'name' } - username = author.nodes.find { |node| node.name == 'poco:preferredUsername' } - expect(name.text).to eq 'username' - expect(username.text).to eq 'username' - end - end - - describe '#entry' do - shared_examples 'not root' do - include_examples 'no namespaces' do - def serialize - subject - end - end - - it 'does not append author element' do - subject.nodes.each { |node| expect(node.name).not_to eq 'author' } - end - end - - context 'it is root' do - include_examples 'namespaces' do - def serialize - stream_entry = Fabricate(:stream_entry) - OStatus::AtomSerializer.new.entry(stream_entry, true) - end - end - - it 'appends author element' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: account) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry, true) - - expect(entry.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' - end - end - - context 'if status is present' do - include_examples 'status attributes' do - def serialize(status) - OStatus::AtomSerializer.new.entry(status.stream_entry, true) - end - end - - it 'appends link element for the public collection if status is publicly visible' do - status = Fabricate(:status, visibility: :public) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - mentioned_person = entry.nodes.find do |node| - node.name == 'link' && - node[:rel] == 'mentioned' && - node['ostatus:object-type'] == OStatus::TagManager::TYPES[:collection] - end - expect(mentioned_person[:href]).to eq OStatus::TagManager::COLLECTIONS[:public] - end - - it 'does not append link element for the public collection if status is not publicly visible' do - status = Fabricate(:status, visibility: :private) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - entry.nodes.each do |node| - if node.name == 'link' && - node[:rel] == 'mentioned' && - node['ostatus:object-type'] == OStatus::TagManager::TYPES[:collection] - expect(mentioned_collection[:href]).not_to eq OStatus::TagManager::COLLECTIONS[:public] - end - end - end - - it 'appends category elements for tags' do - tag = Fabricate(:tag, name: 'tag') - status = Fabricate(:status, tags: [ tag ]) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - expect(entry.category[:term]).to eq 'tag' - end - - it 'appends link elements for media attachments' do - file = attachment_fixture('attachment.jpg') - media_attachment = Fabricate(:media_attachment, file: file) - status = Fabricate(:status, media_attachments: [ media_attachment ]) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - enclosure = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'enclosure' } - expect(enclosure[:type]).to eq 'image/jpeg' - expect(enclosure[:href]).to match /^https:\/\/cb6e6126.ngrok.io\/system\/media_attachments\/files\/.+\/original\/attachment.jpg$/ - end - - it 'appends mastodon:scope element with visibility' do - status = Fabricate(:status, visibility: :public) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - scope = entry.nodes.find { |node| node.name == 'mastodon:scope' } - expect(scope.text).to eq 'public' - end - end - - context 'if status is not present' do - it 'appends content element saying status is deleted' do - status = Fabricate(:status) - status.destroy! - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - expect(entry.content.text).to eq 'Deleted status' - end - - it 'appends title element saying the status is deleted' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: account) - status.destroy! - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - expect(entry.title.text).to eq 'username deleted status' - end - end - - context 'it is not root' do - let(:stream_entry) { Fabricate(:stream_entry) } - subject { OStatus::AtomSerializer.new.entry(stream_entry, false) } - include_examples 'not root' - end - - context 'without root parameter' do - let(:stream_entry) { Fabricate(:stream_entry) } - subject { OStatus::AtomSerializer.new.entry(stream_entry) } - include_examples 'not root' - end - - it 'returns entry element' do - stream_entry = Fabricate(:stream_entry) - entry = OStatus::AtomSerializer.new.entry(stream_entry) - expect(entry.name).to eq 'entry' - end - - it 'appends id element with unique tag' do - status = Fabricate(:status, reblog_of_id: nil, created_at: '2000-01-01T00:00:00Z') - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - expect(entry.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}" - end - - it 'appends published element with created date' do - stream_entry = Fabricate(:stream_entry, created_at: '2000-01-01T00:00:00Z') - entry = OStatus::AtomSerializer.new.entry(stream_entry) - expect(entry.published.text).to eq '2000-01-01T00:00:00Z' - end - - it 'appends updated element with updated date' do - stream_entry = Fabricate(:stream_entry, updated_at: '2000-01-01T00:00:00Z') - entry = OStatus::AtomSerializer.new.entry(stream_entry) - expect(entry.updated.text).to eq '2000-01-01T00:00:00Z' - end - - it 'appends title element with status title' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: account, reblog_of_id: nil) - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - expect(entry.title.text).to eq 'New status by username' - end - - it 'appends activity:object-type element with object type' do - status = Fabricate(:status) - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - object_type = entry.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:note] - end - - it 'appends activity:verb element with object type' do - status = Fabricate(:status) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - object_type = entry.nodes.find { |node| node.name == 'activity:verb' } - expect(object_type.text).to eq OStatus::TagManager::VERBS[:post] - end - - it 'appends activity:object element with target if present' do - reblogged = Fabricate(:status, created_at: '2000-01-01T00:00:00Z') - reblog = Fabricate(:status, reblog: reblogged) - - entry = OStatus::AtomSerializer.new.entry(reblog.stream_entry) - - object = entry.nodes.find { |node| node.name == 'activity:object' } - expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{reblogged.account.to_param}/statuses/#{reblogged.id}" - end - - it 'does not append activity:object element if target is not present' do - status = Fabricate(:status, reblog_of_id: nil) - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - entry.nodes.each { |node| expect(node.name).not_to eq 'activity:object' } - end - - it 'appends link element for an alternative' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: account) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' } - expect(link[:type]).to eq 'text/html' - expect(link[:href]).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}" - end - - it 'appends link element for itself' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: account) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'self' } - expect(link[:type]).to eq 'application/atom+xml' - expect(link[:href]).to eq "https://cb6e6126.ngrok.io/users/username/updates/#{status.stream_entry.id}.atom" - end - - it 'appends thr:in-reply-to element if threaded' do - in_reply_to_status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z', reblog_of_id: nil) - reply_status = Fabricate(:status, in_reply_to_id: in_reply_to_status.id) - - entry = OStatus::AtomSerializer.new.entry(reply_status.stream_entry) - - in_reply_to = entry.nodes.find { |node| node.name == 'thr:in-reply-to' } - expect(in_reply_to[:ref]).to eq "https://cb6e6126.ngrok.io/users/#{in_reply_to_status.account.to_param}/statuses/#{in_reply_to_status.id}" - end - - it 'does not append thr:in-reply-to element if not threaded' do - status = Fabricate(:status) - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - entry.nodes.each { |node| expect(node.name).not_to eq 'thr:in-reply-to' } - end - - it 'appends ostatus:conversation if conversation id is present' do - status = Fabricate(:status) - status.conversation.update!(created_at: '2000-01-01T00:00:00Z') - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - conversation = entry.nodes.find { |node| node.name == 'ostatus:conversation' } - expect(conversation[:ref]).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.conversation_id}:objectType=Conversation" - end - - it 'does not append ostatus:conversation if conversation id is not present' do - status = Fabricate.build(:status, conversation_id: nil) - status.save!(validate: false) - - entry = OStatus::AtomSerializer.new.entry(status.stream_entry) - - entry.nodes.each { |node| expect(node.name).not_to eq 'ostatus:conversation' } - end - end - - describe '#feed' do - include_examples 'namespaces' do - def serialize - account = Fabricate(:account) - OStatus::AtomSerializer.new.feed(account, []) - end - end - - it 'returns feed element' do - account = Fabricate(:account) - feed = OStatus::AtomSerializer.new.feed(account, []) - expect(feed.name).to eq 'feed' - end - - it 'appends id element with account Atom URL' do - account = Fabricate(:account, username: 'username') - feed = OStatus::AtomSerializer.new.feed(account, []) - expect(feed.id.text).to eq 'https://cb6e6126.ngrok.io/users/username.atom' - end - - it 'appends title element with account display name if present' do - account = Fabricate(:account, display_name: 'display name') - feed = OStatus::AtomSerializer.new.feed(account, []) - expect(feed.title.text).to eq 'display name' - end - - it 'does not append title element with account username if account display name is not present' do - account = Fabricate(:account, display_name: '', username: 'username') - feed = OStatus::AtomSerializer.new.feed(account, []) - expect(feed.title.text).to eq 'username' - end - - it 'appends subtitle element with account note' do - account = Fabricate(:account, note: 'note') - feed = OStatus::AtomSerializer.new.feed(account, []) - expect(feed.subtitle.text).to eq 'note' - end - - it 'appends updated element with date account got updated' do - account = Fabricate(:account, updated_at: '2000-01-01T00:00:00Z') - feed = OStatus::AtomSerializer.new.feed(account, []) - expect(feed.updated.text).to eq '2000-01-01T00:00:00Z' - end - - it 'appends logo element with full asset URL for original account avatar' do - account = Fabricate(:account, avatar: attachment_fixture('avatar.gif')) - feed = OStatus::AtomSerializer.new.feed(account, []) - expect(feed.logo.text).to match /^https:\/\/cb6e6126.ngrok.io\/system\/accounts\/avatars\/.+\/original\/avatar.gif/ - end - - it 'appends author element' do - account = Fabricate(:account, username: 'username') - feed = OStatus::AtomSerializer.new.feed(account, []) - expect(feed.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' - end - - it 'appends link element for an alternative' do - account = Fabricate(:account, username: 'username') - - feed = OStatus::AtomSerializer.new.feed(account, []) - - link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' } - expect(link[:type]).to eq 'text/html' - expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/@username' - end - - it 'appends link element for itself' do - account = Fabricate(:account, username: 'username') - - feed = OStatus::AtomSerializer.new.feed(account, []) - - link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'self' } - expect(link[:type]).to eq 'application/atom+xml' - expect(link[:href]).to eq 'https://cb6e6126.ngrok.io/users/username.atom' - end - - it 'appends link element for the next if it has 20 stream entries' do - account = Fabricate(:account, username: 'username') - stream_entry = Fabricate(:stream_entry) - - feed = OStatus::AtomSerializer.new.feed(account, Array.new(20, stream_entry)) - - link = feed.nodes.find { |node| node.name == 'link' && node[:rel] == 'next' } - expect(link[:type]).to eq 'application/atom+xml' - expect(link[:href]).to eq "https://cb6e6126.ngrok.io/users/username.atom?max_id=#{stream_entry.id}" - end - - it 'does not append link element for the next if it does not have 20 stream entries' do - account = Fabricate(:account, username: 'username') - - feed = OStatus::AtomSerializer.new.feed(account, []) - - feed.nodes.each do |node| - expect(node[:rel]).not_to eq 'next' if node.name == 'link' - end - end - - it 'appends stream entries' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: account) - - feed = OStatus::AtomSerializer.new.feed(account, [status.stream_entry]) - - expect(feed.entry.title.text).to eq 'New status by username' - end - end - - describe '#block_salmon' do - include_examples 'namespaces' do - def serialize - block = Fabricate(:block) - OStatus::AtomSerializer.new.block_salmon(block) - end - end - - it 'returns entry element' do - block = Fabricate(:block) - block_salmon = OStatus::AtomSerializer.new.block_salmon(block) - expect(block_salmon.name).to eq 'entry' - end - - it 'appends id element with unique tag' do - block = Fabricate(:block) - - time_before = Time.zone.now - block_salmon = OStatus::AtomSerializer.new.block_salmon(block) - time_after = Time.zone.now - - expect(block_salmon.id.text).to( - eq(OStatus::TagManager.instance.unique_tag(time_before.utc, block.id, 'Block')) - .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, block.id, 'Block'))) - ) - end - - it 'appends title element with description' do - account = Fabricate(:account, domain: nil, username: 'account') - target_account = Fabricate(:account, domain: 'remote', username: 'target_account') - block = Fabricate(:block, account: account, target_account: target_account) - - block_salmon = OStatus::AtomSerializer.new.block_salmon(block) - - expect(block_salmon.title.text).to eq 'account no longer wishes to interact with target_account@remote' - end - - it 'appends author element with account' do - account = Fabricate(:account, domain: nil, username: 'account') - block = Fabricate(:block, account: account) - - block_salmon = OStatus::AtomSerializer.new.block_salmon(block) - - expect(block_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/account' - end - - it 'appends activity:object-type element with activity type' do - block = Fabricate(:block) - - block_salmon = OStatus::AtomSerializer.new.block_salmon(block) - - object_type = block_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] - end - - it 'appends activity:verb element with block' do - block = Fabricate(:block) - - block_salmon = OStatus::AtomSerializer.new.block_salmon(block) - - verb = block_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq OStatus::TagManager::VERBS[:block] - end - - it 'appends activity:object element with target account' do - target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id') - block = Fabricate(:block, target_account: target_account) - - block_salmon = OStatus::AtomSerializer.new.block_salmon(block) - - object = block_salmon.nodes.find { |node| node.name == 'activity:object' } - expect(object.id.text).to eq 'https://domain.test/id' - end - end - - describe '#unblock_salmon' do - include_examples 'namespaces' do - def serialize - block = Fabricate(:block) - OStatus::AtomSerializer.new.unblock_salmon(block) - end - end - - it 'returns entry element' do - block = Fabricate(:block) - unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) - expect(unblock_salmon.name).to eq 'entry' - end - - it 'appends id element with unique tag' do - block = Fabricate(:block) - - time_before = Time.zone.now - unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) - time_after = Time.zone.now - - expect(unblock_salmon.id.text).to( - eq(OStatus::TagManager.instance.unique_tag(time_before.utc, block.id, 'Block')) - .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, block.id, 'Block'))) - ) - end - - it 'appends title element with description' do - account = Fabricate(:account, domain: nil, username: 'account') - target_account = Fabricate(:account, domain: 'remote', username: 'target_account') - block = Fabricate(:block, account: account, target_account: target_account) - - unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) - - expect(unblock_salmon.title.text).to eq 'account no longer blocks target_account@remote' - end - - it 'appends author element with account' do - account = Fabricate(:account, domain: nil, username: 'account') - block = Fabricate(:block, account: account) - - unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) - - expect(unblock_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/account' - end - - it 'appends activity:object-type element with activity type' do - block = Fabricate(:block) - - unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) - - object_type = unblock_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] - end - - it 'appends activity:verb element with block' do - block = Fabricate(:block) - - unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) - - verb = unblock_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq OStatus::TagManager::VERBS[:unblock] - end - - it 'appends activity:object element with target account' do - target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id') - block = Fabricate(:block, target_account: target_account) - - unblock_salmon = OStatus::AtomSerializer.new.unblock_salmon(block) - - object = unblock_salmon.nodes.find { |node| node.name == 'activity:object' } - expect(object.id.text).to eq 'https://domain.test/id' - end - end - - describe '#favourite_salmon' do - include_examples 'namespaces' do - def serialize - favourite = Fabricate(:favourite) - OStatus::AtomSerializer.new.favourite_salmon(favourite) - end - end - - it 'returns entry element' do - favourite = Fabricate(:favourite) - favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) - expect(favourite_salmon.name).to eq 'entry' - end - - it 'appends id element with unique tag' do - favourite = Fabricate(:favourite, created_at: '2000-01-01T00:00:00Z') - favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) - expect(favourite_salmon.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{favourite.id}:objectType=Favourite" - end - - it 'appends author element with account' do - account = Fabricate(:account, domain: nil, username: 'username') - favourite = Fabricate(:favourite, account: account) - - favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) - - expect(favourite_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' - end - - it 'appends activity:object-type element with activity type' do - favourite = Fabricate(:favourite) - - favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) - - object_type = favourite_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq 'http://activitystrea.ms/schema/1.0/activity' - end - - it 'appends activity:verb element with favorite' do - favourite = Fabricate(:favourite) - - favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) - - verb = favourite_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq OStatus::TagManager::VERBS[:favorite] - end - - it 'appends activity:object element with status' do - status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z') - favourite = Fabricate(:favourite, status: status) - - favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) - - object = favourite_salmon.nodes.find { |node| node.name == 'activity:object' } - expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}" - end - - it 'appends thr:in-reply-to element for status' do - status_account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: status_account, created_at: '2000-01-01T00:00:00Z') - favourite = Fabricate(:favourite, status: status) - - favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) - - in_reply_to = favourite_salmon.nodes.find { |node| node.name == 'thr:in-reply-to' } - expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}" - expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}" - end - - it 'includes description' do - account = Fabricate(:account, domain: nil, username: 'account') - status_account = Fabricate(:account, domain: 'remote', username: 'status_account') - status = Fabricate(:status, account: status_account) - favourite = Fabricate(:favourite, account: account, status: status) - - favourite_salmon = OStatus::AtomSerializer.new.favourite_salmon(favourite) - - expect(favourite_salmon.title.text).to eq 'account favourited a status by status_account@remote' - expect(favourite_salmon.content.text).to eq 'account favourited a status by status_account@remote' - end - end - - describe '#unfavourite_salmon' do - include_examples 'namespaces' do - def serialize - favourite = Fabricate(:favourite) - OStatus::AtomSerializer.new.favourite_salmon(favourite) - end - end - - it 'returns entry element' do - favourite = Fabricate(:favourite) - unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - expect(unfavourite_salmon.name).to eq 'entry' - end - - it 'appends id element with unique tag' do - favourite = Fabricate(:favourite) - - time_before = Time.zone.now - unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - time_after = Time.zone.now - - expect(unfavourite_salmon.id.text).to( - eq(OStatus::TagManager.instance.unique_tag(time_before.utc, favourite.id, 'Favourite')) - .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, favourite.id, 'Favourite'))) - ) - end - - it 'appends author element with account' do - account = Fabricate(:account, domain: nil, username: 'username') - favourite = Fabricate(:favourite, account: account) - - unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - - expect(unfavourite_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' - end - - it 'appends activity:object-type element with activity type' do - favourite = Fabricate(:favourite) - - unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - - object_type = unfavourite_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq 'http://activitystrea.ms/schema/1.0/activity' - end - - it 'appends activity:verb element with favorite' do - favourite = Fabricate(:favourite) - - unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - - verb = unfavourite_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq OStatus::TagManager::VERBS[:unfavorite] - end - - it 'appends activity:object element with status' do - status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z') - favourite = Fabricate(:favourite, status: status) - - unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - - object = unfavourite_salmon.nodes.find { |node| node.name == 'activity:object' } - expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}" - end - - it 'appends thr:in-reply-to element for status' do - status_account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: status_account, created_at: '2000-01-01T00:00:00Z') - favourite = Fabricate(:favourite, status: status) - - unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - - in_reply_to = unfavourite_salmon.nodes.find { |node| node.name == 'thr:in-reply-to' } - expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}" - expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}" - end - - it 'includes description' do - account = Fabricate(:account, domain: nil, username: 'account') - status_account = Fabricate(:account, domain: 'remote', username: 'status_account') - status = Fabricate(:status, account: status_account) - favourite = Fabricate(:favourite, account: account, status: status) - - unfavourite_salmon = OStatus::AtomSerializer.new.unfavourite_salmon(favourite) - - expect(unfavourite_salmon.title.text).to eq 'account no longer favourites a status by status_account@remote' - expect(unfavourite_salmon.content.text).to eq 'account no longer favourites a status by status_account@remote' - end - end - - describe '#follow_salmon' do - include_examples 'namespaces' do - def serialize - follow = Fabricate(:follow) - OStatus::AtomSerializer.new.follow_salmon(follow) - end - end - - it 'returns entry element' do - follow = Fabricate(:follow) - follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) - expect(follow_salmon.name).to eq 'entry' - end - - it 'appends id element with unique tag' do - follow = Fabricate(:follow, created_at: '2000-01-01T00:00:00Z') - follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) - expect(follow_salmon.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{follow.id}:objectType=Follow" - end - - it 'appends author element with account' do - account = Fabricate(:account, domain: nil, username: 'username') - follow = Fabricate(:follow, account: account) - - follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) - - expect(follow_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' - end - - it 'appends activity:object-type element with activity type' do - follow = Fabricate(:follow) - - follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) - - object_type = follow_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] - end - - it 'appends activity:verb element with follow' do - follow = Fabricate(:follow) - - follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) - - verb = follow_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq OStatus::TagManager::VERBS[:follow] - end - - it 'appends activity:object element with target account' do - target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id') - follow = Fabricate(:follow, target_account: target_account) - - follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) - - object = follow_salmon.nodes.find { |node| node.name == 'activity:object' } - expect(object.id.text).to eq 'https://domain.test/id' - end - - it 'includes description' do - account = Fabricate(:account, domain: nil, username: 'account') - target_account = Fabricate(:account, domain: 'remote', username: 'target_account') - follow = Fabricate(:follow, account: account, target_account: target_account) - - follow_salmon = OStatus::AtomSerializer.new.follow_salmon(follow) - - expect(follow_salmon.title.text).to eq 'account started following target_account@remote' - expect(follow_salmon.content.text).to eq 'account started following target_account@remote' - end - end - - describe '#unfollow_salmon' do - include_examples 'namespaces' do - def serialize - follow = Fabricate(:follow) - follow.destroy! - OStatus::AtomSerializer.new.unfollow_salmon(follow) - end - end - - it 'returns entry element' do - follow = Fabricate(:follow) - follow.destroy! - - unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - - expect(unfollow_salmon.name).to eq 'entry' - end - - it 'appends id element with unique tag' do - follow = Fabricate(:follow) - follow.destroy! - - time_before = Time.zone.now - unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - time_after = Time.zone.now - - expect(unfollow_salmon.id.text).to( - eq(OStatus::TagManager.instance.unique_tag(time_before.utc, follow.id, 'Follow')) - .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, follow.id, 'Follow'))) - ) - end - - it 'appends title element with description' do - account = Fabricate(:account, domain: nil, username: 'account') - target_account = Fabricate(:account, domain: 'remote', username: 'target_account') - follow = Fabricate(:follow, account: account, target_account: target_account) - follow.destroy! - - unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - - expect(unfollow_salmon.title.text).to eq 'account is no longer following target_account@remote' - end - - it 'appends content element with description' do - account = Fabricate(:account, domain: nil, username: 'account') - target_account = Fabricate(:account, domain: 'remote', username: 'target_account') - follow = Fabricate(:follow, account: account, target_account: target_account) - follow.destroy! - - unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - - expect(unfollow_salmon.content.text).to eq 'account is no longer following target_account@remote' - end - - it 'appends author element with account' do - account = Fabricate(:account, domain: nil, username: 'username') - follow = Fabricate(:follow, account: account) - follow.destroy! - - unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - - expect(unfollow_salmon.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' - end - - it 'appends activity:object-type element with activity type' do - follow = Fabricate(:follow) - follow.destroy! - - unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - - object_type = unfollow_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] - end - - it 'appends activity:verb element with follow' do - follow = Fabricate(:follow) - follow.destroy! - - unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - - verb = unfollow_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq OStatus::TagManager::VERBS[:unfollow] - end - - it 'appends activity:object element with target account' do - target_account = Fabricate(:account, domain: 'domain.test', uri: 'https://domain.test/id') - follow = Fabricate(:follow, target_account: target_account) - follow.destroy! - - unfollow_salmon = OStatus::AtomSerializer.new.unfollow_salmon(follow) - - object = unfollow_salmon.nodes.find { |node| node.name == 'activity:object' } - expect(object.id.text).to eq 'https://domain.test/id' - end - end - - describe '#follow_request_salmon' do - include_examples 'namespaces' do - def serialize - follow_request = Fabricate(:follow_request) - OStatus::AtomSerializer.new.follow_request_salmon(follow_request) - end - end - - context do - def serialize(follow_request) - OStatus::AtomSerializer.new.follow_request_salmon(follow_request) - end - - it_behaves_like 'follow request salmon' - - it 'appends id element with unique tag' do - follow_request = Fabricate(:follow_request, created_at: '2000-01-01T00:00:00Z') - follow_request_salmon = serialize(follow_request) - expect(follow_request_salmon.id.text).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{follow_request.id}:objectType=FollowRequest" - end - - it 'appends title element with description' do - account = Fabricate(:account, domain: nil, username: 'account') - target_account = Fabricate(:account, domain: 'remote', username: 'target_account') - follow_request = Fabricate(:follow_request, account: account, target_account: target_account) - follow_request_salmon = serialize(follow_request) - expect(follow_request_salmon.title.text).to eq 'account requested to follow target_account@remote' - end - end - end - - describe '#authorize_follow_request_salmon' do - include_examples 'namespaces' do - def serialize - follow_request = Fabricate(:follow_request) - OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) - end - end - - it_behaves_like 'follow request salmon' do - def serialize(follow_request) - authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) - authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:object' } - end - end - - it 'appends id element with unique tag' do - follow_request = Fabricate(:follow_request) - - time_before = Time.zone.now - authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) - time_after = Time.zone.now - - expect(authorize_follow_request_salmon.id.text).to( - eq(OStatus::TagManager.instance.unique_tag(time_before.utc, follow_request.id, 'FollowRequest')) - .or(eq(OStatus::TagManager.instance.unique_tag(time_after.utc, follow_request.id, 'FollowRequest'))) - ) - end - - it 'appends title element with description' do - account = Fabricate(:account, domain: 'remote', username: 'account') - target_account = Fabricate(:account, domain: nil, username: 'target_account') - follow_request = Fabricate(:follow_request, account: account, target_account: target_account) - - authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) - - expect(authorize_follow_request_salmon.title.text).to eq 'target_account authorizes follow request by account@remote' - end - - it 'appends activity:object-type element with activity type' do - follow_request = Fabricate(:follow_request) - - authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) - - object_type = authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] - end - - it 'appends activity:verb element with authorize' do - follow_request = Fabricate(:follow_request) - - authorize_follow_request_salmon = OStatus::AtomSerializer.new.authorize_follow_request_salmon(follow_request) - - verb = authorize_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq OStatus::TagManager::VERBS[:authorize] - end - end - - describe '#reject_follow_request_salmon' do - include_examples 'namespaces' do - def serialize - follow_request = Fabricate(:follow_request) - OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) - end - end - - it_behaves_like 'follow request salmon' do - def serialize(follow_request) - reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) - reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:object' } - end - end - - it 'appends id element with unique tag' do - follow_request = Fabricate(:follow_request) - - time_before = Time.zone.now - reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) - time_after = Time.zone.now - - expect(reject_follow_request_salmon.id.text).to( - eq(OStatus::TagManager.instance.unique_tag(time_before.utc, follow_request.id, 'FollowRequest')) - .or(OStatus::TagManager.instance.unique_tag(time_after.utc, follow_request.id, 'FollowRequest')) - ) - end - - it 'appends title element with description' do - account = Fabricate(:account, domain: 'remote', username: 'account') - target_account = Fabricate(:account, domain: nil, username: 'target_account') - follow_request = Fabricate(:follow_request, account: account, target_account: target_account) - reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) - expect(reject_follow_request_salmon.title.text).to eq 'target_account rejects follow request by account@remote' - end - - it 'appends activity:object-type element with activity type' do - follow_request = Fabricate(:follow_request) - reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) - object_type = reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:activity] - end - - it 'appends activity:verb element with authorize' do - follow_request = Fabricate(:follow_request) - reject_follow_request_salmon = OStatus::AtomSerializer.new.reject_follow_request_salmon(follow_request) - verb = reject_follow_request_salmon.nodes.find { |node| node.name == 'activity:verb' } - expect(verb.text).to eq OStatus::TagManager::VERBS[:reject] - end - end - - describe '#object' do - include_examples 'status attributes' do - def serialize(status) - OStatus::AtomSerializer.new.object(status) - end - end - - it 'returns activity:object element' do - status = Fabricate(:status) - object = OStatus::AtomSerializer.new.object(status) - expect(object.name).to eq 'activity:object' - end - - it 'appends id element with URL for status' do - status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z') - object = OStatus::AtomSerializer.new.object(status) - expect(object.id.text).to eq "https://cb6e6126.ngrok.io/users/#{status.account.to_param}/statuses/#{status.id}" - end - - it 'appends published element with created date' do - status = Fabricate(:status, created_at: '2000-01-01T00:00:00Z') - object = OStatus::AtomSerializer.new.object(status) - expect(object.published.text).to eq '2000-01-01T00:00:00Z' - end - - it 'appends updated element with updated date' do - status = Fabricate(:status) - status.updated_at = '2000-01-01T00:00:00Z' - object = OStatus::AtomSerializer.new.object(status) - expect(object.updated.text).to eq '2000-01-01T00:00:00Z' - end - - it 'appends title element with title' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: account) - - object = OStatus::AtomSerializer.new.object(status) - - expect(object.title.text).to eq 'New status by username' - end - - it 'appends author element with account' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: account) - - entry = OStatus::AtomSerializer.new.object(status) - - expect(entry.author.id.text).to eq 'https://cb6e6126.ngrok.io/users/username' - end - - it 'appends activity:object-type element with object type' do - status = Fabricate(:status) - - entry = OStatus::AtomSerializer.new.object(status) - - object_type = entry.nodes.find { |node| node.name == 'activity:object-type' } - expect(object_type.text).to eq OStatus::TagManager::TYPES[:note] - end - - it 'appends activity:verb element with verb' do - status = Fabricate(:status) - - entry = OStatus::AtomSerializer.new.object(status) - - object_type = entry.nodes.find { |node| node.name == 'activity:verb' } - expect(object_type.text).to eq OStatus::TagManager::VERBS[:post] - end - - it 'appends link element for an alternative' do - account = Fabricate(:account, username: 'username') - status = Fabricate(:status, account: account) - - entry = OStatus::AtomSerializer.new.object(status) - - link = entry.nodes.find { |node| node.name == 'link' && node[:rel] == 'alternate' && node[:type] == 'text/html' } - expect(link[:type]).to eq 'text/html' - expect(link[:href]).to eq "https://cb6e6126.ngrok.io/@username/#{status.id}" - end - - it 'appends thr:in-reply-to element if it is a reply and thread is not nil' do - account = Fabricate(:account, username: 'username') - thread = Fabricate(:status, account: account, created_at: '2000-01-01T00:00:00Z') - reply = Fabricate(:status, thread: thread) - - entry = OStatus::AtomSerializer.new.object(reply) - - in_reply_to = entry.nodes.find { |node| node.name == 'thr:in-reply-to' } - expect(in_reply_to.ref).to eq "https://cb6e6126.ngrok.io/users/#{thread.account.to_param}/statuses/#{thread.id}" - expect(in_reply_to.href).to eq "https://cb6e6126.ngrok.io/@username/#{thread.id}" - end - - it 'does not append thr:in-reply-to element if thread is nil' do - status = Fabricate(:status, thread: nil) - entry = OStatus::AtomSerializer.new.object(status) - entry.nodes.each { |node| expect(node.name).not_to eq 'thr:in-reply-to' } - end - - it 'does not append ostatus:conversation element if conversation_id is nil' do - status = Fabricate.build(:status, conversation_id: nil) - status.save!(validate: false) - - entry = OStatus::AtomSerializer.new.object(status) - - entry.nodes.each { |node| expect(node.name).not_to eq 'ostatus:conversation' } - end - - it 'appends ostatus:conversation element if conversation_id is not nil' do - status = Fabricate(:status) - status.conversation.update!(created_at: '2000-01-01T00:00:00Z') - - entry = OStatus::AtomSerializer.new.object(status) - - conversation = entry.nodes.find { |node| node.name == 'ostatus:conversation' } - expect(conversation[:ref]).to eq "tag:cb6e6126.ngrok.io,2000-01-01:objectId=#{status.conversation.id}:objectType=Conversation" - end - end -end diff --git a/spec/lib/status_finder_spec.rb b/spec/lib/status_finder_spec.rb index 6b4ee434f3..61483f4bfe 100644 --- a/spec/lib/status_finder_spec.rb +++ b/spec/lib/status_finder_spec.rb @@ -25,15 +25,6 @@ describe StatusFinder do end end - context 'with a stream entry url' do - let(:stream_entry) { Fabricate(:stream_entry) } - let(:url) { account_stream_entry_url(stream_entry.account, stream_entry) } - - it 'finds the stream entry' do - expect(subject.status).to eq(stream_entry.status) - end - end - context 'with a remote url even if id exists on local' do let(:status) { Fabricate(:status) } let(:url) { "https://example.com/users/test/statuses/#{status.id}" } diff --git a/spec/lib/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb index 3a804ac0f0..e9a7aa9344 100644 --- a/spec/lib/tag_manager_spec.rb +++ b/spec/lib/tag_manager_spec.rb @@ -119,46 +119,4 @@ RSpec.describe TagManager do expect(TagManager.instance.same_acct?('username', 'incorrect@Cb6E6126.nGrOk.Io')).to eq false end end - - describe '#url_for' do - let(:alice) { Fabricate(:account, username: 'alice') } - - subject { TagManager.instance.url_for(target) } - - context 'activity object' do - let(:target) { Fabricate(:status, account: alice, reblog: Fabricate(:status)).stream_entry } - - it 'returns the unique tag for status' do - expect(target.object_type).to eq :activity - is_expected.to eq "https://cb6e6126.ngrok.io/@alice/#{target.id}" - end - end - - context 'comment object' do - let(:target) { Fabricate(:status, account: alice, reply: true) } - - it 'returns the unique tag for status' do - expect(target.object_type).to eq :comment - is_expected.to eq "https://cb6e6126.ngrok.io/@alice/#{target.id}" - end - end - - context 'note object' do - let(:target) { Fabricate(:status, account: alice, reply: false, thread: nil) } - - it 'returns the unique tag for status' do - expect(target.object_type).to eq :note - is_expected.to eq "https://cb6e6126.ngrok.io/@alice/#{target.id}" - end - end - - context 'person object' do - let(:target) { alice } - - it 'returns the URL for account' do - expect(target.object_type).to eq :person - is_expected.to eq 'https://cb6e6126.ngrok.io/@alice' - end - end - end end diff --git a/spec/models/concerns/streamable_spec.rb b/spec/models/concerns/streamable_spec.rb deleted file mode 100644 index b5f2d51923..0000000000 --- a/spec/models/concerns/streamable_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Streamable do - class Parent - def title; end - - def target; end - - def thread; end - - def self.has_one(*); end - - def self.after_create; end - end - - class Child < Parent - include Streamable - end - - child = Child.new - - describe '#title' do - it 'calls Parent#title' do - expect_any_instance_of(Parent).to receive(:title) - child.title - end - end - - describe '#content' do - it 'calls #title' do - expect_any_instance_of(Parent).to receive(:title) - child.content - end - end - - describe '#target' do - it 'calls Parent#target' do - expect_any_instance_of(Parent).to receive(:target) - child.target - end - end - - describe '#object_type' do - it 'returns :activity' do - expect(child.object_type).to eq :activity - end - end - - describe '#thread' do - it 'calls Parent#thread' do - expect_any_instance_of(Parent).to receive(:thread) - child.thread - end - end - - describe '#hidden?' do - it 'returns false' do - expect(child.hidden?).to be false - end - end -end diff --git a/spec/models/remote_profile_spec.rb b/spec/models/remote_profile_spec.rb deleted file mode 100644 index da5048f0a4..0000000000 --- a/spec/models/remote_profile_spec.rb +++ /dev/null @@ -1,143 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe RemoteProfile do - let(:remote_profile) { RemoteProfile.new(body) } - let(:body) do - <<-XML - - John - XML - end - - describe '.initialize' do - it 'calls Nokogiri::XML.parse' do - expect(Nokogiri::XML).to receive(:parse).with(body, nil, 'utf-8') - RemoteProfile.new(body) - end - - it 'sets document' do - remote_profile = RemoteProfile.new(body) - expect(remote_profile).not_to be nil - end - end - - describe '#root' do - let(:document) { remote_profile.document } - - it 'callse document.at_xpath' do - expect(document).to receive(:at_xpath).with( - '/atom:feed|/atom:entry', - atom: OStatus::TagManager::XMLNS - ) - - remote_profile.root - end - end - - describe '#author' do - let(:root) { remote_profile.root } - - it 'calls root.at_xpath' do - expect(root).to receive(:at_xpath).with( - './atom:author|./dfrn:owner', - atom: OStatus::TagManager::XMLNS, - dfrn: OStatus::TagManager::DFRN_XMLNS - ) - - remote_profile.author - end - end - - describe '#hub_link' do - let(:root) { remote_profile.root } - - it 'calls #link_href_from_xml' do - expect(remote_profile).to receive(:link_href_from_xml).with(root, 'hub') - remote_profile.hub_link - end - end - - describe '#display_name' do - let(:author) { remote_profile.author } - - it 'calls author.at_xpath.content' do - expect(author).to receive_message_chain(:at_xpath, :content).with( - './poco:displayName', - poco: OStatus::TagManager::POCO_XMLNS - ).with(no_args) - - remote_profile.display_name - end - end - - describe '#note' do - let(:author) { remote_profile.author } - - it 'calls author.at_xpath.content' do - expect(author).to receive_message_chain(:at_xpath, :content).with( - './atom:summary|./poco:note', - atom: OStatus::TagManager::XMLNS, - poco: OStatus::TagManager::POCO_XMLNS - ).with(no_args) - - remote_profile.note - end - end - - describe '#scope' do - let(:author) { remote_profile.author } - - it 'calls author.at_xpath.content' do - expect(author).to receive_message_chain(:at_xpath, :content).with( - './mastodon:scope', - mastodon: OStatus::TagManager::MTDN_XMLNS - ).with(no_args) - - remote_profile.scope - end - end - - describe '#avatar' do - let(:author) { remote_profile.author } - - it 'calls #link_href_from_xml' do - expect(remote_profile).to receive(:link_href_from_xml).with(author, 'avatar') - remote_profile.avatar - end - end - - describe '#header' do - let(:author) { remote_profile.author } - - it 'calls #link_href_from_xml' do - expect(remote_profile).to receive(:link_href_from_xml).with(author, 'header') - remote_profile.header - end - end - - describe '#locked?' do - before do - allow(remote_profile).to receive(:scope).and_return(scope) - end - - subject { remote_profile.locked? } - - context 'scope is private' do - let(:scope) { 'private' } - - it 'returns true' do - is_expected.to be true - end - end - - context 'scope is not private' do - let(:scope) { 'public' } - - it 'returns false' do - is_expected.to be false - end - end - end -end diff --git a/spec/models/stream_entry_spec.rb b/spec/models/stream_entry_spec.rb deleted file mode 100644 index 8f8bfbd583..0000000000 --- a/spec/models/stream_entry_spec.rb +++ /dev/null @@ -1,192 +0,0 @@ -require 'rails_helper' - -RSpec.describe StreamEntry, type: :model do - let(:alice) { Fabricate(:account, username: 'alice') } - let(:bob) { Fabricate(:account, username: 'bob') } - let(:status) { Fabricate(:status, account: alice) } - let(:reblog) { Fabricate(:status, account: bob, reblog: status) } - let(:reply) { Fabricate(:status, account: bob, thread: status) } - let(:stream_entry) { Fabricate(:stream_entry, activity: activity) } - let(:activity) { reblog } - - describe '#object_type' do - before do - allow(stream_entry).to receive(:orphaned?).and_return(orphaned) - allow(stream_entry).to receive(:targeted?).and_return(targeted) - end - - subject { stream_entry.object_type } - - context 'orphaned? is true' do - let(:orphaned) { true } - let(:targeted) { false } - - it 'returns :activity' do - is_expected.to be :activity - end - end - - context 'targeted? is true' do - let(:orphaned) { false } - let(:targeted) { true } - - it 'returns :activity' do - is_expected.to be :activity - end - end - - context 'orphaned? and targeted? are false' do - let(:orphaned) { false } - let(:targeted) { false } - - context 'activity is reblog' do - let(:activity) { reblog } - - it 'returns :note' do - is_expected.to be :note - end - end - - context 'activity is reply' do - let(:activity) { reply } - - it 'returns :comment' do - is_expected.to be :comment - end - end - end - end - - describe '#verb' do - before do - allow(stream_entry).to receive(:orphaned?).and_return(orphaned) - end - - subject { stream_entry.verb } - - context 'orphaned? is true' do - let(:orphaned) { true } - - it 'returns :delete' do - is_expected.to be :delete - end - end - - context 'orphaned? is false' do - let(:orphaned) { false } - - context 'activity is reblog' do - let(:activity) { reblog } - - it 'returns :share' do - is_expected.to be :share - end - end - - context 'activity is reply' do - let(:activity) { reply } - - it 'returns :post' do - is_expected.to be :post - end - end - end - end - - describe '#mentions' do - before do - allow(stream_entry).to receive(:orphaned?).and_return(orphaned) - end - - subject { stream_entry.mentions } - - context 'orphaned? is true' do - let(:orphaned) { true } - - it 'returns []' do - is_expected.to eq [] - end - end - - context 'orphaned? is false' do - before do - reblog.mentions << Fabricate(:mention, account: alice) - reblog.mentions << Fabricate(:mention, account: bob) - end - - let(:orphaned) { false } - - it 'returns [Account] includes alice and bob' do - is_expected.to eq [alice, bob] - end - end - end - - describe '#targeted?' do - it 'returns true for a reblog' do - expect(reblog.stream_entry.targeted?).to be true - end - - it 'returns false otherwise' do - expect(status.stream_entry.targeted?).to be false - end - end - - describe '#threaded?' do - it 'returns true for a reply' do - expect(reply.stream_entry.threaded?).to be true - end - - it 'returns false otherwise' do - expect(status.stream_entry.threaded?).to be false - end - end - - describe 'delegated methods' do - context 'with a nil status' do - subject { described_class.new(status: nil) } - - it 'returns nil for target' do - expect(subject.target).to be_nil - end - - it 'returns nil for title' do - expect(subject.title).to be_nil - end - - it 'returns nil for content' do - expect(subject.content).to be_nil - end - - it 'returns nil for thread' do - expect(subject.thread).to be_nil - end - end - - context 'with a real status' do - let(:original) { Fabricate(:status, text: 'Test status') } - let(:status) { Fabricate(:status, reblog: original, thread: original) } - subject { described_class.new(status: status) } - - it 'delegates target' do - expect(status.target).not_to be_nil - expect(subject.target).to eq(status.target) - end - - it 'delegates title' do - expect(status.title).not_to be_nil - expect(subject.title).to eq(status.title) - end - - it 'delegates content' do - expect(status.content).not_to be_nil - expect(subject.content).to eq(status.content) - end - - it 'delegates thread' do - expect(status.thread).not_to be_nil - expect(subject.thread).to eq(status.thread) - end - end - end -end diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb index 35a804f2b9..b1abd79b0f 100644 --- a/spec/services/process_mentions_service_spec.rb +++ b/spec/services/process_mentions_service_spec.rb @@ -15,8 +15,8 @@ RSpec.describe ProcessMentionsService, type: :service do subject.call(status) end - it 'creates a mention' do - expect(remote_user.mentions.where(status: status).count).to eq 1 + it 'does not create a mention' do + expect(remote_user.mentions.where(status: status).count).to eq 0 end end diff --git a/spec/services/suspend_account_service_spec.rb b/spec/services/suspend_account_service_spec.rb index 6f45762aa3..896ac17a3b 100644 --- a/spec/services/suspend_account_service_spec.rb +++ b/spec/services/suspend_account_service_spec.rb @@ -27,14 +27,13 @@ RSpec.describe SuspendAccountService, type: :service do [ account.statuses, account.media_attachments, - account.stream_entries, account.notifications, account.favourites, account.active_relationships, account.passive_relationships, account.subscriptions ].map(&:count) - }.from([1, 1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0, 0]) + }.from([1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0]) end it 'sends a delete actor activity to all known inboxes' do @@ -70,14 +69,13 @@ RSpec.describe SuspendAccountService, type: :service do [ remote_bob.statuses, remote_bob.media_attachments, - remote_bob.stream_entries, remote_bob.notifications, remote_bob.favourites, remote_bob.active_relationships, remote_bob.passive_relationships, remote_bob.subscriptions ].map(&:count) - }.from([1, 1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0, 0]) + }.from([1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0]) end it 'sends a reject follow to follwer inboxes' do diff --git a/spec/views/stream_entries/show.html.haml_spec.rb b/spec/views/statuses/show.html.haml_spec.rb similarity index 83% rename from spec/views/stream_entries/show.html.haml_spec.rb rename to spec/views/statuses/show.html.haml_spec.rb index 93f0adb991..dbda3b6655 100644 --- a/spec/views/stream_entries/show.html.haml_spec.rb +++ b/spec/views/statuses/show.html.haml_spec.rb @@ -2,10 +2,9 @@ require 'rails_helper' -describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true do +describe 'statuses/show.html.haml', without_verify_partial_doubles: true do before do double(:api_oembed_url => '') - double(:account_stream_entry_url => '') allow(view).to receive(:show_landing_strip?).and_return(true) allow(view).to receive(:site_title).and_return('example site') allow(view).to receive(:site_hostname).and_return('example.com') @@ -23,9 +22,7 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d reply = Fabricate(:status, account: bob, thread: status, text: 'Hello Alice') assign(:status, status) - assign(:stream_entry, status.stream_entry) assign(:account, alice) - assign(:type, status.stream_entry.activity_type.downcase) assign(:descendant_threads, []) render @@ -46,11 +43,9 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d comment = Fabricate(:status, account: carl, thread: reply, text: 'Hello Bob') assign(:status, reply) - assign(:stream_entry, reply.stream_entry) assign(:account, alice) - assign(:type, reply.stream_entry.activity_type.downcase) - assign(:ancestors, reply.stream_entry.activity.ancestors(1, bob)) - assign(:descendant_threads, [{ statuses: reply.stream_entry.activity.descendants(1) }]) + assign(:ancestors, reply.ancestors(1, bob)) + assign(:descendant_threads, [{ statuses: reply.descendants(1) }]) render @@ -71,9 +66,7 @@ describe 'stream_entries/show.html.haml', without_verify_partial_doubles: true d status = Fabricate(:status, account: alice, text: 'Hello World') assign(:status, status) - assign(:stream_entry, status.stream_entry) assign(:account, alice) - assign(:type, status.stream_entry.activity_type.downcase) assign(:descendant_threads, []) render From 3ece6e25e6771c17bf79774564e6999003499f46 Mon Sep 17 00:00:00 2001 From: Georg Gadinger Date: Sun, 7 Jul 2019 18:13:19 +0200 Subject: [PATCH 05/71] Update fuubar dependency to 2.4.1 (#11248) See also: thekompanee/fuubar#111 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6043a2fa9e..69f7889579 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -231,7 +231,7 @@ GEM fugit (1.1.6) et-orbi (~> 1.1, >= 1.1.6) raabro (~> 1.1) - fuubar (2.4.0) + fuubar (2.4.1) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) get_process_mem (0.2.3) From 87c1ad4ea3c4714e8698a697a380471cc2d2d496 Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 8 Jul 2019 02:24:25 +0200 Subject: [PATCH 06/71] Fix BackupService crashing when an attachment is missing (#11241) * Fix BackupService crashing when an attachment is missing For various reasons such as admin error or out-of-sync media and database backups, it might be possible for local attachments to be lost. This commit allows the BackupService to continue its work even if some media file is missing. * Change error message --- app/services/backup_service.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb index 4cfa22ab89..12e4fa8b44 100644 --- a/app/services/backup_service.rb +++ b/app/services/backup_service.rb @@ -142,5 +142,7 @@ class BackupService < BaseService io.write(buffer) end end + rescue Errno::ENOENT + Rails.logger.warn "Could not backup file #{filename}: file not found" end end From 239befc6e8ab51f802100696947e1696b53c5aa7 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2019 16:48:44 +0900 Subject: [PATCH 07/71] Bump react-redux from 6.0.1 to 7.1.0 (#11256) Bumps [react-redux](https://github.com/reduxjs/react-redux) from 6.0.1 to 7.1.0. - [Release notes](https://github.com/reduxjs/react-redux/releases) - [Changelog](https://github.com/reduxjs/react-redux/blob/master/CHANGELOG.md) - [Commits](https://github.com/reduxjs/react-redux/compare/v6.0.1...v7.1.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 358f5b1563..653439d3ef 100644 --- a/package.json +++ b/package.json @@ -133,7 +133,7 @@ "react-motion": "^0.5.2", "react-notification": "^6.8.4", "react-overlays": "^0.8.3", - "react-redux": "^6.0.1", + "react-redux": "^7.1.0", "react-redux-loading-bar": "^4.0.8", "react-router-dom": "^4.1.1", "react-router-scroll-4": "^1.0.0-beta.1", diff --git a/yarn.lock b/yarn.lock index 6698f6a19e..61e9196f5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -768,7 +768,7 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5": version "7.4.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12" integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ== @@ -8189,7 +8189,7 @@ react-intl@^2.9.0: intl-relativeformat "^2.1.0" invariant "^2.1.1" -react-is@^16.3.2, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.2, react-is@^16.8.4, react-is@^16.8.6: +react-is@^16.3.2, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6: version "16.8.6" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA== @@ -8244,17 +8244,17 @@ react-redux-loading-bar@^4.0.8: prop-types "^15.6.2" react-lifecycles-compat "^3.0.2" -react-redux@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d" - integrity sha512-T52I52Kxhbqy/6TEfBv85rQSDz6+Y28V/pf52vDWs1YRXG19mcFOGfHnY2HsNFHyhP+ST34Aih98fvt6tqwVcQ== +react-redux@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.0.tgz#72af7cf490a74acdc516ea9c1dd80e25af9ea0b2" + integrity sha512-hyu/PoFK3vZgdLTg9ozbt7WF3GgX5+Yn3pZm5/96/o4UueXA+zj08aiSC9Mfj2WtD1bvpIb3C5yvskzZySzzaw== dependencies: - "@babel/runtime" "^7.3.1" + "@babel/runtime" "^7.4.5" hoist-non-react-statics "^3.3.0" invariant "^2.2.4" loose-envify "^1.4.0" prop-types "^15.7.2" - react-is "^16.8.2" + react-is "^16.8.6" react-router-dom@^4.1.1: version "4.3.1" From 6a313f872a5b6b2e99f4503cb4a4f20d09f619b5 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2019 16:49:26 +0900 Subject: [PATCH 08/71] Bump intl-relativeformat from 2.2.0 to 6.4.2 (#11255) Bumps [intl-relativeformat](https://github.com/formatjs/formatjs) from 2.2.0 to 6.4.2. - [Release notes](https://github.com/formatjs/formatjs/releases) - [Commits](https://github.com/formatjs/formatjs/compare/intl-relativeformat@2.2.0...intl-relativeformat@6.4.2) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 653439d3ef..b1a130849e 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "intersection-observer": "^0.7.0", "intl": "^1.2.5", "intl-messageformat": "^2.2.0", - "intl-relativeformat": "^2.2.0", + "intl-relativeformat": "^6.4.2", "is-nan": "^1.2.1", "js-yaml": "^3.13.1", "lodash": "^4.7.11", diff --git a/yarn.lock b/yarn.lock index 61e9196f5f..c7a3930044 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5075,13 +5075,18 @@ intl-messageformat@^2.0.0, intl-messageformat@^2.1.0, intl-messageformat@^2.2.0: dependencies: intl-messageformat-parser "1.4.0" -intl-relativeformat@^2.1.0, intl-relativeformat@^2.2.0: +intl-relativeformat@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/intl-relativeformat/-/intl-relativeformat-2.2.0.tgz#6aca95d019ec8d30b6c5653b6629f9983ea5b6c5" integrity sha512-4bV/7kSKaPEmu6ArxXf9xjv1ny74Zkwuey8Pm01NH4zggPP7JHwg2STk8Y3JdspCKRDriwIyLRfEXnj2ZLr4Bw== dependencies: intl-messageformat "^2.0.0" +intl-relativeformat@^6.4.2: + version "6.4.2" + resolved "https://registry.yarnpkg.com/intl-relativeformat/-/intl-relativeformat-6.4.2.tgz#431f9818449f5b48c209610ff1428d0c663c667f" + integrity sha512-yaOimRUQEn1wOfVGk43H+EVCrxQ5WFEvtYBx4Ffa6QpEHIi6UOuvshx6RltuqIF5UM8xdF4SkzFHXXOnYXlgBA== + intl@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/intl/-/intl-1.2.5.tgz#82244a2190c4e419f8371f5aa34daa3420e2abde" From 1a8bbcdca4f3b9101530526417e7dce89e3c2bd6 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2019 16:49:47 +0900 Subject: [PATCH 09/71] Bump compression-webpack-plugin from 2.0.0 to 3.0.0 (#11224) Bumps [compression-webpack-plugin](https://github.com/webpack-contrib/compression-webpack-plugin) from 2.0.0 to 3.0.0. - [Release notes](https://github.com/webpack-contrib/compression-webpack-plugin/releases) - [Changelog](https://github.com/webpack-contrib/compression-webpack-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/webpack-contrib/compression-webpack-plugin/compare/v2.0.0...v3.0.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 79 ++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index b1a130849e..928d9d72b7 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "babel-runtime": "^6.26.0", "blurhash": "^1.0.0", "classnames": "^2.2.5", - "compression-webpack-plugin": "^2.0.0", + "compression-webpack-plugin": "^3.0.0", "cross-env": "^5.1.4", "css-loader": "^2.1.1", "cssnano": "^4.1.10", diff --git a/yarn.lock b/yarn.lock index c7a3930044..8417836708 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2522,13 +2522,13 @@ compressible@~2.0.16: dependencies: mime-db ">= 1.40.0 < 2" -compression-webpack-plugin@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-2.0.0.tgz#46476350c1eb27f783dccc79ac2f709baa2cffbc" - integrity sha512-bDgd7oTUZC8EkRx8j0sjyCfeiO+e5sFcfgaFcjVhfQf5lLya7oY2BczxcJ7IUuVjz5m6fy8IECFmVFew3xLk8Q== +compression-webpack-plugin@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-3.0.0.tgz#097d2e4d95c3a14cb5c8ed20899009ab5b9bbca0" + integrity sha512-ls+oKw4eRbvaSv/hj9NmctihhBcR26j76JxV0bLRLcWhrUBdQFgd06z/Kgg7exyQvtWWP484wZxs0gIUX3NO0Q== dependencies: cacache "^11.2.0" - find-cache-dir "^2.0.0" + find-cache-dir "^3.0.0" neo-async "^2.5.0" schema-utils "^1.0.0" serialize-javascript "^1.4.0" @@ -4190,6 +4190,15 @@ find-cache-dir@^2.0.0: make-dir "^1.0.0" pkg-dir "^3.0.0" +find-cache-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.0.0.tgz#cd4b7dd97b7185b7e17dbfe2d6e4115ee3eeb8fc" + integrity sha512-t7ulV1fmbxh5G9l/492O1p5+EBbr3uwpt6odhFTMc+nWyhmbloe+ja9BZ8pIBtqFWhOmCWVjx+pTW4zDkFoclw== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.0" + pkg-dir "^4.1.0" + find-root@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" @@ -4209,6 +4218,14 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + findup-sync@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" @@ -6171,6 +6188,13 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + lodash.capitalize@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9" @@ -6272,6 +6296,13 @@ make-dir@^1.0.0, make-dir@^1.3.0: dependencies: pify "^3.0.0" +make-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.0.tgz#1b5f39f6b9270ed33f9f054c5c0f84304989f801" + integrity sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw== + dependencies: + semver "^6.0.0" + makeerror@1.0.x: version "1.0.11" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" @@ -7119,6 +7150,13 @@ p-limit@^2.0.0: dependencies: p-try "^2.0.0" +p-limit@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" + integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ== + dependencies: + p-try "^2.0.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -7133,6 +7171,13 @@ p-locate@^3.0.0: dependencies: p-limit "^2.0.0" +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + p-map@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" @@ -7274,6 +7319,11 @@ path-exists@^3.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -7439,6 +7489,13 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" +pkg-dir@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + pluralize@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" @@ -8972,6 +9029,11 @@ semver@4.3.2: resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7" integrity sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c= +semver@^6.0.0: + version "6.1.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.3.tgz#ef997a1a024f67dd48a7f155df88bb7b5c6c3fc7" + integrity sha512-aymF+56WJJMyXQHcd4hlK4N75rwj5RQpfW8ePlQnJsTYOBLlLbcIErR/G1s9SkIvKBqOudR3KAx4wEqP+F1hNQ== + semver@^6.1.0, semver@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.1.tgz#53f53da9b30b2103cd4f15eab3a18ecbcb210c9b" @@ -8996,12 +9058,7 @@ send@0.17.1: range-parser "~1.2.1" statuses "~1.5.0" -serialize-javascript@^1.4.0: - version "1.6.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.6.1.tgz#4d1f697ec49429a847ca6f442a2a755126c4d879" - integrity sha512-A5MOagrPFga4YaKQSWHryl7AXvbQkEqpw4NNYMTNYUNV51bA8ABHgYFpqKx+YFFrw59xMV1qGH1R4AgoNIVgCw== - -serialize-javascript@^1.7.0: +serialize-javascript@^1.4.0, serialize-javascript@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.7.0.tgz#d6e0dfb2a3832a8c94468e6eb1db97e55a192a65" integrity sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA== From 76d5fd1c5c74eb19d5a874d920ceb20cfcbe376d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2019 18:24:38 +0900 Subject: [PATCH 10/71] Bump @babel/plugin-proposal-class-properties from 7.4.4 to 7.5.0 (#11254) Bumps [@babel/plugin-proposal-class-properties](https://github.com/babel/babel) from 7.4.4 to 7.5.0. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/master/CHANGELOG.md) - [Commits](https://github.com/babel/babel/compare/v7.4.4...v7.5.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 928d9d72b7..5e1e940c35 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "private": true, "dependencies": { "@babel/core": "^7.4.5", - "@babel/plugin-proposal-class-properties": "^7.4.4", + "@babel/plugin-proposal-class-properties": "^7.5.0", "@babel/plugin-proposal-decorators": "^7.4.4", "@babel/plugin-proposal-object-rest-spread": "^7.4.4", "@babel/plugin-syntax-dynamic-import": "^7.2.0", diff --git a/yarn.lock b/yarn.lock index 8417836708..7cb0182101 100644 --- a/yarn.lock +++ b/yarn.lock @@ -103,10 +103,10 @@ "@babel/traverse" "^7.4.4" "@babel/types" "^7.4.4" -"@babel/helper-create-class-features-plugin@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.4.4.tgz#fc3d690af6554cc9efc607364a82d48f58736dba" - integrity sha512-UbBHIa2qeAGgyiNR9RszVF7bUHEdgS4JAUNT8SiqrAN6YJVxlOxeLr5pBzb5kan302dejJ9nla4RyKcR1XT6XA== +"@babel/helper-create-class-features-plugin@^7.4.4", "@babel/helper-create-class-features-plugin@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.5.0.tgz#02edb97f512d44ba23b3227f1bf2ed43454edac5" + integrity sha512-EAoMc3hE5vE5LNhMqDOwB1usHvmRjCDAnH8CD4PVkX9/Yr3W/tcz8xE8QvdZxfsFBDICwZnF2UTHIqslRpvxmA== dependencies: "@babel/helper-function-name" "^7.1.0" "@babel/helper-member-expression-to-functions" "^7.0.0" @@ -297,12 +297,12 @@ "@babel/helper-remap-async-to-generator" "^7.1.0" "@babel/plugin-syntax-async-generators" "^7.2.0" -"@babel/plugin-proposal-class-properties@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.4.4.tgz#93a6486eed86d53452ab9bab35e368e9461198ce" - integrity sha512-WjKTI8g8d5w1Bc9zgwSz2nfrsNQsXcCf9J9cdCvrJV6RF56yztwm4TmJC0MgJ9tvwO9gUA/mcYe89bLdGfiXFg== +"@babel/plugin-proposal-class-properties@^7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.0.tgz#5bc6a0537d286fcb4fd4e89975adbca334987007" + integrity sha512-9L/JfPCT+kShiiTTzcnBJ8cOwdKVmlC1RcCf9F0F9tERVrM4iWtWnXtjWCRqNm2la2BxO1MPArWNsU9zsSJWSQ== dependencies: - "@babel/helper-create-class-features-plugin" "^7.4.4" + "@babel/helper-create-class-features-plugin" "^7.5.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-decorators@^7.4.4": From d0b40ae7dd649ba093cc75866f3e08c40f404dc1 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2019 18:25:20 +0900 Subject: [PATCH 11/71] Bump eslint-plugin-react from 7.12.1 to 7.14.2 (#11253) Bumps [eslint-plugin-react](https://github.com/yannickcr/eslint-plugin-react) from 7.12.1 to 7.14.2. - [Release notes](https://github.com/yannickcr/eslint-plugin-react/releases) - [Changelog](https://github.com/yannickcr/eslint-plugin-react/blob/master/CHANGELOG.md) - [Commits](https://github.com/yannickcr/eslint-plugin-react/compare/v7.12.1...v7.14.2) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 33 ++++++++++++++++++--------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 5e1e940c35..7a27de5f2a 100644 --- a/package.json +++ b/package.json @@ -174,7 +174,7 @@ "eslint-plugin-import": "~2.17.3", "eslint-plugin-jsx-a11y": "~6.2.1", "eslint-plugin-promise": "~4.2.1", - "eslint-plugin-react": "~7.12.1", + "eslint-plugin-react": "~7.14.2", "jest": "^24.8.0", "raf": "^3.4.1", "react-intl-translations-manager": "^5.0.3", diff --git a/yarn.lock b/yarn.lock index 7cb0182101..97096a30f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3698,18 +3698,20 @@ eslint-plugin-promise@~4.2.1: resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a" integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw== -eslint-plugin-react@~7.12.1: - version "7.12.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.12.1.tgz#b9c4639f72469ff317ac31e3bd630d22d0dbf8f4" - integrity sha512-1YyXVhp6KSB+xRC1BWzmlA4BH9Wp9jMMBE6AJizxuk+bg/KUJpQGRwsU1/q1pV8rM6oEdLCxunXn7Nfh2BOWBg== +eslint-plugin-react@~7.14.2: + version "7.14.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.14.2.tgz#94c193cc77a899ac0ecbb2766fbef88685b7ecc1" + integrity sha512-jZdnKe3ip7FQOdjxks9XPN0pjUKZYq48OggNMd16Sk+8VXx6JOvXmlElxROCgp7tiUsTsze3jd78s/9AFJP2mA== dependencies: array-includes "^3.0.3" doctrine "^2.1.0" has "^1.0.3" - jsx-ast-utils "^2.0.1" + jsx-ast-utils "^2.1.0" + object.entries "^1.1.0" object.fromentries "^2.0.0" - prop-types "^15.6.2" - resolve "^1.9.0" + object.values "^1.1.0" + prop-types "^15.7.2" + resolve "^1.10.1" eslint-scope@3.7.1: version "3.7.1" @@ -6047,12 +6049,13 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jsx-ast-utils@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f" - integrity sha1-6AGxs5mF4g//yHtA43SAgOLcrH8= +jsx-ast-utils@^2.0.1, jsx-ast-utils@^2.1.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.1.tgz#4d4973ebf8b9d2837ee91a8208cc66f3a2776cfb" + integrity sha512-v3FxCcAf20DayI+uxnCuw795+oOIkVu6EnJ1+kSzhqqTZHNkTZ7B66ZgLp4oLJ/gbA64cI0B7WRoHZMSRdyVRQ== dependencies: array-includes "^3.0.3" + object.assign "^4.1.0" keycode@^2.1.7: version "2.2.0" @@ -8802,10 +8805,10 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.10.0, resolve@^1.11.0, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.8.1, resolve@^1.9.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.0.tgz#4014870ba296176b86343d50b60f3b50609ce232" - integrity sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw== +resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.8.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.1.tgz#ea10d8110376982fef578df8fc30b9ac30a07a3e" + integrity sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw== dependencies: path-parse "^1.0.6" From f935004a7492b2be4979a344efcf1cf60fbc46bb Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2019 18:26:12 +0900 Subject: [PATCH 12/71] Bump tzinfo-data from 1.2019.1 to 1.2019.2 (#11258) Bumps [tzinfo-data](https://github.com/tzinfo/tzinfo-data) from 1.2019.1 to 1.2019.2. - [Release notes](https://github.com/tzinfo/tzinfo-data/releases) - [Commits](https://github.com/tzinfo/tzinfo-data/compare/v1.2019.1...v1.2019.2) Signed-off-by: dependabot-preview[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 69f7889579..a3c14f16af 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -612,7 +612,7 @@ GEM unf (~> 0.1.0) tzinfo (1.2.5) thread_safe (~> 0.1) - tzinfo-data (1.2019.1) + tzinfo-data (1.2019.2) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext From 01ee35f14ad443d27ea16f17e194cf1a871a73c0 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2019 18:26:41 +0900 Subject: [PATCH 13/71] Bump aws-sdk-s3 from 1.43.0 to 1.45.0 (#11262) Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.43.0 to 1.45.0. - [Release notes](https://github.com/aws/aws-sdk-ruby/releases) - [Changelog](https://github.com/aws/aws-sdk-ruby/blob/master/gems/aws-sdk-s3/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-ruby/compare/v1.43.0...v1.45.0) Signed-off-by: dependabot-preview[bot] --- Gemfile | 2 +- Gemfile.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index 2fcb603c33..4ad055dae8 100644 --- a/Gemfile +++ b/Gemfile @@ -15,7 +15,7 @@ gem 'makara', '~> 0.4' gem 'pghero', '~> 2.2' gem 'dotenv-rails', '~> 2.7' -gem 'aws-sdk-s3', '~> 1.43', require: false +gem 'aws-sdk-s3', '~> 1.45', require: false gem 'fog-core', '<= 2.1.0' gem 'fog-openstack', '~> 0.3', require: false gem 'paperclip', '~> 6.0' diff --git a/Gemfile.lock b/Gemfile.lock index a3c14f16af..8061d7ec4d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -76,17 +76,17 @@ GEM av (0.9.0) cocaine (~> 0.5.3) aws-eventstream (1.0.3) - aws-partitions (1.177.0) - aws-sdk-core (3.56.0) + aws-partitions (1.184.0) + aws-sdk-core (3.59.0) aws-eventstream (~> 1.0, >= 1.0.2) aws-partitions (~> 1.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.22.0) - aws-sdk-core (~> 3, >= 3.56.0) + aws-sdk-kms (1.23.0) + aws-sdk-core (~> 3, >= 3.58.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.43.0) - aws-sdk-core (~> 3, >= 3.56.0) + aws-sdk-s3 (1.45.0) + aws-sdk-core (~> 3, >= 3.58.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) aws-sigv4 (1.1.0) @@ -647,7 +647,7 @@ DEPENDENCIES active_record_query_trace (~> 1.6) addressable (~> 2.6) annotate (~> 2.7) - aws-sdk-s3 (~> 1.43) + aws-sdk-s3 (~> 1.45) better_errors (~> 2.5) binding_of_caller (~> 0.7) blurhash (~> 0.1) From d76ca8964ce77c465e3b92b9e80db5cadf0631d6 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2019 18:28:33 +0900 Subject: [PATCH 14/71] Bump simplecov from 0.16.1 to 0.17.0 (#11260) Bumps [simplecov](https://github.com/colszowka/simplecov) from 0.16.1 to 0.17.0. - [Release notes](https://github.com/colszowka/simplecov/releases) - [Changelog](https://github.com/colszowka/simplecov/blob/master/CHANGELOG.md) - [Commits](https://github.com/colszowka/simplecov/compare/v0.16.1...v0.17.0) Signed-off-by: dependabot-preview[bot] --- Gemfile | 2 +- Gemfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 4ad055dae8..613515628e 100644 --- a/Gemfile +++ b/Gemfile @@ -114,7 +114,7 @@ group :test do gem 'microformats', '~> 4.1' gem 'rails-controller-testing', '~> 1.0' gem 'rspec-sidekiq', '~> 3.0' - gem 'simplecov', '~> 0.16', require: false + gem 'simplecov', '~> 0.17', require: false gem 'webmock', '~> 3.6' gem 'parallel_tests', '~> 2.29' end diff --git a/Gemfile.lock b/Gemfile.lock index 8061d7ec4d..34de2aaf9a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -183,7 +183,7 @@ GEM devise (>= 4.0.0) rpam2 (~> 4.0) diff-lcs (1.3) - docile (1.3.0) + docile (1.3.2) domain_name (0.5.20180417) unf (>= 0.0.5, < 1.0.0) doorkeeper (5.1.0) @@ -289,7 +289,7 @@ GEM iso-639 (0.2.8) jaro_winkler (1.5.3) jmespath (1.4.0) - json (2.1.0) + json (2.2.0) json-ld (3.0.2) multi_json (~> 1.12) rdf (>= 2.2.8, < 4.0) @@ -565,7 +565,7 @@ GEM simple_form (4.1.0) actionpack (>= 5.0) activemodel (>= 5.0) - simplecov (0.16.1) + simplecov (0.17.0) docile (~> 1.1) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) @@ -747,7 +747,7 @@ DEPENDENCIES sidekiq-unique-jobs (~> 6.0) simple-navigation (~> 4.0) simple_form (~> 4.1) - simplecov (~> 0.16) + simplecov (~> 0.17) sprockets-rails (~> 3.2) stackprof stoplight (~> 2.1.3) From f14776475d285bb7d2831a20192c1611c733f410 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2019 18:29:00 +0900 Subject: [PATCH 15/71] Bump faker from 1.9.3 to 1.9.6 (#11259) Bumps [faker](https://github.com/stympy/faker) from 1.9.3 to 1.9.6. - [Release notes](https://github.com/stympy/faker/releases) - [Changelog](https://github.com/stympy/faker/blob/master/CHANGELOG.md) - [Commits](https://github.com/stympy/faker/compare/v1.9.3...1.9.6) Signed-off-by: dependabot-preview[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 34de2aaf9a..340bbcdd87 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -208,7 +208,7 @@ GEM tzinfo excon (0.62.0) fabrication (2.20.2) - faker (1.9.3) + faker (1.9.6) i18n (>= 0.7) faraday (0.15.0) multipart-post (>= 1.2, < 3) From 63c7fe8e4892b22e80c015bf0ecb04496318623b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 8 Jul 2019 12:03:45 +0200 Subject: [PATCH 16/71] Refactor controllers for statuses, accounts, and more (#11249) --- app/controllers/about_controller.rb | 16 +- app/controllers/accounts_controller.rb | 15 +- .../activitypub/collections_controller.rb | 16 +- .../activitypub/inboxes_controller.rb | 7 +- .../activitypub/outboxes_controller.rb | 6 +- .../activitypub/replies_controller.rb | 68 ++++++++ app/controllers/api/proofs_controller.rb | 17 +- app/controllers/application_controller.rb | 4 - .../concerns/account_controller_concern.rb | 34 +--- .../concerns/account_owned_concern.rb | 33 ++++ .../concerns/status_controller_concern.rb | 87 ++++++++++ app/controllers/custom_css_controller.rb | 1 + app/controllers/emojis_controller.rb | 5 +- .../follower_accounts_controller.rb | 2 +- .../following_accounts_controller.rb | 2 +- app/controllers/home_controller.rb | 2 +- app/controllers/intents_controller.rb | 1 + app/controllers/manifests_controller.rb | 1 + app/controllers/media_controller.rb | 1 - .../public_timelines_controller.rb | 14 +- app/controllers/remote_follow_controller.rb | 12 +- app/controllers/statuses_controller.rb | 164 ++---------------- app/controllers/tags_controller.rb | 18 +- .../well_known/host_meta_controller.rb | 2 +- .../well_known/webfinger_controller.rb | 9 +- app/lib/activitypub/activity/announce.rb | 2 +- app/lib/activitypub/activity/create.rb | 2 +- app/lib/activitypub/activity/delete.rb | 2 +- app/lib/activitypub/tag_manager.rb | 2 +- app/models/status.rb | 9 +- .../activitypub/activity_serializer.rb | 3 + .../activitypub/actor_serializer.rb | 2 + .../activitypub/collection_serializer.rb | 2 + .../activitypub/emoji_serializer.rb | 2 + .../activitypub/note_serializer.rb | 2 + app/services/process_hashtags_service.rb | 2 +- app/views/statuses/_simple_status.html.haml | 4 +- config/routes.rb | 3 +- .../account_controller_concern_spec.rb | 2 +- spec/controllers/statuses_controller_spec.rb | 4 +- spec/requests/link_headers_spec.rb | 8 +- 41 files changed, 299 insertions(+), 289 deletions(-) create mode 100644 app/controllers/activitypub/replies_controller.rb create mode 100644 app/controllers/concerns/account_owned_concern.rb create mode 100644 app/controllers/concerns/status_controller_concern.rb diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 52a51fd62e..761c7f5cdf 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -3,11 +3,11 @@ class AboutController < ApplicationController layout 'public' - before_action :set_instance_presenter, only: [:show, :more, :terms] + before_action :set_body_classes, only: :show + before_action :set_instance_presenter + before_action :set_expires_in - def show - @hide_navbar = true - end + def show; end def more; end @@ -27,4 +27,12 @@ class AboutController < ApplicationController def set_instance_presenter @instance_presenter = InstancePresenter.new end + + def set_body_classes + @hide_navbar = true + end + + def set_expires_in + expires_in 0, public: true + end end diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 0657073780..3184a73cb1 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -6,13 +6,13 @@ class AccountsController < ApplicationController include AccountControllerConcern before_action :set_cache_headers + before_action :set_body_classes def show respond_to do |format| format.html do - mark_cacheable! unless user_signed_in? + expires_in 0, public: true unless user_signed_in? - @body_classes = 'with-modals' @pinned_statuses = [] @endorsed_accounts = @account.endorsed_accounts.to_a.sample(4) @@ -32,22 +32,25 @@ class AccountsController < ApplicationController end format.rss do - mark_cacheable! + expires_in 0, public: true @statuses = cache_collection(default_statuses.without_reblogs.without_replies.limit(PAGE_SIZE), Status) render xml: RSS::AccountSerializer.render(@account, @statuses) end format.json do - render_cached_json(['activitypub', 'actor', @account], content_type: 'application/activity+json') do - ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter) - end + expires_in 3.minutes, public: true + render json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter end end end private + def set_body_classes + @body_classes = 'with-modals' + end + def show_pinned_statuses? [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? end diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index 012c3c5388..dd2f111b03 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -2,29 +2,19 @@ class ActivityPub::CollectionsController < Api::BaseController include SignatureVerification + include AccountOwnedConcern - before_action :set_account before_action :set_size before_action :set_statuses before_action :set_cache_headers def show - render_cached_json(['activitypub', 'collection', @account, params[:id]], content_type: 'application/activity+json') do - ActiveModelSerializers::SerializableResource.new( - collection_presenter, - serializer: ActivityPub::CollectionSerializer, - adapter: ActivityPub::Adapter, - skip_activities: true - ) - end + expires_in 3.minutes, public: true + render json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, skip_activities: true end private - def set_account - @account = Account.find_local!(params[:account_username]) - end - def set_statuses @statuses = scope_for_collection @statuses = cache_collection(@statuses, Status) diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index e2cd8eaedb..9be0676e14 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -3,8 +3,7 @@ class ActivityPub::InboxesController < Api::BaseController include SignatureVerification include JsonLdHelper - - before_action :set_account + include AccountOwnedConcern def create if unknown_deleted_account? @@ -27,8 +26,8 @@ class ActivityPub::InboxesController < Api::BaseController false end - def set_account - @account = Account.find_local!(params[:account_username]) if params[:account_username] + def account_required? + params[:account_username].present? end def body diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 5147afbf78..4c0b769f0f 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -4,8 +4,8 @@ class ActivityPub::OutboxesController < Api::BaseController LIMIT = 20 include SignatureVerification + include AccountOwnedConcern - before_action :set_account before_action :set_statuses before_action :set_cache_headers @@ -17,10 +17,6 @@ class ActivityPub::OutboxesController < Api::BaseController private - def set_account - @account = Account.find_local!(params[:account_username]) - end - def outbox_presenter if page_requested? ActivityPub::CollectionPresenter.new( diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb new file mode 100644 index 0000000000..99b7b310f4 --- /dev/null +++ b/app/controllers/activitypub/replies_controller.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +class ActivityPub::RepliesController < Api::BaseController + include SignatureAuthentication + include Authorization + include AccountOwnedConcern + + DESCENDANTS_LIMIT = 60 + + before_action :set_status + before_action :set_cache_headers + before_action :set_replies + + def index + render json: replies_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', skip_activities: true + end + + private + + def set_status + @status = @account.statuses.find(params[:status_id]) + authorize @status, :show? + rescue Mastodon::NotPermittedError + raise ActiveRecord::RecordNotFound + end + + def set_replies + @replies = page_params[:other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses + @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted]) + @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id]) + end + + def replies_collection_presenter + page = ActivityPub::CollectionPresenter.new( + id: account_status_replies_url(@account, @status, page_params), + type: :unordered, + part_of: account_status_replies_url(@account, @status), + next: next_page, + items: @replies.map { |status| status.local ? status : status.id } + ) + + return page if page_requested? + + ActivityPub::CollectionPresenter.new( + id: account_status_replies_url(@account, @status), + type: :unordered, + first: page + ) + end + + def page_requested? + params[:page] == 'true' + end + + def next_page + account_status_replies_url( + @account, + @status, + page: true, + min_id: @replies&.last&.id, + other_accounts: !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT) + ) + end + + def page_params + params_slice(:other_accounts, :min_id).merge(page: true) + end +end diff --git a/app/controllers/api/proofs_controller.rb b/app/controllers/api/proofs_controller.rb index a84ad2014f..a98599eee6 100644 --- a/app/controllers/api/proofs_controller.rb +++ b/app/controllers/api/proofs_controller.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true class Api::ProofsController < Api::BaseController - before_action :set_account + include AccountOwnedConcern + before_action :set_provider - before_action :check_account_approval - before_action :check_account_suspension def index render json: @account, serializer: @provider.serializer_class @@ -16,15 +15,7 @@ class Api::ProofsController < Api::BaseController @provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound) end - def set_account - @account = Account.find_local!(params[:username]) - end - - def check_account_approval - not_found if @account.user_pending? - end - - def check_account_suspension - gone if @account.suspended? + def username_param + params[:username] end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bd8000db0f..cc8b8e4da6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -154,8 +154,4 @@ class ApplicationController < ActionController::Base def set_cache_headers response.headers['Vary'] = 'Accept' end - - def mark_cacheable! - expires_in 0, public: true - end end diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb index 1c422096c6..287a930da4 100644 --- a/app/controllers/concerns/account_controller_concern.rb +++ b/app/controllers/concerns/account_controller_concern.rb @@ -3,24 +3,19 @@ module AccountControllerConcern extend ActiveSupport::Concern + include AccountOwnedConcern + FOLLOW_PER_PAGE = 12 included do layout 'public' - before_action :set_account - before_action :check_account_approval - before_action :check_account_suspension before_action :set_instance_presenter before_action :set_link_headers end private - def set_account - @account = Account.find_local!(username_param) - end - def set_instance_presenter @instance_presenter = InstancePresenter.new end @@ -29,27 +24,15 @@ module AccountControllerConcern response.headers['Link'] = LinkHeader.new( [ webfinger_account_link, - atom_account_url_link, actor_url_link, ] ) end - def username_param - params[:account_username] - end - def webfinger_account_link [ webfinger_account_url, - [%w(rel lrdd), %w(type application/xrd+xml)], - ] - end - - def atom_account_url_link - [ - account_url(@account, format: 'atom'), - [%w(rel alternate), %w(type application/atom+xml)], + [%w(rel lrdd), %w(type application/jrd+json)], ] end @@ -63,15 +46,4 @@ module AccountControllerConcern def webfinger_account_url webfinger_url(resource: @account.to_webfinger_s) end - - def check_account_approval - not_found if @account.user_pending? - end - - def check_account_suspension - if @account.suspended? - expires_in(3.minutes, public: true) - gone - end - end end diff --git a/app/controllers/concerns/account_owned_concern.rb b/app/controllers/concerns/account_owned_concern.rb new file mode 100644 index 0000000000..99c240fe98 --- /dev/null +++ b/app/controllers/concerns/account_owned_concern.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module AccountOwnedConcern + extend ActiveSupport::Concern + + included do + before_action :set_account, if: :account_required? + before_action :check_account_approval, if: :account_required? + before_action :check_account_suspension, if: :account_required? + end + + private + + def account_required? + true + end + + def set_account + @account = Account.find_local!(username_param) + end + + def username_param + params[:account_username] + end + + def check_account_approval + not_found if @account.local? && @account.user_pending? + end + + def check_account_suspension + expires_in(3.minutes, public: true) && gone if @account.suspended? + end +end diff --git a/app/controllers/concerns/status_controller_concern.rb b/app/controllers/concerns/status_controller_concern.rb new file mode 100644 index 0000000000..62a7cf5086 --- /dev/null +++ b/app/controllers/concerns/status_controller_concern.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module StatusControllerConcern + extend ActiveSupport::Concern + + ANCESTORS_LIMIT = 40 + DESCENDANTS_LIMIT = 60 + DESCENDANTS_DEPTH_LIMIT = 20 + + def create_descendant_thread(starting_depth, statuses) + depth = starting_depth + statuses.size + + if depth < DESCENDANTS_DEPTH_LIMIT + { + statuses: statuses, + starting_depth: starting_depth, + } + else + next_status = statuses.pop + + { + statuses: statuses, + starting_depth: starting_depth, + next_status: next_status, + } + end + end + + def set_ancestors + @ancestors = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : [] + @next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift + end + + def set_descendants + @max_descendant_thread_id = params[:max_descendant_thread_id]&.to_i + @since_descendant_thread_id = params[:since_descendant_thread_id]&.to_i + + descendants = cache_collection( + @status.descendants( + DESCENDANTS_LIMIT, + current_account, + @max_descendant_thread_id, + @since_descendant_thread_id, + DESCENDANTS_DEPTH_LIMIT + ), + Status + ) + + @descendant_threads = [] + + if descendants.present? + statuses = [descendants.first] + starting_depth = 0 + + descendants.drop(1).each_with_index do |descendant, index| + if descendants[index].id == descendant.in_reply_to_id + statuses << descendant + else + @descendant_threads << create_descendant_thread(starting_depth, statuses) + + # The thread is broken, assume it's a reply to the root status + starting_depth = 0 + + # ... unless we can find its ancestor in one of the already-processed threads + @descendant_threads.reverse_each do |descendant_thread| + statuses = descendant_thread[:statuses] + + index = statuses.find_index do |thread_status| + thread_status.id == descendant.in_reply_to_id + end + + if index.present? + starting_depth = descendant_thread[:starting_depth] + index + 1 + break + end + end + + statuses = [descendant] + end + end + + @descendant_threads << create_descendant_thread(starting_depth, statuses) + end + + @max_descendant_thread_id = @descendant_threads.pop[:statuses].first.id if descendants.size >= DESCENDANTS_LIMIT + end +end diff --git a/app/controllers/custom_css_controller.rb b/app/controllers/custom_css_controller.rb index 6e80feaf83..7f4dcfcfe6 100644 --- a/app/controllers/custom_css_controller.rb +++ b/app/controllers/custom_css_controller.rb @@ -6,6 +6,7 @@ class CustomCssController < ApplicationController before_action :set_cache_headers def show + expires 3.minutes, public: true render plain: Setting.custom_css || '', content_type: 'text/css' end end diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb index 3feb081325..fe4c19cada 100644 --- a/app/controllers/emojis_controller.rb +++ b/app/controllers/emojis_controller.rb @@ -7,9 +7,8 @@ class EmojisController < ApplicationController def show respond_to do |format| format.json do - render_cached_json(['activitypub', 'emoji', @emoji], content_type: 'application/activity+json') do - ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter) - end + expires_in 3.minutes, public: true + render json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter end end end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 415abe10c1..8baa64490f 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -8,7 +8,7 @@ class FollowerAccountsController < ApplicationController def index respond_to do |format| format.html do - mark_cacheable! unless user_signed_in? + expires_in 0, public: true unless user_signed_in? next if @account.user_hides_network? diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 9487256641..4d1ea4594e 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -8,7 +8,7 @@ class FollowingAccountsController < ApplicationController def index respond_to do |format| format.html do - mark_cacheable! unless user_signed_in? + expires_in 0, public: true unless user_signed_in? next if @account.user_hides_network? diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 85622a7b59..d1c5251347 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -21,7 +21,7 @@ class HomeController < ApplicationController when 'statuses' status = Status.find_by(id: matches[2]) - if status && (status.public_visibility? || status.unlisted_visibility?) + if status&.distributable? redirect_to(ActivityPub::TagManager.instance.url_for(status)) return end diff --git a/app/controllers/intents_controller.rb b/app/controllers/intents_controller.rb index 9f41cf48ac..ca89fc7fe6 100644 --- a/app/controllers/intents_controller.rb +++ b/app/controllers/intents_controller.rb @@ -2,6 +2,7 @@ class IntentsController < ApplicationController before_action :check_uri + rescue_from Addressable::URI::InvalidURIError, with: :handle_invalid_uri def show diff --git a/app/controllers/manifests_controller.rb b/app/controllers/manifests_controller.rb index 332d845d82..1e5db43939 100644 --- a/app/controllers/manifests_controller.rb +++ b/app/controllers/manifests_controller.rb @@ -4,6 +4,7 @@ class ManifestsController < ApplicationController skip_before_action :store_current_location def show + expires_in 3.minutes, public: true render json: InstancePresenter.new, serializer: ManifestSerializer end end diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index d44b52d262..b3b7519a1c 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -31,7 +31,6 @@ class MediaController < ApplicationController def verify_permitted_status! authorize @media_attachment.status, :show? rescue Mastodon::NotPermittedError - # Reraise in order to get a 404 instead of a 403 error code raise ActiveRecord::RecordNotFound end diff --git a/app/controllers/public_timelines_controller.rb b/app/controllers/public_timelines_controller.rb index 53d4472d88..23506b990c 100644 --- a/app/controllers/public_timelines_controller.rb +++ b/app/controllers/public_timelines_controller.rb @@ -8,20 +8,16 @@ class PublicTimelinesController < ApplicationController before_action :set_instance_presenter def show - respond_to do |format| - format.html do - @initial_state_json = ActiveModelSerializers::SerializableResource.new( - InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token), - serializer: InitialStateSerializer - ).to_json - end - end + @initial_state_json = ActiveModelSerializers::SerializableResource.new( + InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token), + serializer: InitialStateSerializer + ).to_json end private def check_enabled - raise ActiveRecord::RecordNotFound unless Setting.timeline_preview + not_found unless Setting.timeline_preview end def set_body_classes diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb index 8ba331cd16..0fb71d3352 100644 --- a/app/controllers/remote_follow_controller.rb +++ b/app/controllers/remote_follow_controller.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true class RemoteFollowController < ApplicationController + include AccountOwnedConcern + layout 'modal' - before_action :set_account - before_action :gone, if: :suspended_account? before_action :set_body_classes def new @@ -32,14 +32,6 @@ class RemoteFollowController < ApplicationController { acct: session[:remote_follow] } end - def set_account - @account = Account.find_local!(params[:account_username]) - end - - def suspended_account? - @account.suspended? - end - def set_body_classes @body_classes = 'modal-layout' @hide_header = true diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 776099ca87..13ce5c691a 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -1,24 +1,21 @@ # frozen_string_literal: true class StatusesController < ApplicationController + include StatusControllerConcern include SignatureAuthentication include Authorization - - ANCESTORS_LIMIT = 40 - DESCENDANTS_LIMIT = 60 - DESCENDANTS_DEPTH_LIMIT = 20 + include AccountOwnedConcern layout 'public' - before_action :set_account before_action :set_status before_action :set_instance_presenter before_action :set_link_headers - before_action :check_account_suspension before_action :redirect_to_original, only: [:show] before_action :set_referrer_policy_header, only: [:show] before_action :set_cache_headers - before_action :set_replies, only: [:replies] + before_action :set_body_classes + before_action :set_autoplay, only: :embed content_security_policy only: :embed do |p| p.frame_ancestors(false) @@ -28,25 +25,20 @@ class StatusesController < ApplicationController respond_to do |format| format.html do expires_in 10.seconds, public: true if current_account.nil? - - @body_classes = 'with-modals' - set_ancestors set_descendants end format.json do - render_cached_json(['activitypub', 'note', @status], content_type: 'application/activity+json', public: @status.distributable?) do - ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter) - end + expires_in 3.minutes, public: @status.distributable? + render json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter end end end def activity - render_cached_json(['activitypub', 'activity', @status], content_type: 'application/activity+json', public: @status.distributable?) do - ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter) - end + expires_in 3.minutes, public: @status.distributable? + render json: @status, content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter end def embed @@ -54,120 +46,14 @@ class StatusesController < ApplicationController expires_in 180, public: true response.headers['X-Frame-Options'] = 'ALLOWALL' - @autoplay = ActiveModel::Type::Boolean.new.cast(params[:autoplay]) render layout: 'embedded' end - def replies - render json: replies_collection_presenter, - serializer: ActivityPub::CollectionSerializer, - adapter: ActivityPub::Adapter, - content_type: 'application/activity+json', - skip_activities: true - end - private - def replies_collection_presenter - page = ActivityPub::CollectionPresenter.new( - id: replies_account_status_url(@account, @status, page_params), - type: :unordered, - part_of: replies_account_status_url(@account, @status), - next: next_page, - items: @replies.map { |status| status.local ? status : status.id } - ) - if page_requested? - page - else - ActivityPub::CollectionPresenter.new( - id: replies_account_status_url(@account, @status), - type: :unordered, - first: page - ) - end - end - - def create_descendant_thread(starting_depth, statuses) - depth = starting_depth + statuses.size - - if depth < DESCENDANTS_DEPTH_LIMIT - { - statuses: statuses, - starting_depth: starting_depth, - } - else - next_status = statuses.pop - - { - statuses: statuses, - starting_depth: starting_depth, - next_status: next_status, - } - end - end - - def set_account - @account = Account.find_local!(params[:account_username]) - end - - def set_ancestors - @ancestors = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : [] - @next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift - end - - def set_descendants - @max_descendant_thread_id = params[:max_descendant_thread_id]&.to_i - @since_descendant_thread_id = params[:since_descendant_thread_id]&.to_i - - descendants = cache_collection( - @status.descendants( - DESCENDANTS_LIMIT, - current_account, - @max_descendant_thread_id, - @since_descendant_thread_id, - DESCENDANTS_DEPTH_LIMIT - ), - Status - ) - - @descendant_threads = [] - - if descendants.present? - statuses = [descendants.first] - starting_depth = 0 - - descendants.drop(1).each_with_index do |descendant, index| - if descendants[index].id == descendant.in_reply_to_id - statuses << descendant - else - @descendant_threads << create_descendant_thread(starting_depth, statuses) - - # The thread is broken, assume it's a reply to the root status - starting_depth = 0 - - # ... unless we can find its ancestor in one of the already-processed threads - @descendant_threads.reverse_each do |descendant_thread| - statuses = descendant_thread[:statuses] - - index = statuses.find_index do |thread_status| - thread_status.id == descendant.in_reply_to_id - end - - if index.present? - starting_depth = descendant_thread[:starting_depth] + index + 1 - break - end - end - - statuses = [descendant] - end - end - - @descendant_threads << create_descendant_thread(starting_depth, statuses) - end - - @max_descendant_thread_id = @descendant_threads.pop[:statuses].first.id if descendants.size >= DESCENDANTS_LIMIT + def set_body_classes + @body_classes = 'with-modals' end def set_link_headers @@ -185,39 +71,15 @@ class StatusesController < ApplicationController @instance_presenter = InstancePresenter.new end - def check_account_suspension - gone if @account.suspended? - end - def redirect_to_original redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog? end def set_referrer_policy_header - return if @status.public_visibility? || @status.unlisted_visibility? - response.headers['Referrer-Policy'] = 'origin' + response.headers['Referrer-Policy'] = 'origin' unless @status.distributable? end - def page_requested? - params[:page] == 'true' - end - - def set_replies - @replies = page_params[:other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses - @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted]) - @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id]) - end - - def next_page - last_reply = @replies.last - return if last_reply.nil? - same_account = last_reply.account_id == @account.id - return unless same_account || @replies.size == DESCENDANTS_LIMIT - same_account = false unless @replies.size == DESCENDANTS_LIMIT - replies_account_status_url(@account, @status, page: true, min_id: last_reply.id, other_accounts: !same_account) - end - - def page_params - { page: true, other_accounts: params[:other_accounts], min_id: params[:min_id] }.compact + def set_autoplay + @autoplay = truthy_param?(:autoplay) end end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 66b1849011..2ecce0ca22 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -5,14 +5,15 @@ class TagsController < ApplicationController layout 'public' + before_action :set_tag before_action :set_body_classes before_action :set_instance_presenter def show - @tag = Tag.find_normalized!(params[:id]) - respond_to do |format| format.html do + expires_in 0, public: true + @initial_state_json = ActiveModelSerializers::SerializableResource.new( InitialStatePresenter.new(settings: {}, token: current_session&.token), serializer: InitialStateSerializer @@ -20,6 +21,8 @@ class TagsController < ApplicationController end format.rss do + expires_in 0, public: true + @statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none)).limit(PAGE_SIZE) @statuses = cache_collection(@statuses, Status) @@ -27,19 +30,22 @@ class TagsController < ApplicationController end format.json do + expires_in 3.minutes, public: true + @statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id]) @statuses = cache_collection(@statuses, Status) - render json: collection_presenter, - serializer: ActivityPub::CollectionSerializer, - adapter: ActivityPub::Adapter, - content_type: 'application/activity+json' + render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end end end private + def set_tag + @tag = Tag.find_normalized!(params[:id]) + end + def set_body_classes @body_classes = 'with-modals' end diff --git a/app/controllers/well_known/host_meta_controller.rb b/app/controllers/well_known/host_meta_controller.rb index 5fb70288a2..2e9298c4ae 100644 --- a/app/controllers/well_known/host_meta_controller.rb +++ b/app/controllers/well_known/host_meta_controller.rb @@ -13,7 +13,7 @@ module WellKnown format.xml { render content_type: 'application/xrd+xml' } end - expires_in(3.days, public: true) + expires_in 3.days, public: true end end end diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb index 28654b61d8..53f7f1e278 100644 --- a/app/controllers/well_known/webfinger_controller.rb +++ b/app/controllers/well_known/webfinger_controller.rb @@ -19,7 +19,7 @@ module WellKnown end end - expires_in(3.days, public: true) + expires_in 3.days, public: true rescue ActiveRecord::RecordNotFound head 404 end @@ -27,12 +27,9 @@ module WellKnown private def username_from_resource - resource_user = resource_param - + resource_user = resource_param username, domain = resource_user.split('@') - if Rails.configuration.x.alternate_domains.include?(domain) - resource_user = "#{username}@#{Rails.configuration.x.local_domain}" - end + resource_user = "#{username}@#{Rails.configuration.x.local_domain}" if Rails.configuration.x.alternate_domains.include?(domain) WebfingerResource.new(resource_user).username end diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb index 1aa6ee9ec2..34c646668b 100644 --- a/app/lib/activitypub/activity/announce.rb +++ b/app/lib/activitypub/activity/announce.rb @@ -40,7 +40,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity end def announceable?(status) - status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility? + status.account_id == @account.id || status.distributable? end def related_to_local_activity? diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 00f0dd42d7..5849c20d77 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -42,7 +42,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity resolve_thread(@status) fetch_replies(@status) distribute(@status) - forward_for_reply if @status.public_visibility? || @status.unlisted_visibility? + forward_for_reply if @status.distributable? end def find_existing_status diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index 0eb14b89ce..1f2b40c150 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -31,7 +31,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity return if @status.nil? - if @status.public_visibility? || @status.unlisted_visibility? + if @status.distributable? forward_for_reply forward_for_reblogs end diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 595291342a..4d452f290b 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -51,7 +51,7 @@ class ActivityPub::TagManager def replies_uri_for(target, page_params = nil) raise ArgumentError, 'target must be a local activity' unless %i(note comment activity).include?(target.object_type) && target.local? - replies_account_status_url(target.account, target, page_params) + account_status_replies_url(target.account, target, page_params) end # Primary audience of a status diff --git a/app/models/status.rb b/app/models/status.rb index 906756e850..6f1e35e4aa 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -193,7 +193,7 @@ class Status < ApplicationRecord end def hidden? - private_visibility? || direct_visibility? || limited_visibility? + !distributable? end def distributable? @@ -446,7 +446,8 @@ class Status < ApplicationRecord end def update_statistics - return unless public_visibility? || unlisted_visibility? + return unless distributable? + ActivityTracker.increment('activity:statuses:local') end @@ -455,7 +456,7 @@ class Status < ApplicationRecord account&.increment_count!(:statuses_count) reblog&.increment_count!(:reblogs_count) if reblog? - thread&.increment_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?) + thread&.increment_count!(:replies_count) if in_reply_to_id.present? && distributable? end def decrement_counter_caches @@ -463,7 +464,7 @@ class Status < ApplicationRecord account&.decrement_count!(:statuses_count) reblog&.decrement_count!(:reblogs_count) if reblog? - thread&.decrement_count!(:replies_count) if in_reply_to_id.present? && (public_visibility? || unlisted_visibility?) + thread&.decrement_count!(:replies_count) if in_reply_to_id.present? && distributable? end def unlink_from_conversations diff --git a/app/serializers/activitypub/activity_serializer.rb b/app/serializers/activitypub/activity_serializer.rb index c06d5c87ca..fdedbc9d1f 100644 --- a/app/serializers/activitypub/activity_serializer.rb +++ b/app/serializers/activitypub/activity_serializer.rb @@ -1,9 +1,12 @@ # frozen_string_literal: true class ActivityPub::ActivitySerializer < ActivityPub::Serializer + cache key: 'activity', expires_in: 3.minutes + attributes :id, :type, :actor, :published, :to, :cc has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, if: :serialize_object? + attribute :proper_uri, key: :object, unless: :serialize_object? attribute :atom_uri, if: :announce? diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 0644219fb6..ab7be27f6e 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -3,6 +3,8 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer include RoutingHelper + cache key: 'actor', expires_in: 3.minutes + context :security context_extensions :manually_approves_followers, :featured, :also_known_as, diff --git a/app/serializers/activitypub/collection_serializer.rb b/app/serializers/activitypub/collection_serializer.rb index da1ba735fc..9dd8134d3e 100644 --- a/app/serializers/activitypub/collection_serializer.rb +++ b/app/serializers/activitypub/collection_serializer.rb @@ -7,6 +7,8 @@ class ActivityPub::CollectionSerializer < ActivityPub::Serializer super end + cache key: 'collection', expires_in: 3.minutes + attribute :id, if: -> { object.id.present? } attribute :type attribute :total_items, if: -> { object.size.present? } diff --git a/app/serializers/activitypub/emoji_serializer.rb b/app/serializers/activitypub/emoji_serializer.rb index 4dc38f3ea6..08df25d7d4 100644 --- a/app/serializers/activitypub/emoji_serializer.rb +++ b/app/serializers/activitypub/emoji_serializer.rb @@ -3,6 +3,8 @@ class ActivityPub::EmojiSerializer < ActivityPub::Serializer include RoutingHelper + cache key: 'emoji', expires_in: 3.minutes + context_extensions :emoji attributes :id, :type, :name, :updated diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 67f596e78a..87acc54292 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ActivityPub::NoteSerializer < ActivityPub::Serializer + cache key: 'note', expires_in: 3.minutes + context_extensions :atom_uri, :conversation, :sensitive, :hashtag, :emoji, :focal_point, :blurhash diff --git a/app/services/process_hashtags_service.rb b/app/services/process_hashtags_service.rb index d5ec076a8b..b6974e598b 100644 --- a/app/services/process_hashtags_service.rb +++ b/app/services/process_hashtags_service.rb @@ -14,7 +14,7 @@ class ProcessHashtagsService < BaseService TrendingTags.record_use!(tag, status.account, status.created_at) if status.public_visibility? end - return unless status.public_visibility? || status.unlisted_visibility? + return unless status.distributable? status.account.featured_tags.where(tag_id: records.map(&:id)).each do |featured_tag| featured_tag.increment(status.created_at) diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml index 11220dfcb1..38fde1be85 100644 --- a/app/views/statuses/_simple_status.html.haml +++ b/app/views/statuses/_simple_status.html.haml @@ -50,9 +50,9 @@ = fa_icon 'reply-all fw' .status__action-bar__counter__label= obscured_counter status.replies_count = link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do - - if status.public_visibility? || status.unlisted_visibility? + - if status.distributable? = fa_icon 'retweet fw' - - elsif status.private_visibility? + - elsif status.private_visibility? || status.limited_visibility? = fa_icon 'lock fw' - else = fa_icon 'envelope fw' diff --git a/config/routes.rb b/config/routes.rb index 69b495a96d..115e7bb447 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -52,8 +52,9 @@ Rails.application.routes.draw do member do get :activity get :embed - get :replies end + + resources :replies, only: [:index], module: :activitypub end resources :followers, only: [:index], controller: :follower_accounts diff --git a/spec/controllers/concerns/account_controller_concern_spec.rb b/spec/controllers/concerns/account_controller_concern_spec.rb index ea2b4a2a1d..7ea214a7d3 100644 --- a/spec/controllers/concerns/account_controller_concern_spec.rb +++ b/spec/controllers/concerns/account_controller_concern_spec.rb @@ -41,7 +41,7 @@ describe ApplicationController, type: :controller do it 'sets link headers' do account = Fabricate(:account, username: 'username', user: Fabricate(:user)) get 'success', params: { account_username: 'username' } - expect(response.headers['Link'].to_s).to eq '; rel="lrdd"; type="application/xrd+xml", ; rel="alternate"; type="application/atom+xml", ; rel="alternate"; type="application/activity+json"' + expect(response.headers['Link'].to_s).to eq '; rel="lrdd"; type="application/jrd+json", ; rel="alternate"; type="application/activity+json"' end it 'returns http success' do diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb index 95e5c363c9..6905dae10e 100644 --- a/spec/controllers/statuses_controller_spec.rb +++ b/spec/controllers/statuses_controller_spec.rb @@ -92,7 +92,7 @@ describe StatusesController do end it 'assigns @max_descendant_thread_id for the last thread if it is hitting the status limit' do - stub_const 'StatusesController::DESCENDANTS_LIMIT', 1 + stub_const 'StatusControllerConcern::DESCENDANTS_LIMIT', 1 status = Fabricate(:status) child = Fabricate(:status, in_reply_to_id: status.id) @@ -103,7 +103,7 @@ describe StatusesController do end it 'assigns @descendant_threads for threads with :next_status key if they are hitting the depth limit' do - stub_const 'StatusesController::DESCENDANTS_DEPTH_LIMIT', 2 + stub_const 'StatusControllerConcern::DESCENDANTS_DEPTH_LIMIT', 2 status = Fabricate(:status) child0 = Fabricate(:status, in_reply_to_id: status.id) child1 = Fabricate(:status, in_reply_to_id: child0.id) diff --git a/spec/requests/link_headers_spec.rb b/spec/requests/link_headers_spec.rb index 3dc408d927..712ee262b8 100644 --- a/spec/requests/link_headers_spec.rb +++ b/spec/requests/link_headers_spec.rb @@ -11,16 +11,16 @@ describe 'Link headers' do end it 'contains webfinger url in link header' do - link_header = link_header_with_type('application/xrd+xml') + link_header = link_header_with_type('application/jrd+json') expect(link_header.href).to match 'http://www.example.com/.well-known/webfinger?resource=acct%3Atest%40cb6e6126.ngrok.io' expect(link_header.attr_pairs.first).to eq %w(rel lrdd) end - it 'contains atom url in link header' do - link_header = link_header_with_type('application/atom+xml') + it 'contains activitypub url in link header' do + link_header = link_header_with_type('application/activity+json') - expect(link_header.href).to eq 'http://www.example.com/users/test.atom' + expect(link_header.href).to eq 'https://cb6e6126.ngrok.io/users/test' expect(link_header.attr_pairs.first).to eq %w(rel alternate) end From ef1524639776fe7d7be2d5c414fc98dd2410a5f4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 8 Jul 2019 12:04:06 +0200 Subject: [PATCH 17/71] Remove unused remote unfollow controller (#11250) --- .../remote_unfollows_controller.rb | 39 ------------------- app/views/remote_unfollows/_card.html.haml | 13 ------- .../_post_follow_actions.html.haml | 4 -- app/views/remote_unfollows/error.html.haml | 3 -- app/views/remote_unfollows/success.html.haml | 10 ----- config/locales/en.yml | 4 -- config/routes.rb | 2 - .../remote_unfollows_controller_spec.rb | 38 ------------------ 8 files changed, 113 deletions(-) delete mode 100644 app/controllers/remote_unfollows_controller.rb delete mode 100644 app/views/remote_unfollows/_card.html.haml delete mode 100644 app/views/remote_unfollows/_post_follow_actions.html.haml delete mode 100644 app/views/remote_unfollows/error.html.haml delete mode 100644 app/views/remote_unfollows/success.html.haml delete mode 100644 spec/controllers/remote_unfollows_controller_spec.rb diff --git a/app/controllers/remote_unfollows_controller.rb b/app/controllers/remote_unfollows_controller.rb deleted file mode 100644 index af5943363a..0000000000 --- a/app/controllers/remote_unfollows_controller.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -class RemoteUnfollowsController < ApplicationController - layout 'modal' - - before_action :authenticate_user! - before_action :set_body_classes - - def create - @account = unfollow_attempt.try(:target_account) - - if @account.nil? - render :error - else - render :success - end - rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError - render :error - end - - private - - def unfollow_attempt - username, domain = acct_without_prefix.split('@') - UnfollowService.new.call(current_account, Account.find_remote!(username, domain)) - end - - def acct_without_prefix - acct_params.gsub(/\Aacct:/, '') - end - - def acct_params - params.fetch(:acct, '') - end - - def set_body_classes - @body_classes = 'modal-layout' - end -end diff --git a/app/views/remote_unfollows/_card.html.haml b/app/views/remote_unfollows/_card.html.haml deleted file mode 100644 index 80ad3bae2d..0000000000 --- a/app/views/remote_unfollows/_card.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -.account-card - .detailed-status__display-name - %div - = image_tag account.avatar.url(:original), alt: '', width: 48, height: 48, class: 'avatar' - - %span.display-name - - account_url = local_assigns[:admin] ? admin_account_path(account.id) : ActivityPub::TagManager.instance.url_for(account) - = link_to account_url, class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do - %strong.emojify= display_name(account, custom_emojify: true) - %span @#{account.acct} - - - if account.note? - .account__header__content.emojify= Formatter.instance.simplified_format(account) diff --git a/app/views/remote_unfollows/_post_follow_actions.html.haml b/app/views/remote_unfollows/_post_follow_actions.html.haml deleted file mode 100644 index 328f7c833b..0000000000 --- a/app/views/remote_unfollows/_post_follow_actions.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -.post-follow-actions - %div= link_to t('authorize_follow.post_follow.web'), web_url("accounts/#{@account.id}"), class: 'button button--block' - %div= link_to t('authorize_follow.post_follow.return'), ActivityPub::TagManager.instance.url_for(@account), class: 'button button--block' - %div= t('authorize_follow.post_follow.close') diff --git a/app/views/remote_unfollows/error.html.haml b/app/views/remote_unfollows/error.html.haml deleted file mode 100644 index cb63f02be3..0000000000 --- a/app/views/remote_unfollows/error.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -.form-container - .flash-message#error_explanation - = t('remote_unfollow.error') diff --git a/app/views/remote_unfollows/success.html.haml b/app/views/remote_unfollows/success.html.haml deleted file mode 100644 index b007eedc7e..0000000000 --- a/app/views/remote_unfollows/success.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -- content_for :page_title do - = t('remote_unfollow.title', acct: @account.acct) - -.form-container - .follow-prompt - %h2= t('remote_unfollow.unfollowed') - - = render 'application/card', account: @account - - = render 'post_follow_actions' diff --git a/config/locales/en.yml b/config/locales/en.yml index 611f36fdd1..00b7d1dbea 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -810,10 +810,6 @@ en: reply: proceed: Proceed to reply prompt: 'You want to reply to this toot:' - remote_unfollow: - error: Error - title: Title - unfollowed: Unfollowed scheduled_statuses: over_daily_limit: You have exceeded the limit of %{limit} scheduled toots for that day over_total_limit: You have exceeded the limit of %{limit} scheduled toots diff --git a/config/routes.rb b/config/routes.rb index 115e7bb447..95f8a39add 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -141,8 +141,6 @@ Rails.application.routes.draw do get '/public', to: 'public_timelines#show', as: :public_timeline get '/media_proxy/:id/(*any)', to: 'media_proxy#show', as: :media_proxy - # Remote follow - resource :remote_unfollow, only: [:create] resource :authorize_interaction, only: [:show, :create] resource :share, only: [:show, :create] diff --git a/spec/controllers/remote_unfollows_controller_spec.rb b/spec/controllers/remote_unfollows_controller_spec.rb deleted file mode 100644 index a1a55ede0b..0000000000 --- a/spec/controllers/remote_unfollows_controller_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe RemoteUnfollowsController do - render_views - - describe '#create' do - subject { post :create, params: { acct: acct } } - - let(:current_user) { Fabricate(:user, account: current_account) } - let(:current_account) { Fabricate(:account) } - let(:remote_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox')).account } - before do - sign_in current_user - current_account.follow!(remote_account) - stub_request(:post, 'http://example.com/inbox') { { status: 200 } } - end - - context 'when successfully unfollow remote account' do - let(:acct) { "acct:#{remote_account.username}@#{remote_account.domain}" } - - it do - is_expected.to render_template :success - expect(current_account.following?(remote_account)).to be false - end - end - - context 'when fails to unfollow remote account' do - let(:acct) { "acct:#{remote_account.username + '_test'}@#{remote_account.domain}" } - - it do - is_expected.to render_template :error - expect(current_account.following?(remote_account)).to be true - end - end - end -end From 1e7187f2a8e0b9ffe4e7d6b06e9f70674c50471e Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 8 Jul 2019 18:17:22 +0200 Subject: [PATCH 18/71] Fix Status.remote scope matching *all* statuses (#11265) --- app/models/status.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/status.rb b/app/models/status.rb index 6f1e35e4aa..23682c84b0 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -75,7 +75,7 @@ class Status < ApplicationRecord default_scope { recent } scope :recent, -> { reorder(id: :desc) } - scope :remote, -> { where(local: false).or(where.not(uri: nil)) } + scope :remote, -> { where(local: false).where.not(uri: nil) } scope :local, -> { where(local: true).or(where(uri: nil)) } scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') } From 4e921832272425352d28cad550bfc4dffd6d0e78 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 9 Jul 2019 03:27:35 +0200 Subject: [PATCH 19/71] Refactor domain block checks (#11268) --- .../concerns/signature_verification.rb | 4 + app/helpers/domain_control_helper.rb | 17 +++ app/lib/tag_manager.rb | 3 + .../fetch_featured_collection_service.rb | 3 +- .../fetch_remote_account_service.rb | 14 ++- .../activitypub/fetch_remote_poll_service.rb | 2 + .../activitypub/process_account_service.rb | 5 +- .../activitypub/process_collection_service.rb | 4 +- .../activitypub/process_poll_service.rb | 1 + app/services/resolve_account_service.rb | 101 +++++++++++------- spec/services/resolve_account_service_spec.rb | 5 + 11 files changed, 108 insertions(+), 51 deletions(-) create mode 100644 app/helpers/domain_control_helper.rb diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 90a57197c0..0ccdf5ec98 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -5,6 +5,8 @@ module SignatureVerification extend ActiveSupport::Concern + include DomainControlHelper + def signed_request? request.headers['Signature'].present? end @@ -126,6 +128,8 @@ module SignatureVerification if key_id.start_with?('acct:') stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) } elsif !ActivityPub::TagManager.instance.local_uri?(key_id) + return if domain_not_allowed?(key_id) + account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account) account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) } account diff --git a/app/helpers/domain_control_helper.rb b/app/helpers/domain_control_helper.rb new file mode 100644 index 0000000000..efd328f817 --- /dev/null +++ b/app/helpers/domain_control_helper.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module DomainControlHelper + def domain_not_allowed?(uri_or_domain) + return if uri_or_domain.blank? + + domain = begin + if uri_or_domain.include?('://') + Addressable::URI.parse(uri_or_domain).domain + else + uri_or_domain + end + end + + DomainBlock.blocked?(domain) + end +end diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb index daf4f556b3..c88cf49947 100644 --- a/app/lib/tag_manager.rb +++ b/app/lib/tag_manager.rb @@ -24,13 +24,16 @@ class TagManager def same_acct?(canonical, needle) return true if canonical.casecmp(needle).zero? + username, domain = needle.split('@') + local_domain?(domain) && canonical.casecmp(username).zero? end def local_url?(url) uri = Addressable::URI.parse(url).normalize domain = uri.host + (uri.port ? ":#{uri.port}" : '') + TagManager.instance.web_domain?(domain) end end diff --git a/app/services/activitypub/fetch_featured_collection_service.rb b/app/services/activitypub/fetch_featured_collection_service.rb index 6a137b520b..2c27704661 100644 --- a/app/services/activitypub/fetch_featured_collection_service.rb +++ b/app/services/activitypub/fetch_featured_collection_service.rb @@ -4,13 +4,12 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService include JsonLdHelper def call(account) - return if account.featured_collection_url.blank? + return if account.featured_collection_url.blank? || account.suspended? || account.local? @account = account @json = fetch_resource(@account.featured_collection_url, true) return unless supported_context? - return if @account.suspended? || @account.local? case @json['type'] when 'Collection', 'CollectionPage' diff --git a/app/services/activitypub/fetch_remote_account_service.rb b/app/services/activitypub/fetch_remote_account_service.rb index 3c20449412..d65c8f9511 100644 --- a/app/services/activitypub/fetch_remote_account_service.rb +++ b/app/services/activitypub/fetch_remote_account_service.rb @@ -2,18 +2,22 @@ class ActivityPub::FetchRemoteAccountService < BaseService include JsonLdHelper + include DomainControlHelper SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze # Does a WebFinger roundtrip on each call, unless `only_key` is true def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false) + return if domain_not_allowed?(uri) return ActivityPub::TagManager.instance.uri_to_resource(uri, Account) if ActivityPub::TagManager.instance.local_uri?(uri) - @json = if prefetched_body.nil? - fetch_resource(uri, id) - else - body_to_json(prefetched_body, compare_id: id ? uri : nil) - end + @json = begin + if prefetched_body.nil? + fetch_resource(uri, id) + else + body_to_json(prefetched_body, compare_id: id ? uri : nil) + end + end return if !supported_context? || !expected_type? || (break_on_redirect && @json['movedTo'].present?) diff --git a/app/services/activitypub/fetch_remote_poll_service.rb b/app/services/activitypub/fetch_remote_poll_service.rb index 854a32d050..1c79ecf116 100644 --- a/app/services/activitypub/fetch_remote_poll_service.rb +++ b/app/services/activitypub/fetch_remote_poll_service.rb @@ -5,7 +5,9 @@ class ActivityPub::FetchRemotePollService < BaseService def call(poll, on_behalf_of = nil) json = fetch_resource(poll.status.uri, true, on_behalf_of) + return unless supported_context?(json) + ActivityPub::ProcessPollService.new.call(poll, json) end end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 3857e7c16d..603e27ed97 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -2,11 +2,12 @@ class ActivityPub::ProcessAccountService < BaseService include JsonLdHelper + include DomainControlHelper # Should be called with confirmed valid JSON # and WebFinger-resolved username and domain def call(username, domain, json, options = {}) - return if json['inbox'].blank? || unsupported_uri_scheme?(json['id']) + return if json['inbox'].blank? || unsupported_uri_scheme?(json['id']) || domain_not_allowed?(domain) @options = options @json = json @@ -15,8 +16,6 @@ class ActivityPub::ProcessAccountService < BaseService @domain = domain @collections = {} - return if auto_suspend? - RedisLock.acquire(lock_options) do |lock| if lock.acquired? @account = Account.find_remote(@username, @domain) diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb index 881df478bf..a2a2e70715 100644 --- a/app/services/activitypub/process_collection_service.rb +++ b/app/services/activitypub/process_collection_service.rb @@ -8,9 +8,7 @@ class ActivityPub::ProcessCollectionService < BaseService @json = Oj.load(body, mode: :strict) @options = options - return unless supported_context? - return if different_actor? && verify_account!.nil? - return if @account.suspended? || @account.local? + return if !supported_context? || (different_actor? && verify_account!.nil?) || @account.suspended? || @account.local? case @json['type'] when 'Collection', 'CollectionPage' diff --git a/app/services/activitypub/process_poll_service.rb b/app/services/activitypub/process_poll_service.rb index 61357abd3c..2fbce65b9c 100644 --- a/app/services/activitypub/process_poll_service.rb +++ b/app/services/activitypub/process_poll_service.rb @@ -5,6 +5,7 @@ class ActivityPub::ProcessPollService < BaseService def call(poll, json) @json = json + return unless expected_type? previous_expires_at = poll.expires_at diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb index 0ea31a0d85..41a2eb158e 100644 --- a/app/services/resolve_account_service.rb +++ b/app/services/resolve_account_service.rb @@ -1,75 +1,108 @@ # frozen_string_literal: true -require_relative '../models/account' - class ResolveAccountService < BaseService include JsonLdHelper + include DomainControlHelper - # Find or create a local account for a remote user. - # When creating, look up the user's webfinger and fetch all - # important information from their feed - # @param [String, Account] uri User URI in the form of username@domain + class WebfingerRedirectError < StandardError; end + + # Find or create an account record for a remote user. When creating, + # look up the user's webfinger and fetch ActivityPub data + # @param [String, Account] uri URI in the username@domain format or account record # @param [Hash] options + # @option options [Boolean] :redirected Do not follow further Webfinger redirects + # @option options [Boolean] :skip_webfinger Do not attempt to refresh account data # @return [Account] def call(uri, options = {}) + return if uri.blank? + + process_options!(uri, options) + + # First of all we want to check if we've got the account + # record with the URI already, and if so, we can exit early + + return if domain_not_allowed?(@domain) + + @account ||= Account.find_remote(@username, @domain) + + return @account if @account&.local? || !webfinger_update_due? + + # At this point we are in need of a Webfinger query, which may + # yield us a different username/domain through a redirect + + process_webfinger! + + # Because the username/domain pair may be different than what + # we already checked, we need to check if we've already got + # the record with that URI, again + + return if domain_not_allowed?(@domain) + + @account ||= Account.find_remote(@username, @domain) + + return @account if @account&.local? || !webfinger_update_due? + + # Now it is certain, it is definitely a remote account, and it + # either needs to be created, or updated from fresh data + + process_account! + rescue Goldfinger::Error, WebfingerRedirectError, Oj::ParseError => e + Rails.logger.debug "Webfinger query for #{@uri} failed: #{e}" + nil + end + + private + + def process_options!(uri, options) @options = options if uri.is_a?(Account) @account = uri @username = @account.username @domain = @account.domain - uri = "#{@username}@#{@domain}" - - return @account if @account.local? || !webfinger_update_due? + @uri = [@username, @domain].compact.join('@') else + @uri = uri @username, @domain = uri.split('@') - - return Account.find_local(@username) if TagManager.instance.local_domain?(@domain) - - @account = Account.find_remote(@username, @domain) - - return @account unless webfinger_update_due? end - Rails.logger.debug "Looking up webfinger for #{uri}" - - @webfinger = Goldfinger.finger("acct:#{uri}") + @domain = nil if TagManager.instance.local_domain?(@domain) + end + def process_webfinger! + @webfinger = Goldfinger.finger("acct:#{@uri}") confirmed_username, confirmed_domain = @webfinger.subject.gsub(/\Aacct:/, '').split('@') if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero? @username = confirmed_username @domain = confirmed_domain - elsif options[:redirected].nil? - return call("#{confirmed_username}@#{confirmed_domain}", options.merge(redirected: true)) + elsif @options[:redirected].nil? + @account = ResolveAccountService.new.call("#{confirmed_username}@#{confirmed_domain}", @options.merge(redirected: true)) else - Rails.logger.debug 'Requested and returned acct URIs do not match' - return + raise WebfingerRedirectError, "The URI #{uri} tries to hijack #{@username}@#{@domain}" end - return Account.find_local(@username) if TagManager.instance.local_domain?(@domain) + @domain = nil if TagManager.instance.local_domain?(@domain) + end + + def process_account! return unless activitypub_ready? RedisLock.acquire(lock_options) do |lock| if lock.acquired? @account = Account.find_remote(@username, @domain) - next unless @account.nil? || @account.activitypub? + next if (@account.present? && !@account.activitypub?) || actor_json.nil? - handle_activitypub + @account = ActivityPub::ProcessAccountService.new.call(@username, @domain, actor_json) else raise Mastodon::RaceConditionError end end @account - rescue Goldfinger::Error => e - Rails.logger.debug "Webfinger query for #{uri} unsuccessful: #{e}" - nil end - private - def webfinger_update_due? @account.nil? || ((!@options[:skip_webfinger] || @account.ostatus?) && @account.possibly_stale?) end @@ -78,14 +111,6 @@ class ResolveAccountService < BaseService !@webfinger.link('self').nil? && ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self').type) end - def handle_activitypub - return if actor_json.nil? - - @account = ActivityPub::ProcessAccountService.new.call(@username, @domain, actor_json) - rescue Oj::ParseError - nil - end - def actor_url @actor_url ||= @webfinger.link('self').href end diff --git a/spec/services/resolve_account_service_spec.rb b/spec/services/resolve_account_service_spec.rb index 7a64f41618..cea942e39c 100644 --- a/spec/services/resolve_account_service_spec.rb +++ b/spec/services/resolve_account_service_spec.rb @@ -53,6 +53,11 @@ RSpec.describe ResolveAccountService, type: :service do fail_occurred = false return_values = Concurrent::Array.new + # Preload classes that throw circular dependency errors in threads + Account + TagManager + DomainBlock + threads = Array.new(5) do Thread.new do true while wait_for_start From 6172263a63de2e4b2a2aeb9cb30b387622fdd3d6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 10 Jul 2019 00:43:30 +0200 Subject: [PATCH 20/71] Fix activity being rendered within activity due to caching (#11271) Fix #11270 --- app/serializers/activitypub/activity_serializer.rb | 2 -- app/serializers/activitypub/actor_serializer.rb | 2 -- app/serializers/activitypub/collection_serializer.rb | 2 -- app/serializers/activitypub/emoji_serializer.rb | 2 -- app/serializers/activitypub/note_serializer.rb | 2 -- 5 files changed, 10 deletions(-) diff --git a/app/serializers/activitypub/activity_serializer.rb b/app/serializers/activitypub/activity_serializer.rb index fdedbc9d1f..d0edad7868 100644 --- a/app/serializers/activitypub/activity_serializer.rb +++ b/app/serializers/activitypub/activity_serializer.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class ActivityPub::ActivitySerializer < ActivityPub::Serializer - cache key: 'activity', expires_in: 3.minutes - attributes :id, :type, :actor, :published, :to, :cc has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, if: :serialize_object? diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index ab7be27f6e..0644219fb6 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -3,8 +3,6 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer include RoutingHelper - cache key: 'actor', expires_in: 3.minutes - context :security context_extensions :manually_approves_followers, :featured, :also_known_as, diff --git a/app/serializers/activitypub/collection_serializer.rb b/app/serializers/activitypub/collection_serializer.rb index 9dd8134d3e..da1ba735fc 100644 --- a/app/serializers/activitypub/collection_serializer.rb +++ b/app/serializers/activitypub/collection_serializer.rb @@ -7,8 +7,6 @@ class ActivityPub::CollectionSerializer < ActivityPub::Serializer super end - cache key: 'collection', expires_in: 3.minutes - attribute :id, if: -> { object.id.present? } attribute :type attribute :total_items, if: -> { object.size.present? } diff --git a/app/serializers/activitypub/emoji_serializer.rb b/app/serializers/activitypub/emoji_serializer.rb index 08df25d7d4..4dc38f3ea6 100644 --- a/app/serializers/activitypub/emoji_serializer.rb +++ b/app/serializers/activitypub/emoji_serializer.rb @@ -3,8 +3,6 @@ class ActivityPub::EmojiSerializer < ActivityPub::Serializer include RoutingHelper - cache key: 'emoji', expires_in: 3.minutes - context_extensions :emoji attributes :id, :type, :name, :updated diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 87acc54292..67f596e78a 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class ActivityPub::NoteSerializer < ActivityPub::Serializer - cache key: 'note', expires_in: 3.minutes - context_extensions :atom_uri, :conversation, :sensitive, :hashtag, :emoji, :focal_point, :blurhash From 27ad4c1501eb391b56e89bdab52624b953fde786 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 10 Jul 2019 17:09:10 +0200 Subject: [PATCH 21/71] Fix old migration script depending on the StreamEntry model (#11278) --- db/migrate/20180528141303_fix_accounts_unique_index.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/db/migrate/20180528141303_fix_accounts_unique_index.rb b/db/migrate/20180528141303_fix_accounts_unique_index.rb index bd4e158b7e..bbbf28d817 100644 --- a/db/migrate/20180528141303_fix_accounts_unique_index.rb +++ b/db/migrate/20180528141303_fix_accounts_unique_index.rb @@ -12,6 +12,11 @@ class FixAccountsUniqueIndex < ActiveRecord::Migration[5.2] end end + class StreamEntry < ApplicationRecord + # Dummy class, to make migration possible across version changes + belongs_to :account, inverse_of: :stream_entries + end + disable_ddl_transaction! def up From 85eb418e1f9ae40ce5fbdc837a354444eec94655 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 10 Jul 2019 17:10:12 +0200 Subject: [PATCH 22/71] Fix handling of webfinger redirects in ResolveAccountService (#11279) --- app/services/resolve_account_service.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb index 41a2eb158e..7864c4bcdb 100644 --- a/app/services/resolve_account_service.rb +++ b/app/services/resolve_account_service.rb @@ -30,7 +30,7 @@ class ResolveAccountService < BaseService # At this point we are in need of a Webfinger query, which may # yield us a different username/domain through a redirect - process_webfinger! + process_webfinger!(@uri) # Because the username/domain pair may be different than what # we already checked, we need to check if we've already got @@ -69,15 +69,16 @@ class ResolveAccountService < BaseService @domain = nil if TagManager.instance.local_domain?(@domain) end - def process_webfinger! + def process_webfinger!(uri, redirected = false) @webfinger = Goldfinger.finger("acct:#{@uri}") confirmed_username, confirmed_domain = @webfinger.subject.gsub(/\Aacct:/, '').split('@') if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero? @username = confirmed_username @domain = confirmed_domain - elsif @options[:redirected].nil? - @account = ResolveAccountService.new.call("#{confirmed_username}@#{confirmed_domain}", @options.merge(redirected: true)) + @uri = uri + elsif !redirected + return process_webfinger!("#{confirmed_username}@#{confirmed_domain}", true) else raise WebfingerRedirectError, "The URI #{uri} tries to hijack #{@username}@#{@domain}" end From d04c584159fdad36d7713718c4ba0b3b42cd27a9 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 10 Jul 2019 17:10:43 +0200 Subject: [PATCH 23/71] Drop magic-public-key from webfinger replies as it's only used for OStatus (#11280) --- app/serializers/webfinger_serializer.rb | 1 - app/views/well_known/webfinger/show.xml.ruby | 5 ----- 2 files changed, 6 deletions(-) diff --git a/app/serializers/webfinger_serializer.rb b/app/serializers/webfinger_serializer.rb index 4220f697e6..f4af215510 100644 --- a/app/serializers/webfinger_serializer.rb +++ b/app/serializers/webfinger_serializer.rb @@ -18,7 +18,6 @@ class WebfingerSerializer < ActiveModel::Serializer { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) }, { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(object, format: 'atom') }, { rel: 'self', type: 'application/activity+json', href: account_url(object) }, - { rel: 'magic-public-key', href: "data:application/magic-public-key,#{object.magic_key}" }, { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" }, ] end diff --git a/app/views/well_known/webfinger/show.xml.ruby b/app/views/well_known/webfinger/show.xml.ruby index c82cdb7b3d..ae80df9d2f 100644 --- a/app/views/well_known/webfinger/show.xml.ruby +++ b/app/views/well_known/webfinger/show.xml.ruby @@ -25,11 +25,6 @@ doc << Ox::Element.new('XRD').tap do |xrd| link['href'] = account_url(@account) end - xrd << Ox::Element.new('Link').tap do |link| - link['rel'] = 'magic-public-key' - link['href'] = "data:application/magic-public-key,#{@account.magic_key}" - end - xrd << Ox::Element.new('Link').tap do |link| link['rel'] = 'http://ostatus.org/schema/1.0/subscribe' link['template'] = "#{authorize_interaction_url}?acct={uri}" From 5d3feed191bcbe2769512119752b426108152fe9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 10 Jul 2019 18:59:28 +0200 Subject: [PATCH 24/71] Refactor fetching of remote resources (#11251) --- app/helpers/jsonld_helper.rb | 47 +++++----- app/lib/request.rb | 2 +- .../fetch_remote_status_service.rb | 20 ++-- app/services/fetch_atom_service.rb | 93 ------------------- app/services/fetch_link_card_service.rb | 2 +- app/services/fetch_remote_account_service.rb | 2 +- app/services/fetch_remote_status_service.rb | 2 +- app/services/fetch_resource_service.rb | 68 ++++++++++++++ app/services/resolve_url_service.rb | 47 ++++------ app/workers/activitypub/delivery_worker.rb | 16 +--- .../fetch_remote_account_service_spec.rb | 1 + ...spec.rb => fetch_resource_service_spec.rb} | 14 +-- spec/services/resolve_url_service_spec.rb | 44 +-------- 13 files changed, 142 insertions(+), 216 deletions(-) delete mode 100644 app/services/fetch_atom_service.rb create mode 100644 app/services/fetch_resource_service.rb rename spec/services/{fetch_atom_service_spec.rb => fetch_resource_service_spec.rb} (84%) diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index 5b40112755..34a657e069 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -16,13 +16,15 @@ module JsonLdHelper # The url attribute can be a string, an array of strings, or an array of objects. # The objects could include a mimeType. Not-included mimeType means it's text/html. def url_to_href(value, preferred_type = nil) - single_value = if value.is_a?(Array) && !value.first.is_a?(String) - value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) } - elsif value.is_a?(Array) - value.first - else - value - end + single_value = begin + if value.is_a?(Array) && !value.first.is_a?(String) + value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) } + elsif value.is_a?(Array) + value.first + else + value + end + end if single_value.nil? || single_value.is_a?(String) single_value @@ -64,7 +66,9 @@ module JsonLdHelper def fetch_resource(uri, id, on_behalf_of = nil) unless id json = fetch_resource_without_id_validation(uri, on_behalf_of) + return unless json + uri = json['id'] end @@ -74,24 +78,26 @@ module JsonLdHelper def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false) build_request(uri, on_behalf_of).perform do |response| - unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error - raise Mastodon::UnexpectedResponseError, response - end + raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error + return body_to_json(response.body_with_limit) if response.code == 200 end + # If request failed, retry without doing it on behalf of a user return if on_behalf_of.nil? + build_request(uri).perform do |response| - unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error - raise Mastodon::UnexpectedResponseError, response - end + raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error + response.code == 200 ? body_to_json(response.body_with_limit) : nil end end def body_to_json(body, compare_id: nil) json = body.is_a?(String) ? Oj.load(body, mode: :strict) : body + return if compare_id.present? && json['id'] != compare_id + json rescue Oj::ParseError nil @@ -105,35 +111,34 @@ module JsonLdHelper end end - private - def response_successful?(response) (200...300).cover?(response.code) end def response_error_unsalvageable?(response) - (400...500).cover?(response.code) && response.code != 429 + response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code)) end def build_request(uri, on_behalf_of = nil) - request = Request.new(:get, uri) - request.on_behalf_of(on_behalf_of) if on_behalf_of - request.add_headers('Accept' => 'application/activity+json, application/ld+json') - request + Request.new(:get, uri).tap do |request| + request.on_behalf_of(on_behalf_of) if on_behalf_of + request.add_headers('Accept' => 'application/activity+json, application/ld+json') + end end def load_jsonld_context(url, _options = {}, &_block) json = Rails.cache.fetch("jsonld:context:#{url}", expires_in: 30.days, raw: true) do request = Request.new(:get, url) request.add_headers('Accept' => 'application/ld+json') - request.perform do |res| raise JSON::LD::JsonLdError::LoadingDocumentFailed unless res.code == 200 && res.mime_type == 'application/ld+json' + res.body_with_limit end end doc = JSON::LD::API::RemoteDocument.new(url, json) + block_given? ? yield(doc) : doc end end diff --git a/app/lib/request.rb b/app/lib/request.rb index 322457ad7e..1fd3f5190c 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -41,7 +41,7 @@ class Request end def on_behalf_of(account, key_id_format = :acct, sign_with: nil) - raise ArgumentError unless account.local? + raise ArgumentError, 'account must be local' unless account&.local? @account = account @keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @account.keypair diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index 469821032a..cf4f62899f 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -5,18 +5,18 @@ class ActivityPub::FetchRemoteStatusService < BaseService # Should be called when uri has already been checked for locality def call(uri, id: true, prefetched_body: nil, on_behalf_of: nil) - @json = if prefetched_body.nil? - fetch_resource(uri, id, on_behalf_of) - else - body_to_json(prefetched_body, compare_id: id ? uri : nil) - end + @json = begin + if prefetched_body.nil? + fetch_resource(uri, id, on_behalf_of) + else + body_to_json(prefetched_body, compare_id: id ? uri : nil) + end + end - return unless supported_context? && expected_type? - - return if actor_id.nil? || !trustworthy_attribution?(@json['id'], actor_id) + return if !(supported_context? && expected_type?) || actor_id.nil? || !trustworthy_attribution?(@json['id'], actor_id) actor = ActivityPub::TagManager.instance.uri_to_resource(actor_id, Account) - actor = ActivityPub::FetchRemoteAccountService.new.call(actor_id, id: true) if actor.nil? || needs_update(actor) + actor = ActivityPub::FetchRemoteAccountService.new.call(actor_id, id: true) if actor.nil? || needs_update?(actor) return if actor.nil? || actor.suspended? @@ -46,7 +46,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService equals_or_includes_any?(@json['type'], ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES) end - def needs_update(actor) + def needs_update?(actor) actor.possibly_stale? end end diff --git a/app/services/fetch_atom_service.rb b/app/services/fetch_atom_service.rb deleted file mode 100644 index d6508a9888..0000000000 --- a/app/services/fetch_atom_service.rb +++ /dev/null @@ -1,93 +0,0 @@ -# frozen_string_literal: true - -class FetchAtomService < BaseService - include JsonLdHelper - - def call(url) - return if url.blank? - - result = process(url) - - # retry without ActivityPub - result ||= process(url) if @unsupported_activity - - result - rescue OpenSSL::SSL::SSLError => e - Rails.logger.debug "SSL error: #{e}" - nil - rescue HTTP::ConnectionError => e - Rails.logger.debug "HTTP ConnectionError: #{e}" - nil - end - - private - - def process(url, terminal = false) - @url = url - perform_request { |response| process_response(response, terminal) } - end - - def perform_request(&block) - accept = 'text/html' - accept = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams", application/atom+xml, ' + accept unless @unsupported_activity - - Request.new(:get, @url).add_headers('Accept' => accept).perform(&block) - end - - def process_response(response, terminal = false) - return nil if response.code != 200 - - if response.mime_type == 'application/atom+xml' - [@url, { prefetched_body: response.body_with_limit }, :ostatus] - elsif ['application/activity+json', 'application/ld+json'].include?(response.mime_type) - body = response.body_with_limit - json = body_to_json(body) - if supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) && json['inbox'].present? - [json['id'], { prefetched_body: body, id: true }, :activitypub] - elsif supported_context?(json) && expected_type?(json) - [json['id'], { prefetched_body: body, id: true }, :activitypub] - else - @unsupported_activity = true - nil - end - elsif !terminal - link_header = response['Link'] && parse_link_header(response) - - if link_header&.find_link(%w(rel alternate)) - process_link_headers(link_header) - elsif response.mime_type == 'text/html' - process_html(response) - end - end - end - - def expected_type?(json) - equals_or_includes_any?(json['type'], ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES) - end - - def process_html(response) - page = Nokogiri::HTML(response.body_with_limit) - - json_link = page.xpath('//link[@rel="alternate"]').find { |link| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link['type']) } - atom_link = page.xpath('//link[@rel="alternate"]').find { |link| link['type'] == 'application/atom+xml' } - - result ||= process(json_link['href'], terminal: true) unless json_link.nil? || @unsupported_activity - result ||= process(atom_link['href'], terminal: true) unless atom_link.nil? - - result - end - - def process_link_headers(link_header) - json_link = link_header.find_link(%w(rel alternate), %w(type application/activity+json)) || link_header.find_link(%w(rel alternate), ['type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']) - atom_link = link_header.find_link(%w(rel alternate), %w(type application/atom+xml)) - - result ||= process(json_link.href, terminal: true) unless json_link.nil? || @unsupported_activity - result ||= process(atom_link.href, terminal: true) unless atom_link.nil? - - result - end - - def parse_link_header(response) - LinkHeader.parse(response['Link'].is_a?(Array) ? response['Link'].first : response['Link']) - end -end diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 75fbd0e8c6..4e75c370fa 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -29,7 +29,7 @@ class FetchLinkCardService < BaseService end attach_card if @card&.persisted? - rescue HTTP::Error, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e + rescue HTTP::Error, OpenSSL::SSL::SSLError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e Rails.logger.debug "Error fetching link #{@url}: #{e}" nil end diff --git a/app/services/fetch_remote_account_service.rb b/app/services/fetch_remote_account_service.rb index a7f95603d9..3cd06e30f6 100644 --- a/app/services/fetch_remote_account_service.rb +++ b/app/services/fetch_remote_account_service.rb @@ -3,7 +3,7 @@ class FetchRemoteAccountService < BaseService def call(url, prefetched_body = nil, protocol = :ostatus) if prefetched_body.nil? - resource_url, resource_options, protocol = FetchAtomService.new.call(url) + resource_url, resource_options, protocol = FetchResourceService.new.call(url) else resource_url = url resource_options = { prefetched_body: prefetched_body } diff --git a/app/services/fetch_remote_status_service.rb b/app/services/fetch_remote_status_service.rb index aac39dfd53..208dc7809c 100644 --- a/app/services/fetch_remote_status_service.rb +++ b/app/services/fetch_remote_status_service.rb @@ -3,7 +3,7 @@ class FetchRemoteStatusService < BaseService def call(url, prefetched_body = nil, protocol = :ostatus) if prefetched_body.nil? - resource_url, resource_options, protocol = FetchAtomService.new.call(url) + resource_url, resource_options, protocol = FetchResourceService.new.call(url) else resource_url = url resource_options = { prefetched_body: prefetched_body } diff --git a/app/services/fetch_resource_service.rb b/app/services/fetch_resource_service.rb new file mode 100644 index 0000000000..c0473f3ad2 --- /dev/null +++ b/app/services/fetch_resource_service.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +class FetchResourceService < BaseService + include JsonLdHelper + + ACCEPT_HEADER = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams", text/html' + + def call(url) + return if url.blank? + + process(url) + rescue HTTP::Error, OpenSSL::SSL::SSLError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e + Rails.logger.debug "Error fetching resource #{@url}: #{e}" + nil + end + + private + + def process(url, terminal = false) + @url = url + + perform_request { |response| process_response(response, terminal) } + end + + def perform_request(&block) + Request.new(:get, @url).add_headers('Accept' => ACCEPT_HEADER).perform(&block) + end + + def process_response(response, terminal = false) + return nil if response.code != 200 + + if ['application/activity+json', 'application/ld+json'].include?(response.mime_type) + body = response.body_with_limit + json = body_to_json(body) + + [json['id'], { prefetched_body: body, id: true }, :activitypub] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) || expected_type?(json)) + elsif !terminal + link_header = response['Link'] && parse_link_header(response) + + if link_header&.find_link(%w(rel alternate)) + process_link_headers(link_header) + elsif response.mime_type == 'text/html' + process_html(response) + end + end + end + + def expected_type?(json) + equals_or_includes_any?(json['type'], ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES) + end + + def process_html(response) + page = Nokogiri::HTML(response.body_with_limit) + json_link = page.xpath('//link[@rel="alternate"]').find { |link| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link['type']) } + + process(json_link['href'], terminal: true) unless json_link.nil? + end + + def process_link_headers(link_header) + json_link = link_header.find_link(%w(rel alternate), %w(type application/activity+json)) || link_header.find_link(%w(rel alternate), ['type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']) + + process(json_link.href, terminal: true) unless json_link.nil? + end + + def parse_link_header(response) + LinkHeader.parse(response['Link'].is_a?(Array) ? response['Link'].first : response['Link']) + end +end diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb index f941b489a5..80381c16b6 100644 --- a/app/services/resolve_url_service.rb +++ b/app/services/resolve_url_service.rb @@ -4,64 +4,49 @@ class ResolveURLService < BaseService include JsonLdHelper include Authorization - attr_reader :url - def call(url, on_behalf_of: nil) - @url = url + @url = url @on_behalf_of = on_behalf_of - return process_local_url if local_url? - - process_url unless fetched_atom_feed.nil? + if local_url? + process_local_url + elsif !fetched_resource.nil? + process_url + end end private def process_url if equals_or_includes_any?(type, ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) - FetchRemoteAccountService.new.call(atom_url, body, protocol) + FetchRemoteAccountService.new.call(resource_url, body, protocol) elsif equals_or_includes_any?(type, ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES) - FetchRemoteStatusService.new.call(atom_url, body, protocol) + FetchRemoteStatusService.new.call(resource_url, body, protocol) end end - def fetched_atom_feed - @_fetched_atom_feed ||= FetchAtomService.new.call(url) + def fetched_resource + @fetched_resource ||= FetchResourceService.new.call(@url) end - def atom_url - fetched_atom_feed.first + def resource_url + fetched_resource.first end def body - fetched_atom_feed.second[:prefetched_body] + fetched_resource.second[:prefetched_body] end def protocol - fetched_atom_feed.third + fetched_resource.third end def type return json_data['type'] if protocol == :activitypub - - case xml_root - when 'feed' - 'Person' - when 'entry' - 'Note' - end end def json_data - @_json_data ||= body_to_json(body) - end - - def xml_root - xml_data.root.name - end - - def xml_data - @_xml_data ||= Nokogiri::XML(body, nil, 'utf-8') + @json_data ||= body_to_json(body) end def local_url? @@ -83,10 +68,10 @@ class ResolveURLService < BaseService def check_local_status(status) return if status.nil? + authorize_with @on_behalf_of, status, :show? status rescue Mastodon::NotPermittedError - # Do not disclose the existence of status the user is not authorized to see nil end end diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb index 8b52b8e490..5457d9d4b0 100644 --- a/app/workers/activitypub/delivery_worker.rb +++ b/app/workers/activitypub/delivery_worker.rb @@ -2,6 +2,7 @@ class ActivityPub::DeliveryWorker include Sidekiq::Worker + include JsonLdHelper STOPLIGHT_FAILURE_THRESHOLD = 10 STOPLIGHT_COOLDOWN = 60 @@ -32,9 +33,10 @@ class ActivityPub::DeliveryWorker private def build_request(http_client) - request = Request.new(:post, @inbox_url, body: @json, http_client: http_client) - request.on_behalf_of(@source_account, :uri, sign_with: @options[:sign_with]) - request.add_headers(HEADERS) + Request.new(:post, @inbox_url, body: @json, http_client: http_client).tap do |request| + request.on_behalf_of(@source_account, :uri, sign_with: @options[:sign_with]) + request.add_headers(HEADERS) + end end def perform_request @@ -53,14 +55,6 @@ class ActivityPub::DeliveryWorker .run end - def response_successful?(response) - (200...300).cover?(response.code) - end - - def response_error_unsalvageable?(response) - response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code)) - end - def failure_tracker @failure_tracker ||= DeliveryFailureTracker.new(@inbox_url) end diff --git a/spec/services/fetch_remote_account_service_spec.rb b/spec/services/fetch_remote_account_service_spec.rb index 37e9910d4b..ee7325be28 100644 --- a/spec/services/fetch_remote_account_service_spec.rb +++ b/spec/services/fetch_remote_account_service_spec.rb @@ -4,6 +4,7 @@ RSpec.describe FetchRemoteAccountService, type: :service do let(:url) { 'https://example.com/alice' } let(:prefetched_body) { nil } let(:protocol) { :ostatus } + subject { FetchRemoteAccountService.new.call(url, prefetched_body, protocol) } let(:actor) do diff --git a/spec/services/fetch_atom_service_spec.rb b/spec/services/fetch_resource_service_spec.rb similarity index 84% rename from spec/services/fetch_atom_service_spec.rb rename to spec/services/fetch_resource_service_spec.rb index 495540004e..17c192c446 100644 --- a/spec/services/fetch_atom_service_spec.rb +++ b/spec/services/fetch_resource_service_spec.rb @@ -1,9 +1,11 @@ require 'rails_helper' -RSpec.describe FetchAtomService, type: :service do +RSpec.describe FetchResourceService, type: :service do + let!(:representative) { Fabricate(:account) } + describe '#call' do let(:url) { 'http://example.com' } - subject { FetchAtomService.new.call(url) } + subject { described_class.new.call(url) } context 'url is blank' do let(:url) { '' } @@ -23,8 +25,7 @@ RSpec.describe FetchAtomService, type: :service do allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(OpenSSL::SSL::SSLError) end - it 'output log and return nil' do - expect_any_instance_of(ActiveSupport::Logger).to receive(:debug).with('SSL error: OpenSSL::SSL::SSLError') + it 'return nil' do is_expected.to be_nil end end @@ -34,8 +35,7 @@ RSpec.describe FetchAtomService, type: :service do allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(HTTP::ConnectionError) end - it 'output log and return nil' do - expect_any_instance_of(ActiveSupport::Logger).to receive(:debug).with('HTTP ConnectionError: HTTP::ConnectionError') + it 'return nil' do is_expected.to be_nil end end @@ -57,7 +57,7 @@ RSpec.describe FetchAtomService, type: :service do context 'content type is application/atom+xml' do let(:content_type) { 'application/atom+xml' } - it { is_expected.to eq [url, { :prefetched_body => "" }, :ostatus] } + it { is_expected.to eq nil } end context 'content_type is activity+json' do diff --git a/spec/services/resolve_url_service_spec.rb b/spec/services/resolve_url_service_spec.rb index 7bb5d19402..aa42046371 100644 --- a/spec/services/resolve_url_service_spec.rb +++ b/spec/services/resolve_url_service_spec.rb @@ -6,48 +6,14 @@ describe ResolveURLService, type: :service do subject { described_class.new } describe '#call' do - it 'returns nil when there is no atom url' do - url = 'http://example.com/missing-atom' + it 'returns nil when there is no resource url' do + url = 'http://example.com/missing-resource' service = double - allow(FetchAtomService).to receive(:new).and_return service + + allow(FetchResourceService).to receive(:new).and_return service allow(service).to receive(:call).with(url).and_return(nil) - result = subject.call(url) - expect(result).to be_nil - end - - it 'fetches remote accounts for feed types' do - url = 'http://example.com/atom-feed' - service = double - allow(FetchAtomService).to receive(:new).and_return service - feed_url = 'http://feed-url' - feed_content = 'contents' - allow(service).to receive(:call).with(url).and_return([feed_url, { prefetched_body: feed_content }]) - - account_service = double - allow(FetchRemoteAccountService).to receive(:new).and_return(account_service) - allow(account_service).to receive(:call) - - _result = subject.call(url) - - expect(account_service).to have_received(:call).with(feed_url, feed_content, nil) - end - - it 'fetches remote statuses for entry types' do - url = 'http://example.com/atom-entry' - service = double - allow(FetchAtomService).to receive(:new).and_return service - feed_url = 'http://feed-url' - feed_content = 'contents' - allow(service).to receive(:call).with(url).and_return([feed_url, { prefetched_body: feed_content }]) - - account_service = double - allow(FetchRemoteStatusService).to receive(:new).and_return(account_service) - allow(account_service).to receive(:call) - - _result = subject.call(url) - - expect(account_service).to have_received(:call).with(feed_url, feed_content, nil) + expect(subject.call(url)).to be_nil end end end From a6dc6a242fdabef2d0fdd9eb7b72ce11cbc22e3e Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 11 Jul 2019 06:33:07 +0200 Subject: [PATCH 25/71] [Security] Bump lodash from 4.17.11 to 4.17.13 (#11287) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.13. **This update includes security fixes.** - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.13) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 7a27de5f2a..9710be7d00 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "intl-relativeformat": "^6.4.2", "is-nan": "^1.2.1", "js-yaml": "^3.13.1", - "lodash": "^4.7.11", + "lodash": "^4.17.13", "mark-loader": "^0.1.6", "marky": "^1.2.1", "mini-css-extract-plugin": "^0.7.0", diff --git a/yarn.lock b/yarn.lock index 97096a30f3..f0cedbba72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6268,10 +6268,10 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.0.0, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.7.11, lodash@~4.17.10: - version "4.17.11" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" - integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== +lodash@^4.0.0, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.5, lodash@^4.3.0, lodash@~4.17.10: + version "4.17.13" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.13.tgz#0bdc3a6adc873d2f4e0c4bac285df91b64fc7b93" + integrity sha512-vm3/XWXfWtRua0FkUyEHBZy8kCPjErNBT9fJx8Zvs+U6zjqPbTUOpkaoum3O5uiA8sm+yNMHXfYkTUHFoMxFNA== loglevel@^1.6.3: version "1.6.3" From 4e8dcc5dbbf625b7268ed10d36122de985da6bdc Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 11 Jul 2019 14:49:55 +0200 Subject: [PATCH 26/71] Add HTTP signatures to all outgoing ActivityPub GET requests (#11284) --- app/helpers/jsonld_helper.rb | 13 +--- app/lib/request.rb | 4 +- app/services/fetch_resource_service.rb | 2 +- .../concerns/signature_verification_spec.rb | 2 +- .../fetch_remote_account_service_spec.rb | 1 + spec/services/fetch_resource_service_spec.rb | 65 +++++++++++-------- 6 files changed, 45 insertions(+), 42 deletions(-) diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index 34a657e069..83a5b2462e 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -77,19 +77,12 @@ module JsonLdHelper end def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false) + on_behalf_of ||= Account.representative + build_request(uri, on_behalf_of).perform do |response| raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error - return body_to_json(response.body_with_limit) if response.code == 200 - end - - # If request failed, retry without doing it on behalf of a user - return if on_behalf_of.nil? - - build_request(uri).perform do |response| - raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error - - response.code == 200 ? body_to_json(response.body_with_limit) : nil + body_to_json(response.body_with_limit) if response.code == 200 end end diff --git a/app/lib/request.rb b/app/lib/request.rb index 1fd3f5190c..9d874fe2ca 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -40,8 +40,8 @@ class Request set_digest! if options.key?(:body) end - def on_behalf_of(account, key_id_format = :acct, sign_with: nil) - raise ArgumentError, 'account must be local' unless account&.local? + def on_behalf_of(account, key_id_format = :uri, sign_with: nil) + raise ArgumentError, 'account must not be nil' if account.nil? @account = account @keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @account.keypair diff --git a/app/services/fetch_resource_service.rb b/app/services/fetch_resource_service.rb index c0473f3ad2..3676d899d2 100644 --- a/app/services/fetch_resource_service.rb +++ b/app/services/fetch_resource_service.rb @@ -23,7 +23,7 @@ class FetchResourceService < BaseService end def perform_request(&block) - Request.new(:get, @url).add_headers('Accept' => ACCEPT_HEADER).perform(&block) + Request.new(:get, @url).add_headers('Accept' => ACCEPT_HEADER).on_behalf_of(Account.representative).perform(&block) end def process_response(response, terminal = false) diff --git a/spec/controllers/concerns/signature_verification_spec.rb b/spec/controllers/concerns/signature_verification_spec.rb index 7206900971..1fa19f54d7 100644 --- a/spec/controllers/concerns/signature_verification_spec.rb +++ b/spec/controllers/concerns/signature_verification_spec.rb @@ -38,7 +38,7 @@ describe ApplicationController, type: :controller do end context 'with signature header' do - let!(:author) { Fabricate(:account) } + let!(:author) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/actor') } context 'without body' do before do diff --git a/spec/services/fetch_remote_account_service_spec.rb b/spec/services/fetch_remote_account_service_spec.rb index ee7325be28..b374458610 100644 --- a/spec/services/fetch_remote_account_service_spec.rb +++ b/spec/services/fetch_remote_account_service_spec.rb @@ -4,6 +4,7 @@ RSpec.describe FetchRemoteAccountService, type: :service do let(:url) { 'https://example.com/alice' } let(:prefetched_body) { nil } let(:protocol) { :ostatus } + let!(:representative) { Fabricate(:account) } subject { FetchRemoteAccountService.new.call(url, prefetched_body, protocol) } diff --git a/spec/services/fetch_resource_service_spec.rb b/spec/services/fetch_resource_service_spec.rb index 17c192c446..98630966b5 100644 --- a/spec/services/fetch_resource_service_spec.rb +++ b/spec/services/fetch_resource_service_spec.rb @@ -5,69 +5,78 @@ RSpec.describe FetchResourceService, type: :service do describe '#call' do let(:url) { 'http://example.com' } + subject { described_class.new.call(url) } - context 'url is blank' do + context 'with blank url' do let(:url) { '' } it { is_expected.to be_nil } end - context 'request failed' do + context 'when request fails' do before do - WebMock.stub_request(:get, url).to_return(status: 500, body: '', headers: {}) + stub_request(:get, url).to_return(status: 500, body: '', headers: {}) end it { is_expected.to be_nil } end - context 'raise OpenSSL::SSL::SSLError' do + context 'when OpenSSL::SSL::SSLError is raised' do before do - allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(OpenSSL::SSL::SSLError) + allow(Request).to receive_message_chain(:new, :add_headers, :on_behalf_of, :perform).and_raise(OpenSSL::SSL::SSLError) end - it 'return nil' do - is_expected.to be_nil - end + it { is_expected.to be_nil } end - context 'raise HTTP::ConnectionError' do + context 'when HTTP::ConnectionError is raised' do before do - allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(HTTP::ConnectionError) + allow(Request).to receive_message_chain(:new, :add_headers, :on_behalf_of, :perform).and_raise(HTTP::ConnectionError) end - it 'return nil' do - is_expected.to be_nil - end + it { is_expected.to be_nil } end - context 'response success' do + context 'when request succeeds' do let(:body) { '' } - let(:headers) { { 'Content-Type' => content_type } } - let(:json) { - { id: 1, + + let(:content_type) { 'application/json' } + + let(:headers) do + { 'Content-Type' => content_type } + end + + let(:json) do + { + id: 1, '@context': ActivityPub::TagManager::CONTEXT, type: 'Note', }.to_json - } - - before do - WebMock.stub_request(:get, url).to_return(status: 200, body: body, headers: headers) end - context 'content type is application/atom+xml' do + before do + stub_request(:get, url).to_return(status: 200, body: body, headers: headers) + end + + it 'signs request' do + subject + expect(a_request(:get, url).with(headers: { 'Signature' => /keyId="#{Regexp.escape(ActivityPub::TagManager.instance.uri_for(representative) + '#main-key')}"/ })).to have_been_made + end + + context 'when content type is application/atom+xml' do let(:content_type) { 'application/atom+xml' } it { is_expected.to eq nil } end - context 'content_type is activity+json' do + context 'when content type is activity+json' do let(:content_type) { 'application/activity+json; charset=utf-8' } let(:body) { json } it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] } end - context 'content_type is ld+json with profile' do + context 'when content type is ld+json with profile' do let(:content_type) { 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' } let(:body) { json } @@ -75,17 +84,17 @@ RSpec.describe FetchResourceService, type: :service do end before do - WebMock.stub_request(:get, url).to_return(status: 200, body: body, headers: headers) - WebMock.stub_request(:get, 'http://example.com/foo').to_return(status: 200, body: json, headers: { 'Content-Type' => 'application/activity+json' }) + stub_request(:get, url).to_return(status: 200, body: body, headers: headers) + stub_request(:get, 'http://example.com/foo').to_return(status: 200, body: json, headers: { 'Content-Type' => 'application/activity+json' }) end - context 'has link header' do + context 'when link header is present' do let(:headers) { { 'Link' => '; rel="alternate"; type="application/activity+json"', } } it { is_expected.to eq [1, { prefetched_body: json, id: true }, :activitypub] } end - context 'content type is text/html' do + context 'when content type is text/html' do let(:content_type) { 'text/html' } let(:body) { '' } From 4e1260feaa09bfa7305887e34cb129b37bee6c52 Mon Sep 17 00:00:00 2001 From: ThibG Date: Thu, 11 Jul 2019 14:50:27 +0200 Subject: [PATCH 27/71] Fix BlockService trying to reject incorrect follow request (#11288) Fixes #11148 --- app/services/block_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/block_service.rb b/app/services/block_service.rb index da06361c20..266a0f4b9d 100644 --- a/app/services/block_service.rb +++ b/app/services/block_service.rb @@ -8,7 +8,7 @@ class BlockService < BaseService UnfollowService.new.call(account, target_account) if account.following?(target_account) UnfollowService.new.call(target_account, account) if target_account.following?(account) - RejectFollowService.new.call(account, target_account) if target_account.requested?(account) + RejectFollowService.new.call(target_account, account) if target_account.requested?(account) block = account.block!(target_account) From 5bf67ca91350e40e6f329271d3ca2bdcba87ab64 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 11 Jul 2019 20:11:09 +0200 Subject: [PATCH 28/71] Add ActivityPub secure mode (#11269) * Add HTTP signature requirement for served ActivityPub resources * Change `SECURE_MODE` to `AUTHORIZED_FETCH` * Add 'Signature' to 'Vary' header and improve code style * Improve code style by adding `public_fetch_mode?` method --- app/controllers/accounts_controller.rb | 13 +++++++-- .../activitypub/collections_controller.rb | 3 ++- .../activitypub/inboxes_controller.rb | 27 +++++++++++-------- .../activitypub/outboxes_controller.rb | 4 +-- .../activitypub/replies_controller.rb | 2 ++ app/controllers/application_controller.rb | 10 ++++++- .../concerns/account_controller_concern.rb | 2 +- .../concerns/signature_verification.rb | 19 ++++++++++--- .../follower_accounts_controller.rb | 12 ++++++--- .../following_accounts_controller.rb | 12 ++++++--- app/controllers/statuses_controller.rb | 9 ++++--- app/controllers/tags_controller.rb | 5 +++- app/lib/activitypub/adapter.rb | 1 + .../activitypub/inboxes_controller_spec.rb | 4 +-- 14 files changed, 89 insertions(+), 34 deletions(-) diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 3184a73cb1..fc913c2ecd 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -4,6 +4,7 @@ class AccountsController < ApplicationController PAGE_SIZE = 20 include AccountControllerConcern + include SignatureAuthentication before_action :set_cache_headers before_action :set_body_classes @@ -39,8 +40,8 @@ class AccountsController < ApplicationController end format.json do - expires_in 3.minutes, public: true - render json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter + expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?) + render json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to end end end @@ -132,4 +133,12 @@ class AccountsController < ApplicationController filtered_statuses.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]).to_a end end + + def restrict_fields_to + if signed_request_account.present? || public_fetch_mode? + # Return all fields + else + %i(id type preferred_username inbox public_key endpoints) + end + end end diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index dd2f111b03..035467f417 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -4,12 +4,13 @@ class ActivityPub::CollectionsController < Api::BaseController include SignatureVerification include AccountOwnedConcern + before_action :require_signature!, if: :authorized_fetch_mode? before_action :set_size before_action :set_statuses before_action :set_cache_headers def show - expires_in 3.minutes, public: true + expires_in 3.minutes, public: public_fetch_mode? render json: collection_presenter, content_type: 'application/activity+json', serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, skip_activities: true end diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index 9be0676e14..7cfd9a25e1 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -5,23 +5,24 @@ class ActivityPub::InboxesController < Api::BaseController include JsonLdHelper include AccountOwnedConcern + before_action :skip_unknown_actor_delete + before_action :require_signature! + def create - if unknown_deleted_account? - head 202 - elsif signed_request_account - upgrade_account - process_payload - head 202 - else - render plain: signature_verification_failure_reason, status: 401 - end + upgrade_account + process_payload + head 202 end private + def skip_unknown_actor_delete + head 202 if unknown_deleted_account? + end + def unknown_deleted_account? json = Oj.load(body, mode: :strict) - json['type'] == 'Delete' && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists? + json.is_a?(Hash) && json['type'] == 'Delete' && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists? rescue Oj::ParseError false end @@ -32,8 +33,12 @@ class ActivityPub::InboxesController < Api::BaseController def body return @body if defined?(@body) - @body = request.body.read.force_encoding('UTF-8') + + @body = request.body.read + @body.force_encoding('UTF-8') if @body.present? + request.body.rewind if request.body.respond_to?(:rewind) + @body end diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 4c0b769f0f..cdfd28ba84 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -6,12 +6,12 @@ class ActivityPub::OutboxesController < Api::BaseController include SignatureVerification include AccountOwnedConcern + before_action :require_signature!, if: :authorized_fetch_mode? before_action :set_statuses before_action :set_cache_headers def show - expires_in 1.minute, public: true unless page_requested? - + expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?) render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index 99b7b310f4..020c077ab0 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -7,11 +7,13 @@ class ActivityPub::RepliesController < Api::BaseController DESCENDANTS_LIMIT = 60 + before_action :require_signature!, if: :authorized_fetch_mode? before_action :set_status before_action :set_cache_headers before_action :set_replies def index + expires_in 0, public: public_fetch_mode? render json: replies_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json', skip_activities: true end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index cc8b8e4da6..16e7d70a37 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -36,6 +36,14 @@ class ApplicationController < ActionController::Base Rails.env.production? end + def authorized_fetch_mode? + ENV['AUTHORIZED_FETCH'] == 'true' + end + + def public_fetch_mode? + !authorized_fetch_mode? + end + def store_current_location store_location_for(:user, request.url) unless request.format == :json end @@ -152,6 +160,6 @@ class ApplicationController < ActionController::Base end def set_cache_headers - response.headers['Vary'] = 'Accept' + response.headers['Vary'] = 'Accept, Signature' end end diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb index 287a930da4..11eac0eb6b 100644 --- a/app/controllers/concerns/account_controller_concern.rb +++ b/app/controllers/concerns/account_controller_concern.rb @@ -11,7 +11,7 @@ module AccountControllerConcern layout 'public' before_action :set_instance_presenter - before_action :set_link_headers + before_action :set_link_headers, if: -> { request.format.nil? || request.format == :html } end private diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 0ccdf5ec98..7b251cf804 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -7,12 +7,20 @@ module SignatureVerification include DomainControlHelper + def require_signature! + render plain: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account + end + def signed_request? request.headers['Signature'].present? end def signature_verification_failure_reason - return @signature_verification_failure_reason if defined?(@signature_verification_failure_reason) + @signature_verification_failure_reason + end + + def signature_verification_failure_code + @signature_verification_failure_code || 401 end def signed_request_account @@ -125,11 +133,16 @@ module SignatureVerification end def account_from_key_id(key_id) + domain = key_id.start_with?('acct:') ? key_id.split('@').last : key_id + + if domain_not_allowed?(domain) + @signature_verification_failure_code = 403 + return + end + if key_id.start_with?('acct:') stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) } elsif !ActivityPub::TagManager.instance.local_uri?(key_id) - return if domain_not_allowed?(key_id) - account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account) account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) } account diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 8baa64490f..6e873de5b6 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -2,7 +2,9 @@ class FollowerAccountsController < ApplicationController include AccountControllerConcern + include SignatureVerification + before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_cache_headers def index @@ -17,9 +19,9 @@ class FollowerAccountsController < ApplicationController end format.json do - raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? + raise Mastodon::NotPermittedError if page_requested? && @account.user_hides_network? - expires_in 3.minutes, public: true if params[:page].blank? + expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?) render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, @@ -35,12 +37,16 @@ class FollowerAccountsController < ApplicationController @follows ||= Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account) end + def page_requested? + params[:page].present? + end + def page_url(page) account_followers_url(@account, page: page) unless page.nil? end def collection_presenter - if params[:page].present? + if page_requested? ActivityPub::CollectionPresenter.new( id: account_followers_url(@account, page: params.fetch(:page, 1)), type: :ordered, diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 4d1ea4594e..07d62f7dd1 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -2,7 +2,9 @@ class FollowingAccountsController < ApplicationController include AccountControllerConcern + include SignatureVerification + before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_cache_headers def index @@ -17,9 +19,9 @@ class FollowingAccountsController < ApplicationController end format.json do - raise Mastodon::NotPermittedError if params[:page].present? && @account.user_hides_network? + raise Mastodon::NotPermittedError if page_requested? && @account.user_hides_network? - expires_in 3.minutes, public: true if params[:page].blank? + expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?) render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, @@ -35,12 +37,16 @@ class FollowingAccountsController < ApplicationController @follows ||= Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account) end + def page_requested? + params[:page].present? + end + def page_url(page) account_following_index_url(@account, page: page) unless page.nil? end def collection_presenter - if params[:page].present? + if page_requested? ActivityPub::CollectionPresenter.new( id: account_following_index_url(@account, page: params.fetch(:page, 1)), type: :ordered, diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 13ce5c691a..22e7519f9a 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -8,11 +8,12 @@ class StatusesController < ApplicationController layout 'public' + before_action :require_signature!, only: :show, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_status before_action :set_instance_presenter before_action :set_link_headers - before_action :redirect_to_original, only: [:show] - before_action :set_referrer_policy_header, only: [:show] + before_action :redirect_to_original, only: :show + before_action :set_referrer_policy_header, only: :show before_action :set_cache_headers before_action :set_body_classes before_action :set_autoplay, only: :embed @@ -30,14 +31,14 @@ class StatusesController < ApplicationController end format.json do - expires_in 3.minutes, public: @status.distributable? + expires_in 3.minutes, public: @status.distributable? && public_fetch_mode? render json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter end end end def activity - expires_in 3.minutes, public: @status.distributable? + expires_in 3.minutes, public: @status.distributable? && public_fetch_mode? render json: @status, content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 2ecce0ca22..d08e5a61a5 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true class TagsController < ApplicationController + include SignatureVerification + PAGE_SIZE = 20 layout 'public' + before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_tag before_action :set_body_classes before_action :set_instance_presenter @@ -30,7 +33,7 @@ class TagsController < ApplicationController end format.json do - expires_in 3.minutes, public: true + expires_in 3.minutes, public: public_fetch_mode? @statuses = HashtagQueryService.new.call(@tag, params.slice(:any, :all, :none), current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id]) @statuses = cache_collection(@statuses, Status) diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index c259c96f41..a1d84de2fb 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -33,6 +33,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base def serializable_hash(options = nil) options = serialization_options(options) serialized_hash = serializer.serializable_hash(options) + serialized_hash = serialized_hash.select { |k, _| options[:fields].include?(k) } if options[:fields] serialized_hash = self.class.transform_key_casing!(serialized_hash, instance_options) { '@context' => serialized_context }.merge(serialized_hash) diff --git a/spec/controllers/activitypub/inboxes_controller_spec.rb b/spec/controllers/activitypub/inboxes_controller_spec.rb index eab4b8c3e6..a9ee754900 100644 --- a/spec/controllers/activitypub/inboxes_controller_spec.rb +++ b/spec/controllers/activitypub/inboxes_controller_spec.rb @@ -4,7 +4,7 @@ require 'rails_helper' RSpec.describe ActivityPub::InboxesController, type: :controller do describe 'POST #create' do - context 'if signed_request_account' do + context 'with signed_request_account' do it 'returns 202' do allow(controller).to receive(:signed_request_account) do Fabricate(:account) @@ -15,7 +15,7 @@ RSpec.describe ActivityPub::InboxesController, type: :controller do end end - context 'not signed_request_account' do + context 'without signed_request_account' do it 'returns 401' do allow(controller).to receive(:signed_request_account) do false From 402302776c82c3853e723fe0c0c4dc99c69da3d9 Mon Sep 17 00:00:00 2001 From: "han@highemelry" Date: Sat, 13 Jul 2019 01:46:21 +0900 Subject: [PATCH 29/71] Change the retry limit in error of web push notification (#11292) - Change the maximum count of retry for web push notification (Default -> 5). - In case of high load of subscribe server, the retries will be repeated many times. - Because the retries occupy the default queue, maximum retry count should be reduced. --- app/workers/web/push_notification_worker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/workers/web/push_notification_worker.rb b/app/workers/web/push_notification_worker.rb index 8e8a359735..9010439758 100644 --- a/app/workers/web/push_notification_worker.rb +++ b/app/workers/web/push_notification_worker.rb @@ -3,7 +3,7 @@ class Web::PushNotificationWorker include Sidekiq::Worker - sidekiq_options backtrace: true + sidekiq_options backtrace: true, retry: 5 def perform(subscription_id, notification_id) subscription = ::Web::PushSubscription.find(subscription_id) From 6ff67be0f6e79ec403e08c69717ee8c89451c70e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 13 Jul 2019 16:45:50 +0200 Subject: [PATCH 30/71] Add a spam check (#11217) * Add a spam check * Use Nilsimsa to generate locality-sensitive hashes and compare using Levenshtein distance * Add more tests * Add exemption when the message is a reply to something that mentions the sender * Use Nilsimsa Compare Value instead of Levenshtein distance * Use MD5 for messages shorter than 10 characters * Add message to automated report, do not add non-public statuses to automated report, add trust level to accounts and make unsilencing raise the trust level to prevent repeated spam checks on that account * Expire spam check data after 3 months * Add support for local statuses, reduce expiration to 1 week, always create a report * Add content warnings to the spam check and exempt empty statuses * Change Nilsimsa threshold to 95 and make sure removed statuses are removed from the spam check * Add all matched statuses into automatic report --- Gemfile | 1 + Gemfile.lock | 8 + app/lib/activitypub/activity/create.rb | 13 ++ app/lib/spam_check.rb | 169 ++++++++++++++++++ app/models/account.rb | 18 +- app/services/remove_status_service.rb | 5 + config/locales/en.yml | 2 + ...90701022101_add_trust_level_to_accounts.rb | 5 + db/schema.rb | 1 + spec/lib/spam_check_spec.rb | 160 +++++++++++++++++ 10 files changed, 377 insertions(+), 5 deletions(-) create mode 100644 app/lib/spam_check.rb create mode 100644 db/migrate/20190701022101_add_trust_level_to_accounts.rb create mode 100644 spec/lib/spam_check_spec.rb diff --git a/Gemfile b/Gemfile index 613515628e..15334678b5 100644 --- a/Gemfile +++ b/Gemfile @@ -58,6 +58,7 @@ gem 'idn-ruby', require: 'idn' gem 'kaminari', '~> 1.1' gem 'link_header', '~> 0.0' gem 'mime-types', '~> 3.2', require: 'mime/types/columnar' +gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532' gem 'nokogiri', '~> 1.10' gem 'nsa', '~> 0.2' gem 'oj', '~> 3.7' diff --git a/Gemfile.lock b/Gemfile.lock index 340bbcdd87..c3198b7d9b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -12,6 +12,13 @@ GIT specs: http_parser.rb (0.6.1) +GIT + remote: https://github.com/witgo/nilsimsa + revision: fd184883048b922b176939f851338d0a4971a532 + ref: fd184883048b922b176939f851338d0a4971a532 + specs: + nilsimsa (1.1.2) + GEM remote: https://rubygems.org/ specs: @@ -704,6 +711,7 @@ DEPENDENCIES microformats (~> 4.1) mime-types (~> 3.2) net-ldap (~> 0.10) + nilsimsa! nokogiri (~> 1.10) nsa (~> 0.2) oj (~> 3.7) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 5849c20d77..56c24680a7 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -41,6 +41,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity resolve_thread(@status) fetch_replies(@status) + check_for_spam distribute(@status) forward_for_reply if @status.distributable? end @@ -406,6 +407,18 @@ class ActivityPub::Activity::Create < ActivityPub::Activity Account.local.where(username: local_usernames).exists? end + def check_for_spam + spam_check = SpamCheck.new(@status) + + return if spam_check.skip? + + if spam_check.spam? + spam_check.flag! + else + spam_check.remember! + end + end + def forward_for_reply return unless @json['signature'].present? && reply_to_local? ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url]) diff --git a/app/lib/spam_check.rb b/app/lib/spam_check.rb new file mode 100644 index 0000000000..923d48a022 --- /dev/null +++ b/app/lib/spam_check.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +class SpamCheck + include Redisable + include ActionView::Helpers::TextHelper + + NILSIMSA_COMPARE_THRESHOLD = 95 + NILSIMSA_MIN_SIZE = 10 + EXPIRE_SET_AFTER = 1.week.seconds + + def initialize(status) + @account = status.account + @status = status + end + + def skip? + already_flagged? || trusted? || no_unsolicited_mentions? || solicited_reply? + end + + def spam? + if insufficient_data? + false + elsif nilsimsa? + any_other_digest?('nilsimsa') { |_, other_digest| nilsimsa_compare_value(digest, other_digest) >= NILSIMSA_COMPARE_THRESHOLD } + else + any_other_digest?('md5') { |_, other_digest| other_digest == digest } + end + end + + def flag! + auto_silence_account! + auto_report_status! + end + + def remember! + # The scores in sorted sets don't actually have enough bits to hold an exact + # value of our snowflake IDs, so we use it only for its ordering property. To + # get the correct status ID back, we have to save it in the string value + + redis.zadd(redis_key, @status.id, digest_with_algorithm) + redis.zremrangebyrank(redis_key, '0', '-10') + redis.expire(redis_key, EXPIRE_SET_AFTER) + end + + def reset! + redis.del(redis_key) + end + + def hashable_text + return @hashable_text if defined?(@hashable_text) + + @hashable_text = @status.text + @hashable_text = remove_mentions(@hashable_text) + @hashable_text = strip_tags(@hashable_text) unless @status.local? + @hashable_text = normalize_unicode(@status.spoiler_text + ' ' + @hashable_text) + @hashable_text = remove_whitespace(@hashable_text) + end + + def insufficient_data? + hashable_text.blank? + end + + def digest + @digest ||= begin + if nilsimsa? + Nilsimsa.new(hashable_text).hexdigest + else + Digest::MD5.hexdigest(hashable_text) + end + end + end + + def digest_with_algorithm + if nilsimsa? + ['nilsimsa', digest, @status.id].join(':') + else + ['md5', digest, @status.id].join(':') + end + end + + private + + def remove_mentions(text) + return text.gsub(Account::MENTION_RE, '') if @status.local? + + Nokogiri::HTML.fragment(text).tap do |html| + mentions = @status.mentions.map { |mention| ActivityPub::TagManager.instance.url_for(mention.account) } + + html.traverse do |element| + element.unlink if element.name == 'a' && mentions.include?(element['href']) + end + end.to_s + end + + def normalize_unicode(text) + text.unicode_normalize(:nfkc).downcase + end + + def remove_whitespace(text) + text.gsub(/\s+/, ' ').strip + end + + def auto_silence_account! + @account.silence! + end + + def auto_report_status! + status_ids = Status.where(visibility: %i(public unlisted)).where(id: matching_status_ids).pluck(:id) + [@status.id] if @status.distributable? + ReportService.new.call(Account.representative, @account, status_ids: status_ids, comment: I18n.t('spam_check.spam_detected_and_silenced')) + end + + def already_flagged? + @account.silenced? + end + + def trusted? + @account.trust_level > Account::TRUST_LEVELS[:untrusted] + end + + def no_unsolicited_mentions? + @status.mentions.all? { |mention| mention.silent? || (!@account.local? && !mention.account.local?) || mention.account.following?(@account) } + end + + def solicited_reply? + !@status.thread.nil? && @status.thread.mentions.where(account: @account).exists? + end + + def nilsimsa_compare_value(first, second) + first = [first].pack('H*') + second = [second].pack('H*') + bits = 0 + + 0.upto(31) do |i| + bits += Nilsimsa::POPC[255 & (first[i].ord ^ second[i].ord)].ord + end + + 128 - bits # -128 <= Nilsimsa Compare Value <= 128 + end + + def nilsimsa? + hashable_text.size > NILSIMSA_MIN_SIZE + end + + def other_digests + redis.zrange(redis_key, 0, -1) + end + + def any_other_digest?(filter_algorithm) + other_digests.any? do |record| + algorithm, other_digest, status_id = record.split(':') + + next unless algorithm == filter_algorithm + + yield algorithm, other_digest, status_id + end + end + + def matching_status_ids + if nilsimsa? + other_digests.select { |record| record.start_with?('nilsimsa') && nilsimsa_compare_value(digest, record.split(':')[1]) >= NILSIMSA_COMPARE_THRESHOLD }.map { |record| record.split(':')[2] }.compact + else + other_digests.select { |record| record.start_with?('md5') && record.split(':')[1] == digest }.map { |record| record.split(':')[2] }.compact + end + end + + def redis_key + @redis_key ||= "spam_check:#{@account.id}" + end +end diff --git a/app/models/account.rb b/app/models/account.rb index d6772eb982..a22b7fd7cc 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -45,6 +45,7 @@ # also_known_as :string is an Array # silenced_at :datetime # suspended_at :datetime +# trust_level :integer # class Account < ApplicationRecord @@ -62,6 +63,11 @@ class Account < ApplicationRecord include AccountCounters include DomainNormalizable + TRUST_LEVELS = { + untrusted: 0, + trusted: 1, + }.freeze + enum protocol: [:ostatus, :activitypub] validates :username, presence: true @@ -163,6 +169,10 @@ class Account < ApplicationRecord last_webfingered_at.nil? || last_webfingered_at <= 1.day.ago end + def trust_level + self[:trust_level] || 0 + end + def refresh! ResolveAccountService.new.call(acct) unless local? end @@ -171,21 +181,19 @@ class Account < ApplicationRecord silenced_at.present? end - def silence!(date = nil) - date ||= Time.now.utc + def silence!(date = Time.now.utc) update!(silenced_at: date) end def unsilence! - update!(silenced_at: nil) + update!(silenced_at: nil, trust_level: trust_level == TRUST_LEVELS[:untrusted] ? TRUST_LEVELS[:trusted] : trust_level) end def suspended? suspended_at.present? end - def suspend!(date = nil) - date ||= Time.now.utc + def suspend!(date = Time.now.utc) transaction do user&.disable! if local? update!(suspended_at: date) diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 6311971ffc..a69fce8b85 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -23,6 +23,7 @@ class RemoveStatusService < BaseService remove_from_hashtags remove_from_public remove_from_media if status.media_attachments.any? + remove_from_spam_check @status.destroy! else @@ -142,6 +143,10 @@ class RemoveStatusService < BaseService redis.publish('timeline:public:local:media', @payload) if @status.local? end + def remove_from_spam_check + redis.zremrangebyscore("spam_check:#{@status.account_id}", @status.id, @status.id) + end + def lock_options { redis: Redis.current, key: "distribute:#{@status.id}" } end diff --git a/config/locales/en.yml b/config/locales/en.yml index 00b7d1dbea..89251ad407 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -875,6 +875,8 @@ en: profile: Profile relationships: Follows and followers two_factor_authentication: Two-factor Auth + spam_check: + spam_detected_and_silenced: This is an automated report. Spam has been detected and the sender has been silenced automatically. If this is a mistake, please unsilence the account. statuses: attached: description: 'Attached: %{attached}' diff --git a/db/migrate/20190701022101_add_trust_level_to_accounts.rb b/db/migrate/20190701022101_add_trust_level_to_accounts.rb new file mode 100644 index 0000000000..917486d2ed --- /dev/null +++ b/db/migrate/20190701022101_add_trust_level_to_accounts.rb @@ -0,0 +1,5 @@ +class AddTrustLevelToAccounts < ActiveRecord::Migration[5.2] + def change + add_column :accounts, :trust_level, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 2e38fb1f26..c7b6b9be69 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -148,6 +148,7 @@ ActiveRecord::Schema.define(version: 2019_07_06_233204) do t.string "also_known_as", array: true t.datetime "silenced_at" t.datetime "suspended_at" + t.integer "trust_level" t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower", unique: true t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id" diff --git a/spec/lib/spam_check_spec.rb b/spec/lib/spam_check_spec.rb new file mode 100644 index 0000000000..c722dc6427 --- /dev/null +++ b/spec/lib/spam_check_spec.rb @@ -0,0 +1,160 @@ +require 'rails_helper' + +RSpec.describe SpamCheck do + let!(:sender) { Fabricate(:account) } + let!(:alice) { Fabricate(:account, username: 'alice') } + let!(:bob) { Fabricate(:account, username: 'bob') } + + def status_with_html(text, options = {}) + status = PostStatusService.new.call(sender, { text: text }.merge(options)) + status.update_columns(text: Formatter.instance.format(status), local: false) + status + end + + describe '#hashable_text' do + it 'removes mentions from HTML for remote statuses' do + status = status_with_html('@alice Hello') + expect(described_class.new(status).hashable_text).to eq 'hello' + end + + it 'removes mentions from text for local statuses' do + status = PostStatusService.new.call(alice, text: "Hey @#{sender.username}, how are you?") + expect(described_class.new(status).hashable_text).to eq 'hey , how are you?' + end + end + + describe '#insufficient_data?' do + it 'returns true when there is no text' do + status = status_with_html('@alice') + expect(described_class.new(status).insufficient_data?).to be true + end + + it 'returns false when there is text' do + status = status_with_html('@alice h') + expect(described_class.new(status).insufficient_data?).to be false + end + end + + describe '#digest' do + it 'returns a string' do + status = status_with_html('@alice Hello world') + expect(described_class.new(status).digest).to be_a String + end + end + + describe '#spam?' do + it 'returns false for a unique status' do + status = status_with_html('@alice Hello') + expect(described_class.new(status).spam?).to be false + end + + it 'returns false for different statuses to the same recipient' do + status1 = status_with_html('@alice Hello') + described_class.new(status1).remember! + status2 = status_with_html('@alice Are you available to talk?') + expect(described_class.new(status2).spam?).to be false + end + + it 'returns false for statuses with different content warnings' do + status1 = status_with_html('@alice Are you available to talk?') + described_class.new(status1).remember! + status2 = status_with_html('@alice Are you available to talk?', spoiler_text: 'This is a completely different matter than what I was talking about previously, I swear!') + expect(described_class.new(status2).spam?).to be false + end + + it 'returns false for different statuses to different recipients' do + status1 = status_with_html('@alice How is it going?') + described_class.new(status1).remember! + status2 = status_with_html('@bob Are you okay?') + expect(described_class.new(status2).spam?).to be false + end + + it 'returns false for very short different statuses to different recipients' do + status1 = status_with_html('@alice 🙄') + described_class.new(status1).remember! + status2 = status_with_html('@bob Huh?') + expect(described_class.new(status2).spam?).to be false + end + + it 'returns false for statuses with no text' do + status1 = status_with_html('@alice') + described_class.new(status1).remember! + status2 = status_with_html('@bob') + expect(described_class.new(status2).spam?).to be false + end + + it 'returns true for duplicate statuses to the same recipient' do + status1 = status_with_html('@alice Hello') + described_class.new(status1).remember! + status2 = status_with_html('@alice Hello') + expect(described_class.new(status2).spam?).to be true + end + + it 'returns true for duplicate statuses to different recipients' do + status1 = status_with_html('@alice Hello') + described_class.new(status1).remember! + status2 = status_with_html('@bob Hello') + expect(described_class.new(status2).spam?).to be true + end + + it 'returns true for nearly identical statuses with random numbers' do + source_text = 'Sodium, atomic number 11, was first isolated by Humphry Davy in 1807. A chemical component of salt, he named it Na in honor of the saltiest region on earth, North America.' + status1 = status_with_html('@alice ' + source_text + ' 1234') + described_class.new(status1).remember! + status2 = status_with_html('@bob ' + source_text + ' 9568') + expect(described_class.new(status2).spam?).to be true + end + end + + describe '#skip?' do + it 'returns true when the sender is already silenced' do + status = status_with_html('@alice Hello') + sender.silence! + expect(described_class.new(status).skip?).to be true + end + + it 'returns true when the mentioned person follows the sender' do + status = status_with_html('@alice Hello') + alice.follow!(sender) + expect(described_class.new(status).skip?).to be true + end + + it 'returns false when even one mentioned person doesn\'t follow the sender' do + status = status_with_html('@alice @bob Hello') + alice.follow!(sender) + expect(described_class.new(status).skip?).to be false + end + + it 'returns true when the sender is replying to a status that mentions the sender' do + parent = PostStatusService.new.call(alice, text: "Hey @#{sender.username}, how are you?") + status = status_with_html('@alice @bob Hello', thread: parent) + expect(described_class.new(status).skip?).to be true + end + end + + describe '#remember!' do + pending + end + + describe '#flag!' do + let!(:status1) { status_with_html('@alice General Kenobi you are a bold one') } + let!(:status2) { status_with_html('@alice @bob General Kenobi, you are a bold one') } + + before do + described_class.new(status1).remember! + described_class.new(status2).flag! + end + + it 'silences the account' do + expect(sender.silenced?).to be true + end + + it 'creates a report about the account' do + expect(sender.targeted_reports.unresolved.count).to eq 1 + end + + it 'attaches both matching statuses to the report' do + expect(sender.targeted_reports.first.status_ids).to include(status1.id, status2.id) + end + end +end From 6af0c955e1ab15f768d4310607177f9f628eb0ad Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sat, 13 Jul 2019 23:52:08 +0900 Subject: [PATCH 31/71] Bump rubocop-rails from 2.0.1 to 2.2.0 (#11257) Bumps [rubocop-rails](https://github.com/rubocop-hq/rubocop-rails) from 2.0.1 to 2.2.0. - [Release notes](https://github.com/rubocop-hq/rubocop-rails/releases) - [Changelog](https://github.com/rubocop-hq/rubocop-rails/blob/master/CHANGELOG.md) - [Commits](https://github.com/rubocop-hq/rubocop-rails/compare/v2.0.1...v2.2.0) Signed-off-by: dependabot-preview[bot] --- Gemfile | 2 +- Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 15334678b5..42208085ca 100644 --- a/Gemfile +++ b/Gemfile @@ -130,7 +130,7 @@ group :development do gem 'letter_opener_web', '~> 1.3' gem 'memory_profiler' gem 'rubocop', '~> 0.72', require: false - gem 'rubocop-rails', '~> 2.0', require: false + gem 'rubocop-rails', '~> 2.2', require: false gem 'brakeman', '~> 4.5', require: false gem 'bundler-audit', '~> 0.6', require: false diff --git a/Gemfile.lock b/Gemfile.lock index c3198b7d9b..4036a05c1e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -538,9 +538,9 @@ GEM rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 1.7) - rubocop-rails (2.0.1) + rubocop-rails (2.2.0) rack (>= 1.1) - rubocop (>= 0.70.0) + rubocop (>= 0.72.0) ruby-progressbar (1.10.1) ruby-saml (1.9.0) nokogiri (>= 1.5.10) @@ -747,7 +747,7 @@ DEPENDENCIES rspec-rails (~> 3.8) rspec-sidekiq (~> 3.0) rubocop (~> 0.72) - rubocop-rails (~> 2.0) + rubocop-rails (~> 2.2) sanitize (~> 5.0) sidekiq (~> 5.2) sidekiq-bulk (~> 0.2.0) From 2ea4dbb035f692c6b5c271e3a6e7625f92f94e73 Mon Sep 17 00:00:00 2001 From: PatOnTheBack <51241310+PatOnTheBack@users.noreply.github.com> Date: Sun, 14 Jul 2019 01:05:11 -0400 Subject: [PATCH 32/71] Bump handlebars from 4.1.0 to 4.1.2 (#11293) Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.1.0 to 4.1.2. - [Release notes](https://github.com/wycats/handlebars.js/releases) - [Changelog](https://github.com/wycats/handlebars.js/blob/master/release-notes.md) - [Commits](https://github.com/wycats/handlebars.js/compare/v4.1.0...v4.1.2) Signed-off-by: dependabot[bot] --- yarn.lock | 38 +++++++++++++------------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/yarn.lock b/yarn.lock index f0cedbba72..623bcd7057 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1681,13 +1681,6 @@ async@^1.5.2: resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= -async@^2.5.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" - integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== - dependencies: - lodash "^4.17.10" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -2495,16 +2488,11 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.11.0, commander@^2.18.0, commander@^2.19.0, commander@^2.8.1: +commander@^2.11.0, commander@^2.18.0, commander@^2.19.0, commander@^2.8.1, commander@~2.20.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== -commander@~2.17.1: - version "2.17.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" - integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== - commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -4598,11 +4586,11 @@ handle-thing@^2.0.0: integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== handlebars@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.0.tgz#0d6a6f34ff1f63cecec8423aa4169827bf787c3a" - integrity sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w== + version "4.1.2" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" + integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw== dependencies: - async "^2.5.0" + neo-async "^2.6.0" optimist "^0.6.1" source-map "^0.6.1" optionalDependencies: @@ -6684,10 +6672,10 @@ negotiator@0.6.2: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== -neo-async@^2.5.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.0.tgz#b9d15e4d71c6762908654b5183ed38b753340835" - integrity sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA== +neo-async@^2.5.0, neo-async@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" + integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== next-tick@1, next-tick@^1.0.0: version "1.0.0" @@ -9930,11 +9918,11 @@ ua-parser-js@^0.7.18: integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ== uglify-js@^3.1.4: - version "3.4.9" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" - integrity sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q== + version "3.6.0" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5" + integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg== dependencies: - commander "~2.17.1" + commander "~2.20.0" source-map "~0.6.1" unicode-astral-regex@^1.0.1: From 3595ce6325faf5148efc152718cbe844b972ea11 Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 15 Jul 2019 02:29:04 +0200 Subject: [PATCH 33/71] Fix leaking private statuses the admin account follows (#11300) Now that the request is signed, it can return private toots. Do not leak them. --- app/services/resolve_url_service.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb index 80381c16b6..aa883597a4 100644 --- a/app/services/resolve_url_service.rb +++ b/app/services/resolve_url_service.rb @@ -21,7 +21,9 @@ class ResolveURLService < BaseService if equals_or_includes_any?(type, ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) FetchRemoteAccountService.new.call(resource_url, body, protocol) elsif equals_or_includes_any?(type, ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES) - FetchRemoteStatusService.new.call(resource_url, body, protocol) + status = FetchRemoteStatusService.new.call(resource_url, body, protocol) + authorize_with @on_behalf_of, status, :show? unless status.nil? + status end end From 2f813b7ea10e0323f76d30bf2d6d1aaa87674bce Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 15 Jul 2019 02:29:39 +0200 Subject: [PATCH 34/71] Disable LDSigning when AUTHORIZED_FETCH is set to true (#11295) --- app/services/concerns/payloadable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/concerns/payloadable.rb b/app/services/concerns/payloadable.rb index 13d9c35483..953740faa3 100644 --- a/app/services/concerns/payloadable.rb +++ b/app/services/concerns/payloadable.rb @@ -14,6 +14,6 @@ module Payloadable end def signing_enabled? - true + ENV['AUTHORIZED_FETCH'] != 'true' end end From e7353c47dbc200536dedb7beea37d60c6ea6fa2f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 15 Jul 2019 05:56:35 +0200 Subject: [PATCH 35/71] Change default interface of web and streaming from 0.0.0.0 to 127.0.0.1 (#11302) --- config/puma.rb | 4 ++-- docker-compose.yml | 2 +- streaming/index.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/puma.rb b/config/puma.rb index 1afdb1c6df..25a5534b29 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -2,9 +2,9 @@ threads_count = ENV.fetch('MAX_THREADS') { 5 }.to_i threads threads_count, threads_count if ENV['SOCKET'] - bind 'unix://' + ENV['SOCKET'] + bind "unix://#{ENV['SOCKET']}" else - port ENV.fetch('PORT') { 3000 } + bind "tcp://127.0.0.1:#{ENV.fetch('PORT', 3000)}" end environment ENV.fetch('RAILS_ENV') { 'development' } diff --git a/docker-compose.yml b/docker-compose.yml index 93d47f1a01..f3fe6cfd0b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -58,7 +58,7 @@ services: image: tootsuite/mastodon restart: always env_file: .env.production - command: yarn start + command: BIND=0.0.0.0 node ./streaming networks: - external_network - internal_network diff --git a/streaming/index.js b/streaming/index.js index 639867b28c..0529804b1b 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -672,7 +672,7 @@ const attachServerWithConfig = (server, onSuccess) => { } }); } else { - server.listen(+process.env.PORT || 4000, process.env.BIND || '0.0.0.0', () => { + server.listen(+process.env.PORT || 4000, process.env.BIND || '127.0.0.1', () => { if (onSuccess) { onSuccess(`${server.address().address}:${server.address().port}`); } From cecd0c3cb1ed208b3291342d644128c8fe71f12d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 15 Jul 2019 06:12:05 +0200 Subject: [PATCH 36/71] New Crowdin translations (#11153) * New translations activerecord.en.yml (Indonesian) [ci skip] * New translations activerecord.en.yml (Italian) [ci skip] * New translations simple_form.en.yml (Persian) [ci skip] * New translations simple_form.en.yml (Norwegian) [ci skip] * New translations en.yml (Russian) [ci skip] * New translations simple_form.en.yml (Finnish) [ci skip] * New translations en.yml (Serbian (Cyrillic)) [ci skip] * New translations en.yml (Serbian (Latin)) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.yml (Swedish) [ci skip] * New translations en.yml (Tamil) [ci skip] * New translations en.yml (Telugu) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations en.yml (Turkish) [ci skip] * New translations en.yml (Ukrainian) [ci skip] * New translations en.yml (Welsh) [ci skip] * New translations simple_form.en.yml (Dutch) [ci skip] * New translations simple_form.en.yml (Esperanto) [ci skip] * New translations simple_form.en.yml (French) [ci skip] * New translations simple_form.en.yml (Galician) [ci skip] * New translations simple_form.en.yml (Georgian) [ci skip] * New translations simple_form.en.yml (German) [ci skip] * New translations simple_form.en.yml (Greek) [ci skip] * New translations simple_form.en.yml (Hebrew) [ci skip] * New translations simple_form.en.yml (Hungarian) [ci skip] * New translations simple_form.en.yml (Ido) [ci skip] * New translations simple_form.en.yml (Indonesian) [ci skip] * New translations simple_form.en.yml (Italian) [ci skip] * New translations simple_form.en.yml (Korean) [ci skip] * New translations doorkeeper.en.yml (Welsh) [ci skip] * New translations simple_form.en.yml (Occitan) [ci skip] * New translations en.yml (Occitan) [ci skip] * New translations en.yml (Occitan) [ci skip] * New translations simple_form.en.yml (Japanese) [ci skip] * New translations simple_form.en.yml (Japanese) [ci skip] * New translations simple_form.en.yml (Japanese) [ci skip] * New translations simple_form.en.yml (Japanese) [ci skip] * New translations simple_form.en.yml (Japanese) [ci skip] * New translations en.json (Italian) [ci skip] * New translations simple_form.en.yml (Greek) [ci skip] * New translations simple_form.en.yml (Czech) [ci skip] * New translations simple_form.en.yml (Basque) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations simple_form.en.yml (German) [ci skip] * New translations en.yml (Polish) [ci skip] * New translations simple_form.en.yml (Polish) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations doorkeeper.en.yml (Chinese Simplified) [ci skip] * New translations simple_form.en.yml (Japanese) [ci skip] * New translations simple_form.en.yml (Thai) [ci skip] * New translations en.json (Thai) [ci skip] * New translations simple_form.en.yml (Slovak) [ci skip] * New translations simple_form.en.yml (Corsican) [ci skip] * New translations simple_form.en.yml (Corsican) [ci skip] * New translations simple_form.en.yml (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations simple_form.en.yml (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations simple_form.en.yml (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Slovenian) [ci skip] * New translations en.yml (Slovenian) [ci skip] * New translations en.yml (Slovenian) [ci skip] * New translations en.yml (Slovenian) [ci skip] * New translations en.yml (Slovenian) [ci skip] * New translations en.yml (Slovenian) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations simple_form.en.yml (Chinese Simplified) [ci skip] * New translations simple_form.en.yml (Galician) [ci skip] * New translations en.json (Galician) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Occitan) [ci skip] * New translations en.json (Portuguese, Brazilian) [ci skip] * New translations en.json (Portuguese, Brazilian) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.yml (Occitan) [ci skip] * New translations doorkeeper.en.yml (Welsh) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations simple_form.en.yml (Welsh) [ci skip] * New translations activerecord.en.yml (Welsh) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Occitan) [ci skip] * New translations en.yml (Occitan) [ci skip] * New translations en.yml (Albanian) [ci skip] * New translations activerecord.en.yml (Serbian (Latin)) [ci skip] * New translations doorkeeper.en.yml (Serbian (Latin)) [ci skip] * New translations devise.en.yml (Serbian (Latin)) [ci skip] * New translations en.yml (Arabic) [ci skip] * New translations en.yml (Basque) [ci skip] * New translations en.yml (Esperanto) [ci skip] * New translations en.yml (Hebrew) [ci skip] * New translations en.yml (Greek) [ci skip] * New translations en.yml (German) [ci skip] * New translations en.yml (Georgian) [ci skip] * New translations en.yml (Galician) [ci skip] * New translations en.yml (French) [ci skip] * New translations en.yml (Finnish) [ci skip] * New translations en.yml (Dutch) [ci skip] * New translations en.yml (Danish) [ci skip] * New translations en.yml (Corsican) [ci skip] * New translations en.yml (Chinese Traditional, Hong Kong) [ci skip] * New translations en.yml (Chinese Traditional) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Catalan) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Indonesian) [ci skip] * New translations en.yml (Czech) [ci skip] * New translations simple_form.en.yml (Serbian (Latin)) [ci skip] * New translations en.yml (Italian) [ci skip] * New translations en.yml (Persian) [ci skip] * New translations en.yml (Serbian (Latin)) [ci skip] * New translations en.yml (Serbian (Cyrillic)) [ci skip] * New translations en.yml (Russian) [ci skip] * New translations en.yml (Portuguese, Brazilian) [ci skip] * New translations en.yml (Portuguese) [ci skip] * New translations en.yml (Polish) [ci skip] * New translations en.yml (Occitan) [ci skip] * New translations en.yml (Slovenian) [ci skip] * New translations en.yml (Norwegian) [ci skip] * New translations en.yml (Lithuanian) [ci skip] * New translations en.yml (Korean) [ci skip] * New translations en.yml (Kazakh) [ci skip] * New translations en.yml (Japanese) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.yml (Swedish) [ci skip] * New translations en.yml (Welsh) [ci skip] * New translations en.yml (Ukrainian) [ci skip] * New translations en.yml (Turkish) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations devise.en.yml (Chinese Simplified) [ci skip] * New translations en.json (Russian) [ci skip] * New translations en.json (Russian) [ci skip] * New translations en.json (Russian) [ci skip] * New translations en.json (Russian) [ci skip] * New translations en.json (Russian) [ci skip] * New translations en.yml (Basque) [ci skip] * New translations simple_form.en.yml (Basque) [ci skip] * New translations en.yml (Basque) [ci skip] * New translations doorkeeper.en.yml (Basque) [ci skip] * New translations en.json (Korean) [ci skip] * New translations doorkeeper.en.yml (Slovak) [ci skip] * New translations en.yml (Finnish) [ci skip] * New translations en.yml (Esperanto) [ci skip] * New translations en.yml (Dutch) [ci skip] * New translations en.yml (Danish) [ci skip] * New translations en.yml (Corsican) [ci skip] * New translations en.yml (Chinese Traditional, Hong Kong) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Catalan) [ci skip] * New translations en.yml (Chinese Traditional) [ci skip] * New translations en.yml (Basque) [ci skip] * New translations en.yml (Asturian) [ci skip] * New translations en.yml (Arabic) [ci skip] * New translations en.yml (Albanian) [ci skip] * New translations en.yml (Czech) [ci skip] * New translations en.yml (French) [ci skip] * New translations en.yml (Galician) [ci skip] * New translations en.yml (Georgian) [ci skip] * New translations en.yml (German) [ci skip] * New translations en.yml (Greek) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Italian) [ci skip] * New translations en.yml (Japanese) [ci skip] * New translations en.yml (Kazakh) [ci skip] * New translations en.yml (Korean) [ci skip] * New translations en.yml (Lithuanian) [ci skip] * New translations en.yml (Occitan) [ci skip] * New translations en.yml (Persian) [ci skip] * New translations en.yml (Polish) [ci skip] * New translations en.yml (Portuguese) [ci skip] * New translations en.yml (Portuguese, Brazilian) [ci skip] * New translations en.yml (Serbian (Cyrillic)) [ci skip] * New translations en.yml (Russian) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.yml (Swedish) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations en.yml (Ukrainian) [ci skip] * New translations en.yml (Welsh) [ci skip] * New translations en.yml (Russian) [ci skip] * New translations simple_form.en.yml (Russian) [ci skip] * New translations simple_form.en.yml (Russian) [ci skip] * New translations en.json (Thai) [ci skip] * New translations en.json (Thai) [ci skip] * New translations simple_form.en.yml (Thai) [ci skip] * New translations simple_form.en.yml (Thai) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations doorkeeper.en.yml (Spanish) [ci skip] * New translations doorkeeper.en.yml (Spanish) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations simple_form.en.yml (Spanish) [ci skip] * New translations simple_form.en.yml (Spanish) [ci skip] * New translations simple_form.en.yml (Spanish) [ci skip] * New translations doorkeeper.en.yml (Spanish) [ci skip] * New translations en.json (Slovak) [ci skip] * New translations devise.en.yml (Slovak) [ci skip] * New translations doorkeeper.en.yml (Slovak) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations simple_form.en.yml (Japanese) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations activerecord.en.yml (Bengali) [ci skip] * New translations devise.en.yml (Bengali) [ci skip] * New translations devise.en.yml (Bengali) [ci skip] * New translations devise.en.yml (Bengali) [ci skip] * New translations devise.en.yml (Bengali) [ci skip] * New translations devise.en.yml (Bengali) [ci skip] * New translations devise.en.yml (Bengali) [ci skip] * i18n-tasks normalize * yarn manage:translations --- app/javascript/mastodon/locales/ar.json | 3 + app/javascript/mastodon/locales/ast.json | 5 +- app/javascript/mastodon/locales/bg.json | 5 +- app/javascript/mastodon/locales/bn.json | 73 +-- app/javascript/mastodon/locales/ca.json | 3 + app/javascript/mastodon/locales/co.json | 3 + app/javascript/mastodon/locales/cs.json | 3 + app/javascript/mastodon/locales/cy.json | 3 + app/javascript/mastodon/locales/da.json | 3 + app/javascript/mastodon/locales/de.json | 3 + .../mastodon/locales/defaultMessages.json | 27 +- app/javascript/mastodon/locales/el.json | 3 + app/javascript/mastodon/locales/en.json | 3 + app/javascript/mastodon/locales/eo.json | 3 + app/javascript/mastodon/locales/es.json | 109 ++-- app/javascript/mastodon/locales/eu.json | 3 + app/javascript/mastodon/locales/fa.json | 3 + app/javascript/mastodon/locales/fi.json | 3 + app/javascript/mastodon/locales/fr.json | 3 + app/javascript/mastodon/locales/gl.json | 5 +- app/javascript/mastodon/locales/he.json | 5 +- app/javascript/mastodon/locales/hi.json | 3 + app/javascript/mastodon/locales/hr.json | 5 +- app/javascript/mastodon/locales/hu.json | 3 + app/javascript/mastodon/locales/hy.json | 5 +- app/javascript/mastodon/locales/id.json | 93 +-- app/javascript/mastodon/locales/io.json | 5 +- app/javascript/mastodon/locales/it.json | 11 +- app/javascript/mastodon/locales/ja.json | 3 + app/javascript/mastodon/locales/ka.json | 3 + app/javascript/mastodon/locales/kk.json | 3 + app/javascript/mastodon/locales/ko.json | 5 +- app/javascript/mastodon/locales/lt.json | 7 +- app/javascript/mastodon/locales/lv.json | 5 +- app/javascript/mastodon/locales/ms.json | 7 +- app/javascript/mastodon/locales/nl.json | 3 + app/javascript/mastodon/locales/no.json | 5 +- app/javascript/mastodon/locales/oc.json | 13 +- app/javascript/mastodon/locales/pl.json | 3 + app/javascript/mastodon/locales/pt-BR.json | 25 +- app/javascript/mastodon/locales/pt.json | 181 +++--- app/javascript/mastodon/locales/ro.json | 3 + app/javascript/mastodon/locales/ru.json | 49 +- app/javascript/mastodon/locales/sk.json | 5 +- app/javascript/mastodon/locales/sl.json | 35 +- app/javascript/mastodon/locales/sq.json | 3 + app/javascript/mastodon/locales/sr-Latn.json | 5 +- app/javascript/mastodon/locales/sr.json | 3 + app/javascript/mastodon/locales/sv.json | 3 + app/javascript/mastodon/locales/ta.json | 3 + app/javascript/mastodon/locales/te.json | 3 + app/javascript/mastodon/locales/th.json | 35 +- app/javascript/mastodon/locales/tr.json | 3 + app/javascript/mastodon/locales/uk.json | 3 + app/javascript/mastodon/locales/zh-CN.json | 73 +-- app/javascript/mastodon/locales/zh-HK.json | 3 + app/javascript/mastodon/locales/zh-TW.json | 3 + config/locales/activerecord.bn.yml | 16 + config/locales/activerecord.cy.yml | 2 +- config/locales/ar.yml | 10 - config/locales/ast.yml | 2 - config/locales/ca.yml | 11 - config/locales/co.yml | 11 - config/locales/cs.yml | 11 - config/locales/cy.yml | 11 - config/locales/da.yml | 11 - config/locales/de.yml | 11 - config/locales/devise.bn.yml | 39 ++ config/locales/devise.sk.yml | 2 +- config/locales/devise.sl.yml | 39 +- config/locales/devise.zh-CN.yml | 2 +- config/locales/doorkeeper.cy.yml | 6 + config/locales/doorkeeper.es.yml | 28 + config/locales/doorkeeper.eu.yml | 16 +- config/locales/doorkeeper.hu.yml | 6 + config/locales/doorkeeper.nl.yml | 6 + config/locales/doorkeeper.oc.yml | 6 + config/locales/doorkeeper.sk.yml | 35 +- config/locales/doorkeeper.zh-CN.yml | 1 + config/locales/el.yml | 11 - config/locales/eo.yml | 20 +- config/locales/es.yml | 233 +++++++- config/locales/eu.yml | 19 +- config/locales/fa.yml | 11 - config/locales/fi.yml | 8 - config/locales/fr.yml | 11 - config/locales/gl.yml | 11 - config/locales/he.yml | 7 - config/locales/hu.yml | 39 +- config/locales/id.yml | 5 - config/locales/it.yml | 13 +- config/locales/ja.yml | 11 - config/locales/ka.yml | 11 - config/locales/kk.yml | 11 - config/locales/ko.yml | 11 - config/locales/lt.yml | 11 - config/locales/nl.yml | 11 - config/locales/no.yml | 6 - config/locales/oc.yml | 121 +++- config/locales/pl.yml | 13 +- config/locales/pt-BR.yml | 10 - config/locales/pt.yml | 10 - config/locales/ru.yml | 15 +- config/locales/simple_form.co.yml | 2 + config/locales/simple_form.cs.yml | 2 + config/locales/simple_form.cy.yml | 2 + config/locales/simple_form.de.yml | 2 + config/locales/simple_form.el.yml | 2 + config/locales/simple_form.es.yml | 31 + config/locales/simple_form.eu.yml | 10 +- config/locales/simple_form.gl.yml | 2 + config/locales/simple_form.ja.yml | 2 + config/locales/simple_form.oc.yml | 8 + config/locales/simple_form.pl.yml | 2 + config/locales/simple_form.ru.yml | 6 +- config/locales/simple_form.sk.yml | 2 + config/locales/simple_form.th.yml | 2 + config/locales/simple_form.zh-CN.yml | 6 +- config/locales/sk.yml | 23 +- config/locales/sl.yml | 545 +++++++++++++++--- config/locales/sq.yml | 10 - config/locales/sr-Latn.yml | 4 - config/locales/sr.yml | 8 - config/locales/sv.yml | 10 - config/locales/th.yml | 13 +- config/locales/tr.yml | 6 - config/locales/uk.yml | 9 - config/locales/zh-CN.yml | 128 +++- config/locales/zh-HK.yml | 11 - config/locales/zh-TW.yml | 11 - 130 files changed, 1737 insertions(+), 924 deletions(-) diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index d05c61f986..d62ee90c2c 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "أساسية", "home.column_settings.show_reblogs": "عرض الترقيات", "home.column_settings.show_replies": "عرض الردود", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# يوم} other {# أيام}}", "intervals.full.hours": "{number, plural, one {# ساعة} other {# ساعات}}", "intervals.full.minutes": "{number, plural, one {# دقيقة} other {# دقائق}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "عنوان القائمة الجديدة", "lists.search": "إبحث في قائمة الحسابات التي تُتابِعها", "lists.subheading": "قوائمك", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "تحميل...", "media_gallery.toggle_visible": "عرض / إخفاء", "missing_indicator.label": "تعذر العثور عليه", @@ -314,6 +316,7 @@ "search_results.accounts": "أشخاص", "search_results.hashtags": "الوُسوم", "search_results.statuses": "التبويقات", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} و {results}}", "status.admin_account": "افتح الواجهة الإدارية لـ @{name}", "status.admin_status": "افتح هذا المنشور على واجهة الإشراف", diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json index b911848ee0..3ae4e5e5e4 100644 --- a/app/javascript/mastodon/locales/ast.json +++ b/app/javascript/mastodon/locales/ast.json @@ -64,7 +64,7 @@ "column_header.show_settings": "Show settings", "column_header.unpin": "Desfixar", "column_subheading.settings": "Axustes", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "Esti toot namái va unviase a los usuarios mentaos.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Basic", "home.column_settings.show_reblogs": "Amosar toots compartíos", "home.column_settings.show_replies": "Amosar rempuestes", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Títulu nuevu de la llista", "lists.search": "Guetar ente la xente que sigues", "lists.subheading": "Les tos llistes", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Cargando...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Nun s'alcontró", @@ -314,6 +316,7 @@ "search_results.accounts": "Xente", "search_results.hashtags": "Etiquetes", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index 783f9eb688..4c97fe1fc0 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -64,7 +64,7 @@ "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.settings": "Settings", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Basic", "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Зареждане...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/bn.json b/app/javascript/mastodon/locales/bn.json index 5b7162ec12..358f994f30 100644 --- a/app/javascript/mastodon/locales/bn.json +++ b/app/javascript/mastodon/locales/bn.json @@ -1,13 +1,13 @@ { "account.add_or_remove_from_list": "তালিকাতে আরো যুক্ত বা মুছে ফেলুন", "account.badges.bot": "রোবট", - "account.block": "@{name} বন্ধ করুন", + "account.block": "@{name} কে বন্ধ করুন", "account.block_domain": "{domain} থেকে সব সরিয়ে ফেলুন", "account.blocked": "বন্ধ করা হয়েছে", - "account.direct": "@{name}কে সরকারি লিখুন", + "account.direct": "@{name} এর কাছে সরকারি লেখা পাঠাতে", "account.domain_blocked": "ওয়েবসাইট সরিয়ে ফেলা হয়েছে", - "account.edit_profile": "নিজের পাতা সম্পাদনা করুন", - "account.endorse": "নিজের পাতায় দেখান", + "account.edit_profile": "নিজের পাতা সম্পাদনা করতে", + "account.endorse": "আপনার নিজের পাতায় দেখাতে", "account.follow": "অনুসরণ করুন", "account.followers": "অনুসরণকারক", "account.followers.empty": "এই ব্যবহারকারীকে কেও এখনো অনুসরণ করে না।", @@ -18,21 +18,21 @@ "account.link_verified_on": "এই লিংকের মালিকানা চেক করা হয়েছে {date} তারিকে", "account.locked_info": "এই নিবন্ধনের গোপনীয়তার ক্ষেত্র তালা দেওয়া আছে। নিবন্ধনকারী অনুসরণ করার অনুমতি যাদেরকে দেবেন, শুধু তারাই অনুসরণ করতে পারবেন।", "account.media": "ছবি বা ভিডিও", - "account.mention": "@{name} কে উল্লেখ করুন", + "account.mention": "@{name} কে উল্লেখ করতে", "account.moved_to": "{name} চলে গেছে এখানে:", - "account.mute": "@{name}র কার্যক্রম সরিয়ে ফেলুন", + "account.mute": "@{name} সব কার্যক্রম আপনার সময়রেখা থেকে সরিয়ে ফেলতে", "account.mute_notifications": "@{name}র প্রজ্ঞাপন আপনার কাছ থেকে সরিয়ে ফেলুন", "account.muted": "সরানো আছে", "account.posts": "টুট", "account.posts_with_replies": "টুট এবং মতামত", - "account.report": "@{name}কে রিপোর্ট করে দিন", + "account.report": "@{name} কে রিপোর্ট করতে", "account.requested": "অনুমতির অপেক্ষায় আছে। অনুসরণ করার অনুরোধ বাতিল করতে এখানে ক্লিক করুন", "account.share": "@{name}র পাতা অন্যদের দেখান", "account.show_reblogs": "@{name}র সমর্থনগুলো দেখুন", "account.unblock": "@{name}র কার্যকলাপ আবার দেখুন", "account.unblock_domain": "{domain}থেকে আবার দেখুন", - "account.unendorse": "নিজের পাতায় এটা দেখতে চান না", - "account.unfollow": "অনুসরণ বন্ধ করুন", + "account.unendorse": "আপনার নিজের পাতায় এটা না দেখাতে", + "account.unfollow": "অনুসরণ না করতে", "account.unmute": "@{name}র কার্যকলাপ আবার দেখুন", "account.unmute_notifications": "@{name}র প্রজ্ঞাপন দেওয়ার অনুমতি দিন", "alert.unexpected.message": "অপ্রত্যাশিত একটি সমস্যা হয়েছে।", @@ -42,7 +42,7 @@ "bundle_column_error.retry": "আবার চেষ্টা করুন", "bundle_column_error.title": "নেটওয়ার্কের সমস্যা হচ্ছে", "bundle_modal_error.close": "বন্ধ করুন", - "bundle_modal_error.message": "এই অংশটি দেখতে যেয়ে কোনো সমস্যা হয়েছে।", + "bundle_modal_error.message": "এই অংশটি দেখাতে যেয়ে কোনো সমস্যা হয়েছে।", "bundle_modal_error.retry": "আবার চেষ্টা করুন", "column.blocks": "যাদের বন্ধ করে রাখা হয়েছে", "column.community": "স্থানীয় সময়সারি", @@ -77,12 +77,12 @@ "compose_form.poll.remove_option": "এই বিকল্পটি মুছে ফেলুন", "compose_form.publish": "টুট", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করতে", "compose_form.sensitive.marked": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করা হয়েছে", "compose_form.sensitive.unmarked": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করা হয়নি", "compose_form.spoiler.marked": "লেখাটি সাবধানতার পেছনে লুকানো আছে", "compose_form.spoiler.unmarked": "লেখাটি লুকানো নেই", - "compose_form.spoiler_placeholder": "আপনার সাবধানতা এখানে লিখুন", + "compose_form.spoiler_placeholder": "আপনার লেখা দেখার সাবধানবাণী লিখুন", "confirmation_modal.cancel": "বাতিল করুন", "confirmations.block.block_and_report": "বন্ধ করুন এবং রিপোর্ট করুন", "confirmations.block.confirm": "বন্ধ করুন", @@ -99,7 +99,7 @@ "confirmations.redraft.message": "আপনি কি নিশ্চিত এটি মুছে ফেলে এবং আবার সম্পাদন করতে চান ? এটাতে যা পছন্দিত, সমর্থন বা মতামত আছে সেগুলো নতুন লেখার সাথে যুক্ত থাকবে না।", "confirmations.reply.confirm": "মতামত", "confirmations.reply.message": "এখন মতামত লিখতে গেলে আপনার এখন যেটা লিখছেন সেটা মুছে যাবে। আপনি নি নিশ্চিত এটা করতে চান ?", - "confirmations.unfollow.confirm": "অনুসরণ বন্ধ করুন", + "confirmations.unfollow.confirm": "অনুসরণ করা বাতিল করতে", "confirmations.unfollow.message": "আপনি কি নিশ্চিত {name} কে আর অনুসরণ করতে চান না ?", "embed.instructions": "এই লেখাটি আপনার ওয়েবসাইটে যুক্ত করতে নিচের কোডটি বেবহার করুন।", "embed.preview": "সেটা দেখতে এরকম হবে:", @@ -137,11 +137,11 @@ "follow_request.authorize": "অনুমতি দিন", "follow_request.reject": "প্রত্যাখ্যান করুন", "getting_started.developers": "তৈরিকারকদের জন্য", - "getting_started.directory": "নিজস্ব পাতার তালিকা", + "getting_started.directory": "নিজস্ব-পাতাগুলির তালিকা", "getting_started.documentation": "নথিপত্র", "getting_started.heading": "শুরু করা", "getting_started.invite": "অন্যদের আমন্ত্রণ করুন", - "getting_started.open_source_notice": "মাস্টাডন একটি মুক্ত সফটওয়্যার। আপনি তৈরিতে সাহায্য করতে পারেন অথবা সমস্যা রিপোর্ট করতে পারেন গিটহাবে {github}।", + "getting_started.open_source_notice": "মাস্টাডন একটি মুক্ত সফটওয়্যার। তৈরিতে সাহায্য করতে বা কোনো সমস্যা সম্পর্কে জানাতে আমাদের গিটহাবে যেতে পারেন {github}।", "getting_started.security": "নিরাপত্তা", "getting_started.terms": "ব্যবহারের নিয়মাবলী", "hashtag.column_header.tag_mode.all": "এবং {additional}", @@ -152,10 +152,11 @@ "hashtag.column_settings.tag_mode.all": "এগুলো সব", "hashtag.column_settings.tag_mode.any": "এর ভেতরে যেকোনোটা", "hashtag.column_settings.tag_mode.none": "এগুলোর একটাও না", - "hashtag.column_settings.tag_toggle": "আরো ট্যাগ এই কলামে যুক্ত করুন", + "hashtag.column_settings.tag_toggle": "আরো ট্যাগ এই কলামে যুক্ত করতে", "home.column_settings.basic": "সাধারণ", "home.column_settings.show_reblogs": "সমর্থনগুলো দেখান", "home.column_settings.show_replies": "মতামত দেখান", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# ঘটা} other {# ঘটা}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -195,7 +196,7 @@ "keyboard_shortcuts.local": "স্থানীয় সময়রেখাতে যেতে", "keyboard_shortcuts.mention": "লেখককে উল্লেখ করতে", "keyboard_shortcuts.muted": "বন্ধ করা ব্যবহারকারীদের তালিকা খুলতে", - "keyboard_shortcuts.my_profile": "নিজের পাতা দেখতে", + "keyboard_shortcuts.my_profile": "আপনার নিজের পাতা দেখতে", "keyboard_shortcuts.notifications": "প্রজ্ঞাপনের কলাম খুলতে", "keyboard_shortcuts.pinned": "পিন দেওয়া টুটের তালিকা খুলতে", "keyboard_shortcuts.profile": "লেখকের পাতা দেখতে", @@ -204,14 +205,14 @@ "keyboard_shortcuts.search": "খোঁজার অংশে ফোকাস করতে", "keyboard_shortcuts.start": "\"প্রথম শুরুর\" কলাম বের করতে", "keyboard_shortcuts.toggle_hidden": "CW লেখা দেখতে বা লুকাতে", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "ভিডিও/ছবি দেখতে বা বন্ধ করতে", "keyboard_shortcuts.toot": "নতুন একটা টুট লেখা শুরু করতে", "keyboard_shortcuts.unfocus": "লেখা বা খোঁজার জায়গায় ফোকাস না করতে", "keyboard_shortcuts.up": "তালিকার উপরের দিকে যেতে", "lightbox.close": "বন্ধ", "lightbox.next": "পরবর্তী", "lightbox.previous": "পূর্ববর্তী", - "lightbox.view_context": "View context", + "lightbox.view_context": "প্রসঙ্গটি দেখতে", "lists.account.add": "তালিকাতে যুক্ত করতে", "lists.account.remove": "তালিকা থেকে বাদ দিতে", "lists.delete": "তালিকা মুছে ফেলতে", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "তালিকার নতুন শিরোনাম দিতে", "lists.search": "যাদের অনুসরণ করেন তাদের ভেতরে খুঁজুন", "lists.subheading": "আপনার তালিকা", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "আসছে...", "media_gallery.toggle_visible": "দৃশ্যতার অবস্থা বদলান", "missing_indicator.label": "খুঁজে পাওয়া যায়নি", @@ -230,14 +232,14 @@ "navigation_bar.blocks": "বন্ধ করা ব্যবহারকারী", "navigation_bar.community_timeline": "স্থানীয় সময়রেখা", "navigation_bar.compose": "নতুন টুট লিখুন", - "navigation_bar.direct": "সরাসরি লেখা", + "navigation_bar.direct": "সরাসরি লেখাগুলি", "navigation_bar.discover": "ঘুরে দেখুন", "navigation_bar.domain_blocks": "বন্ধ করা ওয়েবসাইট", - "navigation_bar.edit_profile": "নিজের পাতা সম্পাদনা করুন", + "navigation_bar.edit_profile": "নিজের পাতা সম্পাদনা করতে", "navigation_bar.favourites": "পছন্দের", "navigation_bar.filters": "বন্ধ করা শব্দ", "navigation_bar.follow_requests": "অনুসরণের অনুরোধগুলি", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "যাদেরকে অনুসরণ করেন এবং যারা তাকে অনুসরণ করে", "navigation_bar.info": "এই সার্ভার সম্পর্কে", "navigation_bar.keyboard_shortcuts": "হটকীগুলি", "navigation_bar.lists": "তালিকাগুলো", @@ -246,7 +248,7 @@ "navigation_bar.personal": "নিজস্ব", "navigation_bar.pins": "পিন দেওয়া টুট", "navigation_bar.preferences": "পছন্দসমূহ", - "navigation_bar.profile_directory": "Profile directory", + "navigation_bar.profile_directory": "নিজস্ব পাতার তালিকা", "navigation_bar.public_timeline": "যুক্তবিশ্বের সময়রেখা", "navigation_bar.security": "নিরাপত্তা", "notification.favourite": "{name} আপনার কার্যক্রম পছন্দ করেছেন", @@ -256,18 +258,18 @@ "notification.reblog": "{name} আপনার কার্যক্রমে সমর্থন দেখিয়েছেন", "notifications.clear": "প্রজ্ঞাপনগুলো মুছে ফেলতে", "notifications.clear_confirmation": "আপনি কি নির্চিত প্রজ্ঞাপনগুলো মুছে ফেলতে চান ?", - "notifications.column_settings.alert": "কম্পিউটারে প্রজ্ঞাপন", + "notifications.column_settings.alert": "কম্পিউটারে প্রজ্ঞাপনগুলি", "notifications.column_settings.favourite": "পছন্দের:", - "notifications.column_settings.filter_bar.advanced": "সব শ্রেণীগুলো দেখতে", - "notifications.column_settings.filter_bar.category": "দ্রুত ছাঁকনি বার", - "notifications.column_settings.filter_bar.show": "দেখতে", + "notifications.column_settings.filter_bar.advanced": "সব শ্রেণীগুলো দেখানো", + "notifications.column_settings.filter_bar.category": "সংক্ষিপ্ত ছাঁকনি অংশ", + "notifications.column_settings.filter_bar.show": "দেখানো", "notifications.column_settings.follow": "নতুন অনুসরণকারীরা:", "notifications.column_settings.mention": "প্রজ্ঞাপনগুলো:", "notifications.column_settings.poll": "নির্বাচনের ফলাফল:", - "notifications.column_settings.push": "পুশ প্রজ্ঞাপন", + "notifications.column_settings.push": "পুশ প্রজ্ঞাপনগুলি", "notifications.column_settings.reblog": "সমর্থনগুলো:", - "notifications.column_settings.show": "কলামে দেখান", - "notifications.column_settings.sound": "শব্দ বাজাতে", + "notifications.column_settings.show": "কলামে দেখানো", + "notifications.column_settings.sound": "শব্দ বাজানো", "notifications.filter.all": "সব", "notifications.filter.boosts": "সমর্থনগুলো", "notifications.filter.favourites": "পছন্দের গুলো", @@ -276,7 +278,7 @@ "notifications.filter.polls": "নির্বাচনের ফলাফল", "notifications.group": "{count} প্রজ্ঞাপন", "poll.closed": "বন্ধ", - "poll.refresh": "আবার সতেজ করতে", + "poll.refresh": "বদলেছে কিনা দেখতে", "poll.total_votes": "{count, plural, one {# ভোট} other {# ভোট}}", "poll.vote": "ভোট", "poll_button.add_poll": "একটা নির্বাচন যোগ করতে", @@ -314,6 +316,7 @@ "search_results.accounts": "মানুষ", "search_results.hashtags": "হ্যাশট্যাগগুলি", "search_results.statuses": "টুট", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {ফলাফল} other {ফলাফল}}", "status.admin_account": "@{name} র জন্য পরিচালনার ইন্টারফেসে ঢুকুন", "status.admin_status": "যায় লেখাটি পরিচালনার ইন্টারফেসে খুলুন", @@ -323,7 +326,7 @@ "status.copy": "লেখাটির লিংক কপি করতে", "status.delete": "মুছে ফেলতে", "status.detailed_status": "বিস্তারিত কথোপকথনের হিসেবে দেখতে", - "status.direct": "@{name} কে সরাসরি পাঠান", + "status.direct": "@{name} কে সরাসরি লেখা পাঠাতে", "status.embed": "এমবেড করতে", "status.favourite": "পছন্দের করতে", "status.filtered": "ছাঁকনিদিত", @@ -344,7 +347,7 @@ "status.redraft": "মুছে আবার নতুন করে লিখতে", "status.reply": "মতামত জানাতে", "status.replyAll": "লেখাযুক্ত সবার কাছে মতামত জানাতে", - "status.report": "@{name}কে রিপোর্ট করতে", + "status.report": "@{name} কে রিপোর্ট করতে", "status.sensitive_warning": "সংবেদনশীল কিছু", "status.share": "অন্যদের জানান", "status.show_less": "কম দেখতে", @@ -354,7 +357,7 @@ "status.show_thread": "আলোচনা দেখতে", "status.unmute_conversation": "আলোচনার প্রজ্ঞাপন চালু করতে", "status.unpin": "নিজের পাতা থেকে পিন করে রাখাটির পিন খুলতে", - "suggestions.dismiss": "সাহায্যের জন্য পরামর্শগুলো সরাতে", + "suggestions.dismiss": "সাহায্যের পরামর্শগুলো সরাতে", "suggestions.header": "আপনি হয়তোবা এগুলোতে আগ্রহী হতে পারেন…", "tabs_bar.federated_timeline": "যুক্তবিশ্ব", "tabs_bar.home": "বাড়ি", @@ -369,7 +372,7 @@ "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} কথা বলছে", "ui.beforeunload": "যে পর্যন্ত এটা লেখা হয়েছে, মাস্টাডন থেকে চলে গেলে এটা মুছে যাবে।", "upload_area.title": "টেনে এখানে ছেড়ে দিলে এখানে যুক্ত করা যাবে", - "upload_button.label": "ছবি বা ভিডিও যুক্ত করতে (এসব ধরণের JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "ছবি বা ভিডিও যুক্ত করতে (এসব ধরণের: JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "যা যুক্ত করতে চাচ্ছেন সেটি বেশি বড়, এখানকার সর্বাধিকের মেমোরির উপরে চলে গেছে।", "upload_error.poll": "নির্বাচনক্ষেত্রে কোনো ফাইল যুক্ত করা যাবেনা।", "upload_form.description": "যারা দেখতে পায়না তাদের জন্য এটা বর্ণনা করতে", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index bb73b2a419..09f8838e9c 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Bàsic", "home.column_settings.show_reblogs": "Mostrar impulsos", "home.column_settings.show_replies": "Mostrar respostes", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# dia} other {# dies}}", "intervals.full.hours": "{number, plural, one {# hora} other {# hores}}", "intervals.full.minutes": "{number, plural, one {# minut} other {# minuts}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Nova llista", "lists.search": "Cercar entre les persones que segueixes", "lists.subheading": "Les teves llistes", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Carregant...", "media_gallery.toggle_visible": "Alternar visibilitat", "missing_indicator.label": "No trobat", @@ -314,6 +316,7 @@ "search_results.accounts": "Gent", "search_results.hashtags": "Etiquetes", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}", "status.admin_account": "Obre l'interfície de moderació per a @{name}", "status.admin_status": "Obre aquest toot a la interfície de moderació", diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json index fb8ffdd510..7a1ff863b0 100644 --- a/app/javascript/mastodon/locales/co.json +++ b/app/javascript/mastodon/locales/co.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Bàsichi", "home.column_settings.show_reblogs": "Vede e spartere", "home.column_settings.show_replies": "Vede e risposte", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# ghjornu} other {# ghjorni}}", "intervals.full.hours": "{number, plural, one {# ora} other {# ore}}", "intervals.full.minutes": "{number, plural, one {# minuta} other {# minute}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Titulu di a lista", "lists.search": "Circà indè i vostr'abbunamenti", "lists.subheading": "E vo liste", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Caricamentu...", "media_gallery.toggle_visible": "Cambià a visibilità", "missing_indicator.label": "Micca trovu", @@ -314,6 +316,7 @@ "search_results.accounts": "Ghjente", "search_results.hashtags": "Hashtag", "search_results.statuses": "Statuti", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {risultatu} other {risultati}}", "status.admin_account": "Apre l'interfaccia di muderazione per @{name}", "status.admin_status": "Apre stu statutu in l'interfaccia di muderazione", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index f10a3f38b7..020fd35b03 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Základní", "home.column_settings.show_reblogs": "Zobrazit boosty", "home.column_settings.show_replies": "Zobrazit odpovědi", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# den} few {# dny} many {# dne} other {# dní}}", "intervals.full.hours": "{number, plural, one {# hodina} few {# hodiny} many {# hodiny} other {# hodin}}", "intervals.full.minutes": "{number, plural, one {# minuta} few {# minuty} many {# minuty} other {# minut}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Název nového seznamu", "lists.search": "Hledejte mezi lidmi, které sledujete", "lists.subheading": "Vaše seznamy", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Načítám…", "media_gallery.toggle_visible": "Přepínat viditelnost", "missing_indicator.label": "Nenalezeno", @@ -314,6 +316,7 @@ "search_results.accounts": "Lidé", "search_results.hashtags": "Hashtagy", "search_results.statuses": "Tooty", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {výsledek} few {výsledky} many {výsledku} other {výsledků}}", "status.admin_account": "Otevřít moderátorské rozhraní pro uživatele @{name}", "status.admin_status": "Otevřít tento toot v moderátorském rozhraní", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index 4ce5d7ad97..9de3efda8c 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Syml", "home.column_settings.show_reblogs": "Dangos bŵstiau", "home.column_settings.show_replies": "Dangos ymatebion", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# ddydd} other {# o ddyddiau}}", "intervals.full.hours": "{number, plural, one {# awr} other {# o oriau}}", "intervals.full.minutes": "{number, plural, one {# funud} other {# o funudau}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Teitl rhestr newydd", "lists.search": "Chwilio ymysg pobl yr ydych yn ei ddilyn", "lists.subheading": "Eich rhestrau", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Llwytho...", "media_gallery.toggle_visible": "Toglo gwelededd", "missing_indicator.label": "Heb ei ganfod", @@ -314,6 +316,7 @@ "search_results.accounts": "Pobl", "search_results.hashtags": "Hanshnodau", "search_results.statuses": "Tŵtiau", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Agor rhyngwyneb goruwchwylio ar gyfer @{name}", "status.admin_status": "Agor y tŵt yn y rhyngwyneb goruwchwylio", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index ba8ba7a28e..17080c41e3 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Grundlæggende", "home.column_settings.show_reblogs": "Vis fremhævelser", "home.column_settings.show_replies": "Vis svar", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Ny liste titel", "lists.search": "Søg iblandt folk du følger", "lists.subheading": "Dine lister", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Indlæser...", "media_gallery.toggle_visible": "Ændre synlighed", "missing_indicator.label": "Ikke fundet", @@ -314,6 +316,7 @@ "search_results.accounts": "Folk", "search_results.hashtags": "Emnetags", "search_results.statuses": "Trut", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, et {result} andre {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index ac8bc9b9f7..4ae785270e 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Einfach", "home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen", "home.column_settings.show_replies": "Antworten anzeigen", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# Tag} other {# Tage}}", "intervals.full.hours": "{number, plural, one {# Stunde} other {# Stunden}}", "intervals.full.minutes": "{number, plural, one {# Minute} other {# Minuten}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Neuer Titel der Liste", "lists.search": "Suche nach Leuten denen du folgst", "lists.subheading": "Deine Listen", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Wird geladen …", "media_gallery.toggle_visible": "Sichtbarkeit umschalten", "missing_indicator.label": "Nicht gefunden", @@ -314,6 +316,7 @@ "search_results.accounts": "Personen", "search_results.hashtags": "Hashtags", "search_results.statuses": "Beiträge", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {Ergebnis} other {Ergebnisse}}", "status.admin_account": "Öffne Moderationsoberfläche für @{name}", "status.admin_status": "Öffne Beitrag in der Moderationsoberfläche", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 4df299d845..43ce28ec09 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -158,6 +158,15 @@ ], "path": "app/javascript/mastodon/components/load_more.json" }, + { + "descriptors": [ + { + "defaultMessage": "{count, plural, one {# new item} other {# new items}}", + "id": "load_pending" + } + ], + "path": "app/javascript/mastodon/components/load_pending.json" + }, { "descriptors": [ { @@ -735,7 +744,7 @@ { "descriptors": [ { - "defaultMessage": "Media Only", + "defaultMessage": "Media only", "id": "community.column_settings.media_only" } ], @@ -1004,6 +1013,10 @@ "defaultMessage": "Toots", "id": "search_results.statuses" }, + { + "defaultMessage": "Searching toots by their content is not enabled on this Mastodon server.", + "id": "search_results.statuses_fts_disabled" + }, { "defaultMessage": "Hashtags", "id": "search_results.hashtags" @@ -1412,10 +1425,6 @@ }, { "descriptors": [ - { - "defaultMessage": "Basic", - "id": "home.column_settings.basic" - }, { "defaultMessage": "Show boosts", "id": "home.column_settings.show_reblogs" @@ -1797,6 +1806,14 @@ "defaultMessage": "Push notifications", "id": "notifications.column_settings.push" }, + { + "defaultMessage": "Basic", + "id": "home.column_settings.basic" + }, + { + "defaultMessage": "Update in real-time", + "id": "home.column_settings.update_live" + }, { "defaultMessage": "Quick filter bar", "id": "notifications.column_settings.filter_bar.category" diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index e118e427b0..df85c025ff 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Βασικά", "home.column_settings.show_reblogs": "Εμφάνιση προωθήσεων", "home.column_settings.show_replies": "Εμφάνιση απαντήσεων", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# μέρα} other {# μέρες}}", "intervals.full.hours": "{number, plural, one {# ώρα} other {# ώρες}}", "intervals.full.minutes": "{number, plural, one {# λεπτό} other {# λεπτά}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Τίτλος νέας λίστα", "lists.search": "Αναζήτησε μεταξύ των ανθρώπων που ακουλουθείς", "lists.subheading": "Οι λίστες σου", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Φορτώνει...", "media_gallery.toggle_visible": "Εναλλαγή ορατότητας", "missing_indicator.label": "Δε βρέθηκε", @@ -314,6 +316,7 @@ "search_results.accounts": "Άνθρωποι", "search_results.hashtags": "Ταμπέλες", "search_results.statuses": "Τουτ", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, zero {αποτελέσματα} one {αποτέλεσμα} other {αποτελέσματα}}", "status.admin_account": "Άνοιγμα λειτουργίας διαμεσολάβησης για τον/την @{name}", "status.admin_status": "Άνοιγμα αυτής της δημοσίευσης στη λειτουργία διαμεσολάβησης", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 67e1e01f85..7e69e27c6f 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Basic", "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 897cb63530..ddc6942521 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Bazaj agordoj", "home.column_settings.show_reblogs": "Montri diskonigojn", "home.column_settings.show_replies": "Montri respondojn", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# tago} other {# tagoj}}", "intervals.full.hours": "{number, plural, one {# horo} other {# horoj}}", "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutoj}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Titolo de la nova listo", "lists.search": "Serĉi inter la homoj, kiujn vi sekvas", "lists.subheading": "Viaj listoj", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Ŝargado…", "media_gallery.toggle_visible": "Baskuligi videblecon", "missing_indicator.label": "Ne trovita", @@ -314,6 +316,7 @@ "search_results.accounts": "Homoj", "search_results.hashtags": "Kradvortoj", "search_results.statuses": "Mesaĝoj", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {rezulto} other {rezultoj}}", "status.admin_account": "Malfermi la kontrolan interfacon por @{name}", "status.admin_status": "Malfermi ĉi tiun mesaĝon en la kontrola interfaco", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 8fe50ace58..dc42bc7efe 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -98,7 +98,7 @@ "confirmations.redraft.confirm": "Borrar y volver a borrador", "confirmations.redraft.message": "Estás seguro de que quieres borrar este estado y volverlo a borrador? Perderás todas las respuestas, impulsos y favoritos asociados a él, y las respuestas a la publicación original quedarán huérfanos.", "confirmations.reply.confirm": "Responder", - "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "confirmations.reply.message": "Responder sobrescribirá el mensaje que estás escribiendo. ¿Estás seguro de que deseas continuar?", "confirmations.unfollow.confirm": "Dejar de seguir", "confirmations.unfollow.message": "¿Estás seguro de que quieres dejar de seguir a {name}?", "embed.instructions": "Añade este toot a tu sitio web con el siguiente código.", @@ -149,33 +149,34 @@ "hashtag.column_header.tag_mode.none": "sin {additional}", "hashtag.column_settings.select.no_options_message": "No se encontraron sugerencias", "hashtag.column_settings.select.placeholder": "Introduzca hashtags…", - "hashtag.column_settings.tag_mode.all": "All of these", + "hashtag.column_settings.tag_mode.all": "Cualquiera de estos", "hashtag.column_settings.tag_mode.any": "Cualquiera de estos", "hashtag.column_settings.tag_mode.none": "Ninguno de estos", "hashtag.column_settings.tag_toggle": "Include additional tags in this column", "home.column_settings.basic": "Básico", "home.column_settings.show_reblogs": "Mostrar retoots", "home.column_settings.show_replies": "Mostrar respuestas", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "home.column_settings.update_live": "Update in real-time", + "intervals.full.days": "{number, plural, one {# día} other {# días}}", + "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}", + "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}", "introduction.federation.action": "Siguiente", "introduction.federation.federated.headline": "Federado", - "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", + "introduction.federation.federated.text": "Los mensajes públicos de otros servidores del fediverso aparecerán en la cronología federada.", "introduction.federation.home.headline": "Inicio", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", + "introduction.federation.home.text": "Los posts de personas que sigues aparecerán en tu cronología. ¡Puedes seguir a cualquiera en cualquier servidor!", "introduction.federation.local.headline": "Local", - "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", + "introduction.federation.local.text": "Los posts públicos de personas en el mismo servidor que aparecerán en la cronología local.", "introduction.interactions.action": "¡Terminar tutorial!", "introduction.interactions.favourite.headline": "Favorito", - "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", - "introduction.interactions.reblog.headline": "Boost", - "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", + "introduction.interactions.favourite.text": "Puedes guardar un toot para más tarde, y hacer saber al autor que te gustó, dándole a favorito.", + "introduction.interactions.reblog.headline": "Retootear", + "introduction.interactions.reblog.text": "Puedes compartir los toots de otras personas con tus seguidores retooteando los mismos.", "introduction.interactions.reply.headline": "Responder", - "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", + "introduction.interactions.reply.text": "Puedes responder a tus propios toots y los de otras personas, que se encadenarán juntos en una conversación.", "introduction.welcome.action": "¡Vamos!", "introduction.welcome.headline": "Primeros pasos", - "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", + "introduction.welcome.text": "¡Bienvenido al fediverso! En unos momentos, podrás transmitir mensajes y hablar con tus amigos a través de una amplia variedad de servidores. Pero este servidor, {domain}, es especial, alberga tu perfil, así que recuerda su nombre.", "keyboard_shortcuts.back": "volver atrás", "keyboard_shortcuts.blocked": "abrir una lista de usuarios bloqueados", "keyboard_shortcuts.boost": "retootear", @@ -184,7 +185,7 @@ "keyboard_shortcuts.description": "Descripción", "keyboard_shortcuts.direct": "abrir la columna de mensajes directos", "keyboard_shortcuts.down": "mover hacia abajo en la lista", - "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.enter": "abrir estado", "keyboard_shortcuts.favourite": "añadir a favoritos", "keyboard_shortcuts.favourites": "abrir la lista de favoritos", "keyboard_shortcuts.federated": "abrir el timeline federado", @@ -204,7 +205,7 @@ "keyboard_shortcuts.search": "para poner el foco en la búsqueda", "keyboard_shortcuts.start": "abrir la columna \"comenzar\"", "keyboard_shortcuts.toggle_hidden": "mostrar/ocultar texto tras aviso de contenido (CW)", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "mostrar/ocultar medios", "keyboard_shortcuts.toot": "para comenzar un nuevo toot", "keyboard_shortcuts.unfocus": "para retirar el foco de la caja de redacción/búsqueda", "keyboard_shortcuts.up": "para ir hacia arriba en la lista", @@ -216,11 +217,12 @@ "lists.account.remove": "Quitar de lista", "lists.delete": "Borrar lista", "lists.edit": "Editar lista", - "lists.edit.submit": "Change title", + "lists.edit.submit": "Cambiar título", "lists.new.create": "Añadir lista", "lists.new.title_placeholder": "Título de la nueva lista", "lists.search": "Buscar entre la gente a la que sigues", "lists.subheading": "Tus listas", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Cargando…", "media_gallery.toggle_visible": "Cambiar visibilidad", "missing_indicator.label": "No encontrado", @@ -237,7 +239,7 @@ "navigation_bar.favourites": "Favoritos", "navigation_bar.filters": "Palabras silenciadas", "navigation_bar.follow_requests": "Solicitudes para seguirte", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Siguiendo y seguidores", "navigation_bar.info": "Información adicional", "navigation_bar.keyboard_shortcuts": "Atajos", "navigation_bar.lists": "Listas", @@ -246,41 +248,41 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Toots fijados", "navigation_bar.preferences": "Preferencias", - "navigation_bar.profile_directory": "Profile directory", + "navigation_bar.profile_directory": "Directorio de perfiles", "navigation_bar.public_timeline": "Historia federada", "navigation_bar.security": "Seguridad", "notification.favourite": "{name} marcó tu estado como favorito", "notification.follow": "{name} te empezó a seguir", "notification.mention": "{name} te ha mencionado", - "notification.poll": "A poll you have voted in has ended", + "notification.poll": "Una encuesta en la que has votado ha terminado", "notification.reblog": "{name} ha retooteado tu estado", "notifications.clear": "Limpiar notificaciones", "notifications.clear_confirmation": "¿Seguro que quieres limpiar permanentemente todas tus notificaciones?", "notifications.column_settings.alert": "Notificaciones de escritorio", "notifications.column_settings.favourite": "Favoritos:", - "notifications.column_settings.filter_bar.advanced": "Display all categories", - "notifications.column_settings.filter_bar.category": "Quick filter bar", - "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías", + "notifications.column_settings.filter_bar.category": "Barra de filtrado rápido", + "notifications.column_settings.filter_bar.show": "Mostrar", "notifications.column_settings.follow": "Nuevos seguidores:", "notifications.column_settings.mention": "Menciones:", - "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.poll": "Resultados de la votación:", "notifications.column_settings.push": "Notificaciones push", "notifications.column_settings.reblog": "Retoots:", "notifications.column_settings.show": "Mostrar en columna", "notifications.column_settings.sound": "Reproducir sonido", - "notifications.filter.all": "All", - "notifications.filter.boosts": "Boosts", - "notifications.filter.favourites": "Favourites", - "notifications.filter.follows": "Follows", - "notifications.filter.mentions": "Mentions", - "notifications.filter.polls": "Poll results", + "notifications.filter.all": "Todos", + "notifications.filter.boosts": "Retoots", + "notifications.filter.favourites": "Favoritos", + "notifications.filter.follows": "Seguidores", + "notifications.filter.mentions": "Menciones", + "notifications.filter.polls": "Resultados de la votación", "notifications.group": "{count} notificaciones", - "poll.closed": "Closed", - "poll.refresh": "Refresh", - "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", - "poll.vote": "Vote", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", + "poll.closed": "Cerrada", + "poll.refresh": "Actualizar", + "poll.total_votes": "{count, plural, one {# voto} other {# votos}}", + "poll.vote": "Votar", + "poll_button.add_poll": "Añadir una encuesta", + "poll_button.remove_poll": "Eliminar encuesta", "privacy.change": "Ajustar privacidad", "privacy.direct.long": "Sólo mostrar a los usuarios mencionados", "privacy.direct.short": "Directo", @@ -289,7 +291,7 @@ "privacy.public.long": "Mostrar en la historia federada", "privacy.public.short": "Público", "privacy.unlisted.long": "No mostrar en la historia federada", - "privacy.unlisted.short": "Sin federar", + "privacy.unlisted.short": "No listado", "regeneration_indicator.label": "Cargando…", "regeneration_indicator.sublabel": "¡Tu historia de inicio se está preparando!", "relative_time.days": "{number}d", @@ -308,19 +310,20 @@ "search_popout.search_format": "Formato de búsqueda avanzada", "search_popout.tips.full_text": "Búsquedas de texto recuperan posts que has escrito, marcado como favoritos, retooteado o en los que has sido mencionado, así como usuarios, nombres y hashtags.", "search_popout.tips.hashtag": "etiqueta", - "search_popout.tips.status": "status", + "search_popout.tips.status": "estado", "search_popout.tips.text": "El texto simple devuelve correspondencias de nombre, usuario y hashtag", "search_popout.tips.user": "usuario", "search_results.accounts": "Gente", "search_results.hashtags": "Etiquetas", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", - "status.admin_account": "Open moderation interface for @{name}", - "status.admin_status": "Open this status in the moderation interface", - "status.block": "Block @{name}", + "status.admin_account": "Abrir interfaz de moderación para @{name}", + "status.admin_status": "Abrir este estado en la interfaz de moderación", + "status.block": "Bloquear a @{name}", "status.cancel_reblog_private": "Des-impulsar", "status.cannot_reblog": "Este toot no puede retootearse", - "status.copy": "Copy link to status", + "status.copy": "Copiar enlace al estado", "status.delete": "Borrar", "status.detailed_status": "Vista de conversación detallada", "status.direct": "Mensaje directo a @{name}", @@ -336,7 +339,7 @@ "status.open": "Expandir estado", "status.pin": "Fijar", "status.pinned": "Toot fijado", - "status.read_more": "Read more", + "status.read_more": "Leer más", "status.reblog": "Retootear", "status.reblog_private": "Implusar a la audiencia original", "status.reblogged_by": "Retooteado por {name}", @@ -351,27 +354,27 @@ "status.show_less_all": "Mostrar menos para todo", "status.show_more": "Mostrar más", "status.show_more_all": "Mostrar más para todo", - "status.show_thread": "Show thread", + "status.show_thread": "Ver hilo", "status.unmute_conversation": "Dejar de silenciar conversación", "status.unpin": "Dejar de fijar", - "suggestions.dismiss": "Dismiss suggestion", - "suggestions.header": "You might be interested in…", + "suggestions.dismiss": "Descartar sugerencia", + "suggestions.header": "Es posible que te interese…", "tabs_bar.federated_timeline": "Federado", "tabs_bar.home": "Inicio", "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notificaciones", "tabs_bar.search": "Buscar", - "time_remaining.days": "{number, plural, one {# day} other {# days}} left", - "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", - "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", - "time_remaining.moments": "Moments remaining", - "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", - "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "time_remaining.days": "{number, plural, one {# día restante} other {# días restantes}}", + "time_remaining.hours": "{number, plural, one {# hora restante} other {# horas restantes}}", + "time_remaining.minutes": "{number, plural, one {# minuto restante} other {# minutos restantes}}", + "time_remaining.moments": "Momentos restantes", + "time_remaining.seconds": "{number, plural, one {# segundo restante} other {# segundos restantes}}", + "trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {personas}} hablando", "ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.", "upload_area.title": "Arrastra y suelta para subir", "upload_button.label": "Subir multimedia (JPEG, PNG, GIF, WebM, MP4, MOV)", - "upload_error.limit": "File upload limit exceeded.", - "upload_error.poll": "File upload not allowed with polls.", + "upload_error.limit": "Límite de subida de archivos excedido.", + "upload_error.poll": "Subida de archivos no permitida con encuestas.", "upload_form.description": "Describir para los usuarios con dificultad visual", "upload_form.focus": "Recortar", "upload_form.undo": "Borrar", diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json index 3e91012b35..0c078840a9 100644 --- a/app/javascript/mastodon/locales/eu.json +++ b/app/javascript/mastodon/locales/eu.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Oinarrizkoa", "home.column_settings.show_reblogs": "Erakutsi bultzadak", "home.column_settings.show_replies": "Erakutsi erantzunak", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {egun #} other {# egun}}", "intervals.full.hours": "{number, plural, one {ordu #} other {# ordu}}", "intervals.full.minutes": "{number, plural, one {minutu #} other {# minutu}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Zerrenda berriaren izena", "lists.search": "Bilatu jarraitzen dituzun pertsonen artean", "lists.subheading": "Zure zerrendak", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Kargatzen...", "media_gallery.toggle_visible": "Txandakatu ikusgaitasuna", "missing_indicator.label": "Ez aurkitua", @@ -314,6 +316,7 @@ "search_results.accounts": "Jendea", "search_results.hashtags": "Traolak", "search_results.statuses": "Toot-ak", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {emaitza} other {emaitzak}}", "status.admin_account": "Ireki @{name} erabiltzailearen moderazio interfazea", "status.admin_status": "Ireki mezu hau moderazio interfazean", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index 68d231ce95..41143bcc88 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "اصلی", "home.column_settings.show_reblogs": "نمایش بازبوق‌ها", "home.column_settings.show_replies": "نمایش پاسخ‌ها", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# روز} other {# روز}}", "intervals.full.hours": "{number, plural, one {# ساعت} other {# ساعت}}", "intervals.full.minutes": "{number, plural, one {# دقیقه} other {# دقیقه}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "نام فهرست تازه", "lists.search": "بین کسانی که پی می‌گیرید بگردید", "lists.subheading": "فهرست‌های شما", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "بارگیری...", "media_gallery.toggle_visible": "تغییر پیدایی", "missing_indicator.label": "پیدا نشد", @@ -314,6 +316,7 @@ "search_results.accounts": "افراد", "search_results.hashtags": "هشتگ‌ها", "search_results.statuses": "بوق‌ها", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}", "status.admin_account": "محیط مدیریت مربوط به @{name} را باز کن", "status.admin_status": "این نوشته را در محیط مدیریت باز کن", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 342a15bfb0..05495d5d74 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Perusasetukset", "home.column_settings.show_reblogs": "Näytä buustaukset", "home.column_settings.show_replies": "Näytä vastaukset", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "Päivä päiviä", "intervals.full.hours": "Tunti tunteja", "intervals.full.minutes": "Minuuti minuuteja", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Uuden listan nimi", "lists.search": "Etsi seuraamistasi henkilöistä", "lists.subheading": "Omat listat", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Ladataan...", "media_gallery.toggle_visible": "Säädä näkyvyyttä", "missing_indicator.label": "Ei löytynyt", @@ -314,6 +316,7 @@ "search_results.accounts": "Ihmiset", "search_results.hashtags": "Hashtagit", "search_results.statuses": "Tuuttaukset", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 06bb70e028..f4db2e7a1a 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Basique", "home.column_settings.show_reblogs": "Afficher les partages", "home.column_settings.show_replies": "Afficher les réponses", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# jour} other {# jours}}", "intervals.full.hours": "{number, plural, one {# heure} other {# heures}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Titre de la nouvelle liste", "lists.search": "Rechercher parmi les gens que vous suivez", "lists.subheading": "Vos listes", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Chargement…", "media_gallery.toggle_visible": "Modifier la visibilité", "missing_indicator.label": "Non trouvé", @@ -314,6 +316,7 @@ "search_results.accounts": "Comptes", "search_results.hashtags": "Hashtags", "search_results.statuses": "Pouets", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}", "status.admin_account": "Ouvrir l'interface de modération pour @{name}", "status.admin_status": "Ouvrir ce statut dans l'interface de modération", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 9b19d6f113..2605f61f8a 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Básico", "home.column_settings.show_reblogs": "Mostrar repeticións", "home.column_settings.show_replies": "Mostrar respostas", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural,one {# día} other {# días}}", "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}", "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Novo título da lista", "lists.search": "Procurar entre a xente que segues", "lists.subheading": "As túas listas", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Cargando...", "media_gallery.toggle_visible": "Ocultar", "missing_indicator.label": "Non atopado", @@ -314,6 +316,7 @@ "search_results.accounts": "Xente", "search_results.hashtags": "Etiquetas", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count,plural,one {result} outros {results}}", "status.admin_account": "Abrir interface de moderación para @{name}", "status.admin_status": "Abrir este estado na interface de moderación", @@ -369,7 +372,7 @@ "trends.count_by_accounts": "{count} {rawCount, plural, one {person} outras {people}} conversando", "ui.beforeunload": "O borrador perderase se sae de Mastodon.", "upload_area.title": "Arrastre e solte para subir", - "upload_button.label": "Engadir medios (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Engadir medios ({formats})", "upload_error.limit": "Excedeu o límite de subida de ficheiros.", "upload_error.poll": "Non se poden subir ficheiros nas sondaxes.", "upload_form.description": "Describa para deficientes visuais", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 248be3c7b5..99bb87a5f1 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -64,7 +64,7 @@ "column_header.show_settings": "הצגת העדפות", "column_header.unpin": "שחרור קיבוע", "column_subheading.settings": "אפשרויות", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "למתחילים", "home.column_settings.show_reblogs": "הצגת הדהודים", "home.column_settings.show_replies": "הצגת תגובות", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "טוען...", "media_gallery.toggle_visible": "נראה\\בלתי נראה", "missing_indicator.label": "לא נמצא", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {תוצאה} other {תוצאות}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/hi.json b/app/javascript/mastodon/locales/hi.json index ac58514d42..d4d9e5f64b 100644 --- a/app/javascript/mastodon/locales/hi.json +++ b/app/javascript/mastodon/locales/hi.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Basic", "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index 6f9b5343af..273b70d079 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -64,7 +64,7 @@ "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.settings": "Postavke", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Osnovno", "home.column_settings.show_reblogs": "Pokaži boostove", "home.column_settings.show_replies": "Pokaži odgovore", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Učitavam...", "media_gallery.toggle_visible": "Preklopi vidljivost", "missing_indicator.label": "Nije nađen", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 1c3b63d7d2..38d30efe43 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Alapértelmezések", "home.column_settings.show_reblogs": "Megtolások mutatása", "home.column_settings.show_replies": "Válaszok mutatása", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# nap} other {# nap}}", "intervals.full.hours": "{number, plural, one {# óra} other {# óra}}", "intervals.full.minutes": "{number, plural, one {# perc} other {# perc}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Új lista címe", "lists.search": "Keresés a követett személyek között", "lists.subheading": "Listáid", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Betöltés...", "media_gallery.toggle_visible": "Láthatóság állítása", "missing_indicator.label": "Nincs találat", @@ -314,6 +316,7 @@ "search_results.accounts": "Emberek", "search_results.hashtags": "Hashtagek", "search_results.statuses": "Tülkök", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {találat} other {találat}}", "status.admin_account": "Moderáció megnyitása @{name} felhasználóhoz", "status.admin_status": "Tülk megnyitása moderációra", diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json index b2dc16a484..801d34380a 100644 --- a/app/javascript/mastodon/locales/hy.json +++ b/app/javascript/mastodon/locales/hy.json @@ -64,7 +64,7 @@ "column_header.show_settings": "Ցուցադրել կարգավորումները", "column_header.unpin": "Հանել", "column_subheading.settings": "Կարգավորումներ", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "Այս թութը չի հաշվառվի որեւէ պիտակի տակ, քանզի այն ծածուկ է։ Միայն հրապարակային թթերը հնարավոր է որոնել պիտակներով։", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Հիմնական", "home.column_settings.show_reblogs": "Ցուցադրել տարածածները", "home.column_settings.show_replies": "Ցուցադրել պատասխանները", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Նոր ցանկի վերնագիր", "lists.search": "Փնտրել քո հետեւած մարդկանց մեջ", "lists.subheading": "Քո ցանկերը", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Բեռնվում է…", "media_gallery.toggle_visible": "Ցուցադրել/թաքցնել", "missing_indicator.label": "Չգտնվեց", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {արդյունք} other {արդյունք}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index 07ce0eb985..daa87f9557 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -1,5 +1,5 @@ { - "account.add_or_remove_from_list": "Add or Remove from lists", + "account.add_or_remove_from_list": "Tambah atau Hapus dari daftar", "account.badges.bot": "Bot", "account.block": "Blokir @{name}", "account.block_domain": "Sembunyikan segalanya dari {domain}", @@ -7,23 +7,23 @@ "account.direct": "Direct Message @{name}", "account.domain_blocked": "Domain disembunyikan", "account.edit_profile": "Ubah profil", - "account.endorse": "Feature on profile", + "account.endorse": "Tampilkan di profil", "account.follow": "Ikuti", "account.followers": "Pengikut", - "account.followers.empty": "No one follows this user yet.", + "account.followers.empty": "Tidak ada satupun yang mengkuti pengguna ini saat ini.", "account.follows": "Mengikuti", - "account.follows.empty": "This user doesn't follow anyone yet.", + "account.follows.empty": "Pengguna ini belum mengikuti siapapun.", "account.follows_you": "Mengikuti anda", "account.hide_reblogs": "Sembunyikan boosts dari @{name}", - "account.link_verified_on": "Ownership of this link was checked on {date}", - "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.link_verified_on": "Kepemilikan tautan ini telah dicek pada {date}", + "account.locked_info": "Status privasi akun ini disetel untuk dikunci. Pemilik secara manual meninjau siapa yang dapat mengikuti mereka.", "account.media": "Media", "account.mention": "Balasan @{name}", "account.moved_to": "{name} telah pindah ke:", "account.mute": "Bisukan @{name}", "account.mute_notifications": "Sembunyikan notifikasi dari @{name}", "account.muted": "Dibisukan", - "account.posts": "Toots", + "account.posts": "Toot", "account.posts_with_replies": "Postingan dengan balasan", "account.report": "Laporkan @{name}", "account.requested": "Menunggu persetujuan. Klik untuk membatalkan permintaan", @@ -31,23 +31,23 @@ "account.show_reblogs": "Tampilkan boost dari @{name}", "account.unblock": "Hapus blokir @{name}", "account.unblock_domain": "Tampilkan {domain}", - "account.unendorse": "Don't feature on profile", + "account.unendorse": "Jangan tampilkan di profil", "account.unfollow": "Berhenti mengikuti", "account.unmute": "Berhenti membisukan @{name}", "account.unmute_notifications": "Munculkan notifikasi dari @{name}", - "alert.unexpected.message": "An unexpected error occurred.", + "alert.unexpected.message": "Terjadi kesalahan yang tidak terduga.", "alert.unexpected.title": "Oops!", "boost_modal.combo": "Anda dapat menekan {combo} untuk melewati ini", "bundle_column_error.body": "Kesalahan terjadi saat memuat komponen ini.", "bundle_column_error.retry": "Coba lagi", - "bundle_column_error.title": "Network error", + "bundle_column_error.title": "Kesalahan jaringan", "bundle_modal_error.close": "Tutup", "bundle_modal_error.message": "Kesalahan terjadi saat memuat komponen ini.", "bundle_modal_error.retry": "Coba lagi", "column.blocks": "Pengguna diblokir", "column.community": "Linimasa Lokal", - "column.direct": "Direct messages", - "column.domain_blocks": "Hidden domains", + "column.direct": "Pesan langsung", + "column.domain_blocks": "Topik tersembunyi", "column.favourites": "Favorit", "column.follow_requests": "Permintaan mengikuti", "column.home": "Beranda", @@ -64,41 +64,41 @@ "column_header.show_settings": "Tampilkan pengaturan", "column_header.unpin": "Lepaskan", "column_subheading.settings": "Pengaturan", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Hanya media", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", - "compose_form.direct_message_warning_learn_more": "Learn more", + "compose_form.direct_message_warning_learn_more": "Pelajari selengkapnya", "compose_form.hashtag_warning": "Toot ini tidak akan ada dalam daftar tagar manapun karena telah di set sebagai tidak terdaftar. Hanya postingan publik yang bisa dicari dengan tagar.", "compose_form.lock_disclaimer": "Akun anda tidak {locked}. Semua orang dapat mengikuti anda untuk melihat postingan khusus untuk pengikut anda.", "compose_form.lock_disclaimer.lock": "terkunci", "compose_form.placeholder": "Apa yang ada di pikiran anda?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", + "compose_form.poll.add_option": "Tambahkan pilihan", + "compose_form.poll.duration": "Durasi jajak pendapat", + "compose_form.poll.option_placeholder": "Pilihan {number}", + "compose_form.poll.remove_option": "Hapus opsi ini", "compose_form.publish": "Toot", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Tandai sebagai media sensitif", "compose_form.sensitive.marked": "Sumber ini telah ditandai sebagai sumber sensitif.", "compose_form.sensitive.unmarked": "Sumber ini tidak ditandai sebagai sumber sensitif", "compose_form.spoiler.marked": "Teks disembunyikan dibalik peringatan", "compose_form.spoiler.unmarked": "Teks tidak tersembunyi", "compose_form.spoiler_placeholder": "Peringatan konten", "confirmation_modal.cancel": "Batal", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.block_and_report": "Blokir & Laporkan", "confirmations.block.confirm": "Blokir", "confirmations.block.message": "Apa anda yakin ingin memblokir {name}?", "confirmations.delete.confirm": "Hapus", "confirmations.delete.message": "Apa anda yakin untuk menghapus status ini?", - "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.confirm": "Hapus", "confirmations.delete_list.message": "Apakah anda yakin untuk menghapus daftar ini secara permanen?", "confirmations.domain_block.confirm": "Sembunyikan keseluruhan domain", "confirmations.domain_block.message": "Apakah anda benar benar yakin untuk memblokir keseluruhan {domain}? Dalam kasus tertentu beberapa pemblokiran atau penyembunyian lebih baik.", "confirmations.mute.confirm": "Bisukan", "confirmations.mute.message": "Apa anda yakin ingin membisukan {name}?", - "confirmations.redraft.confirm": "Delete & redraft", + "confirmations.redraft.confirm": "Hapus dan konsep ulang", "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.", - "confirmations.reply.confirm": "Reply", - "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "confirmations.reply.confirm": "Balas", + "confirmations.reply.message": "Membalas sekarang akan menimpa pesan yang sedang Anda buat. Anda yakin ingin melanjutkan?", "confirmations.unfollow.confirm": "Berhenti mengikuti", "confirmations.unfollow.message": "Apakah anda ingin berhenti mengikuti {name}?", "embed.instructions": "Sematkan status ini di website anda dengan menyalin kode di bawah ini.", @@ -117,38 +117,38 @@ "emoji_button.search_results": "Hasil pencarian", "emoji_button.symbols": "Simbol", "emoji_button.travel": "Tempat Wisata", - "empty_column.account_timeline": "No toots here!", - "empty_column.account_unavailable": "Profile unavailable", - "empty_column.blocks": "You haven't blocked any users yet.", + "empty_column.account_timeline": "Tidak ada toot di sini!", + "empty_column.account_unavailable": "Profil tidak tersedia", + "empty_column.blocks": "Anda belum memblokir siapapun.", "empty_column.community": "Linimasa lokal masih kosong. Tulis sesuatu secara publik dan buat roda berputar!", - "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", - "empty_column.domain_blocks": "There are no hidden domains yet.", - "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", - "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", - "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", + "empty_column.direct": "Anda belum memiliki pesan langsung. Ketika Anda mengirim atau menerimanya, maka akan muncul di sini.", + "empty_column.domain_blocks": "Tidak ada topik tersembunyi.", + "empty_column.favourited_statuses": "Anda belum memiliki toot favorit. Ketika Anda mengirim atau menerimanya, maka akan muncul di sini.", + "empty_column.favourites": "Tidak ada seorangpun yang memfavoritkan toot ini. Ketika seseorang melakukannya, maka akan muncul disini.", + "empty_column.follow_requests": "Anda belum memiliki permintaan mengikuti. Ketika Anda menerimanya, maka akan muncul disini.", "empty_column.hashtag": "Tidak ada apapun dalam hashtag ini.", "empty_column.home": "Linimasa anda kosong! Kunjungi {public} atau gunakan pencarian untuk memulai dan bertemu pengguna lain.", "empty_column.home.public_timeline": "linimasa publik", "empty_column.list": "Tidak ada postingan di list ini. Ketika anggota dari list ini memposting status baru, status tersebut akan tampil disini.", - "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", - "empty_column.mutes": "You haven't muted any users yet.", + "empty_column.lists": "Anda belum memiliki daftar. Ketika Anda membuatnya, maka akan muncul disini.", + "empty_column.mutes": "Anda belum membisukan siapapun.", "empty_column.notifications": "Anda tidak memiliki notifikasi apapun. Berinteraksi dengan orang lain untuk memulai percakapan.", "empty_column.public": "Tidak ada apapun disini! Tulis sesuatu, atau ikuti pengguna lain dari server lain untuk mengisi ini", "follow_request.authorize": "Izinkan", "follow_request.reject": "Tolak", - "getting_started.developers": "Developers", - "getting_started.directory": "Profile directory", - "getting_started.documentation": "Documentation", + "getting_started.developers": "Pengembang", + "getting_started.directory": "Direktori profil", + "getting_started.documentation": "Dokumentasi", "getting_started.heading": "Mulai", - "getting_started.invite": "Invite people", + "getting_started.invite": "Undang orang", "getting_started.open_source_notice": "Mastodon adalah perangkat lunak yang bersifat terbuka. Anda dapat berkontribusi atau melaporkan permasalahan/bug di Github {github}.", - "getting_started.security": "Security", - "getting_started.terms": "Terms of service", - "hashtag.column_header.tag_mode.all": "and {additional}", - "hashtag.column_header.tag_mode.any": "or {additional}", - "hashtag.column_header.tag_mode.none": "without {additional}", - "hashtag.column_settings.select.no_options_message": "No suggestions found", - "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "getting_started.security": "Keamanan", + "getting_started.terms": "Ketentuan layanan", + "hashtag.column_header.tag_mode.all": "dan {additional}", + "hashtag.column_header.tag_mode.any": "atau {additional}", + "hashtag.column_header.tag_mode.none": "tanpa {additional}", + "hashtag.column_settings.select.no_options_message": "Tidak ada saran yang ditemukan", + "hashtag.column_settings.select.placeholder": "Masukkan tagar…", "hashtag.column_settings.tag_mode.all": "All of these", "hashtag.column_settings.tag_mode.any": "Any of these", "hashtag.column_settings.tag_mode.none": "None of these", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Dasar", "home.column_settings.show_reblogs": "Tampilkan boost", "home.column_settings.show_replies": "Tampilkan balasan", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Tunggu sebentar...", "media_gallery.toggle_visible": "Tampil/Sembunyikan", "missing_indicator.label": "Tidak ditemukan", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {hasil} other {hasil}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index c3f8707d13..864d499955 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -64,7 +64,7 @@ "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.settings": "Settings", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Simpla", "home.column_settings.show_reblogs": "Montrar repeti", "home.column_settings.show_replies": "Montrar respondi", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Kargante...", "media_gallery.toggle_visible": "Chanjar videbleso", "missing_indicator.label": "Ne trovita", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {rezulto} other {rezulti}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index f7e2e43538..7925cef8c1 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -4,7 +4,7 @@ "account.block": "Blocca @{name}", "account.block_domain": "Nascondi tutto da {domain}", "account.blocked": "Bloccato", - "account.direct": "Invia messaggio diretto a @{name}", + "account.direct": "Invia messaggio privato a @{name}", "account.domain_blocked": "Dominio nascosto", "account.edit_profile": "Modifica profilo", "account.endorse": "Metti in evidenza sul profilo", @@ -121,7 +121,7 @@ "empty_column.account_unavailable": "Profilo non disponibile", "empty_column.blocks": "Non hai ancora bloccato nessun utente.", "empty_column.community": "La timeline locale è vuota. Condividi qualcosa pubblicamente per dare inizio alla festa!", - "empty_column.direct": "Non hai ancora nessun messaggio diretto. Quando ne manderai o riceverai qualcuno, apparirà qui.", + "empty_column.direct": "Non hai ancora nessun messaggio privato. Quando ne manderai o riceverai qualcuno, apparirà qui.", "empty_column.domain_blocks": "Non vi sono domini nascosti.", "empty_column.favourited_statuses": "Non hai ancora segnato nessun toot come apprezzato. Quando lo farai, comparirà qui.", "empty_column.favourites": "Nessuno ha ancora segnato questo toot come apprezzato. Quando qualcuno lo farà, apparirà qui.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Semplice", "home.column_settings.show_reblogs": "Mostra post condivisi", "home.column_settings.show_replies": "Mostra risposte", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# giorno} other {# giorni}}", "intervals.full.hours": "{number, plural, one {# ora} other {# ore}}", "intervals.full.minutes": "{number, plural, one {# minuto} other {# minuti}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Titolo della nuova lista", "lists.search": "Cerca tra le persone che segui", "lists.subheading": "Le tue liste", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Caricamento...", "media_gallery.toggle_visible": "Imposta visibilità", "missing_indicator.label": "Non trovato", @@ -283,7 +285,7 @@ "poll_button.remove_poll": "Rimuovi sondaggio", "privacy.change": "Modifica privacy del post", "privacy.direct.long": "Invia solo a utenti menzionati", - "privacy.direct.short": "Diretto", + "privacy.direct.short": "Diretto in privato", "privacy.private.long": "Invia solo ai seguaci", "privacy.private.short": "Privato", "privacy.public.long": "Invia alla timeline pubblica", @@ -314,6 +316,7 @@ "search_results.accounts": "Gente", "search_results.hashtags": "Hashtag", "search_results.statuses": "Toot", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count} {count, plural, one {risultato} other {risultati}}", "status.admin_account": "Apri interfaccia di moderazione per @{name}", "status.admin_status": "Apri questo status nell'interfaccia di moderazione", @@ -323,7 +326,7 @@ "status.copy": "Copia link allo status", "status.delete": "Elimina", "status.detailed_status": "Vista conversazione dettagliata", - "status.direct": "Messaggio diretto @{name}", + "status.direct": "Messaggio privato @{name}", "status.embed": "Incorpora", "status.favourite": "Apprezzato", "status.filtered": "Filtrato", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 83a69e5953..7312ce5ac5 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "基本設定", "home.column_settings.show_reblogs": "ブースト表示", "home.column_settings.show_replies": "返信表示", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number}日", "intervals.full.hours": "{number}時間", "intervals.full.minutes": "{number}分", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "新規リスト名", "lists.search": "フォローしている人の中から検索", "lists.subheading": "あなたのリスト", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "読み込み中...", "media_gallery.toggle_visible": "表示切り替え", "missing_indicator.label": "見つかりません", @@ -314,6 +316,7 @@ "search_results.accounts": "人々", "search_results.hashtags": "ハッシュタグ", "search_results.statuses": "トゥート", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number}件の結果", "status.admin_account": "@{name} のモデレーション画面を開く", "status.admin_status": "このトゥートをモデレーション画面で開く", diff --git a/app/javascript/mastodon/locales/ka.json b/app/javascript/mastodon/locales/ka.json index ff7059aea2..a785434761 100644 --- a/app/javascript/mastodon/locales/ka.json +++ b/app/javascript/mastodon/locales/ka.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "ძირითადი", "home.column_settings.show_reblogs": "ბუსტების ჩვენება", "home.column_settings.show_replies": "პასუხების ჩვენება", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "ახალი სიის სათაური", "lists.search": "ძებნა ადამიანებს შორის რომელთაც მიჰყვებით", "lists.subheading": "თქვენი სიები", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "იტვირთება...", "media_gallery.toggle_visible": "ხილვადობის ჩართვა", "missing_indicator.label": "არაა ნაპოვნი", @@ -314,6 +316,7 @@ "search_results.accounts": "ხალხი", "search_results.hashtags": "ჰეშტეგები", "search_results.statuses": "ტუტები", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/kk.json b/app/javascript/mastodon/locales/kk.json index b9bd7cac31..9514d68a9d 100644 --- a/app/javascript/mastodon/locales/kk.json +++ b/app/javascript/mastodon/locales/kk.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Негізгі", "home.column_settings.show_reblogs": "Бөлісулерді көрсету", "home.column_settings.show_replies": "Жауаптарды көрсету", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# күн} other {# күн}}", "intervals.full.hours": "{number, plural, one {# сағат} other {# сағат}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Жаңа тізім аты", "lists.search": "Сіз іздеген адамдар арасында іздеу", "lists.subheading": "Тізімдеріңіз", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Жүктеу...", "media_gallery.toggle_visible": "Көрінуді қосу", "missing_indicator.label": "Табылмады", @@ -314,6 +316,7 @@ "search_results.accounts": "Адамдар", "search_results.hashtags": "Хэштегтер", "search_results.statuses": "Жазбалар", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "@{name} үшін модерация интерфейсін аш", "status.admin_status": "Бұл жазбаны модерация интерфейсінде аш", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 656a36bce2..e716319387 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "기본 설정", "home.column_settings.show_reblogs": "부스트 표시", "home.column_settings.show_replies": "답글 표시", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number} 일", "intervals.full.hours": "{number} 시간", "intervals.full.minutes": "{number} 분", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "새 리스트의 이름", "lists.search": "팔로우 중인 사람들 중에서 찾기", "lists.subheading": "당신의 리스트", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "불러오는 중...", "media_gallery.toggle_visible": "표시 전환", "missing_indicator.label": "찾을 수 없습니다", @@ -314,6 +316,7 @@ "search_results.accounts": "사람", "search_results.hashtags": "해시태그", "search_results.statuses": "툿", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number}건의 결과", "status.admin_account": "@{name}에 대한 모더레이션 인터페이스 열기", "status.admin_status": "모더레이션 인터페이스에서 이 게시물 열기", @@ -326,7 +329,7 @@ "status.direct": "@{name}에게 다이렉트 메시지", "status.embed": "공유하기", "status.favourite": "즐겨찾기", - "status.filtered": "필터링 됨", + "status.filtered": "필터로 걸러짐", "status.load_more": "더 보기", "status.media_hidden": "미디어 숨겨짐", "status.mention": "답장", diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json index ac58514d42..919129cc57 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -64,7 +64,7 @@ "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.settings": "Settings", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Basic", "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -369,7 +372,7 @@ "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Add media ({formats})", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json index 647e23a691..5328f15c5a 100644 --- a/app/javascript/mastodon/locales/lv.json +++ b/app/javascript/mastodon/locales/lv.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Basic", "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -369,7 +372,7 @@ "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Add media ({formats})", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json index d7c5099636..ad72b32331 100644 --- a/app/javascript/mastodon/locales/ms.json +++ b/app/javascript/mastodon/locales/ms.json @@ -64,7 +64,7 @@ "column_header.show_settings": "Show settings", "column_header.unpin": "Unpin", "column_subheading.settings": "Settings", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Basic", "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "New list title", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading...", "media_gallery.toggle_visible": "Toggle visibility", "missing_indicator.label": "Not found", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", @@ -369,7 +372,7 @@ "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", - "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_button.label": "Add media ({formats})", "upload_error.limit": "File upload limit exceeded.", "upload_error.poll": "File upload not allowed with polls.", "upload_form.description": "Describe for the visually impaired", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index f6504f4bb8..d7f428193c 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Algemeen", "home.column_settings.show_reblogs": "Boosts tonen", "home.column_settings.show_replies": "Reacties tonen", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# dag} other {# dagen}}", "intervals.full.hours": "{number, plural, one {# uur} other {# uur}}", "intervals.full.minutes": "{number, plural, one {# minuut} other {# minuten}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Naam nieuwe lijst", "lists.search": "Zoek naar mensen die je volgt", "lists.subheading": "Jouw lijsten", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Laden…", "media_gallery.toggle_visible": "Media wel/niet tonen", "missing_indicator.label": "Niet gevonden", @@ -314,6 +316,7 @@ "search_results.accounts": "Gebruikers", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}", "status.admin_account": "Moderatie-omgeving van @{name} openen", "status.admin_status": "Deze toot in de moderatie-omgeving openen", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index 2ba8236e27..ea722a01e7 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -64,7 +64,7 @@ "column_header.show_settings": "Vis innstillinger", "column_header.unpin": "Løsne", "column_subheading.settings": "Innstillinger", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "Denne tuten blir ikke listet under noen emneknagger da den er ulistet. Kun offentlige tuter kan søktes etter med emneknagg.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Enkel", "home.column_settings.show_reblogs": "Vis fremhevinger", "home.column_settings.show_replies": "Vis svar", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Ny listetittel", "lists.search": "Søk blant personer du følger", "lists.subheading": "Dine lister", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Laster...", "media_gallery.toggle_visible": "Veksle synlighet", "missing_indicator.label": "Ikke funnet", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {resultat} other {resultater}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 3178f200de..34804da208 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -77,7 +77,7 @@ "compose_form.poll.remove_option": "Levar aquesta opcion", "compose_form.publish": "Tut", "compose_form.publish_loud": "{publish} !", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Marcar coma sensible", "compose_form.sensitive.marked": "Lo mèdia es marcat coma sensible", "compose_form.sensitive.unmarked": "Lo mèdia es pas marcat coma sensible", "compose_form.spoiler.marked": "Lo tèxte es rescondut jos l’avertiment", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Basic", "home.column_settings.show_reblogs": "Mostrar los partatges", "home.column_settings.show_replies": "Mostrar las responsas", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# jorn} other {# jorns}}", "intervals.full.hours": "{number, plural, one {# ora} other {# oras}}", "intervals.full.minutes": "{number, plural, one {# minuta} other {# minutas}}", @@ -204,14 +205,14 @@ "keyboard_shortcuts.search": "anar a la recèrca", "keyboard_shortcuts.start": "dobrir la colomna « Per començar »", "keyboard_shortcuts.toggle_hidden": "mostrar/amagar lo tèxte dels avertiments", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "per mostrar/rescondre los mèdias", "keyboard_shortcuts.toot": "començar un estatut tot novèl", "keyboard_shortcuts.unfocus": "quitar lo camp tèxte/de recèrca", "keyboard_shortcuts.up": "far montar dins la lista", "lightbox.close": "Tampar", "lightbox.next": "Seguent", "lightbox.previous": "Precedent", - "lightbox.view_context": "View context", + "lightbox.view_context": "Veire lo contèxt", "lists.account.add": "Ajustar a la lista", "lists.account.remove": "Levar de la lista", "lists.delete": "Suprimir la lista", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Títol de la nòva lista", "lists.search": "Cercar demest lo monde que seguètz", "lists.subheading": "Vòstras listas", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Cargament…", "media_gallery.toggle_visible": "Modificar la visibilitat", "missing_indicator.label": "Pas trobat", @@ -237,7 +239,7 @@ "navigation_bar.favourites": "Favorits", "navigation_bar.filters": "Mots ignorats", "navigation_bar.follow_requests": "Demandas d’abonament", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Abonament e seguidors", "navigation_bar.info": "Tocant aqueste servidor", "navigation_bar.keyboard_shortcuts": "Acorchis clavièr", "navigation_bar.lists": "Listas", @@ -246,7 +248,7 @@ "navigation_bar.personal": "Personal", "navigation_bar.pins": "Tuts penjats", "navigation_bar.preferences": "Preferéncias", - "navigation_bar.profile_directory": "Profile directory", + "navigation_bar.profile_directory": "Annuari de perfils", "navigation_bar.public_timeline": "Flux public global", "navigation_bar.security": "Seguretat", "notification.favourite": "{name} a ajustat a sos favorits", @@ -314,6 +316,7 @@ "search_results.accounts": "Gents", "search_results.hashtags": "Etiquetas", "search_results.statuses": "Tuts", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}", "status.admin_account": "Dobrir l’interfàcia de moderacion per @{name}", "status.admin_status": "Dobrir aqueste estatut dins l’interfàcia de moderacion", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 44edf6e3dd..fb44f6e10c 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Podstawowe", "home.column_settings.show_reblogs": "Pokazuj podbicia", "home.column_settings.show_replies": "Pokazuj odpowiedzi", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# dzień} few {# dni} many {# dni} other {# dni}}", "intervals.full.hours": "{number, plural, one {# godzina} few {# godziny} many {# godzin} other {# godzin}}", "intervals.full.minutes": "{number, plural, one {# minuta} few {# minuty} many {# minut} other {# minut}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Wprowadź tytuł listy", "lists.search": "Szukaj wśród osób które śledzisz", "lists.subheading": "Twoje listy", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Ładowanie…", "media_gallery.toggle_visible": "Przełącz widoczność", "missing_indicator.label": "Nie znaleziono", @@ -314,6 +316,7 @@ "search_results.accounts": "Ludzie", "search_results.hashtags": "Hashtagi", "search_results.statuses": "Wpisy", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {wynik} few {wyniki} many {wyników} more {wyników}}", "status.admin_account": "Otwórz interfejs moderacyjny dla @{name}", "status.admin_status": "Otwórz ten wpis w interfejsie moderacyjnym", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index dca087af95..1fb700874c 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -36,7 +36,7 @@ "account.unmute": "Não silenciar @{name}", "account.unmute_notifications": "Retirar silêncio das notificações vindas de @{name}", "alert.unexpected.message": "Um erro inesperado ocorreu.", - "alert.unexpected.title": "Oops!", + "alert.unexpected.title": "Eita!", "boost_modal.combo": "Você pode pressionar {combo} para ignorar este diálogo na próxima vez", "bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.", "bundle_column_error.retry": "Tente novamente", @@ -77,7 +77,7 @@ "compose_form.poll.remove_option": "Remover essa opção", "compose_form.publish": "Publicar", "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.hide": "Marcar mídia como sensível", "compose_form.sensitive.marked": "Mídia está marcada como sensível", "compose_form.sensitive.unmarked": "Mídia não está marcada como sensível", "compose_form.spoiler.marked": "O texto está escondido por um aviso de conteúdo", @@ -89,7 +89,7 @@ "confirmations.block.message": "Você tem certeza de que quer bloquear {name}?", "confirmations.delete.confirm": "Excluir", "confirmations.delete.message": "Você tem certeza de que quer excluir esta postagem?", - "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.confirm": "Excluir", "confirmations.delete_list.message": "Você tem certeza que quer deletar permanentemente a lista?", "confirmations.domain_block.confirm": "Esconder o domínio inteiro", "confirmations.domain_block.message": "Você quer mesmo bloquear {domain} inteiro? Na maioria dos casos, silenciar ou bloquear alguns usuários é o suficiente e o recomendado. Você não vai ver conteúdo desse domínio em nenhuma das timelines públicas ou nas suas notificações. Seus seguidores desse domínio serão removidos.", @@ -156,13 +156,14 @@ "home.column_settings.basic": "Básico", "home.column_settings.show_reblogs": "Mostrar compartilhamentos", "home.column_settings.show_replies": "Mostrar as respostas", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# dia} other {# dias}}", "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}", "introduction.federation.action": "Próximo", - "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.headline": "Global", "introduction.federation.federated.text": "Posts públicos de outros servidores do fediverso vão aparecer na timeline global.", - "introduction.federation.home.headline": "Home", + "introduction.federation.home.headline": "Início", "introduction.federation.home.text": "Posts de pessoas que você segue vão aparecer na sua página inicial. Você pode seguir pessoas de qualquer servidor!", "introduction.federation.local.headline": "Local", "introduction.federation.local.text": "Posts públicos de pessoas no mesmo servidor que você vão aparecer na timeline local.", @@ -204,23 +205,24 @@ "keyboard_shortcuts.search": "para focar a pesquisa", "keyboard_shortcuts.start": "para abrir a coluna \"primeiros passos\"", "keyboard_shortcuts.toggle_hidden": "mostrar/esconder o texto com aviso de conteúdo", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toggle_sensitivity": "mostrar/esconder mídia", "keyboard_shortcuts.toot": "para compor um novo toot", "keyboard_shortcuts.unfocus": "para remover o foco da área de composição/pesquisa", "keyboard_shortcuts.up": "para mover para cima na lista", "lightbox.close": "Fechar", "lightbox.next": "Próximo", "lightbox.previous": "Anterior", - "lightbox.view_context": "View context", + "lightbox.view_context": "Ver contexto", "lists.account.add": "Adicionar a listas", "lists.account.remove": "Remover da lista", - "lists.delete": "Delete list", + "lists.delete": "Excluir lista", "lists.edit": "Editar lista", "lists.edit.submit": "Mudar o título", "lists.new.create": "Adicionar lista", "lists.new.title_placeholder": "Novo título da lista", "lists.search": "Procurar entre as pessoas que você segue", "lists.subheading": "Suas listas", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Carregando...", "media_gallery.toggle_visible": "Esconder/Mostrar", "missing_indicator.label": "Não encontrado", @@ -237,7 +239,7 @@ "navigation_bar.favourites": "Favoritos", "navigation_bar.filters": "Palavras silenciadas", "navigation_bar.follow_requests": "Seguidores pendentes", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Seguindo e seguidores", "navigation_bar.info": "Mais informações", "navigation_bar.keyboard_shortcuts": "Atalhos de teclado", "navigation_bar.lists": "Listas", @@ -246,7 +248,7 @@ "navigation_bar.personal": "Pessoal", "navigation_bar.pins": "Postagens fixadas", "navigation_bar.preferences": "Preferências", - "navigation_bar.profile_directory": "Profile directory", + "navigation_bar.profile_directory": "Diretório de perfis", "navigation_bar.public_timeline": "Global", "navigation_bar.security": "Segurança", "notification.favourite": "{name} adicionou a sua postagem aos favoritos", @@ -314,6 +316,7 @@ "search_results.accounts": "Pessoas", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", "status.admin_account": "Abrir interface de moderação para @{name}", "status.admin_status": "Abrir esse status na interface de moderação", diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json index 157090c55f..c6ea3f8479 100644 --- a/app/javascript/mastodon/locales/pt.json +++ b/app/javascript/mastodon/locales/pt.json @@ -17,7 +17,7 @@ "account.hide_reblogs": "Esconder partilhas de @{name}", "account.link_verified_on": "A posse deste link foi verificada em {date}", "account.locked_info": "O estatuto de privacidade desta conta é fechado. O dono revê manualmente que a pode seguir.", - "account.media": "Media", + "account.media": "Média", "account.mention": "Mencionar @{name}", "account.moved_to": "{name} mudou a sua conta para:", "account.mute": "Silenciar @{name}", @@ -49,50 +49,50 @@ "column.direct": "Mensagens directas", "column.domain_blocks": "Domínios escondidos", "column.favourites": "Favoritos", - "column.follow_requests": "Seguidores Pendentes", + "column.follow_requests": "Seguidores pendentes", "column.home": "Início", "column.lists": "Listas", "column.mutes": "Utilizadores silenciados", "column.notifications": "Notificações", "column.pins": "Publicações fixas", - "column.public": "Cronologia federativa", + "column.public": "Cronologia federada", "column_back_button.label": "Voltar", - "column_header.hide_settings": "Esconder preferências", + "column_header.hide_settings": "Esconder configurações", "column_header.moveLeft_settings": "Mover coluna para a esquerda", "column_header.moveRight_settings": "Mover coluna para a direita", "column_header.pin": "Fixar", - "column_header.show_settings": "Mostrar preferências", + "column_header.show_settings": "Mostrar configurações", "column_header.unpin": "Desafixar", - "column_subheading.settings": "Preferências", - "community.column_settings.media_only": "Somente media", - "compose_form.direct_message_warning": "Esta publicação só será enviada para os utilizadores mencionados.", - "compose_form.direct_message_warning_learn_more": "Aprender mais", - "compose_form.hashtag_warning": "Esta pulbicacção não será listada em nenhuma hashtag por ser não listada. Somente publicações públicas podem ser pesquisadas por hashtag.", + "column_subheading.settings": "Configurações", + "community.column_settings.media_only": "Somente multimédia", + "compose_form.direct_message_warning": "Esta publicação será enviada apenas para os utilizadores mencionados.", + "compose_form.direct_message_warning_learn_more": "Conhecer mais", + "compose_form.hashtag_warning": "Este toot não será listado em nenhuma hashtag por ser não listado. Apenas toots públics podem ser pesquisados por hashtag.", "compose_form.lock_disclaimer": "A tua conta não está {locked}. Qualquer pessoa pode seguir-te e ver as publicações direcionadas apenas a seguidores.", - "compose_form.lock_disclaimer.lock": "fechada", + "compose_form.lock_disclaimer.lock": "bloqueado", "compose_form.placeholder": "Em que estás a pensar?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", - "compose_form.publish": "Publicar", - "compose_form.publish_loud": "{publicar}!", - "compose_form.sensitive.hide": "Mark media as sensitive", - "compose_form.sensitive.marked": "Media marcado como sensível", - "compose_form.sensitive.unmarked": "Media não está marcado como sensível", + "compose_form.poll.add_option": "Adicionar uma opção", + "compose_form.poll.duration": "Duração da votação", + "compose_form.poll.option_placeholder": "Opção {number}", + "compose_form.poll.remove_option": "Eliminar esta opção", + "compose_form.publish": "Toot", + "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive.hide": "Marcar multimédia como sensível", + "compose_form.sensitive.marked": "Média marcada como sensível", + "compose_form.sensitive.unmarked": "Média não está marcada como sensível", "compose_form.spoiler.marked": "Texto escondido atrás de aviso", "compose_form.spoiler.unmarked": "O texto não está escondido", "compose_form.spoiler_placeholder": "Escreve o teu aviso aqui", "confirmation_modal.cancel": "Cancelar", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.block_and_report": "Bloquear e denunciar", "confirmations.block.confirm": "Bloquear", "confirmations.block.message": "De certeza que queres bloquear {name}?", "confirmations.delete.confirm": "Eliminar", "confirmations.delete.message": "De certeza que queres eliminar esta publicação?", - "confirmations.delete_list.confirm": "Apagar", - "confirmations.delete_list.message": "Tens a certeza de que desejas apagar permanentemente esta lista?", + "confirmations.delete_list.confirm": "Eliminar", + "confirmations.delete_list.message": "Tens a certeza de que desejas eliminar permanentemente esta lista?", "confirmations.domain_block.confirm": "Esconder tudo deste domínio", - "confirmations.domain_block.message": "De certeza que queres bloquear completamente o domínio {domain}? Na maioria dos casos, silenciar ou bloquear alguns utilizadores é o suficiente e o recomendado. Não irás ver conteúdo daquele domínio em cronologia alguma, nem nas tuas notificações. Os teus seguidores daquele domínio serão removidos.", + "confirmations.domain_block.message": "De certeza que queres bloquear completamente o domínio {domain}? Na maioria dos casos, silenciar ou bloquear alguns utilizadores é suficiente e é o recomendado. Não irás ver conteúdo daquele domínio em cronologia alguma nem nas tuas notificações. Os teus seguidores daquele domínio serão removidos.", "confirmations.mute.confirm": "Silenciar", "confirmations.mute.message": "De certeza que queres silenciar {name}?", "confirmations.redraft.confirm": "Apagar & redigir", @@ -109,23 +109,23 @@ "emoji_button.food": "Comida & Bebida", "emoji_button.label": "Inserir Emoji", "emoji_button.nature": "Natureza", - "emoji_button.not_found": "Não tem emojos!! (╯°□°)╯︵ ┻━┻", + "emoji_button.not_found": "Não tem emojis!! (╯°□°)╯︵ ┻━┻", "emoji_button.objects": "Objectos", "emoji_button.people": "Pessoas", - "emoji_button.recent": "Regularmente utilizados", - "emoji_button.search": "Procurar...", + "emoji_button.recent": "Utilizados regularmente", + "emoji_button.search": "Pesquisar...", "emoji_button.search_results": "Resultados da pesquisa", "emoji_button.symbols": "Símbolos", "emoji_button.travel": "Viagens & Lugares", - "empty_column.account_timeline": "Sem publicações!", - "empty_column.account_unavailable": "Profile unavailable", + "empty_column.account_timeline": "Sem toots por aqui!", + "empty_column.account_unavailable": "Perfil indisponível", "empty_column.blocks": "Ainda não bloqueaste qualquer utilizador.", - "empty_column.community": "Ainda não existe conteúdo local para mostrar!", + "empty_column.community": "A timeline local está vazia. Escreve algo publicamente para começar!", "empty_column.direct": "Ainda não tens qualquer mensagem directa. Quando enviares ou receberes alguma, ela irá aparecer aqui.", "empty_column.domain_blocks": "Ainda não há qualquer domínio escondido.", - "empty_column.favourited_statuses": "Ainda não tens quaisquer publicações favoritas. Quando tiveres alguma, ela irá aparecer aqui.", - "empty_column.favourites": "Ainda ninguém favorizou esta publicação. Quando alguém o fizer, ela irá aparecer aqui.", - "empty_column.follow_requests": "Ainda não tens pedido de seguimento algum. Quando receberes algum, ele irá aparecer aqui.", + "empty_column.favourited_statuses": "Ainda não tens quaisquer toots favoritos. Quando tiveres algum, ele irá aparecer aqui.", + "empty_column.favourites": "Ainda ninguém marcou este toot como favorito. Quando alguém o fizer, ele irá aparecer aqui.", + "empty_column.follow_requests": "Ainda não tens nenhum pedido de seguimento. Quando receberes algum, ele irá aparecer aqui.", "empty_column.hashtag": "Não foram encontradas publicações com essa hashtag.", "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.", "empty_column.home.public_timeline": "Cronologia pública", @@ -138,10 +138,10 @@ "follow_request.reject": "Rejeitar", "getting_started.developers": "Responsáveis pelo desenvolvimento", "getting_started.directory": "Directório de perfil", - "getting_started.documentation": "Documentation", + "getting_started.documentation": "Documentação", "getting_started.heading": "Primeiros passos", "getting_started.invite": "Convidar pessoas", - "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}.", + "getting_started.open_source_notice": "Mastodon é software de código aberto (open source). Podes contribuir ou reportar problemas no GitHub do projecto: {github}.", "getting_started.security": "Segurança", "getting_started.terms": "Termos de serviço", "hashtag.column_header.tag_mode.all": "e {additional}", @@ -154,28 +154,29 @@ "hashtag.column_settings.tag_mode.none": "Nenhum destes", "hashtag.column_settings.tag_toggle": "Incluir etiquetas adicionais para esta coluna", "home.column_settings.basic": "Básico", - "home.column_settings.show_reblogs": "Mostrar as partilhas", - "home.column_settings.show_replies": "Mostrar as respostas", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "home.column_settings.show_reblogs": "Mostrar boosts", + "home.column_settings.show_replies": "Mostrar respostas", + "home.column_settings.update_live": "Update in real-time", + "intervals.full.days": "{number, plural, one {# dia} other {# dias}}", + "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}", + "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}", "introduction.federation.action": "Seguinte", - "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.headline": "Federada", "introduction.federation.federated.text": "Publicações públicas de outros servidores do fediverse aparecerão na cronologia federativa.", - "introduction.federation.home.headline": "Home", + "introduction.federation.home.headline": "Início", "introduction.federation.home.text": "As publicações das pessoas que tu segues aparecerão na tua coluna inicial. Tu podes seguir qualquer pessoa em qualquer servidor!", "introduction.federation.local.headline": "Local", "introduction.federation.local.text": "Publicações públicas de pessoas que tu segues no teu servidor aparecerão na coluna local.", "introduction.interactions.action": "Terminar o tutorial!", "introduction.interactions.favourite.headline": "Favorito", - "introduction.interactions.favourite.text": "Tu podes guardar um toot para depois e deixar o autor saber que gostaste dele, favoritando-o.", - "introduction.interactions.reblog.headline": "Partilhar", + "introduction.interactions.favourite.text": "Podes guardar um toot para depois e deixar o autor saber que gostaste dele, marcando-o como favorito.", + "introduction.interactions.reblog.headline": "Boost", "introduction.interactions.reblog.text": "Podes partilhar os toots de outras pessoas com os teus seguidores partilhando-os.", "introduction.interactions.reply.headline": "Responder", "introduction.interactions.reply.text": "Tu podes responder a toots de outras pessoas e aos teus, o que os irá juntar numa conversa.", "introduction.welcome.action": "Vamos!", "introduction.welcome.headline": "Primeiros passos", - "introduction.welcome.text": "Bem-vindo ao fediverse! Em pouco tempo poderás enviar mensagens e falar com os teus amigos numa grande variedade de servidores. Mas este servidor, {domain}, é especial—ele alberga o teu perfil. Por isso, lembra-te do seu nome.", + "introduction.welcome.text": "Bem-vindo ao fediverso! Em pouco tempo poderás enviar mensagens e falar com os teus amigos numa grande variedade de servidores. Mas este servidor, {domain}, é especial—ele alberga o teu perfil. Por isso, lembra-te do seu nome.", "keyboard_shortcuts.back": "para voltar", "keyboard_shortcuts.blocked": "para abrir a lista de utilizadores bloqueados", "keyboard_shortcuts.boost": "para partilhar", @@ -184,10 +185,10 @@ "keyboard_shortcuts.description": "Descrição", "keyboard_shortcuts.direct": "para abrir a coluna das mensagens directas", "keyboard_shortcuts.down": "para mover para baixo na lista", - "keyboard_shortcuts.enter": "para expandir uma publicação", + "keyboard_shortcuts.enter": "para expandir um estado", "keyboard_shortcuts.favourite": "para adicionar aos favoritos", "keyboard_shortcuts.favourites": "para abrir a lista dos favoritos", - "keyboard_shortcuts.federated": "para abrir a cronologia federativa", + "keyboard_shortcuts.federated": "para abrir a cronologia federada", "keyboard_shortcuts.heading": "Atalhos do teclado", "keyboard_shortcuts.home": "para abrir a cronologia inicial", "keyboard_shortcuts.hotkey": "Atalho", @@ -204,31 +205,32 @@ "keyboard_shortcuts.search": "para focar na pesquisa", "keyboard_shortcuts.start": "para abrir a coluna dos \"primeiros passos\"", "keyboard_shortcuts.toggle_hidden": "para mostrar/esconder texto atrás de CW", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", - "keyboard_shortcuts.toot": "para compor um novo post", - "keyboard_shortcuts.unfocus": "para remover o foco da área de publicação/pesquisa", + "keyboard_shortcuts.toggle_sensitivity": "mostrar/ocultar média", + "keyboard_shortcuts.toot": "para compor um novo toot", + "keyboard_shortcuts.unfocus": "para remover o foco da área de texto/pesquisa", "keyboard_shortcuts.up": "para mover para cima na lista", "lightbox.close": "Fechar", "lightbox.next": "Próximo", "lightbox.previous": "Anterior", - "lightbox.view_context": "View context", + "lightbox.view_context": "Ver contexto", "lists.account.add": "Adicionar à lista", "lists.account.remove": "Remover da lista", - "lists.delete": "Delete list", + "lists.delete": "Remover lista", "lists.edit": "Editar lista", "lists.edit.submit": "Mudar o título", "lists.new.create": "Adicionar lista", - "lists.new.title_placeholder": "Novo título da lista", + "lists.new.title_placeholder": "Título da nova lista", "lists.search": "Pesquisa entre as pessoas que segues", "lists.subheading": "As tuas listas", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "A carregar...", - "media_gallery.toggle_visible": "Esconder/Mostrar", + "media_gallery.toggle_visible": "Mostrar/ocultar", "missing_indicator.label": "Não encontrado", "missing_indicator.sublabel": "Este recurso não foi encontrado", "mute_modal.hide_notifications": "Esconder notificações deste utilizador?", "navigation_bar.apps": "Aplicações móveis", "navigation_bar.blocks": "Utilizadores bloqueados", - "navigation_bar.community_timeline": "Local", + "navigation_bar.community_timeline": "Cronologia local", "navigation_bar.compose": "Escrever novo toot", "navigation_bar.direct": "Mensagens directas", "navigation_bar.discover": "Descobrir", @@ -237,23 +239,23 @@ "navigation_bar.favourites": "Favoritos", "navigation_bar.filters": "Palavras silenciadas", "navigation_bar.follow_requests": "Seguidores pendentes", - "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.follows_and_followers": "Seguindo e seguidores", "navigation_bar.info": "Sobre este servidor", "navigation_bar.keyboard_shortcuts": "Atalhos de teclado", "navigation_bar.lists": "Listas", "navigation_bar.logout": "Sair", "navigation_bar.mutes": "Utilizadores silenciados", - "navigation_bar.personal": "Personal", - "navigation_bar.pins": "Posts fixos", + "navigation_bar.personal": "Pessoal", + "navigation_bar.pins": "Toots afixados", "navigation_bar.preferences": "Preferências", - "navigation_bar.profile_directory": "Profile directory", - "navigation_bar.public_timeline": "Global", + "navigation_bar.profile_directory": "Directório de perfis", + "navigation_bar.public_timeline": "Cronologia federada", "navigation_bar.security": "Segurança", - "notification.favourite": "{name} adicionou o teu post aos favoritos", - "notification.follow": "{name} seguiu-te", + "notification.favourite": "{name} adicionou o teu estado aos favoritos", + "notification.follow": "{name} começou a seguir-te", "notification.mention": "{name} mencionou-te", - "notification.poll": "A poll you have voted in has ended", - "notification.reblog": "{name} partilhou o teu post", + "notification.poll": "Uma votação em participaste chegou ao fim", + "notification.reblog": "{name} fez boost ao teu o teu estado", "notifications.clear": "Limpar notificações", "notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?", "notifications.column_settings.alert": "Notificações no computador", @@ -263,24 +265,24 @@ "notifications.column_settings.filter_bar.show": "Mostrar", "notifications.column_settings.follow": "Novos seguidores:", "notifications.column_settings.mention": "Menções:", - "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.poll": "Resultados da votação:", "notifications.column_settings.push": "Notificações Push", - "notifications.column_settings.reblog": "Partilhas:", - "notifications.column_settings.show": "Mostrar nas colunas", + "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.show": "Mostrar na coluna", "notifications.column_settings.sound": "Reproduzir som", "notifications.filter.all": "Todas", - "notifications.filter.boosts": "Partilhas", - "notifications.filter.favourites": "Favoritas", + "notifications.filter.boosts": "Boosts", + "notifications.filter.favourites": "Favoritos", "notifications.filter.follows": "Seguimento", "notifications.filter.mentions": "Referências", - "notifications.filter.polls": "Poll results", + "notifications.filter.polls": "Resultados da votação", "notifications.group": "{count} notificações", "poll.closed": "Fechado", "poll.refresh": "Recarregar", "poll.total_votes": "{contar, plural, um {# vote} outro {# votes}}", "poll.vote": "Votar", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", + "poll_button.add_poll": "Adicionar votação", + "poll_button.remove_poll": "Remover votação", "privacy.change": "Ajustar a privacidade da mensagem", "privacy.direct.long": "Apenas para utilizadores mencionados", "privacy.direct.short": "Directo", @@ -300,26 +302,27 @@ "reply_indicator.cancel": "Cancelar", "report.forward": "Reenviar para {target}", "report.forward_hint": "A conta é de outro servidor. Enviar uma cópia anónima do relatório para lá também?", - "report.hint": "O relatório será enviado para os moderadores do teu servidor. Podes fornecer, em baixo, uma explicação do motivo pelo qual estás a relatar esta conta:", + "report.hint": "O relatório será enviado para os moderadores do teu servidor. Podes fornecer, em baixo, uma explicação do motivo pelo qual estás a denunciar esta conta:", "report.placeholder": "Comentários adicionais", "report.submit": "Enviar", "report.target": "Denunciar", "search.placeholder": "Pesquisar", "search_popout.search_format": "Formato avançado de pesquisa", - "search_popout.tips.full_text": "Texto simples devolve publicações que tu escreveste, favoritaste, partilhaste ou em que foste mencionado, tal como nomes de utilizador correspondentes, alcunhas e hashtags.", + "search_popout.tips.full_text": "Texto simples devolve publicações que tu escreveste, marcaste como favorita, partilhaste ou em que foste mencionado, tal como nomes de utilizador correspondentes, alcunhas e hashtags.", "search_popout.tips.hashtag": "hashtag", "search_popout.tips.status": "estado", "search_popout.tips.text": "O texto simples retorna a correspondência de nomes, utilizadores e hashtags", "search_popout.tips.user": "utilizador", "search_results.accounts": "Pessoas", "search_results.hashtags": "Hashtags", - "search_results.statuses": "Publicações", + "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", "status.admin_account": "Abrir a interface de moderação para @{name}", "status.admin_status": "Abrir esta publicação na interface de moderação", - "status.block": "Block @{name}", - "status.cancel_reblog_private": "Não partilhar", - "status.cannot_reblog": "Este post não pode ser partilhado", + "status.block": "Bloquear @{name}", + "status.cancel_reblog_private": "Remover boost", + "status.cannot_reblog": "Não é possível fazer boost a esta publicação", "status.copy": "Copiar o link para a publicação", "status.delete": "Eliminar", "status.detailed_status": "Vista de conversação detalhada", @@ -328,7 +331,7 @@ "status.favourite": "Adicionar aos favoritos", "status.filtered": "Filtrada", "status.load_more": "Carregar mais", - "status.media_hidden": "Media escondida", + "status.media_hidden": "Média escondida", "status.mention": "Mencionar @{name}", "status.more": "Mais", "status.mute": "Silenciar @{name}", @@ -338,15 +341,15 @@ "status.pinned": "Publicação fixa", "status.read_more": "Ler mais", "status.reblog": "Partilhar", - "status.reblog_private": "Partilhar com a audiência original", - "status.reblogged_by": "{name} partilhou", - "status.reblogs.empty": "Ainda ninguém partilhou esta publicação. Quando alguém o fizer, ela irá aparecer aqui.", + "status.reblog_private": "Fazer boost com a audiência original", + "status.reblogged_by": "{name} fez boost", + "status.reblogs.empty": "Ainda ninguém fez boost a este toot. Quando alguém o fizer, ele irá aparecer aqui.", "status.redraft": "Apagar & reescrever", "status.reply": "Responder", "status.replyAll": "Responder à conversa", "status.report": "Denunciar @{name}", "status.sensitive_warning": "Conteúdo sensível", - "status.share": "Compartilhar", + "status.share": "Partilhar", "status.show_less": "Mostrar menos", "status.show_less_all": "Mostrar menos para todas", "status.show_more": "Mostrar mais", @@ -356,22 +359,22 @@ "status.unpin": "Não fixar no perfil", "suggestions.dismiss": "Dispensar a sugestão", "suggestions.header": "Tu podes estar interessado em…", - "tabs_bar.federated_timeline": "Global", - "tabs_bar.home": "Home", + "tabs_bar.federated_timeline": "Federada", + "tabs_bar.home": "Início", "tabs_bar.local_timeline": "Local", "tabs_bar.notifications": "Notificações", "tabs_bar.search": "Pesquisar", "time_remaining.days": "{número, plural, um {# day} outro {# days}} faltam", "time_remaining.hours": "{número, plural, um {# hour} outro {# hours}} faltam", "time_remaining.minutes": "{número, plural, um {# minute} outro {# minutes}} faltam", - "time_remaining.moments": "Momentos em falta", + "time_remaining.moments": "Momentos restantes", "time_remaining.seconds": "{número, plural, um {# second} outro {# seconds}} faltam", "trends.count_by_accounts": "{count} {rawCount, plural, uma {person} outra {people}} a falar", - "ui.beforeunload": "O teu rascunho vai ser perdido se abandonares o Mastodon.", + "ui.beforeunload": "O teu rascunho será perdido se abandonares o Mastodon.", "upload_area.title": "Arraste e solte para enviar", "upload_button.label": "Adicionar media", "upload_error.limit": "Limite máximo do ficheiro a carregar excedido.", - "upload_error.poll": "File upload not allowed with polls.", + "upload_error.poll": "Carregamento de ficheiros não é permitido em votações.", "upload_form.description": "Descrição da imagem para pessoas com dificuldades visuais", "upload_form.focus": "Alterar previsualização", "upload_form.undo": "Apagar", @@ -379,7 +382,7 @@ "video.close": "Fechar vídeo", "video.exit_fullscreen": "Sair de full screen", "video.expand": "Expandir vídeo", - "video.fullscreen": "Full screen", + "video.fullscreen": "Ecrã completo", "video.hide": "Esconder vídeo", "video.mute": "Silenciar", "video.pause": "Pausar", diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json index dcb7a088df..ac10d46787 100644 --- a/app/javascript/mastodon/locales/ro.json +++ b/app/javascript/mastodon/locales/ro.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "De bază", "home.column_settings.show_reblogs": "Arată redistribuirile", "home.column_settings.show_replies": "Arată răspunsurile", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Titlu pentru noua listă", "lists.search": "Caută printre persoanale pe care le urmărești", "lists.subheading": "Listele tale", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Încărcare...", "media_gallery.toggle_visible": "Comutați vizibilitatea", "missing_indicator.label": "Nu a fost găsit", @@ -314,6 +316,7 @@ "search_results.accounts": "Oameni", "search_results.hashtags": "Hashtaguri", "search_results.statuses": "Postări", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index d720b62729..8a7a39a069 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -13,20 +13,20 @@ "account.followers.empty": "Никто не подписан на этого пользователя.", "account.follows": "Подписки", "account.follows.empty": "Этот пользователь ни на кого не подписан.", - "account.follows_you": "Подписан(а) на Вас", + "account.follows_you": "Подписан(а) на вас", "account.hide_reblogs": "Скрыть реблоги от @{name}", "account.link_verified_on": "Владение этой ссылкой было проверено {date}", "account.locked_info": "Это закрытый аккаунт. Его владелец вручную одобряет подписчиков.", "account.media": "Медиа", "account.mention": "Упомянуть", "account.moved_to": "Ищите {name} здесь:", - "account.mute": "Заглушить", + "account.mute": "Скрыть @{name}", "account.mute_notifications": "Скрыть уведомления от @{name}", - "account.muted": "Приглушён", + "account.muted": "Скрыт", "account.posts": "Посты", - "account.posts_with_replies": "Посты и ответы", + "account.posts_with_replies": "Посты с ответами", "account.report": "Пожаловаться", - "account.requested": "Ожидает подтверждения", + "account.requested": "Ожидает подтверждения. Нажмите для отмены", "account.share": "Поделиться профилем @{name}", "account.show_reblogs": "Показывать продвижения от @{name}", "account.unblock": "Разблокировать", @@ -52,7 +52,7 @@ "column.follow_requests": "Запросы на подписку", "column.home": "Главная", "column.lists": "Списки", - "column.mutes": "Список глушения", + "column.mutes": "Список скрытых пользователей", "column.notifications": "Уведомления", "column.pins": "Закреплённый пост", "column.public": "Глобальная лента", @@ -70,12 +70,12 @@ "compose_form.hashtag_warning": "Этот пост не будет показывается в поиске по хэштегу, т.к. он непубличный. Только публичные посты можно найти в поиске по хэштегу.", "compose_form.lock_disclaimer": "Ваш аккаунт не {locked}. Любой человек может подписаться на Вас и просматривать посты для подписчиков.", "compose_form.lock_disclaimer.lock": "закрыт", - "compose_form.placeholder": "О чем Вы думаете?", + "compose_form.placeholder": "О чем вы думаете?", "compose_form.poll.add_option": "Добавить", "compose_form.poll.duration": "Длительность опроса", "compose_form.poll.option_placeholder": "Вариант {number}", "compose_form.poll.remove_option": "Удалить этот вариант", - "compose_form.publish": "Трубить", + "compose_form.publish": "Запостить", "compose_form.publish_loud": "{publish}!", "compose_form.sensitive.hide": "Пометить медиафайл как чувствительный", "compose_form.sensitive.marked": "Медиафайлы не отмечены как чувствительные", @@ -117,31 +117,31 @@ "emoji_button.search_results": "Результаты поиска", "emoji_button.symbols": "Символы", "emoji_button.travel": "Путешествия", - "empty_column.account_timeline": "Статусов нет!", + "empty_column.account_timeline": "Здесь нет постов!", "empty_column.account_unavailable": "Профиль недоступен", "empty_column.blocks": "Вы ещё никого не заблокировали.", "empty_column.community": "Локальная лента пуста. Напишите что-нибудь, чтобы разогреть народ!", - "empty_column.direct": "У Вас пока нет личных сообщений. Когда Вы начнёте их отправлять или получать, они появятся здесь.", + "empty_column.direct": "У вас пока нет личных сообщений. Как только вы отправите или получите одно, оно появится здесь.", "empty_column.domain_blocks": "Скрытых доменов пока нет.", - "empty_column.favourited_statuses": "Вы не добавили ни одного статуса в 'Избранное'. Как только Вы это сделаете, они появятся здесь.", - "empty_column.favourites": "Никто ещё не добавил этот статус в 'Избранное'. Как только кто-то это сделает, они появятся здесь.", + "empty_column.favourited_statuses": "Вы не добавили ни один пост в «Избранное». Как только вы это сделаете, он появится здесь.", + "empty_column.favourites": "Никто ещё не добавил этот пост в «Избранное». Как только кто-то это сделает, это отобразится здесь.", "empty_column.follow_requests": "Вам ещё не приходили запросы на подписку. Все новые запросы будут показаны здесь.", "empty_column.hashtag": "Статусов с таким хэштегом еще не существует.", - "empty_column.home": "Пока Вы ни на кого не подписаны. Полистайте {public} или используйте поиск, чтобы освоиться и завести новые знакомства.", + "empty_column.home": "Пока вы ни на кого не подписаны. Полистайте {public} или используйте поиск, чтобы освоиться и завести новые знакомства.", "empty_column.home.public_timeline": "публичные ленты", "empty_column.list": "В этом списке пока ничего нет.", - "empty_column.lists": "У Вас ещё нет списков. Все созданные Вами списки будут показаны здесь.", - "empty_column.mutes": "Вы ещё никого не заглушили.", - "empty_column.notifications": "У Вас еще нет уведомлений. Заведите знакомство с другими пользователями, чтобы начать разговор.", + "empty_column.lists": "У вас ещё нет списков. Созданные вами списки будут показаны здесь.", + "empty_column.mutes": "Вы ещё никого не скрывали.", + "empty_column.notifications": "У вас пока нет уведомлений. Взаимодействуйте с другими, чтобы завести разговор.", "empty_column.public": "Здесь ничего нет! Опубликуйте что-нибудь или подпишитесь на пользователей с других узлов, чтобы заполнить ленту.", "follow_request.authorize": "Авторизовать", "follow_request.reject": "Отказать", - "getting_started.developers": "Для разработчиков", + "getting_started.developers": "Разработчикам", "getting_started.directory": "Каталог профилей", "getting_started.documentation": "Документация", "getting_started.heading": "Добро пожаловать", "getting_started.invite": "Пригласить людей", - "getting_started.open_source_notice": "Mastodon - сервис с открытым исходным кодом. Вы можете помочь проекту или сообщить о проблемах на GitHub по адресу {github}.", + "getting_started.open_source_notice": "Mastodon — сервис с открытым исходным кодом. Вы можете внести вклад или сообщить о проблемах на GitHub: {github}.", "getting_started.security": "Безопасность", "getting_started.terms": "Условия использования", "hashtag.column_header.tag_mode.all": "и {additional}", @@ -156,9 +156,10 @@ "home.column_settings.basic": "Основные", "home.column_settings.show_reblogs": "Показывать продвижения", "home.column_settings.show_replies": "Показывать ответы", - "intervals.full.days": "{number, plural, one {# день} few {# дня} many {# дней} other {# дней}}", - "intervals.full.hours": "{number, plural, one {# час} few {# часа} many {# часов} other {# часов}}", - "intervals.full.minutes": "{number, plural, one {# минута} few {# минуты} many {# минут} other {# минут}}", + "home.column_settings.update_live": "Update in real-time", + "intervals.full.days": "{number, plural, one {# день} few {# дня} other {# дней}}", + "intervals.full.hours": "{number, plural, one {# час} few {# часа} other {# часов}}", + "intervals.full.minutes": "{number, plural, one {# минута} few {# минуты} other {# минут}}", "introduction.federation.action": "Далее", "introduction.federation.federated.headline": "Глобальная лента", "introduction.federation.federated.text": "Публичные статусы с других серверов федеративной сети расположатся в глобальной ленте.", @@ -167,7 +168,7 @@ "introduction.federation.local.headline": "Локальная лента", "introduction.federation.local.text": "Публичные статусы от людей с того же сервера, что и вы, будут отображены в локальной ленте.", "introduction.interactions.action": "Завершить обучение", - "introduction.interactions.favourite.headline": "Отметки \"нравится\"", + "introduction.interactions.favourite.headline": "Отметки «нравится»", "introduction.interactions.favourite.text": "Вы можете отметить статус, чтобы вернуться к нему позже и дать знать автору, что запись вам понравилась, поставив отметку \"нравится\".", "introduction.interactions.reblog.headline": "Продвижения", "introduction.interactions.reblog.text": "Вы можете делиться статусами других людей, продвигая их в своём аккаунте.", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Заголовок списка", "lists.search": "Искать из ваших подписок", "lists.subheading": "Ваши списки", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Загрузка...", "media_gallery.toggle_visible": "Показать/скрыть", "missing_indicator.label": "Не найдено", @@ -242,7 +244,7 @@ "navigation_bar.keyboard_shortcuts": "Сочетания клавиш", "navigation_bar.lists": "Списки", "navigation_bar.logout": "Выйти", - "navigation_bar.mutes": "Список глушения", + "navigation_bar.mutes": "Список скрытых пользователей", "navigation_bar.personal": "Личное", "navigation_bar.pins": "Закреплённые посты", "navigation_bar.preferences": "Опции", @@ -314,6 +316,7 @@ "search_results.accounts": "Люди", "search_results.hashtags": "Хэштеги", "search_results.statuses": "Посты", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {результат} few {результата} many {результатов} other {результатов}}", "status.admin_account": "Открыть интерфейс модератора для @{name}", "status.admin_status": "Открыть этот статус в интерфейсе модератора", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index 18993af970..3cc2cbaa7f 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Základné", "home.column_settings.show_reblogs": "Zobraziť povýšené", "home.column_settings.show_replies": "Ukázať odpovede", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# deň} few {# dní} many {# dní} other {# dni}}", "intervals.full.hours": "{number, plural, one {# hodina} few {# hodín} many {# hodín} other {# hodiny}}", "intervals.full.minutes": "{number, plural, one {# minúta} few {# minút} many {# minút} other {# minúty}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Názov nového zoznamu", "lists.search": "Vyhľadávaj medzi užívateľmi, ktorých sleduješ", "lists.subheading": "Tvoje zoznamy", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Načítam...", "media_gallery.toggle_visible": "Zapni/Vypni viditeľnosť", "missing_indicator.label": "Nenájdené", @@ -254,7 +256,7 @@ "notification.mention": "{name} ťa spomenul/a", "notification.poll": "Anketa v ktorej si hlasoval/a sa skončila", "notification.reblog": "{name} zdieľal/a tvoj príspevok", - "notifications.clear": "Vyčistiť zoznam oboznámení", + "notifications.clear": "Vyčisti oboznámenia", "notifications.clear_confirmation": "Naozaj chceš nenávratne prečistiť všetky tvoje oboznámenia?", "notifications.column_settings.alert": "Oboznámenia na ploche", "notifications.column_settings.favourite": "Obľúbené:", @@ -314,6 +316,7 @@ "search_results.accounts": "Ľudia", "search_results.hashtags": "Haštagy", "search_results.statuses": "Príspevky", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {výsledok} many {výsledkov} other {výsledky}}", "status.admin_account": "Otvor moderovacie rozhranie užívateľa @{name}", "status.admin_status": "Otvor tento príspevok v moderovacom rozhraní", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index 51794a8625..f79a7051ae 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Osnovno", "home.column_settings.show_reblogs": "Pokaži spodbude", "home.column_settings.show_replies": "Pokaži odgovore", + "home.column_settings.update_live": "Update in real-time", "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}}", @@ -167,7 +168,7 @@ "introduction.federation.local.headline": "Lokalno", "introduction.federation.local.text": "Javne objave ljudi na istem strežniku, se bodo prikazale na lokalni časovnici.", "introduction.interactions.action": "Zaključi vadnico!", - "introduction.interactions.favourite.headline": "Priljubljeni", + "introduction.interactions.favourite.headline": "Vzljubi", "introduction.interactions.favourite.text": "Tut lahko shranite za pozneje in ga vzljubite ter s tem pokažete avtorju, da vam je ta tut priljubljen.", "introduction.interactions.reblog.headline": "Spodbudi", "introduction.interactions.reblog.text": "Tute drugih ljudi lahko delite z vašimi sledilci, tako da spodbudite tute.", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Nov naslov seznama", "lists.search": "Išči med ljudmi, katerim sledite", "lists.subheading": "Vaši seznami", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Nalaganje...", "media_gallery.toggle_visible": "Preklopi vidljivost", "missing_indicator.label": "Ni najdeno", @@ -314,6 +316,7 @@ "search_results.accounts": "Ljudje", "search_results.hashtags": "Ključniki", "search_results.statuses": "Tuti", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {rezultat} other {rezultatov}}", "status.admin_account": "Odpri vmesnik za moderiranje za @{name}", "status.admin_status": "Odpri status v vmesniku za moderiranje", @@ -351,29 +354,29 @@ "status.show_less_all": "Prikaži manj za vse", "status.show_more": "Prikaži več", "status.show_more_all": "Prikaži več za vse", - "status.show_thread": "Show thread", + "status.show_thread": "Prikaži objavo", "status.unmute_conversation": "Odtišaj pogovor", "status.unpin": "Odpni iz profila", - "suggestions.dismiss": "Dismiss suggestion", - "suggestions.header": "You might be interested in…", + "suggestions.dismiss": "Zavrni predlog", + "suggestions.header": "Morda bi vas zanimalo…", "tabs_bar.federated_timeline": "Združeno", "tabs_bar.home": "Domov", "tabs_bar.local_timeline": "Lokalno", "tabs_bar.notifications": "Obvestila", - "tabs_bar.search": "Poišči", - "time_remaining.days": "{number, plural, one {# day} other {# days}} left", - "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", - "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", - "time_remaining.moments": "Moments remaining", - "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", - "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "tabs_bar.search": "Iskanje", + "time_remaining.days": "{number, plural, one {# dan} other {# dni}} je ostalo", + "time_remaining.hours": "{number, plural, one {# ura} other {# ur}} je ostalo", + "time_remaining.minutes": "{number, plural, one {# minuta} other {# minut}} je ostalo", + "time_remaining.moments": "Preostali trenutki", + "time_remaining.seconds": "{number, plural, one {# sekunda} other {# sekund}} je ostalo", + "trends.count_by_accounts": "{count} {rawCount, plural, one {oseba} other {ljudi}} govori", "ui.beforeunload": "Vaš osnutek bo izgubljen, če zapustite Mastodona.", - "upload_area.title": "Povlecite in spustite za pošiljanje", - "upload_button.label": "Dodaj medij", - "upload_error.limit": "File upload limit exceeded.", - "upload_error.poll": "File upload not allowed with polls.", + "upload_area.title": "Za pošiljanje povlecite in spustite", + "upload_button.label": "Dodaj medije ({formats})", + "upload_error.limit": "Omejitev prenosa datoteke je presežena.", + "upload_error.poll": "Prenos datoteke z anketami ni dovoljen.", "upload_form.description": "Opišite za slabovidne", - "upload_form.focus": "Obreži", + "upload_form.focus": "Spremeni predogled", "upload_form.undo": "Izbriši", "upload_progress.label": "Pošiljanje...", "video.close": "Zapri video", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index 13ce4e9789..21d45f2e8c 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Bazë", "home.column_settings.show_reblogs": "Shfaq përforcime", "home.column_settings.show_replies": "Shfaq përgjigje", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Titull liste të re", "lists.search": "Kërkoni mes personash që ndiqni", "lists.subheading": "Listat tuaja", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Po ngarkohet…", "media_gallery.toggle_visible": "Ndërroni dukshmërinë", "missing_indicator.label": "S’u gjet", @@ -314,6 +316,7 @@ "search_results.accounts": "Persona", "search_results.hashtags": "Hashtagë", "search_results.statuses": "Mesazhe", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, një {result} {results} të tjera}", "status.admin_account": "Hap ndërfaqe moderimi për @{name}", "status.admin_status": "Hape këtë gjendje te ndërfaqja e moderimit", diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json index 8f8ca7c303..55bae4cdd2 100644 --- a/app/javascript/mastodon/locales/sr-Latn.json +++ b/app/javascript/mastodon/locales/sr-Latn.json @@ -64,7 +64,7 @@ "column_header.show_settings": "Prikaži postavke", "column_header.unpin": "Otkači", "column_subheading.settings": "Postavke", - "community.column_settings.media_only": "Media Only", + "community.column_settings.media_only": "Media only", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", @@ -156,6 +156,7 @@ "home.column_settings.basic": "Osnovno", "home.column_settings.show_reblogs": "Prikaži i podržavanja", "home.column_settings.show_replies": "Prikaži odgovore", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Naslov nove liste", "lists.search": "Pretraži među ljudima koje pratite", "lists.subheading": "Vaše liste", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Učitavam...", "media_gallery.toggle_visible": "Uključi/isključi vidljivost", "missing_indicator.label": "Nije pronađeno", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {rezultat} few {rezultata} other {rezultata}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json index 8ef18a7743..a4ae9fcaa4 100644 --- a/app/javascript/mastodon/locales/sr.json +++ b/app/javascript/mastodon/locales/sr.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Основно", "home.column_settings.show_reblogs": "Прикажи и подржавања", "home.column_settings.show_replies": "Прикажи одговоре", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Наслов нове листе", "lists.search": "Претражи међу људима које пратите", "lists.subheading": "Ваше листе", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Учитавам...", "media_gallery.toggle_visible": "Укључи/искључи видљивост", "missing_indicator.label": "Није пронађено", @@ -314,6 +316,7 @@ "search_results.accounts": "Људи", "search_results.hashtags": "Тарабе", "search_results.statuses": "Трубе", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {резултат} few {резултата} other {резултата}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index ab12be885f..fda5c4d570 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Grundläggande", "home.column_settings.show_reblogs": "Visa knuffar", "home.column_settings.show_replies": "Visa svar", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Ny listrubrik", "lists.search": "Sök bland personer du följer", "lists.subheading": "Dina listor", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Laddar...", "media_gallery.toggle_visible": "Växla synlighet", "missing_indicator.label": "Hittades inte", @@ -314,6 +316,7 @@ "search_results.accounts": "Människor", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, ett {result} andra {results}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/ta.json b/app/javascript/mastodon/locales/ta.json index 637ca884a3..87163e6603 100644 --- a/app/javascript/mastodon/locales/ta.json +++ b/app/javascript/mastodon/locales/ta.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "அடிப்படையான", "home.column_settings.show_reblogs": "காட்டு boosts", "home.column_settings.show_replies": "பதில்களைக் காண்பி", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} மற்ற {# days}}", "intervals.full.hours": "{number, plural, one {# hour} மற்ற {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} மற்ற {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "புதிய பட்டியல் தலைப்பு", "lists.search": "நீங்கள் பின்தொடரும் நபர்கள் மத்தியில் தேடுதல்", "lists.subheading": "உங்கள் பட்டியல்கள்", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "ஏற்றுதல்...", "media_gallery.toggle_visible": "நிலைமாற்று தெரியும்", "missing_indicator.label": "கிடைக்கவில்லை", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "ஹாஷ்டேக்குகளைச்", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} மற்ற {results}}", "status.admin_account": "மிதமான இடைமுகத்தை திறக்க @{name}", "status.admin_status": "மிதமான இடைமுகத்தில் இந்த நிலையை திறக்கவும்", diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json index 269ea45c3b..ccb608812c 100644 --- a/app/javascript/mastodon/locales/te.json +++ b/app/javascript/mastodon/locales/te.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "ప్రాథమిక", "home.column_settings.show_reblogs": "బూస్ట్ లను చూపించు", "home.column_settings.show_replies": "ప్రత్యుత్తరాలను చూపించు", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "కొత్త జాబితా శీర్షిక", "lists.search": "మీరు అనుసరించే వ్యక్తులలో శోధించండి", "lists.subheading": "మీ జాబితాలు", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "లోడ్ అవుతోంది...", "media_gallery.toggle_visible": "దృశ్యమానతను టోగుల్ చేయండి", "missing_indicator.label": "దొరకలేదు", @@ -314,6 +316,7 @@ "search_results.accounts": "వ్యక్తులు", "search_results.hashtags": "హాష్ ట్యాగ్లు", "search_results.statuses": "టూట్లు", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {result} other {results}}", "status.admin_account": "@{name} కొరకు సమన్వయ వినిమయసీమను తెరువు", "status.admin_status": "సమన్వయ వినిమయసీమలో ఈ స్టేటస్ ను తెరవండి", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index 3bcf389c7f..e8d7a27edb 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -72,7 +72,7 @@ "compose_form.lock_disclaimer.lock": "ล็อคอยู่", "compose_form.placeholder": "คุณกำลังคิดอะไรอยู่?", "compose_form.poll.add_option": "เพิ่มทางเลือก", - "compose_form.poll.duration": "Poll duration", + "compose_form.poll.duration": "ระยะเวลาการหยั่งเสียง", "compose_form.poll.option_placeholder": "ทางเลือก {number}", "compose_form.poll.remove_option": "เอาทางเลือกนี้ออก", "compose_form.publish": "โพสต์", @@ -156,9 +156,10 @@ "home.column_settings.basic": "พื้นฐาน", "home.column_settings.show_reblogs": "แสดงการดัน", "home.column_settings.show_replies": "แสดงการตอบกลับ", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "home.column_settings.update_live": "Update in real-time", + "intervals.full.days": "{number, plural, other {# วัน}}", + "intervals.full.hours": "{number, plural, other {# ชั่วโมง}}", + "intervals.full.minutes": "{number, plural, other {# นาที}}", "introduction.federation.action": "ถัดไป", "introduction.federation.federated.headline": "ที่ติดต่อกับภายนอก", "introduction.federation.federated.text": "โพสต์สาธารณะจากเซิร์ฟเวอร์อื่น ๆ ของ Fediverse จะปรากฏในเส้นเวลาที่ติดต่อกับภายนอก", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "ชื่อเรื่องรายการใหม่", "lists.search": "ค้นหาในหมู่ผู้คนที่คุณติดตาม", "lists.subheading": "รายการของคุณ", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "กำลังโหลด...", "media_gallery.toggle_visible": "เปิด/ปิดการมองเห็น", "missing_indicator.label": "ไม่พบ", @@ -252,7 +254,7 @@ "notification.favourite": "{name} ได้ชื่นชอบสถานะของคุณ", "notification.follow": "{name} ได้ติดตามคุณ", "notification.mention": "{name} ได้กล่าวถึงคุณ", - "notification.poll": "A poll you have voted in has ended", + "notification.poll": "การหยั่งเสียงที่คุณได้ลงคะแนนได้สิ้นสุดแล้ว", "notification.reblog": "{name} ได้ดันสถานะของคุณ", "notifications.clear": "ล้างการแจ้งเตือน", "notifications.clear_confirmation": "คุณแน่ใจหรือไม่ว่าต้องการล้างการแจ้งเตือนทั้งหมดของคุณอย่างถาวร?", @@ -263,7 +265,7 @@ "notifications.column_settings.filter_bar.show": "แสดง", "notifications.column_settings.follow": "ผู้ติดตามใหม่:", "notifications.column_settings.mention": "การกล่าวถึง:", - "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.poll": "ผลลัพธ์การหยั่งเสียง:", "notifications.column_settings.push": "การแจ้งเตือนแบบผลัก", "notifications.column_settings.reblog": "การดัน:", "notifications.column_settings.show": "แสดงในคอลัมน์", @@ -273,14 +275,14 @@ "notifications.filter.favourites": "รายการโปรด", "notifications.filter.follows": "การติดตาม", "notifications.filter.mentions": "การกล่าวถึง", - "notifications.filter.polls": "Poll results", + "notifications.filter.polls": "ผลลัพธ์การหยั่งเสียง", "notifications.group": "{count} การแจ้งเตือน", "poll.closed": "ปิดแล้ว", "poll.refresh": "รีเฟรช", - "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", - "poll.vote": "Vote", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", + "poll.total_votes": "{count, plural, other {# การลงคะแนน}}", + "poll.vote": "ลงคะแนน", + "poll_button.add_poll": "เพิ่มการหยั่งเสียง", + "poll_button.remove_poll": "เอาการหยั่งเสียงออก", "privacy.change": "ปรับเปลี่ยนความเป็นส่วนตัวของสถานะ", "privacy.direct.long": "โพสต์ไปยังผู้ใช้ที่กล่าวถึงเท่านั้น", "privacy.direct.short": "โดยตรง", @@ -314,7 +316,8 @@ "search_results.accounts": "ผู้คน", "search_results.hashtags": "แฮชแท็ก", "search_results.statuses": "โพสต์", - "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", + "search_results.total": "{count, number} {count, plural, other {ผลลัพธ์}}", "status.admin_account": "เปิดส่วนติดต่อการควบคุมสำหรับ @{name}", "status.admin_status": "เปิดสถานะนี้ในส่วนติดต่อการควบคุม", "status.block": "ปิดกั้น @{name}", @@ -361,11 +364,11 @@ "tabs_bar.local_timeline": "ในเว็บ", "tabs_bar.notifications": "การแจ้งเตือน", "tabs_bar.search": "ค้นหา", - "time_remaining.days": "{number, plural, one {# day} other {# days}} left", - "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", - "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.days": "เหลืออีก {number, plural, other {# วัน}}", + "time_remaining.hours": "เหลืออีก {number, plural, other {# ชั่วโมง}}", + "time_remaining.minutes": "เหลืออีก {number, plural, other {# นาที}}", "time_remaining.moments": "ช่วงเวลาที่เหลือ", - "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", + "time_remaining.seconds": "เหลืออีก {number, plural, other {# วินาที}}", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "ui.beforeunload": "แบบร่างของคุณจะหายไปหากคุณออกจาก Mastodon", "upload_area.title": "ลากแล้วปล่อยเพื่ออัปโหลด", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index ec4657b9bf..0ea015cc64 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Temel", "home.column_settings.show_reblogs": "Boost edilenleri göster", "home.column_settings.show_replies": "Cevapları göster", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Yeni liste başlığı", "lists.search": "Takip ettiğiniz kişiler arasından arayın", "lists.subheading": "Listeleriniz", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Yükleniyor...", "media_gallery.toggle_visible": "Görünürlüğü değiştir", "missing_indicator.label": "Bulunamadı", @@ -314,6 +316,7 @@ "search_results.accounts": "İnsanlar", "search_results.hashtags": "Hashtagler", "search_results.statuses": "Gönderiler", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {sonuç} other {sonuçlar}}", "status.admin_account": "@{name} için denetim arayüzünü açın", "status.admin_status": "Denetim arayüzünde bu durumu açın", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 124b9fb076..17e8cb49f4 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "Основні", "home.column_settings.show_reblogs": "Показувати передмухи", "home.column_settings.show_replies": "Показувати відповіді", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "Нова назва списку", "lists.search": "Шукати серед людей, на яких ви підписані", "lists.subheading": "Ваші списки", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Завантаження...", "media_gallery.toggle_visible": "Показати/приховати", "missing_indicator.label": "Не знайдено", @@ -314,6 +316,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} {count, plural, one {результат} few {результати} many {результатів} other {результатів}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 865d3a5142..bb774f1aaa 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -12,7 +12,7 @@ "account.followers": "关注者", "account.followers.empty": "目前无人关注此用户。", "account.follows": "正在关注", - "account.follows.empty": "此用户目前没有关注任何人。", + "account.follows.empty": "此用户目前尚未关注任何人。", "account.follows_you": "关注了你", "account.hide_reblogs": "隐藏来自 @{name} 的转嘟", "account.link_verified_on": "此链接的所有权已在 {date} 检查", @@ -48,7 +48,7 @@ "column.community": "本站时间轴", "column.direct": "私信", "column.domain_blocks": "已屏蔽的网站", - "column.favourites": "收藏过的嘟文", + "column.favourites": "收藏", "column.follow_requests": "关注请求", "column.home": "主页", "column.lists": "列表", @@ -92,11 +92,11 @@ "confirmations.delete_list.confirm": "删除", "confirmations.delete_list.message": "你确定要永久删除这个列表吗?", "confirmations.domain_block.confirm": "隐藏整个网站的内容", - "confirmations.domain_block.message": "你真的确定要隐藏所有来自 {domain} 的内容吗?多数情况下,屏蔽或隐藏几个特定的用户应该就能满足你的需要了。来自该网站的内容将不再出现在你的公共时间轴或通知列表里。来自该网站的关注者将会被移除。", + "confirmations.domain_block.message": "你真的确定要隐藏所有来自 {domain} 的内容吗?多数情况下,屏蔽或隐藏几个特定的用户就已经足够了。来自该网站的内容将不再出现在你的任何公共时间轴或通知列表里。来自该网站的关注者将会被移除。", "confirmations.mute.confirm": "隐藏", "confirmations.mute.message": "你确定要隐藏 {name} 吗?", "confirmations.redraft.confirm": "删除并重新编辑", - "confirmations.redraft.message": "你确定要删除这条嘟文并重新编辑它吗?所有相关的转嘟和收藏都会被清除,回复将会被孤立。", + "confirmations.redraft.message": "你确定要删除这条嘟文并重新编辑它吗?所有相关的转嘟和收藏都会被清除,回复将会失去关联。", "confirmations.reply.confirm": "回复", "confirmations.reply.message": "回复此消息将会覆盖当前正在编辑的信息。确定继续吗?", "confirmations.unfollow.confirm": "取消关注", @@ -120,28 +120,28 @@ "empty_column.account_timeline": "这里没有嘟文!", "empty_column.account_unavailable": "个人资料不可用", "empty_column.blocks": "你目前没有屏蔽任何用户。", - "empty_column.community": "本站时间轴暂时没有内容,快嘟几个来抢头香啊!", + "empty_column.community": "本站时间轴暂时没有内容,快写点什么让它动起来吧!", "empty_column.direct": "你还没有使用过私信。当你发出或者收到私信时,它会在这里显示。", "empty_column.domain_blocks": "目前没有被隐藏的站点。", "empty_column.favourited_statuses": "你还没有收藏过任何嘟文。收藏过的嘟文会显示在这里。", - "empty_column.favourites": "没人收藏过这条嘟文。假如有人收藏了,就会显示在这里。", + "empty_column.favourites": "没有人收藏过这条嘟文。如果有人收藏了,就会显示在这里。", "empty_column.follow_requests": "你没有收到新的关注请求。收到了之后就会显示在这里。", "empty_column.hashtag": "这个话题标签下暂时没有内容。", - "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。", + "empty_column.home": "你还没有关注任何用户。快看看{public},向其他人问个好吧。", "empty_column.home.public_timeline": "公共时间轴", "empty_column.list": "这个列表中暂时没有内容。列表中用户所发送的的新嘟文将会在这里显示。", - "empty_column.lists": "你没有创建过列表。你创建的列表会在这里显示。", + "empty_column.lists": "你还没有创建过列表。你创建的列表会在这里显示。", "empty_column.mutes": "你没有隐藏任何用户。", - "empty_column.notifications": "你还没有收到过任何通知,快向其他用户搭讪吧。", + "empty_column.notifications": "你还没有收到过任何通知,快和其他用户互动吧。", "empty_column.public": "这里什么都没有!写一些公开的嘟文,或者关注其他服务器的用户后,这里就会有嘟文出现了", "follow_request.authorize": "同意", "follow_request.reject": "拒绝", "getting_started.developers": "开发", - "getting_started.directory": "用户资料目录", + "getting_started.directory": "用户目录", "getting_started.documentation": "文档", "getting_started.heading": "开始使用", "getting_started.invite": "邀请用户", - "getting_started.open_source_notice": "Mastodon 是一个开源软件。欢迎前往 GitHub({github})贡献代码或反馈问题。", + "getting_started.open_source_notice": "Mastodon 是开源软件。欢迎前往 GitHub({github})贡献代码或反馈问题。", "getting_started.security": "帐户安全", "getting_started.terms": "使用条款", "hashtag.column_header.tag_mode.all": "以及 {additional}", @@ -156,14 +156,15 @@ "home.column_settings.basic": "基本设置", "home.column_settings.show_reblogs": "显示转嘟", "home.column_settings.show_replies": "显示回复", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number} 天", "intervals.full.hours": "{number} 小时", "intervals.full.minutes": "{number} 分钟", "introduction.federation.action": "下一步", "introduction.federation.federated.headline": "跨站", - "introduction.federation.federated.text": "其他跨站服务器的公共动态会显示在跨站时间线中。", + "introduction.federation.federated.text": "联邦宇宙中其他服务器的公开嘟文会显示在跨站时间轴中。", "introduction.federation.home.headline": "主页", - "introduction.federation.home.text": "你所关注的用户的动态会显示在主页里。你可以关注任何服务器上的任何人!", + "introduction.federation.home.text": "你所关注的用户的动态会显示在主页里。你可以关注任何服务器上的任何人!", "introduction.federation.local.headline": "本站", "introduction.federation.local.text": "你所关注的用户的动态会显示在主页里,你可以关注任何服务器上的任何人。", "introduction.interactions.action": "教程结束!", @@ -172,10 +173,10 @@ "introduction.interactions.reblog.headline": "转嘟", "introduction.interactions.reblog.text": "通过转嘟,你可以向你的关注者分享其他人的嘟文。", "introduction.interactions.reply.headline": "回复", - "introduction.interactions.reply.text": "你可以向其他人回复,这些回复会像对话一样串在一起。", + "introduction.interactions.reply.text": "你可以回复其他嘟文,这些回复会像对话一样关联在一起。", "introduction.welcome.action": "让我们开始吧!", "introduction.welcome.headline": "首先", - "introduction.welcome.text": "欢迎来到联邦!稍后,您将可以广播消息并和您的朋友交流,这些消息将穿越于联邦中的各式服务器。但是这台服务器,{domain},是特殊的——它保存了你的个人资料,所以请记住它的名字。", + "introduction.welcome.text": "欢迎来到联邦宇宙!很快,您就可以发布信息并和您的朋友交流,这些消息将发送到联邦中的各个服务器中。但是这台服务器,{domain},是特殊的——它保存了你的个人资料,所以请记住它的名字。", "keyboard_shortcuts.back": "返回上一页", "keyboard_shortcuts.blocked": "打开被屏蔽用户列表", "keyboard_shortcuts.boost": "转嘟", @@ -194,9 +195,9 @@ "keyboard_shortcuts.legend": "显示此列表", "keyboard_shortcuts.local": "打开本站时间轴", "keyboard_shortcuts.mention": "提及嘟文作者", - "keyboard_shortcuts.muted": "打开屏蔽用户列表", + "keyboard_shortcuts.muted": "打开隐藏用户列表", "keyboard_shortcuts.my_profile": "打开你的个人资料", - "keyboard_shortcuts.notifications": "打卡通知栏", + "keyboard_shortcuts.notifications": "打开通知栏", "keyboard_shortcuts.pinned": "打开置顶嘟文列表", "keyboard_shortcuts.profile": "打开作者的个人资料", "keyboard_shortcuts.reply": "回复嘟文", @@ -213,7 +214,7 @@ "lightbox.previous": "上一个", "lightbox.view_context": "查看上下文", "lists.account.add": "添加到列表", - "lists.account.remove": "从列表中删除", + "lists.account.remove": "从列表中移除", "lists.delete": "删除列表", "lists.edit": "编辑列表", "lists.edit.submit": "更改标题", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "新列表的标题", "lists.search": "搜索你关注的人", "lists.subheading": "你的列表", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "加载中……", "media_gallery.toggle_visible": "切换显示/隐藏", "missing_indicator.label": "找不到内容", @@ -235,29 +237,29 @@ "navigation_bar.domain_blocks": "已屏蔽的网站", "navigation_bar.edit_profile": "修改个人资料", "navigation_bar.favourites": "收藏的内容", - "navigation_bar.filters": "被隐藏的词", + "navigation_bar.filters": "屏蔽关键词", "navigation_bar.follow_requests": "关注请求", - "navigation_bar.follows_and_followers": "正在关注以及关注者", + "navigation_bar.follows_and_followers": "关注管理", "navigation_bar.info": "关于本站", "navigation_bar.keyboard_shortcuts": "快捷键列表", "navigation_bar.lists": "列表", - "navigation_bar.logout": "注销", + "navigation_bar.logout": "登出", "navigation_bar.mutes": "已隐藏的用户", "navigation_bar.personal": "个人", "navigation_bar.pins": "置顶嘟文", "navigation_bar.preferences": "首选项", - "navigation_bar.profile_directory": "用户资料目录", + "navigation_bar.profile_directory": "用户目录", "navigation_bar.public_timeline": "跨站公共时间轴", "navigation_bar.security": "安全", "notification.favourite": "{name} 收藏了你的嘟文", "notification.follow": "{name} 开始关注你", - "notification.mention": "{name} 提及你", + "notification.mention": "{name} 提及了你", "notification.poll": "你参与的一个投票已经结束", - "notification.reblog": "{name} 转了你的嘟文", + "notification.reblog": "{name} 转嘟了你的嘟文", "notifications.clear": "清空通知列表", "notifications.clear_confirmation": "你确定要永久清空通知列表吗?", "notifications.column_settings.alert": "桌面通知", - "notifications.column_settings.favourite": "你的嘟文被收藏时:", + "notifications.column_settings.favourite": "当你的嘟文被收藏时:", "notifications.column_settings.filter_bar.advanced": "显示所有类别", "notifications.column_settings.filter_bar.category": "快速过滤栏", "notifications.column_settings.filter_bar.show": "显示", @@ -301,25 +303,26 @@ "report.forward": "发送举报至 {target}", "report.forward_hint": "这名用户来自另一个服务器。是否要向那个服务器发送一条匿名的举报?", "report.hint": "举报将会发送给你所在服务器的监察员。你可以在下面填写举报该用户的理由:", - "report.placeholder": "附言", + "report.placeholder": "备注", "report.submit": "提交", "report.target": "举报 {target}", "search.placeholder": "搜索", "search_popout.search_format": "高级搜索格式", - "search_popout.tips.full_text": "输入其他内容将会返回所有你撰写、收藏、转嘟过或提及到你的嘟文,同时也会在用户名、昵称和话题标签中进行搜索。", + "search_popout.tips.full_text": "输入关键词检索所有你发送、收藏、转嘟过或提及到你的嘟文,以及其他用户公开的用户名、昵称和话题标签。", "search_popout.tips.hashtag": "话题标签", "search_popout.tips.status": "嘟文", - "search_popout.tips.text": "输入其他内容将会返回昵称、用户名和话题标签", + "search_popout.tips.text": "输入关键词检索昵称、用户名和话题标签", "search_popout.tips.user": "用户", "search_results.accounts": "用户", "search_results.hashtags": "话题标签", "search_results.statuses": "嘟文", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "共 {count, number} 个结果", "status.admin_account": "打开 @{name} 的管理界面", "status.admin_status": "打开这条嘟文的管理界面", "status.block": "屏蔽 @{name}", "status.cancel_reblog_private": "取消转嘟", - "status.cannot_reblog": "无法转嘟这条嘟文", + "status.cannot_reblog": "这条嘟文不允许被转嘟", "status.copy": "复制嘟文链接", "status.delete": "删除", "status.detailed_status": "对话详情", @@ -338,9 +341,9 @@ "status.pinned": "置顶嘟文", "status.read_more": "阅读全文", "status.reblog": "转嘟", - "status.reblog_private": "转嘟给原有关注者", + "status.reblog_private": "转嘟(可见者不变)", "status.reblogged_by": "{name} 转嘟了", - "status.reblogs.empty": "无人转嘟此条。如果有人转嘟了,就会显示在这里。", + "status.reblogs.empty": "没有人转嘟过此条嘟文。如果有人转嘟了,就会显示在这里。", "status.redraft": "删除并重新编辑", "status.reply": "回复", "status.replyAll": "回复所有人", @@ -367,15 +370,15 @@ "time_remaining.moments": "即将结束", "time_remaining.seconds": "剩余 {number, plural, one {# 秒} other {# 秒}}", "trends.count_by_accounts": "{count} 人正在讨论", - "ui.beforeunload": "如果你现在离开 Mastodon,你的草稿内容将会被丢弃。", + "ui.beforeunload": "如果你现在离开 Mastodon,你的草稿内容将会丢失。", "upload_area.title": "将文件拖放到此处开始上传", "upload_button.label": "上传媒体文件 (JPEG, PNG, GIF, WebM, MP4, MOV)", - "upload_error.limit": "超过文件上传限制。", + "upload_error.limit": "文件大小超过限制。", "upload_error.poll": "投票中不允许上传文件。", "upload_form.description": "为视觉障碍人士添加文字说明", - "upload_form.focus": "剪裁", + "upload_form.focus": "设置缩略图", "upload_form.undo": "删除", - "upload_progress.label": "上传中…", + "upload_progress.label": "上传中……", "video.close": "关闭视频", "video.exit_fullscreen": "退出全屏", "video.expand": "展开视频", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index 2cfc11703d..b4c8b874a8 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "基本", "home.column_settings.show_reblogs": "顯示被轉推的文章", "home.column_settings.show_replies": "顯示回應文章", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "新列表標題", "lists.search": "從你關注的用戶中搜索", "lists.subheading": "列表", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "載入中...", "media_gallery.toggle_visible": "打開或關上", "missing_indicator.label": "找不到內容", @@ -314,6 +316,7 @@ "search_results.accounts": "使用者", "search_results.hashtags": "標籤", "search_results.statuses": "文章", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} 項結果", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 5715ef01a1..5f75b38d6c 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -156,6 +156,7 @@ "home.column_settings.basic": "基本", "home.column_settings.show_reblogs": "顯示轉推", "home.column_settings.show_replies": "顯示回覆", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# 天} other {# 天}}", "intervals.full.hours": "{number, plural, one {# 小時} other {# 小時}}", "intervals.full.minutes": "{number, plural, one {# 分鐘} other {# 分鐘}}", @@ -221,6 +222,7 @@ "lists.new.title_placeholder": "新名單標題", "lists.search": "搜尋您關注的使用者", "lists.subheading": "您的名單", + "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "讀取中...", "media_gallery.toggle_visible": "切換可見性", "missing_indicator.label": "找不到", @@ -314,6 +316,7 @@ "search_results.accounts": "使用者", "search_results.hashtags": "主題標籤", "search_results.statuses": "嘟文", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.total": "{count, number} 項結果", "status.admin_account": "開啟 @{name} 的管理介面", "status.admin_status": "在管理介面開啟此嘟文", diff --git a/config/locales/activerecord.bn.yml b/config/locales/activerecord.bn.yml index 152c698290..e0e6ac90c2 100644 --- a/config/locales/activerecord.bn.yml +++ b/config/locales/activerecord.bn.yml @@ -1 +1,17 @@ +--- bn: + activerecord: + attributes: + poll: + expires_at: শেষ হবে + options: বিকল্প + errors: + models: + account: + attributes: + username: + invalid: শুধুমাত্র অক্ষর, সংখ্যা এবং _ বেবহার করা যাবে + status: + attributes: + reblog: + taken: লেখাটি ইতিপূর্বে ছিল diff --git a/config/locales/activerecord.cy.yml b/config/locales/activerecord.cy.yml index 19547df986..92fba043fb 100644 --- a/config/locales/activerecord.cy.yml +++ b/config/locales/activerecord.cy.yml @@ -3,7 +3,7 @@ cy: activerecord: attributes: poll: - expires_at: Terfyn + expires_at: Terfyn amser options: Dewisiadau errors: models: diff --git a/config/locales/ar.yml b/config/locales/ar.yml index eb6a5ef06c..e48ee89c56 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -478,12 +478,6 @@ ar: no_status_selected: لم يطرأ أي تغيير على أي منشور بما أنه لم يتم اختيار أي واحد title: منشورات الحساب with_media: تحتوي على وسائط - subscriptions: - callback_url: عاود الاتصال بالعنوان - confirmed: مؤكَّد - expires_in: تنتهي مدة صلاحيتها في - last_delivery: آخر إيداع - topic: الموضوع tags: accounts: الحسابات hidden: المخفية @@ -818,10 +812,6 @@ ar: reply: proceed: المواصلة إلى الرد prompt: 'ترغب في الرد على هذا التبويق:' - remote_unfollow: - error: خطأ - title: العنوان - unfollowed: غير متابَع sessions: activity: آخر نشاط browser: المتصفح diff --git a/config/locales/ast.yml b/config/locales/ast.yml index ec545ca578..30390c1633 100644 --- a/config/locales/ast.yml +++ b/config/locales/ast.yml @@ -227,8 +227,6 @@ ast: no_account_html: "¿Nun tienes una cuenta? Pues rexistrate equí" proceed: Siguir prompt: 'Vas siguir a:' - remote_unfollow: - error: Fallu sessions: browser: Restolador browsers: diff --git a/config/locales/ca.yml b/config/locales/ca.yml index a5d96cc1ca..d05406ebb3 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -469,13 +469,6 @@ ca: no_status_selected: No s’han canviat els estatus perquè cap no ha estat seleccionat title: Estats del compte with_media: Amb contingut multimèdia - subscriptions: - callback_url: URL de retorn - confirmed: Confirmat - expires_in: Expira en - last_delivery: Últim lliurament - title: WebSub - topic: Tema tags: accounts: Comptes hidden: Amagat @@ -816,10 +809,6 @@ ca: reply: proceed: Procedir a respondre prompt: 'Vols respondre a aquest toot:' - remote_unfollow: - error: Error - title: Títol - unfollowed: Sense seguir scheduled_statuses: over_daily_limit: Has superat el límit de %{limit} toots programats per a aquell dia over_total_limit: Has superat el limit de %{limit} toots programats diff --git a/config/locales/co.yml b/config/locales/co.yml index b3d14fdb53..4e2ceda228 100644 --- a/config/locales/co.yml +++ b/config/locales/co.yml @@ -469,13 +469,6 @@ co: no_status_selected: I statuti ùn sò micca stati mudificati perchè manc'unu era selezziunatu title: Statutu di u contu with_media: Cù media - subscriptions: - callback_url: URL di richjama - confirmed: Cunfirmatu - expires_in: Spira in - last_delivery: Ultima arricata - title: WebSub - topic: Sughjettu tags: accounts: Conti hidden: Piattatu @@ -816,10 +809,6 @@ co: reply: proceed: Cuntinuà per risponde prompt: 'Vulete risponde à stu statutu:' - remote_unfollow: - error: Errore - title: Titulu - unfollowed: Disabbunatu scheduled_statuses: over_daily_limit: Avete trapassatu a limita di %{limit} statuti planificati per stu ghjornu over_total_limit: Avete trapassatu a limita di %{limit} statuti planificati diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 0735a86981..3518b3b91f 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -481,13 +481,6 @@ cs: no_status_selected: Nebyly změněny žádné tooty, neboť žádné nebyly vybrány title: Tooty účtu with_media: S médii - subscriptions: - callback_url: Zpáteční URL - confirmed: Potvrzeno - expires_in: Vyprší v - last_delivery: Poslední doručení - title: WebSub - topic: Téma tags: accounts: Účty hidden: Skryté @@ -838,10 +831,6 @@ cs: reply: proceed: Pokračovat k odpovězení prompt: 'Chcete odpovědět na tento toot:' - remote_unfollow: - error: Chyba - title: Nadpis - unfollowed: Už nesledujete scheduled_statuses: over_daily_limit: Překročil/a jste limit %{limit} plánovaných tootů pro tento den over_total_limit: Překročil/a jste limit %{limit} plánovaných tootů diff --git a/config/locales/cy.yml b/config/locales/cy.yml index 080e89214b..fbeaa22b16 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -493,13 +493,6 @@ cy: no_status_selected: Ni newidwyd dim statws achos ni ddewiswyd dim un title: Statysau cyfrif with_media: A chyfryngau - subscriptions: - callback_url: URL galw-nôl - confirmed: Wedi'i gadarnhau - expires_in: Dod i ben ymhen - last_delivery: Danfoniad diwethaf - title: WebSub - topic: Pwnc tags: accounts: Cyfrifon hidden: Cudd @@ -862,10 +855,6 @@ cy: reply: proceed: Ymlaen i ateb prompt: 'Hoffech ateb y tŵt hon:' - remote_unfollow: - error: Gwall - title: Teitl - unfollowed: Dad-ddilynwyd scheduled_statuses: over_daily_limit: Rydych wedi rhagori'r cyfwng o %{limit} o dŵtiau rhestredig ar y dydd hynny over_total_limit: Rydych wedi rhagori'r cyfwng o %{limit} o dŵtiau rhestredig diff --git a/config/locales/da.yml b/config/locales/da.yml index da6ab10545..b24c9475c8 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -390,13 +390,6 @@ da: no_status_selected: Ingen statusser blev ændret eller ingen blev valgt title: Konto statusser with_media: Med multimedier - subscriptions: - callback_url: Callback-URL - confirmed: Bekræftet - expires_in: Udløber om - last_delivery: Sidste levering - title: Websub - topic: Emne tags: accounts: Kontoer hidden: Skjult @@ -616,10 +609,6 @@ da: no_account_html: Har du ikke en konto? Du kan oprette dig her proceed: Fortsæt for at følge prompt: 'Du er ved at følge:' - remote_unfollow: - error: Fejl - title: Titel - unfollowed: Følger ikke længere sessions: activity: Sidste aktivitet browsers: diff --git a/config/locales/de.yml b/config/locales/de.yml index cfdaacab06..b9b8c02df6 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -469,13 +469,6 @@ de: no_status_selected: Keine Beiträge wurden geändert, weil keine ausgewählt wurden title: Beiträge des Kontos with_media: Mit Medien - subscriptions: - callback_url: Callback-URL - confirmed: Bestätigt - expires_in: Verfällt in - last_delivery: Letzte Zustellung - title: WebSub - topic: Thema tags: accounts: Konten hidden: Versteckt @@ -816,10 +809,6 @@ de: reply: proceed: Fortfahren zum Antworten prompt: 'Du möchtest auf diesen Beitrag antworten:' - remote_unfollow: - error: Fehler - title: Titel - unfollowed: Entfolgt scheduled_statuses: over_daily_limit: Du hast das Limit für geplante Beiträge, dass %{limit} beträgt, für heute erreicht over_total_limit: Du hast das Limit für geplante Beiträge, dass %{limit} beträgt, erreicht diff --git a/config/locales/devise.bn.yml b/config/locales/devise.bn.yml index 152c698290..cb7179da6f 100644 --- a/config/locales/devise.bn.yml +++ b/config/locales/devise.bn.yml @@ -1 +1,40 @@ +--- bn: + devise: + confirmations: + confirmed: আপনার ইমেইলটি সঠিকভাবে নিশ্চিত করা হয়েছে। + send_instructions: আপনি একটি ইমেইল পাবেন যেটাতে কিভাবে আপনার ইমেইলটি নিশ্চিত করতে হবে সেটা পাঠানো হবে। যদি না পান, অনুগ্রহ করে আপনার স্প্যাম ফোল্ডারটি চেক করবেন। + send_paranoid_instructions: আমাদের ডাটাবেসে যদি আপনার ইমেইল থেকে থাকে, আপনার কাছে একটা ইমেইল পাঠানো হবে যেখানে কিভাবে আপনার ইমেইল নিশ্চিত করতে হবে লেখা থাকবে। যদি না পান, অনুগ্রহ করে আপনার স্প্যাম ফোল্ডারটি চেক করবেন। + failure: + already_authenticated: আপনি ইতিপূর্বে ভেতরে ঢুকেছেন (আবার লাগবে না)। + inactive: আনার নিবন্ধনটি এখনো চালু করা হয়নি। + invalid: ভুল %{authentication_keys} বা পাসওয়ার্ড । + last_attempt: আপনার আর একবার চেষ্টা করার সুযোক আছে, তারপর আপনার নিবন্ধনে ঢোকার ক্ষেত্রে তালা দেওয়া হবে। + locked: নিবন্ধনে ঢোকার ক্ষেত্রে তালা দেওয়া হয়েছে। + not_found_in_database: ভুল %{authentication_keys} বা পাসওয়ার্ড। + pending: আপনার নিবন্ধনটি এখনো পর্যালোচনার জন্য অপেক্ষায় আছে। + timeout: আপনার সেশনটির সময় শেষ হয়ে গেছে। অনুগ্রহ করে আবার নিবন্ধনে ঢুকে চালাতে থাকেন। + unauthenticated: এটা ব্যবহার করতে আপনার আগে আপনার নিবন্ধনে ঢুকতে হবে অথবা নিবন্ধন তৈরি করতে হবে। + unconfirmed: এটা ব্যবহার করতে আপনার আগে আপনার ইমেইলটি নিশ্চিত করতে হবে। + mailer: + confirmation_instructions: + action: ইমেইলটি নিশ্চিত করুন + action_with_app: নিশ্চিত করুন এবং %{app} তে ফিরে যান + explanation: "%{host} তে এই ইমেইল ব্যবহার করে নিবন্ধন করতে হবে। আর একটা ক্লিক করলেই এটা চালু হয়ে যাবে। যদি আপনি এটা না পাঠিয়ে থাকেন, তাহলে অনুগ্রহ করে এই ইমেইলটি উপেক্ষা করুন।" + password_change: + extra: আপনি নিজে যদি পাসওয়ার্ডটি না বদলে থাকেন, খুব সম্ভব অন্যকেও আপনার নিবন্ধনে প্রবেশ করে এটা করেছে। অনুগ্রহ করে যত দ্রুত সম্ভব আপনার পাসওয়ার্ডটি বদলান অথবা যদি আপনি আপনার নিবন্ধনে আর না ঢুকতে পারেন, এই সার্ভারের পরিচালককে জানান। + subject: 'মাস্টাডন: পাসওয়ার্ড বদলানো হয়েছে' + title: পাসওয়ার্ড বদলানো হয়েছে + reconfirmation_instructions: + explanation: নতুন ইমেইলটি নিশ্চিত করুন। + extra: আপনি যদি এটা না চেয়ে থাকেন, এই ইমেইলটি উপেক্ষা করুন। উপরের লিংকটিতে না গেলে আপনার নিবন্ধনের সাথে যুক্ত ইমেইল বদলাবে না। + subject: 'মাস্টাডন: ইমেইল নিশ্চিত করুন %{instance} জন্য' + title: আপনার ইমেইলটি নিশ্চিত করুন + reset_password_instructions: + action: পাসওয়ার্ড বদলান + explanation: আপনি আপনার নিবন্ধনের জন্য নতুন পাসওয়ার্ড চেয়েছেন। + extra: আপনি যদি এটা না চেয়ে থাকেন, এই ইমেইলটি উপেক্ষা করুন। উপরের লিংকটিতে না গেলে আপনার পাসওয়ার্ড বদলাবে না। + subject: 'মাস্টাডন: পাসওয়ার্ড বদলানোর নির্দেশনা' + title: পাসওয়ার্ড বদলানো + registrations: + signed_up: স্বাগতম! আপনার নিবন্ধনটি সঠিকভাবে হয়েছে। diff --git a/config/locales/devise.sk.yml b/config/locales/devise.sk.yml index 85de603d3e..4837390dba 100644 --- a/config/locales/devise.sk.yml +++ b/config/locales/devise.sk.yml @@ -38,7 +38,7 @@ sk: explanation: Potvrď novú emailovú adresu na ktorú chceš zmeniť svoj email. extra: Pokiaľ si túto akciu nevyžiadal/a, prosím ignoruj tento email. Emailová adresa pre tvoj Mastodon účet totiž nebude zmenená pokiaľ nepostúpiš na adresu uvedenú vyššie. subject: 'Mastodon: Potvrďenie emailu pre %{instance}' - title: Overiť emailovú adresu + title: Over emailovú adresu reset_password_instructions: action: Zmeň svoje heslo explanation: Vyžiadal/a si si nové heslo pre svoj účet. diff --git a/config/locales/devise.sl.yml b/config/locales/devise.sl.yml index 7d1e05fdf0..dee1b91253 100644 --- a/config/locales/devise.sl.yml +++ b/config/locales/devise.sl.yml @@ -6,7 +6,7 @@ sl: send_instructions: V nekaj minutah boste prejeli e-poštno sporočilo z navodili za potrditev vašega e-poštnega naslova. Če niste prejeli e-poštnega sporočila, preverite mapo neželena pošta. send_paranoid_instructions: Če vaš e-poštni naslov obstaja v naši podatkovni bazi, boste v nekaj minutah prejeli e-poštno sporočilo z navodili za potrditev vašega e-poštnega naslova. Če niste prejeli e-poštnega sporočila, preverite mapo neželena pošta. failure: - already_authenticated: Prijavljeni ste že. + already_authenticated: Ste že prijavljeni. inactive: Vaš račun še ni aktiviran. invalid: Neveljavno %{authentication_keys} ali geslo. last_attempt: Pred zaklepom računa imate še en poskus. @@ -45,9 +45,44 @@ sl: explanation: Zahtevali ste novo geslo za svoj račun. extra: Če tega niste zahtevali, prezrite to e-poštno sporočilo. Vaše geslo se ne bo spremenilo, dokler ne kliknete na zgornjo povezavo in ustvarite novega. subject: 'Mastodon: Navodila za ponastavitev gesla' - title: Ponastavitev gesla + title: Ponastavi geslo unlock_instructions: subject: 'Mastodon: Odkleni navodila' omniauth_callbacks: failure: Overitev iz %{kind} ni možna zaradi "%{reason}". success: Overitev iz računa %{kind} je bila uspešna. + passwords: + no_token: Do te strani ne morete dostopati, ne da bi prišli iz e-poštne za ponastavitev gesla. Če prihajate iz e-poštne za ponastavitev gesla, se prepričajte, da ste uporabili celoten navedeni URL. + send_instructions: Če vaš e-poštni naslov obstaja v naši bazi podatkov, boste v nekaj minutah na vaš e-poštni naslov prejeli povezavo za obnovitev gesla. Če niste prejeli e-pošte, preverite mapo z neželeno pošto. + send_paranoid_instructions: Če vaš e-poštni naslov obstaja v naši bazi podatkov, boste v nekaj minutah na vaš e-poštni naslov prejeli povezavo za obnovitev gesla. Če niste prejeli e-pošte, preverite mapo z neželeno pošto. + updated: Vaše geslo je bilo uspešno spremenjeno. Zdaj ste prijavljeni. + updated_not_active: Vaše geslo je bilo uspešno spremenjeno. + registrations: + destroyed: Adijo! Vaš račun je bil uspešno preklican. Upamo, da vas bomo kmalu spet videli. + signed_up: Dobrodošli! Uspešno ste se vpisali. + signed_up_but_inactive: Uspešno ste se vpisali. Vendar vas nismo mogli prijaviti, ker vaš račun še ni aktiviran. + signed_up_but_locked: Uspešno ste se vpisali. Vendar vas nismo mogli prijaviti, ker je vaš račun zaklenjen. + signed_up_but_pending: Na vaš e-poštni naslov je bilo poslano sporočilo s povezavo za potrditev. Ko kliknete na povezavo, bomo pregledali vašo prijavo. Obveščeni boste, če bo odobren. + signed_up_but_unconfirmed: Na vaš e-poštni naslov je bilo poslano sporočilo s povezavo za potrditev. Sledite povezavi, da aktivirate svoj račun. Če niste prejeli te e-pošte, preverite mapo z neželeno pošto. + update_needs_confirmation: Uspešno ste posodobili račun, vendar moramo potrditi vaš novi e-poštni naslov. Preverite svojo e-pošto in sledite povezavi za potrditev, da potrdite nov e-poštni naslov. Če niste prejeli te e-poše, preverite mapo z neželeno pošto. + updated: Vaš račun je bil uspešno posodobljen. + sessions: + already_signed_out: Uspešno ste se odjavili. + signed_in: Uspešno ste se prijavili. + signed_out: Uspešno ste se odjavili. + unlocks: + send_instructions: Prejeli boste e-pošto z navodili o tem, kako v nekaj minutah odklenete svoj račun. Če niste prejeli te e-pošte, preverite mapo z neželeno pošto. + send_paranoid_instructions: Če vaš račun obstaja, boste prejeli e-pošto z navodili za njegovo odklepanje v nekaj minutah. Če niste prejeli te e-pošte, preverite mapo z neželeno pošto. + unlocked: Vaš račun je bil uspešno odklenjen. Če želite nadaljevati, se prijavite. + errors: + messages: + already_confirmed: je bil potrjen, poskusite se prijaviti + confirmation_period_expired: mora biti potrjena v %{period}, zahtevajte novo + expired: je potekla, zahtevajte novo + not_found: ni najdeno + not_locked: ni bil zaklenjen + not_saved: + few: "%{count} napake so preprečile shranjevanje %{resource}:" + one: '1 napaka je preprečila shranjevanje %{resource}:' + other: "%{count} napak je preprečilo shranjevanje %{resource}:" + two: "%{count} napaki sta preprečili shranjevanje %{resource}:" diff --git a/config/locales/devise.zh-CN.yml b/config/locales/devise.zh-CN.yml index 22fa130f60..f9943238e3 100644 --- a/config/locales/devise.zh-CN.yml +++ b/config/locales/devise.zh-CN.yml @@ -58,7 +58,7 @@ zh-CN: updated: 你的密码已修改成功,你现在已登录。 updated_not_active: 你的密码已修改成功。 registrations: - destroyed: 再见!你的帐户已成功注销。我们希望很快可以再见到你。 + destroyed: 再见!你的帐户已成功销毁。我们希望很快可以再见到你。 signed_up: 欢迎!你已注册成功。 signed_up_but_inactive: 你已注册,但尚未激活帐户。 signed_up_but_locked: 你已注册,但帐户被锁定了。 diff --git a/config/locales/doorkeeper.cy.yml b/config/locales/doorkeeper.cy.yml index 19798c4d9f..e29043e86f 100644 --- a/config/locales/doorkeeper.cy.yml +++ b/config/locales/doorkeeper.cy.yml @@ -114,6 +114,12 @@ cy: application: title: Mae awdurdodiad OAuth yn ofynnol scopes: + admin:read: darllenwch yr holl ddata ar y serfiwr + admin:read:accounts: darllen gwybodaeth sensitif o'r holl gyfrifon + admin:read:reports: darllen gwybodaeth sensitif am bob adroddiad a chyfrifon yr adroddir amdanynt + admin:write: addasu pob data ar y serfiwr + admin:write:accounts: cyflawni camau cymedroli ar gyfrifon + admin:write:reports: cyflawni camau cymedroli ar adroddiadau follow: addasu perthnasau cyfrif push: derbyn eich hysbysiadau gwthiadwy read: darllen holl ddata eich cyfrif diff --git a/config/locales/doorkeeper.es.yml b/config/locales/doorkeeper.es.yml index 752387d870..1b03e33f2f 100644 --- a/config/locales/doorkeeper.es.yml +++ b/config/locales/doorkeeper.es.yml @@ -114,7 +114,35 @@ es: application: title: OAuth autorización requerida scopes: + admin:read: leer todos los datos en el servidor + admin:read:accounts: leer información sensible de todas las cuentas + admin:read:reports: leer información sensible de todos los informes y cuentas reportadas + admin:write: modificar todos los datos en el servidor + admin:write:accounts: realizar acciones de moderación en cuentas + admin:write:reports: realizar acciones de moderación en informes follow: seguir, bloquear, desbloquear y dejar de seguir cuentas + push: recibir tus notificaciones push read: leer los datos de tu cuenta + read:accounts: ver información de cuentas + read:blocks: ver a quién has bloqueado + read:favourites: ver tus favoritos + read:filters: ver tus filtros + read:follows: ver a quién sigues + read:lists: ver tus listas + read:mutes: ver a quién has silenciado + read:notifications: ver tus notificaciones + read:reports: ver tus informes + read:search: buscar en su nombre + read:statuses: ver todos los estados write: publicar en tu nombre + write:accounts: modifica tu perfil write:blocks: bloquear cuentas y dominios + write:favourites: toots favoritos + write:filters: crear filtros + write:follows: seguir usuarios + write:lists: crear listas + write:media: subir archivos multimedia + write:mutes: silenciar usuarios y conversaciones + write:notifications: limpia tus notificaciones + write:reports: reportar a otras personas + write:statuses: publicar estados diff --git a/config/locales/doorkeeper.eu.yml b/config/locales/doorkeeper.eu.yml index f98babae65..70e52e8ad9 100644 --- a/config/locales/doorkeeper.eu.yml +++ b/config/locales/doorkeeper.eu.yml @@ -5,7 +5,7 @@ eu: doorkeeper/application: name: Aplikazioaren izena redirect_uri: Birbideratu URIa - scopes: Esparruak + scopes: Irismena website: Aplikazioaren webgunea errors: models: @@ -33,14 +33,14 @@ eu: help: native_redirect_uri: Erabili %{native_redirect_uri} proba lokaletarako redirect_uri: Erabili lerro bat URI bakoitzeko - scopes: Banandu esparruak espazioekin. Laga hutsik lehenetsitako esparruak erabiltzeko. + scopes: Banandu irismenak espazioekin. Laga hutsik lehenetsitako irismenak erabiltzeko. index: application: Aplikazioa callback_url: Itzulera URLa delete: Ezabatu name: Izena new: Aplikazio berria - scopes: Esparruak + scopes: Irismena show: Erakutsi title: Zure aplikazioak new: @@ -49,7 +49,7 @@ eu: actions: Ekintzak application_id: Bezeroaren gakoa callback_urls: Itzulera URL-ak - scopes: Esparruak + scopes: Irismena secret: Bezeroaren sekretua title: 'Aplikazioa: %{name}' authorizations: @@ -73,7 +73,7 @@ eu: application: Aplikazioa created_at: Baimenduta date_format: "%Y-%m-%d %H:%M:%S" - scopes: Esparruak + scopes: Irismena title: Zuk baimendutako aplikazioak errors: messages: @@ -114,6 +114,12 @@ eu: application: title: OAuth autorizazioa behar da scopes: + admin:read: zerbitzariko datu guztiak irakurri + admin:read:accounts: kontu guztien informazio sentsiblea irakurri + admin:read:reports: salaketa guztietako eta salatutako kontu guztietako informazio sentsiblea irakurri + admin:write: zerbitzariko datu guztiak aldatu + admin:write:accounts: kontuetan moderazio ekintzak burutu + admin:write:reports: salaketetan moderazio ekintzak burutu follow: aldatu kontuaren erlazioak push: jaso push jakinarazpenak read: irakurri zure kontuko datu guztiak diff --git a/config/locales/doorkeeper.hu.yml b/config/locales/doorkeeper.hu.yml index 122392864d..92b4e6839b 100644 --- a/config/locales/doorkeeper.hu.yml +++ b/config/locales/doorkeeper.hu.yml @@ -114,6 +114,12 @@ hu: application: title: OAuth engedély szükséges scopes: + admin:read: szerver minden adatának olvasása + admin:read:accounts: minden érzékeny fiókadat olvasása + admin:read:reports: minden bejelentés és bejelentett fiók érzékeny adatainak olvasása + admin:write: szerver minden adatának változtatása + admin:write:accounts: moderációs műveletek végzése fiókokon + admin:write:reports: moderációs műveletek végzése bejelentéseken follow: fiókok követése, letiltása, tiltás feloldása és követés abbahagyása push: push értesítések fogadása read: fiókod adatainak olvasása diff --git a/config/locales/doorkeeper.nl.yml b/config/locales/doorkeeper.nl.yml index aa37ea190a..1fabfc1239 100644 --- a/config/locales/doorkeeper.nl.yml +++ b/config/locales/doorkeeper.nl.yml @@ -114,6 +114,12 @@ nl: application: title: OAuth-autorisatie vereist scopes: + admin:read: lees alle gegevens op de server + admin:read:accounts: lees gevoelige informatie van alle accounts + admin:read:reports: lees gevoelige informatie van alle rapportages en gerapporteerde accounts + admin:write: wijzig alle gegevens op de server + admin:write:accounts: moderatieacties op accounts uitvoeren + admin:write:reports: moderatieacties op rapportages uitvoeren follow: relaties tussen accounts bewerken push: ontvang jouw pushmeldingen read: alle gegevens van jouw account lezen diff --git a/config/locales/doorkeeper.oc.yml b/config/locales/doorkeeper.oc.yml index d97c2f600e..e715cc7d52 100644 --- a/config/locales/doorkeeper.oc.yml +++ b/config/locales/doorkeeper.oc.yml @@ -114,6 +114,12 @@ oc: application: title: Cal una autorizacion OAuth scopes: + admin:read: lectura de totas las donadas del servidor + admin:read:accounts: lectura de las informacions sensiblas dels comptes + admin:read:reports: lectura de las informacions sensiblas dels senhalaments e dels comptes senhalats + admin:write: modificacion de las donadas del servidor + admin:write:accounts: realizacion d’accions de moderacion suls comptes + admin:write:reports: realizacion d’accions suls senhalaments follow: modificar las relacions del compte push: recebre vòstras notificacions push read: legir totas las donadas de vòstre compte diff --git a/config/locales/doorkeeper.sk.yml b/config/locales/doorkeeper.sk.yml index f54eb6d48d..9eaef177fb 100644 --- a/config/locales/doorkeeper.sk.yml +++ b/config/locales/doorkeeper.sk.yml @@ -19,34 +19,34 @@ sk: doorkeeper: applications: buttons: - authorize: Overiť - cancel: Zrušiť + authorize: Autorizuj + cancel: Zruš destroy: Zničiť - edit: Upraviť - submit: Poslať + edit: Uprav + submit: Pošli confirmations: destroy: Si si istý/á? edit: - title: Upraviť aplikáciu + title: Uprav aplikáciu form: - error: No teda! Pozrite formulár pre prípadné chyby + error: No teda! Skontroluj formulár pre prípadné chyby help: - native_redirect_uri: Použite %{native_redirect_uri} pre lokálne testy - redirect_uri: Iba jedna URI na riadok - scopes: Oprávnenia oddeľujte medzerami. Nechajte prázdne pre štandardné oprávnenia. + native_redirect_uri: Použi %{native_redirect_uri} pre lokálne testy + redirect_uri: Použi jeden riadok pre každú URI + scopes: Oprávnenia oddeľuj medzerami. Nechaj prázdne pre štandardné oprávnenia. index: application: Aplikácia callback_url: Návratová URL - delete: Zmazať + delete: Vymaž name: Názov new: Nová aplikácia scopes: Oprávnenia - show: Ukázať - title: Vaše aplikácie + show: Ukáž + title: Tvoje aplikácie new: title: Nová aplikácia show: - actions: Akcie + actions: Úkony application_id: Kľúč klienta callback_urls: Návratové URL adresy scopes: Oprávnenia @@ -54,7 +54,7 @@ sk: title: 'Aplikácia: %{name}' authorizations: buttons: - authorize: Overiť + authorize: Over deny: Zamietni error: title: Nastala chyba @@ -91,7 +91,7 @@ sk: resource_owner_authenticator_not_configured: Resource Owner zlyhal pretože Doorkeeper.configure.resource_owner_authenticator nebol nakonfigurovaný. server_error: Nastala neočakávaná chyba na autorizačnom serveri ktorá zabránila vykonať požiadavku. temporarily_unavailable: Autorizačný server ťa teraz nemôže obslúžiť, pretože prebieha údržba alebo je dočasne preťažený. - unauthorized_client: Klient nie je autorizovaný vykonať danú požiadavku takouto metódou. + unauthorized_client: Klient nie je autorizovaný vykonať danú požiadavku týmto spôsobom. unsupported_grant_type: Tento typ oprávnenia nie je podporovaný autorizačným serverom. unsupported_response_type: Autorizačný server nepodporuje typ tejto odpovede. flash: @@ -113,6 +113,11 @@ sk: application: title: Požadovaná OAuth autorizácia scopes: + admin:read: prezeraj všetky dáta na serveri + admin:read:accounts: prezeraj chúlostivé informácie na všetkých účtoch + admin:write: uprav všetky dáta na serveri + admin:write:accounts: urob moderovacie úkony na účtoch + admin:write:reports: urob moderovacie úkony voči hláseniam follow: uprav vzťahy svojho účtu push: dostávaj oboznámenia ohľadom tvojho účtu na obrazovku read: prezri si všetky dáta ohľadom svojho účetu diff --git a/config/locales/doorkeeper.zh-CN.yml b/config/locales/doorkeeper.zh-CN.yml index dd9337904a..015d2c0ce1 100644 --- a/config/locales/doorkeeper.zh-CN.yml +++ b/config/locales/doorkeeper.zh-CN.yml @@ -72,6 +72,7 @@ zh-CN: index: application: 应用 created_at: 授权时间 + date_format: "%H:%M:%S" scopes: 权限范围 title: 已授权的应用列表 errors: diff --git a/config/locales/el.yml b/config/locales/el.yml index a08ec71416..21b0da25c4 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -469,13 +469,6 @@ el: no_status_selected: Καμία δημοσίευση δεν άλλαξε αφού καμία δεν ήταν επιλεγμένη title: Καταστάσεις λογαριασμού with_media: Με πολυμέσα - subscriptions: - callback_url: URL επιστροφής (Callback) - confirmed: Επιβεβαιωμένες - expires_in: Λήγει σε - last_delivery: Τελευταία παράδοση - title: Πρωτόκολλο WebSub - topic: Θέμα tags: accounts: Λογαριασμοί hidden: Κρυμμένες @@ -816,10 +809,6 @@ el: reply: proceed: Συνέχισε για να απαντήσεις prompt: 'Θέλεις να απαντήσεις σε αυτό το τουτ:' - remote_unfollow: - error: Σφάλμα - title: Τίτλος - unfollowed: Σταμάτησες να ακολουθείς scheduled_statuses: over_daily_limit: Έχεις υπερβεί το όριο των %{limit} προγραμματισμένων τουτ για εκείνη τη μέρα over_total_limit: Έχεις υπερβεί το όριο των %{limit} προγραμματισμένων τουτ diff --git a/config/locales/eo.yml b/config/locales/eo.yml index c71b42fdd8..de28be0108 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -174,6 +174,7 @@ eo: statuses: Mesaĝoj subscribe: Aboni suspended: Haltigita + time_in_queue: Atendado en atendovico %{time} title: Kontoj unconfirmed_email: Nekonfirmita retadreso undo_silenced: Malfari kaŝon @@ -334,6 +335,8 @@ eo: expired: Eksvalida title: Filtri title: Invitoj + pending_accounts: + title: Pritraktataj kontoj (%{count}) relays: add_new: Aldoni novan ripetilon delete: Forigi @@ -465,13 +468,6 @@ eo: no_status_selected: Neniu mesaĝo estis ŝanĝita ĉar neniu estis elektita title: Mesaĝoj de la konto with_media: Kun aŭdovidaĵoj - subscriptions: - callback_url: Revena URL - confirmed: Konfirmita - expires_in: Eksvalidiĝas je - last_delivery: Lasta livero - title: WebSub - topic: Temo tags: accounts: Kontoj hidden: Kaŝitaj @@ -780,7 +776,10 @@ eo: too_many_options: ne povas enhavi pli da %{max} proponoj preferences: other: Aliaj aferoj + posting_defaults: Afiŝadoj defaŭltoj + public_timelines: Publikaj templinioj relationships: + activity: Konto aktiveco dormant: Dormanta last_active: Lasta aktiva most_recent: Plej lasta @@ -788,6 +787,9 @@ eo: mutual: Reciproka primary: Primara relationship: Rilato + remove_selected_domains: Forigi ĉiuj sekvantojn el la selektitajn domajnojn + remove_selected_followers: Forigi selektitajn sekvantojn + remove_selected_follows: Malsekvi selektitajn uzantojn status: Statuso de la konto remote_follow: acct: Enmetu vian uzantnomo@domajno de kie vi volas agi @@ -806,10 +808,6 @@ eo: reply: proceed: Konfirmi la respondon prompt: 'Vi volas respondi al ĉi tiu mesaĝo:' - remote_unfollow: - error: Eraro - title: Titolo - unfollowed: Ne plu sekvita scheduled_statuses: over_daily_limit: Vi transpasis la limigon al %{limit} samtage planitaj mesaĝoj over_total_limit: Vi transpasis la limigon al %{limit} planitaj mesaĝoj diff --git a/config/locales/es.yml b/config/locales/es.yml index 49765cd0a3..d6adf4062f 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -5,9 +5,13 @@ es: about_mastodon_html: Mastodon es un servidor de red social libre y de código abierto. Una alternativa descentralizada a plataformas comerciales, que evita el riesgo de que una única compañía monopolice tu comunicación. Cualquiera puede ejecutar Mastodon y participar sin problemas en la red social. about_this: Acerca de esta instancia active_count_after: activo + active_footnote: Usuarios Activos Mensuales (UAM) administered_by: 'Administrado por:' api: API apps: Aplicaciones móviles + apps_platforms: Utiliza Mastodon desde iOS, Android y otras plataformas + browse_directory: Navega por el directorio de perfiles y filtra por intereses + browse_public_posts: Navega por un transmisión en vivo de publicaciones públicas en Mastodon contact: Contacto contact_missing: No especificado contact_unavailable: N/A @@ -111,6 +115,7 @@ es: inbox_url: URL de la bandeja de entrada invited_by: Invitado por ip: IP + joined: Unido location: all: Todos local: Local @@ -245,6 +250,7 @@ es: feature_profile_directory: Directorio de perfil feature_registrations: Registros feature_relay: Relés de federación + feature_timeline_preview: Vista previa de la línea de tiempo features: Características hidden_service: Federación con servicios ocultos open_reports: informes abiertos @@ -279,6 +285,7 @@ es: reject_reports: Rechazar informes reject_reports_hint: Ignore todos los reportes de este dominio. Irrelevante para suspensiones rejecting_media: rechazar archivos multimedia + rejecting_reports: rechazando informes severity: silence: silenciado suspend: susependido @@ -304,8 +311,13 @@ es: title: Lista negra de correo followers: back_to_account: Volver a la cuenta + title: Seguidores de %{acct} instances: by_domain: Dominio + delivery_available: Entrega disponible + known_accounts: + one: "%{count} cuenta conocida" + other: "%{count} cuentas conocidas" moderation: all: Todos limited: Limitado @@ -314,6 +326,7 @@ es: total_blocked_by_us: Bloqueado por nosotros total_followed_by_them: Seguidos por ellos total_followed_by_us: Seguido por nosotros + total_reported: Informes sobre ellas total_storage: Archivos multimedia invites: deactivate_all: Desactivar todos @@ -323,6 +336,8 @@ es: expired: Expiradas title: Filtrar title: Invitaciones + pending_accounts: + title: Cuentas pendientes (%{count}) relays: add_new: Añadir un nuevo relés delete: Borrar @@ -396,6 +411,9 @@ es: preview_sensitive_media: desc_html: Los enlaces de vistas previas en otras web mostrarán una miniatura incluso si el medio está marcado como contenido sensible title: Mostrar contenido sensible en previews de OpenGraph + profile_directory: + desc_html: Permitir que los usuarios puedan ser descubiertos + title: Habilitar directorio de perfiles registrations: closed_message: desc_html: Se muestra en la portada cuando los registros están cerrados. Puedes usar tags HTML @@ -408,8 +426,10 @@ es: title: Permitir invitaciones de registrations_mode: modes: + approved: Se requiere aprobación para registrarse none: Nadie puede registrarse open: Cualquiera puede registrarse + title: Modo de registros show_known_fediverse_at_about_page: desc_html: Cuando esté activado, se mostrarán toots de todo el fediverso conocido en la vista previa. En otro caso, se mostrarán solamente toots locales. title: Mostrar fediverso conocido en la vista previa de la historia @@ -449,26 +469,38 @@ es: no_status_selected: No se cambió ningún estado al no seleccionar ninguno title: Estado de las cuentas with_media: Con multimedia - subscriptions: - callback_url: URL del callback - confirmed: Confirmado - expires_in: Expira en - last_delivery: Última entrega - topic: Tópico + tags: + accounts: Cuentas + hidden: Oculto + hide: Ocultar del directorio + name: Etiqueta + title: Etiquetas + unhide: Mostrar en el directorio + visible: Visible title: Administración warning_presets: add_new: Añadir nuevo delete: Borrar edit: Editar + edit_preset: Editar aviso predeterminado + title: Editar configuración predeterminada de avisos admin_mailer: new_pending_account: body: Los detalles de la nueva cuenta están abajos. Puedes aprobar o rechazar esta aplicación. + subject: Nueva cuenta para revisión en %{instance} (%{username}) new_report: body: "%{reporter} ha reportado a %{target}" body_remote: Alguien de %{domain} a reportado a %{target} subject: Nuevo reporte para la %{instance} (#%{id}) + appearance: + advanced_web_interface: Interfaz web avanzada + advanced_web_interface_hint: 'Si desea utilizar todo el ancho de pantalla, la interfaz web avanzada le permite configurar varias columnas diferentes para ver tanta información al mismo tiempo como quiera: Inicio, notificaciones, línea de tiempo federada, cualquier número de listas y etiquetas.' + animations_and_accessibility: Animaciones y accesibilidad + confirmation_dialogs: Diálogos de confirmación + sensitive_content: Contenido sensible application_mailer: notification_preferences: Cambiar preferencias de correo electrónico + salutation: "%{name}," settings: 'Cambiar preferencias de correo: %{link}' view: 'Vista:' view_profile: Ver perfil @@ -482,6 +514,7 @@ es: warning: Ten mucho cuidado con estos datos. ¡No los compartas con nadie! your_token: Tu token de acceso auth: + apply_for_account: Solicitar una invitación change_password: Contraseña checkbox_agreement_html: Acepto las reglas del servidor y términos de servicio confirm_email: Confirmar email @@ -495,12 +528,16 @@ es: migrate_account: Mudarse a otra cuenta migrate_account_html: Si deseas redireccionar esta cuenta a otra distinta, puedes configurarlo aquí. or_log_in_with: O inicia sesión con + providers: + cas: CAS + saml: SAML register: Registrarse registration_closed: "%{instance} no está aceptando nuevos miembros" resend_confirmation: Volver a enviar el correo de confirmación reset_password: Restablecer contraseña security: Cambiar contraseña set_new_password: Establecer nueva contraseña + trouble_logging_in: "¿Problemas para iniciar sesión?" authorize_follow: already_following: Ya estás siguiendo a esta cuenta error: Desafortunadamente, ha ocurrido un error buscando la cuenta remota @@ -514,10 +551,18 @@ es: title: Seguir a %{acct} datetime: distance_in_words: + about_x_hours: "%{count}h" about_x_months: "%{count}m" + about_x_years: "%{count}a" + almost_x_years: "%{count}a" half_a_minute: Justo ahora + less_than_x_minutes: "%{count}m" less_than_x_seconds: Justo ahora + over_x_years: "%{count}a" + x_days: "%{count}d" + x_minutes: "%{count}m" x_months: "%{count}m" + x_seconds: "%{count}s" deletes: bad_password_msg: "¡Buen intento, hackers! Contraseña incorrecta" confirm_password: Ingresa tu contraseña actual para demostrar tu identidad @@ -527,6 +572,10 @@ es: warning_html: Se garantiza únicamente la eliminación del contenido de esta instancia. El contenido que se haya compartido extensamente dejará sus huellas. Los servidores fuera de línea y los que se hayan desuscrito de tus actualizaciones ya no actualizarán sus bases de datos. warning_title: Disponibilidad diseminada del contenido directories: + directory: Directorio de perfiles + enabled: Actualmente está listado en el directorio. + enabled_but_waiting: Ha optado por ser listado en el directorio, pero aún no cumple con el número mínimo de seguidores (%{min_followers}) para ser listado. + explanation: Descubre usuarios según sus intereses explore_mastodon: Explorar %{title} how_to_enable: Usted no está registrado por el directorio. Puede registrar por abajo. ¡Utilice hashtags en su bio para aparecer bajo hashtags específicos! people: @@ -557,6 +606,7 @@ es: size: Tamaño blocks: Personas que has bloqueado csv: CSV + domain_blocks: Bloqueos de dominios follows: Personas que sigues lists: Listas mutes: Tienes en silencio @@ -594,6 +644,8 @@ es: validation_errors: one: "¡Algo no está bien! Por favor, revisa el error" other: "¡Algo no está bien! Por favor, revise %{count} errores más abajo" + html_validator: + invalid_markup: 'contiene código HTML no válido: %{error}' identity_proofs: active: Activo authorize: Sí, autorizar @@ -603,18 +655,26 @@ es: keybase: invalid_token: Los tokens de Keybase son hashes de firmas y deben tener 66 caracteres hex verification_failed: Keybase no reconoce este token como una firma del usuario de Keybase %{kb_username}. Por favor, inténtelo de nuevo desde Keybase. + wrong_user: No se puede crear una prueba para %{proving} mientras se inicia sesión como %{current}. Inicia sesión como %{proving} e inténtalo de nuevo. + explanation_html: Aquí puedes conectar criptográficamente sus otras identidades, como un perfil de Keybase. Esto permite a otras personas enviarle mensajes encriptados y confiar en el contenido que les envías. + i_am_html: Soy %{username} en %{service}. identity: Identidad inactive: Inactivo + publicize_checkbox: 'Y tootee esto:' + publicize_toot: "¡Comprobado! Soy %{username} en %{service}: %{url}" status: Estado de la verificación view_proof: Ver prueba imports: modes: merge: Unir + merge_long: Mantener registros existentes y añadir nuevos overwrite: Sobrescribir + overwrite_long: Reemplazar registros actuales con los nuevos preface: Puedes importar ciertos datos, como todas las personas que estás siguiendo o bloqueando en tu cuenta en esta instancia, desde archivos exportados de otra instancia. success: Sus datos se han cargado correctamente y serán procesados en brevedad types: blocking: Lista de bloqueados + domain_blocking: Lista de dominios bloqueados following: Lista de seguidos muting: Lista de silenciados upload: Cargar @@ -689,26 +749,70 @@ es: body: "%{name} ha retooteado tu estado:" subject: "%{name} ha retooteado tu estado" title: Nueva difusión + number: + human: + decimal_units: + format: "%n%u" + units: + billion: B + million: M + quadrillion: Q + thousand: m + trillion: T pagination: newer: Más nuevo next: Próximo older: Más antiguo prev: Anterior + truncate: "…" + polls: + errors: + already_voted: Ya has votado en esta encuesta + duplicate_options: contiene elementos duplicados + duration_too_long: está demasiado lejos en el futuro + duration_too_short: es demasiado pronto + expired: La encuesta ya ha terminado + over_character_limit: no puede exceder %{max} caracteres cada uno + too_few_options: debe tener más de un elemento + too_many_options: no puede contener más de %{max} elementos preferences: other: Otros + posting_defaults: Configuración por defecto de publicaciones + public_timelines: Líneas de tiempo públicas relationships: + activity: Actividad de la cuenta + dormant: Inactivo last_active: Última actividad most_recent: Más reciente + moved: Movido + mutual: Mutuo + primary: Principal + relationship: Relación + remove_selected_domains: Eliminar todos los seguidores de los dominios seleccionados + remove_selected_followers: Eliminar los seguidores seleccionados + remove_selected_follows: Dejar de seguir a los usuarios seleccionados + status: Estado de la cuenta remote_follow: acct: Ingesa tu usuario@dominio desde el que quieres seguir missing_resource: No se pudo encontrar la URL de redirección requerida para tu cuenta no_account_html: "¿No tienes una cuenta? Puedes registrarte aqui" proceed: Proceder a seguir prompt: 'Vas a seguir a:' - remote_unfollow: - error: Error - title: Título - unfollowed: Ha dejado de seguirse + reason_html: "¿¿Por qué es necesario este paso? %{instance} puede que no sea el servidor donde estás registrado, así que necesitamos redirigirte primero a tu servidor de origen." + remote_interaction: + favourite: + proceed: Proceder a marcar como favorito + prompt: 'Quieres marcar como favorito este toot:' + reblog: + proceed: Proceder a retootear + prompt: 'Quieres retootear este toot:' + reply: + proceed: Proceder a responder + prompt: 'Quieres responder a este toot:' + scheduled_statuses: + over_daily_limit: Ha superado el límite de %{limit} toots programados para ese día + over_total_limit: Ha superado el límite de %{limit} toots programados + too_soon: La fecha programada debe estar en el futuro sessions: activity: Última actividad browser: Navegador @@ -761,10 +865,14 @@ es: edit_profile: Editar perfil export: Exportar información featured_tags: Hashtags destacados + identity_proofs: Pruebas de identidad import: Importar + import_and_export: Importar y exportar migrate: Migración de cuenta notifications: Notificaciones preferences: Preferencias + profile: Perfil + relationships: Siguiendo y seguidores two_factor_authentication: Autenticación de dos factores statuses: attached: @@ -788,8 +896,14 @@ es: ownership: El toot de alguien más no puede fijarse private: Los toots no-públicos no pueden fijarse reblog: Un boost no puede fijarse + poll: + total_votes: + one: "%{count} voto" + other: "%{count} votos" + vote: Vota show_more: Mostrar más sign_in_to_participate: Regístrate para participar en la conversación + title: '%{name}: "%{quote}"' visibilities: private: Sólo mostrar a seguidores private_long: Solo mostrar a tus seguidores @@ -802,6 +916,87 @@ es: reblogged: retooteado sensitive_content: Contenido sensible terms: + body_html: | +

Política de Privacidad

+

¿Qué información recogemos?

+ +
    +
  • Información básica sobre su cuenta: Si se registra en este servidor, se le requerirá un nombre de usuario, una dirección de correo electrónico y una contraseña. Además puede incluir información adicional en el perfil como un nombre de perfil y una biografía, y subir una foto de perfil y una imagen de cabecera. El nombre de usuario, nombre de perfil, biografía, foto de perfil e imagen de cabecera siempre son visibles públicamente
  • +
  • Publicaciones, seguimiento y otra información pública: La lista de gente a la que sigue es mostrada públicamente, al igual que sus seguidores. Cuando publica un mensaje, la fecha y hora es almacenada, así como la aplicación desde la cual publicó el mensaje. Los mensajes pueden contener archivos adjuntos multimedia, como imágenes y vídeos. Las publicaciones públicas y no listadas están disponibles públicamente. Cuando destaca una entrada en su perfil, también es información disponible públicamente. Sus publicaciones son entregadas a sus seguidores, en algunos casos significa que son entregadas a diferentes servidores y las copias son almacenadas allí. Cuando elimina publicaciones, esto también se transfiere a sus seguidores. La acción de rebloguear o marcar como favorito otra publicación es siempre pública.
  • +
  • Publicaciones directas y sólo para seguidores: Todos los mensajes se almacenan y procesan en el servidor. Los mensajes sólo para seguidores se entregan a los seguidores y usuarios que se mencionan en ellos, y los mensajes directos se entregan sólo a los usuarios que se mencionan en ellos. En algunos casos significa que se entregan a diferentes servidores y que las copias se almacenan allí. Hacemos un esfuerzo de buena fe para limitar el acceso a esas publicaciones sólo a las personas autorizadas, pero otros servidores pueden no hacerlo. Por lo tanto, es importante revisar los servidores a los que pertenecen sus seguidores. Puede cambiar una opción para aprobar y rechazar nuevos seguidores manualmente en la configuración Por favor, tenga en cuenta que los operadores del servidor y de cualquier servidor receptor pueden ver dichos mensajes, y que los destinatarios pueden capturarlos, copiarlos o volver a compartirlos de alguna otra manera. No comparta ninguna información peligrosa en Mastodon.
  • +
  • Direcciones IP y otros metadatos: Al iniciar sesión, registramos la dirección IP desde la que se ha iniciado sesión, así como el nombre de la aplicación de su navegador. Todas las sesiones iniciadas están disponibles para su revisión y revocación en los ajustes. La última dirección IP utilizada se almacena hasta 12 meses. También podemos conservar los registros del servidor que incluyen la dirección IP de cada solicitud a nuestro servidor.
  • +
+ +
+ +

¿Para qué utilizamos su información?

+ +

Toda la información que obtenemos de usted puede ser utilizada de las siguientes maneras:

+ +
    +
  • Para proporcionar la funcionalidad principal de Mastodon. Sólo puedes interactuar con el contenido de otras personas y publicar tu propio contenido cuando estés conectado. Por ejemplo, puedes seguir a otras personas para ver sus mensajes combinados en tu propia línea de tiempo personalizada.
  • +
  • Para ayudar a la moderación de la comunidad, por ejemplo, comparando su dirección IP con otras conocidas para determinar la evasión de prohibiciones u otras violaciones.
  • +
  • La dirección de correo electrónico que nos proporcione podrá utilizarse para enviarle información, notificaciones sobre otras personas que interactúen con su contenido o para enviarle mensajes, así como para responder a consultas y/u otras solicitudes o preguntas.
  • +
+ +
+ +

¿Cómo protegemos su información?

+ +

Implementamos una variedad de medidas de seguridad para mantener la seguridad de su información personal cuando usted ingresa, envía o accede a su información personal. Entre otras cosas, la sesión de su navegador, así como el tráfico entre sus aplicaciones y la API, están protegidos con SSL, y su contraseña está protegida mediante un algoritmo unidireccional fuerte. Puede habilitar la autenticación de dos factores para un acceso más seguro a su cuenta.

+ +
+ +

¿Cuál es nuestra política de retención de datos?

+ +

Haremos un esfuerzo de buena fe para:

+ +
    +
  • Conservar los registros del servidor que contengan la dirección IP de todas las peticiones a este servidor, en la medida en que se mantengan dichos registros, no más de 90 días.
  • +
  • Conservar las direcciones IP asociadas a los usuarios registrados no más de 12 meses.
  • +
+ +

Puede solicitar y descargar un archivo de su contenido, incluidos sus mensajes, archivos adjuntos multimedia, foto de perfil e imagen de cabecera.

+ +

Usted puede borrar su cuenta de forma irreversible en cualquier momento.

+ +
+ +

¿Utilizamos cookies?

+ +

Sí. Las cookies son pequeños archivos que un sitio o su proveedor de servicios transfiere al disco duro de su ordenador a través de su navegador web (si usted lo permite). Estas cookies permiten al sitio reconocer su navegador y, si tiene una cuenta registrada, asociarla con su cuenta registrada.

+ +

Utilizamos cookies para entender y guardar sus preferencias para futuras visitas.

+ +
+ +

¿Revelamos alguna información a terceros?

+ +

No vendemos, comerciamos ni transferimos a terceros su información personal identificable. Esto no incluye a los terceros de confianza que nos asisten en la operación de nuestro sitio, en la realización de nuestros negocios o en la prestación de servicios, siempre y cuando dichas partes acuerden mantener la confidencialidad de esta información. También podemos divulgar su información cuando creamos que es apropiado para cumplir con la ley, hacer cumplir las políticas de nuestro sitio, o proteger nuestros u otros derechos, propiedad o seguridad.

+ +

Su contenido público puede ser descargado por otros servidores de la red. Tus mensajes públicos y sólo para seguidores se envían a los servidores donde residen tus seguidores, y los mensajes directos se envían a los servidores de los destinatarios, en la medida en que dichos seguidores o destinatarios residan en un servidor diferente.

+ +

Cuando usted autoriza a una aplicación a usar su cuenta, dependiendo del alcance de los permisos que usted apruebe, puede acceder a la información de su perfil público, su lista de seguimiento, sus seguidores, sus listas, todos sus mensajes y sus favoritos. Las aplicaciones nunca podrán acceder a su dirección de correo electrónico o contraseña.

+ +
+ +

Uso del sitio por parte de los niños

+ +

Si este servidor está en la UE o en el EEE: Nuestro sitio, productos y servicios están dirigidos a personas mayores de 16 años. Si es menor de 16 años, según los requisitos de la GDPR (General Data Protection Regulation) no utilice este sitio.

+ +

Si este servidor está en los EE.UU.: Nuestro sitio, productos y servicios están todos dirigidos a personas que tienen al menos 13 años de edad. Si usted es menor de 13 años, según los requisitos de COPPA (Children's Online Privacy Protection Act) no utilice este sitio.

+ +

Los requisitos legales pueden ser diferentes si este servidor está en otra jurisdicción.

+ +
+ +

Cambios en nuestra Política de Privacidad

+ +

Si decidimos cambiar nuestra política de privacidad, publicaremos esos cambios en esta página.

+ +

Este documento es CC-BY-SA. Fue actualizado por última vez el 7 de marzo de 2018.

+ +

Adaptado originalmente desde la política de privacidad de Discourse.

title: Términos del Servicio y Políticas de Privacidad de %{instance} themes: contrast: Alto contraste @@ -810,6 +1005,7 @@ es: time: formats: default: "%d de %b del %Y, %H:%M" + month: "%b %Y" two_factor_authentication: code_hint: Ingresa el código generado por tu aplicación de autenticación para confirmar description_html: Si habilitas la autenticación de dos factores, se requerirá estar en posesión de su teléfono, lo que generará tokens para que usted pueda iniciar sesión. @@ -831,6 +1027,22 @@ es: explanation: Has solicitado una copia completa de tu cuenta de Mastodon. ¡Ya está preparada para descargar! subject: Tu archivo está preparado para descargar title: Descargar archivo + warning: + explanation: + disable: Mientras su cuenta esté congelada, la información de su cuenta permanecerá intacta, pero no puede realizar ninguna acción hasta que se desbloquee. + silence: Mientras su cuenta está limitada, sólo las personas que ya le están siguiendo verán sus toots en este servidor, y puede que se le excluya de varios listados públicos. Sin embargo, otros pueden seguirle manualmente. + suspend: Su cuenta ha sido suspendida, y todos tus toots y tus archivos multimedia subidos han sido irreversiblemente eliminados de este servidor, y de los servidores donde tenías seguidores. + review_server_policies: Revisar las políticas del servidor + subject: + disable: Su cuenta %{acct} ha sido congelada + none: Advertencia para %{acct} + silence: Su cuenta %{acct} ha sido limitada + suspend: Su cuenta %{acct} ha sido suspendida + title: + disable: Cuenta congelada + none: Advertencia + silence: Cuenta limitada + suspend: Cuenta suspendida welcome: edit_profile_action: Configurar el perfil edit_profile_step: Puedes personalizar tu perfil subiendo un avatar, una cabecera, cambiando tu nombre de usuario y más cosas. Si quieres revisar a tus nuevos seguidores antes de que se les permita seguirte, puedes bloquear tu cuenta. @@ -846,6 +1058,7 @@ es: tip_following: Sigues a tus administradores de servidor por defecto. Para encontrar más gente interesante, revisa las lineas de tiempo local y federada. tip_local_timeline: La linea de tiempo local is una vista de la gente en %{instance}. Estos son tus vecinos inmediatos! tip_mobile_webapp: Si el navegador de tu dispositivo móvil ofrece agregar Mastodon a tu página de inicio, puedes recibir notificaciones. Actúa como una aplicación nativa en muchas formas! + tips: Consejos title: Te damos la bienvenida a bordo, %{name}! users: follow_limit_reached: No puedes seguir a más de %{limit} personas diff --git a/config/locales/eu.yml b/config/locales/eu.yml index 9b9c2c0271..d3299d7756 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -61,8 +61,8 @@ eu: posts: one: Toot other: Toot - posts_tab_heading: Toot - posts_with_replies: Toot eta erantzunak + posts_tab_heading: Toot-ak + posts_with_replies: Toot-ak eta erantzunak reserved_username: Erabiltzaile-izena erreserbatuta dago roles: admin: Administratzailea @@ -431,7 +431,7 @@ eu: open: Edonork eman dezake izena title: Erregistratzeko modua show_known_fediverse_at_about_page: - desc_html: Txandakatzean, fedibertsu ezagun osoko toot-ak bistaratuko ditu aurrebistan. Bestela, toot lokalak besterik ez ditu erakutsiko. + desc_html: Txandakatzean, fedibertso ezagun osoko toot-ak bistaratuko ditu aurrebistan. Bestela, toot lokalak besterik ez ditu erakutsiko. title: Erakutsi fedibertsu ezagun osoko denbora-lerroa aurrebistan show_staff_badge: desc_html: Erakutsi langile banda erabiltzailearen orrian @@ -469,13 +469,6 @@ eu: no_status_selected: Ez da mezurik aldatu ez delako mezurik aukeratu title: Kontuaren mezuak with_media: Multimediarekin - subscriptions: - callback_url: Itzulera URL-a - confirmed: Berretsita - expires_in: Iraungitzea - last_delivery: Azken bidalketa - title: WebSub - topic: Mintzagaia tags: accounts: Kontuak hidden: Ezkutatuta @@ -783,7 +776,7 @@ eu: too_few_options: elementu bat baino gehiago izan behar du too_many_options: ezin ditu %{max} elementu baino gehiago izan preferences: - other: Beste bat + other: Denetarik posting_defaults: Bidalketarako lehenetsitakoak public_timelines: Denbora-lerro publikoak relationships: @@ -816,10 +809,6 @@ eu: reply: proceed: Ekin erantzuteari prompt: 'Toot honi erantzun nahi diozu:' - remote_unfollow: - error: Errorea - title: Izenburua - unfollowed: Jarraitzeari utzita scheduled_statuses: over_daily_limit: Egun horretarako programatutako toot kopuruaren muga gainditu duzu (%{limit}) over_total_limit: Programatutako toot kopuruaren muga gainditu duzu (%{limit}) diff --git a/config/locales/fa.yml b/config/locales/fa.yml index d37dbdeb49..f6b6c87583 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -469,13 +469,6 @@ fa: no_status_selected: هیچ بوقی تغییری نکرد زیرا هیچ‌کدام از آن‌ها انتخاب نشده بودند title: نوشته‌های حساب with_media: دارای عکس یا ویدیو - subscriptions: - callback_url: نشانی Callback - confirmed: تأییدشده - expires_in: مهلت انقضا - last_delivery: آخرین ارسال - title: WebSub - topic: موضوع tags: accounts: حساب‌ها hidden: پنهان‌شده @@ -816,10 +809,6 @@ fa: reply: proceed: به سمت پاسخ‌دادن prompt: 'شما می‌خواهید به این بوق پاسخ دهید:' - remote_unfollow: - error: خطا - title: عنوان - unfollowed: پایان پیگیری scheduled_statuses: over_daily_limit: شما از حد مجاز %{limit} بوق زمان‌بندی‌شده در آن روز فراتر رفته‌اید over_total_limit: شما از حد مجاز %{limit} بوق زمان‌بندی‌شده فراتر رفته‌اید diff --git a/config/locales/fi.yml b/config/locales/fi.yml index e0dc0f756e..07a8e367b2 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -330,12 +330,6 @@ fi: no_media: Ei mediaa title: Tilin tilat with_media: Sisältää mediaa - subscriptions: - callback_url: Paluu-URL - confirmed: Vahvistettu - expires_in: Vanhenee - last_delivery: Viimeisin toimitus - topic: Aihe title: Ylläpito admin_mailer: new_report: @@ -536,8 +530,6 @@ fi: missing_resource: Vaadittavaa uudelleenohjaus-URL:ää tiliisi ei löytynyt proceed: Siirry seuraamaan prompt: 'Olet aikeissa seurata:' - remote_unfollow: - error: Virhe sessions: activity: Viimeisin toiminta browser: Selain diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 5c15ab6a42..b3ee1d3bde 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -469,13 +469,6 @@ fr: no_status_selected: Aucun statut n’a été modifié car aucun n’a été sélectionné title: État du compte with_media: avec médias - subscriptions: - callback_url: URL de rappel - confirmed: Confirmé - expires_in: Expire dans - last_delivery: Dernière livraison - title: WebSub - topic: Sujet tags: accounts: Comptes hidden: Masqué @@ -816,10 +809,6 @@ fr: reply: proceed: Confirmer la réponse prompt: 'Vous souhaitez répondre à ce pouet :' - remote_unfollow: - error: Erreur - title: Titre - unfollowed: Non-suivi scheduled_statuses: over_daily_limit: Vous avez dépassé la limite de %{limit} pouets planifiés pour ce jour over_total_limit: Vous avez dépassé la limite de %{limit} pouets planifiés diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 79ef993e2e..7ecb50e408 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -469,13 +469,6 @@ gl: no_status_selected: Non se cambiou ningún estado xa que ningún foi seleccionado title: Estados da conta with_media: con medios - subscriptions: - callback_url: URL de chamada - confirmed: Confirmado - expires_in: Caduca en - last_delivery: Última entrega - title: WebSub - topic: Asunto tags: accounts: Contas hidden: Ocultas @@ -816,10 +809,6 @@ gl: reply: proceed: Respostar prompt: 'Vostede quere respostar a este toot:' - remote_unfollow: - error: Fallo - title: Título - unfollowed: Deixou de seguir scheduled_statuses: over_daily_limit: Excedeu o límite de %{limit} toots programados para ese día over_total_limit: Excedeu o límite de %{limit} toots programados diff --git a/config/locales/he.yml b/config/locales/he.yml index 5e50f738df..12953c2234 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -177,13 +177,6 @@ he: title: תיאור אתר מורחב site_title: כותרת האתר title: הגדרות אתר - subscriptions: - callback_url: קישורית Callback - confirmed: מאושר - expires_in: פג תוקף ב- - last_delivery: משלוח אחרון - title: מנוי WebSub - topic: נושא title: ניהול application_mailer: settings: 'שינוי הגדרות דוא"ל: %{link}' diff --git a/config/locales/hu.yml b/config/locales/hu.yml index d771b96832..948f0db13c 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -10,7 +10,7 @@ hu: api: API apps: Mobil appok apps_platforms: Használd a Mastodont iOS-ről, Androidról vagy más platformról - browse_directory: Böngészd a profil adatbázist és szűrj érdeklődési kör szerint + browse_directory: Böngészd a profilokat és szűrj érdeklődési körre browse_public_posts: Nézz bele a Mastodon élő adatfolyamába contact: Kapcsolat contact_missing: Nincs megadva @@ -469,13 +469,6 @@ hu: no_status_selected: Nem változtattunk meg semmit, mert semmi sem volt kiválasztva title: Felhasználó tülkjei with_media: Médiafájlokkal - subscriptions: - callback_url: Callback URL - confirmed: Megerősítve - expires_in: Elévül - last_delivery: Utolsó kézbesítés - title: WebSub - topic: Téma tags: accounts: Fiókok hidden: Rejtett @@ -816,10 +809,6 @@ hu: reply: proceed: Válaszadás prompt: 'Erre a tülkre szeretnél válaszolni:' - remote_unfollow: - error: Hiba - title: Cím - unfollowed: Nem követett scheduled_statuses: over_daily_limit: Túllépted az időzített tülkökre vonatkozó napi limitet (%{limit}) over_total_limit: Túllépted az időzített tülkökre vonatkozó limitet (%{limit}) @@ -924,7 +913,7 @@ hu: unlisted_long: Mindenki látja, de a nyilvános idővonalakon nem jelenik meg stream_entries: pinned: Kitűzött tülk - reblogged: megtolt + reblogged: megtolta sensitive_content: Szenzitív tartalom terms: body_html: | @@ -932,17 +921,17 @@ hu:

Milyen adatokat gyűjtünk?

    -
  • Alapvető fiókadatok: Ha regisztrálsz ezen a szerveren, kérhetünk tőled felhasználói nevet, e-mail címet és jelszót is. Megadhatsz magadról egyéb profil információt, mint megjelenítendő név, bemutatkozás, feltölthetsz profilképet, háttérképet. A felhasználói neved, megjelenítendő neved, bemutatkozásod, profil képed és háttér képed mindig nyilvánosak mindenki számára.
  • -
  • Tülkök (posztok), követések, más nyilvános adatok: Az általad követett emberek listája nyilvános. Ugyanez igaz a te követőidre is. Ha küldesz egy üzenetet, ennek az idejét eltároljuk azzal az alkalmazással együtt, melyből az üzenetet küldted. Az üzenetek tartalmazhatnak média csatolmányt, képeket, videókat. A nyilvános tülkök (posztok) bárki számára elérhetőek. Ha egy tülköt kiemelsz a profilodon, az is nyilvánossá válik. Amikor a tülkjeidet a követőidnek továbbítjuk, a poszt más szerverekre is kerülhet, melyeken így másolatok képződhetnek. Ha törölsz tülköket, ez is továbbítódik a követőid felé. A megtolás (reblog) és kedvencnek jelölés művelete is mindig nyilvános.
  • -
  • Közvetlen üzenetek és csak követőknek szánt tülkök: Minden tülk a szerveren tárolódik. A csak követőknek szánt tülköket a követőidnek és az ezekben megemlítetteknek továbbítjuk, míg a közvetlen üzeneteket kizárólag az ebben megemlítettek kapják. Néhány esetben ez azt jelenti, hogy ezek más szerverekre is továbbítódnak, így ott másolatok keletkezhetnek. Jóhiszeműen feltételezzük, hogy más szerverek is hasonlóan járnak el, mikor ezeket az üzeneteket csak az arra jogosultaknak mutatják meg. Ugyanakkor ez nem feltétlenül igaz. Ezért érdemes megnézni azokat a szervereket, melyeken követőid vannak. Be tudod állítani, hogy minden követési kérelmet jóvá kelljen hagynod. Tartsd észben, hogy a szerver üzemeltetői láthatják az üzeneteket, illetve a fogadók képernyőképet, másolatot készíthetnek belőlük, vagy újraoszthatják őket.Ne ossz meg veszélyes információt a Mastodon hálózaton!
  • -
  • IP címek és egyéb metaadatok: Bejelentkezéskor letároljuk a használt böngésződet és IP címedet. Mindent rögzített munkamenet elérhető és visszavonható a beállítások között. A legutolsó IP címet maximum 12 hónapig tárolunk. Egyéb szerver logokat is megtarthatunk, melyek HTTP kérésenként is tárolhatják az IP címedet.
  • +
  • Alapvető fiókadatok: Ha regisztrálsz ezen a szerveren, kérhetünk tőled felhasználói nevet, e-mail címet és jelszót is. Megadhatsz magadról egyéb profil információt, megjelenítendő nevet, bemutatkozást, feltölthetsz profilképet, háttérképet. A felhasználói neved, megjelenítendő neved, bemutatkozásod, profil képed és háttér képed mindig nyilvánosak mindenki számára.
  • +
  • Tülkök (posztok), követések, más nyilvános adatok: Az általad követett emberek listája nyilvános. Ugyanez igaz a te követőidre is. Ha küldesz egy üzenetet, ennek az idejét eltároljuk azzal az alkalmazással együtt, melyből az üzenetet küldted. Az üzenetek tartalmazhatnak média csatolmányt, képeket, videókat. A nyilvános tülkök (posztok) bárki számára elérhetőek. Ha egy tülköt kiemelsz a profilodon, az is nyilvánossá válik. Amikor a tülkjeidet a követőidnek továbbítjuk, a poszt más szerverekre is átkerülhet, melyeken így másolatok képződhetnek. Ha törölsz tülköket, ez is továbbítódik a követőid felé. A megtolás (reblog) és kedvencnek jelölés művelete is mindig nyilvános.
  • +
  • Közvetlen üzenetek és csak követőknek szánt tülkök: Minden tülk a szerveren tárolódik. A csak követőknek szánt tülköket a követőidnek és az ezekben megemlítetteknek továbbítjuk, míg a közvetlen üzeneteket kizárólag az ebben megemlítettek kapják. Néhány esetben ez azt jelenti, hogy ezek más szerverekre is továbbítódnak, így ott másolatok keletkezhetnek. Jóhiszeműen feltételezzük, hogy más szerverek is hasonlóan járnak el, mikor ezeket az üzeneteket csak az arra jogosultaknak mutatják meg. Ugyanakkor ez nem feltétlenül igaz. Érdemes ezért megvizsgálni azokat a szervereket, melyeken követőid vannak. Be tudod állítani, hogy minden követési kérelmet jóvá kelljen hagynod. Tartsd észben, hogy a szerver üzemeltetői láthatják az üzeneteket, illetve a fogadók képernyőképet, másolatot készíthetnek belőlük, vagy újraoszthatják őket. Ne ossz meg veszélyes információt a Mastodon hálózaton!
  • +
  • IP címek és egyéb metaadatok: Bejelentkezéskor letároljuk a használt böngésződet és IP címedet. Minden rögzített munkamenet elérhető és visszavonható a beállítások között. Az utoljára rögzített IP címet maximum 12 hónapig tároljuk. Egyéb szerver logokat is megtarthatunk, melyek HTTP kérésenként is tárolhatják az IP címedet.

Mire használjuk az adataidat?

-

Bármely tőled begyűjtött adatot a következő célokra használhatjuk:

+

Bármely tőled begyűjtött adatot a következő célokra használhatjuk fel:

  • Mastodon alapfunkcióinak biztosítása: Csak akkor léphetsz kapcsolatba másokkal, ha be vagy jelentkezve. Pl. követhetsz másokat a saját, személyre szabott idővonaladon.
  • @@ -960,14 +949,14 @@ hu:

    Mik az adatmegőrzési szabályaink?

    -

    Jóhiszeműen járunk el, hogy:

    +

    Mindent megteszünk, hogy:

      -
    • A szerver logokat, melyek kérésenként tartalmazzák a felhasználó IP címét maximum 90 napig tartjuk meg.
    • -
    • A regisztrált felhasználók IP címeikkel összekötő adatokat maximum 12 hónapig tartjuk meg.
    • +
    • A szerver logokat, melyek kérésenként tartalmazzák a felhasználó IP címét maximum 90 napig tartsuk meg.
    • +
    • A regisztrált felhasználókat IP címeikkel összekötő adatokat maximum 12 hónapig tartsuk meg.
    -

    Kérhetsz archívot minden tárolt adatodról, tülkjeidről, média fájljaidról, profil- és háttér képedről.

    +

    Kérhetsz mentést minden tárolt adatodról, tülködről, média fájlodról, profil- és háttér képedről.

    Bármikor visszaállíthatatlanul le is törölheted a fiókodat.

    @@ -983,7 +972,7 @@ hu:

    Átadunk bármilyen adatot harmadik személynek?

    -

    Az azonosításodra alkalmazható adatokat nem adjuk el, nem kereskedünk vele, nem adjuk át külső szereplőnek. Ez nem foglalja magába azon harmadik személyeket, aki az üzemeltetésben, felhasználók kiszolgálásban és a tevékenységünkben segítenek, de csak addig, amíg ők is elfogadják, hogy ezeket az adatokat bizalmasan kezelik. Akkor is átadhatjuk ezeket az adatokat, ha erre hitünk szerint törvény kötelez minket, ha betartatjuk az oldalunk szabályzatát vagy megvédjük a saját vagy mások személyiségi jogait, tulajdonát, biztonságát.

    +

    Az azonosításodra alkalmazható adatokat nem adjuk el, nem kereskedünk vele, nem adjuk át külső szereplőnek. Ez nem foglalja magában azon harmadik személyeket, aki az üzemeltetésben, felhasználók kiszolgálásban és a tevékenységünkben segítenek, de csak addig, amíg ők is elfogadják, hogy ezeket az adatokat bizalmasan kezelik. Akkor is átadhatjuk ezeket az adatokat, ha erre hitünk szerint törvény kötelez minket, ha betartatjuk az oldalunk szabályzatát vagy megvédjük a saját vagy mások személyiségi jogait, tulajdonát, biztonságát.

    A nyilvános tartalmaidat más hálózatban lévő szerverek letölthetik. A nyilvános és csak követőknek szánt tülkjeid olyan szerverekre is elküldődnek, melyeken követőid vannak. A közvetlen üzenetek is átkerülnek a címzettek szervereire, ha ők más szerveren regisztráltak.

    @@ -993,9 +982,9 @@ hu:

    Az oldal gyerekek általi használata

    -

    Ha ez a szerver az EU-ban vagy EEA-ban van: Az oldalunk, szolgáltatásaink és termékeink mind 16 éven felülieket céloznak. Ha 16 évnél fiatalabb vagy, a GDPR (General Data Protection Regulation) értelmében kérlek ne használd ezt az oldalt!

    +

    Ha ez a szerver az EU-ban vagy EEA-ban található: Az oldalunk, szolgáltatásaink és termékeink mind 16 éven felülieket céloznak. Ha 16 évnél fiatalabb vagy, a GDPR (General Data Protection Regulation) értelmében kérlek ne használd ezt az oldalt!

    -

    Ha ez a szerver az USA-ban van: Az oldalunk, szolgáltatásaink és termékeink mind 13 éven felülieket céloznak. Ha 13 évnél fiatalabb vagy, a COPPA (Children's Online Privacy Protection Act) értelmében kérlek ne használd ezt az oldalt!

    +

    Ha ez a szerver az USA-ban található: Az oldalunk, szolgáltatásaink és termékeink mind 13 éven felülieket céloznak. Ha 13 évnél fiatalabb vagy, a COPPA (Children's Online Privacy Protection Act) értelmében kérlek ne használd ezt az oldalt!

    A jogi előírások különbözhetnek ettől a világ egyéb tájain.

    diff --git a/config/locales/id.yml b/config/locales/id.yml index 43721b19b9..16098b1890 100644 --- a/config/locales/id.yml +++ b/config/locales/id.yml @@ -191,11 +191,6 @@ id: title: Deskripsi situs tambahan site_title: Judul Situs title: Pengaturan situs - subscriptions: - confirmed: Dikonfirmasi - expires_in: Kadaluarsa dalam - last_delivery: Terakhir dikirim - topic: Topik title: Administrasi application_mailer: settings: 'Ubah pilihan email: %{link}' diff --git a/config/locales/it.yml b/config/locales/it.yml index 6dfe212d14..1df321752a 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -469,13 +469,6 @@ it: no_status_selected: Nessun status è stato modificato perché nessuno era stato selezionato title: Gli status dell'account with_media: con media - subscriptions: - callback_url: URL Callback - confirmed: Confermato - expires_in: Scade in - last_delivery: Ultima distribuzione - title: WebSub - topic: Argomento tags: accounts: Account hidden: Nascosto @@ -818,10 +811,6 @@ it: reply: proceed: Continua per rispondere prompt: 'Vuoi rispondere a questo toot:' - remote_unfollow: - error: Errore - title: Titolo - unfollowed: Non più seguito scheduled_statuses: over_daily_limit: Hai superato il limite di %{limit} toot programmati per questo giorno over_total_limit: Hai superato il limite di %{limit} toot programmati @@ -885,7 +874,7 @@ it: notifications: Notifiche preferences: Preferenze profile: Profilo - relationships: Follows and followers + relationships: Follows e followers two_factor_authentication: Autenticazione a due fattori statuses: attached: diff --git a/config/locales/ja.yml b/config/locales/ja.yml index ca640d07d1..1ff5a2b632 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -463,13 +463,6 @@ ja: no_status_selected: 何も選択されていないため、変更されていません title: トゥート一覧 with_media: メディアあり - subscriptions: - callback_url: コールバックURL - confirmed: 確認済み - expires_in: 期限 - last_delivery: 最終配送 - title: WebSub - topic: トピック tags: accounts: アカウント hidden: 非表示 @@ -805,10 +798,6 @@ ja: reply: proceed: 返信する prompt: '返信しようとしています:' - remote_unfollow: - error: エラー - title: タイトル - unfollowed: フォロー解除しました scheduled_statuses: over_daily_limit: その日予約できる投稿数 %{limit} を超えています over_total_limit: 予約できる投稿数 %{limit} を超えています diff --git a/config/locales/ka.yml b/config/locales/ka.yml index 53057d8603..57dd0f5c02 100644 --- a/config/locales/ka.yml +++ b/config/locales/ka.yml @@ -368,13 +368,6 @@ ka: no_status_selected: სატუსები არ შეცვლილა, რადგან არცერთი არ მონიშნულა title: ანგარიშის სტატუსები with_media: მედიით - subscriptions: - callback_url: ქოლბექ ურლ - confirmed: დამოწმდა - expires_in: ვადა გასდის - last_delivery: ბოლო მიღება - title: ვებ-საბი - topic: სათაური title: ადმინისტრაცია admin_mailer: new_report: @@ -601,10 +594,6 @@ ka: no_account_html: არ გაქვთ ანგარიში? შეგიძლიათ დარეგისტრირდეთ აქ proceed: გააგრძელეთ გასაყოლად prompt: 'თქვენ გაჰყვებით:' - remote_unfollow: - error: შეცდომა - title: სათაური - unfollowed: დადევნების შეწყვეტა sessions: activity: ბოლო აქტივობა browser: ბრაუზერი diff --git a/config/locales/kk.yml b/config/locales/kk.yml index c6212c378f..a3651b1d30 100644 --- a/config/locales/kk.yml +++ b/config/locales/kk.yml @@ -436,13 +436,6 @@ kk: no_status_selected: Бірде-бір статус өзгерген жоқ, себебі ештеңе таңдалмады title: Аккаунт статустары with_media: Медиамен - subscriptions: - callback_url: Callbаck URL - confirmed: Confirmеd - expires_in: Expirеs in - last_delivery: Last dеlivery - title: WеbSub - topic: Tоpic tags: accounts: Accоunts hidden: Hiddеn @@ -726,10 +719,6 @@ kk: reply: proceed: Жауап жазу prompt: 'Сіз мына жазбаға жауап жазасыз:' - remote_unfollow: - error: Қате - title: Тақырыбы - unfollowed: Жазылудан бас тартылды scheduled_statuses: over_daily_limit: Сіз бір күндік %{limit} жазба лимитін тауыстыңыз over_total_limit: Сіз %{limit} жазба лимитін тауыстыңыз diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 3f14d5df64..ec64972ed7 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -465,13 +465,6 @@ ko: no_status_selected: 아무 것도 선택 되지 않아 아무 것도 바뀌지 않았습니다 title: 계정 툿 with_media: 미디어 있음 - subscriptions: - callback_url: 콜백 URL - confirmed: 확인됨 - expires_in: 기한 - last_delivery: 최종 발송 - title: WebSub - topic: 토픽 tags: accounts: 계정들 hidden: 숨겨짐 @@ -807,10 +800,6 @@ ko: reply: proceed: 답장 진행 prompt: '이 툿에 답장을 하려 합니다:' - remote_unfollow: - error: 에러 - title: 타이틀 - unfollowed: 언팔로우됨 scheduled_statuses: over_daily_limit: 그 날짜에 대한 %{limit}개의 예약 툿 제한을 초과합니다 over_total_limit: 예약 툿 제한 %{limit}을 초과합니다 diff --git a/config/locales/lt.yml b/config/locales/lt.yml index 2cf0b7c42b..087cbb582c 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -419,13 +419,6 @@ lt: no_status_selected: Jokie statusai nebuvo pakeisti, nes niekas nepasirinkta title: Paskyros statusai with_media: Su medija - subscriptions: - callback_url: Atgalinė URL - confirmed: Patvirtinta - expires_in: Pasibaigia - last_delivery: Paskutinis pristatymas - title: WebSub protokolas - topic: Tema tags: accounts: Paskyros hidden: Paslėpti @@ -672,10 +665,6 @@ lt: reply: proceed: Atsakyti prompt: 'Jūs norite atsakyti šiam toot''ui:' - remote_unfollow: - error: Klaida - title: Pavadinimas - unfollowed: Nebesekama scheduled_statuses: over_daily_limit: Jūs pasieketė limitą (%{limit}) galimų toot'ų per dieną over_total_limit: Jūs pasieketė %{limit} limitą galimų toot'ų diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 78be7872d8..52c23de8f3 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -469,13 +469,6 @@ nl: no_status_selected: Er werden geen toots gewijzigd, omdat er geen enkele werd geselecteerd title: Toots van account with_media: Met media - subscriptions: - callback_url: Callback-URL - confirmed: Bevestigd - expires_in: Verloopt over - last_delivery: Laatste bezorging - title: WebSub - topic: Account tags: accounts: Accounts hidden: Verborgen @@ -816,10 +809,6 @@ nl: reply: proceed: Doorgaan met reageren prompt: 'Je wilt op de volgende toot reageren:' - remote_unfollow: - error: Fout - title: Titel - unfollowed: Ontvolgd scheduled_statuses: over_daily_limit: Je hebt de limiet van %{limit} in te plannen toots voor die dag overschreden over_total_limit: Je hebt de limiet van %{limit} in te plannen toots overschreden diff --git a/config/locales/no.yml b/config/locales/no.yml index d21dda6fba..fbf1383937 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -267,12 +267,6 @@ no_media: Ingen media title: Kontostatuser with_media: Med media - subscriptions: - callback_url: Callback-URL - confirmed: Bekreftet - expires_in: Utløper om - last_delivery: Siste levering - topic: Emne title: Administrasjon admin_mailer: new_report: diff --git a/config/locales/oc.yml b/config/locales/oc.yml index 785caa4ecc..067c343b81 100644 --- a/config/locales/oc.yml +++ b/config/locales/oc.yml @@ -7,6 +7,7 @@ oc: active_count_after: actius active_footnote: Utilizaire actius per mes (UAM) administered_by: 'Administrat per :' + api: API apps: Aplicacions per mobil apps_platforms: Utilizatz Mastodon d‘iOS, Android o d’autras plataforma estant browse_directory: Navigatz per l’annuari de perfil e filtratz segon çò qu’aimatz @@ -19,6 +20,7 @@ oc: extended_description_html: |

    Una bona plaça per las règlas

    La descripcion longa es pas estada causida pel moment.

    + federation_hint_html: Amb un compte sus %{instance} poiretz sègre de personas de qualque siasque servidor Mastodon e encara mai. generic_description: "%{domain} es un dels servidors del malhum" get_apps: Ensajatz una aplicacion mobil hosted_on: Mastodon albergat sus %{domain} @@ -31,6 +33,7 @@ oc: one: estatut other: estatuts status_count_before: qu’an escrich + tagline: Seguètz d’amics e trobatz-ne de nòus terms: Condicions d’utilizacion user_count_after: one: utilizaire @@ -62,8 +65,10 @@ oc: posts_with_replies: Tuts e responsas reserved_username: Aqueste nom d’utilizaire es reservat roles: + admin: Admin bot: Robòt moderator: Moderador + unavailable: Perfil indisponible unfollow: Quitar de sègre admin: account_actions: @@ -76,7 +81,9 @@ oc: destroyed_msg: Nòta de moderacion ben suprimida ! accounts: approve: Aprovar + approve_all: O validar tot are_you_sure: Sètz segur ? + avatar: Avatar by_domain: Domeni change_email: changed_msg: Adreça corrèctament cambiada ! @@ -107,6 +114,7 @@ oc: header: Bandièra inbox_url: URL de recepcion invited_by: Convidat per + ip: IP joined: Venguèt location: all: Totes @@ -126,6 +134,7 @@ oc: moderation_notes: Nòtas de moderacion most_recent_activity: Activitat mai recenta most_recent_ip: IP mai recenta + no_account_selected: Cap de compte pas cambiat estant que cap èra pas seleccionat no_limits_imposed: Cap de limit impausat not_subscribed: Pas seguidor outbox_url: URL Outbox @@ -134,8 +143,11 @@ oc: profile_url: URL del perfil promote: Promòure protocol: Protocòl + public: Public push_subscription_expires: Fin de l’abonament PuSH redownload: Actualizar lo perfil + reject: Regetar + reject_all: O regetar tot remove_avatar: Supriir l’avatar remove_header: Levar la bandièra resend_confirmation: @@ -145,7 +157,9 @@ oc: reset: Reïnicializar reset_password: Reïnicializar lo senhal resubscribe: Se tornar abonar + role: Autorizacions roles: + admin: Administrator moderator: Moderador staff: Personnal user: Uitlizaire @@ -160,6 +174,7 @@ oc: statuses: Estatuts subscribe: S’abonar suspended: Suspendut + time_in_queue: En espèra a la fila %{time} title: Comptes unconfirmed_email: Adreça pas confirmada undo_silenced: Levar lo silenci @@ -167,6 +182,7 @@ oc: unsubscribe: Se desabonar username: Nom d’utilizaire warn: Avisar + web: Web action_logs: actions: assigned_to_self_report: "%{name} s’assignèt lo rapòrt %{target}" @@ -211,6 +227,7 @@ oc: destroyed_msg: Emoji ben suprimit ! disable: Desactivar disabled_msg: Aqueste emoji es ben desactivat + emoji: Emoji enable: Activar enabled_msg: Aqueste emoji es ben activat image_hint: PNG cap a 50Ko @@ -233,6 +250,7 @@ oc: feature_profile_directory: Annuari de perfils feature_registrations: Inscripcions feature_relay: Relai de federacion + feature_timeline_preview: Apercebut del flux d’actualitats features: Foncionalitats hidden_service: Federacion amb servicis amagats open_reports: Senhalaments dobèrts @@ -252,6 +270,7 @@ oc: created_msg: Domeni blocat es a èsser tractat destroyed_msg: Lo blocatge del domeni es estat levat domain: Domeni + existing_domain_block_html: Impausèretz ja de limitas mai estrictas per %{name}, vos cal lo desblocard’en primièr. new: create: Crear blocatge hint: Lo blocatge empacharà pas la creacion de compte dins la basa de donadas, mai aplicarà la moderacion sus aquestes comptes. @@ -317,6 +336,8 @@ oc: expired: Expirats title: Filtre title: Convits + pending_accounts: + title: Comptes en espèra (%{count}) relays: add_new: Ajustar un nòu relai delete: Suprimir @@ -405,7 +426,9 @@ oc: title: Autorizat amb invitacions registrations_mode: modes: + approved: Validacion necessària per s’inscriure none: Degun pòt pas se marcar + open: Tot lo monde se pòt marcar title: Mòdes d’inscripcion show_known_fediverse_at_about_page: desc_html: Un còp activat mostrarà los tuts de totes los fediverse dins l’apercebut. Autrament mostrarà pas que los tuts locals. @@ -446,12 +469,6 @@ oc: no_status_selected: Cap d’estatut pas cambiat estant que cap èra pas seleccionat title: Estatuts del compte with_media: Amb mèdia - subscriptions: - callback_url: URL de rapèl - confirmed: Confirmat - expires_in: S’acaba dins - last_delivery: Darrièra distribucion - topic: Subjècte tags: accounts: Comptes hidden: Amagat @@ -459,6 +476,7 @@ oc: name: Etiqueta title: Etiquetas unhide: Aparéisser dins l’annuari + visible: Visible title: Administracion warning_presets: add_new: N’ajustar un nòu @@ -467,12 +485,22 @@ oc: edit_preset: Modificar lo tèxt predefinit d’avertiment title: Gerir los tèxtes predefinits admin_mailer: + new_pending_account: + body: Los detalhs del nòu compte son çai-jos. Podètz validar o regetar aquesta demanda. + subject: Nòu compte per repassar sus %{instance} (%{username}) new_report: body: "%{reporter} a senhalat %{target}" body_remote: Qualqu’un de %{domain} senhalèt %{target} subject: Novèl senhalament per %{instance} (#%{id}) + appearance: + advanced_web_interface: Interfàcia web avançada + advanced_web_interface_hint: 'Se volètz utilizar la nautor complèta de l’ecran, l’interfàcia web avançada vos permet de configurar diferentas colomnas per mostrar tan d’informacions que volètz : Acuèlh, notificacions, flux d’actualitat, e d’autras listas e etiquetas.' + animations_and_accessibility: Animacion e accessibilitat + confirmation_dialogs: Fenèstras de confirmacion + sensitive_content: Contengut sensible application_mailer: notification_preferences: Cambiar las preferéncias de corrièl + salutation: "%{name}," settings: 'Cambiar las preferéncias de corrièl : %{link}' view: 'Veire :' view_profile: Veire lo perfil @@ -500,6 +528,9 @@ oc: migrate_account: Mudar endacòm mai migrate_account_html: Se volètz mandar los visitors d’aqueste compte a un autre, podètz o configurar aquí. or_log_in_with: O autentificatz-vos amb + providers: + cas: CAS + saml: SAML register: Se marcar registration_closed: "%{instance} accepta pas de nòus membres" resend_confirmation: Tornar mandar las instruccions de confirmacion @@ -531,6 +562,7 @@ oc: x_days: "%{count} jorns" x_minutes: "%{count} min" x_months: "%{count} meses" + x_seconds: "%{count}s" deletes: bad_password_msg: Ben ensajat pirata ! Senhal incorrècte confirm_password: Picatz vòstre senhal actual per verificar vòstra identitat @@ -561,6 +593,9 @@ oc: content: Un quicomet a pas foncionat coma caliá. title: Aquesta pagina es pas corrècta noscript_html: Per utilizar l’aplicacion web de Mastodon, mercés d’activar JavaScript. O podètz utilizar una aplicacion per vòstra plataforma coma alernativa. + existing_username_validator: + not_found: impossible de trobar un utilizaire local amb aqueste nom d’utilizaire + not_found_multiple: impossible de trobar %{usernames} exports: archive_takeout: date: Data @@ -570,6 +605,7 @@ oc: request: Demandar vòstre archiu size: Talha blocks: Personas que blocatz + csv: CSV domain_blocks: Blocatge de domenis follows: Personas que seguètz lists: Listas @@ -603,16 +639,31 @@ oc: all: Tot changes_saved_msg: Cambiaments ben realizats ! copy: Copiar + order_by: Triar per save_changes: Salvar los cambiaments validation_errors: one: I a quicòm que truca ! Mercés de corregir l’error çai-jos other: I a quicòm que truca ! Mercés de corregir las %{count} errors çai-jos + html_validator: + invalid_markup: 'conten un balisatge HTML invalid : %{error}' identity_proofs: + active: Actiu authorize: Òc, autorizar authorize_connection_prompt: Autorizar aquesta connexion criptografica ? + errors: + failed: La connexion criptografica a fracassat. Ensajatz tornamai de %{provider} estant. + keybase: + invalid_token: Los getons Keybase son de hashes de signaturas e devon èsser de caractèrs 66 hex + verification_failed: Keybase reconeis pas aqueste geton coma signatura de l’utilizaire Keybase %{kb_username}. Ensajatz tornamai de Keybase estant. + wrong_user: Creacion impossibla de la pròva per %{proving} en estant connectat coma %{current}. Connectatz-vos coma %{proving} e ensajatz tornamai. + explanation_html: Aquí podètz connectar d’un biais criptografic vòstras identitats, coma un perfil Keybase. Aquò permet al monde de vos enviar de messatges chifrats e fisar al contengut que lor enviatz. i_am_html: Soi %{username} a %{service}. identity: Identitat + inactive: Inactiu + publicize_checkbox: 'E enviatz lo tut seguent :' + publicize_toot: 'Es provat ! Soi %{username} de %{service} : %{url}' status: Estatut de verificacion + view_proof: Veire la pròva imports: modes: merge: Fondre @@ -698,11 +749,22 @@ oc: body: "%{name} a tornat partejar vòstre estatut :" subject: "%{name} a tornat partejar vòstre estatut" title: Novèl partatge + number: + human: + decimal_units: + format: "%n%u" + units: + billion: B + million: M + quadrillion: Q + thousand: K + trillion: T pagination: newer: Mai recents next: Seguent older: Mai ancians prev: Precedent + truncate: "…" polls: errors: already_voted: Avètz ja votat per aqueste sondatge @@ -715,13 +777,20 @@ oc: too_many_options: pòt pas contenir mai de %{max} opcions preferences: other: Autre + posting_defaults: Valors per defaut de las publicacions + public_timelines: Fluxes d’actualitats publics relationships: activity: Activitat del compte dormant: Inactiu + last_active: Darrièra activitat + most_recent: Mai recenta moved: Mudat mutual: Mutuala primary: Pirmària relationship: Relacion + remove_selected_domains: Levar totes los seguidors dels domenis seleccionats + remove_selected_followers: Levar los seguidors seleccionats + remove_selected_follows: Quitar de sègre las personas seleccionadas status: Estat del compte remote_follow: acct: Picatz vòstre utilizaire@domeni que que volètz utilizar per sègre aqueste utilizaire @@ -740,9 +809,6 @@ oc: reply: proceed: Contunhar per respondre prompt: 'Volètz respondre a aqueste tut :' - remote_unfollow: - title: Títol - unfollowed: Pas mai seguit scheduled_statuses: over_daily_limit: Avètz passat la limita de %{limit} tuts programats per aquel jorn over_total_limit: Avètz passat la limita de %{limit} tuts programats @@ -751,15 +817,47 @@ oc: activity: Darrièra activitat browser: Navigator browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + electron: Electron + firefox: Firefox generic: Navigator desconegut + ie: Internet Explorer + micro_messenger: MicroMessenger + nokia: Nokia S40 Ovi Browser + opera: Opera + otter: Otter + phantom_js: PhantomJS + qq: QQ Browser + safari: Safari + uc_browser: UCBrowser + weibo: Weibo current_session: Session en cors description: "%{browser} sus %{platform}" explanation: Aquí los navigators connectats a vòstre compte Mastodon. + ip: IP platforms: + adobe_air: Adobe Air + android: Android + blackberry: Blackberry + chrome_os: ChromeOS + firefox_os: Firefox OS + ios: iOS + linux: Linux + mac: Mac other: plataforma desconeguda + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone revoke: Revocar revoke_success: Session ben revocada + title: Sessions settings: + account: Compte + account_settings: Paramètres de compte + appearance: Aparéncia authorized_apps: Aplicacions autorizadas back: Tornar a Mastodon delete: Supression de compte @@ -767,10 +865,13 @@ oc: edit_profile: Modificar lo perfil export: Exportar de donadas featured_tags: Etiquetas en avant + identity_proofs: Pròvas d’identitat import: Importar de donadas + import_and_export: Import e export migrate: Migracion de compte notifications: Notificacions preferences: Preferéncias + profile: Perfil relationships: Abonaments e seguidors two_factor_authentication: Autentificacion en dos temps statuses: @@ -806,6 +907,7 @@ oc: visibilities: private: Seguidors solament private_long: Mostrar pas qu’als seguidors + public: Public public_long: Tot lo monde pòt veire unlisted: Pas listat unlisted_long: Tot lo monde pòt veire mai serà pas visible sul flux public @@ -905,6 +1007,7 @@ oc: time: formats: default: Lo %d %b de %Y a %Ho%M + month: "%b de %Y" two_factor_authentication: code_hint: Picatz lo còdi generat per vòstra aplicacion d’autentificacion per confirmar description_html: S’activatz l’autentificacion two-factor, vos caldrà vòstre mobil per vos connectar perque generarà un geton per vos daissar dintrar. diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 78db4c6723..1d6f130906 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -52,7 +52,7 @@ pl: many: śledzących one: śledzący other: Śledzących - following: Śledzonych + following: śledzonych joined: Dołączył(a) %{date} last_active: ostatnio aktywny(-a) link_verified_on: Własność tego odnośnika została sprawdzona %{date} @@ -481,13 +481,6 @@ pl: no_status_selected: Żaden wpis nie został zmieniony, bo żaden nie został wybrany title: Wpisy konta with_media: Z zawartością multimedialną - subscriptions: - callback_url: URL zwrotny - confirmed: Potwierdzone - expires_in: Wygasa - last_delivery: Ostatnio doręczono - title: WebSub - topic: Temat tags: accounts: Konta hidden: Ukryte @@ -838,10 +831,6 @@ pl: reply: proceed: Przejdź do dodawania odpowiedzi prompt: 'Chcesz odpowiedzieć na ten wpis:' - remote_unfollow: - error: Błąd - title: Tytuł - unfollowed: Przestałeś(-aś) śledzić scheduled_statuses: over_daily_limit: Przekroczyłeś(-aś) limit %{limit} zaplanowanych wpisów na ten dzień over_total_limit: Przekroczyłeś(-aś) limit %{limit} zaplanowanych wpisów diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index d75e91b8bc..e29191871e 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -451,12 +451,6 @@ pt-BR: no_status_selected: Nenhum status foi modificado porque nenhum estava selecionado title: Postagens da conta with_media: Com mídia - subscriptions: - callback_url: URL de Callback - confirmed: Confirmado - expires_in: Expira em - last_delivery: Última entrega - topic: Tópico tags: accounts: Contas hidden: Escondido @@ -770,10 +764,6 @@ pt-BR: reply: proceed: Proceder para responder prompt: 'Você quer responder à esse toot:' - remote_unfollow: - error: Erro - title: Título - unfollowed: Deixou de seguir scheduled_statuses: over_daily_limit: Você excedeu o limite de %{limit} toots planejados para esse dia over_total_limit: Você excedeu o limite de %{limit} toots planejados diff --git a/config/locales/pt.yml b/config/locales/pt.yml index 9cd92f6bd8..41c399b7df 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -431,12 +431,6 @@ pt: no_status_selected: Nenhum estado foi alterado porque nenhum foi selecionado title: Estado das contas with_media: Com media - subscriptions: - callback_url: URL de Callback - confirmed: Confirmado - expires_in: Expira em - last_delivery: Última entrega - topic: Tópico tags: accounts: Contas hidden: Escondidas @@ -707,10 +701,6 @@ pt: reply: proceed: Prosseguir com resposta prompt: 'Queres responder a esta publicação:' - remote_unfollow: - error: Erro - title: Título - unfollowed: Não seguido scheduled_statuses: over_daily_limit: Excedeste o limite de %{limit} publicações agendadas para esse dia over_total_limit: Tu excedeste o limite de %{limit} publicações agendadas diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 7e336be984..83eb3089f5 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -478,13 +478,6 @@ ru: no_status_selected: Не выбран ни один статус, ничего не изменено title: Статусы аккаунта with_media: С медиаконтентом - subscriptions: - callback_url: Callback URL - confirmed: Подтверждено - expires_in: Истекает через - last_delivery: Последняя доставка - title: WebSub - topic: Тема tags: accounts: Аккаунты hidden: Скрыты @@ -714,8 +707,8 @@ ru: many: "%{count} исп." one: 1 исп other: "%{count} исп" - max_uses_prompt: Без лимита - prompt: Генерируйте и делитесь ссылками с другими, чтобы предоставить им доступ к этому узлу + max_uses_prompt: Без ограничения + prompt: Создавайте и делитесь ссылками с другими, чтобы предоставить им доступом к этому узлу table: expires_at: Истекает uses: Исп. @@ -831,10 +824,6 @@ ru: reply: proceed: Ответить prompt: 'Вы собираетесь ответить на этот статус:' - remote_unfollow: - error: Ошибка - title: Заголовок - unfollowed: Отписаны scheduled_statuses: over_daily_limit: Вы превысили лимит в %{limit} запланированных постов на указанный день over_total_limit: Вы превысили лимит на %{limit} запланированных постов diff --git a/config/locales/simple_form.co.yml b/config/locales/simple_form.co.yml index 1f5dba43fb..d58e775289 100644 --- a/config/locales/simple_form.co.yml +++ b/config/locales/simple_form.co.yml @@ -34,6 +34,7 @@ co: setting_hide_network: I vostri abbunati è abbunamenti ùn saranu micca mustrati nant’à u vostru prufile setting_noindex: Tocca à u vostru prufile pubblicu è i vostri statuti setting_show_application: L'applicazione chì voi utilizate per mandà statuti sarà affissata indè a vista ditagliata di quelli + setting_use_blurhash: I digradati blurhash sò basati nant'à i culori di u ritrattu piattatu ma senza i ditagli username: U vostru cugnome sarà unicu nant'à %{domain} whole_word: Quandu a parolla o a frasa sana hè alfanumerica, sarà applicata solu s'ella currisponde à a parolla sana featured_tag: @@ -109,6 +110,7 @@ co: setting_system_font_ui: Pulizza di caratteri di u sistemu setting_theme: Tema di u situ setting_unfollow_modal: Mustrà una cunfirmazione per siguità qualch’unu + setting_use_blurhash: Vede digradati di culori per i media piattati severity: Severità type: Tippu d’impurtazione username: Cugnome diff --git a/config/locales/simple_form.cs.yml b/config/locales/simple_form.cs.yml index 3bf74e9718..5191d77fe4 100644 --- a/config/locales/simple_form.cs.yml +++ b/config/locales/simple_form.cs.yml @@ -34,6 +34,7 @@ cs: setting_hide_network: Koho sledujete a kdo sleduje vás nebude zobrazeno na vašem profilu setting_noindex: Ovlivňuje váš veřejný profil a stránky tootů setting_show_application: Aplikace, kterou používáte k psaní tootů, bude zobrazena v detailním zobrazení vašich tootů + setting_use_blurhash: Gradienty jsou založeny na barvách skryté grafiky, ale zakrývají jakékoliv detaily username: Vaše uživatelské jméno bude na %{domain} unikátní whole_word: Je-li klíčové slovo či fráze pouze alfanumerická, bude aplikována pouze, pokud se shoduje s celým slovem featured_tag: @@ -109,6 +110,7 @@ cs: setting_system_font_ui: Použít výchozí písmo systému setting_theme: Motiv stránky setting_unfollow_modal: Zobrazovat před zrušením sledování potvrzovací okno + setting_use_blurhash: Zobrazit pro skrytá média barevné gradienty severity: Přísnost type: Typ importu username: Uživatelské jméno diff --git a/config/locales/simple_form.cy.yml b/config/locales/simple_form.cy.yml index 023506f24f..9f4ef0708e 100644 --- a/config/locales/simple_form.cy.yml +++ b/config/locales/simple_form.cy.yml @@ -34,6 +34,7 @@ cy: setting_hide_network: Ni fydd y rheini yr ydych yn eu dilyn a phwy sy'n eich dilyn chi yn cael ei ddangos ar eich proffil setting_noindex: Mae hyn yn effeithio ar eich proffil cyhoeddus a'ch tudalennau statws setting_show_application: Bydd y offer frydych yn defnyddio i dŵtio yn cael ei arddangos yn golwg manwl eich tŵtiau + setting_use_blurhash: Mae graddiannau wedi'u seilio ar liwiau'r delweddau cudd ond maent yn cuddio unrhyw fanylion username: Bydd eich enw defnyddiwr yn unigryw ar %{domain} whole_word: Os yw'r allweddair neu'r ymadrodd yn alffaniwmerig yn unig, mi fydd ond yn cael ei osod os yw'n cyfateb a'r gair cyfan featured_tag: @@ -109,6 +110,7 @@ cy: setting_system_font_ui: Defnyddio ffont rhagosodedig y system setting_theme: Thema'r wefan setting_unfollow_modal: Dangos deialog cadarnhau cyn dad-ddilyn rhywun + setting_use_blurhash: Dangoswch raddiannau lliwgar ar gyfer cyfryngau cudd severity: Difrifoldeb type: Modd mewnforio username: Enw defnyddiwr diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml index 61e0f9740d..d070796426 100644 --- a/config/locales/simple_form.de.yml +++ b/config/locales/simple_form.de.yml @@ -34,6 +34,7 @@ de: setting_hide_network: Wem du folgst und wer dir folgt, wird in deinem Profil nicht angezeigt setting_noindex: Betrifft dein öffentliches Profil und deine Beiträge setting_show_application: Die Anwendung die du nutzst wird in der detaillierten Ansicht deiner Beiträge angezeigt + setting_use_blurhash: Die Farbverläufe basieren auf den Farben der versteckten Medien, aber verstecken irgendwelche Details, die Reize auslösen könnten username: Dein Profilname wird auf %{domain} einzigartig sein whole_word: Wenn das Schlagwort nur aus Buchstaben und Zahlen besteht, wird es nur angewendet, wenn es dem ganzen Wort entspricht featured_tag: @@ -109,6 +110,7 @@ de: setting_system_font_ui: Standardschriftart des Systems verwenden setting_theme: Theme setting_unfollow_modal: Bestätigungsdialog anzeigen, bevor jemandem entfolgt wird + setting_use_blurhash: Farbverlauf für versteckte Medien anzeigen severity: Schweregrad type: Art des Imports username: Profilname diff --git a/config/locales/simple_form.el.yml b/config/locales/simple_form.el.yml index 67f3b64aa1..099c900a51 100644 --- a/config/locales/simple_form.el.yml +++ b/config/locales/simple_form.el.yml @@ -34,6 +34,7 @@ el: setting_hide_network: Δε θα εμφανίζεται στο προφίλ σου ποιους ακολουθείς και ποιοι σε ακολουθούν setting_noindex: Επηρεάζει το δημόσιο προφίλ και τις δημοσιεύσεις σου setting_show_application: Η εφαρμογή που χρησιμοποιείς για να στέλνεις τα τουτ σου θα εμφανίζεται στις αναλυτικές λεπτομέρειες τους + setting_use_blurhash: Οι χρωματισμοί βασίζονται στα χρώματα του κρυμμένου πολυμέσου αλλά θολώνουν τις λεπτομέρειες username: Το όνομα χρήστη σου θα είναι μοναδικό στο %{domain} whole_word: Όταν η λέξη ή η φράση κλειδί είναι μόνο αλφαριθμητική, θα εφαρμοστεί μόνο αν ταιριάζει με ολόκληρη τη λέξη featured_tag: @@ -109,6 +110,7 @@ el: setting_system_font_ui: Χρησιμοποίησε την προεπιλεγμένη γραμματοσειρά του συστήματος setting_theme: Θέμα ιστότοπου setting_unfollow_modal: Εμφάνιση ερώτησης επιβεβαίωσης πριν διακόψεις την παρακολούθηση κάποιου + setting_use_blurhash: Εμφάνιση χρωματισμών για τα κρυμμένα πολυμέσα severity: Αυστηρότητα type: Τύπος εισαγωγής username: Όνομα χρηστη diff --git a/config/locales/simple_form.es.yml b/config/locales/simple_form.es.yml index 7b871e8ba0..2986cf091e 100644 --- a/config/locales/simple_form.es.yml +++ b/config/locales/simple_form.es.yml @@ -6,12 +6,16 @@ es: text: Puede usar sintaxis de toots, como URLs, hashtags y menciones admin_account_action: send_email_notification: El usuario recibirá una explicación de lo que sucedió con respecto a su cuenta + text_html: Opcional. Puede usar sintaxis de toots. Puede añadir configuraciones predefinidas de advertencia para ahorrar tiempo + type_html: Elige qué hacer con %{acct} + warning_preset_id: Opcional. Aún puede añadir texto personalizado al final de la configuración predefinida defaults: autofollow: Los usuarios que se registren mediante la invitación te seguirán automáticamente avatar: PNG, GIF o JPG. Máximo %{size}. Será escalado a %{dimensions}px bot: Esta cuenta ejecuta principalmente acciones automatizadas y podría no ser monitorizada context: Uno o múltiples contextos en los que debe aplicarse el filtro digest: Solo enviado tras un largo periodo de inactividad y solo si has recibido mensajes personales durante tu ausencia + discoverable_html: El directorio permite a la gente encontrar cuentas basadas en intereses y actividad. Requiere al menos %{min_followers} seguidores email: Se le enviará un correo de confirmación fields: Puedes tener hasta 4 elementos mostrándose como una tabla en tu perfil header: PNG, GIF o JPG. Máximo %{size}. Será escalado a %{dimensions}px @@ -22,10 +26,19 @@ es: password: Utilice al menos 8 caracteres phrase: Se aplicará sin importar las mayúsculas o los avisos de contenido de un toot scopes: Qué APIs de la aplicación tendrán acceso. Si seleccionas el alcance de nivel mas alto, no necesitas seleccionar las individuales. + setting_aggregate_reblogs: No mostrar nuevos retoots para los toots que han sido recientemente retooteados (sólo afecta a los retoots recibidos recientemente) + setting_default_sensitive: El contenido multimedia sensible está oculto por defecto y puede ser mostrado con un click + setting_display_media_default: Ocultar contenido multimedia marcado como sensible + setting_display_media_hide_all: Siempre ocultar todo el contenido multimedia + setting_display_media_show_all: Mostrar siempre contenido multimedia marcado como sensible setting_hide_network: A quién sigues y quién te sigue no será mostrado en tu perfil setting_noindex: Afecta a tu perfil público y páginas de estado setting_show_application: La aplicación que utiliza usted para publicar toots se mostrará en la vista detallada de sus toots + setting_use_blurhash: Los gradientes se basan en los colores de las imágenes ocultas pero haciendo borrosos los detalles + username: Tu nombre de usuario será único en %{domain} whole_word: Cuando la palabra clave o frase es solo alfanumérica, solo será aplicado si concuerda con toda la palabra + featured_tag: + name: 'Puede que quieras usar uno de estos:' imports: data: Archivo CSV exportado desde otra instancia de Mastodon invite_request: @@ -39,15 +52,21 @@ es: fields: name: Etiqueta value: Contenido + account_warning_preset: + text: Texto predefinido admin_account_action: send_email_notification: Notificar al usuario por correo electrónico text: Aviso personalizado type: Acción types: disable: Deshabilitar + none: No hacer nada silence: Silenciar + suspend: Suspender y eliminar de forma irreversible la información de la cuenta + warning_preset_id: Usar un aviso predeterminado defaults: autofollow: Invitar a seguir tu cuenta + avatar: Avatar bot: Esta es una cuenta bot chosen_languages: Filtrar idiomas confirm_new_password: Confirmar nueva contraseña @@ -55,6 +74,7 @@ es: context: Filtrar contextos current_password: Contraseña actual data: Información + discoverable: Listar esta cuenta en el directorio display_name: Nombre para mostrar email: Dirección de correo electrónico expires_in: Expirar tras @@ -70,12 +90,19 @@ es: otp_attempt: Código de dos factores password: Contraseña phrase: Palabra clave o frase + setting_advanced_layout: Habilitar interfaz web avanzada + setting_aggregate_reblogs: Agrupar retoots en las líneas de tiempo setting_auto_play_gif: Reproducir automáticamente los GIFs animados setting_boost_modal: Mostrar ventana de confirmación antes de un Retoot setting_default_language: Idioma de publicación setting_default_privacy: Privacidad de publicaciones setting_default_sensitive: Marcar siempre imágenes como sensibles setting_delete_modal: Mostrar diálogo de confirmación antes de borrar un toot + setting_display_media: Visualización multimedia + setting_display_media_default: Por defecto + setting_display_media_hide_all: Ocultar todo + setting_display_media_show_all: Mostrar todo + setting_expand_spoilers: Siempre expandir los toots marcados con advertencias de contenido setting_hide_network: Ocultar tu red setting_noindex: Excluirse del indexado de motores de búsqueda setting_reduce_motion: Reducir el movimiento de las animaciones @@ -83,11 +110,14 @@ es: setting_system_font_ui: Utilizar la tipografía por defecto del sistema setting_theme: Tema del sitio setting_unfollow_modal: Mostrar diálogo de confirmación antes de dejar de seguir a alguien + setting_use_blurhash: Mostrar gradientes coloridos para contenido multimedia oculto severity: Severidad type: Importar tipo username: Nombre de usuario username_or_email: Usuario o Email whole_word: Toda la palabra + featured_tag: + name: Etiqueta interactions: must_be_follower: Bloquear notificaciones de personas que no te siguen must_be_following: Bloquear notificaciones de personas que no sigues @@ -106,5 +136,6 @@ es: 'no': 'No' recommended: Recomendado required: + mark: "*" text: necesario 'yes': Sí diff --git a/config/locales/simple_form.eu.yml b/config/locales/simple_form.eu.yml index acd5fd6d98..be3883fb0b 100644 --- a/config/locales/simple_form.eu.yml +++ b/config/locales/simple_form.eu.yml @@ -14,7 +14,7 @@ eu: avatar: PNG, GIF edo JPG. Gehienez %{size}. %{dimensions}px neurrira eskalatuko da bot: Kontu honek nagusiki automatizatutako ekintzak burutzen ditu eta agian ez du inork monitorizatzen context: Iragazkia aplikatzeko testuinguru bat edo batzuk - digest: Soilik jarduerarik gabeko epe luze bat eta gero, eta soilik ez zeudela mezu pertsonalen bat jaso baduzu + digest: Jarduerarik gabeko epe luze bat eta gero mezu pertsonalen bat jaso baduzu, besterik ez discoverable_html: Direktorioak Jendea interesen eta jardueraren arabera aurkitzea ahalbidetzen du. Gutxienez %{min_followers} jarraitzaile behar dira bertan agertzeko email: Baieztapen e-mail bat bidaliko zaizu fields: 4 elementu bistaratu ditzakezu taula batean zure profilean @@ -26,7 +26,7 @@ eu: password: Erabili 8 karaktere gutxienez phrase: Bat egingo du Maiuskula/minuskula kontuan hartu gabe eta edukiaren abisua kontuan hartu gabe scopes: Zeintzuk API atzitu ditzakeen aplikazioak. Goi mailako arloa aukeratzen baduzu, ez dituzu azpikoak aukeratu behar. - setting_aggregate_reblogs: Ez erakutsi buktzada berriak berriki bultzada jaso duten tootentzat (berriki jasotako bultzadei eragiten die besterik ez) + setting_aggregate_reblogs: Ez erakutsi bultzada berriak berriki bultzada jaso duten toot-entzat (berriki jasotako bultzadei eragiten die bakarrik) setting_default_sensitive: Multimedia hunkigarria lehenetsita ezkutatzen da, eta sakatuz ikusi daiteke setting_display_media_default: Ezkutatu hunkigarri gisa markatutako multimedia setting_display_media_hide_all: Ezkutatu multimedia guztia beti @@ -34,6 +34,7 @@ eu: setting_hide_network: Nor jarraitzen duzun eta nork jarraitzen zaituen ez da bistaratuko zure profilean setting_noindex: Zure profil publiko eta Toot-en orrietan eragina du setting_show_application: Tootak bidaltzeko erabiltzen duzun aplikazioa zure tooten ikuspegi xehetsuan bistaratuko da + setting_use_blurhash: Gradienteak ezkutatutakoaren koloreetan oinarritzen dira, baina xehetasunak ezkutatzen dituzte username: Zure erabiltzaile-izena bakana izango da %{domain} domeinuan whole_word: Hitz eta esaldi gakoa alfanumerikoa denean, hitz osoarekin bat datorrenean besterik ez da aplikatuko featured_tag: @@ -109,6 +110,7 @@ eu: setting_system_font_ui: Erabili sistemako tipografia lehenetsia setting_theme: Gunearen gaia setting_unfollow_modal: Erakutsi baieztapen elkarrizketa-koadroa inor jarraitzeari utzi aurretik + setting_use_blurhash: Erakutsi gradiente koloretsuak ezkutatutako multimediaren ordez severity: Larritasuna type: Inportazio mota username: Erabiltzaile-izena @@ -118,8 +120,8 @@ eu: name: Traola interactions: must_be_follower: Blokeatu jarraitzaile ez direnen jakinarazpenak - must_be_following: Blokeatu zuk jarraitzen ez dituzunen jakinarazpenak - must_be_following_dm: Blokeatu zuk jarraitzen ez dituzunen mezu zuzenak + must_be_following: Blokeatu zuk jarraitzen ez dituzu horien jakinarazpenak + must_be_following_dm: Blokeatu zuk jarraitzen ez dituzun horien mezu zuzenak invite_request: text: Zergatik elkartu nahi duzu? notification_emails: diff --git a/config/locales/simple_form.gl.yml b/config/locales/simple_form.gl.yml index 22389051fb..122481dcf0 100644 --- a/config/locales/simple_form.gl.yml +++ b/config/locales/simple_form.gl.yml @@ -34,6 +34,7 @@ gl: setting_hide_network: Non se mostrará no seu perfil quen a segue e quen a está a seguir setting_noindex: Afecta ao seu perfil público e páxinas de estado setting_show_application: A aplicación que está a utilizar para enviar toots mostrarase na vista detallada do toot + setting_use_blurhash: Os gradientes toman as cores da imaxe oculta pero esborranchando todos os detalles username: O seu nome de usuaria será único en %{domain} whole_word: Se a chave ou frase de paso é só alfanumérica, só se aplicará se concorda a palabra completa featured_tag: @@ -109,6 +110,7 @@ gl: setting_system_font_ui: Utilizar a tipografía por defecto do sistema setting_theme: Decorado da instancia setting_unfollow_modal: Solicitar confirmación antes de deixar de seguir alguén + setting_use_blurhash: Mostrar gradientes coloridos para medios ocultos severity: Severidade type: Tipo de importación username: Nome de usuaria diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index ccc6c5ca1c..e32eded5d0 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -34,6 +34,7 @@ ja: setting_hide_network: フォローとフォロワーの情報がプロフィールページで見られないようにします setting_noindex: 公開プロフィールおよび各投稿ページに影響します setting_show_application: トゥートするのに使用したアプリがトゥートの詳細ビューに表示されるようになります + setting_use_blurhash: ぼかしはメディアの色を元に生成されますが、細部は見えにくくなっています username: あなたのユーザー名は %{domain} の中で重複していない必要があります whole_word: キーワードまたはフレーズが英数字のみの場合、単語全体と一致する場合のみ適用されるようになります featured_tag: @@ -109,6 +110,7 @@ ja: setting_system_font_ui: システムのデフォルトフォントを使う setting_theme: サイトテーマ setting_unfollow_modal: フォローを解除する前に確認ダイアログを表示する + setting_use_blurhash: 非表示のメディアを色付きのぼかしで表示する severity: 重大性 type: インポートする項目 username: ユーザー名 diff --git a/config/locales/simple_form.oc.yml b/config/locales/simple_form.oc.yml index e0bfcfef9c..9453375b17 100644 --- a/config/locales/simple_form.oc.yml +++ b/config/locales/simple_form.oc.yml @@ -27,12 +27,14 @@ oc: phrase: Serà pres en compte que siá en majuscula o minuscula o dins un avertiment de contengut sensible scopes: A quinas APIs poiràn accedir las aplicacions. Se seleccionatz un encastre de naut nivèl, fa pas mestièr de seleccionar los nivèls mai basses. setting_aggregate_reblogs: Mostrar pas los nòus partatges que son estats partejats recentament (afecta pas que los nòus partatges recebuts) + setting_default_sensitive: Los mèdias sensibles son resconduts per defaut e se revelhan amb un clic setting_display_media_default: Rescondre los mèdias marcats coma sensibles setting_display_media_hide_all: Totjorn rescondre los mèdias setting_display_media_show_all: Totjorn mostrar los mèdias marcats coma sensibles setting_hide_network: Vòstre perfil mostrarà pas los que vos sègon e lo monde que seguètz setting_noindex: Aquò es destinat a vòstre perfil public e vòstra pagina d’estatuts setting_show_application: Lo nom de l’aplicacion qu’utilizatz per publicar serà mostrat dins la vista detalhada de vòstres tuts + setting_use_blurhash: Los degradats venon de las colors de l’imatge rescondut en enfoscar los detalhs username: Vòstre nom d’utilizaire serà unic sus %{domain} whole_word: Quand lo mot-clau o frasa es solament alfranumeric, serà pas qu’aplicat se correspond al mot complèt featured_tag: @@ -64,6 +66,7 @@ oc: warning_preset_id: Utilizar un avertiment predefinit defaults: autofollow: Convidar a sègre vòstre compte + avatar: Avatar bot: Aquò es lo compte a un robòt chosen_languages: Filtrar las lengas confirm_new_password: Confirmacion del nòu senhal @@ -83,9 +86,11 @@ oc: locked: Far venir lo compte privat max_uses: Limit d’utilizacions new_password: Nòu senhal + note: Bio otp_attempt: Còdi Two-factor password: Senhal phrase: Senhal o frasa + setting_advanced_layout: Activar l’interfàcia web avançada setting_aggregate_reblogs: Agropar los partatges dins lo flux d’actualitat setting_auto_play_gif: Lectura automatica dels GIFS animats setting_boost_modal: Mostrar una fenèstra de confirmacion abans de partejar un estatut @@ -105,6 +110,7 @@ oc: setting_system_font_ui: Utilizar la polissa del sistèma setting_theme: Tèma del site setting_unfollow_modal: Mostrar una confirmacion abans de quitar de sègre qualqu’un + setting_use_blurhash: Mostrar los degradats colorats pels mèdias resconduts severity: Severitat type: Tipe d’impòrt username: Nom d’utilizaire @@ -128,6 +134,8 @@ oc: reblog: Enviar un corrièl quand qualqu’un tòrna partejar vòstre estatut report: Enviar un corrièl pels nòus senhalaments 'no': Non + recommended: Recomandat required: + mark: "*" text: requesit 'yes': Òc diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml index 2f9bf5329f..553900855e 100644 --- a/config/locales/simple_form.pl.yml +++ b/config/locales/simple_form.pl.yml @@ -34,6 +34,7 @@ pl: setting_hide_network: Informacje o tym, kto Cię śledzi i kogo śledzisz nie będą widoczne setting_noindex: Wpływa na widoczność strony profilu i Twoich wpisów setting_show_application: W informacjach o wpisie będzie widoczna informacja o aplikacji, z której został wysłany + setting_use_blurhash: Gradienty są oparte na kolorach ukrywanej zawartości, ale uniewidaczniają wszystkie szczegóły username: Twoja nazwa użytkownika będzie niepowtarzalna na %{domain} whole_word: Jeśli słowo lub fraza składa się jedynie z liter lub cyfr, filtr będzie zastosowany tylko do pełnych wystąpień featured_tag: @@ -109,6 +110,7 @@ pl: setting_system_font_ui: Używaj domyślnej czcionki systemu setting_theme: Motyw strony setting_unfollow_modal: Pytaj o potwierdzenie przed cofnięciem śledzenia + setting_use_blurhash: Pokazuj kolorowe gradienty dla ukrytej zawartości multimedialnej severity: Priorytet type: Importowane dane username: Nazwa użytkownika diff --git a/config/locales/simple_form.ru.yml b/config/locales/simple_form.ru.yml index 26a73c3c6d..fcc1c2827a 100644 --- a/config/locales/simple_form.ru.yml +++ b/config/locales/simple_form.ru.yml @@ -10,7 +10,7 @@ ru: type_html: Выберите, что делать с аккаунтом %{acct} warning_preset_id: Необязательно. Вы можете добавить собственный текст в конце шаблона defaults: - autofollow: Люди, пришедшие по этому приглашению автоматически будут подписаны на Вас + autofollow: Люди, пришедшие по этому приглашению, автоматически будут подписаны на вас avatar: PNG, GIF или JPG. Максимально %{size}. Будет уменьшено до %{dimensions}px bot: Этот аккаунт обычно выполяет автоматизированные действия и может не просматриваться владельцем context: Один или несколько контекстов, к которым должны быть применены фильтры @@ -64,7 +64,7 @@ ru: suspend: Заблокировать и безвозвратно удалить все данные аккаунта warning_preset_id: Использовать шаблон defaults: - autofollow: Пригласите подписаться на Ваш аккаунт + autofollow: С подпиской на ваш аккаунт avatar: Аватар bot: Это аккаунт бота chosen_languages: Фильтр языков @@ -83,7 +83,7 @@ ru: irreversible: Удалять, а не скрывать locale: Язык интерфейса locked: Сделать аккаунт закрытым - max_uses: Макс. число использований + max_uses: Максимальное число использований new_password: Новый пароль note: О Вас otp_attempt: Двухфакторный код diff --git a/config/locales/simple_form.sk.yml b/config/locales/simple_form.sk.yml index 8470f79391..4ee251b262 100644 --- a/config/locales/simple_form.sk.yml +++ b/config/locales/simple_form.sk.yml @@ -109,6 +109,7 @@ sk: setting_system_font_ui: Použi základné systémové písmo setting_theme: Vzhľad webu setting_unfollow_modal: Vyžaduj potvrdenie pred skončením sledovania iného užívateľa + setting_use_blurhash: Ukáž farebné prechody pre skryté médiá severity: Závažnosť type: Typ importu username: Prezývka @@ -134,5 +135,6 @@ sk: 'no': Nie recommended: Odporúčané required: + mark: "*" text: povinné 'yes': Áno diff --git a/config/locales/simple_form.th.yml b/config/locales/simple_form.th.yml index 9d6b75ed0d..33a3c5a3ab 100644 --- a/config/locales/simple_form.th.yml +++ b/config/locales/simple_form.th.yml @@ -34,6 +34,7 @@ th: setting_hide_network: จะไม่แสดงผู้ที่คุณติดตามและผู้ที่ติดตามคุณในโปรไฟล์ของคุณ setting_noindex: มีผลต่อโปรไฟล์สาธารณะและหน้าสถานะของคุณ setting_show_application: จะแสดงแอปพลิเคชันที่คุณใช้เพื่อโพสต์ในมุมมองโดยละเอียดของโพสต์ของคุณ + setting_use_blurhash: การไล่ระดับสีอิงตามสีของภาพที่ซ่อนอยู่แต่ทำให้รายละเอียดใด ๆ คลุมเครือ username: ชื่อผู้ใช้ของคุณจะไม่ซ้ำกันบน %{domain} whole_word: เมื่อคำสำคัญหรือวลีมีแค่ตัวอักษรและตัวเลข จะถูกใช้หากตรงกันทั้งคำเท่านั้น featured_tag: @@ -109,6 +110,7 @@ th: setting_system_font_ui: ใช้แบบอักษรเริ่มต้นของระบบ setting_theme: ชุดรูปแบบไซต์ setting_unfollow_modal: แสดงกล่องโต้ตอบการยืนยันก่อนเลิกติดตามใครสักคน + setting_use_blurhash: แสดงการไล่ระดับสีที่มีสีสันสำหรับสื่อที่ซ่อนอยู่ severity: ความรุนแรง type: ชนิดการนำเข้า username: ชื่อผู้ใช้ diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml index b781deb32b..c62e57a518 100644 --- a/config/locales/simple_form.zh-CN.yml +++ b/config/locales/simple_form.zh-CN.yml @@ -15,7 +15,7 @@ zh-CN: bot: 来自这个帐户的绝大多数操作都是自动进行的,并且可能无人监控 context: 过滤器的应用场景 digest: 仅在你长时间未登录,且收到了私信时发送 - discoverable_html: 目录 让大家能根据兴趣和活动寻找用户。需要至少 %{min_followers} 位关注者 + discoverable_html: 用户目录 让大家能根据兴趣和活动寻找用户。需要至少 %{min_followers} 位关注者 email: 我们会向你发送一封确认邮件 fields: 这将会在个人资料页上以表格的形式展示,最多 4 个项目 header: 文件大小限制 %{size},只支持 PNG、GIF 或 JPG 格式。图片分辨率将会压缩至 %{dimensions}px @@ -34,6 +34,7 @@ zh-CN: setting_hide_network: 你关注的人和关注你的人将不会在你的个人资料页上展示 setting_noindex: 此设置会影响到你的公开个人资料以及嘟文页面 setting_show_application: 你用来发表嘟文的应用程序将会在你嘟文的详细内容中显示 + setting_use_blurhash: 渐变是基于模糊后的隐藏内容生成的 username: 你的用户名在 %{domain} 上是独特的 whole_word: 如果关键词只包含字母和数字,就只会在整个词被匹配时才会套用 featured_tag: @@ -73,7 +74,7 @@ zh-CN: context: 过滤器场景 current_password: 当前密码 data: 数据文件 - discoverable: 在本站用户资料目录中列出此账户 + discoverable: 在本站用户目录中收录此账户 display_name: 昵称 email: 电子邮件地址 expires_in: 失效时间 @@ -109,6 +110,7 @@ zh-CN: setting_system_font_ui: 使用系统默认字体 setting_theme: 站点主题 setting_unfollow_modal: 在取消关注前询问我 + setting_use_blurhash: 将隐藏媒体显示为彩色渐变 severity: 级别 type: 导入数据类型 username: 用户名 diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 75a43e322f..5577f93941 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -301,7 +301,7 @@ sk: affected_accounts: few: "%{count} účtov v databázi ovplyvnených" many: "%{count} účtov v databázi ovplyvnených" - one: Jeden účet v databázi ovplyvnený + one: "%{count} účet v databázi ovplyvnený" other: "%{count} účty v databázi ovplyvnené" retroactive: silence: Zruš stíšenie všetkých momentálne utíšených účtov z tejto domény @@ -481,13 +481,6 @@ sk: no_status_selected: Žiadne príspevky neboli zmenené, keďže si žiadne nemal/a zvolené title: Príspevky na účte with_media: S médiami - subscriptions: - callback_url: Zdrojová adresa URL - confirmed: Potvrdené - expires_in: Vyprší do - last_delivery: Posledné doručenie - title: WebSub - topic: Téma tags: accounts: Účty hidden: Skryté @@ -513,6 +506,7 @@ sk: subject: Nové hlásenie pre %{instance} (#%{id}) appearance: advanced_web_interface: Pokročilé webové rozhranie + advanced_web_interface_hint: 'Ak chceš využiť celkovú šírku tvojej obrazovky, pokročilé webové rozhranie ti umožňuje nastaviť mnoho rôznych stĺpcov, aby si videl/a toľko informácií naraz, koľko chceš: Domov, oboznámenia, federovanú časovú os, a ľubovolný počet zoznamov, či haštagov.' animations_and_accessibility: Animácie a prístupnosť confirmation_dialogs: Potvrdzovacie dialógy sensitive_content: Chúlostivý obsah @@ -657,12 +651,19 @@ sk: copy: Kopíruj order_by: Zoraď podľa save_changes: Ulož zmeny + validation_errors: + few: Niečo ešte nieje celkom v poriadku! Prosím skontroluj %{count} chýb uvedených nižšie + many: Niečo ešte nieje celkom v poriadku! Prosím skontroluj %{count} chýb uvedených nižšie + one: Niečo ešte nieje celkom v poriadku! Prosím skontroluj chybu uvedenú nižšie + other: Niečo ešte nieje celkom v poriadku! Prosím skontroluj %{count} chyby uvedené nižšie identity_proofs: active: Aktívne authorize: Áno, povoľ authorize_connection_prompt: Povoliť toto kryptografické prepojenie? errors: failed: Kryptografické prepojenie sa nepodarilo. Prosím skús to znova z %{provider}. + keybase: + verification_failed: Keybase nerozpoznáva tento token ako podpis od Keybase užívateľa menom %{kb_username}. Prosím skús to znova cez Keybase. i_am_html: Na %{service} som %{username}. identity: Identita inactive: Neaktívne @@ -768,6 +769,7 @@ sk: too_many_options: nemôže zahŕňať viac ako %{max} položiek preferences: other: Ostatné + posting_defaults: Východiskové nastavenia príspevkov public_timelines: Verejné časové osi relationships: activity: Aktivita účtu @@ -778,6 +780,7 @@ sk: mutual: Spoločné primary: Hlavné relationship: Vzťah + remove_selected_domains: Vymaž všetkých následovateľov z vybraných domén remove_selected_followers: Odstráň vybraných následovatrľov remove_selected_follows: Prestaň sledovať vybraných užívateľov status: Stav účtu @@ -798,10 +801,6 @@ sk: reply: proceed: Pokračuj odpovedaním prompt: 'Chceš odpovedať na tento príspevok:' - remote_unfollow: - error: Chyba - title: Názov - unfollowed: Už nesleduješ scheduled_statuses: over_daily_limit: Prekročil/a si denný limit %{limit} predplánovaných príspevkov over_total_limit: Prekročil/a si limit %{limit} predplánovaných príspevkov diff --git a/config/locales/sl.yml b/config/locales/sl.yml index 85e167ca9d..ff6c5851af 100644 --- a/config/locales/sl.yml +++ b/config/locales/sl.yml @@ -1,22 +1,33 @@ --- sl: about: - about_hashtag_html: To so javni tuti, označeni z #%{hashtag}. Z njimi se lahko povežete, če imate račun kjerkoli v fediversu. + about_hashtag_html: To so javni tuti, označeni z #%{hashtag}. Z njimi se lahko povežete, če imate račun kjerkoli v fediverse-u. about_mastodon_html: Mastodon je socialno omrežje, ki temelji na odprtih spletnih protokolih in prosti ter odprtokodni programski opremi. Je decentraliziran, kot e-pošta. about_this: O Mastodonu + active_count_after: dejaven + active_footnote: Aktivni mesečni uporabniki (AMU) administered_by: 'Upravlja:' + api: API apps: Mobilne aplikacije + apps_platforms: Uporabljajte Mastodon iz iOS, Android ali iz drugih platform + browse_directory: Brskajte po imeniku profilov in filtriranje po interesih + browse_public_posts: Brskajte javnih objav v živo na Mastodonu contact: Kontakt contact_missing: Ni nastavljeno contact_unavailable: Ni na voljo + discover_users: Odkrijte uporabnike documentation: Dokumentacija extended_description_html: |

    Dober prostor za pravila

    Razširjen opis še ni bil nastavljen.

    + federation_hint_html: Z računom na %{instance} boste lahko spremljali ljudi na kateremkoli Mastodon strežniku. generic_description: "%{domain} je en strežnik v omrežju" + get_apps: Poskusite mobilno aplikacijo hosted_on: Mastodon gostuje na %{domain} - learn_more: Spoznaj več - privacy_policy: Politika zasebnosti + learn_more: Nauči se več + privacy_policy: Pravilnik o zasebnosti + see_whats_happening: Poglejte, kaj se dogaja + server_stats: 'Statistika strežnika:' source_code: Izvorna koda status_count_after: few: stanja @@ -24,12 +35,13 @@ sl: other: stanj two: stanja status_count_before: Ki so avtorji + tagline: Sledite prijateljem in odkrijte nove terms: Pogoji storitve user_count_after: few: uporabniki one: uporabnik other: uporabnikov - two: uporabniki + two: uporabnika user_count_before: Dom za what_is_mastodon: Kaj je Mastodon? accounts: @@ -38,56 +50,61 @@ sl: followers: few: Sledilci one: Sledilec - other: Sledilci - two: Sledilci + other: Sledilcev + two: Sledilca following: Sledim joined: Se je pridružil na %{date} - last_active: zadnji aktivni + last_active: zadnja dejavnost link_verified_on: Lastništvo te povezave je bilo preverjeno na %{date} - media: Medij + media: Mediji moved_html: "%{name} se je prestavil na %{new_profile_link}:" - network_hidden: Te informacije niso na voljo - nothing_here: Nič ni tukaj! + network_hidden: Ta informacija ni na voljo + nothing_here: Tukaj ni ničesar! people_followed_by: Ljudje, ki jim sledi %{name} people_who_follow: Ljudje, ki sledijo %{name} pin_errors: following: Verjetno že sledite osebi, ki jo želite potrditi posts: - few: Trob - one: Trob - other: Trob - two: Trob - posts_tab_heading: Trobi + few: Tuti + one: Tut + other: Tutov + two: Tuta + posts_tab_heading: Tuti posts_with_replies: Tuti in odgovori reserved_username: Uporabniško ime je zasedeno roles: admin: Skrbnik bot: Robot + moderator: Mod + unavailable: Profil ni na voljo unfollow: Prenehaj slediti admin: account_actions: action: Izvedi dejanje - title: Izvedi moderirano dejanje %{acct} + title: Izvedi moderirano dejanje za %{acct} account_moderation_notes: create: Pusti opombo - created_msg: Uspešno ustvarjena opomba moderiranja! + created_msg: Moderirana opomba je uspešno ustvarjena! delete: Izbriši destroyed_msg: Moderirana opomba je uspešno uničena! accounts: - are_you_sure: Ali si prepričan? + approve: Odobri + approve_all: Odobri vse + are_you_sure: Ali ste prepričani? + avatar: Podoba by_domain: Domena change_email: changed_msg: E-pošta računa je uspešno spremenjena! - current_email: Trenutna E-pošta - label: Spremeni E-pošto - new_email: Nova E-pošta - submit: Spremeni E-pošto - title: Spremeni E-pošto za %{username} + current_email: Trenutna e-pošta + label: Spremeni e-pošto + new_email: Nova e-pošta + submit: Spremeni e-pošto + title: Spremeni e-pošto za %{username} confirm: Potrdi confirmed: Potrjeno confirming: Potrjujem deleted: Izbrisano - demote: Ponižaj + demote: Degradiraj disable: Onemogoči disable_two_factor_authentication: Onemogoči 2FA disabled: Onemogočeno @@ -95,44 +112,50 @@ sl: domain: Domena edit: Uredi email: E-pošta - email_status: Stanje E-pošte + email_status: Stanje e-pošte enable: Omogoči enabled: Omogočeno - feed_url: URL vir + feed_url: URL vira followers: Sledilci - followers_url: URL sledilci + followers_url: URL sledilcev follows: Sledi header: Glava - inbox_url: URl v mapi "Prejeto" + inbox_url: URL mape "Prejeto" invited_by: Povabljen od + ip: IP joined: Pridružil location: all: Vse - local: Lokalno + local: Lokalni remote: Oddaljeni title: Lokacija login_status: Stanje prijave - media_attachments: Medijske priloge + media_attachments: Predstavnostne priloge memorialize: Spremenite v spomin moderation: active: Dejaven all: Vse + pending: Na čakanju silenced: Utišan suspended: Suspendiran title: Moderiranje moderation_notes: Opombe moderiranja - most_recent_activity: Zadnja aktivnost + most_recent_activity: Zadnja dejavnost most_recent_ip: Zadnji IP + no_account_selected: Noben račun ni bil spremenjen, ker ni bil izbran noben no_limits_imposed: Brez omejitev - not_subscribed: Ni naročeno - outbox_url: URl za pošiljanje - perform_full_suspension: Začasno ustavi + not_subscribed: Ni naročen + outbox_url: URL za pošiljanje + pending: Čakanje na pregled + perform_full_suspension: Suspendiraj profile_url: URL profila - promote: Spodbujanje + promote: Promoviraj protocol: Protokol public: Javen push_subscription_expires: Naročnina PuSH preteče redownload: Osveži profil + reject: Zavrni + reject_all: Zavrni vse remove_avatar: Odstrani podobo remove_header: Odstrani glavo resend_confirmation: @@ -145,9 +168,11 @@ sl: role: Dovoljenja roles: admin: Skrbnik + moderator: Moderator staff: Osebje user: Uporabnik - search: Poišči + salmon_url: URL lososa + search: Iskanje shared_inbox_url: URL mape "Prejeto v skupni rabi" show: created_reports: Narejene prijave @@ -157,6 +182,7 @@ sl: statuses: Stanja subscribe: Naroči suspended: Suspendiran + time_in_queue: Čakanje v vrsti %{time} title: Računi unconfirmed_email: Nepotrjena e-pošta undo_silenced: Razveljavi utišanje @@ -171,27 +197,27 @@ sl: change_email_user: "%{name} je spremenil naslov e-pošte uporabnika %{target}" confirm_user: "%{name} je potrdil naslov e-pošte uporabnika %{target}" create_account_warning: "%{name} je poslal opozorilo %{target}" - create_custom_emoji: "%{name} je poslal nove emotikone %{target}" + create_custom_emoji: "%{name} je posodobil emotikone %{target}" create_domain_block: "%{name} je blokiral domeno %{target}" create_email_domain_block: "%{name} je dal na črni seznam e-pošto domene %{target}" demote_user: "%{name} je degradiral uporabnika %{target}" - destroy_custom_emoji: "%{name} je uničil emotikon %{target}" + destroy_custom_emoji: "%{name} je uničil emotikone %{target}" destroy_domain_block: "%{name} je odblokiral domeno %{target}" destroy_email_domain_block: "%{name} je dal na beli seznam e-pošto domene %{target}" destroy_status: "%{name} je odstranil stanje od %{target}" disable_2fa_user: "%{name} je onemogočil dvofaktorsko zahtevo za uporabnika %{target}" - disable_custom_emoji: "%{name} je onemogočil emotikon %{target}" + disable_custom_emoji: "%{name} je onemogočil emotikone %{target}" disable_user: "%{name} je onemogočil prijavo za uporabnika %{target}" - enable_custom_emoji: "%{name} je omogočil emotikon %{target}" + enable_custom_emoji: "%{name} je omogočil emotikone %{target}" enable_user: "%{name} je omogočil prijavo za uporabnika %{target}" memorialize_account: "%{name} je spremenil račun od %{target} v stran spominov" - promote_user: "%{name} je spodbudil uporabnika %{target}" + promote_user: "%{name} je promoviral uporabnika %{target}" remove_avatar_user: "%{name} je odstranil podobo od %{target}" reopen_report: "%{name} je ponovno odprl prijavo %{target}" reset_password_user: "%{name} je ponastavil geslo od uporabnika %{target}" resolve_report: "%{name} je razrešil prijavo %{target}" silence_account: "%{name} je utišal račun od %{target}" - suspend_account: "%{name} je začasno ustavil račun od %{target}" + suspend_account: "%{name} je suspendiral račun od %{target}" unassigned_report: "%{name} je nedodeljeno prijavil %{target}" unsilence_account: "%{name} je preklical utišanje računa od %{target}" unsuspend_account: "%{name} je aktiviral račun od %{target}" @@ -201,9 +227,9 @@ sl: title: Dnevnik revizije custom_emojis: by_domain: Domena - copied_msg: Lokalna kopija emotikona je bila uspešno ustvarjena + copied_msg: Lokalna kopija emotikonov je bila uspešno ustvarjena copy: Kopiraj - copy_failed_msg: Lokalne kopije emotikona ni bilo mogoče ustvariti + copy_failed_msg: Lokalne kopije emotikonov ni bilo mogoče ustvariti created_msg: Emotikon je uspešno ustvarjen! delete: Izbriši destroyed_msg: Emotikon je uspešno uničen! @@ -225,13 +251,14 @@ sl: updated_msg: Emotikon je uspešno posodobljen! upload: Pošlji dashboard: - backlog: Zaostala opravila + backlog: zaostala opravila config: Nastavitve feature_deletions: Brisanje računov - feature_invites: Poveza povabil - feature_profile_directory: Mapa profila + feature_invites: Povezave povabil + feature_profile_directory: Imenik profilov feature_registrations: Registracije feature_relay: Rele federacije + feature_timeline_preview: Predogled časovnice features: Zmožnosti hidden_service: Federacija s skritimi storitvami open_reports: odprte prijave @@ -241,7 +268,7 @@ sl: software: Programska oprema space: Uporaba prostora title: Nadzorna plošča - total_users: Skupaj uporabnikov + total_users: skupaj uporabnikov trends: Trendi week_interactions: interakcije ta teden week_users_active: aktivni ta teden @@ -251,12 +278,13 @@ sl: created_msg: Domenski blok se sedaj obdeluje destroyed_msg: Domenski blok je bil razveljavljen domain: Domena + existing_domain_block_html: Uvedli ste strožje omejitve za %{name}, sedaj ga morate najprej odblokirati. new: create: Ustvari blok hint: Domenski blok ne bo preprečil ustvarjanja vnosov računov v zbirko podatkov, ampak bo retroaktivno in samodejno uporabil posebne metode moderiranja na teh računih. severity: desc_html: "Utišaj bo vse objave računa naredil nevidne vsem, ki jih ne sledijo. Suspendiraj bo odstranil vso vsebino, medije in podatke profila računa. Uporabi nič, če želite le zavrniti predstavnostne datoteke." - noop: Nič + noop: Brez silence: Utišaj suspend: Suspendiraj title: Nov domenski blok @@ -271,13 +299,13 @@ sl: suspend: suspendirani show: affected_accounts: - few: "%{count} računov v bazi podatkov so prizadeti" + few: "%{count} računi v bazi podatkov so prizadeti" one: En račun v bazi podatkov je prizadet - other: "%{count} računov v bazi podatkov so prizadeti" - two: "%{count} računov v bazi podatkov so prizadeti" + other: "%{count} računov v bazi podatkov je prizadetih" + two: "%{count} računa v bazi podatkov so prizadeta" retroactive: silence: Prekliči utišanje za vse obstoječe račune iz te domene - suspend: Odsuspendiraj vse obstoječe račune iz te domene + suspend: Aktiviraj vse obstoječe račune iz te domene title: Razveljavi domenski blok za %{domain} undo: Razveljavi undo: Razveljavi domenski blok @@ -290,17 +318,18 @@ sl: new: create: Dodaj domeno title: Nov vnos e-pošte na črni seznam - title: Črni seznam e-pošte + title: Črni seznam e-pošt followers: back_to_account: Nazaj na račun title: Sledilci od %{acct} instances: + by_domain: Domena delivery_available: Na voljo je dostava known_accounts: - few: "%{count} znanih računov" + few: "%{count} znani računi" one: "%{count} znan račun" other: "%{count} znanih računov" - two: "%{count} znanih računov" + two: "%{count} znana računa" moderation: all: Vse limited: Omejeno @@ -317,15 +346,18 @@ sl: all: Vse available: Razpoložljivo expired: Potekel + title: Filter title: Povabila + pending_accounts: + title: "(%{count}) računov na čakanju" relays: add_new: Dodaj nov rele delete: Izbriši - description_html: "Rele federacije je posredniški strežnik, ki si izmenjuje velike količine javnih trobov med strežniki, ki so se naročili in objavili na njem. Majhnim in srednjim strežnikom lahko pomaga pri odkrivanju vsebine iz sistema fediverse, kar bi sicer zahtevalo, da lokalni uporabniki ročno sledijo druge osebe na oddaljenih strežnikih." + description_html: "Rele federacije je posredniški strežnik, ki si izmenjuje velike količine javnih tutov med strežniki, ki so se naročili in objavili na njem. Majhnim in srednjim strežnikom lahko pomaga pri odkrivanju vsebine iz sistema fediverse, kar bi sicer zahtevalo, da lokalni uporabniki ročno sledijo druge osebe na oddaljenih strežnikih." disable: Onemogoči disabled: Onemogočeno enable: Omogoči - enable_hint: Ko je omogočen, se bo vaš strežnik naročil na vse javne trobe iz tega releja in začel pošiljati javne trobe tega strežnika. + enable_hint: Ko je omogočen, se bo vaš strežnik naročil na vse javne tute iz tega releja in začel pošiljati javne tute tega strežnika. enabled: Omogočeno inbox_url: URL releja pending: Čakanje na odobritev releja @@ -345,7 +377,7 @@ sl: assign_to_self: Dodeli meni assigned: Dodeljen moderator comment: - none: Nič + none: Brez created_at: Prijavljeno mark_as_resolved: Označi kot rešeno mark_as_unresolved: Označi kot nerešeno @@ -359,19 +391,19 @@ sl: report: 'Prijavi #%{id}' reported_account: Prijavljeni račun reported_by: Prijavljen od - resolved: Razrešeno + resolved: Razrešeni resolved_msg: Prijava je uspešno razrešena! status: Stanje title: Prijave - unassign: Odstopi - unresolved: Nerešeno - updated_at: Posodobljen + unassign: Odstopljeni + unresolved: Nerešeni + updated_at: Posodobljeni settings: activity_api_enabled: desc_html: Številke lokalno objavljenih stanj, aktivnih uporabnikov in novih registracij na tedenskih seznamih title: Objavi združeno statistiko o dejavnosti uporabnikov bootstrap_timeline_accounts: - desc_html: Več uporabniških imen ločite z vejico. Deluje samo na lokalnih in odklenjenih računih. Privzeto, ko je prazno, pri vseh lokalnih skrbnikih. + desc_html: Več uporabniških imen ločite z vejico. Deluje samo na lokalnih in odklenjenih računih. Privzeto, ko je prazno, je pri vseh lokalnih skrbnikih. title: Privzeta sledenja za nove uporabnike contact_information: email: Poslovna e-pošta @@ -379,21 +411,382 @@ sl: custom_css: desc_html: Spremeni videz z naloženim CSS na vsaki strani title: CSS po meri + hero: + desc_html: Prikazano na sprednji strani. Priporoča se vsaj 600x100px. Ko ni nastavljen, se vrne na sličico strežnika + title: Slika junaka + mascot: + desc_html: Prikazano na več straneh. Priporočena je najmanj 293 × 205 px. Ko ni nastavljen, se vrne na privzeto maskoto + title: Slika maskote + peers_api_enabled: + desc_html: Domene, na katere je ta strežnik naletel na fediverse-u + title: Objavi seznam odkritih strežnikov + preview_sensitive_media: + desc_html: Predogledi povezav na drugih spletiščih bodo prikazali sličico, tudi če je medij označen kot občutljiv + title: Prikaži občutljive medije v predogledih OpenGraph + profile_directory: + desc_html: Dovoli uporabnikom, da jih lahko odkrijejo + title: Omogoči imenik profilov + registrations: + closed_message: + desc_html: Prikazano na prvi strani, ko so registracije zaprte. Lahko uporabite oznake HTML + title: Sporočilo o zaprti registraciji + deletion: + desc_html: Dovoli vsakomur, da izbriše svoj račun + title: Odpri brisanje računa + min_invite_role: + disabled: Nihče + title: Dovoli vabila od + registrations_mode: + modes: + approved: Potrebna je odobritev za prijavo + none: Nihče se ne more prijaviti + open: Vsakdo se lahko prijavi + title: Način registracije + show_known_fediverse_at_about_page: + desc_html: Ko preklopite, bo prikazal tute vseh znanih fediverse-ov v predogledu. V nasprotnem primeru bodo prikazani samo lokalni tuti. + title: Pokaži znane fediverse-e v predogledu časovnice + show_staff_badge: + desc_html: Prikaži značko osebja na uporabniški strani + title: Prikaži značko osebja + site_description: + desc_html: Uvodni odstavek na API-ju. Opišite, zakaj je ta Mastodon strežnik poseben in karkoli pomembnega. Lahko uporabite HTML oznake, zlasti <a> in <em>. + title: Opis strežnika + site_description_extended: + desc_html: Dober kraj za vaš kodeks ravnanja, pravila, smernice in druge stvari, ki ločujejo vaš strežnik. Lahko uporabite oznake HTML + title: Razširjene informacije po meri + site_short_description: + desc_html: Prikazano v stranski vrstici in metaoznakah. V enem odstavku opišite, kaj je Mastodon in kaj naredi ta strežnik poseben. + title: Kratek opis strežnika + site_terms: + desc_html: Lahko napišete svojo pravilnik o zasebnosti, pogoje storitve ali druge pravne dokumente. Lahko uporabite oznake HTML + title: Pogoji storitve po meri + site_title: Ime strežnika + thumbnail: + desc_html: Uporablja se za predogled prek OpenGrapha in API-ja. Priporočamo 1200x630px + title: Sličica strežnika + timeline_preview: + desc_html: Prikaži javno časovnico na ciljni strani + title: Predogled časovnice + title: Nastavitve strani + statuses: + back_to_account: Nazaj na stran računa + batch: + delete: Izbriši + nsfw_off: Označi, da ni občutljivo + nsfw_on: Označi, kot občutljivo + failed_to_execute: Ni bilo mogoče izvesti + media: + title: Mediji + no_media: Ni medijev + no_status_selected: Nobeno stanje ni bilo spremenjeno, ker ni bilo izbrano nobeno + title: Stanja računa + with_media: Z mediji + tags: + accounts: Računi + hidden: Skriti + hide: Skrij iz imenika + name: Ključnik + title: Ključniki + unhide: Prikaži v imeniku + visible: Vidni + title: Upravljanje + warning_presets: + add_new: Dodaj novo + delete: Izbriši + edit: Uredi + edit_preset: Uredi prednastavitev opozoril + title: Upravljaj prednastavitev opozoril + admin_mailer: + new_pending_account: + body: Podrobnosti o novem računu so navedene spodaj. To aplikacijo lahko odobrite ali zavrnete. + subject: Nov račun za pregled na %{instance} (%{username}) + new_report: + body: "%{reporter} je prijavil %{target}" + body_remote: Nekdo iz %{domain} je prijavil %{target} + subject: Nove prijave za %{instance} (#%{id}) + appearance: + advanced_web_interface: Napredni spletni vmesnik + advanced_web_interface_hint: 'Če želite uporabiti celotno širino zaslona, vam napredni spletni vmesnik omogoča, da si nastavite več različnih stolpcev in da si hkrati ogledate toliko informacij, kot želite: domačo stran, obvestila, združeno časovnico, poljubno število seznamov in ključnikov.' + animations_and_accessibility: Animacije in dostopnost + confirmation_dialogs: Potrditvena okna + sensitive_content: Občutljiva vsebina + application_mailer: + notification_preferences: Spremenite e-poštne nastavitve + salutation: "%{name}," + settings: 'Spremenite e-poštne nastavitve: %{link}' + view: 'Pogled:' + view_profile: Ogled profila + view_status: Ogled stanja + applications: + created: Aplikacija je bila uspešno ustvarjena + destroyed: Aplikacija je bila uspešno izbrisana + invalid_url: Navedeni URL je neveljaven + regenerate_token: Obnovite dostopni žeton + token_regenerated: Dostopni žeton je bil uspešno regeneriran + warning: Bodite zelo previdni s temi podatki. Nikoli jih ne delite z nikomer! + your_token: Vaš dostopni žeton + auth: + apply_for_account: Zahtevajte povabilo + change_password: Geslo + checkbox_agreement_html: Strinjam se s pravili strežnika in pogoji storitve + confirm_email: Potrdi e-pošto + delete_account: Izbriši račun + delete_account_html: Če želite izbrisati svoj račun, lahko nadaljujete tukaj. Prosili vas bomo za potrditev. + didnt_get_confirmation: Niste prejeli navodil za potrditev? + forgot_password: Ste pozabili svoje geslo? + invalid_reset_password_token: Žeton za ponastavitev gesla je neveljaven ali je potekel. Zahtevajte novo. + login: Prijava + logout: Odjava + migrate_account: Premakni se na drug račun + migrate_account_html: Če želite ta račun preusmeriti na drugega, ga lahko nastavite tukaj. + or_log_in_with: Ali se prijavite z + providers: + cas: CAS + saml: SAML + register: Vpis + registration_closed: "%{instance} ne sprejema novih članov" + resend_confirmation: Ponovno pošlji navodila za potrditev + reset_password: Ponastavi geslo + security: Varnost + set_new_password: Nastavi novo geslo + trouble_logging_in: Težave pri prijavi? + authorize_follow: + already_following: Temu računu že sledite + error: Na žalost je prišlo do napake pri iskanju oddaljenega računa + follow: Sledi + follow_request: 'Prošnjo za sledenje se poslali:' + following: 'Uspeh! Zdaj sledite:' + post_follow: + close: Lahko pa tudi zaprete to okno. + return: Prikaži uporabnikov profil + web: Pojdi na splet + title: Sledi %{acct} + datetime: + distance_in_words: + about_x_hours: "%{count}h" + about_x_months: "%{count}mo" + about_x_years: "%{count}y" + almost_x_years: "%{count}y" + half_a_minute: Pravkar + less_than_x_minutes: "%{count}m" + less_than_x_seconds: Pravkar + over_x_years: "%{count}y" + x_days: "%{count}d" + x_minutes: "%{count}m" + x_months: "%{count}mo" + x_seconds: "%{count}s" + deletes: + bad_password_msg: Lep poskus, hekerji! napačno geslo + confirm_password: Vnesite svoje trenutno geslo, da potrdite svojo identiteto + description_html: S tem boste trajno, nepovratno odstranili vsebino iz vašega računa in jo deaktivirali. Vaše uporabniško ime bo ostalo rezervirano za preprečevanje prihodnjih lažnih predstav. + proceed: Izbriši račun + success_msg: Vaš račun je bil uspešno izbrisan + warning_html: Zagotovljeno je samo brisanje vsebine iz tega strežnika. Vsebina, ki je široko razširjena, bo verjetno pustila sledi. Strežniki brez povezave in strežniki, ki so se odjavili od vaših posodobitev, ne bodo posodabljali svojih podatkovnih baz. + warning_title: Razširjena razpoložljivost vsebine + directories: + directory: Imenik profilov + enabled: Trenutno ste navedeni v imeniku. + enabled_but_waiting: Vključili ste, da ste navedeni v imeniku, vendar še nimate najmanjšega števila sledilcev (%{min_followers}), da bi vas prikazalo. + explanation: Odkrijte uporabnike glede na njihove interese + explore_mastodon: Razišči %{title} + how_to_enable: Trenutno niste vključeni v imenik. Spodaj se lahko vključite. Uporabite ključnike v vaši biografiji, da boste navedeni pod specifične ključnike! + people: + few: "%{count} osebe" + one: "%{count} oseba" + other: "%{count} oseb" + two: "%{count} osebi" errors: - '403': You don't have permission to view this page. - '404': The page you are looking for isn't here. - '410': The page you were looking for doesn't exist here anymore. - '422': - '429': Throttled - '500': + '403': Nimate dovoljenja za ogled te strani. + '404': Iskana stran ne obstaja. + '410': Iskana stran ne obstaja več. + '422': + content: Varnostno preverjanje ni uspelo. Ali blokirate piškotke? + title: Varnostno preverjanje je spodletelo + '429': Omejeno + '500': + content: Žal nam je, toda na našem koncu je prišlo do napake. + title: Ta stran ni pravilna + noscript_html: Če želite uporabljati spletno aplikacijo Mastodon, omogočite JavaScript. Druga možnost je, da za svojo platformo poskusite eno od lastnih aplikacij za Mastodon. + existing_username_validator: + not_found: s tem uporabniškim imenom ni bilo mogoče najti lokalnega uporabnika + not_found_multiple: ni bilo mogoče najti %{usernames} + exports: + archive_takeout: + date: Datum + download: Prenesi svoj arhiv + hint_html: Zahtevate lahko arhiv vaših tutov in naloženih medijev. Izvoženi podatki bodo v formatu ActivityPub, ki ga bo mogoče brati s katerokoli skladno programsko opremo. Arhiv lahko zahtevate vsakih 7 dni. + in_progress: Prevajanje arhiva... + request: Zahtevajte svoj arhiv + size: Velikost + blocks: Blokirate + csv: CSV + domain_blocks: Bloki domene + follows: Sledite + lists: Seznami + mutes: Utišate + storage: Shranjeni mediji + featured_tags: + add_new: Dodaj novo + errors: + limit: Ste že dodali največje število ključnikov + filters: + contexts: + home: Domača časovnica + notifications: Obvestila + public: Javne časovnice + thread: Pogovori + edit: + title: Uredite filter + errors: + invalid_context: Ne vsebuje nobenega ali vsebuje neveljaven kontekst + invalid_irreversible: Nepovratno filtriranje deluje le v kontekstu doma ali obvestil + index: + delete: Izbriši + title: Filtri + new: + title: Dodaj nov filter + footer: + developers: Razvijalci + more: Več… + resources: Viri + generic: + all: Vse + changes_saved_msg: Spremembe so uspešno shranjene! + copy: Kopiraj + order_by: Razvrsti po + save_changes: Shrani spremembe + validation_errors: + few: Nekaj še ni čisto v redu! Spodaj si oglejte %{count} napake + one: Nekaj še ni čisto v redu! Spodaj si oglejte napako + other: Nekaj še ni čisto v redu! Spodaj si oglejte %{count} napak + two: Nekaj še ni čisto v redu! Spodaj si oglejte %{count} napaki + html_validator: + invalid_markup: 'vsebuje neveljavno oznako HTML: %{error}' + identity_proofs: + active: Dejaven + authorize: Da, odobri + authorize_connection_prompt: Odobrite to kriptografsko povezavo? + errors: + failed: Kriptografska povezava ni uspela. Poskusite znova od %{provider}. + keybase: + invalid_token: Žetoni Keybase so algoritem podpisov in morajo biti sestavljeni iz 66 heksadecimalnih znakov + verification_failed: Keybase ne prepozna tega žetona kot podpis uporabnika %{kb_username}. Poskusite znova s Keybase-om. + wrong_user: Dokler se prijavite kot %{current}, ni mogoče ustvariti dokazila za %{proving}. Prijavite se kot %{proving} in poskusite znova. + explanation_html: Tukaj lahko kriptografsko povežete druge identitete, na primer profil Keybase. To omogoča drugim, da vam pošljejo šifrirana sporočila in zaupate vsebino, ki ste jo poslali. + i_am_html: Jaz sem %{username} na %{service}. + identity: Identiteta + inactive: Neaktiven + publicize_checkbox: 'In to tutnite:' + publicize_toot: 'Dokazano je! Jaz sem %{username} na %{service}: %{url}' + status: Stanje preverjanja + view_proof: Oglejte si dokaz + imports: + modes: + merge: Združi + merge_long: Ohrani obstoječe zapise in dodaj nove + overwrite: Prepiši + overwrite_long: Zamenjaj trenutne zapise z novimi + preface: Podatke, ki ste jih izvozili iz drugega strežnika, lahko uvozite. Na primer seznam oseb, ki jih spremljate ali blokirate. + success: Vaši podatki so bili uspešno naloženi in bodo zdaj pravočasno obdelani + types: + blocking: Seznam blokiranih + domain_blocking: Seznam blokiranih domen + following: Seznam uporabnikov, katerim sledite + muting: Seznam utišanih + upload: Pošlji + in_memoriam_html: V spomin. invites: + delete: Onemogoči + expired: Poteklo expires_in: - '1800': 30 minutes - '21600': 6 hours - '3600': 1 hour - '43200': 12 hours - '604800': 1 week - '86400': 1 day + '1800': 30 minut + '21600': 6 ur + '3600': 1 ura + '43200': 12 ur + '604800': 1 teden + '86400': 1 dan + expires_in_prompt: Nikoli + generate: Ustvari + invited_by: 'Povabil/a vas je:' + max_uses: + few: "%{count} uporabe" + one: 1 uporaba + other: "%{count} uporab" + two: "%{count} uporabi" + max_uses_prompt: Brez omejitve + prompt: Ustvarite in delite povezave z drugimi, da omogočite dostop do tega strežnika + table: + expires_at: Poteče + uses: Uporabe + title: Povabite ljudi + lists: + errors: + limit: Dosegli ste največje število seznamov + media_attachments: + validations: + images_and_video: Videoposnetka ni mogoče priložiti stanju, ki že vsebuje slike + too_many: Ni možno priložiti več kot 4 datoteke + migrations: + acct: username@domain novega računa + currently_redirecting: 'Vaš profil je preusmerjen na:' + proceed: Shrani + updated_msg: Nastavitev selitve računa je bila uspešno posodobljena! + moderation: + title: Moderiranje + notification_mailer: + digest: + action: Prikaži vsa obvestila + body: Tukaj je kratek povzetek sporočil, ki ste jih zamudili od vašega zadnjega obiska v %{since} + mention: "%{name} vas je omenil/a v:" + new_followers_summary: + few: Prav tako ste pridobili %{count} nove sledilce, ko ste bili odsotni! Juhu! + one: Prav tako ste pridobili enega novega sledilca, ko ste bili odsotni! Juhu! + other: Prav tako ste pridobili %{count} novih sledilcev, ko ste bili odsotni! Juhu! + two: Prav tako ste pridobili %{count} nova sledilca, ko ste bili odsotni! Juhu! + subject: + few: "%{count} nova obvestila od vašega zadnjega obiska \U0001F418" + one: "1 novo obvestilo od vašega zadnjega obiska \U0001F418" + other: "%{count} novih obvestil od vašega zadnjega obiska \U0001F418" + two: "%{count} novi obvestili od vašega zadnjega obiska \U0001F418" + title: V vaši odsotnosti... + favourite: + body: "%{name} je vzljubil/a vaše stanje:" + subject: "%{name} je vzljubil/a vaše stanje" + title: Novo priljubljeno + follow: + body: "%{name} vam sedaj sledi!" + subject: "%{name} vam sedaj sledi" + title: Novi sledilec + follow_request: + action: Upravljajte s prošnjami za sledenje + body: "%{name} vas je prosil/a za sledenje" + subject: 'Čakajoči sledilec/ka: %{name}' + title: Nova prošnja za sledenje + mention: + action: Odgovori + body: "%{name} vas je omenil/a v:" + subject: "%{name} vas je omenil/a" + title: Nova omemba + reblog: + body: "%{name} je spodbudil/a vaše stanje:" + subject: "%{name} je spodbudil/a vaše stanje" + title: Nova spodbuda + number: + human: + decimal_units: + format: "%n%u" + units: + billion: B + million: M + quadrillion: Q + thousand: K + trillion: T + pagination: + newer: Novejše + next: Naprej + older: Starejše + prev: Nazaj + truncate: "…" statuses: pin_errors: ownership: Trob nekoga drugega ne more biti pripet diff --git a/config/locales/sq.yml b/config/locales/sq.yml index 6cab033321..cbe2256469 100644 --- a/config/locales/sq.yml +++ b/config/locales/sq.yml @@ -423,12 +423,6 @@ sq: no_status_selected: S’u ndryshua ndonjë gjendje, ngaqë s’u përzgjodh ndonjë e tillë title: Gjendje llogarish with_media: Me media - subscriptions: - callback_url: URL Callback-u - confirmed: U ripohua - expires_in: Skadon më - last_delivery: Dorëzimi e fundit - topic: Temë tags: accounts: Llogari hidden: Fshehur @@ -686,10 +680,6 @@ sq: reply: proceed: Ripohoni përgjigjen prompt: 'Doni t’i përgjigjeni këtij mesazhi:' - remote_unfollow: - error: Gabim - title: Titull - unfollowed: U hoq ndjekja scheduled_statuses: over_daily_limit: Keni tejkaluar kufirin e %{limit} mesazheve të planifikuara për atë ditë over_total_limit: Keni tejkaluar kufirin prej %{limit} mesazhesh të planifikuara diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml index 3310716e0f..6530d4c762 100644 --- a/config/locales/sr-Latn.yml +++ b/config/locales/sr-Latn.yml @@ -263,10 +263,6 @@ sr-Latn: no_media: Bez multimedije title: Statusi naloga with_media: Sa multimedijom - subscriptions: - confirmed: Potvrđeno - expires_in: Ističe za - last_delivery: Poslednja dostava title: Administracija admin_mailer: new_report: diff --git a/config/locales/sr.yml b/config/locales/sr.yml index 1555fb235d..88db0c4f45 100644 --- a/config/locales/sr.yml +++ b/config/locales/sr.yml @@ -440,10 +440,6 @@ sr: no_status_selected: Ниједан статус није промењен јер ниједан није изабран title: Статуси налога with_media: Са мултимедијом - subscriptions: - confirmed: Потврђено - expires_in: Истиче за - last_delivery: Последња достава tags: accounts: Налози hidden: Скривено @@ -693,10 +689,6 @@ sr: reply: proceed: Наставите да бисте одговорили prompt: 'Желите да одговорите на ову трубу:' - remote_unfollow: - error: Грешка - title: Наслов - unfollowed: Отпраћени scheduled_statuses: over_daily_limit: Прекорачили сте границу од %{limit} планираних труба за тај дан over_total_limit: Прекорачили сте границу од %{limit} планираних труба diff --git a/config/locales/sv.yml b/config/locales/sv.yml index d3d0cb888d..c123e2889e 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -313,12 +313,6 @@ sv: no_media: Ingen media title: Kontostatus with_media: med media - subscriptions: - callback_url: Återanrop URL - confirmed: Bekräftad - expires_in: Utgår om - last_delivery: Sista leverans - topic: Ämne admin_mailer: new_report: body: "%{reporter} har rapporterat %{target}" @@ -513,10 +507,6 @@ sv: missing_resource: Det gick inte att hitta den begärda omdirigeringsadressen för ditt konto proceed: Fortsätt för att följa prompt: 'Du kommer att följa:' - remote_unfollow: - error: Fel - title: Titel - unfollowed: Slutade följa sessions: activity: Senaste aktivitet browser: Webbläsare diff --git a/config/locales/th.yml b/config/locales/th.yml index 7a16bc2f32..a009e4ebbd 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -335,13 +335,6 @@ th: title: สื่อ no_media: ไม่มีสื่อ title: สถานะบัญชี - subscriptions: - callback_url: URL เรียกกลับ - confirmed: ยืนยันแล้ว - expires_in: หมดอายุภายใน - last_delivery: ส่งล่าสุด - title: WebSub - topic: หัวข้อ tags: accounts: บัญชี hidden: ซ่อนอยู่ @@ -360,6 +353,7 @@ th: appearance: advanced_web_interface: ส่วนติดต่อเว็บขั้นสูง animations_and_accessibility: ภาพเคลื่อนไหวและการช่วยการเข้าถึง + confirmation_dialogs: กล่องโต้ตอบการยืนยัน sensitive_content: เนื้อหาที่ละเอียดอ่อน application_mailer: notification_preferences: เปลี่ยนการกำหนดลักษณะอีเมล @@ -537,6 +531,7 @@ th: prev: ก่อนหน้า truncate: "…" preferences: + other: อื่น ๆ posting_defaults: ค่าเริ่มต้นการโพสต์ public_timelines: เส้นเวลาสาธารณะ relationships: @@ -562,10 +557,6 @@ th: reply: proceed: ดำเนินการต่อเพื่อตอบกลับ prompt: 'คุณต้องการตอบกลับโพสต์นี้:' - remote_unfollow: - error: ข้อผิดพลาด - title: ชื่อเรื่อง - unfollowed: เลิกติดตามแล้ว sessions: activity: กิจกรรมล่าสุด browser: เบราว์เซอร์ diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 3113e7a08f..5929e1e071 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -207,12 +207,6 @@ tr: title: Sunucu hakkında detaylı bilgi site_title: Site başlığı title: Site Ayarları - subscriptions: - callback_url: Callback linki - confirmed: Onaylandı - expires_in: Bitiş Tarihi - last_delivery: Son gönderim - topic: Konu tags: accounts: Hesaplar name: Etiketler diff --git a/config/locales/uk.yml b/config/locales/uk.yml index e027b6baee..c2d422474d 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -349,11 +349,6 @@ uk: no_media: Немає медіа title: Статуси аккаунтів with_media: З медіа - subscriptions: - confirmed: Підтверджено - expires_in: Спливає через - last_delivery: Остання доставка - topic: Тема title: Адміністрування admin_mailer: new_report: @@ -569,10 +564,6 @@ uk: no_account_html: Не маєте аккаунту? Не біда, ви можете зареєструватися proceed: Перейти до підписки prompt: 'Ви хочете підписатися на:' - remote_unfollow: - error: Помилка - title: Заголовок - unfollowed: Відписані sessions: activity: Остання активність browser: Браузер diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 0c9b291ad3..42ab59d506 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -10,7 +10,7 @@ zh-CN: api: API apps: 移动应用 apps_platforms: 在 iOS、Android 和其他平台上使用 Mastodon - browse_directory: 浏览用户资料目录并按兴趣筛选 + browse_directory: 浏览用户目录并按兴趣筛选 browse_public_posts: 浏览 Mastodon 上公共嘟文的实时信息流 contact: 联系方式 contact_missing: 未设定 @@ -32,7 +32,7 @@ zh-CN: status_count_after: other: 条嘟文 status_count_before: 他们共嘟出了 - tagline: 关注朋友并发现新朋友 + tagline: 关注并发现新朋友 terms: 使用条款 user_count_after: other: 位用户 @@ -243,9 +243,9 @@ zh-CN: config: 服务器配置 feature_deletions: 帐户删除 feature_invites: 邀请链接 - feature_profile_directory: 用户资料目录 + feature_profile_directory: 用户目录 feature_registrations: 公开注册 - feature_relay: 同步中继 + feature_relay: 中继服务器 feature_timeline_preview: 时间轴预览 features: 功能 hidden_service: 匿名服务连通性 @@ -333,9 +333,9 @@ zh-CN: pending_accounts: title: 待处理的帐户 (%{count}) relays: - add_new: 添加新的中继 + add_new: 订阅新的中继 delete: 删除 - description_html: "同步中继是一种中间服务器,各实例可以通过订阅中继和向中继推送信息的方式来大量交换公开嘟文。它可以帮助中小型实例发现网络中的内容,而无需本地用户手动关注其他远程实例上的用户。" + description_html: "中继服务器是一个信息统合服务器,各服务器可以通过订阅中继服务器和向中继服务器推送信息来交换大量公开嘟文。它可以帮助中小型服务器发现联邦宇宙中的其他服务器的内容,而无需本站用户手动关注其他远程服务器上的用户。" disable: 禁用 disabled: 已禁用 enable: 启用 @@ -400,14 +400,14 @@ zh-CN: desc_html: 用于在首页展示。推荐分辨率 293×205px 以上。未指定的情况下将使用默认吉祥物。 title: 吉祥物图像 peers_api_enabled: - desc_html: 截至目前本实例在网络中已发现的域名 + desc_html: 截至目前本服务器在联邦宇宙中已发现的域名 title: 公开已知实例的列表 preview_sensitive_media: desc_html: 始终在站外链接预览中展示缩略图,无论媒体内容是否标记为敏感 title: 在 OpenGraph 预览中显示敏感媒体内容 profile_directory: - desc_html: 允许用户可被发现 - title: 启用用户资料目录 + desc_html: 允许用户被发现 + title: 启用用户目录 registrations: closed_message: desc_html: 本站关闭注册期间的提示信息。可以使用 HTML 标签 @@ -463,20 +463,13 @@ zh-CN: no_status_selected: 因为没有嘟文被选中,所以没有更改 title: 帐户嘟文 with_media: 含有媒体文件 - subscriptions: - callback_url: 回调 URL - confirmed: 已确认 - expires_in: 失效时间 - last_delivery: 最后一次接收数据的时间 - title: WebSub - topic: 话题 tags: accounts: 帐户 hidden: 隐藏 - hide: 从目录隐藏 + hide: 从用户目录中隐藏 name: 话题标签 title: 话题标签 - unhide: 在目录中显示 + unhide: 在用户目录中显示 visible: 可见 title: 管理 warning_presets: @@ -573,12 +566,12 @@ zh-CN: warning_html: 我们只能保证本服务器上的内容将会被彻底删除。对于已经被广泛传播的内容,它们在本服务器以外的某些地方可能仍然可见。此外,失去连接的服务器以及停止接收订阅的服务器所存储的数据亦无法删除。 warning_title: 关于已传播的内容的警告 directories: - directory: 用户资料目录 - enabled: 您目前已被列入目录中。 - enabled_but_waiting: 您已选择列入目录,但是您没有达到关注者数量下限 (%{min_followers} 名) 。 + directory: 用户目录 + enabled: 您已被收录在用户目录中。 + enabled_but_waiting: 你已选择将账号收录到用户目录中,但是你的关注者不足 (%{min_followers}) 人 。 explanation: 根据兴趣发现用户 explore_mastodon: 探索 %{title} - how_to_enable: 您目前没有选择选择列入到目录中。您可以在下面选择列入。可以在个人简介中加上话题标签,话题标签也会显示在用户资料目录里! + how_to_enable: 您目前没有被收录到用户目录中。您可以在下面选择收录。在个人简介中加上话题标签后,话题标签也会显示在用户目录上! people: other: "%{count} 人" errors: @@ -748,6 +741,7 @@ zh-CN: number: human: decimal_units: + format: "%n%u" units: billion: B million: M @@ -759,6 +753,7 @@ zh-CN: next: 下一页 older: 更早 prev: 上一页 + truncate: "…" polls: errors: already_voted: 你已经在这里投过票了 @@ -803,10 +798,6 @@ zh-CN: reply: proceed: 确认回复 prompt: 您想要回复此嘟文: - remote_unfollow: - error: 错误 - title: 标题 - unfollowed: 已取消关注 scheduled_statuses: over_daily_limit: 您已超出每日定时嘟文的上限(%{limit} 条) over_total_limit: 您已超出定时嘟文的上限(%{limit} 条) @@ -870,7 +861,7 @@ zh-CN: notifications: 通知 preferences: 首选项 profile: 个人资料 - relationships: 正在关注以及关注者 + relationships: 关注管理 two_factor_authentication: 双重认证 statuses: attached: @@ -910,6 +901,87 @@ zh-CN: reblogged: 转嘟 sensitive_content: 敏感内容 terms: + body_html: | +

    隐私政策

    +

    我们收集什么信息?

    + +
      +
    • 基本帐户信息:如果您在此服务器上注册,可能会要求您输入用户名,电子邮件地址和密码。 您还可以输入其他个人资料信息,例如显示名称和传记,并上传个人资料照片和标题图像。 用户名,显示名称,传记,个人资料图片和标题图片始终公开列出。
    • +
    • 帖子,关注和其他公共信息: 您关注的人员列表会公开列出,您的粉丝也是如此。 提交邮件时,会存储日期和时间以及您提交邮件的应用程序。 消息可能包含媒体附件,例如图片和视频。 公开和非上市帖子可公开获取。 当您在个人资料中添加帖子时,这也是公开信息。 您的帖子会发送给您的关注者,在某些情况下,这意味着他们会将其发送到不同的服务器,并将副本存储在那里。 当您删除帖子时,同样会将其发送给您的关注者。 重新记录或赞成其他职位的行为始终是公开的。
    • +
    • 直接和关注者的帖子: 所有帖子都在服务器上存储和处理。 仅限关注者的帖子会发送给您的关注者和用户,并且直接帖子仅会发送给他们中提到的用户。 在某些情况下,这意味着它们被传送到不同的服务器并且副本存储在那里。 我们善意努力限制只有授权人员访问这些帖子,但其他服务器可能无法这样做。 因此,查看您的关注者所属的服务器非常重要。 您可以在设置中切换选项以手动批准和拒绝新关注者。 请记住,服务器和任何接收服务器的操作员可能会查看此类消息, 并且收件人可以截图,复制或以其他方式重新共享它们。 不要在 Mastodon 上分享任何危险信息。
    • +
    • IP和其他元数据: 登录时,我们会记录您登录的IP地址以及浏览器应用程序的名称。 所有登录的会话都可供您在设置中查看和撤销。 使用的最新IP地址最长可存储12个月。 我们还可以保留服务器日志,其中包括我们服务器的每个请求的IP地址。
    • +
    + +
    + +

    我们将您的信息用于什么?

    + +

    我们向您收集的任何信息均可通过以下方式使用:

    + +
      +
    • 提供Mastodon的核心功能。 您只能在登录时与其他人的内容进行互动并发布您自己的内容。例如,您可以关注其他人在您自己的个性化家庭时间轴中查看他们的组合帖子。
    • +
    • 为了帮助社区适度,例如将您的IP地址与其他已知的IP地址进行比较,以确定禁止逃税或其他违规行为。
    • +
    • 您提供的电子邮件地址可能用于向您发送信息,有关其他人与您的内容交互或向您发送消息的通知,以及回复查询和/或其他请求或问题。
    • +
    + +
    + +

    我们如何保护您的信息?

    + +

    当您输入,提交或访问您的个人信息时,我们会实施各种安全措施以维护您的个人信息的安全。 除此之外,您的浏览器会话以及应用程序和API之间的流量都使用SSL进行保护,您的密码使用强大的单向算法进行哈希处理。 您可以启用双因素身份验证,以进一步保护对您帐户的访问。

    + +
    + +

    我们的数据保留政策是什么?

    + +

    我们真诚的努力:

    + +
      +
    • 保留包含此服务器的所有请求的IP地址的服务器日志,只要保留此类日志,不超过90天。
    • +
    • 保留与注册用户关联的IP地址不超过12个月。
    • +
    + +

    您可以请求并下载我们内容的存档,包括您的帖子,媒体附件,个人资料图片和标题图片。

    + +

    您可以随时不可逆转地删除您的帐户。

    + +
    + +

    我们使用 cookies 吗?

    + +

    是。 Cookie是网站或其服务提供商通过Web浏览器传输到计算机硬盘的小文件(如果允许)。 这些cookie使网站能够识别您的浏览器,如果您有注册帐户,则将其与您的注册帐户相关联。

    + +

    我们使用Cookie来了解并保存您对未来访问的偏好。

    + +
    + +

    我们是否透露任何信息给其他方?

    + +

    我们不会将您的个人身份信息出售,交易或以其他方式转让给外方。 这不包括协助我们操作我们的网站,开展业务或为您服务的受信任的第三方,只要这些方同意保密这些信息。 当我们认为发布适合遵守法律,执行我们的网站政策或保护我们或他人的权利,财产或安全时,我们也可能会发布您的信息。

    + +

    您的公共内容可能会被网络中的其他服务器下载。 您的公开帖子和关注者帖子会发送到关注者所在的服务器,并且直接邮件会传递到收件人的服务器,只要这些关注者或收件人位于与此不同的服务器上。

    + +

    当您授权应用程序使用您的帐户时,根据您批准的权限范围,它可能会访问您的公开个人资料信息,以下列表,您的关注者,您的列表,所有帖子和您的收藏夹。 应用程序永远不能访问您的电子邮件地址或密码。

    + +
    + +

    儿童使用网站

    + +

    如果此服务器位于欧盟或欧洲经济区:我们的网站,产品和服务都是针对至少16岁的人。 如果您未满16岁,则符合GDPR的要求(General Data Protection Regulation) 不要使用这个网站。

    + +

    如果此服务器位于美国:我们的网站,产品和服务均面向至少13岁的人。 如果您未满13岁,则符合COPPA的要求 (Children's Online Privacy Protection Act) 不要使用这个网站。

    + +

    如果此服务器位于另一个辖区,则法律要求可能不同。

    + +
    + +

    我们隐私政策的变更

    + +

    如果我们决定更改我们的隐私政策,我们会在此页面上发布这些更改。

    + +

    本文件为CC-BY-SA。 它最后更新于2018年3月7日。

    + +

    最初改编自 Discourse 隐私政策.

    title: "%{instance} 使用条款和隐私权政策" themes: contrast: Mastodon(高对比度) @@ -945,7 +1017,7 @@ zh-CN: disable: 虽然您的帐户被冻结,您的帐户数据仍然完整;但是您无法在解锁前执行任何操作。 silence: 当您的帐户受限时,只有已经关注过你的人才会这台服务器上看到你的嘟文,并且您会被排除在各种公共列表之外。但是,其他人仍然可以手动关注你。 suspend: 您的帐户已被封禁,所有的嘟文和您上传的媒体文件都已经从该服务器和您的关注者的服务器上删除并且不可恢复。 - review_server_policies: 审阅服务器条款 + review_server_policies: 查看服务器政策 subject: disable: 您的帐户 %{acct} 已被冻结 none: 对 %{acct} 的警告 diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml index 25e7475a88..520771da4a 100644 --- a/config/locales/zh-HK.yml +++ b/config/locales/zh-HK.yml @@ -327,13 +327,6 @@ zh-HK: no_media: 不含媒體檔案 title: 帳戶文章 with_media: 含有媒體檔案 - subscriptions: - callback_url: 回傳 URL - confirmed: 確定 - expires_in: 期限 - last_delivery: 資料最後送抵時間 - title: PuSH 訂閱 - topic: 所訂閱資源 title: 管理 admin_mailer: new_report: @@ -524,10 +517,6 @@ zh-HK: missing_resource: 無法找到你用戶的轉接網址 proceed: 下一步 prompt: 你希望關注︰ - remote_unfollow: - error: 錯誤 - title: 標題 - unfollowed: 取消關注 sessions: activity: 最近活動 browser: 瀏覽器 diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index d3dcf5133f..801ea7cead 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -401,13 +401,6 @@ zh-TW: no_media: 不含媒體檔案 title: 帳戶嘟文 with_media: 含有媒體檔案 - subscriptions: - callback_url: 回傳網址 - confirmed: 已確認 - expires_in: 期限 - last_delivery: 最後遞送 - title: WebSub 訂閱 - topic: 主題 title: 管理介面 admin_mailer: new_report: @@ -587,10 +580,6 @@ zh-TW: missing_resource: 無法找到資源 proceed: 下一步 prompt: '您希望關注:' - remote_unfollow: - error: 錯誤 - title: 標題 - unfollowed: 取消關注 sessions: activity: 最近活動 browser: 瀏覽器 From b3f44aa186487b4f93f3a457607870c90caaf5df Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 15 Jul 2019 07:50:14 +0200 Subject: [PATCH 37/71] Add periodic removal of older thumbnails for preview cards (#11304) --- .../maintenance/uncache_preview_worker.rb | 18 +++++++++++++++ .../preview_cards_cleanup_scheduler.rb | 22 +++++++++++++++++++ config/sidekiq.yml | 3 +++ 3 files changed, 43 insertions(+) create mode 100644 app/workers/maintenance/uncache_preview_worker.rb create mode 100644 app/workers/scheduler/preview_cards_cleanup_scheduler.rb diff --git a/app/workers/maintenance/uncache_preview_worker.rb b/app/workers/maintenance/uncache_preview_worker.rb new file mode 100644 index 0000000000..810ffd8ccf --- /dev/null +++ b/app/workers/maintenance/uncache_preview_worker.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Maintenance::UncachePreviewWorker + include Sidekiq::Worker + + sidekiq_options queue: 'pull' + + def perform(preview_card_id) + preview_card = PreviewCard.find(preview_card_id) + + return if preview_card.image.blank? + + preview_card.image.destroy + preview_card.save + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/app/workers/scheduler/preview_cards_cleanup_scheduler.rb b/app/workers/scheduler/preview_cards_cleanup_scheduler.rb new file mode 100644 index 0000000000..2b38792f03 --- /dev/null +++ b/app/workers/scheduler/preview_cards_cleanup_scheduler.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class Scheduler::PreviewCardsCleanupScheduler + include Sidekiq::Worker + + sidekiq_options unique: :until_executed, retry: 0 + + def perform + Maintenance::UncachePreviewWorker.push_bulk(recent_link_preview_cards.pluck(:id)) + Maintenance::UncachePreviewWorker.push_bulk(older_preview_cards.pluck(:id)) + end + + private + + def recent_link_preview_cards + PreviewCard.where(type: :link).where('updated_at < ?', 1.month.ago) + end + + def older_preview_cards + PreviewCard.where('updated_at < ?', 6.months.ago) + end +end diff --git a/config/sidekiq.yml b/config/sidekiq.yml index a16dea9679..5c652792c6 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -27,6 +27,9 @@ ip_cleanup_scheduler: cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *' class: Scheduler::IpCleanupScheduler + preview_cards_cleanup_scheduler: + cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *' + class: Scheduler::PreviewCardsCleanupScheduler email_scheduler: cron: '0 10 * * 2' class: Scheduler::EmailScheduler From 5b4a9345caea11be1a6cb8ecfb5cfb6173392264 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Mon, 15 Jul 2019 23:10:24 +0900 Subject: [PATCH 38/71] Fix consistent interpolations (#11317) --- config/locales/sk.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 5577f93941..5b0bbdef45 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -301,7 +301,7 @@ sk: affected_accounts: few: "%{count} účtov v databázi ovplyvnených" many: "%{count} účtov v databázi ovplyvnených" - one: "%{count} účet v databázi ovplyvnený" + one: 1 účet v databázi ovplyvnený other: "%{count} účty v databázi ovplyvnené" retroactive: silence: Zruš stíšenie všetkých momentálne utíšených účtov z tejto domény From 851ad993e0969580bba633916449a3df8ef27331 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2019 00:52:23 +0900 Subject: [PATCH 39/71] Bump eslint-plugin-import from 2.17.3 to 2.18.0 (#11310) Bumps [eslint-plugin-import](https://github.com/benmosher/eslint-plugin-import) from 2.17.3 to 2.18.0. - [Release notes](https://github.com/benmosher/eslint-plugin-import/releases) - [Changelog](https://github.com/benmosher/eslint-plugin-import/blob/master/CHANGELOG.md) - [Commits](https://github.com/benmosher/eslint-plugin-import/compare/v2.17.3...v2.18.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 9710be7d00..21aa9a7aec 100644 --- a/package.json +++ b/package.json @@ -171,7 +171,7 @@ "enzyme": "^3.10.0", "enzyme-adapter-react-16": "^1.14.0", "eslint": "^5.16.0", - "eslint-plugin-import": "~2.17.3", + "eslint-plugin-import": "~2.18.0", "eslint-plugin-jsx-a11y": "~6.2.1", "eslint-plugin-promise": "~4.2.1", "eslint-plugin-react": "~7.14.2", diff --git a/yarn.lock b/yarn.lock index 623bcd7057..1f71c75c08 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3650,10 +3650,10 @@ eslint-module-utils@^2.4.0: debug "^2.6.8" pkg-dir "^2.0.0" -eslint-plugin-import@~2.17.3: - version "2.17.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.17.3.tgz#00548b4434c18faebaba04b24ae6198f280de189" - integrity sha512-qeVf/UwXFJbeyLbxuY8RgqDyEKCkqV7YC+E5S5uOjAp4tOc8zj01JP3ucoBM8JcEqd1qRasJSg6LLlisirfy0Q== +eslint-plugin-import@~2.18.0: + version "2.18.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.0.tgz#7a5ba8d32622fb35eb9c8db195c2090bd18a3678" + integrity sha512-PZpAEC4gj/6DEMMoU2Df01C5c50r7zdGIN52Yfi7CvvWaYssG7Jt5R9nFG5gmqodxNOz9vQS87xk6Izdtpdrig== dependencies: array-includes "^3.0.3" contains-path "^0.1.0" From 23b29f7df16255ffa4067230ae698e10f18f8c68 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2019 01:04:46 +0900 Subject: [PATCH 40/71] Bump @babel/runtime from 7.4.5 to 7.5.4 (#11315) Bumps [@babel/runtime](https://github.com/babel/babel) from 7.4.5 to 7.5.4. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/master/CHANGELOG.md) - [Commits](https://github.com/babel/babel/compare/v7.4.5...v7.5.4) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 21aa9a7aec..358f84d7bf 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "@babel/plugin-transform-runtime": "^7.4.4", "@babel/preset-env": "^7.4.5", "@babel/preset-react": "^7.0.0", - "@babel/runtime": "^7.4.5", + "@babel/runtime": "^7.5.4", "@clusterws/cws": "^0.14.0", "array-includes": "^3.0.3", "autoprefixer": "^9.6.0", diff --git a/yarn.lock b/yarn.lock index 1f71c75c08..c945aaad02 100644 --- a/yarn.lock +++ b/yarn.lock @@ -768,10 +768,10 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5": - version "7.4.5" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12" - integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ== +"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.4": + version "7.5.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.4.tgz#cb7d1ad7c6d65676e66b47186577930465b5271b" + integrity sha512-Na84uwyImZZc3FKf4aUF1tysApzwf3p2yuFBIyBfbzT5glzKTdvYI4KVW4kcgjrzoGUjC7w3YyCHcJKaRxsr2Q== dependencies: regenerator-runtime "^0.13.2" From 7840d56c9df1175c165040aa4554d43d8c03fabc Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2019 01:06:30 +0900 Subject: [PATCH 41/71] Bump lodash from 4.17.13 to 4.17.14 (#11312) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.13 to 4.17.14. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.13...4.17.14) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 358f84d7bf..8e516f1a4a 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "intl-relativeformat": "^6.4.2", "is-nan": "^1.2.1", "js-yaml": "^3.13.1", - "lodash": "^4.17.13", + "lodash": "^4.17.14", "mark-loader": "^0.1.6", "marky": "^1.2.1", "mini-css-extract-plugin": "^0.7.0", diff --git a/yarn.lock b/yarn.lock index c945aaad02..6bad7a37a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6256,10 +6256,10 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.0.0, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.5, lodash@^4.3.0, lodash@~4.17.10: - version "4.17.13" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.13.tgz#0bdc3a6adc873d2f4e0c4bac285df91b64fc7b93" - integrity sha512-vm3/XWXfWtRua0FkUyEHBZy8kCPjErNBT9fJx8Zvs+U6zjqPbTUOpkaoum3O5uiA8sm+yNMHXfYkTUHFoMxFNA== +lodash@^4.0.0, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.5, lodash@^4.3.0, lodash@~4.17.10: + version "4.17.14" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" + integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw== loglevel@^1.6.3: version "1.6.3" From ae16820da809a618dc8a2d5ff84532a23a7ffbec Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2019 01:13:58 +0900 Subject: [PATCH 42/71] Bump webpack from 4.34.0 to 4.35.3 (#11309) Bumps [webpack](https://github.com/webpack/webpack) from 4.34.0 to 4.35.3. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v4.34.0...v4.35.3) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 24 +++++++++--------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 8e516f1a4a..c42deb323c 100644 --- a/package.json +++ b/package.json @@ -158,7 +158,7 @@ "throng": "^4.0.0", "tiny-queue": "^0.2.1", "uuid": "^3.1.0", - "webpack": "^4.34.0", + "webpack": "^4.35.3", "webpack-assets-manifest": "^3.1.1", "webpack-bundle-analyzer": "^3.3.2", "webpack-cli": "^3.3.5", diff --git a/yarn.lock b/yarn.lock index 6bad7a37a0..c7705f8a37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1344,11 +1344,6 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" -acorn-dynamic-import@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" - integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw== - acorn-globals@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.0.tgz#e3b6f8da3c1552a95ae627571f7dd6923bb54103" @@ -1384,10 +1379,10 @@ acorn@^5.5.0, acorn@^5.5.3: resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== -acorn@^6.0.1, acorn@^6.0.5, acorn@^6.0.7: - version "6.1.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" - integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA== +acorn@^6.0.1, acorn@^6.0.7, acorn@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.2.0.tgz#67f0da2fc339d6cfb5d6fb244fd449f33cd8bbe3" + integrity sha512-8oe72N3WPMjA+2zVG71Ia0nXZ8DpQH+QyyHO+p06jT8eg8FGG3FbcUIi8KziHlAfheJQZeoqbvq1mQSQHXKYLw== airbnb-prop-types@^2.13.2: version "2.13.2" @@ -10304,17 +10299,16 @@ webpack-sources@^1.0.0, webpack-sources@^1.0.1, webpack-sources@^1.1.0, webpack- source-list-map "^2.0.0" source-map "~0.6.1" -webpack@^4.34.0: - version "4.34.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.34.0.tgz#a4c30129482f7b4ece4c0842002dedf2b56fab58" - integrity sha512-ry2IQy1wJjOefLe1uJLzn5tG/DdIKzQqNlIAd2L84kcaADqNvQDTBlo8UcCNyDaT5FiaB+16jhAkb63YeG3H8Q== +webpack@^4.35.3: + version "4.35.3" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.35.3.tgz#66bc35ef215a7b75e8790f84d560013ffecf0ca3" + integrity sha512-xggQPwr9ILlXzz61lHzjvgoqGU08v5+Wnut19Uv3GaTtzN4xBTcwnobodrXE142EL1tOiS5WVEButooGzcQzTA== dependencies: "@webassemblyjs/ast" "1.8.5" "@webassemblyjs/helper-module-context" "1.8.5" "@webassemblyjs/wasm-edit" "1.8.5" "@webassemblyjs/wasm-parser" "1.8.5" - acorn "^6.0.5" - acorn-dynamic-import "^4.0.0" + acorn "^6.2.0" ajv "^6.1.0" ajv-keywords "^3.1.0" chrome-trace-event "^1.0.0" From 4562c3cb7ec0b3b7c232a8d7347623f8e214acd7 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2019 01:24:26 +0900 Subject: [PATCH 43/71] Bump eslint-plugin-jsx-a11y from 6.2.1 to 6.2.3 (#11314) Bumps [eslint-plugin-jsx-a11y](https://github.com/evcohen/eslint-plugin-jsx-a11y) from 6.2.1 to 6.2.3. - [Release notes](https://github.com/evcohen/eslint-plugin-jsx-a11y/releases) - [Changelog](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/CHANGELOG.md) - [Commits](https://github.com/evcohen/eslint-plugin-jsx-a11y/compare/v6.2.1...v6.2.3) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index c42deb323c..00ea65a125 100644 --- a/package.json +++ b/package.json @@ -172,7 +172,7 @@ "enzyme-adapter-react-16": "^1.14.0", "eslint": "^5.16.0", "eslint-plugin-import": "~2.18.0", - "eslint-plugin-jsx-a11y": "~6.2.1", + "eslint-plugin-jsx-a11y": "~6.2.3", "eslint-plugin-promise": "~4.2.1", "eslint-plugin-react": "~7.14.2", "jest": "^24.8.0", diff --git a/yarn.lock b/yarn.lock index c7705f8a37..14e4b276b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3662,11 +3662,12 @@ eslint-plugin-import@~2.18.0: read-pkg-up "^2.0.0" resolve "^1.11.0" -eslint-plugin-jsx-a11y@~6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.1.tgz#4ebba9f339b600ff415ae4166e3e2e008831cf0c" - integrity sha512-cjN2ObWrRz0TTw7vEcGQrx+YltMvZoOEx4hWU8eEERDnBIU00OTq7Vr+jA7DFKxiwLNv4tTh5Pq2GUNEa8b6+w== +eslint-plugin-jsx-a11y@~6.2.3: + version "6.2.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz#b872a09d5de51af70a97db1eea7dc933043708aa" + integrity sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg== dependencies: + "@babel/runtime" "^7.4.5" aria-query "^3.0.0" array-includes "^3.0.3" ast-types-flow "^0.0.7" @@ -3674,7 +3675,7 @@ eslint-plugin-jsx-a11y@~6.2.1: damerau-levenshtein "^1.0.4" emoji-regex "^7.0.2" has "^1.0.3" - jsx-ast-utils "^2.0.1" + jsx-ast-utils "^2.2.1" eslint-plugin-promise@~4.2.1: version "4.2.1" @@ -6032,7 +6033,7 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jsx-ast-utils@^2.0.1, jsx-ast-utils@^2.1.0: +jsx-ast-utils@^2.1.0, jsx-ast-utils@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.1.tgz#4d4973ebf8b9d2837ee91a8208cc66f3a2776cfb" integrity sha512-v3FxCcAf20DayI+uxnCuw795+oOIkVu6EnJ1+kSzhqqTZHNkTZ7B66ZgLp4oLJ/gbA64cI0B7WRoHZMSRdyVRQ== From 9b1d3e4acb8190509d98ee4ef2ab9b24b45362ee Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 16 Jul 2019 06:30:47 +0200 Subject: [PATCH 44/71] Add option to disable real-time updates in web UI (#9984) Fix #9031 Fix #7913 --- .../settings/preferences_controller.rb | 1 + .../mastodon/actions/notifications.js | 30 ++++++++++--- app/javascript/mastodon/actions/timelines.js | 43 +++++++++++++------ app/javascript/mastodon/compare_id.js | 5 ++- .../mastodon/components/load_pending.js | 22 ++++++++++ .../mastodon/components/scrollable_list.js | 13 +++++- .../components/column_settings.js | 2 +- .../components/setting_toggle.js | 5 ++- .../mastodon/features/notifications/index.js | 12 +++++- .../ui/containers/status_list_container.js | 5 ++- app/javascript/mastodon/initial_state.js | 1 + .../mastodon/reducers/notifications.js | 30 ++++++++----- app/javascript/mastodon/reducers/settings.js | 6 --- app/javascript/mastodon/reducers/timelines.js | 40 +++++++++++------ app/lib/user_settings_decorator.rb | 5 +++ app/models/user.rb | 2 +- app/serializers/initial_state_serializer.rb | 23 +++++----- .../preferences/appearance/show.html.haml | 3 ++ config/locales/simple_form.en.yml | 2 + config/settings.yml | 1 + 20 files changed, 181 insertions(+), 70 deletions(-) create mode 100644 app/javascript/mastodon/components/load_pending.js diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 0a5c14cca7..742c97cdb6 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -55,6 +55,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_show_application, :setting_advanced_layout, :setting_use_blurhash, + :setting_use_pending_items, notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account), interactions: %i(must_be_follower must_be_following must_be_following_dm) ) diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 56c952cb05..d92d972bc7 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -12,6 +12,8 @@ import { defineMessages } from 'react-intl'; import { List as ImmutableList } from 'immutable'; import { unescapeHTML } from '../utils/html'; import { getFiltersRegex } from '../selectors'; +import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; +import compareId from 'mastodon/compare_id'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; @@ -22,8 +24,9 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET'; -export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; -export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; +export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; +export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; +export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING'; defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, @@ -38,6 +41,10 @@ const fetchRelatedRelationships = (dispatch, notifications) => { } }; +export const loadPending = () => ({ + type: NOTIFICATIONS_LOAD_PENDING, +}); + export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true); @@ -69,6 +76,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { dispatch({ type: NOTIFICATIONS_UPDATE, notification, + usePendingItems: preferPendingItems, meta: (playSound && !filtered) ? { sound: 'boop' } : undefined, }); @@ -122,10 +130,19 @@ export function expandNotifications({ maxId } = {}, done = noOp) { : excludeTypesFromFilter(activeFilter), }; - if (!maxId && notifications.get('items').size > 0) { - params.since_id = notifications.getIn(['items', 0, 'id']); + if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) { + const a = notifications.getIn(['pendingItems', 0, 'id']); + const b = notifications.getIn(['items', 0, 'id']); + + if (a && b && compareId(a, b) > 0) { + params.since_id = a; + } else { + params.since_id = b || a; + } } + const isLoadingRecent = !!params.since_id; + dispatch(expandNotificationsRequest(isLoadingMore)); api(getState).get('/api/v1/notifications', { params }).then(response => { @@ -134,7 +151,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) { dispatch(importFetchedAccounts(response.data.map(item => item.account))); dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); - dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore)); + dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent && preferPendingItems)); fetchRelatedRelationships(dispatch, response.data); done(); }).catch(error => { @@ -151,11 +168,12 @@ export function expandNotificationsRequest(isLoadingMore) { }; }; -export function expandNotificationsSuccess(notifications, next, isLoadingMore) { +export function expandNotificationsSuccess(notifications, next, isLoadingMore, usePendingItems) { return { type: NOTIFICATIONS_EXPAND_SUCCESS, notifications, next, + usePendingItems, skipLoading: !isLoadingMore, }; }; diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 06c21b96b7..7eeba2aa7f 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -1,6 +1,8 @@ import { importFetchedStatus, importFetchedStatuses } from './importer'; -import api, { getLinks } from '../api'; +import api, { getLinks } from 'mastodon/api'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import compareId from 'mastodon/compare_id'; +import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE'; @@ -10,10 +12,15 @@ export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS'; export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; -export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; +export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; +export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING'; +export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; +export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; -export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; -export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; +export const loadPending = timeline => ({ + type: TIMELINE_LOAD_PENDING, + timeline, +}); export function updateTimeline(timeline, status, accept) { return dispatch => { @@ -27,6 +34,7 @@ export function updateTimeline(timeline, status, accept) { type: TIMELINE_UPDATE, timeline, status, + usePendingItems: preferPendingItems, }); }; }; @@ -71,8 +79,15 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { return; } - if (!params.max_id && !params.pinned && timeline.get('items', ImmutableList()).size > 0) { - params.since_id = timeline.getIn(['items', 0]); + if (!params.max_id && !params.pinned && (timeline.get('items', ImmutableList()).size + timeline.get('pendingItems', ImmutableList()).size) > 0) { + const a = timeline.getIn(['pendingItems', 0]); + const b = timeline.getIn(['items', 0]); + + if (a && b && compareId(a, b) > 0) { + params.since_id = a; + } else { + params.since_id = b || a; + } } const isLoadingRecent = !!params.since_id; @@ -82,7 +97,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); - dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore)); + dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); done(); }).catch(error => { dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); @@ -115,7 +130,7 @@ export function expandTimelineRequest(timeline, isLoadingMore) { }; }; -export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore) { +export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore, usePendingItems) { return { type: TIMELINE_EXPAND_SUCCESS, timeline, @@ -123,6 +138,7 @@ export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadi next, partial, isLoadingRecent, + usePendingItems, skipLoading: !isLoadingMore, }; }; @@ -151,9 +167,8 @@ export function connectTimeline(timeline) { }; }; -export function disconnectTimeline(timeline) { - return { - type: TIMELINE_DISCONNECT, - timeline, - }; -}; +export const disconnectTimeline = timeline => ({ + type: TIMELINE_DISCONNECT, + timeline, + usePendingItems: preferPendingItems, +}); diff --git a/app/javascript/mastodon/compare_id.js b/app/javascript/mastodon/compare_id.js index aaff66481a..66cf51c4b6 100644 --- a/app/javascript/mastodon/compare_id.js +++ b/app/javascript/mastodon/compare_id.js @@ -1,10 +1,11 @@ -export default function compareId(id1, id2) { +export default function compareId (id1, id2) { if (id1 === id2) { return 0; } + if (id1.length === id2.length) { return id1 > id2 ? 1 : -1; } else { return id1.length > id2.length ? 1 : -1; } -} +}; diff --git a/app/javascript/mastodon/components/load_pending.js b/app/javascript/mastodon/components/load_pending.js new file mode 100644 index 0000000000..7e27024036 --- /dev/null +++ b/app/javascript/mastodon/components/load_pending.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import PropTypes from 'prop-types'; + +export default class LoadPending extends React.PureComponent { + + static propTypes = { + onClick: PropTypes.func, + count: PropTypes.number, + } + + render() { + const { count } = this.props; + + return ( + + ); + } + +} diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js index 0376cf85ac..553c163524 100644 --- a/app/javascript/mastodon/components/scrollable_list.js +++ b/app/javascript/mastodon/components/scrollable_list.js @@ -3,6 +3,7 @@ import { ScrollContainer } from 'react-router-scroll-4'; import PropTypes from 'prop-types'; import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container'; import LoadMore from './load_more'; +import LoadPending from './load_pending'; import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'; import { throttle } from 'lodash'; import { List as ImmutableList } from 'immutable'; @@ -21,6 +22,7 @@ export default class ScrollableList extends PureComponent { static propTypes = { scrollKey: PropTypes.string.isRequired, onLoadMore: PropTypes.func, + onLoadPending: PropTypes.func, onScrollToTop: PropTypes.func, onScroll: PropTypes.func, trackScroll: PropTypes.bool, @@ -28,6 +30,7 @@ export default class ScrollableList extends PureComponent { isLoading: PropTypes.bool, showLoading: PropTypes.bool, hasMore: PropTypes.bool, + numPending: PropTypes.number, prepend: PropTypes.node, alwaysPrepend: PropTypes.bool, emptyMessage: PropTypes.node, @@ -225,12 +228,18 @@ export default class ScrollableList extends PureComponent { this.props.onLoadMore(); } + handleLoadPending = e => { + e.preventDefault(); + this.props.onLoadPending(); + } + render () { - const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props; + const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props; const { fullscreen } = this.state; const childrenCount = React.Children.count(children); const loadMore = (hasMore && onLoadMore) ? : null; + const loadPending = (numPending > 0) ? : null; let scrollableArea = null; if (showLoading) { @@ -251,6 +260,8 @@ export default class ScrollableList extends PureComponent {
    {prepend} + {loadPending} + {React.Children.map(this.props.children, (child, index) => (
    - } /> + } />
    ); diff --git a/app/javascript/mastodon/features/notifications/components/setting_toggle.js b/app/javascript/mastodon/features/notifications/components/setting_toggle.js index 7aec16d2ea..e6f593ef89 100644 --- a/app/javascript/mastodon/features/notifications/components/setting_toggle.js +++ b/app/javascript/mastodon/features/notifications/components/setting_toggle.js @@ -11,6 +11,7 @@ export default class SettingToggle extends React.PureComponent { settingPath: PropTypes.array.isRequired, label: PropTypes.node.isRequired, onChange: PropTypes.func.isRequired, + defaultValue: PropTypes.bool, } onChange = ({ target }) => { @@ -18,12 +19,12 @@ export default class SettingToggle extends React.PureComponent { } render () { - const { prefix, settings, settingPath, label } = this.props; + const { prefix, settings, settingPath, label, defaultValue } = this.props; const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-'); return (
    - +
    ); diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index 006c456575..df4ad6f2aa 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Column from '../../components/column'; import ColumnHeader from '../../components/column_header'; -import { expandNotifications, scrollTopNotifications } from '../../actions/notifications'; +import { expandNotifications, scrollTopNotifications, loadPending } from '../../actions/notifications'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import NotificationContainer from './containers/notification_container'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; @@ -41,6 +41,7 @@ const mapStateToProps = state => ({ isLoading: state.getIn(['notifications', 'isLoading'], true), isUnread: state.getIn(['notifications', 'unread']) > 0, hasMore: state.getIn(['notifications', 'hasMore']), + numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, }); export default @connect(mapStateToProps) @@ -58,6 +59,7 @@ class Notifications extends React.PureComponent { isUnread: PropTypes.bool, multiColumn: PropTypes.bool, hasMore: PropTypes.bool, + numPending: PropTypes.number, }; static defaultProps = { @@ -80,6 +82,10 @@ class Notifications extends React.PureComponent { this.props.dispatch(expandNotifications({ maxId: last && last.get('id') })); }, 300, { leading: true }); + handleLoadPending = () => { + this.props.dispatch(loadPending()); + }; + handleScrollToTop = debounce(() => { this.props.dispatch(scrollTopNotifications(true)); }, 100); @@ -136,7 +142,7 @@ class Notifications extends React.PureComponent { } render () { - const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, showFilterBar } = this.props; + const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar } = this.props; const pinned = !!columnId; const emptyMessage = ; @@ -178,8 +184,10 @@ class Notifications extends React.PureComponent { isLoading={isLoading} showLoading={isLoading && notifications.size === 0} hasMore={hasMore} + numPending={numPending} emptyMessage={emptyMessage} onLoadMore={this.handleLoadOlder} + onLoadPending={this.handleLoadPending} onScrollToTop={this.handleScrollToTop} onScroll={this.handleScroll} shouldUpdateScroll={shouldUpdateScroll} diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js index 3df5b7beac..7b8eb652b4 100644 --- a/app/javascript/mastodon/features/ui/containers/status_list_container.js +++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import StatusList from '../../../components/status_list'; -import { scrollTopTimeline } from '../../../actions/timelines'; +import { scrollTopTimeline, loadPending } from '../../../actions/timelines'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { createSelector } from 'reselect'; import { debounce } from 'lodash'; @@ -37,6 +37,7 @@ const makeMapStateToProps = () => { isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true), isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false), hasMore: state.getIn(['timelines', timelineId, 'hasMore']), + numPending: state.getIn(['timelines', timelineId, 'pendingItems'], ImmutableList()).size, }); return mapStateToProps; @@ -52,6 +53,8 @@ const mapDispatchToProps = (dispatch, { timelineId }) => ({ dispatch(scrollTopTimeline(timelineId, false)); }, 100), + onLoadPending: () => dispatch(loadPending(timelineId)), + }); export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList); diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index f0f131ff58..cb2ccc7c41 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -21,5 +21,6 @@ export const profile_directory = getMeta('profile_directory'); export const isStaff = getMeta('is_staff'); export const forceSingleColumn = !getMeta('advanced_layout'); export const useBlurhash = getMeta('use_blurhash'); +export const usePendingItems = getMeta('use_pending_items'); export default initialState; diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 4d9604de9e..e94a4946b8 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -6,6 +6,7 @@ import { NOTIFICATIONS_FILTER_SET, NOTIFICATIONS_CLEAR, NOTIFICATIONS_SCROLL_TOP, + NOTIFICATIONS_LOAD_PENDING, } from '../actions/notifications'; import { ACCOUNT_BLOCK_SUCCESS, @@ -16,6 +17,7 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import compareId from '../compare_id'; const initialState = ImmutableMap({ + pendingItems: ImmutableList(), items: ImmutableList(), hasMore: true, top: false, @@ -31,7 +33,11 @@ const notificationToMap = notification => ImmutableMap({ status: notification.status ? notification.status.id : null, }); -const normalizeNotification = (state, notification) => { +const normalizeNotification = (state, notification, usePendingItems) => { + if (usePendingItems) { + return state.update('pendingItems', list => list.unshift(notificationToMap(notification))); + } + const top = state.get('top'); if (!top) { @@ -47,7 +53,7 @@ const normalizeNotification = (state, notification) => { }); }; -const expandNormalizedNotifications = (state, notifications, next) => { +const expandNormalizedNotifications = (state, notifications, next, usePendingItems) => { let items = ImmutableList(); notifications.forEach((n, i) => { @@ -56,7 +62,7 @@ const expandNormalizedNotifications = (state, notifications, next) => { return state.withMutations(mutable => { if (!items.isEmpty()) { - mutable.update('items', list => { + mutable.update(usePendingItems ? 'pendingItems' : 'items', list => { const lastIndex = 1 + list.findLastIndex( item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')) ); @@ -78,7 +84,8 @@ const expandNormalizedNotifications = (state, notifications, next) => { }; const filterNotifications = (state, relationship) => { - return state.update('items', list => list.filterNot(item => item !== null && item.get('account') === relationship.id)); + const helper = list => list.filterNot(item => item !== null && item.get('account') === relationship.id); + return state.update('items', helper).update('pendingItems', helper); }; const updateTop = (state, top) => { @@ -90,34 +97,37 @@ const updateTop = (state, top) => { }; const deleteByStatus = (state, statusId) => { - return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId)); + const helper = list => list.filterNot(item => item !== null && item.get('status') === statusId); + return state.update('items', helper).update('pendingItems', helper); }; export default function notifications(state = initialState, action) { switch(action.type) { + case NOTIFICATIONS_LOAD_PENDING: + return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0); case NOTIFICATIONS_EXPAND_REQUEST: return state.set('isLoading', true); case NOTIFICATIONS_EXPAND_FAIL: return state.set('isLoading', false); case NOTIFICATIONS_FILTER_SET: - return state.set('items', ImmutableList()).set('hasMore', true); + return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', true); case NOTIFICATIONS_SCROLL_TOP: return updateTop(state, action.top); case NOTIFICATIONS_UPDATE: - return normalizeNotification(state, action.notification); + return normalizeNotification(state, action.notification, action.usePendingItems); case NOTIFICATIONS_EXPAND_SUCCESS: - return expandNormalizedNotifications(state, action.notifications, action.next); + return expandNormalizedNotifications(state, action.notifications, action.next, action.usePendingItems); case ACCOUNT_BLOCK_SUCCESS: return filterNotifications(state, action.relationship); case ACCOUNT_MUTE_SUCCESS: return action.relationship.muting_notifications ? filterNotifications(state, action.relationship) : state; case NOTIFICATIONS_CLEAR: - return state.set('items', ImmutableList()).set('hasMore', false); + return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false); case TIMELINE_DELETE: return deleteByStatus(state, action.id); case TIMELINE_DISCONNECT: return action.timeline === 'home' ? - state.update('items', items => items.first() ? items.unshift(null) : items) : + state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) : state; default: return state; diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index a0eea137f1..033bfc999a 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -10,8 +10,6 @@ import uuid from '../uuid'; const initialState = ImmutableMap({ saved: true, - onboarded: false, - skinTone: 1, home: ImmutableMap({ @@ -74,10 +72,6 @@ const initialState = ImmutableMap({ body: '', }), }), - - trends: ImmutableMap({ - show: true, - }), }); const defaultColumns = fromJS([ diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index 309a95a19b..0b036f5fe0 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -8,6 +8,7 @@ import { TIMELINE_SCROLL_TOP, TIMELINE_CONNECT, TIMELINE_DISCONNECT, + TIMELINE_LOAD_PENDING, } from '../actions/timelines'; import { ACCOUNT_BLOCK_SUCCESS, @@ -25,10 +26,11 @@ const initialTimeline = ImmutableMap({ top: true, isLoading: false, hasMore: true, + pendingItems: ImmutableList(), items: ImmutableList(), }); -const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent) => { +const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { mMap.set('isLoading', false); mMap.set('isPartial', isPartial); @@ -38,7 +40,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is if (timeline.endsWith(':pinned')) { mMap.set('items', statuses.map(status => status.get('id'))); } else if (!statuses.isEmpty()) { - mMap.update('items', ImmutableList(), oldIds => { + mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => { const newIds = statuses.map(status => status.get('id')); const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1; @@ -57,7 +59,15 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is })); }; -const updateTimeline = (state, timeline, status) => { +const updateTimeline = (state, timeline, status, usePendingItems) => { + if (usePendingItems) { + if (state.getIn([timeline, 'pendingItems'], ImmutableList()).includes(status.get('id')) || state.getIn([timeline, 'items'], ImmutableList()).includes(status.get('id'))) { + return state; + } + + return state.update(timeline, initialTimeline, map => map.update('pendingItems', list => list.unshift(status.get('id')))); + } + const top = state.getIn([timeline, 'top']); const ids = state.getIn([timeline, 'items'], ImmutableList()); const includesId = ids.includes(status.get('id')); @@ -78,8 +88,10 @@ const updateTimeline = (state, timeline, status) => { const deleteStatus = (state, id, accountId, references, exclude_account = null) => { state.keySeq().forEach(timeline => { - if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) - state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id)); + if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) { + const helper = list => list.filterNot(item => item === id); + state = state.updateIn([timeline, 'items'], helper).updateIn([timeline, 'pendingItems'], helper); + } }); // Remove reblogs of deleted status @@ -109,11 +121,10 @@ const filterTimelines = (state, relationship, statuses) => { return state; }; -const filterTimeline = (timeline, state, relationship, statuses) => - state.updateIn([timeline, 'items'], ImmutableList(), list => - list.filterNot(statusId => - statuses.getIn([statusId, 'account']) === relationship.id - )); +const filterTimeline = (timeline, state, relationship, statuses) => { + const helper = list => list.filterNot(statusId => statuses.getIn([statusId, 'account']) === relationship.id); + return state.updateIn([timeline, 'items'], ImmutableList(), helper).updateIn([timeline, 'pendingItems'], ImmutableList(), helper); +}; const updateTop = (state, timeline, top) => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { @@ -124,14 +135,17 @@ const updateTop = (state, timeline, top) => { export default function timelines(state = initialState, action) { switch(action.type) { + case TIMELINE_LOAD_PENDING: + return state.update(action.timeline, initialTimeline, map => + map.update('items', list => map.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0)); case TIMELINE_EXPAND_REQUEST: return state.update(action.timeline, initialTimeline, map => map.set('isLoading', true)); case TIMELINE_EXPAND_FAIL: return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false)); case TIMELINE_EXPAND_SUCCESS: - return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent); + return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent, action.usePendingItems); case TIMELINE_UPDATE: - return updateTimeline(state, action.timeline, fromJS(action.status)); + return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf); case TIMELINE_CLEAR: @@ -149,7 +163,7 @@ export default function timelines(state = initialState, action) { return state.update( action.timeline, initialTimeline, - map => map.set('online', false).update('items', items => items.first() ? items.unshift(null) : items) + map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) ); default: return state; diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index aaf95cc198..9ae9986c28 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -35,6 +35,7 @@ class UserSettingsDecorator user.settings['show_application'] = show_application_preference if change?('setting_show_application') user.settings['advanced_layout'] = advanced_layout_preference if change?('setting_advanced_layout') user.settings['use_blurhash'] = use_blurhash_preference if change?('setting_use_blurhash') + user.settings['use_pending_items'] = use_pending_items_preference if change?('setting_use_pending_items') end def merged_notification_emails @@ -117,6 +118,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_use_blurhash' end + def use_pending_items_preference + boolean_cast_setting 'setting_use_pending_items' + end + def boolean_cast_setting(key) ActiveModel::Type::Boolean.new.cast(settings[key]) end diff --git a/app/models/user.rb b/app/models/user.rb index 3a4b415dd6..31c99630c3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -106,7 +106,7 @@ class User < ApplicationRecord delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal, :reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network, :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, - :advanced_layout, :use_blurhash, to: :settings, prefix: :setting, allow_nil: false + :advanced_layout, :use_blurhash, :use_pending_items, to: :settings, prefix: :setting, allow_nil: false attr_reader :invite_code attr_writer :external diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 54f99d570e..7e5d3eda90 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -23,17 +23,18 @@ class InitialStateSerializer < ActiveModel::Serializer } if object.current_account - store[:me] = object.current_account.id.to_s - store[:unfollow_modal] = object.current_account.user.setting_unfollow_modal - store[:boost_modal] = object.current_account.user.setting_boost_modal - store[:delete_modal] = object.current_account.user.setting_delete_modal - store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif - store[:display_media] = object.current_account.user.setting_display_media - store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers - store[:reduce_motion] = object.current_account.user.setting_reduce_motion - store[:advanced_layout] = object.current_account.user.setting_advanced_layout - store[:use_blurhash] = object.current_account.user.setting_use_blurhash - store[:is_staff] = object.current_account.user.staff? + store[:me] = object.current_account.id.to_s + store[:unfollow_modal] = object.current_account.user.setting_unfollow_modal + store[:boost_modal] = object.current_account.user.setting_boost_modal + store[:delete_modal] = object.current_account.user.setting_delete_modal + store[:auto_play_gif] = object.current_account.user.setting_auto_play_gif + store[:display_media] = object.current_account.user.setting_display_media + store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers + store[:reduce_motion] = object.current_account.user.setting_reduce_motion + store[:advanced_layout] = object.current_account.user.setting_advanced_layout + store[:use_blurhash] = object.current_account.user.setting_use_blurhash + store[:use_pending_items] = object.current_account.user.setting_use_pending_items + store[:is_staff] = object.current_account.user.staff? end store diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index c138828017..e279a61c48 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -17,6 +17,9 @@ %h4= t 'appearance.animations_and_accessibility' + .fields-group + = f.input :setting_use_pending_items, as: :boolean, wrapper: :with_label + .fields-group = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label, recommended: true = f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 75cbec9de5..12a7ec2b34 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -35,6 +35,7 @@ en: setting_noindex: Affects your public profile and status pages setting_show_application: The application you use to toot will be displayed in the detailed view of your toots setting_use_blurhash: Gradients are based on the colors of the hidden visuals but obfuscate any details + setting_use_pending_items: Hide timeline updates behind a click instead of automatically scrolling the feed username: Your username will be unique on %{domain} whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word featured_tag: @@ -111,6 +112,7 @@ en: setting_theme: Site theme setting_unfollow_modal: Show confirmation dialog before unfollowing someone setting_use_blurhash: Show colorful gradients for hidden media + setting_use_pending_items: Slow mode severity: Severity type: Import type username: Username diff --git a/config/settings.yml b/config/settings.yml index ad71b6008f..75cb2dc853 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -33,6 +33,7 @@ defaults: &defaults aggregate_reblogs: true advanced_layout: false use_blurhash: true + use_pending_items: false notification_emails: follow: false reblog: false From b4c9a860e5c349a9abdfbdc9b76f338e70fc89cc Mon Sep 17 00:00:00 2001 From: Daigo 3 Dango Date: Mon, 15 Jul 2019 18:51:36 -1000 Subject: [PATCH 45/71] Make puma bind address configurable with BIND env var (#11326) --- config/puma.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/puma.rb b/config/puma.rb index 25a5534b29..6a96867d54 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -4,7 +4,7 @@ threads threads_count, threads_count if ENV['SOCKET'] bind "unix://#{ENV['SOCKET']}" else - bind "tcp://127.0.0.1:#{ENV.fetch('PORT', 3000)}" + bind "tcp://#{ENV.fetch('BIND', '127.0.0.1')}:#{ENV.fetch('PORT', 3000)}" end environment ENV.fetch('RAILS_ENV') { 'development' } From 91544a6cb54e54b7cbfb266553d29763d6b68176 Mon Sep 17 00:00:00 2001 From: ThibG Date: Tue, 16 Jul 2019 14:25:56 +0200 Subject: [PATCH 46/71] Remove unused Account#magic_key (#11327) --- app/models/account.rb | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/app/models/account.rb b/app/models/account.rb index a22b7fd7cc..adf4586fa5 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -301,21 +301,6 @@ class Account < ApplicationRecord self.fields = tmp end - def magic_key - modulus, exponent = [keypair.public_key.n, keypair.public_key.e].map do |component| - result = [] - - until component.zero? - result << [component % 256].pack('C') - component >>= 8 - end - - result.reverse.join - end - - (['RSA'] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.') - end - def subscription(webhook_url) @subscription ||= OStatus2::Subscription.new(remote_url, secret: secret, webhook: webhook_url, hub: hub_url) end From 15ddabf95a34d834295484d7e4ee21515e6fc9da Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 17 Jul 2019 00:00:39 +0200 Subject: [PATCH 47/71] Fix caching headers in ActivityPub endpoints (#11331) * Fix reverse-proxy caching in public fetch mode * Fix caching in ActivityPub-specific controllers --- app/controllers/activitypub/base_controller.rb | 9 +++++++++ app/controllers/activitypub/collections_controller.rb | 2 +- app/controllers/activitypub/outboxes_controller.rb | 2 +- app/controllers/activitypub/replies_controller.rb | 2 +- app/controllers/application_controller.rb | 2 +- 5 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 app/controllers/activitypub/base_controller.rb diff --git a/app/controllers/activitypub/base_controller.rb b/app/controllers/activitypub/base_controller.rb new file mode 100644 index 0000000000..a3b5c4dfa5 --- /dev/null +++ b/app/controllers/activitypub/base_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class ActivityPub::BaseController < Api::BaseController + private + + def set_cache_headers + response.headers['Vary'] = 'Signature' if authorized_fetch_mode? + end +end diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index 035467f417..fa925b204e 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::CollectionsController < Api::BaseController +class ActivityPub::CollectionsController < ActivityPub::BaseController include SignatureVerification include AccountOwnedConcern diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index cdfd28ba84..891756b7e6 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::OutboxesController < Api::BaseController +class ActivityPub::OutboxesController < ActivityPub::BaseController LIMIT = 20 include SignatureVerification diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index 020c077ab0..ab755ed4e6 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::RepliesController < Api::BaseController +class ActivityPub::RepliesController < ActivityPub::BaseController include SignatureAuthentication include Authorization include AccountOwnedConcern diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 16e7d70a37..26f3b1def5 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -160,6 +160,6 @@ class ApplicationController < ActionController::Base end def set_cache_headers - response.headers['Vary'] = 'Accept, Signature' + response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature' end end From 5599caef49962ec62c8a557273f4eebb0f94767e Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 17 Jul 2019 01:53:37 +0200 Subject: [PATCH 48/71] Extend AUTHORIZED_FETCH mode to user blocks as well (#11332) * Extend AUTHORIZED_FETCH mode to user blocks as well * Move decision to deny access to StatusPolicy --- app/policies/status_policy.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index 64a5111fc8..118b79f3ed 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -17,7 +17,7 @@ class StatusPolicy < ApplicationPolicy elsif private? owned? || following_author? || mention_exists? else - current_account.nil? || !author_blocking? + current_account.nil? || (!author_blocking? && !author_blocking_domain?) end end @@ -63,6 +63,12 @@ class StatusPolicy < ApplicationPolicy end end + def author_blocking_domain? + return false if current_account.nil? || current_account.domain.nil? + + author.blocking_domain?(current_account.domain) + end + def blocking_author? return false if current_account.nil? From 873828ad2d985220a0aea6b258513a6eaafbe027 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 17 Jul 2019 17:14:25 +0200 Subject: [PATCH 49/71] Fix custom CSS controller (#11336) --- app/controllers/custom_css_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/custom_css_controller.rb b/app/controllers/custom_css_controller.rb index 7f4dcfcfe6..e3f67bd144 100644 --- a/app/controllers/custom_css_controller.rb +++ b/app/controllers/custom_css_controller.rb @@ -6,7 +6,7 @@ class CustomCssController < ApplicationController before_action :set_cache_headers def show - expires 3.minutes, public: true + expires_in 3.minutes, public: true render plain: Setting.custom_css || '', content_type: 'text/css' end end From fccd25cf5397f8c2ee2df98e0cc46be3c2b1ea5d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 17 Jul 2019 19:29:37 +0200 Subject: [PATCH 50/71] Change terms and privacy policy pages to always be accessible (#11334) Fix #11328 --- app/controllers/about_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 761c7f5cdf..52fb1dc1b3 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -7,6 +7,8 @@ class AboutController < ApplicationController before_action :set_instance_presenter before_action :set_expires_in + skip_before_action :check_user_permissions, only: [:more, :terms] + def show; end def more; end From 7e2b6da57f7689757a50fa261c480445b1846703 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 17 Jul 2019 21:09:15 +0200 Subject: [PATCH 51/71] Add setting to disable the anti-spam (#11296) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add environment variable to disable the anti-spam * Move antispam setting to admin settings * Fix typo * antispam → spam_check --- app/controllers/admin/dashboard_controller.rb | 1 + app/lib/spam_check.rb | 6 +++++- app/models/form/admin_settings.rb | 2 ++ app/views/admin/dashboard/index.html.haml | 2 ++ app/views/admin/settings/edit.html.haml | 3 +++ config/locales/en.yml | 4 ++++ config/settings.yml | 1 + 7 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index f23ed15086..e74e4755f8 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -30,6 +30,7 @@ module Admin @trending_hashtags = TrendingTags.get(7) @profile_directory = Setting.profile_directory @timeline_preview = Setting.timeline_preview + @spam_check_enabled = Setting.spam_check_enabled end private diff --git a/app/lib/spam_check.rb b/app/lib/spam_check.rb index 923d48a022..0cf1b87903 100644 --- a/app/lib/spam_check.rb +++ b/app/lib/spam_check.rb @@ -14,7 +14,7 @@ class SpamCheck end def skip? - already_flagged? || trusted? || no_unsolicited_mentions? || solicited_reply? + disabled? || already_flagged? || trusted? || no_unsolicited_mentions? || solicited_reply? end def spam? @@ -80,6 +80,10 @@ class SpamCheck private + def disabled? + !Setting.spam_check_enabled + end + def remove_mentions(text) return text.gsub(Account::MENTION_RE, '') if @status.local? diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 86a86ec666..2c03c88a8c 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -28,6 +28,7 @@ class Form::AdminSettings thumbnail hero mascot + spam_check_enabled ).freeze BOOLEAN_KEYS = %i( @@ -39,6 +40,7 @@ class Form::AdminSettings show_known_fediverse_at_about_page preview_sensitive_media profile_directory + spam_check_enabled ).freeze UPLOAD_KEYS = %i( diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index d448e38629..77cc1a2a00 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -51,6 +51,8 @@ = feature_hint(link_to(t('admin.dashboard.feature_timeline_preview'), edit_admin_settings_path), @timeline_preview) %li = feature_hint(link_to(t('admin.dashboard.feature_relay'), admin_relays_path), @relay_enabled) + %li + = feature_hint(link_to(t('admin.dashboard.feature_spam_check'), edit_admin_settings_path), @spam_check_enabled) .dashboard__widgets__versions %div diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index a67e6a2c82..b3bf3849c0 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -66,6 +66,9 @@ .fields-group = f.input :profile_directory, as: :boolean, wrapper: :with_label, label: t('admin.settings.profile_directory.title'), hint: t('admin.settings.profile_directory.desc_html') + .fields-group + = f.input :spam_check_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.spam_check_enabled.title'), hint: t('admin.settings.spam_check_enabled.desc_html') + %hr.spacer/ .fields-group diff --git a/config/locales/en.yml b/config/locales/en.yml index 89251ad407..4e252945f1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -250,6 +250,7 @@ en: feature_profile_directory: Profile directory feature_registrations: Registrations feature_relay: Federation relay + feature_spam_check: Anti-spam feature_timeline_preview: Timeline preview features: Features hidden_service: Federation with hidden services @@ -449,6 +450,9 @@ en: desc_html: You can write your own privacy policy, terms of service or other legalese. You can use HTML tags title: Custom terms of service site_title: Server name + spam_check_enabled: + desc_html: Mastodon can auto-silence and auto-report accounts based on measures such as detecting accounts who send repeated unsolicited messages. There may be false positives. + title: Anti-spam thumbnail: desc_html: Used for previews via OpenGraph and API. 1200x630px recommended title: Server thumbnail diff --git a/config/settings.yml b/config/settings.yml index 75cb2dc853..ad2970bb7e 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -61,6 +61,7 @@ defaults: &defaults activity_api_enabled: true peers_api_enabled: true show_known_fediverse_at_about_page: true + spam_check_enabled: true development: <<: *defaults From 87f4aea52ade76313530071db77bb3b441a08416 Mon Sep 17 00:00:00 2001 From: ThibG Date: Thu, 18 Jul 2019 00:48:26 +0200 Subject: [PATCH 52/71] Fix typo in StatusPolicy (#11344) --- app/policies/status_policy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index 118b79f3ed..3d4e50d371 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -66,7 +66,7 @@ class StatusPolicy < ApplicationPolicy def author_blocking_domain? return false if current_account.nil? || current_account.domain.nil? - author.blocking_domain?(current_account.domain) + author.domain_blocking?(current_account.domain) end def blocking_author? From 7cc98eba2825072b2d83c1c7fffef2eaaf2473bb Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 18 Jul 2019 01:06:14 +0200 Subject: [PATCH 53/71] Bump puma from 3.12.1 to 4.0.1 (#11306) Bumps [puma](https://github.com/puma/puma) from 3.12.1 to 4.0.1. - [Release notes](https://github.com/puma/puma/releases) - [Changelog](https://github.com/puma/puma/blob/master/History.md) - [Commits](https://github.com/puma/puma/compare/v3.12.1...v4.0.1) Signed-off-by: dependabot-preview[bot] --- Gemfile | 2 +- Gemfile.lock | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 42208085ca..d826733247 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,7 @@ ruby '>= 2.4.0', '< 2.7.0' gem 'pkg-config', '~> 1.3' -gem 'puma', '~> 3.12' +gem 'puma', '~> 4.0' gem 'rails', '~> 5.2.3' gem 'thor', '~> 0.20' diff --git a/Gemfile.lock b/Gemfile.lock index 4036a05c1e..6ff6dac73d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -428,7 +428,8 @@ GEM pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (3.1.1) - puma (3.12.1) + puma (4.0.1) + nio4r (~> 2.0) pundit (2.0.1) activesupport (>= 3.0.0) raabro (1.1.6) @@ -731,7 +732,7 @@ DEPENDENCIES private_address_check (~> 0.5) pry-byebug (~> 3.7) pry-rails (~> 0.3) - puma (~> 3.12) + puma (~> 4.0) pundit (~> 2.0) rack-attack (~> 6.0) rack-cors (~> 1.0) From cbd7748961e2fcbb10d3fe1a549fba5c323fe395 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 18 Jul 2019 01:06:39 +0200 Subject: [PATCH 54/71] Bump rubocop-rails from 2.2.0 to 2.2.1 (#11308) Bumps [rubocop-rails](https://github.com/rubocop-hq/rubocop-rails) from 2.2.0 to 2.2.1. - [Release notes](https://github.com/rubocop-hq/rubocop-rails/releases) - [Changelog](https://github.com/rubocop-hq/rubocop-rails/blob/master/CHANGELOG.md) - [Commits](https://github.com/rubocop-hq/rubocop-rails/compare/v2.2.0...v2.2.1) Signed-off-by: dependabot-preview[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6ff6dac73d..e013c4ec02 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -539,7 +539,7 @@ GEM rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 1.7) - rubocop-rails (2.2.0) + rubocop-rails (2.2.1) rack (>= 1.1) rubocop (>= 0.72.0) ruby-progressbar (1.10.1) From 3a6fe657ba30c56677c271432338a7002191d772 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 18 Jul 2019 01:07:02 +0200 Subject: [PATCH 55/71] Bump rack-attack from 6.0.0 to 6.1.0 (#11313) Bumps [rack-attack](https://github.com/kickstarter/rack-attack) from 6.0.0 to 6.1.0. - [Release notes](https://github.com/kickstarter/rack-attack/releases) - [Changelog](https://github.com/kickstarter/rack-attack/blob/master/CHANGELOG.md) - [Commits](https://github.com/kickstarter/rack-attack/compare/v6.0.0...v6.1.0) Signed-off-by: dependabot-preview[bot] --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index d826733247..d27e3560cb 100644 --- a/Gemfile +++ b/Gemfile @@ -67,7 +67,7 @@ gem 'ox', '~> 2.11' gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c' gem 'pundit', '~> 2.0' gem 'premailer-rails' -gem 'rack-attack', '~> 6.0' +gem 'rack-attack', '~> 6.1' gem 'rack-cors', '~> 1.0', require: 'rack/cors' gem 'rails-i18n', '~> 5.1' gem 'rails-settings-cached', '~> 0.6' diff --git a/Gemfile.lock b/Gemfile.lock index e013c4ec02..7be1679b35 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -434,7 +434,7 @@ GEM activesupport (>= 3.0.0) raabro (1.1.6) rack (2.0.7) - rack-attack (6.0.0) + rack-attack (6.1.0) rack (>= 1.0, < 3) rack-cors (1.0.3) rack-protection (2.0.5) @@ -734,7 +734,7 @@ DEPENDENCIES pry-rails (~> 0.3) puma (~> 4.0) pundit (~> 2.0) - rack-attack (~> 6.0) + rack-attack (~> 6.1) rack-cors (~> 1.0) rails (~> 5.2.3) rails-controller-testing (~> 1.0) From 5bfe1e1f0517a23637a1a132dbf0b62fd29982bc Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 18 Jul 2019 03:02:15 +0200 Subject: [PATCH 56/71] Change language detection to include hashtags as words (#11341) --- app/lib/language_detector.rb | 2 +- spec/lib/language_detector_spec.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/lib/language_detector.rb b/app/lib/language_detector.rb index 1e90af42d7..6f9511a541 100644 --- a/app/lib/language_detector.rb +++ b/app/lib/language_detector.rb @@ -69,7 +69,7 @@ class LanguageDetector new_text = remove_html(text) new_text.gsub!(FetchLinkCardService::URL_PATTERN, '') new_text.gsub!(Account::MENTION_RE, '') - new_text.gsub!(Tag::HASHTAG_RE, '') + new_text.gsub!(Tag::HASHTAG_RE) { |string| string.gsub(/[#_]/, '#' => '', '_' => ' ').gsub(/[a-z][A-Z]|[a-zA-Z][\d]/) { |s| s.insert(1, ' ') }.downcase } new_text.gsub!(/:#{CustomEmoji::SHORTCODE_RE_FRAGMENT}:/, '') new_text.gsub!(/\s+/, ' ') new_text diff --git a/spec/lib/language_detector_spec.rb b/spec/lib/language_detector_spec.rb index 0cb70605ad..b7ba0f6c4f 100644 --- a/spec/lib/language_detector_spec.rb +++ b/spec/lib/language_detector_spec.rb @@ -32,11 +32,11 @@ describe LanguageDetector do expect(result).to eq 'Our website is and also' end - it 'strips #hashtags from strings before detection' do - string = 'Hey look at all the #animals and #fish' + it 'converts #hashtags back to normal text before detection' do + string = 'Hey look at all the #animals and #FishAndChips' result = described_class.instance.send(:prepare_text, string) - expect(result).to eq 'Hey look at all the and' + expect(result).to eq 'Hey look at all the animals and fish and chips' end end From 4906cabc6b8d91136973ad159f6a02209ea8e166 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 18 Jul 2019 03:02:30 +0200 Subject: [PATCH 57/71] Add aac, m4a, 3gp to allowed audio formats (#11342) Fix #11186 --- app/models/media_attachment.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 30d9a98512..05be302aff 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -26,14 +26,14 @@ class MediaAttachment < ApplicationRecord enum type: [:image, :gifv, :video, :unknown, :audio] - IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].freeze - VIDEO_FILE_EXTENSIONS = ['.webm', '.mp4', '.m4v', '.mov'].freeze - AUDIO_FILE_EXTENSIONS = ['.ogg', '.oga', '.mp3', '.wav', '.flac', '.opus'].freeze + IMAGE_FILE_EXTENSIONS = %w(.jpg .jpeg .png .gif .webp).freeze + VIDEO_FILE_EXTENSIONS = %w(.webm .mp4 .m4v .mov).freeze + AUDIO_FILE_EXTENSIONS = %w(.ogg .oga .mp3 .wav .flac .opus .aac .m4a .3gp).freeze - IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze - VIDEO_MIME_TYPES = ['video/webm', 'video/mp4', 'video/quicktime', 'video/ogg'].freeze - VIDEO_CONVERTIBLE_MIME_TYPES = ['video/webm', 'video/quicktime'].freeze - AUDIO_MIME_TYPES = ['audio/wave', 'audio/wav', 'audio/x-wav', 'audio/x-pn-wave', 'audio/ogg', 'audio/mpeg', 'audio/mp3', 'audio/webm', 'audio/flac'].freeze + IMAGE_MIME_TYPES = %w(image/jpeg image/png image/gif image/webp).freeze + VIDEO_MIME_TYPES = %w(video/webm video/mp4 video/quicktime video/ogg).freeze + VIDEO_CONVERTIBLE_MIME_TYPES = %w(video/webm video/quicktime).freeze + AUDIO_MIME_TYPES = %w(audio/wave audio/wav audio/x-wav audio/x-pn-wave audio/ogg audio/mpeg audio/mp3 audio/webm audio/flac audio/aac audio/m4a audio/3gpp).freeze BLURHASH_OPTIONS = { x_comp: 4, From 84e988479e956bcf522072879da936b100d1d46b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 18 Jul 2019 03:02:56 +0200 Subject: [PATCH 58/71] Fix only one middle dot being recognized in hashtags (#11345) Fix #10934 --- app/models/tag.rb | 2 +- spec/models/tag_spec.rb | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/app/models/tag.rb b/app/models/tag.rb index 7db76d157b..01bace2bbf 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -17,7 +17,7 @@ class Tag < ApplicationRecord has_many :featured_tags, dependent: :destroy, inverse_of: :tag has_one :account_tag_stat, dependent: :destroy - HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_·][[:word:]_]*' + HASHTAG_NAME_RE = '[[:word:]_][[:word:]_]*[[:alpha:]_·]*[[:word:]_·]*[[:word:]_]' HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i validates :name, presence: true, uniqueness: true, format: { with: /\A#{HASHTAG_NAME_RE}\z/i } diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index 1ca50cc29f..1618623926 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -31,7 +31,43 @@ RSpec.describe Tag, type: :model do end it 'matches #aesthetic' do - expect(subject.match('this is #aesthetic')).to_not be_nil + expect(subject.match('this is #aesthetic').to_s).to eq ' #aesthetic' + end + + it 'matches digits at the start' do + expect(subject.match('hello #3d').to_s).to eq ' #3d' + end + + it 'matches digits in the middle' do + expect(subject.match('hello #l33ts35k').to_s).to eq ' #l33ts35k' + end + + it 'matches digits at the end' do + expect(subject.match('hello #world2016').to_s).to eq ' #world2016' + end + + it 'matches underscores at the beginning' do + expect(subject.match('hello #_test').to_s).to eq ' #_test' + end + + it 'matches underscores at the end' do + expect(subject.match('hello #test_').to_s).to eq ' #test_' + end + + it 'matches underscores in the middle' do + expect(subject.match('hello #one_two_three').to_s).to eq ' #one_two_three' + end + + it 'matches middle dots' do + expect(subject.match('hello #one·two·three').to_s).to eq ' #one·two·three' + end + + it 'does not match middle dots at the start' do + expect(subject.match('hello #·one·two·three')).to be_nil + end + + it 'does not match middle dots at the end' do + expect(subject.match('hello #one·two·three·').to_s).to eq ' #one·two·three' end end From 15c7478c5560a1f654d0d00d8ee2a624acb34089 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 18 Jul 2019 20:28:05 +0200 Subject: [PATCH 59/71] Change Dockerfile to bind to 0.0.0.0 instead of docker-compose.yml (#11351) --- Dockerfile | 1 + docker-compose.yml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3acbc9d4ce..d8c7e0f0c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -112,6 +112,7 @@ ENV NODE_ENV="production" # Tell rails to serve static files ENV RAILS_SERVE_STATIC_FILES="true" +ENV BIND="0.0.0.0" # Set the run user USER mastodon diff --git a/docker-compose.yml b/docker-compose.yml index f3fe6cfd0b..7406849663 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,7 +38,7 @@ services: image: tootsuite/mastodon restart: always env_file: .env.production - command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000 -b '0.0.0.0'" + command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000" networks: - external_network - internal_network @@ -58,7 +58,7 @@ services: image: tootsuite/mastodon restart: always env_file: .env.production - command: BIND=0.0.0.0 node ./streaming + command: node ./streaming networks: - external_network - internal_network From 730c4053d642024b9949d72c8a9f1873532c6212 Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 19 Jul 2019 01:44:42 +0200 Subject: [PATCH 60/71] Add ActivityPub actor representing the entire server (#11321) * Add support for an instance actor * Skip username validation for local Application accounts * Add migration script to create instance actor * Make Codeclimate happy * Switch to id -99 for instance actor * Remove unused `icon` and `image` attributes from instance actor * Use if/elsif/else instead of return + ternary operator * Add instance actor to fresh installs * Use instance actor as instance representative Use instance actor for forwarding reports, relay operations, and spam auto-reporting. * Seed database in test environment * Fix single-user mode * Fix tests * Fix specs to accomodate for an extra `Account` * Auto-reject follows on instance actor Following an instance actor might make sense, but we are not handling that right now, so auto-reject. * Fix webfinger lookup and serialization for instance actor * Rename instance actor * Make it clear in the HTML view that the instance actor should not be blocked * Raise cache time for instance actor as there's no dynamic content * Re-use /about/more with a flash message for instance actor profile --- app/controllers/about_controller.rb | 4 +- app/controllers/application_controller.rb | 2 +- app/controllers/home_controller.rb | 2 +- app/controllers/instance_actors_controller.rb | 20 +++++++ .../styles/mastodon/containers.scss | 4 ++ app/lib/activitypub/activity/follow.rb | 2 +- app/lib/activitypub/tag_manager.rb | 5 +- app/lib/webfinger_resource.rb | 6 ++ app/models/account.rb | 8 ++- app/models/concerns/account_finder_concern.rb | 2 +- .../activitypub/actor_serializer.rb | 14 +++-- app/serializers/webfinger_serializer.rb | 25 +++++--- app/views/about/more.html.haml | 2 + app/views/well_known/webfinger/show.xml.ruby | 57 ++++++++++++------- config/locales/en.yml | 3 + config/routes.rb | 4 ++ .../20190715164535_add_instance_actor.rb | 9 +++ db/schema.rb | 2 +- db/seeds.rb | 4 +- spec/models/account_spec.rb | 12 ++-- .../fetch_remote_account_service_spec.rb | 1 - spec/services/fetch_resource_service_spec.rb | 4 +- spec/spec_helper.rb | 1 + 23 files changed, 141 insertions(+), 52 deletions(-) create mode 100644 app/controllers/instance_actors_controller.rb create mode 100644 db/migrate/20190715164535_add_instance_actor.rb diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 52fb1dc1b3..33bac9bbc7 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -11,7 +11,9 @@ class AboutController < ApplicationController def show; end - def more; end + def more + flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor] + end def terms; end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 26f3b1def5..51e9764d49 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -91,7 +91,7 @@ class ApplicationController < ActionController::Base end def single_user_mode? - @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.exists? + @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists? end def use_seamless_external_login? diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index d1c5251347..42493cd782 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -58,7 +58,7 @@ class HomeController < ApplicationController if request.path.start_with?('/web') new_user_session_path elsif single_user_mode? - short_account_path(Account.local.without_suspended.first) + short_account_path(Account.local.without_suspended.where('id > 0').first) else about_path end diff --git a/app/controllers/instance_actors_controller.rb b/app/controllers/instance_actors_controller.rb new file mode 100644 index 0000000000..41f33602e6 --- /dev/null +++ b/app/controllers/instance_actors_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class InstanceActorsController < ApplicationController + include AccountControllerConcern + + def show + expires_in 10.minutes, public: true + render json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to + end + + private + + def set_account + @account = Account.find(-99) + end + + def restrict_fields_to + %i(id type preferred_username inbox public_key endpoints url manually_approves_followers) + end +end diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index 3564bf07b4..2b6794ee2c 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -145,6 +145,10 @@ min-height: 100%; } + .flash-message { + margin-bottom: 10px; + } + @media screen and (max-width: 738px) { grid-template-columns: minmax(0, 50%) minmax(0, 50%); diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb index 3eb88339ae..28f1da19f8 100644 --- a/app/lib/activitypub/activity/follow.rb +++ b/app/lib/activitypub/activity/follow.rb @@ -8,7 +8,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id']) || @account.requested?(target_account) - if target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain) || target_account.moved? + if target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain) || target_account.moved? || target_account.instance_actor? reject_follow_request!(target_account) return end diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 4d452f290b..512272dbeb 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -17,7 +17,7 @@ class ActivityPub::TagManager case target.object_type when :person - short_account_url(target) + target.instance_actor? ? about_more_url(instance_actor: true) : short_account_url(target) when :note, :comment, :activity return activity_account_status_url(target.account, target) if target.reblog? short_account_status_url(target.account, target) @@ -29,7 +29,7 @@ class ActivityPub::TagManager case target.object_type when :person - account_url(target) + target.instance_actor? ? instance_actor_url : account_url(target) when :note, :comment, :activity return activity_account_status_url(target.account, target) if target.reblog? account_status_url(target.account, target) @@ -119,6 +119,7 @@ class ActivityPub::TagManager def uri_to_local_id(uri, param = :id) path_params = Rails.application.routes.recognize_path(uri) + path_params[:username] = Rails.configuration.x.local_domain if path_params[:controller] == 'instance_actors' path_params[param] end diff --git a/app/lib/webfinger_resource.rb b/app/lib/webfinger_resource.rb index a54a702a2e..22d78874a4 100644 --- a/app/lib/webfinger_resource.rb +++ b/app/lib/webfinger_resource.rb @@ -23,11 +23,17 @@ class WebfingerResource def username_from_url if account_show_page? path_params[:username] + elsif instance_actor_page? + Rails.configuration.x.local_domain else raise ActiveRecord::RecordNotFound end end + def instance_actor_page? + path_params[:controller] == 'instance_actors' + end + def account_show_page? path_params[:controller] == 'accounts' && path_params[:action] == 'show' end diff --git a/app/models/account.rb b/app/models/account.rb index adf4586fa5..ccd116d6e2 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -77,7 +77,7 @@ class Account < ApplicationRecord validates :username, format: { with: /\A#{USERNAME_RE}\z/i }, if: -> { !local? && will_save_change_to_username? } # Local user validations - validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: 30 }, if: -> { local? && will_save_change_to_username? } + validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: 30 }, if: -> { local? && will_save_change_to_username? && actor_type != 'Application' } validates_with UniqueUsernameValidator, if: -> { local? && will_save_change_to_username? } validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? } validates :display_name, length: { maximum: 30 }, if: -> { local? && will_save_change_to_display_name? } @@ -139,6 +139,10 @@ class Account < ApplicationRecord %w(Application Service).include? actor_type end + def instance_actor? + id == -99 + end + alias bot bot? def bot=(val) @@ -498,7 +502,7 @@ class Account < ApplicationRecord end def generate_keys - return unless local? && !Rails.env.test? + return unless local? && private_key.blank? && public_key.blank? keypair = OpenSSL::PKey::RSA.new(2048) self.private_key = keypair.to_pem diff --git a/app/models/concerns/account_finder_concern.rb b/app/models/concerns/account_finder_concern.rb index ccd7bfa123..a54c2174d4 100644 --- a/app/models/concerns/account_finder_concern.rb +++ b/app/models/concerns/account_finder_concern.rb @@ -13,7 +13,7 @@ module AccountFinderConcern end def representative - find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')) || Account.local.without_suspended.first + Account.find(-99) end def find_local(username) diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 0644219fb6..0bd7aed2e9 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -39,11 +39,17 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer delegate :moved?, to: :object def id - account_url(object) + object.instance_actor? ? instance_actor_url : account_url(object) end def type - object.bot? ? 'Service' : 'Person' + if object.instance_actor? + 'Application' + elsif object.bot? + 'Service' + else + 'Person' + end end def following @@ -55,7 +61,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer end def inbox - account_inbox_url(object) + object.instance_actor? ? instance_actor_inbox_url : account_inbox_url(object) end def outbox @@ -95,7 +101,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer end def url - short_account_url(object) + object.instance_actor? ? about_more_url(instance_actor: true) : short_account_url(object) end def avatar_exists? diff --git a/app/serializers/webfinger_serializer.rb b/app/serializers/webfinger_serializer.rb index f4af215510..008d0c1829 100644 --- a/app/serializers/webfinger_serializer.rb +++ b/app/serializers/webfinger_serializer.rb @@ -10,15 +10,26 @@ class WebfingerSerializer < ActiveModel::Serializer end def aliases - [short_account_url(object), account_url(object)] + if object.instance_actor? + [instance_actor_url] + else + [short_account_url(object), account_url(object)] + end end def links - [ - { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) }, - { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(object, format: 'atom') }, - { rel: 'self', type: 'application/activity+json', href: account_url(object) }, - { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" }, - ] + if object.instance_actor? + [ + { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: about_more_url(instance_actor: true) }, + { rel: 'self', type: 'application/activity+json', href: instance_actor_url }, + ] + else + [ + { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) }, + { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(object, format: 'atom') }, + { rel: 'self', type: 'application/activity+json', href: account_url(object) }, + { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" }, + ] + end end end diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml index b248ed1d23..21431ef8e5 100644 --- a/app/views/about/more.html.haml +++ b/app/views/about/more.html.haml @@ -43,5 +43,7 @@ = mail_to @instance_presenter.site_contact_email, nil, title: @instance_presenter.site_contact_email .column-3 + = render 'application/flashes' + .box-widget .rich-formatting= @instance_presenter.site_extended_description.html_safe.presence || t('about.extended_description_html') diff --git a/app/views/well_known/webfinger/show.xml.ruby b/app/views/well_known/webfinger/show.xml.ruby index ae80df9d2f..f5a54052a6 100644 --- a/app/views/well_known/webfinger/show.xml.ruby +++ b/app/views/well_known/webfinger/show.xml.ruby @@ -4,30 +4,47 @@ doc << Ox::Element.new('XRD').tap do |xrd| xrd['xmlns'] = 'http://docs.oasis-open.org/ns/xri/xrd-1.0' xrd << (Ox::Element.new('Subject') << @account.to_webfinger_s) - xrd << (Ox::Element.new('Alias') << short_account_url(@account)) - xrd << (Ox::Element.new('Alias') << account_url(@account)) - xrd << Ox::Element.new('Link').tap do |link| - link['rel'] = 'http://webfinger.net/rel/profile-page' - link['type'] = 'text/html' - link['href'] = short_account_url(@account) - end + if @account.instance_actor? + xrd << (Ox::Element.new('Alias') << instance_actor_url) - xrd << Ox::Element.new('Link').tap do |link| - link['rel'] = 'http://schemas.google.com/g/2010#updates-from' - link['type'] = 'application/atom+xml' - link['href'] = account_url(@account, format: 'atom') - end + xrd << Ox::Element.new('Link').tap do |link| + link['rel'] = 'http://webfinger.net/rel/profile-page' + link['type'] = 'text/html' + link['href'] = about_more_url(instance_actor: true) + end - xrd << Ox::Element.new('Link').tap do |link| - link['rel'] = 'self' - link['type'] = 'application/activity+json' - link['href'] = account_url(@account) - end + xrd << Ox::Element.new('Link').tap do |link| + link['rel'] = 'self' + link['type'] = 'application/activity+json' + link['href'] = instance_actor_url + end + else + xrd << (Ox::Element.new('Alias') << short_account_url(@account)) + xrd << (Ox::Element.new('Alias') << account_url(@account)) - xrd << Ox::Element.new('Link').tap do |link| - link['rel'] = 'http://ostatus.org/schema/1.0/subscribe' - link['template'] = "#{authorize_interaction_url}?acct={uri}" + xrd << Ox::Element.new('Link').tap do |link| + link['rel'] = 'http://webfinger.net/rel/profile-page' + link['type'] = 'text/html' + link['href'] = short_account_url(@account) + end + + xrd << Ox::Element.new('Link').tap do |link| + link['rel'] = 'http://schemas.google.com/g/2010#updates-from' + link['type'] = 'application/atom+xml' + link['href'] = account_url(@account, format: 'atom') + end + + xrd << Ox::Element.new('Link').tap do |link| + link['rel'] = 'self' + link['type'] = 'application/activity+json' + link['href'] = account_url(@account) + end + + xrd << Ox::Element.new('Link').tap do |link| + link['rel'] = 'http://ostatus.org/schema/1.0/subscribe' + link['template'] = "#{authorize_interaction_url}?acct={uri}" + end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 4e252945f1..89c52b84ae 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -24,6 +24,9 @@ en: generic_description: "%{domain} is one server in the network" get_apps: Try a mobile app hosted_on: Mastodon hosted on %{domain} + instance_actor_flash: | + This account is a virtual actor used to represent the server itself and not any individual user. + It is used for federation purposes and should not be blocked unless you want to block the whole instance, in which case you should use a domain block. learn_more: Learn more privacy_policy: Privacy policy see_whats_happening: See what's happening diff --git a/config/routes.rb b/config/routes.rb index 95f8a39add..27b5366419 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -28,6 +28,10 @@ Rails.application.routes.draw do get 'intent', to: 'intents#show' get 'custom.css', to: 'custom_css#show', as: :custom_css + resource :instance_actor, path: 'actor', only: [:show] do + resource :inbox, only: [:create], module: :activitypub + end + devise_scope :user do get '/invite/:invite_code', to: 'auth/registrations#new', as: :public_invite match '/auth/finish_signup' => 'auth/confirmations#finish_signup', via: [:get, :patch], as: :finish_signup diff --git a/db/migrate/20190715164535_add_instance_actor.rb b/db/migrate/20190715164535_add_instance_actor.rb new file mode 100644 index 0000000000..a26d549493 --- /dev/null +++ b/db/migrate/20190715164535_add_instance_actor.rb @@ -0,0 +1,9 @@ +class AddInstanceActor < ActiveRecord::Migration[5.2] + def up + Account.create!(id: -99, actor_type: 'Application', locked: true, username: Rails.configuration.x.local_domain) + end + + def down + Account.find_by(id: -99, actor_type: 'Application').destroy! + end +end diff --git a/db/schema.rb b/db/schema.rb index c7b6b9be69..a6a14827b9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_07_06_233204) do +ActiveRecord::Schema.define(version: 2019_07_15_164535) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/db/seeds.rb b/db/seeds.rb index 9a6e9dd78e..5f43fbac8b 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,7 +1,9 @@ Doorkeeper::Application.create!(name: 'Web', superapp: true, redirect_uri: Doorkeeper.configuration.native_redirect_uri, scopes: 'read write follow') +domain = ENV['LOCAL_DOMAIN'] || Rails.configuration.x.local_domain +Account.create!(id: -99, actor_type: 'Application', locked: true, username: domain) + if Rails.env.development? - domain = ENV['LOCAL_DOMAIN'] || Rails.configuration.x.local_domain admin = Account.where(username: 'admin').first_or_initialize(username: 'admin') admin.save(validate: false) User.where(email: "admin@#{domain}").first_or_initialize(email: "admin@#{domain}", password: 'mastodonadmin', password_confirmation: 'mastodonadmin', confirmed_at: Time.now.utc, admin: true, account: admin, agreement: true, approved: true).save! diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index ce9ea250d1..6495a61934 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -450,7 +450,7 @@ RSpec.describe Account, type: :model do describe '.domains' do it 'returns domains' do Fabricate(:account, domain: 'domain') - expect(Account.domains).to match_array(['domain']) + expect(Account.remote.domains).to match_array(['domain']) end end @@ -665,7 +665,7 @@ RSpec.describe Account, type: :model do { username: 'b', domain: 'b' }, ].map(&method(:Fabricate).curry(2).call(:account)) - expect(Account.alphabetic).to eq matches + expect(Account.where('id > 0').alphabetic).to eq matches end end @@ -732,7 +732,7 @@ RSpec.describe Account, type: :model do 2.times { Fabricate(:account, domain: 'example.com') } Fabricate(:account, domain: 'example2.com') - results = Account.by_domain_accounts + results = Account.where('id > 0').by_domain_accounts expect(results.length).to eq 2 expect(results.first.domain).to eq 'example.com' expect(results.first.accounts_count).to eq 2 @@ -745,7 +745,7 @@ RSpec.describe Account, type: :model do it 'returns an array of accounts who do not have a domain' do account_1 = Fabricate(:account, domain: nil) account_2 = Fabricate(:account, domain: 'example.com') - expect(Account.local).to match_array([account_1]) + expect(Account.where('id > 0').local).to match_array([account_1]) end end @@ -756,14 +756,14 @@ RSpec.describe Account, type: :model do matches[index] = Fabricate(:account, domain: matches[index]) end - expect(Account.partitioned).to match_array(matches) + expect(Account.where('id > 0').partitioned).to match_array(matches) end end describe 'recent' do it 'returns a relation of accounts sorted by recent creation' do matches = 2.times.map { Fabricate(:account) } - expect(Account.recent).to match_array(matches) + expect(Account.where('id > 0').recent).to match_array(matches) end end diff --git a/spec/services/fetch_remote_account_service_spec.rb b/spec/services/fetch_remote_account_service_spec.rb index b374458610..ee7325be28 100644 --- a/spec/services/fetch_remote_account_service_spec.rb +++ b/spec/services/fetch_remote_account_service_spec.rb @@ -4,7 +4,6 @@ RSpec.describe FetchRemoteAccountService, type: :service do let(:url) { 'https://example.com/alice' } let(:prefetched_body) { nil } let(:protocol) { :ostatus } - let!(:representative) { Fabricate(:account) } subject { FetchRemoteAccountService.new.call(url, prefetched_body, protocol) } diff --git a/spec/services/fetch_resource_service_spec.rb b/spec/services/fetch_resource_service_spec.rb index 98630966b5..f836147d35 100644 --- a/spec/services/fetch_resource_service_spec.rb +++ b/spec/services/fetch_resource_service_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' RSpec.describe FetchResourceService, type: :service do - let!(:representative) { Fabricate(:account) } - describe '#call' do let(:url) { 'http://example.com' } @@ -60,7 +58,7 @@ RSpec.describe FetchResourceService, type: :service do it 'signs request' do subject - expect(a_request(:get, url).with(headers: { 'Signature' => /keyId="#{Regexp.escape(ActivityPub::TagManager.instance.uri_for(representative) + '#main-key')}"/ })).to have_been_made + expect(a_request(:get, url).with(headers: { 'Signature' => /keyId="#{Regexp.escape(ActivityPub::TagManager.instance.uri_for(Account.representative) + '#main-key')}"/ })).to have_been_made end context 'when content type is application/atom+xml' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0cd1f91d02..45ba1bbd9b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -27,6 +27,7 @@ RSpec.configure do |config| end config.before :suite do + Rails.application.load_seed Chewy.strategy(:bypass) end From fda437a02088ac114fecb69e3b1e52f495a2dd9a Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 19 Jul 2019 01:44:58 +0200 Subject: [PATCH 61/71] Fix sanitizing lists contents (#11354) * Add test * Fix code for sanitizing nested lists stripping all tags --- app/lib/sanitize_config.rb | 2 ++ spec/lib/sanitize_config_spec.rb | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/app/lib/sanitize_config.rb b/app/lib/sanitize_config.rb index e82a2a33aa..aba8ce9f61 100644 --- a/app/lib/sanitize_config.rb +++ b/app/lib/sanitize_config.rb @@ -25,6 +25,8 @@ class Sanitize case env[:node_name] when 'li' env[:node].traverse do |node| + next unless %w(p ul ol li).include?(node.name) + node.add_next_sibling('
    ') if node.next_sibling node.replace(node.children) unless node.text? end diff --git a/spec/lib/sanitize_config_spec.rb b/spec/lib/sanitize_config_spec.rb index bb3cf6f0b2..54bd8693cc 100644 --- a/spec/lib/sanitize_config_spec.rb +++ b/spec/lib/sanitize_config_spec.rb @@ -22,5 +22,9 @@ describe Sanitize::Config do it 'converts ul inside ul' do expect(Sanitize.fragment('
    • Foo
      • Bar
      • Baz
    ', subject)).to eq '

    Foo
    Bar
    Baz

    ' end + + it 'keep links in lists' do + expect(Sanitize.fragment('

    Check out:

    ', subject)).to eq '

    Check out:

    joinmastodon.org
    Bar

    ' + end end end From 8df0022e6607e9f18bd2ba8c27f17333f5385693 Mon Sep 17 00:00:00 2001 From: koyu Date: Fri, 19 Jul 2019 03:58:46 +0200 Subject: [PATCH 62/71] Added logout to dropdown menu (#11353) * Added logout to dropdown menu * Triggering build-and-test with empty commit as it seems it failed due to some internal failure * Looks fine, ready to review * Added changes from review * method can be null without any problems * Also target can be null --- app/javascript/mastodon/components/dropdown_menu.js | 4 ++-- .../mastodon/features/compose/components/action_bar.js | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index 91b65a02fb..e122515c49 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -122,11 +122,11 @@ class DropdownMenu extends React.PureComponent { return
  • ; } - const { text, href = '#' } = option; + const { text, href = '#', target = '_blank', method } = option; return (
  • - + {text}
  • diff --git a/app/javascript/mastodon/features/compose/components/action_bar.js b/app/javascript/mastodon/features/compose/components/action_bar.js index 077226d70b..d0303dbfbb 100644 --- a/app/javascript/mastodon/features/compose/components/action_bar.js +++ b/app/javascript/mastodon/features/compose/components/action_bar.js @@ -15,6 +15,7 @@ const messages = defineMessages({ domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, + logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, }); export default @injectIntl @@ -42,6 +43,8 @@ class ActionBar extends React.PureComponent { menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' }); menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' }); menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.logout), href: '/auth/sign_out', target: null, method: 'delete' }); return (
    From 4fa6472523d47e56f2458950af8d7ad9b5817a82 Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 19 Jul 2019 09:18:23 +0200 Subject: [PATCH 63/71] Fix avatar animation on hover when not logged in (#11349) --- app/javascript/packs/public.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index 3135636cfb..2924413855 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -178,7 +178,7 @@ function main() { return ({ target }) => { const swapSrc = target.getAttribute(swapTo); //only change the img source if autoplay is off and the image src is actually different - if(target.getAttribute('data-autoplay') === 'false' && target.src !== swapSrc) { + if(target.getAttribute('data-autoplay') !== 'true' && target.src !== swapSrc) { target.src = swapSrc; } }; From aa22b38fdbc1842549b6cbc0e0d948f85a71b92a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 19 Jul 2019 09:25:22 +0200 Subject: [PATCH 64/71] Change single-column mode to scroll the whole page (#11359) Fix #10840 --- .../mastodon/components/scrollable_list.js | 58 ++++++++++++++----- .../mastodon/containers/media_container.js | 7 +++ .../features/account_timeline/index.js | 4 +- .../mastodon/features/blocks/index.js | 4 +- .../features/community_timeline/index.js | 1 + .../mastodon/features/domain_blocks/index.js | 4 +- .../features/favourited_statuses/index.js | 1 + .../mastodon/features/favourites/index.js | 4 +- .../features/follow_requests/index.js | 4 +- .../mastodon/features/followers/index.js | 4 +- .../mastodon/features/following/index.js | 4 +- .../features/hashtag_timeline/index.js | 1 + .../mastodon/features/home_timeline/index.js | 1 + .../mastodon/features/list_timeline/index.js | 1 + .../mastodon/features/lists/index.js | 4 +- .../mastodon/features/mutes/index.js | 4 +- .../mastodon/features/notifications/index.js | 1 + .../features/pinned_statuses/index.js | 4 +- .../features/public_timeline/index.js | 1 + .../mastodon/features/reblogs/index.js | 4 +- .../features/ui/components/modal_root.js | 24 ++++++++ app/javascript/mastodon/features/ui/index.js | 15 ++++- app/javascript/packs/public.js | 8 --- app/javascript/styles/mastodon/basics.scss | 34 ++++++++--- .../styles/mastodon/components.scss | 7 ++- 25 files changed, 162 insertions(+), 42 deletions(-) diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js index 553c163524..0bf8179233 100644 --- a/app/javascript/mastodon/components/scrollable_list.js +++ b/app/javascript/mastodon/components/scrollable_list.js @@ -35,6 +35,7 @@ export default class ScrollableList extends PureComponent { alwaysPrepend: PropTypes.bool, emptyMessage: PropTypes.node, children: PropTypes.node, + bindToDocument: PropTypes.bool, }; static defaultProps = { @@ -50,7 +51,9 @@ export default class ScrollableList extends PureComponent { handleScroll = throttle(() => { if (this.node) { - const { scrollTop, scrollHeight, clientHeight } = this.node; + const scrollTop = this.getScrollTop(); + const scrollHeight = this.getScrollHeight(); + const clientHeight = this.getClientHeight(); const offset = scrollHeight - scrollTop - clientHeight; if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) { @@ -80,9 +83,14 @@ export default class ScrollableList extends PureComponent { scrollToTopOnMouseIdle = false; setScrollTop = newScrollTop => { - if (this.node.scrollTop !== newScrollTop) { + if (this.getScrollTop() !== newScrollTop) { this.lastScrollWasSynthetic = true; - this.node.scrollTop = newScrollTop; + + if (this.props.bindToDocument) { + document.scrollingElement.scrollTop = newScrollTop; + } else { + this.node.scrollTop = newScrollTop; + } } }; @@ -100,7 +108,7 @@ export default class ScrollableList extends PureComponent { this.clearMouseIdleTimer(); this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY); - if (!this.mouseMovedRecently && this.node.scrollTop === 0) { + if (!this.mouseMovedRecently && this.getScrollTop() === 0) { // Only set if we just started moving and are scrolled to the top. this.scrollToTopOnMouseIdle = true; } @@ -135,15 +143,27 @@ export default class ScrollableList extends PureComponent { } getScrollPosition = () => { - if (this.node && (this.node.scrollTop > 0 || this.mouseMovedRecently)) { - return { height: this.node.scrollHeight, top: this.node.scrollTop }; + if (this.node && (this.getScrollTop() > 0 || this.mouseMovedRecently)) { + return { height: this.getScrollHeight(), top: this.getScrollTop() }; } else { return null; } } + getScrollTop = () => { + return this.props.bindToDocument ? document.scrollingElement.scrollTop : this.node.scrollTop; + } + + getScrollHeight = () => { + return this.props.bindToDocument ? document.scrollingElement.scrollHeight : this.node.scrollHeight; + } + + getClientHeight = () => { + return this.props.bindToDocument ? document.scrollingElement.clientHeight : this.node.clientHeight; + } + updateScrollBottom = (snapshot) => { - const newScrollTop = this.node.scrollHeight - snapshot; + const newScrollTop = this.getScrollHeight() - snapshot; this.setScrollTop(newScrollTop); } @@ -153,8 +173,8 @@ export default class ScrollableList extends PureComponent { React.Children.count(prevProps.children) < React.Children.count(this.props.children) && this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); - if (someItemInserted && (this.node.scrollTop > 0 || this.mouseMovedRecently)) { - return this.node.scrollHeight - this.node.scrollTop; + if (someItemInserted && (this.getScrollTop() > 0 || this.mouseMovedRecently)) { + return this.getScrollHeight() - this.getScrollTop(); } else { return null; } @@ -164,7 +184,7 @@ export default class ScrollableList extends PureComponent { // Reset the scroll position when a new child comes in in order not to // jerk the scrollbar around if you're already scrolled down the page. if (snapshot !== null) { - this.setScrollTop(this.node.scrollHeight - snapshot); + this.setScrollTop(this.getScrollHeight() - snapshot); } } @@ -197,13 +217,23 @@ export default class ScrollableList extends PureComponent { } attachScrollListener () { - this.node.addEventListener('scroll', this.handleScroll); - this.node.addEventListener('wheel', this.handleWheel); + if (this.props.bindToDocument) { + document.addEventListener('scroll', this.handleScroll); + document.addEventListener('wheel', this.handleWheel); + } else { + this.node.addEventListener('scroll', this.handleScroll); + this.node.addEventListener('wheel', this.handleWheel); + } } detachScrollListener () { - this.node.removeEventListener('scroll', this.handleScroll); - this.node.removeEventListener('wheel', this.handleWheel); + if (this.props.bindToDocument) { + document.removeEventListener('scroll', this.handleScroll); + document.removeEventListener('wheel', this.handleWheel); + } else { + this.node.removeEventListener('scroll', this.handleScroll); + this.node.removeEventListener('wheel', this.handleWheel); + } } getFirstChildKey (props) { diff --git a/app/javascript/mastodon/containers/media_container.js b/app/javascript/mastodon/containers/media_container.js index 51d4f0fed7..48492f43d0 100644 --- a/app/javascript/mastodon/containers/media_container.js +++ b/app/javascript/mastodon/containers/media_container.js @@ -8,6 +8,7 @@ import Video from '../features/video'; import Card from '../features/status/components/card'; import Poll from 'mastodon/components/poll'; import ModalRoot from '../components/modal_root'; +import { getScrollbarWidth } from '../features/ui/components/modal_root'; import MediaModal from '../features/ui/components/media_modal'; import { List as ImmutableList, fromJS } from 'immutable'; @@ -31,6 +32,8 @@ export default class MediaContainer extends PureComponent { handleOpenMedia = (media, index) => { document.body.classList.add('with-modals--active'); + document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; + this.setState({ media, index }); } @@ -38,11 +41,15 @@ export default class MediaContainer extends PureComponent { const media = ImmutableList([video]); document.body.classList.add('with-modals--active'); + document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; + this.setState({ media, time }); } handleCloseMedia = () => { document.body.classList.remove('with-modals--active'); + document.documentElement.style.marginRight = 0; + this.setState({ media: null, index: null, time: null }); } diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js index 27581bfdc8..9914b7e654 100644 --- a/app/javascript/mastodon/features/account_timeline/index.js +++ b/app/javascript/mastodon/features/account_timeline/index.js @@ -44,6 +44,7 @@ class AccountTimeline extends ImmutablePureComponent { withReplies: PropTypes.bool, blockedBy: PropTypes.bool, isAccount: PropTypes.bool, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -77,7 +78,7 @@ class AccountTimeline extends ImmutablePureComponent { } render () { - const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, isAccount } = this.props; + const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, isAccount, multiColumn } = this.props; if (!isAccount) { return ( @@ -112,6 +113,7 @@ class AccountTimeline extends ImmutablePureComponent { onLoadMore={this.handleLoadMore} shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} /> ); diff --git a/app/javascript/mastodon/features/blocks/index.js b/app/javascript/mastodon/features/blocks/index.js index 96a219c947..8fb0f051b3 100644 --- a/app/javascript/mastodon/features/blocks/index.js +++ b/app/javascript/mastodon/features/blocks/index.js @@ -32,6 +32,7 @@ class Blocks extends ImmutablePureComponent { accountIds: ImmutablePropTypes.list, hasMore: PropTypes.bool, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -43,7 +44,7 @@ class Blocks extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, accountIds, shouldUpdateScroll, hasMore } = this.props; + const { intl, accountIds, shouldUpdateScroll, hasMore, multiColumn } = this.props; if (!accountIds) { return ( @@ -64,6 +65,7 @@ class Blocks extends ImmutablePureComponent { hasMore={hasMore} shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {accountIds.map(id => diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js index 7d26c98b0d..2f6999f614 100644 --- a/app/javascript/mastodon/features/community_timeline/index.js +++ b/app/javascript/mastodon/features/community_timeline/index.js @@ -126,6 +126,7 @@ class CommunityTimeline extends React.PureComponent { onLoadMore={this.handleLoadMore} emptyMessage={} shouldUpdateScroll={shouldUpdateScroll} + bindToDocument={!multiColumn} /> ); diff --git a/app/javascript/mastodon/features/domain_blocks/index.js b/app/javascript/mastodon/features/domain_blocks/index.js index 7c075f5a5c..16e200b31e 100644 --- a/app/javascript/mastodon/features/domain_blocks/index.js +++ b/app/javascript/mastodon/features/domain_blocks/index.js @@ -33,6 +33,7 @@ class Blocks extends ImmutablePureComponent { hasMore: PropTypes.bool, domains: ImmutablePropTypes.orderedSet, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -44,7 +45,7 @@ class Blocks extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, domains, shouldUpdateScroll, hasMore } = this.props; + const { intl, domains, shouldUpdateScroll, hasMore, multiColumn } = this.props; if (!domains) { return ( @@ -65,6 +66,7 @@ class Blocks extends ImmutablePureComponent { hasMore={hasMore} shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {domains.map(domain => diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js index fa9401b90e..8c7b238696 100644 --- a/app/javascript/mastodon/features/favourited_statuses/index.js +++ b/app/javascript/mastodon/features/favourited_statuses/index.js @@ -95,6 +95,7 @@ class Favourites extends ImmutablePureComponent { onLoadMore={this.handleLoadMore} shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} /> ); diff --git a/app/javascript/mastodon/features/favourites/index.js b/app/javascript/mastodon/features/favourites/index.js index d1ac229a27..464f7aeb0a 100644 --- a/app/javascript/mastodon/features/favourites/index.js +++ b/app/javascript/mastodon/features/favourites/index.js @@ -23,6 +23,7 @@ class Favourites extends ImmutablePureComponent { dispatch: PropTypes.func.isRequired, shouldUpdateScroll: PropTypes.func, accountIds: ImmutablePropTypes.list, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -36,7 +37,7 @@ class Favourites extends ImmutablePureComponent { } render () { - const { shouldUpdateScroll, accountIds } = this.props; + const { shouldUpdateScroll, accountIds, multiColumn } = this.props; if (!accountIds) { return ( @@ -56,6 +57,7 @@ class Favourites extends ImmutablePureComponent { scrollKey='favourites' shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {accountIds.map(id => diff --git a/app/javascript/mastodon/features/follow_requests/index.js b/app/javascript/mastodon/features/follow_requests/index.js index 44624cb406..570cf57c8a 100644 --- a/app/javascript/mastodon/features/follow_requests/index.js +++ b/app/javascript/mastodon/features/follow_requests/index.js @@ -32,6 +32,7 @@ class FollowRequests extends ImmutablePureComponent { hasMore: PropTypes.bool, accountIds: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -43,7 +44,7 @@ class FollowRequests extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, shouldUpdateScroll, accountIds, hasMore } = this.props; + const { intl, shouldUpdateScroll, accountIds, hasMore, multiColumn } = this.props; if (!accountIds) { return ( @@ -64,6 +65,7 @@ class FollowRequests extends ImmutablePureComponent { hasMore={hasMore} shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {accountIds.map(id => diff --git a/app/javascript/mastodon/features/followers/index.js b/app/javascript/mastodon/features/followers/index.js index e3387e1be8..dce05bdc62 100644 --- a/app/javascript/mastodon/features/followers/index.js +++ b/app/javascript/mastodon/features/followers/index.js @@ -36,6 +36,7 @@ class Followers extends ImmutablePureComponent { hasMore: PropTypes.bool, blockedBy: PropTypes.bool, isAccount: PropTypes.bool, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -55,7 +56,7 @@ class Followers extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount } = this.props; + const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn } = this.props; if (!isAccount) { return ( @@ -87,6 +88,7 @@ class Followers extends ImmutablePureComponent { prepend={} alwaysPrepend emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {blockedBy ? [] : accountIds.map(id => diff --git a/app/javascript/mastodon/features/following/index.js b/app/javascript/mastodon/features/following/index.js index 3bf89fb2ba..d9f2ef0790 100644 --- a/app/javascript/mastodon/features/following/index.js +++ b/app/javascript/mastodon/features/following/index.js @@ -36,6 +36,7 @@ class Following extends ImmutablePureComponent { hasMore: PropTypes.bool, blockedBy: PropTypes.bool, isAccount: PropTypes.bool, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -55,7 +56,7 @@ class Following extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount } = this.props; + const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn } = this.props; if (!isAccount) { return ( @@ -87,6 +88,7 @@ class Following extends ImmutablePureComponent { prepend={} alwaysPrepend emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {blockedBy ? [] : accountIds.map(id => diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.js b/app/javascript/mastodon/features/hashtag_timeline/index.js index 0d3c97a648..c50f6a79ae 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/index.js +++ b/app/javascript/mastodon/features/hashtag_timeline/index.js @@ -157,6 +157,7 @@ class HashtagTimeline extends React.PureComponent { onLoadMore={this.handleLoadMore} emptyMessage={} shouldUpdateScroll={shouldUpdateScroll} + bindToDocument={!multiColumn} /> ); diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js index 097f91c16c..bf8ff117bb 100644 --- a/app/javascript/mastodon/features/home_timeline/index.js +++ b/app/javascript/mastodon/features/home_timeline/index.js @@ -119,6 +119,7 @@ class HomeTimeline extends React.PureComponent { timelineId='home' emptyMessage={ }} />} shouldUpdateScroll={shouldUpdateScroll} + bindToDocument={!multiColumn} /> ); diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js index 0db6d22282..844c93db1e 100644 --- a/app/javascript/mastodon/features/list_timeline/index.js +++ b/app/javascript/mastodon/features/list_timeline/index.js @@ -184,6 +184,7 @@ class ListTimeline extends React.PureComponent { onLoadMore={this.handleLoadMore} emptyMessage={} shouldUpdateScroll={shouldUpdateScroll} + bindToDocument={!multiColumn} /> ); diff --git a/app/javascript/mastodon/features/lists/index.js b/app/javascript/mastodon/features/lists/index.js index 015e21b687..a06e0b934f 100644 --- a/app/javascript/mastodon/features/lists/index.js +++ b/app/javascript/mastodon/features/lists/index.js @@ -40,6 +40,7 @@ class Lists extends ImmutablePureComponent { dispatch: PropTypes.func.isRequired, lists: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -47,7 +48,7 @@ class Lists extends ImmutablePureComponent { } render () { - const { intl, shouldUpdateScroll, lists } = this.props; + const { intl, shouldUpdateScroll, lists, multiColumn } = this.props; if (!lists) { return ( @@ -70,6 +71,7 @@ class Lists extends ImmutablePureComponent { shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} prepend={} + bindToDocument={!multiColumn} > {lists.map(list => diff --git a/app/javascript/mastodon/features/mutes/index.js b/app/javascript/mastodon/features/mutes/index.js index 4ed29a1ce6..57d8b9915f 100644 --- a/app/javascript/mastodon/features/mutes/index.js +++ b/app/javascript/mastodon/features/mutes/index.js @@ -32,6 +32,7 @@ class Mutes extends ImmutablePureComponent { hasMore: PropTypes.bool, accountIds: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -43,7 +44,7 @@ class Mutes extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { intl, shouldUpdateScroll, hasMore, accountIds } = this.props; + const { intl, shouldUpdateScroll, hasMore, accountIds, multiColumn } = this.props; if (!accountIds) { return ( @@ -64,6 +65,7 @@ class Mutes extends ImmutablePureComponent { hasMore={hasMore} shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {accountIds.map(id => diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index df4ad6f2aa..e708c4fcf1 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -191,6 +191,7 @@ class Notifications extends React.PureComponent { onScrollToTop={this.handleScrollToTop} onScroll={this.handleScroll} shouldUpdateScroll={shouldUpdateScroll} + bindToDocument={!multiColumn} > {scrollableContent} diff --git a/app/javascript/mastodon/features/pinned_statuses/index.js b/app/javascript/mastodon/features/pinned_statuses/index.js index 98cdbda3c4..64ebfc7ae5 100644 --- a/app/javascript/mastodon/features/pinned_statuses/index.js +++ b/app/javascript/mastodon/features/pinned_statuses/index.js @@ -28,6 +28,7 @@ class PinnedStatuses extends ImmutablePureComponent { statusIds: ImmutablePropTypes.list.isRequired, intl: PropTypes.object.isRequired, hasMore: PropTypes.bool.isRequired, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -43,7 +44,7 @@ class PinnedStatuses extends ImmutablePureComponent { } render () { - const { intl, shouldUpdateScroll, statusIds, hasMore } = this.props; + const { intl, shouldUpdateScroll, statusIds, hasMore, multiColumn } = this.props; return ( @@ -53,6 +54,7 @@ class PinnedStatuses extends ImmutablePureComponent { scrollKey='pinned_statuses' hasMore={hasMore} shouldUpdateScroll={shouldUpdateScroll} + bindToDocument={!multiColumn} /> ); diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js index 2b7d9c56f9..1edb303b84 100644 --- a/app/javascript/mastodon/features/public_timeline/index.js +++ b/app/javascript/mastodon/features/public_timeline/index.js @@ -126,6 +126,7 @@ class PublicTimeline extends React.PureComponent { scrollKey={`public_timeline-${columnId}`} emptyMessage={} shouldUpdateScroll={shouldUpdateScroll} + bindToDocument={!multiColumn} /> ); diff --git a/app/javascript/mastodon/features/reblogs/index.js b/app/javascript/mastodon/features/reblogs/index.js index c05d21c740..26f93ad1b2 100644 --- a/app/javascript/mastodon/features/reblogs/index.js +++ b/app/javascript/mastodon/features/reblogs/index.js @@ -23,6 +23,7 @@ class Reblogs extends ImmutablePureComponent { dispatch: PropTypes.func.isRequired, shouldUpdateScroll: PropTypes.func, accountIds: ImmutablePropTypes.list, + multiColumn: PropTypes.bool, }; componentWillMount () { @@ -36,7 +37,7 @@ class Reblogs extends ImmutablePureComponent { } render () { - const { shouldUpdateScroll, accountIds } = this.props; + const { shouldUpdateScroll, accountIds, multiColumn } = this.props; if (!accountIds) { return ( @@ -56,6 +57,7 @@ class Reblogs extends ImmutablePureComponent { scrollKey='reblogs' shouldUpdateScroll={shouldUpdateScroll} emptyMessage={emptyMessage} + bindToDocument={!multiColumn} > {accountIds.map(id => diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index cc2ab6c8ce..06f9e1bc4d 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -32,6 +32,28 @@ const MODAL_COMPONENTS = { 'LIST_ADDER':ListAdder, }; +let cachedScrollbarWidth = null; + +export const getScrollbarWidth = () => { + if (cachedScrollbarWidth !== null) { + return cachedScrollbarWidth; + } + + const outer = document.createElement('div'); + outer.style.visibility = 'hidden'; + outer.style.overflow = 'scroll'; + document.body.appendChild(outer); + + const inner = document.createElement('div'); + outer.appendChild(inner); + + const scrollbarWidth = outer.offsetWidth - inner.offsetWidth; + cachedScrollbarWidth = scrollbarWidth; + outer.parentNode.removeChild(outer); + + return scrollbarWidth; +}; + export default class ModalRoot extends React.PureComponent { static propTypes = { @@ -47,8 +69,10 @@ export default class ModalRoot extends React.PureComponent { componentDidUpdate (prevProps, prevState, { visible }) { if (visible) { document.body.classList.add('with-modals--active'); + document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; } else { document.body.classList.remove('with-modals--active'); + document.documentElement.style.marginRight = 0; } } diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 791133afd3..d1a3dc9495 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -110,12 +110,25 @@ class SwitchingColumnsArea extends React.PureComponent { componentWillMount () { window.addEventListener('resize', this.handleResize, { passive: true }); + + if (this.state.mobile || forceSingleColumn) { + document.body.classList.toggle('layout-single-column', true); + document.body.classList.toggle('layout-multiple-columns', false); + } else { + document.body.classList.toggle('layout-single-column', false); + document.body.classList.toggle('layout-multiple-columns', true); + } } - componentDidUpdate (prevProps) { + componentDidUpdate (prevProps, prevState) { if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) { this.node.handleChildrenContentChange(); } + + if (prevState.mobile !== this.state.mobile && !forceSingleColumn) { + document.body.classList.toggle('layout-single-column', this.state.mobile); + document.body.classList.toggle('layout-multiple-columns', !this.state.mobile); + } } componentWillUnmount () { diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index 2924413855..0c60d828e9 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -108,14 +108,6 @@ function main() { if (parallaxComponents.length > 0 ) { new Rellax('.parallax', { speed: -1 }); } - - if (document.body.classList.contains('with-modals')) { - const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; - const scrollbarWidthStyle = document.createElement('style'); - scrollbarWidthStyle.id = 'scrollbar-width'; - document.head.appendChild(scrollbarWidthStyle); - scrollbarWidthStyle.sheet.insertRule(`body.with-modals--active { margin-right: ${scrollbarWidth}px; }`, 0); - } }); delegate(document, '.webapp-btn', 'click', ({ target, button }) => { diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss index b5a77ce94f..7df76bdff6 100644 --- a/app/javascript/styles/mastodon/basics.scss +++ b/app/javascript/styles/mastodon/basics.scss @@ -8,7 +8,7 @@ body { font-family: $font-sans-serif, sans-serif; - background: darken($ui-base-color, 8%); + background: darken($ui-base-color, 7%); font-size: 13px; line-height: 18px; font-weight: 400; @@ -35,11 +35,19 @@ body { } &.app-body { - position: absolute; - width: 100%; - height: 100%; padding: 0; - background: $ui-base-color; + + &.layout-single-column { + height: auto; + min-height: 100%; + overflow-y: scroll; + } + + &.layout-multiple-columns { + position: absolute; + width: 100%; + height: 100%; + } &.with-modals--active { overflow-y: hidden; @@ -56,7 +64,6 @@ body { &--active { overflow-y: hidden; - margin-right: 13px; } } @@ -134,9 +141,22 @@ button { & > div { display: flex; width: 100%; - height: 100%; align-items: center; justify-content: center; outline: 0 !important; } } + +.layout-single-column .app-holder { + &, + & > div { + min-height: 100%; + } +} + +.layout-multiple-columns .app-holder { + &, + & > div { + height: 100%; + } +} diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index e413b00131..4eb4e78d63 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1804,6 +1804,7 @@ a.account__display-name { justify-content: center; width: 100%; height: 100%; + min-height: 100vh; &__pane { height: 100%; @@ -1817,6 +1818,7 @@ a.account__display-name { } &__inner { + position: fixed; width: 285px; pointer-events: auto; height: 100%; @@ -1871,7 +1873,6 @@ a.account__display-name { flex-direction: column; width: 100%; height: 100%; - background: darken($ui-base-color, 7%); } .drawer { @@ -2012,6 +2013,10 @@ a.account__display-name { top: 15px; } + .scrollable { + overflow: visible; + } + @media screen and (min-width: $no-gap-breakpoint) { padding: 10px 0; } From 6867a0beb5e1d48eba6d8962f5b0a0e17ba09ba8 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sat, 20 Jul 2019 01:08:02 +0900 Subject: [PATCH 65/71] Bump strong_migrations from 0.4.0 to 0.4.1 (#11307) Bumps [strong_migrations](https://github.com/ankane/strong_migrations) from 0.4.0 to 0.4.1. - [Release notes](https://github.com/ankane/strong_migrations/releases) - [Changelog](https://github.com/ankane/strong_migrations/blob/master/CHANGELOG.md) - [Commits](https://github.com/ankane/strong_migrations/compare/v0.4.0...v0.4.1) Signed-off-by: dependabot-preview[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 7be1679b35..340537ac90 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -593,7 +593,7 @@ GEM stoplight (2.1.3) streamio-ffmpeg (3.0.2) multi_json (~> 1.8) - strong_migrations (0.4.0) + strong_migrations (0.4.1) activerecord (>= 5) temple (0.8.1) terminal-table (1.8.0) From bf3ab44e7788264783d58f89721539183ecddb4d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 7 Jul 2019 16:16:51 +0200 Subject: [PATCH 66/71] Remove Atom feeds and old URLs in the form of `GET /:username/updates/:id` Port SCSS changes from b8514561394767a10d3cf40132ada24d938c1680 to glitch-soc Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/styles/index.scss | 2 +- .../glitch/styles/{stream_entries.scss => statuses.scss} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename app/javascript/flavours/glitch/styles/{stream_entries.scss => statuses.scss} (100%) diff --git a/app/javascript/flavours/glitch/styles/index.scss b/app/javascript/flavours/glitch/styles/index.scss index 323b2e7fee..af73feb899 100644 --- a/app/javascript/flavours/glitch/styles/index.scss +++ b/app/javascript/flavours/glitch/styles/index.scss @@ -14,7 +14,7 @@ @import 'widgets'; @import 'forms'; @import 'accounts'; -@import 'stream_entries'; +@import 'statuses'; @import 'components/index'; @import 'polls'; @import 'about'; diff --git a/app/javascript/flavours/glitch/styles/stream_entries.scss b/app/javascript/flavours/glitch/styles/statuses.scss similarity index 100% rename from app/javascript/flavours/glitch/styles/stream_entries.scss rename to app/javascript/flavours/glitch/styles/statuses.scss From 490cf3aa034ce89c50683ae2eb57339415038f35 Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 19 Jul 2019 01:44:42 +0200 Subject: [PATCH 67/71] [Glitch] Add ActivityPub actor representing the entire server Port SCSS changes from 730c4053d642024b9949d72c8a9f1873532c6212 Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/styles/containers.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss index dc60dd14bf..130e1461cc 100644 --- a/app/javascript/flavours/glitch/styles/containers.scss +++ b/app/javascript/flavours/glitch/styles/containers.scss @@ -147,6 +147,10 @@ min-height: 100%; } + .flash-message { + margin-bottom: 10px; + } + @media screen and (max-width: 738px) { grid-template-columns: minmax(0, 50%) minmax(0, 50%); From 3407ae8683b36fd3514aa518b5b1634d0e88d0c7 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Fri, 19 Jul 2019 19:02:05 +0200 Subject: [PATCH 68/71] Fix sanitizer text case for glitch-soc, which preserves lists --- spec/lib/sanitize_config_spec.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spec/lib/sanitize_config_spec.rb b/spec/lib/sanitize_config_spec.rb index faefac803f..c5143bcefa 100644 --- a/spec/lib/sanitize_config_spec.rb +++ b/spec/lib/sanitize_config_spec.rb @@ -14,9 +14,5 @@ describe Sanitize::Config do it 'keeps ul' do expect(Sanitize.fragment('

    Check out:

    • Foo
    • Bar
    ', subject)).to eq '

    Check out:

    • Foo
    • Bar
    ' end - - it 'keep links in lists' do - expect(Sanitize.fragment('

    Check out:

    ', subject)).to eq '

    Check out:

    joinmastodon.org
    Bar

    ' - end end end From 86d446194823a416a99c5b4657f4684fb0e07ae9 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Fri, 19 Jul 2019 20:15:03 +0200 Subject: [PATCH 69/71] =?UTF-8?q?Fix=20HTML=20entities=20being=20encoded?= =?UTF-8?q?=20as=20hashtag=20links=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/lib/formatter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 113b5c4a08..85bc8eb1f9 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -314,7 +314,7 @@ class Formatter gaps = [] total_offset = 0 - escaped = html.gsub(/<[^>]*>/) do |match| + escaped = html.gsub(/<[^>]*>|&#[0-9]+;/) do |match| total_offset += match.length - 1 end_offset = Regexp.last_match.end(0) gaps << [end_offset - total_offset, total_offset] From 650459f93c6b599c1041ff87f8a78da7fb05cb9d Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 19 Jul 2019 23:13:21 +0200 Subject: [PATCH 70/71] Fix some flash notices/alerts staying on unrelated pages (#11364) --- app/controllers/admin/domain_blocks_controller.rb | 2 +- .../two_factor_authentication/confirmations_controller.rb | 2 +- .../two_factor_authentication/recovery_codes_controller.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 377cac8adc..7129656dab 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -17,7 +17,7 @@ module Admin if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) @domain_block.save - flash[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety + flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety @domain_block.errors[:domain].clear render :new else diff --git a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb index d87117a50a..02652a36c9 100644 --- a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb +++ b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb @@ -14,7 +14,7 @@ module Settings def create if current_user.validate_and_consume_otp!(confirmation_params[:code]) - flash[:notice] = I18n.t('two_factor_authentication.enabled_success') + flash.now[:notice] = I18n.t('two_factor_authentication.enabled_success') current_user.otp_required_for_login = true @recovery_codes = current_user.generate_otp_backup_codes! diff --git a/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb b/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb index c78166c654..874bf532ba 100644 --- a/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb +++ b/app/controllers/settings/two_factor_authentication/recovery_codes_controller.rb @@ -10,7 +10,7 @@ module Settings def create @recovery_codes = current_user.generate_otp_backup_codes! current_user.save! - flash[:notice] = I18n.t('two_factor_authentication.recovery_codes_regenerated') + flash.now[:notice] = I18n.t('two_factor_authentication.recovery_codes_regenerated') render :index end end From c37c1da41e1c490771a409ad1b12f3db55af4cee Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 19 Jul 2019 23:22:35 +0200 Subject: [PATCH 71/71] Disallow numeric-only hashtags (#11363) * Add spec covering numeric-only hashtags * Fix hashtag regex --- app/models/tag.rb | 4 ++-- spec/models/tag_spec.rb | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/models/tag.rb b/app/models/tag.rb index 01bace2bbf..b371d59c1b 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -17,10 +17,10 @@ class Tag < ApplicationRecord has_many :featured_tags, dependent: :destroy, inverse_of: :tag has_one :account_tag_stat, dependent: :destroy - HASHTAG_NAME_RE = '[[:word:]_][[:word:]_]*[[:alpha:]_·]*[[:word:]_·]*[[:word:]_]' + HASHTAG_NAME_RE = '([[:word:]_][[:word:]_·]*[[:alpha:]_·][[:word:]_·]*[[:word:]_])|([[:word:]_]*[[:alpha:]][[:word:]_]*)' HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i - validates :name, presence: true, uniqueness: true, format: { with: /\A#{HASHTAG_NAME_RE}\z/i } + validates :name, presence: true, uniqueness: true, format: { with: /\A(#{HASHTAG_NAME_RE})\z/i } scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(Arel.sql('account_tag_stats.accounts_count desc')) } scope :hidden, -> { where(account_tag_stats: { hidden: true }) } diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index 1618623926..9a30ceaa52 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -69,6 +69,10 @@ RSpec.describe Tag, type: :model do it 'does not match middle dots at the end' do expect(subject.match('hello #one·two·three·').to_s).to eq ' #one·two·three' end + + it 'does not match purely-numeric hashtags' do + expect(subject.match('hello #0123456')).to be_nil + end end describe '#to_param' do