using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using System.Text.Json; using RadicadeServer.DB; using RadicadeServer.Extensions; using RadicadeServer.RecNet.Accounts; using RadicadeServer.RecNet.Notifications; using RadicadeServer.RecNet.Enums.Messages; using RadicadeServer.Utils; namespace RadicadeServer.API.Notifications { public class NotifyHub : Hub { private static readonly string[] TokenCarriers = { "access_token", "accessToken", "token", "id_token" }; private RecDBContext db; public NotifyHub(IHubContext hubContext, RecDBContext db) { Notify.Context = hubContext; this.db = db; } private string? RawToken() { var httpContext = Context.GetHttpContext(); if (httpContext == null) return null; foreach (var name in TokenCarriers) { var fromQuery = httpContext.Request.Query[name].ToString(); if (!string.IsNullOrWhiteSpace(fromQuery)) return fromQuery; } var header = httpContext.Request.Headers["Authorization"].ToString(); if (!string.IsNullOrWhiteSpace(header)) return header; foreach (var name in TokenCarriers) { if (httpContext.Request.Cookies.TryGetValue(name, out var cookie) && !string.IsNullOrWhiteSpace(cookie)) return cookie; } return null; } private long? GetAccountId() { var authenticatedAccount = Context.User?.GetUserId(); if (authenticatedAccount != null) return authenticatedAccount; var raw = RawToken(); return string.IsNullOrWhiteSpace(raw) ? null : raw.GetUserId(); } private static JsonElement RawJson(string json) => JsonDocument.Parse(json).RootElement.Clone(); private async Task SendNotificationToCaller(string id, object msg) { var notification = new Notification { Id = id, Msg = msg }; await Clients.Caller.SendAsync("Notification", notification.ToJson()); } private static List ReadPlayerIds(object? update) { var ids = new List(); void Add(JsonElement element) { if (element.ValueKind == JsonValueKind.Number && element.TryGetInt64(out var single)) { ids.Add(single); return; } if (element.ValueKind == JsonValueKind.String && long.TryParse(element.GetString(), out single)) { ids.Add(single); return; } if (element.ValueKind != JsonValueKind.Array) return; foreach (var entry in element.EnumerateArray()) { if (entry.ValueKind == JsonValueKind.Number && entry.TryGetInt64(out var value)) ids.Add(value); else if (entry.ValueKind == JsonValueKind.String && long.TryParse(entry.GetString(), out value)) ids.Add(value); } } try { if (update is SubscriptionUpdate subscription && subscription.PlayerIds != null) { ids.AddRange(subscription.PlayerIds); return ids; } if (update is not JsonElement json) return ids; if (json.ValueKind == JsonValueKind.Array) { Add(json); return ids; } if (json.ValueKind != JsonValueKind.Object) return ids; foreach (var property in json.EnumerateObject()) { if (!property.Name.Equals("PlayerIds", StringComparison.OrdinalIgnoreCase)) continue; Add(property.Value); } } catch { } return ids; } [HubMethodName("SubscribeToPlayers")] public Task Subscribe(object? update) => ApplySubscriptions(update); [HubMethodName("playerSubscriptions/v1/update")] public Task SubscribeToPlayersV1(object? update) => ApplySubscriptions(update); private async Task ApplySubscriptions(object? update) { long? account = GetAccountId(); if (account == null) return; var ids = ReadPlayerIds(update); var client = Notify.GetConnectionClient(Context.ConnectionId); if (client != null) { lock (client.SubscriptionsLock) { client.Subscriptions = ids.Where(id => id > 0).Distinct().ToList(); client.HasSubscriptions = true; } await Notify.UpdateSubscriptionsForClient(Context.ConnectionId); } } [HubMethodName("SubscribeTo")] public Task SubscribeToChannels(object? update) => ApplyChannelSubscriptions(update, subscribing: true); [HubMethodName("UnsubscribeFrom")] public Task UnsubscribeFromChannels(object? update) => ApplyChannelSubscriptions(update, subscribing: false); private Task ApplyChannelSubscriptions(object? update, bool subscribing) { var channels = ReadChannels(update); if (channels.Count == 0) return Task.CompletedTask; return Task.CompletedTask; } private static List ReadChannels(object? update) { var channels = new List(); try { if (update is not JsonElement json || json.ValueKind != JsonValueKind.Object) return channels; foreach (var property in json.EnumerateObject()) { if (!property.Name.Equals("Channels", StringComparison.OrdinalIgnoreCase)) continue; if (property.Value.ValueKind != JsonValueKind.Array) continue; foreach (var entry in property.Value.EnumerateArray()) { var channel = entry.ValueKind == JsonValueKind.String ? entry.GetString() : null; if (!string.IsNullOrWhiteSpace(channel)) channels.Add(channel!); } } } catch { } return channels; } [HubMethodName("heartbeat2")] public Task Heartbeat() => AnswerHeartbeat(); [HubMethodName("heartbeat")] public Task HeartbeatUnversioned() => AnswerHeartbeat(); private async Task AnswerHeartbeat() { var account = GetAccountId(); if (account == null) return null; var presence = await Notify.BuildSelfPresenceAsync(db, account.Value); if (presence == null) return null; await SendNotificationToCaller("PresenceUpdate", presence); return RawJson($"{{\"Id\":{account.Value},\"Msg\":{presence.ToJson()}}}"); } public override async Task OnConnectedAsync() { var httpContext = Context.GetHttpContext(); long? account = GetAccountId(); var tokenCarrier = string.IsNullOrWhiteSpace(RawToken()) ? "none" : "present"; if (account != null) { Notify.RegisterContext(Context); lock (Notify.ClientsLock) Notify.Clients.Add(new HubClient { AccountId = account.Value, ConnectionId = Context.ConnectionId }); var activePresence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == account.Value); if (activePresence != null && !activePresence.isOnline && activePresence.roomInstance.HasValue) { activePresence.isOnline = true; await db.SaveChangesAsync(); await Notify.UpdateSubscriptions(account.Value); } try { await Notify.SendSelfAccountUpdate(account.Value); } catch (Exception) { } try { var presence = await Notify.BuildSelfPresenceAsync(db, account.Value); if (presence != null) await SendNotificationToCaller("PresenceUpdate", presence); } catch (Exception) { } try { var ban = await db.ResolveBanAsync(account.Value); if (ban != null) { var notification = new Notification { Id = NotificationType.ModerationKick, Msg = await PlayerUtils.BuildBanBlockDetailsAsync(db, ban, account.Value) }; await Clients.Caller.SendAsync("Notification", notification.ToJson()); } } catch (Exception) { } try { var pendingWarning = await db.GetPendingWarningAsync(account.Value); if (pendingWarning != null) { await PlayerUtils.DeliverWarningAsync(account.Value, PlayerUtils.BuildWarningBlockDetails(pendingWarning, (int)PlayerUtils.WarningScreenHold.TotalSeconds)); } } catch (Exception) { } } await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception? exception) { var userId = GetAccountId(); lock (Notify.ClientsLock) Notify.Clients.RemoveAll(x => x.ConnectionId == Context.ConnectionId); Notify.ForgetContext(Context.ConnectionId); if (userId != null) { bool stillConnected; lock (Notify.ClientsLock) stillConnected = Notify.Clients.Any(x => x.AccountId == userId.Value); if (!stillConnected) { var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == userId.Value); lock (Notify.ClientsLock) stillConnected = Notify.Clients.Any(x => x.AccountId == userId.Value); if (presence != null && !stillConnected) { presence.isOnline = false; await db.SaveChangesAsync(); await Notify.UpdateSubscriptions(userId.Value); } } } await base.OnDisconnectedAsync(exception); } } }