Repository: line/line-bot-sdk-python
Pull request #159 (2019-05-16 06:45:50)

Update README as per template
- s/line-bot-sdk-python/LINE Messaging API SDK for Python
- Change documentation links from api reference to messaging api overview 
- Add "Introduction" section
- s/Install/Installation/
- Move up "Requirements" section
- Add "Help and media" section
- Add "Versioning" section
- Add "Contributing" section
- Add "License" section


Patch:
diff --git a/.travis.yml b/.travis.yml
index bda8308..e30565b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,3 +1,5 @@
+dist: xenial
+
 language: python
 
 python:
@@ -5,6 +7,7 @@ python:
   - "3.4"
   - "3.5"
   - "3.6"
+  - "3.7"
 
 install: pip install tox-travis
 
diff --git a/README.rst b/README.rst
index 9573063..2d8772c 100644
--- a/README.rst
+++ b/README.rst
@@ -1,21 +1,31 @@
-line-bot-sdk-python
-===================
+LINE Messaging API SDK for Python
+=================================
 
 |Build Status| |PyPI version| |Documentation Status|
 
 SDK of the LINE Messaging API for Python.
 
-About the LINE Messaging API
-----------------------------
+Introduction
+------------
+The LINE Messaging API SDK for Python makes it easy to develop bots using LINE Messaging API, and you can create a sample bot within minutes.
 
-See the official API documentation for more information.
 
-English: https://developers.line.me/en/docs/messaging-api/reference/
+Documentation
+-------------
 
-Japanese: https://developers.line.me/ja/docs/messaging-api/reference/
+See the official API documentation for more information
 
-Install
--------
+English: https://developers.line.biz/en/docs/messaging-api/overview/
+
+Japanese: https://developers.line.biz/ja/docs/messaging-api/overview/
+
+Requirements
+------------
+
+-  Python >= 2.7 or >= 3.4
+
+Installation
+------------
 
 ::
 
@@ -451,6 +461,17 @@ ImagemapSendMessage
         base_url='https://example.com/base',
         alt_text='this is an imagemap',
         base_size=BaseSize(height=1040, width=1040),
