bungie_sdk 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|