Skip to content

Commit

Permalink
Add REST API for managing and posting to circles
Browse files Browse the repository at this point in the history
Circles are the conceptual opposite of lists. A list is a subdivision
of your follows, a circle is a subdivision of your followers. Posting
to a circle means making content available to only some of your
followers. Circles have been internally supported in Mastodon for
the purposes of federation since mastodon#8950, this adds the REST API
necessary for making use of them in Mastodon itsef.
  • Loading branch information
Gargron authored and noellabo committed Sep 5, 2020
1 parent a6121a1 commit 561abc6
Show file tree
Hide file tree
Showing 19 changed files with 353 additions and 4 deletions.
18 changes: 18 additions & 0 deletions app/controllers/api/v1/accounts/circles_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

class Api::V1::Accounts::CirclesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:circles' }
before_action :require_user!
before_action :set_account

def index
@circles = @account.circles.where(account: current_account)
render json: @circles, each_serializer: REST::CircleSerializer
end

private

def set_account
@account = Account.find(params[:account_id])
end
end
93 changes: 93 additions & 0 deletions app/controllers/api/v1/circles/accounts_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# frozen_string_literal: true

class Api::V1::Circles::AccountsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:circles' }, only: [:show]
before_action -> { doorkeeper_authorize! :write, :'write:circles' }, except: [:show]

before_action :require_user!
before_action :set_circle

after_action :insert_pagination_headers, only: :show

def show
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
end

def create
ApplicationRecord.transaction do
circle_accounts.each do |account|
@circle.accounts << account
end
end

render_empty
end

def destroy
CircleAccount.where(circle: @circle, account_id: account_ids).destroy_all
render_empty
end

private

def set_circle
@circle = current_account.owned_circles.find(params[:circle_id])
end

def load_accounts
if unlimited?
@circle.accounts.includes(:account_stat).all
else
@circle.accounts.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
end
end

def circle_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
return if unlimited?

api_v1_circle_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end

def prev_path
return if unlimited?

api_v1_circle_accounts_url(pagination_params(since_id: pagination_since_id)) unless @accounts.empty?
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.slice(:limit).permit(:limit).merge(core_params)
end

def unlimited?
params[:limit] == '0'
end
end
73 changes: 73 additions & 0 deletions app/controllers/api/v1/circles_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# frozen_string_literal: true

class Api::V1::CirclesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:circles' }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :write, :'write:circles' }, except: [:index, :show]

before_action :require_user!
before_action :set_circle, except: [:index, :create]

after_action :insert_pagination_headers, only: :index

def index
@circles = current_account.owned_circles.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
render json: @circles, each_serializer: REST::CircleSerializer
end

def show
render json: @circle, serializer: REST::CircleSerializer
end

def create
@circle = current_account.owned_circles.create!(circle_params)
render json: @circle, serializer: REST::CircleSerializer
end

def update
@circle.update!(circle_params)
render json: @circle, serializer: REST::CircleSerializer
end

def destroy
@circle.destroy!
render_empty
end

private

def set_circle
@circle = current_account.owned_circles.find(params[:id])
end

def circle_params
params.permit(:title)
end

def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end

def next_path
api_v1_circles_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end

def prev_path
api_v1_circles_url(pagination_params(since_id: pagination_since_id)) unless @circles.empty?
end

def pagination_max_id
@circles.last.id
end

def pagination_since_id
@circles.first.id
end

def records_continue?
@circles.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end

def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params)
end
end
9 changes: 9 additions & 0 deletions app/controllers/api/v1/statuses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class Api::V1::StatusesController < Api::BaseController
before_action :require_user!, except: [:show, :context]
before_action :set_status, only: [:show, :context]
before_action :set_thread, only: [:create]
before_action :set_circle, only: [:create]

override_rate_limit_headers :create, family: :statuses

Expand Down Expand Up @@ -38,6 +39,7 @@ def create
@status = PostStatusService.new.call(current_user.account,
text: status_params[:status],
thread: @thread,
circle: @circle,
media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive],
spoiler_text: status_params[:spoiler_text],
Expand Down Expand Up @@ -77,10 +79,17 @@ def set_thread
render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404
end

def set_circle
@circle = status_params[:circle_id].blank? ? nil : current_account.owned_circles.find(status_params[:circle_id])
rescue ActiveRecord::RecordNotFound
render json: { error: I18n.t('statuses.errors.circle_not_found') }, status: 404
end

