interactions.rb 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. # frozen_string_literal: true
  2. module Account::Interactions
  3. extend ActiveSupport::Concern
  4. class_methods do
  5. def following_map(target_account_ids, account_id)
  6. Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping|
  7. mapping[follow.target_account_id] = {
  8. reblogs: follow.show_reblogs?,
  9. notify: follow.notify?,
  10. languages: follow.languages,
  11. }
  12. end
  13. end
  14. def followed_by_map(target_account_ids, account_id)
  15. follow_mapping(Follow.where(account_id: target_account_ids, target_account_id: account_id), :account_id)
  16. end
  17. def blocking_map(target_account_ids, account_id)
  18. follow_mapping(Block.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
  19. end
  20. def blocked_by_map(target_account_ids, account_id)
  21. follow_mapping(Block.where(account_id: target_account_ids, target_account_id: account_id), :account_id)
  22. end
  23. def muting_map(target_account_ids, account_id)
  24. Mute.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |mute, mapping|
  25. mapping[mute.target_account_id] = {
  26. notifications: mute.hide_notifications?,
  27. }
  28. end
  29. end
  30. def requested_map(target_account_ids, account_id)
  31. FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping|
  32. mapping[follow_request.target_account_id] = {
  33. reblogs: follow_request.show_reblogs?,
  34. notify: follow_request.notify?,
  35. languages: follow_request.languages,
  36. }
  37. end
  38. end
  39. def requested_by_map(target_account_ids, account_id)
  40. follow_mapping(FollowRequest.where(account_id: target_account_ids, target_account_id: account_id), :account_id)
  41. end
  42. def endorsed_map(target_account_ids, account_id)
  43. follow_mapping(AccountPin.where(account_id: account_id, target_account_id: target_account_ids), :target_account_id)
  44. end
  45. def account_note_map(target_account_ids, account_id)
  46. AccountNote.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |note, mapping|
  47. mapping[note.target_account_id] = {
  48. comment: note.comment,
  49. }
  50. end
  51. end
  52. def domain_blocking_map(target_account_ids, account_id)
  53. accounts_map = Account.where(id: target_account_ids).select('id, domain').each_with_object({}) { |a, h| h[a.id] = a.domain }
  54. blocked_domains = domain_blocking_map_by_domain(accounts_map.values.compact, account_id)
  55. accounts_map.reduce({}) { |h, (id, domain)| h.merge(id => blocked_domains[domain]) }
  56. end
  57. def domain_blocking_map_by_domain(target_domains, account_id)
  58. follow_mapping(AccountDomainBlock.where(account_id: account_id, domain: target_domains), :domain)
  59. end
  60. private
  61. def follow_mapping(query, field)
  62. query.pluck(field).index_with(true)
  63. end
  64. end
  65. included do
  66. # Follow relations
  67. has_many :follow_requests, dependent: :destroy
  68. with_options class_name: 'Follow', dependent: :destroy do
  69. has_many :active_relationships, foreign_key: 'account_id', inverse_of: :account
  70. has_many :passive_relationships, foreign_key: 'target_account_id', inverse_of: :target_account
  71. end
  72. has_many :following, -> { order('follows.id desc') }, through: :active_relationships, source: :target_account
  73. has_many :followers, -> { order('follows.id desc') }, through: :passive_relationships, source: :account
  74. # Account notes
  75. has_many :account_notes, dependent: :destroy
  76. # Block relationships
  77. with_options class_name: 'Block', dependent: :destroy do
  78. has_many :block_relationships, foreign_key: 'account_id', inverse_of: :account
  79. has_many :blocked_by_relationships, foreign_key: :target_account_id, inverse_of: :target_account
  80. end
  81. has_many :blocking, -> { order('blocks.id desc') }, through: :block_relationships, source: :target_account
  82. has_many :blocked_by, -> { order('blocks.id desc') }, through: :blocked_by_relationships, source: :account
  83. # Mute relationships
  84. with_options class_name: 'Mute', dependent: :destroy do
  85. has_many :mute_relationships, foreign_key: 'account_id', inverse_of: :account
  86. has_many :muted_by_relationships, foreign_key: :target_account_id, inverse_of: :target_account
  87. end
  88. has_many :muting, -> { order('mutes.id desc') }, through: :mute_relationships, source: :target_account
  89. has_many :muted_by, -> { order('mutes.id desc') }, through: :muted_by_relationships, source: :account
  90. has_many :conversation_mutes, dependent: :destroy
  91. has_many :domain_blocks, class_name: 'AccountDomainBlock', dependent: :destroy
  92. has_many :announcement_mutes, dependent: :destroy
  93. end
  94. def follow!(other_account, reblogs: nil, notify: nil, languages: nil, uri: nil, rate_limit: false, bypass_limit: false)
  95. rel = active_relationships.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, languages: languages, uri: uri, rate_limit: rate_limit, bypass_follow_limit: bypass_limit)
  96. .find_or_create_by!(target_account: other_account)
  97. rel.show_reblogs = reblogs unless reblogs.nil?
  98. rel.notify = notify unless notify.nil?
  99. rel.languages = languages unless languages.nil?
  100. rel.save! if rel.changed?
  101. remove_potential_friendship(other_account)
  102. rel
  103. end
  104. def request_follow!(other_account, reblogs: nil, notify: nil, languages: nil, uri: nil, rate_limit: false, bypass_limit: false)
  105. rel = follow_requests.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, languages: languages, rate_limit: rate_limit, bypass_follow_limit: bypass_limit)
  106. .find_or_create_by!(target_account: other_account)
  107. rel.show_reblogs = reblogs unless reblogs.nil?
  108. rel.notify = notify unless notify.nil?
  109. rel.languages = languages unless languages.nil?
  110. rel.save! if rel.changed?
  111. remove_potential_friendship(other_account)
  112. rel
  113. end
  114. def block!(other_account, uri: nil)
  115. remove_potential_friendship(other_account)
  116. block_relationships.create_with(uri: uri)
  117. .find_or_create_by!(target_account: other_account)
  118. end
  119. def mute!(other_account, notifications: nil, duration: 0)
  120. notifications = true if notifications.nil?
  121. mute = mute_relationships.create_with(hide_notifications: notifications).find_or_initialize_by(target_account: other_account)
  122. mute.expires_in = duration.zero? ? nil : duration
  123. mute.save!
  124. remove_potential_friendship(other_account)
  125. # 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.
  126. mute.update!(hide_notifications: notifications) if mute.hide_notifications? != notifications
  127. mute
  128. end
  129. def mute_conversation!(conversation)
  130. conversation_mutes.find_or_create_by!(conversation: conversation)
  131. end
  132. def block_domain!(other_domain)
  133. domain_blocks.find_or_create_by!(domain: other_domain)
  134. end
  135. def unfollow!(other_account)
  136. follow = active_relationships.find_by(target_account: other_account)
  137. follow&.destroy
  138. end
  139. def unblock!(other_account)
  140. block = block_relationships.find_by(target_account: other_account)
  141. block&.destroy
  142. end
  143. def unmute!(other_account)
  144. mute = mute_relationships.find_by(target_account: other_account)
  145. mute&.destroy
  146. end
  147. def unmute_conversation!(conversation)
  148. mute = conversation_mutes.find_by(conversation: conversation)
  149. mute&.destroy!
  150. end
  151. def unblock_domain!(other_domain)
  152. block = domain_blocks.find_by(domain: other_domain)
  153. block&.destroy
  154. end
  155. def following?(other_account)
  156. active_relationships.where(target_account: other_account).exists?
  157. end
  158. def following_anyone?
  159. active_relationships.exists?
  160. end
  161. def not_following_anyone?
  162. !following_anyone?
  163. end
  164. def followed_by?(other_account)
  165. other_account.following?(self)
  166. end
  167. def blocking?(other_account)
  168. block_relationships.where(target_account: other_account).exists?
  169. end
  170. def domain_blocking?(other_domain)
  171. domain_blocks.where(domain: other_domain).exists?
  172. end
  173. def muting?(other_account)
  174. mute_relationships.where(target_account: other_account).exists?
  175. end
  176. def muting_conversation?(conversation)
  177. conversation_mutes.where(conversation: conversation).exists?
  178. end
  179. def muting_notifications?(other_account)
  180. mute_relationships.where(target_account: other_account, hide_notifications: true).exists?
  181. end
  182. def muting_reblogs?(other_account)
  183. active_relationships.where(target_account: other_account, show_reblogs: false).exists?
  184. end
  185. def requested?(other_account)
  186. follow_requests.where(target_account: other_account).exists?
  187. end
  188. def favourited?(status)
  189. status.proper.favourites.where(account: self).exists?
  190. end
  191. def bookmarked?(status)
  192. status.proper.bookmarks.where(account: self).exists?
  193. end
  194. def reblogged?(status)
  195. status.proper.reblogs.where(account: self).exists?
  196. end
  197. def pinned?(status)
  198. status_pins.where(status: status).exists?
  199. end
  200. def endorsed?(account)
  201. account_pins.where(target_account: account).exists?
  202. end
  203. def status_matches_filters(status)
  204. active_filters = CustomFilter.cached_filters_for(id)
  205. CustomFilter.apply_cached_filters(active_filters, status)
  206. end
  207. def followers_for_local_distribution
  208. followers.local
  209. .joins(:user)
  210. .where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
  211. end
  212. def lists_for_local_distribution
  213. scope = lists.joins(account: :user)
  214. scope.where.not(list_accounts: { follow_id: nil }).or(scope.where(account_id: id))
  215. .where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago)
  216. end
  217. def remote_followers_hash(url)
  218. url_prefix = url[Account::URL_PREFIX_RE]
  219. return if url_prefix.blank?
  220. Rails.cache.fetch("followers_hash:#{id}:#{url_prefix}/") do
  221. digest = "\x00" * 32
  222. followers.where(Account.arel_table[:uri].matches("#{Account.sanitize_sql_like(url_prefix)}/%", false, true)).or(followers.where(uri: url_prefix)).pluck_each(:uri) do |uri|
  223. Xorcist.xor!(digest, Digest::SHA256.digest(uri))
  224. end
  225. digest.unpack1('H*')
  226. end
  227. end
  228. def local_followers_hash
  229. Rails.cache.fetch("followers_hash:#{id}:local") do
  230. digest = "\x00" * 32
  231. followers.where(domain: nil).pluck_each(:username) do |username|
  232. Xorcist.xor!(digest, Digest::SHA256.digest(ActivityPub::TagManager.instance.uri_for_username(username)))
  233. end
  234. digest.unpack1('H*')
  235. end
  236. end
  237. def relations_map(account_ids, domains = nil, **options)
  238. relations = {
  239. blocked_by: Account.blocked_by_map(account_ids, id),
  240. following: Account.following_map(account_ids, id),
  241. }
  242. return relations if options[:skip_blocking_and_muting]
  243. relations.merge!({
  244. blocking: Account.blocking_map(account_ids, id),
  245. muting: Account.muting_map(account_ids, id),
  246. domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, id),
  247. })
  248. end
  249. private
  250. def remove_potential_friendship(other_account)
  251. PotentialFriendshipTracker.remove(id, other_account.id)
  252. end
  253. end