갈루아의 반서재

Django, Ngrok를 활용한 페이스북 메신저 봇 만들기

How to build a Facebook Messenger bot using Django, Ngrok



1. Facebook App 생성


먼저 페이스북앱을 생성한다.

https://developers.facebook.com/ > My Apps > Add a New App > Product Setup > Messenger > Get Started

다음으로 페이스북 페이지를 생성하고, Page Access Token 을 아래와 같이 생성한다.

토큰 생성이 필요한 페이지를 선택하면 다음과 같이 Page Access Token 이 생성된다.



2. Django 서버 설치


이 튜토리얼의 목적은 백엔드 서버로서의 장고의 역할에 대한 것이다.

페이스북 메신저 봇 앱을 생성한다.

1
(envalicia) root@localhost:~/vikander# django-admin.py startapp fb_chatbot
cs

방금 만든 메신저봇앱을 추가한다.

1
2
3
4
5
6
7
8
# vikander/vikander/settings.py
# Application definition
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.staticfiles',
#   ...
    'fb_chatbot',
]
cs

url 을 설정한다.

1
2
3
4
5
6
7
8
9
10
# vikander/vikander/urls.py
 
from django.conf import settings
from django.conf.urls import url, include
from django.contrib import admin
 
urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^fb_chatbot/', include('fb_chatbot.urls')),
]
cs

예를 들어 127.0.0.1/fb_chatbot/<rest-of-the-url> 와 같은 요청이 발생하면, fb_chatbot 앱 디렉토리의 urls.py 로 포워딩되므로 fb_chatbot 앱 디렉토리 내에 아래와 같은 urls.py 파일을 만든다.

1
2
3
4
# vikander/fb_chatbot/urls.py
 
from django.conf.urls import include, url
urlpatterns = []
cs


3.  Webhook 을 통한 연결지점 설정


"A webhook is a way for an app to provide other applications with real-time information."

먼저 어디서 지금 만드는 앱과 페이스북이 "hook up" 할지를 결정해야 한다. URL 을 생성하고 업데이트를 보낼 수 있도록 페이스북에 이를 알려준다. 그리고 이렇게 들어오는 요청을 다룰 수 있는 코드를 작성한다. 여기서 주의해야할 점은 이 URL 을 다른 사람이 가로채지 못하도록 비밀로 유지해야 한다는 것이다. 다음과 같이 랜덤한 문자열을 생성하여 활용하면 도움이 된다.

1
2
3
4
>>> import os, binascii
>>> print(binascii.hexlify(os.urandom(25)))
b'14b50b0fe4d2641f80cc7e3f32933b4ddfdf231df9418f030b'
 
cs

그러면 위의 문자열을 활용하여 webhook URL 을 새롭게 정의하자.

1
2
3
4
5
6
# vikander/fb_chatbot/urls.py
from django.conf.urls import include, url
from .views import fb_chatbotView
urlpatterns = [
    url(r'^14b50b0fe4d2641f80cc7e3f32933b4ddfdf231df9418f030b/?$', fb_chatbotView.as_view())
    ]
cs

아래와 같이 뷰를 업데이트한다.

1
2
3
4
5
6
7
8
# vikander/fb_chatbot/views.py
from django.shortcuts import render
from django.views import generic
from django.http.response import HttpResponse
 
class fb_chatbotView(generic.View):
    def get(self, request, *args, **kwargs):
        return HttpResponse("Hello World!")
cs

웹브라우저에 다음과 같은 URL 을 입력해보면 "Hello World!"가 출력되는 것을 볼 수 있다.

1
http://www.django-wiki.com/fb_chatbot/14b50b0fe4d2641f80cc7e3f32933b4ddfdf231df9418f030b
cs


1
http://127.0.0.1:8000/fb_chatbot/14b50b0fe4d2641f80cc7e3f32933b4ddfdf231df9418f030b
cs



4. Ngrok 를 통한 보안연결


