diff --git a/.gitignore b/.gitignore index 32af67b..4d8b7ee 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ node_modules data.db-journal .vscode *.db -package-lock.json \ No newline at end of file +package-lock.json +yarn.lock \ No newline at end of file diff --git a/app.js b/app.js index 9a2ba9b..ff18796 100644 --- a/app.js +++ b/app.js @@ -25,7 +25,7 @@ app.locals.isErrorMessage = false; setTimeout(async () => { // TODO: Here we need an improvement! I have tried EventEmitter but it's not working. :( await initializeTokenStore(); - await refreshToken(app); + await refreshToken(); setInterval(async () => refreshToken(), 100 * 60 * 1000); }, 1000); diff --git a/common/message.js b/common/message.js index 2a0c56c..9e1948c 100644 --- a/common/message.js +++ b/common/message.js @@ -1,30 +1,37 @@ +const { getUserDefaultMethod } = require('./token'); const { pushWeChatMessage } = require('./wechat'); +const { pushWeChatCorpMessage } = require('./wechat-corp'); const { pushEmailMessage } = require('./email'); const { Message } = require('../models'); async function processMessage(userPrefix, message) { if (message.email) { // If message has the attribute "email", override its type. - message.type = '1'; + message.type = 'email'; } - if (message.content && message.type === '0') { - message = await Message.create(message); + if (!message.type) { + message.type = getUserDefaultMethod(userPrefix); } - let result = { - success: false, - message: `unsupported message type ${message.type}`, - }; + if (message.content && message.type !== 'email') { + // If message is not email type, we should save it because we have to serve the page. + message = await Message.create(message, { raw: true }); + } + let result; switch (message.type) { - case '0': // WeChat message + case 'test': // WeChat message result = await pushWeChatMessage(userPrefix, message); break; - case '1': // Email message + case 'email': // Email message result = await pushEmailMessage(userPrefix, message); break; - case '2': // HTTP GET request - // TODO: HTTP GET request + case 'corp': // WeChar corp message + result = await pushWeChatCorpMessage(userPrefix, message); break; default: + result = { + success: false, + message: `unsupported message type ${message.type}`, + }; break; } return result; diff --git a/common/token.js b/common/token.js index e1d13f2..61918e2 100644 --- a/common/token.js +++ b/common/token.js @@ -7,41 +7,77 @@ async function initializeTokenStore() { if (process.env.MODE === '1') { console.log('Current mode is Heroku mode.'); let user = { + // Common + prefix: process.env.PREFIX, + accessToken: process.env.ACCESS_TOKEN, + defaultMethod: process.env.DEFAULT_METHOD, + // WeChat public account wechatAppId: process.env.WECHAT_APP_ID, wechatAppSecret: process.env.WECHAT_APP_SECRET, wechatTemplateId: process.env.WECHAT_TEMPLATE_ID, wechatOpenId: process.env.WECHAT_OPEN_ID, wechatVerifyToken: process.env.WECHAT_VERIFY_TOKEN, - prefix: process.env.PREFIX, + // Email email: process.env.EMAIL, smtpServer: process.env.SMTP_SERVER, smtpUser: process.env.SMTP_USER, smtpPass: process.env.SMTP_PASS, + // WeChat corporation account + corpId: process.env.CORP_ID, + corpAgentId: process.env.CORP_AGENT_ID, + corpAppSecret: process.env.CORP_APP_SECRET, + corpUserId: process.env.CORP_USER_ID, }; users.push(user); } else { - users = await User.findAll(); + users = await User.findAll({ + raw: true, + }); } users.forEach((user) => { - if (user.wechatAppId) { + if (user.prefix) { tokenStore.set(user.prefix, { + // Common + accessToken: user.accessToken, + defaultMethod: user.defaultMethod, + // WeChat test account wechatAppId: user.wechatAppId, wechatAppSecret: user.wechatAppSecret, wechatTemplateId: user.wechatTemplateId, wechatOpenId: user.wechatOpenId, wechatVerifyToken: user.wechatVerifyToken, - token: '', + wechatToken: '', + // Email email: user.email, smtpServer: user.smtpServer, smtpUser: user.smtpUser, smtpPass: user.smtpPass, + // WeChat corporation account + corpId: user.corpId, + corpAgentId: user.corpAgentId, + corpAppSecret: user.corpAppSecret, + corpUserId: user.corpUserId, + corpToken: '', }); } }); console.log('Token store initialized.'); } +function updateTokenStore(prefix, key, value) { + let user = tokenStore.get(prefix); + user[key] = value; + tokenStore.set(prefix, user); +} + +function getUserDefaultMethod(prefix) { + let user = tokenStore.get(prefix); + return user.defaultMethod; +} + module.exports = { initializeTokenStore, + updateTokenStore, + getUserDefaultMethod, tokenStore, }; diff --git a/common/wechat-corp.js b/common/wechat-corp.js new file mode 100644 index 0000000..0a6ce6c --- /dev/null +++ b/common/wechat-corp.js @@ -0,0 +1,94 @@ +const axios = require('axios'); +const { tokenStore, updateTokenStore } = require('./token'); +const config = require('../config'); + +async function refreshToken() { + for (let [key, value] of tokenStore) { + if (value.corpId) { + value.corpToken = await requestToken(value.corpId, value.corpAppSecret); + tokenStore.set(key, value); + } + } + console.log('Token refreshed.'); +} + +async function requestToken(corpId, corpAppSecret) { + // Reference: https://work.weixin.qq.com/api/doc/90000/90135/91039 + + let token = ''; + try { + let res = await axios.get( + `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${corpId}&corpsecret=${corpAppSecret}` + ); + // console.debug(res); + if (res && res.data) { + if (res.data.access_token) { + token = res.data.access_token; + } else { + console.error(res.data); + } + } + } catch (e) { + console.error(e); + } + return token; +} + +async function pushWeChatCorpMessage(userPrefix, message) { + // Reference: https://work.weixin.qq.com/api/doc/90000/90135/90236 + + let user = tokenStore.get(userPrefix); + if (!user) { + return { + success: false, + message: `不存在的前缀:${userPrefix},请注意大小写`, + }; + } + let access_token = user.corpToken; + let request_data = { + msgtype: 'textcard', + touser: user.corpUserId, + agentid: user.corpAgentId, + textcard: { + title: message.title, + description: message.description, + }, + }; + if (message.content) { + request_data.textcard.url = `${config.href}message/${message.id}`; + } + let requestUrl = `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${access_token}`; + try { + let response = await axios.post(requestUrl, request_data); + if (response && response.data && response.data.errcode !== 0) { + // Failed to push message, get a new token and try again. + access_token = await requestToken(user.corpId, user.corpAppSecret); + updateTokenStore(userPrefix, 'corpToken', access_token); + requestUrl = `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${access_token}`; + response = await axios.post(requestUrl, request_data); + } + if (response.data.errcode === 0) { + return { + success: true, + message: 'ok', + }; + } else { + return { + success: false, + message: response.data.errmsg, + }; + } + } catch (e) { + console.error(e); + return { + success: false, + message: e.message, + }; + } +} + +module.exports = { + refreshToken, + requestToken, + pushWeChatCorpMessage, +}; diff --git a/common/wechat.js b/common/wechat.js index f7bf21e..286064d 100644 --- a/common/wechat.js +++ b/common/wechat.js @@ -1,11 +1,16 @@ const axios = require('axios'); -const { tokenStore } = require('./token'); +const { tokenStore, updateTokenStore } = require('./token'); const config = require('../config'); async function refreshToken() { for (let [key, value] of tokenStore) { - value.token = await requestToken(value.wechatAppId, value.wechatAppSecret); - tokenStore.set(key, value); + if (value.wechatAppId) { + value.wechatToken = await requestToken( + value.wechatAppId, + value.wechatAppSecret + ); + tokenStore.set(key, value); + } } console.log('Token refreshed.'); } @@ -39,22 +44,24 @@ async function pushWeChatMessage(userPrefix, message) { message: `不存在的前缀:${userPrefix},请注意大小写`, }; } - let access_token = user.token; + let access_token = user.wechatToken; let request_data = { touser: user.wechatOpenId, template_id: user.wechatTemplateId, }; if (message.content) { request_data.url = `${config.href}message/${message.id}`; - console.debug(`http://localhost:3000/message/${message.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 !== 0) { - await requestToken(user.wechatAppId, user.wechatAppSecret); - await axios.post(requestUrl, request_data); + // Failed to push message, get a new token and try again. + let token = await requestToken(user.wechatAppId, user.wechatAppSecret); + updateTokenStore(userPrefix, 'wechatToken', token); + requestUrl = `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${access_token}`; + response = await axios.post(requestUrl, request_data); } if (response.data.errcode === 0) { return { diff --git a/models/index.js b/models/index.js index 31441f3..18608b7 100644 --- a/models/index.js +++ b/models/index.js @@ -5,7 +5,7 @@ const sequelize = require('../common/database'); Message.belongsTo(User); (async () => { - await sequelize.sync({ alter: true }); + await sequelize.sync(); console.log('Database initialized.'); const isNoAdminExisted = (await User.findOne({ where: { isAdmin: true } })) === null; diff --git a/models/message.js b/models/message.js index 59ac7e1..056cda0 100644 --- a/models/message.js +++ b/models/message.js @@ -14,8 +14,8 @@ Message.init( description: DataTypes.TEXT, content: DataTypes.TEXT, type: { - type: DataTypes.INTEGER, - defaultValue: 0, + type: DataTypes.STRING, + defaultValue: 'test', }, }, { sequelize } diff --git a/models/user.js b/models/user.js index 4da811c..11c04f4 100644 --- a/models/user.js +++ b/models/user.js @@ -37,17 +37,28 @@ User.init( type: DataTypes.BOOLEAN, defaultValue: false, }, + defaultMethod:{ + type: DataTypes.STRING, + defaultValue: 'test' + }, + // WeChat public account wechatAppId: DataTypes.STRING, wechatAppSecret: DataTypes.STRING, wechatTemplateId: DataTypes.STRING, wechatOpenId: DataTypes.STRING, wechatVerifyToken: DataTypes.STRING, + // Email smtpServer: { type: DataTypes.STRING, defaultValue: 'smtp.qq.com', }, smtpUser: DataTypes.STRING, smtpPass: DataTypes.STRING, + // WeChat corporation + corpId: DataTypes.STRING, + corpAgentId: DataTypes.STRING, + corpAppSecret: DataTypes.STRING, + corpUserId: DataTypes.STRING }, { sequelize } ); diff --git a/routers/user.js b/routers/user.js index 7290e35..4720acb 100644 --- a/routers/user.js +++ b/routers/user.js @@ -24,7 +24,6 @@ router.all('/:userPrefix/verify', (req, res, next) => { router.all('/:userPrefix/:description', async (req, res, next) => { const userPrefix = req.params.userPrefix; let message = { - type: '0', title: '无标题', description: req.params.description, }; @@ -34,7 +33,7 @@ router.all('/:userPrefix/:description', async (req, res, next) => { router.all('/:userPrefix', async (req, res, next) => { const userPrefix = req.params.userPrefix; let message = { - type: req.query.type || req.body.type || '0', + type: req.query.type || req.body.type, title: req.query.title || req.body.title || '无标题', description: req.query.description || req.body.description || '无描述', content: req.query.content || req.body.content,