tkserver
Version:
A simple comment system.
972 lines (925 loc) • 26.6 kB
JavaScript
/*!
* Twikoo self-hosted function
* (c) 2020-present iMaeGoo
* Released under the MIT License.
*/
const { version: VERSION } = require('./package.json')
const fs = require('fs')
const path = require('path')
const Loki = require('lokijs')
const getUserIP = require('get-user-ip')
const Lfsa = require('lokijs/src/loki-fs-structured-adapter')
const { v4: uuidv4 } = require('uuid') // 用户 id 生成
const {
getCheerio,
getDomPurify,
getMd5,
getSha256,
getXml2js
} = require('twikoo-func/utils/lib')
const {
getFuncVersion,
getUrlQuery,
getUrlsQuery,
parseComment,
parseCommentForAdmin,
normalizeMail,
equalsMail,
getMailMd5,
getAvatar,
isQQ,
addQQMailSuffix,
getQQAvatar,
getPasswordStatus,
preCheckSpam,
checkTurnstileCaptcha,
getConfig,
getConfigForAdmin,
validate
} = require('twikoo-func/utils')
const {
jsonParse,
commentImportValine,
commentImportDisqus,
commentImportArtalk,
commentImportArtalk2,
commentImportTwikoo
} = require('twikoo-func/utils/import')
const { postCheckSpam } = require('twikoo-func/utils/spam')
const { sendNotice, emailTest } = require('twikoo-func/utils/notify')
const { uploadImage } = require('twikoo-func/utils/image')
const logger = require('twikoo-func/utils/logger')
const $ = getCheerio()
const DOMPurify = getDomPurify()
const md5 = getMd5()
const sha256 = getSha256()
const xml2js = getXml2js()
// 常量 / constants
const { RES_CODE, MAX_REQUEST_TIMES } = require('twikoo-func/utils/constants')
const TWIKOO_REQ_TIMES_CLEAR_TIME = parseInt(process.env.TWIKOO_REQ_TIMES_CLEAR_TIME) || 10 * 60 * 1000
// 全局变量 / variables
let db = null
let config
let requestTimes = {}
connectToDatabase()
module.exports = async (request, response) => {
let accessToken
const event = request.body || {}
logger.log('请求 IP:', getIp(request))
logger.log('请求函数:', event.event)
logger.log('请求参数:', event)
let res = {}
try {
protect(request)
accessToken = anonymousSignIn(request)
await readConfig()
allowCors(request, response)
if (request.method === 'OPTIONS') {
response.status(204).end()
return
}
switch (event.event) {
case 'GET_FUNC_VERSION':
res = getFuncVersion({ VERSION })
break
case 'COMMENT_GET':
res = await commentGet(event)
break
case 'COMMENT_GET_FOR_ADMIN':
res = await commentGetForAdmin(event)
break
case 'COMMENT_SET_FOR_ADMIN':
res = await commentSetForAdmin(event)
break
case 'COMMENT_DELETE_FOR_ADMIN':
res = await commentDeleteForAdmin(event)
break
case 'COMMENT_IMPORT_FOR_ADMIN':
res = await commentImportForAdmin(event)
break
case 'COMMENT_LIKE':
res = await commentLike(event)
break
case 'COMMENT_SUBMIT':
res = await commentSubmit(event, request)
break
case 'COUNTER_GET':
res = await counterGet(event)
break
case 'GET_PASSWORD_STATUS':
res = await getPasswordStatus(config, VERSION)
break
case 'SET_PASSWORD':
res = await setPassword(event)
break
case 'GET_CONFIG':
res = await getConfig({ config, VERSION, isAdmin: isAdmin(event.accessToken) })
break
case 'GET_CONFIG_FOR_ADMIN':
res = await getConfigForAdmin({ config, isAdmin: isAdmin(event.accessToken) })
break
case 'SET_CONFIG':
res = await setConfig(event)
break
case 'LOGIN':
res = await login(event.password)
break
case 'GET_COMMENTS_COUNT': // >= 0.2.7
res = await getCommentsCount(event)
break
case 'GET_RECENT_COMMENTS': // >= 0.2.7
res = await getRecentComments(event)
break
case 'EMAIL_TEST': // >= 1.4.6
res = await emailTest(event, config, isAdmin(event.accessToken))
break
case 'UPLOAD_IMAGE': // >= 1.5.0
res = await uploadImage(event, config)
break
case 'COMMENT_EXPORT_FOR_ADMIN': // >= 1.6.13
res = await commentExportForAdmin(event)
break
default:
if (event.event) {
res.code = RES_CODE.EVENT_NOT_EXIST
res.message = '请更新 Twikoo 云函数至最新版本'
} else {
res.code = RES_CODE.NO_PARAM
res.message = 'Twikoo 云函数运行正常,请参考 https://twikoo.js.org/frontend.html 完成前端的配置'
res.version = VERSION
}
}
} catch (e) {
logger.error('Twikoo 遇到错误,请参考以下错误信息。如有疑问,请反馈至 https://github.com/twikoojs/twikoo/issues')
logger.error('请求参数:', event)
logger.error('错误信息:', e)
res.code = RES_CODE.FAIL
res.message = e.message
}
if (!res.code && !request.body.accessToken) {
res.accessToken = accessToken
}
logger.log('请求返回:', res)
response.status(200).json(res)
}
function allowCors (request, response) {
if (request.headers.origin) {
response.setHeader('Access-Control-Allow-Credentials', true)
response.setHeader('Access-Control-Allow-Origin', getAllowedOrigin(request))
response.setHeader('Access-Control-Allow-Methods', 'POST')
response.setHeader(
'Access-Control-Allow-Headers',
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'
)
response.setHeader('Access-Control-Max-Age', '600')
}
}
function getAllowedOrigin (request) {
const localhostRegex = /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d{1,5})?$/
if (localhostRegex.test(request.headers.origin)) { // 判断是否为本地主机,如是则允许跨域
return request.headers.origin // Allow
} else if (config.CORS_ALLOW_ORIGIN) { // 如设置了安全域名则检查
// 适配多条 CORS 规则
// 以逗号分隔 CORS
const corsList = config.CORS_ALLOW_ORIGIN.split(',')
// 遍历 CORS 列表
for (let i = 0; i < corsList.length; i++) {
const cors = corsList[i].replace(/\/$/, '') // 获取当前 CORS 并去除末尾的斜杠
if (cors === request.headers.origin) {
return request.headers.origin // Allow
}
}
return '' // 不在安全域名列表中则禁止跨域
} else {
return request.headers.origin // 未设置安全域名直接 Allow
}
}
function anonymousSignIn (request) {
if (request.body) {
if (request.body.accessToken) {
return request.body.accessToken
} else {
return uuidv4().replace(/-/g, '')
}
}
}
async function connectToDatabase () {
if (db) return db
const dataDir = path.resolve(process.cwd(), process.env.TWIKOO_DATA || './data')
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir)
}
logger.info(`Twikoo database stored at ${dataDir}`)
await new Promise((resolve) => {
logger.info('Connecting to database...')
db = new Loki(path.resolve(dataDir, './db.json'), {
adapter: new Lfsa(),
autoload: true,
autoloadCallback: resolve,
autosave: true,
autosaveInterval: 4000
})
})
await createCollections()
logger.info('Connected to database')
return db
}
// 写入管理密码
async function setPassword (event) {
const isAdminUser = isAdmin(event.accessToken)
// 如果数据库里没有密码,则写入密码
// 如果数据库里有密码,则只有管理员可以写入密码
if (config.ADMIN_PASS && !isAdminUser) {
return { code: RES_CODE.PASS_EXIST, message: '请先登录再修改密码' }
}
const ADMIN_PASS = md5(event.password)
await writeConfig({ ADMIN_PASS })
return {
code: RES_CODE.SUCCESS
}
}
// 管理员登录
async function login (password) {
if (!config) {
return { code: RES_CODE.CONFIG_NOT_EXIST, message: '数据库无配置' }
}
if (!config.ADMIN_PASS) {
return { code: RES_CODE.PASS_NOT_EXIST, message: '未配置管理密码' }
}
if (config.ADMIN_PASS !== md5(password)) {
return { code: RES_CODE.PASS_NOT_MATCH, message: '密码错误' }
}
return {
code: RES_CODE.SUCCESS
}
}
// 读取评论
async function commentGet (event) {
const res = {}
try {
validate(event, ['url'])
const uid = event.accessToken
const isAdminUser = isAdmin(event.accessToken)
const limit = parseInt(config.COMMENT_PAGE_SIZE) || 8
let more = false
let condition
let query
condition = {
url: { $in: getUrlQuery(event.url) },
rid: { $exists: false }
}
// 查询非垃圾评论 + 自己的评论
query = getCommentQuery({ condition, uid, isAdminUser })
// 读取总条数
const count = db
.getCollection('comment')
.count(query)
// 读取主楼
if (event.before) {
condition.created = { $lt: event.before }
}
// 不包含置顶
condition.top = { $ne: true }
query = getCommentQuery({ condition, uid, isAdminUser })
let main = db
.getCollection('comment')
.chain()
.find(query)
.compoundsort([['created', true]])
// 流式分页,通过多读 1 条的方式,确认是否还有更多评论
.limit(limit + 1)
.data()
if (main.length > limit) {
// 还有更多评论
more = true
// 删除多读的 1 条
main.splice(limit, 1)
}
let top = []
if (!config.TOP_DISABLED && !event.before) {
// 查询置顶评论
query = {
...condition,
top: true
}
top = db
.getCollection('comment')
.chain()
.find(query)
.compoundsort([['created', true]])
.data()
// 合并置顶评论和非置顶评论
main = [
...top,
...main
]
}
condition = {
rid: { $in: main.map((item) => item._id.toString()) }
}
query = getCommentQuery({ condition, uid, isAdminUser })
// 读取回复楼
const reply = db
.getCollection('comment')
.chain()
.find(query)
.data()
res.data = parseComment([...main, ...reply], uid, config)
res.more = more
res.count = count
} catch (e) {
res.data = []
res.message = e.message
}
return res
}
function getCommentQuery ({ condition, uid, isAdminUser }) {
return {
$or: [
{ ...condition, isSpam: { $ne: isAdminUser ? 'imaegoo' : true } },
{ ...condition, uid }
]
}
}
// 管理员读取评论
async function commentGetForAdmin (event) {
const res = {}
const isAdminUser = isAdmin(event.accessToken)
if (isAdminUser) {
validate(event, ['per', 'page'])
const collection = db
.getCollection('comment')
const condition = getCommentSearchCondition(event)
const count = await collection.count(condition)
const data = await collection
.chain()
.find(condition)
.compoundsort([['created', true]])
.offset(event.per * (event.page - 1))
.limit(event.per)
.data()
res.code = RES_CODE.SUCCESS
res.count = count
res.data = parseCommentForAdmin(data)
} else {
res.code = RES_CODE.NEED_LOGIN
res.message = '请先登录'
}
return res
}
function getCommentSearchCondition (event) {
let condition
if (event.type) {
switch (event.type) {
case 'VISIBLE':
condition = { isSpam: { $ne: true } }
break
case 'HIDDEN':
condition = { isSpam: true }
break
}
}
if (event.keyword) {
const regExp = {
$regex: event.keyword,
$options: 'i'
}
condition = {
$or: [
{ ...condition, nick: regExp },
{ ...condition, mail: regExp },
{ ...condition, link: regExp },
{ ...condition, ip: regExp },
{ ...condition, comment: regExp },
{ ...condition, url: regExp },
{ ...condition, href: regExp }
]
}
}
return condition
}
// 管理员修改评论
async function commentSetForAdmin (event) {
const res = {}
const isAdminUser = isAdmin(event.accessToken)
if (isAdminUser) {
validate(event, ['id', 'set'])
db
.getCollection('comment')
.findAndUpdate({ _id: event.id }, (obj) => {
for (const key of Object.keys(event.set)) {
obj[key] = event.set[key]
}
obj.updated = Date.now()
return obj
})
res.code = RES_CODE.SUCCESS
res.updated = 1
} else {
res.code = RES_CODE.NEED_LOGIN
res.message = '请先登录'
}
return res
}
// 管理员删除评论
async function commentDeleteForAdmin (event) {
const res = {}
const isAdminUser = isAdmin(event.accessToken)
if (isAdminUser) {
validate(event, ['id'])
db
.getCollection('comment')
.findAndRemove({ _id: event.id })
res.code = RES_CODE.SUCCESS
res.deleted = 1
} else {
res.code = RES_CODE.NEED_LOGIN
res.message = '请先登录'
}
return res
}
// 管理员导入评论
async function commentImportForAdmin (event) {
const res = {}
let logText = ''
const log = (message) => {
logText += `${new Date().toLocaleString()} ${message}\n`
}
const isAdminUser = isAdmin(event.accessToken)
if (isAdminUser) {
try {
validate(event, ['source', 'file'])
log(`开始导入 ${event.source}`)
let comments
switch (event.source) {
case 'valine': {
const valineDb = await readFile(event.file, 'json', log)
comments = await commentImportValine(valineDb, log)
break
}
case 'disqus': {
const disqusDb = await readFile(event.file, 'xml', log)
comments = await commentImportDisqus(disqusDb, log)
break
}
case 'artalk': {
const artalkDb = await readFile(event.file, 'json', log)
comments = await commentImportArtalk(artalkDb, log)
break
}
case 'artalk2': {
const artalkDb = await readFile(event.file, 'json', log)
comments = await commentImportArtalk2(artalkDb, log)
break
}
case 'twikoo': {
const twikooDb = await readFile(event.file, 'json', log)
comments = await commentImportTwikoo(twikooDb, log)
break
}
default:
throw new Error(`不支持 ${event.source} 的导入,请更新 Twikoo 云函数至最新版本`)
}
await bulkSaveComments(comments)
log('导入成功')
} catch (e) {
log(e.message)
}
res.code = RES_CODE.SUCCESS
res.log = logText
logger.info(logText)
} else {
res.code = RES_CODE.NEED_LOGIN
res.message = '请先登录'
}
return res
}
async function commentExportForAdmin (event) {
const res = {}
const isAdminUser = isAdmin(event.accessToken)
if (isAdminUser) {
const collection = event.collection || 'comment'
const data = db
.getCollection(collection)
.chain()
.find({})
.data({ removeMeta: true })
res.code = RES_CODE.SUCCESS
res.data = data
} else {
res.code = RES_CODE.NEED_LOGIN
res.message = '请先登录'
}
return res
}
// 读取文件并转为 js object
async function readFile (file, type, log) {
try {
let content = file.toString('utf8')
log('评论文件读取成功')
if (type === 'json') {
content = jsonParse(content)
log('评论文件 JSON 解析成功')
} else if (type === 'xml') {
content = await xml2js.parseStringPromise(content)
log('评论文件 XML 解析成功')
}
return content
} catch (e) {
log(`评论文件读取失败:${e.message}`)
}
}
// 批量导入评论
async function bulkSaveComments (comments) {
db
.getCollection('comment')
.insert(comments)
}
// 点赞 / 取消点赞
async function commentLike (event) {
const res = {}
validate(event, ['id'])
res.updated = await like(event.id, event.accessToken)
return res
}
// 点赞 / 取消点赞
async function like (id, uid) {
const record = db
.getCollection('comment')
const comment = await record
.findOne({ _id: id })
let likes = comment && comment.like ? comment.like : []
if (likes.findIndex((item) => item === uid) === -1) {
// 赞
likes.push(uid)
} else {
// 取消赞
likes = likes.filter((item) => item !== uid)
}
await record.findAndUpdate({ _id: id }, (obj) => {
obj.like = likes
return obj
})
return 1
}
/**
* 提交评论。分为多个步骤
* 1. 参数校验
* 2. 预检测垃圾评论(包括限流、人工审核、违禁词检测等)
* 3. 保存到数据库
* 4. 触发异步任务(包括 IM 通知、邮件通知、第三方垃圾评论检测
* 等,因为这些任务比较耗时,所以要放在另一个线程进行)
* @param {String} event.nick 昵称
* @param {String} event.mail 邮箱
* @param {String} event.link 网址
* @param {String} event.ua UserAgent
* @param {String} event.url 评论页地址
* @param {String} event.comment 评论内容
* @param {String} event.pid 回复的 ID
* @param {String} event.rid 评论楼 ID
*/
async function commentSubmit (event, request) {
const res = {}
// 参数校验
validate(event, ['url', 'ua', 'comment'])
// 限流
await limitFilter(request)
// 验证码
await checkCaptcha(event, request)
// 预检测、转换
const data = await parse(event, request)
// 保存
const comment = await save(data)
res.id = comment.id
// 异步垃圾检测、发送评论通知
logger.log('开始异步垃圾检测、发送评论通知')
// 私有部署支持直接异步调用
postSubmit(comment)
return res
}
// 保存评论
async function save (data) {
db
.getCollection('comment')
.insert(data)
data.id = data._id
return data
}
async function getParentComment (currentComment) {
const parentComment = db
.getCollection('comment')
.findOne({ _id: currentComment.pid })
return parentComment
}
// 异步垃圾检测、发送评论通知
async function postSubmit (comment) {
try {
logger.log('POST_SUBMIT')
// 垃圾检测
const isSpam = await postCheckSpam(comment, config)
await saveSpamCheckResult(comment, isSpam)
// 发送通知
await sendNotice(comment, config, getParentComment)
} catch (e) {
logger.warn('POST_SUBMIT 失败', e)
}
}
// 将评论转为数据库存储格式
async function parse (comment, request) {
const timestamp = Date.now()
const isAdminUser = isAdmin(request.body.accessToken)
const isBloggerMail = equalsMail(comment.mail, config.BLOGGER_EMAIL)
if (isBloggerMail && !isAdminUser) throw new Error('请先登录管理面板,再使用博主身份发送评论')
const hashMethod = config.GRAVATAR_CDN === 'cravatar.cn' ? md5 : sha256
const commentDo = {
_id: uuidv4().replace(/-/g, ''),
uid: request.body.accessToken,
nick: comment.nick ? comment.nick : '匿名',
mail: comment.mail ? comment.mail : '',
mailMd5: comment.mail ? hashMethod(normalizeMail(comment.mail)) : '',
link: comment.link ? comment.link : '',
ua: comment.ua,
ip: getIp(request),
master: isBloggerMail,
url: comment.url,
href: comment.href,
comment: DOMPurify.sanitize(comment.comment, { FORBID_TAGS: ['style'], FORBID_ATTR: ['style'] }),
pid: comment.pid ? comment.pid : comment.rid,
rid: comment.rid,
isSpam: isAdminUser ? false : preCheckSpam(comment, config),
created: timestamp,
updated: timestamp
}
if (isQQ(comment.mail)) {
commentDo.mail = addQQMailSuffix(comment.mail)
commentDo.mailMd5 = hashMethod(normalizeMail(commentDo.mail))
commentDo.avatar = await getQQAvatar(comment.mail)
}
return commentDo
}
// 限流
async function limitFilter (request) {
// 限制每个 IP 每 10 分钟发表的评论数量
let limitPerMinute = parseInt(config.LIMIT_PER_MINUTE)
if (Number.isNaN(limitPerMinute)) limitPerMinute = 10
if (limitPerMinute) {
const count = db
.getCollection('comment')
.count({
ip: getIp(request),
created: { $gt: Date.now() - 600000 }
})
if (count > limitPerMinute) {
throw new Error('发言频率过高')
}
}
// 限制所有 IP 每 10 分钟发表的评论数量
let limitPerMinuteAll = parseInt(config.LIMIT_PER_MINUTE_ALL)
if (Number.isNaN(limitPerMinuteAll)) limitPerMinuteAll = 10
if (limitPerMinuteAll) {
const count = db
.getCollection('comment')
.count({
created: { $gt: Date.now() - 600000 }
})
if (count > limitPerMinuteAll) {
throw new Error('评论太火爆啦 >_< 请稍后再试')
}
}
}
async function checkCaptcha (comment, request) {
if (config.TURNSTILE_SITE_KEY && config.TURNSTILE_SECRET_KEY) {
await checkTurnstileCaptcha({
ip: getIp(request),
turnstileToken: comment.turnstileToken,
turnstileTokenSecretKey: config.TURNSTILE_SECRET_KEY
})
}
}
async function saveSpamCheckResult (comment, isSpam) {
comment.isSpam = isSpam
if (isSpam) {
db
.getCollection('comment')
.findAndUpdate({ created: comment.created }, (obj) => {
obj.isSpam = isSpam
obj.updated = Date.now()
return obj
})
}
}
/**
* 获取文章点击量
* @param {String} event.url 文章地址
*/
async function counterGet (event) {
const res = {}
try {
validate(event, ['url'])
const record = await readCounter(event.url)
res.data = record || {}
res.time = res.data ? res.data.time : 0
res.updated = await incCounter(event)
} catch (e) {
res.message = e.message
return res
}
return res
}
// 读取阅读数
async function readCounter (url) {
return db
.getCollection('counter')
.findOne({ url })
}
/**
* 更新阅读数
* @param {String} event.url 文章地址
* @param {String} event.title 文章标题
*/
async function incCounter (event) {
const counter = db.getCollection('counter')
const result = counter.find({ url: event.url })[0]
if (result) {
result.time = result.time ? result.time + 1 : 1
result.title = event.title
result.updated = Date.now()
counter.update(result)
} else {
counter.insert({
url: event.url,
title: event.title,
time: 1,
created: Date.now(),
updated: Date.now()
})
}
return 1
}
/**
* 批量获取文章评论数 API
* @param {Array} event.urls 不包含协议和域名的文章路径列表,必传参数
* @param {Boolean} event.includeReply 评论数是否包括回复,默认:false
*/
async function getCommentsCount (event) {
const res = {}
try {
validate(event, ['urls'])
const query = {}
query.isSpam = { $ne: true }
query.url = { $in: getUrlsQuery(event.urls) }
if (!event.includeReply) {
query.rid = { $exists: false }
}
res.data = []
const commentCollection = db.getCollection('comment')
for (const url of event.urls) {
const record = commentCollection.count({ ...query, url })
res.data.push({
url,
count: record || 0
})
}
} catch (e) {
res.message = e.message
return res
}
return res
}
/**
* 获取最新评论 API
* @param {Boolean} event.includeReply 评论数是否包括回复,默认:false
*/
async function getRecentComments (event) {
const res = {}
try {
const query = {}
query.isSpam = { $ne: true }
if (event.urls && event.urls.length) {
query.url = { $in: getUrlsQuery(event.urls) }
}
if (!event.includeReply) query.rid = { $exists: false }
if (event.pageSize > 100) event.pageSize = 100
const result = db
.getCollection('comment')
.chain()
.find(query)
.compoundsort([['created', true]])
.limit(event.pageSize || 10)
.data()
res.data = result.map((comment) => {
return {
id: comment._id.toString(),
url: comment.url,
nick: comment.nick,
avatar: getAvatar(comment, config),
mailMd5: getMailMd5(comment),
link: comment.link,
comment: comment.comment,
commentText: $(comment.comment).text(),
created: comment.created
}
})
} catch (e) {
res.message = e.message
return res
}
return res
}
// 修改配置
async function setConfig (event) {
const isAdminUser = isAdmin(event.accessToken)
if (isAdminUser) {
writeConfig(event.config)
return {
code: RES_CODE.SUCCESS
}
} else {
return {
code: RES_CODE.NEED_LOGIN,
message: '请先登录'
}
}
}
function protect (request) {
// 防御
const ip = getIp(request)
requestTimes[ip] = (requestTimes[ip] || 0) + 1
if (requestTimes[ip] > MAX_REQUEST_TIMES) {
logger.warn(`${ip} 当前请求次数为 ${requestTimes[ip]},已超过最大请求次数`)
throw new Error('Too Many Requests')
} else {
logger.log(`${ip} 当前请求次数为 ${requestTimes[ip]}`)
}
}
// 读取配置
async function readConfig () {
try {
const res = db
.getCollection('config')
.findOne({})
config = res || {}
return config
} catch (e) {
logger.error('读取配置失败:', e)
await createCollections()
config = {}
return config
}
}
// 写入配置
async function writeConfig (newConfig) {
if (!Object.keys(newConfig).length) return 0
logger.info('写入配置:', newConfig)
try {
const oldConfig = db
.getCollection('config')
.chain()
.find({})
.limit(1)
.data()[0]
if (oldConfig) {
db.getCollection('config').update({
...oldConfig,
...newConfig
})
} else {
db.getCollection('config').insert(newConfig)
}
// 更新后重置配置缓存
config = null
return 1
} catch (e) {
logger.error('写入配置失败:', e)
return null
}
}
// 判断用户是否管理员
function isAdmin (accessToken) {
return config.ADMIN_PASS === md5(accessToken)
}
// 建立数据库 collections
async function createCollections () {
const collections = ['comment', 'config', 'counter']
const res = {}
for (const collection of collections) {
if (db.getCollection(collection) === null) {
db.addCollection(collection)
}
}
return res
}
function getIp (request) {
try {
const { TWIKOO_IP_HEADERS } = process.env
const headers = TWIKOO_IP_HEADERS ? JSON.parse(TWIKOO_IP_HEADERS) : []
return getUserIP(request, headers)
} catch (e) {
logger.error('获取 IP 错误信息:', e)
}
return getUserIP(request)
}
function clearRequestTimes () {
requestTimes = {}
}
setInterval(clearRequestTimes, TWIKOO_REQ_TIMES_CLEAR_TIME)