about summary refs log tree commit diff
diff options
context:
space:
mode:
authorThibaut Girka <thib@sitedethib.com>2019-02-17 11:58:17 +0100
committerThibaut Girka <thib@sitedethib.com>2019-02-17 12:26:55 +0100
commit369a475a7001677493b452b9aaea1141de442c53 (patch)
treea0c81f0876c68827cfa0e856844054e53cb2e741
parentab9082b3325dfd193d7130d6c880e85ab2781bc0 (diff)
parent147b4c2c3afacd6ad9d5c1353c072861eaca5fd2 (diff)
Merge branch 'master' into glitch-soc/merge-upstream
Conflicts:
- app/serializers/rest/instance_serializer.rb
- app/views/layouts/error.html.haml
- public/oops.png
  Took our version (same as upstream but without the things that only
  make sense in an animation).

Additional changes:
- app/javascript/flavours/vanilla/theme.yml
  Include upstream's javascript in error pages.
-rw-r--r--app/controllers/api/v1/apps/credentials_controller.rb2
-rw-r--r--app/javascript/flavours/vanilla/theme.yml2
-rw-r--r--app/javascript/mastodon/components/status_action_bar.js30
-rw-r--r--app/javascript/mastodon/features/blocks/index.js5
-rw-r--r--app/javascript/mastodon/features/domain_blocks/index.js5
-rw-r--r--app/javascript/mastodon/features/follow_requests/index.js5
-rw-r--r--app/javascript/mastodon/features/mutes/index.js5
-rw-r--r--app/javascript/mastodon/features/status/components/detailed_status.js2
-rw-r--r--app/javascript/packs/error.js13
-rw-r--r--app/javascript/styles/mastodon/basics.scss14
-rw-r--r--app/lib/activitypub/activity.rb19
-rw-r--r--app/lib/activitypub/activity/announce.rb13
-rw-r--r--app/lib/activitypub/activity/create.rb14
-rw-r--r--app/serializers/rest/application_serializer.rb6
-rw-r--r--app/serializers/rest/instance_serializer.rb6
-rw-r--r--app/views/layouts/error.html.haml6
-rw-r--r--spec/lib/activitypub/activity/announce_spec.rb116
-rw-r--r--spec/lib/activitypub/activity/create_spec.rb710
18 files changed, 625 insertions, 348 deletions
diff --git a/app/controllers/api/v1/apps/credentials_controller.rb b/app/controllers/api/v1/apps/credentials_controller.rb
index e469c7d21..8b63d0490 100644
--- a/app/controllers/api/v1/apps/credentials_controller.rb
+++ b/app/controllers/api/v1/apps/credentials_controller.rb
@@ -6,6 +6,6 @@ class Api::V1::Apps::CredentialsController < Api::BaseController
   respond_to :json
 
   def show
-    render json: doorkeeper_token.application, serializer: REST::StatusSerializer::ApplicationSerializer
+    render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key)
   end
 end
diff --git a/app/javascript/flavours/vanilla/theme.yml b/app/javascript/flavours/vanilla/theme.yml
index bd9fb1dab..a215b2625 100644
--- a/app/javascript/flavours/vanilla/theme.yml
+++ b/app/javascript/flavours/vanilla/theme.yml
@@ -7,7 +7,7 @@ pack:
     filename: common.js
     stylesheet: true
   embed: public.js
-  error:
+  error: error.js
   home:
     filename: application.js
     preload:
diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js
index 16c7caf1c..53d17d418 100644
--- a/app/javascript/mastodon/components/status_action_bar.js
+++ b/app/javascript/mastodon/components/status_action_bar.js
@@ -78,7 +78,11 @@ class StatusActionBar extends ImmutablePureComponent {
   ]
 
   handleReplyClick = () => {
-    this.props.onReply(this.props.status, this.context.router.history);
+    if (me) {
+      this.props.onReply(this.props.status, this.context.router.history);
+    } else {
+      this._openInteractionDialog('reply');
+    }
   }
 
   handleShareClick = () => {
@@ -91,11 +95,23 @@ class StatusActionBar extends ImmutablePureComponent {
   }
 
   handleFavouriteClick = () => {
-    this.props.onFavourite(this.props.status);
+    if (me) {
+      this.props.onFavourite(this.props.status);
+    } else {
+      this._openInteractionDialog('favourite');
+    }
+  }
+
+  handleReblogClick = e => {
+    if (me) {
+      this.props.onReblog(this.props.status, e);
+    } else {
+      this._openInteractionDialog('reblog');
+    }
   }
 
-  handleReblogClick = (e) => {
-    this.props.onReblog(this.props.status, e);
+  _openInteractionDialog = type => {
+    window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
   }
 
   handleDeleteClick = () => {
@@ -233,9 +249,9 @@ class StatusActionBar extends ImmutablePureComponent {
 
     return (
       <div className='status__action-bar'>
-        <div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
-        <IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
-        <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
+        <div className='status__action-bar__counter'><IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
+        <IconButton className='status__action-bar-button' disabled={!publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
+        <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
         {shareButton}
 
         <div className='status__action-bar-dropdown'>
diff --git a/app/javascript/mastodon/features/blocks/index.js b/app/javascript/mastodon/features/blocks/index.js
index ca7ce6f8e..96a219c94 100644
--- a/app/javascript/mastodon/features/blocks/index.js
+++ b/app/javascript/mastodon/features/blocks/index.js
@@ -18,6 +18,7 @@ const messages = defineMessages({
 
 const mapStateToProps = state => ({
   accountIds: state.getIn(['user_lists', 'blocks', 'items']),
+  hasMore: !!state.getIn(['user_lists', 'blocks', 'next']),
 });
 
 export default @connect(mapStateToProps)
@@ -29,6 +30,7 @@ class Blocks extends ImmutablePureComponent {
     dispatch: PropTypes.func.isRequired,
     shouldUpdateScroll: PropTypes.func,
     accountIds: ImmutablePropTypes.list,
+    hasMore: PropTypes.bool,
     intl: PropTypes.object.isRequired,
   };
 
@@ -41,7 +43,7 @@ class Blocks extends ImmutablePureComponent {
   }, 300, { leading: true });
 
   render () {
-    const { intl, accountIds, shouldUpdateScroll } = this.props;
+    const { intl, accountIds, shouldUpdateScroll, hasMore } = this.props;
 
     if (!accountIds) {
       return (
@@ -59,6 +61,7 @@ class Blocks extends ImmutablePureComponent {
         <ScrollableList
           scrollKey='blocks'
           onLoadMore={this.handleLoadMore}
+          hasMore={hasMore}
           shouldUpdateScroll={shouldUpdateScroll}
           emptyMessage={emptyMessage}
         >
diff --git a/app/javascript/mastodon/features/domain_blocks/index.js b/app/javascript/mastodon/features/domain_blocks/index.js
index 5c1bd1161..7c075f5a5 100644
--- a/app/javascript/mastodon/features/domain_blocks/index.js
+++ b/app/javascript/mastodon/features/domain_blocks/index.js
@@ -19,6 +19,7 @@ const messages = defineMessages({
 
 const mapStateToProps = state => ({
   domains: state.getIn(['domain_lists', 'blocks', 'items']),
+  hasMore: !!state.getIn(['domain_lists', 'blocks', 'next']),
 });
 
 export default @connect(mapStateToProps)
@@ -29,6 +30,7 @@ class Blocks extends ImmutablePureComponent {
     params: PropTypes.object.isRequired,
     dispatch: PropTypes.func.isRequired,
     shouldUpdateScroll: PropTypes.func,
+    hasMore: PropTypes.bool,
     domains: ImmutablePropTypes.orderedSet,
     intl: PropTypes.object.isRequired,
   };
@@ -42,7 +44,7 @@ class Blocks extends ImmutablePureComponent {
   }, 300, { leading: true });
 
   render () {
-    const { intl, domains, shouldUpdateScroll } = this.props;
+    const { intl, domains, shouldUpdateScroll, hasMore } = this.props;
 
     if (!domains) {
       return (
@@ -60,6 +62,7 @@ class Blocks extends ImmutablePureComponent {
         <ScrollableList
           scrollKey='domain_blocks'
           onLoadMore={this.handleLoadMore}
+          hasMore={hasMore}
           shouldUpdateScroll={shouldUpdateScroll}
           emptyMessage={emptyMessage}
         >
diff --git a/app/javascript/mastodon/features/follow_requests/index.js b/app/javascript/mastodon/features/follow_requests/index.js
index 56ae8764b..3871e0e5d 100644
--- a/app/javascript/mastodon/features/follow_requests/index.js
+++ b/app/javascript/mastodon/features/follow_requests/index.js
@@ -18,6 +18,7 @@ const messages = defineMessages({
 
 const mapStateToProps = state => ({
   accountIds: state.getIn(['user_lists', 'follow_requests', 'items']),
+  hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']),
 });
 
 export default @connect(mapStateToProps)
@@ -28,6 +29,7 @@ class FollowRequests extends ImmutablePureComponent {
     params: PropTypes.object.isRequired,
     dispatch: PropTypes.func.isRequired,
     shouldUpdateScroll: PropTypes.func,
+    hasMore: PropTypes.bool,
     accountIds: ImmutablePropTypes.list,
     intl: PropTypes.object.isRequired,
   };
@@ -41,7 +43,7 @@ class FollowRequests extends ImmutablePureComponent {
   }, 300, { leading: true });
 
   render () {
-    const { intl, shouldUpdateScroll, accountIds } = this.props;
+    const { intl, shouldUpdateScroll, accountIds, hasMore } = this.props;
 
     if (!accountIds) {
       return (
@@ -59,6 +61,7 @@ class FollowRequests extends ImmutablePureComponent {
         <ScrollableList
           scrollKey='follow_requests'
           onLoadMore={this.handleLoadMore}
+          hasMore={hasMore}
           shouldUpdateScroll={shouldUpdateScroll}
           emptyMessage={emptyMessage}
         >
diff --git a/app/javascript/mastodon/features/mutes/index.js b/app/javascript/mastodon/features/mutes/index.js
index f979ef72f..4ed29a1ce 100644
--- a/app/javascript/mastodon/features/mutes/index.js
+++ b/app/javascript/mastodon/features/mutes/index.js
@@ -18,6 +18,7 @@ const messages = defineMessages({
 
 const mapStateToProps = state => ({
   accountIds: state.getIn(['user_lists', 'mutes', 'items']),
+  hasMore: !!state.getIn(['user_lists', 'mutes', 'next']),
 });
 
 export default @connect(mapStateToProps)
@@ -28,6 +29,7 @@ class Mutes extends ImmutablePureComponent {
     params: PropTypes.object.isRequired,
     dispatch: PropTypes.func.isRequired,
     shouldUpdateScroll: PropTypes.func,
+    hasMore: PropTypes.bool,
     accountIds: ImmutablePropTypes.list,
     intl: PropTypes.object.isRequired,
   };
@@ -41,7 +43,7 @@ class Mutes extends ImmutablePureComponent {
   }, 300, { leading: true });
 
   render () {
-    const { intl, shouldUpdateScroll, accountIds } = this.props;
+    const { intl, shouldUpdateScroll, hasMore, accountIds } = this.props;
 
     if (!accountIds) {
       return (
@@ -59,6 +61,7 @@ class Mutes extends ImmutablePureComponent {
         <ScrollableList
           scrollKey='mutes'
           onLoadMore={this.handleLoadMore}
+          hasMore={hasMore}
           shouldUpdateScroll={shouldUpdateScroll}
           emptyMessage={emptyMessage}
         >
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index 734353c9b..49bc43a7b 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -87,7 +87,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
   }
 
   render () {
-    const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status;
+    const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
     const outerStyle = { boxSizing: 'border-box' };
     const { compact } = this.props;
 
diff --git a/app/javascript/packs/error.js b/app/javascript/packs/error.js
new file mode 100644
index 000000000..685c89065
--- /dev/null
+++ b/app/javascript/packs/error.js
@@ -0,0 +1,13 @@
+import ready from '../mastodon/ready';
+
+ready(() => {
+  const image = document.querySelector('img');
+
+  image.addEventListener('mouseenter', () => {
+    image.src = '/oops.gif';
+  });
+
+  image.addEventListener('mouseleave', () => {
+    image.src = '/oops.png';
+  });
+});
diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss
index 746def625..4411ca0b4 100644
--- a/app/javascript/styles/mastodon/basics.scss
+++ b/app/javascript/styles/mastodon/basics.scss
@@ -100,12 +100,14 @@ body {
       vertical-align: middle;
       margin: 20px;
 
-      img {
-        display: block;
-        max-width: 470px;
-        width: 100%;
-        height: auto;
-        margin-top: -120px;
+      &__illustration {
+        img {
+          display: block;
+          max-width: 470px;
+          width: 100%;
+          height: auto;
+          margin-top: -120px;
+        }
       }
 
       h1 {
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index 7e4e19531..8265810a0 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -138,11 +138,13 @@ class ActivityPub::Activity
   def status_from_object
     # If the status is already known, return it
     status = status_from_uri(object_uri)
+
     return status unless status.nil?
 
     # If the boosted toot is embedded and it is a self-boost, handle it like a Create
     unless unsupported_object_type?
       actor_id = value_or_id(first_of_value(@object['attributedTo'])) || @account.uri
+
       if actor_id == @account.uri
         return ActivityPub::Activity.factory({ 'type' => 'Create', 'actor' => actor_id, 'object' => @object }, @account).perform
       end
@@ -166,4 +168,21 @@ class ActivityPub::Activity
   ensure
     redis.del(key)
   end
+
+  def fetch?
+    !@options[:delivery]
+  end
+
+  def followed_by_local_accounts?
+    @account.passive_relationships.exists?
+  end
+
+  def requested_through_relay?
+    @options[:relayed_through_account] && Relay.find_by(inbox_url: @options[:relayed_through_account].inbox_url)&.enabled?
+  end
+
+  def reject_payload!
+    Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_account] && "via #{@options[:relayed_through_account].uri}"}")
+    nil
+  end
 end
diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb
index 04afeea20..9f8ffd9fb 100644
--- a/app/lib/activitypub/activity/announce.rb
+++ b/app/lib/activitypub/activity/announce.rb
@@ -2,8 +2,11 @@
 
 class ActivityPub::Activity::Announce < ActivityPub::Activity
   def perform
+    return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity?
+
     original_status = status_from_object
-    return if original_status.nil? || delete_arrived_first?(@json['id']) || !announceable?(original_status)
+
+    return reject_payload! if original_status.nil? || !announceable?(original_status)
 
     status = Status.find_by(account: @account, reblog: original_status)
 
@@ -39,4 +42,12 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
   def announceable?(status)
     status.account_id == @account.id || status.public_visibility? || status.unlisted_visibility?
   end
+
+  def related_to_local_activity?
+    followed_by_local_accounts? || requested_through_relay? || reblog_of_local_status?
+  end
+
+  def reblog_of_local_status?
+    status_from_uri(object_uri)&.account&.local?
+  end
 end
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index 1b31768d9..d7bd65c80 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -2,7 +2,7 @@
 
 class ActivityPub::Activity::Create < ActivityPub::Activity
   def perform
-    return if unsupported_object_type? || invalid_origin?(@object['id']) || Tombstone.exists?(uri: @object['id']) || !related_to_local_activity?
+    return reject_payload! if unsupported_object_type? || invalid_origin?(@object['id']) || Tombstone.exists?(uri: @object['id']) || !related_to_local_activity?
 
     RedisLock.acquire(lock_options) do |lock|
       if lock.acquired?
@@ -341,18 +341,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
       responds_to_followed_account? || addresses_local_accounts?
   end
 
-  def fetch?
-    !@options[:delivery]
-  end
-
-  def followed_by_local_accounts?
-    @account.passive_relationships.exists?
-  end
-
-  def requested_through_relay?
-    @options[:relayed_through_account] && Relay.find_by(inbox_url: @options[:relayed_through_account].inbox_url)&.enabled?
-  end
-
   def responds_to_followed_account?
     !replied_to_status.nil? && (replied_to_status.account.local? || replied_to_status.account.passive_relationships.exists?)
   end
diff --git a/app/serializers/rest/application_serializer.rb b/app/serializers/rest/application_serializer.rb
index a9316cd4b..ab68219ad 100644
--- a/app/serializers/rest/application_serializer.rb
+++ b/app/serializers/rest/application_serializer.rb
@@ -2,7 +2,7 @@
 
 class REST::ApplicationSerializer < ActiveModel::Serializer
   attributes :id, :name, :website, :redirect_uri,
-             :client_id, :client_secret
+             :client_id, :client_secret, :vapid_key
 
   def id
     object.id.to_s
@@ -19,4 +19,8 @@ class REST::ApplicationSerializer < ActiveModel::Serializer
   def website
     object.website.presence
   end
+
+  def vapid_key
+    Rails.configuration.x.vapid_public_key
+  end
 end
diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb
index cab05e60a..41ed1995d 100644
--- a/app/serializers/rest/instance_serializer.rb
+++ b/app/serializers/rest/instance_serializer.rb
@@ -5,7 +5,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
 
   attributes :uri, :title, :description, :email,
              :version, :urls, :stats, :thumbnail, :max_toot_chars,
-             :languages
+             :languages, :registrations
 
   has_one :contact_account, serializer: REST::AccountSerializer
 
@@ -55,6 +55,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
     [I18n.default_locale]
   end
 
+  def registrations
+    Setting.open_registrations && !Rails.configuration.x.single_user_mode
+  end
+
   private
 
   def instance_presenter
diff --git a/app/views/layouts/error.html.haml b/app/views/layouts/error.html.haml
index d662d85e2..e9c474082 100644
--- a/app/views/layouts/error.html.haml
+++ b/app/views/layouts/error.html.haml
@@ -5,10 +5,12 @@
     %meta{ charset: 'utf-8' }/
     %title= safe_join([yield(:page_title), Setting.default_settings['site_title']], ' - ')
     %meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/
+    = javascript_pack_tag "locales", integrity: true, crossorigin: 'anonymous'
     = render partial: 'layouts/theme', object: (@core || { pack: 'common' })
     = render partial: 'layouts/theme', object: (@theme || { pack: 'common', flavour: 'glitch', skin: 'default' })
   %body.error
     .dialog
-      %img{ alt: Setting.default_settings['site_title'], src: current_user&.setting_auto_play_gif ? '/oops.gif' : '/oops.png' }/
-      %div
+      .dialog__illustration
+        %img{ alt: Setting.default_settings['site_title'], src: '/oops.png' }/
+      .dialog__message
         %h1= yield :content
diff --git a/spec/lib/activitypub/activity/announce_spec.rb b/spec/lib/activitypub/activity/announce_spec.rb
index 1725c2843..94b9d348d 100644
--- a/spec/lib/activitypub/activity/announce_spec.rb
+++ b/spec/lib/activitypub/activity/announce_spec.rb
@@ -22,11 +22,59 @@ RSpec.describe ActivityPub::Activity::Announce do
   end
 
   describe '#perform' do
-    before do
-      subject.perform
+    context 'when sender is followed by a local account' do
+      before do
+        Fabricate(:account).follow!(sender)
+        subject.perform
+      end
+
+      context 'a known status' do
+        let(:object_json) do
+          ActivityPub::TagManager.instance.uri_for(status)
+        end
+
+        it 'creates a reblog by sender of status' do
+          expect(sender.reblogged?(status)).to be true
+        end
+      end
+
+      context 'self-boost of a previously unknown status with missing attributedTo' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            to: 'http://example.com/followers',
+          }
+        end
+
+        it 'creates a reblog by sender of status' do
+          expect(sender.reblogged?(sender.statuses.first)).to be true
+        end
+      end
+
+      context 'self-boost of a previously unknown status with correct attributedTo' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
+            to: 'http://example.com/followers',
+          }
+        end
+
+        it 'creates a reblog by sender of status' do
+          expect(sender.reblogged?(sender.statuses.first)).to be true
+        end
+      end
     end
 
-    context 'a known status' do
+    context 'when the status belongs to a local user' do
+      before do
+        subject.perform
+      end
+
       let(:object_json) do
         ActivityPub::TagManager.instance.uri_for(status)
       end
@@ -36,34 +84,68 @@ RSpec.describe ActivityPub::Activity::Announce do
       end
     end
 
-    context 'self-boost of a previously unknown status with missing attributedTo' do
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum',
-          to: 'http://example.com/followers',
-        }
+    context 'when the sender is relayed' do
+      let!(:relay_account) { Fabricate(:account, inbox_url: 'https://relay.example.com/inbox') }
+      let!(:relay) { Fabricate(:relay, inbox_url: 'https://relay.example.com/inbox') }
+
+      subject { described_class.new(json, sender, relayed_through_account: relay_account) }
+
+      context 'and the relay is enabled' do
+        before do
+          relay.update(state: :accepted)
+          subject.perform
+        end
+
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            to: 'http://example.com/followers',
+          }
+        end
+
+        it 'creates a reblog by sender of status' do
+          expect(sender.statuses.count).to eq 2
+        end
       end
 
-      it 'creates a reblog by sender of status' do
-        expect(sender.reblogged?(sender.statuses.first)).to be true
+      context 'and the relay is disabled' do
+        before do
+          subject.perform
+        end
+
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            to: 'http://example.com/followers',
+          }
+        end
+
+        it 'does not create anything' do
+          expect(sender.statuses.count).to eq 0
+        end
       end
     end
 
-    context 'self-boost of a previously unknown status with correct attributedTo' do
+    context 'when the sender has no relevance to local activity' do
+      before do
+        subject.perform
+      end
+
       let(:object_json) do
         {
           id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
           type: 'Note',
           content: 'Lorem ipsum',
-          attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
           to: 'http://example.com/followers',
         }
       end
 
-      it 'creates a reblog by sender of status' do
-        expect(sender.reblogged?(sender.statuses.first)).to be true
+      it 'does not create anything' do
+        expect(sender.statuses.count).to eq 0
       end
     end
   end
diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb
index cd20b7c7c..26cb84871 100644
--- a/spec/lib/activitypub/activity/create_spec.rb
+++ b/spec/lib/activitypub/activity/create_spec.rb
@@ -13,8 +13,6 @@ RSpec.describe ActivityPub::Activity::Create do
     }.with_indifferent_access
   end
 
-  subject { described_class.new(json, sender) }
-
   before do
     sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
 
@@ -23,59 +21,407 @@ RSpec.describe ActivityPub::Activity::Create do
   end
 
   describe '#perform' do
-    before do
-      subject.perform
-    end
-
-    context 'standalone' do
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum',
-        }
-      end
-
-      it 'creates status' do
-        status = sender.statuses.first
-
-        expect(status).to_not be_nil
-        expect(status.text).to eq 'Lorem ipsum'
-      end
-
-      it 'missing to/cc defaults to direct privacy' do
-        status = sender.statuses.first
+    context 'when fetching' do
+      subject { described_class.new(json, sender) }
+
+      before do
+        subject.perform
+      end
+
+      context 'standalone' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+          }
+        end
+
+        it 'creates status' do
+          status = sender.statuses.first
+
+          expect(status).to_not be_nil
+          expect(status.text).to eq 'Lorem ipsum'
+        end
+
+        it 'missing to/cc defaults to direct privacy' do
+          status = sender.statuses.first
+
+          expect(status).to_not be_nil
+          expect(status.visibility).to eq 'direct'
+        end
+      end
+
+      context 'public' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            to: 'https://www.w3.org/ns/activitystreams#Public',
+          }
+        end
+
+        it 'creates status' do
+          status = sender.statuses.first
+
+          expect(status).to_not be_nil
+          expect(status.visibility).to eq 'public'
+        end
+      end
+
+      context 'unlisted' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            cc: 'https://www.w3.org/ns/activitystreams#Public',
+          }
+        end
+
+        it 'creates status' do
+          status = sender.statuses.first
+
+          expect(status).to_not be_nil
+          expect(status.visibility).to eq 'unlisted'
+        end
+      end
+
+      context 'private' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            to: 'http://example.com/followers',
+          }
+        end
+
+        it 'creates status' do
+          status = sender.statuses.first
+
+          expect(status).to_not be_nil
+          expect(status.visibility).to eq 'private'
+        end
+      end
+
+      context 'limited' do
+        let(:recipient) { Fabricate(:account) }
+
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            to: ActivityPub::TagManager.instance.uri_for(recipient),
+          }
+        end
+
+        it 'creates status' do
+          status = sender.statuses.first
+
+          expect(status).to_not be_nil
+          expect(status.visibility).to eq 'limited'
+        end
+
+        it 'creates silent mention' do
+          status = sender.statuses.first
+          expect(status.mentions.first).to be_silent
+        end
+      end
+
+      context 'direct' do
+        let(:recipient) { Fabricate(:account) }
+
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            to: ActivityPub::TagManager.instance.uri_for(recipient),
+            tag: {
+              type: 'Mention',
+              href: ActivityPub::TagManager.instance.uri_for(recipient),
+            },
+          }
+        end
+
+        it 'creates status' do
+          status = sender.statuses.first
+
+          expect(status).to_not be_nil
+          expect(status.visibility).to eq 'direct'
+        end
+      end
+
+      context 'as a reply' do
+        let(:original_status) { Fabricate(:status) }
+
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status),
+          }
+        end
+
+        it 'creates status' do
+          status = sender.statuses.first
+
+          expect(status).to_not be_nil
+          expect(status.thread).to eq original_status
+          expect(status.reply?).to be true
+          expect(status.in_reply_to_account).to eq original_status.account
+          expect(status.conversation).to eq original_status.conversation
+        end
+      end
+
+      context 'with mentions' do
+        let(:recipient) { Fabricate(:account) }
+
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            tag: [
+              {
+                type: 'Mention',
+                href: ActivityPub::TagManager.instance.uri_for(recipient),
+              },
+            ],
+          }
+        end
+
+        it 'creates status' do
+          status = sender.statuses.first
+
+          expect(status).to_not be_nil
+          expect(status.mentions.map(&:account)).to include(recipient)
+        end
+      end
+
+      context 'with mentions missing href' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            tag: [
+              {
+                type: 'Mention',
+              },
+            ],
+          }
+        end
+
+        it 'creates status' do
+          status = sender.statuses.first
+          expect(status).to_not be_nil
+        end
+      end
+
+      context 'with media attachments' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            attachment: [
+              {
+                type: 'Document',
+                mediaType: 'image/png',
+                url: 'http://example.com/attachment.png',
+              },
+            ],
+          }
+        end
+
+        it 'creates status' do
+          status = sender.statuses.first
+
+          expect(status).to_not be_nil
+          expect(status.media_attachments.map(&:remote_url)).to include('http://example.com/attachment.png')
+        end
+      end
+
+      context 'with media attachments with focal points' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            attachment: [
+              {
+                type: 'Document',
+                mediaType: 'image/png',
+                url: 'http://example.com/attachment.png',
+                focalPoint: [0.5, -0.7],
+              },
+            ],
+          }
+        end
+
+        it 'creates status' do
+          status = sender.statuses.first
+
+          expect(status).to_not be_nil
+          expect(status.media_attachments.map(&:focus)).to include('0.5,-0.7')
+        end
+      end
+
+      context 'with media attachments missing url' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            attachment: [
+              {
+                type: 'Document',
+                mediaType: 'image/png',
+              },
+            ],
+          }
+        end
+
+        it 'creates status' do
+          status = sender.statuses.first
+          expect(status).to_not be_nil
+        end
+      end
+
+      context 'with hashtags' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            tag: [
+              {
+                type: 'Hashtag',
+                href: 'http://example.com/blah',
+                name: '#test',
+              },
+            ],
+          }
+        end
+
+        it 'creates status' do
+          status = sender.statuses.first
+
+          expect(status).to_not be_nil
+          expect(status.tags.map(&:name)).to include('test')
+        end
+      end
+
+      context 'with hashtags missing name' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum',
+            tag: [
+              {
+                type: 'Hashtag',
+                href: 'http://example.com/blah',
+              },
+            ],
+          }
+        end
+
+        it 'creates status' do
+          status = sender.statuses.first
+          expect(status).to_not be_nil
+        end
+      end
+
+      context 'with emojis' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum :tinking:',
+            tag: [
+              {
+                type: 'Emoji',
+                icon: {
+                  url: 'http://example.com/emoji.png',
+                },
+                name: 'tinking',
+              },
+            ],
+          }
+        end
+
+        it 'creates status' do
+          status = sender.statuses.first
+
+          expect(status).to_not be_nil
+          expect(status.emojis.map(&:shortcode)).to include('tinking')
+        end
+      end
+
+      context 'with emojis missing name' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum :tinking:',
+            tag: [
+              {
+                type: 'Emoji',
+                icon: {
+                  url: 'http://example.com/emoji.png',
+                },
+              },
+            ],
+          }
+        end
+
+        it 'creates status' do
+          status = sender.statuses.first
+          expect(status).to_not be_nil
+        end
+      end
+
+      context 'with emojis missing icon' do
+        let(:object_json) do
+          {
+            id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+            type: 'Note',
+            content: 'Lorem ipsum :tinking:',
+            tag: [
+              {
+                type: 'Emoji',
+                name: 'tinking',
+              },
+            ],
+          }
+        end
 
-        expect(status).to_not be_nil
-        expect(status.visibility).to eq 'direct'
+        it 'creates status' do
+          status = sender.statuses.first
+          expect(status).to_not be_nil
+        end
       end
     end
 
-    context 'public' do
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum',
-          to: 'https://www.w3.org/ns/activitystreams#Public',
-        }
-      end
-
-      it 'creates status' do
-        status = sender.statuses.first
+    context 'when sender is followed by local users' do
+      subject { described_class.new(json, sender, delivery: true) }
 
-        expect(status).to_not be_nil
-        expect(status.visibility).to eq 'public'
+      before do
+        Fabricate(:account).follow!(sender)
+        subject.perform
       end
-    end
 
-    context 'unlisted' do
       let(:object_json) do
         {
           id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
           type: 'Note',
           content: 'Lorem ipsum',
-          cc: 'https://www.w3.org/ns/activitystreams#Public',
         }
       end
 
@@ -83,66 +429,25 @@ RSpec.describe ActivityPub::Activity::Create do
         status = sender.statuses.first
 
         expect(status).to_not be_nil
-        expect(status.visibility).to eq 'unlisted'
-      end
-    end
-
-    context 'private' do
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum',
-          to: 'http://example.com/followers',
-        }
-      end
-
-      it 'creates status' do
-        status = sender.statuses.first
-
-        expect(status).to_not be_nil
-        expect(status.visibility).to eq 'private'
+        expect(status.text).to eq 'Lorem ipsum'
       end
     end
 
-    context 'limited' do
-      let(:recipient) { Fabricate(:account) }
+    context 'when sender replies to local status' do
+      let!(:local_status) { Fabricate(:status) }
 
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum',
-          to: ActivityPub::TagManager.instance.uri_for(recipient),
-        }
-      end
-
-      it 'creates status' do
-        status = sender.statuses.first
-
-        expect(status).to_not be_nil
-        expect(status.visibility).to eq 'limited'
-      end
+      subject { described_class.new(json, sender, delivery: true) }
 
