1 // ----------------------------------------------------------------------------------------------
3 // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
4 // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
5 // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
6 // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
7 // ----------------------------------------------------------------------------------------------
9 // Copyright 2015-2024 Ć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
;
28 using System
.ComponentModel
;
29 using System
.ComponentModel
.DataAnnotations
;
30 using System
.Composition
;
31 using System
.Globalization
;
34 using System
.Text
.Json
;
35 using System
.Text
.Json
.Serialization
;
36 using System
.Threading
;
37 using System
.Threading
.Tasks
;
38 using ArchiSteamFarm
.Core
;
39 using ArchiSteamFarm
.Helpers
;
40 using ArchiSteamFarm
.Helpers
.Json
;
41 using ArchiSteamFarm
.OfficialPlugins
.SteamTokenDumper
.Data
;
42 using ArchiSteamFarm
.OfficialPlugins
.SteamTokenDumper
.Localization
;
43 using ArchiSteamFarm
.Plugins
;
44 using ArchiSteamFarm
.Plugins
.Interfaces
;
45 using ArchiSteamFarm
.Steam
;
46 using ArchiSteamFarm
.Steam
.Interaction
;
47 using ArchiSteamFarm
.Storage
;
48 using ArchiSteamFarm
.Web
;
49 using ArchiSteamFarm
.Web
.Responses
;
52 namespace ArchiSteamFarm
.OfficialPlugins
.SteamTokenDumper
;
54 [Export(typeof(IPlugin
))]
55 internal sealed class SteamTokenDumperPlugin
: OfficialPlugin
, IASF
, IBot
, IBotCommand2
, IBotSteamClient
, ISteamPICSChanges
{
56 private const ushort DepotsRateLimitingDelay
= 500;
58 internal static SteamTokenDumperConfig
? Config { get; private set; }
60 private static readonly ConcurrentDictionary
<Bot
, IDisposable
> BotSubscriptions
= new();
61 private static readonly ConcurrentDictionary
<Bot
, (SemaphoreSlim RefreshSemaphore
, Timer RefreshTimer
)> BotSynchronizations
= new();
62 private static readonly SemaphoreSlim SubmissionSemaphore
= new(1, 1);
63 private static readonly Timer SubmissionTimer
= new(OnSubmissionTimer
);
65 private static GlobalCache
? GlobalCache
;
66 private static DateTimeOffset LastUploadAt
= DateTimeOffset
.MinValue
;
70 public override string Name
=> nameof(SteamTokenDumperPlugin
);
74 public override Version Version
=> typeof(SteamTokenDumperPlugin
).Assembly
.GetName().Version
?? throw new InvalidOperationException(nameof(Version
));
76 public Task
<uint> GetPreferredChangeNumberToStartFrom() => Task
.FromResult(GlobalCache
?.LastChangeNumber
?? 0);
78 public async Task
OnASFInit(IReadOnlyDictionary
<string, JsonElement
>? additionalConfigProperties
= null) {
79 if (!SharedInfo
.HasValidToken
) {
80 ASF
.ArchiLogger
.LogGenericError(string.Format(CultureInfo
.CurrentCulture
, Strings
.PluginDisabledMissingBuildToken
, nameof(SteamTokenDumperPlugin
)));
85 bool isEnabled
= false;
86 SteamTokenDumperConfig
? config
= null;
88 if (additionalConfigProperties
!= null) {
89 foreach ((string configProperty
, JsonElement configValue
) in additionalConfigProperties
) {
91 switch (configProperty
) {
92 case nameof(GlobalConfigExtension
.SteamTokenDumperPlugin
):
93 config
= configValue
.ToJsonObject
<SteamTokenDumperConfig
>();
96 case nameof(GlobalConfigExtension
.SteamTokenDumperPluginEnabled
) when configValue
.ValueKind
== JsonValueKind
.False
:
100 case nameof(GlobalConfigExtension
.SteamTokenDumperPluginEnabled
) when configValue
.ValueKind
== JsonValueKind
.True
:
105 } catch (Exception e
) {
106 ASF
.ArchiLogger
.LogGenericException(e
);
107 ASF
.ArchiLogger
.LogGenericWarning(string.Format(CultureInfo
.CurrentCulture
, Strings
.PluginDisabledInConfig
, nameof(SteamTokenDumperPlugin
)));
114 if (GlobalCache
== null) {
115 GlobalCache
? globalCache
= await GlobalCache
.Load().ConfigureAwait(false);
117 if (globalCache
== null) {
118 ASF
.ArchiLogger
.LogGenericError(string.Format(CultureInfo
.CurrentCulture
, Strings
.FileCouldNotBeLoadedFreshInit
, nameof(GlobalCache
)));
120 GlobalCache
= new GlobalCache();
122 GlobalCache
= globalCache
;
126 if (!isEnabled
&& (config
== null)) {
127 ASF
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.PluginDisabledInConfig
, nameof(SteamTokenDumperPlugin
)));
132 config
??= new SteamTokenDumperConfig();
135 config
.Enabled
= true;
138 if (!config
.Enabled
) {
139 ASF
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.PluginDisabledInConfig
, nameof(SteamTokenDumperPlugin
)));
142 if (!config
.SecretAppIDs
.IsEmpty
) {
143 ASF
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.PluginSecretListInitialized
, nameof(config
.SecretAppIDs
), string.Join(", ", config
.SecretAppIDs
)));
146 if (!config
.SecretPackageIDs
.IsEmpty
) {
147 ASF
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.PluginSecretListInitialized
, nameof(config
.SecretPackageIDs
), string.Join(", ", config
.SecretPackageIDs
)));
150 if (!config
.SecretDepotIDs
.IsEmpty
) {
151 ASF
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.PluginSecretListInitialized
, nameof(config
.SecretDepotIDs
), string.Join(", ", config
.SecretDepotIDs
)));
156 if (!config
.Enabled
) {
160 #pragma warning disable CA5394 // This call isn't used in a security-sensitive manner
161 TimeSpan startIn
= TimeSpan
.FromMinutes(Random
.Shared
.Next(SharedInfo
.MinimumMinutesBeforeFirstUpload
, SharedInfo
.MaximumMinutesBeforeFirstUpload
));
162 #pragma warning restore CA5394 // This call isn't used in a security-sensitive manner
164 // ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
165 lock (SubmissionSemaphore
) {
166 SubmissionTimer
.Change(startIn
, TimeSpan
.FromHours(SharedInfo
.HoursBetweenUploads
));
169 ASF
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.PluginInitializedAndEnabled
, nameof(SteamTokenDumperPlugin
), startIn
.ToHumanReadable()));
172 public Task
<string?> OnBotCommand(Bot bot
, EAccess access
, string message
, string[] args
, ulong steamID
= 0) {
173 ArgumentNullException
.ThrowIfNull(bot
);
175 if (!Enum
.IsDefined(access
)) {
176 throw new InvalidEnumArgumentException(nameof(access
), (int) access
, typeof(EAccess
));
179 if ((args
== null) || (args
.Length
== 0)) {
180 throw new ArgumentNullException(nameof(args
));
183 switch (args
.Length
) {
185 switch (args
[0].ToUpperInvariant()) {
187 return Task
.FromResult(ResponseRefreshManually(access
, bot
));
192 switch (args
[0].ToUpperInvariant()) {
194 return Task
.FromResult(ResponseRefreshManually(access
, Utilities
.GetArgsAsText(args
, 1, ","), steamID
));
200 return Task
.FromResult((string?) null);
203 public async Task
OnBotDestroy(Bot bot
) {
204 ArgumentNullException
.ThrowIfNull(bot
);
206 if (BotSubscriptions
.TryRemove(bot
, out IDisposable
? subscription
)) {
207 subscription
.Dispose();
210 if (BotSynchronizations
.TryRemove(bot
, out (SemaphoreSlim RefreshSemaphore
, Timer RefreshTimer
) synchronization
)) {
211 // Ensure the semaphore is empty, otherwise we're risking disposed exceptions
212 await synchronization
.RefreshSemaphore
.WaitAsync().ConfigureAwait(false);
214 synchronization
.RefreshSemaphore
.Dispose();
216 await synchronization
.RefreshTimer
.DisposeAsync().ConfigureAwait(false);
220 public async Task
OnBotInit(Bot bot
) {
221 ArgumentNullException
.ThrowIfNull(bot
);
223 if (GlobalCache
== null) {
224 // We can't operate like this anyway, skip initialization of synchronization structures
228 SemaphoreSlim refreshSemaphore
= new(1, 1);
229 Timer refreshTimer
= new(OnBotRefreshTimer
, bot
, Timeout
.InfiniteTimeSpan
, Timeout
.InfiniteTimeSpan
);
231 if (!BotSynchronizations
.TryAdd(bot
, (refreshSemaphore
, refreshTimer
))) {
232 refreshSemaphore
.Dispose();
234 await refreshTimer
.DisposeAsync().ConfigureAwait(false);
238 public Task
OnBotSteamCallbacksInit(Bot bot
, CallbackManager callbackManager
) {
239 ArgumentNullException
.ThrowIfNull(bot
);
240 ArgumentNullException
.ThrowIfNull(callbackManager
);
242 if (BotSubscriptions
.TryRemove(bot
, out IDisposable
? subscription
)) {
243 subscription
.Dispose();
246 if (Config
is not { Enabled: true }
) {
247 return Task
.CompletedTask
;
250 subscription
= callbackManager
.Subscribe
<SteamApps
.LicenseListCallback
>(callback
=> OnLicenseList(bot
, callback
));
252 if (!BotSubscriptions
.TryAdd(bot
, subscription
)) {
253 subscription
.Dispose();
256 return Task
.CompletedTask
;
259 public Task
<IReadOnlyCollection
<ClientMsgHandler
>?> OnBotSteamHandlersInit(Bot bot
) => Task
.FromResult((IReadOnlyCollection
<ClientMsgHandler
>?) null);
261 public override Task
OnLoaded() {
262 Utilities
.WarnAboutIncompleteTranslation(Strings
.ResourceManager
);
264 return Task
.CompletedTask
;
267 public Task
OnPICSChanges(uint currentChangeNumber
, IReadOnlyDictionary
<uint, SteamApps
.PICSChangesCallback
.PICSChangeData
> appChanges
, IReadOnlyDictionary
<uint, SteamApps
.PICSChangesCallback
.PICSChangeData
> packageChanges
) {
268 ArgumentOutOfRangeException
.ThrowIfZero(currentChangeNumber
);
269 ArgumentNullException
.ThrowIfNull(appChanges
);
270 ArgumentNullException
.ThrowIfNull(packageChanges
);
272 GlobalCache
?.OnPICSChanges(currentChangeNumber
, appChanges
);
274 return Task
.CompletedTask
;
277 public Task
OnPICSChangesRestart(uint currentChangeNumber
) {
278 ArgumentOutOfRangeException
.ThrowIfZero(currentChangeNumber
);
280 GlobalCache
?.OnPICSChangesRestart(currentChangeNumber
);
282 return Task
.CompletedTask
;
285 private static async void OnBotRefreshTimer(object? state
) {
286 if (state
is not Bot bot
) {
287 throw new InvalidOperationException(nameof(state
));
290 await Refresh(bot
).ConfigureAwait(false);
293 private static async void OnLicenseList(Bot bot
, SteamApps
.LicenseListCallback callback
) {
294 ArgumentNullException
.ThrowIfNull(bot
);
295 ArgumentNullException
.ThrowIfNull(callback
);
297 if (Config
is not { Enabled: true }
) {
301 // Schedule a refresh in a while from now
302 if (!BotSynchronizations
.TryGetValue(bot
, out (SemaphoreSlim RefreshSemaphore
, Timer RefreshTimer
) synchronization
)) {
306 if (!await synchronization
.RefreshSemaphore
.WaitAsync(0).ConfigureAwait(false)) {
307 // Another refresh is in progress, skip the refresh for now
312 synchronization
.RefreshTimer
.Change(TimeSpan
.FromMinutes(1), TimeSpan
.FromHours(SharedInfo
.MaximumHoursBetweenRefresh
));
314 synchronization
.RefreshSemaphore
.Release();
318 private static async void OnSubmissionTimer(object? state
= null) => await SubmitData().ConfigureAwait(false);
320 private static async Task
Refresh(Bot bot
) {
321 ArgumentNullException
.ThrowIfNull(bot
);
323 if (GlobalCache
== null) {
324 throw new InvalidOperationException(nameof(GlobalCache
));
327 if (ASF
.GlobalDatabase
== null) {
328 throw new InvalidOperationException(nameof(ASF
.GlobalDatabase
));
331 if (!BotSynchronizations
.TryGetValue(bot
, out (SemaphoreSlim RefreshSemaphore
, Timer RefreshTimer
) synchronization
)) {
332 throw new InvalidOperationException(nameof(synchronization
));
335 if (!await synchronization
.RefreshSemaphore
.WaitAsync(0).ConfigureAwait(false)) {
339 SemaphoreSlim depotsRateLimitingSemaphore
= new(1, 1);
342 if (!bot
.IsConnectedAndLoggedOn
) {
346 HashSet
<uint> packageIDs
= bot
.OwnedPackageIDs
.Where(static package
=> (Config
?.SecretPackageIDs
.Contains(package
.Key
) != true) && ((package
.Value
.PaymentMethod
!= EPaymentMethod
.AutoGrant
) || (Config
?.SkipAutoGrantPackages
== false))).Select(static package
=> package
.Key
).ToHashSet();
348 HashSet
<uint> appIDsToRefresh
= [];
350 foreach (uint packageID
in packageIDs
.Where(static packageID
=> Config
?.SecretPackageIDs
.Contains(packageID
) != true)) {
351 if (!ASF
.GlobalDatabase
.PackagesDataReadOnly
.TryGetValue(packageID
, out PackageData
? packageData
) || (packageData
.AppIDs
== null)) {
352 // ASF might not have the package info for us at the moment, we'll retry later
356 appIDsToRefresh
.UnionWith(packageData
.AppIDs
.Where(static appID
=> (Config
?.SecretAppIDs
.Contains(appID
) != true) && GlobalCache
.ShouldRefreshAppInfo(appID
)));
359 if (appIDsToRefresh
.Count
== 0) {
360 bot
.ArchiLogger
.LogGenericDebug(Strings
.BotNoAppsToRefresh
);
365 bot
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.BotRetrievingTotalAppAccessTokens
, appIDsToRefresh
.Count
));
367 HashSet
<uint> appIDsThisRound
= new(Math
.Min(appIDsToRefresh
.Count
, SharedInfo
.AppInfosPerSingleRequest
));
369 using (HashSet
<uint>.Enumerator enumerator
= appIDsToRefresh
.GetEnumerator()) {
371 if (!bot
.IsConnectedAndLoggedOn
) {
375 while ((appIDsThisRound
.Count
< SharedInfo
.AppInfosPerSingleRequest
) && enumerator
.MoveNext()) {
376 appIDsThisRound
.Add(enumerator
.Current
);
379 if (appIDsThisRound
.Count
== 0) {
383 bot
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.BotRetrievingAppAccessTokens
, appIDsThisRound
.Count
));
385 SteamApps
.PICSTokensCallback response
;
388 response
= await bot
.SteamApps
.PICSGetAccessTokens(appIDsThisRound
, []).ToLongRunningTask().ConfigureAwait(false);
389 } catch (Exception e
) {
390 bot
.ArchiLogger
.LogGenericWarningException(e
);
392 appIDsThisRound
.Clear();
397 bot
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.BotFinishedRetrievingAppAccessTokens
, appIDsThisRound
.Count
));
399 appIDsThisRound
.Clear();
401 GlobalCache
.UpdateAppTokens(response
.AppTokens
, response
.AppTokensDenied
);
405 bot
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.BotFinishedRetrievingTotalAppAccessTokens
, appIDsToRefresh
.Count
));
406 bot
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.BotRetrievingTotalDepots
, appIDsToRefresh
.Count
));
408 (_
, FrozenSet
<uint>? knownDepotIDs
) = await GlobalCache
.KnownDepotIDs
.GetValue(ECacheFallback
.SuccessPreviously
).ConfigureAwait(false);
410 using (HashSet
<uint>.Enumerator enumerator
= appIDsToRefresh
.GetEnumerator()) {
412 if (!bot
.IsConnectedAndLoggedOn
) {
416 while ((appIDsThisRound
.Count
< SharedInfo
.AppInfosPerSingleRequest
) && enumerator
.MoveNext()) {
417 appIDsThisRound
.Add(enumerator
.Current
);
420 if (appIDsThisRound
.Count
== 0) {
424 bot
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.BotRetrievingAppInfos
, appIDsThisRound
.Count
));
426 AsyncJobMultiple
<SteamApps
.PICSProductInfoCallback
>.ResultSet response
;
429 response
= await bot
.SteamApps
.PICSGetProductInfo(appIDsThisRound
.Select(static appID
=> new SteamApps
.PICSRequest(appID
, GlobalCache
.GetAppToken(appID
))), []).ToLongRunningTask().ConfigureAwait(false);
430 } catch (Exception e
) {
431 bot
.ArchiLogger
.LogGenericWarningException(e
);
433 appIDsThisRound
.Clear();
438 if (response
.Results
== null) {
439 bot
.ArchiLogger
.LogGenericWarning(string.Format(CultureInfo
.CurrentCulture
, ArchiSteamFarm
.Localization
.Strings
.WarningFailedWithError
, nameof(response
.Results
)));
441 appIDsThisRound
.Clear();
446 bot
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.BotFinishedRetrievingAppInfos
, appIDsThisRound
.Count
));
448 appIDsThisRound
.Clear();
450 Dictionary
<uint, uint> appChangeNumbers
= new();
452 uint depotKeysSuccessful
= 0;
453 uint depotKeysTotal
= 0;
455 foreach (SteamApps
.PICSProductInfoCallback
.PICSProductInfo app
in response
.Results
.SelectMany(static result
=> result
.Apps
.Values
)) {
456 appChangeNumbers
[app
.ID
] = app
.ChangeNumber
;
458 bool shouldFetchMainKey
= false;
460 foreach (KeyValue depot
in app
.KeyValues
["depots"].Children
) {
461 if (!uint.TryParse(depot
.Name
, out uint depotID
) || (knownDepotIDs
?.Contains(depotID
) == true) || (Config
?.SecretDepotIDs
.Contains(depotID
) == true) || !GlobalCache
.ShouldRefreshDepotKey(depotID
)) {
467 await depotsRateLimitingSemaphore
.WaitAsync().ConfigureAwait(false);
470 SteamApps
.DepotKeyCallback depotResponse
= await bot
.SteamApps
.GetDepotDecryptionKey(depotID
, app
.ID
).ToLongRunningTask().ConfigureAwait(false);
472 depotKeysSuccessful
++;
474 if (depotResponse
.Result
!= EResult
.OK
) {
478 shouldFetchMainKey
= true;
480 GlobalCache
.UpdateDepotKey(depotResponse
);
481 } catch (Exception e
) {
482 // We can still try other depots
483 bot
.ArchiLogger
.LogGenericWarningException(e
);
485 Utilities
.InBackground(
487 await Task
.Delay(DepotsRateLimitingDelay
).ConfigureAwait(false);
489 // ReSharper disable once AccessToDisposedClosure - we're waiting for the semaphore to be free before disposing it
490 depotsRateLimitingSemaphore
.Release();
496 // Consider fetching main appID key only if we've actually considered some new depots for resolving
497 if (shouldFetchMainKey
&& (knownDepotIDs
?.Contains(app
.ID
) != true) && GlobalCache
.ShouldRefreshDepotKey(app
.ID
)) {
500 await depotsRateLimitingSemaphore
.WaitAsync().ConfigureAwait(false);
503 SteamApps
.DepotKeyCallback depotResponse
= await bot
.SteamApps
.GetDepotDecryptionKey(app
.ID
, app
.ID
).ToLongRunningTask().ConfigureAwait(false);
505 depotKeysSuccessful
++;
507 GlobalCache
.UpdateDepotKey(depotResponse
);
508 } catch (Exception e
) {
509 // We can still try other depots
510 bot
.ArchiLogger
.LogGenericWarningException(e
);
512 Utilities
.InBackground(
514 await Task
.Delay(DepotsRateLimitingDelay
).ConfigureAwait(false);
516 // ReSharper disable once AccessToDisposedClosure - we're waiting for the semaphore to be free before disposing it
517 depotsRateLimitingSemaphore
.Release();
524 if (depotKeysTotal
> 0) {
525 bot
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.BotFinishedRetrievingDepotKeys
, depotKeysSuccessful
, depotKeysTotal
));
528 if (depotKeysSuccessful
< depotKeysTotal
) {
529 // We're not going to record app change numbers, as we didn't fetch all the depot keys we wanted
533 GlobalCache
.UpdateAppChangeNumbers(appChangeNumbers
);
537 bot
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.BotFinishedRetrievingTotalDepots
, appIDsToRefresh
.Count
));
539 if (Config
?.Enabled
== true) {
540 TimeSpan timeSpan
= TimeSpan
.FromHours(SharedInfo
.MaximumHoursBetweenRefresh
);
542 synchronization
.RefreshTimer
.Change(timeSpan
, timeSpan
);
545 await depotsRateLimitingSemaphore
.WaitAsync().ConfigureAwait(false);
547 synchronization
.RefreshSemaphore
.Release();
549 depotsRateLimitingSemaphore
.Dispose();
553 private static string? ResponseRefreshManually(EAccess access
, Bot bot
) {
554 if (!Enum
.IsDefined(access
)) {
555 throw new InvalidEnumArgumentException(nameof(access
), (int) access
, typeof(EAccess
));
558 ArgumentNullException
.ThrowIfNull(bot
);
560 if (access
< EAccess
.Master
) {
561 return access
> EAccess
.None
? bot
.Commands
.FormatBotResponse(ArchiSteamFarm
.Localization
.Strings
.ErrorAccessDenied
) : null;
564 if (GlobalCache
== null) {
565 return bot
.Commands
.FormatBotResponse(string.Format(CultureInfo
.CurrentCulture
, ArchiSteamFarm
.Localization
.Strings
.WarningFailedWithError
, nameof(GlobalCache
)));
568 Utilities
.InBackground(
570 await Refresh(bot
).ConfigureAwait(false);
571 await SubmitData().ConfigureAwait(false);
575 return bot
.Commands
.FormatBotResponse(ArchiSteamFarm
.Localization
.Strings
.Done
);
578 private static string? ResponseRefreshManually(EAccess access
, string botNames
, ulong steamID
= 0) {
579 if (!Enum
.IsDefined(access
)) {
580 throw new InvalidEnumArgumentException(nameof(access
), (int) access
, typeof(EAccess
));
583 ArgumentException
.ThrowIfNullOrEmpty(botNames
);
585 if ((steamID
!= 0) && !new SteamID(steamID
).IsIndividualAccount
) {
586 throw new ArgumentOutOfRangeException(nameof(steamID
));
589 HashSet
<Bot
>? bots
= Bot
.GetBots(botNames
);
591 if ((bots
== null) || (bots
.Count
== 0)) {
592 return access
>= EAccess
.Owner
? Commands
.FormatStaticResponse(string.Format(CultureInfo
.CurrentCulture
, ArchiSteamFarm
.Localization
.Strings
.BotNotFound
, botNames
)) : null;
595 if (bots
.RemoveWhere(bot
=> Commands
.GetProxyAccess(bot
, access
, steamID
) < EAccess
.Master
) > 0) {
596 if (bots
.Count
== 0) {
597 return access
>= EAccess
.Owner
? Commands
.FormatStaticResponse(string.Format(CultureInfo
.CurrentCulture
, ArchiSteamFarm
.Localization
.Strings
.BotNotFound
, botNames
)) : null;
601 if (GlobalCache
== null) {
602 return Commands
.FormatStaticResponse(string.Format(CultureInfo
.CurrentCulture
, ArchiSteamFarm
.Localization
.Strings
.WarningFailedWithError
, nameof(GlobalCache
)));
605 Utilities
.InBackground(
607 await Utilities
.InParallel(bots
.Select(static bot
=> Refresh(bot
))).ConfigureAwait(false);
609 await SubmitData().ConfigureAwait(false);
613 return Commands
.FormatStaticResponse(ArchiSteamFarm
.Localization
.Strings
.Done
);
616 private static async Task
SubmitData(CancellationToken cancellationToken
= default) {
617 if (Bot
.Bots
== null) {
618 throw new InvalidOperationException(nameof(Bot
.Bots
));
621 if (GlobalCache
== null) {
622 throw new InvalidOperationException(nameof(GlobalCache
));
625 if (ASF
.WebBrowser
== null) {
626 throw new InvalidOperationException(nameof(ASF
.WebBrowser
));
629 if (LastUploadAt
+ TimeSpan
.FromMinutes(SharedInfo
.MinimumMinutesBetweenUploads
) > DateTimeOffset
.UtcNow
) {
633 if (!await SubmissionSemaphore
.WaitAsync(0, cancellationToken
).ConfigureAwait(false)) {
638 Dictionary
<uint, ulong> appTokens
= GlobalCache
.GetAppTokensForSubmission();
639 Dictionary
<uint, ulong> packageTokens
= GlobalCache
.GetPackageTokensForSubmission();
640 Dictionary
<uint, string> depotKeys
= GlobalCache
.GetDepotKeysForSubmission();
642 if ((appTokens
.Count
== 0) && (packageTokens
.Count
== 0) && (depotKeys
.Count
== 0)) {
643 ASF
.ArchiLogger
.LogGenericInfo(Strings
.SubmissionNoNewData
);
648 ulong contributorSteamID
= ASF
.GlobalConfig
is { SteamOwnerID: > 0 }
&& new SteamID(ASF
.GlobalConfig
.SteamOwnerID
).IsIndividualAccount
? ASF
.GlobalConfig
.SteamOwnerID
: Bot
.Bots
.Values
.Where(static bot
=> bot
.SteamID
> 0).MaxBy(static bot
=> bot
.OwnedPackageIDs
.Count
)?.SteamID
?? 0;
650 if (contributorSteamID
== 0) {
651 ASF
.ArchiLogger
.LogGenericError(string.Format(CultureInfo
.CurrentCulture
, Strings
.SubmissionNoContributorSet
, nameof(ASF
.GlobalConfig
.SteamOwnerID
)));
656 Uri request
= new($"{SharedInfo.ServerURL}/submit");
657 SubmitRequest data
= new(contributorSteamID
, appTokens
, packageTokens
, depotKeys
);
659 ASF
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.SubmissionInProgress
, appTokens
.Count
, packageTokens
.Count
, depotKeys
.Count
));
661 ObjectResponse
<SubmitResponse
>? response
= await ASF
.WebBrowser
.UrlPostToJsonObject
<SubmitResponse
, SubmitRequest
>(request
, data
: data
, requestOptions
: WebBrowser
.ERequestOptions
.ReturnClientErrors
| WebBrowser
.ERequestOptions
.AllowInvalidBodyOnErrors
, cancellationToken
: cancellationToken
).ConfigureAwait(false);
663 if (response
== null) {
664 ASF
.ArchiLogger
.LogGenericWarning(ArchiSteamFarm
.Localization
.Strings
.WarningFailed
);
669 // We've communicated with the server and didn't timeout, regardless of the success, this was the last upload attempt
670 LastUploadAt
= DateTimeOffset
.UtcNow
;
672 if (response
.StatusCode
.IsClientErrorCode()) {
673 ASF
.ArchiLogger
.LogGenericWarning(string.Format(CultureInfo
.CurrentCulture
, ArchiSteamFarm
.Localization
.Strings
.WarningFailedWithError
, response
.StatusCode
));
675 switch (response
.StatusCode
) {
676 case HttpStatusCode
.Forbidden when Config
?.Enabled
== true:
677 // SteamDB told us to stop submitting data for now
678 // ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
679 lock (SubmissionSemaphore
) {
680 SubmissionTimer
.Change(Timeout
.InfiniteTimeSpan
, Timeout
.InfiniteTimeSpan
);
684 case HttpStatusCode
.Conflict
:
685 // SteamDB told us to reset our cache
686 GlobalCache
.Reset(true);
689 case HttpStatusCode
.TooManyRequests when Config
?.Enabled
== true:
690 // SteamDB told us to try again later
691 #pragma warning disable CA5394 // This call isn't used in a security-sensitive manner
692 TimeSpan startIn
= TimeSpan
.FromMinutes(Random
.Shared
.Next(SharedInfo
.MinimumMinutesBeforeFirstUpload
, SharedInfo
.MaximumMinutesBeforeFirstUpload
));
693 #pragma warning restore CA5394 // This call isn't used in a security-sensitive manner
695 // ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
696 lock (SubmissionSemaphore
) {
697 SubmissionTimer
.Change(startIn
, TimeSpan
.FromHours(SharedInfo
.HoursBetweenUploads
));
700 ASF
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.SubmissionFailedTooManyRequests
, startIn
.ToHumanReadable()));
708 if (response
.Content
is not { Success: true }
) {
709 ASF
.ArchiLogger
.LogGenericError(ArchiSteamFarm
.Localization
.Strings
.WarningFailed
);
714 if (response
.Content
.Data
== null) {
715 ASF
.ArchiLogger
.LogGenericError(string.Format(CultureInfo
.CurrentCulture
, ArchiSteamFarm
.Localization
.Strings
.ErrorIsInvalid
, nameof(response
.Content
.Data
)));
720 ASF
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.SubmissionSuccessful
, response
.Content
.Data
.NewApps
.Count
, response
.Content
.Data
.VerifiedApps
.Count
, response
.Content
.Data
.NewPackages
.Count
, response
.Content
.Data
.VerifiedPackages
.Count
, response
.Content
.Data
.NewDepots
.Count
, response
.Content
.Data
.VerifiedDepots
.Count
));
722 GlobalCache
.UpdateSubmittedData(appTokens
, packageTokens
, depotKeys
);
724 if (!response
.Content
.Data
.NewApps
.IsEmpty
) {
725 ASF
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.SubmissionSuccessfulNewApps
, string.Join(", ", response
.Content
.Data
.NewApps
)));
728 if (!response
.Content
.Data
.VerifiedApps
.IsEmpty
) {
729 ASF
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.SubmissionSuccessfulVerifiedApps
, string.Join(", ", response
.Content
.Data
.VerifiedApps
)));
732 if (!response
.Content
.Data
.NewPackages
.IsEmpty
) {
733 ASF
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.SubmissionSuccessfulNewPackages
, string.Join(", ", response
.Content
.Data
.NewPackages
)));
736 if (!response
.Content
.Data
.VerifiedPackages
.IsEmpty
) {
737 ASF
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.SubmissionSuccessfulVerifiedPackages
, string.Join(", ", response
.Content
.Data
.VerifiedPackages
)));
740 if (!response
.Content
.Data
.NewDepots
.IsEmpty
) {
741 ASF
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.SubmissionSuccessfulNewDepots
, string.Join(", ", response
.Content
.Data
.NewDepots
)));
744 if (!response
.Content
.Data
.VerifiedDepots
.IsEmpty
) {
745 ASF
.ArchiLogger
.LogGenericInfo(string.Format(CultureInfo
.CurrentCulture
, Strings
.SubmissionSuccessfulVerifiedDepots
, string.Join(", ", response
.Content
.Data
.VerifiedDepots
)));
748 SubmissionSemaphore
.Release();