Merge pull request #224 from yipdw/merge-upstream
Merge upstream (tootsuite/mastodon#5703)
This commit is contained in:
		
						commit
						284e2cde81
					
				
					 68 changed files with 858 additions and 228 deletions
				
			
		
							
								
								
									
										81
									
								
								app/controllers/api/v1/lists/accounts_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								app/controllers/api/v1/lists/accounts_controller.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,81 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class Api::V1::Lists::AccountsController < Api::BaseController | ||||
|   before_action -> { doorkeeper_authorize! :read },    only: [:show] | ||||
|   before_action -> { doorkeeper_authorize! :write }, except: [:show] | ||||
| 
 | ||||
|   before_action :require_user! | ||||
|   before_action :set_list | ||||
| 
 | ||||
|   after_action :insert_pagination_headers, only: :show | ||||
| 
 | ||||
|   def show | ||||
|     @accounts = @list.accounts.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) | ||||
|     render json: @accounts, each_serializer: REST::AccountSerializer | ||||
|   end | ||||
| 
 | ||||
|   def create | ||||
|     ApplicationRecord.transaction do | ||||
|       list_accounts.each do |account| | ||||
|         @list.accounts << account | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     render_empty | ||||
|   end | ||||
| 
 | ||||
|   def destroy | ||||
|     ListAccount.where(list: @list, account_id: account_ids).destroy_all | ||||
|     render_empty | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def set_list | ||||
|     @list = List.where(account: current_account).find(params[:list_id]) | ||||
|   end | ||||
| 
 | ||||
|   def list_accounts | ||||
|     Account.find(account_ids) | ||||
|   end | ||||
| 
 | ||||
|   def account_ids | ||||
|     Array(resource_params[:account_ids]) | ||||
|   end | ||||
| 
 | ||||
|   def resource_params | ||||
|     params.permit(account_ids: []) | ||||
|   end | ||||
| 
 | ||||
|   def insert_pagination_headers | ||||
|     set_pagination_headers(next_path, prev_path) | ||||
|   end | ||||
| 
 | ||||
|   def next_path | ||||
|     if records_continue? | ||||
|       api_v1_list_accounts_url pagination_params(max_id: pagination_max_id) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def prev_path | ||||
|     unless @accounts.empty? | ||||
|       api_v1_list_accounts_url pagination_params(since_id: pagination_since_id) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def pagination_max_id | ||||
|     @accounts.last.id | ||||
|   end | ||||
| 
 | ||||
|   def pagination_since_id | ||||
|     @accounts.first.id | ||||
|   end | ||||
| 
 | ||||
|   def records_continue? | ||||
|     @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) | ||||
|   end | ||||
| 
 | ||||
|   def pagination_params(core_params) | ||||
|     params.permit(:limit).merge(core_params) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										79
									
								
								app/controllers/api/v1/lists_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								app/controllers/api/v1/lists_controller.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,79 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class Api::V1::ListsController < Api::BaseController | ||||
|   LISTS_LIMIT = 50 | ||||
| 
 | ||||
|   before_action -> { doorkeeper_authorize! :read },    only: [:index, :show] | ||||
|   before_action -> { doorkeeper_authorize! :write }, except: [:index, :show] | ||||
| 
 | ||||
|   before_action :require_user! | ||||
|   before_action :set_list, except: [:index, :create] | ||||
| 
 | ||||
|   after_action :insert_pagination_headers, only: :index | ||||
| 
 | ||||
|   def index | ||||
|     @lists = List.where(account: current_account).paginate_by_max_id(limit_param(LISTS_LIMIT), params[:max_id], params[:since_id]) | ||||
|     render json: @lists, each_serializer: REST::ListSerializer | ||||
|   end | ||||
| 
 | ||||
|   def show | ||||
|     render json: @list, serializer: REST::ListSerializer | ||||
|   end | ||||
| 
 | ||||
|   def create | ||||
|     @list = List.create!(list_params.merge(account: current_account)) | ||||
|     render json: @list, serializer: REST::ListSerializer | ||||
|   end | ||||
| 
 | ||||
|   def update | ||||
|     @list.update!(list_params) | ||||
|     render json: @list, serializer: REST::ListSerializer | ||||
|   end | ||||
| 
 | ||||
|   def destroy | ||||
|     @list.destroy! | ||||
|     render_empty | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def set_list | ||||
|     @list = List.where(account: current_account).find(params[:id]) | ||||
|   end | ||||
| 
 | ||||
|   def list_params | ||||
|     params.permit(:title) | ||||
|   end | ||||
| 
 | ||||
|   def insert_pagination_headers | ||||
|     set_pagination_headers(next_path, prev_path) | ||||
|   end | ||||
| 
 | ||||
|   def next_path | ||||
|     if records_continue? | ||||
|       api_v1_lists_url pagination_params(max_id: pagination_max_id) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def prev_path | ||||
|     unless @lists.empty? | ||||
|       api_v1_lists_url pagination_params(since_id: pagination_since_id) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def pagination_max_id | ||||
|     @lists.last.id | ||||
|   end | ||||
| 
 | ||||
|   def pagination_since_id | ||||
|     @lists.first.id | ||||
|   end | ||||
| 
 | ||||
|   def records_continue? | ||||
|     @lists.size == limit_param(LISTS_LIMIT) | ||||
|   end | ||||
| 
 | ||||
|   def pagination_params(core_params) | ||||
|     params.permit(:limit).merge(core_params) | ||||
|   end | ||||
| end | ||||
|  | @ -31,7 +31,7 @@ class Api::V1::Timelines::HomeController < Api::BaseController | |||
|   end | ||||
| 
 | ||||
|   def account_home_feed | ||||
|     Feed.new(:home, current_account) | ||||
|     HomeFeed.new(current_account) | ||||
|   end | ||||
| 
 | ||||
|   def insert_pagination_headers | ||||
|  |  | |||
							
								
								
									
										66
									
								
								app/controllers/api/v1/timelines/list_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								app/controllers/api/v1/timelines/list_controller.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,66 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class Api::V1::Timelines::ListController < Api::BaseController | ||||
|   before_action -> { doorkeeper_authorize! :read } | ||||
|   before_action :require_user! | ||||
|   before_action :set_list | ||||
|   before_action :set_statuses | ||||
| 
 | ||||
|   after_action :insert_pagination_headers, unless: -> { @statuses.empty? } | ||||
| 
 | ||||
|   def show | ||||
|     render json: @statuses, | ||||
|            each_serializer: REST::StatusSerializer, | ||||
|            relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id) | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def set_list | ||||
|     @list = List.where(account: current_account).find(params[:id]) | ||||
|   end | ||||
| 
 | ||||
|   def set_statuses | ||||
|     @statuses = cached_list_statuses | ||||
|   end | ||||
| 
 | ||||
|   def cached_list_statuses | ||||
|     cache_collection list_statuses, Status | ||||
|   end | ||||
| 
 | ||||
|   def list_statuses | ||||
|     list_feed.get( | ||||
|       limit_param(DEFAULT_STATUSES_LIMIT), | ||||
|       params[:max_id], | ||||
|       params[:since_id] | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|   def list_feed | ||||
|     ListFeed.new(@list) | ||||
|   end | ||||
| 
 | ||||
|   def insert_pagination_headers | ||||
|     set_pagination_headers(next_path, prev_path) | ||||
|   end | ||||
| 
 | ||||
|   def pagination_params(core_params) | ||||
|     params.permit(:limit).merge(core_params) | ||||
|   end | ||||
| 
 | ||||
|   def next_path | ||||
|     api_v1_timelines_list_url params[:id], pagination_params(max_id: pagination_max_id) | ||||
|   end | ||||
| 
 | ||||
|   def prev_path | ||||
|     api_v1_timelines_list_url params[:id], pagination_params(since_id: pagination_since_id) | ||||
|   end | ||||
| 
 | ||||
|   def pagination_max_id | ||||
|     @statuses.last.id | ||||
|   end | ||||
| 
 | ||||
|   def pagination_since_id | ||||
|     @statuses.first.id | ||||
|   end | ||||
| end | ||||
|  | @ -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?) | ||||
|  | @ -200,9 +208,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 | ||||
|  | @ -213,6 +221,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 | ||||
|  | @ -223,7 +232,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 | ||||
|  | @ -238,8 +247,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. | ||||
|  | @ -247,7 +256,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 | ||||
|  | @ -262,7 +271,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) | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| # | ||||
| # Table name: accounts | ||||
| # | ||||
| #  id                      :bigint           not null, primary key | ||||
| #  id                      :integer          not null, primary key | ||||
| #  username                :string           default(""), not null | ||||
| #  domain                  :string | ||||
| #  secret                  :string           default(""), not null | ||||
|  | @ -53,6 +53,7 @@ class Account < ApplicationRecord | |||
|   include AccountInteractions | ||||
|   include Attachmentable | ||||
|   include Remotable | ||||
|   include Paginable | ||||
| 
 | ||||
|   MAX_NOTE_LENGTH = 500 | ||||
| 
 | ||||
