Compare commits
	
		
			2 Commits
		
	
	
		
			f6acb3f2f0
			...
			fa5c59a9c8
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fa5c59a9c8 | |||
| 196547e50f | 
| @@ -35,7 +35,9 @@ public class Profile : ModelBase | ||||
|     [MaxLength(256)] public string? LastName { get; set; } | ||||
|     [MaxLength(4096)] public string? Bio { get; set; } | ||||
|  | ||||
|     public string? PictureId { get; set; } | ||||
|     public Storage.CloudFile? Picture { get; set; } | ||||
|     public string? BackgroundId { get; set; } | ||||
|     public Storage.CloudFile? Background { get; set; } | ||||
|  | ||||
|     [JsonIgnore] public Account Account { get; set; } = null!; | ||||
|   | ||||
| @@ -25,8 +25,6 @@ public class AccountController( | ||||
|     { | ||||
|         var account = await db.Accounts | ||||
|             .Include(e => e.Profile) | ||||
|             .Include(e => e.Profile.Picture) | ||||
|             .Include(e => e.Profile.Background) | ||||
|             .Where(a => a.Name == name) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         return account is null ? new NotFoundResult() : account; | ||||
| @@ -114,8 +112,6 @@ public class AccountController( | ||||
|  | ||||
|         var account = await db.Accounts | ||||
|             .Include(e => e.Profile) | ||||
|             .Include(e => e.Profile.Picture) | ||||
|             .Include(e => e.Profile.Background) | ||||
|             .Where(e => e.Id == userId) | ||||
|             .FirstOrDefaultAsync(); | ||||
|  | ||||
| @@ -202,4 +198,17 @@ public class AccountController( | ||||
|  | ||||
|         return profile; | ||||
|     } | ||||
|  | ||||
|     [HttpGet("search")] | ||||
|     public async Task<List<Account>> Search([FromQuery] string query, [FromQuery] int take = 20) | ||||
|     { | ||||
|         if (string.IsNullOrWhiteSpace(query)) | ||||
|             return []; | ||||
|         return await db.Accounts | ||||
|             .Include(e => e.Profile) | ||||
|             .Where(a => EF.Functions.ILike(a.Name, $"%{query}%") || | ||||
|                         EF.Functions.ILike(a.Nick, $"%{query}%")) | ||||
|             .Take(take) | ||||
|             .ToListAsync(); | ||||
|     } | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| using CorePush.Apple; | ||||
| using CorePush.Firebase; | ||||
| using DysonNetwork.Sphere.Connection; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
|  | ||||
| @@ -8,18 +9,21 @@ namespace DysonNetwork.Sphere.Account; | ||||
| public class NotificationService | ||||
| { | ||||
|     private readonly AppDatabase _db; | ||||
|     private readonly WebSocketService _ws; | ||||
|     private readonly ILogger<NotificationService> _logger; | ||||
|     private readonly FirebaseSender? _fcm; | ||||
|     private readonly ApnSender? _apns; | ||||
|  | ||||
|     public NotificationService( | ||||
|         AppDatabase db, | ||||
|         WebSocketService ws, | ||||
|         IConfiguration cfg, | ||||
|         IHttpClientFactory clientFactory, | ||||
|         ILogger<NotificationService> logger | ||||
|     ) | ||||
|     { | ||||
|         _db = db; | ||||
|         _ws = ws; | ||||
|         _logger = logger; | ||||
|  | ||||
|         var cfgSection = cfg.GetSection("Notifications:Push"); | ||||
| @@ -83,6 +87,7 @@ public class NotificationService | ||||
|  | ||||
|     public async Task<Notification> SendNotification( | ||||
|         Account account, | ||||
|         string topic, | ||||
|         string? title = null, | ||||
|         string? subtitle = null, | ||||
|         string? content = null, | ||||
| @@ -97,6 +102,7 @@ public class NotificationService | ||||
|  | ||||
|         var notification = new Notification | ||||
|         { | ||||
|             Topic = topic, | ||||
|             Title = title, | ||||
|             Subtitle = subtitle, | ||||
|             Content = content, | ||||
| @@ -114,7 +120,11 @@ public class NotificationService | ||||
|  | ||||
|     public async Task DeliveryNotification(Notification notification) | ||||
|     { | ||||
|         // TODO send websocket | ||||
|         _ws.SendPacketToAccount(notification.AccountId, new WebSocketPacket | ||||
|         { | ||||
|             Type = "notifications.new", | ||||
|             Data = notification | ||||
|         }); | ||||
|  | ||||
|         // Pushing the notification | ||||
|         var subscribers = await _db.NotificationPushSubscriptions | ||||
|   | ||||
| @@ -21,8 +21,6 @@ public class ActivityService(AppDatabase db) | ||||
|         { | ||||
|             var posts = await db.Posts.Where(e => postsId.Contains(e.Id)) | ||||
|                 .Include(e => e.Publisher) | ||||
|                 .Include(e => e.Publisher.Picture) | ||||
|                 .Include(e => e.Publisher.Background) | ||||
|                 .Include(e => e.ThreadedPost) | ||||
|                 .Include(e => e.ForwardedPost) | ||||
|                 .Include(e => e.Attachments) | ||||
|   | ||||
| @@ -16,8 +16,6 @@ public class UserInfoMiddleware(RequestDelegate next, IMemoryCache cache) | ||||
|                     .Include(e => e.Challenge) | ||||
|                     .Include(e => e.Account) | ||||
|                     .Include(e => e.Account.Profile) | ||||
|                     .Include(e => e.Account.Profile.Picture) | ||||
|                     .Include(e => e.Account.Profile.Background) | ||||
|                     .Where(e => e.Id == sessionId) | ||||
|                     .FirstOrDefaultAsync(); | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.Text.RegularExpressions; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
|  | ||||
| @@ -19,23 +19,68 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller | ||||
|     public class SendMessageRequest | ||||
|     { | ||||
|         [MaxLength(4096)] public string? Content { get; set; } | ||||
|         public List<CloudFile>? Attachments { get; set; } | ||||
|         [MaxLength(36)] public string? Nonce { get; set; } | ||||
|         public List<string>? AttachmentsId { get; set; } | ||||
|         public Dictionary<string, object>? Meta { get; set; } | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{roomId:long}/messages")] | ||||
|     public async Task<ActionResult<List<Message>>> ListMessages(long roomId, [FromQuery] int offset, [FromQuery] int take = 20) | ||||
|     { | ||||
|         var currentUser = HttpContext.Items["CurrentUser"] as Account.Account; | ||||
|  | ||||
|         var room = await db.ChatRooms.FirstOrDefaultAsync(r => r.Id == roomId); | ||||
|         if (room is null) return NotFound(); | ||||
|  | ||||
|         if (!room.IsPublic) | ||||
|         { | ||||
|             if (currentUser is null) return Unauthorized(); | ||||
|  | ||||
|             var member = await db.ChatMembers | ||||
|                 .Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId) | ||||
|                 .FirstOrDefaultAsync(); | ||||
|             if (member == null || member.Role < ChatMemberRole.Normal) | ||||
|                 return StatusCode(403, "You are not a member of this chat room."); | ||||
|         } | ||||
|  | ||||
|         var totalCount = await db.ChatMessages | ||||
|             .Where(m => m.ChatRoomId == roomId) | ||||
|             .CountAsync(); | ||||
|         var messages = await db.ChatMessages | ||||
|             .Where(m => m.ChatRoomId == roomId) | ||||
|             .OrderByDescending(m => m.CreatedAt) | ||||
|             .Include(m => m.Sender) | ||||
|             .Include(m => m.Sender.Account) | ||||
|             .Include(m => m.Sender.Account.Profile) | ||||
|             .Include(m => m.Sender.Account.Profile.Picture) | ||||
|             .Include(m => m.Sender.Account.Profile.Background) | ||||
|             .Include(m => m.Attachments) | ||||
|             .Skip(offset) | ||||
|             .Take(take) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         Response.Headers["X-Total"] = totalCount.ToString(); | ||||
|  | ||||
|         return Ok(messages); | ||||
|     } | ||||
|  | ||||
|     [GeneratedRegex(@"@([A-Za-z0-9_-]+)")] | ||||
|     private static partial Regex MentionRegex(); | ||||
|  | ||||
|     [HttpPost("{roomId:long}/messages")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult> SendMessage([FromBody] SendMessageRequest request, long roomId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         if (string.IsNullOrWhiteSpace(request.Content) && (request.Attachments == null || request.Attachments.Count == 0)) | ||||
|         if (string.IsNullOrWhiteSpace(request.Content) && (request.AttachmentsId == null || request.AttachmentsId.Count == 0)) | ||||
|             return BadRequest("You cannot send an empty message."); | ||||
|  | ||||
|         var member = await db.ChatMembers | ||||
|             .Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId) | ||||
|             .Include(m => m.ChatRoom) | ||||
|             .Include(m => m.ChatRoom.Realm) | ||||
|             .Include(m => m.Account) | ||||
|             .Include(m => m.Account.Profile) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (member == null || member.Role < ChatMemberRole.Normal) return StatusCode(403, "You need to be a normal member to send messages here."); | ||||
|  | ||||
| @@ -43,11 +88,20 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller | ||||
|         { | ||||
|             SenderId = member.Id, | ||||
|             ChatRoomId = roomId, | ||||
|             Nonce = request.Nonce ?? Guid.NewGuid().ToString(), | ||||
|             Meta = request.Meta ?? new Dictionary<string, object>(), | ||||
|         }; | ||||
|         if (request.Content is not null) | ||||
|             message.Content = request.Content; | ||||
|         if (request.Attachments is not null) | ||||
|             message.Attachments = request.Attachments; | ||||
|         if (request.AttachmentsId is not null) | ||||
|         { | ||||
|             var attachments = await db.Files | ||||
|                 .Where(f => request.AttachmentsId.Contains(f.Id)) | ||||
|                 .ToListAsync(); | ||||
|             message.Attachments = attachments | ||||
|                 .OrderBy(f => request.AttachmentsId.IndexOf(f.Id)) | ||||
|                 .ToList(); | ||||
|         } | ||||
|  | ||||
|         if (request.Content is not null) | ||||
|         { | ||||
| @@ -55,17 +109,16 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller | ||||
|                 .Matches(request.Content) | ||||
|                 .Select(m => m.Groups[1].Value) | ||||
|                 .ToList(); | ||||
|             if (mentioned is not null && mentioned.Count > 0) | ||||
|             if (mentioned.Count > 0) | ||||
|             { | ||||
|                 var mentionedMembers = await db.ChatMembers | ||||
|                    .Where(m => mentioned.Contains(m.Account.Name)) | ||||
|                    .Select(m => m.Id) | ||||
|                    .ToListAsync(); | ||||
|                 message.MembersMetioned = mentionedMembers; | ||||
|                 message.MembersMentioned = mentionedMembers; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         member.Account = currentUser; | ||||
|         var result = await cs.SendMessageAsync(message, member, member.ChatRoom); | ||||
|  | ||||
|         return Ok(result); | ||||
| @@ -78,9 +131,9 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{roomId:long}/sync")] | ||||
|     public async Task<ActionResult<SyncResponse>> GetSyncData([FromQuery] SyncRequest request, long roomId) | ||||
|     public async Task<ActionResult<SyncResponse>> GetSyncData([FromBody] SyncRequest request, long roomId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser)  | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) | ||||
|             return Unauthorized(); | ||||
|  | ||||
|         var isMember = await db.ChatMembers | ||||
|   | ||||
| @@ -19,7 +19,9 @@ public class ChatRoom : ModelBase | ||||
|     public ChatRoomType Type { get; set; } | ||||
|     public bool IsPublic { get; set; } | ||||
|  | ||||
|     public string? PictureId { get; set; } | ||||
|     public CloudFile? Picture { get; set; } | ||||
|     public string? BackgroundId { get; set; } | ||||
|     public CloudFile? Background { get; set; } | ||||
|  | ||||
|     [JsonIgnore] public ICollection<ChatMember> Members { get; set; } = new List<ChatMember>(); | ||||
| @@ -46,9 +48,9 @@ public class ChatMember : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } | ||||
|     public long ChatRoomId { get; set; } | ||||
|     [JsonIgnore] public ChatRoom ChatRoom { get; set; } = null!; | ||||
|     public ChatRoom ChatRoom { get; set; } = null!; | ||||
|     public long AccountId { get; set; } | ||||
|     [JsonIgnore] public Account.Account Account { get; set; } = null!; | ||||
|     public Account.Account Account { get; set; } = null!; | ||||
|  | ||||
|     [MaxLength(1024)] public string? Nick { get; set; } | ||||
|  | ||||
|   | ||||
| @@ -10,15 +10,13 @@ namespace DysonNetwork.Sphere.Chat; | ||||
|  | ||||
| [ApiController] | ||||
| [Route("/chat")] | ||||
| public class ChatRoomController(AppDatabase db, FileService fs) : ControllerBase | ||||
| public class ChatRoomController(AppDatabase db, FileService fs, ChatRoomService crs) : ControllerBase | ||||
| { | ||||
|     [HttpGet("{id:long}")] | ||||
|     public async Task<ActionResult<ChatRoom>> GetChatRoom(long id) | ||||
|     { | ||||
|         var chatRoom = await db.ChatRooms | ||||
|             .Where(c => c.Id == id) | ||||
|             .Include(e => e.Picture) | ||||
|             .Include(e => e.Background) | ||||
|             .Include(e => e.Realm) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (chatRoom is null) return NotFound(); | ||||
| @@ -35,8 +33,6 @@ public class ChatRoomController(AppDatabase db, FileService fs) : ControllerBase | ||||
|             .Where(m => m.AccountId == userId) | ||||
|             .Where(m => m.JoinedAt != null) | ||||
|             .Include(e => e.ChatRoom) | ||||
|             .Include(e => e.ChatRoom.Picture) | ||||
|             .Include(e => e.ChatRoom.Background) | ||||
|             .Select(m => m.ChatRoom) | ||||
|             .ToListAsync(); | ||||
|  | ||||
| @@ -129,7 +125,7 @@ public class ChatRoomController(AppDatabase db, FileService fs) : ControllerBase | ||||
|                 .Where(m => m.RealmId == chatRoom.RealmId) | ||||
|                 .FirstOrDefaultAsync(); | ||||
|             if (realmMember is null || realmMember.Role < RealmMemberRole.Moderator) | ||||
|                 return StatusCode(403, "You need at least be a realm moderator to update the chat.");  | ||||
|                 return StatusCode(403, "You need at least be a realm moderator to update the chat."); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
| @@ -192,7 +188,7 @@ public class ChatRoomController(AppDatabase db, FileService fs) : ControllerBase | ||||
|             .Include(c => c.Background) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (chatRoom is null) return NotFound(); | ||||
|          | ||||
|  | ||||
|         if (chatRoom.RealmId is not null) | ||||
|         { | ||||
|             var realmMember = await db.RealmMembers | ||||
| @@ -200,7 +196,7 @@ public class ChatRoomController(AppDatabase db, FileService fs) : ControllerBase | ||||
|                 .Where(m => m.RealmId == chatRoom.RealmId) | ||||
|                 .FirstOrDefaultAsync(); | ||||
|             if (realmMember is null || realmMember.Role < RealmMemberRole.Moderator) | ||||
|                 return StatusCode(403, "You need at least be a realm moderator to delete the chat.");  | ||||
|                 return StatusCode(403, "You need at least be a realm moderator to delete the chat."); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
| @@ -222,4 +218,311 @@ public class ChatRoomController(AppDatabase db, FileService fs) : ControllerBase | ||||
|  | ||||
|         return NoContent(); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{roomId:long}/members/me")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<ChatMember>> GetRoomIdentity(long roomId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.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) | ||||
|             .FirstOrDefaultAsync(); | ||||
|  | ||||
|         if (member == null) | ||||
|             return NotFound(); | ||||
|  | ||||
|         return Ok(member); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("{roomId:long}/members")] | ||||
|     public async Task<ActionResult<List<ChatMember>>> ListMembers(long roomId, [FromQuery] int take = 20, | ||||
|         [FromQuery] int skip = 0) | ||||
|     { | ||||
|         var currentUser = HttpContext.Items["CurrentUser"] as Account.Account; | ||||
|  | ||||
|         var room = await db.ChatRooms | ||||
|             .FirstOrDefaultAsync(r => r.Id == roomId); | ||||
|         if (room is null) return NotFound(); | ||||
|  | ||||
|         if (!room.IsPublic) | ||||
|         { | ||||
|             if (currentUser is null) return Unauthorized(); | ||||
|             var member = await db.ChatMembers | ||||
|                 .FirstOrDefaultAsync(m => m.ChatRoomId == roomId && m.AccountId == currentUser.Id); | ||||
|             if (member is null) return StatusCode(403, "You need to be a member to see members of private chat room."); | ||||
|         } | ||||
|  | ||||
|         var query = db.ChatMembers | ||||
|             .Where(m => m.ChatRoomId == roomId) | ||||
|             .Include(m => m.Account) | ||||
|             .Include(m => m.Account.Profile); | ||||
|  | ||||
|         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); | ||||
|     } | ||||
|  | ||||
|     public class ChatMemberRequest | ||||
|     { | ||||
|         [Required] public long RelatedUserId { get; set; } | ||||
|         [Required] public ChatMemberRole Role { get; set; } | ||||
|     } | ||||
|  | ||||
|     [HttpPost("invites/{roomId:long}")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<ChatMember>> InviteMember(long roomId, | ||||
|         [FromBody] ChatMemberRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
|  | ||||
|         var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId); | ||||
|         if (relatedUser is null) return BadRequest("Related user was not found"); | ||||
|  | ||||
|         var chatRoom = await db.ChatRooms | ||||
|             .Where(p => p.Id == roomId) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (chatRoom is null) return NotFound(); | ||||
|  | ||||
|         // Handle realm-owned chat rooms | ||||
|         if (chatRoom.RealmId is not null) | ||||
|         { | ||||
|             var realmMember = await db.RealmMembers | ||||
|                 .Where(m => m.AccountId == userId) | ||||
|                 .Where(m => m.RealmId == chatRoom.RealmId) | ||||
|                 .FirstOrDefaultAsync(); | ||||
|             if (realmMember is null || realmMember.Role < RealmMemberRole.Moderator) | ||||
|                 return StatusCode(403, "You need at least be a realm moderator to invite members to this chat."); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             var chatMember = await db.ChatMembers | ||||
|                 .Where(m => m.AccountId == userId) | ||||
|                 .Where(m => m.ChatRoomId == roomId) | ||||
|                 .FirstOrDefaultAsync(); | ||||
|             if (chatMember is null) return StatusCode(403, "You are not even a member of the targeted chat room."); | ||||
|             if (chatMember.Role < ChatMemberRole.Moderator) | ||||
|                 return StatusCode(403, | ||||
|                     "You need at least be a moderator to invite other members to this chat room."); | ||||
|             if (chatMember.Role < request.Role) | ||||
|                 return StatusCode(403, "You cannot invite member with higher permission than yours."); | ||||
|         } | ||||
|  | ||||
|         var newMember = new ChatMember | ||||
|         { | ||||
|             AccountId = relatedUser.Id, | ||||
|             ChatRoomId = roomId, | ||||
|             Role = request.Role, | ||||
|         }; | ||||
|  | ||||
|         db.ChatMembers.Add(newMember); | ||||
|         await db.SaveChangesAsync(); | ||||
|  | ||||
|         await crs.SendInviteNotify(newMember); | ||||
|  | ||||
|         return Ok(newMember); | ||||
|     } | ||||
|  | ||||
|     [HttpGet("invites")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<ChatMember>>> ListChatInvites() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
|  | ||||
|         var members = await db.ChatMembers | ||||
|             .Where(m => m.AccountId == userId) | ||||
|             .Where(m => m.JoinedAt == null) | ||||
|             .Include(e => e.ChatRoom) | ||||
|             .Include(e => e.Account) | ||||
|             .Include(e => e.Account.Profile) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         return members.ToList(); | ||||
|     } | ||||
|  | ||||
|     [HttpPost("invites/{roomId:long}/accept")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<ChatRoom>> AcceptChatInvite(long roomId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
|  | ||||
|         var member = await db.ChatMembers | ||||
|             .Where(m => m.AccountId == userId) | ||||
|             .Where(m => m.ChatRoomId == roomId) | ||||
|             .Where(m => m.JoinedAt == null) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (member is null) return NotFound(); | ||||
|  | ||||
|         member.JoinedAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow); | ||||
|         db.Update(member); | ||||
|         await db.SaveChangesAsync(); | ||||
|  | ||||
|         return Ok(member); | ||||
|     } | ||||
|  | ||||
|     [HttpPost("invites/{roomId:long}/decline")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult> DeclineChatInvite(long roomId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
|  | ||||
|         var member = await db.ChatMembers | ||||
|             .Where(m => m.AccountId == userId) | ||||
|             .Where(m => m.ChatRoomId == roomId) | ||||
|             .Where(m => m.JoinedAt == null) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (member is null) return NotFound(); | ||||
|  | ||||
|         db.ChatMembers.Remove(member); | ||||
|         await db.SaveChangesAsync(); | ||||
|  | ||||
|         return NoContent(); | ||||
|     } | ||||
|  | ||||
|     [HttpPatch("{roomId:long}/members/{memberId:long}/role")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<ChatMember>> UpdateChatMemberRole(long roomId, long memberId,  | ||||
|         [FromBody] ChatMemberRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         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) | ||||
|         { | ||||
|             var realmMember = await db.RealmMembers | ||||
|                 .Where(m => m.AccountId == currentUser.Id) | ||||
|                 .Where(m => m.RealmId == chatRoom.RealmId) | ||||
|                 .FirstOrDefaultAsync(); | ||||
|             if (realmMember is null || realmMember.Role < RealmMemberRole.Moderator) | ||||
|                 return StatusCode(403, "You need at least be a realm moderator to change member roles."); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             // Check if the current user has permission to change roles | ||||
|             var currentMember = await db.ChatMembers | ||||
|                 .Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId) | ||||
|                 .FirstOrDefaultAsync(); | ||||
|             if (currentMember is null || currentMember.Role < ChatMemberRole.Moderator) | ||||
|                 return StatusCode(403, "You need at least be a moderator to change member roles."); | ||||
|  | ||||
|             // Find the target member | ||||
|             var targetMember = await db.ChatMembers | ||||
|                 .Where(m => m.AccountId == memberId && m.ChatRoomId == roomId) | ||||
|                 .FirstOrDefaultAsync(); | ||||
|             if (targetMember is null) return NotFound(); | ||||
|  | ||||
|             // Check if current user has sufficient permissions | ||||
|             if (currentMember.Role <= targetMember.Role) | ||||
|                 return StatusCode(403, "You cannot modify the role of members with equal or higher roles."); | ||||
|             if (currentMember.Role <= request.Role) | ||||
|                 return StatusCode(403, "You cannot assign a role equal to or higher than your own."); | ||||
|  | ||||
|             targetMember.Role = request.Role; | ||||
|             db.ChatMembers.Update(targetMember); | ||||
|             await db.SaveChangesAsync(); | ||||
|  | ||||
|             return Ok(targetMember); | ||||
|         } | ||||
|  | ||||
|         return BadRequest(); | ||||
|     } | ||||
|  | ||||
|     [HttpDelete("{roomId:long}/members/{memberId:long}")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult> RemoveChatMember(long roomId, long memberId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         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) | ||||
|         { | ||||
|             var realmMember = await db.RealmMembers | ||||
|                 .Where(m => m.AccountId == currentUser.Id) | ||||
|                 .Where(m => m.RealmId == chatRoom.RealmId) | ||||
|                 .FirstOrDefaultAsync(); | ||||
|             if (realmMember is null || realmMember.Role < RealmMemberRole.Moderator) | ||||
|                 return StatusCode(403, "You need at least be a realm moderator to remove members."); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             // Check if the current user has permission to remove members | ||||
|             var currentMember = await db.ChatMembers | ||||
|                 .Where(m => m.AccountId == currentUser.Id && m.ChatRoomId == roomId) | ||||
|                 .FirstOrDefaultAsync(); | ||||
|             if (currentMember is null || currentMember.Role < ChatMemberRole.Moderator) | ||||
|                 return StatusCode(403, "You need at least be a moderator to remove members."); | ||||
|  | ||||
|             // Find the target member | ||||
|             var targetMember = await db.ChatMembers | ||||
|                 .Where(m => m.AccountId == memberId && m.ChatRoomId == roomId) | ||||
|                 .FirstOrDefaultAsync(); | ||||
|             if (targetMember is null) return NotFound(); | ||||
|  | ||||
|             // Check if current user has sufficient permissions | ||||
|             if (currentMember.Role <= targetMember.Role) | ||||
|                 return StatusCode(403, "You cannot remove members with equal or higher roles."); | ||||
|  | ||||
|             db.ChatMembers.Remove(targetMember); | ||||
|             await db.SaveChangesAsync(); | ||||
|  | ||||
|             return NoContent(); | ||||
|         } | ||||
|  | ||||
|         return BadRequest(); | ||||
|     } | ||||
|  | ||||
|     [HttpDelete("{roomId:long}/members/me")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult> LeaveChat(long roomId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var member = await db.ChatMembers | ||||
|             .Where(m => m.AccountId == currentUser.Id) | ||||
|             .Where(m => m.ChatRoomId == roomId) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (member is null) return NotFound(); | ||||
|  | ||||
|         if (member.Role == ChatMemberRole.Owner) | ||||
|         { | ||||
|             // Check if this is the only owner | ||||
|             var otherOwners = await db.ChatMembers | ||||
|                 .Where(m => m.ChatRoomId == roomId) | ||||
|                 .Where(m => m.Role == ChatMemberRole.Owner) | ||||
|                 .Where(m => m.AccountId != currentUser.Id) | ||||
|                 .AnyAsync(); | ||||
|              | ||||
|             if (!otherOwners) | ||||
|                 return BadRequest("The last owner cannot leave the chat. Transfer ownership first or delete the chat."); | ||||
|         } | ||||
|  | ||||
|         db.ChatMembers.Remove(member); | ||||
|         await db.SaveChangesAsync(); | ||||
|  | ||||
|         return NoContent(); | ||||
|     } | ||||
| } | ||||
| @@ -1,5 +1,12 @@ | ||||
| using DysonNetwork.Sphere.Account; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Chat; | ||||
|  | ||||
| public class ChatRoomService() | ||||
| public class ChatRoomService(NotificationService nty) | ||||
| { | ||||
|     public async Task SendInviteNotify(ChatMember member) | ||||
|     { | ||||
|         await nty.SendNotification(member.Account, "invites.chats", "New Chat Invitation", null, | ||||
|             $"You just got invited to join {member.ChatRoom.Name}"); | ||||
|     } | ||||
| } | ||||
| @@ -5,40 +5,54 @@ using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Chat; | ||||
|  | ||||
| public class ChatService(AppDatabase db, NotificationService nty, WebSocketService ws) | ||||
| public class ChatService(AppDatabase db, IServiceScopeFactory scopeFactory) | ||||
| { | ||||
|     public async Task<Message> SendMessageAsync(Message message, ChatMember sender, ChatRoom room) | ||||
|     { | ||||
|         // First complete the save operation | ||||
|         db.ChatMessages.Add(message); | ||||
|         await db.SaveChangesAsync(); | ||||
|         _ = DeliverMessageAsync(message, sender, room).ConfigureAwait(false); | ||||
|          | ||||
|         // Then start the delivery process | ||||
|         // Using ConfigureAwait(false) is correct here since we don't need context to flow | ||||
|         _ = Task.Run(() => DeliverMessageAsync(message, sender, room)) | ||||
|             .ConfigureAwait(false); | ||||
|          | ||||
|         return message; | ||||
|     } | ||||
|  | ||||
|     public async Task DeliverMessageAsync(Message message, ChatMember sender, ChatRoom room) | ||||
|     { | ||||
|         var scope = scopeFactory.CreateScope(); | ||||
|         var scopedDb = scope.ServiceProvider.GetRequiredService<AppDatabase>(); | ||||
|         var scopedWs = scope.ServiceProvider.GetRequiredService<WebSocketService>(); | ||||
|         var scopedNty = scope.ServiceProvider.GetRequiredService<NotificationService>(); | ||||
|  | ||||
|         var roomSubject = room.Realm is not null ? $"{room.Name}, {room.Realm.Name}" : room.Name; | ||||
|         var tasks = new List<Task>(); | ||||
|         await foreach ( | ||||
|             var member in db.ChatMembers | ||||
|                    .Where(m => m.ChatRoomId == message.ChatRoomId && m.AccountId != message.Sender.AccountId) | ||||
|                    .Where(m => m.Notify != ChatMemberNotify.None) | ||||
|                    .Where(m => m.Notify != ChatMemberNotify.Mentions || (message.MembersMetioned != null && message.MembersMetioned.Contains(m.Id))) | ||||
|                    .AsAsyncEnumerable() | ||||
|         ) | ||||
|  | ||||
|         var members = await scopedDb.ChatMembers | ||||
|             .Where(m => m.ChatRoomId == message.ChatRoomId) | ||||
|             .Where(m => m.Notify != ChatMemberNotify.None) | ||||
|             .Where(m => m.Notify != ChatMemberNotify.Mentions ||  | ||||
|                    (message.MembersMentioned != null && message.MembersMentioned.Contains(m.Id))) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         foreach (var member in members) | ||||
|         { | ||||
|             ws.SendPacketToAccount(member.AccountId, new WebSocketPacket | ||||
|             scopedWs.SendPacketToAccount(member.AccountId, new WebSocketPacket | ||||
|             { | ||||
|                 Type = "messages.new", | ||||
|                 Data = message | ||||
|             }); | ||||
|             tasks.Add(nty.DeliveryNotification(new Notification | ||||
|             tasks.Add(scopedNty.DeliveryNotification(new Notification | ||||
|             { | ||||
|                 AccountId = member.AccountId, | ||||
|                 Topic = "messages.new", | ||||
|                 Title = $"{sender.Nick ?? sender.Account.Nick} ({roomSubject})", | ||||
|             })); | ||||
|         } | ||||
|  | ||||
|         await Task.WhenAll(tasks); | ||||
|     } | ||||
|  | ||||
| @@ -94,7 +108,7 @@ public class ChatService(AppDatabase db, NotificationService nty, WebSocketServi | ||||
|             .Select(m => new MessageChange | ||||
|             { | ||||
|                 MessageId = m.Id, | ||||
|                 Action = m.DeletedAt != null ? "delete" : (m.EditedAt == null ? "create" : "update"), | ||||
|                 Action = m.DeletedAt != null ? "delete" : (m.UpdatedAt == null ? "create" : "update"), | ||||
|                 Message = m.DeletedAt != null ? null : m, | ||||
|                 Timestamp = m.DeletedAt != null ? m.DeletedAt.Value : m.UpdatedAt | ||||
|             }) | ||||
|   | ||||
| @@ -9,10 +9,10 @@ namespace DysonNetwork.Sphere.Chat; | ||||
| public class Message : ModelBase | ||||
| { | ||||
|     public Guid Id { get; set; } = Guid.NewGuid(); | ||||
|     [MaxLength(1024)] public string Type { get; set; } = null!; | ||||
|     [MaxLength(4096)] public string Content { get; set; } = string.Empty; | ||||
|     [Column(TypeName = "jsonb")] public Dictionary<string, object>? Meta { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public List<Guid>? MembersMetioned { get; set; } | ||||
|     [Column(TypeName = "jsonb")] public List<Guid>? MembersMentioned { get; set; } | ||||
|     [MaxLength(36)] public string Nonce { get; set; } = null!; | ||||
|     public Instant? EditedAt { get; set; } | ||||
|  | ||||
|     public ICollection<CloudFile> Attachments { get; set; } = new List<CloudFile>(); | ||||
| @@ -28,6 +28,33 @@ public class Message : ModelBase | ||||
|     public ChatMember Sender { get; set; } = null!; | ||||
|     public long ChatRoomId { get; set; } | ||||
|     [JsonIgnore] public ChatRoom ChatRoom { get; set; } = null!; | ||||
|      | ||||
|     public Message Clone() | ||||
|     { | ||||
|         return new Message | ||||
|         { | ||||
|             Id = Id, | ||||
|             Content = Content, | ||||
|             Meta = Meta?.ToDictionary(entry => entry.Key, entry => entry.Value), | ||||
|             MembersMentioned = MembersMentioned?.ToList(), | ||||
|             Nonce = Nonce, | ||||
|             EditedAt = EditedAt, | ||||
|             Attachments = new List<CloudFile>(Attachments), | ||||
|             Reactions = new List<MessageReaction>(Reactions), | ||||
|             Statuses = new List<MessageStatus>(Statuses), | ||||
|             RepliedMessageId = RepliedMessageId, | ||||
|             RepliedMessage = RepliedMessage?.Clone() as Message, | ||||
|             ForwardedMessageId = ForwardedMessageId, | ||||
|             ForwardedMessage = ForwardedMessage?.Clone() as Message, | ||||
|             SenderId = SenderId, | ||||
|             Sender = Sender, | ||||
|             ChatRoomId = ChatRoomId, | ||||
|             ChatRoom = ChatRoom, | ||||
|             CreatedAt = CreatedAt, | ||||
|             UpdatedAt = UpdatedAt, | ||||
|             DeletedAt = DeletedAt | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| public enum MessageReactionAttitude | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,238 +0,0 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NodaTime; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class AddChatMessage : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropPrimaryKey( | ||||
|                 name: "pk_chat_members", | ||||
|                 table: "chat_members"); | ||||
|  | ||||
|             migrationBuilder.AddColumn<Guid>( | ||||
|                 name: "message_id", | ||||
|                 table: "files", | ||||
|                 type: "uuid", | ||||
|                 nullable: true); | ||||
|  | ||||
|             migrationBuilder.AddColumn<Guid>( | ||||
|                 name: "id", | ||||
|                 table: "chat_members", | ||||
|                 type: "uuid", | ||||
|                 nullable: false, | ||||
|                 defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); | ||||
|  | ||||
|             migrationBuilder.AddUniqueConstraint( | ||||
|                 name: "ak_chat_members_chat_room_id_account_id", | ||||
|                 table: "chat_members", | ||||
|                 columns: new[] { "chat_room_id", "account_id" }); | ||||
|  | ||||
|             migrationBuilder.AddPrimaryKey( | ||||
|                 name: "pk_chat_members", | ||||
|                 table: "chat_members", | ||||
|                 column: "id"); | ||||
|  | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "chat_messages", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     type = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false), | ||||
|                     content = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false), | ||||
|                     meta = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: true), | ||||
|                     members_metioned = table.Column<List<Guid>>(type: "jsonb", nullable: true), | ||||
|                     edited_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true), | ||||
|                     replied_message_id = table.Column<Guid>(type: "uuid", nullable: true), | ||||
|                     forwarded_message_id = table.Column<Guid>(type: "uuid", nullable: true), | ||||
|                     sender_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     chat_room_id = table.Column<long>(type: "bigint", nullable: false), | ||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("pk_chat_messages", x => x.id); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_chat_messages_chat_members_sender_id", | ||||
|                         column: x => x.sender_id, | ||||
|                         principalTable: "chat_members", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_chat_messages_chat_messages_forwarded_message_id", | ||||
|                         column: x => x.forwarded_message_id, | ||||
|                         principalTable: "chat_messages", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Restrict); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_chat_messages_chat_messages_replied_message_id", | ||||
|                         column: x => x.replied_message_id, | ||||
|                         principalTable: "chat_messages", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Restrict); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_chat_messages_chat_rooms_chat_room_id", | ||||
|                         column: x => x.chat_room_id, | ||||
|                         principalTable: "chat_rooms", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                 }); | ||||
|  | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "chat_statuses", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     message_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     sender_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     read_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("pk_chat_statuses", x => new { x.message_id, x.sender_id }); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_chat_statuses_chat_members_sender_id", | ||||
|                         column: x => x.sender_id, | ||||
|                         principalTable: "chat_members", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_chat_statuses_chat_messages_message_id", | ||||
|                         column: x => x.message_id, | ||||
|                         principalTable: "chat_messages", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                 }); | ||||
|  | ||||
|             migrationBuilder.CreateTable( | ||||
|                 name: "message_reaction", | ||||
|                 columns: table => new | ||||
|                 { | ||||
|                     id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     message_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     sender_id = table.Column<Guid>(type: "uuid", nullable: false), | ||||
|                     symbol = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false), | ||||
|                     attitude = table.Column<int>(type: "integer", nullable: false), | ||||
|                     created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false), | ||||
|                     deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true) | ||||
|                 }, | ||||
|                 constraints: table => | ||||
|                 { | ||||
|                     table.PrimaryKey("pk_message_reaction", x => x.id); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_message_reaction_chat_members_sender_id", | ||||
|                         column: x => x.sender_id, | ||||
|                         principalTable: "chat_members", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                     table.ForeignKey( | ||||
|                         name: "fk_message_reaction_chat_messages_message_id", | ||||
|                         column: x => x.message_id, | ||||
|                         principalTable: "chat_messages", | ||||
|                         principalColumn: "id", | ||||
|                         onDelete: ReferentialAction.Cascade); | ||||
|                 }); | ||||
|  | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_files_message_id", | ||||
|                 table: "files", | ||||
|                 column: "message_id"); | ||||
|  | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_chat_messages_chat_room_id", | ||||
|                 table: "chat_messages", | ||||
|                 column: "chat_room_id"); | ||||
|  | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_chat_messages_forwarded_message_id", | ||||
|                 table: "chat_messages", | ||||
|                 column: "forwarded_message_id"); | ||||
|  | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_chat_messages_replied_message_id", | ||||
|                 table: "chat_messages", | ||||
|                 column: "replied_message_id"); | ||||
|  | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_chat_messages_sender_id", | ||||
|                 table: "chat_messages", | ||||
|                 column: "sender_id"); | ||||
|  | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_chat_statuses_sender_id", | ||||
|                 table: "chat_statuses", | ||||
|                 column: "sender_id"); | ||||
|  | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_message_reaction_message_id", | ||||
|                 table: "message_reaction", | ||||
|                 column: "message_id"); | ||||
|  | ||||
|             migrationBuilder.CreateIndex( | ||||
|                 name: "ix_message_reaction_sender_id", | ||||
|                 table: "message_reaction", | ||||
|                 column: "sender_id"); | ||||
|  | ||||
|             migrationBuilder.AddForeignKey( | ||||
|                 name: "fk_files_chat_messages_message_id", | ||||
|                 table: "files", | ||||
|                 column: "message_id", | ||||
|                 principalTable: "chat_messages", | ||||
|                 principalColumn: "id"); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropForeignKey( | ||||
|                 name: "fk_files_chat_messages_message_id", | ||||
|                 table: "files"); | ||||
|  | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "chat_statuses"); | ||||
|  | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "message_reaction"); | ||||
|  | ||||
|             migrationBuilder.DropTable( | ||||
|                 name: "chat_messages"); | ||||
|  | ||||
|             migrationBuilder.DropIndex( | ||||
|                 name: "ix_files_message_id", | ||||
|                 table: "files"); | ||||
|  | ||||
|             migrationBuilder.DropUniqueConstraint( | ||||
|                 name: "ak_chat_members_chat_room_id_account_id", | ||||
|                 table: "chat_members"); | ||||
|  | ||||
|             migrationBuilder.DropPrimaryKey( | ||||
|                 name: "pk_chat_members", | ||||
|                 table: "chat_members"); | ||||
|  | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "message_id", | ||||
|                 table: "files"); | ||||
|  | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "id", | ||||
|                 table: "chat_members"); | ||||
|  | ||||
|             migrationBuilder.AddPrimaryKey( | ||||
|                 name: "pk_chat_members", | ||||
|                 table: "chat_members", | ||||
|                 columns: new[] { "chat_room_id", "account_id" }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,62 +0,0 @@ | ||||
| using Microsoft.EntityFrameworkCore.Migrations; | ||||
| using NpgsqlTypes; | ||||
|  | ||||
| #nullable disable | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Migrations | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public partial class DontKnowHowToNameThing : Migration | ||||
|     { | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Up(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.AlterColumn<NpgsqlTsVector>( | ||||
|                 name: "search_vector", | ||||
|                 table: "posts", | ||||
|                 type: "tsvector", | ||||
|                 nullable: true, | ||||
|                 oldClrType: typeof(NpgsqlTsVector), | ||||
|                 oldType: "tsvector") | ||||
|                 .OldAnnotation("Npgsql:TsVectorConfig", "simple") | ||||
|                 .OldAnnotation("Npgsql:TsVectorProperties", new[] { "title", "description", "content" }); | ||||
|  | ||||
|             migrationBuilder.AddColumn<string>( | ||||
|                 name: "nick", | ||||
|                 table: "chat_members", | ||||
|                 type: "character varying(1024)", | ||||
|                 maxLength: 1024, | ||||
|                 nullable: true); | ||||
|  | ||||
|             migrationBuilder.AddColumn<int>( | ||||
|                 name: "notify", | ||||
|                 table: "chat_members", | ||||
|                 type: "integer", | ||||
|                 nullable: false, | ||||
|                 defaultValue: 0); | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc /> | ||||
|         protected override void Down(MigrationBuilder migrationBuilder) | ||||
|         { | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "nick", | ||||
|                 table: "chat_members"); | ||||
|  | ||||
|             migrationBuilder.DropColumn( | ||||
|                 name: "notify", | ||||
|                 table: "chat_members"); | ||||
|  | ||||
|             migrationBuilder.AlterColumn<NpgsqlTsVector>( | ||||
|                 name: "search_vector", | ||||
|                 table: "posts", | ||||
|                 type: "tsvector", | ||||
|                 nullable: false, | ||||
|                 oldClrType: typeof(NpgsqlTsVector), | ||||
|                 oldType: "tsvector", | ||||
|                 oldNullable: true) | ||||
|                 .Annotation("Npgsql:TsVectorConfig", "simple") | ||||
|                 .Annotation("Npgsql:TsVectorProperties", new[] { "title", "description", "content" }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -24,8 +24,6 @@ public class PostController(AppDatabase db, PostService ps, RelationshipService | ||||
|             .CountAsync(); | ||||
|         var posts = await db.Posts | ||||
|             .Include(e => e.Publisher) | ||||
|             .Include(e => e.Publisher.Picture) | ||||
|             .Include(e => e.Publisher.Background) | ||||
|             .Include(e => e.ThreadedPost) | ||||
|             .Include(e => e.ForwardedPost) | ||||
|             .Include(e => e.Attachments) | ||||
| @@ -54,8 +52,6 @@ public class PostController(AppDatabase db, PostService ps, RelationshipService | ||||
|         var post = await db.Posts | ||||
|             .Where(e => e.Id == id) | ||||
|             .Include(e => e.Publisher) | ||||
|             .Include(e => e.Publisher.Picture) | ||||
|             .Include(e => e.Publisher.Background) | ||||
|             .Include(e => e.RepliedPost) | ||||
|             .Include(e => e.ThreadedPost) | ||||
|             .Include(e => e.ForwardedPost) | ||||
|   | ||||
| @@ -22,7 +22,9 @@ public class Publisher : ModelBase | ||||
|     [MaxLength(256)] public string Nick { get; set; } = string.Empty; | ||||
|     [MaxLength(4096)] public string? Bio { get; set; } | ||||
|  | ||||
|     public string? PictureId { get; set; } | ||||
|     public CloudFile? Picture { get; set; } | ||||
|     public string? BackgroundId { get; set; } | ||||
|     public CloudFile? Background { get; set; } | ||||
|  | ||||
|     [JsonIgnore] public ICollection<Post> Posts { get; set; } = new List<Post>(); | ||||
|   | ||||
| @@ -20,8 +20,6 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic | ||||
|  | ||||
|         var publisher = await db.Publishers | ||||
|             .Where(e => e.Name == name) | ||||
|             .Include(e => e.Picture) | ||||
|             .Include(e => e.Background) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (publisher is null) return NotFound(); | ||||
|  | ||||
|   | ||||
| @@ -19,9 +19,11 @@ public class Realm : ModelBase | ||||
|     public bool IsCommunity { get; set; } | ||||
|     public bool IsPublic { get; set; } | ||||
|  | ||||
|     public string? PictureId { get; set; } | ||||
|     public CloudFile? Picture { get; set; } | ||||
|     public string? BackgroundId { get; set; } | ||||
|     public CloudFile? Background { get; set; } | ||||
|      | ||||
|  | ||||
|     [JsonIgnore] public ICollection<RealmMember> Members { get; set; } = new List<RealmMember>(); | ||||
|     [JsonIgnore] public ICollection<ChatRoom> ChatRooms { get; set; } = new List<ChatRoom>(); | ||||
|  | ||||
| @@ -39,10 +41,10 @@ public enum RealmMemberRole | ||||
| public class RealmMember : ModelBase | ||||
| { | ||||
|     public long RealmId { get; set; } | ||||
|     [JsonIgnore] public Realm  Realm { get; set; } = null!; | ||||
|     [JsonIgnore] public Realm Realm { get; set; } = null!; | ||||
|     public long AccountId { get; set; } | ||||
|     [JsonIgnore] public Account.Account Account { get; set; } = null!; | ||||
|      | ||||
|  | ||||
|     public RealmMemberRole Role { get; set; } = RealmMemberRole.Normal; | ||||
|     public Instant? JoinedAt { get; set; } | ||||
| } | ||||
| @@ -34,8 +34,6 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs) : | ||||
|             .Where(m => m.AccountId == userId) | ||||
|             .Where(m => m.JoinedAt != null) | ||||
|             .Include(e => e.Realm) | ||||
|             .Include(e => e.Realm.Picture) | ||||
|             .Include(e => e.Realm.Background) | ||||
|             .Select(m => m.Realm) | ||||
|             .ToListAsync(); | ||||
|  | ||||
| @@ -55,8 +53,6 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs) : | ||||
|             .Where(m => m.AccountId == userId) | ||||
|             .Where(m => m.JoinedAt == null) | ||||
|             .Include(e => e.Realm) | ||||
|             .Include(e => e.Realm.Picture) | ||||
|             .Include(e => e.Realm.Background) | ||||
|             .ToListAsync(); | ||||
|  | ||||
|         return members.ToList(); | ||||
| @@ -81,8 +77,6 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs) : | ||||
|  | ||||
|         var realm = await db.Realms | ||||
|             .Where(p => p.Slug == slug) | ||||
|             .Include(publisher => publisher.Picture) | ||||
|             .Include(publisher => publisher.Background) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (realm is null) return NotFound(); | ||||
|  | ||||
| @@ -107,6 +101,9 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs) : | ||||
|         db.RealmMembers.Add(newMember); | ||||
|         await db.SaveChangesAsync(); | ||||
|  | ||||
|         newMember.Account = relatedUser; | ||||
|         await rs.SendInviteNotify(newMember); | ||||
|  | ||||
|         return Ok(newMember); | ||||
|     } | ||||
|  | ||||
| @@ -151,6 +148,29 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs) : | ||||
|         return NoContent(); | ||||
|     } | ||||
|  | ||||
|     [HttpDelete("{slug}/members/me")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult> LeaveRealm(string slug) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
|      | ||||
|         var member = await db.RealmMembers | ||||
|             .Where(m => m.AccountId == userId) | ||||
|             .Where(m => m.Realm.Slug == slug) | ||||
|             .Where(m => m.JoinedAt != null) | ||||
|             .FirstOrDefaultAsync(); | ||||
|         if (member is null) return NotFound(); | ||||
|          | ||||
|         if (member.Role == RealmMemberRole.Owner) | ||||
|             return StatusCode(403, "Owner cannot leave their own realm."); | ||||
|      | ||||
|         db.RealmMembers.Remove(member); | ||||
|         await db.SaveChangesAsync(); | ||||
|      | ||||
|         return NoContent(); | ||||
|     } | ||||
|  | ||||
|     public class RealmRequest | ||||
|     { | ||||
|         [MaxLength(1024)] public string? Slug { get; set; } | ||||
|   | ||||
| @@ -1,5 +1,12 @@ | ||||
| using DysonNetwork.Sphere.Account; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Realm; | ||||
|  | ||||
| public class RealmService(AppDatabase db) | ||||
| public class RealmService(AppDatabase db, NotificationService nty) | ||||
| { | ||||
|     public async Task SendInviteNotify(RealmMember member) | ||||
|     { | ||||
|         await nty.SendNotification(member.Account, "invites.realms", "New Realm Invitation", null, | ||||
|             $"You just got invited to join {member.Realm.Name}"); | ||||
|     } | ||||
| } | ||||
| @@ -7,7 +7,7 @@ | ||||
|   }, | ||||
|   "AllowedHosts": "*", | ||||
|   "ConnectionStrings": { | ||||
|     "App": "Host=localhost;Port=5432;Database=dyson_network;Username=postgres;Password=postgres" | ||||
|     "App": "Host=localhost;Port=5432;Database=dyson_network;Username=postgres;Password=postgres;Include Error Detail=True" | ||||
|   }, | ||||
|   "Authentication": { | ||||
|     "Schemes": { | ||||
| @@ -38,7 +38,7 @@ | ||||
|         "Bucket": "solar-network", | ||||
|         "Endpoint": "0a70a6d1b7128888c823359d0008f4e1.r2.cloudflarestorage.com", | ||||
|         "SecretId": "8ff5d06c7b1639829d60bc6838a542e6", | ||||
|         "SecretKey": "4b000df2b31936e1ceb0aa48bbd4166214945bd7f83b85b26f9d164318587991", | ||||
|         "SecretKey": "fd58158c5201be16d1872c9209d9cf199421dae3c2f9972f94b2305976580d67", | ||||
|         "EnableSigned": true, | ||||
|         "EnableSsl": true | ||||
|       } | ||||
| @@ -47,17 +47,17 @@ | ||||
|   "Captcha": { | ||||
|     "Provider": "cloudflare", | ||||
|     "ApiKey": "0x4AAAAAABCDUdOujj4feOb_", | ||||
|     "ApiSecret": "" | ||||
|     "ApiSecret": "0x4AAAAAABCDUWABiJQweqlB7tYq-IqIm8U" | ||||
|   }, | ||||
|   "Notifications": { | ||||
|     "Push": { | ||||
|       "Production": true, | ||||
|       "Google": "path/to/config.json", | ||||
|       "Google": "./Keys/Solian.json", | ||||
|       "Apple": { | ||||
|         "PrivateKey": "path/to/cert.p8", | ||||
|         "PrivateKeyId": "", | ||||
|         "TeamId": "", | ||||
|         "BundleIdentifier": "" | ||||
|         "PrivateKey": "./Keys/Solian.p8", | ||||
|         "PrivateKeyId": "4US4KSX4W6", | ||||
|         "TeamId": "W7HPZ53V6B", | ||||
|         "BundleIdentifier": "dev.solsynth.solian" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
| @@ -65,7 +65,7 @@ | ||||
|     "Server": "smtpdm.aliyun.com", | ||||
|     "Port": 465, | ||||
|     "Username": "no-reply@mail.solsynth.dev", | ||||
|     "Password": "", | ||||
|     "Password": "Pe1UeV405PMcQZgv", | ||||
|     "FromAddress": "no-reply@mail.solsynth.dev", | ||||
|     "FromName": "Alphabot", | ||||
|     "SubjectPrefix": "Solar Network" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user