diff --git a/DysonNetwork.Pass/Wallet/SubscriptionGiftController.cs b/DysonNetwork.Pass/Wallet/SubscriptionGiftController.cs
new file mode 100644
index 0000000..754c7de
--- /dev/null
+++ b/DysonNetwork.Pass/Wallet/SubscriptionGiftController.cs
@@ -0,0 +1,313 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using NodaTime;
+using System.ComponentModel.DataAnnotations;
+using DysonNetwork.Shared.Models;
+
+namespace DysonNetwork.Pass.Wallet;
+
+[ApiController]
+[Route("/api/subscriptions/gifts")]
+public class GiftController(SubscriptionService subscriptions, AppDatabase db) : ControllerBase
+{
+ ///
+ /// Lists gifts purchased by the current user.
+ ///
+ [HttpGet("sent")]
+ [Authorize]
+ public async Task>> ListSentGifts(
+ [FromQuery] int offset = 0,
+ [FromQuery] int take = 20
+ )
+ {
+ if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
+
+ var query = await subscriptions.GetGiftsByGifterAsync(currentUser.Id);
+ var totalCount = query.Count;
+
+ var gifts = query
+ .Skip(offset)
+ .Take(take)
+ .ToList();
+
+ Response.Headers["X-Total"] = totalCount.ToString();
+
+ return gifts;
+ }
+
+ ///
+ /// Lists gifts received by the current user (both direct and redeemed open gifts).
+ ///
+ [HttpGet("received")]
+ [Authorize]
+ public async Task>> ListReceivedGifts(
+ [FromQuery] int offset = 0,
+ [FromQuery] int take = 20
+ )
+ {
+ if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
+
+ var gifts = await subscriptions.GetGiftsByRecipientAsync(currentUser.Id);
+ var totalCount = gifts.Count;
+
+ gifts = gifts
+ .Skip(offset)
+ .Take(take)
+ .ToList();
+
+ Response.Headers["X-Total"] = totalCount.ToString();
+
+ return gifts;
+ }
+
+ ///
+ /// Gets a specific gift by ID (only if user is the gifter or recipient).
+ ///
+ [HttpGet("{giftId}")]
+ [Authorize]
+ public async Task> GetGift(Guid giftId)
+ {
+ if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
+
+ var gift = await db.WalletGifts
+ .Include(g => g.Gifter)
+ .Include(g => g.Recipient)
+ .Include(g => g.Redeemer)
+ .Include(g => g.Subscription)
+ .Include(g => g.Coupon)
+ .FirstOrDefaultAsync(g => g.Id == giftId);
+
+ if (gift is null) return NotFound();
+ if (gift.GifterId != currentUser.Id && gift.RecipientId != currentUser.Id &&
+ !(gift.IsOpenGift && gift.RedeemerId == currentUser.Id))
+ return NotFound();
+
+ return gift;
+ }
+
+ ///
+ /// Checks if a gift code is valid and redeemable.
+ ///
+ [HttpGet("check/{giftCode}")]
+ [Authorize]
+ public async Task> CheckGiftCode(string giftCode)
+ {
+ if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
+
+ var gift = await subscriptions.GetGiftByCodeAsync(giftCode);
+ if (gift is null) return NotFound("Gift code not found.");
+
+ var canRedeem = false;
+ var error = "";
+
+ if (gift.Status != DysonNetwork.Shared.Models.GiftStatus.Sent)
+ {
+ error = gift.Status switch
+ {
+ DysonNetwork.Shared.Models.GiftStatus.Created => "Gift has not been sent yet.",
+ DysonNetwork.Shared.Models.GiftStatus.Redeemed => "Gift has already been redeemed.",
+ DysonNetwork.Shared.Models.GiftStatus.Expired => "Gift has expired.",
+ DysonNetwork.Shared.Models.GiftStatus.Cancelled => "Gift has been cancelled.",
+ _ => "Gift is not redeemable."
+ };
+ }
+ else if (gift.ExpiresAt < SystemClock.Instance.GetCurrentInstant())
+ {
+ error = "Gift has expired.";
+ }
+ else if (!gift.IsOpenGift && gift.RecipientId != currentUser.Id)
+ {
+ error = "This gift is intended for someone else.";
+ }
+ else
+ {
+ // Check if user already has this subscription type
+ var subscriptionInfo = SubscriptionTypeData
+ .SubscriptionDict.TryGetValue(gift.SubscriptionIdentifier, out var template)
+ ? template
+ : null;
+
+ if (subscriptionInfo != null)
+ {
+ var subscriptionsInGroup = subscriptionInfo.GroupIdentifier is not null
+ ? SubscriptionTypeData.SubscriptionDict
+ .Where(s => s.Value.GroupIdentifier == subscriptionInfo.GroupIdentifier)
+ .Select(s => s.Value.Identifier)
+ .ToArray()
+ : [gift.SubscriptionIdentifier];
+
+ var existingSubscription = await subscriptions.GetSubscriptionAsync(currentUser.Id, subscriptionsInGroup);
+ if (existingSubscription is not null)
+ {
+ error = "You already have an active subscription of this type.";
+ }
+ else if (subscriptionInfo.RequiredLevel > 0)
+ {
+ var profile = await db.AccountProfiles.FirstOrDefaultAsync(p => p.AccountId == currentUser.Id);
+ if (profile is null || profile.Level < subscriptionInfo.RequiredLevel)
+ {
+ error = $"Account level must be at least {subscriptionInfo.RequiredLevel} to redeem this gift.";
+ }
+ else
+ {
+ canRedeem = true;
+ }
+ }
+ else
+ {
+ canRedeem = true;
+ }
+ }
+ }
+
+ return new GiftCheckResponse
+ {
+ GiftCode = giftCode,
+ SubscriptionIdentifier = gift.SubscriptionIdentifier,
+ CanRedeem = canRedeem,
+ Error = error,
+ Message = gift.Message
+ };
+ }
+
+ public class GiftCheckResponse
+ {
+ public string GiftCode { get; set; } = null!;
+ public string SubscriptionIdentifier { get; set; } = null!;
+ public bool CanRedeem { get; set; }
+ public string Error { get; set; } = null!;
+ public string? Message { get; set; }
+ }
+
+ public class PurchaseGiftRequest
+ {
+ [Required] public string SubscriptionIdentifier { get; set; } = null!;
+ public Guid? RecipientId { get; set; }
+ [Required] public string PaymentMethod { get; set; } = null!;
+ [Required] public SnPaymentDetails PaymentDetails { get; set; } = null!;
+ public string? Message { get; set; }
+ public string? Coupon { get; set; }
+ public int? GiftDurationDays { get; set; } = 30; // Gift expires in 30 days by default
+ public int? SubscriptionDurationDays { get; set; } = 30; // Subscription lasts 30 days when redeemed
+ }
+
+ ///
+ /// Purchases a gift subscription.
+ ///
+ [HttpPost("purchase")]
+ [Authorize]
+ public async Task> PurchaseGift([FromBody] PurchaseGiftRequest request)
+ {
+ if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
+
+ Duration? giftDuration = null;
+ if (request.GiftDurationDays.HasValue)
+ giftDuration = Duration.FromDays(request.GiftDurationDays.Value);
+
+ Duration? subscriptionDuration = null;
+ if (request.SubscriptionDurationDays.HasValue)
+ subscriptionDuration = Duration.FromDays(request.SubscriptionDurationDays.Value);
+
+ try
+ {
+ var gift = await subscriptions.PurchaseGiftAsync(
+ currentUser,
+ request.RecipientId,
+ request.SubscriptionIdentifier,
+ request.PaymentMethod,
+ request.PaymentDetails,
+ request.Message,
+ request.Coupon,
+ giftDuration,
+ subscriptionDuration
+ );
+
+ return gift;
+ }
+ catch (ArgumentOutOfRangeException ex)
+ {
+ return BadRequest(ex.Message);
+ }
+ catch (InvalidOperationException ex)
+ {
+ return BadRequest(ex.Message);
+ }
+ }
+
+ public class RedeemGiftRequest
+ {
+ [Required] public string GiftCode { get; set; } = null!;
+ }
+
+ ///
+ /// Redeems a gift using its code, creating a subscription for the current user.
+ ///
+ [HttpPost("redeem")]
+ [Authorize]
+ public async Task> RedeemGift([FromBody] RedeemGiftRequest request)
+ {
+ if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
+
+ try
+ {
+ var (gift, subscription) = await subscriptions.RedeemGiftAsync(currentUser, request.GiftCode);
+
+ return new RedeemGiftResponse
+ {
+ Gift = gift,
+ Subscription = subscription
+ };
+ }
+ catch (InvalidOperationException ex)
+ {
+ return BadRequest(ex.Message);
+ }
+ }
+
+ public class RedeemGiftResponse
+ {
+ public SnWalletGift Gift { get; set; } = null!;
+ public SnWalletSubscription Subscription { get; set; } = null!;
+ }
+
+ ///
+ /// Marks a gift as sent (ready for redemption).
+ ///
+ [HttpPost("{giftId}/send")]
+ [Authorize]
+ public async Task> SendGift(Guid giftId)
+ {
+ if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
+
+ try
+ {
+ var gift = await subscriptions.MarkGiftAsSentAsync(giftId, currentUser.Id);
+ return gift;
+ }
+ catch (InvalidOperationException ex)
+ {
+ return BadRequest(ex.Message);
+ }
+ }
+
+ ///
+ /// Cancels a gift before it's redeemed.
+ ///
+ [HttpPost("{giftId}/cancel")]
+ [Authorize]
+ public async Task> CancelGift(Guid giftId)
+ {
+ if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
+
+ try
+ {
+ var gift = await subscriptions.CancelGiftAsync(giftId, currentUser.Id);
+ return gift;
+ }
+ catch (InvalidOperationException ex)
+ {
+ return BadRequest(ex.Message);
+ }
+ }
+}
diff --git a/DysonNetwork.Sphere/Chat/ChatController.cs b/DysonNetwork.Sphere/Chat/ChatController.cs
index 67db829..a6f30df 100644
--- a/DysonNetwork.Sphere/Chat/ChatController.cs
+++ b/DysonNetwork.Sphere/Chat/ChatController.cs
@@ -85,7 +85,7 @@ public partial class ChatController(
var accountId = Guid.Parse(currentUser.Id);
var member = await db.ChatMembers
- .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId)
+ .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null)
.FirstOrDefaultAsync();
if (member == null || member.Role < ChatMemberRole.Member)
return StatusCode(403, "You are not a member of this chat room.");
@@ -127,7 +127,7 @@ public partial class ChatController(
var accountId = Guid.Parse(currentUser.Id);
var member = await db.ChatMembers
- .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId)
+ .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null)
.FirstOrDefaultAsync();
if (member == null || member.Role < ChatMemberRole.Member)
return StatusCode(403, "You are not a member of this chat room.");
@@ -221,7 +221,8 @@ public partial class ChatController(
.Select(a => Guid.Parse(a.Id))
.ToList();
var mentionedMembers = await db.ChatMembers
- .Where(m => mentionedId.Contains(m.AccountId))
+ .Where(m => m.ChatRoomId == roomId && mentionedId.Contains(m.AccountId))
+ .Where(m => m.JoinedAt != null && m.LeaveAt == null)
.Select(m => m.Id)
.ToListAsync();
message.MembersMentioned = mentionedMembers;
@@ -321,7 +322,7 @@ public partial class ChatController(
var accountId = Guid.Parse(currentUser.Id);
var isMember = await db.ChatMembers
- .AnyAsync(m => m.AccountId == accountId && m.ChatRoomId == roomId);
+ .AnyAsync(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null);
if (!isMember)
return StatusCode(403, "You are not a member of this chat room.");
diff --git a/DysonNetwork.Sphere/Chat/ChatRoomController.cs b/DysonNetwork.Sphere/Chat/ChatRoomController.cs
index 54c5ec0..51e3b4a 100644
--- a/DysonNetwork.Sphere/Chat/ChatRoomController.cs
+++ b/DysonNetwork.Sphere/Chat/ChatRoomController.cs
@@ -56,8 +56,7 @@ public class ChatRoomController(
var chatRooms = await db.ChatMembers
.Where(m => m.AccountId == accountId)
- .Where(m => m.JoinedAt != null)
- .Where(m => m.LeaveAt == null)
+ .Where(m => m.JoinedAt != null && m.LeaveAt == null)
.Include(m => m.ChatRoom)
.Select(m => m.ChatRoom)
.ToListAsync();
@@ -166,7 +165,7 @@ public class ChatRoomController(
public class ChatRoomRequest
{
- [Required] [MaxLength(1024)] public string? Name { get; set; }
+ [Required][MaxLength(1024)] public string? Name { get; set; }
[MaxLength(4096)] public string? Description { get; set; }
[MaxLength(32)] public string? PictureId { get; set; }
[MaxLength(32)] public string? BackgroundId { get; set; }
@@ -475,6 +474,7 @@ public class ChatRoomController(
var member = await db.ChatMembers
.Where(m => m.AccountId == Guid.Parse(currentUser.Id) && m.ChatRoomId == roomId)
+ .Where(m => m.JoinedAt != null && m.LeaveAt == null)
.FirstOrDefaultAsync();
if (member == null)
@@ -496,13 +496,14 @@ public class ChatRoomController(
{
if (currentUser is null) return Unauthorized();
var member = await db.ChatMembers
- .FirstOrDefaultAsync(m => m.ChatRoomId == roomId && m.AccountId == Guid.Parse(currentUser.Id));
+ .Where(m => m.ChatRoomId == roomId && m.AccountId == Guid.Parse(currentUser.Id) && m.JoinedAt != null && m.LeaveAt == null)
+ .FirstOrDefaultAsync();
if (member is null) return StatusCode(403, "You need to be a member to see online count of private chat room.");
}
var members = await db.ChatMembers
.Where(m => m.ChatRoomId == roomId)
- .Where(m => m.LeaveAt == null)
+ .Where(m => m.JoinedAt != null && m.LeaveAt == null)
.Select(m => m.AccountId)
.ToListAsync();
@@ -530,13 +531,14 @@ public class ChatRoomController(
{
if (currentUser is null) return Unauthorized();
var member = await db.ChatMembers
- .FirstOrDefaultAsync(m => m.ChatRoomId == roomId && m.AccountId == Guid.Parse(currentUser.Id));
+ .Where(m => m.ChatRoomId == roomId && m.AccountId == Guid.Parse(currentUser.Id) && m.JoinedAt != null && m.LeaveAt == null)
+ .FirstOrDefaultAsync();
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)
- .Where(m => m.LeaveAt == null);
+ .Where(m => m.JoinedAt != null && m.LeaveAt == null);
if (withStatus)
{
@@ -633,6 +635,7 @@ public class ChatRoomController(
var chatMember = await db.ChatMembers
.Where(m => m.AccountId == accountId)
.Where(m => m.ChatRoomId == roomId)
+ .Where(m => m.JoinedAt != null && m.LeaveAt == null)
.FirstOrDefaultAsync();
if (chatMember is null) return StatusCode(403, "You are not even a member of the targeted chat room.");
if (chatMember.Role < ChatMemberRole.Moderator)
@@ -645,7 +648,7 @@ public class ChatRoomController(
var hasExistingMember = await db.ChatMembers
.Where(m => m.AccountId == request.RelatedUserId)
.Where(m => m.ChatRoomId == roomId)
- .Where(m => m.LeaveAt == null)
+ .Where(m => m.JoinedAt != null && m.LeaveAt == null)
.AnyAsync();
if (hasExistingMember)
return BadRequest("This user has been joined the chat cannot be invited again.");
@@ -775,7 +778,7 @@ public class ChatRoomController(
var accountId = Guid.Parse(currentUser.Id);
var targetMember = await db.ChatMembers
- .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId)
+ .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null)
.FirstOrDefaultAsync();
if (targetMember is null) return BadRequest("You have not joined this chat room.");
if (request.NotifyLevel is not null)
@@ -816,7 +819,7 @@ public class ChatRoomController(
else
{
var targetMember = await db.ChatMembers
- .Where(m => m.AccountId == memberId && m.ChatRoomId == roomId)
+ .Where(m => m.AccountId == memberId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null)
.FirstOrDefaultAsync();
if (targetMember is null) return NotFound();
@@ -884,7 +887,7 @@ public class ChatRoomController(
// Find the target member
var member = await db.ChatMembers
- .Where(m => m.AccountId == memberId && m.ChatRoomId == roomId)
+ .Where(m => m.AccountId == memberId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null)
.FirstOrDefaultAsync();
if (member is null) return NotFound();
@@ -929,7 +932,17 @@ public class ChatRoomController(
var existingMember = await db.ChatMembers
.FirstOrDefaultAsync(m => m.AccountId == Guid.Parse(currentUser.Id) && m.ChatRoomId == roomId);
if (existingMember != null)
+ {
+ if (existingMember.LeaveAt == null)
+ {
+ existingMember.LeaveAt = null;
+ db.Update(existingMember);
+ await db.SaveChangesAsync();
+ _ = crs.PurgeRoomMembersCache(roomId);
+ return Ok(existingMember);
+ }
return BadRequest("You are already a member of this chat room.");
+ }
var newMember = new SnChatMember
{
@@ -962,6 +975,7 @@ public class ChatRoomController(
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
var member = await db.ChatMembers
+ .Where(m => m.JoinedAt != null && m.LeaveAt == null)
.Where(m => m.AccountId == Guid.Parse(currentUser.Id))
.Where(m => m.ChatRoomId == roomId)
.FirstOrDefaultAsync();
@@ -981,6 +995,7 @@ public class ChatRoomController(
}
member.LeaveAt = Instant.FromDateTimeUtc(DateTime.UtcNow);
+ db.Update(member);
await db.SaveChangesAsync();
await crs.PurgeRoomMembersCache(roomId);
@@ -1000,7 +1015,7 @@ public class ChatRoomController(
{
var account = await accounts.GetAccountAsync(new GetAccountRequest { Id = member.AccountId.ToString() });
CultureService.SetCultureInfo(account);
-
+
string title = localizer["ChatInviteTitle"];
string body = member.ChatRoom.Type == ChatRoomType.DirectMessage
diff --git a/DysonNetwork.Sphere/Chat/ChatRoomService.cs b/DysonNetwork.Sphere/Chat/ChatRoomService.cs
index 8bf644e..af51e1f 100644
--- a/DysonNetwork.Sphere/Chat/ChatRoomService.cs
+++ b/DysonNetwork.Sphere/Chat/ChatRoomService.cs
@@ -45,7 +45,7 @@ public class ChatRoomService(
if (member is not null) return member;
member = await db.ChatMembers
- .Where(m => m.AccountId == accountId && m.ChatRoomId == chatRoomId)
+ .Where(m => m.AccountId == accountId && m.ChatRoomId == chatRoomId && m.JoinedAt != null && m.LeaveAt == null)
.Include(m => m.ChatRoom)
.FirstOrDefaultAsync();
@@ -95,7 +95,7 @@ public class ChatRoomService(
? await db.ChatMembers
.Where(m => directRoomsId.Contains(m.ChatRoomId))
.Where(m => m.AccountId != userId)
- .Where(m => m.LeaveAt == null)
+ .Where(m => m.JoinedAt != null && m.LeaveAt == null)
.ToListAsync()
: [];
members = await LoadMemberAccounts(members);
@@ -121,7 +121,7 @@ public class ChatRoomService(
if (room.Type != ChatRoomType.DirectMessage) return room;
var members = await db.ChatMembers
.Where(m => m.ChatRoomId == room.Id && m.AccountId != userId)
- .Where(m => m.LeaveAt == null)
+ .Where(m => m.JoinedAt != null && m.LeaveAt == null)
.ToListAsync();
if (members.Count <= 0) return room;
@@ -139,7 +139,8 @@ public class ChatRoomService(
var maxRequiredRole = requiredRoles.Max();
var member = await db.ChatMembers
- .FirstOrDefaultAsync(m => m.ChatRoomId == roomId && m.AccountId == accountId);
+ .Where(m => m.ChatRoomId == roomId && m.AccountId == accountId && m.JoinedAt != null && m.LeaveAt == null)
+ .FirstOrDefaultAsync();
return member?.Role >= maxRequiredRole;
}
diff --git a/DysonNetwork.Sphere/Chat/ChatService.cs b/DysonNetwork.Sphere/Chat/ChatService.cs
index 7561c3c..7ea9571 100644
--- a/DysonNetwork.Sphere/Chat/ChatService.cs
+++ b/DysonNetwork.Sphere/Chat/ChatService.cs
@@ -441,7 +441,7 @@ public partial class ChatService(
public async Task ReadChatRoomAsync(Guid roomId, Guid userId)
{
var sender = await db.ChatMembers
- .Where(m => m.AccountId == userId && m.ChatRoomId == roomId)
+ .Where(m => m.AccountId == userId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null)
.FirstOrDefaultAsync();
if (sender is null) throw new ArgumentException("User is not a member of the chat room.");
@@ -452,7 +452,7 @@ public partial class ChatService(
public async Task CountUnreadMessage(Guid userId, Guid chatRoomId)
{
var sender = await db.ChatMembers
- .Where(m => m.AccountId == userId && m.ChatRoomId == chatRoomId)
+ .Where(m => m.AccountId == userId && m.ChatRoomId == chatRoomId && m.JoinedAt != null && m.LeaveAt == null)
.Select(m => new { m.LastReadAt })
.FirstOrDefaultAsync();
if (sender?.LastReadAt is null) return 0;
diff --git a/DysonNetwork.Sphere/Chat/RealtimeCallController.cs b/DysonNetwork.Sphere/Chat/RealtimeCallController.cs
index e8d44ef..960fc07 100644
--- a/DysonNetwork.Sphere/Chat/RealtimeCallController.cs
+++ b/DysonNetwork.Sphere/Chat/RealtimeCallController.cs
@@ -52,7 +52,7 @@ public class RealtimeCallController(
var accountId = Guid.Parse(currentUser.Id);
var member = await db.ChatMembers
- .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId)
+ .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null)
.FirstOrDefaultAsync();
if (member == null || member.Role < ChatMemberRole.Member)
@@ -78,7 +78,7 @@ public class RealtimeCallController(
// 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 == accountId && m.ChatRoomId == roomId)
+ .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null)
.FirstOrDefaultAsync();
if (member == null || member.Role < ChatMemberRole.Member)
@@ -151,7 +151,7 @@ public class RealtimeCallController(
var accountId = Guid.Parse(currentUser.Id);
var member = await db.ChatMembers
- .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId)
+ .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null)
.Include(m => m.ChatRoom)
.FirstOrDefaultAsync();
if (member == null || member.Role < ChatMemberRole.Member)
@@ -171,7 +171,7 @@ public class RealtimeCallController(
var accountId = Guid.Parse(currentUser.Id);
var member = await db.ChatMembers
- .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId)
+ .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null)
.FirstOrDefaultAsync();
if (member == null || member.Role < ChatMemberRole.Member)
return StatusCode(403, "You need to be a normal member to end a call.");
@@ -256,4 +256,4 @@ public class CallParticipant
/// When the participant joined the call
///
public DateTime JoinedAt { get; set; }
-}
\ No newline at end of file
+}
diff --git a/DysonNetwork.Sphere/Realm/RealmController.cs b/DysonNetwork.Sphere/Realm/RealmController.cs
index 642167a..f376717 100644
--- a/DysonNetwork.Sphere/Realm/RealmController.cs
+++ b/DysonNetwork.Sphere/Realm/RealmController.cs
@@ -105,10 +105,10 @@ public class RealmController(
var hasExistingMember = await db.RealmMembers
.Where(m => m.AccountId == Guid.Parse(relatedUser.Id))
.Where(m => m.RealmId == realm.Id)
- .Where(m => m.LeaveAt == null)
+ .Where(m => m.JoinedAt != null && m.LeaveAt == null)
.AnyAsync();
if (hasExistingMember)
- return BadRequest("This user has been joined the realm or leave cannot be invited again.");
+ return BadRequest("This user already in the realm cannot be invited again.");
var member = new SnRealmMember
{
@@ -232,7 +232,7 @@ public class RealmController(
var query = db.RealmMembers
.Where(m => m.RealmId == realm.Id)
- .Where(m => m.LeaveAt == null);
+ .Where(m => m.JoinedAt != null && m.LeaveAt == null);
if (withStatus)
{
@@ -289,6 +289,7 @@ public class RealmController(
var member = await db.RealmMembers
.Where(m => m.AccountId == accountId)
.Where(m => m.Realm.Slug == slug)
+ .Where(m => m.JoinedAt != null && m.LeaveAt == null)
.FirstOrDefaultAsync();
if (member is null) return NotFound();
@@ -305,7 +306,7 @@ public class RealmController(
var member = await db.RealmMembers
.Where(m => m.AccountId == accountId)
.Where(m => m.Realm.Slug == slug)
- .Where(m => m.JoinedAt != null)
+ .Where(m => m.JoinedAt != null && m.LeaveAt == null)
.FirstOrDefaultAsync();
if (member is null) return NotFound();
@@ -444,7 +445,7 @@ public class RealmController(
var accountId = Guid.Parse(currentUser.Id);
var member = await db.RealmMembers
- .Where(m => m.AccountId == accountId && m.RealmId == realm.Id && m.JoinedAt != null)
+ .Where(m => m.AccountId == accountId && m.RealmId == realm.Id && m.JoinedAt != null && m.LeaveAt == null)
.FirstOrDefaultAsync();
if (member is null || member.Role < RealmMemberRole.Moderator)
return StatusCode(403, "You do not have permission to update this realm.");
@@ -555,7 +556,7 @@ public class RealmController(
return StatusCode(403, "Only community realms can be joined without invitation.");
var existingMember = await db.RealmMembers
- .Where(m => m.AccountId == Guid.Parse(currentUser.Id) && m.RealmId == realm.Id)
+ .Where(m => m.AccountId == Guid.Parse(currentUser.Id) && m.RealmId == realm.Id && m.JoinedAt != null && m.LeaveAt == null)
.FirstOrDefaultAsync();
if (existingMember is not null)
return BadRequest("You are already a member of this realm.");
@@ -600,7 +601,7 @@ public class RealmController(
if (realm is null) return NotFound();
var member = await db.RealmMembers
- .Where(m => m.AccountId == memberId && m.RealmId == realm.Id)
+ .Where(m => m.AccountId == memberId && m.RealmId == realm.Id && m.JoinedAt != null && m.LeaveAt == null)
.FirstOrDefaultAsync();
if (member is null) return NotFound();
@@ -640,7 +641,7 @@ public class RealmController(
if (realm is null) return NotFound();
var member = await db.RealmMembers
- .Where(m => m.AccountId == memberId && m.RealmId == realm.Id)
+ .Where(m => m.AccountId == memberId && m.RealmId == realm.Id && m.JoinedAt != null && m.LeaveAt == null)
.FirstOrDefaultAsync();
if (member is null) return NotFound();
diff --git a/DysonNetwork.Sphere/Realm/RealmService.cs b/DysonNetwork.Sphere/Realm/RealmService.cs
index 5aab438..495fa16 100644
--- a/DysonNetwork.Sphere/Realm/RealmService.cs
+++ b/DysonNetwork.Sphere/Realm/RealmService.cs
@@ -30,6 +30,7 @@ public class RealmService(
var realms = await db.RealmMembers
.Include(m => m.Realm)
.Where(m => m.AccountId == accountId)
+ .Where(m => m.JoinedAt != null && m.LeaveAt == null)
.Select(m => m.Realm!.Id)
.ToListAsync();
@@ -67,7 +68,8 @@ public class RealmService(
var maxRequiredRole = requiredRoles.Max();
var member = await db.RealmMembers
- .FirstOrDefaultAsync(m => m.RealmId == realmId && m.AccountId == accountId);
+ .Where(m => m.RealmId == realmId && m.AccountId == accountId && m.JoinedAt != null && m.LeaveAt == null)
+ .FirstOrDefaultAsync();
return member?.Role >= maxRequiredRole;
}
@@ -90,4 +92,4 @@ public class RealmService(
return m;
}).ToList();
}
-}
\ No newline at end of file
+}
diff --git a/DysonNetwork.Sphere/Startup/BroadcastEventHandler.cs b/DysonNetwork.Sphere/Startup/BroadcastEventHandler.cs
index 729e66e..644f470 100644
--- a/DysonNetwork.Sphere/Startup/BroadcastEventHandler.cs
+++ b/DysonNetwork.Sphere/Startup/BroadcastEventHandler.cs
@@ -360,7 +360,7 @@ public class BroadcastEventHandler(
// Get user's joined chat rooms
var userRooms = await db.ChatMembers
- .Where(m => m.AccountId == evt.AccountId && m.LeaveAt == null)
+ .Where(m => m.AccountId == evt.AccountId && m.JoinedAt != null && m.LeaveAt == null)
.Select(m => m.ChatRoomId)
.ToListAsync(cancellationToken: stoppingToken);