fedibird-fe/app/models/account.rb
noellabo a27fcf5e30 Add feature circle
Squashed commit of the following:

commit 7b2ba61c4841e23081552fb79270e4e430dd1fe0
Author: noellabo <noel.yoshiba@gmail.com>
Date:   Sat Sep 5 16:03:52 2020 +0900

    Add the ability to change to a new circle by replying to a circle

commit 7013a228c65c7bd147885de458b50095f3c24334
Author: noellabo <noel.yoshiba@gmail.com>
Date:   Sat Sep 5 16:10:57 2020 +0900

    fixup! add-limited-visibility-icon-to-status

commit 679aa8a7f9bef42ee5d0b326d9ae4925a1999939
Author: noellabo <noel.yoshiba@gmail.com>
Date:   Sat Sep 5 15:12:53 2020 +0900

    Fix 14666

commit b3addd8220d8bb3512ff345b32ca83c714dadd2a
Author: noellabo <noel.yoshiba@gmail.com>
Date:   Sat Sep 5 11:44:12 2020 +0900

    Add Japanese translation for circle

commit b7f4b773a0cd554084d5ad6a5923adb06b3acfc4
Author: noellabo <noel.yoshiba@gmail.com>
Date:   Sat Sep 5 11:40:12 2020 +0900

    Squashed commit of the following:

    commit b85a4685b27c49462288aba5f38723b91e936c4a
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Sat Sep 5 10:50:03 2020 +0900

        Changed to remove restrictions on privacy options and allow users to switch circles when replying

    commit 0a8c0140c73d7c5333e4f8017964adb5061a7cf1
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Sat Sep 5 09:33:07 2020 +0900

        Change limited visibility icon

    commit b64adf19788d828249408454ec6afa9beb3d4872
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Mon Aug 31 06:50:56 2020 +0900

        Fix a change to limited-visibility-bearcaps replies

    commit ed361405b5e38857a2f42b0515a599ddcdd412cf
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Thu Aug 27 15:53:18 2020 +0900

        Fix composer text when change visibility

    commit 4da3adddb6ffde43070d743e34c5b56e06579b30
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Sat Aug 22 22:34:23 2020 +0900

        Fix wrong circle_id when changing visibility

    commit 752d7fc2a3c9e34fab9993d767f83c6eae7ba55a
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Sun Aug 9 13:12:51 2020 +0900

        Add circle reply and redraft

    commit 5978bc04a24695edce6717bda89dcf6f861ef2c4
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Mon Jul 27 01:07:52 2020 +0900

        Fix remove unused props

    commit 7970f69676c24b4aa9385fee8b1635c46ba52fcd
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Sun Jul 26 21:17:07 2020 +0900

        Separate circle choice from privacy

    commit 36f6a684c0b0c895d4d0f1b9d09b05c91b104666
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Thu Jul 23 10:54:25 2020 +0900

        Add UI for posting to circles

    commit 7ef48003c1407275663dd603b124d292db2aa93a
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Fri Jul 24 12:55:10 2020 +0900

        Fix silent mention by circle

