From 915c619394165a114a4ab316165aecac3386cf2f Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 26 Jun 2019 19:32:36 +0200 Subject: Add support for Audio activities (#11189) Fixes #11127 --- .../fetch_remote_status_service_spec.rb | 33 ++++++++++++++++++++++ 1 file changed, 33 insertions(+) (limited to 'spec/services') diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb index 9ae409996..78dd59e3b 100644 --- a/spec/services/activitypub/fetch_remote_status_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb @@ -71,6 +71,39 @@ RSpec.describe ActivityPub::FetchRemoteStatusService, type: :service do end end + context 'with Audio object' do + let(:object) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: "https://#{valid_domain}/@foo/1234", + type: 'Audio', + name: 'Nyan Cat 10 hours remix', + attributedTo: ActivityPub::TagManager.instance.uri_for(sender), + url: [ + { + type: 'Link', + mimeType: 'application/x-bittorrent', + href: "https://#{valid_domain}/12345.torrent", + }, + + { + type: 'Link', + mimeType: 'text/html', + href: "https://#{valid_domain}/watch?v=12345", + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.url).to eq "https://#{valid_domain}/watch?v=12345" + expect(strip_tags(status.text)).to eq "Nyan Cat 10 hours remix https://#{valid_domain}/watch?v=12345" + end + end + context 'with wrong id' do let(:note) do { -- cgit From 23aeef52cc4540b4514e9f3b935b21f0530a3746 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 6 Jul 2019 23:26:16 +0200 Subject: 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 --- app/controllers/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 --- app/controllers/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 -- app/services/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 - app/services/pubsubhubbub/subscribe_service.rb | 53 ----- app/services/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 | 131 +---------- 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 - .../admin/subscriptions/_subscription.html.haml | 18 -- app/views/admin/subscriptions/index.html.haml | 16 -- app/views/well_known/webfinger/show.xml.ruby | 5 - app/workers/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 +- app/workers/pubsubhubbub/confirmation_worker.rb | 75 +----- app/workers/pubsubhubbub/delivery_worker.rb | 74 +----- app/workers/pubsubhubbub/distribution_worker.rb | 25 +- .../pubsubhubbub/raw_distribution_worker.rb | 15 +- app/workers/pubsubhubbub/subscribe_worker.rb | 27 +-- app/workers/pubsubhubbub/unsubscribe_worker.rb | 8 +- app/workers/remote_profile_update_worker.rb | 6 +- app/workers/salmon_worker.rb | 6 +- app/workers/scheduler/subscriptions_scheduler.rb | 10 +- config/locales/en.yml | 7 - config/navigation.rb | 1 - config/routes.rb | 14 -- config/sidekiq.yml | 3 - spec/controllers/admin/accounts_controller_spec.rb | 38 ---- .../admin/subscriptions_controller_spec.rb | 32 --- spec/controllers/api/push_controller_spec.rb | 59 ----- spec/controllers/api/salmon_controller_spec.rb | 65 ------ .../api/subscriptions_controller_spec.rb | 68 ------ spec/controllers/api/v1/follows_controller_spec.rb | 51 ----- spec/fixtures/requests/webfinger.txt | 2 +- spec/lib/ostatus/atom_serializer_spec.rb | 145 ------------ spec/services/authorize_follow_service_spec.rb | 7 - .../services/batched_remove_status_service_spec.rb | 13 -- spec/services/block_service_spec.rb | 7 - spec/services/favourite_service_spec.rb | 7 - spec/services/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 --------------------- spec/services/process_interaction_service_spec.rb | 151 ------------ spec/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 +------ spec/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 --- .../services/update_remote_profile_service_spec.rb | 84 ------- .../after_remote_follow_request_worker_spec.rb | 59 ----- spec/workers/after_remote_follow_worker_spec.rb | 59 ----- .../pubsubhubbub/confirmation_worker_spec.rb | 88 ------- spec/workers/pubsubhubbub/delivery_worker_spec.rb | 68 ------ .../pubsubhubbub/distribution_worker_spec.rb | 46 ---- .../scheduler/subscriptions_scheduler_spec.rb | 19 -- 102 files changed, 69 insertions(+), 3568 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 (limited to 'spec/services') diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index a0b7532c2..e2cd8eaed 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 0c7760d77..2fa1dfe5f 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 e04d19125..000000000 --- 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 ac5f3268d..000000000 --- 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 89007f3d6..000000000 --- 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 5420c0533..000000000 --- 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 db70f1998..000000000 --- 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 60de712db..000000000 --- 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 c98f5ee0a..000000000 --- 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 8a6aabc33..000000000 --- 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 755ed8656..000000000 --- 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 5b204b6d8..000000000 --- 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 5ca601415..000000000 --- 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 9a05d96cf..f5c0e85ca 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 c588451fc..d6772eb98 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 8c0b07702..4220f697e 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 29b8700c7..49bef727e 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 e328b1739..cb66debc8 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 c6eef04d4..c5e5e5761 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 9050a4858..da06361c2 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 c2419e9ec..000000000 --- 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 9f6c8a082..000000000 --- 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 128a24ad6..02b26458a 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 cfc560022..a7f95603d 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 9c3008035..aac39dfd5 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 0305e2d62..8e118f5d3 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 7830aee11..34ec6d504 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 30a9dd85e..000000000 --- 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 1fca3832b..000000000 --- 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 bc607dff3..da52bff6a 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 550da6328..000000000 --- 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 646150f7b..000000000 --- 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 9cf4bc128..3bb460fca 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 f87d0ba91..bc0000c8c 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 81adc5aae..a8c9100b3 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 e557706da..0ea31a0d8 100644 --- a/app/services/resolve_account_service.rb +++ b/app/services/resolve_account_service.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true +require_relative '../models/account' + class ResolveAccountService < BaseService - include OStatus2::MagicKey include JsonLdHelper - DFRN_NS = 'http://purl.org/macgirvin/dfrn/1.0' - # 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 @@ -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 3419043e5..000000000 --- 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 83fd64396..000000000 --- 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 95a858e9f..c263ac8af 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 dcc890b7d..37917a64f 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 17dc29735..b7033d7eb 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 95c1fb4fc..000000000 --- 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 403395a0d..000000000 --- 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 205b35d8b..000000000 --- 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 950e61847..de7d2a8ba 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 1dec8e396..000000000 --- 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 83704c8ee..000000000 --- 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 968c8c138..c82cdb7b3 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 84eb6ade2..ce9c65834 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 edab83f85..d9719f2bf 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 da1d6ab45..1c0f001cf 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 978c3aba2..cf3bd8397 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 c0e7b677e..783a8c95f 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 619bfa48a..1260060bd 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 fed5e917d..75bac5d6f 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 16962a623..ece9c80ac 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 2e176d1c1..b861b5e67 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 a271715b7..0c1c263f6 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 03585ad2d..01e8daf8f 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 d37d40432..10200b06c 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 d5873bccb..6903cadc7 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 d4f1855aa..611f36fdd 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 df1024189..ef845d1fc 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 9ab5ba7f0..4b6d464c6 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 0ec1742ab..a16dea967 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 a348ab3d7..608606ff9 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 967152abe..000000000 --- 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 d769d8554..000000000 --- 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 235a29af0..000000000 --- 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 7a4252fe6..000000000 --- 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 089e0fe5e..000000000 --- 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 edb8a2dbb..f337ecae6 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 891871c1c..74ab7576f 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 562ef0041..ce56d57a6 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 e53623449..d52e7f484 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 6584bb90e..de20dd026 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 0a20ccf6e..4c29ea77b 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 3cd86708b..37e9910d4 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 3c4ec59be..86c85293e 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 5cf2dadf0..5355133f4 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 facbe977f..bf06f50e9 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 9d3465f3f..000000000 --- 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 b858c19d0..000000000 --- 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 8a6bb44ac..35a804f2b 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 01c956230..000000000 --- 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 7ed9fc5af..000000000 --- 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 9d84c41d5..58fb46f0f 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 e5ac37ed9..1aec060db 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 7bba83a60..48191d47c 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 27a85af7c..7a64f4161 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 710d8184c..000000000 --- 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 10bdb1ba8..000000000 --- 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 5835b912b..6350c6834 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 8a2881ab1..84b5dafbc 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 54d4b1b53..000000000 --- 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 f3ea70b80..000000000 --- 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 bd623cca5..000000000 --- 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 d93c469f9..000000000 --- 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 1eecdd2b5..000000000 --- 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 c0e0d5186..000000000 --- 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 584485079..000000000 --- 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 a7d1046de..000000000 --- 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 -- cgit From b8514561394767a10d3cf40132ada24d938c1680 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 7 Jul 2019 16:16:51 +0200 Subject: 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 +- app/helpers/statuses_helper.rb | 222 +++ app/helpers/stream_entries_helper.rb | 220 --- app/javascript/styles/application.scss | 2 +- app/javascript/styles/mastodon/statuses.scss | 163 +++ app/javascript/styles/mastodon/stream_entries.scss | 163 --- 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 +- app/views/statuses/_attachment_list.html.haml | 8 + app/views/statuses/_detailed_status.html.haml | 79 ++ app/views/statuses/_og_description.html.haml | 4 + app/views/statuses/_og_image.html.haml | 38 + app/views/statuses/_poll.html.haml | 27 + app/views/statuses/_simple_status.html.haml | 60 + app/views/statuses/_status.html.haml | 62 + app/views/statuses/embed.html.haml | 3 + app/views/statuses/show.html.haml | 24 + .../stream_entries/_attachment_list.html.haml | 8 - .../stream_entries/_detailed_status.html.haml | 79 -- app/views/stream_entries/_og_description.html.haml | 4 - app/views/stream_entries/_og_image.html.haml | 38 - app/views/stream_entries/_poll.html.haml | 27 - app/views/stream_entries/_simple_status.html.haml | 60 - app/views/stream_entries/_status.html.haml | 62 - 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 - spec/controllers/api/oembed_controller_spec.rb | 2 +- spec/controllers/application_controller_spec.rb | 4 - spec/controllers/statuses_controller_spec.rb | 16 +- spec/controllers/stream_entries_controller_spec.rb | 95 -- spec/fabricators/stream_entry_fabricator.rb | 5 - .../admin/account_moderation_notes_helper_spec.rb | 2 +- spec/helpers/statuses_helper_spec.rb | 224 ++++ spec/helpers/stream_entries_helper_spec.rb | 224 ---- 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 --- spec/services/process_mentions_service_spec.rb | 4 +- spec/services/suspend_account_service_spec.rb | 6 +- spec/views/statuses/show.html.haml_spec.rb | 81 ++ spec/views/stream_entries/show.html.haml_spec.rb | 88 -- 81 files changed, 1071 insertions(+), 3732 deletions(-) delete mode 100644 app/controllers/stream_entries_controller.rb create mode 100644 app/helpers/statuses_helper.rb delete mode 100644 app/helpers/stream_entries_helper.rb create mode 100644 app/javascript/styles/mastodon/statuses.scss delete mode 100644 app/javascript/styles/mastodon/stream_entries.scss 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 create mode 100644 app/views/statuses/_attachment_list.html.haml create mode 100644 app/views/statuses/_detailed_status.html.haml create mode 100644 app/views/statuses/_og_description.html.haml create mode 100644 app/views/statuses/_og_image.html.haml create mode 100644 app/views/statuses/_poll.html.haml create mode 100644 app/views/statuses/_simple_status.html.haml create mode 100644 app/views/statuses/_status.html.haml 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/_attachment_list.html.haml delete mode 100644 app/views/stream_entries/_detailed_status.html.haml delete mode 100644 app/views/stream_entries/_og_description.html.haml delete mode 100644 app/views/stream_entries/_og_image.html.haml delete mode 100644 app/views/stream_entries/_poll.html.haml delete mode 100644 app/views/stream_entries/_simple_status.html.haml delete mode 100644 app/views/stream_entries/_status.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 create mode 100644 spec/helpers/statuses_helper_spec.rb delete mode 100644 spec/helpers/stream_entries_helper_spec.rb 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 create mode 100644 spec/views/statuses/show.html.haml_spec.rb delete mode 100644 spec/views/stream_entries/show.html.haml_spec.rb (limited to 'spec/services') diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 73a4b1859..065707378 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 ef26691b2..776099ca8 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 0f7e9e0f5..000000000 --- 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 e5fbb1500..1daa60774 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 df60b7dd7..b66e827fe 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/statuses_helper.rb b/app/helpers/statuses_helper.rb new file mode 100644 index 000000000..e067380f6 --- /dev/null +++ b/app/helpers/statuses_helper.rb @@ -0,0 +1,222 @@ +# frozen_string_literal: true + +module StatusesHelper + EMBEDDED_CONTROLLER = 'statuses' + EMBEDDED_ACTION = 'embed' + + def display_name(account, **options) + if options[:custom_emojify] + Formatter.instance.format_display_name(account, options) + else + account.display_name.presence || account.username + end + end + + def account_action_button(account) + if user_signed_in? + if account.id == current_user.account_id + link_to settings_profile_url, class: 'button logo-button' do + safe_join([svg_logo, t('settings.edit_profile')]) + end + elsif current_account.following?(account) || current_account.requested?(account) + link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do + safe_join([svg_logo, t('accounts.unfollow')]) + end + elsif !(account.memorial? || account.moved?) + link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do + safe_join([svg_logo, t('accounts.follow')]) + end + end + elsif !(account.memorial? || account.moved?) + link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do + safe_join([svg_logo, t('accounts.follow')]) + end + end + end + + def svg_logo + content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976') + end + + def svg_logo_full + content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678') + end + + def account_badge(account, all: false) + if account.bot? + content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles') + elsif (Setting.show_staff_badge && account.user_staff?) || all + content_tag(:div, class: 'roles') do + if all && !account.user_staff? + content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role') + elsif account.user_admin? + content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin') + elsif account.user_moderator? + content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator') + end + end + end + end + + def link_to_more(url) + link_to t('statuses.show_more'), url, class: 'load-more load-gap' + end + + def nothing_here(extra_classes = '') + content_tag(:div, class: "nothing-here #{extra_classes}") do + t('accounts.nothing_here') + end + end + + def account_description(account) + prepend_str = [ + [ + number_to_human(account.statuses_count, strip_insignificant_zeros: true), + I18n.t('accounts.posts', count: account.statuses_count), + ].join(' '), + + [ + number_to_human(account.following_count, strip_insignificant_zeros: true), + I18n.t('accounts.following', count: account.following_count), + ].join(' '), + + [ + number_to_human(account.followers_count, strip_insignificant_zeros: true), + I18n.t('accounts.followers', count: account.followers_count), + ].join(' '), + ].join(', ') + + [prepend_str, account.note].join(' · ') + end + + def media_summary(status) + attachments = { image: 0, video: 0 } + + status.media_attachments.each do |media| + if media.video? + attachments[:video] += 1 + else + attachments[:image] += 1 + end + end + + text = attachments.to_a.reject { |_, value| value.zero? }.map { |key, value| I18n.t("statuses.attached.#{key}", count: value) }.join(' · ') + + return if text.blank? + + I18n.t('statuses.attached.description', attached: text) + end + + 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 + + def status_description(status) + components = [[media_summary(status), status_text_summary(status)].reject(&:blank?).join(' · ')] + + if status.spoiler_text.blank? + components << status.text + components << poll_summary(status) + end + + components.reject(&:blank?).join("\n\n") + end + + def stream_link_target + embedded_view? ? '_blank' : nil + end + + def acct(account) + if account.local? + "@#{account.acct}@#{Rails.configuration.x.local_domain}" + else + "@#{account.acct}" + end + end + + def style_classes(status, is_predecessor, is_successor, include_threads) + classes = ['entry'] + classes << 'entry-predecessor' if is_predecessor + classes << 'entry-reblog' if status.reblog? + classes << 'entry-successor' if is_successor + classes << 'entry-center' if include_threads + classes.join(' ') + end + + def microformats_classes(status, is_direct_parent, is_direct_child) + classes = [] + classes << 'p-in-reply-to' if is_direct_parent + classes << 'p-repost-of' if status.reblog? && is_direct_parent + classes << 'p-comment' if is_direct_child + classes.join(' ') + end + + def microformats_h_class(status, is_predecessor, is_successor, include_threads) + if is_predecessor || status.reblog? || is_successor + 'h-cite' + elsif include_threads + '' + else + 'h-entry' + end + end + + def rtl_status?(status) + status.local? ? rtl?(status.text) : rtl?(strip_tags(status.text)) + end + + def rtl?(text) + text = simplified_text(text) + rtl_words = text.scan(/[\p{Hebrew}\p{Arabic}\p{Syriac}\p{Thaana}\p{Nko}]+/m) + + if rtl_words.present? + total_size = text.size.to_f + rtl_size(rtl_words) / total_size > 0.3 + else + false + end + end + + def fa_visibility_icon(status) + case status.visibility + when 'public' + fa_icon 'globe fw' + when 'unlisted' + fa_icon 'unlock fw' + when 'private' + fa_icon 'lock fw' + when 'direct' + fa_icon 'envelope fw' + end + end + + private + + def simplified_text(text) + text.dup.tap do |new_text| + URI.extract(new_text).each do |url| + new_text.gsub!(url, '') + end + + new_text.gsub!(Account::MENTION_RE, '') + new_text.gsub!(Tag::HASHTAG_RE, '') + new_text.gsub!(/\s+/, '') + end + end + + def rtl_size(words) + words.reduce(0) { |acc, elem| acc + elem.size }.to_f + end + + def embedded_view? + params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION + end +end diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb deleted file mode 100644 index 02a860a74..000000000 --- a/app/helpers/stream_entries_helper.rb +++ /dev/null @@ -1,220 +0,0 @@ -# frozen_string_literal: true - -module StreamEntriesHelper - EMBEDDED_CONTROLLER = 'statuses' - EMBEDDED_ACTION = 'embed' - - def display_name(account, **options) - if options[:custom_emojify] - Formatter.instance.format_display_name(account, options) - else - account.display_name.presence || account.username - end - end - - def account_action_button(account) - if user_signed_in? - if account.id == current_user.account_id - link_to settings_profile_url, class: 'button logo-button' do - safe_join([svg_logo, t('settings.edit_profile')]) - end - elsif current_account.following?(account) || current_account.requested?(account) - link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do - safe_join([svg_logo, t('accounts.unfollow')]) - end - elsif !(account.memorial? || account.moved?) - link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do - safe_join([svg_logo, t('accounts.follow')]) - end - end - elsif !(account.memorial? || account.moved?) - link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do - safe_join([svg_logo, t('accounts.follow')]) - end - end - end - - def svg_logo - content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976') - end - - def svg_logo_full - content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678') - end - - def account_badge(account, all: false) - if account.bot? - content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles') - elsif (Setting.show_staff_badge && account.user_staff?) || all - content_tag(:div, class: 'roles') do - if all && !account.user_staff? - content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role') - elsif account.user_admin? - content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin') - elsif account.user_moderator? - content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator') - end - end - end - end - - def link_to_more(url) - link_to t('statuses.show_more'), url, class: 'load-more load-gap' - end - - def nothing_here(extra_classes = '') - content_tag(:div, class: "nothing-here #{extra_classes}") do - t('accounts.nothing_here') - end - end - - def account_description(account) - prepend_str = [ - [ - number_to_human(account.statuses_count, strip_insignificant_zeros: true), - I18n.t('accounts.posts', count: account.statuses_count), - ].join(' '), - - [ - number_to_human(account.following_count, strip_insignificant_zeros: true), - I18n.t('accounts.following', count: account.following_count), - ].join(' '), - - [ - number_to_human(account.followers_count, strip_insignificant_zeros: true), - I18n.t('accounts.followers', count: account.followers_count), - ].join(' '), - ].join(', ') - - [prepend_str, account.note].join(' · ') - end - - def media_summary(status) - attachments = { image: 0, video: 0 } - - status.media_attachments.each do |media| - if media.video? - attachments[:video] += 1 - else - attachments[:image] += 1 - end - end - - text = attachments.to_a.reject { |_, value| value.zero? }.map { |key, value| I18n.t("statuses.attached.#{key}", count: value) }.join(' · ') - - return if text.blank? - - I18n.t('statuses.attached.description', attached: text) - end - - 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 - - def status_description(status) - components = [[media_summary(status), status_text_summary(status)].reject(&:blank?).join(' · ')] - - if status.spoiler_text.blank? - components << status.text - components << poll_summary(status) - end - - components.reject(&:blank?).join("\n\n") - end - - def stream_link_target - embedded_view? ? '_blank' : nil - end - - def acct(account) - if account.local? - "@#{account.acct}@#{Rails.configuration.x.local_domain}" - else - "@#{account.acct}" - end - end - - def style_classes(status, is_predecessor, is_successor, include_threads) - classes = ['entry'] - classes << 'entry-predecessor' if is_predecessor - classes << 'entry-reblog' if status.reblog? - classes << 'entry-successor' if is_successor - classes << 'entry-center' if include_threads - classes.join(' ') - end - - def microformats_classes(status, is_direct_parent, is_direct_child) - classes = [] - classes << 'p-in-reply-to' if is_direct_parent - classes << 'p-repost-of' if status.reblog? && is_direct_parent - classes << 'p-comment' if is_direct_child - classes.join(' ') - end - - def microformats_h_class(status, is_predecessor, is_successor, include_threads) - if is_predecessor || status.reblog? || is_successor - 'h-cite' - elsif include_threads - '' - else - 'h-entry' - end - end - - def rtl_status?(status) - status.local? ? rtl?(status.text) : rtl?(strip_tags(status.text)) - end - - def rtl?(text) - text = simplified_text(text) - rtl_words = text.scan(/[\p{Hebrew}\p{Arabic}\p{Syriac}\p{Thaana}\p{Nko}]+/m) - - if rtl_words.present? - total_size = text.size.to_f - rtl_size(rtl_words) / total_size > 0.3 - else - false - end - end - - def fa_visibility_icon(status) - case status.visibility - when 'public' - fa_icon 'globe fw' - when 'unlisted' - fa_icon 'unlock fw' - when 'private' - fa_icon 'lock fw' - when 'direct' - fa_icon 'envelope fw' - end - end - - private - - def simplified_text(text) - text.dup.tap do |new_text| - URI.extract(new_text).each do |url| - new_text.gsub!(url, '') - end - - new_text.gsub!(Account::MENTION_RE, '') - new_text.gsub!(Tag::HASHTAG_RE, '') - new_text.gsub!(/\s+/, '') - end - end - - def rtl_size(words) - words.reduce(0) { |acc, elem| acc + elem.size }.to_f - end - - def embedded_view? - params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION - end -end diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index 6db3bc3dc..8ebc45b62 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/statuses.scss b/app/javascript/styles/mastodon/statuses.scss new file mode 100644 index 000000000..19ce0ab8f --- /dev/null +++ b/app/javascript/styles/mastodon/statuses.scss @@ -0,0 +1,163 @@ +.activity-stream { + box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); + border-radius: 4px; + overflow: hidden; + margin-bottom: 10px; + + &--under-tabs { + border-radius: 0 0 4px 4px; + } + + @media screen and (max-width: $no-gap-breakpoint) { + margin-bottom: 0; + border-radius: 0; + box-shadow: none; + } + + &--headless { + border-radius: 0; + margin: 0; + box-shadow: none; + + .detailed-status, + .status { + border-radius: 0 !important; + } + } + + div[data-component] { + width: 100%; + } + + .entry { + background: $ui-base-color; + + .detailed-status, + .status, + .load-more { + animation: none; + } + + &:last-child { + .detailed-status, + .status, + .load-more { + border-bottom: 0; + border-radius: 0 0 4px 4px; + } + } + + &:first-child { + .detailed-status, + .status, + .load-more { + border-radius: 4px 4px 0 0; + } + + &:last-child { + .detailed-status, + .status, + .load-more { + border-radius: 4px; + } + } + } + + @media screen and (max-width: 740px) { + .detailed-status, + .status, + .load-more { + border-radius: 0 !important; + } + } + } + + &--highlighted .entry { + background: lighten($ui-base-color, 8%); + } +} + +.button.logo-button { + flex: 0 auto; + font-size: 14px; + background: $ui-highlight-color; + color: $primary-text-color; + text-transform: none; + line-height: 36px; + height: auto; + padding: 3px 15px; + border: 0; + + svg { + width: 20px; + height: auto; + vertical-align: middle; + margin-right: 5px; + fill: $primary-text-color; + } + + &:active, + &:focus, + &:hover { + background: lighten($ui-highlight-color, 10%); + } + + &:disabled, + &.disabled { + &:active, + &:focus, + &:hover { + background: $ui-primary-color; + } + } + + &.button--destructive { + &:active, + &:focus, + &:hover { + background: $error-red; + } + } + + @media screen and (max-width: $no-gap-breakpoint) { + svg { + display: none; + } + } +} + +.embed, +.public-layout { + .detailed-status { + padding: 15px; + } + + .status { + padding: 15px 15px 15px (48px + 15px * 2); + min-height: 48px + 2px; + + &__avatar { + left: 15px; + top: 17px; + } + + &__content { + padding-top: 5px; + } + + &__prepend { + margin-left: 48px + 15px * 2; + padding-top: 15px; + } + + &__prepend-icon-wrapper { + left: -32px; + } + + .media-gallery, + &__action-bar, + .video-player { + margin-top: 10px; + } + } +} diff --git a/app/javascript/styles/mastodon/stream_entries.scss b/app/javascript/styles/mastodon/stream_entries.scss deleted file mode 100644 index 19ce0ab8f..000000000 --- a/app/javascript/styles/mastodon/stream_entries.scss +++ /dev/null @@ -1,163 +0,0 @@ -.activity-stream { - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - border-radius: 4px; - overflow: hidden; - margin-bottom: 10px; - - &--under-tabs { - border-radius: 0 0 4px 4px; - } - - @media screen and (max-width: $no-gap-breakpoint) { - margin-bottom: 0; - border-radius: 0; - box-shadow: none; - } - - &--headless { - border-radius: 0; - margin: 0; - box-shadow: none; - - .detailed-status, - .status { - border-radius: 0 !important; - } - } - - div[data-component] { - width: 100%; - } - - .entry { - background: $ui-base-color; - - .detailed-status, - .status, - .load-more { - animation: none; - } - - &:last-child { - .detailed-status, - .status, - .load-more { - border-bottom: 0; - border-radius: 0 0 4px 4px; - } - } - - &:first-child { - .detailed-status, - .status, - .load-more { - border-radius: 4px 4px 0 0; - } - - &:last-child { - .detailed-status, - .status, - .load-more { - border-radius: 4px; - } - } - } - - @media screen and (max-width: 740px) { - .detailed-status, - .status, - .load-more { - border-radius: 0 !important; - } - } - } - - &--highlighted .entry { - background: lighten($ui-base-color, 8%); - } -} - -.button.logo-button { - flex: 0 auto; - font-size: 14px; - background: $ui-highlight-color; - color: $primary-text-color; - text-transform: none; - line-height: 36px; - height: auto; - padding: 3px 15px; - border: 0; - - svg { - width: 20px; - height: auto; - vertical-align: middle; - margin-right: 5px; - fill: $primary-text-color; - } - - &:active, - &:focus, - &:hover { - background: lighten($ui-highlight-color, 10%); - } - - &:disabled, - &.disabled { - &:active, - &:focus, - &:hover { - background: $ui-primary-color; - } - } - - &.button--destructive { - &:active, - &:focus, - &:hover { - background: $error-red; - } - } - - @media screen and (max-width: $no-gap-breakpoint) { - svg { - display: none; - } - } -} - -.embed, -.public-layout { - .detailed-status { - padding: 15px; - } - - .status { - padding: 15px 15px 15px (48px + 15px * 2); - min-height: 48px + 2px; - - &__avatar { - left: 15px; - top: 17px; - } - - &__content { - padding-top: 5px; - } - - &__prepend { - margin-left: 48px + 15px * 2; - padding-top: 15px; - } - - &__prepend-icon-wrapper { - left: -32px; - } - - .media-gallery, - &__action-bar, - .video-player { - margin-top: 10px; - } - } -} diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 8a1aad41a..6c1239963 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 f5c0e85ca..000000000 --- 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 4d1aed297..22ced8bf8 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 fb364cb98..daf4f556b 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 db154cad5..9ab3e2bbd 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 66fa337c1..723d901fc 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 70855e054..0921e3252 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 7c9edb8ef..000000000 --- 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 742d2b56f..000000000 --- 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 2258e2d07..906756e85 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 1a9afc5c7..000000000 --- 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 12adc971c..272e3eb9c 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 c9b76cb16..2dc4a1b61 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 88eca79ed..278affe13 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 644380149..e8562ee87 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 cb66debc8..27dc460a6 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 494aaed75..75fbd0e8c 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 da52bff6a..90dca9740 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 a8c9100b3..6311971ff 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 bbdc0a595..f941b489a 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 a5ce3dbd9..0ebe0b562 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 7a777bfea..02fd7bf42 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 de7d2a8ba..0dc984dcc 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 eba3ad804..b057d3e42 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 b3c145120..9376db7ff 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 e6059b035..00254c40c 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 561c60137..dd71160e2 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 c8c08991f..2cc0fcb93 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 9abcfd37e..80ad3bae2 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 2a9c062e9..328f7c833 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/statuses/_attachment_list.html.haml b/app/views/statuses/_attachment_list.html.haml new file mode 100644 index 000000000..d9706f47b --- /dev/null +++ b/app/views/statuses/_attachment_list.html.haml @@ -0,0 +1,8 @@ +.attachment-list + .attachment-list__icon + = fa_icon 'link' + %ul.attachment-list__list + - attachments.each do |media| + %li + - url = media.remote_url.presence || media.file.url + = link_to File.basename(url), url, title: media.description diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml new file mode 100644 index 000000000..8686c2033 --- /dev/null +++ b/app/views/statuses/_detailed_status.html.haml @@ -0,0 +1,79 @@ +.detailed-status.detailed-status--flex + .p-author.h-card + = 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' + - else + = image_tag status.account.avatar_static_url, width: 48, height: 48, alt: '', class: 'account__avatar u-photo' + %span.display-name + %bdi + %strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: autoplay) + %span.display-name__account + = acct(status.account) + = fa_icon('lock') if status.account.locked? + + = account_action_button(status.account) + + .status__content.emojify< + - if status.spoiler_text? + %p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }< + %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}  + %button.status__content__spoiler-link= t('statuses.show_more') + .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" } + = 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: '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: '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: '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 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 + - if status.application.website.blank? + %strong.detailed-status__application= status.application.name + - else + = link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener' + · + = link_to remote_interaction_path(status, type: :reply), class: 'modal-button detailed-status__link' do + - if status.in_reply_to_id.nil? + = fa_icon('reply') + - else + = fa_icon('reply-all') + %span.detailed-status__reblogs>= number_to_human status.replies_count, strip_insignificant_zeros: true + = " " + · + - if status.direct_visibility? + %span.detailed-status__link< + = fa_icon('envelope') + - elsif status.private_visibility? || status.limited_visibility? + %span.detailed-status__link< + = fa_icon('lock') + - else + = link_to remote_interaction_path(status, type: :reblog), class: 'modal-button detailed-status__link' do + = fa_icon('retweet') + %span.detailed-status__reblogs>= number_to_human status.reblogs_count, strip_insignificant_zeros: true + = " " + · + = link_to remote_interaction_path(status, type: :favourite), class: 'modal-button detailed-status__link' do + = fa_icon('star') + %span.detailed-status__favorites>= number_to_human status.favourites_count, strip_insignificant_zeros: true + = " " + + - if user_signed_in? + · + = link_to t('statuses.open_in_web'), web_url("statuses/#{status.id}"), class: 'detailed-status__application', target: '_blank' diff --git a/app/views/statuses/_og_description.html.haml b/app/views/statuses/_og_description.html.haml new file mode 100644 index 000000000..a7b18424d --- /dev/null +++ b/app/views/statuses/_og_description.html.haml @@ -0,0 +1,4 @@ +- description = status_description(activity) + +%meta{ name: 'description', content: description }/ += opengraph 'og:description', description diff --git a/app/views/statuses/_og_image.html.haml b/app/views/statuses/_og_image.html.haml new file mode 100644 index 000000000..67f9274b6 --- /dev/null +++ b/app/views/statuses/_og_image.html.haml @@ -0,0 +1,38 @@ +- if activity.is_a?(Status) && (activity.non_sensitive_with_media? || (activity.with_media? && Setting.preview_sensitive_media)) + - player_card = false + - activity.media_attachments.each do |media| + - if media.image? + = opengraph 'og:image', full_asset_url(media.file.url(:original)) + = opengraph 'og:image:type', media.file_content_type + - unless media.file.meta.nil? + = opengraph 'og:image:width', media.file.meta.dig('original', 'width') + = opengraph 'og:image:height', media.file.meta.dig('original', 'height') + - if media.description.present? + = opengraph 'og:image:alt', media.description + - elsif media.video? || media.gifv? + - player_card = true + = opengraph 'og:image', full_asset_url(media.file.url(:small)) + = opengraph 'og:image:type', 'image/png' + - unless media.file.meta.nil? + = opengraph 'og:image:width', media.file.meta.dig('small', 'width') + = opengraph 'og:image:height', media.file.meta.dig('small', 'height') + = opengraph 'og:video', full_asset_url(media.file.url(:original)) + = opengraph 'og:video:secure_url', full_asset_url(media.file.url(:original)) + = opengraph 'og:video:type', media.file_content_type + = opengraph 'twitter:player', medium_player_url(media) + = opengraph 'twitter:player:stream', full_asset_url(media.file.url(:original)) + = opengraph 'twitter:player:stream:content_type', media.file_content_type + - unless media.file.meta.nil? + = opengraph 'og:video:width', media.file.meta.dig('original', 'width') + = opengraph 'og:video:height', media.file.meta.dig('original', 'height') + = opengraph 'twitter:player:width', media.file.meta.dig('original', 'width') + = opengraph 'twitter:player:height', media.file.meta.dig('original', 'height') + - if player_card + = opengraph 'twitter:card', 'player' + - else + = opengraph 'twitter:card', 'summary_large_image' +- else + = opengraph 'og:image', full_asset_url(account.avatar.url(:original)) + = opengraph 'og:image:width', '120' + = opengraph 'og:image:height','120' + = opengraph 'twitter:card', 'summary' diff --git a/app/views/statuses/_poll.html.haml b/app/views/statuses/_poll.html.haml new file mode 100644 index 000000000..ba34890df --- /dev/null +++ b/app/views/statuses/_poll.html.haml @@ -0,0 +1,27 @@ +- show_results = (user_signed_in? && poll.voted?(current_account)) || poll.expired? + +.poll + %ul + - poll.loaded_options.each do |option| + %li + - if show_results + - percent = poll.votes_count > 0 ? 100 * option.votes_count / poll.votes_count : 0 + %span.poll__chart{ style: "width: #{percent}%" } + + %label.poll__text>< + %span.poll__number= percent.round + = Formatter.instance.format_poll_option(status, option, autoplay: autoplay) + - else + %label.poll__text>< + %span.poll__input{ class: poll.multiple? ? 'checkbox' : nil}>< + = Formatter.instance.format_poll_option(status, option, autoplay: autoplay) + .poll__footer + - unless show_results + %button.button.button-secondary{ disabled: true } + = t('statuses.poll.vote') + + %span= t('statuses.poll.total_votes', count: poll.votes_count) + + - unless poll.expires_at.nil? + · + %span= l poll.expires_at diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml new file mode 100644 index 000000000..11220dfcb --- /dev/null +++ b/app/views/statuses/_simple_status.html.haml @@ -0,0 +1,60 @@ +.status + .status__info + = 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 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 + = image_tag status.account.avatar_original_url, width: 48, height: 48, alt: '', class: 'u-photo account__avatar' + - else + = image_tag status.account.avatar_static_url, width: 48, height: 48, alt: '', class: 'u-photo account__avatar' + %span.display-name + %bdi + %strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: autoplay) +   + %span.display-name__account + = acct(status.account) + = fa_icon('lock') if status.account.locked? + .status__content.emojify< + - if status.spoiler_text? + %p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }< + %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}  + %button.status__content__spoiler-link= t('statuses.show_more') + .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" } + = 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: '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: '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: '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 + + .status__action-bar + .status__action-bar__counter + = link_to remote_interaction_path(status, type: :reply), 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.in_reply_to_id.nil? + = fa_icon 'reply fw' + - else + = 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? + = fa_icon 'retweet fw' + - elsif status.private_visibility? + = fa_icon 'lock fw' + - else + = fa_icon 'envelope fw' + = link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do + = fa_icon 'star fw' diff --git a/app/views/statuses/_status.html.haml b/app/views/statuses/_status.html.haml new file mode 100644 index 000000000..0e3652503 --- /dev/null +++ b/app/views/statuses/_status.html.haml @@ -0,0 +1,62 @@ +:ruby + pinned ||= false + include_threads ||= false + is_predecessor ||= false + is_successor ||= false + direct_reply_id ||= false + parent_id ||= false + autoplay ||= current_account&.user&.setting_auto_play_gif + is_direct_parent = direct_reply_id == status.id + is_direct_child = parent_id == status.in_reply_to_id + centered ||= include_threads && !is_predecessor && !is_successor + h_class = microformats_h_class(status, is_predecessor, is_successor, include_threads) + style_classes = style_classes(status, is_predecessor, is_successor, include_threads) + mf_classes = microformats_classes(status, is_direct_parent, is_direct_child) + entry_classes = h_class + ' ' + mf_classes + ' ' + style_classes + +- if status.reply? && include_threads + - if @next_ancestor + .entry{ class: entry_classes } + = link_to_more ActivityPub::TagManager.instance.url_for(@next_ancestor) + + = 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 } + + - if status.reblog? + .status__prepend + .status__prepend-icon-wrapper + %i.status__prepend-icon.fa.fa-fw.fa-retweet + %span + = 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') + - elsif pinned + .status__prepend + .status__prepend-icon-wrapper + %i.status__prepend-icon.fa.fa-fw.fa-thumb-tack + %span + = t('stream_entries.pinned') + + = 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: '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 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) + +- if include_threads && !embedded_view? && !user_signed_in? + .entry{ class: entry_classes } + = link_to new_user_session_path, class: 'load-more load-gap' do + = fa_icon 'comments' + = t('statuses.sign_in_to_participate') diff --git a/app/views/statuses/embed.html.haml b/app/views/statuses/embed.html.haml new file mode 100644 index 000000000..6f2ec646f --- /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 000000000..704e37a3d --- /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/_attachment_list.html.haml b/app/views/stream_entries/_attachment_list.html.haml deleted file mode 100644 index d9706f47b..000000000 --- a/app/views/stream_entries/_attachment_list.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -.attachment-list - .attachment-list__icon - = fa_icon 'link' - %ul.attachment-list__list - - attachments.each do |media| - %li - - url = media.remote_url.presence || media.file.url - = link_to File.basename(url), url, title: media.description diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml deleted file mode 100644 index 069d0053f..000000000 --- a/app/views/stream_entries/_detailed_status.html.haml +++ /dev/null @@ -1,79 +0,0 @@ -.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 - .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' - - else - = image_tag status.account.avatar_static_url, width: 48, height: 48, alt: '', class: 'account__avatar u-photo' - %span.display-name - %bdi - %strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: autoplay) - %span.display-name__account - = acct(status.account) - = fa_icon('lock') if status.account.locked? - - = account_action_button(status.account) - - .status__content.emojify< - - if status.spoiler_text? - %p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }< - %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}  - %button.status__content__spoiler-link= t('statuses.show_more') - .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" } - = 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 } - - - 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 } - - 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 } - - 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 - %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) - · - - if status.application && @account.user&.setting_show_application - - if status.application.website.blank? - %strong.detailed-status__application= status.application.name - - else - = link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener' - · - = link_to remote_interaction_path(status, type: :reply), class: 'modal-button detailed-status__link' do - - if status.in_reply_to_id.nil? - = fa_icon('reply') - - else - = fa_icon('reply-all') - %span.detailed-status__reblogs>= number_to_human status.replies_count, strip_insignificant_zeros: true - = " " - · - - if status.direct_visibility? - %span.detailed-status__link< - = fa_icon('envelope') - - elsif status.private_visibility? || status.limited_visibility? - %span.detailed-status__link< - = fa_icon('lock') - - else - = link_to remote_interaction_path(status, type: :reblog), class: 'modal-button detailed-status__link' do - = fa_icon('retweet') - %span.detailed-status__reblogs>= number_to_human status.reblogs_count, strip_insignificant_zeros: true - = " " - · - = link_to remote_interaction_path(status, type: :favourite), class: 'modal-button detailed-status__link' do - = fa_icon('star') - %span.detailed-status__favorites>= number_to_human status.favourites_count, strip_insignificant_zeros: true - = " " - - - if user_signed_in? - · - = link_to t('statuses.open_in_web'), web_url("statuses/#{status.id}"), class: 'detailed-status__application', target: '_blank' diff --git a/app/views/stream_entries/_og_description.html.haml b/app/views/stream_entries/_og_description.html.haml deleted file mode 100644 index a7b18424d..000000000 --- a/app/views/stream_entries/_og_description.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- description = status_description(activity) - -%meta{ name: 'description', content: description }/ -= opengraph 'og:description', description diff --git a/app/views/stream_entries/_og_image.html.haml b/app/views/stream_entries/_og_image.html.haml deleted file mode 100644 index 67f9274b6..000000000 --- a/app/views/stream_entries/_og_image.html.haml +++ /dev/null @@ -1,38 +0,0 @@ -- if activity.is_a?(Status) && (activity.non_sensitive_with_media? || (activity.with_media? && Setting.preview_sensitive_media)) - - player_card = false - - activity.media_attachments.each do |media| - - if media.image? - = opengraph 'og:image', full_asset_url(media.file.url(:original)) - = opengraph 'og:image:type', media.file_content_type - - unless media.file.meta.nil? - = opengraph 'og:image:width', media.file.meta.dig('original', 'width') - = opengraph 'og:image:height', media.file.meta.dig('original', 'height') - - if media.description.present? - = opengraph 'og:image:alt', media.description - - elsif media.video? || media.gifv? - - player_card = true - = opengraph 'og:image', full_asset_url(media.file.url(:small)) - = opengraph 'og:image:type', 'image/png' - - unless media.file.meta.nil? - = opengraph 'og:image:width', media.file.meta.dig('small', 'width') - = opengraph 'og:image:height', media.file.meta.dig('small', 'height') - = opengraph 'og:video', full_asset_url(media.file.url(:original)) - = opengraph 'og:video:secure_url', full_asset_url(media.file.url(:original)) - = opengraph 'og:video:type', media.file_content_type - = opengraph 'twitter:player', medium_player_url(media) - = opengraph 'twitter:player:stream', full_asset_url(media.file.url(:original)) - = opengraph 'twitter:player:stream:content_type', media.file_content_type - - unless media.file.meta.nil? - = opengraph 'og:video:width', media.file.meta.dig('original', 'width') - = opengraph 'og:video:height', media.file.meta.dig('original', 'height') - = opengraph 'twitter:player:width', media.file.meta.dig('original', 'width') - = opengraph 'twitter:player:height', media.file.meta.dig('original', 'height') - - if player_card - = opengraph 'twitter:card', 'player' - - else - = opengraph 'twitter:card', 'summary_large_image' -- else - = opengraph 'og:image', full_asset_url(account.avatar.url(:original)) - = opengraph 'og:image:width', '120' - = opengraph 'og:image:height','120' - = opengraph 'twitter:card', 'summary' diff --git a/app/views/stream_entries/_poll.html.haml b/app/views/stream_entries/_poll.html.haml deleted file mode 100644 index ba34890df..000000000 --- a/app/views/stream_entries/_poll.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -- show_results = (user_signed_in? && poll.voted?(current_account)) || poll.expired? - -.poll - %ul - - poll.loaded_options.each do |option| - %li - - if show_results - - percent = poll.votes_count > 0 ? 100 * option.votes_count / poll.votes_count : 0 - %span.poll__chart{ style: "width: #{percent}%" } - - %label.poll__text>< - %span.poll__number= percent.round - = Formatter.instance.format_poll_option(status, option, autoplay: autoplay) - - else - %label.poll__text>< - %span.poll__input{ class: poll.multiple? ? 'checkbox' : nil}>< - = Formatter.instance.format_poll_option(status, option, autoplay: autoplay) - .poll__footer - - unless show_results - %button.button.button-secondary{ disabled: true } - = t('statuses.poll.vote') - - %span= t('statuses.poll.total_votes', count: poll.votes_count) - - - unless poll.expires_at.nil? - · - %span= l poll.expires_at diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml deleted file mode 100644 index 0b924f72f..000000000 --- a/app/views/stream_entries/_simple_status.html.haml +++ /dev/null @@ -1,60 +0,0 @@ -.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 - %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 - .status__avatar - %div - - if current_account&.user&.setting_auto_play_gif || autoplay - = image_tag status.account.avatar_original_url, width: 48, height: 48, alt: '', class: 'u-photo account__avatar' - - else - = image_tag status.account.avatar_static_url, width: 48, height: 48, alt: '', class: 'u-photo account__avatar' - %span.display-name - %bdi - %strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: autoplay) -   - %span.display-name__account - = acct(status.account) - = fa_icon('lock') if status.account.locked? - .status__content.emojify< - - if status.spoiler_text? - %p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }< - %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}  - %button.status__content__spoiler-link= t('statuses.show_more') - .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" } - = 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 } - - - 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 } - - 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 } - - elsif status.preview_card - = react_component :card, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json - - .status__action-bar - .status__action-bar__counter - = link_to remote_interaction_path(status, type: :reply), 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.in_reply_to_id.nil? - = fa_icon 'reply fw' - - else - = 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? - = fa_icon 'retweet fw' - - elsif status.private_visibility? - = fa_icon 'lock fw' - - else - = fa_icon 'envelope fw' - = link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do - = fa_icon 'star fw' diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml deleted file mode 100644 index 83887cd87..000000000 --- a/app/views/stream_entries/_status.html.haml +++ /dev/null @@ -1,62 +0,0 @@ -:ruby - pinned ||= false - include_threads ||= false - is_predecessor ||= false - is_successor ||= false - direct_reply_id ||= false - parent_id ||= false - autoplay ||= current_account&.user&.setting_auto_play_gif - is_direct_parent = direct_reply_id == status.id - is_direct_child = parent_id == status.in_reply_to_id - centered ||= include_threads && !is_predecessor && !is_successor - h_class = microformats_h_class(status, is_predecessor, is_successor, include_threads) - style_classes = style_classes(status, is_predecessor, is_successor, include_threads) - mf_classes = microformats_classes(status, is_direct_parent, is_direct_child) - entry_classes = h_class + ' ' + mf_classes + ' ' + style_classes - -- if status.reply? && include_threads - - if @next_ancestor - .entry{ class: entry_classes } - = link_to_more 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 - -.entry{ class: entry_classes } - - - if status.reblog? - .status__prepend - .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 - %bdi - %strong.emojify= display_name(status.account, custom_emojify: true) - = t('stream_entries.reblogged') - - elsif pinned - .status__prepend - .status__prepend-icon-wrapper - %i.status__prepend-icon.fa.fa-fw.fa-thumb-tack - %span - = t('stream_entries.pinned') - - = render (centered ? 'stream_entries/detailed_status' : 'stream_entries/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 - - - if thread[:next_status] - .entry{ class: entry_classes } - = link_to_more 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) - -- if include_threads && !embedded_view? && !user_signed_in? - .entry{ class: entry_classes } - = link_to new_user_session_path, class: 'load-more load-gap' do - = fa_icon 'comments' - = t('statuses.sign_in_to_participate') diff --git a/app/views/stream_entries/embed.html.haml b/app/views/stream_entries/embed.html.haml deleted file mode 100644 index 4871c101e..000000000 --- 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 0e81c4f68..000000000 --- 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 4b6d464c6..69b495a96 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 000000000..1fecece05 --- /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 09e6c9fae..2e38fb1f2 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 b728d719f..3d2a0665d 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 7fee15a35..b9082bde1 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 33cc71087..27946b60f 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 1bb6636c6..95e5c363c 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 eb7fdf9d7..000000000 --- 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 f33822c7c..000000000 --- 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 c07f6c4b8..ddfe8b46f 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/statuses_helper_spec.rb b/spec/helpers/statuses_helper_spec.rb new file mode 100644 index 000000000..510955a2f --- /dev/null +++ b/spec/helpers/statuses_helper_spec.rb @@ -0,0 +1,224 @@ +require 'rails_helper' + +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") + + expect(helper.display_name(account)).to eq "Display" + end + + it 'uses the username when display name is nil' do + account = Account.new(display_name: nil, username: "Username") + + expect(helper.display_name(account)).to eq "Username" + end + end + + describe '#stream_link_target' do + it 'returns nil if it is not an embedded view' do + set_not_embedded_view + + expect(helper.stream_link_target).to be_nil + end + + it 'returns _blank if it is an embedded view' do + set_embedded_view + + expect(helper.stream_link_target).to eq '_blank' + end + end + + describe '#acct' do + it 'is fully qualified for embedded local accounts' do + allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain') + set_embedded_view + account = Account.new(domain: nil, username: 'user') + + acct = helper.acct(account) + + expect(acct).to eq '@user@local_domain' + end + + it 'is fully qualified for embedded foreign accounts' do + set_embedded_view + account = Account.new(domain: 'foreign_server.com', username: 'user') + + acct = helper.acct(account) + + expect(acct).to eq '@user@foreign_server.com' + end + + it 'is fully qualified for non embedded foreign accounts' do + set_not_embedded_view + account = Account.new(domain: 'foreign_server.com', username: 'user') + + acct = helper.acct(account) + + expect(acct).to eq '@user@foreign_server.com' + end + + it 'is fully qualified for non embedded local accounts' do + allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain') + set_not_embedded_view + account = Account.new(domain: nil, username: 'user') + + acct = helper.acct(account) + + expect(acct).to eq '@user@local_domain' + end + end + + def set_not_embedded_view + params[:controller] = "not_#{StatusesHelper::EMBEDDED_CONTROLLER}" + params[:action] = "not_#{StatusesHelper::EMBEDDED_ACTION}" + end + + def set_embedded_view + params[:controller] = StatusesHelper::EMBEDDED_CONTROLLER + params[:action] = StatusesHelper::EMBEDDED_ACTION + end + + describe '#style_classes' do + it do + status = double(reblog?: false) + classes = helper.style_classes(status, false, false, false) + + expect(classes).to eq 'entry' + end + + it do + status = double(reblog?: true) + classes = helper.style_classes(status, false, false, false) + + expect(classes).to eq 'entry entry-reblog' + end + + it do + status = double(reblog?: false) + classes = helper.style_classes(status, true, false, false) + + expect(classes).to eq 'entry entry-predecessor' + end + + it do + status = double(reblog?: false) + classes = helper.style_classes(status, false, true, false) + + expect(classes).to eq 'entry entry-successor' + end + + it do + status = double(reblog?: false) + classes = helper.style_classes(status, false, false, true) + + expect(classes).to eq 'entry entry-center' + end + + it do + status = double(reblog?: true) + classes = helper.style_classes(status, true, true, true) + + expect(classes).to eq 'entry entry-predecessor entry-reblog entry-successor entry-center' + end + end + + describe '#microformats_classes' do + it do + status = double(reblog?: false) + classes = helper.microformats_classes(status, false, false) + + expect(classes).to eq '' + end + + it do + status = double(reblog?: false) + classes = helper.microformats_classes(status, true, false) + + expect(classes).to eq 'p-in-reply-to' + end + + it do + status = double(reblog?: false) + classes = helper.microformats_classes(status, false, true) + + expect(classes).to eq 'p-comment' + end + + it do + status = double(reblog?: true) + classes = helper.microformats_classes(status, true, false) + + expect(classes).to eq 'p-in-reply-to p-repost-of' + end + + it do + status = double(reblog?: true) + classes = helper.microformats_classes(status, true, true) + + expect(classes).to eq 'p-in-reply-to p-repost-of p-comment' + end + end + + describe '#microformats_h_class' do + it do + status = double(reblog?: false) + css_class = helper.microformats_h_class(status, false, false, false) + + expect(css_class).to eq 'h-entry' + end + + it do + status = double(reblog?: true) + css_class = helper.microformats_h_class(status, false, false, false) + + expect(css_class).to eq 'h-cite' + end + + it do + status = double(reblog?: false) + css_class = helper.microformats_h_class(status, true, false, false) + + expect(css_class).to eq 'h-cite' + end + + it do + status = double(reblog?: false) + css_class = helper.microformats_h_class(status, false, true, false) + + expect(css_class).to eq 'h-cite' + end + + it do + status = double(reblog?: false) + css_class = helper.microformats_h_class(status, false, false, true) + + expect(css_class).to eq '' + end + + it do + status = double(reblog?: true) + css_class = helper.microformats_h_class(status, true, true, true) + + expect(css_class).to eq 'h-cite' + end + end + + describe '#rtl?' do + it 'is false if text is empty' do + expect(helper).not_to be_rtl '' + end + + it 'is false if there are no right to left characters' do + expect(helper).not_to be_rtl 'hello world' + end + + it 'is false if right to left characters are fewer than 1/3 of total text' do + expect(helper).not_to be_rtl 'hello ݟ world' + end + + it 'is true if right to left characters are greater than 1/3 of total text' do + expect(helper).to be_rtl 'aaݟaaݟ' + end + end +end diff --git a/spec/helpers/stream_entries_helper_spec.rb b/spec/helpers/stream_entries_helper_spec.rb deleted file mode 100644 index 845b9974e..000000000 --- a/spec/helpers/stream_entries_helper_spec.rb +++ /dev/null @@ -1,224 +0,0 @@ -require 'rails_helper' - -RSpec.describe StreamEntriesHelper, type: :helper do - describe '#display_name' do - it 'uses the display name when it exists' do - account = Account.new(display_name: "Display", username: "Username") - - expect(helper.display_name(account)).to eq "Display" - end - - it 'uses the username when display name is nil' do - account = Account.new(display_name: nil, username: "Username") - - expect(helper.display_name(account)).to eq "Username" - end - end - - describe '#stream_link_target' do - it 'returns nil if it is not an embedded view' do - set_not_embedded_view - - expect(helper.stream_link_target).to be_nil - end - - it 'returns _blank if it is an embedded view' do - set_embedded_view - - expect(helper.stream_link_target).to eq '_blank' - end - end - - describe '#acct' do - it 'is fully qualified for embedded local accounts' do - allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain') - set_embedded_view - account = Account.new(domain: nil, username: 'user') - - acct = helper.acct(account) - - expect(acct).to eq '@user@local_domain' - end - - it 'is fully qualified for embedded foreign accounts' do - set_embedded_view - account = Account.new(domain: 'foreign_server.com', username: 'user') - - acct = helper.acct(account) - - expect(acct).to eq '@user@foreign_server.com' - end - - it 'is fully qualified for non embedded foreign accounts' do - set_not_embedded_view - account = Account.new(domain: 'foreign_server.com', username: 'user') - - acct = helper.acct(account) - - expect(acct).to eq '@user@foreign_server.com' - end - - it 'is fully qualified for non embedded local accounts' do - allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain') - set_not_embedded_view - account = Account.new(domain: nil, username: 'user') - - acct = helper.acct(account) - - expect(acct).to eq '@user@local_domain' - end - end - - def set_not_embedded_view - params[:controller] = "not_#{StreamEntriesHelper::EMBEDDED_CONTROLLER}" - params[:action] = "not_#{StreamEntriesHelper::EMBEDDED_ACTION}" - end - - def set_embedded_view - params[:controller] = StreamEntriesHelper::EMBEDDED_CONTROLLER - params[:action] = StreamEntriesHelper::EMBEDDED_ACTION - end - - describe '#style_classes' do - it do - status = double(reblog?: false) - classes = helper.style_classes(status, false, false, false) - - expect(classes).to eq 'entry' - end - - it do - status = double(reblog?: true) - classes = helper.style_classes(status, false, false, false) - - expect(classes).to eq 'entry entry-reblog' - end - - it do - status = double(reblog?: false) - classes = helper.style_classes(status, true, false, false) - - expect(classes).to eq 'entry entry-predecessor' - end - - it do - status = double(reblog?: false) - classes = helper.style_classes(status, false, true, false) - - expect(classes).to eq 'entry entry-successor' - end - - it do - status = double(reblog?: false) - classes = helper.style_classes(status, false, false, true) - - expect(classes).to eq 'entry entry-center' - end - - it do - status = double(reblog?: true) - classes = helper.style_classes(status, true, true, true) - - expect(classes).to eq 'entry entry-predecessor entry-reblog entry-successor entry-center' - end - end - - describe '#microformats_classes' do - it do - status = double(reblog?: false) - classes = helper.microformats_classes(status, false, false) - - expect(classes).to eq '' - end - - it do - status = double(reblog?: false) - classes = helper.microformats_classes(status, true, false) - - expect(classes).to eq 'p-in-reply-to' - end - - it do - status = double(reblog?: false) - classes = helper.microformats_classes(status, false, true) - - expect(classes).to eq 'p-comment' - end - - it do - status = double(reblog?: true) - classes = helper.microformats_classes(status, true, false) - - expect(classes).to eq 'p-in-reply-to p-repost-of' - end - - it do - status = double(reblog?: true) - classes = helper.microformats_classes(status, true, true) - - expect(classes).to eq 'p-in-reply-to p-repost-of p-comment' - end - end - - describe '#microformats_h_class' do - it do - status = double(reblog?: false) - css_class = helper.microformats_h_class(status, false, false, false) - - expect(css_class).to eq 'h-entry' - end - - it do - status = double(reblog?: true) - css_class = helper.microformats_h_class(status, false, false, false) - - expect(css_class).to eq 'h-cite' - end - - it do - status = double(reblog?: false) - css_class = helper.microformats_h_class(status, true, false, false) - - expect(css_class).to eq 'h-cite' - end - - it do - status = double(reblog?: false) - css_class = helper.microformats_h_class(status, false, true, false) - - expect(css_class).to eq 'h-cite' - end - - it do - status = double(reblog?: false) - css_class = helper.microformats_h_class(status, false, false, true) - - expect(css_class).to eq '' - end - - it do - status = double(reblog?: true) - css_class = helper.microformats_h_class(status, true, true, true) - - expect(css_class).to eq 'h-cite' - end - end - - describe '#rtl?' do - it 'is false if text is empty' do - expect(helper).not_to be_rtl '' - end - - it 'is false if there are no right to left characters' do - expect(helper).not_to be_rtl 'hello world' - end - - it 'is false if right to left characters are fewer than 1/3 of total text' do - expect(helper).not_to be_rtl 'hello ݟ world' - end - - it 'is true if right to left characters are greater than 1/3 of total text' do - expect(helper).to be_rtl 'aaݟaaݟ' - end - end -end diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb index 6d246629e..1c5c6f0ed 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 74ab7576f..000000000 --- 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 6b4ee434f..61483f4bf 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 3a804ac0f..e9a7aa934 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 b5f2d5192..000000000 --- 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 da5048f0a..000000000 --- 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 8f8bfbd58..000000000 --- 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 35a804f2b..b1abd79b0 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 6f45762aa..896ac17a3 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/statuses/show.html.haml_spec.rb b/spec/views/statuses/show.html.haml_spec.rb new file mode 100644 index 000000000..dbda3b665 --- /dev/null +++ b/spec/views/statuses/show.html.haml_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'statuses/show.html.haml', without_verify_partial_doubles: true do + before do + double(:api_oembed_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') + allow(view).to receive(:full_asset_url).and_return('//asset.host/image.svg') + allow(view).to receive(:local_time) + allow(view).to receive(:local_time_ago) + allow(view).to receive(:current_account).and_return(nil) + assign(:instance_presenter, InstancePresenter.new) + end + + it 'has valid author h-card and basic data for a detailed_status' do + alice = Fabricate(:account, username: 'alice', display_name: 'Alice') + bob = Fabricate(:account, username: 'bob', display_name: 'Bob') + status = Fabricate(:status, account: alice, text: 'Hello World') + reply = Fabricate(:status, account: bob, thread: status, text: 'Hello Alice') + + assign(:status, status) + assign(:account, alice) + assign(:descendant_threads, []) + + render + + mf2 = Microformats.parse(rendered) + + expect(mf2.entry.url.to_s).not_to be_empty + expect(mf2.entry.author.name.to_s).to eq alice.display_name + expect(mf2.entry.author.url.to_s).not_to be_empty + end + + it 'has valid h-cites for p-in-reply-to and p-comment' do + alice = Fabricate(:account, username: 'alice', display_name: 'Alice') + bob = Fabricate(:account, username: 'bob', display_name: 'Bob') + carl = Fabricate(:account, username: 'carl', display_name: 'Carl') + status = Fabricate(:status, account: alice, text: 'Hello World') + reply = Fabricate(:status, account: bob, thread: status, text: 'Hello Alice') + comment = Fabricate(:status, account: carl, thread: reply, text: 'Hello Bob') + + assign(:status, reply) + assign(:account, alice) + assign(:ancestors, reply.ancestors(1, bob)) + assign(:descendant_threads, [{ statuses: reply.descendants(1) }]) + + render + + mf2 = Microformats.parse(rendered) + + expect(mf2.entry.url.to_s).not_to be_empty + expect(mf2.entry.comment.url.to_s).not_to be_empty + expect(mf2.entry.comment.author.name.to_s).to eq carl.display_name + expect(mf2.entry.comment.author.url.to_s).not_to be_empty + + expect(mf2.entry.in_reply_to.url.to_s).not_to be_empty + expect(mf2.entry.in_reply_to.author.name.to_s).to eq alice.display_name + expect(mf2.entry.in_reply_to.author.url.to_s).not_to be_empty + end + + it 'has valid opengraph tags' do + alice = Fabricate(:account, username: 'alice', display_name: 'Alice') + status = Fabricate(:status, account: alice, text: 'Hello World') + + assign(:status, status) + assign(:account, alice) + assign(:descendant_threads, []) + + render + + header_tags = view.content_for(:header_tags) + + expect(header_tags).to match(%r{}) + expect(header_tags).to match(%r{}) + expect(header_tags).to match(%r{}) + expect(header_tags).to match(%r{}) + end +end diff --git a/spec/views/stream_entries/show.html.haml_spec.rb b/spec/views/stream_entries/show.html.haml_spec.rb deleted file mode 100644 index 93f0adb99..000000000 --- a/spec/views/stream_entries/show.html.haml_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'stream_entries/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') - allow(view).to receive(:full_asset_url).and_return('//asset.host/image.svg') - allow(view).to receive(:local_time) - allow(view).to receive(:local_time_ago) - allow(view).to receive(:current_account).and_return(nil) - assign(:instance_presenter, InstancePresenter.new) - end - - it 'has valid author h-card and basic data for a detailed_status' do - alice = Fabricate(:account, username: 'alice', display_name: 'Alice') - bob = Fabricate(:account, username: 'bob', display_name: 'Bob') - status = Fabricate(:status, account: alice, text: 'Hello World') - 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 - - mf2 = Microformats.parse(rendered) - - expect(mf2.entry.url.to_s).not_to be_empty - expect(mf2.entry.author.name.to_s).to eq alice.display_name - expect(mf2.entry.author.url.to_s).not_to be_empty - end - - it 'has valid h-cites for p-in-reply-to and p-comment' do - alice = Fabricate(:account, username: 'alice', display_name: 'Alice') - bob = Fabricate(:account, username: 'bob', display_name: 'Bob') - carl = Fabricate(:account, username: 'carl', display_name: 'Carl') - status = Fabricate(:status, account: alice, text: 'Hello World') - reply = Fabricate(:status, account: bob, thread: status, text: 'Hello Alice') - 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) }]) - - render - - mf2 = Microformats.parse(rendered) - - expect(mf2.entry.url.to_s).not_to be_empty - expect(mf2.entry.comment.url.to_s).not_to be_empty - expect(mf2.entry.comment.author.name.to_s).to eq carl.display_name - expect(mf2.entry.comment.author.url.to_s).not_to be_empty - - expect(mf2.entry.in_reply_to.url.to_s).not_to be_empty - expect(mf2.entry.in_reply_to.author.name.to_s).to eq alice.display_name - expect(mf2.entry.in_reply_to.author.url.to_s).not_to be_empty - end - - it 'has valid opengraph tags' do - alice = Fabricate(:account, username: 'alice', display_name: 'Alice') - 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 - - header_tags = view.content_for(:header_tags) - - expect(header_tags).to match(%r{}) - expect(header_tags).to match(%r{}) - expect(header_tags).to match(%r{}) - expect(header_tags).to match(%r{}) - end -end -- cgit From 4e921832272425352d28cad550bfc4dffd6d0e78 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 9 Jul 2019 03:27:35 +0200 Subject: Refactor domain block checks (#11268) --- app/controllers/concerns/signature_verification.rb | 4 + app/helpers/domain_control_helper.rb | 17 ++++ app/lib/tag_manager.rb | 3 + .../fetch_featured_collection_service.rb | 3 +- .../activitypub/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 +- app/services/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 (limited to 'spec/services') diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 90a57197c..0ccdf5ec9 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 000000000..efd328f81 --- /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 daf4f556b..c88cf4994 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 6a137b520..2c2770466 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 3c2044941..d65c8f951 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 854a32d05..1c79ecf11 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 3857e7c16..603e27ed9 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 881df478b..a2a2e7071 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 61357abd3..2fbce65b9 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 0ea31a0d8..41a2eb158 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 + + class WebfingerRedirectError < StandardError; end - # 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 + # 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 7a64f4161..cea942e39 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 -- cgit From 5d3feed191bcbe2769512119752b426108152fe9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 10 Jul 2019 18:59:28 +0200 Subject: Refactor fetching of remote resources (#11251) --- app/helpers/jsonld_helper.rb | 47 ++++++----- app/lib/request.rb | 2 +- .../activitypub/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 ++-- spec/services/fetch_atom_service_spec.rb | 96 ---------------------- spec/services/fetch_remote_account_service_spec.rb | 1 + spec/services/fetch_resource_service_spec.rb | 96 ++++++++++++++++++++++ spec/services/resolve_url_service_spec.rb | 44 ++-------- 14 files changed, 231 insertions(+), 305 deletions(-) delete mode 100644 app/services/fetch_atom_service.rb create mode 100644 app/services/fetch_resource_service.rb delete mode 100644 spec/services/fetch_atom_service_spec.rb create mode 100644 spec/services/fetch_resource_service_spec.rb (limited to 'spec/services') diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index 5b4011275..34a657e06 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 322457ad7..1fd3f5190 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 469821032..cf4f62899 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 d6508a988..000000000 --- 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 75fbd0e8c..4e75c370f 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 a7f95603d..3cd06e30f 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 aac39dfd5..208dc7809 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 000000000..c0473f3ad --- /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 f941b489a..80381c16b 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 8b52b8e49..5457d9d4b 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_atom_service_spec.rb b/spec/services/fetch_atom_service_spec.rb deleted file mode 100644 index 495540004..000000000 --- a/spec/services/fetch_atom_service_spec.rb +++ /dev/null @@ -1,96 +0,0 @@ -require 'rails_helper' - -RSpec.describe FetchAtomService, type: :service do - describe '#call' do - let(:url) { 'http://example.com' } - subject { FetchAtomService.new.call(url) } - - context 'url is blank' do - let(:url) { '' } - it { is_expected.to be_nil } - end - - context 'request failed' do - before do - WebMock.stub_request(:get, url).to_return(status: 500, body: '', headers: {}) - end - - it { is_expected.to be_nil } - end - - context 'raise OpenSSL::SSL::SSLError' do - before 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') - is_expected.to be_nil - end - end - - context 'raise HTTP::ConnectionError' do - before 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') - is_expected.to be_nil - end - end - - context 'response success' do - let(:body) { '' } - let(:headers) { { 'Content-Type' => content_type } } - let(:json) { - { 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 - let(:content_type) { 'application/atom+xml' } - - it { is_expected.to eq [url, { :prefetched_body => "" }, :ostatus] } - end - - context '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 - let(:content_type) { 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' } - let(:body) { json } - - it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] } - 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' }) - end - - context 'has link header' 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 - let(:content_type) { 'text/html' } - let(:body) { '' } - - it { is_expected.to eq [1, { prefetched_body: json, id: true }, :activitypub] } - end - end - end -end diff --git a/spec/services/fetch_remote_account_service_spec.rb b/spec/services/fetch_remote_account_service_spec.rb index 37e9910d4..ee7325be2 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_resource_service_spec.rb b/spec/services/fetch_resource_service_spec.rb new file mode 100644 index 000000000..17c192c44 --- /dev/null +++ b/spec/services/fetch_resource_service_spec.rb @@ -0,0 +1,96 @@ +require 'rails_helper' + +RSpec.describe FetchResourceService, type: :service do + let!(:representative) { Fabricate(:account) } + + describe '#call' do + let(:url) { 'http://example.com' } + subject { described_class.new.call(url) } + + context 'url is blank' do + let(:url) { '' } + it { is_expected.to be_nil } + end + + context 'request failed' do + before do + WebMock.stub_request(:get, url).to_return(status: 500, body: '', headers: {}) + end + + it { is_expected.to be_nil } + end + + context 'raise OpenSSL::SSL::SSLError' do + before do + allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(OpenSSL::SSL::SSLError) + end + + it 'return nil' do + is_expected.to be_nil + end + end + + context 'raise HTTP::ConnectionError' do + before do + allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(HTTP::ConnectionError) + end + + it 'return nil' do + is_expected.to be_nil + end + end + + context 'response success' do + let(:body) { '' } + let(:headers) { { 'Content-Type' => content_type } } + let(:json) { + { 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 + let(:content_type) { 'application/atom+xml' } + + it { is_expected.to eq nil } + end + + context '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 + let(:content_type) { 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' } + let(:body) { json } + + it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] } + 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' }) + end + + context 'has link header' 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 + let(:content_type) { 'text/html' } + let(:body) { '' } + + it { is_expected.to eq [1, { prefetched_body: json, id: true }, :activitypub] } + end + end + end +end diff --git a/spec/services/resolve_url_service_spec.rb b/spec/services/resolve_url_service_spec.rb index 7bb5d1940..aa4204637 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(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) + allow(FetchResourceService).to receive(:new).and_return service + allow(service).to receive(:call).with(url).and_return(nil) - expect(account_service).to have_received(:call).with(feed_url, feed_content, nil) + expect(subject.call(url)).to be_nil end end end -- cgit From 4e8dcc5dbbf625b7268ed10d36122de985da6bdc Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 11 Jul 2019 14:49:55 +0200 Subject: 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 +- spec/services/fetch_remote_account_service_spec.rb | 1 + spec/services/fetch_resource_service_spec.rb | 61 +++++++++++++--------- 6 files changed, 43 insertions(+), 40 deletions(-) (limited to 'spec/services') diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index 34a657e06..83a5b2462 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) - 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? + on_behalf_of ||= Account.representative - build_request(uri).perform do |response| + 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 - 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 1fd3f5190..9d874fe2c 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 c0473f3ad..3676d899d 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 720690097..1fa19f54d 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 ee7325be2..b37445861 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 17c192c44..98630966b 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 - } + end before do - WebMock.stub_request(:get, url).to_return(status: 200, body: body, headers: headers) + 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 'content type is application/atom+xml' do + 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) { '' } -- cgit From 730c4053d642024b9949d72c8a9f1873532c6212 Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 19 Jul 2019 01:44:42 +0200 Subject: 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 ++++++++ app/javascript/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 +- app/serializers/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 ++ db/migrate/20190715164535_add_instance_actor.rb | 9 ++++ db/schema.rb | 2 +- db/seeds.rb | 4 +- spec/models/account_spec.rb | 12 ++--- spec/services/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 (limited to 'spec/services') diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 52fb1dc1b..33bac9bbc 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 26f3b1def..51e9764d4 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 d1c525134..42493cd78 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 000000000..41f33602e --- /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 3564bf07b..2b6794ee2 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 3eb88339a..28f1da19f 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 4d452f290..512272dbe 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 a54a702a2..22d78874a 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 adf4586fa..ccd116d6e 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 ccd7bfa12..a54c2174d 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 0644219fb..0bd7aed2e 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 f4af21551..008d0c182 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 b248ed1d2..21431ef8e 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 ae80df9d2..f5a54052a 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://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}" + 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 4e252945f..89c52b84a 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 95f8a39ad..27b536641 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 000000000..a26d54949 --- /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 c7b6b9be6..a6a14827b 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 9a6e9dd78..5f43fbac8 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 ce9ea250d..6495a6193 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 b37445861..ee7325be2 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 98630966b..f836147d3 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 0cd1f91d0..45ba1bbd9 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 -- cgit From bd87e6667975bc3b5bfaf3e1cdff97e041ed4c98 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 21 Jul 2019 04:08:00 +0200 Subject: Remove WebSub subscriptions (#11303) --- app/models/concerns/account_associations.rb | 3 - app/models/subscription.rb | 62 -------------------- app/policies/subscription_policy.rb | 7 --- app/services/suspend_account_service.rb | 1 - .../scheduler/subscriptions_cleanup_scheduler.rb | 4 +- config/sidekiq.yml | 3 - .../20190715031050_drop_subscriptions.rb | 11 ++++ db/schema.rb | 14 ----- spec/fabricators/subscription_fabricator.rb | 7 --- spec/models/subscription_spec.rb | 67 ---------------------- spec/policies/subscription_policy_spec.rb | 24 -------- .../services/batched_remove_status_service_spec.rb | 3 - spec/services/remove_status_service_spec.rb | 3 - spec/services/suspend_account_service_spec.rb | 8 +-- 14 files changed, 14 insertions(+), 203 deletions(-) delete mode 100644 app/models/subscription.rb delete mode 100644 app/policies/subscription_policy.rb create mode 100644 db/post_migrate/20190715031050_drop_subscriptions.rb delete mode 100644 spec/fabricators/subscription_fabricator.rb delete mode 100644 spec/models/subscription_spec.rb delete mode 100644 spec/policies/subscription_policy_spec.rb (limited to 'spec/services') diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index 0921e3252..1db7771c7 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -30,9 +30,6 @@ module AccountAssociations has_many :media_attachments, dependent: :destroy has_many :polls, dependent: :destroy - # PuSH subscriptions - has_many :subscriptions, dependent: :destroy - # Report relationships has_many :reports, dependent: :destroy, inverse_of: :account has_many :targeted_reports, class_name: 'Report', foreign_key: :target_account_id, dependent: :destroy, inverse_of: :target_account diff --git a/app/models/subscription.rb b/app/models/subscription.rb deleted file mode 100644 index 79b81828d..000000000 --- a/app/models/subscription.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true -# == Schema Information -# -# Table name: subscriptions -# -# id :bigint(8) not null, primary key -# callback_url :string default(""), not null -# secret :string -# expires_at :datetime -# confirmed :boolean default(FALSE), not null -# created_at :datetime not null -# updated_at :datetime not null -# last_successful_delivery_at :datetime -# domain :string -# account_id :bigint(8) not null -# - -class Subscription < ApplicationRecord - MIN_EXPIRATION = 1.day.to_i - MAX_EXPIRATION = 30.days.to_i - - belongs_to :account - - validates :callback_url, presence: true - validates :callback_url, uniqueness: { scope: :account_id } - - scope :confirmed, -> { where(confirmed: true) } - scope :future_expiration, -> { where(arel_table[:expires_at].gt(Time.now.utc)) } - scope :expired, -> { where(arel_table[:expires_at].lt(Time.now.utc)) } - scope :active, -> { confirmed.future_expiration } - - def lease_seconds=(value) - self.expires_at = future_expiration(value) - end - - def lease_seconds - (expires_at - Time.now.utc).to_i - end - - def expired? - Time.now.utc > expires_at - end - - before_validation :set_min_expiration - - private - - def future_expiration(value) - Time.now.utc + future_offset(value).seconds - end - - def future_offset(seconds) - [ - [MIN_EXPIRATION, seconds.to_i].max, - MAX_EXPIRATION, - ].min - end - - def set_min_expiration - self.lease_seconds = 0 unless expires_at - end -end diff --git a/app/policies/subscription_policy.rb b/app/policies/subscription_policy.rb deleted file mode 100644 index ac9a8a6c4..000000000 --- a/app/policies/subscription_policy.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -class SubscriptionPolicy < ApplicationPolicy - def index? - admin? - end -end diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb index 0ebe0b562..00cffcdfc 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 - subscriptions ).freeze ASSOCIATIONS_ON_DESTROY = %w( diff --git a/app/workers/scheduler/subscriptions_cleanup_scheduler.rb b/app/workers/scheduler/subscriptions_cleanup_scheduler.rb index 5fba120f6..75fe681a9 100644 --- a/app/workers/scheduler/subscriptions_cleanup_scheduler.rb +++ b/app/workers/scheduler/subscriptions_cleanup_scheduler.rb @@ -5,7 +5,5 @@ class Scheduler::SubscriptionsCleanupScheduler sidekiq_options unique: :until_executed, retry: 0 - def perform - Subscription.expired.in_batches.delete_all - end + def perform; end end diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 5c652792c..7f41b6607 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -21,9 +21,6 @@ user_cleanup_scheduler: cron: '<%= Random.rand(0..59) %> <%= Random.rand(4..6) %> * * *' class: Scheduler::UserCleanupScheduler - subscriptions_cleanup_scheduler: - cron: '<%= Random.rand(0..59) %> <%= Random.rand(1..3) %> * * 0' - class: Scheduler::SubscriptionsCleanupScheduler ip_cleanup_scheduler: cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *' class: Scheduler::IpCleanupScheduler diff --git a/db/post_migrate/20190715031050_drop_subscriptions.rb b/db/post_migrate/20190715031050_drop_subscriptions.rb new file mode 100644 index 000000000..3719afe4a --- /dev/null +++ b/db/post_migrate/20190715031050_drop_subscriptions.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class DropSubscriptions < ActiveRecord::Migration[5.2] + def up + drop_table :subscriptions + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/schema.rb b/db/schema.rb index a6a14827b..6319dd932 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -648,19 +648,6 @@ ActiveRecord::Schema.define(version: 2019_07_15_164535) do t.index ["tag_id", "status_id"], name: "index_statuses_tags_on_tag_id_and_status_id", unique: true end - create_table "subscriptions", force: :cascade do |t| - t.string "callback_url", default: "", null: false - t.string "secret" - t.datetime "expires_at" - t.boolean "confirmed", default: false, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.datetime "last_successful_delivery_at" - t.string "domain" - t.bigint "account_id", null: false - t.index ["account_id", "callback_url"], name: "index_subscriptions_on_account_id_and_callback_url", unique: true - end - create_table "tags", force: :cascade do |t| t.string "name", default: "", null: false t.datetime "created_at", null: false @@ -821,7 +808,6 @@ ActiveRecord::Schema.define(version: 2019_07_15_164535) 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 "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 add_foreign_key "users", "accounts", name: "fk_50500f500d", on_delete: :cascade diff --git a/spec/fabricators/subscription_fabricator.rb b/spec/fabricators/subscription_fabricator.rb deleted file mode 100644 index 347dab5df..000000000 --- a/spec/fabricators/subscription_fabricator.rb +++ /dev/null @@ -1,7 +0,0 @@ -Fabricator(:subscription) do - account - callback_url "http://example.com/callback" - secret "foobar" - expires_at "2016-11-28 11:30:07" - confirmed false -end diff --git a/spec/models/subscription_spec.rb b/spec/models/subscription_spec.rb deleted file mode 100644 index b83979d13..000000000 --- a/spec/models/subscription_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'rails_helper' - -RSpec.describe Subscription, type: :model do - let(:alice) { Fabricate(:account, username: 'alice') } - - subject { Fabricate(:subscription, account: alice) } - - describe '#expired?' do - it 'return true when expires_at is past' do - subject.expires_at = 2.days.ago - expect(subject.expired?).to be true - end - - it 'return false when expires_at is future' do - subject.expires_at = 2.days.from_now - expect(subject.expired?).to be false - end - end - - describe 'lease_seconds' do - it 'returns the time remaining until expiration' do - datetime = 1.day.from_now - subscription = Subscription.new(expires_at: datetime) - travel_to(datetime - 12.hours) do - expect(subscription.lease_seconds).to eq(12.hours) - end - end - end - - describe 'lease_seconds=' do - it 'sets expires_at to min expiration when small value is provided' do - subscription = Subscription.new - datetime = 1.day.from_now - too_low = Subscription::MIN_EXPIRATION - 1000 - travel_to(datetime) do - subscription.lease_seconds = too_low - end - - expected = datetime + Subscription::MIN_EXPIRATION.seconds - expect(subscription.expires_at).to be_within(1.0).of(expected) - end - - it 'sets expires_at to value when valid value is provided' do - subscription = Subscription.new - datetime = 1.day.from_now - valid = Subscription::MIN_EXPIRATION + 1000 - travel_to(datetime) do - subscription.lease_seconds = valid - end - - expected = datetime + valid.seconds - expect(subscription.expires_at).to be_within(1.0).of(expected) - end - - it 'sets expires_at to max expiration when large value is provided' do - subscription = Subscription.new - datetime = 1.day.from_now - too_high = Subscription::MAX_EXPIRATION + 1000 - travel_to(datetime) do - subscription.lease_seconds = too_high - end - - expected = datetime + Subscription::MAX_EXPIRATION.seconds - expect(subscription.expires_at).to be_within(1.0).of(expected) - end - end -end diff --git a/spec/policies/subscription_policy_spec.rb b/spec/policies/subscription_policy_spec.rb deleted file mode 100644 index 21d60c15f..000000000 --- a/spec/policies/subscription_policy_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' -require 'pundit/rspec' - -RSpec.describe SubscriptionPolicy do - let(:subject) { described_class } - let(:admin) { Fabricate(:user, admin: true).account } - let(:john) { Fabricate(:user).account } - - permissions :index? do - context 'admin?' do - it 'permits' do - expect(subject).to permit(admin, Subscription) - end - end - - context '!admin?' do - it 'denies' do - expect(subject).to_not permit(john, Subscription) - end - end - end -end diff --git a/spec/services/batched_remove_status_service_spec.rb b/spec/services/batched_remove_status_service_spec.rb index d52e7f484..f84256f18 100644 --- a/spec/services/batched_remove_status_service_spec.rb +++ b/spec/services/batched_remove_status_service_spec.rb @@ -14,11 +14,8 @@ RSpec.describe BatchedRemoveStatusService, type: :service do before do allow(Redis.current).to receive_messages(publish: nil) - stub_request(:post, 'http://example.com/push').to_return(status: 200, body: '', headers: {}) - stub_request(:post, 'http://example.com/salmon').to_return(status: 200, body: '', headers: {}) stub_request(:post, 'http://example.com/inbox').to_return(status: 200) - Fabricate(:subscription, account: alice, callback_url: 'http://example.com/push', confirmed: true, expires_at: 30.days.from_now) jeff.user.update(current_sign_in_at: Time.zone.now) jeff.follow!(alice) hank.follow!(alice) diff --git a/spec/services/remove_status_service_spec.rb b/spec/services/remove_status_service_spec.rb index 48191d47c..06676ec45 100644 --- a/spec/services/remove_status_service_spec.rb +++ b/spec/services/remove_status_service_spec.rb @@ -10,12 +10,9 @@ RSpec.describe RemoveStatusService, type: :service do let!(:bill) { Fabricate(:account, username: 'bill', protocol: :activitypub, domain: 'example2.com', inbox_url: 'http://example2.com/inbox') } before do - stub_request(:post, 'http://example.com/push').to_return(status: 200, body: '', headers: {}) - stub_request(:post, 'http://example.com/salmon').to_return(status: 200, body: '', headers: {}) stub_request(:post, 'http://example.com/inbox').to_return(status: 200) stub_request(:post, 'http://example2.com/inbox').to_return(status: 200) - Fabricate(:subscription, account: alice, callback_url: 'http://example.com/push', confirmed: true, expires_at: 30.days.from_now) jeff.follow!(alice) hank.follow!(alice) diff --git a/spec/services/suspend_account_service_spec.rb b/spec/services/suspend_account_service_spec.rb index 896ac17a3..eebbbc12a 100644 --- a/spec/services/suspend_account_service_spec.rb +++ b/spec/services/suspend_account_service_spec.rb @@ -18,7 +18,6 @@ RSpec.describe SuspendAccountService, type: :service do let!(:favourite) { Fabricate(:favourite, account: account) } let!(:active_relationship) { Fabricate(:follow, account: account) } let!(:passive_relationship) { Fabricate(:follow, target_account: account) } - let!(:subscription) { Fabricate(:subscription, account: account) } let!(:remote_alice) { Fabricate(:account, inbox_url: 'https://alice.com/inbox', protocol: :activitypub) } let!(:remote_bob) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', protocol: :activitypub) } @@ -31,9 +30,8 @@ RSpec.describe SuspendAccountService, type: :service do account.favourites, account.active_relationships, account.passive_relationships, - account.subscriptions ].map(&:count) - }.from([1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0]) + }.from([1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0]) end it 'sends a delete actor activity to all known inboxes' do @@ -62,7 +60,6 @@ RSpec.describe SuspendAccountService, type: :service do let!(:favourite) { Fabricate(:favourite, account: remote_bob) } let!(:active_relationship) { Fabricate(:follow, account: remote_bob, target_account: account) } let!(:passive_relationship) { Fabricate(:follow, target_account: remote_bob) } - let!(:subscription) { Fabricate(:subscription, account: remote_bob) } it 'deletes associated records' do is_expected.to change { @@ -73,9 +70,8 @@ RSpec.describe SuspendAccountService, type: :service do remote_bob.favourites, remote_bob.active_relationships, remote_bob.passive_relationships, - remote_bob.subscriptions ].map(&:count) - }.from([1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0]) + }.from([1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0]) end it 'sends a reject follow to follwer inboxes' do -- cgit From b9fbcbfe4e0a15fcf8a457ce17ea080f0eb939fc Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 27 Jul 2019 04:42:08 +0200 Subject: Add search syntax for operators and phrases (#11411) --- Gemfile | 1 + Gemfile.lock | 2 + app/lib/search_query_parser.rb | 14 ++++++ app/lib/search_query_transformer.rb | 86 ++++++++++++++++++++++++++++++++++++ app/services/search_service.rb | 9 ++-- spec/services/search_service_spec.rb | 6 +-- 6 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 app/lib/search_query_parser.rb create mode 100644 app/lib/search_query_transformer.rb (limited to 'spec/services') diff --git a/Gemfile b/Gemfile index e969c4ae0..96eb44af7 100644 --- a/Gemfile +++ b/Gemfile @@ -64,6 +64,7 @@ gem 'nsa', '~> 0.2' gem 'oj', '~> 3.8' gem 'ostatus2', '~> 2.0' gem 'ox', '~> 2.11' +gem 'parslet' gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c' gem 'pundit', '~> 2.0' gem 'premailer-rails' diff --git a/Gemfile.lock b/Gemfile.lock index f435b3a35..5adae6f9c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -404,6 +404,7 @@ GEM parallel parser (2.6.3.0) ast (~> 2.4.0) + parslet (1.8.2) pastel (0.7.2) equatable (~> 0.5.0) tty-color (~> 0.4.0) @@ -724,6 +725,7 @@ DEPENDENCIES paperclip (~> 6.0) paperclip-av-transcoder (~> 0.6) parallel_tests (~> 2.29) + parslet pg (~> 1.1) pghero (~> 2.2) pkg-config (~> 1.3) diff --git a/app/lib/search_query_parser.rb b/app/lib/search_query_parser.rb new file mode 100644 index 000000000..405ad15b8 --- /dev/null +++ b/app/lib/search_query_parser.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class SearchQueryParser < Parslet::Parser + rule(:term) { match('[^\s":]').repeat(1).as(:term) } + rule(:quote) { str('"') } + rule(:colon) { str(':') } + rule(:space) { match('\s').repeat(1) } + rule(:operator) { (str('+') | str('-')).as(:operator) } + rule(:prefix) { (term >> colon).as(:prefix) } + rule(:phrase) { (quote >> (term >> space.maybe).repeat >> quote).as(:phrase) } + rule(:clause) { (prefix.maybe >> operator.maybe >> (phrase | term)).as(:clause) } + rule(:query) { (clause >> space.maybe).repeat.as(:query) } + root(:query) +end diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb new file mode 100644 index 000000000..2c4144790 --- /dev/null +++ b/app/lib/search_query_transformer.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +class SearchQueryTransformer < Parslet::Transform + class Query + attr_reader :should_clauses, :must_not_clauses, :must_clauses + + def initialize(clauses) + grouped = clauses.chunk(&:operator).to_h + @should_clauses = grouped.fetch(:should, []) + @must_not_clauses = grouped.fetch(:must_not, []) + @must_clauses = grouped.fetch(:must, []) + end + + def apply(search) + should_clauses.each { |clause| search = search.query.should(clause_to_query(clause)) } + must_clauses.each { |clause| search = search.query.must(clause_to_query(clause)) } + must_not_clauses.each { |clause| search = search.query.must_not(clause_to_query(clause)) } + search.query.minimum_should_match(1) + end + + private + + def clause_to_query(clause) + case clause + when TermClause + { multi_match: { type: 'most_fields', query: clause.term, fields: ['text', 'text.stemmed'] } } + when PhraseClause + { match_phrase: { text: { query: clause.phrase } } } + else + raise "Unexpected clause type: #{clause}" + end + end + end + + class Operator + class << self + def symbol(str) + case str + when '+' + :must + when '-' + :must_not + when nil + :should + else + raise "Unknown operator: #{str}" + end + end + end + end + + class TermClause + attr_reader :prefix, :operator, :term + + def initialize(prefix, operator, term) + @prefix = prefix + @operator = Operator.symbol(operator) + @term = term + end + end + + class PhraseClause + attr_reader :prefix, :operator, :phrase + + def initialize(prefix, operator, phrase) + @prefix = prefix + @operator = Operator.symbol(operator) + @phrase = phrase + end + end + + rule(clause: subtree(:clause)) do + prefix = clause[:prefix][:term].to_s if clause[:prefix] + operator = clause[:operator]&.to_s + + if clause[:term] + TermClause.new(prefix, operator, clause[:term].to_s) + elsif clause[:phrase] + PhraseClause.new(prefix, operator, clause[:phrase].map { |p| p[:term].to_s }.join(' ')) + else + raise "Unexpected clause type: #{clause}" + end + end + + rule(query: sequence(:clauses)) { Query.new(clauses) } +end diff --git a/app/services/search_service.rb b/app/services/search_service.rb index e0da61dac..769d1ac7a 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -33,8 +33,7 @@ class SearchService < BaseService end def perform_statuses_search! - definition = StatusesIndex.filter(term: { searchable_by: @account.id }) - .query(multi_match: { type: 'most_fields', query: @query, operator: 'and', fields: %w(text text.stemmed) }) + definition = parsed_query.apply(StatusesIndex.filter(term: { searchable_by: @account.id })) if @options[:account_id].present? definition = definition.filter(term: { account_id: @options[:account_id] }) @@ -70,7 +69,7 @@ class SearchService < BaseService end def url_query? - @options[:type].blank? && @query =~ /\Ahttps?:\/\// + @resolve && @options[:type].blank? && @query =~ /\Ahttps?:\/\// end def url_resource_results @@ -120,4 +119,8 @@ class SearchService < BaseService domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id), } end + + def parsed_query + SearchQueryTransformer.new.apply(SearchQueryParser.new.parse(@query)) + end end diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index d064cd9b8..ade306ed2 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -27,7 +27,7 @@ describe SearchService, type: :service do it 'returns the empty results' do service = double(call: nil) allow(ResolveURLService).to receive(:new).and_return(service) - results = subject.call(@query, nil, 10) + results = subject.call(@query, nil, 10, resolve: true) expect(service).to have_received(:call).with(@query, on_behalf_of: nil) expect(results).to eq empty_results @@ -40,7 +40,7 @@ describe SearchService, type: :service do service = double(call: account) allow(ResolveURLService).to receive(:new).and_return(service) - results = subject.call(@query, nil, 10) + results = subject.call(@query, nil, 10, resolve: true) expect(service).to have_received(:call).with(@query, on_behalf_of: nil) expect(results).to eq empty_results.merge(accounts: [account]) end @@ -52,7 +52,7 @@ describe SearchService, type: :service do service = double(call: status) allow(ResolveURLService).to receive(:new).and_return(service) - results = subject.call(@query, nil, 10) + results = subject.call(@query, nil, 10, resolve: true) expect(service).to have_received(:call).with(@query, on_behalf_of: nil) expect(results).to eq empty_results.merge(statuses: [status]) end -- cgit From bced70469a6c4aecdb3c71055f329a0f579eb14c Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 7 Aug 2019 20:20:23 +0200 Subject: Add domain block notes (#11515) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add database columns for adding notes to domain blocks/restrctions * Add admin UI to set private and public comments when blocking a domain * Add text for private and public comments on domain blocks * Show domain block comments in admin UI * Add comments to the domain block undo page * Make UnblockDomainService more robust regarding upgraded domain blocks * Allow editing domain blocks * Rename button from “undo domain block” to “view domain block” in account admin UI * Change test to unsilence silenced users from upgraded blocks --- app/controllers/admin/domain_blocks_controller.rb | 28 ++++++++++++++++++-- app/controllers/admin/instances_controller.rb | 2 ++ app/models/domain_block.rb | 16 +++++++----- app/services/block_domain_service.rb | 11 +++++++- app/services/unblock_domain_service.rb | 19 ++------------ app/views/admin/accounts/show.html.haml | 2 +- app/views/admin/domain_blocks/edit.html.haml | 30 ++++++++++++++++++++++ app/views/admin/domain_blocks/new.html.haml | 6 +++++ app/views/admin/domain_blocks/show.html.haml | 12 +++++++++ app/views/admin/instances/show.html.haml | 13 ++++++++++ app/workers/domain_block_worker.rb | 4 +-- config/locales/en.yml | 8 ++++++ config/routes.rb | 6 ++++- ...20190807135426_add_comments_to_domain_blocks.rb | 7 +++++ db/schema.rb | 4 ++- spec/services/unblock_domain_service_spec.rb | 2 +- spec/workers/domain_block_worker_spec.rb | 2 +- 17 files changed, 138 insertions(+), 34 deletions(-) create mode 100644 app/views/admin/domain_blocks/edit.html.haml create mode 100644 db/migrate/20190807135426_add_comments_to_domain_blocks.rb (limited to 'spec/services') diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 7129656da..74a36b79c 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -2,13 +2,17 @@ module Admin class DomainBlocksController < BaseController - before_action :set_domain_block, only: [:show, :destroy] + before_action :set_domain_block, only: [:show, :destroy, :edit, :update] def new authorize :domain_block, :create? @domain_block = DomainBlock.new(domain: params[:_domain]) end + def edit + authorize :domain_block, :create? + end + def create authorize :domain_block, :create? @@ -35,6 +39,22 @@ module Admin end end + def update + authorize :domain_block, :create? + + @domain_block.update(update_params) + + severity_changed = @domain_block.severity_changed? + + if @domain_block.save + DomainBlockWorker.perform_async(@domain_block.id, severity_changed) + log_action :create, @domain_block + redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') + else + render :edit + end + end + def show authorize @domain_block, :show? end @@ -52,8 +72,12 @@ module Admin @domain_block = DomainBlock.find(params[:id]) end + def update_params + params.require(:domain_block).permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment) + end + def resource_params - params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports) + params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment) end end end diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index d4f201807..b47b18f8e 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -21,6 +21,8 @@ module Admin @blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count @available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url) @media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size) + @private_comment = @domain_block&.private_comment + @public_comment = @domain_block&.public_comment end private diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index 25d3b87ef..3f5b9f23e 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -3,13 +3,15 @@ # # Table name: domain_blocks # -# id :bigint(8) not null, primary key -# domain :string default(""), not null -# created_at :datetime not null -# updated_at :datetime not null -# severity :integer default("silence") -# reject_media :boolean default(FALSE), not null -# reject_reports :boolean default(FALSE), not null +# id :bigint(8) not null, primary key +# domain :string default(""), not null +# created_at :datetime not null +# updated_at :datetime not null +# severity :integer default("silence") +# reject_media :boolean default(FALSE), not null +# reject_reports :boolean default(FALSE), not null +# private_comment :text +# public_comment :text # class DomainBlock < ApplicationRecord diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb index c5e5e5761..0ec6be503 100644 --- a/app/services/block_domain_service.rb +++ b/app/services/block_domain_service.rb @@ -3,13 +3,22 @@ class BlockDomainService < BaseService attr_reader :domain_block - def call(domain_block) + def call(domain_block, update = false) @domain_block = domain_block process_domain_block! + process_retroactive_updates! if update end private + def process_retroactive_updates! + # If the domain block severity has been changed, undo the appropriate limitations + scope = Account.by_domain_and_subdomains(domain_block.domain) + + scope.where(silenced_at: domain_block.created_at).in_batches.update_all(silenced_at: nil) unless domain_block.silence? + scope.where(suspended_at: domain_block.created_at).in_batches.update_all(suspended_at: nil) unless domain_block.suspend? + end + def process_domain_block! clear_media! if domain_block.reject_media? diff --git a/app/services/unblock_domain_service.rb b/app/services/unblock_domain_service.rb index fc262a50a..d502d9e49 100644 --- a/app/services/unblock_domain_service.rb +++ b/app/services/unblock_domain_service.rb @@ -10,24 +10,9 @@ class UnblockDomainService < BaseService end def process_retroactive_updates - blocked_accounts.in_batches.update_all(update_options) unless domain_block.noop? - end - - def blocked_accounts scope = Account.by_domain_and_subdomains(domain_block.domain) - if domain_block.silence? - scope.where(silenced_at: @domain_block.created_at) - else - scope.where(suspended_at: @domain_block.created_at) - end - end - - def update_options - { domain_block_impact => nil } - end - - def domain_block_impact - domain_block.silence? ? :silenced_at : :suspended_at + scope.where(silenced_at: domain_block.created_at).in_batches.update_all(silenced_at: nil) unless domain_block.noop? + scope.where(suspended_at: domain_block.created_at).in_batches.update_all(suspended_at: nil) if domain_block.suspend? end end diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index 7494c9fa2..59babd3b0 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -174,7 +174,7 @@ - unless @account.local? - if DomainBlock.where(domain: @account.domain).exists? - = link_to t('admin.domain_blocks.undo'), admin_instance_path(@account.domain), class: 'button' + = link_to t('admin.domain_blocks.view'), admin_instance_path(@account.domain), class: 'button' - else = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain), class: 'button button--destructive' diff --git a/app/views/admin/domain_blocks/edit.html.haml b/app/views/admin/domain_blocks/edit.html.haml new file mode 100644 index 000000000..29e47ef3b --- /dev/null +++ b/app/views/admin/domain_blocks/edit.html.haml @@ -0,0 +1,30 @@ +- content_for :header_tags do + = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous' + +- content_for :page_title do + = t('admin.domain_blocks.edit') + += simple_form_for @domain_block, url: admin_domain_block_path(@domain_block), method: :put do |f| + = render 'shared/error_messages', object: @domain_block + + .fields-row + .fields-row__column.fields-row__column-6.fields-group + = f.input :domain, wrapper: :with_label, label: t('admin.domain_blocks.domain'), hint: t('admin.domain_blocks.new.hint'), required: true, readonly: true, disabled: true + + .fields-row__column.fields-row__column-6.fields-group + = f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| t("admin.domain_blocks.new.severity.#{type}") }, hint: t('admin.domain_blocks.new.severity.desc_html') + + .fields-group + = f.input :reject_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_media'), hint: I18n.t('admin.domain_blocks.reject_media_hint') + + .fields-group + = f.input :reject_reports, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_reports'), hint: I18n.t('admin.domain_blocks.reject_reports_hint') + + .field-group + = f.input :private_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.private_comment'), hint: t('admin.domain_blocks.private_comment_hint'), rows: 6 + + .field-group + = f.input :public_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.public_comment'), hint: t('admin.domain_blocks.public_comment_hint'), rows: 6 + + .actions + = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/admin/domain_blocks/new.html.haml b/app/views/admin/domain_blocks/new.html.haml index 055d2fbd7..ed1581936 100644 --- a/app/views/admin/domain_blocks/new.html.haml +++ b/app/views/admin/domain_blocks/new.html.haml @@ -20,5 +20,11 @@ .fields-group = f.input :reject_reports, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_reports'), hint: I18n.t('admin.domain_blocks.reject_reports_hint') + .field-group + = f.input :private_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.private_comment'), hint: t('admin.domain_blocks.private_comment_hint'), rows: 6 + + .field-group + = f.input :public_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.public_comment'), hint: t('admin.domain_blocks.public_comment_hint'), rows: 6 + .actions = f.button :button, t('.create'), type: :submit diff --git a/app/views/admin/domain_blocks/show.html.haml b/app/views/admin/domain_blocks/show.html.haml index dca4dbac7..e64aaa629 100644 --- a/app/views/admin/domain_blocks/show.html.haml +++ b/app/views/admin/domain_blocks/show.html.haml @@ -1,6 +1,18 @@ - content_for :page_title do = t('admin.domain_blocks.show.title', domain: @domain_block.domain) +- if @domain_block.private_comment.present? + .speech-bubble + .speech-bubble__bubble + = simple_format(h(@domain_block.private_comment)) + .speech-bubble__owner= t 'admin.instances.private_comment' + +- if @domain_block.public_comment.present? + .speech-bubble + .speech-bubble__bubble + = simple_format(h(@domain_block.public_comment)) + .speech-bubble__owner= t 'admin.instances.public_comment' + = simple_form_for @domain_block, url: admin_domain_block_path(@domain_block), method: :delete do |f| - unless (@domain_block.noop?) diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml index fbb49ba02..294c9495d 100644 --- a/app/views/admin/instances/show.html.haml +++ b/app/views/admin/instances/show.html.haml @@ -31,6 +31,18 @@ = fa_icon 'times' .dashboard__counters__label= t 'admin.instances.delivery_available' +- if @private_comment.present? + .speech-bubble + .speech-bubble__bubble + = simple_format(h(@private_comment)) + .speech-bubble__owner= t 'admin.instances.private_comment' + +- if @public_comment.present? + .speech-bubble + .speech-bubble__bubble + = simple_format(h(@public_comment)) + .speech-bubble__owner= t 'admin.instances.public_comment' + %hr.spacer/ %div{ style: 'overflow: hidden' } @@ -41,6 +53,7 @@ - if @domain_allow = link_to t('admin.domain_allows.undo'), admin_domain_allow_path(@domain_allow), class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure'), method: :delete } - elsif @domain_block + = link_to t('admin.domain_blocks.edit'), edit_admin_domain_block_path(@domain_block), class: 'button' = link_to t('admin.domain_blocks.undo'), admin_domain_block_path(@domain_block), class: 'button' - else = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @instance.domain), class: 'button' diff --git a/app/workers/domain_block_worker.rb b/app/workers/domain_block_worker.rb index 884477829..35518d6b5 100644 --- a/app/workers/domain_block_worker.rb +++ b/app/workers/domain_block_worker.rb @@ -3,8 +3,8 @@ class DomainBlockWorker include Sidekiq::Worker - def perform(domain_block_id) - BlockDomainService.new.call(DomainBlock.find(domain_block_id)) + def perform(domain_block_id, update = false) + BlockDomainService.new.call(DomainBlock.find(domain_block_id), update) rescue ActiveRecord::RecordNotFound true end diff --git a/config/locales/en.yml b/config/locales/en.yml index 17ff24726..b677a6651 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -284,6 +284,7 @@ en: created_msg: Domain block is now being processed destroyed_msg: Domain block has been undone domain: Domain + edit: Edit domain block existing_domain_block_html: You have already imposed stricter limits on %{name}, you need to unblock it first. new: create: Create block @@ -294,6 +295,10 @@ en: silence: Silence suspend: Suspend title: New domain block + private_comment: Private comment + private_comment_hint: Comment about this domain limitation for internal use by the moderators. + public_comment: Public comment + public_comment_hint: Comment about this domain limitation for the general public, if advertising the list of domain limitations is enabled. reject_media: Reject media files reject_media_hint: Removes locally stored media files and refuses to download any in the future. Irrelevant for suspensions reject_reports: Reject reports @@ -313,6 +318,7 @@ en: title: Undo domain block for %{domain} undo: Undo undo: Undo domain block + view: View domain block email_domain_blocks: add_new: Add new created_msg: Successfully added e-mail domain to blacklist @@ -336,6 +342,8 @@ en: all: All limited: Limited title: Moderation + private_comment: Private comment + public_comment: Public comment title: Federation total_blocked_by_us: Blocked by us total_followed_by_them: Followed by them diff --git a/config/routes.rb b/config/routes.rb index 60f7d2e05..9c33b8190 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -155,7 +155,11 @@ Rails.application.routes.draw do get '/dashboard', to: 'dashboard#index' resources :domain_allows, only: [:new, :create, :show, :destroy] - resources :domain_blocks, only: [:new, :create, :show, :destroy] + resources :domain_blocks, only: [:new, :create, :show, :destroy, :update] do + member do + get :edit + end + end resources :email_domain_blocks, only: [:index, :new, :create, :destroy] resources :action_logs, only: [:index] resources :warning_presets, except: [:new] diff --git a/db/migrate/20190807135426_add_comments_to_domain_blocks.rb b/db/migrate/20190807135426_add_comments_to_domain_blocks.rb new file mode 100644 index 000000000..b660a71ad --- /dev/null +++ b/db/migrate/20190807135426_add_comments_to_domain_blocks.rb @@ -0,0 +1,7 @@ +class AddCommentsToDomainBlocks < ActiveRecord::Migration[5.2] + def change + add_column :domain_blocks, :private_comment, :text + add_column :domain_blocks, :public_comment, :text + end +end + diff --git a/db/schema.rb b/db/schema.rb index d1b6825b4..f8fc6a821 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_08_05_123746) do +ActiveRecord::Schema.define(version: 2019_08_07_135426) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -259,6 +259,8 @@ ActiveRecord::Schema.define(version: 2019_08_05_123746) do t.integer "severity", default: 0 t.boolean "reject_media", default: false, null: false t.boolean "reject_reports", default: false, null: false + t.text "private_comment" + t.text "public_comment" t.index ["domain"], name: "index_domain_blocks_on_domain", unique: true end diff --git a/spec/services/unblock_domain_service_spec.rb b/spec/services/unblock_domain_service_spec.rb index 619aefb5c..27dbc92ad 100644 --- a/spec/services/unblock_domain_service_spec.rb +++ b/spec/services/unblock_domain_service_spec.rb @@ -31,7 +31,7 @@ describe UnblockDomainService, type: :service do subject.call(@domain_block) expect_deleted_domain_block expect(@suspended.reload.suspended?).to be false - expect(@silenced.reload.silenced?).to be true + expect(@silenced.reload.silenced?).to be false expect(@independently_suspended.reload.suspended?).to be true expect(@independently_silenced.reload.silenced?).to be true end diff --git a/spec/workers/domain_block_worker_spec.rb b/spec/workers/domain_block_worker_spec.rb index c4138501f..48b3e38c4 100644 --- a/spec/workers/domain_block_worker_spec.rb +++ b/spec/workers/domain_block_worker_spec.rb @@ -14,7 +14,7 @@ describe DomainBlockWorker do result = subject.perform(domain_block.id) expect(result).to be_nil - expect(service).to have_received(:call).with(domain_block) + expect(service).to have_received(:call).with(domain_block, false) end it 'calls domain block service for relevant domain block' do -- cgit From 8fdff2748ffbc4ce3d36c75f2bd33182b641e895 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 16 Aug 2019 01:24:03 +0200 Subject: Add more accurate account search (#11537) * Add more accurate account search When ElasticSearch is available, a more accurate search is implemented: - Using edge n-gram index for acct and display name - Using asciifolding and cjk width normalization on display names - Using Gaussian decay on account activity for additional scoring (recency) - Using followers/friends ratio for additional scoring (spamminess) - Using followers number for additional scoring (size) The exact match precedence only takes effect when the input conforms to the username format and the username part of it is complete, i.e. when the user started typing the domain part. * Support single-letter usernames * Fix tests * Fix not picking up account updates * Add weights and normalization for scores, skip zero terms queries * Use local counts for accounts index, adjust search parameters * Fix mistakes * Using updated_at of accounts is inadequate for remote accounts --- app/chewy/accounts_index.rb | 36 +++++ app/models/account.rb | 6 + app/models/account_stat.rb | 2 + app/services/account_search_service.rb | 189 ++++++++++++++++++--------- spec/services/account_search_service_spec.rb | 122 ++++------------- 5 files changed, 194 insertions(+), 161 deletions(-) create mode 100644 app/chewy/accounts_index.rb (limited to 'spec/services') diff --git a/app/chewy/accounts_index.rb b/app/chewy/accounts_index.rb new file mode 100644 index 000000000..e11b80039 --- /dev/null +++ b/app/chewy/accounts_index.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class AccountsIndex < Chewy::Index + settings index: { refresh_interval: '5m' }, analysis: { + analyzer: { + content: { + tokenizer: 'whitespace', + filter: %w(lowercase asciifolding cjk_width), + }, + + edge_ngram: { + tokenizer: 'edge_ngram', + filter: %w(lowercase asciifolding cjk_width), + }, + }, + + tokenizer: { + edge_ngram: { + type: 'edge_ngram', + min_gram: 1, + max_gram: 15, + }, + }, + } + + define_type ::Account.searchable.includes(:account_stat), delete_if: ->(account) { account.destroyed? || !account.searchable? } do + root date_detection: false do + field :id, type: 'long' + field :display_name, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' + field :acct, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } + field :following_count, type: 'long', value: ->(account) { account.active_relationships.count } + field :followers_count, type: 'long', value: ->(account) { account.passive_relationships.count } + field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at } + end + end +end diff --git a/app/models/account.rb b/app/models/account.rb index 60c06aaf0..392cc625f 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -127,6 +127,8 @@ class Account < ApplicationRecord delegate :chosen_languages, to: :user, prefix: false, allow_nil: true + update_index('accounts#account', :self) if Chewy.enabled? + def local? domain.nil? end @@ -169,6 +171,10 @@ class Account < ApplicationRecord subscription_expires_at.present? end + def searchable? + !(suspended? || moved?) + end + def possibly_stale? last_webfingered_at.nil? || last_webfingered_at <= 1.day.ago end diff --git a/app/models/account_stat.rb b/app/models/account_stat.rb index 9813aa84f..6d1097cec 100644 --- a/app/models/account_stat.rb +++ b/app/models/account_stat.rb @@ -16,6 +16,8 @@ class AccountStat < ApplicationRecord belongs_to :account, inverse_of: :account_stat + update_index('accounts#account', :account) if Chewy.enabled? + def increment_count!(key) update(attributes_for_increment(key)) end diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb index e1874d045..2d602a31d 100644 --- a/app/services/account_search_service.rb +++ b/app/services/account_search_service.rb @@ -4,105 +4,134 @@ class AccountSearchService < BaseService attr_reader :query, :limit, :offset, :options, :account def call(query, account = nil, options = {}) - @query = query.strip - @limit = options[:limit].to_i - @offset = options[:offset].to_i - @options = options - @account = account + @acct_hint = query.start_with?('@') + @query = query.strip.gsub(/\A@/, '') + @limit = options[:limit].to_i + @offset = options[:offset].to_i + @options = options + @account = account - search_service_results + search_service_results.compact.uniq end private def search_service_results - return [] if query_blank_or_hashtag? || limit < 1 + return [] if query.blank? || limit < 1 - if resolving_non_matching_remote_account? - [ResolveAccountService.new.call("#{query_username}@#{query_domain}")].compact - else - search_results_and_exact_match.compact.uniq - end + [exact_match] + search_results end - def resolving_non_matching_remote_account? - offset.zero? && options[:resolve] && !exact_match? && !domain_is_local? - end + def exact_match + return unless offset.zero? && username_complete? - def search_results_and_exact_match - return search_results.to_a unless offset.zero? + return @exact_match if defined?(@exact_match) - results = [exact_match] + @exact_match = begin + if options[:resolve] + ResolveAccountService.new.call(query) + elsif domain_is_local? + Account.find_local(query_username) + else + Account.find_remote(query_username, query_domain) + end + end + end - return results if exact_match? && limit == 1 + def search_results + return [] if limit_for_non_exact_results.zero? - results + search_results.to_a + @search_results ||= begin + if Chewy.enabled? + from_elasticsearch + else + from_database + end + end end - def query_blank_or_hashtag? - query.blank? || query.start_with?('#') + def from_database + if account + advanced_search_results + else + simple_search_results + end end - def split_query_string - @split_query_string ||= query.gsub(/\A@/, '').split('@') + def advanced_search_results + Account.advanced_search_for(terms_for_query, account, limit_for_non_exact_results, options[:following], offset) end - def query_username - @query_username ||= split_query_string.first || '' + def simple_search_results + Account.search_for(terms_for_query, limit_for_non_exact_results, offset) end - def query_domain - @query_domain ||= query_without_split? ? nil : split_query_string.last - end + def from_elasticsearch + must_clauses = [{ multi_match: { query: terms_for_query, fields: likely_acct? ? %w(acct) : %w(acct^2 display_name), type: 'best_fields' } }] + should_clauses = [] - def query_without_split? - split_query_string.size == 1 - end + if account + return [] if options[:following] && following_ids.empty? - def domain_is_local? - @domain_is_local ||= TagManager.instance.local_domain?(query_domain) - end + if options[:following] + must_clauses << { terms: { id: following_ids } } + elsif following_ids.any? + should_clauses << { terms: { id: following_ids, boost: 100 } } + end + end - def search_from - options[:following] && account ? account.following : Account - end + query = { bool: { must: must_clauses, should: should_clauses } } + functions = [reputation_score_function, followers_score_function, time_distance_function] - def exact_match? - exact_match.present? - end + records = AccountsIndex.query(function_score: { query: query, functions: functions, boost_mode: 'multiply', score_mode: 'avg' }) + .limit(limit_for_non_exact_results) + .offset(offset) + .objects + .compact - def exact_match - return @exact_match if defined?(@exact_match) + ActiveRecord::Associations::Preloader.new.preload(records, :account_stat) - @exact_match = begin - if domain_is_local? - search_from.without_suspended.find_local(query_username) - else - search_from.without_suspended.find_remote(query_username, query_domain) - end - end + records end - def search_results - @search_results ||= begin - if account - advanced_search_results - else - simple_search_results - end - end + def reputation_score_function + { + script_score: { + script: { + source: "(doc['followers_count'].value + 0.0) / (doc['followers_count'].value + doc['following_count'].value + 1)", + }, + }, + } end - def advanced_search_results - Account.advanced_search_for(terms_for_query, account, limit_for_non_exact_results, options[:following], offset) + def followers_score_function + { + field_value_factor: { + field: 'followers_count', + modifier: 'log2p', + missing: 1, + }, + } end - def simple_search_results - Account.search_for(terms_for_query, limit_for_non_exact_results, offset) + def time_distance_function + { + gauss: { + last_status_at: { + scale: '30d', + offset: '30d', + decay: 0.3, + }, + }, + } + end + + def following_ids + @following_ids ||= account.active_relationships.pluck(:target_account_id) end def limit_for_non_exact_results - if offset.zero? && exact_match? + if exact_match? limit - 1 else limit @@ -113,7 +142,39 @@ class AccountSearchService < BaseService if domain_is_local? query_username else - "#{query_username} #{query_domain}" + query end end + + def split_query_string + @split_query_string ||= query.split('@') + end + + def query_username + @query_username ||= split_query_string.first || '' + end + + def query_domain + @query_domain ||= query_without_split? ? nil : split_query_string.last + end + + def query_without_split? + split_query_string.size == 1 + end + + def domain_is_local? + @domain_is_local ||= TagManager.instance.local_domain?(query_domain) + end + + def exact_match? + exact_match.present? + end + + def username_complete? + query.include?('@') && "@#{query}" =~ Account::MENTION_RE + end + + def likely_acct? + @acct_hint || username_complete? + end end diff --git a/spec/services/account_search_service_spec.rb b/spec/services/account_search_service_spec.rb index 7b071b378..5b7182586 100644 --- a/spec/services/account_search_service_spec.rb +++ b/spec/services/account_search_service_spec.rb @@ -1,126 +1,56 @@ require 'rails_helper' describe AccountSearchService, type: :service do - describe '.call' do - describe 'with a query to ignore' do + describe '#call' do + context 'with a query to ignore' do it 'returns empty array for missing query' do results = subject.call('', nil, limit: 10) expect(results).to eq [] end - it 'returns empty array for hashtag query' do - results = subject.call('#tag', nil, limit: 10) - expect(results).to eq [] - end it 'returns empty array for limit zero' do Fabricate(:account, username: 'match') + results = subject.call('match', nil, limit: 0) expect(results).to eq [] end end - describe 'searching for a simple term that is not an exact match' do + context 'searching for a simple term that is not an exact match' do it 'does not return a nil entry in the array for the exact match' do - match = Fabricate(:account, username: 'matchingusername') - + account = Fabricate(:account, username: 'matchingusername') results = subject.call('match', nil, limit: 5) - expect(results).to eq [match] - end - end - describe 'searching local and remote users' do - describe "when only '@'" do - before do - allow(Account).to receive(:find_local) - allow(Account).to receive(:search_for) - subject.call('@', nil, limit: 10) - end - - it 'uses find_local with empty query to look for local accounts' do - expect(Account).to have_received(:find_local).with('') - end - end - - describe 'when no domain' do - before do - allow(Account).to receive(:find_local) - allow(Account).to receive(:search_for) - subject.call('one', nil, limit: 10) - end - - it 'uses find_local to look for local accounts' do - expect(Account).to have_received(:find_local).with('one') - end - - it 'uses search_for to find matches' do - expect(Account).to have_received(:search_for).with('one', 10, 0) - end - end - - describe 'when there is a domain' do - before do - allow(Account).to receive(:find_remote) - end - - it 'uses find_remote to look for remote accounts' do - subject.call('two@example.com', nil, limit: 10) - expect(Account).to have_received(:find_remote).with('two', 'example.com') - end - - describe 'and there is no account provided' do - it 'uses search_for to find matches' do - allow(Account).to receive(:search_for) - subject.call('two@example.com', nil, limit: 10, resolve: false) - - expect(Account).to have_received(:search_for).with('two example.com', 10, 0) - end - end - - describe 'and there is an account provided' do - it 'uses advanced_search_for to find matches' do - account = Fabricate(:account) - allow(Account).to receive(:advanced_search_for) - subject.call('two@example.com', account, limit: 10, resolve: false) - - expect(Account).to have_received(:advanced_search_for).with('two example.com', account, 10, nil, 0) - end - end + expect(results).to eq [account] end end - describe 'with an exact match' do - it 'returns exact match first, and does not return duplicates' do - partial = Fabricate(:account, username: 'exactness') - exact = Fabricate(:account, username: 'exact') - - results = subject.call('exact', nil, limit: 10) - expect(results.size).to eq 2 - expect(results).to eq [exact, partial] - end - end - - describe 'when there is a local domain' do + context 'when there is a local domain' do around do |example| before = Rails.configuration.x.local_domain + example.run + Rails.configuration.x.local_domain = before end it 'returns exact match first' do remote = Fabricate(:account, username: 'a', domain: 'remote', display_name: 'e') remote_too = Fabricate(:account, username: 'b', domain: 'remote', display_name: 'e') - exact = Fabricate(:account, username: 'e') + exact = Fabricate(:account, username: 'e') + Rails.configuration.x.local_domain = 'example.com' results = subject.call('e@example.com', nil, limit: 2) + expect(results.size).to eq 2 expect(results).to eq([exact, remote]).or eq([exact, remote_too]) end end - describe 'when there is a domain but no exact match' do + context 'when there is a domain but no exact match' do it 'follows the remote account when resolve is true' do service = double(call: nil) allow(ResolveAccountService).to receive(:new).and_return(service) @@ -138,23 +68,21 @@ describe AccountSearchService, type: :service do end end - describe 'should not include suspended accounts' do - it 'returns the fuzzy match first, and does not return suspended exacts' do - partial = Fabricate(:account, username: 'exactness') - exact = Fabricate(:account, username: 'exact', suspended: true) + it 'returns the fuzzy match first, and does not return suspended exacts' do + partial = Fabricate(:account, username: 'exactness') + exact = Fabricate(:account, username: 'exact', suspended: true) + results = subject.call('exact', nil, limit: 10) - results = subject.call('exact', nil, limit: 10) - expect(results.size).to eq 1 - expect(results).to eq [partial] - end + expect(results.size).to eq 1 + expect(results).to eq [partial] + end - it "does not return suspended remote accounts" do - remote = Fabricate(:account, username: 'a', domain: 'remote', display_name: 'e', suspended: true) + it "does not return suspended remote accounts" do + remote = Fabricate(:account, username: 'a', domain: 'remote', display_name: 'e', suspended: true) + results = subject.call('a@example.com', nil, limit: 2) - results = subject.call('a@example.com', nil, limit: 2) - expect(results.size).to eq 0 - expect(results).to eq [] - end + expect(results.size).to eq 0 + expect(results).to eq [] end end end -- cgit From 18b451c0e6cf6a927a22084f94b423982de0ee8b Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 27 Sep 2019 21:13:51 +0200 Subject: Change silences to always require approval on follow (#11975) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change silenced accounts to require approval on follow * Also require approval for follows by people explicitly muted by target accounts * Do not auto-accept silenced or muted accounts when switching from locked to unlocked * Add `follow_requests_count` to verify_credentials * Show “Follow requests” menu item if needed even if account is locked * Add tests * Correctly reflect that follow requests weren't auto-accepted when local account is silenced * Accept follow requests from user-muted accounts to avoid leaking mutes --- app/controllers/api/v1/accounts_controller.rb | 2 +- .../mastodon/features/getting_started/index.js | 8 ++--- app/lib/activitypub/activity/follow.rb | 2 +- .../rest/credential_account_serializer.rb | 1 + app/services/follow_service.rb | 2 +- app/services/update_account_service.rb | 4 ++- spec/lib/activitypub/activity/follow_spec.rb | 30 +++++++++++++++++ spec/services/follow_service_spec.rb | 27 +++++++++++++++ spec/services/update_account_service_spec.rb | 38 ++++++++++++++++++++++ 9 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 spec/services/update_account_service_spec.rb (limited to 'spec/services') diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index b306e8e8c..c12e1c12e 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -33,7 +33,7 @@ class Api::V1::AccountsController < Api::BaseController def follow FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs)) - options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } } + options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } } render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options) end diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index f6d90580b..67ec7665b 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -77,16 +77,14 @@ class GettingStarted extends ImmutablePureComponent { }; componentDidMount () { - const { myAccount, fetchFollowRequests, multiColumn } = this.props; + const { fetchFollowRequests, multiColumn } = this.props; if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) { this.context.router.history.replace('/timelines/home'); return; } - if (myAccount.get('locked')) { - fetchFollowRequests(); - } + fetchFollowRequests(); } render () { @@ -134,7 +132,7 @@ class GettingStarted extends ImmutablePureComponent { height += 48*3; - if (myAccount.get('locked')) { + if (myAccount.get('locked') || unreadFollowRequests > 0) { navItems.push(); height += 48; } diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb index 28f1da19f..ec92f4255 100644 --- a/app/lib/activitypub/activity/follow.rb +++ b/app/lib/activitypub/activity/follow.rb @@ -21,7 +21,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id']) - if target_account.locked? + if target_account.locked? || @account.silenced? NotifyService.new.call(target_account, follow_request) else AuthorizeFollowService.new.call(@account, target_account) diff --git a/app/serializers/rest/credential_account_serializer.rb b/app/serializers/rest/credential_account_serializer.rb index fb195eb07..be0d763dc 100644 --- a/app/serializers/rest/credential_account_serializer.rb +++ b/app/serializers/rest/credential_account_serializer.rb @@ -12,6 +12,7 @@ class REST::CredentialAccountSerializer < REST::AccountSerializer language: user.setting_default_language, note: object.note, fields: object.fields.map(&:to_h), + follow_requests_count: FollowRequest.where(target_account: object).limit(40).count, } end end diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 101acdaf9..1941c2e2d 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -30,7 +30,7 @@ class FollowService < BaseService ActivityTracker.increment('activity:interactions') - if target_account.locked? || target_account.activitypub? + if target_account.locked? || source_account.silenced? || target_account.activitypub? request_follow(source_account, target_account, reblogs: reblogs) elsif target_account.local? direct_follow(source_account, target_account, reblogs: reblogs) diff --git a/app/services/update_account_service.rb b/app/services/update_account_service.rb index 01756a73d..ebf24be37 100644 --- a/app/services/update_account_service.rb +++ b/app/services/update_account_service.rb @@ -20,7 +20,9 @@ class UpdateAccountService < BaseService private def authorize_all_follow_requests(account) - AuthorizeFollowWorker.push_bulk(FollowRequest.where(target_account: account).select(:account_id, :target_account_id)) do |req| + follow_requests = FollowRequest.where(target_account: account) + follow_requests = follow_requests.select { |req| !req.account.silenced? } + AuthorizeFollowWorker.push_bulk(follow_requests) do |req| [req.account_id, req.target_account_id] end end diff --git a/spec/lib/activitypub/activity/follow_spec.rb b/spec/lib/activitypub/activity/follow_spec.rb index 6bbacdbe6..05112cc18 100644 --- a/spec/lib/activitypub/activity/follow_spec.rb +++ b/spec/lib/activitypub/activity/follow_spec.rb @@ -31,6 +31,36 @@ RSpec.describe ActivityPub::Activity::Follow do end end + context 'silenced account following an unlocked account' do + before do + sender.touch(:silenced_at) + subject.perform + end + + it 'does not create a follow from sender to recipient' do + expect(sender.following?(recipient)).to be false + end + + it 'creates a follow request' do + expect(sender.requested?(recipient)).to be true + end + end + + context 'unlocked account muting the sender' do + before do + recipient.mute!(sender) + subject.perform + end + + it 'creates a follow from sender to recipient' do + expect(sender.following?(recipient)).to be true + end + + it 'does not create a follow request' do + expect(sender.requested?(recipient)).to be false + end + end + context 'locked account' do before do recipient.update(locked: true) diff --git a/spec/services/follow_service_spec.rb b/spec/services/follow_service_spec.rb index 86c85293e..ae863a9f0 100644 --- a/spec/services/follow_service_spec.rb +++ b/spec/services/follow_service_spec.rb @@ -30,6 +30,33 @@ RSpec.describe FollowService, type: :service do end end + describe 'unlocked account, from silenced account' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } + + before do + sender.touch(:silenced_at) + subject.call(sender, bob.acct) + end + + it 'creates a follow request with reblogs' do + expect(FollowRequest.find_by(account: sender, target_account: bob, show_reblogs: true)).to_not be_nil + end + end + + describe 'unlocked account, from a muted account' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } + + before do + bob.mute!(sender) + subject.call(sender, bob.acct) + end + + it 'creates a following relation with reblogs' do + expect(sender.following?(bob)).to be true + expect(sender.muting_reblogs?(bob)).to be false + end + end + describe 'unlocked account' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } diff --git a/spec/services/update_account_service_spec.rb b/spec/services/update_account_service_spec.rb new file mode 100644 index 000000000..960b26891 --- /dev/null +++ b/spec/services/update_account_service_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +RSpec.describe UpdateAccountService, type: :service do + subject { UpdateAccountService.new } + + describe 'switching form locked to unlocked accounts' do + let(:account) { Fabricate(:account, locked: true) } + let(:alice) { Fabricate(:user, email: 'alice@example.com', account: Fabricate(:account, username: 'alice')).account } + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } + let(:eve) { Fabricate(:user, email: 'eve@example.com', account: Fabricate(:account, username: 'eve')).account } + + before do + bob.touch(:silenced_at) + account.mute!(eve) + + FollowService.new.call(alice, account) + FollowService.new.call(bob, account) + FollowService.new.call(eve, account) + + subject.call(account, { locked: false }) + end + + it 'auto-accepts pending follow requests' do + expect(alice.following?(account)).to be true + expect(alice.requested?(account)).to be false + end + + it 'does not auto-accept pending follow requests from silenced users' do + expect(bob.following?(account)).to be false + expect(bob.requested?(account)).to be true + end + + it 'auto-accepts pending follow requests from muted users so as to not leak mute' do + expect(eve.following?(account)).to be true + expect(eve.requested?(account)).to be false + end + end +end -- cgit From ab33c4df942ec3fdc4d891f3db7ac8cdd3436945 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 28 Sep 2019 01:02:21 +0200 Subject: Add `exclude_unreviewed` param to `GET /api/v2/search` REST API (#11977) Make it so normal search returns even unreviewed matches, but autosuggestions do not. Fix #11960 --- app/controllers/api/v2/search_controller.rb | 2 +- app/javascript/mastodon/actions/compose.js | 1 + app/models/tag.rb | 13 ++++++------- app/services/search_service.rb | 3 ++- app/services/tag_search_service.rb | 16 ++++++++++------ spec/services/search_service_spec.rb | 4 ++-- 6 files changed, 22 insertions(+), 17 deletions(-) (limited to 'spec/services') diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb index c14cd22d7..cbd9b551d 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -22,7 +22,7 @@ class Api::V2::SearchController < Api::BaseController params[:q], current_account, limit_param(RESULTS_LIMIT), - search_params.merge(resolve: truthy_param?(:resolve)) + search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed)) ) end diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 79d64dc3e..8e7906c73 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -369,6 +369,7 @@ const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => { q: token.slice(1), resolve: false, limit: 4, + exclude_unreviewed: true, }, }).then(({ data }) => { dispatch(readyComposeSuggestionsTags(token, data.hashtags)); diff --git a/app/models/tag.rb b/app/models/tag.rb index b52b9bc9f..9aca3983f 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -124,16 +124,15 @@ class Tag < ApplicationRecord end end - def search_for(term, limit = 5, offset = 0) + def search_for(term, limit = 5, offset = 0, options = {}) normalized_term = normalize(term.strip).mb_chars.downcase.to_s pattern = sanitize_sql_like(normalized_term) + '%' + query = Tag.listable.where(arel_table[:name].lower.matches(pattern)) + query = query.where(arel_table[:name].lower.eq(normalized_term).or(arel_table[:reviewed_at].not_eq(nil))) if options[:exclude_unreviewed] - Tag.listable - .where(arel_table[:name].lower.matches(pattern)) - .where(arel_table[:name].lower.eq(normalized_term).or(arel_table[:reviewed_at].not_eq(nil))) - .order(Arel.sql('length(name) ASC, name ASC')) - .limit(limit) - .offset(offset) + query.order(Arel.sql('length(name) ASC, name ASC')) + .limit(limit) + .offset(offset) end def find_normalized(name) diff --git a/app/services/search_service.rb b/app/services/search_service.rb index a5ba5dd11..3a498dcf4 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -60,7 +60,8 @@ class SearchService < BaseService TagSearchService.new.call( @query, limit: @limit, - offset: @offset + offset: @offset, + exclude_unreviewed: @options[:exclude_unreviewed] ) end diff --git a/app/services/tag_search_service.rb b/app/services/tag_search_service.rb index 47b0e876e..b78d65625 100644 --- a/app/services/tag_search_service.rb +++ b/app/services/tag_search_service.rb @@ -2,11 +2,12 @@ class TagSearchService < BaseService def call(query, options = {}) - @query = query.strip.gsub(/\A#/, '') - @offset = options[:offset].to_i - @limit = options[:limit].to_i + @query = query.strip.gsub(/\A#/, '') + @offset = options.delete(:offset).to_i + @limit = options.delete(:limit).to_i + @options = options - results = from_elasticsearch if Chewy.enabled? + results = from_elasticsearch if Chewy.enabled? results ||= from_database results @@ -72,12 +73,15 @@ class TagSearchService < BaseService }, } - TagsIndex.query(query).filter(filter).limit(@limit).offset(@offset).objects.compact + definition = TagsIndex.query(query) + definition = definition.filter(filter) if @options[:exclude_unreviewed] + + definition.limit(@limit).offset(@offset).objects.compact rescue Faraday::ConnectionFailed, Parslet::ParseFailed nil end def from_database - Tag.search_for(@query, @limit, @offset) + Tag.search_for(@query, @limit, @offset, @options) end end diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index ade306ed2..739bb9cf5 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -77,10 +77,10 @@ describe SearchService, type: :service do it 'includes the tag in the results' do query = '#tag' tag = Tag.new - allow(Tag).to receive(:search_for).with('tag', 10, 0).and_return([tag]) + allow(Tag).to receive(:search_for).with('tag', 10, 0, exclude_unreviewed: nil).and_return([tag]) results = subject.call(query, nil, 10) - expect(Tag).to have_received(:search_for).with('tag', 10, 0) + expect(Tag).to have_received(:search_for).with('tag', 10, 0, exclude_unreviewed: nil) expect(results).to eq empty_results.merge(hashtags: [tag]) end it 'does not include tag when starts with @ character' do -- cgit From 9ba40a6bfdb65f6e48eb7de07b2d55314c54fa83 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 1 Oct 2019 04:54:10 +0200 Subject: Remove HEAD request from fetching link previews (#12028) It is not really necessary and we need to reduce requests --- app/services/fetch_link_card_service.rb | 6 ------ spec/services/fetch_link_card_service_spec.rb | 13 +++---------- 2 files changed, 3 insertions(+), 16 deletions(-) (limited to 'spec/services') diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index ac5503d46..f0b1169db 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -39,12 +39,6 @@ class FetchLinkCardService < BaseService def process_url @card ||= PreviewCard.new(url: @url) - failed = Request.new(:head, @url).perform do |res| - res.code != 405 && res.code != 501 && (res.code != 200 || res.mime_type != 'text/html') - end - - return if failed - Request.new(:get, @url).perform do |res| if res.code == 200 && res.mime_type == 'text/html' @html = res.body_with_limit diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb index 50c60aafd..9761c5f06 100644 --- a/spec/services/fetch_link_card_service_spec.rb +++ b/spec/services/fetch_link_card_service_spec.rb @@ -4,20 +4,13 @@ RSpec.describe FetchLinkCardService, type: :service do subject { FetchLinkCardService.new } before do - stub_request(:head, 'http://example.xn--fiqs8s/').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) stub_request(:get, 'http://example.xn--fiqs8s/').to_return(request_fixture('idn.txt')) - stub_request(:head, 'http://example.com/sjis').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) stub_request(:get, 'http://example.com/sjis').to_return(request_fixture('sjis.txt')) - stub_request(:head, 'http://example.com/sjis_with_wrong_charset').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) stub_request(:get, 'http://example.com/sjis_with_wrong_charset').to_return(request_fixture('sjis_with_wrong_charset.txt')) - stub_request(:head, 'http://example.com/koi8-r').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) stub_request(:get, 'http://example.com/koi8-r').to_return(request_fixture('koi8-r.txt')) - stub_request(:head, 'http://example.com/日本語').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) stub_request(:get, 'http://example.com/日本語').to_return(request_fixture('sjis.txt')) - stub_request(:head, 'https://github.com/qbi/WannaCry').to_return(status: 404) - stub_request(:head, 'http://example.com/test-').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) + stub_request(:get, 'https://github.com/qbi/WannaCry').to_return(status: 404) stub_request(:get, 'http://example.com/test-').to_return(request_fixture('idn.txt')) - stub_request(:head, 'http://example.com/windows-1251').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) stub_request(:get, 'http://example.com/windows-1251').to_return(request_fixture('windows-1251.txt')) subject.call(status) @@ -90,11 +83,11 @@ RSpec.describe FetchLinkCardService, type: :service do let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: 'Habt ihr ein paar gute Links zu #Wannacry herumfliegen? Ich will mal unter
https://github.com/qbi/WannaCry was sammeln. !security ') } it 'parses out URLs' do - expect(a_request(:head, 'https://github.com/qbi/WannaCry')).to have_been_made.at_least_once + expect(a_request(:get, 'https://github.com/qbi/WannaCry')).to have_been_made.at_least_once end it 'ignores URLs to hashtags' do - expect(a_request(:head, 'https://quitter.se/tag/wannacry')).to_not have_been_made + expect(a_request(:get, 'https://quitter.se/tag/wannacry')).to_not have_been_made end end end -- cgit From 3a4d994c40962a9abe45565a34e3d7a3ca1ccd49 Mon Sep 17 00:00:00 2001 From: ThibG Date: Tue, 1 Oct 2019 15:10:00 +0200 Subject: Fix BootstrapTimelineService crashing when bootstrapped accounts are invalid (#12037) * Add test to handle suspended and missing users in BootstrapTimelineService * Fix BootstrapTimelineService crashing when bootstrapped accounts are invalid --- app/services/bootstrap_timeline_service.rb | 8 +++++++- spec/services/bootstrap_timeline_service_spec.rb | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) (limited to 'spec/services') diff --git a/app/services/bootstrap_timeline_service.rb b/app/services/bootstrap_timeline_service.rb index db2c83e5d..c489601c1 100644 --- a/app/services/bootstrap_timeline_service.rb +++ b/app/services/bootstrap_timeline_service.rb @@ -17,7 +17,11 @@ class BootstrapTimelineService < BaseService def autofollow_bootstrap_timeline_accounts! bootstrap_timeline_accounts.each do |target_account| - FollowService.new.call(@source_account, target_account) + begin + FollowService.new.call(@source_account, target_account) + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError + nil + end end end @@ -40,7 +44,9 @@ class BootstrapTimelineService < BaseService def local_unlocked_accounts(usernames) Account.local + .without_suspended .where(username: usernames) .where(locked: false) + .where(moved_to_account_id: nil) end end diff --git a/spec/services/bootstrap_timeline_service_spec.rb b/spec/services/bootstrap_timeline_service_spec.rb index a765de791..a28d2407c 100644 --- a/spec/services/bootstrap_timeline_service_spec.rb +++ b/spec/services/bootstrap_timeline_service_spec.rb @@ -22,9 +22,10 @@ RSpec.describe BootstrapTimelineService, type: :service do context 'when setting is set' do let!(:alice) { Fabricate(:account, username: 'alice') } let!(:bob) { Fabricate(:account, username: 'bob') } + let!(:eve) { Fabricate(:account, username: 'eve', suspended: true) } before do - Setting.bootstrap_timeline_accounts = 'alice, bob' + Setting.bootstrap_timeline_accounts = 'alice, @bob, eve, unknown' subject.call(source_account) end @@ -32,6 +33,10 @@ RSpec.describe BootstrapTimelineService, type: :service do expect(source_account.following?(alice)).to be true expect(source_account.following?(bob)).to be true end + + it 'does not follow suspended account' do + expect(source_account.following?(eve)).to be false + end end end end -- cgit From eb83d6256e6dc5d9c93dcb2b0a55e1061fee735f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 3 Oct 2019 17:50:59 +0200 Subject: Add `reason` param to `POST /api/v1/accounts` REST API (#12064) For approval-required registrations mode --- app/controllers/api/v1/accounts_controller.rb | 2 +- app/services/app_sign_up_service.rb | 7 ++++--- spec/services/app_sign_up_service_spec.rb | 9 +++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) (limited to 'spec/services') diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index c12e1c12e..d68d2715f 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -78,7 +78,7 @@ class Api::V1::AccountsController < Api::BaseController end def account_params - params.permit(:username, :email, :password, :agreement, :locale) + params.permit(:username, :email, :password, :agreement, :locale, :reason) end def check_enabled_registrations diff --git a/app/services/app_sign_up_service.rb b/app/services/app_sign_up_service.rb index 6dee9cd81..c9739c77d 100644 --- a/app/services/app_sign_up_service.rb +++ b/app/services/app_sign_up_service.rb @@ -4,9 +4,10 @@ class AppSignUpService < BaseService def call(app, params) return unless allowed_registrations? - user_params = params.slice(:email, :password, :agreement, :locale) - account_params = params.slice(:username) - user = User.create!(user_params.merge(created_by_application: app, password_confirmation: user_params[:password], account_attributes: account_params)) + user_params = params.slice(:email, :password, :agreement, :locale) + account_params = params.slice(:username) + invite_request_params = { text: params[:reason] } + user = User.create!(user_params.merge(created_by_application: app, password_confirmation: user_params[:password], account_attributes: account_params, invite_request_attributes: invite_request_params)) Doorkeeper::AccessToken.create!(application: app, resource_owner_id: user.id, diff --git a/spec/services/app_sign_up_service_spec.rb b/spec/services/app_sign_up_service_spec.rb index 7948bb53b..e7c7f3ba1 100644 --- a/spec/services/app_sign_up_service_spec.rb +++ b/spec/services/app_sign_up_service_spec.rb @@ -38,6 +38,15 @@ RSpec.describe AppSignUpService, type: :service do user = User.find_by(id: access_token.resource_owner_id) expect(user).to_not be_nil expect(user.account).to_not be_nil + expect(user.invite_request).to be_nil + end + + it 'creates an account with invite request text' do + access_token = subject.call(app, good_params.merge(reason: 'Foo bar')) + expect(access_token).to_not be_nil + user = User.find_by(id: access_token.resource_owner_id) + expect(user).to_not be_nil + expect(user.invite_request&.text).to eq 'Foo bar' end end end -- cgit From fccf83e1f2ecd4e23f7b1faee5330976d17da7b8 Mon Sep 17 00:00:00 2001 From: BSKY Date: Fri, 25 Oct 2019 05:44:42 +0900 Subject: Add noopener and/or noreferrer (#12202) --- app/javascript/mastodon/components/attachment_list.js | 4 ++-- app/javascript/mastodon/components/dropdown_menu.js | 2 +- app/javascript/mastodon/components/error_boundary.js | 2 +- app/javascript/mastodon/components/media_gallery.js | 3 ++- app/javascript/mastodon/components/status.js | 4 ++-- app/javascript/mastodon/components/status_content.js | 2 +- .../mastodon/features/account/components/header.js | 6 +++--- .../features/account_gallery/components/media_item.js | 14 +++++++------- app/javascript/mastodon/features/status/components/card.js | 6 +++--- .../mastodon/features/status/components/detailed_status.js | 4 ++-- .../mastodon/features/ui/components/actions_modal.js | 4 ++-- .../mastodon/features/ui/components/boost_modal.js | 2 +- .../mastodon/features/ui/components/link_footer.js | 2 +- app/lib/formatter.rb | 2 +- app/lib/sanitize_config.rb | 2 +- app/views/about/show.html.haml | 2 +- app/views/accounts/_moved.html.haml | 2 +- app/views/admin/reports/_status.html.haml | 2 +- app/views/admin/tags/show.html.haml | 2 +- app/views/application/_card.html.haml | 2 +- app/views/oauth/authorized_applications/index.html.haml | 2 +- app/views/statuses/_detailed_status.html.haml | 4 ++-- app/views/statuses/_simple_status.html.haml | 4 ++-- spec/fixtures/xml/mastodon.atom | 4 ++-- spec/lib/sanitize_config_spec.rb | 2 +- spec/services/fetch_link_card_service_spec.rb | 2 +- spec/services/verify_link_service_spec.rb | 4 ++-- 27 files changed, 46 insertions(+), 45 deletions(-) (limited to 'spec/services') diff --git a/app/javascript/mastodon/components/attachment_list.js b/app/javascript/mastodon/components/attachment_list.js index 5dfa1464c..ebd696583 100644 --- a/app/javascript/mastodon/components/attachment_list.js +++ b/app/javascript/mastodon/components/attachment_list.js @@ -25,7 +25,7 @@ export default class AttachmentList extends ImmutablePureComponent { return (
  • - {filename(displayUrl)} + {filename(displayUrl)}
  • ); })} @@ -46,7 +46,7 @@ export default class AttachmentList extends ImmutablePureComponent { return (
  • - {filename(displayUrl)} + {filename(displayUrl)}
  • ); })} diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index d423378c1..a4f262285 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -143,7 +143,7 @@ class DropdownMenu extends React.PureComponent { return (
  • - + {text}
  • diff --git a/app/javascript/mastodon/components/error_boundary.js b/app/javascript/mastodon/components/error_boundary.js index 82543e118..800b1c270 100644 --- a/app/javascript/mastodon/components/error_boundary.js +++ b/app/javascript/mastodon/components/error_boundary.js @@ -58,7 +58,7 @@ export default class ErrorBoundary extends React.PureComponent {

    -

    Mastodon v{version} · ·

    +

    Mastodon v{version} · ·

    ); diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index e8dd79af9..b8fca8bcb 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -159,7 +159,7 @@ class Item extends React.PureComponent { if (attachment.get('type') === 'unknown') { return ( @@ -187,6 +187,7 @@ class Item extends React.PureComponent { href={attachment.get('remote_url') || originalUrl} onClick={this.handleClick} target='_blank' + rel='noopener noreferrer' >
    - + - +
    {statusAvatar}
    diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js index 4ce9ec49f..d13091325 100644 --- a/app/javascript/mastodon/components/status_content.js +++ b/app/javascript/mastodon/components/status_content.js @@ -59,7 +59,7 @@ export default class StatusContent extends React.PureComponent { } link.setAttribute('target', '_blank'); - link.setAttribute('rel', 'noopener'); + link.setAttribute('rel', 'noopener noreferrer'); } if ( diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index ac97bad71..dbb567e85 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -253,7 +253,7 @@ class Header extends ImmutablePureComponent {
    - + @@ -282,10 +282,10 @@ class Header extends ImmutablePureComponent {
    - + - +
    ))} diff --git a/app/javascript/mastodon/features/account_gallery/components/media_item.js b/app/javascript/mastodon/features/account_gallery/components/media_item.js index b6eec2243..617a45d16 100644 --- a/app/javascript/mastodon/features/account_gallery/components/media_item.js +++ b/app/javascript/mastodon/features/account_gallery/components/media_item.js @@ -1,12 +1,12 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +import { decode } from 'blurhash'; +import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; import { autoPlayGif, displayMedia } from 'mastodon/initial_state'; -import classNames from 'classnames'; -import { decode } from 'blurhash'; import { isIOS } from 'mastodon/is_mobile'; +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; export default class MediaItem extends ImmutablePureComponent { @@ -151,7 +151,7 @@ export default class MediaItem extends ImmutablePureComponent { return (
    - + {visible && thumbnail} {!visible && icon} diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js index 0eff54411..2993fe29a 100644 --- a/app/javascript/mastodon/features/status/components/card.js +++ b/app/javascript/mastodon/features/status/components/card.js @@ -148,7 +148,7 @@ export default class Card extends React.PureComponent { const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded; const interactive = card.get('type') !== 'link'; const className = classnames('status-card', { horizontal, compact, interactive }); - const title = interactive ? {card.get('title')} : {card.get('title')}; + const title = interactive ? {card.get('title')} : {card.get('title')}; const ratio = card.get('width') / card.get('height'); const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio); @@ -180,7 +180,7 @@ export default class Card extends React.PureComponent {
    - {horizontal && } + {horizontal && }
    @@ -208,7 +208,7 @@ export default class Card extends React.PureComponent { } return ( - + {embed} {description} diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index e97f18f08..d5bc82735 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -156,7 +156,7 @@ export default class DetailedStatus extends ImmutablePureComponent { } if (status.get('application')) { - applicationLink = · {status.getIn(['application', 'name'])}; + applicationLink = · {status.getIn(['application', 'name'])}; } if (status.get('visibility') === 'direct') { @@ -220,7 +220,7 @@ export default class DetailedStatus extends ImmutablePureComponent { {media}
    - + {applicationLink} · {reblogLink} · {favouriteLink}
    diff --git a/app/javascript/mastodon/features/ui/components/actions_modal.js b/app/javascript/mastodon/features/ui/components/actions_modal.js index 00280f7a6..875b2b75d 100644 --- a/app/javascript/mastodon/features/ui/components/actions_modal.js +++ b/app/javascript/mastodon/features/ui/components/actions_modal.js @@ -26,7 +26,7 @@ export default class ActionsModal extends ImmutablePureComponent { return (
  • - + {icon && }
    {text}
    @@ -42,7 +42,7 @@ export default class ActionsModal extends ImmutablePureComponent {
    diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.js b/app/javascript/mastodon/features/ui/components/boost_modal.js index 70f4a1282..0e79005f0 100644 --- a/app/javascript/mastodon/features/ui/components/boost_modal.js +++ b/app/javascript/mastodon/features/ui/components/boost_modal.js @@ -61,7 +61,7 @@ class BoostModal extends ImmutablePureComponent {
    diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 990b9f63e..6ba327614 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -251,7 +251,7 @@ class Formatter def link_to_url(entity, options = {}) url = Addressable::URI.parse(entity[:url]) - html_attrs = { target: '_blank', rel: 'nofollow noopener' } + html_attrs = { target: '_blank', rel: 'nofollow noopener noreferrer' } html_attrs[:rel] = "me #{html_attrs[:rel]}" if options[:me] diff --git a/app/lib/sanitize_config.rb b/app/lib/sanitize_config.rb index aba8ce9f6..77045155e 100644 --- a/app/lib/sanitize_config.rb +++ b/app/lib/sanitize_config.rb @@ -45,7 +45,7 @@ class Sanitize add_attributes: { 'a' => { - 'rel' => 'nofollow noopener', + 'rel' => 'nofollow noopener noreferrer', 'target' => '_blank', }, }, diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml index 80f4cd828..e0ec98ec9 100644 --- a/app/views/about/show.html.haml +++ b/app/views/about/show.html.haml @@ -38,7 +38,7 @@ %small= t('about.browse_public_posts') .directory__tag - = link_to 'https://joinmastodon.org/apps', target: '_blank', rel: 'noopener' do + = link_to 'https://joinmastodon.org/apps', target: '_blank', rel: 'noopener noreferrer' do %h4 = fa_icon 'tablet fw' = t('about.get_apps') diff --git a/app/views/accounts/_moved.html.haml b/app/views/accounts/_moved.html.haml index 02fd7bf42..a82f277b1 100644 --- a/app/views/accounts/_moved.html.haml +++ b/app/views/accounts/_moved.html.haml @@ -6,7 +6,7 @@ = 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 ActivityPub::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 noreferrer' 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/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml index 6facc0a56..425d315e1 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.proper.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 ActivityPub::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 noreferrer' do %time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at) - if status.discarded? · diff --git a/app/views/admin/tags/show.html.haml b/app/views/admin/tags/show.html.haml index 5799e5973..c9a147587 100644 --- a/app/views/admin/tags/show.html.haml +++ b/app/views/admin/tags/show.html.haml @@ -3,7 +3,7 @@ .dashboard__counters %div - = link_to tag_url(@tag), target: '_blank', rel: 'noopener' do + = link_to tag_url(@tag), target: '_blank', rel: 'noopener noreferrer' do .dashboard__counters__num= number_with_delimiter @accounts_today .dashboard__counters__label= t 'admin.tags.accounts_today' %div diff --git a/app/views/application/_card.html.haml b/app/views/application/_card.html.haml index 8719ce484..808dce514 100644 --- a/app/views/application/_card.html.haml +++ b/app/views/application/_card.html.haml @@ -1,7 +1,7 @@ - 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 + = link_to account_url, target: '_blank', rel: 'noopener noreferrer' do .card__img = image_tag account.header.url, alt: '' .card__bar diff --git a/app/views/oauth/authorized_applications/index.html.haml b/app/views/oauth/authorized_applications/index.html.haml index 7203d758d..7b77108a9 100644 --- a/app/views/oauth/authorized_applications/index.html.haml +++ b/app/views/oauth/authorized_applications/index.html.haml @@ -16,7 +16,7 @@ - if application.website.blank? = application.name - else - = link_to application.name, application.website, target: '_blank', rel: 'noopener' + = link_to application.name, application.website, target: '_blank', rel: 'noopener noreferrer' %th!= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join(', ') %td= l application.created_at %td diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml index 5cee84ada..3fa52d3f2 100644 --- a/app/views/statuses/_detailed_status.html.haml +++ b/app/views/statuses/_detailed_status.html.haml @@ -44,14 +44,14 @@ .detailed-status__meta %data.dt-published{ value: status.created_at.to_time.iso8601 } - = link_to ActivityPub::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 noreferrer' 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 - if status.application.website.blank? %strong.detailed-status__application= status.application.name - else - = link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener' + = link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener noreferrer' · = link_to remote_interaction_path(status, type: :reply), class: 'modal-button detailed-status__link' do - if status.in_reply_to_id.nil? diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml index a68fe1022..edcfbba2b 100644 --- a/app/views/statuses/_simple_status.html.haml +++ b/app/views/statuses/_simple_status.html.haml @@ -1,11 +1,11 @@ .status .status__info - = link_to ActivityPub::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 noreferrer' 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 ActivityPub::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 noreferrer' do .status__avatar %div - if current_account&.user&.setting_auto_play_gif || autoplay diff --git a/spec/fixtures/xml/mastodon.atom b/spec/fixtures/xml/mastodon.atom index 9ece3bc2e..92921a938 100644 --- a/spec/fixtures/xml/mastodon.atom +++ b/spec/fixtures/xml/mastodon.atom @@ -123,7 +123,7 @@ 2016-10-10T00:41:31Z 2016-10-10T00:41:31Z Social media needs MOAR cats! http://kickass.zone/media/3 - <p>Social media needs MOAR cats! <a rel="nofollow noopener" href="http://kickass.zone/media/3">http://kickass.zone/media/3</a></p> + <p>Social media needs MOAR cats! <a rel="nofollow noopener noreferrer" href="http://kickass.zone/media/3">http://kickass.zone/media/3</a></p> http://activitystrea.ms/schema/1.0/post @@ -135,7 +135,7 @@ 2016-10-10T00:38:39Z 2016-10-10T00:38:39Z http://kickass.zone/media/2 - <p><a rel="nofollow noopener" href="http://kickass.zone/media/2">http://kickass.zone/media/2</a></p> + <p><a rel="nofollow noopener noreferrer" href="http://kickass.zone/media/2">http://kickass.zone/media/2</a></p> http://activitystrea.ms/schema/1.0/post diff --git a/spec/lib/sanitize_config_spec.rb b/spec/lib/sanitize_config_spec.rb index 54bd8693c..feb86af35 100644 --- a/spec/lib/sanitize_config_spec.rb +++ b/spec/lib/sanitize_config_spec.rb @@ -24,7 +24,7 @@ describe Sanitize::Config do end it 'keep links in lists' do - expect(Sanitize.fragment('

    Check out:

    ', subject)).to eq '

    Check out:

    joinmastodon.org
    Bar

    ' + expect(Sanitize.fragment('

    Check out:

    ', subject)).to eq '

    Check out:

    joinmastodon.org
    Bar

    ' end end end diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb index 9761c5f06..3c8f6f578 100644 --- a/spec/services/fetch_link_card_service_spec.rb +++ b/spec/services/fetch_link_card_service_spec.rb @@ -80,7 +80,7 @@ RSpec.describe FetchLinkCardService, type: :service do end context 'in a remote status' do - let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: 'Habt ihr ein paar gute Links zu #Wannacry herumfliegen? Ich will mal unter
    https://github.com/qbi/WannaCry was sammeln. !security ') } + let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: 'Habt ihr ein paar gute Links zu #Wannacry herumfliegen? Ich will mal unter
    https://github.com/qbi/WannaCry was sammeln. !security ') } it 'parses out URLs' do expect(a_request(:get, 'https://github.com/qbi/WannaCry')).to have_been_made.at_least_once diff --git a/spec/services/verify_link_service_spec.rb b/spec/services/verify_link_service_spec.rb index 2edcdb75f..3fc88e60e 100644 --- a/spec/services/verify_link_service_spec.rb +++ b/spec/services/verify_link_service_spec.rb @@ -28,12 +28,12 @@ RSpec.describe VerifyLinkService, type: :service do end end - context 'when a link contains an back' do + context 'when a link contains an back' do let(:html) do <<-HTML - Follow me on Mastodon + Follow me on Mastodon HTML end -- cgit From d14e74eff52352f1a2fb4bc2053bbb28c1aa29e0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 17 Nov 2019 18:40:33 +0100 Subject: Add cache for OEmbed endpoints to avoid extra HTTP requests (#12403) * add youtube oembed endpoint * add check for oembed endpoint * change unless for a more readable if * clear blank lines * endpoint via https * Fix string literal in condition * use cache for endpoints * use cache for endpoints * clean up and adding check * clean up and remove redundant return * add html check * add false to return * use double quotes * use double quotes * Clean up --- app/services/fetch_link_card_service.rb | 23 +++++++++++++++------- app/services/fetch_oembed_service.rb | 31 +++++++++++++++++++++++++++++- spec/services/fetch_oembed_service_spec.rb | 18 +++++++++++++++++ 3 files changed, 64 insertions(+), 8 deletions(-) (limited to 'spec/services') diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 640c60fd4..29880e8d8 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -39,6 +39,12 @@ class FetchLinkCardService < BaseService def process_url @card ||= PreviewCard.new(url: @url) + attempt_oembed || attempt_opengraph + end + + def html + return @html if defined?(@html) + Request.new(:get, @url).perform do |res| if res.code == 200 && res.mime_type == 'text/html' @html = res.body_with_limit @@ -48,10 +54,6 @@ class FetchLinkCardService < BaseService @html_charset = nil end end - - return if @html.nil? - - attempt_oembed || attempt_opengraph end def attach_card @@ -88,12 +90,17 @@ class FetchLinkCardService < BaseService end def attempt_oembed - service = FetchOEmbedService.new - embed = service.call(@url, html: @html) - url = Addressable::URI.parse(service.endpoint_url) + service = FetchOEmbedService.new + url_domain = Addressable::URI.parse(@url).normalized_host + cached_endpoint = Rails.cache.read("oembed_endpoint:#{url_domain}") + + embed = service.call(@url, cached_endpoint: cached_endpoint) unless cached_endpoint.nil? + embed ||= service.call(@url, html: html) unless html.nil? return false if embed.nil? + url = Addressable::URI.parse(service.endpoint_url) + @card.type = embed[:type] @card.title = embed[:title] || '' @card.author_name = embed[:author_name] || '' @@ -127,6 +134,8 @@ class FetchLinkCardService < BaseService end def attempt_opengraph + return if html.nil? + detector = CharlockHolmes::EncodingDetector.new detector.strip_tags = true diff --git a/app/services/fetch_oembed_service.rb b/app/services/fetch_oembed_service.rb index 10176cfb9..4f8498c62 100644 --- a/app/services/fetch_oembed_service.rb +++ b/app/services/fetch_oembed_service.rb @@ -1,13 +1,20 @@ # frozen_string_literal: true class FetchOEmbedService + ENDPOINT_CACHE_EXPIRES_IN = 24.hours.freeze + attr_reader :url, :options, :format, :endpoint_url def call(url, options = {}) @url = url @options = options - discover_endpoint! + if @options[:cached_endpoint] + parse_cached_endpoint! + else + discover_endpoint! + end + fetch! end @@ -32,10 +39,32 @@ class FetchOEmbedService return if @endpoint_url.blank? @endpoint_url = (Addressable::URI.parse(@url) + @endpoint_url).to_s + + cache_endpoint! rescue Addressable::URI::InvalidURIError @endpoint_url = nil end + def parse_cached_endpoint! + cached = @options[:cached_endpoint] + + return if cached[:endpoint].nil? || cached[:format].nil? + + @endpoint_url = Addressable::Template.new(cached[:endpoint]).expand(url: @url).to_s + @format = cached[:format] + end + + def cache_endpoint! + url_domain = Addressable::URI.parse(@url).normalized_host + + endpoint_hash = { + endpoint: @endpoint_url.gsub(URI.encode_www_form_component(@url), '{url}'), + format: @format, + } + + Rails.cache.write("oembed_endpoint:#{url_domain}", endpoint_hash, expires_in: ENDPOINT_CACHE_EXPIRES_IN) + end + def fetch! return if @endpoint_url.blank? diff --git a/spec/services/fetch_oembed_service_spec.rb b/spec/services/fetch_oembed_service_spec.rb index 5789fb53b..a4262b040 100644 --- a/spec/services/fetch_oembed_service_spec.rb +++ b/spec/services/fetch_oembed_service_spec.rb @@ -113,6 +113,24 @@ describe FetchOEmbedService, type: :service do end + context 'when endpoint is cached' do + before do + stub_request(:get, 'http://www.youtube.com/oembed?format=json&url=https://www.youtube.com/watch?v=dqwpQarrDwk').to_return( + status: 200, + headers: { 'Content-Type': 'text/html' }, + body: request_fixture('oembed_json_empty.html') + ) + end + + it 'returns new provider without fetching original URL first' do + subject.call('https://www.youtube.com/watch?v=dqwpQarrDwk', cached_endpoint: { endpoint: 'http://www.youtube.com/oembed?format=json&url={url}', format: :json }) + expect(a_request(:get, 'https://www.youtube.com/watch?v=dqwpQarrDwk')).to_not have_been_made + expect(subject.endpoint_url).to eq 'http://www.youtube.com/oembed?format=json&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DdqwpQarrDwk' + expect(subject.format).to eq :json + expect(a_request(:get, 'http://www.youtube.com/oembed?format=json&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DdqwpQarrDwk')).to have_been_made + end + end + context 'when status code is not 200' do before do stub_request(:get, 'https://host.test/oembed.html').to_return( -- cgit From c656cc2191479551806d288bdfb6a30aa35bcb23 Mon Sep 17 00:00:00 2001 From: ThibG Date: Thu, 21 Nov 2019 16:04:52 +0100 Subject: Fix FetchLinkCardServices crashing on a tags without a target (#12159) * Add test for links without targets * Fix FetchLinkCardServices crashing on a tags without a target --- app/services/fetch_link_card_service.rb | 2 +- spec/services/fetch_link_card_service_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'spec/services') diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 29880e8d8..5d4a7c303 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -67,7 +67,7 @@ class FetchLinkCardService < BaseService else html = Nokogiri::HTML(@status.text) links = html.css('a') - urls = links.map { |a| Addressable::URI.parse(a['href']).normalize unless skip_link?(a) }.compact + urls = links.map { |a| Addressable::URI.parse(a['href']) unless skip_link?(a) }.compact.map(&:normalize).compact end urls.reject { |uri| bad_url?(uri) }.first diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb index 3c8f6f578..8b296cc70 100644 --- a/spec/services/fetch_link_card_service_spec.rb +++ b/spec/services/fetch_link_card_service_spec.rb @@ -80,7 +80,7 @@ RSpec.describe FetchLinkCardService, type: :service do end context 'in a remote status' do - let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: 'Habt ihr ein paar gute Links zu #Wannacry herumfliegen? Ich will mal unter
    https://github.com/qbi/WannaCry was sammeln. !security ') } + let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: 'Habt ihr ein paar gute Links zu foo #Wannacry herumfliegen? Ich will mal unter
    https://github.com/qbi/WannaCry was sammeln. !security ') } it 'parses out URLs' do expect(a_request(:get, 'https://github.com/qbi/WannaCry')).to have_been_made.at_least_once -- cgit