Change auto-following admin-selected accounts, show in recommendations (#16078)
parent
35bd7f4548
commit
f5d59b3979
@ -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