Skip to content

Commit 9e8cb7b

Browse files
authored
Support a new Messaging API endpoint for friend statistics (#216)
* modify to_snake_case for the case that field name is uniqueMediaPlayed25Percent * implement get_insight_message_event for a new api endpoint * add a test for get_insight_message_event * update README * rename response class and modify comments * remove unnecessary sentence * update to_snake_case function
1 parent e5f296f commit 9e8cb7b

File tree

8 files changed

+243
-5
lines changed

8 files changed

+243
-5
lines changed

README.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,18 @@ https://developers.line.biz/en/reference/messaging-api/#get-demographic
496496
insight = line_bot_api.get_insight_demographic()
497497
print(insight.genders)
498498
499+
get\_insight\_message\_event(self, request_id, timeout=None)
500+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
501+
502+
Return statistics about how users interact with broadcast messages.
503+
504+
https://developers.line.biz/en/reference/messaging-api/#get-message-event
505+
506+
.. code:: python
507+
508+
insight = line_bot_api.get_insight_message_event()
509+
print(insight.overview)
510+
499511
※ Error handling
500512
^^^^^^^^^^^^^^^^
501513

linebot/api.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
MessageDeliveryBroadcastResponse, MessageDeliveryMulticastResponse,
2828
MessageDeliveryPushResponse, MessageDeliveryReplyResponse,
2929
InsightMessageDeliveryResponse, InsightFollowersResponse, InsightDemographicResponse,
30+
InsightMessageEventResponse,
3031
)
3132

3233

@@ -933,6 +934,25 @@ def get_insight_demographic(self, timeout=None):
933934

934935
return InsightDemographicResponse.new_from_json_dict(response.json)
935936

937+
def get_insight_message_event(self, request_id, timeout=None):
938+
"""Return statistics about how users interact with broadcast messages.
939+
940+
https://developers.line.biz/en/reference/messaging-api/#get-message-event
941+
942+
:param str request_id: Request ID of broadcast message.
943+
:param timeout: (optional) How long to wait for the server
944+
to send data before giving up, as a float,
945+
or a (connect timeout, read timeout) float tuple.
946+
Default is self.http_client.timeout
947+
:type timeout: float | tuple(float, float)
948+
"""
949+
response = self._get(
950+
'/v2/bot/insight/message/event?requestId={request_id}'.format(request_id=request_id),
951+
timeout=timeout
952+
)
953+
954+
return InsightMessageEventResponse.new_from_json_dict(response.json)
955+
936956
def _get(self, path, params=None, headers=None, stream=False, timeout=None):
937957
url = self.endpoint + path
938958

