1
0
Fork 0

Rearrange testing emoji sheet images

This commit is contained in:
Cadence Ember 2024-03-01 17:28:14 +13:00
parent 18ef337aef
commit c5d6c5e4c7
9 changed files with 515 additions and 133 deletions

3
.gitignore vendored
View file

@ -3,4 +3,5 @@ config.js
registration.yaml registration.yaml
coverage coverage
db/ooye.db* db/ooye.db*
test/res/butterfly* test/res/*
!test/res/lottie*

View file

@ -17,6 +17,8 @@ const eventToMessage = sync.require("../converters/event-to-message")
const api = sync.require("../../matrix/api") const api = sync.require("../../matrix/api")
/** @type {import("../../d2m/actions/register-user")} */ /** @type {import("../../d2m/actions/register-user")} */
const registerUser = sync.require("../../d2m/actions/register-user") const registerUser = sync.require("../../d2m/actions/register-user")
/** @type {import("../converters/emoji-sheet")} */
const emojiSheet = sync.require("../converters/emoji-sheet")
/** /**
* @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[], pendingFiles?: ({name: string, url: string} | {name: string, url: string, key: string, iv: string} | {name: string, buffer: Buffer | Readable})[]}} message * @param {DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[], pendingFiles?: ({name: string, url: string} | {name: string, url: string, key: string, iv: string} | {name: string, buffer: Buffer | Readable})[]}} message
@ -75,7 +77,7 @@ async function sendEvent(event) {
// no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it // no need to sync the matrix member to the other side. but if I did need to, this is where I'd do it
let {messagesToEdit, messagesToSend, messagesToDelete, ensureJoined} = await eventToMessage.eventToMessage(event, guild, {api, snow: discord.snow, fetch}) let {messagesToEdit, messagesToSend, messagesToDelete, ensureJoined} = await eventToMessage.eventToMessage(event, guild, {api, snow: discord.snow, fetch, mxcDownloader: emojiSheet.getAndConvertEmoji})
messagesToEdit = await Promise.all(messagesToEdit.map(async e => { messagesToEdit = await Promise.all(messagesToEdit.map(async e => {
e.message = await resolvePendingFiles(e.message) e.message = await resolvePendingFiles(e.message)

View file

@ -1,6 +1,7 @@
// @ts-check // @ts-check
const assert = require("assert").strict const assert = require("assert").strict
const fs = require("fs")
const {pipeline} = require("stream").promises const {pipeline} = require("stream").promises
const sharp = require("sharp") const sharp = require("sharp")
const {GIFrame} = require("giframe") const {GIFrame} = require("giframe")
@ -16,27 +17,11 @@ const IMAGES_ACROSS = Math.floor(RESULT_WIDTH / SIZE)
/** /**
* Composite a bunch of Matrix emojis into a kind of spritesheet image to upload to Discord. * Composite a bunch of Matrix emojis into a kind of spritesheet image to upload to Discord.
* @param {string[]} mxcs mxc URLs, in order * @param {string[]} mxcs mxc URLs, in order
* @param {(mxc: string) => Promise<Buffer | undefined>} mxcDownloader function that will download the mxc URLs and convert to uncompressed PNG data. use `getAndConvertEmoji` or a mock.
* @returns {Promise<Buffer>} PNG image * @returns {Promise<Buffer>} PNG image
*/ */
async function compositeMatrixEmojis(mxcs) { async function compositeMatrixEmojis(mxcs, mxcDownloader) {
const buffers = await Promise.all(mxcs.map(async mxc => { const buffers = await Promise.all(mxcs.map(mxcDownloader))
const abortController = new AbortController()
const url = utils.getPublicUrlForMxc(mxc)
assert(url)
/** @type {import("node-fetch").Response} */
// If it turns out to be a GIF, we want to abandon the connection without downloading the whole thing.
// If we were using connection pooling, we would be forced to download the entire GIF.
// So we set no agent to ensure we are not connection pooling.
// @ts-ignore the signal is slightly different from the type it wants (still works fine)
const res = await fetch(url, {agent: false, signal: abortController.signal})
return convertImageStream(res.body, () => {
abortController.abort()
res.body.pause()
res.body.emit("end")
})
}))
// Calculate the size of the final composited image // Calculate the size of the final composited image
const totalWidth = Math.min(buffers.length, IMAGES_ACROSS) * SIZE const totalWidth = Math.min(buffers.length, IMAGES_ACROSS) * SIZE
@ -65,6 +50,49 @@ async function compositeMatrixEmojis(mxcs) {
return output.data return output.data
} }
/**
* Downloads the emoji from the web and converts to uncompressed PNG data.
* @param {string} mxc a single mxc:// URL
* @returns {Promise<Buffer | undefined>} uncompressed PNG data, or undefined if the downloaded emoji is not valid
*/
async function getAndConvertEmoji(mxc) {
const abortController = new AbortController()
const url = utils.getPublicUrlForMxc(mxc)
assert(url)
/** @type {import("node-fetch").Response} */
// If it turns out to be a GIF, we want to abandon the connection without downloading the whole thing.
// If we were using connection pooling, we would be forced to download the entire GIF.
// So we set no agent to ensure we are not connection pooling.
// @ts-ignore the signal is slightly different from the type it wants (still works fine)
const res = await fetch(url, {agent: false, signal: abortController.signal})
return convertImageStream(res.body, () => {
abortController.abort()
res.body.pause()
res.body.emit("end")
})
}
/**
* MOCK: Gets the emoji from the filesystem and converts to uncompressed PNG data.
* @param {string} mxc a single mxc:// URL
* @returns {Promise<Buffer | undefined>} uncompressed PNG data, or undefined if the downloaded emoji is not valid
*/
async function _mockGetAndConvertEmoji(mxc) {
const id = mxc.match(/\/([^./]*)$/)?.[1]
let s
if (fs.existsSync(`test/res/${id}.png`)) {
s = fs.createReadStream(`test/res/${id}.png`)
} else {
s = fs.createReadStream(`test/res/${id}.gif`)
}
return convertImageStream(s, () => {
s.pause()
s.emit("end")
})
}
/** /**
* @param {import("node-fetch").Response["body"]} streamIn * @param {import("node-fetch").Response["body"]} streamIn
* @param {() => any} stopStream * @param {() => any} stopStream
@ -128,4 +156,6 @@ async function convertImageStream(streamIn, stopStream) {
} }
module.exports.compositeMatrixEmojis = compositeMatrixEmojis module.exports.compositeMatrixEmojis = compositeMatrixEmojis
module.exports.getAndConvertEmoji = getAndConvertEmoji
module.exports._mockGetAndConvertEmoji = _mockGetAndConvertEmoji
module.exports._convertImageStream = convertImageStream module.exports._convertImageStream = convertImageStream

View file

@ -27,8 +27,9 @@ class Meter extends Transform {
* @param {import("supertape").Test} t * @param {import("supertape").Test} t
* @param {string} path * @param {string} path
* @param {number} totalSize * @param {number} totalSize
* @param {number => boolean} sizeCheck
*/ */
async function runSingleTest(t, path, totalSize) { async function runSingleTest(t, path, totalSize, sizeCheck) {
const file = fs.createReadStream(path) const file = fs.createReadStream(path)
const meter = new Meter() const meter = new Meter()
const p = file.pipe(meter) const p = file.pipe(meter)
@ -36,19 +37,23 @@ async function runSingleTest(t, path, totalSize) {
file.pause() file.pause()
file.emit("end") file.emit("end")
}) })
t.equal(result.subarray(1, 4).toString("ascii"), "PNG", `result was not a PNG file: ${result.toString("base64")}`) t.equal(result.subarray(1, 4).toString("ascii"), "PNG", `test that this is a PNG file: ${result.toString("base64").slice(0, 100)}`)
/* c8 ignore next 5 */ /* c8 ignore next 5 */
if (meter.bytes < totalSize / 4) { // should download less than 25% of each file if (sizeCheck(meter.bytes)) {
t.pass("intentionally read partial file") t.pass("read the correct amount of the file")
} else { } else {
t.fail(`read more than 25% of file, read: ${meter.bytes}, total: ${totalSize}`) t.fail(`read too much or too little of the file, read: ${meter.bytes}, total: ${totalSize}`)
} }
} }
slow()("emoji-sheet: only partial file is read for APNG", async t => { slow()("emoji-sheet: only partial file is read for APNG", async t => {
await runSingleTest(t, "test/res/butterfly.png", 2438998) await runSingleTest(t, "test/res/butterfly.png", 2438998, n => n < 2438998 / 4) // should download less than 25% of the file
}) })
slow()("emoji-sheet: only partial file is read for GIF", async t => { slow()("emoji-sheet: only partial file is read for GIF", async t => {
await runSingleTest(t, "test/res/butterfly.gif", 781223) await runSingleTest(t, "test/res/butterfly.gif", 781223, n => n < 781223 / 4) // should download less than 25% of the file
})
slow()("emoji-sheet: entire file is read for static PNG", async t => {
await runSingleTest(t, "test/res/RLMgJGfgTPjIQtvvWZsYjhjy.png", 11301, n => n === 11301) // should download entire file
}) })

