Call user presence
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful

This commit is contained in:
Mike 2024-12-15 20:20:27 +00:00
parent 81aa0ec1c0
commit d84d7631ce
6 changed files with 154 additions and 12 deletions

View file

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

View file

@ -3,6 +3,7 @@
// </copyright> // </copyright>
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Astral.Services.Dtos;
namespace Astral.ApiServer.Models; namespace Astral.ApiServer.Models;
@ -15,7 +16,7 @@ public class HeartbeatModel
/// True if connected. /// True if connected.
/// </summary> /// </summary>
[JsonPropertyName("connected")] [JsonPropertyName("connected")]
public bool Connected { get; set; } public bool? Connected { get; set; }
/// <summary> /// <summary>
/// Path. /// Path.
@ -27,13 +28,13 @@ public class HeartbeatModel
/// Domain id. /// Domain id.
/// </summary> /// </summary>
[JsonPropertyName("domain_id")] [JsonPropertyName("domain_id")]
public Guid DomainId { get; set; } public string DomainId { get; set; }
/// <summary> /// <summary>
/// Place id. /// Place id.
/// </summary> /// </summary>
[JsonPropertyName("place_id")] [JsonPropertyName("place_id")]
public Guid PlaceId { get; set; } public string PlaceId { get; set; }
/// <summary> /// <summary>
/// Network address. /// Network address.
@ -45,17 +46,71 @@ public class HeartbeatModel
/// Network port. /// Network port.
/// </summary> /// </summary>
[JsonPropertyName("network_port")] [JsonPropertyName("network_port")]
public int NetworkPort { get; set; } public int? NetworkPort { get; set; }
/// <summary> /// <summary>
/// Node id. /// Node id.
/// </summary> /// </summary>
[JsonPropertyName("node_id")] [JsonPropertyName("node_id")]
public Guid NodeId { get; set; } public string NodeId { get; set; }
/// <summary> /// <summary>
/// Availability. /// Availability.
/// </summary> /// </summary>
[JsonPropertyName("availability")] [JsonPropertyName("availability")]
public string Availability { get; set; } 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> /// <summary>
/// True if connected. /// True if connected.
/// </summary> /// </summary>
public bool Connected { get; set; } public bool? Connected { get; set; }
/// <summary> /// <summary>
/// Path. /// Path.
@ -22,12 +22,12 @@ public class UserLocationHeartbeatDto
/// <summary> /// <summary>
/// Domain id. /// Domain id.
/// </summary> /// </summary>
public Guid DomainId { get; set; } public Guid? DomainId { get; set; }
/// <summary> /// <summary>
/// Place id. /// Place id.
/// </summary> /// </summary>
public Guid PlaceId { get; set; } public Guid? PlaceId { get; set; }
/// <summary> /// <summary>
/// Network address. /// Network address.
@ -37,12 +37,12 @@ public class UserLocationHeartbeatDto
/// <summary> /// <summary>
/// Network port. /// Network port.
/// </summary> /// </summary>
public int NetworkPort { get; set; } public int? NetworkPort { get; set; }
/// <summary> /// <summary>
/// Node id. /// Node id.
/// </summary> /// </summary>
public Guid NodeId { get; set; } public Guid? NodeId { get; set; }
/// <summary> /// <summary>
/// Availability. /// 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 (c) alveus.dev. All rights reserved. Licensed under the MIT License.
// </copyright> // </copyright>
using Astral.Core.Constants;
using Astral.Core.Entities; using Astral.Core.Entities;
using Astral.Services.Dtos; using Astral.Services.Dtos;
using AutoMapper; using AutoMapper;
@ -31,5 +32,20 @@ public class AutomapperProfile : Profile
.ForMember(dest => dest.Title, opt => opt.MapFrom(src => src.Title)) .ForMember(dest => dest.Title, opt => opt.MapFrom(src => src.Title))
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description)) .ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description))
.ForMember(dest => dest.Internal, opt => opt.MapFrom(src => src.Internal)); .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.Constants;
using Astral.Services.Dtos; using Astral.Services.Dtos;
using Astral.Services.Exceptions; using Astral.Services.Exceptions;
using Astral.Services.Helpers;
using Astral.Services.Interfaces; using Astral.Services.Interfaces;
using AutoMapper;
using Injectio.Attributes; using Injectio.Attributes;
namespace Astral.Services.Services; namespace Astral.Services.Services;
@ -20,6 +22,7 @@ public class UserPresenceService : IUserPresenceService
private readonly IIdentityProvider _identityProvider; private readonly IIdentityProvider _identityProvider;
private readonly ICryptographyService _cryptographyService; private readonly ICryptographyService _cryptographyService;
private readonly IUserPresenceRepository _userPresenceRepository; private readonly IUserPresenceRepository _userPresenceRepository;
private readonly IMapper _mapper;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="UserPresenceService"/> class. /// 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="identityProvider">Instance of <see cref="IIdentityProvider"/>.</param>
/// <param name="cryptographyService">Instance of <see cref="ICryptographyService"/>.</param> /// <param name="cryptographyService">Instance of <see cref="ICryptographyService"/>.</param>
/// <param name="userPresenceRepository">Instance of <see cref="IUserPresenceRepository"/>.</param> /// <param name="userPresenceRepository">Instance of <see cref="IUserPresenceRepository"/>.</param>
/// <param name="mapper">Instance of <see cref="IMapper"/>.</param>
public UserPresenceService( public UserPresenceService(
IIdentityProvider identityProvider, IIdentityProvider identityProvider,
ICryptographyService cryptographyService, ICryptographyService cryptographyService,
IUserPresenceRepository userPresenceRepository) IUserPresenceRepository userPresenceRepository,
IMapper mapper)
{ {
_identityProvider = identityProvider; _identityProvider = identityProvider;
_cryptographyService = cryptographyService; _cryptographyService = cryptographyService;
_userPresenceRepository = userPresenceRepository; _userPresenceRepository = userPresenceRepository;
_mapper = mapper;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -52,6 +58,7 @@ public class UserPresenceService : IUserPresenceService
{ {
heartbeat = new UserPresence() heartbeat = new UserPresence()
{ {
CreatedAt = DateTime.UtcNow,
Id = userId, Id = userId,
LastHeartbeat = DateTime.UtcNow LastHeartbeat = DateTime.UtcNow
}; };
@ -68,7 +75,27 @@ public class UserPresenceService : IUserPresenceService
/// <inheritdoc /> /// <inheritdoc />
public async Task ConsumeLocationHeartbeat(UserLocationHeartbeatDto heartbeat) 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 /> /// <inheritdoc />