+        video=Video(
+            original_content_url='https://example.com/video.mp4',
+            preview_image_url='https://example.com/video_preview.jpg',
+            area=ImagemapArea(
+                x=0, y=0, width=1040, height=585
+            ),
+            external_link=ExternalLink(
+                link_uri='https://example.com/see_more.html',
+                label='See More',
+            ),
+        ),
         actions=[
             URIImagemapAction(
                 link_uri='https://example.com/',
@@ -828,10 +849,25 @@ API documentation
 
 OR |Documentation Status|
 
-Requirements
-------------
+Help and media
+--------------
+FAQ: https://developers.line.biz/en/faq/
 
--  Python >= 2.7 or >= 3.4
+Community Q&A: https://www.line-community.me/questions
+
+News: https://developers.line.biz/en/news/
+
+Twitter: @LINE_DEV 
+
+Versioning
+----------
+This project respects semantic versioning
+
+See http://semver.org/
+
+Contributing
+------------
+Please check `CONTRIBUTING <contributing.md>`__ before making a contribution.
 
 For SDK developers
 ------------------
@@ -878,3 +914,23 @@ And more... TBD
    :target: https://badge.fury.io/py/line-bot-sdk
 .. |Documentation Status| image:: https://readthedocs.org/projects/line-bot-sdk-python/badge/?version=latest
    :target: http://line-bot-sdk-python.readthedocs.io/en/latest/?badge=latest
+   
+
+License
+--------
+
+::
+
+    Copyright (C) 2016 LINE Corp.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
diff --git a/examples/flask-kitchensink/app.py b/examples/flask-kitchensink/app.py
index 08a41a8..bce1099 100644
--- a/examples/flask-kitchensink/app.py
+++ b/examples/flask-kitchensink/app.py
@@ -40,6 +40,7 @@ from linebot.models import (
     StickerMessage, StickerSendMessage, LocationMessage, LocationSendMessage,
     ImageMessage, VideoMessage, AudioMessage, FileMessage,
     UnfollowEvent, FollowEvent, JoinEvent, LeaveEvent, BeaconEvent,
+    MemberJoinedEvent, MemberLeftEvent,
     FlexSendMessage, BubbleContainer, ImageComponent, BoxComponent,
     TextComponent, SpacerComponent, IconComponent, ButtonComponent,
     SeparatorComponent, QuickReply, QuickReplyButton,
@@ -115,6 +116,50 @@ def handle_text_message(event):
             line_bot_api.reply_message(
                 event.reply_token,
                 TextSendMessage(text="Bot can't use profile API without user ID"))
+    elif text == 'quota':
+        quota = line_bot_api.get_message_quota()
+        line_bot_api.reply_message(
+            event.reply_token, [
+                TextSendMessage(text='type: ' + quota.type),
+                TextSendMessage(text='value: ' + str(quota.value))
+            ]
+        )
+    elif text == 'quota_consumption':
+        quota_consumption = line_bot_api.get_message_quota_consumption()
+        line_bot_api.reply_message(
+            event.reply_token, [
+                TextSendMessage(text='total usage: ' + str(quota_consumption.total_usage)),
+            ]
+        )
+    elif text == 'push':
+        line_bot_api.push_message(
+            event.source.user_id, [
+                TextSendMessage(text='PUSH!'),
+            ]
+        )
+    elif text == 'multicast':
+        line_bot_api.multicast(
+            [event.source.user_id], [
+                TextSendMessage(text='THIS IS A MULTICAST MESSAGE'),
+            ]
+        )
+    elif text == 'broadcast':
+        line_bot_api.broadcast(
+            [
+                TextSendMessage(text='THIS IS A BROADCAST MESSAGE'),
+            ]
+        )
+    elif text.startswith('broadcast '):  # broadcast 20190505
+        date = text.split(' ')[1]
+        print("Getting broadcast result: " + date)
+        result = line_bot_api.get_message_delivery_broadcast(date)
+        line_bot_api.reply_message(
+            event.reply_token, [
+                TextSendMessage(text='Number of sent broadcast messages: ' + date),
+                TextSendMessage(text='status: ' + str(result.status)),
+                TextSendMessage(text='success: ' + str(result.success)),
+            ]
+        )
     elif text == 'bye':
         if isinstance(event.source, SourceGroup):
             line_bot_api.reply_message(
@@ -436,6 +481,20 @@ def handle_beacon(event):
                 event.beacon.hwid, event.beacon.dm)))
 
 
+@handler.add(MemberJoinedEvent)
+def handle_member_joined(event):
+    line_bot_api.reply_message(
+        event.reply_token,
+        TextSendMessage(
+            text='Got memberJoined event. event={}'.format(
+                event)))
+
+
+@handler.add(MemberLeftEvent)
+def handle_member_left(event):
+    app.logger.info("Got memberLeft event")
+
+
 @app.route('/static/<path:path>')
 def send_static_content(path):
     return send_from_directory('static', path)
diff --git a/linebot/api.py b/linebot/api.py
index 70f7faf..ce2e11f 100644
--- a/linebot/api.py
+++ b/linebot/api.py
@@ -22,7 +22,8 @@ from .__about__ import __version__
 from .exceptions import LineBotApiError
 from .http_client import HttpClient, RequestsHttpClient
 from .models import (
-    Error, Profile, MemberIds, Content, RichMenuResponse
+    Error, Profile, MemberIds, Content, RichMenuResponse, MessageQuotaResponse,
+    MessageQuotaConsumptionResponse, MessageDeliveryBroadcastResponse
 )
 
 
@@ -57,7 +58,7 @@ class LineBotApi(object):
         else:
             self.http_client = RequestsHttpClient(timeout=timeout)
 
-    def reply_message(self, reply_token, messages, timeout=None):
+    def reply_message(self, reply_token, messages, notification_disabled=False, timeout=None):
         """Call reply message API.
 
         https://devdocs.line.me/en/#reply-message
@@ -77,6 +78,8 @@ class LineBotApi(object):
             Max: 5
         :type messages: T <= :py:class:`linebot.models.send_messages.SendMessage` |
             list[T <= :py:class:`linebot.models.send_messages.SendMessage`]
+        :param bool notification_disabled: (optional) True to disable push notification
+            when the message is sent. The default value is False.
         :param timeout: (optional) How long to wait for the server
             to send data before giving up, as a float,
             or a (connect timeout, read timeout) float tuple.
@@ -88,14 +91,15 @@ class LineBotApi(object):
 
         data = {
             'replyToken': reply_token,
-            'messages': [message.as_json_dict() for message in messages]
+            'messages': [message.as_json_dict() for message in messages],
+            'notificationDisabled': notification_disabled,
         }
 
         self._post(
             '/v2/bot/message/reply', data=json.dumps(data), timeout=timeout
         )
 
-    def push_message(self, to, messages, timeout=None):
+    def push_message(self, to, messages, notification_disabled=False, timeout=None):
         """Call push message API.
 
         https://devdocs.line.me/en/#push-message
@@ -107,6 +111,8 @@ class LineBotApi(object):
             Max: 5
         :type messages: T <= :py:class:`linebot.models.send_messages.SendMessage` |
             list[T <= :py:class:`linebot.models.send_messages.SendMessage`]
+        :param bool notification_disabled: (optional) True to disable push notification
+            when the message is sent. The default value is False.
         :param timeout: (optional) How long to wait for the server
             to send data before giving up, as a float,
             or a (connect timeout, read timeout) float tuple.
@@ -118,14 +124,15 @@ class LineBotApi(object):
 
         data = {
             'to': to,
-            'messages': [message.as_json_dict() for message in messages]
+            'messages': [message.as_json_dict() for message in messages],
+            'notificationDisabled': notification_disabled,
         }
 
         self._post(
             '/v2/bot/message/push', data=json.dumps(data), timeout=timeout
         )
 
-    def multicast(self, to, messages, timeout=None):
+    def multicast(self, to, messages, notification_disabled=False, timeout=None):
         """Call multicast API.
 
         https://devdocs.line.me/en/#multicast
@@ -139,6 +146,8 @@ class LineBotApi(object):
             Max: 5
         :type messages: T <= :py:class:`linebot.models.send_messages.SendMessage` |
             list[T <= :py:class:`linebot.models.send_messages.SendMessage`]
+        :param bool notification_disabled: (optional) True to disable push notification
+            when the message is sent. The default value is False.
         :param timeout: (optional) How long to wait for the server
             to send data before giving up, as a float,
             or a (connect timeout, read timeout) float tuple.
@@ -150,13 +159,66 @@ class LineBotApi(object):
 
         data = {
             'to': to,
-            'messages': [message.as_json_dict() for message in messages]
+            'messages': [message.as_json_dict() for message in messages],
+            'notificationDisabled': notification_disabled,
         }
 
         self._post(
             '/v2/bot/message/multicast', data=json.dumps(data), timeout=timeout
         )
 
+    def broadcast(self, messages, notification_disabled=False, timeout=None):
+        """Call broadcast API.
+
+        https://developers.line.biz/en/reference/messaging-api/#send-broadcast-message
+
+        Send messages to multiple users at any time.
+
+        :param messages: Messages.
+            Max: 5
+        :type messages: T <= :py:class:`linebot.models.send_messages.SendMessage` |
+            list[T <= :py:class:`linebot.models.send_messages.SendMessage`]
+        :param bool notification_disabled: (optional) True to disable push notification
+            when the message is sent. The default value is False.
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        if not isinstance(messages, (list, tuple)):
+            messages = [messages]
+
+        data = {
+            'messages': [message.as_json_dict() for message in messages],
+            'notificationDisabled': notification_disabled,
+        }
+
+        self._post(
+            '/v2/bot/message/broadcast', data=json.dumps(data), timeout=timeout
+        )
+
+    def get_message_delivery_broadcast(self, date, timeout=None):
+        """Get number of sent broadcast messages.
+
+        https://developers.line.biz/en/reference/messaging-api/#get-number-of-broadcast-messages
+
+        Gets the number of messages sent with the /bot/message/broadcast endpoint.
+
+        :param str date: Date the messages were sent. The format is `yyyyMMdd`(Timezone is UTC+9).
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        """
+        response = self._get(
+            '/v2/bot/message/delivery/broadcast?date={date}'.format(date=date),
+            timeout=timeout
+        )
+
+        return MessageDeliveryBroadcastResponse.new_from_json_dict(response.json)
+
     def get_profile(self, user_id, timeout=None):
         """Call get profile API.
 
@@ -519,7 +581,7 @@ class LineBotApi(object):
             or a (connect timeout, read timeout) float tuple.
             Default is self.http_client.timeout
         :type timeout: float | tuple(float, float)
-        :rtype: list(T <= :py:class:`linebot.models.reponse.RichMenuResponse`)
+        :rtype: list(T <= :py:class:`linebot.models.responses.RichMenuResponse`)
         :return: list[RichMenuResponse] instance
         """
         response = self._get(
@@ -533,6 +595,46 @@ class LineBotApi(object):
 
         return result
 
+    def get_message_quota(self, timeout=None):
+        """Call Get the target limit for additional messages.
+
+        https://developers.line.biz/en/reference/messaging-api/#get-quota
+
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        :rtype: :py:class:`linebot.models.responses.MessageQuotaResponse`
+        :return: MessageQuotaResponse instance
+        """
+        response = self._get(
+            '/v2/bot/message/quota',
+            timeout=timeout
+        )
+
+        return MessageQuotaResponse.new_from_json_dict(response.json)
+
+    def get_message_quota_consumption(self, timeout=None):
+        """Get number of messages sent this month.
+
+        https://developers.line.biz/en/reference/messaging-api/#get-consumption
+
+        :param timeout: (optional) How long to wait for the server
+            to send data before giving up, as a float,
+            or a (connect timeout, read timeout) float tuple.
+            Default is self.http_client.timeout
+        :type timeout: float | tuple(float, float)
+        :rtype: :py:class:`linebot.models.responses.MessageQuotaConsumptionResponse`
+        :return: MessageQuotaConsumptionResponse instance
+        """
+        response = self._get(
+            '/v2/bot/message/quota/consumption',
+            timeout=timeout
+        )
+
+        return MessageQuotaConsumptionResponse.new_from_json_dict(response.json)
+
     def _get(self, path, params=None, headers=None, stream=False, timeout=None):
         url = self.endpoint + path
 
diff --git a/linebot/models/__init__.py b/linebot/models/__init__.py
index 819a993..e7233e8 100644
--- a/linebot/models/__init__.py
+++ b/linebot/models/__init__.py
@@ -45,7 +45,10 @@ from .events import (  # noqa
     LeaveEvent,
     PostbackEvent,
     AccountLinkEvent,
+    MemberJoinedEvent,
+    MemberLeftEvent,
     BeaconEvent,
+    ThingsEvent,
     Postback,
     Beacon,
     Link,
@@ -74,6 +77,8 @@ from .imagemap import (  # noqa
     URIImagemapAction,
     MessageImagemapAction,
     ImagemapArea,
+    Video,
+    ExternalLink,
 )
 from .messages import (  # noqa
     Message,
@@ -90,6 +95,9 @@ from .responses import (  # noqa
     MemberIds,
     Content,
     RichMenuResponse,
+    MessageQuotaResponse,
+    MessageQuotaConsumptionResponse,
+    MessageDeliveryBroadcastResponse,
     Content as MessageContent,  # backward compatibility
 )
 from .rich_menu import (  # noqa
diff --git a/linebot/models/events.py b/linebot/models/events.py
index bc7255d..4783251 100644
--- a/linebot/models/events.py
+++ b/linebot/models/events.py
@@ -261,6 +261,68 @@ class BeaconEvent(Event):
         )
 
 
+class MemberJoinedEvent(Event):
+    """Webhook MemberJoinedEvent.
+
+    https://developers.line.biz/en/reference/messaging-api/#member-joined-event
+
+    Event object for when a user joins a group or room that the bot is in.
+
+    """
+
+    def __init__(self, timestamp=None, source=None, reply_token=None,
+                 joined=None, **kwargs):
+        """__init__ method.
+
+        :param long timestamp: Time of the event in milliseconds
+        :param source: Source object
+        :type source: T <= :py:class:`linebot.models.sources.Source`
+        :param str reply_token: Reply token
+        :param joined: Joined object
+        :type joined: :py:class:`linebot.models.events.Joined`
+        :param kwargs:
+        """
+        super(MemberJoinedEvent, self).__init__(
+            timestamp=timestamp, source=source, **kwargs
+        )
+
+        self.type = 'memberJoined'
+        self.reply_token = reply_token
+        self.joined = self.get_or_new_from_json_dict(
+            joined, Joined
+        )
+
+
+class MemberLeftEvent(Event):
+    """Webhook MemberLeftEvent.
+
+    https://developers.line.biz/en/reference/messaging-api/#member-left-event
+
+    Event object for when a user leaves a group or room that the bot is in.
+
+    """
+
+    def __init__(self, timestamp=None, source=None,
+                 left=None, **kwargs):
+        """__init__ method.
+
+        :param long timestamp: Time of the event in milliseconds
+        :param source: Source object
+        :type source: T <= :py:class:`linebot.models.sources.Source`
+        :param left: Left object
+        :type left: :py:class:`linebot.models.events.Left`
+        :param kwargs:
+        """
+        super(MemberLeftEvent, self).__init__(
+            timestamp=timestamp, source=source, **kwargs
+        )
+
+        self.type = 'memberLeft'
+        self.left = self.get_or_new_from_json_dict(
+            left, Left
+        )
+
+
 class AccountLinkEvent(Event):
     """Webhook AccountLinkEvent.
 
@@ -294,6 +356,37 @@ class AccountLinkEvent(Event):
         )
 
 
+class ThingsEvent(Event):
+    """Webhook ThingsEvent.
+
+    https://developers.line.biz/en/reference/messaging-api/#device-link-event
+
+    Indicates that a LINE Things-compatible device has been linked with LINE by
+    a user operation.
+    """
+
+    def __init__(self, timestamp=None, source=None, reply_token=None, things=None, **kwargs):
+        """__init__ method.
+
+        :param long timestamp: Time of the event in milliseconds
+        :param source: Source object
+        :type source: T <= :py:class:`linebot.models.sources.Source`
+        :param str reply_token: Reply token
+        :param things: Things object
+        :type things: :py:class:`linebot.models.events.Things`
+        :param kwargs:
+        """
+        super(ThingsEvent, self).__init__(
+            timestamp=timestamp, source=source, **kwargs
+        )
+
+        self.type = 'things'
+        self.reply_token = reply_token
+        self.things = self.get_or_new_from_json_dict(
+            things, Things
+        )
+
+
 class Postback(Base):
     """Postback.
 
@@ -345,6 +438,50 @@ class Beacon(Base):
         return bytearray.fromhex(self.dm) if self.dm is not None else None
 
 
+class Joined(Base):
+    """Joined.
+
+    https://developers.line.biz/en/reference/messaging-api/#member-joined-event
+    """
+
+    def __init__(self, members=None, **kwargs):
+        """__init__ method.
+
+        :param dict members: Member of users who joined
+        :param kwargs:
+        """
+        super(Joined, self).__init__(**kwargs)
+
+        self._members = members
+
+    @property
+    def members(self):
+        """Get members as list of SourceUser."""
+        return [SourceUser(user_id=x['userId']) for x in self._members]
+
+
+class Left(Base):
+    """Left.
+
+    https://developers.line.biz/en/reference/messaging-api/#member-left-event
+    """
+
+    def __init__(self, members=None, **kwargs):
+        """__init__ method.
+
+        :param dict members: Member of users who joined
+        :param kwargs:
+        """
+        super(Left, self).__init__(**kwargs)
+
+        self._members = members
+
+    @property
+    def members(self):
+        """Get members as list of SourceUser."""
+        return [SourceUser(user_id=x['userId']) for x in self._members]
+
+
 class Link(Base):
     """Link.
 
@@ -361,3 +498,21 @@ class Link(Base):
 
         self.result = result
         self.nonce = nonce
+
+
+class Things(Base):
+    """Things.
+
+    https://developers.line.biz/en/docs/line-things/develop-bot/#link-event
+    """
+
+    def __init__(self, device_id=None, type=None, **kwargs):
+        """__init__ method.
+
+        :param str device_id: Device ID of the device that was linked with LINE.
+        :param str type: link or unlink
+        """
+        super(Things, self).__init__(**kwargs)
+
+        self.device_id = device_id
+        self.type = type
diff --git a/linebot/models/imagemap.py b/linebot/models/imagemap.py
index cd06f4b..1548684 100644
--- a/linebot/models/imagemap.py
+++ b/linebot/models/imagemap.py
@@ -27,13 +27,14 @@ from .send_messages import SendMessage
 class ImagemapSendMessage(SendMessage):
     """ImagemapSendMessage.
 
-    https://devdocs.line.me/en/#imagemap-message
+    https://developers.line.biz/en/reference/messaging-api/#imagemap-message
 
     Imagemaps are images with one or more links. You can assign one link for the entire image
     or multiple links which correspond to different regions of the image.
     """
 
-    def __init__(self, base_url=None, alt_text=None, base_size=None, actions=None, **kwargs):
+    def __init__(self, base_url=None, alt_text=None, base_size=None,
+                 video=None, actions=None, **kwargs):
         """__init__ method.
 
         :param str base_url: Base URL of image.
@@ -41,6 +42,8 @@ class ImagemapSendMessage(SendMessage):
         :param str alt_text: Alternative text
         :param base_size: Width and height of base image
         :type base_size: :py:class:`linebot.models.imagemap.BaseSize`
+        :param video: Video in imagemap message
+        :type video: :py:class:`linebot.models.imagemap.Video`
         :param actions: Action when tapped
         :type actions: list[T <= :py:class:`linebot.models.imagemap.ImagemapAction`]
         :param kwargs:
@@ -53,6 +56,9 @@ class ImagemapSendMessage(SendMessage):
         self.base_size = self.get_or_new_from_json_dict(
             base_size, BaseSize
         )
+        self.video = self.get_or_new_from_json_dict(
+            video, Video
+        )
 
         new_actions = []
         if actions:
@@ -172,3 +178,52 @@ class ImagemapArea(Base):
         self.y = y
         self.width = width
         self.height = height
+
+
+class Video(Base):
+    """Video.
+
+    https://developers.line.biz/en/reference/messaging-api/#imagemap-message
+
+    Defines the properties of the video object in imagemap.
+    """
+
+    def __init__(self, original_content_url=None, preview_image_url=None,
+                 area=None, external_link=None, **kwargs):
+        """__init__ method.
+
+        :param str original_content_url: URL of the video file
+        :param str preview_image_url: URL of the preview image
+        :param area: Defined video area
+        :type area: :py:class:`linebot.models.imagemap.ImagemapArea`
+        :param external_link: Defined video external link
+        :type external_link: :py:class:`linebot.models.imagemap.ExternalLink`
+        :param kwargs:
+        """
+        super(Video, self).__init__(**kwargs)
+
+        self.original_content_url = original_content_url
+        self.preview_image_url = preview_image_url
+        self.area = self.get_or_new_from_json_dict(area, ImagemapArea)
+        self.external_link = self.get_or_new_from_json_dict(external_link, ExternalLink)
+
+
+class ExternalLink(Base):
+    """ExternalLink.
+
+    https://developers.line.biz/en/reference/messaging-api/#imagemap-message
+
+    Defines URL and label of external link in video.
+    """
+
+    def __init__(self, link_uri=None, label=None, **kwargs):
+        """__init__ method.
+
+        :param str link_uri: Webpage URL
+        :param str label: Label
+        :param kwargs:
+        """
+        super(ExternalLink, self).__init__(**kwargs)
+
+        self.link_uri = link_uri
+        self.label = label
diff --git a/linebot/models/responses.py b/linebot/models/responses.py
index 8cd6e08..ab09af4 100644
--- a/linebot/models/responses.py
+++ b/linebot/models/responses.py
@@ -150,3 +150,60 @@ class RichMenuResponse(Base):
                     self.get_or_new_from_json_dict(area, RichMenuArea)
                 )
         self.areas = new_areas
+
+
+class MessageQuotaResponse(Base):
+    """MessageQuotaResponse.
+
+    https://developers.line.biz/en/reference/messaging-api/#get-quota
+    """
+
+    def __init__(self, type=None, value=None, **kwargs):
+        """__init__ method.
+
+        :param str type: Quota limitation type
+        :param int value: The target limit for additional messages in the current month.
+            This property is returned when the type property has a value of limited.
+        :param kwargs:
+        """
+        super(MessageQuotaResponse, self).__init__(**kwargs)
+
+        self.type = type
+        self.value = value
+
+
+class MessageQuotaConsumptionResponse(Base):
+    """MessageQuotaConsumptionResponse.
+
+    https://developers.line.biz/en/reference/messaging-api/#get-consumption
+    """
+
+    def __init__(self, total_usage=None, **kwargs):
+        """__init__ method.
+
+        :param str total_usage: The number of sent messages in the current month
+        :param kwargs:
+        """
+        super(MessageQuotaConsumptionResponse, self).__init__(**kwargs)
+
+        self.total_usage = total_usage
+
+
+class MessageDeliveryBroadcastResponse(Base):
+    """MessageDeliveryBroadcastResponse.
+
+    https://developers.line.biz/en/reference/messaging-api/#get-number-of-broadcast-messages
+    """
+
+    def __init__(self, status=None, success=None, **kwargs):
+        """__init__ method.
+
+        :param str status: Status of the counting process.
+        :param int success: The number of messages sent with the Messaging API on the
+            date specified in date.
+        :param kwargs:
+        """
+        super(MessageDeliveryBroadcastResponse, self).__init__(**kwargs)
+
+        self.status = status
+        self.success = success
diff --git a/linebot/webhook.py b/linebot/webhook.py
index 6253dcb..1fed867 100644
--- a/linebot/webhook.py
+++ b/linebot/webhook.py
@@ -32,6 +32,9 @@ from .models.events import (
     PostbackEvent,
     BeaconEvent,
     AccountLinkEvent,
+    MemberJoinedEvent,
+    MemberLeftEvent,
+    ThingsEvent,
 )
 from .utils import LOGGER, PY3, safe_compare_digest
 
@@ -144,6 +147,12 @@ class WebhookParser(object):
                 events.append(BeaconEvent.new_from_json_dict(event))
             elif event_type == 'accountLink':
                 events.append(AccountLinkEvent.new_from_json_dict(event))
+            elif event_type == 'memberJoined':
+                events.append(MemberJoinedEvent.new_from_json_dict(event))
+            elif event_type == 'memberLeft':
+                events.append(MemberLeftEvent.new_from_json_dict(event))
+            elif event_type == 'things':
+                events.append(ThingsEvent.new_from_json_dict(event))
             else:
                 LOGGER.warn('Unknown event type. type=' + event_type)
 
diff --git a/tox.ini b/tox.ini
index 4367fb6..0815ab7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py27, py34, py35, py36, py36-flake8-src, py36-flake8-other
+envlist = py27, py34, py35, py36, py37, py37-flake8-src, py37-flake8-other
 
 [testenv]
 deps =
@@ -8,21 +8,21 @@ deps =
     -r{toxinidir}/requirements-test.txt
 commands = py.test -v tests/
 
-[testenv:py36]
-basepython = python3.6
+[testenv:py37]
+basepython = python3.7
 deps = {[testenv]deps}
 commands = py.test -v --cov=linebot tests/
 
-[testenv:py36-flake8-src]
-basepython = python3.6
+[testenv:py37-flake8-src]
+basepython = python3.7
 deps =
     six
     flake8
     flake8-docstrings
 commands = flake8 linebot/
 
-[testenv:py36-flake8-other]
-basepython = python3.6
+[testenv:py37-flake8-other]
+basepython = python3.7
 deps =
     six
     flake8


Test patch:
diff --git a/tests/api/test_send_audio_message.py b/tests/api/test_send_audio_message.py
index 9db1e52..24888a3 100644
--- a/tests/api/test_send_audio_message.py
+++ b/tests/api/test_send_audio_message.py
@@ -62,6 +62,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -85,6 +86,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -108,6 +110,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
diff --git a/tests/api/test_send_image_message.py b/tests/api/test_send_image_message.py
index 0ef0115..1b59e3f 100644
--- a/tests/api/test_send_image_message.py
+++ b/tests/api/test_send_image_message.py
@@ -61,6 +61,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -84,6 +85,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -107,6 +109,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
diff --git a/tests/api/test_send_imagemap_message.py b/tests/api/test_send_imagemap_message.py
index b94f315..bbac91d 100644
--- a/tests/api/test_send_imagemap_message.py
+++ b/tests/api/test_send_imagemap_message.py
@@ -24,7 +24,8 @@ from linebot import (
 )
 from linebot.models import (
     ImagemapSendMessage, BaseSize, URIImagemapAction,
-    ImagemapArea, MessageImagemapAction
+    ImagemapArea, MessageImagemapAction,
+    Video, ExternalLink
 )
 
 
@@ -36,6 +37,17 @@ class TestLineBotApi(unittest.TestCase):
             base_url='https://example.com/base',
             alt_text='this is an imagemap',
             base_size=BaseSize(height=1040, width=1040),
+            video=Video(
+                original_content_url='https://example.com/video.mp4',
+                preview_image_url='https://example.com/video_preview.jpg',
+                area=ImagemapArea(
+                    x=0, y=0, width=1040, height=585
+                ),
+                external_link=ExternalLink(
+                    link_uri='https://example.com/see_more.html',
+                    label='See More',
+                ),
+            ),
             actions=[
                 URIImagemapAction(
                     link_uri='https://example.com/',
@@ -60,6 +72,20 @@ class TestLineBotApi(unittest.TestCase):
                 "height": 1040,
                 "width": 1040
             },
+            "video": {
+                "originalContentUrl": "https://example.com/video.mp4",
+                "previewImageUrl": "https://example.com/video_preview.jpg",
+                "area": {
+                    "x": 0,
+                    "y": 0,
+                    "width": 1040,
+                    "height": 585
+                },
+                "externalLink": {
+                    "linkUri": "https://example.com/see_more.html",
+                    "label": "See More"
+                }
+            },
             "actions": [
                 {
                     "type": "uri",
@@ -103,6 +129,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -126,6 +153,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -149,6 +177,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
diff --git a/tests/api/test_send_location_message.py b/tests/api/test_send_location_message.py
index 03fa5c9..c7f5d0c 100644
--- a/tests/api/test_send_location_message.py
+++ b/tests/api/test_send_location_message.py
@@ -65,6 +65,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -88,6 +89,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -111,6 +113,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
diff --git a/tests/api/test_send_sticker_message.py b/tests/api/test_send_sticker_message.py
index 940c59e..323d9aa 100644
--- a/tests/api/test_send_sticker_message.py
+++ b/tests/api/test_send_sticker_message.py
@@ -60,6 +60,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -83,6 +84,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -106,6 +108,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
diff --git a/tests/api/test_send_template_message.py b/tests/api/test_send_template_message.py
index c0e2e81..de8f840 100644
--- a/tests/api/test_send_template_message.py
+++ b/tests/api/test_send_template_message.py
@@ -380,6 +380,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.button_message
             }
         )
@@ -403,6 +404,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.button_message
             }
         )
@@ -426,6 +428,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.confirm_message
             }
         )
