Files
ArchiSteamFarm/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/SteamTokenDumperPlugin.cs

729 lines
28 KiB
C#
Raw Normal View History

2020-06-13 12:08:21 +02:00
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
2020-06-13 12:08:21 +02:00
// Contact: JustArchi@JustArchi.net
// |
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// |
// http://www.apache.org/licenses/LICENSE-2.0
// |
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
2023-01-23 12:08:30 +01:00
using System.Collections.Immutable;
2022-05-19 21:34:57 +02:00
using System.ComponentModel;
2020-06-13 12:08:21 +02:00
using System.Composition;
2020-11-16 00:44:37 +01:00
using System.Globalization;
2020-06-13 12:08:21 +02:00
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
2023-01-23 12:08:30 +01:00
using ArchiSteamFarm.Helpers;
2022-12-15 19:16:28 +01:00
using ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Data;
using ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Localization;
2020-06-13 12:08:21 +02:00
using ArchiSteamFarm.Plugins;
using ArchiSteamFarm.Plugins.Interfaces;
using ArchiSteamFarm.Steam;
2023-12-05 01:43:55 +01:00
using ArchiSteamFarm.Steam.Interaction;
using ArchiSteamFarm.Storage;
2021-05-06 20:16:06 +02:00
using ArchiSteamFarm.Web;
2021-05-08 01:03:08 +02:00
using ArchiSteamFarm.Web.Responses;
using Newtonsoft.Json;
2020-06-13 12:08:21 +02:00
using Newtonsoft.Json.Linq;
using SteamKit2;
2021-11-10 21:23:24 +01:00
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
2021-11-10 21:23:24 +01:00
[Export(typeof(IPlugin))]
2022-05-19 21:34:57 +02:00
internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotCommand2, IBotSteamClient, ISteamPICSChanges {
private const ushort DepotsRateLimitingDelay = 500;
2021-11-10 21:23:24 +01:00
[JsonProperty]
internal static SteamTokenDumperConfig? Config { get; private set; }
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
private static readonly ConcurrentDictionary<Bot, IDisposable> BotSubscriptions = new();
private static readonly ConcurrentDictionary<Bot, (SemaphoreSlim RefreshSemaphore, Timer RefreshTimer)> BotSynchronizations = new();
private static readonly SemaphoreSlim SubmissionSemaphore = new(1, 1);
2023-11-14 21:20:51 +01:00
private static readonly Timer SubmissionTimer = new(OnSubmissionTimer);
2021-11-10 21:23:24 +01:00
private static GlobalCache? GlobalCache;
2022-05-19 21:34:57 +02:00
private static DateTimeOffset LastUploadAt = DateTimeOffset.MinValue;
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
[JsonProperty]
public override string Name => nameof(SteamTokenDumperPlugin);
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
[JsonProperty]
public override Version Version => typeof(SteamTokenDumperPlugin).Assembly.GetName().Version ?? throw new InvalidOperationException(nameof(Version));
2020-06-13 12:08:21 +02:00
2023-12-05 01:43:55 +01:00
public Task<uint> GetPreferredChangeNumberToStartFrom() => Task.FromResult(GlobalCache?.LastChangeNumber ?? 0);
2020-06-13 15:35:56 +02:00
Plugins breaking: Convert all synchronous interface methods to Task Okay, I wish we had uncovered it earlier as part of V5.2 but it has bitten us in the back just now, so I'm addressing it as part of monthly cycle instead. Previously used void methods did not allow async operations in plugins in a "nice way". If plugin didn't require synchronization with the ASF and just minded its own business, it wasn't half bad as it could use async void signature. However, if plugin by any chance had to do something BEFORE ASF continued with the rest of the logic, it had to explicitly leave non-async void signature and call its async-capable stuff in synchronous manner (usually with Wait() or .Result), which is vastly suboptimal. This was visible even in our STD plugin, which previously had (and still has) GlobalCache initialization in OnASFInit(). If that cache initialization took a bit longer time, STD would hit InvalidOperationException() in OnLicenseList() callback as global cache didn't load yet while we were already long past OnASFInit(). Therefore, I've decided to make a breaking change for a very good reason - all previous methods were converted to tasks, which allows from plugin to do one of three things: - If plugin is async and requires synchronization (like STD), it can declare itself as async await, and do its awaits as-needed, and ASF will wait for those. - If plugin is truly synchronous (and not just a synchronous signature with awful Wait() or .Result, see above), it can simply return Task.CompletedTask and has exactly the same logic. - Finally, if plugin calls some async stuff but doesn't need ASF synchronization, it can "offload" itself from it by calling e.g. ASF's Utilities.InBackground() with whole logic, while returning Task.CompletedTask from the main method. This will allow it to effectively do what async void previously did, by just hooking into the process without intention of slowing it down. All in all I'm confident this approach, while a bit counter-intuitive at first, will result in better compatibility between ASF and the plugins, as if I wanted to fix my STD issue right now without that breaking change, I'd have to actually call .Result on my async global cache loader function, which is utterly stupid if we can fix ASF to do the right thing instead. This "approach" can be commonly found in some other libs with similar to ASF's event-hook behaviour, e.g. Discord.Net. You'll sadly need to do some method signature changes in all of your plugins, as the core OnLoaded() was also changed. See the ones I did in SteamTokenDumperPlugin.cs if you need a practical example, and see ExamplePlugin.cs if you need further explanation.
2021-12-08 16:52:27 +01:00
public async Task OnASFInit(IReadOnlyDictionary<string, JToken>? additionalConfigProperties = null) {
2021-11-10 21:23:24 +01:00
if (!SharedInfo.HasValidToken) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.PluginDisabledMissingBuildToken, nameof(SteamTokenDumperPlugin)));
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
return;
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
bool isEnabled = false;
SteamTokenDumperConfig? config = null;
2021-11-10 21:23:24 +01:00
if (additionalConfigProperties != null) {
foreach ((string configProperty, JToken configValue) in additionalConfigProperties) {
try {
switch (configProperty) {
case nameof(GlobalConfigExtension.SteamTokenDumperPlugin):
config = configValue.ToObject<SteamTokenDumperConfig>();
2021-11-10 21:23:24 +01:00
break;
case nameof(GlobalConfigExtension.SteamTokenDumperPluginEnabled):
isEnabled = configValue.Value<bool>();
2021-11-10 21:23:24 +01:00
break;
2020-06-13 12:08:21 +02:00
}
2021-11-10 21:23:24 +01:00
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.PluginDisabledInConfig, nameof(SteamTokenDumperPlugin)));
return;
2020-06-13 12:08:21 +02:00
}
}
2021-11-10 21:23:24 +01:00
}
2020-06-13 12:08:21 +02:00
2023-12-05 01:43:55 +01:00
if (GlobalCache == null) {
GlobalCache? globalCache = await GlobalCache.Load().ConfigureAwait(false);
if (globalCache == null) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.FileCouldNotBeLoadedFreshInit, nameof(GlobalCache)));
GlobalCache = new GlobalCache();
} else {
GlobalCache = globalCache;
}
}
if (!isEnabled && (config == null)) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginDisabledInConfig, nameof(SteamTokenDumperPlugin)));
return;
}
2021-11-10 21:23:24 +01:00
config ??= new SteamTokenDumperConfig();
2021-11-10 21:23:24 +01:00
if (isEnabled) {
config.Enabled = true;
}
2021-11-10 21:23:24 +01:00
if (!config.Enabled) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginDisabledInConfig, nameof(SteamTokenDumperPlugin)));
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
if (!config.SecretAppIDs.IsEmpty) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginSecretListInitialized, nameof(config.SecretAppIDs), string.Join(", ", config.SecretAppIDs)));
}
2021-11-10 21:23:24 +01:00
if (!config.SecretPackageIDs.IsEmpty) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginSecretListInitialized, nameof(config.SecretPackageIDs), string.Join(", ", config.SecretPackageIDs)));
}
2021-11-10 21:23:24 +01:00
if (!config.SecretDepotIDs.IsEmpty) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginSecretListInitialized, nameof(config.SecretDepotIDs), string.Join(", ", config.SecretDepotIDs)));
}
2023-12-05 01:43:55 +01:00
Config = config;
2021-04-25 23:44:47 +02:00
2023-12-05 01:43:55 +01:00
if (!config.Enabled) {
return;
2021-11-10 21:23:24 +01:00
}
2020-06-13 12:08:21 +02:00
#pragma warning disable CA5394 // This call isn't used in a security-sensitive manner
TimeSpan startIn = TimeSpan.FromMinutes(Random.Shared.Next(SharedInfo.MinimumMinutesBeforeFirstUpload, SharedInfo.MaximumMinutesBeforeFirstUpload));
#pragma warning restore CA5394 // This call isn't used in a security-sensitive manner
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
lock (SubmissionSemaphore) {
2022-05-19 21:34:57 +02:00
SubmissionTimer.Change(startIn, TimeSpan.FromHours(SharedInfo.HoursBetweenUploads));
2020-06-13 12:08:21 +02:00
}
2021-11-10 21:23:24 +01:00
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginInitializedAndEnabled, nameof(SteamTokenDumperPlugin), startIn.ToHumanReadable()));
}
2020-06-13 12:08:21 +02:00
public Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {
2022-05-19 21:34:57 +02:00
ArgumentNullException.ThrowIfNull(bot);
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
if ((args == null) || (args.Length == 0)) {
throw new ArgumentNullException(nameof(args));
}
2023-12-05 01:43:55 +01:00
switch (args.Length) {
case 1:
switch (args[0].ToUpperInvariant()) {
case "STD":
return Task.FromResult(ResponseRefreshManually(access, bot));
2022-05-19 21:34:57 +02:00
}
2023-12-05 01:43:55 +01:00
break;
default:
switch (args[0].ToUpperInvariant()) {
case "STD":
return Task.FromResult(ResponseRefreshManually(access, Utilities.GetArgsAsText(args, 1, ","), steamID));
2022-05-19 21:34:57 +02:00
}
2023-12-05 01:43:55 +01:00
break;
2022-05-19 21:34:57 +02:00
}
2023-12-05 01:43:55 +01:00
return Task.FromResult((string?) null);
2022-05-19 21:34:57 +02:00
}
Plugins breaking: Convert all synchronous interface methods to Task Okay, I wish we had uncovered it earlier as part of V5.2 but it has bitten us in the back just now, so I'm addressing it as part of monthly cycle instead. Previously used void methods did not allow async operations in plugins in a "nice way". If plugin didn't require synchronization with the ASF and just minded its own business, it wasn't half bad as it could use async void signature. However, if plugin by any chance had to do something BEFORE ASF continued with the rest of the logic, it had to explicitly leave non-async void signature and call its async-capable stuff in synchronous manner (usually with Wait() or .Result), which is vastly suboptimal. This was visible even in our STD plugin, which previously had (and still has) GlobalCache initialization in OnASFInit(). If that cache initialization took a bit longer time, STD would hit InvalidOperationException() in OnLicenseList() callback as global cache didn't load yet while we were already long past OnASFInit(). Therefore, I've decided to make a breaking change for a very good reason - all previous methods were converted to tasks, which allows from plugin to do one of three things: - If plugin is async and requires synchronization (like STD), it can declare itself as async await, and do its awaits as-needed, and ASF will wait for those. - If plugin is truly synchronous (and not just a synchronous signature with awful Wait() or .Result, see above), it can simply return Task.CompletedTask and has exactly the same logic. - Finally, if plugin calls some async stuff but doesn't need ASF synchronization, it can "offload" itself from it by calling e.g. ASF's Utilities.InBackground() with whole logic, while returning Task.CompletedTask from the main method. This will allow it to effectively do what async void previously did, by just hooking into the process without intention of slowing it down. All in all I'm confident this approach, while a bit counter-intuitive at first, will result in better compatibility between ASF and the plugins, as if I wanted to fix my STD issue right now without that breaking change, I'd have to actually call .Result on my async global cache loader function, which is utterly stupid if we can fix ASF to do the right thing instead. This "approach" can be commonly found in some other libs with similar to ASF's event-hook behaviour, e.g. Discord.Net. You'll sadly need to do some method signature changes in all of your plugins, as the core OnLoaded() was also changed. See the ones I did in SteamTokenDumperPlugin.cs if you need a practical example, and see ExamplePlugin.cs if you need further explanation.
2021-12-08 16:52:27 +01:00
public async Task OnBotDestroy(Bot bot) {
2021-12-12 01:12:54 +01:00
ArgumentNullException.ThrowIfNull(bot);
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
if (BotSubscriptions.TryRemove(bot, out IDisposable? subscription)) {
subscription.Dispose();
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
if (BotSynchronizations.TryRemove(bot, out (SemaphoreSlim RefreshSemaphore, Timer RefreshTimer) synchronization)) {
2023-07-13 22:47:42 +02:00
// Ensure the semaphore is empty, otherwise we're risking disposed exceptions
await synchronization.RefreshSemaphore.WaitAsync().ConfigureAwait(false);
2021-11-10 21:23:24 +01:00
synchronization.RefreshSemaphore.Dispose();
await synchronization.RefreshTimer.DisposeAsync().ConfigureAwait(false);
2020-06-13 12:08:21 +02:00
}
2021-11-10 21:23:24 +01:00
}
2020-06-13 12:08:21 +02:00
Plugins breaking: Convert all synchronous interface methods to Task Okay, I wish we had uncovered it earlier as part of V5.2 but it has bitten us in the back just now, so I'm addressing it as part of monthly cycle instead. Previously used void methods did not allow async operations in plugins in a "nice way". If plugin didn't require synchronization with the ASF and just minded its own business, it wasn't half bad as it could use async void signature. However, if plugin by any chance had to do something BEFORE ASF continued with the rest of the logic, it had to explicitly leave non-async void signature and call its async-capable stuff in synchronous manner (usually with Wait() or .Result), which is vastly suboptimal. This was visible even in our STD plugin, which previously had (and still has) GlobalCache initialization in OnASFInit(). If that cache initialization took a bit longer time, STD would hit InvalidOperationException() in OnLicenseList() callback as global cache didn't load yet while we were already long past OnASFInit(). Therefore, I've decided to make a breaking change for a very good reason - all previous methods were converted to tasks, which allows from plugin to do one of three things: - If plugin is async and requires synchronization (like STD), it can declare itself as async await, and do its awaits as-needed, and ASF will wait for those. - If plugin is truly synchronous (and not just a synchronous signature with awful Wait() or .Result, see above), it can simply return Task.CompletedTask and has exactly the same logic. - Finally, if plugin calls some async stuff but doesn't need ASF synchronization, it can "offload" itself from it by calling e.g. ASF's Utilities.InBackground() with whole logic, while returning Task.CompletedTask from the main method. This will allow it to effectively do what async void previously did, by just hooking into the process without intention of slowing it down. All in all I'm confident this approach, while a bit counter-intuitive at first, will result in better compatibility between ASF and the plugins, as if I wanted to fix my STD issue right now without that breaking change, I'd have to actually call .Result on my async global cache loader function, which is utterly stupid if we can fix ASF to do the right thing instead. This "approach" can be commonly found in some other libs with similar to ASF's event-hook behaviour, e.g. Discord.Net. You'll sadly need to do some method signature changes in all of your plugins, as the core OnLoaded() was also changed. See the ones I did in SteamTokenDumperPlugin.cs if you need a practical example, and see ExamplePlugin.cs if you need further explanation.
2021-12-08 16:52:27 +01:00
public async Task OnBotInit(Bot bot) {
2021-12-12 01:12:54 +01:00
ArgumentNullException.ThrowIfNull(bot);
2020-06-13 12:08:21 +02:00
2023-12-05 01:53:14 +01:00
if (GlobalCache == null) {
// We can't operate like this anyway, skip initialization of synchronization structures
return;
}
2021-11-10 21:23:24 +01:00
SemaphoreSlim refreshSemaphore = new(1, 1);
Timer refreshTimer = new(OnBotRefreshTimer, bot, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
if (!BotSynchronizations.TryAdd(bot, (refreshSemaphore, refreshTimer))) {
refreshSemaphore.Dispose();
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
await refreshTimer.DisposeAsync().ConfigureAwait(false);
2020-06-13 12:08:21 +02:00
}
2021-11-10 21:23:24 +01:00
}
2020-06-13 12:08:21 +02:00
Plugins breaking: Convert all synchronous interface methods to Task Okay, I wish we had uncovered it earlier as part of V5.2 but it has bitten us in the back just now, so I'm addressing it as part of monthly cycle instead. Previously used void methods did not allow async operations in plugins in a "nice way". If plugin didn't require synchronization with the ASF and just minded its own business, it wasn't half bad as it could use async void signature. However, if plugin by any chance had to do something BEFORE ASF continued with the rest of the logic, it had to explicitly leave non-async void signature and call its async-capable stuff in synchronous manner (usually with Wait() or .Result), which is vastly suboptimal. This was visible even in our STD plugin, which previously had (and still has) GlobalCache initialization in OnASFInit(). If that cache initialization took a bit longer time, STD would hit InvalidOperationException() in OnLicenseList() callback as global cache didn't load yet while we were already long past OnASFInit(). Therefore, I've decided to make a breaking change for a very good reason - all previous methods were converted to tasks, which allows from plugin to do one of three things: - If plugin is async and requires synchronization (like STD), it can declare itself as async await, and do its awaits as-needed, and ASF will wait for those. - If plugin is truly synchronous (and not just a synchronous signature with awful Wait() or .Result, see above), it can simply return Task.CompletedTask and has exactly the same logic. - Finally, if plugin calls some async stuff but doesn't need ASF synchronization, it can "offload" itself from it by calling e.g. ASF's Utilities.InBackground() with whole logic, while returning Task.CompletedTask from the main method. This will allow it to effectively do what async void previously did, by just hooking into the process without intention of slowing it down. All in all I'm confident this approach, while a bit counter-intuitive at first, will result in better compatibility between ASF and the plugins, as if I wanted to fix my STD issue right now without that breaking change, I'd have to actually call .Result on my async global cache loader function, which is utterly stupid if we can fix ASF to do the right thing instead. This "approach" can be commonly found in some other libs with similar to ASF's event-hook behaviour, e.g. Discord.Net. You'll sadly need to do some method signature changes in all of your plugins, as the core OnLoaded() was also changed. See the ones I did in SteamTokenDumperPlugin.cs if you need a practical example, and see ExamplePlugin.cs if you need further explanation.
2021-12-08 16:52:27 +01:00
public Task OnBotSteamCallbacksInit(Bot bot, CallbackManager callbackManager) {
2021-12-12 01:12:54 +01:00
ArgumentNullException.ThrowIfNull(bot);
ArgumentNullException.ThrowIfNull(callbackManager);
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
if (BotSubscriptions.TryRemove(bot, out IDisposable? subscription)) {
subscription.Dispose();
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
if (Config is not { Enabled: true }) {
Plugins breaking: Convert all synchronous interface methods to Task Okay, I wish we had uncovered it earlier as part of V5.2 but it has bitten us in the back just now, so I'm addressing it as part of monthly cycle instead. Previously used void methods did not allow async operations in plugins in a "nice way". If plugin didn't require synchronization with the ASF and just minded its own business, it wasn't half bad as it could use async void signature. However, if plugin by any chance had to do something BEFORE ASF continued with the rest of the logic, it had to explicitly leave non-async void signature and call its async-capable stuff in synchronous manner (usually with Wait() or .Result), which is vastly suboptimal. This was visible even in our STD plugin, which previously had (and still has) GlobalCache initialization in OnASFInit(). If that cache initialization took a bit longer time, STD would hit InvalidOperationException() in OnLicenseList() callback as global cache didn't load yet while we were already long past OnASFInit(). Therefore, I've decided to make a breaking change for a very good reason - all previous methods were converted to tasks, which allows from plugin to do one of three things: - If plugin is async and requires synchronization (like STD), it can declare itself as async await, and do its awaits as-needed, and ASF will wait for those. - If plugin is truly synchronous (and not just a synchronous signature with awful Wait() or .Result, see above), it can simply return Task.CompletedTask and has exactly the same logic. - Finally, if plugin calls some async stuff but doesn't need ASF synchronization, it can "offload" itself from it by calling e.g. ASF's Utilities.InBackground() with whole logic, while returning Task.CompletedTask from the main method. This will allow it to effectively do what async void previously did, by just hooking into the process without intention of slowing it down. All in all I'm confident this approach, while a bit counter-intuitive at first, will result in better compatibility between ASF and the plugins, as if I wanted to fix my STD issue right now without that breaking change, I'd have to actually call .Result on my async global cache loader function, which is utterly stupid if we can fix ASF to do the right thing instead. This "approach" can be commonly found in some other libs with similar to ASF's event-hook behaviour, e.g. Discord.Net. You'll sadly need to do some method signature changes in all of your plugins, as the core OnLoaded() was also changed. See the ones I did in SteamTokenDumperPlugin.cs if you need a practical example, and see ExamplePlugin.cs if you need further explanation.
2021-12-08 16:52:27 +01:00
return Task.CompletedTask;
2021-11-10 21:23:24 +01:00
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
subscription = callbackManager.Subscribe<SteamApps.LicenseListCallback>(callback => OnLicenseList(bot, callback));
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
if (!BotSubscriptions.TryAdd(bot, subscription)) {
subscription.Dispose();
2020-06-13 12:08:21 +02:00
}
Plugins breaking: Convert all synchronous interface methods to Task Okay, I wish we had uncovered it earlier as part of V5.2 but it has bitten us in the back just now, so I'm addressing it as part of monthly cycle instead. Previously used void methods did not allow async operations in plugins in a "nice way". If plugin didn't require synchronization with the ASF and just minded its own business, it wasn't half bad as it could use async void signature. However, if plugin by any chance had to do something BEFORE ASF continued with the rest of the logic, it had to explicitly leave non-async void signature and call its async-capable stuff in synchronous manner (usually with Wait() or .Result), which is vastly suboptimal. This was visible even in our STD plugin, which previously had (and still has) GlobalCache initialization in OnASFInit(). If that cache initialization took a bit longer time, STD would hit InvalidOperationException() in OnLicenseList() callback as global cache didn't load yet while we were already long past OnASFInit(). Therefore, I've decided to make a breaking change for a very good reason - all previous methods were converted to tasks, which allows from plugin to do one of three things: - If plugin is async and requires synchronization (like STD), it can declare itself as async await, and do its awaits as-needed, and ASF will wait for those. - If plugin is truly synchronous (and not just a synchronous signature with awful Wait() or .Result, see above), it can simply return Task.CompletedTask and has exactly the same logic. - Finally, if plugin calls some async stuff but doesn't need ASF synchronization, it can "offload" itself from it by calling e.g. ASF's Utilities.InBackground() with whole logic, while returning Task.CompletedTask from the main method. This will allow it to effectively do what async void previously did, by just hooking into the process without intention of slowing it down. All in all I'm confident this approach, while a bit counter-intuitive at first, will result in better compatibility between ASF and the plugins, as if I wanted to fix my STD issue right now without that breaking change, I'd have to actually call .Result on my async global cache loader function, which is utterly stupid if we can fix ASF to do the right thing instead. This "approach" can be commonly found in some other libs with similar to ASF's event-hook behaviour, e.g. Discord.Net. You'll sadly need to do some method signature changes in all of your plugins, as the core OnLoaded() was also changed. See the ones I did in SteamTokenDumperPlugin.cs if you need a practical example, and see ExamplePlugin.cs if you need further explanation.
2021-12-08 16:52:27 +01:00
return Task.CompletedTask;
2021-11-10 21:23:24 +01:00
}
2020-06-13 12:08:21 +02:00
Plugins breaking: Convert all synchronous interface methods to Task Okay, I wish we had uncovered it earlier as part of V5.2 but it has bitten us in the back just now, so I'm addressing it as part of monthly cycle instead. Previously used void methods did not allow async operations in plugins in a "nice way". If plugin didn't require synchronization with the ASF and just minded its own business, it wasn't half bad as it could use async void signature. However, if plugin by any chance had to do something BEFORE ASF continued with the rest of the logic, it had to explicitly leave non-async void signature and call its async-capable stuff in synchronous manner (usually with Wait() or .Result), which is vastly suboptimal. This was visible even in our STD plugin, which previously had (and still has) GlobalCache initialization in OnASFInit(). If that cache initialization took a bit longer time, STD would hit InvalidOperationException() in OnLicenseList() callback as global cache didn't load yet while we were already long past OnASFInit(). Therefore, I've decided to make a breaking change for a very good reason - all previous methods were converted to tasks, which allows from plugin to do one of three things: - If plugin is async and requires synchronization (like STD), it can declare itself as async await, and do its awaits as-needed, and ASF will wait for those. - If plugin is truly synchronous (and not just a synchronous signature with awful Wait() or .Result, see above), it can simply return Task.CompletedTask and has exactly the same logic. - Finally, if plugin calls some async stuff but doesn't need ASF synchronization, it can "offload" itself from it by calling e.g. ASF's Utilities.InBackground() with whole logic, while returning Task.CompletedTask from the main method. This will allow it to effectively do what async void previously did, by just hooking into the process without intention of slowing it down. All in all I'm confident this approach, while a bit counter-intuitive at first, will result in better compatibility between ASF and the plugins, as if I wanted to fix my STD issue right now without that breaking change, I'd have to actually call .Result on my async global cache loader function, which is utterly stupid if we can fix ASF to do the right thing instead. This "approach" can be commonly found in some other libs with similar to ASF's event-hook behaviour, e.g. Discord.Net. You'll sadly need to do some method signature changes in all of your plugins, as the core OnLoaded() was also changed. See the ones I did in SteamTokenDumperPlugin.cs if you need a practical example, and see ExamplePlugin.cs if you need further explanation.
2021-12-08 16:52:27 +01:00
public Task<IReadOnlyCollection<ClientMsgHandler>?> OnBotSteamHandlersInit(Bot bot) => Task.FromResult((IReadOnlyCollection<ClientMsgHandler>?) null);
2020-06-13 12:08:21 +02:00
Plugins breaking: Convert all synchronous interface methods to Task Okay, I wish we had uncovered it earlier as part of V5.2 but it has bitten us in the back just now, so I'm addressing it as part of monthly cycle instead. Previously used void methods did not allow async operations in plugins in a "nice way". If plugin didn't require synchronization with the ASF and just minded its own business, it wasn't half bad as it could use async void signature. However, if plugin by any chance had to do something BEFORE ASF continued with the rest of the logic, it had to explicitly leave non-async void signature and call its async-capable stuff in synchronous manner (usually with Wait() or .Result), which is vastly suboptimal. This was visible even in our STD plugin, which previously had (and still has) GlobalCache initialization in OnASFInit(). If that cache initialization took a bit longer time, STD would hit InvalidOperationException() in OnLicenseList() callback as global cache didn't load yet while we were already long past OnASFInit(). Therefore, I've decided to make a breaking change for a very good reason - all previous methods were converted to tasks, which allows from plugin to do one of three things: - If plugin is async and requires synchronization (like STD), it can declare itself as async await, and do its awaits as-needed, and ASF will wait for those. - If plugin is truly synchronous (and not just a synchronous signature with awful Wait() or .Result, see above), it can simply return Task.CompletedTask and has exactly the same logic. - Finally, if plugin calls some async stuff but doesn't need ASF synchronization, it can "offload" itself from it by calling e.g. ASF's Utilities.InBackground() with whole logic, while returning Task.CompletedTask from the main method. This will allow it to effectively do what async void previously did, by just hooking into the process without intention of slowing it down. All in all I'm confident this approach, while a bit counter-intuitive at first, will result in better compatibility between ASF and the plugins, as if I wanted to fix my STD issue right now without that breaking change, I'd have to actually call .Result on my async global cache loader function, which is utterly stupid if we can fix ASF to do the right thing instead. This "approach" can be commonly found in some other libs with similar to ASF's event-hook behaviour, e.g. Discord.Net. You'll sadly need to do some method signature changes in all of your plugins, as the core OnLoaded() was also changed. See the ones I did in SteamTokenDumperPlugin.cs if you need a practical example, and see ExamplePlugin.cs if you need further explanation.
2021-12-08 16:52:27 +01:00
public override Task OnLoaded() {
Utilities.WarnAboutIncompleteTranslation(Strings.ResourceManager);
2020-06-13 12:08:21 +02:00
Plugins breaking: Convert all synchronous interface methods to Task Okay, I wish we had uncovered it earlier as part of V5.2 but it has bitten us in the back just now, so I'm addressing it as part of monthly cycle instead. Previously used void methods did not allow async operations in plugins in a "nice way". If plugin didn't require synchronization with the ASF and just minded its own business, it wasn't half bad as it could use async void signature. However, if plugin by any chance had to do something BEFORE ASF continued with the rest of the logic, it had to explicitly leave non-async void signature and call its async-capable stuff in synchronous manner (usually with Wait() or .Result), which is vastly suboptimal. This was visible even in our STD plugin, which previously had (and still has) GlobalCache initialization in OnASFInit(). If that cache initialization took a bit longer time, STD would hit InvalidOperationException() in OnLicenseList() callback as global cache didn't load yet while we were already long past OnASFInit(). Therefore, I've decided to make a breaking change for a very good reason - all previous methods were converted to tasks, which allows from plugin to do one of three things: - If plugin is async and requires synchronization (like STD), it can declare itself as async await, and do its awaits as-needed, and ASF will wait for those. - If plugin is truly synchronous (and not just a synchronous signature with awful Wait() or .Result, see above), it can simply return Task.CompletedTask and has exactly the same logic. - Finally, if plugin calls some async stuff but doesn't need ASF synchronization, it can "offload" itself from it by calling e.g. ASF's Utilities.InBackground() with whole logic, while returning Task.CompletedTask from the main method. This will allow it to effectively do what async void previously did, by just hooking into the process without intention of slowing it down. All in all I'm confident this approach, while a bit counter-intuitive at first, will result in better compatibility between ASF and the plugins, as if I wanted to fix my STD issue right now without that breaking change, I'd have to actually call .Result on my async global cache loader function, which is utterly stupid if we can fix ASF to do the right thing instead. This "approach" can be commonly found in some other libs with similar to ASF's event-hook behaviour, e.g. Discord.Net. You'll sadly need to do some method signature changes in all of your plugins, as the core OnLoaded() was also changed. See the ones I did in SteamTokenDumperPlugin.cs if you need a practical example, and see ExamplePlugin.cs if you need further explanation.
2021-12-08 16:52:27 +01:00
return Task.CompletedTask;
}
public Task OnPICSChanges(uint currentChangeNumber, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> appChanges, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> packageChanges) {
ArgumentOutOfRangeException.ThrowIfZero(currentChangeNumber);
2021-12-12 01:12:54 +01:00
ArgumentNullException.ThrowIfNull(appChanges);
ArgumentNullException.ThrowIfNull(packageChanges);
2020-06-13 12:08:21 +02:00
2023-12-05 01:43:55 +01:00
GlobalCache?.OnPICSChanges(currentChangeNumber, appChanges);
Plugins breaking: Convert all synchronous interface methods to Task Okay, I wish we had uncovered it earlier as part of V5.2 but it has bitten us in the back just now, so I'm addressing it as part of monthly cycle instead. Previously used void methods did not allow async operations in plugins in a "nice way". If plugin didn't require synchronization with the ASF and just minded its own business, it wasn't half bad as it could use async void signature. However, if plugin by any chance had to do something BEFORE ASF continued with the rest of the logic, it had to explicitly leave non-async void signature and call its async-capable stuff in synchronous manner (usually with Wait() or .Result), which is vastly suboptimal. This was visible even in our STD plugin, which previously had (and still has) GlobalCache initialization in OnASFInit(). If that cache initialization took a bit longer time, STD would hit InvalidOperationException() in OnLicenseList() callback as global cache didn't load yet while we were already long past OnASFInit(). Therefore, I've decided to make a breaking change for a very good reason - all previous methods were converted to tasks, which allows from plugin to do one of three things: - If plugin is async and requires synchronization (like STD), it can declare itself as async await, and do its awaits as-needed, and ASF will wait for those. - If plugin is truly synchronous (and not just a synchronous signature with awful Wait() or .Result, see above), it can simply return Task.CompletedTask and has exactly the same logic. - Finally, if plugin calls some async stuff but doesn't need ASF synchronization, it can "offload" itself from it by calling e.g. ASF's Utilities.InBackground() with whole logic, while returning Task.CompletedTask from the main method. This will allow it to effectively do what async void previously did, by just hooking into the process without intention of slowing it down. All in all I'm confident this approach, while a bit counter-intuitive at first, will result in better compatibility between ASF and the plugins, as if I wanted to fix my STD issue right now without that breaking change, I'd have to actually call .Result on my async global cache loader function, which is utterly stupid if we can fix ASF to do the right thing instead. This "approach" can be commonly found in some other libs with similar to ASF's event-hook behaviour, e.g. Discord.Net. You'll sadly need to do some method signature changes in all of your plugins, as the core OnLoaded() was also changed. See the ones I did in SteamTokenDumperPlugin.cs if you need a practical example, and see ExamplePlugin.cs if you need further explanation.
2021-12-08 16:52:27 +01:00
return Task.CompletedTask;
2021-11-10 21:23:24 +01:00
}
Plugins breaking: Convert all synchronous interface methods to Task Okay, I wish we had uncovered it earlier as part of V5.2 but it has bitten us in the back just now, so I'm addressing it as part of monthly cycle instead. Previously used void methods did not allow async operations in plugins in a "nice way". If plugin didn't require synchronization with the ASF and just minded its own business, it wasn't half bad as it could use async void signature. However, if plugin by any chance had to do something BEFORE ASF continued with the rest of the logic, it had to explicitly leave non-async void signature and call its async-capable stuff in synchronous manner (usually with Wait() or .Result), which is vastly suboptimal. This was visible even in our STD plugin, which previously had (and still has) GlobalCache initialization in OnASFInit(). If that cache initialization took a bit longer time, STD would hit InvalidOperationException() in OnLicenseList() callback as global cache didn't load yet while we were already long past OnASFInit(). Therefore, I've decided to make a breaking change for a very good reason - all previous methods were converted to tasks, which allows from plugin to do one of three things: - If plugin is async and requires synchronization (like STD), it can declare itself as async await, and do its awaits as-needed, and ASF will wait for those. - If plugin is truly synchronous (and not just a synchronous signature with awful Wait() or .Result, see above), it can simply return Task.CompletedTask and has exactly the same logic. - Finally, if plugin calls some async stuff but doesn't need ASF synchronization, it can "offload" itself from it by calling e.g. ASF's Utilities.InBackground() with whole logic, while returning Task.CompletedTask from the main method. This will allow it to effectively do what async void previously did, by just hooking into the process without intention of slowing it down. All in all I'm confident this approach, while a bit counter-intuitive at first, will result in better compatibility between ASF and the plugins, as if I wanted to fix my STD issue right now without that breaking change, I'd have to actually call .Result on my async global cache loader function, which is utterly stupid if we can fix ASF to do the right thing instead. This "approach" can be commonly found in some other libs with similar to ASF's event-hook behaviour, e.g. Discord.Net. You'll sadly need to do some method signature changes in all of your plugins, as the core OnLoaded() was also changed. See the ones I did in SteamTokenDumperPlugin.cs if you need a practical example, and see ExamplePlugin.cs if you need further explanation.
2021-12-08 16:52:27 +01:00
public Task OnPICSChangesRestart(uint currentChangeNumber) {
ArgumentOutOfRangeException.ThrowIfZero(currentChangeNumber);
2020-06-13 12:08:21 +02:00
2023-12-05 01:43:55 +01:00
GlobalCache?.OnPICSChangesRestart(currentChangeNumber);
Plugins breaking: Convert all synchronous interface methods to Task Okay, I wish we had uncovered it earlier as part of V5.2 but it has bitten us in the back just now, so I'm addressing it as part of monthly cycle instead. Previously used void methods did not allow async operations in plugins in a "nice way". If plugin didn't require synchronization with the ASF and just minded its own business, it wasn't half bad as it could use async void signature. However, if plugin by any chance had to do something BEFORE ASF continued with the rest of the logic, it had to explicitly leave non-async void signature and call its async-capable stuff in synchronous manner (usually with Wait() or .Result), which is vastly suboptimal. This was visible even in our STD plugin, which previously had (and still has) GlobalCache initialization in OnASFInit(). If that cache initialization took a bit longer time, STD would hit InvalidOperationException() in OnLicenseList() callback as global cache didn't load yet while we were already long past OnASFInit(). Therefore, I've decided to make a breaking change for a very good reason - all previous methods were converted to tasks, which allows from plugin to do one of three things: - If plugin is async and requires synchronization (like STD), it can declare itself as async await, and do its awaits as-needed, and ASF will wait for those. - If plugin is truly synchronous (and not just a synchronous signature with awful Wait() or .Result, see above), it can simply return Task.CompletedTask and has exactly the same logic. - Finally, if plugin calls some async stuff but doesn't need ASF synchronization, it can "offload" itself from it by calling e.g. ASF's Utilities.InBackground() with whole logic, while returning Task.CompletedTask from the main method. This will allow it to effectively do what async void previously did, by just hooking into the process without intention of slowing it down. All in all I'm confident this approach, while a bit counter-intuitive at first, will result in better compatibility between ASF and the plugins, as if I wanted to fix my STD issue right now without that breaking change, I'd have to actually call .Result on my async global cache loader function, which is utterly stupid if we can fix ASF to do the right thing instead. This "approach" can be commonly found in some other libs with similar to ASF's event-hook behaviour, e.g. Discord.Net. You'll sadly need to do some method signature changes in all of your plugins, as the core OnLoaded() was also changed. See the ones I did in SteamTokenDumperPlugin.cs if you need a practical example, and see ExamplePlugin.cs if you need further explanation.
2021-12-08 16:52:27 +01:00
return Task.CompletedTask;
2021-11-10 21:23:24 +01:00
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
private static async void OnBotRefreshTimer(object? state) {
if (state is not Bot bot) {
throw new InvalidOperationException(nameof(state));
2020-06-13 12:08:21 +02:00
}
2021-11-10 21:23:24 +01:00
await Refresh(bot).ConfigureAwait(false);
}
2021-07-12 21:45:17 +02:00
2021-11-10 21:23:24 +01:00
private static async void OnLicenseList(Bot bot, SteamApps.LicenseListCallback callback) {
2021-12-12 01:12:54 +01:00
ArgumentNullException.ThrowIfNull(bot);
ArgumentNullException.ThrowIfNull(callback);
2020-11-14 22:37:00 +01:00
2021-11-10 21:23:24 +01:00
if (Config is not { Enabled: true }) {
return;
}
2020-06-13 12:08:21 +02:00
HashSet<uint> packageIDs = callback.LicenseList.Where(static license => !Config.SecretPackageIDs.Contains(license.PackageID) && ((license.PaymentMethod != EPaymentMethod.AutoGrant) || !Config.SkipAutoGrantPackages)).Select(static license => license.PackageID).ToHashSet();
2020-06-13 12:08:21 +02:00
await Refresh(bot, packageIDs).ConfigureAwait(false);
2021-11-10 21:23:24 +01:00
}
2023-11-14 21:20:51 +01:00
private static async void OnSubmissionTimer(object? state = null) => await SubmitData().ConfigureAwait(false);
2021-11-10 21:23:24 +01:00
private static async Task Refresh(Bot bot, IReadOnlyCollection<uint>? packageIDs = null) {
2021-12-12 01:12:54 +01:00
ArgumentNullException.ThrowIfNull(bot);
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
if (GlobalCache == null) {
throw new InvalidOperationException(nameof(GlobalCache));
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
if (ASF.GlobalDatabase == null) {
2023-12-05 01:43:55 +01:00
throw new InvalidOperationException(nameof(ASF.GlobalDatabase));
2021-11-10 21:23:24 +01:00
}
2020-11-14 22:37:00 +01:00
2021-11-10 21:23:24 +01:00
if (!BotSynchronizations.TryGetValue(bot, out (SemaphoreSlim RefreshSemaphore, Timer RefreshTimer) synchronization)) {
throw new InvalidOperationException(nameof(synchronization));
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
if (!await synchronization.RefreshSemaphore.WaitAsync(0).ConfigureAwait(false)) {
return;
}
2020-06-13 12:08:21 +02:00
SemaphoreSlim depotsRateLimitingSemaphore = new(1, 1);
2021-11-10 21:23:24 +01:00
try {
if (!bot.IsConnectedAndLoggedOn) {
2020-06-13 12:08:21 +02:00
return;
}
2023-12-05 01:43:55 +01:00
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();
2020-06-13 12:08:21 +02:00
2023-12-11 23:55:13 +01:00
HashSet<uint> appIDsToRefresh = [];
2020-06-13 12:08:21 +02:00
2023-12-05 01:43:55 +01:00
foreach (uint packageID in packageIDs.Where(static packageID => Config?.SecretPackageIDs.Contains(packageID) != true)) {
if (!ASF.GlobalDatabase.PackagesDataReadOnly.TryGetValue(packageID, out PackageData? packageData) || (packageData.AppIDs == null)) {
2021-11-10 21:23:24 +01:00
// ASF might not have the package info for us at the moment, we'll retry later
continue;
2020-06-13 12:08:21 +02:00
}
2023-12-05 01:43:55 +01:00
appIDsToRefresh.UnionWith(packageData.AppIDs.Where(static appID => (Config?.SecretAppIDs.Contains(appID) != true) && GlobalCache.ShouldRefreshAppInfo(appID)));
2021-11-10 21:23:24 +01:00
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
if (appIDsToRefresh.Count == 0) {
bot.ArchiLogger.LogGenericDebug(Strings.BotNoAppsToRefresh);
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
return;
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotRetrievingTotalAppAccessTokens, appIDsToRefresh.Count));
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
HashSet<uint> appIDsThisRound = new(Math.Min(appIDsToRefresh.Count, SharedInfo.AppInfosPerSingleRequest));
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
using (HashSet<uint>.Enumerator enumerator = appIDsToRefresh.GetEnumerator()) {
while (true) {
if (!bot.IsConnectedAndLoggedOn) {
return;
}
2021-11-10 21:23:24 +01:00
while ((appIDsThisRound.Count < SharedInfo.AppInfosPerSingleRequest) && enumerator.MoveNext()) {
appIDsThisRound.Add(enumerator.Current);
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
if (appIDsThisRound.Count == 0) {
break;
}
2020-06-20 17:12:18 +02:00
2021-11-10 21:23:24 +01:00
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotRetrievingAppAccessTokens, appIDsThisRound.Count));
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
SteamApps.PICSTokensCallback response;
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
try {
response = await bot.SteamApps.PICSGetAccessTokens(appIDsThisRound, Enumerable.Empty<uint>()).ToLongRunningTask().ConfigureAwait(false);
} catch (Exception e) {
bot.ArchiLogger.LogGenericWarningException(e);
2020-06-13 12:08:21 +02:00
appIDsThisRound.Clear();
continue;
2021-11-10 21:23:24 +01:00
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotFinishedRetrievingAppAccessTokens, appIDsThisRound.Count));
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
appIDsThisRound.Clear();
GlobalCache.UpdateAppTokens(response.AppTokens, response.AppTokensDenied);
2020-06-13 12:08:21 +02:00
}
2021-11-10 21:23:24 +01:00
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotFinishedRetrievingTotalAppAccessTokens, appIDsToRefresh.Count));
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotRetrievingTotalDepots, appIDsToRefresh.Count));
2020-06-13 12:08:21 +02:00
(_, ImmutableHashSet<uint>? knownDepotIDs) = await GlobalCache.KnownDepotIDs.GetValue(ECacheFallback.SuccessPreviously).ConfigureAwait(false);
2023-01-23 12:08:30 +01:00
2021-11-10 21:23:24 +01:00
using (HashSet<uint>.Enumerator enumerator = appIDsToRefresh.GetEnumerator()) {
while (true) {
if (!bot.IsConnectedAndLoggedOn) {
return;
}
2021-11-10 21:23:24 +01:00
while ((appIDsThisRound.Count < SharedInfo.AppInfosPerSingleRequest) && enumerator.MoveNext()) {
appIDsThisRound.Add(enumerator.Current);
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
if (appIDsThisRound.Count == 0) {
break;
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotRetrievingAppInfos, appIDsThisRound.Count));
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
AsyncJobMultiple<SteamApps.PICSProductInfoCallback>.ResultSet response;
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
try {
response = await bot.SteamApps.PICSGetProductInfo(appIDsThisRound.Select(static appID => new SteamApps.PICSRequest(appID, GlobalCache.GetAppToken(appID))), Enumerable.Empty<SteamApps.PICSRequest>()).ToLongRunningTask().ConfigureAwait(false);
} catch (Exception e) {
bot.ArchiLogger.LogGenericWarningException(e);
2020-06-13 12:08:21 +02:00
appIDsThisRound.Clear();
continue;
2021-11-10 21:23:24 +01:00
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
if (response.Results == null) {
bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, nameof(response.Results)));
2020-06-13 12:08:21 +02:00
2023-01-19 15:17:47 +01:00
appIDsThisRound.Clear();
continue;
2021-11-10 21:23:24 +01:00
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotFinishedRetrievingAppInfos, appIDsThisRound.Count));
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
appIDsThisRound.Clear();
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
Dictionary<uint, uint> appChangeNumbers = new();
uint depotKeysSuccessful = 0;
uint depotKeysTotal = 0;
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
foreach (SteamApps.PICSProductInfoCallback.PICSProductInfo app in response.Results.SelectMany(static result => result.Apps.Values)) {
appChangeNumbers[app.ID] = app.ChangeNumber;
2020-06-13 12:08:21 +02:00
2023-01-23 11:30:01 +01:00
bool shouldFetchMainKey = false;
foreach (KeyValue depot in app.KeyValues["depots"].Children) {
2023-12-05 01:43:55 +01:00
if (!uint.TryParse(depot.Name, out uint depotID) || (knownDepotIDs?.Contains(depotID) == true) || (Config?.SecretDepotIDs.Contains(depotID) == true) || !GlobalCache.ShouldRefreshDepotKey(depotID)) {
continue;
}
depotKeysTotal++;
await depotsRateLimitingSemaphore.WaitAsync().ConfigureAwait(false);
try {
SteamApps.DepotKeyCallback depotResponse = await bot.SteamApps.GetDepotDecryptionKey(depotID, app.ID).ToLongRunningTask().ConfigureAwait(false);
depotKeysSuccessful++;
2023-01-23 11:30:01 +01:00
if (depotResponse.Result != EResult.OK) {
continue;
}
shouldFetchMainKey = true;
GlobalCache.UpdateDepotKey(depotResponse);
} catch (Exception e) {
// We can still try other depots
bot.ArchiLogger.LogGenericWarningException(e);
} finally {
Utilities.InBackground(
async () => {
await Task.Delay(DepotsRateLimitingDelay).ConfigureAwait(false);
// ReSharper disable once AccessToDisposedClosure - we're waiting for the semaphore to be free before disposing it
depotsRateLimitingSemaphore.Release();
}
);
}
2021-11-10 21:23:24 +01:00
}
2020-06-13 12:08:21 +02:00
// Consider fetching main appID key only if we've actually considered some new depots for resolving
2023-01-23 12:08:30 +01:00
if (shouldFetchMainKey && (knownDepotIDs?.Contains(app.ID) != true) && GlobalCache.ShouldRefreshDepotKey(app.ID)) {
depotKeysTotal++;
2020-06-13 12:08:21 +02:00
await depotsRateLimitingSemaphore.WaitAsync().ConfigureAwait(false);
2020-06-25 17:53:37 +02:00
try {
SteamApps.DepotKeyCallback depotResponse = await bot.SteamApps.GetDepotDecryptionKey(app.ID, app.ID).ToLongRunningTask().ConfigureAwait(false);
2020-06-25 17:53:37 +02:00
depotKeysSuccessful++;
2023-01-19 15:17:47 +01:00
GlobalCache.UpdateDepotKey(depotResponse);
} catch (Exception e) {
// We can still try other depots
bot.ArchiLogger.LogGenericWarningException(e);
} finally {
Utilities.InBackground(
async () => {
await Task.Delay(DepotsRateLimitingDelay).ConfigureAwait(false);
2023-01-19 15:17:47 +01:00
// ReSharper disable once AccessToDisposedClosure - we're waiting for the semaphore to be free before disposing it
depotsRateLimitingSemaphore.Release();
}
);
2023-01-19 15:17:47 +01:00
}
2021-11-10 21:23:24 +01:00
}
}
2020-06-13 12:08:21 +02:00
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotFinishedRetrievingDepotKeys, depotKeysSuccessful, depotKeysTotal));
2023-01-19 15:17:47 +01:00
if (depotKeysSuccessful < depotKeysTotal) {
// We're not going to record app change numbers, as we didn't fetch all the depot keys we wanted
continue;
2020-06-13 12:08:21 +02:00
}
GlobalCache.UpdateAppChangeNumbers(appChangeNumbers);
2020-06-13 12:08:21 +02:00
}
2021-11-10 21:23:24 +01:00
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotFinishedRetrievingTotalDepots, appIDsToRefresh.Count));
} finally {
2023-12-05 01:43:55 +01:00
if (Config?.Enabled == true) {
TimeSpan timeSpan = TimeSpan.FromHours(SharedInfo.MaximumHoursBetweenRefresh);
2020-06-13 12:08:21 +02:00
2023-12-05 01:43:55 +01:00
synchronization.RefreshTimer.Change(timeSpan, timeSpan);
}
await depotsRateLimitingSemaphore.WaitAsync().ConfigureAwait(false);
2021-11-10 21:23:24 +01:00
synchronization.RefreshSemaphore.Release();
depotsRateLimitingSemaphore.Dispose();
2020-06-13 12:08:21 +02:00
}
2021-11-10 21:23:24 +01:00
}
2020-06-13 12:08:21 +02:00
private static string? ResponseRefreshManually(EAccess access, Bot bot) {
2023-12-05 01:43:55 +01:00
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
ArgumentNullException.ThrowIfNull(bot);
if (access < EAccess.Master) {
return access > EAccess.None ? bot.Commands.FormatBotResponse(ArchiSteamFarm.Localization.Strings.ErrorAccessDenied) : null;
}
if (GlobalCache == null) {
return bot.Commands.FormatBotResponse(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, nameof(GlobalCache)));
}
Utilities.InBackground(
async () => {
await Refresh(bot).ConfigureAwait(false);
await SubmitData().ConfigureAwait(false);
}
);
2023-12-05 01:43:55 +01:00
return bot.Commands.FormatBotResponse(ArchiSteamFarm.Localization.Strings.Done);
}
private static string? ResponseRefreshManually(EAccess access, string botNames, ulong steamID = 0) {
2023-12-05 01:43:55 +01:00
if (!Enum.IsDefined(access)) {
throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
}
ArgumentException.ThrowIfNullOrEmpty(botNames);
if ((steamID != 0) && !new SteamID(steamID).IsIndividualAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));
}
HashSet<Bot>? bots = Bot.GetBots(botNames);
if ((bots == null) || (bots.Count == 0)) {
return access >= EAccess.Owner ? Commands.FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
}
2024-01-01 23:22:19 +01:00
if (bots.RemoveWhere(bot => Commands.GetProxyAccess(bot, access, steamID) < EAccess.Master) > 0) {
if (bots.Count == 0) {
return access >= EAccess.Owner ? Commands.FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
}
}
2023-12-05 01:43:55 +01:00
if (GlobalCache == null) {
return Commands.FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, nameof(GlobalCache)));
}
Utilities.InBackground(
async () => {
await Utilities.InParallel(bots.Select(static bot => Refresh(bot))).ConfigureAwait(false);
2023-12-05 01:43:55 +01:00
await SubmitData().ConfigureAwait(false);
}
);
2023-12-05 01:43:55 +01:00
return Commands.FormatStaticResponse(ArchiSteamFarm.Localization.Strings.Done);
2023-12-05 01:43:55 +01:00
}
2023-11-14 21:20:51 +01:00
private static async Task SubmitData(CancellationToken cancellationToken = default) {
2021-11-10 21:23:24 +01:00
if (Bot.Bots == null) {
throw new InvalidOperationException(nameof(Bot.Bots));
}
2021-11-10 21:23:24 +01:00
if (GlobalCache == null) {
throw new InvalidOperationException(nameof(GlobalCache));
}
2020-11-14 22:37:00 +01:00
2021-11-10 21:23:24 +01:00
if (ASF.WebBrowser == null) {
throw new InvalidOperationException(nameof(ASF.WebBrowser));
}
2020-06-13 12:08:21 +02:00
2022-05-19 21:34:57 +02:00
if (LastUploadAt + TimeSpan.FromMinutes(SharedInfo.MinimumMinutesBetweenUploads) > DateTimeOffset.UtcNow) {
return;
}
2023-11-14 21:20:51 +01:00
if (!await SubmissionSemaphore.WaitAsync(0, cancellationToken).ConfigureAwait(false)) {
2021-11-10 21:23:24 +01:00
return;
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
try {
Dictionary<uint, ulong> appTokens = GlobalCache.GetAppTokensForSubmission();
Dictionary<uint, ulong> packageTokens = GlobalCache.GetPackageTokensForSubmission();
Dictionary<uint, string> depotKeys = GlobalCache.GetDepotKeysForSubmission();
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
if ((appTokens.Count == 0) && (packageTokens.Count == 0) && (depotKeys.Count == 0)) {
ASF.ArchiLogger.LogGenericInfo(Strings.SubmissionNoNewData);
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
return;
}
2020-06-13 12:08:21 +02:00
2022-02-18 15:40:33 +01:00
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;
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
if (contributorSteamID == 0) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.SubmissionNoContributorSet, nameof(ASF.GlobalConfig.SteamOwnerID)));
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
return;
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
Uri request = new($"{SharedInfo.ServerURL}/submit");
2022-12-15 19:16:28 +01:00
SubmitRequest data = new(contributorSteamID, appTokens, packageTokens, depotKeys);
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.SubmissionInProgress, appTokens.Count, packageTokens.Count, depotKeys.Count));
2020-06-13 12:08:21 +02:00
2023-11-14 21:20:51 +01:00
ObjectResponse<SubmitResponse>? response = await ASF.WebBrowser.UrlPostToJsonObject<SubmitResponse, SubmitRequest>(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors, cancellationToken: cancellationToken).ConfigureAwait(false);
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
if (response == null) {
ASF.ArchiLogger.LogGenericWarning(ArchiSteamFarm.Localization.Strings.WarningFailed);
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
return;
}
2021-01-04 17:42:31 +01:00
2022-05-19 21:38:40 +02:00
// We've communicated with the server and didn't timeout, regardless of the success, this was the last upload attempt
LastUploadAt = DateTimeOffset.UtcNow;
2021-11-10 21:23:24 +01:00
if (response.StatusCode.IsClientErrorCode()) {
ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, response.StatusCode));
2021-01-04 17:42:31 +01:00
switch (response.StatusCode) {
2023-12-05 01:43:55 +01:00
case HttpStatusCode.Forbidden when Config?.Enabled == true:
// SteamDB told us to stop submitting data for now
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
lock (SubmissionSemaphore) {
SubmissionTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
}
break;
case HttpStatusCode.Conflict:
// SteamDB told us to reset our cache
GlobalCache.Reset(true);
break;
2023-12-05 01:43:55 +01:00
case HttpStatusCode.TooManyRequests when Config?.Enabled == true:
// SteamDB told us to try again later
#pragma warning disable CA5394 // This call isn't used in a security-sensitive manner
TimeSpan startIn = TimeSpan.FromMinutes(Random.Shared.Next(SharedInfo.MinimumMinutesBeforeFirstUpload, SharedInfo.MaximumMinutesBeforeFirstUpload));
#pragma warning restore CA5394 // This call isn't used in a security-sensitive manner
2020-06-13 12:08:21 +02:00
// ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that
lock (SubmissionSemaphore) {
SubmissionTimer.Change(startIn, TimeSpan.FromHours(SharedInfo.HoursBetweenUploads));
}
2020-06-13 12:08:21 +02:00
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.SubmissionFailedTooManyRequests, startIn.ToHumanReadable()));
break;
2020-06-13 12:08:21 +02:00
}
2021-11-10 21:23:24 +01:00
return;
}
2021-02-22 18:25:29 +01:00
if (response.Content is not { Success: true }) {
2021-11-10 21:23:24 +01:00
ASF.ArchiLogger.LogGenericError(ArchiSteamFarm.Localization.Strings.WarningFailed);
return;
}
2021-02-22 18:25:29 +01:00
2021-11-10 21:23:24 +01:00
if (response.Content.Data == null) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsInvalid), nameof(response.Content.Data));
2021-11-10 21:23:24 +01:00
return;
}
2021-11-10 21:23:24 +01:00
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));
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
GlobalCache.UpdateSubmittedData(appTokens, packageTokens, depotKeys);
2021-05-09 18:59:17 +02:00
2021-11-10 21:23:24 +01:00
if (!response.Content.Data.NewApps.IsEmpty) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.SubmissionSuccessfulNewApps, string.Join(", ", response.Content.Data.NewApps)));
}
2021-05-09 18:59:17 +02:00
2021-11-10 21:23:24 +01:00
if (!response.Content.Data.VerifiedApps.IsEmpty) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.SubmissionSuccessfulVerifiedApps, string.Join(", ", response.Content.Data.VerifiedApps)));
}
2021-05-09 18:59:17 +02:00
2021-11-10 21:23:24 +01:00
if (!response.Content.Data.NewPackages.IsEmpty) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.SubmissionSuccessfulNewPackages, string.Join(", ", response.Content.Data.NewPackages)));
}
2021-05-09 18:59:17 +02:00
2021-11-10 21:23:24 +01:00
if (!response.Content.Data.VerifiedPackages.IsEmpty) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.SubmissionSuccessfulVerifiedPackages, string.Join(", ", response.Content.Data.VerifiedPackages)));
}
2021-05-09 18:59:17 +02:00
2021-11-10 21:23:24 +01:00
if (!response.Content.Data.NewDepots.IsEmpty) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.SubmissionSuccessfulNewDepots, string.Join(", ", response.Content.Data.NewDepots)));
}
2021-05-09 18:59:17 +02:00
2021-11-10 21:23:24 +01:00
if (!response.Content.Data.VerifiedDepots.IsEmpty) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.SubmissionSuccessfulVerifiedDepots, string.Join(", ", response.Content.Data.VerifiedDepots)));
2020-06-13 12:08:21 +02:00
}
2021-11-10 21:23:24 +01:00
} finally {
SubmissionSemaphore.Release();
2020-06-13 12:08:21 +02:00
}
}
}