From 47ef4a6c7a74072daff8b23c4af3e300bb75ba1a Mon Sep 17 00:00:00 2001 From: ThibG Date: Tue, 25 Jun 2019 14:45:14 +0200 Subject: Apply filters to poll options (#11174) * Apply filters to poll options in WebUI Fixes #11128 * Apply filters to poll options server-side * Add poll options to searchable text --- app/lib/feed_manager.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'app/lib/feed_manager.rb') diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index d77cdb3a3..ed3ce6112 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -220,7 +220,8 @@ class FeedManager status = status.reblog if status.reblog? !combined_regex.match(Formatter.instance.plaintext(status)).nil? || - (status.spoiler_text.present? && !combined_regex.match(status.spoiler_text).nil?) + (status.spoiler_text.present? && !combined_regex.match(status.spoiler_text).nil?) || + (status.preloadable_poll && !combined_regex.match(status.preloadable_poll.options.join("\n\n")).nil?) end # Adds a status to an account's feed, returning true if a status was -- cgit From ca17bae904783dfb1f4899a533d28a79da0c6fe9 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Tue, 25 Jun 2019 22:56:32 +0200 Subject: Use a redis-cached feed for the DM timeline --- .../api/v1/timelines/direct_controller.rb | 10 +++--- app/lib/feed_manager.rb | 40 ++++++++++++++++++++++ app/models/direct_feed.rb | 31 +++++++++++++++++ app/services/batched_remove_status_service.rb | 4 +-- app/services/fan_out_on_write_service.rb | 7 ++-- app/services/precompute_feed_service.rb | 1 + app/services/remove_status_service.rb | 4 +-- app/workers/feed_insert_worker.rb | 11 +++++- app/workers/scheduler/feed_cleanup_scheduler.rb | 5 +++ 9 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 app/models/direct_feed.rb (limited to 'app/lib/feed_manager.rb') diff --git a/app/controllers/api/v1/timelines/direct_controller.rb b/app/controllers/api/v1/timelines/direct_controller.rb index d8a76d153..6e98e9cac 100644 --- a/app/controllers/api/v1/timelines/direct_controller.rb +++ b/app/controllers/api/v1/timelines/direct_controller.rb @@ -27,16 +27,18 @@ class Api::V1::Timelines::DirectController < Api::BaseController end def direct_timeline_statuses - # this query requires built in pagination. - Status.as_direct_timeline( - current_account, + account_direct_feed.get( limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id], - true # returns array of cache_ids object + params[:min_id] ) end + def account_direct_feed + DirectFeed.new(current_account) + end + def insert_pagination_headers set_pagination_headers(next_path, prev_path) end diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index ddcbdf6da..59767cdfe 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -22,6 +22,8 @@ class FeedManager filter_from_home?(status, receiver_id) elsif timeline_type == :mentions filter_from_mentions?(status, receiver_id) + elsif timeline_type == :direct + filter_from_direct?(status, receiver_id) else false end @@ -59,6 +61,18 @@ class FeedManager true end + def push_to_direct(account, status) + return false unless add_to_feed(:direct, account.id, status) + trim(:direct, account.id) + PushUpdateWorker.perform_async(account.id, status.id, "timeline:direct:#{account.id}") + true + end + + def unpush_from_direct(account, status) + return false unless remove_from_feed(:direct, account.id, status) + redis.publish("timeline:direct:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) + end + def trim(type, account_id) timeline_key = key(type, account_id) reblog_key = key(type, account_id, 'reblogs') @@ -142,6 +156,27 @@ class FeedManager end end + def populate_direct_feed(account) + added = 0 + limit = FeedManager::MAX_ITEMS / 2 + max_id = nil + + loop do + statuses = Status.as_direct_timeline(account, limit, max_id) + + break if statuses.empty? + + statuses.each do |status| + next if filter_from_direct?(status, account) + added += 1 if add_to_feed(:direct, account.id, status) + end + + break unless added.zero? + + max_id = statuses.last.id + end + end + private def push_update_required?(timeline_id) @@ -199,6 +234,11 @@ class FeedManager should_filter end + def filter_from_direct?(status, receiver_id) + return false if receiver_id == status.account_id + filter_from_mentions?(status, receiver_id) + end + def phrase_filtered?(status, receiver_id, context) active_filters = Rails.cache.fetch("filters:#{receiver_id}") { CustomFilter.where(account_id: receiver_id).active_irreversible.to_a }.to_a diff --git a/app/models/direct_feed.rb b/app/models/direct_feed.rb new file mode 100644 index 000000000..c0b8a0a35 --- /dev/null +++ b/app/models/direct_feed.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class DirectFeed < Feed + include Redisable + + def initialize(account) + @type = :direct + @id = account.id + @account = account + end + + def get(limit, max_id = nil, since_id = nil, min_id = nil) + unless redis.exists("account:#{@account.id}:regeneration") + statuses = super + return statuses unless statuses.empty? + end + from_database(limit, max_id, since_id, min_id) + end + + private + + def from_database(limit, max_id, since_id, min_id) + loop do + statuses = Status.as_direct_timeline(@account, limit, max_id, since_id, min_id) + return statuses if statuses.empty? + max_id = statuses.last.id + statuses = statuses.reject { |status| FeedManager.instance.filter?(:direct, status, @account.id) } + return statuses unless statuses.empty? + end + end +end diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index 02f7076f7..2fe009c91 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -107,9 +107,9 @@ class BatchedRemoveStatusService < BaseService payload = @json_payloads[status.id] redis.pipelined do @mentions[status.id].each do |mention| - redis.publish("timeline:direct:#{mention.account.id}", payload) if mention.account.local? + FeedManager.instance.unpush_from_direct(mention.account, status) if mention.account.local? end - redis.publish("timeline:direct:#{status.account.id}", payload) if status.account.local? + FeedManager.instance.unpush_from_direct(status.account, status) if status.account.local? end end diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index b66dc342e..cf433d8a6 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -37,6 +37,7 @@ class FanOutOnWriteService < BaseService def deliver_to_self(status) Rails.logger.debug "Delivering status #{status.id} to author" FeedManager.instance.push_to_home(status.account, status) + FeedManager.instance.push_to_direct(status.account, status) if status.direct_visibility? end def deliver_to_followers(status) @@ -98,11 +99,9 @@ class FanOutOnWriteService < BaseService def deliver_to_direct_timelines(status) Rails.logger.debug "Delivering status #{status.id} to direct timelines" - status.mentions.includes(:account).each do |mention| - Redis.current.publish("timeline:direct:#{mention.account.id}", @payload) if mention.account.local? + FeedInsertWorker.push_bulk(status.mentions.includes(:account).map(&:account).select { |mentioned_account| mentioned_account.local? }) do |account| + [status.id, account.id, :direct] end - - Redis.current.publish("timeline:direct:#{status.account.id}", @payload) if status.account.local? end def deliver_to_own_conversation(status) diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb index 076dedaca..029c2f6e5 100644 --- a/app/services/precompute_feed_service.rb +++ b/app/services/precompute_feed_service.rb @@ -3,6 +3,7 @@ class PrecomputeFeedService < BaseService def call(account) FeedManager.instance.populate_feed(account) + FeedManager.instance.populate_direct_feed(account) ensure Redis.current.del("account:#{account.id}:regeneration") end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 98972fc70..9d5d0fc14 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -48,6 +48,7 @@ class RemoveStatusService < BaseService def remove_from_self FeedManager.instance.unpush_from_home(@account, @status) + FeedManager.instance.unpush_from_direct(@account, @status) if @status.direct_visibility? end def remove_from_followers @@ -159,9 +160,8 @@ class RemoveStatusService < BaseService def remove_from_direct @mentions.each do |mention| - Redis.current.publish("timeline:direct:#{mention.account.id}", @payload) if mention.account.local? + FeedManager.instance.unpush_from_direct(mention.account, @status) if mention.account.local? end - Redis.current.publish("timeline:direct:#{@account.id}", @payload) if @account.local? end def lock_options diff --git a/app/workers/feed_insert_worker.rb b/app/workers/feed_insert_worker.rb index 1ae3c877b..546f5c0c2 100644 --- a/app/workers/feed_insert_worker.rb +++ b/app/workers/feed_insert_worker.rb @@ -13,6 +13,8 @@ class FeedInsertWorker when :list @list = List.find(id) @follower = @list.account + when :direct + @account = Account.find(id) end check_and_insert @@ -29,7 +31,12 @@ class FeedInsertWorker def feed_filtered? # Note: Lists are a variation of home, so the filtering rules # of home apply to both - FeedManager.instance.filter?(:home, @status, @follower.id) + case @type + when :home, :list + FeedManager.instance.filter?(:home, @status, @follower.id) + when :direct + FeedManager.instance.filter?(:direct, @status, @account.id) + end end def perform_push @@ -38,6 +45,8 @@ class FeedInsertWorker FeedManager.instance.push_to_home(@follower, @status) when :list FeedManager.instance.push_to_list(@list, @status) + when :direct + FeedManager.instance.push_to_direct(@account, @status) end end end diff --git a/app/workers/scheduler/feed_cleanup_scheduler.rb b/app/workers/scheduler/feed_cleanup_scheduler.rb index bf5e20757..4933f1753 100644 --- a/app/workers/scheduler/feed_cleanup_scheduler.rb +++ b/app/workers/scheduler/feed_cleanup_scheduler.rb @@ -9,6 +9,7 @@ class Scheduler::FeedCleanupScheduler def perform clean_home_feeds! clean_list_feeds! + clean_direct_feeds! end private @@ -21,6 +22,10 @@ class Scheduler::FeedCleanupScheduler clean_feeds!(inactive_list_ids, :list) end + def clean_direct_feeds! + clean_feeds!(inactive_account_ids, :direct) + end + def clean_feeds!(ids, type) reblogged_id_sets = {} -- cgit From ff789a751a1c730e4d808410411196b76caff39c Mon Sep 17 00:00:00 2001 From: ThibG Date: Tue, 30 Jul 2019 13:18:23 +0200 Subject: Fix boosting & unboosting preventing a boost from appearing in the TL (#11405) * Fix boosting & unboosting preventing a boost from appearing in the TL * Add tests * Avoids side effects when aggregate_reblogs isn't true --- app/lib/feed_manager.rb | 14 +++++++++----- spec/lib/feed_manager_spec.rb | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) (limited to 'app/lib/feed_manager.rb') diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index ed3ce6112..482b64867 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -35,7 +35,7 @@ class FeedManager end def unpush_from_home(account, status) - return false unless remove_from_feed(:home, account.id, status) + return false unless remove_from_feed(:home, account.id, status, account.user&.aggregates_reblogs?) redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) true end @@ -53,7 +53,7 @@ class FeedManager end def unpush_from_list(list, status) - return false unless remove_from_feed(:list, list.id, status) + return false unless remove_from_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?) redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s)) true end @@ -105,7 +105,7 @@ class FeedManager oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0 from_account.statuses.select('id, reblog_of_id').where('id > ?', oldest_home_score).reorder(nil).find_each do |status| - remove_from_feed(:home, into_account.id, status) + remove_from_feed(:home, into_account.id, status, into_account.user&.aggregates_reblogs?) end end @@ -275,10 +275,11 @@ class FeedManager # with reblogs, and returning true if a status was removed. As with # `add_to_feed`, this does not trigger push updates, so callers must # do so if appropriate. - def remove_from_feed(timeline_type, account_id, status) + def remove_from_feed(timeline_type, account_id, status, aggregate_reblogs = true) timeline_key = key(timeline_type, account_id) + reblog_key = key(timeline_type, account_id, 'reblogs') - if status.reblog? + if status.reblog? && (aggregate_reblogs.nil? || aggregate_reblogs) # 1. If the reblogging status is not in the feed, stop. status_rank = redis.zrevrank(timeline_key, status.id) return false if status_rank.nil? @@ -287,6 +288,7 @@ class FeedManager reblog_set_key = key(timeline_type, account_id, "reblogs:#{status.reblog_of_id}") redis.srem(reblog_set_key, status.id) + redis.zrem(reblog_key, status.reblog_of_id) # 3. Re-insert another reblog or original into the feed if one # remains in the set. We could pick a random element, but this # set should generally be small, and it seems ideal to show the @@ -294,12 +296,14 @@ class FeedManager other_reblog = redis.smembers(reblog_set_key).map(&:to_i).min redis.zadd(timeline_key, other_reblog, other_reblog) if other_reblog + redis.zadd(reblog_key, other_reblog, status.reblog_of_id) if other_reblog # 4. Remove the reblogging status from the feed (as normal) # (outside conditional) else # If the original is getting deleted, no use for reblog references redis.del(key(timeline_type, account_id, "reblogs:#{status.id}")) + redis.srem(reblog_key, status.id) end redis.zrem(timeline_key, status.id) diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index 9bdb675e1..b996997b1 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -247,6 +247,23 @@ RSpec.describe FeedManager do expect(FeedManager.instance.push_to_home(account, reblogs.last)).to be false end + it 'saves a new reblog of a recently-reblogged status when previous reblog has been deleted' do + account = Fabricate(:account) + reblogged = Fabricate(:status) + old_reblog = Fabricate(:status, reblog: reblogged) + + # The first reblog should be accepted + expect(FeedManager.instance.push_to_home(account, old_reblog)).to be true + + # The first reblog should be successfully removed + expect(FeedManager.instance.unpush_from_home(account, old_reblog)).to be true + + reblog = Fabricate(:status, reblog: reblogged) + + # The second reblog should be accepted + expect(FeedManager.instance.push_to_home(account, reblog)).to be true + end + it 'does not save a new reblog of a multiply-reblogged-then-unreblogged status' do account = Fabricate(:account) reblogged = Fabricate(:status) -- cgit From e46e9c9a8ec047825712e1ffa80d179facfb78e0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 31 Jul 2019 09:23:30 +0200 Subject: Fix delete regression (#11450) Regression from ff789a751a1c730e4d808410411196b76caff39c --- app/lib/feed_manager.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/lib/feed_manager.rb') diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 482b64867..ca3d890a8 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -303,7 +303,7 @@ class FeedManager else # If the original is getting deleted, no use for reblog references redis.del(key(timeline_type, account_id, "reblogs:#{status.id}")) - redis.srem(reblog_key, status.id) + redis.zrem(reblog_key, status.id) end redis.zrem(timeline_key, status.id) -- cgit From 70ddef2654a931827ce5e4323e3042365f6078f2 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 2 Sep 2019 18:11:13 +0200 Subject: Change trending hashtags to not disappear instantly after midnight (#11712) --- app/controllers/admin/tags_controller.rb | 2 +- app/lib/feed_manager.rb | 2 +- app/models/tag.rb | 4 +- app/models/trending_tags.rb | 102 +++++++++++++++------ app/workers/scheduler/trending_tags_scheduler.rb | 11 +++ config/sidekiq.yml | 3 + db/migrate/20190901035623_add_max_score_to_tags.rb | 6 ++ .../20190901040524_remove_score_from_tags.rb | 12 +++ db/schema.rb | 6 +- spec/models/trending_tags_spec.rb | 68 ++++++++++++++ 10 files changed, 179 insertions(+), 37 deletions(-) create mode 100644 app/workers/scheduler/trending_tags_scheduler.rb create mode 100644 db/migrate/20190901035623_add_max_score_to_tags.rb create mode 100644 db/post_migrate/20190901040524_remove_score_from_tags.rb create mode 100644 spec/models/trending_tags_spec.rb (limited to 'app/lib/feed_manager.rb') diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb index 25d9b7d3d..8bd4e5f8b 100644 --- a/app/controllers/admin/tags_controller.rb +++ b/app/controllers/admin/tags_controller.rb @@ -57,7 +57,7 @@ module Admin scope = scope.unreviewed if filter_params[:review] == 'unreviewed' scope = scope.reviewed.order(reviewed_at: :desc) if filter_params[:review] == 'reviewed' scope = scope.pending_review.order(requested_review_at: :desc) if filter_params[:review] == 'pending_review' - scope.order(score: :desc) + scope.order(max_score: :desc) end def filter_params diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index ca3d890a8..871ec5c19 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -63,7 +63,7 @@ class FeedManager reblog_key = key(type, account_id, 'reblogs') # Remove any items past the MAX_ITEMS'th entry in our feed - redis.zremrangebyrank(timeline_key, '0', (-(FeedManager::MAX_ITEMS + 1)).to_s) + redis.zremrangebyrank(timeline_key, 0, -(FeedManager::MAX_ITEMS + 1)) # Get the score of the REBLOG_FALLOFF'th item in our feed, and stop # tracking anything after it for deduplication purposes. diff --git a/app/models/tag.rb b/app/models/tag.rb index 945e3a3c6..135e0a030 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -7,14 +7,14 @@ # name :string default(""), not null # created_at :datetime not null # updated_at :datetime not null -# score :integer # usable :boolean # trendable :boolean # listable :boolean # reviewed_at :datetime # requested_review_at :datetime # last_status_at :datetime -# last_trend_at :datetime +# max_score :float +# max_score_at :datetime # class Tag < ApplicationRecord diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb index e4ce988c1..e1b92b175 100644 --- a/app/models/trending_tags.rb +++ b/app/models/trending_tags.rb @@ -7,6 +7,8 @@ class TrendingTags THRESHOLD = 5 LIMIT = 10 REVIEW_THRESHOLD = 3 + MAX_SCORE_COOLDOWN = 3.days.freeze + MAX_SCORE_HALFLIFE = 6.hours.freeze class << self include Redisable @@ -16,14 +18,75 @@ class TrendingTags increment_historical_use!(tag.id, at_time) increment_unique_use!(tag.id, account.id, at_time) - increment_vote!(tag, at_time) + increment_use!(tag.id, at_time) tag.update(last_status_at: Time.now.utc) if tag.last_status_at.nil? || tag.last_status_at < 12.hours.ago - tag.update(last_trend_at: Time.now.utc) if trending?(tag) && (tag.last_trend_at.nil? || tag.last_trend_at < 12.hours.ago) + end + + def update!(at_time = Time.now.utc) + tag_ids = redis.smembers("#{KEY}:used:#{at_time.beginning_of_day.to_i}") + redis.zrange(KEY, 0, -1) + tags = Tag.where(id: tag_ids.uniq) + + # First pass to calculate scores and update the set + + tags.each do |tag| + expected = redis.pfcount("activity:tags:#{tag.id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts").to_f + expected = 1.0 if expected.zero? + observed = redis.pfcount("activity:tags:#{tag.id}:#{at_time.beginning_of_day.to_i}:accounts").to_f + max_time = tag.max_score_at + max_score = tag.max_score + max_score = 0 if max_time.nil? || max_time < (at_time - MAX_SCORE_COOLDOWN) + + score = begin + if expected > observed || observed < THRESHOLD + 0 + else + ((observed - expected)**2) / expected + end + end + + if score > max_score + max_score = score + max_time = at_time + + # Not interested in triggering any callbacks for this + tag.update_columns(max_score: max_score, max_score_at: max_time) + end + + decaying_score = max_score * (0.5**((at_time.to_f - max_time.to_f) / MAX_SCORE_HALFLIFE.to_f)) + + if decaying_score.zero? + redis.zrem(KEY, tag.id) + else + redis.zadd(KEY, decaying_score, tag.id) + end + end + + users_for_review = User.staff.includes(:account).to_a.select(&:allows_trending_tag_emails?) + + # Second pass to notify about previously unreviewed trends + + tags.each do |tag| + current_rank = redis.zrevrank(KEY, tag.id) + needs_review_notification = tag.requires_review? && !tag.requested_review? + rank_passes_threshold = current_rank.present? && current_rank <= REVIEW_THRESHOLD + + next unless !tag.trendable? && rank_passes_threshold && needs_review_notification + + tag.touch(:requested_review_at) + + users_for_review.each do |user| + AdminMailer.new_trending_tag(user.account, tag).deliver_later! + end + end + + # Trim older items + + redis.zremrangebyrank(KEY, 0, -(LIMIT + 1)) end def get(limit, filtered: true) - tag_ids = redis.zrevrange("#{KEY}:#{Time.now.utc.beginning_of_day.to_i}", 0, LIMIT - 1).map(&:to_i) + tag_ids = redis.zrevrange(KEY, 0, LIMIT - 1).map(&:to_i) tags = Tag.where(id: tag_ids) tags = tags.where(trendable: true) if filtered @@ -33,8 +96,8 @@ class TrendingTags end def trending?(tag) - rank = redis.zrevrank("#{KEY}:#{Time.now.utc.beginning_of_day.to_i}", tag.id) - rank.present? && rank <= LIMIT + rank = redis.zrevrank(KEY, tag.id) + rank.present? && rank < LIMIT end private @@ -51,31 +114,10 @@ class TrendingTags redis.expire(key, EXPIRE_HISTORY_AFTER) end - def increment_vote!(tag, at_time) - key = "#{KEY}:#{at_time.beginning_of_day.to_i}" - expected = redis.pfcount("activity:tags:#{tag.id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts").to_f - expected = 1.0 if expected.zero? - observed = redis.pfcount("activity:tags:#{tag.id}:#{at_time.beginning_of_day.to_i}:accounts").to_f - - if expected > observed || observed < THRESHOLD - redis.zrem(key, tag.id) - else - score = ((observed - expected)**2) / expected - old_rank = redis.zrevrank(key, tag.id) - - redis.zadd(key, score, tag.id) - request_review!(tag) if (old_rank.nil? || old_rank > REVIEW_THRESHOLD) && redis.zrevrank(key, tag.id) <= REVIEW_THRESHOLD && !tag.trendable? && tag.requires_review? && !tag.requested_review? - end - - redis.expire(key, EXPIRE_TRENDS_AFTER) - end - - def request_review!(tag) - return unless Setting.trends - - tag.touch(:requested_review_at) - - User.staff.includes(:account).find_each { |u| AdminMailer.new_trending_tag(u.account, tag).deliver_later! if u.allows_trending_tag_emails? } + def increment_use!(tag_id, at_time) + key = "#{KEY}:used:#{at_time.beginning_of_day.to_i}" + redis.sadd(key, tag_id) + redis.expire(key, EXPIRE_HISTORY_AFTER) end end end diff --git a/app/workers/scheduler/trending_tags_scheduler.rb b/app/workers/scheduler/trending_tags_scheduler.rb new file mode 100644 index 000000000..77f0d5747 --- /dev/null +++ b/app/workers/scheduler/trending_tags_scheduler.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Scheduler::TrendingTagsScheduler + include Sidekiq::Worker + + sidekiq_options unique: :until_executed, retry: 0 + + def perform + TrendingTags.update! if Setting.trends + end +end diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 6ebe450b0..5de25de23 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -9,6 +9,9 @@ scheduled_statuses_scheduler: every: '5m' class: Scheduler::ScheduledStatusesScheduler + trending_tags_scheduler: + every: '5m' + class: Scheduler::TrendingTagsScheduler media_cleanup_scheduler: cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *' class: Scheduler::MediaCleanupScheduler diff --git a/db/migrate/20190901035623_add_max_score_to_tags.rb b/db/migrate/20190901035623_add_max_score_to_tags.rb new file mode 100644 index 000000000..f936e9871 --- /dev/null +++ b/db/migrate/20190901035623_add_max_score_to_tags.rb @@ -0,0 +1,6 @@ +class AddMaxScoreToTags < ActiveRecord::Migration[5.2] + def change + add_column :tags, :max_score, :float + add_column :tags, :max_score_at, :datetime + end +end diff --git a/db/post_migrate/20190901040524_remove_score_from_tags.rb b/db/post_migrate/20190901040524_remove_score_from_tags.rb new file mode 100644 index 000000000..a1112700b --- /dev/null +++ b/db/post_migrate/20190901040524_remove_score_from_tags.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class RemoveScoreFromTags < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def change + safety_assured do + remove_column :tags, :score, :int + remove_column :tags, :last_trend_at, :datetime + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 482bca367..5576f70bf 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_08_23_221802) do +ActiveRecord::Schema.define(version: 2019_09_01_040524) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -664,14 +664,14 @@ ActiveRecord::Schema.define(version: 2019_08_23_221802) do t.string "name", default: "", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.integer "score" t.boolean "usable" t.boolean "trendable" t.boolean "listable" t.datetime "reviewed_at" t.datetime "requested_review_at" t.datetime "last_status_at" - t.datetime "last_trend_at" + t.float "max_score" + t.datetime "max_score_at" t.index "lower((name)::text)", name: "index_tags_on_name_lower", unique: true end diff --git a/spec/models/trending_tags_spec.rb b/spec/models/trending_tags_spec.rb new file mode 100644 index 000000000..b6122c994 --- /dev/null +++ b/spec/models/trending_tags_spec.rb @@ -0,0 +1,68 @@ +require 'rails_helper' + +RSpec.describe TrendingTags do + describe '.record_use!' do + pending + end + + describe '.update!' do + let!(:at_time) { Time.now.utc } + let!(:tag1) { Fabricate(:tag, name: 'Catstodon') } + let!(:tag2) { Fabricate(:tag, name: 'DogsOfMastodon') } + let!(:tag3) { Fabricate(:tag, name: 'OCs') } + + before do + allow(Redis.current).to receive(:pfcount) do |key| + case key + when "activity:tags:#{tag1.id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts" + 2 + when "activity:tags:#{tag1.id}:#{at_time.beginning_of_day.to_i}:accounts" + 16 + when "activity:tags:#{tag2.id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts" + 0 + when "activity:tags:#{tag2.id}:#{at_time.beginning_of_day.to_i}:accounts" + 4 + when "activity:tags:#{tag3.id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts" + 13 + end + end + + Redis.current.zadd('trending_tags', 0.9, tag3.id) + Redis.current.sadd("trending_tags:used:#{at_time.beginning_of_day.to_i}", [tag1.id, tag2.id]) + + tag3.update(max_score: 0.9, max_score_at: (at_time - 1.day).beginning_of_day + 12.hours) + + described_class.update!(at_time) + end + + it 'calculates and re-calculates scores' do + expect(described_class.get(10, filtered: false)).to eq [tag1, tag3] + end + + it 'omits hashtags below threshold' do + expect(described_class.get(10, filtered: false)).to_not include(tag2) + end + + it 'decays scores' do + expect(Redis.current.zscore('trending_tags', tag3.id)).to be < 0.9 + end + end + + describe '.trending?' do + let(:tag) { Fabricate(:tag) } + + before do + 10.times { |i| Redis.current.zadd('trending_tags', i + 1, Fabricate(:tag).id) } + end + + it 'returns true if the hashtag is within limit' do + Redis.current.zadd('trending_tags', 11, tag.id) + expect(described_class.trending?(tag)).to be true + end + + it 'returns false if the hashtag is outside the limit' do + Redis.current.zadd('trending_tags', 0, tag.id) + expect(described_class.trending?(tag)).to be false + end + end +end -- cgit From f665901e3c0930fb8b3741f6bc6f6a15dd0343f6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 6 Oct 2019 22:11:17 +0200 Subject: Fix performance of home feed regeneration (#12084) Fetching statuses from all followed accounts at once takes too long within Postgres. Fetching them one by one and merging in Ruby could be a lot less resource-intensive Because the query for dynamically fetching the home timeline is so heavy, we can no longer offer it when the home timeline is missing --- .../api/v1/timelines/home_controller.rb | 6 +- app/javascript/mastodon/actions/timelines.js | 2 +- .../mastodon/components/missing_indicator.js | 23 +++-- .../mastodon/components/regeneration_indicator.js | 18 ++++ app/javascript/mastodon/components/status_list.js | 15 +-- .../mastodon/features/generic_not_found/index.js | 2 +- app/javascript/styles/mastodon/components.scss | 30 ++---- app/javascript/styles/mastodon/introduction.scss | 3 +- app/lib/feed_manager.rb | 106 +++++++++++++++------ app/models/home_feed.rb | 16 +--- app/models/status.rb | 4 - spec/models/home_feed_spec.rb | 5 +- spec/models/status_spec.rb | 43 --------- 13 files changed, 132 insertions(+), 141 deletions(-) create mode 100644 app/javascript/mastodon/components/regeneration_indicator.js (limited to 'app/lib/feed_manager.rb') diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb index fcd0757f1..ff5ede138 100644 --- a/app/controllers/api/v1/timelines/home_controller.rb +++ b/app/controllers/api/v1/timelines/home_controller.rb @@ -13,7 +13,7 @@ class Api::V1::Timelines::HomeController < Api::BaseController render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), - status: regeneration_in_progress? ? 206 : 200 + status: account_home_feed.regenerating? ? 206 : 200 end private @@ -62,8 +62,4 @@ class Api::V1::Timelines::HomeController < Api::BaseController def pagination_since_id @statuses.first.id end - - def regeneration_in_progress? - Redis.current.exists("account:#{current_account.id}:regeneration") - end end diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 7eeba2aa7..bc2ac5e82 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -97,7 +97,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); - dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); + dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); done(); }).catch(error => { dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); diff --git a/app/javascript/mastodon/components/missing_indicator.js b/app/javascript/mastodon/components/missing_indicator.js index 70d8c3b98..7b0101bab 100644 --- a/app/javascript/mastodon/components/missing_indicator.js +++ b/app/javascript/mastodon/components/missing_indicator.js @@ -1,17 +1,24 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; +import illustration from 'mastodon/../images/elephant_ui_disappointed.svg'; +import classNames from 'classnames'; -const MissingIndicator = () => ( -
-
-
+const MissingIndicator = ({ fullPage }) => ( +
+
+ +
-
- - -
+
+ +
); +MissingIndicator.propTypes = { + fullPage: PropTypes.bool, +}; + export default MissingIndicator; diff --git a/app/javascript/mastodon/components/regeneration_indicator.js b/app/javascript/mastodon/components/regeneration_indicator.js new file mode 100644 index 000000000..faf88c6b5 --- /dev/null +++ b/app/javascript/mastodon/components/regeneration_indicator.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import illustration from 'mastodon/../images/elephant_ui_working.svg'; + +const MissingIndicator = () => ( +
+
+ +
+ +
+ + +
+
+); + +export default MissingIndicator; diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js index 745e6422d..e1b370c91 100644 --- a/app/javascript/mastodon/components/status_list.js +++ b/app/javascript/mastodon/components/status_list.js @@ -1,12 +1,12 @@ import { debounce } from 'lodash'; import React from 'react'; -import { FormattedMessage } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import StatusContainer from '../containers/status_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import LoadGap from './load_gap'; import ScrollableList from './scrollable_list'; +import RegenerationIndicator from 'mastodon/components/regeneration_indicator'; export default class StatusList extends ImmutablePureComponent { @@ -81,18 +81,7 @@ export default class StatusList extends ImmutablePureComponent { const { isLoading, isPartial } = other; if (isPartial) { - return ( -
-
-
- -
- - -
-
-
- ); + return ; } let scrollableContent = (isLoading || statusIds.size > 0) ? ( diff --git a/app/javascript/mastodon/features/generic_not_found/index.js b/app/javascript/mastodon/features/generic_not_found/index.js index 0290be47f..41cd61a5f 100644 --- a/app/javascript/mastodon/features/generic_not_found/index.js +++ b/app/javascript/mastodon/features/generic_not_found/index.js @@ -4,7 +4,7 @@ import MissingIndicator from '../../components/missing_indicator'; const GenericNotFound = () => ( - + ); diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index a6675b8ed..d9182ade9 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3127,37 +3127,27 @@ a.status-card.compact:hover { cursor: default; display: flex; flex: 1 1 auto; + flex-direction: column; align-items: center; justify-content: center; padding: 20px; - & > div { - width: 100%; - background: transparent; - padding-top: 0; - } - &__figure { - background: url('../images/elephant_ui_working.svg') no-repeat center 0; - width: 100%; - height: 160px; - background-size: contain; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + &, + img { + display: block; + width: auto; + height: 160px; + margin: 0; + } } - &.missing-indicator { + &--without-header { padding-top: 20px + 48px; - - .regeneration-indicator__figure { - background-image: url('../images/elephant_ui_disappointed.svg'); - } } &__label { - margin-top: 200px; + margin-top: 30px; strong { display: block; diff --git a/app/javascript/styles/mastodon/introduction.scss b/app/javascript/styles/mastodon/introduction.scss index 222d8f60e..b44ae7306 100644 --- a/app/javascript/styles/mastodon/introduction.scss +++ b/app/javascript/styles/mastodon/introduction.scss @@ -3,9 +3,10 @@ flex-direction: column; justify-content: center; align-items: center; + height: 100vh; + background: $ui-base-color; @media screen and (max-width: 920px) { - background: darken($ui-base-color, 8%); display: block !important; } diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 871ec5c19..d8b486b60 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -19,7 +19,7 @@ class FeedManager def filter?(timeline_type, status, receiver_id) if timeline_type == :home - filter_from_home?(status, receiver_id) + filter_from_home?(status, receiver_id, build_crutches(receiver_id, [status])) elsif timeline_type == :mentions filter_from_mentions?(status, receiver_id) else @@ -29,6 +29,7 @@ class FeedManager def push_to_home(account, status) return false unless add_to_feed(:home, account.id, status, account.user&.aggregates_reblogs?) + trim(:home, account.id) PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}") if push_update_required?("timeline:#{account.id}") true @@ -36,6 +37,7 @@ class FeedManager def unpush_from_home(account, status) return false unless remove_from_feed(:home, account.id, status, account.user&.aggregates_reblogs?) + redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) true end @@ -46,7 +48,9 @@ class FeedManager should_filter &&= !ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists? return false if should_filter end + return false unless add_to_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?) + trim(:list, list.id) PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}") if push_update_required?("timeline:list:#{list.id}") true @@ -54,6 +58,7 @@ class FeedManager def unpush_from_list(list, status) return false unless remove_from_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?) + redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s)) true end @@ -85,16 +90,21 @@ class FeedManager def merge_into_timeline(from_account, into_account) timeline_key = key(:home, into_account.id) - query = from_account.statuses.limit(FeedManager::MAX_ITEMS / 4) + aggregate = into_account.user&.aggregates_reblogs? + query = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, reblog: :account).limit(FeedManager::MAX_ITEMS / 4) if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4 - oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0 + oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i query = query.where('id > ?', oldest_home_score) end - query.each do |status| - next if status.direct_visibility? || status.limited_visibility? || filter?(:home, status, into_account) - add_to_feed(:home, into_account.id, status, into_account.user&.aggregates_reblogs?) + statuses = query.to_a + crutches = build_crutches(into_account.id, statuses) + + statuses.each do |status| + next if filter_from_home?(status, into_account, crutches) + + add_to_feed(:home, into_account.id, status, aggregate) end trim(:home, into_account.id) @@ -120,24 +130,35 @@ class FeedManager end def populate_feed(account) - added = 0 - limit = FeedManager::MAX_ITEMS / 2 - max_id = nil + limit = FeedManager::MAX_ITEMS / 2 + aggregate = account.user&.aggregates_reblogs? + timeline_key = key(:home, account.id) - loop do - statuses = Status.as_home_timeline(account) - .paginate_by_max_id(limit, max_id) + account.statuses.where.not(visibility: :direct).limit(limit).each do |status| + add_to_feed(:home, account.id, status, aggregate) + end - break if statuses.empty? + account.following.includes(:account_stat).find_each do |target_account| + if redis.zcard(timeline_key) >= limit + oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i + last_status_score = Mastodon::Snowflake.id_at(account.last_status_at) - statuses.each do |status| - next if filter_from_home?(status, account) - added += 1 if add_to_feed(:home, account.id, status, account.user&.aggregates_reblogs?) + # If the feed is full and this account has not posted more recently + # than the last item on the feed, then we can skip the whole account + # because none of its statuses would stay on the feed anyway + next if last_status_score < oldest_home_score end - break unless added.zero? + statuses = target_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, reblog: :account).limit(limit) + crutches = build_crutches(account.id, statuses) + + statuses.each do |status| + next if filter_from_home?(status, account, crutches) + + add_to_feed(:home, account.id, status, aggregate) + end - max_id = statuses.last.id + trim(:home, account.id) end end @@ -152,31 +173,33 @@ class FeedManager (context == :home ? Mute.where(account_id: receiver_id, target_account_id: account_ids).any? : Mute.where(account_id: receiver_id, target_account_id: account_ids, hide_notifications: true).any?) end - def filter_from_home?(status, receiver_id) + def filter_from_home?(status, receiver_id, crutches) return false if receiver_id == status.account_id return true if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?) return true if phrase_filtered?(status, receiver_id, :home) - check_for_blocks = status.active_mentions.pluck(:account_id) + check_for_blocks = crutches[:active_mentions][status.id] || [] check_for_blocks.concat([status.account_id]) if status.reblog? check_for_blocks.concat([status.reblog.account_id]) - check_for_blocks.concat(status.reblog.active_mentions.pluck(:account_id)) + check_for_blocks.concat(crutches[:active_mentions][status.reblog_of_id] || []) end - return true if blocks_or_mutes?(receiver_id, check_for_blocks, :home) + return true if check_for_blocks.any? { |target_account_id| crutches[:blocking][target_account_id] || crutches[:muting][target_account_id] } if status.reply? && !status.in_reply_to_account_id.nil? # Filter out if it's a reply - should_filter = !Follow.where(account_id: receiver_id, target_account_id: status.in_reply_to_account_id).exists? # and I'm not following the person it's a reply to + should_filter = !crutches[:following][status.in_reply_to_account_id] # and I'm not following the person it's a reply to should_filter &&= receiver_id != status.in_reply_to_account_id # and it's not a reply to me should_filter &&= status.account_id != status.in_reply_to_account_id # and it's not a self-reply - return should_filter + + return !!should_filter elsif status.reblog? # Filter out a reblog - should_filter = Follow.where(account_id: receiver_id, target_account_id: status.account_id, show_reblogs: false).exists? # if the reblogger's reblogs are suppressed - should_filter ||= Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists? # or if the author of the reblogged status is blocking me - should_filter ||= AccountDomainBlock.where(account_id: receiver_id, domain: status.reblog.account.domain).exists? # or the author's domain is blocked - return should_filter + should_filter = crutches[:hiding_reblogs][status.account_id] # if the reblogger's reblogs are suppressed + should_filter ||= crutches[:blocked_by][status.reblog.account_id] # or if the author of the reblogged status is blocking me + should_filter ||= crutches[:domain_blocking][status.reblog.account.domain] # or the author's domain is blocked + + return !!should_filter end false @@ -308,4 +331,31 @@ class FeedManager redis.zrem(timeline_key, status.id) end + + def build_crutches(receiver_id, statuses) + crutches = {} + + crutches[:active_mentions] = Mention.active.where(status_id: statuses.flat_map { |s| [s.id, s.reblog_of_id] }.compact).pluck(:status_id, :account_id).each_with_object({}) { |(id, account_id), mapping| (mapping[id] ||= []).push(account_id) } + + check_for_blocks = statuses.flat_map do |s| + arr = crutches[:active_mentions][s.id] || [] + arr.concat([s.account_id]) + + if s.reblog? + arr.concat([s.reblog.account_id]) + arr.concat(crutches[:active_mentions][s.reblog_of_id] || []) + end + + arr + end + + crutches[:following] = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:in_reply_to_account_id).compact).pluck(:target_account_id).each_with_object({}) { |id, mapping| mapping[id] = true } + crutches[:hiding_reblogs] = Follow.where(account_id: receiver_id, target_account_id: statuses.map { |s| s.account_id if s.reblog? }.compact, show_reblogs: false).pluck(:target_account_id).each_with_object({}) { |id, mapping| mapping[id] = true } + crutches[:blocking] = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).each_with_object({}) { |id, mapping| mapping[id] = true } + crutches[:muting] = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).each_with_object({}) { |id, mapping| mapping[id] = true } + crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.map { |s| s.reblog&.account&.domain }.compact).pluck(:domain).each_with_object({}) { |domain, mapping| mapping[domain] = true } + crutches[:blocked_by] = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| s.reblog&.account_id }.compact).pluck(:account_id).each_with_object({}) { |id, mapping| mapping[id] = true } + + crutches + end end diff --git a/app/models/home_feed.rb b/app/models/home_feed.rb index ba7564983..1fd506138 100644 --- a/app/models/home_feed.rb +++ b/app/models/home_feed.rb @@ -7,19 +7,7 @@ class HomeFeed < Feed @account = account end - def get(limit, max_id = nil, since_id = nil, min_id = nil) - if redis.exists("account:#{@account.id}:regeneration") - from_database(limit, max_id, since_id, min_id) - else - super - end - end - - private - - def from_database(limit, max_id, since_id, min_id) - Status.as_home_timeline(@account) - .paginate_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id) - .reject { |status| FeedManager.instance.filter?(:home, status, @account.id) } + def regenerating? + redis.exists("account:#{@id}:regeneration") end end diff --git a/app/models/status.rb b/app/models/status.rb index 3504ac370..0c01a5389 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -282,10 +282,6 @@ class Status < ApplicationRecord where(language: nil).or where(language: account.chosen_languages) end - def as_home_timeline(account) - where(account: [account] + account.following).where(visibility: [:public, :unlisted, :private]) - end - def as_public_timeline(account = nil, local_only = false) query = timeline_scope(local_only).without_replies diff --git a/spec/models/home_feed_spec.rb b/spec/models/home_feed_spec.rb index 3acb997f1..ee7a83960 100644 --- a/spec/models/home_feed_spec.rb +++ b/spec/models/home_feed_spec.rb @@ -34,11 +34,10 @@ RSpec.describe HomeFeed, type: :model do Redis.current.set("account:#{account.id}:regeneration", true) end - it 'gets statuses with ids in the range from database' do + it 'returns nothing' do results = subject.get(3) - expect(results.map(&:id)).to eq [10, 3, 2] - expect(results.first.attributes.keys).to include('id', 'updated_at') + expect(results.map(&:id)).to eq [] end end end diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index a8c567414..51a10cd17 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -296,49 +296,6 @@ RSpec.describe Status, type: :model do end end - describe '.as_home_timeline' do - let(:account) { Fabricate(:account) } - let(:followed) { Fabricate(:account) } - let(:not_followed) { Fabricate(:account) } - - before do - Fabricate(:follow, account: account, target_account: followed) - - @self_status = Fabricate(:status, account: account, visibility: :public) - @self_direct_status = Fabricate(:status, account: account, visibility: :direct) - @followed_status = Fabricate(:status, account: followed, visibility: :public) - @followed_direct_status = Fabricate(:status, account: followed, visibility: :direct) - @not_followed_status = Fabricate(:status, account: not_followed, visibility: :public) - - @results = Status.as_home_timeline(account) - end - - it 'includes statuses from self' do - expect(@results).to include(@self_status) - end - - it 'does not include direct statuses from self' do - expect(@results).to_not include(@self_direct_status) - end - - it 'includes statuses from followed' do - expect(@results).to include(@followed_status) - end - - it 'does not include direct statuses mentioning recipient from followed' do - Fabricate(:mention, account: account, status: @followed_direct_status) - expect(@results).to_not include(@followed_direct_status) - end - - it 'does not include direct statuses not mentioning recipient from followed' do - expect(@results).not_to include(@followed_direct_status) - end - - it 'does not include statuses from non-followed' do - expect(@results).not_to include(@not_followed_status) - end - end - describe '.as_public_timeline' do it 'only includes statuses with public visibility' do public_status = Fabricate(:status, visibility: :public) -- cgit From 0092096328bcde6f90f965c7e22e5db763732053 Mon Sep 17 00:00:00 2001 From: Darius Kazemi Date: Thu, 7 Nov 2019 11:51:48 -0800 Subject: Fix type mismatch (#12324) This was [causing an issue with feed regeneartion in tootctl](https://github.com/hometown-fork/hometown/issues/24), and @davefp fixed the issue. --- app/lib/feed_manager.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/lib/feed_manager.rb') diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index d8b486b60..3f167f0d8 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -153,7 +153,7 @@ class FeedManager crutches = build_crutches(account.id, statuses) statuses.each do |status| - next if filter_from_home?(status, account, crutches) + next if filter_from_home?(status, account.id, crutches) add_to_feed(:home, account.id, status, aggregate) end -- cgit