Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discord #143

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Content.Packaging/ServerPackaging.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public static class ServerPackaging
// Python script had Npgsql. though we want Npgsql.dll as well soooo
"Npgsql",
"Microsoft",
"Discord.Net", // WD-Edit
};

private static readonly List<string> ServerNotExtraAssemblies = new()
Expand Down
573 changes: 374 additions & 199 deletions Content.Server/Administration/Systems/BwoinkSystem.cs

Large diffs are not rendered by default.

37 changes: 36 additions & 1 deletion Content.Server/Chat/Managers/ChatManager.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Server.Administration.Systems;
using Content.Server.Discord;
using Content.Server.MoMMI;
using Content.Server.Preferences.Managers;
using Content.Shared._White;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Mind;
using Discord.WebSocket;
using Robust.Server.Player;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
Expand Down Expand Up @@ -45,6 +50,8 @@ internal sealed partial class ChatManager : IChatManager
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly DiscordLink _discordLink = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;

/// <summary>
/// The maximum length a player-sent message can be sent
Expand All @@ -53,6 +60,7 @@ internal sealed partial class ChatManager : IChatManager

private bool _oocEnabled = true;
private bool _adminOocEnabled = true;
private ulong _relayChannelId = 0; // WD-EDIT

private readonly Dictionary<NetUserId, ChatUser> _players = new();

Expand All @@ -63,10 +71,37 @@ public void Initialize()

_configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true);
_configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
_configurationManager.OnValueChanged(WhiteCVars.OocRelayChannelId, OnOocChannelChanged, true); // WD-EDIT
_discordLink.OnMessageReceived += MessageReceived; // WD-EDIT

_playerManager.PlayerStatusChanged += PlayerStatusChanged;
}

// WD-EDIT Start
private void MessageReceived(SocketMessage arg)
{
if (arg.Author.IsBot)
{
return;
}
if (arg.Channel.Id == _relayChannelId)
{
_taskManager.RunOnMainThread(() =>
{
SendHookOOC(arg.Author.Username, arg.Content);
});
}
}
private void OnOocChannelChanged(string id)
{
if (!ulong.TryParse(id, out var channelId))
{
return;
}
_relayChannelId = channelId;
}
// WD-EDIT End

private void OnOocEnabledChanged(bool val)
{
if (_oocEnabled == val) return;
Expand Down Expand Up @@ -251,7 +286,7 @@ private void SendOOC(ICommonSession player, string message)

//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);
_discordLink.SendMessage($"**OOC**: `{player.Name}`: {message}", _relayChannelId); // WD-Edit
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"OOC from {player:Player}: {message}");
}

Expand Down
3 changes: 3 additions & 0 deletions Content.Server/Entry/EntryPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Content.Server.Connection;
using Content.Server.JoinQueue;
using Content.Server.Database;
using Content.Server._White.Discord;
using Content.Server.DiscordAuth;
using Content.Server.EUI;
using Content.Server.GameTicking;
Expand Down Expand Up @@ -105,6 +106,7 @@ public override void Init()
IoCManager.Resolve<ContentNetworkResourceManager>().Initialize();
IoCManager.Resolve<GhostKickManager>().Initialize();
IoCManager.Resolve<ServerInfoManager>().Initialize();
IoCManager.Resolve<DiscordLink>().Initialize(); // WD-Edit
IoCManager.Resolve<JoinQueueManager>().Initialize();
IoCManager.Resolve<DiscordAuthManager>().Initialize();
IoCManager.Resolve<ServerApi>().Initialize();
Expand Down Expand Up @@ -174,6 +176,7 @@ protected override void Dispose(bool disposing)
{
_playTimeTracking?.Shutdown();
_dbManager?.Shutdown();
IoCManager.Resolve<DiscordLink>().Shutdown(); // WD-Edit
IoCManager.Resolve<ServerApi>().Shutdown();
}

Expand Down
1 change: 1 addition & 0 deletions Content.Server/IoC/ServerContentIoC.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public static void Register()
IoCManager.Register<ServerDbEntryManager>();
IoCManager.Register<JobWhitelistManager>();
IoCManager.Register<JoinQueueManager>();
IoCManager.Register<DiscordLink>(); // WD-Edit
IoCManager.Register<DiscordAuthManager>();
IoCManager.Register<ISharedPlaytimeManager, PlayTimeTrackingManager>();
IoCManager.Register<ServerApi>();
Expand Down
251 changes: 251 additions & 0 deletions Content.Server/_White/Discord/DiscordLink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
using System.Linq;
using System.Threading.Tasks;
using Content.Shared._White;
using Discord;
using Discord.WebSocket;
using Robust.Shared.Configuration;
using Robust.Shared.Utility;
using LogMessage = Discord.LogMessage;

namespace Content.Server._White.Discord;

/// <summary>
/// Represents the arguments for the <see cref="DiscordLink.OnCommandReceived"/> event.
/// </summary>
public record 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, split by spaces.
/// </summary>
public string[] Arguments { get; init; } = Array.Empty<string>();
/// <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!;
}

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 _sawmillNet = 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;

/// <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 Initialize()
{
_configuration.OnValueChanged(CCVars.DiscordGuildId, OnGuildIdChanged, true);
_configuration.OnValueChanged(CCVars.DiscordPrefix, OnPrefixChanged, true);
_client = new DiscordSocketClient(new DiscordSocketConfig()
{
GatewayIntents = GatewayIntents.All
});
_client.Log += Log;
_client.MessageReceived += OnCommandReceivedInternal;
_client.MessageReceived += OnMessageReceivedInternal;
if (_configuration.GetCVar(CCVars.DiscordToken) is not { } token || token == string.Empty)
{
_sawmill.Info("No Discord token specified, not connecting.");
// The Bot is not connected, so we need to set the client to null, because some methods check if the bot is connected using a null check on the client.
_client = null;
return;
}

// If the Guild ID is empty OR the prefix is empty, we don't want to connect to Discord.
if (_configuration.GetCVar(CCVars.DiscordGuildId) == string.Empty || _configuration.GetCVar(CCVars.DiscordPrefix) == 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.");
_client = null;
return;
}

_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(() =>
{
try
{
LoginAsync(token);
}
catch (Exception e)
{
_sawmill.Error("Failed to connect to Discord!", e);
}
});
}

public void Shutdown()
{
if (_client != null)
{
_sawmill.Info("Disconnecting from Discord.");
_client.Log -= Log;
_client.MessageReceived -= OnCommandReceivedInternal;
_client.MessageReceived -= OnMessageReceivedInternal;

_client.LogoutAsync();
_client.StopAsync();
_client.Dispose();
Comment on lines +127 to +129
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Исправить: добавить ожидание асинхронных методов для корректного завершения

В методе Shutdown, асинхронные методы _client.LogoutAsync() и _client.StopAsync() вызываются без await, что может привести к незавершённому отключению клиента и потенциальным проблемам при выключении сервера. Рекомендуется добавить await перед этими вызовами и сделать метод Shutdown асинхронным (async), чтобы обеспечить корректное завершение асинхронных операций.

_client = null;
}

_configuration.UnsubValueChanged(CCVars.DiscordGuildId, OnGuildIdChanged);
_configuration.UnsubValueChanged(CCVars.DiscordPrefix, OnPrefixChanged);
}

public void PostInject()
{
_sawmill = _logManager.GetSawmill("discord.link");
_sawmillNet = _logManager.GetSawmill("discord.link.log");
}

private Task OnMessageReceivedInternal(SocketMessage message)
{
OnMessageReceived?.Invoke(message);
return Task.CompletedTask;
}

private Task OnCommandReceivedInternal(SocketMessage message)
{
var content = message.Content;
// If the message is too short to be a command, or doesn't start with the bot prefix, ignore it.
if (content.Length <= BotPrefix.Length || !content.StartsWith(BotPrefix))
return Task.CompletedTask;

// Split the message into the command and the arguments.
var split = content[BotPrefix.Length..].Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (split.Length == 0)
return Task.CompletedTask; // No command.

// Raise the event!
OnCommandReceived?.Invoke(new CommandReceivedEventArgs
{
Command = split[0],
Arguments = split.Skip(1).ToArray(),
Message = message
});
return Task.CompletedTask;
}

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.Exception}\n{msg.Message}";
}

private Task Log(LogMessage msg)
{
var logLevel = msg.Severity switch
{
LogSeverity.Critical => LogLevel.Fatal,
LogSeverity.Error => LogLevel.Error,
LogSeverity.Warning => LogLevel.Warning,
_ => LogLevel.Debug
};

_sawmillNet.Log(logLevel, FormatLog(msg));
return Task.CompletedTask;
}

public void SendMessage(string message, ulong channel, bool isTTS = false, Embed? embed = null, AllowedMentions? allowedMentions = null)
{
// Default to none to avoid lazy programmers forgetting to set it. Wouldn't want to ping everyone by accident.
allowedMentions ??= AllowedMentions.None;

var guild = GetGuild();
if (guild is null)
{
return;
}

var textChannel = guild.GetTextChannel(channel);
if (textChannel is null)
{
_sawmill.Error("Tried to send a message to a channel that doesn't exist!");
return;
}
textChannel.SendMessageAsync(message, isTTS, embed, null, allowedMentions);
}
Comment on lines +228 to +229
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Исправить: добавить ожидание асинхронного метода SendMessageAsync

В методе SendMessage, вызов textChannel.SendMessageAsync(...) осуществляется без await, что может привести к необработанным исключениям и непредсказуемому поведению приложения. Рекомендуется добавить await перед вызовом и сделать метод SendMessage асинхронным (async), чтобы обеспечить корректное выполнение асинхронной операции.


public SocketGuild? GetGuild()
{
// While I don't expect this to ever be null when this gets called, it's better to be safe than sorry.
if (_client is null)
{
if (_botToken == string.Empty)
return null; // If the bot is turned off, don't send a warning. It's totally valid for servers to run without the Discord link set up.
_sawmill.Error("Tried to get a Discord guild but the client is null! Is the token not set?");
return null;
}

// Same as above, but for the guild ID.
if (_guildId == 0)
{
_sawmill.Error("Tried to get a Discord guild but the guild ID is not set! Blow up now!");
return null;
}

return _client.GetGuild(_guildId);
}
}
1 change: 1 addition & 0 deletions Content.Shared/Content.Shared.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Discord.Net" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
Expand Down
Loading
Loading