twikoo-func 1.6.16 → 1.6.18
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 +17 -16
- package/package.json +2 -2
- package/utils/image.js +2 -1
- package/utils/index.js +15 -13
- package/utils/logger.js +22 -0
- package/utils/notify.js +21 -16
- package/utils/spam.js +5 -4
package/index.js
CHANGED
|
@@ -40,6 +40,7 @@ const {
|
|
|
40
40
|
const { postCheckSpam } = require('./utils/spam')
|
|
41
41
|
const { sendNotice, emailTest } = require('./utils/notify')
|
|
42
42
|
const { uploadImage } = require('./utils/image')
|
|
43
|
+
const logger = require('./utils/logger')
|
|
43
44
|
|
|
44
45
|
// 云函数 SDK / tencent cloudbase sdk
|
|
45
46
|
const app = tcb.init({ env: tcb.SYMBOL_CURRENT_ENV })
|
|
@@ -60,9 +61,9 @@ const requestTimes = {}
|
|
|
60
61
|
|
|
61
62
|
// 云函数入口点 / entry point
|
|
62
63
|
exports.main = async (event, context) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
logger.log('请求 IP:', auth.getClientIP())
|
|
65
|
+
logger.log('请求函数:', event.event)
|
|
66
|
+
logger.log('请求参数:', event)
|
|
66
67
|
let res = {}
|
|
67
68
|
try {
|
|
68
69
|
protect()
|
|
@@ -141,13 +142,13 @@ exports.main = async (event, context) => {
|
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
} catch (e) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
logger.error('Twikoo 遇到错误,请参考以下错误信息。如有疑问,请反馈至 https://github.com/imaegoo/twikoo/issues')
|
|
146
|
+
logger.error('请求参数:', event)
|
|
147
|
+
logger.error('错误信息:', e)
|
|
147
148
|
res.code = RES_CODE.FAIL
|
|
148
149
|
res.message = e.message
|
|
149
150
|
}
|
|
150
|
-
|
|
151
|
+
logger.log('请求返回:', res)
|
|
151
152
|
return res
|
|
152
153
|
}
|
|
153
154
|
|
|
@@ -182,7 +183,7 @@ async function checkAndSaveCredentials (credentials) {
|
|
|
182
183
|
await writeConfig({ CREDENTIALS: credentials })
|
|
183
184
|
return true
|
|
184
185
|
} catch (e) {
|
|
185
|
-
|
|
186
|
+
logger.error('私钥文件异常:', e)
|
|
186
187
|
return false
|
|
187
188
|
}
|
|
188
189
|
}
|
|
@@ -452,7 +453,7 @@ async function commentImportForAdmin (event) {
|
|
|
452
453
|
}
|
|
453
454
|
res.code = RES_CODE.SUCCESS
|
|
454
455
|
res.log = logText
|
|
455
|
-
|
|
456
|
+
logger.info(logText)
|
|
456
457
|
} else {
|
|
457
458
|
res.code = RES_CODE.NEED_LOGIN
|
|
458
459
|
res.message = '请先登录'
|
|
@@ -572,7 +573,7 @@ async function commentSubmit (event, context) {
|
|
|
572
573
|
data: { event: 'POST_SUBMIT', comment }
|
|
573
574
|
}, { timeout: 300 }) // 设置较短的 timeout 来实现异步
|
|
574
575
|
} catch (e) {
|
|
575
|
-
|
|
576
|
+
logger.log('开始异步垃圾检测、发送评论通知')
|
|
576
577
|
}
|
|
577
578
|
return res
|
|
578
579
|
}
|
|
@@ -836,10 +837,10 @@ function protect () {
|
|
|
836
837
|
const ip = auth.getClientIP()
|
|
837
838
|
requestTimes[ip] = (requestTimes[ip] || 0) + 1
|
|
838
839
|
if (requestTimes[ip] > MAX_REQUEST_TIMES) {
|
|
839
|
-
|
|
840
|
+
logger.warn(`${ip} 当前请求次数为 ${requestTimes[ip]},已超过最大请求次数`)
|
|
840
841
|
throw new Error('Too Many Requests')
|
|
841
842
|
} else {
|
|
842
|
-
|
|
843
|
+
logger.log(`${ip} 当前请求次数为 ${requestTimes[ip]}`)
|
|
843
844
|
}
|
|
844
845
|
}
|
|
845
846
|
|
|
@@ -853,7 +854,7 @@ async function readConfig () {
|
|
|
853
854
|
config = res.data[0] || {}
|
|
854
855
|
return config
|
|
855
856
|
} catch (e) {
|
|
856
|
-
|
|
857
|
+
logger.error('读取配置失败:', e)
|
|
857
858
|
await createCollections()
|
|
858
859
|
config = {}
|
|
859
860
|
return config
|
|
@@ -863,7 +864,7 @@ async function readConfig () {
|
|
|
863
864
|
// 写入配置
|
|
864
865
|
async function writeConfig (newConfig) {
|
|
865
866
|
if (!Object.keys(newConfig).length) return 0
|
|
866
|
-
|
|
867
|
+
logger.info('写入配置:', newConfig)
|
|
867
868
|
try {
|
|
868
869
|
let updated
|
|
869
870
|
let res = await db
|
|
@@ -882,7 +883,7 @@ async function writeConfig (newConfig) {
|
|
|
882
883
|
if (updated > 0) config = null
|
|
883
884
|
return updated
|
|
884
885
|
} catch (e) {
|
|
885
|
-
|
|
886
|
+
logger.error('写入配置失败:', e)
|
|
886
887
|
return null
|
|
887
888
|
}
|
|
888
889
|
}
|
|
@@ -913,7 +914,7 @@ async function createCollections () {
|
|
|
913
914
|
try {
|
|
914
915
|
res[collection] = await db.createCollection(collection)
|
|
915
916
|
} catch (e) {
|
|
916
|
-
|
|
917
|
+
logger.error('建立数据库失败:', e)
|
|
917
918
|
}
|
|
918
919
|
}
|
|
919
920
|
return res
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "twikoo-func",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.18",
|
|
4
4
|
"description": "A simple comment system.",
|
|
5
5
|
"author": "imaegoo <hello@imaegoo.com> (https://github.com/imaegoo)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,6 +27,6 @@
|
|
|
27
27
|
"nodemailer": "^6.4.17",
|
|
28
28
|
"pushoo": "latest",
|
|
29
29
|
"tencentcloud-sdk-nodejs": "^4.0.65",
|
|
30
|
-
"xml2js": "^0.
|
|
30
|
+
"xml2js": "^0.6.0"
|
|
31
31
|
}
|
|
32
32
|
}
|
package/utils/image.js
CHANGED
|
@@ -4,6 +4,7 @@ const path = require('path')
|
|
|
4
4
|
const { isUrl } = require('.')
|
|
5
5
|
const { RES_CODE } = require('./constants')
|
|
6
6
|
const { axios, FormData } = require('./lib')
|
|
7
|
+
const logger = require('./logger')
|
|
7
8
|
|
|
8
9
|
const fn = {
|
|
9
10
|
async uploadImage (event, config) {
|
|
@@ -22,7 +23,7 @@ const fn = {
|
|
|
22
23
|
await fn.uploadImageToLskyPro({ photo, fileName, config, res, imageCdn: config.IMAGE_CDN })
|
|
23
24
|
}
|
|
24
25
|
} catch (e) {
|
|
25
|
-
|
|
26
|
+
logger.error(e)
|
|
26
27
|
res.code = RES_CODE.UPLOAD_FAILED
|
|
27
28
|
res.err = e.message
|
|
28
29
|
}
|
package/utils/index.js
CHANGED
|
@@ -2,6 +2,7 @@ const { URL } = require('url')
|
|
|
2
2
|
const { axios, bowser, ipToRegion, md5 } = require('./lib')
|
|
3
3
|
const { RES_CODE } = require('./constants')
|
|
4
4
|
const ipRegionSearcher = ipToRegion.create() // 初始化 IP 属地
|
|
5
|
+
const logger = require('./logger')
|
|
5
6
|
|
|
6
7
|
const fn = {
|
|
7
8
|
// 获取 Twikoo 云函数版本
|
|
@@ -48,7 +49,7 @@ const fn = {
|
|
|
48
49
|
displayOs = [os.name, os.versionName ? os.versionName : os.version].join(' ')
|
|
49
50
|
displayBrowser = [ua.getBrowserName(), ua.getBrowserVersion()].join(' ')
|
|
50
51
|
} catch (e) {
|
|
51
|
-
|
|
52
|
+
logger.warn('bowser 错误:', e)
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
const showRegion = !!config.SHOW_REGION && config.SHOW_REGION !== 'false'
|
|
@@ -90,17 +91,19 @@ const fn = {
|
|
|
90
91
|
try {
|
|
91
92
|
// 将 IPv6 格式的 IPv4 地址转换为 IPv4 格式
|
|
92
93
|
ip = ip.replace(/^::ffff:/, '')
|
|
94
|
+
// Zeabur 返回的地址带端口号,去掉端口号。TODO: 不知道该怎么去掉 IPv6 地址后面的端口号
|
|
95
|
+
ip = ip.replace(/:[0-9]*$/, '')
|
|
93
96
|
const { region } = ipRegionSearcher.binarySearchSync(ip)
|
|
94
97
|
const [country,, province, city, isp] = region.split('|')
|
|
95
98
|
// 有省显示省,没有省显示国家
|
|
96
|
-
const area = province.trim() ? province : country
|
|
99
|
+
const area = province.trim() && province !== '0' ? province : country
|
|
97
100
|
if (detail) {
|
|
98
101
|
return area === city ? [city, isp].join(' ') : [area, city, isp].join(' ')
|
|
99
102
|
} else {
|
|
100
103
|
return area.replace(/(省|市)$/, '')
|
|
101
104
|
}
|
|
102
105
|
} catch (e) {
|
|
103
|
-
|
|
106
|
+
logger.warn('IP 属地查询失败:', e.message, ip)
|
|
104
107
|
return ''
|
|
105
108
|
}
|
|
106
109
|
},
|
|
@@ -151,15 +154,13 @@ const fn = {
|
|
|
151
154
|
async getQQAvatar (qq) {
|
|
152
155
|
try {
|
|
153
156
|
const qqNum = qq.replace(/@qq.com/ig, '')
|
|
154
|
-
const result = await axios.get(`https://
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return result.data.substring(start, end)
|
|
160
|
-
}
|
|
157
|
+
const result = await axios.get(`https://s.p.qq.com/pub/get_face?img_type=4&uin=${qqNum}`, {
|
|
158
|
+
maxRedirects: 0,
|
|
159
|
+
validateStatus: status => [301, 302, 307, 308].includes(status)
|
|
160
|
+
})
|
|
161
|
+
return result?.headers?.location || null
|
|
161
162
|
} catch (e) {
|
|
162
|
-
|
|
163
|
+
logger.warn('获取 QQ 头像失败:', e)
|
|
163
164
|
}
|
|
164
165
|
},
|
|
165
166
|
// 判断是否存在管理员密码
|
|
@@ -181,13 +182,13 @@ const fn = {
|
|
|
181
182
|
}
|
|
182
183
|
if (config.AKISMET_KEY === 'MANUAL_REVIEW') {
|
|
183
184
|
// 人工审核
|
|
184
|
-
|
|
185
|
+
logger.info('已使用人工审核模式,评论审核后才会发表~')
|
|
185
186
|
return true
|
|
186
187
|
} else if (config.FORBIDDEN_WORDS) {
|
|
187
188
|
// 违禁词检测
|
|
188
189
|
for (const forbiddenWord of config.FORBIDDEN_WORDS.split(',')) {
|
|
189
190
|
if (comment.indexOf(forbiddenWord.trim()) !== -1 || nick.indexOf(forbiddenWord.trim()) !== -1) {
|
|
190
|
-
|
|
191
|
+
logger.warn('包含违禁词,直接标记为垃圾评论~')
|
|
191
192
|
return true
|
|
192
193
|
}
|
|
193
194
|
}
|
|
@@ -211,6 +212,7 @@ const fn = {
|
|
|
211
212
|
SHOW_EMOTION: config.SHOW_EMOTION || 'true',
|
|
212
213
|
EMOTION_CDN: config.EMOTION_CDN,
|
|
213
214
|
COMMENT_PLACEHOLDER: config.COMMENT_PLACEHOLDER,
|
|
215
|
+
DISPLAYED_FIELDS: config.DISPLAYED_FIELDS,
|
|
214
216
|
REQUIRED_FIELDS: config.REQUIRED_FIELDS,
|
|
215
217
|
HIDE_ADMIN_CRYPT: config.HIDE_ADMIN_CRYPT,
|
|
216
218
|
HIGHLIGHT: config.HIGHLIGHT || 'true',
|
package/utils/logger.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
let envLogLevel = process.env.TWIKOO_LOG_LEVEL || 'info'
|
|
2
|
+
envLogLevel = envLogLevel.toLowerCase()
|
|
3
|
+
const logLevel = { verbose: 1, info: 2, warn: 3, error: 4 }[envLogLevel] || 2
|
|
4
|
+
|
|
5
|
+
const logger = {
|
|
6
|
+
log: (...messages) => {
|
|
7
|
+
if (logLevel <= 1) console.log(logPrefix(), ...messages)
|
|
8
|
+
},
|
|
9
|
+
info: (...messages) => {
|
|
10
|
+
if (logLevel <= 2) console.info(logPrefix(), ...messages)
|
|
11
|
+
},
|
|
12
|
+
warn: (...messages) => {
|
|
13
|
+
if (logLevel <= 3) console.warn(logPrefix(), ...messages)
|
|
14
|
+
},
|
|
15
|
+
error: (...messages) => {
|
|
16
|
+
if (logLevel <= 4) console.error(logPrefix(), ...messages)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const logPrefix = () => `${new Date().toLocaleString()} Twikoo:`
|
|
21
|
+
|
|
22
|
+
module.exports = logger
|
package/utils/notify.js
CHANGED
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
pushoo
|
|
6
6
|
} = require('./lib')
|
|
7
7
|
const { RES_CODE } = require('./constants')
|
|
8
|
+
const logger = require('./logger')
|
|
8
9
|
|
|
9
10
|
let transporter
|
|
10
11
|
|
|
@@ -17,7 +18,7 @@ const fn = {
|
|
|
17
18
|
fn.noticeReply(comment, config, getParentComment),
|
|
18
19
|
fn.noticePushoo(comment, config)
|
|
19
20
|
]).catch(err => {
|
|
20
|
-
|
|
21
|
+
logger.error('通知异常:', err)
|
|
21
22
|
})
|
|
22
23
|
},
|
|
23
24
|
// 初始化邮件插件
|
|
@@ -44,31 +45,35 @@ const fn = {
|
|
|
44
45
|
transporter = nodemailer.createTransport(transportConfig)
|
|
45
46
|
try {
|
|
46
47
|
const success = await transporter.verify()
|
|
47
|
-
if (success)
|
|
48
|
+
if (success) logger.info('SMTP 邮箱配置正常')
|
|
48
49
|
} catch (error) {
|
|
49
50
|
throw new Error('SMTP 邮箱配置异常:', error)
|
|
50
51
|
}
|
|
51
52
|
return true
|
|
52
53
|
} catch (e) {
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
if (throwErr) {
|
|
55
|
+
logger.error('邮件初始化异常:', e.message)
|
|
56
|
+
throw e
|
|
57
|
+
} else {
|
|
58
|
+
logger.warn('邮件初始化异常:', e.message)
|
|
59
|
+
}
|
|
55
60
|
return false
|
|
56
61
|
}
|
|
57
62
|
},
|
|
58
63
|
// 博主通知
|
|
59
64
|
async noticeMaster (comment, config) {
|
|
60
65
|
if (!transporter && !await fn.initMailer({ config })) {
|
|
61
|
-
|
|
66
|
+
logger.info('未配置邮箱或邮箱配置有误,不通知')
|
|
62
67
|
return
|
|
63
68
|
}
|
|
64
69
|
if (config.BLOGGER_EMAIL && config.BLOGGER_EMAIL === comment.mail) {
|
|
65
|
-
|
|
70
|
+
logger.info('博主本人评论,不发送通知给博主')
|
|
66
71
|
return
|
|
67
72
|
}
|
|
68
73
|
// 判断是否存在即时消息推送配置
|
|
69
74
|
const hasIMPushConfig = config.PUSHOO_CHANNEL && config.PUSHOO_TOKEN
|
|
70
75
|
if (hasIMPushConfig && config.SC_MAIL_NOTIFY !== 'true') {
|
|
71
|
-
|
|
76
|
+
logger.info('存在即时消息推送配置,默认不发送邮件给博主,您可以在管理面板修改此行为')
|
|
72
77
|
return
|
|
73
78
|
}
|
|
74
79
|
const SITE_NAME = config.SITE_NAME
|
|
@@ -113,17 +118,17 @@ const fn = {
|
|
|
113
118
|
} catch (e) {
|
|
114
119
|
sendResult = e
|
|
115
120
|
}
|
|
116
|
-
|
|
121
|
+
logger.log('博主通知结果:', sendResult)
|
|
117
122
|
return sendResult
|
|
118
123
|
},
|
|
119
124
|
// 即时消息通知
|
|
120
125
|
async noticePushoo (comment, config) {
|
|
121
126
|
if (!config.PUSHOO_CHANNEL || !config.PUSHOO_TOKEN) {
|
|
122
|
-
|
|
127
|
+
logger.info('没有配置 pushoo,放弃即时消息通知')
|
|
123
128
|
return
|
|
124
129
|
}
|
|
125
130
|
if (config.BLOGGER_EMAIL && config.BLOGGER_EMAIL === comment.mail) {
|
|
126
|
-
|
|
131
|
+
logger.info('博主本人评论,不发送通知给博主')
|
|
127
132
|
return
|
|
128
133
|
}
|
|
129
134
|
const pushContent = fn.getIMPushContent(comment, config)
|
|
@@ -137,7 +142,7 @@ const fn = {
|
|
|
137
142
|
}
|
|
138
143
|
}
|
|
139
144
|
})
|
|
140
|
-
|
|
145
|
+
logger.info('即时消息通知结果:', sendResult)
|
|
141
146
|
},
|
|
142
147
|
// 即时消息推送内容获取
|
|
143
148
|
getIMPushContent (comment, config) {
|
|
@@ -165,20 +170,20 @@ const fn = {
|
|
|
165
170
|
// 回复通知
|
|
166
171
|
async noticeReply (currentComment, config, getParentComment) {
|
|
167
172
|
if (!currentComment.pid) {
|
|
168
|
-
|
|
173
|
+
logger.info('无父级评论,不通知')
|
|
169
174
|
return
|
|
170
175
|
}
|
|
171
176
|
if (!transporter && !await fn.initMailer({ config })) {
|
|
172
|
-
|
|
177
|
+
logger.info('未配置邮箱或邮箱配置有误,不通知')
|
|
173
178
|
return
|
|
174
179
|
}
|
|
175
180
|
const parentComment = await getParentComment(currentComment)
|
|
176
181
|
if (config.BLOGGER_EMAIL === parentComment.mail) {
|
|
177
|
-
|
|
182
|
+
logger.info('回复给博主,因为会发博主通知邮件,所以不再重复通知')
|
|
178
183
|
return
|
|
179
184
|
}
|
|
180
185
|
if (currentComment.mail === parentComment.mail) {
|
|
181
|
-
|
|
186
|
+
logger.info('回复自己的评论,不邮件通知')
|
|
182
187
|
return
|
|
183
188
|
}
|
|
184
189
|
const PARENT_NICK = parentComment.nick
|
|
@@ -232,7 +237,7 @@ const fn = {
|
|
|
232
237
|
} catch (e) {
|
|
233
238
|
sendResult = e
|
|
234
239
|
}
|
|
235
|
-
|
|
240
|
+
logger.log('回复通知结果:', sendResult)
|
|
236
241
|
return sendResult
|
|
237
242
|
},
|
|
238
243
|
appendHashToUrl (url, hash) {
|
package/utils/spam.js
CHANGED
|
@@ -3,6 +3,7 @@ const {
|
|
|
3
3
|
CryptoJS,
|
|
4
4
|
tencentcloud
|
|
5
5
|
} = require('./lib')
|
|
6
|
+
const logger = require('./logger')
|
|
6
7
|
|
|
7
8
|
const fn = {
|
|
8
9
|
// 后垃圾评论检测
|
|
@@ -24,7 +25,7 @@ const fn = {
|
|
|
24
25
|
Device: { IP: comment.ip },
|
|
25
26
|
User: { Nickname: comment.nick }
|
|
26
27
|
})
|
|
27
|
-
|
|
28
|
+
logger.log('腾讯云返回结果:', checkResult)
|
|
28
29
|
isSpam = checkResult.EvilFlag !== 0
|
|
29
30
|
} else if (config.AKISMET_KEY) {
|
|
30
31
|
// Akismet
|
|
@@ -34,7 +35,7 @@ const fn = {
|
|
|
34
35
|
})
|
|
35
36
|
const isValid = await akismetClient.verifyKey()
|
|
36
37
|
if (!isValid) {
|
|
37
|
-
|
|
38
|
+
logger.warn('Akismet key 不可用:', config.AKISMET_KEY)
|
|
38
39
|
return
|
|
39
40
|
}
|
|
40
41
|
isSpam = await akismetClient.checkSpam({
|
|
@@ -48,10 +49,10 @@ const fn = {
|
|
|
48
49
|
comment_content: comment.comment
|
|
49
50
|
})
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
+
logger.log('垃圾评论检测结果:', isSpam)
|
|
52
53
|
return isSpam
|
|
53
54
|
} catch (err) {
|
|
54
|
-
|
|
55
|
+
logger.error('垃圾评论检测异常:', err)
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
}
|