using System.IO.Compression; using System.Text.Json; using Iceshrimp.Backend.Core.Middleware; using Microsoft.AspNetCore.StaticFiles; namespace Iceshrimp.Backend.Core.Services; public class EmojiZipEmoji { public string? Name { get; set; } public string? Category { get; set; } public List Aliases { get; set; } = []; } public class EmojiZipEntry { public required string FileName { get; set; } public required EmojiZipEmoji Emoji { get; set; } } public class EmojiZipMeta { public required ushort MetaVersion { get; set; } public required EmojiZipEntry[] Emojis { get; set; } } public record EmojiZip(EmojiZipMeta Metadata, ZipArchive Archive); public class EmojiImportService( EmojiService emojiSvc, ILogger logger ) { public static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web); public async Task Parse(Stream zipStream) { var archive = new ZipArchive(zipStream, ZipArchiveMode.Read); try { var meta = archive.GetEntry("meta.json") ?? throw GracefulException.BadRequest("Invalid emoji zip. Only Misskey-style emoji zips are supported."); var metaJson = await JsonSerializer.DeserializeAsync(meta.Open(), SerializerOptions) ?? throw GracefulException.BadRequest("Invalid emoji zip metadata"); if (metaJson.MetaVersion < 1 || metaJson.MetaVersion > 2) throw GracefulException.BadRequest("Unrecognized metaVersion {version}, expected 1 or 2", metaJson.MetaVersion.ToString()); return new(metaJson, archive); } catch { // We don't want to dispose of archive on success, as Import will do it when it's done. archive.Dispose(); throw; } } public async Task Import(EmojiZip zip) { using var archive = zip.Archive; var contentTypeProvider = new FileExtensionContentTypeProvider(); foreach (var emoji in zip.Metadata.Emojis) { var file = archive.GetEntry(emoji.FileName); if (file == null) { logger.LogWarning("Skipping {file} as no such file was found in the zip.", emoji.FileName); continue; } if (!contentTypeProvider.TryGetContentType(emoji.FileName, out var mimeType)) { logger.LogWarning("Skipping {file} as the mime type could not be detemrined.", emoji.FileName); continue; } // DriveService requires a seekable and .Length-able stream, which the DeflateStream from file.Open does not support. using var buffer = new MemoryStream((int)file.Length); await file.Open().CopyToAsync(buffer); buffer.Seek(0, SeekOrigin.Begin); var name = emoji.Emoji.Name ?? emoji.FileName; try { await emojiSvc.CreateEmojiFromStream( buffer, name, mimeType, emoji.Emoji.Aliases, emoji.Emoji.Category ); logger.LogDebug("Imported emoji {emoji}", name); } catch (GracefulException e) when (e.StatusCode == System.Net.HttpStatusCode.Conflict) { logger.LogDebug("Skipping {emoji} as it already exists.", name); } } } }