- Added retries for failed sends
- Redesigned config for better usability
- Implemented domain blacklist for sending
- Implemented skin functionality
- Fixed Discord message
This commit is contained in:
Patrick Ulrich 2024-11-07 10:16:04 -05:00
parent 3fd538be3e
commit 1b0974d116

View File

@ -10,7 +10,7 @@ using Oxide.Core.Libraries.Covalence;
namespace Oxide.Plugins namespace Oxide.Plugins
{ {
[Info("Orangemart", "saulteafarmer", "0.1.0")] [Info("Orangemart", "saulteafarmer", "0.2.0")]
[Description("Allows players to buy and sell in-game units and VIP status using Bitcoin Lightning Network payments")] [Description("Allows players to buy and sell in-game units and VIP status using Bitcoin Lightning Network payments")]
public class Orangemart : CovalencePlugin public class Orangemart : CovalencePlugin
{ {
@ -24,6 +24,8 @@ namespace Oxide.Plugins
private string CurrencyName; private string CurrencyName;
private int SatsPerCurrencyUnit; private int SatsPerCurrencyUnit;
private int PricePerCurrencyUnit; private int PricePerCurrencyUnit;
private string DiscordChannelName; // Added missing configuration variable
private ulong CurrencySkinID;
// Transaction timing settings (moved to config) // Transaction timing settings (moved to config)
private int CheckIntervalSeconds; private int CheckIntervalSeconds;
@ -31,6 +33,9 @@ namespace Oxide.Plugins
private int RetryDelaySeconds; private int RetryDelaySeconds;
private int MaxRetries; private int MaxRetries;
// Blacklisted domains
private List<string> BlacklistedDomains = new List<string>();
// File names // File names
private const string SellLogFile = "Orangemart/sell_log.json"; private const string SellLogFile = "Orangemart/sell_log.json";
private const string BuyInvoiceLogFile = "Orangemart/buy_invoices.json"; private const string BuyInvoiceLogFile = "Orangemart/buy_invoices.json";
@ -42,6 +47,11 @@ namespace Oxide.Plugins
private List<PendingInvoice> pendingInvoices = new List<PendingInvoice>(); private List<PendingInvoice> pendingInvoices = new List<PendingInvoice>();
private Dictionary<string, int> retryCounts = new Dictionary<string, int>(); private Dictionary<string, int> retryCounts = new Dictionary<string, int>();
// Added for handling 404-specific retries
private Dictionary<string, int> retryCounts404 = new Dictionary<string, int>();
private const int Max404Retries = 5;
private const int RetryDelaySeconds404 = 5;
private class LNDHubConfig private class LNDHubConfig
{ {
public string Username { get; set; } public string Username { get; set; }
@ -164,8 +174,9 @@ namespace Oxide.Plugins
SaveConfig(); SaveConfig();
} }
string lndhubConnectionString = Config["LNDHubConnection"]?.ToString(); // Access LNDHubConnection from the InvoiceSettings section
string discordWebhookUrl = Config["DiscordWebhookUrl"]?.ToString(); string lndhubConnectionString = (Config["InvoiceSettings"] as Dictionary<string, object>)?["LNDHubConnection"]?.ToString();
string discordWebhookUrl = (Config["Discord"] as Dictionary<string, object>)?["DiscordWebhookUrl"]?.ToString();
if (string.IsNullOrEmpty(lndhubConnectionString)) if (string.IsNullOrEmpty(lndhubConnectionString))
{ {
@ -176,22 +187,33 @@ namespace Oxide.Plugins
config = LNDHubConfig.ParseLNDHubConnection(lndhubConnectionString); config = LNDHubConfig.ParseLNDHubConnection(lndhubConnectionString);
config.DiscordWebhookUrl = discordWebhookUrl; config.DiscordWebhookUrl = discordWebhookUrl;
// Load configuration settings // Load other configuration settings...
CurrencyItemID = Convert.ToInt32(Config["CurrencyItemID"] ?? 1776460938); var currencySettings = Config["CurrencySettings"] as Dictionary<string, object>;
BuyCurrencyCommandName = Config["BuyCurrencyCommandName"]?.ToString() ?? "buyblood"; CurrencyItemID = Convert.ToInt32(currencySettings?["CurrencyItemID"] ?? 1776460938);
SendCurrencyCommandName = Config["SendCurrencyCommandName"]?.ToString() ?? "sendblood"; CurrencyName = currencySettings?["CurrencyName"]?.ToString() ?? "blood";
BuyVipCommandName = Config["BuyVipCommandName"]?.ToString() ?? "buyvip"; SatsPerCurrencyUnit = Convert.ToInt32(currencySettings?["SatsPerCurrencyUnit"] ?? 1);
VipPrice = Convert.ToInt32(Config["VipPrice"] ?? 1000); PricePerCurrencyUnit = Convert.ToInt32(currencySettings?["PricePerCurrencyUnit"] ?? 1);
VipPermissionGroup = Config["VipPermissionGroup"]?.ToString() ?? "vip"; CurrencySkinID = (ulong)Convert.ToInt64(currencySettings?["CurrencySkinID"] ?? 0);
CurrencyName = Config["CurrencyName"]?.ToString() ?? "blood";
SatsPerCurrencyUnit = Convert.ToInt32(Config["SatsPerCurrencyUnit"] ?? 1);
PricePerCurrencyUnit = Convert.ToInt32(Config["PricePerCurrencyUnit"] ?? 1);
// Load transaction timing settings from config var commandsSettings = Config["Commands"] as Dictionary<string, object>;
CheckIntervalSeconds = Convert.ToInt32(Config["CheckIntervalSeconds"] ?? 10); BuyCurrencyCommandName = commandsSettings?["BuyCurrencyCommandName"]?.ToString() ?? "buyblood";
InvoiceTimeoutSeconds = Convert.ToInt32(Config["InvoiceTimeoutSeconds"] ?? 300); SendCurrencyCommandName = commandsSettings?["SendCurrencyCommandName"]?.ToString() ?? "sendblood";
RetryDelaySeconds = Convert.ToInt32(Config["RetryDelaySeconds"] ?? 10); BuyVipCommandName = commandsSettings?["BuyVipCommandName"]?.ToString() ?? "buyvip";
MaxRetries = Convert.ToInt32(Config["MaxRetries"] ?? 25);
var vipSettings = Config["VIPSettings"] as Dictionary<string, object>;
VipPrice = Convert.ToInt32(vipSettings?["VipPrice"] ?? 1000);
VipPermissionGroup = vipSettings?["VipPermissionGroup"]?.ToString() ?? "vip";
// Load DiscordChannelName from Discord section
DiscordChannelName = (Config["Discord"] as Dictionary<string, object>)?["DiscordChannelName"]?.ToString() ?? "mart";
// Load invoice settings
var invoiceSettings = Config["InvoiceSettings"] as Dictionary<string, object>;
CheckIntervalSeconds = Convert.ToInt32(invoiceSettings?["CheckIntervalSeconds"] ?? 10);
InvoiceTimeoutSeconds = Convert.ToInt32(invoiceSettings?["InvoiceTimeoutSeconds"] ?? 300);
RetryDelaySeconds = Convert.ToInt32(invoiceSettings?["RetryDelaySeconds"] ?? 10);
MaxRetries = Convert.ToInt32(invoiceSettings?["MaxRetries"] ?? 25);
BlacklistedDomains = (invoiceSettings?["BlacklistedDomains"] as List<object>)?.ConvertAll(d => d.ToString().ToLower()) ?? new List<string>();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -201,23 +223,43 @@ namespace Oxide.Plugins
protected override void LoadDefaultConfig() protected override void LoadDefaultConfig()
{ {
Config["LNDHubConnection"] = "lndhub://admin:password@sats.love/"; Config["Commands"] = new Dictionary<string, object>
Config["DiscordWebhookUrl"] = "https://discord.com/api/webhooks/your_webhook_url"; {
Config["CurrencyItemID"] = 1776460938; ["BuyCurrencyCommandName"] = "buyblood",
Config["BuyCurrencyCommandName"] = "buyblood"; ["BuyVipCommandName"] = "buyvip",
Config["SendCurrencyCommandName"] = "sendblood"; ["SendCurrencyCommandName"] = "sendblood"
Config["BuyVipCommandName"] = "buyvip"; };
Config["VipPrice"] = 1000; // Default price for VIP
Config["VipPermissionGroup"] = "vip";
Config["CurrencyName"] = "blood";
Config["SatsPerCurrencyUnit"] = 1; // Default value
Config["PricePerCurrencyUnit"] = 1; // Default value
// Default transaction timing settings Config["Discord"] = new Dictionary<string, object>
Config["CheckIntervalSeconds"] = 10; {
Config["InvoiceTimeoutSeconds"] = 300; ["DiscordChannelName"] = "mart",
Config["RetryDelaySeconds"] = 10; ["DiscordWebhookUrl"] = "https://discord.com/api/webhooks/your_webhook_url"
Config["MaxRetries"] = 25; };
Config["InvoiceSettings"] = new Dictionary<string, object>
{
["BlacklistedDomains"] = new List<string> { "example.com", "blacklisted.net" },
["CheckIntervalSeconds"] = 10,
["InvoiceTimeoutSeconds"] = 300,
["LNDHubConnection"] = "lndhub://admin:password@sats.love/",
["MaxRetries"] = 25,
["RetryDelaySeconds"] = 10
};
Config["VIPSettings"] = new Dictionary<string, object>
{
["VipPermissionGroup"] = "vip",
["VipPrice"] = 1000
};
Config["CurrencySettings"] = new Dictionary<string, object>
{
["CurrencyItemID"] = 1776460938,
["CurrencyName"] = "blood",
["PricePerCurrencyUnit"] = 1,
["SatsPerCurrencyUnit"] = 1,
["CurrencySkinID"] = 0
};
} }
private void Init() private void Init()
@ -248,6 +290,7 @@ namespace Oxide.Plugins
{ {
pendingInvoices.Clear(); pendingInvoices.Clear();
retryCounts.Clear(); retryCounts.Clear();
retryCounts404.Clear(); // Clear 404-specific retries
authToken = null; authToken = null;
} }
@ -271,7 +314,8 @@ namespace Oxide.Plugins
["FailedToFindBasePlayer"] = "Failed to find base player object for player {0}.", ["FailedToFindBasePlayer"] = "Failed to find base player object for player {0}.",
["FailedToCreateCurrencyItem"] = "Failed to create {0} item for player {1}.", ["FailedToCreateCurrencyItem"] = "Failed to create {0} item for player {1}.",
["AddedToVipGroup"] = "Player {0} added to VIP group '{1}'.", ["AddedToVipGroup"] = "Player {0} added to VIP group '{1}'.",
["InvoiceExpired"] = "Your invoice for {0} sats has expired. Please try again." // New message for expired invoice ["InvoiceExpired"] = "Your invoice for {0} sats has expired. Please try again.",
["BlacklistedDomain"] = "The domain '{0}' is currently blacklisted. Please use a different Lightning address."
}, this); }, this);
} }
@ -385,7 +429,58 @@ namespace Oxide.Plugins
{ {
PrintError($"Error checking invoice status: HTTP {code} (Not Found)"); PrintError($"Error checking invoice status: HTTP {code} (Not Found)");
PrintWarning($"Ensure the correct rHash: {rHash}"); PrintWarning($"Ensure the correct rHash: {rHash}");
callback(false, false);
// Initialize retry count for 404 if not present
if (!retryCounts404.ContainsKey(rHash))
{
retryCounts404[rHash] = 1;
}
else
{
retryCounts404[rHash]++;
}
// Check if retry count has exceeded the maximum allowed retries (5)
if (retryCounts404[rHash] <= Max404Retries)
{
PrintWarning($"Retrying invoice {rHash}. Attempt {retryCounts404[rHash]} of {Max404Retries}.");
// Schedule a retry after 5 seconds
timer.Once(RetryDelaySeconds404, () =>
{
CheckInvoicePaid(rHash, callback);
});
}
else
{
PrintWarning($"Max retries reached for invoice {rHash}. Marking as failed.");
// Remove the retry count as we've exceeded the max retries
retryCounts404.Remove(rHash);
// Optionally, notify the player about the failed payment
var invoice = pendingInvoices.Find(inv => inv.RHash == rHash);
if (invoice != null)
{
pendingInvoices.Remove(invoice);
invoice.Player.Reply(Lang("InvoiceExpired", invoice.Player.Id, invoice.Amount));
var logEntry = new BuyInvoiceLogEntry
{
SteamID = invoice.Player.Id,
InvoiceID = invoice.RHash,
IsPaid = false,
Timestamp = DateTime.UtcNow,
Amount = invoice.Amount,
CurrencyGiven = invoice.Type == PurchaseType.Currency,
VipGranted = invoice.Type == PurchaseType.Vip
};
LogBuyInvoice(logEntry);
}
// Finally, mark the payment as failed
callback(false, false);
}
return; return;
} }
@ -402,11 +497,22 @@ namespace Oxide.Plugins
if (jsonResponse != null && jsonResponse.ContainsKey("paid")) if (jsonResponse != null && jsonResponse.ContainsKey("paid"))
{ {
bool isPaid = (bool)jsonResponse["paid"]; bool isPaid = Convert.ToBoolean(jsonResponse["paid"]);
bool isPending = jsonResponse.ContainsKey("status") && jsonResponse["status"].ToString() == "pending"; bool isPending = jsonResponse.ContainsKey("status") && jsonResponse["status"].ToString().ToLower() == "pending";
if (isPaid) if (isPaid)
{ {
// Remove retry counts (both general and 404-specific) on successful payment
if (retryCounts.ContainsKey(rHash))
{
retryCounts.Remove(rHash);
}
if (retryCounts404.ContainsKey(rHash))
{
retryCounts404.Remove(rHash);
}
callback(true, false); callback(true, false);
} }
else if (isPending) else if (isPending)
@ -415,6 +521,7 @@ namespace Oxide.Plugins
} }
else else
{ {
// For other statuses, you might want to handle them differently
callback(false, false); callback(false, false);
} }
} }
@ -503,8 +610,47 @@ namespace Oxide.Plugins
string lightningAddress = args[1]; string lightningAddress = args[1];
// Check if the Lightning address is from a blacklisted domain
if (IsLightningAddressBlacklisted(lightningAddress))
{
string domain = GetDomainFromLightningAddress(lightningAddress);
player.Reply(Lang("BlacklistedDomain", player.Id, domain));
return;
}
var basePlayer = player.Object as BasePlayer; var basePlayer = player.Object as BasePlayer;
int currencyAmount = basePlayer.inventory.GetAmount(CurrencyItemID); if (basePlayer == null)
{
player.Reply(Lang("FailedToFindBasePlayer", player.Id, player.Name));
return;
}
int currencyAmount = 0;
// If CurrencySkinID is defined and greater than 0, only check for items with that skin ID
if (CurrencySkinID > 0)
{
var itemsWithSkin = basePlayer.inventory.FindItemsByItemID(CurrencyItemID);
foreach (var item in itemsWithSkin)
{
if (item.skin == CurrencySkinID)
{
currencyAmount += item.amount;
}
}
// If no matching items were found, inform the player and stop
if (currencyAmount == 0)
{
player.Reply($"You do not have any {CurrencyName} with the required skin ID.");
return;
}
}
else
{
// Check for all items with the given CurrencyItemID regardless of skin
currencyAmount = basePlayer.inventory.GetAmount(CurrencyItemID);
}
if (currencyAmount < amount) if (currencyAmount < amount)
{ {
@ -513,7 +659,7 @@ namespace Oxide.Plugins
} }
// Reserve the currency by immediately removing it from the player's inventory // Reserve the currency by immediately removing it from the player's inventory
int reservedAmount = ReserveCurrency(basePlayer, amount); int reservedAmount = ReserveCurrencyWithSkin(basePlayer, amount);
if (reservedAmount == 0) if (reservedAmount == 0)
{ {
player.Reply(Lang("FailedToReserveCurrency", player.Id)); player.Reply(Lang("FailedToReserveCurrency", player.Id));
@ -562,6 +708,47 @@ namespace Oxide.Plugins
}); });
} }
// Helper method to reserve currency with the specific skin ID
private int ReserveCurrencyWithSkin(BasePlayer player, int amount)
{
var items = player.inventory.FindItemsByItemID(CurrencyItemID);
int remaining = amount;
int reserved = 0;
foreach (var item in items)
{
// Check if the item matches the defined CurrencySkinID, if applicable
if (CurrencySkinID > 0 && item.skin != CurrencySkinID)
{
continue; // Skip items without the defined skin ID
}
if (item.amount > remaining)
{
item.UseItem(remaining);
reserved += remaining;
remaining = 0;
break;
}
else
{
reserved += item.amount;
remaining -= item.amount;
item.Remove();
}
}
if (remaining > 0)
{
// Rollback if unable to remove the full amount
PrintWarning($"Could not reserve the full amount of {CurrencyName}. {remaining} remaining.");
ReturnCurrency(player, reserved); // Return whatever was taken
return 0; // Indicate failure to reserve
}
return reserved; // Return the amount actually reserved
}
private void CmdBuyCurrency(IPlayer player, string command, string[] args) private void CmdBuyCurrency(IPlayer player, string command, string[] args)
{ {
if (!player.HasPermission("orangemart.buycurrency")) if (!player.HasPermission("orangemart.buycurrency"))
@ -586,7 +773,7 @@ namespace Oxide.Plugins
SendInvoiceToDiscord(player, invoiceResponse.PaymentRequest, amountSats, $"Buying {amount} {CurrencyName}"); SendInvoiceToDiscord(player, invoiceResponse.PaymentRequest, amountSats, $"Buying {amount} {CurrencyName}");
// Notify the player in chat to check the Discord channel // Notify the player in chat to check the Discord channel
player.Reply(Lang("InvoiceCreatedCheckDiscord", player.Id, "buycurrency")); player.Reply(Lang("InvoiceCreatedCheckDiscord", player.Id, DiscordChannelName));
// Add the pending invoice // Add the pending invoice
var pendingInvoice = new PendingInvoice var pendingInvoice = new PendingInvoice
@ -652,7 +839,7 @@ namespace Oxide.Plugins
SendInvoiceToDiscord(player, invoiceResponse.PaymentRequest, amountSats, $"Buying VIP Status"); SendInvoiceToDiscord(player, invoiceResponse.PaymentRequest, amountSats, $"Buying VIP Status");
// Notify the player in chat to check the Discord channel // Notify the player in chat to check the Discord channel
player.Reply(Lang("InvoiceCreatedCheckDiscord", player.Id, "buyvip")); player.Reply(Lang("InvoiceCreatedCheckDiscord", player.Id, DiscordChannelName));
// Add the pending invoice // Add the pending invoice
var pendingInvoice = new PendingInvoice var pendingInvoice = new PendingInvoice
@ -700,6 +887,27 @@ namespace Oxide.Plugins
}); });
} }
private bool IsLightningAddressBlacklisted(string lightningAddress)
{
string domain = GetDomainFromLightningAddress(lightningAddress);
if (string.IsNullOrEmpty(domain))
return false;
return BlacklistedDomains.Contains(domain.ToLower());
}
private string GetDomainFromLightningAddress(string lightningAddress)
{
if (string.IsNullOrEmpty(lightningAddress))
return null;
var parts = lightningAddress.Split('@');
if (parts.Length != 2)
return null;
return parts[1].ToLower();
}
private void QueryLightningAddressForInvoice(string lightningAddress, int satsAmount, Action<string> callback) private void QueryLightningAddressForInvoice(string lightningAddress, int satsAmount, Action<string> callback)
{ {
string[] parts = lightningAddress.Split('@'); string[] parts = lightningAddress.Split('@');
@ -936,8 +1144,13 @@ namespace Oxide.Plugins
var currencyItem = ItemManager.CreateByItemID(CurrencyItemID, amount); var currencyItem = ItemManager.CreateByItemID(CurrencyItemID, amount);
if (currencyItem != null) if (currencyItem != null)
{ {
if (CurrencySkinID > 0) // Check if a skinID is defined and apply it
{
currencyItem.skin = (ulong)CurrencySkinID;
}
basePlayer.GiveItem(currencyItem); basePlayer.GiveItem(currencyItem);
Puts($"Gave {amount} {CurrencyName} to player {player.Id}."); Puts($"Gave {amount} {CurrencyName} (skinID: {CurrencySkinID}) to player {player.Id}.");
} }
else else
{ {