Alfred the Botler

Made by Melissa Powel

Found in Prototyping Bots · UNLISTED (SHOWN IN POOLS)

Store, edit, and email important contacts directly via Slack.

0

Alfred the Botler Guide

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

Alfred's Commands

0

Functioning Commands

0

Future API Integrations

In the future, Alfred will be able to get access to any email account (with your permission, of course) and will be able to automatically store contacts and track your engagement with them. He's a cheaky one, so when you say `hi` you'll notice he's already saying he can do these things. Don't believe him!

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 below for more details.

https://github.com/contextio/contextio-ruby

Hardcoded Commands

0

This bot was created out of necessity to improve how my team was managing our interactions with top priority stakeholders, advisors, and mentors. With 4 people in 3 different time zones, we needed a better process. Without budget to hire an admin, I decided to try to create a bot who could organize this information for us.

The Next Steps will be to integrate the bot with context.io, which will automate logging and tracking email interactions as currently the prototype requires manual contact entry.

Below is a simple workflow, which I used to build the prototype.

Alfred's Prototype Workflow

0

Alfred's Current Data Structure

0

Sample Interaction

0

Core Code

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'

# Below are gems I attempted to integrate, but given time limitations 
# I haven't been able to include in the code.
# require 'validates_phone_number'
# require 'contextio'


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

# 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 }

#This is the name of all the helper files used.
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 FOR SLACK BUTTON SETUP
# ----------------------------------------------------------------------

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


# THis will look a lot liks this: 
# {
#   "token": "9GCx7G3WrHix7EJsP818YOVB",
#   "team_id": "T2QJ6HA0Z",
#   "api_app_id": "A36PS6J72",
#   "event": {
#     "type": "message",
#     "user": "U2QHR0F7W",
#     "text": "g ddf;gkl;d fkg;ldfkg df",
#     "ts": "1480296595.000007",
#     "channel": "D37HZB04D",
#     "event_ts": "1480296595.000007"
#   },
#   "type": "event_callback",
#   "authed_users": [
#     "U37HMQRS8"
#   ]
# }


# TO TEST THIS LOCALLY USE THIS ... 
# DON"T INCLUDE IT IN DEVELOPMENT"

# CALL AS FOLLOWS
# curl -X POST http://127.0.0.1:9393/test_event -F token=ipVjny8vUYOauoH7AhH3an6X -F team_id=T2QJ6HA0Z -F event_type=message -F event_user=U2QHR0F7W -F event_channel=D37HZB04D -F event_ts=1480296595.000007 -F event_text='g ddf;gkl;d fkg;ldfkg df' 

