C#:
/***********************************************************************************************************************/
/*** DO NOT edit this file! Edit the files under `oxide/config` and/or `oxide/lang`, created once plugin has loaded. ***/
/***********************************************************************************************************************/
using System.Collections.Generic;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core;
using Oxide.Core.Libraries;
using System;
using System.Text;
using System.Security.Cryptography;
namespace Oxide.Plugins
{
[Info("Tip4serv", "Murgator & Duster", "1.5.1")]
[Description("Allows Admin to monetize their 7 Days to die & Rust server from their Tip4serv store")]
public class Tip4serv : CovalencePlugin
{
private class PluginConfig
{
public int request_interval_in_minutes;
public string configkey;
public string order_received_text;
}
[Serializable]
public class Payment
{
public PaymentCmd[] cmds;
public string player;
public string username;
public string action;
public string id;
public string steam_id;
}
[Serializable]
public class PaymentCmd
{
public string str;
public int id;
public string state;
}
[Serializable]
public class PaymentUpdate
{
public string action;
public Dictionary<string, string> cmds = new Dictionary<string, string>(); // cmd id -> "Executed" / "Not Executed"
}
private const string STATUS_EXECUTED = "Executed";
private const string STATUS_NOT_EXECUTED = "Not Executed";
private const string STATE_MUST_BE_ONLINE = "Must be online";
private const string API_BASE = "https://api.tip4serv.com/v1/store/server/";
private const string RESPONSE_FILE = "tip4serv_response";
private String key_msg = "Please set the config key to a valid key in your config/Tip4Serv.json file. Make sure you have copied the entire key on Tip4Serv.com (Ctrl+A then CTRL+C)";
private bool Stopped = false;
private bool Busy = false;
private Timer PaymentTimer;
private PluginConfig config;
protected override void LoadDefaultConfig()
{
LogWarning("Creating a new configuration file");
Config.WriteObject(GetDefaultConfig(), true);
}
private PluginConfig GetDefaultConfig()
{
return new PluginConfig
{
request_interval_in_minutes = 1,
configkey = "YOUR_CONFIG_KEY",
order_received_text = "[#cyan][Tip4serv][/#] You have received your order. Thank you !"
};
}
private Dictionary<String, IPlayer> GetPlayers()
{
IEnumerable<IPlayer> players = covalence.Players.Connected;
Dictionary<String, IPlayer> tip4customers = new Dictionary<string, IPlayer>();
foreach (IPlayer player in players)
{
tip4customers[player.Id] = player;
}
return tip4customers;
}
private void Loaded()
{
#if !SEVENDAYSTODIE && !RUST
LogError("This plugin only works for the 7 Days to Die or Rust Game");
Stopped = true;
#else
Tip4Print("Tip4serv plugin has started");
config = Config.ReadObject<PluginConfig>();
#endif
}
private void Unload()
{
key_msg = null;
if (PaymentTimer != null && !PaymentTimer.Destroyed)
PaymentTimer.Destroy();
}
void OnServerInitialized()
{
if (!Stopped)
{
//check Tip4serv connection on script start
string[] key_part = config.configkey.Split('.');
if (key_part.Length != 3)
{
Tip4Print(key_msg);
return;
}
CheckConnection(key_part, GetUnixTime());
PaymentTimer = timer.In((float)config.request_interval_in_minutes * 60f, () => PaymentChecker());
}
}
private void PaymentChecker()
{
string[] key_part = config.configkey.Split('.');
if (key_part.Length != 3)
{
Tip4Print(key_msg);
RescheduleTimer();
return;
}
if (Busy)
{
// previous cycle is still executing commands, skip this tick
RescheduleTimer();
return;
}
check_pending_commands(key_part, GetUnixTime());
RescheduleTimer();
}
private void RescheduleTimer()
{
if (PaymentTimer != null && !PaymentTimer.Destroyed)
PaymentTimer.Destroy();
PaymentTimer = timer.In((float)config.request_interval_in_minutes * 60f, () => PaymentChecker());
}
private string GetUnixTime()
{
long unixTime = ((DateTimeOffset)DateTime.UtcNow).ToUnixTimeSeconds();
return unixTime.ToString();
}
private void Tip4Print(string content)
{
LogWarning(content);
}
private string ServerId(string[] key_parts)
{
return key_parts[0];
}
private Dictionary<string, string> BuildHeaders(string timestamp)
{
//HMAC calculation
string[] key_parts = config.configkey.Split('.');
string HMAC = calculateHMAC(key_parts, timestamp);
return new Dictionary<string, string>
{
{ "Authorization", "Bearer SERVER_KEY:" + HMAC },
{ "Content-Type", "application/json" }
};
}
// Connection check on plugin start (GET on the commands route)
private void CheckConnection(string[] key_parts, string timestamp)
{
string url = API_BASE + ServerId(key_parts) + "/commands?time=" + timestamp;
Dictionary<string, string> headers = BuildHeaders(timestamp);
webrequest.Enqueue(url, null, (code, response) =>
{
if (code == 200)
Tip4Print("Server has been successfully connected");
else
Tip4Print("Tip4serv connection failed (HTTP " + code + "). Please check your config key. " + (response ?? ""));
}, this, RequestMethod.GET, headers, 15f);
}
private void check_pending_commands(string[] key_parts, string timestamp)
{
string commandsUrl = API_BASE + ServerId(key_parts) + "/commands?time=" + timestamp;
Dictionary<string, string> headers = BuildHeaders(timestamp);
//GET pending commands
webrequest.Enqueue(commandsUrl, null, (code, HTTPresponse) =>
{
if (code != 200 || string.IsNullOrEmpty(HTTPresponse))
{
//Tip4Print("Tip4serv API is temporarily unavailable (HTTP " + code + "). Will retry on the next cycle.");
return;
}
List<Payment> payments;
try
{
payments = Utility.ConvertFromJson<List<Payment>>(HTTPresponse);
}
catch (Exception)
{
//Tip4Print("Tip4serv returned an unexpected response: " + HTTPresponse);
return;
}
if (payments == null || payments.Count == 0)
return;
// ledger of already-executed (payment -> cmd id -> status), persisted to survive a failed update POST
Dictionary<string, PaymentUpdate> ledger = LoadLedger();
Dictionary<string, PaymentUpdate> update = new Dictionary<string, PaymentUpdate>();
Dictionary<String, IPlayer> players = GetPlayers();
// commands that still need to be executed this cycle (spaced 1s apart)
List<ExecItem> toExecute = new List<ExecItem>();
for (int i = 0; i < payments.Count; i++)
{
Payment p = payments[i];
if (p == null || string.IsNullOrEmpty(p.id))
continue;
PaymentUpdate pu = new PaymentUpdate { action = p.action };
IPlayer player_infos = checkifPlayerIsLoaded(p.steam_id, players);
if (player_infos != null)
player_infos.Message(config.order_received_text);
if (p.cmds != null)
{
for (int j = 0; j < p.cmds.Length; j++)
{
PaymentCmd cmd = p.cmds[j];
if (cmd == null || cmd.str == null)
continue;
string cmdId = cmd.id.ToString();
// already executed in a previous cycle but not yet acked -> keep it Executed, do not run again
if (AlreadyExecuted(ledger, p.id, cmdId))
{
pu.cmds[cmdId] = STATUS_EXECUTED;
continue;
}
bool mustBeOnline = cmd.state == STATE_MUST_BE_ONLINE || cmd.str.Contains("{");
if (player_infos == null && mustBeOnline)
{
// player offline and command needs them online -> stays IN PROGRESS on Tip4serv
pu.cmds[cmdId] = STATUS_NOT_EXECUTED;
continue;
}
string finalCmd = cmd.str;
if (player_infos != null)
{
#if SEVENDAYSTODIE
finalCmd = finalCmd.Replace("{7dtd_username}", player_infos.Name);
#elif RUST
finalCmd = finalCmd.Replace("{rust_username}", player_infos.Name);
#endif
}
toExecute.Add(new ExecItem { paymentId = p.id, cmdId = cmdId, command = finalCmd });
}
}
update[p.id] = pu;
}
// Execute queued commands with a minimum 1s delay between each, then POST the statuses back
Busy = true;
ProcessQueue(toExecute, 0, update, key_parts);
}, this, RequestMethod.GET, headers, 15f);
}
private class ExecItem
{
public string paymentId;
public string cmdId;
public string command;
}
// Executes commands one by one, at least 1 second apart, then sends the status update
private void ProcessQueue(List<ExecItem> queue, int index, Dictionary<string, PaymentUpdate> update, string[] key_parts)
{
if (index >= queue.Count)
{
SaveLedger(update);
SendUpdate(update, key_parts);
Busy = false;
return;
}
ExecItem item = queue[index];
// an invalid command must not prevent the following ones from running
try
{
exe_command(item.command, new string[] { });
}
catch (Exception e)
{
Tip4Print("Tip4serv command failed but continuing: " + item.command + " (" + e.Message + ")");
}
// mark it executed (we attempted delivery) so it is not sent forever
PaymentUpdate pu;
if (update.TryGetValue(item.paymentId, out pu))
pu.cmds[item.cmdId] = STATUS_EXECUTED;
// schedule the next command at least 1 second later
timer.In(1f, () => ProcessQueue(queue, index + 1, update, key_parts));
}
// POST the executed/not-executed statuses back to the new route
private void SendUpdate(Dictionary<string, PaymentUpdate> update, string[] key_parts)
{
if (update.Count == 0)
return;
string timestamp = GetUnixTime();
string url = API_BASE + ServerId(key_parts) + "/commands?time=" + timestamp;
Dictionary<string, string> headers = BuildHeaders(timestamp);
string body = Utility.ConvertToJson(update);
webrequest.Enqueue(url, body, (code, response) =>
{
if (code == 200)
{
// everything acknowledged: clear the ledger
ClearLedger();
}
else
{
// keep the ledger so executed commands are not run again on the next cycle
//Tip4Print("Tip4serv status update failed (HTTP " + code + "). It will be retried on the next cycle.");
}
}, this, RequestMethod.POST, headers, 15f);
}
private Dictionary<string, PaymentUpdate> LoadLedger()
{
try
{
var ledger = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, PaymentUpdate>>(RESPONSE_FILE);
return ledger ?? new Dictionary<string, PaymentUpdate>();
}
catch (Exception)
{
return new Dictionary<string, PaymentUpdate>();
}
}
private void SaveLedger(Dictionary<string, PaymentUpdate> update)
{
Interface.Oxide.DataFileSystem.WriteObject(RESPONSE_FILE, update);
}
private void ClearLedger()
{
Interface.Oxide.DataFileSystem.WriteObject(RESPONSE_FILE, new Dictionary<string, PaymentUpdate>());
}
private bool AlreadyExecuted(Dictionary<string, PaymentUpdate> ledger, string paymentId, string cmdId)
{
PaymentUpdate pu;
string status;
if (ledger != null && ledger.TryGetValue(paymentId, out pu) && pu.cmds != null && pu.cmds.TryGetValue(cmdId, out status))
return status == STATUS_EXECUTED;
return false;
}
private string calculateHMAC(string[] key_parts, string timestamp)
{
HMACSHA256 Encryptor = new HMACSHA256(Encoding.ASCII.GetBytes(key_parts[1]));
key_parts[1] = "";
string Total_Key = string.Join("", key_parts);
Total_Key += timestamp;
var signature = Encryptor.ComputeHash(Encoding.ASCII.GetBytes(Total_Key));
var HMACstr = BitConverter.ToString(signature).Replace("-", "").ToLower();
HMACstr = Convert.ToBase64String(Encoding.ASCII.GetBytes(HMACstr));
return HMACstr;
}
private IPlayer checkifPlayerIsLoaded(string steam_id, Dictionary<string, IPlayer> players)
{
if (string.IsNullOrEmpty(steam_id))
return null;
IPlayer SteamPlayer = players.TryGetValue(steam_id, out SteamPlayer) ? SteamPlayer : null;
if (SteamPlayer == null)
{
return null;
}
#if RUST
if (SteamPlayer.IsConnected)
{
return SteamPlayer;
}
else
{
return null;
}
#elif SEVENDAYSTODIE
try {
//here is the trick we get the position
//if the position of the player is returned it mean the player is connected
//on the other case if Position() throw an exception it means that the player ain't connected
GenericPosition JUNKPOS = SteamPlayer.Position();
return SteamPlayer;
} catch(KeyNotFoundException e) {
return null;
}
#endif
return null;
}
private void exe_command(string cmd, string[] CmdArgs)
{
Tip4Print("Tip4serv execute command: "+cmd);
server.Command(cmd, CmdArgs);
}
}
}