restiny 5.0.0 → 6.0.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: 366f1afb92a72c9adbc817eb5e8651fcd2df0656584c138660708525343b8407
4
- data.tar.gz: 7fa7f9df35a0ee12206627a43c7ffb4d299819c5d26340c83186f520e30de99e
3
+ metadata.gz: 7a11b1fe287424ab29d345e8598b810afd733d59f634e00badf837ac7f16ec3d
4
+ data.tar.gz: fb704f715f42ad34aa722435079e7aae31c9660d5b9e7b32e1f0d9e82efafabd
5
5
  SHA512:
6
- metadata.gz: 8c71e268039ac692949adf704883eda27bc2da5d747dab7d610a09621921ef3e64e4909bca9456431f404a0ff4211a19f31efbdf7eb57701bf852bacf360cf90
7
- data.tar.gz: 77c6af1bf576b0e590a6659565af7739316cbc3a72b159a26c8b3077308c3713f1cb8cf45c103d3a0993c2012957feaeb372c6bdaec0436b85b4af0b2f290261
6
+ metadata.gz: b32846afdb0a3f1d89c837064f2809d772b8eb1d74903ed449484c75c70975a82a367f591c958a217a616fd55066beef55db5fb6fbff8350787dc163ffe04b6b
7
+ data.tar.gz: 2b8b0b66fdaedcf77cb1b3b308ad54aafd92d5aaa175e9238b28d98ce4ad41da9bc6d6fb4e0c4fdd78dd7c23d6de247bdf7263e28ff4fec1723892866ddae596
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ require 'securerandom'
6
+
7
+ module Restiny
8
+ module Api
9
+ module Authentication
10
+ include Base
11
+
12
+ CODE_RESPONSE_TYPE = 'code'
13
+ AUTH_CODE_GRANT_TYPE = 'authorization_code'
14
+
15
+ def get_authorise_url(redirect_url: nil, state: nil)
16
+ check_oauth_client_id
17
+
18
+ params = {
19
+ response_type: CODE_RESPONSE_TYPE,
20
+ client_id: @oauth_client_id,
21
+ state: state || SecureRandom.hex(15)
22
+ }
23
+
24
+ params['redirect_url'] = redirect_url unless redirect_url.nil?
25
+
26
+ query = params.map { |k, v| "#{k}=#{v}" }.join('&')
27
+
28
+ "#{BUNGIE_URL}/en/oauth/authorize/?#{query}"
29
+ end
30
+
31
+ def request_access_token(code, redirect_url: nil)
32
+ check_oauth_client_id
33
+
34
+ params = { code: code, grant_type: AUTH_CODE_GRANT_TYPE, client_id: @oauth_client_id }
35
+ params['redirect_url'] = redirect_url unless redirect_url.nil?
36
+
37
+ response = http_client.post('/platform/app/oauth/token/', form: params)
38
+ response.raise_for_status
39
+
40
+ response.json
41
+ rescue HTTPX::Error => e
42
+ handle_authentication_error(e)
43
+ end
44
+
45
+ private
46
+
47
+ def handle_authentication_error(error)
48
+ raise Restiny::AuthenticationError,
49
+ "#{error.response.json['error_description']} (#{error.response.json['error']})"
50
+ rescue HTTPX::Error
51
+ raise Restiny::AuthenticationError,
52
+ "#{error.response.status}: #{error.response.headers['x-selfurl']}"
53
+ end
54
+
55
+ def check_oauth_client_id
56
+ return if @oauth_client_id
57
+
58
+ raise Restiny::RequestError,
59
+ 'You need to set an OAuth client ID (Restiny.oauth_client_id)'
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'restiny/constants'
4
+ require 'restiny/errors'
5
+ require 'httpx'
6
+ require 'json'
7
+
8
+ module Restiny
9
+ BUNGIE_URL = 'https://www.bungie.net'
10
+
11
+ attr_accessor :api_key, :oauth_state, :oauth_client_id, :access_token, :user_agent
12
+
13
+ module Api
14
+ module Base
15
+ def get(endpoint)
16
+ make_api_request(endpoint, method: :get)
17
+ end
18
+
19
+ def post(endpoint, params: {})
20
+ make_api_request(endpoint, method: :post, params: params)
21
+ end
22
+
23
+ private
24
+
25
+ def http_client
26
+ HTTPX.with(origin: BUNGIE_URL, headers: api_headers).plugin(:follow_redirects, follow_insecure_redirects: true)
27
+ end
28
+
29
+ def make_api_request(endpoint, method: :get, params: {})
30
+ raise Restiny::InvalidParamsError, 'You need to set an API key (Restiny.api_key)' if @api_key.nil?
31
+
32
+ response = http_client.with(base_path: '/platform/').request(method, endpoint, json: params)
33
+ response.raise_for_status
34
+
35
+ response.json['Response']
36
+ rescue HTTPX::TimeoutError, HTTPX::ResolveError => e
37
+ raise Restiny::RequestError, e.message
38
+ rescue HTTPX::HTTPError => e
39
+ handle_api_error(e)
40
+ end
41
+
42
+ def handle_api_error(error)
43
+ klass = case error.response.status
44
+ when 400..499 then ::Restiny::RequestError
45
+ when 500..599 then ::Restiny::ResponseError
46
+ else ::Restiny::Error
47
+ end
48
+
49
+ raise klass, if error.response.headers['content-type'].match?(%r{^application/json})
50
+ error_message_from_json(error.response.json)
51
+ else
52
+ error.status
53
+ end
54
+ end
55
+
56
+ def error_message_from_json(json)
57
+ "#{json['ErrorStatus']} (#{json['ErrorCode']}): #{json['Message']}"
58
+ end
59
+
60
+ def api_headers
61
+ {}.tap do |headers|
62
+ headers['x-api-key'] = @api_key
63
+ headers['user-agent'] = @user_agent || "restiny v#{Restiny::VERSION}"
64
+ headers['authentication'] = "Bearer #{@access_token}" unless @access_token.nil?
65
+ end
66
+ end
67
+
68
+ def valid_array_param?(param)
69
+ param.is_a?(Array) && !param.empty?
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ require 'tmpdir'
6
+ require 'uri'
7
+
8
+ module Restiny
9
+ module Api
10
+ module Manifest
11
+ include Base
12
+
13
+ def fetch_manifest
14
+ result = get('/Destiny2/Manifest/')
15
+ return result unless result.nil?
16
+
17
+ raise Restiny::ResponseError, 'Unable to fetch manifest details'
18
+ end
19
+
20
+ def download_manifest_json(locale: 'en', definitions: [])
21
+ raise Restiny::InvalidParamsError, 'No definitions provided' unless valid_array_param?(definitions)
22
+ raise Restiny::InvalidParamsError, 'Unknown definitions provided' unless known_definitions?(definitions)
23
+
24
+ paths = fetch_manifest.dig('jsonWorldComponentContentPaths', locale)
25
+ raise Restiny::ResponseError, "Unable to find manifest JSON for locale '#{locale}'" if paths.nil?
26
+
27
+ {}.tap do |files|
28
+ definitions.each do |definition|
29
+ files[definition] = download_manifest_json_by_url(BUNGIE_URL + paths[definition])
30
+ end
31
+ end
32
+ end
33
+
34
+ def known_definitions?(definitions)
35
+ definitions.difference(Restiny::ManifestDefinition.values).empty?
36
+ end
37
+
38
+ def download_manifest_json_by_url(url)
39
+ filename = URI(url).path.split('/').last
40
+ path = File.join(Dir.tmpdir, filename)
41
+
42
+ HTTPX.get(url).copy_to(path)
43
+ raise Restiny::Error, "Unable to download JSON from #{url}" unless File.exist?(path)
44
+
45
+ path
46
+ rescue HTTPX::Error
47
+ raise Restiny::ResponseError, "Unable to download #{definition} JSON file"
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Restiny
6
+ module Api
7
+ module Membership
8
+ include Base
9
+
10
+ def get_user_memberships_by_id(membership_id, membership_type: Platform::ALL)
11
+ raise Restiny::InvalidParamsError, 'Please provide a membership ID' if membership_id.nil?
12
+ raise Restiny::InvalidParamsError, 'Please provide a membership type' if membership_type.nil?
13
+
14
+ get("/User/GetMembershipsById/#{membership_id}/#{membership_type}/")
15
+ end
16
+
17
+ def get_primary_user_membership(membership_id, use_fallback: true)
18
+ raise Restiny::InvalidParamsError, 'Please provide a membership ID' if membership_id.nil?
19
+
20
+ result = get_user_memberships_by_id(membership_id)
21
+ return nil if result.nil?
22
+
23
+ unless result['primaryMembershipId'].nil?
24
+ result['destinyMemberships'].each do |membership|
25
+ return membership if membership['membershipID'] == result['primaryMembershipId']
26
+ end
27
+ end
28
+
29
+ result['destinyMemberships'][0] if use_fallback
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Restiny
6
+ module Api
7
+ module Profile
8
+ include Base
9
+
10
+ def get_profile(membership_id:, membership_type:, components:, type_url: nil)
11
+ raise Restiny::InvalidParamsError, 'No components provided' unless valid_array_param?(components)
12
+
13
+ url = "/Destiny2/#{membership_type}/Profile/#{membership_id}/"
14
+ url += type_url if type_url
15
+ url += "?components=#{components.join(',')}"
16
+
17
+ get(url)
18
+ end
19
+
20
+ def get_character_profile(character_id:, membership_id:, membership_type:, components:)
21
+ get_profile(
22
+ membership_id: membership_id,
23
+ membership_type: membership_type,
24
+ components: components,
25
+ type_url: "Character/#{character_id}/"
26
+ )
27
+ end
28
+
29
+ def get_instanced_item_profile(item_id:, membership_id:, membership_type:, components:)
30
+ get_profile(
31
+ membership_id: membership_id,
32
+ membership_type: membership_type,
33
+ components: components,
34
+ type_url: "Item/#{item_id}/"
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Restiny
6
+ module Api
7
+ module Search
8
+ include Base
9
+
10
+ def search_player_by_bungie_name(name, membership_type: Platform::ALL)
11
+ display_name, display_name_code = name.split('#')
12
+ if display_name.nil? || display_name_code.nil?
13
+ raise Restiny::InvalidParamsError, 'You must provide a valid Bungie name'
14
+ end
15
+
16
+ post("/Destiny2/SearchDestinyPlayerByBungieName/#{membership_type}/", params: {
17
+ displayName: display_name, displayNameCode: display_name_code
18
+ })
19
+ end
20
+
21
+ def search_users_by_global_name(name:, page: 0)
22
+ post("/User/Search/GlobalName/#{page}/", params: { displayNamePrefix: name })
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Restiny
6
+ module Api
7
+ module Stats
8
+ include Base
9
+
10
+ def get_post_game_carnage_report(activity_id:)
11
+ raise Restiny::InvalidParamsError, 'Please provide an activity ID' if activity_id.nil?
12
+
13
+ get("/Destiny2/Stats/PostGameCarnageReport/#{activity_id}/")
14
+ end
15
+
16
+ alias get_pgcr get_post_game_carnage_report
17
+ end
18
+ end
19
+ end
@@ -1,22 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Restiny
4
+ # Definitions for the gaming platforms supported by Destiny 2.
4
5
  module Platform
5
6
  ALL = -1
6
7
  XBOX = 1
7
8
  PSN = 2
8
9
  STEAM = 3
9
10
  EPIC = 6
11
+
12
+ def self.names
13
+ {
14
+ ALL => 'All',
15
+ XBOX => 'Xbox',
16
+ PSN => 'PSN',
17
+ STEAM => 'Steam',
18
+ EPIC => 'Epic'
19
+ }
20
+ end
10
21
  end
11
22
 
12
23
  module ItemLocation
24
+ # Definitions for the possible locations an item can be related to a character.
13
25
  UNKNOWN = 0
14
26
  INVENTORY = 1
15
27
  VAULT = 2
16
28
  VENDOR = 3
17
29
  POSTMASTER = 4
30
+
31
+ def self.names
32
+ {
33
+ UNKNOWN => 'Unknown',
34
+ INVENTORY => 'Inventory',
35
+ VAULT => 'Vault',
36
+ VENDOR => 'Vendor',
37
+ POSTMASTER => 'Postmaster'
38
+ }
39
+ end
18
40
  end
19
41
 
42
+ # Definitions for the tier/rarity of a particular item.
20
43
  module TierType
21
44
  UNKNOWN = 0
22
45
  CURRENCY = 1
@@ -25,47 +48,215 @@ module Restiny
25
48
  RARE = 4
26
49
  SUPERIOR = 5
27
50
  EXOTIC = 6
51
+
52
+ def self.names
53
+ {
54
+ UNKNOWN => 'Unknown',
55
+ CURRENCY => 'Currency',
56
+ BASIC => 'Basic',
57
+ COMMON => 'Common',
58
+ RARE => 'Rare',
59
+ SUPERIOR => 'Superior',
60
+ EXOTIC => 'Exotic'
61
+ }
62
+ end
28
63
  end
29
64
 
65
+ # Definitions for a Guardian's class.
30
66
  module GuardianClass
31
67
  TITAN = 0
32
68
  HUNTER = 1
33
69
  WARLOCK = 2
34
70
  UNKNOWN = 3
71
+
72
+ def self.names
73
+ {
74
+ TITAN => 'Titan',
75
+ HUNTER => 'Hunter',
76
+ WARLOCK => 'Warlock',
77
+ UNKNOWN => 'Unknown'
78
+ }
79
+ end
35
80
  end
36
81
 
82
+ # Definitions for a Guardian's race.
37
83
  module Race
38
84
  HUMAN = 0
39
85
  AWOKEN = 1
40
86
  EXO = 2
41
87
  UNKNOWN = 3
88
+
89
+ def self.names
90
+ {
91
+ HUMAN => 'Human',
92
+ AWOKEN => 'Awoken',
93
+ EXO => 'Exo',
94
+ UNKNOWN => 'Unknown'
95
+ }
96
+ end
42
97
  end
43
98
 
99
+ # Definitions for a Guardian's gender.
44
100
  module Gender
45
101
  MASCULINE = 0
46
102
  FEMININE = 1
47
103
  UNKNOWN = 2
104
+
105
+ def self.names
106
+ {
107
+ MASCULINE => 'Masculine',
108
+ FEMININE => 'Feminine',
109
+ UNKNOWN => 'Unknown'
110
+ }
111
+ end
48
112
  end
49
113
 
114
+ # Definitions for the various types of ammunition used in the game.
50
115
  module Ammunition
51
116
  NONE = 0
52
117
  PRIMARY = 1
53
118
  SPECIAL = 2
54
119
  HEAVY = 3
55
120
  UNKNOWN = 4
121
+
122
+ def self.names
123
+ {
124
+ NONE => 'None',
125
+ PRIMARY => 'Primary',
126
+ SPECIAL => 'Special',
127
+ HEAVY => 'Heavy',
128
+ UNKNOWN => 'Unknown'
129
+ }
130
+ end
56
131
  end
57
132
 
133
+ # Definitions for the various component types used when requesting a profile entry.
58
134
  module ComponentType
59
- CHARACTERS = 'Characters'
60
- CHARACTER_EQUIPMENT = 'CharacterEquipment'
61
- CHARACTER_INVENTORIES = 'CharacterInventories'
62
- CHARACTER_LOADOUTS = 'CharacterLoadouts'
63
- PROFILES = 'Profiles'
64
- PROFILE_INVENTORIES = 'ProfileInventories'
65
- ITEM_INSTANCES = 'ItemInstances'
66
- ITEM_SOCKETS = 'ItemSockets'
67
- ITEM_COMMON_DATA = 'ItemCommonData'
68
- ITEM_PLUG_STATES = 'ItemPlugStates'
69
- ITEM_REUSABLE_PLUGS = 'ItemReusablePlugs'
135
+ PROFILES = '100'
136
+ VENDOR_RECEIPTS = '101'
137
+ PROFILE_INVENTORIES = '102'
138
+ PROFILE_CURRENCIES = '103'
139
+ PROFILE_PROGRESSION = '104'
140
+ PLATFORM_SILVER = '105'
141
+ CHARACTERS = '200'
142
+ CHARACTER_INVENTORIES = '201'
143
+ CHARACTER_PROGRESSIONS = '202'
144
+ CHARACTER_RENDERDATA = '203'
145
+ CHARACTER_ACTIVITIES = '204'
146
+ CHARACTER_EQUIPMENT = '205'
147
+ CHARACTER_LOADOUTS = '206'
148
+ ITEM_INSTANCES = '300'
149
+ ITEM_OBJECTIVES = '301'
150
+ ITEM_PERKS = '302'
151
+ ITEM_RENDER_DATA = '303'
152
+ ITEM_STATS = '304'
153
+ ITEM_SOCKETS = '305'
154
+ ITEM_TALENT_GRIDS = '306'
155
+ ITEM_COMMON_DATA = '307'
156
+ ITEM_PLUG_STATES = '308'
157
+ ITEM_PLUG_OBJECTIVES = '309'
158
+ ITEM_REUSABLE_PLUGS = '310'
159
+ VENDORS = '400'
160
+ VENDOR_CATEGORIES = '401'
161
+ VENDOR_SALES = '402'
162
+ KIOSKS = '500'
163
+ CURRENCY_LOOKUPS = '600'
164
+ PRESENTATION_NODES = '700'
165
+ COLLECTIBLES = '800'
166
+ RECORDS = '900'
167
+ TRANSITORY = '1000'
168
+ METRICS = '1100'
169
+ STRING_VARIABLES = '1200'
170
+ CRAFTABLES = '1300'
171
+ SOCIAL_COMMENDATIONS = '1400'
172
+ end
173
+
174
+ # The categories of data stored in the manifest.
175
+ module ManifestDefinition
176
+ def self.values
177
+ constants.map { |c| const_get(c) }
178
+ end
179
+
180
+ ACHIEVEMENT = 'DestinyAchievementDefinition'
181
+ ACTIVITY = 'DestinyActivityDefinition'
182
+ ACTIVITY_GRAPH = 'DestinyActivityGraphDefinition'
183
+ ACTIVITY_INTERACTABLE = 'DestinyActivityInteractableDefinition'
184
+ ACTIVITY_MODE = 'DestinyActivityModeDefinition'
185
+ ACTIVITY_MODIFIER = 'DestinyActivityModifierDefinition'
186
+ ACTIVITY_TYPE = 'DestinyActivityTypeDefinition'
187
+ ART_DYE_CHANNEL = 'DestinyArtDyeChannelDefinition'
188
+ ART_DYE_REFERENCE = 'DestinyArtDyeReferenceDefinition'
189
+ ARTIFACT = 'DestinyArtifactDefinition'
190
+ BOND = 'DestinyBondDefinition'
191
+ BREAKER_TYPE = 'DestinyBreakerTypeDefinition'
192
+ CHARACTER_CUSTOMIZATION_CATEGORY = 'DestinyCharacterCustomizationCategoryDefinition'
193
+ CHARACTER_CUSTOMIZATION_OPTION = 'DestinyCharacterCustomizationOptionDefinition'
194
+ CHECKLIST = 'DestinyChecklistDefinition'
195
+ CLASS = 'DestinyClassDefinition'
196
+ COLLECTIBLE = 'DestinyCollectibleDefinition'
197
+ DAMAGE_TYPE = 'DestinyDamageTypeDefinition'
198
+ DESTINATION = 'DestinyDestinationDefinition'
199
+ ENERGY_TYPE = 'DestinyEnergyTypeDefinition'
200
+ ENTITLEMENT_OFFER = 'DestinyEntitlementOfferDefinition'
201
+ EQUIPMENT_SLOT = 'DestinyEquipmentSlotDefinition'
202
+ EVENT_CARD = 'DestinyEventCardDefinition'
203
+ FACTION = 'DestinyFactionDefinition'
204
+ GENDER = 'DestinyGenderDefinition'
205
+ GUARDIAN_RANK_CONSTANTS = 'DestinyGuardianRankConstantsDefinition'
206
+ GUARDIAN_RANK = 'DestinyGuardianRankDefinition'
207
+ INVENTORY_BUCKET = 'DestinyInventoryBucketDefinition'
208
+ INVENTORY_ITEM = 'DestinyInventoryItemDefinition'
209
+ INVENTORY_ITEM_LITE = 'DestinyInventoryItemLiteDefinition'
210
+ ITEM_CATEGORY = 'DestinyItemCategoryDefinition'
211
+ ITEM_TIER_TYPE = 'DestinyItemTierTypeDefinition'
212
+ LOADOUT_COLOR = 'DestinyLoadoutColorDefinition'
213
+ LOADOUT_CONSTANTS = 'DestinyLoadoutConstantsDefinition'
214
+ LOADOUT_ICON = 'DestinyLoadoutIconDefinition'
215
+ LOADOUT_NAME = 'DestinyLoadoutNameDefinition'
216
+ LOCATION = 'DestinyLocationDefinition'
217
+ LORE = 'DestinyLoreDefinition'
218
+ MATERIAL_REQUIREMENT_SET = 'DestinyMaterialRequirementSetDefinition'
219
+ MEDAL_TIER = 'DestinyMedalTierDefinition'
220
+ METRIC = 'DestinyMetricDefinition'
221
+ MILESTONE = 'DestinyMilestoneDefinition'
222
+ NODE_STEP_SUMMARY = 'DestinyNodeStepSummaryDefinition'
223
+ OBJECTIVE = 'DestinyObjectiveDefinition'
224
+ PLACE = 'DestinyPlaceDefinition'
225
+ PLATFORM_BUCKET_MAPPING = 'DestinyPlatformBucketMappingDefinition'
226
+ PLUG_SET = 'DestinyPlugSetDefinition'
227
+ POWER_CAP = 'DestinyPowerCapDefinition'
228
+ PRESENTATION_NODE = 'DestinyPresentationNodeDefinition'
229
+ PROGRESSION = 'DestinyProgressionDefinition'
230
+ PROGRESSION_LEVEL_REQUIREMENT = 'DestinyProgressionLevelRequirementDefinition'
231
+ PROGRESSION_MAPPING = 'DestinyProgressionMappingDefinition'
232
+ RACE = 'DestinyRaceDefinition'
233
+ RECORD = 'DestinyRecordDefinition'
234
+ REPORT_REASON_CATEGORY = 'DestinyReportReasonCategoryDefinition'
235
+ REWARD_ADJUSTER_POINTER = 'DestinyRewardAdjusterPointerDefinition'
236
+ REWARD_ADJUSTER_PROGRESSION_MAP = 'DestinyRewardAdjusterProgressionMapDefinition'
237
+ REWARD_ITEM_LIST = 'DestinyRewardItemListDefinition'
238
+ REWARD_MAPPING = 'DestinyRewardMappingDefinition'
239
+ REWARD_SHEET = 'DestinyRewardSheetDefinition'
240
+ REWARD_SOURCE = 'DestinyRewardSourceDefinition'
241
+ SACK_REWARD_ITEM_LIST = 'DestinySackRewardItemListDefinition'
242
+ SANDBOX_PATTERN = 'DestinySandboxPatternDefinition'
243
+ SANDBOX_PERK = 'DestinySandboxPerkDefinition'
244
+ SEASON = 'DestinySeasonDefinition'
245
+ SEASON_PASS = 'DestinySeasonPassDefinition'
246
+ SOCIAL_COMMENDATION = 'DestinySocialCommendationDefinition'
247
+ SOCIAL_COMMENDATION_NODE = 'DestinySocialCommendationNodeDefinition'
248
+ SOCKET_CATEGORY = 'DestinySocketCategoryDefinition'
249
+ SOCKET_TYPE = 'DestinySocketTypeDefinition'
250
+ STAT = 'DestinyStatDefinition'
251
+ STAT_GROUP = 'DestinyStatGroupDefinition'
252
+ TALENT_GRID = 'DestinyTalentGridDefinition'
253
+ TRAIT = 'DestinyTraitDefinition'
254
+ UNLOCK_COUNT_MAPPING = 'DestinyUnlockCountMappingDefinition'
255
+ UNLOCK = 'DestinyUnlockDefinition'
256
+ UNLOCK_EVENT = 'DestinyUnlockEventDefinition'
257
+ UNLOCK_EXPRESSION_MAPPING = 'DestinyUnlockExpressionMappingDefinition'
258
+ UNLOCK_VALUE = 'DestinyUnlockValueDefinition'
259
+ VENDOR = 'DestinyVendorDefinition'
260
+ VENDOR_GROUP = 'DestinyVendorGroupDefinition'
70
261
  end
71
262
  end
@@ -1,28 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Restiny
4
- class Error < StandardError
5
- def initialize(message, status = nil)
6
- @status = status
7
- super(message)
8
- end
9
- end
10
-
11
- class NetworkError < Error
12
- end
13
-
14
- class RequestError < Error
15
- end
16
-
17
- class InvalidParamsError < RequestError
18
- end
19
-
20
- class RateLimitedError < RequestError
21
- end
22
-
23
- class AuthenticationError < RequestError
24
- end
25
-
26
- class ResponseError < Error
27
- end
4
+ class Error < StandardError; end
5
+ class NetworkError < Error; end
6
+ class RequestError < Error; end
7
+ class InvalidParamsError < RequestError; end
8
+ class RateLimitedError < RequestError; end
9
+ class AuthenticationError < RequestError; end
10
+ class ResponseError < Error; end
28
11
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Restiny
4
- VERSION = '5.0.0'
4
+ VERSION = '6.0.0'
5
5
  end
data/lib/restiny.rb CHANGED
@@ -3,198 +3,21 @@
3
3
  $LOAD_PATH.unshift(__dir__)
4
4
 
5
5
  require 'restiny/version'
6
- require 'restiny/constants'
7
- require 'restiny/errors'
8
- require 'restiny/manifest'
9
-
10
- require 'down'
11
- require 'faraday'
12
- require 'faraday/follow_redirects'
13
- require 'faraday/destiny/api'
14
- require 'faraday/destiny/auth'
15
- require 'json'
16
- require 'securerandom'
17
- require 'tmpdir'
18
- require 'zip'
19
-
6
+ require 'restiny/api/authentication'
7
+ require 'restiny/api/manifest'
8
+ require 'restiny/api/membership'
9
+ require 'restiny/api/profile'
10
+ require 'restiny/api/search'
11
+ require 'restiny/api/stats'
12
+
13
+ # The main Restiny module.
20
14
  module Restiny
21
15
  extend self
22
16
 
23
- BUNGIE_URL = 'https://www.bungie.net'
24
- API_BASE_URL = "#{BUNGIE_URL}/platform".freeze
25
-
26
- attr_accessor :api_key, :oauth_state, :oauth_client_id, :access_token, :user_agent
27
-
28
- # OAuth methods
29
-
30
- def get_authorise_url(redirect_url: nil, state: nil)
31
- check_oauth_client_id
32
-
33
- @oauth_state = state || SecureRandom.hex(15)
34
-
35
- params = { response_type: 'code', client_id: @oauth_client_id, state: @oauth_state }
36
- params['redirect_url'] = redirect_url unless redirect_url.nil?
37
-
38
- auth_connection.build_url("#{BUNGIE_URL}/en/oauth/authorize/", params).to_s
39
- end
40
-
41
- def request_access_token(code:, redirect_url: nil)
42
- check_oauth_client_id
43
-
44
- params = { code: code, grant_type: 'authorization_code', client_id: @oauth_client_id }
45
- params['redirect_url'] = redirect_url unless redirect_url.nil?
46
-
47
- auth_connection.post('app/oauth/token/', params).body
48
- end
49
-
50
- # Manifest methods
51
-
52
- def download_manifest(locale: 'en', force_download: false)
53
- result = api_get('Destiny2/Manifest/')
54
- raise Restiny::ResponseError, 'Unable to determine manifest details' if result.nil?
55
-
56
- live_version = result['version']
57
-
58
- @manifests ||= {}
59
- @manifest_versions ||= {}
60
-
61
- if force_download || @manifests[locale].nil? || @manifest_versions[locale] != live_version
62
- manifest_db_url = result.dig('mobileWorldContentPaths', locale)
63
- raise Restiny::RequestError, 'Unknown locale' if manifest_db_url.nil?
64
-
65
- database_file_path = Zip::File.open(Down.download(BUNGIE_URL + manifest_db_url)) do |zip_file|
66
- File.join(Dir.tmpdir, "#{live_version}.en.content.db").tap do |path|
67
- zip_file.first.extract(path) unless File.exist?(path)
68
- end
69
- end
70
-
71
- @manifests[locale] = Manifest.new(database_file_path, live_version)
72
- @manifest_versions[locale] = live_version
73
- end
74
-
75
- @manifests[locale]
76
- rescue Down::Error => e
77
- raise Restiny::NetworkError.new('Unable to download the manifest file', e.response.code)
78
- rescue Zip::Error => e
79
- raise Restiny::Error, "Unable to unzip the manifest file (#{e})"
80
- end
81
-
82
- # Profile and related methods
83
-
84
- def get_profile(membership_id:, membership_type:, components:, type_url: nil)
85
- if !components.is_a?(Array) || components.empty?
86
- raise Restiny::InvalidParamsError, 'Please provide at least one component'
87
- end
88
-
89
- url = "Destiny2/#{membership_type}/Profile/#{membership_id}/"
90
- url += type_url if type_url
91
- url += "?components=#{components.join(',')}"
92
-
93
- api_get(url)
94
- end
95
-
96
- def get_character_profile(character_id:, membership_id:, membership_type:, components:)
97
- get_profile(
98
- membership_id: membership_id,
99
- membership_type: membership_type,
100
- components: components,
101
- type_url: "Character/#{character_id}/"
102
- )
103
- end
104
-
105
- def get_instanced_item_profile(item_id:, membership_id:, membership_type:, components:)
106
- get_profile(
107
- membership_id: membership_id,
108
- membership_type: membership_type,
109
- components: components,
110
- type_url: "Item/#{item_id}/"
111
- )
112
- end
113
-
114
- # User methods.
115
-
116
- def get_user_memberships_by_id(membership_id, membership_type: Platform::ALL)
117
- raise Restiny::InvalidParamsError, 'Please provide a membership ID' if membership_id.nil?
118
-
119
- api_get("User/GetMembershipsById/#{membership_id}/#{membership_type}/")
120
- end
121
-
122
- def get_user_primary_membership(parent_membership_id, use_fallback: true)
123
- result = get_user_memberships_by_id(parent_membership_id)
124
- return nil if result.nil? || result['primaryMembershipId'].nil?
125
-
126
- result['destinyMemberships'].each do |membership|
127
- return membership if membership['membershipID'] == result['primaryMembershipId']
128
- end
129
-
130
- result['destinyMemberships'][0] if use_fallback
131
- end
132
-
133
- def search_player_by_bungie_name(name, membership_type: Platform::ALL)
134
- display_name, display_name_code = name.split('#')
135
- if display_name.nil? || display_name_code.nil?
136
- raise Restiny::InvalidParamsError, 'You must provide a valid Bungie name'
137
- end
138
-
139
- api_post(
140
- "Destiny2/SearchDestinyPlayerByBungieName/#{membership_type}/",
141
- params: {
142
- displayName: display_name,
143
- displayNameCode: display_name_code
144
- }
145
- )
146
- end
147
-
148
- def search_users_by_global_name(name, page: 0)
149
- api_post("User/Search/GlobalName/#{page}/", params: { displayNamePrefix: name })
150
- end
151
-
152
- # General request methods
153
-
154
- def api_get(url, params: {})
155
- api_connection.get(url, params, token_header).body
156
- end
157
-
158
- def api_post(url, params: {})
159
- api_connection.post(url, params, token_header).body
160
- end
161
-
162
- private
163
-
164
- def check_oauth_client_id
165
- raise Restiny::RequestError, 'You need to set an OAuth client ID' unless @oauth_client_id
166
- end
167
-
168
- def default_headers
169
- { 'User-Agent': @user_agent || "restiny v#{Restiny::VERSION}" }
170
- end
171
-
172
- def api_connection
173
- raise Restiny::InvalidParamsError, 'You need to set an API key' unless @api_key
174
-
175
- @connection ||=
176
- Faraday.new(
177
- url: API_BASE_URL,
178
- headers: default_headers.merge('X-API-KEY': @api_key)
179
- ) do |faraday|
180
- faraday.request :json
181
- faraday.response :follow_redirects
182
- faraday.response :destiny_api
183
- faraday.response :json
184
- end
185
- end
186
-
187
- def auth_connection
188
- @auth_connection ||=
189
- Faraday.new(url: API_BASE_URL, headers: default_headers) do |faraday|
190
- faraday.request :url_encoded
191
- faraday.response :follow_redirects
192
- faraday.response :destiny_auth
193
- faraday.response :json
194
- end
195
- end
196
-
197
- def token_header
198
- {}.tap { |headers| headers['authorization'] = "Bearer #{@access_token}" if @access_token }
199
- end
17
+ include Api::Authentication
18
+ include Api::Manifest
19
+ include Api::Membership
20
+ include Api::Profile
21
+ include Api::Search
22
+ include Api::Stats
200
23
  end
metadata CHANGED
@@ -1,85 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: restiny
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.0
4
+ version: 6.0.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-04 00:00:00.000000000 Z
11
+ date: 2023-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: down
14
+ name: httpx
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '5.4'
19
+ version: '1.1'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '5.4'
27
- - !ruby/object:Gem::Dependency
28
- name: faraday
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '2.0'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '2.0'
41
- - !ruby/object:Gem::Dependency
42
- name: faraday-follow_redirects
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '0.3'
48
- type: :runtime
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '0.3'
55
- - !ruby/object:Gem::Dependency
56
- name: rubyzip
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '2.3'
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '2.3'
69
- - !ruby/object:Gem::Dependency
70
- name: sqlite3
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '1.3'
76
- type: :runtime
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '1.3'
26
+ version: '1.1'
83
27
  description: A gem for interacting with Bungie's Destiny API.
84
28
  email:
85
29
  - d+restiny@waferbaby.com
@@ -87,12 +31,16 @@ executables: []
87
31
  extensions: []
88
32
  extra_rdoc_files: []
89
33
  files:
90
- - lib/faraday/destiny/api.rb
91
- - lib/faraday/destiny/auth.rb
92
34
  - lib/restiny.rb
35
+ - lib/restiny/api/authentication.rb
36
+ - lib/restiny/api/base.rb
37
+ - lib/restiny/api/manifest.rb
38
+ - lib/restiny/api/membership.rb
39
+ - lib/restiny/api/profile.rb
40
+ - lib/restiny/api/search.rb
41
+ - lib/restiny/api/stats.rb
93
42
  - lib/restiny/constants.rb
94
43
  - lib/restiny/errors.rb
95
- - lib/restiny/manifest.rb
96
44
  - lib/restiny/version.rb
97
45
  homepage: http://github.com/waferbaby/restiny
98
46
  licenses:
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'faraday'
4
- require 'restiny/errors'
5
-
6
- module Faraday
7
- module Restiny
8
- Faraday::Response.register_middleware(destiny_api: 'Faraday::Restiny::Api')
9
-
10
- class Api < Middleware
11
- def on_complete(env)
12
- return if env['response_body'].empty? || !env['response_body']['ErrorCode']
13
-
14
- if env['response_body']['ErrorCode'] == 1
15
- env[:body] = env['response_body']['Response']
16
- return
17
- end
18
-
19
- klass =
20
- case env['status']
21
- when 400..499
22
- ::Restiny::RequestError
23
- when 500..599
24
- ::Restiny::ResponseError
25
- else
26
- ::Restiny::Error
27
- end
28
-
29
- raise klass.new(env['response_body']['Message'], env['response_body']['ErrorStatus'])
30
- end
31
- end
32
- end
33
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'faraday'
4
- require 'restiny/errors'
5
-
6
- module Faraday
7
- module Restiny
8
- Faraday::Response.register_middleware(destiny_auth: 'Faraday::Restiny::Auth')
9
-
10
- class Auth < Middleware
11
- def on_complete(env)
12
- return if env['response_body'].empty? || env['url'].to_s !~ /oauth/
13
-
14
- return unless env['response_body']['error']
15
-
16
- raise ::Restiny::AuthenticationError.new(
17
- env['response_body']['error_description'],
18
- env['response_body']['error']
19
- )
20
- end
21
- end
22
- end
23
- end
@@ -1,127 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'sqlite3'
4
-
5
- module Restiny
6
- class Manifest
7
- ENTITIES = {
8
- Achievement: %w[achievement achievements],
9
- Activity: %w[activity activities],
10
- ActivityGraph: %w[activity_graph activity_graphs],
11
- ActivityMode: %w[activity_mode activity_modes],
12
- ActivityModifier: %w[activity_modifier activity_modifiers],
13
- ActivityType: %w[activity_type activity_types],
14
- Artifact: %w[artifact artifacts],
15
- Bond: %w[bonds bonds],
16
- BreakerType: %w[breaker_type breaker_types],
17
- Checklist: %w[checklist checklists],
18
- Class: %w[guardian_class guardian_classes],
19
- Collectible: %w[collectible collectibles],
20
- DamageType: %w[damage_type damage_types],
21
- Destination: %w[destination destinations],
22
- EnergyType: %w[energy_type energy_types],
23
- EquipmentSlot: %w[equipment_slot equipment_slots],
24
- EventCard: %w[event_card event_cards],
25
- Faction: %w[faction factions],
26
- Gender: %w[guardian_gender guardian_genders],
27
- GuardianRank: %w[guardian_rank guardian_ranks],
28
- GuardianRankConstants: %w[guardian_rank_constant guardian_rank_constants],
29
- HistoricalStats: %w[historical_stat historical_stats],
30
- InventoryBucket: %w[inventory_bucket inventory_buckets],
31
- InventoryItem: %w[inventory_item inventory_items],
32
- ItemCategory: %w[item_category item_categories],
33
- ItemTierType: %w[item_tier_type item_tier_types],
34
- LoadoutColor: %w[loadout_color loadout_colors],
35
- LoadoutConstants: %w[loadout_constant loadout_constants],
36
- LoadoutIcon: %w[loadout_icon loadout_icons],
37
- LoadoutName: %w[loadout_name loadout_names],
38
- Location: %w[location locations],
39
- Lore: %w[lore_entry lore_entries],
40
- MaterialRequirementSet: %w[material_requirement_set material_requirement_sets],
41
- MedalTier: %w[medal_tier medal_tiers],
42
- Metric: %w[metric metrics],
43
- Milestone: %w[milestone milestones],
44
- Objective: %w[objective objectives],
45
- Place: %w[place places],
46
- PlugSet: %w[plug_set plug_sets],
47
- PowerCap: %w[power_cap power_caps],
48
- PresentationNode: %w[presentation_node presentation_nodes],
49
- Progression: %w[progression progressions],
50
- ProgressionLevelRequirement: %w[progression_level_requirement progression_level_requirements],
51
- Race: %w[guardian_race guardian_races],
52
- Record: %w[record records],
53
- ReportReasonCategory: %w[report_reason_category report_reason_categories],
54
- RewardSource: %w[reward_source reward_sources],
55
- SackRewardItemList: %w[sack_reward_item_list sack_reward_item_lists],
56
- SandboxPattern: %w[sandbox_pattern sandbox_patterns],
57
- SandboxPerk: %w[sandbox_perk sandbox_perks],
58
- Season: %w[season seasons],
59
- SeasonPass: %w[season_pass season_passes],
60
- SocialCommendation: %w[commendation commendations],
61
- SocialCommendationNode: %w[commendation_node commendation_nodes],
62
- SocketCategory: %w[socket_category socket_categories],
63
- SocketType: %w[socket_type socket_types],
64
- Stat: %w[stat stats],
65
- StatGroup: %w[stat_group stat_groups],
66
- TalentGrid: %w[talent_grid talent_grids],
67
- Trait: %w[trait traits],
68
- Unlock: %w[unlock unlocks],
69
- Vendor: %w[vendor vendors],
70
- VendorGroup: %w[vendor_group vendor_groups]
71
- }.freeze
72
-
73
- attr_reader :version
74
-
75
- ENTITIES.each do |entity, method_names|
76
- full_table_name = "Destiny#{entity}Definition"
77
- single_method_name, plural_method_name = method_names
78
-
79
- define_method(single_method_name) { |id| fetch_item(table_name: full_table_name, id: id) }
80
- define_method(plural_method_name) { |limit: nil| fetch_items(table_name: full_table_name, limit: limit) }
81
- end
82
-
83
- def initialize(file_path, version)
84
- if file_path.empty? || !File.exist?(file_path) || !File.file?(file_path)
85
- raise Restiny::InvalidParamsError, 'You must provide a valid path for the manifest file'
86
- end
87
-
88
- @database = SQLite3::Database.new(file_path, results_as_hash: true)
89
- @version = version
90
- end
91
-
92
- private
93
-
94
- def fetch_item(table_name:, id:)
95
- query = "SELECT json FROM #{table_name} WHERE json_extract(json, '$.hash')=?"
96
- result = @database.execute(query, id)
97
-
98
- JSON.parse(result[0]['json']) unless result.nil? || result.count < 1 || !result[0].include?('json')
99
- rescue SQLite3::Exception => e
100
- raise Restiny::RequestError, "Error while fetching item (#{e})"
101
- end
102
-
103
- def fetch_items(table_name:, limit: nil)
104
- bindings = []
105
-
106
- query = "SELECT json FROM #{table_name} ORDER BY json_extract(json, '$.index')"
107
-
108
- if limit
109
- query << ' LIMIT ?'
110
- bindings << limit
111
- end
112
-
113
- items = []
114
-
115
- @database.execute(query, bindings) do |row|
116
- item = JSON.parse(row['json'])
117
- yield item if block_given?
118
-
119
- items << item
120
- end
121
-
122
- items unless block_given?
123
- rescue SQLite3::Exception => e
124
- raise Restiny::RequestError, "Error while fetching items (#{e})"
125
- end
126
- end
127
- end