Store, edit, and email important contacts directly via Slack.
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.
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.
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.
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!
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
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.
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
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
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
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
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
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.
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
This project is only listed in this pool. Be considerate and think twice before sharing.
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
Store, edit, and email important contacts directly via Slack.
December 13th, 2016