Skip to content

Commit cca529c

Browse files
okuebe-hase
authored andcommitted
Support OAuth (#174)
* support OAuth * fix typo and refactor * fix typo in README * fix * add test * small change * fix style error * rename * modify pydoc
1 parent 7c52bd7 commit cca529c

File tree

6 files changed

+229
-1
lines changed

6 files changed

+229
-1
lines changed

README.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,29 @@ https://developers.line.biz/en/reference/messaging-api/#issue-link-token
437437
link_token_response = line_bot_api.issue_link_token(<user_id>)
438438
print(link_token_response)
439439
440+
issue\_channel\_token(self, client_id, client_secret, grant_type='client_credentials', timeout=None)
441+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
442+
443+
Issues a short-lived channel access token.
444+
445+
https://developers.line.biz/en/reference/messaging-api/#issue-channel-access-token
446+
447+
.. code:: python
448+
449+
channel_token_response = line_bot_api.issue_channel_token(<client_id>, <client_secret>)
450+
print(access_token_response)
451+
452+
revoke\_channel\_token(self, access_token, timeout=None)
453+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
454+
455+
Revokes a channel access token.
456+
457+
https://developers.line.biz/en/reference/messaging-api/#revoke-channel-access-token
458+
459+
.. code:: python
460+
461+
line_bot_api.revoke_channel_token(<access_token>)
462+
440463
※ Error handling
441464
^^^^^^^^^^^^^^^^
442465

linebot/api.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
from .http_client import HttpClient, RequestsHttpClient
2424
from .models import (
2525
Error, Profile, MemberIds, Content, RichMenuResponse, MessageQuotaResponse,
26-
MessageQuotaConsumptionResponse, MessageDeliveryBroadcastResponse, IssueLinkTokenResponse
26+
MessageQuotaConsumptionResponse, MessageDeliveryBroadcastResponse, IssueLinkTokenResponse,
27+
IssueChannelTokenResponse,
2728
)
2829

2930

@@ -744,7 +745,13 @@ def issue_link_token(self, user_id, timeout=None):
744745
https://developers.line.biz/en/reference/messaging-api/#issue-link-token
745746
746747
:param str user_id: User ID for the LINE account to be linked
748+
:param timeout: (optional) How long to wait for the server
749+
to send data before giving up, as a float,
750+
or a (connect timeout, read timeout) float tuple.
751+
Default is self.http_client.timeout
747752
:type timeout: float | tuple(float, float)
753+
:rtype: :py:class:`linebot.models.responses.IssueLinkTokenResponse`
754+
:return: IssueLinkTokenResponse instance
748755
"""
749756
response = self._post(
750757
'/v2/bot/user/{user_id}/linkToken'.format(
@@ -755,6 +762,55 @@ def issue_link_token(self, user_id, timeout=None):
755762

756763
return IssueLinkTokenResponse.new_from_json_dict(response.json)
757764

765+
def issue_channel_token(self, client_id, client_secret,
766+
grant_type='client_credentials', timeout=None):
767+
"""Issues a short-lived channel access token.
768+
769+
https://developers.line.biz/en/reference/messaging-api/#issue-channel-access-token
770+
771+
:param str client_id: Channel ID.
772+
:param str client_secret: Channel secret.
773+
:param str grant_type: `client_credentials`
774+
:param timeout: (optional) How long to wait for the server
775+
to send data before giving up, as a float,
776+
or a (connect timeout, read timeout) float tuple.
777+
Default is self.http_client.timeout
778+
:type timeout: float | tuple(float, float)
779+
:rtype: :py:class:`linebot.models.responses.IssueChannelTokenResponse`
780+
:return: IssueChannelTokenResponse instance
781+
"""
782+
response = self._post(
783+
'/v2/oauth/accessToken',
784+
data={
785+
'client_id': client_id,
786+
'client_secret': client_secret,
787+
'grant_type': grant_type,
788+
},
789+
headers={'Content-Type': 'application/x-www-form-urlencoded'},
790+
timeout=timeout
791+
)
792+
793+
return IssueChannelTokenResponse.new_from_json_dict(response.json)
794+
795+
def revoke_channel_token(self, access_token, timeout=None):
796+
"""Revokes a channel access token.
797+
798+
https://developers.line.biz/en/reference/messaging-api/#revoke-channel-access-token
799+
800+
:param str access_token: Channel access token.
801+
:param timeout: (optional) How long to wait for the server
802+
to send data before giving up, as a float,
803+
or a (connect timeout, read timeout) float tuple.
804+
Default is self.http_client.timeout
805+
:type timeout: float | tuple(float, float)
806+
"""
807+
self._post(
808+
'/v2/oauth/revoke',
809+
data={'access_token': access_token},
810+
headers={'Content-Type': 'application/x-www-form-urlencoded'},
811+
timeout=timeout
812+
)
813+
758814
def _get(self, path, params=None, headers=None, stream=False, timeout=None):
759815
url = self.endpoint + path
760816

linebot/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
MessageDeliveryBroadcastResponse,
102102
Content as MessageContent, # backward compatibility,
103103
IssueLinkTokenResponse,
104+
IssueChannelTokenResponse,
104105
)
105106
from .rich_menu import ( # noqa
106107
RichMenu,

linebot/models/responses.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,25 @@ def __init__(self, link_token=None, **kwargs):
224224
super(IssueLinkTokenResponse, self).__init__(**kwargs)
225225

226226
self.link_token = link_token
227+
228+
229+
class IssueChannelTokenResponse(Base):
230+
"""IssueAccessTokenResponse.
231+
232+
https://developers.line.biz/en/reference/messaging-api/#issue-channel-access-token
233+
"""
234+
235+
def __init__(self, access_token=None, expires_in=None, token_type=None, **kwargs):
236+
"""__init__ method.
237+
238+
:param str access_token: Short-lived channel access token.
239+
:param int expires_in: Time until channel access token expires in seconds
240+
from time the token is issued.
241+
:param str token_type: Bearer.
242+
:param kwargs:
243+
"""
244+
super(IssueChannelTokenResponse, self).__init__(**kwargs)
245+
246+
self.access_token = access_token
247+
self.expires_in = expires_in
248+
self.token_type = token_type

tests/api/test_issue_channel_token.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
from __future__ import unicode_literals, absolute_import
16+
17+
import sys
18+
import unittest
19+
20+
import responses
21+
22+
from linebot import (
23+
LineBotApi
24+
)
25+
26+
PY3 = sys.version_info[0] == 3
27+
if PY3:
28+
from urllib import parse
29+
else:
30+
import urlparse as parse
31+
32+
33+
class TestLineBotApi(unittest.TestCase):
34+
def setUp(self):
35+
self.tested = LineBotApi('channel_secret')
36+
self.endpoint = LineBotApi.DEFAULT_API_ENDPOINT + '/v2/oauth/accessToken'
37+
self.access_token = "W1TeHCgfH2Liwa....."
38+
self.expires_in = 2592000
39+
self.token_type = "Bearer"
40+
self.client_id = 'client_id'
41+
self.client_secret = 'client_secret'
42+
43+
@responses.activate
44+
def test_issue_line_token(self):
45+
responses.add(
46+
responses.POST,
47+
self.endpoint,
48+
json={
49+
"access_token": self.access_token,
50+
"expires_in": self.expires_in,
51+
"token_type": self.token_type
52+
},
53+
status=200
54+
)
55+
56+
issue_access_token_response = self.tested.issue_channel_token(
57+
self.client_id,
58+
self.client_secret
59+
)
60+
61+
request = responses.calls[0].request
62+
self.assertEqual('POST', request.method)
63+
self.assertEqual(self.endpoint, request.url)
64+
self.assertEqual('application/x-www-form-urlencoded', request.headers['content-type'])
65+
self.assertEqual(self.access_token, issue_access_token_response.access_token)
66+
self.assertEqual(self.expires_in, issue_access_token_response.expires_in)
67+
self.assertEqual(self.token_type, issue_access_token_response.token_type)
68+
69+
encoded_body = parse.parse_qs(request.body)
70+
self.assertEqual('client_credentials', encoded_body['grant_type'][0])
71+
self.assertEqual(self.client_id, encoded_body['client_id'][0])
72+
self.assertEqual(self.client_secret, encoded_body['client_secret'][0])
73+
74+
75+
if __name__ == '__main__':
76+
unittest.main()
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
from __future__ import unicode_literals, absolute_import
16+
17+
import unittest
18+
19+
import responses
20+
21+
from linebot import (
22+
LineBotApi
23+
)
24+
25+
26+
class TestLineBotApi(unittest.TestCase):
27+
def setUp(self):
28+
self.tested = LineBotApi('channel_secret')
29+
self.endpoint = LineBotApi.DEFAULT_API_ENDPOINT + '/v2/oauth/revoke'
30+
self.access_token = "W1TeHCgfH2Liwa....."
31+
32+
@responses.activate
33+
def test_issue_line_token(self):
34+
responses.add(
35+
responses.POST,
36+
self.endpoint,
37+
status=200
38+
)
39+
40+
self.tested.revoke_channel_token(self.access_token)
41+
42+
request = responses.calls[0].request
43+
self.assertEqual('POST', request.method)
44+
self.assertEqual(self.endpoint, request.url)
45+
self.assertEqual('application/x-www-form-urlencoded', request.headers['content-type'])
46+
self.assertEqual('access_token={}'.format(self.access_token), request.body)
47+
48+
49+
if __name__ == '__main__':
50+
unittest.main()

0 commit comments

Comments
 (0)