diff --git a/DysonNetwork.Shared/Models/ChatRoom.cs b/DysonNetwork.Shared/Models/ChatRoom.cs index 5ab27b4..e74f1cd 100644 --- a/DysonNetwork.Shared/Models/ChatRoom.cs +++ b/DysonNetwork.Shared/Models/ChatRoom.cs @@ -53,8 +53,10 @@ public enum ChatTimeoutCauseType public class ChatTimeoutCause { + [MaxLength(4096)] public string? Reason { get; set; } = null; public ChatTimeoutCauseType Type { get; set; } public Guid? SenderId { get; set; } + public Instant? Since { get; set; } } public class SnChatMember : ModelBase diff --git a/DysonNetwork.Sphere/Chat/ChatRoomController.cs b/DysonNetwork.Sphere/Chat/ChatRoomController.cs index 0a058e5..dc40018 100644 --- a/DysonNetwork.Sphere/Chat/ChatRoomController.cs +++ b/DysonNetwork.Sphere/Chat/ChatRoomController.cs @@ -538,8 +538,7 @@ public class ChatRoomController( { if (currentUser is null) return Unauthorized(); var member = await db.ChatMembers - .Where(m => m.ChatRoomId == roomId && m.AccountId == Guid.Parse(currentUser.Id) && m.JoinedAt != null && - m.LeaveAt == null) + .Where(m => m.ChatRoomId == roomId && m.AccountId == Guid.Parse(currentUser.Id) && m.LeaveAt == null) .FirstOrDefaultAsync(); if (member is null) return StatusCode(403, "You need to be a member to see members of private chat room."); } @@ -630,7 +629,8 @@ public class ChatRoomController( var operatorMember = await db.ChatMembers .Where(p => p.AccountId == accountId && p.ChatRoomId == chatRoom.Id) .FirstOrDefaultAsync(); - if (operatorMember is null) return StatusCode(403, "You need to be a part of chat to invite member to the chat."); + if (operatorMember is null) + return StatusCode(403, "You need to be a part of chat to invite member to the chat."); // Handle realm-owned chat rooms if (chatRoom.RealmId is not null) @@ -813,6 +813,128 @@ public class ChatRoomController( return Ok(targetMember); } + public class ChatTimeoutRequest + { + [MaxLength(4096)] public string? Reason { get; set; } + public Instant TimeoutUntil { get; set; } + } + + [HttpPost("{roomId:guid}/members/{memberId:guid}/timeout")] + [Authorize] + public async Task TimeoutChatMember(Guid roomId, Guid memberId, [FromBody] ChatTimeoutRequest request) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + var accountId = Guid.Parse(currentUser.Id); + + var now = SystemClock.Instance.GetCurrentInstant(); + if (now >= request.TimeoutUntil) + return BadRequest("Timeout can only until a time in the future."); + + var chatRoom = await db.ChatRooms + .Where(r => r.Id == roomId) + .FirstOrDefaultAsync(); + if (chatRoom is null) return NotFound(); + + var operatorMember = await db.ChatMembers + .FirstOrDefaultAsync(m => m.AccountId == accountId && m.ChatRoomId == chatRoom.Id); + if (operatorMember is null) return BadRequest("You have not joined this chat room."); + + // Check if the chat room is owned by a realm + if (chatRoom.RealmId is not null) + { + if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, accountId, [RealmMemberRole.Moderator])) + return StatusCode(403, "You need at least be a realm moderator to timeout members."); + } + else if (chatRoom.Type == ChatRoomType.DirectMessage) + return BadRequest("You cannot timeout member in a direct message."); + else if (chatRoom.AccountId != accountId) + return StatusCode(403, "You need be the owner to timeout member in the chat."); + + // Find the target member + var member = await db.ChatMembers + .Where(m => m.AccountId == memberId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null) + .FirstOrDefaultAsync(); + if (member is null) return NotFound(); + + member.TimeoutCause = new ChatTimeoutCause + { + Reason = request.Reason, + SenderId = operatorMember.Id, + Type = ChatTimeoutCauseType.ByModerator, + Since = now + }; + member.TimeoutUntil = request.TimeoutUntil; + db.Update(member); + await db.SaveChangesAsync(); + _ = crs.PurgeRoomMembersCache(roomId); + + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "chatrooms.timeout", + Meta = + { + { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(roomId.ToString()) }, + { "account_id", Google.Protobuf.WellKnownTypes.Value.ForString(memberId.ToString()) } + }, + AccountId = currentUser.Id, + UserAgent = Request.Headers.UserAgent, + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() + }); + + return NoContent(); + } + + [HttpDelete("{roomId:guid}/members/{memberId:guid}/timeout")] + [Authorize] + public async Task RemoveChatMemberTimeout(Guid roomId, Guid memberId) + { + if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + var accountId = Guid.Parse(currentUser.Id); + + var chatRoom = await db.ChatRooms + .Where(r => r.Id == roomId) + .FirstOrDefaultAsync(); + if (chatRoom is null) return NotFound(); + + // Check if the chat room is owned by a realm + if (chatRoom.RealmId is not null) + { + if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, accountId, [RealmMemberRole.Moderator])) + return StatusCode(403, "You need at least be a realm moderator to remove members."); + } + else if (chatRoom.Type == ChatRoomType.DirectMessage && await crs.IsChatMember(chatRoom.Id, accountId)) + return StatusCode(403, "You need be part of the DM to update the chat."); + else if (chatRoom.AccountId != accountId) + return StatusCode(403, "You need be the owner to update the chat."); + + // Find the target member + var member = await db.ChatMembers + .Where(m => m.AccountId == memberId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null) + .FirstOrDefaultAsync(); + if (member is null) return NotFound(); + + member.TimeoutCause = null; + member.TimeoutUntil = null; + db.Update(member); + await db.SaveChangesAsync(); + _ = crs.PurgeRoomMembersCache(roomId); + + _ = als.CreateActionLogAsync(new CreateActionLogRequest + { + Action = "chatrooms.timeout.remove", + Meta = + { + { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(roomId.ToString()) }, + { "account_id", Google.Protobuf.WellKnownTypes.Value.ForString(memberId.ToString()) } + }, + AccountId = currentUser.Id, + UserAgent = Request.Headers.UserAgent, + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() + }); + + return NoContent(); + } + [HttpDelete("{roomId:guid}/members/{memberId:guid}")] [Authorize] public async Task RemoveChatMember(Guid roomId, Guid memberId) diff --git a/DysonNetwork.Sphere/Chat/ChatRoomService.cs b/DysonNetwork.Sphere/Chat/ChatRoomService.cs index 30d58ac..2633b67 100644 --- a/DysonNetwork.Sphere/Chat/ChatRoomService.cs +++ b/DysonNetwork.Sphere/Chat/ChatRoomService.cs @@ -92,11 +92,10 @@ public class ChatRoomService( .ToList(); if (directRoomsId.Count == 0) return rooms; - List members = directRoomsId.Count != 0 + var members = directRoomsId.Count != 0 ? await db.ChatMembers .Where(m => directRoomsId.Contains(m.ChatRoomId)) .Where(m => m.AccountId != userId) - // Ignored the joined at condition here to keep showing userinfo when other didn't accept the invite of DM .ToListAsync() : []; members = await LoadMemberAccounts(members); @@ -122,7 +121,6 @@ public class ChatRoomService( if (room.Type != ChatRoomType.DirectMessage) return room; var members = await db.ChatMembers .Where(m => m.ChatRoomId == room.Id && m.AccountId != userId) - .Where(m => m.JoinedAt != null && m.LeaveAt == null) .ToListAsync(); if (members.Count <= 0) return room;