-      it 'creates silent mention' do
-        status = sender.statuses.first
-        expect(status.mentions.first).to be_silent
+      before do
+        subject.perform
       end
-    end
-
-    context 'direct' do
-      let(:recipient) { Fabricate(:account) }
 
       let(:object_json) do
         {
           id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
           type: 'Note',
           content: 'Lorem ipsum',
-          to: ActivityPub::TagManager.instance.uri_for(recipient),
-          tag: {
-            type: 'Mention',
-            href: ActivityPub::TagManager.instance.uri_for(recipient),
-          },
+          inReplyTo: ActivityPub::TagManager.instance.uri_for(local_status),
         }
       end
 
@@ -150,47 +455,25 @@ RSpec.describe ActivityPub::Activity::Create do
         status = sender.statuses.first
 
         expect(status).to_not be_nil
-        expect(status.visibility).to eq 'direct'
+        expect(status.text).to eq 'Lorem ipsum'
       end
     end
 
-    context 'as a reply' do
-      let(:original_status) { Fabricate(:status) }
+    context 'when sender targets a local user' do
+      let!(:local_account) { Fabricate(:account) }
 
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum',
-          inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status),
-        }
-      end
-
-      it 'creates status' do
-        status = sender.statuses.first
+      subject { described_class.new(json, sender, delivery: true) }
 
