From dd1dfdcf34bb68a629530ac3f545ebf40dc6157c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=81ukasz=20Domeradzki?= <JustArchi@JustArchi.net>
Date: Mon, 17 Jun 2024 09:17:14 +0200
Subject: [PATCH] Closes #3221

---
 .../ExamplePlugin.cs                          |  5 +-
 .../Plugins/Interfaces/IBotTradeOffer.cs      |  2 +
 .../Plugins/Interfaces/IBotTradeOffer2.cs     | 46 +++++++++++++++++++
 ArchiSteamFarm/Plugins/PluginsCore.cs         | 18 ++++++--
 ArchiSteamFarm/Steam/Exchange/Trading.cs      |  4 +-
 5 files changed, 68 insertions(+), 7 deletions(-)
 create mode 100644 ArchiSteamFarm/Plugins/Interfaces/IBotTradeOffer2.cs

diff --git a/ArchiSteamFarm.CustomPlugins.ExamplePlugin/ExamplePlugin.cs b/ArchiSteamFarm.CustomPlugins.ExamplePlugin/ExamplePlugin.cs
index aed406a322864..365eaec38097d 100644
--- a/ArchiSteamFarm.CustomPlugins.ExamplePlugin/ExamplePlugin.cs
+++ b/ArchiSteamFarm.CustomPlugins.ExamplePlugin/ExamplePlugin.cs
@@ -34,6 +34,7 @@
 using ArchiSteamFarm.Plugins.Interfaces;
 using ArchiSteamFarm.Steam;
 using ArchiSteamFarm.Steam.Data;
+using ArchiSteamFarm.Steam.Exchange;
 using SteamKit2;
 
 namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin;
@@ -45,7 +46,7 @@ namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin;
 // If you do not want to handle a particular action (e.g. OnBotMessage that is offered in IBotMessage), it's the best idea to not inherit it at all
 // This will keep your code compact, efficient and less dependent. You can always add additional interfaces when you'll need them, this example project will inherit quite a bit of them to show you potential usage
 [SuppressMessage("ReSharper", "MemberCanBeFileLocal")]