post "/test_event" do

  if params['token'] != ENV['SLACK_VERIFICATION_TOKEN']
      halt 403, 'Incorrect slack token'
  end
  
  team = Team.find_by( team_id: params[: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 == params[:event_user]
  
  event = Event.create( team_id: params[:team_id], type_name: params[:event_type], user_id: params[:event_user], text: params[:event_text], channel: params[:event_channel ], timestamp: Time.at(params[:event_ts].to_f) )
  event.team = team 
  event.save
  
  client = team.get_client
  
  content_type :json
  return event_to_action client, event 
  
end


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

post "/message_actions" do 
  
end


# ----------------------------------------------------------------------
#     ERRORS
# ----------------------------------------------------------------------


error 401 do 
  "Invalid response or malformed request"
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 where the `return true` validation is checked.
if not event_to_action client, event 
if not view_and_edit client, event
if not fake_contextio client, event
        create_contact client, event

end
end
end
end
Click to Expand
0

Above: app.rb

NOTE: Each of the sections of code is followed by `return true`. This is checked in the main app.rb file as creating multiple helper files was causing duplicate messages to be displayed.

Below: 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
      
      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

Below: CommandsEditContact

0
module Sinatra
  module CommandsEditContact

    # ------------------------------------------------------------------------
    # =>   SEPARATED CODE USED TO EDIT CONTACTS
    # ------------------------------------------------------------------------

    def view_and_edit 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.include? "help"
        client.chat_postMessage(channel: event.channel, text: "Nobody can help you now.", as_user: true)
        client.chat_postMessage(channel: event.channel, text: "I'm only having a laugh. Try typing `add`, `view`, or `when did I last contact Alfred?`.", as_user: true)
        client.chat_postMessage(channel: event.channel, text: "If you've had enough of me, you can also `dismiss` me.", as_user: true)

       return true


     # View contacts
      elsif event.formatted_text.starts_with? "view"
     # print the list
            contact_list = Contact.all
            contact = ""
            contact_list.each_with_index do |item, index|
            contact += "#{ index+ 1 }. #{ item.name } \n"
            end
            
            client.chat_postMessage(channel: event.channel, text: "Here are all your contacts.\n" + contact  , as_user: true )
            client.chat_postMessage(channel: event.channel, text: "Type `delete contact` followed by a number to delete a contact from this list.\n"  , as_user: true )
            client.chat_postMessage(channel: event.channel, text: "Type `add` plus the First and Last name to add a new contact to the list.\n"  , as_user: true )
       
        return true
        
    # Delete specific item
        elsif event.formatted_text.starts_with? "delete contact"
              contact_id = event.formatted_text.gsub( "delete contact", "" ).strip
              input_index = contact_id.to_i - 1
              all_contacts = Contact.order(:id)
              all_contacts[input_index].destroy
                
              client.chat_postMessage(channel: event.channel, text: "I have deleted #{all_contacts[input_index].name}. Type 'add' to create a new contact", as_user: true )
        return true

     # Delete all
     elsif event.formatted_text == "delete all"
         client.chat_postMessage(channel: event.channel, text: "Are you sure you want to delete all contacts? Type `yes delete all` to delete your entire contact list. Type `nooo` to cancel", as_user: true)

       return true
       
     elsif event.formatted_text == "yes delete all"
       Contact.destroy_all
       #create one step that's a warning and prompts yes
       client.chat_postMessage(channel: event.channel, text: "I have deleted your entire list of contacts.", as_user: true)

       return true

     elsif ["nooo", "no", "noo"].any? { |n| event.formatted_text.starts_with? n }   
       #event.formatted_text == "nooo"
         client.chat_postMessage(channel: event.channel, text: "Phew, that was a close one. Your database was left intact", as_user: true)

       return true


     #
     #  # Edit specific item

     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

Below: CommandsFakeContextio

0
module Sinatra
  module CommandsFakeContextio
  
    # ------------------------------------------------------------------------
    # =>   CODE TO SIMULATE INTEGRATION WITH 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

Below: List of all gems used and attempted

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 attempted
# gem 'validates_phone_number'
# gem 'contextio'

# to avoid installing postgres use the below in CLI 
# bundle install --without production

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

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

Learnings: Start Simple, Go Deep

This project has taught me a lot about designing for chat bots. My initial idea was overly complex. I wanted to create a research bot that could scrape twitter, Google research articles, and news headlines to help my team keep an eye on the latest news related to our project. I realized, though, that this use case was 1) overly complex to rapidly prototype in 3 weeks and 2) would likely not provide a better user experience than simply setting up a Google Alert on certain keywords.

This version of the project solves a very concrete need with a limited scope and was a great way to practice the techniques while creating a tool that provides value to the team, even now as a basic prototype. 

I'm excited to build out this project and integrate with Context.io. 

0

Thank You & Credits

A huge shout out to our professor, Daragh Byrne, for countless hours of extra support and office hours and for his invaluable guides, particularly the resources from week 4 and week 6 of his course:, Online Prototypes, when we covered bot workflow diagrams and designing a personality.

https://daraghbyrne.github.io/onlineprototypes2016//schedule/#week-4-recap-tuesday-nov-15

https://daraghbyrne.github.io/onlineprototypes2016//schedule/#week-6-slack-oauth-and-twitter-tuesday-nov-29 

x
Share this Project

This project is only listed in this pool. Be considerate and think twice before sharing.


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


Focused on
About

Store, edit, and email important contacts directly via Slack.

Created

December 13th, 2016