Alfred the Botler

Made by Melissa Powel

Found in Prototyping Bots

Create, manage, and email contacts directly from a team’s slack channel.

0

Alfred the Botler

Who is Alfred?

Alfred the Botler is based on Batman's British butler Alfred Pennyworth. He is highly educated, but as a bot can be a bit simple. He's learning his new role and soon will be up to tip top shape.

What can he do?

Alfred knows you're very busy and in high demand, which is why he's offered to keep track of your contacts. He's still new to all of this, but eventually he'll be able to automatically store new contacts and start writing an email for you! Sadly, at the moment he's only able to take down your contact's name, email, and phone number, show you your list of contacts, and delete a contact from that list.

What's he like?

He is extremely polished and well behaved, but at times can become quite snarky and his remarks may get a bit off colored. You'll have to watch out for him, especially when you dismiss him. Most of the time, though, he's quite the gentleman.

0

Sample Interaction

0

Prototype Workflow

For the experience prototype, I focused on manually manipulating a database. Below is the primary workflow of adding and viewing a list of contacts. In addition to these features, the final prototype was also able to delete a specific contact or clear the entire contact database.

0

Contact DB Data Schema

0

Alfred's Code

Ruby Gem’s Used

From the app's Gemfile

0
source 'https://rubygems.org'

#Gems used
gem 'sinatra'
gem 'json'
gem 'shotgun'
gem "rake"
gem 'activerecord'
gem 'sinatra-activerecord' # excellent gem that ports ActiveRecord for Sinatra
gem 'activesupport'
gem 'haml'
gem 'slack-ruby-client'
gem 'httparty'

#Gems not used
# gem 'validates_phone_number'
# gem 'contextio'

# to avoid installing postgres use the following command in terminal
# bundle install --without production

group :development, :test do
  gem 'sqlite3'
  gem 'dotenv'
end

group :production do
  gem 'pg'
end
Click to Expand
0

Current Commands

The following commands are built into the experience prototype. See helper files below for the specific code used.

0

Hardcoded Commands

The following commands are hard coded and must be typed exactly as they appear below. These were built into the prototype to simulate the automated features that will be built into the next iteration of the app.

0

Working with Helper Files

I was working with helper files to keep my code organized, but I ran into an issue where my bot would repeat himself. This was a bit of an annoying character trait and I needed to teach Alfred better communication skills.

At the end of each snippet of code listed below, you’ll notice `return true` or `return false`. There was  logic added to the main app.rb file that tells code to run through all 4 helper files before returning an error message and it solved the problem. 

0

Below: app.rb

0
require "sinatra"
require 'sinatra/activerecord'
require 'rake'
require 'active_support/all'
require "active_support/core_ext"
require 'haml'
require 'json'
require 'slack-ruby-client'
require 'httparty'



# ----------------------------------------------------------------------

# Load environment variables using Dotenv. If a .env file exists, it will
# set environment variables from that file (useful for dev environments)
configure :development do
  require 'dotenv'
  Dotenv.load
end


# require any models 
# you add to the folder
# using the following syntax:
# require_relative './models/<model_name>'
#require_relative './models/team'

Dir["./models/*.rb"].each {|file| require file }

Dir["./helpers/*.rb"].each {|file| require file }

# The below strings call all four helper files (see bottom of file for code to 
# avoid duplicate responses).
helpers Sinatra::CommandsCreateContact
helpers Sinatra::CommandsEditContact
helpers Sinatra::CommandsFakeContextio
helpers Sinatra::CommandsHelper


# enable sessions for this project
enable :sessions

# ----------------------------------------------------------------------
#     ROUTES, END POINTS AND ACTIONS
# ----------------------------------------------------------------------

get "/" do
  haml :index
end

get "/privacy" do
  "Privacy Statement"
end

get "/about" do
  "About this app"
end

# ----------------------------------------------------------------------
#     OAUTH
# ----------------------------------------------------------------------

