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
44 # memorial :boolean default(FALSE), not null
45 # moved_to_account_id :integer
48 class Account
< ApplicationRecord
49 MENTION_RE
= /(?<=^|[^\/[:word:]])@
(([a-z0-9_
]+
)(?:@
[a-z0-9\
.\
-]+
[a-z0-9
]+
)?)/i
52 include AccountFinderConcern
54 include AccountInteractions
55 include Attachmentable
59 enum protocol
: [:ostatus, :activitypub]
62 has_one
:user, inverse_of
: :account
64 validates
:username, presence
: true
66 # Remote user validations
67 validates
:username, uniqueness
: { scope
: :domain, case_sensitive
: true }, if: -> { !local
? && will_save_change_to_username
? }
69 # Local user validations
70 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
? }
71 validates_with UnreservedUsernameValidator
, if: -> { local
? && will_save_change_to_username
? }
72 validates
:display_name, length
: { maximum
: 30 }, if: -> { local
? && will_save_change_to_display_name
? }
73 validates
:note, length
: { maximum
: 160 }, if: -> { local
? && will_save_change_to_note
? }
76 has_many
:stream_entries, inverse_of
: :account, dependent
: :destroy
77 has_many
:statuses, inverse_of
: :account, dependent
: :destroy
78 has_many
:favourites, inverse_of
: :account, dependent
: :destroy
79 has_many
:mentions, inverse_of
: :account, dependent
: :destroy
80 has_many
:notifications, inverse_of
: :account, dependent
: :destroy
83 has_many
:status_pins, inverse_of
: :account, dependent
: :destroy
84 has_many
:pinned_statuses, -> { reorder('status_pins.created_at DESC') }, through
: :status_pins, class_name
: 'Status', source
: :status
87 has_many
:media_attachments, dependent
: :destroy
90 has_many
:subscriptions, dependent
: :destroy
92 # Report relationships
94 has_many
:targeted_reports, class_name
: 'Report', foreign_key
: :target_account_id
97 has_many
:account_moderation_notes, dependent
: :destroy
98 has_many
:targeted_moderation_notes, class_name
: 'AccountModerationNote', foreign_key
: :target_account_id, dependent
: :destroy
101 has_many
:list_accounts, inverse_of
: :account, dependent
: :destroy
102 has_many
:lists, through
: :list_accounts
105 belongs_to
:moved_to_account, class_name
: 'Account', optional
: true
107 scope
:remote, -> { where
.not(domain
: nil) }
108 scope
:local, -> { where(domain
: nil) }
109 scope
:without_followers, -> { where(followers_count
: 0) }
110 scope
:with_followers, -> { where('followers_count > 0') }
111 scope
:expiring, ->(time
) { remote
.where
.not(subscription_expires_at
: nil).where('subscription_expires_at < ?', time
) }
112 scope
:partitioned, -> { order('row_number() over (partition by domain)') }
113 scope
:silenced, -> { where(silenced
: true) }
114 scope
:suspended, -> { where(suspended
: true) }
115 scope
:recent, -> { reorder(id
: :desc) }
116 scope
:alphabetic, -> { order(domain
: :asc, username
: :asc) }
117 scope
:by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
118 scope
:matches_username, ->(value
) { where(arel_table
[:username].matches("#{value}%")) }
119 scope
:matches_display_name, ->(value
) { where(arel_table
[:display_name].matches("#{value}%")) }
120 scope
:matches_domain, ->(value
) { where(arel_table
[:domain].matches("%#{value}%")) }
134 delegate
:filtered_languages, to
: :user, prefix
: false, allow_nil
: true
141 moved_to_account_id
.present
?
145 local
? ? username
: "#{username}@#{domain}"
148 def local_username_and_domain
149 "#{username}@#{Rails.configuration.x.local_domain}"
153 "acct:#{local_username_and_domain}"
157 subscription_expires_at
.present
?
161 last_webfingered_at
.nil? || last_webfingered_at
<= 1.day
.ago
166 ResolveRemoteAccountService
.new
.call(acct
)
171 user
&.enable!
if local
?
172 update!
(suspended
: false)
178 user
&.disable!
if local
?
179 update!
(memorial
: true)
184 @keypair ||= OpenSSL
::PKey::RSA.new(private_key
|| public_key
)
188 modulus
, exponent
= [keypair
.public_key
.n
, keypair
.public_key
.e
].map
do |component
|
191 until component
.zero
?
192 result
<< [component
% 256].pack('C')
199 (['RSA'] +
[modulus
, exponent
].map
{ |n
| Base64
.urlsafe_encode64(n
) }).join('.')
202 def subscription(webhook_url
)
203 @subscription ||= OStatus2
::Subscription.new(remote_url
, secret
: secret
, webhook
: webhook_url
, hub
: hub_url
)
206 def save_with_optional_media!
208 rescue ActiveRecord
::RecordInvalid
211 self[:avatar_remote_url] = ''
212 self[:header_remote_url] = ''
224 def excluded_from_timeline_account_ids
225 Rails
.cache
.fetch("exclude_account_ids_for:#{id}") { blocking
.pluck(:target_account_id) + blocked_by
.pluck(:account_id) + muting
.pluck(:target_account_id) }
228 def excluded_from_timeline_domains
229 Rails
.cache
.fetch("exclude_domains_for:#{id}") { domain_blocks
.pluck(:domain) }
232 def preferred_inbox_url
233 shared_inbox_url
.presence
|| inbox_url
237 def readonly_attributes
238 super - %w(statuses_count following_count followers_count
)
242 reorder(nil).pluck('distinct accounts.domain')
246 urls
= reorder(nil).where(protocol
: :activitypub).pluck("distinct coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url)")
247 DeliveryFailureTracker
.filter(urls
)
250 def triadic_closures(account
, limit
: 5, offset
: 0)
252 WITH first_degree AS (
253 SELECT target_account_id
255 WHERE account_id = :account_id
259 INNER JOIN accounts ON follows.target_account_id = accounts.id
261 account_id IN (SELECT * FROM first_degree)
262 AND target_account_id NOT IN (SELECT * FROM first_degree)
263 AND target_account_id NOT IN (:excluded_account_ids)
264 AND accounts.suspended = false
265 GROUP BY target_account_id, accounts.id
266 ORDER BY count(account_id) DESC
271 excluded_account_ids
= account
.excluded_from_timeline_account_ids +
[account
.id
]
274 [sql
, { account_id
: account
.id
, excluded_account_ids
: excluded_account_ids
, limit
: limit
, offset
: offset
}]
278 def search_for(terms
, limit
= 10)
279 textsearch
, query
= generate_query_for_search(terms
)
284 ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
286 WHERE #{query} @@ #{textsearch}
287 AND accounts.suspended = false
288 AND accounts.moved_to_account_id IS NULL
293 find_by_sql([sql
, limit
])
296 def advanced_search_for(terms
, account
, limit
= 10, following
= false)
297 textsearch
, query
= generate_query_for_search(terms
)
301 WITH first_degree AS (
302 SELECT target_account_id
308 (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
310 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 = ?)
311 WHERE accounts.id IN (SELECT * FROM first_degree)
312 AND #{query} @@ #{textsearch}
313 AND accounts.suspended = false
314 AND accounts.moved_to_account_id IS NULL
320 find_by_sql([sql
, account
.id
, account
.id
, account
.id
, limit
])
325 (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
327 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 = ?)
328 WHERE #{query} @@ #{textsearch}
329 AND accounts.suspended = false
330 AND accounts.moved_to_account_id IS NULL
336 find_by_sql([sql
, account
.id
, account
.id
, limit
])
342 def generate_query_for_search(terms
)
343 terms
= Arel
.sql(connection
.quote(terms
.gsub(/['?\\:]/, ' ')))
344 textsearch
= "(setweight(to_tsvector('simple', accounts.display_name), 'A') || setweight(to_tsvector('simple', accounts.username), 'B') || setweight(to_tsvector('simple', coalesce(accounts.domain, '')), 'C'))"
345 query
= "to_tsquery('simple', ''' ' || #{terms} || ' ''' || ':*')"
351 before_create
:generate_keys
352 before_validation
:normalize_domain
353 before_validation
:prepare_contents, if: :local?
365 keypair
= OpenSSL
::PKey::RSA.new(Rails
.env.test
? ? 512 : 2048)
366 self.private_key
= keypair
.to_pem
367 self.public_key
= keypair
.public_key
.to_pem
373 self.domain
= TagManager
.instance
.normalize_domain(domain
)