about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/components/components/collapsable.jsx19
-rw-r--r--app/assets/javascripts/components/features/compose/components/compose_form.jsx44
-rw-r--r--app/assets/javascripts/components/reducers/compose.jsx2
-rw-r--r--app/assets/stylesheets/stream_entries.scss2
-rw-r--r--app/controllers/accounts_controller.rb2
-rw-r--r--app/helpers/atom_builder_helper.rb90
-rw-r--r--app/models/block.rb17
-rw-r--r--app/models/favourite.rb21
-rw-r--r--app/models/follow.rb13
-rw-r--r--app/models/follow_request.rb42
-rw-r--r--app/models/stream_entry.rb8
-rw-r--r--app/services/authorize_follow_service.rb36
-rw-r--r--app/services/block_service.rb24
-rw-r--r--app/services/favourite_service.rb29
-rw-r--r--app/services/fetch_atom_service.rb2
-rw-r--r--app/services/follow_service.rb49
-rw-r--r--app/services/process_interaction_service.rb8
-rw-r--r--app/services/reject_follow_service.rb36
-rw-r--r--app/services/remove_status_service.rb4
-rw-r--r--app/services/unblock_service.rb26
-rw-r--r--app/services/unfavourite_service.rb29
-rw-r--r--app/services/unfollow_service.rb26
-rw-r--r--app/views/accounts/show.atom.ruby1
-rw-r--r--app/views/stream_entries/_favourite.html.haml5
-rw-r--r--app/views/stream_entries/_follow.html.haml5
-rw-r--r--app/workers/after_remote_follow_request_worker.rb2
-rw-r--r--app/workers/after_remote_follow_worker.rb2
-rw-r--r--app/workers/pubsubhubbub/distribution_worker.rb9
-rw-r--r--config/routes.rb1
-rw-r--r--docs/Using-Mastodon/List-of-Mastodon-instances.md1
-rw-r--r--spec/lib/tag_manager_spec.rb32
-rw-r--r--spec/models/favourite_spec.rb36
-rw-r--r--spec/models/follow_spec.rb30
-rw-r--r--spec/models/stream_entry_spec.rb14
-rw-r--r--spec/services/authorize_follow_service_spec.rb49
-rw-r--r--spec/services/block_service_spec.rb34
-rw-r--r--spec/services/favourite_service_spec.rb36
-rw-r--r--spec/services/follow_service_spec.rb72
-rw-r--r--spec/services/process_interaction_service_spec.rb96
-rw-r--r--spec/services/reject_follow_service_spec.rb49
-rw-r--r--spec/services/unblock_service_spec.rb36
-rw-r--r--spec/services/unfollow_service_spec.rb37
42 files changed, 729 insertions, 347 deletions
diff --git a/app/assets/javascripts/components/components/collapsable.jsx b/app/assets/javascripts/components/components/collapsable.jsx
new file mode 100644
index 000000000..aeebb4b0f
--- /dev/null
+++ b/app/assets/javascripts/components/components/collapsable.jsx
@@ -0,0 +1,19 @@
+import { Motion, spring } from 'react-motion';
+
+const Collapsable = ({ fullHeight, isVisible, children }) => (
+  <Motion defaultStyle={{ opacity: !isVisible ? 0 : 100, height: isVisible ? fullHeight : 0 }} style={{ opacity: spring(!isVisible ? 0 : 100), height: spring(!isVisible ? 0 : fullHeight) }}>
+    {({ opacity, height }) =>
+      <div style={{ height: `${height}px`, overflow: 'hidden', opacity: opacity / 100, display: Math.floor(opacity) === 0 ? 'none' : 'block' }}>
+        {children}
+      </div>
+    }
+  </Motion>
+);
+
+Collapsable.propTypes = {
+  fullHeight: React.PropTypes.number.isRequired,
+  isVisible: React.PropTypes.bool.isRequired,
+  children: React.PropTypes.node.isRequired
+};
+
+export default Collapsable;
diff --git a/app/assets/javascripts/components/features/compose/components/compose_form.jsx b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
index 8019382cd..166c5fdce 100644
--- a/app/assets/javascripts/components/features/compose/components/compose_form.jsx
+++ b/app/assets/javascripts/components/features/compose/components/compose_form.jsx
@@ -10,7 +10,7 @@ import { debounce } from 'react-decoration';
 import UploadButtonContainer from '../containers/upload_button_container';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import Toggle from 'react-toggle';
-import { Motion, spring } from 'react-motion';
+import Collapsable from '../../../components/collapsable';
 
 const messages = defineMessages({
   placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
@@ -136,13 +136,11 @@ const ComposeForm = React.createClass({
 
     return (
       <div style={{ padding: '10px' }}>
-        <Motion defaultStyle={{ opacity: !this.props.spoiler ? 0 : 100, height: !this.props.spoiler ? 50 : 0 }} style={{ opacity: spring(!this.props.spoiler ? 0 : 100), height: spring(!this.props.spoiler ? 0 : 50) }}>
-          {({ opacity, height }) =>
-            <div className="spoiler-input" style={{ height: `${height}px`, overflow: 'hidden', opacity: opacity / 100 }}>
-              <input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} type="text" className="spoiler-input__input" />
-            </div>
-          }
-        </Motion>
+        <Collapsable isVisible={this.props.spoiler} fullHeight={50}>
+          <div className="spoiler-input">
+            <input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} type="text" className="spoiler-input__input" />
+          </div>
+        </Collapsable>
 
         {replyArea}
 
@@ -183,23 +181,19 @@ const ComposeForm = React.createClass({
           }
         </Motion>
 
-        <Motion defaultStyle={{ opacity: (this.props.private || reply_to_other) ? 0 : 100, height: (this.props.private || reply_to_other) ? 39.5 : 0 }} style={{ opacity: spring((this.props.private || reply_to_other) ? 0 : 100), height: spring((this.props.private || reply_to_other) ? 0 : 39.5) }}>
-          {({ opacity, height }) =>
-            <label className='compose-form__label' style={{ height: `${height}px`, overflow: 'hidden', opacity: opacity / 100 }}>
-              <Toggle checked={this.props.unlisted} onChange={this.handleChangeListability} />
-              <span className='compose-form__label__text'><FormattedMessage id='compose_form.unlisted' defaultMessage='Do not display in public timeline' /></span>
-            </label>
-          }
-        </Motion>
-
-        <Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(this.props.media_count === 0 ? 0 : 100), height: spring(this.props.media_count === 0 ? 0 : 39.5) }}>
-          {({ opacity, height }) =>
-            <label className='compose-form__label' style={{ height: `${height}px`, overflow: 'hidden', opacity: opacity / 100 }}>
-              <Toggle checked={this.props.sensitive} onChange={this.handleChangeSensitivity} />
-              <span className='compose-form__label__text'><FormattedMessage id='compose_form.sensitive' defaultMessage='Mark media as sensitive' /></span>
-            </label>
-          }
-        </Motion>
+        <Collapsable isVisible={!(this.props.private || reply_to_other)} fullHeight={39.5}>
+          <label className='compose-form__label'>
+            <Toggle checked={this.props.unlisted} onChange={this.handleChangeListability} />
+            <span className='compose-form__label__text'><FormattedMessage id='compose_form.unlisted' defaultMessage='Do not display in public timeline' /></span>
+          </label>
+        </Collapsable>
+
+        <Collapsable isVisible={this.props.media_count > 0} fullHeight={39.5}>
+          <label className='compose-form__label'>
+            <Toggle checked={this.props.sensitive} onChange={this.handleChangeSensitivity} />
+            <span className='compose-form__label__text'><FormattedMessage id='compose_form.sensitive' defaultMessage='Mark media as sensitive' /></span>
+          </label>
+        </Collapsable>
       </div>
     );
   }
