diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs b/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs index a6cad8be..f1dacb26 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs @@ -30,8 +30,12 @@ public class NoteRenderer( ? await RenderAsync(note.Renote, user, accounts, mentions, attachments, likeCounts, likedNotes, --recurse) : null; var text = note.Text; - if (quote != null && text != null && !text.EndsWith(quote.Url) && !text.EndsWith(quote.Uri)) - text += $"\n\nRE: {quote.Url}"; //TODO: render as inline quote + if (note is { Renote: not null, IsQuote: true } && text != null) + { + var quoteUri = note.Renote?.Url ?? note.Renote?.Uri ?? note.Renote?.GetPublicUriOrNull(config.Value); + if (quoteUri != null) + text += $"\n\nRE: {quoteUri}"; //TODO: render as inline quote + } var likeCount = likeCounts?.GetValueOrDefault(note.Id, 0) ?? await db.NoteLikes.CountAsync(p => p.Note == note); var liked = likedNotes?.Contains(note.Id) ?? await db.NoteLikes.AnyAsync(p => p.Note == note && p.User == user); diff --git a/Iceshrimp.Backend/Controllers/Mastodon/StatusController.cs b/Iceshrimp.Backend/Controllers/Mastodon/StatusController.cs index 3ea9b9c3..43d77d3b 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/StatusController.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/StatusController.cs @@ -3,6 +3,7 @@ using Iceshrimp.Backend.Controllers.Mastodon.Attributes; using Iceshrimp.Backend.Controllers.Mastodon.Renderers; using Iceshrimp.Backend.Controllers.Mastodon.Schemas; using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; +using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Extensions; using Iceshrimp.Backend.Core.Middleware; @@ -12,6 +13,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; namespace Iceshrimp.Backend.Controllers.Mastodon; @@ -25,7 +27,8 @@ public class StatusController( DatabaseContext db, NoteRenderer noteRenderer, NoteService noteSvc, - IDistributedCache cache + IDistributedCache cache, + IOptions config ) : ControllerBase { [HttpGet("{id}")] @@ -67,6 +70,7 @@ public class StatusController( .RenderAllForMastodonAsync(noteRenderer, user); var descendants = await db.NoteDescendants(id, maxDepth, maxDescendants) + .Where(p => !p.IsQuote || p.RenoteId != id) .IncludeCommonProperties() .EnsureVisibleFor(user) .PrecomputeVisibilities(user) @@ -166,8 +170,19 @@ public class StatusController( ? await db.DriveFiles.Where(p => request.MediaIds.Contains(p.Id)).ToListAsync() : null; - var note = await noteSvc.CreateNoteAsync(user, visibility, request.Text, request.Cw, reply, - attachments: attachments); + var lastToken = request.Text?.Split(' ').LastOrDefault(); + var quoteUri = lastToken?.StartsWith("https://") ?? false ? lastToken : null; + var quote = lastToken?.StartsWith($"https://{config.Value.WebDomain}/notes/") ?? false + ? await db.Notes.IncludeCommonProperties() + .FirstOrDefaultAsync(p => p.Id == + lastToken.Substring($"https://{config.Value.WebDomain}/notes/".Length)) + : await db.Notes.IncludeCommonProperties() + .FirstOrDefaultAsync(p => p.Uri == quoteUri || p.Url == quoteUri); + + if (quote != null && quoteUri != null && request.Text != null) + request.Text = request.Text[..(request.Text.Length - quoteUri.Length - 1)]; + + var note = await noteSvc.CreateNoteAsync(user, visibility, request.Text, request.Cw, reply, quote, attachments); if (idempotencyKey != null) await cache.SetAsync($"idempotency:{idempotencyKey}", note.Id, TimeSpan.FromHours(24)); diff --git a/Iceshrimp.Backend/Core/Database/Tables/Note.cs b/Iceshrimp.Backend/Core/Database/Tables/Note.cs index 530ce44b..947546f2 100644 --- a/Iceshrimp.Backend/Core/Database/Tables/Note.cs +++ b/Iceshrimp.Backend/Core/Database/Tables/Note.cs @@ -238,8 +238,11 @@ public class Note : IEntity [InverseProperty(nameof(InverseReply))] public virtual Note? Reply { get; set; } - [NotMapped] [Projectable] public bool IsPureRenote => RenoteId != null && !IsQuote; - [NotMapped] [Projectable] public bool IsQuote => RenoteId != null && (Text != null || HasPoll || FileIds.Count > 0); + [NotMapped] [Projectable] public bool IsPureRenote => (RenoteId != null || Renote != null) && !IsQuote; + + [NotMapped] + [Projectable] + public bool IsQuote => (RenoteId != null || Renote != null) && (Text != null || HasPoll || FileIds.Count > 0); [ForeignKey("UserId")] [InverseProperty(nameof(Tables.User.Notes))] @@ -285,6 +288,10 @@ public class Note : IEntity ? $"https://{config.WebDomain}/notes/{Id}" : throw new Exception("Cannot access PublicUri for remote note"); + public string? GetPublicUriOrNull(Config.InstanceSection config) => UserHost == null + ? $"https://{config.WebDomain}/notes/{Id}" + : null; + public class MentionedUser { [J("uri")] public required string Uri { get; set; } diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/NoteRenderer.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/NoteRenderer.cs index a15f3bde..a1064e66 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/NoteRenderer.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/NoteRenderer.cs @@ -80,6 +80,9 @@ public class NoteRenderer(IOptions config, MfmConverter .ToListAsync() : null; + var quoteUri = note.IsQuote ? note.Renote?.Uri ?? note.Renote?.GetPublicUriOrNull(config.Value) : null; + var text = quoteUri != null ? note.Text + $"\n\nRE: {quoteUri}" : note.Text; + return new ASNote { Id = id, @@ -93,13 +96,15 @@ public class NoteRenderer(IOptions config, MfmConverter To = to, Tags = tags, Attachments = attachments, - Content = note.Text != null - ? await mfmConverter.ToHtmlAsync(note.Text, mentions, note.UserHost) - : null, - Summary = note.Cw, - Source = note.Text != null - ? new ASNoteSource { Content = note.Text, MediaType = "text/x.misskeymarkdown" } - : null + Content = text != null ? await mfmConverter.ToHtmlAsync(text, mentions, note.UserHost) : null, + Summary = note.Cw, + Source = + text != null + ? new ASNoteSource { Content = text, MediaType = "text/x.misskeymarkdown" } + : null, + MkQuote = quoteUri, + QuoteUri = quoteUri, + QuoteUrl = quoteUri }; } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Services/NotificationService.cs b/Iceshrimp.Backend/Core/Services/NotificationService.cs index e1d5815b..0e89ac6b 100644 --- a/Iceshrimp.Backend/Core/Services/NotificationService.cs +++ b/Iceshrimp.Backend/Core/Services/NotificationService.cs @@ -184,6 +184,7 @@ public class NotificationService( public async Task GenerateRenoteNotification(Note note) { if (note.Renote is not { UserHost: null }) return; + if (note.RenoteUserId == note.UserId) return; if (!note.VisibilityIsPublicOrHome && !await db.Notes.AnyAsync(p => p.Id == note.Id && p.IsVisibleFor(note.Renote.User))) return;