da8fe8079e
* Re-add follow recommendations API
GET /api/v1/suggestions
Removed in 8efa081f21
due to Neo4J
dependency. The algorithm uses triadic closures, takes into account
suspensions, blocks, mutes, domain blocks, excludes locked and moved
accounts, and prefers more recently updated accounts.
* Track interactions with people you don't follow
Replying to, favouriting and reblogging someone you're not following
will make them show up in follow recommendations. The interactions
have different weights:
- Replying is 1
- Favouriting is 10 (decidedly positive interaction, but private)
- Reblogging is 20
Following them, muting or blocking will remove them from the list,
obviously.
* Remove triadic closures, ensure potential friendships are trimmed
210 lines
7.4 KiB
Ruby
210 lines
7.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module AccountInteractions
|
|
extend ActiveSupport::Concern
|
|
|
|
class_methods do
|
|
def following_map(target_account_ids, account_id)
|
|
Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping|
|
|
mapping[follow.target_account_id] = {
|
|
reblogs: follow.show_reblogs?,
|
|
}
|
|
end
|
|
end
|
|
|
|
def followed_by_map(target_account_ids, account_id)
|
|
follow_mapping(Follow.where(account_id: target_account_ids, target_account_id: account_id), :account_id)
|
|
end
|
|
|
|
def blocking_map(target_account_ids, account_id)
|
|
follow_mapping(Block.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
|
|
end
|
|
|
|
def blocked_by_map(target_account_ids, account_id)
|
|
follow_mapping(Block.where(account_id: target_account_ids, target_account_id: account_id), :account_id)
|
|
end
|
|
|
|
def muting_map(target_account_ids, account_id)
|
|
Mute.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |mute, mapping|
|
|
mapping[mute.target_account_id] = {
|
|
notifications: mute.hide_notifications?,
|
|
}
|
|
end
|
|
end
|
|
|
|
def requested_map(target_account_ids, account_id)
|
|
FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping|
|
|
mapping[follow_request.target_account_id] = {
|
|
reblogs: follow_request.show_reblogs?,
|
|
}
|
|
end
|
|
end
|
|
|
|
def domain_blocking_map(target_account_ids, account_id)
|
|
accounts_map = Account.where(id: target_account_ids).select('id, domain').map { |a| [a.id, a.domain] }.to_h
|
|
blocked_domains = domain_blocking_map_by_domain(accounts_map.values.compact, account_id)
|
|
accounts_map.map { |id, domain| [id, blocked_domains[domain]] }.to_h
|
|
end
|
|
|
|
def domain_blocking_map_by_domain(target_domains, account_id)
|
|
follow_mapping(AccountDomainBlock.where(account_id: account_id, domain: target_domains), :domain)
|
|
end
|
|
|
|
private
|
|
|
|
def follow_mapping(query, field)
|
|
query.pluck(field).each_with_object({}) { |id, mapping| mapping[id] = true }
|
|
end
|
|
end
|
|
|
|
included do
|
|
# Follow relations
|
|
has_many :follow_requests, dependent: :destroy
|
|
|
|
has_many :active_relationships, class_name: 'Follow', foreign_key: 'account_id', dependent: :destroy
|
|
has_many :passive_relationships, class_name: 'Follow', foreign_key: 'target_account_id', dependent: :destroy
|
|
|
|
has_many :following, -> { order('follows.id desc') }, through: :active_relationships, source: :target_account
|
|
has_many :followers, -> { order('follows.id desc') }, through: :passive_relationships, source: :account
|
|
|
|
# Block relationships
|
|
has_many :block_relationships, class_name: 'Block', foreign_key: 'account_id', dependent: :destroy
|
|
has_many :blocking, -> { order('blocks.id desc') }, through: :block_relationships, source: :target_account
|
|
has_many :blocked_by_relationships, class_name: 'Block', foreign_key: :target_account_id, dependent: :destroy
|
|
has_many :blocked_by, -> { order('blocks.id desc') }, through: :blocked_by_relationships, source: :account
|
|
|
|
# Mute relationships
|
|
has_many :mute_relationships, class_name: 'Mute', foreign_key: 'account_id', dependent: :destroy
|
|
has_many :muting, -> { order('mutes.id desc') }, through: :mute_relationships, source: :target_account
|
|
has_many :muted_by_relationships, class_name: 'Mute', foreign_key: :target_account_id, dependent: :destroy
|
|
has_many :muted_by, -> { order('mutes.id desc') }, through: :muted_by_relationships, source: :account
|
|
has_many :conversation_mutes, dependent: :destroy
|
|
has_many :domain_blocks, class_name: 'AccountDomainBlock', dependent: :destroy
|
|
end
|
|
|
|
def follow!(other_account, reblogs: nil, uri: nil)
|
|
reblogs = true if reblogs.nil?
|
|
|
|
rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri)
|
|
.find_or_create_by!(target_account: other_account)
|
|
|
|
rel.update!(show_reblogs: reblogs)
|
|
remove_potential_friendship(other_account)
|
|
|
|
rel
|
|
end
|
|
|
|
def block!(other_account, uri: nil)
|
|
remove_potential_friendship(other_account)
|
|
block_relationships.create_with(uri: uri)
|
|
.find_or_create_by!(target_account: other_account)
|
|
end
|
|
|
|
def mute!(other_account, notifications: nil)
|
|
notifications = true if notifications.nil?
|
|
mute = mute_relationships.create_with(hide_notifications: notifications).find_or_create_by!(target_account: other_account)
|
|
remove_potential_friendship(other_account)
|
|
|
|
# When toggling a mute between hiding and allowing notifications, the mute will already exist, so the find_or_create_by! call will return the existing Mute without updating the hide_notifications attribute. Therefore, we check that hide_notifications? is what we want and set it if it isn't.
|
|
if mute.hide_notifications? != notifications
|
|
mute.update!(hide_notifications: notifications)
|
|
end
|
|
|
|
mute
|
|
end
|
|
|
|
def mute_conversation!(conversation)
|
|
conversation_mutes.find_or_create_by!(conversation: conversation)
|
|
end
|
|
|
|
def block_domain!(other_domain)
|
|
domain_blocks.find_or_create_by!(domain: other_domain)
|
|
end
|
|
|
|
def unfollow!(other_account)
|
|
follow = active_relationships.find_by(target_account: other_account)
|
|
follow&.destroy
|
|
end
|
|
|
|
def unblock!(other_account)
|
|
block = block_relationships.find_by(target_account: other_account)
|
|
block&.destroy
|
|
end
|
|
|
|
def unmute!(other_account)
|
|
mute = mute_relationships.find_by(target_account: other_account)
|
|
mute&.destroy
|
|
end
|
|
|
|
def unmute_conversation!(conversation)
|
|
mute = conversation_mutes.find_by(conversation: conversation)
|
|
mute&.destroy!
|
|
end
|
|
|
|
def unblock_domain!(other_domain)
|
|
block = domain_blocks.find_by(domain: other_domain)
|
|
block&.destroy
|
|
end
|
|
|
|
def following?(other_account)
|
|
active_relationships.where(target_account: other_account).exists?
|
|
end
|
|
|
|
def blocking?(other_account)
|
|
block_relationships.where(target_account: other_account).exists?
|
|
end
|
|
|
|
def domain_blocking?(other_domain)
|
|
domain_blocks.where(domain: other_domain).exists?
|
|
end
|
|
|
|
def muting?(other_account)
|
|
mute_relationships.where(target_account: other_account).exists?
|
|
end
|
|
|
|
def muting_conversation?(conversation)
|
|
conversation_mutes.where(conversation: conversation).exists?
|
|
end
|
|
|
|
def muting_notifications?(other_account)
|
|
mute_relationships.where(target_account: other_account, hide_notifications: true).exists?
|
|
end
|
|
|
|
def muting_reblogs?(other_account)
|
|
active_relationships.where(target_account: other_account, show_reblogs: false).exists?
|
|
end
|
|
|
|
def requested?(other_account)
|
|
follow_requests.where(target_account: other_account).exists?
|
|
end
|
|
|
|
def favourited?(status)
|
|
status.proper.favourites.where(account: self).exists?
|
|
end
|
|
|
|
def reblogged?(status)
|
|
status.proper.reblogs.where(account: self).exists?
|
|
end
|
|
|
|
def pinned?(status)
|
|
status_pins.where(status: status).exists?
|
|
end
|
|
|
|
def followers_for_local_distribution
|
|
followers.local
|
|
.joins(:user)
|
|
.where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
|
|
end
|
|
|
|
def lists_for_local_distribution
|
|
lists.joins(account: :user)
|
|
.where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
|
|
end
|
|
|
|
private
|
|
|
|
def remove_potential_friendship(other_account)
|
|
PotentialFriendshipTracker.remove(id, other_account.id)
|
|
end
|
|
end
|