Optimize the process of following someone (#9220)
* Eliminate extra accounts select query from FollowService * Optimistically update follow state in web UI and hide loading bar Fix #6205 * Asynchronize NotifyService in FollowService And fix failing test * Skip Webfinger resolve routine when called from FollowService if possible If an account is ActivityPub, then webfinger re-resolving is not necessary when called from FollowService. Improve options of ResolveAccountService
This commit is contained in:
		
							parent
							
								
									9cfd610484
								
							
						
					
					
						commit
						6d59dfa15d
					
				
					 9 changed files with 67 additions and 26 deletions
				
			
		| 
						 | 
				
			
			@ -17,7 +17,7 @@ class Api::V1::AccountsController < Api::BaseController
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def follow
 | 
			
		||||
    FollowService.new.call(current_user.account, @account.acct, reblogs: truthy_param?(:reblogs))
 | 
			
		||||
    FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs))
 | 
			
		||||
 | 
			
		||||
    options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -145,12 +145,14 @@ export function fetchAccountFail(id, error) {
 | 
			
		|||
export function followAccount(id, reblogs = true) {
 | 
			
		||||
  return (dispatch, getState) => {
 | 
			
		||||
    const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
 | 
			
		||||
    dispatch(followAccountRequest(id));
 | 
			
		||||
    const locked = getState().getIn(['accounts', id, 'locked'], false);
 | 
			
		||||
 | 
			
		||||
    dispatch(followAccountRequest(id, locked));
 | 
			
		||||
 | 
			
		||||
    api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
 | 
			
		||||
      dispatch(followAccountSuccess(response.data, alreadyFollowing));
 | 
			
		||||
    }).catch(error => {
 | 
			
		||||
      dispatch(followAccountFail(error));
 | 
			
		||||
      dispatch(followAccountFail(error, locked));
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -167,10 +169,12 @@ export function unfollowAccount(id) {
 | 
			
		|||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function followAccountRequest(id) {
 | 
			
		||||
export function followAccountRequest(id, locked) {
 | 
			
		||||
  return {
 | 
			
		||||
    type: ACCOUNT_FOLLOW_REQUEST,
 | 
			
		||||
    id,
 | 
			
		||||
    locked,
 | 
			
		||||
    skipLoading: true,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -179,13 +183,16 @@ export function followAccountSuccess(relationship, alreadyFollowing) {
 | 
			
		|||
    type: ACCOUNT_FOLLOW_SUCCESS,
 | 
			
		||||
    relationship,
 | 
			
		||||
    alreadyFollowing,
 | 
			
		||||
    skipLoading: true,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function followAccountFail(error) {
 | 
			
		||||
export function followAccountFail(error, locked) {
 | 
			
		||||
  return {
 | 
			
		||||
    type: ACCOUNT_FOLLOW_FAIL,
 | 
			
		||||
    error,
 | 
			
		||||
    locked,
 | 
			
		||||
    skipLoading: true,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -193,6 +200,7 @@ export function unfollowAccountRequest(id) {
 | 
			
		|||
  return {
 | 
			
		||||
    type: ACCOUNT_UNFOLLOW_REQUEST,
 | 
			
		||||
    id,
 | 
			
		||||
    skipLoading: true,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -201,6 +209,7 @@ export function unfollowAccountSuccess(relationship, statuses) {
 | 
			
		|||
    type: ACCOUNT_UNFOLLOW_SUCCESS,
 | 
			
		||||
    relationship,
 | 
			
		||||
    statuses,
 | 
			
		||||
    skipLoading: true,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -208,6 +217,7 @@ export function unfollowAccountFail(error) {
 | 
			
		|||
  return {
 | 
			
		||||
    type: ACCOUNT_UNFOLLOW_FAIL,
 | 
			
		||||
    error,
 | 
			
		||||
    skipLoading: true,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,10 @@
 | 
			
		|||
import {
 | 
			
		||||
  ACCOUNT_FOLLOW_SUCCESS,
 | 
			
		||||
  ACCOUNT_FOLLOW_REQUEST,
 | 
			
		||||
  ACCOUNT_FOLLOW_FAIL,
 | 
			
		||||
  ACCOUNT_UNFOLLOW_SUCCESS,
 | 
			
		||||
  ACCOUNT_UNFOLLOW_REQUEST,
 | 
			
		||||
  ACCOUNT_UNFOLLOW_FAIL,
 | 
			
		||||
  ACCOUNT_BLOCK_SUCCESS,
 | 
			
		||||
  ACCOUNT_UNBLOCK_SUCCESS,
 | 
			
		||||
  ACCOUNT_MUTE_SUCCESS,
 | 
			
		||||
| 
						 | 
				
			
			@ -37,6 +41,14 @@ const initialState = ImmutableMap();
 | 
			
		|||
 | 
			
		||||
export default function relationships(state = initialState, action) {
 | 
			
		||||
  switch(action.type) {
 | 
			
		||||
  case ACCOUNT_FOLLOW_REQUEST:
 | 
			
		||||
    return state.setIn([action.id, action.locked ? 'requested' : 'following'], true);
 | 
			
		||||
  case ACCOUNT_FOLLOW_FAIL:
 | 
			
		||||
    return state.setIn([action.id, action.locked ? 'requested' : 'following'], false);
 | 
			
		||||
  case ACCOUNT_UNFOLLOW_REQUEST:
 | 
			
		||||
    return state.setIn([action.id, 'following'], false);
 | 
			
		||||
  case ACCOUNT_UNFOLLOW_FAIL:
 | 
			
		||||
    return state.setIn([action.id, 'following'], true);
 | 
			
		||||
  case ACCOUNT_FOLLOW_SUCCESS:
 | 
			
		||||
  case ACCOUNT_UNFOLLOW_SUCCESS:
 | 
			
		||||
  case ACCOUNT_BLOCK_SUCCESS:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,6 +18,6 @@ module AuthorExtractor
 | 
			
		|||
      acct   = "#{username}@#{domain}"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    ResolveAccountService.new.call(acct, update_profile)
 | 
			
		||||
    ResolveAccountService.new.call(acct, update_profile: update_profile)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,9 +7,9 @@ class FollowService < BaseService
 | 
			
		|||
  # @param [Account] source_account From which to follow
 | 
			
		||||
  # @param [String, Account] uri User URI to follow in the form of username@domain (or account record)
 | 
			
		||||
  # @param [true, false, nil] reblogs Whether or not to show reblogs, defaults to true
 | 
			
		||||
  def call(source_account, uri, reblogs: nil)
 | 
			
		||||
  def call(source_account, target_account, reblogs: nil)
 | 
			
		||||
    reblogs = true if reblogs.nil?
 | 
			
		||||
    target_account = uri.is_a?(Account) ? uri : ResolveAccountService.new.call(uri)
 | 
			
		||||
    target_account = ResolveAccountService.new.call(target_account, skip_webfinger: true)
 | 
			
		||||
 | 
			
		||||
    raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
 | 
			
		||||
    raise Mastodon::NotPermittedError  if target_account.blocking?(source_account) || source_account.blocking?(target_account)
 | 
			
		||||
| 
						 | 
				
			
			@ -42,7 +42,7 @@ class FollowService < BaseService
 | 
			
		|||
    follow_request = FollowRequest.create!(account: source_account, target_account: target_account, show_reblogs: reblogs)
 | 
			
		||||
 | 
			
		||||
    if target_account.local?
 | 
			
		||||
      NotifyService.new.call(target_account, follow_request)
 | 
			
		||||
      LocalNotificationWorker.perform_async(target_account.id, follow_request.id, follow_request.class.name)
 | 
			
		||||
    elsif target_account.ostatus?
 | 
			
		||||
      NotificationWorker.perform_async(build_follow_request_xml(follow_request), source_account.id, target_account.id)
 | 
			
		||||
      AfterRemoteFollowRequestWorker.perform_async(follow_request.id)
 | 
			
		||||
| 
						 | 
				
			
			@ -57,7 +57,7 @@ class FollowService < BaseService
 | 
			
		|||
    follow = source_account.follow!(target_account, reblogs: reblogs)
 | 
			
		||||
 | 
			
		||||
    if target_account.local?
 | 
			
		||||
      NotifyService.new.call(target_account, follow)
 | 
			
		||||
      LocalNotificationWorker.perform_async(target_account.id, follow.id, follow.class.name)
 | 
			
		||||
    else
 | 
			
		||||
      Pubsubhubbub::SubscribeWorker.perform_async(target_account.id) unless target_account.subscribed?
 | 
			
		||||
      NotificationWorker.perform_async(build_follow_xml(follow), source_account.id, target_account.id)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,7 +47,7 @@ class ProcessMentionsService < BaseService
 | 
			
		|||
    mentioned_account = mention.account
 | 
			
		||||
 | 
			
		||||
    if mentioned_account.local?
 | 
			
		||||
      LocalNotificationWorker.perform_async(mention.id)
 | 
			
		||||
      LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name)
 | 
			
		||||
    elsif mentioned_account.ostatus? && !@status.stream_entry.hidden?
 | 
			
		||||
      NotificationWorker.perform_async(ostatus_xml, @status.account_id, mentioned_account.id)
 | 
			
		||||
    elsif mentioned_account.activitypub?
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,17 +9,27 @@ class ResolveAccountService < BaseService
 | 
			
		|||
  # Find or create a local account for a remote user.
 | 
			
		||||
  # When creating, look up the user's webfinger and fetch all
 | 
			
		||||
  # important information from their feed
 | 
			
		||||
  # @param [String] uri User URI in the form of username@domain
 | 
			
		||||
  # @param [String, Account] uri User URI in the form of username@domain
 | 
			
		||||
  # @param [Hash] options
 | 
			
		||||
  # @return [Account]
 | 
			
		||||
  def call(uri, update_profile = true, redirected = nil)
 | 
			
		||||
    @username, @domain = uri.split('@')
 | 
			
		||||
    @update_profile    = update_profile
 | 
			
		||||
  def call(uri, options = {})
 | 
			
		||||
    @options = options
 | 
			
		||||
 | 
			
		||||
    return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
 | 
			
		||||
    if uri.is_a?(Account)
 | 
			
		||||
      @account  = uri
 | 
			
		||||
      @username = @account.username
 | 
			
		||||
      @domain   = @account.domain
 | 
			
		||||
 | 
			
		||||
    @account = Account.find_remote(@username, @domain)
 | 
			
		||||
      return @account if @account.local? || !webfinger_update_due?
 | 
			
		||||
    else
 | 
			
		||||
      @username, @domain = uri.split('@')
 | 
			
		||||
 | 
			
		||||
    return @account unless webfinger_update_due?
 | 
			
		||||
      return Account.find_local(@username) if TagManager.instance.local_domain?(@domain)
 | 
			
		||||
 | 
			
		||||
      @account = Account.find_remote(@username, @domain)
 | 
			
		||||
 | 
			
		||||
      return @account unless webfinger_update_due?
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    Rails.logger.debug "Looking up webfinger for #{uri}"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -30,8 +40,8 @@ class ResolveAccountService < BaseService
 | 
			
		|||
    if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
 | 
			
		||||
      @username = confirmed_username
 | 
			
		||||
      @domain   = confirmed_domain
 | 
			
		||||
    elsif redirected.nil?
 | 
			
		||||
      return call("#{confirmed_username}@#{confirmed_domain}", update_profile, true)
 | 
			
		||||
    elsif options[:redirected].nil?
 | 
			
		||||
      return call("#{confirmed_username}@#{confirmed_domain}", options.merge(redirected: true))
 | 
			
		||||
    else
 | 
			
		||||
      Rails.logger.debug 'Requested and returned acct URIs do not match'
 | 
			
		||||
      return
 | 
			
		||||
| 
						 | 
				
			
			@ -76,7 +86,7 @@ class ResolveAccountService < BaseService
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def webfinger_update_due?
 | 
			
		||||
    @account.nil? || @account.possibly_stale?
 | 
			
		||||
    @account.nil? || ((!@options[:skip_webfinger] || @account.ostatus?) && @account.possibly_stale?)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def activitypub_ready?
 | 
			
		||||
| 
						 | 
				
			
			@ -93,7 +103,7 @@ class ResolveAccountService < BaseService
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def update_profile?
 | 
			
		||||
    @update_profile
 | 
			
		||||
    @options[:update_profile]
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_activitypub
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,9 +3,16 @@
 | 
			
		|||
class LocalNotificationWorker
 | 
			
		||||
  include Sidekiq::Worker
 | 
			
		||||
 | 
			
		||||
  def perform(mention_id)
 | 
			
		||||
    mention = Mention.find(mention_id)
 | 
			
		||||
    NotifyService.new.call(mention.account, mention)
 | 
			
		||||
  def perform(receiver_account_id, activity_id = nil, activity_class_name = nil)
 | 
			
		||||
    if activity_id.nil? && activity_class_name.nil?
 | 
			
		||||
      activity = Mention.find(receiver_account_id)
 | 
			
		||||
      receiver = activity.account
 | 
			
		||||
    else
 | 
			
		||||
      receiver = Account.find(receiver_account_id)
 | 
			
		||||
      activity = activity_class_name.constantize.find(activity_id)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    NotifyService.new.call(receiver, activity)
 | 
			
		||||
  rescue ActiveRecord::RecordNotFound
 | 
			
		||||
    true
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -99,10 +99,12 @@ describe AuthorizeInteractionsController do
 | 
			
		|||
 | 
			
		||||
        allow(ResolveAccountService).to receive(:new).and_return(service)
 | 
			
		||||
        allow(service).to receive(:call).with('user@hostname').and_return(target_account)
 | 
			
		||||
        allow(service).to receive(:call).with(target_account, skip_webfinger: true).and_return(target_account)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        post :create, params: { acct: 'acct:user@hostname' }
 | 
			
		||||
 | 
			
		||||
        expect(service).to have_received(:call).with('user@hostname')
 | 
			
		||||
        expect(service).to have_received(:call).with(target_account, skip_webfinger: true)
 | 
			
		||||
        expect(account.following?(target_account)).to be true
 | 
			
		||||
        expect(response).to render_template(:success)
 | 
			
		||||
      end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue