Compare commits

...

17 Commits

Author SHA1 Message Date
Archi
4798b29bff Misc 2023-01-24 23:00:27 +01:00
Łukasz Domeradzki
df4f8d1e62 Improve fairness logic (#2807)
* Improve fairness logic

* Add unit test against abuse

* Further simplify the code

That first pass is not needed anymore, first loop covers it
2023-01-24 22:55:15 +01:00
Archi
00f7d2bfb9 Closes #2787 2023-01-24 22:49:41 +01:00
renovate[bot]
5ab1971018 Update ASF-ui digest to 94ce7b1 2023-01-24 05:32:33 +00:00
ArchiBot
841b80c297 Automatic translations update 2023-01-24 02:18:42 +00:00
Archi
10f2471332 CD: Upload sha512 sums first
So I can !asfapprove faster, lol
2023-01-24 02:27:37 +01:00
Archi
fccb455676 Bump 2023-01-24 01:40:27 +01:00
Archi
c9bc71462e Misc 2023-01-24 01:39:46 +01:00
Archi
e0f9fe3555 Skip empty nickname in self persona state callback 2023-01-24 01:29:55 +01:00
Archi
8bb48d829e STD: Add support for KnownDepotIDs 2023-01-23 12:08:30 +01:00
Archi
4c166102c5 Misc 2023-01-23 11:30:01 +01:00
Archi
a91a4aada6 Fetch main depot key only if we managed to fetch some other ones 2023-01-23 11:25:20 +01:00
renovate[bot]
e05139ea7f Update ASF-ui digest to a3cc476 2023-01-23 04:25:35 +00:00
ArchiBot
6119d33ab8 Automatic translations update 2023-01-23 02:18:51 +00:00
renovate[bot]
5905412d82 Update ASF-ui digest to b2dd529 2023-01-22 04:07:36 +00:00
ArchiBot
7596a89baa Automatic translations update 2023-01-22 02:20:35 +00:00
Archi
7e7fd3bab4 Bump 2023-01-21 23:28:23 +01:00
54 changed files with 540 additions and 459 deletions

View File

@@ -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

Submodule ASF-ui updated: 80e6bd5fac...94ce7b116c

View File

@@ -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>

View File

@@ -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);

View File

@@ -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);
}
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -108,7 +108,6 @@
</data>
<data name="SubmissionNoNewData" xml:space="preserve">
<value>Nincs új beküldendő adat, minden naprakész.</value>
</data>

View File

@@ -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>

View File

@@ -88,7 +88,6 @@
</data>
<data name="SubmissionNoNewData" xml:space="preserve">
<value>새로 등록할 데이터가 없습니다.</value>
</data>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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++;

View File

@@ -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) };

View File

@@ -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>

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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,

View File

@@ -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();

View 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;
}

View File

@@ -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;

View File

@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>5.4.2.10</Version>
<Version>5.4.2.12</Version>
</PropertyGroup>
<PropertyGroup>

2
wiki

Submodule wiki updated: 8c41a08d21...acfee51543