From 24cafd73a2b644025e9aeaadf4fed46dd3ecea4d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 18 Nov 2017 00:16:48 +0100 Subject: Lists (#5703) * Add structure for lists * Add list timeline streaming API * Add list APIs, bind list-account relation to follow relation * Add API for adding/removing accounts from lists * Add pagination to lists API * Add pagination to list accounts API * Adjust scopes for new APIs - Creating and modifying lists merely requires "write" scope - Fetching information about lists merely requires "read" scope * Add test for wrong user context on list timeline * Clean up tests --- app/lib/feed_manager.rb | 73 +++++++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 32 deletions(-) (limited to 'app/lib/feed_manager.rb') diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 58650efb6..79fae6e96 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -26,34 +26,42 @@ class FeedManager end end - def push(timeline_type, account, status) - return false unless add_to_feed(timeline_type, account, status) - - trim(timeline_type, account.id) - - PushUpdateWorker.perform_async(account.id, status.id) if push_update_required?(timeline_type, account.id) - + def push_to_home(account, status) + return false unless add_to_feed(:home, account.id, status) + trim(:home, account.id) + PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}") if push_update_required?("timeline:#{account.id}") true end - def unpush(timeline_type, account, status) - return false unless remove_from_feed(timeline_type, account, status) + def unpush_from_home(account, status) + return false unless remove_from_feed(:home, account.id, status) + Redis.current.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) + true + end - payload = Oj.dump(event: :delete, payload: status.id.to_s) - Redis.current.publish("timeline:#{account.id}", payload) + def push_to_list(list, status) + return false unless add_to_feed(:list, list.id, status) + 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 + end + def unpush_from_list(list, status) + return false unless remove_from_feed(:list, list.id, status) + Redis.current.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s)) true end def trim(type, account_id) timeline_key = key(type, account_id) - reblog_key = key(type, account_id, 'reblogs') + 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) # Get the score of the REBLOG_FALLOFF'th item in our feed, and stop # tracking anything after it for deduplication purposes. - falloff_rank = FeedManager::REBLOG_FALLOFF - 1 + falloff_rank = FeedManager::REBLOG_FALLOFF - 1 falloff_range = redis.zrevrange(timeline_key, falloff_rank, falloff_rank, with_scores: true) falloff_score = falloff_range&.first&.last&.to_i || 0 @@ -69,10 +77,6 @@ class FeedManager end end - def push_update_required?(timeline_type, account_id) - timeline_type != :home || redis.get("subscribed:timeline:#{account_id}").present? - end - def merge_into_timeline(from_account, into_account) timeline_key = key(:home, into_account.id) query = from_account.statuses.limit(FeedManager::MAX_ITEMS / 4) @@ -84,28 +88,28 @@ class FeedManager query.each do |status| next if status.direct_visibility? || filter?(:home, status, into_account) - add_to_feed(:home, into_account, status) + add_to_feed(:home, into_account.id, status) end trim(:home, into_account.id) end def unmerge_from_timeline(from_account, into_account) - timeline_key = key(:home, into_account.id) + timeline_key = key(:home, into_account.id) 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, status) + remove_from_feed(:home, into_account.id, status) end end def clear_from_timeline(account, target_account) - timeline_key = key(:home, account.id) + timeline_key = key(:home, account.id) timeline_status_ids = redis.zrange(timeline_key, 0, -1) - target_statuses = Status.where(id: timeline_status_ids, account: target_account) + target_statuses = Status.where(id: timeline_status_ids, account: target_account) target_statuses.each do |status| - unpush(:home, account, status) + unpush_from_home(account, status) end end @@ -122,7 +126,7 @@ class FeedManager statuses.each do |status| next if filter_from_home?(status, account) - added += 1 if add_to_feed(:home, account, status) + added += 1 if add_to_feed(:home, account.id, status) end break unless added.zero? @@ -137,6 +141,10 @@ class FeedManager Redis.current end + def push_update_required?(timeline_id) + redis.exists("subscribed:#{timeline_id}") + end + def filter_from_home?(status, receiver_id) 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?) @@ -182,9 +190,9 @@ class FeedManager # added, and false if it was not added to the feed. Note that this is # an internal helper: callers must call trim or push updates if # either action is appropriate. - def add_to_feed(timeline_type, account, status) - timeline_key = key(timeline_type, account.id) - reblog_key = key(timeline_type, account.id, 'reblogs') + def add_to_feed(timeline_type, account_id, status) + timeline_key = key(timeline_type, account_id) + reblog_key = key(timeline_type, account_id, 'reblogs') if status.reblog? # If the original status or a reblog of it is within @@ -195,6 +203,7 @@ class FeedManager return false if !rank.nil? && rank < FeedManager::REBLOG_FALLOFF reblog_rank = redis.zrevrank(reblog_key, status.reblog_of_id) + if reblog_rank.nil? # This is not something we've already seen reblogged, so we # can just add it to the feed (and note that we're @@ -205,7 +214,7 @@ class FeedManager # Another reblog of the same status was already in the # REBLOG_FALLOFF most recent statuses, so we note that this # is an "extra" reblog, by storing it in reblog_set_key. - reblog_set_key = key(timeline_type, account.id, "reblogs:#{status.reblog_of_id}") + reblog_set_key = key(timeline_type, account_id, "reblogs:#{status.reblog_of_id}") redis.sadd(reblog_set_key, status.id) return false end @@ -220,8 +229,8 @@ 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, status) - timeline_key = key(timeline_type, account.id) + def remove_from_feed(timeline_type, account_id, status) + timeline_key = key(timeline_type, account_id) if status.reblog? # 1. If the reblogging status is not in the feed, stop. @@ -229,7 +238,7 @@ class FeedManager return false if status_rank.nil? # 2. Remove reblog from set of this status's reblogs. - reblog_set_key = key(timeline_type, account.id, "reblogs:#{status.reblog_of_id}") + reblog_set_key = key(timeline_type, account_id, "reblogs:#{status.reblog_of_id}") redis.srem(reblog_set_key, status.id) # 3. Re-insert another reblog or original into the feed if one @@ -244,7 +253,7 @@ class FeedManager # (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.del(key(timeline_type, account_id, "reblogs:#{status.id}")) end redis.zrem(timeline_key, status.id) -- cgit