lib/diaspora_federation/discovery/web_finger.rb
# frozen_string_literal: true
module DiasporaFederation
module Discovery
# The WebFinger document used for diaspora* user discovery is based on an
# {https://datatracker.ietf.org/doc/html/rfc7033 RFC 7033}.
#
# @example Creating a WebFinger document from a person hash
# wf = WebFinger.new(
# acct_uri: "acct:user@server.example",
# hcard_url: "https://server.example/hcard/users/user",
# seed_url: "https://server.example/",
# profile_url: "https://server.example/u/user",
# atom_url: "https://server.example/public/user.atom",
# salmon_url: "https://server.example/receive/users/0123456789abcdef"
# )
# json_string = wf.to_json
#
# @example Creating a WebFinger instance from an JSON document
# wf = WebFinger.from_json(json_string)
# ...
# hcard_url = wf.hcard_url
# ...
#
# @see http://www.iana.org/assignments/link-relations/link-relations.xhtml
# official list of IANA link relations
class WebFinger < Entity
# @!attribute [r] acct_uri
# The Subject element should contain the WebFinger address that was asked
# for. If it does not, then this WebFinger profile MUST be ignored.
# @return [String]
property :acct_uri, :string
# @!attribute [r] hcard_url
# @return [String] link to the +hCard+
property :hcard_url, :string
# @!attribute [r] seed_url
# @return [String] link to the pod
property :seed_url, :string
# @!attribute [r] profile_url
# @return [String] link to the users profile
property :profile_url, :string, optional: true
# @!attribute [r] atom_url
# This atom feed is an Activity Stream of the user's public posts. diaspora*
# pods SHOULD publish an Activity Stream of public posts, but there is
# currently no requirement to be able to read Activity Streams.
# @see http://activitystrea.ms/ Activity Streams specification
#
# Note that this feed MAY also be made available through the PubSubHubbub
# mechanism by supplying a <link rel="hub"> in the atom feed itself.
# @return [String] atom feed url
property :atom_url, :string, optional: true
# @!attribute [r] salmon_url
# @note could be nil
# @return [String] salmon endpoint url
# @see https://cdn.rawgit.com/salmon-protocol/salmon-protocol/master/draft-panzer-salmon-00.html#SMLR
# Panzer draft for Salmon, paragraph 3.3
property :salmon_url, :string, optional: true
# @!attribute [r] subscribe_url
# This url is used to find another user on the home-pod of the user in the WebFinger.
property :subscribe_url, :string, optional: true
# +hcard_url+ link relation
REL_HCARD = "http://microformats.org/profile/hcard"
# +seed_url+ link relation
REL_SEED = "http://joindiaspora.com/seed_location"
# +profile_url+ link relation.
# @note This might just as well be an +Alias+ instead of a +Link+.
REL_PROFILE = "http://webfinger.net/rel/profile-page"
# +atom_url+ link relation
REL_ATOM = "http://schemas.google.com/g/2010#updates-from"
# +salmon_url+ link relation
REL_SALMON = "salmon"
# +subscribe_url+ link relation
REL_SUBSCRIBE = "http://ostatus.org/schema/1.0/subscribe"
# Additional WebFinger data
# @return [Hash] additional elements
attr_reader :additional_data
# Initializes a new WebFinger Entity
#
# @param [Hash] data WebFinger data
# @param [Hash] additional_data additional WebFinger data
# @option additional_data [Array<String>] :aliases additional aliases
# @option additional_data [Hash] :properties properties
# @option additional_data [Array<Hash>] :links additional link elements
# @see DiasporaFederation::Entity#initialize
def initialize(data, additional_data={})
@additional_data = additional_data
super(data)
end
# @!visibility private
# Generating WebFinger to XML is not supported anymore, use {#to_json} instead.
def to_xml
raise "Generating WebFinger to XML is not supported anymore, use 'to_json' instead."
end
# Creates the JSON string from the current WebFinger instance
# @return [String] JSON string
def to_json(*_args)
to_xrd.to_json
end
# @!visibility private
# Parsing WebFinger as XML is not supported anymore, use {from_json} instead.
def self.from_xml(_webfinger_xml)
raise "Parsing WebFinger as XML is not supported anymore, use 'from_json' instead."
end
# Creates a WebFinger instance from the given JSON string
# @param [String] webfinger_json WebFinger JSON string
# @return [WebFinger] WebFinger instance
def self.from_json(webfinger_json)
from_hash(parse_json_and_validate(webfinger_json))
end
# Creates a WebFinger instance from the given data
# @param [Hash] data WebFinger data hash
# @return [WebFinger] WebFinger instance
def self.from_hash(data)
links = data[:links]
new(
acct_uri: data[:subject],
hcard_url: parse_link(links, REL_HCARD),
seed_url: parse_link(links, REL_SEED),
profile_url: parse_link(links, REL_PROFILE),
atom_url: parse_link(links, REL_ATOM),
salmon_url: parse_link(links, REL_SALMON),
subscribe_url: parse_link_template(links, REL_SUBSCRIBE)
)
end
# @return [String] string representation of this object
def to_s
"WebFinger:#{acct_uri}"
end
private
# Parses the JSON string to a Hash and does some rudimentary checking on
# the data Hash.
# @param [String] webfinger_json WebFinger JSON string
# @return [Hash] data JSON data
# @raise [InvalidData] if the given JSON string is invalid or incomplete
private_class_method def self.parse_json_and_validate(webfinger_json)
XrdDocument.json_data(webfinger_json).tap do |data|
valid = data.key?(:subject) && data.key?(:links)
raise InvalidData, "Webfinger JSON is incomplete" unless valid
end
end
def to_xrd
XrdDocument.new.tap do |xrd|
xrd.subject = acct_uri
xrd.aliases.concat(additional_data[:aliases]) if additional_data[:aliases]
xrd.properties.merge!(additional_data[:properties]) if additional_data[:properties]
add_links_to(xrd)
end
end
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}
add_optional_links_to(doc)
doc.links.concat(additional_data[:links]) if additional_data[:links]
end
def add_optional_links_to(doc)
doc.links << {rel: REL_PROFILE, type: "text/html", href: profile_url} if profile_url
doc.links << {rel: REL_ATOM, type: "application/atom+xml", href: atom_url} if atom_url
doc.links << {rel: REL_SALMON, href: salmon_url} if salmon_url
doc.links << {rel: REL_SUBSCRIBE, template: subscribe_url} if subscribe_url
end
private_class_method def self.find_link(links, rel)
links.find {|l| l[:rel] == rel }
end
private_class_method def self.parse_link(links, rel)
element = find_link(links, rel)
element ? element[:href] : nil
end
private_class_method def self.parse_link_template(links, rel)
element = find_link(links, rel)
element ? element[:template] : nil
end
end
end
end