-internal sealed class ExamplePlugin : IASF, IBot, IBotCommand2, IBotConnection, IBotFriendRequest, IBotMessage, IBotModules, IBotTradeOffer {
+internal sealed class ExamplePlugin : IASF, IBot, IBotCommand2, IBotConnection, IBotFriendRequest, IBotMessage, IBotModules, IBotTradeOffer2 {
 	// This is used for identification purposes, typically you want to use a friendly name of your plugin here, such as the name of your main class
 	// Please note that this property can have direct dependencies only on structures that were initialized by the constructor, as it's possible to be called before OnLoaded() takes place
 	[JsonInclude]
@@ -180,7 +181,7 @@ public async Task OnBotInitModules(Bot bot, IReadOnlyDictionary<string, JsonElem
 	// It allows you not only to analyze such trades, but generate a response whether ASF should accept it (true), or proceed like usual (false)
 	// Thanks to that, you can implement custom rules for all trades that aren't handled by ASF, for example cross-set trading on your own custom rules
 	// You'd implement your own logic here, as an example we'll allow all trades to be accepted if the bot's name starts from "TrashBot"
-	public Task<bool> OnBotTradeOffer(Bot bot, TradeOffer tradeOffer) => Task.FromResult(bot.BotName.StartsWith("TrashBot", StringComparison.OrdinalIgnoreCase));
+	public Task<bool> OnBotTradeOffer(Bot bot, TradeOffer tradeOffer, ParseTradeResult.EResult asfResult) => Task.FromResult(bot.BotName.StartsWith("TrashBot", StringComparison.OrdinalIgnoreCase));
 
 	// This is the earliest method that will be called, right after loading the plugin, long before any bot initialization takes place
 	// It's a good place to initialize all potential (non-bot-specific) structures that you will need across lifetime of your plugin, such as global timers, concurrent dictionaries and alike
diff --git a/ArchiSteamFarm/Plugins/Interfaces/IBotTradeOffer.cs b/ArchiSteamFarm/Plugins/Interfaces/IBotTradeOffer.cs
index 40c7cbb22d6e1..27dcb06afdeaf 100644
--- a/ArchiSteamFarm/Plugins/Interfaces/IBotTradeOffer.cs
+++ b/ArchiSteamFarm/Plugins/Interfaces/IBotTradeOffer.cs
@@ -21,6 +21,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+using System;
 using System.Threading.Tasks;
 using ArchiSteamFarm.Steam;
 using ArchiSteamFarm.Steam.Data;
@@ -33,6 +34,7 @@ namespace ArchiSteamFarm.Plugins.Interfaces;
 ///     Implementing this interface allows your plugin to implement custom logic for accepting trades that ASF isn't willing to handle itself.
 /// </summary>
 [PublicAPI]
+[Obsolete($"Use {nameof(IBotTradeOffer2)} interface instead, this one will be removed in the next version")]
 public interface IBotTradeOffer : IPlugin {
 	/// <summary>
 	///     ASF will call this method for unhandled (ignored and rejected) trade offers received by the bot.
diff --git a/ArchiSteamFarm/Plugins/Interfaces/IBotTradeOffer2.cs b/ArchiSteamFarm/Plugins/Interfaces/IBotTradeOffer2.cs
new file mode 100644
index 0000000000000..e51da1636a826
--- /dev/null
+++ b/ArchiSteamFarm/Plugins/Interfaces/IBotTradeOffer2.cs
@@ -0,0 +1,46 @@
+// ----------------------------------------------------------------------------------------------
+//     _                _      _  ____   _                           _____
+//    / \    _ __  ___ | |__  (_)/ ___| | |_  ___   __ _  _ __ ___  |  ___|__ _  _ __  _ __ ___
+//   / _ \  | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_  / _` || '__|| '_ ` _ \
+//  / ___ \ | |  | (__ | | | || | ___) || |_|  __/| (_| || | | | | ||  _|| (_| || |   | | | | | |
+// /_/   \_\|_|   \___||_| |_||_||____/  \__|\___| \__,_||_| |_| |_||_|   \__,_||_|   |_| |_| |_|
+// ----------------------------------------------------------------------------------------------
+// |
+// Copyright 2015-2024 Ɓ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.Threading.Tasks;
+using ArchiSteamFarm.Steam;
+using ArchiSteamFarm.Steam.Data;
+using ArchiSteamFarm.Steam.Exchange;
+using JetBrains.Annotations;
+
+namespace ArchiSteamFarm.Plugins.Interfaces;
+
+/// <inheritdoc />
+/// <summary>
+///     Implementing this interface allows your plugin to implement custom logic for accepting trades that ASF isn't willing to handle itself.
+/// </summary>
+[PublicAPI]
+public interface IBotTradeOffer2 : IPlugin {
+	/// <summary>
+	///     ASF will call this method for unhandled (ignored and rejected) trade offers received by the bot.
+	/// </summary>
+	/// <param name="bot">Bot object related to this callback.</param>
+	/// <param name="tradeOffer">Trade offer related to this callback.</param>
+	/// <param name="asfResult">ASF result in regards to parsing this trade offer, can be useful for determining why it wasn't accepted as part of the core logic.</param>
+	/// <returns>True if the trade offer should be accepted as part of this plugin, false otherwise.</returns>
+	Task<bool> OnBotTradeOffer(Bot bot, TradeOffer tradeOffer, ParseTradeResult.EResult asfResult);
+}
diff --git a/ArchiSteamFarm/Plugins/PluginsCore.cs b/ArchiSteamFarm/Plugins/PluginsCore.cs
index a56e9b456ecad..ab3851a70ac1b 100644
--- a/ArchiSteamFarm/Plugins/PluginsCore.cs
+++ b/ArchiSteamFarm/Plugins/PluginsCore.cs
@@ -584,7 +584,7 @@ internal static async Task OnBotSteamCallbacksInit(Bot bot, CallbackManager call
 		return responses.Where(static response => response != null).SelectMany(static handlers => handlers ?? []).ToHashSet();
 	}
 
-	internal static async Task<bool> OnBotTradeOffer(Bot bot, TradeOffer tradeOffer) {
+	internal static async Task<bool> OnBotTradeOffer(Bot bot, TradeOffer tradeOffer, ParseTradeResult.EResult asfResult) {
 		ArgumentNullException.ThrowIfNull(bot);
 		ArgumentNullException.ThrowIfNull(tradeOffer);
 
@@ -595,14 +595,26 @@ internal static async Task<bool> OnBotTradeOffer(Bot bot, TradeOffer tradeOffer)
 		IList<bool> responses;
 
 		try {
-			responses = await Utilities.InParallel(ActivePlugins.OfType<IBotTradeOffer>().Select(plugin => plugin.OnBotTradeOffer(bot, tradeOffer))).ConfigureAwait(false);
+			responses = await Utilities.InParallel(ActivePlugins.OfType<IBotTradeOffer2>().Select(plugin => plugin.OnBotTradeOffer(bot, tradeOffer, asfResult))).ConfigureAwait(false);
 		} catch (Exception e) {
 			ASF.ArchiLogger.LogGenericException(e);
 
 			return false;
 		}
 
-		return responses.Any(static response => response);
+#pragma warning disable CS0618 // TODO: Pending removal
+		IList<bool> oldResponses;
+
+		try {
+			oldResponses = await Utilities.InParallel(ActivePlugins.OfType<IBotTradeOffer>().Select(plugin => plugin.OnBotTradeOffer(bot, tradeOffer))).ConfigureAwait(false);
+		} catch (Exception e) {
+			ASF.ArchiLogger.LogGenericException(e);
+
+			return false;
+		}
+#pragma warning restore CS0618 // TODO: Pending removal
+
+		return responses.Any(static response => response) || oldResponses.Any(static response => response);
 	}
 
 	internal static async Task OnBotTradeOfferResults(Bot bot, IReadOnlyCollection<ParseTradeResult> tradeResults) {
diff --git a/ArchiSteamFarm/Steam/Exchange/Trading.cs b/ArchiSteamFarm/Steam/Exchange/Trading.cs
index d75c3ee53696d..709f36ee6346c 100644
--- a/ArchiSteamFarm/Steam/Exchange/Trading.cs
+++ b/ArchiSteamFarm/Steam/Exchange/Trading.cs
@@ -307,10 +307,10 @@ private async Task<ParseTradeResult> ParseTrade(TradeOffer tradeOffer) {
 		bool tradeRequiresMobileConfirmation = false;
 
 		switch (result) {
+			case ParseTradeResult.EResult.Blacklisted:
 			case ParseTradeResult.EResult.Ignored:
 			case ParseTradeResult.EResult.Rejected:
-			case ParseTradeResult.EResult.Blacklisted:
-				bool accept = await PluginsCore.OnBotTradeOffer(Bot, tradeOffer).ConfigureAwait(false);
+				bool accept = await PluginsCore.OnBotTradeOffer(Bot, tradeOffer, result).ConfigureAwait(false);
 
 				if (accept) {
 					result = ParseTradeResult.EResult.Accepted;