Swarm/DysonNetwork.Sphere/Account/RelationshipService.cs

165 lines
6.2 KiB
C#

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<bool> 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<Relationship?> 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<Relationship> 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<Relationship> 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<Relationship> 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<Relationship> 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<List<long>> ListAccountFriends(Account account)
{
if (!cache.TryGetValue($"dyn_user_friends_{account.Id}", out List<long>? 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);
}
}