R7 custom emoji: the server's own, added by administrators, sent on posts, profiles and reactions
LocalEmoji, a new collection (unique shortcode whatever its case, never renamed: the shortcode is its id elsewhere),
managed on /clientapi/admin/emoji/list|insert|update|delete and kept in the staff's log (emoji.insert, .update,
.delete). Its picture goes through IMediaService.EmojiImage: PNG, GIF, WebP or JPEG up to 256 KB, a PNG within 256 px
with alpha kept (an animated GIF or WebP stays an animated GIF, with a still PNG), keep=none, shrunk until it fits
GoToSocial's 100 KiB, held by ServerUse "emoji:<shortcode>" so the janitor and the audit leave it. LocalEmojis keeps the
offered ones in memory, reloaded on every change: GET /api/v1/custom_emojis is real; local posts (words, title, warning,
poll options, each revision) and profiles keep the emoji they name; Notes, Questions, actors and EmojiReacts carry
Mastodon's Emoji tag with its id, served as an Emoji at /emojis/{shortcode} (410 once deleted; the path awaits the
owner's confirmation); a persona reacts with one (Pleroma's reactions, announcements). A restore keeps the live emoji
as it keeps the settings, and trashes server pictures nothing holds. tools/pasture/scenarios/emoji.sh joins batch A.
Verified: built, and run locally against a scratch database (uploads of every type, animation kept, metadata gone,
refusals, rendering, reactions, history, announcements, delete and revive, backup and restore, media audit). The
pasture scenario and the sweep were not run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
1 parent
9f68ca3ed0
commit
d08fbabaa5
38 files changed
+1117
-54
No files matched your search
@@ -115,6 +115,7 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
}
|
||||
if (fields.Count > 0 || Params.Has("fields_attributes[0][name]"))
|
||||
avatar.Fields = fields;
|
||||
avatar.Emojis = PrivaPub.Domain.Content.LocalEmojis.OfProfile(avatar);//the server's custom emoji its name, note and fields use (R7)
|
||||
avatar.UpdatedAt = DateTime.UtcNow;
|
||||
await DB.Default.SaveAsync(avatar, token);
|
||||
foreach (var made in replaced)
|
||||
|
||||
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.Api.Mastodon.Infrastructure;
|
||||
using PrivaPub.Domain.Content;
|
||||
using PrivaPub.Domain.Social;
|
||||
using PrivaPub.Models.Social;
|
||||
|
||||
@@ -65,7 +66,7 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
["mentions"] = Array.Empty<object>(),
|
||||
["statuses"] = Array.Empty<object>(),
|
||||
["tags"] = Array.Empty<object>(),
|
||||
["emojis"] = Array.Empty<object>(),
|
||||
["emojis"] = Mappers.MastodonMapper.LocalEmojiEntities(LocalEmojis.In(a.Text)),//the server's custom emoji its text uses (R7)
|
||||
["reactions"] = reactions.Where(r => r.AnnouncementId == a.ID)
|
||||
.GroupBy(r => r.Name)
|
||||
.OrderBy(g => g.Min(r => r.CreatedAt))
|
||||
@@ -78,12 +79,18 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
// a unicode emoji's reaction: no url (custom emoji come later); `me` only for a persona
|
||||
// a reaction: a unicode emoji's has no url, one of the server's custom emoji's has its pictures (R7); `me` only for a
|
||||
// persona
|
||||
static Dictionary<string, object> Reaction(string name, int count, bool? me)
|
||||
{
|
||||
var reaction = new Dictionary<string, object> { ["name"] = name, ["count"] = count };
|
||||
if (me is { } mine)
|
||||
reaction["me"] = mine;
|
||||
if (LocalEmojis.Find(name) is { } custom)
|
||||
{
|
||||
reaction["url"] = custom.Url;
|
||||
reaction["static_url"] = custom.StaticUrl;
|
||||
}
|
||||
return reaction;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using PrivaPub.Api.Mastodon.Entities;
|
||||
using PrivaPub.Api.Mastodon.Infrastructure;
|
||||
using PrivaPub.Domain.Content;
|
||||
|
||||
namespace PrivaPub.Api.Mastodon.Controllers
|
||||
{
|
||||
// Mastodon's list of the server's custom emoji (completion program R7), for clients' pickers: as Mastodon lists them,
|
||||
// those offered (not disabled) and shown in the picker, by shortcode. Read from LocalEmojis, the copy in memory that every
|
||||
// change reloads at once (and that is read again when five minutes old), so no request reads the database.
|
||||
public class CustomEmojisController : MastodonController
|
||||
{
|
||||
[HttpGet("/api/v1/custom_emojis"), AllowAnonymous]
|
||||
public IActionResult List() => Json(LocalEmojis.Current.Values
|
||||
.Where(e => e.VisibleInPicker)
|
||||
.OrderBy(e => e.Shortcode, StringComparer.Ordinal)
|
||||
.Select(e => new CustomEmojiEntity { Shortcode = e.Shortcode, Url = e.Url, StaticUrl = e.StaticUrl, VisibleInPicker = true, Category = e.Category })
|
||||
.ToList());
|
||||
}
|
||||
}
|
||||
@@ -140,9 +140,6 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
|
||||
public class StubsController : MastodonController
|
||||
{
|
||||
[HttpGet("/api/v1/custom_emojis"), Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
||||
public IActionResult CustomEmojis() => Json(Array.Empty<object>());
|
||||
|
||||
[HttpGet("/api/v1/suggestions"), Scope("read")]
|
||||
public IActionResult SuggestionsV1() => Json(Array.Empty<object>());
|
||||
|
||||
|
||||
@@ -291,7 +291,8 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
SpoilerText = r.SpoilerText ?? string.Empty,
|
||||
Sensitive = r.HasContentWarning,
|
||||
CreatedAt = MastodonJson.Time(r.EditedAt),
|
||||
Account = account
|
||||
Account = account,
|
||||
Emojis = _mapper.EmojisOf(post, r.Emojis, r.ContentHtml, r.SpoilerText, r.Title)
|
||||
}).ToList();
|
||||
edits.Add(new StatusEdit
|
||||
{
|
||||
@@ -299,7 +300,8 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
SpoilerText = post.SpoilerText ?? string.Empty,
|
||||
Sensitive = post.HasContentWarning,
|
||||
CreatedAt = MastodonJson.Time(post.EditedAt ?? post.CreationDate),
|
||||
Account = account
|
||||
Account = account,
|
||||
Emojis = _mapper.EmojisOf(post, post.Emojis, post.ContentHtml, post.Text, post.SpoilerText, post.Title)
|
||||
});
|
||||
return Json(edits);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using PrivaPub.Api.Mastodon.Entities;
|
||||
using PrivaPub.Api.Mastodon.Infrastructure;
|
||||
using PrivaPub.Domain.Content;
|
||||
using PrivaPub.Domain.Media;
|
||||
using PrivaPub.Federation.Actors;
|
||||
using PrivaPub.Federation.Rendering;
|
||||
@@ -123,6 +124,7 @@ namespace PrivaPub.Api.Mastodon.Mappers
|
||||
FollowingCount = (int)following,
|
||||
StatusesCount = (int)statuses,
|
||||
LastStatusAt = latest == default ? default : latest.CreationDate.ToString("yyyy-MM-dd"),
|
||||
Emojis = LocalEmojiEntities(actor.Emojis),
|
||||
Fields = actor.Fields.Select(f => new Field { Name = f.Key, Value = ActivityPubRenderer.FieldValue(f.Value) }).ToList()
|
||||
};
|
||||
if (withSource)
|
||||
@@ -303,7 +305,7 @@ namespace PrivaPub.Api.Mastodon.Mappers
|
||||
MediaAttachments = post.Media.Count > 0 ? post.Media.Select(Media).ToList() : Playable(post),
|
||||
Card = Card(post),
|
||||
Poll = Poll(post, viewerId, ownVotes.GetValueOrDefault(post.ID)),
|
||||
Emojis = Emojis(post.Emojis),
|
||||
Emojis = post.IsFederatedCopy ? Emojis(post.Emojis) : LocalEmojiEntities(post.Emojis),
|
||||
Privapub = Extension(post, accounts, viewerId == default ? default(bool?) : downvoted.Contains(post.ID)),
|
||||
EmojiReactions = reactions.GetValueOrDefault(post.ID) ?? new(),
|
||||
Pleroma = new PleromaStatus { EmojiReactions = reactions.GetValueOrDefault(post.ID) ?? new() },
|
||||
@@ -500,13 +502,31 @@ namespace PrivaPub.Api.Mastodon.Mappers
|
||||
Voted = viewerId == default ? default(bool?) : ownVotes?.Count > 0 || post.GroupUserId == viewerId,
|
||||
OwnVotes = viewerId == default ? default : ownVotes?.ToList() ?? new List<int>(),
|
||||
Options = poll.Options.Select(o => new PollOptionEntity { Title = o.Title, VotesCount = hidden ? default(int?) : o.Votes }).ToList(),
|
||||
Emojis = Emojis(post.Emojis.Where(e => poll.Options.Any(o => o.Title.Contains($":{e.Shortcode}:"))))
|
||||
Emojis = post.IsFederatedCopy
|
||||
? Emojis(post.Emojis.Where(e => poll.Options.Any(o => o.Title.Contains($":{e.Shortcode}:"))))
|
||||
: LocalEmojiEntities(post.Emojis.Where(e => poll.Options.Any(o => o.Title.Contains($":{e.Shortcode}:"))))
|
||||
};
|
||||
}
|
||||
|
||||
List<CustomEmojiEntity> Emojis(IEnumerable<CustomEmoji> emojis) =>
|
||||
emojis?.Select(e => new CustomEmojiEntity { Shortcode = e.Shortcode, Url = Proxied(e.URL), StaticUrl = Proxied(e.URL) }).ToList() ?? new();
|
||||
|
||||
/// <summary>The custom emoji one version of a post names (its history's `emojis`): those the version kept, else the
|
||||
/// post's that its texts name.</summary>
|
||||
public List<CustomEmojiEntity> EmojisOf(PostEntity post, IReadOnlyCollection<CustomEmoji> kept, params string[] texts)
|
||||
{
|
||||
var named = (kept is { Count: > 0 } ? kept : post.Emojis ?? new List<CustomEmoji>()).Where(e => texts.Any(t => t?.Contains($":{e.Shortcode}:") == true));
|
||||
return post.IsFederatedCopy ? Emojis(named) : LocalEmojiEntities(named);
|
||||
}
|
||||
|
||||
// a local post's or persona's emoji are this server's, as they are now: one disabled or deleted since is left out, and
|
||||
// a new picture shows (R7)
|
||||
public static List<CustomEmojiEntity> LocalEmojiEntities(IEnumerable<CustomEmoji> kept) =>
|
||||
LocalEmojis.Of(kept).Select(e => new CustomEmojiEntity
|
||||
{
|
||||
Shortcode = e.Shortcode, Url = e.Url, StaticUrl = e.StaticUrl, VisibleInPicker = e.VisibleInPicker, Category = e.Category
|
||||
}).ToList();
|
||||
|
||||
PrivaPubStatus Extension(PostEntity post, Dictionary<string, Account> accounts, bool? downvoted) => new()
|
||||
{
|
||||
Wall = post.WallOwnerAccountId == default || post.WallOwnerAccountId == AuthorOf(post) ? default : accounts.GetValueOrDefault(post.WallOwnerAccountId),
|
||||
|
||||
Reference in new issue
Block a user