Repository: line/line-bot-sdk-python

Issue:
Member Join/Leave Events
https://developers.line-beta.biz/en/reference/messaging-api/#member-joined-event
https://developers.line-beta.biz/en/reference/messaging-api/#member-left-event


Comments:

Patch:
diff --git a/examples/flask-kitchensink/app.py b/examples/flask-kitchensink/app.py
index 08a41a8..63dc012 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,
@@ -436,6 +437,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/models/__init__.py b/linebot/models/__init__.py
index 819a993..f8894ea 100644
--- a/linebot/models/__init__.py
+++ b/linebot/models/__init__.py
@@ -45,6 +45,8 @@ from .events import (  # noqa
     LeaveEvent,
     PostbackEvent,
     AccountLinkEvent,
+    MemberJoinedEvent,
+    MemberLeftEvent,
     BeaconEvent,
     Postback,
     Beacon,
diff --git a/linebot/models/events.py b/linebot/models/events.py
index bc7255d..2f570b9 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.
 
@@ -345,6 +407,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.
 
diff --git a/linebot/webhook.py b/linebot/webhook.py
index 6253dcb..415ceb1 100644
--- a/linebot/webhook.py
+++ b/linebot/webhook.py
@@ -32,6 +32,8 @@ from .models.events import (
     PostbackEvent,
     BeaconEvent,
     AccountLinkEvent,
+    MemberJoinedEvent,
+    MemberLeftEvent,
 )
 from .utils import LOGGER, PY3, safe_compare_digest
 
@@ -144,6 +146,10 @@ 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))
             else:
                 LOGGER.warn('Unknown event type. type=' + event_type)
 


Test patch:
diff --git a/tests/test_webhook.py b/tests/test_webhook.py
index f26f1c5..980b5a5 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,
     TextMessage, ImageMessage, VideoMessage, AudioMessage,
     LocationMessage, StickerMessage, FileMessage,
     SourceUser, SourceRoom, SourceGroup
@@ -318,6 +319,31 @@ class TestWebhookParser(unittest.TestCase):
         self.assertEqual(events[19].message.file_name, "file.txt")
         self.assertEqual(events[19].message.file_size, 2138)
 
+        # 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...')
+
 
 class TestWebhookHandler(unittest.TestCase):
     def setUp(self):
diff --git a/tests/text/webhook.json b/tests/text/webhook.json
index 01d8bad..09ab5a9 100644
--- a/tests/text/webhook.json
+++ b/tests/text/webhook.json
@@ -260,6 +260,47 @@
         "fileName": "file.txt",
         "fileSize": 2138
       }
+    },
+    {
+      "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..."
+          }
+        ]
+      }
     }
   ]
 }