- A remote `summary` is a content warning only on a Note or Question, or when `sensitive` is set. On Articles,
Events, Videos, Pages and Audio it is an excerpt (WordPress teasers, Mobilizon dates and places, Mbin titles) and
is now kept as `Post.Excerpt` instead of hiding the post. `Post.ObjectType` records the remote type, and the
Mastodon API shows a remote non-Note object's title above its body again.
- Persona usernames must match `^[a-z0-9_]+$`, as groups already did; a name outside it was unreachable from
Mastodon and Misskey.
- `postingRestrictedToMods` is defined in our JSON-LD context (Iceshrimp.NET drops undefined terms).
- `Vary: Accept` on actor and object URLs.
- Owner decision: blocks federate. A block sends `Block`, an unblock `Undo{Block}`; checked live against GoToSocial.
- Owner decision: a persona's and a group's `published`, and the day in new ids, is a random day up to two weeks
before creation, so personas made the same day no longer share a date. Migration _007 gives existing ones theirs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
138 lines
5.9 KiB
C#
138 lines
5.9 KiB
C#
using PrivaPub.Federation.Objects;
|
|
using PrivaPub.Models.Post;
|
|
|
|
using System.Text.Json.Nodes;
|
|
|
|
namespace PrivaPub.Tests.Federation
|
|
{
|
|
public class NoteParserTests
|
|
{
|
|
const string MastodonNote = """
|
|
{
|
|
"id": "https://m.example/users/alice/statuses/1",
|
|
"type": "Note",
|
|
"summary": "spoilers",
|
|
"inReplyTo": "https://privapub.test/peasants/bob/posts/abc",
|
|
"published": "2026-09-30T10:00:00Z",
|
|
"url": "https://m.example/@alice/1",
|
|
"attributedTo": "https://m.example/users/alice",
|
|
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
|
"cc": ["https://m.example/users/alice/followers", "https://privapub.test/peasants/bob"],
|
|
"sensitive": true,
|
|
"conversation": "tag:m.example,2026-09-30:objectId=1:objectType=Conversation",
|
|
"context": "https://m.example/contexts/1",
|
|
"content": "<p><span class=\"h-card\"><a href=\"https://privapub.test/peasants/bob\" class=\"u-url mention\">@<span>bob</span></a></span> hi <a href=\"https://m.example/tags/Fedi\" class=\"mention hashtag\" rel=\"tag\">#<span>Fedi</span></a><script>x</script></p>",
|
|
"contentMap": { "en": "<p>hi</p>" },
|
|
"attachment": [{
|
|
"type": "Document", "mediaType": "image/png", "url": "https://files.m.example/a.png",
|
|
"name": "a cat", "blurhash": "UBL_:rOpGG-oBUNG,qRj2so|=eE1w^n4S5NH", "focalPoint": [0.5, -2.0], "width": 640, "height": 480
|
|
}],
|
|
"tag": [
|
|
{ "type": "Mention", "href": "https://privapub.test/peasants/bob", "name": "@bob@privapub.test" },
|
|
{ "type": "Hashtag", "href": "https://m.example/tags/fedi", "name": "#Fedi" },
|
|
{ "type": "Emoji", "name": ":blob:", "icon": { "type": "Image", "url": "https://files.m.example/blob.png" } }
|
|
]
|
|
}
|
|
""";
|
|
|
|
[Fact]
|
|
public void Reads_a_mastodon_note()
|
|
{
|
|
var note = NoteParser.Parse(JsonNode.Parse(MastodonNote));
|
|
|
|
Assert.Equal("https://m.example/users/alice", note.AttributedTo);
|
|
Assert.Equal("spoilers", note.SpoilerText);
|
|
Assert.True(note.Sensitive);
|
|
Assert.Equal("en", note.Language);
|
|
Assert.Equal("https://m.example/@alice/1", note.Url);
|
|
Assert.Equal("https://m.example/contexts/1", note.Context);
|
|
Assert.DoesNotContain("<script>", note.ContentHtml);
|
|
Assert.Contains("u-url mention", note.ContentHtml);
|
|
Assert.Equal("https://privapub.test/peasants/bob", Assert.Single(note.Mentions).ActorURI);
|
|
Assert.Equal("fedi", Assert.Single(note.Tags));
|
|
var media = Assert.Single(note.Attachments);
|
|
Assert.Equal("a cat", media.Description);
|
|
Assert.Equal(new[] { 0.5f, -1f }, media.Focus);
|
|
Assert.Equal(new DateTime(2026, 9, 30, 10, 0, 0, DateTimeKind.Utc), note.Published);
|
|
Assert.Null(note.Updated);
|
|
}
|
|
|
|
[Fact]
|
|
public void Reads_a_lemmy_page_with_a_title()
|
|
{
|
|
var note = NoteParser.Parse(JsonNode.Parse("""
|
|
{
|
|
"id": "https://l.example/post/7", "type": "Page", "name": "A title",
|
|
"attributedTo": "https://l.example/u/carol", "audience": "https://l.example/c/cats",
|
|
"content": "<p>body</p>", "to": ["https://l.example/c/cats", "https://www.w3.org/ns/activitystreams#Public"],
|
|
"published": "2026-09-30T10:00:00.123456+00:00"
|
|
}
|
|
"""));
|
|
|
|
Assert.Equal("A title", note.Title);
|
|
Assert.Equal("https://l.example/c/cats", note.Audience);
|
|
Assert.False(note.Sensitive);
|
|
}
|
|
|
|
[Fact]
|
|
public void Falls_back_to_misskey_content_and_takes_the_person_from_a_peertube_attribution()
|
|
{
|
|
var note = NoteParser.Parse(JsonNode.Parse("""
|
|
{
|
|
"id": "https://k.example/notes/9", "type": "Note",
|
|
"attributedTo": [{ "type": "Group", "id": "https://k.example/c/chan" }, { "type": "Person", "id": "https://k.example/users/9" }],
|
|
"_misskey_content": "hello <b>there</b>\nnext", "_misskey_quote": "https://k.example/notes/8"
|
|
}
|
|
"""));
|
|
|
|
Assert.Equal("https://k.example/users/9", note.AttributedTo);
|
|
Assert.Equal("hello <b>there</b><br>next", note.ContentHtml);
|
|
Assert.Equal("https://k.example/notes/8", note.QuoteUri);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Article", false, false)]
|
|
[InlineData("Event", false, false)]
|
|
[InlineData("Video", false, false)]
|
|
[InlineData("Page", false, false)]
|
|
[InlineData("Note", false, true)]
|
|
[InlineData("Question", false, true)]
|
|
[InlineData("Article", true, true)]
|
|
public void A_summary_warns_only_on_notes_or_when_marked_sensitive(string type, bool sensitive, bool warns)
|
|
{
|
|
var note = NoteParser.Parse(JsonNode.Parse($$"""
|
|
{
|
|
"id": "https://w.example/?p=12", "type": "{{type}}", "name": "Twelve", "summary": "Saturday, 10:00 at the library",
|
|
"sensitive": {{(sensitive ? "true" : "false")}}, "attributedTo": "https://w.example/author", "content": "<p>body</p>"
|
|
}
|
|
"""));
|
|
|
|
Assert.Equal(warns, note.Sensitive);
|
|
Assert.Equal(warns ? "Saturday, 10:00 at the library" : null, note.SpoilerText);
|
|
Assert.Equal(warns ? null : "Saturday, 10:00 at the library", note.Excerpt);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("""{ "id": "https://x.example/1", "type": "Person" }""")]
|
|
[InlineData("""{ "id": "not a uri", "type": "Note" }""")]
|
|
[InlineData("""[1, 2]""")]
|
|
public void Refuses_what_is_not_a_note(string json) =>
|
|
Assert.Null(NoteParser.Parse(JsonNode.Parse(json)));
|
|
}
|
|
|
|
public class AddressingTests
|
|
{
|
|
const string Followers = "https://m.example/users/alice/followers";
|
|
|
|
[Theory]
|
|
[InlineData(new[] { Addressing.Public }, new[] { Followers }, PostVisibility.Public)]
|
|
[InlineData(new[] { "as:Public" }, new string[0], PostVisibility.Public)]
|
|
[InlineData(new[] { Followers }, new[] { Addressing.Public }, PostVisibility.Unlisted)]
|
|
[InlineData(new[] { Followers }, new[] { "https://privapub.test/peasants/bob" }, PostVisibility.FollowersOnly)]
|
|
[InlineData(new[] { "https://privapub.test/peasants/bob" }, new string[0], PostVisibility.Direct)]
|
|
[InlineData(new[] { "https://m.example/users/mallory/followers" }, new string[0], PostVisibility.Direct)]
|
|
public void Classifies_like_mastodon(string[] to, string[] cc, PostVisibility expected) =>
|
|
Assert.Equal(expected, Addressing.Classify(to, cc, Followers));
|
|
}
|
|
}
|