diff --git a/Gemfile b/Gemfile index 0021695037c..30aa76763bc 100644 --- a/Gemfile +++ b/Gemfile @@ -13,7 +13,8 @@ gem "unicorn-worker-killer", "0.4.4" # Federation -gem "diaspora_federation-rails", "0.2.0" +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) @@ -294,7 +297,7 @@ group :test do gem "timecop", "0.9.1" gem "webmock", "3.0.1", require: false - gem "diaspora_federation-test", "0.2.0" + gem "diaspora_federation-test", "0.2.1" # Coverage gem 'coveralls', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 798e8023830..a987f2839d5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -162,17 +162,18 @@ GEM devise rails (>= 3.0.4) diaspora-prosody-config (0.0.7) - diaspora_federation (0.2.0) + diaspora_federation (0.2.1) faraday (>= 0.9.0, < 0.13.0) - faraday_middleware (>= 0.10.0, < 0.12.0) + faraday_middleware (>= 0.10.0, < 0.13.0) nokogiri (~> 1.6, >= 1.6.8) typhoeus (~> 1.0) valid (~> 1.0) - diaspora_federation-rails (0.2.0) + diaspora_federation-json_schema (0.2.1) + diaspora_federation-rails (0.2.1) actionpack (>= 4.2, < 6) - diaspora_federation (= 0.2.0) - diaspora_federation-test (0.2.0) - diaspora_federation (= 0.2.0) + diaspora_federation (= 0.2.1) + diaspora_federation-test (0.2.1) + diaspora_federation (= 0.2.1) fabrication (~> 2.16.0) uuid (~> 2.3.8) diff-lcs (1.3) @@ -199,7 +200,7 @@ GEM sigar (~> 0.7.3) state_machines thor - fabrication (2.16.1) + fabrication (2.16.2) factory_girl (4.8.0) activesupport (>= 3.0.0) factory_girl_rails (4.8.0) @@ -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,8 +777,9 @@ DEPENDENCIES devise (= 4.3.0) devise_lastseenable (= 0.0.6) diaspora-prosody-config (= 0.0.7) - diaspora_federation-rails (= 0.2.0) - diaspora_federation-test (= 0.2.0) + diaspora_federation-json_schema (= 0.2.1) + diaspora_federation-rails (= 0.2.1) + diaspora_federation-test (= 0.2.1) entypo-rails (= 3.0.0) eye (= 0.9.2) factory_girl_rails (= 4.8.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 d76cfcb927a..169a84c18c8 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -37,7 +37,9 @@ def downcase_diaspora_handle 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 @@ -76,8 +78,8 @@ def downcase_diaspora_handle #not defensive scope :in_aspects, ->(aspect_ids) { - joins(:contacts => :aspect_memberships). - where(:aspect_memberships => {:aspect_id => aspect_ids}) + joins(contacts: :aspect_memberships) + .where(aspect_memberships: {aspect_id: aspect_ids}).distinct } scope :profile_tagged_with, ->(tag_name) { diff --git a/app/models/poll_participation.rb b/app/models/poll_participation.rb index 8dba2746573..7e653bb65c0 100644 --- a/app/models/poll_participation.rb +++ b/app/models/poll_participation.rb @@ -6,6 +6,7 @@ class PollParticipation < ActiveRecord::Base belongs_to :poll belongs_to :poll_answer, counter_cache: :vote_count + has_one :status_message, through: :poll has_one :signature, class_name: "PollParticipationSignature", dependent: :delete diff --git a/app/models/post.rb b/app/models/post.rb index 576717f7985..58dbf12cfbc 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -54,6 +54,17 @@ class Post < ActiveRecord::Base joins(:likes).where(:likes => {:author_id => person.id}) } + scope :subscribed_by, ->(user) { + joins(:participations).where(participations: {author_id: user.person_id}) + } + + scope :reshares, -> { where(type: "Reshare") } + + scope :reshared_by, ->(person) { + # we join on the same table, Rails renames "posts" to "reshares_posts" for the right table + joins(:reshares).where(reshares_posts: {author_id: person.id}) + } + def post_type self.class.name end diff --git a/app/models/status_message.rb b/app/models/status_message.rb index 423ea01fce4..a0c9d727ba2 100644 --- a/app/models/status_message.rb +++ b/app/models/status_message.rb @@ -19,6 +19,7 @@ class StatusMessage < Post has_one :location has_one :poll, autosave: true + has_many :poll_participations, through: :poll attr_accessor :oembed_url attr_accessor :open_graph_url diff --git a/app/serializers/export/comment_serializer.rb b/app/serializers/export/comment_serializer.rb deleted file mode 100644 index 58b5bde1a33..00000000000 --- 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 be025304f02..871486777cc 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 00000000000..96a819c38bb --- /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 00000000000..150ecd42780 --- /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 00000000000..a38f83ccf1e --- /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 00000000000..911062e39e2 --- /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 84b6a91ed23..00000000000 --- 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 b8eb2001f27..00000000000 --- 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 7e3b6b42ddc..09f26d3bdb8 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 00000000000..0458fc73b54 --- /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 00000000000..ea865be8abf --- /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/features/desktop/change_settings.feature b/features/desktop/change_settings.feature index 21c473930bd..6bc8ae64169 100644 --- a/features/desktop/change_settings.feature +++ b/features/desktop/change_settings.feature @@ -53,3 +53,8 @@ Feature: Change settings And I go to the stream page And I expand the publisher Then I should see "Public" within ".aspect_dropdown" + + Scenario: exporting profile data + When I click on the first selector "#account_data a" + Then I should see "Download my profile" + And I should have 1 email delivery diff --git a/lib/account_deleter.rb b/lib/account_deleter.rb index 83c31c5285e..a466a3f2e43 100644 --- a/lib/account_deleter.rb +++ b/lib/account_deleter.rb @@ -101,7 +101,8 @@ def normal_ar_person_associates_to_delete 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 e6f65ac7a67..2c9e2790949 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/non_contact_authors.rb b/lib/diaspora/exporter/non_contact_authors.rb new file mode 100644 index 00000000000..f3cfc4801a1 --- /dev/null +++ b/lib/diaspora/exporter/non_contact_authors.rb @@ -0,0 +1,36 @@ +module Diaspora + class Exporter + # This class is capable of quering a list of people from authors of given posts that are non-contacts of a given + # user. + class NonContactAuthors + # @param posts [Post::ActiveRecord_Relation] posts that we fetch authors from to make authors list + # @param user [User] a user we fetch a contact list from + def initialize(posts, user) + @posts = posts + @user = user + end + + # Create a request of non-contact authors of the posts for the user + # @return [Post::ActiveRecord_Relation] + def query + Person.where(id: non_contact_authors_ids) + end + + private + + def non_contact_authors_ids + posts_authors_ids - contacts_ids + end + + def posts_authors_ids + posts.pluck(:author_id).uniq + end + + def contacts_ids + user.contacts.pluck(:person_id) + end + + attr_reader :posts, :user + end + end +end diff --git a/lib/diaspora/exporter/others_relayables.rb b/lib/diaspora/exporter/others_relayables.rb new file mode 100644 index 00000000000..5876c52cdaa --- /dev/null +++ b/lib/diaspora/exporter/others_relayables.rb @@ -0,0 +1,42 @@ +module Diaspora + class Exporter + # This class implements methods that allow to query relayables (comments, likes, participations, + # poll_participations) of other people for posts of the given person. + class OthersRelayables + # @param person_id [Integer] Database id of a person for whom we want to request relayalbes + def initialize(person_id) + @person_id = person_id + end + + # Comments of other people to the person's post + # @return [Comment::ActiveRecord_Relation] + def comments + Comment + .where.not(author_id: person_id) + .joins("INNER JOIN posts ON (commentable_type = 'Post' AND posts.id = commentable_id)") + .where("posts.author_id = ?", person_id) + end + + # Likes of other people to the person's post + # @return [Like::ActiveRecord_Relation] + def likes + Like + .where.not(author_id: person_id) + .joins("INNER JOIN posts ON (target_type = 'Post' AND posts.id = target_id)") + .where("posts.author_id = ?", person_id) + end + + # Poll participations of other people to the person's polls + # @return [PollParticipation::ActiveRecord_Relation] + def poll_participations + PollParticipation + .where.not(author_id: person_id).joins(:status_message) + .where("posts.author_id = ?", person_id) + end + + private + + attr_reader :person_id + end + end +end diff --git a/lib/diaspora/exporter/posts_with_activity.rb b/lib/diaspora/exporter/posts_with_activity.rb new file mode 100644 index 00000000000..00461c55b18 --- /dev/null +++ b/lib/diaspora/exporter/posts_with_activity.rb @@ -0,0 +1,59 @@ +module Diaspora + class Exporter + # This class allows to query posts where a person made any activity (submitted comments, + # likes, participations or poll participations). + class PostsWithActivity + # @param user [User] user who the activity belongs to (the one who liked, commented posts, etc) + def initialize(user) + @user = user + end + + # Create a request of posts with activity + # @return [Post::ActiveRecord_Relation] + def query + Post.from("(#{sql_union_all_activities}) AS posts") + end + + private + + attr_reader :user + + def person + user.person + end + + def sql_union_all_activities + all_activities.map(&:to_sql).join(" UNION ") + end + + def all_activities + [comments_activity, likes_activity, subscriptions, polls_activity, reshares_activity] + end + + def likes_activity + other_people_posts.liked_by(person) + end + + def comments_activity + other_people_posts.commented_by(person) + end + + def subscriptions + 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}) + end + + def other_people_posts + Post.where.not(author_id: person.id) + end + end + end +end diff --git a/lib/diaspora/shareable.rb b/lib/diaspora/shareable.rb index 96895f521a7..97078dd6a69 100644 --- a/lib/diaspora/shareable.rb +++ b/lib/diaspora/shareable.rb @@ -50,6 +50,14 @@ def subscribers 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 00000000000..11ba60a724d --- /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 fadc1d1722a..30c8cb5acff 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -149,9 +149,9 @@ def r_str 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 @@ -342,6 +342,11 @@ def r_str additional_data { {"new_property" => "some text"} } end + factory :role do + association :person + name "moderator" + end + factory(:poll_participation_signature) do author_signature "some signature" association :signature_order, order: "guid parent_guid author poll_answer_guid new_property" diff --git a/spec/helper_methods.rb b/spec/helper_methods.rb index f7406a3efe9..720f28058eb 100644 --- a/spec/helper_methods.rb +++ b/spec/helper_methods.rb @@ -27,12 +27,12 @@ def uploaded_photo File.open(fixture_name) end - def create_conversation_with_message(sender, recipient_person, subject, text) + def create_conversation_with_message(sender_person, recipient_person, subject, text) create_hash = { - :author => sender.person, - :participant_ids => [sender.person.id, recipient_person.id], - :subject => subject, - :messages_attributes => [ {:author => sender.person, :text => text} ] + author: sender_person, + participant_ids: [sender_person.id, recipient_person.id], + subject: subject, + messages_attributes: [{author: sender_person, text: text}] } Conversation.create!(create_hash) diff --git a/spec/integration/account_deletion_spec.rb b/spec/integration/account_deletion_spec.rb index dfe3ee7c00d..7c934620443 100644 --- a/spec/integration/account_deletion_spec.rb +++ b/spec/integration/account_deletion_spec.rb @@ -1,128 +1,46 @@ -describe "deleteing your account", type: :request do - context "user" do - before do - @person = bob.person - @alices_post = alice.post(:status_message, - text: "@{bob Grimn; #{bob.person.diaspora_handle}} you are silly", - to: alice.aspects.find_by_name("generic")) - - # bob's own content - bob.post(:status_message, text: "asldkfjs", to: bob.aspects.first) - FactoryGirl.create(:photo, author: bob.person) - - @aspect_vis = AspectVisibility.where(aspect_id: bob.aspects.map(&:id)) - - # objects on post - bob.like!(@alices_post) - bob.comment!(@alices_post, "here are some thoughts on your post") - - # conversations - create_conversation_with_message(alice, bob.person, "Subject", "Hey bob") - - # join tables - @users_sv = ShareVisibility.where(user_id: bob.id).load - @persons_sv = ShareVisibility.where(shareable_id: bob.posts.map(&:id), shareable_type: "Post").load - - # user associated objects - @prefs = [] - %w(mentioned liked reshared).each do |pref| - @prefs << bob.user_preferences.create!(email_type: pref) - end - - # notifications - @notifications = [] - 3.times do - @notifications << FactoryGirl.create(:notification, recipient: bob) - end - - # services - @services = [] - 3.times do - @services << FactoryGirl.create(:service, user: bob) - end - - # block - @block = bob.blocks.create!(person: eve.person) - - AccountDeleter.new(bob.person.diaspora_handle).perform! - bob.reload - end - - it "deletes all of the user's preferences" do - expect(UserPreference.where(id: @prefs.map(&:id))).to be_empty - end - - it "deletes all of the user's notifications" do - expect(Notification.where(id: @notifications.map(&:id))).to be_empty - end - - it "deletes all of the users's blocked users" do - expect(Block.where(id: @block.id)).to be_empty - end - - it "deletes all of the user's services" do - expect(Service.where(id: @services.map(&:id))).to be_empty - end - - it "deletes all of bobs share visiblites" do - expect(ShareVisibility.where(id: @users_sv.map(&:id))).to be_empty - expect(ShareVisibility.where(id: @persons_sv.map(&:id))).to be_empty - end - - it "deletes all of bobs aspect visiblites" do - expect(AspectVisibility.where(id: @aspect_vis.map(&:id))).to be_empty - end +describe "deleteing account", type: :request do + def account_removal_method + AccountDeleter.new(subject.diaspora_handle).perform! + subject.reload + end - it "deletes all aspects" do - expect(bob.aspects).to be_empty - end + context "of local user" do + subject(:user) { FactoryGirl.create(:user_with_aspect) } - it "deletes all user contacts" do - expect(bob.contacts).to be_empty - end - - it "clears the account fields" do - bob.send(:clearable_fields).each do |field| - expect(bob.reload[field]).to be_blank - end + before do + DataGenerator.create(subject, :generic_user_data) + end + + it "deletes all of the user data" do + expect { + account_removal_method + }.to change(nil, "user preferences empty?") { UserPreference.where(user_id: user.id).empty? }.to(be_truthy) + .and(change(nil, "notifications empty?") { Notification.where(recipient_id: user.id).empty? }.to(be_truthy)) + .and(change(nil, "blocks empty?") { Block.where(user_id: user.id).empty? }.to(be_truthy)) + .and(change(nil, "services empty?") { Service.where(user_id: user.id).empty? }.to(be_truthy)) + .and(change(nil, "share visibilities empty?") { ShareVisibility.where(user_id: user.id).empty? }.to(be_truthy)) + .and(change(nil, "aspects empty?") { user.aspects.empty? }.to(be_truthy)) + .and(change(nil, "contacts empty?") { user.contacts.empty? }.to(be_truthy)) + .and(change(nil, "tag followings empty?") { user.tag_followings.empty? }.to(be_truthy)) + .and(change(nil, "clearable fields blank?") { + user.send(:clearable_fields).map {|field| + user.reload[field].blank? + } + }.to(eq([true] * user.send(:clearable_fields).count))) + end + + it_behaves_like "it removes the person associations" do + subject(:person) { user.person } end - - it_should_behave_like "it removes the person associations" end - context "remote person" do - before do - @person = remote_raphael - - # contacts - @contacts = @person.contacts - - # posts - @posts = (1..3).map do - FactoryGirl.create(:status_message, author: @person) - end - - @persons_sv = @posts.each do |post| - @contacts.each do |contact| - ShareVisibility.create!(user_id: contact.user.id, shareable: post) - end - end + context "of remote person" do + subject(:person) { remote_raphael } - # photos - @photo = FactoryGirl.create(:photo, author: @person) - - # mentions - @mentions = 3.times do - FactoryGirl.create(:mention, person: @person) - end - - # conversations - create_conversation_with_message(alice, @person, "Subject", "Hey bob") - - AccountDeleter.new(@person.diaspora_handle).perform! - @person.reload + before do + DataGenerator.create(subject, :generic_person_data) end - it_should_behave_like "it removes the person associations" + it_behaves_like "it removes the person associations" end end diff --git a/spec/integration/exporter_spec.rb b/spec/integration/exporter_spec.rb new file mode 100644 index 00000000000..8cea5501af1 --- /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/non_contact_authors_spec.rb b/spec/lib/diaspora/exporter/non_contact_authors_spec.rb new file mode 100644 index 00000000000..cac8c3ca047 --- /dev/null +++ b/spec/lib/diaspora/exporter/non_contact_authors_spec.rb @@ -0,0 +1,25 @@ +describe Diaspora::Exporter::NonContactAuthors do + describe "#query" do + let(:user) { FactoryGirl.create(:user_with_aspect) } + let(:post) { FactoryGirl.create(:status_message) } + let(:instance) { + Diaspora::Exporter::NonContactAuthors.new(Post.where(id: post.id), user) + } + + context "without contact relationship" do + it "includes post author to the result set" do + expect(instance.query).to eq([post.author]) + end + end + + context "with contact relationship" do + before do + user.share_with(post.author, user.aspects.first) + end + + it "doesn't include post author to the result set" do + expect(instance.query).to be_empty + end + end + end +end diff --git a/spec/lib/diaspora/exporter/others_relayables_spec.rb b/spec/lib/diaspora/exporter/others_relayables_spec.rb new file mode 100644 index 00000000000..95595739fae --- /dev/null +++ b/spec/lib/diaspora/exporter/others_relayables_spec.rb @@ -0,0 +1,35 @@ +describe Diaspora::Exporter::OthersRelayables do + let(:status_message) { FactoryGirl.create(:status_message) } + let(:person) { status_message.author } + let(:instance) { Diaspora::Exporter::OthersRelayables.new(person.id) } + + describe "#comments" do + let(:comment) { FactoryGirl.create(:comment, post: status_message) } + + it "has a comment in the data set" do + expect(instance.comments).to eq([comment]) + end + end + + describe "#likes" do + let(:like) { FactoryGirl.create(:like, target: status_message) } + + it "has a like in the data set" do + expect(instance.likes).to eq([like]) + end + end + + describe "#poll_participations" do + let(:status_message) { FactoryGirl.create(:status_message_with_poll) } + let(:poll_participation) { + FactoryGirl.create( + :poll_participation, + poll_answer: status_message.poll.poll_answers.first + ) + } + + it "has a poll participation in the data set" do + expect(instance.poll_participations).to eq([poll_participation]) + 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 new file mode 100644 index 00000000000..4dc27495c84 --- /dev/null +++ b/spec/lib/diaspora/exporter/posts_with_activity_spec.rb @@ -0,0 +1,24 @@ +describe Diaspora::Exporter::PostsWithActivity do + let(:user) { FactoryGirl.create(:user) } + let(:instance) { Diaspora::Exporter::PostsWithActivity.new(user) } + + describe "#query" do + let(:activity) { + [ + 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.posts.reshares.first.root + ] + } + + before do + DataGenerator.create(user, %i[activity participation]) + end + + it "returns all posts with person's activity" do + expect(instance.query).to match_array(activity) + end + end +end diff --git a/spec/lib/diaspora/exporter_spec.rb b/spec/lib/diaspora/exporter_spec.rb index d12cfd2d0bd..7db86e82fd0 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 - - before do - @user1 = alice - - @user1.person.profile.first_name = "