Compare commits
No commits in common. "d1fb0b9b55b804fe10791af7e34ce4841474ddd0" and "1baa3109bc7fccf0202b1fec75ac215515ab7b2f" have entirely different histories.
d1fb0b9b55
...
1baa3109bc
@ -6,7 +6,7 @@ namespace DysonNetwork.Sphere.Account;
|
|||||||
|
|
||||||
public class RelationshipService(AppDatabase db, ICacheService cache)
|
public class RelationshipService(AppDatabase db, ICacheService cache)
|
||||||
{
|
{
|
||||||
private const string UserFriendsCacheKeyPrefix = "accounts:friends:";
|
private const string UserFriendsCacheKeyPrefix = "user:friends:";
|
||||||
|
|
||||||
public async Task<bool> HasExistingRelationship(Guid accountId, Guid relatedId)
|
public async Task<bool> HasExistingRelationship(Guid accountId, Guid relatedId)
|
||||||
{
|
{
|
||||||
|
@ -22,11 +22,8 @@ public class ActivityController(
|
|||||||
/// Besides, when users are logged in, it will also mix the other kinds of data and who're plying to them.
|
/// Besides, when users are logged in, it will also mix the other kinds of data and who're plying to them.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult<List<Activity>>> ListActivities(
|
public async Task<ActionResult<List<Activity>>> ListActivities([FromQuery] string? cursor,
|
||||||
[FromQuery] string? cursor,
|
[FromQuery] int take = 20)
|
||||||
[FromQuery] string? filter,
|
|
||||||
[FromQuery] int take = 20
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
Instant? cursorTimestamp = null;
|
Instant? cursorTimestamp = null;
|
||||||
if (!string.IsNullOrEmpty(cursor))
|
if (!string.IsNullOrEmpty(cursor))
|
||||||
@ -45,6 +42,6 @@ public class ActivityController(
|
|||||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||||
return currentUserValue is not Account.Account currentUser
|
return currentUserValue is not Account.Account currentUser
|
||||||
? Ok(await acts.GetActivitiesForAnyone(take, cursorTimestamp))
|
? Ok(await acts.GetActivitiesForAnyone(take, cursorTimestamp))
|
||||||
: Ok(await acts.GetActivities(take, cursorTimestamp, currentUser, filter));
|
: Ok(await acts.GetActivities(take, cursorTimestamp, currentUser));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -42,57 +42,26 @@ public class ActivityService(AppDatabase db, PublisherService pub, RelationshipS
|
|||||||
return activities;
|
return activities;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Activity>> GetActivities(
|
public async Task<List<Activity>> GetActivities(int take, Instant? cursor, Account.Account currentUser)
|
||||||
int take,
|
|
||||||
Instant? cursor,
|
|
||||||
Account.Account currentUser,
|
|
||||||
string? filter = null
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
var activities = new List<Activity>();
|
var activities = new List<Activity>();
|
||||||
var userFriends = await rels.ListAccountFriends(currentUser);
|
var userFriends = await rels.ListAccountFriends(currentUser);
|
||||||
var userPublishers = await pub.GetUserPublishers(currentUser.Id);
|
var userPublishers = await pub.GetUserPublishers(currentUser.Id);
|
||||||
|
|
||||||
// Get publishers based on filter
|
var publishersId = userPublishers.Select(e => e.Id).ToList();
|
||||||
List<Publisher.Publisher>? filteredPublishers = null;
|
|
||||||
switch (filter)
|
// Crunching data
|
||||||
{
|
var posts = await db.Posts
|
||||||
case "subscriptions":
|
|
||||||
filteredPublishers = await pub.GetSubscribedPublishers(currentUser.Id);
|
|
||||||
break;
|
|
||||||
case "friends":
|
|
||||||
{
|
|
||||||
filteredPublishers = (await pub.GetUserPublishersBatch(userFriends))
|
|
||||||
.SelectMany(x => x.Value)
|
|
||||||
.DistinctBy(x => x.Id)
|
|
||||||
.ToList();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var filteredPublishersId = filteredPublishers?.Select(e => e.Id).ToList();
|
|
||||||
|
|
||||||
// Build the query based on the filter
|
|
||||||
var postsQuery = db.Posts
|
|
||||||
.Include(e => e.RepliedPost)
|
.Include(e => e.RepliedPost)
|
||||||
.Include(e => e.ForwardedPost)
|
.Include(e => e.ForwardedPost)
|
||||||
.Include(e => e.Categories)
|
.Include(e => e.Categories)
|
||||||
.Include(e => e.Tags)
|
.Include(e => e.Tags)
|
||||||
|
.Where(e => e.RepliedPostId == null || publishersId.Contains(e.RepliedPost!.PublisherId))
|
||||||
.Where(p => cursor == null || p.PublishedAt < cursor)
|
.Where(p => cursor == null || p.PublishedAt < cursor)
|
||||||
.OrderByDescending(p => p.PublishedAt)
|
.OrderByDescending(p => p.PublishedAt)
|
||||||
.AsQueryable();
|
.FilterWithVisibility(currentUser, userFriends, userPublishers, isListing: true)
|
||||||
|
|
||||||
if (filteredPublishersId is not null)
|
|
||||||
postsQuery = postsQuery.Where(p => filteredPublishersId.Contains(p.PublisherId));
|
|
||||||
|
|
||||||
// Complete the query with visibility filtering and execute
|
|
||||||
var posts = await postsQuery
|
|
||||||
.FilterWithVisibility(currentUser, userFriends, filter is null ? userPublishers : [], isListing: true)
|
|
||||||
.Take(take)
|
.Take(take)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
posts = await ps.LoadPostInfo(posts, currentUser, true);
|
posts = await ps.LoadPostInfo(posts, currentUser, true);
|
||||||
|
|
||||||
var postsId = posts.Select(e => e.Id).ToList();
|
var postsId = posts.Select(e => e.Id).ToList();
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
using System.Text.RegularExpressions;
|
|
||||||
using DysonNetwork.Sphere.Account;
|
using DysonNetwork.Sphere.Account;
|
||||||
using DysonNetwork.Sphere.Chat.Realtime;
|
using DysonNetwork.Sphere.Chat.Realtime;
|
||||||
using DysonNetwork.Sphere.Connection;
|
using DysonNetwork.Sphere.Connection;
|
||||||
@ -8,7 +7,7 @@ using NodaTime;
|
|||||||
|
|
||||||
namespace DysonNetwork.Sphere.Chat;
|
namespace DysonNetwork.Sphere.Chat;
|
||||||
|
|
||||||
public partial class ChatService(
|
public class ChatService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
FileReferenceService fileRefService,
|
FileReferenceService fileRefService,
|
||||||
IServiceScopeFactory scopeFactory,
|
IServiceScopeFactory scopeFactory,
|
||||||
@ -18,132 +17,6 @@ public partial class ChatService(
|
|||||||
{
|
{
|
||||||
private const string ChatFileUsageIdentifier = "chat";
|
private const string ChatFileUsageIdentifier = "chat";
|
||||||
|
|
||||||
[GeneratedRegex(@"https?://[-A-Za-z0-9+&@#/%?=~_|!:,.;]*[-A-Za-z0-9+&@#/%=~_|]")]
|
|
||||||
private static partial Regex GetLinkRegex();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Process link previews for a message in the background
|
|
||||||
/// This method is designed to be called from a background task
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message">The message to process link previews for</param>
|
|
||||||
private async Task ProcessMessageLinkPreviewAsync(Message message)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Create a new scope for database operations
|
|
||||||
using var scope = scopeFactory.CreateScope();
|
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
|
||||||
var webReader = scope.ServiceProvider.GetRequiredService<Connection.WebReader.WebReaderService>();
|
|
||||||
var newChat = scope.ServiceProvider.GetRequiredService<ChatService>();
|
|
||||||
|
|
||||||
// Preview the links in the message
|
|
||||||
var updatedMessage = await PreviewMessageLinkAsync(message, webReader);
|
|
||||||
|
|
||||||
// If embeds were added, update the message in the database
|
|
||||||
if (updatedMessage.Meta != null &&
|
|
||||||
updatedMessage.Meta.TryGetValue("embeds", out var embeds) &&
|
|
||||||
embeds is List<Dictionary<string, object>> { Count: > 0 } embedsList)
|
|
||||||
{
|
|
||||||
// Get a fresh copy of the message from the database
|
|
||||||
var dbMessage = await dbContext.ChatMessages
|
|
||||||
.Where(m => m.Id == message.Id)
|
|
||||||
.Include(m => m.Sender)
|
|
||||||
.Include(m => m.ChatRoom)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (dbMessage != null)
|
|
||||||
{
|
|
||||||
// Update the meta field with the new embeds
|
|
||||||
dbMessage.Meta ??= new Dictionary<string, object>();
|
|
||||||
dbMessage.Meta["embeds"] = embedsList;
|
|
||||||
|
|
||||||
// Save changes to the database
|
|
||||||
dbContext.Update(dbMessage);
|
|
||||||
await dbContext.SaveChangesAsync();
|
|
||||||
|
|
||||||
logger.LogDebug($"Updated message {message.Id} with {embedsList.Count} link previews");
|
|
||||||
|
|
||||||
// Notify clients of the updated message
|
|
||||||
await newChat.DeliverMessageAsync(
|
|
||||||
dbMessage,
|
|
||||||
dbMessage.Sender,
|
|
||||||
dbMessage.ChatRoom,
|
|
||||||
WebSocketPacketType.MessageUpdate
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Log errors but don't rethrow - this is a background task
|
|
||||||
logger.LogError($"Error processing link previews for message {message.Id}: {ex.Message} {ex.StackTrace}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Processes a message to find and preview links in its content
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message">The message to process</param>
|
|
||||||
/// <param name="webReader">The web reader service</param>
|
|
||||||
/// <returns>The message with link previews added to its meta data</returns>
|
|
||||||
public async Task<Message> PreviewMessageLinkAsync(Message message,
|
|
||||||
Connection.WebReader.WebReaderService? webReader = null)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(message.Content))
|
|
||||||
return message;
|
|
||||||
|
|
||||||
// Find all URLs in the content
|
|
||||||
var matches = GetLinkRegex().Matches(message.Content);
|
|
||||||
|
|
||||||
if (matches.Count == 0)
|
|
||||||
return message;
|
|
||||||
|
|
||||||
// Initialize meta dictionary if null
|
|
||||||
message.Meta ??= new Dictionary<string, object>();
|
|
||||||
|
|
||||||
// Initialize the embeds' array if it doesn't exist
|
|
||||||
if (!message.Meta.TryGetValue("embeds", out var existingEmbeds) ||
|
|
||||||
existingEmbeds is not List<Dictionary<string, object>>)
|
|
||||||
{
|
|
||||||
message.Meta["embeds"] = new List<Dictionary<string, object>>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var embeds = (List<Dictionary<string, object>>)message.Meta["embeds"];
|
|
||||||
webReader ??= scopeFactory.CreateScope().ServiceProvider
|
|
||||||
.GetRequiredService<Connection.WebReader.WebReaderService>();
|
|
||||||
|
|
||||||
// Process up to 3 links to avoid excessive processing
|
|
||||||
var processedLinks = 0;
|
|
||||||
foreach (Match match in matches)
|
|
||||||
{
|
|
||||||
if (processedLinks >= 3)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var url = match.Value;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Check if this URL is already in the embed list
|
|
||||||
var urlAlreadyEmbedded = embeds.Any(e =>
|
|
||||||
e.TryGetValue("Url", out var originalUrl) && (string)originalUrl == url);
|
|
||||||
if (urlAlreadyEmbedded)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Preview the link
|
|
||||||
var linkEmbed = await webReader.GetLinkPreviewAsync(url);
|
|
||||||
embeds.Add(linkEmbed.ToDictionary());
|
|
||||||
processedLinks++;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message.Meta["embeds"] = embeds;
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Message> SendMessageAsync(Message message, ChatMember sender, ChatRoom room)
|
public async Task<Message> SendMessageAsync(Message message, ChatMember sender, ChatRoom room)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(message.Nonce)) message.Nonce = Guid.NewGuid().ToString();
|
if (string.IsNullOrWhiteSpace(message.Nonce)) message.Nonce = Guid.NewGuid().ToString();
|
||||||
@ -184,9 +57,6 @@ public partial class ChatService(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process link preview in the background to avoid delaying message sending
|
|
||||||
_ = Task.Run(async () => await ProcessMessageLinkPreviewAsync(message));
|
|
||||||
|
|
||||||
message.Sender = sender;
|
message.Sender = sender;
|
||||||
message.ChatRoom = room;
|
message.ChatRoom = room;
|
||||||
return message;
|
return message;
|
||||||
@ -423,7 +293,7 @@ public partial class ChatService(
|
|||||||
{
|
{
|
||||||
Type = "call.ended",
|
Type = "call.ended",
|
||||||
ChatRoomId = call.RoomId,
|
ChatRoomId = call.RoomId,
|
||||||
SenderId = call.SenderId,
|
SenderId = sender.Id,
|
||||||
Meta = new Dictionary<string, object>
|
Meta = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
{ "call_id", call.Id },
|
{ "call_id", call.Id },
|
||||||
@ -519,10 +389,6 @@ public partial class ChatService(
|
|||||||
db.Update(message);
|
db.Update(message);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
// Process link preview in the background if content was updated
|
|
||||||
if (content is not null)
|
|
||||||
_ = Task.Run(async () => await ProcessMessageLinkPreviewAsync(message));
|
|
||||||
|
|
||||||
_ = DeliverMessageAsync(
|
_ = DeliverMessageAsync(
|
||||||
message,
|
message,
|
||||||
message.Sender,
|
message.Sender,
|
||||||
|
@ -342,7 +342,7 @@ public partial class PostService(
|
|||||||
// If embeds were added, update the post in the database
|
// If embeds were added, update the post in the database
|
||||||
if (updatedPost.Meta != null &&
|
if (updatedPost.Meta != null &&
|
||||||
updatedPost.Meta.TryGetValue("embeds", out var embeds) &&
|
updatedPost.Meta.TryGetValue("embeds", out var embeds) &&
|
||||||
embeds is List<Dictionary<string, object>> { Count: > 0 } embedsList)
|
embeds is List<EmbeddableBase> { Count: > 0 } embedsList)
|
||||||
{
|
{
|
||||||
// Get a fresh copy of the post from the database
|
// Get a fresh copy of the post from the database
|
||||||
var dbPost = await dbContext.Posts.FindAsync(post.Id);
|
var dbPost = await dbContext.Posts.FindAsync(post.Id);
|
||||||
@ -659,5 +659,6 @@ public static class PostQueryExtensions
|
|||||||
.Where(e => e.Visibility != PostVisibility.Friends ||
|
.Where(e => e.Visibility != PostVisibility.Friends ||
|
||||||
(e.Publisher.AccountId != null && userFriends.Contains(e.Publisher.AccountId.Value)) ||
|
(e.Publisher.AccountId != null && userFriends.Contains(e.Publisher.AccountId.Value)) ||
|
||||||
publishersId.Contains(e.PublisherId));
|
publishersId.Contains(e.PublisherId));
|
||||||
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,7 +9,6 @@ namespace DysonNetwork.Sphere.Publisher;
|
|||||||
public class PublisherService(AppDatabase db, FileReferenceService fileRefService, ICacheService cache)
|
public class PublisherService(AppDatabase db, FileReferenceService fileRefService, ICacheService cache)
|
||||||
{
|
{
|
||||||
private const string UserPublishersCacheKey = "accounts:{0}:publishers";
|
private const string UserPublishersCacheKey = "accounts:{0}:publishers";
|
||||||
private const string UserPublishersBatchCacheKey = "accounts:batch:{0}:publishers";
|
|
||||||
|
|
||||||
public async Task<List<Publisher>> GetUserPublishers(Guid userId)
|
public async Task<List<Publisher>> GetUserPublishers(Guid userId)
|
||||||
{
|
{
|
||||||
@ -35,86 +34,6 @@ public class PublisherService(AppDatabase db, FileReferenceService fileRefServic
|
|||||||
return publishers;
|
return publishers;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Dictionary<Guid, List<Publisher>>> GetUserPublishersBatch(List<Guid> userIds)
|
|
||||||
{
|
|
||||||
var result = new Dictionary<Guid, List<Publisher>>();
|
|
||||||
var missingIds = new List<Guid>();
|
|
||||||
|
|
||||||
// Try to get publishers from cache for each user
|
|
||||||
foreach (var userId in userIds)
|
|
||||||
{
|
|
||||||
var cacheKey = string.Format(UserPublishersCacheKey, userId);
|
|
||||||
var publishers = await cache.GetAsync<List<Publisher>>(cacheKey);
|
|
||||||
if (publishers != null)
|
|
||||||
result[userId] = publishers;
|
|
||||||
else
|
|
||||||
missingIds.Add(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (missingIds.Count <= 0) return result;
|
|
||||||
{
|
|
||||||
// Fetch missing data from database
|
|
||||||
var publisherMembers = await db.PublisherMembers
|
|
||||||
.Where(p => missingIds.Contains(p.AccountId))
|
|
||||||
.Select(p => new { p.AccountId, p.PublisherId })
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var publisherIds = publisherMembers.Select(p => p.PublisherId).Distinct().ToList();
|
|
||||||
var publishers = await db.Publishers
|
|
||||||
.Where(p => publisherIds.Contains(p.Id))
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
// Group publishers by user id
|
|
||||||
foreach (var userId in missingIds)
|
|
||||||
{
|
|
||||||
var userPublisherIds = publisherMembers
|
|
||||||
.Where(p => p.AccountId == userId)
|
|
||||||
.Select(p => p.PublisherId)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var userPublishers = publishers
|
|
||||||
.Where(p => userPublisherIds.Contains(p.Id))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
result[userId] = userPublishers;
|
|
||||||
|
|
||||||
// Cache individual results
|
|
||||||
var cacheKey = string.Format(UserPublishersCacheKey, userId);
|
|
||||||
await cache.SetAsync(cacheKey, userPublishers, TimeSpan.FromMinutes(5));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private const string SubscribedPublishersCacheKey = "accounts:{0}:subscribed-publishers";
|
|
||||||
|
|
||||||
public async Task<List<Publisher>> GetSubscribedPublishers(Guid userId)
|
|
||||||
{
|
|
||||||
var cacheKey = string.Format(SubscribedPublishersCacheKey, userId);
|
|
||||||
|
|
||||||
// Try to get publishers from the cache first
|
|
||||||
var publishers = await cache.GetAsync<List<Publisher>>(cacheKey);
|
|
||||||
if (publishers is not null)
|
|
||||||
return publishers;
|
|
||||||
|
|
||||||
// If not in cache, fetch from a database
|
|
||||||
var publishersId = await db.PublisherSubscriptions
|
|
||||||
.Where(p => p.AccountId == userId)
|
|
||||||
.Select(p => p.PublisherId)
|
|
||||||
.ToListAsync();
|
|
||||||
publishers = await db.Publishers
|
|
||||||
.Where(p => publishersId.Contains(p.Id))
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
// Store in a cache for 5 minutes
|
|
||||||
await cache.SetAsync(cacheKey, publishers, TimeSpan.FromMinutes(5));
|
|
||||||
|
|
||||||
return publishers;
|
|
||||||
}
|
|
||||||
|
|
||||||
private const string PublisherMembersCacheKey = "publishers:{0}:members";
|
private const string PublisherMembersCacheKey = "publishers:{0}:members";
|
||||||
|
|
||||||
public async Task<List<PublisherMember>> GetPublisherMembers(Guid publisherId)
|
public async Task<List<PublisherMember>> GetPublisherMembers(Guid publisherId)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user