|  | @ -97,6 +98,10 @@ class Account < ApplicationRecord | |||
|   has_many :account_moderation_notes, dependent: :destroy | ||||
|   has_many :targeted_moderation_notes, class_name: 'AccountModerationNote', foreign_key: :target_account_id, dependent: :destroy | ||||
| 
 | ||||
|   # Lists | ||||
|   has_many :list_accounts, inverse_of: :account, dependent: :destroy | ||||
|   has_many :lists, through: :list_accounts | ||||
| 
 | ||||
|   scope :remote, -> { where.not(domain: nil) } | ||||
|   scope :local, -> { where(domain: nil) } | ||||
|   scope :without_followers, -> { where(followers_count: 0) } | ||||
|  |  | |||
|  | @ -3,11 +3,11 @@ | |||
| # | ||||
| # Table name: account_domain_blocks | ||||
| # | ||||
| #  id         :integer          not null, primary key | ||||
| #  domain     :string | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
| #  account_id :bigint | ||||
| #  id         :bigint           not null, primary key | ||||
| #  account_id :integer | ||||
| # | ||||
| 
 | ||||
| class AccountDomainBlock < ApplicationRecord | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ | |||
| # | ||||
| # Table name: account_moderation_notes | ||||
| # | ||||
| #  id                :bigint           not null, primary key | ||||
| #  id                :integer          not null, primary key | ||||
| #  content           :text             not null | ||||
| #  account_id        :bigint           not null | ||||
| #  target_account_id :bigint           not null | ||||
| #  account_id        :integer          not null | ||||
| #  target_account_id :integer          not null | ||||
| #  created_at        :datetime         not null | ||||
| #  updated_at        :datetime         not null | ||||
| # | ||||
|  |  | |||
|  | @ -3,11 +3,11 @@ | |||
| # | ||||
| # Table name: blocks | ||||
| # | ||||
| #  id                :integer          not null, primary key | ||||
| #  created_at        :datetime         not null | ||||
| #  updated_at        :datetime         not null | ||||
| #  account_id        :bigint           not null | ||||
| #  id                :bigint           not null, primary key | ||||
| #  target_account_id :bigint           not null | ||||
| #  account_id        :integer          not null | ||||
| #  target_account_id :integer          not null | ||||
| # | ||||
| 
 | ||||
| class Block < ApplicationRecord | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| # | ||||
| # Table name: conversations | ||||
| # | ||||
| #  id         :bigint           not null, primary key | ||||
| #  id         :integer          not null, primary key | ||||
| #  uri        :string | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
|  |  | |||
|  | @ -3,9 +3,9 @@ | |||
| # | ||||
| # Table name: conversation_mutes | ||||
| # | ||||
| #  conversation_id :bigint           not null | ||||
| #  account_id      :bigint           not null | ||||
| #  id              :bigint           not null, primary key | ||||
| #  id              :integer          not null, primary key | ||||
| #  conversation_id :integer          not null | ||||
| #  account_id      :integer          not null | ||||
| # | ||||
| 
 | ||||
| class ConversationMute < ApplicationRecord | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| # | ||||
| # Table name: custom_emojis | ||||
| # | ||||
| #  id                 :bigint           not null, primary key | ||||
| #  id                 :integer          not null, primary key | ||||
| #  shortcode          :string           default(""), not null | ||||
| #  domain             :string | ||||
| #  image_file_name    :string | ||||
|  |  | |||
|  | @ -3,12 +3,12 @@ | |||
| # | ||||
| # Table name: domain_blocks | ||||
| # | ||||
| #  id           :integer          not null, primary key | ||||
| #  domain       :string           default(""), not null | ||||
| #  created_at   :datetime         not null | ||||
| #  updated_at   :datetime         not null | ||||
| #  severity     :integer          default("silence") | ||||
| #  reject_media :boolean          default(FALSE), not null | ||||
| #  id           :bigint           not null, primary key | ||||
| # | ||||
| 
 | ||||
| class DomainBlock < ApplicationRecord | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| # | ||||
| # Table name: email_domain_blocks | ||||
| # | ||||
| #  id         :bigint           not null, primary key | ||||
| #  id         :integer          not null, primary key | ||||
| #  domain     :string           default(""), not null | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
|  |  | |||
|  | @ -3,11 +3,11 @@ | |||
| # | ||||
| # Table name: favourites | ||||
| # | ||||
| #  id         :integer          not null, primary key | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
| #  account_id :bigint           not null | ||||
| #  id         :bigint           not null, primary key | ||||
| #  status_id  :bigint           not null | ||||
| #  account_id :integer          not null | ||||
| #  status_id  :integer          not null | ||||
| # | ||||
| 
 | ||||
| class Favourite < ApplicationRecord | ||||
|  |  | |||
|  | @ -1,36 +1,27 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class Feed | ||||
|   def initialize(type, account) | ||||
|     @type    = type | ||||
|     @account = account | ||||
|   def initialize(type, id) | ||||
|     @type = type | ||||
|     @id   = id | ||||
|   end | ||||
| 
 | ||||
|   def get(limit, max_id = nil, since_id = nil) | ||||
|     if redis.exists("account:#{@account.id}:regeneration") | ||||
|       from_database(limit, max_id, since_id) | ||||
|     else | ||||
|       from_redis(limit, max_id, since_id) | ||||
|     end | ||||
|     from_redis(limit, max_id, since_id) | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
|   protected | ||||
| 
 | ||||
|   def from_redis(limit, max_id, since_id) | ||||
|     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(&:first).map(&:to_i) | ||||
| 
 | ||||
|     Status.where(id: unhydrated).cache_ids | ||||
|   end | ||||
| 
 | ||||
|   def from_database(limit, max_id, since_id) | ||||
|     Status.as_home_timeline(@account) | ||||
|           .paginate_by_max_id(limit, max_id, since_id) | ||||
|           .reject { |status| FeedManager.instance.filter?(:home, status, @account.id) } | ||||
|   end | ||||
| 
 | ||||
|   def key | ||||
|     FeedManager.instance.key(@type, @account.id) | ||||
|     FeedManager.instance.key(@type, @id) | ||||
|   end | ||||
| 
 | ||||
|   def redis | ||||
|  |  | |||
|  | @ -3,11 +3,11 @@ | |||
| # | ||||
| # Table name: follows | ||||
| # | ||||
| #  id                :integer          not null, primary key | ||||
| #  created_at        :datetime         not null | ||||
| #  updated_at        :datetime         not null | ||||
| #  account_id        :bigint          not null | ||||
| #  id                :bigint          not null, primary key | ||||
| #  target_account_id :bigint          not null | ||||
| #  account_id        :integer          not null | ||||
| #  target_account_id :integer          not null | ||||
| #  show_reblogs      :boolean          default(TRUE), not null | ||||
| # | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,11 +3,11 @@ | |||
| # | ||||
| # Table name: follow_requests | ||||
| # | ||||
| #  id                :integer          not null, primary key | ||||
| #  created_at        :datetime         not null | ||||
| #  updated_at        :datetime         not null | ||||
| #  account_id        :bigint          not null | ||||
| #  id                :bigint          not null, primary key | ||||
| #  target_account_id :bigint          not null | ||||
| #  account_id        :integer          not null | ||||
| #  target_account_id :integer          not null | ||||
| #  show_reblogs      :boolean          default(TRUE), not null | ||||
| # | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										25
									
								
								app/models/home_feed.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/models/home_feed.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class HomeFeed < Feed | ||||
|   def initialize(account) | ||||
|     @type    = :home | ||||
|     @id      = account.id | ||||
|     @account = account | ||||
|   end | ||||
| 
 | ||||
|   def get(limit, max_id = nil, since_id = nil) | ||||
|     if redis.exists("account:#{@account.id}:regeneration") | ||||
|       from_database(limit, max_id, since_id) | ||||
|     else | ||||
|       super | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def from_database(limit, max_id, since_id) | ||||
|     Status.as_home_timeline(@account) | ||||
|           .paginate_by_max_id(limit, max_id, since_id) | ||||
|           .reject { |status| FeedManager.instance.filter?(:home, status, @account.id) } | ||||
|   end | ||||
| end | ||||
|  | @ -3,6 +3,7 @@ | |||
| # | ||||
| # Table name: imports | ||||
| # | ||||
| #  id                :integer          not null, primary key | ||||
| #  type              :integer          not null | ||||
| #  approved          :boolean          default(FALSE), not null | ||||
| #  created_at        :datetime         not null | ||||
|  | @ -11,8 +12,7 @@ | |||
| #  data_content_type :string | ||||
| #  data_file_size    :integer | ||||
| #  data_updated_at   :datetime | ||||
| #  account_id        :bigint           not null | ||||
| #  id                :bigint           not null, primary key | ||||
| #  account_id        :integer          not null | ||||
| # | ||||
| 
 | ||||
| class Import < ApplicationRecord | ||||
|  |  | |||
							
								
								
									
										22
									
								
								app/models/list.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/models/list.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | |||
| # frozen_string_literal: true | ||||
| # == Schema Information | ||||
| # | ||||
| # Table name: lists | ||||
| # | ||||
| #  id         :integer          not null, primary key | ||||
| #  account_id :integer | ||||
| #  title      :string           default(""), not null | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
| # | ||||
| 
 | ||||
| class List < ApplicationRecord | ||||
|   include Paginable | ||||
| 
 | ||||
|   belongs_to :account | ||||
| 
 | ||||
|   has_many :list_accounts, inverse_of: :list, dependent: :destroy | ||||
|   has_many :accounts, through: :list_accounts | ||||
| 
 | ||||
|   validates :title, presence: true | ||||
| end | ||||
							
								
								
									
										24
									
								
								app/models/list_account.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								app/models/list_account.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | |||
| # frozen_string_literal: true | ||||
| # == Schema Information | ||||
| # | ||||
| # Table name: list_accounts | ||||
| # | ||||
| #  id         :integer          not null, primary key | ||||
| #  list_id    :integer          not null | ||||
| #  account_id :integer          not null | ||||
| #  follow_id  :integer          not null | ||||
| # | ||||
| 
 | ||||
| class ListAccount < ApplicationRecord | ||||
|   belongs_to :list, required: true | ||||
|   belongs_to :account, required: true | ||||
|   belongs_to :follow, required: true | ||||
| 
 | ||||
|   before_validation :set_follow | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def set_follow | ||||
|     self.follow = Follow.find_by(account_id: list.account_id, target_account_id: account.id) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										8
									
								
								app/models/list_feed.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/models/list_feed.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class ListFeed < Feed | ||||
|   def initialize(list) | ||||
|     @type    = :list | ||||
|     @id      = list.id | ||||
|   end | ||||
| end | ||||
|  | @ -3,19 +3,19 @@ | |||
| # | ||||
| # Table name: media_attachments | ||||
| # | ||||
| #  id                :bigint           not null, primary key | ||||
| #  status_id         :bigint | ||||
| #  id                :integer          not null, primary key | ||||
| #  status_id         :integer | ||||
| #  file_file_name    :string | ||||
| #  file_content_type :string | ||||
| #  file_file_size    :integer | ||||
| #  file_updated_at   :datetime | ||||
| #  remote_url        :string           default(""), not null | ||||
| #  account_id        :bigint | ||||
| #  created_at        :datetime         not null | ||||
| #  updated_at        :datetime         not null | ||||
| #  shortcode         :string | ||||
| #  type              :integer          default("image"), not null | ||||
| #  file_meta         :json | ||||
| #  account_id        :integer | ||||
| #  description       :text | ||||
| # | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,11 +3,11 @@ | |||
| # | ||||
| # Table name: mentions | ||||
| # | ||||
| #  status_id  :bigint | ||||
| #  id         :integer          not null, primary key | ||||
| #  status_id  :integer | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
| #  account_id :bigint | ||||
| #  id         :bigint           not null, primary key | ||||
| #  account_id :integer | ||||
| # | ||||
| 
 | ||||
| class Mention < ApplicationRecord | ||||
|  |  | |||
|  | @ -3,12 +3,12 @@ | |||
| # | ||||
| # Table name: mutes | ||||
| # | ||||
| #  id                 :bigint          not null, primary key | ||||
| #  id                 :integer          not null, primary key | ||||
| #  created_at         :datetime         not null | ||||
| #  updated_at         :datetime         not null | ||||
| #  account_id         :bigint          not null | ||||
| #  target_account_id  :bigint          not null | ||||
| #  hide_notifications :boolean          default(TRUE), not null | ||||
| #  account_id         :integer          not null | ||||
| #  target_account_id  :integer          not null | ||||
| # | ||||
| 
 | ||||
| class Mute < ApplicationRecord | ||||
|  |  | |||
|  | @ -3,13 +3,13 @@ | |||
| # | ||||
| # Table name: notifications | ||||
| # | ||||
| #  id              :bigint           not null, primary key | ||||
| #  account_id      :bigint | ||||
| #  activity_id     :bigint | ||||
| #  id              :integer          not null, primary key | ||||
| #  activity_id     :integer | ||||
| #  activity_type   :string | ||||
| #  created_at      :datetime         not null | ||||
| #  updated_at      :datetime         not null | ||||
| #  from_account_id :bigint | ||||
| #  account_id      :integer | ||||
| #  from_account_id :integer | ||||
| # | ||||
| 
 | ||||
| class Notification < ApplicationRecord | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| # | ||||
| # Table name: preview_cards | ||||
| # | ||||
| #  id                 :bigint           not null, primary key | ||||
| #  id                 :integer          not null, primary key | ||||
| #  url                :string           default(""), not null | ||||
| #  title              :string           default(""), not null | ||||
| #  description        :string           default(""), not null | ||||
|  |  | |||
|  | @ -3,15 +3,15 @@ | |||
| # | ||||
| # Table name: reports | ||||
| # | ||||
| #  id                         :integer          not null, primary key | ||||
| #  status_ids                 :integer          default([]), not null, is an Array | ||||
| #  comment                    :text             default(""), not null | ||||
| #  action_taken               :boolean          default(FALSE), not null | ||||
| #  created_at                 :datetime         not null | ||||
| #  updated_at                 :datetime         not null | ||||
| #  account_id                 :bigint           not null | ||||
| #  action_taken_by_account_id :bigint | ||||
| #  id                         :bigint           not null, primary key | ||||
| #  target_account_id          :bigint           not null | ||||
| #  account_id                 :integer          not null | ||||
| #  action_taken_by_account_id :integer | ||||
| #  target_account_id          :integer          not null | ||||
| # | ||||
| 
 | ||||
| class Report < ApplicationRecord | ||||
|  |  | |||
|  | @ -3,15 +3,15 @@ | |||
| # | ||||
| # Table name: session_activations | ||||
| # | ||||
| #  id                       :bigint           not null, primary key | ||||
| #  user_id                  :bigint           not null | ||||
| #  id                       :integer          not null, primary key | ||||
| #  session_id               :string           not null | ||||
| #  created_at               :datetime         not null | ||||
| #  updated_at               :datetime         not null | ||||
| #  user_agent               :string           default(""), not null | ||||
| #  ip                       :inet | ||||
| #  access_token_id          :bigint | ||||
| #  web_push_subscription_id :bigint | ||||
| #  access_token_id          :integer | ||||
| #  user_id                  :integer          not null | ||||
| #  web_push_subscription_id :integer | ||||
| # | ||||
| 
 | ||||
| #  id              :bigint           not null, primary key | ||||
|  |  | |||
|  | @ -3,13 +3,13 @@ | |||
| # | ||||
| # Table name: settings | ||||
| # | ||||
| #  id         :integer          not null, primary key | ||||
| #  var        :string           not null | ||||
| #  value      :text | ||||
| #  thing_type :string | ||||
| #  created_at :datetime | ||||
| #  updated_at :datetime | ||||
| #  id         :bigint           not null, primary key | ||||
| #  thing_id   :bigint | ||||
| #  thing_id   :integer | ||||
| # | ||||
| 
 | ||||
| class Setting < RailsSettings::Base | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| # | ||||
| # Table name: site_uploads | ||||
| # | ||||
| #  id                :bigint           not null, primary key | ||||
| #  id                :integer          not null, primary key | ||||
| #  var               :string           default(""), not null | ||||
| #  file_file_name    :string | ||||
| #  file_content_type :string | ||||
|  |  | |||
|  | @ -3,26 +3,26 @@ | |||
| # | ||||
| # Table name: statuses | ||||
| # | ||||
| #  id                     :bigint           not null, primary key | ||||
| #  id                     :integer          not null, primary key | ||||
| #  uri                    :string | ||||
| #  account_id             :bigint           not null | ||||
| #  text                   :text             default(""), not null | ||||
| #  created_at             :datetime         not null | ||||
| #  updated_at             :datetime         not null | ||||
| #  in_reply_to_id         :bigint | ||||
| #  reblog_of_id           :bigint | ||||
| #  in_reply_to_id         :integer | ||||
| #  reblog_of_id           :integer | ||||
| #  url                    :string | ||||
| #  sensitive              :boolean          default(FALSE), not null | ||||
| #  visibility             :integer          default("public"), not null | ||||
| #  in_reply_to_account_id :bigint | ||||
| #  application_id         :bigint | ||||
| #  spoiler_text           :text             default(""), not null | ||||
| #  reply                  :boolean          default(FALSE), not null | ||||
| #  favourites_count       :integer          default(0), not null | ||||
| #  reblogs_count          :integer          default(0), not null | ||||
| #  language               :string | ||||
| #  conversation_id        :bigint | ||||
| #  conversation_id        :integer | ||||
| #  local                  :boolean | ||||
| #  account_id             :integer          not null | ||||
| #  application_id         :integer | ||||
| #  in_reply_to_account_id :integer | ||||
| # | ||||
| 
 | ||||
