mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2025-12-28 20:20:48 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4798b29bff | ||
|
|
df4f8d1e62 | ||
|
|
00f7d2bfb9 | ||
|
|
5ab1971018 | ||
|
|
841b80c297 | ||
|
|
10f2471332 | ||
|
|
fccb455676 | ||
|
|
c9bc71462e | ||
|
|
e0f9fe3555 | ||
|
|
8bb48d829e | ||
|
|
4c166102c5 | ||
|
|
a91a4aada6 | ||
|
|
e05139ea7f | ||
|
|
6119d33ab8 | ||
|
|
5905412d82 | ||
|
|
7596a89baa | ||
|
|
7e7fd3bab4 |
20
.github/workflows/publish.yml
vendored
20
.github/workflows/publish.yml
vendored
@@ -542,6 +542,16 @@ jobs:
|
||||
body_path: .github/RELEASE_TEMPLATE.md
|
||||
prerelease: true
|
||||
|
||||
- name: Upload SHA512SUMS to GitHub release
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.github_release.outputs.upload_url }}
|
||||
asset_path: out/SHA512SUMS
|
||||
asset_name: SHA512SUMS
|
||||
asset_content_type: text/plain
|
||||
|
||||
- name: Upload ASF-generic to GitHub release
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
@@ -622,16 +632,6 @@ jobs:
|
||||
asset_name: ASF-win-x64.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Upload SHA512SUMS to GitHub release
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.github_release.outputs.upload_url }}
|
||||
asset_path: out/SHA512SUMS
|
||||
asset_name: SHA512SUMS
|
||||
asset_content_type: text/plain
|
||||
|
||||
- name: Upload SHA512SUMS.sign to GitHub release
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
|
||||
2
ASF-ui
2
ASF-ui
Submodule ASF-ui updated: 80e6bd5fac...94ce7b116c
@@ -78,4 +78,8 @@
|
||||
<value>Неуспешно изпращане на трейд оферта до бот {0} ({1}), продължаваме...</value>
|
||||
<comment>{0} will be replaced by steam ID (number), {1} will be replaced by user's nickname'</comment>
|
||||
</data>
|
||||
<data name="ActivelyMatchingSomeConfirmationsFailed" xml:space="preserve">
|
||||
<value>Някои потвърждения са неуспешни, приблизително {0} от {1} от размените бяха изпратени успешно.</value>
|
||||
<comment>{0} will be replaced by amount of the trade offers that succeeded (number), {1} will be replaced by amount of the trade offers that were supposed to be sent in total (number)</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -41,10 +41,13 @@ using ArchiSteamFarm.Steam.Storage;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using ArchiSteamFarm.Web;
|
||||
using ArchiSteamFarm.Web.Responses;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SteamKit2;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
|
||||
|
||||
internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
private const string MatchActivelyTradeOfferIDsStorageKey = $"{nameof(ItemsMatcher)}-{nameof(MatchActively)}-TradeOfferIDs";
|
||||
private const byte MaxAnnouncementTTL = 60; // Maximum amount of minutes we can wait if the next announcement doesn't happen naturally
|
||||
private const byte MaxTradeOffersActive = 10; // The actual upper limit is 30, but we should use lower amount to allow some bots to react before we hit the maximum allowed
|
||||
private const byte MinAnnouncementTTL = 5; // Minimum amount of minutes we must wait before the next Announcement
|
||||
@@ -320,7 +323,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
SignedInWithSteam = true;
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Localization.Strings.ListingAnnouncing, Bot.SteamID, nickname, assetsForListing.Count));
|
||||
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Localization.Strings.ListingAnnouncing, Bot.SteamID, nickname ?? Bot.SteamID.ToString(CultureInfo.InvariantCulture), assetsForListing.Count));
|
||||
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
BasicResponse? response = await Backend.AnnounceForListing(Bot.SteamID, WebBrowser, assetsForListing, acceptedMatchableTypes, (uint) inventory.Count, matchEverything, tradeToken!, nickname, avatarHash).ConfigureAwait(false);
|
||||
@@ -657,6 +660,50 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cancel previous trade offers sent and deprioritize SteamIDs that didn't answer us in this round
|
||||
HashSet<ulong>? matchActivelyTradeOfferIDs = null;
|
||||
|
||||
JToken? matchActivelyTradeOfferIDsToken = Bot.BotDatabase.LoadFromJsonStorage(MatchActivelyTradeOfferIDsStorageKey);
|
||||
|
||||
if (matchActivelyTradeOfferIDsToken != null) {
|
||||
try {
|
||||
matchActivelyTradeOfferIDs = matchActivelyTradeOfferIDsToken.ToObject<HashSet<ulong>>();
|
||||
} catch (Exception e) {
|
||||
Bot.ArchiLogger.LogGenericWarningException(e);
|
||||
}
|
||||
}
|
||||
|
||||
matchActivelyTradeOfferIDs ??= new HashSet<ulong>();
|
||||
|
||||
HashSet<ulong> deprioritizedSteamIDs = new();
|
||||
|
||||
if (matchActivelyTradeOfferIDs.Count > 0) {
|
||||
// This is not a mandatory step, we allow it to fail
|
||||
HashSet<TradeOffer>? sentTradeOffers = await Bot.ArchiWebHandler.GetTradeOffers(true, false, true, false).ConfigureAwait(false);
|
||||
|
||||
if (sentTradeOffers != null) {
|
||||
HashSet<ulong> activeTradeOfferIDs = new();
|
||||
|
||||
foreach (TradeOffer tradeOffer in sentTradeOffers.Where(tradeOffer => (tradeOffer.State == ETradeOfferState.Active) && matchActivelyTradeOfferIDs.Contains(tradeOffer.TradeOfferID))) {
|
||||
deprioritizedSteamIDs.Add(tradeOffer.OtherSteamID64);
|
||||
|
||||
if (!await Bot.ArchiWebHandler.CancelTradeOffer(tradeOffer.TradeOfferID).ConfigureAwait(false)) {
|
||||
activeTradeOfferIDs.Add(tradeOffer.TradeOfferID);
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchActivelyTradeOfferIDs.SetEquals(activeTradeOfferIDs)) {
|
||||
matchActivelyTradeOfferIDs = activeTradeOfferIDs;
|
||||
|
||||
if (matchActivelyTradeOfferIDs.Count > 0) {
|
||||
Bot.BotDatabase.SaveToJsonStorage(MatchActivelyTradeOfferIDsStorageKey, JToken.FromObject(matchActivelyTradeOfferIDs));
|
||||
} else {
|
||||
Bot.BotDatabase.DeleteFromJsonStorage(MatchActivelyTradeOfferIDsStorageKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HashSet<ulong> pendingMobileTradeOfferIDs = new();
|
||||
|
||||
byte maxTradeHoldDuration = ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration;
|
||||
@@ -664,7 +711,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
byte failuresInRow = 0;
|
||||
uint matchedSets = 0;
|
||||
|
||||
foreach (ListedUser listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderByDescending(static listedUser => listedUser.MatchEverything).ThenBy(static listedUser => listedUser.TotalInventoryCount)) {
|
||||
foreach (ListedUser listedUser in listedUsers.Where(listedUser => (listedUser.SteamID != Bot.SteamID) && acceptedMatchableTypes.Any(listedUser.MatchableTypes.Contains) && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderBy(listedUser => deprioritizedSteamIDs.Contains(listedUser.SteamID)).ThenByDescending(static listedUser => listedUser.MatchEverything).ThenBy(static listedUser => listedUser.TotalInventoryCount)) {
|
||||
if (failuresInRow >= WebBrowser.MaxTries) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, $"{nameof(failuresInRow)} >= {WebBrowser.MaxTries}"));
|
||||
|
||||
@@ -840,7 +887,13 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
||||
|
||||
Bot.ArchiLogger.LogGenericTrace($"{Bot.SteamID} <- {string.Join(", ", itemsToReceive.Select(static item => $"{item.RealAppID}/{item.Type}/{item.Rarity}/{item.ClassID} #{item.Amount}"))} | {string.Join(", ", itemsToGive.Select(static item => $"{item.RealAppID}/{item.Type}/{item.Rarity}/{item.ClassID} #{item.Amount}"))} -> {listedUser.SteamID}");
|
||||
|
||||
(bool success, HashSet<ulong>? mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(listedUser.SteamID, itemsToGive, itemsToReceive, listedUser.TradeToken, true).ConfigureAwait(false);
|
||||
(bool success, HashSet<ulong>? tradeOfferIDs, HashSet<ulong>? mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(listedUser.SteamID, itemsToGive, itemsToReceive, listedUser.TradeToken, true).ConfigureAwait(false);
|
||||
|
||||
if (tradeOfferIDs?.Count > 0) {
|
||||
matchActivelyTradeOfferIDs.UnionWith(tradeOfferIDs);
|
||||
|
||||
Bot.BotDatabase.SaveToJsonStorage(MatchActivelyTradeOfferIDsStorageKey, JToken.FromObject(matchActivelyTradeOfferIDs));
|
||||
}
|
||||
|
||||
if (mobileTradeOfferIDs?.Count > 0) {
|
||||
pendingMobileTradeOfferIDs.UnionWith(mobileTradeOfferIDs);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -29,6 +30,7 @@ using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Helpers;
|
||||
using ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Localization;
|
||||
using ArchiSteamFarm.Web.Responses;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
using SteamKit2;
|
||||
@@ -36,6 +38,8 @@ using SteamKit2;
|
||||
namespace ArchiSteamFarm.OfficialPlugins.SteamTokenDumper;
|
||||
|
||||
internal sealed class GlobalCache : SerializableFile {
|
||||
internal static readonly ArchiCacheable<ImmutableHashSet<uint>> KnownDepotIDs = new(ResolveKnownDepotIDs, TimeSpan.FromDays(7));
|
||||
|
||||
private static string SharedFilePath => Path.Combine(ArchiSteamFarm.SharedInfo.ConfigDirectory, $"{nameof(SteamTokenDumper)}.cache");
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
@@ -326,4 +330,50 @@ internal sealed class GlobalCache : SerializableFile {
|
||||
|
||||
return (depotKey.Length == 64) && Utilities.IsValidHexadecimalText(depotKey);
|
||||
}
|
||||
|
||||
private static async Task<(bool Success, ImmutableHashSet<uint>? Result)> ResolveKnownDepotIDs() {
|
||||
if (ASF.WebBrowser == null) {
|
||||
throw new InvalidOperationException(nameof(ASF.WebBrowser));
|
||||
}
|
||||
|
||||
Uri request = new($"{SharedInfo.ServerURL}/knowndepots.csv");
|
||||
|
||||
StreamResponse? response = await ASF.WebBrowser.UrlGetToStream(request).ConfigureAwait(false);
|
||||
|
||||
if (response?.Content == null) {
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
await using (response.ConfigureAwait(false)) {
|
||||
try {
|
||||
using StreamReader reader = new(response.Content);
|
||||
|
||||
string? countText = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrEmpty(countText) || !int.TryParse(countText, out int count) || (count <= 0)) {
|
||||
ASF.ArchiLogger.LogNullError(nameof(countText));
|
||||
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
HashSet<uint> result = new(count);
|
||||
|
||||
while (await reader.ReadLineAsync().ConfigureAwait(false) is { Length: > 0 } line) {
|
||||
if (!uint.TryParse(line, out uint depotID) || (depotID == 0)) {
|
||||
ASF.ArchiLogger.LogNullError(nameof(depotID));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(depotID);
|
||||
}
|
||||
|
||||
return (result.Count > 0, result.ToImmutableHashSet());
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericWarningException(e);
|
||||
|
||||
return (false, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,14 +109,7 @@
|
||||
<value>Завершана атрыманне інфармацыі пра праграму {0}.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Атрыманне {0} ключоў сховішча...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Завершана атрыманне {0} ключоў сховішча.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Завершана атрыманне ўсіх ключоў ключоў сховішча для {0} праграм.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -109,14 +109,7 @@
|
||||
<value>Приключи събирането на {0} информация за играта или приложението.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Събиране {0} ключове за депо...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Приключи събирането {0} ключове за депо.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Приключи събирането на всички ключове за депа за общо {0} игри или проложения.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -109,14 +109,7 @@
|
||||
<value>Načítání informací o aplikaci {0} bylo dokončeno.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Získávání {0} tokenů úložišť...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Načítání {0} přístupových tokenů bylo dokončeno.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Načítání všech tokenbů úložišť, celkem z {0} aplikací bylo dokončeno.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</root>
|
||||
|
||||
@@ -109,14 +109,7 @@
|
||||
<value>Abruf von {0} App-Infos abgeschlossen.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Rufe {0} Depotschlüssel ab...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Abruf von {0} Depotschlüsseln abgeschlossen.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Abruf aller Depotschlüssel für insgesamt {0} Apps abgeschlossen.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -109,14 +109,7 @@
|
||||
<value>Ολοκληρώθηκε η ανάκτηση {0} πληροφοριών εφαρμογών.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Ανάκτηση {0} κλειδιών αποθήκης...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Ολοκληρώθηκε η ανάκτηση {0} κλειδιών αποθήκευσης.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Ολοκληρώθηκε η ανάκτηση όλων των κλειδιών αποθηκών για συνολικά {0} εφαρμογές.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -109,13 +109,9 @@
|
||||
<value>Se han recuperado {0} datos de aplicación.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Recuperando {0} claves de depósito...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Se han recuperado {0} claves de depósito.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
<value>Se recuperaron exitosamente {0} de {1} claves de depósito.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys that were successfully retrieved, {1} will be replaced by the number (count this batch) of depot keys that were supposed to be retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Se han recuperado todas las claves de depósito para un total de {0} aplicaciones.</value>
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</root>
|
||||
|
||||
@@ -109,14 +109,7 @@
|
||||
<value>Saatiin haettua {0} sovelluksen tiedot.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Haetaan {0} depot-avainta...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Saatiin haettua {0} depot-avainta.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Saatiin haettua kaikki depot-avaimet yhteensä {0} sovellukselle.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -109,14 +109,7 @@
|
||||
<value>Récupération de {0} infos d'application terminée.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Récupération de {0} clés de dépôts...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Récupération de {0} clés de dépôts terminée.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Fin de la récupération de toutes les clés de dépôts pour un total de {0} applications.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -86,14 +86,7 @@
|
||||
|
||||
|
||||
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>מאחזר {0} מפתחות מחסן...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>סיים לאחזר {0} מפתחות מחסן.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>סיים לאחזר את כל מפתחות המחסן עבור סך של {0} אפליקציות.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -108,7 +108,6 @@
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>Nincs új beküldendő adat, minden naprakész.</value>
|
||||
</data>
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</root>
|
||||
|
||||
@@ -109,14 +109,7 @@
|
||||
<value>Hai completato il recupero di {0} informazioni app.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Recupero {0} chiavi...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Completato il recupero di {0} chiavi.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Finito il recupero di tutte le chiavi del deposito per un totale di {0} applicazioni.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</root>
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</root>
|
||||
|
||||
@@ -88,7 +88,6 @@
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>새로 등록할 데이터가 없습니다.</value>
|
||||
</data>
|
||||
|
||||
@@ -105,7 +105,6 @@
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
<data name="SubmissionNoNewData" xml:space="preserve">
|
||||
<value>Nėra jokių naujų duomenų, kuriuos būtų galima pateikti, viskas jau atnaujinta.</value>
|
||||
</data>
|
||||
|
||||
@@ -81,7 +81,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
<data name="SubmissionSuccessfulNewApps" xml:space="preserve">
|
||||
<value>Jaunas aplikācijas: {0}</value>
|
||||
<comment>{0} will be replaced by list of the apps (IDs, numbers), separated by a comma</comment>
|
||||
|
||||
@@ -62,9 +62,14 @@
|
||||
PublicKeyToken=b77a5c561934e089
|
||||
</value>
|
||||
</resheader>
|
||||
|
||||
|
||||
|
||||
<data name="PluginDisabledMissingBuildToken" xml:space="preserve">
|
||||
<value>{0} is uitgeschakeld vanwege een ontbrekende build-token</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
<data name="PluginDisabledInConfig" xml:space="preserve">
|
||||
<value>{0} is momenteel uitgeschakeld volgens uw configuratie. Als je SteamDB wilt helpen bij het indienen van gegevens, bekijk dan onze wiki.</value>
|
||||
<comment>{0} will be replaced by the name of the plugin (e.g. "SteamTokenDumperPlugin")</comment>
|
||||
</data>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -109,13 +109,9 @@
|
||||
<value>Zakończono pobieranie {0} informacji o aplikacji.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Pobieranie {0} kluczy magazynu...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Zakończono pobieranie {0} kluczy magazynu.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
<value>Pomyślnie pobrano {0} z {1} kluczy magazynu.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys that were successfully retrieved, {1} will be replaced by the number (count this batch) of depot keys that were supposed to be retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Zakończono pobieranie wszystkich kluczy magazynu dla {0} aplikacji.</value>
|
||||
|
||||
@@ -109,14 +109,7 @@
|
||||
<value>Recuperamos um total de {0} informações de aplicativos.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Recuperando {0} códigos de depots...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Recuperamos códigos para {0} depots.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Recuperamos códigos de acesso para {0} aplicativos.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</root>
|
||||
|
||||
@@ -109,14 +109,7 @@
|
||||
<value>FINISHD RETRIEVIN {0} APP INFOS.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>RETRIEVIN {0} DEPOT KEYS...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>FINISHD RETRIEVIN {0} DEPOT KEYS.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>FINISHD RETRIEVIN ALL DEPOT KEYS 4 TOTAL OV {0} APPS.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</root>
|
||||
|
||||
@@ -109,14 +109,7 @@
|
||||
<value>Завершено получение информации {0} приложений.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Получение {0} ключей хранилища...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Получение {0} ключей хранилища завершено.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Получение всех ключей хранилища {0} приложений завершено.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -109,14 +109,7 @@
|
||||
<value>Dokončené získavanie informácií {0} aplikácií.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Získavam {0} kľúčov položiek...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Dokončené získavanie {0} kľúčov položiek.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Dokončené získanie všetkých kľúčov položiek {0} aplikácií.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</root>
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</root>
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</root>
|
||||
|
||||
@@ -109,13 +109,9 @@
|
||||
<value>{0} uygulama bilgisinin alınması tamamlandı.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>{0} depo anahtarı alınıyor...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>{0} depo anahtarının alınması tamamlandı.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
<value>{0}/{1} depo anahtarı başarıyla alındı.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys that were successfully retrieved, {1} will be replaced by the number (count this batch) of depot keys that were supposed to be retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Toplam {0} uygulama için tüm depo anahtarlarının alınması tamamlandı.</value>
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</root>
|
||||
|
||||
@@ -109,14 +109,7 @@
|
||||
<value>Đã hoàn tất thu nhận thông tin của {0} ứng dụng.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Đang thu nhận {0} khóa kho...</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>Đã hoàn tất thu nhận {0} khóa kho.</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
</data>
|
||||
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>Đã hoàn tất thu nhận tất cả khóa kho của tổng số {0} ứng dụng.</value>
|
||||
<comment>{0} will be replaced by the number (total count) of apps retrieved</comment>
|
||||
|
||||
@@ -109,13 +109,9 @@
|
||||
<value>已完成获取 {0} 个 App 的信息。</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>正在获取 {0} 个 Depot Key……</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>已完成获取 {0} 个 Depot key。</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
<value>成功获取 {1} 个 Depot Key 中的 {0} 个。</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys that were successfully retrieved, {1} will be replaced by the number (count this batch) of depot keys that were supposed to be retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>已完成获取共计 {0} 个 App 的所有 Depot Key。</value>
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</root>
|
||||
|
||||
@@ -109,13 +109,9 @@
|
||||
<value>已完成檢索 {0} 個應用程式資料。</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of app infos retrieved</comment>
|
||||
</data>
|
||||
<data name="BotRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>正在檢索 {0} 個應用程式的 Depot 金鑰…</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys being retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingDepotKeys" xml:space="preserve">
|
||||
<value>已完成檢索 {0} 個應用程式的 Depot 金鑰。</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys retrieved</comment>
|
||||
<value>成功檢索 {1} 個 Depot 金鑰中的 {0} 個。</value>
|
||||
<comment>{0} will be replaced by the number (count this batch) of depot keys that were successfully retrieved, {1} will be replaced by the number (count this batch) of depot keys that were supposed to be retrieved</comment>
|
||||
</data>
|
||||
<data name="BotFinishedRetrievingTotalDepots" xml:space="preserve">
|
||||
<value>已完成檢索共 {0} 個應用程式的 Depot 金鑰。</value>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Composition;
|
||||
using System.Globalization;
|
||||
@@ -30,6 +31,7 @@ using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Helpers;
|
||||
using ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Data;
|
||||
using ArchiSteamFarm.OfficialPlugins.SteamTokenDumper.Localization;
|
||||
using ArchiSteamFarm.Plugins;
|
||||
@@ -403,6 +405,8 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
|
||||
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotFinishedRetrievingTotalAppAccessTokens, appIDsToRefresh.Count));
|
||||
bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotRetrievingTotalDepots, appIDsToRefresh.Count));
|
||||
|
||||
(_, ImmutableHashSet<uint>? knownDepotIDs) = await GlobalCache.KnownDepotIDs.GetValue(ArchiCacheable<ImmutableHashSet<uint>>.EFallback.SuccessPreviously).ConfigureAwait(false);
|
||||
|
||||
using (HashSet<uint>.Enumerator enumerator = appIDsToRefresh.GetEnumerator()) {
|
||||
while (true) {
|
||||
if (!bot.IsConnectedAndLoggedOn) {
|
||||
@@ -451,16 +455,28 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
|
||||
foreach (SteamApps.PICSProductInfoCallback.PICSProductInfo app in response.Results.SelectMany(static result => result.Apps.Values)) {
|
||||
appChangeNumbers[app.ID] = app.ChangeNumber;
|
||||
|
||||
if (GlobalCache.ShouldRefreshDepotKey(app.ID)) {
|
||||
bool shouldFetchMainKey = false;
|
||||
|
||||
foreach (KeyValue depot in app.KeyValues["depots"].Children) {
|
||||
if (!uint.TryParse(depot.Name, out uint depotID) || (knownDepotIDs?.Contains(depotID) == true) || Config.SecretDepotIDs.Contains(depotID) || !GlobalCache.ShouldRefreshDepotKey(depotID)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
depotKeysTotal++;
|
||||
|
||||
await depotsRateLimitingSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
SteamApps.DepotKeyCallback depotResponse = await bot.SteamApps.GetDepotDecryptionKey(app.ID, app.ID).ToLongRunningTask().ConfigureAwait(false);
|
||||
SteamApps.DepotKeyCallback depotResponse = await bot.SteamApps.GetDepotDecryptionKey(depotID, app.ID).ToLongRunningTask().ConfigureAwait(false);
|
||||
|
||||
depotKeysSuccessful++;
|
||||
|
||||
if (depotResponse.Result != EResult.OK) {
|
||||
continue;
|
||||
}
|
||||
|
||||
shouldFetchMainKey = true;
|
||||
|
||||
GlobalCache.UpdateDepotKey(depotResponse);
|
||||
} catch (Exception e) {
|
||||
// We can still try other depots
|
||||
@@ -477,17 +493,14 @@ internal sealed class SteamTokenDumperPlugin : OfficialPlugin, IASF, IBot, IBotC
|
||||
}
|
||||
}
|
||||
|
||||
foreach (KeyValue depot in app.KeyValues["depots"].Children) {
|
||||
if (!uint.TryParse(depot.Name, out uint depotID) || Config.SecretDepotIDs.Contains(depotID) || !GlobalCache.ShouldRefreshDepotKey(depotID)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Consider fetching main appID key only if we've actually considered some new depots for resolving
|
||||
if (shouldFetchMainKey && (knownDepotIDs?.Contains(app.ID) != true) && GlobalCache.ShouldRefreshDepotKey(app.ID)) {
|
||||
depotKeysTotal++;
|
||||
|
||||
await depotsRateLimitingSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
SteamApps.DepotKeyCallback depotResponse = await bot.SteamApps.GetDepotDecryptionKey(depotID, app.ID).ToLongRunningTask().ConfigureAwait(false);
|
||||
SteamApps.DepotKeyCallback depotResponse = await bot.SteamApps.GetDepotDecryptionKey(app.ID, app.ID).ToLongRunningTask().ConfigureAwait(false);
|
||||
|
||||
depotKeysSuccessful++;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -28,6 +28,28 @@ namespace ArchiSteamFarm.Tests;
|
||||
|
||||
[TestClass]
|
||||
public sealed class Trading {
|
||||
[TestMethod]
|
||||
public void ExploitingNewSetsIsFairButNotNeutral() {
|
||||
HashSet<Asset> inventory = new() {
|
||||
CreateItem(1, 40),
|
||||
CreateItem(2, 10),
|
||||
CreateItem(3, 10)
|
||||
};
|
||||
|
||||
HashSet<Asset> itemsToGive = new() {
|
||||
CreateItem(2, 5),
|
||||
CreateItem(3, 5)
|
||||
};
|
||||
|
||||
HashSet<Asset> itemsToReceive = new() {
|
||||
CreateItem(1, 9),
|
||||
CreateItem(4)
|
||||
};
|
||||
|
||||
Assert.IsTrue(IsFairExchange(itemsToGive, itemsToReceive));
|
||||
Assert.IsFalse(IsTradeNeutralOrBetter(inventory, itemsToGive, itemsToReceive));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MismatchRarityIsNotFair() {
|
||||
HashSet<Asset> itemsToGive = new() { CreateItem(1, rarity: Asset.ERarity.Rare) };
|
||||
|
||||
@@ -223,7 +223,7 @@ StackTrace:
|
||||
<value>Deze bot is al gestopt!</value>
|
||||
</data>
|
||||
<data name="BotNotFound" xml:space="preserve">
|
||||
<value>Geen bot gevonden met de naam {0}!</value>
|
||||
<value>Kon geen enkele bot met de naam {0} vinden!</value>
|
||||
<comment>{0} will be replaced by bot's name query (string)</comment>
|
||||
</data>
|
||||
<data name="BotStatusOverview" xml:space="preserve">
|
||||
@@ -739,5 +739,8 @@ Proces uptime: {1}</value>
|
||||
<value>Het IP adres {0} is niet gebanned!</value>
|
||||
<comment>{0} will be replaced by an IP address which was requested to be unbanned from using IPC</comment>
|
||||
</data>
|
||||
|
||||
<data name="WarningNoLicense" xml:space="preserve">
|
||||
<value>Je hebt geprobeerd om de betaalde functie {0} te gebruiken, maar je hebt geen geldige LicenseID ingesteld in de ASF global config. Controleer uw configuratie, aangezien de functie niet werkt zonder aanvullende details.</value>
|
||||
<comment>{0} will be replaced by feature name (e.g. MatchActively)</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -95,6 +95,10 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
[PublicAPI]
|
||||
public ArchiWebHandler ArchiWebHandler { get; }
|
||||
|
||||
[JsonIgnore]
|
||||
[PublicAPI]
|
||||
public BotDatabase BotDatabase { get; }
|
||||
|
||||
[JsonProperty]
|
||||
[PublicAPI]
|
||||
public string BotName { get; }
|
||||
@@ -135,8 +139,6 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
[PublicAPI]
|
||||
public SteamFriends SteamFriends { get; }
|
||||
|
||||
internal readonly BotDatabase BotDatabase;
|
||||
|
||||
internal bool CanReceiveSteamCards => !IsAccountLimited && !IsAccountLocked;
|
||||
internal bool IsAccountLimited => AccountFlags.HasFlag(EAccountFlags.LimitedUser) || AccountFlags.HasFlag(EAccountFlags.LimitedUserForce);
|
||||
internal bool IsAccountLocked => AccountFlags.HasFlag(EAccountFlags.Lockdown);
|
||||
@@ -3000,11 +3002,12 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
|
||||
return;
|
||||
}
|
||||
|
||||
Nickname = callback.Name;
|
||||
// Empty name should be converted to null, this is actually lack of value, but it's transmitted as empty in protobufs
|
||||
Nickname = !string.IsNullOrEmpty(callback.Name) ? callback.Name : null;
|
||||
|
||||
string? avatarHash = null;
|
||||
|
||||
if ((callback.AvatarHash?.Length > 0) && callback.AvatarHash.Any(static singleByte => singleByte != 0)) {
|
||||
if ((callback.AvatarHash?.Length > 0) && callback.AvatarHash.Any(static singleByte => singleByte > 0)) {
|
||||
#pragma warning disable CA1308 // False positive, we're intentionally converting this part to lowercase and it's not used for any security decisions based on the result of the normalization
|
||||
avatarHash = Convert.ToHexString(callback.AvatarHash).ToLowerInvariant();
|
||||
#pragma warning restore CA1308 // False positive, we're intentionally converting this part to lowercase and it's not used for any security decisions based on the result of the normalization
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -171,33 +171,17 @@ public sealed class Trading : IDisposable {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If amount of unique items in the set increases, this is always a good trade (e.g. 0 2 -> 1 1)
|
||||
if (afterAmounts.Count > beforeAmounts.Count) {
|
||||
continue;
|
||||
// Otherwise, fill the missing holes in our data if needed, since we actually had zeros there
|
||||
for (byte i = 0; i < afterAmounts.Count - beforeAmounts.Count; i++) {
|
||||
beforeAmounts.Insert(0, 0);
|
||||
}
|
||||
|
||||
// At this point we're sure that amount of unique items stays the same, so we can evaluate actual sets
|
||||
// We make use of the fact that our amounts are already sorted in ascending order, so we can just take the first value instead of calculating ourselves
|
||||
uint beforeSets = beforeAmounts[0];
|
||||
uint afterSets = afterAmounts[0];
|
||||
|
||||
// If amount of our sets for this game decreases, this is always a bad trade (e.g. 2 2 2 -> 3 2 1)
|
||||
if (afterSets < beforeSets) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If amount of our sets for this game increases, this is always a good trade (e.g. 3 2 1 -> 2 2 2)
|
||||
if (afterSets > beforeSets) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// At this point we're sure that both number of unique items in the set stays the same, as well as number of our actual sets
|
||||
// We need to ensure set progress here and keep in mind overpaying, so we'll calculate neutrality as a difference in amounts at appropriate indexes
|
||||
// Now we need to ensure set progress and keep in mind overpaying, so we'll calculate neutrality as a difference in amounts at appropriate indexes
|
||||
// We start from the amounts we have the least of, our data is already sorted in ascending order, so we can just subtract and compare until we cover every amount
|
||||
// Neutrality can't reach value below 0 at any single point of calculation, as that would imply a loss of progress even if we'd end up with a positive value by the end
|
||||
int neutrality = 0;
|
||||
|
||||
// Skip initial 0 index, as we already checked it above and it doesn't change neutrality from 0
|
||||
for (byte i = 1; i < afterAmounts.Count; i++) {
|
||||
for (byte i = 0; i < afterAmounts.Count; i++) {
|
||||
// We assume that the difference between amounts will be within int range, therefore we accept underflow here (for subtraction), and since we cast that result to int afterwards, we also accept overflow for the cast itself
|
||||
neutrality += unchecked((int) (afterAmounts[i] - beforeAmounts[i]));
|
||||
|
||||
@@ -414,7 +398,7 @@ public sealed class Trading : IDisposable {
|
||||
}
|
||||
|
||||
private async Task<bool> ParseActiveTrades() {
|
||||
HashSet<TradeOffer>? tradeOffers = await Bot.ArchiWebHandler.GetActiveTradeOffers().ConfigureAwait(false);
|
||||
HashSet<TradeOffer>? tradeOffers = await Bot.ArchiWebHandler.GetTradeOffers(true, true, false, true).ConfigureAwait(false);
|
||||
|
||||
if ((tradeOffers == null) || (tradeOffers.Count == 0)) {
|
||||
return false;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -101,6 +101,17 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
WebBrowser.Dispose();
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public async Task<bool> CancelTradeOffer(ulong tradeID) {
|
||||
if (tradeID == 0) {
|
||||
throw new ArgumentOutOfRangeException(nameof(tradeID));
|
||||
}
|
||||
|
||||
Uri request = new(SteamCommunityURL, $"/tradeoffer/{tradeID}/cancel");
|
||||
|
||||
return await UrlPostWithSession(request).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public async Task<string?> GetAbsoluteProfileURL(bool waitForInitialization = true) {
|
||||
if (waitForInitialization && !Initialized) {
|
||||
@@ -353,6 +364,210 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
return result;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public async Task<HashSet<TradeOffer>?> GetTradeOffers(bool? activeOnly = null, bool? receivedOffers = null, bool? sentOffers = null, bool? withDescriptions = null) {
|
||||
(bool success, string? steamApiKey) = await CachedApiKey.GetValue().ConfigureAwait(false);
|
||||
|
||||
if (!success || string.IsNullOrEmpty(steamApiKey)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Dictionary<string, object?> arguments = new(StringComparer.Ordinal) {
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
{ "key", steamApiKey! }
|
||||
};
|
||||
|
||||
if (activeOnly.HasValue) {
|
||||
arguments["active_only"] = activeOnly.Value ? "true" : "false";
|
||||
|
||||
// This is ridiculous, active_only without historical cutoff is actually active right now + inactive ones that changed their status since our preview request, what the fuck
|
||||
// We're going to make it work as everybody sane expects, by being active ONLY, as the name implies, not active + some shit nobody asked for
|
||||
// https://developer.valvesoftware.com/wiki/Steam_Web_API/IEconService#GetTradeOffers_.28v1.29
|
||||
if (activeOnly.Value) {
|
||||
arguments["time_historical_cutoff"] = uint.MaxValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (receivedOffers.HasValue) {
|
||||
arguments["get_received_offers"] = receivedOffers.Value ? "true" : "false";
|
||||
}
|
||||
|
||||
if (sentOffers.HasValue) {
|
||||
arguments["get_sent_offers"] = sentOffers.Value ? "true" : "false";
|
||||
}
|
||||
|
||||
if (withDescriptions.HasValue) {
|
||||
arguments["get_descriptions"] = withDescriptions.Value ? "true" : "false";
|
||||
}
|
||||
|
||||
KeyValue? response = null;
|
||||
|
||||
for (byte i = 0; (i < WebBrowser.MaxTries) && (response == null); i++) {
|
||||
if ((i > 0) && (WebLimiterDelay > 0)) {
|
||||
await Task.Delay(WebLimiterDelay).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
using WebAPI.AsyncInterface econService = Bot.SteamConfiguration.GetAsyncWebAPIInterface(EconService);
|
||||
|
||||
econService.Timeout = WebBrowser.Timeout;
|
||||
|
||||
try {
|
||||
response = await WebLimitRequest(
|
||||
WebAPI.DefaultBaseAddress,
|
||||
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
async () => await econService.CallAsync(HttpMethod.Get, "GetTradeOffers", args: arguments).ConfigureAwait(false)
|
||||
).ConfigureAwait(false);
|
||||
} catch (TaskCanceledException e) {
|
||||
Bot.ArchiLogger.LogGenericDebuggingException(e);
|
||||
} catch (Exception e) {
|
||||
Bot.ArchiLogger.LogGenericWarningException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (response == null) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Dictionary<(uint AppID, ulong ClassID, ulong InstanceID), InventoryResponse.Description> descriptions = new();
|
||||
|
||||
foreach (KeyValue description in response["descriptions"].Children) {
|
||||
uint appID = description["appid"].AsUnsignedInteger();
|
||||
|
||||
if (appID == 0) {
|
||||
Bot.ArchiLogger.LogNullError(appID);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ulong classID = description["classid"].AsUnsignedLong();
|
||||
|
||||
if (classID == 0) {
|
||||
Bot.ArchiLogger.LogNullError(classID);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ulong instanceID = description["instanceid"].AsUnsignedLong();
|
||||
|
||||
(uint AppID, ulong ClassID, ulong InstanceID) key = (appID, classID, instanceID);
|
||||
|
||||
if (descriptions.ContainsKey(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
InventoryResponse.Description parsedDescription = new() {
|
||||
AppID = appID,
|
||||
ClassID = classID,
|
||||
InstanceID = instanceID,
|
||||
Marketable = description["marketable"].AsBoolean(),
|
||||
Tradable = true // We're parsing active trade offers, we can assume as much
|
||||
};
|
||||
|
||||
List<KeyValue> tags = description["tags"].Children;
|
||||
|
||||
if (tags.Count > 0) {
|
||||
HashSet<Tag> parsedTags = new(tags.Count);
|
||||
|
||||
foreach (KeyValue tag in tags) {
|
||||
string? identifier = tag["category"].AsString();
|
||||
|
||||
if (string.IsNullOrEmpty(identifier)) {
|
||||
Bot.ArchiLogger.LogNullError(identifier);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
string? value = tag["internal_name"].AsString();
|
||||
|
||||
// Apparently, name can be empty, but not null
|
||||
if (value == null) {
|
||||
Bot.ArchiLogger.LogNullError(value);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
parsedTags.Add(new Tag(identifier!, value));
|
||||
}
|
||||
|
||||
parsedDescription.Tags = parsedTags.ToImmutableHashSet();
|
||||
}
|
||||
|
||||
descriptions[key] = parsedDescription;
|
||||
}
|
||||
|
||||
IEnumerable<KeyValue> trades = Enumerable.Empty<KeyValue>();
|
||||
|
||||
if (receivedOffers.GetValueOrDefault(true)) {
|
||||
trades = trades.Concat(response["trade_offers_received"].Children);
|
||||
}
|
||||
|
||||
if (sentOffers.GetValueOrDefault(true)) {
|
||||
trades = trades.Concat(response["trade_offers_sent"].Children);
|
||||
}
|
||||
|
||||
HashSet<TradeOffer> result = new();
|
||||
|
||||
foreach (KeyValue trade in trades) {
|
||||
ETradeOfferState state = trade["trade_offer_state"].AsEnum<ETradeOfferState>();
|
||||
|
||||
if (!Enum.IsDefined(state)) {
|
||||
Bot.ArchiLogger.LogNullError(state);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (activeOnly.HasValue && ((activeOnly.Value && (state != ETradeOfferState.Active)) || (!activeOnly.Value && (state == ETradeOfferState.Active)))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ulong tradeOfferID = trade["tradeofferid"].AsUnsignedLong();
|
||||
|
||||
if (tradeOfferID == 0) {
|
||||
Bot.ArchiLogger.LogNullError(tradeOfferID);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
uint otherSteamID3 = trade["accountid_other"].AsUnsignedInteger();
|
||||
|
||||
if (otherSteamID3 == 0) {
|
||||
Bot.ArchiLogger.LogNullError(otherSteamID3);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
TradeOffer tradeOffer = new(tradeOfferID, otherSteamID3, state);
|
||||
|
||||
List<KeyValue> itemsToGive = trade["items_to_give"].Children;
|
||||
|
||||
if (itemsToGive.Count > 0) {
|
||||
if (!ParseItems(descriptions, itemsToGive, tradeOffer.ItemsToGive)) {
|
||||
Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorParsingObject, nameof(itemsToGive)));
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
List<KeyValue> itemsToReceive = trade["items_to_receive"].Children;
|
||||
|
||||
if (itemsToReceive.Count > 0) {
|
||||
if (!ParseItems(descriptions, itemsToReceive, tradeOffer.ItemsToReceive)) {
|
||||
Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorParsingObject, nameof(itemsToReceive)));
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(tradeOffer);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public async Task<bool?> HasValidApiKey() {
|
||||
(bool success, string? steamApiKey) = await CachedApiKey.GetValue().ConfigureAwait(false);
|
||||
@@ -375,7 +590,7 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public async Task<(bool Success, HashSet<ulong>? MobileTradeOfferIDs)> SendTradeOffer(ulong steamID, IReadOnlyCollection<Asset>? itemsToGive = null, IReadOnlyCollection<Asset>? itemsToReceive = null, string? token = null, bool forcedSingleOffer = false, ushort itemsPerTrade = Trading.MaxItemsPerTrade) {
|
||||
public async Task<(bool Success, HashSet<ulong>? TradeOfferIDs, HashSet<ulong>? MobileTradeOfferIDs)> SendTradeOffer(ulong steamID, IReadOnlyCollection<Asset>? itemsToGive = null, IReadOnlyCollection<Asset>? itemsToReceive = null, string? token = null, bool forcedSingleOffer = false, ushort itemsPerTrade = Trading.MaxItemsPerTrade) {
|
||||
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
|
||||
throw new ArgumentOutOfRangeException(nameof(steamID));
|
||||
}
|
||||
@@ -432,6 +647,7 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
{ "tradeoffermessage", $"Sent by {SharedInfo.PublicIdentifier}/{SharedInfo.Version}" }
|
||||
};
|
||||
|
||||
HashSet<ulong> tradeOfferIDs = new(trades.Count);
|
||||
HashSet<ulong> mobileTradeOfferIDs = new(trades.Count);
|
||||
|
||||
foreach (TradeOfferSendRequest trade in trades) {
|
||||
@@ -443,7 +659,7 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
response = await UrlPostToJsonObjectWithSession<TradeOfferSendResponse>(request, data: data, referer: referer, requestOptions: WebBrowser.ERequestOptions.ReturnServerErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false);
|
||||
|
||||
if (response == null) {
|
||||
return (false, mobileTradeOfferIDs);
|
||||
return (false, tradeOfferIDs, mobileTradeOfferIDs);
|
||||
}
|
||||
|
||||
if (response.StatusCode.IsServerErrorCode()) {
|
||||
@@ -458,26 +674,28 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, response.Content!.ErrorText));
|
||||
|
||||
return (false, mobileTradeOfferIDs);
|
||||
return (false, tradeOfferIDs, mobileTradeOfferIDs);
|
||||
}
|
||||
}
|
||||
|
||||
if (response?.Content == null) {
|
||||
return (false, mobileTradeOfferIDs);
|
||||
return (false, tradeOfferIDs, mobileTradeOfferIDs);
|
||||
}
|
||||
|
||||
if (response.Content.TradeOfferID == 0) {
|
||||
Bot.ArchiLogger.LogNullError(response.Content.TradeOfferID);
|
||||
|
||||
return (false, mobileTradeOfferIDs);
|
||||
return (false, tradeOfferIDs, mobileTradeOfferIDs);
|
||||
}
|
||||
|
||||
tradeOfferIDs.Add(response.Content.TradeOfferID);
|
||||
|
||||
if (response.Content.RequiresMobileConfirmation) {
|
||||
mobileTradeOfferIDs.Add(response.Content.TradeOfferID);
|
||||
}
|
||||
}
|
||||
|
||||
return (true, mobileTradeOfferIDs);
|
||||
return (true, tradeOfferIDs, mobileTradeOfferIDs);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
@@ -1465,182 +1683,6 @@ public sealed class ArchiWebHandler : IDisposable {
|
||||
return response?.Content?.Queue;
|
||||
}
|
||||
|
||||
internal async Task<HashSet<TradeOffer>?> GetActiveTradeOffers() {
|
||||
(bool success, string? steamApiKey) = await CachedApiKey.GetValue().ConfigureAwait(false);
|
||||
|
||||
if (!success || string.IsNullOrEmpty(steamApiKey)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Dictionary<string, object?> arguments = new(5, StringComparer.Ordinal) {
|
||||
{ "active_only", 1 },
|
||||
{ "get_descriptions", 1 },
|
||||
{ "get_received_offers", 1 },
|
||||
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
{ "key", steamApiKey! },
|
||||
|
||||
{ "time_historical_cutoff", uint.MaxValue }
|
||||
};
|
||||
|
||||
KeyValue? response = null;
|
||||
|
||||
for (byte i = 0; (i < WebBrowser.MaxTries) && (response == null); i++) {
|
||||
if ((i > 0) && (WebLimiterDelay > 0)) {
|
||||
await Task.Delay(WebLimiterDelay).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
using WebAPI.AsyncInterface econService = Bot.SteamConfiguration.GetAsyncWebAPIInterface(EconService);
|
||||
|
||||
econService.Timeout = WebBrowser.Timeout;
|
||||
|
||||
try {
|
||||
response = await WebLimitRequest(
|
||||
WebAPI.DefaultBaseAddress,
|
||||
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
async () => await econService.CallAsync(HttpMethod.Get, "GetTradeOffers", args: arguments).ConfigureAwait(false)
|
||||
).ConfigureAwait(false);
|
||||
} catch (TaskCanceledException e) {
|
||||
Bot.ArchiLogger.LogGenericDebuggingException(e);
|
||||
} catch (Exception e) {
|
||||
Bot.ArchiLogger.LogGenericWarningException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (response == null) {
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Dictionary<(uint AppID, ulong ClassID, ulong InstanceID), InventoryResponse.Description> descriptions = new();
|
||||
|
||||
foreach (KeyValue description in response["descriptions"].Children) {
|
||||
uint appID = description["appid"].AsUnsignedInteger();
|
||||
|
||||
if (appID == 0) {
|
||||
Bot.ArchiLogger.LogNullError(appID);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ulong classID = description["classid"].AsUnsignedLong();
|
||||
|
||||
if (classID == 0) {
|
||||
Bot.ArchiLogger.LogNullError(classID);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ulong instanceID = description["instanceid"].AsUnsignedLong();
|
||||
|
||||
(uint AppID, ulong ClassID, ulong InstanceID) key = (appID, classID, instanceID);
|
||||
|
||||
if (descriptions.ContainsKey(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
InventoryResponse.Description parsedDescription = new() {
|
||||
AppID = appID,
|
||||
ClassID = classID,
|
||||
InstanceID = instanceID,
|
||||
Marketable = description["marketable"].AsBoolean(),
|
||||
Tradable = true // We're parsing active trade offers, we can assume as much
|
||||
};
|
||||
|
||||
List<KeyValue> tags = description["tags"].Children;
|
||||
|
||||
if (tags.Count > 0) {
|
||||
HashSet<Tag> parsedTags = new(tags.Count);
|
||||
|
||||
foreach (KeyValue tag in tags) {
|
||||
string? identifier = tag["category"].AsString();
|
||||
|
||||
if (string.IsNullOrEmpty(identifier)) {
|
||||
Bot.ArchiLogger.LogNullError(identifier);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
string? value = tag["internal_name"].AsString();
|
||||
|
||||
// Apparently, name can be empty, but not null
|
||||
if (value == null) {
|
||||
Bot.ArchiLogger.LogNullError(value);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
parsedTags.Add(new Tag(identifier!, value));
|
||||
}
|
||||
|
||||
parsedDescription.Tags = parsedTags.ToImmutableHashSet();
|
||||
}
|
||||
|
||||
descriptions[key] = parsedDescription;
|
||||
}
|
||||
|
||||
HashSet<TradeOffer> result = new();
|
||||
|
||||
foreach (KeyValue trade in response["trade_offers_received"].Children) {
|
||||
ETradeOfferState state = trade["trade_offer_state"].AsEnum<ETradeOfferState>();
|
||||
|
||||
if (!Enum.IsDefined(state)) {
|
||||
Bot.ArchiLogger.LogNullError(state);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (state != ETradeOfferState.Active) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ulong tradeOfferID = trade["tradeofferid"].AsUnsignedLong();
|
||||
|
||||
if (tradeOfferID == 0) {
|
||||
Bot.ArchiLogger.LogNullError(tradeOfferID);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
uint otherSteamID3 = trade["accountid_other"].AsUnsignedInteger();
|
||||
|
||||
if (otherSteamID3 == 0) {
|
||||
Bot.ArchiLogger.LogNullError(otherSteamID3);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
TradeOffer tradeOffer = new(tradeOfferID, otherSteamID3, state);
|
||||
|
||||
List<KeyValue> itemsToGive = trade["items_to_give"].Children;
|
||||
|
||||
if (itemsToGive.Count > 0) {
|
||||
if (!ParseItems(descriptions, itemsToGive, tradeOffer.ItemsToGive)) {
|
||||
Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorParsingObject, nameof(itemsToGive)));
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
List<KeyValue> itemsToReceive = trade["items_to_receive"].Children;
|
||||
|
||||
if (itemsToReceive.Count > 0) {
|
||||
if (!ParseItems(descriptions, itemsToReceive, tradeOffer.ItemsToReceive)) {
|
||||
Bot.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorParsingObject, nameof(itemsToReceive)));
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(tradeOffer);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal async Task<HashSet<uint>?> GetAppList() {
|
||||
KeyValue? response = null;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -340,7 +340,7 @@ public sealed class Actions : IAsyncDisposable, IDisposable {
|
||||
return (false, Strings.BotLootingFailed);
|
||||
}
|
||||
|
||||
(bool success, HashSet<ulong>? mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(targetSteamID, items, token: tradeToken, itemsPerTrade: itemsPerTrade).ConfigureAwait(false);
|
||||
(bool success, _, HashSet<ulong>? mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(targetSteamID, items, token: tradeToken, itemsPerTrade: itemsPerTrade).ConfigureAwait(false);
|
||||
|
||||
if ((mobileTradeOfferIDs?.Count > 0) && Bot.HasMobileAuthenticator) {
|
||||
(bool twoFactorSuccess, _, _) = await HandleTwoFactorAuthenticationConfirmations(true, Confirmation.EType.Trade, mobileTradeOfferIDs, true).ConfigureAwait(false);
|
||||
|
||||
@@ -644,6 +644,7 @@ public sealed class BotConfig {
|
||||
return (botConfig, json != latestJson ? latestJson : null);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public enum EAccess : byte {
|
||||
None,
|
||||
FamilySharing,
|
||||
@@ -652,6 +653,7 @@ public sealed class BotConfig {
|
||||
}
|
||||
|
||||
[Flags]
|
||||
[PublicAPI]
|
||||
public enum EBotBehaviour : byte {
|
||||
None = 0,
|
||||
RejectInvalidFriendInvites = 1,
|
||||
@@ -663,6 +665,7 @@ public sealed class BotConfig {
|
||||
All = RejectInvalidFriendInvites | RejectInvalidTrades | RejectInvalidGroupInvites | DismissInventoryNotifications | MarkReceivedMessagesAsRead | MarkBotMessagesAsRead
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public enum EFarmingOrder : byte {
|
||||
Unordered,
|
||||
AppIDsAscending,
|
||||
@@ -683,6 +686,7 @@ public sealed class BotConfig {
|
||||
}
|
||||
|
||||
[Flags]
|
||||
[PublicAPI]
|
||||
public enum ERedeemingPreferences : byte {
|
||||
None = 0,
|
||||
Forwarding = 1,
|
||||
@@ -693,6 +697,7 @@ public sealed class BotConfig {
|
||||
}
|
||||
|
||||
[Flags]
|
||||
[PublicAPI]
|
||||
public enum ERemoteCommunication : byte {
|
||||
None = 0,
|
||||
SteamGroup = 1,
|
||||
@@ -701,6 +706,7 @@ public sealed class BotConfig {
|
||||
}
|
||||
|
||||
[Flags]
|
||||
[PublicAPI]
|
||||
public enum ETradingPreferences : byte {
|
||||
None = 0,
|
||||
AcceptDonations = 1,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -28,15 +28,15 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Collections;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Helpers;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.Steam.Security;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ArchiSteamFarm.Steam.Storage;
|
||||
|
||||
internal sealed class BotDatabase : SerializableFile {
|
||||
public sealed class BotDatabase : GenericDatabase {
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
internal readonly ConcurrentHashSet<uint> FarmingBlacklistAppIDs = new();
|
||||
|
||||
|
||||
82
ArchiSteamFarm/Storage/GenericDatabase.cs
Normal file
82
ArchiSteamFarm/Storage/GenericDatabase.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// 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 ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Helpers;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace ArchiSteamFarm.Storage;
|
||||
|
||||
public abstract class GenericDatabase : SerializableFile {
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
private readonly ConcurrentDictionary<string, JToken> KeyValueJsonStorage = new();
|
||||
|
||||
[PublicAPI]
|
||||
public void DeleteFromJsonStorage(string key) {
|
||||
if (string.IsNullOrEmpty(key)) {
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
if (!KeyValueJsonStorage.TryRemove(key, out _)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Utilities.InBackground(Save);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public JToken? LoadFromJsonStorage(string key) {
|
||||
if (string.IsNullOrEmpty(key)) {
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
return KeyValueJsonStorage.TryGetValue(key, out JToken? value) ? value : null;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public void SaveToJsonStorage(string key, JToken value) {
|
||||
if (string.IsNullOrEmpty(key)) {
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
|
||||
if (value.Type == JTokenType.Null) {
|
||||
DeleteFromJsonStorage(key);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (KeyValueJsonStorage.TryGetValue(key, out JToken? currentValue) && JToken.DeepEquals(currentValue, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
KeyValueJsonStorage[key] = value;
|
||||
Utilities.InBackground(Save);
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeKeyValueJsonStorage() => !KeyValueJsonStorage.IsEmpty;
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2022 Łukasz "JustArchi" Domeradzki
|
||||
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
|
||||
// Contact: JustArchi@JustArchi.net
|
||||
// |
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -29,17 +29,15 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Collections;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Helpers;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.Steam;
|
||||
using ArchiSteamFarm.Steam.SteamKit2;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace ArchiSteamFarm.Storage;
|
||||
|
||||
public sealed class GlobalDatabase : SerializableFile {
|
||||
public sealed class GlobalDatabase : GenericDatabase {
|
||||
[JsonIgnore]
|
||||
[PublicAPI]
|
||||
public IReadOnlyDictionary<uint, ulong> PackageAccessTokensReadOnly => PackagesAccessTokens;
|
||||
@@ -57,9 +55,6 @@ public sealed class GlobalDatabase : SerializableFile {
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
internal readonly InMemoryServerListProvider ServerListProvider = new();
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
private readonly ConcurrentDictionary<string, JToken> KeyValueJsonStorage = new();
|
||||
|
||||
[JsonProperty(Required = Required.DisallowNull)]
|
||||
private readonly ConcurrentDictionary<uint, ulong> PackagesAccessTokens = new();
|
||||
|
||||
@@ -119,50 +114,6 @@ public sealed class GlobalDatabase : SerializableFile {
|
||||
ServerListProvider.ServerListUpdated += OnObjectModified;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public void DeleteFromJsonStorage(string key) {
|
||||
if (string.IsNullOrEmpty(key)) {
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
if (!KeyValueJsonStorage.TryRemove(key, out _)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Utilities.InBackground(Save);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public JToken? LoadFromJsonStorage(string key) {
|
||||
if (string.IsNullOrEmpty(key)) {
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
return KeyValueJsonStorage.TryGetValue(key, out JToken? value) ? value : null;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public void SaveToJsonStorage(string key, JToken value) {
|
||||
if (string.IsNullOrEmpty(key)) {
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
|
||||
if (value.Type == JTokenType.Null) {
|
||||
DeleteFromJsonStorage(key);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (KeyValueJsonStorage.TryGetValue(key, out JToken? currentValue) && JToken.DeepEquals(currentValue, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
KeyValueJsonStorage[key] = value;
|
||||
Utilities.InBackground(Save);
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeBackingCellID() => BackingCellID != 0;
|
||||
|
||||
@@ -175,9 +126,6 @@ public sealed class GlobalDatabase : SerializableFile {
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeCardCountsPerGame() => !CardCountsPerGame.IsEmpty;
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializeKeyValueJsonStorage() => !KeyValueJsonStorage.IsEmpty;
|
||||
|
||||
[UsedImplicitly]
|
||||
public bool ShouldSerializePackagesAccessTokens() => !PackagesAccessTokens.IsEmpty;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>5.4.2.10</Version>
|
||||
<Version>5.4.2.12</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
2
wiki
2
wiki
Submodule wiki updated: 8c41a08d21...acfee51543
Reference in New Issue
Block a user