1 // ----------------------------------------------------------------------------------------------
3 // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
4 // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
5 // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
6 // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
7 // ----------------------------------------------------------------------------------------------
9 // Copyright 2015-2025 Ćukasz "JustArchi" Domeradzki
10 // Contact: JustArchi@JustArchi.net
12 // Licensed under the Apache License, Version 2.0 (the "License");
13 // you may not use this file except in compliance with the License.
14 // You may obtain a copy of the License at
16 // http://www.apache.org/licenses/LICENSE-2.0
18 // Unless required by applicable law or agreed to in writing, software
19 // distributed under the License is distributed on an "AS IS" BASIS,
20 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 // See the License for the specific language governing permissions and
22 // limitations under the License.
25 using System
.Collections
.Concurrent
;
26 using System
.Collections
.Frozen
;
27 using System
.Collections
.Generic
;
30 using System
.Text
.Json
.Serialization
;
31 using System
.Threading
;
32 using System
.Threading
.Tasks
;
33 using ArchiSteamFarm
.Core
;
34 using ArchiSteamFarm
.Helpers
;
35 using ArchiSteamFarm
.Helpers
.Json
;
36 using ArchiSteamFarm
.OfficialPlugins
.SteamTokenDumper
.Localization
;
37 using ArchiSteamFarm
.Web
.Responses
;
38 using JetBrains
.Annotations
;
41 namespace ArchiSteamFarm
.OfficialPlugins
.SteamTokenDumper
;
43 internal sealed class GlobalCache
: SerializableFile
{
44 internal static readonly ArchiCacheable
<FrozenSet
<uint>> KnownDepotIDs
= new(ResolveKnownDepotIDs
, TimeSpan
.FromDays(7));
46 private static string SharedFilePath
=> Path
.Combine(ArchiSteamFarm
.SharedInfo
.ConfigDirectory
, $"{nameof(SteamTokenDumper)}.cache");
49 internal uint LastChangeNumber { get; private set; }
53 private ConcurrentDictionary
<uint, uint> AppChangeNumbers { get; init; }
= new();
57 private ConcurrentDictionary
<uint, ulong> AppTokens { get; init; }
= new();
61 private ConcurrentDictionary
<uint, string> DepotKeys { get; init; }
= new();
65 private ConcurrentDictionary
<uint, ulong> SubmittedApps { get; init; }
= new();
69 private ConcurrentDictionary
<uint, string> SubmittedDepots { get; init; }
= new();
73 private ConcurrentDictionary
<uint, ulong> SubmittedPackages { get; init; }
= new();
76 internal GlobalCache() => FilePath
= SharedFilePath
;
79 public bool ShouldSerializeAppChangeNumbers() => !AppChangeNumbers
.IsEmpty
;
82 public bool ShouldSerializeAppTokens() => !AppTokens
.IsEmpty
;
85 public bool ShouldSerializeDepotKeys() => !DepotKeys
.IsEmpty
;
88 public bool ShouldSerializeLastChangeNumber() => LastChangeNumber
> 0;
91 public bool ShouldSerializeSubmittedApps() => !SubmittedApps
.IsEmpty
;
94 public bool ShouldSerializeSubmittedDepots() => !SubmittedDepots
.IsEmpty
;
97 public bool ShouldSerializeSubmittedPackages() => !SubmittedPackages
.IsEmpty
;
99 protected override Task
Save() => Save(this);
101 internal ulong GetAppToken(uint appID
) => AppTokens
[appID
];
103 internal Dictionary
<uint, ulong> GetAppTokensForSubmission() => AppTokens
.Where(appToken
=> (SteamTokenDumperPlugin
.Config
?.SecretAppIDs
.Contains(appToken
.Key
) != true) && (appToken
.Value
> 0) && (!SubmittedApps
.TryGetValue(appToken
.Key
, out ulong token
) || (appToken
.Value
!= token
))).ToDictionary(static appToken
=> appToken
.Key
, static appToken
=> appToken
.Value
);
104 internal Dictionary
<uint, string> GetDepotKeysForSubmission() => DepotKeys
.Where(depotKey
=> (SteamTokenDumperPlugin
.Config
?.SecretDepotIDs
.Contains(depotKey
.Key
) != true) && !string.IsNullOrEmpty(depotKey
.Value
) && (!SubmittedDepots
.TryGetValue(depotKey
.Key
, out string? key
) || (depotKey
.Value
!= key
))).ToDictionary(static depotKey
=> depotKey
.Key
, static depotKey
=> depotKey
.Value
);
106 internal Dictionary
<uint, ulong> GetPackageTokensForSubmission() {
107 if (ASF
.GlobalDatabase
== null) {
108 throw new InvalidOperationException(nameof(ASF
.GlobalDatabase
));
111 return ASF
.GlobalDatabase
.PackageAccessTokensReadOnly
.Where(packageToken
=> (SteamTokenDumperPlugin
.Config
?.SecretPackageIDs
.Contains(packageToken
.Key
) != true) && (packageToken
.Value
> 0) && (!SubmittedPackages
.TryGetValue(packageToken
.Key
, out ulong token
) || (packageToken
.Value
!= token
))).ToDictionary(static packageToken
=> packageToken
.Key
, static packageToken
=> packageToken
.Value
);
114 internal static async Task
<GlobalCache
?> Load() {
115 if (!File
.Exists(SharedFilePath
)) {
116 return new GlobalCache();
119 ASF
.ArchiLogger
.LogGenericInfo(Strings
.LoadingGlobalCache
);
121 GlobalCache
? globalCache
;
124 string json
= await File
.ReadAllTextAsync(SharedFilePath
).ConfigureAwait(false);
126 if (string.IsNullOrEmpty(json
)) {
127 ASF
.ArchiLogger
.LogGenericError(ArchiSteamFarm
.Localization
.Strings
.FormatErrorIsEmpty(nameof(json
)));
132 globalCache
= json
.ToJsonObject
<GlobalCache
>();
133 } catch (Exception e
) {
134 ASF
.ArchiLogger
.LogGenericException(e
);
139 if (globalCache
== null) {
140 ASF
.ArchiLogger
.LogNullError(globalCache
);
145 ASF
.ArchiLogger
.LogGenericInfo(Strings
.ValidatingGlobalCacheIntegrity
);
147 if (globalCache
.DepotKeys
.Values
.Any(static depotKey
=> !IsValidDepotKey(depotKey
))) {
148 ASF
.ArchiLogger
.LogGenericError(Strings
.GlobalCacheIntegrityValidationFailed
);
156 internal void OnPICSChanges(uint currentChangeNumber
, IReadOnlyCollection
<KeyValuePair
<uint, SteamApps
.PICSChangesCallback
.PICSChangeData
>> appChanges
) {
157 ArgumentOutOfRangeException
.ThrowIfZero(currentChangeNumber
);
158 ArgumentNullException
.ThrowIfNull(appChanges
);
160 if (currentChangeNumber
<= LastChangeNumber
) {
164 LastChangeNumber
= currentChangeNumber
;
166 foreach ((uint appID
, SteamApps
.PICSChangesCallback
.PICSChangeData appData
) in appChanges
) {
167 if (!AppChangeNumbers
.TryGetValue(appID
, out uint previousChangeNumber
) || (previousChangeNumber
>= appData
.ChangeNumber
)) {
171 AppChangeNumbers
.TryRemove(appID
, out _
);
174 Utilities
.InBackground(Save
);
177 internal void OnPICSChangesRestart(uint currentChangeNumber
) {
178 ArgumentOutOfRangeException
.ThrowIfZero(currentChangeNumber
);
180 if (currentChangeNumber
<= LastChangeNumber
) {
184 LastChangeNumber
= currentChangeNumber
;
189 internal void Reset(bool clear
= false) {
190 AppChangeNumbers
.Clear();
197 Utilities
.InBackground(Save
);
200 internal bool ShouldRefreshAppInfo(uint appID
) => !AppChangeNumbers
.ContainsKey(appID
);
201 internal bool ShouldRefreshDepotKey(uint depotID
) => !DepotKeys
.ContainsKey(depotID
);
203 internal void UpdateAppChangeNumbers(IReadOnlyCollection
<KeyValuePair
<uint, uint>> appChangeNumbers
) {
204 ArgumentNullException
.ThrowIfNull(appChangeNumbers
);
208 foreach ((uint appID
, uint changeNumber
) in appChangeNumbers
) {
209 if (AppChangeNumbers
.TryGetValue(appID
, out uint previousChangeNumber
) && (previousChangeNumber
>= changeNumber
)) {
213 AppChangeNumbers
[appID
] = changeNumber
;
218 Utilities
.InBackground(Save
);
222 internal void UpdateAppTokens(IReadOnlyCollection
<KeyValuePair
<uint, ulong>> appTokens
, IReadOnlyCollection
<uint> publicAppIDs
) {
223 ArgumentNullException
.ThrowIfNull(appTokens
);
224 ArgumentNullException
.ThrowIfNull(publicAppIDs
);
228 foreach ((uint appID
, ulong appToken
) in appTokens
) {
229 if (AppTokens
.TryGetValue(appID
, out ulong previousAppToken
) && (previousAppToken
== appToken
)) {
233 AppTokens
[appID
] = appToken
;
237 foreach (uint appID
in publicAppIDs
) {
238 if (AppTokens
.TryGetValue(appID
, out ulong previousAppToken
) && (previousAppToken
== 0)) {
242 AppTokens
[appID
] = 0;
247 Utilities
.InBackground(Save
);
251 internal void UpdateDepotKey(SteamApps
.DepotKeyCallback depotKeyResult
) {
252 ArgumentNullException
.ThrowIfNull(depotKeyResult
);
254 if (depotKeyResult
.Result
!= EResult
.OK
) {
258 string depotKey
= Convert
.ToHexString(depotKeyResult
.DepotKey
);
260 if (!IsValidDepotKey(depotKey
)) {
261 ASF
.ArchiLogger
.LogGenericWarning(ArchiSteamFarm
.Localization
.Strings
.FormatErrorIsInvalid(nameof(depotKey
)));
266 if (DepotKeys
.TryGetValue(depotKeyResult
.DepotID
, out string? previousDepotKey
) && (previousDepotKey
== depotKey
)) {
270 DepotKeys
[depotKeyResult
.DepotID
] = depotKey
;
272 Utilities
.InBackground(Save
);
275 internal void UpdateSubmittedData(IReadOnlyDictionary
<uint, ulong> apps
, IReadOnlyDictionary
<uint, ulong> packages
, IReadOnlyDictionary
<uint, string> depots
) {
276 ArgumentNullException
.ThrowIfNull(apps
);
277 ArgumentNullException
.ThrowIfNull(packages
);
278 ArgumentNullException
.ThrowIfNull(depots
);
282 foreach ((uint appID
, ulong token
) in apps
) {
283 if (SubmittedApps
.TryGetValue(appID
, out ulong previousToken
) && (previousToken
== token
)) {
287 SubmittedApps
[appID
] = token
;
291 foreach ((uint packageID
, ulong token
) in packages
) {
292 if (SubmittedPackages
.TryGetValue(packageID
, out ulong previousToken
) && (previousToken
== token
)) {
296 SubmittedPackages
[packageID
] = token
;
300 foreach ((uint depotID
, string key
) in depots
) {
301 if (SubmittedDepots
.TryGetValue(depotID
, out string? previousKey
) && (previousKey
== key
)) {
305 SubmittedDepots
[depotID
] = key
;
310 Utilities
.InBackground(Save
);
314 private static bool IsValidDepotKey(string depotKey
) {
315 ArgumentException
.ThrowIfNullOrEmpty(depotKey
);
317 return (depotKey
.Length
== 64) && Utilities
.IsValidHexadecimalText(depotKey
);
320 private static async Task
<(bool Success
, FrozenSet
<uint>? Result
)> ResolveKnownDepotIDs(CancellationToken cancellationToken
= default) {
321 if (ASF
.WebBrowser
== null) {
322 throw new InvalidOperationException(nameof(ASF
.WebBrowser
));
325 Uri request
= new($"{SharedInfo.ServerURL}/knowndepots.csv");
327 StreamResponse
? response
= await ASF
.WebBrowser
.UrlGetToStream(request
, cancellationToken
: cancellationToken
).ConfigureAwait(false);
329 if (response
== null) {
330 return (false, null);
333 HashSet
<uint> result
;
335 await using (response
.ConfigureAwait(false)) {
336 if (response
.Content
== null) {
337 return (false, null);
341 using StreamReader reader
= new(response
.Content
);
343 string? countText
= await reader
.ReadLineAsync(cancellationToken
).ConfigureAwait(false);
345 if (string.IsNullOrEmpty(countText
) || !int.TryParse(countText
, out int count
) || (count
<= 0)) {
346 ASF
.ArchiLogger
.LogNullError(countText
);
348 return (false, null);
351 result
= new HashSet
<uint>(count
);
353 while (await reader
.ReadLineAsync(cancellationToken
).ConfigureAwait(false) is { Length: > 0 } line
) {
354 if (!uint.TryParse(line
, out uint depotID
) || (depotID
== 0)) {
355 ASF
.ArchiLogger
.LogNullError(depotID
);
362 } catch (Exception e
) {
363 ASF
.ArchiLogger
.LogGenericWarningException(e
);
365 return (false, null);
369 return (result
.Count
> 0, result
.ToFrozenSet());