def zendesk_flow(recipient_id):
    """
    Temporary function to handle the migration of users with currently open tickets when Zendesk was turned on.
    This function should be removed once all of these tickets have been closed and migrated.
    """
    fb_json = facebook_requests.get_user_info(recipient_id)
    first_name = fb_json['first_name']
    last_name = fb_json['last_name']
    name = str(first_name + last_name).strip().replace(' ', '').lower()
    users = User.query.filter_by(first_name=name).all()
    for user in users:
        if user.id == user.zendesk_id:
            new_user = User(fb_id=recipient_id,
                            first_name=first_name,
                            last_name=last_name,
                            zendesk_id=user.zendesk_id)
            db.session().add(new_user)
            ZendeskTicket.query.filter_by(user_id=user.zendesk_id).delete()
            User.query.filter_by(id=user.id).delete()
            facebook_requests.post_text(
                recipient_id,
                "Hello! We've recently activated a bot on our Facebook page. To continue "
                "helping you with your active support issue, please provide your email."
            )
            redis_store.set(recipient_id, SessionStates.PROVIDING_EMAIL.value)
            db.session().commit()
            return False
        db.session().commit()
    return True
def recently_solved_action(event):
    """
    Handles the user's reply to the inquiry about a recently solved ticket.
    :param event: JSON containing information about the interaction.
    """
    recipient_id = event['sender']['id']
    payload = trim_payload(full_str=event["postback"]["payload"],
                           payload=Payloads.RECENTLY_SOLVED_TICKET)

    if payload == Payloads.ADD_TO_TICKET.value:
        redis_store.set(recipient_id,
                        SessionStates.ADDING_COMMENT_TO_TICKET.value)
        facebook_requests.post_image(recipient_id,
                                     constants.OWLY_LISTENING_GIF)
        facebook_requests.post_text(recipient_id,
                                    constants.ADD_COMMENT_TO_TICKET)

    elif payload == Payloads.NEW.value:
        ZendeskTicket.query.filter_by(
            user_id=recipient_id, status=ZendeskStatus.SOLVED.value).delete()
        db.session().commit()
        support_action(event)

    elif payload == Payloads.LEARNING.value:
        ZendeskTicket.query.filter_by(
            user_id=recipient_id, status=ZendeskStatus.SOLVED.value).delete()
        db.session().commit()
        facebook_requests.post_image(recipient_id, constants.OWLY_YES_GIF)
        facebook_requests.post_text(recipient_id,
                                    constants.REMEMBER_YOU_CAN_ASK)
        social_networks_action(event)
def article_links_action(event):
    """
    Handles the postback when the user selects a learning objective from a social network, and we need to display links.
    :param event: JSON containing information about the interaction.
    """
    recipient_id = event['sender']['id']
    objective_payload = trim_payload(full_str=event["postback"]["payload"],
                                     payload=Payloads.ARTICLE_LINKS)

    # Handle possible auxiliary options of switching networks or ending session
    if objective_payload == Payloads.SWITCH.value:
        switch_networks_action(event)
        return
    elif objective_payload == Payloads.DONE.value:
        done_action(event)
        return

    # Retrieve and send the list of article links
    objective = LearningObjective.query.filter_by(id=objective_payload).first()
    text = constants.HERES_SOME_INFO_ON % (objective.display_text,
                                           objective.network.display_text)
    facebook_requests.post_text(recipient_id, text)

    db.session().add(UserObjective(recipient_id, objective.id))
    db.session().commit()
    send_article_links_helper(recipient_id, objective.network, objective)
def send_message():
    from hootbot.models.dao.scheduled_message_content import ScheduledMessageContent
    from hootbot.api.facebook import facebook_requests
    day = request.args.get("day")
    fb_id = request.args.get("id")
    content = ScheduledMessageContent.query.filter_by(day=day, topic="daily_tips").first()
    facebook_requests.post_text(fb_id, content.description.replace('<br>', u'\u000A'))
    facebook_requests.post_generic_template(fb_id, content.title, content.image_url, content.link)
    return "ok"