diff --git a/app/assets/javascripts/components/reducers/compose.jsx b/app/assets/javascripts/components/reducers/compose.jsx
index 042a2c67d..ef8cde75b 100644
--- a/app/assets/javascripts/components/reducers/compose.jsx
+++ b/app/assets/javascripts/components/reducers/compose.jsx
@@ -89,7 +89,7 @@ function removeMedia(state, mediaId) {
     map.update('text', text => text.replace(media.get('text_url'), '').trim());
 
     if (prevSize === 1) {
-      map.update('sensitive', false);
+      map.set('sensitive', false);
     }
   });
 };
diff --git a/app/assets/stylesheets/stream_entries.scss b/app/assets/stylesheets/stream_entries.scss
index d427c1466..3b2e88f6d 100644
--- a/app/assets/stylesheets/stream_entries.scss
+++ b/app/assets/stylesheets/stream_entries.scss
@@ -3,7 +3,7 @@
   box-shadow: 0 0 15px rgba($color8, 0.2);
 
   .entry {
-    background: lighten($color2, 8%);
+    background: $color5;
 
     .detailed-status.light, .status.light {
       border-bottom: 1px solid $color2;
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index b837f006e..00f8047fd 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -16,7 +16,7 @@ class AccountsController < ApplicationController
       end
 
       format.atom do
-        @entries = @account.stream_entries.order('id desc').where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
+        @entries = @account.stream_entries.order('id desc').where(activity_type: 'Status').where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
       end
 
       format.activitystreams2
diff --git a/app/helpers/atom_builder_helper.rb b/app/helpers/atom_builder_helper.rb
index 08d70b7ac..d73f09aaf 100644
--- a/app/helpers/atom_builder_helper.rb
+++ b/app/helpers/atom_builder_helper.rb
@@ -90,6 +90,10 @@ module AtomBuilderHelper
     xml.link(rel: 'self', type: 'application/atom+xml', href: url)
   end
 
+  def link_next(xml, url)
+    xml.link(rel: 'next', type: 'application/atom+xml', href: url)
+  end
+
   def link_hub(xml, url)
     xml.link(rel: 'hub', href: url)
   end
@@ -167,6 +171,52 @@ module AtomBuilderHelper
     end
   end
 
+  def include_target(xml, target)
+    simple_id xml, TagManager.instance.uri_for(target)
+
+    if target.object_type == :person
+      include_author xml, target
+    else
+      object_type    xml, target.object_type
+      verb           xml, target.verb
+      title          xml, target.title
+      link_alternate xml, TagManager.instance.url_for(target)
+    end
+
+    # Statuses have content and author
+    return unless target.is_a?(Status)
+
+    rich_content xml, target
+    verb         xml, target.verb
+    published_at xml, target.created_at
+    updated_at   xml, target.updated_at
+
+    author(xml) do
+      include_author xml, target.account
+    end
+
+    if target.reply?
+      in_reply_to xml, TagManager.instance.uri_for(target.thread), TagManager.instance.url_for(target.thread)
+    end
+
+    link_visibility xml, target
+
+    target.mentions.each do |mention|
+      link_mention xml, mention.account
+    end
+
+    target.media_attachments.each do |media|
+      link_enclosure xml, media
+    end
+
+    target.tags.each do |tag|
+      category xml, tag.name
+    end
+
+    category(xml, 'nsfw') if target.sensitive?
+    privacy_scope(xml, target.visibility)
+  end
+
   def include_entry(xml, stream_entry)
     unique_id      xml, stream_entry.created_at, stream_entry.activity_id, stream_entry.activity_type
     published_at   xml, stream_entry.created_at
@@ -185,45 +235,7 @@ module AtomBuilderHelper
 
     if stream_entry.targeted?
       target(xml) do
-        simple_id xml, TagManager.instance.uri_for(stream_entry.target)
-
-        if stream_entry.target.object_type == :person
-          include_author xml, stream_entry.target
-        else
-          object_type    xml, stream_entry.target.object_type
-          verb           xml, stream_entry.target.verb
-          title          xml, stream_entry.target.title
-          link_alternate xml, TagManager.instance.url_for(stream_entry.target)
-        end
-
-        # Statuses have content and author
-        if stream_entry.target.is_a?(Status)
-          rich_content xml, stream_entry.target
-          verb         xml, stream_entry.target.verb
-          published_at xml, stream_entry.target.created_at
-          updated_at   xml, stream_entry.target.updated_at
-
-          author(xml) do
-            include_author xml, stream_entry.target.account
-          end
-
-          link_visibility xml, stream_entry.target
-
-          stream_entry.target.mentions.each do |mention|
-            link_mention xml, mention.account
-          end
-
-          stream_entry.target.media_attachments.each do |media|
-            link_enclosure xml, media
-          end
-
-          stream_entry.target.tags.each do |tag|
-            category xml, tag.name
-          end
-
-          category(xml, 'nsfw') if stream_entry.target.sensitive?
-          privacy_scope(xml, stream_entry.target.visibility)
-        end
+        include_target(xml, stream_entry.target)
       end
     end
 
diff --git a/app/models/block.rb b/app/models/block.rb
index d0662b685..9c55703c9 100644
--- a/app/models/block.rb
+++ b/app/models/block.rb
@@ -2,27 +2,10 @@
 
 class Block < ApplicationRecord
   include Paginable
-  include Streamable
 
   belongs_to :account
   belongs_to :target_account, class_name: 'Account'
 
   validates :account, :target_account, presence: true
   validates :account_id, uniqueness: { scope: :target_account_id }
-
-  def verb
-    destroyed? ? :unblock : :block
-  end
-
-  def target
-    target_account
-  end
-
-  def hidden?
-    true
-  end
-
-  def title
-    destroyed? ? "#{account.acct} is no longer blocking #{target_account.acct}" : "#{account.acct} blocked #{target_account.acct}"
-  end
 end
diff --git a/app/models/favourite.rb b/app/models/favourite.rb
index 82f8c5258..67a293888 100644
--- a/app/models/favourite.rb
+++ b/app/models/favourite.rb
@@ -2,7 +2,6 @@
 
 class Favourite < ApplicationRecord
   include Paginable
-  include Streamable
 
   belongs_to :account, inverse_of: :favourites
   belongs_to :status,  inverse_of: :favourites
@@ -11,26 +10,6 @@ class Favourite < ApplicationRecord
 
   validates :status_id, uniqueness: { scope: :account_id }
 
-  def verb
-    destroyed? ? :unfavorite : :favorite
-  end
-
-  def title
-    destroyed? ? "#{account.acct} no longer favourites a status by #{status.account.acct}" : "#{account.acct} favourited a status by #{status.account.acct}"
-  end
-
-  def thread
-    status
-  end
-
-  def target
-    thread
-  end
-
-  def hidden?
-    status.private_visibility?
-  end
-
   before_validation do
     self.status = status.reblog if status.reblog?
   end
diff --git a/app/models/follow.rb b/app/models/follow.rb
index e25c2bc9f..57db8c462 100644
--- a/app/models/follow.rb
+++ b/app/models/follow.rb
@@ -2,7 +2,6 @@
 
 class Follow < ApplicationRecord
   include Paginable
-  include Streamable
 
   belongs_to :account
   belongs_to :target_account, class_name: 'Account'
@@ -11,16 +10,4 @@ class Follow < ApplicationRecord
 
   validates :account, :target_account, presence: true
   validates :account_id, uniqueness: { scope: :target_account_id }
-
-  def verb
-    destroyed? ? :unfollow : :follow
-  end
-
-  def target
-    target_account
-  end
-
-  def title
-    destroyed? ? "#{account.acct} is no longer following #{target_account.acct}" : "#{account.acct} started following #{target_account.acct}"
-  end
 end
diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb
index 2755ba0ab..4224ab15d 100644
--- a/app/models/follow_request.rb
+++ b/app/models/follow_request.rb
@@ -2,7 +2,6 @@
 
 class FollowRequest < ApplicationRecord
   include Paginable
-  include Streamable
 
   belongs_to :account
   belongs_to :target_account, class_name: 'Account'
@@ -13,9 +12,6 @@ class FollowRequest < ApplicationRecord
   validates :account_id, uniqueness: { scope: :target_account_id }
 
   def authorize!
-    @verb   = :authorize
-    @target = clone.freeze
-
     account.follow!(target_account)
     MergeWorker.perform_async(target_account.id, account.id)
 
@@ -23,44 +19,6 @@ class FollowRequest < ApplicationRecord
   end
 
   def reject!
-    @verb   = :reject
-    @target = clone.freeze
-
     destroy!
   end
-
-  def verb
-    destroyed? ? (@verb || :delete) : :request_friend
-  end
-
-  def target
-    if destroyed? && @verb
-      @target
-    else
-      target_account
-    end
-  end
-
-  def hidden?
-    true
-  end
-
-  def needs_stream_entry?
-    true
-  end
-
-  def title
-    if destroyed?
-      case @verb
-      when :authorize
-        "#{target_account.acct} authorized #{account.acct}'s request to follow"
-      when :reject
-        "#{target_account.acct} rejected #{account.acct}'s request to follow"
-      else
-        "#{account.acct} withdrew the request to follow #{target_account.acct}"
-      end
-    else
-      "#{account.acct} requested to follow #{target_account.acct}"
-    end
-  end
 end
diff --git a/app/models/stream_entry.rb b/app/models/stream_entry.rb
index bb68b1e14..8b41c8c39 100644
--- a/app/models/stream_entry.rb
+++ b/app/models/stream_entry.rb
@@ -6,17 +6,13 @@ class StreamEntry < ApplicationRecord
   belongs_to :account, inverse_of: :stream_entries
   belongs_to :activity, polymorphic: true
 
-  belongs_to :status,         foreign_type: 'Status',        foreign_key: 'activity_id'
-  belongs_to :follow,         foreign_type: 'Follow',        foreign_key: 'activity_id'
-  belongs_to :favourite,      foreign_type: 'Favourite',     foreign_key: 'activity_id'
-  belongs_to :block,          foreign_type: 'Block',         foreign_key: 'activity_id'
-  belongs_to :follow_request, foreign_type: 'FollowRequest', foreign_key: 'activity_id'
+  belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id', inverse_of: :stream_entry
 
   validates :account, :activity, presence: true
 
   STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, mentions: :account], thread: [:stream_entry, :account]].freeze
 
