🔀 Merge branch 'refactor/seprate-auth'
# Conflicts: # DysonNetwork.Sphere/Chat/Realtime/LiveKitService.cs # DysonNetwork.Sphere/Chat/RealtimeCallController.cs # DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs # DysonNetwork.sln.DotSettings.user
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.RegularExpressions;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Content;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -10,7 +12,12 @@ namespace DysonNetwork.Sphere.Chat;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/chat")]
|
||||
public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomService crs) : ControllerBase
|
||||
public partial class ChatController(
|
||||
AppDatabase db,
|
||||
ChatService cs,
|
||||
ChatRoomService crs,
|
||||
FileService.FileServiceClient files
|
||||
) : ControllerBase
|
||||
{
|
||||
public class MarkMessageReadRequest
|
||||
{
|
||||
@@ -32,10 +39,11 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
||||
[Authorize]
|
||||
public async Task<ActionResult<Dictionary<Guid, ChatSummaryResponse>>> GetChatSummary()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var unreadMessages = await cs.CountUnreadMessageForUser(currentUser.Id);
|
||||
var lastMessages = await cs.ListLastMessageForUser(currentUser.Id);
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
var unreadMessages = await cs.CountUnreadMessageForUser(accountId);
|
||||
var lastMessages = await cs.ListLastMessageForUser(accountId);
|
||||
|
||||
var result = unreadMessages.Keys
|
||||
.Union(lastMessages.Keys)
|
||||
@@ -65,7 +73,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
||||
public async Task<ActionResult<List<Message>>> ListMessages(Guid roomId, [FromQuery] int offset,
|
||||
[FromQuery] int take = 20)
|
||||
{
|
||||
var currentUser = HttpContext.Items["CurrentUser"] as Account.Account;
|
||||
var currentUser = HttpContext.Items["CurrentUser"] as Account;
|
||||
|
||||
var room = await db.ChatRooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||
if (room is null) return NotFound();
|
||||
@@ -74,8 +82,9 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
||||
{
|
||||
if (currentUser is null) return Unauthorized();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
var member = await db.ChatMembers
|
||||
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
|
||||
.Where(m => m.AccountId == accountId && m.ChatRoomId == roomId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (member == null || member.Role < ChatMemberRole.Member)
|
||||
return StatusCode(403, "You are not a member of this chat room.");
|
||||
@@ -88,11 +97,15 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
||||
.Where(m => m.ChatRoomId == roomId)
|
||||
.OrderByDescending(m => m.CreatedAt)
|
||||
.Include(m => m.Sender)
|
||||
.Include(m => m.Sender.Account)
|
||||
.Include(m => m.Sender.Account.Profile)
|
||||
.Skip(offset)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
|
||||
var members = messages.Select(m => m.Sender).DistinctBy(x => x.Id).ToList();
|
||||
members = await crs.LoadMemberAccounts(members);
|
||||
|
||||
foreach (var message in messages)
|
||||
message.Sender = members.First(x => x.Id == message.SenderId);
|
||||
|
||||
Response.Headers["X-Total"] = totalCount.ToString();
|
||||
|
||||
@@ -102,7 +115,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
||||
[HttpGet("{roomId:guid}/messages/{messageId:guid}")]
|
||||
public async Task<ActionResult<Message>> GetMessage(Guid roomId, Guid messageId)
|
||||
{
|
||||
var currentUser = HttpContext.Items["CurrentUser"] as Account.Account;
|
||||
var currentUser = HttpContext.Items["CurrentUser"] as Account;
|
||||
|
||||
var room = await db.ChatRooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||
if (room is null) return NotFound();
|
||||
@@ -111,8 +124,9 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
||||
{
|
||||
if (currentUser is null) return Unauthorized();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
var member = await db.ChatMembers
|
||||
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
|
||||
.Where(m => m.AccountId == accountId && m.ChatRoomId == roomId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (member == null || member.Role < ChatMemberRole.Member)
|
||||
return StatusCode(403, "You are not a member of this chat room.");
|
||||
@@ -121,11 +135,11 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
||||
var message = await db.ChatMessages
|
||||
.Where(m => m.Id == messageId && m.ChatRoomId == roomId)
|
||||
.Include(m => m.Sender)
|
||||
.Include(m => m.Sender.Account)
|
||||
.Include(m => m.Sender.Account.Profile)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (message is null) return NotFound();
|
||||
|
||||
message.Sender = await crs.LoadMemberAccount(message.Sender);
|
||||
|
||||
return Ok(message);
|
||||
}
|
||||
@@ -139,14 +153,14 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
||||
[RequiredPermission("global", "chat.messages.create")]
|
||||
public async Task<ActionResult> SendMessage([FromBody] SendMessageRequest request, Guid roomId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
request.Content = TextSanitizer.Sanitize(request.Content);
|
||||
if (string.IsNullOrWhiteSpace(request.Content) &&
|
||||
(request.AttachmentsId == null || request.AttachmentsId.Count == 0))
|
||||
return BadRequest("You cannot send an empty message.");
|
||||
|
||||
var member = await crs.GetRoomMember(currentUser.Id, roomId);
|
||||
var member = await crs.GetRoomMember(Guid.Parse(currentUser.Id), roomId);
|
||||
if (member == null || member.Role < ChatMemberRole.Member)
|
||||
return StatusCode(403, "You need to be a normal member to send messages here.");
|
||||
|
||||
@@ -162,12 +176,12 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
||||
message.Content = request.Content;
|
||||
if (request.AttachmentsId is not null)
|
||||
{
|
||||
var attachments = await db.Files
|
||||
.Where(f => request.AttachmentsId.Contains(f.Id))
|
||||
.ToListAsync();
|
||||
message.Attachments = attachments
|
||||
var queryRequest = new GetFileBatchRequest();
|
||||
queryRequest.Ids.AddRange(request.AttachmentsId);
|
||||
var queryResponse = await files.GetFileBatchAsync(queryRequest);
|
||||
message.Attachments = queryResponse.Files
|
||||
.OrderBy(f => request.AttachmentsId.IndexOf(f.Id))
|
||||
.Select(f => f.ToReferenceObject())
|
||||
.Select(CloudFileReferenceObject.FromProtoValue)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
@@ -216,20 +230,19 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
||||
[Authorize]
|
||||
public async Task<ActionResult> UpdateMessage([FromBody] SendMessageRequest request, Guid roomId, Guid messageId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
request.Content = TextSanitizer.Sanitize(request.Content);
|
||||
|
||||
var message = await db.ChatMessages
|
||||
.Include(m => m.Sender)
|
||||
.Include(m => m.Sender.Account)
|
||||
.Include(m => m.Sender.Account.Profile)
|
||||
.Include(message => message.ChatRoom)
|
||||
.FirstOrDefaultAsync(m => m.Id == messageId && m.ChatRoomId == roomId);
|
||||
|
||||
if (message == null) return NotFound();
|
||||
|
||||
if (message.Sender.AccountId != currentUser.Id)
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (message.Sender.AccountId != accountId)
|
||||
return StatusCode(403, "You can only edit your own messages.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Content) &&
|
||||
@@ -269,7 +282,7 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
||||
[Authorize]
|
||||
public async Task<ActionResult> DeleteMessage(Guid roomId, Guid messageId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var message = await db.ChatMessages
|
||||
.Include(m => m.Sender)
|
||||
@@ -278,7 +291,8 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
||||
|
||||
if (message == null) return NotFound();
|
||||
|
||||
if (message.Sender.AccountId != currentUser.Id)
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (message.Sender.AccountId != accountId)
|
||||
return StatusCode(403, "You can only delete your own messages.");
|
||||
|
||||
// Call service method to delete the message
|
||||
@@ -295,15 +309,16 @@ public partial class ChatController(AppDatabase db, ChatService cs, ChatRoomServ
|
||||
[HttpPost("{roomId:guid}/sync")]
|
||||
public async Task<ActionResult<SyncResponse>> GetSyncData([FromBody] SyncRequest request, Guid roomId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
var isMember = await db.ChatMembers
|
||||
.AnyAsync(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId);
|
||||
.AnyAsync(m => m.AccountId == accountId && m.ChatRoomId == roomId);
|
||||
if (!isMember)
|
||||
return StatusCode(403, "You are not a member of this chat room.");
|
||||
|
||||
var response = await cs.GetSyncDataAsync(roomId, request.LastSyncTimestamp);
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,8 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using NodaTime;
|
||||
using Account = DysonNetwork.Pass.Account.Account;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat;
|
||||
|
||||
@@ -38,7 +39,7 @@ public class ChatRoom : ModelBase, IIdentifiedResource
|
||||
public ICollection<ChatMemberTransmissionObject> DirectMembers { get; set; } =
|
||||
new List<ChatMemberTransmissionObject>();
|
||||
|
||||
public string ResourceIdentifier => $"chatroom/{Id}";
|
||||
public string ResourceIdentifier => $"chatroom:{Id}";
|
||||
}
|
||||
|
||||
public abstract class ChatMemberRole
|
||||
@@ -73,7 +74,7 @@ public class ChatMember : ModelBase
|
||||
public Guid ChatRoomId { get; set; }
|
||||
public ChatRoom ChatRoom { get; set; } = null!;
|
||||
public Guid AccountId { get; set; }
|
||||
public Account.Account Account { get; set; } = null!;
|
||||
[NotMapped] public Account Account { get; set; } = null!;
|
||||
|
||||
[MaxLength(1024)] public string? Nick { get; set; }
|
||||
|
||||
@@ -105,7 +106,7 @@ public class ChatMemberTransmissionObject : ModelBase
|
||||
public Guid Id { get; set; }
|
||||
public Guid ChatRoomId { get; set; }
|
||||
public Guid AccountId { get; set; }
|
||||
public Account.Account Account { get; set; } = null!;
|
||||
[NotMapped] public Account Account { get; set; } = null!;
|
||||
|
||||
[MaxLength(1024)] public string? Nick { get; set; }
|
||||
|
||||
|
@@ -1,11 +1,13 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using DysonNetwork.Shared;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Sphere.Localization;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using DysonNetwork.Sphere.Realm;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using NodaTime;
|
||||
@@ -16,14 +18,14 @@ namespace DysonNetwork.Sphere.Chat;
|
||||
[Route("/api/chat")]
|
||||
public class ChatRoomController(
|
||||
AppDatabase db,
|
||||
FileReferenceService fileRefService,
|
||||
ChatRoomService crs,
|
||||
RealmService rs,
|
||||
ActionLogService als,
|
||||
NotificationService nty,
|
||||
RelationshipService rels,
|
||||
IStringLocalizer<NotificationResource> localizer,
|
||||
AccountEventService aes
|
||||
AccountService.AccountServiceClient accounts,
|
||||
FileService.FileServiceClient files,
|
||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||
ActionLogService.ActionLogServiceClient als,
|
||||
PusherService.PusherServiceClient pusher
|
||||
) : ControllerBase
|
||||
{
|
||||
[HttpGet("{id:guid}")]
|
||||
@@ -36,8 +38,8 @@ public class ChatRoomController(
|
||||
if (chatRoom is null) return NotFound();
|
||||
if (chatRoom.Type != ChatRoomType.DirectMessage) return Ok(chatRoom);
|
||||
|
||||
if (HttpContext.Items["CurrentUser"] is Account.Account currentUser)
|
||||
chatRoom = await crs.LoadDirectMessageMembers(chatRoom, currentUser.Id);
|
||||
if (HttpContext.Items["CurrentUser"] is Account currentUser)
|
||||
chatRoom = await crs.LoadDirectMessageMembers(chatRoom, Guid.Parse(currentUser.Id));
|
||||
|
||||
return Ok(chatRoom);
|
||||
}
|
||||
@@ -46,18 +48,18 @@ public class ChatRoomController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<ChatRoom>>> ListJoinedChatRooms()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var chatRooms = await db.ChatMembers
|
||||
.Where(m => m.AccountId == userId)
|
||||
.Where(m => m.AccountId == accountId)
|
||||
.Where(m => m.JoinedAt != null)
|
||||
.Where(m => m.LeaveAt == null)
|
||||
.Include(m => m.ChatRoom)
|
||||
.Select(m => m.ChatRoom)
|
||||
.ToListAsync();
|
||||
chatRooms = await crs.LoadDirectMessageMembers(chatRooms, userId);
|
||||
chatRooms = await crs.LoadDirectMessageMembers(chatRooms, accountId);
|
||||
chatRooms = await crs.SortChatRoomByLastMessage(chatRooms);
|
||||
|
||||
return Ok(chatRooms);
|
||||
@@ -72,21 +74,29 @@ public class ChatRoomController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<ChatRoom>> CreateDirectMessage([FromBody] DirectMessageRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId);
|
||||
var relatedUser = await accounts.GetAccountAsync(
|
||||
new GetAccountRequest { Id = request.RelatedUserId.ToString() }
|
||||
);
|
||||
if (relatedUser is null)
|
||||
return BadRequest("Related user was not found");
|
||||
|
||||
if (await rels.HasRelationshipWithStatus(currentUser.Id, relatedUser.Id, RelationshipStatus.Blocked))
|
||||
var hasBlocked = await accounts.HasRelationshipAsync(new GetRelationshipRequest()
|
||||
{
|
||||
AccountId = currentUser.Id,
|
||||
RelatedId = request.RelatedUserId.ToString(),
|
||||
Status = -100
|
||||
});
|
||||
if (hasBlocked?.Value ?? false)
|
||||
return StatusCode(403, "You cannot create direct message with a user that blocked you.");
|
||||
|
||||
// Check if DM already exists between these users
|
||||
var existingDm = await db.ChatRooms
|
||||
.Include(c => c.Members)
|
||||
.Where(c => c.Type == ChatRoomType.DirectMessage && c.Members.Count == 2)
|
||||
.Where(c => c.Members.Any(m => m.AccountId == currentUser.Id))
|
||||
.Where(c => c.Members.Any(m => m.AccountId == Guid.Parse(currentUser.Id)))
|
||||
.Where(c => c.Members.Any(m => m.AccountId == request.RelatedUserId))
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
@@ -102,9 +112,9 @@ public class ChatRoomController(
|
||||
{
|
||||
new()
|
||||
{
|
||||
AccountId = currentUser.Id,
|
||||
AccountId = Guid.Parse(currentUser.Id),
|
||||
Role = ChatMemberRole.Owner,
|
||||
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
||||
JoinedAt = Instant.FromDateTimeUtc(DateTime.UtcNow)
|
||||
},
|
||||
new()
|
||||
{
|
||||
@@ -118,10 +128,14 @@ public class ChatRoomController(
|
||||
db.ChatRooms.Add(dmRoom);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomCreate,
|
||||
new Dictionary<string, object> { { "chatroom_id", dmRoom.Id } }, Request
|
||||
);
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "chatrooms.create",
|
||||
Meta = { { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(dmRoom.Id.ToString()) } },
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent,
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
||||
});
|
||||
|
||||
var invitedMember = dmRoom.Members.First(m => m.AccountId == request.RelatedUserId);
|
||||
invitedMember.ChatRoom = dmRoom;
|
||||
@@ -130,18 +144,18 @@ public class ChatRoomController(
|
||||
return Ok(dmRoom);
|
||||
}
|
||||
|
||||
[HttpGet("direct/{userId:guid}")]
|
||||
[HttpGet("direct/{accountId:guid}")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<ChatRoom>> GetDirectChatRoom(Guid userId)
|
||||
public async Task<ActionResult<ChatRoom>> GetDirectChatRoom(Guid accountId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var room = await db.ChatRooms
|
||||
.Include(c => c.Members)
|
||||
.Where(c => c.Type == ChatRoomType.DirectMessage && c.Members.Count == 2)
|
||||
.Where(c => c.Members.Any(m => m.AccountId == currentUser.Id))
|
||||
.Where(c => c.Members.Any(m => m.AccountId == userId))
|
||||
.Where(c => c.Members.Any(m => m.AccountId == Guid.Parse(currentUser.Id)))
|
||||
.Where(c => c.Members.Any(m => m.AccountId == accountId))
|
||||
.FirstOrDefaultAsync();
|
||||
if (room is null) return NotFound();
|
||||
|
||||
@@ -164,7 +178,7 @@ public class ChatRoomController(
|
||||
[RequiredPermission("global", "chat.create")]
|
||||
public async Task<ActionResult<ChatRoom>> CreateChatRoom(ChatRoomRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
|
||||
if (request.Name is null) return BadRequest("You cannot create a chat room without a name.");
|
||||
|
||||
var chatRoom = new ChatRoom
|
||||
@@ -179,30 +193,60 @@ public class ChatRoomController(
|
||||
new()
|
||||
{
|
||||
Role = ChatMemberRole.Owner,
|
||||
AccountId = currentUser.Id,
|
||||
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
||||
AccountId = Guid.Parse(currentUser.Id),
|
||||
JoinedAt = Instant.FromDateTimeUtc(DateTime.UtcNow)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (request.RealmId is not null)
|
||||
{
|
||||
if (!await rs.IsMemberWithRole(request.RealmId.Value, currentUser.Id, RealmMemberRole.Moderator))
|
||||
if (!await rs.IsMemberWithRole(request.RealmId.Value, Guid.Parse(currentUser.Id),
|
||||
RealmMemberRole.Moderator))
|
||||
return StatusCode(403, "You need at least be a moderator to create chat linked to the realm.");
|
||||
chatRoom.RealmId = request.RealmId;
|
||||
}
|
||||
|
||||
if (request.PictureId is not null)
|
||||
{
|
||||
chatRoom.Picture = (await db.Files.FindAsync(request.PictureId))?.ToReferenceObject();
|
||||
if (chatRoom.Picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
||||
try
|
||||
{
|
||||
var fileResponse = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId });
|
||||
if (fileResponse == null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
||||
chatRoom.Picture = CloudFileReferenceObject.FromProtoValue(fileResponse);
|
||||
|
||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||
{
|
||||
FileId = fileResponse.Id,
|
||||
Usage = "chatroom.picture",
|
||||
ResourceId = chatRoom.ResourceIdentifier,
|
||||
});
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
||||
{
|
||||
return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
||||
}
|
||||
}
|
||||
|
||||
if (request.BackgroundId is not null)
|
||||
{
|
||||
chatRoom.Background = (await db.Files.FindAsync(request.BackgroundId))?.ToReferenceObject();
|
||||
if (chatRoom.Background is null)
|
||||
try
|
||||
{
|
||||
var fileResponse = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId });
|
||||
if (fileResponse == null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
||||
chatRoom.Background = CloudFileReferenceObject.FromProtoValue(fileResponse);
|
||||
|
||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||
{
|
||||
FileId = fileResponse.Id,
|
||||
Usage = "chatroom.background",
|
||||
ResourceId = chatRoom.ResourceIdentifier,
|
||||
});
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
||||
{
|
||||
return BadRequest("Invalid background id, unable to find the file on cloud.");
|
||||
}
|
||||
}
|
||||
|
||||
db.ChatRooms.Add(chatRoom);
|
||||
@@ -211,23 +255,33 @@ public class ChatRoomController(
|
||||
var chatRoomResourceId = $"chatroom:{chatRoom.Id}";
|
||||
|
||||
if (chatRoom.Picture is not null)
|
||||
await fileRefService.CreateReferenceAsync(
|
||||
chatRoom.Picture.Id,
|
||||
"chat.room.picture",
|
||||
chatRoomResourceId
|
||||
);
|
||||
{
|
||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||
{
|
||||
FileId = chatRoom.Picture.Id,
|
||||
Usage = "chat.room.picture",
|
||||
ResourceId = chatRoomResourceId
|
||||
});
|
||||
}
|
||||
|
||||
if (chatRoom.Background is not null)
|
||||
await fileRefService.CreateReferenceAsync(
|
||||
chatRoom.Background.Id,
|
||||
"chat.room.background",
|
||||
chatRoomResourceId
|
||||
);
|
||||
{
|
||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||
{
|
||||
FileId = chatRoom.Background.Id,
|
||||
Usage = "chat.room.background",
|
||||
ResourceId = chatRoomResourceId
|
||||
});
|
||||
}
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomCreate,
|
||||
new Dictionary<string, object> { { "chatroom_id", chatRoom.Id } }, Request
|
||||
);
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "chatrooms.create",
|
||||
Meta = { { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(chatRoom.Id.ToString()) } },
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent,
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
||||
});
|
||||
|
||||
return Ok(chatRoom);
|
||||
}
|
||||
@@ -236,7 +290,7 @@ public class ChatRoomController(
|
||||
[HttpPatch("{id:guid}")]
|
||||
public async Task<ActionResult<ChatRoom>> UpdateChatRoom(Guid id, [FromBody] ChatRoomRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
|
||||
|
||||
var chatRoom = await db.ChatRooms
|
||||
.Where(e => e.Id == id)
|
||||
@@ -245,16 +299,17 @@ public class ChatRoomController(
|
||||
|
||||
if (chatRoom.RealmId is not null)
|
||||
{
|
||||
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, currentUser.Id, RealmMemberRole.Moderator))
|
||||
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, Guid.Parse(currentUser.Id),
|
||||
RealmMemberRole.Moderator))
|
||||
return StatusCode(403, "You need at least be a realm moderator to update the chat.");
|
||||
}
|
||||
else if (!await crs.IsMemberWithRole(chatRoom.Id, currentUser.Id, ChatMemberRole.Moderator))
|
||||
else if (!await crs.IsMemberWithRole(chatRoom.Id, Guid.Parse(currentUser.Id), ChatMemberRole.Moderator))
|
||||
return StatusCode(403, "You need at least be a moderator to update the chat.");
|
||||
|
||||
if (request.RealmId is not null)
|
||||
{
|
||||
var member = await db.RealmMembers
|
||||
.Where(m => m.AccountId == currentUser.Id)
|
||||
.Where(m => m.AccountId == Guid.Parse(currentUser.Id))
|
||||
.Where(m => m.RealmId == request.RealmId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (member is null || member.Role < RealmMemberRole.Moderator)
|
||||
@@ -264,38 +319,62 @@ public class ChatRoomController(
|
||||
|
||||
if (request.PictureId is not null)
|
||||
{
|
||||
var picture = await db.Files.FindAsync(request.PictureId);
|
||||
if (picture is null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
||||
try
|
||||
{
|
||||
var fileResponse = await files.GetFileAsync(new GetFileRequest { Id = request.PictureId });
|
||||
if (fileResponse == null) return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
||||
|
||||
// Remove old references for pictures
|
||||
await fileRefService.DeleteResourceReferencesAsync(chatRoom.ResourceIdentifier, "chat.room.picture");
|
||||
// Remove old references for pictures
|
||||
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
||||
{
|
||||
ResourceId = chatRoom.ResourceIdentifier,
|
||||
Usage = "chat.room.picture"
|
||||
});
|
||||
|
||||
// Add a new reference
|
||||
await fileRefService.CreateReferenceAsync(
|
||||
picture.Id,
|
||||
"chat.room.picture",
|
||||
chatRoom.ResourceIdentifier
|
||||
);
|
||||
// Add a new reference
|
||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||
{
|
||||
FileId = fileResponse.Id,
|
||||
Usage = "chat.room.picture",
|
||||
ResourceId = chatRoom.ResourceIdentifier
|
||||
});
|
||||
|
||||
chatRoom.Picture = picture.ToReferenceObject();
|
||||
chatRoom.Picture = CloudFileReferenceObject.FromProtoValue(fileResponse);
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
||||
{
|
||||
return BadRequest("Invalid picture id, unable to find the file on cloud.");
|
||||
}
|
||||
}
|
||||
|
||||
if (request.BackgroundId is not null)
|
||||
{
|
||||
var background = await db.Files.FindAsync(request.BackgroundId);
|
||||
if (background is null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
||||
try
|
||||
{
|
||||
var fileResponse = await files.GetFileAsync(new GetFileRequest { Id = request.BackgroundId });
|
||||
if (fileResponse == null) return BadRequest("Invalid background id, unable to find the file on cloud.");
|
||||
|
||||
// Remove old references for backgrounds
|
||||
await fileRefService.DeleteResourceReferencesAsync(chatRoom.ResourceIdentifier, "chat.room.background");
|
||||
// Remove old references for backgrounds
|
||||
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
||||
{
|
||||
ResourceId = chatRoom.ResourceIdentifier,
|
||||
Usage = "chat.room.background"
|
||||
});
|
||||
|
||||
// Add a new reference
|
||||
await fileRefService.CreateReferenceAsync(
|
||||
background.Id,
|
||||
"chat.room.background",
|
||||
chatRoom.ResourceIdentifier
|
||||
);
|
||||
// Add a new reference
|
||||
await fileRefs.CreateReferenceAsync(new CreateReferenceRequest
|
||||
{
|
||||
FileId = fileResponse.Id,
|
||||
Usage = "chat.room.background",
|
||||
ResourceId = chatRoom.ResourceIdentifier
|
||||
});
|
||||
|
||||
chatRoom.Background = background.ToReferenceObject();
|
||||
chatRoom.Background = CloudFileReferenceObject.FromProtoValue(fileResponse);
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
||||
{
|
||||
return BadRequest("Invalid background id, unable to find the file on cloud.");
|
||||
}
|
||||
}
|
||||
|
||||
if (request.Name is not null)
|
||||
@@ -310,10 +389,14 @@ public class ChatRoomController(
|
||||
db.ChatRooms.Update(chatRoom);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomUpdate,
|
||||
new Dictionary<string, object> { { "chatroom_id", chatRoom.Id } }, Request
|
||||
);
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "chatrooms.update",
|
||||
Meta = { { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(chatRoom.Id.ToString()) } },
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent,
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
||||
});
|
||||
|
||||
return Ok(chatRoom);
|
||||
}
|
||||
@@ -321,7 +404,7 @@ public class ChatRoomController(
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<ActionResult> DeleteChatRoom(Guid id)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
|
||||
|
||||
var chatRoom = await db.ChatRooms
|
||||
.Where(e => e.Id == id)
|
||||
@@ -330,24 +413,32 @@ public class ChatRoomController(
|
||||
|
||||
if (chatRoom.RealmId is not null)
|
||||
{
|
||||
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, currentUser.Id, RealmMemberRole.Moderator))
|
||||
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, Guid.Parse(currentUser.Id),
|
||||
RealmMemberRole.Moderator))
|
||||
return StatusCode(403, "You need at least be a realm moderator to delete the chat.");
|
||||
}
|
||||
else if (!await crs.IsMemberWithRole(chatRoom.Id, currentUser.Id, ChatMemberRole.Owner))
|
||||
else if (!await crs.IsMemberWithRole(chatRoom.Id, Guid.Parse(currentUser.Id), ChatMemberRole.Owner))
|
||||
return StatusCode(403, "You need at least be the owner to delete the chat.");
|
||||
|
||||
var chatRoomResourceId = $"chatroom:{chatRoom.Id}";
|
||||
|
||||
// Delete all file references for this chat room
|
||||
await fileRefService.DeleteResourceReferencesAsync(chatRoomResourceId);
|
||||
await fileRefs.DeleteResourceReferencesAsync(new DeleteResourceReferencesRequest
|
||||
{
|
||||
ResourceId = chatRoomResourceId
|
||||
});
|
||||
|
||||
db.ChatRooms.Remove(chatRoom);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomDelete,
|
||||
new Dictionary<string, object> { { "chatroom_id", chatRoom.Id } }, Request
|
||||
);
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "chatrooms.delete",
|
||||
Meta = { { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(chatRoom.Id.ToString()) } },
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent,
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
||||
});
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
@@ -356,26 +447,28 @@ public class ChatRoomController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<ChatMember>> GetRoomIdentity(Guid roomId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var member = await db.ChatMembers
|
||||
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
|
||||
.Include(m => m.Account)
|
||||
.Include(m => m.Account.Profile)
|
||||
.Where(m => m.AccountId == Guid.Parse(currentUser.Id) && m.ChatRoomId == roomId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (member == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(member);
|
||||
return Ok(await crs.LoadMemberAccount(member));
|
||||
}
|
||||
|
||||
[HttpGet("{roomId:guid}/members")]
|
||||
public async Task<ActionResult<List<ChatMember>>> ListMembers(Guid roomId, [FromQuery] int take = 20,
|
||||
[FromQuery] int skip = 0, [FromQuery] bool withStatus = false, [FromQuery] string? status = null)
|
||||
public async Task<ActionResult<List<ChatMember>>> ListMembers(Guid roomId,
|
||||
[FromQuery] int take = 20,
|
||||
[FromQuery] int skip = 0,
|
||||
[FromQuery] bool withStatus = false,
|
||||
[FromQuery] string? status = null
|
||||
)
|
||||
{
|
||||
var currentUser = HttpContext.Items["CurrentUser"] as Account.Account;
|
||||
var currentUser = HttpContext.Items["CurrentUser"] as Account;
|
||||
|
||||
var room = await db.ChatRooms
|
||||
.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||
@@ -385,57 +478,54 @@ public class ChatRoomController(
|
||||
{
|
||||
if (currentUser is null) return Unauthorized();
|
||||
var member = await db.ChatMembers
|
||||
.FirstOrDefaultAsync(m => m.ChatRoomId == roomId && m.AccountId == currentUser.Id);
|
||||
.FirstOrDefaultAsync(m => m.ChatRoomId == roomId && m.AccountId == Guid.Parse(currentUser.Id));
|
||||
if (member is null) return StatusCode(403, "You need to be a member to see members of private chat room.");
|
||||
}
|
||||
|
||||
IQueryable<ChatMember> query = db.ChatMembers
|
||||
var query = db.ChatMembers
|
||||
.Where(m => m.ChatRoomId == roomId)
|
||||
.Where(m => m.LeaveAt == null) // Add this condition to exclude left members
|
||||
.Include(m => m.Account)
|
||||
.Include(m => m.Account.Profile);
|
||||
.Where(m => m.LeaveAt == null);
|
||||
|
||||
if (withStatus)
|
||||
{
|
||||
var members = await query
|
||||
.OrderBy(m => m.JoinedAt)
|
||||
.ToListAsync();
|
||||
// if (withStatus)
|
||||
// {
|
||||
// var members = await query
|
||||
// .OrderBy(m => m.JoinedAt)
|
||||
// .ToListAsync();
|
||||
//
|
||||
// var memberStatuses = await aes.GetStatuses(members.Select(m => m.AccountId).ToList());
|
||||
//
|
||||
// if (!string.IsNullOrEmpty(status))
|
||||
// {
|
||||
// members = members.Where(m =>
|
||||
// memberStatuses.TryGetValue(m.AccountId, out var s) && s.Label != null &&
|
||||
// s.Label.Equals(status, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
// }
|
||||
//
|
||||
// members = members.OrderByDescending(m => memberStatuses.TryGetValue(m.AccountId, out var s) && s.IsOnline)
|
||||
// .ToList();
|
||||
//
|
||||
// var total = members.Count;
|
||||
// Response.Headers.Append("X-Total", total.ToString());
|
||||
//
|
||||
// var result = members.Skip(skip).Take(take).ToList();
|
||||
//
|
||||
// return Ok(await crs.LoadMemberAccounts(result));
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
var total = await query.CountAsync();
|
||||
Response.Headers.Append("X-Total", total.ToString());
|
||||
|
||||
var memberStatuses = await aes.GetStatuses(members.Select(m => m.AccountId).ToList());
|
||||
var members = await query
|
||||
.OrderBy(m => m.JoinedAt)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(status))
|
||||
{
|
||||
members = members.Where(m =>
|
||||
memberStatuses.TryGetValue(m.AccountId, out var s) && s.Label != null &&
|
||||
s.Label.Equals(status, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
|
||||
members = members.OrderByDescending(m => memberStatuses.TryGetValue(m.AccountId, out var s) && s.IsOnline)
|
||||
.ToList();
|
||||
|
||||
var total = members.Count;
|
||||
Response.Headers.Append("X-Total", total.ToString());
|
||||
|
||||
var result = members.Skip(skip).Take(take).ToList();
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
var total = await query.CountAsync();
|
||||
Response.Headers.Append("X-Total", total.ToString());
|
||||
|
||||
var members = await query
|
||||
.OrderBy(m => m.JoinedAt)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(members);
|
||||
}
|
||||
return Ok(await crs.LoadMemberAccounts(members));
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class ChatMemberRequest
|
||||
{
|
||||
@@ -448,13 +538,23 @@ public class ChatRoomController(
|
||||
public async Task<ActionResult<ChatMember>> InviteMember(Guid roomId,
|
||||
[FromBody] ChatMemberRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId);
|
||||
if (relatedUser is null) return BadRequest("Related user was not found");
|
||||
// Get related user account
|
||||
var relatedUser =
|
||||
await accounts.GetAccountAsync(new GetAccountRequest { Id = request.RelatedUserId.ToString() });
|
||||
if (relatedUser == null) return BadRequest("Related user was not found");
|
||||
|
||||
if (await rels.HasRelationshipWithStatus(currentUser.Id, relatedUser.Id, RelationshipStatus.Blocked))
|
||||
// Check if the user has blocked the current user
|
||||
var relationship = await accounts.GetRelationshipAsync(new GetRelationshipRequest
|
||||
{
|
||||
AccountId = currentUser.Id,
|
||||
RelatedId = relatedUser.Id,
|
||||
Status = -100
|
||||
});
|
||||
|
||||
if (relationship != null && relationship.Relationship.Status == -100)
|
||||
return StatusCode(403, "You cannot invite a user that blocked you.");
|
||||
|
||||
var chatRoom = await db.ChatRooms
|
||||
@@ -466,7 +566,7 @@ public class ChatRoomController(
|
||||
if (chatRoom.RealmId is not null)
|
||||
{
|
||||
var realmMember = await db.RealmMembers
|
||||
.Where(m => m.AccountId == userId)
|
||||
.Where(m => m.AccountId == accountId)
|
||||
.Where(m => m.RealmId == chatRoom.RealmId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (realmMember is null || realmMember.Role < RealmMemberRole.Moderator)
|
||||
@@ -475,7 +575,7 @@ public class ChatRoomController(
|
||||
else
|
||||
{
|
||||
var chatMember = await db.ChatMembers
|
||||
.Where(m => m.AccountId == userId)
|
||||
.Where(m => m.AccountId == accountId)
|
||||
.Where(m => m.ChatRoomId == roomId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (chatMember is null) return StatusCode(403, "You are not even a member of the targeted chat room.");
|
||||
@@ -496,7 +596,7 @@ public class ChatRoomController(
|
||||
|
||||
var newMember = new ChatMember
|
||||
{
|
||||
AccountId = relatedUser.Id,
|
||||
AccountId = Guid.Parse(relatedUser.Id),
|
||||
ChatRoomId = roomId,
|
||||
Role = request.Role,
|
||||
};
|
||||
@@ -507,10 +607,18 @@ public class ChatRoomController(
|
||||
newMember.ChatRoom = chatRoom;
|
||||
await _SendInviteNotify(newMember, currentUser);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomInvite,
|
||||
new Dictionary<string, object> { { "chatroom_id", chatRoom.Id }, { "account_id", relatedUser.Id } }, Request
|
||||
);
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "chatrooms.invite",
|
||||
Meta =
|
||||
{
|
||||
{ "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(chatRoom.Id.ToString()) },
|
||||
{ "account_id", Google.Protobuf.WellKnownTypes.Value.ForString(relatedUser.Id.ToString()) }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent,
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
||||
});
|
||||
|
||||
return Ok(newMember);
|
||||
}
|
||||
@@ -519,36 +627,34 @@ public class ChatRoomController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<ChatMember>>> ListChatInvites()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
if (HttpContext.Items["CurrentUser"] is not Shared.Proto.Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var members = await db.ChatMembers
|
||||
.Where(m => m.AccountId == userId)
|
||||
.Where(m => m.AccountId == accountId)
|
||||
.Where(m => m.JoinedAt == null)
|
||||
.Include(e => e.ChatRoom)
|
||||
.Include(e => e.Account)
|
||||
.Include(e => e.Account.Profile)
|
||||
.ToListAsync();
|
||||
|
||||
var chatRooms = members.Select(m => m.ChatRoom).ToList();
|
||||
var directMembers =
|
||||
(await crs.LoadDirectMessageMembers(chatRooms, userId)).ToDictionary(c => c.Id, c => c.Members);
|
||||
(await crs.LoadDirectMessageMembers(chatRooms, accountId)).ToDictionary(c => c.Id, c => c.Members);
|
||||
|
||||
foreach (var member in members.Where(member => member.ChatRoom.Type == ChatRoomType.DirectMessage))
|
||||
member.ChatRoom.Members = directMembers[member.ChatRoom.Id];
|
||||
|
||||
return members.ToList();
|
||||
|
||||
return Ok(await crs.LoadMemberAccounts(members));
|
||||
}
|
||||
|
||||
[HttpPost("invites/{roomId:guid}/accept")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<ChatRoom>> AcceptChatInvite(Guid roomId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var member = await db.ChatMembers
|
||||
.Where(m => m.AccountId == userId)
|
||||
.Where(m => m.AccountId == accountId)
|
||||
.Where(m => m.ChatRoomId == roomId)
|
||||
.Where(m => m.JoinedAt == null)
|
||||
.FirstOrDefaultAsync();
|
||||
@@ -559,10 +665,14 @@ public class ChatRoomController(
|
||||
await db.SaveChangesAsync();
|
||||
_ = crs.PurgeRoomMembersCache(roomId);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomJoin,
|
||||
new Dictionary<string, object> { { "chatroom_id", roomId } }, Request
|
||||
);
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = ActionLogType.ChatroomJoin,
|
||||
Meta = { { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(roomId.ToString()) } },
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent,
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
||||
});
|
||||
|
||||
return Ok(member);
|
||||
}
|
||||
@@ -571,11 +681,11 @@ public class ChatRoomController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult> DeclineChatInvite(Guid roomId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
var userId = currentUser.Id;
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var member = await db.ChatMembers
|
||||
.Where(m => m.AccountId == userId)
|
||||
.Where(m => m.AccountId == accountId)
|
||||
.Where(m => m.ChatRoomId == roomId)
|
||||
.Where(m => m.JoinedAt == null)
|
||||
.FirstOrDefaultAsync();
|
||||
@@ -600,15 +710,16 @@ public class ChatRoomController(
|
||||
[FromBody] ChatMemberNotifyRequest request
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var chatRoom = await db.ChatRooms
|
||||
.Where(r => r.Id == roomId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (chatRoom is null) return NotFound();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
var targetMember = await db.ChatMembers
|
||||
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
|
||||
.Where(m => m.AccountId == accountId && m.ChatRoomId == roomId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (targetMember is null) return BadRequest("You have not joined this chat room.");
|
||||
if (request.NotifyLevel is not null)
|
||||
@@ -629,7 +740,7 @@ public class ChatRoomController(
|
||||
public async Task<ActionResult<ChatMember>> UpdateChatMemberRole(Guid roomId, Guid memberId, [FromBody] int newRole)
|
||||
{
|
||||
if (newRole >= ChatMemberRole.Owner) return BadRequest("Unable to set chat member to owner or greater role.");
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var chatRoom = await db.ChatRooms
|
||||
.Where(r => r.Id == roomId)
|
||||
@@ -640,7 +751,7 @@ public class ChatRoomController(
|
||||
if (chatRoom.RealmId is not null)
|
||||
{
|
||||
var realmMember = await db.RealmMembers
|
||||
.Where(m => m.AccountId == currentUser.Id)
|
||||
.Where(m => m.AccountId == Guid.Parse(currentUser.Id))
|
||||
.Where(m => m.RealmId == chatRoom.RealmId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (realmMember is null || realmMember.Role < RealmMemberRole.Moderator)
|
||||
@@ -657,7 +768,7 @@ public class ChatRoomController(
|
||||
if (
|
||||
!await crs.IsMemberWithRole(
|
||||
chatRoom.Id,
|
||||
currentUser.Id,
|
||||
Guid.Parse(currentUser.Id),
|
||||
ChatMemberRole.Moderator,
|
||||
targetMember.Role,
|
||||
newRole
|
||||
@@ -671,12 +782,19 @@ public class ChatRoomController(
|
||||
|
||||
await crs.PurgeRoomMembersCache(roomId);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.RealmAdjustRole,
|
||||
new Dictionary<string, object>
|
||||
{ { "chatroom_id", roomId }, { "account_id", memberId }, { "new_role", newRole } },
|
||||
Request
|
||||
);
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "chatrooms.role.edit",
|
||||
Meta =
|
||||
{
|
||||
{ "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(roomId.ToString()) },
|
||||
{ "account_id", Google.Protobuf.WellKnownTypes.Value.ForString(memberId.ToString()) },
|
||||
{ "new_role", Google.Protobuf.WellKnownTypes.Value.ForNumber(newRole) }
|
||||
},
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent,
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
||||
});
|
||||
|
||||
return Ok(targetMember);
|
||||
}
|
||||
@@ -688,7 +806,7 @@ public class ChatRoomController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult> RemoveChatMember(Guid roomId, Guid memberId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var chatRoom = await db.ChatRooms
|
||||
.Where(r => r.Id == roomId)
|
||||
@@ -698,12 +816,13 @@ public class ChatRoomController(
|
||||
// Check if the chat room is owned by a realm
|
||||
if (chatRoom.RealmId is not null)
|
||||
{
|
||||
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, currentUser.Id, RealmMemberRole.Moderator))
|
||||
if (!await rs.IsMemberWithRole(chatRoom.RealmId.Value, Guid.Parse(currentUser.Id),
|
||||
RealmMemberRole.Moderator))
|
||||
return StatusCode(403, "You need at least be a realm moderator to remove members.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!await crs.IsMemberWithRole(chatRoom.Id, currentUser.Id, ChatMemberRole.Moderator))
|
||||
if (!await crs.IsMemberWithRole(chatRoom.Id, Guid.Parse(currentUser.Id), ChatMemberRole.Moderator))
|
||||
return StatusCode(403, "You need at least be a moderator to remove members.");
|
||||
|
||||
// Find the target member
|
||||
@@ -720,10 +839,18 @@ public class ChatRoomController(
|
||||
await db.SaveChangesAsync();
|
||||
_ = crs.PurgeRoomMembersCache(roomId);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomKick,
|
||||
new Dictionary<string, object> { { "chatroom_id", roomId }, { "account_id", memberId } }, Request
|
||||
);
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = "chatrooms.kick",
|
||||
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();
|
||||
}
|
||||
@@ -736,7 +863,7 @@ public class ChatRoomController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<ChatRoom>> JoinChatRoom(Guid roomId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var chatRoom = await db.ChatRooms
|
||||
.Where(r => r.Id == roomId)
|
||||
@@ -746,13 +873,13 @@ public class ChatRoomController(
|
||||
return StatusCode(403, "This chat room isn't a community. You need an invitation to join.");
|
||||
|
||||
var existingMember = await db.ChatMembers
|
||||
.FirstOrDefaultAsync(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId);
|
||||
.FirstOrDefaultAsync(m => m.AccountId == Guid.Parse(currentUser.Id) && m.ChatRoomId == roomId);
|
||||
if (existingMember != null)
|
||||
return BadRequest("You are already a member of this chat room.");
|
||||
|
||||
var newMember = new ChatMember
|
||||
{
|
||||
AccountId = currentUser.Id,
|
||||
AccountId = Guid.Parse(currentUser.Id),
|
||||
ChatRoomId = roomId,
|
||||
Role = ChatMemberRole.Member,
|
||||
JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow)
|
||||
@@ -762,10 +889,14 @@ public class ChatRoomController(
|
||||
await db.SaveChangesAsync();
|
||||
_ = crs.PurgeRoomMembersCache(roomId);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomJoin,
|
||||
new Dictionary<string, object> { { "chatroom_id", roomId } }, Request
|
||||
);
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = ActionLogType.ChatroomJoin,
|
||||
Meta = { { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(roomId.ToString()) } },
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent,
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
||||
});
|
||||
|
||||
return Ok(chatRoom);
|
||||
}
|
||||
@@ -774,10 +905,10 @@ public class ChatRoomController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult> LeaveChat(Guid roomId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var member = await db.ChatMembers
|
||||
.Where(m => m.AccountId == currentUser.Id)
|
||||
.Where(m => m.AccountId == Guid.Parse(currentUser.Id))
|
||||
.Where(m => m.ChatRoomId == roomId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (member is null) return NotFound();
|
||||
@@ -788,7 +919,7 @@ public class ChatRoomController(
|
||||
var otherOwners = await db.ChatMembers
|
||||
.Where(m => m.ChatRoomId == roomId)
|
||||
.Where(m => m.Role == ChatMemberRole.Owner)
|
||||
.Where(m => m.AccountId != currentUser.Id)
|
||||
.Where(m => m.AccountId != Guid.Parse(currentUser.Id))
|
||||
.AnyAsync();
|
||||
|
||||
if (!otherOwners)
|
||||
@@ -799,15 +930,19 @@ public class ChatRoomController(
|
||||
await db.SaveChangesAsync();
|
||||
await crs.PurgeRoomMembersCache(roomId);
|
||||
|
||||
als.CreateActionLogFromRequest(
|
||||
ActionLogType.ChatroomLeave,
|
||||
new Dictionary<string, object> { { "chatroom_id", roomId } }, Request
|
||||
);
|
||||
_ = als.CreateActionLogAsync(new CreateActionLogRequest
|
||||
{
|
||||
Action = ActionLogType.ChatroomLeave,
|
||||
Meta = { { "chatroom_id", Google.Protobuf.WellKnownTypes.Value.ForString(roomId.ToString()) } },
|
||||
AccountId = currentUser.Id,
|
||||
UserAgent = Request.Headers.UserAgent,
|
||||
IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString()
|
||||
});
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private async Task _SendInviteNotify(ChatMember member, Account.Account sender)
|
||||
private async Task _SendInviteNotify(ChatMember member, Account sender)
|
||||
{
|
||||
string title = localizer["ChatInviteTitle"];
|
||||
|
||||
@@ -815,7 +950,19 @@ public class ChatRoomController(
|
||||
? localizer["ChatInviteDirectBody", sender.Nick]
|
||||
: localizer["ChatInviteBody", member.ChatRoom.Name ?? "Unnamed"];
|
||||
|
||||
AccountService.SetCultureInfo(member.Account);
|
||||
await nty.SendNotification(member.Account, "invites.chats", title, null, body, actionUri: "/chat");
|
||||
CultureService.SetCultureInfo(member.Account.Language);
|
||||
await pusher.SendPushNotificationToUserAsync(
|
||||
new SendPushNotificationToUserRequest
|
||||
{
|
||||
UserId = member.Account.Id.ToString(),
|
||||
Notification = new PushNotification
|
||||
{
|
||||
Topic = "invites.chats",
|
||||
Title = title,
|
||||
Body = body,
|
||||
IsSavable = true
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,12 +1,18 @@
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using Account = DysonNetwork.Pass.Account.Account;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat;
|
||||
|
||||
public class ChatRoomService(AppDatabase db, ICacheService cache)
|
||||
public class ChatRoomService(
|
||||
AppDatabase db,
|
||||
ICacheService cache,
|
||||
AccountClientHelper accountsHelper
|
||||
)
|
||||
{
|
||||
public const string ChatRoomGroupPrefix = "chatroom:";
|
||||
private const string ChatRoomGroupPrefix = "chatroom:";
|
||||
private const string RoomMembersCacheKeyPrefix = "chatroom:members:";
|
||||
private const string ChatMemberCacheKey = "chatroom:{0}:member:{1}";
|
||||
|
||||
@@ -18,12 +24,11 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
|
||||
return cachedMembers;
|
||||
|
||||
var members = await db.ChatMembers
|
||||
.Include(m => m.Account)
|
||||
.ThenInclude(m => m.Profile)
|
||||
.Where(m => m.ChatRoomId == roomId)
|
||||
.Where(m => m.JoinedAt != null)
|
||||
.Where(m => m.LeaveAt == null)
|
||||
.ToListAsync();
|
||||
members = await LoadMemberAccounts(members);
|
||||
|
||||
var chatRoomGroup = ChatRoomGroupPrefix + roomId;
|
||||
await cache.SetWithGroupsAsync(cacheKey, members,
|
||||
@@ -40,14 +45,13 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
|
||||
if (member is not null) return member;
|
||||
|
||||
member = await db.ChatMembers
|
||||
.Include(m => m.Account)
|
||||
.ThenInclude(m => m.Profile)
|
||||
.Include(m => m.ChatRoom)
|
||||
.ThenInclude(m => m.Realm)
|
||||
.Where(m => m.AccountId == accountId && m.ChatRoomId == chatRoomId)
|
||||
.Include(m => m.ChatRoom)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (member == null) return member;
|
||||
|
||||
member = await LoadMemberAccount(member);
|
||||
var chatRoomGroup = ChatRoomGroupPrefix + chatRoomId;
|
||||
await cache.SetWithGroupsAsync(cacheKey, member,
|
||||
[chatRoomGroup],
|
||||
@@ -87,16 +91,22 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
|
||||
.ToList();
|
||||
if (directRoomsId.Count == 0) return rooms;
|
||||
|
||||
var directMembers = directRoomsId.Count != 0
|
||||
List<ChatMember> members = directRoomsId.Count != 0
|
||||
? await db.ChatMembers
|
||||
.Where(m => directRoomsId.Contains(m.ChatRoomId))
|
||||
.Where(m => m.AccountId != userId)
|
||||
.Where(m => m.LeaveAt == null)
|
||||
.Include(m => m.Account)
|
||||
.Include(m => m.Account.Profile)
|
||||
.GroupBy(m => m.ChatRoomId)
|
||||
.ToDictionaryAsync(g => g.Key, g => g.ToList())
|
||||
: new Dictionary<Guid, List<ChatMember>>();
|
||||
.ToListAsync()
|
||||
: [];
|
||||
members = await LoadMemberAccounts(members);
|
||||
|
||||
Dictionary<Guid, List<ChatMember>> directMembers = new();
|
||||
foreach (var member in members)
|
||||
{
|
||||
if (!directMembers.ContainsKey(member.ChatRoomId))
|
||||
directMembers[member.ChatRoomId] = [];
|
||||
directMembers[member.ChatRoomId].Add(member);
|
||||
}
|
||||
|
||||
return rooms.Select(r =>
|
||||
{
|
||||
@@ -112,12 +122,13 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
|
||||
var members = await db.ChatMembers
|
||||
.Where(m => m.ChatRoomId == room.Id && m.AccountId != userId)
|
||||
.Where(m => m.LeaveAt == null)
|
||||
.Include(m => m.Account)
|
||||
.Include(m => m.Account.Profile)
|
||||
.ToListAsync();
|
||||
|
||||
if (members.Count > 0)
|
||||
room.DirectMembers = members.Select(ChatMemberTransmissionObject.FromEntity).ToList();
|
||||
if (members.Count <= 0) return room;
|
||||
|
||||
members = await LoadMemberAccounts(members);
|
||||
room.DirectMembers = members.Select(ChatMemberTransmissionObject.FromEntity).ToList();
|
||||
|
||||
return room;
|
||||
}
|
||||
|
||||
@@ -131,4 +142,24 @@ public class ChatRoomService(AppDatabase db, ICacheService cache)
|
||||
.FirstOrDefaultAsync(m => m.ChatRoomId == roomId && m.AccountId == accountId);
|
||||
return member?.Role >= maxRequiredRole;
|
||||
}
|
||||
|
||||
public async Task<ChatMember> LoadMemberAccount(ChatMember member)
|
||||
{
|
||||
var account = await accountsHelper.GetAccount(member.AccountId);
|
||||
member.Account = Account.FromProtoValue(account);
|
||||
return member;
|
||||
}
|
||||
|
||||
public async Task<List<ChatMember>> LoadMemberAccounts(ICollection<ChatMember> members)
|
||||
{
|
||||
var accountIds = members.Select(m => m.AccountId).ToList();
|
||||
var accounts = (await accountsHelper.GetAccountBatch(accountIds)).ToDictionary(a => Guid.Parse(a.Id), a => a);
|
||||
|
||||
return members.Select(m =>
|
||||
{
|
||||
if (accounts.TryGetValue(m.AccountId, out var account))
|
||||
m.Account = Account.FromProtoValue(account);
|
||||
return m;
|
||||
}).ToList();
|
||||
}
|
||||
}
|
@@ -1,8 +1,7 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using DysonNetwork.Sphere.Account;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Sphere.Chat.Realtime;
|
||||
using DysonNetwork.Sphere.Connection;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
@@ -10,7 +9,9 @@ namespace DysonNetwork.Sphere.Chat;
|
||||
|
||||
public partial class ChatService(
|
||||
AppDatabase db,
|
||||
FileReferenceService fileRefService,
|
||||
ChatRoomService crs,
|
||||
FileService.FileServiceClient filesClient,
|
||||
FileReferenceService.FileReferenceServiceClient fileRefs,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IRealtimeService realtime,
|
||||
ILogger<ChatService> logger
|
||||
@@ -33,7 +34,7 @@ public partial class ChatService(
|
||||
// Create a new scope for database operations
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
||||
var webReader = scope.ServiceProvider.GetRequiredService<Connection.WebReader.WebReaderService>();
|
||||
var webReader = scope.ServiceProvider.GetRequiredService<WebReader.WebReaderService>();
|
||||
var newChat = scope.ServiceProvider.GetRequiredService<ChatService>();
|
||||
|
||||
// Preview the links in the message
|
||||
@@ -86,7 +87,7 @@ public partial class ChatService(
|
||||
/// <param name="webReader">The web reader service</param>
|
||||
/// <returns>The message with link previews added to its meta data</returns>
|
||||
public async Task<Message> PreviewMessageLinkAsync(Message message,
|
||||
Connection.WebReader.WebReaderService? webReader = null)
|
||||
WebReader.WebReaderService? webReader = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(message.Content))
|
||||
return message;
|
||||
@@ -109,7 +110,7 @@ public partial class ChatService(
|
||||
|
||||
var embeds = (List<Dictionary<string, object>>)message.Meta["embeds"];
|
||||
webReader ??= scopeFactory.CreateScope().ServiceProvider
|
||||
.GetRequiredService<Connection.WebReader.WebReaderService>();
|
||||
.GetRequiredService<WebReader.WebReaderService>();
|
||||
|
||||
// Process up to 3 links to avoid excessive processing
|
||||
var processedLinks = 0;
|
||||
@@ -157,16 +158,13 @@ public partial class ChatService(
|
||||
var files = message.Attachments.Distinct().ToList();
|
||||
if (files.Count != 0)
|
||||
{
|
||||
var messageResourceId = $"message:{message.Id}";
|
||||
foreach (var file in files)
|
||||
var request = new CreateReferenceBatchRequest
|
||||
{
|
||||
await fileRefService.CreateReferenceAsync(
|
||||
file.Id,
|
||||
ChatFileUsageIdentifier,
|
||||
messageResourceId,
|
||||
duration: Duration.FromDays(30)
|
||||
);
|
||||
}
|
||||
Usage = ChatFileUsageIdentifier,
|
||||
ResourceId = message.ResourceIdentifier,
|
||||
};
|
||||
request.FilesId.AddRange(message.Attachments.Select(a => a.Id));
|
||||
await fileRefs.CreateReferenceBatchAsync(request);
|
||||
}
|
||||
|
||||
// Then start the delivery process
|
||||
@@ -203,8 +201,7 @@ public partial class ChatService(
|
||||
message.ChatRoom = room;
|
||||
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var scopedWs = scope.ServiceProvider.GetRequiredService<WebSocketService>();
|
||||
var scopedNty = scope.ServiceProvider.GetRequiredService<NotificationService>();
|
||||
var scopedNty = scope.ServiceProvider.GetRequiredService<PusherService.PusherServiceClient>();
|
||||
var scopedCrs = scope.ServiceProvider.GetRequiredService<ChatRoomService>();
|
||||
|
||||
var roomSubject = room is { Type: ChatRoomType.DirectMessage, Name: null } ? "DM" :
|
||||
@@ -230,43 +227,48 @@ public partial class ChatService(
|
||||
if (!string.IsNullOrEmpty(room.Name))
|
||||
metaDict["room_name"] = room.Name;
|
||||
|
||||
var notification = new Notification
|
||||
var notification = new PushNotification
|
||||
{
|
||||
Topic = "messages.new",
|
||||
Title = $"{sender.Nick ?? sender.Account.Nick} ({roomSubject})",
|
||||
Content = !string.IsNullOrEmpty(message.Content)
|
||||
Body = !string.IsNullOrEmpty(message.Content)
|
||||
? message.Content[..Math.Min(message.Content.Length, 100)]
|
||||
: "<no content>",
|
||||
Meta = metaDict,
|
||||
Priority = 10,
|
||||
IsSavable = false
|
||||
};
|
||||
notification.Meta.Add(GrpcTypeHelper.ConvertToValueMap(metaDict));
|
||||
|
||||
List<Account.Account> accountsToNotify = [];
|
||||
List<Account> accountsToNotify = [];
|
||||
foreach (var member in members)
|
||||
{
|
||||
scopedWs.SendPacketToAccount(member.AccountId, new WebSocketPacket
|
||||
await scopedNty.PushWebSocketPacketToUsersAsync(new PushWebSocketPacketToUsersRequest
|
||||
{
|
||||
Type = type,
|
||||
Data = message
|
||||
Packet = new WebSocketPacket
|
||||
{
|
||||
Type = type,
|
||||
Data = GrpcTypeHelper.ConvertObjectToValue(metaDict),
|
||||
},
|
||||
});
|
||||
|
||||
if (member.Account.Id == sender.AccountId) continue;
|
||||
if (member.AccountId == sender.AccountId) continue;
|
||||
if (member.Notify == ChatMemberNotify.None) continue;
|
||||
// if (scopedWs.IsUserSubscribedToChatRoom(member.AccountId, room.Id.ToString())) continue;
|
||||
if (message.MembersMentioned is null || !message.MembersMentioned.Contains(member.Account.Id))
|
||||
if (message.MembersMentioned is null || !message.MembersMentioned.Contains(member.AccountId))
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
if (member.BreakUntil is not null && member.BreakUntil > now) continue;
|
||||
}
|
||||
else if (member.Notify == ChatMemberNotify.Mentions) continue;
|
||||
|
||||
accountsToNotify.Add(member.Account);
|
||||
accountsToNotify.Add(member.Account.ToProtoValue());
|
||||
}
|
||||
|
||||
logger.LogInformation($"Trying to deliver message to {accountsToNotify.Count} accounts...");
|
||||
// Only send notifications if there are accounts to notify
|
||||
var ntyRequest = new SendPushNotificationToUsersRequest { Notification = notification };
|
||||
ntyRequest.UserIds.AddRange(accountsToNotify.Select(a => a.Id.ToString()));
|
||||
if (accountsToNotify.Count > 0)
|
||||
await scopedNty.SendNotificationBatch(notification, accountsToNotify, save: false);
|
||||
await scopedNty.SendPushNotificationToUsersAsync(ntyRequest);
|
||||
|
||||
logger.LogInformation($"Delivered message to {accountsToNotify.Count} accounts.");
|
||||
}
|
||||
@@ -332,8 +334,6 @@ public partial class ChatService(
|
||||
var messages = await db.ChatMessages
|
||||
.IgnoreQueryFilters()
|
||||
.Include(m => m.Sender)
|
||||
.Include(m => m.Sender.Account)
|
||||
.Include(m => m.Sender.Account.Profile)
|
||||
.Where(m => userRooms.Contains(m.ChatRoomId))
|
||||
.GroupBy(m => m.ChatRoomId)
|
||||
.Select(g => g.OrderByDescending(m => m.CreatedAt).FirstOrDefault())
|
||||
@@ -341,6 +341,15 @@ public partial class ChatService(
|
||||
m => m!.ChatRoomId,
|
||||
m => m
|
||||
);
|
||||
|
||||
var messageSenders = messages
|
||||
.Select(m => m.Value!.Sender)
|
||||
.DistinctBy(x => x.Id)
|
||||
.ToList();
|
||||
messageSenders = await crs.LoadMemberAccounts(messageSenders);
|
||||
|
||||
foreach (var message in messages)
|
||||
message.Value!.Sender = messageSenders.First(x => x.Id == message.Value.SenderId);
|
||||
|
||||
return messages;
|
||||
}
|
||||
@@ -449,8 +458,6 @@ public partial class ChatService(
|
||||
var changes = await db.ChatMessages
|
||||
.IgnoreQueryFilters()
|
||||
.Include(e => e.Sender)
|
||||
.Include(e => e.Sender.Account)
|
||||
.Include(e => e.Sender.Account.Profile)
|
||||
.Where(m => m.ChatRoomId == roomId)
|
||||
.Where(m => m.UpdatedAt > timestamp || m.DeletedAt > timestamp)
|
||||
.Select(m => new MessageChange
|
||||
@@ -462,6 +469,20 @@ public partial class ChatService(
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var changesMembers = changes
|
||||
.Select(c => c.Message!.Sender)
|
||||
.DistinctBy(x => x.Id)
|
||||
.ToList();
|
||||
changesMembers = await crs.LoadMemberAccounts(changesMembers);
|
||||
|
||||
foreach (var change in changes)
|
||||
{
|
||||
if (change.Message == null) continue;
|
||||
var sender = changesMembers.FirstOrDefault(x => x.Id == change.Message.SenderId);
|
||||
if (sender is not null)
|
||||
change.Message.Sender = sender;
|
||||
}
|
||||
|
||||
return new SyncResponse
|
||||
{
|
||||
Changes = changes,
|
||||
@@ -492,28 +513,25 @@ public partial class ChatService(
|
||||
|
||||
if (attachmentsId is not null)
|
||||
{
|
||||
var messageResourceId = $"message:{message.Id}";
|
||||
|
||||
// Delete existing references for this message
|
||||
await fileRefService.DeleteResourceReferencesAsync(messageResourceId);
|
||||
await fileRefs.DeleteResourceReferencesAsync(
|
||||
new DeleteResourceReferencesRequest { ResourceId = message.ResourceIdentifier }
|
||||
);
|
||||
|
||||
// Create new references for each attachment
|
||||
foreach (var fileId in attachmentsId)
|
||||
var createRequest = new CreateReferenceBatchRequest
|
||||
{
|
||||
await fileRefService.CreateReferenceAsync(
|
||||
fileId,
|
||||
ChatFileUsageIdentifier,
|
||||
messageResourceId,
|
||||
duration: Duration.FromDays(30)
|
||||
);
|
||||
}
|
||||
Usage = ChatFileUsageIdentifier,
|
||||
ResourceId = message.ResourceIdentifier,
|
||||
};
|
||||
createRequest.FilesId.AddRange(attachmentsId);
|
||||
await fileRefs.CreateReferenceBatchAsync(createRequest);
|
||||
|
||||
// Update message attachments by getting files from database
|
||||
var files = await db.Files
|
||||
.Where(f => attachmentsId.Contains(f.Id))
|
||||
.ToListAsync();
|
||||
|
||||
message.Attachments = files.Select(x => x.ToReferenceObject()).ToList();
|
||||
// Update message attachments by getting files from da
|
||||
var queryRequest = new GetFileBatchRequest();
|
||||
queryRequest.Ids.AddRange(attachmentsId);
|
||||
var queryResult = await filesClient.GetFileBatchAsync(queryRequest);
|
||||
message.Attachments = queryResult.Files.Select(CloudFileReferenceObject.FromProtoValue).ToList();
|
||||
}
|
||||
|
||||
message.EditedAt = SystemClock.Instance.GetCurrentInstant();
|
||||
@@ -542,7 +560,9 @@ public partial class ChatService(
|
||||
{
|
||||
// Remove all file references for this message
|
||||
var messageResourceId = $"message:{message.Id}";
|
||||
await fileRefService.DeleteResourceReferencesAsync(messageResourceId);
|
||||
await fileRefs.DeleteResourceReferencesAsync(
|
||||
new DeleteResourceReferencesRequest { ResourceId = messageResourceId }
|
||||
);
|
||||
|
||||
db.ChatMessages.Remove(message);
|
||||
await db.SaveChangesAsync();
|
||||
@@ -575,4 +595,4 @@ public class SyncResponse
|
||||
{
|
||||
public List<MessageChange> Changes { get; set; } = [];
|
||||
public Instant CurrentTimestamp { get; set; }
|
||||
}
|
||||
}
|
@@ -1,8 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat;
|
||||
@@ -19,8 +18,6 @@ public class Message : ModelBase, IIdentifiedResource
|
||||
|
||||
[Column(TypeName = "jsonb")] public List<CloudFileReferenceObject> Attachments { get; set; } = [];
|
||||
|
||||
// Outdated fields, keep for backward compability
|
||||
public ICollection<CloudFile> OutdatedAttachments { get; set; } = new List<CloudFile>();
|
||||
public ICollection<MessageReaction> Reactions { get; set; } = new List<MessageReaction>();
|
||||
|
||||
public Guid? RepliedMessageId { get; set; }
|
||||
@@ -33,7 +30,7 @@ public class Message : ModelBase, IIdentifiedResource
|
||||
public Guid ChatRoomId { get; set; }
|
||||
[JsonIgnore] public ChatRoom ChatRoom { get; set; } = null!;
|
||||
|
||||
public string ResourceIdentifier => $"message/{Id}";
|
||||
public string ResourceIdentifier => $"message:{Id}";
|
||||
}
|
||||
|
||||
public enum MessageReactionAttitude
|
||||
|
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat.Realtime;
|
||||
|
||||
@@ -36,7 +37,7 @@ public interface IRealtimeService
|
||||
/// <param name="sessionId">The session identifier</param>
|
||||
/// <param name="isAdmin">The user is the admin of session</param>
|
||||
/// <returns>User-specific token for the session</returns>
|
||||
string GetUserToken(Account.Account account, string sessionId, bool isAdmin = false);
|
||||
string GetUserToken(Account account, string sessionId, bool isAdmin = false);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a token for user to join the session asynchronously
|
||||
|
@@ -4,6 +4,9 @@ using Livekit.Server.Sdk.Dotnet;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
using System.Text.Json;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat.Realtime;
|
||||
|
||||
@@ -14,7 +17,6 @@ public class LiveKitRealtimeService : IRealtimeService
|
||||
{
|
||||
private readonly AppDatabase _db;
|
||||
private readonly ICacheService _cache;
|
||||
private readonly RealtimeStatusService _callStatus;
|
||||
|
||||
private readonly ILogger<LiveKitRealtimeService> _logger;
|
||||
private readonly RoomServiceClient _roomService;
|
||||
@@ -25,18 +27,17 @@ public class LiveKitRealtimeService : IRealtimeService
|
||||
IConfiguration configuration,
|
||||
ILogger<LiveKitRealtimeService> logger,
|
||||
AppDatabase db,
|
||||
ICacheService cache,
|
||||
RealtimeStatusService callStatus
|
||||
ICacheService cache
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
// Get LiveKit configuration from appsettings
|
||||
var host = configuration["Realtime:LiveKit:Endpoint"] ??
|
||||
var host = configuration["RealtimeChat:Endpoint"] ??
|
||||
throw new ArgumentNullException("Endpoint configuration is required");
|
||||
var apiKey = configuration["Realtime:LiveKit:ApiKey"] ??
|
||||
var apiKey = configuration["RealtimeChat:ApiKey"] ??
|
||||
throw new ArgumentNullException("ApiKey configuration is required");
|
||||
var apiSecret = configuration["Realtime:LiveKit:ApiSecret"] ??
|
||||
var apiSecret = configuration["RealtimeChat:ApiSecret"] ??
|
||||
throw new ArgumentNullException("ApiSecret configuration is required");
|
||||
|
||||
_roomService = new RoomServiceClient(host, apiKey, apiSecret);
|
||||
@@ -45,7 +46,6 @@ public class LiveKitRealtimeService : IRealtimeService
|
||||
|
||||
_db = db;
|
||||
_cache = cache;
|
||||
_callStatus = callStatus;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -112,11 +112,6 @@ public class LiveKitRealtimeService : IRealtimeService
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetUserToken(Account.Account account, string sessionId, bool isAdmin = false)
|
||||
{
|
||||
return GetUserTokenAsync(account, sessionId, isAdmin).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public Task<string> GetUserTokenAsync(Account.Account account, string sessionId, bool isAdmin = false)
|
||||
{
|
||||
var token = _accessToken.WithIdentity(account.Name)
|
||||
.WithName(account.Nick)
|
||||
@@ -133,7 +128,7 @@ public class LiveKitRealtimeService : IRealtimeService
|
||||
.WithMetadata(JsonSerializer.Serialize(new Dictionary<string, string>
|
||||
{ { "account_id", account.Id.ToString() } }))
|
||||
.WithTtl(TimeSpan.FromHours(1));
|
||||
return Task.FromResult(token.ToJwt());
|
||||
return token.ToJwt();
|
||||
}
|
||||
|
||||
public async Task ReceiveWebhook(string body, string authHeader)
|
||||
@@ -164,8 +159,7 @@ public class LiveKitRealtimeService : IRealtimeService
|
||||
evt.Room.Name, evt.Participant.Identity);
|
||||
|
||||
// Broadcast participant list update to all participants
|
||||
var info = await GetRoomParticipantsAsync(evt.Room.Name);
|
||||
await _callStatus.BroadcastParticipantUpdate(evt.Room.Name, info);
|
||||
// await _BroadcastParticipantUpdate(evt.Room.Name);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -180,15 +174,14 @@ public class LiveKitRealtimeService : IRealtimeService
|
||||
evt.Room.Name, evt.Participant.Identity);
|
||||
|
||||
// Broadcast participant list update to all participants
|
||||
var info = await GetRoomParticipantsAsync(evt.Room.Name);
|
||||
await _callStatus.BroadcastParticipantUpdate(evt.Room.Name, info);
|
||||
// await _BroadcastParticipantUpdate(evt.Room.Name);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static string _GetParticipantsKey(string roomName)
|
||||
|
||||
private static string _GetParticipantsKey(string roomName)
|
||||
=> $"RoomParticipants_{roomName}";
|
||||
|
||||
private async Task _AddParticipantToCache(string roomName, ParticipantInfo participant)
|
||||
@@ -208,7 +201,7 @@ public class LiveKitRealtimeService : IRealtimeService
|
||||
}
|
||||
|
||||
// Get the current participants list
|
||||
var participants = await _cache.GetAsync<List<ParticipantInfoItem>>(participantsKey) ??
|
||||
var participants = await _cache.GetAsync<List<ParticipantCacheItem>>(participantsKey) ??
|
||||
[];
|
||||
|
||||
// Check if the participant already exists
|
||||
@@ -248,7 +241,7 @@ public class LiveKitRealtimeService : IRealtimeService
|
||||
}
|
||||
|
||||
// Get current participants list
|
||||
var participants = await _cache.GetAsync<List<ParticipantInfoItem>>(participantsKey);
|
||||
var participants = await _cache.GetAsync<List<ParticipantCacheItem>>(participantsKey);
|
||||
if (participants == null || !participants.Any())
|
||||
return;
|
||||
|
||||
@@ -260,24 +253,36 @@ public class LiveKitRealtimeService : IRealtimeService
|
||||
}
|
||||
|
||||
// Helper method to get participants in a room
|
||||
public async Task<List<ParticipantInfoItem>> GetRoomParticipantsAsync(string roomName)
|
||||
public async Task<List<ParticipantCacheItem>> GetRoomParticipantsAsync(string roomName)
|
||||
{
|
||||
var participantsKey = _GetParticipantsKey(roomName);
|
||||
return await _cache.GetAsync<List<ParticipantInfoItem>>(participantsKey) ?? [];
|
||||
return await _cache.GetAsync<List<ParticipantCacheItem>>(participantsKey) ?? [];
|
||||
}
|
||||
|
||||
private ParticipantInfoItem CreateParticipantCacheItem(ParticipantInfo participant)
|
||||
// Class to represent a participant in the cache
|
||||
public class ParticipantCacheItem
|
||||
{
|
||||
public string Identity { get; set; } = null!;
|
||||
public string Name { get; set; } = null!;
|
||||
public Guid? AccountId { get; set; }
|
||||
public ParticipantInfo.Types.State State { get; set; }
|
||||
public Dictionary<string, string> Metadata { get; set; } = new();
|
||||
public DateTime JoinedAt { get; set; }
|
||||
}
|
||||
|
||||
private ParticipantCacheItem CreateParticipantCacheItem(ParticipantInfo participant)
|
||||
{
|
||||
// Try to parse account ID from metadata
|
||||
Guid? accountId = null;
|
||||
var metadata = new Dictionary<string, string>();
|
||||
|
||||
if (string.IsNullOrEmpty(participant.Metadata))
|
||||
return new ParticipantInfoItem
|
||||
return new ParticipantCacheItem
|
||||
{
|
||||
Identity = participant.Identity,
|
||||
Name = participant.Name,
|
||||
AccountId = accountId,
|
||||
State = participant.State,
|
||||
Metadata = metadata,
|
||||
JoinedAt = DateTime.UtcNow
|
||||
};
|
||||
@@ -295,13 +300,14 @@ public class LiveKitRealtimeService : IRealtimeService
|
||||
_logger.LogError(ex, "Failed to parse participant metadata");
|
||||
}
|
||||
|
||||
return new ParticipantInfoItem
|
||||
return new ParticipantCacheItem
|
||||
{
|
||||
Identity = participant.Identity,
|
||||
Name = participant.Name,
|
||||
AccountId = accountId,
|
||||
State = participant.State,
|
||||
Metadata = metadata,
|
||||
JoinedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,6 @@
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Sphere.Chat.Realtime;
|
||||
using Livekit.Server.Sdk.Dotnet;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -6,6 +8,11 @@ using Swashbuckle.AspNetCore.Annotations;
|
||||
|
||||
namespace DysonNetwork.Sphere.Chat;
|
||||
|
||||
public class RealtimeChatConfiguration
|
||||
{
|
||||
public string Endpoint { get; set; } = null!;
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/chat/realtime")]
|
||||
public class RealtimeCallController(
|
||||
@@ -15,6 +22,9 @@ public class RealtimeCallController(
|
||||
IRealtimeService realtime
|
||||
) : ControllerBase
|
||||
{
|
||||
private readonly RealtimeChatConfiguration _config =
|
||||
configuration.GetSection("RealtimeChat").Get<RealtimeChatConfiguration>()!;
|
||||
|
||||
/// <summary>
|
||||
/// This endpoint is especially designed for livekit webhooks,
|
||||
/// for update the call participates and more.
|
||||
@@ -27,9 +37,9 @@ public class RealtimeCallController(
|
||||
using var reader = new StreamReader(Request.Body);
|
||||
var postData = await reader.ReadToEndAsync();
|
||||
var authHeader = Request.Headers.Authorization.ToString();
|
||||
|
||||
|
||||
await realtime.ReceiveWebhook(postData, authHeader);
|
||||
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@@ -37,10 +47,11 @@ public class RealtimeCallController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<RealtimeCall>> GetOngoingCall(Guid roomId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
var member = await db.ChatMembers
|
||||
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
|
||||
.Where(m => m.AccountId == accountId && m.ChatRoomId == roomId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (member == null || member.Role < ChatMemberRole.Member)
|
||||
@@ -51,8 +62,6 @@ public class RealtimeCallController(
|
||||
.Where(c => c.EndedAt == null)
|
||||
.Include(c => c.Room)
|
||||
.Include(c => c.Sender)
|
||||
.ThenInclude(c => c.Account)
|
||||
.ThenInclude(c => c.Profile)
|
||||
.FirstOrDefaultAsync();
|
||||
if (ongoingCall is null) return NotFound();
|
||||
return Ok(ongoingCall);
|
||||
@@ -62,11 +71,12 @@ public class RealtimeCallController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<JoinCallResponse>> JoinCall(Guid roomId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
// Check if the user is a member of the chat room
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
var member = await db.ChatMembers
|
||||
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
|
||||
.Where(m => m.AccountId == accountId && m.ChatRoomId == roomId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (member == null || member.Role < ChatMemberRole.Member)
|
||||
@@ -82,17 +92,39 @@ public class RealtimeCallController(
|
||||
return BadRequest("Call session is not properly configured.");
|
||||
|
||||
var isAdmin = member.Role >= ChatMemberRole.Moderator;
|
||||
var userToken = await realtime.GetUserTokenAsync(currentUser, ongoingCall.SessionId, isAdmin);
|
||||
var userToken = realtime.GetUserToken(currentUser, ongoingCall.SessionId, isAdmin);
|
||||
|
||||
// Get LiveKit endpoint from configuration
|
||||
var endpoint = configuration[$"Realtime:{realtime.ProviderName}:Endpoint"] ?? realtime.ProviderName switch
|
||||
var endpoint = _config.Endpoint ??
|
||||
throw new InvalidOperationException("LiveKit endpoint configuration is missing");
|
||||
|
||||
// Inject the ChatRoomService
|
||||
var chatRoomService = HttpContext.RequestServices.GetRequiredService<ChatRoomService>();
|
||||
|
||||
// Get current participants from the LiveKit service
|
||||
var participants = new List<CallParticipant>();
|
||||
if (realtime is LiveKitRealtimeService livekitService)
|
||||
{
|
||||
// Unusable for sure, just for placeholder
|
||||
"LiveKit" => "https://livekit.cloud",
|
||||
"Cloudflare" => "https://rtk.realtime.cloudflare.com/v2",
|
||||
// Unusable for sure, just for placeholder
|
||||
_ => "https://example.com"
|
||||
};
|
||||
var roomParticipants = await livekitService.GetRoomParticipantsAsync(ongoingCall.SessionId);
|
||||
participants = [];
|
||||
|
||||
foreach (var p in roomParticipants)
|
||||
{
|
||||
var participant = new CallParticipant
|
||||
{
|
||||
Identity = p.Identity,
|
||||
Name = p.Name,
|
||||
AccountId = p.AccountId,
|
||||
JoinedAt = p.JoinedAt
|
||||
};
|
||||
|
||||
// Fetch the ChatMember profile if we have an account ID
|
||||
if (p.AccountId.HasValue)
|
||||
participant.Profile = await chatRoomService.GetRoomMember(p.AccountId.Value, roomId);
|
||||
|
||||
participants.Add(participant);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the response model
|
||||
var response = new JoinCallResponse
|
||||
@@ -102,7 +134,8 @@ public class RealtimeCallController(
|
||||
Token = userToken,
|
||||
CallId = ongoingCall.Id,
|
||||
RoomName = ongoingCall.SessionId,
|
||||
IsAdmin = isAdmin
|
||||
IsAdmin = isAdmin,
|
||||
Participants = participants
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
@@ -112,10 +145,11 @@ public class RealtimeCallController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<RealtimeCall>> StartCall(Guid roomId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
var member = await db.ChatMembers
|
||||
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
|
||||
.Where(m => m.AccountId == accountId && m.ChatRoomId == roomId)
|
||||
.Include(m => m.ChatRoom)
|
||||
.FirstOrDefaultAsync();
|
||||
if (member == null || member.Role < ChatMemberRole.Member)
|
||||
@@ -131,10 +165,11 @@ public class RealtimeCallController(
|
||||
[Authorize]
|
||||
public async Task<ActionResult<RealtimeCall>> EndCall(Guid roomId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
var member = await db.ChatMembers
|
||||
.Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId)
|
||||
.Where(m => m.AccountId == accountId && m.ChatRoomId == roomId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (member == null || member.Role < ChatMemberRole.Member)
|
||||
return StatusCode(403, "You need to be a normal member to end a call.");
|
||||
@@ -160,7 +195,7 @@ public class JoinCallResponse
|
||||
public string Provider { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The provider server endpoint
|
||||
/// The LiveKit server endpoint
|
||||
/// </summary>
|
||||
public string Endpoint { get; set; } = null!;
|
||||
|
||||
@@ -183,6 +218,11 @@ public class JoinCallResponse
|
||||
/// Whether the user is the admin of the call
|
||||
/// </summary>
|
||||
public bool IsAdmin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current participants in the call
|
||||
/// </summary>
|
||||
public List<CallParticipant> Participants { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -194,22 +234,22 @@ public class CallParticipant
|
||||
/// The participant's identity (username)
|
||||
/// </summary>
|
||||
public string Identity { get; set; } = null!;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The participant's display name
|
||||
/// </summary>
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The participant's account ID if available
|
||||
/// </summary>
|
||||
public Guid? AccountId { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The participant's profile in the chat
|
||||
/// </summary>
|
||||
public ChatMember? Profile { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// When the participant joined the call
|
||||
/// </summary>
|
||||
|
Reference in New Issue
Block a user