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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +147 -0
- data/.solargraph.yml +23 -0
- data/.travis.yml +6 -0
- data/.vim/coc-settings.json +12 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +133 -0
- data/LICENSE.txt +21 -0
- data/README.md +56 -0
- data/Rakefile +6 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/bin/tapioca +29 -0
- data/bungie_sdk.gemspec +35 -0
- data/lib/bungie_sdk/agent.rb +166 -0
- data/lib/bungie_sdk/character.rb +82 -0
- data/lib/bungie_sdk/client.rb +72 -0
- data/lib/bungie_sdk/item.rb +83 -0
- data/lib/bungie_sdk/membership.rb +30 -0
- data/lib/bungie_sdk/profile.rb +36 -0
- data/lib/bungie_sdk/token_manager.rb +136 -0
- data/lib/bungie_sdk/vendor.rb +64 -0
- data/lib/bungie_sdk/version.rb +4 -0
- data/lib/bungie_sdk.rb +104 -0
- data/sorbet/config +3 -0
- data/sorbet/rbi/gems/addressable.rbi +151 -0
- data/sorbet/rbi/gems/addressable@2.8.0.rbi +224 -0
- data/sorbet/rbi/gems/ast@2.4.2.rbi +54 -0
- data/sorbet/rbi/gems/bungie_sdk.rbi +15 -0
- data/sorbet/rbi/gems/coderay.rbi +285 -0
- data/sorbet/rbi/gems/coderay@1.1.3.rbi +1005 -0
- data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +185 -0
- data/sorbet/rbi/gems/dotenv.rbi +68 -0
- data/sorbet/rbi/gems/dotenv@2.7.6.rbi +88 -0
- data/sorbet/rbi/gems/ethon.rbi +716 -0
- data/sorbet/rbi/gems/ethon@0.15.0.rbi +883 -0
- data/sorbet/rbi/gems/faraday-net_http.rbi +33 -0
- data/sorbet/rbi/gems/faraday-net_http@2.0.1.rbi +78 -0
- data/sorbet/rbi/gems/faraday.rbi +696 -0
- data/sorbet/rbi/gems/faraday@2.2.0.rbi +685 -0
- data/sorbet/rbi/gems/ffi.rbi +560 -0
- data/sorbet/rbi/gems/ffi@1.15.5.rbi +849 -0
- data/sorbet/rbi/gems/gem-release.rbi +582 -0
- data/sorbet/rbi/gems/gem-release@2.2.2.rbi +644 -0
- data/sorbet/rbi/gems/hashie.rbi +160 -0
- data/sorbet/rbi/gems/jwt.rbi +274 -0
- data/sorbet/rbi/gems/jwt@2.3.0.rbi +437 -0
- data/sorbet/rbi/gems/launchy.rbi +226 -0
- data/sorbet/rbi/gems/launchy@2.5.0.rbi +327 -0
- data/sorbet/rbi/gems/method_source.rbi +64 -0
- data/sorbet/rbi/gems/method_source@1.0.0.rbi +72 -0
- data/sorbet/rbi/gems/multi_json.rbi +62 -0
- data/sorbet/rbi/gems/multi_json@1.15.0.rbi +96 -0
- data/sorbet/rbi/gems/multi_xml.rbi +35 -0
- data/sorbet/rbi/gems/multi_xml@0.6.0.rbi +36 -0
- data/sorbet/rbi/gems/oauth2.rbi +143 -0
- data/sorbet/rbi/gems/oauth2@1.4.9.rbi +181 -0
- data/sorbet/rbi/gems/parallel@1.22.1.rbi +8 -0
- data/sorbet/rbi/gems/parser@3.1.1.0.rbi +1196 -0
- data/sorbet/rbi/gems/pry.rbi +1898 -0
- data/sorbet/rbi/gems/pry@0.14.1.rbi +2486 -0
- data/sorbet/rbi/gems/public_suffix.rbi +104 -0
- data/sorbet/rbi/gems/public_suffix@4.0.6.rbi +145 -0
- data/sorbet/rbi/gems/rack.rbi +21 -0
- data/sorbet/rbi/gems/rack@2.2.3.rbi +1622 -0
- data/sorbet/rbi/gems/rainbow@3.1.1.rbi +8 -0
- data/sorbet/rbi/gems/rake.rbi +644 -0
- data/sorbet/rbi/gems/rake@12.3.3.rbi +804 -0
- data/sorbet/rbi/gems/rbi@0.0.14.rbi +2073 -0
- data/sorbet/rbi/gems/regexp_parser@2.3.0.rbi +8 -0
- data/sorbet/rbi/gems/rexml@3.2.5.rbi +672 -0
- data/sorbet/rbi/gems/rspec-core.rbi +1898 -0
- data/sorbet/rbi/gems/rspec-core@3.11.0.rbi +2468 -0
- data/sorbet/rbi/gems/rspec-expectations.rbi +1171 -0
- data/sorbet/rbi/gems/rspec-expectations@3.11.0.rbi +1634 -0
- data/sorbet/rbi/gems/rspec-mocks.rbi +1094 -0
- data/sorbet/rbi/gems/rspec-mocks@3.11.1.rbi +1497 -0
- data/sorbet/rbi/gems/rspec-support.rbi +280 -0
- data/sorbet/rbi/gems/rspec-support@3.11.0.rbi +511 -0
- data/sorbet/rbi/gems/rspec.rbi +15 -0
- data/sorbet/rbi/gems/rspec@3.11.0.rbi +40 -0
- data/sorbet/rbi/gems/rubocop-ast@1.17.0.rbi +8 -0
- data/sorbet/rbi/gems/rubocop-sorbet@0.6.7.rbi +8 -0
- data/sorbet/rbi/gems/rubocop@1.27.0.rbi +8 -0
- data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +8 -0
- data/sorbet/rbi/gems/ruby2_keywords@0.0.5.rbi +8 -0
- data/sorbet/rbi/gems/spoom@1.1.11.rbi +1445 -0
- data/sorbet/rbi/gems/tapioca@0.7.1.rbi +1677 -0
- data/sorbet/rbi/gems/thor@1.2.1.rbi +844 -0
- data/sorbet/rbi/gems/typhoeus.rbi +301 -0
- data/sorbet/rbi/gems/typhoeus@1.4.0.rbi +450 -0
- data/sorbet/rbi/gems/unicode-display_width@2.1.0.rbi +8 -0
- data/sorbet/rbi/gems/unparser@0.6.4.rbi +8 -0
- data/sorbet/rbi/gems/webrick@1.7.0.rbi +601 -0
- data/sorbet/rbi/gems/yard-sorbet@0.6.1.rbi +235 -0
- data/sorbet/rbi/gems/yard@0.9.27.rbi +3966 -0
- data/sorbet/rbi/hidden-definitions/errors.txt +4013 -0
- data/sorbet/rbi/hidden-definitions/hidden.rbi +8945 -0
- data/sorbet/rbi/sorbet-typed/lib/faraday/all/faraday.rbi +82 -0
- data/sorbet/rbi/sorbet-typed/lib/rake/all/rake.rbi +645 -0
- data/sorbet/rbi/sorbet-typed/lib/rspec-core/all/rspec-core.rbi +24 -0
- data/sorbet/rbi/todo.rbi +8 -0
- data/sorbet/tapioca/config.yml +13 -0
- data/sorbet/tapioca/require.rb +4 -0
- 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
|