From 6f812a5b8f20c04ba1a7aa01dcec13d2a07ac1af Mon Sep 17 00:00:00 2001 From: cmrd Senya Date: Mon, 11 Jun 2018 04:23:18 +0300 Subject: [PATCH] Add LinksController LinksController redirects requests for provided diaspora:// links to respective entities urls. --- app/controllers/links_controller.rb | 16 +++++ app/models/reference.rb | 3 +- app/services/diaspora_link_service.rb | 45 +++++++++++++ config/routes.rb | 2 + lib/diaspora/entity_finder.rb | 22 +++++++ spec/controllers/links_controller_spec.rb | 71 +++++++++++++++++++++ spec/services/diaspora_link_service_spec.rb | 44 +++++++++++++ 7 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 app/controllers/links_controller.rb create mode 100644 app/services/diaspora_link_service.rb create mode 100644 lib/diaspora/entity_finder.rb create mode 100644 spec/controllers/links_controller_spec.rb create mode 100644 spec/services/diaspora_link_service_spec.rb diff --git a/app/controllers/links_controller.rb b/app/controllers/links_controller.rb new file mode 100644 index 000000000..f28fbbc64 --- /dev/null +++ b/app/controllers/links_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class LinksController < ApplicationController + def resolve + entity = DiasporaLinkService.new(query).find_or_fetch_entity + raise ActiveRecord::RecordNotFound if entity.nil? + + redirect_to url_for(entity) + end + + private + + def query + @query ||= params.fetch(:q) + end +end diff --git a/app/models/reference.rb b/app/models/reference.rb index 3de1ff20a..45501edd4 100644 --- a/app/models/reference.rb +++ b/app/models/reference.rb @@ -22,8 +22,7 @@ class Reference < ApplicationRecord private def add_reference(author, type, guid) - class_name = DiasporaFederation::Entity.entity_class(type).to_s.rpartition("::").last - entity = Diaspora::Federation::Mappings.model_class_for(class_name).find_by(guid: guid) + entity = Diaspora::EntityFinder.new(type, guid).find references.find_or_create_by(target: entity) if entity&.diaspora_handle == author rescue => e # rubocop:disable Lint/RescueWithoutErrorClass logger.warn "ignoring invalid diaspora-url: diaspora://#{author}/#{type}/#{guid}: #{e.class}: #{e.message}" diff --git a/app/services/diaspora_link_service.rb b/app/services/diaspora_link_service.rb new file mode 100644 index 000000000..1923aa01d --- /dev/null +++ b/app/services/diaspora_link_service.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# Encapsulates logic of processing diaspora:// links +class DiasporaLinkService + attr_reader :type, :author, :guid + + def initialize(link) + @link = link.dup + parse + end + + def find_or_fetch_entity + entity_finder.find || fetch_entity + end + + private + + attr_accessor :link + + def fetch_entity + DiasporaFederation::Federation::Fetcher.fetch_public(author, type, guid) + entity_finder.find + rescue DiasporaFederation::Federation::Fetcher::NotFetchable + nil + end + + def entity_finder + @entity_finder ||= Diaspora::EntityFinder.new(type, guid) + end + + def normalize + link.gsub!(%r{^web\+diaspora://}, "diaspora://") || + link.gsub!(%r{^//}, "diaspora://") || + %r{^diaspora://}.match(link) || + self.link = "diaspora://#{link}" + end + + def parse + normalize + match = DiasporaFederation::Federation::DiasporaUrlParser::DIASPORA_URL_REGEX.match(link) + @author = match[1] + @type = match[2] + @guid = match[3] + end +end diff --git a/config/routes.rb b/config/routes.rb index 846921b8b..f7ad53bec 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,6 +74,8 @@ Rails.application.routes.draw do #Search get 'search' => "search#search" + get "link" => "links#resolve" + resources :conversations, except: %i(edit update destroy) do resources :messages, only: %i(create) delete 'visibility' => 'conversation_visibilities#destroy' diff --git a/lib/diaspora/entity_finder.rb b/lib/diaspora/entity_finder.rb new file mode 100644 index 000000000..0bf824382 --- /dev/null +++ b/lib/diaspora/entity_finder.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Diaspora + class EntityFinder + def initialize(type, guid) + @type = type + @guid = guid + end + + def class_name + @class_name ||= DiasporaFederation::Entity.entity_class(type).to_s.rpartition("::").last + end + + def find + Diaspora::Federation::Mappings.model_class_for(class_name).find_by(guid: guid) + end + + private + + attr_reader :type, :guid + end +end diff --git a/spec/controllers/links_controller_spec.rb b/spec/controllers/links_controller_spec.rb new file mode 100644 index 000000000..841b77cd4 --- /dev/null +++ b/spec/controllers/links_controller_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +describe LinksController, type: :controller do + describe "#resolve" do + context "with post" do + let(:post) { FactoryGirl.create(:status_message) } + let(:link_text) { "#{post.author.diaspora_handle}/post/#{post.guid}" } + subject { get :resolve, params: {q: link_query} } + + shared_examples "redirects to the post" do + it "redirects to the post" do + expect(subject).to redirect_to(post_url(post)) + end + end + + context "with stripped link text" do + let(:link_query) { link_text } + include_examples "redirects to the post" + end + + context "with link text starting with //" do + let(:link_query) { "//#{link_text}" } + include_examples "redirects to the post" + end + + context "with link text starting with diaspora://" do + let(:link_query) { "diaspora://#{link_text}" } + include_examples "redirects to the post" + end + + context "with link text starting with web+diaspora://" do + let(:link_query) { "web+diaspora://#{link_text}" } + include_examples "redirects to the post" + end + + context "when post is non-fetchable" do + let(:diaspora_id) { FactoryGirl.create(:person).diaspora_handle } + let(:guid) { "1234567890abcdef" } + let(:link_query) { "web+diaspora://#{diaspora_id}/post/#{guid}" } + + before do + expect(DiasporaFederation::Federation::Fetcher) + .to receive(:fetch_public) + .with(diaspora_id, "post", guid) + .and_raise(DiasporaFederation::Federation::Fetcher::NotFetchable) + end + + it "responds 404" do + expect { subject }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context "when user is non-fetchable" do + let(:diaspora_id) { "unknown@pod.tld" } + let(:guid) { "1234567890abcdef" } + let(:link_query) { "web+diaspora://#{diaspora_id}/post/#{guid}" } + + before do + expect(Person) + .to receive(:find_or_fetch_by_identifier) + .with(diaspora_id) + .and_return(nil) + end + + it "responds 404" do + expect { subject }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end + end +end diff --git a/spec/services/diaspora_link_service_spec.rb b/spec/services/diaspora_link_service_spec.rb new file mode 100644 index 000000000..bde411a27 --- /dev/null +++ b/spec/services/diaspora_link_service_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +describe DiasporaLinkService do + let(:service) { described_class.new(link) } + + describe "#find_or_fetch_entity" do + context "when entity is known" do + let(:post) { FactoryGirl.create(:status_message) } + let(:link) { "diaspora://#{post.author.diaspora_handle}/post/#{post.guid}" } + + it "returns the entity" do + expect(service.find_or_fetch_entity).to eq(post) + end + end + + context "when entity is unknown" do + let(:remote_person) { FactoryGirl.create(:person) } + let(:guid) { "1234567890abcdef" } + let(:link) { "diaspora://#{remote_person.diaspora_handle}/post/#{guid}" } + + it "fetches entity" do + expect(DiasporaFederation::Federation::Fetcher) + .to receive(:fetch_public) + .with(remote_person.diaspora_handle, "post", guid) { + FactoryGirl.create(:status_message, author: remote_person, guid: guid) + } + + entity = service.find_or_fetch_entity + expect(entity).to be_a(StatusMessage) + expect(entity.guid).to eq(guid) + expect(entity.author).to eq(remote_person) + end + + it "returns nil when entity is non fetchable" do + expect(DiasporaFederation::Federation::Fetcher) + .to receive(:fetch_public) + .with(remote_person.diaspora_handle, "post", guid) + .and_raise(DiasporaFederation::Federation::Fetcher::NotFetchable) + + expect(service.find_or_fetch_entity).to be_nil + end + end + end +end