heartbeat and user presence #5

Merged
mike merged 2 commits from 2024-12-14-heartbeat-and-presence into main 2024-12-15 21:22:37 +01:00
6 changed files with 154 additions and 12 deletions
Showing only changes of commit d84d7631ce - Show all commits

View file

@ -71,10 +71,17 @@ public class UserApiController : BaseApiController
/// <summary>
/// Receive a user's location heartbeat.
/// </summary>
/// <param name="heartbeatModel">Instance of <see cref="HeartbeatRootRequestModel"/>.</param>
[Authorize]
[HttpPut("location")]
public async Task<IActionResult> ReceiveLocation([FromBody] HeartbeatRootRequestModel heartbeatModel)
{
if (heartbeatModel?.Location != null)
{
var heartbeatDto = heartbeatModel.Location.ToUserLocationHeartbeat();
await _userPresenceService.ConsumeLocationHeartbeat(heartbeatDto);
}
return SuccessResult();
}

View file

@ -3,6 +3,7 @@
// </copyright>
using System.Text.Json.Serialization;
using Astral.Services.Dtos;
namespace Astral.ApiServer.Models;
@ -15,7 +16,7 @@ public class HeartbeatModel
/// True if connected.
/// </summary>
[JsonPropertyName("connected")]
public bool Connected { get; set; }
public bool? Connected { get; set; }
/// <summary>
/// Path.
@ -27,13 +28,13 @@ public class HeartbeatModel
/// Domain id.
/// </summary>
[JsonPropertyName("domain_id")]
public Guid DomainId { get; set; }
public string DomainId { get; set; }
/// <summary>
/// Place id.
/// </summary>
[JsonPropertyName("place_id")]
public Guid PlaceId { get; set; }
public string PlaceId { get; set; }
/// <summary>
/// Network address.
@ -45,17 +46,71 @@ public class HeartbeatModel
/// Network port.
/// </summary>
[JsonPropertyName("network_port")]
public int NetworkPort { get; set; }
public int? NetworkPort { get; set; }
/// <summary>
/// Node id.
/// </summary>
[JsonPropertyName("node_id")]
public Guid NodeId { get; set; }
public string NodeId { get; set; }
/// <summary>
/// Availability.
/// </summary>
[JsonPropertyName("availability")]
public string Availability { get; set; }
/// <summary>
/// Convert this model to <see cref="UserLocationHeartbeatDto"/>.
/// </summary>
/// <returns>Instance of <see cref="UserLocationHeartbeatDto"/>.</returns>
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;
}
}

View file

@ -12,7 +12,7 @@ public class UserLocationHeartbeatDto
/// <summary>
/// True if connected.
/// </summary>
public bool Connected { get; set; }
public bool? Connected { get; set; }
/// <summary>
/// Path.
@ -22,12 +22,12 @@ public class UserLocationHeartbeatDto
/// <summary>
/// Domain id.
/// </summary>
public Guid DomainId { get; set; }
public Guid? DomainId { get; set; }
/// <summary>
/// Place id.
/// </summary>
public Guid PlaceId { get; set; }
public Guid? PlaceId { get; set; }
/// <summary>
/// Network address.
@ -37,12 +37,12 @@ public class UserLocationHeartbeatDto
/// <summary>
/// Network port.
/// </summary>
public int NetworkPort { get; set; }
public int? NetworkPort { get; set; }
/// <summary>
/// Node id.
/// </summary>
public Guid NodeId { get; set; }
public Guid? NodeId { get; set; }
/// <summary>
/// Availability.

View file

@ -0,0 +1,37 @@
// <copyright file="EntityHelpers.cs" company="alveus.dev">
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
namespace Astral.Services.Helpers;
/// <summary>
/// Entity helpers.
/// </summary>
public static class EntityHelpers
{
/// <summary>
/// Apply changes only when new entity property is different and not null.
/// </summary>
/// <param name="oldEntity">Old entity to compare against.</param>
/// <param name="newEntity">New entity with new values.</param>
/// <typeparam name="TEntity">The type of entity to compare.</typeparam>
/// <returns>The new entity with changes applied.</returns>
public static TEntity ApplyChanges<TEntity>(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;
}
}

View file

@ -2,6 +2,7 @@
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright>
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<UserLocationHeartbeatDto, UserPresence>()
.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));
}
}

View file

@ -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;
/// <summary>
/// Initializes a new instance of the <see cref="UserPresenceService"/> class.
@ -27,14 +30,17 @@ public class UserPresenceService : IUserPresenceService
/// <param name="identityProvider">Instance of <see cref="IIdentityProvider"/>.</param>
/// <param name="cryptographyService">Instance of <see cref="ICryptographyService"/>.</param>
/// <param name="userPresenceRepository">Instance of <see cref="IUserPresenceRepository"/>.</param>
/// <param name="mapper">Instance of <see cref="IMapper"/>.</param>
public UserPresenceService(
IIdentityProvider identityProvider,
ICryptographyService cryptographyService,
IUserPresenceRepository userPresenceRepository)
IUserPresenceRepository userPresenceRepository,
IMapper mapper)
{
_identityProvider = identityProvider;
_cryptographyService = cryptographyService;
_userPresenceRepository = userPresenceRepository;
_mapper = mapper;
}
/// <inheritdoc />
@ -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
/// <inheritdoc />
public async Task ConsumeLocationHeartbeat(UserLocationHeartbeatDto heartbeat)
{
throw new NotImplementedException();
var userId = _identityProvider.GetUserId();
if (userId == Guid.Empty)
{
throw new UnauthorizedException();
}
var newHeartbeat = _mapper.Map<UserPresence>(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);
}
/// <inheritdoc />