오늘날 많은 서비스·플랫폼·정보가 웹에서 제공되고 있다. 웹 API를 사용하면 프로그램 외부의 정보를 가져오거나 프로그램을 외부의 서비스와 연동시킬 수 있다. 간단한 챗봇을 만들며 웹 API를 어떻게 사용하는지 확인해 보자.

12.2.1 챗봇이란?

챗봇(Chatbot)은 사용자가 컴퓨터와 대화하듯이 정보를 주고받는 인터페이스다. 챗봇은 정보 안내·일정 관리·티켓 예매, 쇼핑 등 다양한 서비스에 이용될 수 있으며, 텔레그램·위챗·카카오톡 등의 다양한 메시지 플랫폼에서 챗봇을 구현할 수 있다. 메시지 플랫폼은 대부분 챗봇 인터페이스 구현을 위한 웹 API를 제공하고 있다.

이 예제에서는 사람과 대화하는 것처럼 느껴지는 자연스러운 대화 기능을 제공하는 챗봇을 만들지는 않는다. 그러려면 인공지능·자연어 인식 등의 고급 기술을 다뤄야 하기 때문이다. 이 책에서는 텔레그램 메신저에서 메시지를 주고받는 최소한의 기능만 구현해 볼 것이다. 그것만으로도 많은 유용한 일을 할 수 있고, 프로그래밍 입문자에게 좋은 경험이 된다.

챗봇 개발에 흥미가 있고 더 자세히 알고 싶다면 『봇 설계는 이렇게 한다』(아미르 셔밧 저, 제이펍)를 추천한다.

12.2.2 웹 API란?

웹 API(application programming interface)란 웹 서비스를 컴퓨터 프로그램이 이용할 수 있도록 제공해 둔 인터페이스다. 사람을 위한 웹 문서는 대개 HTML 형식으로 제공되는 경우가 많다. 반면, 웹 API는 대부분 JSON, CSV, XML 등의 형식으로 제공된다. 그래서 웹 API로 얻은 정보는 HTML 문서에 비해 컴퓨터 프로그램으로 해석하기 편리하다. 웹 API는 단순히 정보를 조회하는 것 뿐 아니라 웹 서비스를 이용하기 위한 다양한 기능을 제공한다. 그래서 웹 API를 이용해 여러분이 만든 프로그램을 외부의 웹 서비스와 연동할 수도 있다.

웹 API의 종류

웹 서비스를 운영하는 다양한 기관·회사·개인이 웹 API를 제공하고 있다. “이런 것이 웹 API로 제공되면 좋지 않을까?”하고 떠올려 봄직한 서비스의 상당수가 이미 웹 API로 제공되고 있다. 예를 몇 가지만 들어보자.

  • 대한민국 공공기관 정보: 공공데이터포털(data.go.kr)에서 API 이용권을 얻은 후 열람할 수 있다. 행정정보, 도로정보, 기상정보 등 행정기관과 관련된 정보를 얻을 수 있다.
  • 세계 다국어 사전: 여러 가지 언어로 단어의 뜻을 검색할 수 있다. Glosbe(glosbe.com)에서 사용신청 없이 자유롭게 사용할 수 있다.
  • 세계 영화 정보: 세계의 다양한 영화·드라마의 정보를 확인할 수 있다. TMDb(themoviedb.org)에서 계정을 생성한 뒤 사용할 수 있다.
  • 세계 기상 정보: 세계 곳곳의 기상 정보를 조회할 수 있다. OpenWeatherMap(openweathermap.org/api)에서 계정을 생성한 후 사용할 수 있다.
  • 페이스북 사용자·게시물 정보: 페이스북 개발자 사이트(developers.facebook.com)에서 개발자 계정 등록 후, 페이스북 그래프 API를 이용해 열람할 수 있다.
  • 텔레그램 챗봇 조작: 텔레그램 챗봇 계정 등록 후, 텔레그램 봇 API로 챗봇을 만들고 조종할 수 있다.

이 정보는 책을 쓰는 시점을 기준으로 한 것이기 때문에, 여러분이 책을 보는 시점에는 운영상황이나 이용권이 변경되었을 수도 있다.

웹 API를 사용할 때의 주의사항

