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);
}
///