@@ -450,6 +453,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.confirm_message
             }
         )
@@ -473,6 +477,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.carousel_message
             }
         )
@@ -496,6 +501,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.carousel_message
             }
         )
@@ -519,6 +525,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.carousel_message
             }
         )
@@ -542,6 +549,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.image_carousel_message
             }
         )
@@ -565,6 +573,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.image_carousel_message
             }
         )
@@ -588,6 +597,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
                 "messages": self.image_carousel_message
             }
         )
diff --git a/tests/api/test_send_text_message.py b/tests/api/test_send_text_message.py
index f64921d..1256324 100644
--- a/tests/api/test_send_text_message.py
+++ b/tests/api/test_send_text_message.py
@@ -54,6 +54,7 @@ class TestSendTestMessage(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -77,6 +78,7 @@ class TestSendTestMessage(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -100,6 +102,46 @@ class TestSendTestMessage(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
+                'notificationDisabled': False,
+                "messages": self.message
+            }
+        )
+
+    @responses.activate
+    def test_broadcast_text_message(self):
+        responses.add(
+            responses.POST,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/broadcast',
+            json={}, status=200
+        )
+
+        self.tested.broadcast(self.text_message)
+
+        request = responses.calls[0].request
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/broadcast')
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            json.loads(request.body),
+            {
+                'notificationDisabled': False,
+                "messages": self.message
+            }
+        )
+
+        # call with notification_disable=True
+        self.tested.broadcast(self.text_message, notification_disabled=True)
+
+        request = responses.calls[1].request
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/broadcast')
+        self.assertEqual(request.method, 'POST')
+        self.assertEqual(
+            json.loads(request.body),
+            {
+                'notificationDisabled': True,
                 "messages": self.message
             }
         )