# This will handle the OAuth stuff for adding our app to Slack
# https://99designs.com/tech-blog/blog/2015/08/26/add-to-slack-button/
# check it out here. 

# basically after clicking on the add to slack button 
# it'll return back with a code as a parameter in the query string
# e.g. https://yourdomain.com/oauth?code=92618588033.110206495095.452e860e77&state=

# we need to make a POST request to Slack's API to get a more
# permanent token that we can use to make requests to the API
# https://slack.com/api/oauth.access 
# read also: http://blog.teamtreehouse.com/its-time-to-httparty

get "/oauth" do 
  
  code = params[ :code ]
  
  slack_oauth_request = "https://slack.com/api/oauth.access"
  
  if code 
    response = HTTParty.post slack_oauth_request, body: {client_id: ENV['SLACK_CLIENT_ID'], client_secret: ENV['SLACK_CLIENT_SECRET'], code: code}
    
    puts response.to_s
    
    # We can extract lots of information from this web hook... 
    
    access_token = response["access_token"]
    team_name = response["team_name"]
    team_id = response["team_id"]
    user_id = response["user_id"]
        
    incoming_channel = response['incoming_webhook']['channel']
    incoming_channel_id = response['incoming_webhook']['channel_id']
    incoming_config_url = response['incoming_webhook']['configuration_url']
    incoming_url = response['incoming_webhook']['url']
    
    bot_user_id = response['bot']['bot_user_id']
    bot_access_token = response['bot']['bot_access_token']
    
    # wouldn't it be useful if we could store this? 
    # we can... 
    
    team = Team.find_or_create_by( team_id: team_id, user_id: user_id )
    team.access_token = access_token
    team.team_name = team_name
    team.raw_json = response.to_s
    team.incoming_channel = incoming_channel
    team.incoming_webhook = incoming_url
    team.bot_token = bot_access_token
    team.bot_user_id = bot_user_id
    team.save
    
    # finally respond... 
    "CourseBot Slack App successfully installed!"
    
  else
    401
  end
  
end

# If successful this will give us something like this:
# {"ok"=>true, "access_token"=>"xoxp-92618588033-92603015268-110199165062-deab8ccb6e1d119caaa1b3f2c3e7d690", "scope"=>"identify,bot,commands,incoming-webhook", "user_id"=>"U2QHR0F7W", "team_name"=>"Programming for Online Prototypes", "team_id"=>"T2QJ6HA0Z", "incoming_webhook"=>{"channel"=>"bot-testing", "channel_id"=>"G36QREX9P", "configuration_url"=>"https://onlineprototypes2016.slack.com/services/B385V4V8E", "url"=>"https://hooks.slack.com/services/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"}, "bot"=>{"bot_user_id"=>"U37HMQRS8", "bot_access_token"=>"xoxb-109599841892-oTaxqITzZ8fUSdmMDxl5kraO"}



# ----------------------------------------------------------------------
#     OUTGOING WEBHOOK USED TO SET UP THE STALL SLACK BUTTON
# ----------------------------------------------------------------------

post "/events" do 
  request.body.rewind
  raw_body = request.body.read
  puts "Raw: " + raw_body.to_s
  
  json_request = JSON.parse( raw_body )

  # check for a URL Verification request.
  if json_request['type'] == 'url_verification'
      content_type :json
      return {challenge: json_request['challenge']}.to_json
  end

  if json_request['token'] != ENV['SLACK_VERIFICATION_TOKEN']
      halt 403, 'Incorrect slack token'
  end

  respond_to_slack_event json_request
  
  # always respond with a 200
  # event otherwise it will retry...
  200
  
end


# ----------------------------------------------------------------------
#     INTERACTIVE MESSAGES
# ----------------------------------------------------------------------

post "/message_actions" do 
  
end


# ----------------------------------------------------------------------
#   METHODS
#   Add any custom methods below
# ----------------------------------------------------------------------

