parent
a94072fe1b
commit
0ce7ff2686
11 changed files with 532 additions and 88 deletions
@ -1,8 +1,24 @@ |
||||
var AV = require('leanengine'); |
||||
const AV = require('leanengine'); |
||||
const mail = require('./utilities/send-mail'); |
||||
const spam = require('./utilities/check-spam'); |
||||
|
||||
/** |
||||
* 一个简单的云代码方法 |
||||
*/ |
||||
AV.Cloud.define('hello', function(request) { |
||||
return 'Hello world!'; |
||||
AV.Cloud.afterSave('Comment', function (request) { |
||||
let currentComment = request.object; |
||||
// 检查垃圾评论
|
||||
spam.checkSpam(currentComment); |
||||
|
||||
// 发送博主通知邮件
|
||||
mail.notice(currentComment); |
||||
// AT评论通知
|
||||
let rid = currentComment.get('rid'); |
||||
if (!rid) { |
||||
console.log('没有@任何人,结束!'); |
||||
return; |
||||
} |
||||
let query = new AV.Query('Comment'); |
||||
query.get(rid).then(function (parentComment) { |
||||
mail.send(currentComment, parentComment); |
||||
}, function (error) { |
||||
console.warn('获取@对象失败!'); |
||||
}); |
||||
}); |
||||
|
@ -1,7 +1,152 @@ |
||||
body { |
||||
padding: 50px; |
||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; |
||||
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,sans-serif; |
||||
font-size: 1rem; |
||||
} |
||||
|
||||
a { |
||||
color: #00b7ff; |
||||
text-decoration: none; |
||||
color: #3eb0ef; |
||||
} |
||||
|
||||
.footer { |
||||
font-size: 0.8rem; |
||||
} |
||||
|
||||
.title { |
||||
font-weight: 200; |
||||
font-size: 1.5rem; |
||||
} |
||||
|
||||
ul { |
||||
list-style: none; |
||||
} |
||||
|
||||
li { |
||||
list-style: none; |
||||
} |
||||
|
||||
.content { |
||||
padding: 0; |
||||
} |
||||
|
||||
|
||||
.header{ |
||||
display: flex; |
||||
-ms-flex-direction: row; |
||||
flex-direction: row; |
||||
align-items: flex-start; |
||||
} |
||||
|
||||
.header .title { |
||||
flex: 90%; |
||||
} |
||||
|
||||
.logout-wrapper { |
||||
|
||||
} |
||||
|
||||
.comment-main{ |
||||
padding: 1rem 5rem; |
||||
} |
||||
.vhead { |
||||
padding-bottom: 0.5rem; |
||||
} |
||||
|
||||
.vhead a { |
||||
font-weight: bolder; |
||||
color: #333; |
||||
} |
||||
|
||||
.spacer { |
||||
color: #ccc; |
||||
margin-left: 0.3rem; |
||||
margin-right: 0.3rem; |
||||
} |
||||
|
||||
.vtime { |
||||
color: #a9a4a4; |
||||
display: inline-block; |
||||
padding: 0 5px; |
||||
} |
||||
|
||||
.vcomment { |
||||
line-height: 1.8; |
||||
} |
||||
|
||||
.vcard { |
||||
padding: 2rem 0 2rem 0; |
||||
border-top: 1px solid #dedede; |
||||
} |
||||
|
||||
.check { |
||||
padding-top: 1rem; |
||||
color: #33b1ff; |
||||
} |
||||
|
||||
.sign-in-wrap { |
||||
display: flex; |
||||
-ms-flex-direction: column; |
||||
flex-direction: column; |
||||
width: 100%; |
||||
color: #738a94; |
||||
font-weight: 100; |
||||
text-align: center; |
||||
align-items: center; |
||||
margin-top: 4rem; |
||||
} |
||||
|
||||
.gh-signin { |
||||
display: flex; |
||||
-ms-flex-direction: column; |
||||
flex-direction: column; |
||||
padding: 1rem 2rem; |
||||
width: 350px; |
||||
border: 1px solid #dae1e3; |
||||
background: #f8fbfd; |
||||
border-radius: 5px; |
||||
text-align: left; |
||||
} |
||||
|
||||
.gh-signin .form-group { |
||||
margin-bottom: 1.5rem; |
||||
position: relative; |
||||
width: 100%; |
||||
max-width: 700px; |
||||
-webkit-user-select: text; |
||||
user-select: text; |
||||
} |
||||
|
||||
.sign-in-wrap input { |
||||
padding: 10px; |
||||
margin: 1rem 1rem; |
||||
border: 1px solid #dae1e3; |
||||
font-size: 1rem; |
||||
line-height: 1.5em; |
||||
font-weight: 100; |
||||
display: block; |
||||
user-select: text; |
||||
border-radius: 4px; |
||||
transition: border-color .15s linear; |
||||
-webkit-appearance: none; |
||||
color: #4b5b62; |
||||
-webkit-user-select: text; |
||||
} |
||||
|
||||
.sign-in-wrap input:focus { |
||||
border-color: #c4c8cb; |
||||
outline: 0 |
||||
} |
||||
|
||||
.sign-in-wrap .login-button { |
||||
padding: 8px 10px; |
||||
cursor: pointer; |
||||
background: #33b1ff; |
||||
border: .1rem solid #33b1ff; |
||||
border-radius: .2rem; |
||||
color: #fff; |
||||
} |
||||
|
||||
.red { |
||||
color: #ee1000; |
||||
} |
||||
|
||||
|
@ -0,0 +1,79 @@ |
||||
'use strict'; |
||||
const router = require('express').Router(); |
||||
const AV = require('leanengine'); |
||||
const mail = require('../utilities/send-mail'); |
||||
const spam = require('../utilities/check-spam'); |
||||
|
||||
const Comment = AV.Object.extend('Comment'); |
||||
|
||||
// Comment 列表
|
||||
router.get('/', function (req, res, next) { |
||||
if (req.currentUser) { |
||||
let query = new AV.Query(Comment); |
||||
query.descending('createdAt'); |
||||
query.limit(50); |
||||
query.find().then(function (results) { |
||||
res.render('comments', { |
||||
title: process.env.SITE_NAME + '上的评论', |
||||
comment_list: results |
||||
}); |
||||
}, function (err) { |
||||
if (err.code === 101) { |
||||
res.render('comments', { |
||||
title: process.env.SITE_NAME + '上的评论', |
||||
comment_list: [] |
||||
}); |
||||
} else { |
||||
next(err); |
||||
} |
||||
}).catch(next); |
||||
} else { |
||||
res.redirect('/login'); |
||||
} |
||||
}); |
||||
|
||||
router.get('/not-spam', function (req, res, next) { |
||||
if (req.currentUser) { |
||||
let query = new AV.Query(Comment); |
||||
query.get(req.query.id).then(function (object) { |
||||
object.set('isSpam', false); |
||||
object.save(); |
||||
spam.submitHam(object); |
||||
res.redirect('/comments') |
||||
}, function (err) { |
||||
}).catch(next); |
||||
} else { |
||||
res.redirect('/login'); |
||||
} |
||||
}); |
||||
|
||||
|
||||
router.get('/mark-spam', function (req, res, next) { |
||||
if (req.currentUser) { |
||||
let query = new AV.Query(Comment); |
||||
query.get(req.query.id).then(function (object) { |
||||
object.set('isSpam', true); |
||||
object.save(); |
||||
spam.submitSpam(object); |
||||
res.redirect('/comments') |
||||
}, function (err) { |
||||
}).catch(next); |
||||
} else { |
||||
res.redirect('/'); |
||||
} |
||||
}); |
||||
|
||||
router.get('/resend-email', function (req, res, next) { |
||||
let query = new AV.Query(Comment); |
||||
query.get(req.query.id).then(function (object) { |
||||
query.get(object.get('rid')).then(function (parent) { |
||||
mail.send(object, parent); |
||||
res.redirect('/comments') |
||||
}, function (err) { |
||||
} |
||||
).catch(next); |
||||
}, function (err) { |
||||
}).catch(next); |
||||
}); |
||||
|
||||
module.exports = router; |
@ -1,40 +0,0 @@ |
||||
'use strict'; |
||||
var router = require('express').Router(); |
||||
var AV = require('leanengine'); |
||||
|
||||
var Todo = AV.Object.extend('Todo'); |
||||
|
||||
// 查询 Todo 列表
|
||||
router.get('/', function(req, res, next) { |
||||
var query = new AV.Query(Todo); |
||||
query.descending('createdAt'); |
||||
query.find().then(function(results) { |
||||
res.render('todos', { |
||||
title: 'TODO 列表', |
||||
todos: results |
||||
}); |
||||
}, function(err) { |
||||
if (err.code === 101) { |
||||
// 该错误的信息为:{ code: 101, message: 'Class or object doesn\'t exists.' },说明 Todo 数据表还未创建,所以返回空的 Todo 列表。
|
||||
// 具体的错误代码详见:https://leancloud.cn/docs/error_code.html
|
||||
res.render('todos', { |
||||
title: 'TODO 列表', |
||||
todos: [] |
||||
}); |
||||
} else { |
||||
next(err); |
||||
} |
||||
}).catch(next); |
||||
}); |
||||
|
||||
// 新增 Todo 项目
|
||||
router.post('/', function(req, res, next) { |
||||
var content = req.body.content; |
||||
var todo = new Todo(); |
||||
todo.set('content', content); |
||||
todo.save().then(function(todo) { |
||||
res.redirect('/todos'); |
||||
}).catch(next); |
||||
}); |
||||
|
||||
module.exports = router; |
@ -0,0 +1,91 @@ |
||||
'use strict'; |
||||
const akismet = require('akismet-api'); |
||||
const akismetClient = akismet.client({ |
||||
key : process.env.AKISMET_KEY, |
||||
blog : process.env.SITE_URL |
||||
}); |
||||
|
||||
exports.checkSpam = (comment)=> { |
||||
akismetClient.verifyKey(function(err, valid) { |
||||
if (err) console.log('Akismet key 异常:', err.message); |
||||
if (valid) { |
||||
let ipAddr = comment.get('ip'); |
||||
console.log('评论者的IP是: ' + ipAddr); |
||||
akismetClient.checkSpam({ |
||||
user_ip : ipAddr, |
||||
user_agent : comment.get('ua'), |
||||
referrer : process.env.SITE_URL, // TODO(1) 这里有缺陷
|
||||
permalink : process.env.SITE_URL + comment.get('url'), // TODO(2) 这里有缺陷
|
||||
comment_type : 'comment', |
||||
comment_author : comment.get('nick'), |
||||
comment_author_email : comment.get('mail'), |
||||
comment_author_url : comment.get('link'), |
||||
comment_content : comment.get('comment'), |
||||
// is_test : true // Default value is false
|
||||
}, function(err, spam) { |
||||
if (err) console.log ('垃圾检测出错!'); |
||||
if (spam) { |
||||
console.log('逮到一只垃圾,烧死它!用文火~'); |
||||
comment.set('isSpam', true); |
||||
comment.save(); |
||||
// comment.destroy();
|
||||
} else { |
||||
console.log('放行~完事!\n'); |
||||
} |
||||
}); |
||||
} |
||||
else console.log('Akismet key 异常!'); |
||||
}); |
||||
}; |
||||
|
||||
exports.submitSpam = (comment)=> { |
||||
akismetClient.verifyKey(function(err, valid) { |
||||
if (err) console.log('Akismet key 异常:', err.message); |
||||
if (valid) { |
||||
let ipAddr = comment.get('ip'); |
||||
akismetClient.submitSpam({ |
||||
user_ip : ipAddr, |
||||
user_agent : comment.get('ua'), |
||||
referrer : process.env.SITE_URL, |
||||
permalink : process.env.SITE_URL + comment.get('url'), |
||||
comment_type : 'comment', |
||||
comment_author : comment.get('nick'), |
||||
comment_author_email : comment.get('mail'), |
||||
comment_author_url : comment.get('link'), |
||||
comment_content : comment.get('comment'), |
||||
// is_test : true // Default value is false
|
||||
}, function(err) { |
||||
if (!err) { |
||||
console.log('垃圾评论已经提交!'); |
||||
} |
||||
}); |
||||
} |
||||
else console.log('Akismet key 异常!'); |
||||
}); |
||||
}; |
||||
|
||||
exports.submitHam = (comment)=> { |
||||
akismetClient.verifyKey(function(err, valid) { |
||||
if (err) console.log('Akismet key 异常:', err.message); |
||||
if (valid) { |
||||
let ipAddr = comment.get('ip'); |
||||
akismetClient.submitHam({ |
||||
user_ip : ipAddr, |
||||
user_agent : comment.get('ua'), |
||||
referrer : process.env.SITE_URL, |
||||
permalink : process.env.SITE_URL + comment.get('url'), |
||||
comment_type : 'comment', |
||||
comment_author : comment.get('nick'), |
||||
comment_author_email : comment.get('mail'), |
||||
comment_author_url : comment.get('link'), |
||||
comment_content : comment.get('comment'), |
||||
// is_test : true // Default value is false
|
||||
}, function(err) { |
||||
if (!err) { |
||||
console.log('评论已经标记为非垃圾!'); |
||||
} |
||||
}); |
||||
} |
||||
else console.log('Akismet key 异常!'); |
||||
}); |
||||
}; |
@ -0,0 +1,71 @@ |
||||
'use strict'; |
||||
const nodemailer = require('nodemailer'); |
||||
const transporter = nodemailer.createTransport({ |
||||
host: process.env.SMTP_HOST, |
||||
port: parseInt(process.env.SMTP_PORT), |
||||
secure: true, |
||||
auth: { |
||||
user: process.env.SMTP_USER, |
||||
pass: process.env.SMTP_PASS |
||||
} |
||||
}); |
||||
|
||||
exports.notice = (comment) => { |
||||
let emailSubject = '👉 咚!「' + process.env.SITE_NAME + '」上有新评论了'; |
||||
let emailContent = '<p>「' + process.env.SITE_NAME + '」上 ' |
||||
+ comment.get('nick') |
||||
+' 留下了新评论,内容如下:</p>' |
||||
+ comment.get('comment') |
||||
+ '<br><p> <a href="' |
||||
+ process.env.SITE_URL |
||||
+ comment.get('url') |
||||
+ '">点击前往查看</a>'; |
||||
|
||||
let mailOptions = { |
||||
from: '"' + process.env.SENDER_NAME + '" <' + process.env.SENDER_EMAIL + '>', |
||||
to: process.env.SENDER_EMAIL, |
||||
subject: emailSubject, |
||||
html: emailContent |
||||
}; |
||||
|
||||
transporter.sendMail(mailOptions, (error, info) => { |
||||
if (error) { |
||||
return console.log(error); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
exports.send = (currentComment, parentComment)=> { |
||||
let emailSubject = '👉 叮咚!「' + process.env.SITE_NAME + '」上有人@了你'; |
||||
let emailContent = '<span style="font-size:16px;color:#212121">Hi,' |
||||
+ parentComment.get('nick') |
||||
+ '</span>' |
||||
+ '<p>「' + process.env.SITE_NAME + '」上 ' |
||||
+ currentComment.get('nick') |
||||
+' @了你,回复内容如下:</p>' |
||||
+ currentComment.get('comment') |
||||
+ '<br><p>原评论内容为:' |
||||
+ parentComment.get('comment') |
||||
+ '</p><p> <a href="' |
||||
+ process.env.SITE_URL |
||||
+ currentComment.get('url') |
||||
+ '">点击前往查看</a> <br><p><a href="' |
||||
+ process.env.SITE_URL + '">' |
||||
+ process.env.SITE_NAME + ' </a>欢迎你的再度光临</p>'; |
||||
|
||||
let mailOptions = { |
||||
from: '"' + process.env.SENDER_NAME + '" <' + process.env.SENDER_EMAIL + '>', // sender address
|
||||
to: parentComment.get('mail'), |
||||
subject: emailSubject, |
||||
html: emailContent |
||||
}; |
||||
|
||||
transporter.sendMail(mailOptions, (error, info) => { |
||||
if (error) { |
||||
return console.log(error); |
||||
} |
||||
console.log('邮件 %s 成功发送: %s', info.messageId, info.response); |
||||
currentComment.set('isNotified', true); |
||||
currentComment.save(); |
||||
}); |
||||
}; |
@ -0,0 +1,50 @@ |
||||
<!DOCTYPE HTML> |
||||
<html> |
||||
<head> |
||||
<title><%= title %></title> |
||||
<link rel="stylesheet" href="/stylesheets/style.css"> |
||||
</head> |
||||
<body> |
||||
<div class="comment-main"> |
||||
<div class="header"> |
||||
<div class="title"><span><%= title %></span></div> |
||||
|
||||
<div class="logout-wrapper"><a href="/logout">退出登录</a></div> |
||||
</div> |
||||
|
||||
<ul class="content"> |
||||
<% for(var i = 0; i < comment_list.length; i++) { %> |
||||
<li class="vcard"> |
||||
<div class="vhead"> |
||||
<a href="<%= comment_list[i].get('link') %>" target="_blank" |
||||
rel="nofollow"> <%= comment_list[i].get('nick') %></a> |
||||
<% var date = comment_list[i].get('createdAt') %> |
||||
<span class="spacer">•</span><span class="vtime"><%= dateFormat(date) %></span> |
||||
|
||||
</div> |
||||
<div class="vcomment"> |
||||
<%- comment_list[i].get('comment') %> |
||||
</div> |
||||
<div class="check"> |
||||
<a class="red" href="/comments/delete?id=<%= comment_list[i].get('objectId') %>" rel="nofollow">删除</a><span class="spacer">•</span> |
||||
<% if(comment_list[i].get('isSpam')) { %> |
||||
<a href="/comments/not-spam?id=<%= comment_list[i].get('objectId') %>" rel="nofollow">这不是垃圾评论</a> |
||||
<% } else { %> |
||||
<a href="/comments/mark-spam?id=<%= comment_list[i].get('objectId') %>" rel="nofollow">标记为垃圾评论</a> |
||||
<% } %> |
||||
<% if (comment_list[i].get('rid')) { %> |
||||
<% if(comment_list[i].get('isNotified')) { %> |
||||
<span class="spacer">•</span><span class="vtime">通知已送达</span> |
||||
<% } else { %> |
||||
<span class="spacer">•</span> |
||||
<a href="/comments/resend-email?id=<%= comment_list[i].get('objectId') %>" rel="nofollow">重发通知邮件</a> |
||||
<% } %> |
||||
<% } %> |
||||
</div> |
||||
|
||||
</li> |
||||
<% } %> |
||||
</ul> |
||||
</div> |
||||
</body> |
||||
</html> |
@ -1,13 +1,21 @@ |
||||
<!DOCTYPE HTML> |
||||
<html> |
||||
<head> |
||||
<title>LeanEngine</title> |
||||
<head> |
||||
<title>LeanCloud评论管理</title> |
||||
<link rel="stylesheet" href="/stylesheets/style.css"> |
||||
</head> |
||||
<body> |
||||
<h1>LeanEngine</h1> |
||||
<p>这是 LeanEngine 的示例应用</p> |
||||
<p>当前时间:<%= currentTime %></p> |
||||
<p><a href="/todos">一个简单的「TODO 列表」示例</a></p> |
||||
</body> |
||||
</head> |
||||
<body> |
||||
|
||||
<div class="sign-in-wrap"> |
||||
<p class="title">LeanCloud评论管理</p> |
||||
<form method="post" action="/login" class="gh-signin"> |
||||
<input tabindex="1" name="username" |
||||
placeholder="用户名" autofocus="" type="text" class="gh-input"> |
||||
<input tabindex="2" name="password" placeholder="密码" type="password" class="password gh-input ember-view"> |
||||
|
||||
<input tabindex="3" type="submit" class="login-button" value="登录"> |
||||
</form> |
||||
<div class="footer"><p><a href="https://panjunwen.com">By Deserts</a><p></div> |
||||
</div> |
||||
</body> |
||||
</html> |
||||
|
@ -1,19 +0,0 @@ |
||||
<!DOCTYPE HTML> |
||||
<html> |
||||
<head> |
||||
<title>Todo</title> |
||||
<link rel="stylesheet" href="/stylesheets/style.css"> |
||||
</head> |
||||
<body> |
||||
<h1><%= title %></h1> |
||||
<form action="/todos" method="POST"> |
||||
<input type="text" name="content" /> |
||||
<input type="submit" value="新增" /> |
||||
</form> |
||||
<ul> |
||||
<% for(var i=0; i<todos.length; i++) {%> |
||||
<li><%= todos[i].get('content') %></li> |
||||
<% } %> |
||||
</ul> |
||||
</body> |
||||
</html> |
Loading…
Reference in new issue