diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs b/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs index 70533f5b..22c66f3e 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs @@ -81,7 +81,7 @@ public class NoteRenderer( ? await GetAttachmentsAsync([note]) : [..data.Attachments.Where(p => note.FileIds.Contains(p.Id))]; - if (user == null && security.Value.PublicPreview == Enums.PublicPreview.RestrictedNoMedia) //TODO + if (user == null && security.Value.PublicPreview == Enums.PublicPreview.RestrictedNoMedia) attachments = []; var reactions = data?.Reactions == null @@ -103,9 +103,7 @@ public class NoteRenderer( note.Renote == null && ((note.RenoteId != null && recurse > 0) || note.RenoteUri != null); var sensitive = note.Cw != null || attachments.Any(p => p.Sensitive); - - // TODO: canDisplayInlineMedia oauth_token flag that marks all media as "other" so they end up as links - // (and doesn't remove the attachments) + var inlineMedia = attachments.Select(p => new MfmInlineMedia(p.Type switch { AttachmentType.Audio => MfmInlineMedia.MediaType.Audio, @@ -113,9 +111,10 @@ public class NoteRenderer( AttachmentType.Image or AttachmentType.Gif => MfmInlineMedia.MediaType.Image, _ => MfmInlineMedia.MediaType.Other }, p.RemoteUrl ?? p.Url, p.Description)).ToList(); - + string? content = null; if (data?.Source != true) + { if (text != null || quoteUri != null || quoteInaccessible || replyInaccessible) { (content, inlineMedia) = await mfmConverter.ToHtmlAsync(text ?? "", mentionedUsers, note.UserHost, quoteUri, @@ -127,7 +126,8 @@ public class NoteRenderer( { content = ""; } - + } + var account = data?.Accounts?.FirstOrDefault(p => p.Id == note.UserId) ?? await userRenderer.RenderAsync(note.User, user); @@ -215,9 +215,7 @@ public class NoteRenderer( foreach (var edit in edits) { var files = attachments.Where(p => edit.FileIds.Contains(p.Id)).ToList(); - - // TODO: canDisplayInlineMedia oauth_token flag that marks all media as "other" so they end up as links - // (and doesn't remove the attachments) + var inlineMedia = files.Select(p => new MfmInlineMedia(p.Type switch { AttachmentType.Audio => MfmInlineMedia.MediaType.Audio, @@ -228,7 +226,7 @@ public class NoteRenderer( (var content, inlineMedia) = await mfmConverter.ToHtmlAsync(edit.Text ?? "", mentionedUsers, note.UserHost, media: inlineMedia); files.RemoveAll(attachment => inlineMedia.Any(inline => inline.Src == (attachment.RemoteUrl ?? attachment.Url))); - + var entry = new StatusEdit { Account = account, diff --git a/Iceshrimp.Backend/Core/Database/Migrations/v2024.1-beta4/20241128193322_IndexDriveFileUrl.cs b/Iceshrimp.Backend/Core/Database/Migrations/v2024.1-beta4/20241128193322_IndexDriveFileUrl.cs index 761fd656..7541d878 100644 --- a/Iceshrimp.Backend/Core/Database/Migrations/v2024.1-beta4/20241128193322_IndexDriveFileUrl.cs +++ b/Iceshrimp.Backend/Core/Database/Migrations/v2024.1-beta4/20241128193322_IndexDriveFileUrl.cs @@ -13,13 +13,13 @@ namespace Iceshrimp.Backend.Core.Database.Migrations /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.Sql("""CREATE INDEX IX_drive_file_accessUrl ON drive_file (COALESCE("webpublicUrl", url));"""); + migrationBuilder.Sql("""CREATE INDEX "IX_drive_file_accessUrl" ON "drive_file" (COALESCE("webpublicUrl", url));"""); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.Sql("""DROP INDEX IX_drive_file_accessUrl;"""); + migrationBuilder.Sql("""DROP INDEX "IX_drive_file_accessUrl";"""); } } } diff --git a/Iceshrimp.Backend/Core/Database/Migrations/v2024.1-beta5/20241203204110_AddInlineHtmlFeatureFlag.cs b/Iceshrimp.Backend/Core/Database/Migrations/v2024.1-beta5/20241203204110_AddInlineHtmlFeatureFlag.cs index 80e8973c..4726a242 100644 --- a/Iceshrimp.Backend/Core/Database/Migrations/v2024.1-beta5/20241203204110_AddInlineHtmlFeatureFlag.cs +++ b/Iceshrimp.Backend/Core/Database/Migrations/v2024.1-beta5/20241203204110_AddInlineHtmlFeatureFlag.cs @@ -21,7 +21,7 @@ namespace Iceshrimp.Backend.Core.Database.Migrations defaultValue: false); // automatically enable inline media for pleroma clients, as pleroma itself does support it - migrationBuilder.Sql("""UPDATE oauth_token SET "supportsInlineMedia"=TRUE WHERE "isPleroma"=TRUE;"""); + migrationBuilder.Sql("""UPDATE "oauth_token" SET "supportsInlineMedia"=TRUE WHERE "isPleroma"=TRUE;"""); } /// diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/NoteRenderer.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/NoteRenderer.cs index 91f23fa3..5df80bdb 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/NoteRenderer.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/NoteRenderer.cs @@ -25,7 +25,7 @@ public class NoteRenderer( { return new ASNote(false) { Id = note.Uri ?? note.GetPublicUri(config.Value) }; } - + public async Task RenderAsync(Note note, List? mentions = null) { if (note.IsPureRenote) @@ -125,7 +125,7 @@ public class NoteRenderer( }) .Cast() .ToList(); - + var inlineMedia = driveFiles?.Select(p => new MfmInlineMedia(MfmInlineMedia.GetType(p.Type), p.AccessUrl, p.Comment)) .ToList(); diff --git a/Iceshrimp.Backend/Core/Helpers/LibMfm/Conversion/MfmConverter.cs b/Iceshrimp.Backend/Core/Helpers/LibMfm/Conversion/MfmConverter.cs index 5860da82..3e3736b4 100644 --- a/Iceshrimp.Backend/Core/Helpers/LibMfm/Conversion/MfmConverter.cs +++ b/Iceshrimp.Backend/Core/Helpers/LibMfm/Conversion/MfmConverter.cs @@ -14,7 +14,7 @@ using HtmlParser = AngleSharp.Html.Parser.HtmlParser; namespace Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion; -public record MfmInlineMedia(MfmInlineMedia.MediaType Type, string Src, string? Alt) +public readonly record struct MfmInlineMedia(MfmInlineMedia.MediaType Type, string Src, string? Alt) { public enum MediaType { @@ -23,7 +23,7 @@ public record MfmInlineMedia(MfmInlineMedia.MediaType Type, string Src, string? Video, Audio } - + public static MediaType GetType(string mime) { if (mime.StartsWith("image/")) return MediaType.Image; @@ -34,6 +34,12 @@ public record MfmInlineMedia(MfmInlineMedia.MediaType Type, string Src, string? } } +/// Resulting data after HTML to MFM conversion +public readonly record struct HtmlMfmData(string Mfm, List InlineMedia); + +/// Resulting data after MFM to HTML conversion +public readonly record struct MfmHtmlData(string Html, List InlineMedia); + public class MfmConverter( IOptions config ) : ISingletonService @@ -41,10 +47,10 @@ public class MfmConverter( public AsyncLocal SupportsHtmlFormatting { get; } = new(); public AsyncLocal SupportsInlineMedia { get; } = new(); - public static async Task<(string Mfm, List InlineMedia)> FromHtmlAsync(string? html, List? mentions = null) + public static async Task FromHtmlAsync(string? html, List? mentions = null) { var media = new List(); - if (html == null) return ("", media); + if (html == null) return new HtmlMfmData("", media); // Ensure compatibility with AP servers that send both
as well as newlines var regex = new Regex(@"\r?\n", RegexOptions.IgnoreCase); @@ -54,12 +60,12 @@ public class MfmConverter( html = html.Replace("\u00A0", " "); var dom = await new HtmlParser().ParseDocumentAsync(html); - if (dom.Body == null) return ("", media); + if (dom.Body == null) return new HtmlMfmData("", media); var sb = new StringBuilder(); var parser = new MfmHtmlParser(mentions ?? [], media); dom.Body.ChildNodes.Select(parser.ParseNode).ToList().ForEach(s => sb.Append(s)); - return (sb.ToString().Trim(), media); + return new HtmlMfmData(sb.ToString().Trim(), media); } public static async Task> ExtractMentionsFromHtmlAsync(string? html) @@ -80,7 +86,7 @@ public class MfmConverter( return parser.Mentions; } - public async Task<(string Html, List InlineMedia)> ToHtmlAsync( + public async Task ToHtmlAsync( IEnumerable nodes, List mentions, string? host, string? quoteUri = null, bool quoteInaccessible = false, bool replyInaccessible = false, string rootElement = "p", List? emoji = null, List? media = null @@ -109,7 +115,7 @@ public class MfmConverter( } var usedMedia = new List(); - foreach (var node in nodeList) element.AppendNodes(FromMfmNode(document, node, mentions, host, ref usedMedia, emoji, media)); + foreach (var node in nodeList) element.AppendNodes(FromMfmNode(document, node, mentions, host, usedMedia, emoji, media)); if (quoteUri != null) { @@ -149,10 +155,10 @@ public class MfmConverter( await using var sw = new StringWriter(); await element.ToHtmlAsync(sw); - return (sw.ToString(), usedMedia); + return new MfmHtmlData(sw.ToString(), usedMedia); } - public async Task<(string Html, List InlineMedia)> ToHtmlAsync( + public async Task ToHtmlAsync( string mfm, List mentions, string? host, string? quoteUri = null, bool quoteInaccessible = false, bool replyInaccessible = false, string rootElement = "p", List? emoji = null, List? media = null @@ -164,19 +170,19 @@ public class MfmConverter( } private INode FromMfmNode( - IDocument document, IMfmNode node, List mentions, string? host, ref List usedMedia, + IDocument document, IMfmNode node, List mentions, string? host, List usedMedia, List? emoji = null, List? media = null ) { switch (node) { - case MfmFnNode { Name: "media" } fn when media != null: + case MfmFnNode { Name: "media" } fn when media is { Count: > 0 }: { var urlNode = fn.Children.FirstOrDefault(); if (urlNode is MfmUrlNode url) { - var current = media.FirstOrDefault(m => m.Src == url.Url); - if (current != null) + MfmInlineMedia? maybeCurrent = media.FirstOrDefault(m => m.Src == url.Url); + if (maybeCurrent is { } current) { usedMedia.Add(current); @@ -187,16 +193,16 @@ public class MfmConverter( if (current.Type == MfmInlineMedia.MediaType.Other) el.SetAttribute("download", "true"); - + var icon = current.Type switch { MfmInlineMedia.MediaType.Image => "\ud83d\uddbc\ufe0f", // framed picture emoji MfmInlineMedia.MediaType.Video => "\ud83c\udfac", // clapperboard emoji MfmInlineMedia.MediaType.Audio => "\ud83c\udfb5", // music note emoji - _ => "\ud83d\udcbe", // floppy disk emoji + _ => "\ud83d\udcbe", // floppy disk emoji }; - el.TextContent = $"[{icon} {current.Alt ?? current.Src}]"; + el.TextContent = $"[{icon} {current.Alt ?? current.Src}]"; return el; } else @@ -220,7 +226,7 @@ public class MfmConverter( { var el = CreateInlineFormattingElement(document, "i"); AddHtmlMarkup(document, el, "*"); - AppendChildren(el, document, node, mentions, host, ref usedMedia); + AppendChildren(el, document, node, mentions, host, usedMedia); AddHtmlMarkup(document, el, "*"); return el; } @@ -229,21 +235,21 @@ public class MfmConverter( { var el = CreateInlineFormattingElement(document, "b"); AddHtmlMarkup(document, el, "**"); - AppendChildren(el, document, node, mentions, host, ref usedMedia); + AppendChildren(el, document, node, mentions, host, usedMedia); AddHtmlMarkup(document, el, "**"); return el; } case MfmSmallNode: { var el = document.CreateElement("small"); - AppendChildren(el, document, node, mentions, host, ref usedMedia); + AppendChildren(el, document, node, mentions, host, usedMedia); return el; } case MfmStrikeNode: { var el = CreateInlineFormattingElement(document, "del"); AddHtmlMarkup(document, el, "~~"); - AppendChildren(el, document, node, mentions, host, ref usedMedia); + AppendChildren(el, document, node, mentions, host, usedMedia); AddHtmlMarkup(document, el, "~~"); return el; } @@ -252,7 +258,7 @@ public class MfmConverter( { var el = CreateInlineFormattingElement(document, "i"); AddHtmlMarkup(document, el, "*"); - AppendChildren(el, document, node, mentions, host, ref usedMedia); + AppendChildren(el, document, node, mentions, host, usedMedia); AddHtmlMarkup(document, el, "*"); return el; } @@ -267,7 +273,7 @@ public class MfmConverter( case MfmCenterNode: { var el = document.CreateElement("div"); - AppendChildren(el, document, node, mentions, host, ref usedMedia); + AppendChildren(el, document, node, mentions, host, usedMedia); return el; } case MfmEmojiCodeNode emojiCodeNode: @@ -357,7 +363,7 @@ public class MfmConverter( { var el = CreateInlineFormattingElement(document, "blockquote"); AddHtmlMarkup(document, el, "> "); - AppendChildren(el, document, node, mentions, host, ref usedMedia); + AppendChildren(el, document, node, mentions, host, usedMedia); el.AppendChild(document.CreateElement("br")); return el; } @@ -391,7 +397,7 @@ public class MfmConverter( case MfmPlainNode: { var el = document.CreateElement("span"); - AppendChildren(el, document, node, mentions, host, ref usedMedia); + AppendChildren(el, document, node, mentions, host, usedMedia); return el; } default: @@ -403,11 +409,11 @@ public class MfmConverter( private void AppendChildren( INode element, IDocument document, IMfmNode parent, - List mentions, string? host, ref List usedMedia, + List mentions, string? host, List usedMedia, List? emoji = null, List? media = null ) { - foreach (var node in parent.Children) element.AppendNodes(FromMfmNode(document, node, mentions, host, ref usedMedia, emoji, media)); + foreach (var node in parent.Children) element.AppendNodes(FromMfmNode(document, node, mentions, host, usedMedia, emoji, media)); } private IElement CreateInlineFormattingElement(IDocument document, string name) diff --git a/Iceshrimp.Backend/Core/Helpers/LibMfm/Parsing/HtmlParser.cs b/Iceshrimp.Backend/Core/Helpers/LibMfm/Parsing/HtmlParser.cs index 6dbb9ccb..b702454c 100644 --- a/Iceshrimp.Backend/Core/Helpers/LibMfm/Parsing/HtmlParser.cs +++ b/Iceshrimp.Backend/Core/Helpers/LibMfm/Parsing/HtmlParser.cs @@ -79,7 +79,7 @@ internal class HtmlParser(IEnumerable mentions, ICollection< ? $"\n> {string.Join("\n> ", node.TextContent.Split("\n"))}" : null; } - + case "VIDEO": case "AUDIO": case "IMG": @@ -87,9 +87,9 @@ internal class HtmlParser(IEnumerable mentions, ICollection< if (node is not HtmlElement el) return node.TextContent; var src = el.GetAttribute("src"); - if (!Uri.IsWellFormedUriString(src, UriKind.Absolute)) + if (src == null || !Uri.TryCreate(src, UriKind.Absolute, out var uri) && uri is { Scheme: "http" or "https" }) return node.TextContent; - + var alt = el.GetAttribute("alt") ?? el.GetAttribute("title"); var type = node.NodeName switch @@ -99,10 +99,10 @@ internal class HtmlParser(IEnumerable mentions, ICollection< "IMG" => MfmInlineMedia.MediaType.Image, _ => MfmInlineMedia.MediaType.Other, }; - + media.Add(new MfmInlineMedia(type, src, alt)); - return $"$[media {src} ]"; + return $"$[media {src}]"; } case "P": diff --git a/Iceshrimp.Backend/Core/Services/NoteService.cs b/Iceshrimp.Backend/Core/Services/NoteService.cs index d3ad5b1a..6ff49ebd 100644 --- a/Iceshrimp.Backend/Core/Services/NoteService.cs +++ b/Iceshrimp.Backend/Core/Services/NoteService.cs @@ -494,18 +494,15 @@ public class NoteService( if (node is MfmFnNode { Name: "media" } fn) { var urlNode = fn.Children.FirstOrDefault(); - if (urlNode is MfmUrlNode url) - { - urls.Add(url.Url); - } + if (urlNode is MfmUrlNode url) urls.Add(url.Url); } - + urls.AddRange(GetInlineMediaUrls(node.Children)); } return urls; } - + public async Task UpdateNoteAsync(NoteUpdateData data) { @@ -628,7 +625,7 @@ public class NoteService( } var attachments = data.Attachments?.ToList() ?? []; - + var inlineMediaUrls = nodes != null ? GetInlineMediaUrls(nodes) : []; var newMediaUrls = data.Attachments?.Select(p => p.Url) ?? []; var missingUrls = inlineMediaUrls.Except(newMediaUrls).ToArray(); @@ -1075,9 +1072,7 @@ public class NoteService( List? htmlInlineMedia = null; if (text == null) - { (text, htmlInlineMedia) = await MfmConverter.FromHtmlAsync(note.Content, mentionData.Mentions); - } var cw = note.Summary;