Add basic discord client integration with ooc and admin chat support (#33840)
* Add basic discord client integration with ooc and admin chat support * Use username instead of global name WHY IS GLOBAL NAME NOT JUST THE USERNAME??? WHY ARE THERE NO DOC COMMENTS??? * stuff * Reviews, methinks. * Reviews * reviews
This commit is contained in:
@@ -4,7 +4,7 @@ using System.Runtime.InteropServices;
|
|||||||
using Content.Server.Administration.Logs;
|
using Content.Server.Administration.Logs;
|
||||||
using Content.Server.Administration.Managers;
|
using Content.Server.Administration.Managers;
|
||||||
using Content.Server.Administration.Systems;
|
using Content.Server.Administration.Systems;
|
||||||
using Content.Server.MoMMI;
|
using Content.Server.Discord.DiscordLink;
|
||||||
using Content.Server.Players.RateLimiting;
|
using Content.Server.Players.RateLimiting;
|
||||||
using Content.Server.Preferences.Managers;
|
using Content.Server.Preferences.Managers;
|
||||||
using Content.Shared.Administration;
|
using Content.Shared.Administration;
|
||||||
@@ -36,7 +36,6 @@ internal sealed partial class ChatManager : IChatManager
|
|||||||
|
|
||||||
[Dependency] private readonly IReplayRecordingManager _replay = default!;
|
[Dependency] private readonly IReplayRecordingManager _replay = default!;
|
||||||
[Dependency] private readonly IServerNetManager _netManager = default!;
|
[Dependency] private readonly IServerNetManager _netManager = default!;
|
||||||
[Dependency] private readonly IMoMMILink _mommiLink = default!;
|
|
||||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||||
[Dependency] private readonly IServerPreferencesManager _preferencesManager = default!;
|
[Dependency] private readonly IServerPreferencesManager _preferencesManager = default!;
|
||||||
@@ -45,6 +44,7 @@ internal sealed partial class ChatManager : IChatManager
|
|||||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||||
[Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!;
|
[Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!;
|
||||||
[Dependency] private readonly ISharedPlayerManager _player = default!;
|
[Dependency] private readonly ISharedPlayerManager _player = default!;
|
||||||
|
[Dependency] private readonly DiscordChatLink _discordLink = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The maximum length a player-sent message can be sent
|
/// The maximum length a player-sent message can be sent
|
||||||
@@ -199,6 +199,13 @@ internal sealed partial class ChatManager : IChatManager
|
|||||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Hook OOC from {sender}: {message}");
|
_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
|
#endregion
|
||||||
|
|
||||||
#region Public OOC Chat API
|
#region Public OOC Chat API
|
||||||
@@ -264,7 +271,7 @@ internal sealed partial class ChatManager : IChatManager
|
|||||||
|
|
||||||
//TODO: player.Name color, this will need to change the structure of the MsgChatMessage
|
//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);
|
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}");
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"OOC from {player:Player}: {message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,6 +302,7 @@ internal sealed partial class ChatManager : IChatManager
|
|||||||
author: player.UserId);
|
author: player.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_discordLink.SendMessage(message, player.Name, ChatChannel.AdminChat);
|
||||||
_adminLogger.Add(LogType.Chat, $"Admin chat from {player:Player}: {message}");
|
_adminLogger.Add(LogType.Chat, $"Admin chat from {player:Player}: {message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace Content.Server.Chat.Managers
|
|||||||
void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type);
|
void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type);
|
||||||
|
|
||||||
void SendHookOOC(string sender, string message);
|
void SendHookOOC(string sender, string message);
|
||||||
|
void SendHookAdmin(string sender, string message);
|
||||||
void SendAdminAnnouncement(string message, AdminFlags? flagBlacklist = null, AdminFlags? flagWhitelist = null);
|
void SendAdminAnnouncement(string message, AdminFlags? flagBlacklist = null, AdminFlags? flagWhitelist = null);
|
||||||
void SendAdminAnnouncementMessage(ICommonSession player, string message, bool suppressLog = true);
|
void SendAdminAnnouncementMessage(ICommonSession player, string message, bool suppressLog = true);
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Discord.Net" />
|
||||||
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
|
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
96
Content.Server/Discord/DiscordLink/DiscordChatLink.cs
Normal file
96
Content.Server/Discord/DiscordLink/DiscordChatLink.cs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
267
Content.Server/Discord/DiscordLink/DiscordLink.cs
Normal file
267
Content.Server/Discord/DiscordLink/DiscordLink.cs
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ using Content.Server.Afk;
|
|||||||
using Content.Server.Chat.Managers;
|
using Content.Server.Chat.Managers;
|
||||||
using Content.Server.Connection;
|
using Content.Server.Connection;
|
||||||
using Content.Server.Database;
|
using Content.Server.Database;
|
||||||
|
using Content.Server.Discord.DiscordLink;
|
||||||
using Content.Server.EUI;
|
using Content.Server.EUI;
|
||||||
using Content.Server.GameTicking;
|
using Content.Server.GameTicking;
|
||||||
using Content.Server.GhostKick;
|
using Content.Server.GhostKick;
|
||||||
@@ -146,6 +147,10 @@ namespace Content.Server.Entry
|
|||||||
IoCManager.Resolve<IAdminManager>().Initialize();
|
IoCManager.Resolve<IAdminManager>().Initialize();
|
||||||
IoCManager.Resolve<IAfkManager>().Initialize();
|
IoCManager.Resolve<IAfkManager>().Initialize();
|
||||||
IoCManager.Resolve<RulesManager>().Initialize();
|
IoCManager.Resolve<RulesManager>().Initialize();
|
||||||
|
|
||||||
|
IoCManager.Resolve<DiscordLink>().Initialize();
|
||||||
|
IoCManager.Resolve<DiscordChatLink>().Initialize();
|
||||||
|
|
||||||
_euiManager.Initialize();
|
_euiManager.Initialize();
|
||||||
|
|
||||||
IoCManager.Resolve<IGameMapManager>().Initialize();
|
IoCManager.Resolve<IGameMapManager>().Initialize();
|
||||||
@@ -184,6 +189,9 @@ namespace Content.Server.Entry
|
|||||||
_playTimeTracking?.Shutdown();
|
_playTimeTracking?.Shutdown();
|
||||||
_dbManager?.Shutdown();
|
_dbManager?.Shutdown();
|
||||||
IoCManager.Resolve<ServerApi>().Shutdown();
|
IoCManager.Resolve<ServerApi>().Shutdown();
|
||||||
|
|
||||||
|
IoCManager.Resolve<DiscordLink>().Shutdown();
|
||||||
|
IoCManager.Resolve<DiscordChatLink>().Shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void LoadConfigPresets(IConfigurationManager cfg, IResourceManager res, ISawmill sawmill)
|
private static void LoadConfigPresets(IConfigurationManager cfg, IResourceManager res, ISawmill sawmill)
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ using Content.Server.Chat.Managers;
|
|||||||
using Content.Server.Connection;
|
using Content.Server.Connection;
|
||||||
using Content.Server.Database;
|
using Content.Server.Database;
|
||||||
using Content.Server.Discord;
|
using Content.Server.Discord;
|
||||||
|
using Content.Server.Discord.DiscordLink;
|
||||||
using Content.Server.Discord.WebhookMessages;
|
using Content.Server.Discord.WebhookMessages;
|
||||||
using Content.Server.EUI;
|
using Content.Server.EUI;
|
||||||
using Content.Server.GhostKick;
|
using Content.Server.GhostKick;
|
||||||
using Content.Server.Info;
|
using Content.Server.Info;
|
||||||
using Content.Server.Mapping;
|
using Content.Server.Mapping;
|
||||||
using Content.Server.Maps;
|
using Content.Server.Maps;
|
||||||
using Content.Server.MoMMI;
|
|
||||||
using Content.Server.NodeContainer.NodeGroups;
|
using Content.Server.NodeContainer.NodeGroups;
|
||||||
using Content.Server.Players.JobWhitelist;
|
using Content.Server.Players.JobWhitelist;
|
||||||
using Content.Server.Players.PlayTimeTracking;
|
using Content.Server.Players.PlayTimeTracking;
|
||||||
@@ -39,7 +39,6 @@ namespace Content.Server.IoC
|
|||||||
IoCManager.Register<IChatManager, ChatManager>();
|
IoCManager.Register<IChatManager, ChatManager>();
|
||||||
IoCManager.Register<ISharedChatManager, ChatManager>();
|
IoCManager.Register<ISharedChatManager, ChatManager>();
|
||||||
IoCManager.Register<IChatSanitizationManager, ChatSanitizationManager>();
|
IoCManager.Register<IChatSanitizationManager, ChatSanitizationManager>();
|
||||||
IoCManager.Register<IMoMMILink, MoMMILink>();
|
|
||||||
IoCManager.Register<IServerPreferencesManager, ServerPreferencesManager>();
|
IoCManager.Register<IServerPreferencesManager, ServerPreferencesManager>();
|
||||||
IoCManager.Register<IServerDbManager, ServerDbManager>();
|
IoCManager.Register<IServerDbManager, ServerDbManager>();
|
||||||
IoCManager.Register<RecipeManager, RecipeManager>();
|
IoCManager.Register<RecipeManager, RecipeManager>();
|
||||||
@@ -77,6 +76,9 @@ namespace Content.Server.IoC
|
|||||||
IoCManager.Register<ConnectionManager>();
|
IoCManager.Register<ConnectionManager>();
|
||||||
IoCManager.Register<MultiServerKickManager>();
|
IoCManager.Register<MultiServerKickManager>();
|
||||||
IoCManager.Register<CVarControlManager>();
|
IoCManager.Register<CVarControlManager>();
|
||||||
|
|
||||||
|
IoCManager.Register<DiscordLink>();
|
||||||
|
IoCManager.Register<DiscordChatLink>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Content.Server.MoMMI
|
|
||||||
{
|
|
||||||
public interface IMoMMILink
|
|
||||||
{
|
|
||||||
void SendOOCMessage(string sender, string message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
12
Content.Shared/CCVar/CCVars.Chat.Admin.cs
Normal file
12
Content.Shared/CCVar/CCVars.Chat.Admin.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@@ -24,4 +24,10 @@ public sealed partial class CCVars
|
|||||||
|
|
||||||
public static readonly CVarDef<bool> ShowOocPatronColor =
|
public static readonly CVarDef<bool> ShowOocPatronColor =
|
||||||
CVarDef.Create("ooc.show_ooc_patron_color", true, CVar.ARCHIVE | CVar.REPLICATED | CVar.CLIENT);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Robust.Shared.Configuration;
|
using Robust.Shared.Configuration;
|
||||||
using Robust.Shared.Maths;
|
|
||||||
|
|
||||||
namespace Content.Shared.CCVar;
|
namespace Content.Shared.CCVar;
|
||||||
|
|
||||||
@@ -60,6 +59,28 @@ public sealed partial class CCVars
|
|||||||
public static readonly CVarDef<string> DiscordRoundEndRoleWebhook =
|
public static readonly CVarDef<string> DiscordRoundEndRoleWebhook =
|
||||||
CVarDef.Create("discord.round_end_role", string.Empty, CVar.SERVERONLY);
|
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>
|
/// <summary>
|
||||||
/// URL of the Discord webhook which will relay watchlist connection notifications. If left empty, disables the webhook.
|
/// URL of the Discord webhook which will relay watchlist connection notifications. If left empty, disables the webhook.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
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);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -92,4 +92,24 @@ namespace Content.Shared.Chat
|
|||||||
|
|
||||||
AdminRelated = Admin | AdminAlert | AdminChat,
|
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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<PackageVersion Remove="Npgsql.EntityFrameworkCore.PostgreSQL" />
|
<PackageVersion Remove="Npgsql.EntityFrameworkCore.PostgreSQL" />
|
||||||
<PackageVersion Remove="Microsoft.EntityFrameworkCore.Design" />
|
<PackageVersion Remove="Microsoft.EntityFrameworkCore.Design" />
|
||||||
<PackageVersion Include="CsvHelper" Version="33.0.1" />
|
<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="ImGui.NET" Version="1.87.3" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.1">
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|||||||
2
Resources/Locale/en-US/chat/chat-channel.ftl
Normal file
2
Resources/Locale/en-US/chat/chat-channel.ftl
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
chat-channel-humanized-ooc = OOC
|
||||||
|
chat-channel-humanized-admin = ADMIN
|
||||||
@@ -44,6 +44,7 @@ chat-manager-send-admin-chat-wrap-message = {$adminChannelName}: [bold]{$playerN
|
|||||||
chat-manager-send-admin-announcement-wrap-message = [bold]{$adminChannelName}: {$message}[/bold]
|
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-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-dead-channel-name = DEAD
|
||||||
chat-manager-admin-channel-name = ADMIN
|
chat-manager-admin-channel-name = ADMIN
|
||||||
|
|||||||
Reference in New Issue
Block a user