From 58cede4808baa4db6cc143b80ef23e8179a8415b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 18 Nov 2017 19:39:02 +0100 Subject: Profile redirect notes (#5746) * Serialize moved accounts into REST and ActivityPub APIs * Parse federated moved accounts from ActivityPub * Add note about moved accounts to public profiles * Add moved account message to web UI * Fix code style issues --- app/services/activitypub/process_account_service.rb | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'app/services') diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index f93baf4b5..5ee7d89ee 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -74,6 +74,7 @@ class ActivityPub::ProcessAccountService < BaseService @account.statuses_count = outbox_total_items if outbox_total_items.present? @account.following_count = following_total_items if following_total_items.present? @account.followers_count = followers_total_items if followers_total_items.present? + @account.moved_to_account = moved_account if @json['movedTo'].present? end def after_protocol_change! @@ -137,6 +138,12 @@ class ActivityPub::ProcessAccountService < BaseService @collections[type] = nil end + def moved_account + account = ActivityPub::TagManager.instance.uri_to_resource(@json['movedTo'], Account) + account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], id: true) + account + end + def skip_download? @account.suspended? || domain_block&.reject_media? end -- cgit From 2bcc81700c586a543e849e222c640df89da7fcb0 Mon Sep 17 00:00:00 2001 From: abcang Date: Sun, 19 Nov 2017 23:33:15 +0900 Subject: Fix NoMethodError at ActivityPub::FetchRemoteStatusService (#5753) --- app/services/activitypub/fetch_remote_status_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/services') diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index 8d7b7a17c..8de9283de 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -18,7 +18,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService 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) - return if actor.suspended? + return if actor.nil? || actor.suspended? ActivityPub::Activity.factory(activity_json, actor).perform end -- cgit From 3dce6cbbd77bc8b7907f94fd789452d91933877a Mon Sep 17 00:00:00 2001 From: abcang Date: Tue, 21 Nov 2017 04:45:54 +0900 Subject: Fixed duplicating URL of photo type of oEmbed (#5763) --- app/services/fetch_link_card_service.rb | 3 +++ 1 file changed, 3 insertions(+) (limited to 'app/services') diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 14c21b6cc..37cf75379 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -74,6 +74,9 @@ class FetchLinkCardService < BaseService return false unless response.respond_to?(:type) + # The photo will change the URL. So, to avoid duplication of URLs, PreviewCard needs to be checked again. + @card = PreviewCard.find_by(url: response.url) || @card if response.type == 'photo' + @card.type = response.type @card.title = response.respond_to?(:title) ? response.title : '' @card.author_name = response.respond_to?(:author_name) ? response.author_name : '' -- cgit From b8efb5daed9825323f17039b416527850d017ff2 Mon Sep 17 00:00:00 2001 From: ThibG Date: Tue, 28 Nov 2017 15:00:22 +0100 Subject: Fix handling of temporary failures in ProcessMentionsService (#5842) * Add test for temporary account resolving failures in ProcessMentionsService * Fix processing of mentions to already-known remote accounts on temporary failures --- app/services/process_mentions_service.rb | 2 +- spec/services/process_mentions_service_spec.rb | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) (limited to 'app/services') diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index a229d4ff8..e12721c46 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -18,7 +18,7 @@ class ProcessMentionsService < BaseService end if mentioned_account.nil? - username, domain = match.first.split('@') + username, domain = $1.split('@') mentioned_account = Account.find_remote(username, domain) end diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb index 09f8fa45b..19a8678f0 100644 --- a/spec/services/process_mentions_service_spec.rb +++ b/spec/services/process_mentions_service_spec.rb @@ -41,4 +41,25 @@ RSpec.describe ProcessMentionsService do expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once end end + + context 'Temporarily-unreachable ActivityPub user' do + let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox', last_webfingered_at: nil) } + + subject { ProcessMentionsService.new } + + before do + stub_request(:get, "https://example.com/.well-known/host-meta").to_return(status: 404) + stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:remote_user@example.com").to_return(status: 500) + stub_request(:post, remote_user.inbox_url) + subject.call(status) + end + + it 'creates a mention' do + expect(remote_user.mentions.where(status: status).count).to eq 1 + end + + it 'sends activity to the inbox' do + expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once + end + end end -- cgit From eeaec39888f66bf312ac9a4c58f315ffd8f874f2 Mon Sep 17 00:00:00 2001 From: aschmitz Date: Tue, 28 Nov 2017 08:00:35 -0600 Subject: Allow hiding of reblogs from followed users (#5762) * Allow hiding of reblogs from followed users This adds a new entry to the account menu to allow users to hide future reblogs from a user (and then if they've done that, to show future reblogs instead). This does not remove or add historical reblogs from/to the user's timeline; it only affects new statuses. The API for this operates by sending a "reblogs" key to the follow endpoint. If this is sent when starting a new follow, it will be respected from the beginning of the follow relationship (even if the follow request must be approved by the followee). If this is sent when a follow relationship already exists, it will simply update the existing follow relationship. As with the notification muting, this will now return an object ({reblogs: [true|false]}) or false for each follow relationship when requesting relationship information for an account. This should cause few issues due to an object being truthy in many languages, but some modifications may need to be made in pickier languages. Database changes: adds a show_reblogs column (default true, non-nullable) to the follows and follow_requests tables. Because these are non-nullable, we use the existing MigrationHelpers to perform this change without locking those tables, although the tables are likely to be small anyway. Tests included. See also . * Rubocop fixes * Code review changes * Test fixes This patchset closes #648 and resolves #3271. * Rubocop fix * Revert reblogs defaulting in argument, fix tests It turns out we needed this for the same reason we needed it in muting: if nil gets passed in somehow (most usually by an API client not passing any value), we need to detect and handle it. We could specify a default in the parameter and then also catch nil, but there's no great reason to duplicate the default value. --- app/controllers/api/v1/accounts_controller.rb | 4 +- app/javascript/mastodon/actions/accounts.js | 10 ++-- app/javascript/mastodon/components/account.js | 2 +- .../features/account/components/action_bar.js | 12 +++++ .../features/account_timeline/components/header.js | 6 +++ .../containers/header_container.js | 8 +++ .../mastodon/reducers/accounts_counters.js | 3 +- app/lib/feed_manager.rb | 15 +++--- app/models/concerns/account_interactions.rb | 24 +++++++-- app/models/follow.rb | 1 + app/models/follow_request.rb | 3 +- app/services/follow_service.rb | 29 ++++++++--- app/services/notify_service.rb | 2 +- .../20171028221157_add_reblogs_to_follows.rb | 19 +++++++ db/schema.rb | 2 + .../v1/accounts/relationships_controller_spec.rb | 4 +- .../controllers/api/v1/accounts_controller_spec.rb | 8 +-- spec/lib/feed_manager_spec.rb | 7 +++ spec/models/concerns/account_interactions_spec.rb | 39 ++++++++++++++- spec/models/follow_request_spec.rb | 16 +++++- spec/services/follow_service_spec.rb | 58 ++++++++++++++++++++-- spec/services/notify_service_spec.rb | 20 ++++++++ 22 files changed, 252 insertions(+), 40 deletions(-) create mode 100644 db/migrate/20171028221157_add_reblogs_to_follows.rb (limited to 'app/services') diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 4676f60de..b1a2ed573 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -13,9 +13,9 @@ class Api::V1::AccountsController < Api::BaseController end def follow - FollowService.new.call(current_user.account, @account.acct) + FollowService.new.call(current_user.account, @account.acct, reblogs: params[:reblogs]) - options = @account.locked? ? {} : { following_map: { @account.id => true }, requested_map: { @account.id => false } } + options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: params[:reblogs] } }, requested_map: { @account.id => false } } render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options) end diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index fbaebf786..f63325658 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -105,12 +105,13 @@ export function fetchAccountFail(id, error) { }; }; -export function followAccount(id) { +export function followAccount(id, reblogs = true) { return (dispatch, getState) => { + const alreadyFollowing = getState().getIn(['relationships', id, 'following']); dispatch(followAccountRequest(id)); - api(getState).post(`/api/v1/accounts/${id}/follow`).then(response => { - dispatch(followAccountSuccess(response.data)); + api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => { + dispatch(followAccountSuccess(response.data, alreadyFollowing)); }).catch(error => { dispatch(followAccountFail(error)); }); @@ -136,10 +137,11 @@ export function followAccountRequest(id) { }; }; -export function followAccountSuccess(relationship) { +export function followAccountSuccess(relationship, alreadyFollowing) { return { type: ACCOUNT_FOLLOW_SUCCESS, relationship, + alreadyFollowing, }; }; diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js index 724b10980..1f2d7690f 100644 --- a/app/javascript/mastodon/components/account.js +++ b/app/javascript/mastodon/components/account.js @@ -93,7 +93,7 @@ export default class Account extends ImmutablePureComponent { ); } else { - buttons = ; + buttons = ; } } diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js index e375131d4..389296c42 100644 --- a/app/javascript/mastodon/features/account/components/action_bar.js +++ b/app/javascript/mastodon/features/account/components/action_bar.js @@ -20,6 +20,8 @@ const messages = defineMessages({ media: { id: 'account.media', defaultMessage: 'Media' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, + hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' }, + showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' }, }); @injectIntl @@ -30,6 +32,7 @@ export default class ActionBar extends React.PureComponent { onFollow: PropTypes.func, onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, + onReblogToggle: PropTypes.func.isRequired, onReport: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired, onBlockDomain: PropTypes.func.isRequired, @@ -60,6 +63,15 @@ export default class ActionBar extends React.PureComponent { if (account.get('id') === me) { menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); } else { + const following = account.getIn(['relationship', 'following']); + if (following) { + if (following.get('reblogs')) { + menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); + } else { + menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); + } + } + if (account.getIn(['relationship', 'muting'])) { menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute }); } else { diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js index 5e251c0e5..0ddb6b6c1 100644 --- a/app/javascript/mastodon/features/account_timeline/components/header.js +++ b/app/javascript/mastodon/features/account_timeline/components/header.js @@ -14,6 +14,7 @@ export default class Header extends ImmutablePureComponent { onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, + onReblogToggle: PropTypes.func.isRequired, onReport: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired, onBlockDomain: PropTypes.func.isRequired, @@ -40,6 +41,10 @@ export default class Header extends ImmutablePureComponent { this.props.onReport(this.props.account); } + handleReblogToggle = () => { + this.props.onReblogToggle(this.props.account); + } + handleMute = () => { this.props.onMute(this.props.account); } @@ -80,6 +85,7 @@ export default class Header extends ImmutablePureComponent { account={account} onBlock={this.handleBlock} onMention={this.handleMention} + onReblogToggle={this.handleReblogToggle} onReport={this.handleReport} onMute={this.handleMute} onBlockDomain={this.handleBlockDomain} diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js index 8e50ec405..b41eb19d4 100644 --- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js +++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js @@ -67,6 +67,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(mentionCompose(account, router)); }, + onReblogToggle (account) { + if (account.getIn(['relationship', 'following', 'reblogs'])) { + dispatch(followAccount(account.get('id'), false)); + } else { + dispatch(followAccount(account.get('id'), true)); + } + }, + onReport (account) { dispatch(initReport(account)); }, diff --git a/app/javascript/mastodon/reducers/accounts_counters.js b/app/javascript/mastodon/reducers/accounts_counters.js index 1ed0fe3e3..2a78a9f34 100644 --- a/app/javascript/mastodon/reducers/accounts_counters.js +++ b/app/javascript/mastodon/reducers/accounts_counters.js @@ -126,7 +126,8 @@ export default function accountsCounters(state = initialState, action) { case STATUS_FETCH_SUCCESS: return normalizeAccountFromStatus(state, action.status); case ACCOUNT_FOLLOW_SUCCESS: - return state.updateIn([action.relationship.id, 'followers_count'], num => num + 1); + return action.alreadyFollowing ? state : + state.updateIn([action.relationship.id, 'followers_count'], num => num + 1); case ACCOUNT_UNFOLLOW_SUCCESS: return state.updateIn([action.relationship.id, 'followers_count'], num => Math.max(0, num - 1)); default: diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 79fae6e96..20b10e113 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -159,14 +159,15 @@ class FeedManager return true if Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any? - if status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply - should_filter = !Follow.where(account_id: receiver_id, target_account_id: status.in_reply_to_account_id).exists? # and I'm not following the person it's a reply to - should_filter &&= receiver_id != status.in_reply_to_account_id # and it's not a reply to me - should_filter &&= status.account_id != status.in_reply_to_account_id # and it's not a self-reply + if status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply + should_filter = !Follow.where(account_id: receiver_id, target_account_id: status.in_reply_to_account_id).exists? # and I'm not following the person it's a reply to + should_filter &&= receiver_id != status.in_reply_to_account_id # and it's not a reply to me + should_filter &&= status.account_id != status.in_reply_to_account_id # and it's not a self-reply return should_filter - elsif status.reblog? # Filter out a reblog - should_filter = Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists? # or if the author of the reblogged status is blocking me - should_filter ||= AccountDomainBlock.where(account_id: receiver_id, domain: status.reblog.account.domain).exists? # or the author's domain is blocked + elsif status.reblog? # Filter out a reblog + should_filter = Follow.where(account_id: receiver_id, target_account_id: status.account_id, show_reblogs: false).exists? # if the reblogger's reblogs are suppressed + should_filter ||= Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists? # or if the author of the reblogged status is blocking me + should_filter ||= AccountDomainBlock.where(account_id: receiver_id, domain: status.reblog.account.domain).exists? # or the author's domain is blocked return should_filter end diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 55ad812b2..fdf35a4e3 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -5,7 +5,11 @@ module AccountInteractions class_methods do def following_map(target_account_ids, account_id) - follow_mapping(Follow.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) + Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping| + mapping[follow.target_account_id] = { + reblogs: follow.show_reblogs?, + } + end end def followed_by_map(target_account_ids, account_id) @@ -25,7 +29,11 @@ module AccountInteractions end def requested_map(target_account_ids, account_id) - follow_mapping(FollowRequest.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) + FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping| + mapping[follow_request.target_account_id] = { + reblogs: follow_request.show_reblogs?, + } + end end def domain_blocking_map(target_account_ids, account_id) @@ -66,8 +74,12 @@ module AccountInteractions has_many :domain_blocks, class_name: 'AccountDomainBlock', dependent: :destroy end - def follow!(other_account) - active_relationships.find_or_create_by!(target_account: other_account) + def follow!(other_account, reblogs: nil) + reblogs = true if reblogs.nil? + rel = active_relationships.create_with(show_reblogs: reblogs).find_or_create_by!(target_account: other_account) + rel.update!(show_reblogs: reblogs) + + rel end def block!(other_account) @@ -140,6 +152,10 @@ module AccountInteractions mute_relationships.where(target_account: other_account, hide_notifications: true).exists? end + def muting_reblogs?(other_account) + active_relationships.where(target_account: other_account, show_reblogs: false).exists? + end + def requested?(other_account) follow_requests.where(target_account: other_account).exists? end diff --git a/app/models/follow.rb b/app/models/follow.rb index 795ecf55a..3fb665afc 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -8,6 +8,7 @@ # updated_at :datetime not null # account_id :integer not null # target_account_id :integer not null +# show_reblogs :boolean default(TRUE), not null # class Follow < ApplicationRecord diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index fac91b513..ebf6959ce 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -8,6 +8,7 @@ # updated_at :datetime not null # account_id :integer not null # target_account_id :integer not null +# show_reblogs :boolean default(TRUE), not null # class FollowRequest < ApplicationRecord @@ -21,7 +22,7 @@ class FollowRequest < ApplicationRecord validates :account_id, uniqueness: { scope: :target_account_id } def authorize! - account.follow!(target_account) + account.follow!(target_account, reblogs: show_reblogs) MergeWorker.perform_async(target_account.id, account.id) destroy! diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 791773f25..20579ca63 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -6,25 +6,38 @@ class FollowService < BaseService # Follow a remote user, notify remote user about the follow # @param [Account] source_account From which to follow # @param [String, Account] uri User URI to follow in the form of username@domain (or account record) - def call(source_account, uri) + # @param [true, false, nil] reblogs Whether or not to show reblogs, defaults to true + def call(source_account, uri, reblogs: nil) + reblogs = true if reblogs.nil? target_account = uri.is_a?(Account) ? uri : ResolveRemoteAccountService.new.call(uri) 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) - return if source_account.following?(target_account) || source_account.requested?(target_account) + if source_account.following?(target_account) + # We're already following this account, but we'll call follow! again to + # make sure the reblogs status is set correctly. + source_account.follow!(target_account, reblogs: reblogs) + return + elsif source_account.requested?(target_account) + # This isn't managed by a method in AccountInteractions, so we modify it + # ourselves if necessary. + req = follow_requests.find_by(target_account: other_account) + req.update!(show_reblogs: reblogs) + return + end if target_account.locked? || target_account.activitypub? - request_follow(source_account, target_account) + request_follow(source_account, target_account, reblogs: reblogs) else - direct_follow(source_account, target_account) + direct_follow(source_account, target_account, reblogs: reblogs) end end private - def request_follow(source_account, target_account) - follow_request = FollowRequest.create!(account: source_account, target_account: target_account) + def request_follow(source_account, target_account, reblogs: true) + follow_request = FollowRequest.create!(account: source_account, target_account: target_account, show_reblogs: reblogs) if target_account.local? NotifyService.new.call(target_account, follow_request) @@ -38,8 +51,8 @@ class FollowService < BaseService follow_request end - def direct_follow(source_account, target_account) - follow = source_account.follow!(target_account) + def direct_follow(source_account, target_account, reblogs: true) + follow = source_account.follow!(target_account, reblogs: reblogs) if target_account.local? NotifyService.new.call(target_account, follow) diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 8a77f2f38..d5960c3ad 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -29,7 +29,7 @@ class NotifyService < BaseService end def blocked_reblog? - false + @recipient.muting_reblogs?(@notification.from_account) end def blocked_follow_request? diff --git a/db/migrate/20171028221157_add_reblogs_to_follows.rb b/db/migrate/20171028221157_add_reblogs_to_follows.rb new file mode 100644 index 000000000..4b5d5b7ff --- /dev/null +++ b/db/migrate/20171028221157_add_reblogs_to_follows.rb @@ -0,0 +1,19 @@ +require Rails.root.join('lib', 'mastodon', 'migration_helpers') + +class AddReblogsToFollows < ActiveRecord::Migration[5.1] + include Mastodon::MigrationHelpers + + disable_ddl_transaction! + + def up + safety_assured do + add_column_with_default :follows, :show_reblogs, :boolean, default: true, allow_null: false + add_column_with_default :follow_requests, :show_reblogs, :boolean, default: true, allow_null: false + end + end + + def down + remove_column :follows, :show_reblogs + remove_column :follow_requests, :show_reblogs + end +end diff --git a/db/schema.rb b/db/schema.rb index 120b15950..f2ef60375 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -160,6 +160,7 @@ ActiveRecord::Schema.define(version: 20171125190735) do t.datetime "updated_at", null: false t.bigint "account_id", null: false t.bigint "target_account_id", null: false + t.boolean "show_reblogs", default: true, null: false t.index ["account_id", "target_account_id"], name: "index_follow_requests_on_account_id_and_target_account_id", unique: true end @@ -168,6 +169,7 @@ ActiveRecord::Schema.define(version: 20171125190735) do t.datetime "updated_at", null: false t.bigint "account_id", null: false t.bigint "target_account_id", null: false + t.boolean "show_reblogs", default: true, null: false t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true end diff --git a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb index 431fc2194..f25b86ac1 100644 --- a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb @@ -32,7 +32,7 @@ describe Api::V1::Accounts::RelationshipsController do json = body_as_json expect(json).to be_a Enumerable - expect(json.first[:following]).to be true + expect(json.first[:following]).to be_truthy expect(json.first[:followed_by]).to be false end end @@ -51,7 +51,7 @@ describe Api::V1::Accounts::RelationshipsController do expect(json).to be_a Enumerable expect(json.first[:id]).to eq simon.id.to_s - expect(json.first[:following]).to be true + expect(json.first[:following]).to be_truthy expect(json.first[:followed_by]).to be false expect(json.first[:muting]).to be false expect(json.first[:requested]).to be false diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb index 053c53e5a..f3b879421 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -31,10 +31,10 @@ RSpec.describe Api::V1::AccountsController, type: :controller do expect(response).to have_http_status(:success) end - it 'returns JSON with following=true and requested=false' do + it 'returns JSON with following=truthy and requested=false' do json = body_as_json - expect(json[:following]).to be true + expect(json[:following]).to be_truthy expect(json[:requested]).to be false end @@ -50,11 +50,11 @@ RSpec.describe Api::V1::AccountsController, type: :controller do expect(response).to have_http_status(:success) end - it 'returns JSON with following=false and requested=true' do + it 'returns JSON with following=false and requested=truthy' do json = body_as_json expect(json[:following]).to be false - expect(json[:requested]).to be true + expect(json[:requested]).to be_truthy end it 'creates a follow request relation between user and target user' do diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index 9e2305eca..f2103a4b1 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -56,6 +56,13 @@ RSpec.describe FeedManager do expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true end + it 'returns true for reblog from account with reblogs disabled' do + status = Fabricate(:status, text: 'Hello world', account: jeff) + reblog = Fabricate(:status, reblog: status, account: alice) + bob.follow!(alice, reblogs: false) + expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true + end + it 'returns false for reply by followee to another followee' do status = Fabricate(:status, text: 'Hello world', account: jeff) reply = Fabricate(:status, text: 'Nay', thread: status, account: alice) diff --git a/spec/models/concerns/account_interactions_spec.rb b/spec/models/concerns/account_interactions_spec.rb index 2b8c95d70..d08bdc8b9 100644 --- a/spec/models/concerns/account_interactions_spec.rb +++ b/spec/models/concerns/account_interactions_spec.rb @@ -14,7 +14,7 @@ describe AccountInteractions do context 'account with Follow' do it 'returns { target_account_id => true }' do Fabricate(:follow, account: account, target_account: target_account) - is_expected.to eq(target_account_id => true) + is_expected.to eq(target_account_id => { reblogs: true }) end end @@ -583,4 +583,41 @@ describe AccountInteractions do end end end + + describe 'ignoring reblogs from an account' do + before do + @me = Fabricate(:account, username: 'Me') + @you = Fabricate(:account, username: 'You') + end + + context 'with the reblogs option unspecified' do + before do + @me.follow!(@you) + end + + it 'defaults to showing reblogs' do + expect(@me.muting_reblogs?(@you)).to be(false) + end + end + + context 'with the reblogs option set to false' do + before do + @me.follow!(@you, reblogs: false) + end + + it 'does mute reblogs' do + expect(@me.muting_reblogs?(@you)).to be(true) + end + end + + context 'with the reblogs option set to true' do + before do + @me.follow!(@you, reblogs: true) + end + + it 'does not mute reblogs' do + expect(@me.muting_reblogs?(@you)).to be(false) + end + end + end end diff --git a/spec/models/follow_request_spec.rb b/spec/models/follow_request_spec.rb index 1436501e9..59893a14f 100644 --- a/spec/models/follow_request_spec.rb +++ b/spec/models/follow_request_spec.rb @@ -7,10 +7,24 @@ RSpec.describe FollowRequest, type: :model do let(:target_account) { Fabricate(:account) } it 'calls Account#follow!, MergeWorker.perform_async, and #destroy!' do - expect(account).to receive(:follow!).with(target_account) + expect(account).to receive(:follow!).with(target_account, reblogs: true) expect(MergeWorker).to receive(:perform_async).with(target_account.id, account.id) expect(follow_request).to receive(:destroy!) follow_request.authorize! end + + it 'correctly passes show_reblogs when true' do + follow_request = Fabricate.create(:follow_request, show_reblogs: true) + follow_request.authorize! + target = follow_request.target_account + expect(follow_request.account.muting_reblogs?(target)).to be false + end + + it 'correctly passes show_reblogs when false' do + follow_request = Fabricate.create(:follow_request, show_reblogs: false) + follow_request.authorize! + target = follow_request.target_account + expect(follow_request.account.muting_reblogs?(target)).to be true + end end end diff --git a/spec/services/follow_service_spec.rb b/spec/services/follow_service_spec.rb index ceb39e5e6..e59a2f1a6 100644 --- a/spec/services/follow_service_spec.rb +++ b/spec/services/follow_service_spec.rb @@ -13,8 +13,20 @@ RSpec.describe FollowService do 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 + 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 'locked account, no reblogs' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, locked: true, username: 'bob')).account } + + before do + subject.call(sender, bob.acct, reblogs: false) + end + + it 'creates a follow request without reblogs' do + expect(FollowRequest.find_by(account: sender, target_account: bob, show_reblogs: false)).to_not be_nil end end @@ -25,8 +37,22 @@ RSpec.describe FollowService do subject.call(sender, bob.acct) end - it 'creates a following relation' do + 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, no reblogs' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } + + before do + subject.call(sender, bob.acct, reblogs: false) + end + + it 'creates a following relation without reblogs' do expect(sender.following?(bob)).to be true + expect(sender.muting_reblogs?(bob)).to be true end end @@ -42,6 +68,32 @@ RSpec.describe FollowService do expect(sender.following?(bob)).to be true end end + + describe 'already followed account, turning reblogs off' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } + + before do + sender.follow!(bob, reblogs: true) + subject.call(sender, bob.acct, reblogs: false) + end + + it 'disables reblogs' do + expect(sender.muting_reblogs?(bob)).to be true + end + end + + describe 'already followed account, turning reblogs on' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } + + before do + sender.follow!(bob, reblogs: false) + subject.call(sender, bob.acct, reblogs: true) + end + + it 'disables reblogs' do + expect(sender.muting_reblogs?(bob)).to be false + end + end end context 'remote OStatus account' do diff --git a/spec/services/notify_service_spec.rb b/spec/services/notify_service_spec.rb index fad0dd369..eb6210a1e 100644 --- a/spec/services/notify_service_spec.rb +++ b/spec/services/notify_service_spec.rb @@ -81,6 +81,26 @@ RSpec.describe NotifyService do end end + describe 'reblogs' do + let(:status) { Fabricate(:status, account: Fabricate(:account)) } + let(:activity) { Fabricate(:status, account: sender, reblog: status) } + + it 'shows reblogs by default' do + recipient.follow!(sender) + is_expected.to change(Notification, :count) + end + + it 'shows reblogs when explicitly enabled' do + recipient.follow!(sender, reblogs: true) + is_expected.to change(Notification, :count) + end + + it 'hides reblogs when disabled' do + recipient.follow!(sender, reblogs: false) + is_expected.to_not change(Notification, :count) + end + end + context do let(:asshole) { Fabricate(:account, username: 'asshole') } let(:reply_to) { Fabricate(:status, account: asshole) } -- cgit From 85e97ecab6db67faefb64557af9b2271d2b23735 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 30 Nov 2017 03:50:05 +0100 Subject: Fix too many forwards (#5854) * Avoid sending explicit Undo->Announce when original deleted * Do not forward a reply back to the server that sent it * Deduplicate inboxes of rebloggers' followers for delete forwarding * Adjust test * Fix wrong class, bad SQL, wrong variable, outdated comment --- app/lib/activitypub/activity/create.rb | 2 +- app/lib/activitypub/activity/delete.rb | 7 +++++-- app/models/account.rb | 4 ++++ app/services/remove_status_service.rb | 12 +++++++++--- app/workers/activitypub/raw_distribution_worker.rb | 4 ++-- spec/lib/activitypub/activity/delete_spec.rb | 13 ++++++------- 6 files changed, 27 insertions(+), 15 deletions(-) (limited to 'app/services') diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 66e4f7c5e..3e95cbd06 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -210,7 +210,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def forward_for_reply return unless @json['signature'].present? && reply_to_local? - ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id) + ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url]) end def lock_options diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index 4c6afb090..d0fb49342 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -30,8 +30,11 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity def forward_for_reblogs(status) return if @json['signature'].blank? - ActivityPub::RawDistributionWorker.push_bulk(status.reblogs.includes(:account).references(:account).merge(Account.local).pluck(:account_id)) do |account_id| - [payload, account_id] + rebloggers_ids = status.reblogs.includes(:account).references(:account).merge(Account.local).pluck(:account_id) + inboxes = Account.where(id: ::Follow.where(target_account_id: rebloggers_ids).select(:account_id)).inboxes - [@account.preferred_inbox_url] + + ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url| + [payload, rebloggers_ids.first, inbox_url] end end diff --git a/app/models/account.rb b/app/models/account.rb index f3f604eda..b6d46b2cc 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -214,6 +214,10 @@ class Account < ApplicationRecord Rails.cache.fetch("exclude_domains_for:#{id}") { domain_blocks.pluck(:domain) } end + def preferred_inbox_url + shared_inbox_url.presence || inbox_url + end + class << self def readonly_attributes super - %w(statuses_count following_count followers_count) diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index c75627205..9f603bb36 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -3,7 +3,7 @@ class RemoveStatusService < BaseService include StreamEntryRenderer - def call(status) + def call(status, options = {}) @payload = Oj.dump(event: :delete, payload: status.id.to_s) @status = status @account = status.account @@ -11,6 +11,7 @@ class RemoveStatusService < BaseService @mentions = status.mentions.includes(:account).to_a @reblogs = status.reblogs.to_a @stream_entry = status.stream_entry + @options = options remove_from_self if status.account.local? remove_from_followers @@ -22,7 +23,12 @@ class RemoveStatusService < BaseService @status.destroy! - return unless @account.local? + # There is no reason to send out Undo activities when the + # cause is that the original object has been removed, since + # original object being removed implicitly removes reblogs + # of it. The Delete activity of the original is forwarded + # separately. + return if !@account.local? || @options[:original_removed] remove_from_remote_followers remove_from_remote_affected @@ -104,7 +110,7 @@ class RemoveStatusService < BaseService # without us being able to do all the fancy stuff @reblogs.each do |reblog| - RemoveStatusService.new.call(reblog) + RemoveStatusService.new.call(reblog, original_removed: true) end end diff --git a/app/workers/activitypub/raw_distribution_worker.rb b/app/workers/activitypub/raw_distribution_worker.rb index d73466f6e..41e61132f 100644 --- a/app/workers/activitypub/raw_distribution_worker.rb +++ b/app/workers/activitypub/raw_distribution_worker.rb @@ -5,10 +5,10 @@ class ActivityPub::RawDistributionWorker sidekiq_options queue: 'push' - def perform(json, source_account_id) + def perform(json, source_account_id, exclude_inboxes = []) @account = Account.find(source_account_id) - ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url| + ActivityPub::DeliveryWorker.push_bulk(inboxes - exclude_inboxes) do |inbox_url| [json, @account.id, inbox_url] end rescue ActiveRecord::RecordNotFound diff --git a/spec/lib/activitypub/activity/delete_spec.rb b/spec/lib/activitypub/activity/delete_spec.rb index 38254e31c..37b93ecf7 100644 --- a/spec/lib/activitypub/activity/delete_spec.rb +++ b/spec/lib/activitypub/activity/delete_spec.rb @@ -1,8 +1,8 @@ require 'rails_helper' RSpec.describe ActivityPub::Activity::Delete do - let(:sender) { Fabricate(:account, domain: 'example.com') } - let(:status) { Fabricate(:status, account: sender, uri: 'foobar') } + let(:sender) { Fabricate(:account, domain: 'example.com') } + let(:status) { Fabricate(:status, account: sender, uri: 'foobar') } let(:json) do { @@ -30,13 +30,13 @@ RSpec.describe ActivityPub::Activity::Delete do context 'when the status has been reblogged' do describe '#perform' do subject { described_class.new(json, sender) } - let(:reblogger) { Fabricate(:account) } - let(:follower) { Fabricate(:account, username: 'follower', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } + let!(:reblogger) { Fabricate(:account) } + let!(:follower) { Fabricate(:account, username: 'follower', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } + let!(:reblog) { Fabricate(:status, account: reblogger, reblog: status) } before do stub_request(:post, 'http://example.com/inbox').to_return(status: 200) follower.follow!(reblogger) - Fabricate(:status, account: reblogger, reblog: status) subject.perform end @@ -45,8 +45,7 @@ RSpec.describe ActivityPub::Activity::Delete do end it 'sends delete activity to followers of rebloggers' do - # one for Delete original post, and one for Undo reblog (normal delivery) - expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.twice + expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once end end end -- cgit From 4c6b5dbe96b565d3db3cbf0912f3b9911bc3922a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 30 Nov 2017 04:06:20 +0100 Subject: Add semi-support for Video/Image objects in ActivityPub (#5848) * Add semi-support for Video/Image objects in ActivityPub Video and Image objects will create corresponding status records with manually crafted text contents (title + URL) * Extract html-url-finding logic into JsonLdHelper * Fallback to id when url missing, extract supported object types --- app/helpers/jsonld_helper.rb | 18 ++++++++ app/lib/activitypub/activity/create.rb | 51 ++++++++++++++++------ app/lib/formatter.rb | 15 ++++--- .../activitypub/fetch_remote_status_service.rb | 2 +- .../activitypub/process_account_service.rb | 7 +-- .../fetch_remote_status_service_spec.rb | 36 +++++++++++++++ 6 files changed, 103 insertions(+), 26 deletions(-) (limited to 'app/services') diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index a3441e6f9..6c7c38070 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -9,6 +9,24 @@ module JsonLdHelper value.is_a?(Array) ? value.first : value end + # 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 + + if single_value.nil? || single_value.is_a?(String) + single_value + else + single_value['href'] + end + end + def as_array(value) value.is_a?(Array) ? value : [value] end diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 3e95cbd06..31e0abe39 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true class ActivityPub::Activity::Create < ActivityPub::Activity + SUPPORTED_TYPES = %w(Article Note).freeze + CONVERTED_TYPES = %w(Image Video).freeze + def perform return if delete_arrived_first?(object_uri) || unsupported_object_type? @@ -41,7 +44,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity url: object_url || @object['id'], account: @account, text: text_from_content || '', - language: language_from_content, + language: detected_language, spoiler_text: @object['summary'] || '', created_at: @options[:override_timestamps] ? nil : @object['published'], reply: @object['inReplyTo'].present?, @@ -165,40 +168,62 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def text_from_content + return Formatter.instance.linkify([text_from_name, object_url || @object['id']].join(' ')) if converted_object_type? + if @object['content'].present? @object['content'] - elsif language_map? + elsif content_language_map? @object['contentMap'].values.first end end - def language_from_content - return LanguageDetector.instance.detect(text_from_content, @account) unless language_map? - @object['contentMap'].keys.first + def text_from_name + if @object['name'].present? + @object['name'] + elsif name_language_map? + @object['nameMap'].values.first + end + end + + def detected_language + if content_language_map? + @object['contentMap'].keys.first + elsif name_language_map? + @object['nameMap'].keys.first + elsif supported_object_type? + LanguageDetector.instance.detect(text_from_content, @account) + end end def object_url return if @object['url'].blank? - - value = first_of_value(@object['url']) - - return value if value.is_a?(String) - - value['href'] + url_to_href(@object['url'], 'text/html') end - def language_map? + def content_language_map? @object['contentMap'].is_a?(Hash) && !@object['contentMap'].empty? end + def name_language_map? + @object['nameMap'].is_a?(Hash) && !@object['nameMap'].empty? + end + def unsupported_object_type? - @object.is_a?(String) || !%w(Article Note).include?(@object['type']) + @object.is_a?(String) || !(supported_object_type? || converted_object_type?) end def unsupported_media_type?(mime_type) mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type) end + def supported_object_type? + SUPPORTED_TYPES.include?(@object['type']) + end + + def converted_object_type? + CONVERTED_TYPES.include?(@object['type']) + end + def skip_download? return @skip_download if defined?(@skip_download) @skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media? diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 733a1c4b7..9d8bc52db 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -51,12 +51,7 @@ class Formatter def simplified_format(account) return reformat(account.note).html_safe unless account.local? # rubocop:disable Rails/OutputSafety - - html = encode_and_link_urls(account.note) - html = simple_format(html, {}, sanitize: false) - html = html.delete("\n") - - html.html_safe # rubocop:disable Rails/OutputSafety + linkify(account.note) end def sanitize(html, config) @@ -69,6 +64,14 @@ class Formatter html.html_safe # rubocop:disable Rails/OutputSafety end + def linkify(text) + html = encode_and_link_urls(text) + html = simple_format(html, {}, sanitize: false) + html = html.delete("\n") + + html.html_safe # rubocop:disable Rails/OutputSafety + end + private def encode(html) diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index 8de9283de..7649bceca 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -42,7 +42,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService end def expected_type? - %w(Note Article).include? @json['type'] + (ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES).include? @json['type'] end def needs_update(actor) diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 5ee7d89ee..06ca75563 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -107,12 +107,7 @@ class ActivityPub::ProcessAccountService < BaseService def url return if @json['url'].blank? - - value = first_of_value(@json['url']) - - return value if value.is_a?(String) - - value['href'] + url_to_href(@json['url'], 'text/html') end def outbox_total_items diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb index 51f3fe3a1..ad26abc5b 100644 --- a/spec/services/activitypub/fetch_remote_status_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' RSpec.describe ActivityPub::FetchRemoteStatusService do + include ActionView::Helpers::TextHelper + let(:sender) { Fabricate(:account) } let(:recipient) { Fabricate(:account) } let(:valid_domain) { Rails.configuration.x.local_domain } @@ -19,6 +21,7 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do describe '#call' do before do + stub_request(:head, 'https://example.com/watch?v=12345').to_return(status: 404, body: '') subject.call(object[:id], prefetched_body: Oj.dump(object)) end @@ -32,5 +35,38 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do expect(status.text).to eq 'Lorem ipsum' end end + + context 'with Video object' do + let(:object) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: "https://#{valid_domain}/@foo/1234", + type: 'Video', + name: 'Nyan Cat 10 hours remix', + attributedTo: ActivityPub::TagManager.instance.uri_for(sender), + url: [ + { + type: 'Link', + mimeType: 'application/x-bittorrent', + href: 'https://example.com/12345.torrent', + }, + + { + type: 'Link', + mimeType: 'text/html', + href: 'https://example.com/watch?v=12345', + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.url).to eq 'https://example.com/watch?v=12345' + expect(strip_tags(status.text)).to eq 'Nyan Cat 10 hours remix https://example.com/watch?v=12345' + end + end end end -- cgit