def status_params
params.permit(
:status,
:in_reply_to_id,
:circle_id,
:sensitive,
:spoiler_text,
:visibility,
Expand Down
22 changes: 22 additions & 0 deletions app/models/circle.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: circles
#
# id :bigint(8) not null, primary key
# account_id :bigint(8) not null
# title :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class Circle < ApplicationRecord
include Paginable

belongs_to :account

has_many :circle_accounts, inverse_of: :circle, dependent: :destroy
has_many :accounts, through: :circle_accounts

validates :title, presence: true
end
28 changes: 28 additions & 0 deletions app/models/circle_account.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: circle_accounts
#
# id :bigint(8) not null, primary key
# circle_id :bigint(8) not null
# account_id :bigint(8) not null
# follow_id :bigint(8) not null
# created_at :datetime not null
# updated_at :datetime not null
#
class CircleAccount < ApplicationRecord
belongs_to :circle
belongs_to :account
belongs_to :follow, optional: true

validates :account_id, uniqueness: { scope: :circle_id }

before_validation :set_follow

private

def set_follow
self.follow = Follow.find_by!(target_account_id: circle.account_id, account_id: account.id)
end
end
3 changes: 3 additions & 0 deletions app/models/concerns/account_associations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,12 @@ module AccountAssociations
# Lists (that the account is on, not owned by the account)
has_many :list_accounts, inverse_of: :account, dependent: :destroy
has_many :lists, through: :list_accounts
has_many :circle_accounts, inverse_of: :account, dependent: :destroy
has_many :circles, through: :circle_accounts

# Lists (owned by the account)
has_many :owned_lists, class_name: 'List', dependent: :destroy, inverse_of: :account
has_many :owned_circles, class_name: 'Circle', dependent: :destroy, inverse_of: :account

# Account migrations
belongs_to :moved_to_account, class_name: 'Account', optional: true
Expand Down
5 changes: 4 additions & 1 deletion app/services/post_status_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class PostStatusService < BaseService
# @option [String] :spoiler_text
# @option [String] :language
# @option [String] :scheduled_at
# @option [Circle] :circle Optional circle to target the status to
# @option [Hash] :poll Optional poll to attach
# @option [Enumerable] :media_ids Optional array of media IDs to attach
# @option [Doorkeeper::Application] :application
Expand All @@ -26,6 +27,7 @@ def call(account, options = {})
@options = options
@text = @options[:text] || ''
@in_reply_to = @options[:thread]
@circle = @options[:circle]

return idempotency_duplicate if idempotency_given? && idempotency_duplicate?

Expand All @@ -52,6 +54,7 @@ def preprocess_attributes!
@text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present?
@visibility = @options[:visibility] || @account.user&.setting_default_privacy
@visibility = :unlisted if @visibility&.to_sym == :public && @account.silenced?
@visibility = :limited if @circle.present?
@scheduled_at = @options[:scheduled_at]&.to_datetime
@scheduled_at = nil if scheduled_in_the_past?
rescue ArgumentError
Expand All @@ -67,7 +70,7 @@ def process_status!
end

process_hashtags_service.call(@status)
process_mentions_service.call(@status)
process_mentions_service.call(@status, @circle)
end

def schedule_status!
Expand Down
9 changes: 8 additions & 1 deletion app/services/process_mentions_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ class ProcessMentionsService < BaseService
# local mention pointers, send Salmon notifications to mentioned
# remote users
# @param [Status] status
def call(status)
# @param [Circle] circle
def call(status, circle = nil)
return unless status.local?

@status = status
Expand Down Expand Up @@ -42,6 +43,12 @@ def call(status)
"@#{mentioned_account.acct}"
end

if circle.present?
circle.accounts.find_each do |target_account|
status.mentions.create(silent: true, account: target_account)
end
end

status.save!
check_for_spam(status)

Expand Down
2 changes: 2 additions & 0 deletions config/initializers/doorkeeper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
:'write:accounts',
:'write:blocks',
:'write:bookmarks',
:'write:circles',
:'write:conversations',
:'write:favourites',
:'write:filters',
Expand All @@ -80,6 +81,7 @@
:'read:accounts',
:'read:blocks',
:'read:bookmarks',
:'read:circles',
:'read:favourites',
:'read:filters',
:'read:follows',
Expand Down
5 changes: 5 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@
resources :followers, only: :index, controller: 'accounts/follower_accounts'
resources :following, only: :index, controller: 'accounts/following_accounts'
resources :lists, only: :index, controller: 'accounts/lists'
resources :circles, only: :index, controller: 'accounts/circles'
resources :identity_proofs, only: :index, controller: 'accounts/identity_proofs'
resources :featured_tags, only: :index, controller: 'accounts/featured_tags'

Expand All @@ -451,6 +452,10 @@
resource :accounts, only: [:show, :create, :destroy], controller: 'lists/accounts'
end

resources :circles, only: [:index, :create, :show, :update, :destroy] do
resource :accounts, only: [:show, :create, :destroy], controller: 'circles/accounts'
end

namespace :featured_tags do
get :suggestions, to: 'suggestions#index'
end
Expand Down
10 changes: 10 additions & 0 deletions db/migrate/20200718225713_create_circles.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class CreateCircles < ActiveRecord::Migration[5.2]
def change
create_table :circles do |t|
t.belongs_to :account, foreign_key: { on_delete: :cascade }, null: false
t.string :title, default: '', null: false

t.timestamps
end
end
end
Loading

0 comments on commit 561abc6

Please sign in to comment.