├── .ruby-version
├── .rspec
├── .gitignore
├── Rakefile
├── .travis.yml
├── Gemfile
├── lib
├── goldfinger
│ ├── utils.rb
│ ├── request.rb
│ ├── client.rb
│ ├── link.rb
│ └── result.rb
└── goldfinger.rb
├── spec
├── goldfinger
│ ├── request_spec.rb
│ ├── client_spec.rb
│ └── result_spec.rb
├── spec_helper.rb
└── fixtures
│ ├── quitter.no_.well-known_host-meta
│ ├── quitter.no_.well-known_webfinger.xml
│ └── quitter.no_.well-known_webfinger.json
├── goldfinger.gemspec
├── LICENSE
├── .github
└── workflows
│ └── gempush.yml
├── README.md
├── .rubocop.yml
└── Gemfile.lock
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.5.0
2 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --require spec_helper
3 | --format Fuubar
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /Gemfile.lock
4 | /_yardoc/
5 | /coverage/
6 | /doc/
7 | /pkg/
8 | /spec/reports/
9 | /tmp/
10 | *.gem
11 |
12 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rspec/core/rake_task'
2 | require 'bundler/gem_tasks'
3 |
4 | RSpec::Core::RakeTask.new(:spec) do |task|
5 | task.rspec_opts = ['--color', '--format', 'documentation']
6 | end
7 |
8 | task :default => :spec
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | cache: bundler
3 |
4 | notifications:
5 | email: false
6 |
7 | rvm:
8 | - 2.3.1
9 | - 2.4.1
10 |
11 | bundler_args: --without development --retry=3 --jobs=3
12 |
13 | script: bundle exec rake
14 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | group :development, :test do
4 | gem 'pry', '>= 0.10.3'
5 | end
6 |
7 | group :test do
8 | gem 'rspec', '>= 3.0'
9 | gem 'fuubar'
10 | gem 'webmock'
11 | gem 'rake'
12 | end
13 |
14 | gemspec
15 |
--------------------------------------------------------------------------------
/lib/goldfinger/utils.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Goldfinger
4 | module Utils
5 | def perform_get(path, options = {})
6 | perform_request(:get, path, options)
7 | end
8 |
9 | def perform_request(request_method, path, options = {})
10 | Goldfinger::Request.new(request_method, path, options).perform
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/goldfinger/request_spec.rb:
--------------------------------------------------------------------------------
1 | describe Goldfinger::Request do
2 | describe '#perform' do
3 | before do
4 | stub_request(:get, 'example.com').to_return(body: 'OK')
5 | end
6 |
7 | subject { Goldfinger::Request.new(:get, 'http://example.com').perform }
8 |
9 | it 'returns a http response' do
10 | expect(subject).to be_a HTTP::Response
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'goldfinger'
2 | require 'webmock/rspec'
3 | require 'pry'
4 |
5 | WebMock.disable_net_connect!
6 |
7 | RSpec.configure do |config|
8 | config.expect_with :rspec do |expectations|
9 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
10 | end
11 |
12 | config.mock_with :rspec do |mocks|
13 | mocks.verify_partial_doubles = true
14 | end
15 | end
16 |
17 | def fixture_path(path)
18 | File.join(File.expand_path('../fixtures', __FILE__), path)
19 | end
20 |
21 | def fixture(path)
22 | File.new(fixture_path(path))
23 | end
24 |
--------------------------------------------------------------------------------
/lib/goldfinger/request.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'http'
4 | require 'addressable'
5 |
6 | module Goldfinger
7 | class Request
8 | def initialize(request_method, path, options = {})
9 | @request_method = request_method
10 | @uri = Addressable::URI.parse(path)
11 | @options = options
12 | end
13 |
14 | def perform
15 | http_client.request(@request_method, @uri.to_s, @options)
16 | end
17 |
18 | private
19 |
20 | def http_client
21 | HTTP.timeout(write: 60, connect: 20, read: 60).follow
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/spec/fixtures/quitter.no_.well-known_host-meta:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/lib/goldfinger.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'goldfinger/request'
4 | require 'goldfinger/link'
5 | require 'goldfinger/result'
6 | require 'goldfinger/utils'
7 | require 'goldfinger/client'
8 |
9 | module Goldfinger
10 | class Error < StandardError
11 | end
12 |
13 | class NotFoundError < Error
14 | end
15 |
16 | # Returns result for the Webfinger query
17 | #
18 | # @raise [Goldfinger::NotFoundError] Error raised when the Webfinger resource could not be retrieved
19 | # @raise [Goldfinger::SSLError] Error raised when there was a SSL error when fetching the resource
20 | # @param uri [String] A full resource identifier in the format acct:user@example.com
21 | # @param opts [Hash] Options passed to HTTP.rb client
22 | # @return [Goldfinger::Result]
23 | def self.finger(uri, opts = {})
24 | Goldfinger::Client.new(uri, opts).finger
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/goldfinger.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Gem::Specification.new do |s|
4 | s.name = 'goldfinger'
5 | s.version = '2.1.1'
6 | s.platform = Gem::Platform::RUBY
7 | s.required_ruby_version = '>= 2.3.0'
8 | s.date = '2016-02-17'
9 | s.summary = 'A Webfinger utility for Ruby'
10 | s.description = 'A Webfinger utility for Ruby'
11 | s.authors = ['Eugen Rochko']
12 | s.email = 'eugen@zeonfederated.com'
13 | s.files = `git ls-files lib LICENSE README.md`.split($RS)
14 | s.homepage = 'https://github.com/tootsuite/goldfinger'
15 | s.licenses = ['MIT']
16 |
17 | s.add_dependency('http', '~> 4.0')
18 | s.add_dependency('addressable', '~> 2.5')
19 | s.add_dependency('nokogiri', '~> 1.8')
20 | s.add_dependency('oj', '~> 3.0')
21 |
22 | s.add_development_dependency('bundler', '~> 1.15')
23 | end
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Eugen Rochko
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/.github/workflows/gempush.yml:
--------------------------------------------------------------------------------
1 | name: Ruby Gem
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 |
8 | jobs:
9 | build:
10 | name: Build + Publish
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@master
15 | - name: Set up Ruby 2.6
16 | uses: actions/setup-ruby@v1
17 | with:
18 | version: 2.6.x
19 |
20 | - name: Publish to GPR
21 | run: |
22 | mkdir -p $HOME/.gem
23 | touch $HOME/.gem/credentials
24 | chmod 0600 $HOME/.gem/credentials
25 | printf -- "---\n:github: Bearer ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
26 | gem build *.gemspec
27 | gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
28 | env:
29 | GEM_HOST_API_KEY: ${{secrets.GPR_AUTH_TOKEN}}
30 | OWNER: tootsuite
31 |
32 | - name: Publish to RubyGems
33 | run: |
34 | mkdir -p $HOME/.gem
35 | touch $HOME/.gem/credentials
36 | chmod 0600 $HOME/.gem/credentials
37 | printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
38 | gem build *.gemspec
39 | gem push *.gem
40 | env:
41 | GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_AUTH_TOKEN}}
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Goldfinger, a WebFinger client for Ruby
2 | =======================================
3 |
4 | [][gem]
5 | [][travis]
6 |
7 | [gem]: https://rubygems.org/gems/goldfinger
8 | [travis]: https://travis-ci.org/tootsuite/goldfinger
9 |
10 | A WebFinger client for Ruby. Supports `application/xrd+xml` and `application/jrd+json` responses. Raises `Goldfinger::NotFoundError` on failure to fetch the Webfinger or XRD data, can also raise `HTTP:Error` or `OpenSSL::SSL::SSLError` if something is wrong with the HTTPS connection it uses.
11 |
12 | - **Does not** fall back to HTTP if HTTPS is not available
13 | - **Does** check host-meta XRD, but *only* if the standard WebFinger path yielded no result
14 |
15 | ## Installation
16 |
17 | gem install goldfinger
18 |
19 | ## Usage
20 |
21 | data = Goldfinger.finger('acct:gargron@quitter.no')
22 |
23 | data.link('http://schemas.google.com/g/2010#updates-from').href
24 | # => "https://quitter.no/api/statuses/user_timeline/7477.atom"
25 |
26 | data.aliases
27 | # => ["https://quitter.no/user/7477", "https://quitter.no/gargron"]
28 |
29 | data.subject
30 | # => "acct:gargron@quitter.no"
31 |
32 | ## RFC support
33 |
34 | The official WebFinger RFC is [7033](https://tools.ietf.org/html/rfc7033).
35 |
--------------------------------------------------------------------------------
/lib/goldfinger/client.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'addressable'
4 | require 'nokogiri'
5 |
6 | module Goldfinger
7 | class Client
8 | include Goldfinger::Utils
9 |
10 | def initialize(uri, opts = {})
11 | @uri = uri
12 | @ssl = opts.delete(:ssl) { true }
13 | @scheme = @ssl ? 'https' : 'http'
14 | @opts = opts
15 | end
16 |
17 | def finger
18 | response = perform_get(standard_url, @opts)
19 |
20 | return finger_from_template if response.code != 200
21 |
22 | Goldfinger::Result.new(response)
23 | rescue Addressable::URI::InvalidURIError
24 | raise Goldfinger::NotFoundError, 'Invalid URI'
25 | end
26 |
27 | private
28 |
29 | def finger_from_template
30 | template = perform_get(url, @opts)
31 |
32 | raise Goldfinger::NotFoundError, 'No host-meta on the server' if template.code != 200
33 |
34 | response = perform_get(url_from_template(template.body), @opts)
35 |
36 | raise Goldfinger::NotFoundError, 'No such user on the server' if response.code != 200
37 |
38 | Goldfinger::Result.new(response)
39 | end
40 |
41 | def url
42 | "#{@scheme}://#{domain}/.well-known/host-meta"
43 | end
44 |
45 | def standard_url
46 | "#{@scheme}://#{domain}/.well-known/webfinger?resource=#{@uri}"
47 | end
48 |
49 | def url_from_template(template)
50 | xml = Nokogiri::XML(template)
51 | links = xml.xpath('//xmlns:Link[@rel="lrdd"]')
52 |
53 | raise Goldfinger::NotFoundError if links.empty?
54 |
55 | links.first.attribute('template').value.gsub('{uri}', @uri)
56 | rescue Nokogiri::XML::XPath::SyntaxError
57 | raise Goldfinger::Error, "Bad XML: #{template}"
58 | end
59 |
60 | def domain
61 | @uri.split('@').last
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/lib/goldfinger/link.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Goldfinger
4 | # @!attribute [r] href
5 | # @return [String] The href the link points to
6 | # @!attribute [r] template
7 | # @return [String] The template the link contains
8 | # @!attribute [r] type
9 | # @return [String] The mime type of the link
10 | # @!attribute [r] rel
11 | # @return [String] The relation descriptor of the link
12 | class Link
13 | attr_reader :href, :template, :type, :rel
14 |
15 | def initialize(a)
16 | @href = a[:href]
17 | @template = a[:template]
18 | @type = a[:type]
19 | @rel = a[:rel]
20 | @titles = a[:titles]
21 | @properties = a[:properties]
22 | end
23 |
24 | # The "titles" object comprises zero or more name/value pairs whose
25 | # names are a language tag or the string "und". The string is
26 | # human-readable and describes the link relation.
27 | # @see #title
28 | # @return [Array] Array form of the hash
29 | def titles
30 | @titles.to_a
31 | end
32 |
33 | # The "properties" object within the link relation object comprises
34 | # zero or more name/value pairs whose names are URIs (referred to as
35 | # "property identifiers") and whose values are strings or nil.
36 | # Properties are used to convey additional information about the link
37 | # relation.
38 | # @see #property
39 | # @return [Array] Array form of the hash
40 | def properties
41 | @properties.to_a
42 | end
43 |
44 | # Returns a title for a language
45 | # @param lang [String]
46 | # @return [String]
47 | def title(lang)
48 | @titles[lang]
49 | end
50 |
51 | # Returns a property for a key
52 | # @param key [String]
53 | # @return [String]
54 | def property(key)
55 | @properties[key]
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | TargetRubyVersion: 2.3
3 | Exclude:
4 | - 'spec/**/*'
5 | - 'bin/*'
6 | - 'Rakefile'
7 | - 'vendor/**/*'
8 |
9 | Bundler/OrderedGems:
10 | Enabled: false
11 |
12 | Layout/AccessModifierIndentation:
13 | EnforcedStyle: indent
14 |
15 | Layout/EmptyLineAfterMagicComment:
16 | Enabled: false
17 |
18 | Layout/SpaceInsideHashLiteralBraces:
19 | EnforcedStyle: space
20 |
21 | Metrics/AbcSize:
22 | Max: 100
23 |
24 | Metrics/BlockNesting:
25 | Max: 3
26 |
27 | Metrics/ClassLength:
28 | CountComments: false
29 | Max: 200
30 |
31 | Metrics/CyclomaticComplexity:
32 | Max: 15
33 |
34 | Metrics/LineLength:
35 | AllowURI: true
36 | Enabled: false
37 |
38 | Metrics/MethodLength:
39 | CountComments: false
40 | Max: 55
41 |
42 | Metrics/ModuleLength:
43 | CountComments: false
44 | Max: 200
45 |
46 | Metrics/ParameterLists:
47 | Max: 4
48 | CountKeywordArgs: true
49 |
50 | Metrics/PerceivedComplexity:
51 | Max: 10
52 |
53 | Rails:
54 | Enabled: true
55 |
56 | Rails/HasAndBelongsToMany:
57 | Enabled: false
58 |
59 | Rails/SkipsModelValidations:
60 | Enabled: false
61 |
62 | Style/ClassAndModuleChildren:
63 | Enabled: false
64 |
65 | Style/CollectionMethods:
66 | Enabled: true
67 | PreferredMethods:
68 | find_all: 'select'
69 |
70 | Style/Documentation:
71 | Enabled: false
72 |
73 | Style/DoubleNegation:
74 | Enabled: true
75 |
76 | Style/FrozenStringLiteralComment:
77 | Enabled: true
78 |
79 | Style/GuardClause:
80 | Enabled: false
81 |
82 | Style/Lambda:
83 | Enabled: false
84 |
85 | Style/PercentLiteralDelimiters:
86 | PreferredDelimiters:
87 | '%i': '()'
88 | '%w': '()'
89 |
90 | Style/PerlBackrefs:
91 | AutoCorrect: false
92 |
93 | Style/RegexpLiteral:
94 | Enabled: false
95 |
96 | Style/SymbolArray:
97 | Enabled: false
98 |
99 | Style/TrailingCommaInLiteral:
100 | EnforcedStyleForMultiline: 'comma'
101 |
--------------------------------------------------------------------------------
/spec/fixtures/quitter.no_.well-known_webfinger.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | acct:gargron@quitter.no
4 | https://quitter.no/user/7477
5 | https://quitter.no/gargron
6 | Bob Smith
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | User Photo
21 | Benutzerfoto
22 | 1970-01-01
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | goldfinger (2.1.1)
5 | addressable (~> 2.5)
6 | http (~> 4.0)
7 | nokogiri (~> 1.8)
8 | oj (~> 3.0)
9 |
10 | GEM
11 | remote: https://rubygems.org/
12 | specs:
13 | addressable (2.5.2)
14 | public_suffix (>= 2.0.2, < 4.0)
15 | coderay (1.1.1)
16 | crack (0.4.3)
17 | safe_yaml (~> 1.0.0)
18 | diff-lcs (1.3)
19 | domain_name (0.5.20190701)
20 | unf (>= 0.0.5, < 1.0.0)
21 | ffi (1.12.2)
22 | ffi-compiler (1.0.1)
23 | ffi (>= 1.0.0)
24 | rake
25 | fuubar (2.2.0)
26 | rspec-core (~> 3.0)
27 | ruby-progressbar (~> 1.4)
28 | hashdiff (0.3.4)
29 | http (4.3.0)
30 | addressable (~> 2.3)
31 | http-cookie (~> 1.0)
32 | http-form_data (~> 2.2)
33 | http-parser (~> 1.2.0)
34 | http-cookie (1.0.3)
35 | domain_name (~> 0.5)
36 | http-form_data (2.2.0)
37 | http-parser (1.2.1)
38 | ffi-compiler (>= 1.0, < 2.0)
39 | method_source (0.8.2)
40 | mini_portile2 (2.4.0)
41 | nokogiri (1.10.8)
42 | mini_portile2 (~> 2.4.0)
43 | oj (3.10.2)
44 | pry (0.10.4)
45 | coderay (~> 1.1.0)
46 | method_source (~> 0.8.1)
47 | slop (~> 3.4)
48 | public_suffix (3.0.3)
49 | rake (12.3.3)
50 | rspec (3.6.0)
51 | rspec-core (~> 3.6.0)
52 | rspec-expectations (~> 3.6.0)
53 | rspec-mocks (~> 3.6.0)
54 | rspec-core (3.6.0)
55 | rspec-support (~> 3.6.0)
56 | rspec-expectations (3.6.0)
57 | diff-lcs (>= 1.2.0, < 2.0)
58 | rspec-support (~> 3.6.0)
59 | rspec-mocks (3.6.0)
60 | diff-lcs (>= 1.2.0, < 2.0)
61 | rspec-support (~> 3.6.0)
62 | rspec-support (3.6.0)
63 | ruby-progressbar (1.8.1)
64 | safe_yaml (1.0.4)
65 | slop (3.6.0)
66 | unf (0.1.4)
67 | unf_ext
68 | unf_ext (0.0.7.6)
69 | webmock (3.0.1)
70 | addressable (>= 2.3.6)
71 | crack (>= 0.3.2)
72 | hashdiff
73 |
74 | PLATFORMS
75 | ruby
76 |
77 | DEPENDENCIES
78 | bundler (~> 1.15)
79 | fuubar
80 | goldfinger!
81 | pry (>= 0.10.3)
82 | rake
83 | rspec (>= 3.0)
84 | webmock
85 |
86 | BUNDLED WITH
87 | 1.16.1
88 |
--------------------------------------------------------------------------------
/spec/goldfinger/client_spec.rb:
--------------------------------------------------------------------------------
1 | describe Goldfinger::Client do
2 | context 'with HTTPS available' do
3 | describe '#finger' do
4 | before do
5 | stub_request(:get, 'https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no').to_return(body: fixture('quitter.no_.well-known_webfinger.json'), headers: { content_type: 'application/jrd+json' })
6 | end
7 |
8 | subject { Goldfinger::Client.new('acct:gargron@quitter.no') }
9 |
10 | it 'returns a result' do
11 | expect(subject.finger).to be_instance_of Goldfinger::Result
12 | end
13 |
14 | it 'performs a single HTTP request' do
15 | subject.finger
16 | expect(a_request(:get, 'https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no')).to have_been_made.once
17 | end
18 | end
19 | end
20 |
21 | context 'with only HTTP available' do
22 | describe '#finger' do
23 | before do
24 | stub_request(:get, 'https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no').to_raise(HTTP::Error)
25 | stub_request(:get, 'http://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no').to_return(body: fixture('quitter.no_.well-known_webfinger.json'), headers: { content_type: 'application/jrd+json' })
26 | end
27 |
28 | subject { Goldfinger::Client.new('acct:gargron@quitter.no') }
29 |
30 | it 'raises an error' do
31 | expect { subject.finger }.to raise_error HTTP::Error
32 | end
33 | end
34 | end
35 |
36 | context 'with XRD missing' do
37 | describe '#finger' do
38 | before do
39 | stub_request(:get, 'https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no').to_raise(HTTP::Error)
40 | stub_request(:get, 'http://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no').to_raise(HTTP::Error)
41 | stub_request(:get, 'https://quitter.no/.well-known/host-meta').to_raise(HTTP::Error)
42 | stub_request(:get, 'http://quitter.no/.well-known/host-meta').to_raise(HTTP::Error)
43 | end
44 |
45 | subject { Goldfinger::Client.new('acct:gargron@quitter.no') }
46 |
47 | it 'raises an error' do
48 | expect { subject.finger }.to raise_error(HTTP::Error)
49 | end
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/spec/fixtures/quitter.no_.well-known_webfinger.json:
--------------------------------------------------------------------------------
1 | {
2 | "subject": "acct:gargron@quitter.no",
3 | "aliases": [
4 | "https://quitter.no/user/7477",
5 | "https://quitter.no/gargron"
6 | ],
7 | "properties": {
8 | "http://webfinger.example/ns/name": "Bob Smith"
9 | },
10 | "links": [
11 | {
12 | "rel": "http://webfinger.net/rel/profile-page",
13 | "type": "text/html",
14 | "href": "https://quitter.no/gargron"
15 | },
16 | {
17 | "rel": "http://gmpg.org/xfn/11",
18 | "type": "text/html",
19 | "href": "https://quitter.no/gargron"
20 | },
21 | {
22 | "rel": "describedby",
23 | "type": "application/rdf+xml",
24 | "href": "https://quitter.no/gargron/foaf"
25 | },
26 | {
27 | "rel": "http://apinamespace.org/atom",
28 | "type": "application/atomsvc+xml",
29 | "href": "https://quitter.no/api/statusnet/app/service/gargron.xml"
30 | },
31 | {
32 | "rel": "http://apinamespace.org/twitter",
33 | "href": "https://quitter.no/api/"
34 | },
35 | {
36 | "rel": "http://specs.openid.net/auth/2.0/provider",
37 | "href": "https://quitter.no/gargron"
38 | },
39 | {
40 | "rel": "http://schemas.google.com/g/2010#updates-from",
41 | "type": "application/atom+xml",
42 | "href": "https://quitter.no/api/statuses/user_timeline/7477.atom"
43 | },
44 | {
45 | "rel": "magic-public-key",
46 | "href": "data:application/magic-public-key,RSA.1ZBkHTavLvxH3FzlKv4O6WtlILKRFfNami3_Rcu8EuogtXSYiS-bB6hElZfUCSHbC4uLemOA34PEhz__CDMozax1iI_t8dzjDnh1x0iFSup7pSfW9iXk_WU3Dm74yWWW2jildY41vWgrEstuQ1dJ8vVFfSJ9T_tO4c-T9y8vDI8=.AQAB"
47 | },
48 | {
49 | "rel": "salmon",
50 | "href": "https://quitter.no/main/salmon/user/7477"
51 | },
52 | {
53 | "rel": "http://salmon-protocol.org/ns/salmon-replies",
54 | "href": "https://quitter.no/main/salmon/user/7477"
55 | },
56 | {
57 | "rel": "http://salmon-protocol.org/ns/salmon-mention",
58 | "href": "https://quitter.no/main/salmon/user/7477"
59 | },
60 | {
61 | "rel": "http://ostatus.org/schema/1.0/subscribe",
62 | "template": "https://quitter.no/main/ostatussub?profile={uri}"
63 | },
64 | {
65 | "rel": "http://spec.example.net/photo/1.0",
66 | "type": "image/jpeg",
67 | "href": "http://photos.example.com/gpburdell.jpg",
68 | "titles": {
69 | "en": "User Photo",
70 | "de": "Benutzerfoto"
71 | },
72 | "properties": {
73 | "http://spec.example.net/created/1.0": "1970-01-01"
74 | }
75 | }
76 | ]
77 | }
78 |
--------------------------------------------------------------------------------
/spec/goldfinger/result_spec.rb:
--------------------------------------------------------------------------------
1 | describe Goldfinger::Result do
2 | shared_examples 'a working finger result' do
3 | subject { Goldfinger::Result.new(response) }
4 |
5 | describe '#links' do
6 | it 'returns a non-empty array' do
7 | expect(subject.links).to be_instance_of Array
8 | expect(subject.links).to_not be_empty
9 | end
10 | end
11 |
12 | describe '#link' do
13 | it 'returns a value for a given rel' do
14 | expect(subject.link('http://webfinger.net/rel/profile-page').href).to eql 'https://quitter.no/gargron'
15 | end
16 |
17 | it 'returns nil if no such link exists' do
18 | expect(subject.link('zzzz')).to be_nil
19 | end
20 |
21 | it 'returns titles map' do
22 | expect(subject.link('http://spec.example.net/photo/1.0').title('en')).to eql 'User Photo'
23 | end
24 |
25 | it 'returns a properties map' do
26 | expect(subject.link('http://spec.example.net/photo/1.0').property('http://spec.example.net/created/1.0')).to eql '1970-01-01'
27 | end
28 | end
29 |
30 | describe '#subject' do
31 | it 'returns the subject' do
32 | expect(subject.subject).to eql 'acct:gargron@quitter.no'
33 | end
34 | end
35 |
36 | describe '#aliases' do
37 | it 'returns a non-empty array' do
38 | expect(subject.aliases).to be_instance_of Array
39 | expect(subject.aliases).to_not be_empty
40 | end
41 | end
42 |
43 | describe '#properties' do
44 | it 'returns an array' do
45 | expect(subject.properties).to be_instance_of Array
46 | expect(subject.properties).to_not be_empty
47 | end
48 | end
49 |
50 | describe '#property' do
51 | it 'returns the value for a key' do
52 | expect(subject.property('http://webfinger.example/ns/name')).to eql 'Bob Smith'
53 | end
54 |
55 | it 'returns nil if no such property exists' do
56 | expect(subject.property('zzzz')).to be_nil
57 | end
58 | end
59 | end
60 |
61 | context 'when the input mime type is application/xrd+xml' do
62 | before do
63 | stub_request(:get, 'https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no').to_return(body: fixture('quitter.no_.well-known_webfinger.xml'), headers: { content_type: 'application/xrd+xml' })
64 | end
65 |
66 | let(:response) { HTTP.get('https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no') }
67 |
68 | it_behaves_like 'a working finger result'
69 | end
70 |
71 | context 'when the input mime type is application/jrd+json' do
72 | before do
73 | stub_request(:get, 'https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no').to_return(body: fixture('quitter.no_.well-known_webfinger.json'), headers: { content_type: 'application/jrd+json' })
74 | end
75 |
76 | let(:response) { HTTP.get('https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no') }
77 |
78 | it_behaves_like 'a working finger result'
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/lib/goldfinger/result.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'oj'
4 |
5 | module Goldfinger
6 | # @!attribute [r] subject
7 | # @return [String] URI that identifies the entity that the JRD describes.
8 | # @!attribute [r] aliases
9 | # @return [Array] Zero or more URI strings that identify the same entity as the "subject" URI.
10 | class Result
11 | MIME_TYPES = [
12 | 'application/jrd+json',
13 | 'application/json',
14 | 'application/xrd+xml',
15 | 'application/xml',
16 | 'text/xml',
17 | ].freeze
18 |
19 | attr_reader :subject, :aliases
20 |
21 | def initialize(response)
22 | @mime_type = response.mime_type
23 | @body = response.body
24 | @subject = nil
25 | @aliases = []
26 | @links = {}
27 | @properties = {}
28 |
29 | parse
30 | end
31 |
32 | # The "properties" object comprises zero or more name/value pairs whose
33 | # names are URIs (referred to as "property identifiers") and whose
34 | # values are strings or nil.
35 | # @see #property
36 | # @return [Array] Array form of the hash
37 | def properties
38 | @properties.to_a
39 | end
40 |
41 | # Returns a property for a key
42 | # @param key [String]
43 | # @return [String]
44 | def property(key)
45 | @properties[key]
46 | end
47 |
48 | # The "links" array has any number of member objects, each of which
49 | # represents a link.
50 | # @see #link
51 | # @return [Array] Array form of the hash
52 | def links
53 | @links.to_a
54 | end
55 |
56 | # Returns a key for a relation
57 | # @param key [String]
58 | # @return [Goldfinger::Link]
59 | def link(rel)
60 | @links[rel]
61 | end
62 |
63 | private
64 |
65 | def parse
66 | case @mime_type
67 | when 'application/jrd+json', 'application/json'
68 | parse_json
69 | when 'application/xrd+xml', 'application/xml', 'text/xml'
70 | parse_xml
71 | else
72 | raise Goldfinger::Error, "Invalid response mime type: #{@mime_type}"
73 | end
74 | end
75 |
76 | def parse_json
77 | json = Oj.load(@body.to_s, mode: :null)
78 |
79 | @subject = json['subject']
80 | @aliases = json['aliases'] || []
81 | @properties = json['properties'] || {}
82 |
83 | json['links'].each do |link|
84 | tmp = Hash[link.keys.map { |key| [key.to_sym, link[key]] }]
85 | @links[link['rel']] = Goldfinger::Link.new(tmp)
86 | end
87 | end
88 |
89 | def parse_xml
90 | xml = Nokogiri::XML(@body)
91 |
92 | @subject = xml.at_xpath('//xmlns:Subject').content
93 | @aliases = xml.xpath('//xmlns:Alias').map(&:content)
94 |
95 | properties = xml.xpath('/xmlns:XRD/xmlns:Property')
96 | properties.each { |prop| @properties[prop.attribute('type').value] = prop.attribute('nil') ? nil : prop.content }
97 |
98 | xml.xpath('//xmlns:Link').each do |link|
99 | rel = link.attribute('rel').value
100 | tmp = Hash[link.attributes.keys.map { |key| [key.to_sym, link.attribute(key).value] }]
101 |
102 | tmp[:titles] = {}
103 | tmp[:properties] = {}
104 |
105 | link.xpath('.//xmlns:Title').each { |title| tmp[:titles][title.attribute('lang').value] = title.content }
106 | link.xpath('.//xmlns:Property').each { |prop| tmp[:properties][prop.attribute('type').value] = prop.attribute('nil') ? nil : prop.content }
107 |
108 | @links[rel] = Goldfinger::Link.new(tmp)
109 | end
110 | end
111 | end
112 | end
113 |
--------------------------------------------------------------------------------