bungie_sdk 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +147 -0
  5. data/.solargraph.yml +23 -0
  6. data/.travis.yml +6 -0
  7. data/.vim/coc-settings.json +12 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +19 -0
  10. data/Gemfile.lock +133 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +56 -0
  13. data/Rakefile +6 -0
  14. data/bin/console +15 -0
  15. data/bin/setup +8 -0
  16. data/bin/tapioca +29 -0
  17. data/bungie_sdk.gemspec +35 -0
  18. data/lib/bungie_sdk/agent.rb +166 -0
  19. data/lib/bungie_sdk/character.rb +82 -0
  20. data/lib/bungie_sdk/client.rb +72 -0
  21. data/lib/bungie_sdk/item.rb +83 -0
  22. data/lib/bungie_sdk/membership.rb +30 -0
  23. data/lib/bungie_sdk/profile.rb +36 -0
  24. data/lib/bungie_sdk/token_manager.rb +136 -0
  25. data/lib/bungie_sdk/vendor.rb +64 -0
  26. data/lib/bungie_sdk/version.rb +4 -0
  27. data/lib/bungie_sdk.rb +104 -0
  28. data/sorbet/config +3 -0
  29. data/sorbet/rbi/gems/addressable.rbi +151 -0
  30. data/sorbet/rbi/gems/addressable@2.8.0.rbi +224 -0
  31. data/sorbet/rbi/gems/ast@2.4.2.rbi +54 -0
  32. data/sorbet/rbi/gems/bungie_sdk.rbi +15 -0
  33. data/sorbet/rbi/gems/coderay.rbi +285 -0
  34. data/sorbet/rbi/gems/coderay@1.1.3.rbi +1005 -0
  35. data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +185 -0
  36. data/sorbet/rbi/gems/dotenv.rbi +68 -0
  37. data/sorbet/rbi/gems/dotenv@2.7.6.rbi +88 -0
  38. data/sorbet/rbi/gems/ethon.rbi +716 -0
  39. data/sorbet/rbi/gems/ethon@0.15.0.rbi +883 -0
  40. data/sorbet/rbi/gems/faraday-net_http.rbi +33 -0
  41. data/sorbet/rbi/gems/faraday-net_http@2.0.1.rbi +78 -0
  42. data/sorbet/rbi/gems/faraday.rbi +696 -0
  43. data/sorbet/rbi/gems/faraday@2.2.0.rbi +685 -0
  44. data/sorbet/rbi/gems/ffi.rbi +560 -0
  45. data/sorbet/rbi/gems/ffi@1.15.5.rbi +849 -0
  46. data/sorbet/rbi/gems/gem-release.rbi +582 -0
  47. data/sorbet/rbi/gems/gem-release@2.2.2.rbi +644 -0
  48. data/sorbet/rbi/gems/hashie.rbi +160 -0
  49. data/sorbet/rbi/gems/jwt.rbi +274 -0
  50. data/sorbet/rbi/gems/jwt@2.3.0.rbi +437 -0
  51. data/sorbet/rbi/gems/launchy.rbi +226 -0
  52. data/sorbet/rbi/gems/launchy@2.5.0.rbi +327 -0
  53. data/sorbet/rbi/gems/method_source.rbi +64 -0
  54. data/sorbet/rbi/gems/method_source@1.0.0.rbi +72 -0
  55. data/sorbet/rbi/gems/multi_json.rbi +62 -0
  56. data/sorbet/rbi/gems/multi_json@1.15.0.rbi +96 -0
  57. data/sorbet/rbi/gems/multi_xml.rbi +35 -0
  58. data/sorbet/rbi/gems/multi_xml@0.6.0.rbi +36 -0
  59. data/sorbet/rbi/gems/oauth2.rbi +143 -0
  60. data/sorbet/rbi/gems/oauth2@1.4.9.rbi +181 -0
  61. data/sorbet/rbi/gems/parallel@1.22.1.rbi +8 -0
  62. data/sorbet/rbi/gems/parser@3.1.1.0.rbi +1196 -0
  63. data/sorbet/rbi/gems/pry.rbi +1898 -0
  64. data/sorbet/rbi/gems/pry@0.14.1.rbi +2486 -0
  65. data/sorbet/rbi/gems/public_suffix.rbi +104 -0
  66. data/sorbet/rbi/gems/public_suffix@4.0.6.rbi +145 -0
  67. data/sorbet/rbi/gems/rack.rbi +21 -0
  68. data/sorbet/rbi/gems/rack@2.2.3.rbi +1622 -0
  69. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +8 -0
  70. data/sorbet/rbi/gems/rake.rbi +644 -0
  71. data/sorbet/rbi/gems/rake@12.3.3.rbi +804 -0
  72. data/sorbet/rbi/gems/rbi@0.0.14.rbi +2073 -0
  73. data/sorbet/rbi/gems/regexp_parser@2.3.0.rbi +8 -0
  74. data/sorbet/rbi/gems/rexml@3.2.5.rbi +672 -0
  75. data/sorbet/rbi/gems/rspec-core.rbi +1898 -0
  76. data/sorbet/rbi/gems/rspec-core@3.11.0.rbi +2468 -0
  77. data/sorbet/rbi/gems/rspec-expectations.rbi +1171 -0
  78. data/sorbet/rbi/gems/rspec-expectations@3.11.0.rbi +1634 -0
  79. data/sorbet/rbi/gems/rspec-mocks.rbi +1094 -0
  80. data/sorbet/rbi/gems/rspec-mocks@3.11.1.rbi +1497 -0
  81. data/sorbet/rbi/gems/rspec-support.rbi +280 -0
  82. data/sorbet/rbi/gems/rspec-support@3.11.0.rbi +511 -0
  83. data/sorbet/rbi/gems/rspec.rbi +15 -0
  84. data/sorbet/rbi/gems/rspec@3.11.0.rbi +40 -0
  85. data/sorbet/rbi/gems/rubocop-ast@1.17.0.rbi +8 -0
  86. data/sorbet/rbi/gems/rubocop-sorbet@0.6.7.rbi +8 -0
  87. data/sorbet/rbi/gems/rubocop@1.27.0.rbi +8 -0
  88. data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +8 -0
  89. data/sorbet/rbi/gems/ruby2_keywords@0.0.5.rbi +8 -0
  90. data/sorbet/rbi/gems/spoom@1.1.11.rbi +1445 -0
  91. data/sorbet/rbi/gems/tapioca@0.7.1.rbi +1677 -0
  92. data/sorbet/rbi/gems/thor@1.2.1.rbi +844 -0
  93. data/sorbet/rbi/gems/typhoeus.rbi +301 -0
  94. data/sorbet/rbi/gems/typhoeus@1.4.0.rbi +450 -0
  95. data/sorbet/rbi/gems/unicode-display_width@2.1.0.rbi +8 -0
  96. data/sorbet/rbi/gems/unparser@0.6.4.rbi +8 -0
  97. data/sorbet/rbi/gems/webrick@1.7.0.rbi +601 -0
  98. data/sorbet/rbi/gems/yard-sorbet@0.6.1.rbi +235 -0
  99. data/sorbet/rbi/gems/yard@0.9.27.rbi +3966 -0
  100. data/sorbet/rbi/hidden-definitions/errors.txt +4013 -0
  101. data/sorbet/rbi/hidden-definitions/hidden.rbi +8945 -0
  102. data/sorbet/rbi/sorbet-typed/lib/faraday/all/faraday.rbi +82 -0
  103. data/sorbet/rbi/sorbet-typed/lib/rake/all/rake.rbi +645 -0
  104. data/sorbet/rbi/sorbet-typed/lib/rspec-core/all/rspec-core.rbi +24 -0
  105. data/sorbet/rbi/todo.rbi +8 -0
  106. data/sorbet/tapioca/config.yml +13 -0
  107. data/sorbet/tapioca/require.rb +4 -0
  108. metadata +236 -0
