oak-general-business/lib/endpoints/wechat.js

746 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerWeChatPublicEventCallback = registerWeChatPublicEventCallback;
const tslib_1 = require("tslib");
const assert_1 = require("oak-domain/lib/utils/assert");
const url_1 = tslib_1.__importDefault(require("url"));
const sha1_1 = tslib_1.__importDefault(require("sha1"));
const x2js_1 = tslib_1.__importDefault(require("x2js"));
const WechatSDK_1 = tslib_1.__importDefault(require("oak-external-sdk/lib/WechatSDK"));
const uuid_1 = require("oak-domain/lib/utils/uuid");
const domain_1 = require("oak-domain/lib/utils/domain");
const session_1 = require("../aspects/session");
const application_1 = require("../aspects/application");
const application_2 = require("../utils/application");
const X2Js = new x2js_1.default();
function assertFromWeChat(query, config) {
const { signature, nonce, timestamp } = query;
const token = config.server?.token;
const stringArray = [nonce, timestamp, token];
const sign = stringArray.sort().reduce((acc, val) => {
acc += val;
return acc;
});
const sha1Sign = (0, sha1_1.default)(sign);
return signature === sha1Sign;
}
const CALLBACK = {};
function registerWeChatPublicEventCallback(appId, callback) {
(0, assert_1.assert)(!CALLBACK.hasOwnProperty(appId));
CALLBACK[appId] = callback;
}
/**
* 用户取关事件注意要容wechatUser不存在的情况
* @param openId
* @param context
* @returns
*/
async function setUserUnsubscribed(openId, context) {
const { id: applicationId, type: applicationType } = context.getApplication();
const list = await context.select('wechatUser', {
data: {
id: 1,
subscribed: 1,
subscribedAt: 1,
},
filter: {
applicationId: applicationId,
openId,
},
indexFrom: 0,
count: 10,
}, { dontCollect: true });
if (list && list.length > 0) {
(0, assert_1.assert)(list.length === 1);
const weChatUser = list[0];
if (weChatUser.subscribed) {
await context.operate('wechatUser', {
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'update',
data: {
subscribed: false,
unsubscribedAt: Date.now(),
},
filter: {
id: weChatUser.id,
},
}, { dontCollect: true });
}
}
else {
await context.operate('wechatUser', {
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'create',
data: {
id: await (0, uuid_1.generateNewIdAsync)(),
subscribed: false,
applicationId: applicationId,
openId,
origin: applicationType === 'wechatPublic' ? 'public' : 'web',
},
}, { dontCollect: true });
}
return;
}
async function setUserSubscribed(openId, eventKey, context) {
const { id: applicationId, type: applicationType } = context.getApplication();
const list = await context.select('wechatUser', {
data: {
id: 1,
subscribed: 1,
subscribedAt: 1,
},
filter: {
applicationId,
openId,
},
indexFrom: 0,
count: 1,
}, { dontCollect: true });
const now = Date.now();
const data = {
// activeAt: now,
};
const doUpdate = async () => {
if (list && list.length > 0) {
(0, assert_1.assert)(list.length === 1);
const wechatUser = list[0];
if (!wechatUser.subscribed) {
Object.assign(data, {
subscribed: true,
subscribedAt: now,
});
}
return await context.operate('wechatUser', {
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'update',
data,
filter: {
id: wechatUser.id,
},
}, { dontCollect: true });
}
Object.assign(data, {
id: await (0, uuid_1.generateNewIdAsync)(),
subscribed: true,
subscribedAt: now,
origin: applicationType === 'wechatPublic' ? 'public' : 'web',
applicationId,
openId,
});
// 这里试着直接把user也创建出来by Xc 20190720
/**
* 这里不能创建user否则会出现一个weChatUser有openId和userId却没有unionId
* 当同一个user先从小程序登录再从公众号登录时就会生成两个user
*/
/* return warden.insertEntity(tables.user, {
state: UserState.normal,
activeAt: Date.now(),
}, txn).then(
(user) => {
assign(data, { userId: user.id });
return warden.insertEntity(tables.weChatUser, data, txn);
}
);*/
return await context.operate('wechatUser', {
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'create',
data,
}, { dontCollect: true });
};
if (eventKey) {
// 如果带着场景值,需要查找对应的二维码,如果有公共逻辑在这里处理
let sceneStr;
if (eventKey.startsWith('qrscene_')) {
sceneStr = eventKey.slice(eventKey.indexOf('qrscene_') + 8);
}
else {
sceneStr = eventKey;
}
// sceneStr是id压缩后的字符串
const wcqId = (0, uuid_1.expandUuidTo36Bytes)(sceneStr);
const [wechatQrCode] = await context.select('wechatQrCode', {
data: {
id: 1,
entity: 1,
entityId: 1,
expired: 1,
},
filter: {
id: wcqId,
},
indexFrom: 0,
count: 10,
}, { dontCollect: true });
if (wechatQrCode) {
const application = context.getApplication();
const { type, config, systemId } = application;
(0, assert_1.assert)(type === 'wechatPublic');
const { appId, appSecret, location } = config;
const wechatInstance = WechatSDK_1.default.getInstance(appId, 'wechatPublic', appSecret);
const { expired } = wechatQrCode;
if (expired) {
// 若二维码已经过期,则直接告知用户已经过期
wechatInstance.sendServeMessage({
openId,
type: 'text',
content: '此二维码已经过期,请重新获取',
});
return;
}
const { entity, entityId } = wechatQrCode;
const scanPage = location?.scanPage || 'wechatQrCode/scan';
switch (entity) {
case 'user': {
// 裂变获得的用户
if (list[0] && !list[0].userId) {
Object.assign(data, { userId: entityId });
}
break;
}
case 'userEntityGrant': {
// 授权过来的用户,推送接受分享的客服消息给他
const [userEntityGrant] = await context.select('userEntityGrant', {
data: {
id: 1,
qrCodeType: 1,
granter: {
id: 1,
name: 1,
nickname: 1,
},
expired: 1,
relationEntity: 1,
},
filter: {
id: entityId,
},
}, { dontCollect: true });
const { id, granter, expired, relationEntity: entity2, qrCodeType, } = userEntityGrant;
const name = granter?.name || granter?.nickname || '某用户';
if (qrCodeType === 'wechatPublicForMp') {
// 找到相关的小程序
const [appMp] = await context.select('application', {
data: {
id: 1,
config: 1,
},
filter: {
systemId,
type: 'wechatMp',
},
}, { dontCollect: true });
(0, assert_1.assert)(appMp, '公众号推送小程序码时找不到关联的小程序');
const { config } = appMp;
const { appId } = config;
const url = (0, domain_1.composeUrl)(`pages/${scanPage}/index`, {
scene: sceneStr,
time: `${Date.now()}`,
});
// 发送小程序链接
const content = `${name}为您创建了一个授权,<a href='#' data-miniprogram-appid='${appId}' data-miniprogram-path='${url}'>请点击领取</a>`;
if (!expired) {
wechatInstance.sendServeMessage({
openId,
type: 'text',
content,
});
}
else {
wechatInstance.sendServeMessage({
openId,
type: 'text',
content: '您好,您扫描的二维码已经过期,请联系管理员重新获取',
});
}
}
else {
// const [domain] = await context.select(
// 'domain',
// {
// data: {
// id: 1,
// url: 1,
// apiPath: 1,
// protocol: 1,
// port: 1,
// },
// filter: {
// system: {
// application$system: {
// id: applicationId,
// },
// },
// },
// },
// { dontCollect: true }
// );
// assert(
// domain,
// `处理userEntityGrant时找不到对应的domainapplicationId是「${applicationId}」`
// );
// const url = composeDomainUrl(
// domain as EntityDict['domain']['Schema'],
// scanPage,
// {
// scene: sceneStr,
// time: `${Date.now()}`,
// }
// );
//从application的config中获取前端访问地址
const url = (0, application_2.composeLocationUrl)(location, scanPage, {
scene: sceneStr,
time: `${Date.now()}`,
});
if (!expired) {
wechatInstance.sendServeMessage({
openId,
type: 'news',
url,
title: `${name}为您创建了一个授权`,
description: '请接受',
picurl: 'http://img95.699pic.com/element/40018/2473.png_860.png',
});
}
else {
wechatInstance.sendServeMessage({
openId,
type: 'text',
content: '您好,您扫描的二维码已经过期,请联系管理员重新获取',
});
}
}
break;
}
case 'wechatLogin': {
const [wechatLogin] = await context.select('wechatLogin', {
data: {
id: 1,
qrCodeType: 1,
expired: 1,
userId: 1,
type: 1,
successed: 1,
},
filter: {
id: entityId,
},
}, { dontCollect: true });
const { qrCodeType, expired, userId, type, successed } = wechatLogin;
if (qrCodeType === 'wechatPublicForMp') {
// todo 公众号跳小程序 绑定login 后面再实现
}
else {
//从application的config中获取前端访问地址
const url = (0, application_2.composeLocationUrl)(location, scanPage, {
scene: sceneStr,
time: `${Date.now()}`,
});
const title = type === 'bind' ? '立即绑定' : '立即登录';
const description = type === 'bind' ? '去绑定' : '去登录';
if (!expired) {
wechatInstance.sendServeMessage({
openId,
type: 'news',
url,
title,
description,
picurl: 'http://img95.699pic.com/element/40018/2473.png_860.png',
});
}
else {
wechatInstance.sendServeMessage({
openId,
type: 'text',
content: '您好,您扫描的二维码已经过期,请重新生成',
});
}
}
break;
}
default: {
break;
}
}
}
else {
console.warn(`线上有扫描二维码场景值但找不到对应的qrCodeeventKey是${eventKey}`);
}
}
await doUpdate();
return;
}
async function setClickEventKey(openId, eventKey, context) {
const application = context.getApplication();
const { type, config, systemId, id: applicationId } = application;
(0, assert_1.assert)(type === 'wechatPublic');
const { appId, appSecret } = config;
const wechatInstance = WechatSDK_1.default.getInstance(appId, 'wechatPublic', appSecret);
if (eventKey) {
var indexOfDollarSign = eventKey.indexOf('$');
var resultString = eventKey.substring(0, indexOfDollarSign);
const [wechatMenu] = await context.select('wechatMenu', {
data: {
id: 1,
applicationId: 1,
menuConfig: 1,
wechatPublicTagId: 1,
wechatPublicTag: {
wechatId: 1,
},
},
filter: indexOfDollarSign !== -1
? {
applicationId,
wechatPublicTag: {
wechatId: Number(resultString),
},
}
: {
applicationId,
wechatPublicTagId: {
$exists: false,
},
},
}, { dontCollect: true });
if (wechatMenu) {
let content = null;
wechatMenu.menuConfig?.button.map((ele) => {
if (ele.key === eventKey) {
content = ele.content;
}
else if (ele.sub_button && ele.sub_button.length > 0) {
var subEle = ele.sub_button.find((sub) => {
return sub.key === eventKey;
});
if (subEle) {
content = subEle.content;
}
}
});
if (content) {
wechatInstance.sendServeMessage({
openId,
type: 'text',
content,
});
return;
}
}
}
}
async function setSubscribedEventKey(openId, eventKey, context) {
const application = context.getApplication();
const { type, config, systemId, id: applicationId } = application;
(0, assert_1.assert)(type === 'wechatPublic');
const { appId, appSecret } = config;
const wechatInstance = WechatSDK_1.default.getInstance(appId, 'wechatPublic', appSecret);
if (eventKey) {
const [wechatPublicAutoReply] = await context.select('wechatPublicAutoReply', {
data: {
id: 1,
applicationId: 1,
content: 1,
event: 1,
type: 1,
},
filter: {
applicationId,
event: 'subscribe',
},
}, { dontCollect: true });
if (wechatPublicAutoReply) {
let content = null;
if (wechatPublicAutoReply.type === 'text' &&
wechatPublicAutoReply.content &&
wechatPublicAutoReply.content.text) {
content = wechatPublicAutoReply.content.text;
wechatInstance.sendServeMessage({
openId,
type: 'text',
content,
});
return;
}
}
}
}
async function onWeChatPublicEvent(data, context) {
const { ToUserName, FromUserName, CreateTime, MsgType, Event, EventKey } = data;
const appId = context.getApplicationId();
let evt;
// 如果有应用注入的事件回调则处理之,不依赖其返回
if (CALLBACK[appId]) {
await CALLBACK[appId](data, context);
}
// 接收事件推送
if (MsgType === 'event') {
if (Event) {
const event = Event.toLowerCase();
switch (event) {
case 'subscribe':
await setUserSubscribed(FromUserName, EventKey, context);
evt = `用户${FromUserName}关注公众号`;
break;
case 'scan':
await setUserSubscribed(FromUserName, EventKey, context);
evt = `用户${FromUserName}再次扫描带${EventKey}键值的二维码`;
break;
case 'unsubscribe': {
await setUserUnsubscribed(FromUserName, context);
evt = `用户${FromUserName}取关`;
break;
}
case 'location': {
evt = `用户${FromUserName}上传了地理位置信息`;
break;
}
case 'click': {
await setClickEventKey(FromUserName, EventKey, context);
evt = `用户${FromUserName}点击菜单【${EventKey}`;
break;
}
case 'view': {
evt = `用户${FromUserName}点击菜单跳转链接【${EventKey}`;
break;
}
case 'templatesendjobfinish': {
// 模板消息发送完成去更新对应的messageSent对象
// 这个在线上测试没法通过返回的msgId不符合不知道为什么
const { MsgID: msgId, Status: status, FromUserName: openId, } = data;
evt = `应用${appId}的用户${FromUserName}发来了${Event}事件,内容是${JSON.stringify(data)}`;
break;
}
default: {
evt = `应用${appId}的用户${FromUserName}发来了${Event}事件,内容是${JSON.stringify(data)}`;
break;
}
}
if (process.env.NODE_ENV === 'development') {
console.log(evt);
}
return {
content: '',
contentType: 'application/text',
};
}
}
(0, assert_1.assert)(MsgType);
// 接收普通消息
const content = '<xml>' +
`<ToUserName>${FromUserName}</ToUserName>` +
`<FromUserName>${ToUserName}</FromUserName>` +
`<CreateTime>${CreateTime}</CreateTime>` +
'<MsgType>transfer_customer_service</MsgType>' +
'</xml>';
const { Content, Title, Description, Url, PicUrl } = data;
switch (MsgType) {
case 'text': {
evt = `接收到来自用户「${MsgType}」消息:「${Content}`;
break;
}
case 'link': {
evt = `接收到来自用户「${MsgType}」消息Title =>「${Title}」, Description =>「${Description}」, Url =>「${Url}`;
break;
}
case 'image': {
evt = `接收到来自用户「${MsgType}」消息:「${PicUrl}`;
break;
}
default: {
evt = `接收到来自用户「${MsgType}」消息`;
break;
}
}
if (process.env.NODE_ENV === 'development') {
console.log(evt);
}
try {
await (0, session_1.createSession)({
data,
type: 'wechatPublic',
entity: 'application',
entityId: appId,
}, context);
}
catch (err) {
console.error('onWeChatPublicEvent', err);
return {
content,
contentType: 'application/xml',
};
}
return {
content,
contentType: 'application/xml',
};
}
async function onWeChatMpEvent(data, context) {
const appId = context.getApplicationId();
try {
await (0, session_1.createSession)({
data,
type: 'wechatMp',
entity: 'application',
entityId: appId,
}, context);
}
catch (err) {
console.error('onWeChatMpEvent', err);
return {
content: 'success',
};
}
return {
content: 'success',
};
}
const endpoints = {
wechatPublicEvent: [
{
name: '微信公众号回调接口',
method: 'post',
params: ['appId'],
fn: async (context, params, headers, req, body) => {
const { appId } = params;
if (!appId) {
console.error('applicationId参数不存在');
console.log(JSON.stringify(body));
return '';
}
await context.setApplication(appId);
const { xml: data } = X2Js.xml2js(body);
const { content, contentType } = await onWeChatPublicEvent(data, context);
return content;
},
},
{
name: '微信公众号验证接口',
method: 'get',
params: ['appId'],
fn: async (context, params, body, req, headers) => {
const { searchParams } = new url_1.default.URL(`http://${req.headers.host}${req.url}`);
const { appId } = params;
if (!appId) {
console.error('applicationId参数不存在');
const echostr = searchParams.get('echostr');
return echostr;
}
const [application] = await context.select('application', {
data: {
id: 1,
config: 1,
},
filter: {
id: appId,
},
}, {});
if (!application) {
throw new Error(`未找到${appId}对应的app`);
}
const signature = searchParams.get('signature');
const timestamp = searchParams.get('timestamp');
const nonce = searchParams.get('nonce');
const isWeChat = assertFromWeChat({ signature, timestamp, nonce }, application.config);
if (isWeChat) {
const echostr = searchParams.get('echostr');
return echostr;
}
else {
throw new Error('Verify Failed');
}
},
},
],
wechatMpEvent: [
{
name: '微信小程序回调接口',
method: 'post',
params: ['appId'],
fn: async (context, params, headers, req, body) => {
const { appId } = params;
if (!appId) {
console.error('applicationId参数不存在');
console.log(JSON.stringify(body));
return '';
}
await context.setApplication(appId);
const application = context.getApplication();
const { config } = application;
const { server } = config;
if (!server) {
throw new Error(`请配置:“微信小程序-服务器配置”`);
}
if (server?.dataFormat === 'json') {
const { content } = await onWeChatMpEvent(body, context);
return content;
}
else {
const { xml: data } = X2Js.xml2js(body);
const { content } = await onWeChatMpEvent(data, context);
return content;
}
},
},
{
name: '微信小程序验证接口',
method: 'get',
params: ['appId'],
fn: async (context, params, body, req, headers) => {
const { searchParams } = new url_1.default.URL(`http://${req.headers.host}${req.url}`);
const { appId } = params;
if (!appId) {
console.error('applicationId参数不存在');
const echostr = searchParams.get('echostr');
return echostr;
}
const [application] = await context.select('application', {
data: {
id: 1,
config: 1,
},
filter: {
id: appId,
},
}, {});
if (!application) {
throw new Error(`未找到${appId}对应的app`);
}
const signature = searchParams.get('signature');
const timestamp = searchParams.get('timestamp');
const nonce = searchParams.get('nonce');
const isWeChat = assertFromWeChat({ signature, timestamp, nonce }, application.config);
if (isWeChat) {
const echostr = searchParams.get('echostr');
return echostr;
}
else {
throw new Error('Verify Failed');
}
},
},
],
wechatMaterial: [
{
name: '获取微信素材',
method: 'get',
fn: async (context, params, headers, req, body) => {
const { searchParams } = new url_1.default.URL(`http://${req.headers.host}${req.url}`);
const applicationId = searchParams.get('applicationId');
const mediaId = searchParams.get('mediaId');
const isPermanent = searchParams.get('isPermanent');
const base64 = await (0, application_1.getMaterial)({
applicationId: applicationId,
mediaId: mediaId,
isPermanent: isPermanent === 'true',
}, context);
// 微信临时素材 公众号只支持image和video小程序只支持image
// 现只支持image
const af = Buffer.from(base64, 'base64');
return af;
},
},
],
};
exports.default = endpoints;