provide better output when frontier unable to verify ownership of game

This commit is contained in:
Chris
2024-03-31 13:01:04 -06:00
parent 4bb4948d8d
commit 9ecee20c4d
3 changed files with 83 additions and 48 deletions

View File

@ -4,6 +4,9 @@
### New Features
- Add ability to download products if they aren't yet installed
- Show a more helpful message instead of generic JSON error when Frontier API couldn't verify game ownership.
This error happens intermittently with Steam licenses.
## [0.9.0] - 2023-09-12

View File

@ -1,6 +1,7 @@
module MinEdLauncher.Api
open System
open System.IO
open System.IO.Compression
open System.Net
open System.Net.Http
@ -32,6 +33,7 @@ type AuthResult =
| LinkAvailable of Uri
| Denied of string
| Failed of string
| CouldntConfirmOwnership
let private buildUri (host: Uri) (path: string) queryParams =
let builder = UriBuilder(host)
@ -169,6 +171,13 @@ let authenticate (runningTime: unit -> double) (token: AuthToken) platform machi
match info with
| Error m -> return Failed m
| Ok (path, query, parseMachineToken) ->
let isHtml (stream: Stream) =
let htmlHeader = "<!DOCTYPE html";
let buffer = Array.zeroCreate(htmlHeader.Length);
stream.Read(buffer, 0, buffer.Length) |> ignore
stream.Seek(0, SeekOrigin.Begin) |> ignore
Encoding.UTF8.GetString(buffer) = htmlHeader
use request = new HttpRequestMessage()
request.RequestUri <- buildUri httpClient.BaseAddress path query
match platform with
@ -178,54 +187,60 @@ let authenticate (runningTime: unit -> double) (token: AuthToken) platform machi
use! response = httpClient.SendAsync(request)
use! content = response.Content.ReadAsStreamAsync()
let content = content |> Json.parseStream >>= Json.rootElement
let mapResult f = function
| Ok value -> f value
| Error msg -> Failed msg
let parseError content =
let errorValue = content >>= Json.parseEitherProp "error_enum" "errorCode" >>= Json.toString |> Result.defaultValue "Unknown"
let errorMessage = content >>= Json.parseProp "message" >>= Json.toString |> Result.defaultValue ""
errorValue, errorMessage
return
match response.StatusCode with
| code when int code < 300 ->
let fdevAuthToken = content >>= Json.parseProp "authToken" >>= Json.toString
let machineToken = parseMachineToken content
let registeredName = content >>= Json.parseProp "registeredName"
>>= Json.toString
|> Result.defaultValue $"%s{platform.Name} User"
let errorValue = content >>= Json.parseEitherProp "error_enum" "errorCode" >>= Json.toString
let errorMessage = content >>= Json.parseProp "message" >>= Json.toString
match fdevAuthToken, machineToken, errorValue, errorMessage with
| Error _, Error _, Ok value, Ok msg -> Failed $"%s{value} - %s{msg}"
| Ok fdevToken, Ok machineToken, _, _ ->
let session = { Token = fdevToken; PlatformToken = token; Name = registeredName; MachineToken = machineToken }
Authorized <| new Connection(httpClient, session, runningTime)
| Error msg, _, _, _
| _, Error msg, _, _ -> Failed msg
| HttpStatusCode.Found ->
content >>= Json.parseProp "Location"
>>= Json.asUri
|> mapResult RegistrationRequired
| HttpStatusCode.TemporaryRedirect ->
content >>= Json.parseProp "Location"
>>= Json.asUri
|> mapResult LinkAvailable
| HttpStatusCode.BadRequest ->
let errValue, errMessage = content |> parseError
$"Bad Request: %s{errValue} - %s{errMessage}" |> Denied
| HttpStatusCode.Forbidden ->
let errValue, errMessage = content |> parseError
$"Forbidden: %s{errValue} - %s{errMessage}" |> Denied
| HttpStatusCode.Unauthorized ->
let errValue, errMessage = content |> parseError
$"Unauthorized: %s{errValue} - %s{errMessage}" |> Denied
| HttpStatusCode.ServiceUnavailable ->
Failed "Service unavailable"
| code ->
Failed $"%i{int code}: %s{response.ReasonPhrase}"
// API returns an HTML login page when it can't verify game ownership. Sometimes happens with Steam accounts
// API doesn't respond with Content-Type header so check the first few bytes
if isHtml content then
return CouldntConfirmOwnership
else
let content = content |> Json.parseStream >>= Json.rootElement
let mapResult f = function
| Ok value -> f value
| Error msg -> Failed msg
let parseError content =
let errorValue = content >>= Json.parseEitherProp "error_enum" "errorCode" >>= Json.toString |> Result.defaultValue "Unknown"
let errorMessage = content >>= Json.parseProp "message" >>= Json.toString |> Result.defaultValue ""
errorValue, errorMessage
return
match response.StatusCode with
| code when int code < 300 ->
let fdevAuthToken = content >>= Json.parseProp "authToken" >>= Json.toString
let machineToken = parseMachineToken content
let registeredName = content >>= Json.parseProp "registeredName"
>>= Json.toString
|> Result.defaultValue $"%s{platform.Name} User"
let errorValue = content >>= Json.parseEitherProp "error_enum" "errorCode" >>= Json.toString
let errorMessage = content >>= Json.parseProp "message" >>= Json.toString
match fdevAuthToken, machineToken, errorValue, errorMessage with
| Error _, Error _, Ok value, Ok msg -> Failed $"%s{value} - %s{msg}"
| Ok fdevToken, Ok machineToken, _, _ ->
let session = { Token = fdevToken; PlatformToken = token; Name = registeredName; MachineToken = machineToken }
Authorized <| new Connection(httpClient, session, runningTime)
| Error msg, _, _, _
| _, Error msg, _, _ -> Failed msg
| HttpStatusCode.Found ->
content >>= Json.parseProp "Location"
>>= Json.asUri
|> mapResult RegistrationRequired
| HttpStatusCode.TemporaryRedirect ->
content >>= Json.parseProp "Location"
>>= Json.asUri
|> mapResult LinkAvailable
| HttpStatusCode.BadRequest ->
let errValue, errMessage = content |> parseError
$"Bad Request: %s{errValue} - %s{errMessage}" |> Denied
| HttpStatusCode.Forbidden ->
let errValue, errMessage = content |> parseError
$"Forbidden: %s{errValue} - %s{errMessage}" |> Denied
| HttpStatusCode.Unauthorized ->
let errValue, errMessage = content |> parseError
$"Unauthorized: %s{errValue} - %s{errMessage}" |> Denied
| HttpStatusCode.ServiceUnavailable ->
Failed "Service unavailable"
| code ->
Failed $"%i{int code}: %s{response.ReasonPhrase}"
}
let getAuthorizedProducts platform lang (connection: Connection) = task {

View File

@ -14,6 +14,7 @@ open FsToolkit.ErrorHandling
type LoginError =
| ActionRequired of string
| CouldntConfirmOwnership of Platform
| Failure of string
let login launcherVersion runningTime httpClient machineId (platform: Platform) lang =
let authenticate disposable = function
@ -27,7 +28,9 @@ let login launcherVersion runningTime httpClient machineId (platform: Platform)
| Api.RegistrationRequired uri -> return ActionRequired <| $"Registration is required at %A{uri}" |> Error
| Api.LinkAvailable uri -> return ActionRequired <| $"Link available at %A{uri}" |> Error
| Api.Denied msg -> return Failure msg |> Error
| Api.Failed msg -> return Failure msg |> Error }
| Api.Failed msg -> return Failure msg |> Error
| Api.CouldntConfirmOwnership -> return CouldntConfirmOwnership platform |> Error
}
| Error msg -> Failure msg |> Error |> Task.fromResult
match platform with
@ -247,6 +250,20 @@ module AppError =
| AuthorizedProducts m -> $"Couldn't get available products: %s{m}"
| Login (ActionRequired m) -> $"Unsupported login action required: %s{m}"
| Login (Failure m) -> $"Couldn't login: %s{m}"
| Login (CouldntConfirmOwnership platform) ->
let possibleFixes =
[
$"Ensure you've linked your {platform.Name} account to your Frontier account. https://user.frontierstore.net/user/info"
if platform = Steam then
"Restart Steam"
"Log out and log back in to Steam"
"Restart your computer"
"Wait a minute or two and retry"
"Wait longer"
]
|> List.map (fun s -> " " + s)
|> String.join Environment.NewLine
$"Frontier was unable to verify that you own the game. This happens intermittently. Possible fixes include:{Environment.NewLine}{possibleFixes}"
| NoSelectedProduct -> "No selected project"
| InvalidProductState m -> $"Couldn't start selected product: %s{m}"