View file

@ -304,8 +304,9 @@ function getUserOrProxyOwnerID(mxid) {
* @param {string} content * @param {string} content
* @param {{id: string, name: string}[]} attachments * @param {{id: string, name: string}[]} attachments
* @param {({name: string, url: string} | {name: string, url: string, key: string, iv: string} | {name: string, buffer: Buffer})[]} pendingFiles * @param {({name: string, url: string} | {name: string, url: string, key: string, iv: string} | {name: string, buffer: Buffer})[]} pendingFiles
* @param {(mxc: string) => Promise<Buffer | undefined>} mxcDownloader function that will download the mxc URLs and convert to uncompressed PNG data. use `getAndConvertEmoji` or a mock.
*/ */
async function uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles) { async function uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles, mxcDownloader) {
if (!content.includes("<::>")) return content // No unknown emojis, nothing to do if (!content.includes("<::>")) return content // No unknown emojis, nothing to do
// Remove known and unknown emojis from the end of the message // Remove known and unknown emojis from the end of the message
const r = /<a?:[a-zA-Z0-9_]*:[0-9]*>\s*$/ const r = /<a?:[a-zA-Z0-9_]*:[0-9]*>\s*$/
@ -313,7 +314,7 @@ async function uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles)
content = content.replace(r, "") content = content.replace(r, "")
} }
// Create a sprite sheet of known and unknown emojis from the end of the message // Create a sprite sheet of known and unknown emojis from the end of the message
const buffer = await emojiSheet.compositeMatrixEmojis(endOfMessageEmojis) const buffer = await emojiSheet.compositeMatrixEmojis(endOfMessageEmojis, mxcDownloader)
// Attach it // Attach it
const name = "emojis.png" const name = "emojis.png"
attachments.push({id: String(attachments.length), name}) attachments.push({id: String(attachments.length), name})
@ -421,7 +422,7 @@ const attachmentEmojis = new Map([
/** /**
* @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File | Ty.Event.Outer_M_Sticker | Ty.Event.Outer_M_Room_Message_Encrypted_File} event * @param {Ty.Event.Outer_M_Room_Message | Ty.Event.Outer_M_Room_Message_File | Ty.Event.Outer_M_Sticker | Ty.Event.Outer_M_Room_Message_Encrypted_File} event
* @param {import("discord-api-types/v10").APIGuild} guild * @param {import("discord-api-types/v10").APIGuild} guild
* @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer, fetch: import("node-fetch")["default"]}} di simple-as-nails dependency injection for the matrix API * @param {{api: import("../../matrix/api"), snow: import("snowtransfer").SnowTransfer, fetch: import("node-fetch")["default"], mxcDownloader: (mxc: string) => Promise<Buffer | undefined>}} di simple-as-nails dependency injection for the matrix API
*/ */
async function eventToMessage(event, guild, di) { async function eventToMessage(event, guild, di) {
/** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[]})[]} */ /** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | Readable}[]})[]} */
@ -717,7 +718,7 @@ async function eventToMessage(event, guild, di) {
if (replyLine && content.startsWith("> ")) content = "\n" + content if (replyLine && content.startsWith("> ")) content = "\n" + content
// SPRITE SHEET EMOJIS FEATURE: // SPRITE SHEET EMOJIS FEATURE:
content = await uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles) content = await uploadEndOfMessageSpriteSheet(content, attachments, pendingFiles, di?.mxcDownloader)
} else { } else {
// Looks like we're using the plaintext body! // Looks like we're using the plaintext body!
content = event.content.body content = event.content.body

View file

@ -1,6 +1,7 @@
const assert = require("assert").strict const assert = require("assert").strict
const {test} = require("supertape") const {test} = require("supertape")
const {eventToMessage} = require("./event-to-message") const {eventToMessage} = require("./event-to-message")
const {_mockGetAndConvertEmoji} = require("./emoji-sheet")
const data = require("../../test/data") const data = require("../../test/data")
const {MatrixServerError} = require("../../matrix/mreq") const {MatrixServerError} = require("../../matrix/mreq")
const {db, select, discord} = require("../../passthrough") const {db, select, discord} = require("../../passthrough")
@ -3534,7 +3535,7 @@ slow()("event2message: unknown emoji at the end is reuploaded as a sprite sheet"
}, },
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}) }, {}, {mxcDownloader: _mockGetAndConvertEmoji})
const testResult = { const testResult = {
content: messages.messagesToSend[0].content, content: messages.messagesToSend[0].content,
fileName: messages.messagesToSend[0].pendingFiles[0].name, fileName: messages.messagesToSend[0].pendingFiles[0].name,
@ -3559,7 +3560,7 @@ slow()("event2message: known emoji from an unreachable server at the end is reup
}, },
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}) }, {}, {mxcDownloader: _mockGetAndConvertEmoji})
const testResult = { const testResult = {
content: messages.messagesToSend[0].content, content: messages.messagesToSend[0].content,
fileName: messages.messagesToSend[0].pendingFiles[0].name, fileName: messages.messagesToSend[0].pendingFiles[0].name,
@ -3584,7 +3585,7 @@ slow()("event2message: known and unknown emojis in the end are reuploaded as a s
}, },
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU", event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe" room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
}) }, {}, {mxcDownloader: _mockGetAndConvertEmoji})
const testResult = { const testResult = {
content: messages.messagesToSend[0].content, content: messages.messagesToSend[0].content,
fileName: messages.messagesToSend[0].pendingFiles[0].name, fileName: messages.messagesToSend[0].pendingFiles[0].name,
@ -3609,7 +3610,7 @@ slow()("event2message: all unknown chess emojis are reuploaded as a sprite sheet
}, },
event_id: "$Me6iE8C8CZyrDEOYYrXKSYRuuh_25Jj9kZaNrf7LKr4", event_id: "$Me6iE8C8CZyrDEOYYrXKSYRuuh_25Jj9kZaNrf7LKr4",
room_id: "!maggESguZBqGBZtSnr:cadence.moe" room_id: "!maggESguZBqGBZtSnr:cadence.moe"
}) }, {}, {mxcDownloader: _mockGetAndConvertEmoji})
const testResult = { const testResult = {
content: messages.messagesToSend[0].content, content: messages.messagesToSend[0].content,
fileName: messages.messagesToSend[0].pendingFiles[0].name, fileName: messages.messagesToSend[0].pendingFiles[0].name,
@ -3618,6 +3619,6 @@ slow()("event2message: all unknown chess emojis are reuploaded as a sprite sheet
t.deepEqual(testResult, { t.deepEqual(testResult, {
content: "testing", content: "testing",
fileName: "emojis.png", fileName: "emojis.png",
fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAASAAAAAwCAYAAACxIqevAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAgAElEQVR4nOV9B1xUV9r3JMbEGBQLbRodhukDg2jWZP02" fileContentStart: "iVBORw0KGgoAAAANSUhEUgAAAYAAAABgCAYAAAAU9KWJAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAgAElEQVR4nOx9B3xT1/W/UkImYKZtLdt4a0uWMaQkzS9t"
}) })
}) })

