using DysonNetwork.Sphere.Permission; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using NodaTime; namespace DysonNetwork.Sphere.Account; public class RelationshipService(AppDatabase db, PermissionService pm, IMemoryCache cache) { public async Task HasExistingRelationship(Account userA, Account userB) { var count = await db.AccountRelationships .Where(r => (r.AccountId == userA.Id && r.AccountId == userB.Id) || (r.AccountId == userB.Id && r.AccountId == userA.Id)) .CountAsync(); return count > 0; } public async Task GetRelationship( Account account, Account related, RelationshipStatus? status, bool ignoreExpired = false ) { var now = Instant.FromDateTimeUtc(DateTime.UtcNow); var queries = db.AccountRelationships .Where(r => r.AccountId == account.Id && r.AccountId == related.Id); if (ignoreExpired) queries = queries.Where(r => r.ExpiredAt > now); if (status is not null) queries = queries.Where(r => r.Status == status); var relationship = await queries.FirstOrDefaultAsync(); return relationship; } public async Task CreateRelationship(Account sender, Account target, RelationshipStatus status) { if (status == RelationshipStatus.Pending) throw new InvalidOperationException( "Cannot create relationship with pending status, use SendFriendRequest instead."); if (await HasExistingRelationship(sender, target)) throw new InvalidOperationException("Found existing relationship between you and target user."); var relationship = new Relationship { AccountId = sender.Id, RelatedId = target.Id, Status = status }; db.AccountRelationships.Add(relationship); await db.SaveChangesAsync(); await ApplyRelationshipPermissions(relationship); cache.Remove($"dyn_user_friends_{relationship.AccountId}"); cache.Remove($"dyn_user_friends_{relationship.RelatedId}"); return relationship; } public async Task SendFriendRequest(Account sender, Account target) { if (await HasExistingRelationship(sender, target)) throw new InvalidOperationException("Found existing relationship between you and target user."); var relationship = new Relationship { AccountId = sender.Id, RelatedId = target.Id, Status = RelationshipStatus.Pending, ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(7)) }; db.AccountRelationships.Add(relationship); await db.SaveChangesAsync(); return relationship; } public async Task AcceptFriendRelationship( Relationship relationship, RelationshipStatus status = RelationshipStatus.Friends ) { if (relationship.Status == RelationshipStatus.Pending) throw new ArgumentException("Cannot accept friend request by setting the new status to pending."); // Whatever the receiver decides to apply which status to the relationship, // the sender should always see the user as a friend since the sender ask for it relationship.Status = RelationshipStatus.Friends; relationship.ExpiredAt = null; db.Update(relationship); var relationshipBackward = new Relationship { AccountId = relationship.RelatedId, RelatedId = relationship.AccountId, Status = status }; db.AccountRelationships.Add(relationshipBackward); await db.SaveChangesAsync(); await Task.WhenAll( ApplyRelationshipPermissions(relationship), ApplyRelationshipPermissions(relationshipBackward) ); cache.Remove($"dyn_user_friends_{relationship.AccountId}"); cache.Remove($"dyn_user_friends_{relationship.RelatedId}"); return relationshipBackward; } public async Task UpdateRelationship(Account account, Account related, RelationshipStatus status) { var relationship = await GetRelationship(account, related, status); if (relationship is null) throw new ArgumentException("There is no relationship between you and the user."); if (relationship.Status == status) return relationship; relationship.Status = status; db.Update(relationship); await db.SaveChangesAsync(); await ApplyRelationshipPermissions(relationship); cache.Remove($"dyn_user_friends_{related.Id}"); return relationship; } public async Task> ListAccountFriends(Account account) { if (!cache.TryGetValue($"dyn_user_friends_{account.Id}", out List? friends)) { friends = await db.AccountRelationships .Where(r => r.RelatedId == account.Id) .Where(r => r.Status == RelationshipStatus.Friends) .Select(r => r.AccountId) .ToListAsync(); cache.Set($"dyn_user_friends_{account.Id}", friends, TimeSpan.FromHours(1)); } return friends ?? []; } private async Task ApplyRelationshipPermissions(Relationship relationship) { // Apply the relationship permissions to casbin enforcer // domain: the user // status is friends: all permissions are allowed by default, expect specially specified // status is blocked: all permissions are disallowed by default, expect specially specified // others: use the default permissions by design var domain = $"user:{relationship.AccountId.ToString()}"; var target = $"user:{relationship.RelatedId.ToString()}"; await pm.RemovePermissionNode(target, domain, "*"); bool? value = relationship.Status switch { RelationshipStatus.Friends => true, RelationshipStatus.Blocked => false, _ => null, }; if (value is null) return; await pm.AddPermissionNode(target, domain, "*", value); } }