-  scope :with_includes, -> { includes(:account, status: STATUS_INCLUDES, favourite: [:account, :stream_entry, status: STATUS_INCLUDES], follow: [:target_account, :stream_entry]) }
+  scope :with_includes, -> { includes(:account, status: STATUS_INCLUDES) }
 
   def object_type
     if orphaned?
diff --git a/app/services/authorize_follow_service.rb b/app/services/authorize_follow_service.rb
index 5370b4b61..ac465bdb2 100644
--- a/app/services/authorize_follow_service.rb
+++ b/app/services/authorize_follow_service.rb
@@ -1,12 +1,40 @@
 # frozen_string_literal: true
 
 class AuthorizeFollowService < BaseService
-  include StreamEntryRenderer
-
   def call(source_account, target_account)
     follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account)
     follow_request.authorize!
-    NotificationWorker.perform_async(stream_entry_to_xml(follow_request.stream_entry), target_account.id, source_account.id) unless source_account.local?
-    follow_request.stream_entry.destroy
+    NotificationWorker.perform_async(build_xml(follow_request), target_account.id, source_account.id) unless source_account.local?
+  end
+
+  private
+
+  def build_xml(follow_request)
+    Nokogiri::XML::Builder.new do |xml|
+      entry(xml, true) do
+        unique_id xml, Time.now.utc, follow_request.id, 'FollowRequest'
+        title xml, "#{follow_request.target_account.acct} authorizes follow request by #{follow_request.account.acct}"
+
+        author(xml) do
+          include_author xml, follow_request.target_account
+        end
+
+        object_type xml, :activity
+        verb xml, :authorize
+
+        target(xml) do
+          author(xml) do
+            include_author xml, follow_request.account
+          end
+
+          object_type xml, :activity
+          verb xml, :request_friend
+
+          target(xml) do
+            include_author xml, follow_request.target_account
+          end
+        end
+      end
+    end.to_xml
   end
 end
diff --git a/app/services/block_service.rb b/app/services/block_service.rb
index 095d2a8eb..bd914d8be 100644
--- a/app/services/block_service.rb
+++ b/app/services/block_service.rb
@@ -12,6 +12,28 @@ class BlockService < BaseService
     block = account.block!(target_account)
 
     BlockWorker.perform_async(account.id, target_account.id)
-    NotificationWorker.perform_async(stream_entry_to_xml(block.stream_entry), account.id, target_account.id) unless target_account.local?
+    NotificationWorker.perform_async(build_xml(block), account.id, target_account.id) unless target_account.local?
+  end
+
+  private
+
+  def build_xml(block)
+    Nokogiri::XML::Builder.new do |xml|
+      entry(xml, true) do
+        unique_id xml, block.created_at, block.id, 'Block'
+        title xml, "#{block.account.acct} no longer wishes to interact with #{block.target_account.acct}"
+
+        author(xml) do
+          include_author xml, block.account
+        end
+
+        object_type xml, :activity
+        verb xml, :block
+
+        target(xml) do
+          include_author xml, block.target_account
+        end
+      end
+    end.to_xml
   end
 end
diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb
index ce1722b77..824729ed6 100644
--- a/app/services/favourite_service.rb
+++ b/app/services/favourite_service.rb
@@ -1,8 +1,6 @@
 # frozen_string_literal: true
 
 class FavouriteService < BaseService
-  include StreamEntryRenderer
-
   # Favourite a status and notify remote user
   # @param [Account] account
   # @param [Status] status
@@ -12,14 +10,35 @@ class FavouriteService < BaseService
 
     favourite = Favourite.create!(account: account, status: status)
 
-    Pubsubhubbub::DistributionWorker.perform_async(favourite.stream_entry.id)
-
     if status.local?
       NotifyService.new.call(favourite.status.account, favourite)
     else
-      NotificationWorker.perform_async(stream_entry_to_xml(favourite.stream_entry), account.id, status.account_id)
+      NotificationWorker.perform_async(build_xml(favourite), account.id, status.account_id)
     end
 
     favourite
   end