465
package-lock.json generated
View file

@ -38,9 +38,10 @@
"@types/node": "^18.16.0", "@types/node": "^18.16.0",
"@types/node-fetch": "^2.6.3", "@types/node-fetch": "^2.6.3",
"c8": "^8.0.1", "c8": "^8.0.1",
"colorette": "^1.4.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"discord-api-types": "^0.37.60", "discord-api-types": "^0.37.60",
"supertape": "^8.3.0", "supertape": "^10.3.0",
"tap-dot": "github:cloudrac3r/tap-dot#9dd7750ececeae3a96afba91905be812b6b2cc2d" "tap-dot": "github:cloudrac3r/tap-dot#9dd7750ececeae3a96afba91905be812b6b2cc2d"
} }
}, },
@ -163,6 +164,75 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@istanbuljs/schema": { "node_modules/@istanbuljs/schema": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
@ -173,12 +243,12 @@
} }
}, },
"node_modules/@jest/schemas": { "node_modules/@jest/schemas": {
"version": "29.4.3", "version": "29.6.3",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
"integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@sinclair/typebox": "^0.25.16" "@sinclair/typebox": "^0.27.8"
}, },
"engines": { "engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0" "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
@ -209,17 +279,27 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@putout/cli-keypress": { "node_modules/@putout/cli-keypress": {
"version": "1.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@putout/cli-keypress/-/cli-keypress-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@putout/cli-keypress/-/cli-keypress-2.0.0.tgz",
"integrity": "sha512-w+lRVGZodRM4K214R4jvyOsmCUGA3OnaYDOJ2rpXj6a+O6n91zLlkb7JYsw6I0QCNmXjpNLJSoLgzGWTue6YIg==", "integrity": "sha512-EXJv2HaXM+5scjoxE6Tf+o4+pxwL1tYJZJBDMygrF7cocjirGcU05GgNr9WHOaUPaVOpVjVU98ugYD7XJLmMkw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"ci-info": "^3.1.1", "ci-info": "^4.0.0",
"fullstore": "^3.0.0" "fullstore": "^3.0.0"
}, },
"engines": { "engines": {
"node": ">=14" "node": ">=16"
} }
}, },
"node_modules/@putout/cli-validate-args": { "node_modules/@putout/cli-validate-args": {
@ -236,9 +316,9 @@
} }
}, },
"node_modules/@sinclair/typebox": { "node_modules/@sinclair/typebox": {
"version": "0.25.24", "version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
"integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
"dev": true "dev": true
}, },
"node_modules/@supertape/engine-loader": { "node_modules/@supertape/engine-loader": {
@ -279,19 +359,31 @@
} }
}, },
"node_modules/@supertape/formatter-progress-bar": { "node_modules/@supertape/formatter-progress-bar": {
"version": "4.1.0", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/@supertape/formatter-progress-bar/-/formatter-progress-bar-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@supertape/formatter-progress-bar/-/formatter-progress-bar-5.0.2.tgz",
"integrity": "sha512-MYwso7kbiTE0DaZgbiSlNOikmEcFdL4RQUu1JvnW+cS6ZLl3fqNnmvKa1a14VChKyHzfaTKYLuqToN8zgUjP2g==", "integrity": "sha512-thJlWFQLVeZjK/YZktUNBU69LFaEjBNvRcg6otuXn0RHhorRrgNWHHkA7JLDHRExzc+xaGJJvrWwsslh47vN2g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"chalk": "^4.1.0", "chalk": "^5.3.0",
"ci-info": "^3.1.1", "ci-info": "^4.0.0",
"cli-progress": "^3.8.2", "cli-progress": "^3.8.2",
"fullstore": "^3.0.0", "fullstore": "^3.0.0",
"once": "^1.4.0" "once": "^1.4.0"
}, },
"engines": { "engines": {
"node": ">=16" "node": ">=18"
}
},
"node_modules/@supertape/formatter-progress-bar/node_modules/chalk": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
"dev": true,
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/@supertape/formatter-short": { "node_modules/@supertape/formatter-short": {
@ -312,10 +404,39 @@
"node": ">=16" "node": ">=16"
} }
}, },
"node_modules/@supertape/formatter-time": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@supertape/formatter-time/-/formatter-time-1.0.2.tgz",
"integrity": "sha512-QihQWA/3LSNuODHrL8MGNHkdRunaEqNQkuMUDGNgEQO8MYBB0d83WGlNxDFGjn4kRlq47hovw3Skq7Btb2i2JA==",
"dev": true,
"dependencies": {
"chalk": "^5.3.0",
"ci-info": "^4.0.0",
"cli-progress": "^3.8.2",
"fullstore": "^3.0.0",
"once": "^1.4.0",
"timer-node": "^5.0.7"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@supertape/formatter-time/node_modules/chalk": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
"dev": true,
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@supertape/operator-stub": { "node_modules/@supertape/operator-stub": {
"version": "3.0.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/@supertape/operator-stub/-/operator-stub-3.0.0.tgz", "resolved": "https://registry.npmjs.org/@supertape/operator-stub/-/operator-stub-3.1.0.tgz",
"integrity": "sha512-LZ6E4nSMDMbLOhvEZyeXo8wS5EBiAAffWrohb7yaVHDVTHr+xkczzPxinkvcOBhNuAtC0kVARdMbHg+HULmozA==", "integrity": "sha512-jzC56u1k+3DLRo854+J6v/DP/4SjRV2mAqfR6qzsyaAocC9OFe7NHYQQMmlJ4cUJwgFjUh7AVnjFfC0Z0XuH+g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@cloudcmd/stub": "^4.0.0" "@cloudcmd/stub": "^4.0.0"
@ -722,9 +843,9 @@
} }
}, },
"node_modules/ci-info": { "node_modules/ci-info": {
"version": "3.8.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz",
"integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -1061,9 +1182,9 @@
} }
}, },
"node_modules/diff-sequences": { "node_modules/diff-sequences": {
"version": "29.4.3", "version": "29.6.3",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
"integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0" "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
@ -1087,6 +1208,12 @@
"resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz", "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz",
"integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==" "integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ=="
}, },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -1480,19 +1607,50 @@
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
}, },
"node_modules/glob": { "node_modules/glob": {
"version": "8.1.0", "version": "10.3.10",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"fs.realpath": "^1.0.0", "foreground-child": "^3.1.0",
"inflight": "^1.0.4", "jackspeak": "^2.3.5",
"inherits": "2", "minimatch": "^9.0.1",
"minimatch": "^5.0.1", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
"once": "^1.3.0" "path-scurry": "^1.10.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob/node_modules/foreground-child": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"engines": {
"node": ">=14"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
@ -1779,12 +1937,12 @@
} }
}, },
"node_modules/is-core-module": { "node_modules/is-core-module": {
"version": "2.12.0", "version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
"integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"has": "^1.0.3" "hasown": "^2.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@ -1982,25 +2140,43 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/jackspeak": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
"integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
"dev": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/jest-diff": { "node_modules/jest-diff": {
"version": "29.5.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
"integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"chalk": "^4.0.0", "chalk": "^4.0.0",
"diff-sequences": "^29.4.3", "diff-sequences": "^29.6.3",
"jest-get-type": "^29.4.3", "jest-get-type": "^29.6.3",
"pretty-format": "^29.5.0" "pretty-format": "^29.7.0"
}, },
"engines": { "engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0" "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
} }
}, },
"node_modules/jest-get-type": { "node_modules/jest-get-type": {
"version": "29.4.3", "version": "29.6.3",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
"integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0" "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
@ -2141,15 +2317,18 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "5.1.6", "version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.1"
}, },
"engines": { "engines": {
"node": ">=10" "node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/minimist": { "node_modules/minimist": {
@ -2160,6 +2339,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/minipass": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
"integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
"dev": true,
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/mixin-deep": { "node_modules/mixin-deep": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "git+ssh://git@github.com/cloudrac3r/mixin-deep.git#2dd70d6b8644263f7ed2c1620506c9eb3f11d32a", "resolved": "git+ssh://git@github.com/cloudrac3r/mixin-deep.git#2dd70d6b8644263f7ed2c1620506c9eb3f11d32a",
@ -2398,6 +2586,31 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true "dev": true
}, },
"node_modules/path-scurry": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
"integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
"dev": true,
"dependencies": {
"lru-cache": "^9.1.1 || ^10.0.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
"integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==",
"dev": true,
"engines": {
"node": "14 || >=16.14"
}
},
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.7", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
@ -2454,12 +2667,12 @@
"integrity": "sha512-dLbWOa4xBn+qeWeIF60qRoB6Pk2jX5P3DIVgOQyMyvBpu931Q+8dXz8X0snJiFkQdohDDLnZQECjzsAj75hgZQ==" "integrity": "sha512-dLbWOa4xBn+qeWeIF60qRoB6Pk2jX5P3DIVgOQyMyvBpu931Q+8dXz8X0snJiFkQdohDDLnZQECjzsAj75hgZQ=="
}, },
"node_modules/pretty-format": { "node_modules/pretty-format": {
"version": "29.5.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
"integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jest/schemas": "^29.4.3", "@jest/schemas": "^29.6.3",
"ansi-styles": "^5.0.0", "ansi-styles": "^5.0.0",
"react-is": "^18.0.0" "react-is": "^18.0.0"
}, },
@ -2652,12 +2865,12 @@
} }
}, },
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.2", "version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"is-core-module": "^2.11.0", "is-core-module": "^2.13.0",
"path-parse": "^1.0.7", "path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0" "supports-preserve-symlinks-flag": "^1.0.0"
}, },
@ -3103,6 +3316,42 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width/node_modules/ansi-regex": { "node_modules/string-width/node_modules/ansi-regex": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@ -3125,9 +3374,9 @@
} }
}, },
"node_modules/strip-ansi": { "node_modules/strip-ansi": {
"version": "7.0.1", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"ansi-regex": "^6.0.1" "ansi-regex": "^6.0.1"
@ -3139,6 +3388,28 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1" "url": "https://github.com/chalk/strip-ansi?sponsor=1"
} }
}, },
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/strip-json-comments": { "node_modules/strip-json-comments": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
@ -3164,25 +3435,26 @@
} }
}, },
"node_modules/supertape": { "node_modules/supertape": {
"version": "8.6.0", "version": "10.3.0",
"resolved": "https://registry.npmjs.org/supertape/-/supertape-8.6.0.tgz", "resolved": "https://registry.npmjs.org/supertape/-/supertape-10.3.0.tgz",
"integrity": "sha512-zvAXZaliVu8qpGRx5KiYQfPZcQD9B361lmRtXb3zyilpHHc0/5ygQ9MfWYZEwXywxHDfve3w8ZukI/NKPT9PyA==", "integrity": "sha512-z/HQbwB1+UMpmCQZXxxSU2zhzc+0yQq1Q9naCpyVDcRr2/cO/Bo3YhbSXJpEPVbvGy3BADl+vt5cfbXRJYFO0g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@cloudcmd/stub": "^4.0.0", "@cloudcmd/stub": "^4.0.0",
"@putout/cli-keypress": "^1.0.0", "@putout/cli-keypress": "^2.0.0",
"@putout/cli-validate-args": "^1.0.1", "@putout/cli-validate-args": "^1.0.1",
"@supertape/engine-loader": "^2.0.0", "@supertape/engine-loader": "^2.0.0",
"@supertape/formatter-fail": "^3.0.0", "@supertape/formatter-fail": "^3.0.0",
"@supertape/formatter-json-lines": "^2.0.0", "@supertape/formatter-json-lines": "^2.0.0",
"@supertape/formatter-progress-bar": "^4.0.0", "@supertape/formatter-progress-bar": "^5.0.0",
"@supertape/formatter-short": "^2.0.0", "@supertape/formatter-short": "^2.0.0",
"@supertape/formatter-tap": "^3.0.0", "@supertape/formatter-tap": "^3.0.0",
"@supertape/formatter-time": "^1.0.0",
"@supertape/operator-stub": "^3.0.0", "@supertape/operator-stub": "^3.0.0",
"cli-progress": "^3.8.2", "cli-progress": "^3.8.2",
"deep-equal": "^2.0.3", "deep-equal": "^2.0.3",
"fullstore": "^3.0.0", "fullstore": "^3.0.0",
"glob": "^8.0.3", "glob": "^10.3.10",
"jest-diff": "^29.0.1", "jest-diff": "^29.0.1",
"once": "^1.4.0", "once": "^1.4.0",
"resolve": "^1.17.0", "resolve": "^1.17.0",
@ -3193,11 +3465,11 @@
"yargs-parser": "^21.0.0" "yargs-parser": "^21.0.0"
}, },
"bin": { "bin": {
"supertape": "bin/supertape.mjs", "supertape": "bin/tracer.mjs",
"tape": "bin/supertape.mjs" "tape": "bin/tracer.mjs"
}, },
"engines": { "engines": {
"node": ">=16" "node": ">=18"
} }
}, },
"node_modules/supports-color": { "node_modules/supports-color": {
@ -3378,6 +3650,12 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/timer-node": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/timer-node/-/timer-node-5.0.7.tgz",
"integrity": "sha512-M1aP6ASmuVD0PSxl5fqjCAGY9WyND3DHZ8RwT5I8o7469XE53Lb5zbPai20Dhj7TProyaapfVj3TaT0P+LoSEA==",
"dev": true
},
"node_modules/toidentifier": { "node_modules/toidentifier": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@ -3602,6 +3880,45 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
} }
}, },
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi/node_modules/ansi-regex": { "node_modules/wrap-ansi/node_modules/ansi-regex": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",

View file

@ -44,15 +44,16 @@
"@types/node": "^18.16.0", "@types/node": "^18.16.0",
"@types/node-fetch": "^2.6.3", "@types/node-fetch": "^2.6.3",
"c8": "^8.0.1", "c8": "^8.0.1",
"colorette": "^1.4.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"discord-api-types": "^0.37.60", "discord-api-types": "^0.37.60",
"supertape": "^8.3.0", "supertape": "^10.3.0",
"tap-dot": "github:cloudrac3r/tap-dot#9dd7750ececeae3a96afba91905be812b6b2cc2d" "tap-dot": "github:cloudrac3r/tap-dot#9dd7750ececeae3a96afba91905be812b6b2cc2d"
}, },
"scripts": { "scripts": {
"addbot": "node addbot.js", "addbot": "node addbot.js",
"test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap test/test.js | tap-dot", "test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap test/test.js | tap-dot",
"test-slow": "cross-env FORCE_COLOR=true SUPERTAPE_TIMEOUT=6000 supertape --no-check-assertions-count --format tap test/test.js -- --slow | tap-dot", "test-slow": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js -- --slow | tap-dot",
"cover": "c8 --skip-full -x db/migrations -x matrix/file.js -x matrix/api.js -x matrix/mreq.js -r html -r text supertape --no-check-assertions-count --format fail test/test.js -- --slow" "cover": "c8 --skip-full -x db/migrations -x matrix/file.js -x matrix/api.js -x matrix/mreq.js -r html -r text supertape --no-check-assertions-count --format fail test/test.js -- --slow"
} }
} }