def done_action(event):
    """
    Handles the postback from when the user claims they're done.
    :param event: JSON containing information about the interaction.
    """
    recipient_id = event['sender']['id']
    facebook_requests.post_text(recipient_id, constants.THANKS_FOR_STOPPING_BY)
    facebook_requests.post_image(recipient_id, constants.FIST_BUMP_GIF)

    UserObjective.query.filter_by(user_id=recipient_id).delete()
    db.session().commit()
def get_started_action(event):
    """
    Handles the postback from when a user taps on 'Get Started'.
    :param event: JSON containing information about the interaction.
    """
    recipient_id = event['sender']['id']
    facebook_requests.post_text(recipient_id, constants.HI_THERE_IM_OWLY)
    facebook_requests.post_image(recipient_id, constants.OWLY_WAVING_GIF)

    options = [(Payloads.ASK_TIPS.value, "Teach me!"),
               (Payloads.SUPPORT.value, "Support please!")]
    facebook_requests.post_button_list(recipient_id,
                                       constants.WANT_TO_LEARN_OR_SUPPORT,
                                       Payloads.SOCIAL_OR_SUPPORT, options)
Example #7
0
def reply_to_stop_tips_action(event):
    """
    Handles the user saying whether or not they want to stop tips.
    :param event: JSON containing information about the interaction.
    """
    recipient_id = event['sender']['id']

    payload = trim_payload(full_str=event["postback"]["payload"],
                           payload=Payloads.WIT_STOP_TIPS)
    if payload == Payloads.YES.value:
        ScheduledMessage.query.filter_by(facebook_id=recipient_id).delete()
        db.session().commit()
        facebook_requests.post_text(recipient_id,
                                    constants.IF_YOU_CHANGE_YOUR_MIND)
    elif payload == Payloads.NO.value:
        facebook_requests.post_text(recipient_id, constants.GREAT_STAY_TUNED)
def short_intro_action(event):
    """
    Assume user has already seen the intro options about scheduled messages, and skip that part.
    :param event: JSON containing information about the interaction.
    """
    recipient_id = event['sender']['id']
    facebook_requests.post_text(recipient_id, constants.HI_THERE_IM_OWLY)
    facebook_requests.post_image(recipient_id, constants.OWLY_WAVING_GIF)

    options = [(Payloads.SOCIAL.value, "Teach me!"),
               (Payloads.SUPPORT.value, "Support please!")]
    facebook_requests.post_image(recipient_id,
                                 constants.WAVING_QUESTION_MARKS_GIF)
    facebook_requests.post_button_list(recipient_id,
                                       constants.WANT_TO_LEARN_OR_SUPPORT,
                                       Payloads.SOCIAL_OR_SUPPORT, options)
def restart_scheduled_messages_action(event):
    """
    Handles the postback from when the user claims they want to restart their scheduled messages.
    :param event: JSON containing information about the interaction.
    """
    recipient_id = event['sender']['id']
    payload = trim_payload(full_str=event["postback"]["payload"],
                           payload=Payloads.RESTART_SCHEDULED_MESSAGES)

    if payload == Payloads.YES.value:
        message = ScheduledMessage.query.filter_by(
            facebook_id=recipient_id).first()
        message.next_day_to_send = 1
        db.session().commit()
        facebook_requests.post_text(recipient_id,
                                    constants.SCHEDULED_TIPS_RESET)
    social_networks_action(event)
Example #10
0
def support_action(event):
    """
    Handles the postback from when the user claims they want to speak to support.
    :param event: JSON containing information about the interaction.
    """
    recipient_id = event['sender']['id']
    user = User.query.filter_by(id=recipient_id).first()

    # If the user has an email address defined, we skip asking for their email
    if user.email:
        facebook_requests.post_text(recipient_id,
                                    constants.SUPPORT_REQUEST_ALREADY_EMAIL)
        redis_store.set(recipient_id,
                        SessionStates.DEFINING_SUPPORT_REQUEST.value)
    # If we didn't have their email, we first ask them to provide that, and validate
    else:
        facebook_requests.post_text(recipient_id, constants.GET_EMAIL_FIRST)
        redis_store.set(recipient_id, SessionStates.PROVIDING_EMAIL.value)