private

# for example 
def respond_to_slack_event json
  
  # find the team 
  team_id = json['team_id']
  api_app_id = json['api_app_id']
  event = json['event']
  event_type = event['type']
  event_user = event['user']
  event_text = event['text']
  event_channel = event['channel']
  event_ts = event['ts']
  
  team = Team.find_by( team_id: team_id )
  
  # didn't find a match... this is junk! 
  return if team.nil?
  
  # see if the event user is the bot user 
  # if so we shoud ignore the event
  return if team.bot_user_id == event_user
  
  event = Event.create( team_id: team_id, type_name: event_type, user_id: event_user, text: event_text, channel: event_channel , timestamp: Time.at(event_ts.to_f) )
  event.team = team 
  event.save
  
  client = team.get_client
  

# This is the code that removed duplicate responses.

if not event_to_action client, event 
if not view_and_edit client, event
if not fake_contextio client, event
        create_contact client, event
  # you_always_do_this client, event

end
end
end
end
Click to Expand
0

Below: example helper file 1  (helpers Sinatra::CommandsCreateContact)

0
module Sinatra
  module CommandsCreateContact

    # ------------------------------------------------------------------------
    # =>   MAPS THE CURRENT EVENT TO AN ACTION
    # ------------------------------------------------------------------------

    def create_contact client, event

      puts event
      puts "Formatted Text: #{event.formatted_text}"

      return if event.formatted_text.nil?

      is_admin = is_admin_or_owner client, event

      # Hi Commands
      if ["hi", "hey", "hello"].any? { |a| event.formatted_text.starts_with? a }   
        client.chat_postMessage(channel: event.channel, text: "I'm Alfred, your personal contact management bot. I monitor activity in your email account etanproject.org@gmail.com and keep track of all your important contacts.", as_user: true)  
        client.chat_postMessage(channel: event.channel, text: "What would you like to do first? Type `add`, `view`, or `help` for more options.", as_user: true)
        return true

      # Add New Commands 
      elsif event.formatted_text == "add"
        client.chat_postMessage(channel: event.channel, text: "Who would you like to add? Type `add` and then the First and Last name.", as_user: true)

        return true
        
      elsif event.formatted_text.starts_with? "add"
        contact_name = event.formatted_text.gsub( "add", "" ).strip
        contact = Contact.create(team_id: event.team_id, name: contact_name )
        contact.save

        client.chat_postMessage(channel: event.channel, text: "I've added _#{ contact.name }_ for you. ", as_user: true)
        client.chat_postMessage(channel: event.channel, text: "What is _#{ contact.name }_'s gender? Are the a `male` or a `female`. ", as_user: true)
        client.chat_postMessage(channel: event.channel, text: "You can type `skip` at any point to `skip`.", as_user: true)
        return true

      # Gender Commands  
      elsif event.formatted_text.starts_with? "male"

        contact = Contact.all.last
        contact.gender = "male"
        contact.save!

        client.chat_postMessage(channel: event.channel, text: "So _#{ contact.name }_ is a man. I've updated that. ", as_user: true)
        client.chat_postMessage(channel: event.channel, text: "What is his email? ", as_user: true)

        return true
      
      # References user's gender for proper pronoun usage.
      elsif event.formatted_text.starts_with? "female"

        contact = Contact.all.last
        contact.gender = "female"
        contact.save!

        client.chat_postMessage(channel: event.channel, text: "So _#{ contact.name }_ is a woman. I've updated that. ", as_user: true)
        client.chat_postMessage(channel: event.channel, text: "What is her email? ", as_user: true)

        return true
        
      # Email Commands     
      # Uses the is_email_address validation method defined below.
      elsif is_email_address event.formatted_text

        contact = Contact.all.last
        contact.email = event.formatted_text
        contact.save!

        client.chat_postMessage(channel: event.channel, text: "I've associated the email `#{contact.email}` with _#{ contact.name }_. ", as_user: true)
        
        if contact.gender == "male"
          client.chat_postMessage(channel: event.channel, text: "What's his phone number? Type `phone` followed by the 10 digit number (numbers only, no `-` or `( )`).", as_user: true)
        else
          client.chat_postMessage(channel: event.channel, text: "What's her phone number? Type `phone` followed by the 10 digit number (numbers only, no `-` or `( )`).", as_user: true)
        end
        return true

        # Phone Commands 
        elsif event.formatted_text.starts_with? "phone"
          contact_number = event.formatted_text.gsub( "phone", "" ).strip  
          
          contact = Contact.all.last
          contact.phone = contact_number.to_i
          contact.save!
          client.chat_postMessage(channel: event.channel, text: "I've updated _#{ contact.name }_'s phone number as #{contact.phone}.", as_user: true)
          client.chat_postMessage(channel: event.channel, text: "What would you like to do next? To view your contacts, type `view`. To add another contact, type `add` and then the first and last name.", as_user: true)
          return true
          
        elsif event.formatted_text.starts_with? "skip"
          contact = Contact.all.last
          client.chat_postMessage(channel: event.channel, text: "Let's speed things up a bit.", as_user: true)
          client.chat_postMessage(channel: event.channel, text: "To update #{ contact.name }'s gender, type `male` or `female`", as_user: true)
          client.chat_postMessage(channel: event.channel, text: "To update #{ contact.name }'s email, simple type their email address.", as_user: true)
          client.chat_postMessage(channel: event.channel, text: "To update #{ contact.name }'s phone number, simple type `phone` followed by the 10 digit number (numbers only, no `-` or `( )`).", as_user: true)
          return true
