spinels-redd 0.9.0

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.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.github/dependabot.yml +7 -0
  3. data/.github/workflows/ci.yml +52 -0
  4. data/.gitignore +10 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +29 -0
  7. data/CONTRIBUTING.md +63 -0
  8. data/Gemfile +6 -0
  9. data/Guardfile +7 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +119 -0
  12. data/Rakefile +12 -0
  13. data/TODO.md +423 -0
  14. data/bin/console +127 -0
  15. data/bin/guard +2 -0
  16. data/bin/setup +8 -0
  17. data/docs/guides/.keep +0 -0
  18. data/docs/tutorials/creating-bots-with-redd.md +101 -0
  19. data/docs/tutorials/creating-webapps-with-redd.md +124 -0
  20. data/docs/tutorials/make-a-grammar-bot.md +5 -0
  21. data/docs/tutorials.md +7 -0
  22. data/lib/redd/api_client.rb +116 -0
  23. data/lib/redd/assist/delete_badly_scoring.rb +64 -0
  24. data/lib/redd/auth_strategies/auth_strategy.rb +68 -0
  25. data/lib/redd/auth_strategies/script.rb +35 -0
  26. data/lib/redd/auth_strategies/userless.rb +29 -0
  27. data/lib/redd/auth_strategies/web.rb +36 -0
  28. data/lib/redd/client.rb +91 -0
  29. data/lib/redd/errors.rb +65 -0
  30. data/lib/redd/middleware.rb +125 -0
  31. data/lib/redd/models/access.rb +54 -0
  32. data/lib/redd/models/comment.rb +229 -0
  33. data/lib/redd/models/front_page.rb +55 -0
  34. data/lib/redd/models/gildable.rb +13 -0
  35. data/lib/redd/models/inboxable.rb +33 -0
  36. data/lib/redd/models/listing.rb +52 -0
  37. data/lib/redd/models/live_thread.rb +133 -0
  38. data/lib/redd/models/live_update.rb +46 -0
  39. data/lib/redd/models/messageable.rb +20 -0
  40. data/lib/redd/models/mod_action.rb +59 -0
  41. data/lib/redd/models/model.rb +23 -0
  42. data/lib/redd/models/moderatable.rb +46 -0
  43. data/lib/redd/models/modmail.rb +61 -0
  44. data/lib/redd/models/modmail_conversation.rb +154 -0
  45. data/lib/redd/models/modmail_message.rb +35 -0
  46. data/lib/redd/models/more_comments.rb +96 -0
  47. data/lib/redd/models/multireddit.rb +104 -0
  48. data/lib/redd/models/paginated_listing.rb +124 -0
  49. data/lib/redd/models/postable.rb +83 -0
  50. data/lib/redd/models/private_message.rb +105 -0
  51. data/lib/redd/models/replyable.rb +16 -0
  52. data/lib/redd/models/reportable.rb +14 -0
  53. data/lib/redd/models/searchable.rb +35 -0
  54. data/lib/redd/models/self.rb +17 -0
  55. data/lib/redd/models/session.rb +198 -0
  56. data/lib/redd/models/submission.rb +405 -0
  57. data/lib/redd/models/subreddit.rb +670 -0
  58. data/lib/redd/models/trophy.rb +34 -0
  59. data/lib/redd/models/user.rb +239 -0
  60. data/lib/redd/models/wiki_page.rb +56 -0
  61. data/lib/redd/utilities/error_handler.rb +73 -0
  62. data/lib/redd/utilities/rate_limiter.rb +21 -0
  63. data/lib/redd/utilities/unmarshaller.rb +70 -0
  64. data/lib/redd/version.rb +5 -0
  65. data/lib/redd.rb +129 -0
  66. data/lib/spinels-redd.rb +3 -0
  67. data/logo.png +0 -0
  68. data/spinels-redd.gemspec +39 -0
  69. metadata +298 -0
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model'
4
+ require_relative 'messageable'
5
+
6
+ module Redd
7
+ module Models
8
+ # A reddit user.
9
+ class User < Model
10
+ include Messageable
11
+
12
+ # Get the appropriate listing.
13
+ # @param type [:overview, :submitted, :comments, :liked, :disliked, :hidden, :saved, :gilded]
14
+ # the type of listing to request
15
+ # @param options [Hash] a list of options to send with the request
16
+ # @option options [:hot, :new, :top, :controversial] :sort the order of the listing
17
+ # @option options [String] :after return results after the given fullname
18
+ # @option options [String] :before return results before the given fullname
19
+ # @option options [Integer] :count the number of items already seen in the listing
20
+ # @option options [1..100] :limit the maximum number of things to return
21
+ # @option options [:hour, :day, :week, :month, :year, :all] :time the time period to consider
22
+ # when sorting
23
+ # @option options [:given] :show whether to show the gildings given
24
+ #
25
+ # @note The option :time only applies to the top and controversial sorts.
26
+ # @return [Listing<Submission>]
27
+ def listing(type, **options)
28
+ options[:t] = options.delete(:time) if options.key?(:time)
29
+ PaginatedListing.new(client, **options) do |**req_opts|
30
+ client.model(:get, "/user/#{read_attribute(:name)}/#{type}.json", options.merge(req_opts))
31
+ end
32
+ end
33
+
34
+ # @!method overview(**params)
35
+ # @!method submitted(**params)
36
+ # @!method comments(**params)
37
+ # @!method liked(**params)
38
+ # @!method disliked(**params)
39
+ # @!method hidden(**params)
40
+ # @!method saved(**params)
41
+ # @!method gilded(**params)
42
+ #
43
+ # @see #listing
44
+ %i[overview submitted comments liked disliked hidden saved gilded].each do |type|
45
+ define_method(type) { |**params| listing(type, **params) }
46
+ end
47
+
48
+ # Compose a message to the moderators of a subreddit.
49
+ #
50
+ # @param subject [String] the subject of the message
51
+ # @param text [String] the message text
52
+ # @param from [Subreddit, nil] the subreddit to send the message on behalf of
53
+ def send_message(subject:, text:, from: nil)
54
+ super(to: read_attribute(:name), subject: subject, text: text, from: from)
55
+ end
56
+
57
+ # Block this user.
58
+ def block
59
+ client.post('/api/block_user', account_id: read_attribute(:id))
60
+ end
61
+
62
+ # @return [Array<Trophy>] this user's trophies
63
+ def trophies
64
+ client.get("/api/v1/user/#{read_attribute(:name)}/trophies")
65
+ .body[:data][:trophies]
66
+ .map { |t| client.unmarshal(t) }
67
+ end
68
+
69
+ # Unblock a previously blocked user.
70
+ # @param me [User] (optional) the person doing the unblocking
71
+ def unblock(me: nil) # rubocop:disable Naming/MethodParameterName
72
+ my_id = "t2_ #{me.is_a?(User ? user.id : client.get('/api/v1/me').body[:id])}"
73
+ # Talk about an unintuitive endpoint
74
+ client.post('/api/unfriend', container: my_id, name: read_attribute(:name), type: 'enemy')
75
+ end
76
+
77
+ # Add the user as a friend.
78
+ # @param note [String] a note for the friend
79
+ def friend(note = nil)
80
+ name = read_attribute(:name)
81
+ body = JSON.generate(note ? { name: name, note: note } : { name: name })
82
+ client.request(:put, "/api/v1/me/friends/#{name}", body: body)
83
+ end
84
+
85
+ # Unfriend the user.
86
+ def unfriend
87
+ name = read_attribute(:name)
88
+ client.request(:delete, "/api/v1/me/friends/#{name}", raw: true, form: { id: name })
89
+ end
90
+
91
+ # Gift a redditor reddit gold.
92
+ # @param months [Integer] the number of months of gold to gift
93
+ def gift_gold(months: 1)
94
+ client.post("/api/v1/gold/give/#{read_attribute(:name)}", months: months)
95
+ end
96
+
97
+ # @!attribute [r] name
98
+ # @return [String] the user's username
99
+ property :name
100
+
101
+ # @!attribute [r] employee?
102
+ # @return [Boolean] whether the user is a reddit employee
103
+ property :employee?, from: :is_employee
104
+
105
+ # @!attribute [r] features
106
+ # @return [Hash] a hash of features
107
+ property :features
108
+
109
+ # @!attribute [r] friend?
110
+ # @return [Boolean] whether the user is your friend
111
+ property :friend?, from: :is_friend
112
+
113
+ # @!attribute [r] no_profanity?
114
+ # @return [Boolean] whether the user chooses to filter profanity
115
+ property :no_profanity?, from: :pref_no_profanity
116
+
117
+ # @!attribute [r] suspended?
118
+ # @return [Boolean] whether the user is suspended
119
+ property :suspended?, from: :is_suspended
120
+
121
+ # @!attribute [r] geopopular
122
+ # @return [String]
123
+ property :geopopular, from: :pref_geopopular
124
+
125
+ # @!attribute [r] subreddit
126
+ # @return [Subreddit] the user's personal "subreddit"
127
+ property :subreddit, with: ->(name) { Subreddit.new(client, display_name: name) if name }
128
+
129
+ # @!attribute [r] sponsor?
130
+ # @return [Boolean]
131
+ property :sponsor?, from: :is_sponsor
132
+
133
+ # @!attribute [r] gold_expiration
134
+ # @return [Time, nil] the time when the user's gold expires
135
+ property :gold_expiration, with: ->(epoch) { Time.at(epoch) if epoch }
136
+
137
+ # @!attribute [r] id
138
+ # @return [String] the user's base36 id
139
+ property :id
140
+
141
+ # @!attribute [r] profile_image
142
+ # @return [String] a link to the user's profile image
143
+ property :profile_image, from: :profile_img
144
+
145
+ # @!attribute [r] over_18?
146
+ # @return [Boolean] whether the user's profile is considered over 18.
147
+ property :over_18?, from: :profile_over_18
148
+
149
+ # @!attribute [r] suspension_expiration
150
+ # @return [Time, nil] the time when the user's suspension expires
151
+ property :suspension_expiration, from: :suspension_expiration_utc,
152
+ with: ->(epoch) { Time.at(epoch) if epoch }
153
+
154
+ # @!attribute [r] verified?
155
+ # @return [Boolean] whether the user is verified (?)
156
+ property :verified?, from: :verified
157
+
158
+ # @!attribute [r] new_modmail_exists?
159
+ # @return [Boolean] whether the user has mail in the new modmail
160
+ property :new_modmail_exists?, from: :new_modmail_exists
161
+
162
+ # @!attribute [r] over_18?
163
+ # @return [Boolean] whether the user has indicated they're over 18
164
+ property :over_18?, from: :over_18
165
+
166
+ # @!attribute [r] gold?
167
+ # @return [Boolean] whether the user currently has gold
168
+ property :gold?, from: :is_gold
169
+
170
+ # @!attribute [r] mod?
171
+ # @return [Boolean] whether the user is a moderator
172
+ property :mod?, from: :is_mod
173
+
174
+ # @!attribute [r] has_verified_email?
175
+ # @return [Boolean] whether the user's email has been verified
176
+ property :has_verified_email?, from: :has_verified_email
177
+
178
+ # @!attribute [r] has_mod_mail?
179
+ # @return [Boolean] whether the user has old-style mod mail
180
+ property :has_mod_mail?, from: :has_mod_mail
181
+
182
+ # @!attribute [r] hidden_from_robots?
183
+ # @return [Boolean] whether the user chose to hide from Google
184
+ property :hidden_from_robots?, from: :hide_from_robots
185
+
186
+ # @!attribute [r] link_karma
187
+ # @return [Integer] the user's link karma
188
+ property :link_karma
189
+
190
+ # @!attribute [r] inbox_count
191
+ # @return [Integer] the number of messages in the user's inbox
192
+ property :inbox_count
193
+
194
+ # @!attribute [r] show_top_karma_subreddits?
195
+ # @return [Boolean] whether top karma subreddits are shown on the user's page
196
+ property :show_top_karma_subreddits?, from: :pref_top_karma_subreddits
197
+
198
+ # @!attribute [r] has_mail?
199
+ # @return [Boolean] whether the user has new messages
200
+ property :has_mail?, from: :has_mail
201
+
202
+ # @!attribute [r] show_snoovatar?
203
+ # @return [Boolean] whether the user's snoovatar is shown
204
+ property :show_snoovatar?, from: :pref_show_snoovatar
205
+
206
+ # @!attribute [r] created_at
207
+ # @return [Time] the time the user signed up
208
+ property :created_at, from: :created_utc, with: ->(epoch) { Time.at(epoch) }
209
+
210
+ # @!attribute [r] gold_creddits
211
+ # @return [Integer] the number of gold creddits the user has
212
+ property :gold_creddits
213
+
214
+ # @!attribute [r] in_beta?
215
+ # @return [Boolean] whether the user is in beta
216
+ property :in_beta?, from: :in_beta
217
+
218
+ # @!attribute [r] comment_karma
219
+ # @return [Integer] the user's comment karma
220
+ property :comment_karma
221
+
222
+ # @!attribute [r] has_subscribed?
223
+ # @return [Boolean]
224
+ property :has_subscribed?, from: :has_subscribed
225
+
226
+ private
227
+
228
+ def lazer_reload
229
+ # return load_from_fullname if self[:id] && !self[:name]
230
+ fully_loaded!
231
+ client.get("/user/#{read_attribute(:name)}/about").body[:data]
232
+ end
233
+
234
+ def load_from_fullname
235
+ client.get('/api/user_data_by_account_ids', ids: read_attribute(:id)).body.values.first
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model'
4
+
5
+ module Redd
6
+ module Models
7
+ # A reddit user.
8
+ class WikiPage < Model
9
+ # Edit the wiki page.
10
+ # @param content [String] the new wiki page contents
11
+ # @param reason [String, nil] an optional reason for editing the page
12
+ def edit(content, reason: nil)
13
+ params = { page: read_attribute(:title), content: content }
14
+ params[:reason] = reason if reason
15
+ client.post("/r/#{read_attribute(:subreddit).display_name}/api/wiki/edit", params)
16
+ end
17
+
18
+ # @!attribute [r] title
19
+ # @return [String] the page title
20
+ property :title, :required
21
+
22
+ # @!attribute [r] subreddit
23
+ # @return [Subreddit] the wiki page's (optional) subreddit
24
+ property :subreddit, :nil
25
+
26
+ # @!attribute [r] may_revise?
27
+ # @return [Boolean] not sure, whether you're allowed to edit the page?
28
+ property :may_revise?, from: :may_revise
29
+
30
+ # @!attribute [r] revision_date
31
+ # @return [Time] the time of the last revision
32
+ property :revision_date, with: ->(t) { Time.at(t) }
33
+
34
+ # @!attribute [r] content_md
35
+ # @return [String] the markdown version of the content
36
+ property :content_md
37
+
38
+ # @!attribute [r] content_html
39
+ # @return [String] the html version of the content
40
+ property :content_html
41
+
42
+ # @!attribute [r] revision_by
43
+ # @return [User] the user who made the last revision
44
+ property :revision_by, with: ->(res) { User.new(client, res[:data]) }
45
+
46
+ private
47
+
48
+ def lazer_reload
49
+ fully_loaded!
50
+ path = "/wiki/#{read_attribute(:title)}"
51
+ path = "/r/#{read_attribute(:subreddit).display_name}#{path}" if exists_locally?(:subreddit)
52
+ client.get(path).body[:data]
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+
5
+ module Redd
6
+ module Utilities
7
+ # Handles response errors in API responses.
8
+ class ErrorHandler
9
+ AUTH_HEADER = 'www-authenticate'
10
+ INVALID_TOKEN = 'invalid_token'
11
+ INSUFFICIENT_SCOPE = 'insufficient_scope'
12
+
13
+ HTTP_ERRORS = {
14
+ 400 => Errors::BadRequest,
15
+ 403 => Errors::Forbidden,
16
+ 404 => Errors::NotFound,
17
+ 429 => Errors::TooManyRequests,
18
+ 500 => Errors::ServerError,
19
+ 502 => Errors::ServerError,
20
+ 503 => Errors::ServerError,
21
+ 504 => Errors::ServerError
22
+ }
23
+
24
+ def check_error(res, raw:)
25
+ # Check for status code-based errors first and return it if we found one.
26
+ error = invalid_access_error(res) || insufficient_scope_error(res) || other_http_error(res)
27
+ return error if error || raw
28
+
29
+ # If there wasn't an status code error and we're allowed to look into the response, parse
30
+ # it and check for errors.
31
+ # TODO: deal with errors of type { fields:, explanation:, message:, reason: }
32
+ rate_limit_error(res) || other_api_error(res)
33
+ end
34
+
35
+ private
36
+
37
+ # Deal with an error caused by having an expired or invalid access token.
38
+ def invalid_access_error(res)
39
+ return nil unless res.code == 401 && res.headers[AUTH_HEADER] &&
40
+ res.headers[AUTH_HEADER].include?(INVALID_TOKEN)
41
+
42
+ Errors::InvalidAccess.new(res)
43
+ end
44
+
45
+ # Deal with an error caused by not having enough the correct scope
46
+ def insufficient_scope_error(res)
47
+ return nil unless res.code == 403 && res.headers[AUTH_HEADER] &&
48
+ res.headers[AUTH_HEADER].include?(INSUFFICIENT_SCOPE)
49
+
50
+ Errors::InsufficientScope.new(res)
51
+ end
52
+
53
+ # Deal with an error signalled by the HTTP response code.
54
+ def other_http_error(res)
55
+ HTTP_ERRORS[res.code].new(res) if HTTP_ERRORS.key?(res.code)
56
+ end
57
+
58
+ def rate_limit_error(res)
59
+ return nil unless res.body.is_a?(Hash) && res.body[:json] && res.body[:json][:ratelimit]
60
+
61
+ Errors::RateLimitError.new(res)
62
+ end
63
+
64
+ # Deal with those annoying errors that come with perfect 200 status codes.
65
+ def other_api_error(res)
66
+ return nil unless res.body.is_a?(Hash) && res.body[:json] && res.body[:json][:errors] &&
67
+ !res.body[:json][:errors].empty?
68
+
69
+ Errors::APIError.new(res)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Redd
4
+ module Utilities
5
+ # Manages rate limiting by sleeping.
6
+ class RateLimiter
7
+ def initialize(gap)
8
+ @gap = gap
9
+ @last_request_time = Time.now - gap
10
+ end
11
+
12
+ def after_limit
13
+ sleep_time = (@last_request_time + @gap) - Time.now
14
+ sleep(sleep_time) if sleep_time > 0.01
15
+ response = yield
16
+ @last_request_time += @gap
17
+ response
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Redd
4
+ module Utilities
5
+ # Unmarshals hashes into objects.
6
+ class Unmarshaller
7
+ # Contains the mapping from 'kind' strings to classes.
8
+ # TODO: UserList type!
9
+ MAPPING = {
10
+ 'Listing' => Models::Listing,
11
+ 't1' => Models::Comment,
12
+ 't2' => Models::User,
13
+ 't3' => Models::Submission,
14
+ 't4' => Models::PrivateMessage,
15
+ 't5' => Models::Subreddit,
16
+ 't6' => Models::Trophy,
17
+ 'more' => Models::MoreComments,
18
+ 'wikipage' => Models::WikiPage,
19
+ 'modaction' => Models::ModAction,
20
+ 'LabeledMulti' => Models::Multireddit,
21
+ 'LiveUpdate' => Models::LiveUpdate
22
+ }
23
+
24
+ def initialize(client)
25
+ @client = client
26
+ end
27
+
28
+ def unmarshal(res)
29
+ # I'm loving the hell out of this pattern.
30
+ model = js_listing(res) || js_model(res) || api_model(res)
31
+ raise "cannot unmarshal: #{res.inspect}" if model.nil?
32
+
33
+ model
34
+ end
35
+
36
+ private
37
+
38
+ # Unmarshal frontent API-style listings
39
+ def js_listing(res)
40
+ # One day I'll get to deprecate Ruby 2.2 and jump into the world of Hash#dig.
41
+ return nil unless res[:json] && res[:json][:data] && res[:json][:data][:things]
42
+
43
+ Models::Listing.new(@client, children: res[:json][:data][:things])
44
+ end
45
+
46
+ # Unmarshal frontend API-style models.
47
+ def js_model(res)
48
+ # FIXME: deprecate this? this shouldn't be happening in the API, so this is better handled
49
+ # in the respective classes.
50
+ Models::Model.new(@client, res[:json][:data]) if res[:json] && res[:json][:data]
51
+ end
52
+
53
+ # Unmarshal API-provided listings.
54
+ def api_listing(res)
55
+ return nil unless res[:kind] == 'Listing'
56
+
57
+ attributes = res[:data]
58
+ attributes[:children].map! { |child| unmarshal(child) }
59
+ Models::Listing.new(@client, attributes)
60
+ end
61
+
62
+ # Unmarshal API-provided model.
63
+ def api_model(res)
64
+ return nil unless MAPPING[res[:kind]]
65
+
66
+ MAPPING[res[:kind]].new(@client, res[:data])
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Redd
4
+ VERSION = '0.9.0'
5
+ end
data/lib/redd.rb ADDED
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ # Redd Version
6
+ require_relative 'redd/version'
7
+ # Models
8
+ Dir[File.join(__dir__, 'redd', 'models', '*.rb')].sort.each { |f| require f }
9
+ # Authentication Clients
10
+ Dir[File.join(__dir__, 'redd', 'auth_strategies', '*.rb')].sort.each { |f| require f }
11
+ # Error Classes
12
+ require_relative 'redd/errors'
13
+ # Regular Client
14
+ require_relative 'redd/api_client'
15
+ # Assists
16
+ Dir[File.join(__dir__, 'redd', 'assist', '*.rb')].sort.each { |f| require f }
17
+
18
+ # Redd is a simple and intuitive API wrapper.
19
+ module Redd
20
+ class << self
21
+ # Based on the arguments you provide it, it guesses the appropriate authentication strategy.
22
+ # You can do this manually with:
23
+ #
24
+ # script = Redd::AuthStrategies::Script.new(**arguments)
25
+ # web = Redd::AuthStrategies::Web.new(**arguments)
26
+ # userless = Redd::AuthStrategies::Userless.new(**arguments)
27
+ #
28
+ # It then creates an {APIClient} with the auth strategy provided and calls authenticate on it:
29
+ #
30
+ # client = Redd::APIClient.new(script); client.authenticate(code)
31
+ # client = Redd::APIClient.new(web); client.authenticate
32
+ # client = Redd::APIClient.new(userless); client.authenticate
33
+ #
34
+ # Finally, it creates the {Models::Session} model, which is essentially a starting point for
35
+ # the user. But you can basically create any model with the client.
36
+ #
37
+ # session = Redd::Models::Session.new(client)
38
+ #
39
+ # user = Redd::Models::User.new(client, name: 'Mustermind')
40
+ # puts user.comment_karma
41
+ #
42
+ # If `auto_refresh` is `false` or if the access doesn't have an associated `expires_in`, you
43
+ # can manually refresh the token by calling:
44
+ #
45
+ # session.client.refresh
46
+ #
47
+ # Also, you can swap out the client's access any time.
48
+ #
49
+ # new_access = { access_token: '', refresh_token: '', expires_in: 1234 }
50
+ #
51
+ # session.client.access = Redd::Models::Access.new(script, new_access)
52
+ # session.client.access = Redd::Models::Access.new(web, new_access)
53
+ # session.client.access = Redd::Models::Access.new(userless, new_access)
54
+ #
55
+ # @see https://www.reddit.com/prefs/apps
56
+ # @param opts [Hash] the options to create the object with
57
+ # @option opts [String] :user_agent your app's *unique* and *descriptive* user agent
58
+ # @option opts [String] :client_id the client id of your app
59
+ # @option opts [String] :secret the app secret (for confidential types, i.e. *not* *installed*)
60
+ # @option opts [String] :username the username of your bot (only for *script*)
61
+ # @option opts [String] :password the plaintext password of your bot (only for *script*)
62
+ # @option opts [String] :redirect_uri the provided redirect URI (only for *web* and *installed*)
63
+ # @option opts [String] :code the code given by reddit (required for *web* and *installed*)
64
+ # @return [Models::Session] a fresh {Models::Session} for you to make requests with
65
+ def it(opts = {})
66
+ api_client = script(opts) || web(opts) || userless(opts)
67
+ raise "couldn't guess app type" unless api_client
68
+
69
+ Models::Session.new(api_client)
70
+ end
71
+
72
+ # Create a url to send to users for authorization.
73
+ # @param response_type ['code', 'token'] the type of response from reddit
74
+ # @param state [String] a randomly generated token to avoid CSRF attacks.
75
+ # @param client_id [String] the client id of the app
76
+ # @param redirect_uri [String] the URI for reddit to redirect to after authorization
77
+ # @param scope [Array<String>] an array of scopes to request
78
+ # @param duration ['temporary', 'permanent'] the duration to request the code for (only applies
79
+ # when response_type is 'code')
80
+ # @return [String] the generated url
81
+ def url(client_id:, redirect_uri:, response_type: 'code', state: '', scope: ['identity'],
82
+ duration: 'temporary')
83
+ "https://www.reddit.com/api/v1/authorize?#{
84
+ URI.encode_www_form(
85
+ client_id: client_id,
86
+ redirect_uri: redirect_uri,
87
+ state: state,
88
+ scope: scope.join(','),
89
+ response_type: response_type,
90
+ duration: duration
91
+ )
92
+ }"
93
+ end
94
+
95
+ private
96
+
97
+ def filter_auth(opts)
98
+ opts.select { |k| %i[client_id secret username password redirect_uri user_agent].include?(k) }
99
+ end
100
+
101
+ def filter_api(opts)
102
+ opts.select { |k| %i[user_agent limit_time max_retries auto_refresh].include?(k) }
103
+ end
104
+
105
+ def script(opts = {})
106
+ return unless %i[client_id secret username password].all? { |o| opts.include?(o) }
107
+
108
+ auth = AuthStrategies::Script.new(**filter_auth(opts))
109
+ api = APIClient.new(auth, **filter_api(opts))
110
+ api.tap(&:authenticate)
111
+ end
112
+
113
+ def web(opts = {})
114
+ return unless %i[client_id redirect_uri code].all? { |o| opts.include?(o) }
115
+
116
+ auth = AuthStrategies::Web.new(**filter_auth(opts))
117
+ api = APIClient.new(auth, **filter_api(opts))
118
+ api.tap { |c| c.authenticate(opts[:code]) }
119
+ end
120
+
121
+ def userless(opts = {})
122
+ return unless %i[client_id secret].all? { |o| opts.include?(o) }
123
+
124
+ auth = AuthStrategies::Userless.new(**filter_auth(opts))
125
+ api = APIClient.new(auth, **filter_api(opts))
126
+ api.tap(&:authenticate)
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redd'
data/logo.png ADDED
Binary file
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'redd/version'
6
+
7
+ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
8
+ spec.name = 'spinels-redd'
9
+ spec.version = Redd::VERSION
10
+ spec.authors = ['Avinash Dwarapu']
11
+ spec.email = ['avinash@dwarapu.me']
12
+ spec.summary = 'A batteries-included API wrapper for reddit.'
13
+ spec.homepage = 'https://github.com/spinels/redd'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 2.6'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = 'exe'
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_dependency 'http', '>= 4.0', '< 6.0'
25
+ spec.add_dependency 'lazy_lazer', '~> 0.8.1'
26
+
27
+ spec.add_development_dependency 'bundler', '~> 2.0'
28
+ spec.add_development_dependency 'pry', '~> 0.10'
29
+ spec.add_development_dependency 'rake', '~> 13.0'
30
+ spec.add_development_dependency 'rubocop', '~> 1.26'
31
+ spec.add_development_dependency 'yard', '~> 0.9.9'
32
+
33
+ spec.add_development_dependency 'guard', '~> 2.14'
34
+ spec.add_development_dependency 'guard-rspec', '~> 4.7'
35
+ spec.add_development_dependency 'rspec', '~> 3.5'
36
+ spec.add_development_dependency 'simplecov', '~> 0.13'
37
+ spec.add_development_dependency 'vcr', '~> 6.1'
38
+ spec.add_development_dependency 'webmock', '~> 3.14'
39
+ end