后台服务端 上传文件到微信素材接口 适配

This commit is contained in:
Wang Kejun 2023-09-24 14:25:02 +08:00
parent 36d554ba61
commit b54cdac7eb
5 changed files with 534 additions and 386 deletions

View File

@ -22,6 +22,7 @@ type MpServeMessageOption = {
};
};
type ServeMessageOption = TextServeMessageOption | NewsServeMessageOption | MpServeMessageOption;
type MediaType = 'image' | 'voice' | 'video' | 'thumb';
export declare class WechatPublicInstance {
appId: string;
appSecret?: string;
@ -61,9 +62,8 @@ export declare class WechatPublicInstance {
getMenu(): Promise<any>;
createMenu(menuConfig: any): Promise<any>;
createConditionalMenu(menuConfig: any): Promise<any>;
deleteConditionalMenu(menuid: number): Promise<any>;
deleteConditionalMenu(menuId: number): Promise<any>;
private refreshAccessToken;
decryptData(sessionKey: string, encryptedData: string, iv: string, signature: string): any;
getQrCode(options: {
sceneId?: number;
sceneStr?: string;
@ -95,19 +95,25 @@ export declare class WechatPublicInstance {
article_id: string;
}): Promise<any>;
createMaterial(options: {
type: 'image' | 'voice' | 'video' | 'thumb';
media: File;
type: MediaType;
media: any;
filename: string;
filetype: string;
description?: {
title: string;
introduction: string;
};
}): Promise<any>;
createImgInNewsMaterial(options: {
media: File;
media: any;
filename: string;
filetype: string;
}): Promise<any>;
createTemporaryMaterial(options: {
type: 'image' | 'voice' | 'video' | 'thumb';
media: File;
type: MediaType;
media: any;
filename: string;
filetype: string;
}): Promise<any>;
batchGetMaterialList(options: {
type: 'image' | 'video' | 'voice' | 'news';
@ -115,13 +121,14 @@ export declare class WechatPublicInstance {
count: number;
}): Promise<any>;
getMaterial(options: {
type: 'image' | 'video' | 'voice' | 'news';
media_id: string;
mediaId: string;
}): Promise<any>;
getTemporaryMaterial(options: {
media_id: string;
mediaId: string;
}): Promise<any>;
getTicket(): Promise<string>;
isJson(data: string): boolean;
decryptData(sessionKey: string, encryptedData: string, iv: string, signature: string): any;
private randomString;
signatureJsSDK(options: {
url: string;

View File

@ -2,6 +2,7 @@ require('../../fetch');
import crypto from 'crypto';
import { Buffer } from 'buffer';
import URL from 'url';
import FormData from 'form-data';
export class WechatPublicInstance {
appId;
appSecret;
@ -54,6 +55,18 @@ export class WechatPublicInstance {
contentType.includes('xml') ||
contentType.includes('html')) {
const data = await response.text();
// 某些接口返回contentType为text/plain, 里面text是json结构
const isJson = this.isJson(data);
if (isJson) {
const json = JSON.parse(data);
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;
}
return data;
}
if (contentType.includes('application/octet-stream')) {
@ -62,7 +75,17 @@ export class WechatPublicInstance {
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 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`
// {
// access_token: 'aaa',
// openid: code,
// unionid: code,
// refresh_token: 'aaa',
// is_snapshotuser: false,
// expires_in: 30,
// scope: 'userinfo',
// }
);
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,
@ -76,7 +99,14 @@ export class WechatPublicInstance {
};
}
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 result = await this.access(`https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=${this.appId}&grant_type=refresh_token&refresh_token=${refreshToken}`
// {
// access_token: 'aaa',
// refresh_token: 'aaa',
// expires_in: 30,
// scope: 'userinfo',
// }
);
const { access_token, refresh_token, expires_in, scope } = result;
return {
accessToken: access_token,
@ -86,7 +116,14 @@ export class WechatPublicInstance {
};
}
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 result = await this.access(`https://api.weixin.qq.com/sns/userinfo?access_token=${accessToken}&openid=${openId}&lang=zh_CN`
// {
// nickname: '码农哥',
// sex: 1,
// headimgurl:
// 'https://www.ertongzy.com/uploads/allimg/161005/2021233Y7-0.jpg',
// }
);
const { nickname, sex, headimgurl } = result;
return {
nickname: nickname,
@ -121,8 +158,7 @@ export class WechatPublicInstance {
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/tags/get?access_token=${token}`, undefined, myInit);
return result;
}
async editTag(tag) {
}
async editTag(tag) { }
async getCurrentMenu() {
const myInit = {
method: 'GET',
@ -177,14 +213,14 @@ export class WechatPublicInstance {
}
return Object.assign({ success: false }, result);
}
async deleteConditionalMenu(menuid) {
async deleteConditionalMenu(menuId) {
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
menuid
menuid: menuId,
}),
};
const token = await this.getAccessToken();
@ -198,7 +234,9 @@ export class WechatPublicInstance {
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}`);
: await this.access(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${this.appId}&secret=${this.appSecret}`
//{ access_token: 'mockToken', expires_in: 600 }
);
const { access_token, expires_in } = result;
this.accessToken = access_token;
// 生成下次刷新的定时器
@ -212,21 +250,6 @@ export class WechatPublicInstance {
return this.access(url2.toString(), {}, init);
}
}
decryptData(sessionKey, encryptedData, iv, signature) {
const skBuf = Buffer.from(sessionKey, 'base64');
// const edBuf = Buffer.from(encryptedData, 'base64');
const ivBuf = Buffer.from(iv, 'base64');
const decipher = crypto.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) {
@ -269,8 +292,7 @@ export class WechatPublicInstance {
};
}
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=${token}`,
// {
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=${token}`, undefined, // {
// ticket: `ticket${Date.now()}`,
// url: `http://mock/q/${sceneId ? sceneId : sceneStr}`,
// expireSeconds: expireSeconds,
@ -299,8 +321,7 @@ export class WechatPublicInstance {
}),
};
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${token}`,
// {
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${token}`, undefined, // {
// errcode: 0,
// errmsg: 'ok',
// msgid: Date.now(),
@ -372,8 +393,7 @@ export class WechatPublicInstance {
}
}
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${token}`,
// {
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${token}`, undefined, // {
// errcode: 0,
// errmsg: 'ok',
// },
@ -426,37 +446,39 @@ export class WechatPublicInstance {
}
// 创建永久素材
async createMaterial(options) {
const { type, media, description } = options;
const { type, media, description, filetype, filename } = options;
const formData = new FormData();
formData.append('media', media, {
contentType: filetype,
filename: filename, // 微信识别需要
});
if (type === 'video') {
formData.append('description', JSON.stringify(description));
}
const getLength = () => {
return new Promise((resolve, reject) => {
formData.getLength((err, length) => {
if (err) {
reject(err);
}
else {
resolve(length);
}
});
});
};
const contentLength = await getLength();
const headers = formData.getHeaders();
headers['Content-Length'] = contentLength;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
headers,
};
const formData = new FormData();
formData.append('media', media);
if (type === 'video') {
const formData2 = new FormData();
formData2.append('description', JSON.stringify(description));
Object.assign(myInit, {
body: JSON.stringify({
type,
media: formData,
description: formData2,
}),
});
}
else {
Object.assign(myInit, {
body: JSON.stringify({
type,
media: formData,
}),
});
}
;
Object.assign(myInit, {
body: formData,
});
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 result = await this.access(`https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=${token}&type=${type}`, undefined, myInit);
const { errcode } = result;
if (!errcode) {
return result;
@ -465,52 +487,71 @@ export class WechatPublicInstance {
}
//创建图文消息内的图片获取URL
async createImgInNewsMaterial(options) {
const { media } = options;
const { media, filetype, filename } = options;
const formData = new FormData();
formData.append('media', media);
formData.append('media', media, {
contentType: filetype,
filename: filename, // 微信识别需要
});
const getLength = () => {
return new Promise((resolve, reject) => {
formData.getLength((err, length) => {
if (err) {
reject(err);
}
else {
resolve(length);
}
});
});
};
const contentLength = await getLength();
const headers = formData.getHeaders();
headers['Content-Length'] = contentLength;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
headers,
};
Object.assign(myInit, {
body: JSON.stringify({
media: formData,
}),
body: formData,
});
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=${token}`, undefined, myInit);
const { errcode } = result;
if (!errcode) {
return result;
}
throw new Error(JSON.stringify(result));
return result;
}
//创建临时素材
async createTemporaryMaterial(options) {
const { type, media } = options;
const { type, media, filetype, filename } = options;
const formData = new FormData();
formData.append('media', media, {
contentType: filetype,
filename: filename, // 微信识别需要
});
const getLength = () => {
return new Promise((resolve, reject) => {
formData.getLength((err, length) => {
if (err) {
reject(err);
}
else {
resolve(length);
}
});
});
};
const contentLength = await getLength();
const headers = formData.getHeaders();
headers['Content-Length'] = contentLength;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
headers,
};
const formData = new FormData();
formData.append('media', media);
Object.assign(myInit, {
body: JSON.stringify({
type,
media: formData,
}),
body: formData,
});
const token = await this.getAccessToken();
const result = await this.access(`https https://api.weixin.qq.com/cgi-bin/media/upload?access_token=${token}`, undefined, myInit);
const { errcode } = result;
if (!errcode) {
return result;
}
throw new Error(JSON.stringify(result));
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/media/upload?access_token=${token}&type=${type}`, undefined, myInit);
return result;
}
// 获取素材列表
async batchGetMaterialList(options) {
@ -528,54 +569,39 @@ export class WechatPublicInstance {
};
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));
return result;
}
// 获取永久素材
async getMaterial(options) {
const { type, media_id } = options;
const { mediaId } = options;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
media_id
media_id: mediaId,
}),
};
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;
}
return result;
}
// 获取临时素材
async getTemporaryMaterial(options) {
const { media_id } = options;
const { mediaId } = options;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
media_id
media_id: mediaId,
}),
};
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/media/get?access_token=${token}`, undefined, myInit);
if ('errcode' in result) {
throw new Error(JSON.stringify(result));
}
else {
return result;
}
return result;
}
async getTicket() {
const myInit = {
@ -585,8 +611,7 @@ export class WechatPublicInstance {
},
};
const token = await this.getAccessToken();
const result = (await this.access(`https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${token}&type=jsapi`,
// {
const result = (await this.access(`https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${token}&type=jsapi`, undefined, // {
// ticket: `ticket${Date.now()}`,
// expires_in: 30,
// },
@ -594,6 +619,30 @@ export class WechatPublicInstance {
const { ticket } = result;
return ticket;
}
isJson(data) {
try {
JSON.parse(data);
return true;
}
catch (e) {
return false;
}
}
decryptData(sessionKey, encryptedData, iv, signature) {
const skBuf = Buffer.from(sessionKey, 'base64');
// const edBuf = Buffer.from(encryptedData, 'base64');
const ivBuf = Buffer.from(iv, 'base64');
const decipher = crypto.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;
}
randomString() {
let len = 16;
let $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';

View File

@ -22,6 +22,7 @@ type MpServeMessageOption = {
};
};
type ServeMessageOption = TextServeMessageOption | NewsServeMessageOption | MpServeMessageOption;
type MediaType = 'image' | 'voice' | 'video' | 'thumb';
export declare class WechatPublicInstance {
appId: string;
appSecret?: string;
@ -61,9 +62,8 @@ export declare class WechatPublicInstance {
getMenu(): Promise<any>;
createMenu(menuConfig: any): Promise<any>;
createConditionalMenu(menuConfig: any): Promise<any>;
deleteConditionalMenu(menuid: number): Promise<any>;
deleteConditionalMenu(menuId: number): Promise<any>;
private refreshAccessToken;
decryptData(sessionKey: string, encryptedData: string, iv: string, signature: string): any;
getQrCode(options: {
sceneId?: number;
sceneStr?: string;
@ -95,19 +95,25 @@ export declare class WechatPublicInstance {
article_id: string;
}): Promise<any>;
createMaterial(options: {
type: 'image' | 'voice' | 'video' | 'thumb';
media: File;
type: MediaType;
media: any;
filename: string;
filetype: string;
description?: {
title: string;
introduction: string;
};
}): Promise<any>;
createImgInNewsMaterial(options: {
media: File;
media: any;
filename: string;
filetype: string;
}): Promise<any>;
createTemporaryMaterial(options: {
type: 'image' | 'voice' | 'video' | 'thumb';
media: File;
type: MediaType;
media: any;
filename: string;
filetype: string;
}): Promise<any>;
batchGetMaterialList(options: {
type: 'image' | 'video' | 'voice' | 'news';
@ -115,13 +121,14 @@ export declare class WechatPublicInstance {
count: number;
}): Promise<any>;
getMaterial(options: {
type: 'image' | 'video' | 'voice' | 'news';
media_id: string;
mediaId: string;
}): Promise<any>;
getTemporaryMaterial(options: {
media_id: string;
mediaId: string;
}): Promise<any>;
getTicket(): Promise<string>;
isJson(data: string): boolean;
decryptData(sessionKey: string, encryptedData: string, iv: string, signature: string): any;
private randomString;
signatureJsSDK(options: {
url: string;

View File

@ -6,6 +6,7 @@ require('../../fetch');
const crypto_1 = tslib_1.__importDefault(require("crypto"));
const buffer_1 = require("buffer");
const url_1 = tslib_1.__importDefault(require("url"));
const form_data_1 = tslib_1.__importDefault(require("form-data"));
class WechatPublicInstance {
appId;
appSecret;
@ -58,6 +59,18 @@ class WechatPublicInstance {
contentType.includes('xml') ||
contentType.includes('html')) {
const data = await response.text();
// 某些接口返回contentType为text/plain, 里面text是json结构
const isJson = this.isJson(data);
if (isJson) {
const json = JSON.parse(data);
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;
}
return data;
}
if (contentType.includes('application/octet-stream')) {
@ -66,7 +79,17 @@ class WechatPublicInstance {
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 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`
// {
// access_token: 'aaa',
// openid: code,
// unionid: code,
// refresh_token: 'aaa',
// is_snapshotuser: false,
// expires_in: 30,
// scope: 'userinfo',
// }
);
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,
@ -80,7 +103,14 @@ class WechatPublicInstance {
};
}
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 result = await this.access(`https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=${this.appId}&grant_type=refresh_token&refresh_token=${refreshToken}`
// {
// access_token: 'aaa',
// refresh_token: 'aaa',
// expires_in: 30,
// scope: 'userinfo',
// }
);
const { access_token, refresh_token, expires_in, scope } = result;
return {
accessToken: access_token,
@ -90,7 +120,14 @@ class WechatPublicInstance {
};
}
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 result = await this.access(`https://api.weixin.qq.com/sns/userinfo?access_token=${accessToken}&openid=${openId}&lang=zh_CN`
// {
// nickname: '码农哥',
// sex: 1,
// headimgurl:
// 'https://www.ertongzy.com/uploads/allimg/161005/2021233Y7-0.jpg',
// }
);
const { nickname, sex, headimgurl } = result;
return {
nickname: nickname,
@ -125,8 +162,7 @@ class WechatPublicInstance {
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/tags/get?access_token=${token}`, undefined, myInit);
return result;
}
async editTag(tag) {
}
async editTag(tag) { }
async getCurrentMenu() {
const myInit = {
method: 'GET',
@ -181,14 +217,14 @@ class WechatPublicInstance {
}
return Object.assign({ success: false }, result);
}
async deleteConditionalMenu(menuid) {
async deleteConditionalMenu(menuId) {
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
menuid
menuid: menuId,
}),
};
const token = await this.getAccessToken();
@ -202,7 +238,9 @@ class WechatPublicInstance {
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}`);
: await this.access(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${this.appId}&secret=${this.appSecret}`
//{ access_token: 'mockToken', expires_in: 600 }
);
const { access_token, expires_in } = result;
this.accessToken = access_token;
// 生成下次刷新的定时器
@ -216,21 +254,6 @@ class WechatPublicInstance {
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) {
@ -273,8 +296,7 @@ class WechatPublicInstance {
};
}
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=${token}`,
// {
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=${token}`, undefined, // {
// ticket: `ticket${Date.now()}`,
// url: `http://mock/q/${sceneId ? sceneId : sceneStr}`,
// expireSeconds: expireSeconds,
@ -303,8 +325,7 @@ class WechatPublicInstance {
}),
};
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${token}`,
// {
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${token}`, undefined, // {
// errcode: 0,
// errmsg: 'ok',
// msgid: Date.now(),
@ -376,8 +397,7 @@ class WechatPublicInstance {
}
}
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${token}`,
// {
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${token}`, undefined, // {
// errcode: 0,
// errmsg: 'ok',
// },
@ -430,37 +450,39 @@ class WechatPublicInstance {
}
// 创建永久素材
async createMaterial(options) {
const { type, media, description } = options;
const { type, media, description, filetype, filename } = options;
const formData = new form_data_1.default();
formData.append('media', media, {
contentType: filetype,
filename: filename, // 微信识别需要
});
if (type === 'video') {
formData.append('description', JSON.stringify(description));
}
const getLength = () => {
return new Promise((resolve, reject) => {
formData.getLength((err, length) => {
if (err) {
reject(err);
}
else {
resolve(length);
}
});
});
};
const contentLength = await getLength();
const headers = formData.getHeaders();
headers['Content-Length'] = contentLength;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
headers,
};
const formData = new FormData();
formData.append('media', media);
if (type === 'video') {
const formData2 = new FormData();
formData2.append('description', JSON.stringify(description));
Object.assign(myInit, {
body: JSON.stringify({
type,
media: formData,
description: formData2,
}),
});
}
else {
Object.assign(myInit, {
body: JSON.stringify({
type,
media: formData,
}),
});
}
;
Object.assign(myInit, {
body: formData,
});
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 result = await this.access(`https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=${token}&type=${type}`, undefined, myInit);
const { errcode } = result;
if (!errcode) {
return result;
@ -469,52 +491,71 @@ class WechatPublicInstance {
}
//创建图文消息内的图片获取URL
async createImgInNewsMaterial(options) {
const { media } = options;
const formData = new FormData();
formData.append('media', media);
const { media, filetype, filename } = options;
const formData = new form_data_1.default();
formData.append('media', media, {
contentType: filetype,
filename: filename, // 微信识别需要
});
const getLength = () => {
return new Promise((resolve, reject) => {
formData.getLength((err, length) => {
if (err) {
reject(err);
}
else {
resolve(length);
}
});
});
};
const contentLength = await getLength();
const headers = formData.getHeaders();
headers['Content-Length'] = contentLength;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
headers,
};
Object.assign(myInit, {
body: JSON.stringify({
media: formData,
}),
body: formData,
});
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=${token}`, undefined, myInit);
const { errcode } = result;
if (!errcode) {
return result;
}
throw new Error(JSON.stringify(result));
return result;
}
//创建临时素材
async createTemporaryMaterial(options) {
const { type, media } = options;
const { type, media, filetype, filename } = options;
const formData = new form_data_1.default();
formData.append('media', media, {
contentType: filetype,
filename: filename, // 微信识别需要
});
const getLength = () => {
return new Promise((resolve, reject) => {
formData.getLength((err, length) => {
if (err) {
reject(err);
}
else {
resolve(length);
}
});
});
};
const contentLength = await getLength();
const headers = formData.getHeaders();
headers['Content-Length'] = contentLength;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
headers,
};
const formData = new FormData();
formData.append('media', media);
Object.assign(myInit, {
body: JSON.stringify({
type,
media: formData,
}),
body: formData,
});
const token = await this.getAccessToken();
const result = await this.access(`https https://api.weixin.qq.com/cgi-bin/media/upload?access_token=${token}`, undefined, myInit);
const { errcode } = result;
if (!errcode) {
return result;
}
throw new Error(JSON.stringify(result));
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/media/upload?access_token=${token}&type=${type}`, undefined, myInit);
return result;
}
// 获取素材列表
async batchGetMaterialList(options) {
@ -532,54 +573,39 @@ class WechatPublicInstance {
};
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));
return result;
}
// 获取永久素材
async getMaterial(options) {
const { type, media_id } = options;
const { mediaId } = options;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
media_id
media_id: mediaId,
}),
};
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;
}
return result;
}
// 获取临时素材
async getTemporaryMaterial(options) {
const { media_id } = options;
const { mediaId } = options;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
media_id
media_id: mediaId,
}),
};
const token = await this.getAccessToken();
const result = await this.access(`https://api.weixin.qq.com/cgi-bin/media/get?access_token=${token}`, undefined, myInit);
if ('errcode' in result) {
throw new Error(JSON.stringify(result));
}
else {
return result;
}
return result;
}
async getTicket() {
const myInit = {
@ -589,8 +615,7 @@ class WechatPublicInstance {
},
};
const token = await this.getAccessToken();
const result = (await this.access(`https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${token}&type=jsapi`,
// {
const result = (await this.access(`https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${token}&type=jsapi`, undefined, // {
// ticket: `ticket${Date.now()}`,
// expires_in: 30,
// },
@ -598,6 +623,30 @@ class WechatPublicInstance {
const { ticket } = result;
return ticket;
}
isJson(data) {
try {
JSON.parse(data);
return true;
}
catch (e) {
return false;
}
}
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;
}
randomString() {
let len = 16;
let $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';

View File

@ -2,8 +2,11 @@ require('../../fetch');
import crypto from 'crypto';
import { Buffer } from 'buffer';
import URL from 'url';
import FormData from 'form-data'
// 目前先支持text和news, 其他type文档https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Service_Center_messages.html
// type ServeMessageType = 'text' | 'news' | 'mpnews' | 'mpnewsarticle' | 'image' | 'voice' | 'video' | 'music' | 'msgmenu';/
type TextServeMessageOption = {
openId: string;
type: 'text';
@ -31,6 +34,8 @@ type MpServeMessageOption = {
type ServeMessageOption = TextServeMessageOption | NewsServeMessageOption | MpServeMessageOption;
type MediaType = 'image' | 'voice' | 'video' | 'thumb';
export class WechatPublicInstance {
appId: string;
appSecret?: string;
@ -104,6 +109,21 @@ export class WechatPublicInstance {
contentType.includes('html')
) {
const data = await response.text();
// 某些接口返回contentType为text/plain, 里面text是json结构
const isJson = this.isJson(data);
if (isJson) {
const json = JSON.parse(data);
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;
}
return data;
}
if (contentType.includes('application/octet-stream')) {
@ -115,7 +135,7 @@ export class WechatPublicInstance {
async code2Session(code: string) {
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`,
`https://api.weixin.qq.com/sns/oauth2/access_token?appid=${this.appId}&secret=${this.appSecret}&code=${code}&grant_type=authorization_code`
// {
// access_token: 'aaa',
// openid: code,
@ -150,7 +170,7 @@ export class WechatPublicInstance {
async refreshUserAccessToken(refreshToken: string) {
const result = await this.access(
`https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=${this.appId}&grant_type=refresh_token&refresh_token=${refreshToken}`,
`https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=${this.appId}&grant_type=refresh_token&refresh_token=${refreshToken}`
// {
// access_token: 'aaa',
// refresh_token: 'aaa',
@ -169,7 +189,7 @@ export class WechatPublicInstance {
async getUserInfo(accessToken: string, openId: string) {
const result = await this.access(
`https://api.weixin.qq.com/sns/userinfo?access_token=${accessToken}&openid=${openId}&lang=zh_CN`,
`https://api.weixin.qq.com/sns/userinfo?access_token=${accessToken}&openid=${openId}&lang=zh_CN`
// {
// nickname: '码农哥',
// sex: 1,
@ -185,13 +205,13 @@ export class WechatPublicInstance {
};
}
async createTag(tag: {name: string}) {
async createTag(tag: { name: string }) {
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({tag}),
body: JSON.stringify({ tag }),
};
const token = await this.getAccessToken();
const result = await this.access(
@ -218,13 +238,11 @@ export class WechatPublicInstance {
`https://api.weixin.qq.com/cgi-bin/tags/get?access_token=${token}`,
undefined,
myInit
)
);
return result;
}
async editTag(tag: {}) {
}
async editTag(tag: {}) {}
async getCurrentMenu() {
const myInit = {
@ -238,7 +256,7 @@ export class WechatPublicInstance {
`https://api.weixin.qq.com/cgi-bin/get_current_selfmenu_info?access_token=${token}`,
undefined,
myInit
)
);
return result;
}
@ -254,7 +272,7 @@ export class WechatPublicInstance {
`https://api.weixin.qq.com/cgi-bin/menu/get?access_token=${token}`,
undefined,
myInit
)
);
return result;
}
@ -300,14 +318,14 @@ export class WechatPublicInstance {
return Object.assign({ success: false }, result);
}
async deleteConditionalMenu(menuid: number) {
async deleteConditionalMenu(menuId: number) {
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
menuid
menuid: menuId,
}),
};
const token = await this.getAccessToken();
@ -323,15 +341,13 @@ export class WechatPublicInstance {
return Object.assign({ success: false }, result);
}
private async refreshAccessToken(url?: string, init?: RequestInit) {
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}`,
//{ access_token: 'mockToken', expires_in: 600 }
);
`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${this.appId}&secret=${this.appSecret}`
//{ access_token: 'mockToken', expires_in: 600 }
);
const { access_token, expires_in } = result;
this.accessToken = access_token;
// 生成下次刷新的定时器
@ -341,37 +357,12 @@ export class WechatPublicInstance {
}, (expires_in - 10) * 1000);
if (url) {
const url2 = new URL.URL(url);
url2.searchParams.set('access_token', access_token)
url2.searchParams.set('access_token', access_token);
return this.access(url2.toString(), {}, init);
}
}
decryptData(
sessionKey: string,
encryptedData: string,
iv: string,
signature: string
) {
const skBuf = Buffer.from(sessionKey, 'base64');
// const edBuf = Buffer.from(encryptedData, 'base64');
const ivBuf = Buffer.from(iv, 'base64');
const decipher = crypto.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: {
sceneId?: number;
sceneStr?: string;
@ -384,11 +375,11 @@ export class WechatPublicInstance {
}
const scene = sceneId
? {
scene_id: sceneId,
}
scene_id: sceneId,
}
: {
scene_str: sceneStr,
};
scene_str: sceneStr,
};
let actionName = sceneId ? 'QR_SCENE' : 'QR_STR_SCENE';
let myInit = {
method: 'POST',
@ -421,7 +412,7 @@ export class WechatPublicInstance {
const token = await this.getAccessToken();
const result = await this.access(
`https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=${token}`,
// {
undefined, // {
// ticket: `ticket${Date.now()}`,
// url: `http://mock/q/${sceneId ? sceneId : sceneStr}`,
// expireSeconds: expireSeconds,
@ -465,7 +456,7 @@ export class WechatPublicInstance {
const token = await this.getAccessToken();
const result = await this.access(
`https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${token}`,
// {
undefined, // {
// errcode: 0,
// errmsg: 'ok',
// msgid: Date.now(),
@ -540,7 +531,7 @@ export class WechatPublicInstance {
const token = await this.getAccessToken();
const result = await this.access(
`https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${token}`,
// {
undefined, // {
// errcode: 0,
// errmsg: 'ok',
// },
@ -582,9 +573,7 @@ export class WechatPublicInstance {
throw new Error(JSON.stringify(result));
}
async getArticle(options: {
article_id: string,
}) {
async getArticle(options: { article_id: string }) {
const { article_id } = options;
const myInit = {
method: 'POST',
@ -610,42 +599,51 @@ export class WechatPublicInstance {
// 创建永久素材
async createMaterial(options: {
type: 'image' | 'voice' | 'video' | 'thumb',
media: File,
description?: {title: string, introduction: string}
type: MediaType;
media: any;
filename: string;
filetype: string;
description?: { title: string; introduction: string };
}) {
const { type, media, description } = options;
const { type, media, description, filetype, filename } = options;
const formData = new FormData();
formData.append('media', media, {
contentType: filetype, // 微信识别需要
filename: filename, // 微信识别需要
});
if (type === 'video') {
formData.append('description', JSON.stringify(description));
}
const getLength = () => {
return new Promise((resolve, reject) => {
formData.getLength((err, length) => {
if (err) {
reject(err);
} else {
resolve(length);
}
});
});
};
const contentLength = await getLength();
const headers = formData.getHeaders();
headers['Content-Length'] = contentLength;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
};
const formData = new FormData();
formData.append('media', media);
if (type === 'video') {
const formData2 = new FormData();
formData2.append('description', JSON.stringify(description))
Object.assign(myInit, {
body: JSON.stringify({
type,
media: formData,
description: formData2,
}),
});
} else {
Object.assign(myInit, {
body: JSON.stringify({
type,
media: formData,
}),
});
headers,
};
Object.assign(myInit, {
body: formData,
});
const token = await this.getAccessToken();
const result = await this.access(
`https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=${token}`,
`https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=${token}&type=${type}`,
undefined,
myInit,
myInit
);
const { errcode } = result;
if (!errcode) {
@ -656,72 +654,94 @@ export class WechatPublicInstance {
//创建图文消息内的图片获取URL
async createImgInNewsMaterial(options: {
media: File,
media: any;
filename: string;
filetype: string;
}) {
const { media } = options;
const { media, filetype, filename } = options;
const formData = new FormData();
formData.append('media', media);
formData.append('media', media, {
contentType: filetype, // 微信识别需要
filename: filename, // 微信识别需要
});
const getLength = () => {
return new Promise((resolve, reject) => {
formData.getLength((err, length) => {
if (err) {
reject(err);
} else {
resolve(length);
}
});
});
};
const contentLength = await getLength();
const headers = formData.getHeaders();
headers['Content-Length'] = contentLength;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
headers,
};
Object.assign(myInit, {
body: JSON.stringify({
media: formData,
}),
body: formData,
});
const token = await this.getAccessToken();
const result = await this.access(
`https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=${token}`,
undefined,
myInit,
myInit
);
const { errcode } = result;
if (!errcode) {
return result;
}
throw new Error(JSON.stringify(result));
return result;
}
//创建临时素材
async createTemporaryMaterial(options: {
type: 'image' | 'voice' | 'video' | 'thumb',
media: File,
type: MediaType;
media: any;
filename: string;
filetype: string;
}) {
const { type, media } = options;
const { type, media, filetype, filename } = options;
const formData = new FormData();
formData.append('media', media, {
contentType: filetype, // 微信识别需要
filename: filename, // 微信识别需要
});
const getLength = () => {
return new Promise((resolve, reject) => {
formData.getLength((err, length) => {
if (err) {
reject(err);
} else {
resolve(length);
}
});
});
};
const contentLength = await getLength();
const headers = formData.getHeaders();
headers['Content-Length'] = contentLength;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
headers,
};
const formData = new FormData();
formData.append('media', media);
Object.assign(myInit, {
body: JSON.stringify({
type,
media: formData,
}),
body: formData,
});
const token = await this.getAccessToken();
const result = await this.access(
`https https://api.weixin.qq.com/cgi-bin/media/upload?access_token=${token}`,
`https://api.weixin.qq.com/cgi-bin/media/upload?access_token=${token}&type=${type}`,
undefined,
myInit,
myInit
);
const { errcode } = result;
if (!errcode) {
return result;
}
throw new Error(JSON.stringify(result));
return result;
}
// 获取素材列表
async batchGetMaterialList(options: {
type: 'image' | 'video' | 'voice' | 'news',
type: 'image' | 'video' | 'voice' | 'news';
offset?: number;
count: number;
}) {
@ -743,29 +763,21 @@ export class WechatPublicInstance {
undefined,
myInit
);
const { errcode } = result;
if (!errcode) {
return result;
}
throw new Error(JSON.stringify(result));
return result;
}
// 获取永久素材
async getMaterial(options: {
type: 'image' | 'video' | 'voice' | 'news',
media_id: string,
}) {
const { type, media_id } = options;
async getMaterial(options: { mediaId: string }) {
const { mediaId } = options;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
media_id
media_id: mediaId,
}),
};
let imgFile;
const token = await this.getAccessToken();
const result = await this.access(
@ -773,25 +785,19 @@ export class WechatPublicInstance {
undefined,
myInit
);
if ('errcode' in result) {
throw new Error(JSON.stringify(result));
} else {
return result;
}
return result;
}
// 获取临时素材
async getTemporaryMaterial(options: {
media_id: string,
}) {
const { media_id } = options;
async getTemporaryMaterial(options: { mediaId: string }) {
const { mediaId } = options;
const myInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
media_id
media_id: mediaId,
}),
};
const token = await this.getAccessToken();
@ -800,11 +806,7 @@ export class WechatPublicInstance {
undefined,
myInit
);
if ('errcode' in result) {
throw new Error(JSON.stringify(result));
} else {
return result;
}
return result;
}
async getTicket() {
@ -817,7 +819,7 @@ export class WechatPublicInstance {
const token = await this.getAccessToken();
const result = (await this.access(
`https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${token}&type=jsapi`,
// {
undefined, // {
// ticket: `ticket${Date.now()}`,
// expires_in: 30,
// },
@ -832,6 +834,40 @@ export class WechatPublicInstance {
return ticket;
}
isJson(data: string) {
try {
JSON.parse(data);
return true;
} catch (e) {
return false;
}
}
decryptData(
sessionKey: string,
encryptedData: string,
iv: string,
signature: string
) {
const skBuf = Buffer.from(sessionKey, 'base64');
// const edBuf = Buffer.from(encryptedData, 'base64');
const ivBuf = Buffer.from(iv, 'base64');
const decipher = crypto.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;
}
private randomString() {
let len = 16;
let $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';