mirror of
https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies.git
synced 2026-07-25 06:14:31 +00:00
Refactored MAAC error/status handling and UI feedback
- Introduced centralized MAACRequestHandler for error tracking, retries, and status escalation (Ok/Warning/Error) per account - PortableMaClient now uses new Status property (PortableMaClientStatus) instead of IsError; error history tracked in PortableMaClientErrorData - All MAAC requests now routed through MAACRequestHandler, providing unified error handling and retry logic - MultiAccountAutoConfirmer now filters accounts by Status.StatusType == Ok - Updated MainWindow UI: account name color reflects status (Success/Warning/Error); added "Unattach proxy" to context menu - Removed obsolete PortableMaClientStatusToColorConverter and related bindings - Settings: removed IgnorePatchTuesdayErrors, added MaacErrorThreshold and MaacRetryInterval for error escalation control - Refactored SessionHandler: split into logic, API, and helpers; improved exception propagation and session management - AdmissionHelper methods now handle null sessions and always transfer community cookies if session is missing - Mafile export: always preserves SteamId, even if session data is not exported - ViewModel: RemoveProxy command now parameterized and only enabled if Mafile has a proxy - Enabled concurrent log writing in NLog config - Minor code cleanups and improved exception signatures
This commit is contained in:
@@ -12,7 +12,6 @@
|
||||
<converters:MultiCommandParameterConverter x:Key="MultiCommandParameterConverter" />
|
||||
<converters:ColorToBrushConverter x:Key="ColorToBrushConverter" />
|
||||
<converters:AnyMafilesToVisibilityConverter x:Key="AnyMafilesToVisibilityConverter" />
|
||||
<converters:PortableMaClientStatusToColorConverter x:Key="PortableMaClientStatusToColorConverter" />
|
||||
<converters:BoolToMafileNamingConverter x:Key="BoolToMafileNamingConverter" />
|
||||
<converters:BoolToValueConverter x:Key="BoolToValueConverter" />
|
||||
<converters:NullableToBooleanConverter x:Key="NullableToBooleanConverter" />
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace NebulaAuth.Converters;
|
||||
|
||||
public class PortableMaClientStatusToColorConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is false)
|
||||
{
|
||||
return new SolidColorBrush(Color.FromRgb(187, 224, 139));
|
||||
}
|
||||
|
||||
|
||||
return new SolidColorBrush(Color.FromRgb(224, 139, 139));
|
||||
}
|
||||
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -294,18 +294,42 @@
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock HorizontalAlignment="Stretch" Text="{Binding AccountName}">
|
||||
<TextBlock.Resources>
|
||||
<SolidColorBrush x:Key="BaseContentBrushProxy"
|
||||
Color="{DynamicResource BaseContentColor}" />
|
||||
</TextBlock.Resources>
|
||||
<TextBlock HorizontalAlignment="Stretch"
|
||||
Text="{Binding AccountName}"
|
||||
ToolTip="{Binding LinkedClient.Status.Message, FallbackValue={x:Null}}">
|
||||
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground"
|
||||
Value="{DynamicResource BaseContentBrush}" />
|
||||
|
||||
<Style.Triggers>
|
||||
<DataTrigger
|
||||
Binding="{Binding LinkedClient.Status.StatusType}"
|
||||
Value="Ok">
|
||||
<Setter Property="Foreground"
|
||||
Value="{DynamicResource SuccessBrush}" />
|
||||
</DataTrigger>
|
||||
|
||||
<DataTrigger
|
||||
Binding="{Binding LinkedClient.Status.StatusType}"
|
||||
Value="Warning">
|
||||
<Setter Property="Foreground"
|
||||
Value="{DynamicResource WarningBrush}" />
|
||||
</DataTrigger>
|
||||
|
||||
<DataTrigger
|
||||
Binding="{Binding LinkedClient.Status.StatusType}"
|
||||
Value="Error">
|
||||
<Setter Property="Foreground"
|
||||
Value="{DynamicResource ErrorBrush}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
|
||||
<TextBlock.Foreground>
|
||||
<Binding Path="LinkedClient.IsError"
|
||||
Converter="{StaticResource PortableMaClientStatusToColorConverter}"
|
||||
FallbackValue="{StaticResource BaseContentBrushProxy}" />
|
||||
</TextBlock.Foreground>
|
||||
</TextBlock>
|
||||
|
||||
<StackPanel
|
||||
Visibility="{Binding LinkedClient, Converter={StaticResource NullableToVisibilityConverter}}"
|
||||
Grid.Column="1" Orientation="Horizontal">
|
||||
@@ -393,7 +417,9 @@
|
||||
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.RemoveFromGroup}"
|
||||
Command="{Binding Path=RemoveGroupCommand}"
|
||||
CommandParameter="{Binding Path=PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" />
|
||||
|
||||
<MenuItem Header="{Tr MainWindow.ContextMenus.Mafile.UnattachProxy}"
|
||||
Command="{Binding RemoveProxyCommand}"
|
||||
CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
|
||||
</ContextMenu>
|
||||
</FrameworkElement.ContextMenu>
|
||||
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading.Tasks;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using SteamLib.Exceptions.Authorization;
|
||||
|
||||
namespace NebulaAuth.Model.MAAC;
|
||||
|
||||
public static class MAACRequestHandler
|
||||
{
|
||||
private const string LOC_PATH = "MAAC";
|
||||
private static readonly ConcurrentDictionary<Mafile, PortableMaClientErrorData> _errors = new();
|
||||
|
||||
public static void Register(Mafile mafile)
|
||||
{
|
||||
_errors[mafile] = new PortableMaClientErrorData();
|
||||
}
|
||||
|
||||
public static void Unregister(Mafile mafile)
|
||||
{
|
||||
_errors.TryRemove(mafile, out _);
|
||||
}
|
||||
|
||||
public static PortableMaClientStatus ClearErrors(Mafile mafile)
|
||||
{
|
||||
if (_errors.TryGetValue(mafile, out var data))
|
||||
{
|
||||
data.Clear();
|
||||
}
|
||||
|
||||
return PortableMaClientStatus.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a MAAC request with progressive error handling strategy: retries and status management based on error
|
||||
/// history.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="client"></param>
|
||||
/// <param name="func"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<T> HandleRequest<T>(PortableMaClient client, Func<Task<T>> func)
|
||||
{
|
||||
var result = await client.DoRequest(func, client.Mafile.PreviouslyHadNoErrors());
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
InformRequestSuccessful(client.Mafile);
|
||||
return result.Data;
|
||||
}
|
||||
|
||||
if (client.Mafile.PreviouslyHadNoErrors())
|
||||
{
|
||||
client.SetStatus(PortableMaClientStatus.Warning(GetPortableMaClientStatus("SessionError")));
|
||||
}
|
||||
else if (client.Mafile.LastErrorWasAtLeast(Settings.Instance.MaacRetryInterval))
|
||||
{
|
||||
Shell.Logger.Info("Retrying MAAC request for {name} MAAC account after previous error.",
|
||||
client.Mafile.AccountName);
|
||||
result = await client.DoRequest(func);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
InformRequestSuccessful(client.Mafile);
|
||||
return result.Data;
|
||||
}
|
||||
}
|
||||
|
||||
if (client.Mafile.ErrorPersistedFor(Settings.Instance.MaacErrorThreshold))
|
||||
{
|
||||
client.SetStatus(PortableMaClientStatus.Error(GetPortableMaClientStatus("SessionError")));
|
||||
}
|
||||
|
||||
AddError(client.Mafile, result.Exception!);
|
||||
throw result.Exception!;
|
||||
}
|
||||
|
||||
|
||||
private static async Task<Result<T>> DoRequest<T>(this PortableMaClient client, Func<Task<T>> req,
|
||||
bool withSessionHandler = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
return withSessionHandler
|
||||
? Result<T>.Success(await SessionHandler.Handle(req, client.Mafile, client.Chp(),
|
||||
GetTimerPrefix(client.Mafile)))
|
||||
: Result<T>.Success(await req());
|
||||
}
|
||||
catch (SessionInvalidException ex)
|
||||
{
|
||||
Shell.Logger.Warn("SessionInvalidException caught in MAAC request handler.");
|
||||
return Result<T>.Error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void InformRequestSuccessful(Mafile mafile)
|
||||
{
|
||||
if (_errors.TryGetValue(mafile, out var data))
|
||||
{
|
||||
Shell.Logger.Info("MAAC request for {name} MAAC account succeeded, clearing error history.",
|
||||
mafile.AccountName);
|
||||
data.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddError(Mafile mafile, Exception ex)
|
||||
{
|
||||
if (_errors.TryGetValue(mafile, out var data))
|
||||
{
|
||||
Shell.Logger.Info("Registering error for {name} MAAC account: {ex}", mafile.AccountName, ex.Message);
|
||||
data.AddEntry(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ErrorPersistedFor(this Mafile mafile, TimeSpan span)
|
||||
{
|
||||
if (_errors.TryGetValue(mafile, out var data))
|
||||
{
|
||||
var oldestError = data.GetOldestErrorTime();
|
||||
if (oldestError.HasValue && DateTime.UtcNow - oldestError.Value > span)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private static bool PreviouslyHadNoErrors(this Mafile mafile)
|
||||
{
|
||||
if (_errors.TryGetValue(mafile, out var data))
|
||||
{
|
||||
return data.NoErrors;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool LastErrorWasAtLeast(this Mafile mafile, TimeSpan timeAgo)
|
||||
{
|
||||
if (!_errors.TryGetValue(mafile, out var data)) return false;
|
||||
var latestError = data.GetTimeFromLastError();
|
||||
if (latestError.HasValue && latestError.Value > timeAgo)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private static string GetTimerPrefix(Mafile mafile)
|
||||
{
|
||||
return GetLocalization("TimerPrefix") + mafile.AccountName + ": ";
|
||||
}
|
||||
|
||||
private static string GetLocalization(string key)
|
||||
{
|
||||
return LocManager.GetCodeBehindOrDefault(key, LOC_PATH, key);
|
||||
}
|
||||
|
||||
private static string GetPortableMaClientStatus(string key)
|
||||
{
|
||||
return LocManager.GetCodeBehindOrDefault(key, LOC_PATH, key);
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,8 @@ public static class MultiAccountAutoConfirmer
|
||||
private static async Task TimerConfirmInternal()
|
||||
{
|
||||
var clients = Lock.ReadLock(() => Clients.ToArray());
|
||||
var enabledClients = clients.Where(x => x.LinkedClient is {IsError: false}).ToArray();
|
||||
var enabledClients = clients.Where(x => x.LinkedClient is {Status.StatusType: PortableMaClientStatusType.Ok})
|
||||
.ToArray();
|
||||
enabledClients = DistributeEvenly(enabledClients).ToArray();
|
||||
var confirmed = 0;
|
||||
await Task.Run(async () =>
|
||||
@@ -69,7 +70,8 @@ public static class MultiAccountAutoConfirmer
|
||||
var conf = 0;
|
||||
try
|
||||
{
|
||||
conf = await client.LinkedClient!.Confirm();
|
||||
conf = await MAACRequestHandler.HandleRequest(client.LinkedClient!,
|
||||
() => client.LinkedClient!.Confirm());
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
@@ -114,7 +116,6 @@ public static class MultiAccountAutoConfirmer
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static bool TryAddToConfirm(Mafile mafile)
|
||||
{
|
||||
return Lock.WriteLock(() =>
|
||||
@@ -122,6 +123,7 @@ public static class MultiAccountAutoConfirmer
|
||||
if (Clients.Contains(mafile)) return false;
|
||||
mafile.LinkedClient = new PortableMaClient(mafile);
|
||||
Clients.Add(mafile);
|
||||
MAACRequestHandler.Register(mafile);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -134,6 +136,7 @@ public static class MultiAccountAutoConfirmer
|
||||
Clients.Remove(mafile);
|
||||
mafile.LinkedClient?.Dispose();
|
||||
mafile.LinkedClient = null;
|
||||
MAACRequestHandler.Unregister(mafile);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,9 @@ using AchiesUtilities.Web.Proxy;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Utility;
|
||||
using SteamLib.Api.Mobile;
|
||||
using SteamLib.Api.Trade;
|
||||
using SteamLib.Authentication;
|
||||
using SteamLib.Exceptions.Authorization;
|
||||
using SteamLib.SteamMobile.Confirmations;
|
||||
using SteamLib.Web;
|
||||
using SteamLibForked.Abstractions;
|
||||
@@ -34,7 +32,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
|
||||
[ObservableProperty] private bool _autoConfirmMarket;
|
||||
|
||||
[ObservableProperty] private bool _autoConfirmTrades;
|
||||
[ObservableProperty] private bool _isError;
|
||||
[ObservableProperty] private PortableMaClientStatus _status = PortableMaClientStatus.Ok();
|
||||
|
||||
public PortableMaClient(Mafile mafile)
|
||||
{
|
||||
@@ -46,6 +44,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
|
||||
ClientHandler = pair.Handler;
|
||||
UpdateCookies(mafile.SessionData);
|
||||
Mafile.PropertyChanged += Mafile_PropertyChanged;
|
||||
SetStatus(PortableMaClientStatus.Error("123123"));
|
||||
}
|
||||
|
||||
private void Mafile_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
@@ -56,37 +55,21 @@ public partial class PortableMaClient : ObservableObject, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void UpdateCookies(IMobileSessionData? sessionData)
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() => IsError = sessionData == null);
|
||||
var newStatus = MAACRequestHandler.ClearErrors(Mafile);
|
||||
if (!newStatus.Equals(Status))
|
||||
SetStatus(newStatus);
|
||||
|
||||
ClientHandler.CookieContainer.ClearAllCookies();
|
||||
if (sessionData != null)
|
||||
{
|
||||
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(sessionData);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClientHandler.CookieContainer.ClearSteamCookies();
|
||||
ClientHandler.CookieContainer.AddMinimalMobileCookies();
|
||||
AdmissionHelper.TransferCommunityCookies(ClientHandler.CookieContainer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<int> Confirm()
|
||||
{
|
||||
Proxy.SetData(Mafile.Proxy?.Data ?? MaClient.DefaultProxy);
|
||||
List<Confirmation> conf;
|
||||
try
|
||||
{
|
||||
conf = (await HandleTimerRequest(GetConfirmations)).ToList();
|
||||
}
|
||||
catch (ApplicationException ex)
|
||||
{
|
||||
Shell.Logger.Warn(ex, "Timer {accountName}: Error GetConf in timer.", Mafile.AccountName);
|
||||
return 0;
|
||||
}
|
||||
var conf = (await GetConfirmations()).ToList();
|
||||
|
||||
var toConfirm = new List<Confirmation>();
|
||||
if (AutoConfirmMarket)
|
||||
@@ -103,21 +86,13 @@ public partial class PortableMaClient : ObservableObject, IDisposable
|
||||
}
|
||||
|
||||
if (toConfirm.Count == 0) return 0;
|
||||
try
|
||||
{
|
||||
Shell.Logger.Debug("Timer {accountName}: Sending confirmations. Count: {count}", Mafile.AccountName,
|
||||
toConfirm.Count);
|
||||
var success = await HandleTimerRequest(() => SendConfirmations(toConfirm));
|
||||
var success = await SendConfirmations(toConfirm);
|
||||
//TODO: handle success == false
|
||||
Shell.Logger.Debug("Timer {accountName}: Confirmation sent: {confirmResult}", Mafile.AccountName, success);
|
||||
return toConfirm.Count;
|
||||
}
|
||||
catch (ApplicationException ex)
|
||||
{
|
||||
Shell.Logger.Warn(ex, "Timer {accountName}: MultiConf error in Timer.", Mafile.AccountName);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task<IEnumerable<Confirmation>> GetConfirmations()
|
||||
@@ -148,58 +123,7 @@ public partial class PortableMaClient : ObservableObject, IDisposable
|
||||
Mafile.SessionData!.SteamId, Mafile, true, _cts.Token);
|
||||
}
|
||||
|
||||
private async Task<T> HandleTimerRequest<T>(Func<Task<T>> func)
|
||||
{
|
||||
Exception innerException;
|
||||
try
|
||||
{
|
||||
return await SessionHandler.Handle(func, Mafile, Chp(), GetTimerPrefix());
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
innerException = ex; //Ignored
|
||||
}
|
||||
catch (SessionInvalidException ex)
|
||||
{
|
||||
if (IgnoreTimerErrors())
|
||||
{
|
||||
Shell.Logger.Warn(
|
||||
"Timer {accountName}: Session error while requesting in timer. Ignored due to IgnorePatchTuesdayErrors setting",
|
||||
Mafile.AccountName);
|
||||
}
|
||||
else
|
||||
{
|
||||
Shell.Logger.Warn("Timer {accountName}: Session error while requesting in timer. Timer disabled",
|
||||
Mafile.AccountName);
|
||||
SetError();
|
||||
}
|
||||
|
||||
innerException = ex;
|
||||
}
|
||||
catch (Exception ex) when (ExceptionHandler.Handle(ex, GetTimerPrefix()))
|
||||
{
|
||||
innerException = ex;
|
||||
}
|
||||
|
||||
throw new ApplicationException("Swallowed", innerException);
|
||||
}
|
||||
|
||||
|
||||
private bool IgnoreTimerErrors()
|
||||
{
|
||||
if (!Settings.Instance.IgnorePatchTuesdayErrors) return false;
|
||||
|
||||
var pstZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
|
||||
var pstNow = TimeZoneInfo.ConvertTime(DateTime.UtcNow, pstZone);
|
||||
|
||||
var startTime = pstNow.Date.AddHours(15).AddMinutes(55); // 15:55 PST //22:55 GMT //00:55 GMT+2
|
||||
var endTime = pstNow.Date.AddHours(17).AddMinutes(15); // 17:15 PST //00:15 GMT //02:15 GMT+2
|
||||
|
||||
return pstNow.DayOfWeek == DayOfWeek.Tuesday && pstNow >= startTime && pstNow <= endTime;
|
||||
}
|
||||
|
||||
|
||||
private HttpClientHandlerPair Chp()
|
||||
public HttpClientHandlerPair Chp()
|
||||
{
|
||||
return new HttpClientHandlerPair(Client, ClientHandler);
|
||||
}
|
||||
@@ -214,11 +138,14 @@ public partial class PortableMaClient : ObservableObject, IDisposable
|
||||
return GetLocalization("TimerPrefix") + Mafile.AccountName + ": ";
|
||||
}
|
||||
|
||||
public void SetError()
|
||||
public void SetStatus(PortableMaClientStatus status)
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() => Status = status);
|
||||
if (status.StatusType == PortableMaClientStatusType.Error)
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() => IsError = true);
|
||||
Shell.Logger.Warn("Timer {accountName}: disabled due to error.", Mafile.AccountName);
|
||||
SnackbarController.SendSnackbar(GetTimerPrefix() + GetLocalization("TimerSessionError"));
|
||||
SnackbarController.SendSnackbar(GetTimerPrefix() + status.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace NebulaAuth.Model.MAAC;
|
||||
|
||||
public sealed class PortableMaClientErrorData
|
||||
{
|
||||
public SortedDictionary<DateTime, Exception> Values { get; } = new();
|
||||
public bool NoErrors => Values.Count == 0;
|
||||
private readonly object _lock = new();
|
||||
|
||||
|
||||
public DateTime? GetOldestErrorTime()
|
||||
{
|
||||
if (Values.Count == 0) return null;
|
||||
return Values.Keys.First();
|
||||
}
|
||||
|
||||
public void AddEntry(Exception ex)
|
||||
{
|
||||
lock (_lock)
|
||||
Values[DateTime.UtcNow] = ex;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (_lock)
|
||||
Values.Clear();
|
||||
}
|
||||
|
||||
public TimeSpan? GetTimeFromLastError()
|
||||
{
|
||||
if (Values.Count == 0) return null;
|
||||
var lastEntry = Values.Keys.Last();
|
||||
return DateTime.UtcNow - lastEntry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace NebulaAuth.Model.MAAC;
|
||||
|
||||
public record PortableMaClientStatus(PortableMaClientStatusType StatusType, string? Message = null)
|
||||
{
|
||||
public static PortableMaClientStatus Ok()
|
||||
{
|
||||
return new PortableMaClientStatus(PortableMaClientStatusType.Ok);
|
||||
}
|
||||
|
||||
public static PortableMaClientStatus Warning(string? message)
|
||||
{
|
||||
return new PortableMaClientStatus(PortableMaClientStatusType.Warning, message);
|
||||
}
|
||||
|
||||
public static PortableMaClientStatus Error(string? message)
|
||||
{
|
||||
return new PortableMaClientStatus(PortableMaClientStatusType.Error, message);
|
||||
}
|
||||
}
|
||||
|
||||
public enum PortableMaClientStatusType
|
||||
{
|
||||
Ok,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
|
||||
namespace NebulaAuth.Model.MAAC;
|
||||
|
||||
public struct Result<T>
|
||||
{
|
||||
public bool IsSuccess { get; }
|
||||
public T Data => !IsSuccess ? throw new InvalidOperationException("No data available") : _data!;
|
||||
public Exception? Exception { get; }
|
||||
|
||||
private readonly T? _data;
|
||||
|
||||
private Result(bool success, T? data, Exception? exception)
|
||||
{
|
||||
IsSuccess = success;
|
||||
_data = data;
|
||||
Exception = exception;
|
||||
}
|
||||
|
||||
public static Result<T> Success(T data)
|
||||
{
|
||||
return new Result<T>(true, data, null);
|
||||
}
|
||||
|
||||
public static Result<T> Error(Exception ex)
|
||||
{
|
||||
return new Result<T>(false, default, ex);
|
||||
}
|
||||
}
|
||||
@@ -38,17 +38,7 @@ public static class MaClient
|
||||
{
|
||||
ClientHandler.CookieContainer.ClearAllCookies();
|
||||
if (account == null) return;
|
||||
if (account.SessionData != null)
|
||||
{
|
||||
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(account.SessionData);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClientHandler.CookieContainer.ClearSteamCookies();
|
||||
ClientHandler.CookieContainer.AddMinimalMobileCookies();
|
||||
AdmissionHelper.TransferCommunityCookies(ClientHandler.CookieContainer);
|
||||
}
|
||||
|
||||
Proxy.SetData(account.Proxy?.Data);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using AchiesUtilities.Extensions;
|
||||
using NebulaAuth.Core;
|
||||
using NebulaAuth.Model.Entities;
|
||||
using NebulaAuth.Model.Mafiles;
|
||||
using SteamLibForked.Models.Session;
|
||||
using SteamLibForked.Models.SteamIds;
|
||||
|
||||
namespace NebulaAuth.Model.MafileExport;
|
||||
@@ -71,6 +72,12 @@ public static class MafileExporter
|
||||
|
||||
private static async Task<string?> ExportMafile(string path, MafileExportTemplate template, Mafile mafile)
|
||||
{
|
||||
// We preserve SteamId even if other info is not included
|
||||
var session = template.IncludeSessionData
|
||||
? mafile.SessionData
|
||||
: new MobileSessionData(null!, mafile.SteamId, default, null, tokens: null);
|
||||
|
||||
|
||||
var serializeMaf = new Mafile
|
||||
{
|
||||
SharedSecret = template.IncludeSharedSecret ? mafile.SharedSecret : null!,
|
||||
@@ -78,7 +85,7 @@ public static class MafileExporter
|
||||
DeviceId = template.IncludeIdentitySecret ? mafile.DeviceId : null!,
|
||||
RevocationCode = template.IncludeRCode ? mafile.RevocationCode : null!,
|
||||
AccountName = mafile.AccountName,
|
||||
SessionData = template.IncludeSessionData ? mafile.SessionData : null!,
|
||||
SessionData = session,
|
||||
SteamId = mafile.SteamId,
|
||||
ServerTime = template.IncludeOtherInfo ? mafile.ServerTime : 0,
|
||||
SerialNumber = template.IncludeOtherInfo ? mafile.SerialNumber : 0,
|
||||
|
||||
@@ -67,7 +67,6 @@ public static class Storage
|
||||
}, token);
|
||||
|
||||
MaFiles = new ObservableCollection<Mafile>(localList.OrderBy(m => m.AccountName));
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+15
-60
@@ -34,37 +34,28 @@ public static partial class SessionHandler
|
||||
string? snackbarPrefix = null)
|
||||
{
|
||||
using var scope = Shell.Logger.PushScopeProperty("Scope", "SessionHandler");
|
||||
var mobileTokenExpired = MobileTokenExpired(mafile);
|
||||
var refreshTokenExpired = RefreshTokenExpired(mafile);
|
||||
var password = GetPassword(mafile);
|
||||
Exception? currentException = null;
|
||||
|
||||
if (!mobileTokenExpired)
|
||||
if (!mafile.MobileTokenExpired())
|
||||
{
|
||||
try
|
||||
{
|
||||
return await func();
|
||||
}
|
||||
catch (SessionInvalidException ex)
|
||||
when (!refreshTokenExpired || 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);
|
||||
}
|
||||
Shell.Logger.Warn(ex, "Session on mafile {name} {steamid} is invalid when mobile token is not expired",
|
||||
mafile.AccountName, mafile.SessionData?.SteamId);
|
||||
currentException = ex;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//State: mobileToken invalid/expired, refreshToken maybe not expired
|
||||
if (!refreshTokenExpired)
|
||||
if (!mafile.RefreshTokenExpired())
|
||||
{
|
||||
Shell.Logger.Info("Trying to refresh session on mafile {name} {steamid} using refresh token",
|
||||
mafile.AccountName,
|
||||
mafile.SessionData?.SteamId);
|
||||
|
||||
var refreshed = await RefreshInternal(chp, mafile);
|
||||
if (refreshed)
|
||||
{
|
||||
@@ -77,19 +68,14 @@ public static partial class SessionHandler
|
||||
}
|
||||
catch (SessionInvalidException ex)
|
||||
{
|
||||
Shell.Logger.Info(ex, "MobileToken on {name} {steamid} was refreshed but after it, error occured",
|
||||
Shell.Logger.Warn(ex, "MobileToken on {name} {steamid} was refreshed but after it, error occured",
|
||||
mafile.AccountName, mafile.SessionData?.SteamId);
|
||||
if (password == null)
|
||||
throw;
|
||||
currentException = ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if (mafile.HasPassword(out var password))
|
||||
{
|
||||
var logged = await LoginAgainInternal(chp, mafile, password, true);
|
||||
if (logged)
|
||||
@@ -100,41 +86,10 @@ public static partial class SessionHandler
|
||||
}
|
||||
}
|
||||
|
||||
//Nothing to do more, everything is expired
|
||||
throw new SessionPermanentlyExpiredException(SessionPermanentlyExpiredException.SESSION_EXPIRED_MSG);
|
||||
throw new SessionPermanentlyExpiredException(SessionPermanentlyExpiredException.SESSION_EXPIRED_MSG,
|
||||
currentException);
|
||||
}
|
||||
|
||||
|
||||
private static bool MobileTokenExpired(Mafile mafile)
|
||||
{
|
||||
var mobileToken = mafile.SessionData?.GetMobileToken();
|
||||
return mobileToken == null || mobileToken.Value.IsExpired;
|
||||
}
|
||||
|
||||
private static bool RefreshTokenExpired(Mafile mafile)
|
||||
{
|
||||
var refreshToken = mafile.SessionData?.RefreshToken;
|
||||
return refreshToken == null || refreshToken.Value.IsExpired;
|
||||
}
|
||||
|
||||
private static string? GetPassword(Mafile mafile)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (PHandler.IsPasswordSet && !string.IsNullOrWhiteSpace(mafile.Password))
|
||||
{
|
||||
return PHandler.Decrypt(mafile.Password);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private static async Task<bool> RefreshInternal(HttpClientHandlerPair chp, Mafile mafile)
|
||||
{
|
||||
try
|
||||
-2
@@ -28,7 +28,6 @@ public partial class SessionHandler //API
|
||||
mafile.SessionData.SetMobileToken(newToken);
|
||||
|
||||
//Trigger PropertyChanged event for PortableMaClient handling session updated from MaClient
|
||||
//RETHINK: it makes double operation when session handled from PortableMaClient (more often scenario) which is unwanted behaviour
|
||||
mafile.SetSessionData(mafile.SessionData);
|
||||
await Storage.UpdateMafileAsync(mafile);
|
||||
chp.Handler.CookieContainer.SetSteamMobileCookiesWithMobileToken(mafile.SessionData);
|
||||
@@ -50,7 +49,6 @@ public partial class SessionHandler //API
|
||||
AdmissionHelper.TransferCommunityCookies(chp.Handler.CookieContainer);
|
||||
|
||||
//Triggers PropertyChanged event for PortableMaClient handling session updated from MaClient
|
||||
//RETHINK: it makes double operation when session handled from PortableMaClient (more often scenario) which is unwanted behaviour
|
||||
mafile.SetSessionData((MobileSessionData) result);
|
||||
if (PHandler.IsPasswordSet)
|
||||
mafile.Password = savePassword ? PHandler.Encrypt(password) : null;
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using NebulaAuth.Model.Entities;
|
||||
|
||||
namespace NebulaAuth.Model;
|
||||
|
||||
public partial class SessionHandler //Helpers
|
||||
{
|
||||
private static bool MobileTokenExpired(this Mafile mafile)
|
||||
{
|
||||
var mobileToken = mafile.SessionData?.GetMobileToken();
|
||||
return mobileToken == null || mobileToken.Value.IsExpired;
|
||||
}
|
||||
|
||||
private static bool RefreshTokenExpired(this Mafile mafile)
|
||||
{
|
||||
var refreshToken = mafile.SessionData?.RefreshToken;
|
||||
return refreshToken == null || refreshToken.Value.IsExpired;
|
||||
}
|
||||
|
||||
private static bool HasPassword(this Mafile mafile, [NotNullWhen(true)] out string? plainPassword)
|
||||
{
|
||||
plainPassword = GetPassword(mafile);
|
||||
return !string.IsNullOrWhiteSpace(plainPassword);
|
||||
}
|
||||
|
||||
private static string? GetPassword(Mafile mafile)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (PHandler.IsPasswordSet && !string.IsNullOrWhiteSpace(mafile.Password))
|
||||
{
|
||||
return PHandler.Decrypt(mafile.Password);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,6 @@ public partial class Settings : ObservableObject
|
||||
[ObservableProperty] private bool _legacyMode = true;
|
||||
[ObservableProperty] private bool _allowAutoUpdate;
|
||||
[ObservableProperty] private bool _useAccountNameAsMafileName;
|
||||
[ObservableProperty] private bool _ignorePatchTuesdayErrors;
|
||||
|
||||
[ObservableProperty] private BackgroundMode _backgroundMode = BackgroundMode.Default;
|
||||
[ObservableProperty] private double _leftOpacity = 0.4;
|
||||
@@ -108,6 +107,9 @@ public partial class Settings : ObservableObject
|
||||
[ObservableProperty] private bool _proxyManagerDisplayProtocol;
|
||||
[ObservableProperty] private bool _proxyManagerDisplayCredentials;
|
||||
|
||||
[ObservableProperty] private TimeSpan _maacErrorThreshold = TimeSpan.FromHours(3);
|
||||
[ObservableProperty] private TimeSpan _maacRetryInterval = TimeSpan.FromMinutes(15);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
throwExceptions="true">
|
||||
|
||||
<targets>
|
||||
<target xsi:type="File" name="File" fileName="${basedir}/logs/log-${shortdate}.log">
|
||||
<target xsi:type="File"
|
||||
name="File"
|
||||
fileName="${basedir}/logs/log-${shortdate}.log"
|
||||
concurrentWrites="true"
|
||||
keepFileOpen="false">
|
||||
<layout xsi:type='CompoundLayout'>
|
||||
<layout xsi:type="JsonLayout" includeEventProperties="true" IncludeScopeProperties="true">
|
||||
<attribute name="time" layout="${longdate}" />
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<SatelliteResourceLanguages>en;ru;ua</SatelliteResourceLanguages>
|
||||
<ApplicationIcon>Theme\lock.ico</ApplicationIcon>
|
||||
<SupportedOSPlatformVersion>7.0</SupportedOSPlatformVersion>
|
||||
<AssemblyVersion>1.8.0</AssemblyVersion>
|
||||
<AssemblyVersion>1.8.1</AssemblyVersion>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -63,14 +63,6 @@
|
||||
Content="{Tr SettingsDialog.LegacyMafileMode}"
|
||||
ToolTip="{Tr SettingsDialog.LegacyMafileModeHint}" />
|
||||
<!--<CheckBox IsEnabled="False" Margin="0,10,0,0" FontSize="16" IsChecked="{Binding AllowAutoUpdate}" Content="{Tr SettingsDialog.AllowAutoUpdate}"/>-->
|
||||
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" BorderBrush="AliceBlue"
|
||||
Margin="0,5,0,0"
|
||||
BorderThickness="2"
|
||||
IsChecked="{Binding IgnorePatchTuesdayErrors}"
|
||||
ToolTip="{Tr SettingsDialog.IgnorePatchTuesdayErrorsHint}">
|
||||
<TextBlock TextWrapping="WrapWithOverflow"
|
||||
Text="{Tr SettingsDialog.IgnorePatchTuesdayErrors}" />
|
||||
</CheckBox>
|
||||
|
||||
<!--<CheckBox Margin="0,12,0,0" IsChecked="{Binding UseAccountNameAsMafileName}">
|
||||
<TextBlock TextWrapping="WrapWithOverflow" Text="{Tr SettingsDialog.UseAccountName}" />
|
||||
|
||||
@@ -68,7 +68,9 @@ public partial class MainVM : ObservableObject
|
||||
LoginAgainCommand.NotifyCanExecuteChanged();
|
||||
RemoveAuthenticatorCommand.NotifyCanExecuteChanged();
|
||||
ConfirmLoginCommand.NotifyCanExecuteChanged();
|
||||
RemoveProxyCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -69,15 +69,22 @@ public partial class MainVM
|
||||
CheckProxyExist();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RemoveProxy()
|
||||
[RelayCommand(CanExecute = nameof(RemoveProxyCanExecute))]
|
||||
private void RemoveProxy(Mafile? mafile)
|
||||
{
|
||||
if (SelectedProxy == null) return;
|
||||
if (SelectedMafile == null) return;
|
||||
SelectedMafile.Proxy = null;
|
||||
mafile ??= SelectedMafile;
|
||||
if (mafile?.Proxy == null) return;
|
||||
mafile.Proxy = null;
|
||||
if (mafile == SelectedMafile)
|
||||
SetProxy(null, false); //Not system, triggered by user
|
||||
}
|
||||
|
||||
private bool RemoveProxyCanExecute(Mafile? mafile)
|
||||
{
|
||||
mafile ??= SelectedMafile;
|
||||
return mafile is { Proxy: not null };
|
||||
}
|
||||
|
||||
private void CheckProxyExist()
|
||||
{
|
||||
if (SelectedProxy == null)
|
||||
|
||||
@@ -187,12 +187,6 @@ public partial class SettingsVM : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
public bool IgnorePatchTuesdayErrors
|
||||
{
|
||||
get => Settings.IgnorePatchTuesdayErrors;
|
||||
set => Settings.IgnorePatchTuesdayErrors = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Theme
|
||||
|
||||
@@ -469,6 +469,13 @@
|
||||
"zh": "复制密码",
|
||||
"ua": "Скопіювати пароль",
|
||||
"fr": "Copier le mot de passe"
|
||||
},
|
||||
"UnattachProxy": {
|
||||
"en": "Unattach proxy",
|
||||
"ru": "Открепить прокси",
|
||||
"zh": "取消代理绑定",
|
||||
"ua": "Відкріпити проксі",
|
||||
"fr": "Détacher le proxy"
|
||||
}
|
||||
},
|
||||
"Proxy": {
|
||||
@@ -2190,6 +2197,15 @@
|
||||
"zh": "无法加载自动确认计时器。文件似乎已损坏",
|
||||
"ua": "Не вдалося завантажити таймери авто-підтверджень. Файл, схоже, пошкоджено",
|
||||
"fr": "Impossible de charger les timers d'auto-confirmation. Le fichier semble être corrompu"
|
||||
},
|
||||
"PortableMaClientStatus": {
|
||||
"SessionError": {
|
||||
"ru": "Ошибка сессии. Нужно обновить сессию или перелогиниться",
|
||||
"en": "Session error. Need to refresh session or relogin",
|
||||
"zh": "会话错误。需要刷新会话或重新登录",
|
||||
"ua": "Помилка сесії. Потрібно оновити сесію або перелогінитися",
|
||||
"fr": "Erreur de session. Il faut rafraîchir la session ou se reconnecter"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SettingsVM": {
|
||||
|
||||
@@ -44,10 +44,15 @@ public static class AdmissionHelper
|
||||
/// <summary>
|
||||
/// Clear and set new session
|
||||
/// </summary>
|
||||
public static void SetSteamCookies(this CookieContainer container, ISessionData sessionData,
|
||||
public static void SetSteamCookies(this CookieContainer container, ISessionData? sessionData,
|
||||
string setLanguage = "english")
|
||||
{
|
||||
container.ClearSteamCookies(setLanguage);
|
||||
if (sessionData == null)
|
||||
{
|
||||
TransferCommunityCookies(container);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
AddRefreshToken(container, sessionData.RefreshToken);
|
||||
@@ -79,11 +84,16 @@ public static class AdmissionHelper
|
||||
/// <summary>
|
||||
/// Clear and set new session
|
||||
/// </summary>
|
||||
public static void SetSteamMobileCookies(this CookieContainer container, ISessionData mobileSession,
|
||||
public static void SetSteamMobileCookies(this CookieContainer container, ISessionData? mobileSession,
|
||||
string setLanguage = "english")
|
||||
{
|
||||
container.ClearSteamCookies(setLanguage);
|
||||
container.AddMinimalMobileCookies();
|
||||
if (mobileSession == null)
|
||||
{
|
||||
TransferCommunityCookies(container);
|
||||
return;
|
||||
}
|
||||
|
||||
AddRefreshToken(container, mobileSession.RefreshToken);
|
||||
|
||||
@@ -106,12 +116,19 @@ public static class AdmissionHelper
|
||||
/// Market, Trading and other pages won't be authorized
|
||||
/// </summary>
|
||||
public static void SetSteamMobileCookiesWithMobileToken(this CookieContainer container,
|
||||
IMobileSessionData mobileSession,
|
||||
IMobileSessionData? mobileSession,
|
||||
string setLanguage = "english")
|
||||
{
|
||||
container.ClearSteamCookies(setLanguage);
|
||||
container.AddMinimalMobileCookies();
|
||||
|
||||
if (mobileSession == null)
|
||||
{
|
||||
TransferCommunityCookies(container);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
AddRefreshToken(container, mobileSession.RefreshToken);
|
||||
|
||||
var community = SteamDomains.GetDomainUri(SteamDomain.Community);
|
||||
|
||||
@@ -16,7 +16,7 @@ public class SessionInvalidException : Exception
|
||||
{
|
||||
}
|
||||
|
||||
public SessionInvalidException(string message, Exception? inner) : base(message, inner)
|
||||
public SessionInvalidException(string? message, Exception? inner) : base(message, inner)
|
||||
{
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -17,7 +17,7 @@ public class SessionPermanentlyExpiredException : SessionInvalidException
|
||||
{
|
||||
}
|
||||
|
||||
public SessionPermanentlyExpiredException(string message, Exception inner) : base(message, inner)
|
||||
public SessionPermanentlyExpiredException(string? message, Exception? inner) : base(message, inner)
|
||||
{
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user