| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- 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 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!;
- }
- /// <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 _sawmillNet = default!;
- private ulong _guildId;
- private string _botToken = string.Empty;
- private bool _registeredLinks = false;
- 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;
- }
- _client = new DiscordSocketClient(new DiscordSocketConfig()
- {
- GatewayIntents = GatewayIntents.All
- });
- _client.Log += Log;
- _client.MessageReceived += OnCommandReceivedInternal;
- _client.MessageReceived += OnMessageReceivedInternal;
- // 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.");
- _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(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");
- _sawmillNet = _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
- };
- _sawmillNet.Log(logLevel, FormatLog(msg));
- 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[1..],
- 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 was not found.");
- return;
- }
- await channel.SendMessageAsync(message, allowedMentions: AllowedMentions.None);
- }
- #endregion
- }
|