def handle_messaging_events(payload): """ Handles a POST from the Messenger API, corresponding to an interaction from a user. :param payload: The JSON payload from the POST request. """ try: data = json.loads(payload) messaging_events = data['entry'][0]['messaging'] for event in messaging_events: recipient_id = event['sender']['id'] res = zendesk_flow(recipient_id) if not res: return # Add this user's Facebook Page ID to the database if it doesn't yet exist user = User.get_or_create(search_key={'id': recipient_id}, fb_id=recipient_id)[0] if not handle_support_paths(event, user): return # Handle the cases where the message is either raw text or a postback elif 'postback' in event: handle_postback(event) elif 'message' in event and 'text' in event['message']: handle_text_message(event) elif endpoint_helpers.event_is_image(event): wit_actions.handle_failed_interpretation(event) else: bot_log("Unable to handle messaging event %s" % event) except (KeyError, ValueError) as e: bot_log( "Handling of message events failed with following message: %s" % e)
def delete_article_links(): payload = request.get_data() data = json.loads(payload) for item in data: bot_log("Admin API: Deleting article link ID " + str(item['id'])) ArticleLink.query.filter_by(id=item['id']).delete() db.session().commit() return "ok"
def delete_learning_objectives(): payload = request.get_data() data = json.loads(payload) for item in data: bot_log("Admin API: Deleting objective ID " + str(item['id'])) objective = LearningObjective.query.filter_by(id=item['id']).first() for link in objective.article_links: ArticleLink.query.filter_by(id=link.id).delete() LearningObjective.query.filter_by(id=objective.id).delete() db.session().commit() return "ok"
def post_data(data): """ Posts JSON data to the Facebook messages API :param data: Data to be sent, as a dictionary """ r = requests.post("https://graph.facebook.com/v2.6/me/messages", params={"access_token": config.FB_ACCESS_TOKEN}, data=json.dumps(data), headers={"Content-type": "application/json"}) if r.status_code != requests.codes.ok: bot_log(r.text)
def post_social_networks(): payload = request.get_data() data = json.loads(payload) for item in data: bot_log("Admin API: Adding network " + str(item['display_text'])) if 'id' in item: SocialNetwork.query.filter_by(id=item['id']).first().display_text = item['display_text'] else: network = SocialNetwork(display_text=item['display_text']) db.session().add(network) db.session().commit() return "ok"
def close_ticket(): """ Zendesk will POST to this endpoint when a ticket that's been tagged with 'hootbot' is set to Closed """ data = json.loads(request.get_data()) try: ZendeskTicket.query.filter_by(id=data['ticket']['id']).delete() db.session().commit() except AttributeError as e: bot_log(e) abort(404) return "ok"
def handle_text_message(event): """ Handles a general text message sent by the user - runs NLP to try to interpret the text. :param event: JSON containing information about the interaction. """ event['message']['text'] = event['message']['text'].encode('utf-8') resp = wit_requests.get_intent(event['message']['text']) try: wit_actions.run_actions(event, resp) except (KeyError, IndexError, TypeError): bot_log("Failed to interpret user message: %s" % event['message']['text']) wit_actions.handle_failed_interpretation(event)
def delete_social_networks(): payload = request.get_data() data = json.loads(payload) for item in data: bot_log("Admin API: Deleting network ID " + str(item['id'])) network = SocialNetwork.query.filter_by(id=item['id']).first() for objective in network.objectives: for link in objective.article_links: ArticleLink.query.filter_by(id=link.id).delete() LearningObjective.query.filter_by(id=objective.id).delete() SocialNetwork.query.filter_by(id=network.id).delete() db.session().commit() return "ok"
def handle_postback(event): """ Handles a postback due to a user interacting with the bot. :param event: JSON containing information about the interaction. """ try: # Parse the key from the postback, removing 'POSTBACK_' and passing to the dictionary query_payload = Payloads(event["postback"]["payload"].split("?=")[0]) facebook_actions_dict[query_payload](event) except (KeyError, IndexError, TypeError, ValueError) as e: bot_log("Handling of postback failed with the following message: %s" % e.message) raise
def post_learning_objectives(): payload = request.get_data() data = json.loads(payload) for item in data: bot_log("Admin API: Adding objective " + str(item['display_text'])) if 'id' in item: LearningObjective.query.filter_by(id=item['id']).first().update_for_optional_params(json=item) else: network = SocialNetwork.query.filter_by(display_text=item['network']).first() objective = LearningObjective(item['display_text'], network) db.session().add(objective) db.session().commit() return "ok"
def solve_ticket(): """ Zendesk will POST to this endpoint when a ticket that's been tagged with 'hootbot' is set to Solved """ data = json.loads(request.get_data()) try: ZendeskTicket.query.filter_by(id=data['ticket']['id']).first( ).status = ZendeskStatus.SOLVED.value db.session().commit() except AttributeError as e: bot_log(e) abort(404) return "ok"
def populate_user_info(user): """ Retrieves the user's first/last name from the API and populates their database entry. :param user: The user to retrieve the information for """ fb_json = get_user_info(user.id) try: user.first_name = fb_json["first_name"] user.last_name = fb_json["last_name"] db.session().commit() except KeyError: bot_log( "First/last name not properly returned from Facebook for FB ID " + user.id)
def run_actions(event, resp): """ Determines and runs required actions for the given Wit response :param event: Payload for the event returned by Facebook :param resp: Response returned by the Wit API """ from hootbot.actions.wit.wit_actions_dict import wit_actions_dict bot_log("Wit response received: " + str(resp)) matching_response, query = get_matching_response(resp) if matching_response == WitResponses.ENTITY: wit_actions_dict[query](event['sender']['id'], query) else: wit_actions_dict[query](event)
def get_user_info(facebook_id): """ Retrieves user information from the Facebook API (currently first and last name) :param facebook_id: The FB Page ID of the user :return: JSON response from the FB API, containing first and last name on success """ r = requests.get("https://graph.facebook.com/v2.6/%s" % facebook_id, params={ "fields": "first_name,last_name", "access_token": config.FB_ACCESS_TOKEN }, headers={"Content-type": "application/json"}) if r.status_code != requests.codes.ok: bot_log(r.text) return r.json()
def configure_logging(): """ Configures logging functionality. """ if config.LOCATION != 'deis': formatter = logging.Formatter( '%(asctime)s - %(levelname)s: %(message)s') handler = RotatingFileHandler('hootbot.log', maxBytes=10**8, backupCount=1) handler.setLevel(logging.DEBUG) handler.setFormatter(formatter) app.logger.addHandler(handler) app.logger.setLevel(logging.DEBUG) bot_log("Starting application...")
def update_user(user_id, data): """ Updates an existing Zendesk user. :param user_id: The Facebook ID of the user. :param data: The JSON data to update the user with/ """ r = requests.put("https://%s.zendesk.com/api/v2/users/%s.json" % (config.ZENDESK_SUBDOMAIN, user_id), data=json.dumps(data), headers={"Content-type": "application/json"}, auth=("%s/token" % config.ZENDESK_EMAIL_ADDRESS, config.ZENDESK_ACCESS_TOKEN)) if r.status_code != requests.codes.ok: bot_log(r.text) else: return r.json()
def admin_token_expiry_job(app, token): """ Scheduled job used to expire stale auth tokens """ with app.app_context(): try: if redis_store.get('SCHEDULED_JOB_TOKEN') != token: return bot_log("Running auth token expiry job") AdminToken.query.filter( AdminToken.created_date <= datetime.utcnow() - timedelta(hours=24)).delete() db.session().commit() except Exception as e: print(e.message) raise
def update_ticket(ticket_id, data): """ Updates an existing Zendesk ticket. :param ticket_id: The ID of the ticket to be updated. :param data: JSON data to update the ticket with. """ r = requests.put("https://%s.zendesk.com/api/v2/tickets/%s.json" % (config.ZENDESK_SUBDOMAIN, ticket_id), data=json.dumps(data), headers={"Content-type": "application/json"}, auth=("%s/token" % config.ZENDESK_EMAIL_ADDRESS, config.ZENDESK_ACCESS_TOKEN)) if r.status_code != requests.codes.ok: bot_log(r.text) else: return r.json()
def post_ticket(data): """ Creates a ticket. :param data: JSON data to be sent to the Zendesk API. :return JSON of reply on success """ r = requests.post("https://%s.zendesk.com/api/v2/tickets.json" % config.ZENDESK_SUBDOMAIN, data=json.dumps(data), headers={"Content-type": "application/json"}, auth=("%s/token" % config.ZENDESK_EMAIL_ADDRESS, config.ZENDESK_ACCESS_TOKEN)) if r.status_code != requests.codes.created: bot_log(r.text) else: return r.json()
def get_comments(ticket_id): """ Gets all comments on an existing Zendesk ticket. :param ticket_id: The Zendesk ID of the ticket to be updated. :return: A list of comments with various parameters, defined here: https://developer.zendesk.com/rest_api/docs/core/ticket_comments#list-comments """ r = requests.get( "https://%s.zendesk.com/api/v2/tickets/%s/comments.json?sort_order=desc" % (config.ZENDESK_SUBDOMAIN, ticket_id), headers={"Content-type": "application/json"}, auth=("%s/token" % config.ZENDESK_EMAIL_ADDRESS, config.ZENDESK_ACCESS_TOKEN)) if r.status_code != requests.codes.ok: bot_log(r.text) else: return r.json()
def add_ticket_comment(ticket_id, data, email): """ Adds a comment to an existing Zendesk ticket, acting as an end user - leverages the ZD Requests API :param ticket_id: The ID of the ticket to be updated. :param data: The JSON data to update the ticket with. :param email: The email address of the requester (the end user updating the ticket) :return The JSON response """ r = requests.put("https://%s.zendesk.com/api/v2/requests/%s.json" % (config.ZENDESK_SUBDOMAIN, ticket_id), data=json.dumps(data), headers={"Content-type": "application/json"}, auth=("%s/token" % email, config.ZENDESK_ACCESS_TOKEN)) if r.status_code != requests.codes.ok: bot_log(r.text) else: return r.json()
def get_intent(message): """ Provides text to Wit and retrieves the assumed intent after NLP :param message: Text message to send to Wit :return: Response JSON from the Wit API """ payload = { "v": "20170510", # Version number "q": message } r = requests.get( "https://api.wit.ai/message", headers={"Authorization": "Bearer " + config.WIT_ACCESS_TOKEN}, params=payload) if r.status_code == requests.codes.ok: return r.json() else: bot_log(r.text)
def configure_scheduler(): """ Configures and starts jobs that need to run on a schedule """ if config.ENVIRONMENT != 'production': return token = binascii.hexlify(os.urandom(32)) redis_store.set('SCHEDULED_JOB_TOKEN', token) bot_log("Configuring scheduled jobs...") from hootbot.jobs.daily_tip_job import daily_tip_job daily_tip_scheduler = BackgroundScheduler() daily_tip_scheduler.start() daily_tip_scheduler.add_job(func=(lambda: daily_tip_job(app, token)), trigger=CronTrigger(day_of_week='mon-fri', hour='16', timezone='UTC'), id='daily_tips_job', name="Sends daily scheduled tips at 9am PST", replace_existing=True) atexit.register(lambda: daily_tip_scheduler.shutdown()) from hootbot.jobs.user_objective_expiry_job import user_objective_expiry_job objective_expiry_scheduler = BackgroundScheduler() objective_expiry_scheduler.start() objective_expiry_scheduler.add_job( func=(lambda: user_objective_expiry_job(app, token)), trigger=IntervalTrigger(hours=1, timezone="UTC"), id='user_objective_expiry_job', name="Periodically clears stale user_objective entries", replace_existing=True) atexit.register(lambda: objective_expiry_scheduler.shutdown()) from hootbot.jobs.admin_token_expiry_job import admin_token_expiry_job admin_token_expiry_scheduler = BackgroundScheduler() admin_token_expiry_scheduler.start() admin_token_expiry_scheduler.add_job( func=(lambda: admin_token_expiry_job(app, token)), trigger=IntervalTrigger(hours=8, timezone="UTC"), id='admin_token_expiry_job', name="Periodically clears stale admin auth tokens", replace_existing=True) atexit.register(lambda: admin_token_expiry_scheduler.shutdown())
def post_article_links(): payload = request.get_data() data = json.loads(payload) for item in data: bot_log("Admin API: Adding link " + str(item['title'])) if 'id' in item: ArticleLink.query.filter_by(id=item['id']).first().update_for_optional_params(json=item) else: network = SocialNetwork.query.filter_by(display_text=item['network']).first() objective = LearningObjective.query.filter_by(display_text=item['objective'], network=network).first() article_link = ArticleLink(item['url'], item['title'], objective, str(item['link_type']).upper(), item.get('description'), item.get('image_url')) db.session().add(article_link) db.session().commit() return "ok"
def add_comment(): """ Zendesk will POST to this endpoint when a support advocate publicly comments on a ticket tagged with 'hootbot' """ data = json.loads(request.get_data()) try: ticket = ZendeskTicket.query.filter_by(id=data['ticket']['id']).first() comment = data['ticket']['comment'].encode('utf-8') # Trim the Hootsuite signature footer if comment.endswith(constants.END_OF_TICKET_SIGNATURE): comment = comment[:-len(constants.END_OF_TICKET_SIGNATURE)] # Facebook only allows sending on 640 characters max, split if the text is larger comments = textwrap.wrap(comment, 640, replace_whitespace=False) for text in comments: facebook_requests.post_text(ticket.user_id, text) except AttributeError as e: bot_log(e) abort(404) except KeyError as e: bot_log(e) abort(500) return "ok"
def handle_verification(): bot_log("Handling Verification...") if request.args.get('hub.verify_token', '') == 'hootastic': bot_log("Verification successful!") return request.args.get('hub.challenge', '') else: bot_log("Verification failed!") return 'Error, wrong validation token'
def send_article_links_helper(recipient_id, network, objective): """ Helper method to send a list of article links to the user. :param recipient_id: Facebook ID of the user this message will be sent to. :param network: Social network to retrieve the links for. :param objective: Learning objective to retrieve the links for. """ links = objective.article_links try: chunk_link_list = optimize_link_list(links) for article_links in chunk_link_list: facebook_requests.post_url_list(recipient_id, article_links) except ValueError as e: bot_log( "Optimization of the list of links failed with the following message: %s" % e.message) facebook_requests.post_text(recipient_id, constants.SORRY_SOMETHING_WENT_WRONG) # Provide the user with more learning options for this network text = "Is there anything else you'd like to learn about %s today?" % network.display_text send_objectives_helper(recipient_id, text, network)
def daily_tip_job(app, token): """ Scheduled job used to send users their daily tips """ if config.ENVIRONMENT != 'production': return with app.app_context(): try: if redis_store.get('SCHEDULED_JOB_TOKEN') != token: return bot_log("Running daily tip job") # If the amount of rows in the table starts getting considerably large, consider using a # yield_per() type of method to get the objects in batches scheduled_messages = ScheduledMessage.query.all() for message in scheduled_messages: try: content = ScheduledMessageContent.query.filter_by(day=message.next_day_to_send, topic="daily_tips").first() # Facebook only seems to accept unicode linebreak in this case so we do string replacement facebook_requests.post_text(message.facebook_id, content.description.replace('<br>', u'\u000A')) facebook_requests.post_generic_template(message.facebook_id, content.title, content.image_url, content.link) if message.next_day_to_send < 11: message.next_day_to_send += 1 else: ScheduledMessage.query.filter_by(id=message.id).delete() except Exception as e: # Swallow inner exception to allow other jobs to continue print(e.message) db.session().commit() except Exception as e: # Raise outer layer exception as it's likely catastrophic to the job print(e.message) raise