import * as fs from "node:fs"; import AdmZip from "adm-zip"; import { queueLogger } from "../../logger.js"; import { createTempDir } from "../../../misc/create-temp.js"; import { downloadUrl } from "../../../misc/download-url.js"; import { DriveFiles, Emojis } from "../../../models/index.js"; import { addFile } from "../../../services/drive/add-file.js"; import { genId } from "../../../misc/gen-id.js"; import { db } from "../../../db/postgre.js"; import probeImageSize from "probe-image-size"; import * as path from "path"; const logger = queueLogger.createSubLogger("import-custom-emojis"); // TODO: 名前衝突時の動作を選べるようにする export async function importCustomEmojis(job) { logger.info("Importing custom emojis ..."); const file = await DriveFiles.findOneBy({ id: job.data.fileId }); if (file == null) { return "skip: File not found"; } const [tempPath, cleanup] = await createTempDir(); logger.info(`Temp dir is ${tempPath}`); const destPath = `${tempPath}/emojis.zip`; try { fs.writeFileSync(destPath, "", "binary"); await downloadUrl(file.url, destPath); } catch (e) { // TODO: 何度か再試行 if (e instanceof Error || typeof e === "string") { logger.error(e); } throw e; } const outputPath = `${tempPath}/emojis`; const unzipStream = fs.createReadStream(destPath); const zip = new AdmZip(destPath); logger.succ(`Unzipping to ${outputPath}`); await new Promise((resolve, reject)=>{ zip.extractAllToAsync(outputPath, true, false, async (error)=>{ if (error) reject(error); if (fs.existsSync(`${outputPath}/meta.json`)) { logger.info("starting emoji import with metadata"); const metaRaw = fs.readFileSync(`${outputPath}/meta.json`, "utf-8"); const meta = JSON.parse(metaRaw); for (const record of meta.emojis){ if (!record.downloaded) continue; const emojiInfo = record.emoji; const emojiPath = `${outputPath}/${record.fileName}`; await Emojis.delete({ name: emojiInfo.name }); const driveFile = await addFile({ user: null, path: emojiPath, name: record.fileName, force: true }); const file = fs.createReadStream(emojiPath); const size = await probeImageSize(file); file.destroy(); await Emojis.insert({ id: genId(), updatedAt: new Date(), name: emojiInfo.name, category: emojiInfo.category, host: null, aliases: emojiInfo.aliases, originalUrl: driveFile.url, publicUrl: driveFile.webpublicUrl ?? driveFile.url, type: driveFile.webpublicType ?? driveFile.type, license: emojiInfo.license, glyph: driveFile.type === "image/svg+xml", width: size.width || null, height: size.height || null }).then((x)=>Emojis.findOneByOrFail(x.identifiers[0])); } } else { logger.info("starting emoji import without metadata"); // Since we lack metadata, we import into a randomized category name instead let categoryName = genId(); let containedEmojis = fs.readdirSync(outputPath); // Filter out accidental JSON files containedEmojis = containedEmojis.filter((emoji)=>!emoji.match(/\.(json)$/i)); for (const emojiFilename of containedEmojis){ // strip extension and get filename to use as name const name = path.basename(emojiFilename, path.extname(emojiFilename)); const emojiPath = `${outputPath}/${emojiFilename}`; logger.info(`importing ${name}`); await Emojis.delete({ name: name }); const driveFile = await addFile({ user: null, path: emojiPath, name: path.basename(emojiFilename), force: true }); const file = fs.createReadStream(emojiPath); const size = await probeImageSize(file); file.destroy(); logger.info(`emoji size: ${size.width}x${size.height}`); await Emojis.insert({ id: genId(), updatedAt: new Date(), name: name, category: categoryName, host: null, aliases: [], originalUrl: driveFile.url, publicUrl: driveFile.webpublicUrl ?? driveFile.url, type: driveFile.webpublicType ?? driveFile.type, license: null, glyph: driveFile.type === "image/svg+xml", width: size.width || null, height: size.height || null }).then((x)=>Emojis.findOneByOrFail(x.identifiers[0])); } } await db.queryResultCache.remove([ "meta_emojis" ]); cleanup(); logger.succ("Imported"); resolve(undefined); }); }); return "Success"; }