Compare commits
2 Commits
c038ab9e3c
...
94f4e68120
| Author | SHA1 | Date | |
|---|---|---|---|
|
94f4e68120
|
|||
|
d5510f7e4d
|
@@ -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
|
||||
|
||||
@@ -12,6 +12,7 @@ using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat;
|
||||
|
||||
@@ -246,6 +247,7 @@ public partial class ChatController(
|
||||
public async Task<ActionResult> SendMessage([FromBody] SendMessageRequest request, Guid roomId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
request.Content = TextSanitizer.Sanitize(request.Content);
|
||||
if (string.IsNullOrWhiteSpace(request.Content) &&
|
||||
@@ -254,9 +256,12 @@ public partial class ChatController(
|
||||
!request.PollId.HasValue)
|
||||
return BadRequest("You cannot send an empty message.");
|
||||
|
||||
var member = await crs.GetRoomMember(Guid.Parse(currentUser.Id), roomId);
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var member = await crs.GetRoomMember(accountId, roomId);
|
||||
if (member == null)
|
||||
return StatusCode(403, "You need to be a member to send messages here.");
|
||||
if (member.TimeoutUntil.HasValue && member.TimeoutUntil.Value > now)
|
||||
return StatusCode(403, "You has been timed out in this chat.");
|
||||
|
||||
// Validate fund if provided
|
||||
if (request.FundId.HasValue)
|
||||
@@ -382,6 +387,7 @@ public partial class ChatController(
|
||||
public async Task<ActionResult> UpdateMessage([FromBody] SendMessageRequest request, Guid roomId, Guid messageId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
request.Content = TextSanitizer.Sanitize(request.Content);
|
||||
|
||||
@@ -392,9 +398,11 @@ public partial class ChatController(
|
||||
|
||||
if (message == null) return NotFound();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
if (message.Sender.AccountId != accountId)
|
||||
return StatusCode(403, "You can only edit your own messages.");
|
||||
if (message.Sender.TimeoutUntil.HasValue && message.Sender.TimeoutUntil.Value > now)
|
||||
return StatusCode(403, "You has been timed out in this chat.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Content) &&
|
||||
(request.AttachmentsId == null || request.AttachmentsId.Count == 0) &&
|
||||
@@ -402,23 +410,6 @@ public partial class ChatController(
|
||||
!request.PollId.HasValue)
|
||||
return BadRequest("You cannot send an empty message.");
|
||||
|
||||
// Validate reply and forward message IDs exist
|
||||
if (request.RepliedMessageId.HasValue)
|
||||
{
|
||||
var repliedMessage = await db.ChatMessages
|
||||
.FirstOrDefaultAsync(m => m.Id == request.RepliedMessageId.Value && m.ChatRoomId == roomId);
|
||||
if (repliedMessage == null)
|
||||
return BadRequest("The message you're replying to does not exist.");
|
||||
}
|
||||
|
||||
if (request.ForwardedMessageId.HasValue)
|
||||
{
|
||||
var forwardedMessage = await db.ChatMessages
|
||||
.FirstOrDefaultAsync(m => m.Id == request.ForwardedMessageId.Value);
|
||||
if (forwardedMessage == null)
|
||||
return BadRequest("The message you're forwarding does not exist.");
|
||||
}
|
||||
|
||||
// Update mentions based on new content and references
|
||||
var updatedMentions = await ExtractMentionedUsersAsync(request.Content, request.RepliedMessageId,
|
||||
request.ForwardedMessageId, roomId, accountId);
|
||||
|
||||
@@ -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<ActionResult> 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<ActionResult> 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<ActionResult> RemoveChatMember(Guid roomId, Guid memberId)
|
||||
|
||||
@@ -92,11 +92,10 @@ public class ChatRoomService(
|
||||
.ToList();
|
||||
if (directRoomsId.Count == 0) return rooms;
|
||||
|
||||
List<SnChatMember> 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;
|
||||
|
||||
@@ -707,14 +707,7 @@ public partial class ChatService(
|
||||
if (content is not null)
|
||||
message.Content = content;
|
||||
|
||||
if (meta is not null)
|
||||
message.Meta = meta;
|
||||
|
||||
if (repliedMessageId.HasValue)
|
||||
message.RepliedMessageId = repliedMessageId;
|
||||
|
||||
if (forwardedMessageId.HasValue)
|
||||
message.ForwardedMessageId = forwardedMessageId;
|
||||
// Update do not override meta, replies to and forwarded to
|
||||
|
||||
if (attachmentsId is not null)
|
||||
await UpdateFileReferencesForMessageAsync(message, attachmentsId);
|
||||
|
||||
@@ -4,6 +4,7 @@ using DysonNetwork.Sphere.Chat.Realtime;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat;
|
||||
@@ -81,8 +82,11 @@ public class RealtimeCallController(
|
||||
.Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
if (member == null)
|
||||
return StatusCode(403, "You need to be a member to join a call.");
|
||||
if (member.TimeoutUntil.HasValue && member.TimeoutUntil.Value > now)
|
||||
return StatusCode(403, "You has been timed out in this chat.");
|
||||
|
||||
// Get ongoing call
|
||||
var ongoingCall = await cs.GetCallOngoingAsync(roomId);
|
||||
@@ -150,12 +154,16 @@ public class RealtimeCallController(
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
var member = await db.ChatMembers
|
||||
.Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null)
|
||||
.Include(m => m.ChatRoom)
|
||||
.FirstOrDefaultAsync();
|
||||
if (member == null)
|
||||
return StatusCode(403, "You need to be a member to start a call.");
|
||||
if (member.TimeoutUntil.HasValue && member.TimeoutUntil.Value > now)
|
||||
return StatusCode(403, "You has been timed out in this chat.");
|
||||
|
||||
var ongoingCall = await cs.GetCallOngoingAsync(roomId);
|
||||
if (ongoingCall is not null) return StatusCode(423, "There is already an ongoing call inside the chatroom.");
|
||||
|
||||
Reference in New Issue
Block a user