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 { // Each of the server's custom emoji as an ActivityPub `Emoji` object at its id, /grimaces/{shortcode} (completion program // 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 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().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); } } }