1.5.4 progress

- Mafile version was updated (3), new property SteamId added
- MafileSerializer was refactored to adjust compability and code readability.
- Serialization work moved from Storage to new class NebulaSerializer
This commit is contained in:
Давид Чернопятов
2024-11-10 02:29:40 +02:00
parent 317d8b9754
commit ef8e12e20d
39 changed files with 895 additions and 754 deletions
+14 -3
View File
@@ -1,10 +1,20 @@
using AchiesUtilities.Extensions;
using NebulaAuth.LegacyConverter;
using Newtonsoft.Json;
using SteamLib.Utility.MaFiles;
using SteamLib.Utility.MafileSerialization;
try
{
var mafileSerializer = new MafileSerializer(new MafileSerializerSettings
{
DeserializationOptions =
{
AllowDeviceIdGeneration = true,
AllowSessionIdGeneration = true,
}
});
var currentPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
if (currentPath != null)
Environment.CurrentDirectory = currentPath;
@@ -29,7 +39,7 @@ try
}
}
}
var decryptMode = false;
while (true)
{
@@ -120,7 +130,8 @@ try
}
}
var maf = MafileSerializer.Deserialize(text, true, out _);
var data = mafileSerializer.Deserialize(text);
var maf = data.Data;
var legacy = MafileSerializer.SerializeLegacy(maf, Formatting.Indented);
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
Write(legacy, fileNameWithoutExtension);
+1
View File
@@ -25,6 +25,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "changelog", "changelog", "{
changelog\1.5.1.html = changelog\1.5.1.html
changelog\1.5.2.html = changelog\1.5.2.html
changelog\1.5.3.html = changelog\1.5.3.html
changelog\1.5.4.html = changelog\1.5.4.html
EndProjectSection
EndProject
Global
+1
View File
@@ -45,6 +45,7 @@ public partial class Mafile : MobileDataExtended
ServerTime = data.ServerTime,
TokenGid = data.TokenGid,
Uri = data.Uri,
SteamId = data.SteamId
};
}
public static Mafile FromMobileDataExtended(MobileDataExtended data, MaProxy? proxy, string? group, string? password)
+83
View File
@@ -0,0 +1,83 @@
using NebulaAuth.Model.Entities;
using Newtonsoft.Json.Linq;
using SteamLib;
using SteamLib.Utility.MafileSerialization;
using System.Collections.Generic;
using System;
using Newtonsoft.Json;
namespace NebulaAuth.Model;
public static class NebulaSerializer
{
public static MafileSerializer Serializer { get; }
static NebulaSerializer()
{
Serializer = new MafileSerializer(new MafileSerializerSettings
{
DeserializationOptions =
{
AllowDeviceIdGeneration = true,
AllowSessionIdGeneration = true,
ThrowIfInvalidSteamId = true
}
});
}
public static Mafile Deserialize(string cont)
{
var data = Serializer.Deserialize(cont);
var mobileData = data.Data;
var info = data.Info;
if (info.IsExtended == false)
throw new FormatException("Mafile is not extended data");
var props = info.UnusedProperties ?? new Dictionary<string, JProperty>();
var proxy = GetPropertyValue<MaProxy>("Proxy", props);
var group = GetPropertyValue<string>("Group", props);
var password = GetPropertyValue<string>("Password", props);
return Mafile.FromMobileDataExtended((MobileDataExtended)mobileData, proxy, group, password);
}
private static T? GetPropertyValue<T>(string name, Dictionary<string, JProperty> dictionary)
{
if (dictionary.TryGetValue(name, out var prop) == false) return default;
var value = prop.Value;
try
{
return value.ToObject<T>();
}
catch (Exception ex)
{
Shell.Logger.Warn(ex, "Can't deserialize property {name}", name);
return default;
}
}
public static string SerializeMafile(Mafile data)
{
var props = new Dictionary<string, object?>
{
{nameof(Mafile.Proxy), data.Proxy},
{nameof(Mafile.Group), data.Group},
{nameof(Mafile.Password), data.Password}
};
return SerializeMafile(data, props);
}
public static string SerializeMafile(MobileDataExtended data, Dictionary<string, object?>? properties)
{
if (Settings.Instance.LegacyMode)
{
return MafileSerializer.SerializeLegacy(data, Formatting.Indented, properties);
}
else
{
return MafileSerializer.Serialize(data);
}
}
}
+21 -97
View File
@@ -1,19 +1,18 @@
using NebulaAuth.Model.Entities;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SteamLib;
using SteamLib.Account;
using SteamLib.Core.Models;
using SteamLib.Exceptions;
using SteamLib.SteamMobile;
using SteamLib.Utility.MaFiles;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using AchiesUtilities.Extensions;
using SteamLib;
namespace NebulaAuth.Model;
//RETHINK
public static class Storage
{
public const string MAFILE_F = "maFiles";
@@ -42,21 +41,20 @@ public static class Storage
var hashIds = new HashSet<SteamId>();
var comparer = new MafileNameComparer(Settings.Instance.UseAccountNameAsMafileName);
var ordered = files.Order(comparer).ToList();
foreach (var file in ordered)
foreach (var file in ordered.Where(file => Path.GetExtension(file).EqualsIgnoreCase(".mafile")))
{
if (Path.GetExtension(file).Equals(".mafile", StringComparison.OrdinalIgnoreCase) == false) continue;
try
{
var data = ReadMafile(file);
if (hashNames.Contains(data.AccountName) || (data.SessionData != null && hashIds.Contains(data.SessionData.SteamId)))
if (hashNames.Contains(data.AccountName) || hashIds.Contains(data.SteamId))
{
DuplicateFound++;
Shell.Logger.Error("Duplicate mafile {file}", Path.GetFileName(file));
continue;
}
hashNames.Add(data.AccountName);
if (data.SessionData != null) hashIds.Add(data.SessionData.SteamId);
if (data.SessionData != null) hashIds.Add(data.SteamId);
MaFiles.Add(data);
}
catch (Exception ex)
@@ -76,6 +74,11 @@ public static class Storage
{
data = ReadMafile(path);
}
catch (ArgumentException ex)
when(ex.ParamName == nameof(MobileDataExtended.SteamId))
{
throw new SessionInvalidException(); //Allows to handle LoginOnImport
}
catch (Exception ex)
{
Shell.Logger.Warn(ex, "Can't load mafile");
@@ -94,14 +97,6 @@ public static class Storage
throw new FormatException("Can't generate code on this mafile", ex);
}
if (data.SessionData == null)
throw new SessionInvalidException("File data is not valid. Missing SessionData")
{
Data = { { "mafile", data } }
};
if (overwrite == false && File.Exists(CreatePathForMafile(data)))
{
throw new IOException("File already exist and overwrite is False");
@@ -115,61 +110,13 @@ public static class Storage
public static Mafile ReadMafile(string path)
{
var str = File.ReadAllText(path);
var mafile = MafileSerializer.Deserialize(str, true, out var mafileData);
if (mafileData.IsExtended == false)
throw new FormatException("Mafile is not extended data");
var props = mafileData.UnusedProperties ?? new Dictionary<string, JProperty>();
var proxy = GetPropertyValue<MaProxy>("Proxy", props);
var group = GetPropertyValue<string>("Group", props);
var password = GetPropertyValue<string>("Password", props);
return Mafile.FromMobileDataExtended((MobileDataExtended)mafile, proxy, group, password);
}
public static string SerializeMafile(Mafile data)
{
var props = new Dictionary<string, object?>
{
{nameof(Mafile.Proxy), data.Proxy},
{nameof(Mafile.Group), data.Group},
{nameof(Mafile.Password), data.Password}
};
return SerializeMafile(data, props);
}
public static string SerializeMafile(MobileDataExtended data, Dictionary<string, object?>? properties)
{
if (Settings.Instance.LegacyMode)
{
return MafileSerializer.SerializeLegacy(data, Formatting.Indented, properties);
}
else
{
return MafileSerializer.Serialize(data);
}
}
private static T? GetPropertyValue<T>(string name, Dictionary<string, JProperty> dictionary)
{
if (dictionary.TryGetValue(name, out var prop) == false) return default;
var value = prop.Value;
try
{
return value.ToObject<T>();
}
catch (Exception ex)
{
Shell.Logger.Warn(ex, "Can't deserialize property {name}", name);
return default;
}
return NebulaSerializer.Deserialize(str);
}
public static void SaveMafile(Mafile data)
{
var path = CreatePathForMafile(data);
var str = SerializeMafile(data);
var str = NebulaSerializer.SerializeMafile(data);
File.WriteAllText(Path.GetFullPath(path), str);
var existed = MaFiles.SingleOrDefault(m => m.AccountName == data.AccountName);
@@ -187,23 +134,14 @@ public static class Storage
public static void UpdateMafile(Mafile data)
{
var path = CreatePathForMafile(data);
var str = SerializeMafile(data);
var str = NebulaSerializer.SerializeMafile(data);
File.WriteAllText(Path.GetFullPath(path), str);
}
public static void RemoveMafile(Mafile data)
{
var path = CreatePathForMafile(data);
if (File.Exists(path))
{
File.Delete(path);
}
MaFiles.Remove(data);
}
public static void MoveToRemoved(Mafile data)
{
var path = CreatePathForMafile(data);
var copyPath = Path.Combine(REMOVED_F, data.SessionData!.SteamId + ".mafile");
var copyPath = Path.Combine(REMOVED_F, data.SteamId + ".mafile");
var copyPathCompleted = copyPath;
var i = 0;
while (File.Exists(copyPathCompleted))
@@ -218,18 +156,9 @@ public static class Storage
private static string CreatePathForMafile(Mafile data)
{
string fileName;
if (Settings.Instance.UseAccountNameAsMafileName)
{
fileName = CreateFileNameWithAccountName(data.AccountName);
}
else
{
if(data.SessionData == null)
throw new NullReferenceException("SessionData was null can't retrieve SteamId"); //FIXME: think about better way to handle
fileName = CreateFileNameWithSteamId(data.SessionData.SteamId);
}
var fileName = Settings.Instance.UseAccountNameAsMafileName
? CreateFileNameWithAccountName(data.AccountName)
: CreateFileNameWithSteamId(data.SteamId);
return Path.Combine(MafileFolder, fileName);
}
@@ -238,13 +167,12 @@ public static class Storage
private static string CreateFileNameWithSteamId(SteamId steamId) => steamId.Steam64.Id + ".mafile";
public static string? TryFindMafilePath(Mafile data)
//FIXME: write mode to mafile instead of searching it. Search sometimes represents not actual mafile (in case of duplicate steamId + accountName)
{
var pathFileName = Path.Combine(MafileFolder, CreateFileNameWithAccountName(data.AccountName));
string? pathSteamId = null;
if (data.SessionData != null)
{
pathSteamId = Path.Combine(MafileFolder, CreateFileNameWithSteamId(data.SessionData.SteamId));
pathSteamId = Path.Combine(MafileFolder, CreateFileNameWithSteamId(data.SteamId));
}
var steamIdExist = pathSteamId != null && File.Exists(pathSteamId);
@@ -260,13 +188,9 @@ public static class Storage
}
return null;
}
public static bool ValidateCanSave(Mafile data)
{
return Settings.Instance.UseAccountNameAsMafileName || data.SessionData != null;
}
}
//TODO: Refactor
internal class MafileNameComparer : IComparer<string>
{
public bool MafileNameMode { get; }
+2 -9
View File
@@ -2,9 +2,9 @@
using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Model;
using NebulaAuth.Model.Entities;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using AchiesUtilities.Extensions;
namespace NebulaAuth.ViewModel;
@@ -47,7 +47,6 @@ public partial class MainVM //Groups
var mafile = SelectedMafile;
if (mafile == null) return;
if (string.IsNullOrEmpty(value)) return;
if (!ValidateCanSaveAndWarn(mafile)) return;
mafile.Group = value;
Storage.UpdateMafile(mafile);
@@ -67,7 +66,6 @@ public partial class MainVM //Groups
var mafile = (Mafile?)value[1];
if (group == null || mafile == null) return;
if (!ValidateCanSaveAndWarn(mafile)) return;
mafile.Group = group;
Storage.UpdateMafile(mafile);
QueryGroups();
@@ -79,7 +77,6 @@ public partial class MainVM //Groups
private void RemoveGroup(Mafile? mafile)
{
if (mafile?.Group == null) return;
if (!ValidateCanSaveAndWarn(mafile)) return;
var mafGroup = mafile.Group;
mafile.Group = null;
Storage.UpdateMafile(mafile);
@@ -136,11 +133,7 @@ public partial class MainVM //Groups
bool SearchPredicate(Mafile mafile)
{
if (!mafile.AccountName.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase))
{
return mafile.SessionData != null && mafile.SessionData.SteamId.Steam64.Id.Equals(searchSteamId);
}
return true;
return mafile.AccountName.ContainsIgnoreCase(SearchText) || mafile.SteamId.Steam64.Id.Equals(searchSteamId);
}
}
+3 -13
View File
@@ -1,4 +1,5 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NebulaAuth.Core;
using NebulaAuth.Model;
@@ -101,7 +102,6 @@ public partial class MainVM
{
if (SelectedProxy == null) return;
if (SelectedMafile == null) return;
if (!ValidateCanSaveAndWarn(SelectedMafile)) return;
SelectedMafile.Proxy = null;
SelectedProxy = null;
Storage.UpdateMafile(SelectedMafile);
@@ -117,19 +117,9 @@ public partial class MainVM
}
if (SelectedMafile == null) return;
if (!ValidateCanSaveAndWarn(SelectedMafile)) return;
ProxyExist = true;
SelectedMafile.Proxy = SelectedProxy;
Storage.UpdateMafile(SelectedMafile);
}
private bool ValidateCanSaveAndWarn(Mafile data)
{
var canSave = Storage.ValidateCanSave(data);
if (!canSave)
{
SnackbarController.SendSnackbar(GetLocalization("CantRetrieveSteamIDToUpdate"));
}
return canSave;
}
}
+2 -2
View File
@@ -328,8 +328,8 @@ public partial class LinkAccountVM : ObservableObject, IEmailProvider, IPhoneNum
{
Directory.CreateDirectory("mafiles_backup");
}
var json = Storage.SerializeMafile(data, null);
File.WriteAllText(Path.Combine("mafiles_backup", data.AccountName + ".mafile"), json);
var json = NebulaSerializer.SerializeMafile(data, null);
File.WriteAllText(Path.Combine("mafiles_backup", data.AccountName + ".mafile"), json); //TODO: Move logic to Storage
}
#region Providers
+3 -4
View File
@@ -2,6 +2,7 @@
using SteamLib.Core.Interfaces;
using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;
using SteamLib.Core.Models;
namespace SteamLib.Account;
@@ -35,8 +36,8 @@ public sealed class MobileSessionData : SessionData, IMobileSessionData
}
public MobileSessionData(string sessionId, SteamId steamId, SteamAuthToken refreshToken,
SteamAuthToken? mobileToken, IEnumerable<SteamAuthToken>? tokens)
: base(sessionId, steamId, refreshToken, tokens)
SteamAuthToken? mobileToken, IEnumerable<SteamAuthToken>? tokensCollection)
: base(sessionId, steamId, refreshToken, tokensCollection)
{
MobileToken = mobileToken;
}
@@ -46,6 +47,4 @@ public sealed class MobileSessionData : SessionData, IMobileSessionData
{
return new MobileSessionData(SessionId, SteamId, RefreshToken, MobileToken, Tokens);
}
}
+7 -5
View File
@@ -1,6 +1,7 @@
using Newtonsoft.Json;
using SteamLib.Core.Enums;
using SteamLib.Core.Interfaces;
using SteamLib.Core.Models;
using SteamLib.Web.Converters;
using System.Collections.Concurrent;
@@ -8,8 +9,9 @@ namespace SteamLib.Account;
public class SessionData : ISessionData
{
[JsonIgnore]
public bool? IsValid { get; set; }
[JsonIgnore] public bool? IsValid { get; set; }
[JsonIgnore] public bool IsExpired => RefreshToken.IsExpired;
public string SessionId { get; }
[JsonConverter(typeof(SteamIdToSteam64Converter))]
@@ -26,13 +28,13 @@ public class SessionData : ISessionData
Tokens = new ConcurrentDictionary<SteamDomain, SteamAuthToken>(tokens ?? new Dictionary<SteamDomain, SteamAuthToken>());
}
public SessionData(string sessionId, SteamId steamId, SteamAuthToken refreshToken, IEnumerable<SteamAuthToken>? tokens)
public SessionData(string sessionId, SteamId steamId, SteamAuthToken refreshToken, IEnumerable<SteamAuthToken>? tokensCollection)
{
SessionId = sessionId;
SteamId = steamId;
RefreshToken = refreshToken;
tokens ??= Array.Empty<SteamAuthToken>();
Tokens = new ConcurrentDictionary<SteamDomain, SteamAuthToken>(tokens.ToDictionary(t => t.Domain, t => t));
tokensCollection ??= Array.Empty<SteamAuthToken>();
Tokens = new ConcurrentDictionary<SteamDomain, SteamAuthToken>(tokensCollection.ToDictionary(t => t.Domain, t => t));
}
public virtual SteamAuthToken? GetToken(SteamDomain domain)
+1
View File
@@ -3,6 +3,7 @@ using AchiesUtilities.Newtonsoft.JSON.Converters.Special;
using Newtonsoft.Json;
using SteamLib.Authentication;
using SteamLib.Core.Enums;
using SteamLib.Core.Models;
using SteamLib.Web.Converters;
namespace SteamLib.Account;
-180
View File
@@ -1,180 +0,0 @@
using SteamLib.Utility;
namespace SteamLib.Account;
public struct SteamId //TODO: validation in parse methods
{
public SteamId64 Steam64 { get; }
public SteamId2 Steam2 { get; }
public SteamId3 Steam3 { get; }
public SteamId(SteamId64 steam64, char type = 'U', short universe = 0)
{
Steam64 = steam64;
Steam2 = steam64.ToSteam2(universe);
Steam3 = steam64.ToSteam3(type);
}
public SteamId(SteamId2 steam2, char type = 'U')
{
Steam2 = steam2;
Steam64 = steam2.ToSteam64();
Steam3 = steam2.ToSteam3(type);
}
public SteamId(SteamId3 steam3, byte universe = 0)
{
Steam3 = steam3;
Steam64 = steam3.ToSteam64();
Steam2 = steam3.ToSteam2(universe);
}
public static SteamId FromSteam64(long steam64, char type = 'U', short universe = 0) => new(new SteamId64(steam64), type, universe);
public override string ToString() => Steam64.ToString();
public static bool TryParse(string input, out SteamId result) => SteamIdParser.TryParse(input, out result);
/// <exception cref="FormatException"></exception>
public static SteamId Parse(string input) => SteamIdParser.Parse(input);
public static bool operator ==(SteamId left, SteamId right) => left.Equals(right);
public static bool operator !=(SteamId left, SteamId right) => !left.Equals(right);
public bool Equals(SteamId other) => Steam64.Equals(other.Steam64);
public override bool Equals(object? obj) => obj is SteamId other && Equals(other);
public override int GetHashCode() => Steam64.GetHashCode();
}
public struct SteamId64
{
public const long SEED = 76561197960265728L;
public long Id { get; }
public SteamId64(long id)
{
if (id < SEED)
throw new ArgumentOutOfRangeException(nameof(id),$"Invalid SteamID provided {id}");
Id = id;
}
public SteamId2 ToSteam2(short universe = 0)
{
var accountIdLowBit = (byte)(Id & 1);
var accountIdHighBits = (int)(Id >> 1) & 0x7FFFFFF;
return new SteamId2((byte)universe, accountIdLowBit, accountIdHighBits);
}
public SteamId3 ToSteam3(char type = 'U')
{
var accountIdLowBit = (byte)(Id & 1);
var accountIdHighBits = (int)(Id >> 1) & 0x7FFFFFF;
return new SteamId3(accountIdLowBit + accountIdHighBits * 2, type);
}
public override string ToString()
{
return Id.ToString();
}
public ulong ToUlong() => (ulong)Id;
public long ToLong() => Id;
public bool Equals(SteamId64 other) => Id == other.Id;
public override bool Equals(object? obj) => obj is SteamId64 other && Equals(other);
public override int GetHashCode() => Id.GetHashCode();
public static bool operator ==(SteamId64 left, SteamId64 right) => left.Equals(right);
public static bool operator !=(SteamId64 left, SteamId64 right) => !left.Equals(right);
public static bool TryParse(string input, out SteamId64 result) => SteamIdParser.TryParse64(input, out result);
/// <exception cref="FormatException"></exception>
public static SteamId64 Parse(string input) => SteamIdParser.Parse64(input);
public static implicit operator long(SteamId64 steamId64) => steamId64.ToLong();
}
public struct SteamId2
{
public byte Universe { get; }
public byte LowestBit { get; }
public int HighestBit { get; }
public SteamId2(byte lowestBit, int highestBit)
{
if(lowestBit > 1)
throw new ArgumentOutOfRangeException(nameof(lowestBit), $"Invalid SteamID2 lowestBit provided {lowestBit}. Max value is 1");
if(highestBit < 0)
throw new ArgumentOutOfRangeException(nameof(highestBit), $"Invalid SteamID2 highestBit provided {highestBit}");
LowestBit = lowestBit;
HighestBit = highestBit;
Universe = 0;
}
public SteamId2(byte universe, byte lowestBit, int highestBit)
{
Universe = universe;
LowestBit = lowestBit;
HighestBit = highestBit;
}
public SteamId64 ToSteam64() => new SteamId64(SteamId64.SEED + LowestBit + HighestBit * 2);
public SteamId3 ToSteam3(char type = 'U') => new SteamId3(HighestBit * 2 + LowestBit);
public override string ToString() => $"STEAM_{Universe}:{LowestBit}:{HighestBit}";
public bool Equals(SteamId2 other) => Universe == other.Universe && LowestBit == other.LowestBit && HighestBit == other.HighestBit;
public override bool Equals(object? obj) => obj is SteamId2 other && Equals(other);
public override int GetHashCode() => HashCode.Combine(Universe, LowestBit, HighestBit);
public static bool operator ==(SteamId2 left, SteamId2 right) => left.Equals(right);
public static bool operator !=(SteamId2 left, SteamId2 right) => !left.Equals(right);
public static bool TryParse(string input, out SteamId2 result) => SteamIdParser.TryParse2(input, out result);
/// <exception cref="FormatException"></exception>
public static SteamId2 Parse(string input) => SteamIdParser.Parse2(input);
}
public struct SteamId3
{
public char Type { get; }
public int Id { get; }
public SteamId3(int id, char type = 'U')
{
if (id < 0)
throw new ArgumentOutOfRangeException(nameof(id), $"Invalid SteamID provided {id}");
Type = type;
Id = id;
}
public SteamId2 ToSteam2(byte universe = 0)
{
var bit = Id % 2;
var highestBits = Id / 2;
return new SteamId2(universe, (byte)bit, highestBits);
}
public SteamId64 ToSteam64() => new SteamId64(SteamId64.SEED + Id);
public override string ToString() => $"[{Type}:1:{Id}]";
public string ToString(bool withBrackets)
{
if (withBrackets)
{
return ToString();
}
else
{
return $"{Type}:1:{Id}";
}
}
public int ToInt() => Id;
public bool Equals(SteamId3 other) => Type == other.Type && Id == other.Id;
public override bool Equals(object? obj) => obj is SteamId3 other && Equals(other);
public readonly override int GetHashCode() => HashCode.Combine(Type, Id);
public static bool operator ==(SteamId3 left, SteamId3 right) => left.Equals(right);
public static bool operator !=(SteamId3 left, SteamId3 right) => !left.Equals(right);
public static bool TryParse(string input, out SteamId3 result) => SteamIdParser.TryParse3(input, out result);
/// <exception cref="FormatException"></exception>
public static SteamId3 Parse(string input) => SteamIdParser.Parse3(input);
}
+1 -1
View File
@@ -7,7 +7,7 @@ using SteamLib.ProtoCore.Enums;
using SteamLib.ProtoCore.Exceptions;
using SteamLib.ProtoCore.Services;
using System.Net;
using SteamLib.Account;
using SteamLib.Core.Models;
namespace SteamLib.Api.Mobile;
@@ -1,6 +1,5 @@
using AchiesUtilities.Web.Extensions;
using JetBrains.Annotations;
using SteamLib.Account;
using SteamLib.Core;
using SteamLib.Core.StatusCodes;
using SteamLib.Exceptions;
@@ -9,6 +8,7 @@ using SteamLib.SteamMobile.Confirmations;
using SteamLib.Utility;
using SteamLib.Web.Scrappers.JSON;
using System.Net;
using SteamLib.Core.Models;
namespace SteamLib.Api.Mobile;
@@ -6,6 +6,7 @@ using SteamLib.Account;
using SteamLib.Core;
using SteamLib.Core.Enums;
using SteamLib.Core.Interfaces;
using SteamLib.Core.Models;
using SteamLib.Core.StatusCodes;
using SteamLib.Exceptions;
using SteamLib.Login.Default;
@@ -1,8 +1,8 @@
using JetBrains.Annotations;
using Microsoft.Extensions.Options;
using SteamLib.Account;
using SteamLib.Core.Enums;
using SteamLib.Core.Interfaces;
using SteamLib.Core.Models;
using SteamLib.Exceptions;
namespace SteamLib.Authentication;
@@ -5,6 +5,7 @@ using SteamLib.Core.Enums;
using System.Collections.ObjectModel;
using System.Net.Http.Headers;
using System.Text.RegularExpressions;
using SteamLib.Core.Models;
namespace SteamLib.Authentication;
@@ -1,5 +1,6 @@
using SteamLib.Account;
using SteamLib.Core.Enums;
using SteamLib.Core.Models;
namespace SteamLib.Core.Interfaces;
+42
View File
@@ -0,0 +1,42 @@
using SteamLib.Utility;
namespace SteamLib.Core.Models;
public readonly struct SteamId : IEquatable<SteamId> //TODO: validation in parse methods (in siblings also)
{
public SteamId64 Steam64 { get; }
public SteamId2 Steam2 { get; }
public SteamId3 Steam3 { get; }
public SteamId(SteamId64 steam64, char type = 'U', short universe = 0)
{
Steam64 = steam64;
Steam2 = steam64.ToSteam2(universe);
Steam3 = steam64.ToSteam3(type);
}
public SteamId(SteamId2 steam2, char type = 'U')
{
Steam2 = steam2;
Steam64 = steam2.ToSteam64();
Steam3 = steam2.ToSteam3(type);
}
public SteamId(SteamId3 steam3, byte universe = 0)
{
Steam3 = steam3;
Steam64 = steam3.ToSteam64();
Steam2 = steam3.ToSteam2(universe);
}
public static SteamId FromSteam64(long steam64, char type = 'U', short universe = 0) => new(new SteamId64(steam64), type, universe);
public override string ToString() => Steam64.ToString();
public static bool TryParse(string input, out SteamId result) => SteamIdParser.TryParse(input, out result);
/// <exception cref="FormatException"></exception>
public static SteamId Parse(string input) => SteamIdParser.Parse(input);
public static bool operator ==(SteamId left, SteamId right) => left.Equals(right);
public static bool operator !=(SteamId left, SteamId right) => !left.Equals(right);
public bool Equals(SteamId other) => Steam64.Equals(other.Steam64);
public override bool Equals(object? obj) => obj is SteamId other && Equals(other);
public override int GetHashCode() => Steam64.GetHashCode();
}
@@ -0,0 +1,46 @@
using SteamLib.Utility;
namespace SteamLib.Core.Models;
public readonly struct SteamId2 : IEquatable<SteamId2>
{
public byte Universe { get; }
public byte LowestBit { get; }
public int HighestBit { get; }
public SteamId2(byte lowestBit, int highestBit)
{
if (lowestBit > 1)
throw new ArgumentOutOfRangeException(nameof(lowestBit), $"Invalid SteamID2 lowestBit provided {lowestBit}. Max value is 1");
if (highestBit < 0)
throw new ArgumentOutOfRangeException(nameof(highestBit), $"Invalid SteamID2 highestBit provided {highestBit}");
LowestBit = lowestBit;
HighestBit = highestBit;
Universe = 0;
}
public SteamId2(byte universe, byte lowestBit, int highestBit)
{
Universe = universe;
LowestBit = lowestBit;
HighestBit = highestBit;
}
public SteamId64 ToSteam64() => new SteamId64(SteamId64.SEED + LowestBit + HighestBit * 2);
public SteamId3 ToSteam3(char type = 'U') => new SteamId3(HighestBit * 2 + LowestBit, type);
public override string ToString() => $"STEAM_{Universe}:{LowestBit}:{HighestBit}";
public bool Equals(SteamId2 other) => Universe == other.Universe && LowestBit == other.LowestBit && HighestBit == other.HighestBit;
public override bool Equals(object? obj) => obj is SteamId2 other && Equals(other);
public override int GetHashCode() => HashCode.Combine(Universe, LowestBit, HighestBit);
public static bool operator ==(SteamId2 left, SteamId2 right) => left.Equals(right);
public static bool operator !=(SteamId2 left, SteamId2 right) => !left.Equals(right);
public static bool TryParse(string input, out SteamId2 result) => SteamIdParser.TryParse2(input, out result);
/// <exception cref="FormatException"></exception>
public static SteamId2 Parse(string input) => SteamIdParser.Parse2(input);
}
@@ -0,0 +1,51 @@
using SteamLib.Utility;
namespace SteamLib.Core.Models;
public readonly struct SteamId3 : IEquatable<SteamId3>
{
public char Type { get; }
public int Id { get; }
public SteamId3(int id, char type = 'U')
{
if (id < 0)
throw new ArgumentOutOfRangeException(nameof(id), $"Invalid SteamID provided {id}");
Type = type;
Id = id;
}
public SteamId2 ToSteam2(byte universe = 0)
{
var bit = Id % 2;
var highestBits = Id / 2;
return new SteamId2(universe, (byte)bit, highestBits);
}
public SteamId64 ToSteam64() => new SteamId64(SteamId64.SEED + Id);
public override string ToString() => $"[{Type}:1:{Id}]";
public string ToString(bool withBrackets)
{
if (withBrackets)
{
return ToString();
}
else
{
return $"{Type}:1:{Id}";
}
}
public int ToInt() => Id;
public bool Equals(SteamId3 other) => Type == other.Type && Id == other.Id;
public override bool Equals(object? obj) => obj is SteamId3 other && Equals(other);
public override int GetHashCode() => HashCode.Combine(Type, Id);
public static bool operator ==(SteamId3 left, SteamId3 right) => left.Equals(right);
public static bool operator !=(SteamId3 left, SteamId3 right) => !left.Equals(right);
public static bool TryParse(string input, out SteamId3 result) => SteamIdParser.TryParse3(input, out result);
/// <exception cref="FormatException"></exception>
public static SteamId3 Parse(string input) => SteamIdParser.Parse3(input);
}
@@ -0,0 +1,50 @@
using SteamLib.Utility;
namespace SteamLib.Core.Models;
public readonly struct SteamId64 : IEquatable<SteamId64>
{
public const long SEED = 76561197960265728L;
public long Id { get; }
public SteamId64(long id)
{
if (id < SEED)
throw new ArgumentOutOfRangeException(nameof(id), $"Invalid SteamID provided {id}");
Id = id;
}
public SteamId2 ToSteam2(short universe = 0)
{
var accountIdLowBit = (byte)(Id & 1);
var accountIdHighBits = (int)(Id >> 1) & 0x7FFFFFF;
return new SteamId2((byte)universe, accountIdLowBit, accountIdHighBits);
}
public SteamId3 ToSteam3(char type = 'U')
{
var accountIdLowBit = (byte)(Id & 1);
var accountIdHighBits = (int)(Id >> 1) & 0x7FFFFFF;
return new SteamId3(accountIdLowBit + accountIdHighBits * 2, type);
}
public override string ToString()
{
return Id.ToString();
}
public ulong ToUlong() => (ulong)Id;
public long ToLong() => Id;
public bool Equals(SteamId64 other) => Id == other.Id;
public override bool Equals(object? obj) => obj is SteamId64 other && Equals(other);
public override int GetHashCode() => Id.GetHashCode();
public static bool operator ==(SteamId64 left, SteamId64 right) => left.Equals(right);
public static bool operator !=(SteamId64 left, SteamId64 right) => !left.Equals(right);
public static bool TryParse(string? input, out SteamId64 result) => SteamIdParser.TryParse64(input, out result);
public static bool TryParse(long input, out SteamId64 result) => SteamIdParser.TryParse64(input, out result);
/// <exception cref="FormatException"></exception>
public static SteamId64 Parse(string input) => SteamIdParser.Parse64(input);
public static implicit operator long(SteamId64 steamId64) => steamId64.ToLong();
}
+22 -6
View File
@@ -1,5 +1,7 @@
using Newtonsoft.Json;
using SteamLib.Account;
using SteamLib.Core.Models;
using SteamLib.Web.Converters;
namespace SteamLib;
@@ -9,24 +11,38 @@ public class MobileData
{
[JsonRequired] public string SharedSecret { get; set; } = null!;
[JsonRequired] public string IdentitySecret { get; set; } = null!;
[JsonRequired] public string DeviceId { get; set; } = null!;
//TODO: This property used only for tracing purposes in Steam, so if it's not provided, we can generate it manually
public string DeviceId { get; set; } = null!;
}
public class MobileDataExtended : MobileData
{
public string? RevocationCode { get; set; } = null!;
public string AccountName { get; set; } = null!;
public MobileSessionData? SessionData { get; set; }
public MobileSessionData? SessionData
{
get => _sessionData;
set
{
_sessionData = value;
if (value != null)
{
SteamId = value.SteamId;
}
}
}
private MobileSessionData? _sessionData;
[JsonConverter(typeof(SteamIdToSteam64Converter))]
public SteamId SteamId { get; set; }
#region Unused
public long ServerTime { get; set; } //Unused
public ulong SerialNumber { get; set; } //Unused //greater than long must be ulong or string
public ulong SerialNumber { get; set; } //Unused //fixed64 greater than long must be ulong or string
public string Uri { get; set; } = null!;//Unused
public string TokenGid { get; set; } = null!;//Unused
public string Secret1 { get; set; } = null!; //Unused
#endregion
}
@@ -37,7 +37,7 @@ public class SteamAuthenticatorLinker
SessionData = data;
data.EnsureValidated();
if (data.RefreshToken.IsExpired)
if (data.IsExpired)
{
Logger?.LogError("Session expired");
throw new SessionPermanentlyExpiredException(SessionPermanentlyExpiredException.SESSION_EXPIRED_MSG);
@@ -1,144 +0,0 @@
using System.Globalization;
using JetBrains.Annotations;
using Newtonsoft.Json.Linq;
namespace SteamLib.Utility.MaFiles;
public enum DeserializedMafileSessionResult
{
Missing,
Invalid,
Expired,
Valid,
}
public class DeserializedMafileData
{
public int? Version { get; init; }
public bool IsExtended { get; init; }
public DeserializedMafileSessionResult SessionResult { get; init; }
public Dictionary<string, JProperty>? UnusedProperties { get; init; }
public HashSet<string>? MissingProperties { get; init; } = new();
public bool IsActual => Version == MafileSerializer.MAFILE_VERSION;
public bool IsLegacy => Version == 0;
public bool HasUnusedProperties => UnusedProperties is { Count: > 0 };
public bool HasMissingProperties => MissingProperties is { Count: > 0 };
internal static DeserializedMafileData Create(int? version = null, bool isExtended = false, Dictionary<string, JProperty>? properties = null, HashSet<string>? missingProperties = null, DeserializedMafileSessionResult sessionResult = DeserializedMafileSessionResult.Missing)
{
return new DeserializedMafileData
{
Version = version,
UnusedProperties = properties,
IsExtended = isExtended,
MissingProperties = missingProperties,
SessionResult = sessionResult
};
}
internal static DeserializedMafileData CreateActual(Dictionary<string, JProperty>? properties = null, DeserializedMafileSessionResult sessionResult = DeserializedMafileSessionResult.Valid)
{
return new DeserializedMafileData
{
Version = MafileSerializer.MAFILE_VERSION,
UnusedProperties = properties,
IsExtended = true,
SessionResult = sessionResult
};
}
}
/// <summary>
/// Representation class just for displaying deserialization result
/// </summary>
[PublicAPI]
public class DeserializedMafileResult
{
public static readonly HashSet<string> ImportantProperties = new()
{
nameof(MobileDataExtended.RevocationCode),
nameof(MobileDataExtended.AccountName),
};
public static readonly HashSet<string> NotImportantProperties = new()
{
nameof(MobileDataExtended.ServerTime),
nameof(MobileDataExtended.SerialNumber),
nameof(MobileDataExtended.Uri),
nameof(MobileDataExtended.TokenGid),
nameof(MobileDataExtended.Secret1),
};
public int? Version { get; init; }
public bool IsExtended { get; init; }
public DeserializedMafileSessionResult SessionResult { get; init; }
public HashSet<string>? MissingImportantProperties { get; init; } = new();
public HashSet<string>? MissingProperties { get; init; } = new();
public Dictionary<string, string>? UnusedProperties { get; init; }
public bool IsActual => Version == MafileSerializer.MAFILE_VERSION;
public bool HasUnusedProperties => UnusedProperties is { Count: > 0 };
public bool HasMissingProperties => MissingProperties is { Count: > 0 };
public bool HasMissingImportantProperties => MissingImportantProperties is { Count: > 0 };
public bool HasSession => SessionResult == DeserializedMafileSessionResult.Valid;
public static DeserializedMafileResult FromData(DeserializedMafileData data)
{
HashSet<string>? missingImportantProperties = null;
HashSet<string>? missingProperties = null;
if (data is { IsExtended: true, HasMissingProperties: true })
{
var important = data.MissingProperties?.Intersect(ImportantProperties).ToList();
if (important?.Count > 0) missingImportantProperties = important.ToHashSet();
var notImportant = data.MissingProperties?.Intersect(NotImportantProperties).ToList();
if (notImportant?.Count > 0) missingProperties = notImportant.ToHashSet();
}
return new DeserializedMafileResult
{
Version = data.Version,
IsExtended = data.IsExtended,
MissingImportantProperties = missingImportantProperties,
MissingProperties = missingProperties,
UnusedProperties = data.UnusedProperties?.ToDictionary(x => x.Key, x => JTokenToString(x.Value)),
SessionResult = data.SessionResult
};
}
private static string JTokenToString(JProperty property)
{
switch (property.Value.Type)
{
case JTokenType.None:
case JTokenType.Null:
return "null";
case JTokenType.Object:
return "object";
case JTokenType.Array:
return "array";
case JTokenType.Integer:
return property.Value.Value<long>().ToString();
case JTokenType.Float:
return property.Value.Value<double>().ToString(CultureInfo.InvariantCulture);
case JTokenType.String:
return property.Value.Value<string>()!;
case JTokenType.Boolean:
return property.Value.Value<bool>().ToString();
case JTokenType.Date:
return property.Value.Value<DateTime>().ToString(CultureInfo.InvariantCulture);
case JTokenType.Guid:
return property.Value.Value<Guid>().ToString();
case JTokenType.Uri:
return property.Value.Value<Uri>()!.ToString();
case JTokenType.TimeSpan:
return property.Value.Value<TimeSpan>().ToString();
default:
return "unknown";
}
}
}
@@ -1,41 +0,0 @@
using Newtonsoft.Json;
using SteamLib.Web.Converters;
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
//TODO: Fix pragma warning
namespace SteamLib.Utility.MaFiles;
internal class LegacyMafile //TODO: move
{
[JsonProperty("shared_secret")]
[JsonRequired]
public string SharedSecret { get; set; }
[JsonProperty("identity_secret")]
[JsonRequired]
public string IdentitySecret { get; set; }
[JsonProperty("device_id")]
[JsonRequired]
public string DeviceId { get; set; }
[JsonProperty("revocation_code")]
public string RevocationCode { get; set; }
[JsonProperty("account_name")]
public string AccountName { get; set; }
[JsonProperty("Session")]
public object? SessionData { get; set; }
[JsonProperty("server_time")] public long ServerTime { get; set; } //Unused
[JsonProperty("serial_number")]
[JsonConverter(typeof(StringToLongIfNeededConverter))]
public string SerialNumber { get; set; } //Unused
[JsonProperty("uri")] public string Uri { get; set; } //Unused
[JsonProperty("token_gid")] public string TokenGid { get; set; } //Unused
[JsonProperty("secret_1")] public string Secret1 { get; set; } //Unused
}
@@ -1,150 +0,0 @@
using AchiesUtilities.Models;
using Newtonsoft.Json.Linq;
using SteamLib.Account;
using SteamLib.Authentication;
using SteamLib.Core.Enums;
using System.Security.Cryptography;
using System.Text;
namespace SteamLib.Utility.MaFiles;
public partial class MafileSerializer //SessionData
{
private static MobileSessionData? DeserializeMobileSessionData(JObject j, bool allowSessionIdGeneration, out DeserializedMafileSessionResult result)
{
result = DeserializedMafileSessionResult.Invalid;
var jRefreshToken = GetToken(j, nameof(MobileSessionData.RefreshToken), "refreshtoken", "refresh_token",
"refresh", "OAuthToken");
SteamAuthToken? refreshToken = null;
if (jRefreshToken == null || jRefreshToken.Type == JTokenType.Null) return null;
if (jRefreshToken.Type == JTokenType.String && SteamTokenHelper.TryParse(jRefreshToken.Value<string>()!, out var parsed))
{
refreshToken = parsed;
}
else if (jRefreshToken.Type == JTokenType.Object)
{
try
{
refreshToken = jRefreshToken.ToObject<SteamAuthToken>();
}
catch
{
//Ignored
}
}
//if (refreshToken == null)
//{
// result = DeserializedMafileSessionResult.Invalid;
// return null;
//}
var sessionId = GetString(j, "sessionid", "session_id", "session");
var jAccessToken = GetToken(j, "accesstoken", "access_token", "access");
jAccessToken ??= GetToken(j, "steamLoginSecure");
if (sessionId == null && allowSessionIdGeneration)
{
sessionId = GenerateRandomHex(12);
}else if (sessionId == null)
{
return null;
}
SteamAuthToken? accessToken = null;
if (jAccessToken == null || jAccessToken.Type == JTokenType.Null)
{
accessToken = null;
}
else if (jAccessToken is { Type: JTokenType.Object })
{
try
{
accessToken = jRefreshToken.ToObject<SteamAuthToken>();
}
catch
{
// ignored
}
}
else if (jAccessToken is { Type: JTokenType.String } &&
SteamTokenHelper.TryParse(jAccessToken.Value<string>()!, out var token) && token.Type == SteamAccessTokenType.Mobile)
{
accessToken = token;
}
var steamId = refreshToken?.SteamId ?? GetSessionSteamId(j);
if (steamId == null)
{
result = DeserializedMafileSessionResult.Invalid;
return null;
}
refreshToken ??= CreateInvalid(steamId.Value);
var sessionData = new MobileSessionData(sessionId, steamId.Value, refreshToken.Value, accessToken, new Dictionary<SteamDomain, SteamAuthToken>());
sessionData.IsValid = SessionDataValidator.Validate(null, sessionData).Succeeded;
if(sessionData.IsValid == false)
return null;
if (refreshToken.Value.IsExpired || refreshToken.Value.Type != SteamAccessTokenType.MobileRefresh)
{
result = DeserializedMafileSessionResult.Expired;
}
else
{
result = DeserializedMafileSessionResult.Valid;
}
return sessionData;
}
private static SteamId? GetSessionSteamId(JObject j)
{
var token = GetToken(j, "steamid");
if (token == null || token.Type == JTokenType.Null)
return null;
if(token.Type == JTokenType.Integer)
return SteamId.FromSteam64(token.Value<long>());
if (token.Type == JTokenType.String && long.TryParse(token.Value<string>()!, out var steamId))
{
return SteamId.FromSteam64(steamId);
}
return null;
}
//Workaround to avoid session being invalidated due to missing a valid token.
//The reason for this change is the inability to proxy/change group for old mafiles, which creates more problems than benefits.
//A temporary solution until I decide how to read the SteamID correctly without invalidating the entire session.
//It also makes the LoginAgainOnImport mechanism useless, which is good outcome.
//Most likely I need to reconsider the reaction to an “invalid” session and simply feed it to the software as “expired”.
//Also, when deciding not to validate RefreshToken, I need to reconsider the entire validation method in the Validator class and think through the consequences in the rest of the code.
//FIXME: Refactor code to avoid this workaround and make it more organic.
//TODO: after fixing the issue, reflect changes in the original library
private static SteamAuthToken CreateInvalid(SteamId steamId)
{
return new SteamAuthToken("invalid", steamId, UnixTimeStamp.Zero, SteamDomain.Community, SteamAccessTokenType.MobileRefresh);
}
private static string GenerateRandomHex(int byteLength = 12)
{
byte[] randomBytes = new byte[byteLength];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomBytes);
}
var hex = new StringBuilder(byteLength * 2);
foreach (var b in randomBytes)
hex.Append($"{b:x2}");
return hex.ToString();
}
}
@@ -0,0 +1,111 @@
using JetBrains.Annotations;
using Newtonsoft.Json.Linq;
using SteamLib.Core.Models;
namespace SteamLib.Utility.MafileSerialization;
public enum DeserializedMafileSessionResult
{
Missing,
Invalid,
Valid,
}
[PublicAPI]
public class DeserializedMafileData
{
public MobileData Data { get; init; }
public DeserializedMafileInfo Info { get; }
private DeserializedMafileData(MobileData mobileData, DeserializedMafileInfo info)
{
Data = mobileData;
Info = info;
}
internal static DeserializedMafileData Create(MobileData mobileData, int? version = null, Dictionary<string, JProperty>? properties = null, HashSet<string>? missingProperties = null, DeserializedMafileSessionResult sessionResult = DeserializedMafileSessionResult.Missing)
{
var info = DeserializedMafileInfo.Create(mobileData, version, properties, missingProperties, sessionResult);
return new DeserializedMafileData(mobileData, info);
}
internal static DeserializedMafileData CreateActual(MobileData mobileData, Dictionary<string, JProperty>? properties = null, DeserializedMafileSessionResult sessionResult = DeserializedMafileSessionResult.Valid)
{
var info = DeserializedMafileInfo.Create(mobileData, MafileSerializer.MAFILE_VERSION, properties, null, sessionResult);
return new DeserializedMafileData(mobileData, info);
}
}
/// <summary>
/// Represents information about deserialized mafile
/// </summary>
[PublicAPI]
public class DeserializedMafileInfo
{
public static readonly HashSet<string> ImportantProperties =
[
nameof(MobileDataExtended.RevocationCode),
nameof(MobileDataExtended.AccountName),
];
public static readonly HashSet<string> NotImportantProperties =
[
nameof(MobileDataExtended.ServerTime),
nameof(MobileDataExtended.SerialNumber),
nameof(MobileDataExtended.Uri),
nameof(MobileDataExtended.TokenGid),
nameof(MobileDataExtended.Secret1),
nameof(MobileDataExtended.SteamId)
];
public int? Version { get; init; }
public bool IsExtended { get; init; }
public DeserializedMafileSessionResult SessionResult { get; init; }
public HashSet<string>? MissingImportantProperties { get; init; } = [];
public HashSet<string>? MissingProperties { get; init; } = [];
public Dictionary<string, JProperty>? UnusedProperties { get; init; }
public bool IsActual => Version == MafileSerializer.MAFILE_VERSION;
public bool HasUnusedProperties => UnusedProperties is { Count: > 0 };
public bool HasMissingProperties => MissingProperties is { Count: > 0 };
public bool HasMissingImportantProperties => MissingImportantProperties is { Count: > 0 };
public bool HasSession => SessionResult == DeserializedMafileSessionResult.Valid;
public bool HasIdentificationProperty { get; init; }
internal static DeserializedMafileInfo Create(MobileData mobileData, int? version = null, Dictionary<string, JProperty>? unusedProperties = null, HashSet<string>? missingProperties = null,
DeserializedMafileSessionResult sessionResult = DeserializedMafileSessionResult.Missing)
{
HashSet<string>? missingImportantProperties = null;
var hasIdentificationProperty = false;
var isExtended = false;
if (mobileData is MobileDataExtended ext)
{
hasIdentificationProperty = !string.IsNullOrWhiteSpace(ext.AccountName) || ext.SteamId.Steam64.Id > SteamId64.SEED;
isExtended = true;
}
if (isExtended && missingProperties is { Count: > 0 })
{
var important = missingProperties.Intersect(ImportantProperties).ToList();
if (important.Count > 0) missingImportantProperties = important.ToHashSet();
var notImportant = missingProperties.Intersect(NotImportantProperties).ToList();
if (notImportant.Count > 0) missingProperties = notImportant.ToHashSet();
}
return new DeserializedMafileInfo
{
Version = version,
IsExtended = isExtended,
MissingImportantProperties = missingImportantProperties,
MissingProperties = missingProperties,
UnusedProperties = unusedProperties,
SessionResult = sessionResult,
HasIdentificationProperty = hasIdentificationProperty
};
}
}
@@ -0,0 +1,19 @@
using Newtonsoft.Json;
namespace SteamLib.Utility.MafileSerialization;
internal class LegacyMafile
{
[JsonProperty("shared_secret"), JsonRequired] public string SharedSecret { get; set; } = default!;
[JsonProperty("identity_secret"), JsonRequired] public string IdentitySecret { get; set; } = default!;
[JsonProperty("device_id"), JsonRequired] public string DeviceId { get; set; } = default!;
[JsonProperty("revocation_code")] public string RevocationCode { get; set; } = default!;
[JsonProperty("account_name")] public string AccountName { get; set; } = default!;
[JsonProperty("Session")] public object? SessionData { get; set; } = default!;
[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
}
@@ -1,5 +1,8 @@
namespace SteamLib.Utility.MaFiles;
using JetBrains.Annotations;
namespace SteamLib.Utility.MafileSerialization;
[PublicAPI]
public class MafileCredits : IMafileCredits
{
internal static readonly MafileCredits Instance = new();
@@ -1,23 +1,28 @@
using Newtonsoft.Json;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SteamLib.Account;
using SteamLib.Core.Models;
namespace SteamLib.Utility.MaFiles;
namespace SteamLib.Utility.MafileSerialization;
public static partial class MafileSerializer
[PublicAPI]
public partial class MafileSerializer
{
public const int MAFILE_VERSION = 2;
public const int MAFILE_VERSION = 3;
public const string SIGNATURE_PROPERTY_NAME = "@SLSV";
private static readonly HashSet<string> ActualProperties = typeof(MobileDataExtended).GetProperties().Select(x => x.Name).ToHashSet();
public MafileSerializerSettings Settings { get; }
//TODO: Options with:
//allowSessionIdGeneration
//allowDeviceIdGeneration
//allowInvalidTokensGeneration
//etc…
public static MobileData Deserialize(string json, bool allowSessionIdGeneration, out DeserializedMafileData mafileData)
public MafileSerializer(MafileSerializerSettings? settings = null)
{
Settings = settings ?? new MafileSerializerSettings();
}
public DeserializedMafileData Deserialize(string json)
{
var j = JObject.Parse(json);
@@ -47,9 +52,7 @@ public static partial class MafileSerializer
var data = j.ToObject<MobileDataExtended>()!;
data.SessionData = Validate.ValidateMobileData(data, out var sessionResult);
mafileData = DeserializedMafileData.CreateActual(unused, sessionResult);
return data;
return DeserializedMafileData.CreateActual(data, unused, sessionResult);
}
catch (Exception ex)
{
@@ -57,68 +60,71 @@ public static partial class MafileSerializer
}
}
var sharedSecretToken = GetTokenOrThrow(j, nameof(MobileData.SharedSecret), unusedProperties, "sharedsecret", "shared_secret", "shared");
var identitySecretToken = GetTokenOrThrow(j, nameof(MobileData.IdentitySecret), unusedProperties, "identitysecret", "identity_secret", "identity");
var deviceIdToken = GetTokenOrThrow(j, nameof(MobileData.DeviceId), unusedProperties, "deviceid", "device_id", "device");
//TODO: see MobileData.DeviceId ToDo
var sharedSecretToken = GetTokenOrThrow(j, unusedProperties, nameof(MobileData.SharedSecret), "sharedsecret", "shared_secret", "shared");
var identitySecretToken = GetTokenOrThrow(j, unusedProperties, nameof(MobileData.IdentitySecret), "identitysecret", "identity_secret", "identity");
var deviceId = GetDeviceId(j, Settings, unusedProperties, nameof(MobileData.DeviceId), "deviceid", "device_id", "device");
var sharedSecret = GetBase64(nameof(MobileData.SharedSecret), sharedSecretToken);
var identitySecret = GetBase64(nameof(MobileData.IdentitySecret), identitySecretToken);
var deviceId = deviceIdToken.Value<string>();
Validate.NotNullOrEmpty(nameof(MobileData.DeviceId), deviceId);
var accountNameToken = GetToken(j, unusedProperties, nameof(MobileDataExtended.AccountName), "account_name", "accountname");
var revocationCodeToken = GetToken(j, unusedProperties, nameof(MobileDataExtended.RevocationCode), "revocation_code", "rcode", "r_code", "revocationcode");
var accountName = GetString(j, unusedProperties, nameof(MobileDataExtended.AccountName), "account_name", "accountname");
var revocationCode = GetString(j, unusedProperties, nameof(MobileDataExtended.RevocationCode), "revocation_code", "rcode", "r_code", "revocationcode");
var sessionDataToken = GetToken(j, unusedProperties, nameof(MobileDataExtended.SessionData), "session_data", "sessiondata", "session");
var accountName = accountNameToken?.Value<string>();
var revocationCode = revocationCodeToken?.Value<string>();
//TODO: Better handling & determination of minified
var minified
= string.IsNullOrWhiteSpace(accountName)
&& string.IsNullOrWhiteSpace(revocationCode)
= string.IsNullOrWhiteSpace(accountName)
&& string.IsNullOrWhiteSpace(revocationCode)
&& (sessionDataToken == null || sessionDataToken.Type == JTokenType.Null);
if (minified)
{
mafileData = DeserializedMafileData.Create(version, false, unusedProperties);
return new MobileData
var data = new MobileData
{
DeviceId = deviceId,
IdentitySecret = identitySecret,
SharedSecret = sharedSecret
};
return DeserializedMafileData.Create(data, version, unusedProperties);
}
var missingProperties = new List<string>();
if (string.IsNullOrWhiteSpace(accountName)) missingProperties.Add(nameof(MobileDataExtended.AccountName));
if (string.IsNullOrWhiteSpace(revocationCode)) missingProperties.Add(nameof(MobileDataExtended.RevocationCode));
var serverTime = GetLong(j, unusedProperties, nameof(MobileDataExtended.ServerTime), "server_time", "servertime");
var serialNumber = GetULong(j, unusedProperties, nameof(MobileDataExtended.SerialNumber), "serial_number", "serialnumber");
var uri = GetString(j,unusedProperties, nameof(MobileDataExtended.Uri), "url", "uri");
var serverTime = GetLong(j, unusedProperties, nameof(MobileDataExtended.ServerTime), "server_time", "servertime");
var serialNumber = GetSerialNumber(j, Settings, nameof(MobileDataExtended.SerialNumber), unusedProperties, "serial_number", "serialnumber");
var uri = GetString(j, unusedProperties, nameof(MobileDataExtended.Uri), "url", "uri");
var tokenGid = GetString(j, unusedProperties, nameof(MobileDataExtended.TokenGid), "token_gid", "tokengid");
var secret1 = GetString(j, unusedProperties, nameof(MobileDataExtended.Secret1), "secret_1", "seecret1");
var steamId = GetSteamId(j, unusedProperties, nameof(MobileDataExtended.SteamId), "steam_id", "id");
if(serverTime == null) missingProperties.Add(nameof(MobileDataExtended.ServerTime));
if(serialNumber == null) missingProperties.Add(nameof(MobileDataExtended.SerialNumber));
if(string.IsNullOrWhiteSpace(uri)) missingProperties.Add(nameof(MobileDataExtended.Uri));
if(string.IsNullOrWhiteSpace(tokenGid)) missingProperties.Add(nameof(MobileDataExtended.TokenGid));
if(string.IsNullOrWhiteSpace(secret1)) missingProperties.Add(nameof(MobileDataExtended.Secret1));
if (serverTime == null) missingProperties.Add(nameof(MobileDataExtended.ServerTime));
if (serialNumber == null) missingProperties.Add(nameof(MobileDataExtended.SerialNumber));
if (string.IsNullOrWhiteSpace(uri)) missingProperties.Add(nameof(MobileDataExtended.Uri));
if (string.IsNullOrWhiteSpace(tokenGid)) missingProperties.Add(nameof(MobileDataExtended.TokenGid));
if (string.IsNullOrWhiteSpace(secret1)) missingProperties.Add(nameof(MobileDataExtended.Secret1));
MobileSessionData? sessionData = null;
var sResult = DeserializedMafileSessionResult.Missing;
if (sessionDataToken is { Type: JTokenType.Object })
if (sessionDataToken is JObject sessionObj)
{
sessionData = DeserializeMobileSessionData((JObject) sessionDataToken, allowSessionIdGeneration, out sResult);
sessionData = DeserializeMobileSessionData(sessionObj, out sResult, out steamId);
}
if ((steamId == null || steamId.Value.Steam64.Id < SteamId64.SEED) && Settings.DeserializationOptions.ThrowIfInvalidSteamId)
{
throw new ArgumentException("Can't retrieve SteamId from Mafile", nameof(MobileDataExtended.SteamId));
}
mafileData = DeserializedMafileData.Create(version, true, unusedProperties, missingProperties.ToHashSet(), sResult);
return new MobileDataExtended
if (steamId == null) missingProperties.Add(nameof(MobileDataExtended.SteamId));
// ReSharper disable once UseObjectOrCollectionInitializer
var mobileData = new MobileDataExtended
{
DeviceId = deviceId,
IdentitySecret = identitySecret,
@@ -130,8 +136,11 @@ public static partial class MafileSerializer
Uri = uri ?? string.Empty,
TokenGid = tokenGid ?? string.Empty,
Secret1 = secret1 ?? string.Empty,
SessionData = sessionData
SessionData = sessionData,
};
mobileData.SteamId = steamId.GetValueOrDefault(); //Keep it here because setting SessionData will override SteamId
return DeserializedMafileData.Create(mobileData, version, unusedProperties, missingProperties.ToHashSet(), sResult);
}
}
@@ -0,0 +1,34 @@
namespace SteamLib.Utility.MafileSerialization;
public class MafileSerializerSettings
{
public MafileDeserializationOptions DeserializationOptions { get; set; } = new();
[Obsolete("Currently not used")]
public MafileDeserializationOptions SerializationOptions { get; set; } = new();
}
public class MafileDeserializationOptions
{
public bool AllowDeviceIdGeneration { get; set; }
public bool AllowSessionIdGeneration { get; set; }
/// <summary>
/// Throws if the <see cref="MobileDataExtended.SerialNumber"/> is 0 or invalid. Otherwise, SerialNumber will be set to 0.
/// </summary>
public bool ThrowIfInvalidSerialNumber { get; set; }
/// <summary>
/// Restricts recovering an invalid <see cref="MobileDataExtended.SerialNumber"/> if the value is written as a negative number.
/// This can occur when an incompatible type is used, one that does not support large proto fixed64 values.
/// <br/> Returns 0 if <see langword="true"/>, instead of attempting to repair the value.
/// </summary>
public bool RestrictOverflowSerialNumberRecovery { get; set; }
public bool ThrowIfInvalidSteamId { get; set; }
}
public class MafileSerializationOptions
{
}
@@ -0,0 +1,121 @@
using Newtonsoft.Json.Linq;
using SteamLib.Account;
using SteamLib.Authentication;
using SteamLib.Core.Enums;
using SteamLib.Core.Models;
using System.Security.Cryptography;
using System.Text;
namespace SteamLib.Utility.MafileSerialization;
public partial class MafileSerializer //SessionData
{
private MobileSessionData? DeserializeMobileSessionData(JObject j, out DeserializedMafileSessionResult result,
out SteamId? steamId)
{
steamId = GetSessionSteamId(j);
result = DeserializedMafileSessionResult.Invalid;
var refreshToken = GetAuthToken(j, nameof(MobileSessionData.RefreshToken), "refreshtoken", "refresh_token",
"refresh", "OAuthToken");
if (refreshToken is not { Type: SteamAccessTokenType.MobileRefresh })
{
result = DeserializedMafileSessionResult.Invalid;
return null;
}
var accessToken = GetAuthToken(j, "accesstoken", "access_token", "access", "steamLoginSecure");
if (accessToken is not { Type: SteamAccessTokenType.Mobile })
{
accessToken = null;
}
steamId = refreshToken.Value.SteamId;
var sessionId = GetSessionId(j, Settings, "sessionid", "session_id", "session");
if (sessionId == null)
{
result = DeserializedMafileSessionResult.Invalid;
return null;
}
var sessionData = new MobileSessionData(sessionId, steamId.Value, refreshToken.Value, accessToken, tokens: null);
sessionData.IsValid = SessionDataValidator.Validate(null, sessionData).Succeeded;
if (sessionData.IsValid == false)
return null;
return sessionData;
}
private static SteamAuthToken? GetAuthToken(JObject j, params string[] aliases)
{
var jAuthToken = GetToken(j, aliases);
SteamAuthToken? token = null;
if (jAuthToken == null || jAuthToken.Type == JTokenType.Null) return null;
if (jAuthToken.Type == JTokenType.String &&
SteamTokenHelper.TryParse(jAuthToken.Value<string>()!, out var parsed))
{
token = parsed;
}
else if (jAuthToken.Type == JTokenType.Object)
{
try
{
token = jAuthToken.ToObject<SteamAuthToken>();
}
catch
{
//Ignored
}
}
return token;
}
private static SteamId? GetSessionSteamId(JObject j)
{
var token = GetToken(j, "steamid");
if (token == null || token.Type == JTokenType.Null)
return null;
if (token.Type == JTokenType.Integer)
return SteamId.FromSteam64(token.Value<long>());
if (token.Type == JTokenType.String && SteamId64.TryParse(token.Value<string>(), out var steamId))
{
return new SteamId(steamId);
}
return null;
}
private static string? GetSessionId(JObject j, MafileSerializerSettings settings, params string[] aliases)
{
var sessionId = GetString(j, aliases);
if (sessionId == null && settings.DeserializationOptions.AllowSessionIdGeneration)
{
return GenerateRandomHex();
}
return sessionId;
static string GenerateRandomHex(int byteLength = 12)
{
byte[] randomBytes = new byte[byteLength];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomBytes);
}
var hex = new StringBuilder(byteLength * 2);
foreach (var b in randomBytes)
hex.Append($"{b:x2}");
return hex.ToString();
}
}
}
@@ -1,14 +1,16 @@
using Newtonsoft.Json.Linq;
using System.Numerics;
using Newtonsoft.Json.Linq;
using SteamLib.Core.Models;
namespace SteamLib.Utility.MaFiles;
namespace SteamLib.Utility.MafileSerialization;
public partial class MafileSerializer //Utility
{
private static JToken? GetToken(JObject j, params string[] aliases)
private static JToken? GetToken(JObject j, params string[] aliases)
{
foreach (var name in aliases)
{
if (j.TryGetValue(name, StringComparison.OrdinalIgnoreCase, out var token))
if (j.TryGetValue(name, StringComparison.InvariantCultureIgnoreCase, out var token))
{
return token;
}
@@ -21,7 +23,7 @@ public partial class MafileSerializer //Utility
{
foreach (var name in aliases)
{
if (!j.TryGetValue(name, StringComparison.OrdinalIgnoreCase, out var token)) continue;
if (!j.TryGetValue(name, StringComparison.InvariantCultureIgnoreCase, out var token)) continue;
var parent = token.Parent as JProperty;
removeFrom.Remove(parent!.Name);
return token;
@@ -30,11 +32,11 @@ public partial class MafileSerializer //Utility
return null;
}
private static JToken GetTokenOrThrow(JObject j, string propertyName, Dictionary<string, JProperty> removeFrom, params string[] aliases)
private static JToken GetTokenOrThrow(JObject j, Dictionary<string, JProperty> removeFrom, string propertyName, params string[] aliases)
{
foreach (var name in aliases)
{
if (!j.TryGetValue(name, StringComparison.OrdinalIgnoreCase, out var token)) continue;
if (!j.TryGetValue(name, StringComparison.InvariantCultureIgnoreCase, out var token)) continue;
if (token.Type == JTokenType.Null)
{
throw new ArgumentException($"Required property {propertyName} is null");
@@ -66,7 +68,7 @@ public partial class MafileSerializer //Utility
if (token == null || token.Type == JTokenType.Null)
return null;
return token.Value<string>();
@@ -129,4 +131,76 @@ public partial class MafileSerializer //Utility
$"Not valid token type for base64 property '{propertyName}'. Type: {token.Type}");
}
}
private static string GetDeviceId(JObject j, MafileSerializerSettings settings, Dictionary<string, JProperty> removeFrom, string propertyName,
params string[] aliases)
{
var deviceId = GetString(j, removeFrom, aliases);
if (string.IsNullOrWhiteSpace(deviceId) && !settings.DeserializationOptions.AllowDeviceIdGeneration)
{
throw new ArgumentException($"Required property {propertyName} not found");
}
return deviceId ?? GenerateDeviceId();
static string GenerateDeviceId()
{
return "android:" + Guid.NewGuid();
}
}
private static ulong? GetSerialNumber(JObject j, MafileSerializerSettings settings, string propertyName, Dictionary<string, JProperty> removeFrom,
params string[] aliases)
{
var token = GetToken(j, removeFrom, aliases);
if (token == null || token.Type == JTokenType.Null)
return null;
if (token.Type is JTokenType.Integer or JTokenType.String)
{
var bigInt = token.ToObject<BigInteger>();
ulong res;
if (bigInt < ulong.MinValue && bigInt > long.MinValue) //Negative, e.g. -2260921916482386064
{
res = settings.DeserializationOptions.RestrictOverflowSerialNumberRecovery ? 0 : GetFromOverflow((long)bigInt);
}
else if (bigInt > ulong.MinValue && bigInt < ulong.MaxValue) //Valid range
{
res = (ulong)bigInt;
}
else
{
res = 0;
}
if (res == 0 && settings.DeserializationOptions.ThrowIfInvalidSerialNumber)
throw new ArgumentException(
$"SerialNumber has invalid value. Value: '{token.ToObject<object>()}'. Property: '{(token as JProperty)?.Name ?? propertyName}'");
return res;
}
throw new ArgumentOutOfRangeException(nameof(token.Type),
$"Not valid token type for base64 property '{propertyName}'. Type: {token.Type}");
static ulong GetFromOverflow(long overflow)
{
ulong originalValue;
unchecked
{
originalValue = (ulong)overflow + ulong.MaxValue + 1;
}
return originalValue;
}
}
private static SteamId? GetSteamId(JObject j, Dictionary<string, JProperty> removeFrom,
params string[] aliases)
{
var id = GetLong(j, removeFrom, aliases);
return id switch
{
null or < SteamId64.SEED => null,
_ => SteamId.FromSteam64(id.Value)
};
}
}
@@ -1,9 +1,8 @@
using Newtonsoft.Json.Linq;
using SteamLib.Account;
using SteamLib.Authentication;
using SteamLib.Core.Interfaces;
namespace SteamLib.Utility.MaFiles;
namespace SteamLib.Utility.MafileSerialization;
public partial class MafileSerializer //Validate
{
@@ -60,11 +59,6 @@ public partial class MafileSerializer //Validate
if (d.SessionData == null) return null;
sessionResult = DeserializedMafileSessionResult.Invalid;
if (d.SessionData.RefreshToken.IsExpired)
{
sessionResult = DeserializedMafileSessionResult.Expired;
}
d.SessionData.IsValid = SessionDataValidator.Validate(null, d.SessionData).Succeeded;
if (d.SessionData.IsValid == false) return null;
@@ -73,19 +67,5 @@ public partial class MafileSerializer //Validate
}
return null;
}
public static bool ValidateSessionData(ISessionData sessionData, out bool isOutdated)
{
if (sessionData.RefreshToken.IsExpired)
{
isOutdated = true;
return false;
}
isOutdated = false;
return SessionDataValidator.Validate(null, sessionData).Succeeded;
}
}
}
@@ -1,12 +1,12 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace SteamLib.Utility.MaFiles;
namespace SteamLib.Utility.MafileSerialization;
public static partial class MafileSerializer //Write
public partial class MafileSerializer //Write
{
private const string CREDITS_PROPERTY_NAME = "Credits";
public static string Serialize(MobileData mobileData, Formatting formatting = Formatting.Indented, bool sign = true, MafileCredits? credits = null)
public static string Serialize(MobileDataExtended mobileData, Formatting formatting = Formatting.Indented, bool sign = true, MafileCredits? credits = null)
{
using var w = new StringWriter();
using var write = new JsonTextWriter(w);
@@ -25,7 +25,7 @@ public static partial class MafileSerializer //Write
}
public static async Task<string> SerializeAsync(MobileData mobileData, Formatting formatting = Formatting.Indented, bool sign = true, MafileCredits? credits = null)
public static async Task<string> SerializeAsync(MobileDataExtended mobileData, Formatting formatting = Formatting.Indented, bool sign = true, MafileCredits? credits = null)
{
await using var w = new StringWriter();
await using var write = new JsonTextWriter(w);
@@ -52,7 +52,6 @@ public static partial class MafileSerializer //Write
SharedSecret = mobileData.SharedSecret,
IdentitySecret = mobileData.IdentitySecret,
DeviceId = mobileData.DeviceId,
};
if (mobileData is MobileDataExtended ext)
@@ -62,18 +61,19 @@ public static partial class MafileSerializer //Write
result.SessionData = ext.SessionData == null
? null
: new
{
AccessToken = ext.SessionData?.MobileToken?.Token,
steamLoginSecure = ext.SessionData?.MobileToken?.SignedToken,
RefreshToken = ext.SessionData?.RefreshToken.Token,
SteamID = ext.SessionData?.SteamId.Steam64.Id,
SessionID = ext.SessionData?.SessionId
};
{
AccessToken = ext.SessionData?.MobileToken?.Token,
steamLoginSecure = ext.SessionData?.MobileToken?.SignedToken,
RefreshToken = ext.SessionData?.RefreshToken.Token,
SteamID = ext.SessionData?.SteamId.Steam64.Id,
SessionID = ext.SessionData?.SessionId
};
result.ServerTime = ext.ServerTime;
result.SerialNumber = ext.SerialNumber.ToString();
result.Uri = ext.Uri;
result.TokenGid = ext.TokenGid;
result.Secret1 = ext.Secret1;
result.SteamId = ext.SteamId.Steam64.Id;
}
+19 -7
View File
@@ -1,5 +1,5 @@
using System.Text.RegularExpressions;
using SteamLib.Account;
using SteamLib.Core.Models;
namespace SteamLib.Utility;
@@ -37,19 +37,31 @@ public static class SteamIdParser
return false;
}
public static bool TryParse64(string input, out SteamId64 result)
public static bool TryParse64(string? input, out SteamId64 result)
{
result = default;
if (input == null) return false;
var match64 = Steam64Regex.Match(input);
if (match64.Success)
{
result = new SteamId64(long.Parse(match64.Value));
return true;
return TryParse64(long.Parse(match64.Value), out result);
}
result = default;
return false;
}
public static bool TryParse64(long input, out SteamId64 result)
{
result = default;
if (input < SteamId64.SEED)
{
return false;
}
result = new SteamId64(input);
return true;
}
public static bool TryParse2(string input, out SteamId2 result)
{
var match2 = Steam2Regex.Match(input);
@@ -95,7 +107,7 @@ public static class SteamIdParser
if (TryParse2(input, out var steam2))
{
return new SteamId(steam2);
}
if (TryParse3(input, out var steam3))
@@ -144,7 +156,7 @@ public static class SteamIdParser
var type = match3.Groups["type"].Value[0];
var id = int.Parse(match3.Groups["id"].Value);
return new SteamId3(id, type);
}
else
{
@@ -1,5 +1,5 @@
using Newtonsoft.Json;
using SteamLib.Account;
using SteamLib.Core.Models;
namespace SteamLib.Web.Converters;
@@ -18,7 +18,7 @@ public class SteamIdToSteam64Converter : JsonConverter<SteamId>
return SteamId.FromSteam64(l);
}
var str = (string) reader.Value!;
var str = (string)reader.Value!;
return new SteamId(SteamId64.Parse(str));
}
}
@@ -85,8 +85,8 @@ public class SteamId2ToStringConverter : JsonConverter<SteamId2>
public override SteamId2 ReadJson(JsonReader reader, Type objectType, SteamId2 existingValue, bool hasExistingValue,
JsonSerializer serializer)
{
var str = (string)reader.Value!;
return SteamId2.Parse(str);
var str = (string)reader.Value!;
return SteamId2.Parse(str);
}
}
+80
View File
@@ -0,0 +1,80 @@
<!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">
<!-- Changelog entry -->
<div class="change">
<div class="version">Version 1.5.4</div>
<div class="date">DATE</div>
<div class="description">
- <b>NEWS:</b> Official Telegram group now available! Join us at <b><a href="https://t.me/nebulaauth">t.me/nebulaauth</a></b> <br />
- <b>FIX:</b> TEMPORARY: Added workaround for old mafiles in MafileSerializer <br />
</div>
</div>
</div>
</body>
</html>