commit 7a1caed49333c3d3241301afb77639cdf1cabdc0
Author: noellabo <noel.yoshiba@gmail.com>
Date:   Sat Sep 5 11:38:10 2020 +0900

    Squashed commit of the following:

    commit dca71fab86c830932ca760b7d8b3f89cc25c453e
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Sat Sep 5 09:31:26 2020 +0900

        Revert "Add focus setting when opening the circle column"

        This reverts commit 3a93ac99312a13b68b7edc2b81313fb0ffb7bcdc.

    commit 0a1bc8307bb699c7eb3024072ce14a440df1fc87
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Sat Sep 5 09:31:11 2020 +0900

        Change limited visibility icon

    commit 9784f8b562f6592e9d9190ca29d2b2e870006d10
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Thu Aug 13 21:52:07 2020 +0900

        Add focus setting when opening the circle column

    commit a84f680c167fab9276550850c60f9108d251144e
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Thu Aug 13 15:55:27 2020 +0900

        Fix message

    commit e3f11c4adac57b6e6a15c981ed6f4721a1634212
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Mon Jul 27 01:01:23 2020 +0900

        Fix light-theme

    commit d7d96eda5b86d3e3f654ce79888e7cf5aa535db5
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Sun Jul 26 21:50:56 2020 +0900

        Fix circles loading in share page and followers search

    commit 10b821f7b8c0a87cea3df51f09deeadc2cb40b32
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Fri Jul 24 14:08:00 2020 +0900

        Refactor list items

    commit e020072915572ce409039ccf799d08f8d8b5b393
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Thu Jul 23 20:15:38 2020 +0900

        Fixed a bug that circle name change is not reflected in the list

    commit 735bc41161b4c09a8dafe2c0064096b3ca79f2a0
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Wed Jul 22 08:49:47 2020 +0900

        Add UI for managing circle members

    commit d7c3145b8fa84be0631bf7f41bb229f3e6d03ff1
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Wed Jul 22 07:34:52 2020 +0900

        Add the followers option to AccountSearchSercive

    commit 65e2b0c4299b72ede440b50089c1bd6afa6c9c05
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Wed Jul 22 07:05:56 2020 +0900

        Add CircleSerializer

commit a639e1803abf5590068846dbe98bc5edfaa2ad82
Author: noellabo <noel.yoshiba@gmail.com>
Date:   Sat Sep 5 11:37:30 2020 +0900

    Squashed commit of the following:

    commit 9cb3fb9d980e3ee066083076f508c5ab1447176a
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Sat Sep 5 07:15:19 2020 +0900

        Move the link to the mention list to the menu

    commit b32dd87b43f4e09b8e2c437f1fb5d3ebd6221215
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Sat Sep 5 00:56:12 2020 +0900

        Change limited visibility icon

    commit 8db0d024119d1c2cef8de849f2501496a166a2dd
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Tue Sep 1 01:42:13 2020 +0900

        Fix to disallow getting the list of mentions in limited replies

    commit 490a9d65a59a3dd0d86e81f6780e879dc4313dff
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Fri Jul 24 11:36:24 2020 +0900

        Add column to list mentioned accounts of limited status

    commit 62a423ac2729c16f26fafe111f257bc373218df2
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Thu Jul 23 13:30:17 2020 +0900

        Fix visibility compatibility more

    commit a5cfa54b259054f41e89037f299fa928a2361818
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Mon Jul 20 05:39:49 2020 +0900

        Fix visibility compatibility

    commit 7900ca5650c77565b86ddc594a221dfa3b5321b4
    Author: noellabo <noel.yoshiba@gmail.com>
    Date:   Mon Jul 20 02:01:27 2020 +0900

        Add limited visibility icon to status

commit 66b83965ef068e9ee8c940249c68bcbde15731fe
Author: Eugen Rochko <eugen@zeonfederated.com>
Date:   Wed Aug 26 03:16:47 2020 +0200

    Add conversation-based forwarding for limited visibility statuses through bearcaps

commit 561abc65e0ace89318b3952047025b8d98515fbb
Author: Eugen Rochko <eugen@zeonfederated.com>
Date:   Sun Jul 19 02:05:16 2020 +0200

    Add REST API for managing and posting to circles

    Circles are the conceptual opposite of lists. A list is a subdivision
    of your follows, a circle is a subdivision of your followers. Posting
    to a circle means making content available to only some of your
    followers. Circles have been internally supported in Mastodon for
    the purposes of federation since #8950, this adds the REST API
    necessary for making use of them in Mastodon itsef.
2022-05-11 00:48:02 +09:00

645 lines
19 KiB
Ruby

