restiny 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []