Improve ActivityPub representations (#3844)

* Improve webfinger templates and make tests more flexible

* Clean up AS2 representation of actor

* Refactor outbox

* Create activities representation

* Add representations of followers/following collections, do not redirect /users/:username route if format is empty

* Remove unused translations

* ActivityPub endpoint for single statuses, add ActivityPub::TagManager for better
URL/URI generation

* Add ActivityPub::TagManager#to

* Represent all attachments as Document instead of Image/Video specifically
(Because for remote ones we may not know for sure)

Add mentions and hashtags representation to AP notes

* Add AP-resolvable hashtag URIs

* Use ActiveModelSerializers for ActivityPub

* Clean up unused translations

* Separate route for object and activity

* Adjust cc/to matrices

* Add to/cc to activities, ensure announce activity embeds target status and
not the wrapper status, add "id" to all collections
pull/81/head
Eugen Rochko 2017-07-15 03:01:39 +02:00 committed by GitHub
parent 3fbf1bf35a
commit 8c45cd0e36
61 changed files with 443 additions and 725 deletions

View File

@ -16,7 +16,9 @@ class AccountsController < ApplicationController
render xml: AtomSerializer.render(AtomSerializer.new.feed(@account, @entries.to_a)) render xml: AtomSerializer.render(AtomSerializer.new.feed(@account, @entries.to_a))
end end
format.activitystreams2 format.json do
render json: @account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter
end
end end
end end

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -5,5 +5,25 @@ class FollowerAccountsController < ApplicationController
def index def index
@follows = Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account) @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
end end

View File

@ -5,5 +5,25 @@ class FollowingAccountsController < ApplicationController
def index def index
@follows = Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account) @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
end end

View File

@ -11,10 +11,22 @@ class StatusesController < ApplicationController
before_action :check_account_suspension before_action :check_account_suspension
def show def show
@ancestors = @status.reply? ? cache_collection(@status.ancestors(current_account), Status) : [] respond_to do |format|
@descendants = cache_collection(@status.descendants(current_account), Status) 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 end
private private

View File

@ -5,7 +5,27 @@ class TagsController < ApplicationController
def show def show
@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, 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) @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
end end

View File

@ -1,8 +0,0 @@
# frozen_string_literal: true
module Activitystreams2BuilderHelper
# Gets a usable name for an account, using display name or username.
def account_name(account)
account.display_name.presence || account.username
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
def self.default_key_transform
:camel_lower
end
def serializable_hash(options = nil)
options = serialization_options(options)
serialized_hash = { '@context': 'https://www.w3.org/ns/activitystreams' }.merge(ActiveModelSerializers::Adapter::Attributes.new(serializer, instance_options).serializable_hash(options))
self.class.transform_key_casing!(serialized_hash, instance_options)
end
end

View File

@ -0,0 +1,69 @@
# frozen_string_literal: true
require 'singleton'
class ActivityPub::TagManager
include Singleton
include RoutingHelper
COLLECTIONS = {
public: 'https://www.w3.org/ns/activitystreams#Public',
}.freeze
def url_for(target)
return target.url if target.respond_to?(:local?) && !target.local?
case target.object_type
when :person
short_account_url(target)
when :note, :comment, :activity
short_account_status_url(target.account, target)
end
end
def uri_for(target)
return target.uri if target.respond_to?(:local?) && !target.local?
case target.object_type
when :person
account_url(target)
when :note, :comment, :activity
account_status_url(target.account, target)
end
end
# Primary audience of a status
# Public statuses go out to primarily the public collection
# Unlisted and private statuses go out primarily to the followers collection
# Others go out only to the people they mention
def to(status)
case status.visibility
when 'public'
[COLLECTIONS[:public]]
when 'unlisted', 'private'
[account_followers_url(status.account)]
when 'direct'
status.mentions.map { |mention| uri_for(mention.account) }
end
end
# Secondary audience of a status
# Public statuses go out to followers as well
# Unlisted statuses go to the public as well
# Both of those and private statuses also go to the people mentioned in them
# Direct ones don't have a secondary audience
def cc(status)
cc = []
case status.visibility
when 'public'
cc << account_followers_url(status.account)
when 'unlisted'
cc << COLLECTIONS[:public]
end
cc.concat(status.mentions.map { |mention| uri_for(mention.account) }) unless status.direct_visibility?
cc
end
end

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
class ActivityPub::CollectionPresenter < ActiveModelSerializers::Model
attributes :id, :type, :current, :size, :items
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
class ActivityPub::ActivitySerializer < ActiveModel::Serializer
attributes :id, :type, :actor, :to, :cc
has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer
def id
[ActivityPub::TagManager.instance.uri_for(object), '/activity'].join
end
def type
object.reblog? ? 'Announce' : 'Create'
end
def actor
ActivityPub::TagManager.instance.uri_for(object.account)
end
def to
ActivityPub::TagManager.instance.to(object)
end
def cc
ActivityPub::TagManager.instance.cc(object)
end
end

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
class ActivityPub::ActorSerializer < ActiveModel::Serializer
include RoutingHelper
attributes :id, :type, :following, :followers,
:inbox, :outbox, :preferred_username,
:name, :summary, :icon, :image
def id
account_url(object)
end
def type
'Person'
end
def following
account_following_index_url(object)
end
def followers
account_followers_url(object)
end
def inbox
nil
end
def outbox
account_outbox_url(object)
end
def preferred_username
object.username
end
def name
object.display_name
end
def summary
Formatter.instance.simplified_format(object)
end
def icon
full_asset_url(object.avatar.url(:original))
end
def image
full_asset_url(object.header.url(:original))
end
end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
class ActivityPub::CollectionSerializer < ActiveModel::Serializer
def self.serializer_for(model, options)
return ActivityPub::ActivitySerializer if model.class.name == 'Status'
super
end
attributes :id, :type, :total_items,
:current
has_many :items, key: :ordered_items
def type
case object.type
when :ordered
'OrderedCollection'
else
'Collection'
end
end
def total_items
object.size
end
end

