2019-07-02 08:10:25 +01:00
# frozen_string_literal: true
# == Schema Information
#
# Table name: statuses
#
# id :bigint(8) not null, primary key
# uri :string
# text :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# in_reply_to_id :bigint(8)
# reblog_of_id :bigint(8)
# url :string
# sensitive :boolean default(FALSE), not null
# visibility :integer default("public"), not null
# spoiler_text :text default(""), not null
# reply :boolean default(FALSE), not null
# language :string
# conversation_id :bigint(8)
# local :boolean
# account_id :bigint(8) not null
# application_id :bigint(8)
# in_reply_to_account_id :bigint(8)
# poll_id :bigint(8)
# group_id :integer
2019-08-07 20:53:28 +01:00
# quote_of_id :bigint(8)
2019-09-11 15:21:29 +01:00
# revised_at :datetime
2020-05-10 04:57:38 +01:00
# markdown :text
2020-07-25 00:48:31 +01:00
# expires_at :datetime
2020-08-06 05:17:17 +01:00
# has_quote :boolean
2019-07-02 08:10:25 +01:00
#
class Status < ApplicationRecord
before_destroy :unlink_from_conversations
include Paginable
include Cacheable
include StatusThreadingConcern
# If `override_timestamps` is set at creation time, Snowflake ID creation
# will be based on current time instead of `created_at`
attr_accessor :override_timestamps
update_index ( 'statuses#status' , :proper ) if Chewy . enabled?
2020-09-14 17:05:05 +01:00
enum visibility : [
:public ,
:unlisted ,
:private ,
:limited ,
:private_group ,
] , _suffix : :visibility
2019-07-02 08:10:25 +01:00
belongs_to :application , class_name : 'Doorkeeper::Application' , optional : true
belongs_to :account , inverse_of : :statuses
belongs_to :in_reply_to_account , foreign_key : 'in_reply_to_account_id' , class_name : 'Account' , optional : true
belongs_to :conversation , optional : true
belongs_to :preloadable_poll , class_name : 'Poll' , foreign_key : 'poll_id' , optional : true
belongs_to :group , optional : true
belongs_to :thread , foreign_key : 'in_reply_to_id' , class_name : 'Status' , inverse_of : :replies , optional : true
belongs_to :reblog , foreign_key : 'reblog_of_id' , class_name : 'Status' , inverse_of : :reblogs , optional : true
2019-07-29 13:25:23 +01:00
belongs_to :quote , foreign_key : 'quote_of_id' , class_name : 'Status' , inverse_of : :quotes , optional : true
2019-07-02 08:10:25 +01:00
has_many :favourites , inverse_of : :status , dependent : :destroy
2020-07-25 00:48:31 +01:00
has_many :status_bookmarks , inverse_of : :status , dependent : :destroy
2019-07-02 08:10:25 +01:00
has_many :reblogs , foreign_key : 'reblog_of_id' , class_name : 'Status' , inverse_of : :reblog , dependent : :destroy
2019-07-29 13:25:23 +01:00
has_many :quotes , foreign_key : 'quote_of_id' , class_name : 'Status' , inverse_of : :quote , dependent : :nullify
2019-07-02 08:10:25 +01:00
has_many :replies , foreign_key : 'in_reply_to_id' , class_name : 'Status' , inverse_of : :thread
has_many :mentions , dependent : :destroy , inverse_of : :status
has_many :active_mentions , - > { active } , class_name : 'Mention' , inverse_of : :status
has_many :media_attachments , dependent : :nullify
2019-09-11 15:21:29 +01:00
has_many :revisions , class_name : 'StatusRevision' , dependent : :destroy
2019-07-02 08:10:25 +01:00
has_and_belongs_to_many :tags
has_and_belongs_to_many :preview_cards
has_one :notification , as : :activity , dependent : :destroy
has_one :status_stat , inverse_of : :status
has_one :poll , inverse_of : :status , dependent : :destroy
validates :uri , uniqueness : true , presence : true , unless : :local?
validates :text , presence : true , unless : - > { with_media? || reblog? }
validates_with StatusLengthValidator
validates :reblog , uniqueness : { scope : :account } , if : :reblog?
2020-11-15 18:48:32 +00:00
validates :visibility , exclusion : { in : %w( limited ) } , if : :reblog?
2019-07-02 08:10:25 +01:00
accepts_nested_attributes_for :poll
default_scope { recent }
2020-11-14 13:14:33 +00:00
scope :recent , - > { reorder ( updated_at : :desc ) }
2019-07-02 08:10:25 +01:00
scope :remote , - > { where ( local : false ) . or ( where . not ( uri : nil ) ) }
scope :local , - > { where ( local : true ) . or ( where ( uri : nil ) ) }
2020-08-06 05:33:39 +01:00
scope :only_replies , - > { where ( 'statuses.reply IS TRUE' ) }
scope :without_replies , - > { where ( 'statuses.reply IS FALSE' ) }
2019-07-02 08:10:25 +01:00
scope :without_reblogs , - > { where ( 'statuses.reblog_of_id IS NULL' ) }
scope :with_public_visibility , - > { where ( visibility : :public ) }
scope :tagged_with , - > ( tag ) { joins ( :statuses_tags ) . where ( statuses_tags : { tag_id : tag } ) }
scope :excluding_silenced_accounts , - > { left_outer_joins ( :account ) . where ( accounts : { silenced_at : nil } ) }
scope :including_silenced_accounts , - > { left_outer_joins ( :account ) . where . not ( accounts : { silenced_at : nil } ) }
2020-08-19 17:37:31 +01:00
scope :popular_accounts , - > { left_outer_joins ( :account ) . where ( 'accounts.is_verified=true OR accounts.is_pro=true AND accounts.locked=false' ) }
2019-07-02 08:10:25 +01:00
scope :not_excluded_by_account , - > ( account ) { where . not ( account_id : account . excluded_from_timeline_account_ids ) }
scope :not_domain_blocked_by_account , - > ( account ) { account . excluded_from_timeline_domains . blank? ? left_outer_joins ( :account ) : left_outer_joins ( :account ) . where ( 'accounts.domain IS NULL OR accounts.domain NOT IN (?)' , account . excluded_from_timeline_domains ) }
scope :tagged_with_all , - > ( tags ) {
Array ( tags ) . map ( & :id ) . map ( & :to_i ) . reduce ( self ) do | result , id |
result . joins ( " INNER JOIN statuses_tags t #{ id } ON t #{ id } .status_id = statuses.id AND t #{ id } .tag_id = #{ id } " )
end
}
scope :tagged_with_none , - > ( tags ) {
Array ( tags ) . map ( & :id ) . map ( & :to_i ) . reduce ( self ) do | result , id |
result . joins ( " LEFT OUTER JOIN statuses_tags t #{ id } ON t #{ id } .status_id = statuses.id AND t #{ id } .tag_id = #{ id } " )
. where ( " t #{ id } .tag_id IS NULL " )
end
}
cache_associated :application ,
:media_attachments ,
2019-07-26 19:08:51 +01:00
:group ,
2019-07-02 08:10:25 +01:00
:conversation ,
:status_stat ,
:tags ,
:preview_cards ,
:preloadable_poll ,
account : :account_stat ,
active_mentions : { account : :account_stat } ,
reblog : [
:application ,
:tags ,
:preview_cards ,
:media_attachments ,
:conversation ,
:status_stat ,
:preloadable_poll ,
account : :account_stat ,
active_mentions : { account : :account_stat } ,
] ,
thread : { account : :account_stat }
delegate :domain , to : :account , prefix : true
def searchable_by ( preloaded = nil )
ids = [ account_id ]
if preloaded . nil?
ids += mentions . pluck ( :account_id )
ids += favourites . pluck ( :account_id )
ids += reblogs . pluck ( :account_id )
else
ids += preloaded . mentions [ id ] || [ ]
ids += preloaded . favourites [ id ] || [ ]
ids += preloaded . reblogs [ id ] || [ ]
end
ids . uniq
end
def reply?
! in_reply_to_id . nil? || attributes [ 'reply' ]
end
def local?
attributes [ 'local' ] || uri . nil?
end
def reblog?
! reblog_of_id . nil?
end
2019-08-08 14:25:28 +01:00
def quote?
! quote_of_id . nil?
end
2019-07-02 08:10:25 +01:00
def verb
if destroyed?
:delete
else
reblog? ? :share : :post
end
end
def object_type
reply? ? :comment : :note
end
def proper
reblog? ? reblog : self
end
def content
proper . text
end
def target
reblog
end
def preview_card
preview_cards . first
end
def title
if destroyed?
" #{ account . acct } deleted status "
else
reblog? ? " #{ account . acct } shared a status by #{ reblog . account . acct } " : " New status by #{ account . acct } "
end
end
def hidden?
2020-11-15 18:48:32 +00:00
private_visibility? || private_group_visibility? || limited_visibility?
2019-07-02 08:10:25 +01:00
end
def distributable?
public_visibility? || unlisted_visibility?
end
def with_media?
media_attachments . any?
end
def non_sensitive_with_media?
! sensitive? && with_media?
end
def emojis
return @emojis if defined? ( @emojis )
fields = [ spoiler_text , text ]
fields += preloadable_poll . options unless preloadable_poll . nil?
2020-12-04 03:27:09 +00:00
@emojis = CustomEmoji . from_text ( fields . join ( ' ' ) )
2019-07-02 08:10:25 +01:00
end
def mark_for_mass_destruction!
@marked_for_mass_destruction = true
end
def marked_for_mass_destruction?
@marked_for_mass_destruction
end
def replies_count
status_stat & . replies_count || 0
end
def reblogs_count
status_stat & . reblogs_count || 0
end
def favourites_count
status_stat & . favourites_count || 0
end
def increment_count! ( key )
update_status_stat! ( key = > public_send ( key ) + 1 )
end
def decrement_count! ( key )
update_status_stat! ( key = > [ public_send ( key ) - 1 , 0 ] . max )
end
after_create_commit :increment_counter_caches
after_destroy_commit :decrement_counter_caches
after_create_commit :store_uri , if : :local?
after_create_commit :update_statistics , if : :local?
around_create GabSocial :: Snowflake :: Callbacks
before_validation :prepare_contents , if : :local?
before_validation :set_reblog
before_validation :set_visibility
before_validation :set_conversation
2020-08-06 05:17:17 +01:00
before_validation :set_has_quote
2019-07-02 08:10:25 +01:00
before_validation :set_group_id
before_validation :set_local
after_create :set_poll_id
class << self
def selectable_visibilities
2020-11-15 18:48:32 +00:00
visibilities . keys - %w( limited private_group )
2019-07-02 08:10:25 +01:00
end
def in_chosen_languages ( account )
where ( language : nil ) . or where ( language : account . chosen_languages )
end
2020-05-22 05:40:31 +01:00
def as_home_timeline ( account )
2020-11-13 15:23:00 +00:00
query = where ( 'updated_at > ?' , 5 . days . ago )
2019-08-20 00:54:26 +01:00
query . where ( visibility : [ :public , :unlisted , :private ] )
2020-05-22 06:00:56 +01:00
query . where ( account : [ account ] + account . following ) . without_replies
2019-07-02 08:10:25 +01:00
end
def as_group_timeline ( group )
2020-11-13 15:37:32 +00:00
query = where ( 'updated_at > ?' , 5 . days . ago )
query . where ( group : group ) . without_replies
2019-07-02 08:10:25 +01:00
end
2020-08-06 06:15:47 +01:00
def as_group_collection_timeline ( groupIds )
2020-08-20 23:22:38 +01:00
where ( group : groupIds , reply : false )
2020-08-06 06:15:47 +01:00
end
2020-07-02 02:40:00 +01:00
def as_pro_timeline ( account = nil )
2020-07-10 15:29:51 +01:00
query = timeline_scope . without_replies . popular_accounts . where ( 'statuses.updated_at > ?' , 2 . hours . ago )
2020-07-02 02:40:00 +01:00
apply_timeline_filters ( query , account )
end
2020-06-06 21:25:07 +01:00
def as_tag_timeline ( tag , account = nil )
query = timeline_scope . tagged_with ( tag ) . without_replies
2019-07-02 08:10:25 +01:00
2020-06-06 21:25:07 +01:00
apply_timeline_filters ( query , account )
2019-07-02 08:10:25 +01:00
end
def as_outbox_timeline ( account )
where ( account : account , visibility : :public )
end
def favourites_map ( status_ids , account_id )
Favourite . select ( 'status_id' ) . where ( status_id : status_ids ) . where ( account_id : account_id ) . each_with_object ( { } ) { | f , h | h [ f . status_id ] = true }
end
2020-07-25 00:48:31 +01:00
def bookmarks_map ( status_ids , account_id )
StatusBookmark . select ( 'status_id' ) . where ( status_id : status_ids ) . where ( account_id : account_id ) . map { | f | [ f . status_id , true ] } . to_h
end
2019-07-02 08:10:25 +01:00
def reblogs_map ( status_ids , account_id )
select ( 'reblog_of_id' ) . where ( reblog_of_id : status_ids ) . where ( account_id : account_id ) . reorder ( nil ) . each_with_object ( { } ) { | s , h | h [ s . reblog_of_id ] = true }
end
def pins_map ( status_ids , account_id )
StatusPin . select ( 'status_id' ) . where ( status_id : status_ids ) . where ( account_id : account_id ) . each_with_object ( { } ) { | p , h | h [ p . status_id ] = true }
end
2020-09-10 21:07:01 +01:00
def group_pins_map ( status_ids , group_id = nil )
unless group_id . nil?
GroupPinnedStatus . select ( 'status_id' ) . where ( status_id : status_ids ) . where ( group_id : group_id ) . each_with_object ( { } ) { | p , h | h [ p . status_id ] = true }
end
end
2019-07-02 08:10:25 +01:00
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
account_ids . uniq!
return if account_ids . empty?
accounts = Account . where ( id : account_ids ) . includes ( :account_stat ) . each_with_object ( { } ) { | a , h | h [ a . id ] = a }
cached_items . each do | item |
item . account = accounts [ item . account_id ]
item . reblog . account = accounts [ item . reblog . account_id ] if item . reblog?
end
end
def permitted_for ( target_account , account )
visibility = [ :public , :unlisted ]
if account . nil?
where ( visibility : visibility )
elsif target_account . blocking? ( account ) # get rid of blocked peeps
none
elsif account . id == target_account . id # author can see own stuff
all
else
# followers can see followers-only stuff, but also things they are mentioned in.
# non-followers can see everything that isn't private/direct, but can see stuff they are mentioned in.
visibility . push ( :private ) if account . following? ( target_account )
scope = left_outer_joins ( :reblog )
scope . where ( visibility : visibility )
. or ( scope . where ( id : account . mentions . select ( :status_id ) ) )
. merge ( scope . where ( reblog_of_id : nil ) . or ( scope . where . not ( reblogs_statuses : { account_id : account . excluded_from_timeline_account_ids } ) ) )
end
end
private
2020-06-06 21:25:07 +01:00
def timeline_scope
Status . local
2019-07-02 08:10:25 +01:00
. with_public_visibility
. without_reblogs
end
2020-06-06 21:25:07 +01:00
def apply_timeline_filters ( query , account )
2019-07-02 08:10:25 +01:00
if account . nil?
filter_timeline_default ( query )
else
2020-06-06 21:25:07 +01:00
filter_timeline_for_account ( query , account )
2019-07-02 08:10:25 +01:00
end
end
2020-06-06 21:25:07 +01:00
def filter_timeline_for_account ( query , account )
2019-07-02 08:10:25 +01:00
query = query . not_excluded_by_account ( account )
query = query . in_chosen_languages ( account ) if account . chosen_languages . present?
query . merge ( account_silencing_filter ( account ) )
end
def filter_timeline_default ( query )
query . excluding_silenced_accounts
end
def account_silencing_filter ( account )
if account . silenced?
including_myself = left_outer_joins ( :account ) . where ( account_id : account . id ) . references ( :accounts )
excluding_silenced_accounts . or ( including_myself )
else
excluding_silenced_accounts
end
end
end
private
def update_status_stat! ( attrs )
return if marked_for_destruction? || destroyed?
record = status_stat || build_status_stat
record . update ( attrs )
end
def store_uri
2020-11-15 18:48:32 +00:00
update_column ( :uri , " / #{ self . account . username } /posts/ #{ self . id } " ) if uri . nil?
2019-07-02 08:10:25 +01:00
end
def prepare_contents
text & . strip!
spoiler_text & . strip!
end
def set_reblog
self . reblog = reblog . reblog if reblog? && reblog . reblog?
end
def set_poll_id
update_column ( :poll_id , poll . id ) unless poll . nil?
end
def set_visibility
self . visibility = reblog . visibility if reblog? && visibility . nil?
self . visibility = ( account . locked? ? :private : :public ) if visibility . nil?
self . sensitive = false if sensitive . nil?
end
def set_group_id
self . group_id = thread . group_id if thread & . group_id?
if reply? && ! thread . nil?
replied_status = Status . find ( in_reply_to_id )
self . group_id = replied_status . group_id
end
end
def set_conversation
self . thread = thread . reblog if thread & . reblog?
self . reply = ! ( in_reply_to_id . nil? && thread . nil? ) unless reply
if reply? && ! thread . nil?
self . in_reply_to_account_id = carried_over_reply_to_account_id
self . conversation_id = thread . conversation_id if conversation_id . nil?
elsif conversation_id . nil?
self . conversation = Conversation . new
end
end
2020-08-06 05:17:17 +01:00
def set_has_quote
self . has_quote = ! quote_of_id . nil?
end
2019-07-02 08:10:25 +01:00
def carried_over_reply_to_account_id
if thread . account_id == account_id && thread . reply?
thread . in_reply_to_account_id
else
thread . account_id
end
end
def set_local
self . local = account . local?
end
def update_statistics
return unless public_visibility? || unlisted_visibility?
ActivityTracker . increment ( 'activity:statuses:local' )
end
def increment_counter_caches
account & . increment_count! ( :statuses_count )
reblog & . increment_count! ( :reblogs_count ) if reblog? && ( public_visibility? || unlisted_visibility? )
thread & . increment_count! ( :replies_count ) if in_reply_to_id . present? && ( public_visibility? || unlisted_visibility? )
end
def decrement_counter_caches
2020-11-15 18:48:32 +00:00
return if marked_for_mass_destruction?
2019-07-02 08:10:25 +01:00
account & . decrement_count! ( :statuses_count )
reblog & . decrement_count! ( :reblogs_count ) if reblog? && ( public_visibility? || unlisted_visibility? )
thread & . decrement_count! ( :replies_count ) if in_reply_to_id . present? && ( public_visibility? || unlisted_visibility? )
end
def unlink_from_conversations
2020-11-15 18:48:32 +00:00
# return unless direct_visibility?
2019-07-02 08:10:25 +01:00
2020-11-15 18:48:32 +00:00
# mentioned_accounts = mentions.includes(:account).map(&:account)
# inbox_owners = mentioned_accounts.select(&:local?) + (account.local? ? [account] : [])
2019-07-02 08:10:25 +01:00
2020-11-15 18:48:32 +00:00
# inbox_owners.each do |inbox_owner|
# AccountConversation.remove_status(inbox_owner, self)
# end
2019-07-02 08:10:25 +01:00
end
2019-07-19 23:06:32 +01:00
2019-07-02 08:10:25 +01:00
end