twikoo-func 1.7.24 → 2.0.0-beta.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/utils/index.js DELETED
@@ -1,512 +0,0 @@
1
- const { URL } = require('url')
2
- const {
3
- getAxios,
4
- getFormData,
5
- getBowser,
6
- getIpToRegion,
7
- getMd5,
8
- getSha256
9
- } = require('./lib')
10
- const axios = getAxios()
11
- const FormData = getFormData()
12
- const bowser = getBowser()
13
- const md5 = getMd5()
14
- const sha256 = getSha256()
15
- const { RES_CODE } = require('./constants')
16
- const logger = require('./logger')
17
-
18
- let ipRegionSearcher
19
-
20
- // IP 属地查询
21
- function getIpRegionSearcher () {
22
- if (!ipRegionSearcher) {
23
- const ipToRegion = getIpToRegion()
24
- ipRegionSearcher = ipToRegion.create() // 初始化 IP 属地
25
- }
26
- return ipRegionSearcher
27
- }
28
-
29
- const fn = {
30
- // 获取 Twikoo 云函数版本
31
- getFuncVersion (VERSION) {
32
- return {
33
- code: RES_CODE.SUCCESS,
34
- version: VERSION
35
- }
36
- },
37
- // 同时查询 /path 和 /path/ 的评论
38
- getUrlQuery (url) {
39
- const variantUrl = url[url.length - 1] === '/' ? url.substring(0, url.length - 1) : `${url}/`
40
- return [url, variantUrl]
41
- },
42
- getUrlsQuery (urls) {
43
- const query = []
44
- for (const url of urls) {
45
- if (url) query.push(...fn.getUrlQuery(url))
46
- }
47
- return query
48
- },
49
- // 筛除隐私字段,拼接回复列表
50
- parseComment (comments, uid, config) {
51
- const result = []
52
- for (const comment of comments) {
53
- if (!comment.rid) {
54
- const replies = comments
55
- .filter((item) => item.rid === comment._id.toString())
56
- .map((item) => fn.toCommentDto(item, uid, [], comments, config))
57
- .sort((a, b) => a.created - b.created)
58
- result.push(fn.toCommentDto(comment, uid, replies, [], config))
59
- }
60
- }
61
- return result
62
- },
63
- // 将评论记录转换为前端需要的格式
64
- toCommentDto (comment, uid, replies = [], comments = [], config) {
65
- let displayOs = ''
66
- let displayBrowser = ''
67
- if (config.SHOW_UA !== 'false') {
68
- try {
69
- const ua = bowser.getParser(comment.ua)
70
- const os = fn.fixOS(ua)
71
- displayOs = [os.name, os.versionName ? os.versionName : os.version].join(' ')
72
- displayBrowser = [ua.getBrowserName(), ua.getBrowserVersion()].join(' ')
73
- } catch (e) {
74
- logger.warn('bowser 错误:', e)
75
- }
76
- }
77
- const showRegion = !!config.SHOW_REGION && config.SHOW_REGION !== 'false'
78
- const ups = comment.ups || []
79
- const downs = comment.downs || []
80
- return {
81
- id: comment._id.toString(),
82
- nick: comment.nick,
83
- avatar: comment.avatar,
84
- mailMd5: fn.getMailMd5(comment),
85
- link: comment.link,
86
- comment: comment.comment,
87
- os: displayOs,
88
- browser: displayBrowser,
89
- ipRegion: showRegion ? fn.getIpRegion({ ip: comment.ip }) : '',
90
- master: comment.master,
91
- like: comment.like ? comment.like.length : 0,
92
- ups: ups.length,
93
- downs: downs.length,
94
- liked: ups.includes(uid),
95
- disliked: downs.includes(uid),
96
- replies,
97
- rid: comment.rid,
98
- pid: comment.pid,
99
- ruser: fn.ruser(comment.pid, comments),
100
- top: comment.top,
101
- isSpam: comment.isSpam,
102
- isOwner: Boolean(uid && comment.uid === uid),
103
- created: comment.created,
104
- updated: comment.updated
105
- }
106
- },
107
- fixOS (ua) {
108
- const os = ua.getOS()
109
- if (!os.versionName) {
110
- // fix version name of Win 11 & macOS ^11 & Android ^10
111
- if (os.name === 'Windows' && os.version === 'NT 11.0') {
112
- os.versionName = '11'
113
- } else if (os.name === 'macOS') {
114
- const majorPlatformVersion = os.version.split('.')[0]
115
- os.versionName = {
116
- 11: 'Big Sur',
117
- 12: 'Monterey',
118
- 13: 'Ventura',
119
- 14: 'Sonoma',
120
- 15: 'Sequoia',
121
- 16: 'Tahoe'
122
- }[majorPlatformVersion]
123
- } else if (os.name === 'Android') {
124
- const majorPlatformVersion = os.version.split('.')[0]
125
- os.versionName = {
126
- 10: 'Quince Tart',
127
- 11: 'Red Velvet Cake',
128
- 12: 'Snow Cone',
129
- 13: 'Tiramisu',
130
- 14: 'Upside Down Cake',
131
- 15: 'Vanilla Ice Cream',
132
- 16: 'Baklava'
133
- }[majorPlatformVersion]
134
- } else if (ua.test(/harmony/i)) {
135
- os.name = 'Harmony'
136
- os.version = fn.getFirstMatch(/harmony[\s/-](\d+(\.\d+)*)/i, ua.getUA())
137
- os.versionName = ''
138
- }
139
- }
140
- return os
141
- },
142
- /**
143
- * Get first matched item for a string
144
- * @param {RegExp} regexp
145
- * @param {String} ua
146
- * @return {Array|{index: number, input: string}|*|boolean|string}
147
- */
148
- getFirstMatch (regexp, ua) {
149
- const match = ua.match(regexp)
150
- return (match && match.length > 0 && match[1]) || ''
151
- },
152
- // 获取回复人昵称 / Get replied user nick name
153
- ruser (pid, comments = []) {
154
- const comment = comments.find((item) => item._id === pid)
155
- return comment ? comment.nick : null
156
- },
157
- /**
158
- * 获取 IP 属地
159
- * @param detail true 返回省市运营商,false 只返回省
160
- * @returns {String}
161
- */
162
- getIpRegion ({ ip, detail = false }) {
163
- if (!ip) return ''
164
- try {
165
- // 将 IPv6 格式的 IPv4 地址转换为 IPv4 格式
166
- ip = ip.replace(/^::ffff:/, '')
167
- // Zeabur 返回的地址带端口号,去掉端口号。TODO: 不知道该怎么去掉 IPv6 地址后面的端口号
168
- ip = ip.replace(/:[0-9]*$/, '')
169
- const { region } = getIpRegionSearcher().binarySearchSync(ip)
170
- const [country,, province, city, isp] = region.split('|')
171
- // 有省显示省,没有省显示国家
172
- const area = province.trim() && province !== '0' ? province : country
173
- if (detail) {
174
- return area === city ? [city, isp].join(' ') : [area, city, isp].join(' ')
175
- } else {
176
- return area.replace(/(省|市)$/, '')
177
- }
178
- } catch (e) {
179
- logger.warn('IP 属地查询失败:', e.message, ip)
180
- return ''
181
- }
182
- },
183
- parseCommentForAdmin (comments) {
184
- for (const comment of comments) {
185
- comment.ipRegion = fn.getIpRegion({ ip: comment.ip, detail: true })
186
- }
187
- return comments
188
- },
189
- getRelativeUrl (url) {
190
- try {
191
- return (new URL(url)).pathname
192
- } catch (e) {
193
- // 如果 url 已经是一个相对地址了,会报 ERR_INVALID_URL,返回原始 url 就行
194
- return url
195
- }
196
- },
197
- normalizeMail (mail) {
198
- return String(mail).trim().toLowerCase()
199
- },
200
- equalsMail (mail1, mail2) {
201
- if (!mail1 || !mail2) return false
202
- return fn.normalizeMail(mail1) === fn.normalizeMail(mail2)
203
- },
204
- getMailMd5 (comment) {
205
- if (comment.mailMd5) {
206
- return comment.mailMd5
207
- }
208
- if (comment.mail) {
209
- return md5(fn.normalizeMail(comment.mail))
210
- }
211
- return md5(comment.nick)
212
- },
213
- getMailSha256 (comment) {
214
- if (comment.mail) {
215
- return sha256(fn.normalizeMail(comment.mail))
216
- }
217
- return sha256(comment.nick)
218
- },
219
- getAvatar (comment, config) {
220
- if (comment.avatar) {
221
- return comment.avatar
222
- } else {
223
- const gravatarCdn = config.GRAVATAR_CDN || 'weavatar.com'
224
- let defaultGravatar = `initials&name=${comment.nick}`
225
- if (config.DEFAULT_GRAVATAR) {
226
- defaultGravatar = config.DEFAULT_GRAVATAR
227
- }
228
- const mailHash = gravatarCdn === 'cravatar.cn' ? fn.getMailMd5(comment) : fn.getMailSha256(comment) // Cravatar 不支持 sha256
229
- return `https://${gravatarCdn}/avatar/${mailHash}?d=${defaultGravatar}`
230
- }
231
- },
232
- isUrl (s) {
233
- return /^http(s)?:\/\//.test(s)
234
- },
235
- isValidEmail (mail) {
236
- if (!mail || typeof mail !== 'string') return false
237
- const trimmed = mail.trim()
238
- if (!trimmed) return false
239
- // Reject emails with characters that could trigger nodemailer addressparser group parsing (CVE-2025-14874)
240
- if (trimmed.indexOf(':') !== -1) return false
241
- if (trimmed.indexOf(' ') !== -1) return false
242
- if (trimmed.indexOf(';') !== -1) return false
243
- // Basic email format validation
244
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)
245
- },
246
- isQQ (mail) {
247
- return /^[1-9][0-9]{4,10}$/.test(mail) ||
248
- /^[1-9][0-9]{4,10}@qq.com$/i.test(mail)
249
- },
250
- addQQMailSuffix (mail) {
251
- if (/^[1-9][0-9]{4,10}$/.test(mail)) return `${mail}@qq.com`
252
- else return mail
253
- },
254
- async getQQAvatar (qq) {
255
- try {
256
- const qqNum = qq.replace(/@qq.com/ig, '')
257
- // TODO: 这个接口已经失效了,暂时找不到新的接口
258
- const result = await axios.get(`https://aq.qq.com/cn2/get_img/get_face?img_type=3&uin=${qqNum}`)
259
- return result.data?.url || null
260
- } catch (e) {
261
- logger.warn('获取 QQ 头像失败:', e)
262
- }
263
- },
264
- async getQQNick (qq, qqApiKey) {
265
- try {
266
- const qqNum = qq.replace(/@qq.com/ig, '')
267
- const headers = {}
268
- if (qqApiKey) {
269
- headers.Authorization = `Bearer ${qqApiKey}`
270
- }
271
- const result = await axios.get(`https://v1.tqq.me/v1/qqname?qq=${qqNum}`, { headers })
272
- if (result.data?.code === 200 && result.data?.data?.nick) {
273
- return result.data.data.nick
274
- }
275
- return null
276
- } catch (e) {
277
- logger.warn('获取 QQ 昵称失败:', e)
278
- return null
279
- }
280
- },
281
- // 判断是否存在管理员密码
282
- async getPasswordStatus (config, version) {
283
- return {
284
- code: RES_CODE.SUCCESS,
285
- status: !!config.ADMIN_PASS,
286
- credentials: !!config.CREDENTIALS,
287
- version
288
- }
289
- },
290
- // 预垃圾评论检测
291
- preCheckSpam ({ comment, nick, link, mail }, config) {
292
- // 长度限制
293
- let limitLength = parseInt(config.LIMIT_LENGTH)
294
- if (Number.isNaN(limitLength)) limitLength = 500
295
- if (limitLength && comment.length > limitLength) {
296
- throw new Error('评论内容过长')
297
- }
298
- if (config.BLOCKED_WORDS) {
299
- const commentLowerCase = comment.toLowerCase()
300
- const nickLowerCase = nick.toLowerCase()
301
- for (const blockedWord of config.BLOCKED_WORDS.split(',')) {
302
- const blockedWordLowerCase = blockedWord.trim().toLowerCase()
303
- if (commentLowerCase.indexOf(blockedWordLowerCase) !== -1 || nickLowerCase.indexOf(blockedWordLowerCase) !== -1) {
304
- throw new Error('包含屏蔽词')
305
- }
306
- }
307
- }
308
- if (config.AKISMET_KEY === 'MANUAL_REVIEW') {
309
- // 人工审核
310
- logger.info('已使用人工审核模式,评论审核后才会发表~')
311
- return true
312
- } else if (config.FORBIDDEN_WORDS) {
313
- // 违禁词检测
314
- const commentLowerCase = comment.toLowerCase()
315
- const nickLowerCase = nick.toLowerCase()
316
- const linkLowerCase = (link || '').toLowerCase()
317
- const mailLowerCase = (mail || '').toLowerCase()
318
- for (const forbiddenWord of config.FORBIDDEN_WORDS.replace(/,+$/, '').split(',')) {
319
- const forbiddenWordLowerCase = forbiddenWord.trim().toLowerCase()
320
- if (commentLowerCase.indexOf(forbiddenWordLowerCase) !== -1 || nickLowerCase.indexOf(forbiddenWordLowerCase) !== -1 || linkLowerCase.indexOf(forbiddenWordLowerCase) !== -1 || mailLowerCase.indexOf(forbiddenWordLowerCase) !== -1) {
321
- logger.warn('包含违禁词,直接标记为垃圾评论~')
322
- return true
323
- }
324
- }
325
- }
326
- return false
327
- },
328
- async checkTurnstileCaptcha ({ ip, turnstileToken, turnstileTokenSecretKey }) {
329
- try {
330
- const formData = new FormData()
331
- formData.append('secret', turnstileTokenSecretKey)
332
- formData.append('response', turnstileToken)
333
- formData.append('remoteip', ip)
334
- const { data } = await axios.post('https://challenges.cloudflare.com/turnstile/v0/siteverify', formData, {
335
- headers: formData.getHeaders()
336
- })
337
- logger.log('验证码检测结果', data)
338
- if (!data.success) throw new Error('验证码错误')
339
- } catch (e) {
340
- throw new Error('验证码检测失败: ' + e.message)
341
- }
342
- },
343
- async checkGeeTestCaptcha ({ geeTestCaptchaId, geeTestCaptchaKey, geeTestLotNumber, geeTestCaptchaOutput, geeTestPassToken, geeTestGenTime }) {
344
- try {
345
- logger.log('极验验证参数:', { geeTestCaptchaId, geeTestCaptchaKey: geeTestCaptchaKey ? '***' : undefined, geeTestLotNumber })
346
- const crypto = require('crypto')
347
- const signToken = crypto
348
- .createHmac('sha256', geeTestCaptchaKey)
349
- .update(geeTestLotNumber)
350
- .digest('hex')
351
- const params = new URLSearchParams()
352
- params.append('lot_number', geeTestLotNumber)
353
- params.append('captcha_output', geeTestCaptchaOutput)
354
- params.append('pass_token', geeTestPassToken)
355
- params.append('gen_time', geeTestGenTime)
356
- params.append('sign_token', signToken)
357
- logger.log('极验请求参数:', params.toString())
358
- const url = `https://gcaptcha4.geetest.com/validate?captcha_id=${geeTestCaptchaId}`
359
- const { data } = await axios.post(url, params.toString(), {
360
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
361
- })
362
- logger.log('极验验证码检测结果', JSON.stringify(data))
363
- if (data.result !== 'success') {
364
- logger.error('极验验证失败详情:', data)
365
- throw new Error(data.reason || data.msg || '验证码错误')
366
- }
367
- } catch (e) {
368
- throw new Error('极验验证码检测失败: ' + e.message)
369
- }
370
- },
371
- async checkCapCaptcha ({ capToken, capSecretKey, capApiEndpoint, cap }) {
372
- try {
373
- // 内嵌 Cap:直接 validateToken,无需外部 Standalone
374
- if (cap) {
375
- const { validateToken } = require('./cap')
376
- const ok = await validateToken(cap, capToken)
377
- if (!ok) throw new Error('验证码错误')
378
- return
379
- }
380
- // 外部 Cap Standalone:HTTP siteverify
381
- const endpoint = capApiEndpoint.replace(/\/$/, '')
382
- const url = `${endpoint}/siteverify`
383
- logger.log('Cap验证码验证URL:', url)
384
- logger.log('Cap验证码验证参数:', { secret: capSecretKey ? '***' : undefined, response: capToken.substring(0, 20) + '...' })
385
- const { data } = await axios.post(url, {
386
- secret: capSecretKey,
387
- response: capToken
388
- }, {
389
- headers: { 'Content-Type': 'application/json' }
390
- })
391
- logger.log('Cap验证码检测结果', data)
392
- if (!data.success) throw new Error(data.error || '验证码错误')
393
- } catch (e) {
394
- throw new Error('Cap验证码检测失败: ' + e.message)
395
- }
396
- },
397
- async getConfig ({ config, VERSION, isAdmin }) {
398
- // 构建对外配置,避免在启用某一验证码供应商时泄露另一个供应商的 key
399
- const baseConfig = {
400
- VERSION,
401
- IS_ADMIN: isAdmin,
402
- SITE_NAME: config.SITE_NAME,
403
- SITE_URL: config.SITE_URL,
404
- MASTER_TAG: config.MASTER_TAG,
405
- COMMENT_BG_IMG: config.COMMENT_BG_IMG,
406
- GRAVATAR_CDN: config.GRAVATAR_CDN,
407
- DEFAULT_GRAVATAR: config.DEFAULT_GRAVATAR,
408
- SHOW_IMAGE: config.SHOW_IMAGE || 'true',
409
- IMAGE_CDN: config.IMAGE_CDN,
410
- LIGHTBOX: config.LIGHTBOX || 'false',
411
- SHOW_EMOTION: config.SHOW_EMOTION || 'true',
412
- EMOTION_CDN: config.EMOTION_CDN,
413
- COMMENT_PLACEHOLDER: config.COMMENT_PLACEHOLDER,
414
- SHOW_ORDER: config.SHOW_ORDER || 'true',
415
- SHOW_DISLIKE: config.SHOW_DISLIKE || 'true',
416
- DISPLAYED_FIELDS: config.DISPLAYED_FIELDS,
417
- REQUIRED_FIELDS: config.REQUIRED_FIELDS,
418
- HIDE_ADMIN_CRYPT: config.HIDE_ADMIN_CRYPT,
419
- HIGHLIGHT: config.HIGHLIGHT || 'true',
420
- HIGHLIGHT_THEME: config.HIGHLIGHT_THEME,
421
- HIGHLIGHT_PLUGIN: config.HIGHLIGHT_PLUGIN,
422
- LIMIT_LENGTH: config.LIMIT_LENGTH,
423
- CAPTCHA_PROVIDER: config.CAPTCHA_PROVIDER
424
- }
425
-
426
- // 仅在明确指定使用 Turnstile 时下发 Turnstile 的 site key
427
- if (config.CAPTCHA_PROVIDER === 'Turnstile') {
428
- baseConfig.TURNSTILE_SITE_KEY = config.TURNSTILE_SITE_KEY
429
- }
430
-
431
- // 仅在明确指定使用 Geetest 时下发 Geetest 的 id
432
- if (config.CAPTCHA_PROVIDER === 'Geetest') {
433
- baseConfig.GEETEST_CAPTCHA_ID = config.GEETEST_CAPTCHA_ID
434
- }
435
-
436
- // Cap:有外部 endpoint 则下发;否则标记 builtin,前端走 twikoo 事件代理
437
- if (config.CAPTCHA_PROVIDER === 'Cap') {
438
- if (config.CAP_API_ENDPOINT) {
439
- baseConfig.CAP_API_ENDPOINT = config.CAP_API_ENDPOINT
440
- } else {
441
- baseConfig.CAP_BUILTIN = true
442
- }
443
- }
444
-
445
- return {
446
- code: RES_CODE.SUCCESS,
447
- config: baseConfig
448
- }
449
- },
450
- async getConfigForAdmin ({ config, isAdmin }) {
451
- if (isAdmin) {
452
- delete config.CREDENTIALS
453
- return {
454
- code: RES_CODE.SUCCESS,
455
- config
456
- }
457
- } else {
458
- return {
459
- code: RES_CODE.NEED_LOGIN,
460
- message: '请先登录'
461
- }
462
- }
463
- },
464
- // 请求参数校验
465
- validate (event = {}, requiredParams = []) {
466
- for (const requiredParam of requiredParams) {
467
- if (!event[requiredParam]) {
468
- throw new Error(`参数"${requiredParam}"不合法`)
469
- }
470
- }
471
- },
472
- // 客户端字段类型校验,防止 NoSQL 查询条件注入。
473
- // 以下字段会直接参与数据库查询或作为评论归属标识,
474
- // 如果传入对象,会被数据库解释为查询操作符(如 $ne、$gt),
475
- // 导致越权删除评论、绕过评论可见性限制等问题。
476
- // 允许字段缺省或为 null(兼容首次匿名请求),有值时必须是字符串。
477
- validateClientFields (event = {}) {
478
- const stringFields = ['accessToken', 'id', 'url', 'pid', 'rid']
479
- for (const field of stringFields) {
480
- const value = event[field]
481
- if (value !== undefined && value !== null && typeof value !== 'string') {
482
- throw new Error(`参数"${field}"必须是字符串`)
483
- }
484
- }
485
- if (event.urls !== undefined && event.urls !== null) {
486
- if (!Array.isArray(event.urls) || event.urls.some((url) => typeof url !== 'string')) {
487
- throw new Error('参数"urls"必须是字符串数组')
488
- }
489
- }
490
- },
491
- // 校验评论归属:确认评论存在且属于当前用户
492
- async checkCommentOwnership (id, uid, getComment) {
493
- fn.validate({ id }, ['id'])
494
- // 兜底校验:id 必须是字符串,防止查询操作符对象注入 _id 条件,
495
- // 绕过归属校验后批量删除评论
496
- if (typeof id !== 'string') {
497
- throw new Error('参数"id"必须是字符串')
498
- }
499
- const comment = await getComment(id)
500
- if (!comment) {
501
- throw new Error('评论不存在')
502
- }
503
- // 无 token 的请求不具备任何归属权,必须直接拒绝,
504
- // 防止 comment.uid 与 uid 同时为 undefined 时误判为本人
505
- if (!uid || comment.uid !== uid) {
506
- throw new Error('只能删除自己的评论')
507
- }
508
- return comment
509
- }
510
- }
511
-
512
- module.exports = fn
package/utils/lib.js DELETED
@@ -1,79 +0,0 @@
1
- const crypto = require('crypto')
2
-
3
- let customLibs = {}
4
-
5
- module.exports = {
6
- setCustomLibs (libs) {
7
- customLibs = libs
8
- },
9
- getHtmlToText () {
10
- const { compile } = require('html-to-text') // HTML 转纯文本
11
- return compile({
12
- wordwrap: false,
13
- selectors: [
14
- { selector: 'a', options: { ignoreHref: true } },
15
- { selector: 'img', format: 'skip' }
16
- ]
17
- })
18
- },
19
- getAkismetClient () {
20
- const { AkismetClient } = require('akismet-api') // 反垃圾 API
21
- return AkismetClient
22
- },
23
- getFormData () {
24
- const FormData = require('form-data') // 图片上传
25
- return FormData
26
- },
27
- getAxios () {
28
- const axios = require('axios') // 发送 REST 请求
29
- return axios
30
- },
31
- getBowser () {
32
- const bowser = require('bowser') // UserAgent 格式化
33
- return bowser
34
- },
35
- getDomPurify () {
36
- if (customLibs.DOMPurify) return customLibs.DOMPurify
37
- // 初始化反 XSS
38
- const { JSDOM } = require('jsdom') // document.window 服务器版
39
- const createDOMPurify = require('dompurify') // 反 XSS
40
- const window = new JSDOM('').window
41
- const DOMPurify = createDOMPurify(window)
42
- return DOMPurify
43
- },
44
- getIpToRegion () {
45
- const ipToRegion = require('@imaegoo/node-ip2region') // IP 属地查询
46
- return ipToRegion
47
- },
48
- getMarked () {
49
- const marked = require('marked') // Markdown 解析
50
- return marked
51
- },
52
- getMd5 () {
53
- return (message) => {
54
- return crypto.createHash('md5').update(String(message)).digest('hex')
55
- }
56
- },
57
- getSha256 () {
58
- return (message) => {
59
- return crypto.createHash('sha256').update(message == null ? '' : String(message)).digest('hex')
60
- }
61
- },
62
- getNodemailer () {
63
- if (customLibs.nodemailer) return customLibs.nodemailer
64
- const nodemailer = require('nodemailer') // 发送邮件
65
- return nodemailer
66
- },
67
- getPushoo () {
68
- const pushoo = require('pushoo').default // 即时消息通知
69
- return pushoo
70
- },
71
- getTencentcloudTms () {
72
- const tencentcloudTms = require('tencentcloud-sdk-nodejs-tms') // 腾讯云文本内容安全 SDK
73
- return tencentcloudTms
74
- },
75
- getXml2js () {
76
- const xml2js = require('xml2js') // XML 解析
77
- return xml2js
78
- }
79
- }
package/utils/logger.js DELETED
@@ -1,22 +0,0 @@
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