restiny 0.1.0 → 0.3.0
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 +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
|