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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2ff82944ba14279df69b4c5e8d151a4d0b18c5176273af3ed68142327473993
4
- data.tar.gz: a214d550713959f482943f2120d491a91227f46599ab5f5da46e55abed7174be
3
+ metadata.gz: 9ad23e571d4f6b91fc296e0578074983f8d6883518ffe94743a6003bbf20dfd0
4
+ data.tar.gz: 6aba22d2cfb3a8bc2f8c93d0a11ccfef63a55eaa8aaec008a3b769710d4b3ff5
5
5
  SHA512:
6
- metadata.gz: 269e932d167058936ac4ccc092493b72da71026dbf5947467367671e2a64421496f0f9d40b99962eb1f1dc416754734ed241dfaaee53440ea03bee610efdb2d2
7
- data.tar.gz: def65d2d7fc397ca3e833a6eec1b922d78639d7c800f763f4e15740a9bde08d8e2e36e5db020dd9654594575c11b1f1c078d3d57f0b9f1eb1bbaf2197e70729a
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
@@ -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
- 'DestinyAchievementDefinition': { item: 'achievement', items: 'achievements' },
12
- 'DestinyActivityDefinition': { item: 'activity', items: 'activities' },
13
- 'DestinyActivityGraphDefinition': { item: 'activity_graph', items: 'activity_graphs' },
14
- 'DestinyActivityModeDefinition': { item: 'activity_modes', items: 'activity_modes' },
15
- 'DestinyActivityModifierDefinition': { item: 'activity_modifier', items: 'activity_modifiers' },
16
- 'DestinyActivityTypeDefinition': { item: 'activity_type', items: 'activity_types' },
17
- 'DestinyArtifactDefinition': { item: 'artifact', items: 'artifacts' },
18
- 'DestinyBondDefinition': { item: 'bond', items: 'bonds' },
19
- 'DestinyBreakerTypeDefinition': { item: 'breaker_type', items: 'breaker_types' },
20
- 'DestinyChecklistDefinition': { item: 'checklist', items: 'checklists' },
21
- 'DestinyClassDefinition': { item: 'class', items: 'classes' },
22
- 'DestinyCollectibleDefinition': { item: 'collectible', items: 'collectibles' },
23
- 'DestinyDamageTypeDefinition': { item: 'damage_type', items: 'damage_types' },
24
- 'DestinyDestinationDefinition': { item: 'destination', items: 'destinations' },
25
- 'DestinyEnergyTypeDefinition': { item: 'energy_type', items: 'energy_types' },
26
- 'DestinyEquipmentSlotDefinition': { item: 'eqiupment_slot', items: 'equipment_slots' },
27
- 'DestinyEventCardDefinition': { item: 'event_card', items: 'event_cards' },
28
- 'DestinyFactionDefinition': { item: 'faction', items: 'factions' },
29
- 'DestinyGenderDefinition': { item: 'gender', items: 'genders' },
30
- 'DestinyHistoricalStatsDefinition': { item: 'historical_stat', items: 'historical_stats' },
31
- 'DestinyInventoryBucketDefinition': { item: 'inventory_bucket', items: 'inventory_buckets' },
32
- 'DestinyInventoryItemDefinition': { item: 'inventory_item', items: 'inventory_items' },
33
- 'DestinyItemCategoryDefinition': { item: 'item_category', items: 'item_categories' },
34
- 'DestinyItemTierTypeDefinition': { item: 'item_tier_type', items: 'item_tier_types' },
35
- 'DestinyLocationDefinition': { item: 'location', items: 'locations' },
36
- 'DestinyLoreDefinition': { item: 'lore', items: 'lore_entries' },
37
- 'DestinyMaterialRequirementSetDefinition': { item: 'material_requirement_set', items: 'material_requirement_sets' },
38
- 'DestinyMedalTierDefinition': { item: 'medal_tier', items: 'medal_tiers' },
39
- 'DestinyMetricDefinition': { item: 'metric', items: 'metrics' },
40
- 'DestinyMilestoneDefinition': { item: 'milestone', items: 'milestones' },
41
- 'DestinyObjectiveDefinition': { item: 'objective', items: 'objectives' },
42
- 'DestinyPlaceDefinition': { item: 'place', items: 'places' },
43
- 'DestinyPlugSetDefinition': { item: 'plug_set', items: 'plug_sets' },
44
- 'DestinyPowerCapDefinition': { item: 'power_cap', items: 'power_caps' },
45
- 'DestinyPresentationNodeDefinition': { item: 'presentation_node', items: 'presentation_nodes' },
46
- 'DestinyProgressionDefinition': { item: 'progression', items: 'progression_data' },
47
- 'DestinyProgressionLevelRequirementDefinition': { item: 'progression_level_requirement', items: 'progression_level_requirements' },
48
- 'DestinyRaceDefinition': { item: 'race', items: 'races' },
49
- 'DestinyRecordDefinition': { item: 'record', items: 'records' },
50
- 'DestinyReportReasonCategoryDefinition': { item: 'report_reason_category', items: 'report_reason_categories' },
51
- 'DestinyRewardSourceDefinition': { item: 'reward_source', items: 'reward_sources' },
52
- 'DestinySackRewardItemListDefinition': { item: 'sack_reward_item_list', items: 'sack_reward_item_lists' },
53
- 'DestinySandboxPatternDefinition': { item: 'sandbox_pattern', items: 'sandbox_patterns' },
54
- 'DestinySandboxPerkDefinition': { item: 'sandbox_perk', items: 'sandbox_perks' },
55
- 'DestinySeasonDefinition': { item: 'season', items: 'seasons' },
56
- 'DestinySeasonPassDefinition': { item: 'season_pass', items: 'season_passes' },
57
- 'DestinySocketCategoryDefinition': { item: 'socket_category', items: 'socket_categories' },
58
- 'DestinySocketTypeDefinition': { item: 'socket_type', items: 'socket_types' },
59
- 'DestinyStatDefinition': { item: 'stat', items: 'stats' },
60
- 'DestinyStatGroupDefinition': { item: 'stat_group', items: 'stat_groups' },
61
- 'DestinyTalentGridDefinition': { item: 'talent_grid', items: 'talent_grids' },
62
- 'DestinyTraitCategoryDefinition': { item: 'trait_category', items: 'trait_categories' },
63
- 'DestinyTraitDefinition': { item: 'trait', items: 'traits' },
64
- 'DestinyUnlockDefinition': { item: 'unlock', items: 'unlocks' },
65
- 'DestinyVendorDefinition': { item: 'vendor', items: 'vendors' },
66
- 'DestinyVendorGroupDefinition': { item: 'vendor_group', items: 'vendor_groups' }
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
- define_method method_names[:item] do |id|
73
- query_table(table_name, id: id)[0]
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
- query_table(table_name, limit: limit)
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::Error
89
- raise "Unable to download the manifest from Bungie"
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
- raise "You must provide the file path for the manifest file" if file_path.empty?
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 self.clean_row_keys(row_hash)
104
- OpenStruct.new.tap do |output|
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Restiny
4
- VERSION = '0.1.0'
4
+ VERSION = '0.3.0'
5
5
  end
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/client'
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.1.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-02-19 00:00:00.000000000 Z
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
@@ -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