Compare commits
2 commits
8987107685
...
011889216b
Author | SHA1 | Date | |
---|---|---|---|
011889216b | |||
64beb6c996 |
10 changed files with 152 additions and 37 deletions
53
d2m/actions/lottie.js
Normal file
53
d2m/actions/lottie.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const DiscordTypes = require("discord-api-types/v10")
|
||||||
|
const Ty = require("../../types")
|
||||||
|
const assert = require("assert").strict
|
||||||
|
|
||||||
|
const passthrough = require("../../passthrough")
|
||||||
|
const {sync, db, select} = passthrough
|
||||||
|
/** @type {import("../../matrix/file")} */
|
||||||
|
const file = sync.require("../../matrix/file")
|
||||||
|
/** @type {import("../../matrix/mreq")} */
|
||||||
|
const mreq = sync.require("../../matrix/mreq")
|
||||||
|
/** @type {import("../converters/lottie")} */
|
||||||
|
const convertLottie = sync.require("../converters/lottie")
|
||||||
|
|
||||||
|
const INFO = {
|
||||||
|
mimetype: "image/png",
|
||||||
|
w: convertLottie.SIZE,
|
||||||
|
h: convertLottie.SIZE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DiscordTypes.APIStickerItem} stickerItem
|
||||||
|
* @returns {Promise<{mxc_url: string, info: typeof INFO}>}
|
||||||
|
*/
|
||||||
|
async function convert(stickerItem) {
|
||||||
|
// Reuse sticker if already converted and uploaded
|
||||||
|
const existingMxc = select("lottie", "mxc_url", {sticker_id: stickerItem.id}).pluck().get()
|
||||||
|
if (existingMxc) return {mxc_url: existingMxc, info: INFO}
|
||||||
|
|
||||||
|
// Fetch sticker data from Discord
|
||||||
|
const res = await fetch(file.DISCORD_IMAGES_BASE + file.sticker(stickerItem))
|
||||||
|
if (res.status !== 200) throw new Error("Sticker data file not found.")
|
||||||
|
const text = await res.text()
|
||||||
|
|
||||||
|
// Convert to PNG (readable stream)
|
||||||
|
const readablePng = await convertLottie.convert(text)
|
||||||
|
|
||||||
|
// Upload to MXC
|
||||||
|
/** @type {Ty.R.FileUploaded} */
|
||||||
|
const root = await mreq.mreq("POST", "/media/v3/upload", readablePng, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": INFO.mimetype
|
||||||
|
}
|
||||||
|
})
|
||||||
|
assert(root.content_uri)
|
||||||
|
|
||||||
|
// Save the link for next time
|
||||||
|
db.prepare("INSERT INTO lottie (sticker_id, mxc_url) VALUES (?, ?)").run(stickerItem.id, root.content_uri)
|
||||||
|
return {mxc_url: root.content_uri, info: INFO}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.convert = convert
|
|
@ -1,25 +1,10 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
const DiscordTypes = require("discord-api-types/v10")
|
const stream = require("stream")
|
||||||
const Ty = require("../../types")
|
|
||||||
const assert = require("assert").strict
|
|
||||||
const {PNG} = require("pngjs")
|
const {PNG} = require("pngjs")
|
||||||
|
|
||||||
const passthrough = require("../../passthrough")
|
|
||||||
const {sync, db, discord, select} = passthrough
|
|
||||||
/** @type {import("../../matrix/file")} */
|
|
||||||
const file = sync.require("../../matrix/file")
|
|
||||||
//** @type {import("../../matrix/mreq")} */
|
|
||||||
const mreq = sync.require("../../matrix/mreq")
|
|
||||||
|
|
||||||
const SIZE = 160 // Discord's display size on 1x displays is 160
|
const SIZE = 160 // Discord's display size on 1x displays is 160
|
||||||
|
|
||||||
const INFO = {
|
|
||||||
mimetype: "image/png",
|
|
||||||
w: SIZE,
|
|
||||||
h: SIZE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef RlottieWasm
|
* @typedef RlottieWasm
|
||||||
* @prop {(string) => boolean} load load lottie data from string of json
|
* @prop {(string) => boolean} load load lottie data from string of json
|
||||||
|
@ -34,16 +19,11 @@ const Rlottie = (async () => {
|
||||||
})()
|
})()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {DiscordTypes.APIStickerItem} stickerItem
|
* @param {string} text
|
||||||
* @returns {Promise<{mxc_url: string, info: typeof INFO}>}
|
* @returns {Promise<import("stream").Readable>}
|
||||||
*/
|
*/
|
||||||
async function convert(stickerItem) {
|
async function convert(text) {
|
||||||
const existingMxc = select("lottie", "mxc_url", {sticker_id: stickerItem.id}).pluck().get()
|
|
||||||
if (existingMxc) return {mxc_url: existingMxc, info: INFO}
|
|
||||||
const r = await Rlottie
|
const r = await Rlottie
|
||||||
const res = await fetch(file.DISCORD_IMAGES_BASE + file.sticker(stickerItem))
|
|
||||||
if (res.status !== 200) throw new Error("Sticker data file not found.")
|
|
||||||
const text = await res.text()
|
|
||||||
/** @type RlottieWasm */
|
/** @type RlottieWasm */
|
||||||
const rh = new r.RlottieWasm()
|
const rh = new r.RlottieWasm()
|
||||||
const status = rh.load(text)
|
const status = rh.load(text)
|
||||||
|
@ -58,17 +38,12 @@ async function convert(stickerItem) {
|
||||||
inputHasAlpha: true,
|
inputHasAlpha: true,
|
||||||
})
|
})
|
||||||
png.data = Buffer.from(rendered)
|
png.data = Buffer.from(rendered)
|
||||||
// @ts-ignore wrong type from pngjs
|
// The transform stream is necessary because PNG requires me to pipe it somewhere before this event loop ends
|
||||||
const readablePng = png.pack()
|
const resultStream = png.pack()
|
||||||
/** @type {Ty.R.FileUploaded} */
|
const p = new stream.PassThrough()
|
||||||
const root = await mreq.mreq("POST", "/media/v3/upload", readablePng, {
|
resultStream.pipe(p)
|
||||||
headers: {
|
return p
|
||||||
"Content-Type": INFO.mimetype
|
|
||||||
}
|
|
||||||
})
|
|
||||||
assert(root.content_uri)
|
|
||||||
db.prepare("INSERT INTO lottie (sticker_id, mxc_url) VALUES (?, ?)").run(stickerItem.id, root.content_uri)
|
|
||||||
return {mxc_url: root.content_uri, info: INFO}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.convert = convert
|
module.exports.convert = convert
|
||||||
|
module.exports.SIZE = SIZE
|
||||||
|
|
34
d2m/converters/lottie.test.js
Normal file
34
d2m/converters/lottie.test.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const fs = require("fs")
|
||||||
|
const stream = require("stream")
|
||||||
|
const {test} = require("supertape")
|
||||||
|
const {convert} = require("./lottie")
|
||||||
|
|
||||||
|
const WRITE_PNG = false
|
||||||
|
|
||||||
|
test("lottie: can convert and save PNG", async t => {
|
||||||
|
const input = await fs.promises.readFile("test/res/lottie-bee.json", "utf8")
|
||||||
|
const resultStream = await convert(input)
|
||||||
|
/* c8 ignore next 3 */
|
||||||
|
if (WRITE_PNG) {
|
||||||
|
resultStream.pipe(fs.createWriteStream("test/res/lottie-bee.png"))
|
||||||
|
t.fail("PNG written to /test/res/lottie-bee.png, please manually check it")
|
||||||
|
} else {
|
||||||
|
const expected = await fs.promises.readFile("test/res/lottie-bee.png")
|
||||||
|
const actual = Buffer.alloc(expected.length)
|
||||||
|
let i = 0
|
||||||
|
await stream.promises.pipeline(
|
||||||
|
resultStream,
|
||||||
|
async function* (source) {
|
||||||
|
for await (const chunk of source) {
|
||||||
|
chunk.copy(actual, i)
|
||||||
|
i += chunk.length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new stream.PassThrough()
|
||||||
|
)
|
||||||
|
t.equal(i, actual.length, `allocated ${actual.length} bytes, but wrote ${i}`)
|
||||||
|
t.deepEqual(actual, expected)
|
||||||
|
}
|
||||||
|
})
|
|
@ -12,8 +12,8 @@ const {sync, db, discord, select, from} = passthrough
|
||||||
const file = sync.require("../../matrix/file")
|
const file = sync.require("../../matrix/file")
|
||||||
/** @type {import("./emoji-to-key")} */
|
/** @type {import("./emoji-to-key")} */
|
||||||
const emojiToKey = sync.require("./emoji-to-key")
|
const emojiToKey = sync.require("./emoji-to-key")
|
||||||
/** @type {import("./lottie")} */
|
/** @type {import("../actions/lottie")} */
|
||||||
const lottie = sync.require("./lottie")
|
const lottie = sync.require("../actions/lottie")
|
||||||
/** @type {import("../../m2d/converters/utils")} */
|
/** @type {import("../../m2d/converters/utils")} */
|
||||||
const mxUtils = sync.require("../../m2d/converters/utils")
|
const mxUtils = sync.require("../../m2d/converters/utils")
|
||||||
/** @type {import("../../discord/utils")} */
|
/** @type {import("../../discord/utils")} */
|
||||||
|
|
32
test/data.js
32
test/data.js
|
@ -1331,6 +1331,38 @@ module.exports = {
|
||||||
name: "pomu puff"
|
name: "pomu puff"
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
|
lottie_sticker: {
|
||||||
|
id: "1106366167788044450",
|
||||||
|
type: 0,
|
||||||
|
content: "",
|
||||||
|
channel_id: "122155380120748034",
|
||||||
|
author: {
|
||||||
|
id: "113340068197859328",
|
||||||
|
username: "Cookie 🍪",
|
||||||
|
global_name: null,
|
||||||
|
display_name: null,
|
||||||
|
avatar: "b48302623a12bc7c59a71328f72ccb39",
|
||||||
|
discriminator: "7766",
|
||||||
|
public_flags: 128,
|
||||||
|
avatar_decoration: null
|
||||||
|
},
|
||||||
|
attachments: [],
|
||||||
|
embeds: [],
|
||||||
|
mentions: [],
|
||||||
|
mention_roles: [],
|
||||||
|
pinned: false,
|
||||||
|
mention_everyone: false,
|
||||||
|
tts: false,
|
||||||
|
timestamp: "2023-05-11T23:44:09.690000+00:00",
|
||||||
|
edited_timestamp: null,
|
||||||
|
flags: 0,
|
||||||
|
components: [],
|
||||||
|
sticker_items: [{
|
||||||
|
id: "860171525772279849",
|
||||||
|
format_type: 3,
|
||||||
|
name: "8"
|
||||||
|
}]
|
||||||
|
},
|
||||||
message_in_thread: {
|
message_in_thread: {
|
||||||
type: 0,
|
type: 0,
|
||||||
tts: false,
|
tts: false,
|
||||||
|
|
|
@ -103,6 +103,9 @@ INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES
|
||||||
('!maggESguZBqGBZtSnr:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'),
|
('!maggESguZBqGBZtSnr:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'),
|
||||||
('!CzvdIdUQXgUjDVKxeU:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU');
|
('!CzvdIdUQXgUjDVKxeU:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU');
|
||||||
|
|
||||||
|
INSERT INTO lottie (sticker_id, mxc_url) VALUES
|
||||||
|
('860171525772279849', 'mxc://cadence.moe/ZtvvVbwMIdUZeovWVyGVFCeR');
|
||||||
|
|
||||||
INSERT INTO "auto_emoji" ("name","emoji_id","guild_id") VALUES
|
INSERT INTO "auto_emoji" ("name","emoji_id","guild_id") VALUES
|
||||||
('L1','1144820033948762203','529176156398682115'),
|
('L1','1144820033948762203','529176156398682115'),
|
||||||
('L2','1144820084079087647','529176156398682115'),
|
('L2','1144820084079087647','529176156398682115'),
|
||||||
|
|
1
test/res/lottie-bee.json
Normal file
1
test/res/lottie-bee.json
Normal file
File diff suppressed because one or more lines are too long
16
test/res/lottie-bee.license.txt
Normal file
16
test/res/lottie-bee.license.txt
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# Source
|
||||||
|
|
||||||
|
Flying Bee by Afif Ridwan
|
||||||
|
https://lottiefiles.com/animations/flying-bee-WkXvUiWkZ1
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
Lottie Simple License (FL 9.13.21) https://lottiefiles.com/page/license
|
||||||
|
|
||||||
|
Copyright © 2021 Design Barn Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of the public animation files available for download at the LottieFiles site (“Files”) to download, reproduce, modify, publish, distribute, publicly display, and publicly digitally perform such Files, including for commercial purposes, provided that any display, publication, performance, or distribution of Files must contain (and be subject to) the same terms and conditions of this license. Modifications to Files are deemed derivative works and must also be expressly distributed under the same terms and conditions of this license. You may not purport to impose any additional or different terms or conditions on, or apply any technical measures that restrict exercise of, the rights granted under this license. This license does not include the right to collect or compile Files from LottieFiles to replicate or develop a similar or competing service.
|
||||||
|
|
||||||
|
Use of Files without attributing the creator(s) of the Files is permitted under this license, though attribution is strongly encouraged. If attributions are included, such attributions should be visible to the end user.
|
||||||
|
|
||||||
|
FILES ARE PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL THE CREATOR(S) OF FILES OR DESIGN BARN, INC. BE LIABLE ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF SUCH FILES.
|
BIN
test/res/lottie-bee.png
Normal file
BIN
test/res/lottie-bee.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
|
@ -60,6 +60,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
|
||||||
require("../d2m/actions/register-user.test")
|
require("../d2m/actions/register-user.test")
|
||||||
require("../d2m/converters/edit-to-changes.test")
|
require("../d2m/converters/edit-to-changes.test")
|
||||||
require("../d2m/converters/emoji-to-key.test")
|
require("../d2m/converters/emoji-to-key.test")
|
||||||
|
require("../d2m/converters/lottie.test")
|
||||||
require("../d2m/converters/message-to-event.test")
|
require("../d2m/converters/message-to-event.test")
|
||||||
require("../d2m/converters/message-to-event.embeds.test")
|
require("../d2m/converters/message-to-event.embeds.test")
|
||||||
require("../d2m/converters/pins-to-list.test")
|
require("../d2m/converters/pins-to-list.test")
|
||||||
|
|
Loading…
Reference in a new issue