+
+  private
+
+  def build_xml(favourite)
+    Nokogiri::XML::Builder.new do |xml|
+      entry(xml, true) do
+        unique_id xml, favourite.created_at, favourite.id, 'Favourite'
+        title xml, "#{favourite.account.acct} favourited a status by #{favourite.status.account.acct}"
+
+        author(xml) do
+          include_author xml, favourite.account
+        end
+
+        object_type xml, :activity
+        verb xml, :favorite
+        in_reply_to xml, TagManager.instance.uri_for(favourite.status), TagManager.instance.url_for(favourite.status)
+
+        target(xml) do
+          include_target xml, favourite.status
+        end
+      end
+    end.to_xml
+  end
 end
diff --git a/app/services/fetch_atom_service.rb b/app/services/fetch_atom_service.rb
index 98ee1db84..f7e9c150a 100644
--- a/app/services/fetch_atom_service.rb
+++ b/app/services/fetch_atom_service.rb
@@ -2,6 +2,8 @@
 
 class FetchAtomService < BaseService
   def call(url)
+    return if url.blank?
+
     response = http_client.head(url)
 
     Rails.logger.debug "Remote status HEAD request returned code #{response.code}"
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index ac0392d16..d67b1bf2d 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -7,7 +7,7 @@ class FollowService < BaseService
   # @param [Account] source_account From which to follow
   # @param [String] uri User URI to follow in the form of username@domain
   def call(source_account, uri)
-    target_account = follow_remote_account_service.call(uri)
+    target_account = FollowRemoteAccountService.new.call(uri)
 
     raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
     raise Mastodon::NotPermitted       if target_account.blocking?(source_account) || source_account.blocking?(target_account)
@@ -27,7 +27,7 @@ class FollowService < BaseService
     if target_account.local?
       NotifyService.new.call(target_account, follow_request)
     else
-      NotificationWorker.perform_async(stream_entry_to_xml(follow_request.stream_entry), source_account.id, target_account.id)
+      NotificationWorker.perform_async(build_follow_request_xml(follow_request), source_account.id, target_account.id)
       AfterRemoteFollowRequestWorker.perform_async(follow_request.id)
     end
 
@@ -40,13 +40,12 @@ class FollowService < BaseService
     if target_account.local?
       NotifyService.new.call(target_account, follow)
     else
-      subscribe_service.call(target_account) unless target_account.subscribed?
-      NotificationWorker.perform_async(stream_entry_to_xml(follow.stream_entry), source_account.id, target_account.id)
+      SubscribeService.new.call(target_account) unless target_account.subscribed?
+      NotificationWorker.perform_async(build_follow_xml(follow), source_account.id, target_account.id)
       AfterRemoteFollowWorker.perform_async(follow.id)
     end
 
     MergeWorker.perform_async(target_account.id, source_account.id)
-    Pubsubhubbub::DistributionWorker.perform_async(follow.stream_entry.id)
 
     follow
   end
@@ -55,11 +54,43 @@ class FollowService < BaseService
     Redis.current
   end
 
-  def follow_remote_account_service
-    @follow_remote_account_service ||= FollowRemoteAccountService.new
+  def build_follow_request_xml(follow_request)
+    Nokogiri::XML::Builder.new do |xml|
+      entry(xml, true) do
+        unique_id xml, follow_request.created_at, follow_request.id, 'FollowRequest'
+        title xml, "#{follow_request.account.acct} requested to follow #{follow_request.target_account.acct}"
+
+        author(xml) do
+          include_author xml, follow_request.account
+        end
+
+        object_type xml, :activity
+        verb xml, :request_friend
+
+        target(xml) do
+          include_author xml, follow_request.target_account
+        end
+      end
+    end.to_xml
   end
 
-  def subscribe_service
-    @subscribe_service ||= SubscribeService.new
+  def build_follow_xml(follow)
+    Nokogiri::XML::Builder.new do |xml|
+      entry(xml, true) do
+        unique_id xml, follow.created_at, follow.id, 'Follow'
+        title xml, "#{follow.account.acct} started following #{follow.target_account.acct}"
+
+        author(xml) do
+          include_author xml, follow.account
+        end
+
+        object_type xml, :activity
+        verb xml, :follow
+
+        target(xml) do
+          include_author xml, follow.target_account
+        end
+      end
+    end.to_xml
   end
 end
diff --git a/app/services/process_interaction_service.rb b/app/services/process_interaction_service.rb
index 8420ca351..c74ff9e22 100644
--- a/app/services/process_interaction_service.rb
+++ b/app/services/process_interaction_service.rb
@@ -39,6 +39,8 @@ class ProcessInteractionService < BaseService
         unfollow!(account, target_account)
       when :favorite
         favourite!(xml, account)
+      when :unfavorite
+        unfavourite!(xml, account)
       when :post
         add_post!(body, account) if mentions_account?(xml, target_account)
       when :share
@@ -121,6 +123,12 @@ class ProcessInteractionService < BaseService
     NotifyService.new.call(current_status.account, favourite)
   end
 
+  def unfavourite!(xml, from_account)
+    current_status = status(xml)
+    favourite = current_status.favourites.where(account: from_account).first
+    favourite&.destroy
+  end
+
   def add_post!(body, account)
     process_feed_service.call(body, account)
   end
diff --git a/app/services/reject_follow_service.rb b/app/services/reject_follow_service.rb
index a17d6a7be..1b03d62e6 100644
--- a/app/services/reject_follow_service.rb
+++ b/app/services/reject_follow_service.rb
@@ -1,12 +1,40 @@
 # frozen_string_literal: true
 
 class RejectFollowService < BaseService
-  include StreamEntryRenderer
-
   def call(source_account, target_account)
     follow_request = FollowRequest.find_by!(account: source_account, target_account: target_account)
     follow_request.reject!
-    NotificationWorker.perform_async(stream_entry_to_xml(follow_request.stream_entry), target_account.id, source_account.id) unless source_account.local?
-    follow_request.stream_entry.destroy
+    NotificationWorker.perform_async(build_xml(follow_request), target_account.id, source_account.id) unless source_account.local?
+  end
+
+  private
+
+  def build_xml(follow_request)
+    Nokogiri::XML::Builder.new do |xml|
+      entry(xml, true) do
+        unique_id xml, Time.now.utc, follow_request.id, 'FollowRequest'
+        title xml, "#{follow_request.target_account.acct} rejects follow request by #{follow_request.account.acct}"
+
+        author(xml) do
+          include_author xml, follow_request.target_account
+        end
+
+        object_type xml, :activity
+        verb xml, :reject
+
+        target(xml) do
+          author(xml) do
+            include_author xml, follow_request.account
+          end
+
+          object_type xml, :activity
+          verb xml, :request_friend
+
+          target(xml) do
+            include_author xml, follow_request.target_account
+          end
+        end
+      end
+    end.to_xml
   end
 end
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index b1a646b14..73b545f17 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -32,12 +32,16 @@ class RemoveStatusService < BaseService
   end
 
   def remove_from_mentioned(status)
+    notified_domains = []
+
     status.mentions.each do |mention|
       mentioned_account = mention.account
 
       if mentioned_account.local?
         unpush(:mentions, mentioned_account, status)
       else
+        next if notified_domains.include?(mentioned_account.domain)
+        notified_domains << mentioned_account.domain
         send_delete_salmon(mentioned_account, status)
       end
     end
