psn-client 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 andshrew
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'oauth2'
6
+ require 'json'
7
+
8
+ module PSN
9
+ module Client
10
+ # Handles acquiring PSN access tokens by exchanging an NPSSO cookie
11
+ # for an authorization code and then exchanging that code for a token.
12
+ class Auth
13
+ AUTH_URL = 'https://ca.account.sony.com/api/authz/v3/'
14
+ BASIC_TOKEN = ENV.fetch('PSN_BASIC_TOKEN', nil)
15
+
16
+ class << self
17
+ def authenticate
18
+ new.authenticate
19
+ end
20
+ end
21
+
22
+ def authenticate
23
+ PSN.logger.info('Getting new PSN access token')
24
+
25
+ code = fetch_auth_code
26
+ PSN.logger.debug { "Fetched authorisation code: #{code}" }
27
+
28
+ access_token = fetch_token(code)
29
+ PSN.logger.debug { "Fetched access token which expires in: #{access_token.expires_in / 60} minutes" }
30
+ PSN.logger.info('Acquired new PSN access token')
31
+
32
+ access_token.token
33
+ end
34
+
35
+ private
36
+
37
+ def fetch_auth_code
38
+ uri = URI(auth_url)
39
+
40
+ response = http_for(uri).request(request_for(uri))
41
+ if response['location'].nil?
42
+ message = extract_error_message(response)
43
+ PSN.logger.error(message)
44
+ raise message
45
+ end
46
+
47
+ parse_code(response['location'])
48
+ end
49
+
50
+ def http_for(uri)
51
+ http = Net::HTTP.new(uri.host, uri.port)
52
+ http.use_ssl = uri.scheme == 'https'
53
+ http
54
+ end
55
+
56
+ def request_for(uri)
57
+ request = Net::HTTP::Get.new(uri)
58
+ request['Cookie'] = npsso_cookie
59
+ request
60
+ end
61
+
62
+ def fetch_token(code)
63
+ client.auth_code.get_token(code,
64
+ redirect_uri: 'com.scee.psxandroid.scecompcall://redirect',
65
+ token_format: 'jwt',
66
+ headers: { Authorization: "Basic #{BASIC_TOKEN}" })
67
+ end
68
+
69
+ def parse_code(location)
70
+ params = URI.decode_www_form(URI(location).query.to_s).to_h
71
+ code = params['code']
72
+
73
+ return code if code
74
+
75
+ raise_auth_error(params)
76
+ end
77
+
78
+ def raise_auth_error(params)
79
+ message = get_error_message(params['error'], params['error_description'])
80
+ PSN.logger.error(message)
81
+ raise message
82
+ end
83
+
84
+ def extract_error_message(response)
85
+ error_description = JSON.parse(response.body).fetch('error_description', nil)
86
+
87
+ error_description || 'Failed to fetch PSN auth code, location header missing'
88
+ rescue JSON::ParserError, NoMethodError, TypeError
89
+ 'Failed to fetch PSN auth code, location header missing'
90
+ end
91
+
92
+ def get_error_message(error, error_description = nil)
93
+ return error_description if error_description
94
+
95
+ if error == 'login_required'
96
+ 'PSN authorisation failed, NPSSO code has expired'
97
+ else
98
+ "Unhandled PSN auth error (#{error})"
99
+ end
100
+ end
101
+
102
+ def client
103
+ @client ||= OAuth2::Client.new(ENV.fetch('PSN_CLIENT_ID', nil), '', site: AUTH_URL)
104
+ end
105
+
106
+ def auth_url
107
+ client.auth_code.authorize_url(access_type: 'offline',
108
+ redirect_uri: 'com.scee.psxandroid.scecompcall://redirect',
109
+ scope: 'psn:mobile.v2.core psn:clientapp')
110
+ end
111
+
112
+ def npsso_cookie
113
+ "npsso=#{ENV.fetch('PSN_NPSSO', nil)}"
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+
6
+ module PSN
7
+ module Client
8
+ # Handles HTTP requests to the PSN API
9
+ class Request
10
+ BASE_URL = 'https://m.np.playstation.com/api'
11
+
12
+ def initialize(access_token)
13
+ @access_token = access_token
14
+ end
15
+
16
+ def get(path)
17
+ uri = URI("#{BASE_URL}#{path}")
18
+
19
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
20
+ request = Net::HTTP::Get.new(uri)
21
+ request['Authorization'] = "Bearer #{@access_token}"
22
+ http.request(request)
23
+ end
24
+
25
+ parse_response(response)
26
+ end
27
+
28
+ private
29
+
30
+ def parse_response(response)
31
+ case response
32
+ when Net::HTTPSuccess
33
+ JSON.parse(response.body)
34
+ else
35
+ raise Error, "Request failed: #{response.code} #{response.message} - #{response.body}"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'request'
4
+
5
+ module PSN
6
+ module Client
7
+ # Handles trophy-related API requests
8
+ class Trophies
9
+ def initialize(access_token)
10
+ @request = Request.new(access_token)
11
+ end
12
+
13
+ def trophy_titles(user_id: 'me', limit: 800, offset: 0)
14
+ path = "/trophy/v1/users/#{user_id}/trophyTitles?limit=#{limit}&offset=#{offset}"
15
+ @request.get(path)
16
+ end
17
+
18
+ def trophy_summary(user_id: 'me')
19
+ path = "/trophy/v1/users/#{user_id}/trophySummary"
20
+ @request.get(path)
21
+ end
22
+
23
+ def title_trophy_groups(np_communication_id:, np_service_name: nil, platform: nil)
24
+ query_string = build_query_string(npServiceName: resolve_service_name(np_service_name, platform))
25
+
26
+ path = "/trophy/v1/npCommunicationIds/#{np_communication_id}/trophyGroups?#{query_string}"
27
+ @request.get(path)
28
+ end
29
+
30
+ def earned_trophy_groups(np_communication_id:, user_id: 'me', np_service_name: nil, platform: nil)
31
+ query_string = build_query_string(npServiceName: resolve_service_name(np_service_name, platform))
32
+
33
+ path = "/trophy/v1/users/#{user_id}/npCommunicationIds/#{np_communication_id}/trophyGroups?#{query_string}"
34
+ @request.get(path)
35
+ end
36
+
37
+ def title_trophies(np_communication_id:, trophy_group_id: 'all', np_service_name: nil, platform: nil, **options)
38
+ query_string = build_query_string(
39
+ npServiceName: resolve_service_name(np_service_name, platform),
40
+ limit: options[:limit],
41
+ offset: options[:offset]
42
+ )
43
+
44
+ path = "/trophy/v1/npCommunicationIds/#{np_communication_id}/trophyGroups/#{trophy_group_id}/trophies"
45
+ path += "?#{query_string}"
46
+ @request.get(path)
47
+ end
48
+
49
+ def earned_trophies(np_communication_id:, user_id: 'me', trophy_group_id: 'all', np_service_name: nil,
50
+ platform: nil, **options)
51
+ query_string = build_query_string(
52
+ npServiceName: resolve_service_name(np_service_name, platform),
53
+ limit: options[:limit],
54
+ offset: options[:offset]
55
+ )
56
+
57
+ path = "/trophy/v1/users/#{user_id}/npCommunicationIds/#{np_communication_id}" \
58
+ "/trophyGroups/#{trophy_group_id}/trophies"
59
+ path += "?#{query_string}"
60
+ @request.get(path)
61
+ end
62
+
63
+ private
64
+
65
+ def resolve_service_name(service_name, platform)
66
+ raise ArgumentError, 'Provide either np_service_name or platform, not both' if service_name && platform
67
+
68
+ return service_name if service_name
69
+
70
+ if %w[PS3 PS4 PSVita].include?(platform)
71
+ 'trophy'
72
+ elsif %w[PS5 PC].include?(platform)
73
+ 'trophy2'
74
+ end
75
+ end
76
+
77
+ def build_query_string(**query_params)
78
+ query_params.compact.map { |key, value| "#{key}=#{value}" }.join('&')
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PSN
4
+ module Client
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
data/lib/psn/client.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'client/auth'
4
+ require_relative 'client/request'
5
+ require_relative 'client/trophies'
6
+ require_relative 'client/version'
7
+ require_relative 'logger'
8
+ require 'net/http'
9
+
10
+ module PSN
11
+ module Client
12
+ class Error < StandardError; end
13
+ # Your code goes here...
14
+ end
15
+ end
data/lib/psn/logger.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ # Get/Set logger for the gem
6
+ module PSN
7
+ # defaults to Rails.logger if in a Rails environment, otherwise logs to STDOUT
8
+ def self.logger
9
+ @logger ||= defined?(Rails) ? Rails.logger : Logger.new($stdout)
10
+ end
11
+
12
+ def self.logger=(logger)
13
+ @logger = logger
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ module PSN
2
+ module Client
3
+ class Auth
4
+ AUTH_URL: String
5
+ BASIC_TOKEN: String?
6
+
7
+ def self.authenticate: () -> String
8
+ def authenticate: () -> String
9
+
10
+ private
11
+
12
+ def fetch_auth_code: () -> String
13
+ def http_for: (URI) -> Net::HTTP
14
+ def request_for: (URI) -> Net::HTTP::Get
15
+ def fetch_token: (String) -> OAuth2::AccessToken
16
+ def parse_code: (String) -> String
17
+ def raise_auth_error: (Hash[String, String]) -> void
18
+ def extract_error_message: (untyped) -> String
19
+ def get_error_message: (String?, ?String) -> String
20
+ def client: () -> OAuth2::Client
21
+ def auth_url: () -> String
22
+ def npsso_cookie: () -> String
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ module PSN
2
+ module Client
3
+ class Request
4
+ BASE_URL: String
5
+
6
+ def initialize: (String access_token) -> void
7
+ def get: (String path) -> untyped
8
+
9
+ private
10
+
11
+ def parse_response: (Net::HTTPResponse response) -> untyped
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ module PSN
2
+ module Client
3
+ class Trophies
4
+ @request: Request
5
+
6
+ def initialize: (String access_token) -> void
7
+ def trophy_titles: (?user_id: String, ?limit: Integer, ?offset: Integer) -> untyped
8
+ def trophy_summary: (?user_id: String) -> untyped
9
+ def title_trophy_groups: (np_communication_id: String, ?np_service_name: String?, ?platform: String?) -> untyped
10
+ def earned_trophy_groups: (?user_id: String, np_communication_id: String, ?np_service_name: String?, ?platform: String?) -> untyped
11
+ def title_trophies: (np_communication_id: String, ?trophy_group_id: String, ?np_service_name: String?, ?platform: String?, **untyped options) -> untyped
12
+ def earned_trophies: (?user_id: String, np_communication_id: String, ?trophy_group_id: String, ?np_service_name: String?, ?platform: String?, **untyped options) -> untyped
13
+
14
+ private
15
+
16
+ def resolve_service_name: (String? service_name, String? platform) -> String?
17
+ def build_query_string: (**untyped query_params) -> String
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,6 @@
1
+ module PSN
2
+ module Client
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
@@ -0,0 +1,4 @@
1
+ module PSN
2
+ def self.logger: () -> (Logger | untyped)
3
+ def self.logger=: (Logger | untyped) -> (Logger | untyped)
4
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: psn-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Jacques
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: oauth2
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ description: A Ruby client for the PlayStation Network API that handles authentication
27
+ via NPSSO cookie and provides access to user trophy data.
28
+ email:
29
+ - matty.jacques@proton.me
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - CHANGELOG.md
35
+ - CODE_OF_CONDUCT.md
36
+ - LICENSE.txt
37
+ - README.md
38
+ - docs/andshrew/APIv2.md
39
+ - docs/andshrew/LICENSE
40
+ - lib/psn/client.rb
41
+ - lib/psn/client/auth.rb
42
+ - lib/psn/client/request.rb
43
+ - lib/psn/client/trophies.rb
44
+ - lib/psn/client/version.rb
45
+ - lib/psn/logger.rb
46
+ - sig/psn/client.rbs
47
+ - sig/psn/client/auth.rbs
48
+ - sig/psn/client/request.rbs
49
+ - sig/psn/client/trophies.rbs
50
+ - sig/psn/logger.rbs
51
+ homepage: https://github.com/MattyJacques/psn-client
52
+ licenses:
53
+ - MIT
54
+ metadata:
55
+ homepage_uri: https://github.com/MattyJacques/psn-client
56
+ source_code_uri: https://github.com/MattyJacques/psn-client
57
+ changelog_uri: https://github.com/MattyJacques/psn-client/blob/main/CHANGELOG.md
58
+ rubygems_mfa_required: 'true'
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 3.1.0
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 4.0.3
74
+ specification_version: 4
75
+ summary: A Ruby client for the PlayStation Network API
76
+ test_files: []