View file

@ -2,6 +2,7 @@
const fs = require("fs") const fs = require("fs")
const {join} = require("path") const {join} = require("path")
const stp = require("stream").promises
const sqlite = require("better-sqlite3") const sqlite = require("better-sqlite3")
const migrate = require("../db/migrate") const migrate = require("../db/migrate")
const HeatSync = require("heatsync") const HeatSync = require("heatsync")
@ -10,6 +11,7 @@ const data = require("./data")
/** @type {import("node-fetch").default} */ /** @type {import("node-fetch").default} */
// @ts-ignore // @ts-ignore
const fetch = require("node-fetch") const fetch = require("node-fetch")
const {green} = require("colorette")
const config = require("../config") const config = require("../config")
const passthrough = require("../passthrough") const passthrough = require("../passthrough")
@ -50,6 +52,48 @@ const file = sync.require("../matrix/file")
file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not allowed to upload files during testing.\nURL: ${url}`) } file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not allowed to upload files during testing.\nURL: ${url}`) }
;(async () => { ;(async () => {
/* c8 ignore start - maybe download some more test files in slow mode */
if (process.argv.includes("--slow")) {
test("test files: download", async t => {
/** @param {{url: string, to: string}[]} files */
async function allReporter(files) {
return new Promise(resolve => {
let resolved = 0
const report = files.map(file => file.to.split("/").slice(-1)[0][0])
files.map(download).forEach((p, i) => {
p.then(() => {
report[i] = green(".")
process.stderr.write("\r" + report.join(""))
if (++resolved === files.length) resolve(null)
})
})
})
}
async function download({url, to}) {
if (await fs.existsSync(to)) return
const res = await fetch(url)
await stp.pipeline(res.body, fs.createWriteStream(to, {encoding: "binary"}))
}
await allReporter([
{url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/RLMgJGfgTPjIQtvvWZsYjhjy", to: "test/res/RLMgJGfgTPjIQtvvWZsYjhjy.png"},
{url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/bZFuuUSEebJYXUMSxuuSuLTa", to: "test/res/bZFuuUSEebJYXUMSxuuSuLTa.png"},
{url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/qWmbXeRspZRLPcjseyLmeyXC", to: "test/res/qWmbXeRspZRLPcjseyLmeyXC.png"},
{url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/wcouHVjbKJJYajkhJLsyeJAA", to: "test/res/wcouHVjbKJJYajkhJLsyeJAA.png"},
{url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/WbYqNlACRuicynBfdnPYtmvc", to: "test/res/WbYqNlACRuicynBfdnPYtmvc.gif"},
{url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/HYcztccFIPgevDvoaWNsEtGJ", to: "test/res/HYcztccFIPgevDvoaWNsEtGJ.png"},
{url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/lHfmJpzgoNyNtYHdAmBHxXix", to: "test/res/lHfmJpzgoNyNtYHdAmBHxXix.png"},
{url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/MtRdXixoKjKKOyHJGWLsWLNU", to: "test/res/MtRdXixoKjKKOyHJGWLsWLNU.png"},
{url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/HXfFuougamkURPPMflTJRxGc", to: "test/res/HXfFuougamkURPPMflTJRxGc.png"},
{url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/ikYKbkhGhMERAuPPbsnQzZiX", to: "test/res/ikYKbkhGhMERAuPPbsnQzZiX.png"},
{url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/AYPpqXzVJvZdzMQJGjioIQBZ", to: "test/res/AYPpqXzVJvZdzMQJGjioIQBZ.png"},
{url: "https://matrix.cadence.moe/_matrix/media/r0/download/cadence.moe/UVuzvpVUhqjiueMxYXJiFEAj", to: "test/res/UVuzvpVUhqjiueMxYXJiFEAj.png"},
{url: "https://ezgif.com/images/format-demo/butterfly.gif", to: "test/res/butterfly.gif"},
{url: "https://ezgif.com/images/format-demo/butterfly.gif", to: "test/res/butterfly.png"},
])
}, {timeout: 60000})
}
/* c8 ignore end */
const p = migrate.migrate(db) const p = migrate.migrate(db)
test("migrate: migration works", async t => { test("migrate: migration works", async t => {
await p await p
@ -64,26 +108,6 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
db.exec(fs.readFileSync(join(__dirname, "ooye-test-data.sql"), "utf8")) db.exec(fs.readFileSync(join(__dirname, "ooye-test-data.sql"), "utf8"))
/* c8 ignore start - maybe download some more test files in slow mode */
if (process.argv.includes("--slow")) {
test("test files: download", async t => {
function download(url, to) {
return new Promise(async resolve => {
if (fs.existsSync(to)) return resolve(null)
const res = await fetch(url)
res.body.pipe(fs.createWriteStream(to, {encoding: "binary"}))
res.body.once("finish", resolve)
})
}
await Promise.all([
download("https://ezgif.com/images/format-demo/butterfly.png", "test/res/butterfly.png"),
download("https://ezgif.com/images/format-demo/butterfly.gif", "test/res/butterfly.gif")
])
t.pass("downloaded")
})
}
/* c8 ignore end */
require("../db/orm.test") require("../db/orm.test")
require("../discord/utils.test") require("../discord/utils.test")
require("../matrix/kstate.test") require("../matrix/kstate.test")