| class Status < ApplicationRecord | ||||
|  |  | |||
|  | @ -3,9 +3,9 @@ | |||
| # | ||||
| # Table name: status_pins | ||||
| # | ||||
| #  id         :bigint           not null, primary key | ||||
| #  account_id :bigint           not null | ||||
| #  status_id  :bigint           not null | ||||
| #  id         :integer          not null, primary key | ||||
| #  account_id :integer          not null | ||||
| #  status_id  :integer          not null | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
| # | ||||
|  |  | |||
|  | @ -3,13 +3,13 @@ | |||
| # | ||||
| # Table name: stream_entries | ||||
| # | ||||
| #  activity_id   :bigint | ||||
| #  id            :integer          not null, primary key | ||||
| #  activity_id   :integer | ||||
| #  activity_type :string | ||||
| #  created_at    :datetime         not null | ||||
| #  updated_at    :datetime         not null | ||||
| #  hidden        :boolean          default(FALSE), not null | ||||
| #  account_id    :bigint | ||||
| #  id            :bigint           not null, primary key | ||||
| #  account_id    :integer | ||||
| # | ||||
| 
 | ||||
| class StreamEntry < ApplicationRecord | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ | |||
| # | ||||
| # Table name: subscriptions | ||||
| # | ||||
| #  id                          :integer          not null, primary key | ||||
| #  callback_url                :string           default(""), not null | ||||
| #  secret                      :string | ||||
| #  expires_at                  :datetime | ||||
|  | @ -11,8 +12,7 @@ | |||
| #  updated_at                  :datetime         not null | ||||
| #  last_successful_delivery_at :datetime | ||||
| #  domain                      :string | ||||
| #  account_id                  :bigint           not null | ||||
| #  id                          :bigint           not null, primary key | ||||
| #  account_id                  :integer          not null | ||||
| # | ||||
| 
 | ||||
| class Subscription < ApplicationRecord | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| # | ||||
| # Table name: tags | ||||
| # | ||||
| #  id         :bigint           not null, primary key | ||||
| #  id         :integer          not null, primary key | ||||
| #  name       :string           default(""), not null | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| # | ||||
| # Table name: users | ||||
| # | ||||
| #  id                        :bigint           not null, primary key | ||||
| #  id                        :integer          not null, primary key | ||||
| #  email                     :string           default(""), not null | ||||
| #  created_at                :datetime         not null | ||||
| #  updated_at                :datetime         not null | ||||
|  | @ -30,7 +30,7 @@ | |||
| #  last_emailed_at           :datetime | ||||
| #  otp_backup_codes          :string           is an Array | ||||
| #  filtered_languages        :string           default([]), not null, is an Array | ||||
| #  account_id                :bigint           not null | ||||
| #  account_id                :integer          not null | ||||
| #  disabled                  :boolean          default(FALSE), not null | ||||
| #  moderator                 :boolean          default(FALSE), not null | ||||
| # | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| # | ||||
| # Table name: web_push_subscriptions | ||||
| # | ||||
| #  id         :bigint           not null, primary key | ||||
| #  id         :integer          not null, primary key | ||||
| #  endpoint   :string           not null | ||||
| #  key_p256dh :string           not null | ||||
| #  key_auth   :string           not null | ||||
|  |  | |||
|  | @ -3,11 +3,11 @@ | |||
| # | ||||
| # Table name: web_settings | ||||
| # | ||||
| #  id         :integer          not null, primary key | ||||
| #  data       :json | ||||
| #  created_at :datetime         not null | ||||
| #  updated_at :datetime         not null | ||||
| #  id         :bigint           not null, primary key | ||||
| #  user_id    :bigint | ||||
| #  user_id    :integer | ||||
| # | ||||
| 
 | ||||
| class Web::Setting < ApplicationRecord | ||||
|  |  | |||
							
								
								
									
										5
									
								
								app/serializers/rest/list_serializer.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/serializers/rest/list_serializer.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class REST::ListSerializer < ActiveModel::Serializer | ||||
|   attributes :id, :title | ||||
| end | ||||
|  | @ -30,6 +30,7 @@ class BatchedRemoveStatusService < BaseService | |||
|       account = account_statuses.first.account | ||||
| 
 | ||||
|       unpush_from_home_timelines(account, account_statuses) | ||||
|       unpush_from_list_timelines(account, account_statuses) | ||||
| 
 | ||||
|       if account.local? | ||||
|         batch_stream_entries(account, account_statuses) | ||||
|  | @ -80,7 +81,15 @@ class BatchedRemoveStatusService < BaseService | |||
| 
 | ||||
|     recipients.each do |follower| | ||||
|       statuses.each do |status| | ||||
|         FeedManager.instance.unpush(:home, follower, status) | ||||
|         FeedManager.instance.unpush_from_home(follower, status) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def unpush_from_list_timelines(account, statuses) | ||||
|     account.lists.select(:id, :account_id).each do |list| | ||||
|       statuses.each do |status| | ||||
|         FeedManager.instance.unpush_from_list(list, status) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ class FanOutOnWriteService < BaseService | |||
|       deliver_to_direct_timelines(status) | ||||
|     else | ||||
|       deliver_to_followers(status) | ||||
|       deliver_to_lists(status) | ||||
|     end | ||||
| 
 | ||||
|     return if status.account.silenced? || !status.public_visibility? || status.reblog? | ||||
|  | @ -32,7 +33,7 @@ class FanOutOnWriteService < BaseService | |||
| 
 | ||||
|   def deliver_to_self(status) | ||||
|     Rails.logger.debug "Delivering status #{status.id} to author" | ||||
|     FeedManager.instance.push(:home, status.account, status) | ||||
|     FeedManager.instance.push_to_home(status.account, status) | ||||
|   end | ||||
| 
 | ||||
|   def deliver_to_followers(status) | ||||
|  | @ -40,7 +41,17 @@ class FanOutOnWriteService < BaseService | |||
| 
 | ||||
|     status.account.followers.where(domain: nil).joins(:user).where('users.current_sign_in_at > ?', 14.days.ago).select(:id).reorder(nil).find_in_batches do |followers| | ||||
|       FeedInsertWorker.push_bulk(followers) do |follower| | ||||
|         [status.id, follower.id] | ||||
|         [status.id, follower.id, :home] | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def deliver_to_lists(status) | ||||
|     Rails.logger.debug "Delivering status #{status.id} to lists" | ||||
| 
 | ||||
|     status.account.lists.joins(account: :user).where('users.current_sign_in_at > ?', 14.days.ago).select(:id).reorder(nil).find_in_batches do |lists| | ||||
|       FeedInsertWorker.push_bulk(lists) do |list| | ||||
|         [status.id, list.id, :list] | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | @ -51,7 +62,7 @@ class FanOutOnWriteService < BaseService | |||
|     status.mentions.includes(:account).each do |mention| | ||||
|       mentioned_account = mention.account | ||||
|       next if !mentioned_account.local? || !mentioned_account.following?(status.account) || FeedManager.instance.filter?(:home, status, mention.account_id) | ||||
|       FeedManager.instance.push(:home, mentioned_account, status) | ||||
|       FeedManager.instance.push_to_home(mentioned_account, status) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ class RemoveStatusService < BaseService | |||
| 
 | ||||
|     remove_from_self if status.account.local? | ||||
|     remove_from_followers | ||||
|     remove_from_lists | ||||
|     remove_from_affected | ||||
|     remove_reblogs | ||||
|     remove_from_hashtags | ||||
|  | @ -31,12 +32,18 @@ class RemoveStatusService < BaseService | |||
|   private | ||||
| 
 | ||||
|   def remove_from_self | ||||
|     unpush(:home, @account, @status) | ||||
|     FeedManager.instance.unpush_from_home(@account, @status) | ||||
|   end | ||||
| 
 | ||||
|   def remove_from_followers | ||||
|     @account.followers.local.find_each do |follower| | ||||
|       unpush(:home, follower, @status) | ||||
|       FeedManager.instance.unpush_from_home(follower, @status) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def remove_from_lists | ||||
|     @account.lists.select(:id, :account_id).find_each do |list| | ||||
|       FeedManager.instance.unpush_from_list(list, @status) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  | @ -102,10 +109,6 @@ class RemoveStatusService < BaseService | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def unpush(type, receiver, status) | ||||
|     FeedManager.instance.unpush(type, receiver, status) | ||||
|   end | ||||
| 
 | ||||
|   def remove_from_hashtags | ||||
|     return unless @status.public_visibility? | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,34 +3,41 @@ | |||
| class FeedInsertWorker | ||||
|   include Sidekiq::Worker | ||||
| 
 | ||||
|   attr_reader :status, :follower | ||||
|   def perform(status_id, id, type = :home) | ||||
|     @type     = type.to_sym | ||||
|     @status   = Status.find(status_id) | ||||
| 
 | ||||
|   def perform(status_id, follower_id) | ||||
|     @status = Status.find_by(id: status_id) | ||||
|     @follower = Account.find_by(id: follower_id) | ||||
|     case @type | ||||
|     when :home | ||||
|       @follower = Account.find(id) | ||||
|     when :list | ||||
|       @list     = List.find(id) | ||||
|       @follower = @list.account | ||||
|     end | ||||
| 
 | ||||
|     check_and_insert | ||||
|   rescue ActiveRecord::RecordNotFound | ||||
|     true | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def check_and_insert | ||||
|     if records_available? | ||||
|       perform_push unless feed_filtered? | ||||
|     else | ||||
|       true | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def records_available? | ||||
|     status.present? && follower.present? | ||||
|     perform_push unless feed_filtered? | ||||
|   end | ||||
| 
 | ||||
