Skip to content
Snippets Groups Projects
Commit b13e7dda authored by Eugen Rochko's avatar Eugen Rochko
Browse files

API pagination for all collections using Link header

parent 8d7fc5da
No related branches found
No related tags found
No related merge requests found
......@@ -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)
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
@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
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
......
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
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
......
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]
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment