2016-11-16 02:56:29 +11:00
# frozen_string_literal: true
2017-05-02 10:14:47 +10:00
# == Schema Information
#
# Table name: statuses
#
# id :integer not null, primary key
# uri :string
# account_id :integer not null
# text :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# in_reply_to_id :integer
# reblog_of_id :integer
# url :string
# sensitive :boolean default(FALSE)
# visibility :integer default("public"), not null
# in_reply_to_account_id :integer
# application_id :integer
# spoiler_text :text default(""), not null
# reply :boolean default(FALSE)
# favourites_count :integer default(0), not null
# reblogs_count :integer default(0), not null
# language :string default("en"), not null
2017-05-13 03:09:21 +10:00
# conversation_id :integer
2017-05-02 10:14:47 +10:00
#
2016-11-16 02:56:29 +11:00
2016-08-18 01:56:23 +10:00
class Status < ApplicationRecord
2016-03-24 23:21:53 +11:00
include Paginable
2016-03-25 12:13:30 +11:00
include Streamable
2016-12-01 01:57:56 +11:00
include Cacheable
2016-03-24 23:21:53 +11:00
2017-03-16 08:52:57 +11:00
enum visibility : [ :public , :unlisted , :private , :direct ] , _suffix : :visibility
2016-12-01 07:32:11 +11:00
2017-01-15 08:58:50 +11:00
belongs_to :application , class_name : 'Doorkeeper::Application'
2017-04-17 23:54:33 +10:00
belongs_to :account , inverse_of : :statuses , counter_cache : true , required : true
2017-01-01 00:35:08 +11:00
belongs_to :in_reply_to_account , foreign_key : 'in_reply_to_account_id' , class_name : 'Account'
2017-05-13 03:09:21 +10:00
belongs_to :conversation
2016-02-23 02:00:20 +11:00
2016-03-06 22:34:39 +11:00
belongs_to :thread , foreign_key : 'in_reply_to_id' , class_name : 'Status' , inverse_of : :replies
2017-03-31 00:06:59 +11:00
belongs_to :reblog , foreign_key : 'reblog_of_id' , class_name : 'Status' , inverse_of : :reblogs , counter_cache : :reblogs_count
2016-02-24 05:17:37 +11:00
2016-02-24 22:57:29 +11:00
has_many :favourites , inverse_of : :status , dependent : :destroy
2016-03-16 20:46:15 +11:00
has_many :reblogs , foreign_key : 'reblog_of_id' , class_name : 'Status' , inverse_of : :reblog , dependent : :destroy
2016-03-06 22:34:39 +11:00
has_many :replies , foreign_key : 'in_reply_to_id' , class_name : 'Status' , inverse_of : :thread
2016-03-25 12:13:30 +11:00
has_many :mentions , dependent : :destroy
2016-09-06 01:46:36 +10:00
has_many :media_attachments , dependent : :destroy
2016-11-06 01:20:05 +11:00
has_and_belongs_to_many :tags
2016-02-24 05:17:37 +11:00
2016-11-22 00:59:13 +11:00
has_one :notification , as : :activity , dependent : :destroy
2017-01-20 11:00:14 +11:00
has_one :preview_card , dependent : :destroy
2016-11-22 00:59:13 +11:00
2017-05-19 11:11:23 +10:00
validates :uri , uniqueness : true , unless : :local?
validates :text , presence : true , unless : :reblog?
2017-01-13 15:54:26 +11:00
validates_with StatusLengthValidator
2017-05-19 11:11:23 +10:00
validates :reblog , uniqueness : { scope : :account } , if : :reblog?
2016-02-24 05:17:37 +11:00
2017-05-16 10:54:17 +10:00
default_scope { order ( id : :desc ) }
2016-10-10 00:15:21 +11:00
2016-12-03 00:14:49 +11:00
scope :remote , - > { where . not ( uri : nil ) }
scope :local , - > { where ( uri : nil ) }
2016-12-01 01:57:56 +11:00
2017-03-06 03:27:17 +11:00
scope :without_replies , - > { where ( 'statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id' ) }
scope :without_reblogs , - > { where ( 'statuses.reblog_of_id IS NULL' ) }
2017-04-28 23:10:41 +10:00
scope :with_public_visibility , - > { where ( visibility : :public ) }
scope :tagged_with , - > ( tag ) { joins ( :statuses_tags ) . where ( statuses_tags : { tag_id : tag } ) }
scope :local_only , - > { left_outer_joins ( :account ) . where ( accounts : { domain : nil } ) }
scope :excluding_silenced_accounts , - > { left_outer_joins ( :account ) . where ( accounts : { silenced : false } ) }
scope :including_silenced_accounts , - > { left_outer_joins ( :account ) . where ( accounts : { silenced : true } ) }
2017-05-20 05:05:32 +10:00
scope :not_excluded_by_account , - > ( account ) { where . not ( account_id : account . excluded_from_timeline_account_ids ) }
2017-05-20 21:38:13 +10: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 ) }
2017-03-06 03:27:17 +11:00
2017-01-16 00:01:33 +11:00
cache_associated :account , :application , :media_attachments , :tags , :stream_entry , mentions : :account , reblog : [ :account , :application , :stream_entry , :tags , :media_attachments , mentions : :account ] , thread : :account
2016-03-12 02:47:36 +11:00
2017-02-10 06:25:39 +11:00
def reply?
2017-05-16 05:20:55 +10:00
! in_reply_to_id . nil? || attributes [ 'reply' ]
2017-02-10 06:25:39 +11:00
end
2016-02-24 05:17:37 +11:00
def local?
2016-09-30 05:28:21 +10:00
uri . nil?
2016-02-24 05:17:37 +11:00
end
def reblog?
2016-09-30 05:28:21 +10:00
! reblog_of_id . nil?
2016-02-24 05:17:37 +11:00
end
2016-02-23 04:10:30 +11:00
def verb
2016-02-24 05:17:37 +11:00
reblog? ? :share : :post
2016-02-23 04:10:30 +11:00
end
def object_type
2016-02-24 05:17:37 +11:00
reply? ? :comment : :note
2016-02-23 04:10:30 +11:00
end
2017-04-08 04:18:30 +10:00
def proper
reblog? ? reblog : self
end
2016-02-23 04:10:30 +11:00
def content
2017-04-08 04:18:30 +10:00
proper . text
2016-02-24 05:17:37 +11:00
end
def target
2016-09-30 05:28:21 +10:00
reblog
2016-02-23 04:10:30 +11:00
end
def title
2017-04-10 04:55:54 +10:00
reblog? ? " #{ account . acct } shared a status by #{ reblog . account . acct } " : " New status by #{ account . acct } "
2016-02-23 04:10:30 +11:00
end
2016-12-22 06:00:18 +11:00
def hidden?
2017-03-16 08:52:57 +11:00
private_visibility? || direct_visibility?
2016-03-13 02:09:46 +11:00
end
2016-12-22 06:00:18 +11:00
def permitted? ( other_account = nil )
2017-03-16 08:52:57 +11:00
if direct_visibility?
account . id == other_account & . id || mentions . where ( account : other_account ) . exists?
elsif private_visibility?
account . id == other_account & . id || other_account & . following? ( account ) || mentions . where ( account : other_account ) . exists?
2017-02-12 01:10:22 +11:00
else
other_account . nil? || ! account . blocking? ( other_account )
end
2016-03-13 02:09:46 +11:00
end
2016-11-16 03:33:41 +11:00
def ancestors ( account = nil )
2017-05-17 23:38:16 +10:00
ids = Rails . cache . fetch ( " ancestors: #{ id } " ) { ( Status . find_by_sql ( [ 'WITH RECURSIVE search_tree(id, in_reply_to_id, path) AS (SELECT id, in_reply_to_id, ARRAY[id] FROM statuses WHERE id = ? UNION ALL SELECT statuses.id, statuses.in_reply_to_id, path || statuses.id FROM search_tree JOIN statuses ON statuses.id = search_tree.in_reply_to_id WHERE NOT statuses.id = ANY(path)) SELECT id FROM search_tree ORDER BY path DESC' , id ] ) - [ self ] ) . pluck ( :id ) }
find_statuses_from_tree_path ( ids , account )
2016-03-21 21:43:21 +11:00
end
2016-11-16 03:33:41 +11:00
def descendants ( account = nil )
2017-05-17 23:38:16 +10:00
ids = ( Status . find_by_sql ( [ 'WITH RECURSIVE search_tree(id, path) AS (SELECT id, ARRAY[id] FROM statuses WHERE id = ? UNION ALL SELECT statuses.id, path || statuses.id FROM search_tree JOIN statuses ON statuses.in_reply_to_id = search_tree.id WHERE NOT statuses.id = ANY(path)) SELECT id FROM search_tree ORDER BY path' , id ] ) - [ self ] ) . pluck ( :id )
find_statuses_from_tree_path ( ids , account )
2016-03-21 21:43:21 +11:00
end
2017-04-17 00:38:02 +10:00
def non_sensitive_with_media?
! sensitive? && media_attachments . any?
end
2017-05-13 03:09:21 +10:00
before_validation :prepare_contents
2017-05-16 05:20:55 +10:00
before_validation :set_reblog
before_validation :set_visibility
before_validation :set_conversation
2017-05-13 03:09:21 +10:00
2016-11-06 01:20:05 +11:00
class << self
2017-05-02 01:42:13 +10:00
def in_allowed_languages ( account )
where ( language : account . allowed_languages )
end
2016-11-06 01:20:05 +11:00
def as_home_timeline ( account )
2017-03-03 04:49:32 +11:00
where ( account : [ account ] + account . following )
2016-11-06 01:20:05 +11:00
end
2017-02-07 09:16:20 +11:00
def as_public_timeline ( account = nil , local_only = false )
2017-04-28 23:10:41 +10:00
query = timeline_scope ( local_only ) . without_replies
2016-12-06 08:59:30 +11:00
2017-05-20 05:05:32 +10:00
apply_timeline_filters ( query , account , local_only )
2016-11-06 01:20:05 +11:00
end
2017-02-07 09:16:20 +11:00
def as_tag_timeline ( tag , account = nil , local_only = false )
2017-04-28 23:10:41 +10:00
query = timeline_scope ( local_only ) . tagged_with ( tag )
2017-02-07 09:16:20 +11:00
2017-05-20 05:05:32 +10:00
apply_timeline_filters ( query , account , local_only )
2016-11-06 01:20:05 +11:00
end
2017-04-23 13:21:10 +10:00
def as_outbox_timeline ( account )
where ( account : account , visibility : :public )
end
2016-11-06 01:20:05 +11:00
def favourites_map ( status_ids , account_id )
Favourite . select ( 'status_id' ) . where ( status_id : status_ids ) . where ( account_id : account_id ) . map { | f | [ f . status_id , true ] } . to_h
end
def reblogs_map ( status_ids , account_id )
select ( 'reblog_of_id' ) . where ( reblog_of_id : status_ids ) . where ( account_id : account_id ) . map { | s | [ s . reblog_of_id , true ] } . to_h
end
2016-11-10 10:03:33 +11: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 11:04:13 +10:00
def mutes_map ( conversation_ids , account_id )
ConversationMute . select ( 'conversation_id' ) . where ( conversation_id : conversation_ids ) . where ( account_id : account_id ) . map { | m | [ m . conversation_id , true ] } . to_h
end
2016-12-04 04:21:26 +11: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
accounts = Account . where ( id : account_ids . uniq ) . map { | a | [ a . id , a ] } . to_h
cached_items . each do | item |
item . account = accounts [ item . account_id ]
item . reblog . account = accounts [ item . reblog . account_id ] if item . reblog?
end
end
2016-12-27 05:13:56 +11:00
def permitted_for ( target_account , account )
2017-05-16 10:54:17 +10:00
visibility = [ :public , :unlisted ]
2017-03-16 08:52:57 +11:00
2017-05-16 10:54:17 +10:00
if account . nil?
where ( visibility : visibility )
elsif target_account . blocking? ( account ) # get rid of blocked peeps
2017-04-05 16:02:58 +10:00
none
2017-03-16 08:52:57 +11:00
elsif account . id == target_account . id # author can see own stuff
2017-04-05 16:02:58 +10:00
all
2017-05-16 10:54:17 +10:00
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 )
joins ( " LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = #{ account . id } " )
. where ( arel_table [ :visibility ] . in ( visibility ) . or ( Mention . arel_table [ :id ] . not_eq ( nil ) ) )
. order ( visibility : :desc )
2016-12-27 05:13:56 +11:00
end
end
2016-11-10 10:03:33 +11:00
private
2017-04-28 23:10:41 +10:00
def timeline_scope ( local_only = false )
starting_scope = local_only ? Status . local_only : Status
starting_scope
. with_public_visibility
. without_reblogs
end
2017-05-20 05:05:32 +10:00
def apply_timeline_filters ( query , account , local_only )
2017-04-28 23:10:41 +10:00
if account . nil?
filter_timeline_default ( query )
else
2017-05-20 05:05:32 +10:00
filter_timeline_for_account ( query , account , local_only )
2017-04-28 23:10:41 +10:00
end
end
2017-05-20 05:05:32 +10:00
def filter_timeline_for_account ( query , account , local_only )
2017-04-28 23:10:41 +10:00
query = query . not_excluded_by_account ( account )
2017-05-20 05:05:32 +10:00
query = query . not_domain_blocked_by_account ( account ) unless local_only
2017-05-02 01:42:13 +10:00
query = query . in_allowed_languages ( account ) if account . allowed_languages . present?
2017-04-28 23:10:41 +10:00
query . merge ( account_silencing_filter ( account ) )
2016-12-06 08:59:30 +11:00
end
def filter_timeline_default ( query )
2017-04-28 23:10:41 +10:00
query . excluding_silenced_accounts
end
def account_silencing_filter ( account )
if account . silenced?
including_silenced_accounts
else
excluding_silenced_accounts
end
2016-11-10 10:03:33 +11:00
end
2016-09-09 05:23:29 +10:00
end
2016-09-23 05:39:53 +10:00
2017-05-13 03:09:21 +10:00
private
def prepare_contents
2017-04-04 09:33:34 +10:00
text & . strip!
2017-01-25 10:49:08 +11:00
spoiler_text & . strip!
2017-05-13 03:09:21 +10:00
end
2017-01-01 00:35:08 +11:00
2017-05-13 03:09:21 +10:00
def set_reblog
self . reblog = reblog . reblog if reblog? && reblog . reblog?
2016-12-22 06:00:18 +11:00
end
2017-05-13 03:09:21 +10:00
def set_visibility
self . visibility = ( account . locked? ? :private : :public ) if visibility . nil?
end
def set_conversation
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?
create_conversation
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
2016-12-22 06:00:18 +11:00
2017-05-17 23:38:16 +10:00
def find_statuses_from_tree_path ( ids , account )
Account domain blocks (#2381)
* 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
* Adding account domain blocks that filter notifications and public timelines
* Add tests for domain blocks in notifications, public timelines
Filter reblogs of blocked domains from home
* Add API for listing and creating account domain blocks
* API for creating/deleting domain blocks, tests for Status#ancestors
and Status#descendants, filter domain blocks from them
* Filter domains in streaming API
* Update account_domain_block_spec.rb
2017-05-19 09:14:30 +10:00
statuses = Status . where ( id : ids ) . includes ( :account ) . to_a
# FIXME: n+1 bonanza
2017-05-17 23:38:16 +10:00
statuses . reject! { | status | filter_from_context? ( status , account ) }
# Order ancestors/descendants by tree path
statuses . sort_by! { | status | ids . index ( status . id ) }
end
2016-12-22 06:00:18 +11:00
def filter_from_context? ( status , account )
Account domain blocks (#2381)
* 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
* Adding account domain blocks that filter notifications and public timelines
* Add tests for domain blocks in notifications, public timelines
Filter reblogs of blocked domains from home
* Add API for listing and creating account domain blocks
* API for creating/deleting domain blocks, tests for Status#ancestors
and Status#descendants, filter domain blocks from them
* Filter domains in streaming API
* Update account_domain_block_spec.rb
2017-05-19 09:14:30 +10:00
should_filter = account & . blocking? ( status . account_id )
should_filter || = account & . domain_blocking? ( status . account . domain )
should_filter || = account & . muting? ( status . account_id )
should_filter || = ( status . account . silenced? && ! account & . following? ( status . account_id ) )
should_filter || = ! status . permitted? ( account )
should_filter
2016-09-23 05:39:53 +10:00
end
2016-02-21 08:53:20 +11:00
end