Last active
October 4, 2020 03:46
-
-
Save jtt9340/caf11dd57b8d95f9a846b6add0d7449c to your computer and use it in GitHub Desktop.
A basic example of using Rust to make a Slack bot that tells a (bad) knock-knock joke
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//! Together we will build a Slack bot that makes a simple exchange between the user and the | |
//! bot. The user will first mention the bot, and then the bot will tell a knock-knock joke. | |
//! | |
//! Here is how the exchange will look. | |
//! 🙋♂️ *User:* `@bot` Tell me a joke | |
//! 🤖 *Bot:* Knock, knock. | |
//! 🙋♂️ *User:* Who's there? | |
//! 🤖 *Bot:* Underwear | |
//! 🙋♂️ *User:* Underwear who? | |
//! 🤖 *Bot:* Ever underwear you're going? | |
// This could also be written | |
// use slack; | |
// use slack::Event; | |
// use slack::Message; | |
// use slack::RtmClient; | |
// But that's more lines of code. | |
use slack::{self, Event, Message, RtmClient}; | |
// std::env => used to get command line arguments | |
// std::process => used to exit with a non-0 status code | |
use std::{env, process}; | |
struct MyHandler {} | |
fn determine_response(message: &slack::api::MessageStandard) -> (&str, Option<&String>) { | |
let text = message | |
.text | |
.as_ref() | |
.unwrap_or(&String::default()) // Get an empty string if the message did not exist | |
.to_lowercase(); | |
println!("Message: {}", text); // Rust uses Python-style format strings (with the curly braces {}, instead of C-style %s and all that) | |
let channel = message.channel.as_ref(); | |
let response_text = if text.contains("who's there?") || text.contains("who’s there?") { | |
"Underwear" | |
} else if text.contains("underwear who?") { | |
"Ever underwear you're going? 🤡" | |
} else { | |
"" | |
}; | |
// In Rust, if a line doesn't end with a semicolon, the value is returned. This is the same as | |
// return (response_text, channel); <-- notice the semicolon | |
(response_text, channel) | |
} | |
impl slack::EventHandler for MyHandler { | |
fn on_event(&mut self, cli: &RtmClient, event: Event) { | |
println!("on_event"); | |
match event { | |
// When the user first @'s the bot | |
Event::DesktopNotification { | |
ref content, | |
ref channel, | |
.. // There are other fields in Event::DesktopNotification but we ignore them with this dot-dot | |
} if content | |
.as_ref() | |
.filter(|message| message.to_lowercase().contains("tell me a joke")) | |
.is_some() | |
&& channel.is_some() => | |
{ | |
let _ = cli | |
.sender() | |
.send_message(channel.as_ref().unwrap(), "Knock, knock."); | |
} | |
// Responses after the initial "knock, knock" | |
Event::Message(message) => { | |
let (response_text, channel) = match *message { | |
Message::Standard(ref standard_message) => { | |
/*let text = standard_message | |
.text | |
.as_ref() | |
.unwrap_or_default() | |
.to_lowercase(); | |
println!("Message: {}", text); | |
let channel = standard_message.channel.as_ref(); | |
let response_text = if text.contains("knock, knock") { | |
"" | |
} else if text.contains("who's there?") || text.contains("who’s there?") { | |
"Underwear" | |
} else if text.contains("underwear who?") { | |
"Ever underwear you're going? 🤡" | |
} else { | |
"I don't know how to respond." | |
}; | |
(response_text, channel)*/ | |
determine_response(standard_message) // Notice no semicolon here! Remember this means "output this value" | |
} | |
_ => ("", None), | |
}; | |
if channel.is_some() && !response_text.is_empty() { | |
let _ = cli | |
.sender() | |
.send_message(channel.as_ref().unwrap(), response_text); | |
} | |
} | |
// Any other event | |
_ => println!("{:?}", event), | |
}; | |
} | |
fn on_close(&mut self, _cli: &RtmClient) { | |
println!("on_close"); | |
} | |
fn on_connect(&mut self, cli: &RtmClient) { | |
println!("on_connect"); | |
// The idea is that we want to get the list of channels in a given workspace, | |
// then pick one to post messages into. | |
// let all_channels = cli.start_response().channels; | |
// Uh, oh! We already get our first error: "cannot move (out of a shared reference)" | |
// This is because the cli.start_response() method call gave us &StartResponse, which | |
// is a *reference* (you can think of this as a pointer that is guaranteed not to be | |
// null) to a StartResponse struct (like an object). We then try to access the "channels" | |
// field, which gives us a Vec<Channel> (a Vec is like an ArrayList in Rust). | |
// | |
// By default, Rust has "move semantics." This means we transfer the responsibility | |
// to deallocate memory from one "thing" (function or struct) to another. So in this | |
// case we are trying to transfer ownership of the "channels" vec from the StartResponse | |
// returned by cli.start_response() to this on_connect function. In other words, StartResponse | |
// had prior plans of deallocating "channels" when it itself was deallocated, but we want to | |
// say, "no, *I* will deallocate channels." | |
// | |
// Except this won't work, because in order to do that we'd also have to take ownership of | |
// StartResponse, but we can't because the start_response() method gives us a *reference* to | |
// StartResponse. | |
// | |
// So what do we do? We have two options. If a Vec were copyable, then Rust would automatically | |
// make a copy of "channels" that we would own. But Vec is not copyable, so we cannot do that. | |
// | |
// The other option is to take a reference to "channels." We do this with the as_ref() method. | |
// let all_channels = cli.start_response().channels.as_ref(); | |
// | |
// let botspam_id = match all_channels { | |
// Some(channels) => { | |
// let mut the_id = None; | |
// for channel in channels { | |
// // channel.name is a String, which, like Vec, does not implement | |
// // copy so our only choice is to borrow it (since we cannot move it | |
// // since we cannot take ownership of channel) | |
// // | |
// // This time we use a "ref name" syntax to show that the value | |
// // we want to pattern match against should be a reference | |
// if let Some(ref name) = channel.name { | |
// if name == "botspam" { | |
// the_id = channel.id.as_ref(); | |
// break; | |
// } | |
// } | |
// } | |
// the_id | |
// } | |
// None => panic!("This Slack workspace does not contain any channels!"), | |
// }; | |
// | |
// if let Some(id) = botspam_id { | |
// let _ = cli.sender().send_message(id, "Hello, world!"); | |
// } | |
// Here is a more "functional" (and thus cooler) way to do the above | |
let channel_id = cli | |
/* | |
Remember, cli.start_response().channels.as_ref() is an Option<&String>. There is | |
an "and_then" method on the Option type that allows you to pretend, so to speak, that | |
the value exists. However, if it doesn't it just won't run the code you give it. | |
So the sequence of "and_then" calls here allow to me proceed as if every value that | |
might not exist does exist. And if it doesn't, the sequence of method calls stops. | |
If you are familiar with UNIX shells, "and_then" is very familiar to the && operator. | |
Just like you might have | |
grep cats my_favorite_things.txt && echo A cat is one of my favorite things. | |
in a UNIX shell, where the echo command is only run if the grep command succeeds, | |
"and_then" only runs the code it is called with if it is called on a value that does | |
exist. | |
*/ | |
.start_response() | |
.channels | |
.as_ref() | |
.and_then(|channels| { | |
// We turn channels into an iterator, and then call the find method, which returns | |
// the first element for which the given code segment returns true | |
channels.iter().find(|channel| match channel.name { | |
None => false, | |
Some(ref name) => name == "botspam", | |
}) | |
}) | |
.and_then(|channel| channel.id.as_ref()) | |
.expect("botspam channel not found") // If at the end something we expected to exist that didn't, just crash. | |
; | |
let _ = cli | |
.sender() | |
.send_message(&channel_id, "Hello world!") | |
; | |
} | |
} | |
fn main() { | |
// env::args(), interestingly enough, is an iterator over the command line arguments. We can turn | |
// an iterator into a data structure with the "collect" method. However, we have to specify what | |
// kind of data structure we want to turn the iterator into. In this case I use a Vec(tor). | |
let args = env::args().collect::<Vec<String>>(); | |
// Alternatively | |
// let args: Vec<String> = env::args().collect(); | |
let api_key = match args.len() { | |
0 | 1 => { | |
eprintln!("No API key in args! 🙁 Usage: cargo run -- <API KEY>"); | |
process::exit(1); | |
} | |
x => args[x - 1].clone(), | |
}; | |
let mut handler = MyHandler {}; | |
match RtmClient::login_and_run(&api_key, &mut handler) { | |
Ok(_) => (), | |
Err(err) => panic!("Error: {}", err), | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment