mirror of
https://github.com/achiez/NebulaAuth-Steam-Desktop-Authenticator-by-Achies.git
synced 2026-07-25 06:14:31 +00:00
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:
@@ -1,10 +1,20 @@
|
|||||||
using AchiesUtilities.Extensions;
|
using AchiesUtilities.Extensions;
|
||||||
using NebulaAuth.LegacyConverter;
|
using NebulaAuth.LegacyConverter;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using SteamLib.Utility.MaFiles;
|
using SteamLib.Utility.MafileSerialization;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var mafileSerializer = new MafileSerializer(new MafileSerializerSettings
|
||||||
|
{
|
||||||
|
DeserializationOptions =
|
||||||
|
{
|
||||||
|
AllowDeviceIdGeneration = true,
|
||||||
|
AllowSessionIdGeneration = true,
|
||||||
|
}
|
||||||
|
});
|
||||||
var currentPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
|
var currentPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
|
||||||
if (currentPath != null)
|
if (currentPath != null)
|
||||||
Environment.CurrentDirectory = currentPath;
|
Environment.CurrentDirectory = currentPath;
|
||||||
@@ -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 legacy = MafileSerializer.SerializeLegacy(maf, Formatting.Indented);
|
||||||
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
|
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
|
||||||
Write(legacy, fileNameWithoutExtension);
|
Write(legacy, fileNameWithoutExtension);
|
||||||
|
|||||||
@@ -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.1.html = changelog\1.5.1.html
|
||||||
changelog\1.5.2.html = changelog\1.5.2.html
|
changelog\1.5.2.html = changelog\1.5.2.html
|
||||||
changelog\1.5.3.html = changelog\1.5.3.html
|
changelog\1.5.3.html = changelog\1.5.3.html
|
||||||
|
changelog\1.5.4.html = changelog\1.5.4.html
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public partial class Mafile : MobileDataExtended
|
|||||||
ServerTime = data.ServerTime,
|
ServerTime = data.ServerTime,
|
||||||
TokenGid = data.TokenGid,
|
TokenGid = data.TokenGid,
|
||||||
Uri = data.Uri,
|
Uri = data.Uri,
|
||||||
|
SteamId = data.SteamId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
public static Mafile FromMobileDataExtended(MobileDataExtended data, MaProxy? proxy, string? group, string? password)
|
public static Mafile FromMobileDataExtended(MobileDataExtended data, MaProxy? proxy, string? group, string? password)
|
||||||
|
|||||||
@@ -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
@@ -1,19 +1,18 @@
|
|||||||
using NebulaAuth.Model.Entities;
|
using NebulaAuth.Model.Entities;
|
||||||
using Newtonsoft.Json;
|
using SteamLib.Core.Models;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using SteamLib;
|
|
||||||
using SteamLib.Account;
|
|
||||||
using SteamLib.Exceptions;
|
using SteamLib.Exceptions;
|
||||||
using SteamLib.SteamMobile;
|
using SteamLib.SteamMobile;
|
||||||
using SteamLib.Utility.MaFiles;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using AchiesUtilities.Extensions;
|
||||||
|
using SteamLib;
|
||||||
|
|
||||||
namespace NebulaAuth.Model;
|
namespace NebulaAuth.Model;
|
||||||
|
|
||||||
|
//RETHINK
|
||||||
public static class Storage
|
public static class Storage
|
||||||
{
|
{
|
||||||
public const string MAFILE_F = "maFiles";
|
public const string MAFILE_F = "maFiles";
|
||||||
@@ -42,21 +41,20 @@ public static class Storage
|
|||||||
var hashIds = new HashSet<SteamId>();
|
var hashIds = new HashSet<SteamId>();
|
||||||
var comparer = new MafileNameComparer(Settings.Instance.UseAccountNameAsMafileName);
|
var comparer = new MafileNameComparer(Settings.Instance.UseAccountNameAsMafileName);
|
||||||
var ordered = files.Order(comparer).ToList();
|
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
|
try
|
||||||
{
|
{
|
||||||
var data = ReadMafile(file);
|
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++;
|
DuplicateFound++;
|
||||||
Shell.Logger.Error("Duplicate mafile {file}", Path.GetFileName(file));
|
Shell.Logger.Error("Duplicate mafile {file}", Path.GetFileName(file));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
hashNames.Add(data.AccountName);
|
hashNames.Add(data.AccountName);
|
||||||
if (data.SessionData != null) hashIds.Add(data.SessionData.SteamId);
|
if (data.SessionData != null) hashIds.Add(data.SteamId);
|
||||||
MaFiles.Add(data);
|
MaFiles.Add(data);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -76,6 +74,11 @@ public static class Storage
|
|||||||
{
|
{
|
||||||
data = ReadMafile(path);
|
data = ReadMafile(path);
|
||||||
}
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
when(ex.ParamName == nameof(MobileDataExtended.SteamId))
|
||||||
|
{
|
||||||
|
throw new SessionInvalidException(); //Allows to handle LoginOnImport
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Shell.Logger.Warn(ex, "Can't load mafile");
|
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);
|
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)))
|
if (overwrite == false && File.Exists(CreatePathForMafile(data)))
|
||||||
{
|
{
|
||||||
throw new IOException("File already exist and overwrite is False");
|
throw new IOException("File already exist and overwrite is False");
|
||||||
@@ -115,61 +110,13 @@ public static class Storage
|
|||||||
public static Mafile ReadMafile(string path)
|
public static Mafile ReadMafile(string path)
|
||||||
{
|
{
|
||||||
var str = File.ReadAllText(path);
|
var str = File.ReadAllText(path);
|
||||||
var mafile = MafileSerializer.Deserialize(str, true, out var mafileData);
|
return NebulaSerializer.Deserialize(str);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SaveMafile(Mafile data)
|
public static void SaveMafile(Mafile data)
|
||||||
{
|
{
|
||||||
var path = CreatePathForMafile(data);
|
var path = CreatePathForMafile(data);
|
||||||
var str = SerializeMafile(data);
|
var str = NebulaSerializer.SerializeMafile(data);
|
||||||
File.WriteAllText(Path.GetFullPath(path), str);
|
File.WriteAllText(Path.GetFullPath(path), str);
|
||||||
|
|
||||||
var existed = MaFiles.SingleOrDefault(m => m.AccountName == data.AccountName);
|
var existed = MaFiles.SingleOrDefault(m => m.AccountName == data.AccountName);
|
||||||
@@ -187,23 +134,14 @@ public static class Storage
|
|||||||
public static void UpdateMafile(Mafile data)
|
public static void UpdateMafile(Mafile data)
|
||||||
{
|
{
|
||||||
var path = CreatePathForMafile(data);
|
var path = CreatePathForMafile(data);
|
||||||
var str = SerializeMafile(data);
|
var str = NebulaSerializer.SerializeMafile(data);
|
||||||
File.WriteAllText(Path.GetFullPath(path), str);
|
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)
|
public static void MoveToRemoved(Mafile data)
|
||||||
{
|
{
|
||||||
var path = CreatePathForMafile(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 copyPathCompleted = copyPath;
|
||||||
var i = 0;
|
var i = 0;
|
||||||
while (File.Exists(copyPathCompleted))
|
while (File.Exists(copyPathCompleted))
|
||||||
@@ -218,18 +156,9 @@ public static class Storage
|
|||||||
|
|
||||||
private static string CreatePathForMafile(Mafile data)
|
private static string CreatePathForMafile(Mafile data)
|
||||||
{
|
{
|
||||||
string fileName;
|
var fileName = Settings.Instance.UseAccountNameAsMafileName
|
||||||
if (Settings.Instance.UseAccountNameAsMafileName)
|
? CreateFileNameWithAccountName(data.AccountName)
|
||||||
{
|
: CreateFileNameWithSteamId(data.SteamId);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Path.Combine(MafileFolder, fileName);
|
return Path.Combine(MafileFolder, fileName);
|
||||||
}
|
}
|
||||||
@@ -238,13 +167,12 @@ public static class Storage
|
|||||||
private static string CreateFileNameWithSteamId(SteamId steamId) => steamId.Steam64.Id + ".mafile";
|
private static string CreateFileNameWithSteamId(SteamId steamId) => steamId.Steam64.Id + ".mafile";
|
||||||
|
|
||||||
public static string? TryFindMafilePath(Mafile data)
|
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));
|
var pathFileName = Path.Combine(MafileFolder, CreateFileNameWithAccountName(data.AccountName));
|
||||||
string? pathSteamId = null;
|
string? pathSteamId = null;
|
||||||
if (data.SessionData != 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);
|
var steamIdExist = pathSteamId != null && File.Exists(pathSteamId);
|
||||||
@@ -260,13 +188,9 @@ public static class Storage
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool ValidateCanSave(Mafile data)
|
|
||||||
{
|
|
||||||
return Settings.Instance.UseAccountNameAsMafileName || data.SessionData != null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: Refactor
|
||||||
internal class MafileNameComparer : IComparer<string>
|
internal class MafileNameComparer : IComparer<string>
|
||||||
{
|
{
|
||||||
public bool MafileNameMode { get; }
|
public bool MafileNameMode { get; }
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using NebulaAuth.Model;
|
using NebulaAuth.Model;
|
||||||
using NebulaAuth.Model.Entities;
|
using NebulaAuth.Model.Entities;
|
||||||
using System;
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using AchiesUtilities.Extensions;
|
||||||
|
|
||||||
namespace NebulaAuth.ViewModel;
|
namespace NebulaAuth.ViewModel;
|
||||||
|
|
||||||
@@ -47,7 +47,6 @@ public partial class MainVM //Groups
|
|||||||
var mafile = SelectedMafile;
|
var mafile = SelectedMafile;
|
||||||
if (mafile == null) return;
|
if (mafile == null) return;
|
||||||
if (string.IsNullOrEmpty(value)) return;
|
if (string.IsNullOrEmpty(value)) return;
|
||||||
if (!ValidateCanSaveAndWarn(mafile)) return;
|
|
||||||
|
|
||||||
mafile.Group = value;
|
mafile.Group = value;
|
||||||
Storage.UpdateMafile(mafile);
|
Storage.UpdateMafile(mafile);
|
||||||
@@ -67,7 +66,6 @@ public partial class MainVM //Groups
|
|||||||
var mafile = (Mafile?)value[1];
|
var mafile = (Mafile?)value[1];
|
||||||
|
|
||||||
if (group == null || mafile == null) return;
|
if (group == null || mafile == null) return;
|
||||||
if (!ValidateCanSaveAndWarn(mafile)) return;
|
|
||||||
mafile.Group = group;
|
mafile.Group = group;
|
||||||
Storage.UpdateMafile(mafile);
|
Storage.UpdateMafile(mafile);
|
||||||
QueryGroups();
|
QueryGroups();
|
||||||
@@ -79,7 +77,6 @@ public partial class MainVM //Groups
|
|||||||
private void RemoveGroup(Mafile? mafile)
|
private void RemoveGroup(Mafile? mafile)
|
||||||
{
|
{
|
||||||
if (mafile?.Group == null) return;
|
if (mafile?.Group == null) return;
|
||||||
if (!ValidateCanSaveAndWarn(mafile)) return;
|
|
||||||
var mafGroup = mafile.Group;
|
var mafGroup = mafile.Group;
|
||||||
mafile.Group = null;
|
mafile.Group = null;
|
||||||
Storage.UpdateMafile(mafile);
|
Storage.UpdateMafile(mafile);
|
||||||
@@ -136,11 +133,7 @@ public partial class MainVM //Groups
|
|||||||
|
|
||||||
bool SearchPredicate(Mafile mafile)
|
bool SearchPredicate(Mafile mafile)
|
||||||
{
|
{
|
||||||
if (!mafile.AccountName.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase))
|
return mafile.AccountName.ContainsIgnoreCase(SearchText) || mafile.SteamId.Steam64.Id.Equals(searchSteamId);
|
||||||
{
|
|
||||||
return mafile.SessionData != null && mafile.SessionData.SteamId.Steam64.Id.Equals(searchSteamId);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using System;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using NebulaAuth.Core;
|
using NebulaAuth.Core;
|
||||||
using NebulaAuth.Model;
|
using NebulaAuth.Model;
|
||||||
@@ -101,7 +102,6 @@ public partial class MainVM
|
|||||||
{
|
{
|
||||||
if (SelectedProxy == null) return;
|
if (SelectedProxy == null) return;
|
||||||
if (SelectedMafile == null) return;
|
if (SelectedMafile == null) return;
|
||||||
if (!ValidateCanSaveAndWarn(SelectedMafile)) return;
|
|
||||||
SelectedMafile.Proxy = null;
|
SelectedMafile.Proxy = null;
|
||||||
SelectedProxy = null;
|
SelectedProxy = null;
|
||||||
Storage.UpdateMafile(SelectedMafile);
|
Storage.UpdateMafile(SelectedMafile);
|
||||||
@@ -117,19 +117,9 @@ public partial class MainVM
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (SelectedMafile == null) return;
|
if (SelectedMafile == null) return;
|
||||||
if (!ValidateCanSaveAndWarn(SelectedMafile)) return;
|
|
||||||
ProxyExist = true;
|
ProxyExist = true;
|
||||||
SelectedMafile.Proxy = SelectedProxy;
|
SelectedMafile.Proxy = SelectedProxy;
|
||||||
Storage.UpdateMafile(SelectedMafile);
|
Storage.UpdateMafile(SelectedMafile);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ValidateCanSaveAndWarn(Mafile data)
|
|
||||||
{
|
|
||||||
var canSave = Storage.ValidateCanSave(data);
|
|
||||||
if (!canSave)
|
|
||||||
{
|
|
||||||
SnackbarController.SendSnackbar(GetLocalization("CantRetrieveSteamIDToUpdate"));
|
|
||||||
}
|
|
||||||
return canSave;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -328,8 +328,8 @@ public partial class LinkAccountVM : ObservableObject, IEmailProvider, IPhoneNum
|
|||||||
{
|
{
|
||||||
Directory.CreateDirectory("mafiles_backup");
|
Directory.CreateDirectory("mafiles_backup");
|
||||||
}
|
}
|
||||||
var json = Storage.SerializeMafile(data, null);
|
var json = NebulaSerializer.SerializeMafile(data, null);
|
||||||
File.WriteAllText(Path.Combine("mafiles_backup", data.AccountName + ".mafile"), json);
|
File.WriteAllText(Path.Combine("mafiles_backup", data.AccountName + ".mafile"), json); //TODO: Move logic to Storage
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Providers
|
#region Providers
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using SteamLib.Core.Interfaces;
|
using SteamLib.Core.Interfaces;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using SteamLib.Core.Models;
|
||||||
|
|
||||||
namespace SteamLib.Account;
|
namespace SteamLib.Account;
|
||||||
|
|
||||||
@@ -35,8 +36,8 @@ public sealed class MobileSessionData : SessionData, IMobileSessionData
|
|||||||
}
|
}
|
||||||
|
|
||||||
public MobileSessionData(string sessionId, SteamId steamId, SteamAuthToken refreshToken,
|
public MobileSessionData(string sessionId, SteamId steamId, SteamAuthToken refreshToken,
|
||||||
SteamAuthToken? mobileToken, IEnumerable<SteamAuthToken>? tokens)
|
SteamAuthToken? mobileToken, IEnumerable<SteamAuthToken>? tokensCollection)
|
||||||
: base(sessionId, steamId, refreshToken, tokens)
|
: base(sessionId, steamId, refreshToken, tokensCollection)
|
||||||
{
|
{
|
||||||
MobileToken = mobileToken;
|
MobileToken = mobileToken;
|
||||||
}
|
}
|
||||||
@@ -46,6 +47,4 @@ public sealed class MobileSessionData : SessionData, IMobileSessionData
|
|||||||
{
|
{
|
||||||
return new MobileSessionData(SessionId, SteamId, RefreshToken, MobileToken, Tokens);
|
return new MobileSessionData(SessionId, SteamId, RefreshToken, MobileToken, Tokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using SteamLib.Core.Enums;
|
using SteamLib.Core.Enums;
|
||||||
using SteamLib.Core.Interfaces;
|
using SteamLib.Core.Interfaces;
|
||||||
|
using SteamLib.Core.Models;
|
||||||
using SteamLib.Web.Converters;
|
using SteamLib.Web.Converters;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
@@ -8,8 +9,9 @@ namespace SteamLib.Account;
|
|||||||
|
|
||||||
public class SessionData : ISessionData
|
public class SessionData : ISessionData
|
||||||
{
|
{
|
||||||
[JsonIgnore]
|
[JsonIgnore] public bool? IsValid { get; set; }
|
||||||
public bool? IsValid { get; set; }
|
[JsonIgnore] public bool IsExpired => RefreshToken.IsExpired;
|
||||||
|
|
||||||
public string SessionId { get; }
|
public string SessionId { get; }
|
||||||
|
|
||||||
[JsonConverter(typeof(SteamIdToSteam64Converter))]
|
[JsonConverter(typeof(SteamIdToSteam64Converter))]
|
||||||
@@ -26,13 +28,13 @@ public class SessionData : ISessionData
|
|||||||
Tokens = new ConcurrentDictionary<SteamDomain, SteamAuthToken>(tokens ?? new Dictionary<SteamDomain, SteamAuthToken>());
|
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;
|
SessionId = sessionId;
|
||||||
SteamId = steamId;
|
SteamId = steamId;
|
||||||
RefreshToken = refreshToken;
|
RefreshToken = refreshToken;
|
||||||
tokens ??= Array.Empty<SteamAuthToken>();
|
tokensCollection ??= Array.Empty<SteamAuthToken>();
|
||||||
Tokens = new ConcurrentDictionary<SteamDomain, SteamAuthToken>(tokens.ToDictionary(t => t.Domain, t => t));
|
Tokens = new ConcurrentDictionary<SteamDomain, SteamAuthToken>(tokensCollection.ToDictionary(t => t.Domain, t => t));
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual SteamAuthToken? GetToken(SteamDomain domain)
|
public virtual SteamAuthToken? GetToken(SteamDomain domain)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using AchiesUtilities.Newtonsoft.JSON.Converters.Special;
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using SteamLib.Authentication;
|
using SteamLib.Authentication;
|
||||||
using SteamLib.Core.Enums;
|
using SteamLib.Core.Enums;
|
||||||
|
using SteamLib.Core.Models;
|
||||||
using SteamLib.Web.Converters;
|
using SteamLib.Web.Converters;
|
||||||
|
|
||||||
namespace SteamLib.Account;
|
namespace SteamLib.Account;
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ using SteamLib.ProtoCore.Enums;
|
|||||||
using SteamLib.ProtoCore.Exceptions;
|
using SteamLib.ProtoCore.Exceptions;
|
||||||
using SteamLib.ProtoCore.Services;
|
using SteamLib.ProtoCore.Services;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using SteamLib.Account;
|
using SteamLib.Core.Models;
|
||||||
|
|
||||||
namespace SteamLib.Api.Mobile;
|
namespace SteamLib.Api.Mobile;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using AchiesUtilities.Web.Extensions;
|
using AchiesUtilities.Web.Extensions;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using SteamLib.Account;
|
|
||||||
using SteamLib.Core;
|
using SteamLib.Core;
|
||||||
using SteamLib.Core.StatusCodes;
|
using SteamLib.Core.StatusCodes;
|
||||||
using SteamLib.Exceptions;
|
using SteamLib.Exceptions;
|
||||||
@@ -9,6 +8,7 @@ using SteamLib.SteamMobile.Confirmations;
|
|||||||
using SteamLib.Utility;
|
using SteamLib.Utility;
|
||||||
using SteamLib.Web.Scrappers.JSON;
|
using SteamLib.Web.Scrappers.JSON;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using SteamLib.Core.Models;
|
||||||
|
|
||||||
namespace SteamLib.Api.Mobile;
|
namespace SteamLib.Api.Mobile;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using SteamLib.Account;
|
|||||||
using SteamLib.Core;
|
using SteamLib.Core;
|
||||||
using SteamLib.Core.Enums;
|
using SteamLib.Core.Enums;
|
||||||
using SteamLib.Core.Interfaces;
|
using SteamLib.Core.Interfaces;
|
||||||
|
using SteamLib.Core.Models;
|
||||||
using SteamLib.Core.StatusCodes;
|
using SteamLib.Core.StatusCodes;
|
||||||
using SteamLib.Exceptions;
|
using SteamLib.Exceptions;
|
||||||
using SteamLib.Login.Default;
|
using SteamLib.Login.Default;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using SteamLib.Account;
|
|
||||||
using SteamLib.Core.Enums;
|
using SteamLib.Core.Enums;
|
||||||
using SteamLib.Core.Interfaces;
|
using SteamLib.Core.Interfaces;
|
||||||
|
using SteamLib.Core.Models;
|
||||||
using SteamLib.Exceptions;
|
using SteamLib.Exceptions;
|
||||||
|
|
||||||
namespace SteamLib.Authentication;
|
namespace SteamLib.Authentication;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using SteamLib.Core.Enums;
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using SteamLib.Core.Models;
|
||||||
|
|
||||||
namespace SteamLib.Authentication;
|
namespace SteamLib.Authentication;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using SteamLib.Account;
|
using SteamLib.Account;
|
||||||
using SteamLib.Core.Enums;
|
using SteamLib.Core.Enums;
|
||||||
|
using SteamLib.Core.Models;
|
||||||
|
|
||||||
namespace SteamLib.Core.Interfaces;
|
namespace SteamLib.Core.Interfaces;
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using SteamLib.Account;
|
using SteamLib.Account;
|
||||||
|
using SteamLib.Core.Models;
|
||||||
|
using SteamLib.Web.Converters;
|
||||||
|
|
||||||
namespace SteamLib;
|
namespace SteamLib;
|
||||||
|
|
||||||
@@ -9,24 +11,38 @@ public class MobileData
|
|||||||
{
|
{
|
||||||
[JsonRequired] public string SharedSecret { get; set; } = null!;
|
[JsonRequired] public string SharedSecret { get; set; } = null!;
|
||||||
[JsonRequired] public string IdentitySecret { get; set; } = null!;
|
[JsonRequired] public string IdentitySecret { get; set; } = null!;
|
||||||
[JsonRequired] public string DeviceId { get; set; } = null!;
|
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 class MobileDataExtended : MobileData
|
public class MobileDataExtended : MobileData
|
||||||
{
|
{
|
||||||
public string? RevocationCode { get; set; } = null!;
|
public string? RevocationCode { get; set; } = null!;
|
||||||
public string AccountName { 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
|
#region Unused
|
||||||
public long ServerTime { get; set; } //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 Uri { get; set; } = null!;//Unused
|
||||||
public string TokenGid { get; set; } = null!;//Unused
|
public string TokenGid { get; set; } = null!;//Unused
|
||||||
public string Secret1 { get; set; } = null!; //Unused
|
public string Secret1 { get; set; } = null!; //Unused
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ public class SteamAuthenticatorLinker
|
|||||||
SessionData = data;
|
SessionData = data;
|
||||||
|
|
||||||
data.EnsureValidated();
|
data.EnsureValidated();
|
||||||
if (data.RefreshToken.IsExpired)
|
if (data.IsExpired)
|
||||||
{
|
{
|
||||||
Logger?.LogError("Session expired");
|
Logger?.LogError("Session expired");
|
||||||
throw new SessionPermanentlyExpiredException(SessionPermanentlyExpiredException.SESSION_EXPIRED_MSG);
|
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
|
||||||
|
}
|
||||||
+4
-1
@@ -1,5 +1,8 @@
|
|||||||
namespace SteamLib.Utility.MaFiles;
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
|
namespace SteamLib.Utility.MafileSerialization;
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
public class MafileCredits : IMafileCredits
|
public class MafileCredits : IMafileCredits
|
||||||
{
|
{
|
||||||
internal static readonly MafileCredits Instance = new();
|
internal static readonly MafileCredits Instance = new();
|
||||||
+47
-38
@@ -1,23 +1,28 @@
|
|||||||
using Newtonsoft.Json;
|
using JetBrains.Annotations;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using SteamLib.Account;
|
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";
|
public const string SIGNATURE_PROPERTY_NAME = "@SLSV";
|
||||||
private static readonly HashSet<string> ActualProperties = typeof(MobileDataExtended).GetProperties().Select(x => x.Name).ToHashSet();
|
private static readonly HashSet<string> ActualProperties = typeof(MobileDataExtended).GetProperties().Select(x => x.Name).ToHashSet();
|
||||||
|
|
||||||
|
public MafileSerializerSettings Settings { get; }
|
||||||
|
|
||||||
//TODO: Options with:
|
public MafileSerializer(MafileSerializerSettings? settings = null)
|
||||||
//allowSessionIdGeneration
|
{
|
||||||
//allowDeviceIdGeneration
|
Settings = settings ?? new MafileSerializerSettings();
|
||||||
//allowInvalidTokensGeneration
|
}
|
||||||
//etc…
|
|
||||||
public static MobileData Deserialize(string json, bool allowSessionIdGeneration, out DeserializedMafileData mafileData)
|
|
||||||
|
public DeserializedMafileData Deserialize(string json)
|
||||||
{
|
{
|
||||||
|
|
||||||
var j = JObject.Parse(json);
|
var j = JObject.Parse(json);
|
||||||
@@ -47,9 +52,7 @@ public static partial class MafileSerializer
|
|||||||
var data = j.ToObject<MobileDataExtended>()!;
|
var data = j.ToObject<MobileDataExtended>()!;
|
||||||
data.SessionData = Validate.ValidateMobileData(data, out var sessionResult);
|
data.SessionData = Validate.ValidateMobileData(data, out var sessionResult);
|
||||||
|
|
||||||
mafileData = DeserializedMafileData.CreateActual(unused, sessionResult);
|
return DeserializedMafileData.CreateActual(data, unused, sessionResult);
|
||||||
return data;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -57,24 +60,19 @@ public static partial class MafileSerializer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var sharedSecretToken = GetTokenOrThrow(j, nameof(MobileData.SharedSecret), unusedProperties, "sharedsecret", "shared_secret", "shared");
|
var sharedSecretToken = GetTokenOrThrow(j, unusedProperties, nameof(MobileData.SharedSecret), "sharedsecret", "shared_secret", "shared");
|
||||||
var identitySecretToken = GetTokenOrThrow(j, nameof(MobileData.IdentitySecret), unusedProperties, "identitysecret", "identity_secret", "identity");
|
var identitySecretToken = GetTokenOrThrow(j, unusedProperties, nameof(MobileData.IdentitySecret), "identitysecret", "identity_secret", "identity");
|
||||||
var deviceIdToken = GetTokenOrThrow(j, nameof(MobileData.DeviceId), unusedProperties, "deviceid", "device_id", "device");
|
var deviceId = GetDeviceId(j, Settings, unusedProperties, nameof(MobileData.DeviceId), "deviceid", "device_id", "device");
|
||||||
//TODO: see MobileData.DeviceId ToDo
|
|
||||||
|
|
||||||
var sharedSecret = GetBase64(nameof(MobileData.SharedSecret), sharedSecretToken);
|
var sharedSecret = GetBase64(nameof(MobileData.SharedSecret), sharedSecretToken);
|
||||||
var identitySecret = GetBase64(nameof(MobileData.IdentitySecret), identitySecretToken);
|
var identitySecret = GetBase64(nameof(MobileData.IdentitySecret), identitySecretToken);
|
||||||
var deviceId = deviceIdToken.Value<string>();
|
|
||||||
Validate.NotNullOrEmpty(nameof(MobileData.DeviceId), deviceId);
|
Validate.NotNullOrEmpty(nameof(MobileData.DeviceId), deviceId);
|
||||||
|
|
||||||
var accountNameToken = GetToken(j, unusedProperties, nameof(MobileDataExtended.AccountName), "account_name", "accountname");
|
var accountName = GetString(j, unusedProperties, nameof(MobileDataExtended.AccountName), "account_name", "accountname");
|
||||||
var revocationCodeToken = GetToken(j, unusedProperties, nameof(MobileDataExtended.RevocationCode), "revocation_code", "rcode", "r_code", "revocationcode");
|
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 sessionDataToken = GetToken(j, unusedProperties, nameof(MobileDataExtended.SessionData), "session_data", "sessiondata", "session");
|
||||||
|
|
||||||
var accountName = accountNameToken?.Value<string>();
|
//TODO: Better handling & determination of minified
|
||||||
var revocationCode = revocationCodeToken?.Value<string>();
|
|
||||||
|
|
||||||
|
|
||||||
var minified
|
var minified
|
||||||
= string.IsNullOrWhiteSpace(accountName)
|
= string.IsNullOrWhiteSpace(accountName)
|
||||||
&& string.IsNullOrWhiteSpace(revocationCode)
|
&& string.IsNullOrWhiteSpace(revocationCode)
|
||||||
@@ -83,13 +81,14 @@ public static partial class MafileSerializer
|
|||||||
|
|
||||||
if (minified)
|
if (minified)
|
||||||
{
|
{
|
||||||
mafileData = DeserializedMafileData.Create(version, false, unusedProperties);
|
var data = new MobileData
|
||||||
return new MobileData
|
|
||||||
{
|
{
|
||||||
DeviceId = deviceId,
|
DeviceId = deviceId,
|
||||||
IdentitySecret = identitySecret,
|
IdentitySecret = identitySecret,
|
||||||
SharedSecret = sharedSecret
|
SharedSecret = sharedSecret
|
||||||
};
|
};
|
||||||
|
return DeserializedMafileData.Create(data, version, unusedProperties);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -98,27 +97,34 @@ public static partial class MafileSerializer
|
|||||||
if (string.IsNullOrWhiteSpace(revocationCode)) missingProperties.Add(nameof(MobileDataExtended.RevocationCode));
|
if (string.IsNullOrWhiteSpace(revocationCode)) missingProperties.Add(nameof(MobileDataExtended.RevocationCode));
|
||||||
|
|
||||||
var serverTime = GetLong(j, unusedProperties, nameof(MobileDataExtended.ServerTime), "server_time", "servertime");
|
var serverTime = GetLong(j, unusedProperties, nameof(MobileDataExtended.ServerTime), "server_time", "servertime");
|
||||||
var serialNumber = GetULong(j, unusedProperties, nameof(MobileDataExtended.SerialNumber), "serial_number", "serialnumber");
|
var serialNumber = GetSerialNumber(j, Settings, nameof(MobileDataExtended.SerialNumber), unusedProperties, "serial_number", "serialnumber");
|
||||||
var uri = GetString(j,unusedProperties, nameof(MobileDataExtended.Uri), "url", "uri");
|
var uri = GetString(j, unusedProperties, nameof(MobileDataExtended.Uri), "url", "uri");
|
||||||
var tokenGid = GetString(j, unusedProperties, nameof(MobileDataExtended.TokenGid), "token_gid", "tokengid");
|
var tokenGid = GetString(j, unusedProperties, nameof(MobileDataExtended.TokenGid), "token_gid", "tokengid");
|
||||||
var secret1 = GetString(j, unusedProperties, nameof(MobileDataExtended.Secret1), "secret_1", "seecret1");
|
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 (serverTime == null) missingProperties.Add(nameof(MobileDataExtended.ServerTime));
|
||||||
if(serialNumber == null) missingProperties.Add(nameof(MobileDataExtended.SerialNumber));
|
if (serialNumber == null) missingProperties.Add(nameof(MobileDataExtended.SerialNumber));
|
||||||
if(string.IsNullOrWhiteSpace(uri)) missingProperties.Add(nameof(MobileDataExtended.Uri));
|
if (string.IsNullOrWhiteSpace(uri)) missingProperties.Add(nameof(MobileDataExtended.Uri));
|
||||||
if(string.IsNullOrWhiteSpace(tokenGid)) missingProperties.Add(nameof(MobileDataExtended.TokenGid));
|
if (string.IsNullOrWhiteSpace(tokenGid)) missingProperties.Add(nameof(MobileDataExtended.TokenGid));
|
||||||
if(string.IsNullOrWhiteSpace(secret1)) missingProperties.Add(nameof(MobileDataExtended.Secret1));
|
if (string.IsNullOrWhiteSpace(secret1)) missingProperties.Add(nameof(MobileDataExtended.Secret1));
|
||||||
|
|
||||||
MobileSessionData? sessionData = null;
|
MobileSessionData? sessionData = null;
|
||||||
var sResult = DeserializedMafileSessionResult.Missing;
|
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);
|
if (steamId == null) missingProperties.Add(nameof(MobileDataExtended.SteamId));
|
||||||
return new MobileDataExtended
|
|
||||||
|
// ReSharper disable once UseObjectOrCollectionInitializer
|
||||||
|
var mobileData = new MobileDataExtended
|
||||||
{
|
{
|
||||||
DeviceId = deviceId,
|
DeviceId = deviceId,
|
||||||
IdentitySecret = identitySecret,
|
IdentitySecret = identitySecret,
|
||||||
@@ -130,8 +136,11 @@ public static partial class MafileSerializer
|
|||||||
Uri = uri ?? string.Empty,
|
Uri = uri ?? string.Empty,
|
||||||
TokenGid = tokenGid ?? string.Empty,
|
TokenGid = tokenGid ?? string.Empty,
|
||||||
Secret1 = secret1 ?? 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
+80
-6
@@ -1,6 +1,8 @@
|
|||||||
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
|
public partial class MafileSerializer //Utility
|
||||||
{
|
{
|
||||||
@@ -8,7 +10,7 @@ public partial class MafileSerializer //Utility
|
|||||||
{
|
{
|
||||||
foreach (var name in 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;
|
return token;
|
||||||
}
|
}
|
||||||
@@ -21,7 +23,7 @@ public partial class MafileSerializer //Utility
|
|||||||
{
|
{
|
||||||
foreach (var name in 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;
|
||||||
var parent = token.Parent as JProperty;
|
var parent = token.Parent as JProperty;
|
||||||
removeFrom.Remove(parent!.Name);
|
removeFrom.Remove(parent!.Name);
|
||||||
return token;
|
return token;
|
||||||
@@ -30,11 +32,11 @@ public partial class MafileSerializer //Utility
|
|||||||
return null;
|
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)
|
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)
|
if (token.Type == JTokenType.Null)
|
||||||
{
|
{
|
||||||
throw new ArgumentException($"Required property {propertyName} is null");
|
throw new ArgumentException($"Required property {propertyName} is null");
|
||||||
@@ -129,4 +131,76 @@ public partial class MafileSerializer //Utility
|
|||||||
$"Not valid token type for base64 property '{propertyName}'. Type: {token.Type}");
|
$"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
-21
@@ -1,9 +1,8 @@
|
|||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using SteamLib.Account;
|
using SteamLib.Account;
|
||||||
using SteamLib.Authentication;
|
using SteamLib.Authentication;
|
||||||
using SteamLib.Core.Interfaces;
|
|
||||||
|
|
||||||
namespace SteamLib.Utility.MaFiles;
|
namespace SteamLib.Utility.MafileSerialization;
|
||||||
|
|
||||||
public partial class MafileSerializer //Validate
|
public partial class MafileSerializer //Validate
|
||||||
{
|
{
|
||||||
@@ -60,11 +59,6 @@ public partial class MafileSerializer //Validate
|
|||||||
if (d.SessionData == null) return null;
|
if (d.SessionData == null) return null;
|
||||||
|
|
||||||
sessionResult = DeserializedMafileSessionResult.Invalid;
|
sessionResult = DeserializedMafileSessionResult.Invalid;
|
||||||
if (d.SessionData.RefreshToken.IsExpired)
|
|
||||||
{
|
|
||||||
sessionResult = DeserializedMafileSessionResult.Expired;
|
|
||||||
}
|
|
||||||
|
|
||||||
d.SessionData.IsValid = SessionDataValidator.Validate(null, d.SessionData).Succeeded;
|
d.SessionData.IsValid = SessionDataValidator.Validate(null, d.SessionData).Succeeded;
|
||||||
if (d.SessionData.IsValid == false) return null;
|
if (d.SessionData.IsValid == false) return null;
|
||||||
|
|
||||||
@@ -73,19 +67,5 @@ public partial class MafileSerializer //Validate
|
|||||||
}
|
}
|
||||||
return null;
|
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;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+5
-5
@@ -1,12 +1,12 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
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";
|
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 w = new StringWriter();
|
||||||
using var write = new JsonTextWriter(w);
|
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 w = new StringWriter();
|
||||||
await using var write = new JsonTextWriter(w);
|
await using var write = new JsonTextWriter(w);
|
||||||
@@ -52,7 +52,6 @@ public static partial class MafileSerializer //Write
|
|||||||
SharedSecret = mobileData.SharedSecret,
|
SharedSecret = mobileData.SharedSecret,
|
||||||
IdentitySecret = mobileData.IdentitySecret,
|
IdentitySecret = mobileData.IdentitySecret,
|
||||||
DeviceId = mobileData.DeviceId,
|
DeviceId = mobileData.DeviceId,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mobileData is MobileDataExtended ext)
|
if (mobileData is MobileDataExtended ext)
|
||||||
@@ -74,6 +73,7 @@ public static partial class MafileSerializer //Write
|
|||||||
result.Uri = ext.Uri;
|
result.Uri = ext.Uri;
|
||||||
result.TokenGid = ext.TokenGid;
|
result.TokenGid = ext.TokenGid;
|
||||||
result.Secret1 = ext.Secret1;
|
result.Secret1 = ext.Secret1;
|
||||||
|
result.SteamId = ext.SteamId.Steam64.Id;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using SteamLib.Account;
|
using SteamLib.Core.Models;
|
||||||
|
|
||||||
namespace SteamLib.Utility;
|
namespace SteamLib.Utility;
|
||||||
|
|
||||||
@@ -37,19 +37,31 @@ public static class SteamIdParser
|
|||||||
return false;
|
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);
|
var match64 = Steam64Regex.Match(input);
|
||||||
if (match64.Success)
|
if (match64.Success)
|
||||||
{
|
{
|
||||||
result = new SteamId64(long.Parse(match64.Value));
|
return TryParse64(long.Parse(match64.Value), out result);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result = default;
|
|
||||||
return false;
|
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)
|
public static bool TryParse2(string input, out SteamId2 result)
|
||||||
{
|
{
|
||||||
var match2 = Steam2Regex.Match(input);
|
var match2 = Steam2Regex.Match(input);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using SteamLib.Account;
|
using SteamLib.Core.Models;
|
||||||
|
|
||||||
namespace SteamLib.Web.Converters;
|
namespace SteamLib.Web.Converters;
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ public class SteamIdToSteam64Converter : JsonConverter<SteamId>
|
|||||||
return SteamId.FromSteam64(l);
|
return SteamId.FromSteam64(l);
|
||||||
}
|
}
|
||||||
|
|
||||||
var str = (string) reader.Value!;
|
var str = (string)reader.Value!;
|
||||||
return new SteamId(SteamId64.Parse(str));
|
return new SteamId(SteamId64.Parse(str));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user