twikoo-vercel 1.5.1 → 1.5.4

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.
Files changed (2) hide show
  1. package/api/index.js +97 -17
  2. package/package.json +1 -1
package/api/index.js CHANGED
@@ -46,6 +46,7 @@ const RES_CODE = {
46
46
  AKISMET_ERROR: 1030,
47
47
  UPLOAD_FAILED: 1040
48
48
  }
49
+ const MAX_REQUEST_TIMES = parseInt(process.env.TWIKOO_THROTTLE) || 250
49
50
 
50
51
  // 全局变量 / variables
51
52
  let db = null
@@ -54,15 +55,18 @@ let transporter
54
55
  let request
55
56
  let response
56
57
  let accessToken
58
+ const requestTimes = {}
57
59
 
58
60
  module.exports = async (requestArg, responseArg) => {
59
61
  request = requestArg
60
62
  response = responseArg
61
63
  const event = request.body || {}
64
+ console.log('请求IP:', request.headers['x-real-ip'])
62
65
  console.log('请求方法:', event.event)
63
66
  console.log('请求参数:', event)
64
67
  let res = {}
65
68
  try {
69
+ protect()
66
70
  anonymousSignIn()
67
71
  await connectToDatabase(process.env.MONGODB_URI)
68
72
  await readConfig()
@@ -1174,6 +1178,12 @@ async function limitFilter () {
1174
1178
 
1175
1179
  // 预垃圾评论检测
1176
1180
  function preCheckSpam (comment) {
1181
+ // 长度限制
1182
+ let limitLength = parseInt(config.LIMIT_LENGTH)
1183
+ if (Number.isNaN(limitLength)) limitLength = 500
1184
+ if (limitLength && comment.length > limitLength) {
1185
+ throw new Error('评论内容过长')
1186
+ }
1177
1187
  if (config.AKISMET_KEY === 'MANUAL_REVIEW') {
1178
1188
  // 人工审核
1179
1189
  console.log('已使用人工审核模式,评论审核后才会发表~')
@@ -1393,9 +1403,9 @@ async function emailTest (event) {
1393
1403
  const isAdminUser = await isAdmin()
1394
1404
  if (isAdminUser) {
1395
1405
  try {
1396
- if (!transporter) {
1397
- await initMailer({ throwErr: true })
1398
- }
1406
+ // 邮件测试前清除 transporter,保证读取的是最新的配置
1407
+ transporter = null
1408
+ await initMailer({ throwErr: true })
1399
1409
  const sendResult = await transporter.sendMail({
1400
1410
  from: config.SENDER_EMAIL,
1401
1411
  to: event.mail || config.BLOGGER_EMAIL || config.SENDER_EMAIL,
@@ -1417,21 +1427,16 @@ async function uploadImage (event) {
1417
1427
  const { photo, fileName } = event
1418
1428
  const res = {}
1419
1429
  try {
1420
- if (!config.IMAGE_CDN_TOKEN) {
1430
+ if (!config.IMAGE_CDN || !config.IMAGE_CDN_TOKEN) {
1421
1431
  throw new Error('未配置图片上传服务')
1422
1432
  }
1423
- const formData = new FormData()
1424
- formData.append('image', base64UrlToReadStream(photo, fileName))
1425
- const uploadResult = await axios.post('https://7bu.top/api/upload', formData, {
1426
- headers: {
1427
- ...formData.getHeaders(),
1428
- token: config.IMAGE_CDN_TOKEN
1429
- }
1430
- })
1431
- if (uploadResult.data.code === 200) {
1432
- res.data = uploadResult.data.data
1433
- } else {
1434
- throw new Error(uploadResult.data.msg)
1433
+ // tip: qcloud 图床走前端上传,其他图床走后端上传
1434
+ if (config.IMAGE_CDN === '7bu') {
1435
+ await uploadImageTo7Bu({ photo, fileName, config, res })
1436
+ } else if (config.IMAGE_CDN === 'smms') {
1437
+ await uploadImageToSmms({ photo, fileName, config, res })
1438
+ } else if (isUrl(config.IMAGE_CDN)) {
1439
+ await uploadImageToLskyPro({ photo, fileName, config, res })
1435
1440
  }
1436
1441
  } catch (e) {
1437
1442
  console.error(e)
@@ -1441,6 +1446,64 @@ async function uploadImage (event) {
1441
1446
  return res
1442
1447
  }
1443
1448
 
1449
+ async function uploadImageTo7Bu ({ photo, fileName, config, res }) {
1450
+ // 去不图床旧版本 https://7bu.top
1451
+ // TODO: 2022 年 4 月 30 日后去不图床将会升级新版本,此处逻辑要同步更新
1452
+ const formData = new FormData()
1453
+ formData.append('image', base64UrlToReadStream(photo, fileName))
1454
+ const uploadResult = await axios.post('https://7bu.top/api/upload', formData, {
1455
+ headers: {
1456
+ ...formData.getHeaders(),
1457
+ token: config.IMAGE_CDN_TOKEN
1458
+ }
1459
+ })
1460
+ if (uploadResult.data.code === 200) {
1461
+ res.data = uploadResult.data.data
1462
+ } else {
1463
+ throw new Error(uploadResult.data.msg)
1464
+ }
1465
+ }
1466
+
1467
+ async function uploadImageToSmms ({ photo, fileName, config, res }) {
1468
+ // SM.MS 图床 https://sm.ms
1469
+ const formData = new FormData()
1470
+ formData.append('smfile', base64UrlToReadStream(photo, fileName))
1471
+ const uploadResult = await axios.post('https://sm.ms/api/v2/upload', formData, {
1472
+ headers: {
1473
+ ...formData.getHeaders(),
1474
+ Authorization: config.IMAGE_CDN_TOKEN
1475
+ }
1476
+ })
1477
+ if (uploadResult.data.success) {
1478
+ res.data = uploadResult.data.data
1479
+ } else {
1480
+ throw new Error(uploadResult.data.message)
1481
+ }
1482
+ }
1483
+
1484
+ async function uploadImageToLskyPro ({ photo, fileName, config, res }) {
1485
+ // 自定义兰空图床(v2)URL
1486
+ const formData = new FormData()
1487
+ formData.append('file', base64UrlToReadStream(photo, fileName))
1488
+ const url = `${config.IMAGE_CDN}/api/v1/upload`
1489
+ let token = config.IMAGE_CDN_TOKEN
1490
+ if (!token.startsWith('Bearer')) {
1491
+ token = `Bearer ${token}`
1492
+ }
1493
+ const uploadResult = await axios.post(url, formData, {
1494
+ headers: {
1495
+ ...formData.getHeaders(),
1496
+ Authorization: token
1497
+ }
1498
+ })
1499
+ if (uploadResult.data.status) {
1500
+ res.data = uploadResult.data.data
1501
+ res.data.url = res.data.links.url
1502
+ } else {
1503
+ throw new Error(uploadResult.data.message)
1504
+ }
1505
+ }
1506
+
1444
1507
  function base64UrlToReadStream (base64Url, fileName) {
1445
1508
  const base64 = base64Url.split(';base64,').pop()
1446
1509
  const path = `/tmp/${fileName}`
@@ -1448,6 +1511,10 @@ function base64UrlToReadStream (base64Url, fileName) {
1448
1511
  return fs.createReadStream(path)
1449
1512
  }
1450
1513
 
1514
+ function isUrl (s) {
1515
+ return /^http(s)?:\/\//.test(s)
1516
+ }
1517
+
1451
1518
  function getAvatar (comment) {
1452
1519
  if (comment.avatar) {
1453
1520
  return comment.avatar
@@ -1504,7 +1571,8 @@ async function getConfig () {
1504
1571
  REQUIRED_FIELDS: config.REQUIRED_FIELDS,
1505
1572
  HIDE_ADMIN_CRYPT: config.HIDE_ADMIN_CRYPT,
1506
1573
  HIGHLIGHT: config.HIGHLIGHT || 'true',
1507
- HIGHLIGHT_THEME: config.HIGHLIGHT_THEME
1574
+ HIGHLIGHT_THEME: config.HIGHLIGHT_THEME,
1575
+ LIMIT_LENGTH: config.LIMIT_LENGTH
1508
1576
  }
1509
1577
  }
1510
1578
  }
@@ -1541,6 +1609,18 @@ async function setConfig (event) {
1541
1609
  }
1542
1610
  }
1543
1611
 
1612
+ function protect () {
1613
+ // 防御
1614
+ const ip = request.headers['x-real-ip']
1615
+ requestTimes[ip] = (requestTimes[ip] || 0) + 1
1616
+ if (requestTimes[ip] > MAX_REQUEST_TIMES) {
1617
+ console.log(`${ip} 当前请求次数为 ${requestTimes[ip]},已超过最大请求次数`)
1618
+ throw new Error('Too Many Requests')
1619
+ } else {
1620
+ console.log(`${ip} 当前请求次数为 ${requestTimes[ip]}`)
1621
+ }
1622
+ }
1623
+
1544
1624
  // 读取配置
1545
1625
  async function readConfig () {
1546
1626
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twikoo-vercel",
3
- "version": "1.5.1",
3
+ "version": "1.5.4",
4
4
  "description": "A simple comment system based on Tencent CloudBase (tcb).",
5
5
  "author": "imaegoo <hello@imaegoo.com> (https://github.com/imaegoo)",
6
6
  "license": "MIT",