rippanteq7 commited on
Commit
0dcf096
·
verified ·
1 Parent(s): 6be54bf

Upload folder using huggingface_hub

Browse files
.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.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ PORT="4888"
2
+ 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
+ }
.gitattributes CHANGED
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ assets/emoji/emoji-apple-image.json filter=lfs diff=lfs merge=lfs -text
37
+ assets/emoji/emoji-google-image.json filter=lfs diff=lfs merge=lfs -text
38
+ assets/emoji/emoji-joypixels-image.json filter=lfs diff=lfs merge=lfs -text
39
+ assets/emoji/emoji-twitter-image.json filter=lfs diff=lfs merge=lfs -text
40
+ assets/pattern_ny_old.png filter=lfs diff=lfs merge=lfs -text
.github/workflows/docker.yml ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Docker
2
+
3
+ on:
4
+ push:
5
+ branches: [ "master" ]
6
+ pull_request:
7
+ branches: [ "master" ]
8
+
9
+ env:
10
+ REGISTRY: ghcr.io
11
+ IMAGE_NAME: ${{ github.repository }}
12
+
13
+
14
+ jobs:
15
+ build:
16
+ runs-on: ubuntu-latest
17
+ permissions:
18
+ contents: read
19
+ packages: write
20
+ id-token: write
21
+
22
+ steps:
23
+ - name: Checkout repository
24
+ uses: actions/checkout@v4
25
+
26
+ - name: Set up Docker Buildx
27
+ uses: docker/setup-buildx-action@v3
28
+
29
+ - name: Log into registry ${{ env.REGISTRY }}
30
+ if: github.event_name != 'pull_request'
31
+ uses: docker/login-action@v3
32
+ with:
33
+ registry: ${{ env.REGISTRY }}
34
+ username: ${{ github.actor }}
35
+ password: ${{ secrets.GITHUB_TOKEN }}
36
+
37
+ - name: Extract Docker metadata
38
+ id: meta
39
+ uses: docker/metadata-action@v5
40
+ with:
41
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
42
+ tags: |
43
+ type=raw,value=latest
44
+ type=ref,event=tag
45
+ type=sha
46
+ - name: Build and push Docker image
47
+ id: build-and-push
48
+ uses: docker/build-push-action@v5
49
+ with:
50
+ context: .
51
+ push: ${{ github.event_name != 'pull_request' }}
52
+ tags: ${{ steps.meta.outputs.tags }}
53
+ labels: ${{ steps.meta.outputs.labels }}
54
+ cache-from: type=gha
55
+ cache-to: type=gha,mode=max
.gitignore ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # dotenv environment variables file
58
+ .env
59
+
60
+ # next.js build output
61
+ .next
62
+
63
+ assets/fonts/*
64
+ assets/emojis/*
65
+
66
+ !.gitkeep
.vscode/launch.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ // Use IntelliSense to learn about possible attributes.
3
+ // Hover to view descriptions of existing attributes.
4
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5
+ "version": "0.2.0",
6
+ "configurations": [
7
+ {
8
+ "type": "node",
9
+ "runtimeVersion": "20.13.1",
10
+ "request": "launch",
11
+ "name": "Launch Program",
12
+ "skipFiles": [
13
+ "<node_internals>/**"
14
+ ],
15
+ "program": "${workspaceFolder}/index.js"
16
+ }
17
+ ]
18
+ }
Dockerfile ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine3.16
2
+
3
+ WORKDIR /app
4
+ ADD . /app
5
+
6
+ RUN apk add --no-cache font-noto font-noto-cjk font-noto-extra gcompat libstdc++ libuuid vips-dev build-base jpeg-dev pango-dev cairo-dev imagemagick libssl1.1
7
+ RUN ln -s /lib/libresolv.so.2 /usr/lib/libresolv.so.2
8
+ RUN npm install
9
+
10
+ CMD ["index.js"]
README.md CHANGED
@@ -1,12 +1,232 @@
1
- ---
2
- title: Quote Api
3
- emoji: 📉
4
- colorFrom: indigo
5
- colorTo: red
6
- sdk: gradio
7
- sdk_version: 4.40.0
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # quote-api
2
+
3
+ [![wakatime](https://wakatime.com/badge/github/LyoSU/quote-api.svg)](https://wakatime.com/badge/github/LyoSU/quote-api)
4
+
5
+ Апи для генерации Telegram цитат
6
+
7
+ ## Методы
8
+ ##### Создание цитаты
9
+ ```http
10
+ POST /generate
11
+ ```
12
+
13
+ Пример JSON запроса:
14
+ ```json
15
+ {
16
+ "type": "quote",
17
+ "format": "png",
18
+ "backgroundColor": "#1b1429",
19
+ "width": 512,
20
+ "height": 768,
21
+ "scale": 2,
22
+ "messages": [
23
+ {
24
+ "entities": [],
25
+ "chatId": 66478514,
26
+ "avatar": true,
27
+ "from": {
28
+ "id": 66478514,
29
+ "first_name": "Yuri 💜",
30
+ "last_name": "Ly",
31
+ "username": "LyoSU",
32
+ "language_code": "ru",
33
+ "title": "Yuri 💜 Ly",
34
+ "photo": {
35
+ "small_file_id": "AQADAgADCKoxG7Jh9gMACBbSEZguAAMCAAOyYfYDAATieVimvJOu7M43BQABHgQ",
36
+ "small_file_unique_id": "AQADFtIRmC4AA843BQAB",
37
+ "big_file_id": "AQADAgADCKoxG7Jh9gMACBbSEZguAAMDAAOyYfYDAATieVimvJOu7NA3BQABHgQ",
38
+ "big_file_unique_id": "AQADFtIRmC4AA9A3BQAB"
39
+ },
40
+ "type": "private",
41
+ "name": "Yuri 💜 Ly"
42
+ },
43
+ "text": "I love you 💜",
44
+ "replyMessage": {}
45
+ }
46
+ ]
47
+ }
48
+ ```
49
+
50
+ Медиа:
51
+ ```json
52
+ {
53
+ "type": "quote",
54
+ "format": "png",
55
+ "backgroundColor": "#1b1429",
56
+ "width": 512,
57
+ "height": 768,
58
+ "scale": 2,
59
+ "messages": [
60
+ {
61
+ "media": [
62
+ {
63
+ "file_id": "CAACAgIAAxkBAAIyH2AAAUcJoPJqv4uOPabtiSR3judSnQACaQEAAiI3jgQe29BUaNTqrx4E",
64
+ "file_size": 22811,
65
+ "height": 512,
66
+ "width": 512
67
+ }
68
+ ],
69
+ "mediaType": "sticker",
70
+ "chatId": 66478514,
71
+ "avatar": true,
72
+ "from": {
73
+ "id": 66478514,
74
+ "first_name": "Yuri 💜",
75
+ "last_name": "Ly",
76
+ "username": "LyoSU",
77
+ "language_code": "ru",
78
+ "title": "Yuri 💜 Ly",
79
+ "photo": {
80
+ "small_file_id": "AQADAgADCKoxG7Jh9gMACBbSEZguAAMCAAOyYfYDAATieVimvJOu7M43BQABHgQ",
81
+ "small_file_unique_id": "AQADFtIRmC4AA843BQAB",
82
+ "big_file_id": "AQADAgADCKoxG7Jh9gMACBbSEZguAAMDAAOyYfYDAATieVimvJOu7NA3BQABHgQ",
83
+ "big_file_unique_id": "AQADFtIRmC4AA9A3BQAB"
84
+ },
85
+ "type": "private",
86
+ "name": "Yuri 💜 Ly"
87
+ },
88
+ "replyMessage": {}
89
+ }
90
+ ]
91
+ }
92
+ ```
93
+
94
+ Без Telegram
95
+ ```json
96
+ {
97
+ "type": "quote",
98
+ "format": "png",
99
+ "backgroundColor": "#1b1429",
100
+ "width": 512,
101
+ "height": 768,
102
+ "scale": 2,
103
+ "messages": [
104
+ {
105
+ "entities": [],
106
+ "media": {
107
+ "url": "https://via.placeholder.com/1000"
108
+ },
109
+ "avatar": true,
110
+ "from": {
111
+ "id": 1,
112
+ "name": "Mike",
113
+ "photo": {
114
+ "url": "https://via.placeholder.com/100"
115
+ }
116
+ },
117
+ "text": "Hey",
118
+ "replyMessage": {}
119
+ }
120
+ ]
121
+ }
122
+ ```
123
+
124
+ Параметры:
125
+ | Поле | Тип | Описание |
126
+ | :------------ | :------------ | :------------ |
127
+ | type | string | Тип выходного изображения. Может быть: quote, image, null |
128
+ | backgroundColor | string | Цвет фона цитаты. Может быть Hex, название или random для случайного цвета |
129
+ | messages | array | Массив из сообщений |
130
+ | width | number | Максимальная ширина |
131
+ | height | number | Максимальная высота |
132
+ | scale | number | Маcштаб |
133
+
134
+ Пример ответа:
135
+
136
+ ```json
137
+ {
138
+ "ok": true,
139
+ "result": {
140
+ "image": "base64 image",
141
+ "type": "quote",
142
+ "width": 512,
143
+ "height": 359
144
+ }
145
+ }
146
+
147
+ ```
148
+
149
+ ## Примеры запросов:
150
+ > JavaScript
151
+ ```js
152
+ const axios = require('axios')
153
+ const fs = require('fs')
154
+
155
+ const text = "Hello World"
156
+ const username = "Alι_Aryαɴ"
157
+ const avatar = "https://telegra.ph/file/59952c903fdfb10b752b3.jpg"
158
+
159
+ const json = {
160
+ "type": "quote",
161
+ "format": "png",
162
+ "backgroundColor": "#FFFFFF",
163
+ "width": 512,
164
+ "height": 768,
165
+ "scale": 2,
166
+ "messages": [
167
+ {
168
+ "entities": [],
169
+ "avatar": true,
170
+ "from": {
171
+ "id": 1,
172
+ "name": username,
173
+ "photo": {
174
+ "url": avatar
175
+ }
176
+ },
177
+ "text": text,
178
+ "replyMessage": {}
179
+ }
180
+ ]
181
+ };
182
+ const response = axios.post('https://bot.lyo.su/quote/generate', json, {
183
+ headers: {'Content-Type': 'application/json'}
184
+ }).then(res => {
185
+ const buffer = Buffer.from(res.data.result.image, 'base64')
186
+ fs.writeFile('Quotly.png', buffer, (err) => {
187
+ if (err) throw err;
188
+ })
189
+ });
190
+ ```
191
+
192
+ > Python
193
+ ```py
194
+ import requests
195
+ import base64
196
+
197
+ text = "Hello World"
198
+ username = "Alι_Aryαɴ"
199
+ avatar = "https://telegra.ph/file/59952c903fdfb10b752b3.jpg"
200
+
201
+ json = {
202
+ "type": "quote",
203
+ "format": "webp",
204
+ "backgroundColor": "#FFFFFF",
205
+ "width": 512,
206
+ "height": 768,
207
+ "scale": 2,
208
+ "messages": [
209
+ {
210
+ "entities": [],
211
+ "avatar": True,
212
+ "from": {
213
+ "id": 1,
214
+ "name": username,
215
+ "photo": {
216
+ "url": avatar
217
+ }
218
+ },
219
+ "text": text,
220
+ "replyMessage": {}
221
+ }
222
+ ]
223
+ }
224
+
225
+ response = requests.post('https://bot.lyo.su/quote/generate', json=json).json()
226
+ buffer = base64.b64decode(response['result']['image'].encode('utf-8'))
227
+ open('Quotly.png', 'wb').write(buffer)
228
+ print('Quotly.png')
229
+ ```
230
+ ### Response
231
+
232
+ ![Quotly.png](assets/Quotly.png)
app.js ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const logger = require('koa-logger')
2
+ const responseTime = require('koa-response-time')
3
+ const bodyParser = require('koa-bodyparser')
4
+ const ratelimit = require('koa-ratelimit')
5
+ const Router = require('koa-router')
6
+ const Koa = require('koa')
7
+
8
+ const app = new Koa()
9
+
10
+ app.use(logger())
11
+ app.use(responseTime())
12
+ app.use(bodyParser())
13
+
14
+ const ratelimitВb = new Map()
15
+
16
+ app.use(ratelimit({
17
+ driver: 'memory',
18
+ db: ratelimitВb,
19
+ duration: 1000 * 55,
20
+ errorMessage: {
21
+ ok: false,
22
+ error: {
23
+ code: 429,
24
+ message: 'Rate limit exceeded. See "Retry-After"'
25
+ }
26
+ },
27
+ id: (ctx) => ctx.ip,
28
+ headers: {
29
+ remaining: 'Rate-Limit-Remaining',
30
+ reset: 'Rate-Limit-Reset',
31
+ total: 'Rate-Limit-Total'
32
+ },
33
+ max: 20,
34
+ disableHeader: false,
35
+ whitelist: (ctx) => {
36
+ return ctx.query.botToken === process.env.BOT_TOKEN
37
+ },
38
+ blacklist: (ctx) => {
39
+ }
40
+ }))
41
+
42
+ app.use(require('./helpers').helpersApi)
43
+
44
+ const route = new Router()
45
+
46
+ const routes = require('./routes')
47
+
48
+ route.use('/*', routes.routeApi.routes())
49
+
50
+ app.use(route.routes())
51
+
52
+ const port = process.env.PORT || 3000
53
+
54
+ app.listen(port, () => {
55
+ console.log('Listening on localhost, port', port)
56
+ })
assets/Quotly.png ADDED
assets/emoji/emoji-apple-image.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a7f6822c2fe7de7e5af4637a812c89ffce5826ec546d0852e772ee117c50ecda
3
+ size 28524906
assets/emoji/emoji-blob-image.json ADDED
The diff for this file is too large to render. See raw diff
 
assets/emoji/emoji-google-image.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d2be7165b28cb0346161e94cc0e1813af1c175fab279128851128b09fa8b29ca
3
+ size 18282533
assets/emoji/emoji-joypixels-image.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0f9f1323fddae35bd891dfbe5ad218380e78d197834e4a250a073257d487faad
3
+ size 18326902
assets/emoji/emoji-twitter-image.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fec5f05b7a8735b18883eedbef089532b827fc6c970adb0071f550a60b56ea38
3
+ size 13737806
assets/fonts/.gitkeep ADDED
File without changes
assets/pattern_02.png ADDED
assets/pattern_ny.png ADDED
assets/pattern_ny_old.png ADDED

Git LFS Details

  • SHA256: 498deafb36f8ccd3c154b87f520b10f7edba8c0bc0dc266b9ed38541fef6688d
  • Pointer size: 132 Bytes
  • Size of remote file: 1.01 MB
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: '1000M',
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,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ const canvasQuote = await quoteGenerate.generate(
84
+ backgroundColorOne,
85
+ backgroundColorTwo,
86
+ message,
87
+ parm.width,
88
+ parm.height,
89
+ parseFloat(parm.scale),
90
+ parm.emojiBrand
91
+ )
92
+
93
+ quoteImages.push(canvasQuote)
94
+ }
95
+ }
96
+
97
+ if (quoteImages.length === 0) {
98
+ return {
99
+ error: 'empty_messages'
100
+ }
101
+ }
102
+
103
+ let canvasQuote
104
+
105
+ if (quoteImages.length > 1) {
106
+ let width = 0
107
+ let height = 0
108
+
109
+ for (let index = 0; index < quoteImages.length; index++) {
110
+ if (quoteImages[index].width > width) width = quoteImages[index].width
111
+ height += quoteImages[index].height
112
+ }
113
+
114
+ const quoteMargin = 5 * parm.scale
115
+
116
+ const canvas = createCanvas(width, height + (quoteMargin * quoteImages.length))
117
+ const canvasCtx = canvas.getContext('2d')
118
+
119
+ let imageY = 0
120
+
121
+ for (let index = 0; index < quoteImages.length; index++) {
122
+ canvasCtx.drawImage(quoteImages[index], 0, imageY)
123
+ imageY += quoteImages[index].height + quoteMargin
124
+ }
125
+ canvasQuote = canvas
126
+ } else {
127
+ canvasQuote = quoteImages[0]
128
+ }
129
+
130
+ let quoteImage
131
+
132
+ let { type, format, ext } = parm
133
+
134
+ if (!type && ext) type = 'png'
135
+ if (type !== 'image' && type !== 'stories' && canvasQuote.height > 1024 * 2) type = 'png'
136
+
137
+ if (type === 'quote') {
138
+ const downPadding = 75
139
+ const maxWidth = 512
140
+ const maxHeight = 512
141
+
142
+ const imageQuoteSharp = sharp(canvasQuote.toBuffer())
143
+
144
+ if (canvasQuote.height > canvasQuote.width) imageQuoteSharp.resize({ height: maxHeight })
145
+ else imageQuoteSharp.resize({ width: maxWidth })
146
+
147
+ const canvasImage = await loadImage(await imageQuoteSharp.toBuffer())
148
+
149
+ const canvasPadding = createCanvas(canvasImage.width, canvasImage.height + downPadding)
150
+ const canvasPaddingCtx = canvasPadding.getContext('2d')
151
+
152
+ canvasPaddingCtx.drawImage(canvasImage, 0, 0)
153
+
154
+ const imageSharp = sharp(canvasPadding.toBuffer())
155
+
156
+ if (canvasPadding.height >= canvasPadding.width) imageSharp.resize({ height: maxHeight })
157
+ else imageSharp.resize({ width: maxWidth })
158
+
159
+ if (format === 'png') quoteImage = await imageSharp.png().toBuffer()
160
+ else quoteImage = await imageSharp.webp({ lossless: true, force: true }).toBuffer()
161
+ } else if (type === 'image') {
162
+ const heightPadding = 75 * parm.scale
163
+ const widthPadding = 95 * parm.scale
164
+
165
+ const canvasImage = await loadImage(canvasQuote.toBuffer())
166
+
167
+ const canvasPic = createCanvas(canvasImage.width + widthPadding, canvasImage.height + heightPadding)
168
+ const canvasPicCtx = canvasPic.getContext('2d')
169
+
170
+ // radial gradient background (top left)
171
+ const gradient = canvasPicCtx.createRadialGradient(
172
+ canvasPic.width / 2,
173
+ canvasPic.height / 2,
174
+ 0,
175
+ canvasPic.width / 2,
176
+ canvasPic.height / 2,
177
+ canvasPic.width / 2
178
+ )
179
+
180
+ const patternColorOne = colorLuminance(backgroundColorTwo, 0.15)
181
+ const patternColorTwo = colorLuminance(backgroundColorOne, 0.15)
182
+
183
+ gradient.addColorStop(0, patternColorOne)
184
+ gradient.addColorStop(1, patternColorTwo)
185
+
186
+ canvasPicCtx.fillStyle = gradient
187
+ canvasPicCtx.fillRect(0, 0, canvasPic.width, canvasPic.height)
188
+
189
+ const canvasPatternImage = await loadImage('./assets/pattern_02.png')
190
+ // const canvasPatternImage = await loadImage('./assets/pattern_ny.png');
191
+
192
+ const pattern = canvasPicCtx.createPattern(imageAlpha(canvasPatternImage, 0.3), 'repeat')
193
+
194
+ canvasPicCtx.fillStyle = pattern
195
+ canvasPicCtx.fillRect(0, 0, canvasPic.width, canvasPic.height)
196
+
197
+ // Add shadow effect to the canvas image
198
+ canvasPicCtx.shadowOffsetX = 8
199
+ canvasPicCtx.shadowOffsetY = 8
200
+ canvasPicCtx.shadowBlur = 13
201
+ canvasPicCtx.shadowColor = 'rgba(0, 0, 0, 0.5)'
202
+
203
+ // Draw the image to the canvas with padding centered
204
+ canvasPicCtx.drawImage(canvasImage, widthPadding / 2, heightPadding / 2)
205
+
206
+ canvasPicCtx.shadowOffsetX = 0
207
+ canvasPicCtx.shadowOffsetY = 0
208
+ canvasPicCtx.shadowBlur = 0
209
+ canvasPicCtx.shadowColor = 'rgba(0, 0, 0, 0)'
210
+
211
+ // write text button right
212
+ canvasPicCtx.fillStyle = `rgba(0, 0, 0, 0.3)`
213
+ canvasPicCtx.font = `${8 * parm.scale}px Noto Sans`
214
+ canvasPicCtx.textAlign = 'right'
215
+ canvasPicCtx.fillText('@QuotLyBot', canvasPic.width - 25, canvasPic.height - 25)
216
+
217
+ quoteImage = await sharp(canvasPic.toBuffer()).png({ lossless: true, force: true }).toBuffer()
218
+ } else if (type === 'stories') {
219
+ const canvasPic = createCanvas(720, 1280)
220
+ const canvasPicCtx = canvasPic.getContext('2d')
221
+
222
+ // radial gradient background (top left)
223
+ const gradient = canvasPicCtx.createRadialGradient(
224
+ canvasPic.width / 2,
225
+ canvasPic.height / 2,
226
+ 0,
227
+ canvasPic.width / 2,
228
+ canvasPic.height / 2,
229
+ canvasPic.width / 2
230
+ )
231
+
232
+ const patternColorOne = colorLuminance(backgroundColorTwo, 0.25)
233
+ const patternColorTwo = colorLuminance(backgroundColorOne, 0.15)
234
+
235
+ gradient.addColorStop(0, patternColorOne)
236
+ gradient.addColorStop(1, patternColorTwo)
237
+
238
+ canvasPicCtx.fillStyle = gradient
239
+ canvasPicCtx.fillRect(0, 0, canvasPic.width, canvasPic.height)
240
+
241
+ const canvasPatternImage = await loadImage('./assets/pattern_02.png')
242
+
243
+ const pattern = canvasPicCtx.createPattern(imageAlpha(canvasPatternImage, 0.3), 'repeat')
244
+
245
+ canvasPicCtx.fillStyle = pattern
246
+ canvasPicCtx.fillRect(0, 0, canvasPic.width, canvasPic.height)
247
+
248
+ // Add shadow effect to the canvas image
249
+ canvasPicCtx.shadowOffsetX = 8
250
+ canvasPicCtx.shadowOffsetY = 8
251
+ canvasPicCtx.shadowBlur = 13
252
+ canvasPicCtx.shadowColor = 'rgba(0, 0, 0, 0.5)'
253
+
254
+ let canvasImage = await loadImage(canvasQuote.toBuffer())
255
+
256
+ // мінімальний відступ від країв картинки
257
+ const minPadding = 110
258
+
259
+ // resize canvasImage if it is larger than canvasPic + minPadding
260
+ if (canvasImage.width > canvasPic.width - minPadding * 2 || canvasImage.height > canvasPic.height - minPadding * 2) {
261
+ canvasImage = await sharp(canvasQuote.toBuffer()).resize({
262
+ width: canvasPic.width - minPadding * 2,
263
+ height: canvasPic.height - minPadding * 2,
264
+ fit: 'contain',
265
+ background: { r: 0, g: 0, b: 0, alpha: 0 }
266
+ }).toBuffer()
267
+
268
+ canvasImage = await loadImage(canvasImage)
269
+ }
270
+
271
+ // розмістити canvasImage в центрі по горизонталі і вертикалі
272
+ const imageX = (canvasPic.width - canvasImage.width) / 2
273
+ const imageY = (canvasPic.height - canvasImage.height) / 2
274
+
275
+ canvasPicCtx.drawImage(canvasImage, imageX, imageY)
276
+
277
+ canvasPicCtx.shadowOffsetX = 0
278
+ canvasPicCtx.shadowOffsetY = 0
279
+ canvasPicCtx.shadowBlur = 0
280
+
281
+ // write text vertical left center text
282
+ canvasPicCtx.fillStyle = `rgba(0, 0, 0, 0.4)`
283
+ canvasPicCtx.font = `${16 * parm.scale}px Noto Sans`
284
+ canvasPicCtx.textAlign = 'center'
285
+ canvasPicCtx.translate(70, canvasPic.height / 2)
286
+ canvasPicCtx.rotate(-Math.PI / 2)
287
+ canvasPicCtx.fillText('@QuotLyBot', 0, 0)
288
+
289
+ quoteImage = await sharp(canvasPic.toBuffer()).png({ lossless: true, force: true }).toBuffer()
290
+ } else {
291
+ quoteImage = canvasQuote.toBuffer()
292
+ }
293
+
294
+ const imageMetadata = await sharp(quoteImage).metadata()
295
+
296
+ const width = imageMetadata.width
297
+ const height = imageMetadata.height
298
+
299
+ let image
300
+ if (ext) image = quoteImage
301
+ else image = quoteImage.toString('base64')
302
+
303
+ return {
304
+ image,
305
+ type,
306
+ width,
307
+ height,
308
+ ext
309
+ }
310
+ }
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": "^14.0.1",
25
+ "jimp": "^0.16.1",
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,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const https = require('https')
2
+
3
+ module.exports = (url, filter = false) => {
4
+ return new Promise((resolve, reject) => {
5
+ https.get(url, (res) => {
6
+ if (filter && filter(res.headers)) {
7
+ resolve(Buffer.concat([]))
8
+ }
9
+
10
+ const chunks = []
11
+
12
+ res.on('error', (err) => {
13
+ reject(err)
14
+ })
15
+ res.on('data', (chunk) => {
16
+ chunks.push(chunk)
17
+ })
18
+ res.on('end', () => {
19
+ resolve(Buffer.concat(chunks))
20
+ })
21
+ })
22
+ })
23
+ }
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,1110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = 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
+
110
+ class QuoteGenerate {
111
+ constructor (botToken) {
112
+ this.telegram = new Telegram(botToken)
113
+ }
114
+
115
+ async avatarImageLatters (letters, color) {
116
+ const size = 500
117
+ const canvas = createCanvas(size, size)
118
+ const context = canvas.getContext('2d')
119
+
120
+ const gradient = context.createLinearGradient(0, 0, canvas.width, canvas.height)
121
+
122
+ gradient.addColorStop(0, color[0])
123
+ gradient.addColorStop(1, color[1])
124
+
125
+ context.fillStyle = gradient
126
+ context.fillRect(0, 0, canvas.width, canvas.height)
127
+
128
+ const drawLetters = await this.drawMultilineText(
129
+ letters,
130
+ null,
131
+ size / 2,
132
+ '#FFF',
133
+ 0,
134
+ size,
135
+ size * 5,
136
+ size * 5
137
+ )
138
+
139
+ context.drawImage(drawLetters, (canvas.width - drawLetters.width) / 2, (canvas.height - drawLetters.height) / 1.5)
140
+
141
+ return canvas.toBuffer()
142
+ }
143
+
144
+ async downloadAvatarImage (user) {
145
+ let avatarImage
146
+
147
+ let nameLatters
148
+ if (user.first_name && user.last_name) nameLatters = runes(user.first_name)[0] + (runes(user.last_name || '')[0])
149
+ else {
150
+ let name = user.first_name || user.name || user.title
151
+ name = name.toUpperCase()
152
+ const nameWord = name.split(' ')
153
+
154
+ if (nameWord.length > 1) nameLatters = runes(nameWord[0])[0] + runes(nameWord.splice(-1)[0])[0]
155
+ else nameLatters = runes(nameWord[0])[0]
156
+ }
157
+
158
+ const cacheKey = user.id
159
+
160
+ const avatarImageCache = avatarCache.get(cacheKey)
161
+
162
+ const avatarColorArray = [
163
+ [ '#FF885E', '#FF516A' ], // red
164
+ [ '#FFCD6A', '#FFA85C' ], // orange
165
+ [ '#E0A2F3', '#D669ED' ], // purple
166
+ [ '#A0DE7E', '#54CB68' ], // green
167
+ [ '#53EDD6', '#28C9B7' ], // sea
168
+ [ '#72D5FD', '#2A9EF1' ], // blue
169
+ [ '#FFA8A8', '#FF719A' ] // pink
170
+ ]
171
+
172
+ const nameIndex = Math.abs(user.id) % 7
173
+
174
+ const avatarColor = avatarColorArray[nameIndex]
175
+
176
+ if (avatarImageCache) {
177
+ avatarImage = avatarImageCache
178
+ } else if (user.photo && user.photo.url) {
179
+ avatarImage = await loadImage(user.photo.url)
180
+ } else {
181
+ try {
182
+ let userPhoto, userPhotoUrl
183
+
184
+ if (user.photo && user.photo.big_file_id) userPhotoUrl = await this.telegram.getFileLink(user.photo.big_file_id).catch(console.error)
185
+
186
+ if (!userPhotoUrl) {
187
+ const getChat = await this.telegram.getChat(user.id).catch(console.error)
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)
191
+ else if (user.username) userPhotoUrl = `https://telega.one/i/userpic/320/${user.username}.jpg`
192
+ else avatarImage = await loadImage(await this.avatarImageLatters(nameLatters, avatarColor))
193
+ }
194
+
195
+ if (userPhotoUrl) avatarImage = await loadImage(userPhotoUrl)
196
+
197
+ avatarCache.set(cacheKey, avatarImage)
198
+ } catch (error) {
199
+ avatarImage = await loadImage(await this.avatarImageLatters(nameLatters, avatarColor))
200
+ }
201
+ }
202
+
203
+ return avatarImage
204
+ }
205
+
206
+ ungzip (input, options) {
207
+ return new Promise((resolve, reject) => {
208
+ zlib.gunzip(input, options, (error, result) => {
209
+ if (!error) resolve(result)
210
+ else reject(Error(error))
211
+ })
212
+ })
213
+ }
214
+
215
+ async downloadMediaImage (media, mediaSize, type = 'id', crop = true) {
216
+ let mediaUrl
217
+ if (type === 'id') mediaUrl = await this.telegram.getFileLink(media).catch(console.error)
218
+ else mediaUrl = media
219
+ const load = await loadImageFromUrl(mediaUrl)
220
+ if (crop || mediaUrl.match(/.webp/)) {
221
+ const imageSharp = sharp(load)
222
+ const imageMetadata = await imageSharp.metadata()
223
+ const sharpPng = await imageSharp.png({ lossless: true, force: true }).toBuffer()
224
+
225
+ let croppedImage
226
+
227
+ if (imageMetadata.format === 'webp') {
228
+ const jimpImage = await Jimp.read(sharpPng)
229
+
230
+ croppedImage = await jimpImage.autocrop(false).getBufferAsync(Jimp.MIME_PNG)
231
+ } else {
232
+ const smartcropResult = await smartcrop.crop(sharpPng, { width: mediaSize, height: imageMetadata.height })
233
+ const crop = smartcropResult.topCrop
234
+
235
+ croppedImage = imageSharp.extract({ width: crop.width, height: crop.height, left: crop.x, top: crop.y })
236
+ croppedImage = await imageSharp.png({ lossless: true, force: true }).toBuffer()
237
+ }
238
+
239
+ return loadImage(croppedImage)
240
+ } else {
241
+ return loadImage(load)
242
+ }
243
+ }
244
+
245
+ hexToRgb (hex) {
246
+ return hex.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i
247
+ , (m, r, g, b) => '#' + r + r + g + g + b + b)
248
+ .substring(1).match(/.{2}/g)
249
+ .map(x => parseInt(x, 16))
250
+ }
251
+
252
+ // https://codepen.io/andreaswik/pen/YjJqpK
253
+ lightOrDark (color) {
254
+ let r, g, b
255
+
256
+ // Check the format of the color, HEX or RGB?
257
+ if (color.match(/^rgb/)) {
258
+ // If HEX --> store the red, green, blue values in separate variables
259
+ color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/)
260
+
261
+ r = color[1]
262
+ g = color[2]
263
+ b = color[3]
264
+ } else {
265
+ // If RGB --> Convert it to HEX: http://gist.github.com/983661
266
+ color = +('0x' + color.slice(1).replace(
267
+ color.length < 5 && /./g, '$&$&'
268
+ )
269
+ )
270
+
271
+ r = color >> 16
272
+ g = color >> 8 & 255
273
+ b = color & 255
274
+ }
275
+
276
+ // HSP (Highly Sensitive Poo) equation from http://alienryderflex.com/hsp.html
277
+ const hsp = Math.sqrt(
278
+ 0.299 * (r * r) +
279
+ 0.587 * (g * g) +
280
+ 0.114 * (b * b)
281
+ )
282
+
283
+ // Using the HSP value, determine whether the color is light or dark
284
+ if (hsp > 127.5) {
285
+ return 'light'
286
+ } else {
287
+ return 'dark'
288
+ }
289
+ }
290
+
291
+ async drawMultilineText (text, entities, fontSize, fontColor, textX, textY, maxWidth, maxHeight, emojiBrand = 'apple') {
292
+ if (maxWidth > 10000) maxWidth = 10000
293
+ if (maxHeight > 10000) maxHeight = 10000
294
+
295
+ const emojiImageJson = emojiImageByBrand[emojiBrand]
296
+
297
+ let fallbackEmojiBrand = 'apple'
298
+ if (emojiBrand === 'blob') fallbackEmojiBrand = 'google'
299
+
300
+ const fallbackEmojiImageJson = emojiImageByBrand[fallbackEmojiBrand]
301
+
302
+ const canvas = createCanvas(maxWidth + fontSize, maxHeight + fontSize)
303
+ const canvasCtx = canvas.getContext('2d')
304
+
305
+ // text = text.slice(0, 4096)
306
+ text = text.replace(/і/g, 'i') // замена украинской буквы і на английскую, так как она отсутствует в шрифтах Noto
307
+ const chars = text.split('')
308
+
309
+ const lineHeight = 4 * (fontSize * 0.3)
310
+
311
+ const styledChar = []
312
+
313
+ const emojis = emojiDb.searchFromText({ input: text, fixCodePoints: true })
314
+
315
+ for (let charIndex = 0; charIndex < chars.length; charIndex++) {
316
+ const char = chars[charIndex]
317
+
318
+ styledChar[charIndex] = {
319
+ char,
320
+ style: []
321
+ }
322
+
323
+ if (entities && typeof entities === 'string') styledChar[charIndex].style.push(entities)
324
+ }
325
+
326
+ if (entities && typeof entities === 'object') {
327
+ for (let entityIndex = 0; entityIndex < entities.length; entityIndex++) {
328
+ const entity = entities[entityIndex]
329
+ const style = []
330
+
331
+ if (['pre', 'code', 'pre_code'].includes(entity.type)) {
332
+ style.push('monospace')
333
+ } else if (
334
+ ['mention', 'text_mention', 'hashtag', 'email', 'phone_number', 'bot_command', 'url', 'text_link']
335
+ .includes(entity.type)
336
+ ) {
337
+ style.push('mention')
338
+ } else {
339
+ style.push(entity.type)
340
+ }
341
+
342
+ if (entity.type === 'custom_emoji') {
343
+ styledChar[entity.offset].customEmojiId = entity.custom_emoji_id
344
+ }
345
+
346
+ for (let charIndex = entity.offset; charIndex < entity.offset + entity.length; charIndex++) {
347
+ styledChar[charIndex].style = styledChar[charIndex].style.concat(style)
348
+ }
349
+ }
350
+ }
351
+
352
+ for (let emojiIndex = 0; emojiIndex < emojis.length; emojiIndex++) {
353
+ const emoji = emojis[emojiIndex]
354
+
355
+ for (let charIndex = emoji.offset; charIndex < emoji.offset + emoji.length; charIndex++) {
356
+ styledChar[charIndex].emoji = {
357
+ index: emojiIndex,
358
+ code: emoji.found
359
+ }
360
+ }
361
+ }
362
+
363
+ const styledWords = []
364
+
365
+ let stringNum = 0
366
+
367
+ const breakMatch = /<br>|\n|\r/
368
+ const spaceMatch = /[\f\n\r\t\v\u0020\u1680\u2000-\u200a\u2028\u2029\u205f\u3000]/
369
+ 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]/
370
+ const RTLMatch = /[\u0591-\u07FF\u200F\u202B\u202E\uFB1D-\uFDFD\uFE70-\uFEFC]/
371
+
372
+ for (let index = 0; index < styledChar.length; index++) {
373
+ const charStyle = styledChar[index]
374
+ const lastChar = styledChar[index - 1]
375
+
376
+ if (
377
+ lastChar && (
378
+ (
379
+ (charStyle.emoji && !lastChar.emoji) ||
380
+ (!charStyle.emoji && lastChar.emoji) ||
381
+ (charStyle.emoji && lastChar.emoji && charStyle.emoji.index !== lastChar.emoji.index)
382
+ ) ||
383
+ (
384
+ (charStyle.char.match(breakMatch)) ||
385
+ (charStyle.char.match(spaceMatch) && !lastChar.char.match(spaceMatch)) ||
386
+ (lastChar.char.match(spaceMatch) && !charStyle.char.match(spaceMatch)) ||
387
+ (charStyle.style && lastChar.style && charStyle.style.toString() !== lastChar.style.toString())
388
+ ) || (
389
+ charStyle.char.match(CJKMatch) ||
390
+ lastChar.char.match(CJKMatch)
391
+ )
392
+ )
393
+ ) {
394
+ stringNum++
395
+ }
396
+
397
+ if (!styledWords[stringNum]) {
398
+ styledWords[stringNum] = {
399
+ word: charStyle.char
400
+ }
401
+
402
+ if (charStyle.style) styledWords[stringNum].style = charStyle.style
403
+ if (charStyle.emoji) styledWords[stringNum].emoji = charStyle.emoji
404
+ if (charStyle.customEmojiId) styledWords[stringNum].customEmojiId = charStyle.customEmojiId
405
+ } else styledWords[stringNum].word += charStyle.char
406
+ }
407
+
408
+ let lineX = textX
409
+ let lineY = textY
410
+
411
+ let textWidth = 0
412
+
413
+ // load custom emoji
414
+ const customEmojiIds = []
415
+
416
+ for (let index = 0; index < styledWords.length; index++) {
417
+ const word = styledWords[index]
418
+
419
+ if (word.customEmojiId) {
420
+ customEmojiIds.push(word.customEmojiId)
421
+ }
422
+ }
423
+
424
+ const getCustomEmojiStickers = await this.telegram.callApi('getCustomEmojiStickers', {
425
+ custom_emoji_ids: customEmojiIds
426
+ }).catch(() => {})
427
+
428
+ const customEmojiStickers = {}
429
+
430
+ const loadCustomEmojiStickerPromises = []
431
+
432
+ if (getCustomEmojiStickers) {
433
+ for (let index = 0; index < getCustomEmojiStickers.length; index++) {
434
+ const sticker = getCustomEmojiStickers[index]
435
+
436
+ loadCustomEmojiStickerPromises.push((async () => {
437
+ const getFileLink = await this.telegram.getFileLink(sticker.thumb.file_id).catch(() => {})
438
+
439
+ if (getFileLink) {
440
+ const load = await loadImageFromUrl(getFileLink).catch(() => {})
441
+ const imageSharp = sharp(load)
442
+ const sharpPng = await imageSharp.png({ lossless: true, force: true }).toBuffer()
443
+
444
+ customEmojiStickers[sticker.custom_emoji_id] = await loadImage(sharpPng).catch(() => {})
445
+ }
446
+ })())
447
+ }
448
+
449
+ await Promise.all(loadCustomEmojiStickerPromises).catch(() => {})
450
+ }
451
+
452
+ let breakWrite = false
453
+ let lineDirection = styledWords[0].word.match(RTLMatch) ? 'rtl' : 'ltr'
454
+ for (let index = 0; index < styledWords.length; index++) {
455
+ const styledWord = styledWords[index]
456
+
457
+ let emojiImage
458
+
459
+ if (styledWord.emoji) {
460
+ if (styledWord.customEmojiId && customEmojiStickers[styledWord.customEmojiId]) {
461
+ emojiImage = customEmojiStickers[styledWord.customEmojiId]
462
+ } else {
463
+ const emojiImageBase = emojiImageJson[styledWord.emoji.code]
464
+ if (emojiImageBase) {
465
+ emojiImage = await loadImage(
466
+ Buffer.from(emojiImageBase, 'base64')
467
+ ).catch(() => {})
468
+ }
469
+ if (!emojiImage) {
470
+ emojiImage = await loadImage(
471
+ Buffer.from(fallbackEmojiImageJson[styledWord.emoji.code], 'base64')
472
+ ).catch(() => {})
473
+ }
474
+ }
475
+ }
476
+
477
+ let fontType = ''
478
+ let fontName = 'NotoSans'
479
+ let fillStyle = fontColor
480
+
481
+ if (styledWord.style.includes('bold')) {
482
+ fontType += 'bold '
483
+ }
484
+ if (styledWord.style.includes('italic')) {
485
+ fontType += 'italic '
486
+ }
487
+ if (styledWord.style.includes('monospace')) {
488
+ fontName = 'NotoSansMono'
489
+ fillStyle = '#5887a7'
490
+ }
491
+ if (styledWord.style.includes('mention')) {
492
+ fillStyle = '#6ab7ec'
493
+ }
494
+ if (styledWord.style.includes('spoiler')) {
495
+ const rbaColor = this.hexToRgb(this.normalizeColor(fontColor))
496
+ fillStyle = `rgba(${rbaColor[0]}, ${rbaColor[1]}, ${rbaColor[2]}, 0.15)`
497
+ }
498
+ // else {
499
+ // canvasCtx.font = `${fontSize}px OpenSans`
500
+ // canvasCtx.fillStyle = fontColor
501
+ // }
502
+
503
+ canvasCtx.font = `${fontType} ${fontSize}px ${fontName}`
504
+ canvasCtx.fillStyle = fillStyle
505
+
506
+ if (canvasCtx.measureText(styledWord.word).width > maxWidth - fontSize * 3) {
507
+ while (canvasCtx.measureText(styledWord.word).width > maxWidth - fontSize * 3) {
508
+ styledWord.word = styledWord.word.substr(0, styledWord.word.length - 1)
509
+ if (styledWord.word.length <= 0) break
510
+ }
511
+ styledWord.word += '…'
512
+ }
513
+
514
+ let lineWidth
515
+ const wordlWidth = canvasCtx.measureText(styledWord.word).width
516
+
517
+ if (styledWord.emoji) lineWidth = lineX + fontSize
518
+ else lineWidth = lineX + wordlWidth
519
+
520
+ if (styledWord.word.match(breakMatch) || (lineWidth > maxWidth - fontSize * 2 && wordlWidth < maxWidth)) {
521
+ if (styledWord.word.match(spaceMatch) && !styledWord.word.match(breakMatch)) styledWord.word = ''
522
+ if ((styledWord.word.match(spaceMatch) || !styledWord.word.match(breakMatch)) && lineY + lineHeight > maxHeight) {
523
+ while (lineWidth > maxWidth - fontSize * 2) {
524
+ styledWord.word = styledWord.word.substr(0, styledWord.word.length - 1)
525
+ lineWidth = lineX + canvasCtx.measureText(styledWord.word).width
526
+ if (styledWord.word.length <= 0) break
527
+ }
528
+
529
+ styledWord.word += '…'
530
+ lineWidth = lineX + canvasCtx.measureText(styledWord.word).width
531
+ breakWrite = true
532
+ } else {
533
+ if (styledWord.emoji) lineWidth = textX + fontSize + (fontSize * 0.2)
534
+ else lineWidth = textX + canvasCtx.measureText(styledWord.word).width
535
+
536
+ lineX = textX
537
+ lineY += lineHeight
538
+ if (index < styledWords.length - 1) {
539
+ let nextLineDirection = styledWords[index+1].word.match(RTLMatch) ? 'rtl' : 'ltr'
540
+ if (lineDirection != nextLineDirection) textWidth = maxWidth - fontSize * 2
541
+ lineDirection = nextLineDirection
542
+ }
543
+ }
544
+ }
545
+
546
+ if (styledWord.emoji) lineWidth += (fontSize * 0.2)
547
+
548
+ if (lineWidth > textWidth) textWidth = lineWidth
549
+ if (textWidth > maxWidth) textWidth = maxWidth
550
+
551
+ let wordX = (lineDirection == 'rtl') ? maxWidth-lineX-wordlWidth-fontSize * 2 : lineX
552
+
553
+ if (emojiImage) {
554
+ canvasCtx.drawImage(emojiImage, wordX, lineY - fontSize + (fontSize * 0.15), fontSize + (fontSize * 0.22), fontSize + (fontSize * 0.22))
555
+ } else {
556
+ canvasCtx.fillText(styledWord.word, wordX, lineY)
557
+
558
+ if (styledWord.style.includes('strikethrough')) canvasCtx.fillRect(wordX, lineY - fontSize / 2.8, canvasCtx.measureText(styledWord.word).width, fontSize * 0.1)
559
+ if (styledWord.style.includes('underline')) canvasCtx.fillRect(wordX, lineY + 2, canvasCtx.measureText(styledWord.word).width, fontSize * 0.1)
560
+ }
561
+
562
+ lineX = lineWidth
563
+
564
+ if (breakWrite) break
565
+ }
566
+
567
+ const canvasResize = createCanvas(textWidth, lineY + fontSize)
568
+ const canvasResizeCtx = canvasResize.getContext('2d')
569
+
570
+ let dx = (lineDirection == 'rtl') ? textWidth - maxWidth + fontSize * 2 : 0
571
+ canvasResizeCtx.drawImage(canvas, dx, 0)
572
+
573
+ return canvasResize
574
+ }
575
+
576
+ // https://stackoverflow.com/a/3368118
577
+ drawRoundRect (color, w, h, r) {
578
+ const x = 0
579
+ const y = 0
580
+
581
+ const canvas = createCanvas(w, h)
582
+ const canvasCtx = canvas.getContext('2d')
583
+
584
+ canvasCtx.fillStyle = color
585
+
586
+ if (w < 2 * r) r = w / 2
587
+ if (h < 2 * r) r = h / 2
588
+ canvasCtx.beginPath()
589
+ canvasCtx.moveTo(x + r, y)
590
+ canvasCtx.arcTo(x + w, y, x + w, y + h, r)
591
+ canvasCtx.arcTo(x + w, y + h, x, y + h, r)
592
+ canvasCtx.arcTo(x, y + h, x, y, r)
593
+ canvasCtx.arcTo(x, y, x + w, y, r)
594
+ canvasCtx.closePath()
595
+
596
+ canvasCtx.fill()
597
+
598
+ return canvas
599
+ }
600
+
601
+ drawGradientRoundRect (colorOne, colorTwo, w, h, r) {
602
+ const x = 0
603
+ const y = 0
604
+
605
+ const canvas = createCanvas(w, h)
606
+ const canvasCtx = canvas.getContext('2d')
607
+
608
+ const gradient = canvasCtx.createLinearGradient(0, 0, w, h)
609
+ gradient.addColorStop(0, colorOne)
610
+ gradient.addColorStop(1, colorTwo)
611
+
612
+ canvasCtx.fillStyle = gradient
613
+
614
+ if (w < 2 * r) r = w / 2
615
+ if (h < 2 * r) r = h / 2
616
+ canvasCtx.beginPath()
617
+ canvasCtx.moveTo(x + r, y)
618
+ canvasCtx.arcTo(x + w, y, x + w, y + h, r)
619
+ canvasCtx.arcTo(x + w, y + h, x, y + h, r)
620
+ canvasCtx.arcTo(x, y + h, x, y, r)
621
+ canvasCtx.arcTo(x, y, x + w, y, r)
622
+ canvasCtx.closePath()
623
+
624
+ canvasCtx.fill()
625
+
626
+ return canvas
627
+ }
628
+
629
+ colorLuminance (hex, lum) {
630
+ hex = String(hex).replace(/[^0-9a-f]/gi, '')
631
+ if (hex.length < 6) {
632
+ hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
633
+ }
634
+ lum = lum || 0
635
+
636
+ // convert to decimal and change luminosity
637
+ let rgb = '#'
638
+ let c
639
+ let i
640
+ for (i = 0; i < 3; i++) {
641
+ c = parseInt(hex.substr(i * 2, 2), 16)
642
+ c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16)
643
+ rgb += ('00' + c).substr(c.length)
644
+ }
645
+
646
+ return rgb
647
+ }
648
+
649
+ roundImage (image, r) {
650
+ const w = image.width
651
+ const h = image.height
652
+
653
+ const canvas = createCanvas(w, h)
654
+ const canvasCtx = canvas.getContext('2d')
655
+
656
+ const x = 0
657
+ const y = 0
658
+
659
+ if (w < 2 * r) r = w / 2
660
+ if (h < 2 * r) r = h / 2
661
+ canvasCtx.beginPath()
662
+ canvasCtx.moveTo(x + r, y)
663
+ canvasCtx.arcTo(x + w, y, x + w, y + h, r)
664
+ canvasCtx.arcTo(x + w, y + h, x, y + h, r)
665
+ canvasCtx.arcTo(x, y + h, x, y, r)
666
+ canvasCtx.arcTo(x, y, x + w, y, r)
667
+ canvasCtx.clip()
668
+ canvasCtx.closePath()
669
+ canvasCtx.restore()
670
+ canvasCtx.drawImage(image, x, y)
671
+
672
+ return canvas
673
+ }
674
+
675
+ deawReplyLine (lineWidth, height, color) {
676
+ const canvas = createCanvas(20, height)
677
+ const context = canvas.getContext('2d')
678
+ context.beginPath()
679
+ context.moveTo(10, 0)
680
+ context.lineTo(10, height)
681
+ context.lineWidth = lineWidth
682
+ context.strokeStyle = color
683
+ context.stroke()
684
+
685
+ return canvas
686
+ }
687
+
688
+ async drawAvatar (user) {
689
+ const avatarImage = await this.downloadAvatarImage(user)
690
+
691
+ if (avatarImage) {
692
+ const avatarSize = avatarImage.naturalHeight
693
+
694
+ const canvas = createCanvas(avatarSize, avatarSize)
695
+ const canvasCtx = canvas.getContext('2d')
696
+
697
+ const avatarX = 0
698
+ const avatarY = 0
699
+
700
+ canvasCtx.beginPath()
701
+ canvasCtx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2, true)
702
+ canvasCtx.clip()
703
+ canvasCtx.closePath()
704
+ canvasCtx.restore()
705
+ canvasCtx.drawImage(avatarImage, avatarX, avatarY, avatarSize, avatarSize)
706
+
707
+ return canvas
708
+ }
709
+ }
710
+
711
+ drawLineSegment (ctx, x, y, width, isEven) {
712
+ ctx.lineWidth = 35 // how thick the line is
713
+ ctx.strokeStyle = '#aec6cf' // what color our line is
714
+ ctx.beginPath()
715
+ y = isEven ? y : -y
716
+ ctx.moveTo(x, 0)
717
+ ctx.lineTo(x, y)
718
+ ctx.arc(x + width / 2, y, width / 2, Math.PI, 0, isEven)
719
+ ctx.lineTo(x + width, 0)
720
+ ctx.stroke()
721
+ }
722
+
723
+ drawWaveform (data) {
724
+ const normalizedData = data.map(i => i / 32)
725
+
726
+ const canvas = createCanvas(4500, 500)
727
+ const padding = 50
728
+ canvas.height = (canvas.height + padding * 2)
729
+ const ctx = canvas.getContext('2d')
730
+ ctx.translate(0, canvas.height / 2 + padding)
731
+
732
+ // draw the line segments
733
+ const width = canvas.width / normalizedData.length
734
+ for (let i = 0; i < normalizedData.length; i++) {
735
+ const x = width * i
736
+ let height = normalizedData[i] * canvas.height - padding
737
+ if (height < 0) {
738
+ height = 0
739
+ } else if (height > canvas.height / 2) {
740
+ height = height > canvas.height / 2
741
+ }
742
+ this.drawLineSegment(ctx, x, height, width, (i + 1) % 2)
743
+ }
744
+ return canvas
745
+ }
746
+
747
+ async drawQuote (scale = 1, backgroundColorOne, backgroundColorTwo, avatar, replyName, replyNameColor, replyText, name, text, media, mediaType, maxMediaSize) {
748
+ const avatarPosX = 0 * scale
749
+ const avatarPosY = 5 * scale
750
+ const avatarSize = 50 * scale
751
+
752
+ const blockPosX = avatarSize + 10 * scale
753
+ const blockPosY = 0
754
+
755
+ const indent = 14 * scale
756
+
757
+ if (mediaType === 'sticker') name = undefined
758
+
759
+ let width = 0
760
+ if (name) width = name.width
761
+ if (text && width < text.width + indent) width = text.width + indent
762
+ if (name && width < name.width + indent) width = name.width + indent
763
+ if (replyName) {
764
+ if (width < replyName.width) width = replyName.width + indent * 2
765
+ if (width < replyText.width) width = replyText.width + indent * 2
766
+ }
767
+
768
+ let height = indent
769
+ if (text) height += text.height
770
+ else height += indent
771
+
772
+ if (name) {
773
+ height = name.height
774
+ if (text) height = text.height + name.height
775
+ else height += indent
776
+ }
777
+
778
+ width += blockPosX + indent
779
+ height += blockPosY
780
+
781
+ let namePosX = blockPosX + indent
782
+ let namePosY = indent
783
+
784
+ if (!name) {
785
+ namePosX = 0
786
+ namePosY = -indent
787
+ }
788
+
789
+ const textPosX = blockPosX + indent
790
+ let textPosY = indent
791
+ if (name) {
792
+ textPosY = name.height + indent * 0.25
793
+ height += indent * 0.25
794
+ }
795
+
796
+ let replyPosX = 0
797
+ let replyNamePosY = 0
798
+ let replyTextPosY = 0
799
+
800
+ if (replyName) {
801
+ replyPosX = textPosX + indent
802
+
803
+ const replyNameHeight = replyName.height
804
+ const replyTextHeight = replyText.height * 0.5
805
+
806
+ replyNamePosY = namePosY + replyNameHeight
807
+ replyTextPosY = replyNamePosY + replyTextHeight
808
+
809
+ textPosY += replyNameHeight + replyTextHeight + (indent / 4)
810
+ height += replyNameHeight + replyTextHeight + (indent / 4)
811
+ }
812
+
813
+ let mediaPosX = 0
814
+ let mediaPosY = 0
815
+
816
+ let mediaWidth, mediaHeight
817
+
818
+ if (media) {
819
+ mediaWidth = media.width * (maxMediaSize / media.height)
820
+ mediaHeight = maxMediaSize
821
+
822
+ if (mediaWidth >= maxMediaSize) {
823
+ mediaWidth = maxMediaSize
824
+ mediaHeight = media.height * (maxMediaSize / media.width)
825
+ }
826
+
827
+ if (!text || text.width <= mediaWidth || mediaWidth > (width - blockPosX)) {
828
+ width = mediaWidth + indent * 6
829
+ }
830
+
831
+ height += mediaHeight
832
+ if (!text) height += indent
833
+
834
+ if (name) {
835
+ mediaPosX = namePosX
836
+ mediaPosY = name.height + 5 * scale
837
+ } else {
838
+ mediaPosX = blockPosX + indent
839
+ mediaPosY = indent
840
+ }
841
+ if (replyName) mediaPosY += replyNamePosY + indent / 2
842
+ textPosY = mediaPosY + mediaHeight + 5 * scale
843
+ }
844
+
845
+ if (mediaType === 'sticker' && (name || replyName)) {
846
+ mediaPosY += indent * 4
847
+ height += indent * 2
848
+ }
849
+
850
+ const canvas = createCanvas(width, height)
851
+ const canvasCtx = canvas.getContext('2d')
852
+
853
+ let rectWidth = width - blockPosX
854
+ let rectHeight = height
855
+ const rectPosX = blockPosX
856
+ const rectPosY = blockPosY
857
+ const rectRoundRadius = 25 * scale
858
+
859
+ let rect
860
+ if (mediaType === 'sticker' && (name || replyName)) {
861
+ rectHeight -= mediaHeight + indent * 2
862
+ }
863
+
864
+ if (mediaType !== 'sticker' || name || replyName) {
865
+ if (backgroundColorOne === backgroundColorTwo) {
866
+ rect = this.drawRoundRect(backgroundColorOne, rectWidth, rectHeight, rectRoundRadius)
867
+ } else {
868
+ rect = this.drawGradientRoundRect(backgroundColorOne, backgroundColorTwo, rectWidth, rectHeight, rectRoundRadius)
869
+ }
870
+ }
871
+
872
+ if (avatar) canvasCtx.drawImage(avatar, avatarPosX, avatarPosY, avatarSize, avatarSize)
873
+ if (rect) canvasCtx.drawImage(rect, rectPosX, rectPosY)
874
+ if (name) canvasCtx.drawImage(name, namePosX, namePosY)
875
+ if (text) canvasCtx.drawImage(text, textPosX, textPosY)
876
+ if (media) canvasCtx.drawImage(this.roundImage(media, 5 * scale), mediaPosX, mediaPosY, mediaWidth, mediaHeight)
877
+
878
+ if (replyName) {
879
+ canvasCtx.drawImage(this.deawReplyLine(3 * scale, replyName.height + replyText.height * 0.4, replyNameColor), textPosX - 3, replyNamePosY)
880
+
881
+ canvasCtx.drawImage(replyName, replyPosX, replyNamePosY)
882
+ canvasCtx.drawImage(replyText, replyPosX, replyTextPosY)
883
+ }
884
+
885
+ return canvas
886
+ }
887
+
888
+ normalizeColor (color) {
889
+ const canvas = createCanvas(0, 0)
890
+ const canvasCtx = canvas.getContext('2d')
891
+
892
+ canvasCtx.fillStyle = color
893
+ color = canvasCtx.fillStyle
894
+
895
+ return color
896
+ }
897
+
898
+ async generate (backgroundColorOne, backgroundColorTwo, message, width = 512, height = 512, scale = 2, emojiBrand = 'apple') {
899
+ if (!scale) scale = 2
900
+ if (scale > 20) scale = 20
901
+ width *= scale
902
+ height *= scale
903
+
904
+ // check background style color black/light
905
+ const backStyle = this.lightOrDark(backgroundColorOne)
906
+
907
+
908
+ // historyPeer1NameFg: #c03d33; // red
909
+ // historyPeer2NameFg: #4fad2d; // green
910
+ // historyPeer3NameFg: #d09306; // yellow
911
+ // historyPeer4NameFg: #168acd; // blue
912
+ // historyPeer5NameFg: #8544d6; // purple
913
+ // historyPeer6NameFg: #cd4073; // pink
914
+ // historyPeer7NameFg: #2996ad; // sea
915
+ // historyPeer8NameFg: #ce671b; // orange
916
+
917
+ // { 0, 7, 4, 1, 6, 3, 5 }
918
+ // const nameColor = [
919
+ // '#c03d33', // red
920
+ // '#ce671b', // orange
921
+ // '#8544d6', // purple
922
+ // '#4fad2d', // green
923
+ // '#2996ad', // sea
924
+ // '#168acd', // blue
925
+ // '#cd4073' // pink
926
+ // ]
927
+
928
+ const nameColorLight = [
929
+ '#FC5C51', // red
930
+ '#FA790F', // orange
931
+ '#895DD5', // purple
932
+ '#0FB297', // green
933
+ '#0FC9D6', // sea
934
+ '#3CA5EC', // blue
935
+ '#D54FAF' // pink
936
+ ]
937
+
938
+ const nameColorDark = [
939
+ '#FF8E86', // red
940
+ '#FFA357', // orange
941
+ '#B18FFF', // purple
942
+ '#4DD6BF', // green
943
+ '#45E8D1', // sea
944
+ '#7AC9FF', // blue
945
+ '#FF7FD5' // pink
946
+ ]
947
+
948
+ // user name color
949
+ let nameIndex = 1
950
+ if (message.from.id) nameIndex = Math.abs(message.from.id) % 7
951
+
952
+ const nameColorArray = backStyle === 'light' ? nameColorLight : nameColorDark
953
+
954
+ let nameColor = nameColorArray[nameIndex]
955
+
956
+ const colorContrast = new ColorContrast()
957
+
958
+ // change name color based on background color by contrast
959
+ const contrast = colorContrast.getContrastRatio(this.colorLuminance(backgroundColorOne, 0.55), nameColor)
960
+ if (contrast > 90 || contrast < 30) {
961
+ nameColor = colorContrast.adjustContrast(this.colorLuminance(backgroundColorTwo, 0.55), nameColor)
962
+ }
963
+
964
+ const nameSize = 22 * scale
965
+
966
+ let nameCanvas
967
+ if (message?.from?.name) {
968
+ let name = message.from.name
969
+
970
+ const nameEntities = [
971
+ {
972
+ type: 'bold',
973
+ offset: 0,
974
+ length: name.length
975
+ }
976
+ ]
977
+
978
+ if (message.from.emoji_status) {
979
+ name += ' 🤡'
980
+
981
+ nameEntities.push({
982
+ type: 'custom_emoji',
983
+ offset: name.length - 2,
984
+ length: 2,
985
+ custom_emoji_id: message.from.emoji_status
986
+ })
987
+ }
988
+
989
+ nameCanvas = await this.drawMultilineText(
990
+ name,
991
+ nameEntities,
992
+ nameSize,
993
+ nameColor,
994
+ 0,
995
+ nameSize,
996
+ width,
997
+ nameSize,
998
+ emojiBrand
999
+ )
1000
+ }
1001
+
1002
+ let fontSize = 24 * scale
1003
+
1004
+ let textColor = '#fff'
1005
+ if (backStyle === 'light') textColor = '#000'
1006
+
1007
+ let textCanvas
1008
+ if (message.text) {
1009
+ textCanvas = await this.drawMultilineText(
1010
+ message.text,
1011
+ message.entities,
1012
+ fontSize,
1013
+ textColor,
1014
+ 0,
1015
+ fontSize,
1016
+ width,
1017
+ height - fontSize,
1018
+ emojiBrand
1019
+ )
1020
+ }
1021
+
1022
+ let avatarCanvas
1023
+ if (message.avatar) avatarCanvas = await this.drawAvatar(message.from)
1024
+
1025
+ let replyName, replyNameColor, replyText
1026
+ if (message.replyMessage && message.replyMessage.name && message.replyMessage.text) {
1027
+ const replyNameIndex = Math.abs(message.replyMessage.chatId) % 7
1028
+ replyNameColor = nameColorArray[replyNameIndex]
1029
+
1030
+ const replyNameFontSize = 16 * scale
1031
+ if (message.replyMessage.name) {
1032
+ replyName = await this.drawMultilineText(
1033
+ message.replyMessage.name,
1034
+ 'bold',
1035
+ replyNameFontSize,
1036
+ replyNameColor,
1037
+ 0,
1038
+ replyNameFontSize,
1039
+ width * 0.9,
1040
+ replyNameFontSize,
1041
+ emojiBrand
1042
+ )
1043
+ }
1044
+
1045
+ let textColor = '#fff'
1046
+ if (backStyle === 'light') textColor = '#000'
1047
+
1048
+ const replyTextFontSize = 21 * scale
1049
+ replyText = await this.drawMultilineText(
1050
+ message.replyMessage.text,
1051
+ message.replyMessage.entities,
1052
+ replyTextFontSize,
1053
+ textColor,
1054
+ 0,
1055
+ replyTextFontSize,
1056
+ width * 0.9,
1057
+ replyTextFontSize,
1058
+ emojiBrand
1059
+ )
1060
+ }
1061
+
1062
+ let mediaCanvas, mediaType, maxMediaSize
1063
+ if (message.media) {
1064
+ let media, type
1065
+
1066
+ let crop = false
1067
+ if (message.mediaCrop) crop = true
1068
+
1069
+ if (message.media.url) {
1070
+ type = 'url'
1071
+ media = message.media.url
1072
+ } else {
1073
+ type = 'id'
1074
+ if (message.media.length > 1) {
1075
+ if (crop) media = message.media[1]
1076
+ else media = message.media.pop()
1077
+ } else media = message.media[0]
1078
+ }
1079
+
1080
+ maxMediaSize = width / 3 * scale
1081
+ if (message.text && maxMediaSize < textCanvas.width) maxMediaSize = textCanvas.width
1082
+
1083
+ if (media.is_animated) {
1084
+ media = media.thumb
1085
+ maxMediaSize = maxMediaSize / 2
1086
+ }
1087
+
1088
+ mediaCanvas = await this.downloadMediaImage(media, maxMediaSize, type, crop)
1089
+ mediaType = message.mediaType
1090
+ }
1091
+
1092
+ if (message.voice) {
1093
+ mediaCanvas = this.drawWaveform(message.voice.waveform)
1094
+ maxMediaSize = width / 3 * scale
1095
+ }
1096
+
1097
+ const quote = this.drawQuote(
1098
+ scale,
1099
+ backgroundColorOne, backgroundColorTwo,
1100
+ avatarCanvas,
1101
+ replyName, replyNameColor, replyText,
1102
+ nameCanvas, textCanvas,
1103
+ mediaCanvas, mediaType, maxMediaSize
1104
+ )
1105
+
1106
+ return quote
1107
+ }
1108
+ }
1109
+
1110
+ 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, '&lt;').replace(/>/g, '&gt;')
6
+
7
+ if (url) return `<a href="tg://user?id=${user.id}">${name}</a>`
8
+ return name
9
+ }