Example #11
0
def support_or_learn_action(event):
    """
    Handles the postback from when the user confirms they want support after Wit assumed they did.
    :param event: JSON containing information about the interaction.
    """
    payload = trim_payload(full_str=event["postback"]["payload"],
                           payload=Payloads.WIT_SUPPORT_OR_LEARN)
    if payload == Payloads.SUPPORT.value:
        facebook_actions.support_action(event)
    elif payload == Payloads.HELP.value:
        facebook_requests.post_text(event['sender']['id'],
                                    constants.HELP_CENTER_HAS_GREAT_INFO)
        facebook_requests.post_generic_template(
            event['sender']['id'], "Help Desk Articles",
            "https://hootsuite.com/uploads/images/stock/Help_Desk_Articles.jpg",
            "https://hootsuite.com/help")
    else:
        facebook_actions.social_networks_action(event)
Example #12
0
def article_links_action(event):
    """
    At this point, the user has selected a network and objective, so we display article links
    :param event: JSON containing information about the interaction.
    """
    recipient_id = event['sender']['id']
    full_payload = trim_payload(full_str=event["postback"]["payload"],
                                payload=Payloads.WIT_ARTICLE_LINKS)
    split_str = full_payload.split("_")
    network = SocialNetwork.query.filter_by(id=split_str[0]).first()
    objective = LearningObjective.query.filter_by(network=network).filter_by(
        display_text=split_str[1]).first()
    text = constants.HERES_SOME_INFO_ON % (objective.display_text,
                                           network.display_text)
    facebook_requests.post_text(recipient_id, text)
    db.session().add(UserObjective(recipient_id, objective.id))
    db.session().commit()
    facebook_actions.send_article_links_helper(recipient_id, network,
                                               objective)
Example #13
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"
Example #14
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)
Example #15
0
def new_to_hootsuite_action(event):
    """
    Handles the postback from when the user answers whether or not they want daily tips.
    :param event: JSON containing information about the interaction.
    """
    recipient_id = event['sender']['id']
    payload = trim_payload(full_str=event["postback"]["payload"],
                           payload=Payloads.NEW)

    if payload == Payloads.YES.value:

        scheduled_message, existed = ScheduledMessage.get_or_create(
            search_key={"facebook_id": recipient_id},
            facebook_id=recipient_id,
            topic="daily_social_tips")

        if existed:
            text = constants.CURRENTLY_RECEIVING_TIPS_ON_DAY % scheduled_message.next_day_to_send
            options = [(Payloads.YES.value, "Yes please!"),
                       (Payloads.NO.value, "No thanks!")]
            facebook_requests.post_button_list(
                recipient_id, text, Payloads.RESTART_SCHEDULED_MESSAGES,
                options)
            return
        else:
            facebook_requests.post_text(recipient_id,
                                        constants.EXPECT_TIP_EACH_WEEKDAY)
    else:
        facebook_requests.post_text(recipient_id,
                                    constants.STEP_FURTHER_GET_CERTIFIED)
        facebook_requests.post_generic_template(
            recipient_id, "Hootsuite Pro Certification",
            "https://hootsuite.com/uploads/images/stock/Chatbot-Daily-Icons-Hoot-Pro.png",
            "https://education.hootsuite.com/enroll/20308?coupon=H00tB0tPlatform"
        )
    social_networks_action(event)
