C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using Oxide.Core;
using Oxide.Core.Database;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.SQLite.Libraries;
using Oxide.Core.Libraries;
using Oxide.Core.Plugins;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Linq;
using System.Threading.Tasks;
namespace Oxide.Plugins
{
[Info("AzSync", "theblobinc", "1.0.0")]
[Description("Asynchronously syncs economic and user data between Rust server and Azuriom website")]
public class AzSync : CovalencePlugin
{
#region Configuration
public class AzSyncConfig
{
[JsonProperty("Azuriom Settings")]
public AzuriomSettings Azuriom { get; set; } = new AzuriomSettings();
}
public class AzuriomSettings
{
[JsonProperty("API Base URL")]
public string BaseUrl { get; set; } = "https://yourazuriomsite.com/api/";
[JsonProperty("Site Key (ApiToken)")]
public string SiteKey { get; set; } = ""; // Site Key serves as the ApiToken
[JsonProperty("API IP Address")]
public string ApiIPAddress { get; set; } = "127.0.0.1"; // Default to localhost
[JsonProperty("API Port")]
public int ApiPort { get; set; } = 8181; // Default port (e.g., 8181)
}
public class TransactionRecord
{
[JsonProperty("timestamp")]
public string Timestamp { get; set; } // ISO 8601 format
[JsonProperty("transaction_type")]
public string TransactionType { get; set; } // "deposit", "withdrawal", "transfer", etc.[JsonProperty("steam_id")]
public string SteamId { get; set; } // Player initiating the transaction
[JsonProperty("target_steam_id")]
public string TargetSteamId { get; set; } // Player receiving funds (for transfers)
[JsonProperty("amount")]
public double Amount { get; set; }
}
public static class QueryStringParser
{
public static Dictionary<string, string> Parse(string query)
{
var queryDictionary = new Dictionary<string, string>();
if (string.IsNullOrWhiteSpace(query))
return queryDictionary;
// Remove the leading '?' if present
if (query.StartsWith("?"))
query = query.Substring(1);
var pairs = query.Split('&', StringSplitOptions.RemoveEmptyEntries);
foreach (var pair in pairs)
{
var kvp = pair.Split('=', 2);
if (kvp.Length == 2)
{
var key = Uri.UnescapeDataString(kvp[0]);
var value = Uri.UnescapeDataString(kvp[1]);
queryDictionary[key] = value;
}
else if (kvp.Length == 1)
{
var key = Uri.UnescapeDataString(kvp[0]);
queryDictionary[key] = string.Empty;
}
}
return queryDictionary;
}
}
private AzSyncConfig pluginConfig;
protected override void LoadDefaultConfig()
{
Puts("Creating a new configuration file.");
pluginConfig = new AzSyncConfig();
SaveConfig();
}
private void LoadConfigData()
{
pluginConfig = Config.ReadObject<AzSyncConfig>();
if (pluginConfig == null)
{
Puts("Could not load config, creating new default config.");
pluginConfig = new AzSyncConfig();
SaveConfig();
}
// Validate essential configurations
if (string.IsNullOrEmpty(pluginConfig.Azuriom.BaseUrl))
{
Puts("Azuriom BaseUrl is not set.Please configure it in the plugin config.");
}
if (string.IsNullOrEmpty(pluginConfig.Azuriom.SiteKey))
{
Puts("Azuriom Site Key is not set.Please configure it in the plugin config.");
}
if (string.IsNullOrEmpty(pluginConfig.Azuriom.ApiIPAddress))
{
Puts("API IP Address is not set.Defaulting to 127.0.0.1.");
pluginConfig.Azuriom.ApiIPAddress = "127.0.0.1";
SaveConfig();
}
if (pluginConfig.Azuriom.ApiPort <= 0 || pluginConfig.Azuriom.ApiPort > 65535)
{
Puts($"Invalid API Port '{pluginConfig.Azuriom.ApiPort}' in config.Defaulting to 8181.");
pluginConfig.Azuriom.ApiPort = 8181;
SaveConfig();
}
}
protected override void SaveConfig()
{
Config.WriteObject(pluginConfig, true);
}
#endregion
#region BalanceOperation Enum
/// <summary>
/// Enum representing the type of balance operation./// </summary>
public enum BalanceOperation
{
Add,
Subtract,
Set
}
#endregion
#region Fields and Properties
private Plugin EconomicsPlugin;
private bool isSyncing = false;
private HttpListener _httpListener;
private Core.SQLite.Libraries.SQLite Sqlite;
private Connection Sqlite_conn;
private Dictionary<(string path, string method), Action<HttpListenerRequest, Action<string>>> routeTable;
// Lock object for thread safety
private readonly object balanceLock = new object();
// **Fields for Ledger Management**
private string dbPath;
// Timer for consistency checks
private Timer consistencyTimer;
#endregion
#region Initialization
private void Init()
{
LoadConfigData();
// Define the path to the SQLite database file
dbPath = Path.Combine(Interface.Oxide.DataDirectory, "azsync-transactions.db");
// Retrieve the Sqlite library from Oxide
Sqlite = Interface.Oxide.GetLibrary<Core.SQLite.Libraries.SQLite>();
// Open the database connection
Sqlite_conn = Sqlite.OpenDb(dbPath, this);
if (Sqlite_conn == null)
{
Puts("Failed to open SQLite database connection.");
return;
}
// Initialize the SQLite ledger database
InitializeDatabase();
// **Reordered: Attempt to load the Economics plugin before loading balances**
EconomicsPlugin = plugins.Find("Economics");
if (EconomicsPlugin == null)
{
Puts("Economics plugin not found.AzSync requires the Economics plugin to function.");
return;
}
else
{
Puts("Economics plugin found and loaded successfully.");
}
// Load Economics plugin balances into SQLite
LoadEconomicsBalancesToDatabase();
// Initialize the route table
InitializeRouteTable();
// Start initial synchronization from website to in-game balances
FetchAndSyncBalancesFromWebsiteAsync();
// Start HTTP Listener for receiving data from Azuriom
StartHttpListener();
// Start the consistency verification timer (e.g., every 5 minutes)
consistencyTimer = timer.Repeat(300, 0, VerifyConsistency);
Puts("Started consistency verification timer.");
Puts("AzSync plugin initialized.");
}
private void Unload()
{
StopHttpListener();
if (Sqlite_conn != null)
{
Sqlite.CloseDb(Sqlite_conn);
Puts("SQLite connection closed.");
}
// Destroy the consistency timer
if (consistencyTimer != null)
{
consistencyTimer.Destroy();
Puts("Consistency verification timer stopped.");
}
Puts("AzSync plugin unloaded.");
}
#endregion
#region SQLite Ledger Management
/// <summary>
/// Initializes the SQLite database and creates the necessary tables if they don't exist./// </summary>
private void InitializeDatabase()
{
var createTransactionsTableSql = @"
CREATE TABLE IF NOT EXISTS transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
transaction_type TEXT NOT NULL,
steam_id TEXT NOT NULL,
target_steam_id TEXT,
amount REAL NOT NULL
);";
var createUserBalancesTableSql = @"
CREATE TABLE IF NOT EXISTS user_balances (
steam_id TEXT PRIMARY KEY,
balance REAL NOT NULL
);";
var createUserAccountsTableSql = @"
CREATE TABLE IF NOT EXISTS user_accounts (
steam_id TEXT PRIMARY KEY
-- Add other relevant fields if necessary
);";
try
{
// Create transactions table
Sqlite.ExecuteNonQuery(Core.Database.Sql.Builder.Append(createTransactionsTableSql), Sqlite_conn, (rows) =>
{
Puts("SQLite transactions table ensured.");
});
// Create user_balances table
Sqlite.ExecuteNonQuery(Core.Database.Sql.Builder.Append(createUserBalancesTableSql), Sqlite_conn, (rows) =>
{
Puts("SQLite user_balances table ensured.");
});
// Create user_accounts table
Sqlite.ExecuteNonQuery(Core.Database.Sql.Builder.Append(createUserAccountsTableSql), Sqlite_conn, (rows) =>
{
Puts("SQLite user_accounts table ensured.");
});
}
catch (Exception ex)
{
Puts($"Error creating tables: {ex.Message}");
}
}
/// <summary>
/// Inserts a new transaction record into the SQLite database./// </summary>
/// <param name="transaction">The transaction record to insert.</param>
private void InsertTransaction(TransactionRecord transaction)
{
var sql = Core.Database.Sql.Builder.Append(
"INSERT INTO transactions (timestamp, transaction_type, steam_id, target_steam_id, amount) VALUES (@0, @1, @2, @3, @4)",
transaction.Timestamp,
transaction.TransactionType,
transaction.SteamId,
transaction.TargetSteamId,
transaction.Amount);
try
{
Sqlite.Insert(sql, Sqlite_conn, rowsAffected =>
{
if (rowsAffected > 0)
{
Puts($"Transaction inserted: {transaction.TransactionType} of {transaction.Amount} by {transaction.SteamId}" +
(string.IsNullOrEmpty(transaction.TargetSteamId) ? "" : $" to {transaction.TargetSteamId}"));
}
else
{
Puts("Failed to insert transaction.");
}
});
}
catch (Exception ex)
{
Puts($"Error inserting transaction: {ex.Message}");
}
}
/// <summary>
/// Inserts or updates a user's balance in the SQLite database./// </summary>
/// <param name="steamId">The player's SteamID.</param>
/// <param name="balance">The new balance amount.</param>
/// <returns>True if the operation was successful; otherwise, false.</returns>
private bool UpsertUserBalance(string steamId, double balance)
{
var sql = Core.Database.Sql.Builder.Append(@"
INSERT INTO user_balances (steam_id, balance)
VALUES (@0, @1)
ON CONFLICT(steam_id) DO UPDATE SET balance = @1;",
steamId, balance);
try
{
Sqlite.Insert(sql, Sqlite_conn);
Puts($"Upserted balance for SteamID: {steamId} to {balance}.");
return true;
}
catch (Exception ex)
{
Puts($"Error upserting balance for SteamID {steamId}: {ex.Message}");
return false;
}
}
/// <summary>
/// Retrieves all player SteamIDs from the SQLite database./// </summary>
/// <param name="callback">The callback to execute with the results.</param>
private void GetAllPlayerSteamIds(Action<List<string>> callback)
{
var sql = Core.Database.Sql.Builder.Append("SELECT steam_id FROM user_balances");
Sqlite.Query(sql, Sqlite_conn, list =>
{
var steamIds = new List<string>();
foreach (var record in list)
{
if (record.ContainsKey("steam_id"))
{
steamIds.Add(record["steam_id"].ToString());
}
}
callback(steamIds);
});
}
/// <summary>
/// Retrieves a user's balance from the SQLite database./// </summary>
private void GetDatabaseBalance(string steamId, Action<double> callback)
{
var sql = Core.Database.Sql.Builder.Append("SELECT balance FROM user_balances WHERE steam_id = @0", steamId);
Sqlite.Query(sql, Sqlite_conn, list =>
{
double balance = 0.0;
if (list != null && list.Count > 0)
{
var record = list[0];
if (record.ContainsKey("balance") && double.TryParse(record["balance"].ToString(), out double dbBalance))
{
balance = dbBalance;
}
}
callback(balance);
});
}
/// <summary>
/// Adds a new user to the user_balances table./// </summary>
private void AddUserToDatabase(string steamId, double initialBalance = 0.0)
{
var sql = Core.Database.Sql.Builder.Append(@"
INSERT INTO user_balances (steam_id, balance)
VALUES (@0, @1)
ON CONFLICT(steam_id) DO NOTHING;",
steamId, initialBalance);
try
{
Sqlite.Insert(sql, Sqlite_conn);
Puts($"Added user to database: SteamID {steamId} with balance {initialBalance}.");
}
catch (Exception ex)
{
Puts($"Error adding user {steamId} to database: {ex.Message}");
}
}
/// <summary>
/// Checks if a user exists in the user_balances table./// </summary>
/// <param name="steamId">The SteamID to check for.</param>
/// <param name="callback">The callback to execute with the result.</param>
private void UserExistsInDatabase(string steamId, Action<bool> callback)
{
var sql = Core.Database.Sql.Builder.Append("SELECT COUNT(*) AS count FROM user_balances WHERE steam_id = @0", steamId);
Sqlite.Query(sql, Sqlite_conn, list =>
{
bool exists = false;
if (list != null && list.Count > 0)
{
var record = list[0];
if (record.ContainsKey("count") && int.TryParse(record["count"].ToString(), out int count))
{
exists = count > 0;
}
}
callback(exists);
});
}
/// <summary>
/// Asynchronously retrieves transactions from the SQLite database based on optional time range filters and pagination./// </summary>
private void GetTransactions(DateTime? startTime, DateTime? endTime, int limit, int offset, Action<List<TransactionRecord>> callback)
{
var sqlQuery = "SELECT timestamp, transaction_type, steam_id, target_steam_id, amount FROM transactions WHERE 1=1";
var parameters = new List<object>();
int paramIndex = 0;
if (startTime.HasValue)
{
sqlQuery += $" AND timestamp >= @{paramIndex}";
parameters.Add(startTime.Value.ToString("o")); // ISO 8601 format
paramIndex++;
}
if (endTime.HasValue)
{
sqlQuery += $" AND timestamp <= @{paramIndex}";
parameters.Add(endTime.Value.ToString("o")); // ISO 8601 format
paramIndex++;
}
sqlQuery += $" ORDER BY timestamp DESC LIMIT @{paramIndex} OFFSET @{paramIndex + 1};";
parameters.Add(limit);
parameters.Add(offset);
var sql = Core.Database.Sql.Builder.Append(sqlQuery, parameters.ToArray());
Sqlite.Query(sql, Sqlite_conn, list =>
{
if (list == null || list.Count == 0)
{
Puts("No transactions found or an error occurred while fetching transactions.");
callback(new List<TransactionRecord>());
return;
}
var results = new List<TransactionRecord>();
foreach (var dict in list)
{
var record = new TransactionRecord
{
Timestamp = dict["timestamp"].ToString(),
TransactionType = dict["transaction_type"].ToString(),
SteamId = dict["steam_id"].ToString(),
TargetSteamId = dict.ContainsKey("target_steam_id") ? dict["target_steam_id"]?.ToString() : "",
Amount = Convert.ToDouble(dict["amount"])
};
results.Add(record);
}
callback(results);
});
}
/// <summary>
/// Loads Economics plugin balances into SQLite database./// </summary>
private void LoadEconomicsBalancesToDatabase()
{
Puts("Loading balances from Economics plugin to SQLite database.");
if (EconomicsPlugin == null)
{
Puts("EconomicsPlugin is null.Cannot load balances.");
return;
}
// Retrieve all players from the Economics plugin
var allPlayers = EconomicsPlugin.Call("GetAllPlayers") as List<IPlayer>;
if (allPlayers == null)
{
Puts("EconomicsPlugin returned null or invalid data for GetAllPlayers.");
return;
}
foreach (var player in allPlayers)
{
if (player == null)
{
Puts("Encountered a null player in EconomicsPlugin.GetAllPlayers().Skipping.");
continue;
}
string steamId = player.Id;
object balanceObj = EconomicsPlugin.Call("GetBalance", player);
double balance = balanceObj is double b ? b : 0.0;
// Upsert the user's balance into the SQLite database
UpsertUserBalance(steamId, balance);
Puts($"Loaded balance for SteamID: {steamId} with amount: {balance}.");
}
Puts("Completed loading Economics balances into SQLite database.");
}
#endregion
#region HTTP Listener
private void StartHttpListener()
{
try
{
_httpListener = new HttpListener();
// Retrieve the configured IP address and port
string apiIp = pluginConfig.Azuriom.ApiIPAddress;
int apiPort = pluginConfig.Azuriom.ApiPort;
// Validate IP address format
if (!IPAddress.TryParse(apiIp, out IPAddress ipAddress))
{
Puts($"Invalid API IP Address '{apiIp}' in config.Defaulting to 127.0.0.1.");
apiIp = "127.0.0.1";
}
// Validate port number
if (apiPort <= 0 || apiPort > 65535)
{
Puts($"Invalid API Port '{apiPort}' in config.Defaulting to 8181.");
apiPort = 8181;
}
// Construct the prefix using the configured IP address and port
string prefix = $"http://{apiIp}:{apiPort}/azsync/";
_httpListener.Prefixes.Add(prefix);
_httpListener.Start();
_httpListener.BeginGetContext(OnRequestReceived, null);
Puts($"HTTP listener started on {prefix}");
}
catch (HttpListenerException hlex)
{
Puts($"Failed to start HTTP listener: {hlex.Message}");
}
catch (Exception ex)
{
Puts($"Unexpected error starting HTTP listener: {ex.Message}");
}
}
private void StopHttpListener()
{
try
{
if (_httpListener != null)
{
_httpListener.Stop();
_httpListener.Close();
_httpListener = null;
Puts("HTTP listener stopped.");
}
}
catch (Exception ex)
{
Puts($"Error stopping HTTP listener: {ex.Message}");
}
}
private void InitializeRouteTable()
{
routeTable = new Dictionary<(string path, string method), Action<HttpListenerRequest, Action<string>>>()
{
{ ("/azsync/set-balance", "POST"), HandleBalanceUpdateRequest },
{ ("/azsync/get-balance", "GET"), HandleGetBalanceRequest },
{ ("/azsync/user-check", "GET"), HandleUserCheckRequest },
{ ("/azsync/reset-balance", "POST"), HandleResetBalanceRequest },
{ ("/azsync/get-transactions", "GET"), HandleGetTransactions },
{ ("/azsync/add-user", "POST"), HandleAddUserRequest },
{ ("/azsync/add-balance", "POST"), HandleAddBalanceRequest },
{ ("/azsync/subtract-balance", "POST"), HandleSubtractBalanceRequest },
{ ("/azsync/get-all-balances", "GET"), HandleGetAllBalancesRequest }
// Add more routes here as needed
};
}
private void OnRequestReceived(IAsyncResult result)
{
if (_httpListener == null || !_httpListener.IsListening) return;
HttpListenerContext context = null;
try
{
context = _httpListener.EndGetContext(result);
}
catch (Exception)
{
// Handle if needed
return;
}
finally
{
if (_httpListener.IsListening)
{
_httpListener.BeginGetContext(OnRequestReceived, null);
}
}
var request = context.Request;
var response = context.Response;
// API key verification
if (request.Headers["Azuriom-Link-Token"] == null ||
!request.Headers["Azuriom-Link-Token"].Equals(pluginConfig.Azuriom.SiteKey))
{
Puts($"Unauthorized access attempt from {request.RemoteEndPoint}");
response.StatusCode = (int)HttpStatusCode.Unauthorized;
string unauthorizedResponse = "{ \"error\": \"Unauthorized access\" }";
SendResponseAsync(response, unauthorizedResponse);
return;
}
// Route the request
var path = request.Url.AbsolutePath.ToLower();
var method = request.HttpMethod.ToUpper();
try
{
var key = (path, method);
if (routeTable.ContainsKey(key))
{
routeTable[key](request, responseString =>
{
// Send the response
SendResponseAsync(response, responseString);
});
}
else
{
// Endpoint not found
response.StatusCode = (int)HttpStatusCode.NotFound;
string responseString = "{ \"error\": \"Endpoint not found\" }";
SendResponseAsync(response, responseString);
}
}
catch (Exception ex)
{
Puts($"Error processing request: {ex.Message}");
response.StatusCode = (int)HttpStatusCode.InternalServerError;
string responseString = "{ \"error\": \"Internal server error\" }";
SendResponseAsync(response, responseString);
}
}
private async void SendResponseAsync(HttpListenerResponse response, string responseString)
{
byte[] buffer = Encoding.UTF8.GetBytes(responseString);
response.ContentLength64 = buffer.Length;
response.ContentType = "application/json";
try
{
await response.OutputStream.WriteAsync(buffer, 0, buffer.Length);
}
catch (Exception)
{
// Handle exception if needed
}
finally
{
response.Close();
}
}
// Route handlers
private void HandleBalanceUpdateRequest(HttpListenerRequest request, Action<string> responseCallback)
{
using (var reader = new StreamReader(request.InputStream, request.ContentEncoding))
{
string body = reader.ReadToEnd();
var balanceData = JsonConvert.DeserializeObject<Dictionary<string, string>>(body);
string responseString = HandleBalanceUpdate(balanceData);
responseCallback(responseString);
}
}
/// <summary>
/// Handles the ResetBalance HTTP request to reset a user's balance./// </summary>
private void HandleResetBalanceRequest(HttpListenerRequest request, Action<string> responseCallback)
{
using (var reader = new StreamReader(request.InputStream, request.ContentEncoding))
{
string body = reader.ReadToEnd();
var resetData = JsonConvert.DeserializeObject<Dictionary<string, string>>(body);
// Validate presence of "game_id"
if (!resetData.ContainsKey("game_id") || string.IsNullOrWhiteSpace(resetData["game_id"]))
{
Puts("ResetBalance request missing or has empty 'game_id'.");
responseCallback("{ \"error\": \"Missing or empty 'game_id' in request body.\" }");
return;
}
string steamId = resetData["game_id"];
// Reset balance to zero using 'Set' operation
bool setSuccess = SetEconomicsBalance(steamId, 0.0);
if (!setSuccess)
{
Puts($"Failed to reset balance for SteamID: {steamId} via Economics plugin.");
responseCallback("{ \"error\": \"Failed to reset balance via Economics plugin.\" }");
return;
}
// Find the player in-game
var player = covalence.Players.FindPlayerById(steamId);
if (player != null)
{
Puts($"Reset balance for {steamId} to 0.");
// Send the reset balance to the website
SendBalanceToWebsite(player, 0.0, "set", () =>
{
// Sync the balance from the website to confirm
SyncPlayerBalanceWithWebsite(steamId);
});
// Add to Transaction Ledger
AddTransactionToLedger("reset_balance", steamId, 0.0, "Balance reset via API");
responseCallback("{ \"status\": \"Balance reset successfully.\" }");
}
else
{
// Player is offline; balance is set in local storage only
Puts($"Player {steamId} is offline.Balance reset to 0 in database.");
// Even if the player is offline, send the balance to website and sync
SendBalanceToWebsite(null, 0.0, "set", () =>
{
SyncPlayerBalanceWithWebsite(steamId);
});
// Add to Transaction Ledger
AddTransactionToLedger("reset_balance_offline", steamId, 0.0, "Balance reset via API while offline");
responseCallback("{ \"status\": \"Balance reset successfully.\" }");
}
}
}
private void HandleGetBalanceRequest(HttpListenerRequest request, Action<string> responseCallback)
{
var queryParams = QueryStringParser.Parse(request.Url.Query);
// Validate presence of "game_id"
if (!queryParams.ContainsKey("game_id") || string.IsNullOrWhiteSpace(queryParams["game_id"]))
{
Puts("GetBalance request missing or has empty 'game_id'.");
responseCallback("{ \"error\": \"Missing or empty 'game_id' in query parameters.\" }");
return;
}
string steamId = queryParams["game_id"];
// Retrieve balance from SQLite database asynchronously
GetDatabaseBalance(steamId, balance =>
{
Puts($"Retrieved balance for SteamID: {steamId} - Balance: {balance}");
string jsonResponse = $"{{ \"steam_id\": \"{steamId}\", \"balance\": {balance} }}";
responseCallback(jsonResponse);
});
}
private void HandleUserCheckRequest(HttpListenerRequest request, Action<string> responseCallback)
{
HandleUserCheck(responseCallback);
}
private void HandleGetTransactions(HttpListenerRequest request, Action<string> responseCallback)
{
var queryParams = QueryStringParser.Parse(request.Url.Query);
DateTime? startTime = null;
DateTime? endTime = null;
int limit = 100; // Default limit
int offset = 0; // Default offset
// Parse 'start_time' if provided
if (queryParams.ContainsKey("start_time") && !string.IsNullOrWhiteSpace(queryParams["start_time"]))
{
if (DateTime.TryParse(queryParams["start_time"], null, System.Globalization.DateTimeStyles.RoundtripKind, out DateTime parsedStart))
{
startTime = parsedStart.ToUniversalTime();
}
else
{
Puts($"Invalid 'start_time' format received: {queryParams["start_time"]}");
responseCallback("{ \"error\": \"Invalid 'start_time' format.Use ISO 8601 format.\" }");
return;
}
}
// Parse 'end_time' if provided
if (queryParams.ContainsKey("end_time") && !string.IsNullOrWhiteSpace(queryParams["end_time"]))
{
if (DateTime.TryParse(queryParams["end_time"], null, System.Globalization.DateTimeStyles.RoundtripKind, out DateTime parsedEnd))
{
endTime = parsedEnd.ToUniversalTime();
}
else
{
Puts($"Invalid 'end_time' format received: {queryParams["end_time"]}");
responseCallback("{ \"error\": \"Invalid 'end_time' format.Use ISO 8601 format.\" }");
return;
}
}
// Parse 'limit' if provided
if (queryParams.ContainsKey("limit") && !string.IsNullOrWhiteSpace(queryParams["limit"]))
{
if (int.TryParse(queryParams["limit"], out int parsedLimit))
{
if (parsedLimit > 0 && parsedLimit <= 1000)
{
limit = parsedLimit;
}
else
{
Puts($"Invalid 'limit' value received: {queryParams["limit"]}.Must be between 1 and 1000.");
responseCallback("{ \"error\": \"Invalid 'limit' value.Must be between 1 and 1000.\" }");
return;
}
}
else
{
Puts($"Invalid 'limit' format received: {queryParams["limit"]}");
responseCallback("{ \"error\": \"Invalid 'limit' format.Must be an integer.\" }");
return;
}
}
// Parse 'offset' if provided
if (queryParams.ContainsKey("offset") && !string.IsNullOrWhiteSpace(queryParams["offset"]))
{
if (int.TryParse(queryParams["offset"], out int parsedOffset))
{
if (parsedOffset >= 0)
{
offset = parsedOffset;
}
else
{
Puts($"Invalid 'offset' value received: {queryParams["offset"]}.Must be 0 or greater.");
responseCallback("{ \"error\": \"Invalid 'offset' value.Must be 0 or greater.\" }");
return;
}
}
else
{
Puts($"Invalid 'offset' format received: {queryParams["offset"]}");
responseCallback("{ \"error\": \"Invalid 'offset' format.Must be an integer.\" }");
return;
}
}
// Retrieve transactions from SQLite database asynchronously
lock (balanceLock) // Ensure thread-safe access
{
GetTransactions(startTime, endTime, limit, offset, filteredTransactions =>
{
// Serialize the filtered transactions to JSON
string jsonResponse = JsonConvert.SerializeObject(filteredTransactions, Formatting.Indented);
Puts($"Fetched {filteredTransactions.Count} transaction(s) from the ledger between " +
$"{(startTime.HasValue ? startTime.Value.ToString("o") : "beginning")} and " +
$"{(endTime.HasValue ? endTime.Value.ToString("o") : "now")} with limit {limit} and offset {offset}.");
responseCallback(jsonResponse);
});
}
}
// New route handlers
/// <summary>
/// Handles the AddUser HTTP request to add a new user./// </summary>
private void HandleAddUserRequest(HttpListenerRequest request, Action<string> responseCallback)
{
using (var reader = new StreamReader(request.InputStream, request.ContentEncoding))
{
string body = reader.ReadToEnd();
var userData = JsonConvert.DeserializeObject<Dictionary<string, string>>(body);
// Validate required fields
if (!userData.ContainsKey("steam_id"))
{
Puts("AddUser request missing 'steam_id'.");
responseCallback("{ \"error\": \"Missing 'steam_id' in request body.\" }");
return;
}
string steamId = userData["steam_id"];
// Optional: Initial balance
double initialBalance = 0.0;
if (userData.ContainsKey("balance"))
{
if (!double.TryParse(userData["balance"], out initialBalance))
{
Puts($"Invalid 'balance' value received: {userData["balance"]}");
responseCallback("{ \"error\": \"Invalid 'balance' value.Must be a number.\" }");
return;
}
}
// Check if user already exists in the database
UserExistsInDatabase(steamId, exists =>
{
if (exists)
{
Puts($"User {steamId} already exists in the database.");
responseCallback("{ \"error\": \"User already exists.\" }");
return;
}
// Add user to SQLite database
UpsertUserBalance(steamId, initialBalance);
Puts($"Added user {steamId} with balance {initialBalance} to the database.");
// Record the addition in the transaction ledger
AddTransactionToLedger("add_user", steamId, initialBalance, "User added via API");
responseCallback("{ \"status\": \"User added successfully.\" }");
});
}
}
/// <summary>
/// Handles the AddBalance HTTP request to add balance to an existing user./// </summary>
private void HandleAddBalanceRequest(HttpListenerRequest request, Action<string> responseCallback)
{
using (var reader = new StreamReader(request.InputStream, request.ContentEncoding))
{
string body = reader.ReadToEnd();
var data = JsonConvert.DeserializeObject<Dictionary<string, string>>(body);
// Validate presence of "steam_id" and "amount"
if (!data.ContainsKey("steam_id") || !data.ContainsKey("amount"))
{
Puts("AddBalance request missing 'steam_id' or 'amount'.");
responseCallback("{ \"error\": \"Missing 'steam_id' or 'amount' in request body.\" }");
return;
}
string steamId = data["steam_id"];
if (!double.TryParse(data["amount"], out double amount) || amount <= 0)
{
Puts($"Invalid 'amount' value received: {data["amount"]}");
responseCallback("{ \"error\": \"Invalid 'amount' value.Must be a positive number.\" }");
return;
}
// Check if user exists in the database
UserExistsInDatabase(steamId, exists =>
{
if (!exists)
{
Puts($"User {steamId} does not exist in the database.");
responseCallback("{ \"error\": \"User does not exist.\" }");
return;
}
// Retrieve current balance from the database
GetDatabaseBalance(steamId, currentBalance =>
{
double newBalance = currentBalance + amount;
// Update the balance in the SQLite database
UpsertUserBalance(steamId, newBalance);
Puts($"Added {amount} to balance for SteamID: {steamId}.New balance: {newBalance}.");
// Find if the user is online to update in-game balance
var player = covalence.Players.FindPlayerById(steamId);
if (player != null)
{
bool economicsSuccess = SetEconomicsBalance(steamId, newBalance);
if (economicsSuccess)
{
Puts($"Added {amount} to in-game balance for {steamId}.");
SendBalanceToWebsite(player, amount, "deposit");
}
else
{
Puts($"Failed to add balance via Economics plugin for {steamId}.");
responseCallback("{ \"error\": \"Failed to add balance via Economics plugin.\" }");
return;
}
}
else
{
Puts($"User {steamId} is offline.Balance added to database only.");
}
// Record the addition in the transaction ledger
AddTransactionToLedger("add_balance", steamId, amount, "Balance added via API");
responseCallback("{ \"status\": \"Balance added successfully.\" }");
});
});
}
}
private void HandleGetAllBalancesRequest(HttpListenerRequest request, Action<string> responseCallback)
{
GetAllPlayerBalances(allBalances =>
{
string jsonResponse = JsonConvert.SerializeObject(allBalances, Formatting.Indented);
responseCallback(jsonResponse);
});
}
/// <summary>
/// Handles the SubtractBalance HTTP request to subtract balance from an existing user./// </summary>
private void HandleSubtractBalanceRequest(HttpListenerRequest request, Action<string> responseCallback)
{
using (var reader = new StreamReader(request.InputStream, request.ContentEncoding))
{
string body = reader.ReadToEnd();
var data = JsonConvert.DeserializeObject<Dictionary<string, string>>(body);
// Validate presence of "steam_id" and "amount"
if (!data.ContainsKey("steam_id") || !data.ContainsKey("amount"))
{
Puts("SubtractBalance request missing 'steam_id' or 'amount'.");
responseCallback("{ \"error\": \"Missing 'steam_id' or 'amount' in request body.\" }");
return;
}
string steamId = data["steam_id"];
if (!double.TryParse(data["amount"], out double amount) || amount <= 0)
{
Puts($"Invalid 'amount' value received: {data["amount"]}");
responseCallback("{ \"error\": \"Invalid 'amount' value.Must be a positive number.\" }");
return;
}
// Check if user exists in the database
UserExistsInDatabase(steamId, exists =>
{
if (!exists)
{
Puts($"User {steamId} does not exist in the database.");
responseCallback("{ \"error\": \"User does not exist.\" }");
return;
}
// Retrieve current balance from the database
GetDatabaseBalance(steamId, currentBalance =>
{
if (currentBalance < amount)
{
Puts($"User {steamId} has insufficient balance.Current: {currentBalance}, Attempted to subtract: {amount}");
responseCallback("{ \"error\": \"Insufficient balance.\" }");
return;
}
double newBalance = currentBalance - amount;
// Update the balance in the SQLite database
UpsertUserBalance(steamId, newBalance);
Puts($"Subtracted {amount} from balance for SteamID: {steamId}.New balance: {newBalance}.");
// Find if the user is online to update in-game balance
var player = covalence.Players.FindPlayerById(steamId);
if (player != null)
{
bool economicsSuccess = SetEconomicsBalance(steamId, newBalance);
if (economicsSuccess)
{
Puts($"Subtracted {amount} from in-game balance for {steamId}.");
SendBalanceToWebsite(player, amount, "withdrawal");
}
else
{
Puts($"Failed to subtract balance via Economics plugin for {steamId}.");
responseCallback("{ \"error\": \"Failed to subtract balance via Economics plugin.\" }");
return;
}
}
else
{
Puts($"User {steamId} is offline.Balance subtracted from database only.");
}
// Record the subtraction in the transaction ledger
AddTransactionToLedger("subtract_balance", steamId, amount, "Balance subtracted via API");
responseCallback("{ \"status\": \"Balance subtracted successfully.\" }");
});
});
}
}
#endregion
#region Handling Incoming Data
private string HandleBalanceUpdate(Dictionary<string, string> balanceData)
{
// Validate presence of "game_id"
if (!balanceData.ContainsKey("game_id"))
{
Puts("Balance update request missing 'game_id'.");
return "{ \"error\": \"Missing 'game_id' in balance data.\" }";
}
string steamId = balanceData["game_id"];
// Validate presence of "balance"
if (balanceData.ContainsKey("balance"))
{
if (double.TryParse(balanceData["balance"], out double newBalance))
{
Puts($"Received balance update for SteamID: {steamId} with new balance: {newBalance}");
// Updated call with callback
SetPlayerBalance(steamId, newBalance, BalanceOperation.Set, success =>
{
if (success)
{
Puts($"Successfully set balance for SteamID: {steamId} to {newBalance}.");
}
else
{
Puts($"Failed to set balance for SteamID: {steamId}.");
}
});
// Find the player in-game
var player = covalence.Players.FindPlayerById(steamId);
if (player != null)
{
double currentBalance = GetEconomicsBalance(steamId);
// ... [Code preview truncated due to length. Full source in attached file] ...