Implement DiscordProvider and other things

This commit is contained in:
Cyberboss
2018-07-18 11:57:35 -04:00
parent 64dbd55fa6
commit be64ee4867
11 changed files with 290 additions and 22 deletions
+1 -1
View File
@@ -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; }
}
}