diff --git a/docs/API.dox b/docs/API.dox index 79ca621469..2eb1a0f76a 100644 --- a/docs/API.dox +++ b/docs/API.dox @@ -232,26 +232,26 @@ I DELETE "/Job/{JobId}" => OK @subsection api_chat Chat Bots -Each chat bot is represented by a @ref Tgstation.Server.Api.Models.ChatSettings object +Each chat bot is represented by a @ref Tgstation.Server.Api.Models.ChatBot object Chat bots can be created/updated/deleted with the following requests respectively -I PUT "/Chat" @ref Tgstation.Server.Api.Models.ChatSettings => Tgstation.Server.Api.Models.ChatSettings -I POST "/Chat" @ref Tgstation.Server.Api.Models.ChatSettings => Tgstation.Server.Api.Models.ChatSettings -I DELETE "/Chat/{ChatSettingsId}" => OK +I PUT "/Chat" @ref Tgstation.Server.Api.Models.ChatBot => Tgstation.Server.Api.Models.ChatBot +I POST "/Chat" @ref Tgstation.Server.Api.Models.ChatBot => Tgstation.Server.Api.Models.ChatBot +I DELETE "/Chat/{ChatBotId}" => OK -The @ref Tgstation.Server.Api.Models.Internal.ChatSettings.ConnectionString must differ based on what kind of chat bot you wish to create +The @ref Tgstation.Server.Api.Models.Internal.ChatBot.ConnectionString must differ based on what kind of chat bot you wish to create. Each @ref Tgstation.Server.Api.Models.ChatProvider has a @ref Tgstation.Server.Api.Models.Internal.ChatConnectionStringBuilder that dictates how to form it -For IRC chat bots it should be in the following format: -`";;;<1 to use SSL, 0 otherwise>[;<`The @ref Tgstation.Server.Api.Models.IrcPasswordType`;]"` +For IRC chat bots see @ref Tgstation.Server.Api.Models.IrcConnectionStringBuilder +For Discord chat bots see @ref Tgstation.Server.Api.Models.DiscordConnectionStringBuilder For Discord chat bots it should be the bot's Token A specific bot's settings may be retrieved with: -I GET "/Chat/{ChatSettingsId}" => @ref Tgstation.Server.Api.Models.ChatSettings +I GET "/Chat/{ChatBotId}" => @ref Tgstation.Server.Api.Models.ChatBot -Also note that if the @ref Tgstation.Server.Api.Models.ChatSettings.Channels is present in a POST request, the list will fully replace any active channels +Also note that if the @ref Tgstation.Server.Api.Models.ChatBot.Channels is present in a POST request, the list will fully replace any active channels @subsection api_byond Byond Version Management diff --git a/src/Tgstation.Server.Api/Models/ChatBot.cs b/src/Tgstation.Server.Api/Models/ChatBot.cs index 716469fd6e..97ce91ce3a 100644 --- a/src/Tgstation.Server.Api/Models/ChatBot.cs +++ b/src/Tgstation.Server.Api/Models/ChatBot.cs @@ -15,15 +15,15 @@ namespace Tgstation.Server.Api.Models /// /// Validates are correct for the /// - /// + /// if the are valid for the , otherwise public bool ValidateProviderChannelTypes() { switch (Provider) { case ChatProvider.Discord: - return Channels.Select(x => x.DiscordChannelId.HasValue && x.IrcChannel == null).All(x => x); + return Channels?.Select(x => x.DiscordChannelId.HasValue && x.IrcChannel == null).All(x => x) ?? true; case ChatProvider.Irc: - return Channels.Select(x => !x.DiscordChannelId.HasValue && x.IrcChannel != null).All(x => x); + return Channels?.Select(x => !x.DiscordChannelId.HasValue && x.IrcChannel != null).All(x => x) ?? true; default: throw new InvalidOperationException("Invalid provider type!"); } diff --git a/src/Tgstation.Server.Api/Models/DiscordConnectionStringBuilder.cs b/src/Tgstation.Server.Api/Models/DiscordConnectionStringBuilder.cs new file mode 100644 index 0000000000..a919e18380 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/DiscordConnectionStringBuilder.cs @@ -0,0 +1,37 @@ +using System; +using Tgstation.Server.Api.Models.Internal; + +namespace Tgstation.Server.Api.Models +{ + /// + /// for + /// + public sealed class DiscordConnectionStringBuilder : ChatConnectionStringBuilder + { + /// + public override bool Valid => !String.IsNullOrEmpty(BotToken); + + /// + /// The Discord bot token + /// + /// See https://discordapp.com/developers/docs/topics/oauth2#bots + public string BotToken { get; set; } + + /// + /// Construct a + /// + public DiscordConnectionStringBuilder() { } + + /// + /// Construct a from a + /// + /// The connection string + public DiscordConnectionStringBuilder(string connectionString) + { + BotToken = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); + } + + /// + public override string ToString() => BotToken; + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Api/Models/Internal/ChatBot.cs b/src/Tgstation.Server.Api/Models/Internal/ChatBot.cs index 784133ffd2..577321132d 100644 --- a/src/Tgstation.Server.Api/Models/Internal/ChatBot.cs +++ b/src/Tgstation.Server.Api/Models/Internal/ChatBot.cs @@ -1,4 +1,6 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace Tgstation.Server.Api.Models.Internal { @@ -33,5 +35,33 @@ namespace Tgstation.Server.Api.Models.Internal /// [Required] public string ConnectionString { get; set; } + + /// + /// The which maps to the + /// + [NotMapped] + public ChatConnectionStringBuilder ConnectionStringBuilder + { + get + { + if (ConnectionString == null) + return null; + switch (Provider) + { + case ChatProvider.Discord: + return new DiscordConnectionStringBuilder(ConnectionString); + case ChatProvider.Irc: + return new IrcConnectionStringBuilder(ConnectionString); + default: + throw new InvalidOperationException("Invalid Provider!"); + } + } + set + { + if (value?.Valid == false) + throw new InvalidOperationException("Cannot set invalid ChatConnectionStringBuilder!"); + ConnectionString = value?.ToString(); + } + } } } diff --git a/src/Tgstation.Server.Api/Models/Internal/ChatConnectionStringBuilder.cs b/src/Tgstation.Server.Api/Models/Internal/ChatConnectionStringBuilder.cs new file mode 100644 index 0000000000..b5d3b5f9a3 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/Internal/ChatConnectionStringBuilder.cs @@ -0,0 +1,19 @@ +namespace Tgstation.Server.Api.Models.Internal +{ + /// + /// Helper for building s + /// + public abstract class ChatConnectionStringBuilder + { + /// + /// If the evaluates to a valid + /// + public abstract bool Valid { get; } + + /// + /// Gets the associated with the + /// + /// + public abstract override string ToString(); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Api/Models/IrcConnectionStringBuilder.cs b/src/Tgstation.Server.Api/Models/IrcConnectionStringBuilder.cs new file mode 100644 index 0000000000..ba2f460d47 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/IrcConnectionStringBuilder.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Tgstation.Server.Api.Models.Internal; + +namespace Tgstation.Server.Api.Models +{ + /// + /// for + /// + public sealed class IrcConnectionStringBuilder : ChatConnectionStringBuilder + { + /// + public override bool Valid => Address != null && Port.HasValue && Port != 0 && UseSsl.HasValue && (PasswordType.HasValue ^ Password == null); + + /// + /// The IP address or URL of the IRC server + /// + public string Address { get; set; } + + /// + /// The port the server runs on + /// + public ushort? Port { get; set; } + + /// + /// The nickname for the bot to use + /// + public string Nickname { get; set; } + + /// + /// If the connection should be made using SSL + /// + public bool? UseSsl { get; set; } + + /// + /// The optional to use + /// + public IrcPasswordType? PasswordType { get; set; } + + /// + /// The optional password to use + /// + public string Password { get; set; } + + /// + /// Construct an + /// + public IrcConnectionStringBuilder() { } + + /// + /// Construct a from a + /// + /// The connection string + public IrcConnectionStringBuilder(string connectionString) + { + if (connectionString == null) + throw new ArgumentNullException(nameof(connectionString)); + var splits = connectionString.Split(';'); + + Address = splits[0]; + + if (splits.Length < 2) + return; + + if (UInt16.TryParse(splits[1], out var port)) + Port = port; + + if (splits.Length < 3) + return; + + Nickname = splits[2]; + + if (splits.Length < 4) + return; + + if (Boolean.TryParse(splits[3], out var useSsl)) + UseSsl = useSsl; + + if (splits.Length < 5) + return; + if (Enum.TryParse(splits[4], out var passwordType)) + switch (passwordType) + { + case IrcPasswordType.NickServ: + case IrcPasswordType.Sasl: + case IrcPasswordType.Server: + PasswordType = passwordType; + break; + } + + if (splits.Length < 6) + return; + + var rest = new List(splits); + rest.RemoveRange(0, 5); + Password = String.Join(";", rest); + } + + /// + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(Address); + sb.Append(';'); + sb.Append(Port); + sb.Append(';'); + sb.Append(Nickname); + sb.Append(';'); + if(UseSsl.HasValue) + sb.Append(Convert.ToInt32(UseSsl.Value)); + if (PasswordType.HasValue) + { + sb.Append(';'); + sb.Append((int)PasswordType); + sb.Append(';'); + sb.Append(Password); + } + return sb.ToString(); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcPasswordType.cs b/src/Tgstation.Server.Api/Models/IrcPasswordType.cs similarity index 60% rename from src/Tgstation.Server.Host/Components/Chat/Providers/IrcPasswordType.cs rename to src/Tgstation.Server.Api/Models/IrcPasswordType.cs index d31a3aba73..bb3451bfbd 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcPasswordType.cs +++ b/src/Tgstation.Server.Api/Models/IrcPasswordType.cs @@ -1,9 +1,9 @@ -namespace Tgstation.Server.Host.Components.Chat.Providers +namespace Tgstation.Server.Api.Models { /// - /// Represents the type of a password passed to the constructor of + /// Represents the type of a password for a /// - enum IrcPasswordType + public enum IrcPasswordType { /// /// Use server authentication diff --git a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj index d3a57d45bd..744e45438e 100644 --- a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj +++ b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj @@ -17,7 +17,7 @@ 4.0.0.0 json web api tgstation-server tgstation ss13 byond Prototype release - 4.0.0.0-preview6004 + 4.0.0.0-preview6005 diff --git a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj index cba09ccc5d..3251c49c5d 100644 --- a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj +++ b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj @@ -3,7 +3,7 @@ netstandard2.0 Full - 4.0.0.0-preview9111 + 4.0.0.0-preview9112 true Cyberboss /tg/station 13 diff --git a/src/Tgstation.Server.Host/Components/Chat/ProviderFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ProviderFactory.cs index 928e64f387..aee9d43965 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ProviderFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ProviderFactory.cs @@ -37,52 +37,17 @@ namespace Tgstation.Server.Host.Components.Chat { if (settings == null) throw new ArgumentNullException(nameof(settings)); + var builder = settings.ConnectionStringBuilder; + if (builder == null || !builder.Valid) + throw new InvalidOperationException("Invalid ChatConnectionStringBuilder!"); switch (settings.Provider) { case ChatProvider.Irc: - //Connection string semicolon delimited until the password field - if (settings.ConnectionString == null) - throw new InvalidOperationException("ConnectionString cannot be null!"); - var splits = settings.ConnectionString.Split(';'); - if (splits.Length < 4) - throw new InvalidOperationException("Invalid connection string!"); - - var address = splits[0]; - if (!UInt16.TryParse(splits[1], out var port)) - throw new InvalidOperationException("Unable to parse port!"); - var nick = splits[2]; - if (!Int32.TryParse(splits[3], out var intSsl)) - throw new InvalidOperationException("Unable to parse ssl option!"); - - IrcPasswordType? passwordType = null; - string password = null; - if (splits.Length > 4) - { - if (splits.Length < 6) - throw new InvalidOperationException("Invalid connection string!"); - if (!Int32.TryParse(splits[4], out var intPasswordType)) - throw new InvalidOperationException("Unable to parse password type!"); - - passwordType = (IrcPasswordType)intPasswordType; - switch (passwordType) - { - case IrcPasswordType.NickServ: - case IrcPasswordType.Sasl: - case IrcPasswordType.Server: - break; - default: - throw new InvalidOperationException("Invalid password type!"); - } - - var rest = new List(splits); - rest.RemoveRange(0, 5); - password = String.Join(";", rest); - } - - return new IrcProvider(loggerFactory.CreateLogger(), application, address, port, nick, password, passwordType, intSsl != 0); + var ircBuilder = (IrcConnectionStringBuilder)builder; + return new IrcProvider(loggerFactory.CreateLogger(), application, ircBuilder.Address, ircBuilder.Port.Value, ircBuilder.Nickname, ircBuilder.Password, ircBuilder.PasswordType, ircBuilder.UseSsl.Value); case ChatProvider.Discord: - //discord is just the bot token - return new DiscordProvider(loggerFactory.CreateLogger(), settings.ConnectionString); + var discordBuilder = (DiscordConnectionStringBuilder)builder; + return new DiscordProvider(loggerFactory.CreateLogger(), discordBuilder.BotToken); default: throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider)); } diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index 6828a5fe5e..325f639731 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -81,12 +81,11 @@ namespace Tgstation.Server.Host.Controllers return BadRequest(new ErrorMessage { Message = "Invalid provider!" }); } - if (!model.Enabled.HasValue) - return BadRequest(new ErrorMessage { Message = "enabled cannot be null!" }); - if (!model.ValidateProviderChannelTypes()) return BadRequest(new ErrorMessage { Message = "One or more of channels aren't formatted correctly for the given provider!" }); + model.Enabled = model.Enabled ?? false; + //try to update das db first var dbModel = new Models.ChatBot {