View File

@ -0,0 +1,106 @@
# frozen_string_literal: true
class ActivityPub::NoteSerializer < ActiveModel::Serializer
attributes :id, :type, :summary, :content,
:in_reply_to, :published, :url,
:actor, :to, :cc, :sensitive
has_many :media_attachments, key: :attachment
has_many :virtual_tags, key: :tag
def id
ActivityPub::TagManager.instance.uri_for(object)
end
def type
'Note'
end
def summary
object.spoiler_text.presence
end
def content
Formatter.instance.format(object)
end
def in_reply_to
ActivityPub::TagManager.instance.uri_for(object.thread) if object.reply?
end
def published
object.created_at.iso8601
end
def url
ActivityPub::TagManager.instance.url_for(object)
end
def actor
ActivityPub::TagManager.instance.uri_for(object.account)
end
def to
ActivityPub::TagManager.instance.to(object)
end
def cc
ActivityPub::TagManager.instance.cc(object)
end
def virtual_tags
object.mentions + object.tags
end
class MediaAttachmentSerializer < ActiveModel::Serializer
include RoutingHelper
attributes :type, :media_type, :url
def type
'Document'
end
def media_type
object.file_content_type
end
def url
object.local? ? full_asset_url(object.file.url(:original, false)) : object.remote_url
end
end
class MentionSerializer < ActiveModel::Serializer
attributes :type, :href, :name
def type
'Mention'
end
def href
ActivityPub::TagManager.instance.uri_for(object.account)
end
def name
"@#{object.account.acct}"
end
end
class TagSerializer < ActiveModel::Serializer
include RoutingHelper
attributes :type, :href, :name
def type
'Hashtag'
end
def href
tag_url(object)
end
def name
"##{object.name}"
end
end
end

View File

@ -1,9 +0,0 @@
extends 'activitypub/types/person.activitystreams2.rabl'
object @account
attributes display_name: :name, username: :preferredUsername, note: :summary
node(:icon) { |account| full_asset_url(account.avatar.url(:original)) }
node(:image) { |account| full_asset_url(account.header.url(:original)) }
node(:outbox) { |account| api_activitypub_outbox_url(account.id) }

View File

@ -1 +0,0 @@
node(:'@context') { 'https://www.w3.org/ns/activitystreams' }

View File

@ -1,3 +0,0 @@
extends 'activitypub/base.activitystreams2.rabl'
node(:id) { request.original_url }

View File

@ -1,3 +0,0 @@
extends 'activitypub/intransient.activitystreams2.rabl'
node(:type) { 'Announce' }

View File

@ -1,3 +0,0 @@
extends 'activitypub/intransient.activitystreams2.rabl'
node(:type) { 'Collection' }

View File

@ -1,3 +0,0 @@
extends 'activitypub/intransient.activitystreams2.rabl'
node(:type) { 'Create' }

View File

@ -1,3 +0,0 @@
extends 'activitypub/intransient.activitystreams2.rabl'
node(:type) { 'Note' }

View File

@ -1,3 +0,0 @@
extends 'activitypub/types/collection.activitystreams2.rabl'
node(:type) { 'OrderedCollection' }

View File

@ -1,3 +0,0 @@
extends 'activitypub/types/ordered_collection.activitystreams2.rabl'
node(:type) { 'OrderedCollectionPage' }

View File

@ -1,3 +0,0 @@
extends 'activitypub/intransient.activitystreams2.rabl'
node(:type) { 'Person' }

View File

@ -1,4 +0,0 @@
object @status
node(:actor) { |status| TagManager.instance.url_for(status.account) }
node(:published) { |status| status.created_at.to_time.xmlschema }

View File

@ -1,8 +0,0 @@
extends 'activitypub/types/announce.activitystreams2.rabl'
extends 'api/activitypub/activities/_show_status.activitystreams2.rabl'
object @status
node(:name) { |status| t('activitypub.activity.announce.name', account_name: account_name(status.account)) }
node(:url) { |status| TagManager.instance.url_for(status) }
node(:object) { |status| api_activitypub_status_url(status.reblog_of_id) }

View File

@ -1,8 +0,0 @@
extends 'activitypub/types/create.activitystreams2.rabl'
extends 'api/activitypub/activities/_show_status.activitystreams2.rabl'
object @status
node(:name) { |status| t('activitypub.activity.create.name', account_name: account_name(status.account)) }
node(:url) { |status| TagManager.instance.url_for(status) }
node(:object) { |status| api_activitypub_note_url(status) }

View File

@ -1,11 +0,0 @@
extends 'activitypub/types/note.activitystreams2.rabl'
object @status
attributes :content
node(:name) { |status| status.content }
node(:url) { |status| TagManager.instance.url_for(status) }
node(:attributedTo) { |status| TagManager.instance.url_for(status.account) }
node(:inReplyTo) { |status| api_activitypub_note_url(status.thread) } if @status.thread
node(:published) { |status| status.created_at.to_time.xmlschema }

View File

@ -1,12 +0,0 @@
extends 'activitypub/types/ordered_collection.activitystreams2.rabl'
object @account
node(:totalItems) { @statuses.count }
node(:current) { @first_page_url } if @first_page_url
node(:first) { @first_page_url } if @first_page_url
node(:last) { @last_page_url } if @last_page_url
node(:name) { |account| t('activitypub.outbox.name', account_name: account_name(account)) }
node(:summary) { |account| t('activitypub.outbox.summary', account_name: account_name(account)) }
node(:updated) { |account| (@statuses.empty? ? account.created_at.to_time : @statuses.first.updated_at.to_time).xmlschema }

View File