웹 API를 사용할 때는 다음 세 가지 사항에 주의해야 한다.

  1. 이용권을 확인해야 한다. 모두에게 공개된 웹 API도 있지만, 먼저 허락을 구해야 하는 것도 있다. 또, 무료로 사용할 수 있는 것도 있고, 댓가를 지불해야 하는 것도 있다. 그리고 무료라고 하더라도 하루에 일정 횟수 이하로만 요청을 할 수 있는 제약 등이 있다. 새로운 웹 API를 이용할 때는 이용권에 대한 내용을 잘 살펴보자.
  2. 사용법을 익혀야 한다. 웹 API는 대부분 HTTP 프로토콜을 이용하므로, 기본적으로는 사용법이 유사하다. 하지만 URL의 구조와 제공하는 정보의 형태가 웹 API마다 제각각이기 때문에 사용법을 배워야 한다. 이용하는 웹 API 문서에 대부분 나와 있으나, 문서화가 잘 되지 않은 경우도 있다.
  3. 민감한 정보를 업로드할 때는 웹 API 제공자를 믿을 수 있는지 확인해야 한다. 웹 API는 우리에게 정보를 줄 뿐 아니라 우리가 보낸 정보를 수집하기도 한다. 웹 API에 정보를 보낼 때 상대방이 그것을 어떻게 사용할지 잘 생각해서, 민감한 정보는 보내지 않도록 주의해야 한다.

12.2.3 텔레그램 봇 API 사용 준비하기

이 책에서는 텔레그램을 이용해 챗봇을 만들어 볼 것이다. 다양한 메시지 서비스가 있으나, 텔레그램이 일찍부터 챗봇을 지원했고, 사용하기 쉬운 간결한 웹 API를 제공하며, 서비스를 무료로 제공하기 때문에 선정했다. 실습을 진행하려면 텔레그램 클라이언트 프로그램이 필요하다. 여러분이 사용하는 스마트폰에 ‘텔레그램 공식 앱’을 설치하고, 사용자 계정(전화번호)을 등록한 뒤 실습을 하면 된다.

