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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twikoo-func",
3
- "version": "1.7.1",
3
+ "version": "1.7.2",
4
4
  "description": "A simple comment system.",
5
5
  "author": "imaegoo <hello@imaegoo.com> (https://github.com/imaegoo)",
6
6
  "license": "MIT",
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 (!config.IMAGE_CDN || !config.IMAGE_CDN_TOKEN) {
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 (config.IMAGE_CDN === '7bu') {
34
+ if (imageService === '7bu') {
29
35
  await fn.uploadImageToLskyPro({ photo, fileName, config, res, imageCdn: 'https://7bu.top' })
30
- } else if (config.IMAGE_CDN === 'see') {
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(config.IMAGE_CDN)) {
33
- await fn.uploadImageToLskyPro({ photo, fileName, config, res, imageCdn: config.IMAGE_CDN })
34
- } else if (config.IMAGE_CDN === 'lskypro') {
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 (config.IMAGE_CDN === 'piclist') {
42
+ } else if (imageService === 'piclist') {
37
43
  await fn.uploadImageToPicList({ photo, fileName, config, res, imageCdn: config.IMAGE_CDN_URL })
38
- } else if (config.IMAGE_CDN === 'easyimage') {
44
+ } else if (imageService === 'easyimage') {
39
45
  await fn.uploadImageToEasyImage({ photo, fileName, config, res })
40
- } else if (config.IMAGE_CDN === 'chevereto') {
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,