# frozen_string_literal: true
# == Schema Information
#
# Table name: accounts
#
# id :bigint(8) not null, primary key
# username :string default(""), not null
# domain :string
# private_key :text
# public_key :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# note :text default(""), not null
# display_name :string default(""), not null
# uri :string default(""), not null
# url :string
# avatar_file_name :string
# avatar_content_type :string
# avatar_file_size :integer
# avatar_updated_at :datetime
# header_file_name :string
# header_content_type :string
# header_file_size :integer
# header_updated_at :datetime
# avatar_remote_url :string
# locked :boolean default(FALSE), not null
# header_remote_url :string default(""), not null
# last_webfingered_at :datetime
# inbox_url :string default(""), not null
# outbox_url :string default(""), not null
# shared_inbox_url :string default(""), not null
# followers_url :string default(""), not null
# protocol :integer default("ostatus"), not null
# memorial :boolean default(FALSE), not null
# moved_to_account_id :bigint(8)
# featured_collection_url :string
# fields :jsonb
# actor_type :string
# discoverable :boolean
# also_known_as :string is an Array
# silenced_at :datetime
# suspended_at :datetime
# trust_level :integer
# hide_collections :boolean
# avatar_storage_schema_version :integer
# header_storage_schema_version :integer
# devices_url :string
# suspension_origin :integer
# sensitized_at :datetime
#
class Account < ApplicationRecord
self.ignored_columns = %w(
subscription_expires_at
secret
remote_url
salmon_url
hub_url
)
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[a-z0-9]+)?)/i
URL_PREFIX_RE = /\Ahttp(s?):\/\/[^\/]+/
include AccountAssociations
include AccountAvatar
include AccountFinderConcern
include AccountHeader
include AccountInteractions
include Attachmentable
include Paginable
include AccountCounters
include DomainNormalizable
include DomainMaterializable
include AccountMerging
TRUST_LEVELS = {
untrusted: 0,
trusted: 1,
}.freeze
enum protocol: [:ostatus, :activitypub]
enum suspension_origin: [:local, :remote], _prefix: true
validates :username, presence: true
validates_with UniqueUsernameValidator, if: -> { will_save_change_to_username? }
# Remote user validations
validates :username, format: { with: /\A#{USERNAME_RE}\z/i }, if: -> { !local? && will_save_change_to_username? }
# Local user validations
validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: 30 }, if: -> { local? && will_save_change_to_username? && actor_type != 'Application' }
validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? }
validates :display_name, length: { maximum: 30 }, if: -> { local? && will_save_change_to_display_name? }
validates :note, note_length: { maximum: 500 }, if: -> { local? && will_save_change_to_note? }
validates :fields, length: { maximum: 4 }, if: -> { local? && will_save_change_to_fields? }
scope :remote, -> { where.not(domain: nil) }
scope :local, -> { where(domain: nil) }
scope :partitioned, -> { order(Arel.sql('row_number() over (partition by domain)')) }
scope :silenced, -> { where.not(silenced_at: nil) }
scope :suspended, -> { where.not(suspended_at: nil) }
scope :sensitized, -> { where.not(sensitized_at: nil) }
scope :without_suspended, -> { where(suspended_at: nil) }
scope :without_silenced, -> { where(silenced_at: nil) }
scope :without_instance_actor, -> { where.not(id: -99) }
scope :recent, -> { reorder(id: :desc) }
scope :bots, -> { where(actor_type: %w(Application Service)) }
scope :groups, -> { where(actor_type: 'Group') }
scope :without_groups, -> { where.not(actor_type: 'Group') }
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
scope :searchable, -> { without_suspended.where(moved_to_account_id: nil) }
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) }
scope :followable_by, ->(account) { joins(arel_table.join(Follow.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(Follow.arel_table[:target_account_id]).and(Follow.arel_table[:account_id].eq(account.id))).join_sources).where(Follow.arel_table[:id].eq(nil)).joins(arel_table.join(FollowRequest.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(FollowRequest.arel_table[:target_account_id]).and(FollowRequest.arel_table[:account_id].eq(account.id))).join_sources).where(FollowRequest.arel_table[:id].eq(nil)) }
scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) }
scope :by_recent_sign_in, -> { order(Arel.sql('(case when users.current_sign_in_at is null then 1 else 0 end) asc, users.current_sign_in_at desc, accounts.id desc')) }
scope :popular, -> { order('account_stats.followers_count desc') }
scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }
scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
scope :not_domain_blocked_by_account, ->(account) { where(arel_table[:domain].eq(nil).or(arel_table[:domain].not_in(account.excluded_from_timeline_domains))) }
delegate :email,
:unconfirmed_email,
:current_sign_in_ip,
:current_sign_in_at,
:confirmed?,
:approved?,
:pending?,
:disabled?,
:unconfirmed_or_pending?,
:role,
:admin?,
:moderator?,
:staff?,
:locale,
:hides_network?,
:shows_application?,
to: :user,
prefix: true,
allow_nil: true
delegate :chosen_languages, to: :user, prefix: false, allow_nil: true
update_index('accounts#account', :self)
def local?
domain.nil?
end
def moved?
moved_to_account_id.present?
end
def bot?
%w(Application Service).include? actor_type
end
def instance_actor?
id == -99
end
alias bot bot?
def bot=(val)
self.actor_type = ActiveModel::Type::Boolean.new.cast(val) ? 'Service' : 'Person'
end
def group?
actor_type == 'Group'
end
alias group group?
def acct
local? ? username : "#{username}@#{domain}"
end
def pretty_acct
local? ? username : "#{username}@#{Addressable::IDNA.to_unicode(domain)}"
end
def local_username_and_domain
"#{username}@#{Rails.configuration.x.local_domain}"
end
def local_followers_count
Follow.where(target_account_id: id).count
end
def to_webfinger_s
"acct:#{local_username_and_domain}"
end
def searchable?
!(suspended? || moved?)
end
def possibly_stale?
last_webfingered_at.nil? || last_webfingered_at <= 1.day.ago
end
def trust_level
self[:trust_level] || 0
end
def refresh!
ResolveAccountService.new.call(acct) unless local?
end
def silenced?
silenced_at.present?
end
def silence!(date = Time.now.utc)
update!(silenced_at: date)
end
def unsilence!
update!(silenced_at: nil)
end
def suspended?
suspended_at.present? && !instance_actor?
end
def suspended_permanently?
suspended? && deletion_request.nil?
end
def suspended_temporarily?
suspended? && deletion_request.present?
end
def suspend!(date: Time.now.utc, origin: :local, block_email: true)
transaction do
create_deletion_request!
update!(suspended_at: date, suspension_origin: origin)
create_canonical_email_block! if block_email
end
end
def unsuspend!
transaction do
deletion_request&.destroy!
update!(suspended_at: nil, suspension_origin: nil)
destroy_canonical_email_block!
end
end
def sensitized?
sensitized_at.present?
end
def sensitize!(date = Time.now.utc)
update!(sensitized_at: date)
end
def unsensitize!
update!(sensitized_at: nil)
end
def memorialize!
update!(memorial: true)
end
def sign?
true
end
def keypair
@keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
end
def tags_as_strings=(tag_names)
hashtags_map = Tag.find_or_create_by_names(tag_names).index_by(&:name)
# Remove hashtags that are to be deleted
tags.each do |tag|
if hashtags_map.key?(tag.name)
hashtags_map.delete(tag.name)
else
tags.delete(tag)
end
end
# Add hashtags that were so far missing
hashtags_map.each_value do |tag|
tags << tag
end
end
def also_known_as
self[:also_known_as] || []
end
def fields
(self[:fields] || []).map do |f|
Field.new(self, f)
rescue
nil
end.compact
end
def fields_attributes=(attributes)
fields = []
old_fields = self[:fields] || []
old_fields = [] if old_fields.is_a?(Hash)
if attributes.is_a?(Hash)
attributes.each_value do |attr|
next if attr[:name].blank?
previous = old_fields.find { |item| item['value'] == attr[:value] }
if previous && previous['verified_at'].present?
attr[:verified_at] = previous['verified_at']
end
fields << attr
end
end
self[:fields] = fields
end
DEFAULT_FIELDS_SIZE = 4
def build_fields
return if fields.size >= DEFAULT_FIELDS_SIZE
tmp = self[:fields] || []
tmp = [] if tmp.is_a?(Hash)
(DEFAULT_FIELDS_SIZE - tmp.size).times do
tmp << { name: '', value: '' }
end
self.fields = tmp
end
def save_with_optional_media!
save!
rescue ActiveRecord::RecordInvalid
self.avatar = nil
self.header = nil
save!
end
def hides_followers?
hide_collections? || user_hides_network?
end
def hides_following?
hide_collections? || user_hides_network?
end
def object_type
:person
end
def to_param
username
end
def excluded_from_timeline_account_ids
Rails.cache.fetch("exclude_account_ids_for:#{id}") { block_relationships.pluck(:target_account_id) + blocked_by_relationships.pluck(:account_id) + mute_relationships.pluck(:target_account_id) }
end
def excluded_from_timeline_domains
Rails.cache.fetch("exclude_domains_for:#{id}") { domain_blocks.pluck(:domain) }
end
def preferred_inbox_url
shared_inbox_url.presence || inbox_url
end
def synchronization_uri_prefix
return 'local' if local?
@synchronization_uri_prefix ||= "#{uri[URL_PREFIX_RE]}/"
end
def permitted_statuses(account)
if account.class.name == 'Account' && account.id == id
Status.include_expired.where(account_id: id)
else
statuses
end.permitted_for(self, account)
end
def index_text
ActionController::Base.helpers.strip_tags(note)
end
class Field < ActiveModelSerializers::Model
attributes :name, :value, :verified_at, :account
def initialize(account, attributes)
@original_field = attributes
string_limit = account.local? ? 255 : 2047
super(
account: account,
name: attributes['name'].strip[0, string_limit],
value: attributes['value'].strip[0, string_limit],
verified_at: attributes['verified_at']&.to_datetime,
)
end
def verified?
verified_at.present?
end
def value_for_verification
@value_for_verification ||= begin
if account.local?
value
else
ActionController::Base.helpers.strip_tags(value)
end
end
end
def verifiable?
value_for_verification.present? && value_for_verification.start_with?('http://', 'https://')
end
def mark_verified!
self.verified_at = Time.now.utc
@original_field['verified_at'] = verified_at
end
def to_h
{ name: name, value: value, verified_at: verified_at }
end
end
class << self
def readonly_attributes
super - %w(statuses_count following_count followers_count)
end
def inboxes
urls = reorder(nil).where(protocol: :activitypub).group(:preferred_inbox_url).pluck(Arel.sql("coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url) AS preferred_inbox_url"))
DeliveryFailureTracker.without_unavailable(urls)
end
def search_for(terms, limit = 10, group = false, offset = 0)
textsearch, query = generate_query_for_search(terms)
sql_where_group = <<-SQL if group
AND accounts.actor_type = 'Group'
SQL
sql = <<-SQL.squish
SELECT
accounts.*,
ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
FROM accounts
WHERE #{query} @@ #{textsearch}
AND accounts.suspended_at IS NULL
AND accounts.moved_to_account_id IS NULL
#{sql_where_group}
ORDER BY rank DESC
LIMIT :limit OFFSET :offset
SQL
records = find_by_sql([sql, { limit: limit, offset: offset }])
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
records
end
def advanced_search_for(terms, account, limit = 10, offset = 0, options = {})
textsearch, query = generate_query_for_search(terms)
sql_where_group = <<-SQL if options[:group]
AND accounts.actor_type = 'Group'
SQL
sql = if options[:following] || options[:followers]
sql_first_degree = first_degree(options)
<<-SQL.squish
#{sql_first_degree}
SELECT
accounts.*,
(count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
FROM accounts
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = :account_id)
WHERE accounts.id IN (SELECT * FROM first_degree)
AND #{query} @@ #{textsearch}
AND accounts.suspended_at IS NULL
AND accounts.moved_to_account_id IS NULL
#{sql_where_group}
GROUP BY accounts.id
ORDER BY rank DESC
LIMIT :limit OFFSET :offset
SQL
else
<<-SQL.squish
SELECT
accounts.*,
(count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
FROM accounts
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = :account_id) OR (accounts.id = f.target_account_id AND f.account_id = :account_id)
WHERE #{query} @@ #{textsearch}
AND accounts.suspended_at IS NULL
AND accounts.moved_to_account_id IS NULL
#{sql_where_group}
GROUP BY accounts.id
ORDER BY rank DESC
LIMIT :limit OFFSET :offset
SQL
end
records = find_by_sql([sql, { account_id: account.id, limit: limit, offset: offset }])
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
records
end
def from_text(text)
return [] if text.blank?
text.scan(MENTION_RE).map { |match| match.first.split('@', 2) }.uniq.filter_map do |(username, domain)|
domain = begin
if TagManager.instance.local_domain?(domain)
nil
else
TagManager.instance.normalize_domain(domain)
end
end
EntityCache.instance.mention(username, domain)
end
end
private
def first_degree(options)
if options[:following] && options[:followers]
<<-SQL
WITH first_degree AS (
SELECT target_account_id
FROM follows
WHERE account_id = :account_id
UNION ALL
SELECT account_id
FROM follows
WHERE target_account_id = :account_id
UNION ALL
SELECT :account_id
)
SQL
elsif options[:following]
<<-SQL
WITH first_degree AS (
SELECT target_account_id
FROM follows
WHERE account_id = :account_id
UNION ALL
SELECT :account_id
)
SQL
elsif options[:followers]
<<-SQL
WITH first_degree AS (
SELECT account_id
FROM follows
WHERE target_account_id = :account_id
UNION ALL
SELECT :account_id
)
SQL
end
end
def generate_query_for_search(terms)
terms = Arel.sql(connection.quote(terms.gsub(/['?\\:]/, ' ')))
textsearch = "(setweight(to_tsvector('simple', accounts.display_name), 'A') || setweight(to_tsvector('simple', accounts.username), 'B') || setweight(to_tsvector('simple', coalesce(accounts.domain, '')), 'C'))"
query = "to_tsquery('simple', ''' ' || #{terms} || ' ''' || ':*')"
[textsearch, query]
end
end
def emojis
@emojis ||= CustomEmoji.from_text(emojifiable_text, domain)
end
before_create :generate_keys
before_validation :prepare_contents, if: :local?
before_validation :prepare_username, on: :create
before_destroy :clean_feed_manager
private
def prepare_contents
display_name&.strip!
note&.strip!
end
def prepare_username
username&.squish!
end
def generate_keys
return unless local? && private_key.blank? && public_key.blank?
keypair = OpenSSL::PKey::RSA.new(2048)
self.private_key = keypair.to_pem
self.public_key = keypair.public_key.to_pem
end
def normalize_domain
return if local?
super
end
def emojifiable_text
[note, display_name, fields.map(&:name), fields.map(&:value)].join(' ')
end
def clean_feed_manager
FeedManager.instance.clean_feeds!(:home, [id])
end
def create_canonical_email_block!
return unless local? && user_email.present?
begin
CanonicalEmailBlock.create(reference_account: self, email: user_email)
rescue ActiveRecord::RecordNotUnique
# A canonical e-mail block may already exist for the same e-mail
end
end
def destroy_canonical_email_block!
return unless local?
CanonicalEmailBlock.where(reference_account: self).delete_all
end
end