redd 0.8.1 → 0.8.2
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 +4 -4
- data/README.md +18 -8
- data/bin/console +42 -16
- data/lib/redd.rb +50 -19
- data/lib/redd/api_client.rb +18 -27
- data/lib/redd/auth_strategies/auth_strategy.rb +4 -3
- data/lib/redd/auth_strategies/script.rb +6 -1
- data/lib/redd/auth_strategies/userless.rb +6 -1
- data/lib/redd/auth_strategies/web.rb +4 -4
- data/lib/redd/models/access.rb +6 -1
- data/lib/redd/models/basic_model.rb +12 -55
- data/lib/redd/models/comment.rb +24 -28
- data/lib/redd/models/front_page.rb +1 -1
- data/lib/redd/models/gildable.rb +13 -0
- data/lib/redd/models/inboxable.rb +3 -3
- data/lib/redd/models/lazy_model.rb +7 -8
- data/lib/redd/models/listing.rb +6 -7
- data/lib/redd/models/live_thread.rb +9 -11
- data/lib/redd/models/mod_mail.rb +19 -24
- data/lib/redd/models/more_comments.rb +1 -1
- data/lib/redd/models/multireddit.rb +9 -13
- data/lib/redd/models/postable.rb +3 -3
- data/lib/redd/models/private_message.rb +18 -9
- data/lib/redd/models/searchable.rb +1 -1
- data/lib/redd/models/session.rb +31 -2
- data/lib/redd/models/submission.rb +26 -14
- data/lib/redd/models/subreddit.rb +115 -19
- data/lib/redd/models/user.rb +21 -9
- data/lib/redd/models/wiki_page.rb +8 -11
- data/lib/redd/utilities/unmarshaller.rb +3 -2
- data/lib/redd/version.rb +1 -1
- data/redd.gemspec +1 -1
- metadata +4 -5
- data/TODO.md +0 -8
- data/lib/redd/auth_strategies/installed.rb +0 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eb84c87afa0fb9151b0be21985da7f69c261df24
|
4
|
+
data.tar.gz: 2969272d1ae4cb25234ed9ff3902f2cdce51114d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ad8183da54e6dbbea67234b2cab3796b2aaf79b7bfd8f1e35dc7a6c3f93c5c92a0f63830c8d8f9d39042bcf02ed99841aea0bd4b225754a992328503464eea9
|
7
|
+
data.tar.gz: 8b7bce2305a66a554d484015e593d212f646aaead6efc894219e32ad14a2e86c4b99198d99fd01f83432938f1cf176c4fb2eaf7f1aae35112c5acf53b857c567
|
data/README.md
CHANGED
@@ -17,15 +17,22 @@
|
|
17
17
|
|
18
18
|
<!-- Intro Text -->
|
19
19
|
<p>
|
20
|
-
<strong>Redd</strong> is
|
21
|
-
for <a href="https://www.reddit.com/dev/api">reddit</a
|
22
|
-
that is all about being <strong>simple</strong>
|
23
|
-
and <strong>intuitive</strong>.
|
20
|
+
<strong>Redd</strong> is a <strong>batteries-included</strong>
|
21
|
+
API wrapper for <a href="https://www.reddit.com/dev/api">reddit</a>.
|
24
22
|
</p>
|
25
23
|
</div>
|
26
24
|
|
27
25
|
---
|
28
26
|
|
27
|
+
### Features
|
28
|
+
|
29
|
+
- Supports most of the reddit API, including live threads and the beta mod-mail.
|
30
|
+
- Includes support for streaming new posts and comments.
|
31
|
+
- Built-in rate limiting and error handling.
|
32
|
+
- Automatic retrying of failed requests.
|
33
|
+
|
34
|
+
### Demo
|
35
|
+
|
29
36
|
```ruby
|
30
37
|
require 'redd'
|
31
38
|
|
@@ -46,18 +53,21 @@ session.subreddit('all').comment_stream do |comment|
|
|
46
53
|
end
|
47
54
|
```
|
48
55
|
|
49
|
-
---
|
50
|
-
|
51
56
|
### FAQ
|
52
57
|
|
53
58
|
#### Is that bot fully functional?
|
54
|
-
**Yes**, that's all there is to it! You don't need to handle rate-limiting, refresh access tokens
|
55
|
-
or protect against issues on reddit's end (like 5xx errors).
|
59
|
+
**Yes**, that's all there is to it! You don't need to handle rate-limiting, refresh access tokens or protect against issues on reddit's end (like 5xx errors).
|
56
60
|
|
57
61
|
#### Where can I find the documentation?
|
58
62
|
|
59
63
|
[**Gem**](http://www.rubydoc.info/gems/redd/Redd/Models/Session) / [**GitHub**](http://www.rubydoc.info/github/avinashbot/redd/master/Redd/Models/Session)
|
60
64
|
|
65
|
+
#### How do I request a feature / contribute?
|
66
|
+
|
67
|
+
- The quickest way to get a feature into Redd is to raise a GitHub issue.
|
68
|
+
- Pull requests are also appreciated!
|
69
|
+
- Don't hesitate! There are no stupid questions!
|
70
|
+
|
61
71
|
#### How can I contact you?
|
62
72
|
[Reddit](https://www.reddit.com/message/compose/?to=Mustermind) /
|
63
73
|
[GitHub](https://github.com/avinashbot/redd/issues/new) /
|
data/bin/console
CHANGED
@@ -3,6 +3,14 @@
|
|
3
3
|
|
4
4
|
require 'webrick'
|
5
5
|
require 'redd'
|
6
|
+
require 'pry'
|
7
|
+
|
8
|
+
# The REPL session to initialize Pry in.
|
9
|
+
module Reddit
|
10
|
+
class << self
|
11
|
+
attr_accessor :reddit
|
12
|
+
end
|
13
|
+
end
|
6
14
|
|
7
15
|
server = WEBrick::HTTPServer.new(
|
8
16
|
Port: 8000,
|
@@ -22,7 +30,7 @@ server.mount_proc '/' do |_, res|
|
|
22
30
|
</style>
|
23
31
|
<div class="wrapper">
|
24
32
|
<h1>redd // quickstart</h1>
|
25
|
-
<a href="
|
33
|
+
<a href="/authenticate" target="_blank" id="btn">Start</a>
|
26
34
|
<span>a new session in your terminal?</span>
|
27
35
|
</div>
|
28
36
|
EOS
|
@@ -36,9 +44,10 @@ server.mount_proc '/authenticate' do |_, res|
|
|
36
44
|
response_type: 'code',
|
37
45
|
state: '0',
|
38
46
|
redirect_uri: 'http://localhost:8000/redirect',
|
39
|
-
'
|
40
|
-
|
41
|
-
|
47
|
+
duration: 'permanent',
|
48
|
+
scope: %w(account creddits edit flair history identity livemanage modconfig modcontributors
|
49
|
+
modflair modlog modmail modothers modposts modself modwiki mysubreddits
|
50
|
+
privatemessages read report save submit subscribe vote wikiedit wikiread)
|
42
51
|
)
|
43
52
|
)
|
44
53
|
end
|
@@ -53,11 +62,12 @@ server.mount_proc '/redirect' do |req, res|
|
|
53
62
|
EOS
|
54
63
|
|
55
64
|
unless err
|
56
|
-
|
65
|
+
Reddit.reddit = Redd.it(
|
57
66
|
user_agent: "Ruby:Redd-Quickstart:v#{Redd::VERSION} (by unknown)",
|
58
67
|
client_id: 'P4txR-D6TzF8cg',
|
59
68
|
redirect_uri: 'http://localhost:8000/redirect',
|
60
|
-
code: req.query['code']
|
69
|
+
code: req.query['code'],
|
70
|
+
auto_refresh: true
|
61
71
|
)
|
62
72
|
server.stop
|
63
73
|
end
|
@@ -72,14 +82,30 @@ rescue Interrupt
|
|
72
82
|
exit
|
73
83
|
end
|
74
84
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
85
|
+
Reddit.instance_exec do
|
86
|
+
# Post a colourful welcome message
|
87
|
+
suggestions = [
|
88
|
+
# Session#me
|
89
|
+
'reddit.me.link_karma',
|
90
|
+
# Subreddit listings
|
91
|
+
"reddit.subreddit('pics').hot.first.title",
|
92
|
+
# User listings
|
93
|
+
'puts reddit.me.comments(sort: :top).first.body',
|
94
|
+
# Sending messages
|
95
|
+
"reddit.user('Mustermind').send_message(subject: 'Hi!', text: 'How are you?')",
|
96
|
+
# Subscribing to subreddits
|
97
|
+
"reddit.subreddit('EarthPorn').subscribe",
|
98
|
+
# Upvoting
|
99
|
+
'reddit.front_page.hot(time: :month).first.upvote',
|
100
|
+
# Add friend
|
101
|
+
"reddit.user('Mustermind').friend",
|
102
|
+
# List friends
|
103
|
+
'reddit.friends.each { |friend| puts friend.name };',
|
104
|
+
# Hiding / Duplicates
|
105
|
+
'reddit.front_page.hot.each { |l| l.hide if l.duplicates.count > 2 }'
|
106
|
+
]
|
107
|
+
puts "Hi \e[35m/u/#{reddit.me.name}\e[0m! Try `\e[34m#{suggestions.sample}\e[0m`."
|
82
108
|
|
83
|
-
# Load Pry
|
84
|
-
|
85
|
-
|
109
|
+
# Load Pry
|
110
|
+
Pry.start(self)
|
111
|
+
end
|
data/lib/redd.rb
CHANGED
@@ -14,8 +14,40 @@ require_relative 'redd/api_client'
|
|
14
14
|
# Redd is a simple and intuitive API wrapper.
|
15
15
|
module Redd
|
16
16
|
class << self
|
17
|
-
#
|
18
|
-
#
|
17
|
+
# Based on the arguments you provide it, it guesses the appropriate authentication strategy.
|
18
|
+
# You can do this manually with:
|
19
|
+
#
|
20
|
+
# script = Redd::AuthStrategies::Script.new(**arguments)
|
21
|
+
# web = Redd::AuthStrategies::Web.new(**arguments)
|
22
|
+
# userless = Redd::AuthStrategies::Userless.new(**arguments)
|
23
|
+
#
|
24
|
+
# It then creates an {APIClient} with the auth strategy provided and calls authenticate on it:
|
25
|
+
#
|
26
|
+
# client = Redd::APIClient.new(script); client.authenticate(code)
|
27
|
+
# client = Redd::APIClient.new(web); client.authenticate
|
28
|
+
# client = Redd::APIClient.new(userless); client.authenticate
|
29
|
+
#
|
30
|
+
# Finally, it creates the {Models::Session} model, which is essentially a starting point for
|
31
|
+
# the user. But you can basically create any model with the client.
|
32
|
+
#
|
33
|
+
# session = Redd::Models::Session.new(client)
|
34
|
+
#
|
35
|
+
# user = Redd::Models::User.new(client, name: 'Mustermind')
|
36
|
+
# puts user.comment_karma
|
37
|
+
#
|
38
|
+
# If `auto_refresh` is `false` or if the access doesn't have an associated `expires_in`, you
|
39
|
+
# can manually refresh the token by calling:
|
40
|
+
#
|
41
|
+
# session.client.refresh
|
42
|
+
#
|
43
|
+
# Also, you can swap out the client's access any time.
|
44
|
+
#
|
45
|
+
# new_access = { access_token: '', refresh_token: '', expires_in: 1234 }
|
46
|
+
#
|
47
|
+
# session.client.access = Redd::Models::Access.new(script, new_access)
|
48
|
+
# session.client.access = Redd::Models::Access.new(web, new_access)
|
49
|
+
# session.client.access = Redd::Models::Access.new(userless, new_access)
|
50
|
+
#
|
19
51
|
# @see https://www.reddit.com/prefs/apps
|
20
52
|
# @param opts [Hash] the options to create the object with
|
21
53
|
# @option opts [String] :user_agent your app's *unique* and *descriptive* user agent
|
@@ -27,9 +59,9 @@ module Redd
|
|
27
59
|
# @option opts [String] :code the code given by reddit (required for *web* and *installed*)
|
28
60
|
# @return [Models::Session] a fresh {Models::Session} for you to make requests with
|
29
61
|
def it(opts = {})
|
30
|
-
api_client = script(opts) || web(opts) ||
|
62
|
+
api_client = script(opts) || web(opts) || userless(opts)
|
31
63
|
raise "couldn't guess app type" unless api_client
|
32
|
-
Models::Session.new(api_client)
|
64
|
+
Models::Session.new(api_client)
|
33
65
|
end
|
34
66
|
|
35
67
|
# Create a url to send to users for authorization.
|
@@ -38,14 +70,18 @@ module Redd
|
|
38
70
|
# @param client_id [String] the client id of the app
|
39
71
|
# @param redirect_uri [String] the URI for reddit to redirect to after authorization
|
40
72
|
# @param scope [Array<String>] an array of scopes to request
|
73
|
+
# @param duration ['temporary', 'permanent'] the duration to request the code for (only applies
|
74
|
+
# when response_type is 'code')
|
41
75
|
# @return [String] the generated url
|
42
|
-
def url(client_id:, redirect_uri:, response_type: 'code', state: '', scope: ['identity']
|
76
|
+
def url(client_id:, redirect_uri:, response_type: 'code', state: '', scope: ['identity'],
|
77
|
+
duration: 'temporary')
|
43
78
|
'https://www.reddit.com/api/v1/authorize?' + URI.encode_www_form(
|
44
79
|
client_id: client_id,
|
45
80
|
redirect_uri: redirect_uri,
|
46
81
|
state: state,
|
47
82
|
scope: scope.join(','),
|
48
|
-
response_type: response_type
|
83
|
+
response_type: response_type,
|
84
|
+
duration: duration
|
49
85
|
)
|
50
86
|
end
|
51
87
|
|
@@ -56,32 +92,27 @@ module Redd
|
|
56
92
|
end
|
57
93
|
|
58
94
|
def filter_api(opts)
|
59
|
-
opts.select { |k| %i(user_agent).include?(k) }
|
95
|
+
opts.select { |k| %i(user_agent limit_time max_retries auto_refresh).include?(k) }
|
60
96
|
end
|
61
97
|
|
62
98
|
def script(opts = {})
|
63
99
|
return unless %i(client_id secret username password).all? { |o| opts.include?(o) }
|
64
|
-
|
100
|
+
auth = AuthStrategies::Script.new(filter_auth(opts))
|
101
|
+
api = APIClient.new(auth, **filter_api(opts))
|
65
102
|
api.tap(&:authenticate)
|
66
103
|
end
|
67
104
|
|
68
105
|
def web(opts = {})
|
69
|
-
return unless %i(client_id secret redirect_uri code).all? { |o| opts.include?(o) }
|
70
|
-
code = opts.delete(:code)
|
71
|
-
api = APIClient.new(AuthStrategies::Web.new(**filter_auth(opts)), **filter_api(opts))
|
72
|
-
api.tap { |c| c.authenticate(code) }
|
73
|
-
end
|
74
|
-
|
75
|
-
def installed(opts = {})
|
76
106
|
return unless %i(client_id redirect_uri code).all? { |o| opts.include?(o) }
|
77
|
-
|
78
|
-
api = APIClient.new(
|
79
|
-
api.tap { |c| c.authenticate(code) }
|
107
|
+
auth = AuthStrategies::Web.new(**filter_auth(opts))
|
108
|
+
api = APIClient.new(auth, **filter_api(opts))
|
109
|
+
api.tap { |c| c.authenticate(opts[:code]) }
|
80
110
|
end
|
81
111
|
|
82
112
|
def userless(opts = {})
|
83
113
|
return unless %i(client_id secret).all? { |o| opts.include?(o) }
|
84
|
-
|
114
|
+
auth = AuthStrategies::Userless.new(filter_auth(opts))
|
115
|
+
api = APIClient.new(auth, **filter_api(opts))
|
85
116
|
api.tap(&:authenticate)
|
86
117
|
end
|
87
118
|
end
|
data/lib/redd/api_client.rb
CHANGED
@@ -13,35 +13,27 @@ module Redd
|
|
13
13
|
API_ENDPOINT = 'https://oauth.reddit.com'
|
14
14
|
|
15
15
|
# @return [APIClient] the access the client uses
|
16
|
-
|
16
|
+
attr_accessor :access
|
17
17
|
|
18
|
-
# Create a new API client
|
18
|
+
# Create a new API client with an auth strategy.
|
19
19
|
# @param auth [AuthStrategies::AuthStrategy] the auth strategy to use
|
20
20
|
# @param endpoint [String] the API endpoint
|
21
21
|
# @param user_agent [String] the user agent to send
|
22
22
|
# @param limit_time [Integer] the minimum number of seconds between each request
|
23
|
-
# @param max_retries [Integer] number of times to retry requests that may
|
24
|
-
#
|
25
|
-
# @param auto_login [Boolean] (for script and userless) automatically authenticate if not done
|
26
|
-
# so already
|
27
|
-
# @param auto_refresh [Boolean] (for script and userless) automatically refresh access token if
|
28
|
-
# nearing expiration
|
23
|
+
# @param max_retries [Integer] number of times to retry requests that may succeed if retried
|
24
|
+
# @param auto_refresh [Boolean] automatically refresh access token if nearing expiration
|
29
25
|
def initialize(auth, endpoint: API_ENDPOINT, user_agent: USER_AGENT, limit_time: 1,
|
30
|
-
max_retries: 5,
|
26
|
+
max_retries: 5, auto_refresh: true)
|
31
27
|
super(endpoint: endpoint, user_agent: user_agent)
|
32
28
|
|
33
|
-
@auth
|
34
|
-
@access
|
35
|
-
@max_retries
|
36
|
-
@failures
|
37
|
-
@error_handler
|
38
|
-
@rate_limiter
|
39
|
-
@unmarshaller
|
40
|
-
|
41
|
-
# FIXME: hard dependencies on Script and Userless types
|
42
|
-
can_auto = auth.is_a?(AuthStrategies::Script) || auth.is_a?(AuthStrategies::Userless)
|
43
|
-
@auto_login = can_auto && auto_login
|
44
|
-
@auto_refresh = can_auto && auto_refresh
|
29
|
+
@auth = auth
|
30
|
+
@access = nil
|
31
|
+
@max_retries = max_retries
|
32
|
+
@failures = 0
|
33
|
+
@error_handler = Utilities::ErrorHandler.new
|
34
|
+
@rate_limiter = Utilities::RateLimiter.new(limit_time)
|
35
|
+
@unmarshaller = Utilities::Unmarshaller.new(self)
|
36
|
+
@auto_refresh = auto_refresh
|
45
37
|
end
|
46
38
|
|
47
39
|
# Authenticate the client using the provided auth.
|
@@ -50,8 +42,8 @@ module Redd
|
|
50
42
|
end
|
51
43
|
|
52
44
|
# Refresh the access currently in use.
|
53
|
-
def refresh
|
54
|
-
@access = @auth.refresh(
|
45
|
+
def refresh
|
46
|
+
@access = @auth.refresh(@access)
|
55
47
|
end
|
56
48
|
|
57
49
|
# Revoke the current access and remove it from the client.
|
@@ -84,6 +76,7 @@ module Redd
|
|
84
76
|
@failures = 0
|
85
77
|
response
|
86
78
|
rescue Redd::ServerError, HTTP::TimeoutError => e
|
79
|
+
# FIXME: maybe only retry GET requests, for obvious reasons?
|
87
80
|
@failures += 1
|
88
81
|
raise e if @failures > @max_retries
|
89
82
|
warn "Redd got a #{e.class.name} error (#{e.message}), retrying..."
|
@@ -94,12 +87,10 @@ module Redd
|
|
94
87
|
|
95
88
|
# Makes sure a valid access is present, raising an error if nil
|
96
89
|
def ensure_access_is_valid
|
97
|
-
#
|
98
|
-
authenticate if @access.nil?
|
90
|
+
# If access is nil, panic
|
91
|
+
raise 'client access is nil, try calling #authenticate' if @access.nil?
|
99
92
|
# Refresh access if auto_refresh is enabled
|
100
93
|
refresh if @access.expired? && @auto_refresh
|
101
|
-
# Fuck it, panic
|
102
|
-
raise 'client access is nil, try calling #authenticate' if @access.nil?
|
103
94
|
end
|
104
95
|
|
105
96
|
def connection
|
@@ -26,13 +26,14 @@ module Redd
|
|
26
26
|
end
|
27
27
|
|
28
28
|
# @abstract Refresh the authentication and return the refreshed access
|
29
|
+
# @param _access [Access, String] the access to refresh
|
29
30
|
# @return [Access] the new access
|
30
|
-
def refresh(
|
31
|
+
def refresh(_access)
|
31
32
|
raise 'abstract method: this strategy cannot refresh access'
|
32
33
|
end
|
33
34
|
|
34
35
|
# Revoke the access token, making it invalid for future requests.
|
35
|
-
# @param access [Access] the access
|
36
|
+
# @param access [Access, String] the access to revoke
|
36
37
|
def revoke(access)
|
37
38
|
token =
|
38
39
|
if access.is_a?(String)
|
@@ -42,7 +43,7 @@ module Redd
|
|
42
43
|
else
|
43
44
|
access.access_token
|
44
45
|
end
|
45
|
-
post('/api/v1/revoke_token', token: token)
|
46
|
+
post('/api/v1/revoke_token', token: token)
|
46
47
|
end
|
47
48
|
|
48
49
|
private
|
@@ -17,7 +17,12 @@ module Redd
|
|
17
17
|
def authenticate
|
18
18
|
request_access('password', username: @username, password: @password)
|
19
19
|
end
|
20
|
-
|
20
|
+
|
21
|
+
# Refresh the authentication and return the refreshed access
|
22
|
+
# @return [Access] the new access
|
23
|
+
def refresh(_)
|
24
|
+
authenticate
|
25
|
+
end
|
21
26
|
end
|
22
27
|
end
|
23
28
|
end
|
@@ -11,7 +11,12 @@ module Redd
|
|
11
11
|
def authenticate
|
12
12
|
request_access('client_credentials')
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
|
+
# Refresh the authentication and return the refreshed access
|
16
|
+
# @return [Access] the new access
|
17
|
+
def refresh(_)
|
18
|
+
authenticate
|
19
|
+
end
|
15
20
|
end
|
16
21
|
end
|
17
22
|
end
|
@@ -4,10 +4,9 @@ require_relative 'auth_strategy'
|
|
4
4
|
|
5
5
|
module Redd
|
6
6
|
module AuthStrategies
|
7
|
-
# A typical code-based authentication
|
8
|
-
# Only confidential web apps can be refreshed.
|
7
|
+
# A typical code-based authentication, for 'web' and 'installed' types.
|
9
8
|
class Web < AuthStrategy
|
10
|
-
def initialize(client_id:,
|
9
|
+
def initialize(client_id:, redirect_uri:, secret: '', **kwargs)
|
11
10
|
super(client_id: client_id, secret: secret, **kwargs)
|
12
11
|
@redirect_uri = redirect_uri
|
13
12
|
end
|
@@ -22,7 +21,8 @@ module Redd
|
|
22
21
|
# Refresh the authentication and return a new refreshed access
|
23
22
|
# @return [Access] the new access
|
24
23
|
def refresh(access)
|
25
|
-
|
24
|
+
refresh_token = access.is_a?(String) ? refresh_token : access.refresh_token
|
25
|
+
request_access('refresh_token', refresh_token: refresh_token)
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
data/lib/redd/models/access.rb
CHANGED
@@ -7,7 +7,12 @@ module Redd
|
|
7
7
|
# Models access_token and related keys.
|
8
8
|
class Access < BasicModel
|
9
9
|
def expired?(grace_period = 60)
|
10
|
-
|
10
|
+
return false unless @attributes[:expires_in]
|
11
|
+
Time.now > @created_at + (@attributes[:expires_in] - grace_period)
|
12
|
+
end
|
13
|
+
|
14
|
+
def permanent?
|
15
|
+
!@attributes[:refresh_token].nil?
|
11
16
|
end
|
12
17
|
|
13
18
|
private
|