- 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
77 lines
2.9 KiB
C#
77 lines
2.9 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
using MongoDB.Entities;
|
|
|
|
using PrivaPub.Api.Mastodon.Infrastructure;
|
|
using PrivaPub.Api.Mastodon.Mappers;
|
|
using PrivaPub.Domain.Privacy;
|
|
using PrivaPub.Domain.Social;
|
|
using PrivaPub.Models.Post;
|
|
using PrivaPub.Models.Social;
|
|
using PrivaPub.StaticServices;
|
|
|
|
using PostEntity = PrivaPub.Models.Post.Post;
|
|
|
|
namespace PrivaPub.Api.Mastodon.Controllers
|
|
{
|
|
public class ReactionsController : MastodonController
|
|
{
|
|
readonly DbEntities _dbEntities;
|
|
readonly MastodonMapper _mapper;
|
|
readonly IReactions _reactions;
|
|
|
|
public ReactionsController(DbEntities dbEntities, MastodonMapper mapper, IReactions reactions)
|
|
{
|
|
_dbEntities = dbEntities;
|
|
_mapper = mapper;
|
|
_reactions = reactions;
|
|
}
|
|
|
|
[HttpGet("/api/v1/pleroma/statuses/{id}/reactions"), Scope("read:statuses", requiresUser: false), AllowAnonymous]
|
|
public async Task<IActionResult> List(string id, CancellationToken token)
|
|
{
|
|
var post = await Visible(id, token);
|
|
if (post == default)
|
|
return NotFoundError();
|
|
var reactions = await DB.Default.Find<Reaction>().Match(r => r.PostId == post.ID).ExecuteAsync(token);
|
|
var accounts = await _mapper.Accounts(reactions.Select(r => r.AccountId), token);
|
|
return Json(reactions.GroupBy(r => r.Emoji).Select(group => new
|
|
{
|
|
name = group.Key,
|
|
count = group.Count(),
|
|
me = MyId != default && group.Any(r => r.AccountId == MyId),
|
|
url = _mapper.ProxiedUrl(group.First().EmojiURL),
|
|
accounts = group.Select(r => accounts.GetValueOrDefault(r.AccountId)).Where(a => a != default).ToList()
|
|
}).OrderByDescending(r => r.count).ToList());
|
|
}
|
|
|
|
[HttpPut("/api/v1/pleroma/statuses/{id}/reactions/{emoji}"), Scope("write:favourites")]
|
|
public async Task<IActionResult> React(string id, string emoji, CancellationToken token)
|
|
{
|
|
var post = await Visible(id, token);
|
|
if (post == default)
|
|
return NotFoundError();
|
|
if (!await _reactions.React(Me, post, Uri.UnescapeDataString(emoji), token))
|
|
return Error(StatusCodes.Status422UnprocessableEntity, "Validation failed: That emoji cannot be used here");
|
|
return Json(await _mapper.Status(post, MyId, token));
|
|
}
|
|
|
|
[HttpDelete("/api/v1/pleroma/statuses/{id}/reactions/{emoji}"), Scope("write:favourites")]
|
|
public async Task<IActionResult> Unreact(string id, string emoji, CancellationToken token)
|
|
{
|
|
var post = await Visible(id, token);
|
|
if (post == default)
|
|
return NotFoundError();
|
|
await _reactions.Unreact(Me, post, Uri.UnescapeDataString(emoji), token);
|
|
return Json(await _mapper.Status(post, MyId, token));
|
|
}
|
|
|
|
async Task<PostEntity> Visible(string id, CancellationToken token)
|
|
{
|
|
var post = await _dbEntities.Posts.Match(p => p.ID == id && !p.DeletedAt.HasValue && p.ReblogOfPostId == null).ExecuteFirstAsync(token);
|
|
return post != default && post.Visibility != PostVisibility.LocalGeo && await VisibilityPolicy.CanSee(post, MyId, token) ? post : default;
|
|
}
|
|
}
|
|
}
|