1 # frozen_string_literal: true
2 # == Schema Information
6 # id :integer not null, primary key
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
15 # sensitive :boolean default(FALSE), not null
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), not null
21 # favourites_count :integer default(0), not null
22 # reblogs_count :integer default(0), not null
24 # conversation_id :integer
28 class Status
< ApplicationRecord
32 include StatusThreadingConcern
34 enum visibility
: [:public, :unlisted, :private, :direct], _suffix
: :visibility
36 belongs_to
:application, class_name
: 'Doorkeeper::Application'
38 belongs_to
:account, inverse_of
: :statuses, counter_cache
: true, required
: true
39 belongs_to
:in_reply_to_account, foreign_key
: 'in_reply_to_account_id', class_name
: 'Account'
40 belongs_to
:conversation
42 belongs_to
:thread, foreign_key
: 'in_reply_to_id', class_name
: 'Status', inverse_of
: :replies
43 belongs_to
:reblog, foreign_key
: 'reblog_of_id', class_name
: 'Status', inverse_of
: :reblogs, counter_cache
: :reblogs_count
45 has_many
:favourites, inverse_of
: :status, dependent
: :destroy
46 has_many
:reblogs, foreign_key
: 'reblog_of_id', class_name
: 'Status', inverse_of
: :reblog, dependent
: :destroy
47 has_many
:replies, foreign_key
: 'in_reply_to_id', class_name
: 'Status', inverse_of
: :thread
48 has_many
:mentions, dependent
: :destroy
49 has_many
:media_attachments, dependent
: :destroy
51 has_and_belongs_to_many
:tags
52 has_and_belongs_to_many
:preview_cards
54 has_one
:notification, as
: :activity, dependent
: :destroy
55 has_one
:stream_entry, as
: :activity, inverse_of
: :status
57 validates
:uri, uniqueness
: true, presence
: true, unless: :local?
58 validates
:text, presence
: true, unless: :reblog?
59 validates_with StatusLengthValidator
60 validates
:reblog, uniqueness
: { scope
: :account }, if: :reblog?
62 default_scope
{ recent
}
64 scope
:recent, -> { reorder(id
: :desc) }
65 scope
:remote, -> { where(local
: false).or(where
.not(uri
: nil)) }
66 scope
:local, -> { where(local
: true).or(where(uri
: nil)) }
68 scope
:without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') }
69 scope
:without_reblogs, -> { where('statuses.reblog_of_id IS NULL') }
70 scope
:with_public_visibility, -> { where(visibility
: :public) }
71 scope
:tagged_with, ->(tag
) { joins(:statuses_tags).where(statuses_tags
: { tag_id
: tag
}) }
72 scope
:excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts
: { silenced
: false }) }
73 scope
:including_silenced_accounts, -> { left_outer_joins(:account).where(accounts
: { silenced
: true }) }
74 scope
:not_excluded_by_account, ->(account
) { where
.not(account_id
: account
.excluded_from_timeline_account_ids
) }
75 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
) }
77 cache_associated
:account, :application, :media_attachments, :tags, :stream_entry, mentions
: :account, reblog
: [:account, :application, :stream_entry, :tags, :media_attachments, mentions
: :account], thread
: :account
79 delegate
:domain, to
: :account, prefix
: true
82 !in_reply_to_id
.nil? || attributes
['reply']
86 attributes
['local'] || uri
.nil?
97 reblog
? ? :share : :post
102 reply
? ? :comment : :note
106 reblog
? ? reblog
: self
119 "#{account.acct} deleted status"
121 reblog
? ? "#{account.acct} shared a status by #{reblog.account.acct}" : "New status by #{account.acct}"
126 private_visibility
? || direct_visibility
?
129 def non_sensitive_with_media
?
130 !sensitive
? && media_attachments
.any
?
134 CustomEmoji
.from_text([spoiler_text
, text
].join(' '), account
.domain
)
137 after_create
:store_uri, if: :local?
139 before_validation
:prepare_contents, if: :local?
140 before_validation
:set_reblog
141 before_validation
:set_visibility
142 before_validation
:set_conversation
143 before_validation
:set_sensitivity
144 before_validation
:set_local
147 def not_in_filtered_languages(account
)
148 where(language
: nil).or where
.not(language
: account
.filtered_languages
)
151 def as_home_timeline(account
)
152 where(account
: [account
] + account
.following
).where(visibility
: [:public, :unlisted, :private])
155 def as_public_timeline(account
= nil, local_only
= false)
156 query
= timeline_scope(local_only
).without_replies
158 apply_timeline_filters(query
, account
, local_only
)
161 def as_tag_timeline(tag
, account
= nil, local_only
= false)
162 query
= timeline_scope(local_only
).tagged_with(tag
)
164 apply_timeline_filters(query
, account
, local_only
)
167 def as_outbox_timeline(account
)
168 where(account
: account
, visibility
: :public)
171 def favourites_map(status_ids
, account_id
)
172 Favourite
.select('status_id').where(status_id
: status_ids
).where(account_id
: account_id
).map
{ |f
| [f
.status_id
, true] }.to_h
175 def reblogs_map(status_ids
, account_id
)
176 select('reblog_of_id').where(reblog_of_id
: status_ids
).where(account_id
: account_id
).map
{ |s
| [s
.reblog_of_id
, true] }.to_h
179 def mutes_map(conversation_ids
, account_id
)
180 ConversationMute
.select('conversation_id').where(conversation_id
: conversation_ids
).where(account_id
: account_id
).map
{ |m
| [m
.conversation_id
, true] }.to_h
183 def pins_map(status_ids
, account_id
)
184 StatusPin
.select('status_id').where(status_id
: status_ids
).where(account_id
: account_id
).map
{ |p
| [p
.status_id
, true] }.to_h
187 def reload_stale_associations!
(cached_items
)
190 cached_items
.each
do |item
|
191 account_ids
<< item
.account_id
192 account_ids
<< item
.reblog
.account_id
if item
.reblog
?
195 accounts
= Account
.where(id
: account_ids
.uniq
).map
{ |a
| [a
.id
, a
] }.to_h
197 cached_items
.each
do |item
|
198 item
.account
= accounts
[item
.account_id
]
199 item
.reblog
.account
= accounts
[item
.reblog
.account_id
] if item
.reblog
?
203 def permitted_for(target_account
, account
)
204 visibility
= [:public, :unlisted]
207 where(visibility
: visibility
)
208 elsif target_account
.blocking
?(account
) # get rid of blocked peeps
210 elsif account
.id
== target_account
.id
# author can see own stuff
213 # followers can see followers-only stuff, but also things they are mentioned in.
214 # non-followers can see everything that isn't private/direct, but can see stuff they are mentioned in.
215 visibility
.push(:private) if account
.following
?(target_account
)
217 joins("LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = #{account.id}")
218 .where(arel_table
[:visibility].in(visibility
).or(Mention
.arel_table
[:id].not_eq(nil)))
219 .order(visibility
: :desc)
225 def timeline_scope(local_only
= false)
226 starting_scope
= local_only
? Status
.local
: Status
228 .with_public_visibility
232 def apply_timeline_filters(query
, account
, local_only
)
234 filter_timeline_default(query
)
236 filter_timeline_for_account(query
, account
, local_only
)
240 def filter_timeline_for_account(query
, account
, local_only
)
241 query
= query
.not_excluded_by_account(account
)
242 query
= query
.not_domain_blocked_by_account(account
) unless local_only
243 query
= query
.not_in_filtered_languages(account
) if account
.filtered_languages
.present
?
244 query
.merge(account_silencing_filter(account
))
247 def filter_timeline_default(query
)
248 query
.excluding_silenced_accounts
251 def account_silencing_filter(account
)
253 including_silenced_accounts
255 excluding_silenced_accounts
263 update_attribute(:uri, ActivityPub
::TagManager.instance
.uri_for(self)) if uri
.nil?
272 self.reblog
= reblog
.reblog
if reblog
? && reblog
.reblog
?
276 self.visibility
= (account
.locked
? ? :private : :public) if visibility
.nil?
277 self.sensitive
= false if sensitive
.nil?
281 self.sensitive
= sensitive
|| spoiler_text
.present
?
285 self.reply
= !
(in_reply_to_id
.nil? && thread
.nil?) unless reply
287 if reply
? && !thread
.nil?
288 self.in_reply_to_account_id
= carried_over_reply_to_account_id
289 self.conversation_id
= thread
.conversation_id
if conversation_id
.nil?
290 elsif conversation_id
.nil?
295 def carried_over_reply_to_account_id
296 if thread
.account_id
== account_id
&& thread
.reply
?
297 thread
.in_reply_to_account_id
304 self.local
= account
.local
?