linebot/models/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@
8989
AppTypeInsight,
9090
GenderInsight,
9191
SubscriptionPeriodInsight,
92+
MessageStatistics,
93+
MessageInsight,
94+
ClickInsight,
9295
)
9396
from .messages import ( # noqa
9497
Message,
@@ -117,6 +120,7 @@
117120
InsightMessageDeliveryResponse,
118121
InsightFollowersResponse,
119122
InsightDemographicResponse,
123+
InsightMessageEventResponse,
120124
)
121125
from .rich_menu import ( # noqa
122126
RichMenu,

linebot/models/insight.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,106 @@ def __init__(self, percentage=None, subscription_period=None, **kwargs):
109109
super(SubscriptionPeriodInsight, self).__init__(percentage=percentage, **kwargs)
110110

111111
self.subscription_period = subscription_period
112+
113+
114+
class MessageStatistics(Base):
115+
"""MessageStatistics."""
116+
117+
def __init__(self, request_id=None, timestamp=None, delivered=None,
118+
unique_impression=None, unique_click=None, unique_media_played=None,
119+
unique_media_played_100_percent=None, **kwargs):
120+
"""__init__ method.
121+
122+
:param str request_id: Request ID.
123+
:param int timestamp: UNIX timestamp for message delivery time.
124+
:param int delivered: Number of messages delivered. This property shows values
125+
of less than 20.
126+
:param int unique_impression: Number of people who opened the message,
127+
meaning they displayed at least 1 bubble.
128+
:param int unique_click: Number of people who opened any URL in the message.
129+
:param int unique_media_played: Number of people who started playing any video
130+
or audio in the message.
131+
:param int unique_media_played_100_percent: Number of people who played the entirety of
132+
any video or audio in the message.
133+
"""
134+
super(MessageStatistics, self).__init__(**kwargs)
135+
136+
self.request_id = request_id
137+
self.timestamp = timestamp
138+
self.delivered = delivered
139+
self.unique_impression = unique_impression
140+
self.unique_click = unique_click
141+
self.unique_media_played = unique_media_played
142+
self.unique_media_played_100_percent = unique_media_played_100_percent
143+
144+
145+
class MessageInsight(Base):
146+
"""MessageInsight."""
147+
148+
def __init__(self, seq=None, impression=None, media_played=None,
149+
media_played_25_percent=None, media_played_50_percent=None,
150+
media_played_75_percent=None, media_played_100_percent=None,
151+
unique_media_played=None, unique_media_played_25_percent=None,
152+
unique_media_played_50_percent=None, unique_media_played_75_percent=None,
153+
unique_media_played_100_percent=None, **kwargs):
154+
"""__init__ method.
155+
156+
:param int seq: Bubble's serial number.
157+
:param int impression: Number of times the bubble was displayed.
158+
:param int media_played: Number of times audio or video in the bubble started playing.
159+
:param int media_played_25_percent: Number of times audio or video
160+
in the bubble was played from start to 25%.
161+
:param int media_played_50_percent: Number of times audio or video
162+
in the bubble was played from start to 50%.
163+
:param int media_played_75_percent: Number of times audio or video
164+
in the bubble was played from start to 75%.
165+
:param int media_played_100_percent: Number of times audio or video
166+
in the bubble was played in its entirety.
167+
:param int unique_media_played: Number of people that started playing
168+
audio or video in the bubble.
169+
:param int unique_media_played_25_percent: Number of people that played
170+
audio or video in the bubble from start to 25%.
171+
:param int unique_media_played_50_percent: Number of people that played
172+
audio or video in the bubble from start to 50%.
173+
:param int unique_media_played_75_percent: Number of people that played
174+
audio or video in the bubble from start to 75%.
175+
:param int unique_media_played_100_percent: Number of people that played
176+
audio or video in the bubble in its entirety.
177+
"""
178+
super(MessageInsight, self).__init__(**kwargs)
179+
180+
self.seq = seq
181+
self.impression = impression
182+
self.media_played = media_played
183+
self.media_played_25_percent = media_played_25_percent
184+
self.media_played_50_percent = media_played_50_percent
185+
self.media_played_75_percent = media_played_75_percent
186+
self.media_played_100_percent = media_played_100_percent
187+
self.unique_media_played = unique_media_played
188+
self.unique_media_played_25_percent = unique_media_played_25_percent
189+
self.unique_media_played_50_percent = unique_media_played_50_percent
190+
self.unique_media_played_75_percent = unique_media_played_75_percent
191+
self.unique_media_played_100_percent = unique_media_played_100_percent
192+
193+
194+
class ClickInsight(Base):
195+
"""ClickInsight."""
196+
197+
def __init__(self, seq=None, url=None, click=None, unique_click=None,
198+
unique_click_of_request=None, **kwargs):
199+
"""__init__ method.
200+
201+
:param int seq: The URL's serial number.
202+
:param str url: URL.
203+
:param int click: Number of times the URL was opened.
204+
:param int unique_click: Number of people that opened the URL.
205+
:param int unique_click_of_request: Number of people who opened this url
206+
through any link in the message.
207+
"""
208+
super(ClickInsight, self).__init__(**kwargs)
209+
210+
self.seq = seq
211+
self.url = url
212+
self.click = click
213+
self.unique_click = unique_click
214+
self.unique_click_of_request = unique_click_of_request

linebot/models/responses.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
from .base import Base
2020
from .insight import (
2121
SubscriptionPeriodInsight, AppTypeInsight,
22-
AgeInsight, GenderInsight, AreaInsight
22+
AgeInsight, GenderInsight, AreaInsight,
23+
MessageInsight, ClickInsight, MessageStatistics,
2324
)
2425
from .rich_menu import RichMenuSize, RichMenuArea
2526

@@ -392,3 +393,24 @@ def __init__(self, available=None, genders=None, ages=None,
392393
self.app_types = [self.get_or_new_from_json_dict(it, AppTypeInsight) for it in app_types]
393394
self.subscription_periods = [self.get_or_new_from_json_dict(it, SubscriptionPeriodInsight)
394395
for it in subscription_periods]
396+
397+
398+
class InsightMessageEventResponse(Base):
399+
"""InsightMessageEventResponse."""
400+
401+
def __init__(self, overview=None, messages=None, clicks=None, **kwargs):
402+
"""__init__ method.
403+
404+
:param overview: Summary of message statistics.
405+
:type overview: T <= :py:class:`linebot.models.MessageStatistics`
406+
:param messages: Array of information about individual message bubbles.
407+
:type messages: list[T <= :py:class:`linebot.models.MessageInsight`]
408+
:param clicks: Array of information about URLs in the message.
409+
:type clicks: list[T <= :py:class:`linebot.models.ClickInsight`]
410+
:param kwargs:
411+
"""
412+
super(InsightMessageEventResponse, self).__init__(**kwargs)
413+
414+
self.overview = self.get_or_new_from_json_dict(overview, MessageStatistics)
415+
self.messages = [self.get_or_new_from_json_dict(it, MessageInsight) for it in messages]
416+
self.clicks = [self.get_or_new_from_json_dict(it, ClickInsight) for it in clicks]

linebot/utils.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
1414

15-
"""linebot.http_client module."""
15+
"""linebot.utils module."""
1616

1717
from __future__ import unicode_literals
1818

1919
import logging
2020
import re
21+
2122
import sys
2223

2324
LOGGER = logging.getLogger('linebot')
@@ -31,8 +32,10 @@ def to_snake_case(text):
3132
:param str text:
3233
:rtype: str
3334
"""
34-
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', text)
35-
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
35+
s1 = re.sub('(.)([A-Z])', r'\1_\2', text)
36+
s2 = re.sub('(.)([0-9]+)', r'\1_\2', s1)
37+
s3 = re.sub('([0-9])([a-z])', r'\1_\2', s2)
38+
return s3.lower()
3639

3740

3841
def to_camel_case(text):

tests/api/test_get_insight.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class TestLineBotApi(unittest.TestCase):
3030
def setUp(self):
3131
self.tested = LineBotApi('channel_secret')
3232
self.date = '20190101'
33+
self.request_id = 'f70dd685-499a-4231-a441-f24b8d4fba21'
3334

3435
@responses.activate
3536
def test_get_insight_message_delivery(self):
@@ -187,6 +188,75 @@ def test_get_insight_demographic(self):
187188
res.subscription_periods
188189
)
189190

191+
@responses.activate
192+
def test_get_insight_message_event(self):
193+
json = {
194+
'overview': {
195+
'requestId': 'f70dd685-499a-4231-a441-f24b8d4fba21',
196+
'timestamp': 1568214000,
197+
'delivered': 32,
198+
'uniqueImpression': 4,
199+
'uniqueClick': None,
200+
'uniqueMediaPlayed': 2,
201+
'uniqueMediaPlayed100Percent': -1
202+
},
203+
'messages': [
204+
{
205+
'seq': 1,
206+
'impression': 18,
207+
'mediaPlayed': 11,
208+
'mediaPlayed25Percent': -1,
209+
'mediaPlayed50Percent': -1,
210+
'mediaPlayed75Percent': -1,
211+
'mediaPlayed100Percent': -1,
212+
'uniqueMediaPlayed': 2,
213+
'uniqueMediaPlayed25Percent': -1,
214+
'uniqueMediaPlayed50Percent': -1,
215+
'uniqueMediaPlayed75Percent': -1,
216+
'uniqueMediaPlayed100Percent': -1
217+
}
218+
],
219+
'clicks': [
220+
{
221+
'seq': 1,
222+
'url': 'https://www.yahoo.co.jp/',
223+
'click': -1,
224+
'uniqueClick': -1,
225+
'uniqueClickOfRequest': -1
226+
},
227+
{
228+
'seq': 1,
229+
'url': 'https://www.google.com/?hl=ja',
230+
'click': -1,
231+
'uniqueClick': -1,
232+
'uniqueClickOfRequest': -1
233+
}
234+
]
235+
}
236+
responses.add(
237+
responses.GET,
238+
LineBotApi.DEFAULT_API_ENDPOINT +
239+
'/v2/bot/insight/message/event?requestId={request_id}'.format(request_id=self.request_id),
240+
status=200,
241+
json=json,
242+
)
243+
244+
res = self.tested.get_insight_message_event(self.request_id)
245+
request = responses.calls[0].request
246+
self.assertEqual('GET', request.method)
247+
self.assertEqual(
248+
request.url,
249+
LineBotApi.DEFAULT_API_ENDPOINT +
250+
'/v2/bot/insight/message/event?requestId={request_id}'.format(request_id=self.request_id)
251+
)
252+
253+
self.assertEqual(res.overview.timestamp, json['overview']['timestamp'])
254+
self.assertEqual(res.overview.unique_media_played_100_percent, json['overview']['uniqueMediaPlayed100Percent'])
255+
self.assertEqual(res.messages[0].seq, json['messages'][0]['seq'])
256+
self.assertEqual(res.messages[0].media_played_50_percent, json['messages'][0]['mediaPlayed50Percent'])
257+
self.assertEqual(res.clicks[0].url, json['clicks'][0]['url'])
258+
self.assertEqual(res.clicks[0].unique_click_of_request, json['clicks'][0]['uniqueClickOfRequest'])
259+
190260

191261
if __name__ == '__main__':
192262
unittest.main()

tests/test_utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@
2121

2222
class TestUtils(unittest.TestCase):
2323
def test_to_snake_case(self):
24-
self.assertEqual(to_snake_case('hogeBar'), 'hoge_bar')
24+
self.assertEqual(to_snake_case('hogeBarFuga'), 'hoge_bar_fuga')
25+
self.assertEqual(to_snake_case('uniqueMediaPlayed100Percent'), 'unique_media_played_100_percent')
26+
self.assertEqual(to_snake_case('festival20Days'), 'festival_20_days')
27+
self.assertEqual(to_snake_case('festival20days'), 'festival_20_days')
2528

2629
def test_to_camel_case(self):
2730
self.assertEqual(to_camel_case('hoge_bar'), 'hogeBar')
31+
self.assertEqual(to_camel_case('unique_media_played_100_percent'), 'uniqueMediaPlayed100Percent')
2832

2933
def test_safe_compare_digest_true(self):
3034
self.assertTrue(safe_compare_digest('/gg9a+LvFevTH1sd7', '/gg9a+LvFevTH1sd7'))

0 commit comments

Comments
 (0)