diff --git a/.rubocop.yml b/.rubocop.yml
index 3ecc6cb..db03767 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -11,13 +11,16 @@ Metrics/LineLength:
# Too short methods lead to extraction of single-use methods, which can make
# the code easier to read (by naming things), but can also clutter the class
-Metrics/MethodLength:
+Metrics/MethodLength:
Max: 20
# The guiding principle of classes is SRP, SRP can't be accurately measured by LoC
Metrics/ClassLength:
Max: 1500
+Metrics/ModuleLength:
+ Max: 1500
+
# No space makes the method definition shorter and differentiates
# from a regular assignment.
Style/SpaceAroundEqualsInParameterDefault:
@@ -66,7 +69,7 @@ Lint/AssignmentInCondition:
AllowSafeAssignment: false
# A specialized exception class will take one or more arguments and construct the message from it.
-# So both variants make sense.
+# So both variants make sense.
Style/RaiseArgs:
Enabled: false
@@ -124,11 +127,11 @@ Lint/ShadowingOuterLocalVariable:
# Check with yard instead.
Style/Documentation:
- Enabled: false
+ Enabled: false
# This is just silly. Calling the argument `other` in all cases makes no sense.
Style/OpMethod:
- Enabled: false
+ Enabled: false
# There are valid cases, for example debugging Cucumber steps,
# also they'll fail CI anyway
diff --git a/lib/diaspora_federation/web_finger.rb b/lib/diaspora_federation/web_finger.rb
index 71326f0..5c52b98 100644
--- a/lib/diaspora_federation/web_finger.rb
+++ b/lib/diaspora_federation/web_finger.rb
@@ -10,3 +10,4 @@ end
require "diaspora_federation/web_finger/exceptions"
require "diaspora_federation/web_finger/xrd_document"
require "diaspora_federation/web_finger/host_meta"
+require "diaspora_federation/web_finger/web_finger"
diff --git a/lib/diaspora_federation/web_finger/web_finger.rb b/lib/diaspora_federation/web_finger/web_finger.rb
new file mode 100644
index 0000000..85fd2f7
--- /dev/null
+++ b/lib/diaspora_federation/web_finger/web_finger.rb
@@ -0,0 +1,203 @@
+module DiasporaFederation
+ module WebFinger
+ ##
+ # The WebFinger document used for Diaspora* user discovery is based on an older
+ # draft of the specification you can find in the wiki of the "webfinger" project
+ # on {http://code.google.com/p/webfinger/wiki/WebFingerProtocol Google Code}
+ # (from around 2010).
+ #
+ # In the meantime an actual RFC draft has been in development, which should
+ # serve as a base for all future changes of this implementation.
+ #
+ # @example Creating a WebFinger document from account data
+ # wf = WebFinger.from_account({
+ # acct_uri: "acct:user@server.example",
+ # alias_url: "https://server.example/people/0123456789abcdef",
+ # hcard_url: "https://server.example/hcard/users/user",
+ # seed_url: "https://server.example/",
+ # profile_url: "https://server.example/u/user",
+ # updates_url: "https://server.example/public/user.atom",
+ # guid: "0123456789abcdef",
+ # pubkey: "ABCDEF=="
+ # })
+ # xml_string = wf.to_xml
+ #
+ # @example Creating a WebFinger instance from an xml document
+ # wf = WebFinger.from_xml(xml_string)
+ # ...
+ # hcard_url = wf.hcard_url
+ # ...
+ #
+ # @see http://tools.ietf.org/html/draft-jones-appsawg-webfinger "WebFinger" -
+ # current draft
+ # @see http://code.google.com/p/webfinger/wiki/CommonLinkRelations
+ # @see http://www.iana.org/assignments/link-relations/link-relations.xhtml
+ # official list of IANA link relations
+ class WebFinger
+ private_class_method :new
+
+ attr_reader :acct_uri, :alias_url, :hcard_url, :seed_url, :profile_url, :updates_url
+
+ # @deprecated Either convert these to +Property+ elements or move to the
+ # +hCard+, which actually has fields for an +UID+ and +KEY+ defined in
+ # the +vCard+ specification (will affect older Diaspora* installations).
+ attr_reader :guid, :pubkey
+
+ # +hcard+ link relation
+ REL_HCARD = "http://microformats.org/profile/hcard"
+
+ # +seed_location+ link relation
+ REL_SEED = "http://joindiaspora.com/seed_location"
+
+ # @deprecated This should be a +Property+ or moved to the +hCard+, but +Link+
+ # is inappropriate according to the specification (will affect older
+ # Diaspora* installations).
+ # +guid+ link relation
+ REL_GUID = "http://joindiaspora.com/guid"
+
+ # +profile-page+ link relation.
+ # @note This might just as well be an +Alias+ instead of a +Link+.
+ REL_PROFILE = "http://webfinger.net/rel/profile-page"
+
+ # Atom feed link relation
+ REL_UPDATES = "http://schemas.google.com/g/2010#updates-from"
+
+ # @deprecated This should be a +Property+ or moved to the +hcard+, but +Link+
+ # is inappropriate according to the specification (will affect older
+ # Diaspora* installations).
+ # +diaspora-public-key+ link relation
+ REL_PUBKEY = "diaspora-public-key"
+
+ # Create the XML string from the current WebFinger instance
+ # @return [String] XML string
+ def to_xml
+ doc = XrdDocument.new
+ doc.subject = @acct_uri
+ doc.aliases << @alias_url
+
+ add_links_to(doc)
+
+ doc.to_xml
+ end
+
+ # Create a WebFinger instance from the given account data Hash.
+ # @param [Hash] data account data
+ # @return [WebFinger] WebFinger instance
+ # @raise [InvalidData] if the given data Hash is invalid or incomplete
+ def self.from_account(data)
+ raise InvalidData unless account_data_complete?(data)
+
+ wf = allocate
+ wf.instance_eval {
+ @acct_uri = data[:acct_uri]
+ @alias_url = data[:alias_url]
+ @hcard_url = data[:hcard_url]
+ @seed_url = data[:seed_url]
+ @profile_url = data[:profile_url]
+ @updates_url = data[:updates_url]
+
+ # TODO: change me! #########
+ @guid = data[:guid]
+ @pubkey = data[:pubkey]
+ #############################
+ }
+ wf
+ end
+
+ # Create a WebFinger instance from the given XML string.
+ # @param [String] webfinger_xml WebFinger XML string
+ # @return [WebFinger] WebFinger instance
+ def self.from_xml(webfinger_xml)
+ data = XrdDocument.xml_data(webfinger_xml)
+ raise InvalidData unless xml_data_valid?(data)
+
+ hcard, seed, guid, profile, updates, pubkey = parse_links(data)
+
+ wf = allocate
+ wf.instance_eval {
+ @acct_uri = data[:subject]
+ @alias_url = data[:aliases].first
+ @hcard_url = hcard[:href]
+ @seed_url = seed[:href]
+ @profile_url = profile[:href]
+ @updates_url = updates[:href]
+
+ # TODO: change me! ##########
+ @guid = guid[:href]
+ @pubkey = pubkey[:href]
+ ##############################
+ }
+ wf
+ end
+
+ private
+
+ # Checks the given account data Hash for correct type and completeness.
+ # @param [Hash] data account data
+ # @return [Boolean] validation result
+ def self.account_data_complete?(data)
+ !data.nil? && data.instance_of?(Hash) &&
+ data.key?(:acct_uri) && data.key?(:alias_url) &&
+ data.key?(:hcard_url) && data.key?(:seed_url) &&
+ data.key?(:guid) && data.key?(:profile_url) &&
+ data.key?(:updates_url) && data.key?(:pubkey)
+ end
+ private_class_method :account_data_complete?
+
+ # Does some rudimentary checking on the data Hash produced from parsing the
+ # XML string
+ # @param [Hash] data XML data
+ # @return [Boolean] validation result
+ def self.xml_data_valid?(data)
+ data.key?(:subject) && data.key?(:aliases) && data.key?(:links)
+ end
+ private_class_method :xml_data_valid?
+
+ def add_links_to(doc)
+ doc.links << {rel: REL_HCARD,
+ type: "text/html",
+ href: @hcard_url}
+ doc.links << {rel: REL_SEED,
+ type: "text/html",
+ href: @seed_url}
+
+ # TODO: change me! ##############
+ doc.links << {rel: REL_GUID,
+ type: "text/html",
+ href: @guid}
+ ##################################
+
+ doc.links << {rel: REL_PROFILE,
+ type: "text/html",
+ href: @profile_url}
+ doc.links << {rel: REL_UPDATES,
+ type: "application/atom+xml",
+ href: @updates_url}
+
+ # TODO: change me! ##############
+ doc.links << {rel: REL_PUBKEY,
+ type: "RSA",
+ href: @pubkey}
+ ##################################
+ end
+
+ def self.parse_links(data)
+ links = data[:links]
+ hcard = parse_link(links, REL_HCARD)
+ seed = parse_link(links, REL_SEED)
+ guid = parse_link(links, REL_GUID)
+ profile = parse_link(links, REL_PROFILE)
+ updates = parse_link(links, REL_UPDATES)
+ pubkey = parse_link(links, REL_PUBKEY)
+ raise InvalidData unless [hcard, seed, guid, profile, updates, pubkey].all?
+ [hcard, seed, guid, profile, updates, pubkey]
+ end
+ private_class_method :parse_links
+
+ def self.parse_link(links, rel)
+ links.find {|l| l[:rel] == rel }
+ end
+ private_class_method :parse_link
+ end
+ end
+end
diff --git a/spec/lib/web_finger/web_finger_spec.rb b/spec/lib/web_finger/web_finger_spec.rb
new file mode 100644
index 0000000..ba3d23a
--- /dev/null
+++ b/spec/lib/web_finger/web_finger_spec.rb
@@ -0,0 +1,117 @@
+module DiasporaFederation
+ describe WebFinger::WebFinger do
+ acct = "acct:user@pod.example.tld"
+ alias_url = "http://pod.example.tld/"
+ hcard_url = "https://pod.example.tld/hcard/users/abcdef0123456789"
+ seed_url = "https://pod.geraspora.de/"
+ guid = "abcdef0123456789"
+ profile_url = "https://pod.example.tld/u/user"
+ updates_url = "https://pod.example.tld/public/user.atom"
+ pubkey = "AAAAAA=="
+
+ xml = <<-XML
+
+
+ #{acct}
+ #{alias_url}
+
+
+
+
+
+
+
+XML
+
+ it "must not create blank instances" do
+ expect { WebFinger::WebFinger.new }.to raise_error
+ end
+
+ context "generation" do
+ it "creates a nice XML document" do
+ wf = WebFinger::WebFinger.from_account(
+ acct_uri: acct,
+ alias_url: alias_url,
+ hcard_url: hcard_url,
+ seed_url: seed_url,
+ profile_url: profile_url,
+ updates_url: updates_url,
+ guid: guid,
+ pubkey: pubkey
+ )
+ expect(wf.to_xml).to eq(xml)
+ end
+
+ it "fails if some params are missing" do
+ expect {
+ WebFinger::WebFinger.from_account(
+ acct_uri: acct,
+ alias_url: alias_url,
+ hcard_url: hcard_url
+ )
+ }.to raise_error(WebFinger::InvalidData)
+ end
+
+ it "fails if nothing was given" do
+ expect { WebFinger::WebFinger.from_account({}) }.to raise_error(WebFinger::InvalidData)
+ end
+ end
+
+ context "parsing" do
+ it "reads its own output" do
+ wf = WebFinger::WebFinger.from_xml(xml)
+ expect(wf.acct_uri).to eq(acct)
+ expect(wf.alias_url).to eq(alias_url)
+ expect(wf.hcard_url).to eq(hcard_url)
+ expect(wf.seed_url).to eq(seed_url)
+ expect(wf.profile_url).to eq(profile_url)
+ expect(wf.updates_url).to eq(updates_url)
+
+ expect(wf.guid).to eq(guid)
+ expect(wf.pubkey).to eq(pubkey)
+ end
+
+ it "reads old-style XML" do
+ historic_xml = <<-XML
+
+
+ #{acct}
+ #{alias_url}
+
+
+
+
+
+
+
+
+
+XML
+
+ wf = WebFinger::WebFinger.from_xml(historic_xml)
+ expect(wf.acct_uri).to eq(acct)
+ expect(wf.alias_url).to eq(alias_url)
+ expect(wf.hcard_url).to eq(hcard_url)
+ expect(wf.seed_url).to eq(seed_url)
+ expect(wf.profile_url).to eq(profile_url)
+ expect(wf.updates_url).to eq(updates_url)
+
+ expect(wf.guid).to eq(guid)
+ expect(wf.pubkey).to eq(pubkey)
+ end
+
+ it "fails if the document is empty" do
+ invalid_xml = <
+
+
+XML
+ expect { WebFinger::WebFinger.from_xml(invalid_xml) }.to raise_error(WebFinger::InvalidData)
+ end
+
+ it "fails if the document is not XML" do
+ expect { WebFinger::WebFinger.from_xml("") }.to raise_error(WebFinger::InvalidDocument)
+ end
+ end
+ end
+end