Merge branch 'stable' into develop
This commit is contained in:
commit
e95c742aa1
10 changed files with 185 additions and 41 deletions
6
Gemfile
6
Gemfile
|
|
@ -12,7 +12,7 @@ gem "unicorn", "5.0.1", require: false
|
||||||
|
|
||||||
# Federation
|
# Federation
|
||||||
|
|
||||||
gem "diaspora_federation-rails", "0.0.12"
|
gem "diaspora_federation-rails", "0.0.13"
|
||||||
|
|
||||||
# API and JSON
|
# API and JSON
|
||||||
|
|
||||||
|
|
@ -168,7 +168,7 @@ gem "addressable", "2.3.8", require: "addressable/uri"
|
||||||
gem "faraday", "0.9.2"
|
gem "faraday", "0.9.2"
|
||||||
gem "faraday_middleware", "0.10.0"
|
gem "faraday_middleware", "0.10.0"
|
||||||
gem "faraday-cookie_jar", "0.0.6"
|
gem "faraday-cookie_jar", "0.0.6"
|
||||||
gem "typhoeus", "0.8.0"
|
gem "typhoeus", "1.0.1"
|
||||||
|
|
||||||
# Views
|
# Views
|
||||||
|
|
||||||
|
|
@ -291,7 +291,7 @@ group :test do
|
||||||
gem "webmock", "1.22.6", require: false
|
gem "webmock", "1.22.6", require: false
|
||||||
gem "shoulda-matchers", "3.1.1"
|
gem "shoulda-matchers", "3.1.1"
|
||||||
|
|
||||||
gem "diaspora_federation-test", "0.0.12"
|
gem "diaspora_federation-test", "0.0.13"
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
|
|
|
||||||
22
Gemfile.lock
22
Gemfile.lock
|
|
@ -180,17 +180,17 @@ GEM
|
||||||
eventmachine (~> 1.0.8)
|
eventmachine (~> 1.0.8)
|
||||||
http_parser.rb (~> 0.6)
|
http_parser.rb (~> 0.6)
|
||||||
nokogiri (~> 1.6)
|
nokogiri (~> 1.6)
|
||||||
diaspora_federation (0.0.12)
|
diaspora_federation (0.0.13)
|
||||||
faraday (~> 0.9.0)
|
faraday (~> 0.9.0)
|
||||||
faraday_middleware (~> 0.10.0)
|
faraday_middleware (~> 0.10.0)
|
||||||
nokogiri (~> 1.6, >= 1.6.7.1)
|
nokogiri (~> 1.6, >= 1.6.7.2)
|
||||||
typhoeus (~> 0.7)
|
typhoeus (~> 1.0)
|
||||||
valid (~> 1.0)
|
valid (~> 1.0)
|
||||||
diaspora_federation-rails (0.0.12)
|
diaspora_federation-rails (0.0.13)
|
||||||
diaspora_federation (= 0.0.12)
|
diaspora_federation (= 0.0.13)
|
||||||
rails (~> 4.2)
|
rails (~> 4.2)
|
||||||
diaspora_federation-test (0.0.12)
|
diaspora_federation-test (0.0.13)
|
||||||
diaspora_federation (= 0.0.12)
|
diaspora_federation (= 0.0.13)
|
||||||
factory_girl (~> 4.5, >= 4.5.0)
|
factory_girl (~> 4.5, >= 4.5.0)
|
||||||
diff-lcs (1.2.5)
|
diff-lcs (1.2.5)
|
||||||
docile (1.1.5)
|
docile (1.1.5)
|
||||||
|
|
@ -846,7 +846,7 @@ GEM
|
||||||
simple_oauth (~> 0.3.0)
|
simple_oauth (~> 0.3.0)
|
||||||
twitter-text (1.13.3)
|
twitter-text (1.13.3)
|
||||||
unf (~> 0.1.0)
|
unf (~> 0.1.0)
|
||||||
typhoeus (0.8.0)
|
typhoeus (1.0.1)
|
||||||
ethon (>= 0.8.0)
|
ethon (>= 0.8.0)
|
||||||
tzinfo (1.2.2)
|
tzinfo (1.2.2)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
|
|
@ -916,8 +916,8 @@ DEPENDENCIES
|
||||||
devise-token_authenticatable (~> 0.4.0)
|
devise-token_authenticatable (~> 0.4.0)
|
||||||
devise_lastseenable (= 0.0.6)
|
devise_lastseenable (= 0.0.6)
|
||||||
diaspora-vines (~> 0.2.0.develop)
|
diaspora-vines (~> 0.2.0.develop)
|
||||||
diaspora_federation-rails (= 0.0.12)
|
diaspora_federation-rails (= 0.0.13)
|
||||||
diaspora_federation-test (= 0.0.12)
|
diaspora_federation-test (= 0.0.13)
|
||||||
entypo-rails (= 3.0.0.pre.rc2)
|
entypo-rails (= 3.0.0.pre.rc2)
|
||||||
eye (= 0.8)
|
eye (= 0.8)
|
||||||
factory_girl_rails (= 4.6.0)
|
factory_girl_rails (= 4.6.0)
|
||||||
|
|
@ -1029,7 +1029,7 @@ DEPENDENCIES
|
||||||
turbo_dev_assets (= 0.0.2)
|
turbo_dev_assets (= 0.0.2)
|
||||||
twitter (= 5.16.0)
|
twitter (= 5.16.0)
|
||||||
twitter-text (= 1.13.3)
|
twitter-text (= 1.13.3)
|
||||||
typhoeus (= 0.8.0)
|
typhoeus (= 1.0.1)
|
||||||
uglifier (= 2.7.2)
|
uglifier (= 2.7.2)
|
||||||
unicorn (= 5.0.1)
|
unicorn (= 5.0.1)
|
||||||
uuid (= 2.3.8)
|
uuid (= 2.3.8)
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,20 @@ DiasporaFederation.configure do |config|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
on :save_entity_after_receive do
|
on :receive_entity do
|
||||||
|
# TODO
|
||||||
|
end
|
||||||
|
|
||||||
|
on :fetch_public_entity do |entity_type, guid|
|
||||||
|
entity = entity_type.constantize.find_by(guid: guid, public: true)
|
||||||
|
Diaspora::Federation.post(entity) if entity.is_a? Post
|
||||||
|
end
|
||||||
|
|
||||||
|
on :fetch_person_url_to do |diaspora_id, path|
|
||||||
|
Person.find_by(diaspora_handle: diaspora_id).send(:url_to, path)
|
||||||
|
end
|
||||||
|
|
||||||
|
on :update_pod do
|
||||||
# TODO
|
# TODO
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,14 @@
|
||||||
# the COPYRIGHT file.
|
# the COPYRIGHT file.
|
||||||
|
|
||||||
module Diaspora
|
module Diaspora
|
||||||
require 'diaspora/camo'
|
require "diaspora/camo"
|
||||||
require 'diaspora/exceptions'
|
require "diaspora/exceptions"
|
||||||
require 'diaspora/exporter'
|
require "diaspora/exporter"
|
||||||
require 'diaspora/federated'
|
require "diaspora/federated"
|
||||||
require 'diaspora/fetcher'
|
require "diaspora/federation"
|
||||||
require 'diaspora/markdownify'
|
require "diaspora/fetcher"
|
||||||
require 'diaspora/mentionable'
|
require "diaspora/markdownify"
|
||||||
require 'diaspora/message_renderer'
|
require "diaspora/mentionable"
|
||||||
require 'diaspora/parser'
|
require "diaspora/message_renderer"
|
||||||
|
require "diaspora/parser"
|
||||||
end
|
end
|
||||||
|
|
|
||||||
78
lib/diaspora/federation.rb
Normal file
78
lib/diaspora/federation.rb
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
module Diaspora
|
||||||
|
module Federation
|
||||||
|
def self.post(post)
|
||||||
|
case post
|
||||||
|
when StatusMessage
|
||||||
|
status_message(post)
|
||||||
|
when Reshare
|
||||||
|
reshare(post)
|
||||||
|
else
|
||||||
|
raise ArgumentError, "unknown post-class: #{post.class}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.location(location)
|
||||||
|
DiasporaFederation::Entities::Location.new(
|
||||||
|
address: location.address,
|
||||||
|
lat: location.lat,
|
||||||
|
lng: location.lng
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.photo(photo)
|
||||||
|
DiasporaFederation::Entities::Photo.new(
|
||||||
|
author: photo.diaspora_handle,
|
||||||
|
guid: photo.guid,
|
||||||
|
public: photo.public,
|
||||||
|
created_at: photo.created_at,
|
||||||
|
remote_photo_path: photo.remote_photo_path,
|
||||||
|
remote_photo_name: photo.remote_photo_name,
|
||||||
|
text: photo.text,
|
||||||
|
status_message_guid: photo.status_message_guid,
|
||||||
|
height: photo.height,
|
||||||
|
width: photo.width
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.poll(poll)
|
||||||
|
DiasporaFederation::Entities::Poll.new(
|
||||||
|
guid: poll.guid,
|
||||||
|
question: poll.question,
|
||||||
|
poll_answers: poll.poll_answers.map {|answer| poll_answer(answer) }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.poll_answer(poll_answer)
|
||||||
|
DiasporaFederation::Entities::PollAnswer.new(
|
||||||
|
guid: poll_answer.guid,
|
||||||
|
answer: poll_answer.answer
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.reshare(reshare)
|
||||||
|
DiasporaFederation::Entities::Reshare.new(
|
||||||
|
root_author: reshare.root_diaspora_id,
|
||||||
|
root_guid: reshare.root_guid,
|
||||||
|
author: reshare.diaspora_handle,
|
||||||
|
guid: reshare.guid,
|
||||||
|
public: reshare.public,
|
||||||
|
created_at: reshare.created_at,
|
||||||
|
provider_display_name: reshare.provider_display_name
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.status_message(status_message)
|
||||||
|
DiasporaFederation::Entities::StatusMessage.new(
|
||||||
|
author: status_message.diaspora_handle,
|
||||||
|
guid: status_message.guid,
|
||||||
|
raw_message: status_message.raw_message,
|
||||||
|
photos: status_message.photos.map {|photo| photo(photo) },
|
||||||
|
location: status_message.location ? location(status_message.location) : nil,
|
||||||
|
poll: status_message.poll ? poll(status_message.poll) : nil,
|
||||||
|
public: status_message.public,
|
||||||
|
created_at: status_message.created_at,
|
||||||
|
provider_display_name: status_message.provider_display_name
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -296,4 +296,58 @@ describe "diaspora federation callbacks" do
|
||||||
expect(result).to be_falsey
|
expect(result).to be_falsey
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe ":fetch_public_entity" do
|
||||||
|
it "fetches a Post" do
|
||||||
|
post = FactoryGirl.create(:status_message, author: alice.person, public: true)
|
||||||
|
entity = DiasporaFederation.callbacks.trigger(:fetch_public_entity, "Post", post.guid)
|
||||||
|
|
||||||
|
expect(entity.guid).to eq(post.guid)
|
||||||
|
expect(entity.author).to eq(alice.diaspora_handle)
|
||||||
|
expect(entity.public).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it "fetches a StatusMessage" do
|
||||||
|
post = FactoryGirl.create(:status_message, author: alice.person, public: true)
|
||||||
|
entity = DiasporaFederation.callbacks.trigger(:fetch_public_entity, "StatusMessage", post.guid)
|
||||||
|
|
||||||
|
expect(entity.guid).to eq(post.guid)
|
||||||
|
expect(entity.author).to eq(alice.diaspora_handle)
|
||||||
|
expect(entity.public).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it "fetches a Reshare" do
|
||||||
|
post = FactoryGirl.create(:reshare, author: alice.person, public: true)
|
||||||
|
entity = DiasporaFederation.callbacks.trigger(:fetch_public_entity, "Reshare", post.guid)
|
||||||
|
|
||||||
|
expect(entity.guid).to eq(post.guid)
|
||||||
|
expect(entity.author).to eq(alice.diaspora_handle)
|
||||||
|
expect(entity.public).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not fetch a private post" do
|
||||||
|
post = FactoryGirl.create(:status_message, author: alice.person, public: false)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
DiasporaFederation.callbacks.trigger(:fetch_public_entity, "StatusMessage", post.guid)
|
||||||
|
).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns nil, if the post is unknown" do
|
||||||
|
expect(
|
||||||
|
DiasporaFederation.callbacks.trigger(:fetch_public_entity, "Post", "unknown-guid")
|
||||||
|
).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ":fetch_person_url_to" do
|
||||||
|
it "returns the url with with the pod of the person" do
|
||||||
|
pod = FactoryGirl.create(:pod)
|
||||||
|
person = FactoryGirl.create(:person, pod: pod)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
DiasporaFederation.callbacks.trigger(:fetch_person_url_to, person.diaspora_handle, "/path/on/pod")
|
||||||
|
).to eq("https://#{pod.host}/path/on/pod")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,10 @@ def create_remote_user(pod)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_relayable_entity(entity_name, target, diaspora_id, parent_author_key)
|
def create_relayable_entity(entity_name, target, diaspora_id, parent_author_key)
|
||||||
target_entity_type = FactoryGirl.factory_by_name(entity_name).build_class.get_target_entity_type(@entity.to_h)
|
|
||||||
expect(DiasporaFederation.callbacks).to receive(:trigger)
|
expect(DiasporaFederation.callbacks).to receive(:trigger)
|
||||||
.with(
|
.with(
|
||||||
:fetch_author_private_key_by_entity_guid,
|
:fetch_author_private_key_by_entity_guid,
|
||||||
target_entity_type,
|
FactoryGirl.build(entity_name).parent_type,
|
||||||
target.guid
|
target.guid
|
||||||
)
|
)
|
||||||
.and_return(parent_author_key)
|
.and_return(parent_author_key)
|
||||||
|
|
@ -36,19 +35,18 @@ def create_relayable_entity(entity_name, target, diaspora_id, parent_author_key)
|
||||||
entity_name,
|
entity_name,
|
||||||
conversation_guid: target.guid,
|
conversation_guid: target.guid,
|
||||||
parent_guid: target.guid,
|
parent_guid: target.guid,
|
||||||
diaspora_id: diaspora_id,
|
author: diaspora_id,
|
||||||
poll_answer_guid: target.respond_to?(:poll_answers) ? target.poll_answers.first.guid : nil
|
poll_answer_guid: target.respond_to?(:poll_answers) ? target.poll_answers.first.guid : nil
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_xml(entity, remote_user, recipient=nil)
|
def generate_xml(entity, remote_user, recipient=nil)
|
||||||
if recipient
|
if recipient
|
||||||
DiasporaFederation::Salmon::EncryptedSlap.generate_xml(
|
DiasporaFederation::Salmon::EncryptedSlap.prepare(
|
||||||
remote_user.diaspora_handle,
|
remote_user.diaspora_handle,
|
||||||
OpenSSL::PKey::RSA.new(remote_user.encryption_key),
|
OpenSSL::PKey::RSA.new(remote_user.encryption_key),
|
||||||
entity,
|
entity
|
||||||
OpenSSL::PKey::RSA.new(recipient.encryption_key)
|
).generate_xml(OpenSSL::PKey::RSA.new(recipient.encryption_key))
|
||||||
)
|
|
||||||
else
|
else
|
||||||
DiasporaFederation::Salmon::Slap.generate_xml(
|
DiasporaFederation::Salmon::Slap.generate_xml(
|
||||||
remote_user.diaspora_handle,
|
remote_user.diaspora_handle,
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ describe "Receive federation messages feature" do
|
||||||
it "reshare of public post passes" do
|
it "reshare of public post passes" do
|
||||||
post = FactoryGirl.create(:status_message, author: alice.person, public: true)
|
post = FactoryGirl.create(:status_message, author: alice.person, public: true)
|
||||||
reshare = FactoryGirl.build(
|
reshare = FactoryGirl.build(
|
||||||
:reshare_entity, root_diaspora_id: alice.diaspora_handle, root_guid: post.guid, diaspora_id: sender_id)
|
:reshare_entity, root_author: alice.diaspora_handle, root_guid: post.guid, author: sender_id)
|
||||||
post_message(generate_xml(reshare, sender))
|
post_message(generate_xml(reshare, sender))
|
||||||
|
|
||||||
expect(Reshare.exists?(root_guid: post.guid)).to be_truthy
|
expect(Reshare.exists?(root_guid: post.guid)).to be_truthy
|
||||||
|
|
@ -46,7 +46,7 @@ describe "Receive federation messages feature" do
|
||||||
it "reshare of private post fails" do
|
it "reshare of private post fails" do
|
||||||
post = FactoryGirl.create(:status_message, author: alice.person, public: false)
|
post = FactoryGirl.create(:status_message, author: alice.person, public: false)
|
||||||
reshare = FactoryGirl.build(
|
reshare = FactoryGirl.build(
|
||||||
:reshare_entity, root_diaspora_id: alice.diaspora_handle, root_guid: post.guid, diaspora_id: sender_id)
|
:reshare_entity, root_author: alice.diaspora_handle, root_guid: post.guid, author: sender_id)
|
||||||
expect {
|
expect {
|
||||||
post_message(generate_xml(reshare, sender))
|
post_message(generate_xml(reshare, sender))
|
||||||
}.to raise_error ActiveRecord::RecordInvalid, "Validation failed: Only posts which are public may be reshared."
|
}.to raise_error ActiveRecord::RecordInvalid, "Validation failed: Only posts which are public may be reshared."
|
||||||
|
|
@ -73,7 +73,7 @@ describe "Receive federation messages feature" do
|
||||||
let(:recipient) { alice }
|
let(:recipient) { alice }
|
||||||
|
|
||||||
it "treats sharing request recive correctly" do
|
it "treats sharing request recive correctly" do
|
||||||
entity = FactoryGirl.build(:request_entity, recipient_id: alice.diaspora_handle)
|
entity = FactoryGirl.build(:request_entity, recipient: alice.diaspora_handle)
|
||||||
|
|
||||||
expect(Diaspora::Fetcher::Public).to receive(:queue_for).exactly(1).times
|
expect(Diaspora::Fetcher::Public).to receive(:queue_for).exactly(1).times
|
||||||
|
|
||||||
|
|
@ -95,7 +95,7 @@ describe "Receive federation messages feature" do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn't save the private status message if there is no sharing" do
|
it "doesn't save the private status message if there is no sharing" do
|
||||||
entity = FactoryGirl.build(:status_message_entity, diaspora_id: sender_id, public: false)
|
entity = FactoryGirl.build(:status_message_entity, author: sender_id, public: false)
|
||||||
post_message(generate_xml(entity, sender, alice), alice)
|
post_message(generate_xml(entity, sender, alice), alice)
|
||||||
|
|
||||||
expect(StatusMessage.exists?(guid: entity.guid)).to be_falsey
|
expect(StatusMessage.exists?(guid: entity.guid)).to be_falsey
|
||||||
|
|
@ -112,7 +112,7 @@ describe "Receive federation messages feature" do
|
||||||
it_behaves_like "messages which can't be send without sharing"
|
it_behaves_like "messages which can't be send without sharing"
|
||||||
|
|
||||||
it "treats profile receive correctly" do
|
it "treats profile receive correctly" do
|
||||||
entity = FactoryGirl.build(:profile_entity, diaspora_id: sender_id)
|
entity = FactoryGirl.build(:profile_entity, author: sender_id)
|
||||||
post_message(generate_xml(entity, sender, alice), alice)
|
post_message(generate_xml(entity, sender, alice), alice)
|
||||||
|
|
||||||
expect(Profile.exists?(diaspora_handle: entity.diaspora_id)).to be_truthy
|
expect(Profile.exists?(diaspora_handle: entity.diaspora_id)).to be_truthy
|
||||||
|
|
@ -121,8 +121,8 @@ describe "Receive federation messages feature" do
|
||||||
it "receives conversation correctly" do
|
it "receives conversation correctly" do
|
||||||
entity = FactoryGirl.build(
|
entity = FactoryGirl.build(
|
||||||
:conversation_entity,
|
:conversation_entity,
|
||||||
diaspora_id: sender_id,
|
author: sender_id,
|
||||||
participant_ids: "#{sender_id};#{alice.diaspora_handle}"
|
participants: "#{sender_id};#{alice.diaspora_handle}"
|
||||||
)
|
)
|
||||||
post_message(generate_xml(entity, sender, alice), alice)
|
post_message(generate_xml(entity, sender, alice), alice)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ def retraction_entity(entity_name, target_object, sender)
|
||||||
|
|
||||||
FactoryGirl.build(
|
FactoryGirl.build(
|
||||||
entity_name,
|
entity_name,
|
||||||
diaspora_id: sender.diaspora_handle,
|
author: sender.diaspora_handle,
|
||||||
target_guid: target_object.guid,
|
target_guid: target_object.guid,
|
||||||
target_type: target_object.class.to_s
|
target_type: target_object.class.to_s
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ shared_examples_for "messages which are indifferent about sharing fact" do
|
||||||
let(:public) { recipient.nil? }
|
let(:public) { recipient.nil? }
|
||||||
|
|
||||||
it "treats status message receive correctly" do
|
it "treats status message receive correctly" do
|
||||||
entity = FactoryGirl.build(:status_message_entity, diaspora_id: sender_id, public: public)
|
entity = FactoryGirl.build(:status_message_entity, author: sender_id, public: public)
|
||||||
|
|
||||||
post_message(generate_xml(entity, sender, recipient), recipient)
|
post_message(generate_xml(entity, sender, recipient), recipient)
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@ shared_examples_for "messages which are indifferent about sharing fact" do
|
||||||
|
|
||||||
it "doesn't accept status message with wrong signature" do
|
it "doesn't accept status message with wrong signature" do
|
||||||
allow(sender).to receive(:encryption_key).and_return(OpenSSL::PKey::RSA.new(1024))
|
allow(sender).to receive(:encryption_key).and_return(OpenSSL::PKey::RSA.new(1024))
|
||||||
entity = FactoryGirl.build(:status_message_entity, diaspora_id: sender_id, public: public)
|
entity = FactoryGirl.build(:status_message_entity, author: sender_id, public: public)
|
||||||
|
|
||||||
post_message(generate_xml(entity, sender, recipient), recipient)
|
post_message(generate_xml(entity, sender, recipient), recipient)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue