y_fantasy 0.1.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 (85) hide show
  1. checksums.yaml +7 -0
  2. data/.pryrc +8 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +20 -0
  5. data/.standard.yml +2 -0
  6. data/CHANGELOG.md +5 -0
  7. data/CODE_OF_CONDUCT.md +3 -0
  8. data/Gemfile +5 -0
  9. data/Gemfile.lock +220 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +41 -0
  12. data/Rakefile +12 -0
  13. data/exe/y_fantasy +9 -0
  14. data/lib/y_fantasy/api/authentication.rb +177 -0
  15. data/lib/y_fantasy/api/client.rb +84 -0
  16. data/lib/y_fantasy/api/subresource_param_builder.rb +138 -0
  17. data/lib/y_fantasy/api/url_builder.rb +91 -0
  18. data/lib/y_fantasy/cli.rb +46 -0
  19. data/lib/y_fantasy/concerns/subresourceable.rb +87 -0
  20. data/lib/y_fantasy/ref/nfl.rb +57 -0
  21. data/lib/y_fantasy/resources/base_resource.rb +117 -0
  22. data/lib/y_fantasy/resources/base_subresource.rb +31 -0
  23. data/lib/y_fantasy/resources/game/game_week.rb +26 -0
  24. data/lib/y_fantasy/resources/game/position_type.rb +20 -0
  25. data/lib/y_fantasy/resources/game.rb +136 -0
  26. data/lib/y_fantasy/resources/group/settings.rb +53 -0
  27. data/lib/y_fantasy/resources/group/standings.rb +11 -0
  28. data/lib/y_fantasy/resources/group.rb +76 -0
  29. data/lib/y_fantasy/resources/league/scoreboard.rb +12 -0
  30. data/lib/y_fantasy/resources/league/settings.rb +51 -0
  31. data/lib/y_fantasy/resources/league/standings.rb +25 -0
  32. data/lib/y_fantasy/resources/league.rb +187 -0
  33. data/lib/y_fantasy/resources/pickem_team/pick.rb +31 -0
  34. data/lib/y_fantasy/resources/pickem_team/week_pick.rb +28 -0
  35. data/lib/y_fantasy/resources/pickem_team.rb +98 -0
  36. data/lib/y_fantasy/resources/player/draft_analysis.rb +26 -0
  37. data/lib/y_fantasy/resources/player/ownership_percentage.rb +26 -0
  38. data/lib/y_fantasy/resources/player/stat_collection.rb +32 -0
  39. data/lib/y_fantasy/resources/player.rb +157 -0
  40. data/lib/y_fantasy/resources/shared_subresources/draft_result.rb +39 -0
  41. data/lib/y_fantasy/resources/shared_subresources/matchup.rb +61 -0
  42. data/lib/y_fantasy/resources/shared_subresources/roster_position.rb +38 -0
  43. data/lib/y_fantasy/resources/shared_subresources/stat.rb +16 -0
  44. data/lib/y_fantasy/resources/shared_subresources/stat_category.rb +22 -0
  45. data/lib/y_fantasy/resources/shared_subresources/stat_modifier.rb +10 -0
  46. data/lib/y_fantasy/resources/shared_subresources/stat_winner.rb +8 -0
  47. data/lib/y_fantasy/resources/team/manager.rb +23 -0
  48. data/lib/y_fantasy/resources/team/roster.rb +18 -0
  49. data/lib/y_fantasy/resources/team/standings.rb +33 -0
  50. data/lib/y_fantasy/resources/team/stat_collection.rb +38 -0
  51. data/lib/y_fantasy/resources/team.rb +175 -0
  52. data/lib/y_fantasy/subresource_validator.rb +67 -0
  53. data/lib/y_fantasy/transformations/base_transform.rb +62 -0
  54. data/lib/y_fantasy/transformations/collection_transformer.rb +22 -0
  55. data/lib/y_fantasy/transformations/default_transformer.rb +22 -0
  56. data/lib/y_fantasy/transformations/game/position_types_transformer.rb +21 -0
  57. data/lib/y_fantasy/transformations/game_transformer.rb +54 -0
  58. data/lib/y_fantasy/transformations/group_transformer.rb +39 -0
  59. data/lib/y_fantasy/transformations/instantiator.rb +25 -0
  60. data/lib/y_fantasy/transformations/key_unwrapper.rb +12 -0
  61. data/lib/y_fantasy/transformations/league/scoreboard_transformer.rb +21 -0
  62. data/lib/y_fantasy/transformations/league/settings_transformer.rb +25 -0
  63. data/lib/y_fantasy/transformations/league/standings_transformer.rb +22 -0
  64. data/lib/y_fantasy/transformations/league_transformer.rb +57 -0
  65. data/lib/y_fantasy/transformations/matchups_transformer.rb +22 -0
  66. data/lib/y_fantasy/transformations/pickem_team/week_picks_transformer.rb +29 -0
  67. data/lib/y_fantasy/transformations/pickem_team_transformer.rb +41 -0
  68. data/lib/y_fantasy/transformations/player/ownership_percentage_transformer.rb +21 -0
  69. data/lib/y_fantasy/transformations/player/stats_transformer.rb +32 -0
  70. data/lib/y_fantasy/transformations/player_transformer.rb +34 -0
  71. data/lib/y_fantasy/transformations/stat_categories_transformer.rb +19 -0
  72. data/lib/y_fantasy/transformations/stat_modifiers_transformer.rb +19 -0
  73. data/lib/y_fantasy/transformations/t.rb +44 -0
  74. data/lib/y_fantasy/transformations/team/manager_transformer.rb +19 -0
  75. data/lib/y_fantasy/transformations/team/roster_transformer.rb +27 -0
  76. data/lib/y_fantasy/transformations/team/standings_transformer.rb +42 -0
  77. data/lib/y_fantasy/transformations/team/stats_transformer.rb +30 -0
  78. data/lib/y_fantasy/transformations/team_transformer.rb +54 -0
  79. data/lib/y_fantasy/transformations/user_transformer.rb +17 -0
  80. data/lib/y_fantasy/transformations.rb +54 -0
  81. data/lib/y_fantasy/version.rb +5 -0
  82. data/lib/y_fantasy.rb +36 -0
  83. data/sig/y_fantasy.rbs +4 -0
  84. data/y_fantasy.gemspec +49 -0
  85. metadata +364 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4fa3a23dc0749a4169ee95d862127a9f88d85651439121a5cd694a541d7ec2e6
4
+ data.tar.gz: b7a3fc4a5c320c29eefbbbedbfdd55983dc2c737eb75ab6c5120927b9c04f455
5
+ SHA512:
6
+ metadata.gz: a8b0e3a377c985aa5c90caa3e543d6cff2628f31dfb53826a1e37c15a0a3727687ec2ecc25f739f3d0ba8450d238a468b6b16751c9c20e0cd72c6f11ab805a52
7
+ data.tar.gz: 57d2e8415bd4f77426f4841b8e7ed23eb1d3848163f90eba7f2a6c4acd3efdd6c4466d0ff9adca6d72745b20043e4d433c5b34b1a52aeea361e735886016825e
data/.pryrc ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(PryByebug)
4
+ Pry.commands.alias_command "c", "continue"
5
+ Pry.commands.alias_command "s", "step"
6
+ Pry.commands.alias_command "n", "next"
7
+ Pry.commands.alias_command "f", "finish"
8
+ end
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format progress
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,20 @@
1
+ require: standard
2
+
3
+ inherit_gem:
4
+ standard: config/base.yml
5
+
6
+ AllCops:
7
+ TargetRubyVersion: 3.1
8
+ Exclude:
9
+ - "scratch/**/*"
10
+
11
+ Style/FrozenStringLiteralComment:
12
+ Enabled: true
13
+
14
+ Style/StringLiterals:
15
+ Enabled: true
16
+ EnforcedStyle: double_quotes
17
+
18
+ Style/StringLiteralsInInterpolation:
19
+ Enabled: true
20
+ EnforcedStyle: double_quotes
data/.standard.yml ADDED
@@ -0,0 +1,2 @@
1
+ ignore:
2
+ - "scratch/**/*"
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-07-11
4
+
5
+ - Initial release
@@ -0,0 +1,3 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ Be kind.
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,220 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ y_fantasy (0.1.0)
5
+ dry-configurable (~> 1.0)
6
+ dry-initializer (~> 3.0)
7
+ dry-transformer (~> 1.0)
8
+ dry-types (~> 1.0)
9
+ dry-validation (~> 1.0)
10
+ mechanize (~> 2.9)
11
+ ox (~> 2.14)
12
+ thor (~> 1.3)
13
+ zeitwerk (~> 2.5)
14
+
15
+ GEM
16
+ remote: https://rubygems.org/
17
+ specs:
18
+ activesupport (7.1.5.1)
19
+ base64
20
+ benchmark (>= 0.3)
21
+ bigdecimal
22
+ concurrent-ruby (~> 1.0, >= 1.0.2)
23
+ connection_pool (>= 2.2.5)
24
+ drb
25
+ i18n (>= 1.6, < 2)
26
+ logger (>= 1.4.2)
27
+ minitest (>= 5.1)
28
+ mutex_m
29
+ securerandom (>= 0.3)
30
+ tzinfo (~> 2.0)
31
+ addressable (2.8.7)
32
+ public_suffix (>= 2.0.2, < 7.0)
33
+ ast (2.4.2)
34
+ base64 (0.2.0)
35
+ benchmark (0.4.0)
36
+ bigdecimal (3.1.5)
37
+ byebug (11.1.3)
38
+ coderay (1.1.3)
39
+ concurrent-ruby (1.2.2)
40
+ connection_pool (2.5.0)
41
+ crack (0.4.5)
42
+ rexml
43
+ diff-lcs (1.5.0)
44
+ docile (1.4.1)
45
+ domain_name (0.6.20240107)
46
+ drb (2.2.1)
47
+ dry-configurable (1.1.0)
48
+ dry-core (~> 1.0, < 2)
49
+ zeitwerk (~> 2.6)
50
+ dry-core (1.0.1)
51
+ concurrent-ruby (~> 1.0)
52
+ zeitwerk (~> 2.6)
53
+ dry-inflector (1.0.0)
54
+ dry-initializer (3.1.1)
55
+ dry-logic (1.5.0)
56
+ concurrent-ruby (~> 1.0)
57
+ dry-core (~> 1.0, < 2)
58
+ zeitwerk (~> 2.6)
59
+ dry-schema (1.13.3)
60
+ concurrent-ruby (~> 1.0)
61
+ dry-configurable (~> 1.0, >= 1.0.1)
62
+ dry-core (~> 1.0, < 2)
63
+ dry-initializer (~> 3.0)
64
+ dry-logic (>= 1.4, < 2)
65
+ dry-types (>= 1.7, < 2)
66
+ zeitwerk (~> 2.6)
67
+ dry-transformer (1.0.1)
68
+ zeitwerk (~> 2.6)
69
+ dry-types (1.7.2)
70
+ bigdecimal (~> 3.0)
71
+ concurrent-ruby (~> 1.0)
72
+ dry-core (~> 1.0)
73
+ dry-inflector (~> 1.0)
74
+ dry-logic (~> 1.4)
75
+ zeitwerk (~> 2.6)
76
+ dry-validation (1.10.0)
77
+ concurrent-ruby (~> 1.0)
78
+ dry-core (~> 1.0, < 2)
79
+ dry-initializer (~> 3.0)
80
+ dry-schema (>= 1.12, < 2)
81
+ zeitwerk (~> 2.6)
82
+ factory_bot (6.5.0)
83
+ activesupport (>= 5.0.0)
84
+ hashdiff (1.1.0)
85
+ http-cookie (1.0.8)
86
+ domain_name (~> 0.5)
87
+ i18n (1.14.7)
88
+ concurrent-ruby (~> 1.0)
89
+ json (2.9.1)
90
+ language_server-protocol (3.17.0.3)
91
+ lint_roller (1.1.0)
92
+ logger (1.6.5)
93
+ mechanize (2.14.0)
94
+ addressable (~> 2.8)
95
+ base64
96
+ domain_name (~> 0.5, >= 0.5.20190701)
97
+ http-cookie (~> 1.0, >= 1.0.3)
98
+ mime-types (~> 3.3)
99
+ net-http-digest_auth (~> 1.4, >= 1.4.1)
100
+ net-http-persistent (>= 2.5.2, < 5.0.dev)
101
+ nkf
102
+ nokogiri (~> 1.11, >= 1.11.2)
103
+ rubyntlm (~> 0.6, >= 0.6.3)
104
+ webrick (~> 1.7)
105
+ webrobots (~> 0.1.2)
106
+ method_source (1.1.0)
107
+ mime-types (3.6.0)
108
+ logger
109
+ mime-types-data (~> 3.2015)
110
+ mime-types-data (3.2025.0107)
111
+ minitest (5.25.4)
112
+ mutex_m (0.3.0)
113
+ net-http-digest_auth (1.4.1)
114
+ net-http-persistent (4.0.5)
115
+ connection_pool (~> 2.2)
116
+ nkf (0.2.0)
117
+ nokogiri (1.18.2-arm64-darwin)
118
+ racc (~> 1.4)
119
+ nokogiri (1.18.2-x86_64-darwin)
120
+ racc (~> 1.4)
121
+ ox (2.14.20)
122
+ bigdecimal (>= 3.0)
123
+ parallel (1.26.3)
124
+ parser (3.3.7.0)
125
+ ast (~> 2.4.1)
126
+ racc
127
+ pry (0.14.2)
128
+ coderay (~> 1.1)
129
+ method_source (~> 1.0)
130
+ pry-byebug (3.10.1)
131
+ byebug (~> 11.0)
132
+ pry (>= 0.13, < 0.15)
133
+ public_suffix (6.0.1)
134
+ racc (1.8.1)
135
+ rainbow (3.1.1)
136
+ rake (13.1.0)
137
+ regexp_parser (2.10.0)
138
+ rexml (3.4.0)
139
+ rspec (3.12.0)
140
+ rspec-core (~> 3.12.0)
141
+ rspec-expectations (~> 3.12.0)
142
+ rspec-mocks (~> 3.12.0)
143
+ rspec-core (3.12.2)
144
+ rspec-support (~> 3.12.0)
145
+ rspec-expectations (3.12.3)
146
+ diff-lcs (>= 1.2.0, < 2.0)
147
+ rspec-support (~> 3.12.0)
148
+ rspec-mocks (3.12.6)
149
+ diff-lcs (>= 1.2.0, < 2.0)
150
+ rspec-support (~> 3.12.0)
151
+ rspec-support (3.12.1)
152
+ rubocop (1.70.0)
153
+ json (~> 2.3)
154
+ language_server-protocol (>= 3.17.0)
155
+ parallel (~> 1.10)
156
+ parser (>= 3.3.0.2)
157
+ rainbow (>= 2.2.2, < 4.0)
158
+ regexp_parser (>= 2.9.3, < 3.0)
159
+ rubocop-ast (>= 1.36.2, < 2.0)
160
+ ruby-progressbar (~> 1.7)
161
+ unicode-display_width (>= 2.4.0, < 4.0)
162
+ rubocop-ast (1.37.0)
163
+ parser (>= 3.3.1.0)
164
+ rubocop-performance (1.23.1)
165
+ rubocop (>= 1.48.1, < 2.0)
166
+ rubocop-ast (>= 1.31.1, < 2.0)
167
+ ruby-progressbar (1.13.0)
168
+ rubyntlm (0.6.5)
169
+ base64
170
+ securerandom (0.4.1)
171
+ simplecov (0.22.0)
172
+ docile (~> 1.1)
173
+ simplecov-html (~> 0.11)
174
+ simplecov_json_formatter (~> 0.1)
175
+ simplecov-html (0.13.1)
176
+ simplecov_json_formatter (0.1.4)
177
+ standard (1.44.0)
178
+ language_server-protocol (~> 3.17.0.2)
179
+ lint_roller (~> 1.0)
180
+ rubocop (~> 1.70.0)
181
+ standard-custom (~> 1.0.0)
182
+ standard-performance (~> 1.6)
183
+ standard-custom (1.0.2)
184
+ lint_roller (~> 1.0)
185
+ rubocop (~> 1.50)
186
+ standard-performance (1.6.0)
187
+ lint_roller (~> 1.1)
188
+ rubocop-performance (~> 1.23.0)
189
+ thor (1.3.2)
190
+ tzinfo (2.0.6)
191
+ concurrent-ruby (~> 1.0)
192
+ unicode-display_width (3.1.4)
193
+ unicode-emoji (~> 4.0, >= 4.0.4)
194
+ unicode-emoji (4.0.4)
195
+ webmock (3.19.1)
196
+ addressable (>= 2.8.0)
197
+ crack (>= 0.3.2)
198
+ hashdiff (>= 0.4.0, < 2.0.0)
199
+ webrick (1.9.1)
200
+ webrobots (0.1.2)
201
+ zeitwerk (2.6.12)
202
+
203
+ PLATFORMS
204
+ arm64-darwin-21
205
+ arm64-darwin-23
206
+ x86_64-darwin-21
207
+
208
+ DEPENDENCIES
209
+ bundler (~> 2.3)
210
+ factory_bot (~> 6.5)
211
+ pry-byebug (~> 3.10)
212
+ rake (~> 13.0)
213
+ rspec (~> 3.0)
214
+ simplecov (~> 0.20)
215
+ standard (~> 1.10)
216
+ webmock (~> 3.0)
217
+ y_fantasy!
218
+
219
+ BUNDLED WITH
220
+ 2.3.7
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 eaylward8
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # YFantasy
2
+
3
+ A Ruby wrapper for the Yahoo Fantasy Sports API.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'y_fantasy'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install y_fantasy
20
+
21
+ ## Usage
22
+
23
+ TBD
24
+
25
+ ## Development
26
+
27
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
28
+
29
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
30
+
31
+ ## Contributing
32
+
33
+ Bug reports and pull requests are welcome on GitHub at https://github.com/eaylward8/y_fantasy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/eaylward8/y_fantasy/blob/master/CODE_OF_CONDUCT.md).
34
+
35
+ ## License
36
+
37
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
38
+
39
+ ## Code of Conduct
40
+
41
+ Everyone interacting in the YFantasy project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/eaylward8/y_fantasy/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/exe/y_fantasy ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Add lib directory to load path
4
+ # lib_path = File.expand_path("../lib", __dir__)
5
+ # $LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
6
+
7
+ require "y_fantasy/cli"
8
+
9
+ YFantasy::CLI.start
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "net/http"
5
+ require "uri"
6
+ require "mechanize"
7
+ require "timeout"
8
+
9
+ module YFantasy
10
+ module Api
11
+ class Authentication
12
+ YAHOO_CLIENT_ID = YFantasy.config.yahoo_client_id
13
+ YAHOO_CLIENT_SECRET = YFantasy.config.yahoo_client_secret
14
+ YAHOO_USERNAME = YFantasy.config.yahoo_username
15
+ YAHOO_PASSWORD = YFantasy.config.yahoo_password
16
+
17
+ REQUEST_AUTH_URL = "https://api.login.yahoo.com/oauth2/request_auth?client_id=#{YAHOO_CLIENT_ID}&redirect_uri=oob&response_type=code"
18
+ GET_TOKEN_URL = "https://api.login.yahoo.com/oauth2/get_token"
19
+
20
+ MSG_SEPARATOR = "#" * 50
21
+
22
+ # NOTE: access token expires in 1 hour (3600 seconds)
23
+
24
+ class << self
25
+ # TODO: I don't think any of these readers are needed besides access_token. Maybe the error ones?
26
+ attr_reader :access_token, :expires_at, :refresh_token, :error_type, :error_desc
27
+
28
+ def authenticate
29
+ return true if access_token_valid?
30
+
31
+ if YFantasy.config.yahoo_refresh_token
32
+ @refresh_token = YFantasy.config.yahoo_refresh_token
33
+ end
34
+
35
+ refresh_token? ? authenticate_with_refresh_token : authenticate_with_code
36
+ end
37
+
38
+ def access_token_valid?
39
+ !!access_token && !!expires_at && Time.now.to_i < expires_at
40
+ end
41
+
42
+ private
43
+
44
+ def authenticate_with_refresh_token
45
+ puts "auth with refresh token"
46
+ response = post(GET_TOKEN_URL, post_data("refresh_token", refresh_token: refresh_token))
47
+ handle_response(response)
48
+ end
49
+
50
+ def authenticate_with_code
51
+ puts "auth with code"
52
+ code = YFantasy.config.automate_login ? get_auth_code_automated : get_auth_code_manual
53
+ response = post(GET_TOKEN_URL, post_data("authorization_code", code: code))
54
+ handle_response(response, print_tokens: !YFantasy.config.automate_login)
55
+ end
56
+
57
+ def handle_response(response, print_tokens: false)
58
+ body = JSON.parse(response.body)
59
+
60
+ case response
61
+ when Net::HTTPSuccess
62
+ display_token_message(body) if print_tokens # Display token data to user after manual login
63
+ set_token_data(body["access_token"], body["expires_in"], body["refresh_token"])
64
+ clear_error_data
65
+ true
66
+ when Net::HTTPClientError, Net::HTTPServerError
67
+ set_error_data(body["error"], body["error_description"])
68
+ false
69
+ end
70
+ end
71
+
72
+ def display_token_message(body)
73
+ puts "\nAccess token: #{body["access_token"]}"
74
+ puts "\nAccess token expires in: #{body["expires_in"]} seconds"
75
+ puts "\nRefresh token: #{body["refresh_token"]}"
76
+ puts "\nThe refresh token is long-lived and can be used to re-authenticate without the need for credentials."
77
+ puts "Store it as an ENV variable called YAHOO_REFRESH_TOKEN or save it to a database."
78
+ puts "Then, in your application, set YFantasy.config.yahoo_refresh_token = <your_refresh_token>"
79
+ puts "If the yahoo_refresh_token config is set, YFantasy will use it to obtain a new access token when needed."
80
+ puts "\nRead more about the Yahoo OAuth 2.0 Flow: https://developer.yahoo.com/oauth2/guide/flows_authcode/\n\n"
81
+ puts MSG_SEPARATOR
82
+ puts "# YFantasy Manual Auth Code Flow - END #"
83
+ puts MSG_SEPARATOR
84
+ end
85
+
86
+ def get_auth_code_manual
87
+ return if YFantasy.config.automate_login
88
+
89
+ puts MSG_SEPARATOR
90
+ puts "# YFantasy Manual Auth Code Flow - START #"
91
+ puts MSG_SEPARATOR
92
+ puts "\nGo to this URL and log into your Yahoo Account:"
93
+ puts REQUEST_AUTH_URL
94
+ puts "\nAfter logging in, copy and paste the code here:"
95
+ Timeout.timeout(YFantasy.config.manual_login_timeout_seconds) do
96
+ code = $stdin.gets.chomp
97
+ puts "\nReceived code: #{code}. Attempting to retrieve access token and refresh token...\n"
98
+ code
99
+ end
100
+ end
101
+
102
+ # :nocov:
103
+ def get_auth_code_automated
104
+ return if YFantasy.config.automate_login == false
105
+
106
+ # TODO: error handling
107
+ agent = Mechanize.new
108
+ login_page1 = agent.get(REQUEST_AUTH_URL)
109
+ username_form = login_page1.forms.first
110
+ # Enter username
111
+ username_form.username = YAHOO_USERNAME
112
+ # Click "Next" button
113
+ login_page2 = agent.submit(username_form, username_form.buttons.first)
114
+ pw_form = login_page2.forms.first
115
+ # Enter password
116
+ pw_form.password = YAHOO_PASSWORD
117
+ # Click "Verify password"
118
+ auth_code_page = agent.submit(pw_form, pw_form.button("verifyPassword"))
119
+ # Extract code
120
+ # TODO: sometimes below line fails w/ undefined method [] for nil, add a retry?
121
+ match = auth_code_page.uri.query.match(/code=(?<code>\w+)/)
122
+ return match[:code] if match
123
+
124
+ raise Error.new("Failed to extract auth code")
125
+ end
126
+ # :nocov:
127
+
128
+ def post(url, data)
129
+ Net::HTTP.post(URI(url), URI.encode_www_form(data), post_headers)
130
+ end
131
+
132
+ def post_headers
133
+ {
134
+ "Authorization" => "Basic #{basic_auth_token}",
135
+ "Content-Type" => "application/x-www-form-urlencoded"
136
+ }
137
+ end
138
+
139
+ def post_data(grant_type, refresh_token: nil, code: nil)
140
+ {
141
+ "grant_type" => grant_type,
142
+ "redirect_uri" => "oob",
143
+ "refresh_token" => refresh_token,
144
+ "code" => code
145
+ }
146
+ end
147
+
148
+ def basic_auth_token
149
+ @basic_auth_token ||= Base64.strict_encode64("#{YAHOO_CLIENT_ID}:#{YAHOO_CLIENT_SECRET}")
150
+ end
151
+
152
+ def set_token_data(access_token, expires_in, refresh_token)
153
+ @access_token = access_token
154
+ @expires_at = Time.now.to_i + expires_in.to_i
155
+ @refresh_token = refresh_token
156
+ end
157
+
158
+ def set_error_data(type, desc)
159
+ @error_type = type
160
+ @error_desc = desc
161
+ end
162
+
163
+ def clear_error_data
164
+ @error_type = nil
165
+ @error_desc = nil
166
+ end
167
+
168
+ def refresh_token?
169
+ @refresh_token && @refresh_token.length > 0
170
+ end
171
+ end
172
+
173
+ class Error < StandardError
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+ require "ox"
7
+
8
+ module YFantasy
9
+ module Api
10
+ class Client
11
+ @@retry = true
12
+
13
+ def self.get(resource, keys: [], game_codes: [], subresources: [], **options)
14
+ new.get(resource, keys: keys, game_codes: game_codes, subresources: subresources, **options)
15
+ rescue Client::Error, Authentication::Error
16
+ if @@retry
17
+ @@retry = false
18
+ retry
19
+ end
20
+
21
+ raise
22
+ end
23
+
24
+ def initialize
25
+ authenticate
26
+ end
27
+
28
+ # NOTE: URL construction needs to be sophisticated enough to know when it should use "out" params vs
29
+ # nested subresources.
30
+ # E.g. /league/380.l.190823/teams/stats
31
+ # this is how you get 1 league with all teams, and all stats for each team
32
+ # teams is a subresource of league
33
+ # stats is a subresource of teams
34
+ #
35
+ #
36
+ # This fails because "stats" is not a subresource of league: /league/380.l.190823;out=teams/stats
37
+
38
+ def get(resource, keys: [], game_codes: [], subresources: [], **options)
39
+ refresh_access_token_if_needed
40
+
41
+ url = UrlBuilder.new(resource, keys: keys, game_codes: game_codes, subresources: subresources, **options).build
42
+
43
+ # puts "\n Client#get #{url} \n" # TODO: remove
44
+
45
+ response = Net::HTTP.get_response(URI(url), "Authorization" => "Bearer #{@access_token}")
46
+ body = response.body
47
+
48
+ case response
49
+ when Net::HTTPSuccess
50
+ Ox.load(body, mode: :hash_no_attrs).fetch(:fantasy_content)
51
+ when Net::HTTPClientError
52
+ error = Ox.load(body, mode: :hash_no_attrs)
53
+ error_msg = error.dig(:"yahoo:error", :"yahoo:description") || error.dig(:error, :description)
54
+ msg = "#{response.code}: #{error_msg}"
55
+ raise YFantasy::Api::Client::Error.new(msg)
56
+ else
57
+ raise YFantasy::Api::Client::Error.new("An unknown error occurred")
58
+ end
59
+ end
60
+
61
+ def authenticate
62
+ if YFantasy::Api::Authentication.authenticate
63
+ @access_token = YFantasy::Api::Authentication.access_token
64
+ @refresh_token = YFantasy::Api::Authentication.refresh_token
65
+ else
66
+ @error_type = YFantasy::Api::Authentication.error_type
67
+ @error_desc = YFantasy::Api::Authentication.error_desc
68
+ end
69
+ end
70
+
71
+ def refresh_access_token_if_needed
72
+ return if YFantasy::Api::Authentication.access_token_valid?
73
+
74
+ authenticate
75
+ end
76
+
77
+ class Error < StandardError
78
+ def initialize(msg = "")
79
+ super
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end