@@ -0,0 +1,166 @@
1
+ # typed: true
2
+
3
+ module BungieSdk
4
+ # Class for authentication token-related exceptions
5
+ class TokenException < StandardError; end
6
+
7
+ # Struct used internally to represent Bungie API Responses.
8
+ class ApiResponse < T::Struct
9
+ prop :body, Hash
10
+ prop :headers, Hash
11
+ prop :status, Integer
12
+ end
13
+
14
+ # Base class for all BungieSdk resources. Handles creating and running
15
+ # Typhoeus requests, authentication and auth token management, and
16
+ # basic methods for building endpoints.
17
+ class ApiAgent
18
+ extend T::Sig
19
+ attr_accessor :data
20
+
21
+ BASE_URI = 'https://www.bungie.net'.freeze
22
+
23
+ sig { params(api_data: T.nilable(Hash)).void }
24
+ def initialize(api_data=nil)
25
+ @data = api_data
26
+ end
27
+
28
+ # Runs the given `Typhoeus::Request`, checks for expected errors, and
29
+ # handles refreshing or creating an authentication token for authenticated
30
+ # requests.
31
+ sig { params(request: Typhoeus::Request).returns(ApiResponse) }
32
+ def run(request)
33
+ retries = 0
34
+
35
+ begin
36
+ request.run if request.response.nil? || retries == 1
37
+
38
+ api_response = process_response(request.response)
39
+ @body = api_response.body
40
+ @headers = api_response.headers
41
+ @code = api_response.status
42
+ if @body['ErrorStatus'] == 'WebAuthRequired'
43
+ TokenManager.instance.web_auth
44
+ throw TokenException
45
+ elsif request.response.options[:return_code] == :couldnt_connect
46
+ TokenManager.instance.refresh_token
47
+ throw TokenException
48
+ end
49
+
50
+ api_response
51
+ rescue TokenException
52
+ if retries.zero?
53
+ retries += 1
54
+
55
+ options = request.original_options
56
+ options[:headers] = auth_headers
57
+ request = Typhoeus::Request.new(
58
+ request.base_url,
59
+ **options
60
+ )
61
+ retry
62
+ else
63
+ throw TokenException.new('Invalid OAuth token')
64
+ end
65
+ end
66
+ end
67
+
68
+ # Returns a Typhoeus GET request with the given configuration
69
+ sig do
70
+ params(path: String, params: T.nilable(Hash), body: T.nilable(Hash))
71
+ .returns(Typhoeus::Request)
72
+ end
73
+ def get(path, params: nil, body: nil)
74
+ request(:get, path, params: params, body: body)
75
+ end
76
+
77
+ # Returns a Typhoeus PUT request with the given configuration
78
+ sig do
79
+ params(path: String, params: T.nilable(Hash), body: T.nilable(Hash))
80
+ .returns(Typhoeus::Request)
81
+ end
82
+ def put(path, params: nil, body: nil)
83
+ request(:put, path, params: params, body: body)
84
+ end
85
+
86
+ # Returns a Typhoeus POST request with the given configuration
87
+ sig do
88
+ params(path: String, params: T.nilable(Hash), body: T.nilable(Hash))
89
+ .returns(Typhoeus::Request)
90
+ end
91
+ def post(path, params: nil, body: nil)
92
+ request(:post, path, params: params, body: body)
93
+ end
94
+
95
+ # Returns a Typhoeus DELETE request with the given configuration
96
+ sig do
97
+ params(path: String, params: T.nilable(Hash), body: T.nilable(Hash))
98
+ .returns(Typhoeus::Request)
99
+ end
100
+ def delete(path, params: nil, body: nil)
101
+ request(:delete, path, params: params, body: body)
102
+ end
103
+
104
+ # Returns a Typhoeus request with the given configuration
105
+ sig do
106
+ params(method: T.any(String, Symbol),
107
+ path: String,
108
+ params: T.nilable(Hash),
109
+ body: T.nilable(Hash))
110
+ .returns(Typhoeus::Request)
111
+ end
112
+ def request(method, path, params: nil, body: nil)
113
+ Typhoeus::Request.new(
114
+ "#{BASE_URI}#{path}",
115
+ followlocation: true,
116
+ method: method.to_sym,
117
+ params: params,
118
+ body: body,
119
+ headers: auth_headers
120
+ )
121
+ end
122
+
123
+ private
124
+ sig { params(response: Typhoeus::Response).returns(ApiResponse) }
125
+ def process_response(response)
126
+ response_headers = response.headers.to_h
127
+ response_code = response.code
128
+ response_body = JSON.parse(response.body)['Response']
129
+
130
+ ApiResponse.new(body: response_body, headers: response_headers, status: response_code)
131
+ end
132
+
133
+ sig { params(manifest_type: String, id: T.any(String, Integer)).returns(Typhoeus::Request) }
134
+ def entity_definition(manifest_type, id)
135
+ get(manifest_url(manifest_type, id))
136
+ end
137
+
138
+ sig { params(id: T.any(String, Integer)).returns(Typhoeus::Request) }
139
+ def item_definition(id)
140
+ entity_definition('DestinyInventoryItemDefinition', id)
141
+ end
142
+
143
+ sig { params(id: T.any(String, Integer)).returns(Typhoeus::Request) }
144
+ def vendor_definition(id)
145
+ entity_definition('DestinyVendorDefinition', id)
146
+ end
147
+
148
+ sig { returns(String) }
149
+ def destiny_url
150
+ '/Platform/Destiny2'
151
+ end
152
+
153
+ sig { params(manifest_type: String, id: T.any(String, Integer)).returns(String) }
154
+ def manifest_url(manifest_type, id)
155
+ "#{destiny_url}/Manifest/#{manifest_type}/#{id}"
156
+ end
157
+
158
+ sig { returns(Hash) }
159
+ def auth_headers
160
+ {
161
+ 'Authorization' => "Bearer #{TokenManager.instance.token.token}",
162
+ 'X-API-KEY' => TokenManager.instance.api_key
163
+ }
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,82 @@
1
+ # typed: true
2
+
3
+ module BungieSdk::Destiny2
4
+ # Class representing characters in Destiny 2
5
+ class Character < ApiAgent
6
+
7
+ # Character hash
8
+ sig { returns String }
9
+ def id
10
+ data['characterId']
11
+ end
12
+
13
+ # Destiny2 Membership Type
14
+ sig { returns Integer }
15
+ def membership_type
16
+ data['membershipType']
17
+ end
18
+
19
+ # Destiny2 Membership Id
20
+ sig { returns String }
21
+ def membership_id
22
+ data['membershipId']
23
+ end
24
+
25
+ # Returns `Vendor`s associated with this character.
26
+ # - `can_purchase`: filters for vendors with purchasable items.
27
+ # - `enabled`: filters for vendors that are enabled.
28
+ # - `components`: a list of components to be returned with this API request.
29
+ # The result of this request is memoized, so a new `Character` object will need to
30
+ # be created to retrieve new results.
31
+ sig do
32
+ params(can_purchase: T::Boolean,
33
+ enabled: T::Boolean,
34
+ components: T::Array[T.any(Integer, String)])
35
+ .returns(T::Array[Vendor])
36
+ end
37
+ def vendors(can_purchase: true,
38
+ enabled: true,
39
+ components: [DestinyComponentType.Vendors, DestinyComponentType.VendorSales])
40
+ return @vendors unless @vendors.nil?
41
+
42
+ vendor_data = run(get("#{character_url}/Vendors",
43
+ params: { components: components.join(',') })).body
44
+
45
+ vendor_ids = [vendor_data['vendors']['data'].keys, vendor_data['sales']['data'].keys]
46
+ .flatten
47
+ .uniq
48
+
49
+ vendor_hashes = vendor_ids.map do |id|
50
+ {
51
+ 'vendorData' => vendor_data['vendors']['data'][id],
52
+ 'sales' => vendor_data['sales']['data'][id]
53
+ }
54
+ end.select do |vendor|
55
+ if vendor['vendorData'].nil?
56
+ false
57
+ else
58
+ vendor['vendorData']['canPurchase'] == can_purchase &&
59
+ vendor['vendorData']['enabled'] == enabled
60
+ end
61
+ end
62
+
63
+ hydra = Typhoeus::Hydra.new
64
+ response = vendor_hashes.map do |hash|
65
+ vendor = Vendor.new(hash)
66
+ hydra.queue vendor.definition_request
67
+
68
+ vendor
69
+ end
70
+
71
+ hydra.run
72
+
73
+ @vendors = response
74
+ end
75
+
76
+ private
77
+ sig { returns String }
78
+ def character_url
79
+ "#{destiny_url}/#{membership_type}/Profile/#{membership_id}/Character/#{id}"
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,72 @@
1
+ # typed: true
2
+
3
+ module BungieSdk
4
+ # Base class for the BungieSdk. All workflows should start with a `Client`.
5
+ #
6
+ # This class currently expects the user to authenticate with OAuth in order to
7
+ # access private API endpoints. This will be changed in the future to allow users
8
+ # to not provide an authentication token if they only wish to access public Bungie
9
+ # API endpoints. Bungie Applications can be created [here](https://www.bungie.net/en/Application).
10
+ #
11
+ # The authentication workflow for this SDK allows for a couple of different options.
12
+ # First, if you already have a valid `OAuth2::AccessToken` for your Bungie Application,
13
+ # you can provide that to the `token` parameter on creation of this class and this
14
+ # application will handle refreshing the token when need be. If you do not already
15
+ # have an access token, you can provide your Bungie Application's client id and client secret
16
+ # to their respective arguments in this class's constructor and this application will
17
+ # handle authentication with those credentials. Optionally, if you would like to save your access
18
+ # token to your local filesystem, provide a path for that file in the `token_filepath` parameter.
19
+ # If a filepath is provided in `token_filepath` but no access token is supplied through `token`, then
20
+ # this application will attempt to read that token from file and use it for authentication. This
21
+ # is recommended as it will reduce the number of times you need to authenticate this app in your
22
+ # web browser. If `token_filepath` is supplied, `token` is not, and there is no existing token
23
+ # stored in the file at `token_filepath`, then this application will require the user to
24
+ # authenticate through their web browser and then save that information to file.
25
+ class Client < ApiAgent
26
+ # Client Constructor.
27
+ # - `token`: Optional; `OAuth2::Access` token for your Bungie Application
28
+ # - `token_filepath`: Optional; Path to read/write your Bungie Application's access token.
29
+ # - `api_key`: Bungie API key for your Bungie Application.
30
+ # - `client_id`: Bungie client id for your Bungie Application.
31
+ # - `client_secret`: Bungie client secret for your Bungie Application.
32
+ # - `redirect_uri`: OAuth2 redirect uri for your Bungie Application. Must match your Bungie
33
+ # Appliation's redirect uri configuration.
34
+ sig do
35
+ params(token: T.nilable(T.any(OAuth2::AccessToken, String)),
36
+ token_filepath: T.nilable(String),
37
+ api_key: T.nilable(String),
38
+ client_id: T.nilable(String),
39
+ client_secret: T.nilable(String),
40
+ redirect_uri: String)
41
+ .void
42
+ end
43
+ def initialize(token: nil,
44
+ token_filepath: nil,
45
+ api_key: ENV['BUNGIE_API_KEY'],
46
+ client_id: ENV['BUNGIE_CLIENT_ID'],
47
+ client_secret: ENV['BUNGIE_CLIENT_SECRET'],
48
+ redirect_uri: ENV['BUNGIE_REDIRECT_URI'] || 'http://localhost:8080/oauth/callback')
49
+ super(nil)
50
+ unless TokenManager.instance.initialized?
51
+ TokenManager.instance.setup_token(token,
52
+ token_filepath,
53
+ api_key,
54
+ client_id,
55
+ client_secret,
56
+ redirect_uri)
57
+ end
58
+ end
59
+
60
+ # Returns memberships associated with the current user's Bungie account.
61
+ sig { returns Hash }
62
+ def memberships
63
+ @memberships ||= run(get('/Platform/User/GetMembershipsForCurrentUser')).body
64
+ end
65
+
66
+ # Returns Destiny Memberships associated with the current user's Bungie account.
67
+ sig { returns T::Array[Destiny2::Membership] }
68
+ def destiny_memberships
69
+ memberships['destinyMemberships'].map {|data| Destiny2::Membership.new(data) }
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,83 @@
1
+ # typed: true
2
+
3
+ module BungieSdk::Destiny2
4
+ # Represents Destiny 2 items
5
+ class Item < ApiAgent
6
+ # Returns `Typhoeus::Request` for this item's definition.
7
+ sig { returns Typhoeus::Request }
8
+ def definition_request
9
+ request = item_definition(id)
10
+ request.on_success do |response|
11
+ response = process_response(response)
12
+ data['definition'] = response.body
13
+ end
14
+
15
+ request
16
+ end
17
+
18
+ # This item's definition.
19
+ sig { returns Hash }
20
+ def definition
21
+ if data['definition'].nil?
22
+ definition_request.run
23
+ end
24
+
25
+ data['definition']
26
+ end
27
+
28
+ # Item costs.
29
+ sig { returns T::Array[Hash] }
30
+ def costs
31
+ data['costs']
32
+ end
33
+
34
+ # Item hash
35
+ sig { returns Integer }
36
+ def id
37
+ data['itemHash']
38
+ end
39
+
40
+ # Item name
41
+ sig { returns String }
42
+ def name
43
+ definition['displayProperties']['name']
44
+ end
45
+
46
+ # Item type
47
+ sig { returns String }
48
+ def type
49
+ definition['itemTypeDisplayName']
50
+ end
51
+
52
+ # Item type and tier
53
+ sig { returns String }
54
+ def type_and_tier
55
+ definition['itemTypeAndTierDisplayName']
56
+ end
57
+
58
+ # Tests if this item is an instance item.
59
+ sig { returns T::Boolean }
60
+ def instance_item?
61
+ definition['inventory']['isInstanceItem']
62
+ end
63
+
64
+ # This item's sockets.
65
+ sig { returns T::Array[Hash] }
66
+ def sockets
67
+ item_sockets = definition['sockets']
68
+ item_sockets.nil? ? [] : item_sockets['socketEntries']
69
+ end
70
+
71
+ # A list of the ids for this item's sockets.
72
+ sig { returns T::Array[String] }
73
+ def socket_ids
74
+ return [] if sockets.empty?
75
+
76
+ ids = sockets.map do |socket|
77
+ [socket['singleInitialItemHash'], socket['reusablePlugItems'].map {|s| s['plugItemHash'] }]
78
+ end
79
+
80
+ ids.flatten.uniq.map(&:to_s)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,30 @@
1
+ # typed: true
2
+ module BungieSdk::Destiny2
3
+ # Represents a Destiny 2 Membership
4
+ class Membership < ApiAgent
5
+ # Membership type
6
+ sig { returns Integer }
7
+ def type
8
+ data['membershipType']
9
+ end
10
+
11
+ # Membership id
12
+ sig { returns String }
13
+ def id
14
+ data['membershipId']
15
+ end
16
+
17
+ # Profile associated with this membership
18
+ # - `components`: DestinyComponentType to be supplied to this API endpoint.
19
+ sig { params(components: T::Array[T.any(Integer, String)]).returns(Profile) }
20
+ def profile(components: [DestinyComponentType.Profiles])
21
+ Profile.new(run(get(profile_url, params: { components: components.join(',') })).body)
22
+ end
23
+
24
+ private
25
+ sig { returns String }
26
+ def profile_url
27
+ "#{destiny_url}/#{type}/Profile/#{id}"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,36 @@
1
+ # typed: true
2
+ module BungieSdk::Destiny2
3
+ # Represents a Destiny2 profile
4
+ class Profile < ApiAgent
5
+ # Returns the characters associated with this profile.
6
+ # - `components`: DestinyComponentType to be supplied to this API endpoint.
7
+ sig do
8
+ params(components: T::Array[T.any(Integer, String)])
9
+ .returns(T::Array[Character])
10
+ end
11
+ def characters(components: [DestinyComponentType.Characters])
12
+ characters = run(get(profile_url, params: { components: components.join(',') })).body
13
+ characters['characters']['data'].map do |_, character|
14
+ Character.new(character)
15
+ end
16
+ end
17
+
18
+ # Profile's membership id
19
+ sig { returns String }
20
+ def membership_id
21
+ data['profile']['data']['userInfo']['membershipId']
22
+ end
23
+
24
+ # Profile's membership type
25
+ sig { returns Integer }
26
+ def membership_type
27
+ data['profile']['data']['userInfo']['membershipType']
28
+ end
29
+
30
+ private
31
+ sig { returns String }
32
+ def profile_url
33
+ "#{destiny_url}/#{membership_type}/Profile/#{membership_id}"
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,136 @@
1
+ # typed: true
2
+ require 'singleton'
3
+
4
+ # OAuth2 token manager singleton class for the BungieSdk
5
+ class BungieSdk::TokenManager
6
+ extend T::Sig
7
+ include Singleton
8
+
9
+ attr_reader :token, :api_key
10
+
11
+ AUTH_URL = '/en/Oauth/Authorize'.freeze
12
+ TOKEN_URL = '/Platform/App/Oauth/Token'.freeze
13
+ BASE_URI = 'https://www.bungie.net'.freeze
14
+
15
+ # Setup method for the token manager. Must be called before any
16
+ # API requests are made.
17
+ sig do
18
+ params(user_token: T.nilable(T.any(OAuth2::AccessToken, String)),
19
+ token_path: T.nilable(String),
20
+ api_key: T.nilable(String),
21
+ client_id: T.nilable(String),
22
+ client_secret: T.nilable(String),
23
+ redirect_uri: String)
24
+ .void
25
+ end
26
+ def setup_token(user_token,
27
+ token_path,
28
+ api_key,
29
+ client_id,
30
+ client_secret,
31
+ redirect_uri)
32
+ @token_path = token_path
33
+ @api_key = api_key
34
+ @client_id = client_id
35
+ @client_secret = client_secret
36
+ @redirect_uri = redirect_uri
37
+
38
+ if !@token_path.nil?
39
+ load_token(@token_path)
40
+ elsif !user_token.nil?
41
+ @token = user_token
42
+ else
43
+ web_auth
44
+ end
45
+ end
46
+
47
+ # Tests if this manager has been initialized with an OAuth2 token
48
+ sig { returns T::Boolean }
49
+ def initialized?
50
+ !@token.nil?
51
+ end
52
+
53
+ # Loads the OAuth2 access token from file. If that file does not exist,
54
+ # the token is generated through browser based authentication.
55
+ # - `filepath`: location of token file
56
+ sig { params(filepath: String).returns(OAuth2::AccessToken) }
57
+ def load_token(filepath)
58
+ if @token.nil?
59
+ @token = if File.exist?(filepath)
60
+ load_token_data
61
+ else
62
+ web_auth
63
+ end
64
+ end
65
+
66
+ if @token.expired?
67
+ @token = @token.refresh!
68
+ save_token_data(@token)
69
+ end
70
+
71
+ @token
72
+ end
73
+
74
+ # Creates an authentication token using the user's web browser.
75
+ sig { returns OAuth2::AccessToken }
76
+ def web_auth
77
+ client = oauth_client
78
+ auth_url = client.auth_code.authorize_url(redirect_uri: @redirect_uri)
79
+ puts auth_url
80
+ Launchy.open(auth_url) rescue nil
81
+ puts 'Please go to this url, accept the authorization request, '\
82
+ 'and copy the code parameter from the url into this program:'
83
+ code = gets.chomp
84
+ auth_token = client.auth_code.get_token(code)
85
+ save_token_data(auth_token)
86
+
87
+ @token = auth_token
88
+ end
89
+
90
+ # Refreshes the manager's token.
91
+ sig { returns OAuth2::AccessToken }
92
+ def refresh_token
93
+ @token = @token.refresh!
94
+ end
95
+
96
+ # Loads token data from file
97
+ sig { returns OAuth2::AccessToken }
98
+ def load_token_data
99
+ JSON.parse(File.read(@token_path)).yield_self do |token_data|
100
+ OAuth2::AccessToken.new(
101
+ oauth_client,
102
+ token_data['token'],
103
+ refresh_token: token_data['refresh_token'],
104
+ expires_at: token_data['expires_at']
105
+ )
106
+ end
107
+ end
108
+
109
+ # Returns a configured OAuth2 client
110
+ sig { returns OAuth2::Client }
111
+ def oauth_client
112
+ OAuth2::Client.new(
113
+ @client_id,
114
+ @client_secret,
115
+ site: BASE_URI,
116
+ authorize_url: AUTH_URL,
117
+ token_url: TOKEN_URL
118
+ )
119
+ end
120
+
121
+ # Writes the given auth token to disk
122
+ # - `auth_token`: token to be saved.
123
+ sig { params(auth_token: OAuth2::AccessToken).void }
124
+ def save_token_data(auth_token)
125
+ if @token_path
126
+ File.write(
127
+ @token_path,
128
+ JSON.generate({
129
+ 'token' => auth_token.token,
130
+ 'refresh_token' => auth_token.refresh_token,
131
+ 'expires_at' => auth_token.expires_at
132
+ })
133
+ )
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,64 @@
1
+ # typed: true
2
+
3
+ module BungieSdk::Destiny2
4
+ # Represents vendors in Destiny 2
5
+ class Vendor < ApiAgent
6
+ # Vendor id
7
+ sig { returns Integer }
8
+ def id
9
+ data['vendorData']['vendorHash']
10
+ end
11
+
12
+ # Vendor name
13
+ sig { returns String }
14
+ def name
15
+ definition['displayProperties']['name'] rescue ''
16
+ end
17
+
18
+ # Vendor sales
19
+ sig { returns T::Array[Hash] }
20
+ def sales
21
+ data['sales']['saleItems'].values
22
+ end
23
+
24
+ # Request for vendor's definition
25
+ sig { returns Typhoeus::Request }
26
+ def definition_request
27
+ request = vendor_definition(data['vendorData']['vendorHash'])
28
+ request.on_success do |response|
29
+ response = process_response(response)
30
+ data['definition'] = response.body
31
+ end
32
+
33
+ request
34
+ end
35
+
36
+ # Vendor definition
37
+ sig { returns Hash }
38
+ def definition
39
+ if data['definition'].nil?
40
+ definition_request.run
41
+ end
42
+
43
+ data['definition']
44
+ end
45
+
46
+ # Vendor items
47
+ sig { returns T::Array[Item] }
48
+ def items
49
+ return @items unless @items.nil?
50
+
51
+ hydra = Typhoeus::Hydra.new
52
+ vendor_items = sales.map do |sale|
53
+ item = Item.new(sale)
54
+ hydra.queue item.definition_request
55
+
56
+ item
57
+ end
58
+
59
+ hydra.run
60
+
61
+ @items = vendor_items.reject {|item| item.data['definition'].nil? }
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,4 @@
1
+ # typed: strict
2
+ module BungieSdk
3
+ VERSION = '0.1.1'
4
+ end