✨ Activity-based browsing
This commit is contained in:
parent
42b5129aa4
commit
bf64afd849
@ -10,6 +10,8 @@ public class AccountService(AppDatabase db, PermissionService pm, IMemoryCache c
|
|||||||
{
|
{
|
||||||
public async Task PurgeAccountCache(Account account)
|
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)
|
var sessions = await db.AuthSessions.Where(e => e.Account.Id == account.Id).Select(e => e.Id)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
foreach (var session in sessions)
|
foreach (var session in sessions)
|
||||||
@ -31,144 +33,4 @@ public class AccountService(AppDatabase db, PermissionService pm, IMemoryCache c
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
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<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
|
|
||||||
{
|
|
||||||
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<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
|
|
||||||
{
|
|
||||||
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<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);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -7,7 +7,7 @@ namespace DysonNetwork.Sphere.Account;
|
|||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/relationships")]
|
[Route("/relationships")]
|
||||||
public class RelationshipController(AppDatabase db, AccountService accounts) : ControllerBase
|
public class RelationshipController(AppDatabase db, RelationshipService rels) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
@ -48,7 +48,7 @@ public class RelationshipController(AppDatabase db, AccountService accounts) : C
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var relationship = await accounts.CreateRelationship(
|
var relationship = await rels.CreateRelationship(
|
||||||
currentUser, relatedUser, request.Status
|
currentUser, relatedUser, request.Status
|
||||||
);
|
);
|
||||||
return relationship;
|
return relationship;
|
||||||
|
171
DysonNetwork.Sphere/Account/RelationshipService.cs
Normal file
171
DysonNetwork.Sphere/Account/RelationshipService.cs
Normal file
@ -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<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
|
||||||
|
{
|
||||||
|
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<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
|
||||||
|
{
|
||||||
|
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<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
|
||||||
|
{
|
||||||
|
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<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);
|
||||||
|
}
|
||||||
|
}
|
21
DysonNetwork.Sphere/Activity/Activity.cs
Normal file
21
DysonNetwork.Sphere/Activity/Activity.cs
Normal file
@ -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<string, object> Meta = new();
|
||||||
|
|
||||||
|
public long AccountId { get; set; }
|
||||||
|
public Account.Account Account { get; set; } = null!;
|
||||||
|
}
|
33
DysonNetwork.Sphere/Activity/ActivityController.cs
Normal file
33
DysonNetwork.Sphere/Activity/ActivityController.cs
Normal file
@ -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<ActionResult<List<Activity>>> 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);
|
||||||
|
}
|
||||||
|
}
|
54
DysonNetwork.Sphere/Activity/ActivityService.cs
Normal file
54
DysonNetwork.Sphere/Activity/ActivityService.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using DysonNetwork.Sphere.Post;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Sphere.Activity;
|
||||||
|
|
||||||
|
public class ActivityService(AppDatabase db)
|
||||||
|
{
|
||||||
|
public async Task<Activity> 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<Activity> FilterWithVisibility(this IQueryable<Activity> source,
|
||||||
|
Account.Account? currentUser, List<long> 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);
|
||||||
|
}
|
||||||
|
}
|
@ -37,6 +37,8 @@ public class AppDatabase(
|
|||||||
public DbSet<Auth.Challenge> AuthChallenges { get; set; }
|
public DbSet<Auth.Challenge> AuthChallenges { get; set; }
|
||||||
|
|
||||||
public DbSet<Storage.CloudFile> Files { get; set; }
|
public DbSet<Storage.CloudFile> Files { get; set; }
|
||||||
|
|
||||||
|
public DbSet<Activity.Activity> Activities { get; set; }
|
||||||
|
|
||||||
public DbSet<Post.Publisher> Publishers { get; set; }
|
public DbSet<Post.Publisher> Publishers { get; set; }
|
||||||
public DbSet<Post.PublisherMember> PublisherMembers { get; set; }
|
public DbSet<Post.PublisherMember> PublisherMembers { get; set; }
|
||||||
|
@ -8,13 +8,8 @@ namespace DysonNetwork.Sphere.Connection;
|
|||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/ws")]
|
[Route("/ws")]
|
||||||
public class WebSocketController(ILogger<WebSocketContext> logger) : ControllerBase
|
public class WebSocketController(WebSocketService ws, ILogger<WebSocketContext> logger) : ControllerBase
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<
|
|
||||||
(long AccountId, string DeviceId),
|
|
||||||
(WebSocket Socket, CancellationTokenSource Cts)
|
|
||||||
> ActiveConnections = new();
|
|
||||||
|
|
||||||
[Route("/ws")]
|
[Route("/ws")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[SwaggerIgnore]
|
[SwaggerIgnore]
|
||||||
@ -42,7 +37,7 @@ public class WebSocketController(ILogger<WebSocketContext> logger) : ControllerB
|
|||||||
var cts = new CancellationTokenSource();
|
var cts = new CancellationTokenSource();
|
||||||
var connectionKey = (accountId, deviceId);
|
var connectionKey = (accountId, deviceId);
|
||||||
|
|
||||||
if (!ActiveConnections.TryAdd(connectionKey, (webSocket, cts)))
|
if (!ws.TryAdd(connectionKey, webSocket, cts))
|
||||||
{
|
{
|
||||||
await webSocket.CloseAsync(
|
await webSocket.CloseAsync(
|
||||||
WebSocketCloseStatus.InternalServerError,
|
WebSocketCloseStatus.InternalServerError,
|
||||||
@ -57,7 +52,7 @@ public class WebSocketController(ILogger<WebSocketContext> logger) : ControllerB
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _ConnectionEventLoop(webSocket, connectionKey, cts.Token);
|
await _ConnectionEventLoop(connectionKey, webSocket, cts.Token);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -65,16 +60,15 @@ public class WebSocketController(ILogger<WebSocketContext> logger) : ControllerB
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
ActiveConnections.TryRemove(connectionKey, out _);
|
ws.Disconnect(connectionKey);
|
||||||
cts.Dispose();
|
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
$"Connection disconnected with user @{currentUser.Name}#{currentUser.Id} and device #{deviceId}");
|
$"Connection disconnected with user @{currentUser.Name}#{currentUser.Id} and device #{deviceId}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task _ConnectionEventLoop(
|
private async Task _ConnectionEventLoop(
|
||||||
WebSocket webSocket,
|
|
||||||
(long AccountId, string DeviceId) connectionKey,
|
(long AccountId, string DeviceId) connectionKey,
|
||||||
|
WebSocket webSocket,
|
||||||
CancellationToken cancellationToken
|
CancellationToken cancellationToken
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@ -93,41 +87,17 @@ public class WebSocketController(ILogger<WebSocketContext> logger) : ControllerB
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await webSocket.CloseAsync(
|
// TODO handle values
|
||||||
receiveResult.CloseStatus.Value,
|
|
||||||
receiveResult.CloseStatusDescription,
|
|
||||||
cancellationToken
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// Connection was canceled, close it gracefully
|
|
||||||
if (
|
if (
|
||||||
webSocket.State != WebSocketState.Closed
|
webSocket.State != WebSocketState.Closed
|
||||||
&& webSocket.State != WebSocketState.Aborted
|
&& webSocket.State != WebSocketState.Aborted
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
await webSocket.CloseAsync(
|
ws.Disconnect(connectionKey);
|
||||||
WebSocketCloseStatus.NormalClosure,
|
|
||||||
"Connection closed by server",
|
|
||||||
CancellationToken.None
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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<byte>(buffer, 0, buffer.Length),
|
|
||||||
WebSocketMessageType.Text,
|
|
||||||
true,
|
|
||||||
connection.Cts.Token
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
35
DysonNetwork.Sphere/Connection/WebSocketService.cs
Normal file
35
DysonNetwork.Sphere/Connection/WebSocketService.cs
Normal file
@ -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 _);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Casbin;
|
using Casbin;
|
||||||
|
using DysonNetwork.Sphere.Account;
|
||||||
using DysonNetwork.Sphere.Permission;
|
using DysonNetwork.Sphere.Permission;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -9,16 +10,17 @@ namespace DysonNetwork.Sphere.Post;
|
|||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("/posts")]
|
[Route("/posts")]
|
||||||
public class PostController(AppDatabase db, PostService ps) : ControllerBase
|
public class PostController(AppDatabase db, PostService ps, RelationshipService rels) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult<List<Post>>> ListPosts([FromQuery] int offset = 0, [FromQuery] int take = 20)
|
public async Task<ActionResult<List<Post>>> ListPosts([FromQuery] int offset = 0, [FromQuery] int take = 20)
|
||||||
{
|
{
|
||||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||||
var currentUser = currentUserValue as Account.Account;
|
var currentUser = currentUserValue as Account.Account;
|
||||||
|
var userFriends = await rels.ListAccountFriends(currentUser!);
|
||||||
|
|
||||||
var totalCount = await db.Posts
|
var totalCount = await db.Posts
|
||||||
.FilterWithVisibility(currentUser, isListing: true)
|
.FilterWithVisibility(currentUser, userFriends, isListing: true)
|
||||||
.CountAsync();
|
.CountAsync();
|
||||||
var posts = await db.Posts
|
var posts = await db.Posts
|
||||||
.Include(e => e.Publisher)
|
.Include(e => e.Publisher)
|
||||||
@ -30,7 +32,7 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase
|
|||||||
.Include(e => e.Categories)
|
.Include(e => e.Categories)
|
||||||
.Include(e => e.Tags)
|
.Include(e => e.Tags)
|
||||||
.Where(e => e.RepliedPostId == null)
|
.Where(e => e.RepliedPostId == null)
|
||||||
.FilterWithVisibility(currentUser, isListing: true)
|
.FilterWithVisibility(currentUser, userFriends, isListing: true)
|
||||||
.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt)
|
.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt)
|
||||||
.Skip(offset)
|
.Skip(offset)
|
||||||
.Take(take)
|
.Take(take)
|
||||||
@ -46,6 +48,7 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase
|
|||||||
{
|
{
|
||||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||||
var currentUser = currentUserValue as Account.Account;
|
var currentUser = currentUserValue as Account.Account;
|
||||||
|
var userFriends = await rels.ListAccountFriends(currentUser!);
|
||||||
|
|
||||||
var post = await db.Posts
|
var post = await db.Posts
|
||||||
.Where(e => e.Id == id)
|
.Where(e => e.Id == id)
|
||||||
@ -58,7 +61,7 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase
|
|||||||
.Include(e => e.Tags)
|
.Include(e => e.Tags)
|
||||||
.Include(e => e.Categories)
|
.Include(e => e.Categories)
|
||||||
.Include(e => e.Attachments)
|
.Include(e => e.Attachments)
|
||||||
.FilterWithVisibility(currentUser)
|
.FilterWithVisibility(currentUser, userFriends)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (post is null) return NotFound();
|
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);
|
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||||
var currentUser = currentUserValue as Account.Account;
|
var currentUser = currentUserValue as Account.Account;
|
||||||
|
var userFriends = await rels.ListAccountFriends(currentUser!);
|
||||||
|
|
||||||
var post = await db.Posts
|
var post = await db.Posts
|
||||||
.Where(e => e.Id == id)
|
.Where(e => e.Id == id)
|
||||||
@ -79,7 +83,7 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase
|
|||||||
|
|
||||||
var totalCount = await db.Posts
|
var totalCount = await db.Posts
|
||||||
.Where(e => e.RepliedPostId == post.Id)
|
.Where(e => e.RepliedPostId == post.Id)
|
||||||
.FilterWithVisibility(currentUser, isListing: true)
|
.FilterWithVisibility(currentUser, userFriends, isListing: true)
|
||||||
.CountAsync();
|
.CountAsync();
|
||||||
var posts = await db.Posts
|
var posts = await db.Posts
|
||||||
.Where(e => e.RepliedPostId == id)
|
.Where(e => e.RepliedPostId == id)
|
||||||
@ -91,7 +95,7 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase
|
|||||||
.Include(e => e.Attachments)
|
.Include(e => e.Attachments)
|
||||||
.Include(e => e.Categories)
|
.Include(e => e.Categories)
|
||||||
.Include(e => e.Tags)
|
.Include(e => e.Tags)
|
||||||
.FilterWithVisibility(currentUser, isListing: true)
|
.FilterWithVisibility(currentUser, userFriends, isListing: true)
|
||||||
.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt)
|
.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt)
|
||||||
.Skip(offset)
|
.Skip(offset)
|
||||||
.Take(take)
|
.Take(take)
|
||||||
@ -179,6 +183,7 @@ public class PostController(AppDatabase db, PostService ps) : ControllerBase
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
post = await ps.PostAsync(
|
post = await ps.PostAsync(
|
||||||
|
currentUser,
|
||||||
post,
|
post,
|
||||||
attachments: request.Attachments,
|
attachments: request.Attachments,
|
||||||
tags: request.Tags,
|
tags: request.Tags,
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
|
using DysonNetwork.Sphere.Activity;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Post;
|
namespace DysonNetwork.Sphere.Post;
|
||||||
|
|
||||||
public class PostService(AppDatabase db, FileService fs)
|
public class PostService(AppDatabase db, FileService fs, ActivityService act)
|
||||||
{
|
{
|
||||||
public async Task<Post> PostAsync(
|
public async Task<Post> PostAsync(
|
||||||
|
Account.Account user,
|
||||||
Post post,
|
Post post,
|
||||||
List<string>? attachments = null,
|
List<string>? attachments = null,
|
||||||
List<string>? tags = null,
|
List<string>? tags = null,
|
||||||
@ -66,6 +68,8 @@ public class PostService(AppDatabase db, FileService fs)
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
await fs.MarkUsageRangeAsync(post.Attachments, 1);
|
await fs.MarkUsageRangeAsync(post.Attachments, 1);
|
||||||
|
|
||||||
|
await act.CreateNewPostActivity(user, post);
|
||||||
|
|
||||||
return post;
|
return post;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,7 +157,7 @@ public class PostService(AppDatabase db, FileService fs)
|
|||||||
public static class PostQueryExtensions
|
public static class PostQueryExtensions
|
||||||
{
|
{
|
||||||
public static IQueryable<Post> FilterWithVisibility(this IQueryable<Post> source, Account.Account? currentUser,
|
public static IQueryable<Post> FilterWithVisibility(this IQueryable<Post> source, Account.Account? currentUser,
|
||||||
bool isListing = false)
|
List<long> userFriends, bool isListing = false)
|
||||||
{
|
{
|
||||||
var now = Instant.FromDateTimeUtc(DateTime.UtcNow);
|
var now = Instant.FromDateTimeUtc(DateTime.UtcNow);
|
||||||
|
|
||||||
@ -172,6 +176,9 @@ public static class PostQueryExtensions
|
|||||||
|
|
||||||
return source
|
return source
|
||||||
.Where(e => e.PublishedAt != null && now >= e.PublishedAt && e.Publisher.AccountId == currentUser.Id)
|
.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,7 +7,9 @@ using Casbin;
|
|||||||
using Casbin.Persist.Adapter.EFCore;
|
using Casbin.Persist.Adapter.EFCore;
|
||||||
using DysonNetwork.Sphere;
|
using DysonNetwork.Sphere;
|
||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Sphere.Account;
|
||||||
|
using DysonNetwork.Sphere.Activity;
|
||||||
using DysonNetwork.Sphere.Auth;
|
using DysonNetwork.Sphere.Auth;
|
||||||
|
using DysonNetwork.Sphere.Connection;
|
||||||
using DysonNetwork.Sphere.Permission;
|
using DysonNetwork.Sphere.Permission;
|
||||||
using DysonNetwork.Sphere.Post;
|
using DysonNetwork.Sphere.Post;
|
||||||
using DysonNetwork.Sphere.Storage;
|
using DysonNetwork.Sphere.Storage;
|
||||||
@ -114,14 +116,17 @@ builder.Services.AddSwaggerGen(options =>
|
|||||||
});
|
});
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
|
builder.Services.AddScoped<WebSocketService>();
|
||||||
builder.Services.AddScoped<EmailService>();
|
builder.Services.AddScoped<EmailService>();
|
||||||
builder.Services.AddScoped<PermissionService>();
|
builder.Services.AddScoped<PermissionService>();
|
||||||
builder.Services.AddScoped<AccountService>();
|
builder.Services.AddScoped<AccountService>();
|
||||||
|
builder.Services.AddScoped<RelationshipService>();
|
||||||
builder.Services.AddScoped<MagicSpellService>();
|
builder.Services.AddScoped<MagicSpellService>();
|
||||||
builder.Services.AddScoped<NotificationService>();
|
builder.Services.AddScoped<NotificationService>();
|
||||||
builder.Services.AddScoped<AuthService>();
|
builder.Services.AddScoped<AuthService>();
|
||||||
builder.Services.AddScoped<FileService>();
|
builder.Services.AddScoped<FileService>();
|
||||||
builder.Services.AddScoped<PublisherService>();
|
builder.Services.AddScoped<PublisherService>();
|
||||||
|
builder.Services.AddScoped<ActivityService>();
|
||||||
builder.Services.AddScoped<PostService>();
|
builder.Services.AddScoped<PostService>();
|
||||||
|
|
||||||
// Timed task
|
// Timed task
|
||||||
|
Loading…
x
Reference in New Issue
Block a user