Ngrok를 활용하여 로컬호스트와 연결하는 보안 터널을 만든다. 다음과 같이 Ngrok 다운로드 페이지에서 다운받은 후
/usr/local/bin에 압축을 풀고 실행한다(실행시 허가 거부 메시지가 뜰 수 있으니 권한 여부를 반드시 확인한다).

1
2
3
4
5
6
7
8
9
10
11
12
13
(envalicia) root@localhost:~/vikander# ngrok http 80
 
ngrok by @inconshreveable                                            (Ctrl+C to quit)
 
Session Status                online
Version                       2.2.4
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://d7a72add.ngrok.io -> localhost:80
Forwarding                    https://d7a72add.ngrok.io -> localhost:80
 
Connections                   ttl     opn     rt1     rt5     p50     p90
                              0       0       0.00    0.00    0.00    0.00
cs

Ngrok 에 대해 추가적인 내용은 아래에서 확인할 수 있다.

웹브라우저에서 http:d//d7a72add.ngrok.io 로 접속하면 터미널 하단에서 보듯이 해당 요청에 대한 정보를 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
(envalicia) root@localhost:~/vikander# ngrok http 80
 
ngrok by @inconshreveable                                                                                                                                                                                                    (Ctrl+C to quit)
 
Session Status                online
Version                       2.2.4
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://d7a72add.ngrok.io -> localhost:80
Forwarding                    https://d7a72add.ngrok.io -> localhost:80
 
Connections                   ttl     opn     rt1     rt5     p50     p90
                              84      0       0.36    0.19    0.02    0.26
 
HTTP Requests
-------------
 
GET /static/assets/js/libs/daterangepicker.js  200 OK
GET /static/assets/js/libs/tether.min.js       200 OK
GET /static/assets/js/views/draggable-cards.js 200 OK
GET /static/assets/js/libs/pace.min.js         200 OK
GET /static/assets/js/libs/toastr.min.js       200 OK
GET /static/assets/js/libs/bootstrap.min.js    200 OK
GET /static/assets/js/libs/moment.min.js       200 OK
GET /static/assets/css/glyphicons.css          200 OK
GET /static/assets/js/app.js                   200 OK
GET /static/assets/js/views/index.js           200 OK
cs

다시 페이스북 앱 관리 페이지로 돌아가서 webhooks 을 설정하자. 아래 그림에서와 같이 "Setup Webhooks"을 클릭하면 설정값을 입력하는 팝업창이 뜬다.

Callback URL 에는 3. 에서 설정한 Webhook URL을 입력한다. 다만 보안 연결이 필요하므로 https:// 로 시작되어야 한다. 보안연결 설정이 되어 있지 않은 경우 앞서 Ngrok 를 통해 설정한 https://d7a72add.ngrok.io 를 URL 앞 부분에 대체하여 입력한다. 그리고 "Verify Token" 에는 임의의 값을 입력하고 매칭이 되도록 views.py 를 아래와 같이 수정한다.

즉, Ngrok는 로컬호스트를 외부와 연결해주는 역할도 있지만 보안연결 설정에도 유용하게 활용할 수 있다. 다만 기본적으로 다운로드해서 설치하는 경우에는 제한이 있어 실제로 배포하려면 라이센스 구매가 필요하다.

1
2
3
4
5
6
7
8
9
10
from django.shortcuts import render
from django.views import generic
from django.http.response import HttpResponse
# Create your views here.
class fb_chatbotView(generic.View):
    def get(self, request, *args, **kwargs):
        if self.request.GET['hub.verify_token'== '1234567890':
            return HttpResponse(self.request.GET['hub.challenge'])
        else:
            return HttpResponse('Error, invalid token')      
cs

"Verify and Save"를 클릭하면 다음과 같이 설정이 완료된다. 웹훅을 구독할 페이지를 고른 후 구독 버튼을 누른다.



5. 메시지 주고 받기


앞서 구독설정을 마쳤으므로 해당 페이지에 이벤트가 발생할 때 마다 페이스북은 POST 요청을 보내게 된다. 그러면 간단히 사용자가 보낸 메시지를 똑같이 되돌려보내는 기능을 넣어보자.

1) 메시지 수신
받은 메시지를 출력하기 위해 post 함수를 업데이트하자. 장고의 POST 메소드는 csft 토큰을 기본적으로 요구하지만, dispatcher 메소드에서 페이스북 콜백을 다루도록 이를 면제해주자. views.py 를 다음과 같이 수정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# vikander/fb_chatbot/views.py
import json, requests, random, re
from pprint import pprint
 
