twikoo-func 1.7.1 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -11
- package/package.json +1 -1
- package/utils/image.js +107 -9
- package/utils/index.js +3 -0
package/index.js
CHANGED
|
@@ -725,8 +725,7 @@ async function checkCaptcha (comment) {
|
|
|
725
725
|
turnstileToken: comment.turnstileToken,
|
|
726
726
|
turnstileTokenSecretKey: config.TURNSTILE_SECRET_KEY
|
|
727
727
|
})
|
|
728
|
-
}
|
|
729
|
-
if ((!provider || provider === 'Geetest') && config.GEETEST_CAPTCHA_ID && config.GEETEST_CAPTCHA_KEY) {
|
|
728
|
+
} else if ((!provider || provider === 'Geetest') && config.GEETEST_CAPTCHA_ID && config.GEETEST_CAPTCHA_KEY) {
|
|
730
729
|
await checkGeeTestCaptcha({
|
|
731
730
|
geeTestCaptchaId: config.GEETEST_CAPTCHA_ID,
|
|
732
731
|
geeTestCaptchaKey: config.GEETEST_CAPTCHA_KEY,
|
|
@@ -735,15 +734,6 @@ async function checkCaptcha (comment) {
|
|
|
735
734
|
geeTestPassToken: comment.geeTestPassToken,
|
|
736
735
|
geeTestGenTime: comment.geeTestGenTime
|
|
737
736
|
})
|
|
738
|
-
} else if (config.TURNSTILE_SITE_KEY) {
|
|
739
|
-
if (!config.TURNSTILE_SECRET_KEY) {
|
|
740
|
-
throw new Error('Turnstile 验证码配置不完整,缺少 TURNSTILE_SECRET_KEY')
|
|
741
|
-
}
|
|
742
|
-
await checkTurnstileCaptcha({
|
|
743
|
-
ip: auth.getClientIP(),
|
|
744
|
-
turnstileToken: comment.turnstileToken,
|
|
745
|
-
turnstileTokenSecretKey: config.TURNSTILE_SECRET_KEY
|
|
746
|
-
})
|
|
747
737
|
}
|
|
748
738
|
}
|
|
749
739
|
|
package/package.json
CHANGED
package/utils/image.js
CHANGED
|
@@ -12,8 +12,14 @@ const fn = {
|
|
|
12
12
|
async uploadImage (event, config) {
|
|
13
13
|
const { photo, fileName } = event
|
|
14
14
|
const res = {}
|
|
15
|
+
const imageService = config.IMAGE_SERVICE || config.IMAGE_CDN
|
|
15
16
|
try {
|
|
16
|
-
if (
|
|
17
|
+
if (imageService === 's3') {
|
|
18
|
+
// S3 图床只需要配置相关 S3 参数,不需要 IMAGE_CDN_TOKEN
|
|
19
|
+
if (!config.S3_BUCKET || !config.S3_ACCESS_KEY_ID || !config.S3_SECRET_ACCESS_KEY) {
|
|
20
|
+
throw new Error('未配置 S3 图床参数(S3_BUCKET、S3_ACCESS_KEY_ID、S3_SECRET_ACCESS_KEY)')
|
|
21
|
+
}
|
|
22
|
+
} else if (!imageService || !config.IMAGE_CDN_TOKEN) {
|
|
17
23
|
throw new Error('未配置图片上传服务')
|
|
18
24
|
}
|
|
19
25
|
if (config.NSFW_API_URL) {
|
|
@@ -25,20 +31,22 @@ const fn = {
|
|
|
25
31
|
}
|
|
26
32
|
}
|
|
27
33
|
// tip: qcloud 图床走前端上传,其他图床走后端上传
|
|
28
|
-
if (
|
|
34
|
+
if (imageService === '7bu') {
|
|
29
35
|
await fn.uploadImageToLskyPro({ photo, fileName, config, res, imageCdn: 'https://7bu.top' })
|
|
30
|
-
} else if (
|
|
36
|
+
} else if (imageService === 'see') {
|
|
31
37
|
await fn.uploadImageToSee({ photo, fileName, config, res, imageCdn: 'https://s.ee/api/v1/file/upload' })
|
|
32
|
-
} else if (isUrl(
|
|
33
|
-
await fn.uploadImageToLskyPro({ photo, fileName, config, res, imageCdn:
|
|
34
|
-
} else if (
|
|
38
|
+
} else if (isUrl(imageService)) {
|
|
39
|
+
await fn.uploadImageToLskyPro({ photo, fileName, config, res, imageCdn: imageService })
|
|
40
|
+
} else if (imageService === 'lskypro') {
|
|
35
41
|
await fn.uploadImageToLskyPro({ photo, fileName, config, res, imageCdn: config.IMAGE_CDN_URL })
|
|
36
|
-
} else if (
|
|
42
|
+
} else if (imageService === 'piclist') {
|
|
37
43
|
await fn.uploadImageToPicList({ photo, fileName, config, res, imageCdn: config.IMAGE_CDN_URL })
|
|
38
|
-
} else if (
|
|
44
|
+
} else if (imageService === 'easyimage') {
|
|
39
45
|
await fn.uploadImageToEasyImage({ photo, fileName, config, res })
|
|
40
|
-
} else if (
|
|
46
|
+
} else if (imageService === 'chevereto') {
|
|
41
47
|
await fn.uploadImageToChevereto({ photo, fileName, config, res })
|
|
48
|
+
} else if (imageService === 's3') {
|
|
49
|
+
await fn.uploadImageToS3({ photo, fileName, config, res })
|
|
42
50
|
} else {
|
|
43
51
|
throw new Error('不支持的图片上传服务')
|
|
44
52
|
}
|
|
@@ -214,6 +222,96 @@ const fn = {
|
|
|
214
222
|
throw new Error(`Chevereto 上传失败: ${errMsg}`)
|
|
215
223
|
}
|
|
216
224
|
},
|
|
225
|
+
async uploadImageToS3 ({ photo, fileName, config, res }) {
|
|
226
|
+
// 使用原生 crypto + axios 实现 AWS Signature V4,无需引入 SDK
|
|
227
|
+
if (!config.S3_BUCKET) {
|
|
228
|
+
throw new Error('未配置 S3 存储桶名称 (S3_BUCKET)')
|
|
229
|
+
}
|
|
230
|
+
if (!config.S3_ACCESS_KEY_ID) {
|
|
231
|
+
throw new Error('未配置 S3 Access Key ID (S3_ACCESS_KEY_ID)')
|
|
232
|
+
}
|
|
233
|
+
if (!config.S3_SECRET_ACCESS_KEY) {
|
|
234
|
+
throw new Error('未配置 S3 Secret Access Key (S3_SECRET_ACCESS_KEY)')
|
|
235
|
+
}
|
|
236
|
+
const crypto = require('crypto')
|
|
237
|
+
const region = config.S3_REGION || 'us-east-1'
|
|
238
|
+
// 解析 base64 图片数据
|
|
239
|
+
const base64 = photo.split(';base64,').pop()
|
|
240
|
+
const mimeType = photo.split(';base64,')[0].replace('data:', '') || 'image/webp'
|
|
241
|
+
const body = Buffer.from(base64, 'base64')
|
|
242
|
+
// 构建对象 key
|
|
243
|
+
const prefix = config.S3_PATH_PREFIX ? config.S3_PATH_PREFIX.replace(/\/$/, '') + '/' : ''
|
|
244
|
+
const key = `${prefix}${Date.now()}-${fileName}`
|
|
245
|
+
let endpoint
|
|
246
|
+
if (config.S3_ENDPOINT) {
|
|
247
|
+
// 兼容 R2
|
|
248
|
+
endpoint = `${config.S3_ENDPOINT.replace(/\/$/, '')}/${config.S3_BUCKET}/${key}`
|
|
249
|
+
} else {
|
|
250
|
+
// 标准 AWS S3:virtual-hosted-style URL
|
|
251
|
+
endpoint = `https://${config.S3_BUCKET}.s3.${region}.amazonaws.com/${key}`
|
|
252
|
+
}
|
|
253
|
+
const endpointUrl = new URL(endpoint)
|
|
254
|
+
const host = endpointUrl.host
|
|
255
|
+
const pathname = endpointUrl.pathname
|
|
256
|
+
const now = new Date()
|
|
257
|
+
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '')
|
|
258
|
+
const amzDate = now.toISOString().replace(/[:-]/g, '').slice(0, 15) + 'Z'
|
|
259
|
+
const payloadHash = crypto.createHash('sha256').update(body).digest('hex')
|
|
260
|
+
const signedHeaders = 'content-type;host;x-amz-content-sha256;x-amz-date'
|
|
261
|
+
const canonicalHeaders = [
|
|
262
|
+
`content-type:${mimeType}`,
|
|
263
|
+
`host:${host}`,
|
|
264
|
+
`x-amz-content-sha256:${payloadHash}`,
|
|
265
|
+
`x-amz-date:${amzDate}`
|
|
266
|
+
].join('\n') + '\n'
|
|
267
|
+
const canonicalRequest = [
|
|
268
|
+
'PUT',
|
|
269
|
+
pathname,
|
|
270
|
+
'', // query string
|
|
271
|
+
canonicalHeaders,
|
|
272
|
+
signedHeaders,
|
|
273
|
+
payloadHash
|
|
274
|
+
].join('\n')
|
|
275
|
+
const credentialScope = `${dateStamp}/${region}/s3/aws4_request`
|
|
276
|
+
const stringToSign = [
|
|
277
|
+
'AWS4-HMAC-SHA256',
|
|
278
|
+
amzDate,
|
|
279
|
+
credentialScope,
|
|
280
|
+
crypto.createHash('sha256').update(canonicalRequest).digest('hex')
|
|
281
|
+
].join('\n')
|
|
282
|
+
const hmac = (key, data) => crypto.createHmac('sha256', key).update(data).digest()
|
|
283
|
+
const signingKey = hmac(
|
|
284
|
+
hmac(
|
|
285
|
+
hmac(
|
|
286
|
+
hmac(Buffer.from('AWS4' + config.S3_SECRET_ACCESS_KEY), dateStamp),
|
|
287
|
+
region
|
|
288
|
+
),
|
|
289
|
+
's3'
|
|
290
|
+
),
|
|
291
|
+
'aws4_request'
|
|
292
|
+
)
|
|
293
|
+
const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex')
|
|
294
|
+
const authorization = `AWS4-HMAC-SHA256 Credential=${config.S3_ACCESS_KEY_ID}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
|
|
295
|
+
await axios.put(endpoint, body, {
|
|
296
|
+
headers: {
|
|
297
|
+
'Content-Type': mimeType,
|
|
298
|
+
'x-amz-content-sha256': payloadHash,
|
|
299
|
+
'x-amz-date': amzDate,
|
|
300
|
+
Authorization: authorization
|
|
301
|
+
},
|
|
302
|
+
maxBodyLength: Infinity
|
|
303
|
+
})
|
|
304
|
+
// 构建访问 URL
|
|
305
|
+
let fileUrl
|
|
306
|
+
if (config.S3_CDN_URL) {
|
|
307
|
+
fileUrl = `${config.S3_CDN_URL.replace(/\/$/, '')}/${key}`
|
|
308
|
+
} else if (config.S3_ENDPOINT) {
|
|
309
|
+
fileUrl = `${config.S3_ENDPOINT.replace(/\/$/, '')}/${config.S3_BUCKET}/${key}`
|
|
310
|
+
} else {
|
|
311
|
+
fileUrl = `https://${config.S3_BUCKET}.s3.${region}.amazonaws.com/${key}`
|
|
312
|
+
}
|
|
313
|
+
res.data = { url: fileUrl }
|
|
314
|
+
},
|
|
217
315
|
base64UrlToReadStream (base64Url, fileName) {
|
|
218
316
|
const base64 = base64Url.split(';base64,').pop()
|
|
219
317
|
const writePath = path.resolve(os.tmpdir(), fileName)
|
package/utils/index.js
CHANGED
|
@@ -367,10 +367,13 @@ const fn = {
|
|
|
367
367
|
DEFAULT_GRAVATAR: config.DEFAULT_GRAVATAR,
|
|
368
368
|
SHOW_IMAGE: config.SHOW_IMAGE || 'true',
|
|
369
369
|
IMAGE_CDN: config.IMAGE_CDN,
|
|
370
|
+
IMAGE_SERVICE: config.IMAGE_SERVICE,
|
|
370
371
|
LIGHTBOX: config.LIGHTBOX || 'false',
|
|
371
372
|
SHOW_EMOTION: config.SHOW_EMOTION || 'true',
|
|
372
373
|
EMOTION_CDN: config.EMOTION_CDN,
|
|
373
374
|
COMMENT_PLACEHOLDER: config.COMMENT_PLACEHOLDER,
|
|
375
|
+
SHOW_ORDER: config.SHOW_ORDER || 'true',
|
|
376
|
+
SHOW_DISLIKE: config.SHOW_DISLIKE || 'true',
|
|
374
377
|
DISPLAYED_FIELDS: config.DISPLAYED_FIELDS,
|
|
375
378
|
REQUIRED_FIELDS: config.REQUIRED_FIELDS,
|
|
376
379
|
HIDE_ADMIN_CRYPT: config.HIDE_ADMIN_CRYPT,
|