|   def feed_filtered? | ||||
|     FeedManager.instance.filter?(:home, status, follower.id) | ||||
|     # Note: Lists are a variation of home, so the filtering rules | ||||
|     # of home apply to both | ||||
|     FeedManager.instance.filter?(:home, @status, @follower.id) | ||||
|   end | ||||
| 
 | ||||
|   def perform_push | ||||
|     FeedManager.instance.push(:home, follower, status) | ||||
|     case @type | ||||
|     when :home | ||||
|       FeedManager.instance.push_to_home(@follower, @status) | ||||
|     when :list | ||||
|       FeedManager.instance.push_to_list(@list, @status) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -3,12 +3,13 @@ | |||
| class PushUpdateWorker | ||||
|   include Sidekiq::Worker | ||||
| 
 | ||||
|   def perform(account_id, status_id) | ||||
|     account = Account.find(account_id) | ||||
|     status  = Status.find(status_id) | ||||
|     message = InlineRenderer.render(status, account, :status) | ||||
|   def perform(account_id, status_id, timeline_id = nil) | ||||
|     account     = Account.find(account_id) | ||||
|     status      = Status.find(status_id) | ||||
|     message     = InlineRenderer.render(status, account, :status) | ||||
|     timeline_id = "timeline:#{account.id}" if timeline_id.nil? | ||||
| 
 | ||||
|     Redis.current.publish("timeline:#{account.id}", Oj.dump(event: :update, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i)) | ||||
|     Redis.current.publish(timeline_id, Oj.dump(event: :update, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i)) | ||||
|   rescue ActiveRecord::RecordNotFound | ||||
|     true | ||||
|   end | ||||
|  |  | |||
|  | @ -220,6 +220,7 @@ Rails.application.routes.draw do | |||
|         resource :home, only: :show, controller: :home | ||||
|         resource :public, only: :show, controller: :public | ||||
|         resources :tag, only: :show | ||||
|         resources :list, only: :show | ||||
|       end | ||||
| 
 | ||||
|       resources :streaming, only: [:index] | ||||
|  | @ -283,6 +284,10 @@ Rails.application.routes.draw do | |||
|           post :unmute | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       resources :lists, only: [:index, :create, :show, :update, :destroy] do | ||||
|         resource :accounts, only: [:show, :create, :destroy], controller: 'lists/accounts' | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     namespace :web do | ||||
|  |  | |||
							
								
								
									
										10
									
								
								db/migrate/20171114231651_create_lists.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								db/migrate/20171114231651_create_lists.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| class CreateLists < ActiveRecord::Migration[5.1] | ||||
|   def change | ||||
|     create_table :lists do |t| | ||||
|       t.references :account, foreign_key: { on_delete: :cascade } | ||||
|       t.string :title, null: false, default: '' | ||||
| 
 | ||||
|       t.timestamps | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										12
									
								
								db/migrate/20171116161857_create_list_accounts.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								db/migrate/20171116161857_create_list_accounts.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | |||
| class CreateListAccounts < ActiveRecord::Migration[5.1] | ||||
|   def change | ||||
|     create_table :list_accounts do |t| | ||||
|       t.belongs_to :list, foreign_key: { on_delete: :cascade }, null: false | ||||
|       t.belongs_to :account, foreign_key: { on_delete: :cascade }, null: false | ||||
|       t.belongs_to :follow, foreign_key: { on_delete: :cascade }, null: false | ||||
|     end | ||||
| 
 | ||||
|     add_index :list_accounts, [:account_id, :list_id], unique: true | ||||
|     add_index :list_accounts, [:list_id, :account_id] | ||||
|   end | ||||
| end | ||||
							
								
								
									
										26
									
								
								db/schema.rb
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								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: 20171114080328) do | ||||
| ActiveRecord::Schema.define(version: 20171116161857) do | ||||
| 
 | ||||
|   # These are extensions that must be enabled in order to support this database | ||||
|   enable_extension "plpgsql" | ||||
|  | @ -181,6 +181,25 @@ ActiveRecord::Schema.define(version: 20171114080328) do | |||
|     t.bigint "account_id", null: false | ||||
|   end | ||||
| 
 | ||||
|   create_table "list_accounts", force: :cascade do |t| | ||||
|     t.bigint "list_id", null: false | ||||
|     t.bigint "account_id", null: false | ||||
|     t.bigint "follow_id", null: false | ||||
|     t.index ["account_id", "list_id"], name: "index_list_accounts_on_account_id_and_list_id", unique: true | ||||
|     t.index ["account_id"], name: "index_list_accounts_on_account_id" | ||||
|     t.index ["follow_id"], name: "index_list_accounts_on_follow_id" | ||||
|     t.index ["list_id", "account_id"], name: "index_list_accounts_on_list_id_and_account_id" | ||||
|     t.index ["list_id"], name: "index_list_accounts_on_list_id" | ||||
|   end | ||||
| 
 | ||||
|   create_table "lists", force: :cascade do |t| | ||||
|     t.bigint "account_id" | ||||
|     t.string "title", default: "", null: false | ||||
|     t.datetime "created_at", null: false | ||||
|     t.datetime "updated_at", null: false | ||||
|     t.index ["account_id"], name: "index_lists_on_account_id" | ||||
|   end | ||||
| 
 | ||||
|   create_table "media_attachments", force: :cascade do |t| | ||||
|     t.bigint "status_id" | ||||
|     t.string "file_file_name" | ||||
|  | @ -215,7 +234,6 @@ ActiveRecord::Schema.define(version: 20171114080328) do | |||
|     t.boolean "hide_notifications", default: true, null: false | ||||
|     t.bigint "account_id", null: false | ||||
|     t.bigint "target_account_id", null: false | ||||
|     t.boolean "hide_notifications", default: true, null: false | ||||
|     t.index ["account_id", "target_account_id"], name: "index_mutes_on_account_id_and_target_account_id", unique: true | ||||
|   end | ||||
| 
 | ||||
|  | @ -491,6 +509,10 @@ ActiveRecord::Schema.define(version: 20171114080328) do | |||
|   add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade | ||||
|   add_foreign_key "glitch_keyword_mutes", "accounts", on_delete: :cascade | ||||
|   add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade | ||||
|   add_foreign_key "list_accounts", "accounts", on_delete: :cascade | ||||
|   add_foreign_key "list_accounts", "follows", on_delete: :cascade | ||||
|   add_foreign_key "list_accounts", "lists", on_delete: :cascade | ||||
|   add_foreign_key "lists", "accounts", on_delete: :cascade | ||||
|   add_foreign_key "media_attachments", "accounts", name: "fk_96dd81e81b", on_delete: :nullify | ||||
|   add_foreign_key "media_attachments", "statuses", on_delete: :nullify | ||||
|   add_foreign_key "mentions", "accounts", name: "fk_970d43f9d1", on_delete: :cascade | ||||
|  |  | |||
							
								
								
									
										54
									
								
								spec/controllers/api/v1/lists/accounts_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								spec/controllers/api/v1/lists/accounts_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,54 @@ | |||
| require 'rails_helper' | ||||
| 
 | ||||
| describe Api::V1::Lists::AccountsController do | ||||
|   render_views | ||||
| 
 | ||||
|   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } | ||||
|   let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') } | ||||
|   let(:list)  { Fabricate(:list, account: user.account) } | ||||
| 
 | ||||
|   before do | ||||
|     follow = Fabricate(:follow, account: user.account) | ||||
|     list.accounts << follow.target_account | ||||
|     allow(controller).to receive(:doorkeeper_token) { token } | ||||
|   end | ||||
| 
 | ||||
|   describe 'GET #index' do | ||||
|     it 'returns http success' do | ||||
|       get :show, params: { list_id: list.id } | ||||
| 
 | ||||
|       expect(response).to have_http_status(:success) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe 'POST #create' do | ||||
|     let(:bob) { Fabricate(:account, username: 'bob') } | ||||
| 
 | ||||
|     before do | ||||
|       user.account.follow!(bob) | ||||
|       post :create, params: { list_id: list.id, account_ids: [bob.id] } | ||||
|     end | ||||
| 
 | ||||
|     it 'returns http success' do | ||||
|       expect(response).to have_http_status(:success) | ||||
|     end | ||||
| 
 | ||||
|     it 'adds account to the list' do | ||||
|       expect(list.accounts.include?(bob)).to be true | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe 'DELETE #destroy' do | ||||
|     before do | ||||
|       delete :destroy, params: { list_id: list.id, account_ids: [list.accounts.first.id] } | ||||
|     end | ||||
| 
 | ||||
|     it 'returns http success' do | ||||
|       expect(response).to have_http_status(:success) | ||||
|     end | ||||
| 
 | ||||
|     it 'removes account from the list' do | ||||
|       expect(list.accounts.count).to eq 0 | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										68
									
								
								spec/controllers/api/v1/lists_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								spec/controllers/api/v1/lists_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,68 @@ | |||
| require 'rails_helper' | ||||
| 
 | ||||
| RSpec.describe Api::V1::ListsController, type: :controller do | ||||
|   render_views | ||||
| 
 | ||||
