webmention 9.1.0 → 10.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84916e475f4ede9be2eb1cd988b5b4b54e0225b13dbfca4cdc6e4ea0c10ae174
4
- data.tar.gz: 2f6bbd1523b427a6a32297a86c52b35ea6c91ccff0569d46c80272c21e373c7b
3
+ metadata.gz: 04d6dfabcd47d82e0191f80b7dbbf7b0c78c29e019c1994514f3ac4eb96d025a
4
+ data.tar.gz: a340c6d249f9e16f453be9f1346518268dc2c66f2f85568c1bd8ae15158e5b12
5
5
  SHA512:
6
- metadata.gz: 75f2535f613d08e21149a6678a5cc22748346048b567caa977261c6ea9cc55ced8c421d08599a5780ed0bfa78b8091d901ed9b6cf9dd4ea96e5ba908dc5fe1bf
7
- data.tar.gz: eef43a1a408dabacabc445ec7c85c6ff19aaeec7f40de8897b94e38c31e20e511f9a5cd3ba3879d5152a3b82f66fd7256334fec213d1cab9697132a888416cb3
6
+ metadata.gz: 22d043cdb6500397afc4c5c9b52464ff5026937883fac4b272e277fbb6c208b1856254604b4effd60c87d94c7621550bf0f6103fd6593a6214b51e7f24317b0f
7
+ data.tar.gz: 7a71592e478ab9322c622115ec4327516a472cff31136b48b5b531b5eddc6bc193e5369964246583d9e25a1654f29f78e42e9504014c14937c09884a19abc8ee
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ -
2
+ CHANGELOG.md
3
+ CODE_OF_CONDUCT.md
4
+ CONTRIBUTING.md
5
+ LICENSE
6
+ USAGE.md
data/CONTRIBUTING.md CHANGED
@@ -8,12 +8,12 @@ There are a couple ways you can help improve webmention-client-ruby:
8
8
 
9
9
  ## Getting Started
10
10
 
