1 # frozen_string_literal: true
2 # == Schema Information
6 # id :integer not null, primary key
7 # username :string default(""), not null
9 # secret :string default(""), not null
11 # public_key :text default(""), not null
12 # remote_url :string default(""), not null
13 # salmon_url :string default(""), not null
14 # hub_url :string default(""), not null
15 # created_at :datetime not null
16 # updated_at :datetime not null
17 # note :text default(""), not null
18 # display_name :string default(""), not null
19 # uri :string default(""), not null
21 # avatar_file_name :string
22 # avatar_content_type :string
23 # avatar_file_size :integer
24 # avatar_updated_at :datetime
25 # header_file_name :string
26 # header_content_type :string
27 # header_file_size :integer
28 # header_updated_at :datetime
29 # avatar_remote_url :string
30 # subscription_expires_at :datetime
31 # silenced :boolean default(FALSE), not null
32 # suspended :boolean default(FALSE), not null
33 # locked :boolean default(FALSE), not null
34 # header_remote_url :string default(""), not null
35 # statuses_count :integer default(0), not null
36 # followers_count :integer default(0), not null
37 # following_count :integer default(0), not null
38 # last_webfingered_at :datetime
39 # inbox_url :string default(""), not null
40 # outbox_url :string default(""), not null
41 # shared_inbox_url :string default(""), not null
42 # followers_url :string default(""), not null
43 # protocol :integer default("ostatus"), not null
46 class Account
< ApplicationRecord
47 MENTION_RE
= /(?:^|[^\/[:word:]])@
(([a-z0-9_
]+
)(?:@
[a-z0-9\
.\
-]+
[a-z0-9
]+
)?)/i
50 include AccountFinderConcern
52 include AccountInteractions
53 include Attachmentable
56 enum protocol
: [:ostatus, :activitypub]
59 has_one
:user, inverse_of
: :account
61 validates
:username, presence
: true
63 # Remote user validations
64 validates
:username, uniqueness
: { scope
: :domain, case_sensitive
: true }, if: -> { !local
? && will_save_change_to_username
? }
66 # Local user validations
67 validates
:username, format
: { with
: /\A[a-z0-9_]+\z/i
}, uniqueness
: { scope
: :domain, case_sensitive
: false }, length
: { maximum
: 30 }, if: -> { local
? && will_save_change_to_username
? }
68 validates_with UnreservedUsernameValidator
, if: -> { local
? && will_save_change_to_username
? }
69 validates
:display_name, length
: { maximum
: 30 }, if: -> { local
? && will_save_change_to_display_name
? }
70 validates
:note, length
: { maximum
: 160 }, if: -> { local
? && will_save_change_to_note
? }
73 has_many
:stream_entries, inverse_of
: :account, dependent
: :destroy
74 has_many
:statuses, inverse_of
: :account, dependent
: :destroy
75 has_many
:favourites, inverse_of
: :account, dependent
: :destroy
76 has_many
:mentions, inverse_of
: :account, dependent
: :destroy
77 has_many
:notifications, inverse_of
: :account, dependent
: :destroy
80 has_many
:status_pins, inverse_of
: :account, dependent
: :destroy
81 has_many
:pinned_statuses, -> { reorder('status_pins.created_at DESC') }, through
: :status_pins, class_name
: 'Status', source
: :status
84 has_many
:media_attachments, dependent
: :destroy
87 has_many
:subscriptions, dependent
: :destroy
89 # Report relationships
91 has_many
:targeted_reports, class_name
: 'Report', foreign_key
: :target_account_id
93 scope
:remote, -> { where
.not(domain
: nil) }
94 scope
:local, -> { where(domain
: nil) }
95 scope
:without_followers, -> { where(followers_count
: 0) }
96 scope
:with_followers, -> { where('followers_count > 0') }
97 scope
:expiring, ->(time
) { remote
.where
.not(subscription_expires_at
: nil).where('subscription_expires_at < ?', time
) }
98 scope
:partitioned, -> { order('row_number() over (partition by domain)') }
99 scope
:silenced, -> { where(silenced
: true) }
100 scope
:suspended, -> { where(suspended
: true) }
101 scope
:recent, -> { reorder(id
: :desc) }
102 scope
:alphabetic, -> { order(domain
: :asc, username
: :asc) }
103 scope
:by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
104 scope
:matches_username, ->(value
) { where(arel_table
[:username].matches("#{value}%")) }
105 scope
:matches_display_name, ->(value
) { where(arel_table
[:display_name].matches("#{value}%")) }
106 scope
:matches_domain, ->(value
) { where(arel_table
[:domain].matches("%#{value}%")) }
118 delegate
:filtered_languages, to
: :user, prefix
: false, allow_nil
: true
125 local
? ? username
: "#{username}@#{domain}"
128 def local_username_and_domain
129 "#{username}@#{Rails.configuration.x.local_domain}"
133 "acct:#{local_username_and_domain}"
137 subscription_expires_at
.present
?
141 @keypair ||= OpenSSL
::PKey::RSA.new(private_key
|| public_key
)
144 def subscription(webhook_url
)
145 @subscription ||= OStatus2
::Subscription.new(remote_url
, secret
: secret
, webhook
: webhook_url
, hub
: hub_url
)
148 def save_with_optional_media!
150 rescue ActiveRecord
::RecordInvalid
153 self[:avatar_remote_url] = ''
154 self[:header_remote_url] = ''
166 def excluded_from_timeline_account_ids
167 Rails
.cache
.fetch("exclude_account_ids_for:#{id}") { blocking
.pluck(:target_account_id) + blocked_by
.pluck(:account_id) + muting
.pluck(:target_account_id) }
170 def excluded_from_timeline_domains
171 Rails
.cache
.fetch("exclude_domains_for:#{id}") { domain_blocks
.pluck(:domain) }
175 def readonly_attributes
176 super - %w(statuses_count following_count followers_count
)
180 reorder(nil).pluck('distinct accounts.domain')
184 reorder(nil).where(protocol
: :activitypub).pluck("distinct coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url)")
187 def triadic_closures(account
, limit
: 5, offset
: 0)
189 WITH first_degree AS (
190 SELECT target_account_id
192 WHERE account_id = :account_id
196 INNER JOIN accounts ON follows.target_account_id = accounts.id
198 account_id IN (SELECT * FROM first_degree)
199 AND target_account_id NOT IN (SELECT * FROM first_degree)
200 AND target_account_id NOT IN (:excluded_account_ids)
201 AND accounts.suspended = false
202 GROUP BY target_account_id, accounts.id
203 ORDER BY count(account_id) DESC
208 excluded_account_ids
= account
.excluded_from_timeline_account_ids +
[account
.id
]
211 [sql
, { account_id
: account
.id
, excluded_account_ids
: excluded_account_ids
, limit
: limit
, offset
: offset
}]
215 def search_for(terms
, limit
= 10)
216 textsearch
, query
= generate_query_for_search(terms
)
221 ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
223 WHERE #{query} @@ #{textsearch}
224 AND accounts.suspended = false
229 find_by_sql([sql
, limit
])
232 def advanced_search_for(terms
, account
, limit
= 10)
233 textsearch
, query
= generate_query_for_search(terms
)
238 (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
240 LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
241 WHERE #{query} @@ #{textsearch}
242 AND accounts.suspended = false
248 find_by_sql([sql
, account
.id
, account
.id
, limit
])
253 def generate_query_for_search(terms
)
254 terms
= Arel
.sql(connection
.quote(terms
.gsub(/['?\\:]/, ' ')))
255 textsearch
= "(setweight(to_tsvector('simple', accounts.display_name), 'A') || setweight(to_tsvector('simple', accounts.username), 'B') || setweight(to_tsvector('simple', coalesce(accounts.domain, '')), 'C'))"
256 query
= "to_tsquery('simple', ''' ' || #{terms} || ' ''' || ':*')"
262 before_create
:generate_keys
263 before_validation
:normalize_domain
264 before_validation
:prepare_contents, if: :local?
276 keypair
= OpenSSL
::PKey::RSA.new(Rails
.env.test
? ? 512 : 2048)
277 self.private_key
= keypair
.to_pem
278 self.public_key
= keypair
.public_key
.to_pem
284 self.domain
= TagManager
.instance
.normalize_domain(domain
)