|   let!(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } | ||||
|   let!(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') } | ||||
|   let!(:list)  { Fabricate(:list, account: user.account) } | ||||
| 
 | ||||
|   before { allow(controller).to receive(:doorkeeper_token) { token } } | ||||
| 
 | ||||
|   describe 'GET #index' do | ||||
|     it 'returns http success' do | ||||
|       get :index | ||||
|       expect(response).to have_http_status(:success) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe 'GET #show' do | ||||
|     it 'returns http success' do | ||||
|       get :show, params: { id: list.id } | ||||
|       expect(response).to have_http_status(:success) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe 'POST #create' do | ||||
|     before do | ||||
|       post :create, params: { title: 'Foo bar' } | ||||
|     end | ||||
| 
 | ||||
|     it 'returns http success' do | ||||
|       expect(response).to have_http_status(:success) | ||||
|     end | ||||
| 
 | ||||
|     it 'creates list' do | ||||
|       expect(List.where(account: user.account).count).to eq 2 | ||||
|       expect(List.last.title).to eq 'Foo bar' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe 'PUT #update' do | ||||
|     before do | ||||
|       put :update, params: { id: list.id, title: 'Updated title' } | ||||
|     end | ||||
| 
 | ||||
|     it 'returns http success' do | ||||
|       expect(response).to have_http_status(:success) | ||||
|     end | ||||
| 
 | ||||
|     it 'updates the list' do | ||||
|       expect(list.reload.title).to eq 'Updated title' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe 'DELETE #destroy' do | ||||
|     before do | ||||
|       delete :destroy, params: { id: list.id } | ||||
|     end | ||||
| 
 | ||||
|     it 'returns http success' do | ||||
|       expect(response).to have_http_status(:success) | ||||
|     end | ||||
| 
 | ||||
|     it 'deletes the list' do | ||||
|       expect(List.find_by(id: list.id)).to be_nil | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										56
									
								
								spec/controllers/api/v1/timelines/list_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								spec/controllers/api/v1/timelines/list_controller_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,56 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'rails_helper' | ||||
| 
 | ||||
| describe Api::V1::Timelines::ListController do | ||||
|   render_views | ||||
| 
 | ||||
|   let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } | ||||
|   let(:list) { Fabricate(:list, account: user.account) } | ||||
| 
 | ||||
|   before do | ||||
|     allow(controller).to receive(:doorkeeper_token) { token } | ||||
|   end | ||||
| 
 | ||||
|   context 'with a user context' do | ||||
|     let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') } | ||||
| 
 | ||||
|     describe 'GET #show' do | ||||
|       before do | ||||
|         follow = Fabricate(:follow, account: user.account) | ||||
|         list.accounts << follow.target_account | ||||
|         PostStatusService.new.call(follow.target_account, 'New status for user home timeline.') | ||||
|       end | ||||
| 
 | ||||
|       it 'returns http success' do | ||||
|         get :show, params: { id: list.id } | ||||
|         expect(response).to have_http_status(:success) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'with the wrong user context' do | ||||
|     let(:other_user) { Fabricate(:user, account: Fabricate(:account, username: 'bob')) } | ||||
|     let(:token)      { Fabricate(:accessible_access_token, resource_owner_id: other_user.id, scopes: 'read') } | ||||
| 
 | ||||
|     describe 'GET #show' do | ||||
|       it 'returns http not found' do | ||||
|         get :show, params: { id: list.id } | ||||
|         expect(response).to have_http_status(:not_found) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'without a user context' do | ||||
|     let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: 'read') } | ||||
| 
 | ||||
|     describe 'GET #show' do | ||||
|       it 'returns http unprocessable entity' do | ||||
|         get :show, params: { id: list.id } | ||||
| 
 | ||||
|         expect(response).to have_http_status(:unprocessable_entity) | ||||
|         expect(response.headers['Link']).to be_nil | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -5,7 +5,7 @@ require 'rails_helper' | |||
| describe Api::V1::Timelines::TagController do | ||||
|   render_views | ||||
| 
 | ||||
|   let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } | ||||
|   let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } | ||||
| 
 | ||||
|   before do | ||||
|     allow(controller).to receive(:doorkeeper_token) { token } | ||||
|  |  | |||
							
								
								
									
										5
									
								
								spec/fabricators/list_account_fabricator.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								spec/fabricators/list_account_fabricator.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| Fabricator(:list_account) do | ||||
|   list    nil | ||||
|   account nil | ||||
|   follow  nil | ||||
| end | ||||
							
								
								
									
										4
									
								
								spec/fabricators/list_fabricator.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								spec/fabricators/list_fabricator.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | |||
| Fabricator(:list) do | ||||
|   account nil | ||||
|   title   "MyString" | ||||
| end | ||||
|  | @ -207,21 +207,11 @@ RSpec.describe FeedManager do | |||
|       account = Fabricate(:account) | ||||
|       status = Fabricate(:status) | ||||
|       members = FeedManager::MAX_ITEMS.times.map { |count| [count, count] } | ||||
|       Redis.current.zadd("feed:type:#{account.id}", members) | ||||
|       Redis.current.zadd("feed:home:#{account.id}", members) | ||||
| 
 | ||||
|       FeedManager.instance.push('type', account, status) | ||||
|       FeedManager.instance.push_to_home(account, status) | ||||
| 
 | ||||
|       expect(Redis.current.zcard("feed:type:#{account.id}")).to eq FeedManager::MAX_ITEMS | ||||
|     end | ||||
| 
 | ||||
|     it 'sends push updates for non-home timelines' do | ||||
|       account = Fabricate(:account) | ||||
|       status = Fabricate(:status) | ||||
|       allow(Redis.current).to receive_messages(publish: nil) | ||||
| 
 | ||||
|       FeedManager.instance.push('type', account, status) | ||||
| 
 | ||||
|       expect(Redis.current).to have_received(:publish).with("timeline:#{account.id}", any_args).at_least(:once) | ||||
|       expect(Redis.current.zcard("feed:home:#{account.id}")).to eq FeedManager::MAX_ITEMS | ||||
|     end | ||||
| 
 | ||||
|     context 'reblogs' do | ||||
|  | @ -230,7 +220,7 @@ RSpec.describe FeedManager do | |||
|         reblogged = Fabricate(:status) | ||||
|         reblog = Fabricate(:status, reblog: reblogged) | ||||
| 
 | ||||
|         expect(FeedManager.instance.push('type', account, reblog)).to be true | ||||
|         expect(FeedManager.instance.push_to_home(account, reblog)).to be true | ||||
|       end | ||||
| 
 | ||||
|       it 'does not save a new reblog of a recent status' do | ||||
|  | @ -238,9 +228,9 @@ RSpec.describe FeedManager do | |||
|         reblogged = Fabricate(:status) | ||||
|         reblog = Fabricate(:status, reblog: reblogged) | ||||
| 
 | ||||
|         FeedManager.instance.push('type', account, reblogged) | ||||
|         FeedManager.instance.push_to_home(account, reblogged) | ||||
| 
 | ||||
|         expect(FeedManager.instance.push('type', account, reblog)).to be false | ||||
|         expect(FeedManager.instance.push_to_home(account, reblog)).to be false | ||||
|       end | ||||
| 
 | ||||
|       it 'saves a new reblog of an old status' do | ||||
|  | @ -248,14 +238,14 @@ RSpec.describe FeedManager do | |||
|         reblogged = Fabricate(:status) | ||||
|         reblog = Fabricate(:status, reblog: reblogged) | ||||
| 
 | ||||
|         FeedManager.instance.push('type', account, reblogged) | ||||
|         FeedManager.instance.push_to_home(account, reblogged) | ||||
| 
 | ||||
|         # Fill the feed with intervening statuses | ||||
|         FeedManager::REBLOG_FALLOFF.times do | ||||
|           FeedManager.instance.push('type', account, Fabricate(:status)) | ||||
|           FeedManager.instance.push_to_home(account, Fabricate(:status)) | ||||
|         end | ||||
| 
 | ||||
|         expect(FeedManager.instance.push('type', account, reblog)).to be true | ||||
|         expect(FeedManager.instance.push_to_home(account, reblog)).to be true | ||||
|       end | ||||
| 
 | ||||
|       it 'does not save a new reblog of a recently-reblogged status' do | ||||
|  | @ -264,10 +254,10 @@ RSpec.describe FeedManager do | |||
|         reblogs = 2.times.map { Fabricate(:status, reblog: reblogged) } | ||||
| 
 | ||||
|         # The first reblog will be accepted | ||||
|         FeedManager.instance.push('type', account, reblogs.first) | ||||
|         FeedManager.instance.push_to_home(account, reblogs.first) | ||||
| 
 | ||||
|         # The second reblog should be ignored | ||||
|         expect(FeedManager.instance.push('type', account, reblogs.last)).to be false | ||||
|         expect(FeedManager.instance.push_to_home(account, reblogs.last)).to be false | ||||
|       end | ||||
| 
 | ||||
|       it 'does not save a new reblog of a multiply-reblogged-then-unreblogged status' do | ||||
|  | @ -276,14 +266,14 @@ RSpec.describe FeedManager do | |||
|         reblogs = 3.times.map { Fabricate(:status, reblog: reblogged) } | ||||
| 
 | ||||
