├── tests ├── __init__.py ├── api │ ├── __init__.py │ ├── test_upload_image.py │ ├── test_request_profile.py │ ├── test_message_product.py │ ├── test_typing_action.py │ ├── test_thread.py │ ├── test_error_handle.py │ ├── test_send_text.py │ ├── test_persistent_menu.py │ ├── test_send_image.py │ ├── test_send_composite.py │ └── test_webhook_handler.py ├── models │ ├── __init__.py │ ├── test_events.py │ ├── test_model_error_handle.py │ ├── test_base.py │ ├── test_payload.py │ └── test_buttons.py ├── utils.py └── test_utils.py ├── spec_helper.rb ├── nta ├── __about__.py ├── __init__.py ├── models │ ├── __init__.py │ ├── responses.py │ ├── base.py │ ├── payload.py │ ├── template.py │ ├── buttons.py │ └── events.py ├── utils.py ├── exceptions.py └── api.py ├── requirements.txt ├── .gitignore ├── PULL_REQUEST_TEMPLATE.md ├── LICENSE.txt ├── example ├── persistent_menu_example.py └── example.py ├── .github └── workflows │ └── coveralls.yml ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'coveralls' 2 | Coveralls.wear! -------------------------------------------------------------------------------- /nta/__about__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Meta data for naver-talk-sdk 3 | """ 4 | 5 | __version__ = '1.0.6' 6 | __author__ = 'WonYoHwang' 7 | __license__ = 'MIT' 8 | 9 | __all__ = ( 10 | '__version__' 11 | ) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2024.7.4 2 | chardet==3.0.4 3 | cookies==2.2.1 4 | funcsigs==1.0.2 5 | idna==3.7 6 | mock==2.0.0 7 | pbr==3.1.1 8 | requests>=2.20.0 9 | responses==0.12.0 10 | six==1.11.0 11 | urllib3>=1.23 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | *.pyc 3 | .idea/ 4 | Dockerfile 5 | setup.* 6 | /dist 7 | /*.egg-info 8 | /docs/_build 9 | Gemfile 10 | 11 | # virtualenv 12 | venv/ 13 | 14 | /example/** 15 | !/example/example.py 16 | !/example/persistent_menu_example.py 17 | -------------------------------------------------------------------------------- /tests/models/test_events.py: -------------------------------------------------------------------------------- 1 | #-*- encoding:utf8 -*- 2 | from __future__ import unicode_literals, absolute_import 3 | 4 | import unittest 5 | 6 | from nta.models import ( 7 | Base, events 8 | ) 9 | 10 | class TestNaverTalkEvent(unittest.TestCase): 11 | def test_peresistent_menu_event(self): 12 | pass -------------------------------------------------------------------------------- /nta/__init__.py: -------------------------------------------------------------------------------- 1 | """navertalk pacakge.""" 2 | 3 | from .__about__ import ( 4 | __version__ 5 | ) 6 | from .api import ( 7 | NaverTalkApi 8 | ) 9 | from .exceptions import ( 10 | NaverTalkApiError, 11 | NaverTalkApiConnectionError, 12 | NaverTalkPaymentError 13 | ) 14 | from .models import template as Template 15 | from .models import buttons as Button 16 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility for unittest 3 | """ 4 | 5 | def find_diff_in_dict(dict1, dict2, path=""): 6 | for key, item in dict1.items(): 7 | if key in dict2: 8 | dict2_item = dict2.get(key) 9 | if item == dict2_item: 10 | pass 11 | else: 12 | print(path + "." + key + ": " + "Is Not Same") 13 | if isinstance(item, dict) and isinstance(dict2_item, dict): 14 | find_diff_in_dict(item, dict2_item, path + "." + key) 15 | else: 16 | print(path + "." + key + ": " + "No Key Match.") -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals, absolute_import 2 | 3 | import unittest 4 | 5 | from nta.utils import to_camel_case, to_snake_case, _byteify, PY3 6 | 7 | 8 | class TestUtils(unittest.TestCase): 9 | def test_to_snake_case(self): 10 | self.assertEqual(to_snake_case('hogeBar'), 'hoge_bar') 11 | 12 | def test_to_camel_case(self): 13 | self.assertEqual(to_camel_case('hoge_bar'), 'hogeBar') 14 | 15 | def test__byteify(self): 16 | if not PY3: 17 | self.assertEqual(_byteify(u'test'), str('test')) 18 | self.assertEqual(_byteify([u'test', u'test2']), [str('test'), str('test2')]) 19 | self.assertEqual(_byteify({u'test_key': u'test_value'}), {str('test_key'): str('test_value')}) 20 | 21 | 22 | if __name__ == '__main__': 23 | unittest.main() -------------------------------------------------------------------------------- /tests/models/test_model_error_handle.py: -------------------------------------------------------------------------------- 1 | #-*- encoding:utf8 -*- 2 | from __future__ import unicode_literals, absolute_import 3 | 4 | import unittest 5 | 6 | from nta.models import ( 7 | GenericPayload, ImageContent, Buttons 8 | ) 9 | 10 | 11 | class TestNaverTalkApi(unittest.TestCase): 12 | 13 | def test_payload_error(self): 14 | with self.assertRaises(ValueError): 15 | GenericPayload({'test_key', 'test_value'}, user='test_user') 16 | 17 | def test_image_content_error(self): 18 | with self.assertRaises(TypeError) as e: 19 | ImageContent() 20 | 21 | def test_button_invalid_type_error(self): 22 | with self.assertRaises(ValueError): 23 | Buttons.convert_shortcut_buttons([{'type': 'PEY', 'value': 'test'}]) 24 | 25 | with self.assertRaises(ValueError): 26 | Buttons.convert_shortcut_buttons(['test_button_text']) 27 | 28 | 29 | if __name__ == '__main__': 30 | unittest.main() -------------------------------------------------------------------------------- /nta/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import ( 2 | Base, 3 | ) 4 | from .responses import ( 5 | NaverTalkResponse, 6 | NaverTalkImageResponse, 7 | ) 8 | from .payload import ( 9 | Payload, 10 | ProfilePayload, 11 | GenericPayload, 12 | ThreadPayload, 13 | ActionPayload, 14 | PersistentMenuPayload, 15 | ProductMessage, 16 | ) 17 | from .buttons import ( 18 | ButtonLink, 19 | ButtonText, 20 | ButtonOption, 21 | Buttons, 22 | ButtonPay, 23 | ButtonNested, 24 | ButtonTime, 25 | ButtonCalendar, 26 | ButtonTimeInterval 27 | ) 28 | from .template import ( 29 | TextContent, 30 | ImageContent, 31 | CompositeContent, 32 | Composite, 33 | ElementList, 34 | ElementData, 35 | QuickReply, 36 | PaymentInfo, 37 | ProductItem 38 | ) 39 | from .events import ( 40 | OpenEvent, 41 | SendEvent, 42 | EchoEvent, 43 | LeaveEvent, 44 | ProfileEvent, 45 | PayCompleteEvent, 46 | PayConfirmEvent, 47 | ProfileEvent, 48 | FriendEvent, 49 | HandOverEvent, 50 | ) -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) <2017> 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /example/persistent_menu_example.py: -------------------------------------------------------------------------------- 1 | #-*- encoding:utf-8 -*- 2 | """ 3 | In Persistent Menu, 4 | Option Button is not allowed. 5 | Sad.. 6 | 7 | How to register persistent menu: 8 | python persistent_menu_example.py 9 | 10 | """ 11 | import os 12 | 13 | from nta import NaverTalkApi, Button 14 | 15 | NAVER_TALK_ACCESS_TOKEN = os.environ['naver_talk_access_token'] 16 | ntalk = NaverTalkApi(NAVER_TALK_ACCESS_TOKEN) 17 | 18 | def my_callback(res, payload): 19 | #callback function for showing result of send persistent menu payload 20 | print(res) 21 | 22 | ntalk.persistent_menu( 23 | menus=[ 24 | Button.ButtonText( 25 | '고정 메뉴 테스트', 26 | 'PersistentMenu' 27 | ), 28 | Button.ButtonLink( 29 | 'Link to NTA', 30 | 'https://github.com/HwangWonYo/naver_talk_sdk' 31 | ), 32 | Button.ButtonNested( 33 | title='버튼을 품은 버튼 이라는데?', 34 | menus=[ 35 | Button.ButtonText('아무 의미 없는 버튼'), 36 | Button.ButtonText('카드뷰 보기', 'CardView'), 37 | ] 38 | ) 39 | ], 40 | callback=my_callback 41 | ) -------------------------------------------------------------------------------- /.github/workflows/coveralls.yml: -------------------------------------------------------------------------------- 1 | name: Coveralls Coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - '**/*.py' 9 | pull_request: 10 | branches: 11 | - master 12 | paths: 13 | - '**/*.py' 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | 19 | strategy: 20 | matrix: 21 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 22 | 23 | steps: 24 | - name: Check out the repository 25 | uses: actions/checkout@v3 26 | 27 | - name: Set up Python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v4 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | 32 | - name: Install dependencies 33 | run: | 34 | pip install mock 35 | pip install coveralls 36 | pip install responses 37 | 38 | - name: Run tests 39 | run: | 40 | coverage run -m unittest discover 41 | coverage xml 42 | 43 | - name: Upload coverage to Coveralls 44 | if: success() 45 | run: coveralls 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 49 | -------------------------------------------------------------------------------- /tests/api/test_upload_image.py: -------------------------------------------------------------------------------- 1 | #-*- encoding:utf-8 -*- 2 | import unittest 3 | import responses 4 | try: 5 | from unittest import mock 6 | except: 7 | import mock 8 | 9 | 10 | from nta import ( 11 | NaverTalkApi 12 | ) 13 | 14 | 15 | class TestNaverTalkAPI(unittest.TestCase): 16 | def setUp(self): 17 | self.tested = NaverTalkApi('test_naver_talk_access_token') 18 | 19 | @responses.activate 20 | def test_upload_image(self): 21 | responses.add( 22 | responses.POST, 23 | NaverTalkApi.DEFAULT_API_ENDPOINT, 24 | json={ 25 | "success": True, 26 | "resultCode": "00", 27 | "imageId": "test-1234-image-id" 28 | }, 29 | status=200 30 | ) 31 | 32 | counter = mock.MagicMock() 33 | def test_callback(res, payload): 34 | self.assertEqual(res.result_code, "00") 35 | self.assertEqual(res.success, True) 36 | self.assertEqual(res.image_id, "test-1234-image-id") 37 | counter() 38 | 39 | self.tested.upload_image( 40 | 'test_image_url.jpg', 41 | callback=test_callback 42 | ) 43 | self.assertEqual(counter.call_count, 1) -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute to NaverTalk SDK for Python project 2 | 3 | Thank you so much for taking your time to contribute. Let's make a stress-less world together! 4 | NaverTalk SDK for Python is not very different from any other opensource projects. 5 | It will be amazing if you could help us by doing any of the following: 6 | 7 | - File an issue in [the issue tracker](https://github.com/HwangWonYo/naver_talk_sdk/issues) to report bugs and propose new features and 8 | improvements. 9 | - Ask a question using [the issue tracker](https://github.com/HwangWonYo/naver_talk_sdk/issues). 10 | - Contribute your work by sending [a pull request](https://github.com/HwangWonYo/naver_talk_sdk/pulls). 11 | 12 | ## Development Guide 13 | 14 | 1. register an **issue** 15 | 16 | 2. **synchronize master branch and create issue-branch** 17 | 18 | ``` 19 | git fetch 20 | git checkout -b origin/master 21 | ``` 22 | 23 | - prefix of branch name: **issue** 24 | - ex) issue_modify_button_template 25 | 26 | 3. **develop code** 27 | 28 | 4. **rebase** 29 | 30 | ``` 31 | git rebase origin/develop 32 | git push origin 33 | ``` 34 | 35 | 5. **pull-request** 36 | 37 | - base: **master** <= compare: < issue-branch > 38 | -------------------------------------------------------------------------------- /nta/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | import sys 4 | 5 | LOGGER = logging.getLogger('nta') 6 | 7 | PY3 = sys.version_info[0] == 3 8 | 9 | 10 | def to_snake_case(text): 11 | """ 12 | Convert text to snake case. 13 | 14 | >>> to_snake_case('exampleCode') 15 | "example_code" 16 | 17 | Args: 18 | - text: str 19 | 20 | Return: snake case of str 21 | """ 22 | s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', text) 23 | return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() 24 | 25 | 26 | def to_camel_case(text): 27 | """Convert to camel case. 28 | 29 | >>> to_camel_case('example_code') 30 | 'exampleCode' 31 | 32 | Args: 33 | - text: str 34 | 35 | Retrun: camel case of str 36 | """ 37 | split = text.split('_') 38 | return split[0] + "".join(x.title() for x in split[1:]) 39 | 40 | 41 | def _byteify(input): 42 | """ 43 | for python2 encoding utf-8 error 44 | encode UTF-8 45 | 46 | Args: 47 | - input: unicode string 48 | 49 | Return: utf-8 encoded byte 50 | """ 51 | if isinstance(input, dict): 52 | return {_byteify(key): _byteify(value) 53 | for key, value in input.items()} 54 | elif isinstance(input, list): 55 | return [_byteify(element) for element in input] 56 | elif isinstance(input, unicode): 57 | return input.encode('utf-8') 58 | else: 59 | return input -------------------------------------------------------------------------------- /tests/api/test_request_profile.py: -------------------------------------------------------------------------------- 1 | #-*- encoding:utf-8 -*- 2 | import unittest 3 | import responses 4 | try: 5 | from unittest import mock 6 | except: 7 | import mock 8 | 9 | 10 | from nta import ( 11 | NaverTalkApi 12 | ) 13 | 14 | 15 | class TestNaverTalkAPI(unittest.TestCase): 16 | def setUp(self): 17 | self.tested = NaverTalkApi('test_naver_talk_access_token') 18 | 19 | @responses.activate 20 | def test_request_profile(self): 21 | responses.add( 22 | responses.POST, 23 | NaverTalkApi.DEFAULT_API_ENDPOINT, 24 | json={ 25 | "success": True, 26 | "resultCode": "00" 27 | }, 28 | status=200 29 | ) 30 | 31 | counter = mock.MagicMock() 32 | def test_callback(res, payload): 33 | self.assertEqual(res.result_code, "00") 34 | self.assertEqual(res.success, True) 35 | self.assertEqual( 36 | payload.options, 37 | { 38 | "field": "nickname", 39 | "agreements": ["cellphone", "address"] 40 | } 41 | ) 42 | counter() 43 | 44 | self.tested.request_profile( 45 | 'test_id', 46 | 'nickname', 47 | agreements=['cellphone', 'address'], 48 | callback=test_callback 49 | ) 50 | self.assertEqual(counter.call_count, 1) -------------------------------------------------------------------------------- /nta/models/responses.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | 3 | class Response(Base): 4 | pass 5 | 6 | 7 | class NaverTalkResponse(Response): 8 | """NaverTalkResponse 9 | For Parsing response from navertalk after sending a message request.""" 10 | def __init__(self, success, result_code, result_message=None, **kwargs): 11 | """__init__ method. 12 | 13 | Args: 14 | -success: True or False 15 | -result_code: str result code can see more info in navertalk github page. 16 | -result_message: str result message when request failed. 17 | """ 18 | super(NaverTalkResponse, self).__init__(**kwargs) 19 | 20 | self.success = success 21 | self.result_code = result_code 22 | self.result_message = result_message 23 | 24 | 25 | class NaverTalkImageResponse(Response): 26 | """NaverTalkImageResponse 27 | For Parsing response from navertalk after sending an image upload request""" 28 | def __init__(self, success, result_code, image_id=None, result_message=None, **kwargs): 29 | """ __init__ method. 30 | 31 | Args: 32 | -success: True or False 33 | -result_code: str result code can see more info in navertalk github page. 34 | -image_id: str image_id when request success. 35 | -result_message: str result message when request failed. 36 | """ 37 | super(NaverTalkImageResponse, self).__init__(**kwargs) 38 | 39 | self.success = success 40 | self.result_code = result_code 41 | self.image_id = image_id 42 | self.result_message = result_message -------------------------------------------------------------------------------- /tests/api/test_message_product.py: -------------------------------------------------------------------------------- 1 | # -*- encoding:utf-8 -*- 2 | import unittest 3 | import responses 4 | 5 | try: 6 | from unittest import mock 7 | except: 8 | import mock 9 | 10 | from nta import ( 11 | NaverTalkApi, 12 | ) 13 | 14 | 15 | class TestNaverTalkAPI(unittest.TestCase): 16 | def setUp(self): 17 | self.tested = NaverTalkApi('test_naver_talk_access_token') 18 | 19 | @responses.activate 20 | def test_send_text(self): 21 | responses.add( 22 | responses.POST, 23 | NaverTalkApi.DEFAULT_API_ENDPOINT, 24 | json={ 25 | "success": True, 26 | "resultCode": "00" 27 | }, 28 | status=200 29 | ) 30 | 31 | counter = mock.MagicMock() 32 | 33 | def test_callback(res, payload): 34 | self.assertEqual(res.result_code, "00") 35 | self.assertEqual(res.success, True) 36 | self.assertEqual( 37 | payload, 38 | { 39 | "event": "product", 40 | "options": { 41 | "ids": [ 42 | 1002324883, 43 | 1002793763, 44 | 2265658394, 45 | 2299323502 46 | ], 47 | "displayType": "list" 48 | }, 49 | "user": "test_user_id" 50 | } 51 | ) 52 | counter() 53 | 54 | self.tested.product_message( 55 | 'test_user_id', 56 | ids=[ 57 | 1002324883, 58 | 1002793763, 59 | 2265658394, 60 | 2299323502 61 | ], 62 | display_type='list', 63 | callback=test_callback 64 | ) 65 | self.assertEqual(counter.call_count, 1) -------------------------------------------------------------------------------- /tests/api/test_typing_action.py: -------------------------------------------------------------------------------- 1 | #-*- encoding:utf-8 -*- 2 | import unittest 3 | import responses 4 | try: 5 | from unittest import mock 6 | except: 7 | import mock 8 | 9 | 10 | from nta import ( 11 | NaverTalkApi 12 | ) 13 | 14 | 15 | class TestNaverTalkActionEvent(unittest.TestCase): 16 | def setUp(self): 17 | self.tested = NaverTalkApi('test_naver_talk_access_token') 18 | 19 | @responses.activate 20 | def test_typing_on(self): 21 | responses.add( 22 | responses.POST, 23 | NaverTalkApi.DEFAULT_API_ENDPOINT, 24 | json={ 25 | "success": True, 26 | "resultCode": "00", 27 | }, 28 | status=200 29 | ) 30 | 31 | counter = mock.MagicMock() 32 | def test_callback(res, payload): 33 | self.assertEqual(res.result_code, "00") 34 | self.assertEqual(res.success, True) 35 | self.assertEqual(payload.user, 'test_user_id') 36 | self.assertEqual(payload.event, 'action') 37 | self.assertEqual(payload.options, {"action": "typingOn"}) 38 | counter() 39 | 40 | self.tested.typing_on('test_user_id', callback=test_callback) 41 | self.assertEqual(counter.call_count, 1) 42 | 43 | @responses.activate 44 | def test_typing_off(self): 45 | responses.add( 46 | responses.POST, 47 | NaverTalkApi.DEFAULT_API_ENDPOINT, 48 | json={ 49 | "success": True, 50 | "resultCode": "00", 51 | }, 52 | status=200 53 | ) 54 | 55 | counter = mock.MagicMock() 56 | def test_callback(res, payload): 57 | self.assertEqual(res.result_code, "00") 58 | self.assertEqual(res.success, True) 59 | self.assertEqual(payload.user, 'test_user_id') 60 | self.assertEqual(payload.event, 'action') 61 | self.assertEqual(payload.options, {"action": "typingOff"}) 62 | counter() 63 | 64 | self.tested.typing_off('test_user_id', callback=test_callback) 65 | self.assertEqual(counter.call_count, 1) -------------------------------------------------------------------------------- /nta/exceptions.py: -------------------------------------------------------------------------------- 1 | ''' 2 | navertalk.exceptions module 3 | ''' 4 | from __future__ import unicode_literals 5 | 6 | class BaseError(Exception): 7 | """Base Exception class""" 8 | 9 | def __init__(self, message='-'): 10 | """__init__ method. 11 | 12 | Args: 13 | - message: str readable message 14 | """ 15 | self.message = message 16 | 17 | def __repr__(self): 18 | """ 19 | repr 20 | """ 21 | return str(self) 22 | 23 | def __str__(self): 24 | """ 25 | str 26 | """ 27 | return '<%s [%s]>' % (self.__class__.__name__, self.message) 28 | 29 | 30 | class NaverTalkApiError(BaseError): 31 | """ 32 | When Naver Talk failed to build message, NaverTalkApiError raised 33 | """ 34 | 35 | def __init__(self, api_response): 36 | """__init__ method. 37 | 38 | Args: 39 | - api_response: Response class object 40 | """ 41 | super(NaverTalkApiError, self).__init__(api_response.result_message) 42 | 43 | self._status_code = 200 44 | self.result_code = api_response.result_code 45 | 46 | @property 47 | def status_code(self): 48 | """ 49 | status_code always return 200 50 | """ 51 | return self._status_code 52 | 53 | 54 | class NaverTalkApiConnectionError(BaseError): 55 | """ 56 | When Naver Talk Api server connection failed, NaverTalkApiConnectionError raised 57 | """ 58 | 59 | def __init__(self, response): 60 | """___init__ method. 61 | 62 | Args: 63 | - response: models.response 64 | """ 65 | super(NaverTalkApiConnectionError, self).__init__(response.text) 66 | 67 | self.status_code = response.status_code 68 | self.response = response 69 | 70 | 71 | class NaverTalkPaymentError(BaseError): 72 | """ 73 | Exception for handling Payment to fail easily 74 | for example when pay event occurred for some stuff and the stuff running out, 75 | raise NaverTalkPaymentError and catch exception to make another action 76 | """ 77 | def __init__(self, message='-'): 78 | """ __init__ method. 79 | 80 | Args: 81 | - message: 82 | """ 83 | super(NaverTalkPaymentError, self).__init__(message) -------------------------------------------------------------------------------- /tests/api/test_thread.py: -------------------------------------------------------------------------------- 1 | #-*- encoding:utf-8 -*- 2 | import json 3 | import unittest 4 | import responses 5 | try: 6 | from unittest import mock 7 | except: 8 | import mock 9 | 10 | 11 | from nta import ( 12 | NaverTalkApi, 13 | ) 14 | from nta.models import( 15 | TextContent, QuickReply, ButtonText, ButtonLink 16 | ) 17 | 18 | 19 | class TestNaverTalkAPI(unittest.TestCase): 20 | def setUp(self): 21 | self.tested = NaverTalkApi('test_naver_talk_access_token') 22 | 23 | @responses.activate 24 | def test_thread_taking(self): 25 | responses.add( 26 | responses.POST, 27 | NaverTalkApi.DEFAULT_API_ENDPOINT, 28 | json={ 29 | "success": True, 30 | "resultCode": "00" 31 | }, 32 | status=200 33 | ) 34 | 35 | counter = mock.MagicMock() 36 | def test_callback(res, payload): 37 | self.assertEqual(res.result_code, "00") 38 | self.assertEqual(res.success, True) 39 | self.assertEqual( 40 | payload, 41 | { 42 | 'event': 'handover', 43 | 'user': 'test_user_id', 44 | 'options': { 45 | "control": "takeThread", 46 | "metadata": "" 47 | } 48 | } 49 | ) 50 | counter() 51 | 52 | self.tested.take_thread( 53 | user_id='test_user_id', 54 | callback=test_callback 55 | ) 56 | self.assertEqual(counter.call_count, 1) 57 | 58 | @responses.activate 59 | def test_thread_passing(self): 60 | responses.add( 61 | responses.POST, 62 | NaverTalkApi.DEFAULT_API_ENDPOINT, 63 | json={ 64 | "success": True, 65 | "resultCode": "00" 66 | }, 67 | status=200 68 | ) 69 | 70 | counter = mock.MagicMock() 71 | 72 | def test_callback(res, payload): 73 | self.assertEqual(res.result_code, "00") 74 | self.assertEqual(res.success, True) 75 | self.assertEqual( 76 | payload, 77 | { 78 | 'event': 'handover', 79 | 'user': 'test_user_id', 80 | 'options': { 81 | "control": "passThread", 82 | "targetId": 1 83 | } 84 | } 85 | ) 86 | counter() 87 | 88 | self.tested.pass_thread( 89 | user_id='test_user_id', 90 | callback=test_callback 91 | ) 92 | self.assertEqual(counter.call_count, 1) -------------------------------------------------------------------------------- /nta/models/base.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from .. import utils 4 | 5 | 6 | class Base(object): 7 | """Base class of model. 8 | Suitable for JSON base data. 9 | """ 10 | 11 | def __init__(self, **kwargs): 12 | """__init__ method.""" 13 | pass 14 | 15 | def __str__(self): 16 | """__str__ method.""" 17 | return self.as_json_string() 18 | 19 | def __repr__(self): 20 | """__repr__ method. """ 21 | return str(self) 22 | 23 | def __eq__(self, other): 24 | """__eq__ method.""" 25 | if isinstance(other, dict): 26 | return other == self.as_json_dict() 27 | elif isinstance(other, Base): 28 | return self.as_json_dict() == other.as_json_dict() 29 | return other == str(self) 30 | 31 | def __ne__(self, other): 32 | """__ne__ method.""" 33 | return not self.__eq__(other) 34 | 35 | def as_json_string(self): 36 | """ 37 | Return JSON string from this object 38 | """ 39 | return json.dumps(self.as_json_dict(), sort_keys=True) 40 | 41 | def as_json_dict(self): 42 | """ 43 | Return dictionary from this object. 44 | """ 45 | return self.convert_dict_to_camel_case(self.__dict__) 46 | 47 | @classmethod 48 | def convert_dict_to_camel_case(cls, d): 49 | data = {} 50 | for key, sub_obj in d.items(): 51 | camel_key = utils.to_camel_case(key) 52 | if isinstance(sub_obj, (list, tuple, set)): 53 | data[camel_key] = list() 54 | for obj in sub_obj: 55 | if hasattr(obj, 'as_json_dict'): 56 | data[camel_key].append(obj.as_json_dict()) 57 | elif isinstance(obj, dict): 58 | data[camel_key].append(cls.convert_dict_to_camel_case(obj)) 59 | else: 60 | data[camel_key].append(obj) 61 | 62 | elif isinstance(sub_obj, dict): 63 | data[camel_key] = cls.convert_dict_to_camel_case(sub_obj) 64 | 65 | elif hasattr(sub_obj, 'as_json_dict'): 66 | data[camel_key] = sub_obj.as_json_dict() 67 | 68 | else: 69 | data[camel_key] = sub_obj 70 | 71 | return data 72 | 73 | @classmethod 74 | def new_from_json_dict(cls, data): 75 | """ 76 | Create a new instance from a dict 77 | """ 78 | new_data = cls.dict_to_snake_case(data) 79 | 80 | return cls(**new_data) 81 | 82 | @classmethod 83 | def dict_to_snake_case(cls, data): 84 | """ 85 | Convert dict key into snake_case 86 | """ 87 | if isinstance(data, dict): 88 | new_data = {utils.to_snake_case(key): cls.dict_to_snake_case(value) 89 | for key, value in data.items()} 90 | return new_data 91 | return data -------------------------------------------------------------------------------- /tests/models/test_base.py: -------------------------------------------------------------------------------- 1 | #-*- encoding:utf8 -*- 2 | from __future__ import unicode_literals, absolute_import 3 | 4 | import unittest 5 | 6 | from nta.models import ( 7 | Base, events 8 | ) 9 | from nta import Template 10 | 11 | 12 | class TestBaseTemplate(unittest.TestCase): 13 | 14 | def test_convert_to_camel_case(self): 15 | camel_case_dict = Base.convert_dict_to_camel_case( 16 | { 17 | "event": "send", 18 | "user": "al-2eGuGr5WQOnco1_V-FQ", 19 | "text_content": { 20 | "text": "hello world", 21 | "input_type": "typing" 22 | } 23 | } 24 | ) 25 | self.assertEqual( 26 | camel_case_dict, 27 | { 28 | "event": "send", 29 | "user": "al-2eGuGr5WQOnco1_V-FQ", 30 | "textContent": { 31 | "text": "hello world", 32 | "inputType": "typing" 33 | } 34 | } 35 | ) 36 | camel_case_dict = Base.convert_dict_to_camel_case( 37 | { 38 | "event": "send", 39 | "user": "al-2eGuGr5WQOnco1_V-FQ", 40 | "text_content": Template.TextContent('Test_text', 'Test_Code') 41 | } 42 | ) 43 | self.assertEqual( 44 | camel_case_dict, 45 | { 46 | "event": "send", 47 | "user": "al-2eGuGr5WQOnco1_V-FQ", 48 | "textContent": { 49 | "text": "Test_text", 50 | "code": "Test_Code", 51 | "inputType": None 52 | } 53 | } 54 | ) 55 | 56 | def test_dict_to_snake_case(self): 57 | snake_case_dict = Base.dict_to_snake_case( 58 | { 59 | "camelCaseKey": { 60 | "insideCamelCaseDict":{ 61 | "againInsideCamelCaseDict": "valueDoNotChangeIntoSnakeCase" 62 | } 63 | } 64 | } 65 | ) 66 | self.assertEqual( 67 | snake_case_dict, 68 | { 69 | "camel_case_key":{ 70 | "inside_camel_case_dict": { 71 | "again_inside_camel_case_dict": "valueDoNotChangeIntoSnakeCase" 72 | } 73 | } 74 | } 75 | ) 76 | 77 | def test_new_from_json_dict(self): 78 | new_event = events.LeaveEvent.new_from_json_dict( 79 | { 80 | "event": "leave", 81 | "user": "al-2eGuGr5WQOnco1_V-FQ" 82 | } 83 | 84 | ) 85 | self.assertTrue(isinstance(new_event, events.LeaveEvent)) 86 | 87 | def test_others(self): 88 | new_event = events.LeaveEvent.new_from_json_dict( 89 | { 90 | "event": "leave", 91 | "user": "al-2eGuGr5WQOnco1_V-FQ" 92 | } 93 | 94 | ) 95 | self.assertEqual(str(new_event), new_event.as_json_string()) 96 | self.assertEqual(str(new_event), repr(new_event)) 97 | self.assertTrue(new_event == new_event.as_json_dict()) 98 | self.assertTrue(new_event == str(new_event)) 99 | self.assertFalse(new_event != new_event) 100 | 101 | 102 | 103 | 104 | if __name__ == '__main__': 105 | unittest.main() -------------------------------------------------------------------------------- /tests/models/test_payload.py: -------------------------------------------------------------------------------- 1 | #-*- encoding:utf8 -*- 2 | from __future__ import unicode_literals, absolute_import 3 | 4 | import unittest 5 | 6 | from nta.models.payload import * 7 | from nta import Button 8 | 9 | class TestNaverTalkPayload(unittest.TestCase): 10 | def test_persistent_menu(self): 11 | target = { 12 | "event":"persistentMenu", 13 | "menuContent" : [{ 14 | "menus": 15 | [{ 16 | "type":"TEXT", 17 | "data":{ 18 | "title":"챗봇 안내", 19 | "code":"CHATBOT_GUIDE" 20 | } 21 | },{ 22 | "type":"LINK", 23 | "data":{ 24 | "title":"이벤트 페이지", 25 | "url":"http://your-pc-url.com/event", 26 | "mobileUrl":"http://your-mobile-url.com/event" 27 | } 28 | },{ 29 | "type":"LINK", 30 | "data":{ 31 | "title":"전화하기", 32 | "url":"tel:021234567", 33 | "mobileUrl": None 34 | } 35 | },{ 36 | "type":"NESTED", 37 | "data":{ 38 | "title":"공지사항", 39 | "menus": 40 | [{ 41 | "type":"LINK", 42 | "data":{ 43 | "title":"교환/환불 안내", 44 | "url":"http://your-pc-url.com/guide", 45 | "mobileUrl":"http://your-mobile-url.com/guide" 46 | } 47 | }] 48 | } 49 | }] 50 | }] 51 | } 52 | payload = PersistentMenuPayload( 53 | menus=[ 54 | Button.ButtonText(title='챗봇 안내', code='CHATBOT_GUIDE'), 55 | Button.ButtonLink( 56 | title='이벤트 페이지', 57 | url='http://your-pc-url.com/event', 58 | mobile_url='http://your-mobile-url.com/event' 59 | ), 60 | Button.ButtonLink( 61 | title='전화하기', 62 | url='tel:021234567' 63 | ), 64 | Button.ButtonNested( 65 | title='공지사항', 66 | menus=[ 67 | Button.ButtonLink( 68 | title="교환/환불 안내", 69 | url="http://your-pc-url.com/guide", 70 | mobile_url="http://your-mobile-url.com/guide" 71 | ) 72 | ] 73 | ) 74 | ] 75 | ) 76 | self.assertEqual(target, payload) 77 | 78 | def test_persistent_menu_with_None(self): 79 | payload = PersistentMenuPayload() 80 | self.assertEqual( 81 | payload, 82 | { 83 | "event":"persistentMenu", 84 | "menuContent" : [] 85 | } 86 | ) 87 | 88 | def test_product_message(self): 89 | payload = ProductMessage( 90 | user='test_user_id', 91 | ids=[ 92 | 1,2,3 93 | ], 94 | display_type='list' 95 | ) 96 | self.assertEqual( 97 | payload, 98 | { 99 | 'event': 'product', 100 | 'user': 'test_user_id', 101 | 'options': { 102 | 'ids':[1, 2, 3], 103 | 'displayType': 'list' 104 | } 105 | } 106 | ) -------------------------------------------------------------------------------- /tests/api/test_error_handle.py: -------------------------------------------------------------------------------- 1 | #-*- encoding:utf8 -*- 2 | from __future__ import unicode_literals, absolute_import 3 | 4 | import json 5 | import unittest 6 | import responses 7 | 8 | 9 | from nta import ( 10 | NaverTalkApi 11 | ) 12 | from nta.exceptions import ( 13 | NaverTalkApiError, 14 | NaverTalkApiConnectionError, 15 | NaverTalkPaymentError 16 | ) 17 | 18 | 19 | class TestNaverTalkApi(unittest.TestCase): 20 | def setUp(self): 21 | self.tested = NaverTalkApi('test_naver_talk_access_token') 22 | 23 | @responses.activate 24 | def test_connection_error_handle(self): 25 | responses.add( 26 | responses.POST, 27 | NaverTalkApi.DEFAULT_API_ENDPOINT, 28 | json={ 29 | "message": "Internal Server Error" 30 | }, 31 | status=500 32 | ) 33 | 34 | try: 35 | self.tested.send('test_id', 'test message') 36 | except NaverTalkApiConnectionError as e: 37 | self.assertEqual(e.status_code, 500) 38 | self.assertEqual(e.message, '{"message": "Internal Server Error"}') 39 | 40 | 41 | @responses.activate 42 | def test_error_with_detail_handle(self): 43 | responses.add( 44 | responses.POST, 45 | NaverTalkApi.DEFAULT_API_ENDPOINT, 46 | json={ 47 | 'success': False, 48 | 'resultCode': "02", 49 | 'resultMessage': "request json 문자열 파싱 에러" 50 | }, 51 | status=200 52 | ) 53 | 54 | try: 55 | self.tested.send('test_id', 'test message') 56 | except NaverTalkApiError as e: 57 | self.assertEqual(e.status_code, 200) 58 | self.assertEqual(e.result_code, "02") 59 | self.assertEqual(e.message, 'request json 문자열 파싱 에러') 60 | 61 | @responses.activate 62 | def test_error_handle_get_user_profile(self): 63 | responses.add( 64 | responses.POST, 65 | NaverTalkApi.DEFAULT_API_ENDPOINT, 66 | json={ 67 | 'success': False, 68 | 'resultCode': "02", 69 | 'resultMessage': "request json 문자열 파싱 에러" 70 | }, 71 | status=200 72 | ) 73 | 74 | try: 75 | self.tested.request_profile('test_id', 'nickname') 76 | except NaverTalkApiError as e: 77 | self.assertEqual(e.status_code, 200) 78 | self.assertEqual(e.result_code, "02") 79 | self.assertEqual(e.message, 'request json 문자열 파싱 에러') 80 | self.assertEqual("%s" % e, "") 81 | 82 | @responses.activate 83 | def test_error_handle_upload_image_url(self): 84 | responses.add( 85 | responses.POST, 86 | NaverTalkApi.DEFAULT_API_ENDPOINT, 87 | json={ 88 | 'success': False, 89 | 'resultCode': "IMG-99", 90 | 'resultMessage': "이미지 업로드 중 에러" 91 | }, 92 | status=200 93 | ) 94 | 95 | try: 96 | self.tested.upload_image('https://example.com/test.jpg') 97 | except NaverTalkApiError as e: 98 | self.assertEqual(e.status_code, 200) 99 | self.assertEqual(e.result_code, "IMG-99") 100 | self.assertEqual(e.message, "이미지 업로드 중 에러") 101 | self.assertEqual("%s" % e, "") 102 | 103 | def test_callback_error(self): 104 | with self.assertRaises(ValueError): 105 | @self.tested.callback('Hello') 106 | def callback_test(event): 107 | pass 108 | 109 | def test_naver_pay(self): 110 | req = { 111 | "event": "pay_complete", 112 | "user": "al-2eGuGr5WQOnco1_V-FQ", 113 | "options": { 114 | "paymentResult": { 115 | "code" : "Success", 116 | "paymentId" : "20170811D3adfaasLL", 117 | "merchantPayKey" : "bot-custom-pay-key-1234", 118 | "merchantUserKey" : "al-2eGuGr5WQOnco1_V-FQ", 119 | } 120 | } 121 | } 122 | @self.tested.handle_pay_complete 123 | def pay_complete_fail_error(event): 124 | raise NaverTalkPaymentError('재고 없음') 125 | 126 | try: 127 | self.tested.webhook_handler(json.dumps(req)) 128 | except NaverTalkPaymentError as e: 129 | self.assertEqual(e.message, "재고 없음") 130 | self.assertEqual("%s" % e, "") 131 | 132 | 133 | if __name__ == '__main__': 134 | unittest.main() -------------------------------------------------------------------------------- /nta/models/payload.py: -------------------------------------------------------------------------------- 1 | from .template import * 2 | from .base import Base 3 | 4 | 5 | class Payload(Base): 6 | """ 7 | Base class of payload 8 | """ 9 | def __init__(self, user, **kwargs): 10 | """ __init__ method. 11 | 12 | Args: 13 | - user: user_id 14 | """ 15 | super(Payload, self).__init__(**kwargs) 16 | 17 | self.user = user 18 | 19 | 20 | class GenericPayload(Payload): 21 | """ 22 | General Payload 23 | For Send a message to users. 24 | """ 25 | def __init__(self, message, quick_reply=None, notification=False, read_by_send=False, **kwargs): 26 | """__init__ method. 27 | 28 | Args: 29 | - message: str or Template.TextContent 30 | - quick_reply: list of buttons or Template.QuickReply 31 | - notification: boolean 32 | - readBySend: boolean 33 | """ 34 | super(GenericPayload, self).__init__(**kwargs) 35 | 36 | self.event = 'send' 37 | self.options = { 38 | "notification": notification, 39 | "readBySend": read_by_send 40 | } 41 | if isinstance(message, str): 42 | message = TextContent(message) 43 | if quick_reply: 44 | if isinstance(quick_reply, list): 45 | quick_reply = QuickReply(quick_reply) 46 | message.quick_reply = quick_reply 47 | if isinstance(message, TextContent): 48 | self.text_content = message 49 | elif isinstance(message, ImageContent): 50 | self.image_content = message 51 | elif isinstance(message, CompositeContent): 52 | self.compositeContent = message 53 | else: 54 | raise ValueError("message type must be str or textContent or imageContent or compositeContent type !") 55 | 56 | 57 | class ProfilePayload(Payload): 58 | """ 59 | Porfile Payload 60 | for request user's profile 61 | """ 62 | def __init__(self, field, agreements=None, **kwargs): 63 | """ __init__ method. 64 | 65 | Args: 66 | - field: user profile information to get. 67 | - agreemetns: user profile information to get agreement. 68 | """ 69 | super(ProfilePayload, self).__init__(**kwargs) 70 | 71 | self.event = 'profile' 72 | self.options = { 73 | 'field': field, 74 | 'agreements': agreements 75 | } 76 | 77 | 78 | 79 | class ImageUploadPayload(Payload): 80 | """ 81 | ImageUpload Payload 82 | for Upload image 83 | """ 84 | def __init__(self, image_url, **kwargs): 85 | """__init__ method. 86 | 87 | Args: 88 | - image_url: full image url to convert 89 | """ 90 | 91 | self.image_url = image_url 92 | 93 | class ThreadPayload(Payload): 94 | """ 95 | Thread Payload 96 | """ 97 | def __init__(self, control, **kwargs): 98 | """__init__ method 99 | 100 | Args: 101 | - control: "takeThread" or "passThread" 102 | """ 103 | super(ThreadPayload, self).__init__(**kwargs) 104 | self.event = 'handover' 105 | self.options = { 106 | 'control': control, 107 | } 108 | if control == 'takeThread': 109 | self.options['metadata'] = "" 110 | if control == 'passThread': 111 | self.options['target_id'] = 1 112 | 113 | class ActionPayload(Payload): 114 | """ 115 | Action Payload for typing_on and typing_off 116 | """ 117 | def __init__(self, action, **kwargs): 118 | """__init__ method. 119 | 120 | Args: 121 | - action: "typingOn" or "typingOff" 122 | """ 123 | super(ActionPayload, self).__init__(**kwargs) 124 | 125 | self.event = 'action' 126 | self.options = { 127 | "action": action 128 | } 129 | 130 | 131 | class PersistentMenuPayload(Base): 132 | def __init__(self, menus=None, **kwargs): 133 | super(PersistentMenuPayload, self).__init__(**kwargs) 134 | 135 | self.event = 'persistentMenu' 136 | self.menu_content = [] 137 | if menus is not None: 138 | self.menu_content = [Menus(menus=menus)] 139 | 140 | 141 | class ProductMessage(Payload): 142 | """ 143 | Prduct Payload for prduct message 144 | """ 145 | def __init__(self, ids, display_type='single', **kwargs): 146 | """__init__ method. 147 | 148 | Args: 149 | - ids: list of product numbers 150 | - display_type: single or list. default is single 151 | """ 152 | super(ProductMessage, self).__init__(**kwargs) 153 | 154 | self.event = 'product' 155 | self.options = { 156 | 'ids': ids, 157 | 'displayType': display_type 158 | } -------------------------------------------------------------------------------- /tests/api/test_send_text.py: -------------------------------------------------------------------------------- 1 | #-*- encoding:utf-8 -*- 2 | import json 3 | import unittest 4 | import responses 5 | try: 6 | from unittest import mock 7 | except: 8 | import mock 9 | 10 | 11 | from nta import ( 12 | NaverTalkApi, 13 | ) 14 | from nta.models import( 15 | TextContent, QuickReply, ButtonText, ButtonLink 16 | ) 17 | 18 | 19 | class TestNaverTalkAPI(unittest.TestCase): 20 | def setUp(self): 21 | self.tested = NaverTalkApi('test_naver_talk_access_token') 22 | 23 | @responses.activate 24 | def test_send_text(self): 25 | responses.add( 26 | responses.POST, 27 | NaverTalkApi.DEFAULT_API_ENDPOINT, 28 | json={ 29 | "success": True, 30 | "resultCode": "00" 31 | }, 32 | status=200 33 | ) 34 | 35 | counter = mock.MagicMock() 36 | def test_callback(res, payload): 37 | self.assertEqual(res.result_code, "00") 38 | self.assertEqual(res.success, True) 39 | self.assertEqual( 40 | payload, 41 | { 42 | 'event': 'send', 43 | 'user': 'test_user_id', 44 | 'textContent': { 45 | 'text': 'test_str_message', 46 | 'code': None, 47 | 'inputType': None 48 | }, 49 | 'options': { 50 | 'notification': False, 51 | 'readBySend': False 52 | } 53 | } 54 | ) 55 | counter() 56 | 57 | self.tested.send( 58 | 'test_user_id', 59 | 'test_str_message', 60 | callback=test_callback 61 | ) 62 | self.assertEqual(counter.call_count, 1) 63 | 64 | self.tested.send( 65 | user_id='test_user_id', 66 | message=TextContent('test_str_message'), 67 | callback=test_callback 68 | ) 69 | self.assertEqual(counter.call_count, 2) 70 | 71 | 72 | 73 | @responses.activate 74 | def test_send_with_quick_reply(self): 75 | responses.add( 76 | responses.POST, 77 | NaverTalkApi.DEFAULT_API_ENDPOINT, 78 | json={ 79 | "success": True, 80 | "resultCode": "00" 81 | }, 82 | status=200 83 | ) 84 | 85 | counter = mock.MagicMock() 86 | 87 | def test_callback(res, payload): 88 | self.assertEqual(res.result_code, "00") 89 | self.assertEqual(res.success, True) 90 | self.assertEqual( 91 | payload, 92 | { 93 | "event": "send", 94 | "user": "test_user_id", 95 | "options": { 96 | "notification": False, 97 | "readBySend": False 98 | }, 99 | "textContent": { 100 | "code": None, 101 | "inputType": None, 102 | "quickReply": { 103 | "buttonList": [{ 104 | "data": { 105 | "code": "PAYLOAD", 106 | "title": "text"}, 107 | "type": "TEXT"}, 108 | { 109 | "data": { 110 | "mobileUrl": None, 111 | "title": "text", 112 | "url": "PAYLOAD"}, 113 | "type": "LINK"}]}, 114 | "text": "test_str_message"} 115 | } 116 | ) 117 | 118 | counter() 119 | 120 | self.tested.send( 121 | 'test_user_id', 122 | 'test_str_message', 123 | quick_reply=QuickReply( 124 | [ 125 | {'type': 'TEXT', 'title': 'text', 'value': 'PAYLOAD'}, 126 | {'type': 'LINK', 'title': 'text', 'value': 'PAYLOAD'} 127 | ] 128 | ), 129 | callback=test_callback 130 | ) 131 | self.assertEqual(counter.call_count, 1) 132 | 133 | self.tested.send( 134 | 'test_user_id', 135 | 'test_str_message', 136 | quick_reply=[ 137 | ButtonText('text', 'PAYLOAD'), 138 | ButtonLink('text', 'PAYLOAD') 139 | ], 140 | callback=test_callback 141 | ) 142 | self.assertEqual(counter.call_count, 2) -------------------------------------------------------------------------------- /tests/api/test_persistent_menu.py: -------------------------------------------------------------------------------- 1 | #-*- encoding:utf-8 -*- 2 | import json 3 | import unittest 4 | import responses 5 | try: 6 | from unittest import mock 7 | except: 8 | import mock 9 | 10 | 11 | from nta import ( 12 | NaverTalkApi, Button 13 | ) 14 | from nta.models import( 15 | TextContent, QuickReply, ButtonText, ButtonLink 16 | ) 17 | 18 | 19 | class TestNaverTalkAPI(unittest.TestCase): 20 | def setUp(self): 21 | self.tested = NaverTalkApi('test_naver_talk_access_token') 22 | 23 | @responses.activate 24 | def test_persistent_menu(self): 25 | responses.add( 26 | responses.POST, 27 | NaverTalkApi.DEFAULT_API_ENDPOINT, 28 | json={ 29 | "success": True, 30 | "resultCode": "00" 31 | }, 32 | status=200 33 | ) 34 | 35 | counter = mock.MagicMock() 36 | def test_callback(res, payload): 37 | self.assertEqual( 38 | payload, 39 | { 40 | "event": "persistentMenu", 41 | "menuContent": [{ 42 | "menus": 43 | [{ 44 | "type": "TEXT", 45 | "data": { 46 | "title": "챗봇 안내", 47 | "code": "CHATBOT_GUIDE" 48 | } 49 | }, { 50 | "type": "LINK", 51 | "data": { 52 | "title": "이벤트 페이지", 53 | "url": "http://your-pc-url.com/event", 54 | "mobileUrl": "http://your-mobile-url.com/event" 55 | } 56 | }, { 57 | "type": "LINK", 58 | "data": { 59 | "title": "전화하기", 60 | "url": "tel:021234567", 61 | "mobileUrl": None 62 | } 63 | }, { 64 | "type": "NESTED", 65 | "data": { 66 | "title": "공지사항", 67 | "menus": 68 | [{ 69 | "type": "LINK", 70 | "data": { 71 | "title": "교환/환불 안내", 72 | "url": "http://your-pc-url.com/guide", 73 | "mobileUrl": "http://your-mobile-url.com/guide" 74 | } 75 | }] 76 | } 77 | }] 78 | }] 79 | } 80 | ) 81 | counter() 82 | 83 | self.tested.persistent_menu( 84 | menus=[ 85 | Button.ButtonText(title='챗봇 안내', code='CHATBOT_GUIDE'), 86 | Button.ButtonLink( 87 | title='이벤트 페이지', 88 | url='http://your-pc-url.com/event', 89 | mobile_url='http://your-mobile-url.com/event' 90 | ), 91 | Button.ButtonLink( 92 | title='전화하기', 93 | url='tel:021234567' 94 | ), 95 | Button.ButtonNested( 96 | title='공지사항', 97 | menus=[ 98 | Button.ButtonLink( 99 | title="교환/환불 안내", 100 | url="http://your-pc-url.com/guide", 101 | mobile_url="http://your-mobile-url.com/guide" 102 | ) 103 | ] 104 | ) 105 | ], 106 | callback=test_callback 107 | ) 108 | self.assertEqual(counter.call_count, 1) 109 | 110 | @responses.activate 111 | def test_persistent_menu_with_None(self): 112 | responses.add( 113 | responses.POST, 114 | NaverTalkApi.DEFAULT_API_ENDPOINT, 115 | json={ 116 | "success": True, 117 | "resultCode": "00" 118 | }, 119 | status=200 120 | ) 121 | 122 | counter = mock.MagicMock() 123 | def test_callback(res, payload): 124 | self.assertEqual( 125 | payload, 126 | { 127 | "event": "persistentMenu", 128 | "menuContent": [] 129 | } 130 | ) 131 | counter() 132 | 133 | self.tested.persistent_menu(callback=test_callback) 134 | self.assertEqual(counter.call_count, 1) -------------------------------------------------------------------------------- /tests/api/test_send_image.py: -------------------------------------------------------------------------------- 1 | #-*- encoding:utf-8 -*- 2 | import json 3 | import unittest 4 | import responses 5 | try: 6 | from unittest import mock 7 | except: 8 | import mock 9 | 10 | 11 | from nta import ( 12 | NaverTalkApi 13 | ) 14 | from nta.models import( 15 | ImageContent, ButtonLink, ButtonText, QuickReply 16 | ) 17 | 18 | 19 | class TestNaverTalkAPI(unittest.TestCase): 20 | def setUp(self): 21 | self.tested = NaverTalkApi('test_naver_talk_access_token') 22 | 23 | @responses.activate 24 | def test_send_image(self): 25 | responses.add( 26 | responses.POST, 27 | NaverTalkApi.DEFAULT_API_ENDPOINT, 28 | json={ 29 | "success": True, 30 | "resultCode": "00" 31 | }, 32 | status=200 33 | ) 34 | 35 | counter = mock.MagicMock() 36 | 37 | def test_callback(res, payload): 38 | self.assertEqual(res.result_code, "00") 39 | self.assertEqual(res.success, True) 40 | self.assertEqual( 41 | payload, 42 | { 43 | 'event': 'send', 44 | 'user': 'test_user_id', 45 | 'imageContent': { 46 | 'imageUrl': 'test.jpg', 47 | }, 48 | 'options': { 49 | 'notification': False, 50 | 'readBySend': False 51 | } 52 | } 53 | ) 54 | counter() 55 | 56 | self.tested.send( 57 | 'test_user_id', 58 | ImageContent('test.jpg'), 59 | callback=test_callback 60 | ) 61 | self.assertEqual(counter.call_count, 1) 62 | 63 | counter2 = mock.MagicMock() 64 | def test_image_id_callback(res, payload): 65 | self.assertEqual(res.result_code, "00") 66 | self.assertEqual(res.success, True) 67 | self.assertEqual( 68 | payload, 69 | { 70 | 'event': 'send', 71 | 'user': 'test_user_id', 72 | 'imageContent': { 73 | 'imageId': '1234test', 74 | }, 75 | 'options': { 76 | 'notification': False, 77 | 'readBySend': False 78 | } 79 | } 80 | ) 81 | counter2() 82 | 83 | self.tested.send( 84 | 'test_user_id', 85 | ImageContent(image_id='1234test'), 86 | callback=test_image_id_callback 87 | ) 88 | 89 | self.assertEqual(counter2.call_count, 1) 90 | 91 | @responses.activate 92 | def test_send_image_with_quick_reply(self): 93 | responses.add( 94 | responses.POST, 95 | NaverTalkApi.DEFAULT_API_ENDPOINT, 96 | json={ 97 | "success": True, 98 | "resultCode": "00" 99 | }, 100 | status=200 101 | ) 102 | 103 | counter = mock.MagicMock() 104 | 105 | def test_callback(res, payload): 106 | self.assertEqual(res.result_code, "00") 107 | self.assertEqual(res.success, True) 108 | self.assertEqual( 109 | payload, 110 | { 111 | 'event': 'send', 112 | 'user': 'test_user_id', 113 | 'imageContent': { 114 | 'imageUrl': 'test.jpg', 115 | 'quickReply': { 116 | 'buttonList': [{ 117 | 'data': { 118 | 'code': 'PAYLOAD', 119 | 'title': 'text'}, 120 | 'type': 'TEXT'}, 121 | { 122 | 'data': { 123 | 'mobileUrl': None, 124 | 'title': 'text', 125 | 'url': 'PAYLOAD'}, 126 | 'type': 'LINK'}]} 127 | }, 128 | 'options': { 129 | 'notification': False, 130 | 'readBySend': False 131 | } 132 | } 133 | ) 134 | counter() 135 | 136 | self.tested.send( 137 | 'test_user_id', 138 | ImageContent('test.jpg'), 139 | quick_reply=QuickReply( 140 | [ 141 | {'type': 'TEXT', 'title': 'text', 'value': 'PAYLOAD'}, 142 | {'type': 'LINK', 'title': 'text', 'value': 'PAYLOAD'} 143 | ] 144 | ), 145 | callback=test_callback 146 | ) 147 | self.assertEqual(counter.call_count, 1) 148 | 149 | self.tested.send( 150 | 'test_user_id', 151 | ImageContent( 152 | 'test.jpg', 153 | quick_reply=QuickReply( 154 | [ 155 | ButtonText('text', 'PAYLOAD'), 156 | ButtonLink('text', 'PAYLOAD') 157 | ] 158 | ) 159 | ), 160 | callback=test_callback 161 | ) 162 | self.assertEqual(counter.call_count, 2) 163 | 164 | -------------------------------------------------------------------------------- /nta/models/template.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | from .buttons import Buttons 3 | 4 | 5 | class BaseTemplate(Base): 6 | def __init__(self, quick_reply=None, **kwargs): 7 | """__init__method. 8 | 9 | Args: 10 | - quick_reply: list of Template.Buttons or Template.QuickReply 11 | """ 12 | super(BaseTemplate, self).__init__(**kwargs) 13 | 14 | if quick_reply: 15 | if isinstance(quick_reply, list): 16 | self.quick_reply = QuickReply(quick_reply) 17 | elif isinstance(quick_reply, QuickReply): 18 | self.quick_reply = quick_reply 19 | 20 | 21 | class TextContent(BaseTemplate): 22 | def __init__(self, text, code=None, input_type=None, **kwargs): 23 | super(TextContent, self).__init__(**kwargs) 24 | 25 | self.text = text 26 | self.code = code 27 | self.input_type = input_type 28 | 29 | 30 | class ImageContent(BaseTemplate): 31 | def __init__(self, image_url=None, image_id=None, **kwargs): 32 | super(ImageContent, self).__init__(**kwargs) 33 | 34 | if image_url is not None: 35 | self.image_url = image_url 36 | elif image_id is not None: 37 | self.image_id = image_id 38 | else: 39 | raise TypeError("'required 1 positional argument: 'image_url' or 'image_id'") 40 | 41 | 42 | class CompositeContent(BaseTemplate): 43 | def __init__(self, composite_list, **kwargs): 44 | """__init__ method. 45 | 46 | Args: 47 | - composite_list: list of Composites 48 | """ 49 | super(CompositeContent, self).__init__(**kwargs) 50 | 51 | self.composite_list = composite_list 52 | 53 | 54 | class Composite(BaseTemplate): 55 | """ 56 | Composite 57 | content element for CompositeContent 58 | 59 | """ 60 | def __init__(self, title, description=None, image=None, element_list=None, button_list=None, **kwargs): 61 | """__init__method. 62 | 63 | Args: 64 | - title: str 65 | - description: str 66 | - image: image url 67 | - element_list: list of Template.Elements 68 | - button_list: list of Template.buttons or dict buttons 69 | """ 70 | super(Composite, self).__init__(**kwargs) 71 | 72 | self.title = title 73 | self.description = description 74 | if image is not None: 75 | self.image = ImageContent(image) 76 | self.element_list = element_list 77 | self.button_list = Buttons.convert_shortcut_buttons(button_list) 78 | 79 | 80 | class ElementList(BaseTemplate): 81 | def __init__(self, data, **kwargs): 82 | super(ElementList, self).__init__(**kwargs) 83 | 84 | self.type = "LIST" 85 | self.data = data 86 | 87 | 88 | class ElementData(Base): 89 | def __init__(self, title, description=None, sub_description=None, image=None, button=None, **kwargs): 90 | """__init__ method.""" 91 | super(ElementData, self).__init__(**kwargs) 92 | 93 | self.title = title 94 | self.description = description 95 | self.sub_description = sub_description 96 | if image is not None: 97 | self.image = ImageContent(image) 98 | self.button = button 99 | 100 | 101 | class QuickReply(Base): 102 | def __init__(self, button_list, **kwargs): 103 | super(QuickReply, self).__init__(**kwargs) 104 | 105 | self.button_list = Buttons.convert_shortcut_buttons(button_list) 106 | 107 | 108 | class PaymentInfo(Base): 109 | def __init__( 110 | self, 111 | merchant_pay_key, 112 | total_pay_amount, 113 | product_items, 114 | merchant_user_key=None, 115 | product_name=None, 116 | product_count=None, 117 | delivery_fee=None, 118 | tax_scope_amount=None, 119 | tax_ex_scope_amount=None, 120 | purchaser_name=None, 121 | purchaser_birthday=None, 122 | **kwargs 123 | ): 124 | super(PaymentInfo, self).__init__(**kwargs) 125 | 126 | self.merchant_pay_key = merchant_pay_key 127 | self.total_pay_amount = total_pay_amount 128 | self.product_items = product_items 129 | self.merchant_user_key = merchant_user_key 130 | self.product_name = product_name 131 | self.product_count = product_count 132 | self.delivery_fee = delivery_fee 133 | self.tax_scope_amount = tax_scope_amount 134 | self.tax_ex_scope_amount = tax_ex_scope_amount 135 | self.purchaser_name = purchaser_name 136 | self.purchaser_birthday = purchaser_birthday 137 | 138 | 139 | class ProductItem(Base): 140 | def __init__( 141 | self, 142 | category_type, 143 | category_id, 144 | uid, 145 | name, 146 | start_date=None, 147 | end_date=None, 148 | seller_id=None, 149 | count=None, 150 | **kwargs 151 | ): 152 | super(ProductItem, self).__init__(**kwargs) 153 | 154 | self.category_type = category_type 155 | self.category_id = category_id 156 | self.uid = uid 157 | self.name = name 158 | self.start_date = start_date 159 | self.end_date = end_date 160 | self.seller_id = seller_id 161 | self.count = count 162 | 163 | 164 | class Menus(Base): 165 | def __init__(self, menus, **kwargs): 166 | super(Menus, self).__init__(**kwargs) 167 | 168 | self.menus = menus -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## 1. Purpose 4 | 5 | A primary goal of Naver Talk Sdk is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof). 6 | 7 | This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior. 8 | 9 | We invite all those who participate in Naver Talk Sdk to help us create safe and positive experiences for everyone. 10 | 11 | ## 2. Open Source Citizenship 12 | 13 | A supplemental goal of this Code of Conduct is to increase open source citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community. 14 | 15 | Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society. 16 | 17 | If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know. 18 | 19 | ## 3. Expected Behavior 20 | 21 | The following behaviors are expected and requested of all community members: 22 | 23 | * Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. 24 | * Exercise consideration and respect in your speech and actions. 25 | * Attempt collaboration before conflict. 26 | * Refrain from demeaning, discriminatory, or harassing behavior and speech. 27 | * Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. 28 | * Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations. 29 | 30 | ## 4. Unacceptable Behavior 31 | 32 | The following behaviors are considered harassment and are unacceptable within our community: 33 | 34 | * Violence, threats of violence or violent language directed against another person. 35 | * Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. 36 | * Posting or displaying sexually explicit or violent material. 37 | * Posting or threatening to post other people’s personally identifying information ("doxing"). 38 | * Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. 39 | * Inappropriate photography or recording. 40 | * Inappropriate physical contact. You should have someone’s consent before touching them. 41 | * Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances. 42 | * Deliberate intimidation, stalking or following (online or in person). 43 | * Advocating for, or encouraging, any of the above behavior. 44 | * Sustained disruption of community events, including talks and presentations. 45 | 46 | ## 5. Consequences of Unacceptable Behavior 47 | 48 | Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated. 49 | 50 | Anyone asked to stop unacceptable behavior is expected to comply immediately. 51 | 52 | If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event). 53 | 54 | ## 6. Reporting Guidelines 55 | 56 | If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. hollal0726@gmail.com. 57 | 58 | 59 | 60 | Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress. 61 | 62 | ## 7. Addressing Grievances 63 | 64 | If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify Hwang Won Yo with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies. 65 | 66 | 67 | 68 | ## 8. Scope 69 | 70 | We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues–online and in-person–as well as in all one-on-one communications pertaining to community business. 71 | 72 | This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members. 73 | 74 | ## 9. Contact info 75 | 76 | hollal0726@gmail.com 77 | 78 | ## 10. License and attribution 79 | 80 | This Code of Conduct is distributed under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). 81 | 82 | Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy). 83 | 84 | Retrieved on November 22, 2016 from [http://citizencodeofconduct.org/](http://citizencodeofconduct.org/) 85 | -------------------------------------------------------------------------------- /nta/models/buttons.py: -------------------------------------------------------------------------------- 1 | from .base import Base 2 | 3 | 4 | class Buttons(Base): 5 | """Base class of Buttons.""" 6 | @staticmethod 7 | def convert_shortcut_buttons(items): 8 | """ 9 | support shortcut buttons. 10 | EX) [{'type':'TEXT', 'title':'text', 'value':'PAYLOAD'}] 11 | """ 12 | if items is not None and isinstance(items, list): 13 | result = [] 14 | for item in items: 15 | if isinstance(item, Buttons): 16 | result.append(item) 17 | elif isinstance(item, dict): 18 | if item.get('type') in ['TEXT', 'LINK', 'OPTION', 'PAY']: 19 | type = item.get('type') 20 | title = item.get('title') 21 | value = item.get('value', item.get('url', item.get('code', item.get('buttons')))) 22 | 23 | if type == 'TEXT': 24 | result.append(ButtonText(title=title, code=value)) 25 | elif type == 'LINK': 26 | moburl = item.get('mobile_url') 27 | result.append(ButtonLink(title=title, url=value, mobile_url=moburl)) 28 | elif type == 'OPTION': 29 | result.append(ButtonOption(title=title, button_list=value)) 30 | elif type == 'PAY': 31 | result.append(ButtonPay(payment_info=value)) 32 | 33 | else: 34 | raise ValueError('Invalid button type') 35 | else: 36 | raise ValueError('Invalid buttons variables') 37 | return result 38 | else: 39 | return items 40 | 41 | 42 | class ButtonText(Buttons): 43 | """ 44 | ButtonText for text button to use quickReply and Composite buttons. 45 | Invoke Send Event with code value from navertalk to server when user clicks the button. 46 | """ 47 | def __init__(self, title, code=None, **kwargs): 48 | """ __init__ method. 49 | 50 | :param title: exposed text on the button 51 | :param code: enclosed value under the button 52 | :param kwargs: 53 | """ 54 | super(ButtonText, self).__init__(**kwargs) 55 | 56 | self.type = "TEXT" 57 | self.data = { 58 | "title": title 59 | } 60 | if code: 61 | self.data['code'] = code 62 | 63 | 64 | class ButtonLink(Buttons): 65 | """ 66 | ButtonLink for link button to use quickReply and Composite button. 67 | Links to url address when user click the button. 68 | """ 69 | def __init__(self, title, url, mobile_url=None, webview=False, webview_title=None, webview_height=None, **kwargs): 70 | """ __init__ method. 71 | 72 | :param title: exposed text on the button 73 | :param url: default linked url 74 | :param mobile_url: linked url on mobile device 75 | :param kwargs: 76 | """ 77 | super(ButtonLink, self).__init__(**kwargs) 78 | 79 | self.type = "LINK" 80 | self.data = { 81 | "title":title, 82 | "url":url, 83 | "mobile_url": mobile_url 84 | } 85 | if webview: 86 | self.data['mobile_target'] = 'webview' 87 | self.data['mobile_target_attr'] = { 88 | "webview_title": webview_title, 89 | "webview_height": webview_height 90 | } 91 | 92 | 93 | class ButtonOption(Buttons): 94 | """ 95 | ButtonOption for option button in Composite button. 96 | ButtonOption contains the other buttons. 97 | """ 98 | def __init__(self, title, button_list, **kwargs): 99 | """ __init___ method. 100 | 101 | :param title: exposed text on the button 102 | :param button_list: Contained butons list. List of Template.Button 103 | :param kwargs: 104 | """ 105 | super(ButtonOption, self).__init__(**kwargs) 106 | 107 | if not isinstance(button_list, list): 108 | button_list = [button_list] 109 | self.type = "OPTION" 110 | self.data = { 111 | "title":title, 112 | "button_list": self.convert_shortcut_buttons(button_list) 113 | } 114 | 115 | 116 | class ButtonPay(Buttons): 117 | """ 118 | ButtonPay for pay button in quickReply and Composite button. 119 | User can Proceed payment when clicks the button. 120 | """ 121 | def __init__(self, payment_info, **kwargs): 122 | """ __init__ method. 123 | 124 | :param payment_info: Template.PaymentInfo Instance 125 | :param kwargs: 126 | """ 127 | super(ButtonPay, self).__init__(**kwargs) 128 | 129 | self.type = 'PAY' 130 | self.data = { 131 | 'payment_info': payment_info 132 | } 133 | 134 | 135 | class ButtonNested(Buttons): 136 | def __init__(self, title, menus, **kwargs): 137 | super(ButtonNested, self).__init__(**kwargs) 138 | 139 | self.type = 'NESTED' 140 | self.data = { 141 | 'title': title, 142 | 'menus': menus 143 | } 144 | 145 | 146 | class ButtonTime(Buttons): 147 | def __init__(self, title, code, **kwargs): 148 | super(ButtonTime, self).__init__(**kwargs) 149 | 150 | self.type = 'TIME' 151 | self.data = { 152 | 'title': title, 153 | 'code': code 154 | } 155 | 156 | 157 | class ButtonCalendar(Buttons): 158 | def __init__(self, title=None, code=None, placeholder=None, start=None, end=None, disables=None, **kwargs): 159 | super(ButtonCalendar, self).__init__(**kwargs) 160 | 161 | self.type = 'CALENDAR' 162 | self.data = { 163 | 'title': title, 164 | 'code': code, 165 | 'options':{ 166 | 'calendar': { 167 | 'placeholder': placeholder, 168 | 'start': start, 169 | 'end': end, 170 | 'disables': disables 171 | } 172 | } 173 | } 174 | 175 | 176 | class ButtonTimeInterval(Buttons): 177 | def __init__(self, title=None, code=None, start=None, end=None, interval=None, disables=None ,**kwargs): 178 | super(ButtonTimeInterval, self).__init__(**kwargs) 179 | 180 | self.type = 'TIMEINTERVAL' 181 | self.data = { 182 | 'title': title, 183 | 'code': code, 184 | 'options': { 185 | 'timeInterval': { 186 | 'start': start, 187 | 'end': end, 188 | 'interval': interval, 189 | 'disables': disables 190 | } 191 | } 192 | } -------------------------------------------------------------------------------- /nta/models/events.py: -------------------------------------------------------------------------------- 1 | #-*- encoding:utf-8 -*- 2 | from .base import Base 3 | 4 | class Event(Base): 5 | """ 6 | Base Event class 7 | 8 | user_id property overloaded all subclasses 9 | """ 10 | def __init__(self, user, options=None, standby=False, **kwargs): 11 | super(Event, self).__init__(**kwargs) 12 | 13 | self.user = user 14 | if not options: 15 | options = {} 16 | self.options = options 17 | self.standby = standby 18 | 19 | @property 20 | def user_id(self): 21 | return self.user 22 | 23 | @property 24 | def mobile(self): 25 | return self.options.get('mobile') 26 | 27 | 28 | class OpenEvent(Event): 29 | """ 30 | OpenEvent 31 | 32 | When users enter Navertalk Chat 33 | This event triggered. 34 | """ 35 | def __init__(self, **kwargs): 36 | super(OpenEvent, self).__init__(**kwargs) 37 | 38 | self.event = 'open' 39 | 40 | @property 41 | def inflow(self): 42 | """ 43 | Return way to enter chatting room 44 | """ 45 | return self.options.get('inflow') 46 | 47 | @property 48 | def referer(self): 49 | return self.options.get('referer') 50 | 51 | @property 52 | def friend(self): 53 | return self.options.get('friend') 54 | 55 | @property 56 | def under_14(self): 57 | return self.options.get('under_14') 58 | 59 | @property 60 | def under_19(self): 61 | return self.options.get('under_19') 62 | 63 | 64 | class LeaveEvent(Event): 65 | """ 66 | LeaveEvent 67 | 68 | When users leave Navertalk chat 69 | This event triggered. 70 | """ 71 | def __init__(self, **kwargs): 72 | super(LeaveEvent, self).__init__(**kwargs) 73 | 74 | self.event = 'leave' 75 | 76 | 77 | class FriendEvent(Event): 78 | """ 79 | FriendEvent 80 | 81 | When users add or delete friend 82 | this event triggered. 83 | """ 84 | def __init__(self, options, **kwargs): 85 | super(FriendEvent, self).__init__(**kwargs) 86 | 87 | self.event = 'friend' 88 | self.options = options 89 | 90 | @property 91 | def set_on(self): 92 | return self.options.get('set') == 'on' 93 | 94 | 95 | class SendEvent(Event): 96 | """ 97 | SendEvent 98 | 99 | When users send message to chatbot 100 | this event triggered. 101 | """ 102 | def __init__(self, text_content=None, image_content=None, **kwargs): 103 | super(SendEvent, self).__init__(**kwargs) 104 | 105 | self.event = 'send' 106 | self.text_content = {} 107 | if text_content: 108 | self.text_content = text_content 109 | self.image_content = {} 110 | if image_content: 111 | self.image_content = image_content 112 | 113 | @property 114 | def text(self): 115 | return self.text_content.get('text') 116 | 117 | @property 118 | def code(self): 119 | return self.text_content.get('code') 120 | 121 | @property 122 | def input_type(self): 123 | return self.text_content.get('input_type') 124 | 125 | @property 126 | def is_code(self): 127 | return self.text_content.get('code') is not None 128 | 129 | @property 130 | def image_url(self): 131 | return self.image_content.get('image_url') 132 | 133 | 134 | class EchoEvent(Event): 135 | """ 136 | EchoEvent 137 | """ 138 | def __init__(self, echoed_event, text_content=None, image_content=None, composite_content=None, **kwargs): 139 | super(EchoEvent, self).__init__(**kwargs) 140 | 141 | self.event = 'echo' 142 | self.echoed_event = echoed_event 143 | self.text_content = text_content 144 | self.image_content = image_content 145 | self.composite_content = composite_content 146 | 147 | 148 | class PayCompleteEvent(Event): 149 | """ 150 | PayCompleteEvent 151 | """ 152 | def __init__(self, options, **kwargs): 153 | super(PayCompleteEvent, self).__init__(**kwargs) 154 | 155 | self.event = 'pay_complete' 156 | self.options = options 157 | 158 | @property 159 | def payment_result(self): 160 | return self.options.get('payment_result', {}) 161 | 162 | @property 163 | def code(self): 164 | return self.payment_result.get('code') 165 | 166 | @property 167 | def payment_id(self): 168 | return self.payment_result.get('payment_id') 169 | 170 | @property 171 | def merchant_pay_key(self): 172 | return self.payment_result.get('merchant_pay_key') 173 | 174 | @property 175 | def merchant_user_key(self): 176 | return self.payment_result.get('merchant_user_key') 177 | 178 | @property 179 | def message(self): 180 | return self.payment_result.get('message') 181 | 182 | 183 | class PayConfirmEvent(Event): 184 | """ 185 | PayConfirmEvent 186 | """ 187 | def __init__(self, options, **kwargs): 188 | super(PayConfirmEvent, self).__init__(**kwargs) 189 | 190 | self.event = 'pay_confirm' 191 | self.options = options 192 | 193 | @property 194 | def payment_confirm_result(self): 195 | return self.options.get('payment_confirm_result', {}) 196 | 197 | @property 198 | def code(self): 199 | return self.payment_confirm_result.get('code') 200 | 201 | @property 202 | def message(self): 203 | return self.payment_confirm_result.get('message') 204 | 205 | @property 206 | def payment_id(self): 207 | return self.payment_confirm_result.get('payment_id') 208 | 209 | @property 210 | def detail(self): 211 | """ 212 | 네이버페이 간편결제 결제승인 API 응답본문의 detail 필드를 그대로 반환. 213 | """ 214 | return self.payment_confirm_result.get('detail') 215 | 216 | 217 | class ProfileEvent(Event): 218 | """ 219 | ProfileEvent 220 | 221 | When Agent asks users profile information and request profile info, 222 | this event will triggered. 223 | """ 224 | def __init__(self, options, **kwargs): 225 | super(ProfileEvent, self).__init__(**kwargs) 226 | 227 | self.event = 'profile' 228 | self.options = options 229 | 230 | @property 231 | def result(self): 232 | return self.options.get('result') 233 | 234 | @property 235 | def nickname(self): 236 | return self.options.get('nickname') 237 | 238 | @property 239 | def cellphone(self): 240 | return self.options.get('cellphone') 241 | 242 | @property 243 | def address(self): 244 | return self.options.get('address') 245 | 246 | 247 | class HandOverEvent(Event): 248 | """ 249 | HandOverEvent 250 | 251 | When a conversation is over, 252 | This event will triggered. 253 | """ 254 | def __init__(self, **kwargs): 255 | super(HandOverEvent, self).__init__(**kwargs) 256 | 257 | self.event = 'handover' 258 | 259 | @property 260 | def control(self): 261 | return self.options.get('control') 262 | 263 | @property 264 | def metadata(self): 265 | return self.options.get('metadata') -------------------------------------------------------------------------------- /tests/models/test_buttons.py: -------------------------------------------------------------------------------- 1 | #-*- encoding:utf8 -*- 2 | from __future__ import unicode_literals, absolute_import 3 | 4 | import unittest 5 | 6 | from nta.models import ( 7 | Buttons, ButtonText, ButtonPay, ButtonLink, ButtonOption, ButtonNested, ButtonTime, ButtonCalendar, 8 | ButtonTimeInterval, 9 | PaymentInfo, ProductItem 10 | ) 11 | 12 | 13 | class TestNaverTalkApi(unittest.TestCase): 14 | 15 | def test_button_convert_test(self): 16 | btns = Buttons.convert_shortcut_buttons( 17 | [ 18 | {'type': 'TEXT', 'title': 'test_title', 'value': 'test_payload'}, 19 | {'type': 'LINK', 'title': 'test_title', 'value': 'test_url.com'}, 20 | {'type': 'OPTION', 'title': 'test_title', 'value': [{'type': 'TEXT', 'title': 'under_option_button'}]}, 21 | {'type': 'PAY', 'value': PaymentInfo(1,1,1)} 22 | ] 23 | ) 24 | 25 | self.assertTrue(isinstance(btns[0], ButtonText)) 26 | self.assertTrue(isinstance(btns[1], ButtonLink)) 27 | self.assertTrue(isinstance(btns[2], ButtonOption)) 28 | self.assertTrue(isinstance(btns[3], ButtonPay)) 29 | self.assertEqual(btns[0], ButtonText('test_title', 'test_payload')) 30 | self.assertEqual(btns[1], ButtonLink('test_title', 'test_url.com')) 31 | self.assertEqual(btns[2], ButtonOption('test_title', ButtonText('under_option_button'))) 32 | self.assertEqual(btns[3], ButtonPay(PaymentInfo(1,1,1))) 33 | 34 | 35 | def test_text_button(self): 36 | btn = ButtonText('test_title', 'test_payload') 37 | self.assertEqual(btn, {'type': 'TEXT', 'data': {'title': 'test_title', 'code': 'test_payload'}}) 38 | 39 | def test_link_button(self): 40 | btn1 = ButtonLink('test_title', 'test_url.com') 41 | self.assertEqual( 42 | btn1, 43 | {'type': 'LINK', 'data': {'title': 'test_title', 'url': 'test_url.com', 'mobileUrl': None}} 44 | ) 45 | 46 | btn2 = ButtonLink( 47 | 'test_webview_button', 48 | 'test_url.com', 49 | 'test_webview_url.com', 50 | webview=True, 51 | webview_title='webview_title', 52 | webview_height=50 53 | ) 54 | self.assertEqual( 55 | btn2, 56 | {'type': 'LINK', 57 | 'data': { 58 | 'title': 'test_webview_button', 59 | 'url': 'test_url.com', 60 | 'mobileUrl': 'test_webview_url.com', 61 | 'mobileTarget': 'webview', 62 | 'mobileTargetAttr': { 63 | 'webviewHeight': 50, 64 | 'webviewTitle': 'webview_title' 65 | }} 66 | } 67 | ) 68 | 69 | def test_option_button(self): 70 | btn = ButtonOption('test_title', ButtonText('under_option_button')) 71 | self.assertEqual( 72 | btn, 73 | { 74 | 'type': 'OPTION', 75 | 'data': { 76 | 'title': 'test_title', 77 | 'buttonList': [{'type': 'TEXT', 78 | 'data': { 79 | 'title': 'under_option_button' 80 | }}] 81 | } 82 | } 83 | ) 84 | 85 | def test_pay_button(self): 86 | product_item = ProductItem( 87 | category_type='FOOD', 88 | category_id='DELIVERY', 89 | uid='bot-product-1234', 90 | name='yo', 91 | start_date='20171130', 92 | end_date='20171201', 93 | seller_id='hollal', 94 | count=1 95 | ) 96 | payinfo = PaymentInfo( 97 | merchant_pay_key='bot-pay=1234', 98 | total_pay_amount=15000, 99 | product_items=[product_item], 100 | merchant_user_key=1, 101 | product_name='test_product', 102 | product_count=1, 103 | delivery_fee=1, 104 | tax_scope_amount=1, 105 | tax_ex_scope_amount=1, 106 | purchaser_name='hwang', 107 | purchaser_birthday='0726' 108 | ) 109 | btn = ButtonPay(payinfo) 110 | self.assertEqual( 111 | btn, 112 | { 113 | 'type': 'PAY', 114 | 'data': { 115 | 'paymentInfo': { 116 | 'merchantPayKey': 'bot-pay=1234', 117 | 'totalPayAmount': 15000, 118 | 'productItems': [ 119 | { 120 | 'categoryType': 'FOOD', 121 | 'categoryId': 'DELIVERY', 122 | 'uid': 'bot-product-1234', 123 | 'name': 'yo', 124 | 'startDate': '20171130', 125 | 'endDate': '20171201', 126 | 'sellerId': 'hollal', 127 | 'count': 1 128 | } 129 | ], 130 | 'merchantUserKey': 1, 131 | 'productName': 'test_product', 132 | 'productCount': 1, 133 | 'deliveryFee': 1, 134 | 'taxScopeAmount': 1, 135 | 'taxExScopeAmount': 1, 136 | 'purchaserName': 'hwang', 137 | 'purchaserBirthday': '0726' 138 | } 139 | } 140 | } 141 | ) 142 | 143 | def test_button_nested(self): 144 | target = { 145 | "type":"NESTED", 146 | "data":{ 147 | "title":"공지사항", 148 | "menus": 149 | [{ 150 | "type":"LINK", 151 | "data":{ 152 | "title":"교환/환불 안내", 153 | "url":"http://your-pc-url.com/guide", 154 | "mobileUrl":"http://your-mobile-url.com/guide" 155 | } 156 | }] 157 | } 158 | } 159 | btn = ButtonNested( 160 | title='공지사항', 161 | menus=[ 162 | ButtonLink( 163 | title='교환/환불 안내', 164 | url='http://your-pc-url.com/guide', 165 | mobile_url='http://your-mobile-url.com/guide' 166 | ) 167 | ] 168 | ) 169 | 170 | self.assertEqual(btn, target) 171 | 172 | def test_time_button(self): 173 | target = { 174 | "type": "TIME", 175 | "data": { 176 | "title": "타이틀", 177 | "code": "코드" 178 | } 179 | } 180 | btn = ButtonTime( 181 | title='타이틀', 182 | code='코드' 183 | ) 184 | self.assertEqual(target, btn) 185 | 186 | def test_calendar_button(self): 187 | target = { 188 | "type": "CALENDAR", 189 | "data": { 190 | "title": "방문 날짜 선택하기", 191 | "code": "code_for_your_bot", 192 | "options": { 193 | "calendar": { 194 | "disables": "1,20180309,20180315-20180316", 195 | "end": "20180430", 196 | "placeholder": "방문 날짜를 선택해주세요.", 197 | "start": "20180301", 198 | } 199 | } 200 | } 201 | } 202 | btn = ButtonCalendar( 203 | title= "방문 날짜 선택하기", 204 | code= "code_for_your_bot", 205 | placeholder="방문 날짜를 선택해주세요.", 206 | start="20180301", 207 | end="20180430", 208 | disables="1,20180309,20180315-20180316" 209 | ) 210 | self.assertEqual(target, btn) 211 | 212 | def test_time_interval(self): 213 | target = { 214 | "type": "TIMEINTERVAL", 215 | "data": { 216 | "title": "방문 시간 선택하기", 217 | "code": "code_for_your_bot", 218 | "options": { 219 | "timeInterval": { 220 | "start": "0900", 221 | "end": "2200", 222 | "interval": "15", 223 | "disables": "1000,1115-1130,1200,1400-1430" 224 | } 225 | } 226 | } 227 | } 228 | btn = ButtonTimeInterval( 229 | title="방문 시간 선택하기", 230 | code="code_for_your_bot", 231 | start="0900", 232 | end="2200", 233 | interval="15", 234 | disables="1000,1115-1130,1200,1400-1430" 235 | ) 236 | print(btn) 237 | self.assertEqual(target, btn) 238 | 239 | if __name__ == '__main__': 240 | unittest.main() -------------------------------------------------------------------------------- /tests/api/test_send_composite.py: -------------------------------------------------------------------------------- 1 | #-*- encoding:utf-8 -*- 2 | import json 3 | import unittest 4 | import responses 5 | try: 6 | from unittest import mock 7 | except: 8 | import mock 9 | 10 | 11 | from nta import ( 12 | NaverTalkApi 13 | ) 14 | from nta.models import( 15 | CompositeContent, Composite, ElementData, ElementList, 16 | ButtonText, ButtonLink, ButtonCalendar, QuickReply 17 | ) 18 | 19 | 20 | class TestNaverTalkAPI(unittest.TestCase): 21 | def setUp(self): 22 | self.tested = NaverTalkApi('test_naver_talk_access_token') 23 | 24 | @responses.activate 25 | def test_send_composite(self): 26 | responses.add( 27 | responses.POST, 28 | NaverTalkApi.DEFAULT_API_ENDPOINT, 29 | json={ 30 | "success": True, 31 | "resultCode": "00" 32 | }, 33 | status=200 34 | ) 35 | 36 | counter = mock.MagicMock() 37 | def test_callback(res, payload): 38 | self.assertEqual(res.result_code, "00") 39 | self.assertEqual(res.success, True) 40 | self.assertEqual( 41 | payload.as_json_dict(), 42 | { 43 | 'event': 'send', 44 | 'user': 'test_user_id', 45 | 'compositeContent': { 46 | 'compositeList': [ 47 | { 48 | 'title': 'test_title', 49 | 'description': 'test_descript', 50 | 'image': { 51 | 'imageUrl': 'test_image' 52 | }, 53 | 'elementList':{ 54 | 'type': 'LIST', 55 | 'data': [ 56 | { 57 | 'title': 'test_ed_title', 58 | 'description': 'test_ed_descript', 59 | 'subDescription': 'test_ed_subdescript', 60 | 'image': { 61 | 'imageUrl': 'test_ed_image' 62 | }, 63 | 'button':{ 64 | 'type': 'TEXT', 65 | 'data': { 66 | 'title': 'test' 67 | } 68 | } 69 | } 70 | ] 71 | 72 | }, 73 | 'buttonList': None 74 | } 75 | ] 76 | }, 77 | 'options': { 78 | 'notification': False, 79 | 'readBySend': False 80 | } 81 | } 82 | ) 83 | counter() 84 | 85 | 86 | self.tested.send( 87 | 'test_user_id', 88 | message=CompositeContent( 89 | composite_list=[ 90 | Composite( 91 | title='test_title', 92 | description='test_descript', 93 | image='test_image', 94 | element_list=ElementList([ 95 | ElementData( 96 | title='test_ed_title', 97 | description='test_ed_descript', 98 | sub_description='test_ed_subdescript', 99 | image='test_ed_image', 100 | button=ButtonText('test') 101 | ) 102 | ]) 103 | ) 104 | ] 105 | ), 106 | callback=test_callback 107 | ) 108 | self.assertEqual(counter.call_count, 1) 109 | 110 | @responses.activate 111 | def test_send_composite_with_quick_reply(self): 112 | responses.add( 113 | responses.POST, 114 | NaverTalkApi.DEFAULT_API_ENDPOINT, 115 | json={ 116 | "success": True, 117 | "resultCode": "00" 118 | }, 119 | status=200 120 | ) 121 | 122 | counter = mock.MagicMock() 123 | 124 | def test_callback(res, payload): 125 | self.assertEqual(res.result_code, "00") 126 | self.assertEqual(res.success, True) 127 | self.assertEqual( 128 | payload.as_json_dict(), 129 | { 130 | 'event': 'send', 131 | 'user': 'test_user_id', 132 | 'compositeContent': { 133 | 'compositeList': [ 134 | { 135 | 'title': 'test_title', 136 | 'description': None, 137 | 'elementList': None, 138 | 'buttonList': None 139 | } 140 | ], 141 | 'quickReply': { 142 | 'buttonList': [{ 143 | 'data': { 144 | 'code': 'PAYLOAD', 145 | 'title': 'text'}, 146 | 'type': 'TEXT'}, 147 | { 148 | 'data': { 149 | 'mobileUrl': None, 150 | 'title': 'text', 151 | 'url': 'PAYLOAD'}, 152 | 'type': 'LINK'}]} 153 | 154 | }, 155 | 'options': { 156 | 'notification': False, 157 | 'readBySend': False 158 | } 159 | } 160 | ) 161 | counter() 162 | 163 | self.tested.send( 164 | 'test_user_id', 165 | message=CompositeContent( 166 | composite_list=[ 167 | Composite( 168 | title='test_title' 169 | ) 170 | ] 171 | ), 172 | quick_reply=QuickReply( 173 | [ 174 | {'type': 'TEXT', 'title': 'text', 'value': 'PAYLOAD'}, 175 | {'type': 'LINK', 'title': 'text', 'value': 'PAYLOAD'} 176 | ] 177 | ), 178 | callback=test_callback 179 | ) 180 | self.assertEqual(counter.call_count, 1) 181 | 182 | self.tested.send( 183 | 'test_user_id', 184 | message=CompositeContent( 185 | composite_list=[ 186 | Composite( 187 | title='test_title' 188 | ) 189 | ], 190 | quick_reply=[ 191 | ButtonText('text', 'PAYLOAD'), 192 | ButtonLink('text', 'PAYLOAD') 193 | ] 194 | ), 195 | callback=test_callback 196 | ) 197 | self.assertEqual(counter.call_count, 2) 198 | 199 | @responses.activate 200 | def test_composite_with_calendar(self): 201 | responses.add( 202 | responses.POST, 203 | NaverTalkApi.DEFAULT_API_ENDPOINT, 204 | json={ 205 | "success": True, 206 | "resultCode": "00" 207 | }, 208 | status=200 209 | ) 210 | 211 | counter = mock.MagicMock() 212 | 213 | def test_callback(res, payload): 214 | target = { 215 | "event": "send", 216 | "user": "test_user_id", 217 | "compositeContent": { 218 | "compositeList": [ 219 | { 220 | "title": "톡톡 레스토랑", 221 | "description": "파스타가 맛있는집", 222 | 'elementList': None, 223 | "buttonList": [ 224 | { 225 | "type": "CALENDAR", 226 | "data": { 227 | "title": "방문 날짜 선택하기", 228 | "code": "code_for_your_bot", 229 | "options": { 230 | "calendar": { 231 | "placeholder": "방문 날짜를 선택해주세요.", 232 | "start": "20180301", 233 | "end": "20180430", 234 | "disables": "1,20180309,20180315-20180316" 235 | } 236 | } 237 | } 238 | } 239 | ] 240 | } 241 | ] 242 | }, 243 | 'options': { 244 | 'notification': False, 245 | 'readBySend': False 246 | 247 | } 248 | } 249 | self.assertEqual(target, payload.as_json_dict()) 250 | counter() 251 | 252 | self.tested.send( 253 | "test_user_id", 254 | message=CompositeContent( 255 | composite_list=[ 256 | Composite( 257 | title= "톡톡 레스토랑", 258 | description="파스타가 맛있는집", 259 | button_list=[ 260 | ButtonCalendar( 261 | title="방문 날짜 선택하기", 262 | code="code_for_your_bot", 263 | placeholder="방문 날짜를 선택해주세요.", 264 | start="20180301", 265 | end="20180430", 266 | disables="1,20180309,20180315-20180316" 267 | ) 268 | ] 269 | ) 270 | 271 | ] 272 | ), 273 | callback=test_callback 274 | ) 275 | self.assertEqual(counter.call_count, 1) -------------------------------------------------------------------------------- /example/example.py: -------------------------------------------------------------------------------- 1 | #-*- encoding:utf-8 -*- 2 | """ 3 | Example code for nta 4 | See how it works: https://talk.naver.com/ct/wc4qdz 5 | """ 6 | import os 7 | from flask import Flask, request 8 | 9 | from nta import NaverTalkApi, Template, Button 10 | from nta import NaverTalkApiError, NaverTalkPaymentError, NaverTalkApiConnectionError 11 | 12 | NAVER_TALK_ACCESS_TOKEN = os.environ['naver_talk_access_token'] 13 | 14 | app = Flask(__name__) 15 | ntalk = NaverTalkApi(NAVER_TALK_ACCESS_TOKEN) 16 | 17 | 18 | @app.route('/', methods=['POST']) 19 | def app_enterance(): 20 | print("*" * 40) 21 | req = request.get_data(as_text=True) 22 | print('* Recieved Data:') 23 | print(req) 24 | try: 25 | ntalk.webhook_handler(req) 26 | except NaverTalkApiError as e: 27 | print(e) 28 | except NaverTalkApiConnectionError as e: 29 | print(e) 30 | except NaverTalkPaymentError as e: 31 | return 400 32 | 33 | print("*" * 40) 34 | 35 | return "ok" 36 | 37 | 38 | @ntalk.before_proccess 39 | def do_something_before_event_handle(event): 40 | print('#' * 40) 41 | print('* EventType: %10s' % event.__class__.__name__) 42 | print('* User Id : %10s '% event.user_id) 43 | 44 | 45 | @ntalk.handle_open 46 | def open_handler(event): 47 | user_id = event.user_id 48 | ntalk.send( 49 | user_id, 50 | "테스트에 성공했구나 :)", 51 | quick_reply=Template.QuickReply([ 52 | Button.ButtonLink( 53 | "코드 보러가기", 54 | "https://github.com/HwangWonYo/naver_talk_sdk/blob/master/example/example.py" 55 | ), 56 | Button.ButtonText( 57 | "따라 말하기" 58 | ) 59 | ]) 60 | ) 61 | 62 | 63 | @ntalk.handle_send 64 | def send_handler(event): 65 | """ 66 | 사용자가 버튼을 눌러 코드값 callback을 사용해도 send_handler가 작동한다. 67 | 만약 코드 값의 callback 함수만 동작하게 하고 싶다면 is_code일 경우 return 하면된다. 68 | """ 69 | if event.is_code: 70 | return 71 | user_id = event.user_id 72 | text = event.text 73 | 74 | #주도권이 파트너에게 있는 경우 standby는 True 75 | if event.standby: 76 | ntalk.send( 77 | user_id=user_id, 78 | message="쓰레드 작동중 !!", 79 | quick_reply=Template.QuickReply([Button.ButtonText('쓰레드 가져오기', 'TakeThread')]) 80 | ) 81 | else: 82 | ntalk.send( 83 | user_id, 84 | "무슨 말을 해도 따라합니다.\n" 85 | "카드뷰 형식을 보고 싶으면 퀵리플라이 클릭" 86 | ) 87 | ntalk.send( 88 | user_id, 89 | text, 90 | quick_reply=Template.QuickReply([Button.ButtonText('카드뷰 보기', 'CardView')]) 91 | ) 92 | 93 | 94 | @ntalk.callback(['CardView']) 95 | def carview_show(event): 96 | user_id = event.user_id 97 | ntalk.send( 98 | user_id, 99 | message=Template.CompositeContent( 100 | composite_list=[ 101 | Template.Composite( 102 | title='페이로드 백을 담은 카드뷰', 103 | description='상세 설명', 104 | button_list=[ 105 | Button.ButtonText('쓰레드 넘김', 'PassThread'), 106 | Button.ButtonText('타이핑 액션', 'TYPING_ON'), 107 | Button.ButtonText('프로필 보기', 'Profile') 108 | ] 109 | ), 110 | Template.Composite( 111 | title='링크 버튼을 담은 카드뷰', 112 | description='이건 회색 글씨로 나온다!', 113 | button_list=[ 114 | Button.ButtonLink('nta github page', 'https://github.com/HwangWonYo/naver_talk_sdk'), 115 | Button.ButtonLink('네이버 파트너 센터', 'https://partner.talk.naver.com/'), 116 | Button.ButtonText('ElementList 카드뷰', 'ElementListCardView') 117 | ] 118 | ), 119 | Template.Composite( 120 | title='세번째 카드 리스트', 121 | description='1. Time Component', 122 | button_list=[ 123 | Button.ButtonTime('시간을 눌러봅시다.', code='Time_Test'), 124 | Button.ButtonCalendar( 125 | '날짜를 선택해봅시다.', 126 | code='Calendar_Test', 127 | placeholder='글자 수 제한이 있을까? 이 친구는 5월만 고를 수 있어. 또 일요일은 빼자', 128 | start='20180501', 129 | end='20180531', 130 | disables='0' 131 | ), 132 | Button.ButtonTimeInterval( 133 | '시간을 선택해봅시다.', 134 | code='TimeInterval_test' 135 | ) 136 | ] 137 | ), 138 | Template.Composite( 139 | title='네번째 카드 리스트', 140 | description='New Payload !', 141 | button_list=[ 142 | Button.ButtonText('Product Message 보기', 'SHOW_PRODUCT_MESSAGE') 143 | ] 144 | ) 145 | ] 146 | ) 147 | ) 148 | 149 | 150 | @ntalk.callback(['SHOW_PRODUCT_MESSAGE']) 151 | def product_message_handler(event): 152 | user_id = event.user_id 153 | ntalk.product_message( 154 | user_id, 155 | ids=[ 156 | 12345, 157 | 12344, 158 | 12343 159 | ], 160 | display_type='list' 161 | ) 162 | 163 | 164 | @ntalk.callback(['TimeInterval_test']) 165 | def time_interval_handler(event): 166 | user_id = event.user_id 167 | text = event.text 168 | ntalk.send( 169 | user_id, 170 | '선택하신 시간은 {} 입니다.'.format(text), 171 | quick_reply=Template.QuickReply( 172 | [Button.ButtonText('카드뷰 보기', 'CardView')] 173 | ) 174 | ) 175 | 176 | 177 | @ntalk.callback(['Time_Test']) 178 | def time_component_handler(event): 179 | user_id = event.user_id 180 | text = event.text 181 | ntalk.send( 182 | user_id, 183 | '선택하신 시간은 {} 이군요'.format(text), 184 | quick_reply=Template.QuickReply( 185 | [Button.ButtonText('카드뷰 보기', 'CardView')] 186 | ) 187 | ) 188 | 189 | 190 | @ntalk.callback(['Calendar_Test']) 191 | def calendar_handler(event): 192 | user_id = event.user_id 193 | text = event.text 194 | ntalk.send( 195 | user_id, 196 | '선택하신 날짜는 {} 이군요.'.format(text), 197 | quick_reply=Template.QuickReply( 198 | [ 199 | Button.ButtonText('카드뷰 보기', 'CardView'), 200 | Button.ButtonCalendar( 201 | title='날짜 다시 선택하기', 202 | code='Calendar_Test', 203 | placeholder='다시 선택 하는 거다', 204 | start='20180305', 205 | end='20180417', 206 | disables="1" 207 | ) 208 | ] 209 | ) 210 | ) 211 | 212 | 213 | @ntalk.callback(['ElementListCardView']) 214 | def show_element_list_card_view(event): 215 | user_id = event.user_id 216 | ntalk.send( 217 | user_id, 218 | message=Template.CompositeContent( 219 | composite_list=[ 220 | Template.Composite( 221 | title='엘리먼트 리스트 카드뷰 1', 222 | description='element는 3개까지 가능', 223 | element_list=Template.ElementList( 224 | data=[ 225 | Template.ElementData( 226 | title='쓰레드 넘김', 227 | description='파트너에게 쓰레드를 넘긴다', 228 | sub_description='그러면 standby는 True', 229 | button=Button.ButtonText('쓰레드 넘김', 'PassThread') 230 | ), 231 | Template.ElementData( 232 | title='타이핑 액션', 233 | description='다른 입력이 들어오면 꺼진다', 234 | sub_description='10초간 지속된다.', 235 | button=Button.ButtonText('타이핑 액션', 'TYPING_ON') 236 | ), 237 | Template.ElementData( 238 | title='프로필 보기', 239 | description='프로필 이벤트가 발생한다.', 240 | sub_description='이벤트로 발생해서 사용하기 어려움...', 241 | button=Button.ButtonText('프로필 보기', 'Profile') 242 | ), 243 | ] 244 | ) 245 | ), 246 | Template.Composite( 247 | title='엘리먼트 리스트 카드뷰 2', 248 | description='element는 3개까지 가능', 249 | element_list=Template.ElementList( 250 | data=[ 251 | Template.ElementData( 252 | title='nta 깃헙 페이지', 253 | description='파이썬 개발시 용이하다', 254 | sub_description='쓸만하다', 255 | button=Button.ButtonLink('nta github page', 'https://github.com/HwangWonYo/naver_talk_sdk'), 256 | ), 257 | Template.ElementData( 258 | title='파트너 센터 페이지', 259 | description='네이버 톡톡 챗봇의 시작', 260 | sub_description='계정은 각자 만들어야 한다', 261 | button=Button.ButtonLink('네이버 파트너 센터', 'https://partner.talk.naver.com/'), 262 | ), 263 | Template.ElementData( 264 | title='카드뷰', 265 | description='카드뷰로 보여준다', 266 | sub_description='사용은 각자에게 달려있다', 267 | button=Button.ButtonText('카드뷰', 'CardView') 268 | ), 269 | ] 270 | ) 271 | ) 272 | ] 273 | ) 274 | ) 275 | 276 | 277 | @ntalk.callback(['TYPING_ON']) 278 | def action_typing(event): 279 | print("Activate typing_on") 280 | user_id = event.user_id 281 | ntalk.typing_on(user_id) 282 | 283 | 284 | @ntalk.callback(['PassThread']) 285 | def thread_pass(event): 286 | print('pass thread') 287 | user_id = event.user_id 288 | ntalk.pass_thread( 289 | user_id=user_id, 290 | ) 291 | ntalk.send( 292 | user_id, 293 | "쓰레드 넘기기 성공\n" 294 | "따라하기 기능이 불가능합니다." 295 | ) 296 | 297 | 298 | @ntalk.callback(['TakeThread']) 299 | def thread_take(event): 300 | print('take thread') 301 | user_id = event.user_id 302 | ntalk.take_thread( 303 | user_id=user_id, 304 | ) 305 | ntalk.send( 306 | user_id, 307 | "쓰레드 반환 받기 성공\n" 308 | "다시 따라하기 기능이 가능합니다." 309 | ) 310 | 311 | 312 | @ntalk.callback(['PersistentMenu']) 313 | def persistent_menu_handler(event): 314 | user_id = event.user_id 315 | ntalk.send(user_id, "고정메뉴를 건드렸구나") 316 | 317 | 318 | @ntalk.callback(['Profile']) 319 | def show_profile(event): 320 | user_id = event.user_id 321 | ntalk.request_profile( 322 | user_id, 323 | field="nickname", 324 | agreements=["cellphone", "address"] 325 | ) 326 | 327 | 328 | @ntalk.handle_friend 329 | def friend_event_handler(event): 330 | user_id = event.user_id 331 | if event.set_on: 332 | message = "친구추가해줘서 고마워" 333 | else: 334 | message = "우정이 이렇게 쉽게 깨지는 거였나.." 335 | 336 | ntalk.send(user_id, message) 337 | 338 | 339 | @ntalk.handle_handover 340 | def hand_over_handler(event): 341 | user_id = event.user_id 342 | ntalk.send( 343 | user_id, 344 | "이제 주도권은 나의 손에!" 345 | ) 346 | 347 | 348 | @ntalk.handle_profile 349 | def profile_handler(event): 350 | user_id = event.user_id 351 | nickname = event.nickname 352 | ntalk.send( 353 | user_id, 354 | "안녕 {}".format(nickname) 355 | ) 356 | 357 | 358 | @ntalk.after_send 359 | def do_something_after_send(res, payload): 360 | print('* Message Type: %s' % payload.__class__.__name__) 361 | 362 | 363 | if __name__ == "__main__": 364 | app.run() -------------------------------------------------------------------------------- /nta/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | nta 3 | ~~~ 4 | 5 | :copyright: (c) 2017 by Wonyo Hwang. hollal0726@gmail.com 6 | :license: MIT, see LICENSE for more details. 7 | 8 | """ 9 | import re 10 | import requests 11 | import json 12 | 13 | from .exceptions import NaverTalkApiError, NaverTalkApiConnectionError 14 | from .models.responses import NaverTalkResponse, NaverTalkImageResponse 15 | from .models.payload import ( 16 | ProfilePayload, GenericPayload, ImageUploadPayload, ThreadPayload, ActionPayload, 17 | PersistentMenuPayload, ProductMessage 18 | ) 19 | from .models.events import * 20 | 21 | from .utils import LOGGER, PY3, _byteify 22 | 23 | 24 | class WebhookParser(object): 25 | """Webhook Parser. 26 | WebhookParser for parsing json request from navertalk. 27 | It returns parsed data in an Event instance 28 | with snake case attributes. 29 | """ 30 | 31 | def parse(self, req): 32 | """ 33 | Parse webhook request 34 | and change into the Event instance 35 | 36 | Args: 37 | - req: request body from navertalk 38 | 39 | Returns: 40 | - event: Event instance in mdoels.events 41 | """ 42 | if not PY3: 43 | req_json = json.loads(req, object_hook=_byteify) 44 | else: 45 | req_json = json.loads(req) 46 | 47 | event_type = req_json['event'] 48 | if event_type == 'open': 49 | event = OpenEvent.new_from_json_dict(req_json) 50 | elif event_type == 'send': 51 | event = SendEvent.new_from_json_dict(req_json) 52 | elif event_type == 'leave': 53 | event = LeaveEvent.new_from_json_dict(req_json) 54 | elif event_type == 'friend': 55 | event = FriendEvent.new_from_json_dict(req_json) 56 | elif event_type == 'echo': 57 | event = EchoEvent.new_from_json_dict(req_json) 58 | elif event_type == 'pay_complete': 59 | event = PayCompleteEvent.new_from_json_dict(req_json) 60 | elif event_type == 'pay_confirm': 61 | event = PayConfirmEvent.new_from_json_dict(req_json) 62 | elif event_type == 'profile': 63 | event = ProfileEvent.new_from_json_dict(req_json) 64 | elif event_type == 'handover': 65 | event = HandOverEvent.new_from_json_dict(req_json) 66 | else: 67 | LOGGER.warn('Unknown event type: %s' % event_type) 68 | event = None 69 | 70 | return event 71 | 72 | 73 | class NaverTalkApi(object): 74 | """NaverTalk Webhook Agent""" 75 | 76 | DEFAULT_API_ENDPOINT = 'https://gw.talk.naver.com/chatbot/v1/event' 77 | 78 | def __init__(self, naver_talk_access_token, endpoint=DEFAULT_API_ENDPOINT, **options): 79 | """ __init__ method. 80 | 81 | Args: 82 | - naver_talk_access_token: issued access_token 83 | - endpoint: endpoint to post request 84 | """ 85 | 86 | self._endpoint = endpoint 87 | self._headers = { 88 | 'Content-type': 'application/json;charset=UTF-8', 89 | 'Authorization': naver_talk_access_token 90 | } 91 | self.parser = WebhookParser() 92 | 93 | _webhook_handlers = {} 94 | _button_callbacks = {} 95 | _button_callbacks_key_regex = {} 96 | _default_button_callback = None 97 | _before_process = None 98 | _after_send = None 99 | 100 | def _call_handler(self, name, event): 101 | """ 102 | Call handler for event matched by name. 103 | 104 | Args: 105 | - name: event name 106 | - event: event to be handled 107 | """ 108 | if name in self._webhook_handlers: 109 | func = self._webhook_handlers[name] 110 | func(event) 111 | else: 112 | LOGGER.warn('No matching %s event handler' % name) 113 | 114 | def webhook_handler(self, req): 115 | """Handle webhook. 116 | 117 | :param req: Webhook request body (as text) 118 | """ 119 | event = self.parser.parse(req) 120 | if event is not None: 121 | if self._before_process: 122 | self._before_process(event) 123 | if isinstance(event, SendEvent): 124 | if event.is_code: 125 | _matched_callbacks = self.get_code_callbacks(event.code) 126 | for callback in _matched_callbacks: 127 | callback(event) 128 | name = event.event 129 | self._call_handler(name, event) 130 | 131 | def send(self, user_id, message, quick_reply=None, notification=False, read_by_send=False, callback=None): 132 | """ 133 | Send a message to user_id with quick_reply or not. 134 | If notification True, push alarm occurred on user's phone. 135 | Callback function is invoked after sending the message to user is Success. 136 | 137 | Args: 138 | - user_id: Navertalk user_id. 139 | - message: Instances in Template or str are allowed 140 | - quick_reply: add quickReply end of contents. 141 | - notification: on push alarm if True 142 | - callback: Do something after send a message 143 | """ 144 | if not PY3: 145 | if isinstance(message, unicode): 146 | message = _byteify(message) 147 | 148 | payload = GenericPayload( 149 | user=user_id, 150 | message=message, 151 | quick_reply=quick_reply, 152 | read_by_send=read_by_send, 153 | notification=notification 154 | ) 155 | 156 | self._send(payload, callback=callback) 157 | 158 | def _send(self, payload, callback=None, response_form=NaverTalkResponse): 159 | """ 160 | Request Post to Navertalktalk. 161 | """ 162 | data = payload.as_json_string() 163 | r = requests.post(self._endpoint, 164 | data=data, 165 | headers=self._headers) 166 | 167 | if r.status_code != requests.codes.ok: 168 | raise NaverTalkApiConnectionError(r) 169 | 170 | res = response_form.new_from_json_dict(r.json()) 171 | self.__error_check(res) 172 | 173 | if callback is not None: 174 | callback(res, payload) 175 | 176 | if self._after_send: 177 | self._after_send(res, payload) 178 | 179 | def request_profile(self, user_id, field, agreements=None, callback=None): 180 | """ 181 | Request user's profile with user_id, and agreement fields. 182 | 183 | Args: 184 | - user_id: target user's id 185 | - field: target user info nickname|cellphone|addreess 186 | - agreements: list of user's info nickname|cellphone|addreess 187 | """ 188 | payload = ProfilePayload( 189 | user=user_id, 190 | field=field, 191 | agreements=agreements 192 | ) 193 | 194 | self._send(payload, callback=callback) 195 | 196 | def upload_image(self, image_url, callback=None): 197 | """ 198 | Upload image with url to navertalk and recieve an Image Id. 199 | 200 | Args: 201 | - image_url: imaegUrl to imageId 202 | - callback: function callback after image upload request. 203 | the function will recieve models.NaverTalkImageResponse and payload. 204 | """ 205 | payload = ImageUploadPayload( 206 | image_url 207 | ) 208 | 209 | self._send(payload, callback=callback, response_form=NaverTalkImageResponse) 210 | 211 | def take_thread(self, user_id, callback=None): 212 | """ 213 | take thread from partner for a user's conversation with user_id 214 | 215 | Args: 216 | - user_id: target user 217 | - callback: function callback 218 | """ 219 | payload = ThreadPayload( 220 | user=user_id, 221 | control="takeThread" 222 | ) 223 | 224 | self._send(payload, callback=callback) 225 | 226 | def pass_thread(self, user_id, callback=None): 227 | """ 228 | pass thread to partner for a user's conversation with user_id 229 | 230 | Args: 231 | - user_id: target user 232 | - callback: function callback 233 | """ 234 | payload = ThreadPayload( 235 | user=user_id, 236 | control="passThread" 237 | ) 238 | 239 | self._send(payload, callback=callback) 240 | 241 | def typing_on(self, user_id, callback=None): 242 | """ 243 | make typing_on action on 244 | 245 | Args: 246 | - user_id: target user 247 | - callback: function callback 248 | """ 249 | payload = ActionPayload( 250 | user=user_id, 251 | action="typingOn" 252 | ) 253 | 254 | self._send(payload, callback=callback) 255 | 256 | def typing_off(self, user_id, callback=None): 257 | """ 258 | make typing_on action off 259 | 260 | Args: 261 | - user_id: target user 262 | - callback: function callback 263 | """ 264 | payload = ActionPayload( 265 | user=user_id, 266 | action="typingOff" 267 | ) 268 | 269 | self._send(payload, callback=callback) 270 | 271 | def persistent_menu(self, menus=None, callback=None): 272 | """ 273 | enroll persistent menu. 274 | 275 | Args: 276 | - menus: List of Buttons 277 | """ 278 | payload = PersistentMenuPayload(menus) 279 | 280 | self._send(payload, callback=callback) 281 | 282 | def product_message(self, user_id, ids, display_type=None, callback=None): 283 | """ 284 | show product message 285 | 286 | Args: 287 | - ids: List of Product ids. 288 | - display_type: list | single 289 | """ 290 | payload = ProductMessage( 291 | user=user_id, 292 | ids=ids, 293 | display_type=display_type 294 | ) 295 | 296 | self._send(payload, callback=callback) 297 | 298 | """ 299 | Decorators 300 | for Handling each events. 301 | """ 302 | def handle_open(self, func): 303 | """open decorator""" 304 | self._webhook_handlers['open'] = func 305 | 306 | def handle_send(self, func): 307 | """send decorator""" 308 | self._webhook_handlers['send'] = func 309 | 310 | def handle_leave(self, func): 311 | """leave decorator""" 312 | self._webhook_handlers['leave'] = func 313 | 314 | def handle_friend(self, func): 315 | """friend decorator""" 316 | self._webhook_handlers['friend'] = func 317 | 318 | def handle_profile(self, func): 319 | """profile decorator""" 320 | self._webhook_handlers['profile'] = func 321 | 322 | def handle_pay_complete(self, func): 323 | """payComplete decorator""" 324 | self._webhook_handlers['pay_complete'] = func 325 | 326 | def handle_pay_confirm(self, func): 327 | """payConfirm decorator""" 328 | self._webhook_handlers['pay_confirm'] = func 329 | 330 | def handle_echo(self, func): 331 | """echo decorator""" 332 | self._webhook_handlers['echo'] = func 333 | 334 | def handle_handover(self, func): 335 | """handover decorator""" 336 | self._webhook_handlers['handover'] = func 337 | 338 | def before_proccess(self, func): 339 | """before_proccess decorator. 340 | Decorated function which is invoked ahead of all event handlers. 341 | """ 342 | self._before_process = func 343 | 344 | def after_send(self, func): 345 | """after_send decorator. 346 | Decorated function will be invoked after sending each message. 347 | """ 348 | self._after_send = func 349 | 350 | def callback(self, *args): 351 | """ 352 | Callback wrapper for handling code value. 353 | Regular expression can be used for mathching with code value. 354 | 355 | Args: callbale or list of target code values 356 | """ 357 | def wrapper(func): 358 | if not isinstance(args[0], list): 359 | raise ValueError("Callback params must be List") 360 | for arg in args[0]: 361 | self._button_callbacks[arg] = func 362 | self._button_callbacks_key_regex[arg] = re.compile(arg + '$') 363 | 364 | if not callable(args[0]): 365 | return wrapper 366 | 367 | self._default_button_callback = args[0] 368 | 369 | def get_code_callbacks(self, code): 370 | """ 371 | find callback handlers matched by code value with regular expression. 372 | 373 | Args: 374 | - code: code value from a button. 375 | 376 | Return: 377 | - callbacks: function callbacaks matched by code value 378 | """ 379 | callbacks = [] 380 | for key in self._button_callbacks.keys(): 381 | if self._button_callbacks_key_regex[key].match(code): 382 | callbacks.append(self._button_callbacks[key]) 383 | 384 | if not callbacks: 385 | if self._default_button_callback is not None: 386 | callbacks.append(self._default_button_callback) 387 | return callbacks 388 | 389 | def __error_check(self, response): 390 | """ 391 | check error from navertalk. 392 | When recieved success: false, raise NaverTalkApiError. 393 | """ 394 | if not response.success: 395 | raise NaverTalkApiError(response) -------------------------------------------------------------------------------- /tests/api/test_webhook_handler.py: -------------------------------------------------------------------------------- 1 | #-*- encoding:utf8 -*- 2 | import json 3 | import responses 4 | import unittest 5 | try: 6 | from unittest import mock 7 | except: 8 | import mock 9 | 10 | from nta import ( 11 | NaverTalkApi 12 | ) 13 | from nta.models.events import * 14 | from nta.models import( 15 | NaverTalkResponse, GenericPayload 16 | ) 17 | 18 | 19 | class TestNaverTalkApi(unittest.TestCase): 20 | def setUp(self): 21 | self.tested = NaverTalkApi('test_naver_talk_access_token') 22 | 23 | def test_open_event(self): 24 | event = { 25 | 'event': 'open', 26 | 'user': 'test_user_id', 27 | 'options': { 28 | 'inflow': 'list', 29 | 'referer': 'https://example.com', 30 | 'friend': True, 31 | 'under14': False, 32 | 'under19': False 33 | } 34 | } 35 | counter1 = mock.MagicMock() 36 | @self.tested.handle_open 37 | def test_handle_open(event): 38 | self.assertTrue(isinstance(event, OpenEvent)) 39 | self.assertEqual('test_user_id', event.user_id) 40 | self.assertEqual('list', event.inflow) 41 | self.assertEqual('https://example.com', event.referer) 42 | self.assertFalse(event.under_14) 43 | self.assertFalse(event.under_19) 44 | self.assertTrue(event.friend) 45 | counter1() 46 | 47 | self.tested.webhook_handler(json.dumps(event)) 48 | self.assertEqual(counter1.call_count, 1) 49 | 50 | def test_leave_event(self): 51 | event = { 52 | 'event': 'leave', 53 | 'user': 'test_user_id' 54 | } 55 | counter = mock.MagicMock() 56 | 57 | @self.tested.handle_leave 58 | def handle_leave(event): 59 | self.assertTrue(isinstance(event, LeaveEvent)) 60 | self.assertEqual('test_user_id', event.user_id) 61 | counter() 62 | 63 | self.tested.webhook_handler(json.dumps(event)) 64 | self.assertEqual(counter.call_count, 1) 65 | 66 | def test_friend_event(self): 67 | event = { 68 | 'event': 'friend', 69 | 'user': 'test_user_id', 70 | 'options': { 71 | 'set': 'on' 72 | } 73 | } 74 | 75 | counter = mock.MagicMock() 76 | 77 | @self.tested.handle_friend 78 | def handle_friend(event): 79 | self.assertTrue(isinstance(event, FriendEvent)) 80 | self.assertEqual('test_user_id', event.user_id) 81 | self.assertTrue(event.set_on) 82 | counter() 83 | 84 | self.tested.webhook_handler(json.dumps(event)) 85 | self.assertEqual(counter.call_count, 1) 86 | 87 | def test_send_event_text(self): 88 | event = { 89 | 'event': 'send', 90 | 'user': 'test_user_id', 91 | 'textContent': { 92 | 'text': 'test_text', 93 | 'code': 'test_code', 94 | 'inputType': 'typing' 95 | } 96 | } 97 | counter = mock.MagicMock() 98 | 99 | @self.tested.handle_send 100 | def send_handler(event): 101 | self.assertTrue(isinstance(event, SendEvent)) 102 | self.assertEqual('test_user_id', event.user_id) 103 | self.assertEqual('test_text', event.text) 104 | self.assertEqual('test_code', event.code) 105 | self.assertEqual('typing', event.input_type) 106 | self.assertTrue(event.is_code) 107 | self.assertFalse(event.standby) 108 | counter() 109 | 110 | self.tested.webhook_handler(json.dumps(event)) 111 | self.assertEqual(counter.call_count, 1) 112 | 113 | 114 | def test_send_event_image(self): 115 | event = { 116 | 'event': 'send', 117 | 'user': 'test_user_id', 118 | 'imageContent': { 119 | 'imageUrl': 'https://test.image.jpg' 120 | } 121 | } 122 | counter = mock.MagicMock() 123 | 124 | @self.tested.handle_send 125 | def send_handler(event): 126 | self.assertTrue(isinstance(event, SendEvent)) 127 | self.assertEqual('test_user_id', event.user_id) 128 | self.assertEqual('https://test.image.jpg', event.image_url) 129 | self.assertIsNone(event.text) 130 | counter() 131 | 132 | self.tested.webhook_handler(json.dumps(event)) 133 | self.assertEqual(counter.call_count, 1) 134 | 135 | def test_echo_event(self): 136 | event = { 137 | 'event': 'echo', 138 | 'echoedEvent': 'send', 139 | 'user': 'test_user_id', 140 | 'partner': 'testyo', 141 | 'imageContent': { 142 | 'imageUrl': 'https://example_image.png' 143 | }, 144 | 'textContent':{ 145 | 'text':'text_from_test' 146 | }, 147 | 'compositeContent': { 148 | 'compositeList':[] 149 | }, 150 | 'options': { 151 | 'mobile': False 152 | } 153 | } 154 | counter = mock.MagicMock() 155 | 156 | @self.tested.handle_echo 157 | def echo_handler(event): 158 | self.assertTrue(isinstance(event, EchoEvent)) 159 | self.assertEqual('test_user_id', event.user_id) 160 | self.assertEqual('send', event.echoed_event) 161 | self.assertEqual({'text':'text_from_test'}, event.text_content) 162 | self.assertEqual({'composite_list':[]}, event.composite_content) 163 | self.assertEqual({'image_url': 'https://example_image.png'}, event.image_content) 164 | self.assertFalse(event.mobile) 165 | counter() 166 | 167 | self.tested.webhook_handler(json.dumps(event)) 168 | self.assertEqual(counter.call_count, 1) 169 | 170 | def test_profile_event(self): 171 | event = { 172 | 'event': 'profile', 173 | 'user': 'test_user_id', 174 | 'options': { 175 | 'nickname': 'test_won_yo', 176 | 'cellphone': '01012345678', 177 | 'address': { 178 | 'roadAddr': 'Seoul' 179 | }, 180 | 'result': 'SUCCESS' 181 | } 182 | } 183 | counter = mock.MagicMock() 184 | 185 | @self.tested.handle_profile 186 | def profile_handler(event): 187 | self.assertTrue(isinstance(event, ProfileEvent)) 188 | self.assertEqual('test_user_id', event.user_id) 189 | self.assertEqual('test_won_yo', event.nickname) 190 | self.assertEqual('01012345678', event.cellphone) 191 | self.assertEqual({'road_addr': 'Seoul'}, event.address) 192 | self.assertEqual('SUCCESS', event.result) 193 | counter() 194 | 195 | self.tested.webhook_handler(json.dumps(event)) 196 | self.assertEqual(counter.call_count, 1) 197 | 198 | def test_pay_complete_event(self): 199 | event = { 200 | 'event': 'pay_complete', 201 | 'user': 'test_user_id', 202 | 'options': { 203 | 'paymentResult': { 204 | 'code': 'Success', 205 | 'paymentId': 'test-payment-id', 206 | 'merchantPayKey': 'test_merchant-pay-key', 207 | 'merchantUserKey': 'test-merchant-user-key' 208 | } 209 | } 210 | } 211 | counter = mock.MagicMock() 212 | 213 | @self.tested.handle_pay_complete 214 | def pay_complete_handler(event): 215 | self.assertTrue(isinstance(event, PayCompleteEvent)) 216 | self.assertEqual('Success', event.code) 217 | self.assertEqual('test-payment-id', event.payment_id) 218 | self.assertEqual('test_merchant-pay-key', event.merchant_pay_key) 219 | self.assertEqual('test-merchant-user-key', event.merchant_user_key) 220 | self.assertIsNone(event.message) 221 | counter() 222 | 223 | self.tested.webhook_handler(json.dumps(event)) 224 | self.assertEqual(counter.call_count, 1) 225 | 226 | 227 | def test_pay_confirm_event(self): 228 | event = { 229 | 'event': 'pay_confirm', 230 | 'user': 'test_user_id', 231 | 'options': { 232 | 'paymentConfirmResult': { 233 | 'code': 'Success', 234 | 'message': 'test_message', 235 | 'paymentId': 'test-payment-id', 236 | 'detail': {} 237 | } 238 | } 239 | } 240 | counter = mock.MagicMock() 241 | 242 | @self.tested.handle_pay_confirm 243 | def pay_complete_handler(event): 244 | self.assertTrue(isinstance(event, PayConfirmEvent)) 245 | self.assertEqual('Success', event.code) 246 | self.assertEqual('test_message', event.message) 247 | self.assertEqual('test-payment-id', event.payment_id) 248 | self.assertEqual({}, event.detail) 249 | counter() 250 | 251 | self.tested.webhook_handler(json.dumps(event)) 252 | self.assertEqual(counter.call_count, 1) 253 | 254 | def test_handover_event(self): 255 | event = { 256 | "event": "handover", 257 | "user": "test_user", 258 | "partner": "wc1234", 259 | "options": { 260 | "control": "passThread", 261 | "metadata": "{\"managerNickname\":\"파트너닉네임\",\"autoEnd\":false}" 262 | } 263 | } 264 | counter = mock.MagicMock() 265 | 266 | @self.tested.handle_handover 267 | def hanover_event_handler(event): 268 | self.assertTrue(isinstance(event, HandOverEvent)) 269 | self.assertEqual('test_user', event.user_id) 270 | self.assertEqual('passThread', event.control) 271 | self.assertEqual("{\"managerNickname\":\"파트너닉네임\",\"autoEnd\":false}", event.metadata) 272 | counter() 273 | 274 | self.tested.webhook_handler(json.dumps(event)) 275 | self.assertEqual(counter.call_count, 1) 276 | 277 | def test_standby_true(self): 278 | event = { 279 | 'standby': True, 280 | 'event': 'send', 281 | 'user': 'test_user_id', 282 | 'textContent': { 283 | 'text': 'test_text', 284 | 'code': 'test_code', 285 | 'inputType': 'typing' 286 | }, 287 | 'options': { 288 | 'mobile': False 289 | } 290 | } 291 | counter = mock.MagicMock() 292 | 293 | @self.tested.handle_send 294 | def send_handler(event): 295 | self.assertTrue(event.standby) 296 | counter() 297 | 298 | self.tested.webhook_handler(json.dumps(event)) 299 | self.assertEqual(counter.call_count, 1) 300 | 301 | @responses.activate 302 | def test_after_send(self): 303 | responses.add( 304 | responses.POST, 305 | self.tested.DEFAULT_API_ENDPOINT, 306 | json={ 307 | 'success': True, 308 | 'resultCode': '00' 309 | }, 310 | status=200 311 | ) 312 | 313 | counter = mock.MagicMock() 314 | @self.tested.after_send 315 | def after_send_handler(res, payload): 316 | self.assertTrue(isinstance(res, NaverTalkResponse)) 317 | self.assertTrue(isinstance(payload, GenericPayload)) 318 | counter() 319 | 320 | self.tested.send('test_user_id', 'test_text') 321 | self.assertEqual(counter.call_count, 1) 322 | 323 | def test_before_proccess(self): 324 | event = { 325 | 'event': 'leave', 326 | 'user': 'test_user_id' 327 | } 328 | 329 | counter = mock.MagicMock() 330 | 331 | @self.tested.before_proccess 332 | def before_proccess_handler(event): 333 | counter() 334 | 335 | self.tested.webhook_handler(json.dumps(event)) 336 | self.assertEqual(counter.call_count, 1) 337 | 338 | def test_callback(self): 339 | event = { 340 | 'event': 'send', 341 | 'user': 'test_user_id', 342 | 'textContent': { 343 | 'text': 'test_text', 344 | 'code': 'test_code', 345 | 'inputType': 'typing' 346 | } 347 | } 348 | 349 | counter1 = mock.MagicMock() 350 | @self.tested.callback 351 | def default_callback(event): 352 | self.assertEqual('test_code', event.code) 353 | counter1() 354 | 355 | @self.tested.handle_send 356 | def send_event_handler(event): 357 | if event.is_code: 358 | return 359 | counter1() 360 | 361 | self.tested.webhook_handler(json.dumps(event)) 362 | self.assertEqual(counter1.call_count, 1) 363 | 364 | event2 = { 365 | 'event': 'send', 366 | 'user': 'test_user_id', 367 | 'textContent': { 368 | 'text': 'test_text', 369 | 'code': 'Hello my name is wonyo', 370 | 'inputType': 'typing' 371 | } 372 | } 373 | 374 | counter2 = mock.MagicMock() 375 | @self.tested.callback([('(^Hello).*')]) 376 | def callback_handler(event): 377 | self.assertEqual('Hello my name is wonyo', event.code) 378 | counter2() 379 | 380 | self.tested.webhook_handler(json.dumps(event2)) 381 | self.assertEqual(counter1.call_count, 1) 382 | 383 | event3 = { 384 | 'event': 'send', 385 | 'user': 'test_user_id', 386 | 'textContent': { 387 | 'text': 'test_text', 388 | 'code': '1', 389 | 'inputType': 'typing' 390 | } 391 | } 392 | 393 | counter3 = mock.MagicMock() 394 | @self.tested.callback(['1', '2', '3']) 395 | def callback_handler(event): 396 | self.assertEqual('1', event.code) 397 | counter3() 398 | 399 | self.tested.webhook_handler(json.dumps(event3)) 400 | self.assertEqual(counter3.call_count, 1) 401 | 402 | def test_unknown_event(self): 403 | event = { 404 | 'event': 'unknown' 405 | } 406 | counter = mock.MagicMock() 407 | 408 | @self.tested.handle_open 409 | def handle_open(event): 410 | counter() 411 | 412 | @self.tested.handle_leave 413 | def handle_open(event): 414 | counter() 415 | 416 | @self.tested.handle_send 417 | def handle_open(event): 418 | counter() 419 | 420 | @self.tested.handle_friend 421 | def handle_open(event): 422 | counter() 423 | 424 | @self.tested.handle_echo 425 | def handle_open(event): 426 | counter() 427 | 428 | @self.tested.handle_pay_complete 429 | def handle_open(event): 430 | counter() 431 | 432 | @self.tested.handle_pay_confirm 433 | def handle_open(event): 434 | counter() 435 | 436 | @self.tested.handle_profile 437 | def handle_open(event): 438 | counter() 439 | 440 | 441 | self.tested.webhook_handler(json.dumps(event)) 442 | self.assertEqual(counter.call_count, 0) 443 | 444 | 445 | 446 | if __name__ == '__main__': 447 | unittest.main() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # naver_talk_sdk 2 | [![PyPI](https://img.shields.io/pypi/v/nta.svg?v=1&maxAge=3601)](https://pypi.python.org/pypi/nta) 3 | [![Coverage Status](https://coveralls.io/repos/github/hwonyo/naver_talk_sdk/badge.svg?branch=master)](https://coveralls.io/github/hwonyo/naver_talk_sdk?branch=master) 4 | [![PyPI](https://img.shields.io/pypi/l/nta.svg?v=1&maxAge=2592000?)](https://pypi.python.org/pypi/nta) 5 | 6 | SDK of NAVER TALK API for Python 7 | 8 | 9 | __Inspired By : [fbmq](https://github.com/conbus/fbmq) and [line-bot-sdk](https://github.com/line/line-bot-sdk-python)__
10 | 11 | > 네이버 톡톡 파이썬 모듈입니다.
12 | > 톡톡에서 발생하는 "__이벤트를 중심__"으로 효율적인 코드를 짤 수 있습니다.
13 | > [callback handler](#callback)를 이용하면 버튼의 payload 값에 따라 처리를 분기할 수 있습니다.
14 | > 네이버 톡톡 소개 블로그: http://hollal0726.blogspot.kr/2018/04/nta.html 15 | 16 | 17 | # About NAVERTALK Messaging API 18 | ## Table of Contents 19 | 20 | * [Install](#install) 21 | * [Run Unit Test](#Run-Unit-Test) 22 | * [Synopsis](#synopsis) 23 | * [API](#api) 24 | - [NaverTalkApi](#navertalkapi) 25 | - [handler](#handler) 26 | - [@handle_open](#handle_open) 27 | - [@handle_send](#handle_send) 28 | - [@handle_leave](#handle_leave) 29 | - [@handle_friend](#handle_friend) 30 | - [@handle_profile](#handle_profile) 31 | - [@handle_pay_complete](#handle_pay_complete) 32 | - [@handle_pay_confirm](#handle_pay_confirm) 33 | - [@handle_echo](#handle_echo) 34 | - [@handle_handover](#handle_handover) 35 | - [@handle_before_process](#handle_before_process) 36 | - [@after_send](#after_send) 37 | - [@callback](#callback) 38 | - [Send message](#send-message) 39 | - [Text](#text) 40 | - [Image](#image) 41 | - [CompositeContent](#compositecontent) 42 | - [quick reply](#quick-reply) 43 | - [Utility](#utility) 44 | - [take_thread](#take_threadself-user_id-callbacknone) 45 | - [pass_thread](#pass_threadself-user_id-callbacknone) 46 | - [typing_on](#typing_onself-user_id-callbacknone) 47 | - [typing_off](#typing_offself-user_id-callbacknone) 48 | - [persistent_menu](#persistent_menuself-menus-callbacknone) 49 | - [prdouct_message](#product_messageself-user_id-ids-displaytypesingle-callbacknone) 50 | - [Template](#template) 51 | - [TextContent](#textcontent) 52 | - [ImageContent](#imagecontent) 53 | - [CompositeContent](#compositecontent) 54 | - [Composite](#composite) 55 | - [ElementList](#elementlist) 56 | - [ElementData](#elementdata) 57 | - [QuickReply](#quickreply) 58 | - [PaymentInfo](#paymentinfo) 59 | - [ProductItem](#productitem) 60 | - [Buttons](#buttons) 61 | - [ButtonText](#buttontext) 62 | - [ButtonLink](#buttonlink) 63 | - [ButtonOption](#buttonoption) 64 | - [ButtonTime](#buttontime) 65 | - [ButtonNested](#buttonnested) 66 | - [ButtonCalendar](#buttoncalendar) 67 | - [ButtonTimeInterval](#buttontimeinterval) 68 | - [Exception](#exception) 69 | - [NaverTalkApiError](#navertalkapierror) 70 | - [NaverTalkApiConnectionError](#navertalkapiconnectionerror) 71 | - [NaverTalkPaymentError](#navertalkpaymenterror) 72 | - [Event](#event) 73 | - [OpenEvent](#openevent) 74 | - [LeaveEvent](#leaveevent) 75 | - [FriendEvent](#friendevent) 76 | - [SendEvent](#sendevent) 77 | - [EchoEvent](#echoevent) 78 | - [PayCompleteEvent](#paycompleteevent) 79 | - [PayConfirmEvent](#payconfirmevent) 80 | - [HandOverEvent](#handoverevent) 81 | 82 | ## Install 83 | install as a package 84 | ``` 85 | pip install nta 86 | ``` 87 | To run a dev environment 88 | ``` 89 | pip install -r requirements.txt 90 | export naver_talk_access_token='your_access_token_here' 91 | python example/example.py 92 | ``` 93 | 94 | ## Run Unit Test 95 | ``` 96 | python -m unittest 97 | ``` 98 | 99 | 100 | ## Synopsis 101 | Usage (with flask) 102 | 103 | ```python 104 | from flask import Flask, request 105 | from nta import NaverTalkApi, NaverTalkApiError 106 | from nta import Template 107 | 108 | 109 | app = Flask(__name__) 110 | ntalk = NaverTalkApi('your_naver_talk_access_token') 111 | 112 | 113 | @app.route('/', methods=['POST']) 114 | def message_handler(): 115 | try: 116 | ntalk.webhook_handler( 117 | request.get_data(as_text=True) 118 | ) 119 | except NaverTalkApiError as e: 120 | print(e) 121 | 122 | return "ok" 123 | 124 | 125 | @ntalk.handle_open 126 | def open_handler(event): 127 | """ 128 | :param event: events.OpenEvent 129 | """ 130 | user_id = event.user_id 131 | ntalk.send( 132 | user_id=user_id, 133 | message="Nice to meet you :)" 134 | ) 135 | 136 | @ntalk.handle_send 137 | def send_handler(event): 138 | """ 139 | :param event: events.SendEvent 140 | """ 141 | user_id = event.user_id 142 | text = event.text 143 | ntalk.send( 144 | user_id, 145 | "Echo Message: %s" % text 146 | ) 147 | ``` 148 | See more detail example code: [example/example.py](https://github.com/HwangWonYo/naver_talk_sdk/blob/master/example/example.py) 149 | 150 | And see also implemented naver talktalk agent: https://talk.naver.com/ct/wc4qdz 151 | 152 | # API 153 | 154 | * All attributes in Instances are same with snake case of the key name in json request from navertalk. 155 | * See more info: [Naver Talk Github Page](https://github.com/navertalk/chatbot-api) 156 | 157 | ## NaverTalkApi 158 | Create a new NaverTalk instance 159 | ```python 160 | ntalk = nta.NaverTalkApi('YOUR_NAVER_TALK_ACCESS_TOKEN') 161 | ``` 162 | 163 | ### handler 164 | 165 | Handle event from user with decorators 166 | 167 | Decorated Function takes [Event](#event) paramemter 168 | 169 | #### __@handle_open__ 170 | 171 | - Open Event Handler 172 | - [Open Event 정보](https://github.com/navertalk/chatbot-api#open-%EC%9D%B4%EB%B2%A4%ED%8A%B8) 173 | ```python 174 | @ntalk.handle_open 175 | def open_handler_function(event): 176 | user_id = event.user_id # str: 사용자 고유값 177 | inflow = event.inflow # str: 사용자 접근 방법 178 | refer = event.referer # str: 사용자 접근 url 179 | friend = event.friend # bool: 사용자 친구 여부 180 | under_14 = event.under_14 # bool: 사용자 14세 미만 여부 181 | under_19 = event.under_19 # bool: 사용자 19세 미만 여부 182 | mobile = event.mobile # bool: 모바일 사용 여부 183 | standby = event.standby # bool: 상담사와 연결된 경우 True 184 | ``` 185 | 186 | #### __@handle_send__ 187 | 188 | - Send Event Handler 189 | - [Send Event 정보](https://github.com/navertalk/chatbot-api#send-%EC%9D%B4%EB%B2%A4%ED%8A%B8) 190 | ```python 191 | @ntalk.handle_send 192 | def send_handler_function(event): 193 | user_id = event.user_id # str 194 | text = event.text # str: 사용자가 입력한 텍스트 195 | code = event.code # str: 사용자가 선택한 버튼의 값 196 | input_type = event.input_type # str: 사용자가 입력한 방식 197 | is_code = event.is_code # bool: code값 여부 198 | image_url = event.image_url # str: 사용자가 보낸 이미지 url 199 | mobile = event.mobile # bool: 모바일 사용 여부 200 | standby = event.standby # bool: 상담사와 연결된 경우 True 201 | ``` 202 | #### __@handle_leave__ 203 | 204 | - Leave Event Handler 205 | - [Leave Event 정보](https://github.com/navertalk/chatbot-api#leave-%EC%9D%B4%EB%B2%A4%ED%8A%B8) 206 | ```python 207 | @ntalk.handle_leave 208 | def leave_handler_function(event): 209 | user_id = event.user_id 210 | mobile = event.mobile # bool: 모바일 사용 여부 211 | standby = event.standby # bool: 상담사와 연결된 경우 True 212 | ``` 213 | 214 | #### __@handle_friend__ 215 | 216 | - Friend Event Handler 217 | - [Friend Event 정보](https://github.com/navertalk/chatbot-api#friend-%EC%9D%B4%EB%B2%A4%ED%8A%B8) 218 | ```python 219 | @ntalk.handle_friend 220 | def friend_handler_function(event): 221 | user_id = event.user_id 222 | set_on = event.set_on # bool: 친구추가 여부 223 | mobile = event.mobile # bool: 모바일 사용 여부 224 | standby = event.standby # bool: 상담사와 연결된 경우 True 225 | ``` 226 | #### __@handle_profile__ 227 | 228 | - Profile Event Handler 229 | - [Profile Event 정보](https://github.com/navertalk/chatbot-api/blob/master/profile_api_v1.md) 230 | ```python 231 | @ntalk.handle_profile 232 | def profile_handler_function(event): 233 | user_id = event.user_id 234 | result = event.result # str: 사용자 동의 결과 SUCCESS|DISAGREE|CANCEL 235 | nickname = event.nickname # str: 사용자 이름 or None 236 | cellphone = event.cellphone # str: 사용자 연락처 or None 237 | address = event.address # str: 사용자 주소 or None 238 | mobile = event.mobile # bool: 모바일 사용 여부 239 | standby = event.standby # bool: 상담사와 연결된 경우 True 240 | ``` 241 | 242 | #### __@handle_pay_complete__ 243 | 244 | - PayComplete Event Handler 245 | - [PayComplete Event 정보](https://github.com/navertalk/chatbot-api/blob/master/pay_api_v1.md#pay_complete-이벤트-구조) 246 | ```python 247 | @ntalk.handle_pay_complete 248 | def pay_complete_handler(event): 249 | user_id = event.user_id 250 | code = event.code # str: 페이 성공 여부 Success|Fail 251 | payment_id = event.payment_id # str 결제 성공시 결제번호 252 | merchant_pay_key = event.merchant_pay_key # str 253 | merchant_user_key = event.merchant_user_key # str 254 | message = event.message # str 결제 실패시 메세지 255 | mobile = event.mobile # bool: 모바일 사용 여부 256 | standby = event.standby # bool: 상담사와 연결된 경우 True 257 | ``` 258 | #### __@handle_pay_confirm__ 259 | 260 | - PayConfirm Event Handler 261 | - [PayComfirm Event 정보](https://github.com/navertalk/chatbot-api/blob/master/pay_api_v1.md#pay_confirm-이벤트) 262 | ```python 263 | @ntalk.handle_pay_confirm 264 | def pay_confirm_handler(event): 265 | user_id = event.user_id 266 | code = event.code # str 267 | message = event.message 268 | payment_id = event.payment_id 269 | detail = event.detail # 네이버페이 간편결제 승인 API 응답본문 detail 그대로 반환. 270 | mobile = event.mobile # bool: 모바일 사용 여부 271 | standby = event.standby # bool: 상담사와 연결된 경우 True 272 | ``` 273 | #### __@handle_echo__ 274 | 275 | - Echo Event Handler 276 | - [Echo Event 정보](https://github.com/navertalk/chatbot-api#echo-%EC%9D%B4%EB%B2%A4%ED%8A%B8) 277 | ```python 278 | @ntalk.handle_echo 279 | def echo_handler_function(event): 280 | user_id = event.user_id 281 | mobile = event.mobile # bool: 모바일 사용 여부 282 | standby = event.standby # bool: 상담사와 연결된 경우 True 283 | text_content = event.text_content # dict: text_content or None 284 | image_content = event.image_content # dict: image_content or None 285 | composite_content = event.composite_content # dict: composite_content or None 286 | pass 287 | ``` 288 | #### __@handle_handover__ 289 | 290 | - Handover Event Handler 291 | - [Handover Event 정보](https://github.com/navertalk/chatbot-api/blob/master/handover_v1.md) 292 | ```python 293 | @ntalk.handle_handover 294 | def handover_handler_function(event): 295 | user_id = event.user_id 296 | control = event.control # 주도권이 챗봇에게 넘어온 경우 (발생하는 이벤트의 컨트롤은 항상 passThread) 297 | metadata = event.metadata # 넘어오는 메타 데이터. 298 | ``` 299 | 300 | 301 | #### __@handle_before_process__ 302 | 303 | - Ahead of all event handler 304 | - 이벤트 handler를 사용하기 전에 실행되는 함수. (event 종류에 상관없이 실행된다.) 305 | ```python 306 | @ntalk.handler_before_process 307 | def before_process_function(event): 308 | user_id = event.user_id 309 | pass 310 | ``` 311 | 312 | #### __@after_send__ 313 | 314 | - Handler triggered after sending for each message to user 315 | - ntalk.send를 성공할 때 마다 실행 316 | - With two parameters [Response](#response) and [Payload](#payload) 317 | ```python 318 | @ntalk.after_send 319 | def do_something_after_send_for_each_message(res, payload): 320 | # do something you want 321 | pass 322 | ``` 323 | 324 | #### __@callback__ 325 | 326 | - Callback Handler triggered when user clicks button with code value. 327 | - After Callback Handling, [@handle_send](#handle_send) is activated. 328 | - Regular Expression can be used. 329 | ```python 330 | @ntalk.callback 331 | def calback_handler(event): 332 | user_id = event.user_id 333 | code = event.code 334 | 335 | @ntalk.callback(['(^Hello).*']) 336 | def hello_callback_handler(event): 337 | # This function will be triggered when a user hit the button contains code value starts with Hello 338 | code = event.code # ex) Hello Naver 339 | ``` 340 | 341 | ## Send message 342 | ### __send(self, user_id, message, quick_reply=None, notification=False, callback=None)__ 343 | 344 | - user_id *str*: 보내려는 유저의 고유 아이디 345 | - message *Template* or *str*: 전송하고자 하는 메세지 346 | - quick_reply *Template* or *list*: 빠른 답장 347 | - notification *bool*: 푸쉬 메세지 설정 348 | - readBySend *bool*: 자동 읽음으로 표시 설정 349 | - callback *func*: callback 함수. 메세지를 보내고 난 뒤에 실행된다. 350 | 351 | #### __Text__ 352 | 353 | ```python 354 | ntalk.send(user_id, "Hello Naver :)") 355 | ``` 356 | or 357 | 358 | ```python 359 | ntalk.send(user_id, Template.TextContent("Hello Naver :)") 360 | ``` 361 | 362 | #### __Image__ 363 | ```python 364 | ntalk.send(user_id, Template.ImageContent(image_url)) 365 | ``` 366 | or 367 | 368 | ```python 369 | ntalk.send(user_id, Template.ImageContent(image_id=image_id)) 370 | ``` 371 | 372 | #### __CompositeContent__ 373 | ```python 374 | ntalk.send( 375 | user_id, 376 | message=CompositeContent(composite_list=[ ... ]) 377 | ) 378 | ``` 379 | #### __quick reply__ 380 | ```python 381 | quick_reply = QuickReply( 382 | [ 383 | Button.TextButton('Punch', 'PunchCode'), 384 | Button.LinkButton('Link', 'https://example.link.com') 385 | ] 386 | ) 387 | # can use a list of buttons instead of QuickReply instance 388 | # 389 | # quick_reply = [ {'title': 'Punch', 'value': 'PunchedCode'}, 390 | # {'title': 'Link', 'value': 'https://example.link.com'}] 391 | 392 | ntalk.send( 393 | user_id, 394 | "Quick Reply message", 395 | quick_reply=quick_reply 396 | ) 397 | ``` 398 | 399 | ## Utility 400 | ### __take_thread(self, user_id, callback=None)__ 401 | 402 | - user_id: 권한을 넘길 사용자 403 | - callback: callback 함수 404 | - [자세한 정보](https://github.com/navertalk/chatbot-api/blob/master/handover_v1.md#%EC%B1%97%EB%B4%87%EC%9D%B4-%EB%8C%80%ED%99%94%EC%9D%98-%EC%A3%BC%EB%8F%84%EA%B6%8C%EC%9D%84-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0) 405 | ```python 406 | ntalk.take_thread( 407 | user_id=user_id 408 | ) 409 | ``` 410 | 411 | ### __pass_thread(self, user_id, callback=None)__ 412 | 413 | - user_id: 권한을 넘길 사용자 414 | - callback: callback 함수 415 | - [자세한 정보](https://github.com/navertalk/chatbot-api/blob/master/handover_v1.md#%EC%B1%97%EB%B4%87%EC%9D%98-%EB%A9%94%EC%8B%9C%EC%A7%80%EB%A5%BC-%EC%88%98%EC%8B%A0%ED%95%A0-%EB%95%8C-standby-%EC%86%8D%EC%84%B1) 416 | ```python 417 | ntalk.pass_thread( 418 | user_id=user_id, 419 | ) 420 | ``` 421 | 422 | ### __typing_on(self, user_id, callback=None)__ 423 | 424 | - user_id: 425 | - callback: callback 함수 426 | - [자세한 정보](https://github.com/navertalk/chatbot-api#action-%EC%9D%B4%EB%B2%A4%ED%8A%B8) 427 | ```python 428 | ntalk.typing_on(user_id) 429 | ``` 430 | 431 | ### __typing_off(self, user_id, callback=None)__ 432 | 433 | - user_id: 사용자 아이디 434 | - callback: callback 함수 435 | - [자세한 정보](https://github.com/navertalk/chatbot-api#action-%EC%9D%B4%EB%B2%A4%ED%8A%B8) 436 | ```python 437 | ntalk.typing_off(user_id) 438 | ``` 439 | 440 | ### __persistent_menu(self, menus, callback=None)__ 441 | 442 | - menus: `` 고정메뉴에 보여질 버튼 ( ButtonOption is not allowed ) 443 | - [자세한 정보](https://github.com/navertalk/chatbot-api#persistentmenu-%EC%9D%B4%EB%B2%A4%ED%8A%B8) 444 | ```python 445 | ntalk.persistent_menu(menus=[Button.ButtonText(...), Button.ButtonLink(...), ...]) 446 | ``` 447 | 448 | ### __product_message(self, user_id, ids, displayType='single', callback=None)__ 449 | 450 | - user_id: 유저 아이디 451 | - ids: `` 스토어팜 상품 번호 452 | - displayType: `` 'signle' | 'list' 상품이 보여지는 방식. default: 'single' 453 | - [자세한 정보](https://github.com/navertalk/chatbot-api/blob/master/product_message_api.md) 454 | ```python 455 | ntalk.product_message(user_id, ids=[...], displayType='list') 456 | ``` 457 | 458 | 459 | ## Template 460 | 461 | ```python 462 | from nta import Template 463 | ``` 464 | 465 | ### TextContent 466 | > __init__(self, text, code=None, input_type=None, **kwargs) 467 | 468 | - text: 사용자에게 보낼 텍스트 469 | - code: 사용자에게 받은 텍스트 470 | - input_type: 사용자 입력 타입 471 | - [textContent 정보](https://github.com/navertalk/chatbot-api#textcontent) 472 | ```python 473 | Template.TextContent('너에게 보내는 메세지') 474 | ``` 475 | 476 | ### ImageContent 477 | > __init__(self, image_url=None, image_id=None, **kwargs) 478 | 479 | - image_url: 사용자에게 보낼 이미지 url 480 | - image_id: 사용자에게 보낼 이미지 id 481 | - image_url과 image_id 중 하나를 반드시 포함 (image_url 우선) 482 | - [imageContent 정보](https://github.com/navertalk/chatbot-api#imagecontent) 483 | ```python 484 | Template.ImageContent(image_url='xxx.jpg') 485 | ``` 486 | ### CompositeContent 487 | > __init__(self, composite_list, **kwargs) 488 | 489 | - 카드뷰 형식의 탬플릿 490 | - composite_list: [composite](#composite) 리스트 491 | - [compositeContent 정보](https://github.com/navertalk/chatbot-api#compositecontent) 492 | ```python 493 | Template.CompositeContent( 494 | composite_list = [Template.Composite(...), ...] 495 | ) 496 | ``` 497 | 498 | 499 | ### Composite 500 | > __init__(self, title, description=None, image=None, element_list=None, button_list=None, **kwargs) 501 | 502 | - title: 카드의 타이틀 503 | - description: 카드의 상세설명 504 | - image: 카드에 보이는 이미지 url or 이미지 id 505 | - element_list: 카드를 구성하는 [ElementData](#elementdata) 리스트 506 | - button_list: 카드를 구성하는 [Button](#buttons) 리스트 507 | - [composite 정보](https://github.com/navertalk/chatbot-api#composite-object) 508 | 509 | ```python 510 | Template.Composite( 511 | title="굵은글씨", 512 | description="회색글씨", 513 | image="xxx.jpg", 514 | element_list=Template.ElementList([ 515 | Template.ElementData(...), 516 | ... 517 | ]), 518 | button_list=[ 519 | Template.ButtonText(...), 520 | ... 521 | ] 522 | ) 523 | ``` 524 | 525 | ### ElementList 526 | > __init__(self, data, **kwargs) 527 | 528 | - data: [ElementData](#elementdata) 리스트 529 | - [ElementList 정보](https://github.com/navertalk/chatbot-api#elementlist-object) 530 | ```python 531 | Template.ElementList(data=[ 532 | Template.ElementData(...), 533 | ... 534 | ]) 535 | ``` 536 | 537 | ### ElementData 538 | > __init__(self, title, description=None, sub_description=None, image=None, button=None, **kwargs) 539 | 540 | - title: Element 타이틀 541 | - description: 상세정보 542 | - sub_dscription: 하위 상세정보 543 | - image: 이미지 url or 이미지 id 544 | - button: Template.Button 버튼 하나 545 | - [ElementData 정보](https://github.com/navertalk/chatbot-api#elementdata-object-list-%ED%83%80%EC%9E%85) 546 | ```python 547 | Template.ElementData( 548 | title="굵은글씨", 549 | description="회색글씨", 550 | sub_description="더 회색글씨", 551 | image="xxx.jpg", 552 | button=Template.ButtonText(...) 553 | ) 554 | ``` 555 | 556 | ### QuickReply 557 | > __init__(self, button_list, **kwargs) 558 | 559 | - button_list: 버튼 리스트 560 | - [quickReply 정보](https://github.com/navertalk/chatbot-api#%ED%80%B5%EB%B2%84%ED%8A%BC) 561 | ```python 562 | Template.QuickReply([ 563 | Template.ButtonText(...), 564 | ... 565 | ]) 566 | ``` 567 | 568 | ### PaymentInfo 569 | > __init__(self, merchant_pay_key, total_pay_amount, product_items, merchant_user_key=None, ...) 570 | 571 | - merchant_pay_key: 필수 572 | - total_pay_amount: 필수 573 | - product_items: 필수 [ProductItem](#productitem) 리스트 574 | - 자세한 정보 및 나머지 값들 [PaymentInfo](https://github.com/navertalk/chatbot-api/blob/master/pay_api_v1.md#paymentinfo-오브젝트) 참고 575 | ```python 576 | Template.ProductInfo( 577 | merchant_pay_key="yo-product-123", 578 | total_pay_amount=100000000, 579 | product_items=[ 580 | Template.ProductItem(...), 581 | ... 582 | ], 583 | ... 584 | ) 585 | ``` 586 | 587 | ### ProductItem 588 | > __init__(self, category_type, category_id, uid, name, ...) 589 | 590 | - category_type: 필수 591 | - category_id: 필수 592 | - uid: 필수 593 | - name: 필수 594 | - 자세한 정보 및 나머지 값들 [productItem](https://github.com/navertalk/chatbot-api/blob/master/pay_api_v1.md#productitem-오브젝트)참고 595 | ```python 596 | Template.ProductItem( 597 | category_type="Book", 598 | category_id="yo-123-book", 599 | uid="7269889", 600 | name="yosbest", 601 | ... 602 | ``` 603 | ## Buttons 604 | ```python 605 | from nta import Button 606 | ``` 607 | ### ButtonText 608 | > __init__(self, title, code=None, **kwargs) 609 | 610 | - title: 버튼 값. 611 | - code : 버튼에 숨겨진 code 값. 612 | - 자세한 정보 [buttonText](https://github.com/navertalk/chatbot-api#buttondata-object-text-타입) 613 | ```python 614 | Button.ButtonText('보여지는 타이틀', '숨겨진 코드값') 615 | ``` 616 | 617 | ### ButtonLink 618 | > __init__(self, title, url, mobile_url=None, webview=True, webview_title=None, webview_height=None, **kwargs) 619 | 620 | - title: 보여지는 버튼 값. 621 | - url : 연결되는 링크 622 | - mobile_url: 모바일 상에서 연결되는 링크. 623 | - webview: True or False. If True, activate webview button 624 | - webview_title: title of webview 625 | - webview_height: size ratio of webview 626 | - 자세한 정보 [buttonLink](https://github.com/navertalk/chatbot-api#buttondata-object-link-타입) 627 | - webview에 대한 정보 [webview](https://github.com/navertalk/chatbot-api/blob/master/webview_v1.md) 628 | ```python 629 | Button.ButtonLink( 630 | "title showed up", 631 | "Linked URL", 632 | mobile_url="#Linked URL in Mobile device", 633 | webview=True, 634 | webview_title="Title of webview", 635 | webview_height=50 636 | ) 637 | ``` 638 | 639 | ### ButtonOption 640 | > __init__(self, title, button_list, **kwargs) 641 | 642 | - title: 노출되는 텍스트 643 | - button_list: 숨겨진 버튼 644 | - 자세한 정보 [buttonOption](https://github.com/navertalk/chatbot-api#buttondata-object-option-타입) 645 | ```python 646 | Button.ButtonOption("title showed up", button_list=[Button.ButtonText(...), ...]) 647 | ``` 648 | 649 | ### ButtonTime 650 | > __init__(self, title, code, **kwargs) 651 | 652 | - title: 노출되는 텍스트 653 | - code: 버튼의 코드값 654 | - 자세한 정보 [ButtonTime](https://github.com/navertalk/chatbot-api/blob/master/time_component_v1.md) 655 | ```python 656 | Button.ButtonTime("title showed up", code='Time_Test') 657 | 658 | # Use callback regex matching makes it easy to use 659 | # example callback handler below. 660 | ntalk.callback(['Time_Test']) 661 | def time_test_handler(event): 662 | # event.title: user selected time 663 | pass 664 | ``` 665 | 666 | ### ButtonNested 667 | > __init__(self, title, menus, **kwargs) 668 | 669 | - 고정 메뉴에 사용되는 버튼이다. 버튼을 누르면 숨겨진 버튼이 보여진다. 670 | - title: 노출되는 텍스트 671 | - menus: `` 버튼 리스트 672 | - 자세한 정보 [ButtonNested](https://github.com/navertalk/chatbot-api#menudata-objectnested-%ED%83%80%EC%9E%85) 673 | ```python 674 | Button.ButtonNested("title showed up", menus=[Button.ButtonText(...), Button.ButtonLink(...), ...] 675 | ``` 676 | 677 | ### ButtonCalendar 678 | > __init__(self, title=None, code=None, placeholder=None, start=None, end=None, disables=None, **kwargs) 679 | 680 | - Use built-in calendar webview provided by Naver. 681 | - title: Exposed to button. 682 | - code: Hidden code in button. 683 | - palceholder: Webview title. 684 | - start: start date. 685 | - end: end date. 686 | - disalbes(Str): date to disable. 687 | - More Info See [ButtonCalnedar](https://github.com/navertalk/chatbot-api/blob/master/calendar_component_v1.md) 688 | ```python 689 | Button.ButtonCalnedar(title="title showed up", code="hidden code", ...) 690 | ``` 691 | 692 | ### ButtonTimeInterval 693 | > __init__(self, title=None, code=None, start=None, end=None, interval=None, disables=None, **kwargs) 694 | 695 | - Choose time with selections. 696 | - title: Exposed to button. 697 | - code: Hidden code in button. 698 | - start: start time. 699 | - end: end time. 700 | - disables(Str): time period to disable. 701 | - More Info See [ButtonTimeInterval](https://github.com/navertalk/chatbot-api/blob/master/time_interval_component_v1.md) 702 | ```python 703 | Button.ButtonTimeInterval(title="title showed up", code="hidden code", ...) 704 | ``` 705 | 706 | ## Exception 707 | ```python 708 | from nta.exceptions import ( 709 | NaverTalkApiError, 710 | NaverTalkPaymentError, 711 | NaverTalkApiConnectionError 712 | ) 713 | 714 | def webhook_handler(): 715 | req = requests.get_data(as_text=True) 716 | try: 717 | ntalk.handle_webhook(req) 718 | except NaverTalkApiError as e: 719 | assert e.status_code == 200 720 | assert e.result_code != "00" 721 | # e.message from navertalk 722 | except NaverTalkApiConnectionError as e: 723 | assert e.status_code != 200 724 | except NaverTalkPaymentError as e: 725 | return e.message, 400 726 | 727 | return "ok" 728 | 729 | ``` 730 | ### NaverTalkApiError 731 | 732 | - Naver Talk에 Post 이후 받은 값 Success가 False인 경우 발생 733 | - resultCode가 "00"이 아닌 경우 발생. 734 | - 더 많은 result코드와 내용에 대한 정보 [Error](https://github.com/navertalk/chatbot-api#error-명세서) 735 | 736 | ### NaverTalkApiConnectionError 737 | 738 | - NaverTalk api internal server error. 739 | - 네이버톡으로 부터 200이 아닌 response를 받았을 때 발생. 740 | 741 | ### NaverTalkPaymentError 742 | 743 | - 결제를 취소를 위한 error 744 | - 사용자의 결제를 승인을 거부할 때 사용. 745 | - Pay 개발가이드 참고 [Pay](https://github.com/navertalk/chatbot-api/blob/master/pay_api_v1.md#개발가이드) 746 | 747 | example 748 | ```python 749 | @ntalk.handle_pay_complete 750 | def pay_handle_func(event): 751 | if not 재고: 752 | raise NaverTalkPaymentError('재고 없음') 753 | ``` 754 | 755 | ## Event 756 | handler만 참고 하면 사용에 어려움 없음. 심리적 안정을 위해 추가한 섹션. 757 | - 이벤트의 사용은 [handler](#handler) 참고 758 | - Event.user_id: 사용자 아이디. (모든 이벤트에 해당함.) 759 | ### OpenEvent 760 | - OpenEvent.inflow: 사용자 유입방식 761 | - OpenEvent.referer: 사용자 유입경로 762 | - OpenEvent.friend: 사용자가 친구일 경우 True 763 | - OpenEvent.under_14: 사용자가 14세 미만일 경우 True 764 | - OpenEvent.under_19: 사용자가 19세 미만일 경우 True 765 | - [OpenEvent 참고](https://github.com/navertalk/chatbot-api#open-%EC%9D%B4%EB%B2%A4%ED%8A%B8) 766 | 767 | ### LeaveEvent 768 | 769 | - [LeaveEvent 참고](https://github.com/navertalk/chatbot-api#leave-이벤트) 770 | 771 | ### FriendEvent 772 | - FriendEvent.set_on: 사용자가 친구 추가할 경우 True 773 | - [FriendEvent 참고](https://github.com/navertalk/chatbot-api#friend-이벤트) 774 | 775 | 776 | ### SendEvent 777 | 778 | - SendEvent.text: 사용자가 입력한 문장 779 | - SendEvent.code: 사용자가 선택한 버튼의 code값 780 | - SendEvent.input_type: 사용자가 입력을 한 방식. 781 | - SendEvent.is_code: 사용자의 입력에 코드값이 포함되어 있을 경우. 782 | - SendEvent.image_url: 사용자가 보낸 이미지의 url 783 | - [SendEvent 참고](https://github.com/navertalk/chatbot-api#send-이벤트) 784 | 785 | ### EchoEvent 786 | 787 | - EchoEvent.echoed_event: echo 이벤트에 담겨있는 모든 정보 788 | - [EchoEvent 참고](https://github.com/navertalk/chatbot-api#echo-이벤트) 789 | 790 | ### PayCompleteEvent 791 | 792 | - PayCompleteEvent.payment_result: 사용자 결제 결과. 793 | - PayCompleteEvent.code: 사용자 결제 결과 코드. 794 | - PayCompleteEvent.payment_id: 결제 식별 고유번호. (결제 성공시) 795 | - PayCompleteEvent.message: 결제 실패 정보. (결제 실패시) 796 | - PayCompleteEvent.merchant_pay_key: custum 결제 식별 정보. 797 | - PayCompleteEvent.merchant_user_key: custum 유저 식별 정보. 798 | - [PayCompleteEvent 참고](https://github.com/navertalk/chatbot-api/blob/master/pay_api_v1.md#pay_complete-이벤트-구조) 799 | 800 | ### PayConfirmEvent 801 | 802 | - PayConfirmEvent.code: 결제승인 결과 803 | - PayConfirmEvent.message: 결제승인 결과 메세지 804 | - PayConfirmEvent.payment_id: 결제 식별 고유번호 (결제 성공시 ?) 805 | - PayConfirmEvent.detail: d네이버페이 간편결제 결제승인 API 응답본문 그대로 반환. 806 | - [PayConfirmEvent 참고](https://github.com/navertalk/chatbot-api/blob/master/pay_api_v1.md#pay_confirm-이벤트) 807 | 808 | ### HandOverEvent 809 | 810 | - HandOverEvent.control: passThread 811 | - HandOverEvent.metadata: 네이버톡톡에서 보내온 메타 데이터 --------------------------------------------------------------------------------