2016-11-16 02:56:29 +11:00
# frozen_string_literal: true
2016-08-18 01:56:23 +10:00
class Account < ApplicationRecord
2016-03-25 12:13:30 +11:00
include Targetable
2017-03-06 04:08:19 +11:00
MENTION_RE = / (?:^|[^ \/ \ w])@([a-z0-9_]+(?:@[a-z0-9 \ . \ -]+[a-z0-9]+)?) /i
2016-09-30 05:28:21 +10:00
IMAGE_MIME_TYPES = [ 'image/jpeg' , 'image/png' , 'image/gif' ] . freeze
2016-09-13 02:22:43 +10:00
2016-02-23 02:00:20 +11:00
# Local users
has_one :user , inverse_of : :account
2016-09-25 23:26:56 +10:00
validates :username , presence : true , format : { with : / \ A[a-z0-9_]+ \ z /i , message : 'only letters, numbers and underscores' } , uniqueness : { scope : :domain , case_sensitive : false } , length : { maximum : 30 } , if : 'local?'
2016-09-10 17:43:45 +10:00
validates :username , presence : true , uniqueness : { scope : :domain , case_sensitive : true } , unless : 'local?'
2016-02-23 02:00:20 +11:00
2016-02-28 10:02:59 +11:00
# Avatar upload
2017-04-11 08:38:58 +10:00
has_attached_file :avatar , styles : - > ( f ) { avatar_styles ( f ) } , convert_options : { all : '-quality 80 -strip' }
2016-09-13 02:22:43 +10:00
validates_attachment_content_type :avatar , content_type : IMAGE_MIME_TYPES
2016-09-10 17:43:45 +10:00
validates_attachment_size :avatar , less_than : 2 . megabytes
2016-02-28 10:02:59 +11:00
2016-03-13 06:47:22 +11:00
# Header upload
2017-04-11 08:38:58 +10:00
has_attached_file :header , styles : - > ( f ) { header_styles ( f ) } , convert_options : { all : '-quality 80 -strip' }
2016-09-13 02:22:43 +10:00
validates_attachment_content_type :header , content_type : IMAGE_MIME_TYPES
2016-09-10 17:43:45 +10:00
validates_attachment_size :header , less_than : 2 . megabytes
2016-03-13 06:47:22 +11:00
2017-04-19 07:15:44 +10:00
before_post_process :set_file_extensions
2016-03-16 21:18:09 +11:00
# Local user profile validations
validates :display_name , length : { maximum : 30 } , if : 'local?'
2016-11-07 11:14:12 +11:00
validates :note , length : { maximum : 160 } , if : 'local?'
2016-03-16 21:18:09 +11:00
2016-02-23 02:00:20 +11:00
# Timelines
2016-10-09 23:48:43 +11:00
has_many :stream_entries , inverse_of : :account , dependent : :destroy
has_many :statuses , inverse_of : :account , dependent : :destroy
has_many :favourites , inverse_of : :account , dependent : :destroy
has_many :mentions , inverse_of : :account , dependent : :destroy
2016-11-22 00:59:13 +11:00
has_many :notifications , inverse_of : :account , dependent : :destroy
2016-02-21 08:53:20 +11:00
2016-02-23 02:00:20 +11:00
# Follow relations
2016-12-23 09:03:57 +11:00
has_many :follow_requests , dependent : :destroy
2016-02-23 02:00:20 +11:00
has_many :active_relationships , class_name : 'Follow' , foreign_key : 'account_id' , dependent : :destroy
has_many :passive_relationships , class_name : 'Follow' , foreign_key : 'target_account_id' , dependent : :destroy
2016-11-05 05:12:59 +11:00
has_many :following , - > { order ( 'follows.id desc' ) } , through : :active_relationships , source : :target_account
has_many :followers , - > { order ( 'follows.id desc' ) } , through : :passive_relationships , source : :account
2016-10-04 02:11:54 +11:00
# Block relationships
has_many :block_relationships , class_name : 'Block' , foreign_key : 'account_id' , dependent : :destroy
2016-11-05 05:12:59 +11:00
has_many :blocking , - > { order ( 'blocks.id desc' ) } , through : :block_relationships , source : :target_account
2016-02-23 02:00:20 +11:00
2017-02-06 12:51:56 +11:00
# Mute relationships
has_many :mute_relationships , class_name : 'Mute' , foreign_key : 'account_id' , dependent : :destroy
has_many :muting , - > { order ( 'mutes.id desc' ) } , through : :mute_relationships , source : :target_account
2016-11-28 23:36:47 +11:00
# Media
2016-09-06 01:46:36 +10:00
has_many :media_attachments , dependent : :destroy
2016-11-28 23:36:47 +11:00
# PuSH subscriptions
has_many :subscriptions , dependent : :destroy
2017-04-19 03:36:18 +10:00
# Report relationships
has_many :reports
has_many :targeted_reports , class_name : 'Report' , foreign_key : :target_account_id
2016-09-20 08:39:03 +10:00
scope :remote , - > { where . not ( domain : nil ) }
scope :local , - > { where ( domain : nil ) }
scope :without_followers , - > { where ( '(select count(f.id) from follows as f where f.target_account_id = accounts.id) = 0' ) }
scope :with_followers , - > { where ( '(select count(f.id) from follows as f where f.target_account_id = accounts.id) > 0' ) }
2016-12-03 00:14:49 +11:00
scope :expiring , - > ( time ) { where ( subscription_expires_at : nil ) . or ( where ( 'subscription_expires_at < ?' , time ) ) . remote . with_followers }
2016-12-07 04:22:59 +11:00
scope :silenced , - > { where ( silenced : true ) }
scope :suspended , - > { where ( suspended : true ) }
2017-01-08 12:55:40 +11:00
scope :recent , - > { reorder ( id : :desc ) }
scope :alphabetic , - > { order ( domain : :asc , username : :asc ) }
2017-04-19 05:09:07 +10:00
scope :by_domain_accounts , - > { group ( :domain ) . select ( :domain , 'COUNT(*) AS accounts_count' ) . order ( 'accounts_count desc' ) }
2016-10-17 03:57:54 +11:00
2016-02-23 02:00:20 +11:00
def follow! ( other_account )
2016-09-30 05:28:21 +10:00
active_relationships . where ( target_account : other_account ) . first_or_create! ( target_account : other_account )
2016-02-23 02:00:20 +11:00
end
2016-10-04 02:16:58 +11:00
def block! ( other_account )
block_relationships . where ( target_account : other_account ) . first_or_create! ( target_account : other_account )
end
2017-02-06 12:51:56 +11:00
def mute! ( other_account )
mute_relationships . where ( target_account : other_account ) . first_or_create! ( target_account : other_account )
end
2016-02-23 02:00:20 +11:00
def unfollow! ( other_account )
2016-09-30 05:28:21 +10:00
follow = active_relationships . find_by ( target_account : other_account )
2016-11-25 22:35:21 +11:00
follow & . destroy
2016-02-23 02:00:20 +11:00
end
2016-10-04 02:16:58 +11:00
def unblock! ( other_account )
block = block_relationships . find_by ( target_account : other_account )
2016-11-25 22:35:21 +11:00
block & . destroy
2016-10-04 02:16:58 +11:00
end
2017-02-06 12:51:56 +11:00
def unmute! ( other_account )
mute = mute_relationships . find_by ( target_account : other_account )
mute & . destroy
end
2016-02-23 02:00:20 +11:00
def following? ( other_account )
following . include? ( other_account )
end
2016-10-04 02:11:54 +11:00
def blocking? ( other_account )
blocking . include? ( other_account )
end
2017-02-06 12:51:56 +11:00
def muting? ( other_account )
muting . include? ( other_account )
end
2016-12-23 09:07:46 +11:00
def requested? ( other_account )
follow_requests . where ( target_account : other_account ) . exists?
end
2016-02-23 02:00:20 +11:00
def local?
2016-09-30 05:28:21 +10:00
domain . nil?
2016-02-23 02:00:20 +11:00
end
2016-02-23 04:10:30 +11:00
def acct
2016-09-30 05:28:21 +10:00
local? ? username : " #{ username } @ #{ domain } "
2016-02-23 04:10:30 +11:00
end
2017-04-11 06:58:06 +10:00
def local_username_and_domain
" #{ username } @ #{ Rails . configuration . x . local_domain } "
end
def to_webfinger_s
" acct: #{ local_username_and_domain } "
end
2016-02-23 04:10:30 +11:00
def subscribed?
2017-01-10 00:00:55 +11:00
! subscription_expires_at . blank?
2016-02-23 04:10:30 +11:00
end
2016-03-07 22:42:33 +11:00
def favourited? ( status )
2017-04-08 04:18:30 +10:00
status . proper . favourites . where ( account : self ) . count . positive?
2016-03-07 22:42:33 +11:00
end
def reblogged? ( status )
2017-04-08 04:18:30 +10:00
status . proper . reblogs . where ( account : self ) . count . positive?
2016-03-07 22:42:33 +11:00
end
2016-02-23 02:00:20 +11:00
def keypair
2016-09-30 05:28:21 +10:00
private_key . nil? ? OpenSSL :: PKey :: RSA . new ( public_key ) : OpenSSL :: PKey :: RSA . new ( private_key )
2016-02-23 02:00:20 +11:00
end
2016-02-21 08:53:20 +11:00
def subscription ( webhook_url )
2016-09-30 05:28:21 +10:00
OStatus2 :: Subscription . new ( remote_url , secret : secret , lease_seconds : 86_400 * 30 , webhook : webhook_url , hub : hub_url )
2016-02-21 08:53:20 +11:00
end
2016-02-27 01:28:08 +11:00
2016-12-03 00:14:49 +11:00
def save_with_optional_avatar!
save!
2017-01-19 12:14:57 +11:00
rescue ActiveRecord :: RecordInvalid
2017-01-19 19:37:07 +11:00
self . avatar = nil
2017-03-19 08:51:20 +11:00
self . header = nil
2017-01-19 19:37:07 +11:00
self [ :avatar_remote_url ] = ''
2017-03-19 08:51:20 +11:00
self [ :header_remote_url ] = ''
2017-01-19 12:14:57 +11:00
save!
2016-02-29 00:33:13 +11:00
end
2017-04-11 08:38:58 +10:00
def avatar_original_url
avatar . url ( :original )
end
def avatar_static_url
avatar_content_type == 'image/gif' ? avatar . url ( :static ) : avatar_original_url
end
def header_original_url
header . url ( :original )
end
def header_static_url
header_content_type == 'image/gif' ? header . url ( :static ) : header_original_url
end
2016-02-28 10:51:05 +11:00
def avatar_remote_url = ( url )
2016-11-27 01:45:35 +11:00
parsed_url = URI . parse ( url )
2017-02-23 05:55:14 +11:00
return if ! %w( http https ) . include? ( parsed_url . scheme ) || parsed_url . host . empty? || self [ :avatar_remote_url ] == url
2016-11-27 01:45:35 +11:00
self . avatar = parsed_url
2016-03-23 07:05:23 +11:00
self [ :avatar_remote_url ] = url
2016-11-16 02:56:29 +11:00
rescue OpenURI :: HTTPError = > e
Rails . logger . debug " Error fetching remote avatar: #{ e } "
2016-02-28 10:51:05 +11:00
end
2017-03-19 08:51:20 +11:00
def header_remote_url = ( url )
parsed_url = URI . parse ( url )
return if ! %w( http https ) . include? ( parsed_url . scheme ) || parsed_url . host . empty? || self [ :header_remote_url ] == url
self . header = parsed_url
self [ :header_remote_url ] = url
rescue OpenURI :: HTTPError = > e
Rails . logger . debug " Error fetching remote header: #{ e } "
end
2016-03-25 12:13:30 +11:00
def object_type
:person
end
2016-03-01 05:42:08 +11:00
def to_param
2016-09-30 05:28:21 +10:00
username
2016-03-01 05:42:08 +11:00
end
2016-11-10 03:48:44 +11:00
class << self
def find_local! ( username )
find_remote! ( username , nil )
end
2016-09-05 05:06:04 +10:00
2016-11-10 03:48:44 +11:00
def find_remote! ( username , domain )
2017-01-13 13:24:41 +11:00
return if username . blank?
2017-03-22 13:21:38 +11:00
where ( 'lower(accounts.username) = ?' , username . downcase ) . where ( domain . nil? ? { domain : nil } : 'lower(accounts.domain) = ?' , domain & . downcase ) . take!
2016-11-10 03:48:44 +11:00
end
2016-03-17 04:29:52 +11:00
2016-11-10 03:48:44 +11:00
def find_local ( username )
find_local! ( username )
rescue ActiveRecord :: RecordNotFound
nil
end
2016-03-19 09:23:19 +11:00
2016-11-10 03:48:44 +11:00
def find_remote ( username , domain )
find_remote! ( username , domain )
rescue ActiveRecord :: RecordNotFound
nil
end
2016-09-05 05:06:04 +10:00
2017-03-17 06:10:51 +11:00
def triadic_closures ( account , limit = 5 )
2017-04-09 22:45:01 +10:00
sql = <<-SQL.squish
2017-03-17 06:10:51 +11:00
WITH first_degree AS (
SELECT target_account_id
FROM follows
2017-04-18 09:21:55 +10:00
WHERE account_id = :account_id
2017-03-17 06:10:51 +11:00
)
SELECT accounts . *
FROM follows
INNER JOIN accounts ON follows . target_account_id = accounts . id
2017-04-18 09:21:55 +10:00
WHERE account_id IN ( SELECT * FROM first_degree ) AND target_account_id NOT IN ( SELECT * FROM first_degree ) AND target_account_id < > :account_id
2017-03-17 06:10:51 +11:00
GROUP BY target_account_id , accounts . id
ORDER BY count ( account_id ) DESC
2017-04-18 09:21:55 +10:00
LIMIT :limit
2017-04-09 22:45:01 +10:00
SQL
2017-03-17 06:10:51 +11:00
2017-04-18 09:21:55 +10:00
find_by_sql (
[ sql , { account_id : account . id , limit : limit } ]
)
2017-03-17 06:10:51 +11:00
end
2017-03-18 06:47:38 +11:00
def search_for ( terms , limit = 10 )
2017-03-23 03:36:34 +11:00
terms = Arel . sql ( connection . quote ( terms . gsub ( / ['? \\ :] / , ' ' ) ) )
2017-03-22 13:21:38 +11:00
textsearch = '(setweight(to_tsvector(\'simple\', accounts.display_name), \'A\') || setweight(to_tsvector(\'simple\', accounts.username), \'B\') || setweight(to_tsvector(\'simple\', coalesce(accounts.domain, \'\')), \'C\'))'
2017-03-23 03:36:34 +11:00
query = 'to_tsquery(\'simple\', \'\'\' \' || ' + terms + ' || \' \'\'\' || \':*\')'
2017-03-18 06:47:38 +11:00
2017-04-09 22:45:01 +10:00
sql = <<-SQL.squish
2017-03-18 06:47:38 +11:00
SELECT
accounts . * ,
ts_rank_cd ( #{textsearch}, #{query}, 32) AS rank
FROM accounts
WHERE #{query} @@ #{textsearch}
ORDER BY rank DESC
LIMIT ?
2017-04-09 22:45:01 +10:00
SQL
2017-03-18 06:47:38 +11:00
2017-03-23 03:36:34 +11:00
Account . find_by_sql ( [ sql , limit ] )
2017-03-18 06:47:38 +11:00
end
def advanced_search_for ( terms , account , limit = 10 )
2017-03-23 03:36:34 +11:00
terms = Arel . sql ( connection . quote ( terms . gsub ( / ['? \\ :] / , ' ' ) ) )
2017-03-22 13:21:38 +11:00
textsearch = '(setweight(to_tsvector(\'simple\', accounts.display_name), \'A\') || setweight(to_tsvector(\'simple\', accounts.username), \'B\') || setweight(to_tsvector(\'simple\', coalesce(accounts.domain, \'\')), \'C\'))'
2017-03-23 03:36:34 +11:00
query = 'to_tsquery(\'simple\', \'\'\' \' || ' + terms + ' || \' \'\'\' || \':*\')'
2017-03-18 06:47:38 +11:00
2017-04-09 22:45:01 +10:00
sql = <<-SQL.squish
2017-03-18 06:47:38 +11:00
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 = ?) OR ( accounts . id = f . target_account_id AND f . account_id = ?)
WHERE #{query} @@ #{textsearch}
GROUP BY accounts . id
ORDER BY rank DESC
LIMIT ?
2017-04-09 22:45:01 +10:00
SQL
2017-03-18 06:47:38 +11:00
2017-03-23 03:36:34 +11:00
Account . find_by_sql ( [ sql , account . id , account . id , limit ] )
2017-03-18 06:47:38 +11:00
end
2016-11-10 03:48:44 +11:00
def following_map ( target_account_ids , account_id )
2017-01-08 13:09:00 +11:00
follow_mapping ( Follow . where ( target_account_id : target_account_ids , account_id : account_id ) , :target_account_id )
2016-11-10 03:48:44 +11:00
end
2016-09-22 06:07:18 +10:00
2016-11-10 03:48:44 +11:00
def followed_by_map ( target_account_ids , account_id )
2017-01-08 13:09:00 +11:00
follow_mapping ( Follow . where ( account_id : target_account_ids , target_account_id : account_id ) , :account_id )
2016-11-10 03:48:44 +11:00
end
2016-09-22 06:07:18 +10:00
2016-11-10 03:48:44 +11:00
def blocking_map ( target_account_ids , account_id )
2017-01-08 13:09:00 +11:00
follow_mapping ( Block . where ( target_account_id : target_account_ids , account_id : account_id ) , :target_account_id )
2016-11-10 03:48:44 +11:00
end
2016-12-23 09:03:57 +11:00
2017-02-06 12:51:56 +11:00
def muting_map ( target_account_ids , account_id )
follow_mapping ( Mute . where ( target_account_id : target_account_ids , account_id : account_id ) , :target_account_id )
end
2016-12-23 09:03:57 +11:00
def requested_map ( target_account_ids , account_id )
2017-01-08 13:09:00 +11:00
follow_mapping ( FollowRequest . where ( target_account_id : target_account_ids , account_id : account_id ) , :target_account_id )
end
2017-01-10 00:00:55 +11:00
2017-01-09 05:12:54 +11:00
private
def follow_mapping ( query , field )
query . pluck ( field ) . inject ( { } ) { | mapping , id | mapping [ id ] = true ; mapping }
2016-12-23 09:03:57 +11:00
end
2017-04-11 08:38:58 +10:00
def avatar_styles ( file )
styles = { original : '120x120#' }
styles [ :static ] = { format : 'png' } if file . content_type == 'image/gif'
styles
end
def header_styles ( file )
styles = { original : '700x335#' }
styles [ :static ] = { format : 'png' } if file . content_type == 'image/gif'
styles
end
2016-10-04 02:16:58 +11:00
end
2016-02-27 01:28:08 +11:00
before_create do
if local?
2016-02-29 07:22:56 +11:00
keypair = OpenSSL :: PKey :: RSA . new ( Rails . env . test? ? 1024 : 2048 )
2016-02-27 01:28:08 +11:00
self . private_key = keypair . to_pem
self . public_key = keypair . public_key . to_pem
end
end
2017-04-19 07:15:44 +10:00
private
def set_file_extensions
unless avatar . blank?
extension = Paperclip :: Interpolations . content_type_extension ( avatar , :original )
basename = Paperclip :: Interpolations . basename ( avatar , :original )
avatar . instance_write :file_name , [ basename , extension ] . delete_if ( & :empty? ) . join ( '.' )
end
unless header . blank?
extension = Paperclip :: Interpolations . content_type_extension ( header , :original )
basename = Paperclip :: Interpolations . basename ( header , :original )
header . instance_write :file_name , [ basename , extension ] . delete_if ( & :empty? ) . join ( '.' )
end
end
2016-02-21 08:53:20 +11:00
end