diff --git a/app/services/unblock_service.rb b/app/services/unblock_service.rb
index 84b1050c1..c4f789f74 100644
--- a/app/services/unblock_service.rb
+++ b/app/services/unblock_service.rb
@@ -1,12 +1,32 @@
 # frozen_string_literal: true
 
 class UnblockService < BaseService
-  include StreamEntryRenderer
-
   def call(account, target_account)
     return unless account.blocking?(target_account)
 
     unblock = account.unblock!(target_account)
-    NotificationWorker.perform_async(stream_entry_to_xml(unblock.stream_entry), account.id, target_account.id) unless target_account.local?
+    NotificationWorker.perform_async(build_xml(unblock), account.id, target_account.id) unless target_account.local?
+  end
+
+  private
+
+  def build_xml(block)
+    Nokogiri::XML::Builder.new do |xml|
+      entry(xml, true) do
+        unique_id xml, Time.now.utc, block.id, 'Block'
+        title xml, "#{block.account.acct} no longer blocks #{block.target_account.acct}"
+
+        author(xml) do
+          include_author xml, block.account
+        end
+
+        object_type xml, :activity
+        verb xml, :unblock
+
+        target(xml) do
+          include_author xml, block.target_account
+        end
+      end
+    end.to_xml
   end
 end
diff --git a/app/services/unfavourite_service.rb b/app/services/unfavourite_service.rb
index 04293ee08..1d3e6f06d 100644
--- a/app/services/unfavourite_service.rb
+++ b/app/services/unfavourite_service.rb
@@ -1,16 +1,35 @@
 # frozen_string_literal: true
 
 class UnfavouriteService < BaseService
-  include StreamEntryRenderer
-
   def call(account, status)
     favourite = Favourite.find_by!(account: account, status: status)
     favourite.destroy!
 
-    unless status.local?
-      NotificationWorker.perform_async(stream_entry_to_xml(favourite.stream_entry), account.id, status.account_id)
-    end
+    NotificationWorker.perform_async(build_xml(favourite), account.id, status.account_id) unless status.local?
 
     favourite
   end
+
+  private
+
+  def build_xml(favourite)
+    Nokogiri::XML::Builder.new do |xml|
+      entry(xml, true) do
+        unique_id xml, Time.now.utc, favourite.id, 'Favourite'
+        title xml, "#{favourite.account.acct} no longer favourites a status by #{favourite.status.account.acct}"
+
+        author(xml) do
+          include_author xml, favourite.account
+        end
+
+        object_type xml, :activity
+        verb xml, :unfavorite
+        in_reply_to xml, TagManager.instance.uri_for(favourite.status), TagManager.instance.url_for(favourite.status)
+
+        target(xml) do
+          include_target xml, favourite.status
+        end
+      end
+    end.to_xml
+  end
 end
diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb
index 178da4da3..07f9b93dd 100644
--- a/app/services/unfollow_service.rb
+++ b/app/services/unfollow_service.rb
@@ -1,14 +1,34 @@
 # frozen_string_literal: true
 
 class UnfollowService < BaseService
-  include StreamEntryRenderer
-
   # Unfollow and notify the remote user
   # @param [Account] source_account Where to unfollow from
   # @param [Account] target_account Which to unfollow
   def call(source_account, target_account)
     follow = source_account.unfollow!(target_account)
-    NotificationWorker.perform_async(stream_entry_to_xml(follow.stream_entry), source_account.id, target_account.id) unless target_account.local?
+    NotificationWorker.perform_async(build_xml(follow), source_account.id, target_account.id) unless target_account.local?
     UnmergeWorker.perform_async(target_account.id, source_account.id)
   end
+
+  private
+
+  def build_xml(follow)
+    Nokogiri::XML::Builder.new do |xml|
+      entry(xml, true) do
+        unique_id xml, Time.now.utc, follow.id, 'Follow'
+        title xml, "#{follow.account.acct} is no longer following #{follow.target_account.acct}"
+
+        author(xml) do
+          include_author xml, follow.account
+        end
+
+        object_type xml, :activity
+        verb xml, :unfollow
+
+        target(xml) do
+          include_author xml, follow.target_account
+        end
+      end
+    end.to_xml
+  end
 end
diff --git a/app/views/accounts/show.atom.ruby b/app/views/accounts/show.atom.ruby
index a22568396..3832b8bdc 100644
--- a/app/views/accounts/show.atom.ruby
+++ b/app/views/accounts/show.atom.ruby
@@ -14,6 +14,7 @@ Nokogiri::XML::Builder.new do |xml|
 
     link_alternate xml, TagManager.instance.url_for(@account)
     link_self      xml, account_url(@account, format: 'atom')
+    link_next      xml, account_url(@account, format: 'atom', max_id: @entries.last.id) if @entries.size == 20
     link_hub       xml, api_push_url
     link_salmon    xml, api_salmon_url(@account.id)
 
diff --git a/app/views/stream_entries/_favourite.html.haml b/app/views/stream_entries/_favourite.html.haml
deleted file mode 100644
index ea4879328..000000000
--- a/app/views/stream_entries/_favourite.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-.entry.entry-favourite
-  .content.emojify
-    %strong= favourite.account.acct
-    = t('stream_entries.favourited')
-    %strong= favourite.status.account.acct
diff --git a/app/views/stream_entries/_follow.html.haml b/app/views/stream_entries/_follow.html.haml
deleted file mode 100644
index da6d062f0..000000000
--- a/app/views/stream_entries/_follow.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-.entry.entry-follow
-  .content.emojify
-    %strong= link_to follow.account.acct, account_path(follow.account)
-    = t('stream_entries.is_now_following')
-    %strong= link_to follow.target_account.acct, TagManager.instance.url_for(follow.target_account)
diff --git a/app/workers/after_remote_follow_request_worker.rb b/app/workers/after_remote_follow_request_worker.rb
index ad94d2769..f1d6869cc 100644
--- a/app/workers/after_remote_follow_request_worker.rb
+++ b/app/workers/after_remote_follow_request_worker.rb
@@ -9,7 +9,7 @@ class AfterRemoteFollowRequestWorker
     follow_request  = FollowRequest.find(follow_request_id)
     updated_account = FetchRemoteAccountService.new.call(follow_request.target_account.remote_url)
 
-    return if updated_account.locked?
+    return if updated_account.nil? || updated_account.locked?
 
     follow_request.destroy
     FollowService.new.call(follow_request.account, updated_account.acct)
diff --git a/app/workers/after_remote_follow_worker.rb b/app/workers/after_remote_follow_worker.rb
index 496aaf73e..0d04456a9 100644
--- a/app/workers/after_remote_follow_worker.rb
+++ b/app/workers/after_remote_follow_worker.rb
@@ -9,7 +9,7 @@ class AfterRemoteFollowWorker
     follow          = Follow.find(follow_id)
     updated_account = FetchRemoteAccountService.new.call(follow.target_account.remote_url)
 
-    return unless updated_account.locked?
+    return if updated_account.nil? || !updated_account.locked?
 
     follow.destroy
     FollowService.new.call(follow.account, updated_account.acct)
diff --git a/app/workers/pubsubhubbub/distribution_worker.rb b/app/workers/pubsubhubbub/distribution_worker.rb
index 4576dc4a2..d5437bf6b 100644
--- a/app/workers/pubsubhubbub/distribution_worker.rb
+++ b/app/workers/pubsubhubbub/distribution_worker.rb
@@ -8,18 +8,13 @@ class Pubsubhubbub::DistributionWorker
   def perform(stream_entry_id)
     stream_entry = StreamEntry.find(stream_entry_id)
 
-    # Most hidden stream entries should not be PuSHed,
-    # but statuses need to be distributed to trusted
-    # followers even when they are hidden
-    return if stream_entry.hidden? && stream_entry.activity_type != 'Status'
+    return if stream_entry.hidden?
 
     account  = stream_entry.account
     renderer = AccountsController.renderer.new(method: 'get', http_host: Rails.configuration.x.local_domain, https: Rails.configuration.x.use_https)
     payload  = renderer.render(:show, assigns: { account: account, entries: [stream_entry] }, formats: [:atom])
-    domains  = account.followers_domains
 
-    Subscription.where(account: account).active.select('id, callback_url').find_each do |subscription|
-      next unless domains.include?(Addressable::URI.parse(subscription.callback_url).host)
+    Subscription.where(account: account).active.select('id').find_each do |subscription|
       Pubsubhubbub::DeliveryWorker.perform_async(subscription.id, payload)
     end
   rescue ActiveRecord::RecordNotFound
diff --git a/config/routes.rb b/config/routes.rb
index e17d54995..3da7563fd 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -4,7 +4,6 @@ require 'sidekiq/web'
 
 Rails.application.routes.draw do
   mount LetterOpenerWeb::Engine, at: 'letter_opener' if Rails.env.development?
-  mount ActionCable.server, at: 'cable'
 
   authenticate :user, lambda { |u| u.admin? } do
     mount Sidekiq::Web, at: 'sidekiq', as: :sidekiq
