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)
|
|
|
|
# poll_id :bigint(8)
|
|
|
|
# deleted_at :datetime
|
|
|
|
# edited_at :datetime
|
|
|
|
# trendable :boolean
|
|
|
|
# ordered_media_attachment_ids :bigint(8) is an Array
|
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-12-01 11:00:41 +00:00
|
|
|
include Cacheable
|
2019-08-22 19:55:56 +00:00
|
|
|
include Discard::Model
|
2016-03-24 12:21:53 +00:00
|
|
|
include Paginable
|
2020-03-08 14:17:39 +00:00
|
|
|
include RateLimitable
|
2023-12-01 11:00:41 +00:00
|
|
|
include Status::SafeReblogInsert
|
|
|
|
include Status::SearchConcern
|
|
|
|
include Status::SnapshotConcern
|
|
|
|
include Status::ThreadingConcern
|
2020-03-08 14:17:39 +00:00
|
|
|
|
|
|
|
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)
|
2023-08-24 14:40:04 +00:00
|
|
|
update_index('public_statuses', :proper)
|
2018-02-09 22:04:47 +00:00
|
|
|
|
2024-02-16 14:54:23 +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
|
2023-04-30 12:06:53 +00:00
|
|
|
belongs_to :preloadable_poll, class_name: 'Poll', foreign_key: 'poll_id', optional: true, inverse_of: false
|
2016-02-22 15:00:20 +00:00
|
|
|
|
2024-01-18 12:29:41 +00:00
|
|
|
with_options class_name: 'Status', optional: true do
|
|
|
|
belongs_to :thread, foreign_key: 'in_reply_to_id', inverse_of: :replies
|
|
|
|
belongs_to :reblog, foreign_key: 'reblog_of_id', inverse_of: :reblogs
|
|
|
|
end
|
2016-02-23 18:17:37 +00:00
|
|
|
|
2020-12-21 17:22:17 +00:00
|
|
|
has_many :favourites, inverse_of: :status, dependent: :destroy
|
2019-11-13 22:02:10 +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
|
2023-12-01 15:52:47 +00:00
|
|
|
has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :thread, dependent: nil
|
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-06-04 22:17:38 +00:00
|
|
|
has_many :media_attachments, dependent: :nullify
|
2017-09-01 14:20:16 +00:00
|
|
|
|
2023-12-01 15:52:47 +00:00
|
|
|
# The `dependent` option is enabled by the initial `mentions` association declaration
|
|
|
|
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status # rubocop:disable Rails/HasManyOrHasOneDependent
|
|
|
|
|
2023-08-29 15:51:13 +00:00
|
|
|
# Those associations are used for the private search index
|
|
|
|
has_many :local_mentioned, -> { merge(Account.local) }, through: :active_mentions, source: :account
|
|
|
|
has_many :local_favorited, -> { merge(Account.local) }, through: :favourites, source: :account
|
|
|
|
has_many :local_reblogged, -> { merge(Account.local) }, through: :reblogs, source: :account
|
|
|
|
has_many :local_bookmarked, -> { merge(Account.local) }, through: :bookmarks, source: :account
|
|
|
|
|
2016-11-05 14:20:05 +00:00
|
|
|
has_and_belongs_to_many :tags
|
2016-02-23 18:17:37 +00:00
|
|
|
|
2023-12-07 14:49:05 +00:00
|
|
|
has_one :preview_cards_status, inverse_of: :status, dependent: :delete
|
2023-12-01 15:52:47 +00:00
|
|
|
|
2016-11-21 13:59:13 +00:00
|
|
|
has_one :notification, as: :activity, dependent: :destroy
|
2023-12-01 15:52:47 +00:00
|
|
|
has_one :status_stat, inverse_of: :status, dependent: nil
|
2019-03-28 03:44:59 +00:00
|
|
|
has_one :poll, inverse_of: :status, dependent: :destroy
|
2023-12-01 15:52:47 +00:00
|
|
|
has_one :trend, class_name: 'StatusTrend', inverse_of: :status, dependent: nil
|
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-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 }) }
|
2020-09-23 14:01:54 +00:00
|
|
|
scope :tagged_with, ->(tag_ids) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag_ids }) }
|
2017-05-19 19:05:32 +00:00
|
|
|
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
|
2024-03-27 09:59:45 +00:00
|
|
|
scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).merge(Account.not_domain_blocked_by_account(account)) }
|
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|
|
2023-07-13 13:52:37 +00:00
|
|
|
result.where(<<~SQL.squish, tag_id: id)
|
|
|
|
EXISTS(SELECT 1 FROM statuses_tags WHERE statuses_tags.status_id = statuses.id AND statuses_tags.tag_id = :tag_id)
|
|
|
|
SQL
|
2018-11-05 17:53:25 +00:00
|
|
|
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
|
|
|
}
|
2024-04-16 09:17:03 +00:00
|
|
|
scope :list_eligible_visibility, -> { where(visibility: %i(public unlisted private)) }
|
2017-03-05 16:27:17 +00:00
|
|
|
|
2023-03-19 06:47:54 +00:00
|
|
|
after_create_commit :trigger_create_webhooks
|
|
|
|
after_update_commit :trigger_update_webhooks
|
|
|
|
|
2023-05-02 16:23:35 +00:00
|
|
|
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?
|
|
|
|
|
|
|
|
before_validation :prepare_contents, if: :local?
|
|
|
|
before_validation :set_reblog
|
|
|
|
before_validation :set_visibility
|
|
|
|
before_validation :set_conversation
|
|
|
|
before_validation :set_local
|
|
|
|
|
|
|
|
around_create Mastodon::Snowflake::Callbacks
|
|
|
|
|
|
|
|
after_create :set_poll_id
|
|
|
|
|
|
|
|
# The `prepend: true` option below ensures this runs before
|
|
|
|
# the `dependent: destroy` callbacks remove relevant records
|
|
|
|
before_destroy :unlink_from_conversations!, prepend: true
|
|
|
|
|
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,
|
2019-03-28 03:44:59 +00:00
|
|
|
:preloadable_poll,
|
2023-11-13 09:58:28 +00:00
|
|
|
preview_cards_status: [:preview_card],
|
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,
|
|
|
|
:media_attachments,
|
|
|
|
:conversation,
|
|
|
|
:status_stat,
|
2019-03-28 03:44:59 +00:00
|
|
|
:preloadable_poll,
|
2023-11-13 09:58:28 +00:00
|
|
|
preview_cards_status: [:preview_card],
|
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
|
|
|
],
|
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
|
2023-08-04 14:13:47 +00:00
|
|
|
"v3:#{super}"
|
2023-03-22 14:47:44 +00:00
|
|
|
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
|
|
|
|
|
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
|
2023-11-13 09:58:28 +00:00
|
|
|
preview_cards_status&.preview_card&.tap { |x| x.original_url = preview_cards_status.url }
|
|
|
|
end
|
|
|
|
|
|
|
|
def reset_preview_card!
|
|
|
|
PreviewCardsStatus.where(status_id: id).delete_all
|
2018-10-28 05:35:03 +00:00
|
|
|
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?
|
2022-03-01 21:20:29 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def with_preview_card?
|
2023-11-13 09:58:28 +00:00
|
|
|
preview_cards_status.present?
|
2018-03-07 07:28:52 +00:00
|
|
|
end
|
|
|
|
|
2023-08-24 14:40:04 +00:00
|
|
|
def with_poll?
|
|
|
|
preloadable_poll.present?
|
|
|
|
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?
|
2024-01-24 09:57:49 +00:00
|
|
|
@reported ||= Report.where(target_account: account).unresolved.exists?(['? = ANY(status_ids)', id])
|
2019-09-11 14:32:44 +00:00
|
|
|
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?
|
|
|
|
|
2019-03-20 16:29:12 +00:00
|
|
|
@emojis = CustomEmoji.from_text(fields.join(' '), 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?
|
2023-12-22 15:10:39 +00:00
|
|
|
# NOTE: sort Ruby-side to avoid hitting the database when the status is
|
|
|
|
# not persisted to database yet
|
|
|
|
media_attachments.sort_by(&:id)
|
2022-03-09 08:06:17 +00:00
|
|
|
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
|
|
|
|
|
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
|
|
|
|
|
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
|
|
|
|
|
2019-11-13 22:02:10 +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
|
|
|
|
|
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
|
|
|
|
2019-08-18 12:55:03 +00:00
|
|
|
def status_stat
|
|
|
|
super || build_status_stat
|
|
|
|
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?
|
|
|
|
end
|
|
|
|
|
|
|
|
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
|