2026-10-17 05:57:25 +02:00
|
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
|
|
|
|
|
|
using MongoDB.Entities;
|
|
|
|
|
|
|
|
|
|
using PrivaPub.Domain.Content;
|
|
|
|
|
using PrivaPub.Federation.Actors;
|
|
|
|
|
using PrivaPub.Federation.Rendering;
|
|
|
|
|
using PrivaPub.Models.Post;
|
|
|
|
|
|
|
|
|
|
using System.Text.Json.Nodes;
|
|
|
|
|
|
|
|
|
|
namespace PrivaPub.Federation.Controllers
|
|
|
|
|
{
|
2026-10-18 10:29:53 +02:00
|
|
|
// Each of the server's custom emoji as an ActivityPub `Emoji` object at its id, /grimaces/{shortcode} (completion program
|
2026-10-17 05:57:25 +02:00
|
|
|
// R7; the path is LocalEmojis.Path): the tag that posts, actors and reactions carry, with its @context. GoToSocial takes
|
|
|
|
|
// an emoji only with an id, and may read it there. A disabled emoji is not served (404, it may come back); a deleted one
|
|
|
|
|
// answers 410 with a Tombstone. An emoji tells nothing about anyone, so it is served unsigned under SecureMode too, as the
|
|
|
|
|
// server's own actors are; a browser is sent to the picture.
|
|
|
|
|
[ApiController]
|
|
|
|
|
public class EmojisController : ControllerBase
|
|
|
|
|
{
|
|
|
|
|
const string ActivityContentType = "application/activity+json; charset=utf-8";
|
|
|
|
|
|
|
|
|
|
readonly ILocalActorService _localActors;
|
|
|
|
|
|
|
|
|
|
public EmojisController(ILocalActorService localActors)
|
|
|
|
|
{
|
|
|
|
|
_localActors = localActors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[HttpGet, Route("/" + LocalEmojis.Path + "/{shortcode}")]
|
|
|
|
|
public async Task<IActionResult> Get(string shortcode, CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
Response.Headers.Vary = "Accept";
|
|
|
|
|
if (!LocalEmojis.IsShortcode(shortcode))
|
|
|
|
|
return NotFound();
|
|
|
|
|
var key = shortcode.ToLowerInvariant();
|
|
|
|
|
var emoji = await DB.Default.Find<LocalEmoji>().Match(e => e.Key == key).ExecuteFirstAsync(token);
|
|
|
|
|
if (emoji == default || emoji.Shortcode != shortcode)
|
|
|
|
|
return NotFound();
|
|
|
|
|
if (emoji.DeletedAt is { } deleted)
|
|
|
|
|
return new ContentResult
|
|
|
|
|
{
|
|
|
|
|
Content = new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["@context"] = ActivityPubRenderer.ActivityStreams,
|
|
|
|
|
["id"] = LocalEmojis.Uri(_localActors.BaseAddress, emoji.Shortcode),
|
|
|
|
|
["type"] = "Tombstone",
|
|
|
|
|
["formerType"] = "Emoji",
|
|
|
|
|
["deleted"] = ActivityPubRenderer.Timestamp(deleted)
|
|
|
|
|
}.ToJsonString(),
|
|
|
|
|
ContentType = ActivityContentType,
|
|
|
|
|
StatusCode = StatusCodes.Status410Gone
|
|
|
|
|
};
|
|
|
|
|
if (emoji.Disabled || string.IsNullOrEmpty(emoji.Url))
|
|
|
|
|
return NotFound();
|
|
|
|
|
if (WantsHtml())
|
|
|
|
|
return Redirect(emoji.Url);
|
|
|
|
|
var live = new LocalEmojis.Live(emoji.ID, emoji.Shortcode, emoji.Url, emoji.StaticUrl ?? emoji.Url, emoji.ContentType ?? "image/png",
|
|
|
|
|
emoji.ImageUpdatedAt, emoji.Category, emoji.VisibleInPicker);
|
|
|
|
|
return new ContentResult
|
|
|
|
|
{
|
|
|
|
|
Content = LocalEmojis.Document(live, _localActors.BaseAddress).ToJsonString(),
|
|
|
|
|
ContentType = ActivityContentType,
|
|
|
|
|
StatusCode = StatusCodes.Status200OK
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool WantsHtml()
|
|
|
|
|
{
|
|
|
|
|
var accept = Request.Headers.Accept.ToString();
|
|
|
|
|
return accept.Contains("text/html", StringComparison.OrdinalIgnoreCase)
|
|
|
|
|
&& !accept.Contains("activity+json", StringComparison.OrdinalIgnoreCase)
|
|
|
|
|
&& !accept.Contains("ld+json", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|