- Incoming reactions in all three shapes: Misskey and Sharkey Likes carrying an emoji (`content` or `_misskey_reaction`), Pleroma, Akkoma and Iceshrimp.NET EmojiReacts, and their Undo. Unicode reactions are one grapheme; custom ones need their Emoji tag and keep its image; `:name@host:` is read as `name`. A Like whose content is a heart stays a favourite. - Reactions are kept per account and emoji on our posts and on remote ones we hold, and the author of a local post gets a `pleroma:emoji_reaction` notification with the emoji. - Personas react through Pleroma's API (PUT/DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji, GET for the list), which sends an EmojiReact (or its Undo) to the post's author. - Statuses carry `emoji_reactions` (read by Phanpy) and `pleroma.emoji_reactions`, with counts and whether the viewer reacted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
120 lines
5.3 KiB
C#
120 lines
5.3 KiB
C#
using MongoDB.Entities;
|
|
|
|
using PrivaPub.ClientModels.Post;
|
|
using PrivaPub.Domain.Social;
|
|
using PrivaPub.Federation.Objects;
|
|
using PrivaPub.Models.Post;
|
|
using PrivaPub.Models.Social;
|
|
using PrivaPub.Tests.Support;
|
|
|
|
using System.Text.Json.Nodes;
|
|
|
|
namespace PrivaPub.Tests.Federation
|
|
{
|
|
public class ReactionNamesTests
|
|
{
|
|
[Theory]
|
|
[InlineData(":blobcat@k.example:", "blobcat")]
|
|
[InlineData("👍🏽", "👍🏽")]
|
|
[InlineData("❤️", "❤️")]
|
|
[InlineData("nope", null)]
|
|
[InlineData("👍👍", null)]
|
|
[InlineData(":unknown:", null)]
|
|
public void Normalises_what_peers_send(string content, string expected) =>
|
|
Assert.Equal(expected, Reactions.Normalise(content, new List<CustomEmoji> { new() { Shortcode = "blobcat", URL = "https://k.example/e.png" } })?.Name);
|
|
}
|
|
|
|
[Trait("Category", "Integration")]
|
|
public sealed class ReactionTests : IAsyncLifetime
|
|
{
|
|
Harness _harness;
|
|
|
|
public async ValueTask InitializeAsync()
|
|
{
|
|
Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip);
|
|
_harness = await Harness.Start();
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (_harness != default)
|
|
await _harness.DisposeAsync();
|
|
}
|
|
|
|
static string Origin(RemoteActor actor) => new Uri(actor.Id).GetLeftPart(UriPartial.Authority);
|
|
|
|
static string NewId(RemoteActor actor, string kind) => $"{Origin(actor)}/{kind}/{Guid.NewGuid():N}";
|
|
|
|
async Task<Post> LocalPost()
|
|
{
|
|
var (root, alice) = await _harness.Persona("alice");
|
|
await _harness.Posts.InsertPost(root, new InsertPostForm { AvatarId = alice.Id, Text = "react to me" }, TestContext.Current.CancellationToken);
|
|
return await DB.Default.Find<Post>().Match(p => p.GroupUserId == alice.Id).ExecuteFirstAsync(TestContext.Current.CancellationToken);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Misskey_and_pleroma_reactions_are_kept_apart_from_favourites()
|
|
{
|
|
var token = TestContext.Current.CancellationToken;
|
|
var post = await LocalPost();
|
|
var misskey = new RemoteActor(_harness.Peer, "kitty");
|
|
var pleroma = new RemoteActor(_harness.Peer, "pleroma");
|
|
|
|
await _harness.Deliver(misskey, "/human-centipede", new JsonObject
|
|
{
|
|
["id"] = NewId(misskey, "likes"), ["type"] = "Like", ["actor"] = misskey.Id, ["object"] = post.ObjectURI, ["content"] = ":blobcat:",
|
|
["_misskey_reaction"] = ":blobcat:",
|
|
["tag"] = new JsonArray(new JsonObject { ["type"] = "Emoji", ["name"] = ":blobcat:", ["icon"] = new JsonObject { ["type"] = "Image", ["url"] = $"{Origin(misskey)}/emoji/blobcat.png" } })
|
|
});
|
|
await _harness.Deliver(misskey, "/human-centipede", new JsonObject
|
|
{
|
|
["id"] = NewId(misskey, "likes"), ["type"] = "Like", ["actor"] = misskey.Id, ["object"] = post.ObjectURI, ["content"] = "❤"
|
|
});
|
|
var fire = new JsonObject { ["id"] = NewId(pleroma, "activities"), ["type"] = "EmojiReact", ["actor"] = pleroma.Id, ["object"] = post.ObjectURI, ["content"] = "🔥" };
|
|
await _harness.Deliver(pleroma, "/human-centipede", fire);
|
|
|
|
var kept = await DB.Default.Find<Reaction>().Match(r => r.PostId == post.ID).ExecuteAsync(token);
|
|
Assert.Equal(new HashSet<string> { "blobcat", "🔥" }, kept.Select(r => r.Emoji).ToHashSet());
|
|
Assert.Equal($"{Origin(misskey)}/emoji/blobcat.png", kept.Single(r => r.Emoji == "blobcat").EmojiURL);
|
|
Assert.Equal(1, (await DB.Default.Find<Post>().OneAsync(post.ID, token)).FavouritesCount);
|
|
Assert.True(await DB.Default.Find<Notification>().Match(n => n.Type == NotificationType.Reaction && n.Emoji == "🔥").ExecuteAnyAsync(token));
|
|
|
|
await _harness.Deliver(pleroma, "/human-centipede", new JsonObject
|
|
{
|
|
["id"] = NewId(pleroma, "activities"), ["type"] = "Undo", ["actor"] = pleroma.Id, ["object"] = fire.DeepClone()
|
|
});
|
|
Assert.Equal("blobcat", Assert.Single(await DB.Default.Find<Reaction>().Match(r => r.PostId == post.ID).ExecuteAsync(token)).Emoji);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_persona_reacts_to_a_remote_post_and_takes_it_back()
|
|
{
|
|
var token = TestContext.Current.CancellationToken;
|
|
var (_, alice) = await _harness.Persona("alice");
|
|
var mallory = new RemoteActor(_harness.Peer, "mallory");
|
|
var noteId = NewId(mallory, "notes");
|
|
await _harness.Deliver(mallory, "/human-centipede", new JsonObject
|
|
{
|
|
["id"] = NewId(mallory, "activities"), ["type"] = "Create", ["actor"] = mallory.Id,
|
|
["object"] = new JsonObject
|
|
{
|
|
["id"] = noteId, ["type"] = "Note", ["attributedTo"] = mallory.Id, ["content"] = "<p>hi</p>",
|
|
["to"] = new JsonArray(Addressing.Public), ["cc"] = new JsonArray(alice.Uri),
|
|
["tag"] = new JsonArray(new JsonObject { ["type"] = "Mention", ["href"] = alice.Uri, ["name"] = "@alice" })
|
|
}
|
|
});
|
|
var post = await DB.Default.Find<Post>().Match(p => p.ObjectURI == noteId).ExecuteFirstAsync(token);
|
|
|
|
Assert.True(await _harness.Reactions.React(alice, post, "🎉", token));
|
|
Assert.False(await _harness.Reactions.React(alice, post, "not an emoji", token));
|
|
Assert.True(await _harness.Reactions.Unreact(alice, post, "🎉", token));
|
|
|
|
var sent = await _harness.Outgoing(mallory.Id + "/inbox");
|
|
var react = Assert.Single(sent, a => a["type"]!.GetValue<string>() == "EmojiReact");
|
|
Assert.Equal("🎉", react["content"]!.GetValue<string>());
|
|
Assert.Equal(noteId, react["object"]!.GetValue<string>());
|
|
Assert.Equal(react["id"]!.GetValue<string>(), Assert.Single(sent, a => a["type"]!.GetValue<string>() == "Undo")["object"]!["id"]!.GetValue<string>());
|
|
}
|
|
}
|
|
}
|