Add user online presence tracking and notification

Introduced interfaces and services for tracking user online status, including IOnlineUserStore, IUserNotificationScopeService, and IUserPresenceReader. Added PresenceHub for real-time presence updates via SignalR. Updated OnlinePingingController to use new services and return online status and last seen. Extended UserDto with IsOnline property. Updated dependency injection and privacy settings enum. Removed obsolete IUserPresenceService.
This commit is contained in:
Artemy
2025-07-23 21:07:29 +07:00
parent cbff5857e7
commit 31fdf4cb37
13 changed files with 231 additions and 11 deletions
@@ -1,6 +1,6 @@
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Core.Repositories.Users;
using Govor.Application.Interfaces.UserOnlineStatus;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -13,7 +13,8 @@ public class OnlinePingingController : Controller
{
private readonly ILogger<OnlinePingingController> _logger;
private readonly IPingHandlerService _ping;
private readonly IUserPresenceService _presenceService;
private readonly IUserPresenceReader _presenceReader;
private readonly IOnlineUserStore _userOnlineStore;
private readonly ICurrentUserService _currentUserService;
public OnlinePingingController(ILogger<OnlinePingingController> logger,
@@ -57,11 +58,17 @@ public class OnlinePingingController : Controller
}
[HttpGet("status/{userId}")]
public IActionResult GetStatus(Guid userId)
public async Task<IActionResult> GetStatus(Guid userId)
{
try
{
return Ok(_presenceService.WhenUserWasOnline(userId));
var isOnline = _userOnlineStore.IsOnline(userId);
var lastSeen = await _presenceReader.GetLastSeenAsync(userId);
return Ok(new {
isOnline,
lastSeen
});
}
catch (Exception e)
{
@@ -7,12 +7,14 @@ using Govor.Application.Interfaces.Friends;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Application.Interfaces.Medias;
using Govor.Application.Interfaces.Messages;
using Govor.Application.Interfaces.UserOnlineStatus;
using Govor.Application.Interfaces.UserSession;
using Govor.Application.Services;
using Govor.Application.Services.Authentication;
using Govor.Application.Services.Friends;
using Govor.Application.Services.Medias;
using Govor.Application.Services.Messages;
using Govor.Application.Services.UserOnlineStatus;
using Govor.Application.Services.UserSessions;
using Govor.Core.Infrastructure.Extensions;
using Govor.Core.Infrastructure.Validators;
@@ -74,6 +76,11 @@ public static class ConfigurationProgramExtensions
// UserSession
services.AddScoped<IUserSessionOpener, UserSessionOpener>();
services.AddScoped<IUserSessionRefresher, UserSessionRefresher>();
services.AddScoped<IUserNotificationScopeService, UserNotificationScopeService>();
services.AddScoped<IUserPresenceReader, UserPresenceReader>();
services.AddSingleton<IOnlineUserStore, OnlineUserStore>();
// Auto Mapper
services.AddAutoMapper(typeof(MappingProfile));
}
+83
View File
@@ -0,0 +1,83 @@
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Infrastructure.Extensions;
using Govor.Application.Interfaces.UserOnlineStatus;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.SignalR;
namespace Govor.API.Hubs;
[Authorize(Roles = "Admin, User")]
[Route("hubs/presence")]
public class PresenceHub : Hub
{
private readonly ILogger<PresenceHub> _logger;
private readonly IUserNotificationScopeService _notificationScopeService;
private readonly IOnlineUserStore _onlineUserStore;
public PresenceHub(ILogger<PresenceHub> logger, IUserNotificationScopeService notificationScopeService, IOnlineUserStore onlineUserStore)
{
_logger = logger;
_notificationScopeService = notificationScopeService;
_onlineUserStore = onlineUserStore;
}
public override async Task OnConnectedAsync()
{
var userId = GetUserId();
if (userId == Guid.Empty)
{
_logger.LogWarning("User connected with invalid UserID claim.");
Context.Abort();
return;
}
_onlineUserStore.SetOnlineUser(userId);
await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString());
var friends = await _notificationScopeService.GetNotifiedUsers(userId);
foreach (var recipient in friends)
{
await Clients.Group(recipient.ToString())
.SendAsync("UserOnline", userId);
}
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
var userId = GetUserId();
if (userId == Guid.Empty) return;
_onlineUserStore.SetOfflineUser(userId);
var friends = await _notificationScopeService.GetNotifiedUsers(userId);
foreach (var recipient in friends)
{
await Clients.Group(recipient.ToString())
.SendAsync("UserOffline", userId);
}
await base.OnDisconnectedAsync(exception);
}
private Guid GetUserId(bool suppressException = false)
{
var userIdClaim = Context.User?.FindFirst("userId")?.Value;
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
{
if (!suppressException)
{
_logger.LogError("Could not retrieve sender userId. Claim was: {UserIDClaim}", userIdClaim);
throw new UnauthorizedAccessException("userId claim is missing or invalid.");
}
return Guid.Empty;
}
return userId;
}
}
@@ -0,0 +1,15 @@
using Govor.Application.Services.UserOnlineStatus;
namespace Govor.Application.Tests.Services.UserOnlineStatus;
[TestFixture]
[TestOf(typeof(UserNotificationScopeService))]
public class UserNotificationScopeServiceTests
{
[Test]
public void METHOD()
{
}
}
@@ -1,6 +0,0 @@
namespace Govor.Application.Interfaces;
public interface IUserPresenceService
{
DateTime WhenUserWasOnline(Guid userId);
}
@@ -0,0 +1,9 @@
namespace Govor.Application.Interfaces.UserOnlineStatus;
public interface IOnlineUserStore
{
void SetOnlineUser(Guid userId);
void SetOfflineUser(Guid userId);
bool IsOnline(Guid userId);
IReadOnlyCollection<Guid> GetAllOnlineUsers();
}
@@ -0,0 +1,6 @@
namespace Govor.Application.Interfaces.UserOnlineStatus;
public interface IUserNotificationScopeService
{
Task<List<Guid>> GetNotifiedUsers(Guid userId);
}
@@ -0,0 +1,6 @@
namespace Govor.Application.Interfaces.UserOnlineStatus;
public interface IUserPresenceReader
{
Task<DateTime?> GetLastSeenAsync(Guid userId);
}
@@ -0,0 +1,26 @@
using Govor.Application.Interfaces.UserOnlineStatus;
namespace Govor.Application.Services.UserOnlineStatus;
public class OnlineUserStore : IOnlineUserStore
{
public void SetOnlineUser(Guid userId)
{
throw new NotImplementedException();
}
public void SetOfflineUser(Guid userId)
{
throw new NotImplementedException();
}
public bool IsOnline(Guid userId)
{
throw new NotImplementedException();
}
public IReadOnlyCollection<Guid> GetAllOnlineUsers()
{
throw new NotImplementedException();
}
}
@@ -0,0 +1,54 @@
using Govor.Application.Interfaces.Friends;
using Govor.Application.Interfaces.UserOnlineStatus;
using Govor.Data.Repositories.Exceptions;
using Microsoft.Extensions.Logging;
namespace Govor.Application.Services.UserOnlineStatus;
public class UserNotificationScopeService : IUserNotificationScopeService
{
private readonly ILogger<UserNotificationScopeService> _logger;
private readonly IFriendshipService _friendships;
public UserNotificationScopeService(ILogger<UserNotificationScopeService> logger, IFriendshipService friendships)
{
_logger = logger;
_friendships = friendships;
}
public async Task<List<Guid>> GetNotifiedUsers(Guid userId)
{
try
{
_logger.LogInformation($"Getting notified users of online/offline action user {userId}");
var users = await _friendships.GetFriendsAsync(userId);
return users.Select(u => u.Id).ToList();
}
catch (NotFoundByKeyException<Guid> ex)
{
_logger.LogError(ex, ex.Message);
throw new InvalidOperationException("User not found");
}
}
/*public async Task SetWasOnlineAsync(Guid userId, DateTime when)
{
try
{
_logger.LogInformation("Set was-online for user {UserId}", userId);
var user = await _users.FindByIdAsync(userId);
user.WasOnline = when;
await _users.UpdateAsync(user);
}
catch (NotFoundByKeyException<Guid> ex)
{
_logger.LogError(ex, ex.Message);
throw new InvalidOperationException("User not found");
}
catch (UpdateException ex)
{
_logger.LogError(ex, "Failed to set WasOnline for user {UserId} at {Time}", userId, when);
throw new InvalidOperationException("Something went wrong when trying to set was-online for user");
}
}*/
}
@@ -0,0 +1,11 @@
using Govor.Application.Interfaces.UserOnlineStatus;
namespace Govor.Application.Services.UserOnlineStatus;
public class UserPresenceReader : IUserPresenceReader
{
public Task<DateTime?> GetLastSeenAsync(Guid userId)
{
throw new NotImplementedException();
}
}
+1
View File
@@ -7,4 +7,5 @@ public class UserDto
public string Description { get; set; }
public DateTime WasOnline { get; set; }
public Guid IconId {get; set;}
public bool IsOnline { get; set; }
}
@@ -34,7 +34,8 @@ public enum PrivacyTargetArea
{
CanSend = 0,
CanSeeTimeWas = 1,
CanSeeImage = 2
CanSeeImage = 2,
CanSendImage = 3,
}
public enum PrivacyRuleType