1065 lines
39 KiB
C#
1065 lines
39 KiB
C#
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}");
|
|
}
|
|
}
|
|
}
|
|
} |