Example #16
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
def handle_support_paths(event, user):
    """
    Helper method to handle different states of a customer support flow a user may be in.
    This section has various different rules and is thus heavily commented for clarification.
    :param event: JSON containing information about the interaction.
    :param user: The User object these paths are for
    :return: True if execution should continue handling the event, False if not.
    """
    recipient_id = event['sender']['id']
    user_state = redis_store.get(recipient_id)

    # If this is a postback and the user has open tickets, we can't go through a normal postback flow
    # because that will interfere with the Zendesk integration and adding comments to the ticket.
    # Instead, we don't allow them to interact with buttons while a ticket is currently open.
    if 'postback' in event and (
            user.get_open_tickets()
            or user_state == SessionStates.DEFINING_SUPPORT_REQUEST.value
            or user_state == SessionStates.ADDING_COMMENT_TO_TICKET.value):
        facebook_requests.post_text(recipient_id,
                                    constants.LET_YOU_GET_BACK_TO_THAT)
    # This state occurs after the user had a solved, but not closed, ticket, and we asked them whether
    # they wanted to add a comment to the solved ticket, create a new ticket, or keep learning.
    # We take action on what they replied in the below code block.
    elif user_state == SessionStates.REPLYING_TO_RECENT_TICKET_QUESTION.value:
        redis_store.delete(recipient_id)
        if 'postback' in event:
            handle_postback(event)
        else:
            facebook_requests.post_text(
                recipient_id,
                "Please select one of the options in the menu below!")
            facebook_actions.recent_ticket_question_action(event)
    # When a user wants to create a ticket, if their user object does not have an email already attached to it
    # in the database, we ask them for it and handle validation in the following branch.
    elif user_state == SessionStates.PROVIDING_EMAIL.value:
        if 'message' in event and 'text' in event['message']:
            message = event['message']['text'].strip()
            if message.lower() == 'cancel':
                redis_store.delete(recipient_id)
                facebook_actions.social_networks_action(event)
            elif validate_email(event['message']['text']):
                user = User.query.filter_by(id=recipient_id).first()
                user.email = event['message']['text']
                db.session().commit()
                redis_store.delete(recipient_id)
                facebook_requests.post_text(
                    recipient_id, constants.SUPPORT_REQUEST_NEW_EMAIL)
                redis_store.set(recipient_id,
                                SessionStates.DEFINING_SUPPORT_REQUEST.value)
            else:
                facebook_requests.post_text(recipient_id,
                                            constants.INVALID_EMAIL)
        else:
            facebook_requests.post_text(recipient_id, constants.INVALID_EMAIL)
    # This state occurs when the user said they wanted to create a ticket, and they have no other
    # active/solved tickets in the database. In this case, we asked them to define what they wanted
    # support for. First, we need their email, so we ask for that if we didn't already have it, which is handled above.
    # Following, we ask for them to define their request, and create a ticket/add comments with these messages.
    # We also handle the edge case where the user presses a button while we're expecting text.
    elif user_state == SessionStates.DEFINING_SUPPORT_REQUEST.value:
        zendesk_requests.create_ticket_helper(event, user)
        redis_store.delete(recipient_id)
    # This state occurs when the user said they wanted to add a comment to their recently solved ticket.
    # If they have more than one solved ticket we raise an exception as this is an unexpected state.
    # We then add a comment to the solved ticket and reopen the ticket.
    elif user_state == SessionStates.ADDING_COMMENT_TO_TICKET.value:
        solved_tickets = user.get_solved_tickets()
        if len(solved_tickets) > 1:
            raise ValueError(
                "User has more than one 'solved' ticket - unsure which ticket to reopen."
            )
        ticket = solved_tickets[0]
        # Re-open the Zendesk ticket and add the comment
        zendesk_requests.reopen_ticket(ticket.id)
        zendesk_requests.verify_user_helper(user.zendesk_id)
        zendesk_requests.add_comment_helper(recipient_id, ticket.id, event)
        redis_store.delete(recipient_id)
    # Finally, handle the general state of the user having tickets attached to them.
    elif user.tickets:
        open_tickets = user.get_open_tickets()
        solved_tickets = user.get_solved_tickets()
        # Having more than one open or one solved ticket is an invalid state
        if len(open_tickets) > 1 or len(solved_tickets) > 1:
            raise ValueError(
                "User has more than one 'open' or 'solved' ticket - unsure which ticket to target."
            )
        # If the user has an open ticket, we simply assume they're adding a comment to their ticket
        if open_tickets:
            zendesk_requests.verify_user_helper(user.zendesk_id)
            zendesk_requests.add_comment_helper(recipient_id,
                                                open_tickets[0].id, event)
        # If the user has a solved ticket we ask them which actions they'd like to take (add to, create new, learn)
        elif solved_tickets:
            facebook_actions.recent_ticket_question_action(event)
        else:
            raise AttributeError(
                "User has tickets, but they're neither solved nor open - invalid state"
            )
    else:
        # If none of these states evaluated to true, the user is not currently involved with support at all.
        # We retrieve their name if it doesn't exist, then return True to continue onto the normal flow.
        if not user.first_name or not user.last_name:
            facebook_requests.populate_user_info(user)
        return True
    return False