diff --git a/Govor.API/Controllers/OnlinePingingController.cs b/Govor.API/Controllers/OnlinePingingController.cs index 3f3a026..0a58dfc 100644 --- a/Govor.API/Controllers/OnlinePingingController.cs +++ b/Govor.API/Controllers/OnlinePingingController.cs @@ -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 _logger; private readonly IPingHandlerService _ping; - private readonly IUserPresenceService _presenceService; + private readonly IUserPresenceReader _presenceReader; + private readonly IOnlineUserStore _userOnlineStore; private readonly ICurrentUserService _currentUserService; public OnlinePingingController(ILogger logger, @@ -57,11 +58,17 @@ public class OnlinePingingController : Controller } [HttpGet("status/{userId}")] - public IActionResult GetStatus(Guid userId) + public async Task 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) { diff --git a/Govor.API/Extensions/ConfigurationProgramExtensions.cs b/Govor.API/Extensions/ConfigurationProgramExtensions.cs index d169afb..be0667d 100644 --- a/Govor.API/Extensions/ConfigurationProgramExtensions.cs +++ b/Govor.API/Extensions/ConfigurationProgramExtensions.cs @@ -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(); services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddSingleton(); + // Auto Mapper services.AddAutoMapper(typeof(MappingProfile)); } diff --git a/Govor.API/Hubs/PresenceHub.cs b/Govor.API/Hubs/PresenceHub.cs new file mode 100644 index 0000000..fc23170 --- /dev/null +++ b/Govor.API/Hubs/PresenceHub.cs @@ -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 _logger; + private readonly IUserNotificationScopeService _notificationScopeService; + private readonly IOnlineUserStore _onlineUserStore; + + public PresenceHub(ILogger 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; + } +} \ No newline at end of file diff --git a/Govor.Application.Tests/Services/UserOnlineStatus/UserNotificationScopeServiceTests.cs b/Govor.Application.Tests/Services/UserOnlineStatus/UserNotificationScopeServiceTests.cs new file mode 100644 index 0000000..b4cf4ef --- /dev/null +++ b/Govor.Application.Tests/Services/UserOnlineStatus/UserNotificationScopeServiceTests.cs @@ -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() + { + + } +} \ No newline at end of file diff --git a/Govor.Application/Interfaces/IUserPresenceService.cs b/Govor.Application/Interfaces/IUserPresenceService.cs deleted file mode 100644 index 38eea19..0000000 --- a/Govor.Application/Interfaces/IUserPresenceService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Govor.Application.Interfaces; - -public interface IUserPresenceService -{ - DateTime WhenUserWasOnline(Guid userId); -} \ No newline at end of file diff --git a/Govor.Application/Interfaces/UserOnlineStatus/IOnlineUserStore.cs b/Govor.Application/Interfaces/UserOnlineStatus/IOnlineUserStore.cs new file mode 100644 index 0000000..65c4030 --- /dev/null +++ b/Govor.Application/Interfaces/UserOnlineStatus/IOnlineUserStore.cs @@ -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 GetAllOnlineUsers(); +} \ No newline at end of file diff --git a/Govor.Application/Interfaces/UserOnlineStatus/IUserNotificationScopeService.cs b/Govor.Application/Interfaces/UserOnlineStatus/IUserNotificationScopeService.cs new file mode 100644 index 0000000..0ac93a0 --- /dev/null +++ b/Govor.Application/Interfaces/UserOnlineStatus/IUserNotificationScopeService.cs @@ -0,0 +1,6 @@ +namespace Govor.Application.Interfaces.UserOnlineStatus; + +public interface IUserNotificationScopeService +{ + Task> GetNotifiedUsers(Guid userId); +} \ No newline at end of file diff --git a/Govor.Application/Interfaces/UserOnlineStatus/IUserPresenceReader.cs b/Govor.Application/Interfaces/UserOnlineStatus/IUserPresenceReader.cs new file mode 100644 index 0000000..5291ccb --- /dev/null +++ b/Govor.Application/Interfaces/UserOnlineStatus/IUserPresenceReader.cs @@ -0,0 +1,6 @@ +namespace Govor.Application.Interfaces.UserOnlineStatus; + +public interface IUserPresenceReader +{ + Task GetLastSeenAsync(Guid userId); +} \ No newline at end of file diff --git a/Govor.Application/Services/UserOnlineStatus/OnlineUserStore.cs b/Govor.Application/Services/UserOnlineStatus/OnlineUserStore.cs new file mode 100644 index 0000000..a3813b9 --- /dev/null +++ b/Govor.Application/Services/UserOnlineStatus/OnlineUserStore.cs @@ -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 GetAllOnlineUsers() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Govor.Application/Services/UserOnlineStatus/UserNotificationScopeService.cs b/Govor.Application/Services/UserOnlineStatus/UserNotificationScopeService.cs new file mode 100644 index 0000000..5435f49 --- /dev/null +++ b/Govor.Application/Services/UserOnlineStatus/UserNotificationScopeService.cs @@ -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 _logger; + private readonly IFriendshipService _friendships; + + public UserNotificationScopeService(ILogger logger, IFriendshipService friendships) + { + _logger = logger; + _friendships = friendships; + } + + public async Task> 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 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 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"); + } + }*/ +} \ No newline at end of file diff --git a/Govor.Application/Services/UserOnlineStatus/UserPresenceReader.cs b/Govor.Application/Services/UserOnlineStatus/UserPresenceReader.cs new file mode 100644 index 0000000..aeab7ab --- /dev/null +++ b/Govor.Application/Services/UserOnlineStatus/UserPresenceReader.cs @@ -0,0 +1,11 @@ +using Govor.Application.Interfaces.UserOnlineStatus; + +namespace Govor.Application.Services.UserOnlineStatus; + +public class UserPresenceReader : IUserPresenceReader +{ + public Task GetLastSeenAsync(Guid userId) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Govor.Contracts/DTOs/UserDto.cs b/Govor.Contracts/DTOs/UserDto.cs index 06b00cf..c89db9d 100644 --- a/Govor.Contracts/DTOs/UserDto.cs +++ b/Govor.Contracts/DTOs/UserDto.cs @@ -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; } } \ No newline at end of file diff --git a/Govor.Core/Models/Users/PrivacyUserSettings.cs b/Govor.Core/Models/Users/PrivacyUserSettings.cs index 9997c9f..001c38d 100644 --- a/Govor.Core/Models/Users/PrivacyUserSettings.cs +++ b/Govor.Core/Models/Users/PrivacyUserSettings.cs @@ -34,7 +34,8 @@ public enum PrivacyTargetArea { CanSend = 0, CanSeeTimeWas = 1, - CanSeeImage = 2 + CanSeeImage = 2, + CanSendImage = 3, } public enum PrivacyRuleType