oak-external-sdk/lib/service/wechat/WechatPublic.js

551 lines
20 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.WechatPublicInstance = void 0;
const tslib_1 = require("tslib");
require('../../fetch');
const crypto_1 = tslib_1.__importDefault(require("crypto"));
const buffer_1 = require("buffer");
const url_1 = tslib_1.__importDefault(require("url"));
class WechatPublicInstance {
appId;
appSecret;
accessToken;
refreshAccessTokenHandler;
externalRefreshFn;
constructor(appId, appSecret, accessToken, externalRefreshFn) {
this.appId = appId;
this.appSecret = appSecret;
this.externalRefreshFn = externalRefreshFn;
if (!appSecret && !externalRefreshFn) {
throw new Error('appSecret和externalRefreshFn必须至少支持一个');
}
if (accessToken) {
this.accessToken = accessToken;
}
else {
this.refreshAccessToken();
}
}
async getAccessToken() {
while (true) {
if (this.accessToken) {
return this.accessToken;
}
await new Promise((resolve) => setTimeout(() => resolve(0), 500));
}
}
async access(url, mockData, init) {
if (process.env.NODE_ENV === 'development' && mockData) {
return mockData;
}
const response = await global.fetch(url, init);
const { headers, status } = response;
if (![200, 201].includes(status)) {
throw new Error(`微信服务器返回不正确应答:${status}`);
}
const contentType = headers['Content-Type'] || headers.get('Content-Type');
if (contentType.includes('application/json')) {
const json = await response.json();
if (typeof json.errcode === 'number' && json.errcode !== 0) {
if ([40001, 42001].includes(json.errcode)) {
return this.refreshAccessToken(url, init);
}
throw new Error(`调用微信接口返回出错code是${json.errcode},信息是${json.errmsg}`);
}
return json;
}
if (contentType.includes('text') ||
contentType.includes('xml') ||
contentType.includes('html')) {
const data = await response.text();
return data;
}
if (contentType.includes('application/octet-stream')) {
return await response.arrayBuffer();
}
return response;
}
async code2Session(code) {
const result = await this.access(`https://api.weixin.qq.com/sns/oauth2/access_token?appid=${this.appId}&secret=${this.appSecret}&code=${code}&grant_type=authorization_code`);
const { access_token, openid, unionid, scope, refresh_token, is_snapshotuser, expires_in, } = typeof result === 'string' ? JSON.parse(result) : result; // 这里微信返回的数据有时候竟然是text/plain
return {
accessToken: access_token,
openId: openid,
unionId: unionid,
scope: scope,
refreshToken: refresh_token,
isSnapshotUser: !!is_snapshotuser,
atExpiredAt: Date.now() + expires_in * 1000,
rtExpiredAt: Date.now() + 30 * 86400 * 1000,
};
}
async refreshUserAccessToken(refreshToken) {
const result = await this.access(`https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=${this.appId}&grant_type=refresh_token&refresh_token=${refreshToken}`);
const { access_token, refresh_token, expires_in, scope } = result;
return {
accessToken: access_token,
refreshToken: refresh_token,
atExpiredAt: Date.now() + expires_in * 1000,
scope: scope,
};
}
async getUserInfo(accessToken, openId) {
const result = await this.access(`https://api.weixin.qq.com/sns/userinfo?access_token=${accessToken}&openid=${openId}&lang=zh_CN`);
const { nickname, sex, headimgurl } = result;
return {
nickname: nickname,
gender: sex === 1 ? 'male' : sex === 2 ? 'female' : undefined,
avatar: headimgurl,
};
}
async getTags() {
const myInit = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
};
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/tags/get?access_token=${token}`, undefined, myInit);
return result;
}
async getCurrentMenu() {
const myInit = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
};
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/get_current_selfmenu_info?access_token=${token}`, undefined, myInit);
return result;
}
async getMenu() {
const myInit = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
};
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/menu/get?access_token=${token}`, undefined, myInit);
return result;
}
async createMenu(menuConfig) {
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(menuConfig),
};
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/menu/create?access_token=${token}`, undefined, myInit);
const { errcode } = result;
if (errcode === 0) {
return Object.assign({ success: true }, result);
}
return Object.assign({ success: false }, result);
}
async createConditionalMenu(menuConfig) {
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(menuConfig),
};
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/menu/addconditional?access_token=${token}`, undefined, myInit);
const { errcode } = result;
if (errcode === 0) {
return Object.assign({ success: true }, result);
}
return Object.assign({ success: false }, result);
}
async deleteConditionalMenu(menuid) {
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
menuid
}),
};
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/menu/delconditional?access_token=${token}`, undefined, myInit);
const { errcode } = result;
if (errcode === 0) {
return Object.assign({ success: true }, result);
}
return Object.assign({ success: false }, result);
}
async refreshAccessToken(url, init) {
const result = this.externalRefreshFn
? await this.externalRefreshFn(this.appId)
: await this.access(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${this.appId}&secret=${this.appSecret}`);
const { access_token, expires_in } = result;
this.accessToken = access_token;
// 生成下次刷新的定时器
console.log((expires_in - 10) * 1000);
this.refreshAccessTokenHandler = setTimeout(() => {
this.refreshAccessToken();
}, (expires_in - 10) * 1000);
if (url) {
const url2 = new url_1.default.URL(url);
url2.searchParams.set('access_token', access_token);
return this.access(url2.toString(), {}, init);
}
}
decryptData(sessionKey, encryptedData, iv, signature) {
const skBuf = buffer_1.Buffer.from(sessionKey, 'base64');
// const edBuf = Buffer.from(encryptedData, 'base64');
const ivBuf = buffer_1.Buffer.from(iv, 'base64');
const decipher = crypto_1.default.createDecipheriv('aes-128-cbc', skBuf, ivBuf);
// 设置自动 padding 为 true删除填充补位
decipher.setAutoPadding(true);
let decoded = decipher.update(encryptedData, 'base64', 'utf8');
decoded += decipher.final('utf8');
const data = JSON.parse(decoded);
if (data.watermark.appid !== this.appId) {
throw new Error('Illegal Buffer');
}
return data;
}
async getQrCode(options) {
const { sceneId, sceneStr, expireSeconds, isPermanent } = options;
if (!sceneId && !sceneStr) {
throw new Error('Missing sceneId or sceneStr');
}
const scene = sceneId
? {
scene_id: sceneId,
}
: {
scene_str: sceneStr,
};
let actionName = sceneId ? 'QR_SCENE' : 'QR_STR_SCENE';
let myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
expire_seconds: expireSeconds,
action_name: actionName,
action_info: {
scene,
},
}),
};
if (isPermanent) {
actionName = sceneId ? 'QR_LIMIT_SCENE' : 'QR_LIMIT_STR_SCENE';
myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
action_name: actionName,
action_info: {
scene,
},
}),
};
}
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=${token}`,
// {
// ticket: `ticket${Date.now()}`,
// url: `http://mock/q/${sceneId ? sceneId : sceneStr}`,
// expireSeconds: expireSeconds,
// },
myInit);
return {
ticket: result.ticket,
url: result.url,
expireSeconds: result.expire_seconds,
};
}
async sendTemplateMessage(options) {
const { openId, templateId, url, data, miniProgram, clientMsgId } = options;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
touser: openId,
template_id: templateId,
url,
miniProgram,
client_msg_id: clientMsgId,
data,
}),
};
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${token}`,
// {
// errcode: 0,
// errmsg: 'ok',
// msgid: Date.now(),
// },
myInit);
const { errcode } = result;
if (errcode === 0) {
return Object.assign({ success: true }, result);
}
return Object.assign({ success: false }, result);
}
async sendServeMessage(options) {
const { openId, type } = options;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
};
switch (type) {
case 'text': {
Object.assign(myInit, {
body: JSON.stringify({
touser: openId,
msgtype: 'text',
text: {
content: options.content,
},
}),
});
break;
}
case 'news': {
Object.assign(myInit, {
body: JSON.stringify({
touser: openId,
msgtype: 'news',
news: {
articles: [
{
title: options.title,
description: options.description,
url: options.url,
picurl: options.picurl,
},
],
},
}),
});
break;
}
case 'mp': {
Object.assign(myInit, {
body: JSON.stringify({
touser: openId,
msgtype: 'miniprogrampage',
miniprogrampage: {
title: options.data.title,
appid: options.data.appId,
pagepath: options.data.pagepath,
thumb_media_id: options.data.thumbnailId,
},
}),
});
break;
}
default: {
throw new Error('当前消息类型暂不支持');
}
}
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${token}`,
// {
// errcode: 0,
// errmsg: 'ok',
// },
myInit);
const { errcode } = result;
if (errcode === 0) {
return Object.assign({ success: true }, result);
}
return Object.assign({ success: false }, result);
}
async batchGetArticle(options) {
const { offset, count, noContent } = options;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
offset,
count,
no_content: noContent,
}),
};
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/freepublish/batchget?access_token=${token}`, undefined, myInit);
const { errcode } = result;
if (!errcode) {
return result;
}
throw new Error(JSON.stringify(result));
}
async getArticle(options) {
const { article_id } = options;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
article_id,
}),
};
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/freepublish/getarticle?access_token=${token}`, undefined, myInit);
const { errcode } = result;
if (!errcode) {
return result;
}
throw new Error(JSON.stringify(result));
}
async createMaterial(options) {
const { type, media, description } = options;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
};
if (type === 'video') {
Object.assign(myInit, {
body: JSON.stringify({
type,
media,
description,
}),
});
}
else {
Object.assign(myInit, {
body: JSON.stringify({
type,
media,
}),
});
}
;
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=${token}`, undefined, myInit);
const { errcode } = result;
if (!errcode) {
return result;
}
throw new Error(JSON.stringify(result));
}
async batchGetMaterialList(options) {
const { offset, count, type } = options;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type,
offset,
count,
}),
};
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token=${token}`, undefined, myInit);
const { errcode } = result;
if (!errcode) {
return result;
}
throw new Error(JSON.stringify(result));
}
async getMaterial(options) {
const { type, media_id } = options;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
media_id
}),
};
let imgFile;
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/material/get_material?access_token=${token}`, undefined, myInit);
if ('errcode' in result) {
throw new Error(JSON.stringify(result));
}
else {
return result;
}
}
async getTicket() {
const myInit = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
};
const token = await this.getAccessToken();
const result = (await this.access(`https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${token}&type=jsapi`,
// {
// ticket: `ticket${Date.now()}`,
// expires_in: 30,
// },
myInit));
const { ticket } = result;
return ticket;
}
randomString() {
let len = 16;
let $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
/** **默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
let maxPos = $chars.length;
let pwd = '';
for (let i = 0; i < len; i++) {
pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
}
return pwd;
}
async signatureJsSDK(options) {
const url = options.url;
const noncestr = this.randomString();
const timestamp = parseInt((Date.now() / 1000).toString(), 10);
const jsapi_ticket = await this.getTicket();
const contentArray = {
noncestr,
jsapi_ticket,
timestamp,
url,
};
let zhimaString = '';
Object.keys(contentArray)
.sort()
.forEach((ele, idx) => {
if (idx > 0) {
zhimaString += '&';
}
zhimaString += ele;
zhimaString += '=';
zhimaString += contentArray[ele];
});
return {
signature: crypto_1.default
.createHash('sha1')
.update(zhimaString)
.digest('hex'),
noncestr,
timestamp,
appId: this.appId,
};
}
}
exports.WechatPublicInstance = WechatPublicInstance;