🚧 refactoring is ongoing

This commit is contained in:
Song
2021-01-17 10:49:19 +08:00
parent aa2635afcd
commit 22975603c6
20 changed files with 327 additions and 199 deletions
+24 -21
View File
@@ -1,36 +1,39 @@
const express = require("express"); const express = require('express');
const path = require("path"); const path = require('path');
const cookieParser = require("cookie-parser"); const cookieParser = require('cookie-parser');
const logger = require("morgan"); const logger = require('morgan');
const http = require("http"); const http = require('http');
require("dotenv").config(); const config = require('./config');
const indexRouter = require("./routers/index"); const indexRouter = require('./routers/index');
const messageRouter = require("./routers/message"); const messageRouter = require('./routers/message');
const requestToken = require("./utils/wechat").requestToken; const { refreshToken } = require('./common/wechat');
const { initializeTokenStore } = require('./common/token');
const app = express(); const app = express();
requestToken(app); setTimeout(async () => {
setInterval(() => requestToken(app), 100 * 60 * 1000); // TODO: Here we need an improvement! I have tried EventEmitter but it's not working. :(
await initializeTokenStore();
await refreshToken(app);
setInterval(async () => refreshToken(), 100 * 60 * 1000);
}, 1000);
const port = parseInt(process.env.PORT || "3000"); app.set('views', path.join(__dirname, 'views'));
app.set("port", port); app.set('view engine', 'ejs');
app.set("views", path.join(__dirname, "views")); app.set('trust proxy', true);
app.set("view engine", "ejs");
app.set("trust proxy", true);
app.use(logger("dev")); app.use(logger('dev'));
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: false })); app.use(express.urlencoded({ extended: false }));
app.use(cookieParser()); app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public"))); app.use(express.static(path.join(__dirname, 'public')));
app.use("/", indexRouter); app.use('/', indexRouter);
app.use("/message", messageRouter); app.use('/message', messageRouter);
const server = http.createServer(app); const server = http.createServer(app);
server.listen(port); server.listen(config.port);
module.exports = app; module.exports = app;
+9
View File
@@ -0,0 +1,9 @@
const { Sequelize } = require('sequelize');
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: 'data.db',
logging: false,
});
module.exports = sequelize;
+24
View File
@@ -0,0 +1,24 @@
const { pushWechatMessage } = require('./wechat');
module.exports = {
async processMessage(userPrefix, message) {
let result = {
success: false,
message: `unsupported message type ${message.type}`,
};
switch (message.type) {
case '0': // WeChat message
result = await pushWechatMessage(userPrefix, message);
break;
case '1': // Email message
// TODO: Email message
break;
case '2': // HTTP GET request
// TODO: HTTP GET request
break;
default:
break;
}
return result;
},
};
+25
View File
@@ -0,0 +1,25 @@
const { User } = require('../models');
const tokenStore = new Map();
async function initializeTokenStore() {
const users = await User.findAll();
users.forEach((user) => {
if (user.wechatAppId) {
tokenStore.set(user.prefix, {
appId: user.wechatAppId,
appSecret: user.wechatAppSecret,
templateId: user.wechatTemplateId,
openId: user.wechatOpenId,
token: '',
});
}
});
console.debug(tokenStore);
console.log('Token store initialized.');
}
module.exports = {
initializeTokenStore,
tokenStore,
};
+33
View File
@@ -0,0 +1,33 @@
function formatTime(format) {
if (format === undefined) format = 'yyyy-MM-dd hh:mm:ss';
const date = new Date();
const o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds(),
S: date.getMilliseconds(),
};
if (/(y+)/.test(format)) {
format = format.replace(
RegExp.$1,
(date.getFullYear() + '').substr(4 - RegExp.$1.length)
);
}
for (let k in o) {
if (new RegExp('(' + k + ')').test(format)) {
format = format.replace(
RegExp.$1,
RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)
);
}
}
return format;
}
module.exports = {
formatTime,
};
+68
View File
@@ -0,0 +1,68 @@
const axios = require('axios');
const { tokenStore } = require('./token');
const config = require('../config');
async function refreshToken() {
for (const item of tokenStore) {
item.token = await this.requestToken();
}
console.log('Token refreshed.');
}
async function requestToken(appId, appSecret) {
let token = '';
try {
let res = await axios.get(
`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}`
);
console.log(res);
if (res && res.data && res.data.access_token) {
token = res.data.access_token;
}
} catch (e) {
console.error(e);
}
return token;
}
async function pushWeChatMessage(userPrefix, message) {
// Reference: https://mp.weixin.qq.com/debug/cgi-bin/readtmpl?t=tmplmsg/faq_tmpl
let user = tokenStore.get(userPrefix);
let access_token = user.token;
let request_data = {
touser: user.wechatOpenId,
template_id: user.wechatTemplateId,
};
if (message.content) {
// TODO
// Generate html, save message to database and then return the id
let id = 'TODO';
request_data.url = `${config.href}${userPrefix}/${id}`;
}
request_data.data = { text: { value: message.description } };
let requestUrl = `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${access_token}`;
try {
let response = await axios.post(requestUrl, request_data);
if (response && response.data && response.data.errcode === '40001') {
await requestToken(user.wechatAppId, user.wechatAppSecret);
await axios.post(requestUrl, request_data);
}
console.log(response.data);
return {
success: true,
message: '',
};
} catch (e) {
console.error(e);
return {
success: false,
message: e.message,
};
}
}
module.exports = {
refreshToken,
requestToken,
pushWeChatMessage,
};
+7
View File
@@ -0,0 +1,7 @@
const config = {
port: process.env.PORT || 3000,
database: 'data.db',
href: 'https://github.com/',
};
module.exports = config;
-22
View File
@@ -1,22 +0,0 @@
module.exports = {
development: {
client: "sqlite3",
connection: {
filename: "./data.db",
},
},
staging: {
client: "sqlite3",
connection: {
filename: "./data.db",
},
},
production: {
client: "sqlite3",
connection: {
filename: "./data.db",
},
},
};
View File
View File
@@ -1,13 +0,0 @@
exports.up = function (knex) {
return knex.schema.createTable("messages", (tableBuilder) => {
tableBuilder.uuid("id").primary();
tableBuilder.string("title");
tableBuilder.string("status");
tableBuilder.string("created_by");
tableBuilder.string("created_time");
tableBuilder.string("description");
tableBuilder.text("content");
});
};
exports.down = function (knex) {};
+24
View File
@@ -0,0 +1,24 @@
const User = require('./user');
const Message = require('./message');
const sequelize = require('../common/database');
Message.belongsTo(User);
(async () => {
await sequelize.sync({ alter: true });
console.log('Database initialized.');
const isNoAdminExisted =
(await User.findOne({ where: { isAdmin: true } })) === null;
if (isNoAdminExisted) {
console.log('No admin user existed! Creating one for you.');
await User.create({
username: 'admin',
password: '123456',
isAdmin: true,
prefix: 'admin',
});
}
})();
exports.User = User;
exports.Message = Message;
+21 -16
View File
@@ -1,19 +1,24 @@
const db = require("../utils/database").db; const { DataTypes, Model } = require('sequelize');
const { v4: uuid } = require("uuid"); const sequelize = require('../common/database');
class Message { class Message extends Model {}
create(message) {
message.id = uuid();
return db("messages").insert(message);
}
getById(id) { Message.init(
return db("messages").where("id", id); {
} id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
title: DataTypes.STRING,
description: DataTypes.TEXT,
content: DataTypes.TEXT,
type: {
type: DataTypes.INTEGER,
defaultValue: 0,
},
},
{ sequelize }
);
deleteById(id) { module.exports = Message;
return db("messages").where("id", id).del();
}
}
module.exports.Message = new Message();
+48
View File
@@ -0,0 +1,48 @@
const { DataTypes, Model } = require('sequelize');
const sequelize = require('../common/database');
class User extends Model {}
User.init(
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
username: {
type: DataTypes.STRING,
unique: true,
},
password: {
type: DataTypes.STRING,
allowNull: false,
},
accessToken: {
type: DataTypes.STRING,
defaultValue: '',
},
email: {
type: DataTypes.STRING,
},
prefix: {
type: DataTypes.STRING,
unique: true,
},
isAdmin: {
type: DataTypes.BOOLEAN,
defaultValue: false,
},
isBlocked: {
type: DataTypes.BOOLEAN,
defaultValue: false,
},
wechatAppId: DataTypes.STRING,
wechatAppSecret: DataTypes.STRING,
wechatTemplateId: DataTypes.STRING,
wechatOpenId: DataTypes.STRING,
},
{ sequelize }
);
module.exports = User;
+6 -5
View File
@@ -10,17 +10,18 @@
"axios": "^0.21.1", "axios": "^0.21.1",
"cookie-parser": "~1.4.4", "cookie-parser": "~1.4.4",
"debug": "~2.6.9", "debug": "~2.6.9",
"dotenv": "^8.2.0",
"ejs": "^3.1.5", "ejs": "^3.1.5",
"express": "~4.16.1", "express": "~4.16.1",
"knex": "^0.21.5", "marked": "^1.2.7",
"marked": "^1.1.1",
"morgan": "~1.9.1", "morgan": "~1.9.1",
"sqlite3": "^5.0.0", "sequelize": "^6.3.5",
"uuid": "^8.3.0" "sqlite3": "^5.0.1"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^2.0.6", "nodemon": "^2.0.6",
"prettier": "^2.1.1" "prettier": "^2.1.1"
},
"prettier": {
"singleQuote": true
} }
} }
+24 -30
View File
@@ -1,35 +1,32 @@
const express = require("express"); const express = require('express');
const router = express.Router(); const router = express.Router();
const crypto = require("crypto"); const crypto = require('crypto');
const fs = require("fs"); const fs = require('fs');
router.all("/", (req, res, next) => { router.all('/', (req, res, next) => {
fs.promises fs.promises
.access("./.env") .access('./.env')
.then(() => { .then(() => {
res.render("info", { res.render('info', {
message: "服务已在运行。", message: '服务已在运行。',
}); });
<<<<<<< HEAD:routers/index.js
=======
// pushMessage( // pushMessage(
// req, // req,
// res, // res,
// `请注意,ip 地址为 ${req.ip} 的用户访问了你的消息通知服务,如果非你本人,则你的私有消息通知服务可能已被泄露,当前版本无法阻止其他用户通过本系统向你发送消息。` // `请注意,ip 地址为 ${req.ip} 的用户访问了你的消息通知服务,如果非你本人,则你的私有消息通知服务可能已被泄露,当前版本无法阻止其他用户通过本系统向你发送消息。`
// ); // );
>>>>>>> master:routes.js
}) })
.catch(() => { .catch(() => {
res.render("configure"); res.render('configure');
}); });
}); });
router.post("/configure", (req, res, next) => { router.post('/configure', (req, res, next) => {
fs.promises fs.promises
.access("./.env") .access('./.env')
.then(() => { .then(() => {
res.render("message", { res.render('message', {
message: ".env 文件已经存在,请手动删除该文件后重试!", message: '.env 文件已经存在,请手动删除该文件后重试!',
}); });
}) })
.catch(() => { .catch(() => {
@@ -40,51 +37,48 @@ router.post("/configure", (req, res, next) => {
`TEMPLATE_ID=${req.body.TEMPLATE_ID}\n` + `TEMPLATE_ID=${req.body.TEMPLATE_ID}\n` +
`OPEN_ID=${req.body.OPEN_ID}`; `OPEN_ID=${req.body.OPEN_ID}`;
fs.promises fs.promises
.writeFile("./.env", content, "utf8") .writeFile('./.env', content, 'utf8')
.then(() => { .then(() => {
res.render("message", { res.render('message', {
message: message:
".env 文件写入成功,程序即将自动关闭以应用写入的新的环境变量,需要进程守护程序自动重启应用或者手动重启。", '.env 文件写入成功,程序即将自动关闭以应用写入的新的环境变量,需要进程守护程序自动重启应用或者手动重启。',
}); });
process.exit(); process.exit();
}) })
.catch((e) => { .catch((e) => {
res.render("info", { res.render('info', {
message: "在尝试写入 .env 文件时发生错误:" + e, message: '在尝试写入 .env 文件时发生错误:' + e,
}); });
}); });
}); });
}); });
router.all("/verify", (req, res, next) => { router.all('/verify', (req, res, next) => {
// 验证消息来自微信服务器:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html // 验证消息来自微信服务器:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html
const { signature, timestamp, nonce, echostr } = req.query; const { signature, timestamp, nonce, echostr } = req.query;
const token = process.env.TOKEN; const token = process.env.TOKEN;
let tmp_array = [token, timestamp, nonce].sort(); let tmp_array = [token, timestamp, nonce].sort();
let tmp_string = tmp_array.join(""); let tmp_string = tmp_array.join('');
tmp_string = crypto.createHash("sha1").update(tmp_string).digest("hex"); tmp_string = crypto.createHash('sha1').update(tmp_string).digest('hex');
if (tmp_string === signature) { if (tmp_string === signature) {
res.send(echostr); res.send(echostr);
} else { } else {
res.send("verification failed"); res.send('verification failed');
} }
}); });
<<<<<<< HEAD:routers/index.js router.all('/push', (req, res, next) => {
=======
router.all("/push", (req, res, next) => {
let content = req.query.content || req.body.content; let content = req.query.content || req.body.content;
pushMessage(req, res, content); pushMessage(req, res, content);
}); });
router.get("/favicon.ico", (req, res, next) => { router.get('/favicon.ico', (req, res, next) => {
res.sendStatus(404); res.sendStatus(404);
}); });
router.all("/:content", (req, res, next) => { router.all('/:content', (req, res, next) => {
let content = req.params.content; let content = req.params.content;
pushMessage(req, res, content); pushMessage(req, res, content);
}); });
>>>>>>> master:routes.js
module.exports = router; module.exports = router;
+14 -14
View File
@@ -1,9 +1,9 @@
const express = require("express"); const express = require('express');
const lexer = require("marked").lexer; const lexer = require('marked').lexer;
const parser = require("marked").parser; const parser = require('marked').parser;
const Message = require("../models/message").Message; const Message = require('../models/message').Message;
const pushWeChatMessage = require("../utils/wechat").pushWeChatMessage; const pushWeChatMessage = require('../common/wechat').pushWeChatMessage;
const formatTime = require("../utils/utils").formatTime; const formatTime = require('../common/utils').formatTime;
const router = express.Router(); const router = express.Router();
@@ -11,16 +11,16 @@ function md2html(markdown) {
return parser(lexer(markdown)); return parser(lexer(markdown));
} }
router.get("/:description", (req, res, next) => { router.get('/:description', (req, res, next) => {
req.query.description = req.params.description; req.query.description = req.params.description;
next(); next();
}); });
router.all("/", (req, res) => { router.all('/', (req, res) => {
let message = { let message = {
title: req.query.title || req.body.title || "无标题", title: req.query.title || req.body.title || '无标题',
status: 1, status: 1,
created_by: "系统", // TODO created_by: '系统', // TODO
created_time: formatTime(), created_time: formatTime(),
description: req.query.description || req.body.description, description: req.query.description || req.body.description,
content: md2html(req.query.content || req.body.content), content: md2html(req.query.content || req.body.content),
@@ -42,16 +42,16 @@ router.all("/", (req, res) => {
}); });
}); });
router.get("/detail/:id", (req, res) => { router.get('/detail/:id', (req, res) => {
const id = req.params.id; const id = req.params.id;
Message.getById(id) Message.getById(id)
.then((value) => { .then((value) => {
console.log(value); console.log(value);
res.render("message", value[0]); res.render('message', value[0]);
}) })
.catch((reason) => { .catch((reason) => {
res.render("info", { res.render('info', {
message: "获取该消息时发生了错误:" + reason, message: '获取该消息时发生了错误:' + reason,
}); });
}); });
}); });
-9
View File
@@ -1,9 +0,0 @@
const knex = require("knex");
const environment = process.env.NODE_ENV || "development";
const configuration = require("../knexfile")[environment];
const db = knex(configuration);
module.exports = {
db: db,
};
-33
View File
@@ -1,33 +0,0 @@
function formatTime(format) {
if (format === undefined) format = "yyyy-MM-dd hh:mm:ss";
const date = new Date();
const o = {
"M+": date.getMonth() + 1,
"d+": date.getDate(),
"h+": date.getHours(),
"m+": date.getMinutes(),
"s+": date.getSeconds(),
S: date.getMilliseconds(),
};
if (/(y+)/.test(format)) {
format = format.replace(
RegExp.$1,
(date.getFullYear() + "").substr(4 - RegExp.$1.length)
);
}
for (let k in o) {
if (new RegExp("(" + k + ")").test(format)) {
format = format.replace(
RegExp.$1,
RegExp.$1.length === 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length)
);
}
}
return format;
}
module.exports = {
formatTime,
};
-36
View File
@@ -1,36 +0,0 @@
const axios = require("axios");
module.exports = {
requestToken: function (app) {
let token = "";
axios
.get(
`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${process.env.APP_ID}&secret=${process.env.APP_SECRET}`
)
.then((res) => {
if (res.data && res.data.access_token) {
console.log("Token requested.");
token = res.data.access_token;
app.locals.access_token = token;
process.env.access_token = token;
} else {
console.error(res.data);
}
});
return token;
},
pushWeChatMessage: function (description, link) {
// Reference: https://mp.weixin.qq.com/debug/cgi-bin/readtmpl?t=tmplmsg/faq_tmpl
let access_token = process.env.access_token;
let request_data = {
touser: process.env.OPEN_ID,
template_id: process.env.TEMPLATE_ID,
};
request_data.data = { text: { value: description } };
return axios.post(
`https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${access_token}`,
request_data
);
},
};