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

611 lines
24 KiB
C#
Raw Normal View History

2020-06-13 12:08:21 +02:00
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
2022-01-06 20:22:38 +01:00
// Copyright 2015-2022 Ł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;
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;
using ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Localization;
2020-06-13 12:08:21 +02:00
using ArchiSteamFarm.Plugins;
using ArchiSteamFarm.Plugins.Interfaces;
using ArchiSteamFarm.Steam;
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 {
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);
private static readonly Timer SubmissionTimer = new(SubmitData);
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
2021-11-10 21:23:24 +01:00
public Task<uint> GetPreferredChangeNumberToStartFrom() => Task.FromResult(Config?.Enabled == true ? GlobalCache?.LastChangeNumber ?? 0 : 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
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
return;
}
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)));
}
2021-11-10 21:23:24 +01:00
if (GlobalCache == null) {
GlobalCache? globalCache = await GlobalCache.Load().ConfigureAwait(false);
2021-04-25 23:44:47 +02:00
2021-11-10 21:23:24 +01:00
if (globalCache == null) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.FileCouldNotBeLoadedFreshInit, nameof(GlobalCache)));
2021-04-25 23:44:47 +02:00
2021-11-10 21:23:24 +01:00
GlobalCache = new GlobalCache();
} else {
GlobalCache = globalCache;
2021-04-25 23:44:47 +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
Config = config;
#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
2022-05-19 21:34:57 +02:00
public Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {
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));
}
switch (args[0].ToUpperInvariant()) {
case "STD" when access >= EAccess.Owner:
if (Config is not { Enabled: true }) {
return Task.FromResult((string?) string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.WarningFailedWithError, nameof(Config)));
}
2022-05-19 21:34:57 +02:00
TimeSpan minimumTimeBetweenUpload = TimeSpan.FromMinutes(SharedInfo.MinimumMinutesBetweenUploads);
if (LastUploadAt + minimumTimeBetweenUpload > DateTimeOffset.UtcNow) {
return Task.FromResult((string?) string.Format(CultureInfo.CurrentCulture, Strings.SubmissionFailedTooManyRequests, minimumTimeBetweenUpload.ToHumanReadable()));
}
// 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(TimeSpan.Zero, TimeSpan.FromHours(SharedInfo.HoursBetweenUploads));
}
return Task.FromResult((string?) ArchiSteamFarm.Localization.Strings.Done);
case "STD" when access > EAccess.None:
return Task.FromResult((string?) ArchiSteamFarm.Localization.Strings.ErrorAccessDenied);
default:
return Task.FromResult((string?) null);
}
}
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)) {
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
2021-11-10 21:23:24 +01:00
if (Config is not { Enabled: true }) {
return;
}
2020-06-13 12:08:21 +02:00
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) {
2021-11-10 21:23:24 +01:00
if (currentChangeNumber == 0) {
throw new ArgumentOutOfRangeException(nameof(currentChangeNumber));
}
2020-11-14 22:37:00 +01:00
2021-12-12 01:12:54 +01:00
ArgumentNullException.ThrowIfNull(appChanges);
ArgumentNullException.ThrowIfNull(packageChanges);
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
if (GlobalCache == null) {
throw new InvalidOperationException(nameof(GlobalCache));
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +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) {
2021-11-10 21:23:24 +01:00
if (currentChangeNumber == 0) {
throw new ArgumentOutOfRangeException(nameof(currentChangeNumber));
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
if (GlobalCache == null) {
throw new InvalidOperationException(nameof(GlobalCache));
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +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
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
Dictionary<uint, ulong> packageTokens = callback.LicenseList.Where(static license => !Config.SecretPackageIDs.Contains(license.PackageID) && ((license.PaymentMethod != EPaymentMethod.AutoGrant) || !Config.SkipAutoGrantPackages)).GroupBy(static license => license.PackageID).ToDictionary(static group => group.Key, static group => group.OrderByDescending(static license => license.TimeCreated).First().AccessToken);
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
GlobalCache.UpdatePackageTokens(packageTokens);
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
await Refresh(bot, packageTokens.Keys).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 (Config is not { Enabled: true }) {
return;
}
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) {
throw new InvalidOperationException(nameof(GlobalCache));
}
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
2021-11-10 21:23:24 +01:00
try {
if (!bot.IsConnectedAndLoggedOn) {
2020-06-13 12:08:21 +02:00
return;
}
2021-11-10 21:23:24 +01:00
packageIDs ??= bot.OwnedPackageIDs.Where(static package => !Config.SecretPackageIDs.Contains(package.Key) && ((package.Value.PaymentMethod != EPaymentMethod.AutoGrant) || !Config.SkipAutoGrantPackages)).Select(static package => package.Key).ToHashSet();
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
HashSet<uint> appIDsToRefresh = new();
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
foreach (uint packageID in packageIDs.Where(static packageID => !Config.SecretPackageIDs.Contains(packageID))) {
if (!ASF.GlobalDatabase.PackagesDataReadOnly.TryGetValue(packageID, out (uint ChangeNumber, ImmutableHashSet<uint>? AppIDs) packageData) || (packageData.AppIDs == null)) {
// 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
}
2021-11-10 21:23:24 +01:00
appIDsToRefresh.UnionWith(packageData.AppIDs.Where(static appID => !Config.SecretAppIDs.Contains(appID) && GlobalCache.ShouldRefreshAppInfo(appID)));
}
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) {
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
if (!bot.IsConnectedAndLoggedOn) {
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.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
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.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
2021-11-10 21:23:24 +01:00
using (HashSet<uint>.Enumerator enumerator = appIDsToRefresh.GetEnumerator()) {
while (true) {
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
if (!bot.IsConnectedAndLoggedOn) {
return;
}
2020-06-20 17:12:18 +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
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
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
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.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();
2021-11-10 21:23:24 +01:00
HashSet<Task<SteamApps.DepotKeyCallback>> depotTasks = new();
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
2021-11-10 21:23:24 +01:00
if (GlobalCache.ShouldRefreshDepotKey(app.ID)) {
depotTasks.Add(bot.SteamApps.GetDepotDecryptionKey(app.ID, app.ID).ToLongRunningTask());
}
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
foreach (KeyValue depot in app.KeyValues["depots"].Children) {
if (uint.TryParse(depot.Name, out uint depotID) && !Config.SecretDepotIDs.Contains(depotID) && GlobalCache.ShouldRefreshDepotKey(depotID)) {
depotTasks.Add(bot.SteamApps.GetDepotDecryptionKey(depotID, app.ID).ToLongRunningTask());
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
if (depotTasks.Count > 0) {
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotRetrievingDepotKeys, depotTasks.Count));
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
IList<SteamApps.DepotKeyCallback> results;
2020-06-25 17:53:37 +02:00
2021-11-10 21:23:24 +01:00
try {
results = await Utilities.InParallel(depotTasks).ConfigureAwait(false);
} catch (Exception e) {
bot.ArchiLogger.LogGenericWarningException(e);
2020-06-25 17:53:37 +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.BotFinishedRetrievingDepotKeys, depotTasks.Count));
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
GlobalCache.UpdateDepotKeys(results);
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 {
TimeSpan timeSpan = TimeSpan.FromHours(SharedInfo.MaximumHoursBetweenRefresh);
2020-06-13 12:08:21 +02:00
2021-11-10 21:23:24 +01:00
synchronization.RefreshTimer.Change(timeSpan, timeSpan);
synchronization.RefreshSemaphore.Release();
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
private static async void SubmitData(object? state = null) {
if (Bot.Bots == null) {
throw new InvalidOperationException(nameof(Bot.Bots));
}
2021-11-10 21:23:24 +01:00
if (Config is not { Enabled: true }) {
return;
}
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-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;
}
2021-11-10 21:23:24 +01:00
if (!await SubmissionSemaphore.WaitAsync(0).ConfigureAwait(false)) {
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");
RequestData requestData = 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
2021-11-10 21:23:24 +01:00
ObjectResponse<ResponseData>? response = await ASF.WebBrowser.UrlPostToJsonObject<ResponseData, RequestData>(request, data: requestData, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).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
2020-06-13 12:08:21 +02:00
#if NETFRAMEWORK
2021-11-10 21:47:42 +01:00
if (response.StatusCode == (HttpStatusCode) 429) {
2020-06-13 12:08:21 +02:00
#else
2021-11-10 21:23:24 +01:00
if (response.StatusCode == HttpStatusCode.TooManyRequests) {
2020-06-13 12:08:21 +02:00
#endif
#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.SubmissionFailedTooManyRequests, startIn.ToHumanReadable()));
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
2021-11-10 21:23:24 +01:00
if (!response.Content.Success) {
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
}
}
}