A direct message answers in the form its recipient writes in

Lemmy 0.19 (most of the threadiverse) and Mbin take a private message only
as a ChatMessage and refuse a direct Note. A direct message to one account
elsewhere that writes to us as ChatMessages now goes out as one: to it
alone, without a mention in its text, kept on the post (Post.AsChatMessage)
so the served copy and an edit match. No software name decides it.

Live against Lemmy 0.19: alice's answer to lemmyuser's private message and
another persona's message to lemmyuser arrive (29 checks). A first message
to an account that never wrote to anyone here is still a Note, which they
refuse; G-0008 keeps that open for the owner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-05 18:13:51 +02:00
1 parent a85ebd30c0
commit f38ac73615
10 files changed
+90 -17

No files matched your search

@@ -106,6 +106,49 @@ namespace PrivaPub.Tests.Federation
Assert.NotNull(message.ConversationId);
}
// Lemmy 0.19 and Mbin take a private message only as a ChatMessage: someone who writes to us that way is answered in
// kind, anyone else with a Note
[Fact]
public async Task A_direct_message_answers_in_the_form_its_recipient_writes_in()
{
var token = TestContext.Current.CancellationToken;
var (_, alice) = await _harness.Persona("alice");
var lemmy = new RemoteActor(_harness.Peer, "pm");
var mastodon = new RemoteActor(_harness.Peer, "dm");
async Task<Post> Received(RemoteActor sender, string type)
{
var id = NewId(sender, "messages");
await _harness.Deliver(sender, "/human-centipede", new JsonObject
{
["id"] = NewId(sender, "activities/create"), ["type"] = "Create", ["actor"] = sender.Id, ["to"] = new JsonArray(alice.Uri),
["object"] = new JsonObject
{
["id"] = id, ["type"] = type, ["attributedTo"] = sender.Id, ["to"] = new JsonArray(alice.Uri),
["content"] = "<p>psst</p>", ["published"] = DateTime.UtcNow.ToString("O")
}
});
return await DB.Default.Find<Post>().Match(p => p.ObjectURI == id).ExecuteSingleAsync(token);
}
async Task<JsonObject> Answer(RemoteActor to, Post message)
{
var answer = await _harness.Statuses.Publish(alice, new PrivaPub.Domain.Statuses.StatusDraft
{
Text = "psst back", Visibility = PostVisibility.Direct, ConversationId = message.ConversationId
}, token);
Assert.True(answer.Ok, answer.Error);
return (JsonObject)(await _harness.Outgoing(to.Id + "/inbox")).Last(a => a["type"]!.GetValue<string>() == "Create")["object"]!;
}
var chat = await Answer(lemmy, await Received(lemmy, "ChatMessage"));
var note = await Answer(mastodon, await Received(mastodon, "Note"));
Assert.Equal("ChatMessage", chat["type"]!.GetValue<string>());
Assert.Equal(lemmy.Id, Assert.Single(chat["to"]!.AsArray())!.GetValue<string>());
Assert.Null(chat["cc"]);
Assert.DoesNotContain("h-card", chat["content"]!.GetValue<string>());
Assert.Equal("Note", note["type"]!.GetValue<string>());
}
[Fact]
public async Task An_unreachable_key_asks_the_sender_to_retry_instead_of_refusing()
{