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

539 lines
21 KiB
C#

using Mocha2023.Classes.DBs.DBClasses;
using LiteDB;
using static Mocha2023.Classes.DBs.DBClasses.PlayerDBClasses;
namespace Mocha2023.Classes.DBs
{
public static class RelationshipDB
{
private static readonly object Sync = new();
private static readonly LiteDatabase Database =
new(Path.Combine(Program.dataDir, "DBs", "Relationships.db"));
private static readonly ILiteCollection<RelationshipRecord> Records =
Database.GetCollection<RelationshipRecord>("Relationships");
static RelationshipDB()
{
var rawRecords = Database.GetCollection("Relationships");
foreach (BsonDocument document in rawRecords.FindAll().ToList())
{
BsonValue id = document["_id"];
if (!id.IsString)
rawRecords.Delete(id);
}
Records.EnsureIndex(value => value.SourcePlayerId);
Records.EnsureIndex(value => value.TargetPlayerId);
Records.EnsureIndex(value => value.Kind);
}
public enum RelationshipKind
{
Friendship,
Subscription
}
public sealed class RelationshipRecord
{
[BsonId]
public string Id { get; set; } = string.Empty;
public RelationshipKind Kind { get; set; }
public long SourcePlayerId { get; set; }
public long TargetPlayerId { get; set; }
public RelationshipType RelationshipType { get; set; }
public List<long> FavoritedBy { get; set; } = new();
public List<long> MutedBy { get; set; } = new();
public List<long> IgnoredBy { get; set; } = new();
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public static int MigrateLegacyData(IEnumerable<FullPlayer> players)
{
lock (Sync)
{
int imported = 0;
var allPlayers = players.Where(value => value.Player != null).ToList();
var validIds = allPlayers.Select(value => value.PlayerId).ToHashSet();
foreach (FullPlayer account in allPlayers)
{
foreach (PlayerRelationship legacy in account.Player!.Relationships ?? new())
{
if (legacy.PlayerID == account.PlayerId || !validIds.Contains(legacy.PlayerID))
continue;
string id = FriendshipId(account.PlayerId, legacy.PlayerID);
RelationshipRecord? record = Records.FindById(id);
bool isNew = record == null;
record ??= NewFriendship(account.PlayerId, legacy.PlayerID);
record.Id = id;
if (legacy.RelationshipType == RelationshipType.OutgoingFriendRequest)
{
record.SourcePlayerId = account.PlayerId;
record.TargetPlayerId = legacy.PlayerID;
record.RelationshipType = RelationshipType.OutgoingFriendRequest;
}
else if (legacy.RelationshipType == RelationshipType.IncomingFriendRequest)
{
record.SourcePlayerId = legacy.PlayerID;
record.TargetPlayerId = account.PlayerId;
record.RelationshipType = RelationshipType.OutgoingFriendRequest;
}
else if (legacy.RelationshipType == RelationshipType.Friend)
{
record.RelationshipType = RelationshipType.Friend;
}
else if (isNew)
{
record.RelationshipType = RelationshipType.None;
}
SetFlag(record.FavoritedBy, account.PlayerId, legacy.Favorited);
SetFlag(record.MutedBy, account.PlayerId, legacy.Muted);
SetFlag(record.IgnoredBy, account.PlayerId, legacy.Ignored);
record.UpdatedAt = DateTime.UtcNow;
Records.Upsert(record);
if (isNew)
imported++;
}
foreach (long targetId in account.Player.SubscribedAccountIds ?? new())
{
if (targetId == account.PlayerId || !validIds.Contains(targetId))
continue;
string id = SubscriptionId(account.PlayerId, targetId);
if (Records.Exists(value => value.Id == id))
continue;
Records.Insert(new RelationshipRecord
{
Id = id,
Kind = RelationshipKind.Subscription,
SourcePlayerId = account.PlayerId,
TargetPlayerId = targetId,
RelationshipType = RelationshipType.None
});
imported++;
}
}
return imported;
}
}
public static List<PlayerRelationship> GetRelationships(long playerId)
{
lock (Sync)
{
return Records.Find(value =>
value.Kind == RelationshipKind.Friendship &&
(value.SourcePlayerId == playerId || value.TargetPlayerId == playerId))
.Select(value => ToClientRelationship(value, playerId))
.OrderByDescending(value => value.RelationshipType == RelationshipType.Friend)
.ThenByDescending(value => value.RelationshipType == RelationshipType.IncomingFriendRequest)
.ThenBy(value => value.PlayerID)
.ToList();
}
}
public static PlayerRelationship? GetRelationship(long playerId, long otherPlayerId)
{
lock (Sync)
{
RelationshipRecord? record = Records.FindById(
FriendshipId(playerId, otherPlayerId));
return record == null ? null : ToClientRelationship(record, playerId);
}
}
public static List<ClientRelationshipDTO> GetClientRelationships(long playerId)
{
lock (Sync)
{
return Records.Find(value =>
value.Kind == RelationshipKind.Friendship &&
(value.SourcePlayerId == playerId || value.TargetPlayerId == playerId))
.Select(value => ToClientRelationshipDto(value, playerId))
.OrderByDescending(value => value.RelationshipType == (int)RelationshipType.Friend)
.ThenByDescending(value => value.RelationshipType == (int)RelationshipType.IncomingFriendRequest)
.ThenBy(value => value.PlayerID)
.ToList();
}
}
public static ClientRelationshipDTO? GetClientRelationship(
long playerId,
long otherPlayerId)
{
lock (Sync)
{
RelationshipRecord? record = Records.FindById(
FriendshipId(playerId, otherPlayerId));
return record == null ? null : ToClientRelationshipDto(record, playerId);
}
}
public static PlayerRelationship SendFriendRequest(
long requesterPlayerId,
long receiverPlayerId,
out bool acceptedExistingRequest)
{
lock (Sync)
{
acceptedExistingRequest = false;
string id = FriendshipId(requesterPlayerId, receiverPlayerId);
RelationshipRecord? existing = Records.FindById(id);
if (existing?.RelationshipType == RelationshipType.Friend)
return ToClientRelationship(existing, requesterPlayerId);
if (existing != null &&
existing.RelationshipType != RelationshipType.Friend &&
existing.SourcePlayerId == receiverPlayerId &&
existing.TargetPlayerId == requesterPlayerId)
{
existing.RelationshipType = RelationshipType.Friend;
existing.UpdatedAt = DateTime.UtcNow;
Records.Upsert(existing);
acceptedExistingRequest = true;
return ToClientRelationship(existing, requesterPlayerId);
}
RelationshipRecord record = existing ?? new RelationshipRecord
{
Id = id,
Kind = RelationshipKind.Friendship,
CreatedAt = DateTime.UtcNow
};
record.SourcePlayerId = requesterPlayerId;
record.TargetPlayerId = receiverPlayerId;
record.RelationshipType = RelationshipType.OutgoingFriendRequest;
record.UpdatedAt = DateTime.UtcNow;
Records.Upsert(record);
return ToClientRelationship(record, requesterPlayerId);
}
}
public static PlayerRelationship? AcceptFriendRequest(
long receiverPlayerId,
long requesterPlayerId)
{
lock (Sync)
{
string id = FriendshipId(receiverPlayerId, requesterPlayerId);
RelationshipRecord? record = Records.FindById(id);
if (record == null)
return null;
if (record.RelationshipType == RelationshipType.Friend)
return ToClientRelationship(record, receiverPlayerId);
if (record.SourcePlayerId != requesterPlayerId ||
record.TargetPlayerId != receiverPlayerId)
{
return null;
}
record.RelationshipType = RelationshipType.Friend;
record.UpdatedAt = DateTime.UtcNow;
Records.Upsert(record);
return ToClientRelationship(record, receiverPlayerId);
}
}
public static PlayerRelationship AddFriend(long firstPlayerId, long secondPlayerId)
{
lock (Sync)
{
string id = FriendshipId(firstPlayerId, secondPlayerId);
RelationshipRecord record = Records.FindById(id) ??
NewFriendship(firstPlayerId, secondPlayerId);
record.Id = id;
record.RelationshipType = RelationshipType.Friend;
record.UpdatedAt = DateTime.UtcNow;
Records.Upsert(record);
return ToClientRelationship(record, firstPlayerId);
}
}
public static PlayerRelationship SetFlags(
long viewerPlayerId,
long otherPlayerId,
bool? ignored = null,
bool? muted = null,
bool? favorited = null)
{
lock (Sync)
{
string id = FriendshipId(viewerPlayerId, otherPlayerId);
RelationshipRecord record = Records.FindById(id) ?? new RelationshipRecord
{
Id = id,
Kind = RelationshipKind.Friendship,
SourcePlayerId = viewerPlayerId,
TargetPlayerId = otherPlayerId,
RelationshipType = RelationshipType.None,
CreatedAt = DateTime.UtcNow
};
if (ignored.HasValue)
SetFlag(record.IgnoredBy, viewerPlayerId, ignored.Value);
if (muted.HasValue)
SetFlag(record.MutedBy, viewerPlayerId, muted.Value);
if (favorited.HasValue)
SetFlag(record.FavoritedBy, viewerPlayerId, favorited.Value);
record.UpdatedAt = DateTime.UtcNow;
Records.Upsert(record);
return ToClientRelationship(record, viewerPlayerId);
}
}
public static bool RemoveFriend(long firstPlayerId, long secondPlayerId)
{
lock (Sync)
return Records.Delete(FriendshipId(firstPlayerId, secondPlayerId));
}
public static bool SetSubscription(long subscriberId, long targetId, bool subscribe)
{
lock (Sync)
{
string id = SubscriptionId(subscriberId, targetId);
if (!subscribe)
{
Records.Delete(id);
return true;
}
Records.Upsert(new RelationshipRecord
{
Id = id,
Kind = RelationshipKind.Subscription,
SourcePlayerId = subscriberId,
TargetPlayerId = targetId,
RelationshipType = RelationshipType.None,
UpdatedAt = DateTime.UtcNow
});
return true;
}
}
public static bool IsSubscribed(long subscriberId, long targetId)
{
lock (Sync)
return Records.Exists(value => value.Id == SubscriptionId(subscriberId, targetId));
}
public static int GetSubscriberCount(long targetId)
{
lock (Sync)
return Records.Count(value =>
value.Kind == RelationshipKind.Subscription &&
value.TargetPlayerId == targetId);
}
public static int GetSubscribedCount(long subscriberId)
{
lock (Sync)
return Records.Count(value =>
value.Kind == RelationshipKind.Subscription &&
value.SourcePlayerId == subscriberId);
}
public static long[] GetPersistentLiveSubjectIds(long viewerPlayerId)
{
if (viewerPlayerId <= 0)
return Array.Empty<long>();
lock (Sync)
{
return Records.FindAll()
.Where(record =>
(record.Kind == RelationshipKind.Friendship &&
(record.SourcePlayerId == viewerPlayerId ||
record.TargetPlayerId == viewerPlayerId)) ||
(record.Kind == RelationshipKind.Subscription &&
record.SourcePlayerId == viewerPlayerId))
.Select(record => record.Kind == RelationshipKind.Subscription
? record.TargetPlayerId
: record.SourcePlayerId == viewerPlayerId
? record.TargetPlayerId
: record.SourcePlayerId)
.Where(playerId =>
playerId > 0 &&
playerId != viewerPlayerId)
.Distinct()
.ToArray();
}
}
public static long[] GetPersistentLiveWatcherIds(long subjectPlayerId)
{
if (subjectPlayerId <= 0)
return Array.Empty<long>();
lock (Sync)
{
return Records.FindAll()
.Where(record =>
(record.Kind == RelationshipKind.Friendship &&
(record.SourcePlayerId == subjectPlayerId ||
record.TargetPlayerId == subjectPlayerId)) ||
(record.Kind == RelationshipKind.Subscription &&
record.TargetPlayerId == subjectPlayerId))
.Select(record => record.Kind == RelationshipKind.Subscription
? record.SourcePlayerId
: record.SourcePlayerId == subjectPlayerId
? record.TargetPlayerId
: record.SourcePlayerId)
.Where(playerId =>
playerId > 0 &&
playerId != subjectPlayerId)
.Distinct()
.ToArray();
}
}
public static long GetStableNumericId(long firstPlayerId, long secondPlayerId)
{
unchecked
{
uint hash = 2166136261;
long lower = Math.Min(firstPlayerId, secondPlayerId);
long upper = Math.Max(firstPlayerId, secondPlayerId);
foreach (long value in new[] { lower, upper })
{
ulong bytes = (ulong)value;
for (int index = 0; index < sizeof(long); index++)
{
hash ^= (byte)(bytes >> (index * 8));
hash *= 16777619;
}
}
int id = (int)(hash & 0x7FFFFFFF);
return id == 0 ? 1 : id;
}
}
private static ClientRelationshipDTO ToClientRelationshipDto(
RelationshipRecord record,
long viewerPlayerId)
{
long otherPlayerId = record.SourcePlayerId == viewerPlayerId
? record.TargetPlayerId
: record.SourcePlayerId;
RelationshipType clientType = record.RelationshipType;
if (record.RelationshipType == RelationshipType.OutgoingFriendRequest)
{
clientType = record.SourcePlayerId == viewerPlayerId
? RelationshipType.OutgoingFriendRequest
: RelationshipType.IncomingFriendRequest;
}
return new ClientRelationshipDTO
{
PlayerID = CheckedPlayerId(otherPlayerId),
RelationshipType = (int)clientType,
Favorited = GetPerspectiveStatus(record.FavoritedBy, viewerPlayerId, otherPlayerId),
Muted = GetPerspectiveStatus(record.MutedBy, viewerPlayerId, otherPlayerId),
Ignored = GetPerspectiveStatus(record.IgnoredBy, viewerPlayerId, otherPlayerId)
};
}
private static int GetPerspectiveStatus(
IReadOnlyCollection<long> values,
long viewerPlayerId,
long otherPlayerId)
{
bool local = values.Contains(viewerPlayerId);
bool remote = values.Contains(otherPlayerId);
if (local && remote)
return 3;
if (local)
return 1;
if (remote)
return 2;
return 0;
}
private static int CheckedPlayerId(long playerId)
{
if (playerId <= 0 || playerId > int.MaxValue)
throw new InvalidOperationException(
$"Player id {playerId} cannot be represented by the April 2023 client.");
return (int)playerId;
}
private static PlayerRelationship ToClientRelationship(
RelationshipRecord record,
long viewerPlayerId)
{
long otherPlayerId = record.SourcePlayerId == viewerPlayerId
? record.TargetPlayerId
: record.SourcePlayerId;
RelationshipType clientType = record.RelationshipType;
if (record.RelationshipType == RelationshipType.OutgoingFriendRequest)
{
clientType = record.SourcePlayerId == viewerPlayerId
? RelationshipType.OutgoingFriendRequest
: RelationshipType.IncomingFriendRequest;
}
return new PlayerRelationship
{
Id = GetStableNumericId(record.SourcePlayerId, record.TargetPlayerId),
PlayerID = otherPlayerId,
RelationshipType = clientType,
Favorited = record.FavoritedBy.Contains(viewerPlayerId),
Muted = record.MutedBy.Contains(viewerPlayerId),
Ignored = record.IgnoredBy.Contains(viewerPlayerId)
};
}
private static RelationshipRecord NewFriendship(long firstPlayerId, long secondPlayerId)
{
long source = Math.Min(firstPlayerId, secondPlayerId);
long target = Math.Max(firstPlayerId, secondPlayerId);
return new RelationshipRecord
{
Id = FriendshipId(firstPlayerId, secondPlayerId),
Kind = RelationshipKind.Friendship,
SourcePlayerId = source,
TargetPlayerId = target,
RelationshipType = RelationshipType.Friend
};
}
private static string FriendshipId(long firstPlayerId, long secondPlayerId) =>
$"friend:{Math.Min(firstPlayerId, secondPlayerId)}:{Math.Max(firstPlayerId, secondPlayerId)}";
private static string SubscriptionId(long subscriberId, long targetId) =>
$"subscription:{subscriberId}:{targetId}";
private static void SetFlag(List<long> values, long playerId, bool enabled)
{
values.RemoveAll(value => value == playerId);
if (enabled)
values.Add(playerId);
}
}
}