heartbeat and user presence #5
6 changed files with 154 additions and 12 deletions
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
37
Astral.Services/Helpers/EntityHelpers.cs
Normal file
37
Astral.Services/Helpers/EntityHelpers.cs
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
|
|
Loading…
Reference in a new issue