@waline/vercel
Version:
vercel server for waline comment system
566 lines (475 loc) • 14.6 kB
JavaScript
const crypto = require('node:crypto');
const FormData = require('form-data');
const fetch = require('node-fetch');
const nodemailer = require('nodemailer');
const nunjucks = require('nunjucks');
module.exports = class extends think.Service {
constructor(ctx) {
super(ctx);
this.ctx = ctx;
const {
SMTP_USER,
SMTP_PASS,
SMTP_HOST,
SMTP_PORT,
SMTP_SECURE,
SMTP_SERVICE,
} = process.env;
if (SMTP_HOST || SMTP_SERVICE) {
const config = {
auth: { user: SMTP_USER, pass: SMTP_PASS },
};
if (SMTP_SERVICE) {
config.service = SMTP_SERVICE;
} else {
config.host = SMTP_HOST;
config.port = parseInt(SMTP_PORT);
config.secure = SMTP_SECURE && SMTP_SECURE !== 'false';
}
this.transporter = nodemailer.createTransport(config);
}
}
async sleep(second) {
return new Promise((resolve) => setTimeout(resolve, second * 1000));
}
async mail({ to, title, content }, self, parent) {
if (!this.transporter) {
return;
}
const { SITE_NAME, SITE_URL, SMTP_USER, SENDER_EMAIL, SENDER_NAME } =
process.env;
const data = {
self,
parent,
site: {
name: SITE_NAME,
url: SITE_URL,
postUrl: SITE_URL + self.url + '#' + self.objectId,
},
};
title = this.ctx.locale(title, data);
content = this.ctx.locale(content, data);
return this.transporter.sendMail({
from:
SENDER_EMAIL && SENDER_NAME
? `"${SENDER_NAME}" <${SENDER_EMAIL}>`
: SMTP_USER,
to,
subject: title,
html: content,
});
}
async wechat({ title, content }, self, parent) {
const { SC_KEY, SITE_NAME, SITE_URL } = process.env;
if (!SC_KEY) {
return false;
}
const data = {
self,
parent,
site: {
name: SITE_NAME,
url: SITE_URL,
postUrl: SITE_URL + self.url + '#' + self.objectId,
},
};
const contentWechat =
think.config('SCTemplate') ||
`{{site.name|safe}} 有新评论啦
【评论者昵称】:{{self.nick}}
【评论者邮箱】:{{self.mail}}
【内容】:{{self.comment}}
【地址】:{{site.postUrl}}`;
title = this.ctx.locale(title, data);
content = this.ctx.locale(contentWechat, data);
const form = new FormData();
form.append('text', title);
form.append('desp', content);
return fetch(`https://sctapi.ftqq.com/${SC_KEY}.send`, {
method: 'POST',
headers: form.getHeaders(),
body: form,
}).then((resp) => resp.json());
}
async qywxAmWechat({ title, content }, self, parent) {
const { QYWX_AM, QYWX_PROXY, QYWX_PROXY_PORT, SITE_NAME, SITE_URL } =
process.env;
if (!QYWX_AM) {
return false;
}
const QYWX_AM_AY = QYWX_AM.split(',');
const comment = self.comment
.replace(/<a href="(.*?)">(.*?)<\/a>/g, '\n[$2] $1\n')
.replace(/<[^>]+>/g, '');
const postName = self.url;
const data = {
self: {
...self,
comment,
},
postName,
parent,
site: {
name: SITE_NAME,
url: SITE_URL,
postUrl: SITE_URL + self.url + '#' + self.objectId,
},
};
const contentWechat =
think.config('WXTemplate') ||
`💬 {{site.name|safe}}的文章《{{postName}}》有新评论啦
【评论者昵称】:{{self.nick}}
【评论者邮箱】:{{self.mail}}
【内容】:{{self.comment}}
<a href='{{site.postUrl}}'>查看详情</a>`;
title = this.ctx.locale(title, data);
const desp = this.ctx.locale(contentWechat, data);
content = desp.replace(/\n/g, '<br/>');
const querystring = new URLSearchParams();
querystring.set('corpid', `${QYWX_AM_AY[0]}`);
querystring.set('corpsecret', `${QYWX_AM_AY[1]}`);
let baseUrl = 'https://qyapi.weixin.qq.com';
if (QYWX_PROXY) {
if (!QYWX_PROXY_PORT) {
baseUrl = `http://${QYWX_PROXY}`;
} else {
baseUrl = `http://${QYWX_PROXY}:${QYWX_PROXY_PORT}`;
}
}
const { access_token } = await fetch(
`${baseUrl}/cgi-bin/gettoken?${querystring.toString()}`,
{
headers: {
'content-type': 'application/json',
},
},
).then((resp) => resp.json());
return fetch(
`${baseUrl}/cgi-bin/message/send?access_token=${access_token}`,
{
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
touser: `${QYWX_AM_AY[2]}`,
agentid: `${QYWX_AM_AY[3]}`,
msgtype: 'mpnews',
mpnews: {
articles: [
{
title,
thumb_media_id: `${QYWX_AM_AY[4]}`,
author: `Waline Comment`,
content_source_url: `${data.site.postUrl}`,
content: `${content}`,
digest: `${desp}`,
},
],
},
}),
},
).then((resp) => resp.json());
}
async qq(self, parent) {
const { QMSG_KEY, QQ_ID, SITE_NAME, SITE_URL, QMSG_HOST } = process.env;
if (!QMSG_KEY) {
return false;
}
const comment = self.comment
.replace(/<a href="(.*?)">(.*?)<\/a>/g, '')
.replace(/<[^>]+>/g, '');
const data = {
self: {
...self,
comment,
},
parent,
site: {
name: SITE_NAME,
url: SITE_URL,
postUrl: SITE_URL + self.url + '#' + self.objectId,
},
};
const contentQQ =
think.config('QQTemplate') ||
`💬 {{site.name|safe}} 有新评论啦
{{self.nick}} 评论道:
{{self.comment}}
仅供预览评论,请前往上述页面查看完整內容。`;
const form = new FormData();
form.append('msg', this.ctx.locale(contentQQ, data));
form.append('qq', QQ_ID);
const qmsgHost = QMSG_HOST
? QMSG_HOST.replace(/\/$/, '')
: 'https://qmsg.zendee.cn';
return fetch(`${qmsgHost}/send/${QMSG_KEY}`, {
method: 'POST',
header: form.getHeaders(),
body: form,
}).then((resp) => resp.json());
}
async telegram(self, parent) {
const { TG_BOT_TOKEN, TG_CHAT_ID, SITE_NAME, SITE_URL } = process.env;
if (!TG_BOT_TOKEN || !TG_CHAT_ID) {
return false;
}
let commentLink = '';
const href = self.comment.match(/<a href="(.*?)">(.*?)<\/a>/g);
if (href !== null) {
for (let i = 0; i < href.length; i++) {
href[i] =
'[Link: ' +
href[i].replace(/<a href="(.*?)">(.*?)<\/a>/g, '$2') +
'](' +
href[i].replace(/<a href="(.*?)">(.*?)<\/a>/g, '$1') +
') ';
commentLink = commentLink + href[i];
}
}
if (commentLink !== '') {
commentLink = `\n` + commentLink + `\n`;
}
const comment = self.comment
.replace(/<a href="(.*?)">(.*?)<\/a>/g, '[Link:$2]')
.replace(/<[^>]+>/g, '');
const contentTG =
think.config('TGTemplate') ||
`💬 *[{{site.name}}]({{site.url}}) 有新评论啦*
*{{self.nick}}* 回复说:
\`\`\`
{{self.comment-}}
\`\`\`
{{-self.commentLink}}
*邮箱:*\`{{self.mail}}\`
*审核:*{{self.status}}
仅供评论预览,点击[查看完整內容]({{site.postUrl}})`;
const data = {
self: {
...self,
comment,
commentLink,
},
parent,
site: {
name: SITE_NAME,
url: SITE_URL,
postUrl: SITE_URL + self.url + '#' + self.objectId,
},
};
const form = new FormData();
form.append('text', this.ctx.locale(contentTG, data));
form.append('chat_id', TG_CHAT_ID);
form.append('parse_mode', 'MarkdownV2');
const resp = await fetch(
`https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage`,
{
method: 'POST',
header: form.getHeaders(),
body: form,
},
).then((resp) => resp.json());
if (!resp.ok) {
console.log('Telegram Notification Failed:' + JSON.stringify(resp));
}
}
async pushplus({ title, content }, self, parent) {
const {
PUSH_PLUS_KEY,
PUSH_PLUS_TOPIC: topic,
PUSH_PLUS_TEMPLATE: template,
PUSH_PLUS_CHANNEL: channel,
PUSH_PLUS_WEBHOOK: webhook,
PUSH_PLUS_CALLBACKURL: callbackUrl,
SITE_NAME,
SITE_URL,
} = process.env;
if (!PUSH_PLUS_KEY) {
return false;
}
const data = {
self,
parent,
site: {
name: SITE_NAME,
url: SITE_URL,
postUrl: SITE_URL + self.url + '#' + self.objectId,
},
};
title = this.ctx.locale(title, data);
content = this.ctx.locale(content, data);
const form = new FormData();
if (topic) form.append('topic', topic);
if (template) form.append('template', template);
if (channel) form.append('channel', channel);
if (webhook) form.append('webhook', webhook);
if (callbackUrl) form.append('callbackUrl', callbackUrl);
if (title) form.append('title', title);
if (content) form.append('content', content);
return fetch(`http://www.pushplus.plus/send/${PUSH_PLUS_KEY}`, {
method: 'POST',
header: form.getHeaders(),
body: form,
}).then((resp) => resp.json());
}
async discord({ title, content }, self, parent) {
const { DISCORD_WEBHOOK, SITE_NAME, SITE_URL } = process.env;
if (!DISCORD_WEBHOOK) {
return false;
}
const data = {
self,
parent,
site: {
name: SITE_NAME,
url: SITE_URL,
postUrl: SITE_URL + self.url + '#' + self.objectId,
},
};
title = this.ctx.locale(title, data);
content = this.ctx.locale(
think.config('DiscordTemplate') ||
`💬 {{site.name|safe}} 有新评论啦
【评论者昵称】:{{self.nick}}
【评论者邮箱】:{{self.mail}}
【内容】:{{self.comment}}
【地址】:{{site.postUrl}}`,
data,
);
const form = new FormData();
form.append('content', `${title}\n${content}`);
return fetch(DISCORD_WEBHOOK, {
method: 'POST',
header: form.getHeaders(),
body: form,
}).then((resp) => resp.statusText);
// Expected return value: No Content
// Since Discord doesn't return any response body on success, we just return the status text.
}
async lark({ title, content }, self, parent) {
const { LARK_WEBHOOK, LARK_SECRET, SITE_NAME, SITE_URL } = process.env;
if (!LARK_WEBHOOK) {
return false;
}
self.comment = self.comment.replace(/(<([^>]+)>)/gi, '');
const data = {
self,
parent,
site: {
name: SITE_NAME,
url: SITE_URL,
postUrl: SITE_URL + self.url + '#' + self.objectId,
},
};
content = nunjucks.renderString(
think.config('LarkTemplate') ||
`【网站名称】:{{site.name|safe}} \n【评论者昵称】:{{self.nick}}\n【评论者邮箱】:{{self.mail}}\n【内容】:{{self.comment}}【地址】:{{site.postUrl}}`,
data,
);
const post = {
en_us: {
title: this.ctx.locale(title, data),
content: [
[
{
tag: 'text',
text: content,
},
],
],
},
};
let signData = {};
const msg = {
msg_type: 'post',
content: {
post,
},
};
const sign = (timestamp, secret) => {
const signStr = timestamp + '\n' + secret;
return crypto.createHmac('sha256', signStr).update('').digest('base64');
};
if (LARK_SECRET) {
const timestamp = parseInt(+new Date() / 1000);
signData = { timestamp: timestamp, sign: sign(timestamp, LARK_SECRET) };
}
const resp = await fetch(LARK_WEBHOOK, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...signData,
...msg,
}),
}).then((resp) => resp.json());
if (resp.status !== 200) {
console.log('Lark Notification Failed:' + JSON.stringify(resp));
}
console.log('FeiShu Notification Success:' + JSON.stringify(resp));
}
async run(comment, parent, disableAuthorNotify = false) {
const { AUTHOR_EMAIL, DISABLE_AUTHOR_NOTIFY } = process.env;
const { mailSubject, mailTemplate, mailSubjectAdmin, mailTemplateAdmin } =
think.config();
const AUTHOR = AUTHOR_EMAIL;
const mailList = [];
const isAuthorComment = AUTHOR
? (comment.mail || '').toLowerCase() === AUTHOR.toLowerCase()
: false;
const isReplyAuthor = AUTHOR
? parent && (parent.mail || '').toLowerCase() === AUTHOR.toLowerCase()
: false;
const isCommentSelf =
parent &&
(parent.mail || '').toLowerCase() === (comment.mail || '').toLowerCase();
const title = mailSubjectAdmin || 'MAIL_SUBJECT_ADMIN';
const content = mailTemplateAdmin || 'MAIL_TEMPLATE_ADMIN';
if (!DISABLE_AUTHOR_NOTIFY && !isAuthorComment && !disableAuthorNotify) {
const wechat = await this.wechat({ title, content }, comment, parent);
const qywxAmWechat = await this.qywxAmWechat(
{ title, content },
comment,
parent,
);
const qq = await this.qq(comment, parent);
const telegram = await this.telegram(comment, parent);
const pushplus = await this.pushplus({ title, content }, comment, parent);
const discord = await this.discord({ title, content }, comment, parent);
const lark = await this.lark({ title, content }, comment, parent);
if (
[wechat, qq, telegram, qywxAmWechat, pushplus, discord, lark].every(
think.isEmpty,
)
) {
mailList.push({ to: AUTHOR, title, content });
}
}
const disallowList = ['github', 'twitter', 'facebook', 'qq', 'weibo'].map(
(social) => 'mail.' + social,
);
const fakeMail = new RegExp(`@(${disallowList.join('|')})$`, 'i');
if (
parent &&
!fakeMail.test(parent.mail) &&
!isCommentSelf &&
!isReplyAuthor &&
comment.status !== 'waiting'
) {
mailList.push({
to: parent.mail,
title: mailSubject || 'MAIL_SUBJECT',
content: mailTemplate || 'MAIL_TEMPLATE',
});
}
for (const mail of mailList) {
try {
const response = await this.mail(mail, comment, parent);
console.log('Notification mail send success: %s', response);
} catch (e) {
console.log('Mail send fail:', e);
}
}
}
};