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)
Esempio n. 2
0
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"
Esempio n. 3
0
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"
Esempio n. 4
0
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)
Esempio n. 5
0
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"
Esempio n. 6
0
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)
Esempio n. 8
0
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
Esempio n. 10
0
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"
Esempio n. 11
0
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"
Esempio n. 12
0
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)
Esempio n. 13
0
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)
Esempio n. 14
0
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()
Esempio n. 15
0
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...")
Esempio n. 16
0
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()
Esempio n. 17
0
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
Esempio n. 18
0
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()
Esempio n. 19
0
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()
Esempio n. 20
0
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()
Esempio n. 21
0
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()
Esempio n. 22
0
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)
Esempio n. 23
0
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())
Esempio n. 24
0
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"
Esempio n. 25
0
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"
Esempio n. 26
0
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'
Esempio n. 27
0
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)
Esempio n. 28
0
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