forked from cybrespace/mastodon
		
	Default follows for new users (#4871)
When a new user confirms their e-mail, bootstrap their home timeline by automatically following a set of accounts. By default, all local admin accounts (that are unlocked). Can be customized by new admin setting (comma-separated usernames, local and unlocked only)
This commit is contained in:
		
							parent
							
								
									f2cbfb2eb3
								
							
						
					
					
						commit
						7d7844a47f
					
				
					 11 changed files with 118 additions and 2 deletions
				
			
		|  | @ -13,6 +13,7 @@ module Admin | ||||||
|       closed_registrations_message |       closed_registrations_message | ||||||
|       open_deletion |       open_deletion | ||||||
|       timeline_preview |       timeline_preview | ||||||
|  |       bootstrap_timeline_accounts | ||||||
|     ).freeze |     ).freeze | ||||||
| 
 | 
 | ||||||
|     BOOLEAN_SETTINGS = %w( |     BOOLEAN_SETTINGS = %w( | ||||||
|  |  | ||||||
|  | @ -2,4 +2,10 @@ | ||||||
| 
 | 
 | ||||||
| class Auth::ConfirmationsController < Devise::ConfirmationsController | class Auth::ConfirmationsController < Devise::ConfirmationsController | ||||||
|   layout 'auth' |   layout 'auth' | ||||||
|  | 
 | ||||||
|  |   def show | ||||||
|  |     super do |user| | ||||||
|  |       BootstrapTimelineWorker.perform_async(user.account_id) if user.errors.empty? | ||||||
|  |     end | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -24,6 +24,8 @@ class Form::AdminSettings | ||||||
|     :open_deletion=, |     :open_deletion=, | ||||||
|     :timeline_preview, |     :timeline_preview, | ||||||
|     :timeline_preview=, |     :timeline_preview=, | ||||||
|  |     :bootstrap_timeline_accounts, | ||||||
|  |     :bootstrap_timeline_accounts=, | ||||||
|     to: Setting |     to: Setting | ||||||
|   ) |   ) | ||||||
| end | end | ||||||
|  |  | ||||||
							
								
								
									
										34
									
								
								app/services/bootstrap_timeline_service.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								app/services/bootstrap_timeline_service.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,34 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class BootstrapTimelineService < BaseService | ||||||
|  |   def call(source_account) | ||||||
|  |     bootstrap_timeline_accounts.each do |target_account| | ||||||
|  |       FollowService.new.call(source_account, target_account) | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   private | ||||||
|  | 
 | ||||||
|  |   def bootstrap_timeline_accounts | ||||||
|  |     return @bootstrap_timeline_accounts if defined?(@bootstrap_timeline_accounts) | ||||||
|  | 
 | ||||||
|  |     @bootstrap_timeline_accounts = bootstrap_timeline_accounts_usernames.empty? ? admin_accounts : local_unlocked_accounts(bootstrap_timeline_accounts_usernames) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def bootstrap_timeline_accounts_usernames | ||||||
|  |     @bootstrap_timeline_accounts_usernames ||= (Setting.bootstrap_timeline_accounts || '').split(',').map { |str| str.strip.gsub(/\A@/, '') }.reject(&:blank?) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def admin_accounts | ||||||
|  |     User.admins | ||||||
|  |         .includes(:account) | ||||||
|  |         .where(accounts: { locked: false }) | ||||||
|  |         .map(&:account) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def local_unlocked_accounts(usernames) | ||||||
|  |     Account.local | ||||||
|  |            .where(username: usernames) | ||||||
|  |            .where(locked: false) | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -5,9 +5,9 @@ class FollowService < BaseService | ||||||
| 
 | 
 | ||||||
|   # Follow a remote user, notify remote user about the follow |   # Follow a remote user, notify remote user about the follow | ||||||
|   # @param [Account] source_account From which to follow |   # @param [Account] source_account From which to follow | ||||||
|   # @param [String] uri User URI to follow in the form of username@domain |   # @param [String, Account] uri User URI to follow in the form of username@domain (or account record) | ||||||
|   def call(source_account, uri) |   def call(source_account, uri) | ||||||
|     target_account = ResolveRemoteAccountService.new.call(uri) |     target_account = uri.is_a?(Account) ? uri : ResolveRemoteAccountService.new.call(uri) | ||||||
| 
 | 
 | ||||||
