Merge upstream (#81)
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
class AccountsController < ApplicationController
|
||||
include AccountControllerConcern
|
||||
include SignatureVerification
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
@@ -15,7 +16,9 @@ class AccountsController < ApplicationController
|
||||
render xml: AtomSerializer.render(AtomSerializer.new.feed(@account, @entries.to_a))
|
||||
end
|
||||
|
||||
format.activitystreams2
|
||||
format.json do
|
||||
render json: @account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
28
app/controllers/activitypub/outboxes_controller.rb
Normal file
28
app/controllers/activitypub/outboxes_controller.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::OutboxesController < Api::BaseController
|
||||
before_action :set_account
|
||||
|
||||
def show
|
||||
@statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(20, params[:max_id], params[:since_id])
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
|
||||
render json: outbox_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find_local!(params[:account_username])
|
||||
end
|
||||
|
||||
def outbox_presenter
|
||||
ActivityPub::CollectionPresenter.new(
|
||||
id: account_outbox_url(@account),
|
||||
type: :ordered,
|
||||
current: account_outbox_url(@account),
|
||||
size: @account.statuses_count,
|
||||
items: @statuses
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -1,27 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::ActivityPub::ActivitiesController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
# before_action :set_follow, only: [:show_follow]
|
||||
before_action :set_status, only: [:show_status]
|
||||
|
||||
respond_to :activitystreams2
|
||||
|
||||
# Show a status in AS2 format, as either an Announce (reblog) or a Create (post) activity.
|
||||
def show_status
|
||||
authorize @status, :show?
|
||||
|
||||
if @status.reblog?
|
||||
render :show_status_announce
|
||||
else
|
||||
render :show_status_create
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_status
|
||||
@status = Status.find(params[:id])
|
||||
end
|
||||
end
|
||||
@@ -1,19 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::ActivityPub::NotesController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
before_action :set_status
|
||||
|
||||
respond_to :activitystreams2
|
||||
|
||||
def show
|
||||
authorize @status, :show?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_status
|
||||
@status = Status.find(params[:id])
|
||||
end
|
||||
end
|
||||
@@ -1,69 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::ActivityPub::OutboxController < Api::BaseController
|
||||
before_action :set_account
|
||||
|
||||
respond_to :activitystreams2
|
||||
|
||||
def show
|
||||
if params[:max_id] || params[:since_id]
|
||||
show_outbox_page
|
||||
else
|
||||
show_base_outbox
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def show_base_outbox
|
||||
@statuses = Status.as_outbox_timeline(@account)
|
||||
@statuses = cache_collection(@statuses)
|
||||
|
||||
set_maps(@statuses)
|
||||
|
||||
set_first_last_page(@statuses)
|
||||
|
||||
render :show
|
||||
end
|
||||
|
||||
def show_outbox_page
|
||||
all_statuses = Status.as_outbox_timeline(@account)
|
||||
@statuses = all_statuses.paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
|
||||
|
||||
all_statuses = cache_collection(all_statuses)
|
||||
@statuses = cache_collection(@statuses)
|
||||
|
||||
set_maps(@statuses)
|
||||
|
||||
set_first_last_page(all_statuses)
|
||||
|
||||
@next_page_url = api_activitypub_outbox_url(pagination_params(max_id: @statuses.last.id)) unless @statuses.empty?
|
||||
@prev_page_url = api_activitypub_outbox_url(pagination_params(since_id: @statuses.first.id)) unless @statuses.empty?
|
||||
|
||||
@paginated = @next_page_url || @prev_page_url
|
||||
@part_of_url = api_activitypub_outbox_url
|
||||
|
||||
set_pagination_headers(@next_page_url, @prev_page_url)
|
||||
|
||||
render :show_page
|
||||
end
|
||||
|
||||
def cache_collection(raw)
|
||||
super(raw, Status)
|
||||
end
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:id])
|
||||
end
|
||||
|
||||
def set_first_last_page(statuses) # rubocop:disable Style/AccessorMethodName
|
||||
return if statuses.empty?
|
||||
|
||||
@first_page_url = api_activitypub_outbox_url(max_id: statuses.first.id + 1)
|
||||
@last_page_url = api_activitypub_outbox_url(since_id: statuses.last.id - 1)
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.permit(:local, :limit).merge(core_params)
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::PushController < Api::BaseController
|
||||
include SignatureVerification
|
||||
|
||||
def update
|
||||
response, status = process_push_request
|
||||
render plain: response, status: status
|
||||
@@ -11,7 +13,7 @@ class Api::PushController < Api::BaseController
|
||||
def process_push_request
|
||||
case hub_mode
|
||||
when 'subscribe'
|
||||
Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds)
|
||||
Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds, verified_domain)
|
||||
when 'unsubscribe'
|
||||
Pubsubhubbub::UnsubscribeService.new.call(account_from_topic, hub_callback)
|
||||
else
|
||||
@@ -57,6 +59,10 @@ class Api::PushController < Api::BaseController
|
||||
TagManager.instance.web_domain?(hub_topic_domain)
|
||||
end
|
||||
|
||||
def verified_domain
|
||||
return signed_request_account.domain if signed_request_account
|
||||
end
|
||||
|
||||
def hub_topic_domain
|
||||
hub_topic_uri.host + (hub_topic_uri.port ? ":#{hub_topic_uri.port}" : '')
|
||||
end
|
||||
|
||||
@@ -42,7 +42,7 @@ class Api::SubscriptionsController < Api::BaseController
|
||||
end
|
||||
|
||||
def lease_seconds_or_default
|
||||
(params['hub.lease_seconds'] || 86_400).to_i.seconds
|
||||
(params['hub.lease_seconds'] || 1.day).to_i.seconds
|
||||
end
|
||||
|
||||
def set_account
|
||||
|
||||
@@ -19,7 +19,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
|
||||
|
||||
UnfavouriteWorker.perform_async(current_user.account_id, @status.id)
|
||||
|
||||
render json: @status, serializer: REST::StatusSerializer
|
||||
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_user&.account_id, favourites_map: @favourites_map)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -20,7 +20,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
|
||||
authorize status_for_destroy, :unreblog?
|
||||
RemovalWorker.perform_async(status_for_destroy.id)
|
||||
|
||||
render json: @status, serializer: REST::StatusSerializer
|
||||
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_user&.account_id, reblogs_map: @reblogs_map)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
39
app/controllers/api/web/push_subscriptions_controller.rb
Normal file
39
app/controllers/api/web/push_subscriptions_controller.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::Web::PushSubscriptionsController < Api::BaseController
|
||||
respond_to :json
|
||||
|
||||
before_action :require_user!
|
||||
|
||||
def create
|
||||
params.require(:data).require(:endpoint)
|
||||
params.require(:data).require(:keys).require([:auth, :p256dh])
|
||||
|
||||
active_session = current_session
|
||||
|
||||
unless active_session.web_push_subscription.nil?
|
||||
active_session.web_push_subscription.destroy!
|
||||
active_session.update!(web_push_subscription: nil)
|
||||
end
|
||||
|
||||
web_subscription = ::Web::PushSubscription.create!(
|
||||
endpoint: params[:data][:endpoint],
|
||||
key_p256dh: params[:data][:keys][:p256dh],
|
||||
key_auth: params[:data][:keys][:auth]
|
||||
)
|
||||
|
||||
active_session.update!(web_push_subscription: web_subscription)
|
||||
|
||||
render json: web_subscription.as_payload
|
||||
end
|
||||
|
||||
def update
|
||||
params.require([:id, :data])
|
||||
|
||||
web_subscription = ::Web::PushSubscription.find(params[:id])
|
||||
|
||||
web_subscription.update!(data: params[:data])
|
||||
|
||||
render json: web_subscription.as_payload
|
||||
end
|
||||
end
|
||||
87
app/controllers/concerns/signature_verification.rb
Normal file
87
app/controllers/concerns/signature_verification.rb
Normal file
@@ -0,0 +1,87 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Implemented according to HTTP signatures (Draft 6)
|
||||
# <https://tools.ietf.org/html/draft-cavage-http-signatures-06>
|
||||
module SignatureVerification
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def signed_request?
|
||||
request.headers['Signature'].present?
|
||||
end
|
||||
|
||||
def signed_request_account
|
||||
return @signed_request_account if defined?(@signed_request_account)
|
||||
|
||||
unless signed_request?
|
||||
@signed_request_account = nil
|
||||
return
|
||||
end
|
||||
|
||||
raw_signature = request.headers['Signature']
|
||||
signature_params = {}
|
||||
|
||||
raw_signature.split(',').each do |part|
|
||||
parsed_parts = part.match(/([a-z]+)="([^"]+)"/i)
|
||||
next if parsed_parts.nil? || parsed_parts.size != 3
|
||||
signature_params[parsed_parts[1]] = parsed_parts[2]
|
||||
end
|
||||
|
||||
if incompatible_signature?(signature_params)
|
||||
@signed_request_account = nil
|
||||
return
|
||||
end
|
||||
|
||||
account = ResolveRemoteAccountService.new.call(signature_params['keyId'].gsub(/\Aacct:/, ''))
|
||||
|
||||
if account.nil?
|
||||
@signed_request_account = nil
|
||||
return
|
||||
end
|
||||
|
||||
signature = Base64.decode64(signature_params['signature'])
|
||||
compare_signed_string = build_signed_string(signature_params['headers'])
|
||||
|
||||
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
|
||||
@signed_request_account = account
|
||||
@signed_request_account
|
||||
else
|
||||
@signed_request_account = nil
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_signed_string(signed_headers)
|
||||
signed_headers = 'date' if signed_headers.blank?
|
||||
|
||||
signed_headers.split(' ').map do |signed_header|
|
||||
if signed_header == Request::REQUEST_TARGET
|
||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
||||
else
|
||||
"#{signed_header}: #{request.headers[to_header_name(signed_header)]}"
|
||||
end
|
||||
end.join("\n")
|
||||
end
|
||||
|
||||
def matches_time_window?
|
||||
begin
|
||||
time_sent = DateTime.httpdate(request.headers['Date'])
|
||||
rescue ArgumentError
|
||||
return false
|
||||
end
|
||||
|
||||
(Time.now.utc - time_sent).abs <= 30
|
||||
end
|
||||
|
||||
def to_header_name(name)
|
||||
name.split(/-/).map(&:capitalize).join('-')
|
||||
end
|
||||
|
||||
def incompatible_signature?(signature_params)
|
||||
signature_params['keyId'].blank? ||
|
||||
signature_params['signature'].blank? ||
|
||||
signature_params['algorithm'].blank? ||
|
||||
signature_params['algorithm'] != 'rsa-sha256' ||
|
||||
!signature_params['keyId'].start_with?('acct:')
|
||||
end
|
||||
end
|
||||
@@ -5,5 +5,25 @@ class FollowerAccountsController < ApplicationController
|
||||
|
||||
def index
|
||||
@follows = Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
|
||||
format.json do
|
||||
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def collection_presenter
|
||||
ActivityPub::CollectionPresenter.new(
|
||||
id: account_followers_url(@account),
|
||||
type: :ordered,
|
||||
current: account_followers_url(@account),
|
||||
size: @account.followers_count,
|
||||
items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,5 +5,25 @@ class FollowingAccountsController < ApplicationController
|
||||
|
||||
def index
|
||||
@follows = Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
|
||||
format.json do
|
||||
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def collection_presenter
|
||||
ActivityPub::CollectionPresenter.new(
|
||||
id: account_following_index_url(@account),
|
||||
type: :ordered,
|
||||
current: account_following_index_url(@account),
|
||||
size: @account.following_count,
|
||||
items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.target_account) }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,6 +22,7 @@ class HomeController < ApplicationController
|
||||
def initial_state_params
|
||||
{
|
||||
settings: Web::Setting.find_by(user: current_user)&.data || {},
|
||||
push_subscription: current_account.user.web_push_subscription(current_session),
|
||||
current_account: current_account,
|
||||
token: current_session.token,
|
||||
admin: Account.find_local(Setting.site_contact_username),
|
||||
|
||||
@@ -39,6 +39,7 @@ class Settings::PreferencesController < ApplicationController
|
||||
:setting_delete_modal,
|
||||
:setting_auto_play_gif,
|
||||
:setting_system_font_ui,
|
||||
:setting_noindex,
|
||||
notification_emails: %i(follow follow_request reblog favourite mention digest),
|
||||
interactions: %i(must_be_follower must_be_following)
|
||||
)
|
||||
|
||||
@@ -11,10 +11,22 @@ class StatusesController < ApplicationController
|
||||
before_action :check_account_suspension
|
||||
|
||||
def show
|
||||
@ancestors = @status.reply? ? cache_collection(@status.ancestors(current_account), Status) : []
|
||||
@descendants = cache_collection(@status.descendants(current_account), Status)
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
@ancestors = @status.reply? ? cache_collection(@status.ancestors(current_account), Status) : []
|
||||
@descendants = cache_collection(@status.descendants(current_account), Status)
|
||||
|
||||
render 'stream_entries/show'
|
||||
render 'stream_entries/show'
|
||||
end
|
||||
|
||||
format.json do
|
||||
render json: @status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def activity
|
||||
render json: @status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
class StreamEntriesController < ApplicationController
|
||||
include Authorization
|
||||
include SignatureVerification
|
||||
|
||||
layout 'public'
|
||||
|
||||
|
||||
@@ -5,7 +5,27 @@ class TagsController < ApplicationController
|
||||
|
||||
def show
|
||||
@tag = Tag.find_by!(name: params[:id].downcase)
|
||||
@statuses = @tag.nil? ? [] : Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(20, params[:max_id])
|
||||
@statuses = Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(20, params[:max_id])
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
|
||||
format.json do
|
||||
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def collection_presenter
|
||||
ActivityPub::CollectionPresenter.new(
|
||||
id: tag_url(@tag),
|
||||
type: :ordered,
|
||||
current: tag_url(@tag),
|
||||
size: @tag.statuses.count,
|
||||
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user