from django.views import generic
from django.http.response import HttpResponse
 
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
 
VERIFY_TOKEN = '1234567890'
  
class fb_chatbotView(generic.View):
    def get(self, request, *args, **kwargs):
        if self.request.GET['hub.verify_token'== VERIFY_TOKEN:
            return HttpResponse(self.request.GET['hub.challenge'])
        else:
            return HttpResponse('Error, invalid token')
        
    @method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):
        return generic.View.dispatch(self, request, *args, **kwargs)
 
    # Post function to handle Facebook messages
    def post(self, request, *args, **kwargs):
        # Converts the text payload into a python dictionary
        incoming_message = json.loads(self.request.body.decode('utf-8'))
        # Facebook recommends going through every entry since they might send
        # multiple messages in a single call during high load
        for entry in incoming_message['entry']:
            for message in entry['messaging']:
                # Check to make sure the received call is a message call
                # This might be delivery, optin, postback for other events 
                if 'message' in message:
                    # Print the message to the terminal
                    pprint(message)       
        return HttpResponse()
cs

페이스북 페이지를 열고, 메시지를 클릭한다음 내용을 타이핑한다. 다음과 같이 메시지가 수신되는 것을 볼 수 있다.


2) 메시지 보내기

메시지를 보내기 위해서 page access token를 이용한 Facebook Graph API를 통해 POST 콜을 생성해야 한다. json 의 바디 부분에서 사용자의 아이디와 메시지 내용을 담게 된다. 간단히 텍스트만 전송하거나 이미지, 리스트, 링크 등을 포함한 템플릿을 전송할 수도 있다. 자세한 내용은 Facebook Send API reference 를 참고한다. 본 튜토리얼에서는 간단히 텍스트를 되돌려주는 것만 구현한다. 아래와 같이 수신한 텍스트를 가지고 POST 콜을 생성하는 post_facebook_message 함수를 만들어보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import json, requests, random, re
from pprint import pprint
 
from django.views import generic
from django.http.response import HttpResponse
 
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
 
#  ------------------------ Fill this with your page access token! -------------------------------
PAGE_ACCESS_TOKEN = "kvjkxjviHv9xjvkvjkxjvkjkjxkjkxjkjkxjxxkxkxk"
VERIFY_TOKEN = "1234567890"
 
# Helper function
def post_facebook_message(fbid, recevied_message):           
    post_message_url = 'https://graph.facebook.com/v2.6/me/messages?access_token=%s'%PAGE_ACCESS_TOKEN
    response_msg = json.dumps({"recipient":{"id":fbid}, "message":{"text":recevied_message}})
    status = requests.post(post_message_url, headers={"Content-Type""application/json"},data=response_msg)
    pprint(status.json())
    
# Create your views here.
class fb_chatbotView(generic.View):
    def get(self, request, *args, **kwargs):
        if self.request.GET['hub.verify_token'== VERIFY_TOKEN:
            return HttpResponse(self.request.GET['hub.challenge'])
        else:
            return HttpResponse('Error, invalid token')
        
    @method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):
        return generic.View.dispatch(self, request, *args, **kwargs)
 
    # Post function to handle Facebook messages
    def post(self, request, *args, **kwargs):
        # Converts the text payload into a python dictionary
        incoming_message = json.loads(self.request.body.decode('utf-8'))
        # Facebook recommends going through every entry since they might send
        # multiple messages in a single call during high load
        for entry in incoming_message['entry']:
            for message in entry['messaging']:
                # Check to make sure the received call is a message call
                # This might be delivery, optin, postback for other events 
                if 'message' in message:
                    # Print the message to the terminal
                    pprint(message)     
                    post_facebook_message(message['sender']['id'], message['message']['text'])     
        return HttpResponse()
