Files
mochaisvibecoded 42f7f57b72 :3
2026-08-24 17:00:26 -04:00

821 lines
29 KiB
C#

using LiteDB;
using System.Text.Json;
using System.Text.Json.Serialization;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace Mocha2023.Classes.DBs
{
public static class NotificationDB
{
private static readonly object Sync = new();
private static readonly TimeSpan AcknowledgedInviteGrace =
TimeSpan.FromMinutes(5);
private static readonly LiteDatabase Database =
new(Path.Combine(Program.dataDir, "DBs", "Notifications.db"));
private static readonly ILiteCollection<StoredNotification> Messages =
Database.GetCollection<StoredNotification>("Messages", BsonAutoId.Int64);
private static readonly ILiteCollection<StoredRoomInviteGrant> RoomInviteGrants =
Database.GetCollection<StoredRoomInviteGrant>("RoomInviteGrants");
static NotificationDB()
{
Messages.EnsureIndex(value => value.RecipientPlayerId);
Messages.EnsureIndex(value => value.CreatedAt);
RoomInviteGrants.EnsureIndex(value => value.RecipientPlayerId);
RoomInviteGrants.EnsureIndex(value => value.RoomInstanceId);
RoomInviteGrants.EnsureIndex(value => value.ExpirationTime);
lock (Sync)
{
DeleteExpiredNoLock();
foreach (StoredNotification message in Messages
.FindAll()
.Where(value =>
IsRoomInviteMessageType(value.Type))
.ToList())
{
if (message.FromPlayerId <= 0 ||
message.RecipientPlayerId <= 0 ||
(message.RoomId ?? 0) <= 0 ||
!TryReadRoomInviteId(message, out long roomInstanceId))
{
continue;
}
string wireData = roomInstanceId.ToString(
System.Globalization.CultureInfo.InvariantCulture);
if (message.Type != (int)MessageType.GameInvite ||
!string.Equals(
message.Message,
wireData,
StringComparison.Ordinal))
{
message.Type = (int)MessageType.GameInvite;
message.Message = wireData;
Messages.Update(message);
}
RoomInviteGrants.Upsert(new StoredRoomInviteGrant
{
MessageId = message.Id,
RecipientPlayerId = message.RecipientPlayerId,
InviterPlayerId = message.FromPlayerId,
RoomId = message.RoomId!.Value,
RoomInstanceId = roomInstanceId,
CreatedAt = message.CreatedAt,
ExpirationTime = message.ExpirationTime
});
}
}
}
public enum MessageType
{
GameInvite = 0,
GameInviteDeclined = 1,
GameJoinFailed = 2,
PartyActivitySwitch = 3,
FriendInvite = 4,
VoteToKick = 5,
GameInviteV2 = 6,
PartyActivitySwitchV2 = 7,
RequestGameInvite = 10,
RequestGameInviteDeclined = 11,
FriendStatusOnline = 20,
TextMessage = 30,
FriendRequestAccepted = 40,
PlayerCheer = 50,
PlayerCheerAnonymous = 51,
PartyUpRequest = 120,
FriendIntroduction = 130
}
public sealed class StoredNotification
{
[BsonId]
public long Id { get; set; }
public long RecipientPlayerId { get; set; }
public int FromPlayerId { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public int Type { get; set; }
public string Message { get; set; } = "{}";
public long? RoomId { get; set; }
public long? RoomInstanceId { get; set; }
public long? PlayerEventId { get; set; }
public long? ClubId { get; set; }
public DateTime ExpirationTime { get; set; } = DateTime.UtcNow.AddDays(7);
}
public sealed class StoredRoomInviteGrant
{
[BsonId]
public long MessageId { get; set; }
public long RecipientPlayerId { get; set; }
public long InviterPlayerId { get; set; }
public long RoomId { get; set; }
public long RoomInstanceId { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime ExpirationTime { get; set; } = DateTime.UtcNow.AddHours(2);
}
public sealed class ClientNotification
{
public long Id { get; set; }
public int FromPlayerId { get; set; }
public long ToPlayerId { get; set; }
[JsonPropertyName("SentTime")]
public DateTime CreatedAt { get; set; }
public int Type { get; set; }
[JsonPropertyName("Data")]
public string Message { get; set; } = "{}";
[JsonIgnore]
public long? FromAccountId { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public long? RoomId { get; set; }
[JsonIgnore]
public long? RoomInstanceId { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public long? PlayerEventId { get; set; }
[JsonIgnore]
public long? ClubId { get; set; }
[JsonIgnore]
public DateTime ExpirationTime { get; set; }
}
public static ClientNotification CreateRoomInvite(
long inviterPlayerId,
long receiverPlayerId,
long roomId,
long roomInstanceId,
string? photonRoomId)
{
var inviterInstance = PlayerDB
.GetPlayerHeartbeat(inviterPlayerId)
.roomInstance;
bool isExactInviterInstance =
inviterInstance != null &&
inviterInstance.roomId == roomId &&
inviterInstance.roomInstanceId == roomInstanceId;
string roomName = RoomDB.GetRoom(roomId)?.Name ?? "Room";
return CreateRoomInvite(
inviterPlayerId,
receiverPlayerId,
roomId,
roomInstanceId,
roomName,
inviteMode:
isExactInviterInstance &&
Mocha2023.Classes.Sessions.IsRestrictedInstance(
inviterInstance)
? 1
: 0,
photonRoomId: isExactInviterInstance
? inviterInstance!.photonRoomId
: photonRoomId);
}
public static ClientNotification CreateRoomInvite(
long inviterPlayerId,
long receiverPlayerId,
long roomId,
long roomInstanceId,
string? roomName,
int inviteMode,
string? photonRoomId = null)
{
if (inviterPlayerId is <= 0 or > int.MaxValue)
throw new ArgumentOutOfRangeException(nameof(inviterPlayerId));
if (receiverPlayerId <= 0)
throw new ArgumentOutOfRangeException(nameof(receiverPlayerId));
if (roomId <= 0)
throw new ArgumentOutOfRangeException(nameof(roomId));
if (roomInstanceId <= 0)
throw new ArgumentOutOfRangeException(nameof(roomInstanceId));
var stored = new StoredNotification
{
RecipientPlayerId = receiverPlayerId,
FromPlayerId = checked((int)inviterPlayerId),
CreatedAt = DateTime.UtcNow,
Type = (int)MessageType.GameInvite,
Message = roomInstanceId.ToString(
System.Globalization.CultureInfo.InvariantCulture),
RoomId = roomId,
RoomInstanceId = roomInstanceId,
PlayerEventId = null,
ExpirationTime = DateTime.UtcNow.AddHours(2)
};
lock (Sync)
{
DeleteExpiredNoLock();
List<StoredRoomInviteGrant> replacedGrants = RoomInviteGrants
.Find(value =>
value.RecipientPlayerId == receiverPlayerId &&
value.InviterPlayerId == inviterPlayerId &&
value.RoomInstanceId == roomInstanceId)
.ToList();
foreach (StoredRoomInviteGrant grant in replacedGrants)
{
Messages.Delete(grant.MessageId);
RoomInviteGrants.Delete(grant.MessageId);
}
Messages.Insert(stored);
RoomInviteGrants.Upsert(new StoredRoomInviteGrant
{
MessageId = stored.Id,
RecipientPlayerId = receiverPlayerId,
InviterPlayerId = inviterPlayerId,
RoomId = roomId,
RoomInstanceId = roomInstanceId,
CreatedAt = stored.CreatedAt,
ExpirationTime = stored.ExpirationTime
});
Console.WriteLine(
$"[ROOM INVITE GRANT] issued message={stored.Id} " +
$"from={inviterPlayerId} to={receiverPlayerId} " +
$"room={roomId} instance={roomInstanceId} " +
$"replaced={replacedGrants.Count}");
return ToClient(stored);
}
}
public static ClientNotification CreatePartyInvite(
long inviterPlayerId,
long receiverPlayerId,
long partyId)
{
var messageData = new
{
FromAccountId = checked((int)inviterPlayerId),
InviterPlayerId = inviterPlayerId,
PartyId = partyId
};
return Insert(
inviterPlayerId,
receiverPlayerId,
MessageType.PartyUpRequest,
messageData);
}
public static ClientNotification CreatePlayerMessage(
long senderPlayerId,
long receiverPlayerId,
MessageType type,
string? message,
long? roomId = null)
{
return InsertMessage(
senderPlayerId,
receiverPlayerId,
type,
message ?? string.Empty,
roomId);
}
public static ClientNotification CreateFriendInvite(
long senderPlayerId,
long receiverPlayerId)
{
return Insert(
senderPlayerId,
receiverPlayerId,
MessageType.FriendInvite,
new
{
FromAccountId = checked((int)senderPlayerId),
FromPlayerId = checked((int)senderPlayerId),
FriendPlayerId = checked((int)senderPlayerId)
});
}
public static ClientNotification CreateFriendAccepted(
long acceptingPlayerId,
long receiverPlayerId)
{
return Insert(
acceptingPlayerId,
receiverPlayerId,
MessageType.FriendRequestAccepted,
new
{
FromAccountId = checked((int)acceptingPlayerId),
FromPlayerId = checked((int)acceptingPlayerId),
FriendPlayerId = checked((int)acceptingPlayerId)
});
}
public static ClientNotification CreateFriendIntroduction(
long senderPlayerId,
long receiverPlayerId,
long aboutPlayerId)
{
if (aboutPlayerId is <= 0 or > int.MaxValue)
throw new ArgumentOutOfRangeException(nameof(aboutPlayerId));
int aboutId = checked((int)aboutPlayerId);
return Insert(
senderPlayerId,
receiverPlayerId,
MessageType.FriendIntroduction,
new
{
FromAccountId = checked((int)senderPlayerId),
FromPlayerId = checked((int)senderPlayerId),
AboutAccountId = aboutId,
AboutPlayerId = aboutId,
IntroducedPlayerId = aboutId
});
}
public static ClientNotification CreatePlayerCheer(
long senderPlayerId,
long receiverPlayerId,
int category,
bool anonymous)
{
return InsertMessage(
senderPlayerId,
receiverPlayerId,
anonymous
? MessageType.PlayerCheerAnonymous
: MessageType.PlayerCheer,
category.ToString(System.Globalization.CultureInfo.InvariantCulture),
hideSender: anonymous);
}
public static List<ClientNotification> GetMessages(long recipientPlayerId)
{
lock (Sync)
{
DeleteExpiredNoLock();
return Messages.Find(value => value.RecipientPlayerId == recipientPlayerId)
.OrderByDescending(value => value.CreatedAt)
.Select(ToClient)
.ToList();
}
}
public static int DeleteMessages(
long recipientPlayerId,
IEnumerable<long> messageIds)
{
HashSet<long> ids = messageIds
.Where(value => value > 0)
.ToHashSet();
if (ids.Count == 0)
return 0;
lock (Sync)
{
DeleteExpiredNoLock();
DateTime graceExpiration =
DateTime.UtcNow.Add(AcknowledgedInviteGrace);
int deleted = 0;
var acknowledgedGrantIds = new HashSet<long>();
foreach (StoredNotification message in Messages
.Find(value =>
value.RecipientPlayerId == recipientPlayerId)
.ToList())
{
bool matchesMessageId = ids.Contains(message.Id);
bool matchesInviteId =
IsRoomInviteMessageType(message.Type) &&
TryReadRoomInviteId(message, out long roomInviteId) &&
ids.Contains(roomInviteId);
if ((matchesMessageId || matchesInviteId) &&
Messages.Delete(message.Id))
{
deleted++;
if (IsRoomInviteMessageType(message.Type))
acknowledgedGrantIds.Add(message.Id);
}
}
foreach (StoredRoomInviteGrant grant in RoomInviteGrants
.Find(value =>
value.RecipientPlayerId == recipientPlayerId)
.Where(value =>
acknowledgedGrantIds.Contains(value.MessageId) ||
ids.Contains(value.MessageId) ||
ids.Contains(value.RoomInstanceId))
.ToList())
{
if (grant.ExpirationTime > graceExpiration)
{
grant.ExpirationTime = graceExpiration;
RoomInviteGrants.Update(grant);
}
acknowledgedGrantIds.Add(grant.MessageId);
}
if (acknowledgedGrantIds.Count > 0)
{
Console.WriteLine(
$"[ROOM INVITE ACK] recipient={recipientPlayerId} " +
$"grants={acknowledgedGrantIds.Count} " +
$"graceSeconds={(int)AcknowledgedInviteGrace.TotalSeconds}");
}
return deleted;
}
}
public static int ConsumeRoomInvite(
long recipientPlayerId,
long inviteId)
{
if (recipientPlayerId <= 0 || inviteId <= 0)
return 0;
lock (Sync)
{
DeleteExpiredNoLock();
List<StoredRoomInviteGrant> grants = RoomInviteGrants
.Find(value =>
value.RecipientPlayerId == recipientPlayerId)
.Where(value =>
value.MessageId == inviteId ||
value.RoomInstanceId == inviteId)
.ToList();
HashSet<long> messageIds = grants
.Select(value => value.MessageId)
.ToHashSet();
foreach (StoredNotification message in Messages
.Find(value =>
value.RecipientPlayerId == recipientPlayerId)
.Where(value =>
IsRoomInviteMessageType(value.Type))
.ToList())
{
if (message.Id == inviteId ||
(TryReadRoomInviteId(message, out long roomInviteId) &&
roomInviteId == inviteId))
{
messageIds.Add(message.Id);
}
}
int removed = 0;
foreach (long messageId in messageIds)
{
if (Messages.Delete(messageId))
removed++;
RoomInviteGrants.Delete(messageId);
}
foreach (StoredRoomInviteGrant grant in grants)
RoomInviteGrants.Delete(grant.MessageId);
return removed + grants.Count;
}
}
public static bool TryGetRoomInvite(
long recipientPlayerId,
long inviteId,
out long inviterPlayerId,
out long roomId,
out long roomInstanceId)
{
inviterPlayerId = 0;
roomId = 0;
roomInstanceId = 0;
if (recipientPlayerId <= 0 || inviteId <= 0)
return false;
lock (Sync)
{
DeleteExpiredNoLock();
StoredNotification? message = Messages
.Find(value =>
value.RecipientPlayerId == recipientPlayerId)
.Where(value =>
IsRoomInviteMessageType(value.Type))
.OrderByDescending(value => value.CreatedAt)
.FirstOrDefault(value =>
value.Id == inviteId ||
(TryReadRoomInviteId(value, out long candidateInviteId) &&
candidateInviteId == inviteId));
if (message != null)
{
inviterPlayerId = message.FromPlayerId;
roomId = message.RoomId ?? 0;
if (!TryReadRoomInviteId(message, out roomInstanceId))
return false;
RoomInviteGrants.Upsert(new StoredRoomInviteGrant
{
MessageId = message.Id,
RecipientPlayerId = message.RecipientPlayerId,
InviterPlayerId = message.FromPlayerId,
RoomId = roomId,
RoomInstanceId = roomInstanceId,
CreatedAt = message.CreatedAt,
ExpirationTime = message.ExpirationTime
});
}
else
{
StoredRoomInviteGrant? grant = RoomInviteGrants
.Find(value =>
value.RecipientPlayerId == recipientPlayerId)
.OrderByDescending(value => value.CreatedAt)
.FirstOrDefault(value =>
value.MessageId == inviteId ||
value.RoomInstanceId == inviteId);
if (grant == null)
return false;
inviterPlayerId = grant.InviterPlayerId;
roomId = grant.RoomId;
roomInstanceId = grant.RoomInstanceId;
}
return inviterPlayerId > 0 &&
roomId > 0 &&
roomInstanceId > 0;
}
}
public static bool TryGetLatestRoomInviteForRoom(
long recipientPlayerId,
long roomId,
out long inviteId,
out long inviterPlayerId,
out long roomInstanceId)
{
inviteId = 0;
inviterPlayerId = 0;
roomInstanceId = 0;
if (recipientPlayerId <= 0 || roomId <= 0)
return false;
lock (Sync)
{
DeleteExpiredNoLock();
StoredRoomInviteGrant? grant = RoomInviteGrants
.Find(value =>
value.RecipientPlayerId == recipientPlayerId &&
value.RoomId == roomId)
.OrderByDescending(value => value.CreatedAt)
.FirstOrDefault();
if (grant == null)
return false;
inviteId = grant.MessageId;
inviterPlayerId = grant.InviterPlayerId;
roomInstanceId = grant.RoomInstanceId;
return inviteId > 0 &&
inviterPlayerId > 0 &&
roomInstanceId > 0;
}
}
public static bool HasActiveRoomInvite(
long recipientPlayerId,
long inviterPlayerId,
long roomInstanceId,
long? expectedRoomId = null)
{
if (recipientPlayerId <= 0 ||
inviterPlayerId <= 0 ||
roomInstanceId <= 0)
{
return false;
}
return TryGetRoomInvite(
recipientPlayerId,
roomInstanceId,
out long resolvedInviterPlayerId,
out long resolvedRoomId,
out long resolvedRoomInstanceId) &&
resolvedInviterPlayerId == inviterPlayerId &&
resolvedRoomInstanceId == roomInstanceId &&
(!expectedRoomId.HasValue ||
resolvedRoomId == expectedRoomId.Value);
}
private static bool TryReadRoomInviteId(
StoredNotification message,
out long roomInstanceId)
{
roomInstanceId = message.RoomInstanceId ?? 0;
if (roomInstanceId > 0)
return true;
if (string.IsNullOrWhiteSpace(message.Message))
return false;
if (long.TryParse(
message.Message,
System.Globalization.NumberStyles.Integer,
System.Globalization.CultureInfo.InvariantCulture,
out roomInstanceId) &&
roomInstanceId > 0)
{
return true;
}
try
{
using JsonDocument document =
JsonDocument.Parse(message.Message);
JsonElement root = document.RootElement;
foreach (string propertyName in new[]
{
"InviteId",
"inviteId",
"RoomInstanceId",
"roomInstanceId",
"TargetRoomInstanceId",
"targetRoomInstanceId"
})
{
if (!root.TryGetProperty(
propertyName,
out JsonElement value))
{
continue;
}
if (value.ValueKind == JsonValueKind.Number &&
value.TryGetInt64(out long numericValue) &&
numericValue > 0)
{
roomInstanceId = numericValue;
return true;
}
if (value.ValueKind == JsonValueKind.String &&
long.TryParse(value.GetString(), out long stringValue) &&
stringValue > 0)
{
roomInstanceId = stringValue;
return true;
}
}
}
catch (JsonException)
{
return false;
}
return false;
}
public static int DeletePlayerCheerMessages(long recipientPlayerId)
{
if (recipientPlayerId <= 0)
return 0;
lock (Sync)
{
int deleted = 0;
foreach (StoredNotification message in Messages
.Find(value =>
value.RecipientPlayerId == recipientPlayerId)
.Where(value =>
value.Type == (int)MessageType.PlayerCheer ||
value.Type ==
(int)MessageType.PlayerCheerAnonymous)
.ToList())
{
if (Messages.Delete(message.Id))
deleted++;
}
return deleted;
}
}
private static ClientNotification Insert(
long fromPlayerId,
long recipientPlayerId,
MessageType type,
object messageData,
long? roomId = null,
long? playerEventId = null,
long? clubId = null)
{
return InsertMessage(
fromPlayerId,
recipientPlayerId,
type,
JsonSerializer.Serialize(messageData),
roomId,
playerEventId,
clubId);
}
private static ClientNotification InsertMessage(
long fromPlayerId,
long recipientPlayerId,
MessageType type,
string message,
long? roomId = null,
long? playerEventId = null,
long? clubId = null,
bool hideSender = false)
{
if (fromPlayerId is <= 0 or > int.MaxValue)
throw new ArgumentOutOfRangeException(nameof(fromPlayerId));
if (recipientPlayerId <= 0)
throw new ArgumentOutOfRangeException(nameof(recipientPlayerId));
var stored = new StoredNotification
{
RecipientPlayerId = recipientPlayerId,
FromPlayerId = hideSender
? 0
: checked((int)fromPlayerId),
CreatedAt = DateTime.UtcNow,
Type = (int)type,
Message = message,
RoomId = roomId,
PlayerEventId = playerEventId,
ClubId = clubId,
ExpirationTime = DateTime.UtcNow.AddDays(7)
};
lock (Sync)
{
DeleteExpiredNoLock();
Messages.Insert(stored);
return ToClient(stored);
}
}
private static ClientNotification ToClient(StoredNotification value) => new()
{
Id = value.Id,
FromPlayerId = value.FromPlayerId,
ToPlayerId = value.RecipientPlayerId,
CreatedAt = value.CreatedAt,
Type = value.Type,
Message = value.Message,
FromAccountId = value.FromPlayerId,
RoomId = value.RoomId,
RoomInstanceId = value.RoomInstanceId,
PlayerEventId = value.PlayerEventId,
ClubId = value.ClubId,
ExpirationTime = value.ExpirationTime
};
public static bool IsRoomInviteMessageType(int type) =>
type is
(int)MessageType.GameInvite or
(int)MessageType.GameInviteV2;
private static void DeleteExpiredNoLock()
{
DateTime now = DateTime.UtcNow;
foreach (StoredNotification message in Messages.Find(value =>
value.ExpirationTime < now).ToList())
{
Messages.Delete(message.Id);
}
foreach (StoredRoomInviteGrant grant in RoomInviteGrants.Find(value =>
value.ExpirationTime < now).ToList())
{
RoomInviteGrants.Delete(grant.MessageId);
}
}
}
}