using System; using System.Collections.Generic; using System.Linq; using Rust; using Oxide.Core; using ConVar; using Oxide.Core.Libraries.Covalence; using Oxide.Core.Plugins; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Oxide.Ext.Discord.Builders; using Oxide.Ext.Discord.Constants; using Oxide.Ext.Discord.Entities; using Oxide.Ext.Discord.Libraries; using Oxide.Ext.Discord.Logging; using Oxide.Ext.Discord.Clients; using Oxide.Ext.Discord.Interfaces; namespace Oxide.Plugins { [Info("Live Admin Chat", "Daemante", "1.1.1")] [Description("Allows players to open a live chat with admins on Discord")] public class LiveAdminChat : RustPlugin, IDiscordPlugin { #region Variables public DiscordClient Client { get; set; } = null!; public DiscordGuild _discordGuild { get; private set; } = null!; public DiscordUser _discordBot { get; private set; } = null!; private PluginConfig _config { get; set; } = null!; private readonly ChannelMessagesRequest _messageRequest = new ChannelMessagesRequest { Limit = 50 }; private DiscordSubscriptions _subscriptions { get; set; } = null!; private string? serverName; private Dictionary _lastChatCloseTimes = new Dictionary(); #endregion #region Config public class PluginConfig { [JsonProperty(PropertyName = "Discord Bot Token")] public string ApiKey { get; set; } = string.Empty; [JsonProperty(PropertyName = "Discord Server ID")] public Snowflake GuildId { get; set; } [JsonProperty(PropertyName = "Category ID")] public Snowflake CategoryID; [JsonProperty(PropertyName = "Transcript Channel ID")] public Snowflake TranscriptChannelID { get; set; } [JsonProperty(PropertyName = "Mentioned Role IDs")] public List MentionedRoleIDs { get; set; } = new List(); [JsonProperty(PropertyName = "Reply Command")] public string ReplyCommand = "reply"; [JsonProperty(PropertyName = "Chat Prefix")] public string ChatPrefix = "ADMIN"; [JsonProperty(PropertyName = "Steam Profile Icon")] public string SteamProfileIcon = ""; [JsonProperty(PropertyName = "Default Close Reason")] public string DefaultCloseReason = "Closed by Admin"; [JsonProperty(PropertyName = "Show Admin Username")] public Boolean ShowAdminUsername = false; [JsonProperty(PropertyName = "Close On Disconnect")] public bool CloseOnDisconnect { get; set; } = false; [JsonProperty(PropertyName = "Chat Command")] public string ChatCommand = "adminchat"; [JsonProperty(PropertyName = "Chat Cooldown (minutes)")] public int ChatCooldownMinutes { get; set; } = 5; [JsonConverter(typeof(StringEnumConverter))] [JsonProperty(PropertyName = "Discord Extension Log Level (Verbose, Debug, Info, Warning, Error, Exception, Off)")] public DiscordLogLevel ExtensionDebugging { get; set; } = DiscordLogLevel.Info; } protected override void LoadConfig() { base.LoadConfig(); try { _config = Config.ReadObject(); if (_config == null) { throw new Exception(); } SaveConfig(); } catch { PrintError("Your configuration file contains an error. Using default configuration values."); LoadDefaultConfig(); } } protected override void LoadDefaultConfig() { PrintWarning("Creating a new configuration file."); _config = new PluginConfig { ApiKey = "YOUR-BOT-TOKEN", GuildId = default, CategoryID = default, TranscriptChannelID = default, SteamProfileIcon = "", MentionedRoleIDs = new List { "1234567890", "0987654321" }, ReplyCommand = "reply", ChatPrefix = "ADMIN", ShowAdminUsername = false, CloseOnDisconnect = false, DefaultCloseReason = "Closed by admin", ChatCommand = "adminchat", ChatCooldownMinutes = 5, ExtensionDebugging = DiscordLogLevel.Info }; SaveConfig(); } protected override void SaveConfig() { Config.WriteObject(_config, true); } #endregion #region Localization protected override void LoadDefaultMessages() { lang.RegisterMessages(new Dictionary { ["LiveChatNotAvailable"] = "/adminchat is not available yet.", ["LiveChatSuccess"] = "[#00C851]Your request for an admin has been sent.[/#]", ["LiveChatAlreadyCalled"] = "[#ff4444]Please wait, your chat request is already in progress.[/#]", ["LiveChatMessageLayout"] = "[#00C851]Live Admin Chat[/#] - [#dadada]Reply by typing:[/#] [#00C851]/{1} [/#]\n{0}", ["PlayerOffline"] = "Player is currently offline; message was not sent.", ["PlayerConnected"] = "**{0}** has connected to the server.", ["PlayerDisconnected"] = "**{0}** has disconnected from the server.", ["ReplyNotAvailable"] = "/{0} is not available yet.", ["ReplyCommandUsage"] = "Usage: /{0} [message]", ["ReplyNoLiveChatInProgress"] = "[#FFFF00]You have no live chat in progress.[/#]", ["ReplyWaitForAdminResponse"] = "[#ff4444]Wait until someone responds before sending another message.[/#]", ["ReplyMessageSent"] = "[#00C851]Your message has been sent.[/#]\n{0}: {1}", ["ChatClosed"] = "[#FFFF00]The live admin chat has ended.[/#]", ["NoPermission"] = "[#FFFF00]You do not have permission to use /adminchat.[/#]", ["ChatOnCooldown"] = "[#FFFF00]You must wait {0} minute(s) before starting a new chat.[/#]" }, this); } private string GetTranslation(string key, string? id = null, params object[] args) { if (covalence != null) { return covalence.FormatText(string.Format(lang.GetMessage(key, this, id), args)); } else { return string.Format(lang.GetMessage(key, this, id), args); } } #endregion #region Initialization & Setup private void Init() { _subscriptions = GetLibrary(); permission.RegisterPermission("liveadminchat.use", this); AddCovalenceCommand(_config.ChatCommand, "LiveChatCommand"); AddCovalenceCommand(_config.ReplyCommand, "ReplyCommand"); } private void OnServerInitialized() { if (string.IsNullOrEmpty(_config.ApiKey)) { PrintError("Discord Bot Token is not set. Please check your configuration."); return; } if (Client == null) { PrintError("Discord client is null. Initialization failed."); return; } try { Client.Connect(_config.ApiKey, GatewayIntents.Guilds | GatewayIntents.GuildMembers | GatewayIntents.GuildMessages | GatewayIntents.MessageContent); } catch (Exception ex) { PrintError($"Failed to connect Discord client: {ex.Message}"); } serverName = ConVar.Server.hostname; } [HookMethod(DiscordExtHooks.OnDiscordClientCreated)] private void OnDiscordClientCreated() { if (string.IsNullOrEmpty(_config.ApiKey)) { PrintWarning("Please set the Discord Bot Token and reload the plugin"); return; } if (Client == null) { PrintError("Discord client failed to initialize"); return; } } [HookMethod(DiscordExtHooks.OnDiscordGatewayReady)] private void OnDiscordGatewayReady(GatewayReadyEvent ready) { if (ready == null) { PrintError("Gateway ready event is null"); return; } if (Client?.Bot?.Application == null) { PrintError("Discord client, bot, or application is null"); return; } if (ready.Guilds == null || ready.Guilds.Count == 0) { PrintError("Your bot was not found in any discord servers. Please invite it to a server and reload the plugin."); return; } DiscordGuild? guild = null; if (ready.Guilds.Count == 1 && !_config.GuildId.IsValid()) { guild = ready.Guilds.Values.FirstOrDefault(); } if (guild == null && _config.GuildId.IsValid()) { guild = ready.Guilds[_config.GuildId]; } if (guild == null) { PrintError("Failed to find a matching guild for the Discord Server ID provided in the config."); return; } if (Client.Bot.Application.Flags.HasValue && !Client.Bot.Application.Flags.Value.HasFlag(ApplicationFlags.GatewayGuildMembersLimited)) { PrintError($"You need to enable \"Server Members Intent\" for {Client.Bot.BotUser.Username}"); return; } _discordGuild = guild; _discordBot = Client.Bot.BotUser; DiscordChannel category = _discordGuild.Channels[_config.CategoryID]; if (category == null || category.Type != ChannelType.GuildCategory) { PrintError($"Category with ID \"{_config.CategoryID}\" doesn't exist. Make sure the Category ID is correct and reload the plugin."); } foreach (DiscordChannel channel in _discordGuild.Channels.Values) { if (channel.ParentId == _config.CategoryID) { SubscribeToChannel(channel); } } Puts("Ready to start chatting!"); } #endregion #region Methods [HookMethod("StartAdminChat")] public bool StartAdminChat(string playerID) { BasePlayer? player = GetPlayerByID(playerID); if (player == null) { PrintError($"Player with ID \"{playerID}\" wasn't found!"); return false; } if (Client == null || _discordBot == null) { PrintError("Discord client or bot is null. Initialization failed."); return false; } string channelName = playerID; DiscordChannel existingChannel = _discordGuild.GetChannel(channelName); if (existingChannel != null) { PrintError($"Player \"{playerID}\" already has an opened chat!"); return false; } try { var newChannel = new ChannelCreate { Name = channelName, Type = ChannelType.GuildText, ParentId = _config.CategoryID, DefaultAutoArchiveDuration = 1440 }; _discordGuild.CreateChannel(Client, newChannel).Then(channelTask => { DiscordChannel channel = channelTask; var builder = new MessageComponentBuilder() .AddLinkButton("View steam profile", $"https://steamcommunity.com/profiles/{channel.Name.Split('_')[0]}"); var createMessage = new MessageCreate { Content = $"{string.Join(" ", _config.MentionedRoleIDs.Select(roleId => $"<@&{roleId}>"))}\nNew chat opened with Steam64ID: **{playerID}**\nYou are now chatting with: **{GetPlayerByID(playerID)?.displayName}**\n**Reply with !close to end the chat.**\n**You can supply a reason for closing the chat by typing '!close '.**", Components = builder.Build() }; channel.CreateMessage(Client, createMessage); }).Catch(exception => { PrintError($"Failed to create channel: {exception.Message}"); }); return true; } catch (Exception ex) { PrintError($"Error in StartAdminChat: {ex.Message}"); return false; } } [HookMethod("StopAdminChat")] public void StopAdminChat(string playerID, string reason = null) { DiscordChannel channel = _discordGuild.GetChannel(playerID); if (channel == null) return; DeleteChannel(channel, reason); } private bool IsLiveChatChannel(DiscordChannel channel) { return channel.ParentId == _config.CategoryID && (!_config.TranscriptChannelID.IsValid() || channel.Id != _config.TranscriptChannelID); } private void DeleteChannel(DiscordChannel channel, string reason) { if (channel == null) { PrintError("Channel is null. Unable to delete the channel."); return; } if (!IsLiveChatChannel(channel)) { return; } if (string.IsNullOrEmpty(reason)) { reason = _config.DefaultCloseReason; } channel.CreateMessage(Client, "Closing the chat in 5 seconds...") .Then(messageTask => { SendTranscript(channel, reason); timer.Once(5f, () => { channel.Delete(Client).Then(deleteTask => { Puts($"Successfully deleted channel '{channel.Name}'"); SendMessageToPlayerID(channel.Name, $"The chat has been closed by an admin. \nTicket ID: {channel.Name} \nReason: {reason}"); _lastChatCloseTimes[channel.Name] = DateTime.Now; }).Catch(exception => { PrintError($"Failed to delete channel '{channel.Name}': {exception.Message}"); }); }); }).Catch(exception => { PrintError($"Failed to send closing message: {exception.Message}"); }); } private void SubscribeToChannel(DiscordChannel channel, string reason = null) { string channelName = channel.Name; if (channelName.Length <= 2) { PrintError($"Invalid channel name: {channelName}"); return; } _subscriptions.AddChannelSubscription(Client, channel.Id, message => { if (message.Content.StartsWith("!close")) { string reason = message.Content.Length > 6 ? message.Content.Substring(7).Trim() : null; DeleteChannel(channel, reason); return; } string messageContent = _config.ChatPrefix + ": " + ""; if (_config.ShowAdminUsername) { messageContent += "[#c9c9c9]" + message.Author.Username + ": [/#]"; } messageContent += message.Content; message.CreateReaction(Client, "✅"); if (!SendMessageToPlayerID(channelName, GetTranslation("LiveChatMessageLayout", channelName, messageContent, _config.ReplyCommand))) { channel.CreateMessage(Client, new MessageCreate { Content = GetTranslation("PlayerOffline") }); } }); } private BasePlayer? GetPlayerByID(string ID) { try { return BasePlayer.FindByID(Convert.ToUInt64(ID)); } catch { return null; } } private bool SendMessageToPlayerID(string playerID, string message) { BasePlayer player = BasePlayer.FindByID(Convert.ToUInt64(playerID)); if (player == null) return false; player.Command("chat.add", (int)Chat.ChatChannel.Server, _config.SteamProfileIcon, message); return true; } private void SendTranscript(DiscordChannel channel, string reason) { try { var transcriptChannel = _discordGuild.GetChannel(_config.TranscriptChannelID); if (transcriptChannel == null) { PrintError("Transcript channel ID not set in the config. Unable to send transcript."); return; } channel.GetMessages(Client, new ChannelMessagesRequest { Limit = 100 }) .Then(messages => { if (messages == null || !messages.Any()) return; var messageList = messages.ToList(); messageList.Reverse(); var transcriptBuilder = new System.Text.StringBuilder(); transcriptBuilder.AppendLine($"{serverName}"); transcriptBuilder.AppendLine($"Transcript for ticket: {channel.Name}"); transcriptBuilder.AppendLine($"Closed at: {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); transcriptBuilder.AppendLine($"Reason for closing: {reason}"); transcriptBuilder.AppendLine("```"); foreach (var msg in messageList) { var timestamp = msg.Timestamp.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); transcriptBuilder.AppendLine($"[{timestamp}] {msg.Author.Username}: {msg.Content}"); } transcriptBuilder.AppendLine("```"); var embed = new DiscordEmbed { Title = $"Live Admin Chat", Description = transcriptBuilder.ToString(), Color = DiscordColor.Blue, Timestamp = DateTime.UtcNow }; var createMessage = new MessageCreate { Embeds = new List { embed } }; transcriptChannel.CreateMessage(Client, createMessage) .Then(message => { Puts($"Successfully created transcript for channel '{channel.Name}'"); }) .Catch(exception => { PrintError($"Failed to create transcript message: {exception.Message}"); }); return; }) .Catch(exception => { PrintError($"Failed to get messages for transcript: {exception.Message}"); }); } catch (Exception ex) { PrintError($"Error in SendTranscript: {ex.Message}"); } } #endregion #region Events [HookMethod(DiscordExtHooks.OnDiscordGuildChannelCreated)] private void OnDiscordGuildChannelCreated(DiscordChannel channel) { if (channel.ParentId == _config.CategoryID) SubscribeToChannel(channel); } [HookMethod(DiscordExtHooks.OnDiscordGuildChannelDeleted)] private void OnDiscordGuildChannelDeleted(DiscordChannel channel) { if (channel.ParentId == _config.CategoryID) { string channelName = channel.Name; if (channelName.Length != 17) return; SendMessageToPlayerID(channelName, GetTranslation("ChatClosed", channelName)); } } [HookMethod("OnPlayerConnected")] private void OnPlayerConnected(BasePlayer player) { if (player == null) return; DiscordChannel? channel = _discordGuild?.GetChannel(player.UserIDString); if (channel != null) { try { channel.CreateMessage(Client, new MessageCreate { Content = GetTranslation("PlayerConnected", player.UserIDString, player.displayName) }); } catch (Exception ex) { PrintError($"Failed to send connection message: {ex.Message}"); } } } [HookMethod("OnPlayerDisconnected")] private void OnPlayerDisconnected(BasePlayer player) { if (player == null) return; DiscordChannel? channel = _discordGuild?.GetChannel(player.UserIDString); if (channel != null) { try { channel.CreateMessage(Client, new MessageCreate { Content = GetTranslation("PlayerDisconnected", player.UserIDString, player.displayName) }); } catch (Exception ex) { PrintError($"Failed to send disconnection message: {ex.Message}"); } } if (_config.CloseOnDisconnect) { StopAdminChat(player.UserIDString); } } #endregion #region Commands [ChatCommand("adminchat")] private void LiveChatCommand(IPlayer player, string command, string[] args) { if (Client == null) { player.Reply(GetTranslation("LiveChatNotAvailable", player.Id)); PrintError("Discord client is null"); return; } if (!Client.IsConnected()) { player.Reply(GetTranslation("LiveChatNotAvailable", player.Id)); PrintError("Discord client is not connected"); return; } if (!player.HasPermission("liveadminchat.use")) { player.Reply(GetTranslation("NoPermission", player.Id)); PrintWarning("Player does not have permission."); return; } if (_discordGuild == null) { player.Reply(GetTranslation("LiveChatNotAvailable", player.Id)); PrintError("Discord guild is null."); return; } if (_lastChatCloseTimes.TryGetValue(player.Id, out DateTime lastCloseTime)) { TimeSpan timeSinceLastClose = DateTime.Now - lastCloseTime; if (timeSinceLastClose.TotalMinutes < _config.ChatCooldownMinutes) { int remainingMinutes = _config.ChatCooldownMinutes - (int)timeSinceLastClose.TotalMinutes; SendMessageToPlayerID(player.Id, GetTranslation("ChatOnCooldown", player.Id, remainingMinutes)); return; } } try { bool chatStarted = StartAdminChat(player.Id); if (!chatStarted) { SendMessageToPlayerID( player.Id, GetTranslation("LiveChatAlreadyCalled", player.Id) ); } else { SendMessageToPlayerID( player.Id, GetTranslation("LiveChatSuccess", player.Id) ); } } catch (Exception ex) { PrintError($"Error in LiveChatCommand: {ex.Message}"); player.Reply("An error occurred while processing your request."); } } private void ReplyCommand(IPlayer player, string command, string[] args) { if (_discordGuild == null) { player.Reply(GetTranslation("ReplyNotAvailable", player.Id, _config.ReplyCommand)); return; } if (args.Length < 1) { player.Reply(GetTranslation("ReplyCommandUsage", player.Id, _config.ReplyCommand)); return; } DiscordChannel replyChannel = _discordGuild.GetChannel(player.Id); string sentMessage = string.Join(" ", args); if (replyChannel == null) { SendMessageToPlayerID(player.Id, GetTranslation("ReplyNoLiveChatInProgress", player.Id)); return; } replyChannel.GetMessages(Client, _messageRequest).Then(messagesTask => { var messages = messagesTask; if (messages.Count < 2) { SendMessageToPlayerID(player.Id, GetTranslation("ReplyWaitForAdminResponse", player.Id)); return; } DateTime now = DateTime.Now; replyChannel.CreateMessage(Client, new MessageCreate { Content = $"{player.Name}: {sentMessage}" }); SendMessageToPlayerID(player.Id, GetTranslation("ReplyMessageSent", player.Id, player.Name, sentMessage)); }).Catch(exception => { PrintError($"Failed to get messages: {exception.Message}"); }); } #endregion } }