diff options
Diffstat (limited to 'app/models')
-rw-r--r-- | app/models/account.rb | 105 | ||||
-rw-r--r-- | app/models/block.rb | 21 | ||||
-rw-r--r-- | app/models/concerns/streamable.rb | 6 | ||||
-rw-r--r-- | app/models/domain_block.rb | 2 | ||||
-rw-r--r-- | app/models/favourite.rb | 25 | ||||
-rw-r--r-- | app/models/feed.rb | 12 | ||||
-rw-r--r-- | app/models/follow.rb | 21 | ||||
-rw-r--r-- | app/models/follow_request.rb | 1 | ||||
-rw-r--r-- | app/models/import.rb | 14 | ||||
-rw-r--r-- | app/models/media_attachment.rb | 73 | ||||
-rw-r--r-- | app/models/mute.rb | 11 | ||||
-rw-r--r-- | app/models/report.rb | 10 | ||||
-rw-r--r-- | app/models/setting.rb | 1 | ||||
-rw-r--r-- | app/models/status.rb | 51 | ||||
-rw-r--r-- | app/models/stream_entry.rb | 11 | ||||
-rw-r--r-- | app/models/tag.rb | 22 | ||||
-rw-r--r-- | app/models/user.rb | 7 |
17 files changed, 256 insertions, 137 deletions
diff --git a/app/models/account.rb b/app/models/account.rb index c2a41c4c6..6968607a2 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -2,9 +2,8 @@ class Account < ApplicationRecord include Targetable - include PgSearch - MENTION_RE = /(?:^|[^\/\w])@([a-z0-9_]+(?:@[a-z0-9\.\-]+)?)/i + MENTION_RE = /(?:^|[^\/\w])@([a-z0-9_]+(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze # Local users @@ -46,15 +45,16 @@ class Account < ApplicationRecord has_many :block_relationships, class_name: 'Block', foreign_key: 'account_id', dependent: :destroy has_many :blocking, -> { order('blocks.id desc') }, through: :block_relationships, source: :target_account + # 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 + # Media has_many :media_attachments, dependent: :destroy # PuSH subscriptions has_many :subscriptions, dependent: :destroy - pg_search_scope :search_for, against: { username: 'A', domain: 'B' }, - using: { tsearch: { prefix: true } } - 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') } @@ -73,6 +73,10 @@ class Account < ApplicationRecord block_relationships.where(target_account: other_account).first_or_create!(target_account: other_account) end + def mute!(other_account) + mute_relationships.where(target_account: other_account).first_or_create!(target_account: other_account) + end + def unfollow!(other_account) follow = active_relationships.find_by(target_account: other_account) follow&.destroy @@ -83,6 +87,11 @@ class Account < ApplicationRecord block&.destroy end + def unmute!(other_account) + mute = mute_relationships.find_by(target_account: other_account) + mute&.destroy + end + def following?(other_account) following.include?(other_account) end @@ -91,10 +100,18 @@ class Account < ApplicationRecord blocking.include?(other_account) end + def muting?(other_account) + muting.include?(other_account) + end + def requested?(other_account) follow_requests.where(target_account: other_account).exists? end + def followers_domains + followers.reorder('').select('DISTINCT accounts.domain').map(&:domain) + end + def local? domain.nil? end @@ -127,14 +144,16 @@ class Account < ApplicationRecord save! rescue ActiveRecord::RecordInvalid self.avatar = nil + self.header = nil self[:avatar_remote_url] = '' + self[:header_remote_url] = '' save! end def avatar_remote_url=(url) parsed_url = URI.parse(url) - return if !%w(http https).include?(parsed_url.scheme) || self[:avatar_remote_url] == url + return if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty? || self[:avatar_remote_url] == url self.avatar = parsed_url self[:avatar_remote_url] = url @@ -142,6 +161,17 @@ class Account < ApplicationRecord Rails.logger.debug "Error fetching remote avatar: #{e}" end + 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 + def object_type :person end @@ -157,7 +187,7 @@ class Account < ApplicationRecord def find_remote!(username, domain) return if username.blank? - where(arel_table[:username].matches(username.gsub(/[%_]/, '\\\\\0'))).where(domain.nil? ? { domain: nil } : arel_table[:domain].matches(domain.gsub(/[%_]/, '\\\\\0'))).take! + where('lower(accounts.username) = ?', username.downcase).where(domain.nil? ? { domain: nil } : 'lower(accounts.domain) = ?', domain&.downcase).take! end def find_local(username) @@ -172,6 +202,63 @@ class Account < ApplicationRecord nil end + def triadic_closures(account, limit = 5) + sql = <<SQL + WITH first_degree AS ( + SELECT target_account_id + FROM follows + WHERE account_id = ? + ) + SELECT accounts.* + FROM follows + INNER JOIN accounts ON follows.target_account_id = accounts.id + WHERE account_id IN (SELECT * FROM first_degree) AND target_account_id NOT IN (SELECT * FROM first_degree) AND target_account_id <> ? + GROUP BY target_account_id, accounts.id + ORDER BY count(account_id) DESC + LIMIT ? +SQL + + Account.find_by_sql([sql, account.id, account.id, limit]) + end + + def search_for(terms, limit = 10) + 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 + ' || \' \'\'\' || \':*\')' + + sql = <<SQL + SELECT + accounts.*, + ts_rank_cd(#{textsearch}, #{query}, 32) AS rank + FROM accounts + WHERE #{query} @@ #{textsearch} + ORDER BY rank DESC + LIMIT ? +SQL + + Account.find_by_sql([sql, limit]) + end + + def advanced_search_for(terms, account, limit = 10) + 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 + ' || \' \'\'\' || \':*\')' + + sql = <<SQL + 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 ? +SQL + + Account.find_by_sql([sql, account.id, account.id, limit]) + end + def following_map(target_account_ids, account_id) follow_mapping(Follow.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) end @@ -184,6 +271,10 @@ class Account < ApplicationRecord follow_mapping(Block.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) end + 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 + def requested_map(target_account_ids, account_id) follow_mapping(FollowRequest.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id) end diff --git a/app/models/block.rb b/app/models/block.rb index c2067c5b8..9c55703c9 100644 --- a/app/models/block.rb +++ b/app/models/block.rb @@ -2,31 +2,10 @@ class Block < ApplicationRecord include Paginable - include Streamable belongs_to :account belongs_to :target_account, class_name: 'Account' validates :account, :target_account, presence: true validates :account_id, uniqueness: { scope: :target_account_id } - - def verb - destroyed? ? :unblock : :block - end - - def target - target_account - end - - def object_type - :person - end - - def hidden? - true - end - - def title - destroyed? ? "#{account.acct} is no longer blocking #{target_account.acct}" : "#{account.acct} blocked #{target_account.acct}" - end end diff --git a/app/models/concerns/streamable.rb b/app/models/concerns/streamable.rb index 58c15cfbc..910736dac 100644 --- a/app/models/concerns/streamable.rb +++ b/app/models/concerns/streamable.rb @@ -30,8 +30,12 @@ module Streamable false end + def needs_stream_entry? + account.local? + end + after_create do - account.stream_entries.create!(activity: self, hidden: hidden?) if account.local? + account.stream_entries.create!(activity: self, hidden: hidden?) if needs_stream_entry? end end end diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index b4606da60..3548ccd69 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -6,6 +6,6 @@ class DomainBlock < ApplicationRecord validates :domain, presence: true, uniqueness: true def self.blocked?(domain) - where(domain: domain).exists? + where(domain: domain, severity: :suspend).exists? end end diff --git a/app/models/favourite.rb b/app/models/favourite.rb index 3f3616dce..41d06e734 100644 --- a/app/models/favourite.rb +++ b/app/models/favourite.rb @@ -2,37 +2,14 @@ class Favourite < ApplicationRecord include Paginable - include Streamable belongs_to :account, inverse_of: :favourites - belongs_to :status, inverse_of: :favourites + belongs_to :status, inverse_of: :favourites, counter_cache: true has_one :notification, as: :activity, dependent: :destroy validates :status_id, uniqueness: { scope: :account_id } - def verb - :favorite - end - - def title - "#{account.acct} favourited a status by #{status.account.acct}" - end - - delegate :object_type, to: :target - - def thread - status - end - - def target - thread - end - - def hidden? - status.private_visibility? - end - before_validation do self.status = status.reblog if status.reblog? end diff --git a/app/models/feed.rb b/app/models/feed.rb index 5e1905e15..3cbc160a0 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -10,17 +10,9 @@ class Feed max_id = '+inf' if max_id.blank? since_id = '-inf' if since_id.blank? unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:last).map(&:to_i) + status_map = Status.where(id: unhydrated).cache_ids.map { |s| [s.id, s] }.to_h - # If we're after most recent items and none are there, we need to precompute the feed - if unhydrated.empty? && max_id == '+inf' && since_id == '-inf' - RegenerationWorker.perform_async(@account.id, @type) - @statuses = Status.send("as_#{@type}_timeline", @account).cache_ids.paginate_by_max_id(limit, nil, nil) - else - status_map = Status.where(id: unhydrated).cache_ids.map { |s| [s.id, s] }.to_h - @statuses = unhydrated.map { |id| status_map[id] }.compact - end - - @statuses + unhydrated.map { |id| status_map[id] }.compact end private diff --git a/app/models/follow.rb b/app/models/follow.rb index f83490caa..8bfe8b2f6 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -2,29 +2,12 @@ class Follow < ApplicationRecord include Paginable - include Streamable - belongs_to :account - belongs_to :target_account, class_name: 'Account' + belongs_to :account, counter_cache: :following_count + belongs_to :target_account, class_name: 'Account', counter_cache: :followers_count has_one :notification, as: :activity, dependent: :destroy validates :account, :target_account, presence: true validates :account_id, uniqueness: { scope: :target_account_id } - - def verb - destroyed? ? :unfollow : :follow - end - - def target - target_account - end - - def object_type - :person - end - - def title - destroyed? ? "#{account.acct} is no longer following #{target_account.acct}" : "#{account.acct} started following #{target_account.acct}" - end end diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb index 936ad0691..4224ab15d 100644 --- a/app/models/follow_request.rb +++ b/app/models/follow_request.rb @@ -14,6 +14,7 @@ class FollowRequest < ApplicationRecord def authorize! account.follow!(target_account) MergeWorker.perform_async(target_account.id, account.id) + destroy! end diff --git a/app/models/import.rb b/app/models/import.rb new file mode 100644 index 000000000..5384986d8 --- /dev/null +++ b/app/models/import.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class Import < ApplicationRecord + self.inheritance_column = false + + enum type: [:following, :blocking] + + belongs_to :account + + FILE_TYPES = ['text/plain', 'text/csv'].freeze + + has_attached_file :data, url: '/system/:hash.:extension', hash_secret: ENV['PAPERCLIP_SECRET'] + validates_attachment_content_type :data, content_type: FILE_TYPES +end diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 6925f9b0d..818190214 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -1,15 +1,32 @@ # frozen_string_literal: true class MediaAttachment < ApplicationRecord + self.inheritance_column = nil + + enum type: [:image, :gifv, :video] + IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze + IMAGE_STYLES = { original: '1280x1280>', small: '400x400>' }.freeze + VIDEO_STYLES = { + small: { + convert_options: { + output: { + vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease', + }, + }, + format: 'png', + time: 0, + }, + }.freeze + belongs_to :account, inverse_of: :media_attachments belongs_to :status, inverse_of: :media_attachments has_attached_file :file, - styles: -> (f) { file_styles f }, - processors: -> (f) { f.video? ? [:transcoder] : [:thumbnail] }, + styles: ->(f) { file_styles f }, + processors: ->(f) { file_processors f }, convert_options: { all: '-quality 90 -strip' } validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES validates_attachment_size :file, less_than: 8.megabytes @@ -27,45 +44,49 @@ class MediaAttachment < ApplicationRecord self.file = URI.parse(url) end - def image? - IMAGE_MIME_TYPES.include? file_content_type - end - - def video? - VIDEO_MIME_TYPES.include? file_content_type - end - - def type - image? ? 'image' : 'video' - end - def to_param shortcode end before_create :set_shortcode + before_post_process :set_type class << self private def file_styles(f) - if f.instance.image? + if f.instance.file_content_type == 'image/gif' { - original: '1280x1280>', - small: '400x400>', - } - else - { - small: { + small: IMAGE_STYLES[:small], + original: { + format: 'mp4', convert_options: { output: { - vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease', + 'movflags' => 'faststart', + 'pix_fmt' => 'yuv420p', + 'vf' => 'scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'', + 'vsync' => 'cfr', + 'b:v' => '1300K', + 'maxrate' => '500K', + 'crf' => 6, }, }, - format: 'png', - time: 1, }, } + elsif IMAGE_MIME_TYPES.include? f.instance.file_content_type + IMAGE_STYLES + else + VIDEO_STYLES + end + end + + def file_processors(f) + if f.file_content_type == 'image/gif' + [:gif_transcoder] + elsif VIDEO_MIME_TYPES.include? f.file_content_type + [:video_transcoder] + else + [:thumbnail] end end end @@ -80,4 +101,8 @@ class MediaAttachment < ApplicationRecord break if MediaAttachment.find_by(shortcode: shortcode).nil? end end + + def set_type + self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : :image + end end diff --git a/app/models/mute.rb b/app/models/mute.rb new file mode 100644 index 000000000..a5b334c85 --- /dev/null +++ b/app/models/mute.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Mute < ApplicationRecord + include Paginable + + belongs_to :account + belongs_to :target_account, class_name: 'Account' + + validates :account, :target_account, presence: true + validates :account_id, uniqueness: { scope: :target_account_id } +end diff --git a/app/models/report.rb b/app/models/report.rb new file mode 100644 index 000000000..fd8e46aac --- /dev/null +++ b/app/models/report.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Report < ApplicationRecord + belongs_to :account + belongs_to :target_account, class_name: 'Account' + belongs_to :action_taken_by_account, class_name: 'Account' + + scope :unresolved, -> { where(action_taken: false) } + scope :resolved, -> { where(action_taken: true) } +end diff --git a/app/models/setting.rb b/app/models/setting.rb index 3796253d4..31e1ee198 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -2,7 +2,6 @@ class Setting < RailsSettings::Base source Rails.root.join('config/settings.yml') - namespace Rails.env def to_param var diff --git a/app/models/status.rb b/app/models/status.rb index d2be72308..daf128572 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -6,15 +6,15 @@ class Status < ApplicationRecord include Streamable include Cacheable - enum visibility: [:public, :unlisted, :private], _suffix: :visibility + enum visibility: [:public, :unlisted, :private, :direct], _suffix: :visibility belongs_to :application, class_name: 'Doorkeeper::Application' - belongs_to :account, inverse_of: :statuses + belongs_to :account, inverse_of: :statuses, counter_cache: true belongs_to :in_reply_to_account, foreign_key: 'in_reply_to_account_id', class_name: 'Account' belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies - belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs + belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, counter_cache: :reblogs_count has_many :favourites, inverse_of: :status, dependent: :destroy has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy @@ -37,6 +37,9 @@ class Status < ApplicationRecord scope :remote, -> { where.not(uri: nil) } scope :local, -> { where(uri: nil) } + scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') } + scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') } + cache_associated :account, :application, :media_attachments, :tags, :stream_entry, mentions: :account, reblog: [:account, :application, :stream_entry, :tags, :media_attachments, mentions: :account], thread: :account def reply? @@ -72,11 +75,17 @@ class Status < ApplicationRecord end def hidden? - private_visibility? + private_visibility? || direct_visibility? end def permitted?(other_account = nil) - private_visibility? ? (account.id == other_account&.id || other_account&.following?(account)) : other_account.nil? || !account.blocking?(other_account) + if direct_visibility? + account.id == other_account&.id || mentions.where(account: other_account).exists? + elsif private_visibility? + account.id == other_account&.id || other_account&.following?(account) || mentions.where(account: other_account).exists? + else + other_account.nil? || !account.blocking?(other_account) + end end def ancestors(account = nil) @@ -105,8 +114,8 @@ class Status < ApplicationRecord def as_public_timeline(account = nil, local_only = false) query = joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id') .where(visibility: :public) - .where('(statuses.reply = false OR statuses.in_reply_to_account_id = statuses.account_id)') - .where('statuses.reblog_of_id IS NULL') + .without_replies + .without_reblogs query = query.where('accounts.domain IS NULL') if local_only @@ -117,7 +126,7 @@ class Status < ApplicationRecord query = tag.statuses .joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id') .where(visibility: :public) - .where('statuses.reblog_of_id IS NULL') + .without_reblogs query = query.where('accounts.domain IS NULL') if local_only @@ -149,21 +158,27 @@ class Status < ApplicationRecord end def permitted_for(target_account, account) - if account&.id == target_account.id || account&.following?(target_account) - where('1 = 1') - elsif !account.nil? && target_account.blocking?(account) + return where.not(visibility: [:private, :direct]) if account.nil? + + if target_account.blocking?(account) # get rid of blocked peeps where('1 = 0') - else - where.not(visibility: :private) + elsif account.id == target_account.id # author can see own stuff + where('1 = 1') + elsif account.following?(target_account) # followers can see followers-only stuff, but also things they are mentioned in + joins('LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = ' + account.id.to_s) + .where('statuses.visibility != ? OR mentions.id IS NOT NULL', Status.visibilities[:direct]) + else # non-followers can see everything that isn't private/direct, but can see stuff they are mentioned in + joins('LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = ' + account.id.to_s) + .where('statuses.visibility NOT IN (?) OR mentions.id IS NOT NULL', [Status.visibilities[:direct], Status.visibilities[:private]]) end end private def filter_timeline(query, account) - blocked = Block.where(account: account).pluck(:target_account_id) + Block.where(target_account: account).pluck(:account_id) - query = query.where('statuses.account_id NOT IN (?)', blocked) unless blocked.empty? - query = query.where('accounts.silenced = TRUE') if account.silenced? + blocked = Block.where(account: account).pluck(:target_account_id) + Block.where(target_account: account).pluck(:account_id) + Mute.where(account: account).pluck(:target_account_id) + query = query.where('statuses.account_id NOT IN (?)', blocked) unless blocked.empty? # Only give us statuses from people we haven't blocked, or muted, or that have blocked us + query = query.where('accounts.silenced = TRUE') if account.silenced? # and if we're hellbanned, only people who are also hellbanned query end @@ -173,7 +188,7 @@ class Status < ApplicationRecord end before_validation do - text.strip! + text&.strip! spoiler_text&.strip! self.reply = !(in_reply_to_id.nil? && thread.nil?) unless reply @@ -185,6 +200,6 @@ class Status < ApplicationRecord private def filter_from_context?(status, account) - account&.blocking?(status.account_id) || !status.permitted?(account) + account&.blocking?(status.account_id) || account&.muting?(status.account_id) || (status.account.silenced? && !account&.following?(status.account_id)) || !status.permitted?(account) end end diff --git a/app/models/stream_entry.rb b/app/models/stream_entry.rb index fcc691bef..ae7ae446e 100644 --- a/app/models/stream_entry.rb +++ b/app/models/stream_entry.rb @@ -6,16 +6,13 @@ class StreamEntry < ApplicationRecord belongs_to :account, inverse_of: :stream_entries belongs_to :activity, polymorphic: true - belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id' - belongs_to :follow, foreign_type: 'Follow', foreign_key: 'activity_id' - belongs_to :favourite, foreign_type: 'Favourite', foreign_key: 'activity_id' - belongs_to :block, foreign_type: 'Block', foreign_key: 'activity_id' + belongs_to :status, foreign_type: 'Status', foreign_key: 'activity_id', inverse_of: :stream_entry validates :account, :activity, presence: true STATUS_INCLUDES = [:account, :stream_entry, :media_attachments, :tags, mentions: :account, reblog: [:stream_entry, :account, mentions: :account], thread: [:stream_entry, :account]].freeze - scope :with_includes, -> { includes(:account, status: STATUS_INCLUDES, favourite: [:account, :stream_entry, status: STATUS_INCLUDES], follow: [:target_account, :stream_entry]) } + scope :with_includes, -> { includes(:account, status: STATUS_INCLUDES) } def object_type if orphaned? @@ -30,7 +27,7 @@ class StreamEntry < ApplicationRecord end def targeted? - [:follow, :unfollow, :block, :unblock, :share, :favorite].include? verb + [:follow, :request_friend, :authorize, :reject, :unfollow, :block, :unblock, :share, :favorite].include? verb end def target @@ -58,7 +55,7 @@ class StreamEntry < ApplicationRecord end def activity - !new_record? ? send(activity_type.downcase) : super + !new_record? ? send(activity_type.underscore) || super : super end private diff --git a/app/models/tag.rb b/app/models/tag.rb index 77a73cce8..15625ca43 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -3,11 +3,31 @@ class Tag < ApplicationRecord has_and_belongs_to_many :statuses - HASHTAG_RE = /(?:^|[^\/\w])#([[:word:]_]*[[:alpha:]_][[:word:]_]*)/i + HASHTAG_RE = /(?:^|[^\/\)\w])#([[:word:]_]*[[:alpha:]_][[:word:]_]*)/i validates :name, presence: true, uniqueness: true def to_param name end + + class << self + def search_for(terms, limit = 5) + terms = Arel.sql(connection.quote(terms.gsub(/['?\\:]/, ' '))) + textsearch = 'to_tsvector(\'simple\', tags.name)' + query = 'to_tsquery(\'simple\', \'\'\' \' || ' + terms + ' || \' \'\'\' || \':*\')' + + sql = <<SQL + SELECT + tags.*, + ts_rank_cd(#{textsearch}, #{query}) AS rank + FROM tags + WHERE #{query} @@ #{textsearch} + ORDER BY rank DESC + LIMIT ? +SQL + + Tag.find_by_sql([sql, limit]) + end + end end diff --git a/app/models/user.rb b/app/models/user.rb index 08aac2679..bf2916d90 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -14,9 +14,10 @@ class User < ApplicationRecord validates :locale, inclusion: I18n.available_locales.map(&:to_s), unless: 'locale.nil?' validates :email, email: true - scope :prolific, -> { joins('inner join statuses on statuses.account_id = users.account_id').select('users.*, count(statuses.id) as statuses_count').group('users.id').order('statuses_count desc') } - scope :recent, -> { order('id desc') } - scope :admins, -> { where(admin: true) } + scope :prolific, -> { joins('inner join statuses on statuses.account_id = users.account_id').select('users.*, count(statuses.id) as statuses_count').group('users.id').order('statuses_count desc') } + scope :recent, -> { order('id desc') } + scope :admins, -> { where(admin: true) } + scope :confirmed, -> { where.not(confirmed_at: nil) } def send_devise_notification(notification, *args) devise_mailer.send(notification, self, *args).deliver_later |