From 4d14ede3dc7411892132458e6223917ca497f521 Mon Sep 17 00:00:00 2001 From: Alexa <53471801+Alexa-RR@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:53:12 +0300 Subject: [PATCH] Enhance global friends functionality: suppress auto-generated placeholder accounts in synthesized friend lists and update documentation for clarity. --- .../V2/RelationshipsController.cs | 18 +++++++-- .../Services/RelationshipQueries.cs | 38 +++++++++++++++++-- README.md | 4 +- 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/DorkNet.Server/Controllers/API/Relationships/V2/RelationshipsController.cs b/DorkNet.Server/Controllers/API/Relationships/V2/RelationshipsController.cs index f2df83d..3ccd618 100644 --- a/DorkNet.Server/Controllers/API/Relationships/V2/RelationshipsController.cs +++ b/DorkNet.Server/Controllers/API/Relationships/V2/RelationshipsController.cs @@ -100,12 +100,13 @@ public class RelationshipsController(DorkNetDbContext db, NotificationService no var others = await db.Players .Where(p => p.Id != me && p.Id != RelationshipQueries.SystemAccountId) - .Select(p => p.Id) + .Select(p => new { p.Id, p.Username }) .ToListAsync(); var wire = new List>(others.Count); - foreach (var otherId in others) + foreach (var other in others) { + var otherId = other.Id; if (realByOther.TryGetValue(otherId, out var row)) { // A real block wins — that pair is genuinely not friends. @@ -114,6 +115,9 @@ public class RelationshipsController(DorkNetDbContext db, NotificationService no } else { + // Hide auto-generated Player_NNN placeholders from the + // synthesized list (they never set a real username). + if (RelationshipQueries.IsPlaceholderUsername(other.Username)) continue; wire.Add(SynthFriend(otherId, null, me)); } } @@ -148,11 +152,17 @@ public class RelationshipsController(DorkNetDbContext db, NotificationService no var row = await FindAsync(me, playerId); // Global-friends mode: report Friend for any real account that isn't - // the system account and isn't explicitly blocked. + // the system account and isn't explicitly blocked. Placeholder + // Player_NNN accounts are hidden unless there's a genuine row. if (playerId != RelationshipQueries.SystemAccountId && (row is null || row.Status != EntityStatus.Blocked) && await settings.IsGlobalFriendsEnabledAsync()) - return Ok(SynthFriend(playerId, row, me)); + { + var isPlaceholder = row is null && RelationshipQueries.IsPlaceholderUsername( + await db.Players.Where(p => p.Id == playerId) + .Select(p => p.Username).FirstOrDefaultAsync()); + if (!isPlaceholder) return Ok(SynthFriend(playerId, row, me)); + } return Ok(row is null ? EmptyRelationship(playerId) : BuildRelationship(row, me)); } diff --git a/DorkNet.Server/Services/RelationshipQueries.cs b/DorkNet.Server/Services/RelationshipQueries.cs index 3a126c2..83e2cac 100644 --- a/DorkNet.Server/Services/RelationshipQueries.cs +++ b/DorkNet.Server/Services/RelationshipQueries.cs @@ -31,9 +31,30 @@ public static class RelationshipQueries /// friend in anyone's list. public const long SystemAccountId = 1; + /// True when is still an + /// auto-generated placeholder name (Player_ followed by digits + /// only — see PlayerService.GenerateUsername). These are accounts + /// that never set a real username; the global-friends toggle hides them + /// from the synthesized "everyone" list so they don't clutter it. A + /// genuine Friend row still surfaces them — only the synthetic pairing is + /// suppressed. + public static bool IsPlaceholderUsername(string? username) + { + const string prefix = "Player_"; + if (string.IsNullOrEmpty(username) || + !username.StartsWith(prefix, StringComparison.Ordinal)) return false; + var suffix = username.AsSpan(prefix.Length); + if (suffix.IsEmpty) return false; + foreach (var c in suffix) + if (!char.IsAsciiDigit(c)) return false; + return true; + } + /// The effective friend ids for . With the /// global-friends toggle off this is the real Friend graph; with it on - /// it's every other (non-system, non-blocked) account. + /// it's every other (non-system, non-blocked) account, excluding + /// placeholder Player_NNN accounts that aren't already real + /// friends. public static async Task> EffectiveFriendIdsAsync( DorkNetDbContext db, ServerSettingsService settings, long me) { @@ -53,10 +74,21 @@ public static class RelationshipQueries (r.RequesterId == me || r.TargetId == me)) .Select(r => r.RequesterId == me ? r.TargetId : r.RequesterId) .ToListAsync(); + var realFriendSet = (await db.Relationships + .Where(r => r.Status == RelationshipStatus.Friend && + (r.RequesterId == me || r.TargetId == me)) + .Select(r => r.RequesterId == me ? r.TargetId : r.RequesterId) + .ToListAsync()).ToHashSet(); - return await db.Players + var candidates = await db.Players .Where(p => p.Id != me && p.Id != SystemAccountId && !blocked.Contains(p.Id)) - .Select(p => p.Id) + .Select(p => new { p.Id, p.Username }) .ToListAsync(); + + // Suppress placeholder accounts unless they're an actual friend. + return candidates + .Where(p => realFriendSet.Contains(p.Id) || !IsPlaceholderUsername(p.Username)) + .Select(p => p.Id) + .ToList(); } } diff --git a/README.md b/README.md index 3db4d64..43fe3b0 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,9 @@ rows — the friend graph is synthesized at read time (`RelationshipQueries.EffectiveFriendIdsAsync`, gated on `ServerSettings.GlobalFriendsEnabled`), so flipping it off reverts instantly. Blocks are still honored and the system/coach account is -excluded. Flipping it broadcasts `RelationshipsInvalid` to every connected +excluded, as are auto-generated `Player_NNN` placeholder accounts that +never set a real username (they only appear if there's a genuine friend +row). Flipping it broadcasts `RelationshipsInvalid` to every connected watch, which calls `Relationships.RefreshList` and re-pulls `api/relationships/v2/get` — so players see everyone appear (or disappear) **without relogging**. It covers the friends list, the friends-online HUD,