@ -1,16 +0,0 @@
extends 'activitypub/types/ordered_collection_page.activitystreams2.rabl'
object @account
node(:items) do
@statuses.map { |status| api_activitypub_status_url(status) }
end
node(:next) { @next_page_url } if @next_page_url
node(:prev) { @prev_page_url } if @prev_page_url
node(:current) { @first_page_url } if @first_page_url
node(:first) { @first_page_url } if @first_page_url
node(:last) { @last_page_url } if @last_page_url
node(:partOf) { @part_of_url } if @part_of_url
node(:updated) { |account| (@statuses.empty? ? account.created_at.to_time : @statuses.first.updated_at.to_time).xmlschema }

View File

@ -3,14 +3,14 @@ object @account
node(:subject) { @canonical_account_uri } node(:subject) { @canonical_account_uri }
node(:aliases) do node(:aliases) do
[TagManager.instance.url_for(@account), TagManager.instance.uri_for(@account)] [short_account_url(@account), account_url(@account)]
end end
node(:links) do node(:links) do
[ [
{ rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: TagManager.instance.url_for(@account) }, { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: account_url(@account) },
{ rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }, { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(@account, format: 'atom') },
{ rel: 'self', type: 'application/activity+json', href: TagManager.instance.url_for(@account) }, { rel: 'self', type: 'application/activity+json', href: account_url(@account) },
{ rel: 'salmon', href: api_salmon_url(@account.id) }, { rel: 'salmon', href: api_salmon_url(@account.id) },
{ rel: 'magic-public-key', href: "data:application/magic-public-key,#{@magic_key}" }, { rel: 'magic-public-key', href: "data:application/magic-public-key,#{@magic_key}" },
{ rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_follow_url}?acct={uri}" }, { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_follow_url}?acct={uri}" },

View File

@ -1,10 +1,11 @@
Nokogiri::XML::Builder.new do |xml| Nokogiri::XML::Builder.new do |xml|
xml.XRD(xmlns: 'http://docs.oasis-open.org/ns/xri/xrd-1.0') do xml.XRD(xmlns: 'http://docs.oasis-open.org/ns/xri/xrd-1.0') do
xml.Subject @canonical_account_uri xml.Subject @canonical_account_uri
xml.Alias TagManager.instance.url_for(@account) xml.Alias short_account_url(@account)
xml.Alias TagManager.instance.uri_for(@account) xml.Alias account_url(@account)
xml.Link(rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: TagManager.instance.url_for(@account)) xml.Link(rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: TagManager.instance.url_for(@account))
xml.Link(rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(@account, format: 'atom')) xml.Link(rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(@account, format: 'atom'))
xml.Link(rel: 'self', type: 'application/activity+json', href: account_url(@account))
xml.Link(rel: 'salmon', href: api_salmon_url(@account.id)) xml.Link(rel: 'salmon', href: api_salmon_url(@account.id))
xml.Link(rel: 'magic-public-key', href: "data:application/magic-public-key,#{@magic_key}") xml.Link(rel: 'magic-public-key', href: "data:application/magic-public-key,#{@magic_key}")
xml.Link(rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_follow_url}?acct={uri}") xml.Link(rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_follow_url}?acct={uri}")

View File

@ -14,4 +14,6 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'StatsD' inflect.acronym 'StatsD'
inflect.acronym 'OEmbed' inflect.acronym 'OEmbed'
inflect.acronym 'ActivityPub' inflect.acronym 'ActivityPub'
inflect.acronym 'PubSubHubbub'
inflect.acronym 'ActivityStreams'
end end

View File

@ -1,5 +1,4 @@
# Be sure to restart your server when you modify this file. # Be sure to restart your server when you modify this file.
Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest application/jrd+json ) Mime::Type.register 'application/json', :json, %w(text/x-json application/jsonrequest application/jrd+json application/activity+json)
Mime::Type.register "text/xml", :xml, %w( application/xml application/atom+xml application/xrd+xml ) Mime::Type.register 'text/xml', :xml, %w(application/xml application/atom+xml application/xrd+xml)
Mime::Type.register "application/activity+json", :activitystreams2

View File

@ -30,15 +30,6 @@ ca:
remote_follow: Seguir remote_follow: Seguir
reserved_username: El nom d'usuari està reservat reserved_username: El nom d'usuari està reservat
unfollow: Deixar de seguir unfollow: Deixar de seguir
activitypub:
activity:
announce:
name: "%{account_name} shared an activity."
create:
name: "%{account_name} created a note."
outbox:
name: "%{account_name}'s Outbox"
summary: A collection of activities from user %{account_name}.
admin: admin:
accounts: accounts:
are_you_sure: Estàs segur? are_you_sure: Estàs segur?

View File

@ -44,15 +44,6 @@ en:
remote_follow: Remote follow remote_follow: Remote follow
reserved_username: The username is reserved reserved_username: The username is reserved
unfollow: Unfollow unfollow: Unfollow
activitypub:
activity:
announce:
name: "%{account_name} shared an activity."
create:
name: "%{account_name} created a note."
outbox:
name: "%{account_name}'s Outbox"
summary: A collection of activities from user %{account_name}.
admin: admin:
accounts: accounts:
are_you_sure: Are you sure? are_you_sure: Are you sure?

View File

@ -29,15 +29,6 @@ fa:
posts: نوشته posts: نوشته
remote_follow: پیگیری غیرمستقیم remote_follow: پیگیری غیرمستقیم
unfollow: پایان پیگیری unfollow: پایان پیگیری
activitypub:
activity:
announce:
name: "%{account_name} فعالیتی آغاز کرد."
create:
name: "%{account_name} یادداشتی نوشت."
outbox:
name: صندوق خروجی %{account_name}
summary: مجموعه‌ای از فعالیت‌های کاربر %{account_name}.
admin: admin:
accounts: accounts:
are_you_sure: آیا مطمئن هستید؟ are_you_sure: آیا مطمئن هستید؟

View File