-        expect(status).to_not be_nil
-        expect(status.thread).to eq original_status
-        expect(status.reply?).to be true
-        expect(status.in_reply_to_account).to eq original_status.account
-        expect(status.conversation).to eq original_status.conversation
+      before do
+        subject.perform
       end
-    end
-
-    context 'with mentions' do
-      let(:recipient) { Fabricate(:account) }
 
       let(:object_json) do
         {
           id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
           type: 'Note',
           content: 'Lorem ipsum',
-          tag: [
-            {
-              type: 'Mention',
-              href: ActivityPub::TagManager.instance.uri_for(recipient),
-            },
-          ],
+          to: ActivityPub::TagManager.instance.uri_for(local_account),
         }
       end
 
@@ -198,68 +481,25 @@ RSpec.describe ActivityPub::Activity::Create do
         status = sender.statuses.first
 
         expect(status).to_not be_nil
-        expect(status.mentions.map(&:account)).to include(recipient)
-      end
-    end
-
-    context 'with mentions missing href' do
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum',
-          tag: [
-            {
-              type: 'Mention',
-            },
-          ],
-        }
-      end
-
-      it 'creates status' do
-        status = sender.statuses.first
-        expect(status).to_not be_nil
+        expect(status.text).to eq 'Lorem ipsum'
       end
     end
 
