Repository: Yoctol/bottender (ts)

Issue:
[LINE] cannot sendText or reply/push anything in _error in batch mode
In batch mode, the messages will be pushed into a queue and send in `handlerDidEnd` lifecycle. If you got an error from calling LINE API, the reply token can't use twice and  `handlerDidEnd` is over.

Pull request description:
fix(line): set shouldBatch to false after handlerDidEnd has been called
fix #525

This may be the best way to handle errors in LINE:

```js
module.exports = async function HandleError(context, props) {
  if (process.env.NODE_ENV === 'development') {
    await context.pushText('There are some unexpected errors happened. Please try again later, sorry for the inconvenience.');
    await context.pushText(props.error.stack);
  } else if (!context.isReplied) {
    await context.replyText('There are some unexpected errors happened. Please try again later, sorry for the inconvenience.'
  }
  if (process.env.NODE_ENV === 'production') {
    // send your error to the error tracker, for example: Sentry
  }
};
```

Patch:
diff --git a/package.json b/package.json
index 589fc302..5acb66b3 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,8 @@
     "test": "yarn compile && yarn lint && yarn testonly",
     "testonly": "jest --detectOpenHandles",
     "testonly:cov": "jest --coverage --runInBand --detectOpenHandles",
-    "testonly:watch": "jest --watch"
+    "testonly:watch": "jest --watch",
+    "version": "cp README.md packages/bottender"
   },
   "devDependencies": {
     "@types/body-parser": "^1.17.1",
diff --git a/packages/bottender/README.md b/packages/bottender/README.md
new file mode 100644
index 00000000..4065c738
--- /dev/null
+++ b/packages/bottender/README.md
@@ -0,0 +1,138 @@
+# Bottender
+
+[![npm](https://img.shields.io/npm/v/bottender.svg)](https://www.npmjs.com/package/bottender)
+[![CircleCI](https://circleci.com/gh/Yoctol/bottender.svg?style=shield)](https://circleci.com/gh/Yoctol/bottender)
+[![coverage](https://codecov.io/gh/Yoctol/bottender/branch/master/graph/badge.svg)](https://codecov.io/gh/Yoctol/bottender)
+[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
+[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/Yoctol/bottender#contributing)
+[![join chat](https://img.shields.io/badge/discord-join%20chat-green.svg)](https://discord.gg/apNsWBz)
+[![Follow @bottenderjs](https://img.shields.io/twitter/follow/bottenderjs.svg?label=Follow%20@bottenderjs)](https://twitter.com/intent/follow?screen_name=bottenderjs)
+
+![](https://user-images.githubusercontent.com/3382565/68366395-3080d000-016e-11ea-8bad-d9c52a215376.png)
+
+> The readme below is the documentation for the v1 (stable) version of Bottender. To view the documentation for the old Bottender version visit https://bottender.js.org/docs/0.15.17/getting-started
+
+- **Declarative** - Bottender takes care of the complexity of conversational UIs for you. Design actions for each event and state in your application, and Bottender will run accordingly. his approach makes your code more predictable and easier to debug.
+
+- **Native User Experience** - Bottender lets you create apps on every channel and never compromise on your users’ experience. You can apply progressive enhancement or graceful degradation strategy on your building blocks.
+
+- **Easy Setup** - With Bottender, you only need a few configurations to make your bot work with channels, automatic server listening, webhook setup, signature verification and so much more.
+
+- **Ready for Production** - There are thousands of bots powered by Bottender. It has been optimized for real world use cases, automatic batching request and dozens of other compelling features.
+
+Bottender is built on top of
+[Messaging APIs](https://github.com/Yoctol/messaging-apis).
+
+## Installation
+
+You can create a new Bottender app using the cli tools:
+
+```sh
+npx create-bottender-app my-app
+```
+
+Installation may fail on Windows during compilation of the native dependencies with `node-gyp`. To solve this problem, you can install [`windows-build-tools`](https://github.com/felixrieseberg/windows-build-tools#readme) or check [`node-gyp` documentation](https://github.com/nodejs/node-gyp#on-windows).
+
+## Documentation
+
+You can find the Bottender documentation [on the website](https://bottender.js.org?new).
+
+Check out the [Getting Started](https://bottender.js.org/docs/getting-started?new) page for a quick overview.
+
+## Community
+
+[![join chat](https://img.shields.io/badge/discord-join%20chat-green.svg)](https://discord.gg/unmFzmR)
+
+You can discuss anything about Bottender or chatbot development in our [Discord](https://discordapp.com/) Server. [Join now!](https://discord.gg/unmFzmR)
+
+## Examples
+
+We have a bunch of examples in the
+[examples](https://github.com/Yoctol/bottender/tree/master/examples) folder.
+Here is the first one to get you started:
+
+```js
+// index.js
+module.export = async function App(context) {
+  await context.sendText('Hello World');
+};
+```
+
+```sh
+npx bottender start --console
+```
+
+This will create and run a bot which always reply "Hello World" in the console.
+
+## Notable Features
+
+### Messenger
+
+- Messenger Profile Sync
+- Attachment Upload
+- Handover Protocol
+- Targeting Broadcast Messages
+- Built-in NLP
+- Multiple Pages
+
+### LINE
+
+- Reply, Push, Multicast
+- Imagemap
+- Rich Menu
+- Room, Group Chat
+- Beacon
+
+### Slack
+
+- Channel Chat
+- Interactive Message
+
+### Telegram
+
+- Webhook, Long Polling
+- Update, Delete Messages
+- Keyboard
+- Group Chat
+- Inline Query
+- Message Live Location
+- Payment
+
+### Viber
+
+- Subscribed, Unsubscribed Event
+- Delivered, Seen Event
+
+## Ecosystem
+
+- [bottender-compose](https://github.com/Yoctol/bottender-compose) - An utility library for Bottender and higher-order handlers.
+
+## Contributing
+
+Pull Requests and issue reports are welcome. You can follow steps below to
+submit your pull requests:
+
+Fork, then clone the repo:
+
+```sh
+git clone git@github.com:your-username/bottender.git
+```
+
+Install the dependencies:
+
+```sh
+cd bottender
+yarn
+```
+
+Make sure the tests pass (including eslint, flow checks and jest tests):
+
+```sh
+yarn test
+```
+
+Make your changes and tests, and make sure the tests pass.
+
+## License
+
+MIT © [Yoctol](https://github.com/Yoctol/bottender)
diff --git a/packages/bottender/src/line/LineContext.ts b/packages/bottender/src/line/LineContext.ts
index 6a4128e5..f88e6019 100644
--- a/packages/bottender/src/line/LineContext.ts
+++ b/packages/bottender/src/line/LineContext.ts
@@ -93,6 +93,9 @@ class LineContext extends Context<LineClient, LineEvent>
    */
   async handlerDidEnd() {
     if (this._shouldBatch) {
+      // After starting this batch, every api should be called out of batch mode
+      this._shouldBatch = false;
+
       if (this._replyMessages.length > 0) {
         const messageChunks = chunk(this._replyMessages, 5);
         warning(
diff --git a/packages/bottender/src/telegram/TelegramContext.ts b/packages/bottender/src/telegram/TelegramContext.ts
index 2dcb7c77..5138d080 100644
--- a/packages/bottender/src/telegram/TelegramContext.ts
+++ b/packages/bottender/src/telegram/TelegramContext.ts
@@ -353,6 +353,38 @@ class TelegramContext extends Context<TelegramClient, TelegramEvent>
     });
   }
 
+  async editMessageMedia(
+    messageId: number,
+    media: Type.InputMedia,
+    options?: Type.EditMessageMediaOption
+  ): Promise<Type.Message | boolean | null> {
+    if (!this._session) {
+      warning(
+        false,
+        'editMessageMedia: should not be called in context without session'
+      );
+      return null;
+    }
+
+    this._isHandled = true;
+
+    const chatId = this._getChatId();
+
+    if (chatId === null) {
+      warning(
+        false,
+        'editMessageMedia: should not be called in context without chatId'
+      );
+      return null;
+    }
+
+    return this._client.editMessageMedia(media, {
+      chatId,
+      messageId,
+      ...options,
+    });
+  }
+
   async editMessageReplyMarkup(
     messageId: number,
     replyMarkup: Type.InlineKeyboardMarkup,
@@ -671,7 +703,7 @@ class TelegramContext extends Context<TelegramClient, TelegramEvent>
     if (!this._session) {
       warning(
         false,
-        'sendVideo: should not be called in context without session'
+        'sendAnimation: should not be called in context without session'
       );
       return null;
     }
@@ -683,7 +715,7 @@ class TelegramContext extends Context<TelegramClient, TelegramEvent>
     if (chatId === null) {
       warning(
         false,
-        'sendVideo: should not be called in context without chatId'
+        'sendAnimation: should not be called in context without chatId'
       );
       return null;
     }
@@ -861,7 +893,7 @@ class TelegramContext extends Context<TelegramClient, TelegramEvent>
     if (!this._session) {
       warning(
         false,
-        'sendContact: should not be called in context without session'
+        'sendPoll: should not be called in context without session'
       );
       return null;
     }
@@ -873,7 +905,7 @@ class TelegramContext extends Context<TelegramClient, TelegramEvent>
     if (chatId === null) {
       warning(
         false,
-        'sendContact: should not be called in context without chatId'
+        'sendPoll: should not be called in context without chatId'
       );
       return null;
     }


Test patch:
diff --git a/packages/bottender/src/line/__tests__/LineContext.spec.ts b/packages/bottender/src/line/__tests__/LineContext.spec.ts
index 18bf6dbd..57969716 100644
--- a/packages/bottender/src/line/__tests__/LineContext.spec.ts
+++ b/packages/bottender/src/line/__tests__/LineContext.spec.ts
@@ -1885,6 +1885,53 @@ describe('batch', () => {
     );
     expect(warning).not.toBeCalled();
   });
+
+  it('should not batch after handlerDidEnd has been called', async () => {
+    const { client, context, session } = setup({ shouldBatch: true });
+
+    await context.replyText('1');
+    await context.pushText('2');
+    await context.pushText('3');
+
+    await context.handlerDidEnd();
+
+    await context.pushText('4');
+
+    expect(client.reply).toBeCalledWith(
+      REPLY_TOKEN,
+      [
+        {
+          type: 'text',
+          text: '1',
+        },
+      ],
+      { accessToken: undefined }
+    );
+    expect(client.push).toBeCalledWith(
+      session.user.id,
+      [
+        {
+          type: 'text',
+          text: '2',
+        },
+        {
+          type: 'text',
+          text: '3',
+        },
+      ],
+      { accessToken: undefined }
+    );
+    expect(client.push).toBeCalledWith(
+      session.user.id,
+      [
+        {
+          type: 'text',
+          text: '4',
+        },
+      ],
+      { accessToken: undefined }
+    );
+  });
 });
 
 describe('sendMethod', () => {
diff --git a/packages/bottender/src/telegram/__tests__/TelegramContext.spec.ts b/packages/bottender/src/telegram/__tests__/TelegramContext.spec.ts
index 11143934..bb7f52f7 100644
--- a/packages/bottender/src/telegram/__tests__/TelegramContext.spec.ts
+++ b/packages/bottender/src/telegram/__tests__/TelegramContext.spec.ts
@@ -563,6 +563,28 @@ describe('#editMessageCaption', () => {
   });
 });
 
+describe('#editMessageMedia', () => {
+  it('should to call client.editMessageMedia', async () => {
+    const { context, client } = setup();
+
+    const media = { type: 'photo', media: 'xxx.png' };
+    await context.editMessageMedia(66, media);
+
+    expect(client.editMessageMedia).toBeCalledWith(media, {
+      chatId: 427770117,
+      messageId: 66,
+    });
+  });
+
+  it('should mark context as handled', async () => {
+    const { context } = setup();
+
+    await context.editMessageMedia(66, { type: 'photo', media: 'xxx.png' });
+
+    expect(context.isHandled).toBe(true);
+  });
+});
+
 describe('#editMessageReplyMarkup', () => {
   const markup = {
     keyboard: [[{ text: 'new_button_1' }, { text: 'new_button_2' }]],