diff options
Diffstat (limited to 'app/models')
-rw-r--r-- | app/models/concerns/remotable.rb | 2 | ||||
-rw-r--r-- | app/models/status.rb | 43 | ||||
-rw-r--r-- | app/models/tag.rb | 16 | ||||
-rw-r--r-- | app/models/trending_tags.rb | 61 |
4 files changed, 116 insertions, 6 deletions
diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb index 20ddbca53..c17f04776 100644 --- a/app/models/concerns/remotable.rb +++ b/app/models/concerns/remotable.rb @@ -41,7 +41,7 @@ module Remotable rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e Rails.logger.debug "Error fetching remote #{attachment_name}: #{e}" nil - rescue Paperclip::Error => e + rescue Paperclip::Error, Mastodon::DimensionsValidationError => e Rails.logger.debug "Error processing remote #{attachment_name}: #{e}" nil end diff --git a/app/models/status.rb b/app/models/status.rb index 0b3a7c0aa..c6d6453df 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -195,12 +195,45 @@ class Status < ApplicationRecord where(account: [account] + account.following).where(visibility: [:public, :unlisted, :private]) end - def as_direct_timeline(account) - query = joins("LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = #{account.id}") - .where("mentions.account_id = #{account.id} OR statuses.account_id = #{account.id}") - .where(visibility: [:direct]) + def as_direct_timeline(account, limit = 20, max_id = nil, since_id = nil, cache_ids = false) + # direct timeline is mix of direct message from_me and to_me. + # 2 querys are executed with pagination. + # constant expression using arel_table is required for partial index + + # _from_me part does not require any timeline filters + query_from_me = where(account_id: account.id) + .where(Status.arel_table[:visibility].eq(3)) + .limit(limit) + .order('statuses.id DESC') + + # _to_me part requires mute and block filter. + # FIXME: may we check mutes.hide_notifications? + query_to_me = Status + .joins(:mentions) + .merge(Mention.where(account_id: account.id)) + .where(Status.arel_table[:visibility].eq(3)) + .limit(limit) + .order('mentions.status_id DESC') + .not_excluded_by_account(account) + + if max_id.present? + query_from_me = query_from_me.where('statuses.id < ?', max_id) + query_to_me = query_to_me.where('mentions.status_id < ?', max_id) + end + + if since_id.present? + query_from_me = query_from_me.where('statuses.id > ?', since_id) + query_to_me = query_to_me.where('mentions.status_id > ?', since_id) + end - apply_timeline_filters(query, account, false) + if cache_ids + # returns array of cache_ids object that have id and updated_at + (query_from_me.cache_ids.to_a + query_to_me.cache_ids.to_a).uniq(&:id).sort_by(&:id).reverse.take(limit) + else + # returns ActiveRecord.Relation + items = (query_from_me.select(:id).to_a + query_to_me.select(:id).to_a).uniq(&:id).sort_by(&:id).reverse.take(limit) + Status.where(id: items.map(&:id)) + end end def as_public_timeline(account = nil, local_only = false) diff --git a/app/models/tag.rb b/app/models/tag.rb index 8b1b02412..4f31f796e 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -21,6 +21,22 @@ class Tag < ApplicationRecord name end + def history + days = [] + + 7.times do |i| + day = i.days.ago.beginning_of_day.to_i + + days << { + day: day.to_s, + uses: Redis.current.get("activity:tags:#{id}:#{day}") || '0', + accounts: Redis.current.pfcount("activity:tags:#{id}:#{day}:accounts").to_s, + } + end + + days + end + class << self def search_for(term, limit = 5) pattern = sanitize_sql_like(term.strip) + '%' diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb new file mode 100644 index 000000000..eedd92644 --- /dev/null +++ b/app/models/trending_tags.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +class TrendingTags + KEY = 'trending_tags' + HALF_LIFE = 1.day.to_i + MAX_ITEMS = 500 + EXPIRE_HISTORY_AFTER = 7.days.seconds + + class << self + def record_use!(tag, account, at_time = Time.now.utc) + return if disallowed_hashtags.include?(tag.name) || account.silenced? + + increment_vote!(tag.id, at_time) + increment_historical_use!(tag.id, at_time) + increment_unique_use!(tag.id, account.id, at_time) + end + + def get(limit) + tag_ids = redis.zrevrange(KEY, 0, limit).map(&:to_i) + tags = Tag.where(id: tag_ids).to_a.map { |tag| [tag.id, tag] }.to_h + tag_ids.map { |tag_id| tags[tag_id] }.compact + end + + private + + def increment_vote!(tag_id, at_time) + redis.zincrby(KEY, (2**((at_time.to_i - epoch) / HALF_LIFE)).to_f, tag_id.to_s) + redis.zremrangebyrank(KEY, 0, -MAX_ITEMS) if rand < (2.to_f / MAX_ITEMS) + end + + def increment_historical_use!(tag_id, at_time) + key = "activity:tags:#{tag_id}:#{at_time.beginning_of_day.to_i}" + redis.incrby(key, 1) + redis.expire(key, EXPIRE_HISTORY_AFTER) + end + + def increment_unique_use!(tag_id, account_id, at_time) + key = "activity:tags:#{tag_id}:#{at_time.beginning_of_day.to_i}:accounts" + redis.pfadd(key, account_id) + redis.expire(key, EXPIRE_HISTORY_AFTER) + end + + # The epoch needs to be 2.5 years in the future if the half-life is one day + # While dynamic, it will always be the same within one year + def epoch + @epoch ||= Date.new(Date.current.year + 2.5, 10, 1).to_datetime.to_i + end + + def disallowed_hashtags + return @disallowed_hashtags if defined?(@disallowed_hashtags) + + @disallowed_hashtags = Setting.disallowed_hashtags.nil? ? [] : Setting.disallowed_hashtags + @disallowed_hashtags = @disallowed_hashtags.split(' ') if @disallowed_hashtags.is_a? String + @disallowed_hashtags = @disallowed_hashtags.map(&:downcase) + end + + def redis + Redis.current + end + end +end |