Add support for Lottie stickers
This commit is contained in:
		
							parent
							
								
									5bf051c624
								
							
						
					
					
						commit
						d759b5bd90
					
				
					 8 changed files with 115 additions and 10 deletions
				
			
		
							
								
								
									
										74
									
								
								d2m/converters/lottie.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								d2m/converters/lottie.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,74 @@
 | 
				
			||||||
 | 
					// @ts-check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DiscordTypes = require("discord-api-types/v10")
 | 
				
			||||||
 | 
					const Ty = require("../../types")
 | 
				
			||||||
 | 
					const assert = require("assert").strict
 | 
				
			||||||
 | 
					const {PNG} = require("pngjs")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const passthrough = require("../../passthrough")
 | 
				
			||||||
 | 
					const { sync, db, discord } = 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 INFO = {
 | 
				
			||||||
 | 
						mimetype: "image/png",
 | 
				
			||||||
 | 
						w: SIZE,
 | 
				
			||||||
 | 
						h: SIZE
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @typedef RlottieWasm
 | 
				
			||||||
 | 
					 * @prop {(string) => boolean} load load lottie data from string of json
 | 
				
			||||||
 | 
					 * @prop {() => number} frames get number of frames
 | 
				
			||||||
 | 
					 * @prop {(frameCount: number, width: number, height: number) => Uint8Array} render render lottie data to bitmap
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Rlottie = (async () => {
 | 
				
			||||||
 | 
						const Rlottie = require("./rlottie-wasm.js")
 | 
				
			||||||
 | 
						await new Promise(resolve => Rlottie.onRuntimeInitialized = resolve)
 | 
				
			||||||
 | 
						return Rlottie
 | 
				
			||||||
 | 
					})()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @param {DiscordTypes.APIStickerItem} stickerItem
 | 
				
			||||||
 | 
					 * @returns {Promise<{mxc: string, info: typeof INFO}>}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					async function convert(stickerItem) {
 | 
				
			||||||
 | 
						const existingMxc = db.prepare("SELECT mxc FROM lottie WHERE id = ?").pluck().get(stickerItem.id)
 | 
				
			||||||
 | 
						if (existingMxc) return {mxc: existingMxc, info: INFO}
 | 
				
			||||||
 | 
						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 */
 | 
				
			||||||
 | 
						const rh = new r.RlottieWasm()
 | 
				
			||||||
 | 
						const status = rh.load(text)
 | 
				
			||||||
 | 
						if (!status) throw new Error(`Rlottie unable to load ${text.length} byte data file.`)
 | 
				
			||||||
 | 
						const rendered = rh.render(0, SIZE, SIZE)
 | 
				
			||||||
 | 
						let png = new PNG({
 | 
				
			||||||
 | 
							width: SIZE,
 | 
				
			||||||
 | 
							height: SIZE,
 | 
				
			||||||
 | 
							bitDepth: 8, // 8 red + 8 green + 8 blue + 8 alpha
 | 
				
			||||||
 | 
							colorType: 6, // RGBA
 | 
				
			||||||
 | 
							inputColorType: 6, // RGBA
 | 
				
			||||||
 | 
							inputHasAlpha: true,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						png.data = Buffer.from(rendered)
 | 
				
			||||||
 | 
						// @ts-ignore wrong type from pngjs
 | 
				
			||||||
 | 
						const readablePng = png.pack()
 | 
				
			||||||
 | 
						/** @type {Ty.R.FileUploaded} */
 | 
				
			||||||
 | 
						const root = await mreq.mreq("POST", "/media/v3/upload", readablePng, {
 | 
				
			||||||
 | 
							headers: {
 | 
				
			||||||
 | 
								"Content-Type": INFO.mimetype
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						assert(root.content_uri)
 | 
				
			||||||
 | 
						db.prepare("INSERT INTO lottie (id, mxc) VALUES (?, ?)").run(stickerItem.id, root.content_uri)
 | 
				
			||||||
 | 
						return {mxc: root.content_uri, info: INFO}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports.convert = convert
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,8 @@ const passthrough = require("../../passthrough")
 | 
				
			||||||
const { sync, db, discord } = passthrough
 | 
					const { sync, db, discord } = passthrough
 | 
				
			||||||
/** @type {import("../../matrix/file")} */
 | 
					/** @type {import("../../matrix/file")} */
 | 
				
			||||||
const file = sync.require("../../matrix/file")
 | 
					const file = sync.require("../../matrix/file")
 | 
				
			||||||
 | 
					/** @type {import("./lottie")} */
 | 
				
			||||||
 | 
					const lottie = sync.require("./lottie")
 | 
				
			||||||
const reg = require("../../matrix/read-registration")
 | 
					const reg = require("../../matrix/read-registration")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex))
 | 
					const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex))
 | 
				
			||||||
| 
						 | 
					@ -338,7 +340,25 @@ async function messageToEvent(message, guild, options = {}, di) {
 | 
				
			||||||
	if (message.sticker_items) {
 | 
						if (message.sticker_items) {
 | 
				
			||||||
		const stickerEvents = await Promise.all(message.sticker_items.map(async stickerItem => {
 | 
							const stickerEvents = await Promise.all(message.sticker_items.map(async stickerItem => {
 | 
				
			||||||
			const format = file.stickerFormat.get(stickerItem.format_type)
 | 
								const format = file.stickerFormat.get(stickerItem.format_type)
 | 
				
			||||||
			if (format?.mime) {
 | 
								if (format?.mime === "lottie") {
 | 
				
			||||||
 | 
									try {
 | 
				
			||||||
 | 
										const {mxc, info} = await lottie.convert(stickerItem)
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											$type: "m.sticker",
 | 
				
			||||||
 | 
											"m.mentions": mentions,
 | 
				
			||||||
 | 
											body: stickerItem.name,
 | 
				
			||||||
 | 
											info,
 | 
				
			||||||
 | 
											url: mxc
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									} catch (e) {
 | 
				
			||||||
 | 
										return {
 | 
				
			||||||
 | 
											$type: "m.room.message",
 | 
				
			||||||
 | 
											"m.mentions": mentions,
 | 
				
			||||||
 | 
											msgtype: "m.notice",
 | 
				
			||||||
 | 
											body: `Failed to convert Lottie sticker:\n${e.toString()}\n${e.stack}`
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else if (format?.mime) {
 | 
				
			||||||
				let body = stickerItem.name
 | 
									let body = stickerItem.name
 | 
				
			||||||
				const sticker = guild.stickers.find(sticker => sticker.id === stickerItem.id)
 | 
									const sticker = guild.stickers.find(sticker => sticker.id === stickerItem.id)
 | 
				
			||||||
				if (sticker && sticker.description) body += ` - ${sticker.description}`
 | 
									if (sticker && sticker.description) body += ` - ${sticker.description}`
 | 
				
			||||||
| 
						 | 
					@ -351,13 +371,12 @@ async function messageToEvent(message, guild, options = {}, di) {
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
					url: await file.uploadDiscordFileToMxc(file.sticker(stickerItem))
 | 
										url: await file.uploadDiscordFileToMxc(file.sticker(stickerItem))
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			} else {
 | 
								}
 | 
				
			||||||
				return {
 | 
								return {
 | 
				
			||||||
					$type: "m.room.message",
 | 
									$type: "m.room.message",
 | 
				
			||||||
					"m.mentions": mentions,
 | 
									"m.mentions": mentions,
 | 
				
			||||||
					msgtype: "m.text",
 | 
									msgtype: "m.notice",
 | 
				
			||||||
					body: "Unsupported sticker format. Name: " + stickerItem.name
 | 
									body: `Unsupported sticker format ${format?.mime}. Name: ${stickerItem.name}`
 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}))
 | 
							}))
 | 
				
			||||||
		events.push(...stickerEvents)
 | 
							events.push(...stickerEvents)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								d2m/converters/rlottie-wasm.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								d2m/converters/rlottie-wasm.js
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								d2m/converters/rlottie-wasm.wasm
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								d2m/converters/rlottie-wasm.wasm
									
										
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
						 | 
					@ -87,7 +87,7 @@ function emoji(emojiID, animated) {
 | 
				
			||||||
const stickerFormat = new Map([
 | 
					const stickerFormat = new Map([
 | 
				
			||||||
	[1, {label: "PNG", ext: "png", mime: "image/png"}],
 | 
						[1, {label: "PNG", ext: "png", mime: "image/png"}],
 | 
				
			||||||
	[2, {label: "APNG", ext: "png", mime: "image/apng"}],
 | 
						[2, {label: "APNG", ext: "png", mime: "image/apng"}],
 | 
				
			||||||
	[3, {label: "LOTTIE", ext: "json", mime: null}],
 | 
						[3, {label: "LOTTIE", ext: "json", mime: "lottie"}],
 | 
				
			||||||
	[4, {label: "GIF", ext: "gif", mime: "image/gif"}]
 | 
						[4, {label: "GIF", ext: "gif", mime: "image/gif"}]
 | 
				
			||||||
])
 | 
					])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										9
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										9
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							| 
						 | 
					@ -19,6 +19,7 @@
 | 
				
			||||||
        "matrix-appservice": "^2.0.0",
 | 
					        "matrix-appservice": "^2.0.0",
 | 
				
			||||||
        "mixin-deep": "github:cloudrac3r/mixin-deep#v3.0.0",
 | 
					        "mixin-deep": "github:cloudrac3r/mixin-deep#v3.0.0",
 | 
				
			||||||
        "node-fetch": "^2.6.7",
 | 
					        "node-fetch": "^2.6.7",
 | 
				
			||||||
 | 
					        "pngjs": "^7.0.0",
 | 
				
			||||||
        "prettier-bytes": "^1.0.4",
 | 
					        "prettier-bytes": "^1.0.4",
 | 
				
			||||||
        "snowtransfer": "^0.8.0",
 | 
					        "snowtransfer": "^0.8.0",
 | 
				
			||||||
        "try-to-catch": "^3.0.1",
 | 
					        "try-to-catch": "^3.0.1",
 | 
				
			||||||
| 
						 | 
					@ -2281,6 +2282,14 @@
 | 
				
			||||||
      "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",
 | 
				
			||||||
      "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
 | 
					      "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/pngjs": {
 | 
				
			||||||
 | 
					      "version": "7.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=14.19.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/prebuild-install": {
 | 
					    "node_modules/prebuild-install": {
 | 
				
			||||||
      "version": "7.1.1",
 | 
					      "version": "7.1.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,6 +25,7 @@
 | 
				
			||||||
    "matrix-appservice": "^2.0.0",
 | 
					    "matrix-appservice": "^2.0.0",
 | 
				
			||||||
    "mixin-deep": "github:cloudrac3r/mixin-deep#v3.0.0",
 | 
					    "mixin-deep": "github:cloudrac3r/mixin-deep#v3.0.0",
 | 
				
			||||||
    "node-fetch": "^2.6.7",
 | 
					    "node-fetch": "^2.6.7",
 | 
				
			||||||
 | 
					    "pngjs": "^7.0.0",
 | 
				
			||||||
    "prettier-bytes": "^1.0.4",
 | 
					    "prettier-bytes": "^1.0.4",
 | 
				
			||||||
    "snowtransfer": "^0.8.0",
 | 
					    "snowtransfer": "^0.8.0",
 | 
				
			||||||
    "try-to-catch": "^3.0.1",
 | 
					    "try-to-catch": "^3.0.1",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@ Most features you'd expect in both directions, plus a little extra spice:
 | 
				
			||||||
* Mentions
 | 
					* Mentions
 | 
				
			||||||
* Replies
 | 
					* Replies
 | 
				
			||||||
* Threads
 | 
					* Threads
 | 
				
			||||||
* Stickers
 | 
					* Stickers (all formats: PNG, APNG, GIF, and Lottie)
 | 
				
			||||||
* Attachments
 | 
					* Attachments
 | 
				
			||||||
* Spoiler attachments
 | 
					* Spoiler attachments
 | 
				
			||||||
* Guild-Space details syncing
 | 
					* Guild-Space details syncing
 | 
				
			||||||
| 
						 | 
					@ -124,6 +124,7 @@ I recommend developing in Visual Studio Code so that the JSDoc x TypeScript anno
 | 
				
			||||||
* (70) matrix-appservice: I wish it didn't pull in express :(
 | 
					* (70) matrix-appservice: I wish it didn't pull in express :(
 | 
				
			||||||
* (0) mixin-deep: This is my fork! It fixes a bug in regular mixin-deep.
 | 
					* (0) mixin-deep: This is my fork! It fixes a bug in regular mixin-deep.
 | 
				
			||||||
* (3) node-fetch@2: I like it and it does what I want.
 | 
					* (3) node-fetch@2: I like it and it does what I want.
 | 
				
			||||||
 | 
					* (0) pngjs: Lottie stickers are converted to bitmaps with the vendored Rlottie WASM build, then the bitmaps are converted to PNG with pngjs.
 | 
				
			||||||
* (0) prettier-bytes: It does what I want and has no dependencies.
 | 
					* (0) prettier-bytes: It does what I want and has no dependencies.
 | 
				
			||||||
* (0) try-to-catch: Not strictly necessary, but it does what I want and has no dependencies.
 | 
					* (0) try-to-catch: Not strictly necessary, but it does what I want and has no dependencies.
 | 
				
			||||||
* (1) turndown: I need an HTML-to-Markdown converter and this one looked suitable enough. It has some bugs that I've worked around, so I might switch away from it later.
 | 
					* (1) turndown: I need an HTML-to-Markdown converter and this one looked suitable enough. It has some bugs that I've worked around, so I might switch away from it later.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue