diff --git a/DysonNetwork.Sphere/Account/AccountService.cs b/DysonNetwork.Sphere/Account/AccountService.cs index ba8ef5f..912d944 100644 --- a/DysonNetwork.Sphere/Account/AccountService.cs +++ b/DysonNetwork.Sphere/Account/AccountService.cs @@ -10,6 +10,8 @@ public class AccountService(AppDatabase db, PermissionService pm, IMemoryCache c { public async Task PurgeAccountCache(Account account) { + cache.Remove($"dyn_user_friends_{account.Id}"); + var sessions = await db.AuthSessions.Where(e => e.Account.Id == account.Id).Select(e => e.Id) .ToListAsync(); foreach (var session in sessions) @@ -31,144 +33,4 @@ public class AccountService(AppDatabase db, PermissionService pm, IMemoryCache c return null; } - - 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 - { - Account = sender, - AccountId = sender.Id, - Related = target, - RelatedId = target.Id, - Status = status - }; - - db.AccountRelationships.Add(relationship); - await db.SaveChangesAsync(); - await ApplyRelationshipPermissions(relationship); - - 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 - { - Account = sender, - AccountId = sender.Id, - Related = target, - 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 - { - Account = relationship.Related, - AccountId = relationship.RelatedId, - Related = relationship.Account, - RelatedId = relationship.AccountId, - Status = status - }; - db.AccountRelationships.Add(relationshipBackward); - - await db.SaveChangesAsync(); - - await Task.WhenAll( - ApplyRelationshipPermissions(relationship), - ApplyRelationshipPermissions(relationshipBackward) - ); - - 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); - return relationship; - } - - 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); - } } \ No newline at end of file diff --git a/DysonNetwork.Sphere/Account/RelationshipController.cs b/DysonNetwork.Sphere/Account/RelationshipController.cs index 204bd8e..bfc1603 100644 --- a/DysonNetwork.Sphere/Account/RelationshipController.cs +++ b/DysonNetwork.Sphere/Account/RelationshipController.cs @@ -7,7 +7,7 @@ namespace DysonNetwork.Sphere.Account; [ApiController] [Route("/relationships")] -public class RelationshipController(AppDatabase db, AccountService accounts) : ControllerBase +public class RelationshipController(AppDatabase db, RelationshipService rels) : ControllerBase { [HttpGet] [Authorize] @@ -48,7 +48,7 @@ public class RelationshipController(AppDatabase db, AccountService accounts) : C try { - var relationship = await accounts.CreateRelationship( + var relationship = await rels.CreateRelationship( currentUser, relatedUser, request.Status ); return relationship; diff --git a/DysonNetwork.Sphere/Account/RelationshipService.cs b/DysonNetwork.Sphere/Account/RelationshipService.cs new file mode 100644 index 0000000..4ed4dc6 --- /dev/null +++ b/DysonNetwork.Sphere/Account/RelationshipService.cs @@ -0,0 +1,171 @@ +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 + { + Account = sender, + AccountId = sender.Id, + Related = target, + 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 + { + Account = sender, + AccountId = sender.Id, + Related = target, + 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 + { + Account = relationship.Related, + AccountId = relationship.RelatedId, + Related = relationship.Account, + 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); + } +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Activity/Activity.cs b/DysonNetwork.Sphere/Activity/Activity.cs new file mode 100644 index 0000000..4e90e80 --- /dev/null +++ b/DysonNetwork.Sphere/Activity/Activity.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace DysonNetwork.Sphere.Activity; + +public enum ActivityVisibility +{ + Public, + Friends, +} + +public class Activity : ModelBase +{ + public Guid Id { get; set; } = Guid.NewGuid(); + [MaxLength(1024)] public string Type { get; set; } = null!; + [MaxLength(4096)] public string ResourceIdentifier { get; set; } = null!; + public ActivityVisibility Visibility { get; set; } = ActivityVisibility.Public; + public Dictionary Meta = new(); + + public long AccountId { get; set; } + public Account.Account Account { get; set; } = null!; +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Activity/ActivityController.cs b/DysonNetwork.Sphere/Activity/ActivityController.cs new file mode 100644 index 0000000..b772ac0 --- /dev/null +++ b/DysonNetwork.Sphere/Activity/ActivityController.cs @@ -0,0 +1,33 @@ +using DysonNetwork.Sphere.Account; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace DysonNetwork.Sphere.Activity; + +[ApiController] +[Route("/activities")] +public class ActivityController(AppDatabase db, ActivityService act, RelationshipService rels) : ControllerBase +{ + [HttpGet] + public async Task>> ListActivities([FromQuery] int offset, [FromQuery] int take = 20) + { + HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); + var currentUser = currentUserValue as Account.Account; + var userFriends = await rels.ListAccountFriends(currentUser!); + + var totalCount = await db.Activities + .FilterWithVisibility(currentUser, userFriends) + .CountAsync(); + var posts = await db.Activities + .Include(e => e.Account) + .FilterWithVisibility(currentUser, userFriends) + .OrderByDescending(e => e.CreatedAt) + .Skip(offset) + .Take(take) + .ToListAsync(); + + Response.Headers["X-Total"] = totalCount.ToString(); + + return Ok(posts); + } +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Activity/ActivityService.cs b/DysonNetwork.Sphere/Activity/ActivityService.cs new file mode 100644 index 0000000..4258ca1 --- /dev/null +++ b/DysonNetwork.Sphere/Activity/ActivityService.cs @@ -0,0 +1,54 @@ +using DysonNetwork.Sphere.Post; +using NodaTime; + +namespace DysonNetwork.Sphere.Activity; + +public class ActivityService(AppDatabase db) +{ + public async Task CreateActivity( + Account.Account user, + string type, + string identifier, + ActivityVisibility visibility = ActivityVisibility.Public + ) + { + var activity = new Activity + { + Type = type, + ResourceIdentifier = identifier, + Visibility = visibility, + AccountId = user.Id, + }; + + db.Activities.Add(activity); + await db.SaveChangesAsync(); + + return activity; + } + + public async Task CreateNewPostActivity(Account.Account user, Post.Post post) + { + if (post.Visibility is PostVisibility.Unlisted or PostVisibility.Private) return; + + var identifier = $"posts/{post.Id}"; + await CreateActivity(user, "posts.new", identifier, + post.Visibility == PostVisibility.Friends ? ActivityVisibility.Friends : ActivityVisibility.Public); + } +} + +public static class ActivityQueryExtensions +{ + public static IQueryable FilterWithVisibility(this IQueryable source, + Account.Account? currentUser, List userFriends) + { + var now = Instant.FromDateTimeUtc(DateTime.UtcNow); + + if (currentUser is null) + return source.Where(e => e.Visibility == ActivityVisibility.Public); + + return source + .Where(e => e.Visibility != ActivityVisibility.Friends || + userFriends.Contains(e.AccountId) || + e.AccountId == currentUser.Id); + } +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/AppDatabase.cs b/DysonNetwork.Sphere/AppDatabase.cs index ebcf170..6ece080 100644 --- a/DysonNetwork.Sphere/AppDatabase.cs +++ b/DysonNetwork.Sphere/AppDatabase.cs @@ -37,6 +37,8 @@ public class AppDatabase( public DbSet AuthChallenges { get; set; } public DbSet Files { get; set; } + + public DbSet Activities { get; set; } public DbSet Publishers { get; set; } public DbSet PublisherMembers { get; set; } diff --git a/DysonNetwork.Sphere/Connection/WebSocketController.cs b/DysonNetwork.Sphere/Connection/WebSocketController.cs index 7172627..6db5ac6 100644 --- a/DysonNetwork.Sphere/Connection/WebSocketController.cs +++ b/DysonNetwork.Sphere/Connection/WebSocketController.cs @@ -8,13 +8,8 @@ namespace DysonNetwork.Sphere.Connection; [ApiController] [Route("/ws")] -public class WebSocketController(ILogger logger) : ControllerBase +public class WebSocketController(WebSocketService ws, ILogger logger) : ControllerBase { - private static readonly ConcurrentDictionary< - (long AccountId, string DeviceId), - (WebSocket Socket, CancellationTokenSource Cts) - > ActiveConnections = new(); - [Route("/ws")] [Authorize] [SwaggerIgnore] @@ -42,7 +37,7 @@ public class WebSocketController(ILogger logger) : ControllerB var cts = new CancellationTokenSource(); var connectionKey = (accountId, deviceId); - if (!ActiveConnections.TryAdd(connectionKey, (webSocket, cts))) + if (!ws.TryAdd(connectionKey, webSocket, cts)) { await webSocket.CloseAsync( WebSocketCloseStatus.InternalServerError, @@ -57,7 +52,7 @@ public class WebSocketController(ILogger logger) : ControllerB try { - await _ConnectionEventLoop(webSocket, connectionKey, cts.Token); + await _ConnectionEventLoop(connectionKey, webSocket, cts.Token); } catch (Exception ex) { @@ -65,16 +60,15 @@ public class WebSocketController(ILogger logger) : ControllerB } finally { - ActiveConnections.TryRemove(connectionKey, out _); - cts.Dispose(); + ws.Disconnect(connectionKey); logger.LogInformation( $"Connection disconnected with user @{currentUser.Name}#{currentUser.Id} and device #{deviceId}"); } } - private static async Task _ConnectionEventLoop( - WebSocket webSocket, + private async Task _ConnectionEventLoop( (long AccountId, string DeviceId) connectionKey, + WebSocket webSocket, CancellationToken cancellationToken ) { @@ -93,41 +87,17 @@ public class WebSocketController(ILogger logger) : ControllerB ); } - await webSocket.CloseAsync( - receiveResult.CloseStatus.Value, - receiveResult.CloseStatusDescription, - cancellationToken - ); + // TODO handle values } catch (OperationCanceledException) { - // Connection was canceled, close it gracefully if ( webSocket.State != WebSocketState.Closed && webSocket.State != WebSocketState.Aborted ) { - await webSocket.CloseAsync( - WebSocketCloseStatus.NormalClosure, - "Connection closed by server", - CancellationToken.None - ); + ws.Disconnect(connectionKey); } } } - - // This method will be used later to send messages to specific connections - public static async Task SendMessageAsync(long accountId, string deviceId, string message) - { - if (ActiveConnections.TryGetValue((accountId, deviceId), out var connection)) - { - var buffer = System.Text.Encoding.UTF8.GetBytes(message); - await connection.Socket.SendAsync( - new ArraySegment(buffer, 0, buffer.Length), - WebSocketMessageType.Text, - true, - connection.Cts.Token - ); - } - } } \ No newline at end of file diff --git a/DysonNetwork.Sphere/Connection/WebSocketService.cs b/DysonNetwork.Sphere/Connection/WebSocketService.cs new file mode 100644 index 0000000..61e548d --- /dev/null +++ b/DysonNetwork.Sphere/Connection/WebSocketService.cs @@ -0,0 +1,35 @@ +using System.Collections.Concurrent; +using System.Net.WebSockets; + +namespace DysonNetwork.Sphere.Connection; + +public class WebSocketService +{ + public static readonly ConcurrentDictionary< + (long AccountId, string DeviceId), + (WebSocket Socket, CancellationTokenSource Cts) + > ActiveConnections = new(); + + public bool TryAdd( + (long AccountId, string DeviceId) key, + WebSocket socket, + CancellationTokenSource cts + ) + { + if (ActiveConnections.TryGetValue(key, out _)) + Disconnect(key, "Just connected somewhere else with the same identifier."); // Disconnect the previous one using the same identifier + return ActiveConnections.TryAdd(key, (socket, cts)); + } + + public void Disconnect((long AccountId, string DeviceId) key, string? reason = null) + { + if (!ActiveConnections.TryGetValue(key, out var data)) return; + data.Socket.CloseAsync( + WebSocketCloseStatus.NormalClosure, + reason ?? "Server just decided to disconnect.", + CancellationToken.None + ); + data.Cts.Cancel(); + ActiveConnections.TryRemove(key, out _); + } +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Post/PostController.cs b/DysonNetwork.Sphere/Post/PostController.cs index 31e95e8..2b33737 100644 --- a/DysonNetwork.Sphere/Post/PostController.cs +++ b/DysonNetwork.Sphere/Post/PostController.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using Casbin; +using DysonNetwork.Sphere.Account; using DysonNetwork.Sphere.Permission; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -9,16 +10,17 @@ namespace DysonNetwork.Sphere.Post; [ApiController] [Route("/posts")] -public class PostController(AppDatabase db, PostService ps) : ControllerBase +public class PostController(AppDatabase db, PostService ps, RelationshipService rels) : ControllerBase { [HttpGet] public async Task>> ListPosts([FromQuery] int offset = 0, [FromQuery] int take = 20) { HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); var currentUser = currentUserValue as Account.Account; + var userFriends = await rels.ListAccountFriends(currentUser!); var totalCount = await db.Posts - .FilterWithVisibility(currentUser, isListing: true) + .FilterWithVisibility(currentUser, userFriends, isListing: true) .CountAsync(); var posts = await db.Posts .Include(e => e.Publisher) @@ -30,7 +32,7 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase .Include(e => e.Categories) .Include(e => e.Tags) .Where(e => e.RepliedPostId == null) - .FilterWithVisibility(currentUser, isListing: true) + .FilterWithVisibility(currentUser, userFriends, isListing: true) .OrderByDescending(e => e.PublishedAt ?? e.CreatedAt) .Skip(offset) .Take(take) @@ -46,6 +48,7 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase { HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); var currentUser = currentUserValue as Account.Account; + var userFriends = await rels.ListAccountFriends(currentUser!); var post = await db.Posts .Where(e => e.Id == id) @@ -58,7 +61,7 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase .Include(e => e.Tags) .Include(e => e.Categories) .Include(e => e.Attachments) - .FilterWithVisibility(currentUser) + .FilterWithVisibility(currentUser, userFriends) .FirstOrDefaultAsync(); if (post is null) return NotFound(); @@ -71,6 +74,7 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase { HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); var currentUser = currentUserValue as Account.Account; + var userFriends = await rels.ListAccountFriends(currentUser!); var post = await db.Posts .Where(e => e.Id == id) @@ -79,7 +83,7 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase var totalCount = await db.Posts .Where(e => e.RepliedPostId == post.Id) - .FilterWithVisibility(currentUser, isListing: true) + .FilterWithVisibility(currentUser, userFriends, isListing: true) .CountAsync(); var posts = await db.Posts .Where(e => e.RepliedPostId == id) @@ -91,7 +95,7 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase .Include(e => e.Attachments) .Include(e => e.Categories) .Include(e => e.Tags) - .FilterWithVisibility(currentUser, isListing: true) + .FilterWithVisibility(currentUser, userFriends, isListing: true) .OrderByDescending(e => e.PublishedAt ?? e.CreatedAt) .Skip(offset) .Take(take) @@ -179,6 +183,7 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase try { post = await ps.PostAsync( + currentUser, post, attachments: request.Attachments, tags: request.Tags, diff --git a/DysonNetwork.Sphere/Post/PostService.cs b/DysonNetwork.Sphere/Post/PostService.cs index 58a12e1..35ebc1b 100644 --- a/DysonNetwork.Sphere/Post/PostService.cs +++ b/DysonNetwork.Sphere/Post/PostService.cs @@ -1,12 +1,14 @@ +using DysonNetwork.Sphere.Activity; using DysonNetwork.Sphere.Storage; using Microsoft.EntityFrameworkCore; using NodaTime; namespace DysonNetwork.Sphere.Post; -public class PostService(AppDatabase db, FileService fs) +public class PostService(AppDatabase db, FileService fs, ActivityService act) { public async Task PostAsync( + Account.Account user, Post post, List? attachments = null, List? tags = null, @@ -66,6 +68,8 @@ public class PostService(AppDatabase db, FileService fs) await db.SaveChangesAsync(); await fs.MarkUsageRangeAsync(post.Attachments, 1); + await act.CreateNewPostActivity(user, post); + return post; } @@ -153,7 +157,7 @@ public class PostService(AppDatabase db, FileService fs) public static class PostQueryExtensions { public static IQueryable FilterWithVisibility(this IQueryable source, Account.Account? currentUser, - bool isListing = false) + List userFriends, bool isListing = false) { var now = Instant.FromDateTimeUtc(DateTime.UtcNow); @@ -172,6 +176,9 @@ public static class PostQueryExtensions return source .Where(e => e.PublishedAt != null && now >= e.PublishedAt && e.Publisher.AccountId == currentUser.Id) - .Where(e => e.Visibility != PostVisibility.Private || e.Publisher.AccountId == currentUser.Id); + .Where(e => e.Visibility != PostVisibility.Private || e.Publisher.AccountId == currentUser.Id) + .Where(e => e.Visibility != PostVisibility.Friends || + (e.Publisher.AccountId != null && userFriends.Contains(e.Publisher.AccountId.Value)) || + e.Publisher.AccountId == currentUser.Id); } } \ No newline at end of file diff --git a/DysonNetwork.Sphere/Program.cs b/DysonNetwork.Sphere/Program.cs index 540421f..a1de52c 100644 --- a/DysonNetwork.Sphere/Program.cs +++ b/DysonNetwork.Sphere/Program.cs @@ -7,7 +7,9 @@ using Casbin; using Casbin.Persist.Adapter.EFCore; using DysonNetwork.Sphere; using DysonNetwork.Sphere.Account; +using DysonNetwork.Sphere.Activity; using DysonNetwork.Sphere.Auth; +using DysonNetwork.Sphere.Connection; using DysonNetwork.Sphere.Permission; using DysonNetwork.Sphere.Post; using DysonNetwork.Sphere.Storage; @@ -114,14 +116,17 @@ builder.Services.AddSwaggerGen(options => }); builder.Services.AddOpenApi(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); // Timed task