diff --git a/.gitignore b/.gitignore
index b24d71e..26787d3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,6 +39,7 @@ Thumbs.db
*.app
*.exe
*.war
+*.vs
# Large media files
*.mp4
@@ -48,3 +49,6 @@ Thumbs.db
*.mov
*.wmv
+#Luke
+bin
+obj
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs
new file mode 100644
index 0000000..81d3dcd
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetGuestToken.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace AspShogiSockets.ServiceModels.Api.Messages
+{
+ public class GetGuestToken
+ {
+ public string ClientId { get; set; }
+ }
+
+ public class GetGuestTokenResponse
+ {
+ public string ClientId { get; }
+ public Guid OneTimeToken { get; }
+
+ public GetGuestTokenResponse(string clientId, Guid token)
+ {
+ ClientId = clientId;
+ OneTimeToken = token;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetToken.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetToken.cs
new file mode 100644
index 0000000..bfcc716
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/GetToken.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Websockets.ServiceModels
+{
+ public class GetTokenResponse
+ {
+ public Guid OneTimeToken { get; }
+
+ public GetTokenResponse(Guid token)
+ {
+ OneTimeToken = token;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostGameInvitation.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostGameInvitation.cs
new file mode 100644
index 0000000..6cec25a
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Api/Messages/PostGameInvitation.cs
@@ -0,0 +1,15 @@
+namespace AspShogiSockets.ServiceModels.Api.Messages
+{
+ public class PostGameInvitation
+ {
+ public string SessionName { get; set; }
+ }
+ public class PostGameInvitationResponse
+ {
+ public string Code { get; }
+ public PostGameInvitationResponse(string code)
+ {
+ Code = code;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj b/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj
new file mode 100644
index 0000000..cb63190
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Gameboard.ShogiUI.Sockets.ServiceModels.csproj
@@ -0,0 +1,7 @@
+
+
+
+ netcoreapp3.1
+
+
+
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs
new file mode 100644
index 0000000..dc174c7
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IRequest.cs
@@ -0,0 +1,9 @@
+using Websockets.ServiceModels.Types;
+
+namespace Websockets.ServiceModels.Interfaces
+{
+ public interface IRequest
+ {
+ ClientAction Action { get; set; }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IResponse.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IResponse.cs
new file mode 100644
index 0000000..3712e66
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Interfaces/IResponse.cs
@@ -0,0 +1,8 @@
+namespace Websockets.ServiceModels.Interfaces
+{
+ public interface IResponse
+ {
+ string Action { get; }
+ string Error { get; set; }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs
new file mode 100644
index 0000000..43603d7
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/CreateGame.cs
@@ -0,0 +1,25 @@
+using Websockets.ServiceModels.Interfaces;
+using Websockets.ServiceModels.Types;
+
+namespace Websockets.ServiceModels.Messages
+{
+ public class CreateGameRequest : IRequest
+ {
+ public ClientAction Action { get; set; }
+ public string GameName { get; set; }
+ public bool IsPrivate { get; set; }
+ }
+
+ public class CreateGameResponse : IResponse
+ {
+ public string Action { get; private set; }
+ public string Error { get; set; }
+ public Game Game { get; set; }
+ public string PlayerName { get; set; }
+
+ public CreateGameResponse(ClientAction action)
+ {
+ Action = action.ToString();
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ErrorResponse.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ErrorResponse.cs
new file mode 100644
index 0000000..9875612
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ErrorResponse.cs
@@ -0,0 +1,16 @@
+using Websockets.ServiceModels.Interfaces;
+using Websockets.ServiceModels.Types;
+
+namespace Websockets.ServiceModels.Messages
+{
+ public class ErrorResponse : IResponse
+ {
+ public string Action { get; private set; }
+ public string Error { get; set; }
+
+ public ErrorResponse(ClientAction action)
+ {
+ Action = action.ToString();
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinByCode.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinByCode.cs
new file mode 100644
index 0000000..b0d20b8
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinByCode.cs
@@ -0,0 +1,11 @@
+using Websockets.ServiceModels.Interfaces;
+using Websockets.ServiceModels.Types;
+
+namespace Websockets.ServiceModels.Messages
+{
+ public class JoinByCode : IRequest
+ {
+ public ClientAction Action { get; set; }
+ public string JoinCode { get; set; }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs
new file mode 100644
index 0000000..8a2cd08
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/JoinGame.cs
@@ -0,0 +1,24 @@
+using Websockets.ServiceModels.Interfaces;
+using Websockets.ServiceModels.Types;
+
+namespace Websockets.ServiceModels.Messages
+{
+ public class JoinGameRequest : IRequest
+ {
+ public ClientAction Action { get; set; }
+ public string GameName { get; set; }
+ }
+
+ public class JoinGameResponse : IResponse
+ {
+ public string Action { get; private set; }
+ public string Error { get; set; }
+ public string GameName { get; set; }
+ public string PlayerName { get; set; }
+
+ public JoinGameResponse(ClientAction action)
+ {
+ Action = action.ToString();
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs
new file mode 100644
index 0000000..ebb3691
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/ListGames.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+
+using Websockets.ServiceModels.Interfaces;
+using Websockets.ServiceModels.Types;
+
+namespace Websockets.ServiceModels.Messages
+{
+ public class ListGamesRequest : IRequest
+ {
+ public ClientAction Action { get; set; }
+ }
+
+ public class ListGamesResponse : IResponse
+ {
+ public string Action { get; private set; }
+ public string Error { get; set; }
+ public IEnumerable Games { get; set; }
+
+ public ListGamesResponse(ClientAction action)
+ {
+ Action = action.ToString();
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs
new file mode 100644
index 0000000..e9b1ee7
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/LoadGame.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using Websockets.ServiceModels.Interfaces;
+using Websockets.ServiceModels.Types;
+
+namespace Websockets.ServiceModels.Messages
+{
+ public class LoadGameRequest : IRequest
+ {
+ public ClientAction Action { get; set; }
+ public string GameName { get; set; }
+ }
+
+ public class LoadGameResponse : IResponse
+ {
+ public string Action { get; private set; }
+ public Game Game { get; set; }
+ public IEnumerable Moves { get; set; }
+ public string Error { get; set; }
+
+ public LoadGameResponse(ClientAction action)
+ {
+ Action = action.ToString();
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs
new file mode 100644
index 0000000..eb5d8eb
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Messages/Move.cs
@@ -0,0 +1,26 @@
+using Websockets.ServiceModels.Interfaces;
+using Websockets.ServiceModels.Types;
+
+namespace Websockets.ServiceModels.Messages
+{
+ public class MoveRequest : IRequest
+ {
+ public ClientAction Action { get; set; }
+ public string GameName { get; set; }
+ public Move Move { get; set; }
+ }
+
+ public class MoveResponse : IResponse
+ {
+ public string Action { get; }
+ public string Error { get; set; }
+ public string GameName { get; set; }
+ public Move Move { get; set; }
+ public string PlayerName { get; set; }
+
+ public MoveResponse(ClientAction action)
+ {
+ Action = action.ToString();
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs
new file mode 100644
index 0000000..f921e73
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/ClientActionEnum.cs
@@ -0,0 +1,13 @@
+namespace Websockets.ServiceModels.Types
+{
+ public enum ClientAction
+ {
+ ListGames,
+ CreateGame,
+ JoinGame,
+ JoinByCode,
+ LoadGame,
+ Move,
+ KeepAlive
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Coords.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Coords.cs
new file mode 100644
index 0000000..1588e7c
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Coords.cs
@@ -0,0 +1,8 @@
+namespace Websockets.ServiceModels.Types
+{
+ public class Coords
+ {
+ public int X { get; set; }
+ public int Y { get; set; }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs
new file mode 100644
index 0000000..4da5eac
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Game.cs
@@ -0,0 +1,8 @@
+namespace Websockets.ServiceModels.Types
+{
+ public class Game
+ {
+ public string GameName { get; set; }
+ public string[] Players { get; set; }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs
new file mode 100644
index 0000000..17693d4
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.ServiceModels/Socket/Types/Move.cs
@@ -0,0 +1,33 @@
+namespace Websockets.ServiceModels.Types
+{
+ public class Move
+ {
+ public string PieceFromCaptured { get; set; }
+ public Coords From { get; set; }
+ public Coords To { get; set; }
+ public bool IsPromotion { get; set; }
+
+ ///
+ /// Toggles perspective of this move. (ie from player 1 to player 2)
+ ///
+ public static Move ConvertPerspective(Move m)
+ {
+ var convertedMove = new Move
+ {
+ To = new Coords
+ {
+ X = 8 - m.To.X,
+ Y = 8 - m.To.Y
+ },
+ From = new Coords
+ {
+ X = 8 - m.From.X,
+ Y = 8 - m.From.Y
+ },
+ IsPromotion = m.IsPromotion,
+ PieceFromCaptured = m.PieceFromCaptured
+ };
+ return convertedMove;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets.sln b/Gameboard.ShogiUI.Sockets.sln
new file mode 100644
index 0000000..85cdd75
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30503.244
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets", "Gameboard.ShogiUI.Sockets\Gameboard.ShogiUI.Sockets.csproj", "{4FF35F9D-E525-46CF-A8A6-A147FE50AD68}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gameboard.ShogiUI.Sockets.ServiceModels", "Gameboard.ShogiUI.Sockets.ServiceModels\Gameboard.ShogiUI.Sockets.ServiceModels.csproj", "{FE775DE4-50F0-4C5D-AD2B-01320B1E7086}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {4FF35F9D-E525-46CF-A8A6-A147FE50AD68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4FF35F9D-E525-46CF-A8A6-A147FE50AD68}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4FF35F9D-E525-46CF-A8A6-A147FE50AD68}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4FF35F9D-E525-46CF-A8A6-A147FE50AD68}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FE775DE4-50F0-4C5D-AD2B-01320B1E7086}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {1D0B04F2-0DA1-4CB4-A82A-5A1C3B52ACEB}
+ EndGlobalSection
+EndGlobal
diff --git a/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs
new file mode 100644
index 0000000..414be02
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Controllers/GameController.cs
@@ -0,0 +1,30 @@
+using AspShogiSockets.ServiceModels.Api.Messages;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using System.Linq;
+using System.Threading.Tasks;
+using Websockets.Repositories;
+
+namespace AspShogiSockets.Controllers
+{
+ [Authorize]
+ [ApiController]
+ [Route("[controller]")]
+ public class GameController : ControllerBase
+ {
+ private readonly IGameboardRepository gameboardRepository;
+
+ public GameController(IGameboardRepository gameboardRepository)
+ {
+ this.gameboardRepository = gameboardRepository;
+ }
+
+ [Route("JoinCode")]
+ public async Task PostGameInvitation([FromBody] PostGameInvitation request)
+ {
+ var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value;
+ var code = (await gameboardRepository.PostJoinCode(request.SessionName, userName)).JoinCode;
+ return new CreatedResult("", new PostGameInvitationResponse(code));
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs
new file mode 100644
index 0000000..51058e6
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Controllers/SocketController.cs
@@ -0,0 +1,63 @@
+using AspShogiSockets.Repositories.RepositoryManagers;
+using AspShogiSockets.ServiceModels.Api.Messages;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Websockets.Managers;
+using Websockets.Repositories;
+using Websockets.ServiceModels;
+
+namespace Websockets.Controllers
+{
+ [Authorize]
+ [Route("[controller]")]
+ [ApiController]
+ public class SocketController : ControllerBase
+ {
+ private readonly ISocketTokenManager tokenManager;
+ private readonly IGameboardRepository gameboardRepository;
+ private readonly IGameboardRepositoryManager gameboardManager;
+
+ public SocketController(
+ ISocketTokenManager tokenManager,
+ IGameboardRepository gameboardRepository,
+ IGameboardRepositoryManager gameboardManager)
+ {
+ this.tokenManager = tokenManager;
+ this.gameboardRepository = gameboardRepository;
+ this.gameboardManager = gameboardManager;
+ }
+
+ [Route("Token")]
+ public IActionResult GetToken()
+ {
+ var userName = HttpContext.User.Claims.First(c => c.Type == "preferred_username").Value;
+ var token = tokenManager.GenerateToken(userName);
+ return new JsonResult(new GetTokenResponse(token));
+ }
+
+ [AllowAnonymous]
+ [Route("GuestToken")]
+ public async Task GetGuestToken([FromQuery] GetGuestToken request)
+ {
+ if (request.ClientId == null)
+ {
+ var clientId = await gameboardManager.CreateGuestUser();
+ var token = tokenManager.GenerateToken(clientId);
+ return new JsonResult(new GetGuestTokenResponse(clientId, token));
+ }
+ else
+ {
+ var response = await gameboardRepository.GetPlayer(request.ClientId);
+ if (response != null && response.Player != null)
+ {
+ var token = tokenManager.GenerateToken(response.Player.Name);
+ return new JsonResult(new GetGuestTokenResponse(response.Player.Name, token));
+ }
+ }
+ return new UnauthorizedResult();
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Extensions/LogMiddleware.cs b/Gameboard.ShogiUI.Sockets/Extensions/LogMiddleware.cs
new file mode 100644
index 0000000..3c39341
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Extensions/LogMiddleware.cs
@@ -0,0 +1,43 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using System.Threading.Tasks;
+
+namespace Gameboard.ShogiUI.Sockets.Extensions
+{
+ public class LogMiddleware
+ {
+ private readonly RequestDelegate next;
+ private readonly ILogger logger;
+
+ public LogMiddleware(RequestDelegate next, ILoggerFactory factory)
+ {
+ this.next = next;
+ logger = factory.CreateLogger();
+ }
+
+ public async Task Invoke(HttpContext context)
+ {
+ try
+ {
+ await next(context);
+ }
+ finally
+ {
+ logger.LogInformation("Request {method} {url} => {statusCode}",
+ context.Request?.Method,
+ context.Request?.Path.Value,
+ context.Response?.StatusCode);
+ }
+ }
+ }
+
+ public static class IApplicationBuilderExtensions
+ {
+ public static IApplicationBuilder UseRequestResponseLogging(this IApplicationBuilder builder)
+ {
+ builder.UseMiddleware();
+ return builder;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Extensions/WebSocketExtensions.cs b/Gameboard.ShogiUI.Sockets/Extensions/WebSocketExtensions.cs
new file mode 100644
index 0000000..a97f75b
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Extensions/WebSocketExtensions.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Net.WebSockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AspShogiSockets.Extensions
+{
+ public static class WebSocketExtensions
+ {
+ public static async Task SendTextAsync(this WebSocket self, string message)
+ {
+ await self.SendAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, CancellationToken.None);
+ }
+
+ public static async Task ReceiveTextAsync(this WebSocket self)
+ {
+ var buffer = new ArraySegment(new byte[2048]);
+ var receive = await self.ReceiveAsync(buffer, CancellationToken.None);
+ return Encoding.UTF8.GetString(buffer.Slice(0, receive.Count));
+ // TODO: Make this robust to multi-frame messages.
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj
new file mode 100644
index 0000000..e12061c
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj
@@ -0,0 +1,26 @@
+
+
+
+ netcoreapp3.1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj.user b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj.user
new file mode 100644
index 0000000..d757c8c
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Gameboard.ShogiUI.Sockets.csproj.user
@@ -0,0 +1,11 @@
+
+
+
+ ApiControllerEmptyScaffolder
+ root/Controller
+ AspShogiSockets
+
+
+ ProjectDebugger
+
+
\ No newline at end of file
diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs
new file mode 100644
index 0000000..f8e5fc0
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/CreateGameHandler.cs
@@ -0,0 +1,67 @@
+using AspShogiSockets.Extensions;
+using Gameboard.Shogi.Api.ServiceModels.Messages;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+using Websockets.Repositories;
+using Websockets.ServiceModels.Messages;
+using Websockets.ServiceModels.Types;
+
+namespace Websockets.Managers.ClientActionHandlers
+{
+ public class CreateGameHandler : IActionHandler
+ {
+ private readonly ILogger logger;
+ private readonly IGameboardRepository repository;
+ private readonly ISocketCommunicationManager communicationManager;
+
+ public CreateGameHandler(
+ ILogger logger,
+ ISocketCommunicationManager communicationManager,
+ IGameboardRepository repository)
+ {
+ this.logger = logger;
+ this.repository = repository;
+ this.communicationManager = communicationManager;
+ }
+
+ public async Task Handle(WebSocket socket, string json, string userName)
+ {
+ logger.LogInformation("Socket Request \n{0}\n", new[] { json });
+ var request = JsonConvert.DeserializeObject(json);
+ var postGameResponse = await repository.PostGame(new PostGame
+ {
+ GameName = request.GameName,
+ PlayerName = userName, // TODO : Investigate if needed by UI
+ IsPrivate = request.IsPrivate
+ });
+
+ var response = new CreateGameResponse(request.Action)
+ {
+ PlayerName = userName,
+ Game = new Game
+ {
+ GameName = postGameResponse.GameName,
+ Players = new string[] { userName }
+ }
+ };
+
+ if (string.IsNullOrWhiteSpace(postGameResponse.GameName))
+ {
+ response.Error = "Game already exists.";
+ }
+
+ var serialized = JsonConvert.SerializeObject(response);
+ logger.LogInformation("Socket Response \n{0}\n", new[] { serialized });
+ if (request.IsPrivate)
+ {
+ await socket.SendTextAsync(serialized);
+ }
+ else
+ {
+ await communicationManager.BroadcastToAll(serialized);
+ }
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs
new file mode 100644
index 0000000..13cd3db
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/IActionHandler.cs
@@ -0,0 +1,16 @@
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+using Websockets.ServiceModels.Types;
+
+namespace Websockets.Managers.ClientActionHandlers
+{
+ public interface IActionHandler
+ {
+ ///
+ /// Responsible for parsing json and handling the request.
+ ///
+ Task Handle(WebSocket socket, string json, string userName);
+ }
+
+ public delegate IActionHandler ActionHandlerResolver(ClientAction action);
+}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs
new file mode 100644
index 0000000..56cedf1
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinByCodeHandler.cs
@@ -0,0 +1,75 @@
+using AspShogiSockets.Extensions;
+using Gameboard.Shogi.Api.ServiceModels.Messages;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+using Websockets.Repositories;
+using Websockets.ServiceModels.Messages;
+using Websockets.ServiceModels.Types;
+
+namespace Websockets.Managers.ClientActionHandlers
+{
+ public class JoinByCodeHandler : IActionHandler
+ {
+ private readonly ILogger logger;
+ private readonly IGameboardRepository repository;
+ private readonly ISocketCommunicationManager communicationManager;
+
+ public JoinByCodeHandler(
+ ILogger logger,
+ ISocketCommunicationManager communicationManager,
+ IGameboardRepository repository)
+ {
+ this.logger = logger;
+ this.repository = repository;
+ this.communicationManager = communicationManager;
+ }
+
+ public async Task Handle(WebSocket socket, string json, string userName)
+ {
+ logger.LogInformation("Socket Request \n{0}\n", new[] { json });
+ var request = JsonConvert.DeserializeObject(json);
+ var joinGameResponse = await repository.PostJoinByCode(new PostJoinByCode
+ {
+ PlayerName = userName,
+ JoinCode = request.JoinCode
+ });
+
+ if (joinGameResponse.JoinSucceeded)
+ {
+ var gameName = (await repository.GetGame(joinGameResponse.GameName)).GameName;
+
+ // Other members of the game see a regular JoinGame occur.
+ var response = new JoinGameResponse(ClientAction.JoinGame)
+ {
+ PlayerName = userName,
+ GameName = gameName
+ };
+ var serialized = JsonConvert.SerializeObject(response);
+ await communicationManager.BroadcastToGame(gameName, serialized);
+ communicationManager.SubscribeToGame(socket, gameName, userName);
+
+ // But the player joining sees the JoinByCode occur.
+ response = new JoinGameResponse(ClientAction.JoinByCode)
+ {
+ PlayerName = userName,
+ GameName = gameName
+ };
+ serialized = JsonConvert.SerializeObject(response);
+ await socket.SendTextAsync(serialized);
+ }
+ else
+ {
+ var response = new JoinGameResponse(ClientAction.JoinByCode)
+ {
+ PlayerName = userName,
+ Error = "Error joining game."
+ };
+ var serialized = JsonConvert.SerializeObject(response);
+ logger.LogInformation("Socket Response \n{0}\n", new[] { serialized });
+ await socket.SendTextAsync(serialized);
+ }
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs
new file mode 100644
index 0000000..6fd209e
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/JoinGameHandler.cs
@@ -0,0 +1,54 @@
+using Gameboard.Shogi.Api.ServiceModels.Messages;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+using Websockets.Repositories;
+using Websockets.ServiceModels.Messages;
+using Websockets.ServiceModels.Types;
+
+namespace Websockets.Managers.ClientActionHandlers
+{
+ public class JoinGameHandler : IActionHandler
+ {
+ private readonly ILogger logger;
+ private readonly IGameboardRepository gameboardRepository;
+ private readonly ISocketCommunicationManager communicationManager;
+ public JoinGameHandler(
+ ILogger logger,
+ ISocketCommunicationManager communicationManager,
+ IGameboardRepository gameboardRepository)
+ {
+ this.logger = logger;
+ this.gameboardRepository = gameboardRepository;
+ this.communicationManager = communicationManager;
+ }
+
+ public async Task Handle(WebSocket socket, string json, string userName)
+ {
+ logger.LogInformation("Socket Request \n{0}\n", new[] { json });
+ var request = JsonConvert.DeserializeObject(json);
+ var response = new JoinGameResponse(ClientAction.JoinGame)
+ {
+ PlayerName = userName
+ };
+
+ var joinGameResponse = await gameboardRepository.PostJoinGame(request.GameName, new PostJoinGame
+ {
+ PlayerName = userName
+ });
+
+ if (joinGameResponse.JoinSucceeded)
+ {
+ response.GameName = request.GameName;
+ }
+ else
+ {
+ response.Error = "Game is full or code is incorrect.";
+ }
+ var serialized = JsonConvert.SerializeObject(response);
+ logger.LogInformation("Socket Response \n{0}\n", new[] { serialized });
+ await communicationManager.BroadcastToAll(serialized);
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs
new file mode 100644
index 0000000..3957910
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/ListGamesHandler.cs
@@ -0,0 +1,51 @@
+using AspShogiSockets.Extensions;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using System.Linq;
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+using Websockets.Repositories;
+using Websockets.ServiceModels.Messages;
+using Websockets.ServiceModels.Types;
+
+namespace Websockets.Managers.ClientActionHandlers
+{
+ public class ListGamesHandler : IActionHandler
+ {
+ private readonly ILogger logger;
+ private readonly IGameboardRepository repository;
+
+ public ListGamesHandler(
+ ILogger logger,
+ IGameboardRepository repository)
+ {
+ this.logger = logger;
+ this.repository = repository;
+ }
+
+ public async Task Handle(WebSocket socket, string json, string userName)
+ {
+ logger.LogInformation("Socket Request \n{0}\n", new[] { json });
+ var request = JsonConvert.DeserializeObject(json);
+ var getGamesResponse = string.IsNullOrWhiteSpace(userName)
+ ? await repository.GetGames()
+ : await repository.GetGames(userName);
+
+ var games = getGamesResponse.Games
+ .OrderBy(g => g.Players.Contains(userName))
+ .Select(g => new Game
+ {
+ GameName = g.GameName,
+ Players = g.Players
+ });
+ var response = new ListGamesResponse(ClientAction.ListGames)
+ {
+ Games = games ?? new Game[0]
+ };
+
+ var serialized = JsonConvert.SerializeObject(response);
+ logger.LogInformation("Socket Response \n{0}\n", new[] { serialized });
+ await socket.SendTextAsync(serialized);
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs
new file mode 100644
index 0000000..6eb674c
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/LoadGameHandler.cs
@@ -0,0 +1,63 @@
+using AspShogiSockets.Extensions;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using System.Linq;
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+using Websockets.Managers.Utility;
+using Websockets.Repositories;
+using Websockets.ServiceModels.Messages;
+using Websockets.ServiceModels.Types;
+
+namespace Websockets.Managers.ClientActionHandlers
+{
+ public class LoadGameHandler : IActionHandler
+ {
+ private readonly ILogger logger;
+ private readonly IGameboardRepository gameboardRepository;
+ private readonly ISocketCommunicationManager communicationManager;
+
+ public LoadGameHandler(
+ ILogger logger,
+ ISocketCommunicationManager communicationManager,
+ IGameboardRepository gameboardRepository)
+ {
+ this.logger = logger;
+ this.gameboardRepository = gameboardRepository;
+ this.communicationManager = communicationManager;
+ }
+
+ public async Task Handle(WebSocket socket, string json, string userName)
+ {
+ logger.LogInformation("Socket Request \n{0}\n", json);
+ var request = JsonConvert.DeserializeObject(json);
+ var response = new LoadGameResponse(ClientAction.LoadGame);
+ var getGameResponse = await gameboardRepository.GetGame(request.GameName);
+ var getMovesResponse = await gameboardRepository.GetMoves(request.GameName);
+
+ if (getGameResponse == null || getMovesResponse == null)
+ {
+ response.Error = $"Could not find game.";
+ }
+ else
+ {
+ var player1 = getGameResponse.Players[0];
+ response.Game = new Game
+ {
+ GameName = getGameResponse.GameName,
+ Players = getGameResponse.Players
+ };
+
+ response.Moves = userName.Equals(player1)
+ ? getMovesResponse.Moves.Select(_ => Mapper.Map(_))
+ : getMovesResponse.Moves.Select(_ => Move.ConvertPerspective(Mapper.Map(_)));
+
+ communicationManager.SubscribeToGame(socket, getGameResponse.GameName, userName);
+ }
+
+ var serialized = JsonConvert.SerializeObject(response);
+ logger.LogInformation("Socket Response \n{0}\n", serialized);
+ await socket.SendTextAsync(serialized);
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs
new file mode 100644
index 0000000..13759b4
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Managers/ClientActionHandlers/MoveHandler.cs
@@ -0,0 +1,77 @@
+using AspShogiSockets.Extensions;
+using Gameboard.Shogi.Api.ServiceModels.Messages;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+using Websockets.Managers.Utility;
+using Websockets.Repositories;
+using Websockets.ServiceModels.Messages;
+using Websockets.ServiceModels.Types;
+
+namespace Websockets.Managers.ClientActionHandlers
+{
+ public class MoveHandler : IActionHandler
+ {
+ private readonly ILogger logger;
+ private readonly IGameboardRepository gameboardRepository;
+ private readonly ISocketCommunicationManager communicationManager;
+ public MoveHandler(
+ ILogger logger,
+ ISocketCommunicationManager communicationManager,
+ IGameboardRepository gameboardRepository)
+ {
+ this.logger = logger;
+ this.gameboardRepository = gameboardRepository;
+ this.communicationManager = communicationManager;
+ }
+
+ public async Task Handle(WebSocket socket, string json, string userName)
+ {
+ logger.LogInformation("Socket Request \n{0}\n", new[] { json });
+ var request = JsonConvert.DeserializeObject(json);
+ // Basic move validation
+ var move = request.Move;
+ if (move.To.Equals(move.From))
+ {
+ var serialized = JsonConvert.SerializeObject(
+ new ErrorResponse(ClientAction.Move)
+ {
+ Error = "Error: moving piece from tile to the same tile."
+ });
+ await socket.SendTextAsync(serialized);
+ return;
+ }
+
+ var getGameResponse = await gameboardRepository.GetGame(request.GameName);
+ var isPlayer1 = userName.Equals(getGameResponse.Players[0]);
+ if (!isPlayer1)
+ {
+ // Convert the move coords to player1 perspective.
+ move = Move.ConvertPerspective(move);
+ }
+
+ await gameboardRepository.PostMove(
+ request.GameName,
+ new PostMove { Move = Mapper.Map(move) });
+
+ var response = new MoveResponse(ClientAction.Move)
+ {
+ GameName = request.GameName,
+ PlayerName = userName
+ };
+ await communicationManager.BroadcastToGame(
+ request.GameName,
+ (playerName, sslStream) =>
+ {
+ response.Move = playerName.Equals(userName)
+ ? request.Move
+ : Move.ConvertPerspective(request.Move);
+ var serialized = JsonConvert.SerializeObject(response);
+ logger.LogInformation("Socket Response \n{0}\n", new[] { serialized });
+ return serialized;
+ }
+ );
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs
new file mode 100644
index 0000000..88a13f2
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Managers/SocketCommunicationManager.cs
@@ -0,0 +1,166 @@
+using AspShogiSockets.Extensions;
+using AspShogiSockets.Managers.Utility;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+using Websockets.Managers.ClientActionHandlers;
+using Websockets.ServiceModels.Types;
+
+namespace Websockets.Managers
+{
+ public interface ISocketCommunicationManager
+ {
+ Task CommunicateWith(WebSocket w, string s);
+ Task BroadcastToAll(string msg);
+ Task BroadcastToGame(string gameName, Func msgBuilder);
+ Task BroadcastToGame(string gameName, string msg);
+ void SubscribeToGame(WebSocket socket, string gameName, string playerName);
+ void SubscribeToBroadcast(WebSocket socket, string playerName);
+ void UnsubscribeFromBroadcastAndGames(string playerName);
+ void UnsubscribeFromGame(string gameName, string playerName);
+ }
+
+ public class SocketCommunicationManager : ISocketCommunicationManager
+ {
+ private readonly ConcurrentDictionary connections;
+ private readonly ConcurrentDictionary> gameSeats;
+ private readonly ILogger logger;
+ private readonly ActionHandlerResolver handlerResolver;
+
+ public SocketCommunicationManager(
+ ILogger logger,
+ ActionHandlerResolver handlerResolver)
+ {
+ this.logger = logger;
+ this.handlerResolver = handlerResolver;
+ connections = new ConcurrentDictionary();
+ gameSeats = new ConcurrentDictionary>();
+ }
+
+ public async Task CommunicateWith(WebSocket socket, string userName)
+ {
+ SubscribeToBroadcast(socket, userName);
+
+ while (!socket.CloseStatus.HasValue)
+ {
+ try
+ {
+ var message = await socket.ReceiveTextAsync();
+ if (string.IsNullOrWhiteSpace(message)) continue;
+
+ var request = JsonConvert.DeserializeObject(message);
+ if (!Enum.IsDefined(typeof(ClientAction), request.Action))
+ {
+ await socket.SendTextAsync("Error: Action not recognized.");
+ }
+ else
+ {
+ var handler = handlerResolver(request.Action);
+ await handler.Handle(socket, message, userName);
+ }
+ }
+ catch (OperationCanceledException ex)
+ {
+ logger.LogError(ex.Message);
+ }
+ }
+ UnsubscribeFromBroadcastAndGames(userName);
+ }
+
+ public void SubscribeToBroadcast(WebSocket socket, string playerName)
+ {
+ logger.LogInformation("Subscribing [{0}] to broadcast", playerName);
+ connections.TryAdd(playerName, socket);
+ }
+
+ public void UnsubscribeFromBroadcastAndGames(string playerName)
+ {
+ logger.LogInformation("Unsubscribing [{0}] from broadcast", playerName);
+ connections.TryRemove(playerName, out _);
+ foreach (var game in gameSeats)
+ {
+ game.Value.Remove(playerName);
+ }
+ }
+
+ ///
+ /// Unsubscribes the player from their current game, then subscribes to the new game.
+ ///
+ public void SubscribeToGame(WebSocket socket, string gameName, string playerName)
+ {
+ // Unsubscribe from any other games
+ foreach (var kvp in gameSeats)
+ {
+ var gameNameKey = kvp.Key;
+ UnsubscribeFromGame(gameNameKey, playerName);
+ }
+
+ // Subscribe
+ logger.LogInformation("Subscribing player [{0}] to game [{1}]", playerName, gameName);
+ var addSuccess = gameSeats.TryAdd(gameName, new List { playerName });
+ if (!addSuccess && !gameSeats[gameName].Contains(playerName))
+ {
+ gameSeats[gameName].Add(playerName);
+ }
+ }
+
+ public void UnsubscribeFromGame(string gameName, string playerName)
+ {
+ if (gameSeats.ContainsKey(gameName))
+ {
+ logger.LogInformation("Unsubscribing player [{0}] from game [{1}]", playerName, gameName);
+ gameSeats[gameName].Remove(playerName);
+ if (gameSeats[gameName].Count == 0) gameSeats.TryRemove(gameName, out _);
+ }
+ }
+
+ public async Task BroadcastToAll(string msg)
+ {
+ var tasks = connections.Select(kvp =>
+ {
+ var player = kvp.Key;
+ var socket = kvp.Value;
+ logger.LogInformation("Broadcasting to player [{0}] \n{1}\n", new[] { player, msg });
+ return socket.SendTextAsync(msg);
+ });
+ await Task.WhenAll(tasks);
+ }
+
+ public async Task BroadcastToGame(string gameName, string msg)
+ {
+ if (gameSeats.ContainsKey(gameName))
+ {
+ var tasks = gameSeats[gameName]
+ .Select(playerName =>
+ {
+ logger.LogInformation("Broadcasting to game [{0}], player [{0}] \n{1}\n", gameName, playerName, msg);
+ return connections[playerName];
+ })
+ .Where(stream => stream != null)
+ .Select(socket => socket.SendTextAsync(msg));
+ await Task.WhenAll(tasks);
+ }
+ }
+
+ public async Task BroadcastToGame(string gameName, Func msgBuilder)
+ {
+ if (gameSeats.ContainsKey(gameName))
+ {
+ var tasks = gameSeats[gameName]
+ .Select(playerName =>
+ {
+ var socket = connections[playerName];
+ var msg = msgBuilder(playerName, socket);
+ logger.LogInformation("Broadcasting to game [{0}], player [{0}] \n{1}\n", gameName, playerName, msg);
+ return socket.SendTextAsync(msg);
+ });
+ await Task.WhenAll(tasks);
+ }
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs
new file mode 100644
index 0000000..9c4c579
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Managers/SocketConnectionManager.cs
@@ -0,0 +1,44 @@
+using Microsoft.AspNetCore.Http;
+using System;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace Websockets.Managers
+{
+ public interface ISocketConnectionManager
+ {
+ Task HandleSocketRequest(HttpContext context);
+ }
+
+ public class SocketConnectionManager : ISocketConnectionManager
+ {
+ private readonly ISocketCommunicationManager communicationManager;
+ private readonly ISocketTokenManager tokenManager;
+
+ public SocketConnectionManager(ISocketCommunicationManager communicationManager, ISocketTokenManager tokenManager) : base()
+ {
+ this.communicationManager = communicationManager;
+ this.tokenManager = tokenManager;
+
+ }
+
+ public async Task HandleSocketRequest(HttpContext context)
+ {
+ var hasToken = context.Request.Query.Keys.Contains("token");
+ if (hasToken)
+ {
+ var oneTimeToken = context.Request.Query["token"][0];
+ var tokenAsGuid = Guid.Parse(oneTimeToken);
+ var userName = tokenManager.GetUsername(tokenAsGuid);
+ if (!string.IsNullOrEmpty(userName))
+ {
+ var socket = await context.WebSockets.AcceptWebSocketAsync();
+ await communicationManager.CommunicateWith(socket, userName);
+ return;
+ }
+ }
+ context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
+ return;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs b/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs
new file mode 100644
index 0000000..9a7eb2a
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Managers/SocketTokenManager.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Websockets.Managers
+{
+ public interface ISocketTokenManager
+ {
+ Guid GenerateToken(string s);
+ string GetUsername(Guid g);
+ }
+
+ public class SocketTokenManager : ISocketTokenManager
+ {
+ ///
+ /// Key is userName
+ ///
+ private readonly Dictionary Tokens;
+
+ public SocketTokenManager()
+ {
+ Tokens = new Dictionary();
+ }
+
+ public Guid GenerateToken(string userName)
+ {
+ var guid = Guid.NewGuid();
+
+ if (Tokens.ContainsKey(userName))
+ {
+ Tokens.Remove(userName);
+ }
+ Tokens.Add(userName, guid);
+
+ _ = Task.Run(async () =>
+ {
+ await Task.Delay(TimeSpan.FromMinutes(1));
+ Tokens.Remove(userName);
+ });
+
+ return guid;
+ }
+
+ /// User name associated to the guid or null.
+ public string GetUsername(Guid guid)
+ {
+ if (Tokens.ContainsValue(guid))
+ {
+ var username = Tokens.First(kvp => kvp.Value == guid).Key;
+ Tokens.Remove(username);
+ return username;
+ }
+ return null;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/Utility/JsonRequest.cs b/Gameboard.ShogiUI.Sockets/Managers/Utility/JsonRequest.cs
new file mode 100644
index 0000000..0a2719a
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Managers/Utility/JsonRequest.cs
@@ -0,0 +1,15 @@
+using Websockets.ServiceModels.Interfaces;
+
+namespace Websockets.Managers.Utility
+{
+ public class JsonRequest
+ {
+ public IRequest Request { get; private set; }
+ public string Json { get; private set; }
+ public JsonRequest(IRequest request, string json)
+ {
+ Request = request;
+ Json = json;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs b/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs
new file mode 100644
index 0000000..37662b7
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Managers/Utility/Mapper.cs
@@ -0,0 +1,67 @@
+using Microsoft.FSharp.Core;
+using Websockets.ServiceModels.Types;
+using GameboardTypes = Gameboard.Shogi.Api.ServiceModels.Types;
+
+namespace Websockets.Managers.Utility
+{
+ public static class Mapper
+ {
+ public static GameboardTypes.Move Map(Move source)
+ {
+ var from = source.From;
+ var to = source.To;
+ FSharpOption pieceFromCaptured = source.PieceFromCaptured switch
+ {
+ "B" => new FSharpOption(GameboardTypes.PieceName.Bishop),
+ "G" => new FSharpOption(GameboardTypes.PieceName.GoldenGeneral),
+ "K" => new FSharpOption(GameboardTypes.PieceName.King),
+ "k" => new FSharpOption(GameboardTypes.PieceName.Knight),
+ "L" => new FSharpOption(GameboardTypes.PieceName.Lance),
+ "P" => new FSharpOption(GameboardTypes.PieceName.Pawn),
+ "R" => new FSharpOption(GameboardTypes.PieceName.Rook),
+ "S" => new FSharpOption(GameboardTypes.PieceName.SilverGeneral),
+ _ => null
+ };
+ var target = new GameboardTypes.Move
+ {
+ Origin = new GameboardTypes.BoardLocation { X = from.X, Y = from.Y },
+ Destination = new GameboardTypes.BoardLocation { X = to.X, Y = to.Y },
+ IsPromotion = source.IsPromotion,
+ PieceFromCaptured = pieceFromCaptured
+ };
+ return target;
+ }
+
+ public static Move Map(GameboardTypes.Move source)
+ {
+ var origin = source.Origin;
+ var destination = source.Destination;
+ string pieceFromCaptured = null;
+ if (source.PieceFromCaptured != null)
+ {
+ pieceFromCaptured = source.PieceFromCaptured.Value switch
+ {
+ GameboardTypes.PieceName.Bishop => "B",
+ GameboardTypes.PieceName.GoldenGeneral => "G",
+ GameboardTypes.PieceName.King => "K",
+ GameboardTypes.PieceName.Knight => "k",
+ GameboardTypes.PieceName.Lance => "L",
+ GameboardTypes.PieceName.Pawn => "P",
+ GameboardTypes.PieceName.Rook => "R",
+ GameboardTypes.PieceName.SilverGeneral => "S",
+ _ => ""
+ };
+ }
+
+ var target = new Move
+ {
+ From = new Coords { X = origin.X, Y = origin.Y },
+ To = new Coords { X = destination.X, Y = destination.Y },
+ IsPromotion = source.IsPromotion,
+ PieceFromCaptured = pieceFromCaptured
+ };
+
+ return target;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs b/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs
new file mode 100644
index 0000000..b0c4e95
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Managers/Utility/Request.cs
@@ -0,0 +1,11 @@
+using Websockets.ServiceModels.Interfaces;
+using Websockets.ServiceModels.Types;
+
+namespace AspShogiSockets.Managers.Utility
+{
+ public class Request : IRequest
+ {
+ public ClientAction Action { get; set; }
+ public string PlayerName { get; set; }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Program.cs b/Gameboard.ShogiUI.Sockets/Program.cs
new file mode 100644
index 0000000..af5f468
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Program.cs
@@ -0,0 +1,20 @@
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Hosting;
+
+namespace Websockets
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ CreateHostBuilder(args).Build().Run();
+ }
+
+ public static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseStartup();
+ });
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json b/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json
new file mode 100644
index 0000000..9d79795
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:63676",
+ "sslPort": 44396
+ }
+ },
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchUrl": "weatherforecast",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "AspShogiSockets": {
+ "commandName": "Project",
+ "launchBrowser": false,
+ "launchUrl": "Socket/Token",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "http://127.0.0.1:5101"
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs
new file mode 100644
index 0000000..72f318b
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Repositories/GameboardRepository.cs
@@ -0,0 +1,127 @@
+using Gameboard.Shogi.Api.ServiceModels.Messages;
+using Newtonsoft.Json;
+using System;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Websockets.Repositories.Utility;
+
+namespace Websockets.Repositories
+{
+ public interface IGameboardRepository
+ {
+ Task DeleteGame(string gameName);
+ Task GetGame(string gameName);
+ Task GetGames();
+ Task GetGames(string playerName);
+ Task GetMoves(string gameName);
+ Task PostGame(PostGame request);
+ Task PostJoinByCode(PostJoinByCode request);
+ Task PostJoinGame(string gameName, PostJoinGame request);
+ Task PostMove(string gameName, PostMove request);
+ Task PostJoinCode(string gameName, string userName);
+ Task GetPlayer(string userName);
+ Task PostPlayer(PostPlayer request);
+ }
+
+ public class GameboardRepository : IGameboardRepository
+ {
+ private readonly IAuthenticatedHttpClient client;
+ public GameboardRepository(IAuthenticatedHttpClient client)
+ {
+ this.client = client;
+ }
+
+ public async Task GetGames()
+ {
+ var response = await client.GetAsync("Games");
+ var json = await response.Content.ReadAsStringAsync();
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ public async Task GetGames(string playerName)
+ {
+ var uri = $"Games/{playerName}";
+ var response = await client.GetAsync(Uri.EscapeUriString(uri));
+ var json = await response.Content.ReadAsStringAsync();
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ public async Task GetGame(string gameName)
+ {
+ var uri = $"Game/{gameName}";
+ var response = await client.GetAsync(Uri.EscapeUriString(uri));
+ var json = await response.Content.ReadAsStringAsync();
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ public async Task DeleteGame(string gameName)
+ {
+ var uri = $"Game/{gameName}";
+ await client.DeleteAsync(Uri.EscapeUriString(uri));
+ }
+
+ public async Task PostGame(PostGame request)
+ {
+ var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
+ var response = await client.PostAsync("Game", content);
+ var json = await response.Content.ReadAsStringAsync();
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ public async Task PostJoinGame(string gameName, PostJoinGame request)
+ {
+ var uri = $"Game/{gameName}/Join";
+ var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
+ var response = await client.PostAsync(Uri.EscapeUriString(uri), content);
+ var json = await response.Content.ReadAsStringAsync();
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ public async Task PostJoinByCode(PostJoinByCode request)
+ {
+ var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
+ var response = await client.PostAsync("Game/Join", content);
+ var json = await response.Content.ReadAsStringAsync();
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ public async Task GetMoves(string gameName)
+ {
+ var uri = $"Game/{gameName}/Moves";
+ var response = await client.GetAsync(Uri.EscapeUriString(uri));
+ var json = await response.Content.ReadAsStringAsync();
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ public async Task PostMove(string gameName, PostMove request)
+ {
+ var uri = $"Game/{gameName}/Move";
+ var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
+ await client.PostAsync(Uri.EscapeUriString(uri), content);
+ }
+
+ public async Task PostJoinCode(string gameName, string userName)
+ {
+ var uri = $"JoinCode/{gameName}";
+ var serialized = JsonConvert.SerializeObject(new PostJoinCode { PlayerName = userName });
+ var content = new StringContent(serialized, Encoding.UTF8, "application/json");
+ var json = await (await client.PostAsync(Uri.EscapeUriString(uri), content)).Content.ReadAsStringAsync();
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ public async Task GetPlayer(string playerName)
+ {
+ var uri = $"Player/{playerName}";
+ var response = await client.GetAsync(Uri.EscapeUriString(uri));
+ var json = await response.Content.ReadAsStringAsync();
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ public async Task PostPlayer(PostPlayer request)
+ {
+ var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
+ return await client.PostAsync("Player", content);
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Repositories/PlayerRepository.cs b/Gameboard.ShogiUI.Sockets/Repositories/PlayerRepository.cs
new file mode 100644
index 0000000..843f9e0
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Repositories/PlayerRepository.cs
@@ -0,0 +1,32 @@
+using Gameboard.Shogi.Api.ServiceModels.Messages;
+using Newtonsoft.Json;
+using System;
+using System.Threading.Tasks;
+using Websockets.Repositories.Utility;
+
+namespace Websockets.Repositories
+{
+ [Obsolete("Use GameboardRepository. Functions from PlayerRepository will be moved.")]
+ public class PlayerRepository
+ {
+ private readonly IAuthenticatedHttpClient client;
+
+ public PlayerRepository(IAuthenticatedHttpClient client)
+ {
+ this.client = client;
+ }
+
+ public async Task GetPlayer(string playerName)
+ {
+ var response = await client.GetAsync($"/Player/{playerName}");
+ var json = await response.Content.ReadAsStringAsync();
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ public async Task DeletePlayer(string playerName)
+ {
+ var response = await client.DeleteAsync($"/Player/{playerName}");
+ await response.Content.ReadAsStringAsync();
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs b/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs
new file mode 100644
index 0000000..646b467
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Repositories/RepositoryManagers/GameboardRepositoryManager.cs
@@ -0,0 +1,43 @@
+using Gameboard.Shogi.Api.ServiceModels.Messages;
+using System;
+using System.Threading.Tasks;
+using Websockets.Repositories;
+
+namespace AspShogiSockets.Repositories.RepositoryManagers
+{
+ public interface IGameboardRepositoryManager
+ {
+ Task CreateGuestUser();
+ }
+
+ public class GameboardRepositoryManager : IGameboardRepositoryManager
+ {
+ private readonly IGameboardRepository repository;
+ private const int MaxTries = 3;
+
+ public GameboardRepositoryManager(IGameboardRepository repository)
+ {
+ this.repository = repository;
+ }
+
+ public async Task CreateGuestUser()
+ {
+ var count = 0;
+ while (count < MaxTries)
+ {
+ count++;
+ var clientId = $"Guest-{Guid.NewGuid()}";
+ var request = new PostPlayer
+ {
+ PlayerName = clientId
+ };
+ var response = await repository.PostPlayer(request);
+ if (response.IsSuccessStatusCode)
+ {
+ return clientId;
+ }
+ }
+ throw new OperationCanceledException($"Failed to create guest user after {MaxTries} tries.");
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs b/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs
new file mode 100644
index 0000000..62e066f
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Repositories/Utility/AuthenticatedHttpClient.cs
@@ -0,0 +1,103 @@
+using IdentityModel.Client;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace Websockets.Repositories.Utility
+{
+ public interface IAuthenticatedHttpClient
+ {
+ Task DeleteAsync(string requestUri);
+ Task GetAsync(string requestUri);
+ Task PostAsync(string requestUri, HttpContent content);
+ }
+
+ public class AuthenticatedHttpClient : HttpClient, IAuthenticatedHttpClient
+ {
+ private readonly ILogger logger;
+ private readonly string identityServerUrl;
+ private TokenResponse tokenResponse;
+ private readonly string clientId;
+ private readonly string clientSecret;
+
+ public AuthenticatedHttpClient(ILogger logger, IConfiguration configuration) : base()
+ {
+ this.logger = logger;
+ identityServerUrl = configuration["AppSettings:IdentityServer"];
+ clientId = configuration["AppSettings:ClientId"];
+ clientSecret = configuration["AppSettings:ClientSecret"];
+ BaseAddress = new Uri(configuration["AppSettings:GameboardShogiApi"]);
+ }
+
+ private async Task RefreshBearerToken()
+ {
+ var disco = await this.GetDiscoveryDocumentAsync(identityServerUrl);
+ if (disco.IsError)
+ {
+ logger.LogError("{DiscoveryErrorType}", disco.ErrorType);
+ throw new Exception(disco.Error);
+ }
+
+ var request = new ClientCredentialsTokenRequest
+ {
+ Address = disco.TokenEndpoint,
+ ClientId = clientId,
+ ClientSecret = clientSecret
+ };
+ var response = await this.RequestClientCredentialsTokenAsync(request);
+ if (response.IsError)
+ {
+ throw new Exception(response.Error);
+ }
+ tokenResponse = response;
+ logger.LogInformation("Refreshing Bearer Token to {BaseAddress}", BaseAddress);
+ this.SetBearerToken(tokenResponse.AccessToken);
+ }
+
+ public async new Task GetAsync(string requestUri)
+ {
+ var response = await base.GetAsync(requestUri);
+ if (response.StatusCode == HttpStatusCode.Unauthorized)
+ {
+ await RefreshBearerToken();
+ response = await base.GetAsync(requestUri);
+ }
+ logger.LogInformation(
+ "Repository GET to {BaseUrl}{RequestUrl} \nResponse: {Response}\n",
+ BaseAddress,
+ requestUri,
+ await response.Content.ReadAsStringAsync());
+ return response;
+ }
+ public async new Task PostAsync(string requestUri, HttpContent content)
+ {
+ var response = await base.PostAsync(requestUri, content);
+ if (response.StatusCode == HttpStatusCode.Unauthorized)
+ {
+ await RefreshBearerToken();
+ response = await base.PostAsync(requestUri, content);
+ }
+ logger.LogInformation(
+ "Repository POST to {BaseUrl}{RequestUrl} \nRequest: {Request}\nResponse: {Response}\n",
+ BaseAddress,
+ requestUri,
+ await content.ReadAsStringAsync(),
+ await response.Content.ReadAsStringAsync());
+ return response;
+ }
+ public async new Task DeleteAsync(string requestUri)
+ {
+ var response = await base.DeleteAsync(requestUri);
+ if (response.StatusCode == HttpStatusCode.Unauthorized)
+ {
+ await RefreshBearerToken();
+ response = await base.DeleteAsync(requestUri);
+ }
+ logger.LogInformation("Repository DELETE to {BaseUrl}{RequestUrl}", BaseAddress, requestUri);
+ return response;
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/Startup.cs b/Gameboard.ShogiUI.Sockets/Startup.cs
new file mode 100644
index 0000000..ba57a9f
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/Startup.cs
@@ -0,0 +1,145 @@
+using AspShogiSockets.Repositories.RepositoryManagers;
+using Gameboard.ShogiUI.Sockets.Extensions;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using Newtonsoft.Json.Serialization;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Websockets.Managers;
+using Websockets.Managers.ClientActionHandlers;
+using Websockets.Repositories;
+using Websockets.Repositories.Utility;
+using Websockets.ServiceModels.Types;
+
+namespace Websockets
+{
+ public class Startup
+ {
+ public Startup(IConfiguration configuration)
+ {
+ Configuration = configuration;
+ }
+
+ public IConfiguration Configuration { get; }
+
+ // This method gets called by the runtime. Use this method to add services to the container.
+ public void ConfigureServices(IServiceCollection services)
+ {
+ // Socket ActionHandlers
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ // Managers
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddScoped();
+ services.AddSingleton(sp => action =>
+ {
+ return action switch
+ {
+ ClientAction.ListGames => sp.GetService(),
+ ClientAction.CreateGame => sp.GetService(),
+ ClientAction.JoinGame => sp.GetService(),
+ ClientAction.JoinByCode => sp.GetService(),
+ ClientAction.LoadGame => sp.GetService(),
+ ClientAction.Move => sp.GetService(),
+ _ => throw new KeyNotFoundException($"Unable to resolve {nameof(IActionHandler)} for {nameof(ClientAction)} {action}"),
+ };
+ });
+
+ // Repositories
+ services.AddTransient();
+ services.AddSingleton();
+
+ services.AddControllers();
+
+ services
+ .AddAuthentication(options =>
+ {
+ options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
+ options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
+ })
+ .AddJwtBearer(options =>
+ {
+ options.Authority = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0";
+ options.Audience = "935df672-efa6-45fa-b2e8-b76dfd65a122";
+ options.TokenValidationParameters.ValidateIssuer = true;
+ options.TokenValidationParameters.ValidateAudience = true;
+ });
+ }
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISocketConnectionManager socketConnectionManager)
+ {
+ var origins = new[] { "https://localhost:3000", "https://dev.lucaserver.space", "https://lucaserver.space" };
+ var socketOptions = new WebSocketOptions();
+ foreach (var o in origins)
+ socketOptions.AllowedOrigins.Add(o);
+
+ if (env.IsDevelopment())
+ {
+ app.UseDeveloperExceptionPage();
+ }
+ else
+ {
+ app.UseHsts();
+ }
+ app
+ .UseRequestResponseLogging()
+ .UseCors(
+ opt => opt
+ .WithOrigins(origins)
+ .AllowAnyMethod()
+ .AllowAnyHeader()
+ .AllowCredentials()
+ )
+ .UseRouting()
+ .UseAuthentication()
+ .UseAuthorization()
+ .UseWebSockets(socketOptions)
+ .UseEndpoints(endpoints =>
+ {
+ endpoints.MapControllers();
+ })
+ .Use(async (context, next) =>
+ {
+ var isUpgradeHeader = context
+ .Request
+ .Headers
+ .Any(h => h.Key.Contains("upgrade", StringComparison.InvariantCultureIgnoreCase)
+ && h.Value.ToString().Contains("websocket", StringComparison.InvariantCultureIgnoreCase));
+ if (isUpgradeHeader)
+ {
+ await socketConnectionManager.HandleSocketRequest(context);
+ }
+ else
+ {
+ await next();
+ }
+ });
+
+ JsonConvert.DefaultSettings = () => new JsonSerializerSettings
+ {
+ Formatting = Formatting.Indented,
+ ContractResolver = new DefaultContractResolver
+ {
+ NamingStrategy = new CamelCaseNamingStrategy(),
+ },
+ Converters = new[] { new StringEnumConverter() },
+ NullValueHandling = NullValueHandling.Ignore
+ };
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/appsettings.Development.json b/Gameboard.ShogiUI.Sockets/appsettings.Development.json
new file mode 100644
index 0000000..8983e0f
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/appsettings.Development.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/Gameboard.ShogiUI.Sockets/appsettings.json b/Gameboard.ShogiUI.Sockets/appsettings.json
new file mode 100644
index 0000000..48c43bd
--- /dev/null
+++ b/Gameboard.ShogiUI.Sockets/appsettings.json
@@ -0,0 +1,17 @@
+{
+ "AppSettings": {
+ "IdentityServer": "https://identity.lucaserver.space/",
+ "GameboardShogiApi": "https://api.lucaserver.space/Gameboard.Shogi.Api/",
+ "ClientId": "DevClientId",
+ "ClientSecret": "DevSecret",
+ "Scope": "DevEnvironment"
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "AllowedHosts": "*"
+}
\ No newline at end of file
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
new file mode 100644
index 0000000..2477225
--- /dev/null
+++ b/azure-pipelines.yml
@@ -0,0 +1,64 @@
+# ASP.NET Core
+# Build and test ASP.NET Core projects targeting .NET Core.
+# Add steps that run tests, create a NuGet package, deploy, and more:
+# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core
+
+trigger:
+- master
+
+pr:
+- none
+
+pool:
+ vmImage: 'windows-latest'
+
+variables:
+ solution: '**/*.sln'
+ buildPlatform: 'Any CPU'
+ buildConfiguration: 'Release'
+ projectName: 'Gameboard.ShogiUI.Sockets'
+
+steps:
+
+- task: NuGetToolInstaller@1
+
+- task: NuGetCommand@2
+ inputs:
+ command: 'restore'
+ restoreSolution: '**/*.sln'
+ feedsToUse: 'config'
+
+- task: DotNetCoreCLI@2
+ displayName: Publish
+ env :
+ DOTNET_CLI_TELEMETRY_OPTOUT: 1
+ inputs:
+ command: 'publish'
+ publishWebProjects: false
+ projects: '**/*.csproj '
+ zipAfterPublish: false
+
+- task: FileTransform@1
+ inputs:
+ folderPath: '$(System.DefaultWorkingDirectory)'
+ fileType: 'json'
+ targetFiles: '**/appsettings.json'
+
+- task: CopyFilesOverSSH@0
+ displayName: SSH Copy to 1UB
+ inputs:
+ sshEndpoint: 'LucaServer'
+ sourceFolder: '$(System.DefaultWorkingDirectory)\$(projectName)\bin\Debug\netcoreapp3.1\publish'
+ targetFolder: '/var/www/api/production/$(projectName)'
+ contents: '**'
+ failOnEmptySource: true
+ cleanTargetFolder: true
+
+- task: SSH@0
+ displayName: Restart Kestrel
+ inputs:
+ sshEndpoint: 'LucaServer'
+ runOptions: 'commands'
+ commands: 'sudo systemctl restart kestrel-gameboard.shogiui.sockets.service'
+ readyTimeout: '20000'
+
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 0000000..346522c
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file