From 11bac255e7352feb1f4210f8de529f6cc58112f6 Mon Sep 17 00:00:00 2001 From: arelive Date: Tue, 6 Jun 2023 18:51:15 +0300 Subject: [PATCH 01/60] drawing quote with Playwright --- package-lock.json | 75 +++++++++++++++++++++++++++++++++++++++-- package.json | 2 ++ utils/quote-generate.js | 64 ++++++++++++++++++++++++++--------- utils/render.js | 11 ++++++ utils/views.js | 11 ++++++ views/quote.hbs | 52 ++++++++++++++++++++++++++++ 6 files changed, 197 insertions(+), 18 deletions(-) create mode 100644 utils/render.js create mode 100644 utils/views.js create mode 100644 views/quote.hbs diff --git a/package-lock.json b/package-lock.json index fb0b5cf..8703e1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "quote-api", - "version": "0.13.2", + "version": "0.13.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quote-api", - "version": "0.13.2", + "version": "0.13.17", "license": "MIT", "dependencies": { "canvas": "2.11.0", "dotenv": "^7.0.0", "emoji-db": "^14.0.1", + "handlebars": "^4.7.7", "jimp": "^0.16.1", "jsdom": "^16.5.3", "koa": "^2.11.0", @@ -24,6 +25,7 @@ "lottie-web": "^5.7.8", "lru-cache": "^5.1.1", "object-sizeof": "^1.6.0", + "playwright": "^1.34.3", "runes": "^0.4.3", "sharp": "^0.31.3", "smartcrop-sharp": "^2.0.7", @@ -2129,6 +2131,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -3121,6 +3143,11 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, "node_modules/node-abi": { "version": "3.34.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.34.0.tgz", @@ -3466,6 +3493,32 @@ "pixelmatch": "bin/pixelmatch" } }, + "node_modules/playwright": { + "version": "1.34.3", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.34.3.tgz", + "integrity": "sha512-UOOVE4ZbGfGkP1KVqWTdXOmm8Pw2pBhfbmlqKMkpiRCQjL5W+J+xRQXpgutFr0iM4pWl8g0GyyASMsqjQfFohw==", + "hasInstallScript": true, + "dependencies": { + "playwright-core": "1.34.3" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/playwright-core": { + "version": "1.34.3", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.3.tgz", + "integrity": "sha512-2pWd6G7OHKemc5x1r1rp8aQcpvDh7goMBZlJv6Co5vCNLVcQJdhxRL09SGaY6HcyHH9aT4tiynZabMofVasBYw==", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/pngjs": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", @@ -3996,7 +4049,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -4320,6 +4372,18 @@ "resolved": "https://registry.npmjs.org/typegram/-/typegram-3.7.0.tgz", "integrity": "sha512-IafMO+GRi5H8CtWSNihuD56Bjpmj/ISbg6G8jdTkNxldrym+FOPlo/fxtaPs/LyWnS0l1Bm18MUDwOikZSKmJw==" }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", @@ -4492,6 +4556,11 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index ae3afb5..ab43bf7 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "canvas": "2.11.0", "dotenv": "^7.0.0", "emoji-db": "^14.0.1", + "handlebars": "^4.7.7", "jimp": "^0.16.1", "jsdom": "^16.5.3", "koa": "^2.11.0", @@ -34,6 +35,7 @@ "lottie-web": "^5.7.8", "lru-cache": "^5.1.1", "object-sizeof": "^1.6.0", + "playwright": "^1.34.3", "runes": "^0.4.3", "sharp": "^0.31.3", "smartcrop-sharp": "^2.0.7", diff --git a/utils/quote-generate.js b/utils/quote-generate.js index 52dbfa3..4562006 100644 --- a/utils/quote-generate.js +++ b/utils/quote-generate.js @@ -1,5 +1,5 @@ const fs = require('fs') -const { createCanvas, registerFont } = require('canvas') +const { createCanvas, registerFont, Image } = require('canvas') const EmojiDbLib = require('emoji-db') const { loadImage } = require('canvas') const loadImageFromUrl = require('./image-load-url') @@ -11,6 +11,9 @@ const lottie = require('lottie-node') const zlib = require('zlib') const { Telegram } = require('telegraf') +const render = require('./render') +const { quote: view } = require('./views') + const emojiDb = new EmojiDbLib({ useDefaultDb: true }) function loadFont () { @@ -35,6 +38,7 @@ loadFont() const emojiImageByBrand = require('./emoji-image') const LRU = require('lru-cache') +const page = require('./render') const avatarCache = new LRU({ max: 20, @@ -692,11 +696,11 @@ class QuoteGenerate { const avatarX = 0 const avatarY = 0 - canvasCtx.beginPath() - canvasCtx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2, true) - canvasCtx.clip() - canvasCtx.closePath() - canvasCtx.restore() + // canvasCtx.beginPath() + // canvasCtx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2, true) + // canvasCtx.clip() + // canvasCtx.closePath() + // canvasCtx.restore() canvasCtx.drawImage(avatarImage, avatarX, avatarY, avatarSize, avatarSize) return canvas @@ -940,12 +944,8 @@ class QuoteGenerate { '#FF7FD5' // pink ] - console.log(Math.abs(message.from.id) % 7) - // user name color - let nameIndex = 1 - if (message.from.id) nameIndex = Math.abs(message.from.id) % 7 - + const nameIndex = message.from.id ? Math.abs(message.from.id) % 7 : 1 const nameColorArray = backStyle === 'light' ? nameColorLight : nameColorDark let nameColor = nameColorArray[nameIndex] @@ -958,6 +958,42 @@ class QuoteGenerate { nameColor = colorContrast.adjustContrast(this.colorLuminance(backgroundColorTwo, 0.55), nameColor) } + + const avatarImage = await this.drawAvatar(message.from) + + const content = view({ + width, + height, + scale, + theme: backStyle, + nameColor, + name: message?.from?.name ?? '', + emojiStatus: message?.from?.emoji_status ?? '', + avatarURL: avatarImage.toDataURL(), + text: message.text ?? '', + }) + + const page = await render(content) + const image = await page.locator('#quote').screenshot() + + // convert to canvas for the generator + const canvas = createCanvas(width, height) + const canvasCtx = canvas.getContext('2d') + const img = new Image() + + return new Promise((resolve, reject) => { + img.onload = () => { + canvasCtx.drawImage(img, 0, 0, width, height) + resolve(canvas) + } + img.onerror = console.error + img.src = image + }) + + + // deprecated + // TODO: completely replace, test, remove + const nameSize = 22 * scale let nameCanvas @@ -996,10 +1032,8 @@ class QuoteGenerate { ) } - let fontSize = 24 * scale - - let textColor = '#fff' - if (backStyle === 'light') textColor = '#000' + const fontSize = 24 * scale + const textColor = backStyle == 'light' ? '#000' : '#fff' let textCanvas if (message.text) { diff --git a/utils/render.js b/utils/render.js new file mode 100644 index 0000000..3fbe2e9 --- /dev/null +++ b/utils/render.js @@ -0,0 +1,11 @@ +const { webkit } = require('playwright') + +const promise = webkit.launch() + .then(browser => browser.newContext()) + .then(context => context.newPage()) + +module.exports = async (content) => { + const page = await promise + await page.setContent(content) + return page +} diff --git a/utils/views.js b/utils/views.js new file mode 100644 index 0000000..3c515d3 --- /dev/null +++ b/utils/views.js @@ -0,0 +1,11 @@ +const path = require('path') +const fs = require('fs') +const Handlebars = require('handlebars') + +const quote = Handlebars.compile( + fs.readFileSync(path.resolve('./views/quote.hbs')).toString('utf-8') +) + +module.exports = { + quote +} diff --git a/views/quote.hbs b/views/quote.hbs new file mode 100644 index 0000000..eca43d3 --- /dev/null +++ b/views/quote.hbs @@ -0,0 +1,52 @@ + + + + + + + +
+ +

+ {{name}} + {{emojiStatus}} +

+

{{text}}

+
+ + From 95575105d89d3a53e0919331e011cf7f6d3022e5 Mon Sep 17 00:00:00 2001 From: arelive Date: Tue, 6 Jun 2023 18:51:15 +0300 Subject: [PATCH 02/60] drawing quote with Playwright --- package-lock.json | 75 +++++++++++++++++++++++++++++++++++++++-- package.json | 2 ++ utils/quote-generate.js | 63 +++++++++++++++++++++++++--------- utils/render.js | 11 ++++++ utils/views.js | 11 ++++++ views/quote.hbs | 52 ++++++++++++++++++++++++++++ 6 files changed, 196 insertions(+), 18 deletions(-) create mode 100644 utils/render.js create mode 100644 utils/views.js create mode 100644 views/quote.hbs diff --git a/package-lock.json b/package-lock.json index fb0b5cf..8703e1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "quote-api", - "version": "0.13.2", + "version": "0.13.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quote-api", - "version": "0.13.2", + "version": "0.13.17", "license": "MIT", "dependencies": { "canvas": "2.11.0", "dotenv": "^7.0.0", "emoji-db": "^14.0.1", + "handlebars": "^4.7.7", "jimp": "^0.16.1", "jsdom": "^16.5.3", "koa": "^2.11.0", @@ -24,6 +25,7 @@ "lottie-web": "^5.7.8", "lru-cache": "^5.1.1", "object-sizeof": "^1.6.0", + "playwright": "^1.34.3", "runes": "^0.4.3", "sharp": "^0.31.3", "smartcrop-sharp": "^2.0.7", @@ -2129,6 +2131,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -3121,6 +3143,11 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, "node_modules/node-abi": { "version": "3.34.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.34.0.tgz", @@ -3466,6 +3493,32 @@ "pixelmatch": "bin/pixelmatch" } }, + "node_modules/playwright": { + "version": "1.34.3", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.34.3.tgz", + "integrity": "sha512-UOOVE4ZbGfGkP1KVqWTdXOmm8Pw2pBhfbmlqKMkpiRCQjL5W+J+xRQXpgutFr0iM4pWl8g0GyyASMsqjQfFohw==", + "hasInstallScript": true, + "dependencies": { + "playwright-core": "1.34.3" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/playwright-core": { + "version": "1.34.3", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.3.tgz", + "integrity": "sha512-2pWd6G7OHKemc5x1r1rp8aQcpvDh7goMBZlJv6Co5vCNLVcQJdhxRL09SGaY6HcyHH9aT4tiynZabMofVasBYw==", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/pngjs": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", @@ -3996,7 +4049,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -4320,6 +4372,18 @@ "resolved": "https://registry.npmjs.org/typegram/-/typegram-3.7.0.tgz", "integrity": "sha512-IafMO+GRi5H8CtWSNihuD56Bjpmj/ISbg6G8jdTkNxldrym+FOPlo/fxtaPs/LyWnS0l1Bm18MUDwOikZSKmJw==" }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", @@ -4492,6 +4556,11 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index ae3afb5..ab43bf7 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "canvas": "2.11.0", "dotenv": "^7.0.0", "emoji-db": "^14.0.1", + "handlebars": "^4.7.7", "jimp": "^0.16.1", "jsdom": "^16.5.3", "koa": "^2.11.0", @@ -34,6 +35,7 @@ "lottie-web": "^5.7.8", "lru-cache": "^5.1.1", "object-sizeof": "^1.6.0", + "playwright": "^1.34.3", "runes": "^0.4.3", "sharp": "^0.31.3", "smartcrop-sharp": "^2.0.7", diff --git a/utils/quote-generate.js b/utils/quote-generate.js index 52dbfa3..317e41a 100644 --- a/utils/quote-generate.js +++ b/utils/quote-generate.js @@ -1,5 +1,5 @@ const fs = require('fs') -const { createCanvas, registerFont } = require('canvas') +const { createCanvas, registerFont, Image } = require('canvas') const EmojiDbLib = require('emoji-db') const { loadImage } = require('canvas') const loadImageFromUrl = require('./image-load-url') @@ -11,6 +11,9 @@ const lottie = require('lottie-node') const zlib = require('zlib') const { Telegram } = require('telegraf') +const render = require('./render') +const { quote: view } = require('./views') + const emojiDb = new EmojiDbLib({ useDefaultDb: true }) function loadFont () { @@ -692,11 +695,11 @@ class QuoteGenerate { const avatarX = 0 const avatarY = 0 - canvasCtx.beginPath() - canvasCtx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2, true) - canvasCtx.clip() - canvasCtx.closePath() - canvasCtx.restore() + // canvasCtx.beginPath() + // canvasCtx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2, true) + // canvasCtx.clip() + // canvasCtx.closePath() + // canvasCtx.restore() canvasCtx.drawImage(avatarImage, avatarX, avatarY, avatarSize, avatarSize) return canvas @@ -940,12 +943,8 @@ class QuoteGenerate { '#FF7FD5' // pink ] - console.log(Math.abs(message.from.id) % 7) - // user name color - let nameIndex = 1 - if (message.from.id) nameIndex = Math.abs(message.from.id) % 7 - + const nameIndex = message.from.id ? Math.abs(message.from.id) % 7 : 1 const nameColorArray = backStyle === 'light' ? nameColorLight : nameColorDark let nameColor = nameColorArray[nameIndex] @@ -958,6 +957,42 @@ class QuoteGenerate { nameColor = colorContrast.adjustContrast(this.colorLuminance(backgroundColorTwo, 0.55), nameColor) } + + const avatarImage = await this.drawAvatar(message.from) + + const content = view({ + width, + height, + scale, + theme: backStyle, + nameColor, + name: message?.from?.name ?? '', + emojiStatus: message?.from?.emoji_status ?? '', + avatarURL: avatarImage.toDataURL(), + text: message.text ?? '', + }) + + const page = await render(content) + const image = await page.locator('#quote').screenshot() + + // convert to canvas for the generator + const canvas = createCanvas(width, height) + const canvasCtx = canvas.getContext('2d') + const img = new Image() + + return new Promise((resolve, reject) => { + img.onload = () => { + canvasCtx.drawImage(img, 0, 0, width, height) + resolve(canvas) + } + img.onerror = console.error + img.src = image + }) + + + // deprecated + // TODO: completely replace, test, remove + const nameSize = 22 * scale let nameCanvas @@ -996,10 +1031,8 @@ class QuoteGenerate { ) } - let fontSize = 24 * scale - - let textColor = '#fff' - if (backStyle === 'light') textColor = '#000' + const fontSize = 24 * scale + const textColor = backStyle == 'light' ? '#000' : '#fff' let textCanvas if (message.text) { diff --git a/utils/render.js b/utils/render.js new file mode 100644 index 0000000..3fbe2e9 --- /dev/null +++ b/utils/render.js @@ -0,0 +1,11 @@ +const { webkit } = require('playwright') + +const promise = webkit.launch() + .then(browser => browser.newContext()) + .then(context => context.newPage()) + +module.exports = async (content) => { + const page = await promise + await page.setContent(content) + return page +} diff --git a/utils/views.js b/utils/views.js new file mode 100644 index 0000000..3c515d3 --- /dev/null +++ b/utils/views.js @@ -0,0 +1,11 @@ +const path = require('path') +const fs = require('fs') +const Handlebars = require('handlebars') + +const quote = Handlebars.compile( + fs.readFileSync(path.resolve('./views/quote.hbs')).toString('utf-8') +) + +module.exports = { + quote +} diff --git a/views/quote.hbs b/views/quote.hbs new file mode 100644 index 0000000..1c6a1b9 --- /dev/null +++ b/views/quote.hbs @@ -0,0 +1,52 @@ + + + + + + + +
+ +

+ {{name}} + {{emojiStatus}} +

+

{{text}}

+
+ + From 09722a4bb44b3d4293eb378699164e90b8a5d945 Mon Sep 17 00:00:00 2001 From: arelive Date: Wed, 7 Jun 2023 13:04:42 +0300 Subject: [PATCH 03/60] optimize generation; add stress test --- methods/generate.js | 9 +++-- package-lock.json | 84 +++++++++++++++++++++++++++++++++++++++-- package.json | 7 +++- test/.gitignore | 1 + test/stress.js | 59 +++++++++++++++++++++++++++++ utils/quote-generate.js | 5 +-- 6 files changed, 154 insertions(+), 11 deletions(-) create mode 100644 test/.gitignore create mode 100644 test/stress.js diff --git a/methods/generate.js b/methods/generate.js index bd27063..780e5e8 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -3,6 +3,7 @@ const { } = require('../utils') const { createCanvas, loadImage } = require('canvas') const sharp = require('sharp') +const { Telegram } = require('telegraf') const normalizeColor = (color) => { const canvas = createCanvas(0, 0) @@ -46,14 +47,16 @@ const imageAlpha = (image, alpha) => { return canvas } +let telegramBot + module.exports = async (parm) => { // console.log(JSON.stringify(parm, null, 2)) if (!parm) return { error: 'query_empty' } if (!parm.messages || parm.messages.length < 1) return { error: 'messages_empty' } - let botToken = parm.botToken || process.env.BOT_TOKEN - - const quoteGenerate = new QuoteGenerate(botToken) + const botToken = parm.botToken || process.env.BOT_TOKEN + telegramBot = telegramBot || new Telegram(botToken) + const quoteGenerate = new QuoteGenerate(telegramBot) const quoteImages = [] diff --git a/package-lock.json b/package-lock.json index fb0b5cf..f95e42d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "quote-api", - "version": "0.13.2", + "version": "0.13.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quote-api", - "version": "0.13.2", + "version": "0.13.17", "license": "MIT", "dependencies": { "canvas": "2.11.0", @@ -30,12 +30,14 @@ "telegraf": "^3.38.0" }, "devDependencies": { + "axios": "^1.4.0", "eslint": "^8.6.0", "eslint-config-standard": "^12.0.0", "eslint-plugin-import": "^2.17.3", "eslint-plugin-node": "^9.1.0", "eslint-plugin-promise": "^4.1.1", - "eslint-plugin-standard": "^4.0.0" + "eslint-plugin-standard": "^4.0.0", + "lorem-ipsum": "^2.0.8" } }, "node_modules/@babel/runtime": { @@ -739,6 +741,31 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -980,6 +1007,15 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1959,6 +1995,26 @@ "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/form-data": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", @@ -2909,6 +2965,22 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lorem-ipsum": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/lorem-ipsum/-/lorem-ipsum-2.0.8.tgz", + "integrity": "sha512-5RIwHuCb979RASgCJH0VKERn9cQo/+NcAi2BMe9ddj+gp7hujl6BI+qdOG4nVsLDpwWEJwTVYXNKP6BGgbcoGA==", + "dev": true, + "dependencies": { + "commander": "^9.3.0" + }, + "bin": { + "lorem-ipsum": "dist/bin/lorem-ipsum.bin.js" + }, + "engines": { + "node": ">= 8.x", + "npm": ">= 5.x" + } + }, "node_modules/lottie-node": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lottie-node/-/lottie-node-2.0.0.tgz", @@ -3581,6 +3653,12 @@ "node": ">=0.4.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "node_modules/psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", diff --git a/package.json b/package.json index ae3afb5..614f8d9 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "start": "node index.js", "lint": "node_modules/.bin/eslint --ext js .", - "lint:fix": "node_modules/.bin/eslint --fix --ext js ." + "lint:fix": "node_modules/.bin/eslint --fix --ext js .", + "test": "node test/stress.js 1000" }, "repository": { "type": "git", @@ -40,11 +41,13 @@ "telegraf": "^3.38.0" }, "devDependencies": { + "axios": "^1.4.0", "eslint": "^8.6.0", "eslint-config-standard": "^12.0.0", "eslint-plugin-import": "^2.17.3", "eslint-plugin-node": "^9.1.0", "eslint-plugin-promise": "^4.1.1", - "eslint-plugin-standard": "^4.0.0" + "eslint-plugin-standard": "^4.0.0", + "lorem-ipsum": "^2.0.8" } } diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 0000000..e33609d --- /dev/null +++ b/test/.gitignore @@ -0,0 +1 @@ +*.png diff --git a/test/stress.js b/test/stress.js new file mode 100644 index 0000000..489ff43 --- /dev/null +++ b/test/stress.js @@ -0,0 +1,59 @@ +const axios = require('axios') +const fs = require('fs') +const LoremIpsum = require('lorem-ipsum').LoremIpsum +const path = require('path') + +require('dotenv').config({ path: './.env' }) +require('../app') + +const lorem = new LoremIpsum({ + sentencesPerParagraph: { + max: 8, + min: 1 + }, + wordsPerSentence: { + max: 16, + min: 4 + } +}) + +const nQuotes = parseInt(process.argv[2]) + +;(async () => { + for (let i = 0; i < nQuotes; i++) { + const text = lorem.generateParagraphs(1) + const username = lorem.generateWords(2) + const avatar = 'https://telegra.ph/file/59952c903fdfb10b752b3.jpg' + + const json = { + type: 'quote', + format: 'png', + backgroundColor: '#FFFFFF', + width: 512, + height: 768, + scale: 2, + messages: [ + { + entities: [], + avatar: true, + from: { + id: 1, + name: username, + photo: { + url: avatar + } + }, + text: text, + replyMessage: {} + } + ] + } + + await axios.post('http://localhost:3000/generate', json, { + headers: { 'Content-Type': 'application/json' } + }).then(res => { + const buffer = Buffer.from(res.data.result.image, 'base64') + fs.writeFile(path.resolve(`./test/${i}.png`), buffer, (err) => err && console.error(err)) + }).catch(console.error) + } +})() diff --git a/utils/quote-generate.js b/utils/quote-generate.js index 52dbfa3..2dd7dd7 100644 --- a/utils/quote-generate.js +++ b/utils/quote-generate.js @@ -9,7 +9,6 @@ const smartcrop = require('smartcrop-sharp') const runes = require('runes') const lottie = require('lottie-node') const zlib = require('zlib') -const { Telegram } = require('telegraf') const emojiDb = new EmojiDbLib({ useDefaultDb: true }) @@ -109,8 +108,8 @@ class ColorContrast { class QuoteGenerate { - constructor (botToken) { - this.telegram = new Telegram(botToken) + constructor (telegramBot) { + this.telegram = telegramBot } async avatarImageLatters (letters, color) { From 982320268b89331cb50f74d94c7f517d89ce9ae2 Mon Sep 17 00:00:00 2001 From: arelive Date: Wed, 7 Jun 2023 15:20:01 +0300 Subject: [PATCH 04/60] add async test using direct method --- package-lock.json | 7 ++++ package.json | 4 ++- test/async.js | 71 +++++++++++++++++++++++++++++++++++++++++ test/stress.js | 6 +++- utils/quote-generate.js | 2 -- 5 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 test/async.js diff --git a/package-lock.json b/package-lock.json index f95e42d..d137681 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "telegraf": "^3.38.0" }, "devDependencies": { + "async": "^3.2.4", "axios": "^1.4.0", "eslint": "^8.6.0", "eslint-config-standard": "^12.0.0", @@ -736,6 +737,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", diff --git a/package.json b/package.json index 614f8d9..2067a90 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "start": "node index.js", "lint": "node_modules/.bin/eslint --ext js .", "lint:fix": "node_modules/.bin/eslint --fix --ext js .", - "test": "node test/stress.js 1000" + "test": "node test/stress.js 1000", + "test:async": "node test/async.js 1000 50" }, "repository": { "type": "git", @@ -41,6 +42,7 @@ "telegraf": "^3.38.0" }, "devDependencies": { + "async": "^3.2.4", "axios": "^1.4.0", "eslint": "^8.6.0", "eslint-config-standard": "^12.0.0", diff --git a/test/async.js b/test/async.js new file mode 100644 index 0000000..1ca0a34 --- /dev/null +++ b/test/async.js @@ -0,0 +1,71 @@ +const async = require('async') +const path = require('path') +const fs = require('fs') +const LoremIpsum = require('lorem-ipsum').LoremIpsum + +require('dotenv').config({ path: './.env' }) +require('../app') + +const generate = require('../methods/generate') + +const lorem = new LoremIpsum({ + sentencesPerParagraph: { + max: 8, + min: 1 + }, + wordsPerSentence: { + max: 16, + min: 4 + } +}) + +const nQuotes = parseInt(process.argv[2]) +const nCallsLimit = parseInt(process.argv[3]) + +const queue = async.queue( + (json, cb) => generate(json) + .then(cb) + .catch(console.error), + nCallsLimit +) + +for (let i = 0; i < nQuotes; i++) { + const text = lorem.generateParagraphs(1) + const username = lorem.generateWords(2) + const avatar = 'https://telegra.ph/file/59952c903fdfb10b752b3.jpg' + + const json = { + botToken: process.env.BOT_TOKEN, + type: 'quote', + format: 'png', + backgroundColor: '#FFFFFF', + width: 512, + height: 768, + scale: 2, + messages: [ + { + entities: [], + avatar: true, + from: { + id: 1, + name: username, + photo: { + url: avatar + } + }, + text: text, + replyMessage: {} + } + ] + } + + console.time(i) + queue.push(json, res => { + const buffer = Buffer.from(res.image, 'base64') + fs.writeFile( + path.resolve(`./test/${i}.png`), buffer, + err => err && console.error(err) + ) + console.timeLog(i) + }) +} diff --git a/test/stress.js b/test/stress.js index 489ff43..fc10cdd 100644 --- a/test/stress.js +++ b/test/stress.js @@ -26,6 +26,7 @@ const nQuotes = parseInt(process.argv[2]) const avatar = 'https://telegra.ph/file/59952c903fdfb10b752b3.jpg' const json = { + botToken: process.env.BOT_TOKEN, type: 'quote', format: 'png', backgroundColor: '#FFFFFF', @@ -53,7 +54,10 @@ const nQuotes = parseInt(process.argv[2]) headers: { 'Content-Type': 'application/json' } }).then(res => { const buffer = Buffer.from(res.data.result.image, 'base64') - fs.writeFile(path.resolve(`./test/${i}.png`), buffer, (err) => err && console.error(err)) + fs.writeFile( + path.resolve(`./test/${i}.png`), buffer, + err => err && console.error(err) + ) }).catch(console.error) } })() diff --git a/utils/quote-generate.js b/utils/quote-generate.js index 2dd7dd7..e67fc63 100644 --- a/utils/quote-generate.js +++ b/utils/quote-generate.js @@ -939,8 +939,6 @@ class QuoteGenerate { '#FF7FD5' // pink ] - console.log(Math.abs(message.from.id) % 7) - // user name color let nameIndex = 1 if (message.from.id) nameIndex = Math.abs(message.from.id) % 7 From b615b2adc585512c4b5876db7302fbc84a9524f3 Mon Sep 17 00:00:00 2001 From: arelive Date: Wed, 7 Jun 2023 21:00:55 +0300 Subject: [PATCH 05/60] draw full quote with browser --- methods/generate.js | 116 ++++++++++++++++++++------------- utils/draw-avatar.js | 126 ++++++++++++++++++++++++++++++++++++ utils/get-view.js | 11 ++++ utils/quote-generate.js | 138 +++------------------------------------- utils/render.js | 9 ++- utils/telegram.js | 5 ++ utils/views.js | 11 ---- views/message.hbs | 52 +++++++++++++++ views/quote.hbs | 123 ++++++++++++++++++++++++++++------- 9 files changed, 384 insertions(+), 207 deletions(-) create mode 100644 utils/draw-avatar.js create mode 100644 utils/get-view.js create mode 100644 utils/telegram.js delete mode 100644 utils/views.js create mode 100644 views/message.hbs diff --git a/methods/generate.js b/methods/generate.js index bd27063..f2f0638 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -1,9 +1,12 @@ -const { - QuoteGenerate -} = require('../utils') +const path = require('path') +const fs = require('fs') const { createCanvas, loadImage } = require('canvas') const sharp = require('sharp') +const render = require('../utils/render') +const getView = require('../utils/get-view') +const drawAvatar = require('../utils/draw-avatar') + const normalizeColor = (color) => { const canvas = createCanvas(0, 0) const canvasCtx = canvas.getContext('2d') @@ -46,20 +49,18 @@ const imageAlpha = (image, alpha) => { return canvas } +const buildContent = getView('quote') +// FIXME: doest work +const bgImageURL = fs.readFileSync( + path.resolve('./assets/pattern_02.png') +).toString('base64') + module.exports = async (parm) => { - // console.log(JSON.stringify(parm, null, 2)) if (!parm) return { error: 'query_empty' } - if (!parm.messages || parm.messages.length < 1) return { error: 'messages_empty' } - - let botToken = parm.botToken || process.env.BOT_TOKEN - - const quoteGenerate = new QuoteGenerate(botToken) - - const quoteImages = [] + if (!parm.messages || !parm.messages.length) return { error: 'messages_empty' } let backgroundColor = parm.backgroundColor || '//#292232' - let backgroundColorOne - let backgroundColorTwo + let backgroundColorOne, backgroundColorTwo const backgroundColorSplit = backgroundColor.split('/') @@ -76,9 +77,65 @@ module.exports = async (parm) => { backgroundColorTwo = backgroundColor } - for (const key in parm.messages) { - const message = parm.messages[key] + const scale = parm.scale ? Math.min(parseFloat(parm.scale), 20) : 2 + const nameColors = ['red', 'orange', 'purple', 'green', 'sea', 'blue', 'pink'] + + const messages = await Promise.all(parm.messages + .filter(message => message) + .map(async message => { + const avatarURL = await drawAvatar(message.from) + return { + from: { + name: message.from?.name ?? '', + color: nameColors[message.from?.id ? Math.abs(message.from.id) % 7 : 1], + emoji_status: message.from?.emojiStatus ?? '', + avatar: { url: avatarURL.toDataURL() } + }, + text: message.text ?? '' + } + }) + ) + + if (!messages.length) { + return { error: 'empty_messages' } + } + + const content = buildContent({ + scale, + width: parm.width * scale, + height: parm.height * scale, + theme: 'light', + background: { + image: { url: bgImageURL, }, + color1: colorLuminance(backgroundColorTwo, 0.15), + color2: colorLuminance(backgroundColorOne, 0.15) + }, + messages, + }) + fs.writeFileSync('./content.html', content) + + let { type, format, ext } = parm + const quoteImage = await render(content, '#quote') + const { width, height } = await sharp(quoteImage).metadata() + const image = ext ? quoteImage : quoteImage.toString('base64') + + if ((!type && ext) || (type != 'image' && height > 1024 * 2)) { + type = 'png' + } + + + return { + image, + type, + width, + height, + ext + } + + // deprecated // TODO +/* + for (const message of parm.messages) { if (message) { const canvasQuote = await quoteGenerate.generate( backgroundColorOne, @@ -94,12 +151,6 @@ module.exports = async (parm) => { } } - if (quoteImages.length === 0) { - return { - error: 'empty_messages' - } - } - let canvasQuote if (quoteImages.length > 1) { @@ -129,11 +180,6 @@ module.exports = async (parm) => { let quoteImage - let { type, format, ext } = parm - - if (!type && ext) type = 'png' - if (type !== 'image' && canvasQuote.height > 1024 * 2) type = 'png' - if (type === 'quote') { const downPadding = 75 const maxWidth = 512 @@ -218,21 +264,5 @@ module.exports = async (parm) => { } else { quoteImage = canvasQuote.toBuffer() } - - const imageMetadata = await sharp(quoteImage).metadata() - - const width = imageMetadata.width - const height = imageMetadata.height - - let image - if (ext) image = quoteImage - else image = quoteImage.toString('base64') - - return { - image, - type, - width, - height, - ext - } +*/ } diff --git a/utils/draw-avatar.js b/utils/draw-avatar.js new file mode 100644 index 0000000..3d96283 --- /dev/null +++ b/utils/draw-avatar.js @@ -0,0 +1,126 @@ +const { createCanvas, loadImage } = require('canvas') +const runes = require('runes') +const LRU = require('lru-cache') + +const telegram = require('./telegram') + +const avatarCache = new LRU({ + max: 20, + maxAge: 1000 * 60 * 5 +}) + +async function drawAvatar (user) { + const avatarImage = await downloadAvatarImage(user) + + if (avatarImage) { + const avatarSize = avatarImage.naturalHeight + + const canvas = createCanvas(avatarSize, avatarSize) + const canvasCtx = canvas.getContext('2d') + + const avatarX = 0 + const avatarY = 0 + + // canvasCtx.beginPath() + // canvasCtx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2, true) + // canvasCtx.clip() + // canvasCtx.closePath() + // canvasCtx.restore() + canvasCtx.drawImage(avatarImage, avatarX, avatarY, avatarSize, avatarSize) + + return canvas + } +} + +async function downloadAvatarImage (user) { + let avatarImage + + let nameLetters + if (user.first_name && user.last_name) nameLetters = runes(user.first_name)[0] + (runes(user.last_name || '')[0]) + else { + let name = user.first_name || user.name || user.title + name = name.toUpperCase() + const nameWord = name.split(' ') + + if (nameWord.length > 1) nameLetters = runes(nameWord[0])[0] + runes(nameWord.splice(-1)[0])[0] + else nameLetters = runes(nameWord[0])[0] + } + + const cacheKey = user.id + + const avatarImageCache = avatarCache.get(cacheKey) + + const avatarColorArray = [ + [ '#FF885E', '#FF516A' ], // red + [ '#FFCD6A', '#FFA85C' ], // orange + [ '#E0A2F3', '#D669ED' ], // purple + [ '#A0DE7E', '#54CB68' ], // green + [ '#53EDD6', '#28C9B7' ], // sea + [ '#72D5FD', '#2A9EF1' ], // blue + [ '#FFA8A8', '#FF719A' ] // pink + ] + + const nameIndex = Math.abs(user.id) % 7 + + const avatarColor = avatarColorArray[nameIndex] + + if (avatarImageCache) { + avatarImage = avatarImageCache + } else if (user.photo && user.photo.url) { + avatarImage = await loadImage(user.photo.url) + } else { + try { + let userPhoto, userPhotoUrl + + if (user.photo && user.photo.big_file_id) userPhotoUrl = await telegram.getFileLink(user.photo.big_file_id).catch(console.error) + + if (!userPhotoUrl) { + const getChat = await telegram.getChat(user.id).catch(console.error) + if (getChat && getChat.photo && getChat.photo.big_file_id) userPhoto = getChat.photo.big_file_id + + if (userPhoto) userPhotoUrl = await telegram.getFileLink(userPhoto) + else if (user.username) userPhotoUrl = `https://telega.one/i/userpic/320/${user.username}.jpg` + else avatarImage = await loadImage(await avatarImageLetters(nameLetters, avatarColor)) + } + + if (userPhotoUrl) avatarImage = await loadImage(userPhotoUrl) + + avatarCache.set(cacheKey, avatarImage) + } catch (error) { + avatarImage = await loadImage(await avatarImageLetters(nameLetters, avatarColor)) + } + } + + return avatarImage +} + +async function avatarImageLetters (letters, color) { + const size = 500 + const canvas = createCanvas(size, size) + const context = canvas.getContext('2d') + + const gradient = context.createLinearGradient(0, 0, canvas.width, canvas.height) + + gradient.addColorStop(0, color[0]) + gradient.addColorStop(1, color[1]) + + context.fillStyle = gradient + context.fillRect(0, 0, canvas.width, canvas.height) + + const drawLetters = await this.drawMultilineText( + letters, + null, + size / 2, + '#FFF', + 0, + size, + size * 5, + size * 5 + ) + + context.drawImage(drawLetters, (canvas.width - drawLetters.width) / 2, (canvas.height - drawLetters.height) / 1.5) + + return canvas.toBuffer() +} + +module.exports = drawAvatar diff --git a/utils/get-view.js b/utils/get-view.js new file mode 100644 index 0000000..0a9654d --- /dev/null +++ b/utils/get-view.js @@ -0,0 +1,11 @@ +const path = require('path') +const fs = require('fs') +const Handlebars = require('handlebars') + +module.exports = viewName => { + const template = fs.readFileSync( + path.resolve(`./views/${viewName}.hbs`) + ).toString('utf-8') + + return Handlebars.compile(template) +} diff --git a/utils/quote-generate.js b/utils/quote-generate.js index 317e41a..c8e9a2e 100644 --- a/utils/quote-generate.js +++ b/utils/quote-generate.js @@ -1,7 +1,6 @@ const fs = require('fs') -const { createCanvas, registerFont, Image } = require('canvas') +const { createCanvas, registerFont, Image, loadImage } = require('canvas') const EmojiDbLib = require('emoji-db') -const { loadImage } = require('canvas') const loadImageFromUrl = require('./image-load-url') const sharp = require('sharp') const Jimp = require('jimp') @@ -12,9 +11,12 @@ const zlib = require('zlib') const { Telegram } = require('telegraf') const render = require('./render') -const { quote: view } = require('./views') +const getView = require('./get-view') + +const drawAvatar = require('./draw-avatar') const emojiDb = new EmojiDbLib({ useDefaultDb: true }) +const buildContent = getView('message') function loadFont () { console.log('font load start') @@ -37,13 +39,6 @@ loadFont() const emojiImageByBrand = require('./emoji-image') -const LRU = require('lru-cache') - -const avatarCache = new LRU({ - max: 20, - maxAge: 1000 * 60 * 5 -}) - // write a nodejs function that accepts 2 colors. the first is the background color and the second is the text color. as a result, the first color should come out brighter or darker depending on the contrast. for example, if the first text is dark, then make the second brighter and return it. you need to change not the background color, but the text color // here are all the possible colors that will be passed as the second argument. the first color can be any @@ -116,97 +111,6 @@ class QuoteGenerate { this.telegram = new Telegram(botToken) } - async avatarImageLatters (letters, color) { - const size = 500 - const canvas = createCanvas(size, size) - const context = canvas.getContext('2d') - - const gradient = context.createLinearGradient(0, 0, canvas.width, canvas.height) - - gradient.addColorStop(0, color[0]) - gradient.addColorStop(1, color[1]) - - context.fillStyle = gradient - context.fillRect(0, 0, canvas.width, canvas.height) - - const drawLetters = await this.drawMultilineText( - letters, - null, - size / 2, - '#FFF', - 0, - size, - size * 5, - size * 5 - ) - - context.drawImage(drawLetters, (canvas.width - drawLetters.width) / 2, (canvas.height - drawLetters.height) / 1.5) - - return canvas.toBuffer() - } - - async downloadAvatarImage (user) { - let avatarImage - - let nameLatters - if (user.first_name && user.last_name) nameLatters = runes(user.first_name)[0] + (runes(user.last_name || '')[0]) - else { - let name = user.first_name || user.name || user.title - name = name.toUpperCase() - const nameWord = name.split(' ') - - if (nameWord.length > 1) nameLatters = runes(nameWord[0])[0] + runes(nameWord.splice(-1)[0])[0] - else nameLatters = runes(nameWord[0])[0] - } - - const cacheKey = user.id - - const avatarImageCache = avatarCache.get(cacheKey) - - const avatarColorArray = [ - [ '#FF885E', '#FF516A' ], // red - [ '#FFCD6A', '#FFA85C' ], // orange - [ '#E0A2F3', '#D669ED' ], // purple - [ '#A0DE7E', '#54CB68' ], // green - [ '#53EDD6', '#28C9B7' ], // sea - [ '#72D5FD', '#2A9EF1' ], // blue - [ '#FFA8A8', '#FF719A' ] // pink - ] - - const nameIndex = Math.abs(user.id) % 7 - - const avatarColor = avatarColorArray[nameIndex] - - if (avatarImageCache) { - avatarImage = avatarImageCache - } else if (user.photo && user.photo.url) { - avatarImage = await loadImage(user.photo.url) - } else { - try { - let userPhoto, userPhotoUrl - - if (user.photo && user.photo.big_file_id) userPhotoUrl = await this.telegram.getFileLink(user.photo.big_file_id).catch(console.error) - - if (!userPhotoUrl) { - const getChat = await this.telegram.getChat(user.id).catch(console.error) - if (getChat && getChat.photo && getChat.photo.big_file_id) userPhoto = getChat.photo.big_file_id - - if (userPhoto) userPhotoUrl = await this.telegram.getFileLink(userPhoto) - else if (user.username) userPhotoUrl = `https://telega.one/i/userpic/320/${user.username}.jpg` - else avatarImage = await loadImage(await this.avatarImageLatters(nameLatters, avatarColor)) - } - - if (userPhotoUrl) avatarImage = await loadImage(userPhotoUrl) - - avatarCache.set(cacheKey, avatarImage) - } catch (error) { - avatarImage = await loadImage(await this.avatarImageLatters(nameLatters, avatarColor)) - } - } - - return avatarImage - } - ungzip (input, options) { return new Promise((resolve, reject) => { zlib.gunzip(input, options, (error, result) => { @@ -683,29 +587,6 @@ class QuoteGenerate { return canvas } - async drawAvatar (user) { - const avatarImage = await this.downloadAvatarImage(user) - - if (avatarImage) { - const avatarSize = avatarImage.naturalHeight - - const canvas = createCanvas(avatarSize, avatarSize) - const canvasCtx = canvas.getContext('2d') - - const avatarX = 0 - const avatarY = 0 - - // canvasCtx.beginPath() - // canvasCtx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2, true) - // canvasCtx.clip() - // canvasCtx.closePath() - // canvasCtx.restore() - canvasCtx.drawImage(avatarImage, avatarX, avatarY, avatarSize, avatarSize) - - return canvas - } - } - drawLineSegment (ctx, x, y, width, isEven) { ctx.lineWidth = 35 // how thick the line is ctx.strokeStyle = '#aec6cf' // what color our line is @@ -958,9 +839,9 @@ class QuoteGenerate { } - const avatarImage = await this.drawAvatar(message.from) + const avatarImage = await drawAvatar(message.from) - const content = view({ + const content = buildContent({ width, height, scale, @@ -972,8 +853,7 @@ class QuoteGenerate { text: message.text ?? '', }) - const page = await render(content) - const image = await page.locator('#quote').screenshot() + const image = await render(content, '#quote') // convert to canvas for the generator const canvas = createCanvas(width, height) @@ -1050,7 +930,7 @@ class QuoteGenerate { } let avatarCanvas - if (message.avatar) avatarCanvas = await this.drawAvatar(message.from) + if (message.avatar) avatarCanvas = await drawAvatar(message.from) let replyName, replyNameColor, replyText if (message.replyMessage && message.replyMessage.name && message.replyMessage.text) { diff --git a/utils/render.js b/utils/render.js index 3fbe2e9..5d8a763 100644 --- a/utils/render.js +++ b/utils/render.js @@ -4,8 +4,13 @@ const promise = webkit.launch() .then(browser => browser.newContext()) .then(context => context.newPage()) -module.exports = async (content) => { +module.exports = async (content, selector) => { const page = await promise await page.setContent(content) - return page + const screenshot = await page.locator(selector).screenshot({ + type: 'png', + scale: 'css', + omitBackground: true + }) + return screenshot } diff --git a/utils/telegram.js b/utils/telegram.js new file mode 100644 index 0000000..087e5ab --- /dev/null +++ b/utils/telegram.js @@ -0,0 +1,5 @@ +const { Telegraf } = require('telegraf') + +const botToken = process.env.BOT_TOKEN + +module.exports = new Telegraf(botToken) diff --git a/utils/views.js b/utils/views.js deleted file mode 100644 index 3c515d3..0000000 --- a/utils/views.js +++ /dev/null @@ -1,11 +0,0 @@ -const path = require('path') -const fs = require('fs') -const Handlebars = require('handlebars') - -const quote = Handlebars.compile( - fs.readFileSync(path.resolve('./views/quote.hbs')).toString('utf-8') -) - -module.exports = { - quote -} diff --git a/views/message.hbs b/views/message.hbs new file mode 100644 index 0000000..1c6a1b9 --- /dev/null +++ b/views/message.hbs @@ -0,0 +1,52 @@ + + + + + + + +
+ +

+ {{name}} + {{emojiStatus}} +

+

{{text}}

+
+ + diff --git a/views/quote.hbs b/views/quote.hbs index 1c6a1b9..da91fc8 100644 --- a/views/quote.hbs +++ b/views/quote.hbs @@ -2,51 +2,130 @@ +
- -

- {{name}} - {{emojiStatus}} -

-

{{text}}

-
+
    + {{#each messages}} +
  • + +
    +
    +

    + {{this.from.name}} + +

    +
    +
    +

    {{this.text}}

    +
    +
    +
  • + {{/each}} +
From a922776cc69a2bb08c14cfdf77adba50c14620b8 Mon Sep 17 00:00:00 2001 From: arelive Date: Wed, 7 Jun 2023 22:21:35 +0300 Subject: [PATCH 06/60] fix: using dedicated page for each async thread --- test/async.js | 6 +++--- test/stress.js | 4 ++-- utils/render.js | 10 ++++++---- views/quote.hbs | 5 ++--- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/test/async.js b/test/async.js index 1ca0a34..b7275e2 100644 --- a/test/async.js +++ b/test/async.js @@ -10,12 +10,12 @@ const generate = require('../methods/generate') const lorem = new LoremIpsum({ sentencesPerParagraph: { - max: 8, + max: 4, min: 1 }, wordsPerSentence: { max: 16, - min: 4 + min: 2 } }) @@ -66,6 +66,6 @@ for (let i = 0; i < nQuotes; i++) { path.resolve(`./test/${i}.png`), buffer, err => err && console.error(err) ) - console.timeLog(i) + console.timeEnd(i) }) } diff --git a/test/stress.js b/test/stress.js index fc10cdd..8b90ca9 100644 --- a/test/stress.js +++ b/test/stress.js @@ -8,12 +8,12 @@ require('../app') const lorem = new LoremIpsum({ sentencesPerParagraph: { - max: 8, + max: 4, min: 1 }, wordsPerSentence: { max: 16, - min: 4 + min: 2 } }) diff --git a/utils/render.js b/utils/render.js index 5d8a763..ccda34f 100644 --- a/utils/render.js +++ b/utils/render.js @@ -1,16 +1,18 @@ const { webkit } = require('playwright') -const promise = webkit.launch() - .then(browser => browser.newContext()) - .then(context => context.newPage()) +const promise = webkit.launch().then(browser => browser.newContext()) module.exports = async (content, selector) => { - const page = await promise + const context = await promise + const page = await context.newPage() await page.setContent(content) + await page.waitForSelector(selector, { state: 'visible' }) + const screenshot = await page.locator(selector).screenshot({ type: 'png', scale: 'css', omitBackground: true }) + return screenshot } diff --git a/views/quote.hbs b/views/quote.hbs index da91fc8..dcb5028 100644 --- a/views/quote.hbs +++ b/views/quote.hbs @@ -30,15 +30,14 @@ .message-from-avatar { display: block; float: left; - margin: 0 10px 0 5px; + margin-left: 5px; width: 50px; height: 50px; border-radius: 50%; } .message { - float: left; + margin-left: 65px; padding: 14px; - max-width: calc(100% - 60px); background: linear-gradient(45deg, {{background.color1}}, {{background.color2}}); border-radius: 25px; } From d8b24055f625749ba207fad7099850d29083b73b Mon Sep 17 00:00:00 2001 From: arelive Date: Thu, 8 Jun 2023 18:17:17 +0300 Subject: [PATCH 07/60] resize images to 512px; detect color scheme --- methods/generate.js | 211 +++++++++------------------------------- test/.gitignore | 1 + test/stress.js | 26 ++++- utils/light-or-dark.js | 38 ++++++++ utils/quote-generate.js | 42 +------- utils/render.js | 1 + views/image.hbs | 99 +++++++++++++++++++ views/quote.hbs | 69 +++---------- 8 files changed, 224 insertions(+), 263 deletions(-) create mode 100644 utils/light-or-dark.js create mode 100644 views/image.hbs diff --git a/methods/generate.js b/methods/generate.js index f2f0638..cbb808f 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -6,6 +6,7 @@ const sharp = require('sharp') const render = require('../utils/render') const getView = require('../utils/get-view') const drawAvatar = require('../utils/draw-avatar') +const lightOrDark = require('../utils/light-or-dark') const normalizeColor = (color) => { const canvas = createCanvas(0, 0) @@ -49,19 +50,31 @@ const imageAlpha = (image, alpha) => { return canvas } -const buildContent = getView('quote') -// FIXME: doest work -const bgImageURL = fs.readFileSync( - path.resolve('./assets/pattern_02.png') -).toString('base64') + +const userColors = { + light: ['#FC5C51', '#FA790F', '#895DD5', '#0FB297', '#0FC9D6', '#3CA5EC', '#D54FAF'], + dark: ['#FF8E86', '#FFA357', '#B18FFF', '#4DD6BF', '#45E8D1', '#7AC9FF', '#FF7FD5'] +} +// FIXME: doesnt work +const bgImageURL = 'file://' + path.resolve('./assets/pattern_02.png').replace(/\\/g, '/') module.exports = async (parm) => { - if (!parm) return { error: 'query_empty' } - if (!parm.messages || !parm.messages.length) return { error: 'messages_empty' } + if (!parm || typeof parm != 'object') { + return { error: 'query_empty' } + } + let type = parm.type || 'png' + const format = parm.format || '' + const ext = parm.ext || false + const scale = parm.scale ? Math.min(parseFloat(parm.scale), 20) : 2 let backgroundColor = parm.backgroundColor || '//#292232' - let backgroundColorOne, backgroundColorTwo + let messages = parm.messages?.filter(message => message) + + if (!messages?.length) { + return { error: 'messages_empty' } + } + let backgroundColorOne, backgroundColorTwo const backgroundColorSplit = backgroundColor.split('/') if (backgroundColorSplit && backgroundColorSplit.length > 1 && backgroundColorSplit[0] !== '') { @@ -77,192 +90,60 @@ module.exports = async (parm) => { backgroundColorTwo = backgroundColor } - const scale = parm.scale ? Math.min(parseFloat(parm.scale), 20) : 2 - const nameColors = ['red', 'orange', 'purple', 'green', 'sea', 'blue', 'pink'] + const theme = lightOrDark(backgroundColorOne) - const messages = await Promise.all(parm.messages - .filter(message => message) + messages = await Promise.all(messages .map(async message => { - const avatarURL = await drawAvatar(message.from) + const avatar = await drawAvatar(message.from) + const userColor = userColors[theme][message.from?.id ? Math.abs(message.from.id) % 7 : 1] return { from: { name: message.from?.name ?? '', - color: nameColors[message.from?.id ? Math.abs(message.from.id) % 7 : 1], + color: userColor, emoji_status: message.from?.emojiStatus ?? '', - avatar: { url: avatarURL.toDataURL() } + avatar: { url: avatar.toDataURL() } }, text: message.text ?? '' } }) ) - if (!messages.length) { - return { error: 'empty_messages' } - } - - const content = buildContent({ + const content = getView(type)({ scale, - width: parm.width * scale, - height: parm.height * scale, - theme: 'light', + theme, background: { - image: { url: bgImageURL, }, + image: { url: bgImageURL }, color1: colorLuminance(backgroundColorTwo, 0.15), color2: colorLuminance(backgroundColorOne, 0.15) }, messages, }) - fs.writeFileSync('./content.html', content) - let { type, format, ext } = parm - const quoteImage = await render(content, '#quote') - const { width, height } = await sharp(quoteImage).metadata() - const image = ext ? quoteImage : quoteImage.toString('base64') + let image = await render(content, '#quote') + const imageSharp = await sharp(image) + const { width, height } = await imageSharp.metadata() - if ((!type && ext) || (type != 'image' && height > 1024 * 2)) { + // if height is more than 2 width, return png instead quote + if (type == 'quote' && height > width * 2) { type = 'png' } + if (type == 'quote') { + const maxWidth = 512 + const maxHeight = 512 + + imageSharp.resize(height > width ? { height: maxHeight } : { width: maxWidth }) + + image = format == 'png' ? + await imageSharp.png().toBuffer() : + await imageSharp.webp({ lossless: true, force: true }).toBuffer() + } return { - image, + image: ext ? image : image.toString('base64'), type, width, height, ext } - - - // deprecated // TODO -/* - for (const message of parm.messages) { - if (message) { - const canvasQuote = await quoteGenerate.generate( - backgroundColorOne, - backgroundColorTwo, - message, - parm.width, - parm.height, - parseFloat(parm.scale), - parm.emojiBrand - ) - - quoteImages.push(canvasQuote) - } - } - - let canvasQuote - - if (quoteImages.length > 1) { - let width = 0 - let height = 0 - - for (let index = 0; index < quoteImages.length; index++) { - if (quoteImages[index].width > width) width = quoteImages[index].width - height += quoteImages[index].height - } - - const quoteMargin = 5 * parm.scale - - const canvas = createCanvas(width, height + (quoteMargin * quoteImages.length)) - const canvasCtx = canvas.getContext('2d') - - let imageY = 0 - - for (let index = 0; index < quoteImages.length; index++) { - canvasCtx.drawImage(quoteImages[index], 0, imageY) - imageY += quoteImages[index].height + quoteMargin - } - canvasQuote = canvas - } else { - canvasQuote = quoteImages[0] - } - - let quoteImage - - if (type === 'quote') { - const downPadding = 75 - const maxWidth = 512 - const maxHeight = 512 - - const imageQuoteSharp = sharp(canvasQuote.toBuffer()) - - if (canvasQuote.height > canvasQuote.width) imageQuoteSharp.resize({ height: maxHeight }) - else imageQuoteSharp.resize({ width: maxWidth }) - - const canvasImage = await loadImage(await imageQuoteSharp.toBuffer()) - - const canvasPadding = createCanvas(canvasImage.width, canvasImage.height + downPadding) - const canvasPaddingCtx = canvasPadding.getContext('2d') - - canvasPaddingCtx.drawImage(canvasImage, 0, 0) - - const imageSharp = sharp(canvasPadding.toBuffer()) - - if (canvasPadding.height >= canvasPadding.width) imageSharp.resize({ height: maxHeight }) - else imageSharp.resize({ width: maxWidth }) - - if (format === 'png') quoteImage = await imageSharp.png().toBuffer() - else quoteImage = await imageSharp.webp({ lossless: true, force: true }).toBuffer() - } else if (type === 'image') { - const heightPadding = 75 * parm.scale - const widthPadding = 95 * parm.scale - - const canvasImage = await loadImage(canvasQuote.toBuffer()) - - const canvasPic = createCanvas(canvasImage.width + widthPadding, canvasImage.height + heightPadding) - const canvasPicCtx = canvasPic.getContext('2d') - - // radial gradient background (top left) - const gradient = canvasPicCtx.createRadialGradient( - canvasPic.width / 2, - canvasPic.height / 2, - 0, - canvasPic.width / 2, - canvasPic.height / 2, - canvasPic.width / 2 - ) - - const patternColorOne = colorLuminance(backgroundColorTwo, 0.15) - const patternColorTwo = colorLuminance(backgroundColorOne, 0.15) - - gradient.addColorStop(0, patternColorOne) - gradient.addColorStop(1, patternColorTwo) - - canvasPicCtx.fillStyle = gradient - canvasPicCtx.fillRect(0, 0, canvasPic.width, canvasPic.height) - - const canvasPatternImage = await loadImage('./assets/pattern_02.png') - // const canvasPatternImage = await loadImage('./assets/pattern_ny.png'); - - const pattern = canvasPicCtx.createPattern(imageAlpha(canvasPatternImage, 0.3), 'repeat') - - canvasPicCtx.fillStyle = pattern - canvasPicCtx.fillRect(0, 0, canvasPic.width, canvasPic.height) - - // Add shadow effect to the canvas image - canvasPicCtx.shadowOffsetX = 8 - canvasPicCtx.shadowOffsetY = 8 - canvasPicCtx.shadowBlur = 13 - canvasPicCtx.shadowColor = 'rgba(0, 0, 0, 0.5)' - - // Draw the image to the canvas with padding centered - canvasPicCtx.drawImage(canvasImage, widthPadding / 2, heightPadding / 2) - - canvasPicCtx.shadowOffsetX = 0 - canvasPicCtx.shadowOffsetY = 0 - canvasPicCtx.shadowBlur = 0 - canvasPicCtx.shadowColor = 'rgba(0, 0, 0, 0)' - - // write text button right - canvasPicCtx.fillStyle = `rgba(0, 0, 0, 0.3)` - canvasPicCtx.font = `${8 * parm.scale}px Noto Sans` - canvasPicCtx.textAlign = 'right' - canvasPicCtx.fillText('@QuotLyBot', canvasPic.width - 25, canvasPic.height - 25) - - quoteImage = await sharp(canvasPic.toBuffer()).png({ lossless: true, force: true }).toBuffer() - } else { - quoteImage = canvasQuote.toBuffer() - } -*/ } diff --git a/test/.gitignore b/test/.gitignore index e33609d..cd805b8 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -1 +1,2 @@ *.png +*.webp diff --git a/test/stress.js b/test/stress.js index 8b90ca9..24a0a15 100644 --- a/test/stress.js +++ b/test/stress.js @@ -27,9 +27,7 @@ const nQuotes = parseInt(process.argv[2]) const json = { botToken: process.env.BOT_TOKEN, - type: 'quote', - format: 'png', - backgroundColor: '#FFFFFF', + backgroundColor: '', width: 512, height: 768, scale: 2, @@ -38,7 +36,7 @@ const nQuotes = parseInt(process.argv[2]) entities: [], avatar: true, from: { - id: 1, + id: Math.floor(Math.random() * 100), name: username, photo: { url: avatar @@ -50,7 +48,25 @@ const nQuotes = parseInt(process.argv[2]) ] } - await axios.post('http://localhost:3000/generate', json, { + await axios.post('http://localhost:3000/generate', { + ...json, + type: 'quote', + format: 'webp' + }, { + headers: { 'Content-Type': 'application/json' } + }).then(res => { + const buffer = Buffer.from(res.data.result.image, 'base64') + fs.writeFile( + path.resolve(`./test/${i}.webp`), buffer, + err => err && console.error(err) + ) + }).catch(console.error) + + await axios.post('http://localhost:3000/generate', { + ...json, + type: 'image', + format: 'png' + }, { headers: { 'Content-Type': 'application/json' } }).then(res => { const buffer = Buffer.from(res.data.result.image, 'base64') diff --git a/utils/light-or-dark.js b/utils/light-or-dark.js new file mode 100644 index 0000000..9d07ddf --- /dev/null +++ b/utils/light-or-dark.js @@ -0,0 +1,38 @@ +// https://codepen.io/andreaswik/pen/YjJqpK +module.exports = (color) => { + let r, g, b + + // Check the format of the color, HEX or RGB? + if (color.match(/^rgb/)) { + // If HEX --> store the red, green, blue values in separate variables + color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/) + + r = color[1] + g = color[2] + b = color[3] + } else { + // If RGB --> Convert it to HEX: http://gist.github.com/983661 + color = +('0x' + color.slice(1).replace( + color.length < 5 && /./g, '$&$&' + ) + ) + + r = color >> 16 + g = color >> 8 & 255 + b = color & 255 + } + + // HSP (Highly Sensitive Poo) equation from http://alienryderflex.com/hsp.html + const hsp = Math.sqrt( + 0.299 * (r * r) + + 0.587 * (g * g) + + 0.114 * (b * b) + ) + + // Using the HSP value, determine whether the color is light or dark + if (hsp > 127.5) { + return 'light' + } else { + return 'dark' + } +} diff --git a/utils/quote-generate.js b/utils/quote-generate.js index 3a6fd99..3c7755a 100644 --- a/utils/quote-generate.js +++ b/utils/quote-generate.js @@ -12,6 +12,7 @@ const zlib = require('zlib') const render = require('./render') const getView = require('./get-view') const drawAvatar = require('./draw-avatar') +const lightOrDark = require('./light-or-dark') const emojiDb = new EmojiDbLib({ useDefaultDb: true }) const buildContent = getView('message') @@ -163,45 +164,6 @@ class QuoteGenerate { .map(x => parseInt(x, 16)) } - // https://codepen.io/andreaswik/pen/YjJqpK - lightOrDark (color) { - let r, g, b - - // Check the format of the color, HEX or RGB? - if (color.match(/^rgb/)) { - // If HEX --> store the red, green, blue values in separate variables - color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/) - - r = color[1] - g = color[2] - b = color[3] - } else { - // If RGB --> Convert it to HEX: http://gist.github.com/983661 - color = +('0x' + color.slice(1).replace( - color.length < 5 && /./g, '$&$&' - ) - ) - - r = color >> 16 - g = color >> 8 & 255 - b = color & 255 - } - - // HSP (Highly Sensitive Poo) equation from http://alienryderflex.com/hsp.html - const hsp = Math.sqrt( - 0.299 * (r * r) + - 0.587 * (g * g) + - 0.114 * (b * b) - ) - - // Using the HSP value, determine whether the color is light or dark - if (hsp > 127.5) { - return 'light' - } else { - return 'dark' - } - } - async drawMultilineText (text, entities, fontSize, fontColor, textX, textY, maxWidth, maxHeight, emojiBrand = 'apple') { if (maxWidth > 10000) maxWidth = 10000 if (maxHeight > 10000) maxHeight = 10000 @@ -779,7 +741,7 @@ class QuoteGenerate { height *= scale // check background style color black/light - const backStyle = this.lightOrDark(backgroundColorOne) + const backStyle = lightOrDark(backgroundColorOne) // historyPeer1NameFg: #c03d33; // red diff --git a/utils/render.js b/utils/render.js index ccda34f..ba3fa01 100644 --- a/utils/render.js +++ b/utils/render.js @@ -14,5 +14,6 @@ module.exports = async (content, selector) => { omitBackground: true }) + page.close().catch(console.error) return screenshot } diff --git a/views/image.hbs b/views/image.hbs new file mode 100644 index 0000000..b741fa3 --- /dev/null +++ b/views/image.hbs @@ -0,0 +1,99 @@ + + + + + + + + +
+
    + {{#each messages}} +
  • + +
    +
    +

    + {{this.from.name}} + +

    +
    +
    +

    {{this.text}}

    +
    +
    +
  • + {{/each}} +
+

@QuotLyBot

+ + diff --git a/views/quote.hbs b/views/quote.hbs index dcb5028..e9db03e 100644 --- a/views/quote.hbs +++ b/views/quote.hbs @@ -9,19 +9,18 @@ } #quote { position: absolute; - max-width: {{width}}px; - max-height: {{height}}px; + padding-bottom: 75px; + max-width: 512px; + width: {{width}}px; + height: {{height}}px; transform: scale({{scale}}); - /* background: radial-gradient({{background.color1}}, {{background.color2}}), url("{{background.image.url}}") top left repeat; */ background: none; } .messages-list { list-style: none; - overflow: hidden; } .message-item { display: block; - overflow: hidden; margin-bottom: 5px; } .message-item:last-child { @@ -33,12 +32,17 @@ margin-left: 5px; width: 50px; height: 50px; + font-family: 'Nono Sans', sans-serif; + font-size: 25px; + line-height: 50px; + text-align: center; + color: #FFF; border-radius: 50%; } .message { margin-left: 65px; padding: 14px; - background: linear-gradient(45deg, {{background.color1}}, {{background.color2}}); + background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}}); border-radius: 25px; } .message-from-name { @@ -51,58 +55,15 @@ white-space: nowrap; text-overflow: ellipsis; } - .message-from-name.red { - color: #FC5C51 - } - .message-from-name.orange { - color: #FA790F - } - .message-from-name.purple { - color: #895DD5 - } - .message-from-name.green { - color: #0FB297 - } - .message-from-name.sea { - color: #0FC9D6 - } - .message-from-name.blue { - color: #3CA5EC - } - .message-from-name.pink { - color: #D54FAF - } - .dark .message-from-name.red { - color: #FF8E86 - } - .dark .message-from-name.orange { - color: #FFA357 - } - .dark .message-from-name.purple { - color: #B18FFF - } - .dark .message-from-name.green { - color: #4DD6BF - } - .dark .message-from-name.sea { - color: #45E8D1 - } - .dark .message-from-name.blue { - color: #7AC9FF - } - .dark .message-from-name.pink { - color: #FF7FD5 - } .message-text { overflow: hidden; width: 100%; font-family: 'Noto Sans', sans-serif; font-size: 24px; line-height: 1.2; - color: #000; - } - .dark .message-text { color: #FFF; + mix-blend-mode: difference; + text-overflow: ellipsis; } @@ -111,10 +72,12 @@
diff --git a/views/image.hbs b/views/image.hbs index ddf355e..7154e95 100644 --- a/views/image.hbs +++ b/views/image.hbs @@ -78,6 +78,30 @@ font-size: 21px; white-space: nowrap; } + .message-text code { + font-family: 'Noto Mono', monospace; + } + .message-text .spoiler { + opacity: .3; + } + .message-text .mention, + .message-text .hashtag, + .message-text .cashtag, + .message-text .bot-command, + .message-text .email, + .message-text .phone-number { + text-decoration: none; + color: blue; + } + .light a, + .light .message-text .mention, + .light .message-text .hashtag, + .light .message-text .cashtag, + .light .message-text .bot-command, + .light .message-text .email, + .light .message-text .phone-number { + color: yellow; + } .watermark { position: absolute; bottom: 5px; @@ -125,7 +149,7 @@ {{#if this.media}} {{/if}} -

{{this.text}}

+

{{{this.text}}}

diff --git a/views/quote.hbs b/views/quote.hbs index 159c9b3..eedd1d7 100644 --- a/views/quote.hbs +++ b/views/quote.hbs @@ -75,6 +75,30 @@ font-size: 21px; white-space: nowrap; } + .message-text code { + font-family: 'Noto Mono', monospace; + } + .message-text .spoiler { + opacity: .3; + } + .message-text .mention, + .message-text .hashtag, + .message-text .cashtag, + .message-text .bot-command, + .message-text .email, + .message-text .phone-number { + text-decoration: none; + color: blue; + } + .light a, + .light .message-text .mention, + .light .message-text .hashtag, + .light .message-text .cashtag, + .light .message-text .bot-command, + .light .message-text .email, + .light .message-text .phone-number { + color: yellow; + } @@ -114,7 +138,7 @@ {{#if this.media}} {{/if}} -

{{this.text}}

+

{{{this.text}}}

From a0b073409713ccb72e60549946e348e9779f4521 Mon Sep 17 00:00:00 2001 From: arelive Date: Sat, 10 Jun 2023 20:38:54 +0300 Subject: [PATCH 14/60] fix: uploading avatars --- utils/get-avatar-url.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/get-avatar-url.js b/utils/get-avatar-url.js index ffababb..8ac1a28 100644 --- a/utils/get-avatar-url.js +++ b/utils/get-avatar-url.js @@ -29,7 +29,7 @@ module.exports = async (user) => { return null } - avatarCache.set(user.id, avatarImage) + avatarCache.set(user.id, avatarURL) return avatarURL } From 26a43a6549d811c231307908b7f98ff05d5d15a2 Mon Sep 17 00:00:00 2001 From: arelive Date: Sat, 10 Jun 2023 21:19:57 +0300 Subject: [PATCH 15/60] allow line feeds --- methods/generate.js | 1 + package-lock.json | 24 +++++++++++++----------- test/async.js | 34 ++++++++++++++++++++++------------ test/stress.js | 35 ++++++++++++++++++++--------------- 4 files changed, 56 insertions(+), 38 deletions(-) diff --git a/methods/generate.js b/methods/generate.js index a7df4aa..85883a6 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -121,6 +121,7 @@ const buildMessage = async (message, theme) => { if (Array.isArray(message.entities)) { text = formatHTML(text, message.entities) } + text = text.replace(/\n/g, '
') return { from, replyMessage, text, media, diff --git a/package-lock.json b/package-lock.json index c97fe93..efab6f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "devDependencies": { "async": "^3.2.4", "axios": "^1.4.0", + "emoji-db": "^14.0.1", "eslint": "^8.6.0", "eslint-config-standard": "^12.0.0", "eslint-plugin-import": "^2.17.3", @@ -1122,9 +1123,9 @@ } }, "node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { "ms": "2.1.2" }, @@ -1272,7 +1273,8 @@ "node_modules/emoji-db": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/emoji-db/-/emoji-db-14.0.1.tgz", - "integrity": "sha512-MFiCUr4DsehfCRPAS845AJR4RsekQkh4bUpfag7de3H7FlDHAEbL8Oq03bPzhl4W6rJgrGFhRkcll101ZpQjjg==" + "integrity": "sha512-MFiCUr4DsehfCRPAS845AJR4RsekQkh4bUpfag7de3H7FlDHAEbL8Oq03bPzhl4W6rJgrGFhRkcll101ZpQjjg==", + "dev": true }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -2509,9 +2511,9 @@ } }, "node_modules/is-core-module": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", - "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -3871,12 +3873,12 @@ } }, "node_modules/resolve": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", - "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", "dev": true, "dependencies": { - "is-core-module": "^2.8.0", + "is-core-module": "^2.11.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, diff --git a/test/async.js b/test/async.js index dab4a26..6020bcd 100644 --- a/test/async.js +++ b/test/async.js @@ -46,21 +46,27 @@ const testTemplates = [ const nQuotes = parseInt(process.argv[2]) const nCallsLimit = parseInt(process.argv[3]) -const buildMessage = (index) => { +const buildMessage = (index, hasReply) => { const showAvatar = index === 0 || Math.random() < 0.3 - const fromId = Math.floor(Math.random() * 100) - const fromName = lorem.generateWords(Math.floor(Math.random() * 2)) + const fromId = Math.floor(Math.random() * 100) + 1 + const fromName = lorem.generateWords(Math.floor(Math.random() * 2) + 1) const photo = { url: 'https://telegra.ph/file/59952c903fdfb10b752b3.jpg' } - const text = lorem.generateParagraphs(1) + const paragraphs = Array.from( + { length: Math.floor(Math.random(3)) + 1 }, + (_, k) => lorem.generateParagraphs(1) + ) + const text = paragraphs.join('\n\n') - const replyMessage = { - from: { - id: Math.floor(Math.random() * 100), - name: lorem.generateWords(Math.floor(Math.random() * 2)) - }, - text: lorem.generateParagraphs(1) + const media = { + url: `https://via.placeholder.com/${ + Math.floor(Math.random() * 1900) + 100 + }x${ + Math.floor(Math.random() * 1900) + 100 + }` } + const replyMessage = hasReply ? buildMessage(index, false) : null + return { entities: [], avatar: showAvatar, @@ -70,7 +76,8 @@ const buildMessage = (index) => { photo: Math.random() < 0.5 ? photo : null }, text, - replyMessage: Math.random() < 0.3 ? replyMessage : {} + replyMessage, + media: Math.random() < 0.3 ? media : null } } @@ -94,7 +101,10 @@ const queue = async.queue(({ json, template, i }, cb) => { for (let i = 0; i < nQuotes; i++) { const backgroundColor = Math.random() < 0.3 ? '#FFFFFF' : '' - const messages = Array.from({ length: i % 5 + 1 }, (_, k) => buildMessage(k)) + const messages = Array.from( + { length: i % 5 + 1 }, + (_, k) => buildMessage(k, Math.random() < 0.3) + ) const json = { botToken: process.env.BOT_TOKEN, diff --git a/test/stress.js b/test/stress.js index 6f2bbf9..35d0a7b 100644 --- a/test/stress.js +++ b/test/stress.js @@ -44,24 +44,26 @@ const testTemplates = [ const nQuotes = parseInt(process.argv[2]) -const buildMessage = (index) => { +const buildMessage = (index, hasReply) => { const showAvatar = index === 0 || Math.random() < 0.3 - const fromId = Math.floor(Math.random() * 100) - const fromName = lorem.generateWords(Math.floor(Math.random() * 2)) + const fromId = Math.floor(Math.random() * 100) + 1 + const fromName = lorem.generateWords(Math.floor(Math.random() * 2) + 1) const photo = { url: 'https://telegra.ph/file/59952c903fdfb10b752b3.jpg' } - const text = lorem.generateParagraphs(1) + const paragraphs = Array.from( + { length: Math.floor(Math.random(3)) + 1 }, + (_, k) => lorem.generateParagraphs(1) + ) + const text = paragraphs.join('\n\n') const media = { - url: `https://via.placeholder.com/${Math.floor(Math.random() * 1900) + 100}x${Math.floor(Math.random() * 1900) + 100}` + url: `https://via.placeholder.com/${ + Math.floor(Math.random() * 1900) + 100 + }x${ + Math.floor(Math.random() * 1900) + 100 + }` } - const replyMessage = { - from: { - id: Math.floor(Math.random() * 100), - name: lorem.generateWords(Math.floor(Math.random() * 2)) - }, - text: lorem.generateParagraphs(1) - } + const replyMessage = hasReply ? buildMessage(index, false) : null return { entities: [], @@ -72,15 +74,18 @@ const buildMessage = (index) => { photo: Math.random() < 0.5 ? photo : null }, text, - media: Math.random() < 0.3 ? media : null, - replyMessage: Math.random() < 0.3 ? replyMessage : {} + replyMessage, + media: Math.random() < 0.3 ? media : null } } ;(async () => { for (let i = 0; i < nQuotes; i++) { const backgroundColor = Math.random() < 0.3 ? '#FFFFFF' : '' - const messages = Array.from({ length: i % 5 + 1 }, (_, k) => buildMessage(k)) + const messages = Array.from( + { length: i % 5 + 1 }, + (_, k) => buildMessage(k, Math.random() < 0.3) + ) const json = { botToken: process.env.BOT_TOKEN, From 5d353583aa49d8b1c4ecbf9f84448aafacba5774 Mon Sep 17 00:00:00 2001 From: arelive Date: Sat, 10 Jun 2023 22:32:35 +0300 Subject: [PATCH 16/60] render custom emoji --- methods/generate.js | 2 +- utils/format-html.js | 49 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/methods/generate.js b/methods/generate.js index 85883a6..8ebf47b 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -119,7 +119,7 @@ const buildMessage = async (message, theme) => { let text = message.text ?? '' if (Array.isArray(message.entities)) { - text = formatHTML(text, message.entities) + text = await formatHTML(text, message.entities) } text = text.replace(/\n/g, '
') diff --git a/utils/format-html.js b/utils/format-html.js index 5eb1ca5..72edce7 100644 --- a/utils/format-html.js +++ b/utils/format-html.js @@ -1,3 +1,5 @@ +const telegram = require('./telegram') + const escapedChars = { '"': '"', '&': '&', @@ -10,11 +12,13 @@ const escapeHTML = (string) => { return chars.map(char => escapedChars[char] || char).join('') } - -module.exports = (text = '', entities = []) => { +module.exports = async (text = '', entities = []) => { const available = [...entities] const opened = [] const result = [] + const customEmojiSlices = [] + const requiredCustomEmojiIds = new Set() + for (let offset = 0; offset < text.length; offset++) { while (true) { const index = available.findIndex((entity) => entity.offset === offset) @@ -75,6 +79,12 @@ module.exports = (text = '', entities = []) => { case 'phone_number': result.push(``) break + case 'custom_emoji': + const emojiId = '' + entity.custom_emoji_id + customEmojiSlices.push({ beginIndex: result.length, emojiId: emojiId }) + requiredCustomEmojiIds.add(emojiId) + result.push('') + break } opened.unshift(entity) available.splice(index, 1) @@ -125,9 +135,44 @@ module.exports = (text = '', entities = []) => { case 'phone_number': result.push('') break + case 'custom_emoji': + customEmojiSlices[customEmojiSlices.length - 1].endIndex = result.length + result.push('') + break } opened.splice(index, 1) } } + + if (customEmojiSlices.length) { + const customEmojiFileURLs = {} + const customEmojiStickers = await telegram.callApi('getCustomEmojiStickers', { + custom_emoji_ids: [...requiredCustomEmojiIds] + }).catch(() => {}) + + if (customEmojiStickers) { + await Promise.all(customEmojiStickers.map( + async sticker => async () => { + const fileId = sticker.thumb.file_id + const fileURL = await this.telegram.getFileLink(fileId).catch(() => {}) + customEmojiFileURLs[sticker.custom_emoji_id] = fileURL + } + )) + + for (let slice of customEmojiSlices) { + if (customEmojiFileURLs[slice.emojiId]) { + const emojiURL = customEmojiFileURLs[slice.emojiId] + let altRepr = '' + + for (let i = slice.beginIndex; i < slice.endIndex; i++) { + altRepr += result[i] + result[i] = '' + } + result[slice.endIndex] = `${altRepr}` + } + } + } + } + return result.join('') } From ec5af6a73b0c8d1421ad53db9f3370945c6b6e66 Mon Sep 17 00:00:00 2001 From: arelive Date: Sat, 10 Jun 2023 23:27:22 +0300 Subject: [PATCH 17/60] allow emoji statuses --- methods/generate.js | 17 ++++++++++++++++- utils/format-html.js | 10 +++++----- views/html.hbs | 36 +++++++++++++++++++++++++----------- views/image.hbs | 36 +++++++++++++++++++++++++----------- views/quote.hbs | 36 +++++++++++++++++++++++++----------- 5 files changed, 96 insertions(+), 39 deletions(-) diff --git a/methods/generate.js b/methods/generate.js index 8ebf47b..49fe93a 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -10,6 +10,7 @@ const getAvatarURL = require('../utils/get-avatar-url') const getMediaURL = require('../utils/get-media-url') const lightOrDark = require('../utils/light-or-dark') const formatHTML = require('../utils/format-html') +const telegram = require('../utils/telegram') const normalizeColor = (color) => { const canvas = createCanvas(0, 0) @@ -53,10 +54,24 @@ const imageAlpha = (image, alpha) => { return canvas } +const getEmojiStatusURL = async (emojiId) => { + const customEmojiStickers = await telegram.callApi('getCustomEmojiStickers', { + custom_emoji_ids: [emojiId] + }).catch(() => {}) + + if (!Array.isArray(customEmojiStickers) || !customEmojiStickers.length) { + return null + } + + const fileId = customEmojiStickers[0].thumb.file_id + const fileURL = await telegram.getFileLink(fileId).catch(() => {}) + return fileURL || null +} + const buildUser = async (user, theme, options={ getAvatar: false }) => { const index = user.id ? Math.abs(user.id) % 7 : 1 const color = userColors[theme][index] - const emojiStatus = user.emojiStatus ?? '' + const emojiStatus = user.emoji_status ? await getEmojiStatusURL(user.emoji_status) : null let photo = null if (options.getAvatar) { diff --git a/utils/format-html.js b/utils/format-html.js index 72edce7..ff40b6f 100644 --- a/utils/format-html.js +++ b/utils/format-html.js @@ -150,13 +150,13 @@ module.exports = async (text = '', entities = []) => { custom_emoji_ids: [...requiredCustomEmojiIds] }).catch(() => {}) - if (customEmojiStickers) { + if (Array.isArray(customEmojiStickers)) { await Promise.all(customEmojiStickers.map( - async sticker => async () => { + async sticker => (async () => { const fileId = sticker.thumb.file_id - const fileURL = await this.telegram.getFileLink(fileId).catch(() => {}) + const fileURL = await telegram.getFileLink(fileId).catch(() => {}) customEmojiFileURLs[sticker.custom_emoji_id] = fileURL - } + })() )) for (let slice of customEmojiSlices) { @@ -168,7 +168,7 @@ module.exports = async (text = '', entities = []) => { altRepr += result[i] result[i] = '' } - result[slice.endIndex] = `${altRepr}` + result[slice.endIndex] = `${altRepr}` } } } diff --git a/views/html.hbs b/views/html.hbs index 5f3a6dd..fe6f039 100644 --- a/views/html.hbs +++ b/views/html.hbs @@ -70,8 +70,11 @@ width: 100%; font: 24px/1.2 'Noto Sans', sans-serif; text-overflow: ellipsis; + color: #000; + } + .dark .message-text, + .dark .message-reply-text { color: #FFF; - mix-blend-mode: difference; } .message-reply-text { font-size: 21px; @@ -92,14 +95,21 @@ text-decoration: none; color: blue; } - .light a, - .light .message-text .mention, - .light .message-text .hashtag, - .light .message-text .cashtag, - .light .message-text .bot-command, - .light .message-text .email, - .light .message-text .phone-number { - color: yellow; + .custom-emoji, + .emoji-status { + display: inline; + } + .message-reply-text .custom-emoji { + height: 21px; + } + .message-text .custom-emoji { + height: 24px; + } + .message-from-name .emoji-status { + height: 22px; + } + .message-reply-from-name .emoji-status { + height: 16px; } .watermark { position: absolute; @@ -130,7 +140,9 @@

{{this.from.name}} - {{this.from.emojiStatus}} + {{#if this.from.emojiStatus}} + + {{/if}}

@@ -139,7 +151,9 @@

{{this.from.name}} - {{this.from.emojiStatus}} + {{#if this.from.emojiStatus}} + + {{/if}}

{{this.text}}

diff --git a/views/image.hbs b/views/image.hbs index 7154e95..7c7c9d2 100644 --- a/views/image.hbs +++ b/views/image.hbs @@ -71,8 +71,11 @@ width: 100%; font: 24px/1.2 'Noto Sans', sans-serif; text-overflow: ellipsis; + color: #000; + } + .dark .message-text, + .dark .message-reply-text { color: #FFF; - mix-blend-mode: difference; } .message-reply-text { font-size: 21px; @@ -93,14 +96,21 @@ text-decoration: none; color: blue; } - .light a, - .light .message-text .mention, - .light .message-text .hashtag, - .light .message-text .cashtag, - .light .message-text .bot-command, - .light .message-text .email, - .light .message-text .phone-number { - color: yellow; + .custom-emoji, + .emoji-status { + display: inline; + } + .message-reply-text .custom-emoji { + height: 21px; + } + .message-text .custom-emoji { + height: 24px; + } + .message-from-name .emoji-status { + height: 22px; + } + .message-reply-from-name .emoji-status { + height: 16px; } .watermark { position: absolute; @@ -131,7 +141,9 @@

{{this.from.name}} - {{this.from.emojiStatus}} + {{#if this.from.emojiStatus}} + + {{/if}}

@@ -140,7 +152,9 @@

{{this.from.name}} - {{this.from.emojiStatus}} + {{#if this.from.emojiStatus}} + + {{/if}}

{{this.text}}

diff --git a/views/quote.hbs b/views/quote.hbs index eedd1d7..8130f82 100644 --- a/views/quote.hbs +++ b/views/quote.hbs @@ -68,8 +68,11 @@ width: 100%; font: 24px/1.2 'Noto Sans', sans-serif; text-overflow: ellipsis; + color: #000; + } + .dark .message-text, + .dark .message-reply-text { color: #FFF; - mix-blend-mode: difference; } .message-reply-text { font-size: 21px; @@ -90,14 +93,21 @@ text-decoration: none; color: blue; } - .light a, - .light .message-text .mention, - .light .message-text .hashtag, - .light .message-text .cashtag, - .light .message-text .bot-command, - .light .message-text .email, - .light .message-text .phone-number { - color: yellow; + .custom-emoji, + .emoji-status { + display: inline; + } + .message-reply-text .custom-emoji { + height: 21px; + } + .message-text .custom-emoji { + height: 24px; + } + .message-from-name .emoji-status { + height: 22px; + } + .message-reply-from-name .emoji-status { + height: 16px; } @@ -120,7 +130,9 @@

{{this.from.name}} - {{this.from.emojiStatus}} + {{#if this.from.emojiStatus}} + + {{/if}}

@@ -129,7 +141,9 @@

{{this.from.name}} - {{this.from.emojiStatus}} + {{#if this.from.emojiStatus}} + + {{/if}}

{{this.text}}

From c034d97daa7c8252aab9513a879ab2373ccb4b3a Mon Sep 17 00:00:00 2001 From: arelive Date: Sun, 11 Jun 2023 23:11:00 +0300 Subject: [PATCH 18/60] allow stickers --- methods/generate.js | 16 +++++++++------- test/async.js | 3 ++- test/stress.js | 3 ++- utils/get-media-url.js | 4 ++-- views/html.hbs | 11 ++++++++++- views/image.hbs | 11 ++++++++++- views/quote.hbs | 11 ++++++++++- 7 files changed, 45 insertions(+), 14 deletions(-) diff --git a/methods/generate.js b/methods/generate.js index 49fe93a..fa069c9 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -122,14 +122,16 @@ const buildMessage = async (message, theme) => { } let media = message.media || null - if (media && !media.url) { - if (media.length) { - const mediaId = media.pop() - const mediaURL = await getMediaURL(mediaId) - media = { url: mediaURL } - } else { - media = null + if (media) { + if (!media.url) { + if (media.length) { + const mediaInfo = media.pop() + const mediaURL = await getMediaURL(mediaInfo) + } else { + media = null + } } + media.type = message.mediaType || (mediaURL.endsWith('.webp') ? 'sticker' : 'image') } let text = message.text ?? '' diff --git a/test/async.js b/test/async.js index 6020bcd..6ba351c 100644 --- a/test/async.js +++ b/test/async.js @@ -77,7 +77,8 @@ const buildMessage = (index, hasReply) => { }, text, replyMessage, - media: Math.random() < 0.3 ? media : null + media: Math.random() < 0.3 ? media : null, + mediaType: Math.random() < 1.5 ? 'sticker' : 'image' } } diff --git a/test/stress.js b/test/stress.js index 35d0a7b..4142e33 100644 --- a/test/stress.js +++ b/test/stress.js @@ -75,7 +75,8 @@ const buildMessage = (index, hasReply) => { }, text, replyMessage, - media: Math.random() < 0.3 ? media : null + media: Math.random() < 0.3 ? media : null, + mediaType: Math.random() < 1.5 ? 'sticker' : 'image' } } diff --git a/utils/get-media-url.js b/utils/get-media-url.js index c59d69f..af1bbae 100644 --- a/utils/get-media-url.js +++ b/utils/get-media-url.js @@ -1,6 +1,6 @@ const telegram = require('./telegram') -module.exports = async (mediaId) => { - const mediaURL = await telegram.getFileLink(mediaId).catch(console.error) +module.exports = async (mediaInfo) => { + const mediaURL = await telegram.getFileLink(mediaInfo).catch(console.error) return mediaURL } diff --git a/views/html.hbs b/views/html.hbs index fe6f039..6529ec9 100644 --- a/views/html.hbs +++ b/views/html.hbs @@ -111,6 +111,15 @@ .message-reply-from-name .emoji-status { height: 16px; } + .message-media { + min-width: 100%; + max-height: 512px; + object-fit: cover; + object-position: center; + } + .message-media.sticker { + border-radius: 5px; + } .watermark { position: absolute; bottom: 5px; @@ -160,7 +169,7 @@ {{/with}} {{/if}} {{#if this.media}} - + {{/if}}

{{{this.text}}}

diff --git a/views/image.hbs b/views/image.hbs index 7c7c9d2..b9acb2c 100644 --- a/views/image.hbs +++ b/views/image.hbs @@ -112,6 +112,15 @@ .message-reply-from-name .emoji-status { height: 16px; } + .message-media { + min-width: 100%; + max-height: 512px; + object-fit: cover; + object-position: center; + } + .message-media.sticker { + border-radius: 5px; + } .watermark { position: absolute; bottom: 5px; @@ -161,7 +170,7 @@ {{/with}} {{/if}} {{#if this.media}} - + {{/if}}

{{{this.text}}}

diff --git a/views/quote.hbs b/views/quote.hbs index 8130f82..3d31d21 100644 --- a/views/quote.hbs +++ b/views/quote.hbs @@ -109,6 +109,15 @@ .message-reply-from-name .emoji-status { height: 16px; } + .message-media { + min-width: 100%; + max-height: 512px; + object-fit: cover; + object-position: center; + } + .message-media.sticker { + border-radius: 5px; + } @@ -150,7 +159,7 @@ {{/with}} {{/if}} {{#if this.media}} - + {{/if}}

{{{this.text}}}

From 30fe39b6dfa4f36878299690c3c915b7ca70c587 Mon Sep 17 00:00:00 2001 From: arelive Date: Sun, 11 Jun 2023 23:12:07 +0300 Subject: [PATCH 19/60] hotfix of tests --- test/async.js | 2 +- test/stress.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/async.js b/test/async.js index 6ba351c..929105d 100644 --- a/test/async.js +++ b/test/async.js @@ -78,7 +78,7 @@ const buildMessage = (index, hasReply) => { text, replyMessage, media: Math.random() < 0.3 ? media : null, - mediaType: Math.random() < 1.5 ? 'sticker' : 'image' + mediaType: Math.random() < 0.5 ? 'sticker' : 'image' } } diff --git a/test/stress.js b/test/stress.js index 4142e33..75745f2 100644 --- a/test/stress.js +++ b/test/stress.js @@ -76,7 +76,7 @@ const buildMessage = (index, hasReply) => { text, replyMessage, media: Math.random() < 0.3 ? media : null, - mediaType: Math.random() < 1.5 ? 'sticker' : 'image' + mediaType: Math.random() < 0.5 ? 'sticker' : 'image' } } From 470df7c21c3800be0f22a13ab36bc47f1dfa2ad7 Mon Sep 17 00:00:00 2001 From: arelive Date: Tue, 13 Jun 2023 17:52:45 +0300 Subject: [PATCH 20/60] fix: from info in reply message; add png type --- methods/generate.js | 14 +++- views/png.hbs | 180 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 views/png.hbs diff --git a/methods/generate.js b/methods/generate.js index fa069c9..4cadc5d 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -112,6 +112,15 @@ const buildMessage = async (message, theme) => { let replyMessage = message.replyMessage if (replyMessage && Object.keys(replyMessage) != 0) { + // kostyl + if (!replyMessage.from) { + replyMessage.from = { + id: replyMessage.chatId, + name: replyMessage.name, + emoji_status: null, + photo: null + } + } replyMessage = { from: await buildUser(replyMessage.from, theme), text: replyMessage.text @@ -127,11 +136,14 @@ const buildMessage = async (message, theme) => { if (media.length) { const mediaInfo = media.pop() const mediaURL = await getMediaURL(mediaInfo) + media = { url: mediaURL } } else { media = null } } - media.type = message.mediaType || (mediaURL.endsWith('.webp') ? 'sticker' : 'image') + } + if (media) { + media.type = message.mediaType || (media.url.endsWith('.webp') ? 'sticker' : 'image') } let text = message.text ?? '' diff --git a/views/png.hbs b/views/png.hbs new file mode 100644 index 0000000..bbec5bb --- /dev/null +++ b/views/png.hbs @@ -0,0 +1,180 @@ + + + + + + + + + +
+
    + {{#each messages}} +
  • + {{#if this.showAvatar}} + {{#if this.from.photo}} + {{this.from.initials}} + {{else}} +

    + {{this.from.initials}} +

    + {{/if}} + {{/if}} +
    +
    +

    + {{this.from.name}} + {{#if this.from.emojiStatus}} + + {{/if}} +

    +
    +
    + {{#if this.replyMessage}} + {{#with this.replyMessage}} +
    +

    + {{this.from.name}} + {{#if this.from.emojiStatus}} + + {{/if}} +

    +

    {{this.text}}

    +
    + {{/with}} + {{/if}} + {{#if this.media}} + + {{/if}} +

    {{{this.text}}}

    +
    +
    +
  • + {{/each}} +
+

@QuotLyBot

+ + From ac87509790c6d63afbba2989867a0cec19d14d96 Mon Sep 17 00:00:00 2001 From: arelive Date: Tue, 13 Jun 2023 18:28:45 +0300 Subject: [PATCH 21/60] refactor: cache views, separate css and html --- methods/generate.js | 2 +- utils/get-view.js | 18 ++++- views/default.hbs | 59 ++++++++++++++ views/html.css | 124 ++++++++++++++++++++++++++++++ views/html.hbs | 182 ------------------------------------------- views/image.css | 124 ++++++++++++++++++++++++++++++ views/image.hbs | 183 -------------------------------------------- views/message.hbs | 52 ------------- views/png.css | 121 +++++++++++++++++++++++++++++ views/png.hbs | 180 ------------------------------------------- views/quote.css | 113 +++++++++++++++++++++++++++ views/quote.hbs | 171 ----------------------------------------- 12 files changed, 558 insertions(+), 771 deletions(-) create mode 100644 views/default.hbs create mode 100644 views/html.css delete mode 100644 views/html.hbs create mode 100644 views/image.css delete mode 100644 views/image.hbs delete mode 100644 views/message.hbs create mode 100644 views/png.css delete mode 100644 views/png.hbs create mode 100644 views/quote.css delete mode 100644 views/quote.hbs diff --git a/methods/generate.js b/methods/generate.js index 4cadc5d..0a6a36b 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -203,7 +203,7 @@ module.exports = async (parm) => { return { error: 'messages_empty' } } - const content = getView(type)({ + const content = getView('default', type)({ scale, theme, background: { diff --git a/utils/get-view.js b/utils/get-view.js index 0a9654d..6950bea 100644 --- a/utils/get-view.js +++ b/utils/get-view.js @@ -2,10 +2,24 @@ const path = require('path') const fs = require('fs') const Handlebars = require('handlebars') -module.exports = viewName => { +const cache = {} + +module.exports = (viewName, styleName) => { + const cacheKey = `${viewName}/${styleName}` + if (cache[cacheKey]) { + return cache[cacheKey] + } + const template = fs.readFileSync( path.resolve(`./views/${viewName}.hbs`) ).toString('utf-8') - return Handlebars.compile(template) + const style = fs.readFileSync( + path.resolve(`./views/${styleName}.css`) + ).toString('utf-8') + + const view = Handlebars.compile(template.replace('{{> style}}', style)) + + cache[cacheKey] = view + return view } diff --git a/views/default.hbs b/views/default.hbs new file mode 100644 index 0000000..999a215 --- /dev/null +++ b/views/default.hbs @@ -0,0 +1,59 @@ + + + + + + + + + +
+
    + {{#each messages}} +
  • + {{#if this.showAvatar}} + {{#if this.from.photo}} + {{this.from.initials}} + {{else}} +

    + {{this.from.initials}} +

    + {{/if}} + {{/if}} +
    +
    +

    + {{this.from.name}} + {{#if this.from.emojiStatus}} + + {{/if}} +

    +
    +
    + {{#if this.replyMessage}} + {{#with this.replyMessage}} +
    +

    + {{this.from.name}} + {{#if this.from.emojiStatus}} + + {{/if}} +

    +

    {{this.text}}

    +
    + {{/with}} + {{/if}} + {{#if this.media}} + + {{/if}} +

    {{{this.text}}}

    +
    +
    +
  • + {{/each}} +
+

@QuotLyBot

+ + diff --git a/views/html.css b/views/html.css new file mode 100644 index 0000000..3646c46 --- /dev/null +++ b/views/html.css @@ -0,0 +1,124 @@ +body { + background: transparent; +} +#quote { + position: absolute; + padding: 35px 55px; + max-width: 512px; + max-width: {{width}}px; + max-height: {{height}}px; + background: + url("{{background.image.url}}") top left / cover repeat, + radial-gradient(ellipse farthest-corner at center, {{background.color1}}, {{background.color2}}); +} +.messages-list { + list-style: none; +} +.message-item { + position: relative; + display: block; + margin-bottom: 5px; +} +.message-item:last-child { + margin-bottom: 0; +} +.message-from-avatar, +.message-from-initials { + position: absolute; + top: 0; + left: 0; + width: 50px; + height: 50px; + font: 25px/2 'Nono Sans', sans-serif; + text-align: center; + color: #FFF; + border-radius: 50%; + box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); +} +.message { + margin-left: 65px; + padding: 14px; + background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}}); + border-radius: 25px; + box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); +} +.message-from-name, +.message-reply-from-name { + overflow: hidden; + width: 100%; + font: bold 22px/1.2 'Noto Sans', sans-serif; + white-space: nowrap; + text-overflow: ellipsis; +} +.message-reply { + margin: 8px 0; + padding: 0 14px; + border-left: 3px solid; +} +.message-reply-from-name { + font-size: 16px; +} +.message-text, +.message-reply-text { + overflow: hidden; + width: 100%; + font: 24px/1.2 'Noto Sans', sans-serif; + text-overflow: ellipsis; + color: #000; +} +.dark .message-text, +.dark .message-reply-text { + color: #FFF; +} +.message-reply-text { + font-size: 21px; + white-space: nowrap; +} +.message-text code { + font-family: 'Noto Mono', monospace; +} +.message-text .spoiler { + opacity: .3; +} +.message-text .mention, +.message-text .hashtag, +.message-text .cashtag, +.message-text .bot-command, +.message-text .email, +.message-text .phone-number { + text-decoration: none; + color: blue; +} +.custom-emoji, +.emoji-status { + display: inline; +} +.message-reply-text .custom-emoji { + height: 21px; +} +.message-text .custom-emoji { + height: 24px; +} +.message-from-name .emoji-status { + height: 22px; +} +.message-reply-from-name .emoji-status { + height: 16px; +} +.message-media { + min-width: 100%; + max-height: 512px; + object-fit: cover; + object-position: center; +} +.message-media.sticker { + border-radius: 5px; +} +.watermark { + position: absolute; + bottom: 5px; + right: 5px; + font: 12px 'Noto Sans'; + text-align: right; + color: rgba(0, 0, 0, .3); +} diff --git a/views/html.hbs b/views/html.hbs deleted file mode 100644 index 6529ec9..0000000 --- a/views/html.hbs +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - - - - -
-
    - {{#each messages}} -
  • - {{#if this.showAvatar}} - {{#if this.from.photo}} - {{this.from.initials}} - {{else}} -

    - {{this.from.initials}} -

    - {{/if}} - {{/if}} -
    -
    -

    - {{this.from.name}} - {{#if this.from.emojiStatus}} - - {{/if}} -

    -
    -
    - {{#if this.replyMessage}} - {{#with this.replyMessage}} -
    -

    - {{this.from.name}} - {{#if this.from.emojiStatus}} - - {{/if}} -

    -

    {{this.text}}

    -
    - {{/with}} - {{/if}} - {{#if this.media}} - - {{/if}} -

    {{{this.text}}}

    -
    -
    -
  • - {{/each}} -
-

@QuotLyBot

- - diff --git a/views/image.css b/views/image.css new file mode 100644 index 0000000..3534621 --- /dev/null +++ b/views/image.css @@ -0,0 +1,124 @@ +body { + background: transparent; +} +#quote { + position: absolute; + padding: 35px 55px; + max-width: {{width}}px; + max-height: {{height}}px; + transform: scale({{scale}}); + background: + url("{{background.image.url}}") top left / cover repeat, + radial-gradient(ellipse farthest-corner at center, {{background.color1}}, {{background.color2}}); +} +.messages-list { + list-style: none; +} +.message-item { + position: relative; + display: block; + margin-bottom: 5px; +} +.message-item:last-child { + margin-bottom: 0; +} +.message-from-avatar, +.message-from-initials { + position: absolute; + top: 0; + left: 0; + width: 50px; + height: 50px; + font: 25px/2 'Nono Sans', sans-serif; + text-align: center; + color: #FFF; + border-radius: 50%; + box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); +} +.message { + margin-left: 65px; + padding: 14px; + background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}}); + border-radius: 25px; + box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); +} +.message-from-name, +.message-reply-from-name { + overflow: hidden; + width: 100%; + font: bold 22px/1.2 'Noto Sans', sans-serif; + white-space: nowrap; + text-overflow: ellipsis; +} +.message-reply { + margin: 8px 0; + padding: 0 14px; + border-left: 3px solid; +} +.message-reply-from-name { + font-size: 16px; +} +.message-text, +.message-reply-text { + overflow: hidden; + width: 100%; + font: 24px/1.2 'Noto Sans', sans-serif; + text-overflow: ellipsis; + color: #000; +} +.dark .message-text, +.dark .message-reply-text { + color: #FFF; +} +.message-reply-text { + font-size: 21px; + white-space: nowrap; +} +.message-text code { + font-family: 'Noto Mono', monospace; +} +.message-text .spoiler { + opacity: .3; +} +.message-text .mention, +.message-text .hashtag, +.message-text .cashtag, +.message-text .bot-command, +.message-text .email, +.message-text .phone-number { + text-decoration: none; + color: blue; +} +.custom-emoji, +.emoji-status { + display: inline; +} +.message-reply-text .custom-emoji { + height: 21px; +} +.message-text .custom-emoji { + height: 24px; +} +.message-from-name .emoji-status { + height: 22px; +} +.message-reply-from-name .emoji-status { + height: 16px; +} +.message-media { + min-width: 100%; + max-height: 512px; + object-fit: cover; + object-position: center; +} +.message-media.sticker { + border-radius: 5px; +} +.watermark { + position: absolute; + bottom: 5px; + right: 5px; + font: 12px 'Noto Sans'; + text-align: right; + color: rgba(0, 0, 0, .3); +} diff --git a/views/image.hbs b/views/image.hbs deleted file mode 100644 index b9acb2c..0000000 --- a/views/image.hbs +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - - - - -
-
    - {{#each messages}} -
  • - {{#if this.showAvatar}} - {{#if this.from.photo}} - {{this.from.initials}} - {{else}} -

    - {{this.from.initials}} -

    - {{/if}} - {{/if}} -
    -
    -

    - {{this.from.name}} - {{#if this.from.emojiStatus}} - - {{/if}} -

    -
    -
    - {{#if this.replyMessage}} - {{#with this.replyMessage}} -
    -

    - {{this.from.name}} - {{#if this.from.emojiStatus}} - - {{/if}} -

    -

    {{this.text}}

    -
    - {{/with}} - {{/if}} - {{#if this.media}} - - {{/if}} -

    {{{this.text}}}

    -
    -
    -
  • - {{/each}} -
-

@QuotLyBot

- - diff --git a/views/message.hbs b/views/message.hbs deleted file mode 100644 index 1c6a1b9..0000000 --- a/views/message.hbs +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - -
- -

- {{name}} - {{emojiStatus}} -

-

{{text}}

-
- - diff --git a/views/png.css b/views/png.css new file mode 100644 index 0000000..9a42c4d --- /dev/null +++ b/views/png.css @@ -0,0 +1,121 @@ +body { + background: transparent; +} +#quote { + position: absolute; + padding: 35px 55px; + max-width: {{width}}px; + max-height: {{height}}px; + transform: scale({{scale}}); + background: none; +.messages-list { + list-style: none; +} +.message-item { + position: relative; + display: block; + margin-bottom: 5px; +} +.message-item:last-child { + margin-bottom: 0; +} +.message-from-avatar, +.message-from-initials { + position: absolute; + top: 0; + left: 0; + width: 50px; + height: 50px; + font: 25px/2 'Nono Sans', sans-serif; + text-align: center; + color: #FFF; + border-radius: 50%; + box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); +} +.message { + margin-left: 65px; + padding: 14px; + background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}}); + border-radius: 25px; + box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); +} +.message-from-name, +.message-reply-from-name { + overflow: hidden; + width: 100%; + font: bold 22px/1.2 'Noto Sans', sans-serif; + white-space: nowrap; + text-overflow: ellipsis; +} +.message-reply { + margin: 8px 0; + padding: 0 14px; + border-left: 3px solid; +} +.message-reply-from-name { + font-size: 16px; +} +.message-text, +.message-reply-text { + overflow: hidden; + width: 100%; + font: 24px/1.2 'Noto Sans', sans-serif; + text-overflow: ellipsis; + color: #000; +} +.dark .message-text, +.dark .message-reply-text { + color: #FFF; +} +.message-reply-text { + font-size: 21px; + white-space: nowrap; +} +.message-text code { + font-family: 'Noto Mono', monospace; +} +.message-text .spoiler { + opacity: .3; +} +.message-text .mention, +.message-text .hashtag, +.message-text .cashtag, +.message-text .bot-command, +.message-text .email, +.message-text .phone-number { + text-decoration: none; + color: blue; +} +.custom-emoji, +.emoji-status { + display: inline; +} +.message-reply-text .custom-emoji { + height: 21px; +} +.message-text .custom-emoji { + height: 24px; +} +.message-from-name .emoji-status { + height: 22px; +} +.message-reply-from-name .emoji-status { + height: 16px; +} +.message-media { + min-width: 100%; + max-height: 512px; + object-fit: cover; + object-position: center; +} +.message-media.sticker { + border-radius: 5px; +} +.watermark { + position: absolute; + bottom: 5px; + right: 5px; + font: 12px 'Noto Sans'; + text-align: right; + color: rgba(0, 0, 0, .3); +} diff --git a/views/png.hbs b/views/png.hbs deleted file mode 100644 index bbec5bb..0000000 --- a/views/png.hbs +++ /dev/null @@ -1,180 +0,0 @@ - - - - - - - - - -
-
    - {{#each messages}} -
  • - {{#if this.showAvatar}} - {{#if this.from.photo}} - {{this.from.initials}} - {{else}} -

    - {{this.from.initials}} -

    - {{/if}} - {{/if}} -
    -
    -

    - {{this.from.name}} - {{#if this.from.emojiStatus}} - - {{/if}} -

    -
    -
    - {{#if this.replyMessage}} - {{#with this.replyMessage}} -
    -

    - {{this.from.name}} - {{#if this.from.emojiStatus}} - - {{/if}} -

    -

    {{this.text}}

    -
    - {{/with}} - {{/if}} - {{#if this.media}} - - {{/if}} -

    {{{this.text}}}

    -
    -
    -
  • - {{/each}} -
-

@QuotLyBot

- - diff --git a/views/quote.css b/views/quote.css new file mode 100644 index 0000000..36d9d23 --- /dev/null +++ b/views/quote.css @@ -0,0 +1,113 @@ +body { + background: transparent; +} +#quote { + position: absolute; + padding-bottom: 75px; + max-width: {{width}}px; + max-height: {{height}}px; + transform: scale({{scale}}); + background: none; +} +.messages-list { + list-style: none; +} +.message-item { + position: relative; + display: block; + margin-bottom: 5px; +} +.message-item:last-child { + margin-bottom: 0; +} +.message-from-avatar, +.message-from-initials { + position: absolute; + top: 0; + left: 0; + width: 50px; + height: 50px; + font: 25px/2 'Nono Sans', sans-serif; + text-align: center; + color: #FFF; + border-radius: 50%; + box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); +} +.message { + margin-left: 65px; + padding: 14px; + background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}}); + border-radius: 25px; +} +.message-from-name, +.message-reply-from-name { + overflow: hidden; + width: 100%; + font: bold 22px/1.2 'Noto Sans', sans-serif; + white-space: nowrap; + text-overflow: ellipsis; +} +.message-reply { + margin: 8px 0; + padding: 0 14px; + border-left: 3px solid; +} +.message-reply-from-name { + font-size: 16px; +} +.message-text, +.message-reply-text { + overflow: hidden; + width: 100%; + font: 24px/1.2 'Noto Sans', sans-serif; + text-overflow: ellipsis; + color: #000; +} +.dark .message-text, +.dark .message-reply-text { + color: #FFF; +} +.message-reply-text { + font-size: 21px; + white-space: nowrap; +} +.message-text code { + font-family: 'Noto Mono', monospace; +} +.message-text .spoiler { + opacity: .3; +} +.message-text .mention, +.message-text .hashtag, +.message-text .cashtag, +.message-text .bot-command, +.message-text .email, +.message-text .phone-number { + text-decoration: none; + color: blue; +} +.custom-emoji, +.emoji-status { + display: inline; +} +.message-reply-text .custom-emoji { + height: 21px; +} +.message-text .custom-emoji { + height: 24px; +} +.message-from-name .emoji-status { + height: 22px; +} +.message-reply-from-name .emoji-status { + height: 16px; +} +.message-media { + min-width: 100%; + max-height: 512px; + object-fit: cover; + object-position: center; +} +.message-media.sticker { + border-radius: 5px; +} diff --git a/views/quote.hbs b/views/quote.hbs deleted file mode 100644 index 3d31d21..0000000 --- a/views/quote.hbs +++ /dev/null @@ -1,171 +0,0 @@ - - - - - - - - - -
-
    - {{#each messages}} -
  • - {{#if this.showAvatar}} - {{#if this.from.photo}} - {{this.from.initials}} - {{else}} -

    - {{this.from.initials}} -

    - {{/if}} - {{/if}} -
    -
    -

    - {{this.from.name}} - {{#if this.from.emojiStatus}} - - {{/if}} -

    -
    -
    - {{#if this.replyMessage}} - {{#with this.replyMessage}} -
    -

    - {{this.from.name}} - {{#if this.from.emojiStatus}} - - {{/if}} -

    -

    {{this.text}}

    -
    - {{/with}} - {{/if}} - {{#if this.media}} - - {{/if}} -

    {{{this.text}}}

    -
    -
    -
  • - {{/each}} -
- - From cfa5537ffff8ca12194dd6404edea2015490ff35 Mon Sep 17 00:00:00 2001 From: arelive Date: Tue, 13 Jun 2023 18:35:24 +0300 Subject: [PATCH 22/60] style: float quotes to left --- views/html.css | 1 + views/image.css | 1 + views/png.css | 1 + views/quote.css | 1 + 4 files changed, 4 insertions(+) diff --git a/views/html.css b/views/html.css index 3646c46..3b45db9 100644 --- a/views/html.css +++ b/views/html.css @@ -17,6 +17,7 @@ body { .message-item { position: relative; display: block; + float: left; margin-bottom: 5px; } .message-item:last-child { diff --git a/views/image.css b/views/image.css index 3534621..0c231d7 100644 --- a/views/image.css +++ b/views/image.css @@ -17,6 +17,7 @@ body { .message-item { position: relative; display: block; + float: left; margin-bottom: 5px; } .message-item:last-child { diff --git a/views/png.css b/views/png.css index 9a42c4d..354147d 100644 --- a/views/png.css +++ b/views/png.css @@ -14,6 +14,7 @@ body { .message-item { position: relative; display: block; + float: left; margin-bottom: 5px; } .message-item:last-child { diff --git a/views/quote.css b/views/quote.css index 36d9d23..c827a90 100644 --- a/views/quote.css +++ b/views/quote.css @@ -15,6 +15,7 @@ body { .message-item { position: relative; display: block; + float: left; margin-bottom: 5px; } .message-item:last-child { From c2edbf4bd5be9066f6abb7c54ad0ff445ceeb37f Mon Sep 17 00:00:00 2001 From: arelive Date: Tue, 13 Jun 2023 19:18:45 +0300 Subject: [PATCH 23/60] style: pretty for stickers, minimize width --- methods/generate.js | 6 +++++- views/default.hbs | 4 ++-- views/html.css | 21 +++++++++++++++++---- views/image.css | 20 +++++++++++++++++--- views/png.css | 20 +++++++++++++++++--- views/quote.css | 22 ++++++++++++++++++++-- 6 files changed, 78 insertions(+), 15 deletions(-) diff --git a/methods/generate.js b/methods/generate.js index 0a6a36b..3573dbb 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -152,8 +152,10 @@ const buildMessage = async (message, theme) => { } text = text.replace(/\n/g, '
') + const type = media ? media.type == 'sticker' ? 'sticker' : 'image' : 'regular' + return { - from, replyMessage, text, media, + type, from, replyMessage, text, media, showAvatar: message.avatar } } @@ -205,6 +207,8 @@ module.exports = async (parm) => { const content = getView('default', type)({ scale, + width: parm.width, + height: parm.height, theme, background: { image: { url: bgImageURL }, diff --git a/views/default.hbs b/views/default.hbs index 999a215..5537925 100644 --- a/views/default.hbs +++ b/views/default.hbs @@ -12,7 +12,7 @@
diff --git a/views/quote.hbs b/views/quote.hbs index 4b5dec8..159c9b3 100644 --- a/views/quote.hbs +++ b/views/quote.hbs @@ -111,6 +111,9 @@ {{/with}} {{/if}} + {{#if this.media}} + + {{/if}}

{{this.text}}

From ba295d7d5a1435d7f087c16ce448743e1296a4b7 Mon Sep 17 00:00:00 2001 From: arelive Date: Sat, 10 Jun 2023 00:34:04 +0300 Subject: [PATCH 42/60] markup test with entities --- methods/generate.js | 15 +++-- utils/format-html.js | 133 +++++++++++++++++++++++++++++++++++++++++++ views/html.hbs | 26 ++++++++- views/image.hbs | 26 ++++++++- views/quote.hbs | 26 ++++++++- 5 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 utils/format-html.js diff --git a/methods/generate.js b/methods/generate.js index d305a52..a7df4aa 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -9,6 +9,7 @@ const getView = require('../utils/get-view') const getAvatarURL = require('../utils/get-avatar-url') const getMediaURL = require('../utils/get-media-url') const lightOrDark = require('../utils/light-or-dark') +const formatHTML = require('../utils/format-html') const normalizeColor = (color) => { const canvas = createCanvas(0, 0) @@ -92,6 +93,8 @@ const buildUser = async (user, theme, options={ getAvatar: false }) => { } const buildMessage = async (message, theme) => { + const from = await buildUser(message.from, theme, { getAvatar: message.avatar || false }) + let replyMessage = message.replyMessage if (replyMessage && Object.keys(replyMessage) != 0) { replyMessage = { @@ -114,12 +117,14 @@ const buildMessage = async (message, theme) => { } } + let text = message.text ?? '' + if (Array.isArray(message.entities)) { + text = formatHTML(text, message.entities) + } + return { - from: await buildUser(message.from, theme, { getAvatar: message.avatar || false }), - replyMessage, - showAvatar: message.avatar, - text: message.text ?? '', - media + from, replyMessage, text, media, + showAvatar: message.avatar } } diff --git a/utils/format-html.js b/utils/format-html.js new file mode 100644 index 0000000..5eb1ca5 --- /dev/null +++ b/utils/format-html.js @@ -0,0 +1,133 @@ +const escapedChars = { + '"': '"', + '&': '&', + '<': '<', + '>': '>' +} + +const escapeHTML = (string) => { + const chars = [...string] + return chars.map(char => escapedChars[char] || char).join('') +} + + +module.exports = (text = '', entities = []) => { + const available = [...entities] + const opened = [] + const result = [] + for (let offset = 0; offset < text.length; offset++) { + while (true) { + const index = available.findIndex((entity) => entity.offset === offset) + if (index === -1) { + break + } + const entity = available[index] + switch (entity.type) { + case 'bold': + result.push('') + break + case 'italic': + result.push('') + break + case 'code': + result.push('') + break + case 'pre': + if (entity.language) { + result.push(``) + } else { + result.push('') + } + break + case 'strikethrough': + result.push('') + break + case 'underline': + result.push('') + break + case 'text_mention': + result.push(``) + break + case 'text_link': + result.push(``) + break + case 'url': + result.push(``) + break + case 'spoiler': + result.push(``) + break + case 'mention': + result.push(``) + break + case 'hashtag': + result.push(``) + break + case 'cashtag': + result.push(``) + break + case 'bot_command': + result.push(``) + break + case 'email': + result.push(`') + break + case 'italic': + result.push('') + break + case 'code': + result.push('') + break + case 'pre': + if (entity.language) { + result.push('') + } else { + result.push('') + } + break + case 'strikethrough': + result.push('') + break + case 'underline': + result.push('') + break + case 'text_mention': + case 'text_link': + case 'url': + result.push('') + break + case 'spoiler': + case 'mention': + case 'hashtag': + case 'cashtag': + case 'bot_command': + case 'email': + case 'phone_number': + result.push('') + break + } + opened.splice(index, 1) + } + } + return result.join('') +} diff --git a/views/html.hbs b/views/html.hbs index ef0c86b..5f3a6dd 100644 --- a/views/html.hbs +++ b/views/html.hbs @@ -77,6 +77,30 @@ font-size: 21px; white-space: nowrap; } + .message-text code { + font-family: 'Noto Mono', monospace; + } + .message-text .spoiler { + opacity: .3; + } + .message-text .mention, + .message-text .hashtag, + .message-text .cashtag, + .message-text .bot-command, + .message-text .email, + .message-text .phone-number { + text-decoration: none; + color: blue; + } + .light a, + .light .message-text .mention, + .light .message-text .hashtag, + .light .message-text .cashtag, + .light .message-text .bot-command, + .light .message-text .email, + .light .message-text .phone-number { + color: yellow; + } .watermark { position: absolute; bottom: 5px; @@ -124,7 +148,7 @@ {{#if this.media}} {{/if}} -

{{this.text}}

+

{{{this.text}}}

diff --git a/views/image.hbs b/views/image.hbs index ddf355e..7154e95 100644 --- a/views/image.hbs +++ b/views/image.hbs @@ -78,6 +78,30 @@ font-size: 21px; white-space: nowrap; } + .message-text code { + font-family: 'Noto Mono', monospace; + } + .message-text .spoiler { + opacity: .3; + } + .message-text .mention, + .message-text .hashtag, + .message-text .cashtag, + .message-text .bot-command, + .message-text .email, + .message-text .phone-number { + text-decoration: none; + color: blue; + } + .light a, + .light .message-text .mention, + .light .message-text .hashtag, + .light .message-text .cashtag, + .light .message-text .bot-command, + .light .message-text .email, + .light .message-text .phone-number { + color: yellow; + } .watermark { position: absolute; bottom: 5px; @@ -125,7 +149,7 @@ {{#if this.media}} {{/if}} -

{{this.text}}

+

{{{this.text}}}

diff --git a/views/quote.hbs b/views/quote.hbs index 159c9b3..eedd1d7 100644 --- a/views/quote.hbs +++ b/views/quote.hbs @@ -75,6 +75,30 @@ font-size: 21px; white-space: nowrap; } + .message-text code { + font-family: 'Noto Mono', monospace; + } + .message-text .spoiler { + opacity: .3; + } + .message-text .mention, + .message-text .hashtag, + .message-text .cashtag, + .message-text .bot-command, + .message-text .email, + .message-text .phone-number { + text-decoration: none; + color: blue; + } + .light a, + .light .message-text .mention, + .light .message-text .hashtag, + .light .message-text .cashtag, + .light .message-text .bot-command, + .light .message-text .email, + .light .message-text .phone-number { + color: yellow; + } @@ -114,7 +138,7 @@ {{#if this.media}} {{/if}} -

{{this.text}}

+

{{{this.text}}}

From f27c78d8b4b46dc25067952bad5dc77994df6ec2 Mon Sep 17 00:00:00 2001 From: arelive Date: Sat, 10 Jun 2023 20:38:54 +0300 Subject: [PATCH 43/60] fix: uploading avatars --- utils/get-avatar-url.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/get-avatar-url.js b/utils/get-avatar-url.js index ffababb..8ac1a28 100644 --- a/utils/get-avatar-url.js +++ b/utils/get-avatar-url.js @@ -29,7 +29,7 @@ module.exports = async (user) => { return null } - avatarCache.set(user.id, avatarImage) + avatarCache.set(user.id, avatarURL) return avatarURL } From 4ab63677c276734b920dfe2d74c8351968cc58f3 Mon Sep 17 00:00:00 2001 From: arelive Date: Sat, 10 Jun 2023 21:19:57 +0300 Subject: [PATCH 44/60] allow line feeds --- methods/generate.js | 1 + package-lock.json | 24 +++++++++++++----------- test/async.js | 34 ++++++++++++++++++++++------------ test/stress.js | 35 ++++++++++++++++++++--------------- 4 files changed, 56 insertions(+), 38 deletions(-) diff --git a/methods/generate.js b/methods/generate.js index a7df4aa..85883a6 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -121,6 +121,7 @@ const buildMessage = async (message, theme) => { if (Array.isArray(message.entities)) { text = formatHTML(text, message.entities) } + text = text.replace(/\n/g, '
') return { from, replyMessage, text, media, diff --git a/package-lock.json b/package-lock.json index c97fe93..efab6f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "devDependencies": { "async": "^3.2.4", "axios": "^1.4.0", + "emoji-db": "^14.0.1", "eslint": "^8.6.0", "eslint-config-standard": "^12.0.0", "eslint-plugin-import": "^2.17.3", @@ -1122,9 +1123,9 @@ } }, "node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { "ms": "2.1.2" }, @@ -1272,7 +1273,8 @@ "node_modules/emoji-db": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/emoji-db/-/emoji-db-14.0.1.tgz", - "integrity": "sha512-MFiCUr4DsehfCRPAS845AJR4RsekQkh4bUpfag7de3H7FlDHAEbL8Oq03bPzhl4W6rJgrGFhRkcll101ZpQjjg==" + "integrity": "sha512-MFiCUr4DsehfCRPAS845AJR4RsekQkh4bUpfag7de3H7FlDHAEbL8Oq03bPzhl4W6rJgrGFhRkcll101ZpQjjg==", + "dev": true }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -2509,9 +2511,9 @@ } }, "node_modules/is-core-module": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", - "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -3871,12 +3873,12 @@ } }, "node_modules/resolve": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", - "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", "dev": true, "dependencies": { - "is-core-module": "^2.8.0", + "is-core-module": "^2.11.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, diff --git a/test/async.js b/test/async.js index dab4a26..6020bcd 100644 --- a/test/async.js +++ b/test/async.js @@ -46,21 +46,27 @@ const testTemplates = [ const nQuotes = parseInt(process.argv[2]) const nCallsLimit = parseInt(process.argv[3]) -const buildMessage = (index) => { +const buildMessage = (index, hasReply) => { const showAvatar = index === 0 || Math.random() < 0.3 - const fromId = Math.floor(Math.random() * 100) - const fromName = lorem.generateWords(Math.floor(Math.random() * 2)) + const fromId = Math.floor(Math.random() * 100) + 1 + const fromName = lorem.generateWords(Math.floor(Math.random() * 2) + 1) const photo = { url: 'https://telegra.ph/file/59952c903fdfb10b752b3.jpg' } - const text = lorem.generateParagraphs(1) + const paragraphs = Array.from( + { length: Math.floor(Math.random(3)) + 1 }, + (_, k) => lorem.generateParagraphs(1) + ) + const text = paragraphs.join('\n\n') - const replyMessage = { - from: { - id: Math.floor(Math.random() * 100), - name: lorem.generateWords(Math.floor(Math.random() * 2)) - }, - text: lorem.generateParagraphs(1) + const media = { + url: `https://via.placeholder.com/${ + Math.floor(Math.random() * 1900) + 100 + }x${ + Math.floor(Math.random() * 1900) + 100 + }` } + const replyMessage = hasReply ? buildMessage(index, false) : null + return { entities: [], avatar: showAvatar, @@ -70,7 +76,8 @@ const buildMessage = (index) => { photo: Math.random() < 0.5 ? photo : null }, text, - replyMessage: Math.random() < 0.3 ? replyMessage : {} + replyMessage, + media: Math.random() < 0.3 ? media : null } } @@ -94,7 +101,10 @@ const queue = async.queue(({ json, template, i }, cb) => { for (let i = 0; i < nQuotes; i++) { const backgroundColor = Math.random() < 0.3 ? '#FFFFFF' : '' - const messages = Array.from({ length: i % 5 + 1 }, (_, k) => buildMessage(k)) + const messages = Array.from( + { length: i % 5 + 1 }, + (_, k) => buildMessage(k, Math.random() < 0.3) + ) const json = { botToken: process.env.BOT_TOKEN, diff --git a/test/stress.js b/test/stress.js index 6f2bbf9..35d0a7b 100644 --- a/test/stress.js +++ b/test/stress.js @@ -44,24 +44,26 @@ const testTemplates = [ const nQuotes = parseInt(process.argv[2]) -const buildMessage = (index) => { +const buildMessage = (index, hasReply) => { const showAvatar = index === 0 || Math.random() < 0.3 - const fromId = Math.floor(Math.random() * 100) - const fromName = lorem.generateWords(Math.floor(Math.random() * 2)) + const fromId = Math.floor(Math.random() * 100) + 1 + const fromName = lorem.generateWords(Math.floor(Math.random() * 2) + 1) const photo = { url: 'https://telegra.ph/file/59952c903fdfb10b752b3.jpg' } - const text = lorem.generateParagraphs(1) + const paragraphs = Array.from( + { length: Math.floor(Math.random(3)) + 1 }, + (_, k) => lorem.generateParagraphs(1) + ) + const text = paragraphs.join('\n\n') const media = { - url: `https://via.placeholder.com/${Math.floor(Math.random() * 1900) + 100}x${Math.floor(Math.random() * 1900) + 100}` + url: `https://via.placeholder.com/${ + Math.floor(Math.random() * 1900) + 100 + }x${ + Math.floor(Math.random() * 1900) + 100 + }` } - const replyMessage = { - from: { - id: Math.floor(Math.random() * 100), - name: lorem.generateWords(Math.floor(Math.random() * 2)) - }, - text: lorem.generateParagraphs(1) - } + const replyMessage = hasReply ? buildMessage(index, false) : null return { entities: [], @@ -72,15 +74,18 @@ const buildMessage = (index) => { photo: Math.random() < 0.5 ? photo : null }, text, - media: Math.random() < 0.3 ? media : null, - replyMessage: Math.random() < 0.3 ? replyMessage : {} + replyMessage, + media: Math.random() < 0.3 ? media : null } } ;(async () => { for (let i = 0; i < nQuotes; i++) { const backgroundColor = Math.random() < 0.3 ? '#FFFFFF' : '' - const messages = Array.from({ length: i % 5 + 1 }, (_, k) => buildMessage(k)) + const messages = Array.from( + { length: i % 5 + 1 }, + (_, k) => buildMessage(k, Math.random() < 0.3) + ) const json = { botToken: process.env.BOT_TOKEN, From 4804ce1c73feae081c758ea35fc1b148f7ad035e Mon Sep 17 00:00:00 2001 From: arelive Date: Sat, 10 Jun 2023 22:32:35 +0300 Subject: [PATCH 45/60] render custom emoji --- methods/generate.js | 2 +- utils/format-html.js | 49 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/methods/generate.js b/methods/generate.js index 85883a6..8ebf47b 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -119,7 +119,7 @@ const buildMessage = async (message, theme) => { let text = message.text ?? '' if (Array.isArray(message.entities)) { - text = formatHTML(text, message.entities) + text = await formatHTML(text, message.entities) } text = text.replace(/\n/g, '
') diff --git a/utils/format-html.js b/utils/format-html.js index 5eb1ca5..72edce7 100644 --- a/utils/format-html.js +++ b/utils/format-html.js @@ -1,3 +1,5 @@ +const telegram = require('./telegram') + const escapedChars = { '"': '"', '&': '&', @@ -10,11 +12,13 @@ const escapeHTML = (string) => { return chars.map(char => escapedChars[char] || char).join('') } - -module.exports = (text = '', entities = []) => { +module.exports = async (text = '', entities = []) => { const available = [...entities] const opened = [] const result = [] + const customEmojiSlices = [] + const requiredCustomEmojiIds = new Set() + for (let offset = 0; offset < text.length; offset++) { while (true) { const index = available.findIndex((entity) => entity.offset === offset) @@ -75,6 +79,12 @@ module.exports = (text = '', entities = []) => { case 'phone_number': result.push(``) break + case 'custom_emoji': + const emojiId = '' + entity.custom_emoji_id + customEmojiSlices.push({ beginIndex: result.length, emojiId: emojiId }) + requiredCustomEmojiIds.add(emojiId) + result.push('') + break } opened.unshift(entity) available.splice(index, 1) @@ -125,9 +135,44 @@ module.exports = (text = '', entities = []) => { case 'phone_number': result.push('') break + case 'custom_emoji': + customEmojiSlices[customEmojiSlices.length - 1].endIndex = result.length + result.push('') + break } opened.splice(index, 1) } } + + if (customEmojiSlices.length) { + const customEmojiFileURLs = {} + const customEmojiStickers = await telegram.callApi('getCustomEmojiStickers', { + custom_emoji_ids: [...requiredCustomEmojiIds] + }).catch(() => {}) + + if (customEmojiStickers) { + await Promise.all(customEmojiStickers.map( + async sticker => async () => { + const fileId = sticker.thumb.file_id + const fileURL = await this.telegram.getFileLink(fileId).catch(() => {}) + customEmojiFileURLs[sticker.custom_emoji_id] = fileURL + } + )) + + for (let slice of customEmojiSlices) { + if (customEmojiFileURLs[slice.emojiId]) { + const emojiURL = customEmojiFileURLs[slice.emojiId] + let altRepr = '' + + for (let i = slice.beginIndex; i < slice.endIndex; i++) { + altRepr += result[i] + result[i] = '' + } + result[slice.endIndex] = `${altRepr}` + } + } + } + } + return result.join('') } From f0ffdcd1f494ba3b6630bfc410cc64c875e389e7 Mon Sep 17 00:00:00 2001 From: arelive Date: Sat, 10 Jun 2023 23:27:22 +0300 Subject: [PATCH 46/60] allow emoji statuses --- methods/generate.js | 17 ++++++++++++++++- utils/format-html.js | 10 +++++----- views/html.hbs | 36 +++++++++++++++++++++++++----------- views/image.hbs | 36 +++++++++++++++++++++++++----------- views/quote.hbs | 36 +++++++++++++++++++++++++----------- 5 files changed, 96 insertions(+), 39 deletions(-) diff --git a/methods/generate.js b/methods/generate.js index 8ebf47b..49fe93a 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -10,6 +10,7 @@ const getAvatarURL = require('../utils/get-avatar-url') const getMediaURL = require('../utils/get-media-url') const lightOrDark = require('../utils/light-or-dark') const formatHTML = require('../utils/format-html') +const telegram = require('../utils/telegram') const normalizeColor = (color) => { const canvas = createCanvas(0, 0) @@ -53,10 +54,24 @@ const imageAlpha = (image, alpha) => { return canvas } +const getEmojiStatusURL = async (emojiId) => { + const customEmojiStickers = await telegram.callApi('getCustomEmojiStickers', { + custom_emoji_ids: [emojiId] + }).catch(() => {}) + + if (!Array.isArray(customEmojiStickers) || !customEmojiStickers.length) { + return null + } + + const fileId = customEmojiStickers[0].thumb.file_id + const fileURL = await telegram.getFileLink(fileId).catch(() => {}) + return fileURL || null +} + const buildUser = async (user, theme, options={ getAvatar: false }) => { const index = user.id ? Math.abs(user.id) % 7 : 1 const color = userColors[theme][index] - const emojiStatus = user.emojiStatus ?? '' + const emojiStatus = user.emoji_status ? await getEmojiStatusURL(user.emoji_status) : null let photo = null if (options.getAvatar) { diff --git a/utils/format-html.js b/utils/format-html.js index 72edce7..ff40b6f 100644 --- a/utils/format-html.js +++ b/utils/format-html.js @@ -150,13 +150,13 @@ module.exports = async (text = '', entities = []) => { custom_emoji_ids: [...requiredCustomEmojiIds] }).catch(() => {}) - if (customEmojiStickers) { + if (Array.isArray(customEmojiStickers)) { await Promise.all(customEmojiStickers.map( - async sticker => async () => { + async sticker => (async () => { const fileId = sticker.thumb.file_id - const fileURL = await this.telegram.getFileLink(fileId).catch(() => {}) + const fileURL = await telegram.getFileLink(fileId).catch(() => {}) customEmojiFileURLs[sticker.custom_emoji_id] = fileURL - } + })() )) for (let slice of customEmojiSlices) { @@ -168,7 +168,7 @@ module.exports = async (text = '', entities = []) => { altRepr += result[i] result[i] = '' } - result[slice.endIndex] = `${altRepr}` + result[slice.endIndex] = `${altRepr}` } } } diff --git a/views/html.hbs b/views/html.hbs index 5f3a6dd..fe6f039 100644 --- a/views/html.hbs +++ b/views/html.hbs @@ -70,8 +70,11 @@ width: 100%; font: 24px/1.2 'Noto Sans', sans-serif; text-overflow: ellipsis; + color: #000; + } + .dark .message-text, + .dark .message-reply-text { color: #FFF; - mix-blend-mode: difference; } .message-reply-text { font-size: 21px; @@ -92,14 +95,21 @@ text-decoration: none; color: blue; } - .light a, - .light .message-text .mention, - .light .message-text .hashtag, - .light .message-text .cashtag, - .light .message-text .bot-command, - .light .message-text .email, - .light .message-text .phone-number { - color: yellow; + .custom-emoji, + .emoji-status { + display: inline; + } + .message-reply-text .custom-emoji { + height: 21px; + } + .message-text .custom-emoji { + height: 24px; + } + .message-from-name .emoji-status { + height: 22px; + } + .message-reply-from-name .emoji-status { + height: 16px; } .watermark { position: absolute; @@ -130,7 +140,9 @@

{{this.from.name}} - {{this.from.emojiStatus}} + {{#if this.from.emojiStatus}} + + {{/if}}

@@ -139,7 +151,9 @@

{{this.from.name}} - {{this.from.emojiStatus}} + {{#if this.from.emojiStatus}} + + {{/if}}

{{this.text}}

diff --git a/views/image.hbs b/views/image.hbs index 7154e95..7c7c9d2 100644 --- a/views/image.hbs +++ b/views/image.hbs @@ -71,8 +71,11 @@ width: 100%; font: 24px/1.2 'Noto Sans', sans-serif; text-overflow: ellipsis; + color: #000; + } + .dark .message-text, + .dark .message-reply-text { color: #FFF; - mix-blend-mode: difference; } .message-reply-text { font-size: 21px; @@ -93,14 +96,21 @@ text-decoration: none; color: blue; } - .light a, - .light .message-text .mention, - .light .message-text .hashtag, - .light .message-text .cashtag, - .light .message-text .bot-command, - .light .message-text .email, - .light .message-text .phone-number { - color: yellow; + .custom-emoji, + .emoji-status { + display: inline; + } + .message-reply-text .custom-emoji { + height: 21px; + } + .message-text .custom-emoji { + height: 24px; + } + .message-from-name .emoji-status { + height: 22px; + } + .message-reply-from-name .emoji-status { + height: 16px; } .watermark { position: absolute; @@ -131,7 +141,9 @@

{{this.from.name}} - {{this.from.emojiStatus}} + {{#if this.from.emojiStatus}} + + {{/if}}

@@ -140,7 +152,9 @@

{{this.from.name}} - {{this.from.emojiStatus}} + {{#if this.from.emojiStatus}} + + {{/if}}

{{this.text}}

diff --git a/views/quote.hbs b/views/quote.hbs index eedd1d7..8130f82 100644 --- a/views/quote.hbs +++ b/views/quote.hbs @@ -68,8 +68,11 @@ width: 100%; font: 24px/1.2 'Noto Sans', sans-serif; text-overflow: ellipsis; + color: #000; + } + .dark .message-text, + .dark .message-reply-text { color: #FFF; - mix-blend-mode: difference; } .message-reply-text { font-size: 21px; @@ -90,14 +93,21 @@ text-decoration: none; color: blue; } - .light a, - .light .message-text .mention, - .light .message-text .hashtag, - .light .message-text .cashtag, - .light .message-text .bot-command, - .light .message-text .email, - .light .message-text .phone-number { - color: yellow; + .custom-emoji, + .emoji-status { + display: inline; + } + .message-reply-text .custom-emoji { + height: 21px; + } + .message-text .custom-emoji { + height: 24px; + } + .message-from-name .emoji-status { + height: 22px; + } + .message-reply-from-name .emoji-status { + height: 16px; } @@ -120,7 +130,9 @@

{{this.from.name}} - {{this.from.emojiStatus}} + {{#if this.from.emojiStatus}} + + {{/if}}

@@ -129,7 +141,9 @@

{{this.from.name}} - {{this.from.emojiStatus}} + {{#if this.from.emojiStatus}} + + {{/if}}

{{this.text}}

From 12afe67bbb96b3a3045fb4a0caae3c34f9d397bf Mon Sep 17 00:00:00 2001 From: arelive Date: Sun, 11 Jun 2023 23:11:00 +0300 Subject: [PATCH 47/60] allow stickers --- methods/generate.js | 16 +++++++++------- test/async.js | 3 ++- test/stress.js | 3 ++- utils/get-media-url.js | 4 ++-- views/html.hbs | 11 ++++++++++- views/image.hbs | 11 ++++++++++- views/quote.hbs | 11 ++++++++++- 7 files changed, 45 insertions(+), 14 deletions(-) diff --git a/methods/generate.js b/methods/generate.js index 49fe93a..fa069c9 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -122,14 +122,16 @@ const buildMessage = async (message, theme) => { } let media = message.media || null - if (media && !media.url) { - if (media.length) { - const mediaId = media.pop() - const mediaURL = await getMediaURL(mediaId) - media = { url: mediaURL } - } else { - media = null + if (media) { + if (!media.url) { + if (media.length) { + const mediaInfo = media.pop() + const mediaURL = await getMediaURL(mediaInfo) + } else { + media = null + } } + media.type = message.mediaType || (mediaURL.endsWith('.webp') ? 'sticker' : 'image') } let text = message.text ?? '' diff --git a/test/async.js b/test/async.js index 6020bcd..6ba351c 100644 --- a/test/async.js +++ b/test/async.js @@ -77,7 +77,8 @@ const buildMessage = (index, hasReply) => { }, text, replyMessage, - media: Math.random() < 0.3 ? media : null + media: Math.random() < 0.3 ? media : null, + mediaType: Math.random() < 1.5 ? 'sticker' : 'image' } } diff --git a/test/stress.js b/test/stress.js index 35d0a7b..4142e33 100644 --- a/test/stress.js +++ b/test/stress.js @@ -75,7 +75,8 @@ const buildMessage = (index, hasReply) => { }, text, replyMessage, - media: Math.random() < 0.3 ? media : null + media: Math.random() < 0.3 ? media : null, + mediaType: Math.random() < 1.5 ? 'sticker' : 'image' } } diff --git a/utils/get-media-url.js b/utils/get-media-url.js index c59d69f..af1bbae 100644 --- a/utils/get-media-url.js +++ b/utils/get-media-url.js @@ -1,6 +1,6 @@ const telegram = require('./telegram') -module.exports = async (mediaId) => { - const mediaURL = await telegram.getFileLink(mediaId).catch(console.error) +module.exports = async (mediaInfo) => { + const mediaURL = await telegram.getFileLink(mediaInfo).catch(console.error) return mediaURL } diff --git a/views/html.hbs b/views/html.hbs index fe6f039..6529ec9 100644 --- a/views/html.hbs +++ b/views/html.hbs @@ -111,6 +111,15 @@ .message-reply-from-name .emoji-status { height: 16px; } + .message-media { + min-width: 100%; + max-height: 512px; + object-fit: cover; + object-position: center; + } + .message-media.sticker { + border-radius: 5px; + } .watermark { position: absolute; bottom: 5px; @@ -160,7 +169,7 @@ {{/with}} {{/if}} {{#if this.media}} - + {{/if}}

{{{this.text}}}

diff --git a/views/image.hbs b/views/image.hbs index 7c7c9d2..b9acb2c 100644 --- a/views/image.hbs +++ b/views/image.hbs @@ -112,6 +112,15 @@ .message-reply-from-name .emoji-status { height: 16px; } + .message-media { + min-width: 100%; + max-height: 512px; + object-fit: cover; + object-position: center; + } + .message-media.sticker { + border-radius: 5px; + } .watermark { position: absolute; bottom: 5px; @@ -161,7 +170,7 @@ {{/with}} {{/if}} {{#if this.media}} - + {{/if}}

{{{this.text}}}

diff --git a/views/quote.hbs b/views/quote.hbs index 8130f82..3d31d21 100644 --- a/views/quote.hbs +++ b/views/quote.hbs @@ -109,6 +109,15 @@ .message-reply-from-name .emoji-status { height: 16px; } + .message-media { + min-width: 100%; + max-height: 512px; + object-fit: cover; + object-position: center; + } + .message-media.sticker { + border-radius: 5px; + } @@ -150,7 +159,7 @@ {{/with}} {{/if}} {{#if this.media}} - + {{/if}}

{{{this.text}}}

From e06b650dc361459885b317851c4ce8497dcad1a0 Mon Sep 17 00:00:00 2001 From: arelive Date: Sun, 11 Jun 2023 23:12:07 +0300 Subject: [PATCH 48/60] hotfix of tests --- test/async.js | 2 +- test/stress.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/async.js b/test/async.js index 6ba351c..929105d 100644 --- a/test/async.js +++ b/test/async.js @@ -78,7 +78,7 @@ const buildMessage = (index, hasReply) => { text, replyMessage, media: Math.random() < 0.3 ? media : null, - mediaType: Math.random() < 1.5 ? 'sticker' : 'image' + mediaType: Math.random() < 0.5 ? 'sticker' : 'image' } } diff --git a/test/stress.js b/test/stress.js index 4142e33..75745f2 100644 --- a/test/stress.js +++ b/test/stress.js @@ -76,7 +76,7 @@ const buildMessage = (index, hasReply) => { text, replyMessage, media: Math.random() < 0.3 ? media : null, - mediaType: Math.random() < 1.5 ? 'sticker' : 'image' + mediaType: Math.random() < 0.5 ? 'sticker' : 'image' } } From 46fb9580c4babe4a8a3ba64435db4255f0c85336 Mon Sep 17 00:00:00 2001 From: arelive Date: Tue, 13 Jun 2023 17:52:45 +0300 Subject: [PATCH 49/60] fix: from info in reply message; add png type --- methods/generate.js | 14 +++- views/png.hbs | 180 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 views/png.hbs diff --git a/methods/generate.js b/methods/generate.js index fa069c9..4cadc5d 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -112,6 +112,15 @@ const buildMessage = async (message, theme) => { let replyMessage = message.replyMessage if (replyMessage && Object.keys(replyMessage) != 0) { + // kostyl + if (!replyMessage.from) { + replyMessage.from = { + id: replyMessage.chatId, + name: replyMessage.name, + emoji_status: null, + photo: null + } + } replyMessage = { from: await buildUser(replyMessage.from, theme), text: replyMessage.text @@ -127,11 +136,14 @@ const buildMessage = async (message, theme) => { if (media.length) { const mediaInfo = media.pop() const mediaURL = await getMediaURL(mediaInfo) + media = { url: mediaURL } } else { media = null } } - media.type = message.mediaType || (mediaURL.endsWith('.webp') ? 'sticker' : 'image') + } + if (media) { + media.type = message.mediaType || (media.url.endsWith('.webp') ? 'sticker' : 'image') } let text = message.text ?? '' diff --git a/views/png.hbs b/views/png.hbs new file mode 100644 index 0000000..bbec5bb --- /dev/null +++ b/views/png.hbs @@ -0,0 +1,180 @@ + + + + + + + + + +
+
    + {{#each messages}} +
  • + {{#if this.showAvatar}} + {{#if this.from.photo}} + {{this.from.initials}} + {{else}} +

    + {{this.from.initials}} +

    + {{/if}} + {{/if}} +
    +
    +

    + {{this.from.name}} + {{#if this.from.emojiStatus}} + + {{/if}} +

    +
    +
    + {{#if this.replyMessage}} + {{#with this.replyMessage}} +
    +

    + {{this.from.name}} + {{#if this.from.emojiStatus}} + + {{/if}} +

    +

    {{this.text}}

    +
    + {{/with}} + {{/if}} + {{#if this.media}} + + {{/if}} +

    {{{this.text}}}

    +
    +
    +
  • + {{/each}} +
+

@QuotLyBot

+ + From 9a272ae98a773d9bd69c519979ad409144134189 Mon Sep 17 00:00:00 2001 From: arelive Date: Tue, 13 Jun 2023 18:28:45 +0300 Subject: [PATCH 50/60] refactor: cache views, separate css and html --- methods/generate.js | 2 +- utils/get-view.js | 18 ++++- views/default.hbs | 59 ++++++++++++++ views/html.css | 124 ++++++++++++++++++++++++++++++ views/html.hbs | 182 ------------------------------------------- views/image.css | 124 ++++++++++++++++++++++++++++++ views/image.hbs | 183 -------------------------------------------- views/message.hbs | 52 ------------- views/png.css | 121 +++++++++++++++++++++++++++++ views/png.hbs | 180 ------------------------------------------- views/quote.css | 113 +++++++++++++++++++++++++++ views/quote.hbs | 171 ----------------------------------------- 12 files changed, 558 insertions(+), 771 deletions(-) create mode 100644 views/default.hbs create mode 100644 views/html.css delete mode 100644 views/html.hbs create mode 100644 views/image.css delete mode 100644 views/image.hbs delete mode 100644 views/message.hbs create mode 100644 views/png.css delete mode 100644 views/png.hbs create mode 100644 views/quote.css delete mode 100644 views/quote.hbs diff --git a/methods/generate.js b/methods/generate.js index 4cadc5d..0a6a36b 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -203,7 +203,7 @@ module.exports = async (parm) => { return { error: 'messages_empty' } } - const content = getView(type)({ + const content = getView('default', type)({ scale, theme, background: { diff --git a/utils/get-view.js b/utils/get-view.js index 0a9654d..6950bea 100644 --- a/utils/get-view.js +++ b/utils/get-view.js @@ -2,10 +2,24 @@ const path = require('path') const fs = require('fs') const Handlebars = require('handlebars') -module.exports = viewName => { +const cache = {} + +module.exports = (viewName, styleName) => { + const cacheKey = `${viewName}/${styleName}` + if (cache[cacheKey]) { + return cache[cacheKey] + } + const template = fs.readFileSync( path.resolve(`./views/${viewName}.hbs`) ).toString('utf-8') - return Handlebars.compile(template) + const style = fs.readFileSync( + path.resolve(`./views/${styleName}.css`) + ).toString('utf-8') + + const view = Handlebars.compile(template.replace('{{> style}}', style)) + + cache[cacheKey] = view + return view } diff --git a/views/default.hbs b/views/default.hbs new file mode 100644 index 0000000..999a215 --- /dev/null +++ b/views/default.hbs @@ -0,0 +1,59 @@ + + + + + + + + + +
+
    + {{#each messages}} +
  • + {{#if this.showAvatar}} + {{#if this.from.photo}} + {{this.from.initials}} + {{else}} +

    + {{this.from.initials}} +

    + {{/if}} + {{/if}} +
    +
    +

    + {{this.from.name}} + {{#if this.from.emojiStatus}} + + {{/if}} +

    +
    +
    + {{#if this.replyMessage}} + {{#with this.replyMessage}} +
    +

    + {{this.from.name}} + {{#if this.from.emojiStatus}} + + {{/if}} +

    +

    {{this.text}}

    +
    + {{/with}} + {{/if}} + {{#if this.media}} + + {{/if}} +

    {{{this.text}}}

    +
    +
    +
  • + {{/each}} +
+

@QuotLyBot

+ + diff --git a/views/html.css b/views/html.css new file mode 100644 index 0000000..3646c46 --- /dev/null +++ b/views/html.css @@ -0,0 +1,124 @@ +body { + background: transparent; +} +#quote { + position: absolute; + padding: 35px 55px; + max-width: 512px; + max-width: {{width}}px; + max-height: {{height}}px; + background: + url("{{background.image.url}}") top left / cover repeat, + radial-gradient(ellipse farthest-corner at center, {{background.color1}}, {{background.color2}}); +} +.messages-list { + list-style: none; +} +.message-item { + position: relative; + display: block; + margin-bottom: 5px; +} +.message-item:last-child { + margin-bottom: 0; +} +.message-from-avatar, +.message-from-initials { + position: absolute; + top: 0; + left: 0; + width: 50px; + height: 50px; + font: 25px/2 'Nono Sans', sans-serif; + text-align: center; + color: #FFF; + border-radius: 50%; + box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); +} +.message { + margin-left: 65px; + padding: 14px; + background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}}); + border-radius: 25px; + box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); +} +.message-from-name, +.message-reply-from-name { + overflow: hidden; + width: 100%; + font: bold 22px/1.2 'Noto Sans', sans-serif; + white-space: nowrap; + text-overflow: ellipsis; +} +.message-reply { + margin: 8px 0; + padding: 0 14px; + border-left: 3px solid; +} +.message-reply-from-name { + font-size: 16px; +} +.message-text, +.message-reply-text { + overflow: hidden; + width: 100%; + font: 24px/1.2 'Noto Sans', sans-serif; + text-overflow: ellipsis; + color: #000; +} +.dark .message-text, +.dark .message-reply-text { + color: #FFF; +} +.message-reply-text { + font-size: 21px; + white-space: nowrap; +} +.message-text code { + font-family: 'Noto Mono', monospace; +} +.message-text .spoiler { + opacity: .3; +} +.message-text .mention, +.message-text .hashtag, +.message-text .cashtag, +.message-text .bot-command, +.message-text .email, +.message-text .phone-number { + text-decoration: none; + color: blue; +} +.custom-emoji, +.emoji-status { + display: inline; +} +.message-reply-text .custom-emoji { + height: 21px; +} +.message-text .custom-emoji { + height: 24px; +} +.message-from-name .emoji-status { + height: 22px; +} +.message-reply-from-name .emoji-status { + height: 16px; +} +.message-media { + min-width: 100%; + max-height: 512px; + object-fit: cover; + object-position: center; +} +.message-media.sticker { + border-radius: 5px; +} +.watermark { + position: absolute; + bottom: 5px; + right: 5px; + font: 12px 'Noto Sans'; + text-align: right; + color: rgba(0, 0, 0, .3); +} diff --git a/views/html.hbs b/views/html.hbs deleted file mode 100644 index 6529ec9..0000000 --- a/views/html.hbs +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - - - - -
-
    - {{#each messages}} -
  • - {{#if this.showAvatar}} - {{#if this.from.photo}} - {{this.from.initials}} - {{else}} -

    - {{this.from.initials}} -

    - {{/if}} - {{/if}} -
    -
    -

    - {{this.from.name}} - {{#if this.from.emojiStatus}} - - {{/if}} -

    -
    -
    - {{#if this.replyMessage}} - {{#with this.replyMessage}} -
    -

    - {{this.from.name}} - {{#if this.from.emojiStatus}} - - {{/if}} -

    -

    {{this.text}}

    -
    - {{/with}} - {{/if}} - {{#if this.media}} - - {{/if}} -

    {{{this.text}}}

    -
    -
    -
  • - {{/each}} -
-

@QuotLyBot

- - diff --git a/views/image.css b/views/image.css new file mode 100644 index 0000000..3534621 --- /dev/null +++ b/views/image.css @@ -0,0 +1,124 @@ +body { + background: transparent; +} +#quote { + position: absolute; + padding: 35px 55px; + max-width: {{width}}px; + max-height: {{height}}px; + transform: scale({{scale}}); + background: + url("{{background.image.url}}") top left / cover repeat, + radial-gradient(ellipse farthest-corner at center, {{background.color1}}, {{background.color2}}); +} +.messages-list { + list-style: none; +} +.message-item { + position: relative; + display: block; + margin-bottom: 5px; +} +.message-item:last-child { + margin-bottom: 0; +} +.message-from-avatar, +.message-from-initials { + position: absolute; + top: 0; + left: 0; + width: 50px; + height: 50px; + font: 25px/2 'Nono Sans', sans-serif; + text-align: center; + color: #FFF; + border-radius: 50%; + box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); +} +.message { + margin-left: 65px; + padding: 14px; + background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}}); + border-radius: 25px; + box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); +} +.message-from-name, +.message-reply-from-name { + overflow: hidden; + width: 100%; + font: bold 22px/1.2 'Noto Sans', sans-serif; + white-space: nowrap; + text-overflow: ellipsis; +} +.message-reply { + margin: 8px 0; + padding: 0 14px; + border-left: 3px solid; +} +.message-reply-from-name { + font-size: 16px; +} +.message-text, +.message-reply-text { + overflow: hidden; + width: 100%; + font: 24px/1.2 'Noto Sans', sans-serif; + text-overflow: ellipsis; + color: #000; +} +.dark .message-text, +.dark .message-reply-text { + color: #FFF; +} +.message-reply-text { + font-size: 21px; + white-space: nowrap; +} +.message-text code { + font-family: 'Noto Mono', monospace; +} +.message-text .spoiler { + opacity: .3; +} +.message-text .mention, +.message-text .hashtag, +.message-text .cashtag, +.message-text .bot-command, +.message-text .email, +.message-text .phone-number { + text-decoration: none; + color: blue; +} +.custom-emoji, +.emoji-status { + display: inline; +} +.message-reply-text .custom-emoji { + height: 21px; +} +.message-text .custom-emoji { + height: 24px; +} +.message-from-name .emoji-status { + height: 22px; +} +.message-reply-from-name .emoji-status { + height: 16px; +} +.message-media { + min-width: 100%; + max-height: 512px; + object-fit: cover; + object-position: center; +} +.message-media.sticker { + border-radius: 5px; +} +.watermark { + position: absolute; + bottom: 5px; + right: 5px; + font: 12px 'Noto Sans'; + text-align: right; + color: rgba(0, 0, 0, .3); +} diff --git a/views/image.hbs b/views/image.hbs deleted file mode 100644 index b9acb2c..0000000 --- a/views/image.hbs +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - - - - -
-
    - {{#each messages}} -
  • - {{#if this.showAvatar}} - {{#if this.from.photo}} - {{this.from.initials}} - {{else}} -

    - {{this.from.initials}} -

    - {{/if}} - {{/if}} -
    -
    -

    - {{this.from.name}} - {{#if this.from.emojiStatus}} - - {{/if}} -

    -
    -
    - {{#if this.replyMessage}} - {{#with this.replyMessage}} -
    -

    - {{this.from.name}} - {{#if this.from.emojiStatus}} - - {{/if}} -

    -

    {{this.text}}

    -
    - {{/with}} - {{/if}} - {{#if this.media}} - - {{/if}} -

    {{{this.text}}}

    -
    -
    -
  • - {{/each}} -
-

@QuotLyBot

- - diff --git a/views/message.hbs b/views/message.hbs deleted file mode 100644 index 1c6a1b9..0000000 --- a/views/message.hbs +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - -
- -

- {{name}} - {{emojiStatus}} -

-

{{text}}

-
- - diff --git a/views/png.css b/views/png.css new file mode 100644 index 0000000..9a42c4d --- /dev/null +++ b/views/png.css @@ -0,0 +1,121 @@ +body { + background: transparent; +} +#quote { + position: absolute; + padding: 35px 55px; + max-width: {{width}}px; + max-height: {{height}}px; + transform: scale({{scale}}); + background: none; +.messages-list { + list-style: none; +} +.message-item { + position: relative; + display: block; + margin-bottom: 5px; +} +.message-item:last-child { + margin-bottom: 0; +} +.message-from-avatar, +.message-from-initials { + position: absolute; + top: 0; + left: 0; + width: 50px; + height: 50px; + font: 25px/2 'Nono Sans', sans-serif; + text-align: center; + color: #FFF; + border-radius: 50%; + box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); +} +.message { + margin-left: 65px; + padding: 14px; + background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}}); + border-radius: 25px; + box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); +} +.message-from-name, +.message-reply-from-name { + overflow: hidden; + width: 100%; + font: bold 22px/1.2 'Noto Sans', sans-serif; + white-space: nowrap; + text-overflow: ellipsis; +} +.message-reply { + margin: 8px 0; + padding: 0 14px; + border-left: 3px solid; +} +.message-reply-from-name { + font-size: 16px; +} +.message-text, +.message-reply-text { + overflow: hidden; + width: 100%; + font: 24px/1.2 'Noto Sans', sans-serif; + text-overflow: ellipsis; + color: #000; +} +.dark .message-text, +.dark .message-reply-text { + color: #FFF; +} +.message-reply-text { + font-size: 21px; + white-space: nowrap; +} +.message-text code { + font-family: 'Noto Mono', monospace; +} +.message-text .spoiler { + opacity: .3; +} +.message-text .mention, +.message-text .hashtag, +.message-text .cashtag, +.message-text .bot-command, +.message-text .email, +.message-text .phone-number { + text-decoration: none; + color: blue; +} +.custom-emoji, +.emoji-status { + display: inline; +} +.message-reply-text .custom-emoji { + height: 21px; +} +.message-text .custom-emoji { + height: 24px; +} +.message-from-name .emoji-status { + height: 22px; +} +.message-reply-from-name .emoji-status { + height: 16px; +} +.message-media { + min-width: 100%; + max-height: 512px; + object-fit: cover; + object-position: center; +} +.message-media.sticker { + border-radius: 5px; +} +.watermark { + position: absolute; + bottom: 5px; + right: 5px; + font: 12px 'Noto Sans'; + text-align: right; + color: rgba(0, 0, 0, .3); +} diff --git a/views/png.hbs b/views/png.hbs deleted file mode 100644 index bbec5bb..0000000 --- a/views/png.hbs +++ /dev/null @@ -1,180 +0,0 @@ - - - - - - - - - -
-
    - {{#each messages}} -
  • - {{#if this.showAvatar}} - {{#if this.from.photo}} - {{this.from.initials}} - {{else}} -

    - {{this.from.initials}} -

    - {{/if}} - {{/if}} -
    -
    -

    - {{this.from.name}} - {{#if this.from.emojiStatus}} - - {{/if}} -

    -
    -
    - {{#if this.replyMessage}} - {{#with this.replyMessage}} -
    -

    - {{this.from.name}} - {{#if this.from.emojiStatus}} - - {{/if}} -

    -

    {{this.text}}

    -
    - {{/with}} - {{/if}} - {{#if this.media}} - - {{/if}} -

    {{{this.text}}}

    -
    -
    -
  • - {{/each}} -
-

@QuotLyBot

- - diff --git a/views/quote.css b/views/quote.css new file mode 100644 index 0000000..36d9d23 --- /dev/null +++ b/views/quote.css @@ -0,0 +1,113 @@ +body { + background: transparent; +} +#quote { + position: absolute; + padding-bottom: 75px; + max-width: {{width}}px; + max-height: {{height}}px; + transform: scale({{scale}}); + background: none; +} +.messages-list { + list-style: none; +} +.message-item { + position: relative; + display: block; + margin-bottom: 5px; +} +.message-item:last-child { + margin-bottom: 0; +} +.message-from-avatar, +.message-from-initials { + position: absolute; + top: 0; + left: 0; + width: 50px; + height: 50px; + font: 25px/2 'Nono Sans', sans-serif; + text-align: center; + color: #FFF; + border-radius: 50%; + box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); +} +.message { + margin-left: 65px; + padding: 14px; + background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}}); + border-radius: 25px; +} +.message-from-name, +.message-reply-from-name { + overflow: hidden; + width: 100%; + font: bold 22px/1.2 'Noto Sans', sans-serif; + white-space: nowrap; + text-overflow: ellipsis; +} +.message-reply { + margin: 8px 0; + padding: 0 14px; + border-left: 3px solid; +} +.message-reply-from-name { + font-size: 16px; +} +.message-text, +.message-reply-text { + overflow: hidden; + width: 100%; + font: 24px/1.2 'Noto Sans', sans-serif; + text-overflow: ellipsis; + color: #000; +} +.dark .message-text, +.dark .message-reply-text { + color: #FFF; +} +.message-reply-text { + font-size: 21px; + white-space: nowrap; +} +.message-text code { + font-family: 'Noto Mono', monospace; +} +.message-text .spoiler { + opacity: .3; +} +.message-text .mention, +.message-text .hashtag, +.message-text .cashtag, +.message-text .bot-command, +.message-text .email, +.message-text .phone-number { + text-decoration: none; + color: blue; +} +.custom-emoji, +.emoji-status { + display: inline; +} +.message-reply-text .custom-emoji { + height: 21px; +} +.message-text .custom-emoji { + height: 24px; +} +.message-from-name .emoji-status { + height: 22px; +} +.message-reply-from-name .emoji-status { + height: 16px; +} +.message-media { + min-width: 100%; + max-height: 512px; + object-fit: cover; + object-position: center; +} +.message-media.sticker { + border-radius: 5px; +} diff --git a/views/quote.hbs b/views/quote.hbs deleted file mode 100644 index 3d31d21..0000000 --- a/views/quote.hbs +++ /dev/null @@ -1,171 +0,0 @@ - - - - - - - - - -
-
    - {{#each messages}} -
  • - {{#if this.showAvatar}} - {{#if this.from.photo}} - {{this.from.initials}} - {{else}} -

    - {{this.from.initials}} -

    - {{/if}} - {{/if}} -
    -
    -

    - {{this.from.name}} - {{#if this.from.emojiStatus}} - - {{/if}} -

    -
    -
    - {{#if this.replyMessage}} - {{#with this.replyMessage}} -
    -

    - {{this.from.name}} - {{#if this.from.emojiStatus}} - - {{/if}} -

    -

    {{this.text}}

    -
    - {{/with}} - {{/if}} - {{#if this.media}} - - {{/if}} -

    {{{this.text}}}

    -
    -
    -
  • - {{/each}} -
- - From 7e921e13505239524f0bf775df7d82926f0d8f67 Mon Sep 17 00:00:00 2001 From: arelive Date: Tue, 13 Jun 2023 18:35:24 +0300 Subject: [PATCH 51/60] style: float quotes to left --- views/html.css | 1 + views/image.css | 1 + views/png.css | 1 + views/quote.css | 1 + 4 files changed, 4 insertions(+) diff --git a/views/html.css b/views/html.css index 3646c46..3b45db9 100644 --- a/views/html.css +++ b/views/html.css @@ -17,6 +17,7 @@ body { .message-item { position: relative; display: block; + float: left; margin-bottom: 5px; } .message-item:last-child { diff --git a/views/image.css b/views/image.css index 3534621..0c231d7 100644 --- a/views/image.css +++ b/views/image.css @@ -17,6 +17,7 @@ body { .message-item { position: relative; display: block; + float: left; margin-bottom: 5px; } .message-item:last-child { diff --git a/views/png.css b/views/png.css index 9a42c4d..354147d 100644 --- a/views/png.css +++ b/views/png.css @@ -14,6 +14,7 @@ body { .message-item { position: relative; display: block; + float: left; margin-bottom: 5px; } .message-item:last-child { diff --git a/views/quote.css b/views/quote.css index 36d9d23..c827a90 100644 --- a/views/quote.css +++ b/views/quote.css @@ -15,6 +15,7 @@ body { .message-item { position: relative; display: block; + float: left; margin-bottom: 5px; } .message-item:last-child { From 7c8bb3564884e99aeda1a657d77a20068f3c5751 Mon Sep 17 00:00:00 2001 From: arelive Date: Tue, 13 Jun 2023 19:18:45 +0300 Subject: [PATCH 52/60] style: pretty for stickers, minimize width --- methods/generate.js | 6 +++++- views/default.hbs | 4 ++-- views/html.css | 21 +++++++++++++++++---- views/image.css | 20 +++++++++++++++++--- views/png.css | 20 +++++++++++++++++--- views/quote.css | 22 ++++++++++++++++++++-- 6 files changed, 78 insertions(+), 15 deletions(-) diff --git a/methods/generate.js b/methods/generate.js index 0a6a36b..3573dbb 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -152,8 +152,10 @@ const buildMessage = async (message, theme) => { } text = text.replace(/\n/g, '
') + const type = media ? media.type == 'sticker' ? 'sticker' : 'image' : 'regular' + return { - from, replyMessage, text, media, + type, from, replyMessage, text, media, showAvatar: message.avatar } } @@ -205,6 +207,8 @@ module.exports = async (parm) => { const content = getView('default', type)({ scale, + width: parm.width, + height: parm.height, theme, background: { image: { url: bgImageURL }, diff --git a/views/default.hbs b/views/default.hbs index 999a215..5537925 100644 --- a/views/default.hbs +++ b/views/default.hbs @@ -12,7 +12,7 @@
    {{#each messages}} -
  • +
  • {{#if this.showAvatar}} {{#if this.from.photo}} {{this.from.initials}} @@ -34,7 +34,7 @@
    {{#if this.replyMessage}} {{#with this.replyMessage}} -
    +

    {{this.from.name}} {{#if this.from.emojiStatus}} diff --git a/views/html.css b/views/html.css index 3b45db9..93cb7a2 100644 --- a/views/html.css +++ b/views/html.css @@ -4,7 +4,6 @@ body { #quote { position: absolute; padding: 35px 55px; - max-width: 512px; max-width: {{width}}px; max-height: {{height}}px; background: @@ -18,7 +17,9 @@ body { position: relative; display: block; float: left; + clear: both; margin-bottom: 5px; + max-width: 100%; } .message-item:last-child { margin-bottom: 0; @@ -43,6 +44,12 @@ body { border-radius: 25px; box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); } +.message-item.sticker .message { + padding: 0; + background: none; + border-radius: 5px; + box-shadow: none; +} .message-from-name, .message-reply-from-name { overflow: hidden; @@ -51,11 +58,17 @@ body { white-space: nowrap; text-overflow: ellipsis; } +.message-item.sticker .message-from-name { + display: none; +} .message-reply { margin: 8px 0; padding: 0 14px; border-left: 3px solid; } +.message-item.sticker .message-reply { + display: none; +} .message-reply-from-name { font-size: 16px; } @@ -67,6 +80,9 @@ body { text-overflow: ellipsis; color: #000; } +.message-item.sticker .message-text { + display: none; +} .dark .message-text, .dark .message-reply-text { color: #FFF; @@ -112,9 +128,6 @@ body { object-fit: cover; object-position: center; } -.message-media.sticker { - border-radius: 5px; -} .watermark { position: absolute; bottom: 5px; diff --git a/views/image.css b/views/image.css index 0c231d7..0e7755e 100644 --- a/views/image.css +++ b/views/image.css @@ -18,7 +18,9 @@ body { position: relative; display: block; float: left; + clear: both; margin-bottom: 5px; + max-width: 100%; } .message-item:last-child { margin-bottom: 0; @@ -43,6 +45,12 @@ body { border-radius: 25px; box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); } +.message-item.sticker .message { + padding: 0; + background: none; + border-radius: 5px; + box-shadow: none; +} .message-from-name, .message-reply-from-name { overflow: hidden; @@ -51,11 +59,17 @@ body { white-space: nowrap; text-overflow: ellipsis; } +.message-item.sticker .message-from-name { + display: none; +} .message-reply { margin: 8px 0; padding: 0 14px; border-left: 3px solid; } +.message-item.sticker .message-reply { + display: none; +} .message-reply-from-name { font-size: 16px; } @@ -67,6 +81,9 @@ body { text-overflow: ellipsis; color: #000; } +.message-item.sticker .message-text { + display: none; +} .dark .message-text, .dark .message-reply-text { color: #FFF; @@ -112,9 +129,6 @@ body { object-fit: cover; object-position: center; } -.message-media.sticker { - border-radius: 5px; -} .watermark { position: absolute; bottom: 5px; diff --git a/views/png.css b/views/png.css index 354147d..6a261b0 100644 --- a/views/png.css +++ b/views/png.css @@ -15,7 +15,9 @@ body { position: relative; display: block; float: left; + clear: both; margin-bottom: 5px; + max-width: 100%; } .message-item:last-child { margin-bottom: 0; @@ -40,6 +42,12 @@ body { border-radius: 25px; box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); } +.message-item.sticker .message { + padding: 0; + background: none; + border-radius: 5px; + box-shadow: none; +} .message-from-name, .message-reply-from-name { overflow: hidden; @@ -48,11 +56,17 @@ body { white-space: nowrap; text-overflow: ellipsis; } +.message-item.sticker .message-from-name { + display: none; +} .message-reply { margin: 8px 0; padding: 0 14px; border-left: 3px solid; } +.message-item.sticker .message-reply { + display: none; +} .message-reply-from-name { font-size: 16px; } @@ -64,6 +78,9 @@ body { text-overflow: ellipsis; color: #000; } +.message-item.sticker .message-text { + display: none; +} .dark .message-text, .dark .message-reply-text { color: #FFF; @@ -109,9 +126,6 @@ body { object-fit: cover; object-position: center; } -.message-media.sticker { - border-radius: 5px; -} .watermark { position: absolute; bottom: 5px; diff --git a/views/quote.css b/views/quote.css index c827a90..34daaec 100644 --- a/views/quote.css +++ b/views/quote.css @@ -16,7 +16,9 @@ body { position: relative; display: block; float: left; + clear: both; margin-bottom: 5px; + max-width: 100%; } .message-item:last-child { margin-bottom: 0; @@ -40,6 +42,12 @@ body { background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}}); border-radius: 25px; } +.message-item.sticker .message { + padding: 0; + background: none; + border-radius: 5px; + box-shadow: none; +} .message-from-name, .message-reply-from-name { overflow: hidden; @@ -48,11 +56,17 @@ body { white-space: nowrap; text-overflow: ellipsis; } +.message-item.sticker .message-from-name { + display: none; +} .message-reply { margin: 8px 0; padding: 0 14px; border-left: 3px solid; } +.message-item.sticker .message-reply { + display: none; +} .message-reply-from-name { font-size: 16px; } @@ -64,6 +78,9 @@ body { text-overflow: ellipsis; color: #000; } +.message-item.sticker .message-text { + display: none; +} .dark .message-text, .dark .message-reply-text { color: #FFF; @@ -109,6 +126,7 @@ body { object-fit: cover; object-position: center; } -.message-media.sticker { - border-radius: 5px; +.watermark { + display: none; } + From 5aec95112da69c2d3ce476dc316ca5799e15a312 Mon Sep 17 00:00:00 2001 From: arelive Date: Tue, 13 Jun 2023 20:53:02 +0300 Subject: [PATCH 53/60] feat: docker --- .dockerignore | 1 + Dockerfile | 14 ++++++-------- docker-compose.yml | 5 ++--- 3 files changed, 9 insertions(+), 11 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +node_modules diff --git a/Dockerfile b/Dockerfile index 87595e8..b4a703c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,9 @@ -FROM nikolaik/python-nodejs:python3.8-nodejs12 AS builder +FROM mcr.microsoft.com/playwright:v1.35.0-jammy -ENV NODE_WORKDIR /app -WORKDIR $NODE_WORKDIR +WORKDIR /app +ADD . /app -ADD . $NODE_WORKDIR +#RUN apt-get update && apt-get install -y build-essential gcc wget git libvips && rm -rf /var/lib/apt/lists/* -RUN apt-get update && apt-get install -y build-essential gcc wget git libvips && rm -rf /var/lib/apt/lists/* - - -RUN npm install canvas@2.6.1 && npm install # TODO: canvas crashes if installed via npm install from package.json \ No newline at end of file +RUN npm install && npx playwright install +ENTRYPOINT [ "npm", "start" ] diff --git a/docker-compose.yml b/docker-compose.yml index 73b6d28..d043daa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,11 +13,10 @@ services: max-file: "3" networks: - quotly - command: node index.js ports: - - 127.0.0.1:4888:4888 + - 4888:4888 networks: quotly: - external: true \ No newline at end of file + external: true From 4491dca9b48c2dde65f7fdec4155fc4a89a5fa84 Mon Sep 17 00:00:00 2001 From: arelive Date: Thu, 15 Jun 2023 18:35:54 +0300 Subject: [PATCH 54/60] allow .tgs lottie stickers --- app.js | 7 +-- cache/.gitkeep | 0 methods/generate.js | 97 ++++++++++++++------------------------- test/async.js | 8 ++-- test/stress.js | 8 ++-- utils/color-manipulate.js | 54 ++++++++++++++++++++++ utils/get-view.js | 4 +- 7 files changed, 103 insertions(+), 75 deletions(-) create mode 100644 cache/.gitkeep create mode 100644 utils/color-manipulate.js diff --git a/app.js b/app.js index 230c06e..2476251 100644 --- a/app.js +++ b/app.js @@ -13,13 +13,14 @@ const app = new Koa() app.use(logger()) app.use(responseTime()) app.use(bodyParser()) -app.use(mount('/assets', serve(path.resolve('./assets')))) +app.use(mount('/assets', serve(path.resolve(__dirname, 'assets')))) +app.use(mount('/cache', serve(path.resolve(__dirname, 'cache')))) -const ratelimitВb = new Map() +const ratelimitDb = new Map() app.use(ratelimit({ driver: 'memory', - db: ratelimitВb, + db: ratelimitDb, duration: 1000 * 55, errorMessage: { ok: false, diff --git a/cache/.gitkeep b/cache/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/methods/generate.js b/methods/generate.js index 3573dbb..348ae41 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -3,6 +3,9 @@ const fs = require('fs') const { createCanvas, loadImage } = require('canvas') const sharp = require('sharp') const runes = require('runes') +const axios = require('axios') +const lottie = require('lottie-node') +const zlib = require('zlib') const render = require('../utils/render') const getView = require('../utils/get-view') @@ -11,48 +14,7 @@ const getMediaURL = require('../utils/get-media-url') const lightOrDark = require('../utils/light-or-dark') const formatHTML = require('../utils/format-html') const telegram = require('../utils/telegram') - -const normalizeColor = (color) => { - const canvas = createCanvas(0, 0) - const canvasCtx = canvas.getContext('2d') - - canvasCtx.fillStyle = color - color = canvasCtx.fillStyle - - return color -} - -const colorLuminance = (hex, lum) => { - hex = String(hex).replace(/[^0-9a-f]/gi, '') - if (hex.length < 6) { - hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2] - } - lum = lum || 0 - - // convert to decimal and change luminosity - let rgb = '#' - let c - let i - for (i = 0; i < 3; i++) { - c = parseInt(hex.substr(i * 2, 2), 16) - c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16) - rgb += ('00' + c).substr(c.length) - } - - return rgb -} - -const imageAlpha = (image, alpha) => { - const canvas = createCanvas(image.width, image.height) - - const canvasCtx = canvas.getContext('2d') - - canvasCtx.globalAlpha = alpha - - canvasCtx.drawImage(image, 0, 0) - - return canvas -} +const { getBackground, colorLuminance } = require('../utils/color-manipulate') const getEmojiStatusURL = async (emojiId) => { const customEmojiStickers = await telegram.callApi('getCustomEmojiStickers', { @@ -143,7 +105,33 @@ const buildMessage = async (message, theme) => { } } if (media) { - media.type = message.mediaType || (media.url.endsWith('.webp') ? 'sticker' : 'image') + media.type = message.mediaType + if (!media.type) { + media.type = media.url.endsWith('.webp') || media.url.endsWith('.tgs') ? 'sticker' : 'image' + } + + if (media.url.endsWith('.tgs')) { + const tgsCompressed = await axios + .get(media.url, { responseType: 'arraybuffer' }) + .then(res => Buffer.from(res.data)) + .catch(console.error) + const tgs = await new Promise((resolve, reject) => zlib.gunzip(tgsCompressed, (error, result) => { + if (error) { + return reject(error) + } + resolve(JSON.parse(result.toString())) + })) + + const canvas = createCanvas(512, 512) + const animation = lottie(tgs, canvas) + const middleFrame = Math.floor(animation.getDuration(true) / 2) + + animation.goToAndStop(middleFrame, true) + const filename = media.url.split('/').pop() + fs.writeFileSync(path.resolve(__dirname, `../cache/${filename}.png`), canvas.toBuffer()) + + media.url = `http://localhost:${process.env.PORT}/cache/${filename}.png` + } } let text = message.text ?? '' @@ -152,7 +140,7 @@ const buildMessage = async (message, theme) => { } text = text.replace(/\n/g, '
    ') - const type = media ? media.type == 'sticker' ? 'sticker' : 'image' : 'regular' + const type = media ? media.type : 'regular' return { type, from, replyMessage, text, media, @@ -176,24 +164,9 @@ module.exports = async (parm) => { const ext = parm.ext || false const scale = parm.scale ? Math.min(parseFloat(parm.scale), 20) : 2 - let backgroundColor = parm.backgroundColor || '//#292232' - let backgroundColorOne, backgroundColorTwo - const backgroundColorSplit = backgroundColor.split('/') - - // TODO effect colors with CSS - if (backgroundColorSplit && backgroundColorSplit.length > 1 && backgroundColorSplit[0] !== '') { - backgroundColorOne = normalizeColor(backgroundColorSplit[0]) - backgroundColorTwo = normalizeColor(backgroundColorSplit[1]) - } else if (backgroundColor.startsWith('//')) { - backgroundColor = normalizeColor(backgroundColor.replace('//', '')) - backgroundColorOne = colorLuminance(backgroundColor, 0.35) - backgroundColorTwo = colorLuminance(backgroundColor, -0.15) - } else { - backgroundColor = normalizeColor(backgroundColor) - backgroundColorOne = backgroundColor - backgroundColorTwo = backgroundColor - } - + const { + backgroundColor, backgroundColorOne, backgroundColorTwo + } = getBackground(parm.backgroundColor || '//#292232') const theme = lightOrDark(backgroundColorOne) const messages = await Promise.all(parm.messages diff --git a/test/async.js b/test/async.js index 929105d..b63d4dc 100644 --- a/test/async.js +++ b/test/async.js @@ -25,21 +25,21 @@ const testTemplates = [ type: 'quote', format: 'webp' }, - filename: (index) => `./test/quote/${index}.webp` + filename: (index) => `../test/quote/${index}.webp` }, { method: 'generate', params: { type: 'image', format: 'png' }, - filename: (index) => `./test/image/${index}.png` + filename: (index) => `../test/image/${index}.png` }, { method: 'generate', params: { type: 'html', format: 'html' }, - filename: (index) => `./test/html/${index}.html` + filename: (index) => `../test/html/${index}.html` } ] @@ -91,7 +91,7 @@ const queue = async.queue(({ json, template, i }, cb) => { ).then(res => { console.timeEnd(`${i}-${template.params.type}`) fs.writeFile( - path.resolve(template.filename(i)), + path.resolve(__dirname, template.filename(i)), Buffer.from(res.data.result.image, 'base64'), err => err && console.error(err) ) diff --git a/test/stress.js b/test/stress.js index 75745f2..4f555c2 100644 --- a/test/stress.js +++ b/test/stress.js @@ -24,21 +24,21 @@ const testTemplates = [ type: 'quote', format: 'webp' }, - filename: (index) => `./test/quote/${index}.webp` + filename: (index) => `../test/quote/${index}.webp` }, { method: 'generate', params: { type: 'image', format: 'png' }, - filename: (index) => `./test/image/${index}.png` + filename: (index) => `../test/image/${index}.png` }, { method: 'generate', params: { type: 'html', format: 'html' }, - filename: (index) => `./test/html/${index}.html` + filename: (index) => `../test/html/${index}.html` } ] @@ -106,7 +106,7 @@ const buildMessage = (index, hasReply) => { ).then(res => { console.timeEnd(`${i}-${template.params.type}`) fs.writeFile( - path.resolve(template.filename(i)), + path.resolve(__dirname, template.filename(i)), Buffer.from(res.data.result.image, 'base64'), err => err && console.error(err) ) diff --git a/utils/color-manipulate.js b/utils/color-manipulate.js new file mode 100644 index 0000000..d0508ec --- /dev/null +++ b/utils/color-manipulate.js @@ -0,0 +1,54 @@ +const { createCanvas } = require('canvas') + +const normalizeColor = (color) => { + const canvas = createCanvas(0, 0) + const canvasCtx = canvas.getContext('2d') + + canvasCtx.fillStyle = color + color = canvasCtx.fillStyle + + return color +} + +const colorLuminance = (hex, lum) => { + hex = String(hex).replace(/[^0-9a-f]/gi, '') + if (hex.length < 6) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2] + } + lum = lum || 0 + + // convert to decimal and change luminosity + let rgb = '#' + let c + let i + for (i = 0; i < 3; i++) { + c = parseInt(hex.substr(i * 2, 2), 16) + c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16) + rgb += ('00' + c).substr(c.length) + } + + return rgb +} + +const getBackground = (backgroundColor) => { + let backgroundColorOne, backgroundColorTwo + const backgroundColorSplit = backgroundColor.split('/') + + // TODO effect colors with CSS + if (backgroundColorSplit && backgroundColorSplit.length > 1 && backgroundColorSplit[0] !== '') { + backgroundColorOne = normalizeColor(backgroundColorSplit[0]) + backgroundColorTwo = normalizeColor(backgroundColorSplit[1]) + } else if (backgroundColor.startsWith('//')) { + backgroundColor = normalizeColor(backgroundColor.replace('//', '')) + backgroundColorOne = colorLuminance(backgroundColor, 0.35) + backgroundColorTwo = colorLuminance(backgroundColor, -0.15) + } else { + backgroundColor = normalizeColor(backgroundColor) + backgroundColorOne = backgroundColor + backgroundColorTwo = backgroundColor + } + + return { backgroundColor, backgroundColorOne, backgroundColorTwo } +} + +module.exports = { getBackground, colorLuminance } diff --git a/utils/get-view.js b/utils/get-view.js index 6950bea..502b3f9 100644 --- a/utils/get-view.js +++ b/utils/get-view.js @@ -11,11 +11,11 @@ module.exports = (viewName, styleName) => { } const template = fs.readFileSync( - path.resolve(`./views/${viewName}.hbs`) + path.resolve(__dirname, `../views/${viewName}.hbs`) ).toString('utf-8') const style = fs.readFileSync( - path.resolve(`./views/${styleName}.css`) + path.resolve(__dirname, `../views/${styleName}.css`) ).toString('utf-8') const view = Handlebars.compile(template.replace('{{> style}}', style)) From 9b2b75c87e18f4debafcac7feb87231567923f33 Mon Sep 17 00:00:00 2001 From: arelive Date: Fri, 16 Jun 2023 02:31:30 +0300 Subject: [PATCH 55/60] allow video stickers and animations --- methods/generate.js | 13 ++++------- package.json | 3 ++- test/async.js | 6 ++--- test/runtime.js | 55 +++++++++++++++++++++++++++++++++++++++++++++ test/stress.js | 6 ++--- 5 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 test/runtime.js diff --git a/methods/generate.js b/methods/generate.js index 348ae41..ef5f147 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -95,16 +95,11 @@ const buildMessage = async (message, theme) => { let media = message.media || null if (media) { if (!media.url) { - if (media.length) { - const mediaInfo = media.pop() - const mediaURL = await getMediaURL(mediaInfo) - media = { url: mediaURL } - } else { - media = null - } + const mediaInfo = media.length ? media.pop() : media + const mediaURL = await getMediaURL(mediaInfo) + media = { url: mediaURL } } - } - if (media) { + media.type = message.mediaType if (!media.type) { media.type = media.url.endsWith('.webp') || media.url.endsWith('.tgs') ? 'sticker' : 'image' diff --git a/package.json b/package.json index ab764bd..43dfcfb 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "start": "node index.js", "lint": "node_modules/.bin/eslint --ext js .", "lint:fix": "node_modules/.bin/eslint --fix --ext js .", - "test": "node test/stress.js 1000", + "test": "node test/runtime.js", + "test:stress": "node test/stress.js 1000", "test:async": "node test/async.js 1000 50" }, "repository": { diff --git a/test/async.js b/test/async.js index b63d4dc..ce3258d 100644 --- a/test/async.js +++ b/test/async.js @@ -25,21 +25,21 @@ const testTemplates = [ type: 'quote', format: 'webp' }, - filename: (index) => `../test/quote/${index}.webp` + filename: (index) => `quote/${index}.webp` }, { method: 'generate', params: { type: 'image', format: 'png' }, - filename: (index) => `../test/image/${index}.png` + filename: (index) => `image/${index}.png` }, { method: 'generate', params: { type: 'html', format: 'html' }, - filename: (index) => `../test/html/${index}.html` + filename: (index) => `html/${index}.html` } ] diff --git a/test/runtime.js b/test/runtime.js new file mode 100644 index 0000000..efbae13 --- /dev/null +++ b/test/runtime.js @@ -0,0 +1,55 @@ +const axios = require('axios') +const path = require('path') +const fs = require('fs') +const { Telegraf } = require('telegraf') + +require('dotenv').config({ path: './.env' }) +require('../app') + +const bot = new Telegraf(process.env.BOT_TOKEN) + +bot.on('message', async ctx => { + const message = JSON.parse(JSON.stringify(ctx.message)) + message.avatar = true + message.media = message.photo || message.sticker || message.animation?.thumb || null + if (message.media?.is_video) { + message.media = message.media.thumb + } + + await axios.post( + `http://localhost:${process.env.PORT}/generate`, + { + type: 'image', + format: 'png', + messages: [message] + }, + { headers: { 'Content-Type': 'application/json' } } + ).then(async res => { + fs.writeFileSync( + path.resolve(__dirname, 'response.png'), + Buffer.from(res.data.result.image, 'base64'), + err => err && console.error(err) + ) + + await ctx.replyWithPhoto({ source: path.resolve(__dirname, 'response.png') }) + }).catch(console.error) + + await axios.post( + `http://localhost:${process.env.PORT}/generate`, + { + type: 'html', + format: 'html', + messages: [message] + }, + { headers: { 'Content-Type': 'application/json' } } + ).then(async res => { + fs.writeFileSync( + path.resolve(__dirname, 'response.html'), + Buffer.from(res.data.result.image, 'base64'), + err => err && console.error(err) + ) + }).catch(console.error) +}) + +bot.catch(console.error) +bot.launch() diff --git a/test/stress.js b/test/stress.js index 4f555c2..80fc0aa 100644 --- a/test/stress.js +++ b/test/stress.js @@ -24,21 +24,21 @@ const testTemplates = [ type: 'quote', format: 'webp' }, - filename: (index) => `../test/quote/${index}.webp` + filename: (index) => `quote/${index}.webp` }, { method: 'generate', params: { type: 'image', format: 'png' }, - filename: (index) => `../test/image/${index}.png` + filename: (index) => `image/${index}.png` }, { method: 'generate', params: { type: 'html', format: 'html' }, - filename: (index) => `../test/html/${index}.html` + filename: (index) => `html/${index}.html` } ] From 765db4a8c91e526fef5a40bf22fb6242c5b7a1b0 Mon Sep 17 00:00:00 2001 From: arelive Date: Tue, 20 Jun 2023 05:03:38 +0300 Subject: [PATCH 56/60] clean up --- cache/.gitkeep | 0 methods/generate.js | 116 +++++++++--------- utils/color-liminance.js | 19 +++ utils/emoji-image.js | 106 ---------------- ...{color-manipulate.js => get-background.js} | 25 +--- utils/get-media-url.js | 6 - utils/image-load-path.js | 9 -- utils/image-load-url.js | 23 ---- utils/index.js | 13 +- utils/promise-concurrent.js | 29 ----- utils/user-name.js | 9 -- views/html.css | 3 +- views/image.css | 2 +- views/png.css | 2 +- views/quote.css | 2 +- 15 files changed, 93 insertions(+), 271 deletions(-) delete mode 100644 cache/.gitkeep create mode 100644 utils/color-liminance.js delete mode 100644 utils/emoji-image.js rename utils/{color-manipulate.js => get-background.js} (65%) delete mode 100644 utils/get-media-url.js delete mode 100644 utils/image-load-path.js delete mode 100644 utils/image-load-url.js delete mode 100644 utils/promise-concurrent.js delete mode 100644 utils/user-name.js diff --git a/cache/.gitkeep b/cache/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/methods/generate.js b/methods/generate.js index ef5f147..172f712 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -1,32 +1,28 @@ const path = require('path') const fs = require('fs') -const { createCanvas, loadImage } = require('canvas') +const { createCanvas } = require('canvas') const sharp = require('sharp') const runes = require('runes') const axios = require('axios') const lottie = require('lottie-node') const zlib = require('zlib') -const render = require('../utils/render') -const getView = require('../utils/get-view') -const getAvatarURL = require('../utils/get-avatar-url') -const getMediaURL = require('../utils/get-media-url') -const lightOrDark = require('../utils/light-or-dark') -const formatHTML = require('../utils/format-html') -const telegram = require('../utils/telegram') -const { getBackground, colorLuminance } = require('../utils/color-manipulate') +const { + telegram, render, getView, getAvatarURL, formatHTML, getBackground, colorLuminance, lightOrDark +} = require('../utils') const getEmojiStatusURL = async (emojiId) => { const customEmojiStickers = await telegram.callApi('getCustomEmojiStickers', { custom_emoji_ids: [emojiId] - }).catch(() => {}) + }).catch(console.error) if (!Array.isArray(customEmojiStickers) || !customEmojiStickers.length) { return null } const fileId = customEmojiStickers[0].thumb.file_id - const fileURL = await telegram.getFileLink(fileId).catch(() => {}) + const fileURL = await telegram.getFileLink(fileId).catch(console.error) + return fileURL || null } @@ -37,8 +33,8 @@ const buildUser = async (user, theme, options={ getAvatar: false }) => { let photo = null if (options.getAvatar) { - photo = user.photo || photo - if (!photo?.url) { + photo = user.photo || null + if (!photo || !photo.url) { const photoURL = await getAvatarURL(user) photo = photoURL ? { url: photoURL } : photo } @@ -61,7 +57,7 @@ const buildUser = async (user, theme, options={ getAvatar: false }) => { } } else { - name == '' + name = '' initials = '' } } @@ -69,45 +65,40 @@ const buildUser = async (user, theme, options={ getAvatar: false }) => { return { name, initials, color, photo, emojiStatus } } -const buildMessage = async (message, theme) => { - const from = await buildUser(message.from, theme, { getAvatar: message.avatar || false }) +const buildReplyMessage = async (message, theme) => { + if (!Object.keys(message).length) { + return null + } - let replyMessage = message.replyMessage - if (replyMessage && Object.keys(replyMessage) != 0) { - // kostyl - if (!replyMessage.from) { - replyMessage.from = { - id: replyMessage.chatId, - name: replyMessage.name, - emoji_status: null, - photo: null - } - } - replyMessage = { - from: await buildUser(replyMessage.from, theme), - text: replyMessage.text - } + // kostyl + const from = message.from || { + id: message.chatId, + name: message.name, + emoji_status: null, + photo: null } - else { - replyMessage = null + + return { + from: await buildUser(from, theme), + text: message.text } +} - let media = message.media || null - if (media) { - if (!media.url) { - const mediaInfo = media.length ? media.pop() : media - const mediaURL = await getMediaURL(mediaInfo) - media = { url: mediaURL } - } +const buildMedia = async (media) => { + let url = media.url + if (!url) { + const mediaInfo = Array.isArray(media) ? media.pop() : media + url = await telegram.getFileLink(mediaInfo).catch(console.error) + } - media.type = message.mediaType - if (!media.type) { - media.type = media.url.endsWith('.webp') || media.url.endsWith('.tgs') ? 'sticker' : 'image' - } + let type = message.mediaType + if (!type) { + type = url.endsWith('.webp') || url.endsWith('.tgs') ? 'sticker' : 'image' + } - if (media.url.endsWith('.tgs')) { + if (url.endsWith('.tgs')) { const tgsCompressed = await axios - .get(media.url, { responseType: 'arraybuffer' }) + .get(url, { responseType: 'arraybuffer' }) .then(res => Buffer.from(res.data)) .catch(console.error) const tgs = await new Promise((resolve, reject) => zlib.gunzip(tgsCompressed, (error, result) => { @@ -122,12 +113,19 @@ const buildMessage = async (message, theme) => { const middleFrame = Math.floor(animation.getDuration(true) / 2) animation.goToAndStop(middleFrame, true) - const filename = media.url.split('/').pop() + const filename = url.split('/').pop() fs.writeFileSync(path.resolve(__dirname, `../cache/${filename}.png`), canvas.toBuffer()) - media.url = `http://localhost:${process.env.PORT}/cache/${filename}.png` + url = `http://localhost:${process.env.PORT}/cache/${filename}.png` } - } + + return { url, type } +} + +const buildMessage = async (message, theme) => { + const from = await buildUser(message.from, theme, { getAvatar: message.avatar || false }) + const replyMessage = message.replyMessage ? await buildReplyMessage(message.replyMessage, theme) : null + const media = message.media ? await buildMedia(message.media) : null let text = message.text ?? '' if (Array.isArray(message.entities)) { @@ -148,6 +146,12 @@ const userColors = { dark: ['#FF8E86', '#FFA357', '#B18FFF', '#4DD6BF', '#45E8D1', '#7AC9FF', '#FF7FD5'] } const bgImageURL = `http://localhost:${process.env.PORT}/assets/pattern_02_alpha.png` +const MAX_SCALE = 20 +const DEFAULT_BG_COLOR = '//#292232' +const DEFAULT_VIEW_NAME = 'default' +const RENDER_SELECTOR = '#quote' +const MAX_QUOTE_WIDTH = 512 +const MAX_QUOTE_HEIGHT = 512 module.exports = async (parm) => { if (!parm || typeof parm != 'object') { @@ -157,11 +161,11 @@ module.exports = async (parm) => { let type = parm.type || 'png' const format = parm.format || '' const ext = parm.ext || false - const scale = parm.scale ? Math.min(parseFloat(parm.scale), 20) : 2 + const scale = parm.scale ? Math.min(parseFloat(parm.scale), MAX_SCALE) : 2 const { backgroundColor, backgroundColorOne, backgroundColorTwo - } = getBackground(parm.backgroundColor || '//#292232') + } = getBackground(parm.backgroundColor || DEFAULT_BG_COLOR) const theme = lightOrDark(backgroundColorOne) const messages = await Promise.all(parm.messages @@ -173,7 +177,8 @@ module.exports = async (parm) => { return { error: 'messages_empty' } } - const content = getView('default', type)({ + const view = getView(DEFAULT_VIEW_NAME, type) + const content = view({ scale, width: parm.width, height: parm.height, @@ -195,7 +200,7 @@ module.exports = async (parm) => { } } - let image = await render(content, '#quote') + let image = await render(content, RENDER_SELECTOR) const imageSharp = await sharp(image) const { width, height } = await imageSharp.metadata() @@ -205,10 +210,7 @@ module.exports = async (parm) => { } if (type == 'quote') { - const maxWidth = 512 - const maxHeight = 512 - - imageSharp.resize(height > width ? { height: maxHeight } : { width: maxWidth }) + imageSharp.resize(height > width ? { height: MAX_QUOTE_HEIGHT } : { width: MAX_QUOTE_WIDTH }) image = format == 'png' ? await imageSharp.png().toBuffer() : diff --git a/utils/color-liminance.js b/utils/color-liminance.js new file mode 100644 index 0000000..8022b06 --- /dev/null +++ b/utils/color-liminance.js @@ -0,0 +1,19 @@ +module.exports = (hex, lum) => { + hex = String(hex).replace(/[^0-9a-f]/gi, '') + if (hex.length < 6) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2] + } + lum = lum || 0 + + // convert to decimal and change luminosity + let rgb = '#' + let c + let i + for (i = 0; i < 3; i++) { + c = parseInt(hex.substr(i * 2, 2), 16) + c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16) + rgb += ('00' + c).substr(c.length) + } + + return rgb +} diff --git a/utils/emoji-image.js b/utils/emoji-image.js deleted file mode 100644 index db5562b..0000000 --- a/utils/emoji-image.js +++ /dev/null @@ -1,106 +0,0 @@ -const path = require('path') -const fs = require('fs') -const loadImageFromUrl = require('./image-load-url') -const EmojiDbLib = require('emoji-db') -const promiseAllStepN = require('./promise-concurrent') - -const emojiDb = new EmojiDbLib({ useDefaultDb: true }) - -const emojiJFilesDir = '../assets/emoji/' - -const brandFoledIds = { - apple: 325, - google: 313, - twitter: 322, - joypixels: 340, - blob: 56 -} - -const emojiJsonByBrand = { - apple: 'emoji-apple-image.json', - google: 'emoji-google-image.json', - twitter: 'emoji-twitter-image.json', - joypixels: 'emoji-joypixels-image.json', - blob: 'emoji-blob-image.json' -} - -let emojiImageByBrand = { - apple: [], - google: [], - twitter: [], - joypixels: [], - blob: [] -} - -async function downloadEmoji (brand) { - console.log('emoji image load start') - - const emojiImage = emojiImageByBrand[brand] - - const emojiJsonFile = path.resolve( - __dirname, - emojiJFilesDir + emojiJsonByBrand[brand] - ) - - const dbData = emojiDb.dbData - const dbArray = Object.keys(dbData) - const emojiPromiseArray = [] - - for (const key of dbArray) { - const emoji = dbData[key] - - if (!emoji.qualified && !emojiImage[key]) { - emojiPromiseArray.push(async () => { - let brandFolderName = brand - if (brand === 'blob') brandFolderName = 'google' - - const fileUrl = `${process.env.EMOJI_DOMAIN}/thumbs/60/${brandFolderName}/${brandFoledIds[brand]}/${emoji.image.file_name}` - - const img = await loadImageFromUrl(fileUrl, (headers) => { - return !headers['content-type'].match(/image/) - }) - - const base64 = img.toString('base64') - - if (base64) { - return { - key, - base64 - } - } - }) - } - } - - const donwloadResult = await promiseAllStepN(200)(emojiPromiseArray) - - for (const emojiData of donwloadResult) { - if (emojiData) emojiImage[emojiData.key] = emojiData.base64 - } - - if (Object.keys(emojiImage).length > 0) { - const emojiJson = JSON.stringify(emojiImage, null, 2) - - fs.writeFile(emojiJsonFile, emojiJson, (err) => { - if (err) return console.log(err) - }) - } - - console.log('emoji image load end') -} - -for (const brand in emojiJsonByBrand) { - const emojiJsonFile = path.resolve( - __dirname, - emojiJFilesDir + emojiJsonByBrand[brand] - ) - - try { - if (fs.existsSync(emojiJsonFile)) emojiImageByBrand[brand] = require(emojiJsonFile) - } catch (error) { - console.log(error) - } - // downloadEmoji(brand) -} - -module.exports = emojiImageByBrand diff --git a/utils/color-manipulate.js b/utils/get-background.js similarity index 65% rename from utils/color-manipulate.js rename to utils/get-background.js index d0508ec..1b4d279 100644 --- a/utils/color-manipulate.js +++ b/utils/get-background.js @@ -1,3 +1,4 @@ +const colorLuminance = require('./color-liminance') const { createCanvas } = require('canvas') const normalizeColor = (color) => { @@ -10,27 +11,7 @@ const normalizeColor = (color) => { return color } -const colorLuminance = (hex, lum) => { - hex = String(hex).replace(/[^0-9a-f]/gi, '') - if (hex.length < 6) { - hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2] - } - lum = lum || 0 - - // convert to decimal and change luminosity - let rgb = '#' - let c - let i - for (i = 0; i < 3; i++) { - c = parseInt(hex.substr(i * 2, 2), 16) - c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16) - rgb += ('00' + c).substr(c.length) - } - - return rgb -} - -const getBackground = (backgroundColor) => { +module.exports = (backgroundColor) => { let backgroundColorOne, backgroundColorTwo const backgroundColorSplit = backgroundColor.split('/') @@ -50,5 +31,3 @@ const getBackground = (backgroundColor) => { return { backgroundColor, backgroundColorOne, backgroundColorTwo } } - -module.exports = { getBackground, colorLuminance } diff --git a/utils/get-media-url.js b/utils/get-media-url.js deleted file mode 100644 index af1bbae..0000000 --- a/utils/get-media-url.js +++ /dev/null @@ -1,6 +0,0 @@ -const telegram = require('./telegram') - -module.exports = async (mediaInfo) => { - const mediaURL = await telegram.getFileLink(mediaInfo).catch(console.error) - return mediaURL -} diff --git a/utils/image-load-path.js b/utils/image-load-path.js deleted file mode 100644 index 6671ade..0000000 --- a/utils/image-load-path.js +++ /dev/null @@ -1,9 +0,0 @@ -const fs = require('fs') - -module.exports = (path) => { - return new Promise((resolve, reject) => { - fs.readFile(path, (_error, data) => { - resolve(data) - }) - }) -} diff --git a/utils/image-load-url.js b/utils/image-load-url.js deleted file mode 100644 index 0548c57..0000000 --- a/utils/image-load-url.js +++ /dev/null @@ -1,23 +0,0 @@ -const https = require('https') - -module.exports = (url, filter = false) => { - return new Promise((resolve, reject) => { - https.get(url, (res) => { - if (filter && filter(res.headers)) { - resolve(Buffer.concat([])) - } - - const chunks = [] - - res.on('error', (err) => { - reject(err) - }) - res.on('data', (chunk) => { - chunks.push(chunk) - }) - res.on('end', () => { - resolve(Buffer.concat(chunks)) - }) - }) - }) -} diff --git a/utils/index.js b/utils/index.js index 94ee398..c48f3e8 100644 --- a/utils/index.js +++ b/utils/index.js @@ -1,7 +1,10 @@ module.exports = { - QuoteGenerate: require('./quote-generate'), - loadImageFromUrl: require('./image-load-url'), - loadImageFromPath: require('./image-load-path'), - promiseAllStepN: require('./promise-concurrent'), - userName: require('./user-name') + telegram: require('./telegram'), + render: require('./render'), + getBackground: require('./get-background'), + getView: require('./get-view'), + getAvatarURL: require('./get-avatar-url'), + formatHTML: require('./format-html'), + colorLuminance: require('./color-liminance'), + lightOrDark: require('./light-or-dark') } diff --git a/utils/promise-concurrent.js b/utils/promise-concurrent.js deleted file mode 100644 index 7c82774..0000000 --- a/utils/promise-concurrent.js +++ /dev/null @@ -1,29 +0,0 @@ -function promiseAllStepN (n, list) { - let tail = list.splice(n) - let head = list - let resolved = [] - let processed = 0 - return new Promise(resolve => { - head.forEach(x => { - let res = x() - resolved.push(res) - res.then(y => { - runNext() - return y - }) - }) - function runNext () { - if (processed == tail.length) { - resolve(Promise.all(resolved)) - } else { - resolved.push(tail[processed]().then(x => { - runNext() - return x - })) - processed++ - } - } - }) -} - -module.exports = n => list => promiseAllStepN(n, list) diff --git a/utils/user-name.js b/utils/user-name.js deleted file mode 100644 index 63073c9..0000000 --- a/utils/user-name.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = (user, url = false) => { - let name = user.first_name - - if (user.last_name) name += ` ${user.last_name}` - name = name.replace(//g, '>') - - if (url) return `${name}` - return name -} diff --git a/views/html.css b/views/html.css index 93cb7a2..f62b953 100644 --- a/views/html.css +++ b/views/html.css @@ -5,7 +5,7 @@ body { position: absolute; padding: 35px 55px; max-width: {{width}}px; - max-height: {{height}}px; + max-height: none; background: url("{{background.image.url}}") top left / cover repeat, radial-gradient(ellipse farthest-corner at center, {{background.color1}}, {{background.color2}}); @@ -40,6 +40,7 @@ body { .message { margin-left: 65px; padding: 14px; + max-height: {{height}}px; background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}}); border-radius: 25px; box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); diff --git a/views/image.css b/views/image.css index 0e7755e..e80596e 100644 --- a/views/image.css +++ b/views/image.css @@ -5,7 +5,6 @@ body { position: absolute; padding: 35px 55px; max-width: {{width}}px; - max-height: {{height}}px; transform: scale({{scale}}); background: url("{{background.image.url}}") top left / cover repeat, @@ -41,6 +40,7 @@ body { .message { margin-left: 65px; padding: 14px; + max-height: {{height}}px; background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}}); border-radius: 25px; box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); diff --git a/views/png.css b/views/png.css index 6a261b0..eb31747 100644 --- a/views/png.css +++ b/views/png.css @@ -5,7 +5,6 @@ body { position: absolute; padding: 35px 55px; max-width: {{width}}px; - max-height: {{height}}px; transform: scale({{scale}}); background: none; .messages-list { @@ -38,6 +37,7 @@ body { .message { margin-left: 65px; padding: 14px; + max-height: {{height}}px; background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}}); border-radius: 25px; box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); diff --git a/views/quote.css b/views/quote.css index 34daaec..b025698 100644 --- a/views/quote.css +++ b/views/quote.css @@ -5,7 +5,6 @@ body { position: absolute; padding-bottom: 75px; max-width: {{width}}px; - max-height: {{height}}px; transform: scale({{scale}}); background: none; } @@ -39,6 +38,7 @@ body { .message { margin-left: 65px; padding: 14px; + max-height: {{height}}px; background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}}); border-radius: 25px; } From 2731eb82492582c2a3e67bbe324c4a7a719712ff Mon Sep 17 00:00:00 2001 From: arelive Date: Wed, 21 Jun 2023 15:42:45 +0300 Subject: [PATCH 57/60] chore: update Docker --- Dockerfile | 4 ++-- docker-compose.yml | 5 ++--- methods/generate.js | 5 ++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index b4a703c..c35c53c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ FROM mcr.microsoft.com/playwright:v1.35.0-jammy WORKDIR /app -ADD . /app +COPY . /app -#RUN apt-get update && apt-get install -y build-essential gcc wget git libvips && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y build-essential libvips libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev && rm -rf /var/lib/apt/lists/* RUN npm install && npx playwright install ENTRYPOINT [ "npm", "start" ] diff --git a/docker-compose.yml b/docker-compose.yml index d043daa..e992be2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,8 +2,7 @@ version: '3' services: api: - build: - context: . + build: . env_file: .env restart: always logging: @@ -14,7 +13,7 @@ services: networks: - quotly ports: - - 4888:4888 + - ${PORT}:${PORT} networks: diff --git a/methods/generate.js b/methods/generate.js index 172f712..9761c08 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -84,14 +84,13 @@ const buildReplyMessage = async (message, theme) => { } } -const buildMedia = async (media) => { +const buildMedia = async (media, type) => { let url = media.url if (!url) { const mediaInfo = Array.isArray(media) ? media.pop() : media url = await telegram.getFileLink(mediaInfo).catch(console.error) } - let type = message.mediaType if (!type) { type = url.endsWith('.webp') || url.endsWith('.tgs') ? 'sticker' : 'image' } @@ -125,7 +124,7 @@ const buildMedia = async (media) => { const buildMessage = async (message, theme) => { const from = await buildUser(message.from, theme, { getAvatar: message.avatar || false }) const replyMessage = message.replyMessage ? await buildReplyMessage(message.replyMessage, theme) : null - const media = message.media ? await buildMedia(message.media) : null + const media = message.media ? await buildMedia(message.media, message.mediaType) : null let text = message.text ?? '' if (Array.isArray(message.entities)) { From f39179ee6b0060fa00ce6de5df58255ad562989a Mon Sep 17 00:00:00 2001 From: arelive Date: Mon, 27 May 2024 00:02:54 +0300 Subject: [PATCH 58/60] optimize: reuse open pages --- methods/generate.js | 16 ++--- package-lock.json | 8 +-- package.json | 2 +- pages/html.html | 146 ++++++++++++++++++++++++++++++++++++++++++++ pages/image.html | 144 +++++++++++++++++++++++++++++++++++++++++++ pages/png.html | 145 +++++++++++++++++++++++++++++++++++++++++++ pages/quote.html | 139 +++++++++++++++++++++++++++++++++++++++++ utils/compile.js | 19 ++++++ utils/get-view.js | 25 -------- utils/index.js | 2 +- utils/page-pool.js | 45 ++++++++++++++ utils/render.js | 20 +++--- utils/views.js | 11 ---- views/default.hbs | 59 ------------------ views/html.css | 139 ----------------------------------------- views/html.hbs | 51 ++++++++++++++++ views/image.css | 139 ----------------------------------------- views/image.hbs | 51 ++++++++++++++++ views/png.css | 136 ----------------------------------------- views/png.hbs | 51 ++++++++++++++++ views/quote.css | 132 --------------------------------------- views/quote.hbs | 51 ++++++++++++++++ 22 files changed, 866 insertions(+), 665 deletions(-) create mode 100644 pages/html.html create mode 100644 pages/image.html create mode 100644 pages/png.html create mode 100644 pages/quote.html create mode 100644 utils/compile.js delete mode 100644 utils/get-view.js create mode 100644 utils/page-pool.js delete mode 100644 utils/views.js delete mode 100644 views/default.hbs delete mode 100644 views/html.css create mode 100644 views/html.hbs delete mode 100644 views/image.css create mode 100644 views/image.hbs delete mode 100644 views/png.css create mode 100644 views/png.hbs delete mode 100644 views/quote.css create mode 100644 views/quote.hbs diff --git a/methods/generate.js b/methods/generate.js index 9761c08..1996166 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -8,7 +8,7 @@ const lottie = require('lottie-node') const zlib = require('zlib') const { - telegram, render, getView, getAvatarURL, formatHTML, getBackground, colorLuminance, lightOrDark + telegram, render, compile, getAvatarURL, formatHTML, getBackground, colorLuminance, lightOrDark } = require('../utils') const getEmojiStatusURL = async (emojiId) => { @@ -147,11 +147,12 @@ const userColors = { const bgImageURL = `http://localhost:${process.env.PORT}/assets/pattern_02_alpha.png` const MAX_SCALE = 20 const DEFAULT_BG_COLOR = '//#292232' -const DEFAULT_VIEW_NAME = 'default' -const RENDER_SELECTOR = '#quote' const MAX_QUOTE_WIDTH = 512 const MAX_QUOTE_HEIGHT = 512 +// kostyl +const htmlWrapper = fs.readFileSync(path.resolve(__dirname, '../pages/html.html'), { encoding: 'utf-8' }) + module.exports = async (parm) => { if (!parm || typeof parm != 'object') { return { error: 'query_empty' } @@ -176,8 +177,7 @@ module.exports = async (parm) => { return { error: 'messages_empty' } } - const view = getView(DEFAULT_VIEW_NAME, type) - const content = view({ + const content = compile(type, { scale, width: parm.width, height: parm.height, @@ -190,16 +190,18 @@ module.exports = async (parm) => { messages }) + // kostyl if (type == 'html') { + const result = htmlWrapper.replace('{{> content}}', content) return { - image: ext ? content : Buffer.from(content).toString('base64'), + image: ext ? content : Buffer.from(result).toString('base64'), width: parm.width, height: parm.height, type, ext } } - let image = await render(content, RENDER_SELECTOR) + let image = await render(type, content) const imageSharp = await sharp(image) const { width, height } = await imageSharp.metadata() diff --git a/package-lock.json b/package-lock.json index efab6f1..546a048 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "quote-api", - "version": "0.13.17", + "version": "0.14.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quote-api", - "version": "0.13.17", + "version": "0.14.0", "license": "MIT", "dependencies": { "canvas": "2.11.0", @@ -36,7 +36,6 @@ "devDependencies": { "async": "^3.2.4", "axios": "^1.4.0", - "emoji-db": "^14.0.1", "eslint": "^8.6.0", "eslint-config-standard": "^12.0.0", "eslint-plugin-import": "^2.17.3", @@ -1273,8 +1272,7 @@ "node_modules/emoji-db": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/emoji-db/-/emoji-db-14.0.1.tgz", - "integrity": "sha512-MFiCUr4DsehfCRPAS845AJR4RsekQkh4bUpfag7de3H7FlDHAEbL8Oq03bPzhl4W6rJgrGFhRkcll101ZpQjjg==", - "dev": true + "integrity": "sha512-MFiCUr4DsehfCRPAS845AJR4RsekQkh4bUpfag7de3H7FlDHAEbL8Oq03bPzhl4W6rJgrGFhRkcll101ZpQjjg==" }, "node_modules/emoji-regex": { "version": "8.0.0", diff --git a/package.json b/package.json index 43dfcfb..8ed1880 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "quote-api", - "version": "0.14.0", + "version": "0.14.1", "description": "", "main": "index.js", "scripts": { diff --git a/pages/html.html b/pages/html.html new file mode 100644 index 0000000..d17bc96 --- /dev/null +++ b/pages/html.html @@ -0,0 +1,146 @@ + + + + + + + + + + {{> content}} + + diff --git a/pages/image.html b/pages/image.html new file mode 100644 index 0000000..92d3967 --- /dev/null +++ b/pages/image.html @@ -0,0 +1,144 @@ + + + + + + + + + + + diff --git a/pages/png.html b/pages/png.html new file mode 100644 index 0000000..a5e23a8 --- /dev/null +++ b/pages/png.html @@ -0,0 +1,145 @@ + + + + + + + + + + + diff --git a/pages/quote.html b/pages/quote.html new file mode 100644 index 0000000..a218ab4 --- /dev/null +++ b/pages/quote.html @@ -0,0 +1,139 @@ + + + + + + + + + + + diff --git a/utils/compile.js b/utils/compile.js new file mode 100644 index 0000000..478c1e8 --- /dev/null +++ b/utils/compile.js @@ -0,0 +1,19 @@ +const path = require('path') +const fs = require('fs') +const Handlebars = require('handlebars') + +const cache = {} + +module.exports = (viewName, data) => { + let view = cache[viewName] + + if (view == null) { + const template = fs.readFileSync( + path.resolve(__dirname, `../views/${viewName}.hbs`) + ).toString('utf-8') + view = Handlebars.compile(template) + cache[viewName] = view + } + + return view(data) +} diff --git a/utils/get-view.js b/utils/get-view.js deleted file mode 100644 index 502b3f9..0000000 --- a/utils/get-view.js +++ /dev/null @@ -1,25 +0,0 @@ -const path = require('path') -const fs = require('fs') -const Handlebars = require('handlebars') - -const cache = {} - -module.exports = (viewName, styleName) => { - const cacheKey = `${viewName}/${styleName}` - if (cache[cacheKey]) { - return cache[cacheKey] - } - - const template = fs.readFileSync( - path.resolve(__dirname, `../views/${viewName}.hbs`) - ).toString('utf-8') - - const style = fs.readFileSync( - path.resolve(__dirname, `../views/${styleName}.css`) - ).toString('utf-8') - - const view = Handlebars.compile(template.replace('{{> style}}', style)) - - cache[cacheKey] = view - return view -} diff --git a/utils/index.js b/utils/index.js index c48f3e8..46be5f5 100644 --- a/utils/index.js +++ b/utils/index.js @@ -2,7 +2,7 @@ module.exports = { telegram: require('./telegram'), render: require('./render'), getBackground: require('./get-background'), - getView: require('./get-view'), + compile: require('./compile'), getAvatarURL: require('./get-avatar-url'), formatHTML: require('./format-html'), colorLuminance: require('./color-liminance'), diff --git a/utils/page-pool.js b/utils/page-pool.js new file mode 100644 index 0000000..ee42eb0 --- /dev/null +++ b/utils/page-pool.js @@ -0,0 +1,45 @@ +const path = require('path') +const fs = require('fs') +const { webkit } = require('playwright') + +const promise = webkit.launch().then(browser => browser.newContext()) +const files = {} // cache of contents from .html files +const pages = {} // arrays of browser pages by name + +const popPage = async (pageName) => { + const context = await promise + if (!Array.isArray(pages[pageName])) { + pages[pageName] = [] + } + + if (pages[pageName].length) { + const page = pages[pageName].pop() + return page + } else { + // if pool of appropriate pages is empty, create new page + let html = files[pageName] + if (html == null) { + html = fs.readFileSync( + path.resolve(__dirname, `../pages/${pageName}.html`) + ).toString('utf-8') + files[pageName] = html + } + + const page = await context.newPage() + page.on('console', message => console.log(message.text())) + await page.setContent(html) + + return page + } +} + +const pushPage = (pageName, page) => { + if (!Array.isArray(pages[pageName])) { + pages[pageName] = [] + } + pages[pageName].push(page) +} + +module.exports = { + popPage, pushPage +} diff --git a/utils/render.js b/utils/render.js index 1d60c0f..bbd4c4b 100644 --- a/utils/render.js +++ b/utils/render.js @@ -1,21 +1,21 @@ -const { webkit } = require('playwright') +const { popPage, pushPage } = require('./page-pool') -const promise = webkit.launch().then(browser => browser.newContext()) +module.exports = async (pageName, content) => { + const page = await popPage(pageName) + const body = await page.locator('body') -module.exports = async (content, selector) => { - const context = await promise - const page = await context.newPage() + await body.evaluate((element, content) => { element.innerHTML = content }, content) - page.on('console', console.log) - await page.setContent(content) - await page.waitForSelector(selector, { state: 'visible' }) + const quote = await body.locator('#quote') + await quote.waitFor({ state: 'visible' }) + await page.waitForLoadState('networkidle') - const screenshot = await page.locator(selector).screenshot({ + const screenshot = await quote.screenshot({ type: 'png', scale: 'css', omitBackground: true }) - page.close().catch(console.error) + pushPage(page) return screenshot } diff --git a/utils/views.js b/utils/views.js deleted file mode 100644 index 3c515d3..0000000 --- a/utils/views.js +++ /dev/null @@ -1,11 +0,0 @@ -const path = require('path') -const fs = require('fs') -const Handlebars = require('handlebars') - -const quote = Handlebars.compile( - fs.readFileSync(path.resolve('./views/quote.hbs')).toString('utf-8') -) - -module.exports = { - quote -} diff --git a/views/default.hbs b/views/default.hbs deleted file mode 100644 index 5537925..0000000 --- a/views/default.hbs +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - -

    -
      - {{#each messages}} -
    • - {{#if this.showAvatar}} - {{#if this.from.photo}} - {{this.from.initials}} - {{else}} -

      - {{this.from.initials}} -

      - {{/if}} - {{/if}} -
      -
      -

      - {{this.from.name}} - {{#if this.from.emojiStatus}} - - {{/if}} -

      -
      -
      - {{#if this.replyMessage}} - {{#with this.replyMessage}} -
      -

      - {{this.from.name}} - {{#if this.from.emojiStatus}} - - {{/if}} -

      -

      {{this.text}}

      -
      - {{/with}} - {{/if}} - {{#if this.media}} - - {{/if}} -

      {{{this.text}}}

      -
      -
      -
    • - {{/each}} -
    -

    @QuotLyBot

    - - diff --git a/views/html.css b/views/html.css deleted file mode 100644 index f62b953..0000000 --- a/views/html.css +++ /dev/null @@ -1,139 +0,0 @@ -body { - background: transparent; -} -#quote { - position: absolute; - padding: 35px 55px; - max-width: {{width}}px; - max-height: none; - background: - url("{{background.image.url}}") top left / cover repeat, - radial-gradient(ellipse farthest-corner at center, {{background.color1}}, {{background.color2}}); -} -.messages-list { - list-style: none; -} -.message-item { - position: relative; - display: block; - float: left; - clear: both; - margin-bottom: 5px; - max-width: 100%; -} -.message-item:last-child { - margin-bottom: 0; -} -.message-from-avatar, -.message-from-initials { - position: absolute; - top: 0; - left: 0; - width: 50px; - height: 50px; - font: 25px/2 'Nono Sans', sans-serif; - text-align: center; - color: #FFF; - border-radius: 50%; - box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); -} -.message { - margin-left: 65px; - padding: 14px; - max-height: {{height}}px; - background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}}); - border-radius: 25px; - box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); -} -.message-item.sticker .message { - padding: 0; - background: none; - border-radius: 5px; - box-shadow: none; -} -.message-from-name, -.message-reply-from-name { - overflow: hidden; - width: 100%; - font: bold 22px/1.2 'Noto Sans', sans-serif; - white-space: nowrap; - text-overflow: ellipsis; -} -.message-item.sticker .message-from-name { - display: none; -} -.message-reply { - margin: 8px 0; - padding: 0 14px; - border-left: 3px solid; -} -.message-item.sticker .message-reply { - display: none; -} -.message-reply-from-name { - font-size: 16px; -} -.message-text, -.message-reply-text { - overflow: hidden; - width: 100%; - font: 24px/1.2 'Noto Sans', sans-serif; - text-overflow: ellipsis; - color: #000; -} -.message-item.sticker .message-text { - display: none; -} -.dark .message-text, -.dark .message-reply-text { - color: #FFF; -} -.message-reply-text { - font-size: 21px; - white-space: nowrap; -} -.message-text code { - font-family: 'Noto Mono', monospace; -} -.message-text .spoiler { - opacity: .3; -} -.message-text .mention, -.message-text .hashtag, -.message-text .cashtag, -.message-text .bot-command, -.message-text .email, -.message-text .phone-number { - text-decoration: none; - color: blue; -} -.custom-emoji, -.emoji-status { - display: inline; -} -.message-reply-text .custom-emoji { - height: 21px; -} -.message-text .custom-emoji { - height: 24px; -} -.message-from-name .emoji-status { - height: 22px; -} -.message-reply-from-name .emoji-status { - height: 16px; -} -.message-media { - min-width: 100%; - max-height: 512px; - object-fit: cover; - object-position: center; -} -.watermark { - position: absolute; - bottom: 5px; - right: 5px; - font: 12px 'Noto Sans'; - text-align: right; - color: rgba(0, 0, 0, .3); -} diff --git a/views/html.hbs b/views/html.hbs new file mode 100644 index 0000000..de5fe34 --- /dev/null +++ b/views/html.hbs @@ -0,0 +1,51 @@ +
    +
      + {{#each messages}} +
    • + {{#if this.showAvatar}} + {{#if this.from.photo}} + {{this.from.initials}} + {{else}} +

      + {{this.from.initials}} +

      + {{/if}} + {{/if}} +
      +
      +

      + {{this.from.name}} + {{#if this.from.emojiStatus}} + + {{/if}} +

      +
      +
      + {{#if this.replyMessage}} + {{#with this.replyMessage}} +
      +

      + {{this.from.name}} + {{#if this.from.emojiStatus}} + + {{/if}} +

      +

      {{this.text}}

      +
      + {{/with}} + {{/if}} + {{#if this.media}} + + {{/if}} +

      {{{this.text}}}

      +
      +
      +
    • + {{/each}} +
    +

    @QuotLyBot

    +
    diff --git a/views/image.css b/views/image.css deleted file mode 100644 index e80596e..0000000 --- a/views/image.css +++ /dev/null @@ -1,139 +0,0 @@ -body { - background: transparent; -} -#quote { - position: absolute; - padding: 35px 55px; - max-width: {{width}}px; - transform: scale({{scale}}); - background: - url("{{background.image.url}}") top left / cover repeat, - radial-gradient(ellipse farthest-corner at center, {{background.color1}}, {{background.color2}}); -} -.messages-list { - list-style: none; -} -.message-item { - position: relative; - display: block; - float: left; - clear: both; - margin-bottom: 5px; - max-width: 100%; -} -.message-item:last-child { - margin-bottom: 0; -} -.message-from-avatar, -.message-from-initials { - position: absolute; - top: 0; - left: 0; - width: 50px; - height: 50px; - font: 25px/2 'Nono Sans', sans-serif; - text-align: center; - color: #FFF; - border-radius: 50%; - box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); -} -.message { - margin-left: 65px; - padding: 14px; - max-height: {{height}}px; - background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}}); - border-radius: 25px; - box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); -} -.message-item.sticker .message { - padding: 0; - background: none; - border-radius: 5px; - box-shadow: none; -} -.message-from-name, -.message-reply-from-name { - overflow: hidden; - width: 100%; - font: bold 22px/1.2 'Noto Sans', sans-serif; - white-space: nowrap; - text-overflow: ellipsis; -} -.message-item.sticker .message-from-name { - display: none; -} -.message-reply { - margin: 8px 0; - padding: 0 14px; - border-left: 3px solid; -} -.message-item.sticker .message-reply { - display: none; -} -.message-reply-from-name { - font-size: 16px; -} -.message-text, -.message-reply-text { - overflow: hidden; - width: 100%; - font: 24px/1.2 'Noto Sans', sans-serif; - text-overflow: ellipsis; - color: #000; -} -.message-item.sticker .message-text { - display: none; -} -.dark .message-text, -.dark .message-reply-text { - color: #FFF; -} -.message-reply-text { - font-size: 21px; - white-space: nowrap; -} -.message-text code { - font-family: 'Noto Mono', monospace; -} -.message-text .spoiler { - opacity: .3; -} -.message-text .mention, -.message-text .hashtag, -.message-text .cashtag, -.message-text .bot-command, -.message-text .email, -.message-text .phone-number { - text-decoration: none; - color: blue; -} -.custom-emoji, -.emoji-status { - display: inline; -} -.message-reply-text .custom-emoji { - height: 21px; -} -.message-text .custom-emoji { - height: 24px; -} -.message-from-name .emoji-status { - height: 22px; -} -.message-reply-from-name .emoji-status { - height: 16px; -} -.message-media { - min-width: 100%; - max-height: 512px; - object-fit: cover; - object-position: center; -} -.watermark { - position: absolute; - bottom: 5px; - right: 5px; - font: 12px 'Noto Sans'; - text-align: right; - color: rgba(0, 0, 0, .3); -} diff --git a/views/image.hbs b/views/image.hbs new file mode 100644 index 0000000..6eed850 --- /dev/null +++ b/views/image.hbs @@ -0,0 +1,51 @@ +
    +
      + {{#each messages}} +
    • + {{#if this.showAvatar}} + {{#if this.from.photo}} + {{this.from.initials}} + {{else}} +

      + {{this.from.initials}} +

      + {{/if}} + {{/if}} +
      +
      +

      + {{this.from.name}} + {{#if this.from.emojiStatus}} + + {{/if}} +

      +
      +
      + {{#if this.replyMessage}} + {{#with this.replyMessage}} +
      +

      + {{this.from.name}} + {{#if this.from.emojiStatus}} + + {{/if}} +

      +

      {{this.text}}

      +
      + {{/with}} + {{/if}} + {{#if this.media}} + + {{/if}} +

      {{{this.text}}}

      +
      +
      +
    • + {{/each}} +
    +

    @QuotLyBot

    +
    diff --git a/views/png.css b/views/png.css deleted file mode 100644 index eb31747..0000000 --- a/views/png.css +++ /dev/null @@ -1,136 +0,0 @@ -body { - background: transparent; -} -#quote { - position: absolute; - padding: 35px 55px; - max-width: {{width}}px; - transform: scale({{scale}}); - background: none; -.messages-list { - list-style: none; -} -.message-item { - position: relative; - display: block; - float: left; - clear: both; - margin-bottom: 5px; - max-width: 100%; -} -.message-item:last-child { - margin-bottom: 0; -} -.message-from-avatar, -.message-from-initials { - position: absolute; - top: 0; - left: 0; - width: 50px; - height: 50px; - font: 25px/2 'Nono Sans', sans-serif; - text-align: center; - color: #FFF; - border-radius: 50%; - box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); -} -.message { - margin-left: 65px; - padding: 14px; - max-height: {{height}}px; - background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}}); - border-radius: 25px; - box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); -} -.message-item.sticker .message { - padding: 0; - background: none; - border-radius: 5px; - box-shadow: none; -} -.message-from-name, -.message-reply-from-name { - overflow: hidden; - width: 100%; - font: bold 22px/1.2 'Noto Sans', sans-serif; - white-space: nowrap; - text-overflow: ellipsis; -} -.message-item.sticker .message-from-name { - display: none; -} -.message-reply { - margin: 8px 0; - padding: 0 14px; - border-left: 3px solid; -} -.message-item.sticker .message-reply { - display: none; -} -.message-reply-from-name { - font-size: 16px; -} -.message-text, -.message-reply-text { - overflow: hidden; - width: 100%; - font: 24px/1.2 'Noto Sans', sans-serif; - text-overflow: ellipsis; - color: #000; -} -.message-item.sticker .message-text { - display: none; -} -.dark .message-text, -.dark .message-reply-text { - color: #FFF; -} -.message-reply-text { - font-size: 21px; - white-space: nowrap; -} -.message-text code { - font-family: 'Noto Mono', monospace; -} -.message-text .spoiler { - opacity: .3; -} -.message-text .mention, -.message-text .hashtag, -.message-text .cashtag, -.message-text .bot-command, -.message-text .email, -.message-text .phone-number { - text-decoration: none; - color: blue; -} -.custom-emoji, -.emoji-status { - display: inline; -} -.message-reply-text .custom-emoji { - height: 21px; -} -.message-text .custom-emoji { - height: 24px; -} -.message-from-name .emoji-status { - height: 22px; -} -.message-reply-from-name .emoji-status { - height: 16px; -} -.message-media { - min-width: 100%; - max-height: 512px; - object-fit: cover; - object-position: center; -} -.watermark { - position: absolute; - bottom: 5px; - right: 5px; - font: 12px 'Noto Sans'; - text-align: right; - color: rgba(0, 0, 0, .3); -} diff --git a/views/png.hbs b/views/png.hbs new file mode 100644 index 0000000..fe0889f --- /dev/null +++ b/views/png.hbs @@ -0,0 +1,51 @@ +
    +
      + {{#each messages}} +
    • + {{#if this.showAvatar}} + {{#if this.from.photo}} + {{this.from.initials}} + {{else}} +

      + {{this.from.initials}} +

      + {{/if}} + {{/if}} +
      +
      +

      + {{this.from.name}} + {{#if this.from.emojiStatus}} + + {{/if}} +

      +
      +
      + {{#if this.replyMessage}} + {{#with this.replyMessage}} +
      +

      + {{this.from.name}} + {{#if this.from.emojiStatus}} + + {{/if}} +

      +

      {{this.text}}

      +
      + {{/with}} + {{/if}} + {{#if this.media}} + + {{/if}} +

      {{{this.text}}}

      +
      +
      +
    • + {{/each}} +
    +

    @QuotLyBot

    +
    diff --git a/views/quote.css b/views/quote.css deleted file mode 100644 index b025698..0000000 --- a/views/quote.css +++ /dev/null @@ -1,132 +0,0 @@ -body { - background: transparent; -} -#quote { - position: absolute; - padding-bottom: 75px; - max-width: {{width}}px; - transform: scale({{scale}}); - background: none; -} -.messages-list { - list-style: none; -} -.message-item { - position: relative; - display: block; - float: left; - clear: both; - margin-bottom: 5px; - max-width: 100%; -} -.message-item:last-child { - margin-bottom: 0; -} -.message-from-avatar, -.message-from-initials { - position: absolute; - top: 0; - left: 0; - width: 50px; - height: 50px; - font: 25px/2 'Nono Sans', sans-serif; - text-align: center; - color: #FFF; - border-radius: 50%; - box-shadow: 8px 8px 13px rgba(0, 0, 0, .5); -} -.message { - margin-left: 65px; - padding: 14px; - max-height: {{height}}px; - background: linear-gradient(-45deg, {{background.color1}}, {{background.color2}}); - border-radius: 25px; -} -.message-item.sticker .message { - padding: 0; - background: none; - border-radius: 5px; - box-shadow: none; -} -.message-from-name, -.message-reply-from-name { - overflow: hidden; - width: 100%; - font: bold 22px/1.2 'Noto Sans', sans-serif; - white-space: nowrap; - text-overflow: ellipsis; -} -.message-item.sticker .message-from-name { - display: none; -} -.message-reply { - margin: 8px 0; - padding: 0 14px; - border-left: 3px solid; -} -.message-item.sticker .message-reply { - display: none; -} -.message-reply-from-name { - font-size: 16px; -} -.message-text, -.message-reply-text { - overflow: hidden; - width: 100%; - font: 24px/1.2 'Noto Sans', sans-serif; - text-overflow: ellipsis; - color: #000; -} -.message-item.sticker .message-text { - display: none; -} -.dark .message-text, -.dark .message-reply-text { - color: #FFF; -} -.message-reply-text { - font-size: 21px; - white-space: nowrap; -} -.message-text code { - font-family: 'Noto Mono', monospace; -} -.message-text .spoiler { - opacity: .3; -} -.message-text .mention, -.message-text .hashtag, -.message-text .cashtag, -.message-text .bot-command, -.message-text .email, -.message-text .phone-number { - text-decoration: none; - color: blue; -} -.custom-emoji, -.emoji-status { - display: inline; -} -.message-reply-text .custom-emoji { - height: 21px; -} -.message-text .custom-emoji { - height: 24px; -} -.message-from-name .emoji-status { - height: 22px; -} -.message-reply-from-name .emoji-status { - height: 16px; -} -.message-media { - min-width: 100%; - max-height: 512px; - object-fit: cover; - object-position: center; -} -.watermark { - display: none; -} - diff --git a/views/quote.hbs b/views/quote.hbs new file mode 100644 index 0000000..fe0889f --- /dev/null +++ b/views/quote.hbs @@ -0,0 +1,51 @@ +
    +
      + {{#each messages}} +
    • + {{#if this.showAvatar}} + {{#if this.from.photo}} + {{this.from.initials}} + {{else}} +

      + {{this.from.initials}} +

      + {{/if}} + {{/if}} +
      +
      +

      + {{this.from.name}} + {{#if this.from.emojiStatus}} + + {{/if}} +

      +
      +
      + {{#if this.replyMessage}} + {{#with this.replyMessage}} +
      +

      + {{this.from.name}} + {{#if this.from.emojiStatus}} + + {{/if}} +

      +

      {{this.text}}

      +
      + {{/with}} + {{/if}} + {{#if this.media}} + + {{/if}} +

      {{{this.text}}}

      +
      +
      +
    • + {{/each}} +
    +

    @QuotLyBot

    +
    From 10e5418c8ac2fdc0cd977c910937f2f10c10b445 Mon Sep 17 00:00:00 2001 From: arelive Date: Mon, 27 May 2024 09:54:23 +0300 Subject: [PATCH 59/60] optimize: cache assets and images --- app.js | 4 ++-- assets/reset.min.css | 1 + methods/generate.js | 4 +++- package.json | 2 +- pages/html.html | 2 +- pages/image.html | 2 +- pages/png.html | 2 +- pages/quote.html | 2 +- utils/page-pool.js | 6 +++--- utils/render.js | 11 +++++++---- 10 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 assets/reset.min.css diff --git a/app.js b/app.js index 2476251..f0a5ced 100644 --- a/app.js +++ b/app.js @@ -13,8 +13,8 @@ const app = new Koa() app.use(logger()) app.use(responseTime()) app.use(bodyParser()) -app.use(mount('/assets', serve(path.resolve(__dirname, 'assets')))) -app.use(mount('/cache', serve(path.resolve(__dirname, 'cache')))) +app.use(mount('/assets', serve(path.resolve(__dirname, 'assets'), { maxAge: 1000 * 3600 * 4, immutable: true }))) +app.use(mount('/cache', serve(path.resolve(__dirname, 'cache'), { maxAge: 1000 * 3600 * 4, immutable: true }))) const ratelimitDb = new Map() diff --git a/assets/reset.min.css b/assets/reset.min.css new file mode 100644 index 0000000..a4b442c --- /dev/null +++ b/assets/reset.min.css @@ -0,0 +1 @@ +/* v1.1 | https://github.com/arebaka/reset.css | License: MIT*/*,:before,:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;font:100% inherit;vertical-align:baseline;background-repeat:no-repeat;border:none;text-shadow:none;box-shadow:none}:hover,:focus,:active{outline:none}::-moz-selection,::selection{text-shadow:none}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section{display:block}[hidden]{display:none}:root{-webkit-tab-size:4;-moz-tab-size:4;-ms-tab-size:4;-o-tab-size:4;-webkit-tap-highlight-color:transparent;-moz-tap-highlight-color:transparent;-ms-tap-highlight-color:transparent;-o-tap-highlight-color:transparent;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;-ms-text-size-adjust:100%;-o-text-size-adjust:100%;tab-size:4;line-height:1.5;text-size-adjust:100%;overflow-wrap:break-word;cursor:default}body{scroll-behavior:smooth;min-height:100vh;color:#000;background-color:#fff}ol,ul{list-style:none}ol{list-style-type:decimal}ul{list-style-type:disc}nav ol,nav ul{list-style-type:none}li{display:list-item}menu{list-style-type:disc}blockquote,q{quotes:none}cite{quotes:none;font-style:italic}blockquote:before,blockquote:after,q:before,q:after,cite:before,cite:after{content:"";content:none}table{text-indent:0;border-collapse:collapse;border-spacing:0;border-color:currentColor}thead{display:table-header-group;vertical-align:middle;border-color:inherit}tbody{display:table-row-group;vertical-align:middle;border-color:inherit}tfoot{display:table-footer-group;vertical-align:middle;border-color:inherit}col{display:table-column}colgroup{display:table-column-group}tr{display:table-row;vertical-align:inherit;border-color:inherit}th{display:table-cell;vertical-align:inherit;font-weight:700;text-align:center}td{display:table-cell;vertical-align:inherit}label,legend{font:inherit;white-space:normal;color:inherit;cursor:default}legend{display:table}input,textarea,select,button{display:inline-block;max-width:100%;height:auto;font:inherit;line-height:normal;letter-spacing:inherit;color:inherit;background-color:transparent;border:1px solid;border-radius:0}select,button{text-transform:none}optgroup{font:inherit;line-height:normal;text-transform:none}button,input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;-moz-appearance:button;-ms-appearance:button;-o-appearance:button;overflow:visible;cursor:pointer;appearance:button}button:disabled,input:disabled{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button:-moz-focusring,input:-moz-focusring{outline:none}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;-ms-box-sizing:content-box;-o-box-sizing:content-box;-webkit-appearance:none;-moz-appearance:none;-moz-appearance:none;-o-appearance:none;box-sizing:content-box;appearance:none;outline:none}::-webkit-search-cancel-button,::-webkit-search-decoration,::-webkit-search-results-button,::-webkit-search-results-decoration{-webkit-appearance:none;-moz-appearance:none;-moz-appearance:none;-o-appearance:none;appearance:none}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}textarea{overflow:auto;resize:vertical}::-webkit-file-upload-button{-webkit-appearance:button;-moz-appearance:button;-moz-appearance:button;-o-appearance:button;font:inherit;appearance:button}img,audio,video,canvas,svg,iframe{max-width:100%;height:auto;vertical-align:middle;border:none}img,audio,video,canvas,svg{-ms-interpolation-mode:bicubic;display:inline-block}iframe{display:block}address{font-style:italic}abbr[title]{text-decoration:underline dotted;border-bottom:0}caption{display:table-caption;text-align:center}fieldset{border:none}summary{display:list-item}template{display:none}a{text-decoration:underline;color:blue;background-color:transparent;cursor:pointer}b,strong{font-weight:700}dfn,em,i,var{font-style:italic}code,kbd,samp,tt{font:1em monospace}pre{overflow:auto;display:block;font:1em monospace;white-space:pre}del,s,strike{text-decoration:line-through}ins{text-decoration:underline}mark{color:#000;background-color:#ff0}sup{font-size:smaller;vertical-align:super}sub{font-size:smaller;vertical-align:sub}big{font-size:larger}small{font-size:smaller}center{text-align:center}h1,h2,h3,h4,h5,h6{font-weight:700;text-align:center}h1{font-size:2em;line-height:3.34em}h2{font-size:1.5em;line-height:3.16em}h3{font-size:1.17em;line-height:3.17em}h4{font-size:1em;line-height:3.66em}h5{font-size:.83em;line-height:4.17em}h6{font-size:.67em;line-height:5.33em}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;-ms-box-sizing:content-box;-o-box-sizing:content-box;box-sizing:content-box;display:block;margin:.5em auto;height:0;border:1px inset}@media (prefers-reduced-motion: reduce){*,:before,:after{animation-delay:-1ms!important;animation-duration:1ms!important;animation-iteration-count:1!important;background-attachment:initial!important;scroll-behavior:auto!important;transition-delay:0!important;transition-duration:0!important}} diff --git a/methods/generate.js b/methods/generate.js index 1996166..6626254 100644 --- a/methods/generate.js +++ b/methods/generate.js @@ -192,7 +192,9 @@ module.exports = async (parm) => { // kostyl if (type == 'html') { - const result = htmlWrapper.replace('{{> content}}', content) + const result = htmlWrapper + .replace('{{> content}}', content) + .replace('{{> CSSresetURL}}', `http://localhost:${process.env.PORT}/assets/reset.min.css`) return { image: ext ? content : Buffer.from(result).toString('base64'), width: parm.width, diff --git a/package.json b/package.json index 8ed1880..d4669b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "quote-api", - "version": "0.14.1", + "version": "0.14.2", "description": "", "main": "index.js", "scripts": { diff --git a/pages/html.html b/pages/html.html index d17bc96..c9bf582 100644 --- a/pages/html.html +++ b/pages/html.html @@ -2,7 +2,7 @@ - +