mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-27 07:04:57 +01:00
Implement DiscordProvider and other things
This commit is contained in:
+1
-1
@@ -103,7 +103,7 @@
|
||||
|
||||
//represents a chat user
|
||||
/datum/tgs_chat_user
|
||||
var/id //Internal user representation
|
||||
var/id //Internal user representation, requires channel to be unique
|
||||
var/friendly_name //The user's public name
|
||||
var/mention //The text to use to ping this user in a message
|
||||
var/datum/tgs_chat_channel/channel //The /datum/tgs_chat_channel this user was from
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
/// <summary>
|
||||
/// The Discord channel ID
|
||||
/// </summary>
|
||||
public long? DiscordChannelId { get; set; }
|
||||
public ulong? DiscordChannelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="ChatChannel"/> is an admin channel
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
/// The <see cref="Providers.IProvider"/> channel Id.
|
||||
/// </summary>
|
||||
/// <remarks><see cref="Chat"/> remaps this to an internal id using <see cref="ChannelMapping"/></remarks>
|
||||
public long Id { get; set; }
|
||||
public ulong Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user friendly name of the <see cref="Channel"/>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
sealed class ChannelMapping
|
||||
{
|
||||
public long ProviderId { get; set; }
|
||||
public long ProviderChannelId { get; set; }
|
||||
public ulong ProviderChannelId { get; set; }
|
||||
public bool IsWatchdogChannel { get; set; }
|
||||
|
||||
public Channel Channel { get; set; }
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// <inheritdoc />
|
||||
sealed class Chat : IChat
|
||||
{
|
||||
const string CommonMention = "!tgs";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IProviderFactory"/> for the <see cref="Chat"/>
|
||||
/// </summary>
|
||||
@@ -36,7 +38,7 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// <summary>
|
||||
/// Map of <see cref="Channel.Id"/>s to <see cref="ChannelMapping"/>s
|
||||
/// </summary>
|
||||
readonly Dictionary<long, ChannelMapping> mappedChannels;
|
||||
readonly Dictionary<ulong, ChannelMapping> mappedChannels;
|
||||
|
||||
/// <summary>
|
||||
/// The active <see cref="IJsonTrackingContext"/>s for the <see cref="Chat"/>
|
||||
@@ -51,7 +53,7 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// <summary>
|
||||
/// Used for remapping <see cref="Channel.Id"/>s
|
||||
/// </summary>
|
||||
long channelIdCounter;
|
||||
ulong channelIdCounter;
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="StartAsync(CancellationToken)"/> has been called
|
||||
@@ -71,7 +73,7 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
builtinCommands = commandFactory?.GenerateCommands() ?? throw new ArgumentNullException(nameof(commandFactory));
|
||||
|
||||
providers = new Dictionary<long, IProvider>();
|
||||
mappedChannels = new Dictionary<long, ChannelMapping>();
|
||||
mappedChannels = new Dictionary<ulong, ChannelMapping>();
|
||||
trackingContexts = new List<IJsonTrackingContext>();
|
||||
channelIdCounter = 1;
|
||||
}
|
||||
@@ -131,11 +133,11 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
Channel = y
|
||||
});
|
||||
|
||||
long baseId;
|
||||
ulong baseId;
|
||||
lock (this)
|
||||
{
|
||||
baseId = channelIdCounter;
|
||||
channelIdCounter += results.Count;
|
||||
channelIdCounter += (ulong)results.Count;
|
||||
}
|
||||
|
||||
Task task;
|
||||
@@ -185,7 +187,7 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendMessage(string message, IEnumerable<long> channelIds, CancellationToken cancellationToken)
|
||||
public Task SendMessage(string message, IEnumerable<ulong> channelIds, CancellationToken cancellationToken)
|
||||
{
|
||||
if (message == null)
|
||||
throw new ArgumentNullException(nameof(message));
|
||||
@@ -209,7 +211,7 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// <inheritdoc />
|
||||
public Task SendWatchdogMessage(string message, CancellationToken cancellationToken)
|
||||
{
|
||||
List<long> wdChannels;
|
||||
List<ulong> wdChannels;
|
||||
lock (mappedChannels) //so it doesn't change while we're using it
|
||||
wdChannels = mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList();
|
||||
return SendMessage(message, wdChannels, cancellationToken);
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// <param name="channelIds">The <see cref="Models.ChatChannel.Id"/>s of the <see cref="Host.Models.ChatChannel"/>s to send to</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task SendMessage(string message, IEnumerable<long> channelIds, CancellationToken cancellationToken);
|
||||
Task SendMessage(string message, IEnumerable<ulong> channelIds, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Send a chat <paramref name="message"/> to configured watchdog channels
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{
|
||||
sealed class Message
|
||||
{
|
||||
string Content { get; set; }
|
||||
User User { get; set; }
|
||||
public string Content { get; set; }
|
||||
public User User { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
using Discord;
|
||||
using Discord.Net;
|
||||
using Discord.WebSocket;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IProvider"/> for the Discord app
|
||||
/// </summary>
|
||||
sealed class DiscordProvider : IProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool Connected { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string BotMention
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Connected)
|
||||
throw new InvalidOperationException("Provider not connected");
|
||||
return client.CurrentUser.Mention;
|
||||
}
|
||||
}
|
||||
|
||||
readonly ILogger<DiscordProvider> logger;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DiscordSocketClient"/> for the <see cref="DiscordProvider"/>
|
||||
/// </summary>
|
||||
readonly DiscordSocketClient client;
|
||||
|
||||
/// <summary>
|
||||
/// The name used for populating <see cref="Channel.ConnectionName"/>
|
||||
/// </summary>
|
||||
readonly string connectionName;
|
||||
|
||||
/// <summary>
|
||||
/// The token used for connecting to discord
|
||||
/// </summary>
|
||||
readonly string botToken;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Queue{T}"/> of received <see cref="Message"/>s
|
||||
/// </summary>
|
||||
readonly Queue<Message> messageQueue;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="List{T}"/> of mapped <see cref="ITextChannel"/> <see cref="IEntity{TId}.Id"/>s
|
||||
/// </summary>
|
||||
readonly List<ulong> mappedChannels;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="TaskCompletionSource{TResult}"/> that completes while <see cref="messageQueue"/> isn't empty
|
||||
/// </summary>
|
||||
TaskCompletionSource<object> nextMessage;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="DiscordProvider"/>
|
||||
/// </summary>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
/// <param name="connectionName">The value of <see cref="connectionName"/></param>
|
||||
/// <param name="botToken">The value of <see cref="botToken"/></param>
|
||||
public DiscordProvider(ILogger<DiscordProvider> logger, string connectionName, string botToken)
|
||||
{
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
this.connectionName = connectionName ?? throw new ArgumentNullException(nameof(connectionName));
|
||||
this.botToken = botToken ?? throw new ArgumentNullException(nameof(botToken));
|
||||
client = new DiscordSocketClient();
|
||||
client.MessageReceived += Client_MessageReceived;
|
||||
nextMessage = new TaskCompletionSource<object>();
|
||||
mappedChannels = new List<ulong>();
|
||||
messageQueue = new Queue<Message>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => client.Dispose();
|
||||
|
||||
/// <summary>
|
||||
/// Handle a message recieved from Discord
|
||||
/// </summary>
|
||||
/// <param name="e">The <see cref="SocketMessage"/></param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task Client_MessageReceived(SocketMessage e)
|
||||
{
|
||||
if (e.Author.Id != client.CurrentUser.Id)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var pm = e.Channel is IPrivateChannel;
|
||||
|
||||
if (!pm && !mappedChannels.Contains(e.Channel.Id))
|
||||
return Task.CompletedTask;
|
||||
|
||||
var result = new Message {
|
||||
Content = e.Content,
|
||||
User = new User
|
||||
{
|
||||
Id = e.Author.Id,
|
||||
Channel = new Channel
|
||||
{
|
||||
Id = e.Channel.Id,
|
||||
IsAdmin = false,
|
||||
IsPrivate = true,
|
||||
ConnectionName = connectionName,
|
||||
FriendlyName = e.Channel.Name
|
||||
},
|
||||
FriendlyName = e.Author.Username,
|
||||
Mention = e.Author.Mention
|
||||
}
|
||||
};
|
||||
|
||||
lock (this)
|
||||
{
|
||||
messageQueue.Enqueue(result);
|
||||
nextMessage.TrySetResult(null);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Message> NextMessage(CancellationToken cancellationToken)
|
||||
{
|
||||
var cancelTcs = new TaskCompletionSource<object>();
|
||||
using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
|
||||
await Task.WhenAny(nextMessage.Task, cancelTcs.Task).ConfigureAwait(false);
|
||||
lock (this)
|
||||
{
|
||||
var result = messageQueue.Dequeue();
|
||||
if (messageQueue.Count == 0)
|
||||
nextMessage = new TaskCompletionSource<object>();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> Connect(CancellationToken cancellationToken)
|
||||
{
|
||||
if (Connected)
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
await client.LoginAsync(TokenType.Bot, botToken, true).ConfigureAwait(false);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await client.StartAsync().ConfigureAwait(false);
|
||||
|
||||
var channelsAvailable = new TaskCompletionSource<object>();
|
||||
client.Ready += () =>
|
||||
{
|
||||
channelsAvailable.SetResult(null);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
using (cancellationToken.Register(() => channelsAvailable.SetCanceled()))
|
||||
await channelsAvailable.Task.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning("Error connecting to Discord: {0}", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
Connected = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task Disconnect(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Connected)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
await client.StopAsync().ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await client.LogoutAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning("Error disconnecting from discord: {0}", e);
|
||||
}
|
||||
Connected = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken)
|
||||
{
|
||||
if (channels == null)
|
||||
throw new ArgumentNullException(nameof(channels));
|
||||
|
||||
if (!Connected)
|
||||
throw new InvalidOperationException("Provider not connected!");
|
||||
|
||||
Channel GetChannelForChatChannel(ChatChannel channel)
|
||||
{
|
||||
if (!channel.DiscordChannelId.HasValue)
|
||||
throw new InvalidOperationException("ChatChannel missing DiscordChannelId!");
|
||||
|
||||
var discordChannel = client.GetChannel(channel.DiscordChannelId.Value);
|
||||
|
||||
if (discordChannel == null)
|
||||
return null;
|
||||
|
||||
return new Channel
|
||||
{
|
||||
Id = discordChannel.Id,
|
||||
IsAdmin = channel.IsAdminChannel,
|
||||
ConnectionName = connectionName,
|
||||
FriendlyName = (discordChannel as ITextChannel)?.Name ?? "UNKNOWN",
|
||||
IsPrivate = false
|
||||
};
|
||||
};
|
||||
|
||||
var enumerator = channels.Select(x => GetChannelForChatChannel(x)).Where(x => x != null);
|
||||
|
||||
lock (this)
|
||||
{
|
||||
mappedChannels.Clear();
|
||||
mappedChannels.AddRange(enumerator.Select(x => x.Id));
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyList<Channel>>(enumerator.ToList());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken) {
|
||||
try
|
||||
{
|
||||
await ((client.GetChannel(channelId) as ITextChannel)?.SendMessageAsync(message, false, null, new RequestOptions
|
||||
{
|
||||
CancelToken = cancellationToken
|
||||
}) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning("Error sending discord message: {0}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,10 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// <summary>
|
||||
/// Get a <see cref="Task{TResult}"/> resulting in the next <see cref="Message"/> the <see cref="IProvider"/> recieves or <see langword="null"/> on a disconnect
|
||||
/// </summary>
|
||||
Task<Message> NextMessage { get; }
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the next available <see cref="Message"/></returns>
|
||||
/// <remarks>Note that private messages will come in the form of <see cref="Channel"/>s not returned in <see cref="MapChannels(IEnumerable{Api.Models.ChatChannel}, CancellationToken)"/></remarks>
|
||||
Task<Message> NextMessage(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to connect the <see cref="IProvider"/>
|
||||
@@ -54,6 +57,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// <param name="message">The message contents</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task SendMessage(long channelId, string message, CancellationToken cancellationToken);
|
||||
Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,6 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// <summary>
|
||||
/// The list of internal channel ids to send <see cref="Message"/> to
|
||||
/// </summary>
|
||||
public List<long> ChannelIds { get; set; }
|
||||
public List<ulong> ChannelIds { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// Represents a tgs_chat_user datum
|
||||
/// </summary>
|
||||
public sealed class User
|
||||
{
|
||||
long Id { get; set; }
|
||||
string FriendlyName { get; set; }
|
||||
string Mention { get; set; }
|
||||
Channel Channel { get; set; }
|
||||
/// <summary>
|
||||
/// The internal user id
|
||||
/// </summary>
|
||||
public ulong Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The friendly name of the user
|
||||
/// </summary>
|
||||
public string FriendlyName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The text to mention the user
|
||||
/// </summary>
|
||||
public string Mention { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Components.Chat.Channel"/> the user spoke from
|
||||
/// </summary>
|
||||
public Channel Channel { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user