Ditzzy AF commited on
Commit ·
ff883f5
1
Parent(s): d1670eb
feat(README.md): enhance README with detailed project info and quick start guide
Browse files- Updated project title, emoji, and color scheme
- Added project description, features, and configuration links
- Included quick start guide for Docker deployment
- .editorconfig +9 -0
- .env +3 -0
- .env.example +3 -0
- .eslintrc.json +17 -0
- .gitignore +63 -0
- Dockerfile +34 -0
- README.md +49 -4
- app.js +80 -0
- assets/fonts/.gitkeep +0 -0
- docker-compose.yml +23 -0
- ecosystem.config.js +17 -0
- helpers/api.js +47 -0
- helpers/index.js +5 -0
- index.js +2 -0
- methods/generate.js +359 -0
- methods/index.js +38 -0
- package-lock.json +0 -0
- package.json +48 -0
- routes/api.js +14 -0
- routes/index.js +5 -0
- utils/emoji-image.js +106 -0
- utils/image-load-path.js +9 -0
- utils/image-load-url.js +28 -0
- utils/index.js +7 -0
- utils/promise-concurrent.js +29 -0
- utils/quote-generate.js +1308 -0
- utils/user-name.js +9 -0
.editorconfig
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
root = true
|
| 2 |
+
|
| 3 |
+
[*]
|
| 4 |
+
charset = utf-8
|
| 5 |
+
end_of_line = lf
|
| 6 |
+
insert_final_newline = true
|
| 7 |
+
trim_trailing_whitespace = true
|
| 8 |
+
indent_style = space
|
| 9 |
+
indent_size = 2
|
.env
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
PORT="7860"
|
| 2 |
+
BOT_TOKEN="7409167350:AAEfeq81FFYg_XYm7RGUROWDKJiPzaivc00"
|
| 3 |
+
EMOJI_DOMAIN="https://emojipedia.org/"
|
.env.example
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
PORT="7860"
|
| 2 |
+
BOT_TOKEN="" #your telegram bot token
|
| 3 |
+
EMOJI_DOMAIN="https://emojipedia.org/"
|
.eslintrc.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"env": {
|
| 3 |
+
"es6": true,
|
| 4 |
+
"node": true
|
| 5 |
+
},
|
| 6 |
+
"extends": "standard",
|
| 7 |
+
"globals": {
|
| 8 |
+
"Atomics": "readonly",
|
| 9 |
+
"SharedArrayBuffer": "readonly"
|
| 10 |
+
},
|
| 11 |
+
"parserOptions": {
|
| 12 |
+
"ecmaVersion": 2018,
|
| 13 |
+
"sourceType": "module"
|
| 14 |
+
},
|
| 15 |
+
"rules": {
|
| 16 |
+
}
|
| 17 |
+
}
|
.gitignore
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
|
| 8 |
+
# Runtime data
|
| 9 |
+
pids
|
| 10 |
+
*.pid
|
| 11 |
+
*.seed
|
| 12 |
+
*.pid.lock
|
| 13 |
+
|
| 14 |
+
# Directory for instrumented libs generated by jscoverage/JSCover
|
| 15 |
+
lib-cov
|
| 16 |
+
|
| 17 |
+
# Coverage directory used by tools like istanbul
|
| 18 |
+
coverage
|
| 19 |
+
|
| 20 |
+
# nyc test coverage
|
| 21 |
+
.nyc_output
|
| 22 |
+
|
| 23 |
+
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
| 24 |
+
.grunt
|
| 25 |
+
|
| 26 |
+
# Bower dependency directory (https://bower.io/)
|
| 27 |
+
bower_components
|
| 28 |
+
|
| 29 |
+
# node-waf configuration
|
| 30 |
+
.lock-wscript
|
| 31 |
+
|
| 32 |
+
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
| 33 |
+
build/Release
|
| 34 |
+
|
| 35 |
+
# Dependency directories
|
| 36 |
+
node_modules/
|
| 37 |
+
jspm_packages/
|
| 38 |
+
|
| 39 |
+
# TypeScript v1 declaration files
|
| 40 |
+
typings/
|
| 41 |
+
|
| 42 |
+
# Optional npm cache directory
|
| 43 |
+
.npm
|
| 44 |
+
|
| 45 |
+
# Optional eslint cache
|
| 46 |
+
.eslintcache
|
| 47 |
+
|
| 48 |
+
# Optional REPL history
|
| 49 |
+
.node_repl_history
|
| 50 |
+
|
| 51 |
+
# Output of 'npm pack'
|
| 52 |
+
*.tgz
|
| 53 |
+
|
| 54 |
+
# Yarn Integrity file
|
| 55 |
+
.yarn-integrity
|
| 56 |
+
|
| 57 |
+
# next.js build output
|
| 58 |
+
.next
|
| 59 |
+
|
| 60 |
+
assets/fonts/*
|
| 61 |
+
assets/emojis/*
|
| 62 |
+
|
| 63 |
+
!.gitkeep
|
Dockerfile
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-alpine3.16
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
ADD . /app
|
| 5 |
+
|
| 6 |
+
# install dependency sistem
|
| 7 |
+
RUN apk add --no-cache \
|
| 8 |
+
font-noto font-noto-cjk font-noto-extra gcompat libstdc++ libuuid \
|
| 9 |
+
vips-dev build-base jpeg-dev pango-dev cairo-dev imagemagick libssl1.1 \
|
| 10 |
+
curl
|
| 11 |
+
|
| 12 |
+
# buat folder assets
|
| 13 |
+
RUN mkdir -p assets
|
| 14 |
+
|
| 15 |
+
# download file PNG
|
| 16 |
+
RUN curl -o assets/pattern_02.png https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/pattern_02.png \
|
| 17 |
+
&& curl -o assets/pattern_ny.png https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/pattern_ny.png \
|
| 18 |
+
&& curl -o assets/pattern_ny_old.png https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/pattern_ny_old.png
|
| 19 |
+
|
| 20 |
+
# download emoji JSON (sesuai sebelumnya)
|
| 21 |
+
RUN mkdir -p assets/emoji \
|
| 22 |
+
&& curl -o assets/emoji/emoji-apple-image.json https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/emoji/emoji-apple-image.json \
|
| 23 |
+
&& curl -o assets/emoji/emoji-google-image.json https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/emoji/emoji-google-image.json \
|
| 24 |
+
&& curl -o assets/emoji/emoji-joypixels-image.json https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/emoji/emoji-joypixels-image.json \
|
| 25 |
+
&& curl -o assets/emoji/emoji-twitter-image.json https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/emoji/emoji-twitter-image.json
|
| 26 |
+
|
| 27 |
+
# buat symlink resolv
|
| 28 |
+
RUN ln -s /lib/libresolv.so.2 /usr/lib/libresolv.so.2
|
| 29 |
+
|
| 30 |
+
# install node modules
|
| 31 |
+
RUN npm install
|
| 32 |
+
|
| 33 |
+
EXPOSE 7860
|
| 34 |
+
CMD ["node", "index.js"]
|
README.md
CHANGED
|
@@ -1,10 +1,55 @@
|
|
| 1 |
---
|
| 2 |
title: Quotely
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Quotely
|
| 3 |
+
emoji: 🌖
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
+
<h1 align="center">🌖 Quotely</h1>
|
| 11 |
+
<p align="center">
|
| 12 |
+
<b>Minimalist Quote Generator</b><br>
|
| 13 |
+
<a href="https://www.docker.com/"><img src="https://img.shields.io/badge/Docker-Ready-blue?logo=docker" alt="Docker"></a>
|
| 14 |
+
<a href="https://huggingface.co/docs/hub/spaces-config-reference"><img src="https://img.shields.io/badge/Hugging%20Face-Docs-purple?logo=huggingface" alt="Hugging Face Docs"></a>
|
| 15 |
+
</p>
|
| 16 |
+
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
<p align="center">
|
| 20 |
+
<i>Generate beautiful, shareable quotes in seconds.<br>
|
| 21 |
+
Deploy anywhere with Docker.</i>
|
| 22 |
+
</p>
|
| 23 |
+
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
## ✨ Features
|
| 27 |
+
|
| 28 |
+
- 🎨 Customizable backgrounds & avatars
|
| 29 |
+
- 🖼️ High-quality `webp` image output
|
| 30 |
+
- 🚀 One-command Docker deployment
|
| 31 |
+
- ⚡ Fast, lightweight, and scalable
|
| 32 |
+
|
| 33 |
+
---
|
| 34 |
+
|
| 35 |
+
## 🚀 Quick Start
|
| 36 |
+
|
| 37 |
+
```bash
|
| 38 |
+
# Clone the repo
|
| 39 |
+
git clone https://huggingface.co/spaces/cv3inx/quotely
|
| 40 |
+
cd quotely
|
| 41 |
+
|
| 42 |
+
# Build & run with Docker
|
| 43 |
+
docker build -t quotely .
|
| 44 |
+
docker run -p 7860:7860 quotely
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
Open [http://localhost:7860](http://localhost:7860) in your browser.
|
| 48 |
+
|
| 49 |
+
---
|
| 50 |
+
|
| 51 |
+
## ⚙️ Configuration
|
| 52 |
+
|
| 53 |
+
See [Hugging Face Spaces Config Reference](https://huggingface.co/docs/hub/spaces-config-reference) for advanced options.
|
| 54 |
+
|
| 55 |
+
---
|
app.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const logger = require('koa-logger')
|
| 2 |
+
const responseTime = require('koa-response-time')
|
| 3 |
+
const bodyParser = require('koa-bodyparser')
|
| 4 |
+
const Router = require('koa-router')
|
| 5 |
+
const Koa = require('koa')
|
| 6 |
+
const os = require('os')
|
| 7 |
+
|
| 8 |
+
const app = new Koa()
|
| 9 |
+
|
| 10 |
+
app.use(logger())
|
| 11 |
+
app.use(responseTime())
|
| 12 |
+
app.use(bodyParser())
|
| 13 |
+
|
| 14 |
+
app.use(require('./helpers').helpersApi)
|
| 15 |
+
|
| 16 |
+
const route = new Router()
|
| 17 |
+
const routes = require('./routes')
|
| 18 |
+
|
| 19 |
+
// helper untuk format memory
|
| 20 |
+
function formatBytes(bytes) {
|
| 21 |
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
| 22 |
+
if (bytes === 0) return '0 B'
|
| 23 |
+
const i = Math.floor(Math.log(bytes) / Math.log(1024))
|
| 24 |
+
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`
|
| 25 |
+
}
|
| 26 |
+
function formatUptime(seconds) {
|
| 27 |
+
const days = Math.floor(seconds / (24 * 3600));
|
| 28 |
+
seconds %= 24 * 3600;
|
| 29 |
+
const hours = Math.floor(seconds / 3600);
|
| 30 |
+
seconds %= 3600;
|
| 31 |
+
const minutes = Math.floor(seconds / 60);
|
| 32 |
+
seconds = Math.floor(seconds % 60);
|
| 33 |
+
|
| 34 |
+
const parts = [];
|
| 35 |
+
if (days) parts.push(`${days} day${days > 1 ? 's' : ''}`);
|
| 36 |
+
if (hours) parts.push(`${hours} hour${hours > 1 ? 's' : ''}`);
|
| 37 |
+
if (minutes) parts.push(`${minutes} minute${minutes > 1 ? 's' : ''}`);
|
| 38 |
+
if (seconds || parts.length === 0) parts.push(`${seconds} second${seconds > 1 ? 's' : ''}`);
|
| 39 |
+
|
| 40 |
+
return parts.join(' ');
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
route.get(['/', '/ping'], async (ctx) => {
|
| 45 |
+
const totalMem = os.totalmem()
|
| 46 |
+
const freeMem = os.freemem()
|
| 47 |
+
const usedMem = totalMem - freeMem
|
| 48 |
+
const usagePercent = ((usedMem / totalMem) * 100).toFixed(2)
|
| 49 |
+
|
| 50 |
+
const info = {
|
| 51 |
+
hostname: os.hostname(),
|
| 52 |
+
platform: os.platform(),
|
| 53 |
+
arch: os.arch(),
|
| 54 |
+
uptime: formatUptime(os.uptime()),
|
| 55 |
+
cpu: {
|
| 56 |
+
model: os.cpus()[0].model,
|
| 57 |
+
cores: os.cpus().length
|
| 58 |
+
},
|
| 59 |
+
memory: {
|
| 60 |
+
total: formatBytes(totalMem),
|
| 61 |
+
used: formatBytes(usedMem),
|
| 62 |
+
free: formatBytes(freeMem),
|
| 63 |
+
usage: `${usagePercent}%`
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
ctx.type = 'application/json'
|
| 68 |
+
ctx.body = JSON.stringify(info, null, 2) // JSON formatted with 2 spaces
|
| 69 |
+
})
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
route.use('/*', routes.routeApi.routes())
|
| 73 |
+
app.use(route.routes())
|
| 74 |
+
|
| 75 |
+
const port = process.env.PORT || 7860
|
| 76 |
+
app.listen(port, () => {
|
| 77 |
+
console.log('Listening on localhost, port', port)
|
| 78 |
+
})
|
| 79 |
+
|
| 80 |
+
|
assets/fonts/.gitkeep
ADDED
|
File without changes
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
api:
|
| 5 |
+
build:
|
| 6 |
+
context: .
|
| 7 |
+
env_file: .env
|
| 8 |
+
restart: always
|
| 9 |
+
logging:
|
| 10 |
+
driver: "json-file"
|
| 11 |
+
options:
|
| 12 |
+
max-size: "10m"
|
| 13 |
+
max-file: "3"
|
| 14 |
+
networks:
|
| 15 |
+
- quotly
|
| 16 |
+
command: node index.js
|
| 17 |
+
ports:
|
| 18 |
+
- 127.0.0.1:4888:4888
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
networks:
|
| 22 |
+
quotly:
|
| 23 |
+
external: true
|
ecosystem.config.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
+
apps: [{
|
| 3 |
+
name: 'quote-api',
|
| 4 |
+
script: './index.js',
|
| 5 |
+
max_memory_restart: '2500M',
|
| 6 |
+
instances: 3,
|
| 7 |
+
exec_mode: 'cluster',
|
| 8 |
+
watch: true,
|
| 9 |
+
ignore_watch: ['node_modules', 'assets'],
|
| 10 |
+
env: {
|
| 11 |
+
NODE_ENV: 'development'
|
| 12 |
+
},
|
| 13 |
+
env_production: {
|
| 14 |
+
NODE_ENV: 'production'
|
| 15 |
+
}
|
| 16 |
+
}]
|
| 17 |
+
}
|
helpers/api.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = async (ctx, next) => {
|
| 2 |
+
ctx.props = Object.assign(ctx.query || {}, ctx.request.body || {})
|
| 3 |
+
|
| 4 |
+
try {
|
| 5 |
+
await next()
|
| 6 |
+
|
| 7 |
+
if (!ctx.body) {
|
| 8 |
+
ctx.assert(ctx.result, 404, 'Not Found')
|
| 9 |
+
|
| 10 |
+
if (ctx.result.error) {
|
| 11 |
+
ctx.status = 400
|
| 12 |
+
ctx.body = {
|
| 13 |
+
ok: false,
|
| 14 |
+
error: {
|
| 15 |
+
code: 400,
|
| 16 |
+
message: ctx.result.error
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
} else {
|
| 20 |
+
if (ctx.result.ext) {
|
| 21 |
+
if (ctx.result.ext === 'webp') ctx.response.set('content-type', 'image/webp')
|
| 22 |
+
if (ctx.result.ext === 'png') ctx.response.set('content-type', 'image/png')
|
| 23 |
+
ctx.response.set('quote-type', ctx.result.type)
|
| 24 |
+
ctx.response.set('quote-width', ctx.result.width)
|
| 25 |
+
ctx.response.set('quote-height', ctx.result.height)
|
| 26 |
+
ctx.body = ctx.result.image
|
| 27 |
+
} else {
|
| 28 |
+
ctx.body = {
|
| 29 |
+
ok: true,
|
| 30 |
+
result: ctx.result
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
} catch (error) {
|
| 36 |
+
console.error(error)
|
| 37 |
+
ctx.status = error.statusCode || error.status || 500
|
| 38 |
+
ctx.body = {
|
| 39 |
+
ok: false,
|
| 40 |
+
error: {
|
| 41 |
+
code: ctx.status,
|
| 42 |
+
message: error.message,
|
| 43 |
+
description: error.description
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
}
|
helpers/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const helpersApi = require('./api')
|
| 2 |
+
|
| 3 |
+
module.exports = {
|
| 4 |
+
helpersApi
|
| 5 |
+
}
|
index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
require('dotenv').config({ path: './.env' })
|
| 2 |
+
require('./app')
|
methods/generate.js
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const {
|
| 2 |
+
QuoteGenerate
|
| 3 |
+
} = require('../utils')
|
| 4 |
+
const { createCanvas, loadImage } = require('canvas')
|
| 5 |
+
const sharp = require('sharp')
|
| 6 |
+
|
| 7 |
+
const normalizeColor = (color) => {
|
| 8 |
+
const canvas = createCanvas(0, 0)
|
| 9 |
+
const canvasCtx = canvas.getContext('2d')
|
| 10 |
+
|
| 11 |
+
canvasCtx.fillStyle = color
|
| 12 |
+
color = canvasCtx.fillStyle
|
| 13 |
+
|
| 14 |
+
return color
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
const colorLuminance = (hex, lum) => {
|
| 18 |
+
hex = String(hex).replace(/[^0-9a-f]/gi, '')
|
| 19 |
+
if (hex.length < 6) {
|
| 20 |
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
|
| 21 |
+
}
|
| 22 |
+
lum = lum || 0
|
| 23 |
+
|
| 24 |
+
// convert to decimal and change luminosity
|
| 25 |
+
let rgb = '#'
|
| 26 |
+
let c
|
| 27 |
+
let i
|
| 28 |
+
for (i = 0; i < 3; i++) {
|
| 29 |
+
c = parseInt(hex.substr(i * 2, 2), 16)
|
| 30 |
+
c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16)
|
| 31 |
+
rgb += ('00' + c).substr(c.length)
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
return rgb
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
const imageAlpha = (image, alpha) => {
|
| 38 |
+
const canvas = createCanvas(image.width, image.height)
|
| 39 |
+
|
| 40 |
+
const canvasCtx = canvas.getContext('2d')
|
| 41 |
+
|
| 42 |
+
canvasCtx.globalAlpha = alpha
|
| 43 |
+
|
| 44 |
+
canvasCtx.drawImage(image, 0, 0)
|
| 45 |
+
|
| 46 |
+
return canvas
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
module.exports = async (parm) => {
|
| 50 |
+
// console.log(JSON.stringify(parm, null, 2))
|
| 51 |
+
if (!parm) return { error: 'query_empty' }
|
| 52 |
+
if (!parm.messages || parm.messages.length < 1) return { error: 'messages_empty' }
|
| 53 |
+
|
| 54 |
+
let botToken = parm.botToken || process.env.BOT_TOKEN
|
| 55 |
+
|
| 56 |
+
const quoteGenerate = new QuoteGenerate(botToken)
|
| 57 |
+
|
| 58 |
+
const quoteImages = []
|
| 59 |
+
|
| 60 |
+
let backgroundColor = parm.backgroundColor || '//#292232'
|
| 61 |
+
let backgroundColorOne
|
| 62 |
+
let backgroundColorTwo
|
| 63 |
+
|
| 64 |
+
const backgroundColorSplit = backgroundColor.split('/')
|
| 65 |
+
|
| 66 |
+
if (backgroundColorSplit && backgroundColorSplit.length > 1 && backgroundColorSplit[0] !== '') {
|
| 67 |
+
backgroundColorOne = normalizeColor(backgroundColorSplit[0])
|
| 68 |
+
backgroundColorTwo = normalizeColor(backgroundColorSplit[1])
|
| 69 |
+
} else if (backgroundColor.startsWith('//')) {
|
| 70 |
+
backgroundColor = normalizeColor(backgroundColor.replace('//', ''))
|
| 71 |
+
backgroundColorOne = colorLuminance(backgroundColor, 0.35)
|
| 72 |
+
backgroundColorTwo = colorLuminance(backgroundColor, -0.15)
|
| 73 |
+
} else {
|
| 74 |
+
backgroundColor = normalizeColor(backgroundColor)
|
| 75 |
+
backgroundColorOne = backgroundColor
|
| 76 |
+
backgroundColorTwo = backgroundColor
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
for (const key in parm.messages) {
|
| 80 |
+
const message = parm.messages[key]
|
| 81 |
+
|
| 82 |
+
if (message) {
|
| 83 |
+
// Ensure message has the required structure to prevent errors
|
| 84 |
+
if (!message.from) {
|
| 85 |
+
message.from = { id: 0 }
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// Ensure from object has photo property
|
| 89 |
+
if (!message.from.photo) {
|
| 90 |
+
message.from.photo = {}
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// Make sure name exists in from object
|
| 94 |
+
if (!message.from.name && (message.from.first_name || message.from.last_name)) {
|
| 95 |
+
message.from.name = [message.from.first_name, message.from.last_name]
|
| 96 |
+
.filter(Boolean)
|
| 97 |
+
.join(' ')
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// Ensure reply message has required structure to prevent errors
|
| 101 |
+
if (message.replyMessage) {
|
| 102 |
+
// Initialize chatId if missing - required for replyNameIndex calculation
|
| 103 |
+
if (!message.replyMessage.chatId) {
|
| 104 |
+
message.replyMessage.chatId = message.from?.id || 0
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// Ensure entities array exists
|
| 108 |
+
if (!message.replyMessage.entities) {
|
| 109 |
+
message.replyMessage.entities = []
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// Ensure the reply message has a from property if needed
|
| 113 |
+
if (!message.replyMessage.from) {
|
| 114 |
+
message.replyMessage.from = {
|
| 115 |
+
name: message.replyMessage.name,
|
| 116 |
+
photo: {}
|
| 117 |
+
}
|
| 118 |
+
} else if (!message.replyMessage.from.photo) {
|
| 119 |
+
message.replyMessage.from.photo = {}
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
try {
|
| 124 |
+
const canvasQuote = await quoteGenerate.generate(
|
| 125 |
+
backgroundColorOne,
|
| 126 |
+
backgroundColorTwo,
|
| 127 |
+
message,
|
| 128 |
+
parm.width,
|
| 129 |
+
parm.height,
|
| 130 |
+
parseFloat(parm.scale) || 2, // Default scale to 2 if not provided
|
| 131 |
+
parm.emojiBrand || 'apple' // Default emoji brand to apple if not provided
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
if (canvasQuote) {
|
| 135 |
+
quoteImages.push(canvasQuote)
|
| 136 |
+
} else {
|
| 137 |
+
console.warn('Failed to generate quote for message, skipping')
|
| 138 |
+
}
|
| 139 |
+
} catch (error) {
|
| 140 |
+
console.error('Error generating quote for message:', error.message)
|
| 141 |
+
// Continue with next message instead of crashing
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
if (quoteImages.length === 0) {
|
| 147 |
+
return {
|
| 148 |
+
error: 'empty_messages'
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
let canvasQuote
|
| 153 |
+
|
| 154 |
+
if (quoteImages.length > 1) {
|
| 155 |
+
let width = 0
|
| 156 |
+
let height = 0
|
| 157 |
+
|
| 158 |
+
for (let index = 0; index < quoteImages.length; index++) {
|
| 159 |
+
if (quoteImages[index].width > width) width = quoteImages[index].width
|
| 160 |
+
height += quoteImages[index].height
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
const quoteMargin = 5 * parm.scale
|
| 164 |
+
|
| 165 |
+
const canvas = createCanvas(width, height + (quoteMargin * quoteImages.length))
|
| 166 |
+
const canvasCtx = canvas.getContext('2d')
|
| 167 |
+
|
| 168 |
+
let imageY = 0
|
| 169 |
+
|
| 170 |
+
for (let index = 0; index < quoteImages.length; index++) {
|
| 171 |
+
canvasCtx.drawImage(quoteImages[index], 0, imageY)
|
| 172 |
+
imageY += quoteImages[index].height + quoteMargin
|
| 173 |
+
}
|
| 174 |
+
canvasQuote = canvas
|
| 175 |
+
} else {
|
| 176 |
+
canvasQuote = quoteImages[0]
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
let quoteImage
|
| 180 |
+
|
| 181 |
+
let { type, format, ext } = parm
|
| 182 |
+
|
| 183 |
+
if (!type && ext) type = 'png'
|
| 184 |
+
if (type !== 'image' && type !== 'stories' && canvasQuote.height > 1024 * 2) type = 'png'
|
| 185 |
+
|
| 186 |
+
if (type === 'quote') {
|
| 187 |
+
const downPadding = 75
|
| 188 |
+
const maxWidth = 512
|
| 189 |
+
const maxHeight = 512
|
| 190 |
+
|
| 191 |
+
const imageQuoteSharp = sharp(canvasQuote.toBuffer())
|
| 192 |
+
|
| 193 |
+
if (canvasQuote.height > canvasQuote.width) imageQuoteSharp.resize({ height: maxHeight })
|
| 194 |
+
else imageQuoteSharp.resize({ width: maxWidth })
|
| 195 |
+
|
| 196 |
+
const canvasImage = await loadImage(await imageQuoteSharp.toBuffer())
|
| 197 |
+
|
| 198 |
+
const canvasPadding = createCanvas(canvasImage.width, canvasImage.height + downPadding)
|
| 199 |
+
const canvasPaddingCtx = canvasPadding.getContext('2d')
|
| 200 |
+
|
| 201 |
+
canvasPaddingCtx.drawImage(canvasImage, 0, 0)
|
| 202 |
+
|
| 203 |
+
const imageSharp = sharp(canvasPadding.toBuffer())
|
| 204 |
+
|
| 205 |
+
if (canvasPadding.height >= canvasPadding.width) imageSharp.resize({ height: maxHeight })
|
| 206 |
+
else imageSharp.resize({ width: maxWidth })
|
| 207 |
+
|
| 208 |
+
if (format === 'png') quoteImage = await imageSharp.png().toBuffer()
|
| 209 |
+
else quoteImage = await imageSharp.webp({ lossless: true, force: true }).toBuffer()
|
| 210 |
+
} else if (type === 'image') {
|
| 211 |
+
const heightPadding = 75 * parm.scale
|
| 212 |
+
const widthPadding = 95 * parm.scale
|
| 213 |
+
|
| 214 |
+
const canvasImage = await loadImage(canvasQuote.toBuffer())
|
| 215 |
+
|
| 216 |
+
const canvasPic = createCanvas(canvasImage.width + widthPadding, canvasImage.height + heightPadding)
|
| 217 |
+
const canvasPicCtx = canvasPic.getContext('2d')
|
| 218 |
+
|
| 219 |
+
// radial gradient background (top left)
|
| 220 |
+
const gradient = canvasPicCtx.createRadialGradient(
|
| 221 |
+
canvasPic.width / 2,
|
| 222 |
+
canvasPic.height / 2,
|
| 223 |
+
0,
|
| 224 |
+
canvasPic.width / 2,
|
| 225 |
+
canvasPic.height / 2,
|
| 226 |
+
canvasPic.width / 2
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
const patternColorOne = colorLuminance(backgroundColorTwo, 0.15)
|
| 230 |
+
const patternColorTwo = colorLuminance(backgroundColorOne, 0.15)
|
| 231 |
+
|
| 232 |
+
gradient.addColorStop(0, patternColorOne)
|
| 233 |
+
gradient.addColorStop(1, patternColorTwo)
|
| 234 |
+
|
| 235 |
+
canvasPicCtx.fillStyle = gradient
|
| 236 |
+
canvasPicCtx.fillRect(0, 0, canvasPic.width, canvasPic.height)
|
| 237 |
+
|
| 238 |
+
const canvasPatternImage = await loadImage('./assets/pattern_02.png')
|
| 239 |
+
// const canvasPatternImage = await loadImage('./assets/pattern_ny.png');
|
| 240 |
+
|
| 241 |
+
const pattern = canvasPicCtx.createPattern(imageAlpha(canvasPatternImage, 0.3), 'repeat')
|
| 242 |
+
|
| 243 |
+
canvasPicCtx.fillStyle = pattern
|
| 244 |
+
canvasPicCtx.fillRect(0, 0, canvasPic.width, canvasPic.height)
|
| 245 |
+
|
| 246 |
+
// Add shadow effect to the canvas image
|
| 247 |
+
canvasPicCtx.shadowOffsetX = 8
|
| 248 |
+
canvasPicCtx.shadowOffsetY = 8
|
| 249 |
+
canvasPicCtx.shadowBlur = 13
|
| 250 |
+
canvasPicCtx.shadowColor = 'rgba(0, 0, 0, 0.5)'
|
| 251 |
+
|
| 252 |
+
// Draw the image to the canvas with padding centered
|
| 253 |
+
canvasPicCtx.drawImage(canvasImage, widthPadding / 2, heightPadding / 2)
|
| 254 |
+
|
| 255 |
+
canvasPicCtx.shadowOffsetX = 0
|
| 256 |
+
canvasPicCtx.shadowOffsetY = 0
|
| 257 |
+
canvasPicCtx.shadowBlur = 0
|
| 258 |
+
canvasPicCtx.shadowColor = 'rgba(0, 0, 0, 0)'
|
| 259 |
+
|
| 260 |
+
// write text button right
|
| 261 |
+
canvasPicCtx.fillStyle = `rgba(0, 0, 0, 0.3)`
|
| 262 |
+
canvasPicCtx.font = `${8 * parm.scale}px Noto Sans`
|
| 263 |
+
canvasPicCtx.textAlign = 'right'
|
| 264 |
+
canvasPicCtx.fillText('@QuotLyBot', canvasPic.width - 25, canvasPic.height - 25)
|
| 265 |
+
|
| 266 |
+
quoteImage = await sharp(canvasPic.toBuffer()).png({ lossless: true, force: true }).toBuffer()
|
| 267 |
+
} else if (type === 'stories') {
|
| 268 |
+
const canvasPic = createCanvas(720, 1280)
|
| 269 |
+
const canvasPicCtx = canvasPic.getContext('2d')
|
| 270 |
+
|
| 271 |
+
// radial gradient background (top left)
|
| 272 |
+
const gradient = canvasPicCtx.createRadialGradient(
|
| 273 |
+
canvasPic.width / 2,
|
| 274 |
+
canvasPic.height / 2,
|
| 275 |
+
0,
|
| 276 |
+
canvasPic.width / 2,
|
| 277 |
+
canvasPic.height / 2,
|
| 278 |
+
canvasPic.width / 2
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
const patternColorOne = colorLuminance(backgroundColorTwo, 0.25)
|
| 282 |
+
const patternColorTwo = colorLuminance(backgroundColorOne, 0.15)
|
| 283 |
+
|
| 284 |
+
gradient.addColorStop(0, patternColorOne)
|
| 285 |
+
gradient.addColorStop(1, patternColorTwo)
|
| 286 |
+
|
| 287 |
+
canvasPicCtx.fillStyle = gradient
|
| 288 |
+
canvasPicCtx.fillRect(0, 0, canvasPic.width, canvasPic.height)
|
| 289 |
+
|
| 290 |
+
const canvasPatternImage = await loadImage('./assets/pattern_02.png')
|
| 291 |
+
|
| 292 |
+
const pattern = canvasPicCtx.createPattern(imageAlpha(canvasPatternImage, 0.3), 'repeat')
|
| 293 |
+
|
| 294 |
+
canvasPicCtx.fillStyle = pattern
|
| 295 |
+
canvasPicCtx.fillRect(0, 0, canvasPic.width, canvasPic.height)
|
| 296 |
+
|
| 297 |
+
// Add shadow effect to the canvas image
|
| 298 |
+
canvasPicCtx.shadowOffsetX = 8
|
| 299 |
+
canvasPicCtx.shadowOffsetY = 8
|
| 300 |
+
canvasPicCtx.shadowBlur = 13
|
| 301 |
+
canvasPicCtx.shadowColor = 'rgba(0, 0, 0, 0.5)'
|
| 302 |
+
|
| 303 |
+
let canvasImage = await loadImage(canvasQuote.toBuffer())
|
| 304 |
+
|
| 305 |
+
// мінімальний відступ від країв картинки
|
| 306 |
+
const minPadding = 110
|
| 307 |
+
|
| 308 |
+
// resize canvasImage if it is larger than canvasPic + minPadding
|
| 309 |
+
if (canvasImage.width > canvasPic.width - minPadding * 2 || canvasImage.height > canvasPic.height - minPadding * 2) {
|
| 310 |
+
canvasImage = await sharp(canvasQuote.toBuffer()).resize({
|
| 311 |
+
width: canvasPic.width - minPadding * 2,
|
| 312 |
+
height: canvasPic.height - minPadding * 2,
|
| 313 |
+
fit: 'contain',
|
| 314 |
+
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
| 315 |
+
}).toBuffer()
|
| 316 |
+
|
| 317 |
+
canvasImage = await loadImage(canvasImage)
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
// розмістити canvasImage в центрі по горизонталі і вертикалі
|
| 321 |
+
const imageX = (canvasPic.width - canvasImage.width) / 2
|
| 322 |
+
const imageY = (canvasPic.height - canvasImage.height) / 2
|
| 323 |
+
|
| 324 |
+
canvasPicCtx.drawImage(canvasImage, imageX, imageY)
|
| 325 |
+
|
| 326 |
+
canvasPicCtx.shadowOffsetX = 0
|
| 327 |
+
canvasPicCtx.shadowOffsetY = 0
|
| 328 |
+
canvasPicCtx.shadowBlur = 0
|
| 329 |
+
|
| 330 |
+
// write text vertical left center text
|
| 331 |
+
canvasPicCtx.fillStyle = `rgba(0, 0, 0, 0.4)`
|
| 332 |
+
canvasPicCtx.font = `${16 * parm.scale}px Noto Sans`
|
| 333 |
+
canvasPicCtx.textAlign = 'center'
|
| 334 |
+
canvasPicCtx.translate(70, canvasPic.height / 2)
|
| 335 |
+
canvasPicCtx.rotate(-Math.PI / 2)
|
| 336 |
+
canvasPicCtx.fillText('@QuotLyBot', 0, 0)
|
| 337 |
+
|
| 338 |
+
quoteImage = await sharp(canvasPic.toBuffer()).png({ lossless: true, force: true }).toBuffer()
|
| 339 |
+
} else {
|
| 340 |
+
quoteImage = canvasQuote.toBuffer()
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
const imageMetadata = await sharp(quoteImage).metadata()
|
| 344 |
+
|
| 345 |
+
const width = imageMetadata.width
|
| 346 |
+
const height = imageMetadata.height
|
| 347 |
+
|
| 348 |
+
let image
|
| 349 |
+
if (ext) image = quoteImage
|
| 350 |
+
else image = quoteImage.toString('base64')
|
| 351 |
+
|
| 352 |
+
return {
|
| 353 |
+
image,
|
| 354 |
+
type,
|
| 355 |
+
width,
|
| 356 |
+
height,
|
| 357 |
+
ext
|
| 358 |
+
}
|
| 359 |
+
}
|
methods/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const crypto = require('crypto')
|
| 2 |
+
const LRU = require('lru-cache')
|
| 3 |
+
const sizeof = require('object-sizeof')
|
| 4 |
+
|
| 5 |
+
const generate = require('./generate')
|
| 6 |
+
|
| 7 |
+
const methods = {
|
| 8 |
+
generate
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const cache = new LRU({
|
| 12 |
+
max: 1000 * 1000 * 1000,
|
| 13 |
+
length: (n) => { return sizeof(n) },
|
| 14 |
+
maxAge: 1000 * 60 * 45
|
| 15 |
+
})
|
| 16 |
+
|
| 17 |
+
module.exports = async (method, parm) => {
|
| 18 |
+
if (methods[method]) {
|
| 19 |
+
let methodResult = {}
|
| 20 |
+
|
| 21 |
+
let cacheString = crypto.createHash('md5').update(JSON.stringify({ method, parm })).digest('hex')
|
| 22 |
+
const methodResultCache = cache.get(cacheString)
|
| 23 |
+
|
| 24 |
+
if (!methodResultCache) {
|
| 25 |
+
methodResult = await methods[method](parm)
|
| 26 |
+
|
| 27 |
+
if (!methodResult.error) cache.set(cacheString, methodResult)
|
| 28 |
+
} else {
|
| 29 |
+
methodResult = methodResultCache
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
return methodResult
|
| 33 |
+
} else {
|
| 34 |
+
return {
|
| 35 |
+
error: 'method not found'
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
}
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "quote-api",
|
| 3 |
+
"version": "0.14.0",
|
| 4 |
+
"description": "",
|
| 5 |
+
"main": "index.js",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"start": "node index.js",
|
| 8 |
+
"lint": "node_modules/.bin/eslint --ext js .",
|
| 9 |
+
"lint:fix": "node_modules/.bin/eslint --fix --ext js ."
|
| 10 |
+
},
|
| 11 |
+
"repository": {
|
| 12 |
+
"type": "git",
|
| 13 |
+
"url": "git+https://github.com/LyoSU/quote-api.git"
|
| 14 |
+
},
|
| 15 |
+
"author": "LyoSU",
|
| 16 |
+
"license": "MIT",
|
| 17 |
+
"bugs": {
|
| 18 |
+
"url": "https://github.com/LyoSU/quote-api/issues"
|
| 19 |
+
},
|
| 20 |
+
"homepage": "https://github.com/LyoSU/quote-api#readme",
|
| 21 |
+
"dependencies": {
|
| 22 |
+
"canvas": "^2.8.0",
|
| 23 |
+
"dotenv": "^7.0.0",
|
| 24 |
+
"emoji-db": "^15.1.2",
|
| 25 |
+
"jimp": "^1.6.0",
|
| 26 |
+
"jsdom": "^16.5.3",
|
| 27 |
+
"koa": "^2.11.0",
|
| 28 |
+
"koa-bodyparser": "^4.2.1",
|
| 29 |
+
"koa-logger": "^3.2.1",
|
| 30 |
+
"koa-ratelimit": "^4.2.1",
|
| 31 |
+
"koa-response-time": "^2.1.0",
|
| 32 |
+
"koa-router": "^7.4.0",
|
| 33 |
+
"lru-cache": "^5.1.1",
|
| 34 |
+
"object-sizeof": "^1.6.0",
|
| 35 |
+
"runes": "^0.4.3",
|
| 36 |
+
"sharp": "^0.32.5",
|
| 37 |
+
"smartcrop-sharp": "^2.0.7",
|
| 38 |
+
"telegraf": "^3.38.0"
|
| 39 |
+
},
|
| 40 |
+
"devDependencies": {
|
| 41 |
+
"eslint": "^8.6.0",
|
| 42 |
+
"eslint-config-standard": "^12.0.0",
|
| 43 |
+
"eslint-plugin-import": "^2.17.3",
|
| 44 |
+
"eslint-plugin-node": "^9.1.0",
|
| 45 |
+
"eslint-plugin-promise": "^4.1.1",
|
| 46 |
+
"eslint-plugin-standard": "^4.0.0"
|
| 47 |
+
}
|
| 48 |
+
}
|
routes/api.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const Router = require('koa-router')
|
| 2 |
+
const api = new Router()
|
| 3 |
+
|
| 4 |
+
const method = require('../methods')
|
| 5 |
+
|
| 6 |
+
const apiHandle = async (ctx) => {
|
| 7 |
+
const methodWithExt = ctx.params[0].match(/(.*).(png|webp)/)
|
| 8 |
+
if (methodWithExt) ctx.props.ext = methodWithExt[2]
|
| 9 |
+
ctx.result = await method(methodWithExt ? methodWithExt[1] : ctx.params[0], ctx.props)
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
api.post('/', apiHandle)
|
| 13 |
+
|
| 14 |
+
module.exports = api
|
routes/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const routeApi = require('./api')
|
| 2 |
+
|
| 3 |
+
module.exports = {
|
| 4 |
+
routeApi
|
| 5 |
+
}
|
utils/emoji-image.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const path = require('path')
|
| 2 |
+
const fs = require('fs')
|
| 3 |
+
const loadImageFromUrl = require('./image-load-url')
|
| 4 |
+
const EmojiDbLib = require('emoji-db')
|
| 5 |
+
const promiseAllStepN = require('./promise-concurrent')
|
| 6 |
+
|
| 7 |
+
const emojiDb = new EmojiDbLib({ useDefaultDb: true })
|
| 8 |
+
|
| 9 |
+
const emojiJFilesDir = '../assets/emoji/'
|
| 10 |
+
|
| 11 |
+
const brandFoledIds = {
|
| 12 |
+
apple: 325,
|
| 13 |
+
google: 313,
|
| 14 |
+
twitter: 322,
|
| 15 |
+
joypixels: 340,
|
| 16 |
+
blob: 56
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const emojiJsonByBrand = {
|
| 20 |
+
apple: 'emoji-apple-image.json',
|
| 21 |
+
google: 'emoji-google-image.json',
|
| 22 |
+
twitter: 'emoji-twitter-image.json',
|
| 23 |
+
joypixels: 'emoji-joypixels-image.json',
|
| 24 |
+
blob: 'emoji-blob-image.json'
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
let emojiImageByBrand = {
|
| 28 |
+
apple: [],
|
| 29 |
+
google: [],
|
| 30 |
+
twitter: [],
|
| 31 |
+
joypixels: [],
|
| 32 |
+
blob: []
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
async function downloadEmoji (brand) {
|
| 36 |
+
console.log('emoji image load start')
|
| 37 |
+
|
| 38 |
+
const emojiImage = emojiImageByBrand[brand]
|
| 39 |
+
|
| 40 |
+
const emojiJsonFile = path.resolve(
|
| 41 |
+
__dirname,
|
| 42 |
+
emojiJFilesDir + emojiJsonByBrand[brand]
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
const dbData = emojiDb.dbData
|
| 46 |
+
const dbArray = Object.keys(dbData)
|
| 47 |
+
const emojiPromiseArray = []
|
| 48 |
+
|
| 49 |
+
for (const key of dbArray) {
|
| 50 |
+
const emoji = dbData[key]
|
| 51 |
+
|
| 52 |
+
if (!emoji.qualified && !emojiImage[key]) {
|
| 53 |
+
emojiPromiseArray.push(async () => {
|
| 54 |
+
let brandFolderName = brand
|
| 55 |
+
if (brand === 'blob') brandFolderName = 'google'
|
| 56 |
+
|
| 57 |
+
const fileUrl = `${process.env.EMOJI_DOMAIN}/thumbs/60/${brandFolderName}/${brandFoledIds[brand]}/${emoji.image.file_name}`
|
| 58 |
+
|
| 59 |
+
const img = await loadImageFromUrl(fileUrl, (headers) => {
|
| 60 |
+
return !headers['content-type'].match(/image/)
|
| 61 |
+
})
|
| 62 |
+
|
| 63 |
+
const base64 = img.toString('base64')
|
| 64 |
+
|
| 65 |
+
if (base64) {
|
| 66 |
+
return {
|
| 67 |
+
key,
|
| 68 |
+
base64
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
})
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
const donwloadResult = await promiseAllStepN(200)(emojiPromiseArray)
|
| 76 |
+
|
| 77 |
+
for (const emojiData of donwloadResult) {
|
| 78 |
+
if (emojiData) emojiImage[emojiData.key] = emojiData.base64
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
if (Object.keys(emojiImage).length > 0) {
|
| 82 |
+
const emojiJson = JSON.stringify(emojiImage, null, 2)
|
| 83 |
+
|
| 84 |
+
fs.writeFile(emojiJsonFile, emojiJson, (err) => {
|
| 85 |
+
if (err) return console.log(err)
|
| 86 |
+
})
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
console.log('emoji image load end')
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
for (const brand in emojiJsonByBrand) {
|
| 93 |
+
const emojiJsonFile = path.resolve(
|
| 94 |
+
__dirname,
|
| 95 |
+
emojiJFilesDir + emojiJsonByBrand[brand]
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
try {
|
| 99 |
+
if (fs.existsSync(emojiJsonFile)) emojiImageByBrand[brand] = require(emojiJsonFile)
|
| 100 |
+
} catch (error) {
|
| 101 |
+
console.log(error)
|
| 102 |
+
}
|
| 103 |
+
// downloadEmoji(brand)
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
module.exports = emojiImageByBrand
|
utils/image-load-path.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const fs = require('fs')
|
| 2 |
+
|
| 3 |
+
module.exports = (path) => {
|
| 4 |
+
return new Promise((resolve, reject) => {
|
| 5 |
+
fs.readFile(path, (_error, data) => {
|
| 6 |
+
resolve(data)
|
| 7 |
+
})
|
| 8 |
+
})
|
| 9 |
+
}
|
utils/image-load-url.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const https = require('https')
|
| 2 |
+
|
| 3 |
+
module.exports = (url, filter = false) => {
|
| 4 |
+
return new Promise((resolve, reject) => {
|
| 5 |
+
const options = new URL(url)
|
| 6 |
+
options.headers = {
|
| 7 |
+
'User-Agent': 'curl/8.4.0'
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
https.get(options, (res) => {
|
| 11 |
+
if (filter && filter(res.headers)) {
|
| 12 |
+
resolve(Buffer.concat([]))
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
const chunks = []
|
| 16 |
+
|
| 17 |
+
res.on('error', (err) => {
|
| 18 |
+
reject(err)
|
| 19 |
+
})
|
| 20 |
+
res.on('data', (chunk) => {
|
| 21 |
+
chunks.push(chunk)
|
| 22 |
+
})
|
| 23 |
+
res.on('end', () => {
|
| 24 |
+
resolve(Buffer.concat(chunks))
|
| 25 |
+
})
|
| 26 |
+
})
|
| 27 |
+
})
|
| 28 |
+
}
|
utils/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
+
QuoteGenerate: require('./quote-generate'),
|
| 3 |
+
loadImageFromUrl: require('./image-load-url'),
|
| 4 |
+
loadImageFromPath: require('./image-load-path'),
|
| 5 |
+
promiseAllStepN: require('./promise-concurrent'),
|
| 6 |
+
userName: require('./user-name')
|
| 7 |
+
}
|
utils/promise-concurrent.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
function promiseAllStepN (n, list) {
|
| 2 |
+
let tail = list.splice(n)
|
| 3 |
+
let head = list
|
| 4 |
+
let resolved = []
|
| 5 |
+
let processed = 0
|
| 6 |
+
return new Promise(resolve => {
|
| 7 |
+
head.forEach(x => {
|
| 8 |
+
let res = x()
|
| 9 |
+
resolved.push(res)
|
| 10 |
+
res.then(y => {
|
| 11 |
+
runNext()
|
| 12 |
+
return y
|
| 13 |
+
})
|
| 14 |
+
})
|
| 15 |
+
function runNext () {
|
| 16 |
+
if (processed == tail.length) {
|
| 17 |
+
resolve(Promise.all(resolved))
|
| 18 |
+
} else {
|
| 19 |
+
resolved.push(tail[processed]().then(x => {
|
| 20 |
+
runNext()
|
| 21 |
+
return x
|
| 22 |
+
}))
|
| 23 |
+
processed++
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
})
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
module.exports = n => list => promiseAllStepN(n, list)
|
utils/quote-generate.js
ADDED
|
@@ -0,0 +1,1308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const fs = require('fs')
|
| 2 |
+
const { createCanvas, registerFont } = require('canvas')
|
| 3 |
+
const EmojiDbLib = require('emoji-db')
|
| 4 |
+
const { loadImage } = require('canvas')
|
| 5 |
+
const loadImageFromUrl = require('./image-load-url')
|
| 6 |
+
const sharp = require('sharp')
|
| 7 |
+
const { Jimp, JimpMime } = require('jimp')
|
| 8 |
+
const smartcrop = require('smartcrop-sharp')
|
| 9 |
+
const runes = require('runes')
|
| 10 |
+
const zlib = require('zlib')
|
| 11 |
+
const { Telegram } = require('telegraf')
|
| 12 |
+
|
| 13 |
+
const emojiDb = new EmojiDbLib({ useDefaultDb: true })
|
| 14 |
+
|
| 15 |
+
function loadFont () {
|
| 16 |
+
console.log('font load start')
|
| 17 |
+
const fontsDir = 'assets/fonts/'
|
| 18 |
+
|
| 19 |
+
fs.readdir(fontsDir, (_err, files) => {
|
| 20 |
+
files.forEach((file) => {
|
| 21 |
+
try {
|
| 22 |
+
registerFont(`${fontsDir}${file}`, { family: file.replace(/\.[^/.]+$/, '') })
|
| 23 |
+
} catch (error) {
|
| 24 |
+
console.error(`${fontsDir}${file} not font file`)
|
| 25 |
+
}
|
| 26 |
+
})
|
| 27 |
+
})
|
| 28 |
+
|
| 29 |
+
console.log('font load end')
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
loadFont()
|
| 33 |
+
|
| 34 |
+
const emojiImageByBrand = require('./emoji-image')
|
| 35 |
+
|
| 36 |
+
const LRU = require('lru-cache')
|
| 37 |
+
|
| 38 |
+
const avatarCache = new LRU({
|
| 39 |
+
max: 20,
|
| 40 |
+
maxAge: 1000 * 60 * 5
|
| 41 |
+
})
|
| 42 |
+
|
| 43 |
+
// 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
|
| 44 |
+
|
| 45 |
+
// here are all the possible colors that will be passed as the second argument. the first color can be any
|
| 46 |
+
class ColorContrast {
|
| 47 |
+
constructor () {
|
| 48 |
+
this.brightnessThreshold = 175 // A threshold to determine when a color is considered bright or dark
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
getBrightness (color) {
|
| 52 |
+
// Calculates the brightness of a color using the formula from the WCAG 2.0
|
| 53 |
+
// See: https://www.w3.org/TR/WCAG20-TECHS/G18.html#G18-tests
|
| 54 |
+
const [r, g, b] = this.hexToRgb(color)
|
| 55 |
+
return (r * 299 + g * 587 + b * 114) / 1000
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
hexToRgb (hex) {
|
| 59 |
+
// Converts a hex color string to an RGB array
|
| 60 |
+
const r = parseInt(hex.substring(1, 3), 16)
|
| 61 |
+
const g = parseInt(hex.substring(3, 5), 16)
|
| 62 |
+
const b = parseInt(hex.substring(5, 7), 16)
|
| 63 |
+
return [r, g, b]
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
rgbToHex ([r, g, b]) {
|
| 67 |
+
// Converts an RGB array to a hex color string
|
| 68 |
+
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
adjustBrightness (color, amount) {
|
| 72 |
+
// Adjusts the brightness of a color by a specified amount
|
| 73 |
+
const [r, g, b] = this.hexToRgb(color)
|
| 74 |
+
const newR = Math.max(0, Math.min(255, r + amount))
|
| 75 |
+
const newG = Math.max(0, Math.min(255, g + amount))
|
| 76 |
+
const newB = Math.max(0, Math.min(255, b + amount))
|
| 77 |
+
return this.rgbToHex([newR, newG, newB])
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
getContrastRatio (background, foreground) {
|
| 81 |
+
// Calculates the contrast ratio between two colors using the formula from the WCAG 2.0
|
| 82 |
+
// See: https://www.w3.org/TR/WCAG20-TECHS/G18.html#G18-tests
|
| 83 |
+
const brightness1 = this.getBrightness(background)
|
| 84 |
+
const brightness2 = this.getBrightness(foreground)
|
| 85 |
+
const lightest = Math.max(brightness1, brightness2)
|
| 86 |
+
const darkest = Math.min(brightness1, brightness2)
|
| 87 |
+
return (lightest + 0.05) / (darkest + 0.05)
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
adjustContrast (background, foreground) {
|
| 91 |
+
// Adjusts the brightness of the foreground color to meet the minimum contrast ratio
|
| 92 |
+
// with the background color
|
| 93 |
+
const contrastRatio = this.getContrastRatio(background, foreground)
|
| 94 |
+
const brightnessDiff = this.getBrightness(background) - this.getBrightness(foreground)
|
| 95 |
+
if (contrastRatio >= 4.5) {
|
| 96 |
+
return foreground // The contrast ratio is already sufficient
|
| 97 |
+
} else if (brightnessDiff >= 0) {
|
| 98 |
+
// The background is brighter than the foreground
|
| 99 |
+
const amount = Math.ceil((this.brightnessThreshold - this.getBrightness(foreground)) / 2)
|
| 100 |
+
return this.adjustBrightness(foreground, amount)
|
| 101 |
+
} else {
|
| 102 |
+
// The background is darker than the foreground
|
| 103 |
+
const amount = Math.ceil((this.getBrightness(foreground) - this.brightnessThreshold) / 2)
|
| 104 |
+
return this.adjustBrightness(foreground, -amount)
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
class QuoteGenerate {
|
| 110 |
+
constructor (botToken) {
|
| 111 |
+
this.telegram = new Telegram(botToken)
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
async avatarImageLatters (letters, color) {
|
| 115 |
+
const size = 500
|
| 116 |
+
const canvas = createCanvas(size, size)
|
| 117 |
+
const context = canvas.getContext('2d')
|
| 118 |
+
|
| 119 |
+
const gradient = context.createLinearGradient(0, 0, canvas.width, canvas.height)
|
| 120 |
+
|
| 121 |
+
gradient.addColorStop(0, color[0])
|
| 122 |
+
gradient.addColorStop(1, color[1])
|
| 123 |
+
|
| 124 |
+
context.fillStyle = gradient
|
| 125 |
+
context.fillRect(0, 0, canvas.width, canvas.height)
|
| 126 |
+
|
| 127 |
+
const drawLetters = await this.drawMultilineText(
|
| 128 |
+
letters,
|
| 129 |
+
null,
|
| 130 |
+
size / 2,
|
| 131 |
+
'#FFF',
|
| 132 |
+
0,
|
| 133 |
+
size,
|
| 134 |
+
size * 5,
|
| 135 |
+
size * 5
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
context.drawImage(drawLetters, (canvas.width - drawLetters.width) / 2, (canvas.height - drawLetters.height) / 1.5)
|
| 139 |
+
|
| 140 |
+
return canvas.toBuffer()
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
async downloadAvatarImage (user) {
|
| 144 |
+
let avatarImage
|
| 145 |
+
|
| 146 |
+
let nameLatters
|
| 147 |
+
if (user.first_name && user.last_name) nameLatters = runes(user.first_name)[0] + (runes(user.last_name || '')[0])
|
| 148 |
+
else {
|
| 149 |
+
let name = user.first_name || user.name || user.title
|
| 150 |
+
name = name.toUpperCase()
|
| 151 |
+
const nameWord = name.split(' ')
|
| 152 |
+
|
| 153 |
+
if (nameWord.length > 1) nameLatters = runes(nameWord[0])[0] + runes(nameWord.splice(-1)[0])[0]
|
| 154 |
+
else nameLatters = runes(nameWord[0])[0]
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
const cacheKey = user.id
|
| 158 |
+
|
| 159 |
+
const avatarImageCache = avatarCache.get(cacheKey)
|
| 160 |
+
|
| 161 |
+
const avatarColorArray = [
|
| 162 |
+
[ '#FF885E', '#FF516A' ], // red
|
| 163 |
+
[ '#FFCD6A', '#FFA85C' ], // orange
|
| 164 |
+
[ '#E0A2F3', '#D669ED' ], // purple
|
| 165 |
+
[ '#A0DE7E', '#54CB68' ], // green
|
| 166 |
+
[ '#53EDD6', '#28C9B7' ], // sea
|
| 167 |
+
[ '#72D5FD', '#2A9EF1' ], // blue
|
| 168 |
+
[ '#FFA8A8', '#FF719A' ] // pink
|
| 169 |
+
]
|
| 170 |
+
|
| 171 |
+
const nameIndex = Math.abs(user.id) % 7
|
| 172 |
+
|
| 173 |
+
const avatarColor = avatarColorArray[nameIndex]
|
| 174 |
+
|
| 175 |
+
if (avatarImageCache) {
|
| 176 |
+
avatarImage = avatarImageCache
|
| 177 |
+
} else if (user.photo && user.photo.url) {
|
| 178 |
+
avatarImage = await loadImage(user.photo.url)
|
| 179 |
+
} else {
|
| 180 |
+
try {
|
| 181 |
+
let userPhoto, userPhotoUrl
|
| 182 |
+
|
| 183 |
+
if (user.photo && user.photo.big_file_id) userPhotoUrl = await this.telegram.getFileLink(user.photo.big_file_id).catch(() => {})
|
| 184 |
+
|
| 185 |
+
if (!userPhotoUrl) {
|
| 186 |
+
const getChat = await this.telegram.getChat(user.id).catch(() => {})
|
| 187 |
+
|
| 188 |
+
if (getChat && getChat.photo && getChat.photo.big_file_id) userPhoto = getChat.photo.big_file_id
|
| 189 |
+
|
| 190 |
+
if (userPhoto) userPhotoUrl = await this.telegram.getFileLink(userPhoto).catch(() => {})
|
| 191 |
+
|
| 192 |
+
else if (user.username) userPhotoUrl = `https://telega.one/i/userpic/320/${user.username}.jpg`
|
| 193 |
+
|
| 194 |
+
else avatarImage = await loadImage(await this.avatarImageLatters(nameLatters, avatarColor)).catch(() => {})
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
if (userPhotoUrl) {
|
| 198 |
+
const imageBuffer = await loadImageFromUrl(userPhotoUrl).catch((error) => {
|
| 199 |
+
console.warn('Failed to load user photo from URL:', error.message)
|
| 200 |
+
return null
|
| 201 |
+
})
|
| 202 |
+
|
| 203 |
+
if (imageBuffer) {
|
| 204 |
+
avatarImage = await loadImage(imageBuffer).catch((error) => {
|
| 205 |
+
console.warn('Failed to process user photo buffer:', error.message)
|
| 206 |
+
return null
|
| 207 |
+
})
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
if (avatarImage) {
|
| 212 |
+
avatarCache.set(cacheKey, avatarImage)
|
| 213 |
+
}
|
| 214 |
+
} catch (error) {
|
| 215 |
+
console.warn('Error getting user photo:', error.message)
|
| 216 |
+
avatarImage = null
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
// Fallback to letters avatar if no image was loaded
|
| 220 |
+
if (!avatarImage) {
|
| 221 |
+
try {
|
| 222 |
+
avatarImage = await loadImage(await this.avatarImageLatters(nameLatters, avatarColor))
|
| 223 |
+
avatarCache.set(cacheKey, avatarImage)
|
| 224 |
+
} catch (error) {
|
| 225 |
+
console.warn('Failed to create letters avatar:', error.message)
|
| 226 |
+
avatarImage = null
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
return avatarImage
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
ungzip (input, options) {
|
| 235 |
+
return new Promise((resolve, reject) => {
|
| 236 |
+
zlib.gunzip(input, options, (error, result) => {
|
| 237 |
+
if (!error) resolve(result)
|
| 238 |
+
else reject(Error(error))
|
| 239 |
+
})
|
| 240 |
+
})
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
async downloadMediaImage (media, mediaSize, type = 'id', crop = true) {
|
| 244 |
+
try {
|
| 245 |
+
let mediaUrl
|
| 246 |
+
if (type === 'id') mediaUrl = await this.telegram.getFileLink(media).catch(console.error)
|
| 247 |
+
else mediaUrl = media
|
| 248 |
+
|
| 249 |
+
if (!mediaUrl) {
|
| 250 |
+
console.warn('Failed to get media URL, skipping media')
|
| 251 |
+
return null
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
const load = await loadImageFromUrl(mediaUrl).catch((error) => {
|
| 255 |
+
console.warn('Failed to load image from URL:', error.message)
|
| 256 |
+
return null
|
| 257 |
+
})
|
| 258 |
+
|
| 259 |
+
if (!load) {
|
| 260 |
+
console.warn('Failed to load media, skipping')
|
| 261 |
+
return null
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
if (crop || (mediaUrl && mediaUrl.match(/.webp/))) {
|
| 265 |
+
try {
|
| 266 |
+
const imageSharp = sharp(load)
|
| 267 |
+
const imageMetadata = await imageSharp.metadata()
|
| 268 |
+
const sharpPng = await imageSharp.png({ lossless: true, force: true }).toBuffer()
|
| 269 |
+
|
| 270 |
+
if (!imageMetadata || !imageMetadata.width || !imageMetadata.height || !sharpPng) {
|
| 271 |
+
// Fallback to original image without processing
|
| 272 |
+
try {
|
| 273 |
+
return await loadImage(load)
|
| 274 |
+
} catch (fallbackError) {
|
| 275 |
+
console.warn('Failed to load original image as fallback:', fallbackError.message)
|
| 276 |
+
return null
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
let croppedImage
|
| 281 |
+
|
| 282 |
+
if (imageMetadata.format === 'webp') {
|
| 283 |
+
try {
|
| 284 |
+
const jimpImage = await Jimp.read(sharpPng)
|
| 285 |
+
croppedImage = await jimpImage.autocrop().getBuffer(JimpMime.png)
|
| 286 |
+
} catch (jimpError) {
|
| 287 |
+
console.warn('Failed to process webp with Jimp, using original:', jimpError.message)
|
| 288 |
+
croppedImage = sharpPng
|
| 289 |
+
}
|
| 290 |
+
} else {
|
| 291 |
+
try {
|
| 292 |
+
const smartcropResult = await smartcrop.crop(sharpPng, { width: mediaSize, height: imageMetadata.height })
|
| 293 |
+
const crop = smartcropResult.topCrop
|
| 294 |
+
|
| 295 |
+
croppedImage = await imageSharp.extract({ width: crop.width, height: crop.height, left: crop.x, top: crop.y }).png({ lossless: true, force: true }).toBuffer()
|
| 296 |
+
} catch (cropError) {
|
| 297 |
+
console.warn('Failed to crop image, using original:', cropError.message)
|
| 298 |
+
croppedImage = sharpPng
|
| 299 |
+
}
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
try {
|
| 303 |
+
return await loadImage(croppedImage)
|
| 304 |
+
} catch (loadError) {
|
| 305 |
+
console.warn('Failed to load processed image, trying original:', loadError.message)
|
| 306 |
+
try {
|
| 307 |
+
return await loadImage(load)
|
| 308 |
+
} catch (originalError) {
|
| 309 |
+
console.warn('Failed to load original image as final fallback:', originalError.message)
|
| 310 |
+
return null
|
| 311 |
+
}
|
| 312 |
+
}
|
| 313 |
+
} catch (sharpError) {
|
| 314 |
+
console.warn('Failed to process image with Sharp, trying original:', sharpError.message)
|
| 315 |
+
try {
|
| 316 |
+
return await loadImage(load)
|
| 317 |
+
} catch (originalError) {
|
| 318 |
+
console.warn('Failed to load original image:', originalError.message)
|
| 319 |
+
return null
|
| 320 |
+
}
|
| 321 |
+
}
|
| 322 |
+
} else {
|
| 323 |
+
try {
|
| 324 |
+
return await loadImage(load)
|
| 325 |
+
} catch (loadError) {
|
| 326 |
+
console.warn('Failed to load image:', loadError.message)
|
| 327 |
+
return null
|
| 328 |
+
}
|
| 329 |
+
}
|
| 330 |
+
} catch (error) {
|
| 331 |
+
console.error('Critical error in downloadMediaImage:', error.message)
|
| 332 |
+
return null
|
| 333 |
+
}
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
hexToRgb (hex) {
|
| 337 |
+
return hex.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i
|
| 338 |
+
, (m, r, g, b) => '#' + r + r + g + g + b + b)
|
| 339 |
+
.substring(1).match(/.{2}/g)
|
| 340 |
+
.map(x => parseInt(x, 16))
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
// https://codepen.io/andreaswik/pen/YjJqpK
|
| 344 |
+
lightOrDark (color) {
|
| 345 |
+
let r, g, b
|
| 346 |
+
|
| 347 |
+
// Check the format of the color, HEX or RGB?
|
| 348 |
+
if (color.match(/^rgb/)) {
|
| 349 |
+
// If HEX --> store the red, green, blue values in separate variables
|
| 350 |
+
color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/)
|
| 351 |
+
|
| 352 |
+
r = color[1]
|
| 353 |
+
g = color[2]
|
| 354 |
+
b = color[3]
|
| 355 |
+
} else {
|
| 356 |
+
// If RGB --> Convert it to HEX: http://gist.github.com/983661
|
| 357 |
+
color = +('0x' + color.slice(1).replace(
|
| 358 |
+
color.length < 5 && /./g, '$&$&'
|
| 359 |
+
)
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
r = color >> 16
|
| 363 |
+
g = color >> 8 & 255
|
| 364 |
+
b = color & 255
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
// HSP (Highly Sensitive Poo) equation from http://alienryderflex.com/hsp.html
|
| 368 |
+
const hsp = Math.sqrt(
|
| 369 |
+
0.299 * (r * r) +
|
| 370 |
+
0.587 * (g * g) +
|
| 371 |
+
0.114 * (b * b)
|
| 372 |
+
)
|
| 373 |
+
|
| 374 |
+
// Using the HSP value, determine whether the color is light or dark
|
| 375 |
+
if (hsp > 127.5) {
|
| 376 |
+
return 'light'
|
| 377 |
+
} else {
|
| 378 |
+
return 'dark'
|
| 379 |
+
}
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
async drawMultilineText (text, entities, fontSize, fontColor, textX, textY, maxWidth, maxHeight, emojiBrand = 'apple') {
|
| 383 |
+
if (maxWidth > 10000) maxWidth = 10000
|
| 384 |
+
if (maxHeight > 10000) maxHeight = 10000
|
| 385 |
+
|
| 386 |
+
const emojiImageJson = emojiImageByBrand[emojiBrand]
|
| 387 |
+
|
| 388 |
+
let fallbackEmojiBrand = 'apple'
|
| 389 |
+
if (emojiBrand === 'blob') fallbackEmojiBrand = 'google'
|
| 390 |
+
|
| 391 |
+
const fallbackEmojiImageJson = emojiImageByBrand[fallbackEmojiBrand]
|
| 392 |
+
|
| 393 |
+
// Pre-calculate text dimensions to avoid creating oversized canvas
|
| 394 |
+
const canvas = createCanvas(maxWidth + fontSize, maxHeight + fontSize)
|
| 395 |
+
const canvasCtx = canvas.getContext('2d')
|
| 396 |
+
|
| 397 |
+
// text = text.slice(0, 4096)
|
| 398 |
+
text = text.replace(/і/g, 'i') // замена украинской буквы і на английскую, так как она отсутствует в шрифтах Noto
|
| 399 |
+
const chars = text.split('')
|
| 400 |
+
|
| 401 |
+
const lineHeight = 4 * (fontSize * 0.3)
|
| 402 |
+
|
| 403 |
+
const styledChar = []
|
| 404 |
+
|
| 405 |
+
const emojis = emojiDb.searchFromText({ input: text, fixCodePoints: true })
|
| 406 |
+
|
| 407 |
+
// Pre-load all emojis for better performance
|
| 408 |
+
const emojiCache = new Map()
|
| 409 |
+
const emojiLoadPromises = []
|
| 410 |
+
|
| 411 |
+
for (let emojiIndex = 0; emojiIndex < emojis.length; emojiIndex++) {
|
| 412 |
+
const emoji = emojis[emojiIndex]
|
| 413 |
+
if (!emojiCache.has(emoji.found)) {
|
| 414 |
+
emojiLoadPromises.push(
|
| 415 |
+
(async () => {
|
| 416 |
+
const emojiImageBase = emojiImageJson[emoji.found]
|
| 417 |
+
if (emojiImageBase) {
|
| 418 |
+
try {
|
| 419 |
+
const image = await loadImage(Buffer.from(emojiImageBase, 'base64'))
|
| 420 |
+
emojiCache.set(emoji.found, image)
|
| 421 |
+
} catch (error) {
|
| 422 |
+
try {
|
| 423 |
+
const fallbackImage = await loadImage(Buffer.from(fallbackEmojiImageJson[emoji.found], 'base64'))
|
| 424 |
+
emojiCache.set(emoji.found, fallbackImage)
|
| 425 |
+
} catch (fallbackError) {
|
| 426 |
+
// Skip if both fail
|
| 427 |
+
}
|
| 428 |
+
}
|
| 429 |
+
} else {
|
| 430 |
+
try {
|
| 431 |
+
const fallbackImage = await loadImage(Buffer.from(fallbackEmojiImageJson[emoji.found], 'base64'))
|
| 432 |
+
emojiCache.set(emoji.found, fallbackImage)
|
| 433 |
+
} catch (error) {
|
| 434 |
+
// Skip if fails
|
| 435 |
+
}
|
| 436 |
+
}
|
| 437 |
+
})()
|
| 438 |
+
)
|
| 439 |
+
}
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
// Wait for all emojis to load
|
| 443 |
+
await Promise.all(emojiLoadPromises)
|
| 444 |
+
|
| 445 |
+
for (let charIndex = 0; charIndex < chars.length; charIndex++) {
|
| 446 |
+
const char = chars[charIndex]
|
| 447 |
+
|
| 448 |
+
styledChar[charIndex] = {
|
| 449 |
+
char,
|
| 450 |
+
style: []
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
if (entities && typeof entities === 'string') styledChar[charIndex].style.push(entities)
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
if (entities && typeof entities === 'object') {
|
| 457 |
+
for (let entityIndex = 0; entityIndex < entities.length; entityIndex++) {
|
| 458 |
+
const entity = entities[entityIndex]
|
| 459 |
+
const style = []
|
| 460 |
+
|
| 461 |
+
if (['pre', 'code', 'pre_code'].includes(entity.type)) {
|
| 462 |
+
style.push('monospace')
|
| 463 |
+
} else if (
|
| 464 |
+
['mention', 'text_mention', 'hashtag', 'email', 'phone_number', 'bot_command', 'url', 'text_link']
|
| 465 |
+
.includes(entity.type)
|
| 466 |
+
) {
|
| 467 |
+
style.push('mention')
|
| 468 |
+
} else {
|
| 469 |
+
style.push(entity.type)
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
if (entity.type === 'custom_emoji') {
|
| 473 |
+
styledChar[entity.offset].customEmojiId = entity.custom_emoji_id
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
for (let charIndex = entity.offset; charIndex < entity.offset + entity.length; charIndex++) {
|
| 477 |
+
styledChar[charIndex].style = styledChar[charIndex].style.concat(style)
|
| 478 |
+
}
|
| 479 |
+
}
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
for (let emojiIndex = 0; emojiIndex < emojis.length; emojiIndex++) {
|
| 483 |
+
const emoji = emojis[emojiIndex]
|
| 484 |
+
|
| 485 |
+
for (let charIndex = emoji.offset; charIndex < emoji.offset + emoji.length; charIndex++) {
|
| 486 |
+
styledChar[charIndex].emoji = {
|
| 487 |
+
index: emojiIndex,
|
| 488 |
+
code: emoji.found
|
| 489 |
+
}
|
| 490 |
+
}
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
const styledWords = []
|
| 494 |
+
|
| 495 |
+
let stringNum = 0
|
| 496 |
+
|
| 497 |
+
const breakMatch = /<br>|\n|\r/
|
| 498 |
+
const spaceMatch = /[\f\n\r\t\v\u0020\u1680\u2000-\u200a\u2028\u2029\u205f\u3000]/
|
| 499 |
+
const CJKMatch = /[\u1100-\u11ff\u2e80-\u2eff\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\u3100-\u312f\u3130-\u318f\u3190-\u319f\u31a0-\u31bf\u31c0-\u31ef\u31f0-\u31ff\u3200-\u32ff\u3300-\u33ff\u3400-\u4dbf\u4e00-\u9fff\uac00-\ud7af\uf900-\ufaff]/
|
| 500 |
+
|
| 501 |
+
for (let index = 0; index < styledChar.length; index++) {
|
| 502 |
+
const charStyle = styledChar[index]
|
| 503 |
+
const lastChar = styledChar[index - 1]
|
| 504 |
+
|
| 505 |
+
if (
|
| 506 |
+
lastChar && (
|
| 507 |
+
(
|
| 508 |
+
(charStyle.emoji && !lastChar.emoji) ||
|
| 509 |
+
(!charStyle.emoji && lastChar.emoji) ||
|
| 510 |
+
(charStyle.emoji && lastChar.emoji && charStyle.emoji.index !== lastChar.emoji.index)
|
| 511 |
+
) ||
|
| 512 |
+
(
|
| 513 |
+
(charStyle.char.match(breakMatch)) ||
|
| 514 |
+
(charStyle.char.match(spaceMatch) && !lastChar.char.match(spaceMatch)) ||
|
| 515 |
+
(lastChar.char.match(spaceMatch) && !charStyle.char.match(spaceMatch)) ||
|
| 516 |
+
(charStyle.style && lastChar.style && charStyle.style.toString() !== lastChar.style.toString())
|
| 517 |
+
) || (
|
| 518 |
+
charStyle.char.match(CJKMatch) ||
|
| 519 |
+
lastChar.char.match(CJKMatch)
|
| 520 |
+
)
|
| 521 |
+
)
|
| 522 |
+
) {
|
| 523 |
+
stringNum++
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
if (!styledWords[stringNum]) {
|
| 527 |
+
styledWords[stringNum] = {
|
| 528 |
+
word: charStyle.char
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
if (charStyle.style) styledWords[stringNum].style = charStyle.style
|
| 532 |
+
if (charStyle.emoji) styledWords[stringNum].emoji = charStyle.emoji
|
| 533 |
+
if (charStyle.customEmojiId) styledWords[stringNum].customEmojiId = charStyle.customEmojiId
|
| 534 |
+
} else styledWords[stringNum].word += charStyle.char
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
let lineX = textX
|
| 538 |
+
let lineY = textY
|
| 539 |
+
|
| 540 |
+
let textWidth = 0
|
| 541 |
+
|
| 542 |
+
// load custom emoji
|
| 543 |
+
const customEmojiIds = []
|
| 544 |
+
|
| 545 |
+
for (let index = 0; index < styledWords.length; index++) {
|
| 546 |
+
const word = styledWords[index]
|
| 547 |
+
|
| 548 |
+
if (word.customEmojiId) {
|
| 549 |
+
customEmojiIds.push(word.customEmojiId)
|
| 550 |
+
}
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
const getCustomEmojiStickers = await this.telegram.callApi('getCustomEmojiStickers', {
|
| 554 |
+
custom_emoji_ids: customEmojiIds
|
| 555 |
+
}).catch(() => {})
|
| 556 |
+
|
| 557 |
+
const customEmojiStickers = {}
|
| 558 |
+
|
| 559 |
+
const loadCustomEmojiStickerPromises = []
|
| 560 |
+
|
| 561 |
+
if (getCustomEmojiStickers) {
|
| 562 |
+
for (let index = 0; index < getCustomEmojiStickers.length; index++) {
|
| 563 |
+
const sticker = getCustomEmojiStickers[index]
|
| 564 |
+
|
| 565 |
+
loadCustomEmojiStickerPromises.push((async () => {
|
| 566 |
+
const getFileLink = await this.telegram.getFileLink(sticker.thumb.file_id).catch(() => {})
|
| 567 |
+
|
| 568 |
+
if (getFileLink) {
|
| 569 |
+
const load = await loadImageFromUrl(getFileLink).catch(() => {})
|
| 570 |
+
const imageSharp = sharp(load)
|
| 571 |
+
const sharpPng = await imageSharp.png({ lossless: true, force: true }).toBuffer()
|
| 572 |
+
|
| 573 |
+
customEmojiStickers[sticker.custom_emoji_id] = await loadImage(sharpPng).catch(() => {})
|
| 574 |
+
}
|
| 575 |
+
})())
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
await Promise.all(loadCustomEmojiStickerPromises).catch(() => {})
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
let breakWrite = false
|
| 582 |
+
let lineDirection = this.getLineDirection(styledWords, 0)
|
| 583 |
+
|
| 584 |
+
// Pre-set font to avoid repeated font changes
|
| 585 |
+
let currentFont = null
|
| 586 |
+
let currentFillStyle = null
|
| 587 |
+
|
| 588 |
+
for (let index = 0; index < styledWords.length; index++) {
|
| 589 |
+
const styledWord = styledWords[index]
|
| 590 |
+
|
| 591 |
+
let emojiImage
|
| 592 |
+
|
| 593 |
+
if (styledWord.emoji) {
|
| 594 |
+
if (styledWord.customEmojiId && customEmojiStickers[styledWord.customEmojiId]) {
|
| 595 |
+
emojiImage = customEmojiStickers[styledWord.customEmojiId]
|
| 596 |
+
} else {
|
| 597 |
+
// Use pre-loaded emoji from cache
|
| 598 |
+
emojiImage = emojiCache.get(styledWord.emoji.code)
|
| 599 |
+
}
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
let fontType = ''
|
| 603 |
+
let fontName = 'NotoSans'
|
| 604 |
+
let fillStyle = fontColor
|
| 605 |
+
|
| 606 |
+
if (styledWord.style.includes('bold')) {
|
| 607 |
+
fontType += 'bold '
|
| 608 |
+
}
|
| 609 |
+
if (styledWord.style.includes('italic')) {
|
| 610 |
+
fontType += 'italic '
|
| 611 |
+
}
|
| 612 |
+
if (styledWord.style.includes('monospace')) {
|
| 613 |
+
fontName = 'NotoSansMono'
|
| 614 |
+
fillStyle = '#5887a7'
|
| 615 |
+
}
|
| 616 |
+
if (styledWord.style.includes('mention')) {
|
| 617 |
+
fillStyle = '#6ab7ec'
|
| 618 |
+
}
|
| 619 |
+
if (styledWord.style.includes('spoiler')) {
|
| 620 |
+
const rbaColor = this.hexToRgb(this.normalizeColor(fontColor))
|
| 621 |
+
fillStyle = `rgba(${rbaColor[0]}, ${rbaColor[1]}, ${rbaColor[2]}, 0.15)`
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
const newFont = `${fontType} ${fontSize}px ${fontName}`
|
| 625 |
+
|
| 626 |
+
// Only change font if different from current
|
| 627 |
+
if (currentFont !== newFont) {
|
| 628 |
+
canvasCtx.font = newFont
|
| 629 |
+
currentFont = newFont
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
// Only change fill style if different from current
|
| 633 |
+
if (currentFillStyle !== fillStyle) {
|
| 634 |
+
canvasCtx.fillStyle = fillStyle
|
| 635 |
+
currentFillStyle = fillStyle
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
// Pre-truncate long words before measurement
|
| 639 |
+
let wordToMeasure = styledWord.word
|
| 640 |
+
const maxWordWidth = maxWidth - fontSize * 3
|
| 641 |
+
|
| 642 |
+
if (wordToMeasure.length > 50) { // Quick length check before expensive measurement
|
| 643 |
+
while (canvasCtx.measureText(wordToMeasure).width > maxWordWidth && wordToMeasure.length > 0) {
|
| 644 |
+
wordToMeasure = wordToMeasure.substr(0, wordToMeasure.length - 1)
|
| 645 |
+
}
|
| 646 |
+
if (wordToMeasure.length < styledWord.word.length) {
|
| 647 |
+
styledWord.word = wordToMeasure + '…'
|
| 648 |
+
}
|
| 649 |
+
} else if (canvasCtx.measureText(wordToMeasure).width > maxWordWidth) {
|
| 650 |
+
while (canvasCtx.measureText(wordToMeasure).width > maxWordWidth && wordToMeasure.length > 0) {
|
| 651 |
+
wordToMeasure = wordToMeasure.substr(0, wordToMeasure.length - 1)
|
| 652 |
+
}
|
| 653 |
+
styledWord.word = wordToMeasure + '…'
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
let lineWidth
|
| 657 |
+
const wordlWidth = canvasCtx.measureText(styledWord.word).width
|
| 658 |
+
|
| 659 |
+
if (styledWord.emoji) lineWidth = lineX + fontSize
|
| 660 |
+
else lineWidth = lineX + wordlWidth
|
| 661 |
+
|
| 662 |
+
if (styledWord.word.match(breakMatch) || (lineWidth > maxWidth - fontSize * 2 && wordlWidth < maxWidth)) {
|
| 663 |
+
if (styledWord.word.match(spaceMatch) && !styledWord.word.match(breakMatch)) styledWord.word = ''
|
| 664 |
+
if ((styledWord.word.match(spaceMatch) || !styledWord.word.match(breakMatch)) && lineY + lineHeight > maxHeight) {
|
| 665 |
+
while (lineWidth > maxWidth - fontSize * 2) {
|
| 666 |
+
styledWord.word = styledWord.word.substr(0, styledWord.word.length - 1)
|
| 667 |
+
lineWidth = lineX + canvasCtx.measureText(styledWord.word).width
|
| 668 |
+
if (styledWord.word.length <= 0) break
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
styledWord.word += '…'
|
| 672 |
+
lineWidth = lineX + canvasCtx.measureText(styledWord.word).width
|
| 673 |
+
breakWrite = true
|
| 674 |
+
} else {
|
| 675 |
+
if (styledWord.emoji) lineWidth = textX + fontSize + (fontSize * 0.2)
|
| 676 |
+
else lineWidth = textX + canvasCtx.measureText(styledWord.word).width
|
| 677 |
+
|
| 678 |
+
lineX = textX
|
| 679 |
+
lineY += lineHeight
|
| 680 |
+
if (index < styledWords.length - 1) {
|
| 681 |
+
let nextLineDirection = this.getLineDirection(styledWords, index + 1)
|
| 682 |
+
if (lineDirection !== nextLineDirection) textWidth = maxWidth - fontSize * 2
|
| 683 |
+
lineDirection = nextLineDirection
|
| 684 |
+
}
|
| 685 |
+
}
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
if (styledWord.emoji) lineWidth += (fontSize * 0.2)
|
| 689 |
+
|
| 690 |
+
if (lineWidth > textWidth) textWidth = lineWidth
|
| 691 |
+
if (textWidth > maxWidth) textWidth = maxWidth
|
| 692 |
+
|
| 693 |
+
let wordX = (lineDirection === 'rtl') ? maxWidth - lineX - wordlWidth - fontSize * 2 : lineX
|
| 694 |
+
|
| 695 |
+
if (emojiImage) {
|
| 696 |
+
canvasCtx.drawImage(emojiImage, wordX, lineY - fontSize + (fontSize * 0.15), fontSize + (fontSize * 0.22), fontSize + (fontSize * 0.22))
|
| 697 |
+
} else {
|
| 698 |
+
canvasCtx.fillText(styledWord.word, wordX, lineY)
|
| 699 |
+
|
| 700 |
+
if (styledWord.style.includes('strikethrough')) canvasCtx.fillRect(wordX, lineY - fontSize / 2.8, canvasCtx.measureText(styledWord.word).width, fontSize * 0.1)
|
| 701 |
+
if (styledWord.style.includes('underline')) canvasCtx.fillRect(wordX, lineY + 2, canvasCtx.measureText(styledWord.word).width, fontSize * 0.1)
|
| 702 |
+
}
|
| 703 |
+
|
| 704 |
+
lineX = lineWidth
|
| 705 |
+
|
| 706 |
+
if (breakWrite) break
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
const canvasResize = createCanvas(textWidth, lineY + fontSize)
|
| 710 |
+
const canvasResizeCtx = canvasResize.getContext('2d')
|
| 711 |
+
|
| 712 |
+
let dx = (lineDirection === 'rtl') ? textWidth - maxWidth + fontSize * 2 : 0
|
| 713 |
+
canvasResizeCtx.drawImage(canvas, dx, 0)
|
| 714 |
+
|
| 715 |
+
return canvasResize
|
| 716 |
+
}
|
| 717 |
+
|
| 718 |
+
// https://stackoverflow.com/a/3368118
|
| 719 |
+
drawRoundRect (color, w, h, r) {
|
| 720 |
+
const x = 0
|
| 721 |
+
const y = 0
|
| 722 |
+
|
| 723 |
+
const canvas = createCanvas(w, h)
|
| 724 |
+
const canvasCtx = canvas.getContext('2d')
|
| 725 |
+
|
| 726 |
+
canvasCtx.fillStyle = color
|
| 727 |
+
|
| 728 |
+
if (w < 2 * r) r = w / 2
|
| 729 |
+
if (h < 2 * r) r = h / 2
|
| 730 |
+
canvasCtx.beginPath()
|
| 731 |
+
canvasCtx.moveTo(x + r, y)
|
| 732 |
+
canvasCtx.arcTo(x + w, y, x + w, y + h, r)
|
| 733 |
+
canvasCtx.arcTo(x + w, y + h, x, y + h, r)
|
| 734 |
+
canvasCtx.arcTo(x, y + h, x, y, r)
|
| 735 |
+
canvasCtx.arcTo(x, y, x + w, y, r)
|
| 736 |
+
canvasCtx.closePath()
|
| 737 |
+
|
| 738 |
+
canvasCtx.fill()
|
| 739 |
+
|
| 740 |
+
return canvas
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
drawGradientRoundRect (colorOne, colorTwo, w, h, r) {
|
| 744 |
+
const x = 0
|
| 745 |
+
const y = 0
|
| 746 |
+
|
| 747 |
+
const canvas = createCanvas(w, h)
|
| 748 |
+
const canvasCtx = canvas.getContext('2d')
|
| 749 |
+
|
| 750 |
+
const gradient = canvasCtx.createLinearGradient(0, 0, w, h)
|
| 751 |
+
gradient.addColorStop(0, colorOne)
|
| 752 |
+
gradient.addColorStop(1, colorTwo)
|
| 753 |
+
|
| 754 |
+
canvasCtx.fillStyle = gradient
|
| 755 |
+
|
| 756 |
+
if (w < 2 * r) r = w / 2
|
| 757 |
+
if (h < 2 * r) r = h / 2
|
| 758 |
+
canvasCtx.beginPath()
|
| 759 |
+
canvasCtx.moveTo(x + r, y)
|
| 760 |
+
canvasCtx.arcTo(x + w, y, x + w, y + h, r)
|
| 761 |
+
canvasCtx.arcTo(x + w, y + h, x, y + h, r)
|
| 762 |
+
canvasCtx.arcTo(x, y + h, x, y, r)
|
| 763 |
+
canvasCtx.arcTo(x, y, x + w, y, r)
|
| 764 |
+
canvasCtx.closePath()
|
| 765 |
+
|
| 766 |
+
canvasCtx.fill()
|
| 767 |
+
|
| 768 |
+
return canvas
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
colorLuminance (hex, lum) {
|
| 772 |
+
hex = String(hex).replace(/[^0-9a-f]/gi, '')
|
| 773 |
+
if (hex.length < 6) {
|
| 774 |
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
|
| 775 |
+
}
|
| 776 |
+
lum = lum || 0
|
| 777 |
+
|
| 778 |
+
// convert to decimal and change luminosity
|
| 779 |
+
let rgb = '#'
|
| 780 |
+
let c
|
| 781 |
+
let i
|
| 782 |
+
for (i = 0; i < 3; i++) {
|
| 783 |
+
c = parseInt(hex.substr(i * 2, 2), 16)
|
| 784 |
+
c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16)
|
| 785 |
+
rgb += ('00' + c).substr(c.length)
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
return rgb
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
roundImage (image, r) {
|
| 792 |
+
const w = image.width
|
| 793 |
+
const h = image.height
|
| 794 |
+
|
| 795 |
+
const canvas = createCanvas(w, h)
|
| 796 |
+
const canvasCtx = canvas.getContext('2d')
|
| 797 |
+
|
| 798 |
+
const x = 0
|
| 799 |
+
const y = 0
|
| 800 |
+
|
| 801 |
+
if (w < 2 * r) r = w / 2
|
| 802 |
+
if (h < 2 * r) r = h / 2
|
| 803 |
+
canvasCtx.beginPath()
|
| 804 |
+
canvasCtx.moveTo(x + r, y)
|
| 805 |
+
canvasCtx.arcTo(x + w, y, x + w, y + h, r)
|
| 806 |
+
canvasCtx.arcTo(x + w, y + h, x, y + h, r)
|
| 807 |
+
canvasCtx.arcTo(x, y + h, x, y, r)
|
| 808 |
+
canvasCtx.arcTo(x, y, x + w, y, r)
|
| 809 |
+
canvasCtx.save()
|
| 810 |
+
canvasCtx.clip()
|
| 811 |
+
canvasCtx.closePath()
|
| 812 |
+
canvasCtx.drawImage(image, x, y)
|
| 813 |
+
canvasCtx.restore()
|
| 814 |
+
|
| 815 |
+
return canvas
|
| 816 |
+
}
|
| 817 |
+
|
| 818 |
+
drawReplyLine (lineWidth, height, color) {
|
| 819 |
+
const canvas = createCanvas(20, height)
|
| 820 |
+
const context = canvas.getContext('2d')
|
| 821 |
+
context.beginPath()
|
| 822 |
+
context.moveTo(10, 0)
|
| 823 |
+
context.lineTo(10, height)
|
| 824 |
+
context.lineWidth = lineWidth
|
| 825 |
+
context.strokeStyle = color
|
| 826 |
+
context.stroke()
|
| 827 |
+
context.closePath()
|
| 828 |
+
|
| 829 |
+
return canvas
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
async drawAvatar (user) {
|
| 833 |
+
try {
|
| 834 |
+
const avatarImage = await this.downloadAvatarImage(user)
|
| 835 |
+
|
| 836 |
+
if (avatarImage) {
|
| 837 |
+
const avatarSize = avatarImage.naturalHeight || avatarImage.height
|
| 838 |
+
|
| 839 |
+
const canvas = createCanvas(avatarSize, avatarSize)
|
| 840 |
+
const canvasCtx = canvas.getContext('2d')
|
| 841 |
+
|
| 842 |
+
const avatarX = 0
|
| 843 |
+
const avatarY = 0
|
| 844 |
+
|
| 845 |
+
canvasCtx.beginPath()
|
| 846 |
+
canvasCtx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2, true)
|
| 847 |
+
canvasCtx.clip()
|
| 848 |
+
canvasCtx.closePath()
|
| 849 |
+
canvasCtx.restore()
|
| 850 |
+
canvasCtx.drawImage(avatarImage, avatarX, avatarY, avatarSize, avatarSize)
|
| 851 |
+
|
| 852 |
+
return canvas
|
| 853 |
+
} else {
|
| 854 |
+
console.warn('No avatar image available for user')
|
| 855 |
+
return null
|
| 856 |
+
}
|
| 857 |
+
} catch (error) {
|
| 858 |
+
console.warn('Error drawing avatar:', error.message)
|
| 859 |
+
return null
|
| 860 |
+
}
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
drawLineSegment (ctx, x, y, width, isEven) {
|
| 864 |
+
ctx.lineWidth = 35 // how thick the line is
|
| 865 |
+
ctx.strokeStyle = '#aec6cf' // what color our line is
|
| 866 |
+
ctx.beginPath()
|
| 867 |
+
y = isEven ? y : -y
|
| 868 |
+
ctx.moveTo(x, 0)
|
| 869 |
+
ctx.lineTo(x, y)
|
| 870 |
+
ctx.arc(x + width / 2, y, width / 2, Math.PI, 0, isEven)
|
| 871 |
+
ctx.lineTo(x + width, 0)
|
| 872 |
+
ctx.stroke()
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
drawWaveform (data) {
|
| 876 |
+
const normalizedData = data.map(i => i / 32)
|
| 877 |
+
|
| 878 |
+
const canvas = createCanvas(4500, 500)
|
| 879 |
+
const padding = 50
|
| 880 |
+
canvas.height = (canvas.height + padding * 2)
|
| 881 |
+
const ctx = canvas.getContext('2d')
|
| 882 |
+
ctx.translate(0, canvas.height / 2 + padding)
|
| 883 |
+
|
| 884 |
+
// draw the line segments
|
| 885 |
+
const width = canvas.width / normalizedData.length
|
| 886 |
+
for (let i = 0; i < normalizedData.length; i++) {
|
| 887 |
+
const x = width * i
|
| 888 |
+
let height = normalizedData[i] * canvas.height - padding
|
| 889 |
+
if (height < 0) {
|
| 890 |
+
height = 0
|
| 891 |
+
} else if (height > canvas.height / 2) {
|
| 892 |
+
height = height > canvas.height / 2
|
| 893 |
+
}
|
| 894 |
+
this.drawLineSegment(ctx, x, height, width, (i + 1) % 2)
|
| 895 |
+
}
|
| 896 |
+
return canvas
|
| 897 |
+
}
|
| 898 |
+
|
| 899 |
+
async drawQuote (scale = 1, backgroundColorOne, backgroundColorTwo, avatar, replyName, replyNameColor, replyText, name, text, media, mediaType, maxMediaSize) {
|
| 900 |
+
const avatarPosX = 0 * scale
|
| 901 |
+
const avatarPosY = 5 * scale
|
| 902 |
+
const avatarSize = 50 * scale
|
| 903 |
+
|
| 904 |
+
const blockPosX = avatarSize + 10 * scale
|
| 905 |
+
const blockPosY = 0
|
| 906 |
+
|
| 907 |
+
const indent = 14 * scale
|
| 908 |
+
|
| 909 |
+
if (mediaType === 'sticker') name = undefined
|
| 910 |
+
|
| 911 |
+
let width = 0
|
| 912 |
+
if (name) width = name.width
|
| 913 |
+
if (text && width < text.width + indent) width = text.width + indent
|
| 914 |
+
if (name && width < name.width + indent) width = name.width + indent
|
| 915 |
+
if (replyName) {
|
| 916 |
+
if (width < replyName.width) width = replyName.width + indent * 2
|
| 917 |
+
if (replyText && width < replyText.width) width = replyText.width + indent * 2
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
let height = indent
|
| 921 |
+
if (text) height += text.height
|
| 922 |
+
else height += indent
|
| 923 |
+
|
| 924 |
+
if (name) {
|
| 925 |
+
height = name.height
|
| 926 |
+
if (text) height = text.height + name.height
|
| 927 |
+
else height += indent
|
| 928 |
+
}
|
| 929 |
+
|
| 930 |
+
width += blockPosX + indent
|
| 931 |
+
height += blockPosY
|
| 932 |
+
|
| 933 |
+
let namePosX = blockPosX + indent
|
| 934 |
+
let namePosY = indent
|
| 935 |
+
|
| 936 |
+
if (!name) {
|
| 937 |
+
namePosX = 0
|
| 938 |
+
namePosY = -indent
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
const textPosX = blockPosX + indent
|
| 942 |
+
let textPosY = indent
|
| 943 |
+
if (name) {
|
| 944 |
+
textPosY = name.height + indent * 0.25
|
| 945 |
+
height += indent * 0.25
|
| 946 |
+
}
|
| 947 |
+
|
| 948 |
+
let replyPosX = 0
|
| 949 |
+
let replyNamePosY = 0
|
| 950 |
+
let replyTextPosY = 0
|
| 951 |
+
|
| 952 |
+
if (replyName && replyText) {
|
| 953 |
+
replyPosX = textPosX + indent
|
| 954 |
+
|
| 955 |
+
const replyNameHeight = replyName.height
|
| 956 |
+
const replyTextHeight = replyText.height * 0.5
|
| 957 |
+
|
| 958 |
+
replyNamePosY = namePosY + replyNameHeight
|
| 959 |
+
replyTextPosY = replyNamePosY + replyTextHeight
|
| 960 |
+
|
| 961 |
+
textPosY += replyNameHeight + replyTextHeight + (indent / 4)
|
| 962 |
+
height += replyNameHeight + replyTextHeight + (indent / 4)
|
| 963 |
+
}
|
| 964 |
+
|
| 965 |
+
let mediaPosX = 0
|
| 966 |
+
let mediaPosY = 0
|
| 967 |
+
|
| 968 |
+
let mediaWidth, mediaHeight
|
| 969 |
+
|
| 970 |
+
if (media) {
|
| 971 |
+
mediaWidth = media.width * (maxMediaSize / media.height)
|
| 972 |
+
mediaHeight = maxMediaSize
|
| 973 |
+
|
| 974 |
+
if (mediaWidth >= maxMediaSize) {
|
| 975 |
+
mediaWidth = maxMediaSize
|
| 976 |
+
mediaHeight = media.height * (maxMediaSize / media.width)
|
| 977 |
+
}
|
| 978 |
+
|
| 979 |
+
if (!text || text.width <= mediaWidth || mediaWidth > (width - blockPosX)) {
|
| 980 |
+
width = mediaWidth + indent * 6
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
height += mediaHeight
|
| 984 |
+
if (!text) height += indent
|
| 985 |
+
|
| 986 |
+
if (name) {
|
| 987 |
+
mediaPosX = namePosX
|
| 988 |
+
mediaPosY = name.height + 5 * scale
|
| 989 |
+
} else {
|
| 990 |
+
mediaPosX = blockPosX + indent
|
| 991 |
+
mediaPosY = indent
|
| 992 |
+
}
|
| 993 |
+
if (replyName) mediaPosY += replyNamePosY + indent / 2
|
| 994 |
+
textPosY = mediaPosY + mediaHeight + 5 * scale
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
+
// Declare rectWidth and rectHeight variables before using them
|
| 998 |
+
let rectWidth = width - blockPosX
|
| 999 |
+
let rectHeight = height
|
| 1000 |
+
|
| 1001 |
+
if (mediaType === 'sticker' && (name || replyName)) {
|
| 1002 |
+
rectHeight = replyName && replyText ? (replyName.height + replyText.height * 0.5) + indent * 2 : indent * 2
|
| 1003 |
+
backgroundColorOne = backgroundColorTwo = 'rgba(0, 0, 0, 0.5)'
|
| 1004 |
+
}
|
| 1005 |
+
|
| 1006 |
+
const canvas = createCanvas(width, height)
|
| 1007 |
+
const canvasCtx = canvas.getContext('2d')
|
| 1008 |
+
|
| 1009 |
+
const rectPosX = blockPosX
|
| 1010 |
+
const rectPosY = blockPosY
|
| 1011 |
+
const rectRoundRadius = 25 * scale
|
| 1012 |
+
|
| 1013 |
+
let rect
|
| 1014 |
+
if (mediaType === 'sticker' && (name || replyName)) {
|
| 1015 |
+
rectHeight = (replyName.height + replyText.height * 0.5) + indent * 2
|
| 1016 |
+
backgroundColorOne = backgroundColorTwo = 'rgba(0, 0, 0, 0.5)'
|
| 1017 |
+
}
|
| 1018 |
+
|
| 1019 |
+
if (mediaType !== 'sticker' || name || replyName) {
|
| 1020 |
+
if (backgroundColorOne === backgroundColorTwo) {
|
| 1021 |
+
rect = this.drawRoundRect(backgroundColorOne, rectWidth, rectHeight, rectRoundRadius)
|
| 1022 |
+
} else {
|
| 1023 |
+
rect = this.drawGradientRoundRect(backgroundColorOne, backgroundColorTwo, rectWidth, rectHeight, rectRoundRadius)
|
| 1024 |
+
}
|
| 1025 |
+
}
|
| 1026 |
+
|
| 1027 |
+
if (avatar) canvasCtx.drawImage(avatar, avatarPosX, avatarPosY, avatarSize, avatarSize)
|
| 1028 |
+
if (rect) canvasCtx.drawImage(rect, rectPosX, rectPosY)
|
| 1029 |
+
if (name) canvasCtx.drawImage(name, namePosX, namePosY)
|
| 1030 |
+
if (text) canvasCtx.drawImage(text, textPosX, textPosY)
|
| 1031 |
+
if (media) canvasCtx.drawImage(this.roundImage(media, 5 * scale), mediaPosX, mediaPosY, mediaWidth, mediaHeight)
|
| 1032 |
+
|
| 1033 |
+
if (replyName && replyText) {
|
| 1034 |
+
canvasCtx.drawImage(this.drawReplyLine(3 * scale, replyName.height + replyText.height * 0.4, replyNameColor), textPosX - 3, replyNamePosY)
|
| 1035 |
+
|
| 1036 |
+
canvasCtx.drawImage(replyName, replyPosX, replyNamePosY)
|
| 1037 |
+
canvasCtx.drawImage(replyText, replyPosX, replyTextPosY)
|
| 1038 |
+
}
|
| 1039 |
+
|
| 1040 |
+
return canvas
|
| 1041 |
+
}
|
| 1042 |
+
|
| 1043 |
+
normalizeColor (color) {
|
| 1044 |
+
const canvas = createCanvas(0, 0)
|
| 1045 |
+
const canvasCtx = canvas.getContext('2d')
|
| 1046 |
+
|
| 1047 |
+
canvasCtx.fillStyle = color
|
| 1048 |
+
color = canvasCtx.fillStyle
|
| 1049 |
+
|
| 1050 |
+
return color
|
| 1051 |
+
}
|
| 1052 |
+
|
| 1053 |
+
getLineDirection (words, startIndex) {
|
| 1054 |
+
const RTLMatch = /[\u0591-\u07FF\u200F\u202B\u202E\uFB1D-\uFDFD\uFE70-\uFEFC]/
|
| 1055 |
+
const neutralMatch = /[\u0001-\u0040\u005B-\u0060\u007B-\u00BF\u00D7\u00F7\u02B9-\u02FF\u2000-\u2BFF\u2010-\u2029\u202C\u202F-\u2BFF\u1F300-\u1F5FF\u1F600-\u1F64F]/
|
| 1056 |
+
|
| 1057 |
+
for (let index = startIndex; index < words.length; index++) {
|
| 1058 |
+
if (words[index].word.match(RTLMatch)) {
|
| 1059 |
+
return 'rtl'
|
| 1060 |
+
} else {
|
| 1061 |
+
if (!words[index].word.match(neutralMatch)) { return 'ltr' }
|
| 1062 |
+
}
|
| 1063 |
+
}
|
| 1064 |
+
return 'ltr'
|
| 1065 |
+
}
|
| 1066 |
+
|
| 1067 |
+
async generate (backgroundColorOne, backgroundColorTwo, message, width = 512, height = 512, scale = 2, emojiBrand = 'apple') {
|
| 1068 |
+
if (!scale) scale = 2
|
| 1069 |
+
if (scale > 20) scale = 20
|
| 1070 |
+
width = width || 512 // Ensure width has a default value
|
| 1071 |
+
height = height || 512 // Ensure height has a default value
|
| 1072 |
+
width *= scale
|
| 1073 |
+
height *= scale
|
| 1074 |
+
|
| 1075 |
+
// check background style color black/light
|
| 1076 |
+
const backStyle = this.lightOrDark(backgroundColorOne)
|
| 1077 |
+
|
| 1078 |
+
// historyPeer1NameFg: #c03d33; // red
|
| 1079 |
+
// historyPeer2NameFg: #4fad2d; // green
|
| 1080 |
+
// historyPeer3NameFg: #d09306; // yellow
|
| 1081 |
+
// historyPeer4NameFg: #168acd; // blue
|
| 1082 |
+
// historyPeer5NameFg: #8544d6; // purple
|
| 1083 |
+
// historyPeer6NameFg: #cd4073; // pink
|
| 1084 |
+
// historyPeer7NameFg: #2996ad; // sea
|
| 1085 |
+
// historyPeer8NameFg: #ce671b; // orange
|
| 1086 |
+
|
| 1087 |
+
// { 0, 7, 4, 1, 6, 3, 5 }
|
| 1088 |
+
// const nameColor = [
|
| 1089 |
+
// '#c03d33', // red
|
| 1090 |
+
// '#ce671b', // orange
|
| 1091 |
+
// '#8544d6', // purple
|
| 1092 |
+
// '#4fad2d', // green
|
| 1093 |
+
// '#2996ad', // sea
|
| 1094 |
+
// '#168acd', // blue
|
| 1095 |
+
// '#cd4073' // pink
|
| 1096 |
+
// ]
|
| 1097 |
+
|
| 1098 |
+
const nameColorLight = [
|
| 1099 |
+
'#FC5C51', // red
|
| 1100 |
+
'#FA790F', // orange
|
| 1101 |
+
'#895DD5', // purple
|
| 1102 |
+
'#0FB297', // green
|
| 1103 |
+
'#0FC9D6', // sea
|
| 1104 |
+
'#3CA5EC', // blue
|
| 1105 |
+
'#D54FAF' // pink
|
| 1106 |
+
]
|
| 1107 |
+
|
| 1108 |
+
const nameColorDark = [
|
| 1109 |
+
'#FF8E86', // red
|
| 1110 |
+
'#FFA357', // orange
|
| 1111 |
+
'#B18FFF', // purple
|
| 1112 |
+
'#4DD6BF', // green
|
| 1113 |
+
'#45E8D1', // sea
|
| 1114 |
+
'#7AC9FF', // blue
|
| 1115 |
+
'#FF7FD5' // pink
|
| 1116 |
+
]
|
| 1117 |
+
|
| 1118 |
+
// user name color
|
| 1119 |
+
let nameIndex = 1
|
| 1120 |
+
if (message.from && message.from.id) nameIndex = Math.abs(message.from.id) % 7
|
| 1121 |
+
|
| 1122 |
+
const nameColorArray = backStyle === 'light' ? nameColorLight : nameColorDark
|
| 1123 |
+
|
| 1124 |
+
let nameColor = nameColorArray[nameIndex]
|
| 1125 |
+
|
| 1126 |
+
const colorContrast = new ColorContrast()
|
| 1127 |
+
|
| 1128 |
+
// change name color based on background color by contrast
|
| 1129 |
+
const contrast = colorContrast.getContrastRatio(this.colorLuminance(backgroundColorOne, 0.55), nameColor)
|
| 1130 |
+
if (contrast > 90 || contrast < 30) {
|
| 1131 |
+
nameColor = colorContrast.adjustContrast(this.colorLuminance(backgroundColorTwo, 0.55), nameColor)
|
| 1132 |
+
}
|
| 1133 |
+
|
| 1134 |
+
const nameSize = 22 * scale
|
| 1135 |
+
|
| 1136 |
+
let nameCanvas
|
| 1137 |
+
if ((message.from && message.from.name) || (message.from && (message.from.first_name || message.from.last_name))) {
|
| 1138 |
+
let name = message.from.name || `${message.from.first_name || ''} ${message.from.last_name || ''}`.trim()
|
| 1139 |
+
|
| 1140 |
+
if (!name) name = 'User' // Default name if none provided
|
| 1141 |
+
|
| 1142 |
+
const nameEntities = [
|
| 1143 |
+
{
|
| 1144 |
+
type: 'bold',
|
| 1145 |
+
offset: 0,
|
| 1146 |
+
length: name.length
|
| 1147 |
+
}
|
| 1148 |
+
]
|
| 1149 |
+
|
| 1150 |
+
if (message.from.emoji_status) {
|
| 1151 |
+
name += ' 🤡'
|
| 1152 |
+
|
| 1153 |
+
nameEntities.push({
|
| 1154 |
+
type: 'custom_emoji',
|
| 1155 |
+
offset: name.length - 2,
|
| 1156 |
+
length: 2,
|
| 1157 |
+
custom_emoji_id: message.from.emoji_status
|
| 1158 |
+
})
|
| 1159 |
+
}
|
| 1160 |
+
|
| 1161 |
+
nameCanvas = await this.drawMultilineText(
|
| 1162 |
+
name,
|
| 1163 |
+
nameEntities,
|
| 1164 |
+
nameSize,
|
| 1165 |
+
nameColor,
|
| 1166 |
+
0,
|
| 1167 |
+
nameSize,
|
| 1168 |
+
width,
|
| 1169 |
+
nameSize,
|
| 1170 |
+
emojiBrand
|
| 1171 |
+
)
|
| 1172 |
+
}
|
| 1173 |
+
|
| 1174 |
+
let fontSize = 24 * scale
|
| 1175 |
+
|
| 1176 |
+
let textColor = '#fff'
|
| 1177 |
+
if (backStyle === 'light') textColor = '#000'
|
| 1178 |
+
|
| 1179 |
+
let textCanvas
|
| 1180 |
+
if (message.text) {
|
| 1181 |
+
textCanvas = await this.drawMultilineText(
|
| 1182 |
+
message.text,
|
| 1183 |
+
message.entities,
|
| 1184 |
+
fontSize,
|
| 1185 |
+
textColor,
|
| 1186 |
+
0,
|
| 1187 |
+
fontSize,
|
| 1188 |
+
width,
|
| 1189 |
+
height - fontSize,
|
| 1190 |
+
emojiBrand
|
| 1191 |
+
)
|
| 1192 |
+
}
|
| 1193 |
+
|
| 1194 |
+
let avatarCanvas
|
| 1195 |
+
if (message.avatar && message.from) {
|
| 1196 |
+
try {
|
| 1197 |
+
avatarCanvas = await this.drawAvatar(message.from)
|
| 1198 |
+
} catch (error) {
|
| 1199 |
+
console.warn('Error drawing avatar:', error.message)
|
| 1200 |
+
avatarCanvas = null
|
| 1201 |
+
}
|
| 1202 |
+
}
|
| 1203 |
+
|
| 1204 |
+
let replyName, replyNameColor, replyText
|
| 1205 |
+
if (message.replyMessage && message.replyMessage.name && message.replyMessage.text) {
|
| 1206 |
+
try {
|
| 1207 |
+
// Ensure chatId exists to prevent NaN in calculations
|
| 1208 |
+
const chatId = message.replyMessage.chatId || 0
|
| 1209 |
+
const replyNameIndex = Math.abs(chatId) % 7
|
| 1210 |
+
replyNameColor = nameColorArray[replyNameIndex]
|
| 1211 |
+
|
| 1212 |
+
const replyNameFontSize = 16 * scale
|
| 1213 |
+
replyName = await this.drawMultilineText(
|
| 1214 |
+
message.replyMessage.name,
|
| 1215 |
+
'bold',
|
| 1216 |
+
replyNameFontSize,
|
| 1217 |
+
replyNameColor,
|
| 1218 |
+
0,
|
| 1219 |
+
replyNameFontSize,
|
| 1220 |
+
width * 0.9,
|
| 1221 |
+
replyNameFontSize,
|
| 1222 |
+
emojiBrand
|
| 1223 |
+
)
|
| 1224 |
+
|
| 1225 |
+
let textColor = '#fff'
|
| 1226 |
+
if (backStyle === 'light') textColor = '#000'
|
| 1227 |
+
|
| 1228 |
+
const replyTextFontSize = 21 * scale
|
| 1229 |
+
replyText = await this.drawMultilineText(
|
| 1230 |
+
message.replyMessage.text,
|
| 1231 |
+
message.replyMessage.entities || [],
|
| 1232 |
+
replyTextFontSize,
|
| 1233 |
+
textColor,
|
| 1234 |
+
0,
|
| 1235 |
+
replyTextFontSize,
|
| 1236 |
+
width * 0.9,
|
| 1237 |
+
replyTextFontSize,
|
| 1238 |
+
emojiBrand
|
| 1239 |
+
)
|
| 1240 |
+
} catch (error) {
|
| 1241 |
+
console.error('Error generating reply message:', error)
|
| 1242 |
+
// If reply message generation fails, continue without it
|
| 1243 |
+
replyName = null
|
| 1244 |
+
replyText = null
|
| 1245 |
+
}
|
| 1246 |
+
}
|
| 1247 |
+
|
| 1248 |
+
let mediaCanvas, mediaType, maxMediaSize
|
| 1249 |
+
if (message.media) {
|
| 1250 |
+
let media, type
|
| 1251 |
+
|
| 1252 |
+
let crop = false
|
| 1253 |
+
if (message.mediaCrop) crop = true
|
| 1254 |
+
|
| 1255 |
+
if (message.media.url) {
|
| 1256 |
+
type = 'url'
|
| 1257 |
+
media = message.media.url
|
| 1258 |
+
} else {
|
| 1259 |
+
type = 'id'
|
| 1260 |
+
if (message.media.length > 1) {
|
| 1261 |
+
if (crop) media = message.media[1]
|
| 1262 |
+
else media = message.media.pop()
|
| 1263 |
+
} else media = message.media[0]
|
| 1264 |
+
}
|
| 1265 |
+
|
| 1266 |
+
maxMediaSize = width / 3 * scale
|
| 1267 |
+
if (message.text && textCanvas && maxMediaSize < textCanvas.width) maxMediaSize = textCanvas.width
|
| 1268 |
+
|
| 1269 |
+
if (media && media.is_animated) {
|
| 1270 |
+
media = media.thumb
|
| 1271 |
+
maxMediaSize = maxMediaSize / 2
|
| 1272 |
+
}
|
| 1273 |
+
|
| 1274 |
+
try {
|
| 1275 |
+
mediaCanvas = await this.downloadMediaImage(media, maxMediaSize, type, crop)
|
| 1276 |
+
if (mediaCanvas) {
|
| 1277 |
+
mediaType = message.mediaType
|
| 1278 |
+
} else {
|
| 1279 |
+
console.warn('Failed to download media image, skipping media for this message')
|
| 1280 |
+
mediaCanvas = null
|
| 1281 |
+
mediaType = null
|
| 1282 |
+
}
|
| 1283 |
+
} catch (error) {
|
| 1284 |
+
console.warn('Error downloading media image:', error.message)
|
| 1285 |
+
mediaCanvas = null
|
| 1286 |
+
mediaType = null
|
| 1287 |
+
}
|
| 1288 |
+
}
|
| 1289 |
+
|
| 1290 |
+
if (message.voice) {
|
| 1291 |
+
mediaCanvas = this.drawWaveform(message.voice.waveform)
|
| 1292 |
+
maxMediaSize = width / 3 * scale
|
| 1293 |
+
}
|
| 1294 |
+
|
| 1295 |
+
const quote = this.drawQuote(
|
| 1296 |
+
scale,
|
| 1297 |
+
backgroundColorOne, backgroundColorTwo,
|
| 1298 |
+
avatarCanvas,
|
| 1299 |
+
replyName, replyNameColor, replyText,
|
| 1300 |
+
nameCanvas, textCanvas,
|
| 1301 |
+
mediaCanvas, mediaType, maxMediaSize
|
| 1302 |
+
)
|
| 1303 |
+
|
| 1304 |
+
return quote
|
| 1305 |
+
}
|
| 1306 |
+
}
|
| 1307 |
+
|
| 1308 |
+
module.exports = QuoteGenerate
|
utils/user-name.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = (user, url = false) => {
|
| 2 |
+
let name = user.first_name
|
| 3 |
+
|
| 4 |
+
if (user.last_name) name += ` ${user.last_name}`
|
| 5 |
+
name = name.replace(/</g, '<').replace(/>/g, '>')
|
| 6 |
+
|
| 7 |
+
if (url) return `<a href="tg://user?id=${user.id}">${name}</a>`
|
| 8 |
+
return name
|
| 9 |
+
}
|