mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,482 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using System.Globalization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.Json;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Accounts;
|
||||
using RadicadeServer.RecNet.Auth;
|
||||
using RadicadeServer.RecNet.Enums.Messages;
|
||||
using RadicadeServer.RecNet.Messages;
|
||||
using RadicadeServer.RecNet.Notifications;
|
||||
using RadicadeServer.RecNet.Rooms;
|
||||
using RadicadeServer.Utils;
|
||||
|
||||
namespace RadicadeServer.API.Notifications
|
||||
{
|
||||
public static class Notify
|
||||
{
|
||||
public static IDbContextFactory<RecDBContext> DbFactory = null!;
|
||||
public static IHubContext<NotifyHub> Context = null!;
|
||||
public static List<HubClient> Clients = new List<HubClient>();
|
||||
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, HubCallerContext> _contexts = new();
|
||||
|
||||
public static void RegisterContext(HubCallerContext context)
|
||||
{
|
||||
_contexts[context.ConnectionId] = context;
|
||||
}
|
||||
|
||||
public static void ForgetContext(string connectionId)
|
||||
{
|
||||
_contexts.TryRemove(connectionId, out _);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public static readonly object ClientsLock = new();
|
||||
|
||||
public static HubClient? GetClient(long accountId)
|
||||
{
|
||||
lock (ClientsLock)
|
||||
return Clients.FirstOrDefault(n => n.AccountId == accountId);
|
||||
}
|
||||
|
||||
public static HubClient? GetConnectionClient(string connectionId)
|
||||
{
|
||||
lock (ClientsLock)
|
||||
return Clients.FirstOrDefault(n => n.ConnectionId == connectionId);
|
||||
}
|
||||
|
||||
public static int ConnectedConnectionCount
|
||||
{
|
||||
get { lock (ClientsLock) return Clients.Count; }
|
||||
}
|
||||
|
||||
public static List<long> ConnectedAccountIds()
|
||||
{
|
||||
lock (ClientsLock)
|
||||
return Clients.Select(n => n.AccountId).Distinct().ToList();
|
||||
}
|
||||
|
||||
public static List<HubClient> SnapshotClients()
|
||||
{
|
||||
lock (ClientsLock)
|
||||
{
|
||||
return Clients.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SendMessage(long TargetId, Message msg, string? idFormOverride = null,
|
||||
Action<Message>? prepareWithId = null)
|
||||
{
|
||||
if (DbFactory == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
msg.SentTime = DateTime.UtcNow;
|
||||
msg.ToPlayerId = TargetId;
|
||||
|
||||
var originalId = msg.Id;
|
||||
await ctx.Database.CreateExecutionStrategy().ExecuteAsync(async () =>
|
||||
{
|
||||
await using var attempt = await DbFactory.CreateDbContextAsync();
|
||||
msg.Id = originalId;
|
||||
await using var transaction = prepareWithId != null && attempt.Database.IsRelational()
|
||||
? await attempt.Database.BeginTransactionAsync() : null;
|
||||
await attempt.Messages.AddAsync(msg);
|
||||
await attempt.SaveChangesAsync();
|
||||
if (prepareWithId != null)
|
||||
{
|
||||
prepareWithId(msg);
|
||||
await attempt.SaveChangesAsync();
|
||||
}
|
||||
if (transaction != null) await transaction.CommitAsync();
|
||||
});
|
||||
|
||||
List<HubClient> clients;
|
||||
lock (ClientsLock)
|
||||
clients = Notify.Clients.Where(n => n.AccountId == TargetId).ToList();
|
||||
|
||||
var notif = new Notification { Id = NotificationType.MessageReceived, Msg = msg };
|
||||
|
||||
if (clients.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var client in clients)
|
||||
await SendToConnection(client.ConnectionId, notif, idFormOverride);
|
||||
}
|
||||
|
||||
public static async Task<int> SendUntilGoneAsync(long accountId, Notification message, TimeSpan window, TimeSpan interval, Notification? alternate = null)
|
||||
{
|
||||
if (window <= TimeSpan.Zero) window = TimeSpan.FromSeconds(8);
|
||||
if (interval <= TimeSpan.Zero) interval = TimeSpan.FromSeconds(1);
|
||||
|
||||
var deadline = DateTime.UtcNow + window;
|
||||
var pushes = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
List<HubClient> clients;
|
||||
lock (ClientsLock)
|
||||
clients = Clients.Where(n => n.AccountId == accountId).ToList();
|
||||
|
||||
if (clients.Count == 0)
|
||||
{
|
||||
return pushes;
|
||||
}
|
||||
|
||||
pushes++;
|
||||
|
||||
var frame = alternate != null && pushes % 2 == 0 ? alternate : message;
|
||||
foreach (var client in clients)
|
||||
await SendToConnection(client.ConnectionId, frame);
|
||||
|
||||
if (DateTime.UtcNow >= deadline)
|
||||
{
|
||||
return pushes;
|
||||
}
|
||||
|
||||
await Task.Delay(interval);
|
||||
}
|
||||
}
|
||||
|
||||
private static string PayloadProbe(Notification frame)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (frame.Msg == null) return "null";
|
||||
if (frame.Msg is string s) return s.Length == 0 ? "empty string" : "string";
|
||||
return frame.Msg.GetType().Name switch
|
||||
{
|
||||
"JsonElement" => "object",
|
||||
_ => frame.Msg.GetType().Name
|
||||
};
|
||||
}
|
||||
catch { return "unknown"; }
|
||||
}
|
||||
|
||||
public static Notification EmptyObjectPayload(NotificationType id)
|
||||
=> new() { Id = id, Msg = JsonDocument.Parse("{}").RootElement.Clone() };
|
||||
|
||||
public static Notification EmptyObjectPayload(string id)
|
||||
=> new() { Id = id, Msg = JsonDocument.Parse("{}").RootElement.Clone() };
|
||||
|
||||
public static void SendReliably(long accountId, Notification message, TimeSpan window, TimeSpan interval, Notification? alternate = null)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try { await SendUntilGoneAsync(accountId, message, window, interval, alternate); }
|
||||
catch (Exception) { }
|
||||
});
|
||||
}
|
||||
|
||||
public static int DisconnectAccount(long accountId, string reason)
|
||||
{
|
||||
List<string> connectionIds;
|
||||
lock (ClientsLock)
|
||||
connectionIds = Clients.Where(n => n.AccountId == accountId).Select(n => n.ConnectionId).ToList();
|
||||
|
||||
var closed = 0;
|
||||
foreach (var connectionId in connectionIds)
|
||||
{
|
||||
if (!_contexts.TryRemove(connectionId, out var context)) continue;
|
||||
|
||||
try
|
||||
{
|
||||
context.Abort();
|
||||
closed++;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return closed;
|
||||
}
|
||||
|
||||
public static async Task SendNotify(long AccountId, Notification Message)
|
||||
{
|
||||
List<HubClient> clients;
|
||||
lock (ClientsLock)
|
||||
clients = Notify.Clients.Where(n => n.AccountId == AccountId).ToList();
|
||||
|
||||
if (clients.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
foreach (var client in clients)
|
||||
await SendToConnection(client.ConnectionId, Message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public static async Task SendSelfAccountUpdate(long accountId)
|
||||
{
|
||||
if (DbFactory == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
var profile = await ctx.Profiles.FirstOrDefaultAsync(x => x.accountId == accountId);
|
||||
if (profile == null) return;
|
||||
|
||||
var plrData = await ctx.Players.FirstOrDefaultAsync(x => x.AccountId == accountId);
|
||||
if (plrData == null) return;
|
||||
|
||||
await SendNotify(accountId, new Notification
|
||||
{
|
||||
Id = "SelfAccountUpdate",
|
||||
Msg = PlayerUtils.BuildSelfAccount(profile, plrData)
|
||||
});
|
||||
}
|
||||
|
||||
public static async Task<Presence?> BuildSelfPresenceAsync(RecDBContext ctx, long accountId)
|
||||
{
|
||||
var realPresence = await ctx.Presences.FirstOrDefaultAsync(x => x.playerId == accountId);
|
||||
if (realPresence == null) return null;
|
||||
|
||||
RoomInstance? roomInstance = realPresence.roomInstance is > 0
|
||||
? await ctx.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == realPresence.roomInstance)
|
||||
: null;
|
||||
|
||||
return await Presence.CreateAsync(ctx, realPresence, roomInstance);
|
||||
}
|
||||
|
||||
public static async Task SendGlobalNotify(Notification Message)
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (Notify.Context == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await Notify.Context.Clients.All.SendAsync("Notification", Message.ToJson());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private static async Task SendToConnection(string connectionId, Notification Message, string? idFormOverride = null)
|
||||
{
|
||||
if (Notify.Context == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await Notify.Context.Clients.Client(connectionId).SendAsync("Notification",
|
||||
idFormOverride == null ? Message.ToJson() : EnvelopeWithIdForm(Message, idFormOverride));
|
||||
|
||||
}
|
||||
|
||||
public static string EnvelopeWithIdForm(Notification message, string idJson)
|
||||
=> $"{{\"Id\":{idJson},\"Msg\":{message.Msg?.ToJson() ?? "null"}}}";
|
||||
|
||||
public static string IdFormJson(string? form, NotificationType type)
|
||||
{
|
||||
var number = Convert.ToInt64(type, CultureInfo.InvariantCulture)
|
||||
.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
return (form ?? "string").Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"number" => number,
|
||||
"name" => JsonSerializer.Serialize(type.ToString()),
|
||||
_ => JsonSerializer.Serialize(number),
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<string> ReplayStoredCoachMessageAsync(long accountId, string? form)
|
||||
{
|
||||
if (DbFactory == null) return "no database configured";
|
||||
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
var stored = await ctx.Messages
|
||||
.Where(m => m.ToPlayerId == accountId && m.Type == MessageType.CoachMessage)
|
||||
.OrderByDescending(m => m.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (stored == null)
|
||||
return $"account {accountId} has no stored coach message - send one from the admin panel first, then replay it.";
|
||||
|
||||
List<HubClient> clients;
|
||||
lock (ClientsLock)
|
||||
clients = Notify.Clients.Where(n => n.AccountId == accountId).ToList();
|
||||
|
||||
if (clients.Count == 0)
|
||||
return $"account {accountId} has {connectedClause()} but no notify hub connection; nothing to push to. " +
|
||||
"Check the [NotifyHub] connect line in server-console.log.";
|
||||
|
||||
var notif = new Notification { Id = NotificationType.MessageReceived, Msg = stored };
|
||||
var idJson = IdFormJson(form, NotificationType.MessageReceived);
|
||||
|
||||
foreach (var client in clients)
|
||||
await SendToConnection(client.ConnectionId, notif, idJson);
|
||||
|
||||
var wire = EnvelopeWithIdForm(notif, idJson);
|
||||
|
||||
return $"replayed stored coach message {stored.Id} to account {accountId} on {clients.Count} connection(s).\n" +
|
||||
$"Id token on the wire: {idJson}\n" +
|
||||
$"envelope: {wire}\n" +
|
||||
ExpectedClientReaction(idJson, NotificationType.MessageReceived);
|
||||
}
|
||||
|
||||
private static string connectedClause()
|
||||
{
|
||||
var accounts = ConnectedAccountIds();
|
||||
return accounts.Count == 0
|
||||
? "no notify hub connections at all"
|
||||
: $"{accounts.Count} account(s) connected ({string.Join(", ", accounts)})";
|
||||
}
|
||||
|
||||
private static string ExpectedClientReaction(string idJson, NotificationType type)
|
||||
{
|
||||
var number = Convert.ToInt64(type, CultureInfo.InvariantCulture).ToString(CultureInfo.InvariantCulture);
|
||||
if (idJson == number)
|
||||
return "expected: the client logs a hub-frame parse failure and handles nothing - this is NOT a fix.";
|
||||
if (idJson.Trim('"') == type.ToString())
|
||||
return "expected: the client parses the frame, finds no handler under that name, and silently ignores it.";
|
||||
return $"expected: the client handles it as {type} ({number}) - the message appears on the watch without a relog.";
|
||||
}
|
||||
|
||||
public static string WireId(object? id)
|
||||
{
|
||||
try { return JsonSerializer.Serialize(id); }
|
||||
catch { return id?.ToString() ?? "null"; }
|
||||
}
|
||||
|
||||
public static async Task SendAccountUpdate(long accountId)
|
||||
{
|
||||
if (DbFactory == null) return;
|
||||
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
var profile = await ctx.Profiles.FirstOrDefaultAsync(x => x.accountId == accountId);
|
||||
if (profile == null) return;
|
||||
|
||||
await SendNotify(accountId, new Notification { Id = "AccountUpdate", Msg = profile });
|
||||
await UpdateSubscriptions(accountId, Notifs.SubscriptionUpdateProfile, profile);
|
||||
await SendSelfAccountUpdate(accountId);
|
||||
}
|
||||
|
||||
public static Task UpdateEveryone(long playerId) => UpdateSubscriptions(playerId);
|
||||
|
||||
public static async Task UpdateSubscriptions(long playerId)
|
||||
{
|
||||
if (DbFactory == null) return;
|
||||
|
||||
var subscribers = GetSubscriberConnections(playerId);
|
||||
if (subscribers.Count == 0) return;
|
||||
|
||||
var factory = DbFactory;
|
||||
await Task.WhenAll(subscribers.Select(async subscriber =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var ctx = await factory.CreateDbContextAsync();
|
||||
await SendStatusTo(ctx, playerId, subscriber);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public static async Task UpdateSubscriptionsForClient(string connectionId)
|
||||
{
|
||||
if (DbFactory == null) return;
|
||||
|
||||
var client = GetConnectionClient(connectionId);
|
||||
if (client == null) return;
|
||||
|
||||
long[] subscriptions;
|
||||
lock (client.SubscriptionsLock)
|
||||
subscriptions = client.HasSubscriptions ? client.Subscriptions.Distinct().ToArray() : Array.Empty<long>();
|
||||
|
||||
if (subscriptions.Length == 0) return;
|
||||
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
foreach (var playerId in subscriptions)
|
||||
await SendStatusTo(ctx, playerId, client);
|
||||
}
|
||||
|
||||
public static async Task UpdateSubscriptions(long playerId, object id, object message)
|
||||
{
|
||||
var subscribers = GetSubscriberConnections(playerId);
|
||||
var wireId = id is Notifs.SubscriptionUpdateProfile
|
||||
? "AccountUpdate"
|
||||
: id;
|
||||
var notification = new Notification { Id = wireId, Msg = message };
|
||||
if (notification.Id is "AccountUpdate")
|
||||
await UpdateSubscriptions(playerId);
|
||||
|
||||
foreach (var subscriber in subscribers)
|
||||
await SendToConnection(subscriber.ConnectionId, notification);
|
||||
}
|
||||
|
||||
public static async Task SendToSubscribers(long playerId, Notification message)
|
||||
{
|
||||
foreach (var subscriber in GetSubscriberConnections(playerId))
|
||||
await SendToConnection(subscriber.ConnectionId, message);
|
||||
}
|
||||
|
||||
private static List<HubClient> GetSubscriberConnections(long playerId)
|
||||
{
|
||||
return SnapshotClients().Where(client =>
|
||||
{
|
||||
lock (client.SubscriptionsLock)
|
||||
return client.HasSubscriptions && client.Subscriptions.Contains(playerId);
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private static async Task SendStatusTo(RecDBContext ctx, long fromPlayerId, HubClient recipient)
|
||||
{
|
||||
var realPresence = await ctx.Presences.FirstOrDefaultAsync(x => x.playerId == fromPlayerId);
|
||||
if (realPresence == null) return;
|
||||
|
||||
RoomInstance? roomInstance = realPresence.roomInstance is > 0
|
||||
? await ctx.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == realPresence.roomInstance)
|
||||
: null;
|
||||
roomInstance = await RoomUtils.ForViewerAsync(roomInstance, recipient.AccountId);
|
||||
|
||||
if (roomInstance != null)
|
||||
{
|
||||
roomInstance = roomInstance.Copy();
|
||||
roomInstance.photonRegion = string.Empty;
|
||||
roomInstance.photonRoomId = string.Empty;
|
||||
roomInstance.dataBlob = string.Empty;
|
||||
roomInstance.roomCode = string.Empty;
|
||||
}
|
||||
|
||||
var playerPresence = await Presence.CreateAsync(ctx, realPresence, roomInstance);
|
||||
playerPresence.ForViewer(recipient.AccountId);
|
||||
await SendToConnection(recipient.ConnectionId, new Notification
|
||||
{
|
||||
Id = "PresenceUpdate",
|
||||
Msg = playerPresence
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user