diff --git a/app/controllers/social_relay_controller.rb b/app/controllers/social_relay_controller.rb new file mode 100644 index 000000000..28cf5065a --- /dev/null +++ b/app/controllers/social_relay_controller.rb @@ -0,0 +1,7 @@ +class SocialRelayController < ApplicationController + respond_to :json + + def well_known + render json: SocialRelayPresenter.new + end +end diff --git a/app/presenters/social_relay_presenter.rb b/app/presenters/social_relay_presenter.rb new file mode 100644 index 000000000..eab42a4f3 --- /dev/null +++ b/app/presenters/social_relay_presenter.rb @@ -0,0 +1,24 @@ +class SocialRelayPresenter + def as_json(*) + { + "subscribe" => AppConfig.relay.inbound.subscribe, + "scope" => AppConfig.relay.inbound.scope, + "tags" => tags + } + end + + def tags + return [] unless AppConfig.relay.inbound.scope == "tags" + tags = AppConfig.relay.inbound.pod_tags.present? ? AppConfig.relay.inbound.pod_tags.split(",") : [] + add_user_tags(tags) + tags.uniq + end + + def add_user_tags(tags) + if AppConfig.relay.inbound.include_user_tags? + user_ids = User.halfyear_actives.pluck(:id) + tag_ids = TagFollowing.where(user: user_ids).select(:tag_id).distinct.pluck(:tag_id) + tags.concat ActsAsTaggableOn::Tag.where(id: tag_ids).pluck(:name) + end + end +end diff --git a/app/workers/deferred_dispatch.rb b/app/workers/deferred_dispatch.rb index 46fa894c3..fb5d4a311 100644 --- a/app/workers/deferred_dispatch.rb +++ b/app/workers/deferred_dispatch.rb @@ -10,14 +10,38 @@ module Workers user = User.find(user_id) object = object_class_name.constantize.find(object_id) opts = HashWithIndifferentAccess.new(opts) - opts[:services] = user.services.where(:type => opts.delete(:service_types)) - - if opts[:additional_subscribers].present? - opts[:additional_subscribers] = Person.where(:id => opts[:additional_subscribers]) - end + opts[:services] = user.services.where(type: opts.delete(:service_types)) + add_additional_subscribers(object, object_class_name, opts) Postzord::Dispatcher.build(user, object, opts).post rescue ActiveRecord::RecordNotFound # The target got deleted before the job was run end + + def add_additional_subscribers(object, object_class_name, opts) + if AppConfig.relay.outbound.send? && + object_class_name == "StatusMessage" && + object.respond_to?(:public?) && object.public? + handle_relay(opts) + end + + if opts[:additional_subscribers].present? + opts[:additional_subscribers] = Person.where(id: opts[:additional_subscribers]) + end + end + + def handle_relay(opts) + relay_person = Person.find_by diaspora_handle: AppConfig.relay.outbound.handle.to_s + if relay_person + add_person_to_subscribers(opts, relay_person) + else + # Skip this message for relay and just queue a webfinger fetch for the relay handle + Workers::FetchWebfinger.perform_async(AppConfig.relay.outbound.handle) + end + end + + def add_person_to_subscribers(opts, person) + opts[:additional_subscribers] ||= [] + opts[:additional_subscribers] << person.id + end end end diff --git a/config/defaults.yml b/config/defaults.yml index 6f13c8409..bf1e9de22 100644 --- a/config/defaults.yml +++ b/config/defaults.yml @@ -193,6 +193,15 @@ defaults: admins: account: podmin_email: + relay: + outbound: + send: false + handle: 'relay@relay.iliketoast.net' + inbound: + subscribe: false + scope: tags + include_user_tags: false + pod_tags: # List valid environment variables redistogo_url: diff --git a/config/diaspora.yml.example b/config/diaspora.yml.example index 9ddbb5327..c3bc0e0a3 100644 --- a/config/diaspora.yml.example +++ b/config/diaspora.yml.example @@ -679,6 +679,38 @@ configuration: ## Section ## E-mail address to contact the administrator. #podmin_email: 'podmin@example.org' + ## Settings related to relays + relay: ## Section + + ## Relays are applications that exist to push public posts around to + ## pods which want to subscribe to them but would not otherwise + ## receive them due to not having direct contact with the remote pods. + ## + ## See more regarding relays: https://wiki.diasporafoundation.org/Relay_servers_for_public_posts + + outbound: ## Section + ## Enable this setting to send out public posts from this pod to a relay + #send: false + ## Change default remote relay handle used for sending out here + #handle: 'relay@relay.iliketoast.net' + + inbound: ## Section + ## Enable this to receive public posts from relays + #subscribe: false + + ## Scope is either 'all' or 'tags' (default). + ## - 'all', means this pod wants to receive all public posts from a relay + ## - 'tags', means this pod wants only posts tagged with certain tags + #scope: tags + + ## If scope is 'tags', should we include tags that users on this pod follow? + ## These are added in addition to 'pod_tags', if set. + #include_user_tags: false + + ## If scope is 'tags', a comma separated list of tags here can be set. + ## For example "linux,diaspora", to receive posts related to these tags + #pod_tags: + ## Here you can override settings defined above if you need ## to have them different in different environments. production: ## Section diff --git a/config/routes.rb b/config/routes.rb index c0fd79d2b..e1b9aed21 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -244,6 +244,9 @@ Diaspora::Application.routes.draw do get 'terms' => 'terms#index' end + # Relay + get ".well-known/x-social-relay" => "social_relay#well_known" + # Startpage root :to => 'home#show' end diff --git a/spec/controllers/social_relay_controller_spec.rb b/spec/controllers/social_relay_controller_spec.rb new file mode 100644 index 000000000..9b2f10be7 --- /dev/null +++ b/spec/controllers/social_relay_controller_spec.rb @@ -0,0 +1,16 @@ +require "spec_helper" + +describe SocialRelayController, type: :controller do + describe "#well_known" do + it "responds to format json" do + get :well_known, format: "json" + expect(response.code).to eq("200") + end + + it "contains json" do + get :well_known, format: "json" + json = JSON.parse(response.body) + expect(json["scope"]).to be_present + end + end +end diff --git a/spec/presenters/social_relay_presenter_spec.rb b/spec/presenters/social_relay_presenter_spec.rb new file mode 100644 index 000000000..27d132994 --- /dev/null +++ b/spec/presenters/social_relay_presenter_spec.rb @@ -0,0 +1,122 @@ +require "spec_helper" + +describe SocialRelayPresenter do + before do + @presenter = SocialRelayPresenter.new + end + + describe "#as_json" do + it "works" do + expect(@presenter.as_json).to be_present + expect(@presenter.as_json).to be_a Hash + end + end + + describe "#social relay well-known contents" do + describe "defaults" do + it "provides valid detault data" do + expect(@presenter.as_json).to eq( + "subscribe" => false, + "scope" => "tags", + "tags" => [] + ) + end + end + + describe "pod tags" do + before do + AppConfig.relay.inbound.pod_tags = "foo,bar" + AppConfig.relay.inbound.include_user_tags = false + end + + it "provides pod tags" do + expect(@presenter.as_json).to eq( + "subscribe" => false, + "scope" => "tags", + "tags" => ["foo", "bar"] + ) + end + end + + describe "user tags" do + before do + AppConfig.relay.inbound.pod_tags = "" + AppConfig.relay.inbound.include_user_tags = true + ceetag = FactoryGirl.create(:tag, name: "cee") + lootag = FactoryGirl.create(:tag, name: "loo") + FactoryGirl.create(:tag_following, user: alice, tag: ceetag) + FactoryGirl.create(:tag_following, user: alice, tag: lootag) + alice.last_seen = Time.now - 2.month + alice.save + end + + it "provides user tags" do + expect(@presenter.as_json).to eq( + "subscribe" => false, + "scope" => "tags", + "tags" => ["cee", "loo"] + ) + end + end + + describe "pod tags combined with user tags" do + before do + AppConfig.relay.inbound.pod_tags = "foo,bar" + AppConfig.relay.inbound.include_user_tags = true + ceetag = FactoryGirl.create(:tag, name: "cee") + lootag = FactoryGirl.create(:tag, name: "loo") + FactoryGirl.create(:tag_following, user: alice, tag: ceetag) + FactoryGirl.create(:tag_following, user: alice, tag: lootag) + alice.last_seen = Time.now - 2.month + alice.save + end + + it "provides combined pod and user tags" do + expect(@presenter.as_json).to eq( + "subscribe" => false, + "scope" => "tags", + "tags" => ["foo", "bar", "cee", "loo"] + ) + end + end + + describe "user tags for inactive user" do + before do + AppConfig.relay.inbound.pod_tags = "" + AppConfig.relay.inbound.include_user_tags = true + ceetag = FactoryGirl.create(:tag, name: "cee") + lootag = FactoryGirl.create(:tag, name: "loo") + FactoryGirl.create(:tag_following, user: alice, tag: ceetag) + FactoryGirl.create(:tag_following, user: alice, tag: lootag) + alice.last_seen = Time.now - 8.month + alice.save + end + + it "ignores user tags" do + expect(@presenter.as_json).to eq( + "subscribe" => false, + "scope" => "tags", + "tags" => [] + ) + end + end + + describe "when scope is all" do + before do + AppConfig.relay.inbound.scope = "all" + AppConfig.relay.inbound.pod_tags = "foo,bar" + AppConfig.relay.inbound.include_user_tags = true + ceetag = FactoryGirl.create(:tag, name: "cee") + FactoryGirl.create(:tag_following, user: alice, tag: ceetag) + end + + it "provides empty tags list" do + expect(@presenter.as_json).to eq( + "subscribe" => false, + "scope" => "all", + "tags" => [] + ) + end + end + end +end diff --git a/spec/workers/deferred_dispatch_spec.rb b/spec/workers/deferred_dispatch_spec.rb index f0a20d768..0d4064bbb 100644 --- a/spec/workers/deferred_dispatch_spec.rb +++ b/spec/workers/deferred_dispatch_spec.rb @@ -3,7 +3,36 @@ require 'spec_helper' describe Workers::DeferredDispatch do it "handles non existing records gracefully" do expect { - described_class.new.perform(alice.id, 'Comment', 0, {}) + described_class.new.perform(alice.id, "Comment", 0, {}) }.to_not raise_error end + + describe "#social relay functionality" do + let(:message) { FactoryGirl.create(:status_message, author: alice.person, public: true) } + before do + AppConfig.relay.outbound.send = true + end + + it "triggers fetch of relay handle" do + allow(Person).to receive(:find_by).and_return(nil) + + expect(Workers::FetchWebfinger).to receive(:perform_async) + + described_class.new.perform(alice.id, "StatusMessage", message.id, {}) + end + + it "triggers post to relay" do + relay_person = FactoryGirl.create(:person, diaspora_handle: AppConfig.relay.outbound.handle) + opts = {"additional_subscribers" => [relay_person], "services" => []} + allow(Person).to receive(:find_by).and_return(relay_person) + postzord = double + allow(Postzord::Dispatcher).to receive(:build).with(any_args).and_return(postzord) + allow(postzord).to receive(:post) + allow(Person).to receive(:where).and_return([relay_person]) + + expect(Postzord::Dispatcher).to receive(:build).with(alice, message, opts) + + described_class.new.perform(alice.id, "StatusMessage", message.id, {}) + end + end end