|         # Accept the reblogs | ||||
|         FeedManager.instance.push('type', account, reblogs[0]) | ||||
|         FeedManager.instance.push('type', account, reblogs[1]) | ||||
|         FeedManager.instance.push_to_home(account, reblogs[0]) | ||||
|         FeedManager.instance.push_to_home(account, reblogs[1]) | ||||
| 
 | ||||
|         # Unreblog the first one | ||||
|         FeedManager.instance.unpush('type', account, reblogs[0]) | ||||
|         FeedManager.instance.unpush_from_home(account, reblogs[0]) | ||||
| 
 | ||||
|         # The last reblog should still be ignored | ||||
|         expect(FeedManager.instance.push('type', account, reblogs.last)).to be false | ||||
|         expect(FeedManager.instance.push_to_home(account, reblogs.last)).to be false | ||||
|       end | ||||
| 
 | ||||
|       it 'saves a new reblog of a long-ago-reblogged status' do | ||||
|  | @ -292,15 +282,15 @@ RSpec.describe FeedManager do | |||
|         reblogs = 2.times.map { Fabricate(:status, reblog: reblogged) } | ||||
| 
 | ||||
|         # The first reblog will be accepted | ||||
|         FeedManager.instance.push('type', account, reblogs.first) | ||||
|         FeedManager.instance.push_to_home(account, reblogs.first) | ||||
| 
 | ||||
|         # Fill the feed with intervening statuses | ||||
|         FeedManager::REBLOG_FALLOFF.times do | ||||
|           FeedManager.instance.push('type', account, Fabricate(:status)) | ||||
|           FeedManager.instance.push_to_home(account, Fabricate(:status)) | ||||
|         end | ||||
| 
 | ||||
|         # The second reblog should also be accepted | ||||
|         expect(FeedManager.instance.push('type', account, reblogs.last)).to be true | ||||
|         expect(FeedManager.instance.push_to_home(account, reblogs.last)).to be true | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | @ -312,11 +302,11 @@ RSpec.describe FeedManager do | |||
|       reblogged      = Fabricate(:status) | ||||
|       status         = Fabricate(:status, reblog: reblogged) | ||||
|       another_status = Fabricate(:status, reblog: reblogged) | ||||
|       reblogs_key    = FeedManager.instance.key('type', receiver.id, 'reblogs') | ||||
|       reblog_set_key = FeedManager.instance.key('type', receiver.id, "reblogs:#{reblogged.id}") | ||||
|       reblogs_key    = FeedManager.instance.key('home', receiver.id, 'reblogs') | ||||
|       reblog_set_key = FeedManager.instance.key('home', receiver.id, "reblogs:#{reblogged.id}") | ||||
| 
 | ||||
|       FeedManager.instance.push('type', receiver, status) | ||||
|       FeedManager.instance.push('type', receiver, another_status) | ||||
|       FeedManager.instance.push_to_home(receiver, status) | ||||
|       FeedManager.instance.push_to_home(receiver, another_status) | ||||
| 
 | ||||
|       # We should have a tracking set and an entry in reblogs. | ||||
|       expect(Redis.current.exists(reblog_set_key)).to be true | ||||
|  | @ -324,12 +314,12 @@ RSpec.describe FeedManager do | |||
| 
 | ||||
|       # Push everything off the end of the feed. | ||||
|       FeedManager::MAX_ITEMS.times do | ||||
|         FeedManager.instance.push('type', receiver, Fabricate(:status)) | ||||
|         FeedManager.instance.push_to_home(receiver, Fabricate(:status)) | ||||
|       end | ||||
| 
 | ||||
|       # `trim` should be called automatically, but do it anyway, as | ||||
|       # we're testing `trim`, not side effects of `push`. | ||||
|       FeedManager.instance.trim('type', receiver.id) | ||||
|       FeedManager.instance.trim('home', receiver.id) | ||||
| 
 | ||||
|       # We should not have any reblog tracking data. | ||||
|       expect(Redis.current.exists(reblog_set_key)).to be false | ||||
|  | @ -344,32 +334,32 @@ RSpec.describe FeedManager do | |||
|       reblogged = Fabricate(:status) | ||||
|       status    = Fabricate(:status, reblog: reblogged) | ||||
| 
 | ||||
|       FeedManager.instance.push('type', receiver, reblogged) | ||||
|       FeedManager::REBLOG_FALLOFF.times { FeedManager.instance.push('type', receiver, Fabricate(:status)) } | ||||
|       FeedManager.instance.push('type', receiver, status) | ||||
|       FeedManager.instance.push_to_home(receiver, reblogged) | ||||
|       FeedManager::REBLOG_FALLOFF.times { FeedManager.instance.push_to_home(receiver, Fabricate(:status)) } | ||||
|       FeedManager.instance.push_to_home(receiver, status) | ||||
| 
 | ||||
|       # The reblogging status should show up under normal conditions. | ||||
|       expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to include(status.id.to_s) | ||||
|       expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s) | ||||
| 
 | ||||
|       FeedManager.instance.unpush('type', receiver, status) | ||||
|       FeedManager.instance.unpush_from_home(receiver, status) | ||||
| 
 | ||||
|       # Restore original status | ||||
|       expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to_not include(status.id.to_s) | ||||
|       expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to include(reblogged.id.to_s) | ||||
|       expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s) | ||||
|       expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to include(reblogged.id.to_s) | ||||
|     end | ||||
| 
 | ||||
|     it 'removes a reblogged status if it was only reblogged once' do | ||||
|       reblogged = Fabricate(:status) | ||||
|       status    = Fabricate(:status, reblog: reblogged) | ||||
| 
 | ||||
|       FeedManager.instance.push('type', receiver, status) | ||||
|       FeedManager.instance.push_to_home(receiver, status) | ||||
| 
 | ||||
|       # The reblogging status should show up under normal conditions. | ||||
|       expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to eq [status.id.to_s] | ||||
|       expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [status.id.to_s] | ||||
| 
 | ||||
|       FeedManager.instance.unpush('type', receiver, status) | ||||
|       FeedManager.instance.unpush_from_home(receiver, status) | ||||
| 
 | ||||
|       expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to be_empty | ||||
|       expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to be_empty | ||||
|     end | ||||
| 
 | ||||
|     it 'leaves a multiply-reblogged status if another reblog was in feed' do | ||||
|  | @ -377,26 +367,26 @@ RSpec.describe FeedManager do | |||
|       reblogs   = 3.times.map { Fabricate(:status, reblog: reblogged) } | ||||
| 
 | ||||
|       reblogs.each do |reblog| | ||||
|         FeedManager.instance.push('type', receiver, reblog) | ||||
|         FeedManager.instance.push_to_home(receiver, reblog) | ||||
|       end | ||||
| 
 | ||||
|       # The reblogging status should show up under normal conditions. | ||||
|       expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to eq [reblogs.first.id.to_s] | ||||
|       expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.first.id.to_s] | ||||
| 
 | ||||
|       reblogs[0...-1].each do |reblog| | ||||
|         FeedManager.instance.unpush('type', receiver, reblog) | ||||
|         FeedManager.instance.unpush_from_home(receiver, reblog) | ||||
|       end | ||||
| 
 | ||||
|       expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to eq [reblogs.last.id.to_s] | ||||
|       expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.last.id.to_s] | ||||
|     end | ||||
| 
 | ||||
|     it 'sends push updates' do | ||||
|       status  = Fabricate(:status) | ||||
| 
 | ||||
|       FeedManager.instance.push('type', receiver, status) | ||||
|       FeedManager.instance.push_to_home(receiver, status) | ||||
| 
 | ||||
|       allow(Redis.current).to receive_messages(publish: nil) | ||||
|       FeedManager.instance.unpush('type', receiver, status) | ||||
|       FeedManager.instance.unpush_from_home(receiver, status) | ||||
| 
 | ||||
|       deletion = Oj.dump(event: :delete, payload: status.id.to_s) | ||||
|       expect(Redis.current).to have_received(:publish).with("timeline:#{receiver.id}", deletion) | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| require 'rails_helper' | ||||
| 
 | ||||
| RSpec.describe AccountModerationNote, type: :model do | ||||
|   pending "add some examples to (or delete) #{__FILE__}" | ||||
| 
 | ||||
| end | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| require 'rails_helper' | ||||
| 
 | ||||
| RSpec.describe Feed, type: :model do | ||||
| RSpec.describe HomeFeed, type: :model do | ||||
|   let(:account) { Fabricate(:account) } | ||||
| 
 | ||||
|   subject { described_class.new(:home, account) } | ||||
|   subject { described_class.new(account) } | ||||
| 
 | ||||
|   describe '#get' do | ||||
|     before do | ||||
							
								
								
									
										5
									
								
								spec/models/list_account_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								spec/models/list_account_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| require 'rails_helper' | ||||
| 
 | ||||
| RSpec.describe ListAccount, type: :model do | ||||
| 
 | ||||
| end | ||||
							
								
								
									
										5
									
								
								spec/models/list_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								spec/models/list_spec.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| require 'rails_helper' | ||||
