From 4109fa8461c2e9d44e3eb811bd9b43ac991b32f4 Mon Sep 17 00:00:00 2001 From: Ben Stein <115497763+sei-bstein@users.noreply.github.com> Date: Thu, 10 Nov 2022 08:31:51 -0500 Subject: [PATCH] - Adds a call when Gameboard creates challenge data for Unity games to update the console URLs in gamebrain. (#63) - Gameboard now correctly only creates one copy of the Unity challenge per team, designing either the manager or the calling player as the team leader. --- .../Features/Challenge/ChallengeController.cs | 53 +++++++++--------- .../Features/UnityGames/IUnityGameService.cs | 2 +- .../Features/UnityGames/IUnityStore.cs | 3 +- .../UnityGames/UnityGameController.cs | 56 +++++++++++++++++-- .../Features/UnityGames/UnityGameService.cs | 41 +++++++------- .../Features/UnityGames/UnityStore.cs | 10 +--- 6 files changed, 101 insertions(+), 64 deletions(-) diff --git a/src/Gameboard.Api/Features/Challenge/ChallengeController.cs b/src/Gameboard.Api/Features/Challenge/ChallengeController.cs index 8d3737e4..5e6585d1 100644 --- a/src/Gameboard.Api/Features/Challenge/ChallengeController.cs +++ b/src/Gameboard.Api/Features/Challenge/ChallengeController.cs @@ -1,18 +1,17 @@ // Copyright 2021 Carnegie Mellon University. All Rights Reserved. // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. +using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - +using Gameboard.Api.Hubs; using Gameboard.Api.Services; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.AspNetCore.Authorization; -using TopoMojo.Api.Client; using Gameboard.Api.Validators; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; -using Gameboard.Api.Hubs; -using System.Collections.Generic; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; +using TopoMojo.Api.Client; namespace Gameboard.Api.Controllers { @@ -32,7 +31,7 @@ public ChallengeController( PlayerService playerService, IHubContext hub, ConsoleActorMap actormap - ): base(logger, cache, validator) + ) : base(logger, cache, validator) { ChallengeService = challengeService; PlayerService = playerService; @@ -83,14 +82,14 @@ await Hub.Clients.Group(result.TeamId).ChallengeEvent( /// [HttpGet("api/challenge/{id}")] [Authorize] - public async Task Retrieve([FromRoute]string id) + public async Task Retrieve([FromRoute] string id) { AuthorizeAny( () => Actor.IsDirector, () => ChallengeService.UserIsTeamPlayer(id, Actor.Id).Result ); - await Validate(new Entity{ Id = id }); + await Validate(new Entity { Id = id }); return await ChallengeService.Retrieve(id); } @@ -102,7 +101,7 @@ public async Task Retrieve([FromRoute]string id) /// [HttpPost("api/challenge/preview")] [Authorize] - public async Task Preview([FromBody]NewChallenge model) + public async Task Preview([FromBody] NewChallenge model) { AuthorizeAny( () => IsSelf(model.PlayerId).Result @@ -133,14 +132,14 @@ public Task Update([FromBody] ChangedChallenge model) /// [HttpDelete("/api/challenge/{id}")] [Authorize] - public async Task Delete([FromRoute]string id) + public async Task Delete([FromRoute] string id) { AuthorizeAny( () => Actor.IsDirector // () => Actor.IsTester && ChallengeService.UserIsTeamPlayer(id, Actor.Id).Result ); - await Validate(new Entity{ Id = id }); + await Validate(new Entity { Id = id }); await ChallengeService.Delete(id); return; @@ -153,7 +152,7 @@ public async Task Delete([FromRoute]string id) /// [HttpPut("/api/challenge/start")] [Authorize] - public async Task StartGamespace([FromBody]ChangedChallenge model) + public async Task StartGamespace([FromBody] ChangedChallenge model) { AuthorizeAny( () => Actor.IsDirector, @@ -178,14 +177,14 @@ await Hub.Clients.Group(result.TeamId).ChallengeEvent( /// [HttpPut("/api/challenge/stop")] [Authorize] - public async Task StopGamespace([FromBody]ChangedChallenge model) + public async Task StopGamespace([FromBody] ChangedChallenge model) { AuthorizeAny( () => Actor.IsDirector, () => ChallengeService.UserIsTeamPlayer(model.Id, Actor.Id).Result ); - await Validate(new Entity{ Id = model.Id }); + await Validate(new Entity { Id = model.Id }); var result = await ChallengeService.StopGamespace(model.Id, Actor.Id); @@ -203,7 +202,7 @@ await Hub.Clients.Group(result.TeamId).ChallengeEvent( /// [HttpPut("/api/challenge/grade")] [Authorize(AppConstants.GraderPolicy)] - public async Task Grade([FromBody]SectionSubmission model) + public async Task Grade([FromBody] SectionSubmission model) { AuthorizeAny( () => Actor.IsDirector, @@ -211,7 +210,7 @@ public async Task Grade([FromBody]SectionSubmission model) () => ChallengeService.UserIsTeamPlayer(model.Id, Actor.Id).Result ); - await Validate(new Entity{ Id = model.Id }); + await Validate(new Entity { Id = model.Id }); var result = await ChallengeService.Grade(model, Actor.Id); @@ -229,7 +228,7 @@ await Hub.Clients.Group(result.TeamId).ChallengeEvent( /// [HttpPut("/api/challenge/regrade")] [Authorize] - public async Task Regrade([FromBody]Entity model) + public async Task Regrade([FromBody] Entity model) { AuthorizeAny( () => Actor.IsDirector @@ -253,7 +252,7 @@ await Hub.Clients.Group(result.TeamId).ChallengeEvent( /// [HttpGet("/api/challenge/{id}/audit")] [Authorize] - public async Task Audit([FromRoute]string id) + public async Task Audit([FromRoute] string id) { AuthorizeAny( () => Actor.IsDirector @@ -271,7 +270,7 @@ public async Task Audit([FromRoute]string id) /// [HttpPost("/api/challenge/console")] [Authorize(AppConstants.ConsolePolicy)] - public async Task GetConsole([FromBody]ConsoleRequest model) + public async Task GetConsole([FromBody] ConsoleRequest model) { await Validate(new Entity { Id = model.SessionId }); @@ -301,7 +300,7 @@ await ChallengeService.SetConsoleActor(model, Actor.Id, Actor.ApprovedName) /// [HttpPut("/api/challenge/console")] [Authorize(AppConstants.ConsolePolicy)] - public async Task SetConsoleActor([FromBody]ConsoleRequest model) + public async Task SetConsoleActor([FromBody] ConsoleRequest model) { await Validate(new Entity { Id = model.SessionId }); @@ -315,7 +314,7 @@ await ChallengeService.SetConsoleActor(model, Actor.Id, Actor.ApprovedName) [HttpGet("/api/challenge/consoles")] [Authorize] - public async Task> FindConsoles([FromQuery]string gid) + public async Task> FindConsoles([FromQuery] string gid) { AuthorizeAny( () => Actor.IsDirector, @@ -327,7 +326,7 @@ public async Task> FindConsoles([FromQuery]string gid) [HttpGet("/api/challenge/consoleactors")] [Authorize] - public ConsoleActor[] GetConsoleActors([FromQuery]string gid) + public ConsoleActor[] GetConsoleActors([FromQuery] string gid) { AuthorizeAny( () => Actor.IsDirector, @@ -339,7 +338,7 @@ public ConsoleActor[] GetConsoleActors([FromQuery]string gid) [HttpGet("/api/challenge/consoleactor")] [Authorize(AppConstants.ConsolePolicy)] - public ConsoleActor GetConsoleActor([FromQuery]string uid) + public ConsoleActor GetConsoleActor([FromQuery] string uid) { AuthorizeAny( () => Actor.IsDirector, @@ -402,7 +401,7 @@ public async Task ListArchived([FromQuery] SearchFilter mod private async Task IsSelf(string playerId) { - return await PlayerService.MapId(playerId) == Actor.Id; + return await PlayerService.MapId(playerId) == Actor.Id; } } } diff --git a/src/Gameboard.Api/Features/UnityGames/IUnityGameService.cs b/src/Gameboard.Api/Features/UnityGames/IUnityGameService.cs index 6b926ed7..e5207518 100644 --- a/src/Gameboard.Api/Features/UnityGames/IUnityGameService.cs +++ b/src/Gameboard.Api/Features/UnityGames/IUnityGameService.cs @@ -6,7 +6,7 @@ namespace Gameboard.Api.Features.UnityGames; public interface IUnityGameService { - Task> AddChallengeEvent(NewUnityChallengeEvent model, string userId); + Task AddChallengeEvent(NewUnityChallengeEvent model, string userId); Task AddChallenge(NewUnityChallenge newChallenge, User actor); Task CreateMissionEvent(UnityMissionUpdate model, Api.User actor); Task DeleteChallengeData(string gameId); diff --git a/src/Gameboard.Api/Features/UnityGames/IUnityStore.cs b/src/Gameboard.Api/Features/UnityGames/IUnityStore.cs index 09b6cab6..825e1a1f 100644 --- a/src/Gameboard.Api/Features/UnityGames/IUnityStore.cs +++ b/src/Gameboard.Api/Features/UnityGames/IUnityStore.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Threading.Tasks; using Gameboard.Api.Data.Abstractions; @@ -6,5 +5,5 @@ namespace Gameboard.Api.Features.UnityGames; public interface IUnityStore : IStore { - Task> AddUnityChallengeEvents(IEnumerable challengeEvents); + Task AddUnityChallengeEvent(Data.ChallengeEvent challengeEvent); } \ No newline at end of file diff --git a/src/Gameboard.Api/Features/UnityGames/UnityGameController.cs b/src/Gameboard.Api/Features/UnityGames/UnityGameController.cs index 76d21330..33a3c65f 100644 --- a/src/Gameboard.Api/Features/UnityGames/UnityGameController.cs +++ b/src/Gameboard.Api/Features/UnityGames/UnityGameController.cs @@ -1,9 +1,11 @@ // Copyright 2021 Carnegie Mellon University. All Rights Reserved. // Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information. -using System.Collections.Generic; +using System; using System.Linq; using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; using System.Threading.Tasks; using AutoMapper; using Gameboard.Api.Features.UnityGames; @@ -13,6 +15,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Logging; @@ -22,10 +25,12 @@ namespace Gameboard.Api.Controllers; [Authorize] public class UnityGameController : _Controller { + private readonly CoreOptions _appSettings; private readonly ConsoleActorMap _actorMap; private readonly GameService _gameService; private readonly IHttpClientFactory _httpClientFactory; private readonly IHubContext _hub; + private readonly LinkGenerator _linkGenerator; private readonly IMapper _mapper; private readonly IUnityGameService _unityGameService; @@ -36,21 +41,32 @@ public UnityGameController( UnityGamesValidator validator, // other stuff ConsoleActorMap actorMap, + CoreOptions appSettings, GameService gameService, IHttpClientFactory httpClientFactory, IUnityGameService unityGameService, IHubContext hub, + LinkGenerator link, IMapper mapper ) : base(logger, cache, validator) { _actorMap = actorMap; + _appSettings = appSettings; _gameService = gameService; _httpClientFactory = httpClientFactory; _hub = hub; + _linkGenerator = link; _mapper = mapper; _unityGameService = unityGameService; } + [HttpGet("/api/unity")] + [AllowAnonymous] + public IActionResult Hi() + { + return Ok(_appSettings.GameEngineUrl + " " + Request.GetTypedHeaders().Referer.ToString()); + } + [HttpGet("/api/unity/{gid}/{tid}")] [Authorize] public async Task GetGamespace([FromRoute] string gid, [FromRoute] string tid) @@ -113,7 +129,7 @@ public async Task UndeployUnitySpace([FromQuery] string gid, [FromRoute] /// NewChallengeEvent /// ChallengeEvent [Authorize] - [HttpPost("api/unity/challenges")] + [HttpPost("api/unity/challenge")] public async Task CreateChallenge([FromBody] NewUnityChallenge model) { AuthorizeAny( @@ -125,9 +141,37 @@ public async Task UndeployUnitySpace([FromQuery] string gid, [FromRoute] await Validate(model); var result = await _unityGameService.AddChallenge(model, Actor); - await _hub.Clients + // now that we have challenge IDs, we can update gamebrain's console urls + var gamebrainClient = await CreateGamebrain(); + + var vmData = model.Vms.Select(vm => + { + var consoleHost = new UriBuilder(Request.Scheme, Request.Host.Host, Request.Host.Port ?? -1, "test/gb/mks"); + consoleHost.Query = $"f=1&s={result.Id}&v={vm.Name}"; + + return new UnityGameVm + { + Id = vm.Id, + Url = consoleHost.Uri.ToString(), + Name = vm.Name, + }; + }).ToArray(); + + try + { + await gamebrainClient.PostAsync($"admin/update_console_urls/{model.TeamId}", JsonContent.Create(vmData, mediaType: MediaTypeHeaderValue.Parse("application/json"))); + } + catch (Exception ex) + { + Console.Write("Calling gamebrain failed with", ex); + } + finally + { + // notify the hub (if there is one) + await _hub.Clients .Group(model.TeamId) .ChallengeEvent(new HubEvent(_mapper.Map(result), EventAction.Updated)); + } return result; } @@ -137,9 +181,9 @@ await _hub.Clients /// /// NewChallengeEvent /// ChallengeEvent - [HttpPost("api/unity/challengeEvents")] + [HttpPost("api/unity/challengeEvent")] [Authorize] - public async Task> CreateChallengeEvent([FromBody] NewUnityChallengeEvent model) + public async Task CreateChallengeEvent([FromBody] NewUnityChallengeEvent model) { AuthorizeAny( () => Actor.IsDirector, @@ -151,8 +195,8 @@ await _hub.Clients return await _unityGameService.AddChallengeEvent(model, Actor.Id); } - [Authorize] [HttpPost("api/unity/mission-update")] + [Authorize] public async Task CreateMissionEvent([FromBody] UnityMissionUpdate model) { AuthorizeAny( diff --git a/src/Gameboard.Api/Features/UnityGames/UnityGameService.cs b/src/Gameboard.Api/Features/UnityGames/UnityGameService.cs index e80d372e..4c5f6b5b 100644 --- a/src/Gameboard.Api/Features/UnityGames/UnityGameService.cs +++ b/src/Gameboard.Api/Features/UnityGames/UnityGameService.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; -using System.Threading; using System.Threading.Tasks; using AutoMapper; using Gameboard.Api.Data; @@ -40,13 +39,14 @@ ConsoleActorMap actorMap public async Task AddChallenge(NewUnityChallenge newChallenge, User actor) { + // each _team_ should only get one copy of the challenge. If anyone else tries, send them on their way // each player should only create their challenge data once, so if they call again, just return what they've already // got var existingChallenge = await Store.DbContext .Challenges .AsNoTracking() .Include(c => c.Events) - .Where(c => c.GameId == newChallenge.GameId && c.PlayerId == newChallenge.PlayerId) + .Where(c => c.GameId == newChallenge.GameId && c.TeamId == newChallenge.TeamId) .FirstOrDefaultAsync(); if (existingChallenge != null) @@ -61,8 +61,7 @@ ConsoleActorMap actorMap .Where(p => p.TeamId == newChallenge.TeamId) .ToListAsync(); - // team name? - var teamCaptain = ResolveTeamCaptain(teamPlayers, newChallenge.TeamId); + var teamCaptain = ResolveTeamCaptain(teamPlayers, newChallenge); var challengeName = $"{teamCaptain.ApprovedName} vs. Cubespace"; // load the spec associated with the game @@ -78,6 +77,11 @@ ConsoleActorMap actorMap .Games .FirstOrDefaultAsync(g => g.Id == newChallenge.GameId); + if (game == null) + { + throw new ResourceNotFound(newChallenge.GameId); + } + // this is some guesswork on my part and omits some fields. // we'll see how it goes - BS var state = new TopoMojo.Api.Client.GameState @@ -147,21 +151,15 @@ ConsoleActorMap actorMap WhenCreated = DateTimeOffset.UtcNow, }; - await Store.DbContext.Challenges.AddAsync(newChallengeEntity); await Store.DbContext.SaveChangesAsync(); return newChallengeEntity; } - public async Task> AddChallengeEvent(NewUnityChallengeEvent model, string userId) + public async Task AddChallengeEvent(NewUnityChallengeEvent model, string userId) { - var teamPlayers = await Store.DbContext - .Players - .Where(p => p.TeamId == model.TeamId) - .ToListAsync(); - - var events = teamPlayers.Select(p => new Data.ChallengeEvent + var challengeEvent = new Data.ChallengeEvent { ChallengeId = model.ChallengeId, UserId = userId, @@ -169,9 +167,9 @@ public async Task> AddChallengeEvent(NewUnityChallen Text = model.Text, Type = model.Type, Timestamp = model.Timestamp - }); + }; - return await Store.AddUnityChallengeEvents(events); + return await Store.AddUnityChallengeEvent(challengeEvent); } public async Task DeleteChallengeData(string gameId) @@ -223,17 +221,18 @@ public async Task CreateMissionEvent(UnityMissionUpdate model, Api.User actor) await Store.DbContext.SaveChangesAsync(); } - private Data.Player ResolveTeamCaptain(IEnumerable players, string teamId) + private Data.Player ResolveTeamCaptain(IEnumerable players, NewUnityChallenge newChallenge) { if (players.Count() == 0) { - throw new CaptainResolutionFailure(teamId); + throw new CaptainResolutionFailure(newChallenge.TeamId); } - // find the player with the earliest alphabetic name in case we need to tiebreak - // between captains or if the team doesn't have one. sometimes weird stuff happens. - // you can quote me. + // if the team has a captain (manager, yay) + // if they have too many, boo (pick one by name which is stupid but stupid things happen sometimes) + // if they have none, congratulations to the player who called the API! var sortedPlayers = players.OrderBy(p => p.ApprovedName); + var actingPlayer = players.First(p => p.Id == newChallenge.PlayerId); var captains = players.Where(p => p.IsManager); if (captains.Count() == 1) @@ -242,9 +241,9 @@ private Data.Player ResolveTeamCaptain(IEnumerable players, string } else if (captains.Count() > 1) { - sortedPlayers = captains.OrderBy(c => c.ApprovedName); + return captains.OrderBy(c => c.ApprovedName).First(); } - return sortedPlayers.First(); + return actingPlayer; } } \ No newline at end of file diff --git a/src/Gameboard.Api/Features/UnityGames/UnityStore.cs b/src/Gameboard.Api/Features/UnityGames/UnityStore.cs index 0fc4caba..d9adb56a 100644 --- a/src/Gameboard.Api/Features/UnityGames/UnityStore.cs +++ b/src/Gameboard.Api/Features/UnityGames/UnityStore.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Gameboard.Api.Data; -using Microsoft.EntityFrameworkCore; namespace Gameboard.Api.Features.UnityGames; @@ -13,12 +9,12 @@ public UnityStore(GameboardDbContext dbContext) : base(dbContext) { } - public async Task> AddUnityChallengeEvents(IEnumerable challengeEvents) + public async Task AddUnityChallengeEvent(Data.ChallengeEvent challengeEvent) { - this.DbContext.ChallengeEvents.AddRange(challengeEvents); + this.DbContext.ChallengeEvents.Add(challengeEvent); await this.DbContext.SaveChangesAsync(); - return challengeEvents; + return challengeEvent; } // public async Task UpdateAvgDeployTime(string gameId)