#         # add additional commands here...
      
      else
        client.chat_postMessage(channel: event.channel, text: "Terribly sorry, 'ol chap. I don't understand the youth these days. Type `help` and we'll get this sorted out.", as_user: true)
       return true
      end

    end



    # ------------------------------------------------------------------------
    # =>   GETS USEFUL INFO FROM SLACK
    # ------------------------------------------------------------------------

    # NOTE: I made an attempt to validate the phone number using a variety of gem files and regex codes, but so far I haven't been able to get it to work.
    
    # def is_phone_number int
    #   return int.match( ((\(\d{3}\) ?)|(\d{3}-))?\d{3}-\d{4} )
    # end
  
    #Email validation using regex
    def is_email_address str
      return str.match(/[a-zA-Z0-9._%]@(?:[a-zA-Z0-9]+\.)[a-zA-Z]{2,4}/)
    end


    def get_user_name client, event
      # calls users_info on slack
      info = client.users_info(user: event.user_id )
      info['user']['name']
    end

    def is_admin_or_owner client, event
      # calls users_info on slack
      info = client.users_info(user: event.user_id )
      info['user']['is_admin'] || info['user']['is_owner']
    end

  end

end
Click to Expand
0

Next Steps

Currently, Alfred is only able to manually add, display, and delete contacts from the table migrations, but with the context.io and Google APIs it’s possible to automatically track and store

context.io API

For the automated email integration functionality, I'm looking into using the context.io (https://context.io/) API. Examples of functionality from that API that might be used by Alfred in the future are listed below. See GitHub documentation listed below for more details. https://github.com/contextio/contextio-ruby

0

Future Commands

