diff --git a/Astral.ApiServer/Controllers/V1/UserApiController.cs b/Astral.ApiServer/Controllers/V1/UserApiController.cs index 9652504..1ca787e 100644 --- a/Astral.ApiServer/Controllers/V1/UserApiController.cs +++ b/Astral.ApiServer/Controllers/V1/UserApiController.cs @@ -71,10 +71,17 @@ public class UserApiController : BaseApiController /// /// Receive a user's location heartbeat. /// + /// Instance of . [Authorize] [HttpPut("location")] public async Task ReceiveLocation([FromBody] HeartbeatRootRequestModel heartbeatModel) { + if (heartbeatModel?.Location != null) + { + var heartbeatDto = heartbeatModel.Location.ToUserLocationHeartbeat(); + await _userPresenceService.ConsumeLocationHeartbeat(heartbeatDto); + } + return SuccessResult(); } diff --git a/Astral.ApiServer/Models/HeartbeatModel.cs b/Astral.ApiServer/Models/HeartbeatModel.cs index f2a3c21..85c5299 100644 --- a/Astral.ApiServer/Models/HeartbeatModel.cs +++ b/Astral.ApiServer/Models/HeartbeatModel.cs @@ -3,6 +3,7 @@ // using System.Text.Json.Serialization; +using Astral.Services.Dtos; namespace Astral.ApiServer.Models; @@ -15,7 +16,7 @@ public class HeartbeatModel /// True if connected. /// [JsonPropertyName("connected")] - public bool Connected { get; set; } + public bool? Connected { get; set; } /// /// Path. @@ -27,13 +28,13 @@ public class HeartbeatModel /// Domain id. /// [JsonPropertyName("domain_id")] - public Guid DomainId { get; set; } + public string DomainId { get; set; } /// /// Place id. /// [JsonPropertyName("place_id")] - public Guid PlaceId { get; set; } + public string PlaceId { get; set; } /// /// Network address. @@ -45,17 +46,71 @@ public class HeartbeatModel /// Network port. /// [JsonPropertyName("network_port")] - public int NetworkPort { get; set; } + public int? NetworkPort { get; set; } /// /// Node id. /// [JsonPropertyName("node_id")] - public Guid NodeId { get; set; } + public string NodeId { get; set; } /// /// Availability. /// [JsonPropertyName("availability")] public string Availability { get; set; } + + /// + /// Convert this model to . + /// + /// Instance of . + public UserLocationHeartbeatDto ToUserLocationHeartbeat() + { + var result = new UserLocationHeartbeatDto() + { + Connected = Connected, + Path = Path, + NetworkAddress = NetworkAddress, + NetworkPort = NetworkPort, + Availability = Availability + }; + + if (!string.IsNullOrEmpty(DomainId)) + { + if (Guid.TryParse(DomainId, out var guid)) + { + result.DomainId = guid; + } + else + { + result.DomainId = null; + } + } + + if (!string.IsNullOrEmpty(PlaceId)) + { + if (Guid.TryParse(PlaceId, out var guid)) + { + result.PlaceId = guid; + } + else + { + PlaceId = null; + } + } + + if (!string.IsNullOrEmpty(NodeId)) + { + if (Guid.TryParse(NodeId, out var guid)) + { + result.NodeId = guid; + } + else + { + result.NodeId = null; + } + } + + return result; + } } diff --git a/Astral.Services/Dtos/UserLocationHeartbeatDto.cs b/Astral.Services/Dtos/UserLocationHeartbeatDto.cs index 3154dda..6361a58 100644 --- a/Astral.Services/Dtos/UserLocationHeartbeatDto.cs +++ b/Astral.Services/Dtos/UserLocationHeartbeatDto.cs @@ -12,7 +12,7 @@ public class UserLocationHeartbeatDto /// /// True if connected. /// - public bool Connected { get; set; } + public bool? Connected { get; set; } /// /// Path. @@ -22,12 +22,12 @@ public class UserLocationHeartbeatDto /// /// Domain id. /// - public Guid DomainId { get; set; } + public Guid? DomainId { get; set; } /// /// Place id. /// - public Guid PlaceId { get; set; } + public Guid? PlaceId { get; set; } /// /// Network address. @@ -37,12 +37,12 @@ public class UserLocationHeartbeatDto /// /// Network port. /// - public int NetworkPort { get; set; } + public int? NetworkPort { get; set; } /// /// Node id. /// - public Guid NodeId { get; set; } + public Guid? NodeId { get; set; } /// /// Availability. diff --git a/Astral.Services/Helpers/EntityHelpers.cs b/Astral.Services/Helpers/EntityHelpers.cs new file mode 100644 index 0000000..a5ba048 --- /dev/null +++ b/Astral.Services/Helpers/EntityHelpers.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. +// + +namespace Astral.Services.Helpers; + +/// +/// Entity helpers. +/// +public static class EntityHelpers +{ + /// + /// Apply changes only when new entity property is different and not null. + /// + /// Old entity to compare against. + /// New entity with new values. + /// The type of entity to compare. + /// The new entity with changes applied. + public static TEntity ApplyChanges(TEntity oldEntity, TEntity newEntity) + { + var entityType = typeof(TEntity); + var properties = entityType.GetProperties(); + foreach (var property in properties) + { + if (property.GetValue(newEntity) != null && property.GetValue(newEntity) != property.GetValue(oldEntity)) + { + property.SetValue(oldEntity, property.GetValue(newEntity)); + } + else + { + property.SetValue(oldEntity, property.GetValue(oldEntity)); + } + } + + return oldEntity; + } +} diff --git a/Astral.Services/Profiles/AutomapperProfile.cs b/Astral.Services/Profiles/AutomapperProfile.cs index fef364d..9c9e6f5 100644 --- a/Astral.Services/Profiles/AutomapperProfile.cs +++ b/Astral.Services/Profiles/AutomapperProfile.cs @@ -2,6 +2,7 @@ // Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License. // +using Astral.Core.Constants; using Astral.Core.Entities; using Astral.Services.Dtos; using AutoMapper; @@ -31,5 +32,20 @@ public class AutomapperProfile : Profile .ForMember(dest => dest.Title, opt => opt.MapFrom(src => src.Title)) .ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description)) .ForMember(dest => dest.Internal, opt => opt.MapFrom(src => src.Internal)); + + CreateMap() + .ForMember( + dest => dest.Availability, + opt => opt.MapFrom((src, dest) => + Enum.TryParse(src.Availability, true, out Discoverability discoverability) + ? discoverability + : Discoverability.None)) + .ForMember(dest => dest.Connected, opt => opt.MapFrom(src => src.Connected)) + .ForMember(dest => dest.Path, opt => opt.MapFrom(src => src.Path)) + .ForMember(dest => dest.DomainId, opt => opt.MapFrom(src => src.DomainId)) + .ForMember(dest => dest.NetworkAddress, opt => opt.MapFrom(src => src.NetworkAddress)) + .ForMember(dest => dest.NetworkPort, opt => opt.MapFrom(src => src.NetworkPort)) + .ForMember(dest => dest.NodeId, opt => opt.MapFrom(src => src.NodeId)) + .ForMember(dest => dest.PlaceId, opt => opt.MapFrom(src => src.PlaceId)); } } diff --git a/Astral.Services/Services/UserPresenceService.cs b/Astral.Services/Services/UserPresenceService.cs index a53fb05..10d8a4e 100644 --- a/Astral.Services/Services/UserPresenceService.cs +++ b/Astral.Services/Services/UserPresenceService.cs @@ -8,7 +8,9 @@ using Astral.Core.RepositoryInterfaces; using Astral.Services.Constants; using Astral.Services.Dtos; using Astral.Services.Exceptions; +using Astral.Services.Helpers; using Astral.Services.Interfaces; +using AutoMapper; using Injectio.Attributes; namespace Astral.Services.Services; @@ -20,6 +22,7 @@ public class UserPresenceService : IUserPresenceService private readonly IIdentityProvider _identityProvider; private readonly ICryptographyService _cryptographyService; private readonly IUserPresenceRepository _userPresenceRepository; + private readonly IMapper _mapper; /// /// Initializes a new instance of the class. @@ -27,14 +30,17 @@ public class UserPresenceService : IUserPresenceService /// Instance of . /// Instance of . /// Instance of . + /// Instance of . public UserPresenceService( IIdentityProvider identityProvider, ICryptographyService cryptographyService, - IUserPresenceRepository userPresenceRepository) + IUserPresenceRepository userPresenceRepository, + IMapper mapper) { _identityProvider = identityProvider; _cryptographyService = cryptographyService; _userPresenceRepository = userPresenceRepository; + _mapper = mapper; } /// @@ -52,6 +58,7 @@ public class UserPresenceService : IUserPresenceService { heartbeat = new UserPresence() { + CreatedAt = DateTime.UtcNow, Id = userId, LastHeartbeat = DateTime.UtcNow }; @@ -68,7 +75,27 @@ public class UserPresenceService : IUserPresenceService /// public async Task ConsumeLocationHeartbeat(UserLocationHeartbeatDto heartbeat) { - throw new NotImplementedException(); + var userId = _identityProvider.GetUserId(); + + if (userId == Guid.Empty) + { + throw new UnauthorizedException(); + } + + var newHeartbeat = _mapper.Map(heartbeat); + newHeartbeat.Id = userId; + + var oldHeartbeat = await _userPresenceRepository.FindByIdAsync(userId) ?? new UserPresence() + { + CreatedAt = DateTime.UtcNow, + Id = userId + }; + newHeartbeat.CreatedAt = oldHeartbeat.CreatedAt; + newHeartbeat.UpdatedAt = oldHeartbeat.UpdatedAt; + newHeartbeat.LastHeartbeat = oldHeartbeat.LastHeartbeat; + newHeartbeat = EntityHelpers.ApplyChanges(oldHeartbeat, newHeartbeat); + + await _userPresenceRepository.UpdateAsync(newHeartbeat); } ///