C#:
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
namespace Oxide.Plugins
{
[Info("PluginLister", "Milestorme", "1.5.2")]
[Description("Lists installed plugins in-game and sends a polished summary to a Discord webhook.")]
public class PluginLister : CovalencePlugin
{
private PluginConfig config;
private const string CurrentConfigVersion = "1.5";
private const string AdminPermission = "pluginlister.admin";
private readonly Dictionary<string, DateTime> cooldowns = new Dictionary<string, DateTime>();
private class PluginConfig
{
[JsonProperty("Enable Plugin")]
public bool EnablePlugin { get; set; } = true;
[JsonProperty("Enable Discord Webhook")]
public bool EnableDiscordWebhook { get; set; } = true;
[JsonProperty("Webhook URL")]
public string WebhookUrl { get; set; } = "YOUR_DISCORD_WEBHOOK_URL_HERE";
[JsonProperty("Command Cooldown Seconds")]
public int CommandCooldownSeconds { get; set; } = 30;
[JsonProperty("Sort Plugins Alphabetically")]
public bool SortPluginsAlphabetically { get; set; } = true;
[JsonProperty("Discord: Webhook Username")]
public string WebhookUsername { get; set; } = "RustStorm";
[JsonProperty("Discord: Avatar URL")]
public string WebhookAvatarUrl { get; set; } = string.Empty;
[JsonProperty("Discord: Embed Title")]
public string EmbedTitle { get; set; } = "🌩 RustStorm Plugin Audit";
[JsonProperty("Discord: Embed Color")]
public int EmbedColor { get; set; } = 5814783;
[JsonProperty("Discord: Footer Text")]
public string FooterText { get; set; } = "RustStorm • Plugin Inventory";
[JsonProperty("Discord: Embed Description")]
public string EmbedDescription { get; set; } = "Live plugin inventory snapshot from your RustStorm server.";
[JsonProperty("Discord: Thumbnail URL")]
public string ThumbnailUrl { get; set; } = string.Empty;
[JsonProperty("Discord: Banner Image URL")]
public string BannerImageUrl { get; set; } = string.Empty;
[JsonProperty("Discord: Mention Everyone")]
public bool MentionEveryone { get; set; } = false;
[JsonProperty("Discord: Max Plugins Per Embed")]
public int MaxPluginsPerEmbed { get; set; } = 20;
[JsonProperty("Discord: Include Text Fallback")]
public bool IncludeTextFallback { get; set; } = true;
[JsonProperty("Plugin Blacklist")]
public List<string> PluginBlacklist { get; set; } = new List<string>
{
"Unity",
"Rust",
"PlayerAdministration",
"PermissionsManager",
"Copy Paste",
"AdminMenu",
"Admin Panel"
};
[JsonProperty("Config Version")]
public string ConfigVersion { get; set; } = CurrentConfigVersion;
}
private class PluginInfo
{
public int Number { get; set; }
public string Name { get; set; }
public string Version { get; set; }
}
protected override void LoadDefaultConfig()
{
config = new PluginConfig();
SaveConfig();
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<PluginConfig>();
if (config == null)
throw new Exception("Configuration file is null");
Puts("Config loaded successfully.");
Puts("Config Version: " + config.ConfigVersion);
if (config.ConfigVersion != CurrentConfigVersion)
{
Puts("Updating configuration to match the latest version.");
UpdateConfig();
}
}
catch (Exception ex)
{
Puts("Configuration file is corrupted or missing, creating a new one. " + ex.Message);
LoadDefaultConfig();
}
}
protected override void SaveConfig() => Config.WriteObject(config, true);
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["NoPermission"] = "You do not have permission to use this command.",
["PluginDisabled"] = "This plugin is currently disabled.",
["NoPlugins"] = "No plugins are currently installed.",
["CooldownMessage"] = "You must wait {0} seconds before using this command again.",
["PluginsListed"] = "Installed Plugins ({0}):\n{1}",
["DiscordSent"] = "Plugin list sent to Discord."
}, this);
}
private void UpdateConfig()
{
if (string.IsNullOrWhiteSpace(config.WebhookUrl))
config.WebhookUrl = "YOUR_DISCORD_WEBHOOK_URL_HERE";
if (config.CommandCooldownSeconds <= 0)
config.CommandCooldownSeconds = 30;
if (config.MaxPluginsPerEmbed <= 0)
config.MaxPluginsPerEmbed = 20;
if (config.MaxPluginsPerEmbed > 50)
config.MaxPluginsPerEmbed = 50;
if (string.IsNullOrWhiteSpace(config.WebhookUsername))
config.WebhookUsername = "RustStorm";
if (string.IsNullOrWhiteSpace(config.EmbedTitle))
config.EmbedTitle = "🌩 RustStorm Plugin Audit";
if (string.IsNullOrWhiteSpace(config.EmbedDescription))
config.EmbedDescription = "Live plugin inventory snapshot from your RustStorm server.";
if (string.IsNullOrWhiteSpace(config.FooterText))
config.FooterText = "RustStorm • Plugin Inventory";
if (config.PluginBlacklist == null)
{
config.PluginBlacklist = new List<string>
{
"Unity",
"Rust",
"PlayerAdministration",
"PermissionsManager",
"CopyPaste",
"AdminMenu",
"AdminPanel"
};
}
config.ConfigVersion = CurrentConfigVersion;
SaveConfig();
}
private void Init()
{
permission.RegisterPermission(AdminPermission, this);
}
private void ConditionalPrint(string message) => Puts(message);
private string Lang(string key, string playerId = null, params object[] args)
{
var message = lang.GetMessage(key, this, playerId);
return args == null || args.Length == 0 ? message : string.Format(message, args);
}
private List<Plugin> GetFilteredPlugins()
{
var blacklist = new HashSet<string>(
config.PluginBlacklist ?? new List<string>(),
StringComparer.OrdinalIgnoreCase);
var plugins = Interface.Oxide.RootPluginManager.GetPlugins()
.Where(p => p != null && !blacklist.Contains(p.Title))
.ToList();
if (config.SortPluginsAlphabetically)
plugins = plugins.OrderBy(p => p.Title, StringComparer.OrdinalIgnoreCase).ToList();
return plugins;
}
[Command("listplugins")]
[Permission(AdminPermission)]
private void ListPluginsCommand(IPlayer player, string command, string[] args)
{
if (!config.EnablePlugin)
{
player.Reply(Lang("PluginDisabled", player.Id));
return;
}
if (cooldowns.ContainsKey(player.Id) && cooldowns[player.Id] > DateTime.Now)
{
var remaining = (cooldowns[player.Id] - DateTime.Now).TotalSeconds;
player.Reply(Lang("CooldownMessage", player.Id, Math.Ceiling(remaining)));
return;
}
cooldowns[player.Id] = DateTime.Now.AddSeconds(config.CommandCooldownSeconds);
var plugins = GetFilteredPlugins();
if (!plugins.Any())
{
player.Reply(Lang("NoPlugins", player.Id));
return;
}
var pluginNames = plugins.Select((p, i) => $"{i + 1}. {p.Title} v{p.Version}").ToList();
player.Reply(Lang("PluginsListed", player.Id, pluginNames.Count, string.Join("\n", pluginNames)));
if (config.EnableDiscordWebhook)
{
SendToDiscord(player, plugins);
player.Reply(Lang("DiscordSent", player.Id));
}
}
private void SendToDiscord(IPlayer requester, List<Plugin> plugins)
{
if (string.IsNullOrWhiteSpace(config.WebhookUrl) || config.WebhookUrl == "YOUR_DISCORD_WEBHOOK_URL_HERE")
{
ConditionalPrint("Webhook URL is not configured or is using the default placeholder.");
return;
}
var pluginDetails = plugins.Select((p, i) => new PluginInfo
{
Number = i + 1,
Name = p.Title,
Version = p.Version != null ? p.Version.ToString() : "Unknown"
}).ToList();
var embeds = BuildDiscordEmbeds(requester, pluginDetails);
var requests = BuildWebhookPayloads(embeds, plugins.Count);
ConditionalPrint($"Preparing {requests.Count} Discord webhook request(s) for {plugins.Count} plugin(s).");
SendDiscordPayloadsSequentially(requests, 0);
}
private List<Dictionary<string, object>> BuildWebhookPayloads(List<Dictionary<string, object>> embeds, int pluginCount)
{
const int maxEmbedsPerRequest = 10;
var payloads = new List<Dictionary<string, object>>();
for (var i = 0; i < embeds.Count; i += maxEmbedsPerRequest)
{
var payload = new Dictionary<string, object>
{
["username"] = config.WebhookUsername,
["embeds"] = embeds.Skip(i).Take(maxEmbedsPerRequest).ToList()
};
if (config.IncludeTextFallback)
{
payload["content"] = config.MentionEveryone
? $"@everyone RustStorm plugin audit generated ({pluginCount} plugins)."
: $"RustStorm plugin audit generated ({pluginCount} plugins).";
}
else if (config.MentionEveryone)
{
payload["content"] = "@everyone";
}
if (!string.IsNullOrWhiteSpace(config.WebhookAvatarUrl))
payload["avatar_url"] = config.WebhookAvatarUrl;
payloads.Add(payload);
}
return payloads;
}
private List<Dictionary<string, object>> BuildDiscordEmbeds(IPlayer requester, List<PluginInfo> plugins)
{
var embeds = new List<Dictionary<string, object>>();
var chunkSize = Math.Max(1, Math.Min(config.MaxPluginsPerEmbed, 50));
var totalPages = (int)Math.Ceiling(plugins.Count / (double)chunkSize);
for (var page = 0; page < totalPages; page++)
{
var chunk = plugins.Skip(page * chunkSize).Take(chunkSize).ToList();
var descriptionLines = chunk.Select(p => $"`{p.Number,2}.` **{EscapeDiscord(p.Name)}** `v{EscapeDiscord(p.Version)}`").ToList();
var description = string.Join("\n", descriptionLines);
if (!string.IsNullOrWhiteSpace(config.EmbedDescription))
description = config.EmbedDescription + "\n\n" + description;
if (description.Length > 4096)
description = description.Substring(0, 4090) + "...";
var fields = new List<object>();
if (page == 0)
{
fields.Add(new
{
name = "Server Snapshot",
value = $"**Total Plugins:** {plugins.Count}\n**Requested By:** {EscapeDiscord(requester?.Name ?? "Console")}\n**Requested At:** <t:{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}:F>",
inline = false
});
}
var embed = new Dictionary<string, object>
{
["title"] = totalPages > 1 ? $"{config.EmbedTitle} — Page {page + 1}/{totalPages}" : config.EmbedTitle,
["description"] = description,
["color"] = config.EmbedColor,
["fields"] = fields,
["footer"] = new { text = config.FooterText },
["timestamp"] = DateTime.UtcNow.ToString("o")
};
if (!string.IsNullOrWhiteSpace(config.ThumbnailUrl))
embed["thumbnail"] = new { url = config.ThumbnailUrl };
if (!string.IsNullOrWhiteSpace(config.BannerImageUrl))
embed["image"] = new { url = config.BannerImageUrl };
embeds.Add(embed);
}
return embeds;
}
private void SendDiscordPayloadsSequentially(List<Dictionary<string, object>> payloads, int index)
{
if (payloads == null || index >= payloads.Count)
return;
var payloadJson = JsonConvert.SerializeObject(payloads[index]);
SendDiscordPayload(payloadJson, () => SendDiscordPayloadsSequentially(payloads, index + 1));
}
private void SendDiscordPayload(string payload, Action onSuccess = null, int retryCount = 0)
{
var headers = new Dictionary<string, string>
{
{ "Content-Type", "application/json" }
};
var url = config.WebhookUrl;
if (!url.Contains("wait=true"))
url += (url.Contains("?") ? "&" : "?") + "wait=true";
webrequest.Enqueue(
url,
payload,
(code, response) =>
{
if (code != 200 && code != 204)
{
ConditionalPrint($"Failed to send data to Discord. Response code: {code}. Response: {response}");
if (retryCount < 3)
timer.Once(5f, () => SendDiscordPayload(payload, onSuccess, retryCount + 1));
return;
}
ConditionalPrint($"Successfully sent data to Discord. Response code: {code}.");
if (!string.IsNullOrWhiteSpace(response))
ConditionalPrint($"Discord response: {response}");
onSuccess?.Invoke();
},
this,
RequestMethod.POST,
headers
);
}
private string EscapeDiscord(string value)
{
if (string.IsNullOrEmpty(value))
return string.Empty;
return value
.Replace("\\", "\\\\")
.Replace("*", "\\*")
.Replace("_", "\\_")
.Replace("`", "\\`")
.Replace("~", "\\~")
.Replace("|", "\\|");
}
}
}