|     raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended? |     raise 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) |     raise Mastodon::NotPermittedError  if target_account.blocking?(source_account) || source_account.blocking?(target_account) | ||||||
|  |  | ||||||
|  | @ -28,5 +28,10 @@ | ||||||
|     = f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } |     = f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } | ||||||
|     = f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 } |     = f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 } | ||||||
| 
 | 
 | ||||||
|  |   %hr/ | ||||||
|  | 
 | ||||||
|  |   .fields-group | ||||||
|  |     = f.input :bootstrap_timeline_accounts, wrapper: :with_block_label, label: t('admin.settings.bootstrap_timeline_accounts.title'), hint: t('admin.settings.bootstrap_timeline_accounts.desc_html') | ||||||
|  | 
 | ||||||
|   .actions |   .actions | ||||||
|     = f.button :button, t('generic.save_changes'), type: :submit |     = f.button :button, t('generic.save_changes'), type: :submit | ||||||
|  |  | ||||||
							
								
								
									
										9
									
								
								app/workers/bootstrap_timeline_worker.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/workers/bootstrap_timeline_worker.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class BootstrapTimelineWorker | ||||||
|  |   include Sidekiq::Worker | ||||||
|  | 
 | ||||||
|  |   def perform(account_id) | ||||||
|  |     BootstrapTimelineService.new.call(Account.find(account_id)) | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -167,6 +167,9 @@ en: | ||||||
|       unresolved: Unresolved |       unresolved: Unresolved | ||||||
|       view: View |       view: View | ||||||
|     settings: |     settings: | ||||||
|  |       bootstrap_timeline_accounts: | ||||||
|  |         desc_html: Separate multiple usernames by comma. Only local and unlocked accounts will work. Default when empty is all local admins. | ||||||
|  |         title: Default follows for new users | ||||||
|       contact_information: |       contact_information: | ||||||
|         email: Business e-mail |         email: Business e-mail | ||||||
|         username: Contact username |         username: Contact username | ||||||
|  |  | ||||||
|  | @ -41,6 +41,7 @@ defaults: &defaults | ||||||
|     - root |     - root | ||||||
|     - webmaster |     - webmaster | ||||||
|     - administrator |     - administrator | ||||||
|  |   bootstrap_timeline_accounts: '' | ||||||
| 
 | 
 | ||||||
| development: | development: | ||||||
|   <<: *defaults |   <<: *defaults | ||||||
|  |  | ||||||
|  | @ -10,4 +10,22 @@ describe Auth::ConfirmationsController, type: :controller do | ||||||
|       expect(response).to have_http_status(:success) |       expect(response).to have_http_status(:success) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   describe 'GET #show' do | ||||||
|  |     let!(:user) { Fabricate(:user, confirmation_token: 'foobar', confirmed_at: nil) } | ||||||
|  | 
 | ||||||
|  |     before do | ||||||
|  |       allow(BootstrapTimelineWorker).to receive(:perform_async) | ||||||
|  |       @request.env['devise.mapping'] = Devise.mappings[:user] | ||||||
|  |       get :show, params: { confirmation_token: 'foobar' } | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'redirects to login' do | ||||||
|  |       expect(response).to redirect_to(new_user_session_path) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'queues up bootstrapping of home timeline' do | ||||||
|  |       expect(BootstrapTimelineWorker).to have_received(:perform_async).with(user.account_id) | ||||||
|  |     end | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
							
								
								
									
										37
									
								
								spec/services/bootstrap_timeline_service_spec.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								spec/services/bootstrap_timeline_service_spec.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | ||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe BootstrapTimelineService do | ||||||
|  |   subject { described_class.new } | ||||||
|  | 
 | ||||||
|  |   describe '#call' do | ||||||
|  |     let(:source_account) { Fabricate(:account) } | ||||||
|  | 
 | ||||||
|  |     context 'when setting is empty' do | ||||||
|  |       let!(:admin) { Fabricate(:user, admin: true) } | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         Setting.bootstrap_timeline_accounts = nil | ||||||
|  |         subject.call(source_account) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'follows admin accounts from account' do | ||||||
|  |         expect(source_account.following?(admin.account)).to be true | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when setting is set' do | ||||||
|  |       let!(:alice) { Fabricate(:account, username: 'alice') } | ||||||
|  |       let!(:bob)   { Fabricate(:account, username: 'bob') } | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         Setting.bootstrap_timeline_accounts = 'alice, bob' | ||||||
|  |         subject.call(source_account) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'follows found accounts from account' do | ||||||
|  |         expect(source_account.following?(alice)).to be true | ||||||
|  |         expect(source_account.following?(bob)).to be true | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue