diff options
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 |