diff --git a/Gemfile b/Gemfile index 639f7cbf4..30aa76763 100644 --- a/Gemfile +++ b/Gemfile @@ -13,6 +13,7 @@ gem "unicorn-worker-killer", "0.4.4" # Federation +gem "diaspora_federation-json_schema", "0.2.1" gem "diaspora_federation-rails", "0.2.1" # API and JSON @@ -277,6 +278,8 @@ group :test do gem "fixture_builder", "0.5.0" gem "fuubar", "2.2.0" + gem "json-schema-rspec", "0.0.4" + gem "rspec-json_expectations", "~> 2.1" gem "test_after_commit", "1.1.0" # Cucumber (integration tests) diff --git a/Gemfile.lock b/Gemfile.lock index b07bb4e82..a987f2839 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -168,6 +168,7 @@ GEM nokogiri (~> 1.6, >= 1.6.8) typhoeus (~> 1.0) valid (~> 1.0) + diaspora_federation-json_schema (0.2.1) diaspora_federation-rails (0.2.1) actionpack (>= 4.2, < 6) diaspora_federation (= 0.2.1) @@ -334,6 +335,9 @@ GEM url_safe_base64 json-schema (2.8.0) addressable (>= 2.4) + json-schema-rspec (0.0.4) + json-schema (~> 2.5) + rspec jsonpath (0.8.5) multi_json jwt (1.5.6) @@ -582,6 +586,7 @@ GEM rspec-expectations (3.6.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.6.0) + rspec-json_expectations (2.1.0) rspec-mocks (3.6.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.6.0) @@ -772,6 +777,7 @@ DEPENDENCIES devise (= 4.3.0) devise_lastseenable (= 0.0.6) diaspora-prosody-config (= 0.0.7) + diaspora_federation-json_schema (= 0.2.1) diaspora_federation-rails (= 0.2.1) diaspora_federation-test (= 0.2.1) entypo-rails (= 3.0.0) @@ -800,6 +806,7 @@ DEPENDENCIES js_image_paths (= 0.1.0) json (= 2.1.0) json-schema (= 2.8.0) + json-schema-rspec (= 0.0.4) leaflet-rails (= 1.1.0) logging-rails (= 0.6.0) markerb (= 1.1.0) @@ -858,6 +865,7 @@ DEPENDENCIES rb-inotify (= 0.9.10) redcarpet (= 3.4.0) responders (= 2.4.0) + rspec-json_expectations (~> 2.1) rspec-rails (= 3.6.0) rubocop (= 0.49.1) ruby-oembed (= 0.12.0) diff --git a/app/models/person.rb b/app/models/person.rb index d76cfcb92..3f20a3a84 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -37,7 +37,9 @@ class Person < ActiveRecord::Base has_many :posts, :foreign_key => :author_id, :dependent => :destroy # This person's own posts has_many :photos, :foreign_key => :author_id, :dependent => :destroy # This person's own photos has_many :comments, :foreign_key => :author_id, :dependent => :destroy # This person's own comments + has_many :likes, foreign_key: :author_id, dependent: :destroy # This person's own likes has_many :participations, :foreign_key => :author_id, :dependent => :destroy + has_many :poll_participations, foreign_key: :author_id, dependent: :destroy has_many :conversation_visibilities has_many :roles diff --git a/app/serializers/export/comment_serializer.rb b/app/serializers/export/comment_serializer.rb deleted file mode 100644 index 58b5bde1a..000000000 --- a/app/serializers/export/comment_serializer.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Export - class CommentSerializer < ActiveModel::Serializer - attributes :guid, - :text, - :post_guid - - def post_guid - object.post.guid - end - end -end diff --git a/app/serializers/export/contact_serializer.rb b/app/serializers/export/contact_serializer.rb index be025304f..871486777 100644 --- a/app/serializers/export/contact_serializer.rb +++ b/app/serializers/export/contact_serializer.rb @@ -2,11 +2,33 @@ module Export class ContactSerializer < ActiveModel::Serializer attributes :sharing, :receiving, + :following, + :followed, :person_guid, :person_name, - :person_first_name, - :person_diaspora_handle + :account_id, + :public_key - has_many :aspects, each_serializer: Export::AspectSerializer + has_many :contact_groups_membership + + def following + object.sharing + end + + def followed + object.receiving + end + + def account_id + object.person_diaspora_handle + end + + def contact_groups_membership + object.aspects.map(&:name) + end + + def public_key + object.person.serialized_public_key + end end end diff --git a/app/serializers/export/others_data_serializer.rb b/app/serializers/export/others_data_serializer.rb new file mode 100644 index 000000000..96a819c38 --- /dev/null +++ b/app/serializers/export/others_data_serializer.rb @@ -0,0 +1,34 @@ +module Export + class OthersDataSerializer < ActiveModel::Serializer + # Relayables of other people in the archive: comments, likes, participations, poll participations where author is + # the archive owner + has_many :relayables, each_serializer: FederationEntitySerializer + + # Parent posts of user's own relayables. We have to save metadata to use + # it in case when posts temporary unavailable on the target pod. + has_many :posts, each_serializer: FederationEntitySerializer + + # Authors of posts where we participated and authors are not in contacts + has_many :non_contact_authors, each_serializer: PersonMetadataSerializer + + private + + def relayables + %i[comments likes poll_participations].map {|relayable| + others_relayables.send(relayable) + }.sum + end + + def others_relayables + @others_relayables ||= Diaspora::Exporter::OthersRelayables.new(object.person_id) + end + + def posts + @posts ||= Diaspora::Exporter::PostsWithActivity.new(object).query + end + + def non_contact_authors + Diaspora::Exporter::NonContactAuthors.new(posts, object).query + end + end +end diff --git a/app/serializers/export/own_post_serializer.rb b/app/serializers/export/own_post_serializer.rb new file mode 100644 index 000000000..150ecd427 --- /dev/null +++ b/app/serializers/export/own_post_serializer.rb @@ -0,0 +1,33 @@ +module Export + # This is a serializer for the user's own posts + class OwnPostSerializer < FederationEntitySerializer + # Only for public posts. + # Includes URIs of pods which must be notified on the post updates. + # Must always include local pod URI since we will want all the updates on the post if user migrates. + has_many :subscribed_pods_uris + + # Only for private posts. + # Includes diaspora* IDs of people who must be notified on post updates. + has_many :subscribed_users_ids + + # Normally accepts Post as an object. + def initialize(*) + super + self.except = [excluded_subscription_key] + end + + private + + def subscribed_pods_uris + object.subscribed_pods_uris.push(AppConfig.pod_uri.to_s) + end + + def subscribed_users_ids + object.subscribers.map(&:diaspora_handle) + end + + def excluded_subscription_key + entity.public ? :subscribed_users_ids : :subscribed_pods_uris + end + end +end diff --git a/app/serializers/export/own_relayables_serializer.rb b/app/serializers/export/own_relayables_serializer.rb new file mode 100644 index 000000000..a38f83ccf --- /dev/null +++ b/app/serializers/export/own_relayables_serializer.rb @@ -0,0 +1,13 @@ +module Export + # This is a serializer for the user's own relayables. We remove signature from the own relayables since it isn't + # useful and takes space. + class OwnRelayablesSerializer < FederationEntitySerializer + private + + def modify_serializable_object(hash) + super.tap {|hash| + hash[:entity_data].delete(:author_signature) + } + end + end +end diff --git a/app/serializers/export/person_metadata_serializer.rb b/app/serializers/export/person_metadata_serializer.rb new file mode 100644 index 000000000..911062e39 --- /dev/null +++ b/app/serializers/export/person_metadata_serializer.rb @@ -0,0 +1,17 @@ +module Export + class PersonMetadataSerializer < ActiveModel::Serializer + attributes :guid, + :account_id, + :public_key + + private + + def account_id + object.diaspora_handle + end + + def public_key + object.serialized_public_key + end + end +end diff --git a/app/serializers/export/post_serializer.rb b/app/serializers/export/post_serializer.rb deleted file mode 100644 index 84b6a91ed..000000000 --- a/app/serializers/export/post_serializer.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Export - class PostSerializer < ActiveModel::Serializer - attributes :guid, - :text, - :public, - :diaspora_handle, - :type, - :likes_count, - :comments_count, - :reshares_count, - :created_at - end -end diff --git a/app/serializers/export/profile_serializer.rb b/app/serializers/export/profile_serializer.rb deleted file mode 100644 index b8eb2001f..000000000 --- a/app/serializers/export/profile_serializer.rb +++ /dev/null @@ -1,14 +0,0 @@ -module Export - class ProfileSerializer < ActiveModel::Serializer - attributes :first_name, - :last_name, - :gender, - :bio, - :birthday, - :location, - :image_url, - :diaspora_handle, - :searchable, - :nsfw - end -end diff --git a/app/serializers/export/user_serializer.rb b/app/serializers/export/user_serializer.rb index 7e3b6b42d..09f26d3bd 100644 --- a/app/serializers/export/user_serializer.rb +++ b/app/serializers/export/user_serializer.rb @@ -1,24 +1,49 @@ module Export class UserSerializer < ActiveModel::Serializer - attributes :name, + attributes :username, :email, :language, - :username, - :serialized_private_key, + :private_key, :disable_mail, :show_community_spotlight_in_stream, :auto_follow_back, :auto_follow_back_aspect, :strip_exif - has_one :profile, serializer: Export::ProfileSerializer - has_many :aspects, each_serializer: Export::AspectSerializer + has_one :profile, serializer: FederationEntitySerializer + has_many :contact_groups, each_serializer: Export::AspectSerializer has_many :contacts, each_serializer: Export::ContactSerializer - has_many :posts, each_serializer: Export::PostSerializer - has_many :comments, each_serializer: Export::CommentSerializer + has_many :posts, each_serializer: Export::OwnPostSerializer + has_many :followed_tags + has_many :post_subscriptions - def comments - object.person.comments + has_many :relayables, each_serializer: Export::OwnRelayablesSerializer + + private + + def relayables + [*comments, *likes, *poll_participations] end + %i[comments likes poll_participations].each {|collection| + delegate collection, to: :person + } + + delegate :person, to: :object + + def contact_groups + object.aspects + end + + def private_key + object.serialized_private_key + end + + def followed_tags + object.followed_tags.map(&:name) + end + + def post_subscriptions + Post.subscribed_by(object).pluck(:guid) + end end end diff --git a/app/serializers/federation_entity_serializer.rb b/app/serializers/federation_entity_serializer.rb new file mode 100644 index 000000000..0458fc73b --- /dev/null +++ b/app/serializers/federation_entity_serializer.rb @@ -0,0 +1,16 @@ +# This is an ActiveModel::Serializer based class which uses DiasporaFederation::Entity JSON serialization +# features in order to serialize local DB objects. To determine a type of entity class to use the same routines +# are used as for federation messages generation. +class FederationEntitySerializer < ActiveModel::Serializer + include SerializerPostProcessing + + private + + def modify_serializable_object(hash) + hash.merge(entity.to_json) + end + + def entity + @entity ||= Diaspora::Federation::Entities.build(object) + end +end diff --git a/app/serializers/serializer_post_processing.rb b/app/serializers/serializer_post_processing.rb new file mode 100644 index 000000000..ea865be8a --- /dev/null +++ b/app/serializers/serializer_post_processing.rb @@ -0,0 +1,20 @@ +# This module encapsulates knowledge about the way AMS works with the serializable object. +# The main responsibility of this module is to allow changing resulting object just before the +# JSON serialization happens. +module SerializerPostProcessing + # serializable_object output is used in AMS to produce a hash from input object that is passed to JSON serializer. + # serializable_object of ActiveModel::Serializer is not documented as officialy available API + # NOTE: if we ever move to AMS 0.10, this method was renamed there to serializable_hash + def serializable_object(options={}) + modify_serializable_object(super) + end + + # Users of this module may override this method in order to change serializable_object after + # the serializable hash generation and before its serialization. + def modify_serializable_object(hash) + hash + end + + # except is an array of keys that are excluded from serialized_object before JSON serialization + attr_accessor :except +end diff --git a/lib/account_deleter.rb b/lib/account_deleter.rb index 83c31c528..a466a3f2e 100644 --- a/lib/account_deleter.rb +++ b/lib/account_deleter.rb @@ -101,7 +101,8 @@ class AccountDeleter end def ignored_or_special_ar_person_associations - %i(comments contacts notification_actors notifications owner profile conversation_visibilities pod) + %i[comments likes poll_participations contacts notification_actors notifications owner profile + conversation_visibilities pod] end def mark_account_deletion_complete diff --git a/lib/diaspora/exporter.rb b/lib/diaspora/exporter.rb index e6f65ac7a..2c9e27909 100644 --- a/lib/diaspora/exporter.rb +++ b/lib/diaspora/exporter.rb @@ -6,22 +6,23 @@ module Diaspora class Exporter - SERIALIZED_VERSION = '1.0' + SERIALIZED_VERSION = "2.0".freeze def initialize(user) @user = user end def execute - @export ||= JSON.generate serialized_user.merge(version: SERIALIZED_VERSION) + JSON.generate full_archive end private - def serialized_user - @serialized_user ||= Export::UserSerializer.new(@user).as_json + def full_archive + {version: SERIALIZED_VERSION} + .merge(Export::UserSerializer.new(@user).as_json) + .merge(Export::OthersDataSerializer.new(@user).as_json) end - end end diff --git a/lib/diaspora/exporter/posts_with_activity.rb b/lib/diaspora/exporter/posts_with_activity.rb index 9cbec03c1..00461c55b 100644 --- a/lib/diaspora/exporter/posts_with_activity.rb +++ b/lib/diaspora/exporter/posts_with_activity.rb @@ -3,12 +3,13 @@ module Diaspora # This class allows to query posts where a person made any activity (submitted comments, # likes, participations or poll participations). class PostsWithActivity - # TODO: docs + # @param user [User] user who the activity belongs to (the one who liked, commented posts, etc) def initialize(user) @user = user end - # TODO: docs + # Create a request of posts with activity + # @return [Post::ActiveRecord_Relation] def query Post.from("(#{sql_union_all_activities}) AS posts") end @@ -26,7 +27,7 @@ module Diaspora end def all_activities - [comments_activity, likes_activity, subscriptions, polls_activity].compact + [comments_activity, likes_activity, subscriptions, polls_activity, reshares_activity] end def likes_activity @@ -41,6 +42,10 @@ module Diaspora other_people_posts.subscribed_by(user) end + def reshares_activity + other_people_posts.reshared_by(person) + end + def polls_activity StatusMessage.where.not(author_id: person.id).joins(:poll_participations) .where(poll_participations: {author_id: person.id}) diff --git a/lib/diaspora/shareable.rb b/lib/diaspora/shareable.rb index 96895f521..97078dd6a 100644 --- a/lib/diaspora/shareable.rb +++ b/lib/diaspora/shareable.rb @@ -50,6 +50,14 @@ module Diaspora end end + # Remote pods which are known to be subscribed to the post. Must include all pods which received the post in the + # past. + # + # @return [Array] The list of pods' URIs + def subscribed_pods_uris + Pod.find(subscribers.select(&:remote?).map(&:pod_id).uniq).map {|pod| pod.url_to("") } + end + module QueryMethods def owned_or_visible_by_user(user) with_visibility.where( diff --git a/lib/schemas/archive-format.json b/lib/schemas/archive-format.json new file mode 100644 index 000000000..11ba60a72 --- /dev/null +++ b/lib/schemas/archive-format.json @@ -0,0 +1,255 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://diaspora.github.io/diaspora/schemas/archive_format.json", + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "email": { "type": "string" }, + "language": { "type": "string" }, + "username": { "type": "string" }, + "private_key": { "type": "string" }, + "disable_mail": { "type": "boolean" }, + "show_community_spotlight_in_stream": { "type": "boolean" }, + "auto_follow_back": { "type": "boolean" }, + "auto_follow_back_aspect": { + "type": [ + "string", + "null" + ] + }, + "strip_exif": { "type": "boolean" }, + + "profile": { + "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#definitions/profile" + }, + + "contact_groups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "contacts_visible": { "type": "boolean" }, + "chat_enabled": { "type": "boolean" } + }, + "required": [ + "name" + ] + } + }, + + "contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "sharing": { "type": "boolean" }, + "following": { "type": "boolean" }, + "receiving": { "type": "boolean" }, + "followed": { "type": "boolean" }, + "account_id": { "type": "string" }, + "contact_groups_membership": { + "type": "array", + "items": { "type": "string" } + }, + "person_name": { "type": "string" }, + "person_guid": { + "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/guid" + }, + "public_key": { "type": "string" } + }, + "required": [ + "sharing", + "following", + "receiving", + "followed", + "account_id", + "contact_groups_membership" + ] + } + }, + + "posts": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/definitions/posts" + }, + { + "oneOf": [ + { "$ref": "#/definitions/remote_subscription/public" }, + { "$ref": "#/definitions/remote_subscription/private" } + ] + } + ] + + } + }, + + "relayables": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/comment" + }, + { + "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/like" + }, + { + "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/poll_participation" + } + ] + } + }, + + "followed_tags": { + "type": "array", + "items": { + "type": "string" + } + }, + + "post_subscriptions": { + "type": "array", + "description": "GUIDs of posts for which changes we want to be subscribed", + "items": { + "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/guid" + } + } + }, + "required": [ + "username", + "email", + "private_key", + "profile" + ] + }, + "others_data": { + "type": "object", + "properties": { + "relayables": { + "type": "array", + "items": { + "allOf": [ + { + "oneOf": [ + { + "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/comment" + }, + { + "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/like" + }, + { + "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/poll_participation" + } + ] + } + ] + } + }, + + "non_contact_authors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "account_id": { + "type": "string" + }, + "guid": { + "type": "string" + }, + "public_key": { + "type": "string" + } + }, + "required": [ + "account_id", + "guid", + "public_key" + ] + } + }, + + "posts": { + "type": "array", + "items": { + "$ref": "#/definitions/posts" + } + } + } + }, + "version": { + "type": "string", + "pattern": "^2\.0$" + } + }, + "required": [ + "user", + "version" + ], + "definitions": { + "posts": { + "oneOf": [ + { "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/status_message" }, + { "$ref": "https://diaspora.github.io/diaspora_federation/schemas/federation_entities.json#/definitions/reshare" } + ] + }, + + "remote_subscription": { + "public": { + "type": "object", + "properties": { + "subscribed_pods_uris": { + "type": "array", + "items": { + "type": "string" + } + }, + "entity_data": { + "type": "object", + "properties": { + "public": { + "enum": [ true ] + } + }, + "required": [ + "public" + ] + } + }, + "required": [ + "entity_data" + ] + }, + + "private": { + "type": "object", + "properties": { + "subscribed_users_ids": { + "type": "array", + "items": { + "type": "string" + } + }, + + "entity_data": { + "type": "object", + "properties": { + "public": { + "enum": [ false ] + } + }, + "required": [ + "public" + ] + } + } + } + } + } +} diff --git a/spec/factories.rb b/spec/factories.rb index 1f5f59d2e..30c8cb5ac 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -149,9 +149,9 @@ FactoryGirl.define do end factory(:location) do - address "Fernsehturm Berlin, Berlin, Germany" - lat 52.520645 - lng 13.409779 + sequence(:address) {|n| "Fernsehturm Berlin, #{n}, Berlin, Germany" } + sequence(:lat) {|n| 52.520645 + 0.0000001 * n } + sequence(:lng) {|n| 13.409779 + 0.0000001 * n } end factory :participation do diff --git a/spec/integration/exporter_spec.rb b/spec/integration/exporter_spec.rb new file mode 100644 index 000000000..8cea5501a --- /dev/null +++ b/spec/integration/exporter_spec.rb @@ -0,0 +1,444 @@ +# Copyright (c) 2010-2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +describe Diaspora::Exporter do + let(:user) { FactoryGirl.create(:user_with_aspect) } + + context "output json" do + let(:json) { Diaspora::Exporter.new(user).execute } + + it "matches archive schema" do + DataGenerator.create( + user, + %i[generic_user_data activity status_messages_flavours work_aspect] + ) + + expect(JSON.parse(json)).to match_json_schema(:archive_schema) + end + + it "contains basic user data" do + user_properties = build_property_hash( + user, + %i[email username language disable_mail show_community_spotlight_in_stream auto_follow_back + auto_follow_back_aspect strip_exif], + private_key: :serialized_private_key + ) + + user_properties[:profile] = { + entity_type: "profile", + entity_data: build_property_hash( + user.profile, + %i[first_name last_name gender bio location image_url birthday searchable nsfw tag_string], + author: :diaspora_handle + ) + } + + expect(json).to include_json(user: user_properties) + end + + it "contains aspects" do + DataGenerator.create(user, :work_aspect) + + expect(json).to include_json( + user: { + "contact_groups": [ + { + "name": "generic", + "contacts_visible": true, + "chat_enabled": false + }, + { + "name": "Work", + "contacts_visible": false, + "chat_enabled": false + } + ] + } + ) + end + + it "contains contacts" do + friends = DataGenerator.create(user, Array.new(2, :mutual_friend)) + serialized_contacts = friends.map {|friend| + contact = Contact.find_by(person_id: friend.person_id) + hash = build_property_hash( + contact, + %i[sharing receiving person_guid person_name], + following: :sharing, followed: :receiving, account_id: :person_diaspora_handle + ) + hash[:public_key] = contact.person.serialized_public_key + hash[:contact_groups_membership] = contact.aspects.map(&:name) + hash + } + + expect(json).to include_json(user: {contacts: serialized_contacts}) + end + + it "contains a public status message" do + status_message = FactoryGirl.create(:status_message, author: user.person, public: true) + serialized = { + "subscribed_pods_uris": [AppConfig.pod_uri.to_s], + "entity_type": "status_message", + "entity_data": { + "author": user.diaspora_handle, + "guid": status_message.guid, + "created_at": status_message.created_at.iso8601, + "text": status_message.text, + "public": true + } + } + + expect(json).to include_json(user: {posts: [serialized]}) + end + + it "contains a status message with subscribers" do + subscriber, status_message = DataGenerator.create(user, :status_message_with_subscriber) + serialized = { + "subscribed_users_ids": [subscriber.diaspora_handle], + "entity_type": "status_message", + "entity_data": { + "author": user.diaspora_handle, + "guid": status_message.guid, + "created_at": status_message.created_at.iso8601, + "text": status_message.text, + "public": false + } + } + + expect(json).to include_json(user: {posts: [serialized]}) + end + + it "contains a status message with a poll" do + status_message = FactoryGirl.create(:status_message_with_poll, author: user.person) + serialized = { + "entity_type": "status_message", + "entity_data": { + "author": user.diaspora_handle, + "guid": status_message.guid, + "created_at": status_message.created_at.iso8601, + "text": status_message.text, + "poll": { + "entity_type": "poll", + "entity_data": { + "guid": status_message.poll.guid, + "question": status_message.poll.question, + "poll_answers": status_message.poll.poll_answers.map {|answer| + { + "entity_type": "poll_answer", + "entity_data": { + "guid": answer.guid, + "answer": answer.answer + } + } + } + } + }, + "public": false + } + } + + expect(json).to include_json(user: {posts: [serialized]}) + end + + it "contains a status message with a photo" do + status_message = FactoryGirl.create(:status_message_with_photo, author: user.person) + + serialized = { + "entity_type": "status_message", + "entity_data": { + "author": user.diaspora_handle, + "guid": status_message.guid, + "created_at": status_message.created_at.iso8601, + "text": status_message.text, + "photos": [ + { + "entity_type": "photo", + "entity_data": { + "guid": status_message.photos.first.guid, + "author": user.diaspora_handle, + "public": false, + "created_at": status_message.photos.first.created_at.iso8601, + "remote_photo_path": "#{AppConfig.pod_uri}uploads\/images\/", + "remote_photo_name": status_message.photos.first.remote_photo_name, + "status_message_guid": status_message.guid, + "height": 42, + "width": 23 + } + } + ], + "public": false + } + } + + expect(json).to include_json(user: {posts: [serialized]}) + end + + it "contains a status message with a location" do + status_message = FactoryGirl.create(:status_message_with_location, author: user.person) + + serialized = { + "entity_type": "status_message", + "entity_data": { + "author": user.diaspora_handle, + "guid": status_message.guid, + "created_at": status_message.created_at.iso8601, + "text": status_message.text, + "location": { + "entity_type": "location", + "entity_data": { + "address": status_message.location.address, + "lat": status_message.location.lat, + "lng": status_message.location.lng + } + }, + "public": false + } + } + + expect(json).to include_json(user: {posts: [serialized]}) + end + + it "contains a reshare and its root" do + reshare = FactoryGirl.create(:reshare, author: user.person) + serialized_reshare = { + "subscribed_pods_uris": [reshare.root.author.pod.url_to(""), AppConfig.pod_uri.to_s], + "entity_type": "reshare", + "entity_data": { + "author": user.diaspora_handle, + "guid": reshare.guid, + "created_at": reshare.created_at.iso8601, + "public": true, + "root_author": reshare.root_author.diaspora_handle, + "root_guid": reshare.root_guid + } + } + + status_message = reshare.root + serialized_parent = { + "entity_type": "status_message", + "entity_data": { + "author": status_message.diaspora_handle, + "guid": status_message.guid, + "created_at": status_message.created_at.iso8601, + "text": status_message.text, + "public": true + } + } + + expect(json).to include_json( + user: {posts: [serialized_reshare]}, + others_data: {posts: [serialized_parent]} + ) + end + + it "contains followed tags" do + tag_following = DataGenerator.create(user, :tag_following) + expect(json).to include_json(user: {followed_tags: [tag_following.tag.name]}) + end + + it "contains post subscriptions" do + subscription = DataGenerator.create(user, :subscription) + expect(json).to include_json(user: {post_subscriptions: [subscription.target.guid]}) + end + + it "contains a comment and the commented post" do + comment = FactoryGirl.create(:comment, author: user.person) + serialized_comment = { + "entity_type": "comment", + "entity_data": { + "author": user.diaspora_handle, + "guid": comment.guid, + "parent_guid": comment.parent.guid, + "text": comment.text, + "created_at": comment.created_at.iso8601 + }, + "property_order": %w[author guid parent_guid text created_at] + } + + status_message = comment.parent + serialized_post = { + "entity_type": "status_message", + "entity_data": { + "author": status_message.diaspora_handle, + "guid": status_message.guid, + "created_at": status_message.created_at.iso8601, + "text": status_message.text, + "public": false + } + } + + expect(json).to include_json( + user: {relayables: [serialized_comment]}, + others_data: {posts: [serialized_post]} + ) + end + + it "contains a like and the liked post" do + like = FactoryGirl.create(:like, author: user.person) + serialized_like = { + "entity_type": "like", + "entity_data": { + "author": user.diaspora_handle, + "guid": like.guid, + "parent_guid": like.parent.guid, + "parent_type": like.target_type, + "positive": like.positive + }, + "property_order": %w[author guid parent_guid parent_type positive] + } + + status_message = like.target + serialized_post = { + "entity_type": "status_message", + "entity_data": { + "author": status_message.diaspora_handle, + "guid": status_message.guid, + "created_at": status_message.created_at.iso8601, + "text": status_message.text, + "public": false + } + } + + expect(json).to include_json( + user: {relayables: [serialized_like]}, + others_data: {posts: [serialized_post]} + ) + end + + it "contains a poll participation and post with this poll" do + poll_participation = FactoryGirl.create(:poll_participation, author: user.person) + serialized_participation = { + "entity_type": "poll_participation", + "entity_data": { + "author": user.diaspora_handle, + "guid": poll_participation.guid, + "parent_guid": poll_participation.parent.guid, + "poll_answer_guid": poll_participation.poll_answer.guid + }, + "property_order": %w[author guid parent_guid poll_answer_guid] + } + + poll = poll_participation.poll + status_message = poll_participation.status_message + serialized_post = { + "entity_type": "status_message", + "entity_data": { + "author": status_message.diaspora_handle, + "guid": status_message.guid, + "created_at": status_message.created_at.iso8601, + "text": status_message.text, + "poll": { + "entity_type": "poll", + "entity_data": { + "guid": poll.guid, + "question": poll.question, + "poll_answers": poll.poll_answers.map {|answer| + { + "entity_type": "poll_answer", + "entity_data": { + "guid": answer.guid, + "answer": answer.answer + } + } + } + } + }, + "public": false + } + } + + expect(json).to include_json( + user: {relayables: [serialized_participation]}, + others_data: {posts: [serialized_post]} + ) + end + + it "contains a comment for the user's post" do + status_message, comment = DataGenerator.create(user, :status_message_with_comment) + serialized = { + "entity_type": "comment", + "entity_data": { + "author": comment.diaspora_handle, + "guid": comment.guid, + "parent_guid": status_message.guid, + "text": comment.text, + "created_at": comment.created_at.iso8601, + "author_signature": Diaspora::Federation::Entities.build(comment).to_h[:author_signature] + }, + "property_order": %w[author guid parent_guid text created_at] + } + + expect(json).to include_json(others_data: {relayables: [serialized]}) + end + + it "contains a like for the user's post" do + status_message, like = DataGenerator.create(user, :status_message_with_like) + serialized = { + "entity_type": "like", + "entity_data": { + "author": like.diaspora_handle, + "guid": like.guid, + "parent_guid": status_message.guid, + "parent_type": like.target_type, + "positive": like.positive, + "author_signature": Diaspora::Federation::Entities.build(like).to_h[:author_signature] + }, + "property_order": %w[author guid parent_guid parent_type positive] + } + + expect(json).to include_json(others_data: {relayables: [serialized]}) + end + + it "contains a poll participation for the user's post" do + _, poll_participation = DataGenerator.create(user, :status_message_with_poll_participation) + serialized = { + "entity_type": "poll_participation", + "entity_data": { + "author": poll_participation.diaspora_handle, + "guid": poll_participation.guid, + "parent_guid": poll_participation.parent.guid, + "poll_answer_guid": poll_participation.poll_answer.guid, + "author_signature": Diaspora::Federation::Entities.build(poll_participation).to_h[:author_signature] + }, + "property_order": %w[author guid parent_guid poll_answer_guid] + } + + expect(json).to include_json(others_data: {relayables: [serialized]}) + end + + it "contains metadata of a non-contact author of a post where we commented" do + comment = FactoryGirl.create(:comment, author: user.person) + + author = comment.parent.author + expect(json).to include_json( + others_data: { + non_contact_authors: [ + { + "guid": author.guid, + "account_id": author.diaspora_handle, + "public_key": author.serialized_public_key + } + ] + } + ) + end + + def transform_value(value) + return value.iso8601 if value.is_a? Date + value + end + + def build_property_hash(object, direct_properties, aliased_properties={}) + props = direct_properties.map {|key| + [key, transform_value(object.send(key))] + }.to_h + + aliased = aliased_properties.map {|key, key_alias| + [key, object.send(key_alias)] + }.to_h + + props.merge(aliased) + end + end +end diff --git a/spec/lib/diaspora/exporter/posts_with_activity_spec.rb b/spec/lib/diaspora/exporter/posts_with_activity_spec.rb index c6335cbb4..4dc27495c 100644 --- a/spec/lib/diaspora/exporter/posts_with_activity_spec.rb +++ b/spec/lib/diaspora/exporter/posts_with_activity_spec.rb @@ -8,7 +8,8 @@ describe Diaspora::Exporter::PostsWithActivity do user.person.likes.first.target, user.person.comments.first.parent, user.person.poll_participations.first.parent.status_message, - user.person.participations.first.target + user.person.participations.first.target, + user.person.posts.reshares.first.root ] } diff --git a/spec/lib/diaspora/exporter_spec.rb b/spec/lib/diaspora/exporter_spec.rb index d12cfd2d0..7db86e82f 100644 --- a/spec/lib/diaspora/exporter_spec.rb +++ b/spec/lib/diaspora/exporter_spec.rb @@ -1,86 +1,15 @@ -# Copyright (c) 2010-2011, Diaspora Inc. This file is -# licensed under the Affero General Public License version 3 or later. See -# the COPYRIGHT file. - -require Rails.root.join('lib', 'diaspora', 'exporter') - describe Diaspora::Exporter do + describe "#execute" do + it "calls exporters and forms JSON" do + expect_any_instance_of(Export::UserSerializer).to receive(:as_json).and_return(user: "user_data") + expect_any_instance_of(Export::OthersDataSerializer).to receive(:as_json).and_return(others_date: "others_data") - before do - @user1 = alice - - @user1.person.profile.first_name = "