Improve NoteService.CreateNote

This commit is contained in:
Laura Hausmann 2024-01-26 03:09:52 +01:00
parent ffe8408738
commit 0dc1533b75
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
7 changed files with 144 additions and 6 deletions

View file

@ -1,7 +1,17 @@
using System.Globalization;
namespace Iceshrimp.Backend.Core.Extensions;
public static class StringExtensions {
public static string Truncate(this string target, int maxLength) {
return target[..Math.Min(target.Length, maxLength)];
}
public static string ToPunycode(this string target) {
return new IdnMapping().GetAscii(target);
}
public static string FromPunycode(this string target) {
return new IdnMapping().GetUnicode(target);
}
}

View file

@ -14,8 +14,8 @@ public class ASNote : ASObject {
public string? Content { get; set; }
[J("https://www.w3.org/ns/activitystreams#url")]
[JC(typeof(LDIdObjectConverter))]
public LDIdObject? Url { get; set; }
[JC(typeof(ASLinkConverter))]
public ASLink? Url { get; set; }
[J("https://www.w3.org/ns/activitystreams#sensitive")]
[JC(typeof(VC))]

View file

@ -0,0 +1,98 @@
using System.Text;
using System.Text.RegularExpressions;
using AngleSharp.Dom;
using AngleSharp.Html.Parser;
namespace Iceshrimp.Backend.Core.Helpers;
public static class MfmHelpers {
public static async Task<string> FromHtml(string? html) {
if (html == null) return "";
// Ensure compatibility with AP servers that send both <br> as well as newlines
var regex = new Regex(@"<br\s?\/?>\r?\n", RegexOptions.IgnoreCase);
html = regex.Replace(html, "\n");
var dom = await new HtmlParser().ParseDocumentAsync(html);
if (dom.Body == null) return "";
var sb = new StringBuilder();
dom.Body.ChildNodes.Select(ParseNode).ToList().ForEach(s => sb.Append(s));
return sb.ToString().Trim();
}
private static string? ParseNode(INode node) {
if (node.NodeType is NodeType.Text)
return node.TextContent;
if (node.NodeType is NodeType.Comment or NodeType.Document)
return null;
switch (node.NodeName) {
case "BR": {
return "\n";
}
case "A": {
//TODO: implement parsing of links & mentions (automatically correct split domain mentions for the latter)
return null;
}
case "H1": {
return $"【{ParseChildren(node)}】\n";
}
case "B":
case "STRONG": {
return $"**{ParseChildren(node)}**";
}
case "SMALL": {
return $"<small>{ParseChildren(node)}</small>";
}
case "S":
case "DEL": {
return $"~~{ParseChildren(node)}~~";
}
case "I":
case "EM": {
return $"<i>{ParseChildren(node)}</i>";
}
case "PRE": {
return node.ChildNodes is [{ NodeName: "CODE" }]
? $"\n```\n{string.Join(null, node.ChildNodes[0].TextContent)}\n```\n"
: ParseChildren(node);
}
case "CODE": {
return $"`{ParseChildren(node)}`";
}
case "BLOCKQUOTE": {
return node.TextContent.Length > 0
? $"\n> {string.Join("\n> ", node.TextContent.Split("\n"))}"
: null;
}
case "P":
case "H2":
case "H3":
case "H4":
case "H5":
case "H6": {
return $"\n\n{ParseChildren(node)}";
}
case "DIV":
case "HEADER":
case "FOOTER":
case "ARTICLE":
case "LI":
case "DT":
case "DD": {
return $"\n{ParseChildren(node)}";
}
default: {
return ParseChildren(node);
}
}
}
private static string ParseChildren(INode node) {
return string.Join(null, node.ChildNodes.Select(ParseNode));
}
}

View file

@ -69,6 +69,14 @@ public class GracefulException(HttpStatusCode statusCode, string error, string m
public GracefulException(string message, string? details = null) :
this(HttpStatusCode.InternalServerError, HttpStatusCode.InternalServerError.ToString(), message, details) { }
public static GracefulException UnprocessableEntity(string message, string? details = null) {
return new GracefulException(HttpStatusCode.UnprocessableEntity, message, details);
}
public static GracefulException Forbidden(string message, string? details = null) {
return new GracefulException(HttpStatusCode.Forbidden, message, details);
}
}
public enum ExceptionVerbosity {

View file

@ -3,6 +3,7 @@ using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Federation.ActivityPub;
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Middleware;
using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Core.Services;
@ -19,16 +20,34 @@ public class NoteService(ILogger<NoteService> logger, DatabaseContext db, UserRe
var user = await userResolver.Resolve(actor.Id);
logger.LogDebug("Resolved user to {userId}", user.Id);
// Validate note
if (note.AttributedTo is not { Count: 1 } || note.AttributedTo[0].Id != user.Uri)
throw GracefulException.UnprocessableEntity("User.Uri doesn't match Note.AttributedTo");
if (user.Uri == null)
throw GracefulException.UnprocessableEntity("User.Uri is null");
if (new Uri(note.Id).IdnHost != new Uri(user.Uri).IdnHost)
throw GracefulException.UnprocessableEntity("User.Uri host doesn't match Note.Id host");
if (!note.Id.StartsWith("https://"))
throw GracefulException.UnprocessableEntity("Note.Id schema is invalid");
if (note.Url?.Link != null && !note.Url.Link.StartsWith("https://"))
throw GracefulException.UnprocessableEntity("Note.Url schema is invalid");
if (note.PublishedAt is null or { Year: < 2007 } || note.PublishedAt > DateTime.Now + TimeSpan.FromDays(3))
throw GracefulException.UnprocessableEntity("Note.PublishedAt is nonsensical");
if (user.IsSuspended)
throw GracefulException.Forbidden("User is suspended");
//TODO: validate AP object type
//TODO: parse note visibility
//TODO: resolve anything related to the note as well (reply thread, attachments, emoji, etc)
var dbNote = new Note {
Id = IdHelpers.GenerateSlowflakeId(),
Uri = note.Id,
Url = note.Url?.Id, //FIXME: this doesn't seem to work yet
Text = note.MkContent ?? note.Content, //TODO: html-to-mfm
Url = note.Url?.Id, //FIXME: this doesn't seem to work yet
Text = note.MkContent ?? await MfmHelpers.FromHtml(note.Content),
UserId = user.Id,
CreatedAt = note.PublishedAt?.ToUniversalTime() ??
throw new Exception("Missing or invalid PublishedAt field"),
throw GracefulException.UnprocessableEntity("Missing or invalid PublishedAt field"),
UserHost = user.Host,
Visibility = Note.NoteVisibility.Public //TODO: parse to & cc fields
};

View file

@ -1,7 +1,9 @@
using System.Globalization;
using System.Net;
using System.Security.Cryptography;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Federation.ActivityPub;
using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Middleware;
@ -16,7 +18,7 @@ public class UserService(ILogger<UserService> logger, DatabaseContext db, APFetc
var split = acct[5..].Split('@');
if (split.Length != 2) throw new GracefulException(HttpStatusCode.BadRequest, "Invalid query");
return (split[0], split[1]);
return (split[0], split[1].ToPunycode());
}
public Task<User?> GetUserFromQuery(string query) {

View file

@ -14,6 +14,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="1.1.0" />
<PackageReference Include="Asp.Versioning.Http" Version="8.0.0"/>
<PackageReference Include="cuid.net" Version="5.0.2"/>
<PackageReference Include="dotNetRdf.Core" Version="3.2.1-dev"/>