@ -30,15 +30,6 @@ fr:
remote_follow: Suivre à distance remote_follow: Suivre à distance
reserved_username: Ce nom dutilisateur⋅ice est réservé reserved_username: Ce nom dutilisateur⋅ice est réservé
unfollow: Ne plus suivre unfollow: Ne plus suivre
activitypub:
activity:
announce:
name: "%{account_name} a partagé une activité."
create:
name: "%{account_name} a créé une note."
outbox:
name: Boîte denvoi de %{account_name}
summary: Liste dactivités de %{account_name}
admin: admin:
accounts: accounts:
are_you_sure: Êtes-vous certain⋅e? are_you_sure: Êtes-vous certain⋅e?

View File

@ -29,15 +29,6 @@ he:
posts: הודעות posts: הודעות
remote_follow: מעקב מרחוק remote_follow: מעקב מרחוק
unfollow: הפסקת מעקב unfollow: הפסקת מעקב
activitypub:
activity:
announce:
name: הודעה שותפה על ידי %{account_name}.
create:
name: הודעה חדשה מאת %{account_name}.
outbox:
name: תיבת הדוא"ל היוצא של %{account_name}
summary: אוסף הפעילויות של %{account_name}.
admin: admin:
accounts: accounts:
are_you_sure: בטוח? are_you_sure: בטוח?

View File

@ -29,15 +29,6 @@ id:
posts: Postingan posts: Postingan
remote_follow: Mengikuti remote_follow: Mengikuti
unfollow: Berhenti mengikuti unfollow: Berhenti mengikuti
activitypub:
activity:
announce:
name: "%{account_name} membagikan aktivitas."
create:
name: "%{account_name} membuat catatan."
outbox:
name: "%{account_name} Outbox"
summary: Koleksi aktivitas dari pengguna %{account_name}.
admin: admin:
accounts: accounts:
are_you_sure: Anda yakin? are_you_sure: Anda yakin?

View File

@ -30,15 +30,6 @@ ja:
remote_follow: リモートフォロー remote_follow: リモートフォロー
reserved_username: このユーザー名は予約されています。 reserved_username: このユーザー名は予約されています。
unfollow: フォロー解除 unfollow: フォロー解除
activitypub:
activity:
announce:
name: "%{account_name} さんがアクティビティをシェアしました"
create:
name: "%{account_name} さんがノートを作成しました"
outbox:
name: "%{account_name} さんの送信トレイ"
summary: "%{account_name} さんからのアクティビティコレクション"
admin: admin:
accounts: accounts:
are_you_sure: 本当に実行しますか? are_you_sure: 本当に実行しますか?

View File

@ -30,15 +30,6 @@ ko:
remote_follow: 리모트 팔로우 remote_follow: 리모트 팔로우
reserved_username: 이 아이디는 예약되어 있습니다. reserved_username: 이 아이디는 예약되어 있습니다.
unfollow: 팔로우 해제 unfollow: 팔로우 해제
activitypub:
activity:
announce:
name: "%{account_name} 님이 액티비티를 공유했습니다"
create:
name: "%{account_name} 님이 노트를 작성했습니다"
outbox:
name: "%{account_name} 님의 송신함"
summary: "%{account_name} 님의 액티비티 모음"
admin: admin:
accounts: accounts:
are_you_sure: 정말로 실행하시겠습니까? are_you_sure: 정말로 실행하시겠습니까?

View File

@ -29,15 +29,6 @@
posts: Poster posts: Poster
remote_follow: Følg fra andre instanser remote_follow: Følg fra andre instanser
unfollow: Avfølg unfollow: Avfølg
activitypub:
activity:
announce:
name: "%{account_name} delte en aktivitet."
create:
name: "%{account_name} laget en aktivitet."
outbox:
name: "%{account_name} sin utboks"
summary: En samling aktiviteter fra brukeren %{account_name}.
admin: admin:
accounts: accounts:
are_you_sure: Er du sikker? are_you_sure: Er du sikker?

View File

@ -29,15 +29,6 @@ oc:
posts: Estatuts posts: Estatuts
remote_follow: Sègre a distància remote_follow: Sègre a distància
unfollow: Quitar de sègre unfollow: Quitar de sègre
activitypub:
activity:
announce:
name: "%{account_name} a partejat una activitat."
create:
name: "%{account_name} a creat una nòta."
outbox:
name: "%{account_name}'s Outbox"
summary: A collection of activities from user %{account_name}.
admin: admin:
accounts: accounts:
are_you_sure: Sètz segur ? are_you_sure: Sètz segur ?

View File

@ -44,15 +44,6 @@ pl:
remote_follow: Zdalne śledzenie remote_follow: Zdalne śledzenie
reserved_username: Ta nazwa użytkownika jest zarezerwowana. reserved_username: Ta nazwa użytkownika jest zarezerwowana.
unfollow: Przestań śledzić unfollow: Przestań śledzić
activitypub:
activity:
announce:
name: "%{account_name} udostępnił(a) aktywność."
create:
name: "%{account_name} utworzył(a) wpis."
outbox:
name: Skrzynka %{account_name}
summary: Zbiór aktywności użytkownika %{account_name}.
admin: admin:
accounts: accounts:
are_you_sure: Jesteś tego pewien? are_you_sure: Jesteś tego pewien?

View File

@ -29,15 +29,6 @@ pt-BR:
posts: Posts posts: Posts
remote_follow: Acesso remoto remote_follow: Acesso remoto
unfollow: Unfollow unfollow: Unfollow
activitypub:
activity:
announce:
name: "%{account_name} compartilhou uma atividade."
create:
name: "%{account_name} criou uma nota."
outbox:
name: "%{account_name}'s Outbox"
summary: Uma coleção de atividades do usuário %{account_name}.
admin: admin:
accounts: accounts:
are_you_sure: Você tem certeza? are_you_sure: Você tem certeza?

View File

