bungie_sdk 0.1.1

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 (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