You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

306 lines
11 KiB

7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
Account domain blocks (#2381) * Add <ostatus:conversation /> tag to Atom input/output Only uses ref attribute (not href) because href would be the alternate link that's always included also. Creates new conversation for every non-reply status. Carries over conversation for every reply. Keeps remote URIs verbatim, generates local URIs on the fly like the rest of them. * Conversation muting - prevents notifications that reference a conversation (including replies, favourites, reblogs) from being created. API endpoints /api/v1/statuses/:id/mute and /api/v1/statuses/:id/unmute Currently no way to tell when a status/conversation is muted, so the web UI only has a "disable notifications" button, doesn't work as a toggle * Display "Dismiss notifications" on all statuses in notifications column, not just own * Add "muted" as a boolean attribute on statuses JSON For now always false on contained reblogs, since it's only relevant for statuses returned from the notifications endpoint, which are not nested Remove "Disable notifications" from detailed status view, since it's only relevant in the notifications column * Up max class length * Remove pending test for conversation mute * Add tests, clean up * Rename to "mute conversation" and "unmute conversation" * Raise validation error when trying to mute/unmute status without conversation * Adding account domain blocks that filter notifications and public timelines * Add tests for domain blocks in notifications, public timelines Filter reblogs of blocked domains from home * Add API for listing and creating account domain blocks * API for creating/deleting domain blocks, tests for Status#ancestors and Status#descendants, filter domain blocks from them * Filter domains in streaming API * Update account_domain_block_spec.rb
7 years ago
Account domain blocks (#2381) * Add <ostatus:conversation /> tag to Atom input/output Only uses ref attribute (not href) because href would be the alternate link that's always included also. Creates new conversation for every non-reply status. Carries over conversation for every reply. Keeps remote URIs verbatim, generates local URIs on the fly like the rest of them. * Conversation muting - prevents notifications that reference a conversation (including replies, favourites, reblogs) from being created. API endpoints /api/v1/statuses/:id/mute and /api/v1/statuses/:id/unmute Currently no way to tell when a status/conversation is muted, so the web UI only has a "disable notifications" button, doesn't work as a toggle * Display "Dismiss notifications" on all statuses in notifications column, not just own * Add "muted" as a boolean attribute on statuses JSON For now always false on contained reblogs, since it's only relevant for statuses returned from the notifications endpoint, which are not nested Remove "Disable notifications" from detailed status view, since it's only relevant in the notifications column * Up max class length * Remove pending test for conversation mute * Add tests, clean up * Rename to "mute conversation" and "unmute conversation" * Raise validation error when trying to mute/unmute status without conversation * Adding account domain blocks that filter notifications and public timelines * Add tests for domain blocks in notifications, public timelines Filter reblogs of blocked domains from home * Add API for listing and creating account domain blocks * API for creating/deleting domain blocks, tests for Status#ancestors and Status#descendants, filter domain blocks from them * Filter domains in streaming API * Update account_domain_block_spec.rb
7 years ago
8 years ago
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: statuses
  5. #
  6. # id :integer not null, primary key
  7. # uri :string
  8. # account_id :integer not null
  9. # text :text default(""), not null
  10. # created_at :datetime not null
  11. # updated_at :datetime not null
  12. # in_reply_to_id :integer
  13. # reblog_of_id :integer
  14. # url :string
  15. # sensitive :boolean default(FALSE)
  16. # visibility :integer default("public"), not null
  17. # in_reply_to_account_id :integer
  18. # application_id :integer
  19. # spoiler_text :text default(""), not null
  20. # reply :boolean default(FALSE)
  21. # favourites_count :integer default(0), not null
  22. # reblogs_count :integer default(0), not null
  23. # language :string default("en"), not null
  24. # conversation_id :integer
  25. #
  26. class Status < ApplicationRecord
  27. include Paginable
  28. include Streamable
  29. include Cacheable
  30. enum visibility: [:public, :unlisted, :private, :direct], _suffix: :visibility
  31. belongs_to :application, class_name: 'Doorkeeper::Application'
  32. belongs_to :account, inverse_of: :statuses, counter_cache: true, required: true
  33. belongs_to :in_reply_to_account, foreign_key: 'in_reply_to_account_id', class_name: 'Account'
  34. belongs_to :conversation
  35. belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies
  36. belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, counter_cache: :reblogs_count
  37. has_many :favourites, inverse_of: :status, dependent: :destroy
  38. has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy
  39. has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :thread
  40. has_many :mentions, dependent: :destroy
  41. has_many :media_attachments, dependent: :destroy
  42. has_and_belongs_to_many :tags
  43. has_one :notification, as: :activity, dependent: :destroy
  44. has_one :preview_card, dependent: :destroy
  45. validates :uri, uniqueness: true, unless: :local?
  46. validates :text, presence: true, unless: :reblog?
  47. validates_with StatusLengthValidator
  48. validates :reblog, uniqueness: { scope: :account }, if: :reblog?
  49. default_scope { order(id: :desc) }
  50. scope :remote, -> { where.not(uri: nil) }
  51. scope :local, -> { where(uri: nil) }
  52. scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') }
  53. scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') }
  54. scope :with_public_visibility, -> { where(visibility: :public) }
  55. scope :tagged_with, ->(tag) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag }) }
  56. scope :local_only, -> { left_outer_joins(:account).where(accounts: { domain: nil }) }
  57. scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced: false }) }
  58. scope :including_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced: true }) }
  59. scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
  60. scope :not_domain_blocked_by_account, ->(account) { left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) }
  61. cache_associated :account, :application, :media_attachments, :tags, :stream_entry, mentions: :account, reblog: [:account, :application, :stream_entry, :tags, :media_attachments, mentions: :account], thread: :account
  62. def reply?
  63. !in_reply_to_id.nil? || attributes['reply']
  64. end
  65. def local?
  66. uri.nil?
  67. end
  68. def reblog?
  69. !reblog_of_id.nil?
  70. end
  71. def verb
  72. reblog? ? :share : :post
  73. end
  74. def object_type
  75. reply? ? :comment : :note
  76. end
  77. def proper
  78. reblog? ? reblog : self
  79. end
  80. def content
  81. proper.text
  82. end
  83. def target
  84. reblog
  85. end
  86. def title
  87. reblog? ? "#{account.acct} shared a status by #{reblog.account.acct}" : "New status by #{account.acct}"
  88. end
  89. def hidden?
  90. private_visibility? || direct_visibility?
  91. end
  92. def permitted?(other_account = nil)
  93. if direct_visibility?
  94. account.id == other_account&.id || mentions.where(account: other_account).exists?
  95. elsif private_visibility?
  96. account.id == other_account&.id || other_account&.following?(account) || mentions.where(account: other_account).exists?
  97. else
  98. other_account.nil? || !account.blocking?(other_account)
  99. end
  100. end
  101. def ancestors(account = nil)
  102. ids = Rails.cache.fetch("ancestors:#{id}") { (Status.find_by_sql(['WITH RECURSIVE search_tree(id, in_reply_to_id, path) AS (SELECT id, in_reply_to_id, ARRAY[id] FROM statuses WHERE id = ? UNION ALL SELECT statuses.id, statuses.in_reply_to_id, path || statuses.id FROM search_tree JOIN statuses ON statuses.id = search_tree.in_reply_to_id WHERE NOT statuses.id = ANY(path)) SELECT id FROM search_tree ORDER BY path DESC', id]) - [self]).pluck(:id) }
  103. find_statuses_from_tree_path(ids, account)
  104. end
  105. def descendants(account = nil)
  106. ids = (Status.find_by_sql(['WITH RECURSIVE search_tree(id, path) AS (SELECT id, ARRAY[id] FROM statuses WHERE id = ? UNION ALL SELECT statuses.id, path || statuses.id FROM search_tree JOIN statuses ON statuses.in_reply_to_id = search_tree.id WHERE NOT statuses.id = ANY(path)) SELECT id FROM search_tree ORDER BY path', id]) - [self]).pluck(:id)
  107. find_statuses_from_tree_path(ids, account)
  108. end
  109. def non_sensitive_with_media?
  110. !sensitive? && media_attachments.any?
  111. end
  112. before_validation :prepare_contents
  113. before_validation :set_reblog
  114. before_validation :set_visibility
  115. before_validation :set_conversation
  116. class << self
  117. def in_allowed_languages(account)
  118. where(language: account.allowed_languages)
  119. end
  120. def as_home_timeline(account)
  121. where(account: [account] + account.following)
  122. end
  123. def as_public_timeline(account = nil, local_only = false)
  124. query = timeline_scope(local_only).without_replies
  125. apply_timeline_filters(query, account, local_only)
  126. end
  127. def as_tag_timeline(tag, account = nil, local_only = false)
  128. query = timeline_scope(local_only).tagged_with(tag)
  129. apply_timeline_filters(query, account, local_only)
  130. end
  131. def as_outbox_timeline(account)
  132. where(account: account, visibility: :public)
  133. end
  134. def favourites_map(status_ids, account_id)
  135. Favourite.select('status_id').where(status_id: status_ids).where(account_id: account_id).map { |f| [f.status_id, true] }.to_h
  136. end
  137. def reblogs_map(status_ids, account_id)
  138. select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).map { |s| [s.reblog_of_id, true] }.to_h
  139. end
  140. def mutes_map(conversation_ids, account_id)
  141. ConversationMute.select('conversation_id').where(conversation_id: conversation_ids).where(account_id: account_id).map { |m| [m.conversation_id, true] }.to_h
  142. end
  143. def reload_stale_associations!(cached_items)
  144. account_ids = []
  145. cached_items.each do |item|
  146. account_ids << item.account_id
  147. account_ids << item.reblog.account_id if item.reblog?
  148. end
  149. accounts = Account.where(id: account_ids.uniq).map { |a| [a.id, a] }.to_h
  150. cached_items.each do |item|
  151. item.account = accounts[item.account_id]
  152. item.reblog.account = accounts[item.reblog.account_id] if item.reblog?
  153. end
  154. end
  155. def permitted_for(target_account, account)
  156. visibility = [:public, :unlisted]
  157. if account.nil?
  158. where(visibility: visibility)
  159. elsif target_account.blocking?(account) # get rid of blocked peeps
  160. none
  161. elsif account.id == target_account.id # author can see own stuff
  162. all
  163. else
  164. # followers can see followers-only stuff, but also things they are mentioned in.
  165. # non-followers can see everything that isn't private/direct, but can see stuff they are mentioned in.
  166. visibility.push(:private) if account.following?(target_account)
  167. joins("LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = #{account.id}")
  168. .where(arel_table[:visibility].in(visibility).or(Mention.arel_table[:id].not_eq(nil)))
  169. .order(visibility: :desc)
  170. end
  171. end
  172. private
  173. def timeline_scope(local_only = false)
  174. starting_scope = local_only ? Status.local_only : Status
  175. starting_scope
  176. .with_public_visibility
  177. .without_reblogs
  178. end
  179. def apply_timeline_filters(query, account, local_only)
  180. if account.nil?
  181. filter_timeline_default(query)
  182. else
  183. filter_timeline_for_account(query, account, local_only)
  184. end
  185. end
  186. def filter_timeline_for_account(query, account, local_only)
  187. query = query.not_excluded_by_account(account)
  188. query = query.not_domain_blocked_by_account(account) unless local_only
  189. query = query.in_allowed_languages(account) if account.allowed_languages.present?
  190. query.merge(account_silencing_filter(account))
  191. end
  192. def filter_timeline_default(query)
  193. query.excluding_silenced_accounts
  194. end
  195. def account_silencing_filter(account)
  196. if account.silenced?
  197. including_silenced_accounts
  198. else
  199. excluding_silenced_accounts
  200. end
  201. end
  202. end
  203. private
  204. def prepare_contents
  205. text&.strip!
  206. spoiler_text&.strip!
  207. end
  208. def set_reblog
  209. self.reblog = reblog.reblog if reblog? && reblog.reblog?
  210. end
  211. def set_visibility
  212. self.visibility = (account.locked? ? :private : :public) if visibility.nil?
  213. end
  214. def set_conversation
  215. self.reply = !(in_reply_to_id.nil? && thread.nil?) unless reply
  216. if reply? && !thread.nil?
  217. self.in_reply_to_account_id = carried_over_reply_to_account_id
  218. self.conversation_id = thread.conversation_id if conversation_id.nil?
  219. elsif conversation_id.nil?
  220. create_conversation
  221. end
  222. end
  223. def carried_over_reply_to_account_id
  224. if thread.account_id == account_id && thread.reply?
  225. thread.in_reply_to_account_id
  226. else
  227. thread.account_id
  228. end
  229. end
  230. def find_statuses_from_tree_path(ids, account)
  231. statuses = Status.where(id: ids).includes(:account).to_a
  232. # FIXME: n+1 bonanza
  233. statuses.reject! { |status| filter_from_context?(status, account) }
  234. # Order ancestors/descendants by tree path
  235. statuses.sort_by! { |status| ids.index(status.id) }
  236. end
  237. def filter_from_context?(status, account)
  238. should_filter = account&.blocking?(status.account_id)
  239. should_filter ||= account&.domain_blocking?(status.account.domain)
  240. should_filter ||= account&.muting?(status.account_id)
  241. should_filter ||= (status.account.silenced? && !account&.following?(status.account_id))
  242. should_filter ||= !status.permitted?(account)
  243. should_filter
  244. end
  245. end