diff options
Diffstat (limited to 'lib')
44 files changed, 1663 insertions, 77 deletions
diff --git a/lib/api/openid_connect/authorization_point/endpoint.rb b/lib/api/openid_connect/authorization_point/endpoint.rb index c88a43a09..ef12179ff 100644 --- a/lib/api/openid_connect/authorization_point/endpoint.rb +++ b/lib/api/openid_connect/authorization_point/endpoint.rb @@ -47,12 +47,18 @@ module Api end def build_scopes(req) - replace_profile_scope_with_specific_claims(req) @scopes = req.scope.map {|scope| scope.tap do |scope_name| - req.invalid_scope! "Unknown scope: #{scope_name}" unless auth_scopes.include? scope_name + req.invalid_scope! I18n.t("api.openid_connect.authorizations.new.unknown_scope", scope_name: scope_name) \ + unless auth_scopes.include?(scope_name) end } + + @scopes.push("public:read") unless @scopes.include?("public:read") + has_private_scope = @scopes.include?("private:read") || @scopes.include?("private:modify") + has_contacts_scope = @scopes.include? "contacts:read" + req.invalid_scope! I18n.t("api.openid_connect.authorizations.new.private_contacts_linkage_error") \ + if has_private_scope && !has_contacts_scope end def auth_scopes diff --git a/lib/api/openid_connect/authorization_point/endpoint_confirmation_point.rb b/lib/api/openid_connect/authorization_point/endpoint_confirmation_point.rb index 9c4eadeea..7cdd02143 100644 --- a/lib/api/openid_connect/authorization_point/endpoint_confirmation_point.rb +++ b/lib/api/openid_connect/authorization_point/endpoint_confirmation_point.rb @@ -21,10 +21,6 @@ module Api end end - def replace_profile_scope_with_specific_claims(_req) - # Empty - end - def build_from_request_object(_req) # Empty end diff --git a/lib/api/openid_connect/authorization_point/endpoint_start_point.rb b/lib/api/openid_connect/authorization_point/endpoint_start_point.rb index 7ce6b6b5d..f0dd6a6dc 100644 --- a/lib/api/openid_connect/authorization_point/endpoint_start_point.rb +++ b/lib/api/openid_connect/authorization_point/endpoint_start_point.rb @@ -16,12 +16,6 @@ module Api @response_type = req.response_type end - def replace_profile_scope_with_specific_claims(req) - profile_claims = %w(sub aud name nickname profile picture) - scopes_as_claims = req.scope.flat_map {|scope| scope == "profile" ? profile_claims : [scope] }.uniq - req.update_param("scope", scopes_as_claims) - end - private def build_request_object(req) diff --git a/lib/api/openid_connect/id_token.rb b/lib/api/openid_connect/id_token.rb index 44b957790..231507a29 100644 --- a/lib/api/openid_connect/id_token.rb +++ b/lib/api/openid_connect/id_token.rb @@ -53,7 +53,7 @@ module Api def claims sub = build_sub @claims ||= { - iss: Rails.application.routes.url_helpers.root_url, + iss: AppConfig.environment.url, sub: sub, aud: @authorization.o_auth_application.client_id, exp: @expires_at.to_i, diff --git a/lib/api/openid_connect/protected_resource_endpoint.rb b/lib/api/openid_connect/protected_resource_endpoint.rb index b75513183..23be2fc4c 100644 --- a/lib/api/openid_connect/protected_resource_endpoint.rb +++ b/lib/api/openid_connect/protected_resource_endpoint.rb @@ -29,11 +29,15 @@ module Api attr_reader :current_token def require_access_token(required_scopes) + raise Rack::OAuth2::Server::Resource::Bearer::Forbidden.new(:insufficient_scope) unless + access_token?(required_scopes) + end + + def access_token?(required_scopes) @current_token = request.env[Rack::OAuth2::Server::Resource::ACCESS_TOKEN] raise Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new("Unauthorized user") unless @current_token && @current_token.authorization - raise Rack::OAuth2::Server::Resource::Bearer::Forbidden.new(:insufficient_scope) unless - @current_token.authorization.try(:accessible?, required_scopes) + @current_token.authorization.try(:accessible?, required_scopes) end end end diff --git a/lib/api/openid_connect/token_endpoint.rb b/lib/api/openid_connect/token_endpoint.rb index b9b9d39e9..96fa44632 100644 --- a/lib/api/openid_connect/token_endpoint.rb +++ b/lib/api/openid_connect/token_endpoint.rb @@ -27,6 +27,7 @@ module Api auth = Api::OpenidConnect::Authorization.with_redirect_uri(req.redirect_uri).use_code(req.code) req.invalid_grant! if auth.blank? res.access_token = auth.create_access_token + res.access_token.refresh_token = auth.refresh_token if auth.accessible? "openid" id_token = auth.create_id_token res.id_token = id_token.to_jwt(access_token: res.access_token) diff --git a/lib/api/paging/index_paginator.rb b/lib/api/paging/index_paginator.rb new file mode 100644 index 000000000..8dc5108e0 --- /dev/null +++ b/lib/api/paging/index_paginator.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Api + module Paging + class IndexPaginator + def initialize(query_base, current_page, limit) + @query_base = query_base + @current_page = current_page.to_i + @limit = limit.to_i + end + + def page_data + @page_data ||= @query_base.paginate(page: @current_page, per_page: @limit) + @max_page = (@query_base.count * 1.0 / @limit * 1.0).ceil + @max_page = 1 if @max_page < 1 + @page_data + end + + def next_page(for_url=true) + page_data + return nil if for_url && @current_page == @max_page + + return "page=#{@current_page + 1}" if for_url + + IndexPaginator.new(@query_base, @current_page + 1, @limit) + end + + def previous_page(for_url=true) + page_data + return nil if for_url && @current_page == 1 + + return "page=#{@current_page - 1}" if for_url + + IndexPaginator.new(@query_base, @current_page - 1, @limit) + end + + def filter_parameters(parameters) + parameters.delete(:page) + end + end + end +end diff --git a/lib/api/paging/rest_paged_response_builder.rb b/lib/api/paging/rest_paged_response_builder.rb new file mode 100644 index 000000000..727a0180b --- /dev/null +++ b/lib/api/paging/rest_paged_response_builder.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Api + module Paging + class RestPagedResponseBuilder + def initialize(pager, request, allowed_params=nil) + @pager = pager + @base_url = request.original_url.split("?").first if request + @query_parameters = if allowed_params + allowed_params + elsif request&.query_parameters + request&.query_parameters + else + {} + end + end + + def response + { + links: navigation_builder, + data: @pager.page_data + } + end + + private + + def navigation_builder + previous_page = @pager.previous_page + links = {} + links[:previous] = link_builder(previous_page) if previous_page + + next_page = @pager.next_page + links[:next] = link_builder(next_page) if next_page + + links + end + + def link_builder(page_parameter) + "#{@base_url}?#{filtered_original_parameters}#{page_parameter}" + end + + def filtered_original_parameters + @pager.filter_parameters(@query_parameters) + return "" if @query_parameters.empty? + + @query_parameters.map {|k, v| "#{k}=#{v}" }.join("&") + "&" + end + end + end +end diff --git a/lib/api/paging/rest_paginator_builder.rb b/lib/api/paging/rest_paginator_builder.rb new file mode 100644 index 000000000..36614b26d --- /dev/null +++ b/lib/api/paging/rest_paginator_builder.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +module Api + module Paging + class RestPaginatorBuilder + MAX_LIMIT = 100 + DEFAULT_LIMIT = 15 + + def initialize(base_query, request, allow_default_page=true, default_limit=DEFAULT_LIMIT) + @base_query = base_query + @request = request + @allow_default_page = allow_default_page + @default_limit = if default_limit < MAX_LIMIT && default_limit > 0 + default_limit + else + DEFAULT_LIMIT + end + end + + def index_pager(params) + current_page = current_page_settings(params) + paged_response_builder(IndexPaginator.new(@base_query, current_page, limit_settings(params))) + end + + def time_pager(params, query_time_field="created_at", data_time_field=query_time_field) + is_descending, current_time = time_settings(params) + paged_response_builder( + TimePaginator.new( + query_base: @base_query, + query_time_field: query_time_field, + data_time_field: data_time_field, + current_time: current_time, + is_descending: is_descending, + limit: limit_settings(params) + ) + ) + end + + private + + def current_page_settings(params) + if params["page"] + requested_page = params["page"].to_i + requested_page = 1 if requested_page < 1 + requested_page + elsif @allow_default_page + 1 + else + raise ActionController::ParameterMissing + end + end + + def paged_response_builder(paginator) + Api::Paging::RestPagedResponseBuilder.new(paginator, @request) + end + + def time_settings(params) + time_params = params.permit("before", "after") + time_params["before"] = (Time.current + 1.year).iso8601 if time_params.empty? && @allow_default_page + + raise "Missing time parameters for query building" if time_params.empty? + + if time_params["before"] + is_descending = true + current_time = Time.iso8601(time_params["before"]) + else + is_descending = false + current_time = Time.iso8601(time_params["after"]) + end + [is_descending, current_time] + end + + def limit_settings(params) + requested_limit = params["per_page"].to_i if params["per_page"] + return @default_limit unless requested_limit + + requested_limit = [1, requested_limit].max + [requested_limit, MAX_LIMIT].min + end + end + end +end diff --git a/lib/api/paging/time_paginator.rb b/lib/api/paging/time_paginator.rb new file mode 100644 index 000000000..e9293aa32 --- /dev/null +++ b/lib/api/paging/time_paginator.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +module Api + module Paging + class TimePaginator + def initialize(opts={}) + @query_base = opts[:query_base] + @query_time_field = opts[:query_time_field] + @data_time_field = opts[:data_time_field] + @current_time = opts[:current_time] + @limit = opts[:limit] + @is_descending = opts[:is_descending] + direction = if @is_descending + "<" + else + ">" + end + @time_query_string = "#{@query_time_field} #{direction} ?" + @sort_string = if @is_descending + "#{@query_time_field} DESC" + else + "#{@query_time_field} ASC" + end + end + + def page_data + return @data if @data + + @data = @query_base.where([@time_query_string, @current_time.iso8601(3)]).limit(@limit).order(@sort_string) + time_data = @data.map {|d| d[@data_time_field] }.sort + @min_time = time_data.first + @max_time = time_data.last + 0.001.seconds if time_data.last + + @data + end + + def next_page(for_url=true) + page_data + return nil unless next_time + + return next_page_as_query_parameter if for_url + + TimePaginator.new( + query_base: @query_base, + query_time_field: @query_time_field, + query_data_field: @data_time_field, + current_time: next_time, + is_descending: @is_descending, + limit: @limit + ) + end + + def previous_page(for_url=true) + page_data + return nil unless previous_time + + return previous_page_as_query_parameter if for_url + + TimePaginator.new( + query_base: @query_base, + query_time_field: @query_time_field, + query_data_field: @data_time_field, + current_time: previous_time, + is_descending: !@is_descending, + limit: @limit + ) + end + + def filter_parameters(parameters) + parameters.delete(:before) + parameters.delete(:after) + end + + private + + def next_time + if @is_descending + @min_time + else + @max_time + end + end + + def previous_time + if @is_descending + @max_time + else + @min_time + end + end + + def next_page_as_query_parameter + if @is_descending + "before=#{next_time.iso8601(3)}" + else + "after=#{next_time.iso8601(3)}" + end + end + + def previous_page_as_query_parameter + if @is_descending + "after=#{previous_time.iso8601(3)}" + else + "before=#{previous_time.iso8601(3)}" + end + end + end + end +end diff --git a/lib/archive_importer.rb b/lib/archive_importer.rb new file mode 100644 index 000000000..4b307aa88 --- /dev/null +++ b/lib/archive_importer.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +class ArchiveImporter + include ArchiveHelper + include Diaspora::Logging + + attr_accessor :user + + def initialize(archive_hash) + @archive_hash = archive_hash + end + + def import + import_tag_followings + import_aspects + import_contacts + import_posts + import_relayables + import_subscriptions + import_others_relayables + import_blocks + end + + def create_user(attr) + allowed_keys = %w[ + email strip_exif show_community_spotlight_in_stream language disable_mail auto_follow_back + ] + data = convert_keys(archive_hash["user"], allowed_keys) + # setting getting_started to false as the user doesn't need to see the getting started wizard + data.merge!( + username: attr[:username], + password: attr[:password], + password_confirmation: attr[:password], + getting_started: false, + person: { + profile_attributes: profile_attributes + } + ) + self.user = User.build(data) + user.save! + end + + private + + attr_reader :archive_hash + + def profile_attributes + allowed_keys = %w[first_name last_name image_url bio gender location birthday searchable nsfw tag_string] + profile_data = archive_hash["user"]["profile"]["entity_data"] + convert_keys(profile_data, allowed_keys).tap do |attrs| + attrs[:public_details] = profile_data["public"] + end + end + + def import_contacts + import_collection(contacts, ContactImporter) + end + + def set_auto_follow_back_aspect + name = archive_hash["user"]["auto_follow_back_aspect"] + return if name.nil? + + aspect = user.aspects.find_by(name: name) + user.update(auto_follow_back_aspect: aspect) if aspect + end + + def import_aspects + contact_groups.each do |group| + begin + user.aspects.create!(group.slice("name")) + rescue ActiveRecord::RecordInvalid => e + logger.warn "#{self}: #{e}" + end + end + set_auto_follow_back_aspect + end + + def import_posts + import_collection(posts, PostImporter) + end + + def import_relayables + import_collection(relayables, OwnRelayableImporter) + end + + def import_others_relayables + import_collection(others_relayables, EntityImporter) + end + + def import_blocks + import_collection(blocks, BlockImporter) + end + + def import_collection(collection, importer_class) + collection.each do |object| + importer_class.new(object, user).import + end + end + + def import_tag_followings + archive_hash.fetch("user").fetch("followed_tags", []).each do |tag_name| + begin + tag = ActsAsTaggableOn::Tag.find_or_create_by(name: tag_name) + user.tag_followings.create!(tag: tag) + rescue ActiveRecord::RecordInvalid => e + logger.warn "#{self}: #{e}" + end + end + end + + def import_subscriptions + post_subscriptions.each do |post_guid| + post = Post.find_or_fetch_by(archive_author_diaspora_id, post_guid) + if post.nil? + logger.warn "#{self}: post with guid #{post_guid} not found, can't subscribe" + next + end + begin + user.participations.create!(target: post) + rescue ActiveRecord::RecordInvalid => e + logger.warn "#{self}: #{e}" + end + end + end + + def convert_keys(hash, allowed_keys) + hash + .slice(*allowed_keys) + .symbolize_keys + end + + def to_s + "#{self.class}:#{archive_author_diaspora_id}:#{user.diaspora_handle}" + end +end diff --git a/lib/archive_importer/archive_helper.rb b/lib/archive_importer/archive_helper.rb new file mode 100644 index 000000000..9813c8219 --- /dev/null +++ b/lib/archive_importer/archive_helper.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +class ArchiveImporter + module ArchiveHelper + def posts + @posts ||= archive_hash.fetch("user").fetch("posts", []) + end + + def relayables + @relayables ||= archive_hash.fetch("user").fetch("relayables", []) + end + + def others_relayables + @others_relayables ||= archive_hash.fetch("others_data", {}).fetch("relayables", []) + end + + def post_subscriptions + archive_hash.fetch("user").fetch("post_subscriptions", []) + end + + def contacts + archive_hash.fetch("user").fetch("contacts", []) + end + + def contact_groups + @contact_groups ||= archive_hash.fetch("user").fetch("contact_groups", []) + end + + def archive_author_diaspora_id + @archive_author_diaspora_id ||= archive_hash.fetch("user").fetch("profile").fetch("entity_data").fetch("author") + end + + def person + @person ||= Person.find_or_fetch_by_identifier(archive_author_diaspora_id) + end + + def blocks + @blocks ||= archive_hash.fetch("user").fetch("blocks", []) + end + + def private_key + OpenSSL::PKey::RSA.new(serialized_private_key) + end + + def serialized_private_key + archive_hash.fetch("user").fetch("private_key") + end + end +end diff --git a/lib/archive_importer/block_importer.rb b/lib/archive_importer/block_importer.rb new file mode 100644 index 000000000..882dd6986 --- /dev/null +++ b/lib/archive_importer/block_importer.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class ArchiveImporter + class BlockImporter + include Diaspora::Logging + attr_reader :json, :user + + def initialize(json, user) + @json = json + @user = user + end + + def import + p = Person.find_or_fetch_by_identifier(json) + migrant_person = handle_migrant_person(p) + user.blocks.create(person_id: migrant_person.id) + rescue ActiveRecord::RecordInvalid, + DiasporaFederation::Discovery::DiscoveryError => e + logger.warn "#{self}: #{e}" + end + + private + + def handle_migrant_person(person) + return person if person.account_migration.nil? + + person.account_migration.newest_person + end + end +end diff --git a/lib/archive_importer/contact_importer.rb b/lib/archive_importer/contact_importer.rb new file mode 100644 index 000000000..b64dfe7b7 --- /dev/null +++ b/lib/archive_importer/contact_importer.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class ArchiveImporter + class ContactImporter + include Diaspora::Logging + + def initialize(json, user) + @json = json + @user = user + end + + attr_reader :json + attr_reader :user + + def import + @imported_contact = create_contact + add_to_aspects + rescue ActiveRecord::RecordInvalid => e + logger.warn "#{self}: #{e}" + end + + private + + def add_to_aspects + json.fetch("contact_groups_membership", []).each do |group_name| + aspect = user.aspects.find_by(name: group_name) + if aspect.nil? + logger.warn "#{self}: aspect \"#{group_name}\" is missing" + next + end + @imported_contact.aspects << aspect + end + end + + def create_contact + person = Person.by_account_identifier(json.fetch("account_id")) + user.contacts.create!(person_id: person.id, sharing: json.fetch("sharing"), receiving: json.fetch("receiving")) + end + end +end diff --git a/lib/archive_importer/entity_importer.rb b/lib/archive_importer/entity_importer.rb new file mode 100644 index 000000000..22c6f49e4 --- /dev/null +++ b/lib/archive_importer/entity_importer.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class ArchiveImporter + class EntityImporter + include ArchiveValidator::EntitiesHelper + include Diaspora::Logging + + def initialize(json, user) + @json = json + @user = user + end + + def import + self.persisted_object = Diaspora::Federation::Receive.perform(entity, skip_relaying: true) + rescue DiasporaFederation::Entities::Signable::SignatureVerificationFailed, + DiasporaFederation::Discovery::InvalidDocument, + DiasporaFederation::Discovery::DiscoveryError, + DiasporaFederation::Federation::Fetcher::NotFetchable, + OwnRelayableImporter::NoParentError, + ActiveRecord::RecordInvalid => e + logger.warn "#{self}: #{e}" + self.persisted_object = nil + end + + attr_reader :json + attr_reader :user + attr_accessor :persisted_object + + def entity + entity_class.from_json(json) + end + end +end diff --git a/lib/archive_importer/own_entity_importer.rb b/lib/archive_importer/own_entity_importer.rb new file mode 100644 index 000000000..736115f63 --- /dev/null +++ b/lib/archive_importer/own_entity_importer.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class ArchiveImporter + class OwnEntityImporter < EntityImporter + def import + substitute_author + super + rescue Diaspora::Federation::InvalidAuthor + return if real_author == old_author_id + + logger.warn "#{self.class}: attempt to import an entity with guid \"#{guid}\" which belongs to #{real_author}" + end + + private + + def substitute_author + @old_author_id = entity_data["author"] + entity_data["author"] = user.diaspora_handle + end + + attr_reader :old_author_id + + def persisted_object + return @persisted_object if defined?(@persisted_object) + + @persisted_object = (instance if real_author == old_author_id) + end + + def real_author + instance.author.diaspora_handle + end + end +end diff --git a/lib/archive_importer/own_relayable_importer.rb b/lib/archive_importer/own_relayable_importer.rb new file mode 100644 index 000000000..c9ff6f82f --- /dev/null +++ b/lib/archive_importer/own_relayable_importer.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class ArchiveImporter + class OwnRelayableImporter < OwnEntityImporter + class NoParentError < RuntimeError; end + + def entity + fetch_parent(symbolized_entity_data) + entity_class.new(symbolized_entity_data) + end + + private + + def symbolized_entity_data + @symbolized_entity_data ||= entity_data.slice(*entity_class.class_props.keys.map(&:to_s)).symbolize_keys + end + + # Copied over from DiasporaFederation::Entities::Relayable + def fetch_parent(data) + type = data.fetch(:parent_type) { + break entity_class::PARENT_TYPE if entity_class.const_defined?(:PARENT_TYPE) + } + entity = Diaspora::Federation::Mappings.model_class_for(type).find_by(guid: data.fetch(:parent_guid)) + raise NoParentError if entity.nil? + + data[:parent] = Diaspora::Federation::Entities.related_entity(entity) + end + end +end diff --git a/lib/archive_importer/post_importer.rb b/lib/archive_importer/post_importer.rb new file mode 100644 index 000000000..79d592521 --- /dev/null +++ b/lib/archive_importer/post_importer.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class ArchiveImporter + class PostImporter < OwnEntityImporter + include Diaspora::Logging + + def import + super + import_subscriptions if persisted_object + end + + private + + def substitute_author + super + return unless entity_type == "status_message" + + entity_data["photos"].each do |photo| + photo["entity_data"]["author"] = user.diaspora_handle + end + end + + def import_subscriptions + json.fetch("subscribed_users_ids", []).each do |diaspora_id| + begin + person = Person.find_or_fetch_by_identifier(diaspora_id) + person = person.account_migration.newest_person unless person.account_migration.nil? + next if person.closed_account? + # TODO: unless person.nil? import subscription: subscription import is not supported yet + rescue DiasporaFederation::Discovery::DiscoveryError + end + end + end + end +end diff --git a/lib/archive_validator.rb b/lib/archive_validator.rb new file mode 100644 index 000000000..4dda3df2e --- /dev/null +++ b/lib/archive_validator.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require "yajl" + +# ArchiveValidator checks for errors in archive. It also find non-critical problems and fixes them in the archive hash +# so that the ArchiveImporter doesn't have to handle this issues. Non-critical problems found are indicated as warnings. +# Also it performs necessary data fetch where required. +class ArchiveValidator + include ArchiveImporter::ArchiveHelper + + def initialize(archive) + @archive = archive + end + + def validate + run_validators(CRITICAL_VALIDATORS, errors) + run_validators(NON_CRITICAL_VALIDATORS, warnings) + rescue KeyError => e + errors.push("Missing mandatory data: #{e}") + rescue Yajl::ParseError => e + errors.push("Bad JSON provided: #{e}") + end + + def errors + @errors ||= [] + end + + def warnings + @warnings ||= [] + end + + def archive_hash + @archive_hash ||= Yajl::Parser.new.parse(archive) + end + + CRITICAL_VALIDATORS = [ + SchemaValidator, + AuthorPrivateKeyValidator + ].freeze + + NON_CRITICAL_VALIDATORS = [ + ContactsValidator, + PostsValidator, + RelayablesValidator, + OthersRelayablesValidator + ].freeze + + private_constant :CRITICAL_VALIDATORS, :NON_CRITICAL_VALIDATORS + + private + + attr_reader :archive + + def run_validators(list, messages) + list.each do |validator_class| + validator = validator_class.new(archive_hash) + messages.concat(validator.messages) + end + end +end diff --git a/lib/archive_validator/author_private_key_validator.rb b/lib/archive_validator/author_private_key_validator.rb new file mode 100644 index 000000000..7a4c747e3 --- /dev/null +++ b/lib/archive_validator/author_private_key_validator.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class ArchiveValidator + class AuthorPrivateKeyValidator < BaseValidator + include Diaspora::Logging + + def validate + return if person.public_key.export == private_key.public_key.export + + messages.push("Private key in the archive doesn't match the known key of #{person.diaspora_handle}") + rescue DiasporaFederation::Discovery::DiscoveryError + logger.info "Archive author couldn't be fetched (old home pod is down?), will continue with data"\ + " import only" + end + end +end diff --git a/lib/archive_validator/base_validator.rb b/lib/archive_validator/base_validator.rb new file mode 100644 index 000000000..183cafa22 --- /dev/null +++ b/lib/archive_validator/base_validator.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class ArchiveValidator + class BaseValidator + include ArchiveImporter::ArchiveHelper + attr_reader :archive_hash + + def initialize(archive_hash) + @archive_hash = archive_hash + validate + end + + def messages + @messages ||= [] + end + + def valid? + @valid.nil? ? messages.empty? : @valid + end + + private + + attr_writer :valid + + def validate; end + end +end diff --git a/lib/archive_validator/collection_validator.rb b/lib/archive_validator/collection_validator.rb new file mode 100644 index 000000000..e03c2fe2f --- /dev/null +++ b/lib/archive_validator/collection_validator.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class ArchiveValidator + class CollectionValidator < BaseValidator + # Runs validations over each element in collection and removes every element + # which fails the validations. Any messages produced by the entity_validator are + # concatenated to the messages of the CollectionValidator instance. + def validate + collection.keep_if do |item| + subvalidator = entity_validator.new(archive_hash, item) + messages.concat(subvalidator.messages) + subvalidator.valid? + end + end + end +end diff --git a/lib/archive_validator/contact_validator.rb b/lib/archive_validator/contact_validator.rb new file mode 100644 index 000000000..4b79b0d30 --- /dev/null +++ b/lib/archive_validator/contact_validator.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class ArchiveValidator + class ContactValidator < BaseValidator + def initialize(archive_hash, contact) + @contact = contact + super(archive_hash) + end + + private + + def validate + handle_migrant_contact + self.valid = account_open? + rescue DiasporaFederation::Discovery::DiscoveryError => e + messages.push("#{self.class}: failed to fetch person #{diaspora_id}: #{e}") + self.valid = false + end + + attr_reader :contact + + def diaspora_id + contact.fetch("account_id") + end + + def handle_migrant_contact + return if person.account_migration.nil? + + contact["account_id"] = person.account_migration.newest_person.diaspora_handle + @person = nil + end + + def person + @person ||= Person.find_or_fetch_by_identifier(diaspora_id) + end + + def account_open? + !person.closed_account? || (messages.push("#{self.class}: account #{diaspora_id} is closed") && false) + end + end +end diff --git a/lib/archive_validator/contacts_validator.rb b/lib/archive_validator/contacts_validator.rb new file mode 100644 index 000000000..f7968012d --- /dev/null +++ b/lib/archive_validator/contacts_validator.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ArchiveValidator + class ContactsValidator < CollectionValidator + def collection + contacts + end + + def entity_validator + ContactValidator + end + end +end diff --git a/lib/archive_validator/entities_helper.rb b/lib/archive_validator/entities_helper.rb new file mode 100644 index 000000000..40c47a59a --- /dev/null +++ b/lib/archive_validator/entities_helper.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class ArchiveValidator + module EntitiesHelper + private + + def instance + @instance ||= model_class.find_by(guid: guid) + end + + def entity_type + json.fetch("entity_type") + end + + def entity_data + json.fetch("entity_data") + end + + def model_class + @model_class ||= Diaspora::Federation::Mappings.model_class_for(entity_type.camelize) + end + + def entity_class + DiasporaFederation::Entity.entity_class(entity_type) + end + + def guid + @guid ||= entity_data.fetch("guid") + end + + def to_s + "#{entity_class.class_name}:#{guid}" + end + end +end diff --git a/lib/archive_validator/others_relayables_validator.rb b/lib/archive_validator/others_relayables_validator.rb new file mode 100644 index 000000000..7352c9957 --- /dev/null +++ b/lib/archive_validator/others_relayables_validator.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ArchiveValidator + class OthersRelayablesValidator < CollectionValidator + def collection + others_relayables + end + + def entity_validator + RelayableValidator + end + end +end diff --git a/lib/archive_validator/own_relayable_validator.rb b/lib/archive_validator/own_relayable_validator.rb new file mode 100644 index 000000000..01b8dfdbe --- /dev/null +++ b/lib/archive_validator/own_relayable_validator.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class ArchiveValidator + class OwnRelayableValidator < RelayableValidator + private + + def post_find_by_guid(guid) + super || by_guid(Post, guid) + end + + def post_find_by_poll_guid(guid) + super || by_guid(Poll, guid)&.status_message + end + + def by_guid(klass, guid) + klass.find_or_fetch_by(archive_author_diaspora_id, guid) + end + end +end diff --git a/lib/archive_validator/post_validator.rb b/lib/archive_validator/post_validator.rb new file mode 100644 index 000000000..f1f2bf142 --- /dev/null +++ b/lib/archive_validator/post_validator.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class ArchiveValidator + class PostValidator < BaseValidator + include EntitiesHelper + + def initialize(archive_hash, post) + @json = post + super(archive_hash) + end + + private + + def validate + return unless entity_type == "reshare" && entity_data["root_guid"].nil? + + messages.push("reshare #{self} doesn't have a root, ignored") + end + + attr_reader :json + end +end diff --git a/lib/archive_validator/posts_validator.rb b/lib/archive_validator/posts_validator.rb new file mode 100644 index 000000000..fb2a1d744 --- /dev/null +++ b/lib/archive_validator/posts_validator.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ArchiveValidator + class PostsValidator < CollectionValidator + def collection + posts + end + + def entity_validator + PostValidator + end + end +end diff --git a/lib/archive_validator/relayable_validator.rb b/lib/archive_validator/relayable_validator.rb new file mode 100644 index 000000000..e026ff2a0 --- /dev/null +++ b/lib/archive_validator/relayable_validator.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +class ArchiveValidator + # We have to validate relayables before import because during import we'll not be able to fetch parent anymore + # because parent author will point to ourselves. + class RelayableValidator < BaseValidator + include EntitiesHelper + + def initialize(archive_hash, relayable) + @relayable = relayable + super(archive_hash) + end + + private + + def validate + self.valid = parent_present? + end + + attr_reader :relayable + alias json relayable + + # Common methods used by subclasses: + + def missing_parent_message + messages.push("Parent entity for #{self} is missing. Impossible to import, ignoring.") + end + + def parent_present? + parent.present? || (missing_parent_message && false) + end + + def parent + @parent ||= find_parent + end + + def find_parent + if entity_type == "poll_participation" + post_find_by_poll_guid(parent_guid) + else + post_find_by_guid(parent_guid) + end + end + + def parent_guid + entity_data.fetch("parent_guid") + end + + def post_find_by_guid(guid) + posts.find {|post| + post.fetch("entity_data").fetch("guid") == guid + } + end + + def post_find_by_poll_guid(guid) + posts.find {|post| + post.fetch("entity_data").fetch("poll", nil)&.fetch("entity_data", nil)&.fetch("guid", nil) == guid + } + end + end +end diff --git a/lib/archive_validator/relayables_validator.rb b/lib/archive_validator/relayables_validator.rb new file mode 100644 index 000000000..ecfdb6a04 --- /dev/null +++ b/lib/archive_validator/relayables_validator.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ArchiveValidator + class RelayablesValidator < CollectionValidator + def collection + relayables + end + + def entity_validator + OwnRelayableValidator + end + end +end diff --git a/lib/archive_validator/schema_validator.rb b/lib/archive_validator/schema_validator.rb new file mode 100644 index 000000000..1d4d1ab64 --- /dev/null +++ b/lib/archive_validator/schema_validator.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ArchiveValidator + class SchemaValidator < BaseValidator + JSON_SCHEMA = "lib/schemas/archive-format.json" + + def validate + return if JSON::Validator.validate(JSON_SCHEMA, archive_hash) + + messages.push("Archive schema validation failed") + end + end +end diff --git a/lib/configuration_methods.rb b/lib/configuration_methods.rb index 58a050f8c..22a5e1a16 100644 --- a/lib/configuration_methods.rb +++ b/lib/configuration_methods.rb @@ -50,6 +50,15 @@ module Configuration self["services.#{service}.authorized"] == true end + def local_posts_stream?(user) + return true if settings.enable_local_posts_stream == "admins" && + Role.is_admin?(user) + return true if settings.enable_local_posts_stream == "moderators" && + (Role.moderator?(user) || Role.is_admin?(user)) + + settings.enable_local_posts_stream == "everyone" + end + def secret_token if heroku? return ENV["SECRET_TOKEN"] if ENV["SECRET_TOKEN"] @@ -128,11 +137,6 @@ module Configuration end def bitcoin_donation_address - if AppConfig.settings.bitcoin_wallet_id.present? - warn "WARNING: bitcoin_wallet_id is now bitcoin_address. Change in diaspora.yml." - return AppConfig.settings.bitcoin_wallet_id - end - if AppConfig.settings.bitcoin_address.present? AppConfig.settings.bitcoin_address end diff --git a/lib/diaspora/federated/fetchable.rb b/lib/diaspora/federated/fetchable.rb new file mode 100644 index 000000000..b0ab24290 --- /dev/null +++ b/lib/diaspora/federated/fetchable.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Diaspora + module Federated + module Fetchable + extend ActiveSupport::Concern + + module ClassMethods + def find_or_fetch_by(diaspora_id, guid) + instance = find_by(guid: guid) + return instance if instance.present? + + DiasporaFederation::Federation::Fetcher.fetch_public(diaspora_id, to_s, guid) + find_by(guid: guid) + rescue DiasporaFederation::Federation::Fetcher::NotFetchable + nil + end + end + end + end +end diff --git a/lib/diaspora/federation/dispatcher/public.rb b/lib/diaspora/federation/dispatcher/public.rb index 51b7b5454..a82dfe09a 100644 --- a/lib/diaspora/federation/dispatcher/public.rb +++ b/lib/diaspora/federation/dispatcher/public.rb @@ -12,7 +12,7 @@ module Diaspora end def deliver_to_remote(people) - targets = target_urls(people) + additional_target_urls + targets = target_urls(people) return if targets.empty? @@ -25,11 +25,6 @@ module Diaspora active.map {|pod| pod.url_to("/receive/public") } end - def additional_target_urls - return [] unless AppConfig.relay.outbound.send? && object.instance_of?(StatusMessage) - [AppConfig.relay.outbound.url] - end - def deliver_to_hub logger.debug "deliver to pubsubhubbub sender: #{sender.diaspora_handle}" Workers::PublishToHub.perform_async(sender.atom_url) diff --git a/lib/diaspora/federation/receive.rb b/lib/diaspora/federation/receive.rb index edbdf2b9f..5f63b72af 100644 --- a/lib/diaspora/federation/receive.rb +++ b/lib/diaspora/federation/receive.rb @@ -5,8 +5,8 @@ module Diaspora module Receive extend Diaspora::Logging - def self.perform(entity) - public_send(Mappings.receiver_for(entity), entity) + def self.perform(entity, opts={}) + public_send(Mappings.receiver_for(entity), entity, opts) end def self.handle_closed_recipient(sender, recipient) @@ -26,9 +26,9 @@ module Diaspora logger.warn "ignoring error on receive AccountDeletion:#{entity.author}: #{e.class}: #{e.message}" end - def self.account_migration(entity) + def self.account_migration(entity, opts) old_person = author_of(entity) - profile = profile(entity.profile) + profile = profile(entity.profile, opts) return if AccountMigration.exists?(old_person: old_person, new_person: profile.person) AccountMigration.create!(old_person: old_person, new_person: profile.person).tap do |migration| @@ -42,8 +42,8 @@ module Diaspora nil end - def self.comment(entity) - receive_relayable(Comment, entity) do + def self.comment(entity, opts) + receive_relayable(Comment, entity, opts) do Comment.new( author: author_of(entity), guid: entity.guid, @@ -54,7 +54,7 @@ module Diaspora end end - def self.contact(entity) + def self.contact(entity, _opts) recipient = Person.find_by(diaspora_handle: entity.recipient).owner if entity.sharing Contact.create_or_update_sharing_contact(recipient, author_of(entity)) @@ -64,7 +64,7 @@ module Diaspora end end - def self.conversation(entity) + def self.conversation(entity, _opts) author = author_of(entity) ignore_existing_guid(Conversation, entity.guid, author) do Conversation.create!( @@ -78,8 +78,8 @@ module Diaspora end end - def self.like(entity) - receive_relayable(Like, entity) do + def self.like(entity, opts) + receive_relayable(Like, entity, opts) do Like.new( author: author_of(entity), guid: entity.guid, @@ -89,13 +89,13 @@ module Diaspora end end - def self.message(entity) + def self.message(entity, _opts) ignore_existing_guid(Message, entity.guid, author_of(entity)) do build_message(entity).tap(&:save!) end end - def self.participation(entity) + def self.participation(entity, _opts) author = author_of(entity) ignore_existing_guid(Participation, entity.guid, author) do Participation.create!( @@ -106,7 +106,7 @@ module Diaspora end end - def self.photo(entity) + def self.photo(entity, _opts) author = author_of(entity) persisted_photo = load_from_database(Photo, entity.guid, author) @@ -128,8 +128,8 @@ module Diaspora end end - def self.poll_participation(entity) - receive_relayable(PollParticipation, entity) do + def self.poll_participation(entity, opts) + receive_relayable(PollParticipation, entity, opts) do PollParticipation.new( author: author_of(entity), guid: entity.guid, @@ -139,7 +139,7 @@ module Diaspora end end - def self.profile(entity) + def self.profile(entity, _opts) author_of(entity).profile.tap do |profile| profile.update_attributes( first_name: entity.first_name, @@ -159,7 +159,7 @@ module Diaspora end end - def self.reshare(entity) + def self.reshare(entity, _opts) author = author_of(entity) ignore_existing_guid(Reshare, entity.guid, author) do Reshare.create!( @@ -192,7 +192,7 @@ module Diaspora end end - def self.status_message(entity) + def self.status_message(entity, _opts) # rubocop:disable Metrics/AbcSize try_load_existing_guid(StatusMessage, entity.guid, author_of(entity)) do StatusMessage.new( author: author_of(entity), @@ -271,8 +271,9 @@ module Diaspora end end - private_class_method def self.receive_relayable(klass, entity) - save_relayable(klass, entity) { yield }.tap {|relayable| relay_relayable(relayable) if relayable } + private_class_method def self.receive_relayable(klass, entity, opts) + save_relayable(klass, entity) { yield } + .tap {|relayable| relay_relayable(relayable) if relayable && !opts[:skip_relaying] } end private_class_method def self.save_relayable(klass, entity) diff --git a/lib/diaspora/markdownify/html.rb b/lib/diaspora/markdownify/html.rb index 19d4dd649..610c45223 100644 --- a/lib/diaspora/markdownify/html.rb +++ b/lib/diaspora/markdownify/html.rb @@ -6,7 +6,7 @@ module Diaspora include ActionView::Helpers::TextHelper def autolink link, type - Twitter::Autolink.auto_link_urls( + Twitter::TwitterText::Autolink.auto_link_urls( link, url_target: "_blank", link_attribute_block: lambda {|_, attr| attr[:rel] += " noopener noreferrer" } diff --git a/lib/diaspora/mentionable.rb b/lib/diaspora/mentionable.rb index 59f2da8f0..fe6396a9d 100644 --- a/lib/diaspora/mentionable.rb +++ b/lib/diaspora/mentionable.rb @@ -71,20 +71,6 @@ module Diaspora::Mentionable } end - # Regex to find mentions with new syntax, only used for backporting to old syntax - NEW_SYNTAX_REGEX = /@\{[^\} ]+\}/ - - # replaces new syntax with old syntax, to be compatible with old pods - # @deprecated remove when most of the posts can handle the new syntax - def self.backport_mention_syntax(text) - text.to_s.gsub(NEW_SYNTAX_REGEX) do |match_str| - _, diaspora_id = mention_attrs(match_str) - person = find_or_fetch_person_by_identifier(diaspora_id) - old_syntax = "@{#{person.name.delete('{}')}; #{diaspora_id}}" if person - old_syntax || match_str - end - end - private_class_method def self.find_or_fetch_person_by_identifier(identifier) Person.find_or_fetch_by_identifier(identifier) if Validation::Rule::DiasporaId.new.valid_value?(identifier) rescue DiasporaFederation::Discovery::DiscoveryError diff --git a/lib/diaspora/mentions_container.rb b/lib/diaspora/mentions_container.rb index b02dddb20..e3f3ab9a1 100644 --- a/lib/diaspora/mentions_container.rb +++ b/lib/diaspora/mentions_container.rb @@ -5,11 +5,6 @@ module Diaspora extend ActiveSupport::Concern included do - before_create do - # TODO: remove when most of the posts can handle the new syntax - self.text = Diaspora::Mentionable.backport_mention_syntax(text) if text && author.local? - end - after_create :create_mentions has_many :mentions, as: :mentions_container, dependent: :destroy end diff --git a/lib/diaspora/message_renderer.rb b/lib/diaspora/message_renderer.rb index d3f0e0232..4f12c85f8 100644 --- a/lib/diaspora/message_renderer.rb +++ b/lib/diaspora/message_renderer.rb @@ -248,7 +248,7 @@ module Diaspora # Extracts all the urls from the raw message and return them in the form of a string # Different URLs are seperated with a space def urls - @urls ||= Twitter::Extractor.extract_urls(plain_text_without_markdown).map {|url| + @urls ||= Twitter::TwitterText::Extractor.extract_urls(plain_text_without_markdown).map {|url| Addressable::URI.parse(url).normalize.to_s } end diff --git a/lib/schemas/api_v1.json b/lib/schemas/api_v1.json new file mode 100644 index 000000000..2d807610a --- /dev/null +++ b/lib/schemas/api_v1.json @@ -0,0 +1,486 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://diaspora.software/api/v1/schema.json", + "oneOf": [ + {"$ref": "https://diaspora.software/api/v1/schema.json#/definitions/aspects"}, + {"$ref": "https://diaspora.software/api/v1/schema.json#/definitions/aspect"}, + {"$ref": "https://diaspora.software/api/v1/schema.json#/definitions/comments"}, + {"$ref": "https://diaspora.software/api/v1/schema.json#/definitions/messages"}, + {"$ref": "https://diaspora.software/api/v1/schema.json#/definitions/users"}, + {"$ref": "https://diaspora.software/api/v1/schema.json#/definitions/conversations"}, + {"$ref": "https://diaspora.software/api/v1/schema.json#/definitions/conversation"}, + {"$ref": "https://diaspora.software/api/v1/schema.json#/definitions/authored_content_references"}, + {"$ref": "https://diaspora.software/api/v1/schema.json#/definitions/likes"}, + {"$ref": "https://diaspora.software/api/v1/schema.json#/definitions/notifications"}, + {"$ref": "https://diaspora.software/api/v1/schema.json#/definitions/notification"}, + {"$ref": "https://diaspora.software/api/v1/schema.json#/definitions/photos"}, + {"$ref": "https://diaspora.software/api/v1/schema.json#/definitions/photo"}, + {"$ref": "https://diaspora.software/api/v1/schema.json#/definitions/post"}, + {"$ref": "https://diaspora.software/api/v1/schema.json#/definitions/posts"}, + {"$ref": "https://diaspora.software/api/v1/schema.json#/definitions/tags"}, + {"$ref": "https://diaspora.software/api/v1/schema.json#/definitions/own_user"}, + {"$ref": "https://diaspora.software/api/v1/schema.json#/definitions/user"} + ], + + "definitions": { + "guid": { + "type": "string", + "minLength": 16, + "maxLength": 255 + }, + + "timestamp": { + "type": "string", + "format": "date-time" + }, + + "url": { + "type": "string", + "pattern": "^https?://" + }, + + "short_profile": { + "type": "object", + "properties": { + "guid": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/guid" }, + "diaspora_id": { "type": "string" }, + "name": { "type": "string" }, + "avatar": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/url" } + }, + "required": ["guid", "diaspora_id", "name"], + "additionalProperties": false + }, + + "aspects": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "order": { + "type": "integer" + } + }, + "required": ["id", "name", "order"], + "additionalProperties": false + } + }, + + "aspect": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "order": { + "type": "integer" + } + }, + "required": ["id", "name", "order"], + "additionalProperties": false + }, + + "comments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "guid": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/guid" }, + "created_at": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/timestamp" }, + "author": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/short_profile" }, + "body": { "type": "string" }, + "mentioned_people": { + "type": "array", + "items": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/short_profile" } + }, + "reported": { "type": "boolean" } + }, + "required": ["guid", "created_at", "author", "body", "reported"], + "additionalProperties": false + } + }, + + "messages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "guid": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/guid" }, + "created_at": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/timestamp" }, + "author": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/short_profile" }, + "body": { "type": "string" } + }, + "required": ["guid", "created_at", "author", "body"], + "additionalProperties": false + } + }, + + "users": { + "type": "array", + "items": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/short_profile" } + }, + + "conversations": { + "type": "array", + "items": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/conversation" } + }, + + "conversation": { + "type": "object", + "properties": { + "guid": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/guid" }, + "subject": { "type": "string" }, + "created_at": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/timestamp" }, + "read": { "type": "boolean" }, + "participants": { + "type": "array", + "items": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/short_profile" } + } + }, + "required": ["subject", "created_at", "read", "participants"], + "additionalProperties": false + }, + + "authored_content_reference": { + "type": "object", + "created_at": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/timestamp" }, + "properties": { + "guid": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/guid" }, + "author": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/short_profile" } + }, + "required": ["guid", "created_at", "author"], + "additionalProperties": false + }, + + "authored_content_references": { + "type": "array", + "items": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/authored_content_reference" } + }, + + "likes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "guid": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/guid" }, + "author": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/short_profile" } + }, + "required": ["guid", "author"], + "additionalProperties": false + } + }, + + "notifications": { + "type": "array", + "items": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/notification" } + }, + + "notification": { + "type": "object", + "properties": { + "guid": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/guid" }, + "type": { + "enum": [ + "also_commented", + "comment_on_post", + "liked", + "mentioned", + "mentioned_in_comment", + "reshared", + "started_sharing", + "contacts_birthday" + ] + }, + "read": { "type": "boolean" }, + "created_at": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/timestamp" }, + "target": { + "type": "object", + "properties": { + "guid": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/guid" }, + "author": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/short_profile" } + }, + "required": ["guid"] + }, + "event_creators": { + "type": "array", + "items": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/short_profile" } + } + }, + "required": ["guid", "type", "read", "created_at"], + "additionalProperties": false + }, + + "photos": { + "type": "array", + "items": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/photo"} + }, + + "photo_sizes": { + "type": "object", + "properties": { + "raw": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/url" }, + "large": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/url" }, + "medium": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/url" }, + "small": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/url" } + }, + "required": ["raw", "large", "medium", "small"], + "additionalProperties": false + }, + + "photo_dimensions": { + "type": "object", + "properties": { + "width": { "type": ["integer", "null"] }, + "height": { "type": ["integer", "null"] } + }, + "required": ["width", "height"] + }, + + "photo": { + "type": "object", + "properties": { + "guid": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/guid" }, + "post": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/guid" }, + "created_at": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/timestamp" }, + "dimensions": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/photo_dimensions" }, + "sizes": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/photo_sizes" } + }, + "required": ["guid", "created_at", "dimensions", "sizes"], + "additionalProperties": false + }, + + "post_common": { + "type": "object", + "properties": { + "guid": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/guid" }, + "created_at": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/timestamp" }, + "title": { "type": "string" }, + "body": { "type": "string" }, + "provider_display_name": { "type": "string" }, + "public": { "type": "boolean" }, + "nsfw": { "type": "boolean" }, + "author": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/short_profile" }, + "interaction_counters": { + "type": "object", + "properties": { + "comments": { "type": "integer" }, + "likes": { "type": "integer" }, + "reshares":{ "type": "integer" } + }, + "required": ["comments", "likes", "reshares"], + "additionalProperties": false + }, + "own_interaction_state": { + "type": "object", + "properties": { + "liked": { "type": "boolean" }, + "reshared": { "type": "boolean" }, + "subscribed": { "type": "boolean" }, + "reported": { "type": "boolean" } + }, + "required": ["liked", "reshared", "subscribed", "reported"], + "additionalProperties": false + }, + "mentioned_people": { + "type": "array", + "items": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/short_profile" } + }, + "photos": { + "type": "array", + "items": { + "type": "object", + "properties": { + "dimensions": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/photo_dimensions" }, + "sizes": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/photo_sizes" } + }, + "required": ["dimensions", "sizes"] + } + }, + "poll": { + "type": "object", + "properties": { + "guid": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/guid" }, + "participation_count": { "type": "integer" }, + "already_participated": { "type": "boolean" }, + "question": { "type": "string" }, + "poll_answers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "answer": { "type": "string" }, + "vote_count": { "type": "integer" }, + "own_answer": { "type": "boolean" } + }, + "required": ["id", "answer", "vote_count"], + "additionalProperties": false + } + } + }, + "required": ["guid", "participation_count", "already_participated", "question", "poll_answers"], + "additionalProperties": false + }, + "location": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "lat": { "type": "number" }, + "lng": { "type": "number" } + }, + "required": ["address", "lat", "lng"], + "additionalProperties": false + }, + "open_graph_object": { + "type": "object", + "properties": { + "url": { "type": "string" }, + "type": { "type": "string" }, + "title": { "type": "string" }, + "image": { "type": "string" }, + "description": { "type": "string" }, + "video_url": { "type": "string" } + }, + "required": ["url", "type", "title", "image"], + "additionalProperties": false + }, + "oembed": { + "type": "object", + "description": "An oEmbed response according to 2.3.4 of the oEmbed spec.", + "properties": { + "trusted_endpoint_url": { "type": "string" } + }, + "required": ["trusted_endpoint_url"], + "additionalProperties": true + } + }, + "required": ["guid", "created_at", "title", "body", "public", "nsfw", "author", "interaction_counters", "own_interaction_state", "mentioned_people", "photos"] + }, + + "post": { + "anyOf": [ + { + "allOf": [ + { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/post_common" }, + { + "properties": { + "post_type": { "type": "string", "format": "^StatusMessage$" } + }, + "required": ["post_type"] + } + ] + }, + { + "allOf": [ + { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/post_common" }, + { + "properties": { + "post_type": { "type": "string", "format": "^Reshare$" }, + "root": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/authored_content_reference" } + }, + "required": ["post_type", "root"] + } + ] + } + ] + }, + + "reshares": { + "type": "array", + "items": { + "type": "object", + "properties": { + "guid": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/guid" }, + "created_at": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/timestamp" }, + "author": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/short_profile" } + }, + "required": ["guid", "created_at", "author"], + "additionalProperties": false + } + }, + + "posts": { + "type": "array", + "items": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/post" } + }, + + "tags": { + "type": "array", + "items": { "type": "string", "pattern": "^[^#]" } + }, + + "birthday": { "type": "string", "pattern": "^\\d\\d\\d\\d-\\d\\d-\\d\\d$" }, + + "user_data": { + "type": "object", + "properties": { + "guid": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/guid" }, + "diaspora_id": { "type": "string" }, + "name": { "type": "string" }, + "birthday": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/birthday" }, + "gender": { "type": "string" }, + "location": { "type": "string" }, + "bio": { "type": "string" }, + "avatar": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/photo_sizes" }, + "tags": { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/tags" } + }, + "required": ["guid", "diaspora_id", "tags"] + }, + + "own_user": { + "allOf": [ + { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/user_data" }, + { + "type": "object", + "properties": { + "searchable": { "type": "boolean" }, + "show_profile_info": { "type": "boolean" } + }, + "required": ["searchable", "show_profile_info"] + } + ] + }, + + "user": { + "allOf": [ + { "$ref": "https://diaspora.software/api/v1/schema.json#/definitions/user_data" }, + { + "type": "object", + "properties": { + "blocked": { "type": "boolean" }, + "relationship": { + "type": "object", + "properties": { + "receiving": { "type": "boolean" }, + "sharing": { "type": "boolean" } + }, + "required": ["receiving", "sharing"], + "additionalProperties": false + }, + "aspects": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" } + }, + "required": ["id", "name"], + "additionalProperties": false + } + } + }, + "required": ["blocked", "relationship", "aspects"] + } + ] + } + } +} diff --git a/lib/stream.rb b/lib/stream.rb index 27cb67e25..51c7512e6 100644 --- a/lib/stream.rb +++ b/lib/stream.rb @@ -1,14 +1,15 @@ # frozen_string_literal: true module Stream - require 'stream/activity' - require 'stream/aspect' - require 'stream/comments' - require 'stream/followed_tag' - require 'stream/likes' - require 'stream/mention' - require 'stream/multi' - require 'stream/person' - require 'stream/public' - require 'stream/tag' + require "stream/activity" + require "stream/aspect" + require "stream/comments" + require "stream/followed_tag" + require "stream/likes" + require "stream/mention" + require "stream/multi" + require "stream/person" + require "stream/public" + require "stream/local_public" + require "stream/tag" end diff --git a/lib/stream/local_public.rb b/lib/stream/local_public.rb new file mode 100644 index 000000000..ecbb02133 --- /dev/null +++ b/lib/stream/local_public.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# rubocop:disable Style/ClassAndModuleChildren +class Stream::LocalPublic < Stream::Base + def link(opts={}) + Rails.application.routes.url_helpers.local_public_stream_path(opts) + end + + def title + I18n.t("streams.local_public.title") + end + + # @return [ActiveRecord::Association<Post>] AR association of posts + def posts + @posts ||= Post.all_local_public + end + + # Override base class method + def aspects + ["public"] + end +end +# rubocop:enable Style/ClassAndModuleChildren diff --git a/lib/tasks/accounts.rake b/lib/tasks/accounts.rake new file mode 100644 index 000000000..c33c528fd --- /dev/null +++ b/lib/tasks/accounts.rake @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +namespace :accounts do + desc "Perform migration" + task :migration, %i[archive_path new_user_name] => :environment do |_t, args| + puts "Account migration is requested" + args = %i[archive_path new_user_name].map {|name| [name, args[name]] }.to_h + process_arguments(args) + + begin + service = MigrationService.new(args[:archive_path], args[:new_user_name]) + service.validate + puts "Warnings:\n#{service.warnings.join("\n")}\n-----" if service.warnings.any? + if service.only_import? + puts "Warning: Archive owner is not fetchable. Proceeding with data import, but account migration record "\ + "won't be created" + end + print "Do you really want to execute the archive import? Note: this is irreversible! [y/N]: " + next unless $stdin.gets.strip.casecmp?("y") + + start_time = Time.now.getlocal + service.perform! + puts service.only_import? ? "Data import complete!" : "Data import and migration complete!" + puts "Migration took #{Time.now.getlocal - start_time} seconds" + rescue MigrationService::ArchiveValidationFailed => exception + puts "Errors in the archive found:\n#{exception.message}\n-----" + rescue MigrationService::MigrationAlreadyExists + puts "Migration record already exists for the user, can't continue" + end + end + + def process_arguments(args) + if args[:archive_path].nil? + print "Enter the archive path: " + args[:archive_path] = $stdin.gets.strip + end + if args[:new_user_name].nil? + print "Enter the new user name: " + args[:new_user_name] = $stdin.gets.strip + end + puts "Archive path: #{args[:archive_path]}" + puts "New username: #{args[:new_user_name]}" + end +end |