feat: implement notification sound functionality with user customization options

This commit is contained in:
HueByte
2026-02-19 22:03:16 +01:00
parent b508a5854a
commit 7a8b5f02d3
15 changed files with 236 additions and 35 deletions
@@ -72,13 +72,17 @@ public class ModerationController : ControllerBase
if (target.Role >= caller!.Role)
return BadRequest(new ErrorResponse("Cannot kick a user with equal or higher role."));
// Broadcast kick to all channels the user is in
// Broadcast kick to all channels the user is in, then clean up presence
var channels = _presenceTracker.GetChannelsForUser(target.Username);
foreach (var channel in channels)
{
await BroadcastToAllAsync(b => b.SendUserKickedAsync(channel, target.Username, request?.Reason));
}
// Remove from presence tracker and force disconnect all connections
var reason = request?.Reason ?? "You have been kicked from the server.";
await ForceDisconnectAndCleanupAsync(target.Username, reason);
return Ok(new { Message = $"{target.Username} has been kicked." });
}
@@ -98,8 +102,12 @@ public class ModerationController : ControllerBase
target.IsBanned = true;
await _db.SaveChangesAsync();
// Broadcast ban notification, then force disconnect
await BroadcastToAllAsync(b => b.SendUserBannedAsync(target.Username, request?.Reason));
var reason = request?.Reason ?? "You have been banned from this server.";
await ForceDisconnectAndCleanupAsync(target.Username, reason);
return Ok(new { Message = $"{target.Username} has been banned." });
}
@@ -217,6 +225,36 @@ public class ModerationController : ControllerBase
return (caller, null);
}
/// <summary>
/// Remove user from presence tracking, broadcast their departure from all channels,
/// send a ForceDisconnect signal, and update their DB status.
/// </summary>
private async Task ForceDisconnectAndCleanupAsync(string username, string reason)
{
var (connectionIds, channels) = _presenceTracker.ForceRemoveUser(username);
// Notify remaining users that this person left each channel
foreach (var channel in channels)
{
await BroadcastToAllAsync(b => b.SendUserLeftAsync(channel, username));
}
// Signal the user's clients to disconnect
if (connectionIds.Count > 0)
{
await BroadcastToAllAsync(b => b.ForceDisconnectUserAsync(connectionIds, reason));
}
// Mark user offline in DB
var user = await _db.Users.FirstOrDefaultAsync(u => u.Username == username);
if (user is not null)
{
user.Status = UserStatus.Invisible;
user.LastSeenAt = DateTimeOffset.UtcNow;
await _db.SaveChangesAsync();
}
}
private async Task BroadcastToAllAsync(Func<IChatBroadcaster, Task> action)
{
foreach (var broadcaster in _broadcasters)
@@ -151,4 +151,27 @@ public class PresenceTracker
{
return _userConnections.Count;
}
/// <summary>
/// Forcibly remove a user from all tracking. Returns their connection IDs and channels
/// so the caller can broadcast departures and force-disconnect connections.
/// </summary>
public (List<string> ConnectionIds, List<string> Channels) ForceRemoveUser(string username)
{
lock (_lock)
{
var channels = _userChannels.TryRemove(username, out var ch)
? ch.ToList()
: [];
var connectionIds = _userConnections.TryRemove(username, out var conns)
? conns.ToList()
: [];
foreach (var connId in connectionIds)
_connections.TryRemove(connId, out _);
return (connectionIds, channels);
}
}
}
@@ -73,4 +73,13 @@ public class SignalRBroadcaster : IChatBroadcaster
return HubContext.Clients.Client(connectionId).Error(message);
}
public Task ForceDisconnectUserAsync(List<string> connectionIds, string reason)
{
var signalRIds = connectionIds.Where(c => !c.StartsWith("irc-")).ToList();
if (signalRIds.Count == 0)
return Task.CompletedTask;
return HubContext.Clients.Clients(signalRIds).ForceDisconnect(reason);
}
}