chore(deps): update crowdin/github-action action to v2.5.2
[ArchiSteamFarm.git] / ArchiSteamFarm.OfficialPlugins.SteamTokenDumper / GlobalCache.cs
blob4233a17d1aef0e18cb00cea823d94034ba2ee445
1 // ----------------------------------------------------------------------------------------------
2 // _ _ _ ____ _ _____
3 // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
4 // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
5 // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
6 // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
7 // ----------------------------------------------------------------------------------------------
8 // |
9 // Copyright 2015-2025 Ɓukasz "JustArchi" Domeradzki
10 // Contact: JustArchi@JustArchi.net
11 // |
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
15 // |
16 // http://www.apache.org/licenses/LICENSE-2.0
17 // |
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.
24 using System;
25 using System.Collections.Concurrent;
26 using System.Collections.Frozen;
27 using System.Collections.Generic;
28 using System.IO;
29 using System.Linq;
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;
39 using SteamKit2;
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");
48 [JsonInclude]
49 internal uint LastChangeNumber { get; private set; }
51 [JsonDisallowNull]
52 [JsonInclude]
53 private ConcurrentDictionary<uint, uint> AppChangeNumbers { get; init; } = new();
55 [JsonDisallowNull]
56 [JsonInclude]
57 private ConcurrentDictionary<uint, ulong> AppTokens { get; init; } = new();
59 [JsonDisallowNull]
60 [JsonInclude]
61 private ConcurrentDictionary<uint, string> DepotKeys { get; init; } = new();
63 [JsonDisallowNull]
64 [JsonInclude]
65 private ConcurrentDictionary<uint, ulong> SubmittedApps { get; init; } = new();
67 [JsonDisallowNull]
68 [JsonInclude]
69 private ConcurrentDictionary<uint, string> SubmittedDepots { get; init; } = new();
71 [JsonDisallowNull]
72 [JsonInclude]
73 private ConcurrentDictionary<uint, ulong> SubmittedPackages { get; init; } = new();
75 [JsonConstructor]
76 internal GlobalCache() => FilePath = SharedFilePath;
78 [UsedImplicitly]
79 public bool ShouldSerializeAppChangeNumbers() => !AppChangeNumbers.IsEmpty;
81 [UsedImplicitly]
82 public bool ShouldSerializeAppTokens() => !AppTokens.IsEmpty;
84 [UsedImplicitly]
85 public bool ShouldSerializeDepotKeys() => !DepotKeys.IsEmpty;
87 [UsedImplicitly]
88 public bool ShouldSerializeLastChangeNumber() => LastChangeNumber > 0;
90 [UsedImplicitly]
91 public bool ShouldSerializeSubmittedApps() => !SubmittedApps.IsEmpty;
93 [UsedImplicitly]
94 public bool ShouldSerializeSubmittedDepots() => !SubmittedDepots.IsEmpty;
96 [UsedImplicitly]
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;
123 try {
124 string json = await File.ReadAllTextAsync(SharedFilePath).ConfigureAwait(false);
126 if (string.IsNullOrEmpty(json)) {
127 ASF.ArchiLogger.LogGenericError(ArchiSteamFarm.Localization.Strings.FormatErrorIsEmpty(nameof(json)));
129 return null;
132 globalCache = json.ToJsonObject<GlobalCache>();
133 } catch (Exception e) {
134 ASF.ArchiLogger.LogGenericException(e);
136 return null;
139 if (globalCache == null) {
140 ASF.ArchiLogger.LogNullError(globalCache);
142 return null;
145 ASF.ArchiLogger.LogGenericInfo(Strings.ValidatingGlobalCacheIntegrity);
147 if (globalCache.DepotKeys.Values.Any(static depotKey => !IsValidDepotKey(depotKey))) {
148 ASF.ArchiLogger.LogGenericError(Strings.GlobalCacheIntegrityValidationFailed);
150 return null;
153 return globalCache;
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) {
161 return;
164 LastChangeNumber = currentChangeNumber;
166 foreach ((uint appID, SteamApps.PICSChangesCallback.PICSChangeData appData) in appChanges) {
167 if (!AppChangeNumbers.TryGetValue(appID, out uint previousChangeNumber) || (previousChangeNumber >= appData.ChangeNumber)) {
168 continue;
171 AppChangeNumbers.TryRemove(appID, out _);
174 Utilities.InBackground(Save);
177 internal void OnPICSChangesRestart(uint currentChangeNumber) {
178 ArgumentOutOfRangeException.ThrowIfZero(currentChangeNumber);
180 if (currentChangeNumber <= LastChangeNumber) {
181 return;
184 LastChangeNumber = currentChangeNumber;
186 Reset();
189 internal void Reset(bool clear = false) {
190 AppChangeNumbers.Clear();
192 if (clear) {
193 AppTokens.Clear();
194 DepotKeys.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);
206 bool save = false;
208 foreach ((uint appID, uint changeNumber) in appChangeNumbers) {
209 if (AppChangeNumbers.TryGetValue(appID, out uint previousChangeNumber) && (previousChangeNumber >= changeNumber)) {
210 continue;
213 AppChangeNumbers[appID] = changeNumber;
214 save = true;
217 if (save) {
218 Utilities.InBackground(Save);
222 internal void UpdateAppTokens(IReadOnlyCollection<KeyValuePair<uint, ulong>> appTokens, IReadOnlyCollection<uint> publicAppIDs) {
223 ArgumentNullException.ThrowIfNull(appTokens);
224 ArgumentNullException.ThrowIfNull(publicAppIDs);
226 bool save = false;
228 foreach ((uint appID, ulong appToken) in appTokens) {
229 if (AppTokens.TryGetValue(appID, out ulong previousAppToken) && (previousAppToken == appToken)) {
230 continue;
233 AppTokens[appID] = appToken;
234 save = true;
237 foreach (uint appID in publicAppIDs) {
238 if (AppTokens.TryGetValue(appID, out ulong previousAppToken) && (previousAppToken == 0)) {
239 continue;
242 AppTokens[appID] = 0;
243 save = true;
246 if (save) {
247 Utilities.InBackground(Save);
251 internal void UpdateDepotKey(SteamApps.DepotKeyCallback depotKeyResult) {
252 ArgumentNullException.ThrowIfNull(depotKeyResult);
254 if (depotKeyResult.Result != EResult.OK) {
255 return;
258 string depotKey = Convert.ToHexString(depotKeyResult.DepotKey);
260 if (!IsValidDepotKey(depotKey)) {
261 ASF.ArchiLogger.LogGenericWarning(ArchiSteamFarm.Localization.Strings.FormatErrorIsInvalid(nameof(depotKey)));
263 return;
266 if (DepotKeys.TryGetValue(depotKeyResult.DepotID, out string? previousDepotKey) && (previousDepotKey == depotKey)) {
267 return;
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);
280 bool save = false;
282 foreach ((uint appID, ulong token) in apps) {
283 if (SubmittedApps.TryGetValue(appID, out ulong previousToken) && (previousToken == token)) {
284 continue;
287 SubmittedApps[appID] = token;
288 save = true;
291 foreach ((uint packageID, ulong token) in packages) {
292 if (SubmittedPackages.TryGetValue(packageID, out ulong previousToken) && (previousToken == token)) {
293 continue;
296 SubmittedPackages[packageID] = token;
297 save = true;
300 foreach ((uint depotID, string key) in depots) {
301 if (SubmittedDepots.TryGetValue(depotID, out string? previousKey) && (previousKey == key)) {
302 continue;
305 SubmittedDepots[depotID] = key;
306 save = true;
309 if (save) {
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);
340 try {
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);
357 continue;
360 result.Add(depotID);
362 } catch (Exception e) {
363 ASF.ArchiLogger.LogGenericWarningException(e);
365 return (false, null);
369 return (result.Count > 0, result.ToFrozenSet());