텔레그램 봇 API의 사용법은 텔레그램 봇 API 공식 문서(https://core.telegram.org/bots/api)에서 자세히 배울 수 있다. 관심이 있다면 이 책에서 기초 사항을 실습한 뒤 도전해봐도 좋다.

챗봇 계정 등록하기

텔레그램에서 챗봇을 만들기 위해서는 사용자 계정과 별도로 챗봇 계정을 생성해야 한다. 챗봇 계정은 챗봇(여러분의 프로그램)이 사람(여러분 또는 다른 사용자)과 메시지를 주고받을 (여러분 또는 다른 사용자)때 사용하는 계정이다. 챗봇 계정을 등록하려면 텔레그램 앱에서 ‘봇파더(@BotFather)’라는 챗봇에게 말을 걸어야 한다. 텔레그램 앱에서 사용자 검색 버튼을 누른 뒤 ‘@BotFather’를 검색한다.

그림 12-19 봇파더 검색하기

그림 12-19 봇파더 검색하기

@BotFather에게 말을 걸면 다음과 같은 화면이 나온다. 이 챗봇이 제공하는 기능을 간단하게 소개하는 화면이다. 챗봇은 사용자에게 먼저 대화를 시작하지 못한다. (챗봇 계정을 이용한 스팸 메시지를 방지하기 위해서다.) ‘Start’ 버튼을 누르면 챗봇이 여러분에게 말을 걸 수 있게 된다.

그림 12-20 봇파더 대화 화면

그림 12-20 봇파더 대화 화면

봇파더가 자신에게 지시할 수 있는 명령어의 목록을 알려줄 것이다. 봇파더에게 ‘/newbot’이라는 메시지를 보내, 새 챗봇 계정 등록을 시작한다. 그러면 봇파더가 챗봇의 이름을 입력하라고 말할 것이다.

그림 12-21 새 챗봇 계정 등록하기

그림 12-21 새 챗봇 계정 등록하기

원하는 챗봇의 이름을 대답한다. 챗봇의 이름은 한글로 지어도 된다.

그림 12-22 챗봇 이름 정하기

그림 12-22 챗봇 이름 정하기

이름을 지은 뒤에는 사용할 챗봇의 계정 이름(username)을 대답해야 한다. 계정 이름에는 한글을 사용할 수 없고, ‘bot’, ‘Bot’ 등으로 끝나는 이름이어야 한다. 예제에서는 ‘chatbot_practice_bot’이라고 지었다. 챗봇 이름은 고유해야 하므로 여러분은 이것과 다른 이름으로 지어야 한다.

그림 12-23 챗봇 계정 이름 정하기

그림 12-23 챗봇 계정 이름 정하기

중복되지 않는 챗봇 계정 이름을 지었으면, 봇파더가 “Done! Congratulations on your new bot…“이라며 챗봇 계정이 생성되었다고 알려줄 것이다.

그림 12-24 챗봇 등록 완료 메시지

그림 12-24 챗봇 등록 완료 메시지

챗봇 보안 토큰

챗봇 정을 생성한 뒤 봇파더의 완료 메시지를 자세히 살펴보면, “Use this token to access this HTTP API: 718342091:AAG9cV0NeCVpPcAVtJqeNoaYWXkI9uhhz6w”라는 부분이 있을 것이다. 이 메시지에서 ‘718342091:AAG9cV0NeCVpPcAVtJqeNoaYWXkI9uhhz6w’가 보안 토큰이다. 계정마다 보안 토큰이 다르기 때문에 여러분이 발급받은 토큰은 이것과 다를 것이다.

보안 토큰은 챗봇 계정을 조작하기 위한 열쇠로, 웹 API를 사용할 때 꼭 필요하다. 보안 토큰을 다른 사람에게 알려주거나 보안 토큰이 들어 있는 소스코드를 웹(깃허브 등)에 게시해서는 안 된다. 만약 보안 토큰이 무엇인지 잊어버렸거나 유출되었다면 봇파더에게 ‘/revoke’라는 메시지를 보내면 된다. 그러면 봇파더가 기존에 발급된 토큰을 무효화하고, 새 토큰을 발급해 알려줄 것이다.

12.2.4 텔레그램 봇 API로 메시지 주고받기

등록한 챗봇 계정을 이용해 파이썬 프로그램으로 메시지를 보내고 받아 보자. 먼저, 파이참에서 ‘chatbot’ 프로젝트를 새로 만들고 시작하자.(12.1절 참고)

챗봇 웹 API에 요청 보내기

텔레그램 챗봇 웹 API에 HTTP 요청을 보내 볼 것이다. HTTP 요청을 보내는 방법은 11.5절을 참고하면 된다. 파이참에서 대화식 셸을 열고, 코드 11-77의 웹 문서 요청 함수 request()를 정의해 두자.

코드 12-26 request() 함수 정의하기

>>> import urllib.request
>>> def request(url):
...     """지정한 url의 웹 문서를 요청하여, 본문을 반환한다."""
...     response = urllib.request.urlopen(url)
...     byte_data = response.read()
...     text_data = byte_data.decode()
...     return text_data
...

챗봇을 사용하기 위한 보안 토큰도 변수에 저장해 두자. 봇파더가 알려준 토큰을 입력해야 한다. 여러분의 토큰은 아래의 토큰과는 다르다.

코드 12-27 챗봇 보안 토큰을 변수에 저장하기

>>> TOKEN = '718342091:AAG9cV0NeCVpPcAVtJqeNoaYWXkI9uhhz6w'

챗봇 웹 API에 요청을 보낼 때는 https://api.telegram.org/bot{보안토큰}/{메서드}?{질의조건} 양식의 URL을 사용한다. 여기서 ‘메서드’란 텔레그램 챗봇 웹 API가 제공하는 여러 가지 명령으로, 파이썬의 함수라고 생각하면 된다. ‘질의조건’은 메서드에 전달할 인자와 같은 것이라고 생각하면 된다. 메서드를 인자로 전달받아 URL을 만들어주는 함수를 정의하자. 11.2절에서 배운 양식 문자열 리터럴을 이용하면 편리하다.

코드 12-28 챗봇 요청 URL을 생성 함수 정의하기

>>> def build_url(method, query):
...     """텔레그램 챗봇 웹 API에 요청을 보내기 위한 URL을 만들어 반환한다."""
...     return f'https://api.telegram.org/bot{TOKEN}/{method}?{query}'
...

등록한 챗봇 계정으로 챗봇 웹 API와 통신을 할 수 있는지 확인하려면 getMe 메서드로 요청을 보내면 된다. getMe 메서드에는 질의조건이 필요하지 않으므로 질의조건은 빈 문자열('')로 지정하면 된다. build_url('getMe', '')로 URL을 만들고 request() 함수로 요청해 보자.

코드 12-29 getMe 메서드로 요청하여 웹 API 상태 확인하기

>>> request(build_url('getMe'))
'{"ok":true,"result":{"id":718342091,"is_bot":true,"first_name":"\\ucc57\\ubd07\\uc5f0\\uc2b5\\ubd07","username":"chatbot_practice_bot"}}'

코드를 실행했을 때, HTTPError: HTTP Error 404: Not Found 예외가 발생한다면 URL이 잘못된 것이다. 또, HTTPError: HTTP Error 401: Unauthorized 예외가 발생한다면 토큰이 잘못된 것이다. 잘못된 부분을 확인하고 다시 시도해 보자.

JSON 형식의 문자열이 반환되면 성공이다. JSON 형식의 텍스트 데이터는 11.4절에서 배운 json 모듈의 json.loads() 함수를 이용해 파이썬 컬렉션 객체로 해석할 수 있다. 다시 요청하고 결과를 해석해보자.

코드 12-30 챗봇 웹 API 응답 결과를 파이썬 컬렉션으로 해석하기

>>> import json
>>> response = request(build_url('getMe'))
>>> json.loads(response)
{'ok': True, 'result': {'id': 718342091, 'is_bot': True, 'first_name': '챗봇연습봇',
'username': 'chatbot_practice_bot'}}

결과가 파이썬 사전으로 해석되었다. 이제 챗봇 웹 API를 정상적으로 사용할 수 있다. URL을 생성하고, 챗봇 웹 API에 요청하고, 결과를 해석하는 과정을 하나의 함수로 정의해 두자.

코드 12-31 챗봇 웹 API 요청 과정을 하나의 함수로 정의하기

>>> def request_to_chatbot_api(method, query):
...     """메서드(method)와 질의조건(query)을 전달받아 텔레그램 챗봇 웹 API에 요청을 보내고,
...        응답 결과를 사전 객체로 해석해 반환한다."""
...     url = build_url(method, query)
...     response = request(url)
...     return json.loads(response)
...

이제 request_to_chatbot_api() 함수로 챗봇 웹 API를 쉽게 이용할 수 있다.

챗봇과 대화 시작하기

챗봇은 사용자에게 먼저 대화를 시작하지 못한다. 챗봇과 대화를 하기 위해서는 여러분이 챗봇을 찾아 먼저 대화를 시작해야 한다. 텔레그램 앱에서 방금 생성한 챗봇의 계정 이름에 ‘@’ 기호를 붙여 검색한다. 챗봇의 계정 이름이 ‘chatbot_practice_bot’이라면 ‘@chatbot_practice_bot`이라고 검색한다. 검색한 후 ‘시작’을 누르면 챗봇이 활성화되고, 채팅할 준비가 된다.

그림 12-25 등록한 챗봇 검색하기

그림 12-25 등록한 챗봇 검색하기

챗봇으로 메시지 읽기

텔레그램 앱에서 챗봇에 ‘hello’ 라고 메시지를 보내 두자. 사용자가 챗봇에게 방금 보낸 이 메시지를 챗봇이 읽어올 수 있도록 해 보자.

챗봇 웹 API에 getUpdates 메서드로 요청을 보내면, 그동안 받은 새 메시지를 조회할 수 있다. getUpdates 메서드에는 offset이라는 질의조건을 지정해야 한다. offset은 한 번 확인한 메시지를 다시 전달받지 않기 위해 지정하는 것이다. 어떤 값을 입력해야 하는지는 다시 살펴볼텐데, 지금은 처음이니 offset=0으로 지정하자. 앞서 정의한 request_to_chatbot_api() 함수로 이 메서드에 요청해보자.

코드 12-32 챗봇이 받은 메시지 확인하기

>>> response = request_to_chatbot_api('getUpdates', 'offset=0')
>>> response
(... 요청 결과가 JSON 형식의 텍스트로 응답된다)

응답된 데이터의 내용이 많아서 확인하기가 쉽지 않다. pprint 모듈의 pprint() 함수를 이용하면 사전 데이터를 화면에 보기 좋게 출력할 수 있다. 단, 코드 12-33은 설명을 위해 조금 더 꾸며 놓은 것이어서 이 예제와 똑같이 출력되지는 않을 것이다.

코드 12-33 pprint() 함수로 메시지 내용 자세히 살펴보기

>>> from pprint import pprint
>>> pprint(response)
{
    'ok': True,  # ❶
    'result': [  # ❷
        {        # ❸
            'update_id': 100,        # ❹
            'message': {             # ❺
                'message_id': 2,
                'text': 'hello'      # ❻
                'date': 1557680045,  # ❼
                'from': {            # ❽
                    'first_name': 'Yeon O',  # ❾
                    'id': 9999999999,        # ❿
                    'is_bot': False,
                    'language_code': 'ko',
                    'last_name': 'Bak',
                    'username': 'username',
                },
            },
        }
    ]
}

응답받은 사전은 크게 'ok' 키와 'result' 키로 되어 있다. ❶ 'ok'키의 값은 요청이 성공했는지 여부를 알려준다. True이므로 성공한 것이다. 중요한 부분은 ❷ 'result' 키의 값이며 이것이 챗봇이 받은 메시지의 리스트다. 리스트 속에 들어 있는 각각의 사전마다 메시지 하나씩을 담고 있다. 지금은 메시지를 하나밖에 받지 않았으므로, 리스트에는 ❸ 하나의 사전만 들어 있다. 사전을 다시 자세히 살펴보자. ❹ 'update_id'getUpdates 메서드를 사용할 때 이 메시지 이후에 온 메시지만 받고 싶을 때 참고해야 하는 값으로, 이 값이 100이라면 다음 번 요청에서는 여기에 1을 더해 101을 offset 질의조건으로 지정하면 된다. ❺ message 키에 메시지의 자세한 내용이 나와 있다. 가장 중요한 ❻ 'text'가 메시지의 내용을 담고 있다. ❼ 'date'는 메시지를 받은 날짜다. ❽ 'from'에는 다시 사전이 들어 있는데, 이 사전은 메시지를 보낸 사람의 정보다. 이 사전에서 ❾ 'first_name'은 보낸 사람의 이름, ❿ 'id'가 보낸 사람의 텔레그램 ID다. 텔레그램 ID는 챗봇이 이 사람에게 메시지를 보낼 때 이용한다.

이 상태로는 활용하기가 불편하다. update_id, 메시지 내용, 보낸 사람의 ID만 있으면 되는데, 너무 많은 정보가 복잡하게 들어 있다. 응답 결과를 가공해서 사용하기 쉽게 변환하자.

코드 12-34 getUpdate 메서드의 요청 결과에서 필요한 정보만 걸러내기

>>> def simplify_messages(response):
...     """텔레그램 챗봇 API의 getUpdate 메서드 요청 결과에서 필요한 정보만 남긴다."""
...     result = response['result']  # ❶
...     if not result:  # ❷
...         return None, []
...     last_update_id = max(item['update_id'] for item in result)  # ❸
...     messages = [item['message'] for item in result]  # ❹
...     simplified_messages = [{'from_id': message['from']['id'],
...                              'text': message['text']}
...                            for message in messages]  # ❺
...     return last_update_id, simplified_messages  # 
...

함수 내용이 다소 복잡하게 느껴질 수 있지만 모두 배운 것이니 하나씩 살펴보자. ❶ 응답 결과에서 'result'의 값만 필요하므로 이것만 꺼냈다. ❷ 받은 메시지가 하나도 없을 수도 있다. 이 때는 result가 빈 리스트가 될 것인데, 이 경우에는 None, []를 튜플로 반환한다. ❸ 여러 개의 메시지를 전달받았을 때, 다음에 새로운 메시지만 받고 싶다면 여러 메시지 중 가장 큰 update_id만 있으면 된다. 그러므로 max() 함수로 가장 큰 것을 구한다. (item['update_id'] for item in result)는 7.4절에서 배운 생성기 식이다. ❹ 7.3절에서 배운 리스트 조건제시법으로 result 리스트에서 'message'의 값만 꺼낸다. ❺는 길어서 어려워 보일 수 있지만 ❹와 동일한 형식의 리스트 조건제시법이다. 메시지 리스트의 각 메시지 사전을 {'from_id': from_id, 'text': text} 형식의 간단한 사전으로 가공한다. ❻ 가공한 결과로 last_update_id, simplified_messages를 튜플로 반환한다.

이 함수를 실행해보면 다음과 같이 적절히 가공되는 것을 확인할 수 있다.

코드 12-35 요청 결과 가공 확인하기

>>> simplify_messages(response)
(100, [{'from_id': 9999999999, 'text': 'hello'}])

새 메시지를 확인하고 결과를 가공해 반환하는 과정을 get_updates()라는 함수로 정의해 두자.

코드 12-36 메시지 수신 함수 정의하기

>>> def get_updates(update_id):
...     """챗봇 API로 update_id 이후에 수신한 메시지를 조회하여 반환한다."""
...     query = f'offset={update_id}'
...     response = request_to_chatbot_api(method='getUpdates', query=query)
...     return simplify_messages(response)
... 

텔레그램 앱에서 메시지를 여러 개 더 보낸 뒤 이 함수를 호출해 보자.

코드 12-37 메시지 수신 함수 확인하기

>>> get_updates(101)
(102, [{'from_id': 9999999999, 'text': '안녕?'}, {'from_id': 9999999999, 'text': '나는 박연오야'}])

이제 간편히 get_updates() 함수만 호출하면 받은 메시지를 확인할 수 있다.

챗봇으로 메시지 보내기

챗봇 웹 API에 sendMessage 메서드로 요청을 보내면 대화를 시작한 사용자에게 메시지를 보낼 수 있다. sendMessage 메서드에는 메시지를 받은 사용자의 ID를 지정하는 chat_id와 메시지 내용을 지정하는 text 질의조건을 지정해야 한다. 여러 개의 질의조건을 지정할 때는 chat_id=9999999999&text=Hi와 같이 각 질의조건을 & 기호로 구별하면 된다.

아까 챗봇에 메시지를 보낸 사용자의 ID(여러분의 ID)로 메시지를 발신해 보자.

코드 12-38 메시지 발신하기

>>> request_to_chatbot_api('sendMessage', 'chat_id=9999999999&text=Hi')
(... 요청 결과가 JSON 형식의 텍스트로 응답된다)

그림 12-26 챗봇이 메시지를 보냈다

그림 12-26 챗봇이 메시지를 보냈다

요청이 성공했으면 JSON 형식의 텍스트가 응답되고, 텔레그램 앱으로 챗봇이 보낸 메시지가 왔을 것이다. 만약 HTTPError: HTTP Error 400: Bad Request 예외가 발생했다면 메시지를 받을 사람의 ID를 잘못 지정한 것일 가능성이 높다. 여러분의 텔레그램 ID는 챗봇에 메시지를 보내고, getUpdate 메서드로 그 메시지를 확인하여 from_id를 구해 확인할 수 있다. (앞의 내용 참고) 확인하고 다시 입력하도록 하자.

이번에는 한글로 메시지를 보내 보자. 오류가 발생할 것이다.

코드 12-39 한글 메시지를 발신하면 오류가 발생한다

>>> request_to_chatbot_api('sendMessage', 'chat_id=9999999999&text=안녕')
UnicodeEncodeError: 'ascii' codec can't encode characters in position 88-89: ordinal not in range(128)

HTTP 요청을 할 때 URL에 한글을 표기하고 싶다면 11.5절에서 배운 것처럼 urllib.parse 모듈의 urllib.parse.quote() 함수를 이용해 퍼센트 인코딩을 해야 한다. 발신할 메시지를 퍼센트 인코딩하면 정상적으로 한글 메시지를 보낼 수 있다.

코드 12-40 퍼센트 인코딩을 이용해 한글 메시지 발신하기

>>> import urllib.parse
>>> text = urllib.parse.quote('안녕')
>>> request_to_chatbot_api('sendMessage', f'chat_id=9999999999&text={text}')
(... 요청 결과가 JSON 형식의 텍스트로 응답된다)

그림 12-27 챗봇이 한글 메시지를 보냈다

그림 12-27 챗봇이 한글 메시지를 보냈다

메시지 발신 과정을 함수로 정의해 두자.

코드 12-41 메시지 발신 함수 정의하기

>>> def send_message(chat_id, text):
...     """챗봇 API로 메시지를 chat_id 사용자에게 text 메시지를 발신한다."""
...     text = urllib.parse.quote(text)
...     query = f'chat_id={chat_id}&text={text}'
...     response = request_to_chatbot_api(method='sendMessage', query=query)
...     return response
... 

이제 언제든지 이 함수를 호출해 메시지를 보낼 수 있다. 잘 되는지 확인해보자.

코드 12-42 메시지 발신 함수 확인하기

>>> send_message(9999999999, '오늘은 기분이 좋아.')
(... 요청 결과가 JSON 형식의 텍스트로 응답된다)

메시지에 자동으로 응답하기

이제 여러분은 대화식 셸에서 수동으로 메시지를 확인하고 발신할 수 있다. 하지만 챗봇이라면 메시지를 받은 뒤 자동으로 답신할 줄 알아야 할 것이다. 5초에 한 번씩 메시지를 확인하고, 받은 메시지에 자동으로 응답하는 프로그램을 만들어 보자.

파이참에서 새로 만들어 둔 chatbot 프로젝트에서 chatbot.py 파일을 만들자. 그리고 대화식 셸에서 작성한 코드들을 이 파일에 복사해 두자. 잠시후 사용하는 time 모듈도 임포트 해 두자.

코드 12-43 그동안 작성한 코드를 chatbot.py 모듈에 모으기

import json
import time  # 추가함
import urllib.parse
import urllib.request

TOKEN = '718342091:AAG9cV0NeCVpPcAVtJqeNoaYWXkI9uhhz6w'  # 여러분의 토큰으로 변경

def request(url):
    """지정한 url의 웹 문서를 요청하여, 본문을 반환한다."""
    response = urllib.request.urlopen(url)
    byte_data = response.read()
    text_data = byte_data.decode()
    return text_data

def build_url(method, query):
    """텔레그램 챗봇 웹 API에 요청을 보내기 위한 URL을 만들어 반환한다."""
    return f'https://api.telegram.org/bot{TOKEN}/{method}?{query}'

def request_to_chatbot_api(method, query):
    """텔레그램 챗봇 웹 API에 요청하고 응답 결과를 사전 객체로 해석해 반환한다."""
    url = build_url(method, query)
    response = request(url)
    return json.loads(response)

def simplify_messages(response):
    """텔레그램 챗봇 API의 getUpdate 메서드 요청 결과에서 필요한 정보만 남긴다."""
    result = response['result']
    if not result:
        return None, []
    last_update_id = max(item['update_id'] for item in result)
    messages = [item['message'] for item in result]
    simplified_messages = [{'from_id': message['from']['id'],
                            'text': message['text']}
                           for message in messages]
    return last_update_id, simplified_messages

def get_updates(update_id):
    """챗봇 API로 update_id 이후에 수신한 메시지를 조회하여 반환한다."""
    query = f'offset={update_id}'
    response = request_to_chatbot_api(method='getUpdates', query=query)
    return simplify_messages(response)

def send_message(chat_id, text):
    """챗봇 API로 메시지를 chat_id 사용자에게 text 메시지를 발신한다."""
    text = urllib.parse.quote(text)
    query = f'chat_id={chat_id}&text={text}'
    response = request_to_chatbot_api(method='sendMessage', query=query)
    return response

chatbot 모듈에 받은 메시지를 확인하고 응답하는 함수 check_messages_and_response()를 정의하자.

코드 12-44 메시지를 확인하고 응답하는 함수 정의하기

def check_messages_and_response(next_update_id):
    """챗봇으로 메시지를 확인하고, 적절히 응답한다."""
    last_update_id, recieved_messages = get_updates(next_update_id)  # ❶
    for message in recieved_messages:  # ❷
        chat_id = message['from_id']
        text = message['text']
        send_text = text + '라고 말씀하셨군요~' # ❸
        send_message(chat_id, send_text)  # ❹
    return last_update_id  # ❺

❶ 앞서 정의한 get_updates() 함수로 받은 메시지를 확인한다. ❷ 받은 메시지를 순회하며 처리한다. ❸ 받은 메시지 내용을 가공해 응답할 메시지를 새로 만든다. 이 메시지를 어떻게 만드느냐에 따라 챗봇이 대답하는 내용이 달라질 것이다. ❹ 앞서 정의한 send_message() 함수로 메시지를 발송한다. ❺ last_update_id를 반환한다. 나중에 이 함수를 다수 호출한다면 여기에 1을 더한 값을 전달해야 할 것이다.

이제 소스 코드 마지막 부분에 다음 코드를 추가해 이 함수를 5초에 한 번씩 실행하도록 한다.

코드 12-45 5초마다 메시지 확인하고 답장하기

if __name__ == '__main__':  # ❶
    next_update_id = 0  # ❷
    while True:  # ❸
        last_update_id = check_messages_and_response(next_update_id)  # ❹
        if last_update_id:  # ❺
            next_update_id = last_update_id + 1
        time.sleep(5)  # ❻

❶ 10.1절에서 배운 것을 활용해 이 모듈이 최상위 모듈로 실행된 경우에만 실행하도록 했다. ❷ get_update()를 실행할 때 이미 처리한 메시지를 다시 처리하지 않도록 새로 요청할 update_idnext_update_id를 기록한다. 기본값은 일단 0으로 해 둔다. ❸ 프로그램을 종료할 때까지 계속 동작하도록 무한 반복을 이용한다. ❹ 받은 메시지를 확인하고 응답을 한다. ❺ 받은 메시지가 없는 경우, last_update_idNone으로 반환될 것이다.(simplify_messages() 함수의 정의를 참고) last_update_idNone이 아닌 경우에만 next_update_id를 갱신하도록 한다. ❻ 웹 API에 요청을 연속으로 너무 많이 하면 좋지 않으므로 time.sleep() 함수를 이용해 5초간 기다린다. time.sleep() 함수는 time 모듈을 임포트해야 사용할 수 있다.

이제 프로그램을 실행해 둔 채로, 텔레그램 앱에서 챗봇에게 메시지를 보내 보자. 자동으로 답장하는 것을 확인할 수 있을 것이다.

그림 12-28 챗봇이 메시지에 자동으로 답장한다

그림 12-28 챗봇이 메시지에 자동으로 답장한다

여전히 한 가지 문제가 남아있다. 프로그램을 종료한 뒤 다시 실행하면 next_update_id가 다시 0으로 초기화되는 문제다. 이것은 next_update_id를 파일에 기록하도록 하여 해결할 수 있다.

코드 12-46 파일에 다음에 요청할 update_id 기록하기

if __name__ == '__main__':
    try:
        with open('last_update_id.txt', 'r') as file:  # ❶
            next_update_id = int(file.read())
    except (FileNotFoundError, ValueError):  # ❷
        next_update_id = 0
    while True:
        last_update_id = check_messages_and_response(next_update_id)
        if last_update_id:
            next_update_id = last_update_id + 1
            with open('last_update_id.txt', 'w') as file:  # ❸
                file.write(str(next_update_id))
        time.sleep(5)

❶ 프로그램을 시작할 때, 파일을 열어 다음 요청 update_id를 읽어들인다. ❷ 파일이 없거나 잘못되었을 수도 있기 때문에 예외 발생시 값을 0으로 지정한다. ❸ 메시지 확인과 답신을 완료할 때마다 파일에 update_id를 기록한다. 파일을 읽고 쓰는 법을 모르겠다면 11.4절을 참고하도록 한다.

여러분은 텔레그램 챗봇 웹 API를 이용해 가장 기본적인 챗봇을 만드는 데 성공했다. 이제 기본은 알았고 응용만 하면 된다. 메시지를 받은 뒤 프로그램이 어떤 일을 하도록 하거나, 좀더 의미있는 메시지를 답신하도록 하면 더욱 유용한 챗봇 프로그램을 만들 수 있을 것이다.