API pagination for all collections using Link header
This commit is contained in:
		| @@ -4,7 +4,7 @@ class Api::V1::AccountsController < ApiController | ||||
|   before_action :require_user!, except: [:show, :following, :followers, :statuses] | ||||
|   before_action :set_account, except: [:verify_credentials, :suggestions] | ||||
|  | ||||
|   respond_to    :json | ||||
|   respond_to :json | ||||
|  | ||||
|   def show | ||||
|   end | ||||
| @@ -15,12 +15,26 @@ class Api::V1::AccountsController < ApiController | ||||
|   end | ||||
|  | ||||
|   def following | ||||
|     @accounts = @account.following.with_counters.limit(40) | ||||
|     results   = Follow.where(account: @account).paginate_by_max_id(DEFAULT_ACCOUNTS_LIMIT, params[:max_id], params[:since_id]) | ||||
|     @accounts = Account.where(id: results.map(&:account_id)).with_counters.to_a | ||||
|  | ||||
|     next_path = following_api_v1_account_url(max_id: results.last.id)    if results.size == DEFAULT_ACCOUNTS_LIMIT | ||||
|     prev_path = following_api_v1_account_url(since_id: results.first.id) if results.size > 0 | ||||
|  | ||||
|     set_pagination_headers(next_path, prev_path) | ||||
|  | ||||
|     render action: :index | ||||
|   end | ||||
|  | ||||
|   def followers | ||||
|     @accounts = @account.followers.with_counters.limit(40) | ||||
|     results   = Follow.where(target_account: @account).paginate_by_max_id(DEFAULT_ACCOUNTS_LIMIT, params[:max_id], params[:since_id]) | ||||
|     @accounts = Account.where(id: results.map(&:account_id)).with_counters.to_a | ||||
|  | ||||
|     next_path = following_api_v1_account_url(max_id: results.last.id)    if results.size == DEFAULT_ACCOUNTS_LIMIT | ||||
|     prev_path = following_api_v1_account_url(since_id: results.first.id) if results.size > 0 | ||||
|  | ||||
|     set_pagination_headers(next_path, prev_path) | ||||
|  | ||||
|     render action: :index | ||||
|   end | ||||
|  | ||||
| @@ -35,8 +49,14 @@ class Api::V1::AccountsController < ApiController | ||||
|   end | ||||
|  | ||||
|   def statuses | ||||
|     @statuses = @account.statuses.with_includes.with_counters.paginate_by_max_id(20, params[:max_id], params[:since_id]).to_a | ||||
|     @statuses = @account.statuses.with_includes.with_counters.paginate_by_max_id(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id]).to_a | ||||
|  | ||||
|     set_maps(@statuses) | ||||
|  | ||||
|     next_path = statuses_api_v1_account_url(max_id: @statuses.last.id)    if @statuses.size == DEFAULT_STATUSES_LIMIT | ||||
|     prev_path = statuses_api_v1_account_url(since_id: @statuses.first.id) if @statuses.size > 0 | ||||
|  | ||||
|     set_pagination_headers(next_path, prev_path) | ||||
|   end | ||||
|  | ||||
|   def follow | ||||
|   | ||||
| @@ -2,7 +2,7 @@ class Api::V1::FollowsController < ApiController | ||||
|   before_action -> { doorkeeper_authorize! :follow } | ||||
|   before_action :require_user! | ||||
|  | ||||
|   respond_to    :json | ||||
|   respond_to :json | ||||
|  | ||||
|   def create | ||||
|     raise ActiveRecord::RecordNotFound if params[:uri].blank? | ||||
|   | ||||
| @@ -2,7 +2,7 @@ class Api::V1::MediaController < ApiController | ||||
|   before_action -> { doorkeeper_authorize! :write } | ||||
|   before_action :require_user! | ||||
|  | ||||
|   respond_to    :json | ||||
|   respond_to :json | ||||
|  | ||||
|   def create | ||||
|     @media = MediaAttachment.create!(account: current_user.account, file: params[:file]) | ||||
|   | ||||
| @@ -15,12 +15,26 @@ class Api::V1::StatusesController < ApiController | ||||
|   end | ||||
|  | ||||
|   def reblogged_by | ||||
|     @accounts = @status.reblogged_by(40) | ||||
|     results   = @status.reblogs.paginate_by_max_id(DEFAULT_ACCOUNTS_LIMIT, params[:max_id], params[:since_id]) | ||||
|     @accounts = Account.where(id: results.map(&:account_id)).with_counters.to_a | ||||
|  | ||||
|     next_path = reblogged_by_api_v1_status_url(max_id: results.last.id)    if results.size == DEFAULT_ACCOUNTS_LIMIT | ||||
|     prev_path = reblogged_by_api_v1_status_url(since_id: results.first.id) if results.size > 0 | ||||
|  | ||||
|     set_pagination_headers(next_path, prev_path) | ||||
|  | ||||
|     render action: :accounts | ||||
|   end | ||||
|  | ||||
|   def favourited_by | ||||
|     @accounts = @status.favourited_by(40) | ||||
|     results   = @status.favourites.paginate_by_max_id(DEFAULT_ACCOUNTS_LIMIT, params[:max_id], params[:since_id]) | ||||
|     @accounts = Account.where(id: results.map(&:account_id)).with_counters.to_a | ||||
|  | ||||
|     next_path = favourited_by_api_v1_status_url(max_id: results.last.id)    if results.size == DEFAULT_ACCOUNTS_LIMIT | ||||
|     prev_path = favourited_by_api_v1_status_url(since_id: results.first.id) if results.size > 0 | ||||
|  | ||||
|     set_pagination_headers(next_path, prev_path) | ||||
|  | ||||
|     render action: :accounts | ||||
|   end | ||||
|  | ||||
|   | ||||
| @@ -5,32 +5,54 @@ class Api::V1::TimelinesController < ApiController | ||||
|   respond_to :json | ||||
|  | ||||
|   def home | ||||
|     @statuses = Feed.new(:home, current_account).get(20, params[:max_id], params[:since_id]).to_a | ||||
|     @statuses = Feed.new(:home, current_account).get(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id]).to_a | ||||
|  | ||||
|     set_maps(@statuses) | ||||
|  | ||||
|     next_path = api_v1_home_timeline_url(max_id: @statuses.last.id)    if @statuses.size == DEFAULT_STATUSES_LIMIT | ||||
|     prev_path = api_v1_home_timeline_url(since_id: @statuses.first.id) if @statuses.size > 0 | ||||
|  | ||||
|     set_pagination_headers(next_path, prev_path) | ||||
|  | ||||
|     render action: :index | ||||
|   end | ||||
|  | ||||
|   def mentions | ||||
|     @statuses = Feed.new(:mentions, current_account).get(20, params[:max_id], params[:since_id]).to_a | ||||
|     @statuses = Feed.new(:mentions, current_account).get(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id]).to_a | ||||
|  | ||||
|     set_maps(@statuses) | ||||
|  | ||||
|     next_path = api_v1_mentions_timeline_url(max_id: @statuses.last.id)    if @statuses.size == DEFAULT_STATUSES_LIMIT | ||||
|     prev_path = api_v1_mentions_timeline_url(since_id: @statuses.first.id) if @statuses.size > 0 | ||||
|  | ||||
|     set_pagination_headers(next_path, prev_path) | ||||
|  | ||||
|     render action: :index | ||||
|   end | ||||
|  | ||||
|   def public | ||||
|     @statuses = Status.as_public_timeline(current_account).paginate_by_max_id(20, params[:max_id], params[:since_id]).to_a | ||||
|     @statuses = Status.as_public_timeline(current_account).paginate_by_max_id(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id]).to_a | ||||
|  | ||||
|     set_maps(@statuses) | ||||
|  | ||||
|     next_path = api_v1_public_timeline_url(max_id: @statuses.last.id)    if @statuses.size == DEFAULT_STATUSES_LIMIT | ||||
|     prev_path = api_v1_public_timeline_url(since_id: @statuses.first.id) if @statuses.size > 0 | ||||
|  | ||||
|     set_pagination_headers(next_path, prev_path) | ||||
|  | ||||
|     render action: :index | ||||
|   end | ||||
|  | ||||
|   def tag | ||||
|     @tag = Tag.find_by(name: params[:id].downcase) | ||||
|     @tag      = Tag.find_by(name: params[:id].downcase) | ||||
|     @statuses = @tag.nil? ? [] : Status.as_tag_timeline(@tag, current_account).paginate_by_max_id(DEFAULT_STATUSES_LIMIT, params[:max_id], params[:since_id]).to_a | ||||
|  | ||||
|     if @tag.nil? | ||||
|       @statuses = [] | ||||
|     else | ||||
|       @statuses = Status.as_tag_timeline(@tag, current_account).paginate_by_max_id(20, params[:max_id], params[:since_id]).to_a | ||||
|       set_maps(@statuses) | ||||
|     end | ||||
|     set_maps(@statuses) | ||||
|  | ||||
|     next_path = api_v1_hashtag_timeline_url(params[:id], max_id: @statuses.last.id)    if @statuses.size == DEFAULT_STATUSES_LIMIT | ||||
|     prev_path = api_v1_hashtag_timeline_url(params[:id], since_id: @statuses.first.id) if @statuses.size > 0 | ||||
|  | ||||
|     set_pagination_headers(next_path, prev_path) | ||||
|  | ||||
|     render action: :index | ||||
|   end | ||||
|   | ||||
| @@ -1,4 +1,7 @@ | ||||
| class ApiController < ApplicationController | ||||
|   DEFAULT_STATUSES_LIMIT = 20 | ||||
|   DEFAULT_ACCOUNTS_LIMIT = 40 | ||||
|  | ||||
|   protect_from_forgery with: :null_session | ||||
|  | ||||
|   skip_before_action :verify_authenticity_token | ||||
| @@ -54,6 +57,13 @@ class ApiController < ApplicationController | ||||
|     response.headers['Access-Control-Allow-Headers']  = 'Origin, X-Requested-With, Content-Type, Accept, Authorization' | ||||
|   end | ||||
|  | ||||
|   def set_pagination_headers(next_path = nil, prev_path = nil) | ||||
|     links = [] | ||||
|     links << [next_path, [['rel', 'next']]] if next_path | ||||
|     links << [prev_path, [['rel', 'prev']]] if prev_path | ||||
|     response.headers['Link'] = LinkHeader.new(links) | ||||
|   end | ||||
|  | ||||
|   def current_resource_owner | ||||
|     User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token | ||||
|   end | ||||
|   | ||||
| @@ -133,36 +133,38 @@ class Account < ApplicationRecord | ||||
|     [] | ||||
|   end | ||||
|  | ||||
|   def self.find_local!(username) | ||||
|     find_remote!(username, nil) | ||||
|   end | ||||
|   class << self | ||||
|     def find_local!(username) | ||||
|       find_remote!(username, nil) | ||||
|     end | ||||
|  | ||||
|   def self.find_remote!(username, domain) | ||||
|     where(arel_table[:username].matches(username)).where(domain.nil? ? { domain: nil } : arel_table[:domain].matches(domain)).take! | ||||
|   end | ||||
|     def find_remote!(username, domain) | ||||
|       where(arel_table[:username].matches(username)).where(domain.nil? ? { domain: nil } : arel_table[:domain].matches(domain)).take! | ||||
|     end | ||||
|  | ||||
|   def self.find_local(username) | ||||
|     find_local!(username) | ||||
|   rescue ActiveRecord::RecordNotFound | ||||
|     nil | ||||
|   end | ||||
|     def find_local(username) | ||||
|       find_local!(username) | ||||
|     rescue ActiveRecord::RecordNotFound | ||||
|       nil | ||||
|     end | ||||
|  | ||||
|   def self.find_remote(username, domain) | ||||
|     find_remote!(username, domain) | ||||
|   rescue ActiveRecord::RecordNotFound | ||||
|     nil | ||||
|   end | ||||
|     def find_remote(username, domain) | ||||
|       find_remote!(username, domain) | ||||
|     rescue ActiveRecord::RecordNotFound | ||||
|       nil | ||||
|     end | ||||
|  | ||||
|   def self.following_map(target_account_ids, account_id) | ||||
|     Follow.where(target_account_id: target_account_ids).where(account_id: account_id).map { |f| [f.target_account_id, true] }.to_h | ||||
|   end | ||||
|     def following_map(target_account_ids, account_id) | ||||
|       Follow.where(target_account_id: target_account_ids).where(account_id: account_id).map { |f| [f.target_account_id, true] }.to_h | ||||
|     end | ||||
|  | ||||
|   def self.followed_by_map(target_account_ids, account_id) | ||||
|     Follow.where(account_id: target_account_ids).where(target_account_id: account_id).map { |f| [f.account_id, true] }.to_h | ||||
|   end | ||||
|     def followed_by_map(target_account_ids, account_id) | ||||
|       Follow.where(account_id: target_account_ids).where(target_account_id: account_id).map { |f| [f.account_id, true] }.to_h | ||||
|     end | ||||
|  | ||||
|   def self.blocking_map(target_account_ids, account_id) | ||||
|     Block.where(target_account_id: target_account_ids).where(account_id: account_id).map { |b| [b.target_account_id, true] }.to_h | ||||
|     def blocking_map(target_account_ids, account_id) | ||||
|       Block.where(target_account_id: target_account_ids).where(account_id: account_id).map { |b| [b.target_account_id, true] }.to_h | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   before_create do | ||||
|   | ||||
| @@ -2,11 +2,11 @@ module Paginable | ||||
|   extend ActiveSupport::Concern | ||||
|  | ||||
|   included do | ||||
|     def self.paginate_by_max_id(limit, max_id = nil, since_id = nil) | ||||
|       query = order('id desc').limit(limit) | ||||
|     scope :paginate_by_max_id, -> (limit, max_id = nil, since_id = nil) { | ||||
|       query = order(arel_table[:id].desc).limit(limit) | ||||
|       query = query.where(arel_table[:id].lt(max_id)) unless max_id.blank? | ||||
|       query = query.where(arel_table[:id].gt(since_id)) unless since_id.blank? | ||||
|       query | ||||
|     end | ||||
|     } | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| class Favourite < ApplicationRecord | ||||
|   include Paginable | ||||
|   include Streamable | ||||
|  | ||||
|   belongs_to :account, inverse_of: :favourites | ||||
|   | ||||
| @@ -12,11 +12,13 @@ class Feed | ||||
|     # If we're after most recent items and none are there, we need to precompute the feed | ||||
|     if unhydrated.empty? && max_id == '+inf' && since_id == '-inf' | ||||
|       RegenerationWorker.perform_async(@account.id, @type) | ||||
|       Status.send("as_#{@type}_timeline", @account).paginate_by_max_id(limit, nil, nil) | ||||
|       @statuses = Status.send("as_#{@type}_timeline", @account).paginate_by_max_id(limit, nil, nil) | ||||
|     else | ||||
|       status_map = Status.where(id: unhydrated).with_includes.with_counters.map { |status| [status.id, status] }.to_h | ||||
|       unhydrated.map { |id| status_map[id] }.compact | ||||
|       @statuses = unhydrated.map { |id| status_map[id] }.compact | ||||
|     end | ||||
|  | ||||
|     @statuses | ||||
|   end | ||||
|  | ||||
|   private | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| class Follow < ApplicationRecord | ||||
|   include Paginable | ||||
|   include Streamable | ||||
|  | ||||
|   belongs_to :account | ||||
|   | ||||
| @@ -78,14 +78,6 @@ class Status < ApplicationRecord | ||||
|     ids.map { |id| statuses[id].first } | ||||
|   end | ||||
|  | ||||
|   def reblogged_by(limit) | ||||
|     Account.where(id: reblogs.limit(limit).pluck(:account_id)).with_counters | ||||
|   end | ||||
|  | ||||
|   def favourited_by(limit) | ||||
|     Account.where(id: favourites.limit(limit).pluck(:account_id)).with_counters | ||||
|   end | ||||
|  | ||||
|   class << self | ||||
|     def as_home_timeline(account) | ||||
|       where(account: [account] + account.following).with_includes.with_counters | ||||
|   | ||||
| @@ -67,14 +67,10 @@ Rails.application.routes.draw do | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       resources :timelines, only: [] do | ||||
|         collection do | ||||
|           get :home | ||||
|           get :mentions | ||||
|           get :public | ||||
|           get '/tag/:id', action: :tag | ||||
|         end | ||||
|       end | ||||
|       get '/timelines/home',     to: 'timelines#home', as: :home_timeline | ||||
|       get '/timelines/mentions', to: 'timelines#mentions', as: :mentions_timeline | ||||
|       get '/timelines/public',   to: 'timelines#public', as: :public_timeline | ||||
|       get '/timelines/tag/:id',  to: 'timelines#tag', as: :hashtag_timeline | ||||
|  | ||||
|       resources :follows,  only: [:create] | ||||
|       resources :media,    only: [:create] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user