steam_api_client 0.10.0 → 1.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 +4 -4
- data/CHANGELOG.md +66 -8
- data/README.md +13 -47
- data/docs/USAGE.md +326 -0
- data/lib/steam_api_client/client.rb +58 -35
- data/lib/steam_api_client/configuration.rb +55 -0
- data/lib/steam_api_client/endpoint/param.rb +83 -0
- data/lib/steam_api_client/endpoint.rb +90 -0
- data/lib/steam_api_client/endpoints.rb +122 -0
- data/lib/steam_api_client/errors/api_error.rb +10 -8
- data/lib/steam_api_client/errors/connection_error.rb +7 -0
- data/lib/steam_api_client/errors/response_parse_error.rb +6 -6
- data/lib/steam_api_client/failure_handler.rb +1 -6
- data/lib/steam_api_client/model/field.rb +42 -0
- data/lib/steam_api_client/model/scalars.rb +59 -0
- data/lib/steam_api_client/model.rb +102 -0
- data/lib/steam_api_client/models/app_details.rb +118 -0
- data/lib/steam_api_client/models/friend.rb +11 -0
- data/lib/steam_api_client/models/game.rb +35 -0
- data/lib/steam_api_client/models/global_achievement.rb +10 -0
- data/lib/steam_api_client/models/news_item.rb +20 -0
- data/lib/steam_api_client/models/player_achievements.rb +19 -0
- data/lib/steam_api_client/models/player_game_stats.rb +25 -0
- data/lib/steam_api_client/models/player_summary.rb +37 -0
- data/lib/steam_api_client/request.rb +53 -59
- data/lib/steam_api_client/requestor.rb +42 -0
- data/lib/steam_api_client/response.rb +30 -22
- data/lib/steam_api_client/version.rb +1 -1
- data/lib/steam_api_client.rb +18 -57
- metadata +32 -86
- data/lib/steam_api_client/errors/url_error.rb +0 -5
- data/lib/steam_api_client/requestors/app_details.rb +0 -27
- data/lib/steam_api_client/requestors/app_global_achievements.rb +0 -19
- data/lib/steam_api_client/requestors/app_news.rb +0 -23
- data/lib/steam_api_client/requestors/base.rb +0 -32
- data/lib/steam_api_client/requestors/game_current_players.rb +0 -19
- data/lib/steam_api_client/requestors/player_achievements.rb +0 -21
- data/lib/steam_api_client/requestors/player_friends.rb +0 -21
- data/lib/steam_api_client/requestors/player_game_stats.rb +0 -21
- data/lib/steam_api_client/requestors/player_level.rb +0 -18
- data/lib/steam_api_client/requestors/player_owned_games.rb +0 -23
- data/lib/steam_api_client/requestors/player_recent_games.rb +0 -21
- data/lib/steam_api_client/requestors/player_steamid.rb +0 -19
- data/lib/steam_api_client/requestors/player_summaries.rb +0 -19
- data/lib/steam_api_client/structs/app_details/achievement.rb +0 -12
- data/lib/steam_api_client/structs/app_details/category.rb +0 -12
- data/lib/steam_api_client/structs/app_details/details.rb +0 -82
- data/lib/steam_api_client/structs/app_details/genre.rb +0 -12
- data/lib/steam_api_client/structs/app_details/movie.rb +0 -25
- data/lib/steam_api_client/structs/app_details/package_group.rb +0 -18
- data/lib/steam_api_client/structs/app_details/requirements.rb +0 -12
- data/lib/steam_api_client/structs/app_details/screenshot.rb +0 -13
- data/lib/steam_api_client/structs/app_details/sub.rb +0 -18
- data/lib/steam_api_client/structs/app_global_achievements.rb +0 -18
- data/lib/steam_api_client/structs/app_new.rb +0 -20
- data/lib/steam_api_client/structs/app_news.rb +0 -13
- data/lib/steam_api_client/structs/base.rb +0 -15
- data/lib/steam_api_client/structs/game_current_players.rb +0 -12
- data/lib/steam_api_client/structs/player_achievements.rb +0 -23
- data/lib/steam_api_client/structs/player_friends.rb +0 -17
- data/lib/steam_api_client/structs/player_game_stats.rb +0 -28
- data/lib/steam_api_client/structs/player_level.rb +0 -11
- data/lib/steam_api_client/structs/player_owned_games.rb +0 -31
- data/lib/steam_api_client/structs/player_recent_games.rb +0 -22
- data/lib/steam_api_client/structs/player_steamid.rb +0 -16
- data/lib/steam_api_client/structs/player_summaries.rb +0 -11
- data/lib/steam_api_client/structs/player_summary.rb +0 -46
- data/lib/steam_api_client/structs/requests/abstract.rb +0 -59
- data/lib/steam_api_client/structs/requests/get_app_details.rb +0 -48
- data/lib/steam_api_client/structs/requests/get_app_global_achievements.rb +0 -48
- data/lib/steam_api_client/structs/requests/get_app_news.rb +0 -56
- data/lib/steam_api_client/structs/requests/get_game_current_players.rb +0 -48
- data/lib/steam_api_client/structs/requests/get_player_achievements.rb +0 -48
- data/lib/steam_api_client/structs/requests/get_player_friends.rb +0 -51
- data/lib/steam_api_client/structs/requests/get_player_game_stats.rb +0 -48
- data/lib/steam_api_client/structs/requests/get_player_level.rb +0 -42
- data/lib/steam_api_client/structs/requests/get_player_owned_games.rb +0 -50
- data/lib/steam_api_client/structs/requests/get_player_recent_games.rb +0 -48
- data/lib/steam_api_client/structs/requests/get_player_steamid.rb +0 -42
- data/lib/steam_api_client/structs/requests/get_player_summaries.rb +0 -46
- data/lib/steam_api_client/structs/requests/storefront_abstract.rb +0 -58
- data/lib/steam_api_client/structs/types.rb +0 -23
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module SteamApiClient
|
|
2
|
+
module Model
|
|
3
|
+
# Conversions of the scalar types
|
|
4
|
+
module Scalars
|
|
5
|
+
TYPES = %i[integer float number string boolean].freeze
|
|
6
|
+
INTEGER = /\A-?\d+\z/
|
|
7
|
+
FLOAT = /\A-?\d+(\.\d+)?\z/
|
|
8
|
+
|
|
9
|
+
def self.method_for(type)
|
|
10
|
+
raise ArgumentError, "unknown type #{type.inspect}" unless TYPES.include?(type)
|
|
11
|
+
|
|
12
|
+
:"to_#{type}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.to_integer(value)
|
|
16
|
+
return value if value.is_a?(::Integer)
|
|
17
|
+
return Integer(value, 10) if value.is_a?(::String) && value.match?(INTEGER)
|
|
18
|
+
|
|
19
|
+
invalid("an Integer", value)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.to_float(value)
|
|
23
|
+
return value.to_f if value.is_a?(::Integer) || value.is_a?(::Float)
|
|
24
|
+
return Float(value) if value.is_a?(::String) && value.match?(FLOAT)
|
|
25
|
+
|
|
26
|
+
invalid("a Float", value)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Integer or Float, as Steam sends it (converting would truncate or change stats)
|
|
30
|
+
def self.to_number(value)
|
|
31
|
+
return value if value.is_a?(::Integer) || value.is_a?(::Float)
|
|
32
|
+
return to_integer(value) if value.is_a?(::String) && value.match?(INTEGER)
|
|
33
|
+
|
|
34
|
+
to_float(value)
|
|
35
|
+
rescue Error
|
|
36
|
+
invalid("a number", value)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.to_string(value)
|
|
40
|
+
return value if value.is_a?(::String)
|
|
41
|
+
return value.to_s if value.is_a?(::Numeric)
|
|
42
|
+
|
|
43
|
+
invalid("a String", value)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Steam's flags are true/false or 1/0
|
|
47
|
+
def self.to_boolean(value)
|
|
48
|
+
return value if value == true || value == false
|
|
49
|
+
return value == 1 if value == 0 || value == 1
|
|
50
|
+
|
|
51
|
+
invalid("true or false", value)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.invalid(expected, value)
|
|
55
|
+
raise Error, "expected #{expected}, got #{value.class}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
module SteamApiClient
|
|
2
|
+
# Builds the immutable Data classes the endpoints return, from Steam's JSON:
|
|
3
|
+
#
|
|
4
|
+
# PlayerLevel = Model.define(
|
|
5
|
+
# steam_id: [:integer, from: :steamid],
|
|
6
|
+
# level: [:integer, from: %i[stats level], optional: true],
|
|
7
|
+
# tags: [[:string], default: []]
|
|
8
|
+
# )
|
|
9
|
+
# PlayerLevel.from_json({steamid: "76561197960435530", stats: {level: 9}})
|
|
10
|
+
#
|
|
11
|
+
# Each field is `name: type` or `name: [type, options]`. The type is :integer, :float,
|
|
12
|
+
# :string, :boolean, :number (an Integer or a Float, as Steam sends it), another model,
|
|
13
|
+
# or [type] for a list. Options:
|
|
14
|
+
# - from: Steam's key (Steam's names have no separators, e.g. steamid), or a path of keys
|
|
15
|
+
# - optional: true when Steam may leave the key out. The field is then nil.
|
|
16
|
+
# - default: the value used when Steam leaves the key out or sends null (implies optional)
|
|
17
|
+
#
|
|
18
|
+
# Values are converted (Steam often sends numbers as strings), nil passes through (Steam
|
|
19
|
+
# sends null for values it doesn't have), and anything else raises Model::Error.
|
|
20
|
+
module Model
|
|
21
|
+
# Raised when Steam's JSON doesn't match a model. The message names the field
|
|
22
|
+
# but never the value, which could be personal data.
|
|
23
|
+
class Error < StandardError; end
|
|
24
|
+
|
|
25
|
+
# @param [Hash{Symbol => Object}] fields name => type, or name => [type, options]
|
|
26
|
+
# @yield the class body, like Data.define's block
|
|
27
|
+
# @return [Class] a Data class that also responds to .from_json
|
|
28
|
+
def self.define(**fields, &block)
|
|
29
|
+
fields = fields.to_h do |name, spec|
|
|
30
|
+
type, options = spec_parts(spec)
|
|
31
|
+
[name, Field.new(name, type, **options)]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Data.define(*fields.keys, &block).tap do |klass|
|
|
35
|
+
klass.instance_variable_set(:@fields, fields.freeze)
|
|
36
|
+
klass.extend(ClassMethods)
|
|
37
|
+
klass.include(InstanceMethods)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# `[type, {options}]`, or the bare type
|
|
42
|
+
def self.spec_parts(spec)
|
|
43
|
+
type, options = spec
|
|
44
|
+
(spec.is_a?(Array) && options.is_a?(Hash)) ? [type, options] : [spec, {}]
|
|
45
|
+
end
|
|
46
|
+
private_class_method :spec_parts
|
|
47
|
+
|
|
48
|
+
# Converts one JSON value to the given type
|
|
49
|
+
# @raise [Model::Error] if the value doesn't convert
|
|
50
|
+
def self.cast(type, value)
|
|
51
|
+
return if value.nil?
|
|
52
|
+
|
|
53
|
+
case type
|
|
54
|
+
when Array then cast_list(type.first, value)
|
|
55
|
+
when Class then cast_model(type, value)
|
|
56
|
+
else Scalars.public_send(Scalars.method_for(type), value)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.cast_list(type, value)
|
|
61
|
+
raise Error, "expected a list, got #{value.class}" unless value.is_a?(Array)
|
|
62
|
+
|
|
63
|
+
value.map { |item| cast(type, item) }
|
|
64
|
+
end
|
|
65
|
+
private_class_method :cast_list
|
|
66
|
+
|
|
67
|
+
# Steam sends an empty array instead of an object when there's nothing
|
|
68
|
+
# (e.g. an app's mac_requirements)
|
|
69
|
+
def self.cast_model(model, value)
|
|
70
|
+
(value == []) ? nil : model.from_json(value)
|
|
71
|
+
end
|
|
72
|
+
private_class_method :cast_model
|
|
73
|
+
|
|
74
|
+
# Turns models (and lists of them) into plain hashes
|
|
75
|
+
def self.to_plain(value)
|
|
76
|
+
case value
|
|
77
|
+
when Data then value.to_h
|
|
78
|
+
when Array then value.map { |item| to_plain(item) }
|
|
79
|
+
else value
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Added to every model class
|
|
84
|
+
module ClassMethods
|
|
85
|
+
# Builds the model from Steam's JSON (a Hash with symbol keys)
|
|
86
|
+
# @raise [Model::Error] if the JSON doesn't match the model
|
|
87
|
+
def from_json(json)
|
|
88
|
+
raise Error, "expected an object, got #{json.class}" unless json.is_a?(Hash)
|
|
89
|
+
|
|
90
|
+
new(**@fields.transform_values { |field| field.read(json) })
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Added to every model
|
|
95
|
+
module InstanceMethods
|
|
96
|
+
# @return [Hash] the fields, with nested models as hashes too
|
|
97
|
+
def to_h
|
|
98
|
+
super.transform_values { |value| Model.to_plain(value) }
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
module SteamApiClient
|
|
2
|
+
# The parts of AppDetails, built first, then named under it below
|
|
3
|
+
# (e.g. AppDetails::Category)
|
|
4
|
+
parts = {
|
|
5
|
+
Requirements: Model.define(
|
|
6
|
+
# HTML
|
|
7
|
+
minimum: [:string, optional: true],
|
|
8
|
+
recommended: [:string, optional: true]
|
|
9
|
+
),
|
|
10
|
+
Price: Model.define(
|
|
11
|
+
currency: :string,
|
|
12
|
+
# In cents
|
|
13
|
+
initial: :integer,
|
|
14
|
+
final: :integer,
|
|
15
|
+
discount_percent: :integer,
|
|
16
|
+
initial_formatted: :string,
|
|
17
|
+
final_formatted: :string
|
|
18
|
+
),
|
|
19
|
+
Package: Model.define(
|
|
20
|
+
package_id: [:integer, from: :packageid],
|
|
21
|
+
percent_savings_text: :string,
|
|
22
|
+
percent_savings: :integer,
|
|
23
|
+
option_text: :string,
|
|
24
|
+
option_description: :string,
|
|
25
|
+
can_get_free_license: :string,
|
|
26
|
+
is_free_license: :boolean,
|
|
27
|
+
price_in_cents_with_discount: :integer
|
|
28
|
+
),
|
|
29
|
+
Platforms: Model.define(windows: :boolean, mac: :boolean, linux: :boolean),
|
|
30
|
+
Metacritic: Model.define(score: :integer, url: :string),
|
|
31
|
+
Category: Model.define(id: :integer, description: :string),
|
|
32
|
+
Genre: Model.define(id: :integer, description: :string),
|
|
33
|
+
Screenshot: Model.define(id: :integer, path_thumbnail: :string, path_full: :string),
|
|
34
|
+
Movie: Model.define(
|
|
35
|
+
id: :integer,
|
|
36
|
+
name: :string,
|
|
37
|
+
thumbnail: :string,
|
|
38
|
+
# Streaming manifests (MPEG-DASH and HLS)
|
|
39
|
+
dash_av1: [:string, optional: true],
|
|
40
|
+
dash_h264: [:string, optional: true],
|
|
41
|
+
hls_h264: [:string, optional: true],
|
|
42
|
+
highlight: :boolean
|
|
43
|
+
),
|
|
44
|
+
HighlightedAchievement: Model.define(name: :string, path: :string),
|
|
45
|
+
ReleaseDate: Model.define(coming_soon: :boolean, date: :string),
|
|
46
|
+
SupportInfo: Model.define(url: :string, email: :string),
|
|
47
|
+
ContentDescriptors: Model.define(ids: [[:integer], default: []], notes: [:string, optional: true])
|
|
48
|
+
}
|
|
49
|
+
parts[:PackageGroup] = Model.define(
|
|
50
|
+
name: :string,
|
|
51
|
+
title: :string,
|
|
52
|
+
description: :string,
|
|
53
|
+
selection_text: :string,
|
|
54
|
+
save_text: :string,
|
|
55
|
+
display_type: :integer,
|
|
56
|
+
is_recurring_subscription: :string,
|
|
57
|
+
subs: [[parts[:Package]], default: []]
|
|
58
|
+
)
|
|
59
|
+
parts[:Achievements] = Model.define(
|
|
60
|
+
total: :integer,
|
|
61
|
+
highlighted: [[parts[:HighlightedAchievement]], default: []]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# An app's store page, from `app_details`. Every field is nil when Steam leaves
|
|
65
|
+
# it out (e.g. when it isn't in the requested filters).
|
|
66
|
+
# @see https://wiki.teamfortress.com/wiki/User:RJackson/StorefrontAPI#appdetails
|
|
67
|
+
AppDetails = Model.define(
|
|
68
|
+
app_id: [:integer, from: :steam_appid, optional: true],
|
|
69
|
+
# "game", "dlc", "demo", "music"...
|
|
70
|
+
type: [:string, optional: true],
|
|
71
|
+
name: [:string, optional: true],
|
|
72
|
+
required_age: [:integer, optional: true],
|
|
73
|
+
is_free: [:boolean, optional: true],
|
|
74
|
+
dlc: [[:integer], optional: true],
|
|
75
|
+
# HTML
|
|
76
|
+
detailed_description: [:string, optional: true],
|
|
77
|
+
about_the_game: [:string, optional: true],
|
|
78
|
+
short_description: [:string, optional: true],
|
|
79
|
+
supported_languages: [:string, optional: true],
|
|
80
|
+
header_image: [:string, optional: true],
|
|
81
|
+
website: [:string, optional: true],
|
|
82
|
+
pc_requirements: [parts[:Requirements], optional: true],
|
|
83
|
+
mac_requirements: [parts[:Requirements], optional: true],
|
|
84
|
+
linux_requirements: [parts[:Requirements], optional: true],
|
|
85
|
+
developers: [[:string], optional: true],
|
|
86
|
+
publishers: [[:string], optional: true],
|
|
87
|
+
# Not sent for free apps
|
|
88
|
+
price_overview: [parts[:Price], optional: true],
|
|
89
|
+
packages: [[:integer], optional: true],
|
|
90
|
+
package_groups: [[parts[:PackageGroup]], optional: true],
|
|
91
|
+
platforms: [parts[:Platforms], optional: true],
|
|
92
|
+
metacritic: [parts[:Metacritic], optional: true],
|
|
93
|
+
categories: [[parts[:Category]], optional: true],
|
|
94
|
+
genres: [[parts[:Genre]], optional: true],
|
|
95
|
+
screenshots: [[parts[:Screenshot]], optional: true],
|
|
96
|
+
movies: [[parts[:Movie]], optional: true],
|
|
97
|
+
# Number of user reviews
|
|
98
|
+
recommendations: [:integer, from: %i[recommendations total], optional: true],
|
|
99
|
+
achievements: [parts[:Achievements], optional: true],
|
|
100
|
+
release_date: [parts[:ReleaseDate], optional: true],
|
|
101
|
+
support_info: [parts[:SupportInfo], optional: true],
|
|
102
|
+
background: [:string, optional: true],
|
|
103
|
+
background_raw: [:string, optional: true],
|
|
104
|
+
content_descriptors: [parts[:ContentDescriptors], optional: true]
|
|
105
|
+
) do
|
|
106
|
+
# Steam keys the response by app ID: {"570": {"success": true, "data": {...}}}.
|
|
107
|
+
# Returns the data, or nil when the app is unavailable (success: false).
|
|
108
|
+
# @param [Hash] json
|
|
109
|
+
# @return [Hash, nil]
|
|
110
|
+
def self.unwrap(json)
|
|
111
|
+
app_id, entry = json.first
|
|
112
|
+
return unless entry.is_a?(Hash) && entry[:success]
|
|
113
|
+
|
|
114
|
+
{steam_appid: app_id.to_s}.merge(entry.fetch(:data, {}))
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
parts.each { |name, model| AppDetails.const_set(name, model) }
|
|
118
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module SteamApiClient
|
|
2
|
+
# A friend in a player's friend list, from `player_friends`
|
|
3
|
+
# @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetFriendList_.28v0001.29
|
|
4
|
+
Friend = Model.define(
|
|
5
|
+
steam_id: [:integer, from: :steamid],
|
|
6
|
+
# "friend"
|
|
7
|
+
relationship: :string,
|
|
8
|
+
# When they became friends, in unix time
|
|
9
|
+
friend_since: [:integer, optional: true]
|
|
10
|
+
)
|
|
11
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module SteamApiClient
|
|
2
|
+
# A game in a player's library, from `player_owned_games` and `player_recent_games`.
|
|
3
|
+
# Playtimes are in minutes.
|
|
4
|
+
# @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetOwnedGames_.28v0001.29
|
|
5
|
+
Game = Model.define(
|
|
6
|
+
app_id: [:integer, from: :appid],
|
|
7
|
+
# Name and icon: only with include_appinfo: true (always sent for recent games)
|
|
8
|
+
name: [:string, optional: true],
|
|
9
|
+
img_icon_url: [:string, optional: true],
|
|
10
|
+
playtime_forever: :integer,
|
|
11
|
+
# Only sent when played in the last two weeks
|
|
12
|
+
playtime_2weeks: [:integer, optional: true],
|
|
13
|
+
has_community_visible_stats: [:boolean, optional: true],
|
|
14
|
+
has_leaderboards: [:boolean, optional: true],
|
|
15
|
+
content_descriptor_ids: [[:integer], from: :content_descriptorids, optional: true],
|
|
16
|
+
# Steam doesn't send these for most libraries anymore
|
|
17
|
+
playtime_windows_forever: [:integer, optional: true],
|
|
18
|
+
playtime_mac_forever: [:integer, optional: true],
|
|
19
|
+
playtime_linux_forever: [:integer, optional: true],
|
|
20
|
+
# Last time played, in unix time
|
|
21
|
+
rtime_last_played: [:integer, optional: true]
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# A player's library, from `player_owned_games`
|
|
25
|
+
OwnedGames = Model.define(
|
|
26
|
+
game_count: [:integer, optional: true],
|
|
27
|
+
games: [[Game], default: []]
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# The games a player played in the last two weeks, from `player_recent_games`
|
|
31
|
+
RecentGames = Model.define(
|
|
32
|
+
total_count: [:integer, optional: true],
|
|
33
|
+
games: [[Game], default: []]
|
|
34
|
+
)
|
|
35
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module SteamApiClient
|
|
2
|
+
# The share of players who unlocked an achievement, from `app_global_achievements`
|
|
3
|
+
# @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetGlobalAchievementPercentagesForApp_.28v0002.29
|
|
4
|
+
GlobalAchievement = Model.define(
|
|
5
|
+
# The achievement's API name
|
|
6
|
+
name: :string,
|
|
7
|
+
# Percentage of players, e.g. 49.6
|
|
8
|
+
percent: :float
|
|
9
|
+
)
|
|
10
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module SteamApiClient
|
|
2
|
+
# A news item of an app, from `app_news`
|
|
3
|
+
# @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetNewsForApp_.28v0002.29
|
|
4
|
+
NewsItem = Model.define(
|
|
5
|
+
gid: :string,
|
|
6
|
+
title: :string,
|
|
7
|
+
url: :string,
|
|
8
|
+
is_external_url: :boolean,
|
|
9
|
+
author: :string,
|
|
10
|
+
# Shortened to the requested maxlength
|
|
11
|
+
contents: :string,
|
|
12
|
+
feed_label: [:string, from: :feedlabel],
|
|
13
|
+
# Publication date, in unix time
|
|
14
|
+
date: :integer,
|
|
15
|
+
feed_name: [:string, from: :feedname],
|
|
16
|
+
feed_type: :integer,
|
|
17
|
+
app_id: [:integer, from: :appid],
|
|
18
|
+
tags: [[:string], optional: true]
|
|
19
|
+
)
|
|
20
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module SteamApiClient
|
|
2
|
+
# Built first, then named PlayerAchievements::Achievement below
|
|
3
|
+
achievement = Model.define(
|
|
4
|
+
# The achievement's API name, e.g. "TF_PLAY_GAME_EVERYCLASS"
|
|
5
|
+
api_name: [:string, from: :apiname],
|
|
6
|
+
achieved: :boolean,
|
|
7
|
+
# When it was unlocked, in unix time (0 when locked, or unknown)
|
|
8
|
+
unlock_time: [:integer, from: :unlocktime, optional: true]
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# A player's achievements in a game, from `player_achievements`
|
|
12
|
+
# @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetPlayerAchievements_.28v0001.29
|
|
13
|
+
PlayerAchievements = Model.define(
|
|
14
|
+
steam_id: [:integer, from: :steamID, optional: true],
|
|
15
|
+
game_name: [:string, from: :gameName, optional: true],
|
|
16
|
+
achievements: [[achievement], default: []]
|
|
17
|
+
)
|
|
18
|
+
PlayerAchievements.const_set(:Achievement, achievement)
|
|
19
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module SteamApiClient
|
|
2
|
+
# Built first, then named under PlayerGameStats below
|
|
3
|
+
stat = Model.define(
|
|
4
|
+
# A game-defined stat, e.g. "Scout.accum.iDamageDealt"
|
|
5
|
+
name: :string,
|
|
6
|
+
# Integer or Float, as Steam sends it
|
|
7
|
+
value: :number
|
|
8
|
+
)
|
|
9
|
+
achievement = Model.define(
|
|
10
|
+
# The achievement's API name, e.g. "TF_PLAY_GAME_EVERYCLASS"
|
|
11
|
+
name: :string,
|
|
12
|
+
achieved: :boolean
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# A player's stats and unlocked achievements in a game, from `player_game_stats`
|
|
16
|
+
# @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetUserStatsForGame_.28v0002.29
|
|
17
|
+
PlayerGameStats = Model.define(
|
|
18
|
+
steam_id: [:integer, from: :steamID, optional: true],
|
|
19
|
+
game_name: [:string, from: :gameName, optional: true],
|
|
20
|
+
stats: [[stat], default: []],
|
|
21
|
+
achievements: [[achievement], default: []]
|
|
22
|
+
)
|
|
23
|
+
PlayerGameStats.const_set(:Stat, stat)
|
|
24
|
+
PlayerGameStats.const_set(:Achievement, achievement)
|
|
25
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module SteamApiClient
|
|
2
|
+
# A player's public profile, from `player_summaries`. The optional fields are
|
|
3
|
+
# nil when the profile (or that part of it) isn't public.
|
|
4
|
+
# @see https://developer.valvesoftware.com/wiki/Steam_Web_API#GetPlayerSummaries_.28v0002.29
|
|
5
|
+
PlayerSummary = Model.define(
|
|
6
|
+
# 64 bit SteamID
|
|
7
|
+
steam_id: [:integer, from: :steamid],
|
|
8
|
+
# Display name
|
|
9
|
+
persona_name: [:string, from: :personaname],
|
|
10
|
+
profile_url: [:string, from: :profileurl],
|
|
11
|
+
# 32x32, 64x64 and 184x184 avatar URLs (Steam's "?" avatar when none is set)
|
|
12
|
+
avatar: :string,
|
|
13
|
+
avatar_medium: [:string, from: :avatarmedium],
|
|
14
|
+
avatar_full: [:string, from: :avatarfull],
|
|
15
|
+
# Hash used in the avatar URLs
|
|
16
|
+
avatar_hash: [:string, from: :avatarhash, optional: true],
|
|
17
|
+
# 0 offline, 1 online, 2 busy, 3 away, 4 snooze, 5 looking to trade, 6 looking to play.
|
|
18
|
+
# Always 0 for private profiles.
|
|
19
|
+
persona_state: [:integer, from: :personastate],
|
|
20
|
+
persona_state_flags: [:integer, from: :personastateflags, optional: true],
|
|
21
|
+
# 1 private (not visible to you), 3 public
|
|
22
|
+
community_visibility_state: [:integer, from: :communityvisibilitystate],
|
|
23
|
+
# 1 when the player has set up a community profile
|
|
24
|
+
profile_state: [:integer, from: :profilestate, optional: true],
|
|
25
|
+
# Last time online, in unix time
|
|
26
|
+
last_logoff: [:integer, from: :lastlogoff, optional: true],
|
|
27
|
+
# 1 when the profile allows public comments
|
|
28
|
+
comment_permission: [:integer, from: :commentpermission, optional: true],
|
|
29
|
+
real_name: [:string, from: :realname, optional: true],
|
|
30
|
+
# Account creation, in unix time
|
|
31
|
+
time_created: [:integer, from: :timecreated, optional: true],
|
|
32
|
+
# ISO 3166 country code
|
|
33
|
+
country_code: [:string, from: :loccountrycode, optional: true],
|
|
34
|
+
# The player's primary group (a group SteamID)
|
|
35
|
+
primary_clan_id: [:integer, from: :primaryclanid, optional: true]
|
|
36
|
+
)
|
|
37
|
+
end
|
|
@@ -1,93 +1,87 @@
|
|
|
1
1
|
module SteamApiClient
|
|
2
|
+
# One call to an endpoint: builds the URL from the arguments and sends it
|
|
3
|
+
# through the client's Faraday connection
|
|
2
4
|
class Request
|
|
3
|
-
|
|
5
|
+
# The API token in a request URL's query string
|
|
6
|
+
API_KEY_PARAM = /(?<=[?&]key=)[^&]*/
|
|
7
|
+
|
|
8
|
+
# @param [SteamApiClient::Endpoint] endpoint
|
|
9
|
+
# @param [Hash] params the keyword arguments given to #retrieve
|
|
10
|
+
# @param [SteamApiClient::Client] client
|
|
11
|
+
# @raise [ArgumentError] on missing, unknown or invalid params
|
|
12
|
+
# @raise [SteamApiClient::Errors::MissingApiToken] if the endpoint needs a token and the client has none
|
|
13
|
+
def initialize(endpoint:, params:, client:)
|
|
4
14
|
@failure_handler = SteamApiClient::FailureHandler.instance
|
|
5
|
-
@
|
|
6
|
-
@response_struct = response_struct
|
|
15
|
+
@endpoint = endpoint
|
|
7
16
|
@client = client
|
|
8
17
|
|
|
18
|
+
query = endpoint.query(params)
|
|
9
19
|
ensure_api_token!
|
|
10
|
-
|
|
20
|
+
@url = build_url(query)
|
|
11
21
|
end
|
|
12
22
|
|
|
13
23
|
def run
|
|
14
24
|
freeze
|
|
15
25
|
|
|
16
|
-
raw_response =
|
|
26
|
+
raw_response = perform
|
|
17
27
|
|
|
18
|
-
Response.new(raw_response: raw_response,
|
|
28
|
+
Response.new(raw_response: raw_response, endpoint: endpoint, request: self).tap do |response|
|
|
19
29
|
failure_handler.call(response)
|
|
20
30
|
end
|
|
21
31
|
end
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def ensure_api_token!
|
|
28
|
-
return unless request_struct.needs_authorization?
|
|
29
|
-
return unless client.api_token.to_s.empty?
|
|
30
|
-
|
|
31
|
-
raise SteamApiClient::Errors::MissingApiToken
|
|
33
|
+
# Shows the method and the URL, with the API token redacted
|
|
34
|
+
# @return [String]
|
|
35
|
+
def inspect
|
|
36
|
+
"#<#{self.class.name} GET #{url.sub(API_KEY_PARAM, "[REDACTED]")}>"
|
|
32
37
|
end
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
url = nil
|
|
39
|
+
private
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
when :base_endpoint then url = build_base_url
|
|
39
|
-
when :storefront_endpoint then url = build_storefront_url
|
|
40
|
-
else raise SteamApiClient::Errors::EndpointKindNotSupported
|
|
41
|
-
end
|
|
41
|
+
attr_reader :endpoint, :client, :url, :failure_handler
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
# Sends the request through the client's Faraday connection, turning
|
|
44
|
+
# Faraday's network errors into the gem's errors (the original is #cause).
|
|
45
|
+
# Every Steam endpoint is a GET, with its params in the query string.
|
|
46
|
+
def perform
|
|
47
|
+
client.connection.run_request(:get, url, nil, nil) do |request|
|
|
48
|
+
request_options.each { |option, value| request.options.public_send(:"#{option}=", value) }
|
|
49
|
+
end
|
|
50
|
+
rescue ::Faraday::TimeoutError
|
|
51
|
+
raise_timeout
|
|
52
|
+
rescue ::Faraday::ConnectionFailed => error
|
|
53
|
+
# Faraday reports a connect timeout (Net::OpenTimeout) as ConnectionFailed
|
|
54
|
+
raise_timeout if error.wrapped_exception.is_a?(::Timeout::Error)
|
|
55
|
+
raise SteamApiClient::Errors::ConnectionError, "Could not connect to Steam: #{error.message}"
|
|
56
|
+
rescue ::Faraday::SSLError => error
|
|
57
|
+
raise SteamApiClient::Errors::ConnectionError, "Could not connect to Steam: #{error.message}"
|
|
44
58
|
end
|
|
45
59
|
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
client.
|
|
49
|
-
request_struct.interface,
|
|
50
|
-
request_struct.path,
|
|
51
|
-
request_struct.version
|
|
60
|
+
def raise_timeout
|
|
61
|
+
raise SteamApiClient::Errors::HttpTimeout.new(
|
|
62
|
+
nil, "The request to Steam timed out (timeout: #{client.timeout}s, connect_timeout: #{client.connect_timeout}s)"
|
|
52
63
|
)
|
|
53
|
-
url.query = query_params
|
|
54
|
-
|
|
55
|
-
url
|
|
56
64
|
end
|
|
57
65
|
|
|
58
|
-
def
|
|
59
|
-
|
|
60
|
-
client.storefront_endpoint,
|
|
61
|
-
request_struct.path
|
|
62
|
-
)
|
|
63
|
-
url.query = query_params
|
|
64
|
-
|
|
65
|
-
url
|
|
66
|
+
def request_options
|
|
67
|
+
{timeout: client.timeout, open_timeout: client.connect_timeout}
|
|
66
68
|
end
|
|
67
69
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
params.merge!(request_struct.query_params)
|
|
72
|
-
end
|
|
70
|
+
def ensure_api_token!
|
|
71
|
+
return unless endpoint.key?
|
|
72
|
+
return unless client.api_token.to_s.empty?
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
raise SteamApiClient::Errors::MissingApiToken
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
followlocation: true,
|
|
82
|
-
timeout_ms: (client.timeout * 1000).to_i,
|
|
83
|
-
connecttimeout_ms: (client.connect_timeout * 1000).to_i,
|
|
84
|
-
# Required for sub-second timeouts and safe use of timeouts across threads
|
|
85
|
-
nosignal: true
|
|
86
|
-
}
|
|
87
|
-
# GET requests (every Steam endpoint so far) carry their params in the query string
|
|
88
|
-
return options if request_struct.method == :get
|
|
77
|
+
# The API key goes first, then the endpoint's params
|
|
78
|
+
def build_url(query)
|
|
79
|
+
query = {key: client.api_token}.merge(query) if endpoint.key?
|
|
80
|
+
base = (endpoint.api == :store) ? client.storefront_endpoint : client.base_endpoint
|
|
89
81
|
|
|
90
|
-
|
|
82
|
+
url = URI.join(base, endpoint.path)
|
|
83
|
+
url.query = URI.encode_www_form(query)
|
|
84
|
+
url.to_s
|
|
91
85
|
end
|
|
92
86
|
end
|
|
93
87
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module SteamApiClient
|
|
2
|
+
# What the client exposes for each endpoint, e.g. `client.player_level`
|
|
3
|
+
class Requestor
|
|
4
|
+
attr_reader :endpoint
|
|
5
|
+
|
|
6
|
+
# @param [SteamApiClient::Client] client
|
|
7
|
+
# @param [SteamApiClient::Endpoint] endpoint
|
|
8
|
+
def initialize(client, endpoint)
|
|
9
|
+
@client = client
|
|
10
|
+
@endpoint = endpoint
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Calls the endpoint. The keyword arguments are the endpoint's params
|
|
14
|
+
# (see SteamApiClient::Endpoints).
|
|
15
|
+
# @return [Data, Array, Integer, nil] the endpoint's result
|
|
16
|
+
# @raise [ArgumentError] on missing, unknown or invalid arguments
|
|
17
|
+
def retrieve(**params)
|
|
18
|
+
run(params).result
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Calls the endpoint and returns Steam's JSON untouched (keys symbolized), e.g.
|
|
22
|
+
# for fields the result doesn't have
|
|
23
|
+
# @return [Hash]
|
|
24
|
+
def retrieve_raw(**params)
|
|
25
|
+
run(params).json
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Keeps the client (and its token) out of the output
|
|
29
|
+
# @return [String]
|
|
30
|
+
def inspect
|
|
31
|
+
"#<#{self.class.name} #{endpoint.name} #{endpoint.path}>"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
attr_reader :client
|
|
37
|
+
|
|
38
|
+
def run(params)
|
|
39
|
+
Request.new(endpoint: endpoint, params: params, client: client).run
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|