diff --git a/.github/RELEASE_TEMPLATE.md b/.github/RELEASE_TEMPLATE.md index e6e9ac42561d2..65d37e6dace50 100644 --- a/.github/RELEASE_TEMPLATE.md +++ b/.github/RELEASE_TEMPLATE.md @@ -1,6 +1,6 @@ ### Notice -**Pre-releases are experimental versions that often contain unpatched bugs, work-in-progress features and rewritten implementations. If you don't consider yourself advanced user, please download **[latest stable release](https://github.com/JustArchiNET/ArchiSteamFarm/releases/latest)** instead. Pre-release versions are dedicated to users who know how to report bugs, deal with issues and give feedback - no technical support will be given. Check out ASF **[release cycle](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Release-cycle)** if you'd like to learn more.** +**Pre-releases are test versions that often contain unpatched bugs, work-in-progress features and rewritten implementations. If you don't consider yourself advanced user, please download **[latest stable release](https://github.com/JustArchiNET/ArchiSteamFarm/releases/latest)** instead. Pre-release versions are dedicated to users who know how to report bugs, deal with issues and give feedback - no technical support will be given. Check out ASF **[release cycle](https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Release-cycle)** if you'd like to learn more.** --- diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index a2c4c98afe942..af001970b02e1 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -34,6 +34,6 @@ jobs: QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} - name: Report Qodana results to GitHub - uses: github/codeql-action/upload-sarif@v3.26.9 + uses: github/codeql-action/upload-sarif@v3.26.10 with: sarif_file: ${{ runner.temp }}/qodana/results/qodana.sarif.json diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml index 65cbe68c85334..a53ea7c42d4a7 100644 --- a/.github/workflows/docker-ci.yml +++ b/.github/workflows/docker-ci.yml @@ -28,7 +28,7 @@ jobs: uses: docker/setup-buildx-action@v3.6.1 - name: Build ${{ matrix.configuration }} Docker image from ${{ matrix.file }} - uses: docker/build-push-action@v6.7.0 + uses: docker/build-push-action@v6.8.0 with: build-args: CONFIGURATION=${{ matrix.configuration }} context: . diff --git a/.github/workflows/docker-publish-latest.yml b/.github/workflows/docker-publish-latest.yml index f884aaa679b58..6940b802dea7e 100644 --- a/.github/workflows/docker-publish-latest.yml +++ b/.github/workflows/docker-publish-latest.yml @@ -50,7 +50,7 @@ jobs: echo "DH_REPOSITORY=$(echo ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" - name: Build and publish Docker image from Dockerfile.Service - uses: docker/build-push-action@v6.7.0 + uses: docker/build-push-action@v6.8.0 with: context: . file: Dockerfile.Service diff --git a/.github/workflows/docker-publish-main.yml b/.github/workflows/docker-publish-main.yml index 9cabed31c896c..2cb2aa045bcc8 100644 --- a/.github/workflows/docker-publish-main.yml +++ b/.github/workflows/docker-publish-main.yml @@ -50,7 +50,7 @@ jobs: echo "DH_REPOSITORY=$(echo ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" - name: Build and publish Docker image from Dockerfile - uses: docker/build-push-action@v6.7.0 + uses: docker/build-push-action@v6.8.0 with: context: . platforms: ${{ env.PLATFORMS }} diff --git a/.github/workflows/docker-publish-released.yml b/.github/workflows/docker-publish-released.yml index dd424c06dd817..331f05d82c29a 100644 --- a/.github/workflows/docker-publish-released.yml +++ b/.github/workflows/docker-publish-released.yml @@ -51,7 +51,7 @@ jobs: echo "DH_REPOSITORY=$(echo ${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" - name: Build and publish Docker image from Dockerfile - uses: docker/build-push-action@v6.7.0 + uses: docker/build-push-action@v6.8.0 with: context: . platforms: ${{ env.PLATFORMS }} diff --git a/ASF-ui b/ASF-ui index 9f5672d65a1bd..fa46e12afad52 160000 --- a/ASF-ui +++ b/ASF-ui @@ -1 +1 @@ -Subproject commit 9f5672d65a1bd3b0f5d16ea6a1b5d220d670223c +Subproject commit fa46e12afad522ea60014c995b40f0a0b656cde7 diff --git a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs index b7389d784ebef..8c5681958d4e0 100644 --- a/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs +++ b/ArchiSteamFarm.OfficialPlugins.ItemsMatcher/RemoteCommunication.cs @@ -1255,7 +1255,7 @@ private async Task MatchActively(ImmutableHashSet listedUsers, Dictionary fairClassIDsToGive = new(); Dictionary fairClassIDsToReceive = new(); - foreach (ListedUser listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderByDescending(listedUser => !deprioritizedSteamIDs.Contains(listedUser.SteamID)).ThenByDescending(static listedUser => listedUser.TotalGamesCount > 1).ThenByDescending(static listedUser => listedUser.MatchEverything).ThenBy(static listedUser => listedUser.TotalInventoryCount)) { + foreach (ListedUser listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && acceptedMatchableTypes.Overlaps(listedUser.MatchableTypes) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderByDescending(listedUser => !deprioritizedSteamIDs.Contains(listedUser.SteamID)).ThenByDescending(static listedUser => listedUser.TotalGamesCount > 1).ThenByDescending(static listedUser => listedUser.MatchEverything).ThenBy(static listedUser => listedUser.TotalInventoryCount)) { if (failuresInRow >= WebBrowser.MaxTries) { Bot.ArchiLogger.LogGenericWarning(Strings.FormatWarningFailedWithError($"{nameof(failuresInRow)} >= {WebBrowser.MaxTries}")); diff --git a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Localization/Strings.cs-CZ.resx b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Localization/Strings.cs-CZ.resx index 6c4314502d49b..9a2aa3924e729 100644 --- a/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Localization/Strings.cs-CZ.resx +++ b/ArchiSteamFarm.OfficialPlugins.SteamTokenDumper/Localization/Strings.cs-CZ.resx @@ -68,7 +68,10 @@ {0} je v současné době v souladu s vaší konfigurací zakázán. Pokud byste chtěli pomoci SteamDB při odesílání dat, podívejte se na naši wiki. {0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin") - + + {0} byl úspěšně inicializován, předem děkujeme za vaši pomoc. První zpráva bude vygenerována zhruba za {1}. + {0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin"), {1} will be replaced by translated TimeSpan string (such as "53 minutes") + {0} nelze načíst, nová instance bude inicializována... {0} will be replaced by the name of the file (e.g. "GlobalCache") diff --git a/ArchiSteamFarm/Helpers/ArchiCacheable.cs b/ArchiSteamFarm/Helpers/ArchiCacheable.cs index 8ad0641eac88e..e64c193aac2eb 100644 --- a/ArchiSteamFarm/Helpers/ArchiCacheable.cs +++ b/ArchiSteamFarm/Helpers/ArchiCacheable.cs @@ -87,6 +87,10 @@ public ArchiCacheable(Func> r } catch (OperationCanceledException e) { ASF.ArchiLogger.LogGenericDebuggingException(e); + return GetFailedValueFor(cacheFallback); + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + return GetFailedValueFor(cacheFallback); } finally { InitSemaphore.Release(); diff --git a/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs b/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs index ab78e4edae78e..8973a6b5de9cb 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs @@ -74,6 +74,34 @@ public async Task> AddLicensePost(string botNames, return Ok(new GenericResponse>(result)); } + /// + /// Redeems points on given bots. + /// + [Consumes("application/json")] + [HttpPost("{botNames:required}/RedeemPoints/{definitionID:required}")] + [ProducesResponseType>>((int) HttpStatusCode.OK)] + [ProducesResponseType((int) HttpStatusCode.BadRequest)] + public async Task> AddLicensePost(string botNames, uint definitionID) { + ArgumentException.ThrowIfNullOrEmpty(botNames); + ArgumentOutOfRangeException.ThrowIfZero(definitionID); + + HashSet? bots = Bot.GetBots(botNames); + + if ((bots == null) || (bots.Count == 0)) { + return BadRequest(new GenericResponse(false, Strings.FormatBotNotFound(botNames))); + } + + IList results = await Utilities.InParallel(bots.Select(bot => bot.Actions.RedeemPoints(definitionID))).ConfigureAwait(false); + + Dictionary result = new(bots.Count, Bot.BotsComparer); + + foreach (Bot bot in bots) { + result[bot.BotName] = results[result.Count]; + } + + return Ok(new GenericResponse>(result)); + } + /// /// Deletes all files related to given bots. /// diff --git a/ArchiSteamFarm/Steam/Bot.cs b/ArchiSteamFarm/Steam/Bot.cs index 86bdd955a0ff7..8e762bab24fd8 100644 --- a/ArchiSteamFarm/Steam/Bot.cs +++ b/ArchiSteamFarm/Steam/Bot.cs @@ -3562,14 +3562,7 @@ private async void RedeemGamesInBackground(object? state = null) { (string? key, string? name) = BotDatabase.GetGameToRedeemInBackground(); if (string.IsNullOrEmpty(key)) { - ArchiLogger.LogNullError(key); - - break; - } - - if (string.IsNullOrEmpty(name)) { - ArchiLogger.LogNullError(name); - + // No more games to redeem left, possible due to e.g. queue purge break; } @@ -3637,6 +3630,8 @@ private async void RedeemGamesInBackground(object? state = null) { BotDatabase.RemoveGameToRedeemInBackground(key); // If user omitted the name or intentionally provided the same name as key, replace it with the Steam result + name ??= key; + if (name.Equals(key, StringComparison.OrdinalIgnoreCase) && (items?.Count > 0)) { name = string.Join(", ", items.Values); } diff --git a/ArchiSteamFarm/Steam/Exchange/Trading.cs b/ArchiSteamFarm/Steam/Exchange/Trading.cs index 900c8e6d7d2bd..8d5431d241a03 100644 --- a/ArchiSteamFarm/Steam/Exchange/Trading.cs +++ b/ArchiSteamFarm/Steam/Exchange/Trading.cs @@ -556,7 +556,7 @@ private async Task ParseTrade(TradeOffer tradeOffer, ISet<(uin if (accept) { // Ensure that accepting this trade offer does not create conflicts with other lock (handledSets) { - if (wantedSets.Any(handledSets.Contains)) { + if (handledSets.Overlaps(wantedSets)) { Bot.ArchiLogger.LogGenericDebug(Strings.FormatBotTradeOfferResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.RetryAfterOthers, nameof(handledSets))); return ParseTradeResult.EResult.RetryAfterOthers; diff --git a/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs b/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs index 90160b51c9a28..f4787fba8cb04 100644 --- a/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs +++ b/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs @@ -61,6 +61,7 @@ public sealed class ArchiHandler : ClientMsgHandler { private readonly SteamUnifiedMessages.UnifiedService UnifiedEconService; private readonly SteamUnifiedMessages.UnifiedService UnifiedFamilyGroups; private readonly SteamUnifiedMessages.UnifiedService UnifiedFriendMessagesService; + private readonly SteamUnifiedMessages.UnifiedService UnifiedLoyaltyRewards; private readonly SteamUnifiedMessages.UnifiedService UnifiedPlayerService; private readonly SteamUnifiedMessages.UnifiedService UnifiedStoreService; private readonly SteamUnifiedMessages.UnifiedService UnifiedTwoFactorService; @@ -80,6 +81,7 @@ internal ArchiHandler(ArchiLogger archiLogger, SteamUnifiedMessages steamUnified UnifiedEconService = steamUnifiedMessages.CreateService(); UnifiedFamilyGroups = steamUnifiedMessages.CreateService(); UnifiedFriendMessagesService = steamUnifiedMessages.CreateService(); + UnifiedLoyaltyRewards = steamUnifiedMessages.CreateService(); UnifiedPlayerService = steamUnifiedMessages.CreateService(); UnifiedStoreService = steamUnifiedMessages.CreateService(); UnifiedTwoFactorService = steamUnifiedMessages.CreateService(); @@ -357,6 +359,47 @@ public async IAsyncEnumerable GetMyInventoryAsync(uint appID = Asset.Stea return body.games.ToDictionary(static game => (uint) game.appid, static game => game.name); } + [PublicAPI] + public async Task GetPointsBalance() { + if (Client == null) { + throw new InvalidOperationException(nameof(Client)); + } + + if (!Client.IsConnected) { + return null; + } + + if (Client.SteamID == null) { + throw new InvalidOperationException(nameof(Client.SteamID)); + } + + ulong steamID = Client.SteamID; + + if (steamID == 0) { + throw new InvalidOperationException(nameof(Client.SteamID)); + } + + CLoyaltyRewards_GetSummary_Request request = new() { steamid = steamID }; + + SteamUnifiedMessages.ServiceMethodResponse response; + + try { + response = await UnifiedLoyaltyRewards.SendMessage(x => x.GetSummary(request)).ToLongRunningTask().ConfigureAwait(false); + } catch (Exception e) { + ArchiLogger.LogGenericWarningException(e); + + return null; + } + + if (response.Result != EResult.OK) { + return null; + } + + CLoyaltyRewards_GetSummary_Response body = response.GetDeserializedResponse(); + + return body.summary?.points; + } + [PublicAPI] public async Task GetSteamGuardStatus() { if (Client == null) { @@ -500,6 +543,35 @@ public async Task LeaveChatRoomGroup(ulong chatGroupID) { return response.Result == EResult.OK; } + [PublicAPI] + public async Task RedeemPoints(uint definitionID) { + ArgumentOutOfRangeException.ThrowIfZero(definitionID); + + if (Client == null) { + throw new InvalidOperationException(nameof(Client)); + } + + if (!Client.IsConnected) { + return EResult.NoConnection; + } + + CLoyaltyRewards_RedeemPoints_Request request = new() { + defid = definitionID + }; + + SteamUnifiedMessages.ServiceMethodResponse response; + + try { + response = await UnifiedLoyaltyRewards.SendMessage(x => x.RedeemPoints(request)).ToLongRunningTask().ConfigureAwait(false); + } catch (Exception e) { + ArchiLogger.LogGenericWarningException(e); + + return EResult.Timeout; + } + + return response.Result; + } + [PublicAPI] public async Task RemoveFriend(ulong steamID) { if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) { diff --git a/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs index 229dfeb581e53..f94aae319319d 100644 --- a/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs +++ b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs @@ -55,7 +55,10 @@ public sealed class ArchiWebHandler : IDisposable { internal const ushort MaxItemsInSingleInventoryRequest = 5000; private const string EconService = "IEconService"; + + [Obsolete] private const string LoyaltyRewardsService = "ILoyaltyRewardsService"; + private const byte MaxTradeOfferMessageLength = 128; private const byte MinimumSessionValidityInSeconds = 10; private const byte SessionIDLength = 24; // For maximum compatibility, should be divisible by 2 and match the length of "sessionid" property that Steam uses across their websites @@ -394,6 +397,7 @@ public async IAsyncEnumerable GetInventoryAsync(ulong steamID = 0, uint a } } + [Obsolete($"Use {nameof(ArchiHandler)}.{nameof(ArchiHandler.GetPointsBalance)} instead, this endpoint will be removed in the future version")] [PublicAPI] public async Task GetPointsBalance() { string? accessToken = Bot.AccessToken; diff --git a/ArchiSteamFarm/Steam/Interaction/Actions.cs b/ArchiSteamFarm/Steam/Interaction/Actions.cs index 12526ee487a7a..31e0fcf3b9b21 100644 --- a/ArchiSteamFarm/Steam/Interaction/Actions.cs +++ b/ArchiSteamFarm/Steam/Interaction/Actions.cs @@ -313,6 +313,13 @@ public static string Hash(ArchiCryptoHelper.EHashingMethod hashingMethod, string return await Bot.ArchiHandler.RedeemKey(key).ConfigureAwait(false); } + [PublicAPI] + public async Task RedeemPoints(uint definitionID) { + ArgumentOutOfRangeException.ThrowIfZero(definitionID); + + return await Bot.ArchiHandler.RedeemPoints(definitionID).ConfigureAwait(false); + } + [PublicAPI] public static (bool Success, string Message) Restart() { if (!Program.RestartAllowed) { diff --git a/ArchiSteamFarm/Steam/Interaction/Commands.cs b/ArchiSteamFarm/Steam/Interaction/Commands.cs index 37c14b7ac4e8f..f69d02bd235f5 100644 --- a/ArchiSteamFarm/Steam/Interaction/Commands.cs +++ b/ArchiSteamFarm/Steam/Interaction/Commands.cs @@ -127,6 +127,8 @@ public static EAccess GetProxyAccess(Bot bot, EAccess access, ulong steamID = 0) return ResponseWalletBalance(access); case "BGR": return ResponseBackgroundGamesRedeemer(access); + case "BGRCLEAR": + return ResponseBackgroundGamesRedeemerClear(access); case "EXIT": return ResponseExit(access); case "FARM": @@ -198,6 +200,8 @@ public static EAccess GetProxyAccess(Bot bot, EAccess access, ulong steamID = 0) return await ResponseWalletBalance(access, Utilities.GetArgsAsText(args, 1, ","), steamID).ConfigureAwait(false); case "BGR": return await ResponseBackgroundGamesRedeemer(access, Utilities.GetArgsAsText(args, 1, ","), steamID).ConfigureAwait(false); + case "BGRCLEAR": + return await ResponseBackgroundGamesRedeemerClear(access, Utilities.GetArgsAsText(args, 1, ","), steamID).ConfigureAwait(false); case "ENCRYPT" when args.Length > 2: return ResponseEncrypt(access, args[1], Utilities.GetArgsAsText(message, 2)); case "FARM": @@ -294,6 +298,10 @@ public static EAccess GetProxyAccess(Bot bot, EAccess access, ulong steamID = 0) return await ResponseAdvancedRedeem(access, args[1], args[2], Utilities.GetArgsAsText(args, 3, ","), steamID).ConfigureAwait(false); case "R^" or "REDEEM^" when args.Length > 2: return await ResponseAdvancedRedeem(access, args[1], args[2], steamID).ConfigureAwait(false); + case "RP" or "REDEEMPOINTS" when args.Length > 2: + return await ResponseRedeemPoints(access, args[1], Utilities.GetArgsAsText(args, 2, ","), steamID).ConfigureAwait(false); + case "RP" or "REDEEMPOINTS": + return await ResponseRedeemPoints(access, args[1]).ConfigureAwait(false); case "RESET": return await ResponseReset(access, Utilities.GetArgsAsText(args, 1, ","), steamID).ConfigureAwait(false); case "RESUME": @@ -1148,6 +1156,40 @@ internal void OnNewLicenseList() { return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null; } + private string? ResponseBackgroundGamesRedeemerClear(EAccess access) { + if (!Enum.IsDefined(access)) { + throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); + } + + if (access < EAccess.Master) { + return null; + } + + Bot.BotDatabase.ClearGamesToRedeemInBackground(); + + return FormatBotResponse(Strings.Done); + } + + private static async Task ResponseBackgroundGamesRedeemerClear(EAccess access, string botNames, ulong steamID = 0) { + if (!Enum.IsDefined(access)) { + throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); + } + + ArgumentException.ThrowIfNullOrEmpty(botNames); + + HashSet? bots = Bot.GetBots(botNames); + + if ((bots == null) || (bots.Count == 0)) { + return access >= EAccess.Owner ? FormatStaticResponse(Strings.FormatBotNotFound(botNames)) : null; + } + + IList results = await Utilities.InParallel(bots.Select(bot => Task.Run(() => bot.Commands.ResponseBackgroundGamesRedeemerClear(GetProxyAccess(bot, access, steamID))))).ConfigureAwait(false); + + List responses = [..results.Where(static result => !string.IsNullOrEmpty(result))!]; + + return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null; + } + private static string? ResponseEncrypt(EAccess access, string cryptoMethodText, string stringToEncrypt) { if (!Enum.IsDefined(access)) { throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); @@ -2267,7 +2309,7 @@ internal void OnNewLicenseList() { return FormatBotResponse(Strings.BotNotConnected); } - uint? points = await Bot.ArchiWebHandler.GetPointsBalance().ConfigureAwait(false); + long? points = await Bot.ArchiHandler.GetPointsBalance().ConfigureAwait(false); return FormatBotResponse(points.HasValue ? Strings.FormatBotPointsBalance(points) : Strings.WarningFailed); } @@ -2725,6 +2767,91 @@ internal void OnNewLicenseList() { return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null; } + private async Task ResponseRedeemPoints(EAccess access, HashSet definitionIDs) { + if (!Enum.IsDefined(access)) { + throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); + } + + if ((definitionIDs == null) || (definitionIDs.Count == 0)) { + throw new ArgumentNullException(nameof(definitionIDs)); + } + + if (access < EAccess.Operator) { + return null; + } + + if (!Bot.IsConnectedAndLoggedOn) { + return FormatBotResponse(Strings.BotNotConnected); + } + + IList results = await Utilities.InParallel(definitionIDs.Select(Bot.Actions.RedeemPoints)).ConfigureAwait(false); + + int i = 0; + + StringBuilder response = new(); + + foreach (uint definitionID in definitionIDs) { + response.AppendLine(FormatBotResponse(Strings.FormatBotAddLicense(definitionID, results[i++]))); + } + + return response.Length > 0 ? response.ToString() : null; + } + + private async Task ResponseRedeemPoints(EAccess access, string targetDefinitionIDs) { + if (!Enum.IsDefined(access)) { + throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); + } + + ArgumentException.ThrowIfNullOrEmpty(targetDefinitionIDs); + + if (access < EAccess.Operator) { + return null; + } + + if (!Bot.IsConnectedAndLoggedOn) { + return FormatBotResponse(Strings.BotNotConnected); + } + + string[] definitions = targetDefinitionIDs.Split(SharedInfo.ListElementSeparators, StringSplitOptions.RemoveEmptyEntries); + + if (definitions.Length == 0) { + return FormatBotResponse(Strings.FormatErrorIsEmpty(nameof(definitions))); + } + + HashSet definitionIDs = new(definitions.Length); + + foreach (string definition in definitions) { + if (!uint.TryParse(definition, out uint definitionID) || (definitionID == 0)) { + return FormatBotResponse(Strings.FormatErrorIsInvalid(nameof(definition))); + } + + definitionIDs.Add(definitionID); + } + + return await ResponseRedeemPoints(access, definitionIDs).ConfigureAwait(false); + } + + private static async Task ResponseRedeemPoints(EAccess access, string botNames, string targetDefinitionIDs, ulong steamID = 0) { + if (!Enum.IsDefined(access)) { + throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); + } + + ArgumentException.ThrowIfNullOrEmpty(botNames); + ArgumentException.ThrowIfNullOrEmpty(targetDefinitionIDs); + + HashSet? bots = Bot.GetBots(botNames); + + if ((bots == null) || (bots.Count == 0)) { + return access >= EAccess.Owner ? FormatStaticResponse(Strings.FormatBotNotFound(botNames)) : null; + } + + IList results = await Utilities.InParallel(bots.Select(bot => bot.Commands.ResponseRedeemPoints(GetProxyAccess(bot, access, steamID), targetDefinitionIDs))).ConfigureAwait(false); + + List responses = [..results.Where(static result => !string.IsNullOrEmpty(result))!]; + + return responses.Count > 0 ? string.Join(Environment.NewLine, responses) : null; + } + private async Task ResponseReset(EAccess access) { if (!Enum.IsDefined(access)) { throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); diff --git a/ArchiSteamFarm/Steam/Storage/BotDatabase.cs b/ArchiSteamFarm/Steam/Storage/BotDatabase.cs index 38559dc3de4a5..102ac265d2a96 100644 --- a/ArchiSteamFarm/Steam/Storage/BotDatabase.cs +++ b/ArchiSteamFarm/Steam/Storage/BotDatabase.cs @@ -256,6 +256,18 @@ internal void AddGamesToRedeemInBackground(IOrderedDictionary games) { Utilities.InBackground(Save); } + internal void ClearGamesToRedeemInBackground() { + lock (GamesToRedeemInBackground) { + if (GamesToRedeemInBackground.Count == 0) { + return; + } + + GamesToRedeemInBackground.Clear(); + } + + Utilities.InBackground(Save); + } + internal static async Task CreateOrLoad(string filePath) { ArgumentException.ThrowIfNullOrEmpty(filePath); diff --git a/ArchiSteamFarm/Storage/GlobalConfig.cs b/ArchiSteamFarm/Storage/GlobalConfig.cs index c38795d8c46d5..5c64ca98c05ff 100644 --- a/ArchiSteamFarm/Storage/GlobalConfig.cs +++ b/ArchiSteamFarm/Storage/GlobalConfig.cs @@ -619,6 +619,6 @@ public enum EPluginsUpdateMode : byte { public enum EUpdateChannel : byte { None, Stable, - Experimental + PreRelease } } diff --git a/Directory.Build.props b/Directory.Build.props index ef5643117e716..f2ec8dca186a9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 6.0.7.5 + 6.0.8.1 diff --git a/Directory.Packages.props b/Directory.Packages.props index c02dcd69dfc70..8d50dd0019029 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,8 +16,8 @@ - - + + diff --git a/README.md b/README.md index 25080bf4a4a98..78ae97a74c99c 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ [![GitHub stable release date](https://img.shields.io/github/release-date/JustArchiNET/ArchiSteamFarm.svg?label=Released&logo=github&cacheSeconds=600)](https://github.com/JustArchiNET/ArchiSteamFarm/releases/latest) [![Github stable release downloads](https://img.shields.io/github/downloads/JustArchiNET/ArchiSteamFarm/latest/total.svg?label=Downloads&logo=github&cacheSeconds=600)](https://github.com/JustArchiNET/ArchiSteamFarm/releases/latest) -[![GitHub experimental release version](https://img.shields.io/github/v/release/JustArchiNET/ArchiSteamFarm?include_prereleases&label=Experimental&logo=github&cacheSeconds=600)](https://github.com/JustArchiNET/ArchiSteamFarm/releases) -[![GitHub experimental release date](https://img.shields.io/github/release-date-pre/JustArchiNET/ArchiSteamFarm.svg?label=Released&logo=github&cacheSeconds=600)](https://github.com/JustArchiNET/ArchiSteamFarm/releases) -[![Github experimental release downloads](https://img.shields.io/github/downloads-pre/JustArchiNET/ArchiSteamFarm/latest/total.svg?label=Downloads&logo=github&cacheSeconds=600)](https://github.com/JustArchiNET/ArchiSteamFarm/releases) +[![GitHub pre-release version](https://img.shields.io/github/v/release/JustArchiNET/ArchiSteamFarm?include_prereleases&label=Pre-release&logo=github&cacheSeconds=600)](https://github.com/JustArchiNET/ArchiSteamFarm/releases) +[![GitHub pre-release date](https://img.shields.io/github/release-date-pre/JustArchiNET/ArchiSteamFarm.svg?label=Released&logo=github&cacheSeconds=600)](https://github.com/JustArchiNET/ArchiSteamFarm/releases) +[![Github pre-release downloads](https://img.shields.io/github/downloads-pre/JustArchiNET/ArchiSteamFarm/latest/total.svg?label=Downloads&logo=github&cacheSeconds=600)](https://github.com/JustArchiNET/ArchiSteamFarm/releases) [![GitHub sponsor](https://img.shields.io/badge/GitHub-sponsor-ea4aaa.svg?logo=github-sponsors)](https://github.com/sponsors/JustArchi) [![PayPal.me donate](https://img.shields.io/badge/PayPal.me-donate-00457c.svg?logo=paypal)](https://paypal.me/JustArchi) diff --git a/wiki b/wiki index 7d6723153936d..66d27099d8889 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 7d6723153936ded85980765045984b08dd9bb1a8 +Subproject commit 66d27099d888992dab65b7eae01f04813cb667af