Work on implementing chat

This commit is contained in:
Cyberboss
2018-07-13 15:58:14 -04:00
parent 805d63c6d4
commit d7b8689576
20 changed files with 355 additions and 97 deletions
@@ -19,5 +19,10 @@
/// If the <see cref="ChatChannel"/> is an admin channel
/// </summary>
public bool IsAdminChannel { get; set; }
/// <summary>
/// If the <see cref="ChatChannel"/> is a watchdog channel
/// </summary>
public bool IsWatchdogChannel { get; set; }
}
}
@@ -0,0 +1,17 @@
namespace Tgstation.Server.Api.Models
{
/// <summary>
/// Represents a chat service provider
/// </summary>
public enum ChatProvider
{
/// <summary>
/// Internet relay chat
/// </summary>
Irc,
/// <summary>
/// Superior chat service
/// </summary>
Discord
}
}
@@ -6,22 +6,10 @@ namespace Tgstation.Server.Api.Models
/// <inheritdoc />
public sealed class ChatSettings : Internal.ChatSettings
{
/// <summary>
/// If the IRC connection is established
/// </summary>
[Permissions(DenyWrite = true)]
bool IrcConnected { get; set; }
/// <summary>
/// If the Discord connection is established
/// </summary>
[Permissions(DenyWrite = true)]
bool DiscordConnected { get; set; }
/// <summary>
/// Channels the Discord bot should listen/announce in
/// </summary>
[Permissions(WriteRight = ChatSettingsRights.SetChannels)]
[Permissions(WriteRight = ChatSettingsRights.WriteChannels)]
public List<ChatChannel> Channels { get; set; }
}
}
@@ -1,6 +1,4 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models.Internal
@@ -8,44 +6,39 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// Manage the server chat bots
/// </summary>
[Model(RightsType.ChatSettings, RequiresInstance = true)]
[Model(RightsType.ChatSettings, RequiresInstance = true, CanCrud = true, ReadRight = ChatSettingsRights.Read)]
public class ChatSettings
{
/// <summary>
/// If the IRC client is enabled
/// The settings id
/// </summary>
[Permissions(WriteRight = ChatSettingsRights.SetIrcEnabled)]
public bool IrcEnabled { get; set; }
[Permissions(DenyWrite = true)]
public long Id { get; set; }
/// <summary>
/// The IRC server name
/// The name of the connection
/// </summary>
[Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)]
[Permissions(WriteRight = ChatSettingsRights.WriteName)]
[Required]
public string IrcHost { get; set; }
public string Name { get; set; }
/// <summary>
/// The IRC server port
/// If the connection is enabled
/// </summary>
[Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)]
public ushort IrcPort { get; set; }
[Permissions(WriteRight = ChatSettingsRights.WriteEnabled)]
public bool Enabled { get; set; }
/// <summary>
/// The IRC server NickServ password
/// The <see cref="ChatProvider"/> used for the connection
/// </summary>
[Permissions(ReadRight = ChatSettingsRights.SetIrcSettings, WriteRight = ChatSettingsRights.SetIrcSettings)]
public string IrcNickServPassword { get; set; }
[Permissions(WriteRight = ChatSettingsRights.WriteProvider)]
public ChatProvider Provider { get; set; }
/// <summary>
/// If the Discord bot is enabled
/// The information used to connect to the <see cref="Provider"/>
/// </summary>
[Permissions(WriteRight = ChatSettingsRights.SetDiscordEnabled)]
public bool DiscordEnabled { get; set; }
/// <summary>
/// The Discord bot token
/// </summary>
[Permissions(ReadRight = ChatSettingsRights.SetDiscordSettings, WriteRight = ChatSettingsRights.SetDiscordSettings)]
public string DiscordBotToken { get; set; }
[Permissions(ReadRight = ChatSettingsRights.ReadConnectionString, WriteRight = ChatSettingsRights.ReadConnectionString)]
[Required]
public string ConnectionString { get; set; }
}
}
@@ -13,24 +13,32 @@ namespace Tgstation.Server.Api.Rights
/// </summary>
None = 0,
/// <summary>
/// User can enable/disable the IRC client
/// User can change <see cref="Models.Internal.ChatSettings.Enabled"/>
/// </summary>
SetIrcEnabled = 1,
WriteEnabled = 1,
/// <summary>
/// User can change the IRC settings
/// User can change <see cref="Models.Internal.ChatSettings.Provider"/>
/// </summary>
SetIrcSettings = 2,
WriteProvider = 2,
/// <summary>
/// User can change the chat channels
/// User can change <see cref="Models.ChatSettings.Channels"/>
/// </summary>
SetChannels = 4,
WriteChannels = 4,
/// <summary>
/// User can enable/disable the Discord bot
/// User can change <see cref="Models.Internal.ChatSettings.ConnectionString"/>
/// </summary>
SetDiscordEnabled = 8,
WriteConnectionString = 8,
/// <summary>
/// User can change the Discord settings
/// User can read <see cref="Models.Internal.ChatSettings.ConnectionString"/>
/// </summary>
SetDiscordSettings = 16,
ReadConnectionString = 16,
/// <summary>
/// User can read all chat settings except <see cref="Models.Internal.ChatSettings.ConnectionString"/>
/// </summary>
Read = 32,
/// <summary>
/// User can change <see cref="Models.Internal.ChatSettings.Name"/>
/// </summary>
WriteName = 32
}
}
@@ -0,0 +1,14 @@
namespace Tgstation.Server.Host.Components.Chat
{
sealed class Channel
{
public long Id { get; set; }
public string FriendlyName { get; set; }
public string ConnectionName { get; set; }
public bool IsAdminChannel { get; set; }
public bool IsPrivateChannel { get; set; }
}
}
@@ -0,0 +1,9 @@
namespace Tgstation.Server.Host.Components.Chat
{
sealed class ChannelMapping
{
public long ProviderId { get; set; }
public long ProviderChannelId { get; set; }
public bool IsWatchdogChannel { get; set; }
}
}
+139 -23
View File
@@ -1,59 +1,175 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Chat.Providers;
using Tgstation.Server.Host.Core;
namespace Tgstation.Server.Host.Components.Chat
{
/// <inheritdoc />
sealed class Chat : IChat
{
/// <inheritdoc />
public bool IrcConnected => throw new System.NotImplementedException();
/// <summary>
/// The <see cref="IProviderFactory"/> for the <see cref="Chat"/>
/// </summary>
readonly IProviderFactory providerFactory;
/// <inheritdoc />
public bool DiscordConnected => throw new System.NotImplementedException();
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="Chat"/>
/// </summary>
readonly IIOManager ioManager;
/// <inheritdoc />
public Task ChangeChannels(IEnumerable<ChatChannel> newChannels, CancellationToken cancellationToken)
/// <summary>
/// Map of <see cref="IProvider"/>s in use, keyed by <see cref="ChatSettings.Id"/>
/// </summary>
readonly Dictionary<long, IProvider> providers;
/// <summary>
/// Map of <see cref="Channel.Id"/>s to <see cref="ChannelMapping"/>s
/// </summary>
readonly Dictionary<long, ChannelMapping> mappedChannels;
/// <summary>
/// Used for remapping <see cref="Channel.Id"/>s
/// </summary>
long channelIdCounter;
/// <summary>
/// Construct a <see cref="Chat"/>
/// </summary>
/// <param name="providerFactory">The value of <see cref="providerFactory"/></param>
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
public Chat(IProviderFactory providerFactory, IIOManager ioManager)
{
throw new System.NotImplementedException();
this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
providers = new Dictionary<long, IProvider>();
mappedChannels = new Dictionary<long, ChannelMapping>();
channelIdCounter = 1;
}
/// <inheritdoc />
public Task ChangeSettings(Api.Models.Internal.ChatSettings newSettings, CancellationToken cancellationToken)
public void Dispose()
{
throw new System.NotImplementedException();
foreach (var I in providers)
I.Value.Dispose();
}
/// <inheritdoc />
public async Task ChangeChannels(long connectionId, IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken)
{
if (newChannels == null)
throw new ArgumentNullException(nameof(newChannels));
IProvider provider;
lock (providers)
if (!providers.TryGetValue(connectionId, out provider))
return;
var results = await provider.MapChannels(newChannels, cancellationToken).ConfigureAwait(false);
if (results == null) //aborted
return;
var mappings = Enumerable.Zip(newChannels, results, (x, y) => new ChannelMapping
{
IsWatchdogChannel = x.IsWatchdogChannel,
ProviderChannelId = y.Id,
ProviderId = connectionId
});
long baseId;
lock (this)
{
baseId = channelIdCounter;
channelIdCounter += results.Count;
}
lock (mappedChannels)
{
lock (providers)
if (!providers.TryGetValue(connectionId, out IProvider verify) || verify != provider) //aborted again
return;
foreach (var I in mappings)
mappedChannels.Add(baseId++, I);
}
}
/// <inheritdoc />
public async Task ChangeSettings(ChatSettings newSettings, CancellationToken cancellationToken)
{
if (newSettings == null)
throw new ArgumentNullException(nameof(newSettings));
IProvider provider;
lock (providers)
{
//raw settings changes forces a rebuild of the provider
if (providers.TryGetValue(newSettings.Id, out provider))
{
providers.Remove(newSettings.Id);
provider.Dispose();
}
if (newSettings.Enabled)
{
provider = providerFactory.CreateProvider(newSettings);
providers.Add(newSettings.Id, provider);
}
}
lock (mappedChannels)
foreach (var channelId in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key))
mappedChannels.Remove(channelId);
if (newSettings.Enabled)
await provider.Connect(cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public Task SendMessage(string message, IEnumerable<long> channelIds, CancellationToken cancellationToken)
{
throw new System.NotImplementedException();
if (message == null)
throw new ArgumentNullException(nameof(message));
if (channelIds == null)
throw new ArgumentNullException(nameof(channelIds));
return Task.WhenAll(channelIds.Select(x =>
{
ChannelMapping channelMapping;
lock(mappedChannels)
if (!mappedChannels.TryGetValue(x, out channelMapping))
return Task.CompletedTask;
IProvider provider;
lock (providers)
if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
return Task.CompletedTask;
return provider.SendMessage(channelMapping.ProviderChannelId, message, cancellationToken);
}));
}
/// <inheritdoc />
public Task SendWatchdogMessage(string message, CancellationToken cancellationToken)
{
throw new System.NotImplementedException();
List<long> 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);
}
/// <inheritdoc />
public Task StartAsync(CancellationToken cancellationToken)
public Task StartAsync(CancellationToken cancellationToken) => Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken)));
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
/// <inheritdoc />
public Task<IJsonTrackingContext> TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken)
{
throw new System.NotImplementedException();
ioManager.ResolvePath(".");
throw new NotImplementedException();
}
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken)
public bool Connected(long connectionId)
{
throw new System.NotImplementedException();
}
/// <inheritdoc />
public Task<IChatJsonTrackingContext> TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken)
{
throw new System.NotImplementedException();
lock (providers)
return providers.TryGetValue(connectionId, out var provider) && provider.Connected;
}
}
}
@@ -10,33 +10,31 @@ namespace Tgstation.Server.Host.Components.Chat
/// <summary>
/// For managing connected chat services
/// </summary>
public interface IChat : IHostedService
public interface IChat : IHostedService, IDisposable
{
/// <summary>
/// If the IRC client is connected
/// If a given set of <see cref="ChatSettings"/> is connected
/// </summary>
bool IrcConnected { get; }
/// <param name="connectionId">The <see cref="ChatSettings.Id"/> of the connection</param>
/// <returns><see langword="true"/> if it is connected, <see langword="false"/> otherwise</returns>
bool Connected(long connectionId);
/// <summary>
/// If the Discord client is connected
/// </summary>
bool DiscordConnected { get; }
/// <summary>
/// Change chat settings
/// Change chat settings. If the <see cref="ChatSettings.Id"/> is not currently in use, a new connection will be made instead
/// </summary>
/// <param name="newSettings">The new <see cref="ChatSettings"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
/// <returns>A <see cref="Task"/> representing the running operation. Will complete immediately if the <see cref="ChatSettings.Enabled"/> property of <paramref name="newSettings"/> is <see langword="false"/></returns>
Task ChangeSettings(ChatSettings newSettings, CancellationToken cancellationToken);
/// <summary>
/// Change chat channels
/// </summary>
/// <param name="connectionId">The <see cref="ChatSettings.Id"/> of the connection</param>
/// <param name="newChannels">An <see cref="IEnumerable{T}"/> of the new list of <see cref="Api.Models.ChatChannel"/>s</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task ChangeChannels(IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken);
Task ChangeChannels(long connectionId, IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken);
/// <summary>
/// Send a chat <paramref name="message"/> to a given set of <paramref name="channelIds"/>
@@ -63,6 +61,6 @@ namespace Tgstation.Server.Host.Components.Chat
/// <param name="commandsJsonName">The name of the chat commands json</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IDisposable"/> tied to the lifetime of the json trackings</returns>
Task<IChatJsonTrackingContext> TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken);
Task<IJsonTrackingContext> TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken);
}
}
@@ -5,7 +5,7 @@ namespace Tgstation.Server.Host.Components.Chat
/// <summary>
/// Represents a tracking of dynamic chat json files
/// </summary>
public interface IChatJsonTrackingContext : IDisposable
public interface IJsonTrackingContext : IDisposable
{
}
}
@@ -0,0 +1,18 @@
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Chat.Providers;
namespace Tgstation.Server.Host.Components.Chat
{
/// <summary>
/// Factory for <see cref="IProvider"/>s
/// </summary>
interface IProviderFactory
{
/// <summary>
/// Create a <see cref="IProvider"/>
/// </summary>
/// <param name="settings">The <see cref="ChatSettings"/> for the new provider</param>
/// <returns>A new <see cref="IProvider"/></returns>
IProvider CreateProvider(ChatSettings settings);
}
}
@@ -0,0 +1,8 @@
namespace Tgstation.Server.Host.Components.Chat.Providers
{
sealed class Message
{
string Content { get; set; }
User User { get; set; }
}
}
@@ -0,0 +1,27 @@
using System;
using System.Globalization;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Components.Chat.Providers;
namespace Tgstation.Server.Host.Components.Chat
{
/// <inheritdoc />
sealed class ProviderFactory : IProviderFactory
{
/// <inheritdoc />
public IProvider CreateProvider(Api.Models.Internal.ChatSettings settings)
{
if (settings == null)
throw new ArgumentNullException(nameof(settings));
switch (settings.Provider)
{
case ChatProvider.Irc:
throw new NotImplementedException();
case ChatProvider.Discord:
throw new NotImplementedException();
default:
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider));
}
}
}
}
@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Components.Chat.Providers
{
/// <summary>
/// For interacting with a chat service
/// </summary>
interface IProvider : IDisposable
{
/// <summary>
/// If the <see cref="IProvider"/>
/// </summary>
bool Connected { get; }
/// <summary>
/// The <see cref="string"/> that indicates the <see cref="IProvider"/> was mentioned
/// </summary>
string BotMention { get; }
/// <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; }
/// <summary>
/// Attempt to connect the <see cref="IProvider"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> on success, <see langword="false"/> otherwise</returns>
Task<bool> Connect(CancellationToken cancellationToken);
/// <summary>
/// Get the <see cref="Channel"/>s for given <paramref name="channels"/>
/// </summary>
/// <param name="channels">The <see cref="Api.Models.ChatChannel"/>s to map</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IReadOnlyList{T}"/> of the <see cref="Channel"/>s representing <paramref name="channels"/></returns>
Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<Api.Models.ChatChannel> channels, CancellationToken cancellationToken);
/// <summary>
/// Send a message to the <see cref="IProvider"/>
/// </summary>
/// <param name="channelId">The <see cref="Channel.Id"/> to send to</param>
/// <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);
}
}
@@ -5,7 +5,7 @@ namespace Tgstation.Server.Host.Components.Chat
/// <summary>
/// Represents a chat message requested by DD
/// </summary>
sealed class ChatResponse
sealed class Response
{
/// <summary>
/// The message string
@@ -0,0 +1,10 @@
namespace Tgstation.Server.Host.Components.Chat
{
class User
{
long Id { get; set; }
string FriendlyName { get; set; }
string Mention { get; set; }
Channel channel { get; set; }
}
}
@@ -102,9 +102,9 @@ namespace Tgstation.Server.Host.Components.Watchdog
readonly ISession session;
/// <summary>
/// The <see cref="IChatJsonTrackingContext"/> for the <see cref="SessionController"/>
/// The <see cref="IJsonTrackingContext"/> for the <see cref="SessionController"/>
/// </summary>
readonly IChatJsonTrackingContext chatJsonTrackingContext;
readonly IJsonTrackingContext chatJsonTrackingContext;
/// <summary>
/// The <see cref="IChat"/> for the <see cref="SessionController"/>
@@ -154,7 +154,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="chat">The value of <see cref="chat"/></param>
/// <param name="chatJsonTrackingContext">The value of <see cref="chatJsonTrackingContext"/></param>
/// <param name="logger">The value of <see cref="logger"/></param>
public SessionController(ReattachInformation reattachInformation, ISession session, IByondTopicSender byondTopicSender, IInteropRegistrar interopRegistrar, IChatJsonTrackingContext chatJsonTrackingContext, IChat chat, ILogger<SessionController> logger)
public SessionController(ReattachInformation reattachInformation, ISession session, IByondTopicSender byondTopicSender, IInteropRegistrar interopRegistrar, IJsonTrackingContext chatJsonTrackingContext, IChat chat, ILogger<SessionController> logger)
{
this.chatJsonTrackingContext = chatJsonTrackingContext; //null valid
this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation));
@@ -539,10 +539,10 @@ namespace Tgstation.Server.Host.Components.Watchdog
if (results == null)
return;
List<ChatResponse> responses;
List<Response> responses;
try
{
responses = JsonConvert.DeserializeObject<List<ChatResponse>>(results);
responses = JsonConvert.DeserializeObject<List<Response>>(results);
}
catch
{
@@ -9,7 +9,7 @@
public long Id { get; set; }
/// <summary>
/// The <see cref="ChatSettings.Id"/>
/// The <see cref="Api.Models.Internal.ChatSettings.Id"/>
/// </summary>
public long ChatSettingsId { get; set; }
@@ -5,12 +5,7 @@ namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class ChatSettings : Api.Models.Internal.ChatSettings
{
/// <summary>
/// The row Id
/// </summary>
public long Id { get; set; }
{
/// <summary>
/// The <see cref="Api.Models.Instance.Id"/>
/// </summary>