diff --git a/GenOnlineService/Controllers/Friends/SocialController.cs b/GenOnlineService/Controllers/Friends/SocialController.cs index 3c4ba57..e55dfeb 100644 --- a/GenOnlineService/Controllers/Friends/SocialController.cs +++ b/GenOnlineService/Controllers/Friends/SocialController.cs @@ -67,6 +67,8 @@ public override Type GetReturnType() [Route("env/{environment}/contract/{contract_version}/[controller]")] public class SocialController : ControllerBase { + private const int FriendsLimit = 200; + private readonly IDbContextFactory _dbFactory; private readonly ILogger _logger; @@ -81,18 +83,47 @@ public SocialController(IDbContextFactory dbFactory, ILogger HelperFunction_AcceptFriendRequest(Int64 source_user_id, Int64 target_user_id) { + // NOTE: target user does NOT need to be signed in SharedUserData? sharedUserDataSource = GenOnlineService.WebSocketManager.GetSharedDataForUser(source_user_id); SharedUserData? sharedUserDataTarget = GenOnlineService.WebSocketManager.GetSharedDataForUser(target_user_id); + if (sharedUserDataSource == null) + { + return false; + } + await using var db = await _dbFactory.CreateDbContextAsync(); - // NOTE: target user does NOT need to be signed in + + // a friendship adds an entry to both lists, so the target must have room too + int targetFriendCount = sharedUserDataTarget != null + ? sharedUserDataTarget.GetSocialContainer().Friends.Count + : await Database.Social.CountFriends(db, target_user_id); + + if (targetFriendCount >= FriendsLimit) + { + return false; + } // remove the request from requestor (online version) -#pragma warning disable CS8602 // Dereference of a possibly null reference. sharedUserDataSource.GetSocialContainer().PendingRequests.Remove(target_user_id); -#pragma warning restore CS8602 // Dereference of a possibly null reference. // remove the request from requestor (db) await Database.Social.RemovePendingFriendRequest(db, source_user_id, target_user_id); @@ -141,6 +172,8 @@ private async Task HelperFunction_AcceptFriendRequest(Int64 source_user_id, Int6 // send to all sessions WebsocketHelper.SendToAllSessionsOfUser(target_user_id, bytesJSON); } + + return true; } // Accept a request @@ -150,13 +183,23 @@ public async Task AcceptPendingRequest(Int64 target_user_id) { // source user must be signed in (anywhere) Int64 source_user_id = TokenHelper.GetUserID(this); - if (source_user_id == -1 || WebSocketManager.GetSharedDataForUser(source_user_id) == null) + SharedUserData? userData = WebSocketManager.GetSharedDataForUser(source_user_id); + if (source_user_id == -1 || userData == null) { Response.StatusCode = (int)HttpStatusCode.Forbidden; return; } - HelperFunction_AcceptFriendRequest(source_user_id, target_user_id); + if (RejectIfFriendsListFull(source_user_id, userData)) + { + return; + } + + if (!await HelperFunction_AcceptFriendRequest(source_user_id, target_user_id)) + { + Response.StatusCode = (int)HttpStatusCode.Forbidden; + return; + } SocialHelper.NotifyFriendslistDirty(source_user_id); SocialHelper.NotifyFriendslistDirty(target_user_id); @@ -169,17 +212,15 @@ public async Task RejectPendingRequest(Int64 target_user_id) { // source user must be signed in Int64 source_user_id = TokenHelper.GetUserID(this); - if (source_user_id == -1 || WebSocketManager.GetSharedDataForUser(source_user_id) == null) + SharedUserData? userData = WebSocketManager.GetSharedDataForUser(source_user_id); + if (source_user_id == -1 || userData == null) { Response.StatusCode = (int)HttpStatusCode.Forbidden; return; } // remove the request from requestor (online version) -#pragma warning disable CS8602 // Dereference of a possibly null reference. - SharedUserData? userData = WebSocketManager.GetSharedDataForUser(source_user_id); userData.GetSocialContainer().PendingRequests.Remove(target_user_id); -#pragma warning restore CS8602 // Dereference of a possibly null reference. // remove the request from requestor (db) // NOTE: Target and source are inverted here because the target is actually the person who sent the request, source is the person taking action on the friend request @@ -197,21 +238,19 @@ public async Task RemoveFriend(Int64 target_user_id) { // source user must be signed in Int64 source_user_id = TokenHelper.GetUserID(this); - if (source_user_id == -1 || WebSocketManager.GetSharedDataForUser(source_user_id) == null) + SharedUserData? userData = WebSocketManager.GetSharedDataForUser(source_user_id); + if (source_user_id == -1 || userData == null) { Response.StatusCode = (int)HttpStatusCode.Forbidden; return; } // must be friends -#pragma warning disable CS8602 // Dereference of a possibly null reference. - SharedUserData? userData = WebSocketManager.GetSharedDataForUser(source_user_id); if (!userData.GetSocialContainer().Friends.Contains(target_user_id)) { Response.StatusCode = (int)HttpStatusCode.NotFound; return; } -#pragma warning restore CS8602 // Dereference of a possibly null reference. // remove the request from requestor (online version) userData.GetSocialContainer().Friends.Remove(target_user_id); @@ -239,36 +278,24 @@ public async Task AddFriend(Int64 target_user_id) { // source user must be signed in Int64 requester_user_id = TokenHelper.GetUserID(this); - if (requester_user_id == -1 || WebSocketManager.GetSharedDataForUser(requester_user_id) == null) + SharedUserData? userData = WebSocketManager.GetSharedDataForUser(requester_user_id); + if (requester_user_id == -1 || userData == null) { Response.StatusCode = (int)HttpStatusCode.Forbidden; return; } - // too many friends? - const int friendsLimit = 200; - SharedUserData? userData = WebSocketManager.GetSharedDataForUser(requester_user_id); - if (userData.GetSocialContainer().Friends.Count >= friendsLimit) + if (RejectIfFriendsListFull(requester_user_id, userData)) { - if (userData != null) - { - WebSocketMessage_Social_FriendsListFull friendsListFullEvent = new(); - friendsListFullEvent.msg_id = (int)EWebSocketMessageID.SOCIAL_CANT_ADD_FRIEND_LIST_FULL; - byte[] bytesJSON = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(friendsListFullEvent)); - - // send to all sessions - WebsocketHelper.SendToAllSessionsOfUser(requester_user_id, bytesJSON); - } - } + return; + } // Check not already friends -#pragma warning disable CS8602 // Dereference of a possibly null reference. if (userData.GetSocialContainer().Friends.Contains(target_user_id)) { Response.StatusCode = (int)HttpStatusCode.Conflict; return; } -#pragma warning restore CS8602 // Dereference of a possibly null reference. await using var db = await _dbFactory.CreateDbContextAsync(); @@ -309,7 +336,11 @@ public async Task AddFriend(Int64 target_user_id) if (userData.GetSocialContainer().PendingRequests.Contains(target_user_id)) { // accept their request - HelperFunction_AcceptFriendRequest(requester_user_id, target_user_id); + if (!await HelperFunction_AcceptFriendRequest(requester_user_id, target_user_id)) + { + Response.StatusCode = (int)HttpStatusCode.Forbidden; + return; + } } else { @@ -341,18 +372,14 @@ public async Task Get_FriendsAndRequests() // source user must be signed in Int64 requester_user_id = TokenHelper.GetUserID(this); - if (requester_user_id == -1 || WebSocketManager.GetSharedDataForUser(requester_user_id) == null) + SharedUserData? sourceData = WebSocketManager.GetSharedDataForUser(requester_user_id); + if (requester_user_id == -1 || sourceData == null) { Response.StatusCode = (int)HttpStatusCode.Forbidden; return result; } - // get websockets & data - SharedUserData? sourceData = WebSocketManager.GetSharedDataForUser(requester_user_id); - -#pragma warning disable CS8602 // Dereference of a possibly null reference. HashSet setFriends = sourceData.GetSocialContainer().Friends; -#pragma warning restore CS8602 // Dereference of a possibly null reference. HashSet setPendingRequests = sourceData.GetSocialContainer().PendingRequests; List lstCombined = new List(); @@ -437,17 +464,14 @@ public async Task Get_Blocked() // source user must be signed in Int64 requester_user_id = TokenHelper.GetUserID(this); - if (requester_user_id == -1 || WebSocketManager.GetSharedDataForUser(requester_user_id) == null) + SharedUserData? sourceData = WebSocketManager.GetSharedDataForUser(requester_user_id); + if (requester_user_id == -1 || sourceData == null) { Response.StatusCode = (int)HttpStatusCode.Forbidden; return result; } - SharedUserData? sourceData = WebSocketManager.GetSharedDataForUser(requester_user_id); - -#pragma warning disable CS8602 // Dereference of a possibly null reference. HashSet setBlocked = sourceData.GetSocialContainer().Blocked; -#pragma warning restore CS8602 // Dereference of a possibly null reference. await using var db = await _dbFactory.CreateDbContextAsync(); Dictionary dictDisplayNames = await Database.Users.GetDisplayNameBulk(db, setBlocked.ToList()); @@ -500,16 +524,14 @@ public async Task Add_Block(Int64 target_user_id) { // source user must be signed in Int64 requester_user_id = TokenHelper.GetUserID(this); - if (requester_user_id == -1 || WebSocketManager.GetSharedDataForUser(requester_user_id) == null) + SharedUserData? sourceData = WebSocketManager.GetSharedDataForUser(requester_user_id); + if (requester_user_id == -1 || sourceData == null) { Response.StatusCode = (int)HttpStatusCode.Forbidden; return; } // Check not already blocked - SharedUserData? sourceData = WebSocketManager.GetSharedDataForUser(requester_user_id); - -#pragma warning disable CS8602 // Dereference of a possibly null reference. if (sourceData.GetSocialContainer().Blocked.Contains(target_user_id)) { Response.StatusCode = (int)HttpStatusCode.Conflict; @@ -527,8 +549,6 @@ public async Task Add_Block(Int64 target_user_id) } } -#pragma warning restore CS8602 // Dereference of a possibly null reference. - // We must: //// - Remove from source friends, DB (if present) //// - Remove from source friends, Cache (if present) @@ -573,22 +593,19 @@ public async Task Remove_Block(Int64 target_user_id) // source user must be signed in Int64 requester_user_id = TokenHelper.GetUserID(this); - if (requester_user_id == -1 || WebSocketManager.GetSharedDataForUser(requester_user_id) == null) + SharedUserData? sourceData = WebSocketManager.GetSharedDataForUser(requester_user_id); + if (requester_user_id == -1 || sourceData == null) { Response.StatusCode = (int)HttpStatusCode.Forbidden; return; } - SharedUserData? sourceData = WebSocketManager.GetSharedDataForUser(requester_user_id); - // Check blocked -#pragma warning disable CS8602 // Dereference of a possibly null reference. if (!sourceData.GetSocialContainer().Blocked.Contains(target_user_id)) { Response.StatusCode = (int)HttpStatusCode.Conflict; return; } -#pragma warning restore CS8602 // Dereference of a possibly null reference. // - Remove from block list (cache) sourceData.GetSocialContainer().Blocked.Remove(target_user_id); diff --git a/GenOnlineService/Database/Database.Social.cs b/GenOnlineService/Database/Database.Social.cs index d60876a..63ae0f2 100644 --- a/GenOnlineService/Database/Database.Social.cs +++ b/GenOnlineService/Database/Database.Social.cs @@ -100,6 +100,13 @@ public static class Social .Where(f => f.UserId1 == userId || f.UserId2 == userId) ); + private static readonly Func> _countFriends = + EF.CompileAsyncQuery( + (AppDbContext db, long userId) => + db.Friends + .Count(f => f.UserId1 == userId || f.UserId2 == userId) + ); + private static readonly Func> _getBlocked = EF.CompileAsyncQuery( (AppDbContext db, long userId) => @@ -139,6 +146,23 @@ public static async Task> GetFriends(AppDbContext db, long userId) } + public static async Task CountFriends(AppDbContext db, long userId) + { + try + { + return await _countFriends(db, userId); + } + catch (Exception ex) + { + Console.WriteLine($"[ERROR] CountFriends failed: {ex.Message}"); + SentrySdk.CaptureException(ex); + + // Treat an unreadable count as full so the friends limit is never bypassed. + return int.MaxValue; + } + } + + public static async Task> GetBlocked(AppDbContext db, long sourceUserId) { HashSet result = new();