restiny 0.1.0 → 0.3.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 +88 -99
- 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: 9ad23e571d4f6b91fc296e0578074983f8d6883518ffe94743a6003bbf20dfd0
|
4
|
+
data.tar.gz: 6aba22d2cfb3a8bc2f8c93d0a11ccfef63a55eaa8aaec008a3b769710d4b3ff5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e600c2e4a0552420404dd6aa11692613e65cd6ee7efddc7a5200f8750569e39f259d5156b25fb8baca7175f7575b870794827213548e74877c7faeea23c18e75
|
7
|
+
data.tar.gz: 2e319308c54b1e793862400bc5ff362d0570924edf372d1f07a1f06766210e031dda2c79b7db8d171588d8dc8b849c482f6bb4d0ca86902e7d739bd6c831a410
|
@@ -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
@@ -1,5 +1,4 @@
|
|
1
1
|
# frozen_string/literal: true
|
2
|
-
|
3
2
|
require 'down'
|
4
3
|
require 'json'
|
5
4
|
require 'sqlite3'
|
@@ -8,73 +7,91 @@ require 'zip'
|
|
8
7
|
module Restiny
|
9
8
|
class Manifest
|
10
9
|
TABLES = {
|
11
|
-
'
|
12
|
-
'
|
13
|
-
'
|
14
|
-
'
|
15
|
-
'
|
16
|
-
'
|
17
|
-
'
|
18
|
-
'
|
19
|
-
'
|
20
|
-
'
|
21
|
-
'
|
22
|
-
'
|
23
|
-
'
|
24
|
-
'
|
25
|
-
'
|
26
|
-
'
|
27
|
-
'
|
28
|
-
'
|
29
|
-
'
|
30
|
-
'
|
31
|
-
'
|
32
|
-
'
|
33
|
-
'
|
34
|
-
'
|
35
|
-
'
|
36
|
-
'
|
37
|
-
'
|
38
|
-
'
|
39
|
-
'
|
40
|
-
'
|
41
|
-
'
|
42
|
-
'
|
43
|
-
'
|
44
|
-
'
|
45
|
-
'
|
46
|
-
'
|
47
|
-
'
|
48
|
-
'
|
49
|
-
'
|
50
|
-
'
|
51
|
-
'
|
52
|
-
'
|
53
|
-
'
|
54
|
-
'
|
55
|
-
'
|
56
|
-
'
|
57
|
-
'
|
58
|
-
'
|
59
|
-
'
|
60
|
-
'
|
61
|
-
'
|
62
|
-
'
|
63
|
-
'
|
64
|
-
'
|
65
|
-
'
|
66
|
-
'
|
10
|
+
'Achievement': { item: 'achievement', items: 'achievements' },
|
11
|
+
'Activity': { item: 'activity', items: 'activities' },
|
12
|
+
'ActivityGraph': { item: 'activity_graph', items: 'activity_graphs' },
|
13
|
+
'ActivityMode': { item: 'activity_modes', items: 'activity_modes' },
|
14
|
+
'ActivityModifier': { item: 'activity_modifier', items: 'activity_modifiers' },
|
15
|
+
'ActivityType': { item: 'activity_type', items: 'activity_types' },
|
16
|
+
'Artifact': { item: 'artifact', items: 'artifacts' },
|
17
|
+
'Bond': { item: 'bond', items: 'bonds' },
|
18
|
+
'BreakerType': { item: 'breaker_type', items: 'breaker_types' },
|
19
|
+
'Checklist': { item: 'checklist', items: 'checklists' },
|
20
|
+
'Class': { item: 'class', items: 'classes' },
|
21
|
+
'Collectible': { item: 'collectible', items: 'collectibles' },
|
22
|
+
'DamageType': { item: 'damage_type', items: 'damage_types' },
|
23
|
+
'Destination': { item: 'destination', items: 'destinations' },
|
24
|
+
'EnergyType': { item: 'energy_type', items: 'energy_types' },
|
25
|
+
'EquipmentSlot': { item: 'equipment_slot', items: 'equipment_slots' },
|
26
|
+
'EventCard': { item: 'event_card', items: 'event_cards' },
|
27
|
+
'Faction': { item: 'faction', items: 'factions' },
|
28
|
+
'Gender': { item: 'gender', items: 'genders' },
|
29
|
+
'HistoricalStats': { item: 'historical_stat', items: 'historical_stats' },
|
30
|
+
'InventoryBucket': { item: 'inventory_bucket', items: 'inventory_buckets' },
|
31
|
+
'InventoryItem': { item: 'inventory_item', items: 'inventory_items' },
|
32
|
+
'ItemCategory': { item: 'item_category', items: 'item_categories' },
|
33
|
+
'ItemTierType': { item: 'item_tier_type', items: 'item_tier_types' },
|
34
|
+
'Location': { item: 'location', items: 'locations' },
|
35
|
+
'Lore': { item: 'lore', items: 'lore_entries' },
|
36
|
+
'MaterialRequirementSet': { item: 'material_requirement_set', items: 'material_requirement_sets' },
|
37
|
+
'MedalTier': { item: 'medal_tier', items: 'medal_tiers' },
|
38
|
+
'Metric': { item: 'metric', items: 'metrics' },
|
39
|
+
'Milestone': { item: 'milestone', items: 'milestones' },
|
40
|
+
'Objective': { item: 'objective', items: 'objectives' },
|
41
|
+
'Place': { item: 'place', items: 'places' },
|
42
|
+
'PlugSet': { item: 'plug_set', items: 'plug_sets' },
|
43
|
+
'PowerCap': { item: 'power_cap', items: 'power_caps' },
|
44
|
+
'PresentationNode': { item: 'presentation_node', items: 'presentation_nodes' },
|
45
|
+
'Progression': { item: 'progression', items: 'progression_data' },
|
46
|
+
'ProgressionLevelRequirement': { item: 'progression_level_requirement', items: 'progression_level_requirements' },
|
47
|
+
'Race': { item: 'race', items: 'races' },
|
48
|
+
'Record': { item: 'record', items: 'records' },
|
49
|
+
'ReportReasonCategory': { item: 'report_reason_category', items: 'report_reason_categories' },
|
50
|
+
'RewardSource': { item: 'reward_source', items: 'reward_sources' },
|
51
|
+
'SackRewardItemList': { item: 'sack_reward_item_list', items: 'sack_reward_item_lists' },
|
52
|
+
'SandboxPattern': { item: 'sandbox_pattern', items: 'sandbox_patterns' },
|
53
|
+
'SandboxPerk': { item: 'sandbox_perk', items: 'sandbox_perks' },
|
54
|
+
'Season': { item: 'season', items: 'seasons' },
|
55
|
+
'SeasonPass': { item: 'season_pass', items: 'season_passes' },
|
56
|
+
'SocketCategory': { item: 'socket_category', items: 'socket_categories' },
|
57
|
+
'SocketType': { item: 'socket_type', items: 'socket_types' },
|
58
|
+
'Stat': { item: 'stat', items: 'stats' },
|
59
|
+
'StatGroup': { item: 'stat_group', items: 'stat_groups' },
|
60
|
+
'TalentGrid': { item: 'talent_grid', items: 'talent_grids' },
|
61
|
+
'TraitCategory': { item: 'trait_category', items: 'trait_categories' },
|
62
|
+
'Trait': { item: 'trait', items: 'traits' },
|
63
|
+
'Unlock': { item: 'unlock', items: 'unlocks' },
|
64
|
+
'Vendor': { item: 'vendor', items: 'vendors' },
|
65
|
+
'VendorGroup': { item: 'vendor_group', items: 'vendor_groups' }
|
67
66
|
}
|
68
67
|
|
69
68
|
attr_reader :file_path
|
70
69
|
|
71
70
|
TABLES.each do |table_name, method_names|
|
72
|
-
|
73
|
-
|
71
|
+
full_table_name = "Destiny#{table_name}Definition"
|
72
|
+
|
73
|
+
define_method method_names[:item] do |hash|
|
74
|
+
query = "SELECT json FROM #{full_table_name} WHERE json_extract(json, '$.hash')=?"
|
75
|
+
|
76
|
+
result = perform_query(query, [hash])
|
77
|
+
result = JSON.parse(result[0]['json']) unless result.nil?
|
74
78
|
end
|
75
79
|
|
76
|
-
define_method method_names[:items] do |limit: nil|
|
77
|
-
|
80
|
+
define_method method_names[:items] do |limit: nil, filter_empty: false, &block|
|
81
|
+
query = "SELECT json_extract(json, '$.hash') AS hash, json_extract(json, '$.displayProperties.name') AS name
|
82
|
+
FROM #{full_table_name} "
|
83
|
+
|
84
|
+
query << "WHERE json_extract(json, '$.displayProperties.name') IS NOT NULL " if filter_empty
|
85
|
+
query << "ORDER BY json_extract(json, '$.index')"
|
86
|
+
|
87
|
+
bindings = []
|
88
|
+
|
89
|
+
if limit
|
90
|
+
query << " LIMIT ?"
|
91
|
+
bindings << limit
|
92
|
+
end
|
93
|
+
|
94
|
+
perform_query(query, bindings)
|
78
95
|
end
|
79
96
|
end
|
80
97
|
|
@@ -85,14 +102,16 @@ module Restiny
|
|
85
102
|
Zip::File.open(zipped_file) { |file| file.first.extract(manifest_path) }
|
86
103
|
|
87
104
|
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"
|
105
|
+
rescue Down::ResponseError => error
|
106
|
+
raise Restiny::NetworkError.new("Unable to download the manifest file", error.response.code)
|
107
|
+
rescue Zip::Error => error
|
108
|
+
raise Restiny::Error.new("Unable to unzip the manifest file (#{e})")
|
92
109
|
end
|
93
110
|
|
94
111
|
def initialize(file_path)
|
95
|
-
|
112
|
+
if file_path.empty? || !File.exist?(file_path) || !File.file?(file_path)
|
113
|
+
raise Restiny::InvalidParamsError.new("You must provide a valid path for the manifest file")
|
114
|
+
end
|
96
115
|
|
97
116
|
@database = SQLite3::Database.new(file_path, results_as_hash: true)
|
98
117
|
@file_path = file_path
|
@@ -100,40 +119,10 @@ module Restiny
|
|
100
119
|
|
101
120
|
private
|
102
121
|
|
103
|
-
def
|
104
|
-
|
105
|
-
row_hash.each_pair do |key, value|
|
106
|
-
key = key.gsub(/(\B[A-Z])/, '_\1') if key =~ /\B[A-Z]/
|
107
|
-
output[key.downcase] = if value.is_a?(Hash)
|
108
|
-
clean_row_keys(value)
|
109
|
-
else
|
110
|
-
value
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
def query_table(table_name, id: nil, limit: nil)
|
117
|
-
query = "SELECT json FROM #{table_name}"
|
118
|
-
bindings = []
|
119
|
-
|
120
|
-
if id
|
121
|
-
query << " WHERE id=?"
|
122
|
-
bindings << id
|
123
|
-
end
|
124
|
-
|
125
|
-
query << " ORDER BY json_extract(json, '$.index')" unless id
|
126
|
-
|
127
|
-
if limit
|
128
|
-
query << " LIMIT ?"
|
129
|
-
bindings << limit
|
130
|
-
end
|
131
|
-
|
132
|
-
@database.execute(query, bindings).map do |row|
|
133
|
-
Manifest::clean_row_keys(JSON.parse(row['json'])) unless row['json'].nil?
|
134
|
-
end
|
122
|
+
def perform_query(query, bindings, &block)
|
123
|
+
@database.execute(query, bindings)
|
135
124
|
rescue SQLite3::Exception => e
|
136
|
-
raise "Error while querying the manifest (#{e})"
|
125
|
+
raise Restiny::RequestError.new("Error while querying the manifest (#{e})")
|
137
126
|
end
|
138
127
|
end
|
139
128
|
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.3.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-15 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
|