]> cat aescling's git repositories - mastodon.git/blob - app/models/account.rb
Change belongs_to_required_by_default to true (#5888)
[mastodon.git] / app / models / account.rb
1 # frozen_string_literal: true
2 # == Schema Information
3 #
4 # Table name: accounts
5 #
6 # id :integer not null, primary key
7 # username :string default(""), not null
8 # domain :string
9 # secret :string default(""), not null
10 # private_key :text
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
20 # url :string
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
46 #
47
48 class Account < ApplicationRecord
49 MENTION_RE = /(?<=^|[^\/[:word:]])@(([a-z0-9_]+)(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
50
51 include AccountAvatar
52 include AccountFinderConcern
53 include AccountHeader
54 include AccountInteractions
55 include Attachmentable
56 include Remotable
57 include Paginable
58
59 enum protocol: [:ostatus, :activitypub]
60
61 # Local users
62 has_one :user, inverse_of: :account
63
64 validates :username, presence: true
65
66 # Remote user validations
67 validates :username, uniqueness: { scope: :domain, case_sensitive: true }, if: -> { !local? && will_save_change_to_username? }
68
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? }
74
75 # Timelines
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
81
82 # Pinned statuses
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
85
86 # Media
87 has_many :media_attachments, dependent: :destroy
88
89 # PuSH subscriptions
90 has_many :subscriptions, dependent: :destroy
91
92 # Report relationships
93 has_many :reports
94 has_many :targeted_reports, class_name: 'Report', foreign_key: :target_account_id
95
96 # Moderation notes
97 has_many :account_moderation_notes, dependent: :destroy
98 has_many :targeted_moderation_notes, class_name: 'AccountModerationNote', foreign_key: :target_account_id, dependent: :destroy
99
100 # Lists
101 has_many :list_accounts, inverse_of: :account, dependent: :destroy
102 has_many :lists, through: :list_accounts
103
104 # Account migrations
105 belongs_to :moved_to_account, class_name: 'Account', optional: true
106
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}%")) }
121
122 delegate :email,
123 :current_sign_in_ip,
124 :current_sign_in_at,
125 :confirmed?,
126 :admin?,
127 :moderator?,
128 :staff?,
129 :locale,
130 to: :user,
131 prefix: true,
132 allow_nil: true
133
134 delegate :filtered_languages, to: :user, prefix: false, allow_nil: true
135
136 def local?
137 domain.nil?
138 end
139
140 def moved?
141 moved_to_account_id.present?
142 end
143
144 def acct
145 local? ? username : "#{username}@#{domain}"
146 end
147
148 def local_username_and_domain
149 "#{username}@#{Rails.configuration.x.local_domain}"
150 end
151
152 def to_webfinger_s
153 "acct:#{local_username_and_domain}"
154 end
155
156 def subscribed?
157 subscription_expires_at.present?
158 end
159
160 def possibly_stale?
161 last_webfingered_at.nil? || last_webfingered_at <= 1.day.ago
162 end
163
164 def refresh!
165 return if local?
166 ResolveRemoteAccountService.new.call(acct)
167 end
168
169 def unsuspend!
170 transaction do
171 user&.enable! if local?
172 update!(suspended: false)
173 end
174 end
175
176 def memorialize!
177 transaction do
178 user&.disable! if local?
179 update!(memorial: true)
180 end
181 end
182
183 def keypair
184 @keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
185 end
186
187 def magic_key
188 modulus, exponent = [keypair.public_key.n, keypair.public_key.e].map do |component|
189 result = []
190
191 until component.zero?
192 result << [component % 256].pack('C')
193 component >>= 8
194 end
195
196 result.reverse.join
197 end
198
199 (['RSA'] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.')
200 end
201
202 def subscription(webhook_url)
203 @subscription ||= OStatus2::Subscription.new(remote_url, secret: secret, webhook: webhook_url, hub: hub_url)
204 end
205
206 def save_with_optional_media!
207 save!
208 rescue ActiveRecord::RecordInvalid
209 self.avatar = nil
210 self.header = nil
211 self[:avatar_remote_url] = ''
212 self[:header_remote_url] = ''
213 save!
214 end
215
216 def object_type
217 :person
218 end
219
220 def to_param
221 username
222 end
223
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) }
226 end
227
228 def excluded_from_timeline_domains
229 Rails.cache.fetch("exclude_domains_for:#{id}") { domain_blocks.pluck(:domain) }
230 end
231
232 def preferred_inbox_url
233 shared_inbox_url.presence || inbox_url
234 end
235
236 class << self
237 def readonly_attributes
238 super - %w(statuses_count following_count followers_count)
239 end
240
241 def domains
242 reorder(nil).pluck('distinct accounts.domain')
243 end
244
245 def inboxes
246 urls = reorder(nil).where(protocol: :activitypub).pluck("distinct coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url)")
247 DeliveryFailureTracker.filter(urls)
248 end
249
250 def triadic_closures(account, limit: 5, offset: 0)
251 sql = <<-SQL.squish
252 WITH first_degree AS (
253 SELECT target_account_id
254 FROM follows
255 WHERE account_id = :account_id
256 )
257 SELECT accounts.*
258 FROM follows
259 INNER JOIN accounts ON follows.target_account_id = accounts.id
260 WHERE
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
267 OFFSET :offset
268 LIMIT :limit
269 SQL
270
271 excluded_account_ids = account.excluded_from_timeline_account_ids + [account.id]
272
273 find_by_sql(
274 [sql, { account_id: account.id, excluded_account_ids: excluded_account_ids, limit: limit, offset: offset }]
275 )
276 end
277
278 def search_for(terms, limit = 10)
279 textsearch, query = generate_query_for_search(terms)
280
281 sql = <<-SQL.squish
282 SELECT
283 accounts.*,
284 ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
285 FROM accounts
286 WHERE #{query} @@ #{textsearch}
287 AND accounts.suspended = false
288 AND accounts.moved_to_account_id IS NULL
289 ORDER BY rank DESC
290 LIMIT ?
291 SQL
292
293 find_by_sql([sql, limit])
294 end
295
296 def advanced_search_for(terms, account, limit = 10, following = false)
297 textsearch, query = generate_query_for_search(terms)
298
299 if following
300 sql = <<-SQL.squish
301 WITH first_degree AS (
302 SELECT target_account_id
303 FROM follows
304 WHERE account_id = ?
305 )
306 SELECT
307 accounts.*,
308 (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
309 FROM accounts
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
315 GROUP BY accounts.id
316 ORDER BY rank DESC
317 LIMIT ?
318 SQL
319
320 find_by_sql([sql, account.id, account.id, account.id, limit])
321 else
322 sql = <<-SQL.squish
323 SELECT
324 accounts.*,
325 (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
326 FROM accounts
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
331 GROUP BY accounts.id
332 ORDER BY rank DESC
333 LIMIT ?
334 SQL
335
336 find_by_sql([sql, account.id, account.id, limit])
337 end
338 end
339
340 private
341
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} || ' ''' || ':*')"
346
347 [textsearch, query]
348 end
349 end
350
351 before_create :generate_keys
352 before_validation :normalize_domain
353 before_validation :prepare_contents, if: :local?
354
355 private
356
357 def prepare_contents
358 display_name&.strip!
359 note&.strip!
360 end
361
362 def generate_keys
363 return unless local?
364
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
368 end
369
370 def normalize_domain
371 return if local?
372
373 self.domain = TagManager.instance.normalize_domain(domain)
374 end
375 end
This page took 0.172471 seconds and 4 git commands to generate.