mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-27 07:04:57 +01:00
Doc comments and model tweaks
This commit is contained in:
@@ -6,6 +6,18 @@ namespace Tgstation.Server.Api.Models
|
||||
/// <inheritdoc />
|
||||
public sealed class ChatSettings : Internal.ChatSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// If the IRC connection is established
|
||||
/// </summary>
|
||||
[Permissions(DenyWrite = true)]
|
||||
bool IrcConnected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the Discord connection is established
|
||||
/// </summary>
|
||||
[Permissions(DenyWrite = true)]
|
||||
bool DiscordConnected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Channels the Discord bot should listen/announce in
|
||||
/// </summary>
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
/// The <see cref="User"/> that triggered the job
|
||||
/// </summary>
|
||||
public User TriggeredBy { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="User"/> that cancelled the job if any
|
||||
/// </summary>
|
||||
public User CancelledBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Git revision the compiler ran on. Not modifiable
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Tgstation.Server.Api.Rights;
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
|
||||
namespace Tgstation.Server.Api.Models
|
||||
{
|
||||
@@ -6,35 +6,12 @@ namespace Tgstation.Server.Api.Models
|
||||
/// Represents a static game file. Create and delete actions uncerimonuously overwrite/delete files
|
||||
/// </summary>
|
||||
#pragma warning disable CA1724 // System.Configuration name conflict
|
||||
[Model(RightsType.Configuration, CanCrud = true, CanList = true, RequiresInstance = true, ReadRight = ConfigurationRights.Read, WriteRight = ConfigurationRights.Write)]
|
||||
public sealed class Configuration
|
||||
public sealed class Configuration : ConfigurationFileMetadata
|
||||
#pragma warning restore CA1724 // System.Configuration name conflict
|
||||
{
|
||||
/// <summary>
|
||||
/// The path to the <see cref="Configuration"/> file
|
||||
/// </summary>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If read access to the <see cref="Configuration"/> file was denied
|
||||
/// </summary>
|
||||
[Permissions(DenyWrite = true)]
|
||||
public bool ReadDenied { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="Path"/> represents a directory. Will only be <see langword="true"/> if <see cref="ReadDenied"/> is <see langword="true"/>
|
||||
/// </summary>
|
||||
[Permissions(DenyWrite = true)]
|
||||
public bool IsDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The content of the <see cref="Configuration"/> file. Will be <see langword="null"/> if <see cref="ReadDenied"/> is <see langword="true"/> or during listing operations
|
||||
/// </summary>
|
||||
public byte[] Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The MD5 hash of the file when last read by the user. Will be <see langword="null"/> if <see cref="ReadDenied"/> is <see langword="true"/>. If this doesn't match during update actions, the write will be denied with error code 409
|
||||
/// </summary>
|
||||
public string LastReadHash { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,18 @@ namespace Tgstation.Server.Api.Models.Internal
|
||||
/// <summary>
|
||||
/// When the compilation finished
|
||||
/// </summary>
|
||||
[Required]
|
||||
public DateTimeOffset FinishedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the compiler targeted the primary directory
|
||||
/// The detected DMAPI version
|
||||
/// </summary>
|
||||
public bool? TargetedPrimaryDirectory { get; set; }
|
||||
public Version DMApiVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The .dme file used for compilation
|
||||
/// </summary>
|
||||
public string DmePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Textual output of DM
|
||||
@@ -35,7 +41,7 @@ namespace Tgstation.Server.Api.Models.Internal
|
||||
public string Output { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Exit code of DM. If <see langword="null"/>, the job was cancelled
|
||||
/// Exit code of DM. If <see langword="null"/>
|
||||
/// </summary>
|
||||
public int? ExitCode { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using Tgstation.Server.Api.Rights;
|
||||
|
||||
namespace Tgstation.Server.Api.Models.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata about a <see cref="Configuration"/> file
|
||||
/// </summary>
|
||||
[Model(RightsType.Configuration, CanCrud = true, CanList = true, RequiresInstance = true, ReadRight = ConfigurationRights.Read, WriteRight = ConfigurationRights.Write)]
|
||||
public class ConfigurationFileMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// The path to the <see cref="Configuration"/> file
|
||||
/// </summary>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If read access to the <see cref="Configuration"/> file was denied
|
||||
/// </summary>
|
||||
[Permissions(DenyWrite = true)]
|
||||
public bool ReadDenied { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="Path"/> represents a directory. Will only be <see langword="true"/> if <see cref="ReadDenied"/> is <see langword="true"/>
|
||||
/// </summary>
|
||||
[Permissions(DenyWrite = true)]
|
||||
public bool IsDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The MD5 hash of the file when last read by the user. Will be <see langword="null"/> if <see cref="ReadDenied"/> is <see langword="true"/>. If this doesn't match during update actions, the write will be denied with error code 409
|
||||
/// </summary>
|
||||
public string LastReadHash { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Tgstation.Server.Api.Rights;
|
||||
|
||||
namespace Tgstation.Server.Api.Models.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Launch settings for DreamDaemon
|
||||
/// </summary>
|
||||
[Model(RightsType.DreamDaemon, CanCrud = true, RequiresInstance = true)]
|
||||
public class DreamDaemonLaunchParameters
|
||||
{
|
||||
/// <summary>
|
||||
/// If the BYOND web client can be used to connect to the game server
|
||||
/// </summary>
|
||||
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetWebClient)]
|
||||
public bool AllowWebClient { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DreamDaemonSecurity"/> level of <see cref="DreamDaemon"/>
|
||||
/// </summary>
|
||||
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetSecurity)]
|
||||
public DreamDaemonSecurity SecurityLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The first port <see cref="DreamDaemon"/> uses. This should be the publically advertised port
|
||||
/// </summary>
|
||||
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetPorts)]
|
||||
public ushort PrimaryPort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The second port <see cref="DreamDaemon"/> uses
|
||||
/// </summary>
|
||||
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetPorts)]
|
||||
public ushort SecondaryPort { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,7 @@ namespace Tgstation.Server.Api.Models.Internal
|
||||
/// <summary>
|
||||
/// Configurable settings for <see cref="DreamDaemon"/>
|
||||
/// </summary>
|
||||
[Model(RightsType.DreamDaemon, CanCrud = true, RequiresInstance = true)]
|
||||
public class DreamDaemonSettings
|
||||
public class DreamDaemonSettings : DreamDaemonLaunchParameters
|
||||
{
|
||||
/// <summary>
|
||||
/// If <see cref="DreamDaemon"/> starts when it's <see cref="Instance"/> starts
|
||||
@@ -14,12 +13,6 @@ namespace Tgstation.Server.Api.Models.Internal
|
||||
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetAutoStart)]
|
||||
public bool AutoStart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the BYOND web client can be used to connect to the game server
|
||||
/// </summary>
|
||||
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetWebClient)]
|
||||
public bool AllowWebClient { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the server is undergoing a soft reset. This may be automatically set by changes to other fields
|
||||
/// </summary>
|
||||
@@ -31,23 +24,5 @@ namespace Tgstation.Server.Api.Models.Internal
|
||||
/// </summary>
|
||||
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SoftShutdown)]
|
||||
public bool SoftShutdown { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DreamDaemonSecurity"/> level of <see cref="DreamDaemon"/>
|
||||
/// </summary>
|
||||
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetSecurity)]
|
||||
public DreamDaemonSecurity SecurityLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The first port <see cref="DreamDaemon"/> uses. This should be the publically advertised port
|
||||
/// </summary>
|
||||
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetPorts)]
|
||||
public ushort PrimaryPort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The second port <see cref="DreamDaemon"/> uses
|
||||
/// </summary>
|
||||
[Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetPorts)]
|
||||
public ushort SecondaryPort { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ namespace Tgstation.Server.Api.Models.Internal
|
||||
public int? AutoCompileInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The .dme file <see cref="DreamMakerSettings"/> tries to compile with
|
||||
/// The .dme file <see cref="DreamMakerSettings"/> tries to compile with without the extension
|
||||
/// </summary>
|
||||
[Permissions(WriteRight = DreamMakerRights.SetDme)]
|
||||
public string TargetDme { get; set; }
|
||||
public string ProjectName { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,35 @@
|
||||
namespace Tgstation.Server.Host.Components
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// For managing the BYOND installation
|
||||
/// </summary>
|
||||
interface IByond
|
||||
{
|
||||
/// <summary>
|
||||
/// Change the current BYOND version
|
||||
/// </summary>
|
||||
/// <param name="version">The new <see cref="Version"/></param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
Task ChangeVersion(Version version, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Get the currently installed BYOND version
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>The current BYOND version</returns>
|
||||
Task<Version> GetVersion(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Lock the current installation's location and run an <paramref name="operation"/>
|
||||
/// </summary>
|
||||
/// <param name="operation">A <see cref="Func{T, TResult}"/> taking the path to either dm.exe or dreamdaemon.exe and returning a <see cref="Task"/></param>
|
||||
/// <param name="stagedIfExists">Use the staged installation if possible</param>
|
||||
/// <param name="dreamDaemon">Pass the path of dreamdaemon.exe to <paramref name="operation"/> if <see langword="true"/> dm.exe otherwise</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running <paramref name="operation"/></returns>
|
||||
Task UseExecutable(Func<string, Task> operation, bool stagedIfExists, bool dreamDaemon);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,40 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
|
||||
namespace Tgstation.Server.Host.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// For managing connected chat services
|
||||
/// </summary>
|
||||
interface IChat : IHostedService
|
||||
{
|
||||
/// <summary>
|
||||
/// If the IRC client is connected
|
||||
/// </summary>
|
||||
bool IrcConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If the Discord client is connected
|
||||
/// </summary>
|
||||
bool DiscordConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Change chat settings
|
||||
/// </summary>
|
||||
/// <param name="newSettings">The new <see cref="ChatSettings"/></param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task ChangeSettings(ChatSettings newSettings, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Change chat channels
|
||||
/// </summary>
|
||||
/// <param name="newChannels">An <see cref="IEnumerable{T}"/> of the new list of <see cref="Api.Models.ChatChannel"/>s</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task ChangeChannels(IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,56 @@
|
||||
namespace Tgstation.Server.Host.Components
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
namespace Tgstation.Server.Host.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// For managing the Configuration directory
|
||||
/// </summary>
|
||||
interface IConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Copies all files in the CodeModifications directory to <paramref name="destination"/>
|
||||
/// </summary>
|
||||
/// <param name="destination">Path to the destination folder</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resultin in a <see cref="IReadOnlyList{T}"/> of #include lines for the .dm files copied</returns>
|
||||
Task<IReadOnlyList<string>> CopyDMFilesTo(string destination, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Symlinks all directories in the GameData directory to <paramref name="destination"/>
|
||||
/// </summary>
|
||||
/// <param name="destination">Path to the destination folder</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task SymlinkStaticFilesTo(string destination, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Get <see cref="ConfigurationFileMetadata"/> for all items in a given <paramref name="configurationRelativePath"/>
|
||||
/// </summary>
|
||||
/// <param name="configurationRelativePath">The relative path in the Configuration directory</param>
|
||||
/// <param name="systemIdentity">The <see cref="ISystemIdentity"/> for the operation. If <see langword="null"/>, the operation will be performed as the user of the <see cref="Core.Application"/></param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="ConfigurationFileMetadata"/> for the items in the directory</returns>
|
||||
Task<IReadOnlyList<ConfigurationFileMetadata>> ListDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="configurationRelativePath">The relative path in the Configuration directory</param>
|
||||
/// <param name="systemIdentity">The <see cref="ISystemIdentity"/> for the operation. If <see langword="null"/>, the operation will be performed as the user of the <see cref="Core.Application"/></param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="Api.Models.Configuration"/> of the file</returns>
|
||||
Task<Api.Models.Configuration> Read(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="configurationRelativePath">The relative path in the Configuration directory</param>
|
||||
/// <param name="data">The data to write</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation. Usage may result in partial writes</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the operation succeeded, <see langword="false"/> if it failed due to permission errors</returns>
|
||||
Task<bool> Write(string configurationRelativePath, ISystemIdentity systemIdentity, byte[] data, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,63 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
|
||||
namespace Tgstation.Server.Host.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// For managing DreamDaemon
|
||||
/// </summary>
|
||||
interface IDreamDaemon : IHostedService
|
||||
{
|
||||
/// <summary>
|
||||
/// If DreamDaemon is running
|
||||
/// </summary>
|
||||
bool Running { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The port DreamDaemon is currently running on
|
||||
/// </summary>
|
||||
ushort? CurrentPort { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The access token used for communication with the DMAPI
|
||||
/// </summary>
|
||||
string AccessToken { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Launch DreamDaemon
|
||||
/// </summary>
|
||||
/// <param name="launchParameters">The <see cref="DreamDaemonLaunchParameters"/> for the launch</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task Launch(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Changes the <see cref="DreamDaemonLaunchParameters"/> if currently <see cref="Running"/>. Triggers a graceful restart
|
||||
/// </summary>
|
||||
/// <param name="launchParameters">The new <see cref="DreamDaemonLaunchParameters"/></param>
|
||||
void ChangeSettings(DreamDaemonLaunchParameters launchParameters);
|
||||
|
||||
/// <summary>
|
||||
/// Restarts DreamDaemon
|
||||
/// </summary>
|
||||
/// <param name="graceful">If <see langword="true"/> the restart will be delayed until a reboot is detected in the DMAPI and this function will retrun immediately. If the DMAPI isn't installed, this parameter is ignored</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task Restart(bool graceful, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Terminates DreamDaemon
|
||||
/// </summary>
|
||||
/// <param name="graceful">If <see langword="true"/> the termination will be delayed until a reboot is detected in the DMAPI and this function will return immediately. If the DMAPI isn't installed, this parameter is ignored</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task Terminate(bool graceful, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Cancels any pending graceful reboots or terminations
|
||||
/// </summary>
|
||||
void CancelGracefulActions();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,21 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// For managing the compiler
|
||||
/// </summary>
|
||||
interface IDreamMaker : IHostedService
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts a compile
|
||||
/// </summary>
|
||||
/// <param name="dmePath">The .dme file to use</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</returns>
|
||||
Task<CompileJob> Compile(string dmePath, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,16 @@ namespace Tgstation.Server.Host.Models
|
||||
/// <summary>
|
||||
/// See <see cref="Api.Models.CompileJob.TriggeredBy"/>
|
||||
/// </summary>
|
||||
[Required]
|
||||
public User TriggeredBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="Api.Models.CompileJob.CancelledBy"/>
|
||||
/// </summary>
|
||||
public User CancelledBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="Api.Models.CompileJob.RevisionInformation"/>
|
||||
/// </summary>
|
||||
[Required]
|
||||
public RevisionInformation RevisionInformation { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user