catstodon/app/models/status.rb
Eugen Rochko b891a81008 Follow call on locked account creates follow request instead
Reflect "requested" relationship in API and UI
Reflect inability of private posts to be reblogged in the UI
Disable Webfinger for locked accounts
2016-12-22 23:03:57 +01:00

181 lines
6.3 KiB
Ruby

# frozen_string_literal: true
class Status < ApplicationRecord
include Paginable
include Streamable
include Cacheable
enum visibility: [:public, :unlisted, :private], _suffix: :visibility
belongs_to :account, inverse_of: :statuses
belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies
belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, touch: true
has_many :favourites, inverse_of: :status, dependent: :destroy
has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy
has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :thread
has_many :mentions, dependent: :destroy
has_many :media_attachments, dependent: :destroy
has_and_belongs_to_many :tags
has_one :notification, as: :activity, dependent: :destroy
validates :account, presence: true
validates :uri, uniqueness: true, unless: 'local?'
validates :text, presence: true, length: { maximum: 500 }, if: proc { |s| s.local? && !s.reblog? }
validates :text, presence: true, if: proc { |s| !s.local? && !s.reblog? }
validates :reblog, uniqueness: { scope: :account, message: 'of status already exists' }, if: 'reblog?'
default_scope { order('id desc') }
scope :remote, -> { where.not(uri: nil) }
scope :local, -> { where(uri: nil) }
cache_associated :account, :media_attachments, :tags, :stream_entry, mentions: :account, reblog: [:account, :stream_entry, :tags, :media_attachments, mentions: :account], thread: :account
def local?
uri.nil?
end
def reblog?
!reblog_of_id.nil?
end
def reply?
!in_reply_to_id.nil?
end
def verb
reblog? ? :share : :post
end
def object_type
reply? ? :comment : :note
end
def content
reblog? ? reblog.text : text
end
def target
reblog
end
def title
content
end
def hidden?
private_visibility?
end
def permitted?(other_account = nil)
private_visibility? ? (account.id == other_account&.id || other_account&.following?(account)) : true
end
def ancestors(account = nil)
ids = (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)
statuses = Status.where(id: ids).with_includes.group_by(&:id)
results = ids.map { |id| statuses[id].first }
results = results.reject { |status| filter_from_context?(status, account) }
results
end
def descendants(account = nil)
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)
statuses = Status.where(id: ids).with_includes.group_by(&:id)
results = ids.map { |id| statuses[id].first }
results = results.reject { |status| filter_from_context?(status, account) }
results
end
class << self
def as_home_timeline(account)
where(account: [account] + account.following)
end
def as_mentions_timeline(account)
where(id: Mention.where(account: account).select(:status_id))
end
def as_public_timeline(account = nil)
query = joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id')
.where(visibility: :public)
.where('(statuses.in_reply_to_id IS NULL OR statuses.in_reply_to_account_id = statuses.account_id)')
.where('statuses.reblog_of_id IS NULL')
account.nil? ? filter_timeline_default(query) : filter_timeline_default(filter_timeline(query, account))
end
def as_tag_timeline(tag, account = nil)
query = tag.statuses
.joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id')
.where(visibility: :public)
.where('(statuses.in_reply_to_id IS NULL OR statuses.in_reply_to_account_id = statuses.account_id)')
.where('statuses.reblog_of_id IS NULL')
account.nil? ? filter_timeline_default(query) : filter_timeline_default(filter_timeline(query, account))
end
def favourites_map(status_ids, account_id)
Favourite.select('status_id').where(status_id: status_ids).where(account_id: account_id).map { |f| [f.status_id, true] }.to_h
end
def reblogs_map(status_ids, account_id)
select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).map { |s| [s.reblog_of_id, true] }.to_h
end
def permitted_for(target_account, account)
if account&.id == target_account.id || account&.following?(target_account)
self
else
where.not(visibility: :private)
end
end
def reload_stale_associations!(cached_items)
account_ids = []
cached_items.each do |item|
account_ids << item.account_id
account_ids << item.reblog.account_id if item.reblog?
end
accounts = Account.where(id: account_ids.uniq).map { |a| [a.id, a] }.to_h
cached_items.each do |item|
item.account = accounts[item.account_id]
item.reblog.account = accounts[item.reblog.account_id] if item.reblog?
end
end
private
def filter_timeline(query, account)
blocked = Block.where(account: account).pluck(:target_account_id)
query = query.where('statuses.account_id NOT IN (?)', blocked) unless blocked.empty?
query = query.where('accounts.silenced = TRUE') if account.silenced?
query
end
def filter_timeline_default(query)
query.where('accounts.silenced = FALSE')
end
end
before_validation do
text.strip!
self.reblog = reblog.reblog if reblog? && reblog.reblog?
self.in_reply_to_account_id = thread.account_id if reply?
self.visibility = (account.locked? ? :private : :public) if visibility.nil?
end
private
def filter_from_context?(status, account)
account&.blocking?(status.account) || !status.permitted?(account)
end
end