]> cat aescling's git repositories - mastodon.git/blob - app/models/account.rb
Add emoji autosuggest (#5053)
[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 #
45
46 class Account < ApplicationRecord
47 MENTION_RE = /(?:^|[^\/[:word:]])@(([a-z0-9_]+)(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
48
49 include AccountAvatar
50 include AccountFinderConcern
51 include AccountHeader
52 include AccountInteractions
53 include Attachmentable
54 include Remotable
55
56 enum protocol: [:ostatus, :activitypub]
57
58 # Local users
59 has_one :user, inverse_of: :account
60
61 validates :username, presence: true
62
63 # Remote user validations
64 validates :username, uniqueness: { scope: :domain, case_sensitive: true }, if: -> { !local? && will_save_change_to_username? }
65
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? }
71
72 # Timelines
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
78
79 # Pinned statuses
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
82
83 # Media
84 has_many :media_attachments, dependent: :destroy
85
86 # PuSH subscriptions
87 has_many :subscriptions, dependent: :destroy
88
89 # Report relationships
90 has_many :reports
91 has_many :targeted_reports, class_name: 'Report', foreign_key: :target_account_id
92
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}%")) }
107
108 delegate :email,
109 :current_sign_in_ip,
110 :current_sign_in_at,
111 :confirmed?,
112 :admin?,
113 :locale,
114 to: :user,
115 prefix: true,
116 allow_nil: true
117
118 delegate :filtered_languages, to: :user, prefix: false, allow_nil: true
119
120 def local?
121 domain.nil?
122 end
123
124 def acct
125 local? ? username : "#{username}@#{domain}"
126 end
127
128 def local_username_and_domain
129 "#{username}@#{Rails.configuration.x.local_domain}"
130 end
131
132 def to_webfinger_s
133 "acct:#{local_username_and_domain}"
134 end
135
136 def subscribed?
137 subscription_expires_at.present?
138 end
139
140 def keypair
141 @keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
142 end
143
144 def subscription(webhook_url)
145 @subscription ||= OStatus2::Subscription.new(remote_url, secret: secret, webhook: webhook_url, hub: hub_url)
146 end
147
148 def save_with_optional_media!
149 save!
150 rescue ActiveRecord::RecordInvalid
151 self.avatar = nil
152 self.header = nil
153 self[:avatar_remote_url] = ''
154 self[:header_remote_url] = ''
155 save!
156 end
157
158 def object_type
159 :person
160 end
161
162 def to_param
163 username
164 end
165
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) }
168 end
169
170 def excluded_from_timeline_domains
171 Rails.cache.fetch("exclude_domains_for:#{id}") { domain_blocks.pluck(:domain) }
172 end
173
174 class << self
175 def readonly_attributes
176 super - %w(statuses_count following_count followers_count)
177 end
178
179 def domains
180 reorder(nil).pluck('distinct accounts.domain')
181 end
182
183 def inboxes
184 reorder(nil).where(protocol: :activitypub).pluck("distinct coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url)")
185 end
186
187 def triadic_closures(account, limit: 5, offset: 0)
188 sql = <<-SQL.squish
189 WITH first_degree AS (
190 SELECT target_account_id
191 FROM follows
192 WHERE account_id = :account_id
193 )
194 SELECT accounts.*
195 FROM follows
196 INNER JOIN accounts ON follows.target_account_id = accounts.id
197 WHERE
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
204 OFFSET :offset
205 LIMIT :limit
206 SQL
207
208 excluded_account_ids = account.excluded_from_timeline_account_ids + [account.id]
209
210 find_by_sql(
211 [sql, { account_id: account.id, excluded_account_ids: excluded_account_ids, limit: limit, offset: offset }]
212 )
213 end
214
215 def search_for(terms, limit = 10)
216 textsearch, query = generate_query_for_search(terms)
217
218 sql = <<-SQL.squish
219 SELECT
220 accounts.*,
221 ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
222 FROM accounts
223 WHERE #{query} @@ #{textsearch}
224 AND accounts.suspended = false
225 ORDER BY rank DESC
226 LIMIT ?
227 SQL
228
229 find_by_sql([sql, limit])
230 end
231
232 def advanced_search_for(terms, account, limit = 10)
233 textsearch, query = generate_query_for_search(terms)
234
235 sql = <<-SQL.squish
236 SELECT
237 accounts.*,
238 (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
239 FROM accounts
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
243 GROUP BY accounts.id
244 ORDER BY rank DESC
245 LIMIT ?
246 SQL
247
248 find_by_sql([sql, account.id, account.id, limit])
249 end
250
251 private
252
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} || ' ''' || ':*')"
257
258 [textsearch, query]
259 end
260 end
261
262 before_create :generate_keys
263 before_validation :normalize_domain
264 before_validation :prepare_contents, if: :local?
265
266 private
267
268 def prepare_contents
269 display_name&.strip!
270 note&.strip!
271 end
272
273 def generate_keys
274 return unless local?
275
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
279 end
280
281 def normalize_domain
282 return if local?
283
284 self.domain = TagManager.instance.normalize_domain(domain)
285 end
286 end
This page took 0.152915 seconds and 4 git commands to generate.