闭社主体 forked from https://github.com/tootsuite/mastodon
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.

304 lines
11 KiB

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
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, accounts: { domain: account.excluded_from_timeline_domains }) }
  60. cache_associated :account, :application, :media_attachments, :tags, :stream_entry, mentions: :account, reblog: [:account, :application, :stream_entry, :tags, :media_attachments, mentions: :account], thread: :account
  61. def reply?
  62. !in_reply_to_id.nil? || attributes['reply']
  63. end
  64. def local?
  65. uri.nil?
  66. end
  67. def reblog?
  68. !reblog_of_id.nil?
  69. end
  70. def verb
  71. reblog? ? :share : :post
  72. end
  73. def object_type
  74. reply? ? :comment : :note
  75. end
  76. def proper
  77. reblog? ? reblog : self
  78. end
  79. def content
  80. proper.text
  81. end
  82. def target
  83. reblog
  84. end
  85. def title
  86. reblog? ? "#{account.acct} shared a status by #{reblog.account.acct}" : "New status by #{account.acct}"
  87. end
  88. def hidden?
  89. private_visibility? || direct_visibility?
  90. end
  91. def permitted?(other_account = nil)
  92. if direct_visibility?
  93. account.id == other_account&.id || mentions.where(account: other_account).exists?
  94. elsif private_visibility?
  95. account.id == other_account&.id || other_account&.following?(account) || mentions.where(account: other_account).exists?
  96. else
  97. other_account.nil? || !account.blocking?(other_account)
  98. end
  99. end
  100. def ancestors(account = nil)
  101. 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) }
  102. find_statuses_from_tree_path(ids, account)
  103. end
  104. def descendants(account = nil)
  105. 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)
  106. find_statuses_from_tree_path(ids, account)
  107. end
  108. def non_sensitive_with_media?
  109. !sensitive? && media_attachments.any?
  110. end
  111. before_validation :prepare_contents
  112. before_validation :set_reblog
  113. before_validation :set_visibility
  114. before_validation :set_conversation
  115. class << self
  116. def in_allowed_languages(account)
  117. where(language: account.allowed_languages)
  118. end
  119. def as_home_timeline(account)
  120. where(account: [account] + account.following)
  121. end
  122. def as_public_timeline(account = nil, local_only = false)
  123. query = timeline_scope(local_only).without_replies
  124. apply_timeline_filters(query, account)
  125. end
  126. def as_tag_timeline(tag, account = nil, local_only = false)
  127. query = timeline_scope(local_only).tagged_with(tag)
  128. apply_timeline_filters(query, account)
  129. end
  130. def as_outbox_timeline(account)
  131. where(account: account, visibility: :public)
  132. end
  133. def favourites_map(status_ids, account_id)
  134. Favourite.select('status_id').where(status_id: status_ids).where(account_id: account_id).map { |f| [f.status_id, true] }.to_h
  135. end
  136. def reblogs_map(status_ids, account_id)
  137. select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).map { |s| [s.reblog_of_id, true] }.to_h
  138. end
  139. def mutes_map(conversation_ids, account_id)
  140. ConversationMute.select('conversation_id').where(conversation_id: conversation_ids).where(account_id: account_id).map { |m| [m.conversation_id, true] }.to_h
  141. end
  142. def reload_stale_associations!(cached_items)
  143. account_ids = []
  144. cached_items.each do |item|
  145. account_ids << item.account_id
  146. account_ids << item.reblog.account_id if item.reblog?
  147. end
  148. accounts = Account.where(id: account_ids.uniq).map { |a| [a.id, a] }.to_h
  149. cached_items.each do |item|
  150. item.account = accounts[item.account_id]
  151. item.reblog.account = accounts[item.reblog.account_id] if item.reblog?
  152. end
  153. end
  154. def permitted_for(target_account, account)
  155. visibility = [:public, :unlisted]
  156. if account.nil?
  157. where(visibility: visibility)
  158. elsif target_account.blocking?(account) # get rid of blocked peeps
  159. none
  160. elsif account.id == target_account.id # author can see own stuff
  161. all
  162. else
  163. # followers can see followers-only stuff, but also things they are mentioned in.
  164. # non-followers can see everything that isn't private/direct, but can see stuff they are mentioned in.
  165. visibility.push(:private) if account.following?(target_account)
  166. joins("LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = #{account.id}")
  167. .where(arel_table[:visibility].in(visibility).or(Mention.arel_table[:id].not_eq(nil)))
  168. .order(visibility: :desc)
  169. end
  170. end
  171. private
  172. def timeline_scope(local_only = false)
  173. starting_scope = local_only ? Status.local_only : Status
  174. starting_scope
  175. .with_public_visibility
  176. .without_reblogs
  177. end
  178. def apply_timeline_filters(query, account)
  179. if account.nil?
  180. filter_timeline_default(query)
  181. else
  182. filter_timeline_for_account(query, account)
  183. end
  184. end
  185. def filter_timeline_for_account(query, account)
  186. query = query.not_excluded_by_account(account)
  187. query = query.in_allowed_languages(account) if account.allowed_languages.present?
  188. query.merge(account_silencing_filter(account))
  189. end
  190. def filter_timeline_default(query)
  191. query.excluding_silenced_accounts
  192. end
  193. def account_silencing_filter(account)
  194. if account.silenced?
  195. including_silenced_accounts
  196. else
  197. excluding_silenced_accounts
  198. end
  199. end
  200. end
  201. private
  202. def prepare_contents
  203. text&.strip!
  204. spoiler_text&.strip!
  205. end
  206. def set_reblog
  207. self.reblog = reblog.reblog if reblog? && reblog.reblog?
  208. end
  209. def set_visibility
  210. self.visibility = (account.locked? ? :private : :public) if visibility.nil?
  211. end
  212. def set_conversation
  213. self.reply = !(in_reply_to_id.nil? && thread.nil?) unless reply
  214. if reply? && !thread.nil?
  215. self.in_reply_to_account_id = carried_over_reply_to_account_id
  216. self.conversation_id = thread.conversation_id if conversation_id.nil?
  217. elsif conversation_id.nil?
  218. create_conversation
  219. end
  220. end
  221. def carried_over_reply_to_account_id
  222. if thread.account_id == account_id && thread.reply?
  223. thread.in_reply_to_account_id
  224. else
  225. thread.account_id
  226. end
  227. end
  228. def find_statuses_from_tree_path(ids, account)
  229. statuses = Status.where(id: ids).includes(:account).to_a
  230. # FIXME: n+1 bonanza
  231. statuses.reject! { |status| filter_from_context?(status, account) }
  232. # Order ancestors/descendants by tree path
  233. statuses.sort_by! { |status| ids.index(status.id) }
  234. end
  235. def filter_from_context?(status, account)
  236. should_filter = account&.blocking?(status.account_id)
  237. should_filter ||= account&.domain_blocking?(status.account.domain)
  238. should_filter ||= account&.muting?(status.account_id)
  239. should_filter ||= (status.account.silenced? && !account&.following?(status.account_id))
  240. should_filter ||= !status.permitted?(account)
  241. should_filter
  242. end
  243. end