using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Server.Administration.Systems;
-using Content.Server.MoMMI;
+using Content.Server.Discord.DiscordLink;
using Content.Server.Players.RateLimiting;
using Content.Server.Preferences.Managers;
using Content.Shared.Administration;
[Dependency] private readonly IReplayRecordingManager _replay = default!;
[Dependency] private readonly IServerNetManager _netManager = default!;
- [Dependency] private readonly IMoMMILink _mommiLink = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IServerPreferencesManager _preferencesManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!;
[Dependency] private readonly ISharedPlayerManager _player = default!;
+ [Dependency] private readonly DiscordChatLink _discordLink = default!;
/// <summary>
/// The maximum length a player-sent message can be sent
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Hook OOC from {sender}: {message}");
}
+ public void SendHookAdmin(string sender, string message)
+ {
+ var wrappedMessage = Loc.GetString("chat-manager-send-hook-admin-wrap-message", ("senderName", sender), ("message", FormattedMessage.EscapeText(message)));
+ ChatMessageToAll(ChatChannel.AdminChat, message, wrappedMessage, source: EntityUid.Invalid, hideChat: false, recordReplay: false);
+ _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Hook admin from {sender}: {message}");
+ }
+
#endregion
#region Public OOC Chat API
//TODO: player.Name color, this will need to change the structure of the MsgChatMessage
ChatMessageToAll(ChatChannel.OOC, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride, author: player.UserId);
- _mommiLink.SendOOCMessage(player.Name, message.Replace("@", "\\@").Replace("<", "\\<").Replace("/", "\\/")); // @ and < are both problematic for discord due to pinging. / is sanitized solely to kneecap links to murder embeds via blunt force
+ _discordLink.SendMessage(message, player.Name, ChatChannel.OOC);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"OOC from {player:Player}: {message}");
}
author: player.UserId);
}
+ _discordLink.SendMessage(message, player.Name, ChatChannel.AdminChat);
_adminLogger.Add(LogType.Chat, $"Admin chat from {player:Player}: {message}");
}
void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type);
void SendHookOOC(string sender, string message);
+ void SendHookAdmin(string sender, string message);
void SendAdminAnnouncement(string message, AdminFlags? flagBlacklist = null, AdminFlags? flagWhitelist = null);
void SendAdminAnnouncementMessage(ICommonSession player, string message, bool suppressLog = true);
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
<ItemGroup>
+ <PackageReference Include="Discord.Net" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
--- /dev/null
+using System.Threading.Tasks;
+using Content.Server.Chat.Managers;
+using Content.Shared.CCVar;
+using Content.Shared.Chat;
+using Discord.WebSocket;
+using Robust.Shared.Asynchronous;
+using Robust.Shared.Configuration;
+
+namespace Content.Server.Discord.DiscordLink;
+
+public sealed class DiscordChatLink
+{
+ [Dependency] private readonly DiscordLink _discordLink = default!;
+ [Dependency] private readonly IConfigurationManager _configurationManager = default!;
+ [Dependency] private readonly IChatManager _chatManager = default!;
+ [Dependency] private readonly ITaskManager _taskManager = default!;
+
+ private ulong? _oocChannelId;
+ private ulong? _adminChannelId;
+
+ public void Initialize()
+ {
+ _discordLink.OnMessageReceived += OnMessageReceived;
+
+ _configurationManager.OnValueChanged(CCVars.OocDiscordChannelId, OnOocChannelIdChanged, true);
+ _configurationManager.OnValueChanged(CCVars.AdminChatDiscordChannelId, OnAdminChannelIdChanged, true);
+ }
+
+ public void Shutdown()
+ {
+ _discordLink.OnMessageReceived -= OnMessageReceived;
+
+ _configurationManager.UnsubValueChanged(CCVars.OocDiscordChannelId, OnOocChannelIdChanged);
+ _configurationManager.UnsubValueChanged(CCVars.AdminChatDiscordChannelId, OnAdminChannelIdChanged);
+ }
+
+ private void OnOocChannelIdChanged(string channelId)
+ {
+ if (string.IsNullOrEmpty(channelId))
+ {
+ _oocChannelId = null;
+ return;
+ }
+
+ _oocChannelId = ulong.Parse(channelId);
+ }
+
+ private void OnAdminChannelIdChanged(string channelId)
+ {
+ if (string.IsNullOrEmpty(channelId))
+ {
+ _adminChannelId = null;
+ return;
+ }
+
+ _adminChannelId = ulong.Parse(channelId);
+ }
+
+ private void OnMessageReceived(SocketMessage message)
+ {
+ if (message.Author.IsBot)
+ return;
+
+ var contents = message.Content.ReplaceLineEndings(" ");
+
+ if (message.Channel.Id == _oocChannelId)
+ {
+ _taskManager.RunOnMainThread(() => _chatManager.SendHookOOC(message.Author.Username, contents));
+ }
+ else if (message.Channel.Id == _adminChannelId)
+ {
+ _taskManager.RunOnMainThread(() => _chatManager.SendHookAdmin(message.Author.Username, contents));
+ }
+ }
+
+ public async Task SendMessage(string message, string author, ChatChannel channel)
+ {
+ var channelId = channel switch
+ {
+ ChatChannel.OOC => _oocChannelId,
+ ChatChannel.AdminChat => _adminChannelId,
+ _ => throw new InvalidOperationException("Channel not linked to Discord."),
+ };
+
+ if (channelId == null)
+ {
+ // Configuration not set up. Ignore.
+ return;
+ }
+
+ // @ and < are both problematic for discord due to pinging. / is sanitized solely to kneecap links to murder embeds via blunt force
+ message = message.Replace("@", "\\@").Replace("<", "\\<").Replace("/", "\\/");
+
+ await _discordLink.SendMessageAsync(channelId.Value, $"**{channel.GetString()}**: `{author}`: {message}");
+ }
+}
--- /dev/null
+using System.Linq;
+using System.Threading.Tasks;
+using Content.Shared.CCVar;
+using Discord;
+using Discord.WebSocket;
+using Robust.Shared.Configuration;
+using Robust.Shared.Reflection;
+using Robust.Shared.Utility;
+using LogMessage = Discord.LogMessage;
+
+namespace Content.Server.Discord.DiscordLink;
+
+/// <summary>
+/// Represents the arguments for the <see cref="DiscordLink.OnCommandReceived"/> event.
+/// </summary>
+public sealed class CommandReceivedEventArgs
+{
+ /// <summary>
+ /// The command that was received. This is the first word in the message, after the bot prefix.
+ /// </summary>
+ public string Command { get; init; } = string.Empty;
+
+ /// <summary>
+ /// The arguments to the command. This is everything after the command
+ /// </summary>
+ public string Arguments { get; init; } = string.Empty;
+ /// <summary>
+ /// Information about the message that the command was received from. This includes the message content, author, etc.
+ /// Use this to reply to the message, delete it, etc.
+ /// </summary>
+ public SocketMessage Message { get; init; } = default!;
+}
+
+/// <summary>
+/// Handles the connection to Discord and provides methods to interact with it.
+/// </summary>
+public sealed class DiscordLink : IPostInjectInit
+{
+ [Dependency] private readonly ILogManager _logManager = default!;
+ [Dependency] private readonly IConfigurationManager _configuration = default!;
+
+ /// <summary>
+ /// The Discord client. This is null if the bot is not connected.
+ /// </summary>
+ /// <remarks>
+ /// This should not be used directly outside of DiscordLink. So please do not make it public. Use the methods in this class instead.
+ /// </remarks>
+ private DiscordSocketClient? _client;
+ private ISawmill _sawmill = default!;
+ private ISawmill _sawmillLog = default!;
+
+ private ulong _guildId;
+ private string _botToken = string.Empty;
+
+ public string BotPrefix = default!;
+ /// <summary>
+ /// If the bot is currently connected to Discord.
+ /// </summary>
+ public bool IsConnected => _client != null;
+
+ #region Events
+
+ /// <summary>
+ /// Event that is raised when a command is received from Discord.
+ /// </summary>
+ public event Action<CommandReceivedEventArgs>? OnCommandReceived;
+ /// <summary>
+ /// Event that is raised when a message is received from Discord. This is raised for every message, including commands.
+ /// </summary>
+ public event Action<SocketMessage>? OnMessageReceived;
+
+ public void RegisterCommandCallback(Action<CommandReceivedEventArgs> callback, string command)
+ {
+ OnCommandReceived += args =>
+ {
+ if (args.Command == command)
+ callback(args);
+ };
+ }
+
+ #endregion
+
+ public void Initialize()
+ {
+ _configuration.OnValueChanged(CCVars.DiscordGuildId, OnGuildIdChanged, true);
+ _configuration.OnValueChanged(CCVars.DiscordPrefix, OnPrefixChanged, true);
+
+ if (_configuration.GetCVar(CCVars.DiscordToken) is not { } token || token == string.Empty)
+ {
+ _sawmill.Info("No Discord token specified, not connecting.");
+ return;
+ }
+
+ // If the Guild ID is empty OR the prefix is empty, we don't want to connect to Discord.
+ if (_guildId == 0 || BotPrefix == string.Empty)
+ {
+ // This is a warning, not info, because it's a configuration error.
+ // It is valid to not have a Discord token set which is why the above check is an info.
+ // But if you have a token set, you should also have a guild ID and prefix set.
+ _sawmill.Warning("No Discord guild ID or prefix specified, not connecting.");
+ return;
+ }
+
+ _client = new DiscordSocketClient(new DiscordSocketConfig()
+ {
+ GatewayIntents = GatewayIntents.All
+ });
+ _client.Log += Log;
+ _client.MessageReceived += OnCommandReceivedInternal;
+ _client.MessageReceived += OnMessageReceivedInternal;
+
+ _botToken = token;
+ // Since you cannot change the token while the server is running / the DiscordLink is initialized,
+ // we can just set the token without updating it every time the cvar changes.
+
+ _client.Ready += () =>
+ {
+ _sawmill.Info("Discord client ready.");
+ return Task.CompletedTask;
+ };
+
+ Task.Run(async () =>
+ {
+ try
+ {
+ await LoginAsync(token);
+ }
+ catch (Exception e)
+ {
+ _sawmill.Error("Failed to connect to Discord!", e);
+ }
+ });
+ }
+
+ public async Task Shutdown()
+ {
+ if (_client != null)
+ {
+ _sawmill.Info("Disconnecting from Discord.");
+
+ // Unsubscribe from the events.
+ _client.MessageReceived -= OnCommandReceivedInternal;
+ _client.MessageReceived -= OnMessageReceivedInternal;
+
+ await _client.LogoutAsync();
+ await _client.StopAsync();
+ await _client.DisposeAsync();
+ _client = null;
+ }
+
+ _configuration.UnsubValueChanged(CCVars.DiscordGuildId, OnGuildIdChanged);
+ _configuration.UnsubValueChanged(CCVars.DiscordPrefix, OnPrefixChanged);
+ }
+
+ void IPostInjectInit.PostInject()
+ {
+ _sawmill = _logManager.GetSawmill("discord.link");
+ _sawmillLog = _logManager.GetSawmill("discord.link.log");
+ }
+
+ private void OnGuildIdChanged(string guildId)
+ {
+ _guildId = ulong.TryParse(guildId, out var id) ? id : 0;
+ }
+
+ private void OnPrefixChanged(string prefix)
+ {
+ BotPrefix = prefix;
+ }
+
+ private async Task LoginAsync(string token)
+ {
+ DebugTools.Assert(_client != null);
+ DebugTools.Assert(_client.LoginState == LoginState.LoggedOut);
+
+ await _client.LoginAsync(TokenType.Bot, token);
+ await _client.StartAsync();
+
+
+ _sawmill.Info("Connected to Discord.");
+ }
+
+ private string FormatLog(LogMessage msg)
+ {
+ return msg.Exception is null
+ ? $"{msg.Source}: {msg.Message}"
+ : $"{msg.Source}: {msg.Message}\n{msg.Exception}";
+ }
+
+ private Task Log(LogMessage msg)
+ {
+ var logLevel = msg.Severity switch
+ {
+ LogSeverity.Critical => LogLevel.Fatal,
+ LogSeverity.Error => LogLevel.Error,
+ LogSeverity.Warning => LogLevel.Warning,
+ _ => LogLevel.Debug
+ };
+
+ _sawmillLog.Log(logLevel, FormatLog(msg));
+ return Task.CompletedTask;
+ }
+
+ private Task OnCommandReceivedInternal(SocketMessage message)
+ {
+ var content = message.Content;
+ // If the message doesn't start with the bot prefix, ignore it.
+ if (!content.StartsWith(BotPrefix))
+ return Task.CompletedTask;
+
+ // Split the message into the command and the arguments.
+ var trimmedInput = content[BotPrefix.Length..].Trim();
+ var firstSpaceIndex = trimmedInput.IndexOf(' ');
+
+ string command, arguments;
+
+ if (firstSpaceIndex == -1)
+ {
+ command = trimmedInput;
+ arguments = string.Empty;
+ }
+ else
+ {
+ command = trimmedInput[..firstSpaceIndex];
+ arguments = trimmedInput[(firstSpaceIndex + 1)..].Trim();
+ }
+
+ // Raise the event!
+ OnCommandReceived?.Invoke(new CommandReceivedEventArgs
+ {
+ Command = command,
+ Arguments = arguments,
+ Message = message,
+ });
+ return Task.CompletedTask;
+ }
+
+ private Task OnMessageReceivedInternal(SocketMessage message)
+ {
+ OnMessageReceived?.Invoke(message);
+ return Task.CompletedTask;
+ }
+
+ #region Proxy methods
+
+ /// <summary>
+ /// Sends a message to a Discord channel with the specified ID. Without any mentions.
+ /// </summary>
+ public async Task SendMessageAsync(ulong channelId, string message)
+ {
+ if (_client == null)
+ {
+ return;
+ }
+
+ var channel = _client.GetChannel(channelId) as IMessageChannel;
+ if (channel == null)
+ {
+ _sawmill.Error("Tried to send a message to Discord but the channel {Channel} was not found.", channel);
+ return;
+ }
+
+ await channel.SendMessageAsync(message, allowedMentions: AllowedMentions.None);
+ }
+
+ #endregion
+}
using Content.Server.Chat.Managers;
using Content.Server.Connection;
using Content.Server.Database;
+using Content.Server.Discord.DiscordLink;
using Content.Server.EUI;
using Content.Server.GameTicking;
using Content.Server.GhostKick;
IoCManager.Resolve<IAdminManager>().Initialize();
IoCManager.Resolve<IAfkManager>().Initialize();
IoCManager.Resolve<RulesManager>().Initialize();
+
+ IoCManager.Resolve<DiscordLink>().Initialize();
+ IoCManager.Resolve<DiscordChatLink>().Initialize();
+
_euiManager.Initialize();
IoCManager.Resolve<IGameMapManager>().Initialize();
_playTimeTracking?.Shutdown();
_dbManager?.Shutdown();
IoCManager.Resolve<ServerApi>().Shutdown();
+
+ IoCManager.Resolve<DiscordLink>().Shutdown();
+ IoCManager.Resolve<DiscordChatLink>().Shutdown();
}
private static void LoadConfigPresets(IConfigurationManager cfg, IResourceManager res, ISawmill sawmill)
using Content.Server.Connection;
using Content.Server.Database;
using Content.Server.Discord;
+using Content.Server.Discord.DiscordLink;
using Content.Server.Discord.WebhookMessages;
using Content.Server.EUI;
using Content.Server.GhostKick;
using Content.Server.Info;
using Content.Server.Mapping;
using Content.Server.Maps;
-using Content.Server.MoMMI;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.Players.JobWhitelist;
using Content.Server.Players.PlayTimeTracking;
IoCManager.Register<IChatManager, ChatManager>();
IoCManager.Register<ISharedChatManager, ChatManager>();
IoCManager.Register<IChatSanitizationManager, ChatSanitizationManager>();
- IoCManager.Register<IMoMMILink, MoMMILink>();
IoCManager.Register<IServerPreferencesManager, ServerPreferencesManager>();
IoCManager.Register<IServerDbManager, ServerDbManager>();
IoCManager.Register<RecipeManager, RecipeManager>();
IoCManager.Register<ConnectionManager>();
IoCManager.Register<MultiServerKickManager>();
IoCManager.Register<CVarControlManager>();
+
+ IoCManager.Register<DiscordLink>();
+ IoCManager.Register<DiscordChatLink>();
}
}
}
+++ /dev/null
-namespace Content.Server.MoMMI
-{
- public interface IMoMMILink
- {
- void SendOOCMessage(string sender, string message);
- }
-}
+++ /dev/null
-using System.Net;
-using System.Net.Http;
-using System.Net.Http.Json;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using System.Threading.Tasks;
-using Content.Server.Chat.Managers;
-using Content.Shared.CCVar;
-using Robust.Server.ServerStatus;
-using Robust.Shared.Asynchronous;
-using Robust.Shared.Configuration;
-
-namespace Content.Server.MoMMI
-{
- internal sealed class MoMMILink : IMoMMILink, IPostInjectInit
- {
- [Dependency] private readonly IConfigurationManager _configurationManager = default!;
- [Dependency] private readonly IStatusHost _statusHost = default!;
- [Dependency] private readonly IChatManager _chatManager = default!;
- [Dependency] private readonly ITaskManager _taskManager = default!;
-
- private readonly HttpClient _httpClient = new();
-
- void IPostInjectInit.PostInject()
- {
- _statusHost.AddHandler(HandleChatPost);
- }
-
- public async void SendOOCMessage(string sender, string message)
- {
- var sentMessage = new MoMMIMessageOOC
- {
- Sender = sender,
- Contents = message
- };
-
- await SendMessageInternal("ooc", sentMessage);
- }
-
- private async Task SendMessageInternal(string type, object messageObject)
- {
- var url = _configurationManager.GetCVar(CCVars.StatusMoMMIUrl);
- var password = _configurationManager.GetCVar(CCVars.StatusMoMMIPassword);
- if (string.IsNullOrWhiteSpace(url))
- {
- return;
- }
-
- if (string.IsNullOrWhiteSpace(password))
- {
- Logger.WarningS("mommi", "MoMMI URL specified but not password!");
- return;
- }
-
- var sentMessage = new MoMMIMessageBase
- {
- Password = password,
- Type = type,
- Contents = messageObject
- };
-
- var request = await _httpClient.PostAsJsonAsync(url, sentMessage);
-
- if (!request.IsSuccessStatusCode)
- {
- throw new Exception($"MoMMI returned bad status code: {request.StatusCode}");
- }
- }
-
- private async Task<bool> HandleChatPost(IStatusHandlerContext context)
- {
- if (context.RequestMethod != HttpMethod.Post || context.Url.AbsolutePath != "/ooc")
- {
- return false;
- }
-
- var password = _configurationManager.GetCVar(CCVars.StatusMoMMIPassword);
-
- if (string.IsNullOrEmpty(password))
- {
- await context.RespondErrorAsync(HttpStatusCode.Forbidden);
- return true;
- }
-
- OOCPostMessage? message = null;
- try
- {
- message = await context.RequestBodyJsonAsync<OOCPostMessage>();
- }
- catch (JsonException)
- {
- // message null so enters block down below.
- }
-
- if (message == null)
- {
- await context.RespondErrorAsync(HttpStatusCode.BadRequest);
- return true;
- }
-
- if (message.Password != password)
- {
- await context.RespondErrorAsync(HttpStatusCode.Forbidden);
- return true;
- }
-
- var sender = message.Sender;
- var contents = message.Contents.ReplaceLineEndings(" ");
-
- _taskManager.RunOnMainThread(() => _chatManager.SendHookOOC(sender, contents));
-
- await context.RespondAsync("Success", HttpStatusCode.OK);
- return true;
- }
-
- private sealed class MoMMIMessageBase
- {
- [JsonInclude] [JsonPropertyName("password")]
- public string Password = null!;
-
- [JsonInclude] [JsonPropertyName("type")]
- public string Type = null!;
-
- [JsonInclude] [JsonPropertyName("contents")]
- public object Contents = null!;
- }
-
- private sealed class MoMMIMessageOOC
- {
- [JsonInclude] [JsonPropertyName("sender")]
- public string Sender = null!;
-
- [JsonInclude] [JsonPropertyName("contents")]
- public string Contents = null!;
- }
-
- private sealed class OOCPostMessage
- {
-#pragma warning disable CS0649
- [JsonInclude] [JsonPropertyName("password")]
- public string Password = null!;
-
- [JsonInclude] [JsonPropertyName("sender")]
- public string Sender = null!;
-
- [JsonInclude] [JsonPropertyName("contents")]
- public string Contents = null!;
-#pragma warning restore CS0649
- }
- }
-}
--- /dev/null
+using Robust.Shared.Configuration;
+
+namespace Content.Shared.CCVar;
+
+public sealed partial class CCVars
+{
+ /// <summary>
+ /// The discord channel ID to send admin chat messages to (also receive them). This requires the Discord Integration to be enabled and configured.
+ /// </summary>
+ public static readonly CVarDef<string> AdminChatDiscordChannelId =
+ CVarDef.Create("admin.chat_discord_channel_id", string.Empty, CVar.SERVERONLY);
+}
public static readonly CVarDef<bool> ShowOocPatronColor =
CVarDef.Create("ooc.show_ooc_patron_color", true, CVar.ARCHIVE | CVar.REPLICATED | CVar.CLIENT);
+
+ /// <summary>
+ /// The discord channel ID to send OOC messages to (also recieve them). This requires the Discord Integration to be enabled and configured.
+ /// </summary>
+ public static readonly CVarDef<string> OocDiscordChannelId =
+ CVarDef.Create("ooc.discord_channel_id", string.Empty, CVar.SERVERONLY);
}
-using Robust.Shared.Configuration;
-using Robust.Shared.Maths;
+using Robust.Shared.Configuration;
namespace Content.Shared.CCVar;
public static readonly CVarDef<string> DiscordRoundEndRoleWebhook =
CVarDef.Create("discord.round_end_role", string.Empty, CVar.SERVERONLY);
+
+ /// <summary>
+ /// The token used to authenticate with Discord. For the Bot to function set: discord.token, discord.guild_id, and discord.prefix.
+ /// If this is empty, the bot will not connect.
+ /// </summary>
+ public static readonly CVarDef<string> DiscordToken =
+ CVarDef.Create("discord.token", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL);
+
+ /// <summary>
+ /// The Discord guild ID to use for commands as well as for several other features.
+ /// If this is empty, the bot will not connect.
+ /// </summary>
+ public static readonly CVarDef<string> DiscordGuildId =
+ CVarDef.Create("discord.guild_id", string.Empty, CVar.SERVERONLY);
+
+ /// <summary>
+ /// Prefix used for commands for the Discord bot.
+ /// If this is empty, the bot will not connect.
+ /// </summary>
+ public static readonly CVarDef<string> DiscordPrefix =
+ CVarDef.Create("discord.prefix", "!", CVar.SERVERONLY);
+
/// <summary>
/// URL of the Discord webhook which will relay watchlist connection notifications. If left empty, disables the webhook.
/// </summary>
+++ /dev/null
-using Robust.Shared.Configuration;
-
-namespace Content.Shared.CCVar;
-
-public sealed partial class CCVars
-{
- public static readonly CVarDef<string> StatusMoMMIUrl =
- CVarDef.Create("status.mommiurl", "", CVar.SERVERONLY);
-
- public static readonly CVarDef<string> StatusMoMMIPassword =
- CVarDef.Create("status.mommipassword", "", CVar.SERVERONLY | CVar.CONFIDENTIAL);
-
-}
AdminRelated = Admin | AdminAlert | AdminChat,
}
+
+ /// <summary>
+ /// Contains extension methods for <see cref="ChatChannel"/>
+ /// </summary>
+ public static class ChatChannelExt
+ {
+ /// <summary>
+ /// Gets a string representation of a chat channel.
+ /// </summary>
+ /// <exception cref="ArgumentOutOfRangeException">Thrown when this channel does not have a string representation set.</exception>
+ public static string GetString(this ChatChannel channel)
+ {
+ return channel switch
+ {
+ ChatChannel.OOC => Loc.GetString("chat-channel-humanized-ooc"),
+ ChatChannel.Admin => Loc.GetString("chat-channel-humanized-admin"),
+ _ => throw new ArgumentOutOfRangeException(nameof(channel), channel, null)
+ };
+ }
+ }
}
<PackageVersion Remove="Npgsql.EntityFrameworkCore.PostgreSQL" />
<PackageVersion Remove="Microsoft.EntityFrameworkCore.Design" />
<PackageVersion Include="CsvHelper" Version="33.0.1" />
+ <PackageVersion Include="Discord.Net" Version="3.16.0" />
<PackageVersion Include="ImGui.NET" Version="1.87.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.1">
<PrivateAssets>all</PrivateAssets>
<PackageVersion Include="Veldrid" Version="4.8.0" />
<PackageVersion Include="Veldrid.SPIRV" Version="1.0.15" />
</ItemGroup>
-</Project>
\ No newline at end of file
+</Project>
--- /dev/null
+chat-channel-humanized-ooc = OOC
+chat-channel-humanized-admin = ADMIN
chat-manager-send-admin-announcement-wrap-message = [bold]{$adminChannelName}: {$message}[/bold]
chat-manager-send-hook-ooc-wrap-message = OOC: [bold](D){$senderName}:[/bold] {$message}
+chat-manager-send-hook-admin-wrap-message = ADMIN: [bold](D){$senderName}:[/bold] {$message}
chat-manager-dead-channel-name = DEAD
chat-manager-admin-channel-name = ADMIN