cs

구현된 모습이다. 받은 대로 돌려주고 있는 것을 볼 수 있다.


3) 조크로 응답하기

앞서 만든 봇에 사용자의 메시지에 따라 조크로 응답하는 기능을 추가해보자. 수신된 텍스트와 이에 대응하는 발신할 조크는 아래와 같이 간단한 딕셔너리로 구성한다.

1
2
3
4
5
6
7
8
jokes = {
         'stupid': ["""Yo' Mama is so stupid, she needs a recipe to make ice cubes.""",
                    """Yo' Mama is so stupid, she thinks DNA is the National Dyslexics Association."""],
         'fat':    ["""Yo' Mama is so fat, when she goes to a restaurant, instead of a menu, she gets an estimate.""",
                    """ Yo' Mama is so fat, when the cops see her on a street corner, they yell, "Hey you guys, break it up!" """],
         'dumb':   ["""Yo' Mama is so dumb, when God was giving out brains, she thought they were milkshakes and asked for extra thick.""",
                    """Yo' Mama is so dumb, she locked her keys inside her motorcycle."""
         }
cs


그러면 post_facebook_message 함수를 수정해서 앞서 딕셔너리에 포함된 3개의 키워드를 찾아 조크를 전송해주도록 업데이트하자. 만약 키워드가 없다면 대응하는 조크가 없다는 메시지를 보내자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Helper function
def post_facebook_message(fbid, recevied_message):
    
    tokens = re.sub(r"[^a-zA-Z0-9\s]",' ',recevied_message).lower().split()
    joke_text = ''
    for token in tokens:
        if token in jokes:
            joke_text = random.choice(jokes[token])
            break
    if not joke_text:
        joke_text = "I didn't understand! Send 'stupid', 'fat', 'dumb' for a Yo Mama joke!"       
               
    post_message_url = 'https://graph.facebook.com/v2.9/me/messages?access_token=%s'%PAGE_ACCESS_TOKEN
    response_msg = json.dumps({"recipient":{"id":fbid}, "message":{"text":joke_text}})
    status = requests.post(post_message_url, headers={"Content-Type""application/json"},data=response_msg)
    pprint(status.json())
cs

그러면 아래와 같이 해당 키워드에 맞는 응답을 보내주는 것을 볼 수 있다.


4) 사용자 이름 추가하기
지막으로 여기에 하나를 더해보자. 유저 아이디로부터 기본적인 사용자 정보를 조회하는 API를 통해서 사용자의 이름과 해당 프로필 사진 링크 등을 구할 수 있다. 아래와 같이 views.py의 post_facebook_message 함수 부분을 업데이트하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# vikander/fb_chatbot/views.py
def post_facebook_message(fbid, recevied_message):
    
    tokens = re.sub(r"[^a-zA-Z0-9\s]",' ',recevied_message).lower().split()
    joke_text = ''
    for token in tokens:
        if token in jokes:
            joke_text = random.choice(jokes[token])
            break
    if not joke_text:
        joke_text = "I didn't understand! Send 'stupid', 'fat', 'dumb' for a Yo Mama joke!"       
        
    user_details_url = "https://graph.facebook.com/v2.9/%s"%fbid
  user_details_params = {'fields':'first_name,last_name,profile_pic', 'access_token':PAGE_ACCESS_TOKEN}
    user_details = requests.get(user_details_url, user_details_params).json()
    joke_text = 'Yo '+user_details['first_name']+'..!' + joke_text
               
    post_message_url = 'https://graph.facebook.com/v2.9/me/messages?access_token=%s'%PAGE_ACCESS_TOKEN
    response_msg = json.dumps({"recipient":{"id":fbid}, "message":{"text":joke_text}})
    status = requests.post(post_message_url, headers={"Content-Type""application/json"},data=response_msg)
    pprint(status.json())
   
cs

보는대로 사용자 기존의 응답에 사용자 이름(firstname)이 추가된 것을 알 수 있다.