diff --git a/docs/Using-Mastodon/List-of-Mastodon-instances.md b/docs/Using-Mastodon/List-of-Mastodon-instances.md
index c4e228769..b79620fc6 100644
--- a/docs/Using-Mastodon/List-of-Mastodon-instances.md
+++ b/docs/Using-Mastodon/List-of-Mastodon-instances.md
@@ -9,7 +9,6 @@ List of Known Mastodon instances
 | [animalliberation.social](https://animalliberation.social) |Animal Rights|Yes|
 | [socially.constructed.space](https://socially.constructed.space) |Single user|No|
 | [epiktistes.com](https://epiktistes.com) |N/A|Yes|
-| [toot.zone](https://toot.zone) |N/A|Yes|
 | [on.vu](https://on.vu) | Appears defunct|No|
 | [gay.crime.team](https://gay.crime.team) |N/A|Yes(?)|
 | [gnusocial.me](https://gnusocial.me) |Yes, it's a mastodon instance now|Yes|
diff --git a/spec/lib/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb
index b60584253..cbb427a8c 100644
--- a/spec/lib/tag_manager_spec.rb
+++ b/spec/lib/tag_manager_spec.rb
@@ -47,22 +47,6 @@ RSpec.describe TagManager do
         expect(subject).to be_a String
       end
     end
-
-    context 'Follow' do
-      let(:target) { Fabricate(:follow, account: alice, target_account: bob) }
-
-      it 'returns a string' do
-        expect(subject).to be_a String
-      end
-    end
-
-    context 'Favourite' do
-      let(:target) { Fabricate(:favourite, account: bob, status: status) }
-
-      it 'returns a string' do
-        expect(subject).to be_a String
-      end
-    end
   end
 
   describe '#url_for' do
@@ -87,21 +71,5 @@ RSpec.describe TagManager do
         expect(subject).to be_a String
       end
     end
-
-    context 'Follow' do
-      let(:target) { Fabricate(:follow, account: alice, target_account: bob) }
-
-      it 'returns a URL' do
-        expect(subject).to be_a String
-      end
-    end
-
-    context 'Favourite' do
-      let(:target) { Fabricate(:favourite, account: bob, status: status) }
-
-      it 'returns a URL' do
-        expect(subject).to be_a String
-      end
-    end
   end
 end
diff --git a/spec/models/favourite_spec.rb b/spec/models/favourite_spec.rb
index cc3d604d6..5b7126506 100644
--- a/spec/models/favourite_spec.rb
+++ b/spec/models/favourite_spec.rb
@@ -6,40 +6,4 @@ RSpec.describe Favourite, type: :model do
   let(:status) { Fabricate(:status, account: bob) }
 
   subject { Favourite.new(account: alice, status: status) }
-
-  describe '#verb' do
-    it 'is always favorite' do
-      expect(subject.verb).to be :favorite
-    end
-  end
-
-  describe '#title' do
-    it 'describes the favourite' do
-      expect(subject.title).to eql 'alice favourited a status by bob'
-    end
-  end
-
-  describe '#content' do
-    it 'equals the title' do
-      expect(subject.content).to eq subject.title
-    end
-  end
-
-  describe '#object_type' do
-    it 'is an activity' do
-      expect(subject.object_type).to be :activity
-    end
-  end
-
-  describe '#target' do
-    it 'is the status that was favourited' do
-      expect(subject.target).to eq status
-    end
-  end
-
-  describe '#thread' do
-    it 'equals the target' do
-      expect(subject.thread).to eq subject.target
-    end
-  end
 end
diff --git a/spec/models/follow_spec.rb b/spec/models/follow_spec.rb
index bc887b60d..eb21f3e18 100644
--- a/spec/models/follow_spec.rb
+++ b/spec/models/follow_spec.rb
@@ -5,34 +5,4 @@ RSpec.describe Follow, type: :model do
   let(:bob)   { Fabricate(:account, username: 'bob') }
 
   subject { Follow.new(account: alice, target_account: bob) }
-
-  describe '#verb' do
-    it 'is follow' do
-      expect(subject.verb).to be :follow
-    end
-  end
-
-  describe '#title' do
-    it 'describes the follow' do
-      expect(subject.title).to eql 'alice started following bob'
-    end
-  end
-
-  describe '#content' do
-    it 'is the same as the title' do
-      expect(subject.content).to eql subject.title
-    end
-  end
-
-  describe '#object_type' do
-    it 'is an activity' do
-      expect(subject.object_type).to be :activity
-    end
-  end
-
-  describe '#target' do
-    it 'is the person being followed' do
-      expect(subject.target).to eq bob
-    end
-  end
 end
diff --git a/spec/models/stream_entry_spec.rb b/spec/models/stream_entry_spec.rb
index 9ecf6412a..45bf26899 100644
--- a/spec/models/stream_entry_spec.rb
+++ b/spec/models/stream_entry_spec.rb
@@ -3,21 +3,11 @@ require 'rails_helper'
 RSpec.describe StreamEntry, type: :model do
   let(:alice)     { Fabricate(:account, username: 'alice') }
   let(:bob)       { Fabricate(:account, username: 'bob') }
-  let(:follow)    { Fabricate(:follow, account: alice, target_account: bob) }
   let(:status)    { Fabricate(:status, account: alice) }
   let(:reblog)    { Fabricate(:status, account: bob, reblog: status) }
   let(:reply)     { Fabricate(:status, account: bob, thread: status) }
-  let(:favourite) { Fabricate(:favourite, account: alice, status: status) }
 
   describe '#targeted?' do
-    it 'returns true for a follow' do
-      expect(follow.stream_entry.targeted?).to be true
-    end
-
-    it 'returns true for a favourite' do
-      expect(favourite.stream_entry.targeted?).to be true
-    end
-
     it 'returns true for a reblog' do
       expect(reblog.stream_entry.targeted?).to be true
     end
@@ -28,10 +18,6 @@ RSpec.describe StreamEntry, type: :model do
   end
 
   describe '#threaded?' do
-    it 'returns true for a favourite' do
-      expect(favourite.stream_entry.threaded?).to be true
-    end
-
     it 'returns true for a reply' do
       expect(reply.stream_entry.threaded?).to be true
     end
diff --git a/spec/services/authorize_follow_service_spec.rb b/spec/services/authorize_follow_service_spec.rb
new file mode 100644
index 000000000..3f3a2bc56
--- /dev/null
+++ b/spec/services/authorize_follow_service_spec.rb
@@ -0,0 +1,49 @@
+require 'rails_helper'
+
+RSpec.describe AuthorizeFollowService do
+  let(:sender) { Fabricate(:account, username: 'alice') }
+
+  subject { AuthorizeFollowService.new }
+
+  describe 'local' do
+    let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
+
+    before do
+      FollowRequest.create(account: bob, target_account: sender)
+      subject.call(bob, sender)
+    end
+
+    it 'removes follow request' do
+      expect(bob.requested?(sender)).to be false
+    end
+
+    it 'creates follow relation' do
+      expect(bob.following?(sender)).to be true
+    end
+  end
+
+  describe 'remote' do
+    let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
+
+    before do
+      FollowRequest.create(account: bob, target_account: sender)
+      stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
+      subject.call(bob, sender)
+    end
+
+    it 'removes follow request' do
+      expect(bob.requested?(sender)).to be false
+    end
+
+    it 'creates follow relation' do
+      expect(bob.following?(sender)).to be true
+    end
+
+    it 'sends a follow request authorization salmon slap' do
+      expect(a_request(:post, "http://salmon.example.com/").with { |req|
+        xml = OStatus2::Salmon.new.unpack(req.body)
+        xml.match(TagManager::VERBS[:authorize])
+      }).to have_been_made.once
+    end
+  end
+end
diff --git a/spec/services/block_service_spec.rb b/spec/services/block_service_spec.rb
index f6f07fa20..2a54e032e 100644
--- a/spec/services/block_service_spec.rb
+++ b/spec/services/block_service_spec.rb
@@ -1,5 +1,39 @@
 require 'rails_helper'
 
 RSpec.describe BlockService do
+  let(:sender) { Fabricate(:account, username: 'alice') }
+
   subject { BlockService.new }
+
+  describe 'local' do
+    let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
+
+    before do
+      subject.call(sender, bob)
+    end
+
+    it 'creates a blocking relation' do
+      expect(sender.blocking?(bob)).to be true
+    end
+  end
+
+  describe 'remote' do
+    let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
+
+    before do
+      stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
+      subject.call(sender, bob)
+    end
+
+    it 'creates a blocking relation' do
+      expect(sender.blocking?(bob)).to be true
+    end
+
+    it 'sends a block salmon slap' do
+      expect(a_request(:post, "http://salmon.example.com/").with { |req|
+        xml = OStatus2::Salmon.new.unpack(req.body)
+        xml.match(TagManager::VERBS[:block])
+      }).to have_been_made.once
+    end
+  end
 end
diff --git a/spec/services/favourite_service_spec.rb b/spec/services/favourite_service_spec.rb
index eb961c28e..36f1b64d4 100644
--- a/spec/services/favourite_service_spec.rb
+++ b/spec/services/favourite_service_spec.rb
@@ -1,5 +1,41 @@
 require 'rails_helper'
 
 RSpec.describe FavouriteService do
+  let(:sender) { Fabricate(:account, username: 'alice') }
+
   subject { FavouriteService.new }
+
+  describe 'local' do
+    let(:bob)    { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
+    let(:status) { Fabricate(:status, account: bob) }
+
+    before do
+      subject.call(sender, status)
+    end
+
+    it 'creates a favourite' do
+      expect(status.favourites.first).to_not be_nil
+    end
+  end
+
+  describe 'remote' do
+    let(:bob)    { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
+    let(:status) { Fabricate(:status, account: bob, uri: 'tag:example.com:blahblah') }
+
+    before do
+      stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
+      subject.call(sender, status)
+    end
+
+    it 'creates a favourite' do
+      expect(status.favourites.first).to_not be_nil
+    end
+
+    it 'sends a salmon slap' do
+      expect(a_request(:post, "http://salmon.example.com/").with { |req|
+        xml = OStatus2::Salmon.new.unpack(req.body)
+        xml.match(TagManager::VERBS[:favorite])
+      }).to have_been_made.once
+    end
+  end
 end
diff --git a/spec/services/follow_service_spec.rb b/spec/services/follow_service_spec.rb
index 304e0cf71..2ce0fa464 100644
--- a/spec/services/follow_service_spec.rb
+++ b/spec/services/follow_service_spec.rb
@@ -1,9 +1,75 @@
 require 'rails_helper'
 
 RSpec.describe FollowService do
+  let(:sender) { Fabricate(:account, username: 'alice') }
+
   subject { FollowService.new }
 
-  it 'creates a following relation'
-  it 'creates local account for remote user'
-  it 'sends follow to the remote user'
+  context 'local account' do
+    describe 'locked account' do
+      let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, locked: true, username: 'bob')).account }
+
+      before 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
+      end
+    end
+
+    describe 'unlocked account' do
+      let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
+
+      before do
+        subject.call(sender, bob.acct)
+      end
+
+      it 'creates a following relation' do
+        expect(sender.following?(bob)).to be true
+      end
+    end
+  end
+
+  context 'remote account' do
+    describe 'locked account' do
+      let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, locked: true, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
+
+      before do
+        stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
+        subject.call(sender, bob.acct)
+      end
+
+      it 'creates a follow request' do
+        expect(FollowRequest.find_by(account: sender, target_account: bob)).to_not be_nil
+      end
+
+      it 'sends a follow request salmon slap' do
+        expect(a_request(:post, "http://salmon.example.com/").with { |req|
+          xml = OStatus2::Salmon.new.unpack(req.body)
+          xml.match(TagManager::VERBS[:request_friend])
+        }).to have_been_made.once
+      end
+    end
+
+    describe 'unlocked account' do
+      let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
+
+      before do
+        stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
+        subject.call(sender, bob.acct)
+      end
+
+      it 'creates a following relation' do
+        expect(sender.following?(bob)).to be true
+      end
+
+      it 'sends a follow salmon slap' do
+        expect(a_request(:post, "http://salmon.example.com/").with { |req|
+          xml = OStatus2::Salmon.new.unpack(req.body)
+          xml.match(TagManager::VERBS[:follow])
+        }).to have_been_made.once
+      end
+    end
+  end
 end
diff --git a/spec/services/process_interaction_service_spec.rb b/spec/services/process_interaction_service_spec.rb
index 931815dc2..0845e09ed 100644
--- a/spec/services/process_interaction_service_spec.rb
+++ b/spec/services/process_interaction_service_spec.rb
@@ -1,15 +1,93 @@
 require 'rails_helper'
 
 RSpec.describe ProcessInteractionService do
+  let(:receiver) { Fabricate(:user, email: 'alice@example.com', account: Fabricate(:account, username: 'alice')).account }
+  let(:sender)   { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
+
   subject { ProcessInteractionService.new }
 
-  it 'creates account for new remote user'
-  it 'updates account for existing remote user'
-  it 'ignores envelopes that do not address the local user'
-  it 'accepts a status that mentions the local user'
-  it 'accepts a status that is a reply to the local user\'s'
-  it 'accepts a favourite to a status by the local user'
-  it 'accepts a reblog of a status of the local user'
-  it 'accepts a follow of the local user'
-  it 'accepts an unfollow of the local user'
+  describe 'follow request slap' do
+    before do
+      receiver.update(locked: true)
+
+      payload = <<XML
+<entry xmlns="http://www.w3.org/2005/Atom" xmlns:activity="http://activitystrea.ms/spec/1.0/">
+  <author>
+    <name>bob</name>
+    <uri>https://cb6e6126.ngrok.io/users/bob</uri>
+  </author>
+
+  <id>someIdHere</id>
+  <activity:verb>http://activitystrea.ms/schema/1.0/request-friend</activity:verb>
+</entry>
+XML
+
+      envelope = OStatus2::Salmon.new.pack(payload, sender.keypair)
+      subject.call(envelope, receiver)
+    end
+
+    it 'creates a record' do
+      expect(FollowRequest.find_by(account: sender, target_account: receiver)).to_not be_nil
+    end
+  end
+
+  describe 'follow request authorization slap' do
+    before do
+      receiver.update(locked: true)
+      FollowRequest.create(account: sender, target_account: receiver)
+
+      payload = <<XML
+<entry xmlns="http://www.w3.org/2005/Atom" xmlns:activity="http://activitystrea.ms/spec/1.0/">
+  <author>
+    <name>alice</name>
+    <uri>https://cb6e6126.ngrok.io/users/alice</uri>
+  </author>
+
+  <id>someIdHere</id>
+  <activity:verb>http://activitystrea.ms/schema/1.0/authorize</activity:verb>
+</entry>
+XML
+
+      envelope = OStatus2::Salmon.new.pack(payload, receiver.keypair)
+      subject.call(envelope, sender)
+    end
+
+    it 'creates a follow relationship' do
+      expect(Follow.find_by(account: sender, target_account: receiver)).to_not be_nil
+    end
+
+    it 'removes the follow request' do
+      expect(FollowRequest.find_by(account: sender, target_account: receiver)).to be_nil
+    end
+  end
+
+  describe 'follow request rejection slap' do
+    before do
+      receiver.update(locked: true)
+      FollowRequest.create(account: sender, target_account: receiver)
+
+      payload = <<XML
+<entry xmlns="http://www.w3.org/2005/Atom" xmlns:activity="http://activitystrea.ms/spec/1.0/">
+  <author>
+    <name>alice</name>
+    <uri>https://cb6e6126.ngrok.io/users/alice</uri>
+  </author>
+
+  <id>someIdHere</id>
+  <activity:verb>http://activitystrea.ms/schema/1.0/reject</activity:verb>
+</entry>
+XML
+
+      envelope = OStatus2::Salmon.new.pack(payload, receiver.keypair)
+      subject.call(envelope, sender)
+    end
+
+    it 'does not create a follow relationship' do
+      expect(Follow.find_by(account: sender, target_account: receiver)).to be_nil
+    end
+
+    it 'removes the follow request' do
+      expect(FollowRequest.find_by(account: sender, target_account: receiver)).to be_nil
+    end
+  end
 end
diff --git a/spec/services/reject_follow_service_spec.rb b/spec/services/reject_follow_service_spec.rb
new file mode 100644
index 000000000..50749b633
--- /dev/null
+++ b/spec/services/reject_follow_service_spec.rb
@@ -0,0 +1,49 @@
+require 'rails_helper'
+
+RSpec.describe RejectFollowService do
+  let(:sender) { Fabricate(:account, username: 'alice') }
+
+  subject { RejectFollowService.new }
+
+  describe 'local' do
+    let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
+
+    before do
+      FollowRequest.create(account: bob, target_account: sender)
+      subject.call(bob, sender)
+    end
+
+    it 'removes follow request' do
+      expect(bob.requested?(sender)).to be false
+    end
+
+    it 'does not create follow relation' do
+      expect(bob.following?(sender)).to be false
+    end
+  end
+
+  describe 'remote' do
+    let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
+
+    before do
+      FollowRequest.create(account: bob, target_account: sender)
+      stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
+      subject.call(bob, sender)
+    end
+
+    it 'removes follow request' do
+      expect(bob.requested?(sender)).to be false
+    end
+
+    it 'does not create follow relation' do
+      expect(bob.following?(sender)).to be false
+    end
+
+    it 'sends a follow request rejection salmon slap' do
+      expect(a_request(:post, "http://salmon.example.com/").with { |req|
+        xml = OStatus2::Salmon.new.unpack(req.body)
+        xml.match(TagManager::VERBS[:reject])
+      }).to have_been_made.once
+    end
+  end
+end
diff --git a/spec/services/unblock_service_spec.rb b/spec/services/unblock_service_spec.rb
index 126f70ff1..1b9ae1239 100644
--- a/spec/services/unblock_service_spec.rb
+++ b/spec/services/unblock_service_spec.rb
@@ -1,5 +1,41 @@
 require 'rails_helper'
 
 RSpec.describe UnblockService do
+  let(:sender) { Fabricate(:account, username: 'alice') }
+
   subject { UnblockService.new }
+
+  describe 'local' do
+    let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
+
+    before do
+      sender.block!(bob)
+      subject.call(sender, bob)
+    end
+
+    it 'destroys the blocking relation' do
+      expect(sender.blocking?(bob)).to be false
+    end
+  end
+
+  describe 'remote' do
+    let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
+
+    before do
+      sender.block!(bob)
+      stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
+      subject.call(sender, bob)
+    end
+
+    it 'destroys the blocking relation' do
+      expect(sender.following?(bob)).to be false
+    end
+
+    it 'sends an unblock salmon slap' do
+      expect(a_request(:post, "http://salmon.example.com/").with { |req|
+        xml = OStatus2::Salmon.new.unpack(req.body)
+        xml.match(TagManager::VERBS[:unblock])
+      }).to have_been_made.once
+    end
+  end
 end
diff --git a/spec/services/unfollow_service_spec.rb b/spec/services/unfollow_service_spec.rb
index 6541415d0..8ec2148a1 100644
--- a/spec/services/unfollow_service_spec.rb
+++ b/spec/services/unfollow_service_spec.rb
@@ -1,8 +1,41 @@
 require 'rails_helper'
 
 RSpec.describe UnfollowService do
+  let(:sender) { Fabricate(:account, username: 'alice') }
+
   subject { UnfollowService.new }
 
-  it 'destroys the following relation'
-  it 'sends remote interaction for remote user'
+  describe 'local' do
+    let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
+
+    before do
+      sender.follow!(bob)
+      subject.call(sender, bob)
+    end
+
+    it 'destroys the following relation' do
+      expect(sender.following?(bob)).to be false
+    end
+  end
+
+  describe 'remote' do
+    let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://salmon.example.com')).account }
+
+    before do
+      sender.follow!(bob)
+      stub_request(:post, "http://salmon.example.com/").to_return(:status => 200, :body => "", :headers => {})
+      subject.call(sender, bob)
+    end
+
+    it 'destroys the following relation' do
+      expect(sender.following?(bob)).to be false
+    end
+
+    it 'sends an unfollow salmon slap' do
+      expect(a_request(:post, "http://salmon.example.com/").with { |req|
+        xml = OStatus2::Salmon.new.unpack(req.body)
+        xml.match(TagManager::VERBS[:unfollow])
+      }).to have_been_made.once
+    end
+  end
 end