yikes,
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Vanadium.Auth;
|
||||
using Vanadium.Classes;
|
||||
using Vanadium.Classes.DBs;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace Vanadium.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
public partial class AnnouncementsController : ControllerBase
|
||||
{
|
||||
[HttpGet("/api/announcement/v1/get")]
|
||||
public async Task<IActionResult> AnnouncementV1Get()
|
||||
{
|
||||
return Ok(AnnoucementDB.GetAllAnnouncements() ?? ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
// todo db for each
|
||||
|
||||
[HttpGet("v2/subscription/mine/unread")]
|
||||
[HttpGet("v2/mine/unread")]
|
||||
[HttpGet("v1/get")]
|
||||
public async Task<IActionResult> SubscriptionAndALlat()
|
||||
{
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,1065 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Vanadium.Auth;
|
||||
using Newtonsoft.Json;
|
||||
using Vanadium.Classes;
|
||||
using Vanadium.Classes.WebSocket;
|
||||
using Vanadium.Hubs;
|
||||
using Vanadium.Classes.DBs;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
|
||||
|
||||
namespace Vanadium.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("notifications")]
|
||||
public class NotificationsController : Controller
|
||||
{
|
||||
public static ConcurrentDictionary<string, WebSocket> WebSockets { get; } = new();
|
||||
public static ConcurrentDictionary<long, HashSet<string>> PlayerConnections { get; } = new();
|
||||
private static readonly ConcurrentDictionary<string, SemaphoreSlim> SendGates = new();
|
||||
private static readonly byte[] Separator = Encoding.UTF8.GetBytes("\u001e");
|
||||
|
||||
public static class EventTypes
|
||||
{
|
||||
public const string AccountUpdate = NotiEventTypes.AccountUpdate;
|
||||
public const string SelfAccountUpdate = NotiEventTypes.SelfAccountUpdate;
|
||||
public const string PresenceUpdate = NotiEventTypes.PresenceUpdate;
|
||||
public const string RoomUpdate = NotiEventTypes.RoomUpdate;
|
||||
public const string RoomInstanceUpdate = NotiEventTypes.RoomInstanceUpdate;
|
||||
public const string ReputationUpdate = NotiEventTypes.ReputationUpdate;
|
||||
public const string PlayerProgressionLevelUpdate = NotiEventTypes.PlayerProgressionLevelUpdate;
|
||||
public const string PhotonAccessToken = NotiEventTypes.PhotonAccessToken;
|
||||
public const string InfluencerSupportedUpdate = NotiEventTypes.InfluencerSupportedUpdate;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult NameserverNoti()
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
Auth = "https://reloxa.xyz/notifications",
|
||||
API = "https://reloxa.xyz/notifications",
|
||||
WWW = "https://reloxa.xyz/notifications",
|
||||
Notifications = "https://reloxa.xyz/notifications",
|
||||
Images = "https://reloxa.xyz/notifications/img",
|
||||
CDN = "https://reloxa.xyz/cdn",
|
||||
Commerce = "https://reloxa.xyz/notifications",
|
||||
Matchmaking = "https://reloxa.xyz/notifications",
|
||||
Storage = "https://reloxa.xyz/notifications",
|
||||
Chat = "https://reloxa.xyz/notifications/chat",
|
||||
Leaderboard = "https://reloxa.xyz/notifications",
|
||||
Accounts = "https://reloxa.xyz/notifications",
|
||||
Link = "https://reloxa.xyz/notifications",
|
||||
RoomComments = "https://reloxa.xyz/notifications",
|
||||
Clubs = "https://reloxa.xyz/notifications",
|
||||
Rooms = "https://reloxa.xyz/notifications/roomserver",
|
||||
PlatformNotifications = "https://reloxa.xyz/notifications/pn",
|
||||
Moderation = "https://reloxa.xyz/notifications/mo",
|
||||
DataCollection = "https://reloxa.xyz/notifications/dc",
|
||||
BugReporting = "https://reloxa.xyz/notifications/br",
|
||||
Discovery = "https://reloxa.xyz/notifications/disc",
|
||||
Econ = "https://reloxa.xyz/notifications",
|
||||
CMS = "https://reloxa.xyz/notifications/cms",
|
||||
GameLogs = "https://reloxa.xyz/notifications/gl",
|
||||
Lists = "https://reloxa.xyz/notifications/li",
|
||||
PlayerSettings = "https://reloxa.xyz/notifications",
|
||||
Strings = "https://reloxa.xyz/notifications/s",
|
||||
StringsCDN = "https://reloxa.xyz/notifications/scdn",
|
||||
Studio = "https://reloxa.xyz/notifications/st"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpPost("hub/v1/negotiate")]
|
||||
public IActionResult Negotiate()
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
|
||||
string connectionId = Guid.NewGuid().ToString("N");
|
||||
|
||||
var response = new
|
||||
{
|
||||
negotiateVersion = 0,
|
||||
connectionId = connectionId,
|
||||
availableTransports = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
transport = "WebSockets",
|
||||
transferFormats = new[] { "Text", "Binary" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[Route("hub/v1")]
|
||||
public async Task HandleHub([FromQuery] string id, [FromHeader(Name = "Authorization")] string? auth)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
{
|
||||
HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
return;
|
||||
}
|
||||
|
||||
string connectionId = id;
|
||||
long pId = playerId.Value;
|
||||
|
||||
using var socket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
//Console.WriteLine($"[WebSocket] Player {pId} connected with connection {connectionId}");
|
||||
|
||||
PlayerConnections.AddOrUpdate(
|
||||
pId,
|
||||
_ => new HashSet<string> { connectionId },
|
||||
(_, hs) =>
|
||||
{
|
||||
lock (hs) { hs.Add(connectionId); }
|
||||
return hs;
|
||||
}
|
||||
);
|
||||
|
||||
await HandleConnectionAsync(pId, connectionId, socket);
|
||||
}
|
||||
|
||||
[HttpGet("preferences")]
|
||||
public IActionResult GetPreferences()
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
return Ok(new { mutedCategories = Array.Empty<object>() });
|
||||
}
|
||||
|
||||
[HttpGet("crm/me/config/v3")]
|
||||
public IActionResult GetCRMConfig()
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
return Ok(Array.Empty<object>());
|
||||
}
|
||||
|
||||
[HttpGet("accounts/{id}/receives/GameplayInvites")]
|
||||
public IActionResult GetGameplayInvites(ulong id)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
[HttpGet("config/categories")]
|
||||
public IActionResult GetCategories()
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "NotiConfigCategories.json");
|
||||
if (!System.IO.File.Exists(path))
|
||||
return Ok(new { results = Array.Empty<object>(), totalResults = 0 });
|
||||
return PhysicalFile(path, "application/json");
|
||||
}
|
||||
|
||||
[HttpGet("/announcements/v2/mine/unread")]
|
||||
public IActionResult GetUnreadAnnouncements()
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
[HttpGet("/announcements/v2/subscription/mine/unread")]
|
||||
public IActionResult GetUnreadSubscriptionAnnouncements()
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
[HttpGet("/subscription/mine/member")]
|
||||
public IActionResult GetSubscriptionMember()
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
public static int ReconcileStartupOnlineHeartbeats(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var connectedPlayerIds = PlayerConnections.Keys.ToHashSet();
|
||||
|
||||
var onlinePlayers = PlayerDB.Players
|
||||
.FindAll()
|
||||
.Where(p => p?.Player?.PlayerExtra?.Heartbeat?.isOnline == true)
|
||||
.ToList();
|
||||
|
||||
int fixedCount = 0;
|
||||
|
||||
foreach (var player in onlinePlayers)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
if (player == null || player.Player == null || player.Player.PlayerExtra == null)
|
||||
continue;
|
||||
|
||||
if (connectedPlayerIds.Contains(player.PlayerId))
|
||||
continue;
|
||||
|
||||
player.Player.PlayerExtra.Heartbeat = new Heartbeat
|
||||
{
|
||||
playerId = player.PlayerId,
|
||||
isOnline = false,
|
||||
roomInstance = null,
|
||||
errorCode = 0
|
||||
};
|
||||
|
||||
PlayerDB.Players.Update(player);
|
||||
fixedCount++;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[Notifications] Startup heartbeat reconcile complete. Marked {fixedCount} stale online heartbeats offline.");
|
||||
return fixedCount;
|
||||
}
|
||||
|
||||
private static async Task HandleConnectionAsync(long playerId, string connectionId, WebSocket socket)
|
||||
{
|
||||
using var pingCts = new CancellationTokenSource();
|
||||
|
||||
try
|
||||
{
|
||||
WebSockets[connectionId] = socket;
|
||||
SendGates[connectionId] = new SemaphoreSlim(1, 1);
|
||||
|
||||
await SendHandshakeAsync(connectionId, socket);
|
||||
|
||||
_ = Task.Run(() => PingLoopAsync(connectionId, socket, pingCts.Token));
|
||||
|
||||
var buffer = new byte[4096];
|
||||
while (socket.State == WebSocketState.Open)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
WebSocketReceiveResult result;
|
||||
do
|
||||
{
|
||||
result = await socket.ReceiveAsync(buffer, CancellationToken.None);
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
break;
|
||||
ms.Write(buffer, 0, result.Count);
|
||||
}
|
||||
while (!result.EndOfMessage);
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
break;
|
||||
|
||||
var message = Encoding.UTF8.GetString(ms.GetBuffer(), 0, (int)ms.Length).TrimEnd('\x1e');
|
||||
//Console.WriteLine($"Player ({playerId}) sent: {message}");
|
||||
|
||||
await HandleClientMessageAsync(connectionId, socket, message);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"[Error:{connectionId}] {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
pingCts.Cancel();
|
||||
WebSockets.TryRemove(connectionId, out _);
|
||||
|
||||
if (SendGates.TryRemove(connectionId, out var gate))
|
||||
gate.Dispose();
|
||||
|
||||
if (PlayerConnections.TryGetValue(playerId, out var connections))
|
||||
{
|
||||
lock (connections)
|
||||
{
|
||||
connections.Remove(connectionId);
|
||||
|
||||
if (connections.Count == 0)
|
||||
PlayerConnections.TryRemove(playerId, out _);
|
||||
}
|
||||
}
|
||||
|
||||
if (socket.State is WebSocketState.Open or WebSocketState.CloseReceived)
|
||||
{
|
||||
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing connection", CancellationToken.None);
|
||||
}
|
||||
|
||||
//Console.WriteLine($"[WebSocket] Player {playerId} disconnected from connection {connectionId}");
|
||||
|
||||
bool lastConnection = !PlayerConnections.ContainsKey(playerId);
|
||||
if (lastConnection)
|
||||
{
|
||||
var offlineHb = PlayerDB.UpdatePlayerHeartbeat(playerId, null, online: false);
|
||||
if (offlineHb != null)
|
||||
{
|
||||
offlineHb.playerId = playerId;
|
||||
var friendIds = FriendsDB.GetRelationships(playerId).Select(r => r.OtherPlayerID).ToList();
|
||||
var hbJson = JsonConvert.SerializeObject(new WebsocketEvents.Response
|
||||
{
|
||||
Id = NotiEventTypes.PresenceUpdate, // to show they are offline now
|
||||
Msg = offlineHb
|
||||
});
|
||||
await SendToPlayers(friendIds, hbJson);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task HandleClientMessageAsync(string connectionId, WebSocket socket, string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(message);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (root.TryGetProperty("type", out var typeProp)
|
||||
&& typeProp.ValueKind == System.Text.Json.JsonValueKind.Number
|
||||
&& typeProp.GetInt32() == 1)
|
||||
{
|
||||
string target = root.TryGetProperty("target", out var t) ? t.GetString() ?? string.Empty : string.Empty;
|
||||
string invocationId = root.TryGetProperty("invocationId", out var i) ? i.GetString() ?? string.Empty : string.Empty;
|
||||
|
||||
if (target == "SubscribeToPlayers")
|
||||
{
|
||||
var response = new
|
||||
{
|
||||
type = 3,
|
||||
invocationId = invocationId,
|
||||
result = (object?)null
|
||||
};
|
||||
await SendJsonAsync(connectionId, socket, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"[ParseError] {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SendHandshakeAsync(string connectionId, WebSocket socket)
|
||||
{
|
||||
var handshake = new { protocol = "json", version = 1 };
|
||||
await SendJsonAsync(connectionId, socket, handshake);
|
||||
}
|
||||
|
||||
private static async Task PingLoopAsync(string connectionId, WebSocket socket, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!token.IsCancellationRequested && socket.State == WebSocketState.Open)
|
||||
{
|
||||
var ping = new { type = 6 };
|
||||
await SendJsonAsync(connectionId, socket, ping);
|
||||
await Task.Delay(10000, token);
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"[PingError] {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SendNotificationToPlayer(long playerId, object message)
|
||||
{
|
||||
foreach (var connectionId in SnapshotConnections(playerId))
|
||||
{
|
||||
if (WebSockets.TryGetValue(connectionId, out var socket) && socket.State == WebSocketState.Open)
|
||||
{
|
||||
await SendJsonAsync(connectionId, socket, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SendToPlayer(long playerId, string message)
|
||||
{
|
||||
var connectionIds = SnapshotConnections(playerId);
|
||||
if (connectionIds.Length == 0)
|
||||
return;
|
||||
|
||||
var tasks = new List<Task>();
|
||||
|
||||
foreach (var connectionId in connectionIds)
|
||||
{
|
||||
if (WebSockets.TryGetValue(connectionId, out var socket) && socket != null)
|
||||
{
|
||||
tasks.Add(SendToSocketAsync(connectionId, socket, message));
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public static async Task SendRawToPlayer(long playerId, byte[] payload)
|
||||
{
|
||||
var connectionIds = SnapshotConnections(playerId);
|
||||
if (connectionIds.Length == 0)
|
||||
return;
|
||||
|
||||
var tasks = new List<Task>();
|
||||
|
||||
foreach (var connectionId in connectionIds)
|
||||
{
|
||||
if (WebSockets.TryGetValue(connectionId, out var socket) && socket != null)
|
||||
{
|
||||
tasks.Add(SendRawToSocketAsync(connectionId, socket, payload));
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public static async Task SendToAll(string message)
|
||||
{
|
||||
var tasks = WebSockets
|
||||
.Select(kv => SendToSocketAsync(kv.Key, kv.Value, message));
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public static async Task SendToPlayers(List<long> playerIds, string message)
|
||||
{
|
||||
var tasks = new List<Task>();
|
||||
|
||||
foreach (var id in playerIds)
|
||||
{
|
||||
foreach (var connectionId in SnapshotConnections(id))
|
||||
{
|
||||
if (WebSockets.TryGetValue(connectionId, out var socket) && socket != null)
|
||||
{
|
||||
tasks.Add(SendToSocketAsync(connectionId, socket, message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public static async Task SendClubMembershipUpdate(long toPlayerId, long clubMemberId, long accountId, long clubId, int membershipType, DateTime createdAt, int invitedMembershipType = 0)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(WebsocketEvents.CreateClubMembershipUpdateResponse(clubMemberId, accountId, clubId, membershipType, createdAt, invitedMembershipType));
|
||||
await SendToPlayer(toPlayerId, json);
|
||||
}
|
||||
|
||||
public static async Task BroadcastClubMembershipUpdate(long clubMemberId, long accountId, long clubId, int membershipType, DateTime createdAt, int invitedMembershipType = 0)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(WebsocketEvents.CreateClubMembershipUpdateResponse(clubMemberId, accountId, clubId, membershipType, createdAt, invitedMembershipType));
|
||||
await SendToAll(json);
|
||||
}
|
||||
|
||||
public static async Task SendCreatorClubSubscriptionUpdate(long toPlayerId, long creatorAccountId, long clubId, int membershipType)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(WebsocketEvents.CreateCreatorClubSubscriptionUpdateResponse(creatorAccountId, clubId, membershipType));
|
||||
await SendToPlayer(toPlayerId, json);
|
||||
}
|
||||
|
||||
public static async Task BroadcastCreatorClubSubscriptionUpdate(long creatorAccountId, long clubId, int membershipType)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(WebsocketEvents.CreateCreatorClubSubscriptionUpdateResponse(creatorAccountId, clubId, membershipType));
|
||||
await SendToAll(json);
|
||||
}
|
||||
|
||||
private static string[] SnapshotConnections(long playerId)
|
||||
{
|
||||
if (!PlayerConnections.TryGetValue(playerId, out var set))
|
||||
return Array.Empty<string>();
|
||||
lock (set) { return set.ToArray(); }
|
||||
}
|
||||
|
||||
private static async Task SendToSocketAsync(string connectionId, WebSocket socket, string message)
|
||||
{
|
||||
var packet = new SockSignalR
|
||||
{
|
||||
type = MessageTypes.Invocation,
|
||||
target = "Notification",
|
||||
arguments = new object[] { message },
|
||||
nonblocking = true,
|
||||
result = "200 OK"
|
||||
};
|
||||
|
||||
var data = Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize(packet));
|
||||
var buffer = new byte[data.Length + Separator.Length];
|
||||
Buffer.BlockCopy(data, 0, buffer, 0, data.Length);
|
||||
Buffer.BlockCopy(Separator, 0, buffer, data.Length, Separator.Length);
|
||||
|
||||
await SendBytesAsync(connectionId, socket, buffer, WebSocketMessageType.Text);
|
||||
}
|
||||
|
||||
private static async Task SendRawToSocketAsync(string connectionId, WebSocket socket, byte[] payload)
|
||||
{
|
||||
await SendBytesAsync(connectionId, socket, payload, WebSocketMessageType.Binary);
|
||||
}
|
||||
|
||||
private static async Task SendJsonAsync(string connectionId, WebSocket socket, object obj)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize(obj) + "\x1e");
|
||||
await SendBytesAsync(connectionId, socket, bytes, WebSocketMessageType.Text);
|
||||
}
|
||||
|
||||
private static async Task SendBytesAsync(string connectionId, WebSocket socket, byte[] buffer, WebSocketMessageType type)
|
||||
{
|
||||
var gate = SendGates.GetOrAdd(connectionId, _ => new SemaphoreSlim(1, 1));
|
||||
await gate.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (socket.State != WebSocketState.Open)
|
||||
return;
|
||||
|
||||
await socket.SendAsync(
|
||||
new ArraySegment<byte>(buffer),
|
||||
type,
|
||||
endOfMessage: true,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (socket.State != WebSocketState.Closed && socket.State != WebSocketState.Aborted)
|
||||
{
|
||||
await socket.CloseAsync(WebSocketCloseStatus.InternalServerError, "Send failed", CancellationToken.None);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
gate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private class SockSignalR
|
||||
{
|
||||
public MessageTypes type { get; set; }
|
||||
public string? target { get; set; }
|
||||
public object[]? arguments { get; set; }
|
||||
public bool nonblocking { get; set; }
|
||||
public object? result { get; set; }
|
||||
}
|
||||
|
||||
private enum MessageTypes
|
||||
{
|
||||
Invocation = 1,
|
||||
StreamItem,
|
||||
Completion,
|
||||
StreamInvocation,
|
||||
CancelInvocation,
|
||||
Ping,
|
||||
Close
|
||||
}
|
||||
|
||||
public static async Task<bool> RefreshAccount(long accountId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var selfDTO = PlayerDB.GetAccountMe(accountId);
|
||||
var globalDTO = PlayerDB.GetPlayerDTOById(accountId, accountMe: false);
|
||||
|
||||
var selfUpdate = JsonConvert.SerializeObject(WebsocketEvents.CreateWSEventString("SelfAccountUpdate", selfDTO));
|
||||
var globalUpdate = JsonConvert.SerializeObject(WebsocketEvents.CreateWSEventString("AccountUpdate", globalDTO));
|
||||
|
||||
var notifySelfTask = SendToPlayer(accountId, selfUpdate);
|
||||
var notifyAllTask = SendToAll(globalUpdate);
|
||||
|
||||
await Task.WhenAll(notifySelfTask, notifyAllTask);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"Failed to send notification: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> RefreshRoom(long roomId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
|
||||
var globalUpdate = JsonConvert.SerializeObject(WebsocketEvents.CreateWSEventString("RoomUpdate", room));
|
||||
|
||||
await SendToAll(globalUpdate);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"Failed to send notification: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> RefreshHeartbeat(long playerId, PlayerDBClasses.Heartbeat newHeartbeat)
|
||||
{
|
||||
try
|
||||
{
|
||||
var globalUpdate = JsonConvert.SerializeObject(WebsocketEvents.CreateWSEventString("PresenceUpdate", newHeartbeat));
|
||||
|
||||
//<<<<<<< HEAD
|
||||
var friendIds = FriendsDB.GetRelationships(playerId)
|
||||
.Select(r => r.OtherPlayerID)
|
||||
.ToList();
|
||||
|
||||
friendIds.Add(playerId);
|
||||
|
||||
await SendToPlayers(friendIds, globalUpdate);
|
||||
/*=======
|
||||
// Scoped to friends: SendToAll here makes every in-room client refresh and dump to dorm.
|
||||
var recipients = FriendsDB.GetRelationships(playerId)
|
||||
.Where(r => r.RelationshipType == Vanadium.Classes.DBs.DBClasses.FriendsDBClasses.RelationshipType.Friend)
|
||||
.Select(r => r.OtherPlayerID)
|
||||
.ToList();
|
||||
recipients.Add(playerId);
|
||||
|
||||
await SendToPlayers(recipients, globalUpdate);
|
||||
>>>>>>> 1a0e5b1d7a6cffb81e5d328fc59daaf33d9082f4*/
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"Failed to send notification: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> CreateAndSendWSMessageRecieved(
|
||||
long toPlayerId,
|
||||
long fromPlayerId,
|
||||
MessageType type,
|
||||
string? data,
|
||||
long? roomId = null,
|
||||
long? playerEventId = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var message = PlayerDB.AddMessage(
|
||||
toPlayerId,
|
||||
fromPlayerId,
|
||||
type,
|
||||
data,
|
||||
roomId,
|
||||
playerEventId
|
||||
);
|
||||
|
||||
if (message == null)
|
||||
return false;
|
||||
|
||||
var wsEventTo = WebsocketEvents.CreateMessageResponse(message, toPlayerId);
|
||||
var wsEventFrom = WebsocketEvents.CreateMessageResponse(message, fromPlayerId);
|
||||
|
||||
await Task.WhenAll(
|
||||
SendToPlayer(toPlayerId, JsonConvert.SerializeObject(wsEventTo)),
|
||||
SendToPlayer(fromPlayerId, JsonConvert.SerializeObject(wsEventFrom))
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"WS MessageReceived failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> SendBanned(long accountId, PlayerDBClasses.ModerationBlockDetails mbd)
|
||||
{
|
||||
try
|
||||
{
|
||||
mbd.Message = await APIController.FormatModerationBlockMessageString(mbd);
|
||||
var selfUpdate = JsonConvert.SerializeObject(WebsocketEvents.CreateBanResponse(mbd));
|
||||
await SendToPlayer(accountId, selfUpdate);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"Failed to send notification: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> RefreshHeartbeatsBulk(List<long> playerIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tasks = playerIds.Select(async id =>
|
||||
{
|
||||
var hb = PlayerDB.GetPlayerHeartbeat(id);
|
||||
if (hb == null) return;
|
||||
|
||||
var updateString = JsonConvert.SerializeObject(WebsocketEvents.CreateWSEventString("PresenceUpdate", hb));
|
||||
|
||||
var friendIds = FriendsDB.GetRelationships(id)
|
||||
.Select(r => r.OtherPlayerID)
|
||||
.ToList();
|
||||
|
||||
friendIds.Add(id);
|
||||
|
||||
await SendToPlayers(friendIds, updateString);
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"[Notifications] Bulk refresh failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> BalanceUpdate(long accountId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var selfDTO = PlayerDB.GetPlayerDTOById(accountId, accountMe: true);
|
||||
var selfUpdate = JsonConvert.SerializeObject(WebsocketEvents.CreateWSEventString("SelfAccountUpdate", selfDTO));
|
||||
await SendToPlayer(accountId, selfUpdate);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"Failed to send notification: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> GameRewardReceived(long accountId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var selfDTO = PlayerDB.GetPlayerDTOById(accountId, accountMe: true);
|
||||
var selfUpdate = JsonConvert.SerializeObject(WebsocketEvents.CreateWSEventString("SelfAccountUpdate", selfDTO));
|
||||
await SendToPlayer(accountId, selfUpdate);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"Failed to send notification: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SendEvent(long playerId, string eventType, object payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
var message = JsonConvert.SerializeObject(new WebsocketEvents.Response
|
||||
{
|
||||
Id = eventType,
|
||||
Msg = payload
|
||||
});
|
||||
|
||||
await SendToPlayer(playerId, message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"Failed to send event: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SendMaintenance(int minutes)
|
||||
{
|
||||
var safeMinutes = Math.Clamp(minutes, 1, 1440);
|
||||
var json = JsonConvert.SerializeObject(new WebsocketEvents.Response
|
||||
{
|
||||
Id = ((int)WebsocketEvents.ResponseResults.ServerMaintenance).ToString(),
|
||||
Msg = new { StartsInMinutes = safeMinutes }
|
||||
});
|
||||
await SendToAll(json);
|
||||
}
|
||||
|
||||
public static async Task SendAnnoucementUpdate(AnnoucementDBClasses.Announcement announcement)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "AnnouncementUpdate",
|
||||
Msg = announcement
|
||||
});
|
||||
await SendToAll(json);
|
||||
}
|
||||
|
||||
public static async Task SendAnnoucementDelete(long announcementId)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "AnnouncementDelete",
|
||||
Msg = new
|
||||
{
|
||||
AnnouncementId = announcementId
|
||||
}
|
||||
});
|
||||
await SendToAll(json);
|
||||
}
|
||||
|
||||
public static async Task SendCoahMessage(string msg)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "2",
|
||||
Msg = new
|
||||
{
|
||||
FromPlayerId = 1,
|
||||
Id = Random.Shared.Next(1, 0x7ffffff),
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = 100,
|
||||
Data = msg,
|
||||
RoomId = 1
|
||||
}
|
||||
});
|
||||
await SendToAll(json);
|
||||
}
|
||||
|
||||
public static async Task SendAccountUpdate(long playerId, object? accountData)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new WebsocketEvents.Response
|
||||
{
|
||||
Id = NotiEventTypes.SelfAccountUpdate,
|
||||
Msg = accountData
|
||||
});
|
||||
await SendToPlayer(playerId, json);
|
||||
}
|
||||
|
||||
public static async Task BroadcastAccountUpdate(long playerId, object? publicDto)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new WebsocketEvents.Response
|
||||
{
|
||||
Id = NotiEventTypes.AccountUpdate,
|
||||
Msg = publicDto
|
||||
});
|
||||
await SendToAll(json);
|
||||
}
|
||||
|
||||
public static async Task SendRoomUpdate(long playerId, object room)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new WebsocketEvents.Response
|
||||
{
|
||||
Id = NotiEventTypes.RoomUpdate,
|
||||
Msg = room
|
||||
});
|
||||
await SendToPlayer(playerId, json);
|
||||
}
|
||||
|
||||
public static async Task SendPresenceUpdate(long playerId, object heartbeat)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new WebsocketEvents.Response
|
||||
{
|
||||
Id = NotiEventTypes.PresenceUpdate,
|
||||
Msg = heartbeat
|
||||
});
|
||||
await SendToPlayer(playerId, json);
|
||||
}
|
||||
|
||||
public static async Task SendReputationUpdate(long playerId, object reputation)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new WebsocketEvents.Response
|
||||
{
|
||||
Id = NotiEventTypes.ReputationUpdate,
|
||||
Msg = reputation
|
||||
});
|
||||
await SendToPlayer(playerId, json);
|
||||
}
|
||||
|
||||
public static async Task SendInvite(long playerId, long fromPlayerId, long roomId, MatchDBClasses.InviteDTO invite)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "2",
|
||||
Msg = new
|
||||
{
|
||||
FromPlayerId = fromPlayerId,
|
||||
Id = Random.Shared.Next(1, 0x7ffffff),
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = MessageType.GameInviteV2,
|
||||
Data = JsonConvert.SerializeObject(invite),
|
||||
RoomId = roomId,
|
||||
PlayerEventId = (long?)null
|
||||
}
|
||||
});
|
||||
await SendToPlayer(playerId, json);
|
||||
}
|
||||
|
||||
public static async Task SendPlayerOnline(long fromPlayerId, MatchDBClasses.PlayerOnlineDTO invite)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "2",
|
||||
Msg = new
|
||||
{
|
||||
FromPlayerId = fromPlayerId,
|
||||
Id = Random.Shared.Next(1, 0x7ffffff),
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = MessageType.FriendStatusOnline,
|
||||
Data = JsonConvert.SerializeObject(invite),
|
||||
RoomId = (long?)null,
|
||||
PlayerEventId = (long?)null
|
||||
}
|
||||
});
|
||||
var relationships = FriendsDB.GetRelationships(fromPlayerId);
|
||||
var friendIds = relationships
|
||||
.Where(r => r.RelationshipType == Vanadium.Classes.DBs.DBClasses.FriendsDBClasses.RelationshipType.Friend)
|
||||
.Select(r => r.OtherPlayerID)
|
||||
.Where(friendId =>
|
||||
{
|
||||
var theirRel = FriendsDB.GetRelationships(friendId)
|
||||
.FirstOrDefault(x => x.OtherPlayerID == fromPlayerId);
|
||||
return theirRel?.Favorited == Vanadium.Classes.DBs.DBClasses.FriendsDBClasses.ReciprocalStatus.Local
|
||||
|| theirRel?.Favorited == Vanadium.Classes.DBs.DBClasses.FriendsDBClasses.ReciprocalStatus.Mutual;
|
||||
})
|
||||
.ToList();
|
||||
await SendToPlayers(friendIds, json);
|
||||
}
|
||||
|
||||
public static async Task SendPartyActivitySwitch(long fromPlayerId, List<long> targetPlayerIds, long roomId, Heartbeat heartbeat)
|
||||
{
|
||||
var invite = new MatchDBClasses.InviteDTO
|
||||
{
|
||||
InviteId = heartbeat?.roomInstance?.roomInstanceId ?? 0,
|
||||
Name = heartbeat?.roomInstance?.Name ?? "Unknown Room",
|
||||
InviteMode = 0
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "2",
|
||||
Msg = new
|
||||
{
|
||||
FromPlayerId = fromPlayerId,
|
||||
Id = Random.Shared.Next(1, 0x7ffffff),
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = WebsocketEvents.WebsocketMessageType.PartyActivitySwitchV2,
|
||||
Data = JsonConvert.SerializeObject(invite),
|
||||
RoomId = roomId,
|
||||
PlayerEventId = (long?)null
|
||||
}
|
||||
});
|
||||
|
||||
foreach (var pid in targetPlayerIds)
|
||||
await SendToPlayer(pid, json);
|
||||
}
|
||||
|
||||
public static async Task SendPlayerCheer(long toPlayerId, long fromPlayerId, int cheerCategory)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "2",
|
||||
Msg = new
|
||||
{
|
||||
FromPlayerId = fromPlayerId,
|
||||
Id = Random.Shared.Next(1, 0x7ffffff),
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = MessageType.PlayerCheer,
|
||||
Data = cheerCategory.ToString(),
|
||||
RoomId = (long?)null,
|
||||
PlayerEventId = (long?)null
|
||||
}
|
||||
});
|
||||
await SendToPlayer(toPlayerId, json);
|
||||
}
|
||||
|
||||
public static async Task SendPlayerCheerAnonymous(long toPlayerId, long fromPlayerId, int cheerCategory)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "2",
|
||||
Msg = new
|
||||
{
|
||||
FromPlayerId = fromPlayerId,
|
||||
Id = Random.Shared.Next(1, 0x7ffffff),
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = MessageType.PlayerCheerAnonymous,
|
||||
Data = cheerCategory.ToString(),
|
||||
RoomId = (long?)null,
|
||||
PlayerEventId = (long?)null
|
||||
}
|
||||
});
|
||||
await SendToPlayer(toPlayerId, json);
|
||||
}
|
||||
|
||||
public static async Task SendMessage(long toPlayerId, long fromPlayerId, int type, string data)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "2",
|
||||
Msg = new
|
||||
{
|
||||
FromPlayerId = fromPlayerId,
|
||||
Id = Random.Shared.Next(1, 0x7ffffff),
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = type,
|
||||
Data = data,
|
||||
RoomId = (long?)null,
|
||||
PlayerEventId = (long?)null
|
||||
}
|
||||
});
|
||||
await SendToPlayer(toPlayerId, json);
|
||||
}
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
[Route("api/messages/v2")] // request to join player
|
||||
public class MessagesController : Controller
|
||||
{
|
||||
[HttpPost("send")]
|
||||
public async Task<IActionResult> SendMessage(
|
||||
[FromForm] long ToPlayerId,
|
||||
[FromForm] int Type,
|
||||
[FromForm] string Data = "")
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
await NotificationsController.SendMessage(ToPlayerId, id.Value, Type, Data);
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
}
|
||||
|
||||
public class NotificationStartupHeartbeatCleanupService : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(60), stoppingToken);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
NotificationsController.ReconcileStartupOnlineHeartbeats(stoppingToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"[Notifications] Startup heartbeat reconcile failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user