2016-11-15 15:56:29 +00:00
# frozen_string_literal: true
2023-02-20 05:58:28 +00:00
2017-05-02 00:14:47 +00:00
# == Schema Information
#
# Table name: statuses
#
2022-03-09 08:06:17 +00:00
# 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)
2022-03-10 08:52:45 +00:00
# local_only :boolean
2022-03-09 08:06:17 +00:00
# poll_id :bigint(8)
2022-03-10 08:52:45 +00:00
# content_type :string
2022-03-09 08:06:17 +00:00
# deleted_at :datetime
# edited_at :datetime
# trendable :boolean
# ordered_media_attachment_ids :bigint(8) is an Array
2022-12-25 02:07:07 +00:00
# quote_id :bigint(8)
2017-05-02 00:14:47 +00:00
#
2016-11-15 15:56:29 +00:00
2016-08-17 15:56:23 +00:00
class Status < ApplicationRecord
2023-01-11 20:57:24 +00:00
before_destroy :unlink_from_conversations!
2018-10-07 21:44:58 +00:00
2019-08-22 19:55:56 +00:00
include Discard :: Model
2016-03-24 12:21:53 +00:00
include Paginable
2016-11-30 14:57:56 +00:00
include Cacheable
2017-06-05 14:07:44 +00:00
include StatusThreadingConcern
2022-03-25 23:38:44 +00:00
include StatusSnapshotConcern
2020-03-08 14:17:39 +00:00
include RateLimitable
rate_limit by : :account , family : :statuses
2016-03-24 12:21:53 +00:00
2019-08-22 19:55:56 +00:00
self . discard_column = :deleted_at
2018-05-03 21:02:46 +00:00
# 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
2021-11-18 21:02:08 +00:00
update_index ( 'statuses' , :proper )
2018-02-09 22:04:47 +00:00
2023-02-20 04:00:36 +00:00
enum visibility : { public : 0 , unlisted : 1 , private : 2 , direct : 3 , limited : 4 } , _suffix : :visibility
2016-11-30 20:32:11 +00:00
2018-01-19 19:56:47 +00:00
belongs_to :application , class_name : 'Doorkeeper::Application' , optional : true
2017-01-14 21:58:50 +00:00
2018-05-30 00:50:23 +00:00
belongs_to :account , inverse_of : :statuses
2023-02-20 01:19:40 +00:00
belongs_to :in_reply_to_account , class_name : 'Account' , optional : true
2018-01-19 19:56:47 +00:00
belongs_to :conversation , optional : true
2019-03-28 03:44:59 +00:00
belongs_to :preloadable_poll , class_name : 'Poll' , foreign_key : 'poll_id' , optional : true
2016-02-22 15:00:20 +00:00
2018-01-19 19:56:47 +00:00
belongs_to :thread , foreign_key : 'in_reply_to_id' , class_name : 'Status' , inverse_of : :replies , optional : true
2018-05-30 00:50:23 +00:00
belongs_to :reblog , foreign_key : 'reblog_of_id' , class_name : 'Status' , inverse_of : :reblogs , optional : true
2022-12-25 02:07:07 +00:00
belongs_to :quote , foreign_key : 'quote_id' , class_name : 'Status' , inverse_of : :quote , optional : true
2016-02-23 18:17:37 +00:00
2016-02-24 11:57:29 +00:00
has_many :favourites , inverse_of : :status , dependent : :destroy
2018-04-10 20:13:15 +00:00
has_many :bookmarks , inverse_of : :status , dependent : :destroy
2016-03-16 09:46:15 +00:00
has_many :reblogs , foreign_key : 'reblog_of_id' , class_name : 'Status' , inverse_of : :reblog , dependent : :destroy
2022-02-11 21:20:19 +00:00
has_many :reblogged_by_accounts , through : :reblogs , class_name : 'Account' , source : :account
2016-03-06 11:34:39 +00:00
has_many :replies , foreign_key : 'in_reply_to_id' , class_name : 'Status' , inverse_of : :thread
2018-10-17 15:13:04 +00:00
has_many :mentions , dependent : :destroy , inverse_of : :status
refactor(vacuum statuses): reduce amount of db queries and load for each query - improve performance (#21487)
* refactor(statuses_vacuum): remove dead code - unused
Method is not called inside class and private.
Clean up dead code.
* refactor(statuses_vacuum): make retention_period present test explicit
This private method only hides functionality.
It is best practice to be as explicit as possible.
* refactor(statuses_vacuum): improve query performance
- fix statuses_scope having sub-select for Account.remote scope by
`joins(:account).merge(Account.remote)`
- fix statuses_scope unnecessary use of `Status.arel_table[:id].lt`
because it is inexplicit, bad practice and even slower than normal
`.where('statuses.id < ?'`
- fix statuses_scope remove select(:id, :visibility) for having reusable
active record query batches (no re queries)
- fix vacuum_statuses! to use in_batches instead of find_in_batches,
because in_batches delivers a full blown active record query result,
in stead of an array - no requeries necessary
- send(:unlink_from_conversations) not to perform another db query, but
reuse the in_batches result instead.
- remove now obsolete remove_from_account_conversations method
- remove_from_search_index uses array of ids, instead of mapping
the ids from an array - this should be more efficient
- use the in_batches scope to call delete_all, instead of running
another db query for this - because it is again more efficient
- add TODO comment for calling models private method with send
* refactor(status): simplify unlink_from_conversations
- add `has_many through:` relation mentioned_accounts
- use model scope local instead of method call `Status#local?`
- more readable add account to inbox_owners when account.local?
* refactor(status): searchable_by way less sub selects
These queries all included a sub-select. Doing the same with a joins
should be more efficient.
Since this method does 5 such queries, this should be significant,
since it technically halves the query count.
This is how it was:
```ruby
[3] pry(main)> Status.first.mentions.where(account: Account.local, silent: false).explain
Status Load (1.6ms) SELECT "statuses".* FROM "statuses" WHERE "statuses"."deleted_at" IS NULL ORDER BY "statuses"."id" DESC LIMIT $1 [["LIMIT", 1]]
Mention Load (1.5ms) SELECT "mentions".* FROM "mentions" WHERE "mentions"."status_id" = $1 AND "mentions"."account_id" IN (SELECT "accounts"."id" FROM "accounts" WHERE "accounts"."domain" IS NULL) AND "mentions"."silent" = $2 [["status_id", 109382923142288414], ["silent", false]]
=> EXPLAIN for: SELECT "mentions".* FROM "mentions" WHERE "mentions"."status_id" = $1 AND "mentions"."account_id" IN (SELECT "accounts"."id" FROM "accounts" WHERE "accounts"."domain" IS NULL) AND "mentions"."silent" = $2 [["status_id", 109382923142288414], ["silent", false]]
QUERY PLAN
------------------------------------------------------------------------------------------------------------------
Nested Loop (cost=0.15..23.08 rows=1 width=41)
-> Seq Scan on accounts (cost=0.00..10.90 rows=1 width=8)
Filter: (domain IS NULL)
-> Index Scan using index_mentions_on_account_id_and_status_id on mentions (cost=0.15..8.17 rows=1 width=41)
Index Cond: ((account_id = accounts.id) AND (status_id = '109382923142288414'::bigint))
Filter: (NOT silent)
(6 rows)
```
This is how it is with this change:
```ruby
[4] pry(main)> Status.first.mentions.joins(:account).merge(Account.local).active.explain
Status Load (1.7ms) SELECT "statuses".* FROM "statuses" WHERE "statuses"."deleted_at" IS NULL ORDER BY "statuses"."id" DESC LIMIT $1 [["LIMIT", 1]]
Mention Load (0.7ms) SELECT "mentions".* FROM "mentions" INNER JOIN "accounts" ON "accounts"."id" = "mentions"."account_id" WHERE "mentions"."status_id" = $1 AND "accounts"."domain" IS NULL AND "mentions"."silent" = $2 [["status_id", 109382923142288414], ["silent", false]]
=> EXPLAIN for: SELECT "mentions".* FROM "mentions" INNER JOIN "accounts" ON "accounts"."id" = "mentions"."account_id" WHERE "mentions"."status_id" = $1 AND "accounts"."domain" IS NULL AND "mentions"."silent" = $2 [["status_id", 109382923142288414], ["silent", false]]
QUERY PLAN
------------------------------------------------------------------------------------------------------------------
Nested Loop (cost=0.15..23.08 rows=1 width=41)
-> Seq Scan on accounts (cost=0.00..10.90 rows=1 width=8)
Filter: (domain IS NULL)
-> Index Scan using index_mentions_on_account_id_and_status_id on mentions (cost=0.15..8.17 rows=1 width=41)
Index Cond: ((account_id = accounts.id) AND (status_id = '109382923142288414'::bigint))
Filter: (NOT silent)
(6 rows)
```
2022-11-27 19:41:18 +00:00
has_many :mentioned_accounts , through : :mentions , source : :account , class_name : 'Account'
2018-10-17 15:13:04 +00:00
has_many :active_mentions , - > { active } , class_name : 'Mention' , inverse_of : :status
2018-06-04 22:17:38 +00:00
has_many :media_attachments , dependent : :nullify
2022-12-25 02:07:07 +00:00
has_many :quoted , foreign_key : 'quote_id' , class_name : 'Status' , inverse_of : :quote , dependent : :nullify
2017-09-01 14:20:16 +00:00
2016-11-05 14:20:05 +00:00
has_and_belongs_to_many :tags
2017-09-01 14:20:16 +00:00
has_and_belongs_to_many :preview_cards
2016-02-23 18:17:37 +00:00
2016-11-21 13:59:13 +00:00
has_one :notification , as : :activity , dependent : :destroy
2018-08-14 17:19:32 +00:00
has_one :status_stat , inverse_of : :status
2019-03-28 03:44:59 +00:00
has_one :poll , inverse_of : :status , dependent : :destroy
2022-10-08 14:45:40 +00:00
has_one :trend , class_name : 'StatusTrend' , inverse_of : :status
2016-11-21 13:59:13 +00:00
2017-09-17 13:21:57 +00:00
validates :uri , uniqueness : true , presence : true , unless : :local?
2018-03-07 07:28:52 +00:00
validates :text , presence : true , unless : - > { with_media? || reblog? }
2017-01-13 04:54:26 +00:00
validates_with StatusLengthValidator
2018-04-23 21:52:58 +00:00
validates_with DisallowedHashtagsValidator
2017-05-19 01:11:23 +00:00
validates :reblog , uniqueness : { scope : :account } , if : :reblog?
2019-03-15 03:36:41 +00:00
validates :visibility , exclusion : { in : %w( direct limited ) } , if : :reblog?
2019-05-12 19:33:37 +00:00
validates :content_type , inclusion : { in : %w( text/plain text/markdown text/html ) } , allow_nil : true
2022-12-25 03:56:19 +00:00
validates :quote_visibility , inclusion : { in : %w( public unlisted ) } , if : :quote?
2019-03-15 12:36:38 +00:00
2019-03-28 03:44:59 +00:00
accepts_nested_attributes_for :poll
2016-02-23 18:17:37 +00:00
2019-08-22 19:55:56 +00:00
default_scope { recent . kept }
2016-10-09 13:15:21 +00:00
2017-05-23 00:53:01 +00:00
scope :recent , - > { reorder ( id : :desc ) }
2019-07-08 16:17:22 +00:00
scope :remote , - > { where ( local : false ) . where . not ( uri : nil ) }
2017-09-07 18:18:34 +00:00
scope :local , - > { where ( local : true ) . or ( where ( uri : nil ) ) }
2020-01-11 10:55:33 +00:00
scope :with_accounts , - > ( ids ) { where ( id : ids ) . includes ( :account ) }
2017-03-05 16:27:17 +00:00
scope :without_replies , - > { where ( 'statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id' ) }
2023-02-20 18:20:56 +00:00
scope :without_reblogs , - > { where ( statuses : { reblog_of_id : nil } ) }
2017-04-28 13:10:41 +00:00
scope :with_public_visibility , - > { where ( visibility : :public ) }
2020-09-23 14:01:54 +00:00
scope :tagged_with , - > ( tag_ids ) { joins ( :statuses_tags ) . where ( statuses_tags : { tag_id : tag_ids } ) }
2019-05-14 17:05:02 +00:00
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 } ) }
2017-05-19 19:05:32 +00:00
scope :not_excluded_by_account , - > ( account ) { where . not ( account_id : account . excluded_from_timeline_account_ids ) }
2017-05-20 11:38:13 +00:00
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 ) }
2023-02-18 11:39:00 +00:00
scope :tagged_with_all , lambda { | tag_ids |
2022-01-23 17:10:10 +00:00
Array ( tag_ids ) . map ( & :to_i ) . reduce ( self ) do | result , id |
2018-11-05 17:53:25 +00:00
result . joins ( " INNER JOIN statuses_tags t #{ id } ON t #{ id } .status_id = statuses.id AND t #{ id } .tag_id = #{ id } " )
end
}
2023-02-18 11:39:00 +00:00
scope :tagged_with_none , lambda { | tag_ids |
2022-01-23 17:10:10 +00:00
where ( 'NOT EXISTS (SELECT * FROM statuses_tags forbidden WHERE forbidden.status_id = statuses.id AND forbidden.tag_id IN (?))' , tag_ids )
2018-11-05 17:53:25 +00:00
}
2017-03-05 16:27:17 +00:00
2017-12-11 01:41:33 +00:00
scope :not_local_only , - > { where ( local_only : [ false , nil ] ) }
2017-12-10 23:04:28 +00:00
2023-03-19 06:47:54 +00:00
after_create_commit :trigger_create_webhooks
after_update_commit :trigger_update_webhooks
2018-11-18 23:43:52 +00:00
cache_associated :application ,
2018-08-14 17:19:32 +00:00
:media_attachments ,
:conversation ,
:status_stat ,
:tags ,
2018-10-28 05:35:03 +00:00
:preview_cards ,
2019-03-28 03:44:59 +00:00
:preloadable_poll ,
2023-01-25 18:55:40 +00:00
account : [ :account_stat , user : :role ] ,
2018-11-18 23:43:52 +00:00
active_mentions : { account : :account_stat } ,
2018-08-14 17:19:32 +00:00
reblog : [
:application ,
:tags ,
2018-10-28 05:35:03 +00:00
:preview_cards ,
2018-08-14 17:19:32 +00:00
:media_attachments ,
:conversation ,
:status_stat ,
2019-03-28 03:44:59 +00:00
:preloadable_poll ,
2023-01-25 18:55:40 +00:00
account : [ :account_stat , user : :role ] ,
2018-11-18 23:43:52 +00:00
active_mentions : { account : :account_stat } ,
2018-08-14 17:19:32 +00:00
] ,
2022-12-25 02:07:07 +00:00
quote : [
:application ,
:tags ,
:preview_cards ,
:media_attachments ,
:conversation ,
:status_stat ,
:preloadable_poll ,
account : [ :account_stat , :user ] ,
active_mentions : { account : :account_stat } ,
] ,
2018-11-18 23:43:52 +00:00
thread : { account : :account_stat }
2016-03-11 15:47:36 +00:00
2017-05-31 18:38:17 +00:00
delegate :domain , to : :account , prefix : true
2018-02-17 13:28:48 +00:00
REAL_TIME_WINDOW = 6 . hours
2023-03-22 14:47:44 +00:00
def cache_key
" v2: #{ super } "
end
2018-02-09 22:04:47 +00:00
def searchable_by ( preloaded = nil )
2019-10-02 18:04:46 +00:00
ids = [ ]
ids << account_id if local?
2018-02-09 22:04:47 +00:00
if preloaded . nil?
refactor(vacuum statuses): reduce amount of db queries and load for each query - improve performance (#21487)
* refactor(statuses_vacuum): remove dead code - unused
Method is not called inside class and private.
Clean up dead code.
* refactor(statuses_vacuum): make retention_period present test explicit
This private method only hides functionality.
It is best practice to be as explicit as possible.
* refactor(statuses_vacuum): improve query performance
- fix statuses_scope having sub-select for Account.remote scope by
`joins(:account).merge(Account.remote)`
- fix statuses_scope unnecessary use of `Status.arel_table[:id].lt`
because it is inexplicit, bad practice and even slower than normal
`.where('statuses.id < ?'`
- fix statuses_scope remove select(:id, :visibility) for having reusable
active record query batches (no re queries)
- fix vacuum_statuses! to use in_batches instead of find_in_batches,
because in_batches delivers a full blown active record query result,
in stead of an array - no requeries necessary
- send(:unlink_from_conversations) not to perform another db query, but
reuse the in_batches result instead.
- remove now obsolete remove_from_account_conversations method
- remove_from_search_index uses array of ids, instead of mapping
the ids from an array - this should be more efficient
- use the in_batches scope to call delete_all, instead of running
another db query for this - because it is again more efficient
- add TODO comment for calling models private method with send
* refactor(status): simplify unlink_from_conversations
- add `has_many through:` relation mentioned_accounts
- use model scope local instead of method call `Status#local?`
- more readable add account to inbox_owners when account.local?
* refactor(status): searchable_by way less sub selects
These queries all included a sub-select. Doing the same with a joins
should be more efficient.
Since this method does 5 such queries, this should be significant,
since it technically halves the query count.
This is how it was:
```ruby
[3] pry(main)> Status.first.mentions.where(account: Account.local, silent: false).explain
Status Load (1.6ms) SELECT "statuses".* FROM "statuses" WHERE "statuses"."deleted_at" IS NULL ORDER BY "statuses"."id" DESC LIMIT $1 [["LIMIT", 1]]
Mention Load (1.5ms) SELECT "mentions".* FROM "mentions" WHERE "mentions"."status_id" = $1 AND "mentions"."account_id" IN (SELECT "accounts"."id" FROM "accounts" WHERE "accounts"."domain" IS NULL) AND "mentions"."silent" = $2 [["status_id", 109382923142288414], ["silent", false]]
=> EXPLAIN for: SELECT "mentions".* FROM "mentions" WHERE "mentions"."status_id" = $1 AND "mentions"."account_id" IN (SELECT "accounts"."id" FROM "accounts" WHERE "accounts"."domain" IS NULL) AND "mentions"."silent" = $2 [["status_id", 109382923142288414], ["silent", false]]
QUERY PLAN
------------------------------------------------------------------------------------------------------------------
Nested Loop (cost=0.15..23.08 rows=1 width=41)
-> Seq Scan on accounts (cost=0.00..10.90 rows=1 width=8)
Filter: (domain IS NULL)
-> Index Scan using index_mentions_on_account_id_and_status_id on mentions (cost=0.15..8.17 rows=1 width=41)
Index Cond: ((account_id = accounts.id) AND (status_id = '109382923142288414'::bigint))
Filter: (NOT silent)
(6 rows)
```
This is how it is with this change:
```ruby
[4] pry(main)> Status.first.mentions.joins(:account).merge(Account.local).active.explain
Status Load (1.7ms) SELECT "statuses".* FROM "statuses" WHERE "statuses"."deleted_at" IS NULL ORDER BY "statuses"."id" DESC LIMIT $1 [["LIMIT", 1]]
Mention Load (0.7ms) SELECT "mentions".* FROM "mentions" INNER JOIN "accounts" ON "accounts"."id" = "mentions"."account_id" WHERE "mentions"."status_id" = $1 AND "accounts"."domain" IS NULL AND "mentions"."silent" = $2 [["status_id", 109382923142288414], ["silent", false]]
=> EXPLAIN for: SELECT "mentions".* FROM "mentions" INNER JOIN "accounts" ON "accounts"."id" = "mentions"."account_id" WHERE "mentions"."status_id" = $1 AND "accounts"."domain" IS NULL AND "mentions"."silent" = $2 [["status_id", 109382923142288414], ["silent", false]]
QUERY PLAN
------------------------------------------------------------------------------------------------------------------
Nested Loop (cost=0.15..23.08 rows=1 width=41)
-> Seq Scan on accounts (cost=0.00..10.90 rows=1 width=8)
Filter: (domain IS NULL)
-> Index Scan using index_mentions_on_account_id_and_status_id on mentions (cost=0.15..8.17 rows=1 width=41)
Index Cond: ((account_id = accounts.id) AND (status_id = '109382923142288414'::bigint))
Filter: (NOT silent)
(6 rows)
```
2022-11-27 19:41:18 +00:00
ids += mentions . joins ( :account ) . merge ( Account . local ) . active . pluck ( :account_id )
ids += favourites . joins ( :account ) . merge ( Account . local ) . pluck ( :account_id )
ids += reblogs . joins ( :account ) . merge ( Account . local ) . pluck ( :account_id )
ids += bookmarks . joins ( :account ) . merge ( Account . local ) . pluck ( :account_id )
ids += poll . votes . joins ( :account ) . merge ( Account . local ) . pluck ( :account_id ) if poll . present?
2018-02-09 22:04:47 +00:00
else
ids += preloaded . mentions [ id ] || [ ]
ids += preloaded . favourites [ id ] || [ ]
ids += preloaded . reblogs [ id ] || [ ]
2020-03-21 02:14:10 +00:00
ids += preloaded . bookmarks [ id ] || [ ]
2022-04-23 19:47:27 +00:00
ids += preloaded . votes [ id ] || [ ]
2018-02-09 22:04:47 +00:00
end
ids . uniq
end
2022-03-30 22:49:24 +00:00
def searchable_text
[
spoiler_text ,
FormattingHelper . extract_status_plain_text ( self ) ,
preloadable_poll ? preloadable_poll . options . join ( " \n \n " ) : nil ,
ordered_media_attachments . map ( & :description ) . join ( " \n \n " ) ,
] . compact . join ( " \n \n " )
end
2022-08-25 18:39:40 +00:00
def to_log_human_identifier
account . acct
end
def to_log_permalink
ActivityPub :: TagManager . instance . uri_for ( self )
end
2017-02-09 19:25:39 +00:00
def reply?
2017-05-15 19:20:55 +00:00
! in_reply_to_id . nil? || attributes [ 'reply' ]
2017-02-09 19:25:39 +00:00
end
2016-02-23 18:17:37 +00:00
def local?
2017-09-06 17:01:28 +00:00
attributes [ 'local' ] || uri . nil?
2016-02-23 18:17:37 +00:00
end
2021-04-24 11:35:39 +00:00
def in_reply_to_local_account?
reply? && thread & . account & . local?
end
2016-02-23 18:17:37 +00:00
def reblog?
2016-09-29 19:28:21 +00:00
! reblog_of_id . nil?
2016-02-23 18:17:37 +00:00
end
2022-12-25 02:07:07 +00:00
def quote?
! quote_id . nil? && quote
end
2022-12-25 03:56:19 +00:00
def quote_visibility
quote & . visibility
end
2018-02-17 13:28:48 +00:00
def within_realtime_window?
created_at > = REAL_TIME_WINDOW . ago
end
2016-02-22 17:10:30 +00:00
def verb
2017-08-29 14:11:05 +00:00
if destroyed?
:delete
else
reblog? ? :share : :post
end
2016-02-22 17:10:30 +00:00
end
def object_type
2016-02-23 18:17:37 +00:00
reply? ? :comment : :note
2016-02-22 17:10:30 +00:00
end
2017-04-07 18:18:30 +00:00
def proper
reblog? ? reblog : self
end
2016-02-22 17:10:30 +00:00
def content
2017-04-07 18:18:30 +00:00
proper . text
2016-02-23 18:17:37 +00:00
end
def target
2016-09-29 19:28:21 +00:00
reblog
2016-02-22 17:10:30 +00:00
end
2018-10-28 05:35:03 +00:00
def preview_card
preview_cards . first
end
2016-12-21 19:00:18 +00:00
def hidden?
2019-07-08 10:03:45 +00:00
! distributable?
2018-10-17 15:13:04 +00:00
end
def distributable?
public_visibility? || unlisted_visibility?
2016-03-12 15:09:46 +00:00
end
2019-06-04 21:11:18 +00:00
alias sign? distributable?
2018-03-07 07:28:52 +00:00
def with_media?
2022-03-09 08:06:17 +00:00
ordered_media_attachments . any?
2018-03-07 07:28:52 +00:00
end
2022-03-01 21:20:29 +00:00
def with_preview_card?
preview_cards . any?
2018-03-07 07:28:52 +00:00
end
2017-04-16 14:38:02 +00:00
def non_sensitive_with_media?
2018-03-07 07:28:52 +00:00
! sensitive? && with_media?
2017-04-16 14:38:02 +00:00
end
2019-09-11 14:32:44 +00:00
def reported?
@reported || = Report . where ( target_account : account ) . unresolved . where ( '? = ANY(status_ids)' , id ) . exists?
end
2017-09-19 00:42:40 +00:00
def emojis
2019-03-20 16:29:12 +00:00
return @emojis if defined? ( @emojis )
2019-03-28 03:44:59 +00:00
fields = [ spoiler_text , text ]
fields += preloadable_poll . options unless preloadable_poll . nil?
2022-12-25 02:07:07 +00:00
@emojis = CustomEmoji . from_text ( fields . join ( ' ' ) , account . domain ) + ( quote? ? CustomEmoji . from_text ( [ quote . spoiler_text , quote . text ] . join ( ' ' ) , quote . account . domain ) : [ ] )
2017-09-19 00:42:40 +00:00
end
2022-03-09 08:06:17 +00:00
def ordered_media_attachments
if ordered_media_attachment_ids . nil?
media_attachments
else
map = media_attachments . index_by ( & :id )
2022-04-30 22:55:26 +00:00
ordered_media_attachment_ids . filter_map { | media_attachment_id | map [ media_attachment_id ] }
2022-03-09 08:06:17 +00:00
end
end
2018-08-14 17:19:32 +00:00
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
2022-02-24 23:34:14 +00:00
def trendable?
if attributes [ 'trendable' ] . nil?
account . trendable?
else
attributes [ 'trendable' ]
end
end
2022-03-15 06:51:55 +00:00
def requires_review?
attributes [ 'trendable' ] . nil? && account . requires_review?
end
2022-02-24 23:34:14 +00:00
def requires_review_notification?
attributes [ 'trendable' ] . nil? && account . requires_review_notification?
end
2018-12-05 21:51:12 +00:00
after_create_commit :increment_counter_caches
after_destroy_commit :decrement_counter_caches
2018-05-30 00:50:23 +00:00
2017-10-13 00:52:09 +00:00
after_create_commit :store_uri , if : :local?
2017-12-29 18:52:04 +00:00
after_create_commit :update_statistics , if : :local?
2017-09-06 17:01:28 +00:00
2017-07-14 17:47:53 +00:00
before_validation :prepare_contents , if : :local?
2017-05-15 19:20:55 +00:00
before_validation :set_reblog
before_validation :set_visibility
before_validation :set_conversation
2017-09-06 17:01:28 +00:00
before_validation :set_local
2023-01-12 09:15:46 +00:00
before_create :set_locality
2017-05-12 17:09:21 +00:00
2023-01-11 20:57:24 +00:00
around_create Mastodon :: Snowflake :: Callbacks
2017-05-12 17:09:21 +00:00
2019-03-03 23:39:06 +00:00
after_create :set_poll_id
2019-03-03 21:18:23 +00:00
2016-11-05 14:20:05 +00:00
class << self
2018-10-17 20:04:40 +00:00
def selectable_visibilities
visibilities . keys - %w( direct limited )
end
2023-04-09 09:25:30 +00:00
def as_direct_timeline ( account , limit = 20 , max_id = nil , since_id = nil )
2018-05-28 09:04:06 +00:00
# direct timeline is mix of direct message from_me and to_me.
2018-09-13 22:53:09 +00:00
# 2 queries are executed with pagination.
2018-05-28 09:04:06 +00:00
# constant expression using arel_table is required for partial index
# _from_me part does not require any timeline filters
query_from_me = where ( account_id : account . id )
. where ( Status . arel_table [ :visibility ] . eq ( 3 ) )
. limit ( limit )
. order ( 'statuses.id DESC' )
# _to_me part requires mute and block filter.
# FIXME: may we check mutes.hide_notifications?
query_to_me = Status
. joins ( :mentions )
. merge ( Mention . where ( account_id : account . id ) )
. where ( Status . arel_table [ :visibility ] . eq ( 3 ) )
. limit ( limit )
. order ( 'mentions.status_id DESC' )
. not_excluded_by_account ( account )
if max_id . present?
query_from_me = query_from_me . where ( 'statuses.id < ?' , max_id )
query_to_me = query_to_me . where ( 'mentions.status_id < ?' , max_id )
end
if since_id . present?
query_from_me = query_from_me . where ( 'statuses.id > ?' , since_id )
query_to_me = query_to_me . where ( 'mentions.status_id > ?' , since_id )
end
2023-04-09 09:25:30 +00:00
# returns ActiveRecord.Relation
items = ( query_from_me . select ( :id ) . to_a + query_to_me . select ( :id ) . to_a ) . uniq ( & :id ) . sort_by ( & :id ) . reverse . take ( limit )
Status . where ( id : items . map ( & :id ) )
2017-10-16 04:02:39 +00:00
end
2016-11-05 14:20:05 +00:00
def favourites_map ( status_ids , account_id )
2018-11-16 14:02:18 +00:00
Favourite . select ( 'status_id' ) . where ( status_id : status_ids ) . where ( account_id : account_id ) . each_with_object ( { } ) { | f , h | h [ f . status_id ] = true }
2016-11-05 14:20:05 +00:00
end
2018-04-10 20:13:15 +00:00
def bookmarks_map ( status_ids , account_id )
Bookmark . select ( 'status_id' ) . where ( status_id : status_ids ) . where ( account_id : account_id ) . map { | f | [ f . status_id , true ] } . to_h
end
2016-11-05 14:20:05 +00:00
def reblogs_map ( status_ids , account_id )
2019-09-28 03:23:32 +00:00
unscoped . select ( 'reblog_of_id' ) . where ( reblog_of_id : status_ids ) . where ( account_id : account_id ) . each_with_object ( { } ) { | s , h | h [ s . reblog_of_id ] = true }
2016-11-05 14:20:05 +00:00
end
2016-11-09 23:03:33 +00:00
Feature conversations muting (#3017)
* 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
2017-05-15 01:04:13 +00:00
def mutes_map ( conversation_ids , account_id )
2018-11-16 14:02:18 +00:00
ConversationMute . select ( 'conversation_id' ) . where ( conversation_id : conversation_ids ) . where ( account_id : account_id ) . each_with_object ( { } ) { | m , h | h [ m . conversation_id ] = true }
Feature conversations muting (#3017)
* 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
2017-05-15 01:04:13 +00:00
end
2017-08-24 23:41:18 +00:00
def pins_map ( status_ids , account_id )
2018-11-16 14:02:18 +00:00
StatusPin . select ( 'status_id' ) . where ( status_id : status_ids ) . where ( account_id : account_id ) . each_with_object ( { } ) { | p , h | h [ p . status_id ] = true }
2017-08-24 23:41:18 +00:00
end
2016-12-03 17:21:26 +00: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
2017-10-13 14:44:29 +00:00
account_ids . uniq!
return if account_ids . empty?
2021-01-31 20:24:17 +00:00
accounts = Account . where ( id : account_ids ) . includes ( :account_stat , :user ) . index_by ( & :id )
2016-12-03 17:21:26 +00:00
cached_items . each do | item |
item . account = accounts [ item . account_id ]
item . reblog . account = accounts [ item . reblog . account_id ] if item . reblog?
end
end
2020-03-08 15:10:48 +00:00
def from_text ( text )
return [ ] if text . blank?
2021-11-05 20:14:35 +00:00
text . scan ( FetchLinkCardService :: URL_PATTERN ) . map ( & :second ) . uniq . filter_map do | url |
2023-02-18 22:09:40 +00:00
status = if TagManager . instance . local_url? ( url )
ActivityPub :: TagManager . instance . uri_to_resource ( url , Status )
else
EntityCache . instance . status ( url )
end
2020-03-08 15:10:48 +00:00
status & . distributable? ? status : nil
2021-01-09 23:32:01 +00:00
end
2020-03-08 15:10:48 +00:00
end
2016-09-08 19:23:29 +00:00
end
2016-09-22 19:39:53 +00:00
2017-12-10 23:04:28 +00:00
def marked_local_only?
2017-10-09 13:56:17 +00:00
# match both with and without U+FE0F (the emoji variation selector)
2017-12-14 08:27:42 +00:00
/ #{ local_only_emoji } \ ufe0f? \ z / . match? ( content )
end
def local_only_emoji
'👁'
2017-10-09 13:56:17 +00:00
end
2019-08-18 12:55:03 +00:00
def status_stat
super || build_status_stat
end
2023-04-12 10:47:05 +00:00
# This is a hack to ensure that no reblogs of discarded statuses are created,
# as this cannot be enforced through database constraints the same way we do
# for reblogs of deleted statuses.
#
# To achieve this, we redefine the internal method responsible for issuing
# the "INSERT" statement and replace the "INSERT INTO ... VALUES ..." query
# with an "INSERT INTO ... SELECT ..." query with a "WHERE deleted_at IS NULL"
# clause on the reblogged status to ensure consistency at the database level.
#
# Otherwise, the code is kept as close as possible to ActiveRecord::Persistence
# code, and actually calls it if we are not handling a reblog.
2022-03-09 19:49:14 +00:00
def self . _insert_record ( values )
2023-04-12 10:47:05 +00:00
return super unless values . is_a? ( Hash ) && values [ 'reblog_of_id' ] . present?
2022-03-09 19:49:14 +00:00
2023-04-12 10:47:05 +00:00
primary_key = self . primary_key
primary_key_value = nil
2022-03-09 19:49:14 +00:00
2023-04-12 10:47:05 +00:00
if primary_key
primary_key_value = values [ primary_key ]
if ! primary_key_value && prefetch_primary_key?
primary_key_value = next_sequence_value
values [ primary_key ] = primary_key_value
2022-03-09 19:49:14 +00:00
end
2023-04-12 10:47:05 +00:00
end
2022-03-09 19:49:14 +00:00
2023-04-12 10:47:05 +00:00
# The following line is where we differ from stock ActiveRecord implementation
im = _compile_reblog_insert ( values )
2022-03-09 19:49:14 +00:00
2023-04-12 10:47:05 +00:00
# Since we are using SELECT instead of VALUES, a non-error `nil` return is possible.
# For our purposes, it's equivalent to a foreign key constraint violation
result = connection . insert ( im , " #{ self } Create " , primary_key || false , primary_key_value )
raise ActiveRecord :: InvalidForeignKey , " (reblog_of_id)=( #{ values [ 'reblog_of_id' ] } ) is not present in table \" statuses \" " if result . nil?
2022-03-09 19:49:14 +00:00
2023-04-12 10:47:05 +00:00
result
2022-03-09 19:49:14 +00:00
end
def self . _compile_reblog_insert ( values )
# This is somewhat equivalent to the following code of ActiveRecord::Persistence:
# `arel_table.compile_insert(_substitute_values(values))`
# The main difference is that we use a `SELECT` instead of a `VALUES` clause,
# which means we have to build the `SELECT` clause ourselves and do a bit more
# manual work.
# Instead of using Arel::InsertManager#values, we are going to use Arel::InsertManager#select
im = Arel :: InsertManager . new
im . into ( arel_table )
binds = [ ]
reblog_bind = nil
values . each do | name , value |
attr = arel_table [ name ]
bind = predicate_builder . build_bind_attribute ( attr . name , value )
im . columns << attr
binds << bind
reblog_bind = bind if name == 'reblog_of_id'
end
im . select ( arel_table . where ( arel_table [ :id ] . eq ( reblog_bind ) ) . where ( arel_table [ :deleted_at ] . eq ( nil ) ) . project ( * binds ) )
im
end
2022-11-04 15:31:44 +00:00
def discard_with_reblogs
discard_time = Time . current
Status . unscoped . where ( reblog_of_id : id , deleted_at : [ nil , deleted_at ] ) . in_batches . update_all ( deleted_at : discard_time ) unless reblog?
update_attribute ( :deleted_at , discard_time )
end
2023-01-11 20:57:24 +00:00
def unlink_from_conversations!
return unless direct_visibility?
inbox_owners = mentioned_accounts . local
inbox_owners += [ account ] if account . local?
inbox_owners . each do | inbox_owner |
AccountConversation . remove_status ( inbox_owner , self )
end
end
2017-05-12 17:09:21 +00:00
private
2018-08-14 17:19:32 +00:00
def update_status_stat! ( attrs )
2018-08-18 01:03:23 +00:00
return if marked_for_destruction? || destroyed?
2019-08-18 12:55:03 +00:00
status_stat . update ( attrs )
2018-08-14 17:19:32 +00:00
end
2017-09-06 17:01:28 +00:00
def store_uri
2018-12-05 21:51:12 +00:00
update_column ( :uri , ActivityPub :: TagManager . instance . uri_for ( self ) ) if uri . nil?
2017-09-06 17:01:28 +00:00
end
2017-05-12 17:09:21 +00:00
def prepare_contents
2017-04-03 23:33:34 +00:00
text & . strip!
2017-01-24 23:49:08 +00:00
spoiler_text & . strip!
2017-05-12 17:09:21 +00:00
end
2016-12-31 13:35:08 +00:00
2017-05-12 17:09:21 +00:00
def set_reblog
self . reblog = reblog . reblog if reblog? && reblog . reblog?
2016-12-21 19:00:18 +00:00
end
2019-03-03 21:18:23 +00:00
def set_poll_id
2022-05-25 22:20:30 +00:00
update_column ( :poll_id , poll . id ) if association ( :poll ) . loaded? && poll . present?
2019-03-03 21:18:23 +00:00
end
2017-05-12 17:09:21 +00:00
def set_visibility
2019-03-17 13:54:09 +00:00
self . visibility = reblog . visibility if reblog? && visibility . nil?
2017-05-12 17:09:21 +00:00
self . visibility = ( account . locked? ? :private : :public ) if visibility . nil?
2017-07-13 01:12:25 +00:00
self . sensitive = false if sensitive . nil?
2017-05-12 17:09:21 +00:00
end
2017-12-11 03:39:27 +00:00
def set_locality
2023-04-09 09:25:30 +00:00
return unless account . domain . nil? && ! attribute_changed? ( :local_only )
self . local_only = marked_local_only?
2017-12-11 03:39:27 +00:00
end
2017-05-12 17:09:21 +00:00
def set_conversation
2018-11-25 15:35:21 +00:00
self . thread = thread . reblog if thread & . reblog?
2017-05-12 17:09:21 +00:00
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?
2018-04-12 12:45:17 +00:00
self . conversation = Conversation . new
2017-05-12 17:09:21 +00:00
end
end
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
2017-09-06 17:01:28 +00:00
def set_local
self . local = account . local?
end
2017-12-29 18:52:04 +00:00
def update_statistics
2019-07-08 10:03:45 +00:00
return unless distributable?
2017-12-29 18:52:04 +00:00
ActivityTracker . increment ( 'activity:statuses:local' )
end
2018-05-30 00:50:23 +00:00
def increment_counter_caches
return if direct_visibility?
2018-11-18 23:43:52 +00:00
account & . increment_count! ( :statuses_count )
2019-06-04 21:11:44 +00:00
reblog & . increment_count! ( :reblogs_count ) if reblog?
2019-07-08 10:03:45 +00:00
thread & . increment_count! ( :replies_count ) if in_reply_to_id . present? && distributable?
2018-05-30 00:50:23 +00:00
end
def decrement_counter_caches
2021-10-14 17:59:21 +00:00
return if direct_visibility? || new_record?
2018-05-30 00:50:23 +00:00
2018-11-18 23:43:52 +00:00
account & . decrement_count! ( :statuses_count )
2019-06-04 21:11:44 +00:00
reblog & . decrement_count! ( :reblogs_count ) if reblog?
2019-07-08 10:03:45 +00:00
thread & . decrement_count! ( :replies_count ) if in_reply_to_id . present? && distributable?
2018-05-30 00:50:23 +00:00
end
2023-03-19 06:47:54 +00:00
def trigger_create_webhooks
TriggerWebhookWorker . perform_async ( 'status.created' , 'Status' , id ) if local?
end
def trigger_update_webhooks
TriggerWebhookWorker . perform_async ( 'status.updated' , 'Status' , id ) if local?
end
2016-02-20 21:53:20 +00:00
end