Compare commits

..

11 Commits

Author SHA1 Message Date
github-actions 5de26381bb chore(release): 1.8.1 2026-01-25 17:25:47 +00:00
Achies 16fd4992f8 Merge pull request #14 from achiez/1.8.1
Release 1.8.1 → Merge into master
2026-01-25 19:25:39 +02:00
achiez 7835a53717 Prepare 1.8.1
- Added changelog/1.8.1.html with detailed update notes and links
- Improved MAAC auto-confirmation logic: now uses IsReady() for smarter account readiness checks and retries
- Added GetReadyAccounts() to MultiAccountAutoConfirmer for flexible account selection
- Fixed localization lookup in GetPortableMaClientStatus
- Set PortableMaClient initial status to Ok instead of test error
2026-01-25 19:21:50 +02:00
achiez f15632f2d0 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
2026-01-25 18:48:11 +02:00
achiez b56c0e578d Fix checkbox margin accordingly to new MaterialDesignInXaml update 2026-01-19 21:36:42 +02:00
achiez 969590d9f2 Add MafileExport feature and Perform Resharper Cleanup 2026-01-19 21:28:44 +02:00
achiez 3e87a0d50e Improve localization structure and add missing translations
Thanks to @77155331 for Chinese translation (Issue #11)
Thanks to @klNuno for French localization (PR #13)
2026-01-02 22:43:24 +02:00
achiez 690c98527f Update mafiles removal logic and changelog URL generation
- Fixed crash: MaFiles collection in MainVM is now a new ObservableCollection. Manual removal from collection is omitted
- Improved logic for moving mafiles to "removed": file extension is now always correct and name conflicts are handled more reliably
- Github workflow: Changelog URL in update.xml now uses repository name only for correct links
- On MaFiles collection change, PerformQuery is called to update search results
- Fixed parameter order in mafiles renaming result string to match localization
2026-01-02 22:09:31 +02:00
achiez 5242b0009b Refactor mafile storage: async, modular, bulk rename
- Fix crash: Updated SaveMafileAsync to properly update or add to MaFiles collection
- Fix naming logic: Added Filename reset logic to adding new mafile to prevent inconsistent and duplicating import
- Extracted mafile path and naming logic to MafilesStorageHelper for better modularity
- Refactored Storage methods to use MafilesStorageHelper
- Replaced all sync mafile save/add calls with async/await versions throughout the codebase
- Performed code cleanups and improved maintainability for mafile-related operations
2026-01-02 21:23:11 +02:00
Achies 1f14bd77e7 chore(update.xml) Fix changelog URL in update.xml 2025-11-14 22:35:39 +02:00
achiez f2b1cdfcc9 Fix MAACStorage.cs
- fixed collection was untracked if no `maac.json` file was created
- fixed incorrect change tracking for persisted accounts after reboot
2025-11-07 21:58:52 +02:00
66 changed files with 2927 additions and 732 deletions
+2 -2
View File
@@ -34,7 +34,7 @@ jobs:
<item>
<version>${{ steps.ver.outputs.version }}</version>
<url>https://github.com/${{ github.repository }}/releases/download/${{ steps.ver.outputs.version }}/NebulaAuth.${{ steps.ver.outputs.version }}.zip</url>
<changelog>https://achiez.github.io/${{ github.repository }}/changelog/${{ steps.ver.outputs.version }}.html</changelog>
<changelog>https://achiez.github.io/${{ github.event.repository.name }}/changelog/${{ steps.ver.outputs.version }}.html</changelog>
<mandatory>false</mandatory>
</item>
EOF
@@ -62,4 +62,4 @@ jobs:
- name: Skip message
if: steps.diff.outputs.no_changes == 'true'
run: echo "No changes in version or update.xml skipping release preparation."
run: echo "No changes in version or update.xml - skipping release preparation."
+1
View File
@@ -24,6 +24,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "changelog", "changelog", "{
changelog\1.7.3.html = changelog\1.7.3.html
changelog\1.7.4.html = changelog\1.7.4.html
changelog\1.8.0.html = changelog\1.8.0.html
changelog\1.8.1.html = changelog\1.8.1.html
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SteamLibForked", "src\SteamLibForked\SteamLibForked.csproj", "{224F9DB0-3D20-A614-BA2A-12F22B13A2C6}"
+3 -3
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>1.8.0</version>
<url>https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/download/1.8.0/NebulaAuth.1.8.0.zip</url>
<changelog>https://achiez.github.io/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/1.8.0.html</changelog>
<version>1.8.1</version>
<url>https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/releases/download/1.8.1/NebulaAuth.1.8.1.zip</url>
<changelog>https://achiez.github.io/NebulaAuth-Steam-Desktop-Authenticator-by-Achies/changelog/1.8.1.html</changelog>
<mandatory>false</mandatory>
</item>
+87
View File
@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Changelog</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #eeeeee;
color: #333;
line-height: 1.6;
}
.changelog-container {
background-color: #fff;
border-radius: 10px;
padding: 25px;
margin: 25px auto;
max-width: 800px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.change {
margin-bottom: 20px;
padding: 0;
border-left: 4px solid #a50ec7;
background-color: #f9f9f9;
}
.version {
font-weight: 600;
font-size: 1.5em;
color: #a50ec7;
margin-left: 10px;
}
.date {
font-style: italic;
color: #888;
margin-bottom: 10px;
margin-left: 15px;
}
.description {
font-size: 1em;
padding: 0 15px;
}
.description ul {
list-style: inside square;
padding: 0;
}
@media only screen and (max-width: 600px) {
.changelog-container {
width: 90%;
margin: 25px auto;
padding: 25px;
}
}
</style>
</head>
<body>
<div class="changelog-container">
<div class="change">
<div class="version">Version 1.8.1</div>
<div class="date">25.01.2026</div>
<div class="description">
<ul>
<li><b>NEW:</b> Added Mafile Export tool with templates, batch export and flexible field filtering.</li>
<li><b>IMPROVEMENT:</b> Reworked auto-confirmation error handling logic (MAAC) — fewer false disables, smarter retries.</li>
<li><b>UPDATE:</b> Added “Unattach Proxy” option to the account context menu.</li>
<li><b>LOCALIZATION:</b> Added Chinese (Simplified) and French languages.</li>
<li><b>FIX:</b> Fixed crashes related to duplicate mafiles, timers, search filtering and import logic.</li>
<li>
<b>DETAILS:</b> Full patch notes:
<a href="https://teletype.in/@achies_raw/nebula-1-8-1-eng">ENG</a> |
<a href="https://teletype.in/@achies_raw/nebula-1-8-1-rus">RUS</a>
</li>
</ul>
</div>
</div>
</div>
</body>
</html>
+2 -7
View File
@@ -3,7 +3,6 @@ using System.Windows;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Exceptions;
using NebulaAuth.Model.MAAC;
namespace NebulaAuth;
@@ -16,12 +15,8 @@ public partial class App
var splashScreen = new SplashScreen("Theme\\SplashScreen.png");
splashScreen.Show(false, true);
base.OnStartup(e);
LocManager.Init();
LocManager.SetApplicationLocalization(Settings.Instance.Language);
Shell.Initialize();
var threads = Environment.ProcessorCount > 0 ? Environment.ProcessorCount : 1;
await Storage.Initialize(threads);
MAACStorage.Initialize();
await Shell.Initialize();
var mainWindow = new MainWindow();
Current.MainWindow = mainWindow;
mainWindow.Show();
+1 -1
View File
@@ -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" />
@@ -23,6 +22,7 @@
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<materialDesign:NullableToVisibilityConverter x:Key="NullableToVisibilityConverter" />
<converters:InverseBooleanToVisibilityConverter x:Key="InverseBooleanToVisibilityConverter" />
<system:Boolean x:Key="True">True</system:Boolean>
<system:Boolean x:Key="False">False</system:Boolean>
@@ -1,25 +1,19 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
namespace NebulaAuth.Converters;
public class PortableMaClientStatusToColorConverter : IValueConverter
public class InverseBooleanToVisibilityConverter : 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));
return value is bool b && !b ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotSupportedException();
return value is Visibility v && v != Visibility.Visible;
}
}
+10
View File
@@ -113,6 +113,16 @@ public static class DialogsController
await DialogHost.Show(dialog);
}
public static async Task ShowExportDialog()
{
var vm = new MafileExporterVM();
var dialog = new MafileExporterView
{
DataContext = vm
};
await DialogHost.Show(dialog);
}
#region CommonDialogs
public static async Task<bool> ShowConfirmCancelDialog(string? msg = null)
+5 -1
View File
@@ -31,6 +31,8 @@ public static class LocManager
LocalizationLanguage.English => "en",
LocalizationLanguage.Russian => "ru",
LocalizationLanguage.Ukrainian => "ua",
LocalizationLanguage.ChineseSimplified => "zh",
LocalizationLanguage.French => "fr",
_ => throw new ArgumentOutOfRangeException(nameof(language), language, null)
};
}
@@ -107,5 +109,7 @@ public enum LocalizationLanguage
{
English,
Russian,
Ukrainian
Ukrainian,
ChineseSimplified,
French
}
+1 -1
View File
@@ -69,7 +69,7 @@ public static class TrayManager
private static void HideMainWindow()
{
if (!Init) return;
if (IsEnabled == false) return;
if (!IsEnabled) return;
_notifyIcon.Visible = true;
MainWindow.Hide();
}
+42 -14
View File
@@ -73,7 +73,7 @@
Kind="FileReplaceOutline" />
</StackPanel>
<ToolBarTray>
<ToolBarTray Grid.Row="0">
<ToolBar ClipToBounds="True" HorizontalContentAlignment="Stretch"
Style="{StaticResource MaterialDesignToolBar}" FontSize="16">
<Menu FontSize="16">
@@ -92,7 +92,9 @@
<MenuItem IsEnabled="False" Header="⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯" Height="15" />
<MenuItem Header="{Tr MainWindow.Menu.File.Other.Title}">
<MenuItem Header="{Tr MainWindow.Menu.File.Other.SetPasswords}"
Command="{Binding DebugCommand}" />
Command="{Binding OpenSetPasswordsDialogCommand}" />
<MenuItem Header="{Tr MainWindow.Menu.File.Other.OpenExport}"
Command="{Binding OpenExporterDialogCommand}" />
</MenuItem>
</MenuItem>
</Menu>
@@ -292,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">
@@ -356,7 +382,7 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<md:PackIcon Kind="PlusCircle"
VerticalAlignment="Center" />
VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center"
Margin="5,0,0,0" Grid.Column="1"
Text="{Tr MainWindow.ContextMenus.Mafile.CreateGroup}" />
@@ -391,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>
+5 -5
View File
@@ -30,7 +30,7 @@ public partial class MainWindow
private async void OnApplicationStarted(object? sender, EventArgs e)
{
((MainVM) DataContext).CurrentDialogHost = DialogHostInstance;
if (Settings.Instance.IsPasswordSet == false) return;
if (!Settings.Instance.IsPasswordSet) return;
Topmost = false;
await Dispatcher.InvokeAsync(ShowSetPasswordDialog, DispatcherPriority.ContextIdle);
}
@@ -46,7 +46,7 @@ public partial class MainWindow
var result = await DialogHost.Show(dialog);
var pass = vm.Password;
if (result is true && string.IsNullOrWhiteSpace(pass) == false)
if (result is true && !string.IsNullOrWhiteSpace(pass))
{
PHandler.SetPassword(pass);
}
@@ -79,12 +79,12 @@ public partial class MainWindow
private void UIElement_OnPreviewTextInput(object sender, TextCompositionEventArgs e)
{
if (int.TryParse(e.Text, out _) == false) e.Handled = true;
if (!int.TryParse(e.Text, out _)) e.Handled = true;
}
private void Rectangle_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop) == false)
if (!e.Data.GetDataPresent(DataFormats.FileDrop))
{
e.Handled = true;
return;
@@ -96,7 +96,7 @@ public partial class MainWindow
private async void Rectangle_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) return;
if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
var filePaths = (string[]) e.Data.GetData(DataFormats.FileDrop)!;
if (filePaths.Length == 0) return;
if (DataContext is MainVM mainVm)
-1
View File
@@ -28,7 +28,6 @@ public partial class Mafile : MobileDataExtended
}
private string? _filename;
[JsonIgnore] private PortableMaClient? _linkedClient;
public void SetSessionData(MobileSessionData? sessionData)
@@ -0,0 +1,174 @@
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;
}
public static bool IsReady(Mafile maf)
{
if (maf.LinkedClient is {Status.StatusType: PortableMaClientStatusType.Ok}) return true;
return maf.LinkedClient is {Status.StatusType: PortableMaClientStatusType.Warning}
&& maf.LastErrorWasAtLeast(Settings.Instance.MaacRetryInterval);
}
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, "PortableMaClientStatus", key);
}
}
+3 -2
View File
@@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using NebulaAuth.Core;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using Newtonsoft.Json;
namespace NebulaAuth.Model.MAAC;
@@ -93,6 +94,7 @@ public static class MAACStorage
public static void Initialize()
{
MultiAccountAutoConfirmer.Clients.CollectionChanged += ClientsOnCollectionChanged;
if (!File.Exists("maac.json")) return;
try
{
@@ -107,6 +109,7 @@ public static class MAACStorage
{
mafile.LinkedClient.AutoConfirmMarket = storedClient.AutoConfirmMarket;
mafile.LinkedClient.AutoConfirmTrades = storedClient.AutoConfirmTrades;
mafile.LinkedClient.PropertyChanged += LinkedClientOnPropertyChanged;
Clients[fileName] = storedClient;
}
}
@@ -117,8 +120,6 @@ public static class MAACStorage
SnackbarController.SendSnackbar(
LocManager.GetCodeBehindOrDefault("FailedToLoadStorage", "MAAC", "FailedToLoadStorage"));
}
MultiAccountAutoConfirmer.Clients.CollectionChanged += ClientsOnCollectionChanged;
}
public static void NotifyMafilesRenamed(IDictionary<string, string> oldNewNames)
@@ -59,7 +59,7 @@ 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 = GetReadyAccounts();
enabledClients = DistributeEvenly(enabledClients).ToArray();
var confirmed = 0;
await Task.Run(async () =>
@@ -69,7 +69,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 +115,21 @@ public static class MultiAccountAutoConfirmer
}
}
private static IEnumerable<Mafile> GetReadyAccounts()
{
var clients = Lock.ReadLock(() => Clients.ToArray());
var res = new List<Mafile>();
foreach (var maf in clients)
{
if(maf.LinkedClient == null) continue;
if (MAACRequestHandler.IsReady(maf))
{
res.Add(maf);
}
}
return res;
}
public static bool TryAddToConfirm(Mafile mafile)
{
return Lock.WriteLock(() =>
@@ -122,6 +137,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 +150,7 @@ public static class MultiAccountAutoConfirmer
Clients.Remove(mafile);
mafile.LinkedClient?.Dispose();
mafile.LinkedClient = null;
MAACRequestHandler.Unregister(mafile);
});
}
+22 -95
View File
@@ -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.Ok());
}
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);
}
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(sessionData);
}
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,20 +86,12 @@ 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));
//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;
}
Shell.Logger.Debug("Timer {accountName}: Sending confirmations. Count: {count}", Mafile.AccountName,
toConfirm.Count);
var success = await SendConfirmations(toConfirm);
//TODO: handle success == false
Shell.Logger.Debug("Timer {accountName}: Confirmation sent: {confirmResult}", Mafile.AccountName, success);
return toConfirm.Count;
}
@@ -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 == false) 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(() => IsError = true);
Shell.Logger.Warn("Timer {accountName}: disabled due to error.", Mafile.AccountName);
SnackbarController.SendSnackbar(GetTimerPrefix() + GetLocalization("TimerSessionError"));
Application.Current.Dispatcher.Invoke(() => Status = status);
if (status.StatusType == PortableMaClientStatusType.Error)
{
Shell.Logger.Warn("Timer {accountName}: disabled due to error.", Mafile.AccountName);
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
}
+29
View File
@@ -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);
}
}
+2 -12
View File
@@ -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);
}
ClientHandler.CookieContainer.SetSteamMobileCookiesWithMobileToken(account.SessionData);
Proxy.SetData(account.Proxy?.Data);
}
@@ -153,7 +143,7 @@ public static class MaClient
if (mafile.SessionData.RefreshToken.IsExpired)
throw new SessionPermanentlyExpiredException();
if (ignoreAccessToken == false)
if (!ignoreAccessToken)
{
var access = mafile.SessionData.GetMobileToken();
if (access == null || access.Value.IsExpired)
@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using NebulaAuth.Model.Entities;
namespace NebulaAuth.Model.MafileExport;
public class ExportResult
{
public string? Error { get; }
public Dictionary<Mafile, string>? Exported { get; }
public List<string>? NotFound { get; }
public List<string>? Conflict { get; }
[MemberNotNullWhen(true, nameof(Exported))]
[MemberNotNullWhen(true, nameof(NotFound))]
[MemberNotNullWhen(true, nameof(Conflict))]
[MemberNotNullWhen(false, nameof(Error))]
public bool Success => Error == null;
private ExportResult(string? error, Dictionary<Mafile, string>? exported, List<string>? notFound,
List<string>? conflict)
{
Error = error;
Exported = exported;
NotFound = notFound;
Conflict = conflict;
}
public ExportResult(Dictionary<Mafile, string> exported, List<string> notFound, List<string> conflict)
: this(null, exported, notFound, conflict)
{
}
public ExportResult(string? error)
: this(error, null, null, null)
{
}
}
@@ -0,0 +1,16 @@
namespace NebulaAuth.Model.MafileExport;
public class MafileExportTemplate
{
public string Name { get; set; }
public bool UseLoginAsMafileName { get; set; }
public bool IncludeSharedSecret { get; set; }
public bool IncludeIdentitySecret { get; set; }
public bool IncludeRCode { get; set; }
public bool IncludeSessionData { get; set; }
public bool IncludeOtherInfo { get; set; }
public bool IncludeNebulaProxy { get; set; }
public bool IncludeNebulaPassword { get; set; }
public bool IncludeNebulaGroup { get; set; }
public string? Path { get; set; }
}
@@ -0,0 +1,166 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
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;
public static class MafileExporter
{
public static async Task<ExportResult> ExportMafiles(IEnumerable<string> keys, MafileExportTemplate template)
{
if (template.Path == null || !PathIsValid(template.Path))
{
return new ExportResult(LocManager.GetCodeBehindOrDefault("InvalidPath", "MafileExporter.InvalidPath"));
}
if (!Directory.Exists(template.Path))
{
Directory.CreateDirectory(template.Path);
}
if (IsNebulaUtilizedDirectory(template.Path))
{
return new ExportResult(LocManager.GetCodeBehindOrDefault("NebulaUtilizedDirectory",
"MafileExporter.NebulaUtilizedDirectory"));
}
if (!IsAllowedToWrite(template.Path))
{
return new ExportResult(LocManager.GetCodeBehindOrDefault("AccessDenied", "MafileExporter.AccessDenied"));
}
var exported = new Dictionary<Mafile, string>();
var notFound = new List<string>();
var conflict = new List<string>();
foreach (var key in keys)
{
SteamId? steamId = null;
if (SteamId64.TryParse(key, out var id64))
{
steamId = id64;
}
var maf = Storage.MaFiles.FirstOrDefault(m => m.AccountName.EqualsIgnoreCase(key) || m.SteamId == steamId);
if (maf != null)
{
var fileName = await ExportMafile(template.Path, template, maf);
if (fileName == null)
{
conflict.Add(key);
continue;
}
exported[maf] = fileName;
}
else
{
notFound.Add(key);
}
}
return new ExportResult(exported, notFound, conflict);
}
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!,
IdentitySecret = template.IncludeIdentitySecret ? mafile.IdentitySecret : null!,
DeviceId = template.IncludeIdentitySecret ? mafile.DeviceId : null!,
RevocationCode = template.IncludeRCode ? mafile.RevocationCode : null!,
AccountName = mafile.AccountName,
SessionData = session,
SteamId = mafile.SteamId,
ServerTime = template.IncludeOtherInfo ? mafile.ServerTime : 0,
SerialNumber = template.IncludeOtherInfo ? mafile.SerialNumber : 0,
Uri = template.IncludeOtherInfo ? mafile.Uri : null!,
TokenGid = template.IncludeOtherInfo ? mafile.TokenGid : null!,
Secret1 = template.IncludeOtherInfo ? mafile.Secret1 : null!,
Proxy = template.IncludeNebulaProxy ? mafile.Proxy : null!,
Group = template.IncludeNebulaGroup ? mafile.Group : null!,
Password = template.IncludeNebulaPassword ? mafile.Password : null!,
LinkedClient = null,
Filename = null
};
var serialized = NebulaSerializer.SerializeMafile(serializeMaf);
var strategy = template.UseLoginAsMafileName ? MafileNamingStrategy.Login : MafileNamingStrategy.SteamId;
var fileName = strategy.GetMafileName(serializeMaf);
var fullPath = Path.Combine(path, fileName);
if (File.Exists(fullPath))
{
return null;
}
await File.WriteAllTextAsync(fullPath, serialized);
return fullPath;
}
private static bool PathIsValid(string path)
{
if (string.IsNullOrWhiteSpace(path)) return false;
try
{
var full = Path.GetFullPath(path);
var invalidPathChars = Path.GetInvalidPathChars();
if (full.IndexOfAny(invalidPathChars) >= 0) return false;
var invalidFileChars = Path.GetInvalidFileNameChars();
foreach (var part in full.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))
{
if (string.IsNullOrEmpty(part)) continue;
if (part is [_, ':']) continue;
if (part.IndexOfAny(invalidFileChars) >= 0) return false;
}
return true;
}
catch
{
return false;
}
}
private static bool IsAllowedToWrite(string path)
{
try
{
var testFilePath = Path.Combine(path, "test.tmp");
using (var fs = File.Create(testFilePath))
{
}
File.Delete(testFilePath);
return true;
}
catch
{
return false;
}
}
private static bool IsNebulaUtilizedDirectory(string path)
{
return Storage.MafilesDirectories.Any(x => Path.GetFullPath(path).EqualsIgnoreCase(x));
}
}
@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
namespace NebulaAuth.Model.MafileExport;
public static class MafileExporterStorage
{
private const string FILE_NAME = "export_templates.json";
public static ObservableCollection<MafileExportTemplate> Templates { get; } = new();
public static void Initialize()
{
LoadOrCreateTemplate();
}
public static MafileExportTemplate? GetTemplate(string name)
{
return Templates.FirstOrDefault(x => x.Name == name);
}
public static void AddTemplate(MafileExportTemplate template)
{
Templates.Add(template);
Save();
}
public static void DeleteTemplate(MafileExportTemplate template)
{
if (Templates.Remove(template))
{
Save();
}
}
private static void LoadOrCreateTemplate()
{
if (!File.Exists(FILE_NAME))
{
Templates.Clear();
foreach (var mafileExportTemplate in CreateDefaultTemplate())
{
Templates.Add(mafileExportTemplate);
}
Save();
return;
}
var json = File.ReadAllText(FILE_NAME);
try
{
var loadedTemplates = JsonConvert.DeserializeObject<List<MafileExportTemplate>>(json);
if (loadedTemplates != null)
{
Templates.Clear();
foreach (var tmpl in loadedTemplates)
{
Templates.Add(tmpl);
}
}
}
catch (Exception ex)
{
Shell.Logger.Error(ex, $"Failed to deserialize {FILE_NAME}");
Templates.Clear();
Save();
}
}
public static void Save()
{
var json = JsonConvert.SerializeObject(Templates, Formatting.Indented);
File.WriteAllText(FILE_NAME, json);
}
private static IEnumerable<MafileExportTemplate> CreateDefaultTemplate()
{
return
[
new MafileExportTemplate
{
Name = "Default",
UseLoginAsMafileName = false,
IncludeSharedSecret = true,
IncludeIdentitySecret = true,
IncludeRCode = true,
IncludeSessionData = true,
IncludeOtherInfo = true,
IncludeNebulaProxy = true,
IncludeNebulaPassword = true,
IncludeNebulaGroup = true,
Path = null
},
new MafileExportTemplate
{
Name = "Auth only",
UseLoginAsMafileName = true,
IncludeSharedSecret = true,
IncludeIdentitySecret = false,
IncludeRCode = false,
IncludeSessionData = false,
IncludeOtherInfo = false,
IncludeNebulaProxy = false,
IncludeNebulaPassword = false,
IncludeNebulaGroup = false,
Path = null
}
];
}
}
@@ -0,0 +1,11 @@
namespace NebulaAuth.Model.Mafiles;
public class MafilesBulkRenameResult
{
public int Total { get; set; }
public int Renamed { get; set; }
public int NotRenamed => Errors + Conflict;
public int Errors { get; set; }
public int Conflict { get; set; }
public string BackupFileName { get; set; }
}
@@ -0,0 +1,38 @@
using System.IO;
using NebulaAuth.Model.Entities;
namespace NebulaAuth.Model.Mafiles;
internal static class MafilesStorageHelper
{
/// <summary>
/// Returns <see cref="Mafile.Filename" /> or creates a new one and updates the property.
/// <see cref="Mafile.Filename" /> is always not <see langword="null" /> after this method call.
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public static string GetOrUpdateMafilePath(Mafile data)
{
if (data.Filename != null)
{
return Path.Combine(Storage.MafilesDirectory, data.Filename);
}
var fileName = CreateMafileFileName(data, Settings.Instance.UseAccountNameAsMafileName);
data.Filename = fileName;
return Path.Combine(Storage.MafilesDirectory, fileName);
}
/// <summary>
/// Creates mafile file name according to the current settings.
/// </summary>
/// <param name="data"></param>
/// <param name="useAccountName"></param>
/// <returns></returns>
public static string CreateMafileFileName(Mafile data, bool useAccountName)
{
return useAccountName
? MafileNamingStrategy.Login.GetMafileName(data)
: MafileNamingStrategy.SteamId.GetMafileName(data);
}
}
@@ -11,24 +11,30 @@ using AchiesUtilities.Extensions;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Exceptions;
using NebulaAuth.Model.MAAC;
using NebulaAuth.Model.Mafiles;
using SteamLib;
using SteamLib.Exceptions.Authorization;
using SteamLib.SteamMobile;
namespace NebulaAuth.Model;
namespace NebulaAuth.Model.Mafiles;
public static class Storage
{
public const string DIR_MAFILES = "maFiles";
public const string DIR_REMOVED_MAFILES = "maFiles_removed";
public const string DIR_BACKUP_MAFILES = "maFiles_backup";
public static readonly string[] MafilesDirectories;
public static string MafilesDirectory { get; } = Path.GetFullPath(DIR_MAFILES);
public static string RemovedMafilesDirectory { get; } = Path.GetFullPath(DIR_REMOVED_MAFILES);
public static string BackupMafilesDirectory { get; } = Path.GetFullPath(DIR_BACKUP_MAFILES);
public static ObservableCollection<Mafile> MaFiles { get; private set; } = [];
static Storage()
{
MafilesDirectories = [MafilesDirectory, RemovedMafilesDirectory, BackupMafilesDirectory];
}
public static async Task Initialize(int threadCount, CancellationToken token = default)
{
Directory.CreateDirectory(MafilesDirectory);
@@ -70,12 +76,13 @@ public static class Storage
/// <exception cref="FormatException"></exception>
/// <exception cref="SessionInvalidException"></exception>
/// <exception cref="IOException"></exception>
public static void AddNewMafile(string path, bool overwrite)
public static async Task AddNewMafile(string path, bool overwrite)
{
Mafile data;
try
{
data = ReadMafile(path);
data = await ReadMafileAsync(path);
data.Filename = null;
}
catch (Exception ex)
when (ex is not MafileNeedReloginException)
@@ -96,19 +103,13 @@ public static class Storage
throw new FormatException("Can't generate code on this mafile", ex);
}
if (overwrite == false && File.Exists(GetOrCreateMafilePath(data)))
if (!overwrite && File.Exists(MafilesStorageHelper.GetOrUpdateMafilePath(data)))
{
throw new IOException("File already exist and overwrite is False"); //TODO: Custom Exception
}
SaveMafile(data);
}
public static Mafile ReadMafile(string path)
{
var str = File.ReadAllText(path);
return NebulaSerializer.Deserialize(str, path);
await SaveMafileAsync(data);
}
public static async Task<Mafile> ReadMafileAsync(string path)
@@ -117,66 +118,52 @@ public static class Storage
return NebulaSerializer.Deserialize(str, path);
}
public static void SaveMafile(Mafile data)
{
var path = GetOrCreateMafilePath(data);
var str = NebulaSerializer.SerializeMafile(data);
File.WriteAllText(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 async Task SaveMafileAsync(Mafile data)
{
var path = GetOrCreateMafilePath(data);
var path = MafilesStorageHelper.GetOrUpdateMafilePath(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)
// Replace if exist
for (var i = 0; i < MaFiles.Count; i++)
{
var index = MaFiles.IndexOf(existed);
MaFiles[index] = data;
}
else
{
MaFiles.Add(data);
var existed = MaFiles[i];
if (ReferenceEquals(existed, data) || (existed.Filename != null && existed.Filename == data.Filename))
{
MaFiles[i] = data;
return;
}
}
MaFiles.Add(data);
}
public static void UpdateMafile(Mafile data)
{
var path = GetOrCreateMafilePath(data);
var path = MafilesStorageHelper.GetOrUpdateMafilePath(data);
var str = NebulaSerializer.SerializeMafile(data);
File.WriteAllText(Path.GetFullPath(path), str);
}
public static async Task UpdateMafileAsync(Mafile data)
{
var path = GetOrCreateMafilePath(data);
var path = MafilesStorageHelper.GetOrUpdateMafilePath(data);
var str = NebulaSerializer.SerializeMafile(data);
await File.WriteAllTextAsync(Path.GetFullPath(path), str);
}
public static void MoveToRemoved(Mafile data)
{
var sourcePath = GetOrCreateMafilePath(data);
var destinationPath = Path.Combine(DIR_REMOVED_MAFILES, data.Filename + ".mafile");
var destinationPathFinal = destinationPath;
var sourcePath = MafilesStorageHelper.GetOrUpdateMafilePath(data);
var fileName = Path.GetFileNameWithoutExtension(data.Filename!);
var destinationPathBase = Path.Combine(DIR_REMOVED_MAFILES, fileName);
var destinationPathFinal = Path.ChangeExtension(destinationPathBase, MafileNamingStrategy.DEF_EXTENSION);
var i = 0;
while (File.Exists(destinationPathFinal))
{
i++;
destinationPathFinal = destinationPath + $" ({i})";
destinationPathFinal = destinationPathBase + $" ({i})";
destinationPathFinal = Path.ChangeExtension(destinationPathFinal, MafileNamingStrategy.DEF_EXTENSION);
}
File.Copy(sourcePath, destinationPathFinal, false);
@@ -184,24 +171,6 @@ public static class Storage
MaFiles.Remove(data);
}
private static string GetOrCreateMafilePath(Mafile data)
{
if (data.Filename != null)
{
return Path.Combine(MafilesDirectory, data.Filename);
}
var fileName = CreateMafileFileName(data, Settings.Instance.UseAccountNameAsMafileName);
data.Filename = fileName;
return Path.Combine(MafilesDirectory, fileName);
}
private static string CreateMafileFileName(Mafile data, bool useAccountName)
{
return useAccountName
? MafileNamingStrategy.Login.GetMafileName(data)
: MafileNamingStrategy.SteamId.GetMafileName(data);
}
public static string? TryGetMafilePath(Mafile data)
{
@@ -221,9 +190,10 @@ public static class Storage
File.WriteAllText(Path.Combine(DIR_BACKUP_MAFILES, accountName + MafileNamingStrategy.DEF_EXTENSION), data);
}
public static async Task<MafileRenameResult> RenameMafiles(bool loginAsFileName, IProgress<double>? progress = null)
public static async Task<MafilesBulkRenameResult> RenameMafiles(bool loginAsFileName,
IProgress<double>? progress = null)
{
if (MaFiles.Count == 0) return new MafileRenameResult();
if (MaFiles.Count == 0) return new MafilesBulkRenameResult();
var now = DateTime.Now;
var backupFileName = $"rename_backup_{now:yyyy-MM-dd_HH-mm-ss}.zip";
var zipPath = Path.Combine(BackupMafilesDirectory, backupFileName);
@@ -255,7 +225,7 @@ public static class Storage
}
var mafiles = MaFiles.ToList();
var res = new MafileRenameResult
var res = new MafilesBulkRenameResult
{
Total = mafiles.Count,
BackupFileName = backupFileName
@@ -266,7 +236,7 @@ public static class Storage
{
try
{
var targetFileName = CreateMafileFileName(mafile, loginAsFileName);
var targetFileName = MafilesStorageHelper.CreateMafileFileName(mafile, loginAsFileName);
if (mafile.Filename == targetFileName || mafile.Filename == null)
{
res.Renamed += 1;
@@ -314,14 +284,4 @@ public static class Storage
res.Errors += 1;
}
}
public class MafileRenameResult
{
public int Total { get; set; }
public int Renamed { get; set; }
public int NotRenamed => Errors + Conflict;
public int Errors { get; set; }
public int Conflict { get; set; }
public string BackupFileName { get; set; }
}
}
+2 -2
View File
@@ -38,7 +38,7 @@ public static class NebulaSerializer
var data = Serializer.Deserialize(cont);
var mobileData = data.Data;
var info = data.Info;
if (info.IsExtended == false)
if (!info.IsExtended)
throw new FormatException("Mafile is not extended data");
@@ -58,7 +58,7 @@ public static class NebulaSerializer
private static T? GetPropertyValue<T>(string name, Dictionary<string, JProperty> dictionary)
{
if (dictionary.TryGetValue(name, out var prop) == false) return default;
if (!dictionary.TryGetValue(name, out var prop)) return default;
var value = prop.Value;
try
{
+1 -1
View File
@@ -28,7 +28,7 @@ public static class ProxyStorage
static ProxyStorage()
{
if (File.Exists("proxies.json") == false)
if (!File.Exists("proxies.json"))
return;
try
{
@@ -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 == 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);
}
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
@@ -1,6 +1,7 @@
using System.Threading.Tasks;
using AchiesUtilities.Web.Models;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using SteamLib.Api.Mobile;
using SteamLib.Authentication;
using SteamLib.Authentication.LoginV2;
@@ -27,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);
@@ -49,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;
}
}
+4 -2
View File
@@ -25,7 +25,7 @@ public partial class Settings : ObservableObject
static Settings()
{
if (File.Exists("settings.json") == false)
if (!File.Exists("settings.json"))
{
Instance = new Settings();
Instance.PropertyChanged += SettingsOnPropertyChanged;
@@ -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
}
+16 -2
View File
@@ -1,6 +1,12 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NebulaAuth.Core;
using NebulaAuth.Model.Exceptions;
using NebulaAuth.Model.MAAC;
using NebulaAuth.Model.MafileExport;
using NebulaAuth.Model.Mafiles;
using NLog;
using NLog.Extensions.Logging;
using SteamLib.Core;
@@ -14,8 +20,11 @@ public static class Shell
public static Logger Logger { get; private set; } = null!;
public static ILogger ExtensionsLogger { get; private set; } = null!;
public static void Initialize()
public static async Task Initialize()
{
File.Delete("log.log");
LocManager.Init();
LocManager.SetApplicationLocalization(Settings.Instance.Language);
Logger = LogManager.GetLogger("Logger");
var lp = new NLogLoggerProvider();
var logger = lp.CreateLogger("SteamLib");
@@ -27,13 +36,18 @@ public static class Shell
try
{
TimeAligner.AlignTime();
await TimeAligner.AlignTimeAsync();
}
catch (Exception ex)
{
throw new CantAlignTimeException("", ex);
}
var threads = Environment.ProcessorCount > 0 ? Environment.ProcessorCount : 1;
await Storage.Initialize(threads);
MAACStorage.Initialize();
MafileExporterStorage.Initialize();
ExtensionsLogger.LogDebug("Application started");
}
+5 -2
View File
@@ -5,8 +5,11 @@
throwExceptions="true">
<targets>
<target xsi:type="File" name="File" fileName="log.log"
deleteOldFileOnStartup="true">
<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}" />
+2 -2
View File
@@ -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>
@@ -28,7 +28,7 @@
<PackageReference Include="CodingSeb.Localization.WPF" Version="1.4.1" />
<PackageReference Include="CodingSebLocalization.Fody" Version="1.4.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="MaterialDesignThemes" Version="5.2.1" />
<PackageReference Include="MaterialDesignThemes" Version="5.3.0" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
<PackageReference Include="NLog" Version="5.5.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.5.0" />
+7 -5
View File
@@ -11,13 +11,16 @@ public static class LanguageUtility
{
var userCulture = CultureInfo.CurrentUICulture;
var userLang = userCulture.TwoLetterISOLanguageName;
var userRegion = userCulture.Name;
var userRegion = new RegionInfo(userCulture.Name).TwoLetterISORegionName;
switch (userLang)
{
case "ru": return LocalizationLanguage.Russian;
case "uk": return LocalizationLanguage.Ukrainian;
case "en": return LocalizationLanguage.English;
case "zh": return LocalizationLanguage.ChineseSimplified;
case "fr": return LocalizationLanguage.French;
}
if (userRegion.EndsWith("UA", StringComparison.OrdinalIgnoreCase))
@@ -28,9 +31,8 @@ public static class LanguageUtility
"RU", "BY", "KZ", "KG", "TJ", "TM", "UZ", "AM", "AZ", "GE", "MD"
];
if (cisRegions.Any(r => userRegion.EndsWith(r, StringComparison.OrdinalIgnoreCase)))
return LocalizationLanguage.Russian;
return LocalizationLanguage.English;
return cisRegions.Any(r => userRegion.EndsWith(r, StringComparison.OrdinalIgnoreCase))
? LocalizationLanguage.Russian
: LocalizationLanguage.English;
}
}
@@ -58,7 +58,7 @@
materialDesign:HintAssist.IsFloating="False"
materialDesign:TextFieldAssist.LeadingIcon="Key"
materialDesign:TextFieldAssist.HasLeadingIcon="True" />
<CheckBox FontSize="15" Grid.Row="1" Margin="10,10,10,0"
<CheckBox FontSize="15" Grid.Row="1" Margin="10,5,10,0"
IsEnabled="{Binding Source={x:Static model:PHandler.IsPasswordSet}}"
IsChecked="{Binding SavePassword}" Content="{Tr LoginAgainDialog.SaveEncryptedPassword}" />
<Grid Grid.Row="2" Margin="10,10,10,0">
@@ -74,10 +74,10 @@
<KeyBinding Key="Delete" Command="{Binding RemoveProxyCommand}" />
</UIElement.InputBindings>
</ComboBox>
<CheckBox FontSize="15" Grid.Row="2" Margin="10,10,10,0"
<CheckBox FontSize="15" Grid.Row="2" Margin="10,5,10,0"
IsEnabled="{Binding Source={x:Static model:PHandler.IsPasswordSet}}"
IsChecked="{Binding SavePassword}" Content="{Tr LoginAgainDialog.SaveEncryptedPassword}" />
<CheckBox FontSize="15" Grid.Row="3" Margin="10,10,10,0" IsEnabled="{Binding MafileHasProxy}"
<CheckBox FontSize="15" Grid.Row="3" Margin="10,5,10,0" IsEnabled="{Binding MafileHasProxy}"
Content="{Tr LoginAgainDialog.UseMafileProxy}" IsChecked="{Binding UseMafileProxy}" />
<Grid Grid.Row="4" Margin="10,10,10,0">
<Grid.ColumnDefinitions>
+240
View File
@@ -0,0 +1,240 @@
<UserControl x:Class="NebulaAuth.View.MafileExporterView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:other="clr-namespace:NebulaAuth.ViewModel.Other"
xmlns:nebulaAuth="clr-namespace:NebulaAuth"
mc:Ignorable="d"
TextElement.Foreground="{DynamicResource BaseContentBrush}"
FontFamily="{materialDesign:MaterialDesignFont}"
MinWidth="400"
MaxWidth="400"
Background="{DynamicResource WindowBackground}"
d:DataContext="{d:DesignInstance other:MafileExporterVM}"
d:Background="#141119"
d:Foreground="White"
FontSize="18">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Margin="10,10,10,5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="FileExport" Width="20" Height="20" Margin="0,0,5,0"
VerticalAlignment="Center"
Foreground="{DynamicResource PrimaryContentBrush}" />
<TextBlock FontStyle="Normal" Foreground="{DynamicResource BaseContentBrush}"
HorizontalAlignment="Left"
VerticalAlignment="Center" FontSize="18" Text="{Tr ExportDialog.ExportTitle}" />
</StackPanel>
<Button Grid.Column="1" Width="30" Height="30" Style="{StaticResource MaterialDesignIconForegroundButton}"
HorizontalAlignment="Right" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
IsCancel="True">
<materialDesign:PackIcon Kind="Close" Width="24" Height="24" Foreground="IndianRed" />
</Button>
</Grid>
<Separator Background="DarkGray" Opacity="0.4" Grid.Row="1" />
<Grid Grid.Row="2" Margin="15" TextElement.FontSize="14">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ComboBox Grid.Column="0"
Padding="10"
Style="{StaticResource MaterialDesignOutlinedComboBox}"
Margin="0,0,5,0"
materialDesign:HintAssist.Hint="{Tr ExportDialog.TemplateHint}"
ItemsSource="{Binding Templates}"
DisplayMemberPath="Name"
SelectedItem="{Binding CurrentTemplate}"
Visibility="{Binding EditMode, Converter={StaticResource InverseBooleanToVisibilityConverter}}" />
<TextBox x:Name="EditTemplateNameTextBox"
Grid.Column="0"
Padding="10"
Margin="0,0,5,0"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
materialDesign:HintAssist.Hint="{Tr ExportDialog.TemplateHint}"
Text="{Binding CurrentTemplate.Name,
FallbackValue=''}"
IsEnabled="{Binding CurrentTemplate, Converter={StaticResource NullableToBooleanConverter}}"
Visibility="{Binding EditMode, Converter={StaticResource BooleanToVisibilityConverter}}">
<TextBox.InputBindings>
<KeyBinding Key="Enter"
Command="{Binding ToggleEditModeCommand}" />
</TextBox.InputBindings>
</TextBox>
<Button Command="{Binding AddTemplateCommand}" Grid.Column="1">
<materialDesign:PackIcon Kind="Add" />
</Button>
<Button Command="{Binding ToggleEditModeCommand}" Click="EditButton_Click" Grid.Column="2">
<materialDesign:PackIcon Kind="Edit" />
</Button>
<Button Grid.Column="3" Command="{Binding RemoveCurrentTemplateCommand}">
<materialDesign:PackIcon Kind="Trash" />
</Button>
</Grid>
<Grid Margin="0,15,0,0" Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
IsEnabled="{Binding CurrentTemplate, Converter={StaticResource NullableToBooleanConverter}}"
MouseDoubleClick="PathTB_DoubleClick" materialDesign:TextFieldAssist.HasClearButton="True"
Text="{Binding CurrentTemplate.Path, FallbackValue=''}" Padding="10"
Style="{StaticResource MaterialDesignOutlinedTextBox}" Grid.Column="0"
materialDesign:HintAssist.Hint="{Tr ExportDialog.SelectPathHint}" />
<Button Command="{Binding OpenSelectDirectoryDialogCommand}" Grid.Column="1" Margin="5,0,0,0">
<materialDesign:PackIcon Kind="FolderOpen" />
</Button>
</Grid>
<Border Grid.Row="2" BorderThickness="1" CornerRadius="4" Margin="0,15,0,0" Padding="2">
<Border.BorderBrush>
<SolidColorBrush Color="{DynamicResource BaseContentColor}" Opacity="0.4" />
</Border.BorderBrush>
<Expander x:Name="TemplateExpander"
Header="{Tr ExportDialog.ExportSettingsHeader}"
materialDesign:ExpanderAssist.HorizontalHeaderPadding="5"
materialDesign:ExpanderAssist.ExpanderButtonPosition="Start"
HorizontalAlignment="Stretch"
Padding="5,0,5,5"
IsEnabled="{Binding CurrentTemplate, Converter={StaticResource NullableToBooleanConverter}}">
<StackPanel>
<TextBlock FontWeight="DemiBold"
Text="{Tr ExportDialog.ExportSettingsCommon}" />
<CheckBox IsChecked="{Binding CurrentTemplate.UseLoginAsMafileName, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.UseLoginAsMafileName}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.UseLoginAsMafileName}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeSharedSecret, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeSharedSecret}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeSharedSecret}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeIdentitySecret, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeIdentitySecret}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeIdentitySecret}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeRCode, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeRCode}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeRCode}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeSessionData, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeSessionData}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeSessionData}" />
<CheckBox IsChecked="{Binding CurrentTemplate.IncludeOtherInfo, FallbackValue=False}"
Margin="3,0,0,0"
Content="{Tr ExportDialog.ExportOptions.IncludeOtherInfo}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.IncludeOtherInfo}" />
<TextBlock FontWeight="DemiBold"
Margin="0,10,0,0"
Text="{Tr ExportDialog.ExportSettingsNebula}" />
<Grid Margin="3,0,3,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<CheckBox Grid.Column="0"
IsChecked="{Binding CurrentTemplate.IncludeNebulaProxy, FallbackValue=False}"
Content="{Tr ExportDialog.ExportOptions.NebulaProxy}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.NebulaProxy}" />
<CheckBox Grid.Column="1"
HorizontalAlignment="Center"
IsChecked="{Binding CurrentTemplate.IncludeNebulaPassword, FallbackValue=False}"
Content="{Tr ExportDialog.ExportOptions.NebulaPassword}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.NebulaPassword}" />
<CheckBox Grid.Column="2"
HorizontalAlignment="Right"
IsChecked="{Binding CurrentTemplate.IncludeNebulaGroup, FallbackValue=False}"
Content="{Tr ExportDialog.ExportOptions.NebulaGroup}"
ToolTipService.InitialShowDelay="200"
ToolTip="{Tr ExportDialog.Tooltips.NebulaGroup}" />
</Grid>
</StackPanel>
</Expander>
</Border>
<TextBox GotFocus="ExportTB_GotFocus" PreviewMouseUp="ExportTB_GotFocus" Grid.Row="3"
Style="{StaticResource MaterialDesignOutlinedTextBox}" MaxHeight="600" Margin="0,10,0,0"
MinHeight="250" Height="250" AcceptsReturn="True"
Text="{Binding AccountsText, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding ExportCommand.IsRunning, Converter={StaticResource ReverseBooleanConverter}}"
Name="AccountsTB" />
<TextBlock Grid.Row="3"
Text="{Tr ExportDialog.ExportAccountsPlaceholder}"
Margin="18,27,10,10"
Opacity="0.6"
IsHitTestVisible="False"
TextWrapping="Wrap"
Visibility="{Binding Text.IsEmpty,
ElementName=AccountsTB,
Converter={StaticResource BooleanToVisibilityConverter}}" />
<nebulaAuth:HintBox Margin="0,10,0,0" Severity="{Binding HintBoxSeverity}" Grid.Row="4"
Text="{Binding HintText}" />
</Grid>
</Grid>
<Separator Grid.Row="3" />
<Button Command="{Binding ExportCommand}" Style="{StaticResource MaterialDesignFlatDarkBgButton}" Grid.Row="4"
Margin="20,7,20,7" MaxWidth="150" Content="{Tr ExportDialog.ExportButton}" />
</Grid>
</UserControl>
@@ -0,0 +1,32 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
namespace NebulaAuth.View;
public partial class MafileExporterView : UserControl
{
public MafileExporterView()
{
InitializeComponent();
}
private void PathTB_DoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is TextBox textBox)
{
textBox.SelectAll();
}
}
private void ExportTB_GotFocus(object sender, RoutedEventArgs e)
{
TemplateExpander.IsExpanded = false;
}
private void EditButton_Click(object sender, RoutedEventArgs e)
{
Dispatcher.BeginInvoke(() => { EditTemplateNameTextBox.Focus(); }, DispatcherPriority.Input);
}
}
+5 -13
View File
@@ -55,27 +55,19 @@
ItemsSource="{Binding Languages}" DisplayMemberPath="Value" SelectedValuePath="Key"
SelectedValue="{Binding Language}"
materialDesign:HintAssist.Hint="{Tr LanguageWord}" />
<CheckBox Margin="0,18,0,0" IsChecked="{Binding HideToTray}"
<CheckBox Margin="0,5,0,0" IsChecked="{Binding HideToTray}"
Content="{Tr SettingsDialog.MinimizeToTray}" />
<CheckBox Margin="0,18,0,0" IsChecked="{Binding LegacyMode}"
<CheckBox Margin="0,5,0,0" IsChecked="{Binding LegacyMode}"
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,18,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}" />
</CheckBox>-->
<CheckBox Margin="0,20,0,0" IsChecked="{Binding UseIcon}"
<CheckBox Margin="0,5,0,0" IsChecked="{Binding UseIcon}"
Content="{Tr SettingsDialog.UseIndicator}"
ToolTip="{Tr SettingsDialog.UseIndicatorHint}" />
<materialDesign:ColorPicker Margin="-5,0,-5,0" IsEnabled="{Binding UseIcon}"
@@ -191,10 +183,10 @@
PreviewMouseUp="Slider_OnMouseUp" />
<CheckBox Margin="0,15,0,0" Content="{Tr SettingsDialog.Theme.DialogBlur}"
<CheckBox Margin="0,0,0,0" Content="{Tr SettingsDialog.Theme.DialogBlur}"
IsChecked="{Binding ApplyBlurBackground}"
FontSize="15" />
<CheckBox Margin="0,15,0,0" Content="{Tr SettingsDialog.Theme.RippleDisabled}"
<CheckBox Margin="0,0,0,0" Content="{Tr SettingsDialog.Theme.RippleDisabled}"
FontSize="15"
IsChecked="{Binding RippleDisabled}" />
<Button Margin="0,15,0,0" Command="{Binding ResetThemeDefaultsCommand}"
@@ -12,6 +12,7 @@ using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using NebulaAuth.Utility;
using NLog;
using SteamLib;
@@ -224,7 +225,7 @@ public partial class LinkAccountVM : ObservableObject, ISmsCodeProvider, IPhoneN
Logger.Error(ex, "Error during saving Nebula data to mafile");
}
Storage.SaveMafile(mafile);
await Storage.SaveMafileAsync(mafile);
await Done(mafile.RevocationCode ?? string.Empty, mafile.SteamId.Steam64.ToString(), login);
}
@@ -12,6 +12,7 @@ using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using NebulaAuth.Utility;
using NebulaAuth.ViewModel.Linker;
using Newtonsoft.Json;
+5 -23
View File
@@ -10,8 +10,8 @@ using MaterialDesignThemes.Wpf;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
using NebulaAuth.Utility;
using NebulaAuth.View;
using NebulaAuth.View.Dialogs;
using SteamLib.SteamMobile;
using SteamLibForked.Exceptions.Authorization;
@@ -22,7 +22,6 @@ public partial class MainVM : ObservableObject
{
[UsedImplicitly] public SnackbarMessageQueue MessageQueue => SnackbarController.MessageQueue;
public Mafile? SelectedMafile
{
get => _selectedMafile;
@@ -32,7 +31,7 @@ public partial class MainVM : ObservableObject
public bool IsMafileSelected => SelectedMafile != null;
public DialogHost CurrentDialogHost { get; set; } = null!;
[ObservableProperty] private ObservableCollection<Mafile> _maFiles = Storage.MaFiles;
[ObservableProperty] private ObservableCollection<Mafile> _maFiles = new(Storage.MaFiles);
private Mafile? _selectedMafile;
@@ -49,10 +48,8 @@ public partial class MainVM : ObservableObject
[RelayCommand]
public async Task Debug()
{
await DialogsController.ShowSetAccountsPasswordDialog();
}
private void SetMafile(Mafile? mafile)
{
if (mafile != SelectedMafile)
@@ -71,7 +68,9 @@ public partial class MainVM : ObservableObject
LoginAgainCommand.NotifyCanExecuteChanged();
RemoveAuthenticatorCommand.NotifyCanExecuteChanged();
ConfirmLoginCommand.NotifyCanExecuteChanged();
RemoveProxyCommand.NotifyCanExecuteChanged();
}
}
@@ -114,7 +113,7 @@ public partial class MainVM : ObservableObject
private void MaFilesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
SearchText = string.Empty;
PerformQuery();
}
@@ -134,12 +133,6 @@ public partial class MainVM : ObservableObject
}
}
[RelayCommand]
public async Task LinkAccount()
{
await DialogsController.ShowLinkerDialog();
}
[RelayCommand]
public async Task MoveAccount()
{
@@ -172,7 +165,6 @@ public partial class MainVM : ObservableObject
if (result.Success)
{
Storage.MoveToRemoved(selectedMafile);
MaFiles.Remove(selectedMafile);
}
}
else
@@ -218,16 +210,6 @@ public partial class MainVM : ObservableObject
}
}
[RelayCommand]
private async Task OpenLinksView()
{
CurrentDialogHost.CloseOnClickAway = true;
var view = new LinksView();
await DialogHost.Show(view);
CurrentDialogHost.CloseOnClickAway = false;
}
private static string GetLocalization(string key)
{
const string locPath = "MainVM";
+4 -3
View File
@@ -13,6 +13,7 @@ using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Exceptions;
using NebulaAuth.Model.Mafiles;
using NebulaAuth.Utility;
using NebulaAuth.View;
using NebulaAuth.View.Dialogs;
@@ -78,7 +79,7 @@ public partial class MainVM //File //TODO: Refactor
{
try
{
Storage.AddNewMafile(str, confirmOverwrite ?? false);
await Storage.AddNewMafile(str, confirmOverwrite ?? false);
added++;
}
catch (FormatException)
@@ -92,7 +93,7 @@ public partial class MainVM //File //TODO: Refactor
if (confirmOverwrite == true)
{
Storage.AddNewMafile(str, true);
await Storage.AddNewMafile(str, true);
added++;
}
else if (confirmOverwrite == false)
@@ -239,7 +240,7 @@ public partial class MainVM //File //TODO: Refactor
}
var arr = files.Cast<string>().ToArray();
if (arr.All(p => p.ContainsIgnoreCase("mafile") == false)) return;
if (arr.All(p => !p.ContainsIgnoreCase("mafile"))) return;
await AddMafile(arr);
+3 -3
View File
@@ -5,8 +5,8 @@ using AchiesUtilities.Extensions;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
namespace NebulaAuth.ViewModel;
@@ -88,7 +88,7 @@ public partial class MainVM //Groups
Storage.UpdateMafile(mafile);
OnPropertyChanged(nameof(SelectedMafile)); //For bindings
QueryGroups();
if (Groups.All(g => g.Equals(mafGroup) == false))
if (Groups.All(g => !g.Equals(mafGroup)))
{
SelectedGroup = null;
}
@@ -104,7 +104,7 @@ public partial class MainVM //Groups
private void QueryGroups()
{
var groups = Storage.MaFiles
.Where(m => string.IsNullOrWhiteSpace(m.Group) == false)
.Where(m => !string.IsNullOrWhiteSpace(m.Group))
.Select(m => m.Group)
.Distinct()
.Order()
+1 -1
View File
@@ -130,7 +130,7 @@ public partial class MainVM //MAAC
{
mafiles = mafiles.ToArray();
var turnOn = mafiles.All(m => m.LinkedClient == null || GetCurrentMode(m.LinkedClient) == false);
var turnOn = mafiles.All(m => m.LinkedClient == null || !GetCurrentMode(m.LinkedClient));
if (turnOn)
{
foreach (var mafile in mafiles)
+38
View File
@@ -0,0 +1,38 @@
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MaterialDesignThemes.Wpf;
using NebulaAuth.Core;
using NebulaAuth.View;
namespace NebulaAuth.ViewModel;
public partial class MainVM : ObservableObject
{
[RelayCommand]
private Task OpenSetPasswordsDialog()
{
return DialogsController.ShowSetAccountsPasswordDialog();
}
[RelayCommand]
private Task OpenExporterDialog()
{
return DialogsController.ShowExportDialog();
}
[RelayCommand]
public Task LinkAccount()
{
return DialogsController.ShowLinkerDialog();
}
[RelayCommand]
private async Task OpenLinksView()
{
CurrentDialogHost.CloseOnClickAway = true;
var view = new LinksView();
await DialogHost.Show(view);
CurrentDialogHost.CloseOnClickAway = false;
}
}
+14 -6
View File
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Mafiles;
namespace NebulaAuth.ViewModel;
@@ -68,13 +69,20 @@ 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;
SetProxy(null, false); //Not system, triggered by user
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()
@@ -0,0 +1,368 @@
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model.MafileExport;
namespace NebulaAuth.ViewModel.Other;
public partial class MafileExporterVM : ObservableObject
{
public MafileExportTemplateVM? CurrentTemplate
{
get => _currentTemplate;
set => SetCurrentTemplate(value);
}
public ObservableCollection<MafileExportTemplateVM> Templates { get; }
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ExportCommand))]
private string? _accountsText;
private string? _cachedName;
private MafileExportTemplateVM? _currentTemplate;
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(AddTemplateCommand), nameof(RemoveCurrentTemplateCommand))]
private bool _editMode;
[ObservableProperty] private HintBoxSeverity _hintBoxSeverity = HintBoxSeverity.Error;
[ObservableProperty] private string? _hintText;
public MafileExporterVM()
{
var templates = MafileExporterStorage.Templates
.Select(MafileExportTemplateVM.FromModel);
Templates = new ObservableCollection<MafileExportTemplateVM>(templates);
MafileExporterStorage.Templates.CollectionChanged += OnTemplatesOnCollectionChanged;
CurrentTemplate = Templates.FirstOrDefault();
}
private void SetCurrentTemplate(MafileExportTemplateVM? value)
{
if (_currentTemplate != null)
{
_currentTemplate.PropertyChanged -= OnCurrentTemplatePropertyChanged;
}
SetProperty(ref _currentTemplate, value, nameof(CurrentTemplate));
if (value != null)
{
value.PropertyChanged += OnCurrentTemplatePropertyChanged;
}
ToggleEditModeCommand.NotifyCanExecuteChanged();
RemoveCurrentTemplateCommand.NotifyCanExecuteChanged();
OpenSelectDirectoryDialogCommand.NotifyCanExecuteChanged();
ExportCommand.NotifyCanExecuteChanged();
}
private void OnTemplatesOnCollectionChanged(object? s, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (MafileExportTemplate newItem in e.NewItems!)
{
Templates.Add(MafileExportTemplateVM.FromModel(newItem));
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (MafileExportTemplate oldItem in e.OldItems!)
{
var vmToRemove = Templates.FirstOrDefault(x => x.Name == oldItem.Name);
if (vmToRemove != null)
{
Templates.Remove(vmToRemove);
}
}
break;
case NotifyCollectionChangedAction.Replace:
foreach (MafileExportTemplate newItem in e.NewItems!)
{
var vmToReplace = Templates.FirstOrDefault(x => x.Name == newItem.Name);
if (vmToReplace != null)
{
var index = Templates.IndexOf(vmToReplace);
Templates[index] = MafileExportTemplateVM.FromModel(newItem);
}
}
break;
case NotifyCollectionChangedAction.Reset:
Templates.Clear();
break;
case NotifyCollectionChangedAction.Move:
default:
break;
}
}
private void OnCurrentTemplatePropertyChanged(object? sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
var currentTemplate = CurrentTemplate;
if (currentTemplate == null) return;
var key = propertyChangedEventArgs.PropertyName == nameof(MafileExportTemplateVM.Name)
? _cachedName!
: currentTemplate.Name;
var existed = MafileExporterStorage.GetTemplate(key);
if (existed != null)
{
currentTemplate.ApplyTo(existed);
MafileExporterStorage.Save();
}
else
{
MafileExporterStorage.AddTemplate(currentTemplate.ToModel());
}
}
[RelayCommand(CanExecute = nameof(CurrentTemplateNotNull))]
private void ToggleEditMode()
{
EditMode = !EditMode;
if (EditMode) _cachedName = CurrentTemplate?.Name;
}
[RelayCommand(CanExecute = nameof(NotEditMode))]
private void AddTemplate()
{
ResetHintText();
var name = "New Template";
var i = 0;
while (Templates.Any(x => x.Name == name))
{
name = $"New Template ({++i})";
}
var template = new MafileExportTemplate
{
Name = name,
UseLoginAsMafileName = false,
IncludeSharedSecret = true,
IncludeIdentitySecret = true,
IncludeRCode = true,
IncludeSessionData = true,
IncludeOtherInfo = true,
IncludeNebulaProxy = true,
IncludeNebulaPassword = true,
IncludeNebulaGroup = true,
Path = null
};
MafileExporterStorage.AddTemplate(template);
CurrentTemplate = Templates.FirstOrDefault(x => x.Name == name);
}
[RelayCommand(CanExecute = nameof(CurrentTemplateNotNullAndNotEditMode))]
private void RemoveCurrentTemplate()
{
if (CurrentTemplate != null)
{
ResetHintText();
var index = Templates.IndexOf(CurrentTemplate);
var existed = MafileExporterStorage.GetTemplate(CurrentTemplate.Name);
if (existed != null)
{
MafileExporterStorage.DeleteTemplate(existed);
}
if (Templates.Count > 0)
{
if (index >= Templates.Count)
{
index = Templates.Count - 1;
}
CurrentTemplate = Templates[index];
}
else
{
CurrentTemplate = null;
}
}
}
[RelayCommand(CanExecute = nameof(CurrentTemplateNotNull))]
private void OpenSelectDirectoryDialog()
{
if (CurrentTemplate == null) return;
var dialog = new FolderBrowserDialog
{
UseDescriptionForTitle = true,
ShowNewFolderButton = true
};
var result = dialog.ShowDialog();
if (result == DialogResult.OK)
{
CurrentTemplate!.Path = dialog.SelectedPath;
}
}
[RelayCommand(CanExecute = nameof(ExportCanExecute))]
private async Task Export()
{
var lines = AccountsText;
var template = CurrentTemplate;
if (string.IsNullOrWhiteSpace(lines) || template == null)
{
return;
}
var split = Regex.Split(lines, "\r\n|\r|\n").Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();
ResetHintText();
ExportResult res;
try
{
res = await MafileExporter.ExportMafiles(split, template.ToModel());
}
catch (Exception ex)
{
SetHintText(ex.Message);
return;
}
if (!res.Success)
{
SetHintText(res.Error);
return;
}
var hint = string.Format(LocManager.GetCodeBehindOrDefault("Exported", "MafileExporterVM.Result.Exported"),
res.Exported.Count);
if (res.NotFound.Count > 0)
{
hint += string.Format(LocManager.GetCodeBehindOrDefault("NotFound", "MafileExporterVM.Result.NotFound"),
res.NotFound.Count);
}
if (res.Conflict.Count > 0)
{
hint += string.Format(LocManager.GetCodeBehindOrDefault("Conflict", "MafileExporterVM.Result.Conflict"),
res.Conflict.Count);
}
SetHintText(hint, HintBoxSeverity.Info);
var errors = res.NotFound.Concat(res.Conflict).ToHashSet();
var errorLines = split.Where(errors.Contains);
var text = string.Join(Environment.NewLine, errorLines);
AccountsText = text;
}
private void SetHintText(string? text, HintBoxSeverity severity = HintBoxSeverity.Error)
{
HintText = text;
HintBoxSeverity = severity;
}
private void ResetHintText()
{
SetHintText(null);
}
#region CanExecute
private bool ExportCanExecute()
{
return CurrentTemplateNotNull() && !string.IsNullOrWhiteSpace(AccountsText);
}
private bool CurrentTemplateNotNull()
{
return CurrentTemplate != null;
}
private bool CurrentTemplateNotNullAndNotEditMode()
{
return CurrentTemplateNotNull() && NotEditMode();
}
private bool NotEditMode()
{
return !EditMode;
}
#endregion
}
public partial class MafileExportTemplateVM : ObservableObject
{
[ObservableProperty] private bool _includeIdentitySecret;
[ObservableProperty] private bool _includeNebulaGroup;
[ObservableProperty] private bool _includeNebulaPassword;
[ObservableProperty] private bool _includeNebulaProxy;
[ObservableProperty] private bool _includeOtherInfo;
[ObservableProperty] private bool _includeRCode;
[ObservableProperty] private bool _includeSessionData;
[ObservableProperty] private bool _includeSharedSecret;
[ObservableProperty] private string _name;
[ObservableProperty] private string? _path;
[ObservableProperty] private bool _useLoginAsMafileName;
public static MafileExportTemplateVM FromModel(MafileExportTemplate x)
{
return new MafileExportTemplateVM
{
Name = x.Name,
UseLoginAsMafileName = x.UseLoginAsMafileName,
IncludeSharedSecret = x.IncludeSharedSecret,
IncludeIdentitySecret = x.IncludeIdentitySecret,
IncludeRCode = x.IncludeRCode,
IncludeSessionData = x.IncludeSessionData,
IncludeOtherInfo = x.IncludeOtherInfo,
IncludeNebulaProxy = x.IncludeNebulaProxy,
IncludeNebulaPassword = x.IncludeNebulaPassword,
IncludeNebulaGroup = x.IncludeNebulaGroup,
Path = x.Path
};
}
public MafileExportTemplate ToModel()
{
return new MafileExportTemplate
{
Name = Name,
UseLoginAsMafileName = UseLoginAsMafileName,
IncludeSharedSecret = IncludeSharedSecret,
IncludeIdentitySecret = IncludeIdentitySecret,
IncludeRCode = IncludeRCode,
IncludeSessionData = IncludeSessionData,
IncludeOtherInfo = IncludeOtherInfo,
IncludeNebulaProxy = IncludeNebulaProxy,
IncludeNebulaPassword = IncludeNebulaPassword,
IncludeNebulaGroup = IncludeNebulaGroup,
Path = Path
};
}
public void ApplyTo(MafileExportTemplate model)
{
model.Name = Name;
model.UseLoginAsMafileName = UseLoginAsMafileName;
model.IncludeSharedSecret = IncludeSharedSecret;
model.IncludeIdentitySecret = IncludeIdentitySecret;
model.IncludeRCode = IncludeRCode;
model.IncludeSessionData = IncludeSessionData;
model.IncludeOtherInfo = IncludeOtherInfo;
model.IncludeNebulaProxy = IncludeNebulaProxy;
model.IncludeNebulaPassword = IncludeNebulaPassword;
model.IncludeNebulaGroup = IncludeNebulaGroup;
model.Path = Path;
}
}
@@ -61,7 +61,7 @@ public partial class ProxyManagerVM : ObservableObject
var split = input
.Split(Environment.NewLine)
.Where(s => string.IsNullOrWhiteSpace(s) == false)
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(x => x.Trim())
.ToArray();
@@ -72,7 +72,7 @@ public partial class ProxyManagerVM : ObservableObject
var i = 0;
foreach (var s in split.Where(s => string.IsNullOrWhiteSpace(s) == false))
foreach (var s in split.Where(s => !string.IsNullOrWhiteSpace(s)))
{
i++;
var str = s;
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Mafiles;
using SteamLibForked.Models.SteamIds;
namespace NebulaAuth.ViewModel.Other;
+7 -10
View File
@@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
using NebulaAuth.Model.Mafiles;
namespace NebulaAuth.ViewModel.Other;
@@ -53,7 +54,7 @@ public partial class SettingsVM : ObservableObject
var targetValue = UseAccountNameAsMafileNamePreview;
if (UseAccountNameAsMafileName == targetValue) return;
RenameMafilesProgress = 0;
Storage.MafileRenameResult? result = null;
MafilesBulkRenameResult? result = null;
try
{
result = await Storage.RenameMafiles(targetValue, new Progress<double>(p => RenameMafilesProgress = p));
@@ -85,7 +86,7 @@ public partial class SettingsVM : ObservableObject
{
var l = GetLoc("PartialSuccess");
RenameResultText =
string.Format(l, result.Total, result.Renamed, result.Errors, result.Conflict, result.BackupFileName);
string.Format(l, result.Total, result.Renamed, result.Conflict, result.Errors, result.BackupFileName);
}
string GetLoc(string key)
@@ -127,7 +128,9 @@ public partial class SettingsVM : ObservableObject
{
{LocalizationLanguage.English, "English"},
{LocalizationLanguage.Russian, "Русский"},
{LocalizationLanguage.Ukrainian, "Українська"}
{LocalizationLanguage.Ukrainian, "Українська"},
{LocalizationLanguage.ChineseSimplified, "简体中文"},
{LocalizationLanguage.French, "Français"}
};
public Color? IconColor
@@ -141,7 +144,7 @@ public partial class SettingsVM : ObservableObject
get => IconColor != null;
set
{
if (value == false)
if (!value)
IconColor = null;
else
IconColor = Color.FromRgb(202, 39, 39);
@@ -184,12 +187,6 @@ public partial class SettingsVM : ObservableObject
}
}
public bool IgnorePatchTuesdayErrors
{
get => Settings.IgnorePatchTuesdayErrors;
set => Settings.IgnorePatchTuesdayErrors = value;
}
#endregion
#region Theme
File diff suppressed because it is too large Load Diff
@@ -152,7 +152,7 @@ public static class SteamAuthenticatorLinkerApi
var content = await resp.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
var j = JObject.Parse(content);
if (j["success"]!.Value<bool>() == false)
if (!j["success"]!.Value<bool>())
return CheckPhoneResult.GeneralFailure;
if (j["is_voip"]!.Value<bool>())
@@ -187,7 +187,7 @@ public static class SteamAuthenticatorLinkerApi
var resp = await client.PostProto<IsAccountWaitingForEmailConfirmation_Response>(reqUri,
new EmptyMessage());
if (resp.IsWaiting == false) return true;
if (!resp.IsWaiting) return true;
await Task.Delay(resp.SecondsToWait * 1000);
}
@@ -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);
@@ -68,7 +73,7 @@ public static class AdmissionHelper
{
var uri = SteamDomains.GetDomainUri(domain);
foreach (var cookie in container.GetCookies(uri)
.Where(c => c.Expired == false && c.Name.EqualsIgnoreCase(ACCESS_COOKIE_NAME)))
.Where(c => !c.Expired && c.Name.EqualsIgnoreCase(ACCESS_COOKIE_NAME)))
{
cookie.Expired = true;
}
@@ -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);
@@ -131,7 +148,7 @@ public static class AdmissionHelper
}
var mobileToken = mobileSession.GetMobileToken();
if (domainCookieSet == false && mobileToken is {IsExpired: false})
if (!domainCookieSet && mobileToken is {IsExpired: false})
{
var domain = SteamDomains.GetDomainUri(SteamDomain.Community);
container.Add(domain, new Cookie(ACCESS_COOKIE_NAME, mobileToken.Value.SignedToken)
@@ -193,7 +210,7 @@ public static class AdmissionHelper
foreach (Cookie cookie in cookies)
{
if (cookie.Domain.Contains("steamcommunity.com") == false || cookie.Expired ||
if (!cookie.Domain.Contains("steamcommunity.com") || cookie.Expired ||
cookie.Name.EqualsIgnoreCase(ACCESS_COOKIE_NAME)) continue;
@@ -253,7 +270,7 @@ public static class AdmissionHelper
var cookies = container.GetAllCookies();
return cookies
.FirstOrDefault(c => c.Name.Equals(SESSION_ID_COOKIE_NAME, StringComparison.InvariantCultureIgnoreCase)
&& c.Expired == false
&& !c.Expired
&& c.Domain.Contains(domain, StringComparison.InvariantCultureIgnoreCase))?
.Value;
}
@@ -44,7 +44,7 @@ public static class SteamTokenHelper
{
result = default;
var match = SteamTokenRegex.Match(input);
if (match.Success == false)
if (!match.Success)
{
if (trying) return false;
throw new ArgumentException("Provided Token is not valid SteamLoginSecure or SteamAccess token");
@@ -64,7 +64,7 @@ public static class SteamTokenHelper
}
var steamIdStr = jwt.Subject;
if (steamIdStr == null || long.TryParse(steamIdStr, out var steamId) == false || steamId < SteamId64.SEED)
if (steamIdStr == null || !long.TryParse(steamIdStr, out var steamId) || steamId < SteamId64.SEED)
{
if (trying) return false;
throw new ArgumentException(
@@ -101,7 +101,7 @@ public static class SteamTokenHelper
if (jwt.Audiences.ToList().Count > 0)
{
var aud = audiences[0];
if (AudDomains.TryGetValue(aud, out domain) == false && aud != "web")
if (!AudDomains.TryGetValue(aud, out domain) && aud != "web")
{
if (trying)
{
@@ -180,13 +180,13 @@ public static class SteamTokenHelper
internal static string CombineLoginValueIfNeeded(long steamId, string loginValue)
{
return CheckIfProperLoginValue(loginValue) == false ? CombineJwtWithSteamId(steamId, loginValue) : loginValue;
return !CheckIfProperLoginValue(loginValue) ? CombineJwtWithSteamId(steamId, loginValue) : loginValue;
}
public static string ExtractJwtToken(string steamLoginSecure)
{
var match = SteamTokenRegex.Match(steamLoginSecure);
if (match.Success == false) throw new ArgumentException("Can't extract JWT Access Token from steamLoginSecure");
if (!match.Success) throw new ArgumentException("Can't extract JWT Access Token from steamLoginSecure");
return match.Groups["jwt"].Value;
}
}
@@ -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)
{
}
}
@@ -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)
{
}
}
@@ -11,7 +11,7 @@ public static class ProtoHttpClientExtension
this HttpClient client, Uri uri, HttpMethod method, string? protoMsg, CancellationToken cancellationToken)
where TProtoResponse : class, IProtoMsg
{
var resp = await SendProto(client, uri, method, protoMsg, cancellationToken);
var resp = await client.SendProto(uri, method, protoMsg, cancellationToken);
return await ProtoResponse<TProtoResponse>.FromHttpResponseAsync(resp, cancellationToken);
}
@@ -19,7 +19,7 @@ public static class ProtoHttpClientExtension
string? protoMsg,
CancellationToken cancellationToken)
{
var resp = await SendProto(client, uri, method, protoMsg, cancellationToken);
var resp = await client.SendProto(uri, method, protoMsg, cancellationToken);
resp.EnsureSuccessStatusCode();
return ProtoHelpers.GetEResult(resp);
}
@@ -97,7 +97,7 @@ public static class ProtoHttpClientExtension
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
{
var str = ProtoHelpers.ProtoToString(request);
return SendProtoWithResponse<TProtoResponse>(client, uri, HttpMethod.Post, str, cancellationToken);
return client.SendProtoWithResponse<TProtoResponse>(uri, HttpMethod.Post, str, cancellationToken);
}
public static Task<ProtoResponse<TProtoResponse>> PostProtoMsg<TProtoResponse>(this HttpClient client, string uri,
@@ -105,7 +105,7 @@ public static class ProtoHttpClientExtension
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
{
var str = ProtoHelpers.ProtoToString(request);
return SendProtoWithResponse<TProtoResponse>(client, new Uri(uri), HttpMethod.Post, str, cancellationToken);
return client.SendProtoWithResponse<TProtoResponse>(new Uri(uri), HttpMethod.Post, str, cancellationToken);
}
#endregion
@@ -117,7 +117,7 @@ public static class ProtoHttpClientExtension
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
{
var str = ProtoHelpers.ProtoToString(request);
var res = await SendProtoWithResponse<TProtoResponse>(client, uri, HttpMethod.Post, str, cancellationToken);
var res = await client.SendProtoWithResponse<TProtoResponse>(uri, HttpMethod.Post, str, cancellationToken);
return res.GetResponseEnsureSuccess();
}
@@ -126,7 +126,7 @@ public static class ProtoHttpClientExtension
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
{
var str = ProtoHelpers.ProtoToString(request);
var res = await SendProtoWithResponse<TProtoResponse>(client, new Uri(uri), HttpMethod.Post, str,
var res = await client.SendProtoWithResponse<TProtoResponse>(new Uri(uri), HttpMethod.Post, str,
cancellationToken);
return res.GetResponseEnsureSuccess();
}
@@ -179,7 +179,7 @@ public static class ProtoHttpClientExtension
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
{
var str = ProtoHelpers.ProtoToString(request);
return SendProtoWithResponse<TProtoResponse>(client, uri, HttpMethod.Get, str, cancellationToken);
return client.SendProtoWithResponse<TProtoResponse>(uri, HttpMethod.Get, str, cancellationToken);
}
public static Task<ProtoResponse<TProtoResponse>> GetProtoMsg<TProtoResponse>(this HttpClient client, string uri,
@@ -187,7 +187,7 @@ public static class ProtoHttpClientExtension
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
{
var str = ProtoHelpers.ProtoToString(request);
return SendProtoWithResponse<TProtoResponse>(client, new Uri(uri), HttpMethod.Get, str, cancellationToken);
return client.SendProtoWithResponse<TProtoResponse>(new Uri(uri), HttpMethod.Get, str, cancellationToken);
}
#endregion
@@ -199,7 +199,7 @@ public static class ProtoHttpClientExtension
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
{
var str = ProtoHelpers.ProtoToString(request);
var res = await SendProtoWithResponse<TProtoResponse>(client, uri, HttpMethod.Get, str, cancellationToken);
var res = await client.SendProtoWithResponse<TProtoResponse>(uri, HttpMethod.Get, str, cancellationToken);
res.Result.EnsureSuccessEResult();
return res.ResponseMsg ?? throw new NullReferenceException("ProtoMsg in response was null");
@@ -210,7 +210,7 @@ public static class ProtoHttpClientExtension
CancellationToken cancellationToken = default) where TProtoResponse : class, IProtoMsg
{
var str = ProtoHelpers.ProtoToString(request);
var res = await SendProtoWithResponse<TProtoResponse>(client, new Uri(uri), HttpMethod.Get, str,
var res = await client.SendProtoWithResponse<TProtoResponse>(new Uri(uri), HttpMethod.Get, str,
cancellationToken);
res.Result.EnsureSuccessEResult();
return res.ResponseMsg ?? throw new NullReferenceException("ProtoMsg in response was null");
@@ -74,18 +74,18 @@ public class SteamAuthenticatorLinker
long? phoneNumber = null;
if (hasPhone == false && Options.PhoneNumberProvider != null)
if (!hasPhone && Options.PhoneNumberProvider != null)
{
phoneNumber = await Options.PhoneNumberProvider.GetPhoneNumber(Consumer);
}
if (hasPhone == false && phoneNumber != null)
if (!hasPhone && phoneNumber != null)
{
Logger?.LogInformation("Attaching phone number {phoneNumber}", phoneNumber);
//if (await this.IsValidPhoneNumber(phoneNumber.Value) == false)
// throw new AuthenticatorLinkerException(AuthenticatorLinkerError.InvalidPhoneNumber);
if (await this.AttachPhone(phoneNumber.Value) == false)
if (!await this.AttachPhone(phoneNumber.Value))
throw new AuthenticatorLinkerException(AuthenticatorLinkerError.CantAttachPhone);
@@ -150,7 +150,7 @@ public class SteamAuthenticatorLinker
Logger?.LogInformation("Finalizing link");
var result = await this.FinalizeLink(code, resp.SharedSecret, isPhone);
if (result.Success == false)
if (!result.Success)
{
var error = result.Error switch
{
@@ -5,24 +5,21 @@ namespace SteamLib.Utility.MafileSerialization;
internal class LegacyMafile
{
[JsonProperty("shared_secret")]
[JsonRequired]
public string SharedSecret { get; set; } = default!;
public string SharedSecret { get; set; } = null!;
[JsonProperty("identity_secret")]
[JsonRequired]
public string IdentitySecret { get; set; } = default!;
public string IdentitySecret { get; set; } = null!;
[JsonProperty("device_id")]
[JsonRequired]
public string DeviceId { get; set; } = default!;
public string DeviceId { get; set; } = null!;
[JsonProperty("revocation_code")] public string RevocationCode { get; set; } = default!;
[JsonProperty("account_name")] public string AccountName { get; set; } = default!;
[JsonProperty("revocation_code")] public string RevocationCode { get; set; } = null!;
[JsonProperty("account_name")] public string AccountName { get; set; } = null!;
[JsonProperty("Session")] public object? SessionData { get; set; }
[JsonProperty("server_time")] public long ServerTime { get; set; } //Unused
[JsonProperty("steamid")] public long SteamId { get; set; }
[JsonProperty("serial_number")] public string SerialNumber { get; set; } = default!; //Unused
[JsonProperty("uri")] public string Uri { get; set; } = default!; //Unused
[JsonProperty("token_gid")] public string TokenGid { get; set; } = default!; //Unused
[JsonProperty("secret_1")] public string Secret1 { get; set; } = default!; //Unused
[JsonProperty("serial_number")] public string SerialNumber { get; set; } = null!; //Unused
[JsonProperty("uri")] public string Uri { get; set; } = null!; //Unused
[JsonProperty("token_gid")] public string TokenGid { get; set; } = null!; //Unused
[JsonProperty("secret_1")] public string Secret1 { get; set; } = null!; //Unused
}
@@ -38,7 +38,7 @@ public partial class MafileSerializer //Validate
public static void IsValidBase64(string name, string base64)
{
var buffer = new Span<byte>(new byte[base64.Length]);
if (Convert.TryFromBase64String(base64, buffer, out _) == false)
if (!Convert.TryFromBase64String(base64, buffer, out _))
throw new ArgumentException($"{name} is not valid base64 string");
}
@@ -39,7 +39,7 @@ public static class MobileConfirmationScrapper
throw new SessionInvalidException();
}
if (conf.Success == false)
if (!conf.Success)
{
var error = LoadConfirmationsError.Unknown;
if (conf.Message != null && ErrorMessages.TryGetValue(conf.Message, out var e))