"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}为您创建了一个授权,请点击领取`;
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时,找不到对应的domain,applicationId是「${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(`线上有扫描二维码场景值,但找不到对应的qrCode,eventKey是${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 = '' +
`${FromUserName}` +
`${ToUserName}` +
`${CreateTime}` +
'transfer_customer_service' +
'';
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;