Files
2026-10-16 23:33:13 -07:00

303 lines
8.3 KiB
C#

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<NotifyHub> 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<long> ReadPlayerIds(object? update)
{
var ids = new List<long>();
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<string> ReadChannels(object? update)
{
var channels = new List<string>();
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<object?> Heartbeat() => AnswerHeartbeat();
[HubMethodName("heartbeat")]
public Task<object?> HeartbeatUnversioned() => AnswerHeartbeat();
private async Task<object?> 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);
}
}
}