Change auto-following admin-selected accounts, show in recommendations (#16078)
parent
863ae47b51
commit
daccc07dc1
@ -1,17 +1,28 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class AccountSuggestions |
||||
class Suggestion < ActiveModelSerializers::Model |
||||
attributes :account, :source |
||||
end |
||||
SOURCES = [ |
||||
AccountSuggestions::SettingSource, |
||||
AccountSuggestions::PastInteractionsSource, |
||||
AccountSuggestions::GlobalSource, |
||||
].freeze |
||||
|
||||
def self.get(account, limit) |
||||
suggestions = PotentialFriendshipTracker.get(account, limit).map { |target_account| Suggestion.new(account: target_account, source: :past_interaction) } |
||||
suggestions.concat(FollowRecommendation.get(account, limit - suggestions.size, suggestions.map { |suggestion| suggestion.account.id }).map { |target_account| Suggestion.new(account: target_account, source: :global) }) if suggestions.size < limit |
||||
suggestions |
||||
SOURCES.each_with_object([]) do |source_class, suggestions| |
||||
source_suggestions = source_class.new.get( |
||||
account, |
||||
skip_account_ids: suggestions.map(&:account_id), |
||||
limit: limit - suggestions.size |
||||
) |
||||
|
||||
suggestions.concat(source_suggestions) |
||||
end |
||||
end |
||||
|
||||
def self.remove(account, target_account_id) |
||||
PotentialFriendshipTracker.remove(account.id, target_account_id) |
||||
SOURCES.each do |source_class| |
||||
source = source_class.new |
||||
source.remove(account, target_account_id) |
||||
end |
||||
end |
||||
end |
||||
|
@ -0,0 +1,37 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class AccountSuggestions::GlobalSource < AccountSuggestions::Source |
||||
def key |
||||
:global |
||||
end |
||||
|
||||
def get(account, skip_account_ids: [], limit: 40) |
||||
account_ids = account_ids_for_locale(account.user_locale) - [account.id] - skip_account_ids |
||||
|
||||
as_ordered_suggestions( |
||||
scope(account).where(id: account_ids), |
||||
account_ids |
||||
).take(limit) |
||||
end |
||||
|
||||
def remove(_account, _target_account_id) |
||||
nil |
||||
end |
||||
|
||||
private |
||||
|
||||
def scope(account) |
||||
Account.searchable |
||||
.followable_by(account) |
||||
.not_excluded_by_account(account) |
||||
.not_domain_blocked_by_account(account) |
||||
end |
||||
|
||||
def account_ids_for_locale(locale) |
||||
Redis.current.zrevrange("follow_recommendations:#{locale}", 0, -1).map(&:to_i) |
||||
end |
||||
|
||||
def to_ordered_list_key(account) |
||||
account.id |
||||
end |
||||
end |
@ -0,0 +1,36 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class AccountSuggestions::PastInteractionsSource < AccountSuggestions::Source |
||||
include Redisable |
||||
|
||||
def key |
||||
:past_interactions |
||||
end |
||||
|
||||
def get(account, skip_account_ids: [], limit: 40) |
||||
account_ids = account_ids_for_account(account.id, limit + skip_account_ids.size) - skip_account_ids |
||||
|
||||
as_ordered_suggestions( |
||||
scope.where(id: account_ids), |
||||
account_ids |
||||
).take(limit) |
||||
end |
||||
|
||||
def remove(account, target_account_id) |
||||
redis.zrem("interactions:#{account.id}", target_account_id) |
||||
end |
||||
|
||||
private |
||||
|
||||
def scope |
||||
Account.searchable |
||||
end |
||||
|
||||
def account_ids_for_account(account_id, limit) |
||||
redis.zrevrange("interactions:#{account_id}", 0, limit).map(&:to_i) |
||||
end |
||||
|
||||
def to_ordered_list_key(account) |
||||
account.id |
||||
end |
||||
end |
@ -0,0 +1,68 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class AccountSuggestions::SettingSource < AccountSuggestions::Source |
||||
def key |
||||
:staff |
||||
end |
||||
|
||||
def get(account, skip_account_ids: [], limit: 40) |
||||
return [] unless setting_enabled? |
||||
|
||||
as_ordered_suggestions( |
||||
scope(account).where(setting_to_where_condition).where.not(id: skip_account_ids), |
||||
usernames_and_domains |
||||
).take(limit) |
||||
end |
||||
|
||||
def remove(_account, _target_account_id) |
||||
nil |
||||
end |
||||
|
||||
private |
||||
|
||||
def scope(account) |
||||
Account.searchable |
||||
.followable_by(account) |
||||
.not_excluded_by_account(account) |
||||
.not_domain_blocked_by_account(account) |
||||
.where(locked: false) |
||||
.where.not(id: account.id) |
||||
end |
||||
|
||||
def usernames_and_domains |
||||
@usernames_and_domains ||= setting_to_usernames_and_domains |
||||
end |
||||
|
||||
def setting_enabled? |
||||
setting.present? |
||||
end |
||||
|
||||
def setting_to_where_condition |
||||
usernames_and_domains.map do |(username, domain)| |
||||
Arel::Nodes::Grouping.new( |
||||
Account.arel_table[:username].lower.eq(username.downcase).and( |
||||
Account.arel_table[:domain].lower.eq(domain&.downcase) |
||||
) |
||||
) |
||||
end.reduce(:or) |
||||
end |
||||
|
||||
def setting_to_usernames_and_domains |
||||
setting.split(',').map do |str| |
||||
username, domain = str.strip.gsub(/\A@/, '').split('@', 2) |
||||
domain = nil if TagManager.instance.local_domain?(domain) |
||||
|
||||
next if username.blank? |
||||
|
||||
[username, domain] |
||||
end.compact |
||||
end |
||||
|
||||
def setting |
||||
Setting.bootstrap_timeline_accounts |
||||
end |
||||
|
||||
def to_ordered_list_key(account) |
||||
[account.username, account.domain] |
||||
end |
||||
end |
@ -0,0 +1,34 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class AccountSuggestions::Source |
||||
def key |
||||
raise NotImplementedError |
||||
end |
||||
|
||||
def get(_account, **kwargs) |
||||
raise NotImplementedError |
||||
end |
||||
|
||||
def remove(_account, target_account_id) |
||||
raise NotImplementedError |
||||
end |
||||
|
||||
protected |
||||
|
||||
def as_ordered_suggestions(scope, ordered_list) |
||||
return [] if ordered_list.empty? |
||||
|
||||
map = scope.index_by(&method(:to_ordered_list_key)) |
||||
|
||||
ordered_list.map { |ordered_list_key| map[ordered_list_key] }.compact.map do |account| |
||||
AccountSuggestions::Suggestion.new( |
||||
account: account, |
||||
source: key |
||||
) |
||||
end |
||||
end |
||||
|
||||
def to_ordered_list_key(_account) |
||||
raise NotImplementedError |
||||
end |
||||
end |
@ -0,0 +1,7 @@ |
||||
# frozen_string_literal: true |
||||
|
||||
class AccountSuggestions::Suggestion < ActiveModelSerializers::Model |
||||
attributes :account, :source |
||||
|
||||
delegate :id, to: :account, prefix: true |
||||
end |
@ -1,42 +1,4 @@ |
||||
require 'rails_helper' |
||||
|
||||
RSpec.describe BootstrapTimelineService, type: :service 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') } |
||||
let!(:eve) { Fabricate(:account, username: 'eve', suspended: true) } |
||||
|
||||
before do |
||||
Setting.bootstrap_timeline_accounts = 'alice, @bob, eve, unknown' |
||||
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 |
||||
|
||||
it 'does not follow suspended account' do |
||||
expect(source_account.following?(eve)).to be false |
||||
end |
||||
end |
||||
end |
||||
end |
||||
|
Loading…
Reference in new issue