From 8e5fea54cd16b1718d70e5a03a4741df6ed735d7 Mon Sep 17 00:00:00 2001 From: achiez Date: Fri, 7 Nov 2025 10:27:16 +0200 Subject: [PATCH] Added MAAC persistence support and password management features - Introduced `MAACStorage` for managing multi-account auto-confirmers persistence - Added `SetAccountPasswordsView` dialog for managing account passwords setup - Enhanced `Storage` with async methods for file operations - Added `BoolToValueConverter` for flexible XAML data bindings - Enhanced user experience in dialogs with `IsDefault` and `IsCancel` properties --- src/NebulaAuth/App.xaml.cs | 8 +- .../Converters/BoolToValueConverter.cs | 28 ++++ src/NebulaAuth/Converters/Converters.xaml | 2 +- src/NebulaAuth/Core/DialogsController.cs | 10 ++ src/NebulaAuth/MainWindow.xaml | 21 ++- src/NebulaAuth/MainWindow.xaml.cs | 2 +- src/NebulaAuth/Model/Entities/Mafile.cs | 9 +- src/NebulaAuth/Model/MAAC/MAACStorage.cs | 139 +++++++++++++++ .../Model/MAAC/MultiAccountAutoConfirmer.cs | 16 +- src/NebulaAuth/Model/MAAC/PortableMaClient.cs | 18 +- src/NebulaAuth/Model/MaClient.cs | 3 - src/NebulaAuth/Model/SessionHandler_API.cs | 4 +- src/NebulaAuth/Model/Shell.cs | 6 +- src/NebulaAuth/Model/Storage.cs | 45 ++++- .../View/Dialogs/ConfirmCancelDialog.xaml | 3 +- .../View/Dialogs/TextFieldDialog.xaml | 1 + .../View/SetAccountPasswordsView.xaml | 158 ++++++++++++++++++ .../View/SetAccountPasswordsView.xaml.cs | 13 ++ .../ViewModel/MafileMover/MafileMoverVM.cs | 2 +- src/NebulaAuth/ViewModel/MainVM.cs | 2 +- src/NebulaAuth/ViewModel/MainVM_File.cs | 2 +- .../ViewModel/Other/SetAccountPasswordsVM.cs | 122 ++++++++++++++ src/NebulaAuth/ViewModel/Other/SettingsVM.cs | 3 +- src/NebulaAuth/localization.loc.json | 102 ++++++++--- 24 files changed, 648 insertions(+), 71 deletions(-) create mode 100644 src/NebulaAuth/Converters/BoolToValueConverter.cs create mode 100644 src/NebulaAuth/Model/MAAC/MAACStorage.cs create mode 100644 src/NebulaAuth/View/SetAccountPasswordsView.xaml create mode 100644 src/NebulaAuth/View/SetAccountPasswordsView.xaml.cs create mode 100644 src/NebulaAuth/ViewModel/Other/SetAccountPasswordsVM.cs diff --git a/src/NebulaAuth/App.xaml.cs b/src/NebulaAuth/App.xaml.cs index d91f119..537f14a 100644 --- a/src/NebulaAuth/App.xaml.cs +++ b/src/NebulaAuth/App.xaml.cs @@ -1,8 +1,9 @@ -using System; -using System.Windows; -using NebulaAuth.Core; +using NebulaAuth.Core; using NebulaAuth.Model; using NebulaAuth.Model.Exceptions; +using NebulaAuth.Model.MAAC; +using System; +using System.Windows; namespace NebulaAuth; @@ -20,6 +21,7 @@ public partial class App Shell.Initialize(); var threads = Environment.ProcessorCount > 0 ? Environment.ProcessorCount : 1; await Storage.Initialize(threads); + MAACStorage.Initialize(); var mainWindow = new MainWindow(); Current.MainWindow = mainWindow; mainWindow.Show(); diff --git a/src/NebulaAuth/Converters/BoolToValueConverter.cs b/src/NebulaAuth/Converters/BoolToValueConverter.cs new file mode 100644 index 0000000..3a31d79 --- /dev/null +++ b/src/NebulaAuth/Converters/BoolToValueConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace NebulaAuth.Converters; + +public class BoolToValueConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not bool b) return Binding.DoNothing; + if (parameter is Array) + { + var arr = (Array)parameter; + if (arr.Length == 0) return Binding.DoNothing; + var first = arr.GetValue(0); + var second = arr.Length > 1 ? arr.GetValue(1) : null; + return b ? first : second; + } + + return b ? parameter : null; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/NebulaAuth/Converters/Converters.xaml b/src/NebulaAuth/Converters/Converters.xaml index 5eaeb62..60d8388 100644 --- a/src/NebulaAuth/Converters/Converters.xaml +++ b/src/NebulaAuth/Converters/Converters.xaml @@ -14,7 +14,7 @@ - + diff --git a/src/NebulaAuth/Core/DialogsController.cs b/src/NebulaAuth/Core/DialogsController.cs index a679800..8888b73 100644 --- a/src/NebulaAuth/Core/DialogsController.cs +++ b/src/NebulaAuth/Core/DialogsController.cs @@ -121,4 +121,14 @@ public static class DialogsController vm?.Dispose(); } } + + public static async Task ShowSetAccountsPasswordDialog() + { + var vm = new SetAccountPasswordsVM(); + var dialog = new SetAccountPasswordsView + { + DataContext = vm + }; + await DialogHost.Show(dialog); + } } \ No newline at end of file diff --git a/src/NebulaAuth/MainWindow.xaml b/src/NebulaAuth/MainWindow.xaml index caaedcb..99e3acb 100644 --- a/src/NebulaAuth/MainWindow.xaml +++ b/src/NebulaAuth/MainWindow.xaml @@ -85,8 +85,17 @@ + + + + + + @@ -314,6 +323,9 @@ + @@ -321,7 +333,11 @@ Header="{Tr MainWindow.ContextMenus.Mafile.CopyLogin}" Command="{Binding CopyLoginCommand}" CommandParameter="{Binding PlacementTarget.SelectedValue, RelativeSource={RelativeSource AncestorType=ContextMenu}}" /> - + - diff --git a/src/NebulaAuth/MainWindow.xaml.cs b/src/NebulaAuth/MainWindow.xaml.cs index fbb3b9d..5f20986 100644 --- a/src/NebulaAuth/MainWindow.xaml.cs +++ b/src/NebulaAuth/MainWindow.xaml.cs @@ -64,7 +64,7 @@ public partial class MainWindow if (ItemsControl.ContainerFromElement((ListBox) sender, (DependencyObject) e.OriginalSource) is ListBoxItem { IsSelected: true - } item) + }) { e.Handled = true; } diff --git a/src/NebulaAuth/Model/Entities/Mafile.cs b/src/NebulaAuth/Model/Entities/Mafile.cs index 59ff59e..3d9ce00 100644 --- a/src/NebulaAuth/Model/Entities/Mafile.cs +++ b/src/NebulaAuth/Model/Entities/Mafile.cs @@ -20,9 +20,16 @@ public partial class Mafile : MobileDataExtended set => SetProperty(ref _linkedClient, value); } - [JsonIgnore] public string? Filename { get; set; } + [JsonIgnore] public string? Filename + { + get => _filename; + set => SetProperty(ref _filename, value); + } + [JsonIgnore] private PortableMaClient? _linkedClient; + private string? _filename; + public void SetSessionData(MobileSessionData? sessionData) { SessionData = sessionData; diff --git a/src/NebulaAuth/Model/MAAC/MAACStorage.cs b/src/NebulaAuth/Model/MAAC/MAACStorage.cs new file mode 100644 index 0000000..e878468 --- /dev/null +++ b/src/NebulaAuth/Model/MAAC/MAACStorage.cs @@ -0,0 +1,139 @@ +using System; +using NebulaAuth.Model.Entities; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.IO; +using System.Linq; +using NebulaAuth.Core; + +namespace NebulaAuth.Model.MAAC; + +public static class MAACStorage +{ + private static Dictionary Clients { get; set; } = []; + + static MAACStorage(){} + + private static void ClientsOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Reset) + { + Clients.Clear(); + } + else if (e.NewItems != null) + { + foreach (var item in e.NewItems) + { + if (item is Mafile { Filename: not null } mafile) + { + if (mafile.LinkedClient != null) + mafile.LinkedClient.PropertyChanged += LinkedClientOnPropertyChanged; + + Clients.Add(mafile.Filename, new StoredClient + { + AutoConfirmMarket = mafile.LinkedClient?.AutoConfirmMarket ?? false, + AutoConfirmTrades = mafile.LinkedClient?.AutoConfirmTrades ?? false + }); + } + } + } + + if (e.OldItems != null) + { + foreach (var item in e.OldItems) + { + if (item is Mafile { Filename: not null } mafile) + { + if (mafile.LinkedClient != null) + mafile.LinkedClient.PropertyChanged -= LinkedClientOnPropertyChanged; + + Clients.Remove(mafile.Filename); + } + } + } + + Save(); + } + + private static void LinkedClientOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (sender is not PortableMaClient client) return; + if (client.Mafile.Filename == null) return; + var anyChanges = false; + if (!Clients.TryGetValue(client.Mafile.Filename, out var storedClient)) + { + client.PropertyChanged -= LinkedClientOnPropertyChanged; + return; + } + if (e.PropertyName == nameof(PortableMaClient.AutoConfirmMarket)) + { + anyChanges = storedClient.AutoConfirmMarket != client.AutoConfirmMarket; + storedClient.AutoConfirmMarket = client.AutoConfirmMarket; + } + else if (e.PropertyName == nameof(PortableMaClient.AutoConfirmTrades)) + { + anyChanges = storedClient.AutoConfirmTrades != client.AutoConfirmTrades; + storedClient.AutoConfirmTrades = client.AutoConfirmTrades; + } + if (anyChanges) + Save(); + } + + public static void Save() + { + var json = JsonConvert.SerializeObject(Clients, Formatting.Indented); + File.WriteAllText("maac.json", json); + } + + public static void Initialize() + { + if (!File.Exists("maac.json")) return; + try + { + var json = File.ReadAllText("maac.json"); + var clients = JsonConvert.DeserializeObject>(json) ?? []; + foreach (var (fileName, storedClient) in clients) + { + var mafile = Storage.MaFiles.FirstOrDefault(x => x.Filename == fileName); + if (mafile == null) continue; + if(storedClient is {AutoConfirmMarket: false, AutoConfirmTrades: false}) continue; + if (MultiAccountAutoConfirmer.TryAddToConfirm(mafile) && mafile.LinkedClient != null) + { + mafile.LinkedClient.AutoConfirmMarket = storedClient.AutoConfirmMarket; + mafile.LinkedClient.AutoConfirmTrades = storedClient.AutoConfirmTrades; + Clients[fileName] = storedClient; + } + } + } + catch(Exception ex) + { + Shell.Logger.Error(ex, "Failed to load MAAC storage"); + SnackbarController.SendSnackbar(LocManager.GetCodeBehindOrDefault("FailedToLoadStorage", "MAAC", "FailedToLoadStorage")); + } + MultiAccountAutoConfirmer.Clients.CollectionChanged += ClientsOnCollectionChanged; + + } + + public static void NotifyMafilesRenamed(IDictionary oldNewNames) + { + var updatedClients = new Dictionary(); + foreach (var (oldName, newName) in oldNewNames) + { + if(Clients.Remove(oldName, out var storedClient)) + { + updatedClients[newName] = storedClient; + } + } + Clients = updatedClients; + Save(); + } + + private class StoredClient + { + public bool AutoConfirmMarket { get; set; } + public bool AutoConfirmTrades { get; set; } + + } +} \ No newline at end of file diff --git a/src/NebulaAuth/Model/MAAC/MultiAccountAutoConfirmer.cs b/src/NebulaAuth/Model/MAAC/MultiAccountAutoConfirmer.cs index eb9823b..b9d0ffc 100644 --- a/src/NebulaAuth/Model/MAAC/MultiAccountAutoConfirmer.cs +++ b/src/NebulaAuth/Model/MAAC/MultiAccountAutoConfirmer.cs @@ -25,7 +25,7 @@ public static class MultiAccountAutoConfirmer Clients = []; Timer = new Timer(TimerConfirm); Settings.Instance.PropertyChanged += SettingsOnPropertyChanged; - UpdateTimer(); + UpdateTimer(true); } // ReSharper disable once AsyncVoidMethod //Already safe @@ -120,8 +120,8 @@ public static class MultiAccountAutoConfirmer return Lock.WriteLock(() => { if (Clients.Contains(mafile)) return false; - Clients.Add(mafile); mafile.LinkedClient = new PortableMaClient(mafile); + Clients.Add(mafile); return true; }); } @@ -131,9 +131,10 @@ public static class MultiAccountAutoConfirmer { Lock.WriteLock(() => { + Clients.Remove(mafile); mafile.LinkedClient?.Dispose(); mafile.LinkedClient = null; - Clients.Remove(mafile); + }); } @@ -144,11 +145,16 @@ public static class MultiAccountAutoConfirmer UpdateTimer(); } - private static void UpdateTimer() + private static void UpdateTimer(bool firstStart = false) { var timerInterval = Settings.Instance.TimerSeconds; var intervalTimeSpan = TimeSpan.FromSeconds(timerInterval); - Timer.Change(intervalTimeSpan, intervalTimeSpan); + var dueTime = intervalTimeSpan; + if (firstStart) + { + dueTime = timerInterval >= 30 ? intervalTimeSpan : TimeSpan.FromSeconds(30); + } + Timer.Change(dueTime, intervalTimeSpan); } private static string GetLocalization(string key) diff --git a/src/NebulaAuth/Model/MAAC/PortableMaClient.cs b/src/NebulaAuth/Model/MAAC/PortableMaClient.cs index 2ec1137..9f103b1 100644 --- a/src/NebulaAuth/Model/MAAC/PortableMaClient.cs +++ b/src/NebulaAuth/Model/MAAC/PortableMaClient.cs @@ -1,12 +1,4 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; -using AchiesUtilities.Web.Models; +using AchiesUtilities.Web.Models; using AchiesUtilities.Web.Proxy; using CommunityToolkit.Mvvm.ComponentModel; using NebulaAuth.Core; @@ -19,6 +11,14 @@ using SteamLib.Exceptions.Authorization; using SteamLib.SteamMobile.Confirmations; using SteamLib.Web; using SteamLibForked.Abstractions; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; namespace NebulaAuth.Model.MAAC; diff --git a/src/NebulaAuth/Model/MaClient.cs b/src/NebulaAuth/Model/MaClient.cs index 66065b2..b37d974 100644 --- a/src/NebulaAuth/Model/MaClient.cs +++ b/src/NebulaAuth/Model/MaClient.cs @@ -20,11 +20,8 @@ namespace NebulaAuth.Model; public static class MaClient { private static HttpClientHandler ClientHandler { get; } - private static HttpClient Client { get; } - private static DynamicProxy Proxy { get; } - public static ProxyData? DefaultProxy { get; set; } diff --git a/src/NebulaAuth/Model/SessionHandler_API.cs b/src/NebulaAuth/Model/SessionHandler_API.cs index 91728ed..3c8337b 100644 --- a/src/NebulaAuth/Model/SessionHandler_API.cs +++ b/src/NebulaAuth/Model/SessionHandler_API.cs @@ -29,7 +29,7 @@ public partial class SessionHandler //API //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); - Storage.UpdateMafile(mafile); + await Storage.UpdateMafileAsync(mafile); chp.Handler.CookieContainer.SetSteamMobileCookiesWithMobileToken(mafile.SessionData); } @@ -53,6 +53,6 @@ public partial class SessionHandler //API mafile.SetSessionData((MobileSessionData) result); if (PHandler.IsPasswordSet) mafile.Password = savePassword ? PHandler.Encrypt(password) : null; - Storage.UpdateMafile(mafile); + await Storage.UpdateMafileAsync(mafile); } } \ No newline at end of file diff --git a/src/NebulaAuth/Model/Shell.cs b/src/NebulaAuth/Model/Shell.cs index f9d598a..f427087 100644 --- a/src/NebulaAuth/Model/Shell.cs +++ b/src/NebulaAuth/Model/Shell.cs @@ -1,10 +1,10 @@ -using System; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using NebulaAuth.Model.Exceptions; using NLog; using NLog.Extensions.Logging; using SteamLib.Core; using SteamLib.SteamMobile; +using System; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace NebulaAuth.Model; @@ -39,7 +39,7 @@ public static class Shell private static void CurrentDomainOnUnhandledException(object sender, UnhandledExceptionEventArgs e) { - Logger.Fatal((Exception) e.ExceptionObject); + Logger.Fatal((Exception)e.ExceptionObject); LogManager.Shutdown(); } } \ No newline at end of file diff --git a/src/NebulaAuth/Model/Storage.cs b/src/NebulaAuth/Model/Storage.cs index 98b40c3..b58cb49 100644 --- a/src/NebulaAuth/Model/Storage.cs +++ b/src/NebulaAuth/Model/Storage.cs @@ -7,6 +7,7 @@ using SteamLib.Exceptions.Authorization; using SteamLib.SteamMobile; using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.IO.Compression; @@ -14,6 +15,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Data; +using NebulaAuth.Model.MAAC; namespace NebulaAuth.Model; @@ -131,12 +133,37 @@ public static class Storage MaFiles.Add(data); } } + + public static async Task SaveMafileAsync(Mafile data) + { + var path = GetOrCreateMafilePath(data); + var str = NebulaSerializer.SerializeMafile(data); + await File.WriteAllTextAsync(Path.GetFullPath(path), str); + + var existed = MaFiles.SingleOrDefault(m => m.AccountName == data.AccountName); + if (existed != null) + { + var index = MaFiles.IndexOf(existed); + MaFiles[index] = data; + } + else + { + MaFiles.Add(data); + } + } public static void UpdateMafile(Mafile data) { var path = GetOrCreateMafilePath(data); var str = NebulaSerializer.SerializeMafile(data); File.WriteAllText(Path.GetFullPath(path), str); } + + public static async Task UpdateMafileAsync(Mafile data) + { + var path = GetOrCreateMafilePath(data); + var str = NebulaSerializer.SerializeMafile(data); + await File.WriteAllTextAsync(Path.GetFullPath(path), str); + } public static void MoveToRemoved(Mafile data) { var sourcePath = GetOrCreateMafilePath(data); @@ -229,19 +256,22 @@ public static class Storage BackupFileName = backupFileName }; + var dic = new Dictionary(); foreach (var mafile in mafiles) { try { + var targetFileName = CreateMafileFileName(mafile, loginAsFileName); if (mafile.Filename == targetFileName || mafile.Filename == null) { res.Renamed += 1; continue; } - + var sourceFileName = mafile.Filename; + var fullSourcePath = Path.Combine(MafilesDirectory, sourceFileName); var fullTargetPath = Path.Combine(MafilesDirectory, targetFileName); - var fullSourcePath = Path.Combine(MafilesDirectory, mafile.Filename); + if (!File.Exists(fullSourcePath)) { Shell.Logger.Warn("Can't rename mafile {old} to {new} because source file not found", @@ -254,11 +284,11 @@ public static class Storage { Shell.Logger.Warn("Can't rename mafile {old} to {new} because target file already exist", mafile.Filename, targetFileName); - res.AlreadyExist += 1; + res.Conflict += 1; continue; } - File.Move(fullSourcePath, fullTargetPath); + dic[sourceFileName] = targetFileName; res.Renamed += 1; mafile.Filename = targetFileName; @@ -269,8 +299,9 @@ public static class Storage mafile.AccountName); IncErrors(); } - } + + MAACStorage.NotifyMafilesRenamed(dic); return res; void IncErrors() => res.Errors += 1; @@ -280,9 +311,9 @@ public static class Storage { public int Total { get; set; } public int Renamed { get; set; } - public int NotRenamed => Errors + AlreadyExist; + public int NotRenamed => Errors + Conflict; public int Errors { get; set; } - public int AlreadyExist { get; set; } + public int Conflict { get; set; } public string BackupFileName { get; set; } } } \ No newline at end of file diff --git a/src/NebulaAuth/View/Dialogs/ConfirmCancelDialog.xaml b/src/NebulaAuth/View/Dialogs/ConfirmCancelDialog.xaml index eb8b5f6..88b7304 100644 --- a/src/NebulaAuth/View/Dialogs/ConfirmCancelDialog.xaml +++ b/src/NebulaAuth/View/Dialogs/ConfirmCancelDialog.xaml @@ -55,7 +55,8 @@ Style="{StaticResource MaterialDesignRaisedButton}" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" CommandParameter="{StaticResource True}" - Content="{Tr Common.OK}"/> + Content="{Tr Common.OK}" + IsDefault="True"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + CheckBox + CloseBox + + + + + + + + + + + + + + + + + + + + + + + + + +