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
@@ -15,9 +15,11 @@ namespace PrivaPub.Infrastructure.Backup
|
||||
// - a root keeps P's password, e-mail, ban and policies;
|
||||
// - roots, personas and groups made since become tombstones: deleted, their names kept; local posts made since answer
|
||||
// 410; media made since go to the trash, as do media trashed since (and they leave the posts restored with them),
|
||||
// but the server's own pictures the live settings hold;
|
||||
// but the server's own pictures the live settings and custom emoji hold; a picture of the server's that neither holds
|
||||
// any more goes to the trash;
|
||||
// - the staff's log is the union (a restore never loses an entry, its own request included), and the server's
|
||||
// settings are P's: the live settings outlive a restore;
|
||||
// settings are P's: the live settings outlive a restore; so do its custom emoji (R7), as P has them: one deleted since
|
||||
// stays deleted (its id answers 410), one added or changed since stays as it is;
|
||||
// - every session ends, and every server's circuit is closed again.
|
||||
// Run on the imported database, it reads P's files only, so it can run again after a failed attempt.
|
||||
public static class ProtectiveMerge
|
||||
@@ -44,8 +46,8 @@ namespace PrivaPub.Infrastructure.Backup
|
||||
IEnumerable<BsonDocument> P(string collection) => ServerRestore.Read(Path.Combine(previous, collection + ".jsonl.gz"));
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// who follows whom: P's; and the server's settings as they were live
|
||||
foreach (var collection in new[] { "Follower", "Following", "ServerSettings" })
|
||||
// who follows whom: P's; and the server's settings and custom emoji as they were live
|
||||
foreach (var collection in new[] { "Follower", "Following", "ServerSettings", "LocalEmoji" })
|
||||
{
|
||||
await database.DropCollectionAsync(collection, token);
|
||||
await ServerRestore.Insert(database.GetCollection<BsonDocument>(collection), P(collection), token);
|
||||
@@ -151,6 +153,7 @@ namespace PrivaPub.Infrastructure.Backup
|
||||
|
||||
await Posts(database, P("Post"), report, token);
|
||||
await Media(database, P("MediaAttachment"), report, token);
|
||||
await ServerPictures(database, report, token);
|
||||
// what the staff took down stays down, and every post's flags follow the moderation now kept (R6)
|
||||
foreach (var post in P("Post").Where(p => IsSet(p, "ModeratedAt")))
|
||||
await database.GetCollection<BsonDocument>("Post").UpdateOneAsync(
|
||||
@@ -228,8 +231,8 @@ namespace PrivaPub.Infrastructure.Backup
|
||||
{
|
||||
if (!restored.TryGetValue(row["_id"], out var mine))
|
||||
{
|
||||
// made since, and to the trash, but a picture of the server's own (its thumbnail): the live settings,
|
||||
// which a restore keeps, still show it
|
||||
// made since, and to the trash, but a picture of the server's own (its thumbnail, a custom emoji's):
|
||||
// the live settings and emoji, which a restore keeps, still show it
|
||||
if (!IsSet(row, "TrashedAt") && !IsSet(row, "ServerUse"))
|
||||
{
|
||||
row["TrashedAt"] = now;
|
||||
@@ -258,6 +261,31 @@ namespace PrivaPub.Infrastructure.Backup
|
||||
Builders<BsonDocument>.Update.Set("TrashedAt", now).Set("TrashReason", "restore: its post was deleted"), cancellationToken: token)).ModifiedCount;
|
||||
}
|
||||
|
||||
// the server's own pictures (ServerUse) that neither the settings nor a custom emoji, both P's now, hold: to the trash.
|
||||
// One the backup still had and that was replaced and purged since would otherwise be held by nothing for ever.
|
||||
static async Task ServerPictures(IMongoDatabase database, RestoreReport report, CancellationToken token)
|
||||
{
|
||||
var held = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var settings in await (await database.GetCollection<BsonDocument>("ServerSettings").FindAsync(FilterDefinition<BsonDocument>.Empty,
|
||||
cancellationToken: token)).ToListAsync(token))
|
||||
if (settings.GetValue("ThumbnailMediaId", BsonNull.Value) is BsonString thumbnail)
|
||||
held.Add(thumbnail.AsString);
|
||||
foreach (var emoji in await (await database.GetCollection<BsonDocument>("LocalEmoji").FindAsync(new BsonDocument("DeletedAt", BsonNull.Value),
|
||||
cancellationToken: token)).ToListAsync(token))
|
||||
if (emoji.GetValue("MediaId", BsonNull.Value) is BsonString picture)
|
||||
held.Add(picture.AsString);
|
||||
var media = database.GetCollection<BsonDocument>("MediaAttachment");
|
||||
var rows = await (await media.FindAsync(new BsonDocument { { "ServerUse", new BsonDocument("$ne", BsonNull.Value) }, { "TrashedAt", BsonNull.Value } },
|
||||
new FindOptions<BsonDocument> { Projection = new BsonDocument("_id", 1) }, token)).ToListAsync(token);
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var row in rows.Where(r => !held.Contains(r["_id"].ToString())))
|
||||
{
|
||||
await media.UpdateOneAsync(Id(row["_id"]), Builders<BsonDocument>.Update.Set("TrashedAt", now).Set("TrashReason", "restore: no longer the server's"),
|
||||
cancellationToken: token);
|
||||
report.MediaTrashed++;
|
||||
}
|
||||
}
|
||||
|
||||
// P's rows added, each replacing the restored row with the same key: how many P had
|
||||
static async Task<int> Union(IMongoCollection<BsonDocument> collection, IEnumerable<BsonDocument> previous, string[] key, CancellationToken token)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user