diff --git a/tests/api/test_send_text_message_with_quick_reply.py b/tests/api/test_send_text_message_with_quick_reply.py
index 1ff8f5e..ae50260 100644
--- a/tests/api/test_send_text_message_with_quick_reply.py
+++ b/tests/api/test_send_text_message_with_quick_reply.py
@@ -81,6 +81,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": [
                     {
                         "type": "text",
diff --git a/tests/api/test_send_video_message.py b/tests/api/test_send_video_message.py
index 217b583..e3c6e02 100644
--- a/tests/api/test_send_video_message.py
+++ b/tests/api/test_send_video_message.py
@@ -61,6 +61,7 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": "to",
+                'notificationDisabled': False,
                 "messages": self.message
             }
         )
@@ -84,7 +85,8 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "replyToken": "replyToken",
-                "messages": self.message
+                "messages": self.message,
+                'notificationDisabled': False,
             }
         )
 
@@ -107,7 +109,8 @@ class TestLineBotApi(unittest.TestCase):
             json.loads(request.body),
             {
                 "to": ['to1', 'to2'],
-                "messages": self.message
+                "messages": self.message,
+                "notificationDisabled": False,
             }
         )
 
diff --git a/tests/test_webhook.py b/tests/test_webhook.py
index f26f1c5..a6e3b5f 100644
--- a/tests/test_webhook.py
+++ b/tests/test_webhook.py
@@ -24,6 +24,7 @@ from linebot import (
 from linebot.models import (
     MessageEvent, FollowEvent, UnfollowEvent, JoinEvent,
     LeaveEvent, PostbackEvent, BeaconEvent, AccountLinkEvent,
+    MemberJoinedEvent, MemberLeftEvent, ThingsEvent,
     TextMessage, ImageMessage, VideoMessage, AudioMessage,
     LocationMessage, StickerMessage, FileMessage,
     SourceUser, SourceRoom, SourceGroup
@@ -303,20 +304,67 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[18].postback.data, 'action=buyItem&itemId=123123&color=red')
         self.assertEqual(events[18].postback.params['datetime'], '2013-04-01T10:00')
 
-        # MessageEvent, SourceUser, FileMessage
-        self.assertIsInstance(events[19], MessageEvent)
+        # ThingsEvent, SourceUser, link
+        self.assertIsInstance(events[19], ThingsEvent)
         self.assertEqual(events[19].reply_token, 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA')
-        self.assertEqual(events[19].type, 'message')
+        self.assertEqual(events[19].type, 'things')
         self.assertEqual(events[19].timestamp, 1462629479859)
         self.assertIsInstance(events[19].source, SourceUser)
         self.assertEqual(events[19].source.type, 'user')
         self.assertEqual(events[19].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertEqual(events[19].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
-        self.assertIsInstance(events[19].message, FileMessage)
-        self.assertEqual(events[19].message.id, '325708')
-        self.assertEqual(events[19].message.type, 'file')
-        self.assertEqual(events[19].message.file_name, "file.txt")
-        self.assertEqual(events[19].message.file_size, 2138)
+        self.assertEqual(events[19].things.device_id, 't2c449c9d1')
+        self.assertEqual(events[19].things.type, 'link')
+
+        # MemberJoinedEvent
+        self.assertIsInstance(events[20], MemberJoinedEvent)
+        self.assertEqual(events[20].reply_token, '0f3779fba3b349968c5d07db31eabf65')
+        self.assertEqual(events[20].type, 'memberJoined')
+        self.assertEqual(events[20].timestamp, 1462629479859)
+        self.assertIsInstance(events[20].source, SourceGroup)
+        self.assertEqual(events[20].source.type, 'group')
+        self.assertEqual(events[20].source.group_id, 'C4af4980629...')
+        self.assertEqual(len(events[20].joined.members), 2)
+        self.assertIsInstance(events[20].joined.members[0], SourceUser)
+        self.assertEqual(events[20].joined.members[0].user_id, 'U4af4980629...')
+        self.assertEqual(events[20].joined.members[1].user_id, 'U91eeaf62d9...')
+
+        # MemberLeftEvent
+        self.assertIsInstance(events[21], MemberLeftEvent)
+        self.assertEqual(events[21].type, 'memberLeft')
+        self.assertEqual(events[21].timestamp, 1462629479960)
+        self.assertIsInstance(events[21].source, SourceGroup)
+        self.assertEqual(events[21].source.type, 'group')
+        self.assertEqual(events[21].source.group_id, 'C4af4980629...')
+        self.assertEqual(len(events[21].left.members), 2)
+        self.assertIsInstance(events[21].left.members[0], SourceUser)
+        self.assertEqual(events[21].left.members[0].user_id, 'U4af4980629...')
+        self.assertEqual(events[21].left.members[1].user_id, 'U91eeaf62d9...')
+
+        # ThingsEvent, SourceUser, unlink
+        self.assertIsInstance(events[22], ThingsEvent)
+        self.assertEqual(events[22].reply_token, 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA')
+        self.assertEqual(events[22].type, 'things')
+        self.assertEqual(events[22].timestamp, 1462629479859)
+        self.assertIsInstance(events[22].source, SourceUser)
+        self.assertEqual(events[22].source.type, 'user')
+        self.assertEqual(events[22].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[22].things.device_id, 't2c449c9d1')
+        self.assertEqual(events[22].things.type, 'unlink')
+
+        # MessageEvent, SourceUser, FileMessage
+        self.assertIsInstance(events[23], MessageEvent)
+        self.assertEqual(events[23].reply_token, 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA')
+        self.assertEqual(events[23].type, 'message')
+        self.assertEqual(events[23].timestamp, 1462629479859)
+        self.assertIsInstance(events[23].source, SourceUser)
+        self.assertEqual(events[23].source.type, 'user')
+        self.assertEqual(events[23].source.user_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertEqual(events[23].source.sender_id, 'U206d25c2ea6bd87c17655609a1c37cb8')
+        self.assertIsInstance(events[23].message, FileMessage)
+        self.assertEqual(events[23].message.id, '325708')
+        self.assertEqual(events[23].message.type, 'file')
+        self.assertEqual(events[23].message.file_name, "file.txt")
+        self.assertEqual(events[23].message.file_size, 2138)
 
 
 class TestWebhookHandler(unittest.TestCase):
diff --git a/tests/text/webhook.json b/tests/text/webhook.json
index 01d8bad..bfa54ec 100644
--- a/tests/text/webhook.json
+++ b/tests/text/webhook.json
@@ -246,6 +246,73 @@
         }
       }
     },
+    {
+      "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
+      "type": "things",
+      "timestamp": 1462629479859,
+      "source": {
+        "type": "user",
+        "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
+      },
+      "things": {
+        "deviceId": "t2c449c9d1",
+        "type": "link"
+      }
+    },
+    {
+      "replyToken": "0f3779fba3b349968c5d07db31eabf65",
+      "type": "memberJoined",
+      "timestamp": 1462629479859,
+      "source": {
+        "type": "group",
+        "groupId": "C4af4980629..."
+      },
+      "joined": {
+        "members": [
+          {
+            "type": "user",
+            "userId": "U4af4980629..."
+          },
+          {
+            "type": "user",
+            "userId": "U91eeaf62d9..."
+          }
+        ]
+      }
+    },
+    {
+      "type": "memberLeft",
+      "timestamp": 1462629479960,
+      "source": {
+        "type": "group",
+        "groupId": "C4af4980629..."
+      },
+      "left": {
+        "members": [
+          {
+            "type": "user",
+            "userId": "U4af4980629..."
+          },
+          {
+            "type": "user",
+            "userId": "U91eeaf62d9..."
+          }
+        ]
+      }
+    },
+    {
+      "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
+      "type": "things",
+      "timestamp": 1462629479859,
+      "source": {
+        "type": "user",
+        "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
+      },
+      "things": {
+        "deviceId": "t2c449c9d1",
+        "type": "unlink"
+      }
+    },
     {
       "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
       "type": "message",