diff --git a/examples/wechat-hello-world/README.md b/examples/wechat-hello-world/README.md new file mode 100644 index 000000000..feb46bb87 --- /dev/null +++ b/examples/wechat-hello-world/README.md @@ -0,0 +1,52 @@ +# Wecaht hello world + +## Install and Run + +Download this example or clone [bottender](https://github.com/Yoctol/bottender). + +``` +curl https://codeload.github.com/Yoctol/bottender/tar.gz/master | tar -xz --strip=2 bottender-master/examples/wechat-hello-world +cd wechat-hello-world +``` + +Install dependencies: + +``` +npm install +``` + +You must put `accessToken`, `appSecret` and `verifyToken` into `bottender.config.js`. + +After that, you can run the bot with this npm script: + +``` +npm run dev +``` + +This command will start server for bot developing at `http://localhost:5000`. + +## Set webhook + +While the server running, you can run following command with global `bottender` to set up the webhook: + +``` +bottender messenger webhook set -w +``` + +If you want to expose the server on your local development machine and get a secure URL, [ngrok](https://ngrok.com/) or [localtunnel](https://localtunnel.github.io/www/) may be good tools for you. + +> Note: You must put `appId` and `appSecret` into `bottender.config.js` before running this command. + +## Idea of this example + +This example is a simple bot running on [Messenger](https://www.messenger.com/). +For more information, check our [Messenger guides](https://bottender.js.org/docs/Platforms-Messenger). + +## Related examples + +- [messenger-hello-world](../messenger-hello-world) +- [console-hello-world](../console-hello-world) +- [line-hello-world](../line-hello-world) +- [slack-hello-world](../slack-hello-world) +- [telegram-hello-world](../telegram-hello-world) +- [viber-hello-world](../viber-hello-world) diff --git a/examples/wechat-hello-world/bottender.config.js b/examples/wechat-hello-world/bottender.config.js new file mode 100644 index 000000000..d8439696a --- /dev/null +++ b/examples/wechat-hello-world/bottender.config.js @@ -0,0 +1,7 @@ +module.exports = { + wechat: { + appId: '__PUT_YOUR_APP_ID_HERE__', + appSecret: '__PUT_YOUR_APP_SECRET_HERE__', + verifyToken: '__PUT_YOUR_VERITY_TOKEN_HERE__', + }, +}; diff --git a/examples/wechat-hello-world/index.js b/examples/wechat-hello-world/index.js new file mode 100644 index 000000000..50192201e --- /dev/null +++ b/examples/wechat-hello-world/index.js @@ -0,0 +1,20 @@ +const { WechatBot } = require('../../lib'); +const { createServer } = require('../../lib/express'); + +const config = require('./bottender.config').wechat; + +const bot = new WechatBot({ + accessToken: config.accessToken, + appSecret: config.appSecret, + verifyToken: config.verifyToken, +}); + +bot.onEvent(async context => { + await context.replyText('Hello World'); +}); + +const server = createServer(bot); + +server.listen(5000, () => { + console.log('server is running on 5000 port...'); +}); diff --git a/examples/wechat-hello-world/package.json b/examples/wechat-hello-world/package.json new file mode 100644 index 000000000..7c0b0e156 --- /dev/null +++ b/examples/wechat-hello-world/package.json @@ -0,0 +1,12 @@ +{ + "dependencies": { + "bottender": "latest" + }, + "devDependencies": { + "nodemon": "^1.11.0" + }, + "scripts": { + "dev": "nodemon index.js", + "start": "node index.js" + } +} diff --git a/package.json b/package.json index 7c8fc0ba1..cbb28e73d 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "deep-object-diff": "^1.1.0", "delay": "^4.1.0", "express": "^4.16.4", + "express-xml-bodyparser": "^0.3.0", "figures": "^2.0.0", "file-type": "^10.4.0", "fs-extra": "^7.0.1", @@ -75,6 +76,7 @@ "messaging-api-slack": "^0.7.11", "messaging-api-telegram": "^0.7.11", "messaging-api-viber": "^0.7.11", + "messaging-api-wechat": "^0.7.11", "messenger-batch": "^0.3.0", "micro": "^9.3.3", "minimist": "^1.2.0", diff --git a/src/bot/WechatBot.js b/src/bot/WechatBot.js new file mode 100644 index 000000000..67ec3837f --- /dev/null +++ b/src/bot/WechatBot.js @@ -0,0 +1,21 @@ +/* @flow */ + +import { type SessionStore } from '../session/SessionStore'; + +import Bot from './Bot'; +import WechatConnector from './WechatConnector'; + +export default class WechatBot extends Bot { + constructor({ + sessionStore, + verifyToken, + origin, + }: { + sessionStore: SessionStore, + verifyToken?: string, + origin?: string, + }) { + const connector = new WechatConnector({ verifyToken, origin }); + super({ connector, sessionStore, sync: true }); + } +} diff --git a/src/bot/WechatConnector.js b/src/bot/WechatConnector.js new file mode 100644 index 000000000..820fc4900 --- /dev/null +++ b/src/bot/WechatConnector.js @@ -0,0 +1,92 @@ +/* @flow */ +import { WechatClient } from 'messaging-api-wechat'; + +import WechatContext from '../context/WechatContext'; +import WechatEvent, { type WechatRawEvent } from '../context/WechatEvent'; +import { type Session } from '../session/Session'; + +import { type Connector } from './Connector'; + +export type WechatRequestBody = WechatRawEvent; + +type ConstructorOptions = {| + client?: WechatClient, + origin?: string, +|}; + +export default class WechatConnector implements Connector { + _client: WechatClient; + + _verifyToken: ?string; + + constructor({ client, verifyToken, origin }: ConstructorOptions) { + this._client = + client || + WechatClient.connect({ + origin, + }); + + this._verifyToken = verifyToken; + } + + _getRawEventFromRequest(body: WechatRequestBody): WechatRawEvent { + return body.xml; + } + + get platform(): string { + return 'wechat'; + } + + get client(): WechatClient { + return this._client; + } + + get verifyToken(): ?string { + return this._verifyToken; + } + + getUniqueSessionKey(body: WechatRequestBody): string { + return body.openid; + } + + async updateSession( + session: Session, + body: WechatRequestBody + ): Promise { + console.log('updateSession', { body }); + if (!session.user) { + session.user = { + id: body.openid, + _updatedAt: new Date().toISOString(), + }; + } + + Object.freeze(session.user); + Object.defineProperty(session, 'user', { + configurable: false, + enumerable: true, + writable: false, + value: session.user, + }); + } + + mapRequestToEvents(body: WechatRequestBody): Array { + const rawEvent = this._getRawEventFromRequest(body); + + console.log('mapRequestToEvents', { rawEvent }); + + return [new WechatEvent(rawEvent)]; + } + + createContext(params: { + event: WechatEvent, + session: ?Session, + initialState: ?Object, + requestContext: ?Object, + }): WechatContext { + return new WechatContext({ + ...params, + client: this._client, + }); + } +} diff --git a/src/bot/__tests__/WechatBot.spec.js b/src/bot/__tests__/WechatBot.spec.js new file mode 100644 index 000000000..4ded0a5ab --- /dev/null +++ b/src/bot/__tests__/WechatBot.spec.js @@ -0,0 +1,11 @@ +import WechatBot from '../WechatBot'; +import WechatConnector from '../WechatConnector'; + +it('should construct bot with WechatConnector', () => { + const bot = new WechatBot(); + expect(bot).toBeDefined(); + expect(bot.onEvent).toBeDefined(); + expect(bot.createRequestHandler).toBeDefined(); + expect(bot.connector).toBeDefined(); + expect(bot.connector).toBeInstanceOf(WechatConnector); +}); diff --git a/src/bot/__tests__/WechatConnector.spec.js b/src/bot/__tests__/WechatConnector.spec.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/context/WechatContext.js b/src/context/WechatContext.js new file mode 100644 index 000000000..1378cc750 --- /dev/null +++ b/src/context/WechatContext.js @@ -0,0 +1,204 @@ +/* @flow */ + +import invariant from 'invariant'; +import sleep from 'delay'; +import warning from 'warning'; +import { WechatClient } from 'messaging-api-wechat'; + +import { type Session } from '../session/Session'; + +import Context from './Context'; +import WechatEvent from './WechatEvent'; +import { type PlatformContext } from './PlatformContext'; + +type Options = {| + client: WechatClient, + event: WechatEvent, + session: ?Session, + initialState: ?Object, + requestContext: ?Object, +|}; + +class WechatContext extends Context implements PlatformContext { + _client: WechatClient = this._client; + + _event: WechatEvent = this._event; + + _session: ?Session = this.session; + + _isReplied: boolean = false; + + constructor({ + client, + event, + session, + initialState, + requestContext, + }: Options) { + super({ client, event, session, initialState, requestContext }); + } + + /** + * The name of the platform. + * + */ + get platform(): string { + return 'wecaht'; + } + + /** + * Delay and show indicators for milliseconds. + * + */ + async typing(milliseconds: number): Promise { + if (milliseconds > 0) { + await sleep(milliseconds); + } + } + + /** + * Send text to the owner of the session. + * + */ + async sendText(text: string, options?: Object): Promise { + if (!this._session) { + warning( + false, + 'sendText: should not be called in context without session' + ); + return; + } + + this._isHandled = true; + + // FIXME + return this._client.sendText(this._session.user.id, text, options); + } + + /** + * https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140543 + */ + async replyText(text: string, options?: Object): Promise { + invariant(!this._isReplied, 'Can not reply event mulitple times'); + + if (!this._session) { + warning( + false, + 'replyText: should not be called in context without session' + ); + return; + } + + this._isReplied = true; + this._isHandled = true; + + this.response.headers['content-type'] = 'application/xml'; + this.response.body = `${Date.now()}`; + } + + async replyImage(imageId: string, options?: Object): Promise { + invariant(!this._isReplied, 'Can not reply event mulitple times'); + + if (!this._session) { + warning( + false, + 'replyImage: should not be called in context without session' + ); + return; + } + + this._isReplied = true; + this._isHandled = true; + + this.response.headers['content-type'] = 'application/xml'; + this.response.body = `${Date.now()}`; + } + + async replyVoice(voiceId: string, options?: Object): Promise { + invariant(!this._isReplied, 'Can not reply event mulitple times'); + + if (!this._session) { + warning( + false, + 'replyVoice: should not be called in context without session' + ); + return; + } + + this._isReplied = true; + this._isHandled = true; + + this.response.headers['content-type'] = 'application/xml'; + this.response.body = `${Date.now()}`; + } + + async replyVideo(videoId: string, options?: Object): Promise { + invariant(!this._isReplied, 'Can not reply event mulitple times'); + + if (!this._session) { + warning( + false, + 'replyVideo: should not be called in context without session' + ); + return; + } + + this._isReplied = true; + this._isHandled = true; + + this.response.headers['content-type'] = 'application/xml'; + // FIXME: Title, Description + this.response.body = `${Date.now()}`; + } + + async replyMusic(thumbMediaId: string, options?: Object): Promise { + invariant(!this._isReplied, 'Can not reply event mulitple times'); + + if (!this._session) { + warning( + false, + 'replyMusic: should not be called in context without session' + ); + return; + } + + this._isReplied = true; + this._isHandled = true; + + this.response.headers['content-type'] = 'application/xml'; + // FIXME: Title, Description, MusicURL, HQMusicUrl + this.response.body = `${Date.now()}`; + } + + async replyNews(thumbMediaId: string, options?: Object): Promise { + invariant(!this._isReplied, 'Can not reply event mulitple times'); + + if (!this._session) { + warning( + false, + 'replyNews: should not be called in context without session' + ); + return; + } + + this._isReplied = true; + this._isHandled = true; + + this.response.headers['content-type'] = 'application/xml'; + + // TODO: 回复图文消息 + this.response.body = ''; + } +} + +export default WechatContext; diff --git a/src/context/WechatEvent.js b/src/context/WechatEvent.js new file mode 100644 index 000000000..4989d37a5 --- /dev/null +++ b/src/context/WechatEvent.js @@ -0,0 +1,176 @@ +/* @flow */ + +import { type Event } from './Event'; + +type Message = { + text: string, +}; + +export type WechatRawEvent = { + message?: Message, + payload?: string, +}; + +/** + * https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140453 + */ +export default class WechatEvent implements Event { + _rawEvent: WechatRawEvent; + + constructor(rawEvent: WechatRawEvent) { + this._rawEvent = rawEvent; + } + + /** + * Underlying raw event from Wechat. + * + */ + get rawEvent(): WechatRawEvent { + return this._rawEvent; + } + + /** + * Determine if the event is a message event. + * + */ + get isMessage(): boolean { + return !!this._rawEvent.msgtype; + } + + /** + * The message object from Wechat raw event. + * + */ + get message(): ?Message { + if (this.isMessage) { + return this._rawEvent; + } + return null; + } + + /** + * Determine if the event is a message event which includes text. + * + */ + get isText(): boolean { + if (this.isMessage && this._rawEvent.msgtype[0] === 'text') { + return true; + } + return false; + } + + /** + * The text string from Wechat raw event. + * + */ + get text(): ?string { + if (this.isText) { + return ((this.message: any): Message).content[0]; + } + return null; + } + + /** + * Determine if the event is a payload event. + * + */ + get isPayload(): boolean { + return false; + } + + /** + * The payload string from Wechat raw event. + * + */ + get payload(): ?string { + return null; + } + + /** + * Determine if the event is a message event which includes image. + * + */ + get isImage(): boolean { + if (this.isMessage && this._rawEvent.msgtype[0] === 'image') { + return true; + } + return false; + } + + /** + * Determine if the event is a message event which includes voice. + * + */ + get isVoice(): boolean { + if (this.isMessage && this._rawEvent.msgtype[0] === 'voice') { + return true; + } + return false; + } + + /** + * Determine if the event is a message event which includes video. + * + */ + get isVideo(): boolean { + if (this.isMessage && this._rawEvent.msgtype[0] === 'video') { + return true; + } + return false; + } + + /** + * Determine if the event is a message event which includes short video. + * + */ + get isShortVideo(): boolean { + if (this.isMessage && this._rawEvent.msgtype[0] === 'shortvideo') { + return true; + } + return false; + } + + /** + * Determine if the event is a message event which includes location. + * + */ + get isLocation(): boolean { + if (this.isMessage && this._rawEvent.msgtype[0] === 'location') { + return true; + } + return false; + } + + /** + * Determine if the event is a message event which includes link. + * + */ + get isLink(): boolean { + if (this.isMessage && this._rawEvent.msgtype[0] === 'link') { + return true; + } + return false; + } + + /** + * Determine if the event is a subscribe event. + * + */ + get isSubscribe(): boolean { + if (this.isMessage && this._rawEvent.msgtype[0] === 'subscribe') { + return true; + } + return false; + } + + /** + * Determine if the event is a scan event. + * + */ + get isScan(): boolean { + if (this.isMessage && this._rawEvent.msgtype[0] === 'scan') { + return true; + } + return false; + } +} diff --git a/src/context/__tests__/WechatContext.spec.js b/src/context/__tests__/WechatContext.spec.js new file mode 100644 index 000000000..ffebaa0e0 --- /dev/null +++ b/src/context/__tests__/WechatContext.spec.js @@ -0,0 +1 @@ +import WechatContext from '../WechatContext'; diff --git a/src/context/__tests__/WechatEvent.spec.js b/src/context/__tests__/WechatEvent.spec.js new file mode 100644 index 000000000..6d26fcd8e --- /dev/null +++ b/src/context/__tests__/WechatEvent.spec.js @@ -0,0 +1,78 @@ +import WechatEvent from '../WechatEvent'; + +const textMessage = { + tousername: ['gh_29d2d7084492'], + fromusername: ['oLt9j58X9wLaU0caBfY38Tu_AXR8'], + createtime: ['1547399295'], + msgtype: ['text'], + content: ['你好'], + msgid: ['6646029366319039068'], +}; + +const imageMessage = { + tousername: ['gh_29d2d7084492'], + fromusername: ['oLt9j58X9wLaU0caBfY38Tu_AXR8'], + createtime: ['1547436685'], + msgtype: ['image'], + picurl: [ + 'http://mmbiz.qpic.cn/mmbiz_jpg/uxcEX01JbicLTFwJ2WNNGrKpZQYWUkMeGdLNn3ib0s4Zbd6w7xbdSLClYVocYYb4IQDLgnT3fvv6ZCJFpoY30Xug/0', + ], + msgid: ['6646189955146236510'], + mediaid: ['WpiMoqMPY_n4YmDEQWXkZLcnkcqVk3FfzctIMY5CQUnqp9_rDkWeYs7mmaioSLL5'], +}; + +const locationMessage = { + tousername: ['gh_29d2d7084492'], + fromusername: ['oLt9j58X9wLaU0caBfY38Tu_AXR8'], + createtime: ['1547437159'], + msgtype: ['location'], + location_x: ['25.034454'], + location_y: ['121.526857'], + scale: ['15'], + label: [''], + msgid: ['6646191990960734815'], +}; + +it('#rawEvent', () => { + expect(new WechatEvent(textMessage).rawEvent).toEqual(textMessage); +}); + +it('#isMessage', () => { + expect(new WechatEvent(textMessage).isMessage).toEqual(true); + expect(new WechatEvent(imageMessage).isMessage).toEqual(true); +}); + +it('#isText', () => { + expect(new WechatEvent(textMessage).isText).toEqual(true); + expect(new WechatEvent(imageMessage).isText).toEqual(false); +}); + +it('#isImage', () => { + expect(new WechatEvent(textMessage).isImage).toEqual(false); + expect(new WechatEvent(imageMessage).isImage).toEqual(true); +}); + +it('#isVoice', () => { + expect(new WechatEvent(textMessage).isVoice).toEqual(false); + expect(new WechatEvent(imageMessage).isVoice).toEqual(false); +}); + +it('#isVideo', () => { + expect(new WechatEvent(textMessage).isVideo).toEqual(false); + expect(new WechatEvent(imageMessage).isVideo).toEqual(false); +}); + +it('#isShortVideo', () => { + expect(new WechatEvent(textMessage).isShortVideo).toEqual(false); + expect(new WechatEvent(imageMessage).isShortVideo).toEqual(false); +}); + +it('#isLocation', () => { + expect(new WechatEvent(textMessage).isLocation).toEqual(false); + expect(new WechatEvent(imageMessage).isLocation).toEqual(false); +}); + +it('#isLink', () => { + expect(new WechatEvent(textMessage).isLink).toEqual(false); + expect(new WechatEvent(imageMessage).isLink).toEqual(false); +}); diff --git a/src/express/createMiddleware.js b/src/express/createMiddleware.js index 635eda67a..43419612f 100644 --- a/src/express/createMiddleware.js +++ b/src/express/createMiddleware.js @@ -22,6 +22,7 @@ function createMiddleware(bot) { res, } ); + console.log(response); if (response) { res.set(response.headers || {}); res.status(response.status || 200); diff --git a/src/express/createServer.js b/src/express/createServer.js index 00af3f22f..af40722c7 100644 --- a/src/express/createServer.js +++ b/src/express/createServer.js @@ -1,5 +1,6 @@ import bodyParser from 'body-parser'; import express from 'express'; +import xmlBodyParser from 'express-xml-bodyparser'; import registerRoutes from './registerRoutes'; @@ -14,6 +15,7 @@ function createServer(bot, config = {}) { }, }) ); + server.use(xmlBodyParser()); registerRoutes(server, bot, config); diff --git a/src/express/registerRoutes.js b/src/express/registerRoutes.js index 1f0f6dd21..ad23c3098 100644 --- a/src/express/registerRoutes.js +++ b/src/express/registerRoutes.js @@ -9,6 +9,8 @@ import verifyMessengerWebhook from './verifyMessengerWebhook'; import verifySlackSignature from './verifySlackSignature'; import verifySlackWebhook from './verifySlackWebhook'; import verifyViberSignature from './verifyViberSignature'; +import verifyWechatSignature from './verifyWechatSignature'; +import verifyWechatWebhook from './verifyWechatWebhook'; function registerRoutes(server, bot, config = {}) { const path = config.path || '/'; @@ -28,6 +30,11 @@ function registerRoutes(server, bot, config = {}) { middleware.unshift(verifyLineSignature(bot)); } else if (bot.connector.platform === 'viber') { middleware.unshift(verifyViberSignature(bot)); + } else if (bot.connector.platform === 'wechat') { + verifyToken = + config.verifyToken || bot.connector.verifyToken || shortid.generate(); + server.get(path, verifyWechatWebhook({ verifyToken })); + middleware.unshift(verifyWechatSignature(bot)); } server.post(path, ...middleware, createMiddleware(bot)); diff --git a/src/express/verifyWechatSignature.js b/src/express/verifyWechatSignature.js new file mode 100644 index 000000000..8cc787fba --- /dev/null +++ b/src/express/verifyWechatSignature.js @@ -0,0 +1,21 @@ +const verifyWechatSignature = bot => (req, res, next) => { + if ( + true // FIXME + ) { + return next(); + } + const error = { + message: 'Wechat Signature Validation Failed!', + request: { + rawBody: req.rawBody, + headers: { + 'x-hub-signature': req.headers['x-hub-signature'], + }, + }, + }; + console.error(error); + res.status(400); + res.send({ error }); +}; + +export default verifyWechatSignature; diff --git a/src/express/verifyWechatWebhook.js b/src/express/verifyWechatWebhook.js new file mode 100644 index 000000000..eb46cb1d4 --- /dev/null +++ b/src/express/verifyWechatWebhook.js @@ -0,0 +1,21 @@ +import hasha from 'hasha'; + +// 1)将 token、timestamp、nonce 三个参数进行字典序排序 +// 2)将三个参数字符串拼接成一个字符串进行 sha1 加密 +// 3)开发者获得加密后的字符串可与 signature 对比,标识该请求来源于微信 +const verifyWechatWebhook = ({ verifyToken }) => (req, res) => { + const { signature, timestamp, nonce, echostr } = req.query; + + const input = [verifyToken, timestamp, nonce].sort().join(''); + + const hash = hasha(input, { algorithm: 'sha1' }); + + if (hash === signature) { + res.send(echostr); + } else { + console.error('Failed validation. Make sure the signature match.'); + res.sendStatus(403); + } +}; + +export default verifyWechatWebhook; diff --git a/src/index.js b/src/index.js index aea336bb2..4095a81d8 100644 --- a/src/index.js +++ b/src/index.js @@ -11,6 +11,7 @@ export { default as LineBot } from './bot/LineBot'; export { default as SlackBot } from './bot/SlackBot'; export { default as TelegramBot } from './bot/TelegramBot'; export { default as ViberBot } from './bot/ViberBot'; +export { default as WechatBot } from './bot/WechatBot'; /* Connector */ export { default as ConsoleConnector } from './bot/ConsoleConnector'; @@ -20,6 +21,7 @@ export { default as LineConnector } from './bot/LineConnector'; export { default as SlackConnector } from './bot/SlackConnector'; export { default as TelegramConnector } from './bot/TelegramConnector'; export { default as ViberConnector } from './bot/ViberConnector'; +export { default as WechatConnector } from './bot/WechatConnector'; /* HandlerBuilder */ export { default as middleware } from './handlers/middleware'; @@ -62,6 +64,7 @@ export { default as LineContext } from './context/LineContext'; export { default as SlackContext } from './context/SlackContext'; export { default as TelegramContext } from './context/TelegramContext'; export { default as ViberContext } from './context/ViberContext'; +export { default as WechatContext } from './context/WechatContext'; /* Event */ export { default as ConsoleEvent } from './context/ConsoleEvent'; @@ -71,6 +74,7 @@ export { default as LineEvent } from './context/LineEvent'; export { default as SlackEvent } from './context/SlackEvent'; export { default as TelegramEvent } from './context/TelegramEvent'; export { default as ViberEvent } from './context/ViberEvent'; +export { default as WechatEvent } from './context/WechatEvent'; /* Utils */ export { utils }; diff --git a/yarn.lock b/yarn.lock index b3f3e91f8..af92de654 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3043,6 +3043,13 @@ expect@^23.6.0: jest-message-util "^23.4.0" jest-regex-util "^23.3.0" +express-xml-bodyparser@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/express-xml-bodyparser/-/express-xml-bodyparser-0.3.0.tgz#b1f5a98adf6c6e412c4ccba634234b82945c62be" + integrity sha1-sfWpit9sbkEsTMumNCNLgpRcYr4= + dependencies: + xml2js "^0.4.11" + express@^4.16.4: version "4.16.4" resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" @@ -5446,6 +5453,18 @@ messaging-api-viber@^0.7.11: lodash.omit "^4.5.0" url-join "^4.0.0" +messaging-api-wechat@^0.7.11: + version "0.7.11" + resolved "https://registry.yarnpkg.com/messaging-api-wechat/-/messaging-api-wechat-0.7.11.tgz#22145de750ed4e0bb5240fb32709f53182b4886b" + integrity sha512-O1eZgbM7fNxLH4pdRFPaqlc8QdmRROIZoh37gNCB+feb3CZ90As6UGTYoqCgHiobtevXnG7wHFJfJV0JPyrWdA== + dependencies: + axios "^0.18.0" + axios-error "^0.7.11" + debug "^4.0.1" + form-data "^2.3.2" + lodash.omit "^4.5.0" + url-join "^4.0.0" + messenger-batch@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/messenger-batch/-/messenger-batch-0.3.0.tgz#90071757c34ea93a76b331d207ef79ed0c2cf42f" @@ -7114,7 +7133,7 @@ sane@^2.0.0: optionalDependencies: fsevents "^1.2.3" -sax@^1.2.4: +sax@>=0.6.0, sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -8297,11 +8316,24 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml2js@^0.4.11: + version "0.4.19" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + xml@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU= +xmlbuilder@~9.0.1: + version "9.0.7" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= + y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"