using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using PlotLine.Models; using PlotLine.Services; using System.Security.Claims; namespace PlotLine.Hubs; [Authorize] public sealed class WordCompanionFollowHub(IWordCompanionPresenceService presence) : Hub { public override async Task OnConnectedAsync() { if (TryGetUserId(out var userId)) { await Groups.AddToGroupAsync(Context.ConnectionId, PresenceGroup(userId)); } await base.OnConnectedAsync(); } public async Task WatchCompanionPresence() { var userId = RequireUserId(); await Groups.AddToGroupAsync(Context.ConnectionId, PresenceGroup(userId)); return await presence.GetStatusAsync(userId); } public async Task RegisterCompanion(WordCompanionPresenceRegistration registration) { var userId = RequireUserId(); if (registration.UserID.HasValue && registration.UserID.Value != userId) { throw new HubException("The Companion is signed in as a different user."); } var status = await presence.RegisterAsync(userId, Context.ConnectionId, registration); await Clients.Group(PresenceGroup(userId)).SendAsync("WordCompanionPresenceChanged", status); return status; } public async Task CompanionHeartbeat(WordCompanionPresenceHeartbeat heartbeat) { var userId = RequireUserId(); var status = await presence.HeartbeatAsync(userId, Context.ConnectionId, heartbeat); await Clients.Group(PresenceGroup(userId)).SendAsync("WordCompanionPresenceChanged", status); return status; } public override async Task OnDisconnectedAsync(Exception? exception) { var status = await presence.DisconnectAsync(Context.ConnectionId); if (status is not null) { await Clients.Group(PresenceGroup(status.UserID)).SendAsync("WordCompanionPresenceChanged", status); } await base.OnDisconnectedAsync(exception); } private int RequireUserId() => TryGetUserId(out var userId) ? userId : throw new HubException("Sign in to use the Word Companion."); private bool TryGetUserId(out int userId) => int.TryParse(Context.User?.FindFirstValue(ClaimTypes.NameIdentifier), out userId); private static string PresenceGroup(int userId) => $"word-companion-presence:{userId}"; }