C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Oxide.Core;
using Oxide.Core.Libraries;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using UnityEngine;
#if RUST
using UnityEngine.Networking;
using System.Collections;
using System.IO;
using ConVar;
#endif
namespace Oxide.Plugins;
[Info("Discord Wipe", "MJSU", "2.4.1")]
[Description("Sends a notification to a discord channel when the server wipes or protocol changes")]
internal class DiscordWipe : CovalencePlugin
{
#region Class Fields
[PluginReference] private Plugin RustMapApi, PlaceholderAPI;
private PluginConfig _pluginConfig; //Plugin Config
private StoredData _storedData; //Plugin Data
private const string DefaultUrl = "https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks";
private const string DefaultRustMapsApiKey = "Get Your API Key @ https://rustmaps.com/dashboard";
private const string AdminPermission = "discordwipe.admin";
private const string AttachmentBase = "attachment://";
private const string MapAttachment = AttachmentBase + MapFilename;
private const string MapFilename = "map.jpg";
private const int MaxImageSize = 8 * 1024 * 1024;
private string _protocol;
private string _previousProtocol;
private bool _hasStarted;
private readonly StringBuilder _parser = new();
private Action<IPlayer, StringBuilder, bool> _replacer;
private enum DebugEnum {Message, None, Error, Warning, Info}
public enum SendMode {Always, Random}
private enum EncodingMode {Jpg = 1, Png = 2}
#if RUST
private enum RustMapMode {None, RustMaps, RustMapApi}
private RustMapsResponse _rustMapsResponse;
#endif
#endregion
#region Setup & Loading
private void Init()
{
UnsubscribeAll();
AddCovalenceCommand(_pluginConfig.Command, nameof(SendWipeCommand));
_storedData = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name);
_previousProtocol = _storedData.Protocol;
permission.RegisterPermission(AdminPermission, this);
_pluginConfig.ProtocolWebhook = UpdateWebhookUrl(_pluginConfig.ProtocolWebhook);
_pluginConfig.WipeWebhook = UpdateWebhookUrl(_pluginConfig.WipeWebhook);
foreach (DiscordMessageConfig embed in _pluginConfig.WipeEmbeds)
{
if (!string.IsNullOrEmpty(embed.WebhookOverride) && embed.WebhookOverride != DefaultUrl)
{
embed.WebhookOverride = UpdateWebhookUrl(embed.WebhookOverride);
}
}
foreach (DiscordMessageConfig embed in _pluginConfig.ProtocolEmbeds)
{
if (!string.IsNullOrEmpty(embed.WebhookOverride) && embed.WebhookOverride != DefaultUrl)
{
embed.WebhookOverride = UpdateWebhookUrl(embed.WebhookOverride);
}
}
#if RUST
_rustMapGenerateHeaders["X-API-Key"] = _pluginConfig.ImageSettings.RustMaps.ApiKey;
_rustMapGetHeaders["X-API-Key"] = _pluginConfig.ImageSettings.RustMaps.ApiKey;
#endif
}
public string UpdateWebhookUrl(string url)
{
return url.Replace("/api/webhooks", "/api/v10/webhooks").Replace("https://discordapp.com/", "https://discord.com/");
}
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
[LangKeys.NoPermission] = "You do not have permission to use this command",
[LangKeys.SentWipe] = "You have sent a test wipe message",
[LangKeys.SentProtocol] = "You have sent a test protocol message",
[LangKeys.Help] = "Sends test message for plugin\n" +
"{0}{1} wipe - sends a wipe test message\n" +
"{0}{1} protocol - sends a protocol test message\n" +
"{0}{1} - displays this help text again" ,
}, this);
}
protected override void LoadDefaultConfig()
{
PrintWarning("Loading Default Config");
}
protected override void LoadConfig()
{
base.LoadConfig();
Config.Settings.DefaultValueHandling = DefaultValueHandling.Populate;
_pluginConfig = AdditionalConfig(Config.ReadObject<PluginConfig>());
Config.WriteObject(_pluginConfig);
}
private PluginConfig AdditionalConfig(PluginConfig config)
{
config.WipeEmbeds ??= new List<DiscordMessageConfig> {
new()
{
Content = "@everyone",
SendMode = SendMode.Always,
Embed = new EmbedConfig
{
Title = "{server.name}",
Description = "The server has wiped!",
Url = string.Empty,
Color = "#de8732",
Image = MapAttachment,
Thumbnail = string.Empty,
Fields = new List<FieldConfig>
{
#if RUST
new()
{
Title = "Seed",
Value = "[{world.seed}](https://rustmaps.com/map/{world.size}_{world.seed})",
Inline = true,
Enabled = true
},
new()
{
Title = "Size",
Value = "{world.size}M ({world.size!km^2}km^2)",
Inline = true,
Enabled = true
},
new()
{
Title = "Protocol",
Value = "{server.protocol.network}",
Inline = true,
Enabled = true
},
#endif
new()
{
Title = "Click & Connect",
Value = "steam://connect/{server.address}:{server.port}",
Inline = false,
Enabled = true
}
},
Footer = new FooterConfig
{
IconUrl = string.Empty,
Text = string.Empty,
Enabled = true
},
Enabled = true
}
}};
config.ProtocolEmbeds ??= new List<DiscordMessageConfig> {
new()
{
Content = "@everyone",
SendMode = SendMode.Always,
Embed = new EmbedConfig
{
Title = "{server.name}",
Description = "The server protocol has changed!",
Url = string.Empty,
Color = "#de8732",
Image = string.Empty,
Thumbnail = string.Empty,
Fields = new List<FieldConfig>
{
new()
{
Title = "Protocol",
Value = "{server.protocol.network}",
Inline = true,
Enabled = true
},
new()
{
Title = "Previous Protocol",
Value = "{server.protocol.previous}",
Inline = true,
Enabled = true
},
new()
{
Title = "Mandatory Client Update",
Value = "This update requires a mandatory client update in order to be able to play on the server",
Inline = false,
Enabled = true
},
new()
{
Title = "Click & Connect",
Value = "steam://connect/{server.address}:{server.port}",
Inline = false,
Enabled = true
}
},
Footer = new FooterConfig
{
IconUrl = string.Empty,
Text = string.Empty,
Enabled = true
},
Enabled = true
}
}};
#if RUST
config.ImageSettings = new RustMapImageSettings
{
MapMode = config.ImageSettings?.MapMode ?? (RustMapApi != null ? RustMapMode.RustMapApi : RustMapMode.None),
RustMapApi = new RustMapApiSettings
{
Name = config.ImageSettings?.RustMapApi?.Name ?? "Icons",
Scale = config.ImageSettings?.RustMapApi?.Scale ?? 0.5f,
FileType = config.ImageSettings?.RustMapApi?.FileType ?? EncodingMode.Jpg
},
RustMaps = new RustMapSettings
{
ApiKey = config.ImageSettings?.RustMaps?.ApiKey ?? "Get Your API Key @ https://rustmaps.com/user/profile",
Staging = config.ImageSettings?.RustMaps?.Staging ?? false
}
};
#endif
foreach (DiscordMessageConfig embed in config.WipeEmbeds)
{
if (string.IsNullOrEmpty(embed.WebhookOverride))
{
embed.WebhookOverride = DefaultUrl;
}
}
foreach (DiscordMessageConfig embed in config.ProtocolEmbeds)
{
if (string.IsNullOrEmpty(embed.WebhookOverride))
{
embed.WebhookOverride = DefaultUrl;
}
}
return config;
}
private void OnServerInitialized()
{
_protocol = GetProtocol();
if (PlaceholderAPI is not { IsLoaded: true })
{
PrintError("Missing plugin dependency PlaceholderAPI: https://umod.org/plugins/placeholder-api");
return;
}
if(PlaceholderAPI.Version < new VersionNumber(2, 2, 0))
{
PrintError("Placeholder API plugin must be version 2.2.0 or higher");
return;
}
#if RUST
if (IsRustMapApiLoaded() && _pluginConfig.ImageSettings.MapMode == RustMapMode.RustMapApi)
{
if(RustMapApi.Version < new VersionNumber(1,3,2))
{
PrintError("RustMapApi plugin must be version 1.3.2 or higher");
return;
}
if (!IsRustMapApiReady())
{
Debug(DebugEnum.Info, "Waiting for Rust Maps Api Plugin to be Ready");
SubscribeAll();
return;
}
}
if (_pluginConfig.ImageSettings.MapMode == RustMapMode.RustMaps && !string.IsNullOrEmpty(_pluginConfig.ImageSettings.RustMaps.ApiKey) && _pluginConfig.ImageSettings.RustMaps.ApiKey != DefaultRustMapsApiKey)
{
Debug(DebugEnum.Info, "Loading Map from RustMaps.com");
GetRustMapsMap();
//timer.In(15 * 60f, HandleStartup);
return;
}
#endif
//Delayed so PlaceholderAPI can be ready before we call
timer.In(1f, () => HandleStartup("OnServerInit"));
}
private void OnRustMapApiReady()
{
HandleStartup("OnRustMapApiReady");
}
private void HandleStartup(string source)
{
Debug(DebugEnum.Info, $"HandleStartup - {source}");
if (_hasStarted)
{
Debug(DebugEnum.Info, "HandleStartup - Skipping as already started");
return;
}
_hasStarted = true;
if (string.IsNullOrEmpty(_storedData.Protocol))
{
Debug(DebugEnum.Info, $"HandleStartup - Protocol is not set setting protocol to: {_protocol}");
_storedData.Protocol = _protocol;
SaveData();
}
else if (_storedData.Protocol != _protocol)
{
Debug(DebugEnum.Info, $"HandleStartup - Protocol has changed {_storedData.Protocol} -> {_protocol}");
if (_pluginConfig.SendProtocolAutomatically)
{
SendProtocol();
}
_storedData.Protocol = _protocol;
SaveData();
Puts("Protocol notification sent");
}
else
{
Debug(DebugEnum.Info, "HandleStartup - Protocol has not changed");
}
if (_storedData.IsWipe)
{
if (_pluginConfig.SendWipeAutomatically)
{
Debug(DebugEnum.Info, "HandleStartup - IsWipe is set.Sending wipe message.");
SendWipe();
Puts("Wipe notification sent");
}
else
{
Debug(DebugEnum.Info, "SendWipeAutomatically is disabled");
}
_storedData.IsWipe = false;
SaveData();
}
else
{
Debug(DebugEnum.Info, "HandleStartup - Not a wipe");
}
}
private void OnNewSave()
{
_storedData.IsWipe = true;
Debug(DebugEnum.Info, "OnNewSave - Wipe Detected");
SaveData();
}
private void Unload()
{
SaveData();
}
private string GetProtocol()
{
#if RUST
return Rust.Protocol.network.ToString();
#else
return covalence.Server.Protocol;
#endif
}
#endregion
#region Command
private bool SendWipeCommand(IPlayer player, string cmd, string[] args)
{
if (!HasPermission(player, AdminPermission))
{
player.Message(Lang(LangKeys.NoPermission, player));
return true;
}
Debug(DebugEnum.Info, $"SendWipeCommand command called: {string.Join(" ", args)}");
string commandPrefix = player.IsServer ? "" : "/";
if (args.Length == 0)
{
player.Message(Lang(LangKeys.Help, player, commandPrefix, _pluginConfig.Command));
return true;
}
switch (args[0].ToLower())
{
case "wipe":
SendWipe();
player.Message(Lang(LangKeys.SentWipe, player));
break;
case "protocol":
SendProtocol();
player.Message(Lang(LangKeys.SentProtocol, player));
break;
default:
player.Message(Lang(LangKeys.Help, player,commandPrefix, _pluginConfig.Command));
break;
}
return true;
}
#endregion
#region Message Handling
private void SendWipe()
{
Debug(DebugEnum.Info, "SendWipe - Sending wipe message");
if (string.IsNullOrEmpty(_pluginConfig.WipeWebhook) || _pluginConfig.WipeWebhook == DefaultUrl)
{
Debug(DebugEnum.Info, "SendWipe - Wipe message not sent due to Wipe Webhook being blank or matching the default url");
return;
}
List<DiscordMessageConfig> messages = _pluginConfig.WipeEmbeds.Where(w => w.SendMode == SendMode.Always).ToList();
DiscordMessageConfig random = _pluginConfig.WipeEmbeds.Where(w => w.SendMode == SendMode.Random).OrderBy(w => Guid.NewGuid()).FirstOrDefault();
if (random != null)
{
messages.Add(random);
}
SendMessage(_pluginConfig.WipeWebhook, messages);
Debug(DebugEnum.Message, "");
}
private void SendProtocol()
{
Debug(DebugEnum.Info, "SendProtocol - Sending protocol message");
if (string.IsNullOrEmpty(_pluginConfig.ProtocolWebhook) || _pluginConfig.ProtocolWebhook == DefaultUrl)
{
Debug(DebugEnum.Info, "SendProtocol - Protocol message not sent due to Protocol Webhook being blank or matching the default url");
return;
}
List<DiscordMessageConfig> messages = _pluginConfig.ProtocolEmbeds.Where(w => w.SendMode == SendMode.Always).ToList();
DiscordMessageConfig random = _pluginConfig.ProtocolEmbeds.Where(w => w.SendMode == SendMode.Random).OrderBy(w => Guid.NewGuid()).FirstOrDefault();
if (random != null)
{
messages.Add(random);
}
SendMessage(_pluginConfig.ProtocolWebhook, messages);
}
private void SendMessage(string url, List<DiscordMessageConfig> messageConfigs)
{
for (int index = 0; index < messageConfigs.Count; index++)
{
DiscordMessageConfig messageConfig = messageConfigs[index];
DiscordMessage message = ParseMessage(messageConfig);
timer.In(index + 1, () =>
{
#if RUST
List<Attachment> attachments = new();
Debug(DebugEnum.Info, $"SendMessage - MapMode={_pluginConfig.ImageSettings.MapMode.ToString()}");
if (_pluginConfig.ImageSettings.MapMode == RustMapMode.RustMapApi)
{
AttachMap(attachments, messageConfig);
}
if (!string.IsNullOrEmpty(messageConfig.WebhookOverride) && messageConfig.WebhookOverride != DefaultUrl)
{
url = messageConfig.WebhookOverride;
}
SendDiscordAttachmentMessage(url, message, attachments);
#else
SendDiscordMessage(url, message);
#endif
});
}
}
#endregion
#if RUST
private void AttachMap(List<Attachment> attachments, DiscordMessageConfig messageConfig)
{
Debug(DebugEnum.Info, $"Can attach map? RustMapApi Loaded: {IsRustMapApiLoaded()} RustMapApiReady: {IsRustMapApiReady()} Attachment Base:{messageConfig.Embed.Image.StartsWith(AttachmentBase)}");
if (IsRustMapApiLoaded() && IsRustMapApiReady() && messageConfig.Embed.Image.StartsWith(AttachmentBase))
{
Debug(DebugEnum.Info, "AttachMap - RustMapApi is ready, attaching map");
List<string> maps = RustMapApi.Call<List<string>>("GetSavedMaps");
string mapName = _pluginConfig.ImageSettings.RustMapApi.Name;
if (maps != null)
{
mapName = maps.FirstOrDefault(m => m.Equals(mapName, StringComparison.InvariantCultureIgnoreCase));
if (string.IsNullOrEmpty(mapName))
{
PrintWarning($"Map name not found {_pluginConfig.ImageSettings.RustMapApi.Name}.Valid names are {string.Join(", ", maps.ToArray())}");
mapName = "Icons";
}
}
Debug(DebugEnum.Info, $"AttachMap - RustMapApi map name set to: {mapName}");
int resolution = (int) (World.Size * _pluginConfig.ImageSettings.RustMapApi.Scale);
AttachmentContentType contentType = _pluginConfig.ImageSettings.RustMapApi.FileType == EncodingMode.Jpg ? AttachmentContentType.Jpg : AttachmentContentType.Png;
object response = RustMapApi.Call("CreatePluginImage", this, mapName, resolution, (int)_pluginConfig.ImageSettings.RustMapApi.FileType);
if (response is string)
{
PrintError($"An error occurred creating the plugin image: {response}");
return;
}
Hash<string, object> map = response as Hash<string, object>;
byte[] mapData = map?["image"] as byte[];
if (mapData != null)
{
if (mapData.Length >= 8 * 1024 * 1024)
{
PrintError( "Map Image too large." +
$"Image size is {mapData.Length / 1024.0 / 1024.0:0.00}MB." +
$"Max size is {MaxImageSize / 1024 / 1024}MB." +
"Please reduce the \"Image Resolution Scale\"");
}
else
{
attachments.Add(new Attachment(mapData, MapFilename, contentType));
Debug(DebugEnum.Info, "AttachMap - Successfully attached map");
}
}
else
{
Debug(DebugEnum.Warning, "AttachMap - MapData was null!!!");
}
}
}
#region RustMaps.com
private readonly Dictionary<string, string> _rustMapGenerateHeaders = new()
{
["Content-Type"] = "application/json",
};
private readonly Dictionary<string, string> _rustMapGetHeaders = new();
public void GetRustMapsMap()
{
if (string.IsNullOrEmpty(ConVar.Server.levelurl))
{
uint seed = World.Seed;
uint size = World.Size;
string url = $"https://api.rustmaps.com/v4/maps/{size}/{seed}?staging={_pluginConfig.ImageSettings.RustMaps.Staging}";
Debug(DebugEnum.Info, $"RustMaps.com Requesting Rust Map.Url: {url}");
webrequest.Enqueue(url, null, (code, response) => RustMapsGetCallback(code, response, false), this, RequestMethod.GET, _rustMapGetHeaders);
}
else if(ConVar.Server.levelurl.StartsWith("https://maps.rustmaps.com"))
{
Regex regex = new(@"https:\/\/maps\.rustmaps\.com\/\d*\/([a-z\d]*)\/");
string match = regex.Match(ConVar.Server.levelurl).Groups[1].Value;
if (!string.IsNullOrEmpty(match))
{
string url = $"https://api.rustmaps.com/v4/maps/{match}";
Debug(DebugEnum.Info, $"RustMaps.com Requesting Rust Map.Url: {url}");
webrequest.Enqueue(url, null, (code, response) => RustMapsGetCallback(code, response, true), this, RequestMethod.GET, _rustMapGetHeaders);
}
}
}
private void RustMapsGetCallback(int code, string response, bool isCustom)
{
if (code == 409)
{
Debug(DebugEnum.Message, "RustMaps.com is still generating the map image.Trying again in 60 seconds.");
timer.In(60f, GetRustMapsMap);
}
else if (!isCustom && code == 404)
{
Debug(DebugEnum.Message, $"RustMaps.com map does not exist.Requesting Generation.\n{response}");
RustMapsRequestMap();
}
else if (code == 200)
{
Debug(DebugEnum.Info, "RustMaps.com map image found.");
_rustMapsResponse = JsonConvert.DeserializeObject<RustMapsResponse>(response);
HandleStartup("RustMapsGetCallback");
}
else
{
Debug(DebugEnum.Error,$"An error occured trying to get map image from RustMaps.com - Code:{code} Response:\n{response}");
timer.In(60f, GetRustMapsMap);
}
}
public void RustMapsRequestMap()
{
Debug(DebugEnum.Info, "RustMaps.com Generating Rust Map.");
RustMapsApiRequest request = new()
{
Size = World.Size,
Seed = World.Seed,
Staging = _pluginConfig.ImageSettings.RustMaps.Staging
};
string json = JsonConvert.SerializeObject(request);
Debug(DebugEnum.Info, json);
webrequest.Enqueue("https://api.rustmaps.com/v4/maps", json, RustMapsPostCallback, this, RequestMethod.POST, _rustMapGenerateHeaders);
}
private void RustMapsPostCallback(int code, string response)
{
if (code == 201)
{
Debug(DebugEnum.Info, "RustMaps.com image requested successfully.");
timer.In(60f, GetRustMapsMap);
}
else if (code == 409)
{
Debug(DebugEnum.Info, "RustMaps.com map already exists.Requesting image.");
GetRustMapsMap();
}
else
{
Debug(DebugEnum.Error,$"An error occured trying to request map generation from RustMaps.com - Code:{code} Response:\n{response}");
timer.In(60f, GetRustMapsMap);
}
}
#endregion
#endif
#region PlaceholderAPI
private string ParseField(string field)
{
_parser.Length = 0;
_parser.Append(field);
GetReplacer()?.Invoke(null, _parser, false);
return _parser.ToString();
}
private void OnPluginUnloaded(Plugin plugin)
{
if (plugin?.Name == "PlaceholderAPI")
{
_replacer = null;
}
}
private void OnPlaceholderAPIReady()
{
RegisterPlaceholder("server.protocol.previous", (player, s) => _previousProtocol, "Displays the previous protocol version if it changed during the last restart", double.MaxValue);
RegisterPlaceholder("timestamp.now", (player, s) => UnixTimeNow(), "Displays the current unix timestamp", double.MaxValue);
#if RUST
RegisterPlaceholder("rustmaps.com.map", (player, s) => _rustMapsResponse?.Data?.ImageUrl ?? string.Empty, "RustMaps.com map image url", double.MaxValue);
RegisterPlaceholder("rustmaps.com.icons", (player, s) => _rustMapsResponse?.Data?.ImageIconUrl ?? string.Empty, "RustMaps.com icon map image url", double.MaxValue);
RegisterPlaceholder("rustmaps.com.thumbnail", (player, s) => _rustMapsResponse?.Data?.ThumbnailUrl ?? string.Empty, "RustMaps.com thumbnail map image url", double.MaxValue);
#endif
}
private void RegisterPlaceholder(string key, Func<IPlayer, string, object> action, string description = null, double ttl = double.NaN)
{
if (IsPlaceholderApiLoaded())
{
PlaceholderAPI.Call("AddPlaceholder", this, key, action, description, ttl);
}
}
private Action<IPlayer, StringBuilder, bool> GetReplacer()
{
if (!IsPlaceholderApiLoaded())
{
return _replacer;
}
return _replacer ?? (_replacer = PlaceholderAPI.Call<Action<IPlayer, StringBuilder, bool>>("GetProcessPlaceholders", 1));
}
private bool IsPlaceholderApiLoaded() => PlaceholderAPI != null && PlaceholderAPI.IsLoaded;
#endregion
#region Helpers
public long UnixTimeNow()
{
TimeSpan timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0);
return (long)timeSpan.TotalSeconds;
}
public void UnsubscribeAll()
{
Unsubscribe(nameof(OnRustMapApiReady));
}
public void SubscribeAll()
{
Subscribe(nameof(OnRustMapApiReady));
}
private void Debug(DebugEnum level, string message)
{
if (level > _pluginConfig.DebugLevel)
{
return;
}
switch (level)
{
case DebugEnum.Error:
PrintError(message);
break;
case DebugEnum.Warning:
PrintWarning(message);
break;
default:
Puts($"{level}: {message}");
break;
}
}
private string Lang(string key, IPlayer player = null, params object[] args)
{
try
{
return string.Format(lang.GetMessage(key, this, player?.Id), args);
}
catch(Exception ex)
{
PrintError($"Lang Key '{key}' threw exception\n:{ex.Message}");
throw;
}
}
private bool IsRustMapApiLoaded() => RustMapApi is { IsLoaded: true };
private bool IsRustMapApiReady() => RustMapApi.Call<bool>("IsReady");
private void SaveData()
{
if (_storedData != null)
{
Interface.Oxide.DataFileSystem.WriteObject(Name, _storedData);
}
}
private bool HasPermission(IPlayer player, string perm) => permission.UserHasPermission(player.Id, perm);
#endregion
#region Classes
private class PluginConfig
{
[JsonConverter(typeof(StringEnumConverter))]
[DefaultValue(DebugEnum.Warning)]
[JsonProperty(PropertyName = "Debug Level (None, Error, Warning, Info)")]
public DebugEnum DebugLevel { get; set; }
[DefaultValue("dw")]
[JsonProperty(PropertyName = "Command")]
public string Command { get; set; }
#if RUST
[JsonProperty(PropertyName = "Rust Map Image Settings")]
public RustMapImageSettings ImageSettings { get; set; }
#endif
[DefaultValue(true)]
[JsonProperty(PropertyName = "Send wipe message when server wipes")]
public bool SendWipeAutomatically { get; set; }
[DefaultValue(DefaultUrl)]
[JsonProperty(PropertyName = "Wipe Webhook url")]
public string WipeWebhook { get; set; }
[DefaultValue(true)]
[JsonProperty(PropertyName = "Send protocol message when server protocol changes")]
public bool SendProtocolAutomatically { get; set; }
[DefaultValue(DefaultUrl)]
[JsonProperty(PropertyName = "Protocol Webhook url")]
public string ProtocolWebhook { get; set; }
[JsonProperty(PropertyName = "Wipe messages")]
public List<DiscordMessageConfig> WipeEmbeds { get; set; }
[JsonProperty(PropertyName = "Protocol messages")]
public List<DiscordMessageConfig> ProtocolEmbeds { get; set; }
}
public class RustMapsApiRequest
{
[JsonProperty("size")]
public uint Size { get; set; }
[JsonProperty("seed")]
public uint Seed { get; set; }
[JsonProperty("staging")]
public bool Staging { get; set; }
[JsonProperty("barren")]
public bool Barren { get; set; }
}
#if RUST
private class RustMapImageSettings
{
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty(PropertyName = "Map Image Source (None, RustMaps, RustMapApi)")]
public RustMapMode MapMode { get; set; }
[JsonProperty(PropertyName = "RustMaps.com Settings")]
public RustMapSettings RustMaps { get; set; }
[JsonProperty(PropertyName = "RustMapApi Settings")]
public RustMapApiSettings RustMapApi { get; set; }
}
private class RustMapSettings
{
[JsonProperty(PropertyName = "RustMap.com API Key")]
public string ApiKey { get; set; }
[JsonProperty(PropertyName = "Generate Staging Map")]
public bool Staging { get; set; }
}
private class RustMapApiSettings
{
[DefaultValue("Icons")]
[JsonProperty(PropertyName = "Render Name")]
public string Name { get; set; }
[DefaultValue(0.5f)]
[JsonProperty(PropertyName = "Image Resolution Scale")]
public float Scale { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
[DefaultValue(EncodingMode.Jpg)]
[JsonProperty(PropertyName = "File Type (Jpg, Png")]
public EncodingMode FileType { get; set; }
}
public class RustMapsResponse
{
[JsonProperty("data")]
public RustMapsData Data { get; set; }
}
public class RustMapsData
{
[JsonProperty("imageUrl")]
public string ImageUrl { get; set; }
[JsonProperty("imageIconUrl")]
public string ImageIconUrl { get; set; }
[JsonProperty("thumbnailUrl")]
public string ThumbnailUrl { get; set; }
}
#endif
private class StoredData
{
public bool IsWipe { get; set; }
public string Protocol { get; set; }
}
private class LangKeys
{
public const string NoPermission = "NoPermission";
public const string SentWipe = "SentWipe";
public const string SentProtocol = "SentProtocol";
public const string Help = "Help";
}
#endregion
#region Discord Embed
#region Send Embed Methods
/// <summary>
/// Headers when sending an embeded message
/// </summary>
private readonly Dictionary<string, string> _headers = new()
{
{"Content-Type", "application/json"}
};
/// <summary>
/// Sends the DiscordMessage to the specified webhook url
/// </summary>
/// <param name="url">Webhook url</param>
/// <param name="message">Message being sent</param>
private void SendDiscordMessage(string url, DiscordMessage message)
{
StringBuilder json = message.ToJson();
if (_pluginConfig.DebugLevel >= DebugEnum.Info)
{
Debug(DebugEnum.Info, $"{nameof(SendDiscordMessage)} message.ToJson()\n{json}");
}
webrequest.Enqueue(url, json.ToString(), SendDiscordMessageCallback, this, RequestMethod.POST, _headers);
}
/// <summary>
/// Callback when sending the embed if any errors occured
/// </summary>
/// <param name="code">HTTP response code</param>
/// <param name="message">Response message</param>
private void SendDiscordMessageCallback(int code, string message)
{
if (code != 204)
{
PrintError(message);
}
}
#if RUST
/// <summary>
/// Sends the DiscordMessage to the specified webhook url with attachments
/// </summary>
/// <param name="url">Webhook url</param>
/// <param name="message">Message being sent</param>
/// <param name="files">Attachments to be added to the DiscordMessage</param>
private void SendDiscordAttachmentMessage(string url, DiscordMessage message, List<Attachment> files)
{
StringBuilder json = message.ToJson();
if (_pluginConfig.DebugLevel >= DebugEnum.Info)
{
Debug(DebugEnum.Info, $"{nameof(SendDiscordAttachmentMessage)} message.ToJson()\n{json}");
}
List<IMultipartFormSection> formData = new()
{
new MultipartFormDataSection("payload_json", json.ToString())
};
for (int i = 0; i < files.Count; i++)
{
Attachment attachment = files[i];
formData.Add(new MultipartFormFileSection($"file{i + 1}", attachment.Data, attachment.Filename, attachment.ContentType));
}
InvokeHandler.Instance.StartCoroutine(SendDiscordAttachmentMessageHandler(url, formData));
}
private IEnumerator SendDiscordAttachmentMessageHandler(string url, List<IMultipartFormSection> data)
{
UnityWebRequest www = UnityWebRequest.Post(url, data);
yield return www.SendWebRequest();
if (www.isNetworkError || www.isHttpError)
{
PrintError($"CODE: {www.error} ERROR: {www.downloadHandler.text}");
}
}
#endif
#endregion
#region Helper Methods
private const string OwnerIcon = "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/47/47db946f27bc76d930ac82f1656f7a10707bb67d_full.jpg";
private void AddPluginInfoFooter(Embed embed)
{
embed.AddFooter($"{Title} V{Version} by {Author}", OwnerIcon);
}
private string GetPositionField(Vector3 pos)
{
return $"{pos.x:0.00} {pos.y:0.00} {pos.z:0.00}";
}
#endregion
#region Embed Classes
private class DiscordMessage
{
/// <summary>
/// The name of the user sending the message changing this will change the webhook bots name
/// </summary>
[JsonProperty("username")]
private string Username { get; set; }
/// <summary>
/// The avatar url of the user sending the message changing this will change the webhook bots avatar
/// </summary>
[JsonProperty("avatar_url")]
private string AvatarUrl { get; set; }
/// <summary>
/// String only content to be sent
/// </summary>
[JsonProperty("content")]
private string Content { get; set; }
/// <summary>
/// Embeds to be sent
/// </summary>
[JsonProperty("embeds")]
private List<Embed> Embeds { get; }
public DiscordMessage(string username = null, string avatarUrl = null)
{
Username = username;
AvatarUrl = avatarUrl;
Embeds = new List<Embed>();
}
public DiscordMessage(string content, string username = null, string avatarUrl = null)
{
Content = content;
Username = username;
AvatarUrl = avatarUrl;
Embeds = new List<Embed>();
}
public DiscordMessage(Embed embed, string username = null, string avatarUrl = null)
{
Embeds = new List<Embed> {embed};
Username = username;
AvatarUrl = avatarUrl;
}
/// <summary>
/// Adds a new embed to the list of embed to send
/// </summary>
/// <param name="embed">Embed to add</param>
/// <returns>This</returns>
/// <exception cref="IndexOutOfRangeException">Thrown if more than 10 embeds are added in a send as that is the discord limit</exception>
public DiscordMessage AddEmbed(Embed embed)
{
if (Embeds.Count >= 10)
{
throw new IndexOutOfRangeException("Only 10 embed are allowed per message");
}
Embeds.Add(embed);
return this;
}
/// <summary>
/// Adds string content to the message
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
public DiscordMessage AddContent(string content)
{
Content = content;
return this;
}
/// <summary>
/// Changes the username and avatar image for the bot sending the message
/// </summary>
/// <param name="username">username to change</param>
/// <param name="avatarUrl">avatar img url to change</param>
/// <returns>This</returns>
public DiscordMessage AddSender(string username, string avatarUrl)
{
Username = username;
AvatarUrl = avatarUrl;
return this;
}
/// <summary>
/// Returns message as JSON to be sent in the web request
/// </summary>
/// <returns></returns>
public StringBuilder ToJson() => new(JsonConvert.SerializeObject(this, Formatting.None,
new JsonSerializerSettings {NullValueHandling = NullValueHandling.Ignore}));
}
private class Embed
{
/// <summary>
/// Color of the left side bar of the embed message
/// </summary>
[JsonProperty("color")]
private int Color { get; set; }
/// <summary>
/// Fields to be added to the embed message
/// </summary>
[JsonProperty("fields")]
private List<Field> Fields { get; } = new();
/// <summary>
/// Title of the embed message
/// </summary>
[JsonProperty("title")]
private string Title { get; set; }
/// <summary>
/// Description of the embed message
/// </summary>
[JsonProperty("description")]
private string Description { get; set; }
/// <summary>
/// Description of the embed message
/// </summary>
[JsonProperty("url")]
private string Url { get; set; }
/// <summary>
/// Image to added to the embed message.Appears at the bottom of the message above the footer
/// </summary>
[JsonProperty("image")]
private Image Image { get; set; }
/// <summary>
/// Thumbnail image added to the embed message.Appears in the top right corner
/// </summary>
[JsonProperty("thumbnail")]
private Image Thumbnail { get; set; }
/// <summary>
/// Video to add to the embed message
/// </summary>
[JsonProperty("video")]
private Video Video { get; set; }
/// <summary>
/// Author to add to the embed message.Appears above the title./// </summary>
[JsonProperty("author")]
private AuthorInfo Author { get; set; }
/// <summary>
/// Footer to add to the embed message.Appears below all content./// </summary>
[JsonProperty("footer")]
private Footer Footer { get; set; }
/// <summary>
/// Adds a title to the embed message
/// </summary>
/// <param name="title">Title to add</param>
/// <returns>This</returns>
public Embed AddTitle(string title)
{
Title = title;
return this;
}
/// <summary>
/// Adds a description to the embed message
/// </summary>
/// <param name="description">description to add</param>
/// <returns>This</returns>
public Embed AddDescription(string description)
{
Description = description;
return this;
}
/// <summary>
/// Adds a url to the embed message
/// </summary>
/// <param name="url"></param>
/// <returns>This</returns>
public Embed AddUrl(string url)
{
Url = url;
return this;
}
/// <summary>
/// Adds an author to the embed message.The author will appear above the title
/// </summary>
/// <param name="name">Name of the author</param>
/// <param name="iconUrl">Icon Url to use for the author</param>
/// <param name="url">Url to go to when the authors name is clicked on</param>
/// <param name="proxyIconUrl">Backup icon url.Can be left null if you only have one icon url</param>
/// <returns>This</returns>
public Embed AddAuthor(string name, string iconUrl = null, string url = null, string proxyIconUrl = null)
{
Author = new AuthorInfo(name, iconUrl, url, proxyIconUrl);
return this;
}
/// <summary>
/// Adds a footer to the embed message
/// </summary>
/// <param name="text">Text to be added to the footer</param>
/// <param name="iconUrl">Icon url to add in the footer.Appears to the left of the text</param>
/// <param name="proxyIconUrl">Backup icon url.Can be left null if you only have one icon url</param>
/// <returns>This</returns>
public Embed AddFooter(string text, string iconUrl = null, string proxyIconUrl = null)
{
Footer = new Footer(text, iconUrl, proxyIconUrl);
return this;
}
/// <summary>
/// Adds an int based color to the embed.Color appears as a bar on the left side of the message
/// </summary>
/// <param name="color"></param>
/// <returns></returns>
public Embed AddColor(int color)
{
if (color < 0x0 || color > 0xFFFFFF)
{
throw new Exception($"Color '{color}' is outside the valid color range");
}
Color = color;
return this;
}
/// <summary>
/// Adds a hex based color.Color appears as a bar on the left side of the message
/// </summary>
/// <param name="color">Color in string hex format</param>
/// <returns>This</returns>
/// <exception cref="Exception">Exception thrown if color is outside of range</exception>
public Embed AddColor(string color)
{
int parsedColor = int.Parse(color.TrimStart('#'), NumberStyles.AllowHexSpecifier);
if (parsedColor < 0x0 || parsedColor > 0xFFFFFF)
{
throw new Exception($"Color '{color}' is outside the valid color range");
}
Color = parsedColor;
return this;
}
/// <summary>
/// Adds a RGB based color.Color appears as a bar on the left side of the message
/// </summary>
/// <param name="red">Red value between 0 - 255</param>
/// <param name="green">Green value between 0 - 255</param>
/// <param name="blue">Blue value between 0 - 255</param>
/// <returns>This</returns>
/// <exception cref="Exception">Thrown if red, green, or blue is outside of range</exception>
public Embed AddColor(int red, int green, int blue)
{
if (red < 0 || red > 255 || green < 0 || green > 255 || green < 0 || green > 255)
{
throw new Exception($"Color Red:{red} Green:{green} Blue:{blue} is outside the valid color range.Must be between 0 - 255");
}
Color = red * 65536 + green * 256 + blue;;
return this;
}
/// <summary>
/// Adds a blank field./// If inline it will add a blank column./// If not inline will add a blank row
/// </summary>
/// <param name="inline">If the field is inline</param>
/// <returns>This</returns>
public Embed AddBlankField(bool inline)
{
Fields.Add(new Field("", "", inline));
return this;
}
/// <summary>
/// Adds a new field with the name as the title and value as the value./// If inline will add a new column.If row will add in a new row./// </summary>
/// <param name="name"></param>
/// <param name="value"></param>
/// <param name="inline"></param>
/// <returns></returns>
public Embed AddField(string name, string value, bool inline)
{
Fields.Add(new Field(name, value, inline));
return this;
}
/// <summary>
/// Adds an image to the embed.The url should point to the url of the image./// If using attachment image you can make the url: "attachment://{image name}.{image extension}
/// </summary>
/// <param name="url">Url for the image</param>
/// <param name="width">width of the image</param>
/// <param name="height">height of the image</param>
/// <param name="proxyUrl">Backup url for the image</param>
/// <returns></returns>
public Embed AddImage(string url, int? width = null, int? height = null, string proxyUrl = null)
{
Image = new Image(url, width, height, proxyUrl);
return this;
}
/// <summary>
/// Adds a thumbnail in the top right corner of the embed
/// If using attachment image you can make the url: "attachment://{image name}.{image extension}
/// </summary>
/// <param name="url">Url for the image</param>
/// <param name="width">width of the image</param>
/// <param name="height">height of the image</param>
/// <param name="proxyUrl">Backup url for the image</param>
/// <returns></returns>
public Embed AddThumbnail(string url, int? width = null, int? height = null, string proxyUrl = null)
{
Thumbnail = new Image(url, width, height, proxyUrl);
return this;
}
/// <summary>
/// Adds a video to the embed
/// </summary>
/// <param name="url">Url for the video</param>
/// <param name="width">Width of the video</param>
/// <param name="height">Height of the video</param>
/// <returns></retu
// ... [Code preview truncated due to length. Full source in attached file] ...