C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
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.1")]
[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: {0} \"<discord name>\"\n Example: {0} \"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.Length > 0 && api_request.URI.StartsWith(DebugURI)) {
Puts($"Request from {api_request.Plugin.Name} to {api_request.URI} with RequestUUID {api_request.RequestUUID}\n{api_request.Body}");
}
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(string.Format(lang.GetMessage("usage.pbreg", this, player.Id), lang.GetMessage("command.poundbot_register", 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
}
}