From 60541d8821cdf04a750143238e72d6a6ad0b29e8 Mon Sep 17 00:00:00 2001 From: cplnathan Date: Sun, 15 Oct 2023 19:38:54 +0100 Subject: [PATCH] improvements from private repo --- GoogleHelper/GoogleHelper.csproj | 7 +- GoogleHelper/Json/IntentResponse.cs | 28 +-- GoogleHelper/Services/BaseService.cs | 8 +- GoogleHelper/Services/GoogleService.cs | 39 ++--- .../Server/Context/HttpContext.cs | 30 +++- .../Controllers/AuthenticationController.cs | 159 +++++++++--------- .../Server/Controllers/GoogleController.cs | 59 ++++--- SurePet2Google.Blazor/Server/Dockerfile | 27 +++ .../Models/Responses/Timeline/GetTimeline.cs | 5 +- SurePet2Google.Blazor/Server/Program.cs | 12 +- .../Server/Properties/launchSettings.json | 13 +- .../Services/Devices/DualSmartFlapService.cs | 113 ++++++------- .../Services/Devices/SmartHubService.cs | 10 +- .../Notifications/NotificationServiceV1.cs | 2 +- .../Notifications/NotificationServiceV2.cs | 107 ++++++++---- .../Server/Services/PersistenceService.cs | 13 ++ .../Server/Services/SurePetService.cs | 56 +++--- .../SurePet2Google.Blazor.Server.csproj | 25 ++- 18 files changed, 419 insertions(+), 294 deletions(-) create mode 100644 SurePet2Google.Blazor/Server/Dockerfile diff --git a/GoogleHelper/GoogleHelper.csproj b/GoogleHelper/GoogleHelper.csproj index 12c7ed6..c724cb7 100644 --- a/GoogleHelper/GoogleHelper.csproj +++ b/GoogleHelper/GoogleHelper.csproj @@ -1,14 +1,15 @@ - net7.0 + net8.0 enable enable + AnyCPU - - + + diff --git a/GoogleHelper/Json/IntentResponse.cs b/GoogleHelper/Json/IntentResponse.cs index 95e49cf..4347669 100644 --- a/GoogleHelper/Json/IntentResponse.cs +++ b/GoogleHelper/Json/IntentResponse.cs @@ -40,17 +40,16 @@ public class QueryPayload : ResponsePayload public class ExecutePayload : ResponsePayload { [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string errorCode { get; set; } + public string? errorCode { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string debugString { get; set; } + public string? debugString { get; set; } public List commands { get; set; } } // Important for polymorphism [JsonDerivedType(typeof(LockDeviceData))] - [JsonDerivedType(typeof(ThermostatDeviceData))] public class QueryDeviceData { public string status { get; set; } @@ -64,19 +63,6 @@ public class LockDeviceData : QueryDeviceData public string descriptiveCapacityRemaining { get; set; } } - public class ThermostatDeviceData : QueryDeviceData - { - public string thermostatMode { get; set; } - public decimal thermostatTemperatureSetpoint { get; set; } - public decimal thermostatTemperatureAmbient { get; set; } - } - - public class ValueUnit - { - public int rawValue { get; set; } - public string unit { get; set; } - } - [JsonDerivedType(typeof(ExecuteDeviceDataSuccess))] [JsonDerivedType(typeof(ExecuteDeviceDataError))] public class ExecuteDeviceData @@ -95,14 +81,10 @@ public class ExecuteDeviceDataError : ExecuteDeviceData { public ExecuteDeviceDataError() { } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string errorCode { get; set; } - public string errorCodeReason { get; set; } - } - public class Color - { - public string name { get; set; } - public int spectrumRGB { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string errorCodeReason { get; set; } } - } diff --git a/GoogleHelper/Services/BaseService.cs b/GoogleHelper/Services/BaseService.cs index 44638aa..186478d 100644 --- a/GoogleHelper/Services/BaseService.cs +++ b/GoogleHelper/Services/BaseService.cs @@ -7,7 +7,7 @@ namespace GoogleHelper.Services { public interface IDeviceService { - public Task QueryAsync(BaseContext session, BaseDeviceModel deviceModel, string deviceId) + public Task QueryAsync(BaseContext session, BaseDeviceModel deviceModel, string deviceId, CancellationToken token) where TResponse : QueryDeviceData, new(); public Task ExecuteAsync(BaseContext session, BaseDeviceModel deviceModel, string deviceId, string requestId, JsonObject data, CancellationToken token) where TResponse : ExecuteDeviceData, new(); @@ -38,15 +38,15 @@ public BaseDeviceService() public List ModelIdentifiers => this.DeviceModel.ModelIdentifiers; - public abstract Task QueryAsyncImplementation(TContext session, TModel deviceModel, string deviceId) where TResponse : QueryDeviceData, new(); + public abstract Task QueryAsyncImplementation(TContext session, TModel deviceModel, string deviceId, CancellationToken token) where TResponse : QueryDeviceData, new(); public abstract Task ExecuteAsyncImplementation(TContext session, TModel deviceModel, string deviceId, string requestId, JsonObject data, CancellationToken token) where TResponse : ExecuteDeviceData, new(); public abstract Task FetchAsyncImplementation(TContext session, TModel deviceModel, string deviceId, bool forceFetch = false); - public async Task QueryAsync(BaseContext session, BaseDeviceModel deviceModel, string deviceId) where TResponse : QueryDeviceData, new() + public async Task QueryAsync(BaseContext session, BaseDeviceModel deviceModel, string deviceId, CancellationToken token) where TResponse : QueryDeviceData, new() { - return await this.QueryAsyncImplementation((TContext)session, (TModel)deviceModel, deviceId); + return await this.QueryAsyncImplementation((TContext)session, (TModel)deviceModel, deviceId, token); } public async Task ExecuteAsync(BaseContext session, BaseDeviceModel deviceModel, string deviceId, string requestId, JsonObject data, CancellationToken token) where TResponse : ExecuteDeviceData, new() diff --git a/GoogleHelper/Services/GoogleService.cs b/GoogleHelper/Services/GoogleService.cs index 86e33f6..576a4a3 100644 --- a/GoogleHelper/Services/GoogleService.cs +++ b/GoogleHelper/Services/GoogleService.cs @@ -1,5 +1,6 @@ // Copyright (c) Nathan Ford. All rights reserved. Class1.cs +using Flurl; using Flurl.Http; using GoogleHelper.Context; using GoogleHelper.Json; @@ -55,14 +56,14 @@ private string BuildSignedJWT(string privateKey, string privateKeyId, string cli return token; } - public async Task ProvideFollowUp(string privateKey, string privateKeyId, string clientEmail, string agentUserId, string requestId, string deviceId, string deviceAction, JsonObject data) + public async Task ProvideFollowUp(string privateKey, string privateKeyId, string clientEmail, string agentUserId, string requestId, string deviceId, string deviceAction, JsonObject data, CancellationToken token) { - string token = this.BuildSignedJWT(privateKey, privateKeyId, clientEmail, agentUserId); + string authToken = this.BuildSignedJWT(privateKey, privateKeyId, clientEmail, agentUserId); try { IFlurlResponse unused = await "https://homegraph.googleapis.com/v1/devices:reportStateAndNotification" - .WithOAuthBearerToken(token) + .WithOAuthBearerToken(authToken) .PostJsonAsync(new HomegraphResponse() { agentUserId = agentUserId, @@ -85,7 +86,7 @@ public async Task ProvideFollowUp(string privateKey, string privateKeyId, string } } } - }, cancellationToken: CancellationToken.None); + }, cancellationToken: token); } catch (Exception ex) { @@ -93,14 +94,14 @@ public async Task ProvideFollowUp(string privateKey, string privateKeyId, string } } - public async Task ProvideObjectDetection(string privateKey, string privateKeyId, string clientEmail, string agentUserId, string deviceId, string objectName) + public async Task ProvideObjectDetection(string privateKey, string privateKeyId, string clientEmail, string agentUserId, string deviceId, string objectName, CancellationToken token) { - string token = this.BuildSignedJWT(privateKey, privateKeyId, clientEmail, agentUserId); + string authToken = this.BuildSignedJWT(privateKey, privateKeyId, clientEmail, agentUserId); try { IFlurlResponse unused = await "https://homegraph.googleapis.com/v1/devices:reportStateAndNotification" - .WithOAuthBearerToken(token) + .WithOAuthBearerToken(authToken) .PostJsonAsync(new HomegraphResponse() { agentUserId = agentUserId, @@ -134,7 +135,7 @@ public async Task ProvideObjectDetection(string privateKey, string privateKeyId, } } } - }, cancellationToken: CancellationToken.None); + }, cancellationToken: token); } catch (Exception ex) { @@ -142,11 +143,11 @@ public async Task ProvideObjectDetection(string privateKey, string privateKeyId, } } - public async Task HandleGoogleResponse(TContext context, GoogleIntentRequest request, IEnumerable supportedDevices, string sessionId, CancellationToken token) + public async Task HandleGoogleResponse(TContext? context, GoogleIntentRequest request, IEnumerable supportedDevices, string sessionId, CancellationToken token) { if (context is null) { - return null; + throw new InvalidOperationException("Invalid context provided to Google handler."); } GoogleIntentResponse response = new(request); @@ -174,7 +175,7 @@ public async Task ProvideObjectDetection(string privateKey, string privateKeyId, { IEnumerable>> groupedServiceModels = this.GroupedSupportedDevices(context, supportedDevices, action.payload?.devices); - Dictionary> deviceQueryTasks = groupedServiceModels.SelectMany(gp => gp.Select(device => (device.Key, gp.Key.QueryAsync(context, device.Value, device.Key)))) + Dictionary> deviceQueryTasks = groupedServiceModels.SelectMany(gp => gp.Select(device => (device.Key, gp.Key.QueryAsync(context, device.Value, device.Key, token)))) .ToDictionary(item => item.Key, item => item.Item2); IEnumerable<(string First, QueryDeviceData Second)> deviceQueryResults = deviceQueryTasks.Keys.Zip(await Task.WhenAll(deviceQueryTasks.Values)); @@ -196,8 +197,8 @@ public async Task ProvideObjectDetection(string privateKey, string privateKeyId, case "EXECUTE": { List executedCommands = new(); - string errorCode = null; - string debugMessage = null; + string? errorCode = null; + string? debugMessage = null; try { @@ -205,21 +206,21 @@ public async Task ProvideObjectDetection(string privateKey, string privateKeyId, { IEnumerable>> groupedServiceModels = this.GroupedSupportedDevices(context, supportedDevices, command?.devices); - foreach (Execution execution in command.execution) + foreach (Execution execution in command?.execution ?? Enumerable.Empty()) { List updatedIds = new(); - string parsedCommand = execution.command.Split("action.devices.commands.")[1]; + // string parsedCommand = execution.command.Split("action.devices.commands.")[1]; Dictionary> deviceExecuteTasks = groupedServiceModels.SelectMany(gp => gp.Select(device => (device.Key, gp.Key.ExecuteAsync(context, device.Value, device.Key, request.requestId/*parsedCommand*/, execution._params, token)))) .ToDictionary(item => item.Key, item => item.Item2); - IEnumerable<(string First, ExecuteDeviceData Second)> deviceExecuteResults = deviceExecuteTasks.Keys.Zip(await Task.WhenAll(deviceExecuteTasks.Values)); + IEnumerable<(string deviceKey, ExecuteDeviceData deviceData)> deviceExecuteResults = deviceExecuteTasks.Keys.Zip(await Task.WhenAll(deviceExecuteTasks.Values)); - foreach ((string First, ExecuteDeviceData Second) in deviceExecuteResults) + foreach ((string deviceKey, ExecuteDeviceData deviceData) in deviceExecuteResults) { - ExecuteDeviceData state = Second; - state.ids = new List() { First }; + ExecuteDeviceData state = deviceData; + state.ids = new List() { deviceKey }; executedCommands.Add(state); } diff --git a/SurePet2Google.Blazor/Server/Context/HttpContext.cs b/SurePet2Google.Blazor/Server/Context/HttpContext.cs index ce170a8..217dabf 100644 --- a/SurePet2Google.Blazor/Server/Context/HttpContext.cs +++ b/SurePet2Google.Blazor/Server/Context/HttpContext.cs @@ -1,3 +1,4 @@ +using Flurl; using Flurl.Http; using Flurl.Http.Configuration; using Microsoft.Extensions.Http; @@ -6,7 +7,7 @@ namespace SurePet2Google.Blazor.Server.Context { - public class GlobalHttpContext : DefaultHttpClientFactory + public class GlobalHttpContext : FlurlClientFactoryBase { public override HttpMessageHandler CreateMessageHandler() { @@ -20,7 +21,21 @@ public static IAsyncPolicy BuildRetryPolicy() { var retryPolicy = Policy - .Handle((exception) => new List() { HttpStatusCode.TooManyRequests, HttpStatusCode.BadRequest }.Contains(exception.StatusCode ?? HttpStatusCode.BadRequest)) + .Handle((exception) => + { + int? statusCode = -1; + + if (exception is FlurlHttpException flurlException) + { + statusCode = flurlException.StatusCode; + } + else if (exception is HttpRequestException httpException) + { + statusCode = (int?)httpException.StatusCode; + } + + return new List() { (int)HttpStatusCode.TooManyRequests, (int)HttpStatusCode.BadRequest, (int)HttpStatusCode.GatewayTimeout }.Contains(statusCode ?? (int)HttpStatusCode.BadRequest); + }) .WaitAndRetryAsync(5, retryAttempt => { var nextAttemptIn = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)); @@ -30,5 +45,16 @@ public static IAsyncPolicy BuildRetryPolicy() return retryPolicy; } + + protected override IFlurlClient Create(Url url) + { + var client = base.Create(url); + return client; + } + + protected override string GetCacheKey(Url url) + { + return "StaticCache"; + } } } diff --git a/SurePet2Google.Blazor/Server/Controllers/AuthenticationController.cs b/SurePet2Google.Blazor/Server/Controllers/AuthenticationController.cs index 2baa185..38d5943 100644 --- a/SurePet2Google.Blazor/Server/Controllers/AuthenticationController.cs +++ b/SurePet2Google.Blazor/Server/Controllers/AuthenticationController.cs @@ -28,92 +28,93 @@ public AuthenticationController(SurePetService surePetService, PersistenceServic } [HttpPost] - public async Task Register([FromBody] RegisterModel form) + public async Task Register([FromBody] RegisterModel form) { - if (form == null) + if (form != null && form.Data != null) { - return this.Json(this.BadRequest()); - } - - switch (form.request) - { - case RequestState.Initial: - { - if (form.Data.client_id != this.Configuration["Google:client_id"]) - { - return this.Json(this.BadRequest()); - } - - string? redirect_application = new Uri(form.Data.redirect_uri).AbsolutePath.Split('/') - .Where(item => !string.IsNullOrEmpty(item)) - .Skip(1) - .Take(1) - .FirstOrDefault(); - - if (redirect_application != this.Configuration["Google:client_reference"]) - { - return this.Json(this.BadRequest()); - } - - GoogleAuth auth = new() - { - redirect_uri = form.Data.redirect_uri, - client_id = form.Data.client_id, - state = form.Data.state - }; - - this.PersistenceService.AddOrUpdateGoogleAuth(auth); - - return this.Json(new RegisterResponse + switch (form.request) + { + case RequestState.Initial: { - message = ResponseState.Success, - success = true, - Data = auth, - request = RequestState.Final - }); - } - case RequestState.Final: - { - GoogleAuth? authEntity = this.PersistenceService.GetGoogleAuth(form.Data.state); - if (authEntity == null) - { - return this.Json(this.BadRequest()); + if (form.Data.client_id != this.Configuration["Google:client_id"]) + { + return this.Json(this.BadRequest()); + } + + string? redirect_application = new Uri(form.Data.redirect_uri).AbsolutePath.Split('/') + .Where(item => !string.IsNullOrEmpty(item)) + .Skip(1) + .Take(1) + .FirstOrDefault(); + + if (redirect_application != this.Configuration["Google:client_reference"]) + { + return this.Json(this.BadRequest()); + } + + GoogleAuth auth = new() + { + redirect_uri = form.Data.redirect_uri, + client_id = form.Data.client_id, + state = form.Data.state + }; + + this.PersistenceService.AddOrUpdateGoogleAuth(auth); + + return this.Json(new RegisterResponse + { + message = ResponseState.Success, + success = true, + Data = auth, + request = RequestState.Final + }); } - - RegisterResponse response = new() + case RequestState.Final: { - Data = authEntity - }; - - string? bearer = await this.SurePetService.AuthenticateWithCredentials(form.Username, form.Password, CancellationToken.None); - PetContext context = new PetContext(bearer, form.Username, form.Password); - - if (!string.IsNullOrEmpty(bearer)) - { - Guid refreshToken = Guid.NewGuid(); - - GetDevices? devices = await this.SurePetService.GetDevices(bearer, CancellationToken.None); - - context.Devices = this.SurePetService.ParseDevices(devices, this.SupportedDevices); - - this.PersistenceService.AddOrUpdatePetContext(context, refreshToken.ToString()); - - response.success = true; - response.Data = form.Data; - response.message = ResponseState.Success; - response.code = refreshToken; - } - else - { - response.success = false; - response.message = ResponseState.InvalidCredentials; + GoogleAuth? authEntity = this.PersistenceService.GetGoogleAuth(form.Data.state); + if (authEntity == null) + { + return this.Json(this.BadRequest()); + } + + RegisterResponse response = new() + { + Data = authEntity + }; + + string? bearer = await this.SurePetService.AuthenticateWithCredentials(form.Username, form.Password, CancellationToken.None); + if (!string.IsNullOrEmpty(bearer)) + { + PetContext context = new PetContext(bearer, form.Username, form.Password); + + Guid refreshToken = Guid.NewGuid(); + + GetDevices? devices = await this.SurePetService.GetDevices(bearer, CancellationToken.None); + + context.Devices = this.SurePetService.ParseDevices(devices, this.SupportedDevices); + + this.PersistenceService.DeletePetContextByUsername(context.Username); + this.PersistenceService.AddOrUpdatePetContext(context, refreshToken.ToString()); + + response.success = true; + response.Data = form.Data; + response.message = ResponseState.Success; + response.code = refreshToken; + } + else + { + response.success = false; + response.message = ResponseState.InvalidCredentials; + } + + return this.Json(response); } - - return this.Json(response); - } - default: - return this.Json(new RegisterResponse { message = ResponseState.BadRequest, success = false }); + default: + return this.Json(new RegisterResponse { message = ResponseState.BadRequest, success = false }); + } } + + return this.BadRequest(); } } } diff --git a/SurePet2Google.Blazor/Server/Controllers/GoogleController.cs b/SurePet2Google.Blazor/Server/Controllers/GoogleController.cs index 60c856b..55fdd0b 100644 --- a/SurePet2Google.Blazor/Server/Controllers/GoogleController.cs +++ b/SurePet2Google.Blazor/Server/Controllers/GoogleController.cs @@ -33,28 +33,35 @@ public async Task Fulfillment([FromBody] GoogleIntentRequest reque { //contacts.Save("data.xml"); - string bearer = this.HttpContext.Request.Headers.Authorization; - bearer = bearer.Split("Bearer ")[1]; + string? bearer = this.HttpContext.Request.Headers.Authorization; + bearer = bearer?.Split("Bearer ")?[1]; - PetContext? context = this.PersistenceService.GetPetContextByAccess(bearer); + if (bearer != null) + { + PetContext? context = this.PersistenceService.GetPetContextByAccess(bearer); - GoogleIntentResponse response = await this.GoogleService.HandleGoogleResponse(context, request, this.SupportedDevices, bearer, token); + if (context != null) + { + GoogleIntentResponse response = await this.GoogleService.HandleGoogleResponse(context, request, this.SupportedDevices, bearer, token); - return this.Json(response); + return this.Json(response); + } + } + return this.BadRequest(); } [HttpPost] [Consumes("application/x-www-form-urlencoded")] - public JsonResult Token([FromForm] IFormCollection form) + public IActionResult Token([FromForm] IFormCollection form) { if (form == null || form.Count <= 0) { - return this.Json(this.BadRequest()); + return this.BadRequest(); } if (form["client_secret"] != this.Configuration["Google:client_secret"] || form["client_id"] != this.Configuration["Google:client_id"]) { - return this.Json(this.BadRequest()); + return this.BadRequest(); } switch (form["grant_type"]) @@ -66,37 +73,41 @@ public JsonResult Token([FromForm] IFormCollection form) return this.Json(this.BadRequest()); } - Microsoft.Extensions.Primitives.StringValues refreshToken = form["code"]; - PetContext? context = this.PersistenceService.GetPetContextByRefresh(refreshToken); - if (context == null) + string? refreshToken = form?["code"]; + if (refreshToken != null) { - return this.Json(this.BadRequest()); - } + PetContext? context = this.PersistenceService.GetPetContextByRefresh(refreshToken); + if (context != null) + { + this.PersistenceService.DeletePetContextByRefresh(refreshToken); - this.PersistenceService.DeletePetContextByRefresh(refreshToken); + refreshToken = Guid.NewGuid().ToString(); - refreshToken = Guid.NewGuid().ToString(); + this.PersistenceService.AddOrUpdatePetContext(context, refreshToken); + context.GoogleAccessToken = Guid.NewGuid().ToString(); - this.PersistenceService.AddOrUpdatePetContext(context, refreshToken); - context.GoogleAccessToken = Guid.NewGuid().ToString(); + return this.Json(new RefreshTokenDto(refreshToken, context.GoogleAccessToken)); + } + } - return this.Json(new RefreshTokenDto(refreshToken, context.GoogleAccessToken)); + return this.BadRequest(); } case "refresh_token": { PetContext? context = this.PersistenceService.GetPetContextByRefresh(form["refresh_token"].ToString()); - if (context == null) + if (context != null) { - return this.Json(this.BadRequest()); - } - context.GoogleAccessToken = Guid.NewGuid().ToString(); + context.GoogleAccessToken = Guid.NewGuid().ToString(); + + return this.Json(new AccessTokenDto(context.GoogleAccessToken)); + } - return this.Json(new AccessTokenDto(context.GoogleAccessToken)); + return this.BadRequest(); } default: - return this.Json(this.BadRequest()); + return this.BadRequest(); } } } diff --git a/SurePet2Google.Blazor/Server/Dockerfile b/SurePet2Google.Blazor/Server/Dockerfile new file mode 100644 index 0000000..b54a965 --- /dev/null +++ b/SurePet2Google.Blazor/Server/Dockerfile @@ -0,0 +1,27 @@ +#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy-arm64v8 AS base +USER app +WORKDIR /app +EXPOSE 8080 +ENV ASPNETCORE_URLS=http://*:8080 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["SurePet2Google.Blazor/Server/SurePet2Google.Blazor.Server.csproj", "SurePet2Google.Blazor/Server/"] +COPY ["GoogleHelper/GoogleHelper.csproj", "GoogleHelper/"] +COPY ["SurePet2Google.Blazor/Client/SurePet2Google.Blazor.Client.csproj", "SurePet2Google.Blazor/Client/"] +COPY ["SurePet2Google.Blazor/Shared/SurePet2Google.Blazor.Shared.csproj", "SurePet2Google.Blazor/Shared/"] +RUN dotnet restore "SurePet2Google.Blazor/Server/SurePet2Google.Blazor.Server.csproj" +COPY . . +WORKDIR "/src/SurePet2Google.Blazor/Server" +RUN dotnet build "SurePet2Google.Blazor.Server.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +RUN dotnet publish "SurePet2Google.Blazor.Server.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "SurePet2Google.Blazor.Server.dll"] \ No newline at end of file diff --git a/SurePet2Google.Blazor/Server/Models/Responses/Timeline/GetTimeline.cs b/SurePet2Google.Blazor/Server/Models/Responses/Timeline/GetTimeline.cs index 17fb2a4..fed2fc9 100644 --- a/SurePet2Google.Blazor/Server/Models/Responses/Timeline/GetTimeline.cs +++ b/SurePet2Google.Blazor/Server/Models/Responses/Timeline/GetTimeline.cs @@ -11,8 +11,9 @@ public enum Direction public enum MovementType { - UnknownPeeked = 13, - UnknownMoved = 11, + // Not sure what the difference is here, upon inspecting API both seem to be 'unknown moved' so can't see why there is both? + UnknownMovedOrPeekedA = 13, + UnknownMovedOrPeekedB = 11, KnownMoved = 6, KnownPeeked = 4, } diff --git a/SurePet2Google.Blazor/Server/Program.cs b/SurePet2Google.Blazor/Server/Program.cs index bf87a19..809fa98 100644 --- a/SurePet2Google.Blazor/Server/Program.cs +++ b/SurePet2Google.Blazor/Server/Program.cs @@ -1,5 +1,6 @@ using Flurl.Http; using GoogleHelper.Services; +using SurePet2Google.Blazor.Client; using SurePet2Google.Blazor.Server.Context; using SurePet2Google.Blazor.Server.Services; using SurePet2Google.Blazor.Server.Services.Devices; @@ -16,12 +17,15 @@ public static void Main(string[] args) // Add services to the container. builder.Configuration.AddEnvironmentVariables(); - builder.WebHost.UseUrls(builder.Configuration["APPLICATION_URL"] ?? string.Empty); + //builder.WebHost.UseUrls(builder.Configuration["APPLICATION_URL"] ?? string.Empty); + builder.WebHost.UseUrls("http://*:8080"); builder.Services.AddControllersWithViews(); builder.Services.AddRazorPages(); - // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle - builder.Services.AddEndpointsApiExplorer(); + if (builder.Environment.IsDevelopment()) + { + builder.Services.AddEndpointsApiExplorer(); + } builder.Services.AddLogging(); builder.Services.AddScoped(typeof(IDeviceService), typeof(DualSmartFlapService)); @@ -36,7 +40,7 @@ public static void Main(string[] args) settings.BeforeCall = beforeCall; settings.BeforeCallAsync = async (httpCall) => await Task.Run(() => beforeCall.Invoke(httpCall)); - settings.HttpClientFactory = new GlobalHttpContext(); + settings.FlurlClientFactory = new GlobalHttpContext(); }); builder.Services.AddSingleton>(); diff --git a/SurePet2Google.Blazor/Server/Properties/launchSettings.json b/SurePet2Google.Blazor/Server/Properties/launchSettings.json index 4830335..936663e 100644 --- a/SurePet2Google.Blazor/Server/Properties/launchSettings.json +++ b/SurePet2Google.Blazor/Server/Properties/launchSettings.json @@ -18,7 +18,7 @@ }, "dotnetRunMessages": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", - "applicationUrl": "https://localhost:7161;http://localhost:5115;http://nathan.ford.xyz:80;https://nathan.ford.xyz:443" + "applicationUrl": "https://localhost:7161;http://localhost:5115" }, "IIS Express": { "commandName": "IISExpress", @@ -27,6 +27,17 @@ "ASPNETCORE_ENVIRONMENT": "Development" }, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}" + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true } }, "iisExpress": { diff --git a/SurePet2Google.Blazor/Server/Services/Devices/DualSmartFlapService.cs b/SurePet2Google.Blazor/Server/Services/Devices/DualSmartFlapService.cs index 37559dc..0b47407 100644 --- a/SurePet2Google.Blazor/Server/Services/Devices/DualSmartFlapService.cs +++ b/SurePet2Google.Blazor/Server/Services/Devices/DualSmartFlapService.cs @@ -24,17 +24,11 @@ public DualSmartFlapService(SurePetService surePetService, GoogleService ExecuteAsyncImplementation(PetContext session, FlapModel deviceModel, string deviceId, string requestId, JsonObject data, CancellationToken token) { - CancellationTokenSource cancellationToken = new(); - LockStatus lockRequest = data["lock"]?.GetValue() ?? true ? LockStatus.EnterOnly : LockStatus.Unlocked; string? followUpToken = data["followUpToken"]?.GetValue(); - Task lockExecute = this.SurePetService.UpdateLock(session.SurePetBearerToken, deviceId, lockRequest, token); - Task batteryQuery = this.SurePetService.GetBattery(session.SurePetBearerToken, deviceId, token); - Task onlineQuery = this.SurePetService.GetOnline(session.SurePetBearerToken, deviceId, token); - Task lockQuery = this.SurePetService.GetLock(session.SurePetBearerToken, deviceId, token); - - if (lockQuery is null) + var deviceStatus = await this.SurePetService.GetStatus(session.SurePetBearerToken, deviceId, token); + if (deviceStatus.lockStatus is LockStatus.Unknown) { return (TResponse)(ExecuteDeviceData)new ExecuteDeviceDataError() { @@ -42,42 +36,46 @@ public override async Task ExecuteAsyncImplementation(PetC errorCode = "relinkRequired" }; } - else if ((await lockQuery) == lockRequest) + else if (deviceStatus.lockStatus == lockRequest) { return (TResponse)(ExecuteDeviceData)new ExecuteDeviceDataError() { status = "FAILURE", - errorCode = (await lockQuery is LockStatus.EnterOnly or LockStatus.Locked) ? "alreadyLocked" : "alreadyUnlocked" + errorCode = (deviceStatus.lockStatus is LockStatus.EnterOnly or LockStatus.Locked) ? "alreadyLocked" : "alreadyUnlocked" }; } - else - { - bool allCompleted = Task.WaitAll(new Task[] { batteryQuery, lockExecute, lockQuery, onlineQuery }, 2500); - if ((await onlineQuery) == false) + if (deviceStatus.onlineStatus == false) + { + return (TResponse)(ExecuteDeviceData)new ExecuteDeviceDataError() { - return (TResponse)(ExecuteDeviceData)new ExecuteDeviceDataError() + status = "FAILURE", + states = new JsonObject() { - status = "FAILURE", - states = new JsonObject() - { - { "online", false }, - }, - errorCode = "deviceOffline" - }; - } - else if (followUpToken is not null && !allCompleted) + { "online", false }, + }, + errorCode = "deviceOffline" + }; + } + + Task lockExecute = Task.FromResult(LockStatus.Unknown); + if (!token.IsCancellationRequested) + { + lockExecute = this.SurePetService.UpdateLock(session.SurePetBearerToken, deviceId, lockRequest, token); + } + + if (!string.IsNullOrEmpty(followUpToken)) + { + var followUpTask = Task.Run(async () => { - new Thread(async () => - { - var completedSuccess = Task.WaitAll(new Task[] { lockExecute }, 10000); + var lockExecuteResult = await lockExecute; - JsonObject followUpData = new JsonObject() - { + JsonObject followUpData = new JsonObject() + { { "priority", 0 }, { "followUpResponse", - !completedSuccess ? new JsonObject() + lockExecuteResult is LockStatus.Unknown ? new JsonObject() { { "status", "FAILURE" }, { "errorCode", "deviceOffline" }, @@ -85,42 +83,44 @@ public override async Task ExecuteAsyncImplementation(PetC } : new JsonObject() { { "status", "SUCCESS" }, - { "isLocked", await lockExecute == LockStatus.EnterOnly || await lockExecute == LockStatus.Locked }, + { "isLocked", (await lockExecute) is LockStatus.EnterOnly or LockStatus.Locked }, { "followUpToken", followUpToken } } } - }; + }; - await this.GoogleService.ProvideFollowUp(this.Configuration["Google:Homegraph:private_key"], this.Configuration["Google:Homegraph:private_key_id"], this.Configuration["Google:Homegraph:client_email"], session.GoogleAccessToken, requestId, deviceId, "LockUnlock", followUpData); - }).Start(); + await this.GoogleService.ProvideFollowUp(this.Configuration["Google:Homegraph:private_key"], this.Configuration["Google:Homegraph:private_key_id"], this.Configuration["Google:Homegraph:client_email"], session.GoogleAccessToken, requestId, deviceId, "LockUnlock", followUpData, token); + }); + var completedInTime = followUpTask.Wait(2500); + + if (!completedInTime) + { return (TResponse)new ExecuteDeviceData() { status = "PENDING", states = new JsonObject() { - { "online", await lockQuery != null }, - { "isLocked", await lockQuery == LockStatus.EnterOnly || await lockQuery == LockStatus.Locked }, + { "online", deviceStatus.onlineStatus }, + { "isLocked", deviceStatus.lockStatus is LockStatus.EnterOnly or LockStatus.Locked }, { "isJammed", false }, - { "descriptiveCapacityRemaining", this.GetDescriptiveBattery(await batteryQuery, 4) }, + { "descriptiveCapacityRemaining", this.GetDescriptiveBattery(deviceStatus.batteryStatus, 4) }, } }; } - else + } + + return (TResponse)new ExecuteDeviceData() + { + status = "SUCCESS", + states = new JsonObject() { - return (TResponse)new ExecuteDeviceData() - { - status = "SUCCESS", - states = new JsonObject() - { - { "online", await lockExecute != null }, - { "isLocked", await lockExecute == LockStatus.EnterOnly || await lockExecute == LockStatus.Locked }, - { "isJammed", false }, - { "descriptiveCapacityRemaining", this.GetDescriptiveBattery(await batteryQuery, 4) }, - } - }; + { "online", deviceStatus.onlineStatus }, + { "isLocked", (await lockExecute) is LockStatus.EnterOnly or LockStatus.Locked }, + { "isJammed", false }, + { "descriptiveCapacityRemaining", this.GetDescriptiveBattery(deviceStatus.batteryStatus, 4) }, } - } + }; } public override Task FetchAsyncImplementation(PetContext session, FlapModel deviceModel, string deviceId, bool forceFetch = false) @@ -128,22 +128,19 @@ public override Task FetchAsyncImplementation(PetContext session, FlapMode throw new NotImplementedException(); } - public override async Task QueryAsyncImplementation(PetContext session, FlapModel deviceModel, string deviceId) + public override async Task QueryAsyncImplementation(PetContext session, FlapModel deviceModel, string deviceId, CancellationToken token) { CancellationTokenSource cancellationToken = new(); - Task lockResult = this.SurePetService.GetLock(session.SurePetBearerToken, deviceId, cancellationToken.Token); - Task batteryResult = this.SurePetService.GetBattery(session.SurePetBearerToken, deviceId, cancellationToken.Token); - - Task.WaitAll(lockResult, batteryResult); + var deviceStatus = await this.SurePetService.GetStatus(session.SurePetBearerToken, deviceId, token); return (TResponse)(QueryDeviceData)new LockDeviceData() { - online = await lockResult != null, + online = deviceStatus.onlineStatus, status = "SUCCESS", isJammed = false, - isLocked = await lockResult is LockStatus.Locked or LockStatus.EnterOnly, - descriptiveCapacityRemaining = this.GetDescriptiveBattery(await batteryResult, 4) + isLocked = deviceStatus.lockStatus is LockStatus.Locked or LockStatus.EnterOnly, + descriptiveCapacityRemaining = this.GetDescriptiveBattery(deviceStatus.batteryStatus, 4) }; } diff --git a/SurePet2Google.Blazor/Server/Services/Devices/SmartHubService.cs b/SurePet2Google.Blazor/Server/Services/Devices/SmartHubService.cs index 1c4560b..378379c 100644 --- a/SurePet2Google.Blazor/Server/Services/Devices/SmartHubService.cs +++ b/SurePet2Google.Blazor/Server/Services/Devices/SmartHubService.cs @@ -31,13 +31,15 @@ public override Task FetchAsyncImplementation(PetContext session, HubModel return Task.FromResult(false); } - public override async Task QueryAsyncImplementation(PetContext session, HubModel deviceModel, string deviceId) + public override async Task QueryAsyncImplementation(PetContext session, HubModel deviceModel, string deviceId, CancellationToken token) { return await Task.Run(() => - (TResponse)(QueryDeviceData)new LockDeviceData() { - online = true, - status = "SUCCESS" + return (TResponse)(QueryDeviceData)new LockDeviceData() + { + online = true, + status = "SUCCESS" + }; }); } diff --git a/SurePet2Google.Blazor/Server/Services/Notifications/NotificationServiceV1.cs b/SurePet2Google.Blazor/Server/Services/Notifications/NotificationServiceV1.cs index 243fcf2..3bc0e8e 100644 --- a/SurePet2Google.Blazor/Server/Services/Notifications/NotificationServiceV1.cs +++ b/SurePet2Google.Blazor/Server/Services/Notifications/NotificationServiceV1.cs @@ -87,7 +87,7 @@ protected override async Task DoNotifications() var currentLocation = (PetPosition)currentPosition.where; string objectName = currentLocation == PetPosition.Outside ? $"{currentPosition.pet_name} Left" : $"{currentPosition.pet_name} Entered"; - this.GoogleService.ProvideObjectDetection(this.Configuration["Google:Homegraph:private_key"], this.Configuration["Google:Homegraph:private_key_id"], this.Configuration["Google:Homegraph:client_email"], currentContext.GoogleAccessToken, triggeredDevice.Key, objectName); + this.GoogleService.ProvideObjectDetection(this.Configuration["Google:Homegraph:private_key"], this.Configuration["Google:Homegraph:private_key_id"], this.Configuration["Google:Homegraph:client_email"], currentContext.GoogleAccessToken, triggeredDevice.Key, objectName, cancellationToken); } } } diff --git a/SurePet2Google.Blazor/Server/Services/Notifications/NotificationServiceV2.cs b/SurePet2Google.Blazor/Server/Services/Notifications/NotificationServiceV2.cs index 396ba67..7ff7572 100644 --- a/SurePet2Google.Blazor/Server/Services/Notifications/NotificationServiceV2.cs +++ b/SurePet2Google.Blazor/Server/Services/Notifications/NotificationServiceV2.cs @@ -1,6 +1,9 @@ using GoogleHelper.Services; +using Polly; using SurePet2Google.Blazor.Server.Context; +using SurePet2Google.Blazor.Server.Models.Responses.Devices; using SurePet2Google.Blazor.Server.Models.Responses.Pets; +using SurePet2Google.Blazor.Server.Models.Responses.Timeline; namespace SurePet2Google.Blazor.Server.Services.Notifications { @@ -13,55 +16,95 @@ public NotificationServiceV2(SurePetService surePetService, GoogleService peekedType = new List() { Models.Responses.Timeline.MovementType.UnknownPeeked, Models.Responses.Timeline.MovementType.KnownPeeked }; - private List movedType = new List() { Models.Responses.Timeline.MovementType.UnknownMoved, Models.Responses.Timeline.MovementType.KnownMoved }; - protected override async Task DoNotifications() { try { + List notificationTasks = new(); + foreach (KeyValuePair context in this.PersistenceService.GooglePetContextReadOnly) { - List<(string device, string text)> newEvents = new(); - - if (!context.Value.Pets?.Any() ?? true) + notificationTasks.Add(Task.Run(async () => { - var pets = await this.SurePetService.GetPets(context.Value.SurePetBearerToken, this.cancellationToken); + List<(string? device, string text)> newEvents = await this.GetEvents(context.Value); + + await this.DispatchEvents(context.Value, newEvents); + })); + } - context.Value.Pets = pets?.data?.Where(x => x is not null).ToList() ?? Enumerable.Empty().ToList(); - } + await Task.WhenAll(notificationTasks); - var results = (await this.SurePetService.GetTimeline(context.Value.SurePetBearerToken, this.cancellationToken))?.data - .Where(x => x?.movements != null) - .Where(x => x.movements?.Any(y => y.created_at >= lastUpdated) ?? false); + lastUpdated = DateTime.UtcNow; + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + } + } - if (results == null) - continue; + private async Task> GetEvents(PetContext petContext) + { + List<(string? device, string text)> newEvents = new(); - results.Where(x => x.movements.Any(y => this.peekedType.Contains(y.type))).ToList() - .ForEach(x => newEvents.Add((x.movements[0].device_id.ToString(), x.pets?.Any() ?? false ? $"{x.pets[0].name} Peeked" : "An Animal"))); + if (!petContext.Pets?.Any() ?? true) + { + var pets = await this.SurePetService.GetPets(petContext.SurePetBearerToken, this.cancellationToken); - results.Where(x => x.movements.Any(y => this.movedType.Contains(y.type) && y.direction != Models.Responses.Timeline.Direction.Looked)).Where(x => x.pets.Any()).ToList() - .ForEach(x => newEvents.Add((x.movements[0].device_id.ToString(), (x.pets?.Any() ?? false ? x.pets[0].name : "An Animal") + " " + (x.movements[0].direction == Models.Responses.Timeline.Direction.Left ? "Left" : "Entered")))); + petContext.Pets = pets?.data?.Where(x => x is not null).ToList() ?? Enumerable.Empty().ToList(); + } - foreach (var movementEvent in newEvents) - { - KeyValuePair triggeredDevice = context.Value.Devices.FirstOrDefault(device => device.Key == movementEvent.device); - if (triggeredDevice.Value is null) - { - continue; - } - - _ = this.GoogleService.ProvideObjectDetection(this.Configuration["Google:Homegraph:private_key"], this.Configuration["Google:Homegraph:private_key_id"], this.Configuration["Google:Homegraph:client_email"], context.Value.GoogleAccessToken, triggeredDevice.Key, movementEvent.text); - } - } + var results = (await this.SurePetService.GetTimeline(petContext.SurePetBearerToken, this.cancellationToken))?.data + .Where(x => x?.movements != null) + .Where(x => x.movements?.Any(y => y.created_at.ToLocalTime() >= lastUpdated.ToLocalTime()) ?? false); - lastUpdated = DateTime.UtcNow; + if (results != null) + { + foreach (var result in results) + { + var pet = petContext.Pets?.FirstOrDefault(x => x.tag_id == result.tags?[0].id); + var petName = pet?.name ?? "An Animal"; + + string actionDescription = this.GetActionDescription(result.movements?[0]); + + newEvents.Add((result.movements?[0].device_id.ToString(), $"{petName} {actionDescription}")); + } } - catch (Exception ex) + + return newEvents; + } + + private async Task DispatchEvents(PetContext petContext, List<(string? device, string text)> events) + { + foreach (var movementEvent in events.Where(x => x.device is not null)) { - Console.WriteLine(ex.ToString()); + KeyValuePair triggeredDevice = petContext.Devices.FirstOrDefault(device => device.Key == movementEvent.device); + if (triggeredDevice.Value is null) + { + continue; + } + + await this.GoogleService.ProvideObjectDetection(this.Configuration["Google:Homegraph:private_key"], this.Configuration["Google:Homegraph:private_key_id"], this.Configuration["Google:Homegraph:client_email"], petContext.GoogleAccessToken, triggeredDevice.Key, movementEvent.text, cancellationToken) + .ConfigureAwait(false); } } + + private string GetActionDescription(Movement? movement) + { + return movement switch + { + { + direction: Direction.Looked, + type: MovementType.UnknownMovedOrPeekedA or MovementType.UnknownMovedOrPeekedB or MovementType.KnownPeeked + } => "Peeked", + { + direction: Direction.Entered, + type: MovementType.KnownMoved + } => "Entered", + { + direction: Direction.Left + } => "Left", + _ => string.Empty + }; + } } } diff --git a/SurePet2Google.Blazor/Server/Services/PersistenceService.cs b/SurePet2Google.Blazor/Server/Services/PersistenceService.cs index 80068c4..ac7a44e 100644 --- a/SurePet2Google.Blazor/Server/Services/PersistenceService.cs +++ b/SurePet2Google.Blazor/Server/Services/PersistenceService.cs @@ -53,6 +53,19 @@ public void DeletePetContextByRefresh(string refreshToken) this.GooglePetContext.Remove(refreshToken); } + public void DeletePetContextByUsername(string username) + { + if (username == null) + { + return; + } + + foreach (var refreshToken in this.GooglePetContext.Keys.ToList()) + { + this.DeletePetContextByRefresh(refreshToken); + } + } + public PetContext? GetPetContextByAccess(string accessToken) { return accessToken == null diff --git a/SurePet2Google.Blazor/Server/Services/SurePetService.cs b/SurePet2Google.Blazor/Server/Services/SurePetService.cs index b5c2435..2136638 100644 --- a/SurePet2Google.Blazor/Server/Services/SurePetService.cs +++ b/SurePet2Google.Blazor/Server/Services/SurePetService.cs @@ -2,6 +2,7 @@ using Flurl.Http; using GoogleHelper.Models; using GoogleHelper.Services; +using SurePet2Google.Blazor.Server.Context; using SurePet2Google.Blazor.Server.Models.Responses.Auth; using SurePet2Google.Blazor.Server.Models.Responses.Devices; using SurePet2Google.Blazor.Server.Models.Responses.Pets; @@ -11,6 +12,7 @@ namespace SurePet2Google.Blazor.Server.Services { public enum LockStatus { + Unknown = -1, Unlocked = 0, EnterOnly = 1, ExitOnly = 2, @@ -92,45 +94,39 @@ private IFlurlRequest MakeRequest(string request, string endpoint, string bearer return response; } - public async Task GetLock(string bearer, string deviceId, CancellationToken cancellationToken) + public async Task<(LockStatus lockStatus, double? batteryStatus, bool onlineStatus)> GetStatus(string bearer, string deviceId, CancellationToken cancellationToken) { - GetDevice response = await this - .MakeRequest(BaseUrl, StatusEndpoint.Replace("{device}", deviceId), bearer) - .GetJsonAsync(cancellationToken: cancellationToken); - - int? lockData = response?.data?["locking"]?["mode"]?.GetValue(); - - return (LockStatus?)lockData ?? null; - } - - public async Task GetBattery(string bearer, string deviceId, CancellationToken cancellationToken) - { - GetDevice response = await this - .MakeRequest(BaseUrl, StatusEndpoint.Replace("{device}", deviceId), bearer) - .GetJsonAsync(cancellationToken: cancellationToken); - - return response?.data?["battery"]?.GetValue(); - } + try + { + GetDevice response = await this + .MakeRequest(BaseUrl, StatusEndpoint.Replace("{device}", deviceId), bearer) + .GetJsonAsync(cancellationToken: cancellationToken); - public async Task GetOnline(string bearer, string deviceId, CancellationToken cancellationToken) - { - GetDevice response = await this - .MakeRequest(BaseUrl, StatusEndpoint.Replace("{device}", deviceId), bearer) - .GetJsonAsync(cancellationToken: cancellationToken); + int? lockData = response?.data?["locking"]?["mode"]?.GetValue(); + double? batteryData = response?.data?["battery"]?.GetValue(); + bool onlineData = response?.data?["online"]?.GetValue() ?? false; - return response?.data?["online"]?.GetValue(); + return ((LockStatus)(lockData ?? -1), batteryData, onlineData); + } + catch + { + return (LockStatus.Unknown, null, false); + } } public async Task GetTimeline(string bearer, CancellationToken cancellationToken) { - GetTimeline response = await this - .MakeRequest(BaseUrl, TimelineEndpoint, bearer) - .GetJsonAsync(cancellationToken: cancellationToken); + var response = await GlobalHttpContext.BuildRetryPolicy().ExecuteAsync(async () => + { + return (await this + .MakeRequest(BaseUrl, TimelineEndpoint, bearer) + .GetAsync()).ResponseMessage; + }); - return response; + return await response.Content.ReadFromJsonAsync(); } - public async Task UpdateLock(string bearer, string deviceId, LockStatus newStatus, CancellationToken cancellationToken) + public async Task UpdateLock(string bearer, string deviceId, LockStatus newStatus, CancellationToken cancellationToken) { ControlDevice response = await this .MakeRequest(BaseUrl, ControlEndpoint.Replace("{device}", deviceId), bearer) @@ -143,7 +139,7 @@ private IFlurlRequest MakeRequest(string request, string endpoint, string bearer int? lockData = response?.data?["locking"]?.GetValue(); - return (LockStatus?)lockData ?? null; + return ((LockStatus?)lockData) ?? LockStatus.Unknown; } public Dictionary ParseDevices(GetDevices devices, IEnumerable supportedDevices) diff --git a/SurePet2Google.Blazor/Server/SurePet2Google.Blazor.Server.csproj b/SurePet2Google.Blazor/Server/SurePet2Google.Blazor.Server.csproj index 5122ac8..6cd6cf5 100644 --- a/SurePet2Google.Blazor/Server/SurePet2Google.Blazor.Server.csproj +++ b/SurePet2Google.Blazor/Server/SurePet2Google.Blazor.Server.csproj @@ -1,9 +1,18 @@ - + - net7.0 + net8.0 enable enable + e750cc01-fec9-4bbe-8e0f-dd878a094920 + Linux + ..\.. + + + + -p 8080:8080 + True + snupkg @@ -13,13 +22,13 @@ - - + + - - - - + + + +