diff --git a/DysonNetwork.Sphere/Chat/ChatService.cs b/DysonNetwork.Sphere/Chat/ChatService.cs
index f893d11..db0fcad 100644
--- a/DysonNetwork.Sphere/Chat/ChatService.cs
+++ b/DysonNetwork.Sphere/Chat/ChatService.cs
@@ -1,3 +1,4 @@
+using System.Text.RegularExpressions;
using DysonNetwork.Sphere.Account;
using DysonNetwork.Sphere.Chat.Realtime;
using DysonNetwork.Sphere.Connection;
@@ -7,7 +8,7 @@ using NodaTime;
namespace DysonNetwork.Sphere.Chat;
-public class ChatService(
+public partial class ChatService(
AppDatabase db,
FileReferenceService fileRefService,
IServiceScopeFactory scopeFactory,
@@ -17,6 +18,131 @@ public class ChatService(
{
private const string ChatFileUsageIdentifier = "chat";
+ [GeneratedRegex(@"https?://[-A-Za-z0-9+&@#/%?=~_|!:,.;]*[-A-Za-z0-9+&@#/%=~_|]")]
+ private static partial Regex GetLinkRegex();
+
+ ///
+ /// Process link previews for a message in the background
+ /// This method is designed to be called from a background task
+ ///
+ /// The message to process link previews for
+ private async Task ProcessMessageLinkPreviewAsync(Message message)
+ {
+ try
+ {
+ // Create a new scope for database operations
+ using var scope = scopeFactory.CreateScope();
+ var dbContext = scope.ServiceProvider.GetRequiredService();
+ var webReader = scope.ServiceProvider.GetRequiredService();
+
+ // 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> { 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();
+ 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 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}");
+ }
+ }
+
+ ///
+ /// Processes a message to find and preview links in its content
+ ///
+ /// The message to process
+ /// The web reader service
+ /// The message with link previews added to its meta data
+ public async Task 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();
+
+ // Initialize the embeds' array if it doesn't exist
+ if (!message.Meta.TryGetValue("embeds", out var existingEmbeds) ||
+ existingEmbeds is not List>)
+ {
+ message.Meta["embeds"] = new List>();
+ }
+
+ var embeds = (List>)message.Meta["embeds"];
+ webReader ??= scopeFactory.CreateScope().ServiceProvider
+ .GetRequiredService();
+
+ // 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 SendMessageAsync(Message message, ChatMember sender, ChatRoom room)
{
if (string.IsNullOrWhiteSpace(message.Nonce)) message.Nonce = Guid.NewGuid().ToString();
@@ -57,6 +183,9 @@ public class ChatService(
}
});
+ // Process link preview in the background to avoid delaying message sending
+ _ = Task.Run(async () => await ProcessMessageLinkPreviewAsync(message));
+
message.Sender = sender;
message.ChatRoom = room;
return message;
@@ -293,7 +422,7 @@ public class ChatService(
{
Type = "call.ended",
ChatRoomId = call.RoomId,
- SenderId = sender.Id,
+ SenderId = call.SenderId,
Meta = new Dictionary
{
{ "call_id", call.Id },
@@ -389,6 +518,10 @@ public class ChatService(
db.Update(message);
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(
message,
message.Sender,