restiny 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e2ff82944ba14279df69b4c5e8d151a4d0b18c5176273af3ed68142327473993
4
+ data.tar.gz: a214d550713959f482943f2120d491a91227f46599ab5f5da46e55abed7174be
5
+ SHA512:
6
+ metadata.gz: 269e932d167058936ac4ccc092493b72da71026dbf5947467367671e2a64421496f0f9d40b99962eb1f1dc416754734ed241dfaaee53440ea03bee610efdb2d2
7
+ data.tar.gz: def65d2d7fc397ca3e833a6eec1b922d78639d7c800f763f4e15740a9bde08d8e2e36e5db020dd9654594575c11b1f1c078d3d57f0b9f1eb1bbaf2197e70729a
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Restiny
4
+ class Character
5
+ attr_accessor :id, :playtime, :light_level, :stats, :emblem, :progression
6
+
7
+ def initialize(id:, session_playtime:, total_playtime:, light_level:, stats:, emblem:, progression:)
8
+ @id = id
9
+ @playtime = { session: session_playtime, total: total_playtime }
10
+ @light_level = light_level
11
+ @stats = stats
12
+ @emblem = emblem
13
+ @progression = progression
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,134 @@
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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ PLATFORM_ALL = -1
4
+ PLATFORM_XBOX = 1
5
+ PLATFORM_PSN = 2
6
+ PLATFORM_STEAM = 3
7
+ PLATFORM_EPIC = 6
8
+
9
+ COMPONENT_TYPE_PROFILES = 'Profiles'.freeze
10
+ COMPONENT_TYPE_PROFILE_INVENTORIES = 'ProfileInventories'.freeze
11
+ COMPONENT_TYPE_CHARACTERS = 'Characters'.freeze
12
+ COMPONENT_TYPE_CHARACTER_INVENTORIES = 'CharacterInventories'.freeze
@@ -0,0 +1,139 @@
1
+ # frozen_string/literal: true
2
+
3
+ require 'down'
4
+ require 'json'
5
+ require 'sqlite3'
6
+ require 'zip'
7
+
8
+ module Restiny
9
+ class Manifest
10
+ 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' }
67
+ }
68
+
69
+ attr_reader :file_path
70
+
71
+ TABLES.each do |table_name, method_names|
72
+ define_method method_names[:item] do |id|
73
+ query_table(table_name, id: id)[0]
74
+ end
75
+
76
+ define_method method_names[:items] do |limit: nil|
77
+ query_table(table_name, limit: limit)
78
+ end
79
+ end
80
+
81
+ def self.download(url)
82
+ zipped_file = Down.download(url)
83
+ manifest_path = zipped_file.path + ".db"
84
+
85
+ Zip::File.open(zipped_file) { |file| file.first.extract(manifest_path) }
86
+
87
+ 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"
92
+ end
93
+
94
+ def initialize(file_path)
95
+ raise "You must provide the file path for the manifest file" if file_path.empty?
96
+
97
+ @database = SQLite3::Database.new(file_path, results_as_hash: true)
98
+ @file_path = file_path
99
+ end
100
+
101
+ private
102
+
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
135
+ rescue SQLite3::Exception => e
136
+ raise "Error while querying the manifest (#{e})"
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Restiny
4
+ class Membership
5
+ attr_reader :id, :type, :cross_save_override, :icon_path, :is_public, :types
6
+
7
+ def self.platform(type)
8
+ case type
9
+ when PLATFORM_XBOX
10
+ :xbox
11
+ when PLATFORM_PSN
12
+ :playstation
13
+ when PLATFORM_STEAM
14
+ :steam
15
+ when PLATFORM_EPIC
16
+ :epic
17
+ end
18
+ end
19
+
20
+ def initialize(id:, type:, cross_save_override:, icon_path:, is_public:, types:)
21
+ @id = id
22
+ @type = type
23
+ @cross_save_override = cross_save_override
24
+ @icon_path = icon_path
25
+ @is_public = is_public
26
+ @types = types
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Restiny
4
+ class User
5
+ attr_reader :display_name, :display_name_code, :memberships
6
+
7
+ def initialize(display_name:, display_name_code:, memberships:)
8
+ @display_name = display_name
9
+ @display_name_code = display_name_code
10
+
11
+ self.memberships = memberships
12
+ end
13
+
14
+ def memberships=(raw_memberships)
15
+ @memberships = {}
16
+
17
+ raw_memberships.each do |ship|
18
+ platform = Restiny::Membership.platform(ship['membershipType'])
19
+
20
+ @memberships[platform] = Restiny::Membership.new(
21
+ id: ship['membershipId'],
22
+ type: ship['membershipType'],
23
+ cross_save_override: ship['crossSaveOverride'],
24
+ icon_path: ship['iconPath'],
25
+ is_public: ship['isPublic'],
26
+ types: ship['applicableMembershipTypes']
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Restiny
4
+ VERSION = '0.1.0'
5
+ end
data/lib/restiny.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(__dir__)
4
+
5
+ require 'restiny/version'
6
+ require 'restiny/constants'
7
+ require 'restiny/client'
8
+ require 'restiny/manifest'
9
+ require 'restiny/user'
10
+ require 'restiny/membership'
11
+ require 'restiny/character'
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: restiny
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Bogan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-02-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday-follow_redirects
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sqlite3
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: down
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.4'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubyzip
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.3'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 12.3.3
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 12.3.3
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.10'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.10'
111
+ description: A gem for interacting with Bungie's Destiny API.
112
+ email:
113
+ - d+restiny@waferbaby.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - lib/restiny.rb
119
+ - lib/restiny/character.rb
120
+ - lib/restiny/client.rb
121
+ - lib/restiny/constants.rb
122
+ - lib/restiny/manifest.rb
123
+ - lib/restiny/membership.rb
124
+ - lib/restiny/user.rb
125
+ - lib/restiny/version.rb
126
+ homepage: http://github.com/waferbaby/restiny
127
+ licenses:
128
+ - MIT
129
+ metadata: {}
130
+ post_install_message:
131
+ rdoc_options: []
132
+ require_paths:
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ required_rubygems_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ requirements: []
145
+ rubygems_version: 3.1.6
146
+ signing_key:
147
+ specification_version: 4
148
+ summary: A Destiny API gem
149
+ test_files: []