@ -29,15 +29,6 @@ pt:
posts: Posts posts: Posts
remote_follow: Seguir remotamente remote_follow: Seguir remotamente
unfollow: Deixar de seguir unfollow: Deixar de seguir
activitypub:
activity:
announce:
name: "%{account_name} anunciou uma atividade."
create:
name: "%{account_name} criou uma nota."
outbox:
name: "%{account_name}'s Outbox"
summary: Uma coleção de atividades do usuário %{account_name}.
admin: admin:
accounts: accounts:
are_you_sure: Tens a certeza? are_you_sure: Tens a certeza?

View File

@ -29,15 +29,6 @@ th:
posts: โพสต์ posts: โพสต์
remote_follow: Remote follow remote_follow: Remote follow
unfollow: เลิกติดตาม unfollow: เลิกติดตาม
activitypub:
activity:
announce:
name: "%{account_name} แชร์กิจกรรม."
create:
name: "%{account_name} สร้างโน๊ต."
outbox:
name: "%{account_name}'s Outbox"
summary: รวมกิจกรรมของผู้ใช้ %{account_name}.
admin: admin:
accounts: accounts:
are_you_sure: แน่ใจนะ? are_you_sure: แน่ใจนะ?

View File

@ -29,15 +29,6 @@ tr:
posts: Gönderiler posts: Gönderiler
remote_follow: Uzaktan takip et remote_follow: Uzaktan takip et
unfollow: Takibi bırak unfollow: Takibi bırak
activitypub:
activity:
announce:
name: "%{account_name} bir aktivite paylaştı."
create:
name: "%{account_name} bir not oluşturdu."
outbox:
name: "%{account_name}'in Gönderdikleri"
summary: "%{account_name}'den gelen aktiviteler."
admin: admin:
accounts: accounts:
are_you_sure: Emin misiniz? are_you_sure: Emin misiniz?

View File

@ -29,15 +29,6 @@ zh-CN:
posts: 嘟文 posts: 嘟文
remote_follow: 跨站关注 remote_follow: 跨站关注
unfollow: 取消关注 unfollow: 取消关注
activitypub:
activity:
announce:
name: "%{account_name} 分享了一个活动。"
create:
name: "%{account_name} 创建了一个记事。"
outbox:
name: "%{account_name} 的集合"
summary: "%{account_name} 的活动集合"
admin: admin:
accounts: accounts:
are_you_sure: 你确定吗? are_you_sure: 你确定吗?

View File

@ -29,15 +29,6 @@ zh-HK:
posts: 文章 posts: 文章
remote_follow: 跨站關注 remote_follow: 跨站關注
unfollow: 取消關注 unfollow: 取消關注
activitypub:
activity:
announce:
name: "%{account_name} 分享了一項活動。"
create:
name: "%{account_name} 新增了一篇筆記。"
outbox:
name: "%{account_name} 的活動"
summary: "%{account_name} 分享的活動列表。"
admin: admin:
accounts: accounts:
are_you_sure: 你確定嗎? are_you_sure: 你確定嗎?

View File

@ -26,7 +26,7 @@ Rails.application.routes.draw do
confirmations: 'auth/confirmations', confirmations: 'auth/confirmations',
} }
get '/users/:username', to: redirect('/@%{username}'), constraints: { format: :html } get '/users/:username', to: redirect('/@%{username}'), constraints: lambda { |req| req.format.nil? }
resources :accounts, path: 'users', only: [:show], param: :username do resources :accounts, path: 'users', only: [:show], param: :username do
resources :stream_entries, path: 'updates', only: [:show] do resources :stream_entries, path: 'updates', only: [:show] do
@ -38,10 +38,17 @@ Rails.application.routes.draw do
get :remote_follow, to: 'remote_follow#new' get :remote_follow, to: 'remote_follow#new'
post :remote_follow, to: 'remote_follow#create' post :remote_follow, to: 'remote_follow#create'
resources :statuses, only: [:show] do
member do
get :activity
end
end
resources :followers, only: [:index], controller: :follower_accounts resources :followers, only: [:index], controller: :follower_accounts
resources :following, only: [:index], controller: :following_accounts resources :following, only: [:index], controller: :following_accounts
resource :follow, only: [:create], controller: :account_follow resource :follow, only: [:create], controller: :account_follow
resource :unfollow, only: [:create], controller: :account_unfollow resource :unfollow, only: [:create], controller: :account_unfollow
resource :outbox, only: [:show], module: :activitypub
end end
get '/@:username', to: 'accounts#show', as: :short_account get '/@:username', to: 'accounts#show', as: :short_account
@ -119,13 +126,6 @@ Rails.application.routes.draw do
# OEmbed # OEmbed
get '/oembed', to: 'oembed#show', as: :oembed get '/oembed', to: 'oembed#show', as: :oembed
# ActivityPub
namespace :activitypub do
get '/users/:id/outbox', to: 'outbox#show', as: :outbox
get '/statuses/:id', to: 'activities#show_status', as: :status
resources :notes, only: [:show]
end
# 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

View File

@ -38,7 +38,7 @@ RSpec.describe AccountsController, type: :controller do
context 'activitystreams2' do context 'activitystreams2' do
before do before do
get :show, params: { username: alice.username }, format: 'activitystreams2' get :show, params: { username: alice.username }, format: 'json'
end end
it 'assigns @account' do it 'assigns @account' do

View File

