Usando datos del API de Facebook en tu bot de Messenger
Este tutorial muestra cómo crear un servicio de Django que obtenga información del Graph API de Facebook para que sea consumida por un bot de Messenger. El código de demo está disponible en GitHub y el proyecto de muestra se puede probar en este bot.
Descripción del Proyecto
El proyecto que mostramos en este tutorial es un bot de Messenger que ayuda a los miembros de nuestro grupo de Facebook de Developer Circles a descubrir el contenido más relevante del grupo. Este proyecto obtiene información del grupo de Facebook usando el Graph API, posteriormente analiza la información y finalmente la expone a través de una respuesta JSON para que pueda mostrarse en un bot de Messenger.
El siguiente diagrama muestra una vista básica de los componentes del proyecto. Nos enfocaremos en el lado derecho de la imagen. En particular en el Content Parser Service
el cual es el encargado de obtener y procesar la información del API y de entregarla en formato JSON. Este servicio puede estar hosteado en el mismo servidor que el bot o en uno diferente, lo cual facilita su integración.
Pre-requisitos
- Python y pipenv instalados.
- Familiaridad con la biblioteca requests de Python.
- Saber cómo crear proyectos de Django y familiaridad con conceptos de Django como views, responses y templates.
- Saber cómo configurar un bot de Messenger en cualquier plataforma que permita consumir un servicio web o API. En este tutorial usaremos Chatfuel.
Configurando el ambiente virtual usando pipenv
Primero, creamos una nueva carpeta para nuestro proyecto y nos movemos dentro de él
mkdir fb_groups_api_for_messenger && cd fb_groups_api_for_messenger
Después usamos pipenv para instalar Django
pipenv install django
Luego activamos nuestro ambiente virtual
pipenv shell
Ahora creamos nuestro proyecto de Django
django-admin startproject fb_groups_api_for_messenger
Y finalmente creamos una app para nuestro proyecto
python manage.py startapp fb_group_data
Configurando el proyecto de Django
Con nuestro proyecto Django ya configurado agregaremos algunas urls, una es para completar el proceso de autenticación de Facebook y las otras son para que el usuario seleccione el grupo y la información que desea obtener.
Estas urls se agregan al archivo de urls de la app fb_group_data
.
urlpatterns = [
path('fb_login_redirect', views.fb_login_redirect, name='fb_login_redirect'),
path('', views.home, name='home'),
path('group/<int:group_id>', views.group, name='group'),
path('group/<int:group_id>/weekly_summary?group_name=<str:group_name>'
'&format=<str:resp_format>', views.group_weekly_summary,
name='group_weekly_summary'),
En nuestras urls usamos estos parámetros:
group_id
: Para saber de que grupo obtendremos info.group_name
: Se usa como etiqueta en una vista HTML.resp_format
: Para indicar el formato de respuesta deseado, html o json.
Y ahora incluimos estas urls en el archivo de urls del proyecto
urlpatterns = [
path('', include('fb_group_data.urls', namespace='fb')),
Luego agregamos las views correspondientes a estas urls, en las siguientes secciones describiremos a detalle cómo funcionan estas views.
def fb_login_redirect(request):
@login_required(login_url=settings.FB_LOGIN_URL)
def home(request):
@login_required(login_url=settings.FB_LOGIN_URL)
def group(request, group_id):
@login_required(login_url=settings.FB_LOGIN_URL)
def group_weekly_summary(request, group_id, group_name, resp_format='html'):
No olvides ejecutar las migraciones de Django para que puedas ejecutar y probar el proyecto.
python manage.py migrate
Settings y .env
Los valores a los que se hace referencia con el prefijo settings
están definidos en el archivo de settings de Django y algunos de ellos se importan usando la biblioteca dotenv
, de esta forma podemos mantener está información segura en nuestro servidor y evitar exponerla cuando enviemos nuestro código a servidores públicos como GitHub.
import os
from pathlib import Path
from dotenv import load_dotenv
# Loading values from .env file
basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'))
# Facebook API settings, should remain secret and not be uploaded to public repositories
FB_APP_ID = os.getenv('FB_APP_ID')
FB_APP_SECRET = os.getenv('FB_APP_SECRET')
FB_LOGIN_STATE_PARAM = os.getenv('FB_LOGIN_STATE_PARAM')
# Constants used for building URLs of the Graph API endpoints
GRAPH_API_VERSION = 'v8.0'
HOST_NAME = 'http://localhost:8000/'
FB_LOGIN_REDIRECT_PATH = 'fb_login_redirect'
FB_LOGIN_REDIRECT_URI = f'{HOST_NAME}' + FB_LOGIN_REDIRECT_PATH
FB_AUTH_PARAMS = f'client_id={FB_APP_ID}&redirect_uri={FB_LOGIN_REDIRECT_URI}'
FB_LOGIN_URL = f'https://www.facebook.com/{GRAPH_API_VERSION}/dialog/oauth?{FB_AUTH_PARAMS} \
&state={FB_LOGIN_STATE_PARAM}&scope=groups_show_list'
GRAPH_API_BASE_URL = f'https://graph.facebook.com/{GRAPH_API_VERSION}'
GRAPH_API_ACCESS_TOKEN_PATH = 'oauth/access_token'
FB_APP_ACCESS_TOKEN_URL = f'{GRAPH_API_BASE_URL}/{GRAPH_API_ACCESS_TOKEN_PATH}?client_id={FB_APP_ID} \
&grant_type=client_credentials'
Proceso Manual de Login en Facebook
Actualmente el uso principal del API de Facebook sucede en plataformas cliente como el navegador web o aplicaciones móviles por ello la documentación contiene poco contenido sobre plataformas de servidor así que prácticamente solo nos queda la opción de realizar manualmente el inicio de sesión, así que eso es lo que haremos.
El flujo de inicio de sesión comienza en nuestra vista home
@login_required(login_url=settings.FB_LOGIN_URL)
def home(request):
Debido a que agregamos el decorador login_required
a nuestra view el usuario no tiene una sesión activa será redirigido a la siguiente URL para que inicie sesión:
FB_LOGIN_URL = f'https://www.facebook.com/{GRAPH_API_VERSION}/dialog/oauth?{FB_AUTH_PARAMS} \
&state={FB_LOGIN_STATE_PARAM}&scope=groups_show_list'
Los parámetros de esta URL están definidos en el archivo auxiliar fb_api_requests_urls.py
y las constantes mostradas se cargan de igual manera desde el archivo settings como se describió en la sección de configuración del proyecto.
Después de que el usuario inicia sesión y autoriza nuestra aplicación, se llama a nuestra URL callback, la cual se procesa en esta view:
def fb_login_redirect(request):
"""
Process the response from the FB Login, checks the state and error fields, if no errors were
found, it uses the code from the response to request an auth token. Adds the auth token to
the session.
:param request: Django request object :return: Redirect to Home page to execute token
validation if success, otherwise Error view (#todo)
"""
# Check existing sessions
saved_token = request.session.get(fb_api.KEY_FB_AUTH_TOKEN, None)
if not fb_api.validate_auth_token(request, saved_token):
return HttpResponse("Error validating token")
# Validate CSRF and check if there was an error
state = request.GET['state']
# TODO Pretty error pages
if not state == settings.FB_LOGIN_STATE_PARAM:
return 'CSRF Error'
if 'error' in request.GET:
error = request.GET['error']
return f'Login Error: {error}'
# Parse auth response
auth_code = request.GET['code']
auth_token_request = requests.get(fb_api.build_auth_token_url(auth_code))
token_response = auth_token_request.json()
# TODO Validate permissions
# https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow#permscheck
# Save token in session and redirect to main page
request.session[fb_api.KEY_FB_AUTH_TOKEN] = token_response['access_token']
return redirect('fb:home')
Este código es algo extenso, pero básicamente verifica la validez de auth_token
, si no es válida o si no existe un token guardado anteriormente, se solicita un token nuevo y se redirige al usuario nuevamente al view home
en donde podrá continuar con el flujo normal para seleccionar el grupo y el tipo de contenido.
El API de Grupos de Facebook
Una vez en la view home
se muestra al usuario una lista de los grupos que administra.
Esta vista web es creada por la view home
, y en el siguiente código vemos que usamos una función auxiliar para obtener los grupos deseados y luego simplemente se muestran usando un template de Django.
@login_required(login_url=settings.FB_LOGIN_URL)
def home(request):
"""
Shows a view that allows the user to select the FB group to analyze
:param request: Django request object :return: Home page view or redirects to FB login
"""
user_managed_groups = get_managed_groups(request)
context = {'groups': user_managed_groups}
# TODO Pretty templates
return render(request, 'fb_data_miner/groups.html', context)
Este es el código de la función auxiliar get_managed_groups
def get_managed_groups(request):
user_managed_groups = []
auth_token = request.session[fb_api.KEY_FB_AUTH_TOKEN]
user_groups_url = fb_api.build_user_groups_url(request.user.fbprofile.fb_id, auth_token)
while True:
user_groups_response = requests.get(user_groups_url)
user_groups_dict = user_groups_response.json()
if not group_request_has_data(user_groups_dict):
print('no data')
break
user_groups = user_groups_dict["data"]
managed_groups = list(filter(lambda g: g['administrator'], user_groups))
user_managed_groups.extend(managed_groups)
if not group_request_has_next(user_groups_dict):
print('no next')
break
user_groups_url = user_groups_dict['paging']['next']
return user_managed_groups
Esta función contiene un ciclo while que llama al API de grupos para obtener todos los grupos del usuarios, los cuales son entregados en distintas páginas. Y debido a que deseamos obtener solo los grupos que el usuario administra, filtramos la respuesta de cada página usando una función lambda que verifica si la propiedad administrator
de cada grupo tiene un valor de true
.
También es importante notar que el ciclo while terminará cuando la respuesta de cierta página no contiene una propiedad next
la cual es la forma de indicar el final de los resultados, según se describe en la sección de paginación del Graph API.
Cuando seleccionamos la opción Weekly summary
, el usuario verá una lista de los posts más populares que han sido compartidos durante esta semana, aunque también es posible ajustar el periodo de tiempo usando parámetros de las funciones que obtienen la información del grupo.
@login_required(login_url=settings.FB_LOGIN_URL)
def group_weekly_summary(request, group_id, group_name, resp_format='html'):
"""
Shows weekly summary of the group, meaning the top posts (also users and topics?) from that
week, with a default start day of Monday
:param request: Django request object :param group_id: id of the group to get the summary
:param group_name: name of the selected group :param resp_format: format of the reponse, html or json :return: View that shows a weekly summary of a group identified by group_id
"""
auth_token = request.session[fb_api.KEY_FB_AUTH_TOKEN]
group_feed_request_url = fb_api.build_group_feed_url(group_id, auth_token, fb_api.SummaryPeriod.CurrentWeek)
group_feed = get_all_group_post_from_period(group_feed_request_url)
group_feed = list(filter(lambda x: (x.get('shares', {'count': 0})['count'] > 0 or len(x.get('comments', {'data': []})['data']) > 0), group_feed))[:5]
group_feed.sort(key=lambda x: x.get('shares', {'count': 0})['count'] + len(x.get('comments',{'data': []})['data']), reverse=True)
total_comments, top_commented_post, total_shares, top_shared_post = parse_feed_info(group_feed)
El proceso para obtener los posts más populares es muy similar a como obtuvimos los grupos que el usuario administra, usamos un ciclo while (dentro de la función auxiliar get_all_group_post_from_period
) para obtener todas las páginas de resultados de posts y los filtramos los posts de baja interacción usando funciones lambda. Y finalmente ordenamos los posts de mayor a menor interacción para que el usuario vea primer los posts más relevantes.
Continuando con la descripción del view group_weekly_summary
, dependiendo del valor del parámetro format
en la URL, la respuesta puede mostrarse como HTML, como lo hemos estado haciendo, o en formato JSON para que pueda ser consumida por nuestro bot de Messenger.
if resp_format == 'html':
context = {
'group_name': group_name,
'group_feed': group_feed,
'total_comments': total_comments,
'top_commented_post': top_commented_post,
'total_shares': total_shares,
'top_shared_post': top_shared_post,
}
return render(request, 'fb_data_miner/group_weekly_summary.html', context)
else:
for post in group_feed:
message = message_element.copy()
message['title'] = post['message']
message['default_action']['url'] = post['permalink_url']
message_gallery['messages'][0]['attachment']['payload']['elements'].append(message)
return JsonResponse(message_gallery)
Plantillas de Mensajes de Facebook Messenger
Como se puede ver en el código anterior, para construir una respuesta JSON para nuestro bot, usamos plantillas definidas por el Messenger API. Estas plantillas las definimos al inicio del archivo como un diccionario de python y en esta función simplemente colocamos en las plantillas la información que obtuvimos del Graph API.
Consumiendo nuestro servicio desde un bot de Messenger
Finalmente estamos listos para conectar nuestro servicio con nuestro bot. Para realizar este proceso es necesario usar el Messaging API, el cual puede implementarse en cualquier momento en el que recibas un mensaje de Messenger. Y existen algunas plataformas que ya han realizado esta implementación por nosotros y nos permiten usarla solo configurando algunos parámetros. Nosotros usaremos una de estas plataformas, la cual es Chatfuel.
Dentro de un flow de Chatfuel conectamos un elemento mensaje con una nueva action, la cual crearemos arrastrando el punto de conexión de mensaje hacia un área vacía de la vista del flow de Chatfuel, como se muestra en la siguiente imagen.
Se creará un nuevo elemento action, presionamos en el botón +Add action
del nuevo elemento y seleccionamos la opción JSON Request en el menú que aparece.
Cuando seleccionamos esa opción se mostrará una sección para configurar la petición a nuestro servidor como JSON Request, en esta vista, seleccionamos el tipo del request como GET colocamos la url del que corresponde a nuestra vista de Weekly Summary
, asegurándonos de que el parámetro format
tenga el valor de json
Y eso es todo, ya podemos enviar mensajes desde nuestro bot que dispararán una petición de información al API Graph de Facebook que después podemos procesar y enviar al usuario. ¡Espero que haya sido útil!
Referencias
Facebook Graph API Facebook Messenger API Django documentation Chatfuel Docs