1.5.3 progress

- SessionHandler refactored. Now it used as global authorization API
- Added semaphore to SessionHandler
- WaitLogin dialog flow moved to SessionHandler
- MaClient and PortableMaClient authorization logic now separeted and works correctly
- Fixed App startup error was not localized and closed immediatly
- Fixed code progress bar was freezing due to low Dispatcher priority
- Fixed legacy serializer typo error in AccessToken field on write operation
This commit is contained in:
Давид Чернопятов
2024-10-22 19:25:27 +03:00
parent acf187fc6b
commit 06748d6f83
15 changed files with 268 additions and 176 deletions
+3 -2
View File
@@ -25,11 +25,12 @@ public partial class App
var msg = ex.ToString();
if (ex is CantAlignTimeException)
{
msg = Loc.Tr(LocManager.GetCodeBehind("CantAlignTimeError"));
msg = LocManager.Get("CantAlignTimeError");
}
MessageBox.Show(msg);
MessageBox.Show(msg, "Error", MessageBoxButton.OK, MessageBoxImage.Stop, MessageBoxResult.OK, MessageBoxOptions.DefaultDesktopOnly);
throw;
}
}
}
+9 -5
View File
@@ -16,6 +16,7 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AchiesUtilities.Web.Models;
namespace NebulaAuth.Model.MAAC;
@@ -80,7 +81,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
}
catch (ApplicationException ex)
{
Shell.Logger.Warn(ex, "Error GetConf in timer.");
Shell.Logger.Warn(ex, "Timer {accountName}: Error GetConf in timer.", Mafile.AccountName);
return 0;
}
var toConfirm = new List<Confirmation>();
@@ -98,13 +99,14 @@ public partial class PortableMaClient : ObservableObject, IDisposable
if (toConfirm.Count == 0) return 0;
try
{
Shell.Logger.Debug("Sending confirmations. Count: {count}", toConfirm.Count);
await HandleTimerRequest(() => SendConfirmations(toConfirm));
Shell.Logger.Debug("Timer {accountName}: Sending confirmations. Count: {count}", Mafile.AccountName, toConfirm.Count);
var success = await HandleTimerRequest(() => SendConfirmations(toConfirm));
Shell.Logger.Debug("Timer {accountName}: Confirmation sent: {confirmResult}", Mafile.AccountName, success);
return toConfirm.Count;
}
catch (ApplicationException ex)
{
Shell.Logger.Warn(ex, "MultiConf error in Timer.");
Shell.Logger.Warn(ex, "Timer {accountName}: MultiConf error in Timer.", Mafile.AccountName);
return 0;
}
}
@@ -125,7 +127,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
Exception innerException;
try
{
return await SessionHandler.Handle(func, Mafile, GetTimerPrefix());
return await SessionHandler.Handle(func, Mafile, Chp(), GetTimerPrefix());
}
catch (OperationCanceledException ex)
{
@@ -144,6 +146,8 @@ public partial class PortableMaClient : ObservableObject, IDisposable
throw new ApplicationException("Swallowed", innerException);
}
private HttpClientHandlerPair Chp() => new(Client, ClientHandler);
private static string GetLocalization(string key)
{
return LocManager.GetCodeBehindOrDefault(key, LOC_PATH, key);
+8 -43
View File
@@ -1,15 +1,11 @@
using AchiesUtilities.Web.Proxy;
using AchiesUtilities.Web.Models;
using AchiesUtilities.Web.Proxy;
using NebulaAuth.Model.Entities;
using SteamLib.Account;
using SteamLib.Api;
using SteamLib.Api.Mobile;
using SteamLib.Authentication;
using SteamLib.Authentication.LoginV2;
using SteamLib.Core.Enums;
using SteamLib.Core.Interfaces;
using SteamLib.Exceptions;
using SteamLib.ProtoCore.Services;
using SteamLib.SteamMobile;
using SteamLib.SteamMobile.Confirmations;
using SteamLib.Web;
using System;
@@ -30,6 +26,7 @@ public static class MaClient
private static DynamicProxy Proxy { get; }
public static ProxyData? DefaultProxy { get; set; }
public static HttpClientHandlerPair Chp => new(Client, ClientHandler);
static MaClient()
{
@@ -66,50 +63,18 @@ public static class MaClient
return SteamMobileConfirmationsApi.GetConfirmations(Client, mafile, mafile.SessionData!.SteamId);
}
public static async Task LoginAgain(Mafile mafile, string password, bool savePassword, ICaptchaResolver? resolver)
public static Task LoginAgain(Mafile mafile, string password, bool savePassword, ICaptchaResolver? resolver)
{
SetProxy(mafile);
var sgGenerator = new SteamGuardCodeGenerator(mafile.SharedSecret);
var options = new LoginV2ExecutorOptions(LoginV2Executor.NullConsumer, Client)
{
Logger = Shell.ExtensionsLogger,
SteamGuardProvider = sgGenerator,
DeviceDetails = LoginV2ExecutorOptions.GetMobileDefaultDevice(),
WebsiteId = "Mobile"
};
ClientHandler.CookieContainer.ClearMobileSessionCookies();
var result = await LoginV2Executor.DoLogin(options, mafile.AccountName, password);
AdmissionHelper.TransferCommunityCookies(ClientHandler.CookieContainer);
mafile.SetSessionData((MobileSessionData)result);
if (PHandler.IsPasswordSet)
mafile.Password = (savePassword ? PHandler.Encrypt(password) : null);
Storage.UpdateMafile(mafile);
return SessionHandler.LoginAgain(Chp, mafile, password, savePassword);
}
public static async Task RefreshSession(Mafile mafile)
public static Task RefreshSession(Mafile mafile)
{
ValidateMafile(mafile, true);
SetProxy(mafile);
var token = mafile.SessionData!.GetMobileToken();
if (token == null || token.Value.IsExpired)
{
var sessionToken = await SteamMobileApi.RefreshJwt(Client, mafile.SessionData!.RefreshToken.Token, mafile.SessionData.SteamId.Steam64);
var newToken = SteamTokenHelper.Parse(sessionToken);
mafile.SessionData.SetMobileToken(newToken);
}
//RETHINK: Do we need this? Mobile token is enough
var communityToken = mafile.SessionData!.GetToken(SteamDomain.Community);
if (communityToken == null || communityToken.Value.IsExpired)
{
var communityTokenString = await SteamGlobalApi.RefreshJwt(Client, SteamDomain.Community);
var newToken = SteamTokenHelper.Parse(communityTokenString);
mafile.SessionData.SetToken(SteamDomain.Community, newToken);
}
Storage.UpdateMafile(mafile);
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(mafile.SessionData);
return SessionHandler.RefreshMobileToken(Chp, mafile);
}
public static Task<bool> SendConfirmation(Mafile mafile, Confirmation confirmation, bool confirm)
@@ -122,7 +87,7 @@ public static class MaClient
public static Task<bool> SendMultipleConfirmation(Mafile mafile, IEnumerable<Confirmation> confirmations, bool confirm)
{
var enumerable = confirmations.ToList();
if (!enumerable.Any())
if (enumerable.Count == 0)
{
return Task.FromResult(result: false);
}
+164 -62
View File
@@ -1,24 +1,122 @@
using NebulaAuth.Core;
using AchiesUtilities.Web.Models;
using MaterialDesignThemes.Wpf;
using NebulaAuth.Core;
using NebulaAuth.Model.Entities;
using SteamLib.Exceptions;
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using NebulaAuth.View.Dialogs;
namespace NebulaAuth.Model;
public static class SessionHandler
public static partial class SessionHandler
{
public static event EventHandler? LoginStarted;
public static event EventHandler? LoginCompleted;
public static async Task<T> Handle<T>(Func<Task<T>> func, Mafile mafile, string? snackbarPrefix = null)
private static readonly SemaphoreSlim Semaphore = new(1, 1);
public static async Task<T> Handle<T>(Func<Task<T>> func, Mafile mafile,
HttpClientHandlerPair? chp = null, string? snackbarPrefix = null)
{
chp ??= MaClient.Chp;
await Semaphore.WaitAsync();
try
{
return await HandleInternal(func, chp.Value, mafile, snackbarPrefix);
}
finally
{
Semaphore.Release();
}
}
private static async Task<T> HandleInternal<T>(Func<Task<T>> func, HttpClientHandlerPair chp, Mafile mafile,
string? snackbarPrefix = null)
{
using var scope = Shell.Logger.PushScopeProperty("Scope", "SessionHandler");
var mobileTokenExpired = MobileTokenExpired(mafile);
var refreshTokenExpired = RefreshTokenExpired(mafile);
var password = GetPassword(mafile);
if (!mobileTokenExpired)
{
try
{
return await func();
}
catch (SessionInvalidException ex)
when (refreshTokenExpired == false || password != null)
{
if (ex is SessionPermanentlyExpiredException)
{
Shell.Logger.Debug(ex, "RefreshToken on mafile {name} {steamid} is expired", mafile.AccountName, mafile.SessionData?.SteamId);
refreshTokenExpired = true;
}
else
{
Shell.Logger.Debug(ex, "MobileToken on mafile {name} {steamid} is expired", mafile.AccountName, mafile.SessionData?.SteamId);
}
}
}
//State: mobileToken invalid/expired, refreshToken maybe not expired
if (!refreshTokenExpired)
{
var refreshed = await RefreshInternal(chp, mafile);
if (refreshed)
{
SnackbarController.SendSnackbar(snackbarPrefix + LocManager.GetCodeBehindOrDefault("SessionWasRefreshedAutomatically", "SessionHandler", "SessionWasRefreshedAutomatically"));
try
{
return await func();
}
catch (SessionInvalidException)
when (password != null)
{
}
}
}
Shell.Logger.Debug("Session on mafile {name} {steamid} is invalid/expired", mafile.AccountName, mafile.SessionData?.SteamId);
//State: mobileToken invalid/expired, refreshToken invalid/expired
if (password != null)
{
var logged = await LoginAgainInternal(chp, mafile, password, true);
if (logged)
{
Shell.Logger.Debug("Mafile {name} {steamid} was succesfully auto-relogined", mafile.AccountName, mafile.SessionData?.SteamId);
return await func();
}
}
//Nothing to do more, everything is expired
throw new SessionPermanentlyExpiredException(SessionPermanentlyExpiredException.SESSION_EXPIRED_MSG);
}
private static bool MobileTokenExpired(Mafile mafile)
{
var mobileToken = mafile.SessionData?.GetMobileToken();
return mobileToken == null || mobileToken.Value.IsExpired;
}
private static bool RefreshTokenExpired(Mafile mafile)
{
return mafile.SessionData?.RefreshToken.IsExpired != false;
}
private static string? GetPassword(Mafile mafile)
{
string? password = null;
try
{
if (PHandler.IsPasswordSet && !string.IsNullOrWhiteSpace(mafile.Password))
{
password = PHandler.Decrypt(mafile.Password);
return PHandler.Decrypt(mafile.Password);
}
}
catch
@@ -26,70 +124,74 @@ public static class SessionHandler
// ignored
}
var refreshed = false;
try
{
return await func();
}
catch (SessionInvalidException) when (mafile.SessionData is { RefreshToken.IsExpired: false})
{
Shell.Logger.Debug("Token on mafile {name} {steamid} expired. Trying to refresh", mafile.AccountName, mafile.SessionData?.SteamId);
refreshed = await TryRefresh(mafile);
}
catch (SessionInvalidException)
when (password != null)
{
}
if (refreshed)
{
Shell.Logger.Debug("Token on mafile {name} {steamid} refreshed", mafile.AccountName,
mafile.SessionData?.SteamId);
try
{
return await func();
}
catch (Exception ex3)
when (password != null && ex3 is SessionPermanentlyExpiredException or SessionInvalidException)
{
}
}
if (password == null)
{
throw new SessionInvalidException();
}
try
{
LoginStarted?.Invoke(null, EventArgs.Empty);
await MaClient.LoginAgain(mafile, password, savePassword: true, null);
Shell.Logger.Debug("Mafile {name} {steamid} succesfully auto-relogined", mafile.AccountName,
mafile.SessionData?.SteamId);
}
finally
{
LoginCompleted?.Invoke(null, EventArgs.Empty);
}
return await func();
return null;
}
private static async Task<bool> TryRefresh(Mafile mafile, string? snackbarPrefix = null)
private static async Task<bool> RefreshInternal(HttpClientHandlerPair chp, Mafile mafile)
{
try
{
await MaClient.RefreshSession(mafile);
SnackbarController.SendSnackbar(snackbarPrefix + LocManager.GetCodeBehindOrDefault("SessionWasRefreshedAutomatically", "SessionHandler", "SessionWasRefreshedAutomatically"));
await RefreshMobileToken(chp, mafile);
return true;
}
catch (SessionInvalidException)
catch(Exception ex)
{
Shell.Logger.Debug("Token on mafile {name} {steamid} not refreshed", mafile.AccountName, mafile.SessionData?.SteamId);
Shell.Logger.Debug(ex, "Failed to refresh session on mafile {name} {steamid}", mafile.AccountName, mafile.SessionData?.SteamId);
return false;
}
}
private static async Task<bool> LoginAgainInternal(HttpClientHandlerPair chp, Mafile mafile, string password,
bool savePassword)
{
var t = Task.Run(OnLoginStarted);
try
{
await LoginAgain(chp, mafile, password, savePassword);
return true;
}
catch (Exception ex)
{
Shell.Logger.Debug(ex, "Failed to relogin mafile {name} {steamid}", mafile.AccountName,
mafile.SessionData?.SteamId);
return false;
}
finally
{
OnLoginCompleted();
await t;
}
}
private static async Task OnLoginStarted()
{
if (DialogHost.IsDialogOpen(null)) return;
await Application.Current.Dispatcher.BeginInvoke(async () =>
{
await DialogHost.Show(new WaitLoginDialog());
});
}
private static void OnLoginCompleted()
{
var currentSession = DialogHost.GetDialogSession(null);
Application.Current.Dispatcher.BeginInvoke(() =>
{
if (currentSession is { Content: WaitLoginDialog, IsEnded: false })
{
try
{
currentSession.Close();
}
catch
{
//Ignored
}
}
});
}
}
+49
View File
@@ -0,0 +1,49 @@
using AchiesUtilities.Web.Models;
using NebulaAuth.Model.Entities;
using SteamLib.Account;
using SteamLib.Api.Mobile;
using SteamLib.Authentication;
using SteamLib.Authentication.LoginV2;
using SteamLib.Exceptions;
using SteamLib.SteamMobile;
using System.Threading.Tasks;
namespace NebulaAuth.Model;
public partial class SessionHandler //API
{
public static async Task RefreshMobileToken(HttpClientHandlerPair chp, Mafile mafile)
{
if (mafile.SessionData is not { RefreshToken.IsExpired: false })
throw new SessionPermanentlyExpiredException(SessionInvalidException.SESSION_NULL_MSG);
var mobileToken = await SteamMobileApi.RefreshJwt(chp.Client, mafile.SessionData.RefreshToken.Token, mafile.SessionData.SteamId);
Shell.Logger.Info("MobileToken on {name} {steamid} successfully refreshed", mafile.AccountName, mafile.SessionData.SteamId);
var newToken = SteamTokenHelper.Parse(mobileToken);
mafile.SessionData.SetMobileToken(newToken);
mafile.SetSessionData(mafile.SessionData); //Trigger event
Storage.UpdateMafile(mafile);
chp.Handler.CookieContainer.SetSteamMobileCookiesWithMobileToken(mafile.SessionData);
}
public static async Task LoginAgain(HttpClientHandlerPair chp, Mafile mafile, string password, bool savePassword)
{
var sgGenerator = new SteamGuardCodeGenerator(mafile.SharedSecret);
var options = new LoginV2ExecutorOptions(LoginV2Executor.NullConsumer, chp.Client)
{
Logger = Shell.ExtensionsLogger,
SteamGuardProvider = sgGenerator,
DeviceDetails = LoginV2ExecutorOptions.GetMobileDefaultDevice(),
WebsiteId = "Mobile",
};
chp.Handler.CookieContainer.ClearMobileSessionCookies();
var result = await LoginV2Executor.DoLogin(options, mafile.AccountName, password);
Shell.Logger.Info("Logged in again on {name} {steamid}", mafile.AccountName, result.SteamId);
AdmissionHelper.TransferCommunityCookies(chp.Handler.CookieContainer);
mafile.SetSessionData((MobileSessionData)result);
if (PHandler.IsPasswordSet)
mafile.Password = (savePassword ? PHandler.Encrypt(password) : null);
Storage.UpdateMafile(mafile);
}
}
+10 -4
View File
@@ -1,5 +1,6 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using JetBrains.Annotations;
using MaterialDesignThemes.Wpf;
using NebulaAuth.Core;
using NebulaAuth.Model;
@@ -12,7 +13,6 @@ using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace NebulaAuth.ViewModel;
@@ -21,8 +21,9 @@ public partial class MainVM : ObservableObject
{
[ObservableProperty]
private ObservableCollection<Mafile> _maFiles = Storage.MaFiles;
public SnackbarMessageQueue MessageQueue => SnackbarController.MessageQueue;
[UsedImplicitly]
public SnackbarMessageQueue MessageQueue => SnackbarController.MessageQueue;
public Mafile? SelectedMafile
@@ -42,8 +43,6 @@ public partial class MainVM : ObservableObject
new MaProxy(kvp.Key, kvp.Value)));
Storage.MaFiles.CollectionChanged += MaFilesOnCollectionChanged;
QueryGroups();
SessionHandler.LoginStarted += SessionHandlerOnLoginStarted;
SessionHandler.LoginCompleted += SessionHandlerOnLoginCompleted;
UpdateManager.CheckForUpdates();
if (Storage.DuplicateFound > 0)
{
@@ -200,4 +199,11 @@ public partial class MainVM : ObservableObject
Shell.Logger.Error(ex);
}
}
private static string GetLocalization(string key)
{
const string LOC_PATH = "MainVM";
return LocManager.GetCodeBehindOrDefault(key, LOC_PATH, key);
}
}
+3 -3
View File
@@ -30,7 +30,7 @@ public partial class MainVM
private void UpdateCode(object? state = null)
{
var currentTime = TimeAligner.GetSteamTime();
var untilChange = currentTime - currentTime / 30L * 30L;
var untilChange = currentTime % 30;
var codeProgress = untilChange / 30D * 100;
string? code = null;
@@ -41,12 +41,12 @@ public partial class MainVM
if (Application.Current == null) return;
Application.Current.Dispatcher.Invoke((string? c) =>
Application.Current.Dispatcher.BeginInvoke((string? c) =>
{
if (Application.Current.MainWindow?.WindowState == WindowState.Minimized) return;
CodeProgress = codeProgress;
if (c != null) Code = c;
}, DispatcherPriority.Background, code);
}, DispatcherPriority.DataBind, code);
}
[RelayCommand(AllowConcurrentExecutions = false)]
-47
View File
@@ -1,47 +0,0 @@
using MaterialDesignThemes.Wpf;
using System;
using System.Windows;
using NebulaAuth.View.Dialogs;
using NebulaAuth.Core;
namespace NebulaAuth.ViewModel;
public partial class MainVM //Other
{
private const string LOC_PATH = "MainVM";
private static string GetLocalization(string key)
{
return LocManager.GetCodeBehindOrDefault(key, LOC_PATH, key);
}
private void SessionHandlerOnLoginCompleted(object? sender, EventArgs e)
{
var currentSession = DialogHost.GetDialogSession(null);
Application.Current.Dispatcher.BeginInvoke(() =>
{
if (currentSession is { Content: WaitLoginDialog, IsEnded: false })
{
try
{
currentSession.Close();
}
catch
{
//Ignored
}
}
});
}
private async void SessionHandlerOnLoginStarted(object? sender, EventArgs e)
{
if (DialogHost.IsDialogOpen(null)) return;
await Application.Current.Dispatcher.BeginInvoke(async () =>
{
await DialogHost.Show(new WaitLoginDialog());
});
}
}
+3 -2
View File
@@ -7,6 +7,7 @@ using SteamLib.ProtoCore.Enums;
using SteamLib.ProtoCore.Exceptions;
using SteamLib.ProtoCore.Services;
using System.Net;
using SteamLib.Account;
namespace SteamLib.Api.Mobile;
@@ -28,12 +29,12 @@ public static class SteamMobileApi
/// <param name="cancellationToken"></param>
/// <returns>Refreshed AccessToken</returns>
/// <exception cref="SessionInvalidException"></exception>
public static async Task<string> RefreshJwt(HttpClient client, string refreshToken, long steamId, CancellationToken cancellationToken = default)
public static async Task<string> RefreshJwt(HttpClient client, string refreshToken, SteamId steamId, CancellationToken cancellationToken = default)
{
var req = new GenerateAccessTokenForApp_Request
{
RefreshToken = refreshToken,
SteamId = steamId,
SteamId = steamId.Steam64,
TokenRenewalType = true
};
@@ -28,12 +28,12 @@ public static class SteamMobileConfirmationsApi
var reqMsg = new HttpRequestMessage(HttpMethod.Get, req);
var resp = await client.SendAsync(reqMsg, cancellationToken);
var respStr = await resp.Content.ReadAsStringAsync(cancellationToken);
if (resp.StatusCode == HttpStatusCode.Redirect)
{
throw new SessionPermanentlyExpiredException("Mobile session expired");
throw new SessionInvalidException("Mobile session expired");
}
var respStr = await resp.Content.ReadAsStringAsync(cancellationToken);
resp.EnsureSuccessStatusCode();
try
@@ -41,7 +41,7 @@ public static class SteamMobileConfirmationsApi
return MobileConfirmationScrapper.Scrap(respStr);
}
catch (Exception ex)
when (ex is not (SessionPermanentlyExpiredException or CantLoadConfirmationsException))
when (ex is not (SessionInvalidException or CantLoadConfirmationsException))
{
SteamLibErrorMonitor.LogErrorResponse(respStr, ex);
throw;
@@ -79,7 +79,7 @@ public static class AdmissionHelper
}
/// <summary>
/// Clear and set new session. Not recommended. Uses <see cref="IMobileSessionData.GetMobileToken()"/> for domain <see cref="SteamDomain.Community"/> instead of its own cookie. It's okay to use it only for confirmations. But Market, Trading and other pages won't be authenticated
/// Clear and set new session. Not recommended. Uses <see cref="IMobileSessionData.GetMobileToken()"/> for domain <see cref="SteamDomain.Community"/> instead of its own cookie. It's okay to use it only for confirmations. But Market, Trading and other pages won't be authorized
/// </summary>
public static void SetSteamMobileCookiesWithMobileToken(this CookieContainer container, IMobileSessionData mobileSession,
string setLanguage = "english")
@@ -52,7 +52,7 @@ public class SteamAuthenticatorLinker
if (accessToken.Value.Type != SteamAccessTokenType.Mobile)
Logger?.LogWarning("Provided access token is not of type Mobile. Actual type: {actualType}", accessToken.Value.Type);
var refreshed = await SteamMobileApi.RefreshJwt(Options.HttpClient, data.RefreshToken.Token, data.RefreshToken.SteamId.Steam64);
var refreshed = await SteamMobileApi.RefreshJwt(Options.HttpClient, data.RefreshToken.Token, data.RefreshToken.SteamId);
accessToken = SteamTokenHelper.Parse(refreshed);
data.SetMobileToken(accessToken.Value);
}
@@ -63,7 +63,7 @@ public static partial class MafileSerializer //Write
? null
: new
{
AccesToken = ext.SessionData?.MobileToken?.Token,
AccessToken = ext.SessionData?.MobileToken?.Token,
steamLoginSecure = ext.SessionData?.MobileToken?.SignedToken,
RefreshToken = ext.SessionData?.RefreshToken.Token,
SteamID = ext.SessionData?.SteamId.Steam64.Id,
@@ -14,6 +14,14 @@ public static class MobileConfirmationScrapper
{"You are not set up to receive mobile confirmations", LoadConfirmationsError.NotSetupToReceiveConfirmations}
};
/// <summary>
///
/// </summary>
/// <param name="response"></param>
/// <returns></returns>
/// <exception cref="UnsupportedResponseException"></exception>
/// <exception cref="SessionInvalidException"></exception>
/// <exception cref="CantLoadConfirmationsException"></exception>
public static List<Confirmation> Scrap(string response)
{
ConfirmationsJson conf;
@@ -28,7 +36,7 @@ public static class MobileConfirmationScrapper
if (conf.NeedAuth)
{
throw new SessionPermanentlyExpiredException();
throw new SessionInvalidException();
}
if (conf.Success == false)
+3
View File
@@ -74,10 +74,13 @@
- UPDATE: Added hyperlink to troubleshooting guide in "Linking" window <br />
- UPDATE: Added socks4, socks5 proxy support (not tested) <br />
- FIX: Now errors in receiving confirmations are localized and displayed correctly. <br/>
- FIX: Fixed typo in "AccesToken" when serializing in legacy mode <br/>
- FIX: Fixed application startup error was not shown correctly and had empty message when app wasn't able to align time <br/>
- UI: Added "Copy RCode" button after linking account <br />
- UI: Auto-confirm timers now use icons instead of text-chips <br />
- UI: Minor design and localization improvements <br />
- UI: Hyperlink "by achies" now leads to repository instead of "achiez/" user page<br />
- UI: Fixed code progress bar was "lagging" <br/>
- DEV: Big code clean-ups and refactoring <br />
- DEV: Updated dependency and SteamLib libraries <br />
</div>