@ -1,69 +0,0 @@
require 'rails_helper'
RSpec.describe Api::ActivityPub::ActivitiesController, type: :controller do
render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
describe 'GET #show' do
describe 'normal status' do
public_status = nil
before do
public_status = Fabricate(:status, account: user.account, text: 'Hello world', visibility: :public)
@request.env['HTTP_ACCEPT'] = 'application/activity+json'
get :show_status, params: { id: public_status.id }
end
it 'returns http success' do
expect(response).to have_http_status(:success)
end
it 'sets Content-Type header to AS2' do
expect(response.header['Content-Type']).to include 'application/activity+json'
end
it 'returns http success' do
json_data = JSON.parse(response.body)
expect(json_data).to include('@context' => 'https://www.w3.org/ns/activitystreams')
expect(json_data).to include('type' => 'Create')
expect(json_data).to include('id' => @request.url)
expect(json_data).to include('type' => 'Create')
expect(json_data).to include('object' => api_activitypub_note_url(public_status))
expect(json_data).to include('url' => TagManager.instance.url_for(public_status))
end
end
describe 'reblog' do
original = nil
reblog = nil
before do
original = Fabricate(:status, account: user.account, text: 'Hello world', visibility: :public)
reblog = Fabricate(:status, account: user.account, reblog_of_id: original.id, visibility: :public)
@request.env['HTTP_ACCEPT'] = 'application/activity+json'
get :show_status, params: { id: reblog.id }
end
it 'returns http success' do
expect(response).to have_http_status(:success)
end
it 'sets Content-Type header to AS2' do
expect(response.header['Content-Type']).to include 'application/activity+json'
end
it 'returns http success' do
json_data = JSON.parse(response.body)
expect(json_data).to include('@context' => 'https://www.w3.org/ns/activitystreams')
expect(json_data).to include('type' => 'Announce')
expect(json_data).to include('id' => @request.url)
expect(json_data).to include('type' => 'Announce')
expect(json_data).to include('object' => api_activitypub_status_url(original))
expect(json_data).to include('url' => TagManager.instance.url_for(reblog))
end
end
end
end

View File

@ -1,73 +0,0 @@
require 'rails_helper'
RSpec.describe Api::ActivityPub::NotesController, type: :controller do
render_views
let(:user_alice) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:user_bob) { Fabricate(:user, account: Fabricate(:account, username: 'bob')) }
describe 'GET #show' do
describe 'normal status' do
public_status = nil
before do
public_status = Fabricate(:status, account: user_alice.account, text: 'Hello world', visibility: :public)
@request.env['HTTP_ACCEPT'] = 'application/activity+json'
get :show, params: { id: public_status.id }
end
it 'returns http success' do
expect(response).to have_http_status(:success)
end
it 'sets Content-Type header to AS2' do
expect(response.header['Content-Type']).to include 'application/activity+json'
end
it 'returns http success' do
json_data = JSON.parse(response.body)
expect(json_data).to include('@context' => 'https://www.w3.org/ns/activitystreams')
expect(json_data).to include('type' => 'Note')
expect(json_data).to include('id' => @request.url)
expect(json_data).to include('name' => 'Hello world')
expect(json_data).to include('content' => 'Hello world')
expect(json_data).to include('published')
expect(json_data).to include('url' => TagManager.instance.url_for(public_status))
end
end
describe 'reply' do
original = nil
reply = nil
before do
original = Fabricate(:status, account: user_alice.account, text: 'Hello world', visibility: :public)
reply = Fabricate(:status, account: user_bob.account, text: 'Hello world', in_reply_to_id: original.id, visibility: :public)
@request.env['HTTP_ACCEPT'] = 'application/activity+json'
get :show, params: { id: reply.id }
end
it 'returns http success' do
expect(response).to have_http_status(:success)
end
it 'sets Content-Type header to AS2' do
expect(response.header['Content-Type']).to include 'application/activity+json'
end
it 'returns http success' do
json_data = JSON.parse(response.body)
expect(json_data).to include('@context' => 'https://www.w3.org/ns/activitystreams')
expect(json_data).to include('type' => 'Note')
expect(json_data).to include('id' => @request.url)
expect(json_data).to include('name' => 'Hello world')
expect(json_data).to include('content' => 'Hello world')
expect(json_data).to include('published')
expect(json_data).to include('url' => TagManager.instance.url_for(reply))
expect(json_data).to include('inReplyTo' => api_activitypub_note_url(original))
end
end
end
end

View File

