Merge pull request #563 from Cyberboss/Chatrevisionconfig

Iiiiii've been working on the railllllroooooadddd
This commit is contained in:
Jordan Brown
2018-08-13 23:36:42 -04:00
committed by GitHub
113 changed files with 2101 additions and 704 deletions
+15 -5
View File
@@ -9,7 +9,7 @@ The TGS4 API is designed to be a fully realized RESTful service. Once hosted, fo
Routes and their usages are defined as follows
`[I (If Instance is required)] <Http Method> "<Route>" [Request Model] => <Response Model>`
[I (If Instance is required)] <`Http Method`> "<`Route`>" [Request Model] => <`Response Model`>
@section api_lib Official Libraries
@@ -29,7 +29,7 @@ This document will reference the canonical C# models in the @ref Tgstation.Serve
@section api_header Headers
TGS4 expects this set of headers. Failure to provide them may result in
TGS4 expects this set of headers. Failure to provide them may result in 400 error responses
- User-Agent: The user agent product header value of the calling program. Should be in the form Agent/Version (i.e. SomeTgsClient/1.2.4)
- Accept: application/json
@@ -261,7 +261,7 @@ To clone the repository if it doesn't yet exist use the following request:
I PUT "/Repository" => @ref Tgstation.Server.Api.Models.Repository
The clone job will be represented by the @ref Tgstation.Server.Api.Models.Repository.ActiveJob field. Specify the @ref Tgstation.Server.Api.Models.Repository.Origin URL. Optionally specify the initial @ref Tgstation.Server.Api.Models.Repository.Reference as a git tag or branch. Be sure to specify the authentication fields if necessary to access your repository
The clone job will be represented by the @ref Tgstation.Server.Api.Models.Repository.ActiveJob field. Specify the @ref Tgstation.Server.Api.Models.Repository.Origin URL. Optionally specify the initial @ref Tgstation.Server.Api.Models.Repository.Reference as a git tag or branch. Be sure to specify the authentication fields if necessary to access your repository. Will return 409 if the repository already exists or is already being cloned
To delete an existing repository make the following request:
@@ -284,14 +284,14 @@ git pull (Only if @ref Tgstation.Server.Api.Models.Repository.Reference is set):
}
@endcode
git checkout <commit sha> (Unsets @ref Tgstation.Server.Api.Models.Repository.Reference):
git checkout <`commit sha`> (Unsets @ref Tgstation.Server.Api.Models.Repository.Reference):
@code{.json}
{
"checkoutSha": "<commit sha>"
}
@endcode
git checkout -f <branch or tag> && git clean -fxd (Sets @ref Tgstation.Server.Api.Models.Repository.Reference):
git checkout -f <`branch or tag`> && git clean -fxd (Sets @ref Tgstation.Server.Api.Models.Repository.Reference):
@code{.json}
{
"reference": "<branch or tag>"
@@ -364,4 +364,14 @@ The job object returned represents the compile job
{}
@endcode
The compiler endpoint is also used to read compile jobs
To list successful compile jobs ids use:
I GET "/DreamMaker/List" => Array of @ref Tgstation.Server.Api.Models.CompileJob
To get a specific compile job use:
I GET "/DreamMaker/{CompileJobId}" => @ref Tgstation.Server.Api.Models.CompileJob
*/
+21 -8
View File
@@ -48,7 +48,7 @@ namespace Tgstation.Server.Api
/// <summary>
/// The current <see cref="AssemblyName"/>
/// </summary>
static readonly AssemblyName assemblyName = Assembly.GetExecutingAssembly().GetName();
internal static readonly AssemblyName assemblyName = Assembly.GetExecutingAssembly().GetName();
/// <summary>
/// The <see cref="Models.Instance.Id"/> being accessed
@@ -85,6 +85,17 @@ namespace Tgstation.Server.Api
/// </summary>
public bool IsTokenAuthentication => Token != null;
/// <summary>
/// Checks if a given <paramref name="otherVersion"/> is compatible with our own
/// </summary>
/// <param name="otherVersion">The <see cref="Version"/> to test</param>
/// <returns><see langword="true"/> if the given version is compatible with the API. <see langword="false"/> otherwise</returns>
public static bool CheckCompatibility(Version otherVersion)
{
var ourVersion = assemblyName.Version;
return !(ourVersion.Major != otherVersion.Major || ourVersion.Minor != otherVersion.Minor || ourVersion.Build > otherVersion.Build);
}
/// <summary>
/// Construct <see cref="ApiHeaders"/> for JWT authentication
/// </summary>
@@ -141,10 +152,8 @@ namespace Tgstation.Server.Api
ApiVersion = apiVersion;
UserAgent = clientUserAgent.Product;
//check api version compatibility
var ourVersion = assemblyName.Version;
if (ourVersion.Major != ApiVersion.Major || ourVersion.Minor != ApiVersion.Minor || ourVersion.Build > ApiVersion.Build)
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Given API version is incompatible with version {0}!", ourVersion));
if(!CheckCompatibility(ApiVersion))
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Given API version is incompatible with version {0}!", ApiVersion));
if (!requestHeaders.Headers.TryGetValue(HeaderNames.Authorization, out StringValues authorization))
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Missing {0} header!", HeaderNames.Authorization));
@@ -207,10 +216,13 @@ namespace Tgstation.Server.Api
/// Set <see cref="HttpRequestHeaders"/> using the <see cref="ApiHeaders"/>. This initially clears <paramref name="headers"/>
/// </summary>
/// <param name="headers">The <see cref="HttpRequestHeaders"/> to set</param>
public void SetRequestHeaders(HttpRequestHeaders headers)
/// <param name="instanceId">The <see cref="Models.Instance.Id"/> for the request</param>
public void SetRequestHeaders(HttpRequestHeaders headers, long? instanceId = null)
{
if (headers == null)
throw new ArgumentNullException(nameof(headers));
if (instanceId.HasValue && InstanceId.HasValue && instanceId != InstanceId)
throw new InvalidOperationException("Specified instance ID in constructor and SetRequestHeaders!");
headers.Clear();
headers.Accept.Add(new MediaTypeWithQualityHeaderValue(ApplicationJson));
@@ -223,8 +235,9 @@ namespace Tgstation.Server.Api
}
headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
headers.Add(ApiVersionHeader, ApiVersion.ToString());
if(InstanceId.HasValue)
headers.Add(instanceIdHeader, InstanceId.ToString());
instanceId = instanceId ?? InstanceId;
if (instanceId.HasValue)
headers.Add(instanceIdHeader, instanceId.ToString());
}
}
}
@@ -6,16 +6,16 @@ using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models
{
/// <inheritdoc />
public sealed class ChatSettings : Internal.ChatSettings
public sealed class ChatBot : Internal.ChatBot
{
/// <summary>
/// Channels the Discord bot should listen/announce in
/// </summary>
[Permissions(WriteRight = ChatSettingsRights.WriteChannels)]
[Permissions(WriteRight = ChatBotRights.WriteChannels)]
public List<ChatChannel> Channels { get; set; }
/// <summary>
/// Validates <see cref="Channels"/> are correct for the <see cref="Internal.ChatSettings.Provider"/>
/// Validates <see cref="Channels"/> are correct for the <see cref="Internal.ChatBot.Provider"/>
/// </summary>
/// <returns></returns>
public bool ValidateProviderChannelTypes()
@@ -1,5 +1,4 @@
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models
{
@@ -1,4 +1,6 @@
namespace Tgstation.Server.Api.Models
using System;
namespace Tgstation.Server.Api.Models
{
/// <summary>
/// Represents an error message returned by the server
@@ -9,5 +11,10 @@
/// A human readable description of the error
/// </summary>
public string Message { get; set; }
/// <summary>
/// The version of the API the server is using
/// </summary>
public Version SeverApiVersion { get; set; } = ApiHeaders.assemblyName.Version;
}
}
@@ -47,10 +47,10 @@ namespace Tgstation.Server.Api.Models
public RepositoryRights? RepositoryRights { get; set; }
/// <summary>
/// The <see cref="Rights.ChatSettingsRights"/> of the <see cref="InstanceUser"/>
/// The <see cref="Rights.ChatBotRights"/> of the <see cref="InstanceUser"/>
/// </summary>
[Required]
public ChatSettingsRights? ChatSettingsRights { get; set; }
public ChatBotRights? ChatBotRights { get; set; }
/// <summary>
/// The <see cref="Rights.ConfigurationRights"/> of the <see cref="InstanceUser"/>
@@ -6,8 +6,8 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// Manage the server chat bots
/// </summary>
[Model(RightsType.ChatSettings, RequiresInstance = true, CanList = true, CanCrud = true, ReadRight = ChatSettingsRights.Read)]
public class ChatSettings
[Model(RightsType.ChatBots, RequiresInstance = true, CanList = true, CanCrud = true, ReadRight = ChatBotRights.Read)]
public class ChatBot
{
/// <summary>
/// The settings id
@@ -18,26 +18,26 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The name of the connection
/// </summary>
[Permissions(WriteRight = ChatSettingsRights.WriteName)]
[Permissions(WriteRight = ChatBotRights.WriteName)]
[Required]
public string Name { get; set; }
/// <summary>
/// If the connection is enabled
/// </summary>
[Permissions(WriteRight = ChatSettingsRights.WriteEnabled)]
[Permissions(WriteRight = ChatBotRights.WriteEnabled)]
public bool? Enabled { get; set; }
/// <summary>
/// The <see cref="ChatProvider"/> used for the connection
/// </summary>
[Permissions(WriteRight = ChatSettingsRights.WriteProvider)]
[Permissions(WriteRight = ChatBotRights.WriteProvider)]
public ChatProvider? Provider { get; set; }
/// <summary>
/// The information used to connect to the <see cref="Provider"/>
/// </summary>
[Permissions(ReadRight = ChatSettingsRights.ReadConnectionString, WriteRight = ChatSettingsRights.ReadConnectionString)]
[Permissions(ReadRight = ChatBotRights.ReadConnectionString, WriteRight = ChatBotRights.ReadConnectionString)]
[Required]
public string ConnectionString { get; set; }
}
@@ -1,4 +1,5 @@
using Tgstation.Server.Api.Rights;
using System.ComponentModel.DataAnnotations;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Api.Models.Internal
{
@@ -13,5 +14,12 @@ namespace Tgstation.Server.Api.Models.Internal
/// </summary>
[Permissions(WriteRight = DreamMakerRights.SetDme)]
public string ProjectName { get; set; }
/// <summary>
/// The port used during compilation to validate the TGS API
/// </summary>
[Permissions(WriteRight = DreamMakerRights.SetApiValidationPort)]
[Required]
public ushort? ApiValidationPort { get; set; }
}
}
@@ -59,6 +59,6 @@ namespace Tgstation.Server.Api.Models.Internal
/// The <see cref="Rights"/> required to cancel the <see cref="Job"/>
/// </summary>
[Permissions(DenyWrite = true)]
public int? CancelRight { get; set; }
public ulong? CancelRight { get; set; }
}
}
+8 -1
View File
@@ -1,4 +1,6 @@
namespace Tgstation.Server.Api.Models
using System;
namespace Tgstation.Server.Api.Models
{
/// <summary>
/// Represents a JWT returned by the API
@@ -9,5 +11,10 @@
/// The value of the JWT
/// </summary>
public string Bearer { get; set; }
/// <summary>
/// When the <see cref="Token"/> expires
/// </summary>
public DateTimeOffset? ExpiresAt { get; set; }
}
}
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Api.Rights
/// Rights for <see cref="Models.Administration"/>
/// </summary>
[Flags]
public enum AdministrationRights
public enum AdministrationRights : ulong
{
/// <summary>
/// User has no rights
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Api.Rights
/// Rights for <see cref="Models.Byond"/>
/// </summary>
[Flags]
public enum ByondRights
public enum ByondRights : ulong
{
/// <summary>
/// User has no rights
@@ -3,49 +3,49 @@
namespace Tgstation.Server.Api.Rights
{
/// <summary>
/// Rights for <see cref="Models.ChatSettings"/>
/// Rights for <see cref="Models.ChatBot"/>
/// </summary>
[Flags]
public enum ChatSettingsRights
public enum ChatBotRights : ulong
{
/// <summary>
/// User has no rights
/// </summary>
None = 0,
/// <summary>
/// User can change <see cref="Models.Internal.ChatSettings.Enabled"/>
/// User can change <see cref="Models.Internal.ChatBot.Enabled"/>
/// </summary>
WriteEnabled = 1,
/// <summary>
/// User can change <see cref="Models.Internal.ChatSettings.Provider"/>
/// User can change <see cref="Models.Internal.ChatBot.Provider"/>
/// </summary>
WriteProvider = 2,
/// <summary>
/// User can change <see cref="Models.ChatSettings.Channels"/>
/// User can change <see cref="Models.ChatBot.Channels"/>
/// </summary>
WriteChannels = 4,
/// <summary>
/// User can change <see cref="Models.Internal.ChatSettings.ConnectionString"/>
/// User can change <see cref="Models.Internal.ChatBot.ConnectionString"/>
/// </summary>
WriteConnectionString = 8,
/// <summary>
/// User can read <see cref="Models.Internal.ChatSettings.ConnectionString"/> requires <see cref="Read"/>
/// User can read <see cref="Models.Internal.ChatBot.ConnectionString"/> requires <see cref="Read"/>
/// </summary>
ReadConnectionString = 16,
/// <summary>
/// User can read all chat settings except <see cref="Models.Internal.ChatSettings.ConnectionString"/>
/// User can read all chat settings except <see cref="Models.Internal.ChatBot.ConnectionString"/>
/// </summary>
Read = 32,
/// <summary>
/// User can change <see cref="Models.Internal.ChatSettings.Name"/>
/// User can change <see cref="Models.Internal.ChatBot.Name"/>
/// </summary>
WriteName = 32,
/// <summary>
/// User can create new <see cref="Models.ChatSettings"/>
/// User can create new <see cref="Models.ChatBot"/>
/// </summary>
Create = 64,
/// <summary>
/// User can delete <see cref="Models.ChatSettings"/>
/// User can delete <see cref="Models.ChatBot"/>
/// </summary>
Delete = 128
}
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Api.Rights
/// Rights for <see cref="Models.ConfigurationFile"/>
/// </summary>
[Flags]
public enum ConfigurationRights
public enum ConfigurationRights : ulong
{
/// <summary>
/// User has no rights
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Api.Rights
/// Rights for <see cref="Models.DreamDaemon"/>
/// </summary>
[Flags]
public enum DreamDaemonRights
public enum DreamDaemonRights : ulong
{
/// <summary>
/// User has no rights
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Api.Rights
/// Rights for <see cref="Models.DreamMaker"/>
/// </summary>
[Flags]
public enum DreamMakerRights
public enum DreamMakerRights : ulong
{
/// <summary>
/// User has no rights
@@ -27,6 +27,14 @@ namespace Tgstation.Server.Api.Rights
/// <summary>
/// User may modify <see cref="Models.Internal.DreamMakerSettings.ProjectName"/>
/// </summary>
SetDme = 8
SetDme = 8,
/// <summary>
/// User may modify <see cref="Models.Internal.DreamMakerSettings.ApiValidationPort"/>
/// </summary>
SetApiValidationPort = 16,
/// <summary>
/// User may list and read all <see cref="Models.CompileJob"/>s
/// </summary>
List = 32
}
}
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Api.Rights
/// Rights for managing <see cref="Models.Instance"/>s
/// </summary>
[Flags]
public enum InstanceManagerRights
public enum InstanceManagerRights : ulong
{
/// <summary>
/// User has no rights
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Api.Rights
/// Rights for an <see cref="Models.Instance"/>
/// </summary>
[Flags]
public enum InstanceUserRights
public enum InstanceUserRights : ulong
{
/// <summary>
/// User has no rights
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Api.Rights
/// Rights for a <see cref="Models.Repository"/>
/// </summary>
[Flags]
public enum RepositoryRights
public enum RepositoryRights : ulong
{
/// <summary>
/// User has no rights
@@ -21,7 +21,7 @@ namespace Tgstation.Server.Api.Rights
{ RightsType.Byond, typeof(ByondRights) },
{ RightsType.DreamMaker, typeof(DreamMakerRights) },
{ RightsType.DreamDaemon, typeof(DreamDaemonRights) },
{ RightsType.ChatSettings, typeof(ChatSettingsRights) },
{ RightsType.ChatBots, typeof(ChatBotRights) },
{ RightsType.Configuration, typeof(ConfigurationRights) },
{ RightsType.InstanceUser, typeof(InstanceUserRights) }
};
@@ -3,7 +3,7 @@
/// <summary>
/// The type of rights a model uses
/// </summary>
public enum RightsType
public enum RightsType : ulong
{
/// <summary>
/// <see cref="AdministrationRights"/>
@@ -30,9 +30,9 @@
/// </summary>
DreamDaemon,
/// <summary>
/// <see cref="ChatSettingsRights"/>
/// <see cref="ChatBotRights"/>
/// </summary>
ChatSettings,
ChatBots,
/// <summary>
/// <see cref="ConfigurationRights"/>
/// </summary>
-28
View File
@@ -1,28 +0,0 @@
using System;
using System.Net.Http;
namespace Tgstation.Server.Api
{
/// <summary>
/// Represents a route to a server action
/// </summary>
public sealed class Route
{
/// <summary>
/// The path to the action
/// </summary>
public string Path { get; set; }
/// <summary>
/// The method of the action
/// </summary>
public HttpMethod Method { get; set; }
/// <summary>
/// Adds a <paramref name="host"/> portion to <see cref="Path"/>
/// </summary>
/// <param name="host">The host address</param>
/// <returns>A combined <paramref name="host"/> and <see cref="Path"/> <see cref="Uri"/></returns>
public Uri Flatten(Uri host) => new Uri(String.Concat(host.ToString().TrimEnd('/'), Path));
}
}
-163
View File
@@ -1,163 +0,0 @@
using System;
using System.Linq;
using System.Net.Http;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Api
{
/// <summary>
/// Gets routes for a given model
/// </summary>
public static class RouteHelper
{
/// <summary>
/// Read a <typeparamref name="TModel"/>
/// </summary>
/// <typeparam name="TModel">The model type to read</typeparam>
/// <param name="objectId">The optional ID to pass in</param>
/// <returns>A route to the read action</returns>
static Route Read<TModel>(long? objectId) where TModel : class
{
var result = new Route { Path = String.Concat('/', typeof(TModel).Name), Method = HttpMethod.Get };
if (objectId.HasValue)
result.Path = String.Concat(result.Path, '/', objectId);
return result;
}
/// <summary>
/// Get the <see cref="Route"/> to the read action for a given <typeparamref name="TModel"/>
/// </summary>
/// <typeparam name="TModel">The model to read</typeparam>
/// <param name="instance"><see cref="Instance"/> to read from if required</param>
/// <returns>A <see cref="Route"/> to the read action</returns>
public static Route Read<TModel>(Instance instance) where TModel : class
{
var model = (ModelAttribute)typeof(TModel).GetCustomAttributes(typeof(ModelAttribute), false).FirstOrDefault();
if (model == default(ModelAttribute))
throw new InvalidOperationException("TModel must have the ModelAttribute");
if (model.RequiresInstance ^ instance != null)
throw (model.RequiresInstance ? new ArgumentNullException(nameof(instance)) : new ArgumentException("Instance is not used for this route!", nameof(instance)));
return Read<TModel>(instance?.Id);
}
/// <summary>
/// Get the <see cref="Route"/> to the update action for a given <typeparamref name="TModel"/>
/// </summary>
/// <typeparam name="TModel">The model to update</typeparam>
/// <returns>A <see cref="Route"/> to the update action</returns>
public static Route Update<TModel>(Instance instance) where TModel : class
{
var result = Read<TModel>(instance);
result.Method = HttpMethod.Post;
return result;
}
/// <summary>
/// Get the <see cref="Route"/> to the list action for a given <typeparamref name="TModel"/>
/// </summary>
/// <typeparam name="TModel">The model to list</typeparam>
/// <returns>A <see cref="Route"/> to the list action</returns>
public static Route List<TModel>(Instance instance) where TModel : class
{
var result = Read<TModel>(instance);
result.Path = String.Concat(result.Path, '/', nameof(List));
return result;
}
/// <summary>
/// Get the <see cref="Route"/> to the create action for a given <typeparamref name="TModel"/>
/// </summary>
/// <typeparam name="TModel">The model to create</typeparam>
/// <returns>A <see cref="Route"/> to the create action</returns>
public static Route Create<TModel>(Instance instance) where TModel : class
{
var result = Read<TModel>(instance);
result.Method = HttpMethod.Put;
return result;
}
/// <summary>
/// Get the <see cref="Route"/> to the delete action for a given <typeparamref name="TModel"/>
/// </summary>
/// <typeparam name="TModel">The model to delete</typeparam>
/// <returns>A <see cref="Route"/> to the delete action</returns>
public static Route Delete<TModel>(Instance instance) where TModel : class
{
var result = Read<TModel>(instance);
result.Method = HttpMethod.Delete;
return result;
}
/// <summary>
/// Get the <see cref="Route"/> to a given <paramref name="job"/>
/// </summary>
/// <param name="job">The <see cref="Job"/> to get</param>
/// <returns>A <see cref="Route"/> to the get action</returns>
public static Route GetJob(Job job) => Read<Job>(job.Id);
/// <summary>
/// Get the <see cref="Route"/> to a given <paramref name="user"/>'s token list
/// </summary>
/// <param name="user">The <see cref="User"/> to list tokens for</param>
/// <returns>A <see cref="Route"/> to the list action</returns>
public static Route ListUserTokens(User user)
{
var result = List<Token>(null);
result.Path = String.Concat(result.Path, '/', user.Id);
return result;
}
/// <summary>
/// Get the <see cref="Route"/> to a server's version
/// </summary>
/// <returns></returns>
public static Route ServerVersion() => new Route { Path = "/", Method = HttpMethod.Get };
/// <summary>
/// Get the <see cref="Route"/> to read a <see cref="ConfigurationFile"/> file
/// </summary>
/// <param name="instance">The <see cref="Instance"/> the <see cref="ConfigurationFile"/> file resides in</param>
/// <param name="path">The path to the file in the <see cref="ConfigurationFile"/> directory</param>
/// <returns>A <see cref="Route"/> to the read action</returns>
public static Route ReadFile(Instance instance, string path) => new Route { Path = String.Concat("/Configuration/", instance?.Id ?? throw new ArgumentNullException(nameof(instance)), '/', path?.TrimStart('/') ?? throw new ArgumentNullException(nameof(path))), Method = HttpMethod.Get };
/// <summary>
/// Get the <see cref="Route"/> to list <see cref="ConfigurationFile"/> files for a <paramref name="directory"/>
/// </summary>
/// <param name="instance">The <see cref="Instance"/> the <see cref="ConfigurationFile"/> file resides in</param>
/// <param name="directory">The <see cref="ConfigurationFile"/> directory to list</param>
/// <returns>A <see cref="Route"/> to the read action</returns>
public static Route ListFiles(Instance instance, string directory)
{
var result = ReadFile(instance, directory);
result.Path = String.Concat("/ConfigurationList", result.Path.Substring(result.Path.IndexOf('/', 1)));
return result;
}
/// <summary>
/// Get the <see cref="Route"/> to create a <see cref="ConfigurationFile"/> file
/// </summary>
/// <param name="instance">The <see cref="Instance"/> the <see cref="ConfigurationFile"/> file resides in</param>
/// <param name="path">The path to the file in the <see cref="ConfigurationFile"/> directory</param>
/// <returns>A <see cref="Route"/> to the create action</returns>
public static Route CreateFile(Instance instance, string path)
{
var result = ReadFile(instance, path);
result.Method = HttpMethod.Put;
return result;
}
/// <summary>
/// Get the <see cref="Route"/> to delete a <see cref="ConfigurationFile"/> file
/// </summary>
/// <param name="instance">The <see cref="Instance"/> the <see cref="ConfigurationFile"/> file resides in</param>
/// <param name="path">The path to the file in the <see cref="ConfigurationFile"/> directory</param>
/// <returns>A <see cref="Route"/> to the delete action</returns>
public static Route DeleteFile(Instance instance, string path)
{
var result = ReadFile(instance, path);
result.Method = HttpMethod.Delete;
return result;
}
}
}
+86
View File
@@ -0,0 +1,86 @@
using System;
using System.Globalization;
namespace Tgstation.Server.Api
{
/// <summary>
/// Routes to a server actions
/// </summary>
public static class Routes
{
/// <summary>
/// The root controller
/// </summary>
public const string Root = "/";
/// <summary>
/// The <see cref="Models.Administration"/> controller
/// </summary>
public const string Administration = Root + nameof(Models.Administration);
/// <summary>
/// The <see cref="Models.User"/> controller
/// </summary>
public const string User = Root + nameof(Models.User);
/// <summary>
/// The <see cref="Models.Instance"/> controller
/// </summary>
public const string InstanceManager = Root + nameof(Models.Instance);
/// <summary>
/// The <see cref="Models.Byond"/> controller
/// </summary>
public const string Byond = Root + nameof(Models.Byond);
/// <summary>
/// The <see cref="Models.Repository"/> controller
/// </summary>
public const string Repository = Root + nameof(Models.Repository);
/// <summary>
/// The <see cref="Models.DreamDaemon"/> controller
/// </summary>
public const string DreamDaemon = Root + nameof(Models.DreamDaemon);
/// <summary>
/// The <see cref="Models.ConfigurationFile"/> controller
/// </summary>
public const string Configuration = Root + "Config";
/// <summary>
/// The <see cref="Models.InstanceUser"/> controller
/// </summary>
public const string InstanceUser = Root + nameof(Models.InstanceUser);
/// <summary>
/// The <see cref="Models.ChatBot"/> controller
/// </summary>
public const string Chat = Root + "Chat";
/// <summary>
/// The <see cref="Models.DreamMaker"/> controller
/// </summary>
public const string DreamMaker = Root + nameof(Models.DreamMaker);
/// <summary>
/// The <see cref="Models.Job"/> controller
/// </summary>
public const string Jobs = Root + nameof(Models.Job);
/// <summary>
/// Apply an <paramref name="id"/> postfix to a <paramref name="route"/>
/// </summary>
/// <param name="route">The route</param>
/// <param name="id">The ID</param>
/// <returns>The <paramref name="route"/> with <paramref name="id"/> appended</returns>
public static string SetID(string route, long id) => String.Format(CultureInfo.InvariantCulture, "{0}/{1}", route, id);
/// <summary>
/// Get the /List postfix for a <paramref name="route"/>
/// </summary>
/// <param name="route">The route</param>
/// <returns>The <paramref name="route"/> with /List appended</returns>
public static string List(string route) => String.Format(CultureInfo.InvariantCulture, "{0}/List", route);
}
}
@@ -1,13 +0,0 @@
namespace Tgstation.Server.Api.Routes
{
/// <summary>
/// Routes for <see cref="Models.Administration"/>
/// </summary>
public static class Administration
{
/// <summary>
/// The base route
/// </summary>
public const string Base = "/" + nameof(Administration);
}
}
@@ -25,12 +25,12 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<DocumentationFile>bin\Release\netstandard2.0\Tgstation.Server.Api.xml</DocumentationFile>
<NoWarn>1701;1702;1705;CA2227;CA1819</NoWarn>
<NoWarn>1701;1702;1705;CA2227;CA1819;CA1028</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<LangVersion>latest</LangVersion>
<NoWarn>1701;1702;1705;CA2227;CA1819</NoWarn>
<NoWarn>1701;1702;1705;CA2227;CA1819;CA1028</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -0,0 +1,31 @@
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
/// <inheritdoc />
sealed class AdministrationClient : IAdministrationClient
{
/// <summary>
/// The <see cref="apiClient"/> for the <see cref="AdministrationClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// Construct an <see cref="AdministrationClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
public AdministrationClient(IApiClient apiClient)
{
this.apiClient = apiClient;
}
/// <inheritdoc />
public Task<Administration> Read(CancellationToken cancellationToken) => apiClient.Read<Administration>(Routes.Administration, cancellationToken);
/// <inheritdoc />
public Task Update(Administration administration, CancellationToken cancellationToken) => apiClient.Update(Routes.Administration, administration, cancellationToken);
}
}
+177
View File
@@ -0,0 +1,177 @@
using Newtonsoft.Json;
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
/// <inheritdoc />
sealed class ApiClient : IApiClient
{
/// <inheritdoc />
public Uri Url { get; }
/// <inheritdoc />
public ApiHeaders Headers { get; }
/// <inheritdoc />
public TimeSpan Timeout
{
get => httpClient.Timeout;
set => httpClient.Timeout = value;
}
/// <summary>
/// The <see cref="HttpClient"/> for the <see cref="IApiClient"/>
/// </summary>
readonly HttpClient httpClient;
/// <summary>
/// Construct an <see cref="ApiClient"/>
/// </summary>
/// <param name="url">The value of <see cref="Url"/></param>
/// <param name="apiHeaders">The value of <see cref="ApiHeaders"/></param>
public ApiClient(Uri url, ApiHeaders apiHeaders)
{
Url = url ?? throw new ArgumentNullException(nameof(url));
Headers = apiHeaders ?? throw new ArgumentNullException(nameof(apiHeaders));
httpClient = new HttpClient();
}
/// <inheritdoc />
public void Dispose() => httpClient.Dispose();
/// <summary>
/// Main request method
/// </summary>
/// <param name="route">The route to run</param>
/// <param name="body">The body of the request</param>
/// <param name="method">The method of the request</param>
/// <param name="instanceId">The optional <see cref="Api.Models.Instance.Id"/> for the request</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the response on success</returns>
async Task<TResult> RunRequest<TResult>(string route, object body, HttpMethod method, long? instanceId, CancellationToken cancellationToken)
{
if (route == null)
throw new ArgumentNullException(nameof(route));
if (method == null)
throw new ArgumentNullException(nameof(method));
if (body == null && (method == HttpMethod.Post || method == HttpMethod.Put))
throw new InvalidOperationException("Body cannot be null for POST or PUT!");
var fullUri = new Uri(Url, route);
HttpContent content = null;
if (body != null)
content = new StringContent(JsonConvert.SerializeObject(body));
Task<HttpResponseMessage> task;
lock (this)
{
httpClient.DefaultRequestHeaders.Clear();
Headers.SetRequestHeaders(httpClient.DefaultRequestHeaders, instanceId);
if (method == HttpMethod.Get)
task = httpClient.GetAsync(route);
else if (method == HttpMethod.Put)
task = httpClient.PutAsync(fullUri, content, cancellationToken);
else if (method == HttpMethod.Post)
task = httpClient.PostAsync(fullUri, content, cancellationToken);
else if (method == HttpMethod.Delete)
task = httpClient.DeleteAsync(fullUri, cancellationToken);
else
throw new NotSupportedException();
}
var response = await task.ConfigureAwait(false);
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (!response.IsSuccessStatusCode) {
ErrorMessage errorMessage = null;
try
{
//check if json serializes to an error message
errorMessage = JsonConvert.DeserializeObject<ErrorMessage>(json);
}
catch (JsonSerializationException) { }
switch (response.StatusCode)
{
case HttpStatusCode.BadRequest:
//validate our api version is compatible
if(errorMessage != null && ApiHeaders.CheckCompatibility(errorMessage.SeverApiVersion))
throw new ApiMismatchException(errorMessage);
goto default;
case HttpStatusCode.Unauthorized:
throw new UnauthorizedException();
case HttpStatusCode.RequestTimeout:
throw new RequestTimeoutException();
case HttpStatusCode.Forbidden:
throw new InsufficientPermissionsException();
case HttpStatusCode.Gone:
case HttpStatusCode.NotFound:
case HttpStatusCode.Conflict:
throw new ConflictException(errorMessage, response.StatusCode);
case HttpStatusCode.NotImplemented:
throw new MethodNotSupportedException();
case HttpStatusCode.InternalServerError:
//response
throw new ServerErrorException(json); //json is html
case (HttpStatusCode)429: //rate limited
response.Headers.TryGetValues("Retry-After", out var values);
throw new RateLimitException(values?.FirstOrDefault());
default:
throw new ApiConflictException(errorMessage, response.StatusCode);
}
}
if (String.IsNullOrWhiteSpace(json))
json = JsonConvert.SerializeObject(new object());
return JsonConvert.DeserializeObject<TResult>(json);
}
/// <inheritdoc />
public Task<TResult> Create<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Put, null, cancellationToken);
/// <inheritdoc />
public Task<TResult> Read<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Get, null, cancellationToken);
/// <inheritdoc />
public Task<TResult> Update<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Post, null, cancellationToken);
/// <inheritdoc />
public Task<TResult> Update<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Post, null, cancellationToken);
/// <inheritdoc />
public Task Update<TBody>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<object>(route, body, HttpMethod.Post, null, cancellationToken);
/// <inheritdoc />
public Task<TResult> Create<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Put, null, cancellationToken);
/// <inheritdoc />
public Task Delete(string route, CancellationToken cancellationToken) => RunRequest<object>(route, null, HttpMethod.Delete, null, cancellationToken);
/// <inheritdoc />
public Task<TResult> Create<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Put, instanceId, cancellationToken);
/// <inheritdoc />
public Task<TResult> Read<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Get, instanceId, cancellationToken);
/// <inheritdoc />
public Task<TResult> Update<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Post, instanceId, cancellationToken);
/// <inheritdoc />
public Task Delete(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<object>(route, null, HttpMethod.Delete, instanceId, cancellationToken);
/// <inheritdoc />
public Task<TResult> Create<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Put, instanceId, cancellationToken);
}
}
@@ -0,0 +1,12 @@
using System;
using Tgstation.Server.Api;
namespace Tgstation.Server.Client
{
/// <inheritdoc />
sealed class ApiClientFactory : IApiClientFactory
{
/// <inheritdoc />
public IApiClient CreateApiClient(Uri url, ApiHeaders apiHeaders) => new ApiClient(url, apiHeaders);
}
}
@@ -0,0 +1,37 @@
using System;
using System.Net;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
/// <summary>
/// Occurs when the server returns an unknown response
/// </summary>
public sealed class ApiConflictException : ClientException
{
/// <summary>
/// Construct an <see cref="ApiConflictException"/> using an <paramref name="errorMessage"/>
/// </summary>
/// <param name="errorMessage">The <see cref="ErrorMessage"/> for the <see cref="ClientException"/></param>
/// <param name="statusCode">The <see cref="HttpStatusCode"/> for the <see cref="ClientException"/></param>
public ApiConflictException(ErrorMessage errorMessage, HttpStatusCode statusCode) : base(errorMessage, statusCode) { }
/// <summary>
/// Construct an <see cref="ApiConflictException"/>
/// </summary>
public ApiConflictException() { }
/// <summary>
/// Construct an <see cref="ApiConflictException"/> with a <paramref name="message"/>
/// </summary>
/// <param name="message">The message for the <see cref="Exception"/></param>
public ApiConflictException(string message) : base(message) { }
/// <summary>
/// Construct an <see cref="ApiConflictException"/> with a <paramref name="message"/> and <paramref name="innerException"/>
/// </summary>
/// <param name="message">The message for the <see cref="Exception"/></param>
/// <param name="innerException">The inner <see cref="Exception"/> for the base <see cref="Exception"/></param>
public ApiConflictException(string message, Exception innerException) : base(message, innerException) { }
}
}
@@ -0,0 +1,40 @@
using System;
using System.Net;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
/// <summary>
/// Occurs when the API version of the client is not compatible with the server's
/// </summary>
public sealed class ApiMismatchException : ClientException
{
/// <summary>
/// Construct an <see cref="ApiMismatchException"/> using an <paramref name="errorMessage"/>
/// </summary>
/// <param name="errorMessage">The <see cref="ErrorMessage"/> for the <see cref="ClientException"/></param>
public ApiMismatchException(ErrorMessage errorMessage) : base(errorMessage, HttpStatusCode.BadRequest)
{
if (errorMessage == null)
throw new ArgumentNullException(nameof(errorMessage));
}
/// <summary>
/// Construct an <see cref="ApiMismatchException"/>
/// </summary>
public ApiMismatchException() { }
/// <summary>
/// Construct an <see cref="ApiMismatchException"/> with a <paramref name="message"/>
/// </summary>
/// <param name="message">The message for the <see cref="Exception"/></param>
public ApiMismatchException(string message) : base(message) { }
/// <summary>
/// Construct an <see cref="ApiMismatchException"/> with a <paramref name="message"/> and <paramref name="innerException"/>
/// </summary>
/// <param name="message">The message for the <see cref="Exception"/></param>
/// <param name="innerException">The inner <see cref="Exception"/> for the base <see cref="Exception"/></param>
public ApiMismatchException(string message, Exception innerException) : base(message, innerException) { }
}
}
@@ -0,0 +1,48 @@
using System;
using System.Net;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
/// <summary>
/// Exceptions thrown by <see cref="IServerClient"/>s
/// </summary>
public abstract class ClientException : Exception
{
/// <summary>
/// The <see cref="HttpStatusCode"/> of the <see cref="ClientException"/>
/// </summary>
HttpStatusCode StatusCode { get; }
Version ServerApiVersion { get; }
/// <summary>
/// Construct a <see cref="ClientException"/> using an <paramref name="errorMessage"/> and <paramref name="statusCode"/>
/// </summary>
/// <param name="errorMessage">The <see cref="ErrorMessage"/> associated with the <see cref="ClientException"/></param>
/// <param name="statusCode">The <see cref="HttpStatusCode"/> of the <see cref="ClientException"/></param>
protected ClientException(ErrorMessage errorMessage, HttpStatusCode statusCode) : base(errorMessage == null ? throw new ArgumentNullException(nameof(errorMessage)) : errorMessage.Message ?? "Unknown Error")
{
StatusCode = statusCode;
ServerApiVersion = errorMessage?.SeverApiVersion;
}
/// <summary>
/// Construct a <see cref="ClientException"/>
/// </summary>
protected ClientException() { }
/// <summary>
/// Construct a <see cref="ClientException"/> with a <paramref name="message"/>
/// </summary>
/// <param name="message">The message for the <see cref="Exception"/></param>
protected ClientException(string message) : base(message) { }
/// <summary>
/// Construct a <see cref="ClientException"/> with a <paramref name="message"/> and <paramref name="innerException"/>
/// </summary>
/// <param name="message">The message for the <see cref="Exception"/></param>
/// <param name="innerException">The inner <see cref="Exception"/> for the base <see cref="Exception"/></param>
protected ClientException(string message, Exception innerException) : base(message, innerException) { }
}
}
@@ -0,0 +1,38 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
/// <inheritdoc />
sealed class ByondClient : IByondClient
{
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="ByondClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="ByondClient"/>
/// </summary>
readonly Instance instance;
/// <summary>
/// Construct a <see cref="ByondClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="instance">The value of <see cref="Instance"/></param>
public ByondClient(IApiClient apiClient, Instance instance)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
/// <inheritdoc />
public Task<Byond> Read(CancellationToken cancellationToken) => apiClient.Read<Byond>(Routes.Byond, instance.Id, cancellationToken);
/// <inheritdoc />
public Task<Byond> Update(Byond byond, CancellationToken cancellationToken) => apiClient.Update<Byond, Byond>(Routes.Byond, byond, instance.Id, cancellationToken);
}
}
@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
/// <inheritdoc />
sealed class ChatBotsClient : IChatBotsClient
{
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="ChatBotsClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="ChatBotsClient"/>
/// </summary>
readonly Instance instance;
/// <summary>
/// Construct a <see cref="ChatBotsClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="instance">The value of <see cref="instance"/></param>
public ChatBotsClient(IApiClient apiClient, Instance instance)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
/// <inheritdoc />
public Task<ChatBot> Create(ChatBot settings, CancellationToken cancellationToken) => apiClient.Create<ChatBot, ChatBot>(Routes.Chat, settings, instance.Id, cancellationToken);
/// <inheritdoc />
public Task Delete(ChatBot settings, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.Chat, settings.Id), instance.Id, cancellationToken);
/// <inheritdoc />
public Task<IReadOnlyList<ChatBot>> List(CancellationToken cancellationToken) => apiClient.Create<IReadOnlyList<ChatBot>>(Routes.List(Routes.Chat), instance.Id, cancellationToken);
/// <inheritdoc />
public Task<ChatBot> Update(ChatBot settings, CancellationToken cancellationToken) => apiClient.Update<ChatBot, ChatBot>(Routes.Chat, settings, instance.Id, cancellationToken);
}
}
@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
/// <inheritdoc />
sealed class ConfigurationClient : IConfigurationClient
{
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="ConfigurationClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="ConfigurationClient"/>
/// </summary>
readonly Instance instance;
/// <summary>
/// Construct a <see cref="ConfigurationClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="instance">The value of <see cref="instance"/></param>
public ConfigurationClient(IApiClient apiClient, Instance instance)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
/// <inheritdoc />
public Task<IReadOnlyList<ConfigurationFile>> List(string directory, CancellationToken cancellationToken)
{
if (directory == null)
directory = String.Empty;
return apiClient.Read<IReadOnlyList<ConfigurationFile>>(Routes.List(Routes.Configuration) + directory, instance.Id, cancellationToken);
}
/// <inheritdoc />
public Task<ConfigurationFile> Read(ConfigurationFile file, CancellationToken cancellationToken)
{
if (file == null)
throw new ArgumentNullException(nameof(file));
return apiClient.Read<ConfigurationFile>(Routes.Configuration + file.Path, instance.Id, cancellationToken);
}
/// <inheritdoc />
public Task<ConfigurationFile> Write(ConfigurationFile file, CancellationToken cancellationToken) => apiClient.Update<ConfigurationFile, ConfigurationFile>(Routes.Configuration, file, instance.Id, cancellationToken);
}
}
@@ -0,0 +1,44 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
/// <inheritdoc />
sealed class DreamDaemonClient : IDreamDaemonClient
{
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="DreamDaemonClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="DreamDaemonClient"/>
/// </summary>
readonly Instance instance;
/// <summary>
/// Construct a <see cref="DreamDaemonClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="instance">The value of <see cref="instance"/></param>
public DreamDaemonClient(IApiClient apiClient, Instance instance)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
/// <inheritdoc />
public Task Shutdown(CancellationToken cancellationToken) => apiClient.Delete(Routes.DreamDaemon, instance.Id, cancellationToken);
/// <inheritdoc />
public Task<DreamDaemon> Start(CancellationToken cancellationToken) => apiClient.Create<DreamDaemon>(Routes.DreamDaemon, instance.Id, cancellationToken);
/// <inheritdoc />
public Task<DreamDaemon> Read(CancellationToken cancellationToken) => apiClient.Read<DreamDaemon>(Routes.DreamDaemon, instance.Id, cancellationToken);
/// <inheritdoc />
public Task<DreamDaemon> Update(DreamDaemon dreamDaemon, CancellationToken cancellationToken) => apiClient.Update<DreamDaemon, DreamDaemon>(Routes.DreamDaemon, dreamDaemon, instance.Id, cancellationToken);
}
}
@@ -0,0 +1,41 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
/// <inheritdoc />
sealed class DreamMakerClient : IDreamMakerClient
{
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="DreamMakerClient"/>
/// </summary>
private IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="DreamMakerClient"/>
/// </summary>
private Instance instance;
/// <summary>
/// Construct a <see cref="DreamMakerClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="instance">The value of <see cref="Instance"/></param>
public DreamMakerClient(IApiClient apiClient, Instance instance)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
/// <inheritdoc />
public Task<Job> Compile(CancellationToken cancellationToken) => apiClient.Create<Job>(Routes.DreamMaker, instance.Id, cancellationToken);
/// <inheritdoc />
public Task<DreamMaker> Read(CancellationToken cancellationToken) => apiClient.Read<DreamMaker>(Routes.DreamMaker, instance.Id, cancellationToken);
/// <inheritdoc />
public Task<DreamMaker> Update(DreamMaker dreamMaker, CancellationToken cancellationToken) => apiClient.Update<DreamMaker, DreamMaker>(Routes.DreamMaker, dreamMaker, instance.Id, cancellationToken);
}
}
@@ -1,29 +1,27 @@
using System;
using System.Threading;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Client.Components
{
/// <summary>
/// For managing the <see cref="Byond"/> installation
/// </summary>
public interface IByondClient : IRightsClient<ByondRights>
public interface IByondClient
{
/// <summary>
/// Get the <see cref="Byond"/> represented by the <see cref="IByondClient"/>
/// Get the <see cref="Byond"/> information
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="Byond"/> represented by the <see cref="IByondClient"/></returns>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="Byond"/> information</returns>
Task<Byond> Read(CancellationToken cancellationToken);
/// <summary>
/// Updates the installed BYOND <see cref="Version"/>
/// Updates the <see cref="Byond"/> information
/// </summary>
/// <param name="version">The <see cref="Version"/> to set to active</param>
/// <param name="byond">The <see cref="Byond"/> information to update</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task SetActiveVersion(Version version, CancellationToken cancellationToken);
/// <returns>A <see cref="Task{TResult}"/> resulting in the updated <see cref="Byond"/> information</returns>
Task<Byond> Update(Byond byond, CancellationToken cancellationToken);
}
}
@@ -0,0 +1,44 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
/// <summary>
/// For managing the chat bots
/// </summary>
public interface IChatBotsClient
{
/// <summary>
/// List the <see cref="ChatBot"/>
/// </summary>
/// <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="ChatBot"/> of the server</returns>
Task<IReadOnlyList<ChatBot>> List(CancellationToken cancellationToken);
/// <summary>
/// Create a <see cref="ChatBot"/>
/// </summary>
/// <param name="settings">The <see cref="ChatBot"/> to create</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the new <see cref="ChatBot"/></returns>
Task<ChatBot> Create(ChatBot settings, CancellationToken cancellationToken);
/// <summary>
/// Updates a <see cref="ChatBot"/> setttings
/// </summary>
/// <param name="settings">The <see cref="ChatBot"/> to update</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the updated <see cref="ChatBot"/></returns>
Task<ChatBot> Update(ChatBot settings, CancellationToken cancellationToken);
/// <summary>
/// Delete a <see cref="ChatBot"/>
/// </summary>
/// <param name="settings">The <see cref="ChatBot"/> to delete</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Delete(ChatBot settings, CancellationToken cancellationToken);
}
}
@@ -1,28 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Client.Components
{
/// <summary>
/// For managing the chat bots
/// </summary>
public interface IChatSettingsClient : IRightsClient<ChatSettingsRights>
{
/// <summary>
/// Get the <see cref="ChatSettings"/> represented by the <see cref="IChatSettingsClient"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="ChatSettings"/> represented by the <see cref="IChatSettingsClient"/></returns>
Task<ChatSettings> Read(CancellationToken cancellationToken);
/// <summary>
/// Updates the <see cref="ChatSettings"/> setttings
/// </summary>
/// <param name="chat">The <see cref="ChatSettings"/> to update</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Update(ChatSettings chat, CancellationToken cancellationToken);
}
}
@@ -1,16 +1,14 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Client.Components
{
/// <summary>
/// For managing <see cref="ConfigurationFile"/> files
/// </summary>
public interface IConfigurationClient : IRightsClient<ConfigurationRights>
public interface IConfigurationClient
{
/// <summary>
/// List configuration files
@@ -26,30 +24,14 @@ namespace Tgstation.Server.Client.Components
/// <param name="file">The <see cref="ConfigurationFile"/> file to read</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Read(ConfigurationFile file, CancellationToken cancellationToken);
Task<ConfigurationFile> Read(ConfigurationFile file, CancellationToken cancellationToken);
/// <summary>
/// Overwrite a <see cref="ConfigurationFile"/> file with integrity checks
/// Overwrite a <see cref="ConfigurationFile"/> file
/// </summary>
/// <param name="file">The <see cref="ConfigurationFile"/> file to write</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Write(ConfigurationFile file, CancellationToken cancellationToken);
/// <summary>
/// Create/overwrite a <see cref="ConfigurationFile"/> file
/// </summary>
/// <param name="file">The <see cref="ConfigurationFile"/> file to write</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Create(ConfigurationFile file, CancellationToken cancellationToken);
/// <summary>
/// Delete a <see cref="ConfigurationFile"/> file
/// </summary>
/// <param name="file">The <see cref="ConfigurationFile"/> file to delete</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Delete(ConfigurationFile file, CancellationToken cancellationToken);
Task<ConfigurationFile> Write(ConfigurationFile file, CancellationToken cancellationToken);
}
}
@@ -1,34 +1,33 @@
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Client.Components
{
/// <summary>
/// For managing <see cref="DreamDaemon"/>
/// </summary>
public interface IDreamDaemonClient: IRightsClient<DreamDaemonRights>
public interface IDreamDaemonClient
{
/// <summary>
/// Get the <see cref="DreamDaemon"/> represented by the <see cref="IDreamDaemonClient"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="DreamDaemon"/> represented by the <see cref="IDreamDaemonClient"/></returns>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="DreamDaemon"/> information</returns>
Task<DreamDaemon> Read(CancellationToken cancellationToken);
/// <summary>
/// Start <see cref="DreamDaemon"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Start(CancellationToken cancellationToken);
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="DreamDaemon"/> information</returns>
Task<DreamDaemon> Start(CancellationToken cancellationToken);
/// <summary>
/// Shutdown <see cref="DreamDaemon"/>
/// </summary>
/// <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{TResult}"/> resulting in the <see cref="DreamDaemon"/> information</returns>
Task Shutdown(CancellationToken cancellationToken);
/// <summary>
@@ -36,7 +35,7 @@ namespace Tgstation.Server.Client.Components
/// </summary>
/// <param name="dreamDaemon">The <see cref="DreamDaemon"/> to update</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Update(DreamDaemon dreamDaemon, CancellationToken cancellationToken);
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="DreamDaemon"/> information</returns>
Task<DreamDaemon> Update(DreamDaemon dreamDaemon, CancellationToken cancellationToken);
}
}
@@ -1,21 +1,20 @@
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Client.Components
{
/// <summary>
/// For managing the compiler
/// </summary>
public interface IDreamMakerClient : IRightsClient<DreamMakerRights>
public interface IDreamMakerClient
{
/// <summary>
/// Get the <see cref="DreamMaker"/> represented by the <see cref="IDreamMakerClient"/>
/// Get the <see cref="DreamMaker"/> information
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="DreamMaker"/> represented by the <see cref="IDreamMakerClient"/></returns>
Task<ChatSettings> Read(CancellationToken cancellationToken);
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="DreamMaker"/> information</returns>
Task<DreamMaker> Read(CancellationToken cancellationToken);
/// <summary>
/// Updates the <see cref="DreamMaker"/> setttings
@@ -23,13 +22,13 @@ namespace Tgstation.Server.Client.Components
/// <param name="dreamMaker">The <see cref="DreamMaker"/> to update</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Update(DreamMaker dreamMaker, CancellationToken cancellationToken);
Task<DreamMaker> Update(DreamMaker dreamMaker, CancellationToken cancellationToken);
/// <summary>
/// Compile the current repository revision
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Compile(CancellationToken cancellationToken);
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="Job"/> for the compile</returns>
Task<Job> Compile(CancellationToken cancellationToken);
}
}
@@ -1,5 +1,4 @@
using System.Threading;
using System.Threading.Tasks;
using System;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
@@ -9,6 +8,11 @@ namespace Tgstation.Server.Client.Components
/// </summary>
public interface IInstanceClient
{
/// <summary>
/// The <see cref="Instance"/> used to create the <see cref="IInstanceClient"/>
/// </summary>
Instance Metadata { get; }
/// <summary>
/// Access the <see cref="IByondClient"/>
/// </summary>
@@ -19,10 +23,10 @@ namespace Tgstation.Server.Client.Components
/// </summary>
IRepositoryClient Repository { get; }
/// <summary>
/// Access the <see cref="IDreamDaemonClient"/>
/// </summary>
IDreamDaemonClient DreamDaemon { get; }
/// <summary>
/// Access the <see cref="IDreamDaemonClient"/>
/// </summary>
IDreamDaemonClient DreamDaemon { get; }
/// <summary>
/// Access the <see cref="IConfigurationClient"/>
@@ -35,20 +39,18 @@ namespace Tgstation.Server.Client.Components
IInstanceUserClient Users { get; }
/// <summary>
/// Access the <see cref="IChatSettingsClient"/>
/// Access the <see cref="IChatBotsClient"/>
/// </summary>
IChatSettingsClient Chat { get; }
IChatBotsClient ChatBots { get; }
/// <summary>
/// Access the <see cref="IDreamMakerClient"/>
/// </summary>
IDreamMakerClient DreamMaker { get; }
/// <summary>
/// Get the <see cref="Instance"/> represented by the <see cref="IInstanceClient"/>
/// Access the <see cref="IJobsClient"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="Instance"/> represented by the <see cref="IInstanceClient"/></returns>
Task<Instance> Read(CancellationToken cancellationToken);
IJobsClient Jobs { get; }
}
}
@@ -2,28 +2,50 @@
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Client.Components
{
/// <summary>
/// For managing <see cref="InstanceUser"/>s
/// </summary>
public interface IInstanceUserClient : IRightsClient<InstanceUserRights>
{
/// <summary>
/// Get the <see cref="InstanceUser"/>s in the <see cref="Instance"/>
/// </summary>
/// <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 <see cref="InstanceUser"/>s in the instance</returns>
Task<IReadOnlyList<InstanceUser>> Read(CancellationToken cancellationToken);
public interface IInstanceUserClient
{
/// <summary>
/// Get the <see cref="InstanceUser"/> associated with the logged on user
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="InstanceUser"/> associated with the logged on user</returns>
Task<InstanceUser> Read(CancellationToken cancellationToken);
/// <summary>
/// Update a <paramref name="instanceUser"/>
/// </summary>
/// <param name="instanceUser">The <see cref="InstanceUser"/> to update</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Update(InstanceUser instanceUser, CancellationToken cancellationToken);
}
/// <summary>
/// Get the <see cref="InstanceUser"/>s in the <see cref="Instance"/>
/// </summary>
/// <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 <see cref="InstanceUser"/>s in the instance</returns>
Task<IReadOnlyList<InstanceUser>> List(CancellationToken cancellationToken);
/// <summary>
/// Update a <paramref name="instanceUser"/>
/// </summary>
/// <param name="instanceUser">The <see cref="InstanceUser"/> to update</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task<InstanceUser> Update(InstanceUser instanceUser, CancellationToken cancellationToken);
/// <summary>
/// Create a <paramref name="instanceUser"/>
/// </summary>
/// <param name="instanceUser">The <see cref="InstanceUser"/> to create</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> reulting in the new <see cref="InstanceUser"/></returns>
Task<InstanceUser> Create(InstanceUser instanceUser, CancellationToken cancellationToken);
/// <summary>
/// Delete a <paramref name="instanceUser"/>
/// </summary>
/// <param name="instanceUser">The <see cref="InstanceUser"/> to delete</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Delete(InstanceUser instanceUser, CancellationToken cancellationToken);
}
}
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
@@ -11,12 +12,20 @@ namespace Tgstation.Server.Client.Components
public interface IJobsClient
{
/// <summary>
/// List the active jobs the user can view
/// List the <see cref="Api.Models.Internal.Job.Id"/>s in the <see cref="Instance"/>
/// </summary>
/// <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 active <see cref="Job"/>s the user can view</returns>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IReadOnlyList{T}"/> of the <see cref="Api.Models.Internal.Job.Id"/>s in the <see cref="Instance"/></returns>
Task<IReadOnlyList<Job>> List(CancellationToken cancellationToken);
/// <summary>
/// Get a <paramref name="job"/>
/// </summary>
/// <param name="job">The <see cref="Job"/> to get</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="Job"/></returns>
Task<Job> Read(Job job, CancellationToken cancellationToken);
/// <summary>
/// Cancels a <paramref name="job"/>
/// </summary>
@@ -24,5 +33,15 @@ namespace Tgstation.Server.Client.Components
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Cancel(Job job, CancellationToken cancellationToken);
}
/// <summary>
/// Creates a <see cref="Task{TResult}"/> that completes when a given <paramref name="job"/> is completed
/// </summary>
/// <param name="job">The <see cref="Job"/> to create a <see cref="Task"/> for</param>
/// <param name="requeryRate">The rate in to poll the server for results</param>
/// <param name="progressCallback">A <see cref="Action{T}"/> to run with 0-100 progress</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> which will trigger the cancellation of the <paramref name="job"/></param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a complete <see cref="Job"/></returns>
Task<Job> CreateTaskFromJob(Job job, TimeSpan requeryRate, Action<int> progressCallback, CancellationToken cancellationToken);
}
}
@@ -2,29 +2,34 @@
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Client.Components
{
/// <summary>
/// For managing the <see cref="Repository"/>
/// </summary>
public interface IRepositoryClient : IRightsClient<RepositoryRights>
public interface IRepositoryClient
{
/// <summary>
/// Get the <see cref="Repository"/> represented by the <see cref="IRepositoryClient"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="Repository"/> represented by the <see cref="IRepositoryClient"/></returns>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="Repository"/></returns>
Task<Repository> Read(CancellationToken cancellationToken);
/// <summary>
/// Update the <see cref="Repository"/>
/// </summary>
/// <param name="repository">The <see cref="Repository"/> to update</param>
/// <param name="progressCallback">Optional action to take when progress is reported. Will either not be called for some operations, or be called with the numbers 1-100</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the updated <see cref="Repository"/></returns>
Task<Repository> Update(Repository repository, CancellationToken cancellationToken);
/// <summary>
/// Deletes the <see cref="Repository"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Update(Repository repository, Action<int> progressCallback, CancellationToken cancellationToken);
Task Delete(CancellationToken cancellationToken);
}
}
@@ -0,0 +1,61 @@
using System;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
/// <inheritdoc />
sealed class InstanceClient : IInstanceClient
{
/// <inheritdoc />
public Instance Metadata { get; }
/// <inheritdoc />
public IByondClient Byond { get; }
/// <inheritdoc />
public IRepositoryClient Repository { get; }
/// <inheritdoc />
public IDreamDaemonClient DreamDaemon { get; }
/// <inheritdoc />
public IConfigurationClient Configuration { get; }
/// <inheritdoc />
public IInstanceUserClient Users { get; }
/// <inheritdoc />
public IChatBotsClient ChatBots { get; }
/// <inheritdoc />
public IDreamMakerClient DreamMaker { get; }
/// <inheritdoc />
public IJobsClient Jobs { get; }
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="InstanceClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// Construct a <see cref="InstanceClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="instance">The value of <see cref="Metadata"/></param>
public InstanceClient(IApiClient apiClient, Instance instance)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
Metadata = instance ?? throw new ArgumentNullException(nameof(instance));
Byond = new ByondClient(apiClient, instance);
Repository = new RepositoryClient(apiClient, instance);
DreamDaemon = new DreamDaemonClient(apiClient, instance);
Configuration = new ConfigurationClient(apiClient, instance);
Users = new InstanceUserClient(apiClient, instance);
ChatBots = new ChatBotsClient(apiClient, instance);
DreamMaker = new DreamMakerClient(apiClient, instance);
Jobs = new JobsClient(apiClient, instance);
}
}
}
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
/// <inheritdoc />
sealed class InstanceUserClient : IInstanceUserClient
{
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="InstanceUserClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="InstanceUserClient"/>
/// </summary>
readonly Instance instance;
/// <summary>
/// Construct an <see cref="InstanceUserClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="instance">The value of <see cref="instance"/></param>
public InstanceUserClient(IApiClient apiClient, Instance instance)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
/// <inheritdoc />
public Task<InstanceUser> Create(InstanceUser user, CancellationToken cancellationToken) => apiClient.Create<InstanceUser, InstanceUser>(Routes.InstanceUser, user, instance.Id, cancellationToken);
public Task Delete(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.InstanceUser, instanceUser.UserId.Value), instance.Id, cancellationToken);
/// <inheritdoc />
public Task<InstanceUser> Read(CancellationToken cancellationToken) => apiClient.Read<InstanceUser>(Routes.InstanceUser, instance.Id, cancellationToken);
/// <inheritdoc />
public Task<InstanceUser> Update(InstanceUser user, CancellationToken cancellationToken) => apiClient.Update<InstanceUser, InstanceUser>(Routes.InstanceUser, user, instance.Id, cancellationToken);
/// <inheritdoc />
public Task<IReadOnlyList<InstanceUser>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<InstanceUser>>(Routes.List(Routes.InstanceUser), instance.Id, cancellationToken);
}
}
@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
/// <inheritdoc />
sealed class JobsClient : IJobsClient
{
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="JobsClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="JobsClient"/>
/// </summary>
readonly Instance instance;
/// <summary>
/// Construct a <see cref="JobsClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="instance">The value of <see cref="Instance"/></param>
public JobsClient(IApiClient apiClient, Instance instance)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
/// <inheritdoc />
public Task Cancel(Job job, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.Jobs, job.Id), instance.Id, cancellationToken);
/// <inheritdoc />
public Task<IReadOnlyList<Job>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<Job>>(Routes.List(Routes.Jobs), instance.Id, cancellationToken);
/// <inheritdoc />
public Task<Job> Read(Job job, CancellationToken cancellationToken) => apiClient.Read<Job>(Routes.SetID(Routes.Jobs, job.Id), instance.Id, cancellationToken);
/// <inheritdoc />
public async Task<Job> CreateTaskFromJob(Job job, TimeSpan requeryRate, Action<int> progressCallback, CancellationToken cancellationToken)
{
if (job == null)
throw new ArgumentNullException(nameof(job));
int? lastProgress = null;
while (!job.StoppedAt.HasValue)
{
await Task.Delay(requeryRate, cancellationToken).ConfigureAwait(false);
job = await Read(job, cancellationToken).ConfigureAwait(false);
if(job.Progress.HasValue && job.Progress != lastProgress)
{
progressCallback(job.Progress.Value);
lastProgress = job.Progress;
}
}
return job;
}
}
}
@@ -0,0 +1,40 @@
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
/// <inheritdoc />
sealed class RepositoryClient : IRepositoryClient
{
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="RepositoryClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="RepositoryClient"/>
/// </summary>
readonly Instance instance;
/// <summary>
/// Construct a <see cref="RepositoryClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="instance"></param>
public RepositoryClient(IApiClient apiClient, Instance instance)
{
this.apiClient = apiClient;
this.instance = instance;
}
/// <inheritdoc />
public Task Delete(CancellationToken cancellationToken) => apiClient.Delete(Routes.Repository, instance.Id, cancellationToken);
/// <inheritdoc />
public Task<Repository> Read(CancellationToken cancellationToken) => apiClient.Read<Repository>(Routes.Repository, instance.Id, cancellationToken);
/// <inheritdoc />
public Task<Repository> Update(Repository repository, CancellationToken cancellationToken) => apiClient.Update<Repository, Repository>(Routes.Repository, repository, instance.Id, cancellationToken);
}
}
@@ -0,0 +1,38 @@
using System;
using System.Net;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
/// <summary>
/// Occurs when the client performs an action that would result in data conflict
/// </summary>
public sealed class ConflictException : ClientException
{
/// <summary>
/// Construct an <see cref="ConflictException"/> with a <paramref name="errorMessage"/> and <paramref name="statusCode"/>
/// </summary>
/// <param name="errorMessage">The <see cref="ErrorMessage"/> for the <see cref="ClientException"/></param>
/// <param name="statusCode">The <see cref="HttpStatusCode"/> for the <see cref="ClientException"/></param>
public ConflictException(ErrorMessage errorMessage, HttpStatusCode statusCode) : base(errorMessage, statusCode)
{ }
/// <summary>
/// Construct a <see cref="ConflictException"/>
/// </summary>
public ConflictException() { }
/// <summary>
/// Construct an <see cref="ConflictException"/> with a <paramref name="message"/>
/// </summary>
/// <param name="message">The message for the <see cref="Exception"/></param>
public ConflictException(string message) : base(message) { }
/// <summary>
/// Construct an <see cref="ConflictException"/> with a <paramref name="message"/> and <paramref name="innerException"/>
/// </summary>
/// <param name="message">The message for the <see cref="Exception"/></param>
/// <param name="innerException">The inner <see cref="Exception"/> for the base <see cref="Exception"/></param>
public ConflictException(string message, Exception innerException) : base(message, innerException) { }
}
}
@@ -1,14 +1,13 @@
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
namespace Tgstation.Server.Client.Components
namespace Tgstation.Server.Client
{
/// <summary>
/// For managing server administration
/// </summary>
public interface IAdministrationClient : IRightsClient<AdministrationRights>
public interface IAdministrationClient
{
/// <summary>
/// Get the <see cref="Administration"/> represented by the <see cref="IAdministrationClient"/>
+33
View File
@@ -0,0 +1,33 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
namespace Tgstation.Server.Client
{
/// <summary>
/// Web interface for the API
/// </summary>
interface IApiClient : IDisposable
{
ApiHeaders Headers { get; }
Uri Url { get; }
TimeSpan Timeout { get; set; }
Task<TResult> Create<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken);
Task<TResult> Create<TResult>(string route, CancellationToken cancellationToken);
Task<TResult> Read<TResult>(string route, CancellationToken cancellationToken);
Task<TResult> Update<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken);
Task<TResult> Update<TResult>(string route, CancellationToken cancellationToken);
Task Update<TBody>(string route, TBody body, CancellationToken cancellationToken);
Task Delete(string route, CancellationToken cancellationToken);
Task<TResult> Create<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken);
Task<TResult> Create<TResult>(string route, long instanceId, CancellationToken cancellationToken);
Task<TResult> Read<TResult>(string route, long instanceId, CancellationToken cancellationToken);
Task<TResult> Update<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken);
Task Delete(string route, long instanceId, CancellationToken cancellationToken);
}
}
@@ -0,0 +1,19 @@
using System;
using Tgstation.Server.Api;
namespace Tgstation.Server.Client
{
/// <summary>
/// For creating <see cref="IApiClient"/>s
/// </summary>
interface IApiClientFactory
{
/// <summary>
/// Create an <see cref="IApiClient"/>
/// </summary>
/// <param name="url">The base <see cref="Uri"/></param>
/// <param name="apiHeaders">The <see cref="ApiHeaders"/> for the <see cref="IApiClient"/></param>
/// <returns>A new <see cref="IApiClient"/></returns>
IApiClient CreateApiClient(Uri url, ApiHeaders apiHeaders);
}
}
@@ -2,37 +2,37 @@
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Client.Components;
namespace Tgstation.Server.Client.Components
namespace Tgstation.Server.Client
{
/// <summary>
/// For managing <see cref="Instance"/>s
/// </summary>
public interface IInstanceManagerClient : IRightsClient<InstanceUserRights>
public interface IInstanceManagerClient
{
/// <summary>
/// Get all <see cref="IInstanceClient"/>s for <see cref="Instance"/>s the user can view
/// </summary>
/// <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 all <see cref="IInstanceClient"/>s for <see cref="Instance"/>s the user can view</returns>
Task<IReadOnlyList<IInstanceClient>> Read(CancellationToken cancellationToken);
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IReadOnlyList{T}"/> of all <see cref="Instance"/>s the user can view</returns>
Task<IReadOnlyList<Instance>> List(CancellationToken cancellationToken);
/// <summary>
/// Create an <paramref name="instance"/>
/// </summary>
/// <param name="instance">The <see cref="Instance"/> to create. <see cref="Instance.Id"/> will be ignored</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in <see cref="IInstanceClient"/> for the created <see cref="Instance"/></returns>
Task<IInstanceClient> Create(Instance instance, CancellationToken cancellationToken);
/// <returns>A <see cref="Task{TResult}"/> resulting in the created <see cref="Instance"/></returns>
Task<Instance> Create(Instance instance, CancellationToken cancellationToken);
/// <summary>
/// Relocates, renamed, and/or on/offlines an <paramref name="instance"/>
/// </summary>
/// <param name="instance">The <see cref="Instance"/> to update</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Update(Instance instance, CancellationToken cancellationToken);
/// <returns>A <see cref="Task{TResult}"/> resulting in the updated <see cref="Instance"/></returns>
Task<Instance> Update(Instance instance, CancellationToken cancellationToken);
/// <summary>
/// Deletes an <paramref name="instance"/>
@@ -41,5 +41,12 @@ namespace Tgstation.Server.Client.Components
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Delete(Instance instance, CancellationToken cancellationToken);
/// <summary>
/// Create an <see cref="IInstanceClient"/> for a given <see cref="Instance"/>
/// </summary>
/// <param name="instance">The <see cref="Instance"/> to create an <see cref="IInstanceClient"/> for</param>
/// <returns>A new <see cref="IInstanceClient"/></returns>
IInstanceClient CreateClient(Instance instance);
}
}
@@ -1,19 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Client
{
/// <summary>
/// <see cref="Api"/> client that has a <typeparamref name="TRights"/> bitset
/// </summary>
/// <typeparam name="TRights">The <see cref="Api.Rights"/> for the <see cref="IRightsClient{TRights}"/></typeparam>
public interface IRightsClient<TRights>
{
/// <summary>
/// Get the <typeparamref name="TRights"/> for the <see cref="IRightsClient{TRights}"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <typeparamref name="TRights"/> for the <see cref="IRightsClient{TRights}"/></returns>
Task<TRights> Rights(CancellationToken cancellationToken);
}
}
+9 -21
View File
@@ -1,7 +1,7 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Client.Components;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
@@ -11,24 +11,14 @@ namespace Tgstation.Server.Client
public interface IServerClient : IDisposable
{
/// <summary>
/// The <see cref="System.Version"/> of the <see cref="IServerClient"/>
/// The <see cref="Token"/> being used to access the server
/// </summary>
Version Version { get; }
Token Token { get; }
/// <summary>
/// The connection timeout in milliseconds. Defaults to 10000
/// The connection timeout
/// </summary>
int Timeout { get; set; }
/// <summary>
/// How long to return initially cached models for in seconds. Defaults to 60
/// </summary>
int CacheExpiry { get; set; }
/// <summary>
/// The requery rate for job updates in milliseconds. Defaults to 5000
/// </summary>
int RequeryRate { get; set; }
TimeSpan Timeout { get; set; }
/// <summary>
/// Access the <see cref="IInstanceManagerClient"/>
@@ -41,15 +31,13 @@ namespace Tgstation.Server.Client
IAdministrationClient Administration { get; }
/// <summary>
/// These generally shouldn't be used in favor of the <see cref="Task"/> based polling and <see cref="CancellationToken"/>s other clients use. However, this is the only way to access jobs the client didn't start in it's current session
/// Access the <see cref="IUsersClient"/>
/// </summary>
IJobsClient Jobs { get; }
IUsersClient Users { get; }
/// <summary>
/// The <see cref="System.Version"/> of the connected server
/// The <see cref="System.Version"/> of the <see cref="IServerClient"/>
/// </summary>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="System.Version"/> of the connected server</returns>
/// <remarks>Note that if the <see cref="System.Version.Build"/> differs from the <see cref="Version"/>'s API functionality will most likely be compromised</remarks>
Task<Version> GetServerVersion(CancellationToken cancellationToken);
Task<Version> Version(CancellationToken cancellationToken);
}
}
@@ -1,5 +1,7 @@
using System.Threading;
using System;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
@@ -11,24 +13,21 @@ namespace Tgstation.Server.Client
/// <summary>
/// Create a <see cref="IServerClient"/>
/// </summary>
/// <param name="hostname">The URL to access tgstation-server at</param>
/// <param name="host">The URL to access TGS</param>
/// <param name="username">The username to for the <see cref="IServerClient"/></param>
/// <param name="password">The password for the <see cref="IServerClient"/></param>
/// <param name="timeout">The initial timeout for the connection in milliseconds</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <param name="timeout">The <see cref="TimeSpan"/> representing timeout for the connection</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a new <see cref="IServerClient"/></returns>
/// <exception cref="System.UnauthorizedAccessException">If the <paramref name="username"/> and/or <paramref name="password"/> is invalid</exception>
Task<IServerClient> CreateServerClient(string hostname, string username, string password, int timeout, CancellationToken cancellationToken);
Task<IServerClient> CreateServerClient(Uri host, string username, string password, TimeSpan timeout = default, CancellationToken cancellationToken = default);
/// <summary>
/// Create a <see cref="IServerClient"/>
/// </summary>
/// <param name="hostname">The URL to access tgstation-server at</param>
/// <param name="token">The <see cref="Api.Models.Token.Bearer"/> to access the API with</param>
/// <param name="timeout">The initial timeout for the connection</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a new <see cref="IServerClient"/></returns>
/// <exception cref="System.UnauthorizedAccessException">If the <paramref name="token"/> invalid</exception>
Task<IServerClient> CreateServerClient(string hostname, string token, int timeout, CancellationToken cancellationToken);
/// <param name="host">The URL to access TGS</param>
/// <param name="token">The <see cref="Token"/> to access the API with</param>
/// <param name="timeout">The <see cref="TimeSpan"/> representing timeout for the connection</param>
/// <returns>A new <see cref="IServerClient"/></returns>
IServerClient CreateServerClient(Uri host, Token token, TimeSpan timeout = default);
}
}
@@ -0,0 +1,35 @@
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
/// <summary>
/// For managing <see cref="User"/>s
/// </summary>
public interface IUsersClient
{
/// <summary>
/// Read the current user's information and general rights
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns></returns>
Task<User> Read(CancellationToken cancellationToken);
/// <summary>
/// Create a new <paramref name="user"/>
/// </summary>
/// <param name="user">The <see cref="UserUpdate"/> used to create the new <see cref="User"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>The new <see cref="User"/></returns>
Task<User> Create(UserUpdate user, CancellationToken cancellationToken);
/// <summary>
/// Update a <paramref name="user"/>
/// </summary>
/// <param name="user">The <see cref="UserUpdate"/> used to update the <see cref="User"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>The updated <see cref="User"/></returns>
Task<User> Update(UserUpdate user, CancellationToken cancellationToken);
}
}
@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Client.Components;
namespace Tgstation.Server.Client
{
/// <inheritdoc />
sealed class InstanceManagerClient : IInstanceManagerClient
{
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="InstanceManagerClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// Map of already created <see cref="IInstanceClient"/>s
/// </summary>
readonly Dictionary<long, IInstanceClient> cachedClients;
/// <summary>
/// Construct an <see cref="InstanceManagerClient"/>
/// </summary>
/// <param name="apiClient"></param>
public InstanceManagerClient(IApiClient apiClient)
{
this.apiClient = apiClient;
cachedClients = new Dictionary<long, IInstanceClient>();
}
/// <inheritdoc />
public Task<Instance> Create(Instance instance, CancellationToken cancellationToken) => apiClient.Create<Instance, Instance>(Routes.InstanceManager, instance, cancellationToken);
/// <inheritdoc />
public Task Delete(Instance instance, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.InstanceManager, instance.Id), cancellationToken);
/// <inheritdoc />
public Task<IReadOnlyList<Instance>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<Instance>>(Routes.List(Routes.InstanceManager), cancellationToken);
/// <inheritdoc />
public Task<Instance> Update(Instance instance, CancellationToken cancellationToken) => apiClient.Update<Instance, Instance>(Routes.InstanceManager, instance, cancellationToken);
/// <inheritdoc />
public IInstanceClient CreateClient(Instance instance)
{
if (!cachedClients.TryGetValue(instance.Id, out var client))
{
client = new InstanceClient(apiClient, instance);
cachedClients.Add(instance.Id, client);
}
return client;
}
}
}
@@ -0,0 +1,35 @@
using System;
using System.Net;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
/// <summary>
/// Occurs when the client attempts to perform an action they do not have the rights for
/// </summary>
public sealed class InsufficientPermissionsException : ClientException
{
/// <summary>
/// Construct an <see cref="InsufficientPermissionsException"/>
/// </summary>
public InsufficientPermissionsException() : base(new ErrorMessage
{
Message = "The credentials provided do not have sufficient rights to make this request!",
SeverApiVersion = null
}, HttpStatusCode.Forbidden)
{ }
/// <summary>
/// Construct an <see cref="InsufficientPermissionsException"/> with a <paramref name="message"/>
/// </summary>
/// <param name="message">The message for the <see cref="Exception"/></param>
public InsufficientPermissionsException(string message) : base(message) { }
/// <summary>
/// Construct an <see cref="InsufficientPermissionsException"/> with a <paramref name="message"/> and <paramref name="innerException"/>
/// </summary>
/// <param name="message">The message for the <see cref="Exception"/></param>
/// <param name="innerException">The inner <see cref="Exception"/> for the base <see cref="Exception"/></param>
public InsufficientPermissionsException(string message, Exception innerException) : base(message, innerException) { }
}
}
@@ -0,0 +1,35 @@
using System;
using System.Net;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
/// <summary>
/// Occurs when the client tries to use a currently unsupported API
/// </summary>
public sealed class MethodNotSupportedException : ClientException
{
/// <summary>
/// Construct an <see cref="MethodNotSupportedException"/>
/// </summary>
public MethodNotSupportedException() : base(new ErrorMessage
{
Message = "This method is not currently supported!",
SeverApiVersion = null
}, HttpStatusCode.NotImplemented)
{ }
/// <summary>
/// Construct an <see cref="MethodNotSupportedException"/> with a <paramref name="message"/>
/// </summary>
/// <param name="message">The message for the <see cref="Exception"/></param>
public MethodNotSupportedException(string message) : base(message) { }
/// <summary>
/// Construct an <see cref="MethodNotSupportedException"/> with a <paramref name="message"/> and <paramref name="innerException"/>
/// </summary>
/// <param name="message">The message for the <see cref="Exception"/></param>
/// <param name="innerException">The inner <see cref="Exception"/> for the base <see cref="Exception"/></param>
public MethodNotSupportedException(string message, Exception innerException) : base(message, innerException) { }
}
}
@@ -0,0 +1,42 @@
using System;
using System.Net;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
/// <summary>
/// Occurs when a GitHub rate limit occurs
/// </summary>
public sealed class RateLimitException : ClientException
{
DateTimeOffset RetryAfter { get; }
/// <summary>
/// Construct an <see cref="RateLimitException"/>
/// </summary>
public RateLimitException() { }
/// <summary>
/// Construct an <see cref="RateLimitException"/> with a <paramref name="secondsString"/>
/// </summary>
/// <param name="secondsString">The message for the <see cref="Exception"/></param>
public RateLimitException(string secondsString) : base(new ErrorMessage
{
Message = "GitHub rate limit reached!",
SeverApiVersion = null
}, (HttpStatusCode)429)
{
if (Int32.TryParse(secondsString, out var seconds) && seconds >= 0)
RetryAfter = DateTimeOffset.Now.AddSeconds(seconds);
else
RetryAfter = DateTimeOffset.Now;
}
/// <summary>
/// Construct an <see cref="RateLimitException"/> with a <paramref name="message"/> and <paramref name="innerException"/>
/// </summary>
/// <param name="message">The message for the <see cref="Exception"/></param>
/// <param name="innerException">The inner <see cref="Exception"/> for the base <see cref="Exception"/></param>
public RateLimitException(string message, Exception innerException) : base(message, innerException) { }
}
}
@@ -0,0 +1,35 @@
using System;
using System.Net;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
/// <summary>
/// Occurs when the client provides invalid credentials
/// </summary>
public sealed class RequestTimeoutException : ClientException
{
/// <summary>
/// Construct an <see cref="RequestTimeoutException"/>
/// </summary>
public RequestTimeoutException() : base(new ErrorMessage
{
Message = "The request timed out!",
SeverApiVersion = null
}, HttpStatusCode.RequestTimeout)
{ }
/// <summary>
/// Construct an <see cref="RequestTimeoutException"/> with a <paramref name="message"/>
/// </summary>
/// <param name="message">The message for the <see cref="Exception"/></param>
public RequestTimeoutException(string message) : base(message) { }
/// <summary>
/// Construct an <see cref="RequestTimeoutException"/> with a <paramref name="message"/> and <paramref name="innerException"/>
/// </summary>
/// <param name="message">The message for the <see cref="Exception"/></param>
/// <param name="innerException">The inner <see cref="Exception"/> for the base <see cref="Exception"/></param>
public RequestTimeoutException(string message, Exception innerException) : base(message, innerException) { }
}
}
@@ -0,0 +1,59 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
/// <inheritdoc />
sealed class ServerClient : IServerClient
{
/// <inheritdoc />
public Token Token { get; }
/// <inheritdoc />
public TimeSpan Timeout
{
get => apiClient.Timeout;
set => apiClient.Timeout = value;
}
/// <inheritdoc />
public IInstanceManagerClient Instances { get; }
/// <inheritdoc />
public IAdministrationClient Administration { get; }
/// <inheritdoc />
public IUsersClient Users { get; }
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="ServerClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// Construct a <see cref="ServerClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="token">The value of <see cref="Token"/></param>
public ServerClient(IApiClient apiClient, Token token)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
Token = token ?? throw new ArgumentNullException(nameof(token));
if (Token.Bearer != apiClient.Headers.Token)
throw new ArgumentOutOfRangeException(nameof(token), token, "Provided token does not match apiClient headers!");
Instances = new InstanceManagerClient(apiClient);
Users = new UsersClient(apiClient);
Administration = new AdministrationClient(apiClient);
}
/// <inheritdoc />
public void Dispose() => apiClient.Dispose();
/// <inheritdoc />
public Task<Version> Version(CancellationToken cancellationToken) => apiClient.Read<Version>(Routes.Root, cancellationToken);
}
}
@@ -1,16 +1,67 @@
using System;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
/// <inheritdoc />
public sealed class ServerClientFactory : IServerClientFactory
{
/// <summary>
/// The <see cref="IApiClientFactory"/> for the <see cref="ServerClientFactory"/>
/// </summary>
static readonly IApiClientFactory apiClientFactory = new ApiClientFactory();
/// <summary>
/// The <see cref="ProductHeaderValue"/> for the <see cref="ServerClientFactory"/>
/// </summary>
readonly ProductHeaderValue productHeaderValue;
/// <summary>
/// Construct a <see cref="ServerClientFactory"/>
/// </summary>
/// <param name="productHeaderValue">The value of <see cref="productHeaderValue"/></param>
public ServerClientFactory(ProductHeaderValue productHeaderValue)
{
this.productHeaderValue = productHeaderValue ?? throw new ArgumentNullException(nameof(productHeaderValue));
}
/// <inheritdoc />
public Task<IServerClient> CreateServerClient(string hostname, string username, string password, int timeout, CancellationToken cancellationToken) => throw new NotImplementedException();
public async Task<IServerClient> CreateServerClient(Uri host, string username, string password, TimeSpan timeout, CancellationToken cancellationToken)
{
if (host == null)
throw new ArgumentNullException(nameof(host));
if (username == null)
throw new ArgumentNullException(nameof(username));
if (password == null)
throw new ArgumentNullException(nameof(password));
if (timeout == null)
throw new ArgumentNullException(nameof(timeout));
Token token;
using (var api = apiClientFactory.CreateApiClient(host, new ApiHeaders(productHeaderValue, username, password)))
{
if (timeout != default)
api.Timeout = timeout;
token = await api.Update<Token>(Routes.Root, cancellationToken).ConfigureAwait(false);
}
return CreateServerClient(host, token, timeout);
}
/// <inheritdoc />
public Task<IServerClient> CreateServerClient(string hostname, string token, int timeout, CancellationToken cancellationToken) => throw new NotImplementedException();
public IServerClient CreateServerClient(Uri host, Token token, TimeSpan timeout)
{
if (host == null)
throw new ArgumentNullException(nameof(host));
if (token == null)
throw new ArgumentNullException(nameof(token));
var result = new ServerClient(apiClientFactory.CreateApiClient(host, new ApiHeaders(productHeaderValue, token.Bearer)), token);
if (timeout != default)
result.Timeout = timeout;
return result;
}
}
}
@@ -0,0 +1,37 @@
using System;
using System.Net;
namespace Tgstation.Server.Client
{
/// <summary>
/// Occurs when an error occurs in the server
/// </summary>
public sealed class ServerErrorException : ClientException
{
/// <summary>
/// The raw HTML of the error
/// </summary>
public string Html { get; }
/// <summary>
/// Construct an <see cref="ServerErrorException"/>
/// </summary>
public ServerErrorException() { }
/// <summary>
/// Construct an <see cref="ServerErrorException"/> with <paramref name="html"/>
/// </summary>
/// <param name="html">The raw HTML response of the <see cref="ServerErrorException"/></param>
public ServerErrorException(string html) : base(null, HttpStatusCode.InternalServerError)
{
Html = html;
}
/// <summary>
/// Construct an <see cref="ServerErrorException"/> with a <paramref name="message"/> and <paramref name="innerException"/>
/// </summary>
/// <param name="message">The message for the <see cref="Exception"/></param>
/// <param name="innerException">The inner <see cref="Exception"/> for the base <see cref="Exception"/></param>
public ServerErrorException(string message, Exception innerException) : base(message, innerException) { }
}
}
@@ -30,6 +30,10 @@
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Tgstation.Server.Api\Tgstation.Server.Api.csproj" />
</ItemGroup>
@@ -0,0 +1,35 @@
using System;
using System.Net;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
/// <summary>
/// Occurs when the client provides invalid credentials
/// </summary>
public sealed class UnauthorizedException : ClientException
{
/// <summary>
/// Construct an <see cref="UnauthorizedException"/>
/// </summary>
public UnauthorizedException() : base(new ErrorMessage
{
Message = "Invalid credentials!",
SeverApiVersion = null
}, HttpStatusCode.Unauthorized)
{ }
/// <summary>
/// Construct an <see cref="UnauthorizedException"/> with a <paramref name="message"/>
/// </summary>
/// <param name="message">The message for the <see cref="Exception"/></param>
public UnauthorizedException(string message) : base(message) { }
/// <summary>
/// Construct an <see cref="UnauthorizedException"/> with a <paramref name="message"/> and <paramref name="innerException"/>
/// </summary>
/// <param name="message">The message for the <see cref="Exception"/></param>
/// <param name="innerException">The inner <see cref="Exception"/> for the base <see cref="Exception"/></param>
public UnauthorizedException(string message, Exception innerException) : base(message, innerException) { }
}
}
@@ -0,0 +1,35 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
/// <inheritdoc />
sealed class UsersClient : IUsersClient
{
/// <summary>
/// The <see cref="apiClient"/> for the <see cref="UsersClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// Construct an <see cref="UsersClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
public UsersClient(IApiClient apiClient)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
}
/// <inheritdoc />
public Task<User> Create(UserUpdate user, CancellationToken cancellationToken) => apiClient.Create<UserUpdate, User>(Routes.User, user, cancellationToken);
/// <inheritdoc />
public Task<User> Read(CancellationToken cancellationToken) => apiClient.Read<User>(Routes.User, cancellationToken);
/// <inheritdoc />
public Task<User> Update(UserUpdate user, CancellationToken cancellationToken) => apiClient.Update<UserUpdate, User>(Routes.User, user, cancellationToken);
}
}
@@ -44,7 +44,7 @@ namespace Tgstation.Server.Host.Components.Chat
readonly Dictionary<string, ICommand> builtinCommands;
/// <summary>
/// Map of <see cref="IProvider"/>s in use, keyed by <see cref="ChatSettings.Id"/>
/// Map of <see cref="IProvider"/>s in use, keyed by <see cref="ChatBot.Id"/>
/// </summary>
readonly Dictionary<long, IProvider> providers;
@@ -64,9 +64,9 @@ namespace Tgstation.Server.Host.Components.Chat
readonly CancellationTokenSource handlerCts;
/// <summary>
/// The initial <see cref="Models.ChatSettings"/> for the <see cref="Chat"/>
/// The initial <see cref="Models.ChatBot"/> for the <see cref="Chat"/>
/// </summary>
readonly List<Models.ChatSettings> initialChatSettings;
readonly List<Models.ChatBot> initialChatBots;
/// <summary>
/// The <see cref="ICustomCommandHandler"/> for the <see cref="ChangeChannels(long, IEnumerable{Api.Models.ChatChannel}, CancellationToken)"/>
@@ -79,7 +79,7 @@ namespace Tgstation.Server.Host.Components.Chat
Task chatHandler;
/// <summary>
/// The <see cref="TaskCompletionSource{TResult}"/> that completes when <see cref="ChatSettings"/> change
/// The <see cref="TaskCompletionSource{TResult}"/> that completes when <see cref="ChatBot"/>s change
/// </summary>
TaskCompletionSource<object> connectionsUpdated;
@@ -100,14 +100,14 @@ namespace Tgstation.Server.Host.Components.Chat
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
/// <param name="logger">The value of <see cref="logger"/></param>
/// <param name="commandFactory">The value of <see cref="commandFactory"/></param>
/// <param name="initialChatSettings">The <see cref="IEnumerable{T}"/> used to populate <see cref="initialChatSettings"/></param>
public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, ILogger<Chat> logger, IEnumerable<Models.ChatSettings> initialChatSettings)
/// <param name="initialChatBots">The <see cref="IEnumerable{T}"/> used to populate <see cref="initialChatBots"/></param>
public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, ILogger<Chat> logger, IEnumerable<Models.ChatBot> initialChatBots)
{
this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.initialChatSettings = initialChatSettings?.ToList() ?? throw new ArgumentNullException(nameof(initialChatSettings));
this.initialChatBots = initialChatBots?.ToList() ?? throw new ArgumentNullException(nameof(initialChatBots));
builtinCommands = new Dictionary<string, ICommand>();
providers = new Dictionary<long, IProvider>();
@@ -129,7 +129,7 @@ namespace Tgstation.Server.Host.Components.Chat
/// <summary>
/// Remove a <see cref="IProvider"/> from <see cref="providers"/> and <see cref="mappedChannels"/> optionally updating the <see cref="trackingContexts"/> as well
/// </summary>
/// <param name="connectionId">The <see cref="ChatSettings.Id"/> of the <see cref="IProvider"/> to delete</param>
/// <param name="connectionId">The <see cref="ChatBot.Id"/> of the <see cref="IProvider"/> to delete</param>
/// <param name="updateTrackings">If <see cref="trackingContexts"/> should be update</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IProvider"/> being removed if it exists, <see langword="false"/> otherwise</returns>
@@ -164,8 +164,6 @@ namespace Tgstation.Server.Host.Components.Chat
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
{
logger.LogTrace("Chat message: {0}. User (Note unconverted provider Id): {1}", message.Content, JsonConvert.SerializeObject(message.User));
//map the channel if it's private and we haven't seen it
if (message.User.Channel.IsPrivate)
lock (providers)
@@ -205,6 +203,8 @@ namespace Tgstation.Server.Host.Components.Chat
//no mention
return;
logger.LogTrace("Chat command: {0}. User (True provider Id): {1}", message.Content, JsonConvert.SerializeObject(message.User));
if (addressed)
splits.RemoveAt(0);
@@ -375,7 +375,7 @@ namespace Tgstation.Server.Host.Components.Chat
}
/// <inheritdoc />
public async Task ChangeSettings(ChatSettings newSettings, CancellationToken cancellationToken)
public async Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken)
{
if (newSettings == null)
throw new ArgumentNullException(nameof(newSettings));
@@ -478,9 +478,9 @@ namespace Tgstation.Server.Host.Components.Chat
{
foreach (var I in commandFactory.GenerateCommands())
builtinCommands.Add(I.Name.ToUpperInvariant(), I);
await Task.WhenAll(initialChatSettings.Select(x => ChangeSettings(x, cancellationToken))).ConfigureAwait(false);
await Task.WhenAll(initialChatBots.Select(x => ChangeSettings(x, cancellationToken))).ConfigureAwait(false);
await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(false);
await Task.WhenAll(initialChatSettings.Select(x => ChangeChannels(x.Id, x.Channels, cancellationToken))).ConfigureAwait(false);
await Task.WhenAll(initialChatBots.Select(x => ChangeChannels(x.Id, x.Channels, cancellationToken))).ConfigureAwait(false);
chatHandler = MonitorMessages(handlerCts.Token);
started = true;
}
@@ -45,6 +45,6 @@ namespace Tgstation.Server.Host.Components.Chat
}
/// <inheritdoc />
public IChat CreateChat(IEnumerable<Models.ChatSettings> initialChatSettings) => new Chat(providerFactory, ioManager, commandFactory, loggerFactory.CreateLogger<Chat>(), initialChatSettings);
public IChat CreateChat(IEnumerable<Models.ChatBot> initialChatBots) => new Chat(providerFactory, ioManager, commandFactory, loggerFactory.CreateLogger<Chat>(), initialChatBots);
}
}
@@ -13,9 +13,9 @@ namespace Tgstation.Server.Host.Components.Chat
public interface IChat : IHostedService, IDisposable
{
/// <summary>
/// If a given set of <see cref="ChatSettings"/> is connected
/// If a given set of <see cref="ChatBot"/> is connected
/// </summary>
/// <param name="connectionId">The <see cref="ChatSettings.Id"/> of the connection</param>
/// <param name="connectionId">The <see cref="ChatBot.Id"/> of the connection</param>
/// <returns><see langword="true"/> if it is connected, <see langword="false"/> otherwise</returns>
bool Connected(long connectionId);
@@ -26,17 +26,17 @@ namespace Tgstation.Server.Host.Components.Chat
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler);
/// <summary>
/// Change chat settings. If the <see cref="ChatSettings.Id"/> is not currently in use, a new connection will be made instead
/// Change chat settings. If the <see cref="ChatBot.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="newSettings">The new <see cref="ChatBot"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <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);
/// <returns>A <see cref="Task"/> representing the running operation. Will complete immediately if the <see cref="ChatBot.Enabled"/> property of <paramref name="newSettings"/> is <see langword="false"/></returns>
Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken);
/// <summary>
/// Disconnects and deletes a given connection
/// </summary>
/// <param name="connectionId">The <see cref="ChatSettings.Id"/> of the connection</param>
/// <param name="connectionId">The <see cref="ChatBot.Id"/> of the connection</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task DeleteConnection(long connectionId, CancellationToken cancellationToken);
@@ -44,7 +44,7 @@ namespace Tgstation.Server.Host.Components.Chat
/// <summary>
/// Change chat channels
/// </summary>
/// <param name="connectionId">The <see cref="ChatSettings.Id"/> of the connection</param>
/// <param name="connectionId">The <see cref="ChatBot.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>
@@ -10,8 +10,8 @@ namespace Tgstation.Server.Host.Components.Chat
/// <summary>
/// Create a <see cref="IChat"/>
/// </summary>
/// <param name="initialChatSettings">The initial <see cref="Models.ChatSettings"/> for the <see cref="IChat"/></param>
/// <param name="initialChatBots">The initial <see cref="Models.ChatBot"/> for the <see cref="IChat"/></param>
/// <returns>A new <see cref="IChat"/></returns>
IChat CreateChat(IEnumerable<Models.ChatSettings> initialChatSettings);
IChat CreateChat(IEnumerable<Models.ChatBot> initialChatBots);
}
}
@@ -11,8 +11,8 @@ namespace Tgstation.Server.Host.Components.Chat
/// <summary>
/// Create a <see cref="IProvider"/>
/// </summary>
/// <param name="settings">The <see cref="ChatSettings"/> for the new provider</param>
/// <param name="settings">The <see cref="ChatBot"/> containing settings for the new provider</param>
/// <returns>A new <see cref="IProvider"/></returns>
IProvider CreateProvider(ChatSettings settings);
IProvider CreateProvider(ChatBot settings);
}
}
@@ -33,7 +33,7 @@ namespace Tgstation.Server.Host.Components.Chat
}
/// <inheritdoc />
public IProvider CreateProvider(Api.Models.Internal.ChatSettings settings)
public IProvider CreateProvider(Api.Models.Internal.ChatBot settings)
{
if (settings == null)
throw new ArgumentNullException(nameof(settings));
@@ -80,7 +80,7 @@ namespace Tgstation.Server.Host.Components.Compiler
/// The <see cref="ILogger"/> for <see cref="DreamMaker"/>
/// </summary>
readonly ILogger<DreamMaker> logger;
/// <summary>
/// Construct <see cref="DreamMaker"/>
/// </summary>
@@ -115,15 +115,16 @@ namespace Tgstation.Server.Host.Components.Compiler
/// <param name="securityLevel">The <see cref="DreamDaemonSecurity"/> level to use to validate the API</param>
/// <param name="job">The <see cref="Models.CompileJob"/> for the operation</param>
/// <param name="byondLock">The current <see cref="IByondExecutableLock"/></param>
/// <param name="portToUse">The port to use for API validation</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the DMAPI was successfully validated, <see langword="false"/> otherwise</returns>
async Task<bool> VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, CancellationToken cancellationToken)
async Task<bool> VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken)
{
logger.LogTrace("Verifying DMAPI...");
logger.LogTrace("Verifying DMAPI...");
var launchParameters = new DreamDaemonLaunchParameters
{
AllowWebClient = false,
PrimaryPort = 0, //pick any port
PrimaryPort = portToUse,
SecurityLevel = securityLevel, //all it needs to read the file and exit
StartupTimeout = timeout
};
@@ -229,8 +230,14 @@ namespace Tgstation.Server.Host.Components.Compiler
}
/// <inheritdoc />
public async Task<Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, string projectName, DreamDaemonSecurity securityLevel, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken)
public async Task<Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, DreamMakerSettings dreamMakerSettings, DreamDaemonSecurity securityLevel, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken)
{
if (revisionInformation == null)
throw new ArgumentNullException(nameof(revisionInformation));
if (dreamMakerSettings == null)
throw new ArgumentNullException(nameof(dreamMakerSettings));
if (repository == null)
throw new ArgumentNullException(nameof(repository));
@@ -242,7 +249,7 @@ namespace Tgstation.Server.Host.Components.Compiler
var job = new Models.CompileJob
{
DirectoryName = Guid.NewGuid(),
DmeName = projectName,
DmeName = dreamMakerSettings.ProjectName,
RevisionInformation = revisionInformation
};
@@ -260,12 +267,15 @@ namespace Tgstation.Server.Host.Components.Compiler
Status = CompilerStatus.Copying;
}
var commitInsert = revisionInformation.CommitSha;
var remoteCommitInsert = String.Empty;
if (commitInsert == revisionInformation.OriginCommitSha)
commitInsert = String.Format(CultureInfo.InvariantCulture, "^{0}", commitInsert.Substring(0, 7));
var commitInsert = revisionInformation.CommitSha.Substring(0, 7);
string remoteCommitInsert;
if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
{
commitInsert = String.Format(CultureInfo.InvariantCulture, "^{0}", commitInsert);
remoteCommitInsert = String.Empty;
}
else
remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha);
remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7));
var testmergeInsert = revisionInformation.ActiveTestMerges.Count == 0 ? String.Empty : String.Format(CultureInfo.InvariantCulture, " (Test Merges: {0})",
String.Join(", ", revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x =>
@@ -286,11 +296,11 @@ namespace Tgstation.Server.Host.Components.Compiler
var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
var dirB = ioManager.ConcatPath(job.DirectoryName.ToString(), BDirectoryName);
async Task CleanupFailedCompile()
async Task CleanupFailedCompile(bool announce)
{
logger.LogTrace("Cleaning compile directory...");
Status = CompilerStatus.Cleanup;
var chatTask = chat.SendUpdateMessage("DM: Deploy failed!", cancellationToken);
var chatTask = announce ? chat.SendUpdateMessage("Deploy failed!", cancellationToken) : Task.CompletedTask;
try
{
await ioManager.DeleteDirectory(job.DirectoryName.ToString(), CancellationToken.None).ConfigureAwait(false);
@@ -314,7 +324,8 @@ namespace Tgstation.Server.Host.Components.Compiler
Status = CompilerStatus.PreCompile;
await eventConsumer.HandleEvent(EventType.CompileStart, new List<string> { ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)), repoOrigin }, cancellationToken).ConfigureAwait(false);
var resolvedGameDirectory = ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName));
await eventConsumer.HandleEvent(EventType.CompileStart, new List<string> { resolvedGameDirectory, repoOrigin }, cancellationToken).ConfigureAwait(false);
Status = CompilerStatus.Modifying;
@@ -339,53 +350,51 @@ namespace Tgstation.Server.Host.Components.Compiler
Status = CompilerStatus.Compiling;
//run compiler, verify api
bool ddVerified;
job.ByondVersion = byondLock.Version.ToString();
await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false);
Status = CompilerStatus.Verifying;
if (job.ExitCode == 0)
{
Status = CompilerStatus.Verifying;
ddVerified = job.ExitCode == 0 && await VerifyApi(apiValidateTimeout, securityLevel, job, byondLock, cancellationToken).ConfigureAwait(false);
job.DMApiValidated = await VerifyApi(apiValidateTimeout, securityLevel, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false);
}
if (!ddVerified)
if (job.DMApiValidated != true)
{
//server never validated or compile failed
await CleanupFailedCompile().ConfigureAwait(false);
await eventConsumer.HandleEvent(EventType.CompileFailure, new List<string> { job.ExitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false);
await eventConsumer.HandleEvent(EventType.CompileFailure, new List<string> { resolvedGameDirectory, job.ExitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false);
throw new Exception(job.ExitCode == 0 ? "Validation of the TGS api failed!" : "DM exited with a non-zero code!");
}
else
{
job.DMApiValidated = true;
logger.LogTrace("Running post compile event...");
Status = CompilerStatus.PostCompile;
await eventConsumer.HandleEvent(EventType.CompileComplete, new List<string> { ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)) }, cancellationToken).ConfigureAwait(false);
logger.LogTrace("Running post compile event...");
Status = CompilerStatus.PostCompile;
await eventConsumer.HandleEvent(EventType.CompileComplete, new List<string> { ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)) }, cancellationToken).ConfigureAwait(false);
logger.LogTrace("Duplicating compiled game...");
Status = CompilerStatus.Duplicating;
logger.LogTrace("Duplicating compiled game...");
Status = CompilerStatus.Duplicating;
//duplicate the dmb et al
await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false);
//duplicate the dmb et al
await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false);
logger.LogTrace("Applying static game file symlinks...");
Status = CompilerStatus.Symlinking;
logger.LogTrace("Applying static game file symlinks...");
Status = CompilerStatus.Symlinking;
//symlink in the static data
var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken);
var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken);
//symlink in the static data
var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken);
var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken);
await Task.WhenAll(symATask, symBTask).ConfigureAwait(false);
await Task.WhenAll(symATask, symBTask).ConfigureAwait(false);
await chat.SendUpdateMessage("Deployment complete! Changes will be applied on next server reboot.", cancellationToken).ConfigureAwait(false);
await chat.SendUpdateMessage("Deployment complete! Changes will be applied on next server reboot.", cancellationToken).ConfigureAwait(false);
logger.LogDebug("Compile complete!");
}
logger.LogDebug("Compile complete!");
return job;
}
catch
catch (Exception e)
{
await CleanupFailedCompile().ConfigureAwait(false);
await CleanupFailedCompile(!(e is OperationCanceledException)).ConfigureAwait(false);
throw;
}
}
@@ -1,6 +1,7 @@
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Repository;
namespace Tgstation.Server.Host.Components.Compiler
@@ -19,12 +20,12 @@ namespace Tgstation.Server.Host.Components.Compiler
/// Starts a compile
/// </summary>
/// <param name="revisionInformation">The <see cref="Models.RevisionInformation"/> being compiled from the <paramref name="repository"/></param>
/// <param name="projectName">The optional name of the .dme to compile without the extension if not pre</param>
/// <param name="dreamMakerSettings">The <see cref="DreamMakerSettings"/> for the compile</param>
/// <param name="securityLevel">The <see cref="DreamDaemonSecurity"/> level allowed for API validation</param>
/// <param name="apiValidateTimeout">The time in seconds to wait while validating the API</param>
/// <param name="repository">The <see cref="IRepository"/> to copy from</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the partially populated <see cref="CompileJob"/> for the operation. In particular, note the <see cref="CompileJob.RevisionInformation"/> field will only have it's <see cref="Api.Models.Internal.RevisionInformation.CommitSha"/> field populated</returns>
Task<Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, string projectName, DreamDaemonSecurity securityLevel, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken);
/// <returns>A <see cref="Task{TResult}"/> resulting in the partially populated <see cref="Models.CompileJob"/> for the operation. In particular, note the <see cref="Models.CompileJob.RevisionInformation"/> field will only have it's <see cref="Api.Models.Internal.RevisionInformation.CommitSha"/> field populated</returns>
Task<Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, DreamMakerSettings dreamMakerSettings, DreamDaemonSecurity securityLevel, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken);
}
}
@@ -47,7 +47,7 @@
/// </summary>
CompileCancelled = 9,
/// <summary>
/// Parameters: "1" if compile succeeded and api validation failed, "0" otherwise
/// Parameters: Game directory path, "1" if compile succeeded and api validation failed, "0" otherwise
/// </summary>
CompileFailure = 10,
/// <summary>
@@ -133,8 +133,8 @@ namespace Tgstation.Server.Host.Components
StartupTimeout = x.StartupTimeout,
SecurityLevel = x.SecurityLevel
}).FirstAsync(cancellationToken);
var projectNameTask = instanceQuery.Select(x => x.DreamMakerSettings.ProjectName).FirstOrDefaultAsync(cancellationToken);
var repositorySettingsTask = instanceQuery.Select(x => x.RepositorySettings).FirstOrDefaultAsync(cancellationToken);
var dmSettingsTask = instanceQuery.Select(x => x.DreamMakerSettings).FirstAsync(cancellationToken);
var repositorySettingsTask = instanceQuery.Select(x => x.RepositorySettings).FirstAsync(cancellationToken);
using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
{
@@ -169,7 +169,7 @@ namespace Tgstation.Server.Host.Components
await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, shouldSyncTracked, cancellationToken).ConfigureAwait(false);
//finish other queries
var projectName = await projectNameTask.ConfigureAwait(false);
var dmSettings = await dmSettingsTask.ConfigureAwait(false);
var ddSettings = await ddSettingsTask.ConfigureAwait(false);
var revInfo = await revInfoTask.ConfigureAwait(false);
@@ -190,7 +190,7 @@ namespace Tgstation.Server.Host.Components
}
//finally start compile
job = await DreamMaker.Compile(revInfo, projectName, ddSettings.SecurityLevel.Value, ddSettings.StartupTimeout.Value, repo, cancellationToken).ConfigureAwait(false);
job = await DreamMaker.Compile(revInfo, dmSettings, ddSettings.SecurityLevel.Value, ddSettings.StartupTimeout.Value, repo, cancellationToken).ConfigureAwait(false);
}
db.CompileJobs.Add(job);
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
@@ -96,7 +97,10 @@ namespace Tgstation.Server.Host.Components.Interop
CommCommand command;
try
{
command = JsonConvert.DeserializeObject<CommCommand>(file);
command = new CommCommand
{
Parameters = JsonConvert.DeserializeObject<IReadOnlyDictionary<string, string>>(file)
};
}
catch (JsonSerializationException ex)
{
@@ -9,6 +9,11 @@ namespace Tgstation.Server.Host.Components.Repository
/// </summary>
public interface IRepositoryManager : IDisposable
{
/// <summary>
/// If a <see cref="CloneRepository(Uri, string, string, string, Action{int}, CancellationToken)"/> operation is in progress
/// </summary>
bool CloneInProgress { get; }
/// <summary>
/// Attempt to load the <see cref="IRepository"/> from the default location
/// </summary>
@@ -11,6 +11,9 @@ namespace Tgstation.Server.Host.Components.Repository
/// <inheritdoc />
sealed class RepositoryManager : IRepositoryManager
{
/// <inheritdoc />
public bool CloneInProgress { get; private set; }
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="RepositoryManager"/>
/// </summary>
@@ -51,56 +54,72 @@ namespace Tgstation.Server.Host.Components.Repository
/// <inheritdoc />
public async Task<IRepository> CloneRepository(Uri url, string initialBranch, string username, string password, Action<int> progressReporter, CancellationToken cancellationToken)
{
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
if (!await ioManager.DirectoryExists(".", cancellationToken).ConfigureAwait(false))
try
{
await Task.Factory.StartNew(() =>
{
string path = null;
try
{
path = LibGit2Sharp.Repository.Clone(url.ToString(), ioManager.ResolvePath("."), new CloneOptions
{
OnProgress = (a) => !cancellationToken.IsCancellationRequested,
OnTransferProgress = (a) =>
{
var percentage = 100 * (((float)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2));
progressReporter((int)percentage);
return !cancellationToken.IsCancellationRequested;
},
RecurseSubmodules = true,
OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
RepositoryOperationStarting = (a) => !cancellationToken.IsCancellationRequested,
BranchName = initialBranch,
CredentialsProvider = (a, b, c) => username != null ? (Credentials)new UsernamePasswordCredentials
{
Username = username,
Password = password
} : new DefaultCredentials()
});
}
catch (UserCancelledException) { }
cancellationToken.ThrowIfCancellationRequested();
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
}
catch
{
lock (this)
{
if (CloneInProgress)
throw new InvalidOperationException("The repository is already being cloned!");
CloneInProgress = true;
}
try
{
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
if (!await ioManager.DirectoryExists(".", cancellationToken).ConfigureAwait(false))
try
{
await ioManager.DeleteDirectory(".", default).ConfigureAwait(false);
await Task.Factory.StartNew(() =>
{
string path = null;
try
{
path = LibGit2Sharp.Repository.Clone(url.ToString(), ioManager.ResolvePath("."), new CloneOptions
{
OnProgress = (a) => !cancellationToken.IsCancellationRequested,
OnTransferProgress = (a) =>
{
var percentage = 100 * (((float)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2));
progressReporter((int)percentage);
return !cancellationToken.IsCancellationRequested;
},
RecurseSubmodules = true,
OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
RepositoryOperationStarting = (a) => !cancellationToken.IsCancellationRequested,
BranchName = initialBranch,
CredentialsProvider = (a, b, c) => username != null ? (Credentials)new UsernamePasswordCredentials
{
Username = username,
Password = password
} : new DefaultCredentials()
});
}
catch (UserCancelledException) { }
cancellationToken.ThrowIfCancellationRequested();
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
}
catch { }
throw;
}
else
return null;
catch
{
try
{
await ioManager.DeleteDirectory(".", default).ConfigureAwait(false);
}
catch { }
throw;
}
else
return null;
}
finally
{
CloneInProgress = false;
}
return await LoadRepository(cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<IRepository> LoadRepository(CancellationToken cancellationToken)
{
lock(this)
if (CloneInProgress)
throw new InvalidOperationException("The repository is being cloned!");
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
LibGit2Sharp.Repository repo = null;
await Task.Factory.StartNew(() =>
@@ -271,7 +271,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
if (portClosed)
{
reattachInformation.Port = nextPort;
portAssignmentTcs.SetResult(true);
portAssignmentTcs.TrySetResult(true);
portAssignmentTcs = null;
portClosed = false;
}
@@ -379,9 +379,12 @@ namespace Tgstation.Server.Host.Components.Watchdog
lock (this)
{
if (portAssignmentTcs != null)
{
//someone was trying to change the port before us, ignore them
//shouldn't happen anyway, add logging here
portAssignmentTcs.SetResult(false);
logger.LogWarning("Hey uhhh, this shouldn't happen ok? Pls to tell cyberboss. SessionController.SetPort");
portAssignmentTcs.TrySetResult(false);
}
nextPort = port;
portAssignmentTcs = new TaskCompletionSource<bool>();
toWait = portAssignmentTcs.Task;
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Octokit;
using System;
using System.Collections.Generic;
@@ -9,6 +10,7 @@ using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Configuration;
@@ -22,7 +24,7 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// <see cref="ModelController{TModel}"/> for <see cref="Administration"/>
/// </summary>
[Route(Api.Routes.Administration.Base)]
[Route(Routes.Administration)]
public sealed class AdministrationController : ModelController<Administration>
{
/// <summary>
@@ -79,7 +81,7 @@ namespace Tgstation.Server.Host.Controllers
{
Logger.LogWarning("Exceeded GitHub rate limit!");
var secondsString = Math.Ceiling((exception.Reset - DateTimeOffset.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture);
Response.Headers.Add("Retry-After", new Microsoft.Extensions.Primitives.StringValues { });
Response.Headers.Add("Retry-After", new StringValues(secondsString));
return StatusCode(RateLimitHttpStatusCode);
}
@@ -103,7 +103,7 @@ namespace Tgstation.Server.Host.Controllers
//if there's no instance user, do a weird thing and add all the instance roles
//we need it so we can get to OnActionExecutionAsync where we can properly decide between BadRequest and Forbid
//if user is null that means they got the token with an expired password
var rightInt = authenticationContext.User == null || (RightsHelper.IsInstanceRight(I) && authenticationContext.InstanceUser == null) ? ~0 : authenticationContext.GetRight(I);
var rightInt = authenticationContext.User == null || (RightsHelper.IsInstanceRight(I) && authenticationContext.InstanceUser == null) ? ~0U : authenticationContext.GetRight(I);
var rightEnum = RightsHelper.RightToType(I);
var right = (Enum)Enum.ToObject(rightEnum, rightInt);
foreach (Enum J in Enum.GetValues(rightEnum))
@@ -5,6 +5,7 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Components;
@@ -17,7 +18,7 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// Controller for managing <see cref="Api.Models.Byond.Version"/>s
/// </summary>
[Route("/" + nameof(Byond))]
[Route(Routes.Byond)]
public sealed class ByondController : ModelController<Api.Models.Byond>
{
/// <summary>
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Components;
@@ -19,10 +20,10 @@ using Z.EntityFramework.Plus;
namespace Tgstation.Server.Host.Controllers
{
/// <summary>
/// <see cref="ModelController{TModel}"/> for managing <see cref="Api.Models.ChatSettings"/>
/// <see cref="ModelController{TModel}"/> for managing <see cref="Api.Models.ChatBot"/>s
/// </summary>
[Route("/" + nameof(Components.Chat.Chat))]
public sealed class ChatController : ModelController<Api.Models.ChatSettings>
[Route(Routes.Chat)]
public sealed class ChatController : ModelController<Api.Models.ChatBot>
{
/// <summary>
/// The <see cref="IInstanceManager"/> for the <see cref="ChatController"/>
@@ -56,8 +57,8 @@ namespace Tgstation.Server.Host.Controllers
};
/// <inheritdoc />
[TgsAuthorize(ChatSettingsRights.Create)]
public override async Task<IActionResult> Create([FromBody] Api.Models.ChatSettings model, CancellationToken cancellationToken)
[TgsAuthorize(ChatBotRights.Create)]
public override async Task<IActionResult> Create([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
@@ -87,7 +88,7 @@ namespace Tgstation.Server.Host.Controllers
return BadRequest(new ErrorMessage { Message = "One or more of channels aren't formatted correctly for the given provider!" });
//try to update das db first
var dbModel = new Models.ChatSettings
var dbModel = new Models.ChatBot
{
Name = model.Name,
ConnectionString = model.ConnectionString,
@@ -97,7 +98,7 @@ namespace Tgstation.Server.Host.Controllers
Provider = model.Provider,
};
DatabaseContext.ChatSettings.Add(dbModel);
DatabaseContext.ChatBots.Add(dbModel);
try
{
@@ -122,7 +123,7 @@ namespace Tgstation.Server.Host.Controllers
catch
{
//undo the add
DatabaseContext.ChatSettings.Remove(dbModel);
DatabaseContext.ChatBots.Remove(dbModel);
await DatabaseContext.Save(default).ConfigureAwait(false);
throw;
}
@@ -135,24 +136,24 @@ namespace Tgstation.Server.Host.Controllers
}
/// <inheritdoc />
[TgsAuthorize(ChatSettingsRights.Delete)]
[TgsAuthorize(ChatBotRights.Delete)]
public override async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
{
var instance = instanceManager.GetInstance(Instance);
await Task.WhenAll(instance.Chat.DeleteConnection(id, cancellationToken), DatabaseContext.ChatSettings.Where(x => x.Id == id).DeleteAsync(cancellationToken)).ConfigureAwait(false);
await Task.WhenAll(instance.Chat.DeleteConnection(id, cancellationToken), DatabaseContext.ChatBots.Where(x => x.Id == id).DeleteAsync(cancellationToken)).ConfigureAwait(false);
return Ok();
}
/// <inheritdoc />
[TgsAuthorize(ChatSettingsRights.Read)]
[TgsAuthorize(ChatBotRights.Read)]
public override async Task<IActionResult> List(CancellationToken cancellationToken)
{
var query = DatabaseContext.ChatSettings.Where(x => x.InstanceId == Instance.Id).Include(x => x.Channels);
var query = DatabaseContext.ChatBots.Where(x => x.InstanceId == Instance.Id).Include(x => x.Channels);
var results = await query.ToListAsync(cancellationToken).ConfigureAwait(false);
var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatSettings) & (int)ChatSettingsRights.ReadConnectionString) != 0;
var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatBots) & (int)ChatBotRights.ReadConnectionString) != 0;
if (!connectionStrings)
foreach (var I in results)
@@ -162,16 +163,16 @@ namespace Tgstation.Server.Host.Controllers
}
/// <inheritdoc />
[TgsAuthorize(ChatSettingsRights.Read)]
[TgsAuthorize(ChatBotRights.Read)]
public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
{
var query = DatabaseContext.ChatSettings.Where(x => x.Id == id).Include(x => x.Channels);
var query = DatabaseContext.ChatBots.Where(x => x.Id == id).Include(x => x.Channels);
var results = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
if (results == default)
return NotFound();
var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatSettings) & (int)ChatSettingsRights.ReadConnectionString) != 0;
var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatBots) & (int)ChatBotRights.ReadConnectionString) != 0;
if (!connectionStrings)
results.ConnectionString = null;
@@ -180,24 +181,24 @@ namespace Tgstation.Server.Host.Controllers
}
/// <inheritdoc />
[TgsAuthorize(ChatSettingsRights.WriteChannels | ChatSettingsRights.WriteConnectionString | ChatSettingsRights.WriteEnabled | ChatSettingsRights.WriteName | ChatSettingsRights.WriteProvider)]
public override async Task<IActionResult> Update([FromBody] Api.Models.ChatSettings model, CancellationToken cancellationToken)
[TgsAuthorize(ChatBotRights.WriteChannels | ChatBotRights.WriteConnectionString | ChatBotRights.WriteEnabled | ChatBotRights.WriteName | ChatBotRights.WriteProvider)]
public override async Task<IActionResult> Update([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
var query = DatabaseContext.ChatSettings.Where(x => x.InstanceId == Instance.Id && x.Id == model.Id).Include(x => x.Channels);
var query = DatabaseContext.ChatBots.Where(x => x.InstanceId == Instance.Id && x.Id == model.Id).Include(x => x.Channels);
var current = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
if (current == default)
return StatusCode((int)HttpStatusCode.Gone);
var userRights = (ChatSettingsRights)AuthenticationContext.GetRight(RightsType.ChatSettings);
var userRights = (ChatBotRights)AuthenticationContext.GetRight(RightsType.ChatBots);
bool anySettingsModified = false;
bool CheckModified<T>(Expression<Func<Api.Models.Internal.ChatSettings, T>> expression, ChatSettingsRights requiredRight)
bool CheckModified<T>(Expression<Func<Api.Models.Internal.ChatBot, T>> expression, ChatBotRights requiredRight)
{
var memberSelectorExpression = (MemberExpression)expression.Body;
var property = (PropertyInfo)memberSelectorExpression.Member;
@@ -213,11 +214,11 @@ namespace Tgstation.Server.Host.Controllers
return false;
};
if (CheckModified(x => x.ConnectionString, ChatSettingsRights.WriteConnectionString)
|| CheckModified(x => x.Enabled, ChatSettingsRights.WriteEnabled)
|| CheckModified(x => x.Name, ChatSettingsRights.WriteName)
|| CheckModified(x => x.Provider, ChatSettingsRights.WriteProvider)
|| (model.Channels != null && !userRights.HasFlag(ChatSettingsRights.WriteChannels)))
if (CheckModified(x => x.ConnectionString, ChatBotRights.WriteConnectionString)
|| CheckModified(x => x.Enabled, ChatBotRights.WriteEnabled)
|| CheckModified(x => x.Name, ChatBotRights.WriteName)
|| CheckModified(x => x.Provider, ChatBotRights.WriteProvider)
|| (model.Channels != null && !userRights.HasFlag(ChatBotRights.WriteChannels)))
return Forbid();
if (model.Channels != null)
@@ -239,9 +240,9 @@ namespace Tgstation.Server.Host.Controllers
if (model.Channels != null || anySettingsModified)
await chat.ChangeChannels(current.Id, current.Channels, cancellationToken).ConfigureAwait(false);
if (userRights.HasFlag(ChatSettingsRights.Read))
if (userRights.HasFlag(ChatBotRights.Read))
{
if (!userRights.HasFlag(ChatSettingsRights.ReadConnectionString))
if (!userRights.HasFlag(ChatBotRights.ReadConnectionString))
current.ConnectionString = null;
return Json(current.ToApi());
}
@@ -4,6 +4,7 @@ using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Components;
@@ -15,7 +16,7 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// The <see cref="ModelController{TModel}"/> for <see cref="ConfigurationFile"/>s
/// </summary>
[Route("/" + nameof(Configuration))]
[Route(Routes.Configuration)]
public sealed class ConfigurationController : ModelController<ConfigurationFile>
{
/// <summary>
@@ -8,6 +8,7 @@ using System.Net;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Components;
@@ -21,7 +22,7 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// <see cref="ModelController{TModel}"/> for managing the <see cref="DreamDaemon"/>
/// </summary>
[Route("/" + nameof(DreamDaemon))]
[Route(Routes.DreamDaemon)]
public sealed class DreamDaemonController : ModelController<DreamDaemon>
{
/// <summary>
@@ -6,6 +6,7 @@ using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Components;
using Tgstation.Server.Host.Core;
@@ -17,7 +18,7 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// Controller for managing the compiler
/// </summary>
[Route("/" + nameof(Api.Models.DreamMaker))]
[Route(Routes.DreamMaker)]
public sealed class DreamMakerController : ModelController<Api.Models.DreamMaker>
{
/// <summary>
@@ -58,6 +59,29 @@ namespace Tgstation.Server.Host.Controllers
});
}
/// <inheritdoc />
[TgsAuthorize(DreamMakerRights.List)]
public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
{
var compileJob = await DatabaseContext.CompileJobs
.Where(x => x.Id == id && x.Job.Instance.Id == Instance.Id)
.Include(x => x.Job).ThenInclude(x => x.StartedBy)
.Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge)
.Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge)
.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
if (compileJob == default)
return NotFound();
return Json(compileJob.ToApi());
}
/// <inheritdoc />
[TgsAuthorize(DreamMakerRights.List)]
public override async Task<IActionResult> List(CancellationToken cancellationToken)
{
var compileJobs = await DatabaseContext.CompileJobs.Where(x => x.Job.Instance.Id == Instance.Id).ToListAsync(cancellationToken).ConfigureAwait(false);
return Json(compileJobs.Select(x => x.ToApi()));
}
/// <inheritdoc />
[TgsAuthorize(DreamMakerRights.Compile)]
public override async Task<IActionResult> Create([FromBody] Api.Models.DreamMaker model, CancellationToken cancellationToken)
@@ -75,15 +99,33 @@ namespace Tgstation.Server.Host.Controllers
}
/// <inheritdoc />
[TgsAuthorize(DreamMakerRights.SetDme)]
[TgsAuthorize(DreamMakerRights.SetDme | DreamMakerRights.SetApiValidationPort)]
public override async Task<IActionResult> Update([FromBody] Api.Models.DreamMaker model, CancellationToken cancellationToken)
{
var hostModel = new DreamMakerSettings
{
InstanceId = Instance.Id
};
DatabaseContext.DreamMakerSettings.Attach(hostModel);
hostModel.ProjectName = model.ProjectName;
if (model.ProjectName != null)
{
if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetDme))
return Forbid();
if (model.ProjectName.Length == 0)
hostModel.ProjectName = null;
else
hostModel.ProjectName = model.ProjectName;
}
if (model.ApiValidationPort.HasValue)
{
if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationPort))
return Forbid();
hostModel.ApiValidationPort = model.ApiValidationPort;
}
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
return await Read(cancellationToken).ConfigureAwait(false);
}
@@ -104,8 +146,8 @@ namespace Tgstation.Server.Host.Controllers
var ddSettingsTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == instanceModel.Id).Select(x => new DreamDaemonSettings{
StartupTimeout = x.StartupTimeout,
SecurityLevel = x.SecurityLevel
}).FirstOrDefaultAsync(cancellationToken);
var projectName = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == instanceModel.Id).Select(x => x.ProjectName).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
}).FirstAsync(cancellationToken);
var dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == instanceModel.Id).FirstAsync(cancellationToken).ConfigureAwait(false);
var ddSettings = await ddSettingsTask.ConfigureAwait(false);
var instance = instanceManager.GetInstance(instanceModel);
@@ -136,7 +178,7 @@ namespace Tgstation.Server.Host.Controllers
databaseContext.Instances.Attach(revInfo.Instance);
}
compileJob = await instance.DreamMaker.Compile(revInfo, projectName, ddSettings.SecurityLevel.Value, ddSettings.StartupTimeout.Value, repo, cancellationToken).ConfigureAwait(false);
compileJob = await instance.DreamMaker.Compile(revInfo, dreamMakerSettings, ddSettings.SecurityLevel.Value, ddSettings.StartupTimeout.Value, repo, cancellationToken).ConfigureAwait(false);
}
if (compileJob.DMApiValidated != true)
@@ -15,7 +15,7 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// Main <see cref="ApiController"/> for the <see cref="Application"/>
/// </summary>
[Route("/")]
[Route(Routes.Root)]
public sealed class HomeController : ApiController
{
/// <summary>
@@ -124,9 +124,9 @@ namespace Tgstation.Server.Host.Controllers
if (!user.Enabled.Value)
return Forbid();
var token = tokenFactory.CreateToken(user, out var expiry);
var token = tokenFactory.CreateToken(user);
if (identity != null)
identityCache.CacheSystemIdentity(user, identity, expiry.AddMinutes(1)); //expire the identity slightly after the auth token in case of lag
identityCache.CacheSystemIdentity(user, identity, token.ExpiresAt.Value.AddMinutes(1)); //expire the identity slightly after the auth token in case of lag
Logger.LogDebug("Successfully logged in user {0}!", user.Id);
@@ -12,6 +12,7 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Components;
@@ -25,7 +26,7 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// Controller for managing <see cref="Components.Instance"/>s
/// </summary>
[Route("/Instance")]
[Route(Routes.InstanceManager)]
public sealed class InstanceController : ModelController<Api.Models.Instance>
{
/// <summary>
@@ -80,13 +81,13 @@ namespace Tgstation.Server.Host.Controllers
Models.InstanceUser InstanceAdminUser() => new Models.InstanceUser
{
ByondRights = (ByondRights)~0,
ChatSettingsRights = (ChatSettingsRights)~0,
ConfigurationRights = (ConfigurationRights)~0,
DreamDaemonRights = (DreamDaemonRights)~0,
DreamMakerRights = (DreamMakerRights)~0,
RepositoryRights = (RepositoryRights)~0,
InstanceUserRights = (InstanceUserRights)~0,
ByondRights = (ByondRights)~0U,
ChatBotRights = (ChatBotRights)~0U,
ConfigurationRights = (ConfigurationRights)~0U,
DreamDaemonRights = (DreamDaemonRights)~0U,
DreamMakerRights = (DreamMakerRights)~0U,
RepositoryRights = (RepositoryRights)~0U,
InstanceUserRights = (InstanceUserRights)~0U,
UserId = AuthenticationContext.User.Id
};
@@ -122,7 +123,10 @@ namespace Tgstation.Server.Host.Controllers
SoftShutdown = false,
StartupTimeout = 20
},
DreamMakerSettings = new DreamMakerSettings(),
DreamMakerSettings = new DreamMakerSettings
{
ApiValidationPort = 1339
},
Name = model.Name,
Online = false,
Path = model.Path,
@@ -1,5 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
@@ -7,6 +6,7 @@ using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Models;
@@ -18,7 +18,7 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// For managing <see cref="User"/>s
/// </summary>
[Route("/" + nameof(Models.InstanceUser))]
[Route(Routes.InstanceUser)]
public sealed class InstanceUserController : ModelController<Api.Models.InstanceUser>
{
/// <summary>
@@ -57,7 +57,7 @@ namespace Tgstation.Server.Host.Controllers
var dbUser = new Models.InstanceUser
{
ByondRights = model.ByondRights ?? ByondRights.None,
ChatSettingsRights = model.ChatSettingsRights ?? ChatSettingsRights.None,
ChatBotRights = model.ChatBotRights ?? ChatBotRights.None,
ConfigurationRights = model.ConfigurationRights ?? ConfigurationRights.None,
DreamDaemonRights = model.DreamDaemonRights ?? DreamDaemonRights.None,
DreamMakerRights = model.DreamMakerRights ?? DreamMakerRights.None,
@@ -93,7 +93,7 @@ namespace Tgstation.Server.Host.Controllers
return StatusCode((int)HttpStatusCode.Gone);
originalUser.ByondRights = model.ByondRights ?? originalUser.ByondRights;
originalUser.ChatSettingsRights = model.ChatSettingsRights ?? originalUser.ChatSettingsRights;
originalUser.ChatBotRights = model.ChatBotRights ?? originalUser.ChatBotRights;
originalUser.ConfigurationRights = model.ConfigurationRights ?? originalUser.ConfigurationRights;
originalUser.DreamDaemonRights = model.DreamDaemonRights ?? originalUser.DreamDaemonRights;
originalUser.DreamMakerRights = model.DreamMakerRights ?? originalUser.DreamMakerRights;
@@ -6,6 +6,7 @@ using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
@@ -15,7 +16,7 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// <see cref="ModelController{TModel}"/> for <see cref="Api.Models.Job"/>s
/// </summary>
[Route("/" + nameof(Job))]
[Route(Routes.Jobs)]
public sealed class JobController : ModelController<Api.Models.Job>
{
/// <summary>
@@ -11,6 +11,7 @@ using System.Net;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Components;
@@ -23,7 +24,7 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// Controller for managing the <see cref="Repository"/>s
/// </summary>
[Route("/" + nameof(Repository))]
[Route(Routes.Repository)]
public sealed class RepositoryController : ModelController<Repository>
{
/// <summary>
@@ -63,7 +64,7 @@ namespace Tgstation.Server.Host.Controllers
IQueryable<Models.RevisionInformation> queryTarget = databaseContext.RevisionInformations;
var revisionInfo = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id)
var revisionInfo = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id)
.Include(x => x.CompileJobs)
.Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge) //minimal info, they can query the rest if they're allowed
.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); //search every rev info because LOL SHA COLLISIONS
@@ -138,6 +139,9 @@ namespace Tgstation.Server.Host.Controllers
var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
if (repoManager.CloneInProgress)
return Conflict();
using (var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false))
{
if (repo != null)
@@ -243,9 +247,12 @@ namespace Tgstation.Server.Host.Controllers
if (model.Origin != null)
return BadRequest(new ErrorMessage { Message = "origin cannot be modified without deleting the repository!" });
if(model.NewTestMerges.Any(x => !x.Number.HasValue))
if(model.NewTestMerges?.Any(x => !x.Number.HasValue) == true)
return BadRequest(new ErrorMessage { Message = "All new test merges must provide a number!" });
if(model.NewTestMerges?.Any(x => model.NewTestMerges.Any(y => x != y && x.Number == y.Number)) == true)
return BadRequest(new ErrorMessage { Message = "Cannot test merge the same PR twice in one job!" });
var newTestMerges = model.NewTestMerges != null && model.NewTestMerges.Count > 0;
var userRights = (RepositoryRights)AuthenticationContext.GetRight(RightsType.Repository);
if (newTestMerges && !userRights.HasFlag(RepositoryRights.MergePullRequest))
@@ -282,7 +289,7 @@ namespace Tgstation.Server.Host.Controllers
|| (model.UpdateFromOrigin == true && !userRights.HasFlag(RepositoryRights.UpdateBranch)))
return Forbid();
if (currentModel.AccessToken.Length == 0 && currentModel.AccessUser.Length == 0)
if (currentModel.AccessToken?.Length == 0 && currentModel.AccessUser?.Length == 0)
{
//setting an empty string clears everything
currentModel.AccessUser = null;
@@ -359,7 +366,10 @@ namespace Tgstation.Server.Host.Controllers
throw new InvalidOperationException("Merge conflict occurred during origin update!");
await UpdateRevInfo().ConfigureAwait(false);
if (fastForward.Value)
{
lastRevisionInfo.OriginCommitSha = repo.Head;
await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, true, ct).ConfigureAwait(false);
}
}
}
@@ -385,50 +395,94 @@ namespace Tgstation.Server.Host.Controllers
lastRevisionInfo.OriginCommitSha = repo.Head;
}
}
Dictionary<int, Octokit.PullRequest> prMap = null;
//test merging
if (newTestMerges)
{
//optimization: if we've already merged these exact same commits in this fashion before, just find the rev info for it and check it out
Models.RevisionInformation revInfoWereLookingFor = null;
if(lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha)
{
foreach (var I in model.NewTestMerges)
//normalize the shas to lowercase ala libgit2
#pragma warning disable CA1308 // Normalize strings to uppercase
I.PullRequestRevision = I.PullRequestRevision?.ToLowerInvariant();
#pragma warning restore CA1308 // Normalize strings to uppercase
//bit of sanitization
foreach (var I in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.PullRequestRevision)))
I.PullRequestRevision = null;
revInfoWereLookingFor = await databaseContext.RevisionInformations
.Where(x => x.OriginCommitSha == lastRevisionInfo.OriginCommitSha && x.ActiveTestMerges.Count == model.NewTestMerges.Count)
//split here cause this bit probably has to be done locally
.Where(x => x.ActiveTestMerges.Select(y => y.TestMerge)
.All(y => model.NewTestMerges.Any(z => y.Number == z.Number && y.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal))))
.Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge)
.FirstOrDefaultAsync(ct).ConfigureAwait(false);
var gitHubClient = currentModel.AccessToken != null ? gitHubClientFactory.CreateClient(currentModel.AccessToken) : gitHubClientFactory.CreateClient();
var repoOwner = repo.GitHubOwner;
var repoName = repo.GitHubRepoName;
Models.RevisionInformation revInfoWereLookingFor = null;
//optimization: if we've already merged these exact same commits in this fashion before, just find the rev info for it and check it out
if (lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha)
{
//In order for this to work though we need the shas of all the commits
if (model.NewTestMerges.Any(x => x.PullRequestRevision == null))
prMap = new Dictionary<int, Octokit.PullRequest>();
bool cantSearch = false;
foreach (var I in model.NewTestMerges)
{
if (I.PullRequestRevision != null)
//normalize the shas to lowercase ala libgit2
#pragma warning disable CA1308 // Normalize strings to uppercase
I.PullRequestRevision = I.PullRequestRevision?.ToLowerInvariant();
#pragma warning restore CA1308 // Normalize strings to uppercase
else
//retrieve the latest sha
try
{
var pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number.Value).ConfigureAwait(false);
prMap.Add(I.Number.Value, pr);
I.PullRequestRevision = pr.Head.Sha;
}
catch
{
cantSearch = true;
break;
}
}
if (!cantSearch)
{
var dbPull = await databaseContext.RevisionInformations
.Where(x => x.Instance.Id == Instance.Id
&& x.OriginCommitSha == lastRevisionInfo.OriginCommitSha
&& x.ActiveTestMerges.Count == model.NewTestMerges.Count)
.Include(x => x.ActiveTestMerges)
.ThenInclude(x => x.TestMerge)
.ToListAsync(cancellationToken).ConfigureAwait(false);
//split here cause this bit has to be done locally
revInfoWereLookingFor = dbPull
.Where(x => x.ActiveTestMerges.Select(y => y.TestMerge)
.All(y => model.NewTestMerges.Any(z =>
y.Number == z.Number
&& y.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal)
&& y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null)))
.FirstOrDefault();
}
}
if (revInfoWereLookingFor != null)
{
//goteem
await repo.ResetToSha(revInfoWereLookingFor.CommitSha, cancellationToken).ConfigureAwait(false);
lastRevisionInfo = revInfoWereLookingFor;
}
else
{
var gitHubClient = currentModel.AccessToken != null ? gitHubClientFactory.CreateClient(currentModel.AccessToken) : gitHubClientFactory.CreateClient();
var contextUser = new Models.User
{
Id = AuthenticationContext.User.Id
};
databaseContext.Users.Attach(contextUser);
var repoOwner = repo.GitHubOwner;
var repoName = repo.GitHubRepoName;
foreach (var I in model.NewTestMerges)
{
Octokit.PullRequest pr = null;
string errorMessage = null;
try
{
pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number.Value).ConfigureAwait(false);
//load from cache if possible
if (prMap == null || !prMap.TryGetValue(I.Number.Value, out pr))
pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number.Value).ConfigureAwait(false);
}
catch (Octokit.RateLimitExceededException)
{
@@ -477,9 +531,8 @@ namespace Tgstation.Server.Host.Controllers
}
}
}
//never synchronize with test merges
if (startSha != repo.Head && lastRevisionInfo.ActiveTestMerges.Count == 0)
if (startSha != repo.Head)
{
await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, false, ct).ConfigureAwait(false);
await UpdateRevInfo().ConfigureAwait(false);
@@ -52,10 +52,10 @@ namespace Tgstation.Server.Host.Controllers
public TgsAuthorizeAttribute(DreamDaemonRights requiredRights) => Roles = RightsHelper.RoleNames(requiredRights);
/// <summary>
/// Construct a <see cref="TgsAuthorizeAttribute"/> for <see cref="ChatSettingsRights"/>
/// Construct a <see cref="TgsAuthorizeAttribute"/> for <see cref="ChatBotRights"/>
/// </summary>
/// <param name="requiredRights">The rights required</param>
public TgsAuthorizeAttribute(ChatSettingsRights requiredRights) => Roles = RightsHelper.RoleNames(requiredRights);
public TgsAuthorizeAttribute(ChatBotRights requiredRights) => Roles = RightsHelper.RoleNames(requiredRights);
/// <summary>
/// Construct a <see cref="TgsAuthorizeAttribute"/> for <see cref="ConfigurationRights"/>
@@ -9,6 +9,7 @@ using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Rights;
using Tgstation.Server.Host.Configuration;
@@ -20,7 +21,7 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// For managing <see cref="User"/>s
/// </summary>
[Route("/" + nameof(Models.User))]
[Route(Routes.User)]
public sealed class UserController : ModelController<UserUpdate>
{
/// <summary>
@@ -1,12 +1,11 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class ChatSettings : Api.Models.Internal.ChatSettings, IApiConvertable<Api.Models.ChatSettings>
public sealed class ChatBot : Api.Models.Internal.ChatBot, IApiConvertable<Api.Models.ChatBot>
{
/// <summary>
/// The <see cref="Api.Models.Instance.Id"/>
@@ -20,12 +19,12 @@ namespace Tgstation.Server.Host.Models
public Instance Instance { get; set; }
/// <summary>
/// See <see cref="Api.Models.ChatSettings.Channels"/>
/// See <see cref="Api.Models.ChatBot.Channels"/>
/// </summary>
public List<ChatChannel> Channels { get; set; }
/// <inheritdoc />
public Api.Models.ChatSettings ToApi() => new Api.Models.ChatSettings
public Api.Models.ChatBot ToApi() => new Api.Models.ChatBot
{
Channels = Channels.Select(x => x.ToApi()).ToList(),
ConnectionString = ConnectionString,

Some files were not shown because too many files have changed in this diff Show More