mirror of
https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies.git
synced 2026-07-25 06:14:31 +00:00
f15632f2d0
- 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
287 lines
10 KiB
C#
287 lines
10 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using AchiesUtilities.Extensions;
|
|
using NebulaAuth.Model.Entities;
|
|
using NebulaAuth.Model.Exceptions;
|
|
using NebulaAuth.Model.MAAC;
|
|
using SteamLib;
|
|
using SteamLib.Exceptions.Authorization;
|
|
using SteamLib.SteamMobile;
|
|
|
|
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);
|
|
Directory.CreateDirectory(RemovedMafilesDirectory);
|
|
Directory.CreateDirectory(BackupMafilesDirectory);
|
|
|
|
var files = Directory
|
|
.GetFiles(MafilesDirectory)
|
|
.Where(file => Path.GetExtension(file).EqualsIgnoreCase(".mafile"))
|
|
.ToList();
|
|
|
|
|
|
var localList = new ConcurrentBag<Mafile>();
|
|
await Task.Run(() =>
|
|
{
|
|
return Parallel.ForEachAsync(files,
|
|
new ParallelOptions {CancellationToken = token, MaxDegreeOfParallelism = threadCount},
|
|
async (file, ct) =>
|
|
{
|
|
try
|
|
{
|
|
var data = await ReadMafileAsync(file);
|
|
localList.Add(data);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Shell.Logger.Error(ex, "Can't load mafile {file}", Path.GetFileName(file));
|
|
}
|
|
});
|
|
}, token);
|
|
|
|
MaFiles = new ObservableCollection<Mafile>(localList.OrderBy(m => m.AccountName));
|
|
}
|
|
|
|
/// <summary>
|
|
/// </summary>
|
|
/// <param name="path"></param>
|
|
/// <param name="overwrite"></param>
|
|
/// <exception cref="FormatException"></exception>
|
|
/// <exception cref="SessionInvalidException"></exception>
|
|
/// <exception cref="IOException"></exception>
|
|
public static async Task AddNewMafile(string path, bool overwrite)
|
|
{
|
|
Mafile data;
|
|
try
|
|
{
|
|
data = await ReadMafileAsync(path);
|
|
data.Filename = null;
|
|
}
|
|
catch (Exception ex)
|
|
when (ex is not MafileNeedReloginException)
|
|
{
|
|
Shell.Logger.Warn(ex, "Can't load mafile");
|
|
throw new FormatException("File data is not valid", ex);
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(data.AccountName))
|
|
throw new FormatException("File data is not valid. Missing AccountName");
|
|
|
|
try
|
|
{
|
|
_ = SteamGuardCodeGenerator.GenerateCode(data.SharedSecret);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new FormatException("Can't generate code on this mafile", ex);
|
|
}
|
|
|
|
if (!overwrite && File.Exists(MafilesStorageHelper.GetOrUpdateMafilePath(data)))
|
|
{
|
|
throw new IOException("File already exist and overwrite is False"); //TODO: Custom Exception
|
|
}
|
|
|
|
|
|
await SaveMafileAsync(data);
|
|
}
|
|
|
|
public static async Task<Mafile> ReadMafileAsync(string path)
|
|
{
|
|
var str = await File.ReadAllTextAsync(path);
|
|
return NebulaSerializer.Deserialize(str, path);
|
|
}
|
|
|
|
public static async Task SaveMafileAsync(Mafile data)
|
|
{
|
|
var path = MafilesStorageHelper.GetOrUpdateMafilePath(data);
|
|
var str = NebulaSerializer.SerializeMafile(data);
|
|
await File.WriteAllTextAsync(Path.GetFullPath(path), str);
|
|
|
|
// Replace if exist
|
|
for (var i = 0; i < MaFiles.Count; i++)
|
|
{
|
|
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 = MafilesStorageHelper.GetOrUpdateMafilePath(data);
|
|
var str = NebulaSerializer.SerializeMafile(data);
|
|
File.WriteAllText(Path.GetFullPath(path), str);
|
|
}
|
|
|
|
public static async Task UpdateMafileAsync(Mafile 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 = 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 = destinationPathBase + $" ({i})";
|
|
destinationPathFinal = Path.ChangeExtension(destinationPathFinal, MafileNamingStrategy.DEF_EXTENSION);
|
|
}
|
|
|
|
File.Copy(sourcePath, destinationPathFinal, false);
|
|
File.Delete(sourcePath);
|
|
MaFiles.Remove(data);
|
|
}
|
|
|
|
|
|
public static string? TryGetMafilePath(Mafile data)
|
|
{
|
|
if (data.Filename == null) return null;
|
|
return Path.Combine(MafilesDirectory, data.Filename);
|
|
}
|
|
|
|
public static void WriteBackup(MobileDataExtended data)
|
|
{
|
|
var json = NebulaSerializer.SerializeMafile(data, null);
|
|
WriteBackup(data.AccountName, json);
|
|
}
|
|
|
|
public static void WriteBackup(string accountName, string data)
|
|
{
|
|
Directory.CreateDirectory(DIR_BACKUP_MAFILES);
|
|
File.WriteAllText(Path.Combine(DIR_BACKUP_MAFILES, accountName + MafileNamingStrategy.DEF_EXTENSION), data);
|
|
}
|
|
|
|
public static async Task<MafilesBulkRenameResult> RenameMafiles(bool loginAsFileName,
|
|
IProgress<double>? progress = null)
|
|
{
|
|
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);
|
|
await using (var zipStream = new FileStream(zipPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
|
{
|
|
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Create, false);
|
|
var files = Directory
|
|
.EnumerateFiles(MafilesDirectory, "*.*", SearchOption.TopDirectoryOnly)
|
|
.Where(f => Path.GetExtension(f).Equals(".mafile", StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
|
|
var counter = 0;
|
|
foreach (var file in files)
|
|
{
|
|
counter++;
|
|
var entry = archive.CreateEntry(Path.GetFileName(file), CompressionLevel.Optimal);
|
|
|
|
await using var entryStream = entry.Open();
|
|
await using var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096,
|
|
true);
|
|
await fileStream.CopyToAsync(entryStream);
|
|
|
|
if (counter % 5 == 0)
|
|
{
|
|
progress?.Report(counter / (double) files.Count);
|
|
await Task.Delay(10);
|
|
}
|
|
}
|
|
}
|
|
|
|
var mafiles = MaFiles.ToList();
|
|
var res = new MafilesBulkRenameResult
|
|
{
|
|
Total = mafiles.Count,
|
|
BackupFileName = backupFileName
|
|
};
|
|
|
|
var dic = new Dictionary<string, string>();
|
|
foreach (var mafile in mafiles)
|
|
{
|
|
try
|
|
{
|
|
var targetFileName = MafilesStorageHelper.CreateMafileFileName(mafile, loginAsFileName);
|
|
if (mafile.Filename == targetFileName || mafile.Filename == null)
|
|
{
|
|
res.Renamed += 1;
|
|
continue;
|
|
}
|
|
|
|
var sourceFileName = mafile.Filename;
|
|
var fullSourcePath = Path.Combine(MafilesDirectory, sourceFileName);
|
|
var fullTargetPath = Path.Combine(MafilesDirectory, targetFileName);
|
|
|
|
if (!File.Exists(fullSourcePath))
|
|
{
|
|
Shell.Logger.Warn("Can't rename mafile {old} to {new} because source file not found",
|
|
mafile.Filename, targetFileName);
|
|
IncErrors();
|
|
continue;
|
|
}
|
|
|
|
if (File.Exists(fullTargetPath))
|
|
{
|
|
Shell.Logger.Warn("Can't rename mafile {old} to {new} because target file already exist",
|
|
mafile.Filename, targetFileName);
|
|
res.Conflict += 1;
|
|
continue;
|
|
}
|
|
|
|
File.Move(fullSourcePath, fullTargetPath);
|
|
dic[sourceFileName] = targetFileName;
|
|
res.Renamed += 1;
|
|
mafile.Filename = targetFileName;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Shell.Logger.Error(ex, "Error renaming mafile {file} {accountName}", mafile.Filename,
|
|
mafile.AccountName);
|
|
IncErrors();
|
|
}
|
|
}
|
|
|
|
MAACStorage.NotifyMafilesRenamed(dic);
|
|
return res;
|
|
|
|
void IncErrors()
|
|
{
|
|
res.Errors += 1;
|
|
}
|
|
}
|
|
} |