-    context 'with media attachments' do
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum',
-          attachment: [
-            {
-              type: 'Document',
-              mediaType: 'image/png',
-              url: 'http://example.com/attachment.png',
-            },
-          ],
-        }
-      end
+    context 'when sender cc\'s a local user' do
+      let!(:local_account) { Fabricate(:account) }
 
-      it 'creates status' do
-        status = sender.statuses.first
+      subject { described_class.new(json, sender, delivery: true) }
 
-        expect(status).to_not be_nil
-        expect(status.media_attachments.map(&:remote_url)).to include('http://example.com/attachment.png')
+      before do
+        subject.perform
       end
-    end
 
-    context 'with media attachments with focal points' do
       let(:object_json) do
         {
           id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
           type: 'Note',
           content: 'Lorem ipsum',
-          attachment: [
-            {
-              type: 'Document',
-              mediaType: 'image/png',
-              url: 'http://example.com/attachment.png',
-              focalPoint: [0.5, -0.7],
-            },
-          ],
+          cc: ActivityPub::TagManager.instance.uri_for(local_account),
         }
       end
 
@@ -267,143 +507,27 @@ RSpec.describe ActivityPub::Activity::Create do
         status = sender.statuses.first
 
         expect(status).to_not be_nil
-        expect(status.media_attachments.map(&:focus)).to include('0.5,-0.7')
+        expect(status.text).to eq 'Lorem ipsum'
       end
     end
 
