C#:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Libraries;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Oxide.Plugins
{
[Info("Pound Bot", "MrPoundsign", "2.0.0")]
[Description("Connector for the Discord bot PoundBot.")]
class PoundBot : CovalencePlugin
{
const string EntityDeathURI = "/entity_death";
const string ApiMessageBaseURI = "/messages";
const string ApiRolesURI = "/roles";
const string ApiChatURI = "/chat";
const string ApiRegisteredPlayersURI = "/players/registered";
const string ApiDiscordAuthURI = "/discord_auth";
const string ApiClansURI = "/clans";
private string ApiBaseURI;
enum DebugLevels { TRACE, INFO };
protected string DebugURI = "";
protected int DebugLevel = (int)DebugLevels.INFO;
protected int ApiRetrySeconds = 1;
protected int ApiRetryNotify = 30;
protected string RegisteredUsersGroup;
protected bool ApiInError;
protected bool ApiRetry;
protected uint ApiRetryAttempts;
protected DateTime ApiErrorTime;
protected DateTime LastApiAttempt;
private Dictionary<string, string> RequestHeaders;
protected bool RegisteredUsersInFlight;
private ChannelList KnownChannelList;
class ApiErrorResponse
{
public string Error;
}
class ApiRequest
{
public RequestMethod Method;
public string URI;
public string Body;
public Plugin Plugin;
public string RequestUUID;
public ApiRequest(RequestMethod method, string uri, string body, Plugin plugin)
{
Method = method;
URI = uri;
Body = body;
Plugin = plugin;
RequestUUID = Guid.NewGuid().ToString();
}
}
#region PoundBot messages
class Channel
{
public string ID;
public string Name;
public bool CanSend;
public bool CanStyle;
public bool IsAvailable()
{
return CanSend || CanStyle;
}
}
class ChannelList
{
public Channel[] Channels;
public bool CanSendTo(string channel)
{
foreach (Channel chan in Channels)
{
if (chan.Name == channel || chan.ID == channel)
{
return chan.CanSend;
}
}
return false;
}
public bool CanStyleTo(string channel)
{
foreach (Channel chan in Channels)
{
if (chan.Name == channel || chan.ID == channel)
{
return chan.CanStyle;
}
}
return false;
}
}
class DiscordAuth
{
public string PlayerID;
public string DisplayName;
public string ClanTag;
public string DiscordName;
public int Pin;
public DateTime CreatedAt;
public DiscordAuth(string displayName, string discordName, string playerid)
{
Pin = new System.Random().Next(1, 9999);
DisplayName = displayName;
DiscordName = discordName;
PlayerID = playerid;
CreatedAt = DateTime.UtcNow;
}
}
public class GameMessageEmbedStyle
{
public string Color { get; set; }
}
class GameMessagePart
{
public string Content { get; set; }
public bool Escape { get; set; }
}
class GameMessage
{
public GameMessagePart[] MessageParts { get; set; }
public int Type { get; set; } // 0 = plain, 1 = embedded
public GameMessageEmbedStyle EmbedStyle { get; set; }
}
class RoleSyncRequest
{
public string GuildID;
public string[] PlayerIDs;
}
class EntityDeath
{
public string Name;
public string GridPos;
public string[] OwnerIDs;
public DateTime CreatedAt;
public EntityDeath(string name, string gridpos, string[] ownerIDs)
{
Name = name;
GridPos = gridpos;
OwnerIDs = ownerIDs;
CreatedAt = DateTime.UtcNow;
}
}
#endregion
#region Configuration
protected override void LoadDefaultConfig()
{
Config["api.url"] = "https://api.poundbot.com/";
Config["api.key"] = "API KEY HERE";
Config["config.version"] = 2;
Config["players.registered.group"] = "poundbot.registered";
}
void UpgradeConfig()
{
string configVersion = "config.version";
bool dirty = false;
if (Config[configVersion] == null)
{
Config[configVersion] = 1;
}
else
{
try
{
var foo = (string)Config[configVersion];
Config[configVersion] = 2;
dirty = true;
}
catch (InvalidCastException) { } // testing if it can be converted to a string or not. No need to change it because it's not a string.
}
if ((int)Config[configVersion] < 2)
{
LogWarning(string.Format(lang.GetMessage("config.upgrading", this), "1.1.2"));
// Update the API endpoint
string api_url = (string)Config["api_url"];
if (api_url == "http://poundbot.mrpoundsign.com/" || api_url == "http://api.poundbot.com/")
{
Config["api.url"] = "https://api.poundbot.com/";
}
else
{
Config["api.url"] = (string)Config["api_url"];
}
Config["api.key"] = (string)Config["api_key"];
Config.Remove("api_url");
Config.Remove("api_key");
Config["players.registered.group"] = "poundbot.registered";
Config[configVersion] = 2;
dirty = true;
}
if (dirty)
{
SaveConfig();
}
}
#endregion
#region Language
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["config.upgrading"] = "Upgrading config to v{0}",
["config.api_key_updated"] = "Your API key has been updated. Check config/PoundBot.json.",
["command.poundbot_register"] = "pbreg",
["connector.reconnected"] = "Reconnected with PoundBot",
["connector.time_in_error"] = "Total time in error: {0}",
["connector.error"] = "Error communicating with PoundBot: {0}:{1}",
["connector.error_with_rid"] = "Error communicating with PoundBot: [{0}] {1}:{2}",
["connector.user_error"] = "Cannot connect to PoundBot right now. Please alert the admins.",
["discord.pin"] = "Enter the following PIN to the bot in discord: {0}.",
["discord.connected"] = "You are registered.",
["discord.already_connected"] = "You are already registered.",
["discord.not_connected"] = "Registration not found.",
["discord.channels_updated"] = "Channel cache updated.",
["discord.channel_cannot_send"] = "Cannot send to channel {0}. Please check that the bot can send to this channel. Run pb.updatechannels after making Discord changes to update the cache.",
["discord.channel_cannot_style"] = "Cannot send to channel {0}. Please check that the bot can embed links to this channel. Run pb.updatechannels after making Discord changes to update the cache.",
["usage.pbreg"] = "Usage: /pbreg \"<discord name>\"\n Example: /discord \"Fancy Guy#8080\"",
["usage.set_api_key"] = "Usage:\n\tpb.set_api_key <api key>",
["discord.console.registration_attempt"] = "Player {0}({1}) attempting to register as {2}",
}, this);
}
#endregion
#region Oxide API
void Init()
{
UpgradeConfig();
ApiBaseURI = $"{Config["api.url"]}api";
ApplyConfig();
RegisteredUsersGroup = (string)Config["players.registered.group"];
permission.RegisterPermission(RegisteredUsersGroup, this);
if (!permission.GroupExists(RegisteredUsersGroup))
{
permission.CreateGroup(RegisteredUsersGroup, "PoundBot Registered Users", 0);
}
AddLocalizedCommand("command.poundbot_register", "CommandPoundBotRegister");
Connected();
timer.Every(5f, () =>
{
if (RegisteredUsersInFlight) return;
RegisteredUsersInFlight = true;
Request(new ApiRequest(RequestMethod.GET, ApiRegisteredPlayersURI, null, this),
(code, response) =>
{
RegisteredUsersInFlight = false;
if (code == 200)
{
string[] groupPlayerIDs = permission.GetUsersInGroup(RegisteredUsersGroup);
string[] playerIDs = JsonConvert.DeserializeObject<string[]>(response);
for (int i = 0; i < groupPlayerIDs.Length; i++)
{
groupPlayerIDs[i] = groupPlayerIDs[i].Substring(0, groupPlayerIDs[i].IndexOf(' '));
}
// hot group diff action
string[] playersToRemove = groupPlayerIDs.Except(playerIDs).ToArray();
string[] playersToAdd = playerIDs.Except(groupPlayerIDs).ToArray();
if (playersToAdd.Length + playersToRemove.Length == 0)
{
return true;
}
foreach (string id in playersToRemove)
{
Puts($"Removing user {id} from {RegisteredUsersGroup}");
permission.RemoveUserGroup(id, RegisteredUsersGroup);
}
foreach (string id in playersToAdd)
{
Puts($"Adding user {id} to {RegisteredUsersGroup}");
permission.AddUserGroup(id, RegisteredUsersGroup);
}
return true;
}
Puts("Unuccessful registered players get");
Puts(response);
return false;
});
});
}
#endregion
private void ApplyConfig()
{
RequestHeaders = new Dictionary<string, string>
{
["Content-type"] = "application/json",
["Authorization"] = $"Token {Config["api.key"]}",
["X-PoundBotConnector-Version"] = Version.ToString(),
["User-Agent"] = $"PoundBotConnector/{Version.ToString()}",
["X-PoundBot-Game"] = covalence.Game.ToLower()
};
}
#region API
// private Dictionary<string, string> Headers() => RequestHeaders;
private string API_RegisteredUsersGroup() => RegisteredUsersGroup;
private string ApiBase() => ApiBaseURI;
private bool ApiRequestOk()
{
if (ApiInError && !ApiRetry && (LastApiAttempt.AddSeconds(ApiRetrySeconds) < DateTime.Now))
{
ApiRetryAttempts++;
if (ApiRetryAttempts == 1 || ApiRetryAttempts % ApiRetryNotify == 0)
{
Puts(string.Format(lang.GetMessage("connector.time_in_error", this), DateTime.Now.Subtract(ApiErrorTime)));
}
ApiRetry = true;
}
return (!ApiInError || ApiRetry);
}
private void ApiError(int code, string response, string requestID = null)
{
string error;
if (ApiInError)
{
if (ApiRetry)
{
LastApiAttempt = DateTime.Now;
ApiRetry = false;
}
if (ApiRetryAttempts % ApiRetryNotify != 0) return;
}
else
{
ApiErrorTime = DateTime.Now;
LastApiAttempt = DateTime.Now;
ApiInError = true;
ApiRetry = false;
}
if (code == 0)
{
error = "Connection Failure!";
}
else
{
try
{
var air = JsonConvert.DeserializeObject<ApiErrorResponse>(response);
error = air.Error;
}
catch
{
error = response;
}
}
if (code == 0)
{
Puts(string.Format(lang.GetMessage("connector.error", this), code, error));
return;
}
Puts(string.Format(lang.GetMessage("connector.error_with_rid", this), requestID, code, error));
}
private bool ApiSuccess(bool success)
{
// Reset retry variables if we're successful
if (ApiInError && success)
{
Puts(lang.GetMessage("connector.reconnected", this));
Puts(string.Format(lang.GetMessage("connector.time_in_error", this), DateTime.Now.Subtract(ApiErrorTime)));
ApiRetryAttempts = 0;
ApiInError = false;
ApiRetry = true;
Connected();
}
return success;
}
#region Requests
private bool Request(ApiRequest api_request, Func<int, string, bool> callback)
{
if (!ApiRequestOk()) return false;
if (DebugURI != "" && api_request.URI.StartsWith(DebugURI))
{
Puts($"Request from {api_request.Plugin.Name} to {api_request.URI} with RequestUUID {api_request.RequestUUID}");
}
if (api_request.Plugin == null)
{
api_request.Plugin = this;
}
Dictionary<string, string> rHeaders = new Dictionary<string, string>(RequestHeaders);
rHeaders["X-Request-ID"] = api_request.RequestUUID;
webrequest.Enqueue($"{ApiBaseURI}{api_request.URI}", api_request.Body,
(code, response) =>
{
if (!ApiSuccess(callback(code, response)))
{
ApiError(code, response, rHeaders["X-Request-ID"]);
}
},
api_request.Plugin, api_request.Method, rHeaders, 12000f);
return true;
}
// Returns true if request was sent, false otherwise.
private bool API_Request(string uri, string body, Func<int, string, bool> callback, Plugin owner, RequestMethod method = RequestMethod.GET, Dictionary<string, string> headers = null)
{
return Request(new ApiRequest(method, uri, body, this), callback);
}
#endregion
#region Messages API
private bool API_SendChannelMessage(Plugin owner, string channel, KeyValuePair<string, bool>[] message_parts, string embed_color = null, Func<int, string, bool> callback = null, Dictionary<string, string> headers = null, string type = "plain")
{
List<GameMessagePart> parts = new List<GameMessagePart>();
foreach (KeyValuePair<string, bool> part in message_parts)
{
parts.Add(
new GameMessagePart
{
Content = part.Key,
Escape = part.Value
}
);
}
GameMessage sm = new GameMessage
{
MessageParts = parts.ToArray()
};
if (embed_color != null)
{
if (!KnownChannelList.CanStyleTo(channel))
{
Puts(string.Format(lang.GetMessage("discord.channel_cannot_style", this), channel));
return false;
}
sm.Type = 1;
sm.EmbedStyle = new GameMessageEmbedStyle
{
Color = embed_color
};
}
else
{
if (!KnownChannelList.CanSendTo(channel))
{
Puts(string.Format(lang.GetMessage("discord.channel_cannot_send", this), channel));
return false;
}
}
if (channel[0] == '#')
{
channel = channel.Substring(1);
}
if (callback == null)
{
callback = (int code, string response) => (code == 200);
}
string body = JsonConvert.SerializeObject(sm);
return Request(new ApiRequest(RequestMethod.POST, $"{ApiMessageBaseURI}/{channel}", body, owner), callback);
}
private bool API_GetChannelMessage(Plugin owner, string channel, Func<int, string, bool> callback = null)
{
return Request(new ApiRequest(RequestMethod.GET, ApiChatURI, null, owner), callback);
}
#endregion
#region Roles API
private bool API_SendRole(Plugin owner, string[] groupPlayerIDs, string role, Func<int, string, bool> callback)
{
RoleSyncRequest rsr = new RoleSyncRequest
{
PlayerIDs = groupPlayerIDs,
};
return Request(new ApiRequest(RequestMethod.POST, $"{ApiRolesURI}/{role}", JsonConvert.SerializeObject(rsr), owner), callback);
}
#endregion
private bool API_SendEntityDeath(Plugin owner, string name, string gridPos, string[] owners, Func<int, string, bool> callback)
{
EntityDeath di = new EntityDeath(name, gridPos, owners);
return Request(new ApiRequest(RequestMethod.PUT, EntityDeathURI, JsonConvert.SerializeObject(di), owner), callback);
}
private bool API_SendClans(Plugin owner, List<JObject> clans, Func<int, string, bool> callback)
{
return Request(new ApiRequest(RequestMethod.PUT, ApiClansURI, JsonConvert.SerializeObject(clans), owner), callback);
}
private bool API_SendClan(Plugin owner, string tag, JObject clan, Func<int, string, bool> callback)
{
return Request(new ApiRequest(RequestMethod.PUT, $"{ApiClansURI}/{tag}", JsonConvert.SerializeObject(clan), owner), callback);
}
private bool API_DeleteClan(Plugin owner, string tag, Func<int, string, bool> callback)
{
return Request(new ApiRequest(RequestMethod.DELETE, $"{ApiClansURI}/{tag}", null, owner), callback);
}
private void Connected()
{
UpdateChannels();
Interface.Call("OnPoundBotConnected");
}
#endregion
private void UpdateChannels()
{
Request(new ApiRequest(RequestMethod.GET, ApiMessageBaseURI, null, this),
(code, response) =>
{
if (code == 200)
{
KnownChannelList = JsonConvert.DeserializeObject<ChannelList>(response);
Puts(lang.GetMessage("discord.channels_updated", this));
return true;
}
return false;
});
}
public void PrintChannels(bool all = false)
{
foreach (Channel c in KnownChannelList.Channels)
{
if (all || c.IsAvailable())
{
Puts($"ID: {c.ID}, Name: {c.Name}, CanSend: {c.CanSend}, CanStyle: {c.CanStyle}");
}
}
}
#region Helpers
private void AddLocalizedCommand(string key, string command)
{
foreach (var language in lang.GetLanguages(this))
{
var messages = lang.GetMessages(language, this);
foreach (var message in messages.Where(m => m.Key.Equals(key)))
if (!string.IsNullOrEmpty(message.Value)) AddCovalenceCommand(message.Value, command);
}
}
#endregion
#region Commands
private void CommandPoundBotRegister(IPlayer player, string command, string[] args)
{
if (args.Length != 1)
{
player.Message(lang.GetMessage("usage.pbreg", this, player.Id));
return;
}
if (args[0] == "check")
{
Request(new ApiRequest(RequestMethod.GET, $"{ApiDiscordAuthURI}/check/{player.Id}", null, this),
(code, response) =>
{
if (code == 200)
{
player.Message(lang.GetMessage("discord.connected", this, player.Id));
return true;
}
if (code == 404) // Method not allowed means we're already connected
{
player.Message(lang.GetMessage("discord.not_connected", this, player.Id));
return true;
}
return false;
});
return;
}
Regex r = new Regex(@"^[^#]+#[0-9]+$");
if (!r.Match(args[0]).Success)
{
player.Message(lang.GetMessage("usage.pbreg", this, player.Id));
return;
}
string.Format(lang.GetMessage("discord.console.registration_attempt", this), player.Name, player.Id, args[0]);
var da = new DiscordAuth(player.Name, args[0], player.Id);
Request(new ApiRequest(RequestMethod.PUT, ApiDiscordAuthURI, JsonConvert.SerializeObject(da), this),
(code, response) =>
{
if (code == 200)
{
player.Message(string.Format(lang.GetMessage("discord.pin", this, player.Id), da.Pin.ToString("D4")));
return true;
}
if (code == 409) // Conflict means we're already connected
{
player.Message(lang.GetMessage("discord.already_connected", this, player.Id));
return true;
}
return false;
});
}
[Command("pb.update_channels")]
private void ConsoleCommandUpdateChannels(IPlayer player, string command, string[] args)
{
UpdateChannels();
}
[Command("pb.channels")]
private void ConsoleCommandChannels(IPlayer player, string command, string[] args)
{
PrintChannels((args.Length != 0));
}
[Command("pb.set_api_key")]
private void ConsoleCommandSetAPIKey(IPlayer player, string command, string[] args)
{
if (args.Count() != 1)
{
Puts(lang.GetMessage("usage.set_api_key", this));
return;
}
Puts(lang.GetMessage("config.api_key_updated", this));
Config["api.key"] = args[0];
SaveConfig();
ApplyConfig();
}
[Command("pb.set_debug_uri")]
private void ConsoleCommandSetDebugURI(IPlayer player, string command, string[] args)
{
if (args.Count() != 1)
{
Puts(lang.GetMessage("usage.set_debug_uri", this));
return;
}
DebugURI = args[0];
}
#endregion
}
}