@ -1,156 +0,0 @@
require 'rails_helper'
RSpec.describe Api::ActivityPub::OutboxController, type: :controller do
render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
describe 'GET #show' do
before do
@request.headers['ACCEPT'] = 'application/activity+json'
end
describe 'collection with small number of statuses' do
public_status = nil
before do
public_status = Fabricate(:status, account: user.account, text: 'Hello world', visibility: :public)
Fabricate(:status, account: user.account, text: 'Hello world', visibility: :private)
Fabricate(:status, account: user.account, text: 'Hello world', visibility: :unlisted)
Fabricate(:status, account: user.account, text: 'Hello world', visibility: :direct)
get :show, params: { id: user.account.id }
end
it 'returns http success' do
expect(response).to have_http_status(:success)
end
it 'sets Content-Type header to AS2' do
expect(response.header['Content-Type']).to include 'application/activity+json'
end
it 'returns AS2 JSON body' do
json_data = JSON.parse(response.body)
expect(json_data).to include('@context' => 'https://www.w3.org/ns/activitystreams')
expect(json_data).to include('id' => @request.url)
expect(json_data).to include('type' => 'OrderedCollection')
expect(json_data).to include('totalItems' => 1)
expect(json_data).to include('current')
expect(json_data).to include('first')
expect(json_data).to include('last')
end
end
describe 'collection with large number of statuses' do
before do
30.times do
Fabricate(:status, account: user.account, text: 'Hello world', visibility: :public)
end
Fabricate(:status, account: user.account, text: 'Hello world', visibility: :private)
Fabricate(:status, account: user.account, text: 'Hello world', visibility: :unlisted)
Fabricate(:status, account: user.account, text: 'Hello world', visibility: :direct)
get :show, params: { id: user.account.id }
end
it 'returns http success' do
expect(response).to have_http_status(:success)
end
it 'sets Content-Type header to AS2' do
expect(response.header['Content-Type']).to include 'application/activity+json'
end
it 'returns AS2 JSON body' do
json_data = JSON.parse(response.body)
expect(json_data).to include('@context' => 'https://www.w3.org/ns/activitystreams')
expect(json_data).to include('id' => @request.url)
expect(json_data).to include('type' => 'OrderedCollection')
expect(json_data).to include('totalItems' => 30)
expect(json_data).to include('current')
expect(json_data).to include('first')
expect(json_data).to include('last')
end
end
describe 'page with small number of statuses' do
statuses = []
before do
5.times do
statuses << Fabricate(:status, account: user.account, text: 'Hello world', visibility: :public)
end
Fabricate(:status, account: user.account, text: 'Hello world', visibility: :private)
Fabricate(:status, account: user.account, text: 'Hello world', visibility: :unlisted)
Fabricate(:status, account: user.account, text: 'Hello world', visibility: :direct)
get :show, params: { id: user.account.id, max_id: statuses.last.id + 1 }
end
it 'returns http success' do
expect(response).to have_http_status(:success)
end
it 'sets Content-Type header to AS2' do
expect(response.header['Content-Type']).to include 'application/activity+json'
end
it 'returns AS2 JSON body' do
json_data = JSON.parse(response.body)
expect(json_data).to include('@context' => 'https://www.w3.org/ns/activitystreams')
expect(json_data).to include('id' => @request.url)
expect(json_data).to include('type' => 'OrderedCollectionPage')
expect(json_data).to include('partOf')
expect(json_data).to include('items')
expect(json_data['items'].length).to eq(5)
expect(json_data).to include('prev')
expect(json_data).to include('next')
expect(json_data).to include('current')
expect(json_data).to include('first')
expect(json_data).to include('last')
end
end
describe 'page with large number of statuses' do
statuses = []
before do
30.times do
statuses << Fabricate(:status, account: user.account, text: 'Hello world', visibility: :public)
end
Fabricate(:status, account: user.account, text: 'Hello world', visibility: :private)
Fabricate(:status, account: user.account, text: 'Hello world', visibility: :unlisted)
Fabricate(:status, account: user.account, text: 'Hello world', visibility: :direct)
get :show, params: { id: user.account.id, max_id: statuses.last.id + 1 }
end
it 'returns http success' do
expect(response).to have_http_status(:success)
end
it 'sets Content-Type header to AS2' do
expect(response.header['Content-Type']).to include 'application/activity+json'
end
it 'returns AS2 JSON body' do
json_data = JSON.parse(response.body)
expect(json_data).to include('@context' => 'https://www.w3.org/ns/activitystreams')
expect(json_data).to include('id' => @request.url)
expect(json_data).to include('type' => 'OrderedCollectionPage')
expect(json_data).to include('partOf')
expect(json_data).to include('items')
expect(json_data['items'].length).to eq(20)
expect(json_data).to include('prev')
expect(json_data).to include('next')
expect(json_data).to include('current')
expect(json_data).to include('first')
expect(json_data).to include('last')
end
end
end
end

View File

@ -9,7 +9,7 @@ describe WellKnown::WebfingerController, type: :controller do
end end
before do before do
alice.private_key = <<PEM alice.private_key = <<-PEM
-----BEGIN RSA PRIVATE KEY----- -----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDHgPoPJlrfMZrVcuF39UbVssa8r4ObLP3dYl9Y17Mgp5K4mSYD MIICXQIBAAKBgQDHgPoPJlrfMZrVcuF39UbVssa8r4ObLP3dYl9Y17Mgp5K4mSYD
R/Y2ag58tSi6ar2zM3Ze3QYsNfTq0NqN1g89eAu0MbSjWqpOsgntRPJiFuj3hai2 R/Y2ag58tSi6ar2zM3Ze3QYsNfTq0NqN1g89eAu0MbSjWqpOsgntRPJiFuj3hai2
@ -27,7 +27,7 @@ FTX8IvYBNTbpEttc1VCf/0ccnNpfb0CrFNSPWxRj7t7D
-----END RSA PRIVATE KEY----- -----END RSA PRIVATE KEY-----
PEM PEM
alice.public_key = <<PEM alice.public_key = <<-PEM
-----BEGIN PUBLIC KEY----- -----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHgPoPJlrfMZrVcuF39UbVssa8 MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHgPoPJlrfMZrVcuF39UbVssa8
r4ObLP3dYl9Y17Mgp5K4mSYDR/Y2ag58tSi6ar2zM3Ze3QYsNfTq0NqN1g89eAu0 r4ObLP3dYl9Y17Mgp5K4mSYDR/Y2ag58tSi6ar2zM3Ze3QYsNfTq0NqN1g89eAu0
@ -48,29 +48,23 @@ PEM
it 'returns JSON when account can be found' do it 'returns JSON when account can be found' do
get :show, params: { resource: alice.to_webfinger_s }, format: :json get :show, params: { resource: alice.to_webfinger_s }, format: :json
json = body_as_json
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
expect(response.content_type).to eq 'application/jrd+json' expect(response.content_type).to eq 'application/jrd+json'
expect(response.body).to eq "{\"subject\":\"acct:alice@cb6e6126.ngrok.io\",\"aliases\":[\"https://cb6e6126.ngrok.io/@alice\",\"https://cb6e6126.ngrok.io/users/alice\"],\"links\":[{\"rel\":\"http://webfinger.net/rel/profile-page\",\"type\":\"text/html\",\"href\":\"https://cb6e6126.ngrok.io/@alice\"},{\"rel\":\"http://schemas.google.com/g/2010#updates-from\",\"type\":\"application/atom+xml\",\"href\":\"https://cb6e6126.ngrok.io/users/alice.atom\"},{\"rel\":\"self\",\"type\":\"application/activity+json\",\"href\":\"https://cb6e6126.ngrok.io/@alice\"},{\"rel\":\"salmon\",\"href\":\"#{api_salmon_url(alice.id)}\"},{\"rel\":\"magic-public-key\",\"href\":\"data:application/magic-public-key,RSA.x4D6DyZa3zGa1XLhd_VG1bLGvK-Dmyz93WJfWNezIKeSuJkmA0f2NmoOfLUoumq9szN2Xt0GLDX06tDajdYPPXgLtDG0o1qqTrIJ7UTyYhbo94Wotl9iJvEwa5IjP1Mn00YJ_KvFrzKCm15PC7up6r-NtHsqoYS8X1KAqcbnptU=.AQAB\"},{\"rel\":\"http://ostatus.org/schema/1.0/subscribe\",\"template\":\"https://cb6e6126.ngrok.io/authorize_follow?acct={uri}\"}]}" expect(json[:subject]).to eq 'acct:alice@cb6e6126.ngrok.io'
expect(json[:aliases]).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice')
end end
it 'returns JSON when account can be found' do it 'returns JSON when account can be found' do
get :show, params: { resource: alice.to_webfinger_s }, format: :xml get :show, params: { resource: alice.to_webfinger_s }, format: :xml
xml = Nokogiri::XML(response.body)
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
expect(response.content_type).to eq 'application/xrd+xml' expect(response.content_type).to eq 'application/xrd+xml'
expect(response.body).to eq <<"XML" expect(xml.at_xpath('//xmlns:Subject').content).to eq 'acct:alice@cb6e6126.ngrok.io'
<?xml version="1.0"?> expect(xml.xpath('//xmlns:Alias').map(&:content)).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice')
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
<Subject>acct:alice@cb6e6126.ngrok.io</Subject>
<Alias>https://cb6e6126.ngrok.io/@alice</Alias>
<Alias>https://cb6e6126.ngrok.io/users/alice</Alias>
<Link rel="http://webfinger.net/rel/profile-page" type="text/html" href="https://cb6e6126.ngrok.io/@alice"/>
<Link rel="http://schemas.google.com/g/2010#updates-from" type="application/atom+xml" href="https://cb6e6126.ngrok.io/users/alice.atom"/>
<Link rel="salmon" href="#{api_salmon_url(alice.id)}"/>
<Link rel="magic-public-key" href="data:application/magic-public-key,RSA.x4D6DyZa3zGa1XLhd_VG1bLGvK-Dmyz93WJfWNezIKeSuJkmA0f2NmoOfLUoumq9szN2Xt0GLDX06tDajdYPPXgLtDG0o1qqTrIJ7UTyYhbo94Wotl9iJvEwa5IjP1Mn00YJ_KvFrzKCm15PC7up6r-NtHsqoYS8X1KAqcbnptU=.AQAB"/>
<Link rel="http://ostatus.org/schema/1.0/subscribe" template="https://cb6e6126.ngrok.io/authorize_follow?acct={uri}"/>
</XRD>
XML
end end
it 'returns http not found when account cannot be found' do it 'returns http not found when account cannot be found' do
@ -80,19 +74,22 @@ XML
end end
it 'returns JSON when account can be found with alternate domains' do it 'returns JSON when account can be found with alternate domains' do
Rails.configuration.x.alternate_domains = ["foo.org"] Rails.configuration.x.alternate_domains = ['foo.org']
username, domain = alice.to_webfinger_s.split("@") username, = alice.to_webfinger_s.split('@')
get :show, params: { resource: "#{username}@foo.org" }, format: :json get :show, params: { resource: "#{username}@foo.org" }, format: :json
json = body_as_json
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
expect(response.content_type).to eq 'application/jrd+json' expect(response.content_type).to eq 'application/jrd+json'
expect(response.body).to eq "{\"subject\":\"acct:alice@cb6e6126.ngrok.io\",\"aliases\":[\"https://cb6e6126.ngrok.io/@alice\",\"https://cb6e6126.ngrok.io/users/alice\"],\"links\":[{\"rel\":\"http://webfinger.net/rel/profile-page\",\"type\":\"text/html\",\"href\":\"https://cb6e6126.ngrok.io/@alice\"},{\"rel\":\"http://schemas.google.com/g/2010#updates-from\",\"type\":\"application/atom+xml\",\"href\":\"https://cb6e6126.ngrok.io/users/alice.atom\"},{\"rel\":\"self\",\"type\":\"application/activity+json\",\"href\":\"https://cb6e6126.ngrok.io/@alice\"},{\"rel\":\"salmon\",\"href\":\"#{api_salmon_url(alice.id)}\"},{\"rel\":\"magic-public-key\",\"href\":\"data:application/magic-public-key,RSA.x4D6DyZa3zGa1XLhd_VG1bLGvK-Dmyz93WJfWNezIKeSuJkmA0f2NmoOfLUoumq9szN2Xt0GLDX06tDajdYPPXgLtDG0o1qqTrIJ7UTyYhbo94Wotl9iJvEwa5IjP1Mn00YJ_KvFrzKCm15PC7up6r-NtHsqoYS8X1KAqcbnptU=.AQAB\"},{\"rel\":\"http://ostatus.org/schema/1.0/subscribe\",\"template\":\"https://cb6e6126.ngrok.io/authorize_follow?acct={uri}\"}]}" expect(json[:subject]).to eq 'acct:alice@cb6e6126.ngrok.io'
expect(json[:aliases]).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice')
end end
it 'returns http not found when account can not be found with alternate domains' do it 'returns http not found when account can not be found with alternate domains' do
Rails.configuration.x.alternate_domains = ["foo.org"] Rails.configuration.x.alternate_domains = ['foo.org']
username, domain = alice.to_webfinger_s.split("@") username, = alice.to_webfinger_s.split('@')
get :show, params: { resource: "#{username}@bar.org" }, format: :json get :show, params: { resource: "#{username}@bar.org" }, format: :json

View File

@ -1,15 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe Activitystreams2BuilderHelper, type: :helper do
it 'returns display name if present' do
account = Fabricate(:account, display_name: 'display name', username: 'username')
expect(account_name(account)).to eq 'display name'
end
it 'returns username if display name is not present' do
account = Fabricate(:account, display_name: '', username: 'username')
expect(account_name(account)).to eq 'username'
end
end