-    context 'with media attachments missing url' do
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum',
-          attachment: [
-            {
-              type: 'Document',
-              mediaType: 'image/png',
-            },
-          ],
-        }
-      end
+    context 'when the sender has no relevance to local activity' do
+      subject { described_class.new(json, sender, delivery: true) }
 
-      it 'creates status' do
-        status = sender.statuses.first
-        expect(status).to_not be_nil
+      before do
+        subject.perform
       end
-    end
 
-    context 'with hashtags' do
       let(:object_json) do
         {
           id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
           type: 'Note',
           content: 'Lorem ipsum',
-          tag: [
-            {
-              type: 'Hashtag',
-              href: 'http://example.com/blah',
-              name: '#test',
-            },
-          ],
         }
       end
 
-      it 'creates status' do
-        status = sender.statuses.first
-
-        expect(status).to_not be_nil
-        expect(status.tags.map(&:name)).to include('test')
-      end
-    end
-
-    context 'with hashtags missing name' do
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum',
-          tag: [
-            {
-              type: 'Hashtag',
-              href: 'http://example.com/blah',
-            },
-          ],
-        }
-      end
-
-      it 'creates status' do
-        status = sender.statuses.first
-        expect(status).to_not be_nil
-      end
-    end
-
-    context 'with emojis' do
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum :tinking:',
-          tag: [
-            {
-              type: 'Emoji',
-              icon: {
-                url: 'http://example.com/emoji.png',
-              },
-              name: 'tinking',
-            },
-          ],
-        }
-      end
-
-      it 'creates status' do
-        status = sender.statuses.first
-
-        expect(status).to_not be_nil
-        expect(status.emojis.map(&:shortcode)).to include('tinking')
-      end
-    end
-
-    context 'with emojis missing name' do
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum :tinking:',
-          tag: [
-            {
-              type: 'Emoji',
-              icon: {
-                url: 'http://example.com/emoji.png',
-              },
-            },
-          ],
-        }
-      end
-
-      it 'creates status' do
-        status = sender.statuses.first
-        expect(status).to_not be_nil
-      end
-    end
-
-    context 'with emojis missing icon' do
-      let(:object_json) do
-        {
-          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
-          type: 'Note',
-          content: 'Lorem ipsum :tinking:',
-          tag: [
-            {
-              type: 'Emoji',
-              name: 'tinking',
-            },
-          ],
-        }
-      end
-
-      it 'creates status' do
-        status = sender.statuses.first
-        expect(status).to_not be_nil
+      it 'does not create anything' do
+        expect(sender.statuses.count).to eq 0
       end
     end
   end