bridgetown-webfinger 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +15 -0
- data/CONTRIBUTING.md +55 -0
- data/LICENSE.md +22 -0
- data/README.md +125 -0
- data/bridgetown-webfinger.gemspec +35 -0
- data/lib/bridgetown/webfinger/alias.rb +29 -0
- data/lib/bridgetown/webfinger/builder.rb +133 -0
- data/lib/bridgetown/webfinger/href.rb +29 -0
- data/lib/bridgetown/webfinger/initializer.rb +23 -0
- data/lib/bridgetown/webfinger/jrd.rb +164 -0
- data/lib/bridgetown/webfinger/link.rb +118 -0
- data/lib/bridgetown/webfinger/link_relation_type.rb +159 -0
- data/lib/bridgetown/webfinger/logging.rb +38 -0
- data/lib/bridgetown/webfinger/model.rb +13 -0
- data/lib/bridgetown/webfinger/parameters.rb +166 -0
- data/lib/bridgetown/webfinger/properties.rb +44 -0
- data/lib/bridgetown/webfinger/titles.rb +40 -0
- data/lib/bridgetown/webfinger/uri/acct.rb +138 -0
- data/lib/bridgetown/webfinger/version.rb +8 -0
- data/lib/bridgetown-webfinger.rb +49 -0
- data/lib/roda/plugins/bridgetown_webfinger.rb +149 -0
- metadata +131 -0
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bridgetown
|
4
|
+
module Webfinger
|
5
|
+
# Wraps a [link][1] object within a {JRD}
|
6
|
+
#
|
7
|
+
# Links represent links to resources external to the entity. They are
|
8
|
+
# _optional_ within a JRD so may not appear in your Webfinger setup.
|
9
|
+
#
|
10
|
+
# [1]: https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4
|
11
|
+
class Link
|
12
|
+
extend Logging
|
13
|
+
|
14
|
+
# Parses and maybe-returns a {Link} when the value is one
|
15
|
+
#
|
16
|
+
# [Links][1] within the {JRD} are member objects representing a link to
|
17
|
+
# another resource.
|
18
|
+
#
|
19
|
+
# [1]: https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4
|
20
|
+
#
|
21
|
+
# @since 0.1.0
|
22
|
+
# @api private
|
23
|
+
#
|
24
|
+
# @param data [Hash] the data to parse as a link object
|
25
|
+
# @return [Link, nil] the link parsed from the data
|
26
|
+
def self.parse(data)
|
27
|
+
unless (rel = LinkRelationType.parse(data[:rel]))
|
28
|
+
return warn(
|
29
|
+
"Webfinger link rel is missing or malformed: #{data.inspect}, ignoring"
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
new(
|
34
|
+
rel: rel,
|
35
|
+
href: Href.parse(data[:href]),
|
36
|
+
properties: Properties.parse(data[:properties]),
|
37
|
+
titles: Titles.parse(data[:titles]),
|
38
|
+
type: data[:type]
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Creates a new {Link}
|
43
|
+
#
|
44
|
+
# @since 0.1.0
|
45
|
+
# @api private
|
46
|
+
#
|
47
|
+
# @param rel [LinkRelationType] the type of relation the {Link} represents
|
48
|
+
# @param href [Href, nil] the optional {Href} URI of the link
|
49
|
+
# @param properties [Properties, nil] the optional list of {Properties}
|
50
|
+
# describing the link
|
51
|
+
# @param titles [Titles, nil] the optional list of {Titles} naming the link
|
52
|
+
# @param type [String, nil] the optional media type of the target resource
|
53
|
+
def initialize(rel:, href: nil, properties: nil, titles: nil, type: nil)
|
54
|
+
@href = href
|
55
|
+
@properties = properties
|
56
|
+
@rel = rel
|
57
|
+
@titles = titles
|
58
|
+
@type = type
|
59
|
+
end
|
60
|
+
|
61
|
+
# The optional hypertext reference to a URI for the {Link}
|
62
|
+
#
|
63
|
+
# @since 0.1.0
|
64
|
+
# @api private
|
65
|
+
#
|
66
|
+
# @return [Href, nil] the optional {Href} URI of the link
|
67
|
+
attr_reader :href
|
68
|
+
|
69
|
+
# The optional {Properties} characterizing the {Link}
|
70
|
+
#
|
71
|
+
# @since 0.1.0
|
72
|
+
# @api private
|
73
|
+
#
|
74
|
+
# @return [Properties, nil] the optional list of {Properties} describing
|
75
|
+
# the link
|
76
|
+
attr_reader :properties
|
77
|
+
|
78
|
+
# The {LinkRelationType} describing what the {Link} links to
|
79
|
+
#
|
80
|
+
# @since 0.1.0
|
81
|
+
# @api private
|
82
|
+
#
|
83
|
+
# @return [LinkRelationType] the type of relation the {Link} represents
|
84
|
+
attr_reader :rel
|
85
|
+
|
86
|
+
# The optional {Titles} describing the {Link}
|
87
|
+
#
|
88
|
+
# @since 0.1.0
|
89
|
+
# @api private
|
90
|
+
#
|
91
|
+
# @return [Titles, nil] the optional list of {Titles} naming the link
|
92
|
+
attr_reader :titles
|
93
|
+
|
94
|
+
# The optional media type of the {#href} for the {Link}
|
95
|
+
#
|
96
|
+
# @since 0.1.0
|
97
|
+
# @api private
|
98
|
+
#
|
99
|
+
# @return [String, nil] the optional media type of the target resource
|
100
|
+
attr_reader :type
|
101
|
+
|
102
|
+
# Converts the {Link} into a JSON-serializable Hash
|
103
|
+
#
|
104
|
+
# @since 0.1.0
|
105
|
+
# @api private
|
106
|
+
#
|
107
|
+
# @return [Hash] the Link as a JSON-compatible Hash
|
108
|
+
def to_h
|
109
|
+
result = {rel: rel}
|
110
|
+
result[:href] = href if href
|
111
|
+
result[:properties] = properties if properties
|
112
|
+
result[:titles] = titles if titles
|
113
|
+
result[:type] = type if type
|
114
|
+
result
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bridgetown
|
4
|
+
module Webfinger
|
5
|
+
# Wraps the type of a {Link} relation
|
6
|
+
class LinkRelationType < Model
|
7
|
+
# The list of relation types, as [registered with IANA][1]
|
8
|
+
#
|
9
|
+
# This constant can be regenerated with the `data:link_relations` Rake
|
10
|
+
# task.
|
11
|
+
#
|
12
|
+
# [1]: https://www.iana.org/assignments/link-relations
|
13
|
+
#
|
14
|
+
# @since 0.1.0
|
15
|
+
# @api private
|
16
|
+
REGISTERED = Set.new(%w[
|
17
|
+
about
|
18
|
+
acl
|
19
|
+
alternate
|
20
|
+
amphtml
|
21
|
+
appendix
|
22
|
+
apple-touch-icon
|
23
|
+
apple-touch-startup-image
|
24
|
+
archives
|
25
|
+
author
|
26
|
+
blocked-by
|
27
|
+
bookmark
|
28
|
+
canonical
|
29
|
+
chapter
|
30
|
+
cite-as
|
31
|
+
collection
|
32
|
+
contents
|
33
|
+
convertedfrom
|
34
|
+
copyright
|
35
|
+
create-form
|
36
|
+
current
|
37
|
+
describedby
|
38
|
+
describes
|
39
|
+
disclosure
|
40
|
+
dns-prefetch
|
41
|
+
duplicate
|
42
|
+
edit
|
43
|
+
edit-form
|
44
|
+
edit-media
|
45
|
+
enclosure
|
46
|
+
external
|
47
|
+
first
|
48
|
+
glossary
|
49
|
+
help
|
50
|
+
hosts
|
51
|
+
hub
|
52
|
+
icon
|
53
|
+
index
|
54
|
+
intervalafter
|
55
|
+
intervalbefore
|
56
|
+
intervalcontains
|
57
|
+
intervaldisjoint
|
58
|
+
intervalduring
|
59
|
+
intervalequals
|
60
|
+
intervalfinishedby
|
61
|
+
intervalfinishes
|
62
|
+
intervalin
|
63
|
+
intervalmeets
|
64
|
+
intervalmetby
|
65
|
+
intervaloverlappedby
|
66
|
+
intervaloverlaps
|
67
|
+
intervalstartedby
|
68
|
+
intervalstarts
|
69
|
+
item
|
70
|
+
last
|
71
|
+
latest-version
|
72
|
+
license
|
73
|
+
linkset
|
74
|
+
lrdd
|
75
|
+
manifest
|
76
|
+
mask-icon
|
77
|
+
media-feed
|
78
|
+
memento
|
79
|
+
micropub
|
80
|
+
modulepreload
|
81
|
+
monitor
|
82
|
+
monitor-group
|
83
|
+
next
|
84
|
+
next-archive
|
85
|
+
nofollow
|
86
|
+
noopener
|
87
|
+
noreferrer
|
88
|
+
opener
|
89
|
+
openid2.local_id
|
90
|
+
openid2.provider
|
91
|
+
original
|
92
|
+
p3pv1
|
93
|
+
payment
|
94
|
+
pingback
|
95
|
+
preconnect
|
96
|
+
predecessor-version
|
97
|
+
prefetch
|
98
|
+
preload
|
99
|
+
prerender
|
100
|
+
prev
|
101
|
+
preview
|
102
|
+
previous
|
103
|
+
prev-archive
|
104
|
+
privacy-policy
|
105
|
+
profile
|
106
|
+
publication
|
107
|
+
related
|
108
|
+
restconf
|
109
|
+
replies
|
110
|
+
ruleinput
|
111
|
+
search
|
112
|
+
section
|
113
|
+
self
|
114
|
+
service
|
115
|
+
service-desc
|
116
|
+
service-doc
|
117
|
+
service-meta
|
118
|
+
sip-trunking-capability
|
119
|
+
sponsored
|
120
|
+
start
|
121
|
+
status
|
122
|
+
stylesheet
|
123
|
+
subsection
|
124
|
+
successor-version
|
125
|
+
sunset
|
126
|
+
tag
|
127
|
+
terms-of-service
|
128
|
+
timegate
|
129
|
+
timemap
|
130
|
+
type
|
131
|
+
ugc
|
132
|
+
up
|
133
|
+
version-history
|
134
|
+
via
|
135
|
+
webmention
|
136
|
+
working-copy
|
137
|
+
working-copy-of
|
138
|
+
]).freeze
|
139
|
+
|
140
|
+
# Parses a maybe-returns a {LinkRelationType} when the value is one
|
141
|
+
#
|
142
|
+
# Link relation types may either be a URI to a relation type description
|
143
|
+
# or a value [registered with IANA][1].
|
144
|
+
#
|
145
|
+
# [1]: https://www.iana.org/assignments/link-relations
|
146
|
+
#
|
147
|
+
# @since 0.1.0
|
148
|
+
# @api private
|
149
|
+
#
|
150
|
+
# @param rel [String] the rel to parse and validate
|
151
|
+
# @return [LinkRelationType, nil] the link relation type parsed from the rel
|
152
|
+
def self.parse(rel)
|
153
|
+
return unless Webfinger.uri?(rel) || REGISTERED.include?(rel)
|
154
|
+
|
155
|
+
new(rel)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bridgetown
|
4
|
+
module Webfinger
|
5
|
+
# Helper methods for logging purposes
|
6
|
+
module Logging
|
7
|
+
# When including the module, also extend it as well
|
8
|
+
#
|
9
|
+
# @since 0.1.0
|
10
|
+
# @api private
|
11
|
+
# @private
|
12
|
+
#
|
13
|
+
# @param base [Module] the base module or class including the module
|
14
|
+
# @return [void]
|
15
|
+
def self.included(base)
|
16
|
+
super
|
17
|
+
base.extend(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Sends a warning message through the Bridgetown logger
|
21
|
+
#
|
22
|
+
# @since 0.1.0
|
23
|
+
# @api public
|
24
|
+
#
|
25
|
+
# @example Print a warning in the logger
|
26
|
+
#
|
27
|
+
# extend Bridgetown::Webfinger::Logging
|
28
|
+
# warn "I like some of you less than you deserve"
|
29
|
+
#
|
30
|
+
# @param msg [String] the message to log
|
31
|
+
# @return [void]
|
32
|
+
def warn(msg)
|
33
|
+
Bridgetown.logger.warn(msg)
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
module Bridgetown
|
6
|
+
module Webfinger
|
7
|
+
# Parses URI parameters in the Webfinger style
|
8
|
+
#
|
9
|
+
# This is necessary because Rack handles parameters differently than the
|
10
|
+
# Webfinger specification. In Rack, repeated parameters compete in a
|
11
|
+
# last-one-wins scheme. For example:
|
12
|
+
#
|
13
|
+
# https://example.com?foo=1&foo=2&bar=3
|
14
|
+
#
|
15
|
+
# parses into:
|
16
|
+
#
|
17
|
+
# {"foo" => 2, "bar" => 3}
|
18
|
+
#
|
19
|
+
# whereas Webfinger's processing [wants this][1]:
|
20
|
+
#
|
21
|
+
# {"foo" => [1, 2], "bar" => 3}
|
22
|
+
#
|
23
|
+
# per the phrasing “the "rel" parameter MAY be included multiple times in
|
24
|
+
# order to request multiple link relation types.”
|
25
|
+
#
|
26
|
+
# [1]: https://datatracker.ietf.org/doc/html/rfc7033#section-4.3
|
27
|
+
#
|
28
|
+
# @since 0.1.0
|
29
|
+
# @api private
|
30
|
+
class Parameters
|
31
|
+
# Splits query parameters at the ampersand and optional spaces
|
32
|
+
#
|
33
|
+
# This was borrowed from [Rack::QueryParser][2] and simplified.
|
34
|
+
#
|
35
|
+
# [2]: https://rubydoc.info/gems/rack/Rack/QueryParser
|
36
|
+
SPLITTER = %r{& *}n
|
37
|
+
|
38
|
+
# Parses a query string for a webfinger request into a hash
|
39
|
+
#
|
40
|
+
# This method cleans any unrelated parameters from the result and combines
|
41
|
+
# multiple requests for `rel`s into an array. It also handles unescaping
|
42
|
+
# terms.
|
43
|
+
#
|
44
|
+
# @since 0.1.0
|
45
|
+
# @api private
|
46
|
+
#
|
47
|
+
# @param [String] query_string the query string for a webfinger request
|
48
|
+
# @return [Parameters] the cleaned param values
|
49
|
+
def self.from_query_string(query_string)
|
50
|
+
query_string
|
51
|
+
.split(SPLITTER)
|
52
|
+
.map { |pair| pair.split("=", 2).map! { |component| decode(component) } }
|
53
|
+
.each_with_object(Parameters.new) do |(param, value), result|
|
54
|
+
case param
|
55
|
+
when "resource" then result.resource = value
|
56
|
+
when "rel" then result.add_rel(value)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Decodes a URI component; an alias to the URI method, for brevity
|
62
|
+
#
|
63
|
+
# @since 0.1.0
|
64
|
+
# @api private
|
65
|
+
# @private
|
66
|
+
#
|
67
|
+
# @param component [String] the component from the URI
|
68
|
+
# @return [String] the decoded component
|
69
|
+
private_class_method def self.decode(component)
|
70
|
+
URI.decode_uri_component(component)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Creates a new {Parameters} from an optional resource and rel
|
74
|
+
#
|
75
|
+
#
|
76
|
+
#
|
77
|
+
# @since 0.1.0
|
78
|
+
# @api public
|
79
|
+
#
|
80
|
+
# @example Create an empty
|
81
|
+
#
|
82
|
+
# @param resource [String, nil] the resource for the request
|
83
|
+
# @param rel [Array<String>, nil] the types of relation to scope the links to
|
84
|
+
# @return [void]
|
85
|
+
def initialize(resource: nil, rel: nil)
|
86
|
+
@resource = resource
|
87
|
+
@rel = rel
|
88
|
+
end
|
89
|
+
|
90
|
+
# The list of link relations requested within the {Parameters}
|
91
|
+
#
|
92
|
+
# @since 0.1.0
|
93
|
+
# @api public
|
94
|
+
#
|
95
|
+
# @example Reading the link relations requested as a URL parameter
|
96
|
+
#
|
97
|
+
# params = Bridgetown::Webfinger::Parameters.from_query_string(
|
98
|
+
# "resource=acct%3Abilbo%40bagend.com&" \
|
99
|
+
# "rel=http%3A%2F%2Fwebfinger.net%2Frel%2Favatar&" \
|
100
|
+
# "rel=http%3A%2F%2Fwebfinger.net%2Frel%2Fprofile-page"
|
101
|
+
# )
|
102
|
+
# params.rel
|
103
|
+
# #=> ["https://webfinger.net/rel/avatar", "https://webfinger.net/rel/profile-page"]
|
104
|
+
# @return [Array<String>, nil] the types of relation to scope the links to
|
105
|
+
attr_reader :rel
|
106
|
+
|
107
|
+
# The decoded resource requested within the {Parameters}
|
108
|
+
#
|
109
|
+
# @since 0.1.0
|
110
|
+
# @api public
|
111
|
+
#
|
112
|
+
# @example Reading the resource requested as a URL parameter
|
113
|
+
#
|
114
|
+
# params = Bridgetown::Webfinger::Parameters.from_query_string(
|
115
|
+
# "resource=acct%3Abilbo%40bagend.com"
|
116
|
+
# )
|
117
|
+
# params.resource #=> "acct:bilbo@bagend.com"
|
118
|
+
#
|
119
|
+
# @return [String, nil] the resource for the request
|
120
|
+
attr_accessor :resource
|
121
|
+
|
122
|
+
# Checks for value object equality
|
123
|
+
#
|
124
|
+
# @since 0.1.0
|
125
|
+
# @api private
|
126
|
+
#
|
127
|
+
# @param other [Parameters] the other parameter set to compare against
|
128
|
+
# @return [Boolean] true when they are equal, false otherwise
|
129
|
+
def ==(other)
|
130
|
+
other.instance_of?(Parameters) &&
|
131
|
+
resource == other.resource &&
|
132
|
+
rel == other.rel
|
133
|
+
end
|
134
|
+
alias_method :eql?, :==
|
135
|
+
|
136
|
+
# Adds a link relation, or "rel," to the {Parameters}
|
137
|
+
#
|
138
|
+
# @since 0.1.0
|
139
|
+
# @api public
|
140
|
+
#
|
141
|
+
# @example Adding a link relation to a new {Parameters} instance
|
142
|
+
#
|
143
|
+
# params = Bridgetown::Webfinger::Parameters.new
|
144
|
+
# params.add_rel("http://webfinger.net/rel/avatar")
|
145
|
+
# params.rel #=> ["http://webfinger.net/rel/avatar"]
|
146
|
+
#
|
147
|
+
# @param rel [String] a type of relation to add to the scope request
|
148
|
+
# @return [void]
|
149
|
+
def add_rel(rel)
|
150
|
+
return unless rel
|
151
|
+
|
152
|
+
(@rel ||= []).push(rel)
|
153
|
+
end
|
154
|
+
|
155
|
+
# Allows the {Parameters} to be used in a Hash key; part of value object semantics
|
156
|
+
#
|
157
|
+
# @since 0.1.0
|
158
|
+
# @api private
|
159
|
+
#
|
160
|
+
# @return [Integer]
|
161
|
+
def hash
|
162
|
+
[resource, *rel].hash
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bridgetown
|
4
|
+
module Webfinger
|
5
|
+
# Wraps a `properties` member within a {Link} or {JRD}
|
6
|
+
class Properties < Model
|
7
|
+
# Parses and maybe-returns {Properties} when the value is one
|
8
|
+
#
|
9
|
+
# Properties within the {JRD} are name/value pairs [with URIs for names
|
10
|
+
# and strings or nulls for values][1].
|
11
|
+
#
|
12
|
+
# [1]: https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.5
|
13
|
+
#
|
14
|
+
# @since 0.1.0
|
15
|
+
# @api private
|
16
|
+
#
|
17
|
+
# @param data [Hash] the data to parse as a properties object
|
18
|
+
# @return [Properties, nil] the properties parsed from the data
|
19
|
+
def self.parse(data)
|
20
|
+
return unless data
|
21
|
+
|
22
|
+
unless data.is_a?(Hash)
|
23
|
+
return warn("Webfinger link properties are malformed: #{data.inspect}, ignoring")
|
24
|
+
end
|
25
|
+
|
26
|
+
properties = data.select do |name, value|
|
27
|
+
if !Webfinger.uri?(name)
|
28
|
+
next warn("Webfinger property name is not a URI: #{name}, ignoring")
|
29
|
+
elsif !value.is_a?(String) || value.nil?
|
30
|
+
next warn("Webfinger property value is not a nullable string: #{value}, ignoring")
|
31
|
+
else
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
if !properties.empty?
|
37
|
+
new(properties)
|
38
|
+
else
|
39
|
+
warn("All Webfinger link properties pruned: #{data.inspect}, ignoring")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bridgetown
|
4
|
+
module Webfinger
|
5
|
+
# Wraps a `titles` member within a {Link}
|
6
|
+
class Titles < Model
|
7
|
+
# Parses and maybe-returns {Titles} when the value is one
|
8
|
+
#
|
9
|
+
# Titles within the {JRD} are name/value pairs [with language tag names
|
10
|
+
# and string values][2]. When the language is indeterminate, use `"und"`.
|
11
|
+
#
|
12
|
+
# [1]: https://datatracker.ietf.org/doc/html/rfc7033#section-4.4.4.4
|
13
|
+
#
|
14
|
+
# @since 0.1.0
|
15
|
+
# @api private
|
16
|
+
#
|
17
|
+
# @param data [Hash] the data to parse as a titles object
|
18
|
+
# @return [Titles, nil] the titles parsed from the data
|
19
|
+
def self.parse(data)
|
20
|
+
return unless data.is_a?(Hash)
|
21
|
+
|
22
|
+
data = data.select do |key, value|
|
23
|
+
if !key.is_a?(String)
|
24
|
+
next warn("Webfinger title key is not a string: #{key}, ignoring")
|
25
|
+
elsif !value.is_a?(String)
|
26
|
+
next warn("Webfinger title value for #{key} is not a string: #{value}, ignoring")
|
27
|
+
else
|
28
|
+
true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
if !data.empty?
|
33
|
+
new(data)
|
34
|
+
else
|
35
|
+
warn("All Webfinger link titles pruned: #{data.inspect}, ignoring")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|