-
+
{accountIds.map(id =>
)}
{loadMore}
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index 6f4a3b491..9b00f0f52 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -46,6 +46,10 @@ class ActivityPub::Activity
ActivityPub::Activity::Reject
when 'Flag'
ActivityPub::Activity::Flag
+ when 'Add'
+ ActivityPub::Activity::Add
+ when 'Remove'
+ ActivityPub::Activity::Remove
end
end
end
diff --git a/app/lib/activitypub/activity/add.rb b/app/lib/activitypub/activity/add.rb
new file mode 100644
index 000000000..ea94d2f98
--- /dev/null
+++ b/app/lib/activitypub/activity/add.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class ActivityPub::Activity::Add < ActivityPub::Activity
+ def perform
+ return unless @json['target'].present? && value_or_id(@json['target']) == @account.featured_collection_url
+
+ status = status_from_uri(object_uri)
+
+ return unless status.account_id == @account.id && !@account.pinned?(status)
+
+ StatusPin.create!(account: @account, status: status)
+ end
+end
diff --git a/app/lib/activitypub/activity/remove.rb b/app/lib/activitypub/activity/remove.rb
new file mode 100644
index 000000000..97cee5116
--- /dev/null
+++ b/app/lib/activitypub/activity/remove.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class ActivityPub::Activity::Remove < ActivityPub::Activity
+ def perform
+ return unless @json['origin'].present? && value_or_id(@json['origin']) == @account.featured_collection_url
+
+ status = status_from_uri(object_uri)
+
+ return unless status.account_id == @account.id
+
+ pin = StatusPin.find_by(account: @account, status: status)
+ pin&.destroy!
+ end
+end
diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb
index 8198ac580..f19b04ae6 100644
--- a/app/lib/activitypub/adapter.rb
+++ b/app/lib/activitypub/adapter.rb
@@ -18,6 +18,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
'toot' => 'http://joinmastodon.org/ns#',
'Emoji' => 'toot:Emoji',
'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' },
+ 'featured' => 'toot:featured',
},
],
}.freeze
diff --git a/app/models/account.rb b/app/models/account.rb
index 13692d0d7..c1347fe65 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -43,6 +43,7 @@
# protocol :integer default("ostatus"), not null
# memorial :boolean default(FALSE), not null
# moved_to_account_id :integer
+# featured_collection_url :string
#
class Account < ApplicationRecord
diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb
index 622bdde0c..afcd37771 100644
--- a/app/serializers/activitypub/actor_serializer.rb
+++ b/app/serializers/activitypub/actor_serializer.rb
@@ -4,7 +4,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
include RoutingHelper
attributes :id, :type, :following, :followers,
- :inbox, :outbox,
+ :inbox, :outbox, :featured,
:preferred_username, :name, :summary,
:url, :manually_approves_followers
@@ -53,6 +53,10 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
account_outbox_url(object)
end
+ def featured
+ account_collection_url(object, :featured)
+ end
+
def endpoints
object
end
diff --git a/app/serializers/activitypub/add_serializer.rb b/app/serializers/activitypub/add_serializer.rb
new file mode 100644
index 000000000..a5f091e37
--- /dev/null
+++ b/app/serializers/activitypub/add_serializer.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class ActivityPub::AddSerializer < ActiveModel::Serializer
+ include RoutingHelper
+
+ attributes :type, :actor, :target
+ attribute :proper_object, key: :object
+
+ def type
+ 'Add'
+ end
+
+ def actor
+ ActivityPub::TagManager.instance.uri_for(object.account)
+ end
+
+ def proper_object
+ ActivityPub::TagManager.instance.uri_for(object)
+ end
+
+ def target
+ account_collection_url(object, :featured)
+ end
+end
diff --git a/app/serializers/activitypub/collection_serializer.rb b/app/serializers/activitypub/collection_serializer.rb
index d43af3f8e..1ae492945 100644
--- a/app/serializers/activitypub/collection_serializer.rb
+++ b/app/serializers/activitypub/collection_serializer.rb
@@ -2,7 +2,7 @@
class ActivityPub::CollectionSerializer < ActiveModel::Serializer
def self.serializer_for(model, options)
- return ActivityPub::ActivitySerializer if model.class.name == 'Status'
+ return ActivityPub::NoteSerializer if model.class.name == 'Status'
return ActivityPub::CollectionSerializer if model.class.name == 'ActivityPub::CollectionPresenter'
super
end
diff --git a/app/serializers/activitypub/outbox_serializer.rb b/app/serializers/activitypub/outbox_serializer.rb
new file mode 100644
index 000000000..48fbad0fd
--- /dev/null
+++ b/app/serializers/activitypub/outbox_serializer.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class ActivityPub::OutboxSerializer < ActivityPub::CollectionSerializer
+ def self.serializer_for(model, options)
+ return ActivityPub::ActivitySerializer if model.is_a?(Status)
+ super
+ end
+end
diff --git a/app/serializers/activitypub/remove_serializer.rb b/app/serializers/activitypub/remove_serializer.rb
new file mode 100644
index 000000000..6da7e35d3
--- /dev/null
+++ b/app/serializers/activitypub/remove_serializer.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class ActivityPub::RemoveSerializer < ActiveModel::Serializer
+ include RoutingHelper
+
+ attributes :type, :actor, :origin
+ attribute :proper_object, key: :object
+
+ def type
+ 'Remove'
+ end
+
+ def actor
+ ActivityPub::TagManager.instance.uri_for(object.account)
+ end
+
+ def proper_object
+ ActivityPub::TagManager.instance.uri_for(object)
+ end
+
+ def origin
+ account_collection_url(object, :featured)
+ end
+end
diff --git a/app/services/activitypub/fetch_featured_collection_service.rb b/app/services/activitypub/fetch_featured_collection_service.rb
new file mode 100644
index 000000000..40714e980
--- /dev/null
+++ b/app/services/activitypub/fetch_featured_collection_service.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+class ActivityPub::FetchFeaturedCollectionService < BaseService
+ include JsonLdHelper
+
+ def call(account)
+ @account = account
+ @json = fetch_resource(@account.featured_collection_url, true)
+
+ return unless supported_context?
+ return if @account.suspended? || @account.local?
+
+ case @json['type']
+ when 'Collection', 'CollectionPage'
+ process_items @json['items']
+ when 'OrderedCollection', 'OrderedCollectionPage'
+ process_items @json['orderedItems']
+ end
+ end
+
+ private
+
+ def process_items(items)
+ status_ids = items.map { |item| value_or_id(item) }
+ .reject { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }
+ .map { |uri| ActivityPub::FetchRemoteStatusService.new.call(uri) }
+ .compact
+ .select { |status| status.account_id == @account.id }
+ .map(&:id)
+
+ to_remove = []
+ to_add = status_ids
+
+ StatusPin.where(account: @account).pluck(:status_id).each do |status_id|
+ if status_ids.include?(status_id)
+ to_add.delete(status_id)
+ else
+ to_remove << status_id
+ end
+ end
+
+ StatusPin.where(account: @account, status_id: to_remove).delete_all unless to_remove.empty?
+
+ to_add.each do |status_id|
+ StatusPin.create!(account: @account, status_id: status_id)
+ end
+ end
+
+ def supported_context?
+ super(@json)
+ end
+end
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index f43edafe7..68e9db766 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -27,6 +27,7 @@ class ActivityPub::ProcessAccountService < BaseService
after_protocol_change! if protocol_changed?
after_key_change! if key_changed?
+ check_featured_collection! if @account.featured_collection_url.present?
@account
rescue Oj::ParseError
@@ -57,14 +58,15 @@ class ActivityPub::ProcessAccountService < BaseService
end
def set_immediate_attributes!
- @account.inbox_url = @json['inbox'] || ''
- @account.outbox_url = @json['outbox'] || ''
- @account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
- @account.followers_url = @json['followers'] || ''
- @account.url = url || @uri
- @account.display_name = @json['name'] || ''
- @account.note = @json['summary'] || ''
- @account.locked = @json['manuallyApprovesFollowers'] || false
+ @account.inbox_url = @json['inbox'] || ''
+ @account.outbox_url = @json['outbox'] || ''
+ @account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
+ @account.followers_url = @json['followers'] || ''
+ @account.featured_collection_url = @json['featured'] || ''
+ @account.url = url || @uri
+ @account.display_name = @json['name'] || ''
+ @account.note = @json['summary'] || ''
+ @account.locked = @json['manuallyApprovesFollowers'] || false
end
def set_fetchable_attributes!
@@ -85,6 +87,10 @@ class ActivityPub::ProcessAccountService < BaseService
RefollowWorker.perform_async(@account.id)
end
+ def check_featured_collection!
+ ActivityPub::SynchronizeFeaturedCollectionWorker.perform_async(@account.id)
+ end
+
def image_url(key)
value = first_of_value(@json[key])
diff --git a/app/workers/activitypub/synchronize_featured_collection_worker.rb b/app/workers/activitypub/synchronize_featured_collection_worker.rb
new file mode 100644
index 000000000..dd676a3ee
--- /dev/null
+++ b/app/workers/activitypub/synchronize_featured_collection_worker.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class ActivityPub::SynchronizeFeaturedCollectionWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: 'pull'
+
+ def perform(account_id)
+ ActivityPub::FetchFeaturedCollectionService.new.call(Account.find(account_id))
+ rescue ActiveRecord::RecordNotFound
+ true
+ end
+end
diff --git a/config/routes.rb b/config/routes.rb
index 7ed2d61f1..0542cb680 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -58,8 +58,10 @@ Rails.application.routes.draw do
resources :following, only: [:index], controller: :following_accounts
resource :follow, only: [:create], controller: :account_follow
resource :unfollow, only: [:create], controller: :account_unfollow
+
resource :outbox, only: [:show], module: :activitypub
resource :inbox, only: [:create], module: :activitypub
+ resources :collections, only: [:show], module: :activitypub
end
resource :inbox, only: [:create], module: :activitypub
diff --git a/db/migrate/20180304013859_add_featured_collection_url_to_accounts.rb b/db/migrate/20180304013859_add_featured_collection_url_to_accounts.rb
new file mode 100644
index 000000000..e0b8ed5cc
--- /dev/null
+++ b/db/migrate/20180304013859_add_featured_collection_url_to_accounts.rb
@@ -0,0 +1,5 @@
+class AddFeaturedCollectionUrlToAccounts < ActiveRecord::Migration[5.1]
+ def change
+ add_column :accounts, :featured_collection_url, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c6b81df34..c52a6f0d4 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20180211015820) do
+ActiveRecord::Schema.define(version: 20180304013859) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -73,6 +73,7 @@ ActiveRecord::Schema.define(version: 20180211015820) do
t.integer "protocol", default: 0, null: false
t.boolean "memorial", default: false, null: false
t.bigint "moved_to_account_id"
+ t.string "featured_collection_url"
t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower"
t.index ["uri"], name: "index_accounts_on_uri"
diff --git a/spec/lib/activitypub/activity/add_spec.rb b/spec/lib/activitypub/activity/add_spec.rb
new file mode 100644
index 000000000..3ebab4e37
--- /dev/null
+++ b/spec/lib/activitypub/activity/add_spec.rb
@@ -0,0 +1,29 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Add do
+ let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured') }
+ let(:status) { Fabricate(:status, account: sender) }
+
+ let(:json) do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'foo',
+ type: 'Add',
+ actor: ActivityPub::TagManager.instance.uri_for(sender),
+ object: ActivityPub::TagManager.instance.uri_for(status),
+ target: sender.featured_collection_url,
+ }.with_indifferent_access
+ end
+
+ describe '#perform' do
+ subject { described_class.new(json, sender) }
+
+ before do
+ subject.perform
+ end
+
+ it 'creates a pin' do
+ expect(sender.pinned?(status)).to be true
+ end
+ end
+end
diff --git a/spec/lib/activitypub/activity/remove_spec.rb b/spec/lib/activitypub/activity/remove_spec.rb
new file mode 100644
index 000000000..c3f015053
--- /dev/null
+++ b/spec/lib/activitypub/activity/remove_spec.rb
@@ -0,0 +1,30 @@
+require 'rails_helper'
+
+RSpec.describe ActivityPub::Activity::Remove do
+ let(:sender) { Fabricate(:account, featured_collection_url: 'https://example.com/featured') }
+ let(:status) { Fabricate(:status, account: sender) }
+
+ let(:json) do
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: 'foo',
+ type: 'Add',
+ actor: ActivityPub::TagManager.instance.uri_for(sender),
+ object: ActivityPub::TagManager.instance.uri_for(status),
+ origin: sender.featured_collection_url,
+ }.with_indifferent_access
+ end
+
+ describe '#perform' do
+ subject { described_class.new(json, sender) }
+
+ before do
+ StatusPin.create!(account: sender, status: status)
+ subject.perform
+ end
+
+ it 'removes a pin' do
+ expect(sender.pinned?(status)).to be false
+ end
+ end
+end
diff --git a/spec/lib/activitypub/activity/update_spec.rb b/spec/lib/activitypub/activity/update_spec.rb
index ea308e35c..fbfc585cf 100644
--- a/spec/lib/activitypub/activity/update_spec.rb
+++ b/spec/lib/activitypub/activity/update_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe ActivityPub::Activity::Update do
stub_request(:get, actor_json[:outbox]).to_return(status: 404)
stub_request(:get, actor_json[:followers]).to_return(status: 404)
stub_request(:get, actor_json[:following]).to_return(status: 404)
+ stub_request(:get, actor_json[:featured]).to_return(status: 404)
sender.update!(uri: ActivityPub::TagManager.instance.uri_for(sender))
end