restiny 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/restiny/errors.rb +19 -0
- data/lib/restiny/manifest.rb +11 -7
- data/lib/restiny/version.rb +1 -1
- data/lib/restiny.rb +209 -1
- metadata +17 -3
- data/lib/restiny/client.rb +0 -134
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 525f30b84be418f2709f43396452fe3fb5f1b7fc307f5096a6698da8a1a61d3b
|
4
|
+
data.tar.gz: 3e008a19f1766d4d0a89a840e7439acae17d714bd5ec22cfd637af8aa9790f46
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4a741c23f022a5a9faa659aa26a8de4b39233caceaf1b11713209b605012d0cf721b2c2f06c4bbb865fe5f28f166d0b88a1446a06052aad88b35810ebc293a33
|
7
|
+
data.tar.gz: 530fd77d6b734bf749e9ce55b950dae7da4acc8f4719c95d57bf8f3dbb6403f0b804b7960b09cc434814b6f77e1c649ca651b86e649dd3b69081cea56d3e0359
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Restiny
|
4
|
+
class Error < StandardError; end
|
5
|
+
class RequestError < Error; end
|
6
|
+
class InvalidParamsError < RequestError; end
|
7
|
+
class RateLimitedError < RequestError
|
8
|
+
end
|
9
|
+
|
10
|
+
class ResponseError < Error; end
|
11
|
+
class NetworkError < Error
|
12
|
+
attr_accessor :status_code
|
13
|
+
|
14
|
+
def initialize(message, status_code = nil)
|
15
|
+
@status_code = status_code
|
16
|
+
super(message)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/restiny/manifest.rb
CHANGED
@@ -23,7 +23,7 @@ module Restiny
|
|
23
23
|
'DestinyDamageTypeDefinition': { item: 'damage_type', items: 'damage_types' },
|
24
24
|
'DestinyDestinationDefinition': { item: 'destination', items: 'destinations' },
|
25
25
|
'DestinyEnergyTypeDefinition': { item: 'energy_type', items: 'energy_types' },
|
26
|
-
'DestinyEquipmentSlotDefinition': { item: '
|
26
|
+
'DestinyEquipmentSlotDefinition': { item: 'equipment_slot', items: 'equipment_slots' },
|
27
27
|
'DestinyEventCardDefinition': { item: 'event_card', items: 'event_cards' },
|
28
28
|
'DestinyFactionDefinition': { item: 'faction', items: 'factions' },
|
29
29
|
'DestinyGenderDefinition': { item: 'gender', items: 'genders' },
|
@@ -85,14 +85,16 @@ module Restiny
|
|
85
85
|
Zip::File.open(zipped_file) { |file| file.first.extract(manifest_path) }
|
86
86
|
|
87
87
|
self.new(manifest_path)
|
88
|
-
rescue Down::
|
89
|
-
raise "Unable to download the manifest
|
90
|
-
rescue Zip::Error
|
91
|
-
raise "Unable to unzip the manifest file"
|
88
|
+
rescue Down::ResponseError => error
|
89
|
+
raise Restiny::NetworkError.new("Unable to download the manifest file", error.response.code)
|
90
|
+
rescue Zip::Error => error
|
91
|
+
raise Restiny::Error.new("Unable to unzip the manifest file (#{e})")
|
92
92
|
end
|
93
93
|
|
94
94
|
def initialize(file_path)
|
95
|
-
|
95
|
+
if file_path.empty? || !File.exist?(file_path) || !File.file?(file_path)
|
96
|
+
raise Restiny::InvalidParamsError.new("You must provide a valid path for the manifest file")
|
97
|
+
end
|
96
98
|
|
97
99
|
@database = SQLite3::Database.new(file_path, results_as_hash: true)
|
98
100
|
@file_path = file_path
|
@@ -106,6 +108,8 @@ module Restiny
|
|
106
108
|
key = key.gsub(/(\B[A-Z])/, '_\1') if key =~ /\B[A-Z]/
|
107
109
|
output[key.downcase] = if value.is_a?(Hash)
|
108
110
|
clean_row_keys(value)
|
111
|
+
elsif value.is_a?(Array)
|
112
|
+
value.map { |item| item.is_a?(Hash) ? clean_row_keys(item) : value }
|
109
113
|
else
|
110
114
|
value
|
111
115
|
end
|
@@ -133,7 +137,7 @@ module Restiny
|
|
133
137
|
Manifest::clean_row_keys(JSON.parse(row['json'])) unless row['json'].nil?
|
134
138
|
end
|
135
139
|
rescue SQLite3::Exception => e
|
136
|
-
raise "Error while querying the manifest (#{e})"
|
140
|
+
raise Restiny::Error.new("Error while querying the manifest (#{e})")
|
137
141
|
end
|
138
142
|
end
|
139
143
|
end
|
data/lib/restiny/version.rb
CHANGED
data/lib/restiny.rb
CHANGED
@@ -4,8 +4,216 @@ $LOAD_PATH.unshift(__dir__)
|
|
4
4
|
|
5
5
|
require 'restiny/version'
|
6
6
|
require 'restiny/constants'
|
7
|
-
require 'restiny/
|
7
|
+
require 'restiny/errors'
|
8
8
|
require 'restiny/manifest'
|
9
9
|
require 'restiny/user'
|
10
10
|
require 'restiny/membership'
|
11
11
|
require 'restiny/character'
|
12
|
+
|
13
|
+
require 'faraday'
|
14
|
+
require 'faraday/follow_redirects'
|
15
|
+
require 'securerandom'
|
16
|
+
|
17
|
+
module Restiny
|
18
|
+
extend self
|
19
|
+
|
20
|
+
BUNGIE_URL = "https://www.bungie.net"
|
21
|
+
API_BASE_URL = BUNGIE_URL + "/platform"
|
22
|
+
|
23
|
+
attr_accessor :api_key, :oauth_state, :oauth_client_id, :access_token, :refresh_token, :manifest
|
24
|
+
|
25
|
+
# OAuth methods
|
26
|
+
|
27
|
+
def authorise_url(redirect_url = nil, state = nil)
|
28
|
+
check_oauth_client_id
|
29
|
+
|
30
|
+
@oauth_state = state || SecureRandom.hex(15)
|
31
|
+
|
32
|
+
params = {
|
33
|
+
response_type: 'code',
|
34
|
+
client_id: @oauth_client_id,
|
35
|
+
state: @oauth_state
|
36
|
+
}
|
37
|
+
|
38
|
+
params[:redirect_url] = redirect_url unless redirect_url.nil?
|
39
|
+
|
40
|
+
connection.build_url(BUNGIE_URL + "/en/oauth/authorize", params).to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
def request_access_token(code, redirect_url = nil)
|
44
|
+
check_oauth_client_id
|
45
|
+
|
46
|
+
params = {
|
47
|
+
code: code,
|
48
|
+
grant_type: 'authorization_code',
|
49
|
+
client_id: @oauth_client_id
|
50
|
+
}
|
51
|
+
|
52
|
+
params[:redirect_url] = redirect_url unless redirect_url.nil?
|
53
|
+
|
54
|
+
post('/platform/app/oauth/token/', params, "Content-Type" => "application/x-www-form-urlencoded")
|
55
|
+
end
|
56
|
+
|
57
|
+
def request_refresh_token
|
58
|
+
end
|
59
|
+
|
60
|
+
# Manifest methods
|
61
|
+
|
62
|
+
def download_manifest(locale = 'en')
|
63
|
+
response = get("/platform/Destiny2/Manifest/")
|
64
|
+
|
65
|
+
manifest_path = response.dig('Response', 'mobileWorldContentPaths', locale)
|
66
|
+
raise Restiny::ResponseError.new("Unable to determine manifest URL") if manifest_path.nil?
|
67
|
+
|
68
|
+
Manifest.download(BUNGIE_URL + manifest_path)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Profile methods
|
72
|
+
|
73
|
+
def get_profile(membership_id, membership_type, components = [])
|
74
|
+
raise Restiny::InvalidParamsError.new("You must provide at least one component") if components.empty?
|
75
|
+
|
76
|
+
component_query = components.join(",")
|
77
|
+
response = get("/platform/Destiny2/#{membership_type}/Profile/#{membership_id}?components=#{component_query}")
|
78
|
+
|
79
|
+
{}.tap do |output|
|
80
|
+
components.each do |component|
|
81
|
+
case component.downcase
|
82
|
+
when 'characters'
|
83
|
+
output[:characters] = parse_profile_characters_response(response)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Account methods
|
90
|
+
|
91
|
+
def get_user_by_membership_id(membership_id, membership_type = PLATFORM_ALL)
|
92
|
+
raise Restiny::InvalidParamsError.new("You must provide a valid membership ID") if membership_id.nil?
|
93
|
+
|
94
|
+
response = get("/platform/User/GetMembershipsById/#{membership_id}/#{membership_type}/")
|
95
|
+
results = response.dig('Response')
|
96
|
+
|
97
|
+
return nil if results.nil?
|
98
|
+
|
99
|
+
Restiny::User.new(
|
100
|
+
display_name: results['bungieNetUser']['cachedBungieGlobalDisplayName'],
|
101
|
+
display_name_code: results['bungieNetUser']['cachedBungieGlobalDisplayNameCode'],
|
102
|
+
memberships: results['destinyMemberships']
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
def get_user_by_bungie_name(full_display_name, membership_type = PLATFORM_ALL)
|
107
|
+
display_name, display_name_code = full_display_name.split('#')
|
108
|
+
raise Restiny::InvalidParamsError.new("You must provide a valid Bungie name") if display_name.nil? || display_name_code.nil?
|
109
|
+
|
110
|
+
params = {
|
111
|
+
displayName: display_name,
|
112
|
+
displayNameCode: display_name_code
|
113
|
+
}
|
114
|
+
|
115
|
+
response = post("/platform/Destiny2/SearchDestinyPlayerByBungieName/#{membership_type}/", params)
|
116
|
+
result = response.dig('Response')
|
117
|
+
|
118
|
+
return nil if result.nil?
|
119
|
+
|
120
|
+
Restiny::User.new(
|
121
|
+
display_name: result[0]['bungieGlobalDisplayName'],
|
122
|
+
display_name_code: result[0]['bungieGlobalDisplayNameCode'],
|
123
|
+
memberships: result
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
def search_users(name, page = 0)
|
128
|
+
response = post("/platform/User/Search/GlobalName/#{page}", displayNamePrefix: name)
|
129
|
+
search_results = response.dig('Response', 'searchResults')
|
130
|
+
return [] if search_results.nil?
|
131
|
+
|
132
|
+
search_results.map do |user|
|
133
|
+
Restiny::User.new(
|
134
|
+
display_name: user['bungieGlobalDisplayName'],
|
135
|
+
display_name_code: user['bungieGlobalDisplayNameCode'],
|
136
|
+
memberships: user['destinyMemberships']
|
137
|
+
)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
def get(endpoint_url, params = {}, headers = {})
|
144
|
+
make_api_request(:get, endpoint_url, params, headers)
|
145
|
+
end
|
146
|
+
|
147
|
+
def post(endpoint_url, body, headers = {})
|
148
|
+
make_api_request(:post, endpoint_url, body, headers)
|
149
|
+
end
|
150
|
+
|
151
|
+
def make_api_request(type, url, params, headers = {})
|
152
|
+
raise Restiny::InvalidParamsError.new("You need to set an API key (Restiny.api_key = XXX)") unless @api_key
|
153
|
+
|
154
|
+
headers[:authorization] = "Bearer #{@oauth_token}" if @oauth_token
|
155
|
+
|
156
|
+
response = case type
|
157
|
+
when :get
|
158
|
+
connection.get(url, params, headers)
|
159
|
+
when :post
|
160
|
+
connection.post(url, params, headers)
|
161
|
+
end
|
162
|
+
|
163
|
+
response.body
|
164
|
+
rescue Faraday::Error => error
|
165
|
+
message = if error.response_body && error.response_headers['content-type'] =~ /application\/json;/i
|
166
|
+
JSON.parse(error.response_body)['Message']
|
167
|
+
else
|
168
|
+
error.message
|
169
|
+
end
|
170
|
+
|
171
|
+
case error
|
172
|
+
when Faraday::ClientError, Faraday::ServerError, Faraday::ConnectionFailed
|
173
|
+
raise Restiny::NetworkError.new(message, error.response_status)
|
174
|
+
else
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def parse_profile_characters_response(response)
|
179
|
+
characters = response.dig('Response', 'characters', 'data')
|
180
|
+
return [] if characters.nil?
|
181
|
+
|
182
|
+
result = []
|
183
|
+
|
184
|
+
[].tap do |result|
|
185
|
+
characters.each_value do |character|
|
186
|
+
result << Restiny::Character.new(
|
187
|
+
id: character['characterId'],
|
188
|
+
session_playtime: character['minutesPlayedThisSession'],
|
189
|
+
total_playtime: character['minutesPlayedTotal'],
|
190
|
+
light_level: character['light'],
|
191
|
+
stats: [],
|
192
|
+
emblem: [],
|
193
|
+
progression: character['progression']
|
194
|
+
)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def check_oauth_client_id
|
200
|
+
raise Restiny::RequestError.new("You need to set an OAuth client ID (Restiny.oauth_client_id = XXX)") unless @oauth_client_id
|
201
|
+
end
|
202
|
+
|
203
|
+
def default_headers
|
204
|
+
{
|
205
|
+
'User-Agent': "restiny v#{Restiny::VERSION}",
|
206
|
+
'X-API-KEY': @api_key
|
207
|
+
}
|
208
|
+
end
|
209
|
+
|
210
|
+
def connection
|
211
|
+
@connection ||= Faraday.new(url: API_BASE_URL, headers: default_headers) do |faraday|
|
212
|
+
faraday.request :json
|
213
|
+
faraday.request :url_encoded
|
214
|
+
faraday.response :json
|
215
|
+
faraday.response :follow_redirects
|
216
|
+
faraday.response :raise_error
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: restiny
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Bogan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-05-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -108,6 +108,20 @@ dependencies:
|
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '3.10'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: vcr
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
111
125
|
description: A gem for interacting with Bungie's Destiny API.
|
112
126
|
email:
|
113
127
|
- d+restiny@waferbaby.com
|
@@ -117,8 +131,8 @@ extra_rdoc_files: []
|
|
117
131
|
files:
|
118
132
|
- lib/restiny.rb
|
119
133
|
- lib/restiny/character.rb
|
120
|
-
- lib/restiny/client.rb
|
121
134
|
- lib/restiny/constants.rb
|
135
|
+
- lib/restiny/errors.rb
|
122
136
|
- lib/restiny/manifest.rb
|
123
137
|
- lib/restiny/membership.rb
|
124
138
|
- lib/restiny/user.rb
|
data/lib/restiny/client.rb
DELETED
@@ -1,134 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'faraday'
|
4
|
-
require 'faraday/follow_redirects'
|
5
|
-
|
6
|
-
module Restiny
|
7
|
-
class Client
|
8
|
-
BUNGIE_URL = "https://www.bungie.net"
|
9
|
-
API_BASE_URL = BUNGIE_URL + "/Platform"
|
10
|
-
|
11
|
-
attr_accessor :api_key, :manifest
|
12
|
-
|
13
|
-
def initialize(api_key)
|
14
|
-
@api_key = api_key
|
15
|
-
end
|
16
|
-
|
17
|
-
# Manifest methods
|
18
|
-
|
19
|
-
def download_manifest(locale = 'en')
|
20
|
-
response = get("/Destiny2/Manifest/")
|
21
|
-
|
22
|
-
manifest_path = response.body.dig('Response', 'mobileWorldContentPaths', locale)
|
23
|
-
raise "Unable to determine manifest URL" if manifest_path.nil?
|
24
|
-
|
25
|
-
Manifest.download(BUNGIE_URL + manifest_path)
|
26
|
-
end
|
27
|
-
|
28
|
-
# Profile methods
|
29
|
-
|
30
|
-
def get_profile(membership_id, membership_type, components = [])
|
31
|
-
raise "You must provide at least one component" if components.empty?
|
32
|
-
|
33
|
-
component_query = components.join(",")
|
34
|
-
response = get("/Destiny2/#{membership_type}/Profile/#{membership_id}?components=#{component_query}")
|
35
|
-
|
36
|
-
{}.tap do |output|
|
37
|
-
components.each do |component|
|
38
|
-
case component.downcase
|
39
|
-
when 'characters'
|
40
|
-
output[:characters] = parse_profile_characters_response(response)
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
# Account methods
|
47
|
-
|
48
|
-
def get_user_by_bungie_name(full_display_name, membership_type = PLATFORM_ALL)
|
49
|
-
display_name, display_name_code = full_display_name.split('#')
|
50
|
-
raise "You must provide a valid Bungie name" if display_name.nil? || display_name_code.nil?
|
51
|
-
|
52
|
-
params = {
|
53
|
-
displayName: display_name,
|
54
|
-
displayNameCode: display_name_code
|
55
|
-
}
|
56
|
-
|
57
|
-
response = post("/Destiny2/SearchDestinyPlayerByBungieName/#{membership_type}/", params)
|
58
|
-
result = response.body.dig('Response')
|
59
|
-
|
60
|
-
return [] if result.nil?
|
61
|
-
|
62
|
-
Restiny::User.new(
|
63
|
-
display_name: result[0]['bungieGlobalDisplayName'],
|
64
|
-
display_name_code: result[0]['bungieGlobalDisplayNameCode'],
|
65
|
-
memberships: result
|
66
|
-
)
|
67
|
-
end
|
68
|
-
|
69
|
-
def search_users(name, page = 0)
|
70
|
-
params = { displayNamePrefix: name }
|
71
|
-
response = post("/User/Search/GlobalName/#{page}", params).body
|
72
|
-
return [] if response.nil?
|
73
|
-
|
74
|
-
search_results = response.dig('Response', 'searchResults')
|
75
|
-
return [] if search_results.nil?
|
76
|
-
|
77
|
-
search_results.map do |user|
|
78
|
-
Restiny::User.new(
|
79
|
-
display_name: user['bungieGlobalDisplayName'],
|
80
|
-
display_name_code: user['bungieGlobalDisplayNameCode'],
|
81
|
-
memberships: user['destinyMemberships']
|
82
|
-
)
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
def get(endpoint_url, params = {})
|
87
|
-
connection.get(API_BASE_URL + endpoint_url, params)
|
88
|
-
end
|
89
|
-
|
90
|
-
def post(endpoint_url, body, headers = {})
|
91
|
-
connection.post(API_BASE_URL + endpoint_url, body, headers)
|
92
|
-
end
|
93
|
-
|
94
|
-
private
|
95
|
-
|
96
|
-
def parse_profile_characters_response(response)
|
97
|
-
characters = response.body.dig('Response', 'characters', 'data')
|
98
|
-
return [] if characters.nil?
|
99
|
-
|
100
|
-
result = []
|
101
|
-
|
102
|
-
[].tap do |result|
|
103
|
-
characters.each_value do |character|
|
104
|
-
result << Restiny::Character.new(
|
105
|
-
id: character['characterId'],
|
106
|
-
session_playtime: character['minutesPlayedThisSession'],
|
107
|
-
total_playtime: character['minutesPlayedTotal'],
|
108
|
-
light_level: character['light'],
|
109
|
-
stats: [],
|
110
|
-
emblem: [],
|
111
|
-
progression: character['progression']
|
112
|
-
)
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
def default_headers
|
118
|
-
{
|
119
|
-
'User-Agent': "restiny v#{Restiny::VERSION}",
|
120
|
-
'X-API-KEY': @api_key
|
121
|
-
}
|
122
|
-
end
|
123
|
-
|
124
|
-
def connection
|
125
|
-
@connection ||= Faraday.new(headers: default_headers) do |faraday|
|
126
|
-
faraday.request :json
|
127
|
-
faraday.request :url_encoded
|
128
|
-
faraday.response :json
|
129
|
-
faraday.response :follow_redirects
|
130
|
-
faraday.response :raise_error
|
131
|
-
end
|
132
|
-
end
|
133
|
-
end
|
134
|
-
end
|