0
module Sinatra
  module CommandsFakeContextio
  
    # ------------------------------------------------------------------------
    # =>   CODE USED TO SIMULATE AUTOMATED FUNCTIONALITY OF CONTEXT.IO API
    # ------------------------------------------------------------------------
    
    def fake_contextio client, event
      
      puts event
      puts "Formatted Text: #{event.formatted_text}"
      
      return if event.formatted_text.nil?
      
      is_admin = is_admin_or_owner client, event
        
      if event.formatted_text.starts_with? "when"     #simulates a response to "When was the last time I contacted Alfred Pennyworth"
         client.chat_postMessage(channel: event.channel, text: "You last contacted Alfred on December 11. He has yet to respond. Type 'email' to email him now or you can ask me another question.", as_user: true)
       return true

      elsif event.formatted_text.starts_with? "how"   #simulates a response to "How long has it been since I contacted Jeeves"
          client.chat_postMessage(channel: event.channel, text: "It's been 1 day since you contacted Jeeves. His average response rate is every 3 days. Type 'set reminder' and I'll remind you to email him in 2 days.", as_user: true)
        return true
        
      elsif event.formatted_text.starts_with? "set"   #simulates a response to "set reminder"
          client.chat_postMessage(channel: event.channel, text: "Very well. I'll remind you to email Jeeves in 2 days", as_user: true)
          client.chat_postMessage(channel: event.channel, text: "You mentioned once he can be quite ornary, so be sure to use pleasantries.", as_user: true)
          client.chat_postMessage(channel: event.channel, text: "Anything else I can do for you today? If not, just say 'bye' and I'll leave you in peace.", as_user: true)
        return true
         

      else
       return false
      end
      
    end


    # ------------------------------------------------------------------------
    # =>   GETS USEFUL INFO FROM SLACK
    # ------------------------------------------------------------------------
    
    def get_user_name client, event
      # calls users_info on slack
      info = client.users_info(user: event.user_id ) 
      info['user']['name']
    end
    
    def is_admin_or_owner client, event
      # calls users_info on slack
      info = client.users_info(user: event.user_id ) 
      info['user']['is_admin'] || info['user']['is_owner']
    end
  
  end
  
end
Click to Expand
0

Learnings: Start Simple, Go Deep

The biggest learning from this project was the importance of simplicity. My original bot was going to be a research assistant that would monitor Twitter, Google research publications, and news headlines to help my team stay up to date with information related to our area of interest. After thinking through the integrations and use cases, however, I realized nothing I was designing would be better than setting up a Google Alert or following specific accounts on Twitter.

One of my teammates approached me asking for help managing and organizing all our contacts and I realized creating a collaborative CRM built right into our communication tool, Slack, would solve a huge need and could be prototype within the time frame.

I know conceptually that it’s important to build a product that does one thing really well, but I enjoyed the process of appropriately matching problem to technology and then simplifying features to build a working “experience prototype”.

Resources

Bot Workflows

https://daraghbyrne.github.io/onlineprototypes2016/deliverables/project-workflow/

Character Development

https://daraghbyrne.github.io/onlineprototypes2016/in-class/in-class-2/

Email Validation

http://stackoverflow.com/questions/22993545/ruby-email-validation-with-regex

Phone Validation

Regex: http://www.regexlib.com/Search.aspx?k=phone

Gem: https://github.com/travisjeffery/validates_phone_number

https://rubygems.org/gems/validates_phone_number

Thank You’s

I want to recognize our course instructor, Daragh Byrne, for the detailed resources and extra office hours. Resources from week 4 and 6 were particularly useful with regard to product conceptualization.

https://daraghbyrne.github.io/onlineprototypes2016/in-class/in-class-2/

https://daraghbyrne.github.io/onlineprototypes2016/deliverables/project-workflow/

I also want to thank my classmates Sharon for her guidance on proper file setup, Swarna for her help writing code that edits past database entries, and Elijah for his suggestion to add “return true” and “return false” to each line of my helper code - Now Alfred doesn’t keep repeating himself! Such a relief. 

x
Share this Project

Courses

49-714 Programming for Online Prototypes

· 14 members

An introduction to rapidly prototyping web-based products and services. This 7-week experience will teach students the basics of web development for online services. Specifically, well focus on lig...more


About

Create, manage, and email contacts directly from a team’s slack channel.

Created

December 15th, 2016