from rest_framework.views import APIView
from permabots.models import MessengerBot, MessengerMessage
from rest_framework.response import Response
from rest_framework import status
import logging
from permabots.tasks import handle_messenger_message
from datetime import datetime
from permabots import caching
import sys
import traceback
from time import mktime
from six import iteritems
logger = logging.getLogger(__name__)
[docs]class OnlyTextMessages(Exception):
pass
[docs]class Resource(object):
[docs] def to_json(self):
output_json = {}
for obj_key, json_key in iteritems(self.property_mapping()):
attr = getattr(self, obj_key)
if attr is not None:
output_json[json_key] = attr
return output_json
@classmethod
[docs] def from_json(cls, json):
mapping = {v: k for k, v in iteritems(cls.property_mapping())}
return cls(**{mapping[key]: value for key, value in iteritems(json) if key in mapping})
@classmethod
[docs] def property_mapping(cls):
"""
A map of property name to json key name for properties that can be simply serialized to/from json
(no objects, etc.)
"""
raise NotImplementedError('Resource objects must define a property_mapping')
[docs]class MessengerTextMessage(Resource):
def __init__(self, mid, seq, text):
self.mid = mid
self.seq = seq
self.text = text
@classmethod
[docs] def property_mapping(cls):
return {
'mid': 'mid',
'seq': 'seq',
'text': 'text'
}
[docs]class MessengerPostbackMessage(Resource):
def __init__(self, payload):
self.payload = payload
@classmethod
[docs] def property_mapping(cls):
return {
'payload': 'payload'
}
[docs]class MessengerMessaging(Resource):
def __init__(self, sender=None, recipient=None, timestamp=None, type=None, message=None):
self.sender = sender
self.recipient = recipient
self.timestamp = timestamp
self.type = type
self.message = message
@property
def is_message(self):
return self.type == 'message'
@property
def is_postback(self):
return self.type == 'postback'
@property
def is_delivery(self):
return self.type == 'delivery'
@classmethod
[docs] def property_mapping(cls):
return {}
[docs] def to_json(self):
output_json = super(MessengerMessaging, self).to_json()
output_json['sender'] = {'id': self.sender}
output_json['recipient'] = {'id': self.recipient}
output_json['timestamp'] = int(mktime(self.timestamp.timetuple()))
output_json[self.type] = self.message.to_json()
return output_json
@classmethod
[docs] def from_json(cls, json):
message = super(MessengerMessaging, cls).from_json(json)
if 'timestamp' in json:
message.timestamp = datetime.fromtimestamp(json['timestamp']/1000.)
if 'sender' in json:
message.sender = json['sender']['id']
if 'recipient' in json:
message.recipient = json['recipient']['id']
if 'message' in json:
message.type = 'message'
message.message = MessengerTextMessage.from_json(json['message'])
elif 'postback' in json:
message.type = 'postback'
message.message = MessengerPostbackMessage.from_json(json['postback'])
else:
message.type = 'delivery'
return message
[docs]class MessengerEntry(Resource):
def __init__(self, page_id, time=None, messaging=None):
self.page_id = page_id
self.time = time
self.messaging = messaging
@classmethod
[docs] def property_mapping(cls):
return {
'page_id': 'id',
}
[docs] def to_json(self):
output_json = super(MessengerEntry, self).to_json()
output_json['time'] = int(mktime(self.time.timetuple()))
output_json['messaging'] = [message.to_json() for message in self.messaging]
return output_json
@classmethod
[docs] def from_json(cls, json):
entry = super(MessengerEntry, cls).from_json(json)
if 'time' in json:
entry.time = datetime.fromtimestamp(json['time']/1000.)
if 'messaging' in json:
entry.messaging = [MessengerMessaging.from_json(msg) for msg in json['messaging']]
return entry
[docs]class Webhook(Resource):
def __init__(self, object, entries=None):
self.entries = entries
[docs] def to_json(self):
output_json = {'object': "page",
'entry': [entry.to_json() for entry in self.entries]}
return output_json
@classmethod
[docs] def from_json(cls, json):
webhook = super(Webhook, cls).from_json(json)
if 'entry' in json:
webhook.entries = [MessengerEntry.from_json(entry) for entry in json['entry']]
return webhook
@classmethod
[docs] def property_mapping(cls):
return {
'object': 'object',
}
[docs]class MessengerHookView(APIView):
"""
View for Facebook Messenger webhook
"""
[docs] def get(self, request, hook_id):
"""
Verify token when configuring webhook from facebook dev.
MessengerBot.id is used for verification
"""
try:
bot = caching.get_or_set(MessengerBot, hook_id)
except MessengerBot.DoesNotExist:
logger.warning("Hook id %s not associated to a bot" % hook_id)
return Response(status=status.HTTP_404_NOT_FOUND)
if request.query_params.get('hub.verify_token') == str(bot.id):
return Response(int(request.query_params.get('hub.challenge')))
return Response('Error, wrong validation token')
[docs] def create_message(self, webhook_message, bot):
if webhook_message.is_message:
type = MessengerMessage.MESSAGE
text = webhook_message.message.text
postback = None
else:
type = MessengerMessage.POSTBACK
text = None
postback = webhook_message.message.payload
message, _ = MessengerMessage.objects.get_or_create(bot=bot,
sender=webhook_message.sender,
recipient=webhook_message.recipient,
timestamp=webhook_message.timestamp,
type=type,
text=text,
postback=postback)
caching.set(message)
return message
[docs] def post(self, request, hook_id):
"""
Process Messenger webhook.
1. Get an enabled Messenger bot
3. For each message serialize
4. For each message create :class:`MessengerMessage <permabots.models.messenger_api.MessengerMessage>`
5. Delay processing of each message to a task
6. Response provider
"""
try:
bot = caching.get_or_set(MessengerBot, hook_id)
except MessengerBot.DoesNotExist:
logger.warning("Hook id %s not associated to a bot" % hook_id)
return Response(status=status.HTTP_404_NOT_FOUND)
logger.debug("Messenger Bot %s attending request %s" % (bot, request.data))
webhook = Webhook.from_json(request.data)
for webhook_entry in webhook.entries:
for webhook_message in webhook_entry.messaging:
try:
if webhook_message.is_delivery:
raise OnlyTextMessages
message = self.create_message(webhook_message, bot)
if bot.enabled:
logger.debug("Messenger Bot %s attending request %s" % (bot, message))
handle_messenger_message.delay(message.id, bot.id)
else:
logger.error("Message %s ignored by disabled bot %s" % (message, bot))
except OnlyTextMessages:
logger.warning("Not text message %s for bot %s" % (message, hook_id))
except:
exc_info = sys.exc_info()
traceback.print_exception(*exc_info)
logger.error("Error processing %s for bot %s" % (webhook_message, hook_id))
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response(status=status.HTTP_200_OK)