Move timelines API from statuses to its own controller, add a check for

resources that require a user context vs those that don't (such as public timeline)

/api/v1/statuses/public   -> /api/v1/timelines/public
/api/v1/statuses/home     -> /api/v1/timelines/home
/api/v1/statuses/mentions -> /api/v1/timelines/mentions
/api/v1/statuses/tag/:tag -> /api/v1/timelines/tag/:tag
main
Eugen Rochko 2016-11-08 23:22:44 +01:00
parent 86574ea524
commit 9aecc0f48a
12 changed files with 170 additions and 87 deletions

View File

@ -73,7 +73,7 @@ export function refreshTimeline(timeline, replace = false, id = null) {
path = `${path}/${id}` path = `${path}/${id}`
} }
api(getState).get(`/api/v1/statuses/${path}${params}`).then(function (response) { api(getState).get(`/api/v1/timelines/${path}${params}`).then(function (response) {
dispatch(refreshTimelineSuccess(timeline, response.data, replace)); dispatch(refreshTimelineSuccess(timeline, response.data, replace));
}).catch(function (error) { }).catch(function (error) {
dispatch(refreshTimelineFail(timeline, error)); dispatch(refreshTimelineFail(timeline, error));
@ -101,7 +101,7 @@ export function expandTimeline(timeline, id = null) {
path = `${path}/${id}` path = `${path}/${id}`
} }
api(getState).get(`/api/v1/statuses/${path}?max_id=${lastId}`).then(response => { api(getState).get(`/api/v1/timelines/${path}?max_id=${lastId}`).then(response => {
dispatch(expandTimelineSuccess(timeline, response.data)); dispatch(expandTimelineSuccess(timeline, response.data));
}).catch(error => { }).catch(error => {
dispatch(expandTimelineFail(timeline, error)); dispatch(expandTimelineFail(timeline, error));

View File

@ -1,8 +1,9 @@
class Api::V1::AccountsController < ApiController class Api::V1::AccountsController < ApiController
before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock] before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock]
before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock] before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock]
before_action :require_user!, except: [:show, :following, :followers, :statuses]
before_action :set_account, except: [:verify_credentials, :suggestions] before_action :set_account, except: [:verify_credentials, :suggestions]
respond_to :json respond_to :json
def show def show

View File

@ -1,5 +1,7 @@
class Api::V1::FollowsController < ApiController class Api::V1::FollowsController < ApiController
before_action -> { doorkeeper_authorize! :follow } before_action -> { doorkeeper_authorize! :follow }
before_action :require_user!
respond_to :json respond_to :json
def create def create

View File

@ -1,5 +1,7 @@
class Api::V1::MediaController < ApiController class Api::V1::MediaController < ApiController
before_action -> { doorkeeper_authorize! :write } before_action -> { doorkeeper_authorize! :write }
before_action :require_user!
respond_to :json respond_to :json
def create def create

View File

@ -1,8 +1,8 @@
class Api::V1::StatusesController < ApiController class Api::V1::StatusesController < ApiController
before_action -> { doorkeeper_authorize! :read }, except: [:create, :destroy, :reblog, :unreblog, :favourite, :unfavourite] before_action -> { doorkeeper_authorize! :read }, except: [:create, :destroy, :reblog, :unreblog, :favourite, :unfavourite]
before_action -> { doorkeeper_authorize! :write }, only: [:create, :destroy, :reblog, :unreblog, :favourite, :unfavourite] before_action -> { doorkeeper_authorize! :write }, only: [:create, :destroy, :reblog, :unreblog, :favourite, :unfavourite]
before_action :require_user!, except: [:show, :context, :reblogged_by, :favourited_by]
before_action :set_status, only: [:show, :context, :reblogged_by, :favourited_by] before_action :set_status, only: [:show, :context, :reblogged_by, :favourited_by]
respond_to :json respond_to :json
@ -56,37 +56,6 @@ class Api::V1::StatusesController < ApiController
render action: :show render action: :show
end end
def home
@statuses = Feed.new(:home, current_user.account).get(20, params[:max_id], params[:since_id]).to_a
set_maps(@statuses)
render action: :index
end
def mentions
@statuses = Feed.new(:mentions, current_user.account).get(20, params[:max_id], params[:since_id]).to_a
set_maps(@statuses)
render action: :index
end
def public
@statuses = Status.as_public_timeline(current_user.account).paginate_by_max_id(20, params[:max_id], params[:since_id]).to_a
set_maps(@statuses)
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_user.account).paginate_by_max_id(20, params[:max_id], params[:since_id]).to_a
set_maps(@statuses)
end
render action: :index
end
private private
def set_status def set_status

View File

@ -0,0 +1,37 @@
class Api::V1::TimelinesController < ApiController
before_action -> { doorkeeper_authorize! :read }
before_action :require_user!, only: [:home, :mentions]
respond_to :json
def home
@statuses = Feed.new(:home, current_account).get(20, params[:max_id], params[:since_id]).to_a
set_maps(@statuses)
render action: :index
end
def mentions
@statuses = Feed.new(:mentions, current_account).get(20, params[:max_id], params[:since_id]).to_a
set_maps(@statuses)
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
set_maps(@statuses)
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
render action: :index
end
end

View File

@ -60,6 +60,14 @@ class ApiController < ApplicationController
def current_user def current_user
super || current_resource_owner super || current_resource_owner
rescue ActiveRecord::RecordNotFound
nil
end
def require_user!
current_resource_owner
rescue ActiveRecord::RecordNotFound
render json: { error: 'This method requires an authenticated user' }, status: 422
end end
def render_empty def render_empty
@ -67,8 +75,14 @@ class ApiController < ApplicationController
end end
def set_maps(statuses) def set_maps(statuses)
if current_account.nil?
@reblogs_map = {}
@favourites_map = {}
return
end
status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.compact.uniq status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.compact.uniq
@reblogs_map = Status.reblogs_map(status_ids, current_user.account) @reblogs_map = Status.reblogs_map(status_ids, current_account)
@favourites_map = Status.favourites_map(status_ids, current_user.account) @favourites_map = Status.favourites_map(status_ids, current_account)
end end
end end

View File

@ -95,23 +95,29 @@ class Status < ApplicationRecord
where(id: Mention.where(account: account).pluck(:status_id)).with_includes.with_counters where(id: Mention.where(account: account).pluck(:status_id)).with_includes.with_counters
end end
def as_public_timeline(account) def as_public_timeline(account = nil)
joins('LEFT OUTER JOIN statuses AS reblogs ON statuses.reblog_of_id = reblogs.id') query = joins('LEFT OUTER JOIN statuses AS reblogs ON statuses.reblog_of_id = reblogs.id')
.joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id') .joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id')
.where('accounts.silenced = FALSE') .where('accounts.silenced = FALSE')
.where('(reblogs.account_id IS NULL OR reblogs.account_id NOT IN (SELECT target_account_id FROM blocks WHERE account_id = ?)) AND statuses.account_id NOT IN (SELECT target_account_id FROM blocks WHERE account_id = ?)', account.id, account.id)
.with_includes unless account.nil?
.with_counters query = query.where('(reblogs.account_id IS NULL OR reblogs.account_id NOT IN (SELECT target_account_id FROM blocks WHERE account_id = ?)) AND statuses.account_id NOT IN (SELECT target_account_id FROM blocks WHERE account_id = ?)', account.id, account.id)
end
query.with_includes.with_counters
end end
def as_tag_timeline(tag, account) def as_tag_timeline(tag, account = nil)
tag.statuses query = tag.statuses
.joins('LEFT OUTER JOIN statuses AS reblogs ON statuses.reblog_of_id = reblogs.id') .joins('LEFT OUTER JOIN statuses AS reblogs ON statuses.reblog_of_id = reblogs.id')
.joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id') .joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id')
.where('accounts.silenced = FALSE') .where('accounts.silenced = FALSE')
.where('(reblogs.account_id IS NULL OR reblogs.account_id NOT IN (SELECT target_account_id FROM blocks WHERE account_id = ?)) AND statuses.account_id NOT IN (SELECT target_account_id FROM blocks WHERE account_id = ?)', account.id, account.id)
.with_includes unless account.nil?
.with_counters query = query.where('(reblogs.account_id IS NULL OR reblogs.account_id NOT IN (SELECT target_account_id FROM blocks WHERE account_id = ?)) AND statuses.account_id NOT IN (SELECT target_account_id FROM blocks WHERE account_id = ?)', account.id, account.id)
end
query.with_includes.with_counters
end end
def favourites_map(status_ids, account_id) def favourites_map(status_ids, account_id)

