From 3411fbef19c71f3a60e9fbb175a0728a6cfc2ed8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 21 Mar 2019 23:33:28 +0100 Subject: Improve config serializer for Keybase (#10338) - Regex must no longer be surrounded by `/` - Description must be short and cannot contain HTML tags --- app/lib/proof_provider/keybase/config_serializer.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'app/lib') diff --git a/app/lib/proof_provider/keybase/config_serializer.rb b/app/lib/proof_provider/keybase/config_serializer.rb index 474ea74e2..557bafe84 100644 --- a/app/lib/proof_provider/keybase/config_serializer.rb +++ b/app/lib/proof_provider/keybase/config_serializer.rb @@ -2,6 +2,7 @@ class ProofProvider::Keybase::ConfigSerializer < ActiveModel::Serializer include RoutingHelper + include ActionView::Helpers::TextHelper attributes :version, :domain, :display_name, :username, :brand_color, :logo, :description, :prefill_url, @@ -29,11 +30,11 @@ class ProofProvider::Keybase::ConfigSerializer < ActiveModel::Serializer end def description - Setting.site_short_description.presence || Setting.site_description.presence || I18n.t('about.about_mastodon_html') + strip_tags(Setting.site_short_description.presence || I18n.t('about.about_mastodon_html')) end def username - { min: 1, max: 30, re: Account::USERNAME_RE.inspect } + { min: 1, max: 30, re: '[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?' } end def prefill_url -- cgit From e6cfa7ab897ac4fd6bf9bbcafe09fc42c4cc2c5d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 26 Mar 2019 01:23:59 +0100 Subject: Change language detector threshold from 140 characters to 4 words (#10376) Add `lang` attribute to statuses in web UI --- app/javascript/mastodon/components/status_content.js | 6 ++++-- app/lib/language_detector.rb | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) (limited to 'app/lib') diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js index 70713047d..fa8901386 100644 --- a/app/javascript/mastodon/components/status_content.js +++ b/app/javascript/mastodon/components/status_content.js @@ -183,14 +183,14 @@ export default class StatusContent extends React.PureComponent { return (
{mentionsPlaceholder} -
+
); } else if (this.props.onClick) { @@ -202,6 +202,7 @@ export default class StatusContent extends React.PureComponent { className={classNames} style={directionStyle} dangerouslySetInnerHTML={content} + lang={status.get('language')} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} />, @@ -220,6 +221,7 @@ export default class StatusContent extends React.PureComponent { className='status__content' style={directionStyle} dangerouslySetInnerHTML={content} + lang={status.get('language')} /> ); } diff --git a/app/lib/language_detector.rb b/app/lib/language_detector.rb index 70a9084d1..1e90af42d 100644 --- a/app/lib/language_detector.rb +++ b/app/lib/language_detector.rb @@ -3,7 +3,7 @@ class LanguageDetector include Singleton - CHARACTER_THRESHOLD = 140 + WORDS_THRESHOLD = 4 RELIABLE_CHARACTERS_RE = /[\p{Hebrew}\p{Arabic}\p{Syriac}\p{Thaana}\p{Nko}\p{Han}\p{Katakana}\p{Hiragana}\p{Hangul}]+/m def initialize @@ -37,7 +37,7 @@ class LanguageDetector end def sufficient_text_length?(text) - text.size >= CHARACTER_THRESHOLD + text.split(/\s+/).size >= WORDS_THRESHOLD end def language_specific_character_set?(text) -- cgit From 11fe293e1b318a12b75f0c5d1bb208fdbb46417e Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 27 Mar 2019 15:55:23 +0100 Subject: Remove unused ActivityPub `@context` values depending on response (#10378) Fix #8078 --- app/lib/activitypub/adapter.rb | 75 +++++++++++------- app/lib/activitypub/serializer.rb | 30 ++++++++ .../activitypub/accept_follow_serializer.rb | 2 +- app/serializers/activitypub/activity_serializer.rb | 2 +- app/serializers/activitypub/actor_serializer.rb | 13 +++- app/serializers/activitypub/add_serializer.rb | 2 +- app/serializers/activitypub/block_serializer.rb | 2 +- .../activitypub/collection_serializer.rb | 2 +- .../activitypub/delete_actor_serializer.rb | 2 +- app/serializers/activitypub/delete_serializer.rb | 6 +- app/serializers/activitypub/emoji_serializer.rb | 4 +- app/serializers/activitypub/flag_serializer.rb | 2 +- app/serializers/activitypub/follow_serializer.rb | 2 +- app/serializers/activitypub/image_serializer.rb | 4 +- app/serializers/activitypub/like_serializer.rb | 2 +- app/serializers/activitypub/note_serializer.rb | 15 ++-- .../activitypub/public_key_serializer.rb | 4 +- .../activitypub/reject_follow_serializer.rb | 2 +- app/serializers/activitypub/remove_serializer.rb | 2 +- .../activitypub/undo_announce_serializer.rb | 2 +- .../activitypub/undo_block_serializer.rb | 2 +- .../activitypub/undo_follow_serializer.rb | 2 +- .../activitypub/undo_like_serializer.rb | 2 +- .../activitypub/update_poll_serializer.rb | 2 +- app/serializers/activitypub/update_serializer.rb | 2 +- app/serializers/activitypub/vote_serializer.rb | 4 +- config/initializers/active_model_serializers.rb | 19 +++++ spec/lib/activitypub/adapter_spec.rb | 88 ++++++++++++++++++++++ 28 files changed, 235 insertions(+), 61 deletions(-) create mode 100644 app/lib/activitypub/serializer.rb create mode 100644 spec/lib/activitypub/adapter_spec.rb (limited to 'app/lib') diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index 99f4d9305..7e0b16c25 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -1,30 +1,23 @@ # frozen_string_literal: true class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base - CONTEXT = { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - - { - 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', - 'sensitive' => 'as:sensitive', - 'movedTo' => { '@id' => 'as:movedTo', '@type' => '@id' }, - 'alsoKnownAs' => { '@id' => 'as:alsoKnownAs', '@type' => '@id' }, - 'Hashtag' => 'as:Hashtag', - 'ostatus' => 'http://ostatus.org#', - 'atomUri' => 'ostatus:atomUri', - 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', - 'conversation' => 'ostatus:conversation', - 'toot' => 'http://joinmastodon.org/ns#', - 'Emoji' => 'toot:Emoji', - 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' }, - 'featured' => { '@id' => 'toot:featured', '@type' => '@id' }, - 'schema' => 'http://schema.org#', - 'PropertyValue' => 'schema:PropertyValue', - 'value' => 'schema:value', - }, - ], + NAMED_CONTEXT_MAP = { + activitystreams: 'https://www.w3.org/ns/activitystreams', + security: 'https://w3id.org/security/v1', + }.freeze + + CONTEXT_EXTENSION_MAP = { + manually_approves_followers: { 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers' }, + sensitive: { 'sensitive' => 'as:sensitive' }, + hashtag: { 'Hashtag' => 'as:Hashtag' }, + moved_to: { 'movedTo' => { '@id' => 'as:movedTo', '@type' => '@id' } }, + also_known_as: { 'alsoKnownAs' => { '@id' => 'as:alsoKnownAs', '@type' => '@id' } }, + emoji: { 'toot' => 'http://joinmastodon.org/ns#', 'Emoji' => 'toot:Emoji' }, + featured: { 'toot' => 'http://joinmastodon.org/ns#', 'featured' => { '@id' => 'toot:featured', '@type' => '@id' } }, + property_value: { 'schema' => 'http://schema.org#', 'PropertyValue' => 'schema:PropertyValue', 'value' => 'schema:value' }, + atom_uri: { 'ostatus' => 'http://ostatus.org#', 'atomUri' => 'ostatus:atomUri' }, + conversation: { 'ostatus' => 'http://ostatus.org#', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'conversation' => 'ostatus:conversation' }, + focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } }, }.freeze def self.default_key_transform @@ -36,8 +29,36 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base end def serializable_hash(options = nil) - options = serialization_options(options) - serialized_hash = ActiveModelSerializers::Adapter::Attributes.new(serializer, instance_options).serializable_hash(options) - CONTEXT.merge(self.class.transform_key_casing!(serialized_hash, instance_options)) + options = serialization_options(options) + serialized_hash = serializer.serializable_hash(options) + serialized_hash = self.class.transform_key_casing!(serialized_hash, instance_options) + + { '@context' => serialized_context }.merge(serialized_hash) + end + + private + + def serialized_context + context_array = [] + + serializer_options = serializer.send(:instance_options) || {} + named_contexts = [:activitystreams] + serializer._named_contexts.keys + serializer_options.fetch(:named_contexts, {}).keys + context_extensions = serializer._context_extensions.keys + serializer_options.fetch(:context_extensions, {}).keys + + named_contexts.each do |key| + context_array << NAMED_CONTEXT_MAP[key] + end + + extensions = context_extensions.each_with_object({}) do |key, h| + h.merge!(CONTEXT_EXTENSION_MAP[key]) + end + + context_array << extensions unless extensions.empty? + + if context_array.size == 1 + context_array.first + else + context_array + end end end diff --git a/app/lib/activitypub/serializer.rb b/app/lib/activitypub/serializer.rb new file mode 100644 index 000000000..07bd8c494 --- /dev/null +++ b/app/lib/activitypub/serializer.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class ActivityPub::Serializer < ActiveModel::Serializer + with_options instance_writer: false, instance_reader: true do |serializer| + serializer.class_attribute :_named_contexts + serializer.class_attribute :_context_extensions + + self._named_contexts ||= {} + self._context_extensions ||= {} + end + + def self.inherited(base) + super + + base._named_contexts = _named_contexts.dup + base._context_extensions = _context_extensions.dup + end + + def self.context(*named_contexts) + named_contexts.each do |context| + _named_contexts[context] = true + end + end + + def self.context_extensions(*extension_names) + extension_names.each do |extension_name| + _context_extensions[extension_name] = true + end + end +end diff --git a/app/serializers/activitypub/accept_follow_serializer.rb b/app/serializers/activitypub/accept_follow_serializer.rb index 3e23591a5..1c1c6ab73 100644 --- a/app/serializers/activitypub/accept_follow_serializer.rb +++ b/app/serializers/activitypub/accept_follow_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::AcceptFollowSerializer < ActiveModel::Serializer +class ActivityPub::AcceptFollowSerializer < ActivityPub::Serializer attributes :id, :type, :actor has_one :object, serializer: ActivityPub::FollowSerializer diff --git a/app/serializers/activitypub/activity_serializer.rb b/app/serializers/activitypub/activity_serializer.rb index c001e28aa..c06d5c87c 100644 --- a/app/serializers/activitypub/activity_serializer.rb +++ b/app/serializers/activitypub/activity_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::ActivitySerializer < ActiveModel::Serializer +class ActivityPub::ActivitySerializer < ActivityPub::Serializer attributes :id, :type, :actor, :published, :to, :cc has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, if: :serialize_object? diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 6746c1782..4b982b955 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -1,8 +1,13 @@ # frozen_string_literal: true -class ActivityPub::ActorSerializer < ActiveModel::Serializer +class ActivityPub::ActorSerializer < ActivityPub::Serializer include RoutingHelper + context :security + + context_extensions :manually_approves_followers, :featured, :also_known_as, + :moved_to, :property_value, :hashtag, :emoji + attributes :id, :type, :following, :followers, :inbox, :outbox, :featured, :preferred_username, :name, :summary, @@ -16,7 +21,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer attribute :moved_to, if: :moved? attribute :also_known_as, if: :also_known_as? - class EndpointsSerializer < ActiveModel::Serializer + class EndpointsSerializer < ActivityPub::Serializer include RoutingHelper attributes :shared_inbox @@ -124,7 +129,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer class CustomEmojiSerializer < ActivityPub::EmojiSerializer end - class TagSerializer < ActiveModel::Serializer + class TagSerializer < ActivityPub::Serializer include RoutingHelper attributes :type, :href, :name @@ -142,7 +147,7 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer end end - class Account::FieldSerializer < ActiveModel::Serializer + class Account::FieldSerializer < ActivityPub::Serializer attributes :type, :name, :value def type diff --git a/app/serializers/activitypub/add_serializer.rb b/app/serializers/activitypub/add_serializer.rb index c0906e8d0..6f5aab17f 100644 --- a/app/serializers/activitypub/add_serializer.rb +++ b/app/serializers/activitypub/add_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::AddSerializer < ActiveModel::Serializer +class ActivityPub::AddSerializer < ActivityPub::Serializer include RoutingHelper attributes :type, :actor, :target diff --git a/app/serializers/activitypub/block_serializer.rb b/app/serializers/activitypub/block_serializer.rb index 624ce2fce..e6c69329d 100644 --- a/app/serializers/activitypub/block_serializer.rb +++ b/app/serializers/activitypub/block_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::BlockSerializer < ActiveModel::Serializer +class ActivityPub::BlockSerializer < ActivityPub::Serializer attributes :id, :type, :actor attribute :virtual_object, key: :object diff --git a/app/serializers/activitypub/collection_serializer.rb b/app/serializers/activitypub/collection_serializer.rb index b03609957..da1ba735f 100644 --- a/app/serializers/activitypub/collection_serializer.rb +++ b/app/serializers/activitypub/collection_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::CollectionSerializer < ActiveModel::Serializer +class ActivityPub::CollectionSerializer < ActivityPub::Serializer def self.serializer_for(model, options) return ActivityPub::NoteSerializer if model.class.name == 'Status' return ActivityPub::CollectionSerializer if model.class.name == 'ActivityPub::CollectionPresenter' diff --git a/app/serializers/activitypub/delete_actor_serializer.rb b/app/serializers/activitypub/delete_actor_serializer.rb index ddf59be97..a6c5e2385 100644 --- a/app/serializers/activitypub/delete_actor_serializer.rb +++ b/app/serializers/activitypub/delete_actor_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::DeleteActorSerializer < ActiveModel::Serializer +class ActivityPub::DeleteActorSerializer < ActivityPub::Serializer attributes :id, :type, :actor, :to attribute :virtual_object, key: :object diff --git a/app/serializers/activitypub/delete_serializer.rb b/app/serializers/activitypub/delete_serializer.rb index 5012a8383..a7d5bd469 100644 --- a/app/serializers/activitypub/delete_serializer.rb +++ b/app/serializers/activitypub/delete_serializer.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true -class ActivityPub::DeleteSerializer < ActiveModel::Serializer - class TombstoneSerializer < ActiveModel::Serializer +class ActivityPub::DeleteSerializer < ActivityPub::Serializer + class TombstoneSerializer < ActivityPub::Serializer + context_extensions :atom_uri + attributes :id, :type, :atom_uri def id diff --git a/app/serializers/activitypub/emoji_serializer.rb b/app/serializers/activitypub/emoji_serializer.rb index 7b06b1e5d..4dc38f3ea 100644 --- a/app/serializers/activitypub/emoji_serializer.rb +++ b/app/serializers/activitypub/emoji_serializer.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true -class ActivityPub::EmojiSerializer < ActiveModel::Serializer +class ActivityPub::EmojiSerializer < ActivityPub::Serializer include RoutingHelper + context_extensions :emoji + attributes :id, :type, :name, :updated has_one :icon, serializer: ActivityPub::ImageSerializer diff --git a/app/serializers/activitypub/flag_serializer.rb b/app/serializers/activitypub/flag_serializer.rb index 1e7a46dd9..2f2a707d3 100644 --- a/app/serializers/activitypub/flag_serializer.rb +++ b/app/serializers/activitypub/flag_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::FlagSerializer < ActiveModel::Serializer +class ActivityPub::FlagSerializer < ActivityPub::Serializer attributes :id, :type, :actor, :content attribute :virtual_object, key: :object diff --git a/app/serializers/activitypub/follow_serializer.rb b/app/serializers/activitypub/follow_serializer.rb index bb204ee8f..9228d7716 100644 --- a/app/serializers/activitypub/follow_serializer.rb +++ b/app/serializers/activitypub/follow_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::FollowSerializer < ActiveModel::Serializer +class ActivityPub::FollowSerializer < ActivityPub::Serializer attributes :id, :type, :actor attribute :virtual_object, key: :object diff --git a/app/serializers/activitypub/image_serializer.rb b/app/serializers/activitypub/image_serializer.rb index 3c08f77e8..1060f9691 100644 --- a/app/serializers/activitypub/image_serializer.rb +++ b/app/serializers/activitypub/image_serializer.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true -class ActivityPub::ImageSerializer < ActiveModel::Serializer +class ActivityPub::ImageSerializer < ActivityPub::Serializer include RoutingHelper + context_extensions :focal_point + attributes :type, :media_type, :url attribute :focal_point, if: :focal_point? diff --git a/app/serializers/activitypub/like_serializer.rb b/app/serializers/activitypub/like_serializer.rb index c1a7ff6f6..0f170ddb4 100644 --- a/app/serializers/activitypub/like_serializer.rb +++ b/app/serializers/activitypub/like_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::LikeSerializer < ActiveModel::Serializer +class ActivityPub::LikeSerializer < ActivityPub::Serializer attributes :id, :type, :actor attribute :virtual_object, key: :object diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 553f333d8..0666bea5a 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true -class ActivityPub::NoteSerializer < ActiveModel::Serializer +class ActivityPub::NoteSerializer < ActivityPub::Serializer + context_extensions :atom_uri, :conversation, :sensitive, + :hashtag, :emoji, :focal_point + attributes :id, :type, :summary, :in_reply_to, :published, :url, :attributed_to, :to, :cc, :sensitive, @@ -147,7 +150,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer object.poll&.expired? end - class MediaAttachmentSerializer < ActiveModel::Serializer + class MediaAttachmentSerializer < ActivityPub::Serializer include RoutingHelper attributes :type, :media_type, :url, :name @@ -178,7 +181,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer end end - class MentionSerializer < ActiveModel::Serializer + class MentionSerializer < ActivityPub::Serializer attributes :type, :href, :name def type @@ -194,7 +197,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer end end - class TagSerializer < ActiveModel::Serializer + class TagSerializer < ActivityPub::Serializer include RoutingHelper attributes :type, :href, :name @@ -215,8 +218,8 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer class CustomEmojiSerializer < ActivityPub::EmojiSerializer end - class OptionSerializer < ActiveModel::Serializer - class RepliesSerializer < ActiveModel::Serializer + class OptionSerializer < ActivityPub::Serializer + class RepliesSerializer < ActivityPub::Serializer attributes :type, :total_items def type diff --git a/app/serializers/activitypub/public_key_serializer.rb b/app/serializers/activitypub/public_key_serializer.rb index 38e9e93ba..62ed49e81 100644 --- a/app/serializers/activitypub/public_key_serializer.rb +++ b/app/serializers/activitypub/public_key_serializer.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true -class ActivityPub::PublicKeySerializer < ActiveModel::Serializer +class ActivityPub::PublicKeySerializer < ActivityPub::Serializer + context :security + attributes :id, :owner, :public_key_pem def id diff --git a/app/serializers/activitypub/reject_follow_serializer.rb b/app/serializers/activitypub/reject_follow_serializer.rb index 7814f4f57..4996c9a3c 100644 --- a/app/serializers/activitypub/reject_follow_serializer.rb +++ b/app/serializers/activitypub/reject_follow_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::RejectFollowSerializer < ActiveModel::Serializer +class ActivityPub::RejectFollowSerializer < ActivityPub::Serializer attributes :id, :type, :actor has_one :object, serializer: ActivityPub::FollowSerializer diff --git a/app/serializers/activitypub/remove_serializer.rb b/app/serializers/activitypub/remove_serializer.rb index c2a5ae1b3..7fefda59d 100644 --- a/app/serializers/activitypub/remove_serializer.rb +++ b/app/serializers/activitypub/remove_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::RemoveSerializer < ActiveModel::Serializer +class ActivityPub::RemoveSerializer < ActivityPub::Serializer include RoutingHelper attributes :type, :actor, :target diff --git a/app/serializers/activitypub/undo_announce_serializer.rb b/app/serializers/activitypub/undo_announce_serializer.rb index 4fc042727..6758af679 100644 --- a/app/serializers/activitypub/undo_announce_serializer.rb +++ b/app/serializers/activitypub/undo_announce_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::UndoAnnounceSerializer < ActiveModel::Serializer +class ActivityPub::UndoAnnounceSerializer < ActivityPub::Serializer attributes :id, :type, :actor, :to has_one :object, serializer: ActivityPub::ActivitySerializer diff --git a/app/serializers/activitypub/undo_block_serializer.rb b/app/serializers/activitypub/undo_block_serializer.rb index 2f43d8402..b4f049377 100644 --- a/app/serializers/activitypub/undo_block_serializer.rb +++ b/app/serializers/activitypub/undo_block_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::UndoBlockSerializer < ActiveModel::Serializer +class ActivityPub::UndoBlockSerializer < ActivityPub::Serializer attributes :id, :type, :actor has_one :object, serializer: ActivityPub::BlockSerializer diff --git a/app/serializers/activitypub/undo_follow_serializer.rb b/app/serializers/activitypub/undo_follow_serializer.rb index e5b7f143d..9b3e0ca3c 100644 --- a/app/serializers/activitypub/undo_follow_serializer.rb +++ b/app/serializers/activitypub/undo_follow_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::UndoFollowSerializer < ActiveModel::Serializer +class ActivityPub::UndoFollowSerializer < ActivityPub::Serializer attributes :id, :type, :actor has_one :object, serializer: ActivityPub::FollowSerializer diff --git a/app/serializers/activitypub/undo_like_serializer.rb b/app/serializers/activitypub/undo_like_serializer.rb index 25f4ccaae..20c786cb7 100644 --- a/app/serializers/activitypub/undo_like_serializer.rb +++ b/app/serializers/activitypub/undo_like_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::UndoLikeSerializer < ActiveModel::Serializer +class ActivityPub::UndoLikeSerializer < ActivityPub::Serializer attributes :id, :type, :actor has_one :object, serializer: ActivityPub::LikeSerializer diff --git a/app/serializers/activitypub/update_poll_serializer.rb b/app/serializers/activitypub/update_poll_serializer.rb index f7933346f..a9a09747f 100644 --- a/app/serializers/activitypub/update_poll_serializer.rb +++ b/app/serializers/activitypub/update_poll_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::UpdatePollSerializer < ActiveModel::Serializer +class ActivityPub::UpdatePollSerializer < ActivityPub::Serializer attributes :id, :type, :actor, :to has_one :object, serializer: ActivityPub::NoteSerializer diff --git a/app/serializers/activitypub/update_serializer.rb b/app/serializers/activitypub/update_serializer.rb index 48d7a1929..a5eb857d3 100644 --- a/app/serializers/activitypub/update_serializer.rb +++ b/app/serializers/activitypub/update_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::UpdateSerializer < ActiveModel::Serializer +class ActivityPub::UpdateSerializer < ActivityPub::Serializer attributes :id, :type, :actor, :to has_one :object, serializer: ActivityPub::ActorSerializer diff --git a/app/serializers/activitypub/vote_serializer.rb b/app/serializers/activitypub/vote_serializer.rb index 248190404..71ef5c77e 100644 --- a/app/serializers/activitypub/vote_serializer.rb +++ b/app/serializers/activitypub/vote_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -class ActivityPub::VoteSerializer < ActiveModel::Serializer - class NoteSerializer < ActiveModel::Serializer +class ActivityPub::VoteSerializer < ActivityPub::Serializer + class NoteSerializer < ActivityPub::Serializer attributes :id, :type, :name, :attributed_to, :in_reply_to, :to diff --git a/config/initializers/active_model_serializers.rb b/config/initializers/active_model_serializers.rb index 0e69e1d96..329a5fb2c 100644 --- a/config/initializers/active_model_serializers.rb +++ b/config/initializers/active_model_serializers.rb @@ -3,3 +3,22 @@ ActiveModelSerializers.config.tap do |config| end ActiveSupport::Notifications.unsubscribe(ActiveModelSerializers::Logging::RENDER_EVENT) + +class ActiveModel::Serializer::Reflection + # We monkey-patch this method so that when we include associations in a serializer, + # the nested serializers can send information about used contexts upwards back to + # the root. We do this via instance_options because the nesting can be dynamic. + def build_association(parent_serializer, parent_serializer_options, include_slice = {}) + serializer = options[:serializer] + + parent_serializer_options.merge!(named_contexts: serializer._named_contexts, context_extensions: serializer._context_extensions) if serializer.respond_to?(:_named_contexts) + + association_options = { + parent_serializer: parent_serializer, + parent_serializer_options: parent_serializer_options, + include_slice: include_slice, + } + + ActiveModel::Serializer::Association.new(self, association_options) + end +end diff --git a/spec/lib/activitypub/adapter_spec.rb b/spec/lib/activitypub/adapter_spec.rb new file mode 100644 index 000000000..ea03797aa --- /dev/null +++ b/spec/lib/activitypub/adapter_spec.rb @@ -0,0 +1,88 @@ +require 'rails_helper' + +RSpec.describe ActivityPub::Adapter do + class TestObject < ActiveModelSerializers::Model + attributes :foo + end + + class TestWithBasicContextSerializer < ActivityPub::Serializer + attributes :foo + end + + class TestWithNamedContextSerializer < ActivityPub::Serializer + context :security + attributes :foo + end + + class TestWithNestedNamedContextSerializer < ActivityPub::Serializer + attributes :foo + + has_one :virtual_object, key: :baz, serializer: TestWithNamedContextSerializer + + def virtual_object + object + end + end + + class TestWithContextExtensionSerializer < ActivityPub::Serializer + context_extensions :sensitive + attributes :foo + end + + class TestWithNestedContextExtensionSerializer < ActivityPub::Serializer + context_extensions :manually_approves_followers + attributes :foo + + has_one :virtual_object, key: :baz, serializer: TestWithContextExtensionSerializer + + def virtual_object + object + end + end + + describe '#serializable_hash' do + let(:serializer_class) {} + + subject { ActiveModelSerializers::SerializableResource.new(TestObject.new(foo: 'bar'), serializer: serializer_class, adapter: described_class).as_json } + + context 'when serializer defines no context' do + let(:serializer_class) { TestWithBasicContextSerializer } + + it 'renders a basic @context' do + expect(subject).to include({ '@context' => 'https://www.w3.org/ns/activitystreams' }) + end + end + + context 'when serializer defines a named context' do + let(:serializer_class) { TestWithNamedContextSerializer } + + it 'renders a @context with both items' do + expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] }) + end + end + + context 'when serializer has children that define a named context' do + let(:serializer_class) { TestWithNestedNamedContextSerializer } + + it 'renders a @context with both items' do + expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] }) + end + end + + context 'when serializer defines context extensions' do + let(:serializer_class) { TestWithContextExtensionSerializer } + + it 'renders a @context with the extension' do + expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', { 'sensitive' => 'as:sensitive' }] }) + end + end + + context 'when serializer has children that define context extensions' do + let(:serializer_class) { TestWithNestedContextExtensionSerializer } + + it 'renders a @context with both extensions' do + expect(subject).to include({ '@context' => ['https://www.w3.org/ns/activitystreams', { 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', 'sensitive' => 'as:sensitive' }] }) + end + end + end +end -- cgit From e86663b1da9c31b57baf244effb94c063e10a424 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 27 Mar 2019 19:58:24 +0100 Subject: Fix alternative relay support regression (#10398) Fix #10324 --- app/lib/activitypub/activity/announce.rb | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'app/lib') diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb index 9f8ffd9fb..1aa6ee9ec 100644 --- a/app/lib/activitypub/activity/announce.rb +++ b/app/lib/activitypub/activity/announce.rb @@ -47,6 +47,10 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity followed_by_local_accounts? || requested_through_relay? || reblog_of_local_status? end + def requested_through_relay? + super || Relay.find_by(inbox_url: @account.inbox_url)&.enabled? + end + def reblog_of_local_status? status_from_uri(object_uri)&.account&.local? end -- cgit From f1bc90ab508cbdebc646324f87db48a9e80036f4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 28 Mar 2019 04:44:59 +0100 Subject: Rename :poll to :preloadable_poll and :owned_poll to :poll on Status (#10401) Also, fix some n+1 queries Resolve #10365 --- app/helpers/stream_entries_helper.rb | 4 ++-- app/lib/activitypub/activity/create.rb | 10 +++++----- app/lib/activitypub/activity/update.rb | 4 ++-- app/lib/formatter.rb | 4 ++-- app/models/notification.rb | 2 +- app/models/status.rb | 19 ++++++++++--------- app/serializers/activitypub/note_serializer.rb | 14 +++++++------- app/serializers/activitypub/update_poll_serializer.rb | 2 +- app/serializers/rest/status_serializer.rb | 2 +- app/services/post_status_service.rb | 2 +- app/views/stream_entries/_detailed_status.html.haml | 6 +++--- app/views/stream_entries/_simple_status.html.haml | 6 +++--- .../activitypub/distribute_poll_update_worker.rb | 4 ++-- config/locales/activerecord.en.yml | 5 +++-- spec/lib/activitypub/activity/create_spec.rb | 4 ++-- 15 files changed, 45 insertions(+), 43 deletions(-) (limited to 'app/lib') diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb index 8392afa73..4734e32a4 100644 --- a/app/helpers/stream_entries_helper.rb +++ b/app/helpers/stream_entries_helper.rb @@ -105,8 +105,8 @@ module StreamEntriesHelper end def poll_summary(status) - return unless status.poll - status.poll.options.map { |o| "[ ] #{o}" }.join("\n") + return unless status.preloadable_poll + status.preloadable_poll.options.map { |o| "[ ] #{o}" }.join("\n") end def status_description(status) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 8fe7b9138..dabdcbcf7 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -68,7 +68,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity thread: replied_to_status, conversation: conversation_from_uri(@object['conversation']), media_attachment_ids: process_attachments.take(4).map(&:id), - owned_poll: process_poll, + poll: process_poll, } end end @@ -240,11 +240,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def poll_vote? - return false if replied_to_status.nil? || replied_to_status.poll.nil? || !replied_to_status.local? || !replied_to_status.poll.options.include?(@object['name']) + return false if replied_to_status.nil? || replied_to_status.preloadable_poll.nil? || !replied_to_status.local? || !replied_to_status.preloadable_poll.options.include?(@object['name']) - unless replied_to_status.poll.expired? - replied_to_status.poll.votes.create!(account: @account, choice: replied_to_status.poll.options.index(@object['name']), uri: @object['id']) - ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, replied_to_status.id) unless replied_to_status.poll.hide_totals? + unless replied_to_status.preloadable_poll.expired? + replied_to_status.preloadable_poll.votes.create!(account: @account, choice: replied_to_status.preloadable_poll.options.index(@object['name']), uri: @object['id']) + ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, replied_to_status.id) unless replied_to_status.preloadable_poll.hide_totals? end true diff --git a/app/lib/activitypub/activity/update.rb b/app/lib/activitypub/activity/update.rb index bc9a63f98..70035325b 100644 --- a/app/lib/activitypub/activity/update.rb +++ b/app/lib/activitypub/activity/update.rb @@ -23,8 +23,8 @@ class ActivityPub::Activity::Update < ActivityPub::Activity return reject_payload! if invalid_origin?(@object['id']) status = Status.find_by(uri: object_uri, account_id: @account.id) - return if status.nil? || status.poll.nil? + return if status.nil? || status.preloadable_poll.nil? - ActivityPub::ProcessPollService.new.call(status.poll, @object) + ActivityPub::ProcessPollService.new.call(status.preloadable_poll, @object) end end diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index aadf03b2a..59dfc9004 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -19,8 +19,8 @@ class Formatter raw_content = status.text - if options[:inline_poll_options] && status.poll - raw_content = raw_content + "\n\n" + status.poll.options.map { |title| "[ ] #{title}" }.join("\n") + if options[:inline_poll_options] && status.preloadable_poll + raw_content = raw_content + "\n\n" + status.preloadable_poll.options.map { |title| "[ ] #{title}" }.join("\n") end return '' if raw_content.blank? diff --git a/app/models/notification.rb b/app/models/notification.rb index 982136c05..300269e24 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -25,7 +25,7 @@ class Notification < ApplicationRecord poll: 'Poll', }.freeze - STATUS_INCLUDES = [:account, :application, :media_attachments, :tags, active_mentions: :account, reblog: [:account, :application, :media_attachments, :tags, active_mentions: :account]].freeze + STATUS_INCLUDES = [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, :tags, active_mentions: :account]].freeze belongs_to :account, optional: true belongs_to :from_account, class_name: 'Account', optional: true diff --git a/app/models/status.rb b/app/models/status.rb index d3fb83cca..8d31fd382 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -45,7 +45,7 @@ class Status < ApplicationRecord belongs_to :account, inverse_of: :statuses belongs_to :in_reply_to_account, foreign_key: 'in_reply_to_account_id', class_name: 'Account', optional: true belongs_to :conversation, optional: true - belongs_to :poll, optional: true + belongs_to :preloadable_poll, class_name: 'Poll', foreign_key: 'poll_id', optional: true belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true @@ -63,7 +63,7 @@ class Status < ApplicationRecord has_one :notification, as: :activity, dependent: :destroy has_one :stream_entry, as: :activity, inverse_of: :status has_one :status_stat, inverse_of: :status - has_one :owned_poll, class_name: 'Poll', inverse_of: :status, dependent: :destroy + has_one :poll, inverse_of: :status, dependent: :destroy validates :uri, uniqueness: true, presence: true, unless: :local? validates :text, presence: true, unless: -> { with_media? || reblog? } @@ -72,7 +72,7 @@ class Status < ApplicationRecord validates :reblog, uniqueness: { scope: :account }, if: :reblog? validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog? - accepts_nested_attributes_for :owned_poll + accepts_nested_attributes_for :poll default_scope { recent } @@ -107,7 +107,7 @@ class Status < ApplicationRecord :tags, :preview_cards, :stream_entry, - :poll, + :preloadable_poll, account: :account_stat, active_mentions: { account: :account_stat }, reblog: [ @@ -118,7 +118,7 @@ class Status < ApplicationRecord :media_attachments, :conversation, :status_stat, - :poll, + :preloadable_poll, account: :account_stat, active_mentions: { account: :account_stat }, ], @@ -214,10 +214,11 @@ class Status < ApplicationRecord def emojis return @emojis if defined?(@emojis) - fields = [spoiler_text, text] - fields += owned_poll.options unless owned_poll.nil? + + fields = [spoiler_text, text] + fields += preloadable_poll.options unless preloadable_poll.nil? + @emojis = CustomEmoji.from_text(fields.join(' '), account.domain) - @emojis end def mark_for_mass_destruction! @@ -453,7 +454,7 @@ class Status < ApplicationRecord end def set_poll_id - update_column(:poll_id, owned_poll.id) unless owned_poll.nil? + update_column(:poll_id, poll.id) unless poll.nil? end def set_visibility diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 0666bea5a..d11cfa59a 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -29,7 +29,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer end def type - object.poll ? 'Question' : 'Note' + object.preloadable_poll ? 'Question' : 'Note' end def summary @@ -125,29 +125,29 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer end def poll_options - object.poll.loaded_options + object.preloadable_poll.loaded_options end def poll_and_multiple? - object.poll&.multiple? + object.preloadable_poll&.multiple? end def poll_and_not_multiple? - object.poll && !object.poll.multiple? + object.preloadable_poll && !object.preloadable_poll.multiple? end def closed - object.poll.expires_at.iso8601 + object.preloadable_poll.expires_at.iso8601 end alias end_time closed def poll_and_expires? - object.poll&.expires_at&.present? + object.preloadable_poll&.expires_at&.present? end def poll_and_expired? - object.poll&.expired? + object.preloadable_poll&.expired? end class MediaAttachmentSerializer < ActivityPub::Serializer diff --git a/app/serializers/activitypub/update_poll_serializer.rb b/app/serializers/activitypub/update_poll_serializer.rb index a9a09747f..b894f309f 100644 --- a/app/serializers/activitypub/update_poll_serializer.rb +++ b/app/serializers/activitypub/update_poll_serializer.rb @@ -6,7 +6,7 @@ class ActivityPub::UpdatePollSerializer < ActivityPub::Serializer has_one :object, serializer: ActivityPub::NoteSerializer def id - [ActivityPub::TagManager.instance.uri_for(object), '#updates/', object.poll.updated_at.to_i].join + [ActivityPub::TagManager.instance.uri_for(object), '#updates/', object.preloadable_poll.updated_at.to_i].join end def type diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 30edf397b..106777b6e 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -21,7 +21,7 @@ class REST::StatusSerializer < ActiveModel::Serializer has_many :emojis, serializer: REST::CustomEmojiSerializer has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer - has_one :poll, serializer: REST::PollSerializer + has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer def id object.id.to_s diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 3f392a6e6..e7366c7e8 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -155,7 +155,7 @@ class PostStatusService < BaseService text: @text, media_attachments: @media || [], thread: @in_reply_to, - owned_poll_attributes: poll_attributes, + poll_attributes: poll_attributes, sensitive: (@options[:sensitive].nil? ? @account.user&.setting_default_sensitive : @options[:sensitive]) || @options[:spoiler_text].present?, spoiler_text: @options[:spoiler_text] || '', visibility: @visibility, diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index d18ecd37a..4459581d9 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -22,9 +22,9 @@ %a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more') .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay) - - if status.poll - = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do - = render partial: 'stream_entries/poll', locals: { status: status, poll: status.poll, autoplay: autoplay } + - if status.preloadable_poll + = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do + = render partial: 'stream_entries/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay } - elsif !status.media_attachments.empty? - if status.media_attachments.first.video? - video = status.media_attachments.first diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index a499a8634..ba22c5340 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -26,9 +26,9 @@ %a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more') .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay) - - if status.poll - = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do - = render partial: 'stream_entries/poll', locals: { status: status, poll: status.poll, autoplay: autoplay } + - if status.preloadable_poll + = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do + = render partial: 'stream_entries/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay } - elsif !status.media_attachments.empty? - if status.media_attachments.first.video? - video = status.media_attachments.first diff --git a/app/workers/activitypub/distribute_poll_update_worker.rb b/app/workers/activitypub/distribute_poll_update_worker.rb index d60fde557..5eaca6fda 100644 --- a/app/workers/activitypub/distribute_poll_update_worker.rb +++ b/app/workers/activitypub/distribute_poll_update_worker.rb @@ -9,7 +9,7 @@ class ActivityPub::DistributePollUpdateWorker @status = Status.find(status_id) @account = @status.account - return unless @status.poll + return unless @status.preloadable_poll ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url| [payload, @account.id, inbox_url] @@ -29,7 +29,7 @@ class ActivityPub::DistributePollUpdateWorker def inboxes return @inboxes if defined?(@inboxes) - @inboxes = [@status.mentions, @status.reblogs, @status.poll.votes].flat_map do |relation| + @inboxes = [@status.mentions, @status.reblogs, @status.preloadable_poll.votes].flat_map do |relation| relation.includes(:account).map do |record| record.account.preferred_inbox_url if !record.account.local? && record.account.activitypub? end diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml index 561ce68b8..8533418cc 100644 --- a/config/locales/activerecord.en.yml +++ b/config/locales/activerecord.en.yml @@ -2,8 +2,9 @@ en: activerecord: attributes: - status: - owned_poll: Poll + poll: + expires_at: Deadline + options: Choices errors: models: account: diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 3a1463d95..412609de4 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -464,7 +464,7 @@ RSpec.describe ActivityPub::Activity::Create do context 'when a vote to a local poll' do let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) } - let!(:local_status) { Fabricate(:status, owned_poll: poll) } + let!(:local_status) { Fabricate(:status, poll: poll) } let(:object_json) do { @@ -489,7 +489,7 @@ RSpec.describe ActivityPub::Activity::Create do poll.save(validate: false) poll end - let!(:local_status) { Fabricate(:status, owned_poll: poll) } + let!(:local_status) { Fabricate(:status, poll: poll) } let(:object_json) do { -- cgit From 69141dca26f8a28d3aff63387b1c8d2bba7fdfa3 Mon Sep 17 00:00:00 2001 From: Alex Gessner Date: Thu, 28 Mar 2019 13:01:09 -0400 Subject: squashed identity proof updates (#10375) --- .../api/v1/accounts/identity_proofs_controller.rb | 19 ++++++++ .../settings/identity_proofs_controller.rb | 22 ++++++++- app/javascript/mastodon/actions/identity_proofs.js | 30 ++++++++++++ .../mastodon/features/account/components/header.js | 17 ++++++- .../features/account_timeline/components/header.js | 4 +- .../containers/header_container.js | 2 + .../mastodon/features/account_timeline/index.js | 3 ++ .../mastodon/reducers/identity_proofs.js | 25 ++++++++++ app/javascript/mastodon/reducers/index.js | 2 + app/javascript/styles/mastodon/containers.scss | 8 ++- app/javascript/styles/mastodon/forms.scss | 9 +++- app/lib/proof_provider/keybase.rb | 3 +- .../proof_provider/keybase/config_serializer.rb | 4 +- app/lib/proof_provider/keybase/verifier.rb | 6 +-- app/models/account_identity_proof.rb | 2 +- app/serializers/rest/identity_proof_serializer.rb | 17 +++++++ app/views/settings/identity_proofs/new.html.haml | 5 ++ config/locales/en.yml | 3 ++ config/routes.rb | 1 + .../settings/identity_proofs_controller_spec.rb | 57 ++++++++++++++++++++-- 20 files changed, 214 insertions(+), 25 deletions(-) create mode 100644 app/controllers/api/v1/accounts/identity_proofs_controller.rb create mode 100644 app/javascript/mastodon/actions/identity_proofs.js create mode 100644 app/javascript/mastodon/reducers/identity_proofs.js create mode 100644 app/serializers/rest/identity_proof_serializer.rb (limited to 'app/lib') diff --git a/app/controllers/api/v1/accounts/identity_proofs_controller.rb b/app/controllers/api/v1/accounts/identity_proofs_controller.rb new file mode 100644 index 000000000..bea51ae11 --- /dev/null +++ b/app/controllers/api/v1/accounts/identity_proofs_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class Api::V1::Accounts::IdentityProofsController < Api::BaseController + before_action :require_user! + before_action :set_account + + respond_to :json + + def index + @proofs = @account.identity_proofs.active + render json: @proofs, each_serializer: REST::IdentityProofSerializer + end + + private + + def set_account + @account = Account.find(params[:account_id]) + end +end diff --git a/app/controllers/settings/identity_proofs_controller.rb b/app/controllers/settings/identity_proofs_controller.rb index 4a3b89a5e..8f857fdcc 100644 --- a/app/controllers/settings/identity_proofs_controller.rb +++ b/app/controllers/settings/identity_proofs_controller.rb @@ -18,7 +18,12 @@ class Settings::IdentityProofsController < Settings::BaseController provider_username: params[:provider_username] ) - render layout: 'auth' + if current_account.username == params[:username] + render layout: 'auth' + else + flash[:alert] = I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username) + redirect_to settings_identity_proofs_path + end end def create @@ -26,6 +31,7 @@ class Settings::IdentityProofsController < Settings::BaseController @proof.token = resource_params[:token] if @proof.save + PostStatusService.new.call(current_user.account, text: post_params[:status_text]) if publish_proof? redirect_to @proof.on_success_path(params[:user_agent]) else flash[:alert] = I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize) @@ -36,10 +42,22 @@ class Settings::IdentityProofsController < Settings::BaseController private def check_required_params - redirect_to settings_identity_proofs_path unless [:provider, :provider_username, :token].all? { |k| params[k].present? } + redirect_to settings_identity_proofs_path unless [:provider, :provider_username, :username, :token].all? { |k| params[k].present? } end def resource_params params.require(:account_identity_proof).permit(:provider, :provider_username, :token) end + + def publish_proof? + ActiveModel::Type::Boolean.new.cast(post_params[:post_status]) + end + + def post_params + params.require(:account_identity_proof).permit(:post_status, :status_text) + end + + def set_body_classes + @body_classes = '' + end end diff --git a/app/javascript/mastodon/actions/identity_proofs.js b/app/javascript/mastodon/actions/identity_proofs.js new file mode 100644 index 000000000..449debf61 --- /dev/null +++ b/app/javascript/mastodon/actions/identity_proofs.js @@ -0,0 +1,30 @@ +import api from '../api'; + +export const IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST = 'IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST'; +export const IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS = 'IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS'; +export const IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL = 'IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL'; + +export const fetchAccountIdentityProofs = accountId => (dispatch, getState) => { + dispatch(fetchAccountIdentityProofsRequest(accountId)); + + api(getState).get(`/api/v1/accounts/${accountId}/identity_proofs`) + .then(({ data }) => dispatch(fetchAccountIdentityProofsSuccess(accountId, data))) + .catch(err => dispatch(fetchAccountIdentityProofsFail(accountId, err))); +}; + +export const fetchAccountIdentityProofsRequest = id => ({ + type: IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST, + id, +}); + +export const fetchAccountIdentityProofsSuccess = (accountId, identity_proofs) => ({ + type: IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS, + accountId, + identity_proofs, +}); + +export const fetchAccountIdentityProofsFail = (accountId, err) => ({ + type: IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL, + accountId, + err, +}); diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index d957de73d..76f50a5a4 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -62,6 +62,7 @@ class Header extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map, + identity_props: ImmutablePropTypes.list, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -81,7 +82,7 @@ class Header extends ImmutablePureComponent { } render () { - const { account, intl, domain } = this.props; + const { account, intl, domain, identity_proofs } = this.props; if (!account) { return null; @@ -234,8 +235,20 @@ class Header extends ImmutablePureComponent {
- {fields.size > 0 && ( + { (fields.size > 0 || identity_proofs.size > 0) && (
+ {identity_proofs.map((proof, i) => ( +
+
+ +
+ + + + +
+
+ ))} {fields.map((pair, i) => (
diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js index 16ada18c0..27dfcc516 100644 --- a/app/javascript/mastodon/features/account_timeline/components/header.js +++ b/app/javascript/mastodon/features/account_timeline/components/header.js @@ -12,6 +12,7 @@ export default class Header extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map, + identity_proofs: ImmutablePropTypes.list, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, @@ -84,7 +85,7 @@ export default class Header extends ImmutablePureComponent { } render () { - const { account, hideTabs } = this.props; + const { account, hideTabs, identity_proofs } = this.props; if (account === null) { return ; @@ -96,6 +97,7 @@ export default class Header extends ImmutablePureComponent { { const mapStateToProps = (state, { accountId }) => ({ account: getAccount(state, accountId), domain: state.getIn(['meta', 'domain']), + identity_proofs: state.getIn(['identity_proofs', accountId], ImmutableList()), }); return mapStateToProps; diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js index afc484c60..883f40d77 100644 --- a/app/javascript/mastodon/features/account_timeline/index.js +++ b/app/javascript/mastodon/features/account_timeline/index.js @@ -12,6 +12,7 @@ import ColumnBackButton from '../../components/column_back_button'; import { List as ImmutableList } from 'immutable'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; +import { fetchAccountIdentityProofs } from '../../actions/identity_proofs'; const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => { const path = withReplies ? `${accountId}:with_replies` : accountId; @@ -42,6 +43,7 @@ class AccountTimeline extends ImmutablePureComponent { const { params: { accountId }, withReplies } = this.props; this.props.dispatch(fetchAccount(accountId)); + this.props.dispatch(fetchAccountIdentityProofs(accountId)); if (!withReplies) { this.props.dispatch(expandAccountFeaturedTimeline(accountId)); } @@ -51,6 +53,7 @@ class AccountTimeline extends ImmutablePureComponent { componentWillReceiveProps (nextProps) { if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) { this.props.dispatch(fetchAccount(nextProps.params.accountId)); + this.props.dispatch(fetchAccountIdentityProofs(nextProps.params.accountId)); if (!nextProps.withReplies) { this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId)); } diff --git a/app/javascript/mastodon/reducers/identity_proofs.js b/app/javascript/mastodon/reducers/identity_proofs.js new file mode 100644 index 000000000..58af0a5fa --- /dev/null +++ b/app/javascript/mastodon/reducers/identity_proofs.js @@ -0,0 +1,25 @@ +import { Map as ImmutableMap, fromJS } from 'immutable'; +import { + IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST, + IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS, + IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL, +} from '../actions/identity_proofs'; + +const initialState = ImmutableMap(); + +export default function identityProofsReducer(state = initialState, action) { + switch(action.type) { + case IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST: + return state.set('isLoading', true); + case IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL: + return state.set('isLoading', false); + case IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS: + return state.update(identity_proofs => identity_proofs.withMutations(map => { + map.set('isLoading', false); + map.set('loaded', true); + map.set(action.accountId, fromJS(action.identity_proofs)); + })); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index a7e9c4d0f..981ad8e64 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -30,6 +30,7 @@ import filters from './filters'; import conversations from './conversations'; import suggestions from './suggestions'; import polls from './polls'; +import identity_proofs from './identity_proofs'; const reducers = { dropdown_menu, @@ -56,6 +57,7 @@ const reducers = { notifications, height_cache, custom_emojis, + identity_proofs, lists, listEditor, listAdder, diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index 2b1d988f2..368c2304b 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -10,12 +10,10 @@ } .logo-container { - margin: 100px auto; - margin-bottom: 50px; + margin: 100px auto 50px; - @media screen and (max-width: 400px) { - margin: 30px auto; - margin-bottom: 20px; + @media screen and (max-width: 500px) { + margin: 40px auto 0; } h1 { diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 3ea104786..91888d305 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -854,13 +854,19 @@ code { flex: 1; flex-direction: column; flex-shrink: 1; + max-width: 50%; &-sep { + align-self: center; flex-grow: 0; overflow: visible; position: relative; z-index: 1; } + + p { + word-break: break-word; + } } .account__avatar { @@ -882,12 +888,13 @@ code { height: 100%; left: 50%; position: absolute; + top: 0; width: 1px; } } &__row { - align-items: center; + align-items: flex-start; display: flex; flex-direction: row; } diff --git a/app/lib/proof_provider/keybase.rb b/app/lib/proof_provider/keybase.rb index 96322a265..672e1cb4b 100644 --- a/app/lib/proof_provider/keybase.rb +++ b/app/lib/proof_provider/keybase.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true class ProofProvider::Keybase - BASE_URL = 'https://keybase.io' + BASE_URL = ENV.fetch('KEYBASE_BASE_URL', 'https://keybase.io') + DOMAIN = ENV.fetch('KEYBASE_DOMAIN', Rails.configuration.x.local_domain) class Error < StandardError; end diff --git a/app/lib/proof_provider/keybase/config_serializer.rb b/app/lib/proof_provider/keybase/config_serializer.rb index 557bafe84..5241d201f 100644 --- a/app/lib/proof_provider/keybase/config_serializer.rb +++ b/app/lib/proof_provider/keybase/config_serializer.rb @@ -14,7 +14,7 @@ class ProofProvider::Keybase::ConfigSerializer < ActiveModel::Serializer end def domain - Rails.configuration.x.local_domain + ProofProvider::Keybase::DOMAIN end def display_name @@ -66,6 +66,6 @@ class ProofProvider::Keybase::ConfigSerializer < ActiveModel::Serializer end def contact - [Setting.site_contact_email.presence].compact + [Setting.site_contact_email.presence || 'unknown'].compact end end diff --git a/app/lib/proof_provider/keybase/verifier.rb b/app/lib/proof_provider/keybase/verifier.rb index 86f249dd7..ab1422323 100644 --- a/app/lib/proof_provider/keybase/verifier.rb +++ b/app/lib/proof_provider/keybase/verifier.rb @@ -49,14 +49,10 @@ class ProofProvider::Keybase::Verifier def query_params { - domain: domain, + domain: ProofProvider::Keybase::DOMAIN, kb_username: @provider_username, username: @local_username, sig_hash: @token, } end - - def domain - Rails.configuration.x.local_domain - end end diff --git a/app/models/account_identity_proof.rb b/app/models/account_identity_proof.rb index e7a3f97e5..1ac234735 100644 --- a/app/models/account_identity_proof.rb +++ b/app/models/account_identity_proof.rb @@ -26,7 +26,7 @@ class AccountIdentityProof < ApplicationRecord scope :active, -> { where(verified: true, live: true) } - after_create_commit :queue_worker + after_commit :queue_worker, if: :saved_change_to_token? delegate :refresh!, :on_success_path, :badge, to: :provider_instance diff --git a/app/serializers/rest/identity_proof_serializer.rb b/app/serializers/rest/identity_proof_serializer.rb new file mode 100644 index 000000000..0e7415935 --- /dev/null +++ b/app/serializers/rest/identity_proof_serializer.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class REST::IdentityProofSerializer < ActiveModel::Serializer + attributes :provider, :provider_username, :updated_at, :proof_url, :profile_url + + def proof_url + object.badge.proof_url + end + + def profile_url + object.badge.profile_url + end + + def provider + object.provider.capitalize + end +end diff --git a/app/views/settings/identity_proofs/new.html.haml b/app/views/settings/identity_proofs/new.html.haml index 8ce6e61c9..5e4e9895d 100644 --- a/app/views/settings/identity_proofs/new.html.haml +++ b/app/views/settings/identity_proofs/new.html.haml @@ -27,5 +27,10 @@ %p= t('identity_proofs.i_am_html', username: content_tag(:strong, @proof.provider_username), service: @proof.provider.capitalize) + .connection-prompt__post + = f.input :post_status, label: t('identity_proofs.publicize_checkbox'), as: :boolean, wrapper: :with_label, :input_html => { checked: true } + + = f.input :status_text, as: :text, input_html: { value: t('identity_proofs.publicize_toot', username: @proof.provider_username, service: @proof.provider.capitalize, url: @proof.badge.proof_url), rows: 4 } + = f.button :button, t('identity_proofs.authorize'), type: :submit = link_to t('simple_form.no'), settings_identity_proofs_url, class: 'button negative' diff --git a/config/locales/en.yml b/config/locales/en.yml index d91e89d95..64467be39 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -648,10 +648,13 @@ en: keybase: invalid_token: Keybase tokens are hashes of signatures and must be 66 hex characters verification_failed: Keybase does not recognize this token as a signature of Keybase user %{kb_username}. Please retry from Keybase. + wrong_user: Cannot create a proof for %{proving} while logged in as %{current}. Log in as %{proving} and try again. explanation_html: Here you can cryptographically connect your other identities, such as a Keybase profile. This lets other people send you encrypted messages and trust content you send them. i_am_html: I am %{username} on %{service}. identity: Identity inactive: Inactive + publicize_checkbox: 'And toot this:' + publicize_toot: 'It is proven! I am %{username} on %{service}: %{url}' status: Verification status view_proof: View proof imports: diff --git a/config/routes.rb b/config/routes.rb index 194b4c09b..a98dbb700 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -354,6 +354,7 @@ Rails.application.routes.draw do resources :followers, only: :index, controller: 'accounts/follower_accounts' resources :following, only: :index, controller: 'accounts/following_accounts' resources :lists, only: :index, controller: 'accounts/lists' + resources :identity_proofs, only: :index, controller: 'accounts/identity_proofs' member do post :follow diff --git a/spec/controllers/settings/identity_proofs_controller_spec.rb b/spec/controllers/settings/identity_proofs_controller_spec.rb index 46af3ccf4..5c05eb83c 100644 --- a/spec/controllers/settings/identity_proofs_controller_spec.rb +++ b/spec/controllers/settings/identity_proofs_controller_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' describe Settings::IdentityProofsController do + include RoutingHelper render_views let(:user) { Fabricate(:user) } @@ -9,8 +10,15 @@ describe Settings::IdentityProofsController do let(:provider) { 'keybase' } let(:findable_id) { Faker::Number.number(5) } let(:unfindable_id) { Faker::Number.number(5) } + let(:new_proof_params) do + { provider: provider, provider_username: kbname, token: valid_token, username: user.account.username } + end + let(:status_text) { "i just proved that i am also #{kbname} on #{provider}." } + let(:status_posting_params) do + { post_status: '0', status_text: status_text } + end let(:postable_params) do - { account_identity_proof: { provider: provider, provider_username: kbname, token: valid_token } } + { account_identity_proof: new_proof_params.merge(status_posting_params) } end before do @@ -19,10 +27,32 @@ describe Settings::IdentityProofsController do end describe 'new proof creation' do - context 'GET #new with no existing proofs' do - it 'redirects to :index' do - get :new - expect(response).to redirect_to settings_identity_proofs_path + context 'GET #new' do + context 'with all of the correct params' do + before do + allow_any_instance_of(ProofProvider::Keybase::Badge).to receive(:avatar_url) { full_pack_url('media/images/void.png') } + end + + it 'renders the template' do + get :new, params: new_proof_params + expect(response).to render_template(:new) + end + end + + context 'without any params' do + it 'redirects to :index' do + get :new, params: {} + expect(response).to redirect_to settings_identity_proofs_path + end + end + + context 'with params to prove a different, not logged-in user' do + let(:wrong_user_params) { new_proof_params.merge(username: 'someone_else') } + + it 'shows a helpful alert' do + get :new, params: wrong_user_params + expect(flash[:alert]).to eq I18n.t('identity_proofs.errors.wrong_user', proving: 'someone_else', current: user.account.username) + end end end @@ -44,6 +74,23 @@ describe Settings::IdentityProofsController do post :create, params: postable_params expect(response).to redirect_to root_url end + + it 'does not post a status' do + expect(PostStatusService).not_to receive(:new) + post :create, params: postable_params + end + + context 'and the user has requested to post a status' do + let(:postable_params_with_status) do + postable_params.tap { |p| p[:account_identity_proof][:post_status] = '1' } + end + + it 'posts a status' do + expect_any_instance_of(PostStatusService).to receive(:call).with(user.account, text: status_text) + + post :create, params: postable_params_with_status + end + end end context 'when saving fails' do -- cgit From 1714ea597866556ef9dd21d5d382f1d9181e0924 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 30 Mar 2019 02:12:06 +0100 Subject: Add ActivityPub representation for identity proofs (#10414) * Add ActivityPub representation for identity proofs * Add tests --- app/lib/activitypub/adapter.rb | 1 + app/lib/proof_provider/keybase.rb | 3 +- app/serializers/activitypub/actor_serializer.rb | 24 +++++++++++-- .../activitypub/process_account_service.rb | 28 ++++++++++++++- .../activitypub/process_account_service_spec.rb | 41 ++++++++++++++++++++++ 5 files changed, 93 insertions(+), 4 deletions(-) (limited to 'app/lib') diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index 7e0b16c25..94eb2899c 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -18,6 +18,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base atom_uri: { 'ostatus' => 'http://ostatus.org#', 'atomUri' => 'ostatus:atomUri' }, conversation: { 'ostatus' => 'http://ostatus.org#', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'conversation' => 'ostatus:conversation' }, focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } }, + identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' }, }.freeze def self.default_key_transform diff --git a/app/lib/proof_provider/keybase.rb b/app/lib/proof_provider/keybase.rb index 672e1cb4b..628972e9d 100644 --- a/app/lib/proof_provider/keybase.rb +++ b/app/lib/proof_provider/keybase.rb @@ -28,7 +28,8 @@ class ProofProvider::Keybase return end - return if @proof.provider_username.blank? + # Do not perform synchronous validation for remote accounts + return if @proof.provider_username.blank? || !@proof.account.local? if verifier.valid? @proof.verified = true diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 4b982b955..0644219fb 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -6,7 +6,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer context :security context_extensions :manually_approves_followers, :featured, :also_known_as, - :moved_to, :property_value, :hashtag, :emoji + :moved_to, :property_value, :hashtag, :emoji, :identity_proof attributes :id, :type, :following, :followers, :inbox, :outbox, :featured, @@ -115,7 +115,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer end def virtual_attachments - object.fields + object.fields + object.identity_proofs.active end def moved_to @@ -158,4 +158,24 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer Formatter.instance.format_field(object.account, object.value) end end + + class AccountIdentityProofSerializer < ActivityPub::Serializer + attributes :type, :name, :signature_algorithm, :signature_value + + def type + 'IdentityProof' + end + + def name + object.provider_username + end + + def signature_algorithm + object.provider + end + + def signature_value + object.token + end + end end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 5e3308428..6d0609ca0 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -24,6 +24,7 @@ class ActivityPub::ProcessAccountService < BaseService create_account if @account.nil? update_account process_tags + process_attachments else raise Mastodon::RaceConditionError end @@ -151,7 +152,7 @@ class ActivityPub::ProcessAccountService < BaseService def property_values return unless @json['attachment'].is_a?(Array) - @json['attachment'].select { |attachment| attachment['type'] == 'PropertyValue' }.map { |attachment| attachment.slice('name', 'value') } + as_array(@json['attachment']).select { |attachment| attachment['type'] == 'PropertyValue' }.map { |attachment| attachment.slice('name', 'value') } end def mismatching_origin?(url) @@ -231,6 +232,23 @@ class ActivityPub::ProcessAccountService < BaseService end end + def process_attachments + return if @json['attachment'].blank? + + previous_proofs = @account.identity_proofs.to_a + current_proofs = [] + + as_array(@json['attachment']).each do |attachment| + next unless equals_or_includes?(attachment['type'], 'IdentityProof') + current_proofs << process_identity_proof(attachment) + end + + previous_proofs.each do |previous_proof| + next if current_proofs.any? { |current_proof| current_proof.id == previous_proof.id } + previous_proof.delete + end + end + def process_emoji(tag) return if skip_download? return if tag['name'].blank? || tag['icon'].blank? || tag['icon']['url'].blank? @@ -247,4 +265,12 @@ class ActivityPub::ProcessAccountService < BaseService emoji.image_remote_url = image_url emoji.save end + + def process_identity_proof(attachment) + provider = attachment['signatureAlgorithm'] + provider_username = attachment['name'] + token = attachment['signatureValue'] + + @account.identity_proofs.where(provider: provider, provider_username: provider_username).find_or_create_by(provider: provider, provider_username: provider_username, token: token) + end end diff --git a/spec/services/activitypub/process_account_service_spec.rb b/spec/services/activitypub/process_account_service_spec.rb index d3318b2ed..ab8fb5cc3 100644 --- a/spec/services/activitypub/process_account_service_spec.rb +++ b/spec/services/activitypub/process_account_service_spec.rb @@ -28,4 +28,45 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do expect(account.fields[1].value).to eq 'Unit test' end end + + context 'identity proofs' do + let(:payload) do + { + id: 'https://foo.test', + type: 'Actor', + inbox: 'https://foo.test/inbox', + attachment: [ + { type: 'IdentityProof', name: 'Alice', signatureAlgorithm: 'keybase', signatureValue: 'a' * 66 }, + ], + }.with_indifferent_access + end + + it 'parses out of attachment' do + account = subject.call('alice', 'example.com', payload) + + expect(account.identity_proofs.count).to eq 1 + + proof = account.identity_proofs.first + + expect(proof.provider).to eq 'keybase' + expect(proof.provider_username).to eq 'Alice' + expect(proof.token).to eq 'a' * 66 + end + + it 'removes no longer present proofs' do + account = Fabricate(:account, username: 'alice', domain: 'example.com') + old_proof = Fabricate(:account_identity_proof, account: account, provider: 'keybase', provider_username: 'Bob', token: 'b' * 66) + + subject.call('alice', 'example.com', payload) + + expect(account.identity_proofs.count).to eq 1 + expect(account.identity_proofs.find_by(id: old_proof.id)).to be_nil + end + + it 'queues a validity check on the proof' do + allow(ProofProvider::Keybase::Worker).to receive(:perform_async) + account = subject.call('alice', 'example.com', payload) + expect(ProofProvider::Keybase::Worker).to have_received(:perform_async) + end + end end -- cgit