Change auto-following admin-selected accounts, show in recommendations (#16078)
parent
863ae47b51
commit
daccc07dc1
@ -1,17 +1,28 @@ |
|||||||
# frozen_string_literal: true |
# frozen_string_literal: true |
||||||
|
|
||||||
class AccountSuggestions |
class AccountSuggestions |
||||||
class Suggestion < ActiveModelSerializers::Model |
SOURCES = [ |
||||||
attributes :account, :source |
AccountSuggestions::SettingSource, |
||||||
end |
AccountSuggestions::PastInteractionsSource, |
||||||
|
AccountSuggestions::GlobalSource, |
||||||
|
].freeze |
||||||
|
|
||||||
def self.get(account, limit) |
def self.get(account, limit) |
||||||
suggestions = PotentialFriendshipTracker.get(account, limit).map { |target_account| Suggestion.new(account: target_account, source: :past_interaction) } |
SOURCES.each_with_object([]) do |source_class, suggestions| |
||||||
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 |
source_suggestions = source_class.new.get( |
||||||
suggestions |
account, |
||||||
|
skip_account_ids: suggestions.map(&:account_id), |
||||||
|
limit: limit - suggestions.size |
||||||
|
) |
||||||
|
|
||||||
|
suggestions.concat(source_suggestions) |
||||||
|
end |
||||||
end |
end |
||||||
|
|
||||||
def self.remove(account, target_account_id) |
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 |
||||||
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' |
require 'rails_helper' |
||||||
|
|
||||||
RSpec.describe BootstrapTimelineService, type: :service do |
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 |
end |
||||||
|
Loading…
Reference in new issue