11
- webmention-client-ruby is developed using Ruby 3.4 and is tested against additional Ruby versions using [GitHub Actions](https://github.com/indieweb/webmention-client-ruby/actions).
11
+ webmention-client-ruby is developed using Ruby 4.0 and is tested against additional Ruby versions using [GitHub Actions](https://github.com/indieweb/webmention-client-ruby/actions).
12
12
 
13
13
  > [!TIP]
14
14
  > This project is configured with a [Dev Container](https://containers.dev) which includes everything you'd need to contribute to webmention-client-ruby. If you use a supported code editor or IDE, you're encouraged to use the existing Dev Container setup.
15
15
 
16
- Before making changes to webmention-client-ruby, you'll want to install Ruby 3.4. Using a Ruby version managment tool like [rbenv](https://github.com/rbenv/rbenv), [chruby](https://github.com/postmodern/chruby), or [rvm](https://github.com/rvm/rvm) is recommended. Once you've installed Ruby 3.4 using your method of choice, install the project's gems by running:
16
+ Before making changes to webmention-client-ruby, you'll want to install Ruby 4.0. Using a Ruby version managment tool like [rv](https://github.com/spinel-coop/rv) or [rbenv](https://github.com/rbenv/rbenv) is recommended. Once you've installed Ruby 4.0 using your method of choice, install the project's gems by running:
17
17
 
18
18
  ```sh
19
19
  bundle install
data/README.md CHANGED
@@ -16,9 +16,9 @@ A Ruby gem for sending and verifying [Webmention](https://indieweb.org/Webmentio
16
16
 
17
17
  ## Getting Started
18
18
 
19
- Before installing and using webmention-client-ruby, you'll want to have [Ruby](https://www.ruby-lang.org) 2.7 (or newer) installed. Using a Ruby version managment tool like [rbenv](https://github.com/rbenv/rbenv), [chruby](https://github.com/postmodern/chruby), or [rvm](https://github.com/rvm/rvm) is recommended.
19
+ Before installing and using webmention-client-ruby, you'll want to have [Ruby](https://www.ruby-lang.org) 2.7 (or newer) installed. Using a Ruby version managment tool like [rv](https://github.com/spinel-coop/rv) or [rbenv](https://github.com/rbenv/rbenv) is recommended.
20
20
 
21
- webmention-client-ruby is developed using Ruby 3.4 and is tested against additional Ruby versions using [GitHub Actions](https://github.com/indieweb/webmention-client-ruby/actions).
21
+ webmention-client-ruby is developed using Ruby 4.0 and is tested against additional Ruby versions using [GitHub Actions](https://github.com/indieweb/webmention-client-ruby/actions).
22
22
 
23
23
  ## Installation
24
24
 
@@ -36,7 +36,7 @@ See [USAGE.md](USAGE.md) for documentation of webmention-client-ruby's features.
36
36
 
37
37
  ## Contributing
38
38
 
39
- See [CONTRIBUTING.md](https://github.com/indieweb/webmention-client-ruby/blob/main/CONTRIBUTING.md) for more on how to contribute to webmention-client-ruby. Your help is greatly appreciated!
39
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for more on how to contribute to webmention-client-ruby. Your help is greatly appreciated!
40
40
 
41
41
  By contributing to and participating in the development of webmention-client-ruby, you acknowledge that you have read and agree to the [IndieWeb Code of Conduct](https://indieweb.org/code-of-conduct).
42
42
 
data/USAGE.md CHANGED
@@ -29,7 +29,7 @@ response.class
29
29
  #=> Webmention::ErrorResponse
30
30
 
31
31
  response.message
32
- #=> "No webmention endpoint found for target URL https://aaronpk.example/post/100"
32
+ #=> "No Webmention endpoint found for target URL https://aaronpk.example/post/100"
33
33
  ```
34
34
 
35
35
  A `Webmention::Response` will be returned in all other cases.
@@ -147,6 +147,4 @@ verification.vouch_mentions_source?
147
147
 
148
148
  ## Exception Handling
149
149
 
150
- webmention-client-ruby avoids raising exceptions when making HTTP requests. As noted above, a `Webmention::ErrorResponse` should be returned in cases where an HTTP request triggers an exception.
151
-
152
- When crawling the supplied URL, `Webmention.mentioned_urls` _may_ raise a `NoMethodError` if a `Webmention::ErrorResponse` is returned, or the response is of an unsupported MIME type.
150
+ webmention-client-ruby avoids raising exceptions when making HTTP requests. As noted above, a `Webmention::ErrorResponse` should be returned in cases where an HTTP request triggers an exception. `Webmention.mentioned_urls` will raise a `Burly::UnsupportedMimeType` exception when the response is of an unsupported MIME type, though.
@@ -1,25 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Webmention
4
+ # A client interface to send and verify Webmention notifications.
4
5
  class Client
5
- @registered_parsers = {}
6
-
7
- class << self
8
- # @api private
9
- attr_reader :registered_parsers
10
- end
11
-
12
6
  # @return [Url]
13
7
  attr_reader :source_url
14
8
 
15
9
  # @return [Url]
16
10
  attr_reader :vouch_url
17
11
 
18
- # @api private
19
- def self.register_parser(klass)
20
- klass.mime_types.each { |mime_type| @registered_parsers[mime_type] = klass }
21
- end
22
-
23
12
  # Create a new {Client}.
24
13
  #
25
14
  # @example
@@ -28,52 +17,47 @@ module Webmention
28
17
  # @example
29
18
  # Webmention::Client.new("https://jgarber.example/posts/100", vouch: "https://tantek.example/notes/1")
30
19
  #
20
+ # @see https://indieweb.org/Vouch
21
+ #
31
22
  # @param source [String, HTTP::URI, #to_s]
32
23
  # An absolute URL representing a source document.
33
24
  # @param vouch [String, HTTP::URI, #to_s]
34
25
  # An absolute URL representing a document vouching for the source document.
35
- # See https://indieweb.org/Vouch for additional details.
36
26
  def initialize(source, vouch: nil)
37
27
  @source_url = Url.new(source)
38
28
  @vouch_url = Url.new(vouch)
39
29
  end
40
30
 
41
- # :nocov:
42
- # @return [String]
31
+ # @!visibility private
32
+ # simplecov:disable
43
33
  def inspect
44
34
  "#<#{self.class}:#{format("%#0x", object_id)} " \
45
35
  "source_url: #{source_url} " \
46
36
  "vouch_url: #{vouch_url}>"
47
37
  end
48
- # :nocov:
38
+ # simplecov:enable
49
39
 
50
- # Retrieve unique URLs mentioned by this client's source URL.
40
+ # Unique URLs mentioned by {source_url}.
51
41
  #
52
42
  # @example
53
43
  # client = Webmention::Client.new("https://jgarber.example/posts/100")
54
44
  # client.mentioned_urls
55
45
  #
56
- # @raise [NoMethodError]
57
- # Raised when response is an {ErrorResponse} or response is of an
58
- # unsupported MIME type.
46
+ # @raise [Burly::UnsupportedMimeType]
47
+ # Raised when response is of an unsupported MIME type.
59
48
  #
60
49
  # @return [Array<String>]
61
50
  def mentioned_urls
62
51
  response = source_url.response
63
52
 
64
- urls = Set.new(
65
- self.class
66
- .registered_parsers[response.mime_type]
67
- .new(response.body, response.uri)
68
- .results
69
- )
70
-
71
- urls.reject! { |url| url.match(/^#{response.uri}(?:#.*)?$/) }
72
-
73
- urls.to_a.sort
53
+ response
54
+ .mentioned_urls
55
+ .lazy
56
+ .reject { |url| url.match(/^#{response.uri}(?:#.*)?$/) }
57
+ .sort
74
58
  end
75
59
 
76
- # Send a webmention from this client's source URL to a target URL.
60
+ # Send a webmention from this client's {source_url} to a target URL.
77
61
  #
78
62
  # @example
79
63
  # client = Webmention::Client.new("https://jgarber.example/posts/100")
@@ -85,20 +69,20 @@ module Webmention
85
69
  # @return [Response, ErrorResponse]
86
70
  def send_webmention(target)
87
71
  target_url = Url.new(target)
72
+ endpoint = target_url.webmention_endpoint
73
+ response = target_url.response
88
74
 
89
75
  # A Webmention endpoint exists. Send the request and return the response.
90
- if target_url.webmention_endpoint?
91
- return Request.post(target_url.webmention_endpoint, **request_options_for(target))
92
- end
76
+ return Request.new(:post, endpoint, **request_options_for(target)).perform unless endpoint.nil?
93
77
 
94
78
  # An error was encountered fetching the target URL. Return the response.
95
- return target_url.response unless target_url.response.ok?
79
+ return response unless response.ok?
96
80
 
97
81
  # No Webmention endpoint exists. Return a new ErrorResponse.
98
- ErrorResponse.new("No webmention endpoint found for target URL #{target}", target_url.response.request)
82
+ ErrorResponse.new("No Webmention endpoint found for target URL #{target}", response.request)
99
83
  end
100
84
 
101
- # Send webmentions from this client's source URL to multiple target URLs.
85
+ # Send webmentions from {source_url} to multiple target URLs.
102
86
  #
103
87
  # @example
104
88
  # client = Webmention::Client.new("https://jgarber.example/posts/100")
@@ -113,10 +97,9 @@ module Webmention
113
97
  targets.map { |target| send_webmention(target) }
114
98
  end
115
99
 
116
- # Verify that this client's source URL links to a target URL.
100
+ # Verify that {source_url} links to a target URL.
117
101
  #
118
- # @param target [String, HTTP::URI, #to_s]
119
- # An absolute URL representing a target document.
102
+ # @param target (see #send_webmention)
120
103
  #
121
104
  # @raise (see Client#mentioned_urls)
122
105
  #
@@ -127,7 +110,7 @@ module Webmention
127
110
 
128
111
  private
129
112
 
130
- # @param target [String, HTTP::URI, #to_s]
113
+ # @param target (see #send_webmention)
131
114
  #
132
115
  # @return [Hash{Symbol => String}]
133
116
  def request_options_for(target)
@@ -140,7 +123,7 @@ module Webmention
140
123
  opts.transform_values! { |value| value.to_s.strip }
141
124
  opts.reject! { |_, value| value.empty? }
142
125
 
143
- opts
126
+ { form: opts }
144
127
  end
145
128
  end
146
129
  end
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Webmention
4
+ # An interface to instances of the {Request} class.
5
+ #
6
+ # An {ErrorResponse} represents {Request}s that generated errors (e.g.
7
+ # connection error, SSL error) or that could not locate a Webmention endpoint.
8
+ # The nature of the error is captured in the {#message} instance method.
4
9
  class ErrorResponse
5
10
  # @return [String]
6
11
  attr_reader :message
@@ -10,10 +15,7 @@ module Webmention
10
15
 
11
16
  # Create a new {ErrorResponse}.
12
17
  #
13
- # Instances of this class represent HTTP requests that generated errors
14
- # (e.g. connection error, SSL error) or that could not locate a Webmention
15
- # endpoint. The nature of the error is captured in the {#message} instance
16
- # method.
18
+ # @api private
17
19
  #
18
20
  # @param message [String]
19
21
  # @param request [Request]
@@ -22,15 +24,24 @@ module Webmention
22
24
  @request = request
23
25
  end
24
26
 
25
- # :nocov:
26
- # @return [String]
27
+ # @!visibility private
28
+ # simplecov:disable
27
29
  def inspect
28
30
  "#<#{self.class}:#{format("%#0x", object_id)} " \
29
31
  "message: #{message}>"
30
32
  end
31
- # :nocov:
33
+ # simplecov:enable
32
34
 
33
- # @return [Boolean]
35
+ # An {ErrorResponse} has no mentioned URLs.
36
+ #
37
+ # @return [Set]
38
+ def mentioned_urls
39
+ Set.new
40
+ end
41
+
42
+ # An {ErrorResponse} is never okay!
43
+ #
44
+ # @return [false]
34
45
  def ok?
35
46
  false
36
47
  end
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Webmention
4
+ # An interface for executing HTTP requests and generating {Response}s and
5
+ # {ErrorResponse}s.
6
+ #
7
+ # @api private
4
8
  class Request
5
9
  # Set defaults derived from Webmention specification examples.
6
10
  #
@@ -31,64 +35,45 @@ module Webmention
31
35
  # @return [Hash]
32
36
  attr_reader :options
33
37
 
34
- # Send an HTTP GET request to the supplied URL.
38
+ # Create a new {Request}.
35
39
  #
36
40
  # @example
37
- # Webmention::Request.get("https://jgarber.example/posts/100")
38
- #
39
- # @param url [String]
40
- #
41
- # @return [Response, ErrorResponse]
42
- def self.get(url)
43
- new(:get, url).perform
44
- end
45
-
46
- # Send an HTTP POST request with form-encoded data to the supplied URL.
41
+ # Webmention::Request.new(:get, "https://jgarber.example/posts/100")
47
42
  #
48
43
  # @example
49
- # Webmention::Request.post(
44
+ # Webmention::Request.new(
45
+ # :post,
50
46
  # "https://aaronpk.example/webmention",
51
- # source: "https://jgarber.examples/posts/100",
52
- # target: "https://aaronpk.example/notes/1",
53
- # vouch: "https://tantek.example/notes/1"
47
+ # form: {
48
+ # source: "https://jgarber.examples/posts/100",
49
+ # target: "https://aaronpk.example/notes/1",
50
+ # vouch: "https://tantek.example/notes/1",
51
+ # },
54
52
  # )
55
53
  #
56
- # @param url [String]
57
- # @param options [Hash{Symbol => String}]
58
- # @option options [String] :source
59
- # An absolute URL representing a source document.
60
- # @option options [String] :target
61
- # An absolute URL representing a target document.
62
- # @option options [String] :vouch
63
- # An absolute URL representing a document vouching for the source document.
64
- # See https://indieweb.org/Vouch for additional details.
65
- #
66
- # @return [Response, ErrorResponse]
67
- def self.post(url, **options)
68
- new(:post, url, form: options.slice(:source, :target, :vouch)).perform
69
- end
70
-
71
- # Create a new {Request}.
54
+ # @see https://indieweb.org/Vouch
72
55
  #
73
56
  # @param method [Symbol]
74
57
  # @param url [String]
75
- # @param options [Hash{Symbol => String}]
58
+ # @param options [Hash{Symbol => Hash{Symbol => String}}]
59
+ # @option options [Hash{Symbol => String}] :form
60
+ # A Hash of options to serialize as form-encoded.
76
61
  def initialize(method, url, **options)
77
62
  @method = method.to_sym
78
63
  @uri = HTTP::URI.parse(url.to_s)
79
64
  @options = options
80
65
  end
81
66
 
82
- # :nocov:
83
- # @return [String]
67
+ # @!visibility private
68
+ # simplecov:disable
84
69
  def inspect
85
70
  "#<#{self.class}:#{format("%#0x", object_id)} " \
86
71
  "method: #{method.upcase}, " \
87
72
  "url: #{uri}>"
88
73
  end
89
- # :nocov:
74
+ # simplecov:enable
90
75
 
91
- # Submit the {Request}.
76
+ # Execute the {Request}.
92
77
  #
93
78
  # @return [Response, ErrorResponse]
94
79
  def perform
@@ -1,33 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Webmention
4
+ # An interface to instances of the {Request} and `HTTP::Response` classes.
5
+ #
6
+ # A {Response} represents the results of a successful {Request}.
4
7
  class Response
5
8
  extend Forwardable
6
9
 
10
+ # Consider the first h-entry to be the authoritative content container when
11
+ # parsing a document for mentioned URLs. Fall back to the `<body>` element
12
+ # when no h-entry is found.
13
+ #
14
+ # @see http://microformats.org/wiki/h-entry
15
+ PARSE_CONTEXT = [".h-entry .e-content", ".h-entry", "body"].freeze
16
+
17
+ URI_REGEXP = URI::RFC2396_Parser.new.make_regexp(["http", "https"]).freeze
18
+
19
+ public_constant :PARSE_CONTEXT
20
+ private_constant :URI_REGEXP
21
+
22
+ # The {Request} associated with this {Response}.
23
+ #
7
24
  # @return [Request]
8
25
  attr_reader :request
9
26
 
10
- # @!method
27
+ # @!method headers
11
28
  # @return [HTTP::Headers]
12
29
  def_delegator :@response, :headers
13
30
 
14
- # @!method
31
+ # @!method body
15
32
  # @return [HTTP::Response::Body]
16
33
  def_delegator :@response, :body
17
34
 
18
- # @!method
35
+ # @!method code
19
36
  # @return [Integer]
20
37
  def_delegator :@response, :code
21
38
 
22
- # @!method
39
+ # @!method reason
23
40
  # @return [String]
24
41
  def_delegator :@response, :reason
25
42
 
26
- # !@method
43
+ # @!method mime_type
27
44
  # @return [String]
28
45
  def_delegator :@response, :mime_type
29
46
 
30
- # !@method
47
+ # @!method uri
31
48
  # @return [HTTP::URI]
32
49
  def_delegator :@response, :uri
33
50
 
@@ -37,6 +54,8 @@ module Webmention
37
54
  # of which may be accessed using the delegated {#code} and {#reason}
38
55
  # instance methods.
39
56
  #
57
+ # @api private
58
+ #
40
59
  # @param response [HTTP::Response]
41
60
  # @param request [Request]
42
61
  def initialize(response, request)
@@ -44,17 +63,45 @@ module Webmention
44
63
  @request = request
45
64
  end
46
65
 
47
- # :nocov:
48
- # @return [String]
66
+ # @!visibility private
67
+ # simplecov:disable
49
68
  def inspect
50
69
  "#<#{self.class}:#{format("%#0x", object_id)} " \
51
70
  "code: #{code.inspect}, " \
52
71
  "reason: #{reason}, " \
53
72
  "url: #{request.uri}>"
54
73
  end
55
- # :nocov:
74
+ # simplecov:enable
56
75
 
57
- # @return [Boolean]
76
+ # Unique host names parsed from the response body.
77
+ #
78
+ # The returned `Set` contains the unique host names ("domain names") derived
79
+ # from the `Set` of {mentioned_urls}. Order is not guaranteed.
80
+ #
81
+ # @return [Set<String>]
82
+ def mentioned_hosts
83
+ @mentioned_hosts ||= Set.new(mentioned_urls.map { |url| HTTP::URI.parse(url).host })
84
+ end
85
+
86
+ # Unique mentioned URLs parsed from the response body.
87
+ #
88
+ # The returned `Set` contains the unique URLs parsed from the
89
+ # {PARSE_CONTEXT}. Order is not guaranteed.
90
+ #
91
+ # @return [Set<String>]
92
+ def mentioned_urls
93
+ @mentioned_urls ||=
94
+ Set.new(
95
+ Burly
96
+ .parse(@response.body, context: PARSE_CONTEXT, mime_type: @response.mime_type)
97
+ .map { |url| @response.uri.join(url).to_s }
98
+ .grep(URI_REGEXP)
99
+ )
100
+ end
101
+
102
+ # A {Response} is always okay!
103
+ #
104
+ # @return [true]
58
105
  def ok?
59
106
  true
60
107
  end
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Webmention
4
+ # An interface representing a parsed URL and its {Response}.
5
+ #
6
+ # @api private
4
7
  class Url
5
8
  extend Forwardable
6
9
 
@@ -18,27 +21,22 @@ module Webmention
18
21
  @uri = HTTP::URI.parse(url.to_s)
19
22
  end
20
23
 
21
- # :nocov:
22
- # @return [String]
24
+ # @!visibility private
25
+ # simplecov:disable
23
26
  def inspect
24
27
  "#<#{self.class}:#{format("%#0x", object_id)} " \
25
28
  "uri: #{uri}>"
26
29
  end
27
- # :nocov:
30
+ # simplecov:enable
28
31
 
29
32
  # @return [Response, ErrorResponse]
30
33
  def response
31
- @response ||= Request.get(uri)
34
+ @response ||= Request.new(:get, uri).perform
32
35
  end
33
36
 
34
37
  # @return [String, nil]
35
38
  def webmention_endpoint
36
39
  @webmention_endpoint ||= IndieWeb::Endpoints::Parser.new(response).to_h[:webmention] if response.ok?
37
40
  end
38
-
39
- # @return [Boolean]
40
- def webmention_endpoint?
41
- !webmention_endpoint.nil?
42
- end
43
41
  end
44
42
  end
@@ -1,86 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Webmention
4
+ # Verify that a source URL links to a target URL.
4
5
  class Verification
5
6
  # Create a new {Verification}.
6
7
  #
8
+ # @api private
9
+ #
10
+ # @see https://indieweb.org/Vouch
11
+ #
7
12
  # @param source_url [Url]
8
13
  # An {URL} representing a source document.
9
14
  # @param target_url [Url]
10
15
  # An {URL} representing a target document.
11
16
  # @param vouch_url [Url]
12
17
  # An {URL} representing a document vouching for the source document.
13
- # See https://indieweb.org/Vouch for additional details.
14
18
  def initialize(source_url, target_url, vouch_url: nil)
15
19
  @source_url = source_url
16
20
  @target_url = target_url
17
21
  @vouch_url = vouch_url
18
22
  end
19
23
 
20
- # :nocov:
21
- # @return [String]
24
+ # @!visibility private
25
+ # simplecov:disable
22
26
  def inspect
23
27
  "#<#{self.class}:#{format("%#0x", object_id)} " \
24
- "source_url: #{source_url} " \
25
- "target_url: #{target_url} " \
26
- "vouch_url: #{vouch_url}>"
28
+ "source_url: #{@source_url} " \
29
+ "target_url: #{@target_url} " \
30
+ "vouch_url: #{@vouch_url}>"
27
31
  end
28
- # :nocov:
32
+ # simplecov:enable
29
33
 
30
34
  # @return [Boolean]
31
35
  def source_mentions_target?
32
- @source_mentions_target ||= mentioned_urls(source_url.response).any?(target_url.to_s)
36
+ @source_mentions_target ||= @source_url.response.mentioned_urls.any?(@target_url.to_s)
33
37
  end
34
38
 
35
39
  # @return [Boolean]
36
40
  def verified?
37
- return source_mentions_target? unless verify_vouch?
41
+ return source_mentions_target? && vouch_mentions_source? if verify_vouch?
38
42
 
39
- source_mentions_target? && vouch_mentions_source?
43
+ source_mentions_target?
40
44
  end
41
45
 
42
46
  # @return [Boolean]
43
47
  def verify_vouch?
44
- !vouch_url.nil? && !vouch_url.to_s.strip.empty?
48
+ !@vouch_url.to_s.strip.empty?
45
49
  end
46
50
 
47
51
  # @return [Boolean]
48
52
  def vouch_mentions_source?
49
- @vouch_mentions_source ||=
50
- verify_vouch? && mentioned_domains(vouch_url.response).any?(source_url.uri.host)
51
- end
52
-
53
- private
54
-
55
- # @return [Url]
56
- attr_reader :source_url
57
-
58
- # @return [Url]
59
- attr_reader :target_url
60
-
61
- # @return [Url]
62
- attr_reader :vouch_url
63
-
64
- # @param response [Response]
65
- #
66
- # @raise (see Client#mentioned_urls)
67
- #
68
- # @return [Array<String>]
69
- def mentioned_domains(response)
70
- Set.new(mentioned_urls(response).map { |url| HTTP::URI.parse(url).host }).to_a
71
- end
72
-
73
- # @param response [Response]
74
- #
75
- # @raise (see Client#mentioned_urls)
76
- #
77
- # @return [Array<String>]
78
- def mentioned_urls(response)
79
- Set.new(
80
- Client.registered_parsers[response.mime_type]
81
- .new(response.body, response.uri)
82
- .results
83
- ).to_a
53
+ @vouch_mentions_source ||= verify_vouch? && @vouch_url.response.mentioned_hosts.any?(@source_url.uri.host)
84
54
  end
85
55
  end
86
56
  end
data/lib/webmention.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "json"
4
4
 
5
+ require "burly"
5
6
  require "http"
6
7
  require "indieweb/endpoints"
7
8
  require "nokogiri"
@@ -13,11 +14,9 @@ require_relative "webmention/response"
13
14
  require_relative "webmention/error_response"
14
15
  require_relative "webmention/verification"
15
16
 
16
- require_relative "webmention/parser"
17
- require_relative "webmention/parsers/html_parser"
18
- require_relative "webmention/parsers/json_parser"
19
- require_relative "webmention/parsers/plaintext_parser"
20
-
17
+ # Send and verify Webmention notifications.
18
+ #
19
+ # @see https://indieweb.org/Webmention
21
20
  module Webmention
22
21
  # Retrieve unique URLs mentioned by the provided URL.
23
22
  #
@@ -26,11 +25,9 @@ module Webmention
26
25
  #
27
26
  # @param url [String, HTTP::URI, #to_s] An absolute URL.
28
27
  #
29
- # @raise [NoMethodError]
30
- # Raised when response is an {ErrorResponse} or response is of an
31
- # unsupported MIME type.
28
+ # @raise (see Client#mentioned_urls)
32
29
  #
33
- # @return [Array<String>]
30
+ # @return (see Client#mentioned_urls)
34
31
  def self.mentioned_urls(url)
35
32
  Client.new(url).mentioned_urls
36
33
  end
@@ -47,15 +44,13 @@ module Webmention
47
44
  # target = "https://aaronpk.example/notes/1"
48
45
  # Webmention.send_webmention(source, target, vouch: "https://tantek.example/notes/1")
49
46
  #
50
- # @param source [String, HTTP::URI, #to_s]
51
- # An absolute URL representing a source document.
52
- # @param target [String, HTTP::URI, #to_s]
53
- # An absolute URL representing a target document.
54
- # @param vouch [String, HTTP::URI, #to_s]
55
- # An absolute URL representing a document vouching for the source document.
56
- # See https://indieweb.org/Vouch for additional details.
47
+ # @see https://indieweb.org/Vouch
57
48
  #
58
- # @return [Response, ErrorResponse]
49
+ # @param source (see Client#initialize)
50
+ # @param target (see Client#send_webmention)
51
+ # @param vouch (see Client#initialize)
52
+ #
53
+ # @return (see Client#send_webmention)
59
54
  def self.send_webmention(source, target, vouch: nil)
60
55
  Client.new(source, vouch: vouch).send_webmention(target)
61
56
  end
@@ -72,13 +67,9 @@ module Webmention
72
67
  # targets = ["https://aaronpk.example/notes/1", "https://adactio.example/notes/1"]
73
68
  # Webmention.send_webmentions(source, targets, vouch: "https://tantek.example/notes/1")
74
69
  #
75
- # @param source [String, HTTP::URI, #to_s]
76
- # An absolute URL representing a source document.
77
- # @param targets [Array<String, HTTP::URI, #to_s>]
78
- # An array of absolute URLs representing multiple target documents.
79
- # @param vouch [String, HTTP::URI, #to_s]
80
- # An absolute URL representing a document vouching for the source document.
81
- # See https://indieweb.org/Vouch for additional details.
70
+ # @see https://indieweb.org/Vouch
71
+ #
72
+ # @param (see .send_webmention)
82
73
  #
83
74
  # @return [Array<Response, ErrorResponse>]
84
75
  def self.send_webmentions(source, *targets, vouch: nil)
@@ -87,7 +78,7 @@ module Webmention
87
78
 
88
79
  # Verify that a source URL links to a target URL.
89
80
  #
90
- # @param (see Webmention.send_webmention)
81
+ # @param (see .send_webmention)
91
82
  #
92
83
  # @raise (see Client#mentioned_urls)
93
84
  #
data/webmention.gemspec CHANGED
@@ -4,7 +4,7 @@ Gem::Specification.new do |spec|
4
4
  spec.required_ruby_version = ">= 2.7"
5
5
 
6
6
  spec.name = "webmention"
7
- spec.version = "9.1.0"
7
+ spec.version = "10.0.1"
8
8
  spec.authors = ["Jason Garber"]
9
9
  spec.email = ["jason@sixtwothree.org"]
10
10
 
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
14
14
  spec.license = "Apache-2.0"
15
15
 
16
16
  spec.files = Dir["lib/**/*"].reject { |f| File.directory?(f) }
17
- spec.files += ["LICENSE", "CHANGELOG.md", "CONTRIBUTING.md", "README.md", "USAGE.md"]
17
+ spec.files += [".yardopts", "LICENSE", "CHANGELOG.md", "CONTRIBUTING.md", "README.md", "USAGE.md"]
18
18
  spec.files += ["webmention.gemspec"]
19
19
 
20
20
  spec.require_paths = ["lib"]
@@ -28,6 +28,7 @@ Gem::Specification.new do |spec|
28
28
  "source_code_uri" => "#{spec.homepage}/tree/v#{spec.version}",
29
29
  }
30
30
 
31
+ spec.add_dependency "burly", "~> 0.4"
31
32
  spec.add_dependency "http", ">= 5.3"
32
33
  spec.add_dependency "indieweb-endpoints", "~> 10.1"
33
34
  spec.add_dependency "nokogiri", ">= 1.14"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: webmention
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.1.0
4
+ version: 10.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Garber
@@ -9,6 +9,20 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: burly
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.4'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.4'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: http
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -58,6 +72,7 @@ executables: []
58
72
  extensions: []
59
73
  extra_rdoc_files: []
60
74
  files:
75
+ - ".yardopts"
61
76
  - CHANGELOG.md
62
77
  - CONTRIBUTING.md
63
78
  - LICENSE
@@ -66,10 +81,6 @@ files:
66
81
  - lib/webmention.rb
67
82
  - lib/webmention/client.rb
68
83
  - lib/webmention/error_response.rb
69
- - lib/webmention/parser.rb
70
- - lib/webmention/parsers/html_parser.rb
71
- - lib/webmention/parsers/json_parser.rb
72
- - lib/webmention/parsers/plaintext_parser.rb
73
84
  - lib/webmention/request.rb
74
85
  - lib/webmention/response.rb
75
86
  - lib/webmention/url.rb
@@ -80,11 +91,11 @@ licenses:
80
91
  - Apache-2.0
81
92
  metadata:
82
93
  bug_tracker_uri: https://github.com/indieweb/webmention-client-ruby/issues
83
- changelog_uri: https://github.com/indieweb/webmention-client-ruby/releases/tag/v9.1.0
84
- documentation_uri: https://rubydoc.info/gems/webmention/9.1.0
94
+ changelog_uri: https://github.com/indieweb/webmention-client-ruby/releases/tag/v10.0.1
95
+ documentation_uri: https://rubydoc.info/gems/webmention/10.0.1
85
96
  homepage_uri: https://github.com/indieweb/webmention-client-ruby
86
97
  rubygems_mfa_required: 'true'
87
- source_code_uri: https://github.com/indieweb/webmention-client-ruby/tree/v9.1.0
98
+ source_code_uri: https://github.com/indieweb/webmention-client-ruby/tree/v10.0.1
88
99
  rdoc_options: []
89
100
  require_paths:
90
101
  - lib
@@ -99,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
99
110
  - !ruby/object:Gem::Version
100
111
  version: '0'
101
112
  requirements: []
102
- rubygems_version: 4.0.10
113
+ rubygems_version: 4.0.20
103
114
  specification_version: 4
104
115
  summary: A Ruby gem for sending and verifying Webmention notifications.
105
116
  test_files: []
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Webmention
4
- # @api private
5
- class Parser
6
- URI_PARSER = defined?(URI::RFC2396_PARSER) ? URI::RFC2396_PARSER : URI::Generic::DEFAULT_PARSER
7
-
8
- URI_REGEXP = URI_PARSER.make_regexp(["http", "https"]).freeze
9
-
10
- public_constant :URI_REGEXP
11
-
12
- class << self
13
- # @return [Array<String>]
14
- attr_reader :mime_types
15
- end
16
-
17
- # @param response_body [HTTP::Response::Body, String, #to_s]
18
- # @param response_uri [String, HTTP::URI, #to_s]
19
- def initialize(response_body, response_uri)
20
- @response_body = response_body.to_s
21
- @response_uri = HTTP::URI.parse(response_uri.to_s)
22
- end
23
-
24
- private
25
-
26
- # @return [String]
27
- attr_reader :response_body
28
-
29
- # @return [HTTP::URI]
30
- attr_reader :response_uri
31
- end
32
- end
@@ -1,73 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Webmention
4
- module Parsers
5
- # @api private
6
- class HtmlParser < Parser
7
- @mime_types = ["text/html"]
8
-
9
- Client.register_parser(self)
10
-
11
- HTML_ATTRIBUTES_MAP = {
12
- "cite" => ["blockquote", "del", "ins", "q"],
13
- "data" => ["object"],
14
- "href" => ["a", "area"],
15
- "poster" => ["video"],
16
- "src" => ["audio", "embed", "img", "source", "track", "video"],
17
- "srcset" => ["img", "source"],
18
- }.freeze
19
-
20
- CSS_SELECTORS_ARRAY = HTML_ATTRIBUTES_MAP
21
- .flat_map { |attribute, names| names.map { |name| "#{name}[#{attribute}]" } }.freeze
22
-
23
- ROOT_NODE_SELECTORS_ARRAY = [".h-entry .e-content", ".h-entry", "body"].freeze
24
-
25
- private_constant :HTML_ATTRIBUTES_MAP
26
- private_constant :CSS_SELECTORS_ARRAY
27
- private_constant :ROOT_NODE_SELECTORS_ARRAY
28
-
29
- # @return [Array<String>] An array of absolute URLs.
30
- def results
31
- @results ||=
32
- extract_urls_from(*url_attributes)
33
- .map { |url| response_uri.join(url).to_s }
34
- .grep(Parser::URI_REGEXP)
35
- end
36
-
37
- private
38
-
39
- # @return [Nokogiri::HTML5::Document]
40
- def doc
41
- Nokogiri.HTML5(response_body)
42
- end
43
-
44
- # @param attributes [Array<Nokogiri::XML::Attr>]
45
- #
46
- # @return [Array<String>]
47
- def extract_urls_from(*attributes)
48
- attributes.flat_map do |attribute|
49
- if attribute.name == "srcset"
50
- attribute.value.split(",").map { |value| value.strip.match(/^\S+/).to_s }
51
- else
52
- attribute.value
53
- end
54
- end
55
- end
56
-
57
- # @return [Nokogiri::XML::Element]
58
- def root_node
59
- doc.at_css(*ROOT_NODE_SELECTORS_ARRAY)
60
- end
61
-
62
- # @return [Array<Nokogiri::XML::Attr>]
63
- def url_attributes
64
- url_nodes.flat_map(&:attribute_nodes).select { |attribute| HTML_ATTRIBUTES_MAP.key?(attribute.name) }
65
- end
66
-
67
- # @return [Nokogiri::XML::NodeSet]
68
- def url_nodes
69
- root_node.css(*CSS_SELECTORS_ARRAY)
70
- end
71
- end
72
- end
73
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Webmention
4
- module Parsers
5
- # @api private
6
- class JsonParser < Parser
7
- @mime_types = ["application/json"]
8
-
9
- Client.register_parser(self)
10
-
11
- # @return [Array<String>] An array of absolute URLs.
12
- def results
13
- @results ||= extract_urls_from(JSON.parse(response_body))
14
- end
15
-
16
- private
17
-
18
- # @param objs [Array<Hash, Array, String, Integer, Boolean, nil>]
19
- #
20
- # @return [Array<String>]
21
- def extract_urls_from(*objs)
22
- objs.flat_map do |obj|
23
- return obj.flat_map { |value| extract_urls_from(value) }.compact if obj.is_a?(Array)
24
- return extract_urls_from(obj.values) if obj.is_a?(Hash)
25
-
26
- obj if obj.is_a?(String) && obj.match?(Parser::URI_REGEXP)
27
- end
28
- end
29
- end
30
- end
31
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Webmention
4
- module Parsers
5
- # @api private
6
- class PlaintextParser < Parser
7
- @mime_types = ["text/plain"]
8
-
9
- Client.register_parser(self)
10
-
11
- # @return [Array<String>] An array of absolute URLs.
12
- def results
13
- @results ||= URI_PARSER.extract(response_body, ["http", "https"])
14
- end
15
- end
16
- end
17
- end