View File

@ -0,0 +1,2 @@
collection @statuses
extends('api/v1/statuses/show')

View File

@ -55,13 +55,6 @@ Rails.application.routes.draw do
# JSON / REST API # JSON / REST API
namespace :v1 do namespace :v1 do
resources :statuses, only: [:create, :show, :destroy] do resources :statuses, only: [:create, :show, :destroy] do
collection do
get :home
get :mentions
get :public
get '/tag/:id', action: :tag
end
member do member do
get :context get :context
get :reblogged_by get :reblogged_by
@ -74,6 +67,15 @@ Rails.application.routes.draw do
end end
end end
resources :timelines, only: [] do
collection do
get :home
get :mentions
get :public
get '/tag/:id', action: :tag
end
end
resources :follows, only: [:create] resources :follows, only: [:create]
resources :media, only: [:create] resources :media, only: [:create]
resources :apps, only: [:create] resources :apps, only: [:create]

View File

@ -59,38 +59,6 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
end end
end end
describe 'GET #home' do
it 'returns http success' do
get :home
expect(response).to have_http_status(:success)
end
end
describe 'GET #mentions' do
it 'returns http success' do
get :mentions
expect(response).to have_http_status(:success)
end
end
describe 'GET #public' do
it 'returns http success' do
get :public
expect(response).to have_http_status(:success)
end
end
describe 'GET #tag' do
before do
post :create, params: { status: 'It is a #test' }
end
it 'returns http success' do
get :tag, params: { id: 'test' }
expect(response).to have_http_status(:success)
end
end
describe 'POST #create' do describe 'POST #create' do
before do before do
post :create, params: { status: 'Hello world' } post :create, params: { status: 'Hello world' }

View File

@ -0,0 +1,80 @@
require 'rails_helper'
RSpec.describe Api::V1::TimelinesController, type: :controller do
render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
before do
stub_request(:post, "https://pubsubhubbub.superfeedr.com/").to_return(:status => 200, :body => "", :headers => {})
allow(controller).to receive(:doorkeeper_token) { token }
end
context 'with a user context' do
let(:token) { double acceptable?: true, resource_owner_id: user.id }
describe 'GET #home' do
it 'returns http success' do
get :home
expect(response).to have_http_status(:success)
end
end
describe 'GET #mentions' do
it 'returns http success' do
get :mentions
expect(response).to have_http_status(:success)
end
end
describe 'GET #public' do
it 'returns http success' do
get :public
expect(response).to have_http_status(:success)
end
end
describe 'GET #tag' do
before do
PostStatusService.new.call(user.account, 'It is a #test')
end
it 'returns http success' do
get :tag, params: { id: 'test' }
expect(response).to have_http_status(:success)
end
end
end
context 'without a user context' do
let(:token) { double acceptable?: true, resource_owner_id: nil }
describe 'GET #home' do
it 'returns http unprocessable entity' do
get :home
expect(response).to have_http_status(:unprocessable_entity)
end
end
describe 'GET #mentions' do
it 'returns http unprocessable entity' do
get :mentions
expect(response).to have_http_status(:unprocessable_entity)
end
end
describe 'GET #public' do
it 'returns http success' do
get :public
expect(response).to have_http_status(:success)
end
end
describe 'GET #tag' do
it 'returns http success' do
get :tag, params: { id: 'test' }
expect(response).to have_http_status(:success)
end
end
end
end