| 
 | ||||
| RSpec.describe List, type: :model do | ||||
| 
 | ||||
| end | ||||
|  | @ -18,8 +18,8 @@ RSpec.describe AfterBlockService do | |||
|     end | ||||
| 
 | ||||
|     it "clears account's statuses" do | ||||
|       FeedManager.instance.push(:home, account, status) | ||||
|       FeedManager.instance.push(:home, account, other_account_status) | ||||
|       FeedManager.instance.push_to_home(account, status) | ||||
|       FeedManager.instance.push_to_home(account, other_account_status) | ||||
| 
 | ||||
|       is_expected.to change { | ||||
|         Redis.current.zrange(home_timeline_key, 0, -1) | ||||
|  |  | |||
|  | @ -30,11 +30,11 @@ RSpec.describe BatchedRemoveStatusService do | |||
|   end | ||||
| 
 | ||||
|   it 'removes statuses from author\'s home feed' do | ||||
|     expect(Feed.new(:home, alice).get(10)).to_not include([status1.id, status2.id]) | ||||
|     expect(HomeFeed.new(alice).get(10)).to_not include([status1.id, status2.id]) | ||||
|   end | ||||
| 
 | ||||
|   it 'removes statuses from local follower\'s home feed' do | ||||
|     expect(Feed.new(:home, jeff).get(10)).to_not include([status1.id, status2.id]) | ||||
|     expect(HomeFeed.new(jeff).get(10)).to_not include([status1.id, status2.id]) | ||||
|   end | ||||
| 
 | ||||
|   it 'notifies streaming API of followers' do | ||||
|  |  | |||
|  | @ -19,12 +19,12 @@ RSpec.describe FanOutOnWriteService do | |||
|   end | ||||
| 
 | ||||
|   it 'delivers status to home timeline' do | ||||
|     expect(Feed.new(:home, author).get(10).map(&:id)).to include status.id | ||||
|     expect(HomeFeed.new(author).get(10).map(&:id)).to include status.id | ||||
|   end | ||||
| 
 | ||||
|   it 'delivers status to local followers' do | ||||
|     pending 'some sort of problem in test environment causes this to sometimes fail' | ||||
|     expect(Feed.new(:home, follower).get(10).map(&:id)).to include status.id | ||||
|     expect(HomeFeed.new(follower).get(10).map(&:id)).to include status.id | ||||
|   end | ||||
| 
 | ||||
|   it 'delivers status to hashtag' do | ||||
|  |  | |||
|  | @ -18,8 +18,8 @@ RSpec.describe MuteService do | |||
|     end | ||||
| 
 | ||||
|     it "clears account's statuses" do | ||||
|       FeedManager.instance.push(:home, account, status) | ||||
|       FeedManager.instance.push(:home, account, other_account_status) | ||||
|       FeedManager.instance.push_to_home(account, status) | ||||
|       FeedManager.instance.push_to_home(account, other_account_status) | ||||
| 
 | ||||
|       is_expected.to change { | ||||
|         Redis.current.zrange(home_timeline_key, 0, -1) | ||||
|  |  | |||
|  | @ -25,11 +25,11 @@ RSpec.describe RemoveStatusService do | |||
|   end | ||||
| 
 | ||||
|   it 'removes status from author\'s home feed' do | ||||
|     expect(Feed.new(:home, alice).get(10)).to_not include(@status.id) | ||||
|     expect(HomeFeed.new(alice).get(10)).to_not include(@status.id) | ||||
|   end | ||||
| 
 | ||||
|   it 'removes status from local follower\'s home feed' do | ||||
|     expect(Feed.new(:home, jeff).get(10)).to_not include(@status.id) | ||||
|     expect(HomeFeed.new(jeff).get(10)).to_not include(@status.id) | ||||
|   end | ||||
| 
 | ||||
|   it 'sends PuSH update to PuSH subscribers' do | ||||
|  |  | |||
|  | @ -11,41 +11,41 @@ describe FeedInsertWorker do | |||
| 
 | ||||
|     context 'when there are no records' do | ||||
|       it 'skips push with missing status' do | ||||
|         instance = double(push: nil) | ||||
|         instance = double(push_to_home: nil) | ||||
|         allow(FeedManager).to receive(:instance).and_return(instance) | ||||
|         result = subject.perform(nil, follower.id) | ||||
| 
 | ||||
|         expect(result).to eq true | ||||
|         expect(instance).not_to have_received(:push) | ||||
|         expect(instance).not_to have_received(:push_to_home) | ||||
|       end | ||||
| 
 | ||||
|       it 'skips push with missing account' do | ||||
|         instance = double(push: nil) | ||||
|         instance = double(push_to_home: nil) | ||||
|         allow(FeedManager).to receive(:instance).and_return(instance) | ||||
|         result = subject.perform(status.id, nil) | ||||
| 
 | ||||
|         expect(result).to eq true | ||||
|         expect(instance).not_to have_received(:push) | ||||
|         expect(instance).not_to have_received(:push_to_home) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when there are real records' do | ||||
|       it 'skips the push when there is a filter' do | ||||
|         instance = double(push: nil, filter?: true) | ||||
|         instance = double(push_to_home: nil, filter?: true) | ||||
|         allow(FeedManager).to receive(:instance).and_return(instance) | ||||
|         result = subject.perform(status.id, follower.id) | ||||
| 
 | ||||
|         expect(result).to be_nil | ||||
|         expect(instance).not_to have_received(:push) | ||||
|         expect(instance).not_to have_received(:push_to_home) | ||||
|       end | ||||
| 
 | ||||
|       it 'pushes the status onto the home timeline without filter' do | ||||
|         instance = double(push: nil, filter?: false) | ||||
|         instance = double(push_to_home: nil, filter?: false) | ||||
|         allow(FeedManager).to receive(:instance).and_return(instance) | ||||
|         result = subject.perform(status.id, follower.id) | ||||
| 
 | ||||
|         expect(result).to be_nil | ||||
|         expect(instance).to have_received(:push).with(:home, follower, status) | ||||
|         expect(instance).to have_received(:push_to_home).with(follower, status) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -254,6 +254,26 @@ const startWorker = (workerId) => { | |||
| 
 | ||||
|   const placeholders = (arr, shift = 0) => arr.map((_, i) => `$${i + 1 + shift}`).join(', '); | ||||
| 
 | ||||
|   const authorizeListAccess = (id, req, next) => { | ||||
|     pgPool.connect((err, client, done) => { | ||||
|       if (err) { | ||||
|         next(false); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       client.query('SELECT id, account_id FROM lists WHERE id = $1 LIMIT 1', [id], (err, result) => { | ||||
|         done(); | ||||
| 
 | ||||
|         if (err || result.rows.length === 0 || result.rows[0].account_id !== req.accountId) { | ||||
|           next(false); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         next(true); | ||||
|       }); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   const streamFrom = (id, req, output, attachCloseHandler, needsFiltering = false, notificationOnly = false) => { | ||||
|     const streamType = notificationOnly ? ' (notification)' : ''; | ||||
|     log.verbose(req.requestId, `Starting stream from ${id} for ${req.accountId}${streamType}`); | ||||
|  | @ -414,7 +434,22 @@ const startWorker = (workerId) => { | |||
|     streamFrom(`timeline:hashtag:${req.query.tag.toLowerCase()}:local`, req, streamToHttp(req, res), streamHttpEnd(req), true); | ||||
|   }); | ||||
| 
 | ||||
|   const wss    = new WebSocket.Server({ server, verifyClient: wsVerifyClient }); | ||||
|   app.get('/api/v1/streaming/list', (req, res) => { | ||||
|     const listId = req.query.list; | ||||
| 
 | ||||
|     authorizeListAccess(listId, req, authorized => { | ||||
|       if (!authorized) { | ||||
|         res.writeHead(404, { 'Content-Type': 'application/json' }); | ||||
|         res.end(JSON.stringify({ error: 'Not found' })); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       const channel = `timeline:list:${listId}`; | ||||
|       streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req, subscriptionHeartbeat(channel))); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   const wss = new WebSocket.Server({ server, verifyClient: wsVerifyClient }); | ||||
| 
 | ||||
|   wss.on('connection', ws => { | ||||
|     const req      = ws.upgradeReq; | ||||
|  | @ -450,6 +485,19 @@ const startWorker = (workerId) => { | |||
|     case 'hashtag:local': | ||||
|       streamFrom(`timeline:hashtag:${location.query.tag.toLowerCase()}:local`, req, streamToWs(req, ws), streamWsEnd(req, ws), true); | ||||
|       break; | ||||
|     case 'list': | ||||
|       const listId = location.query.list; | ||||
| 
 | ||||
|       authorizeListAccess(listId, req, authorized => { | ||||
|         if (!authorized) { | ||||
|           ws.close(); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         const channel = `timeline:list:${listId}`; | ||||
|         streamFrom(channel, req, streamToWs(req, ws), streamWsEnd(req, ws, subscriptionHeartbeat(channel))); | ||||
|       }); | ||||
|       break; | ||||
|     default: | ||||
|       ws.close(); | ||||
|     } | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue