Build a Reddit Slack Bot in Haskell

August 10, 2017
Written by

slackreddithaskell

Do you like Reddit? Do you like bots? If you answered yes to one, both, or neither of those, then you are in luck. This post will go over how to build a Reddit Slack bot in Haskell.

This is the second Haskell post here on the Twilio blog. So if you haven’t read the first one on setting up your Haskell developer environment, check that out here.

What are Slash Commands?

According to the Slack API site, messages beginning with “/” are treated differently from other messages: they “enable Slack users to interact with your app directly from Slack” and all send their messages “…to the configured external URL via HTTP POST.

In this post, we’ll build a custom slash command that shares a trending programming post from Reddit about whatever you pass it as a command. For example, if you typed /redditbot python, it would return an article about Python.

What We’ll be Using

Cloning the GitHub repo

Get started by cloning the repo here by entering the following in your terminal:

git clone git@github.com:elizabethsiegle/reddit-slack-bot.git
cd reddit-slack-bot

This keeps things simple because otherwise, so many package dependencies would clash and turn into cabal hell.

Next, fire up another terminal, cd into the directory you cloned the repo into, and let’s open up a Ngrok tunnel.

ngrok http 3000

The core of stack.yaml, our auto-generated stack file, should look like this:

stack.yaml

We need that location part to pull from the move-to-stack fork of Twilio-Haskell which works with the dependencies of all the other packages.

Get Started with Slack Apps

First, let’s go to https://api.slack.com. Then, click Your Apps in the top right-hand corner. You should see a page like the one below, and then click Create New App.

Slack Your Apps

Pick a name for your bot and pick one of your Slack channels to deploy the bot to. Many groups now have a channel dedicated to testing bots. That’s one channel you could use. You should see something like this image below, which asks for your app name and a team you’re in (like a hackathon you were in, a class you took, a team you were on, or your company):

Create-an-App-Page

Click Create App, and then under features on the left-hand side, click on Webhooks. Make sure Activate Incoming Webhooks is on, and then scroll down and click Add New Webhook to Team. That page should look something like this:

Activate Incoming Webhooks Page

You should then see a page asking to confirm your identity. Pick the channel you wish to post to, click Authorize, and you should then see this page:

webhook url

Copy this webhook URL and paste it into a new file in our project called hook.

Sweet! Now, again on the left column of your app’s page, under Features go to Slash Commands. Click Create New Command, and you should see this following page:

Create New Command page

Fill in the text boxes above however you wish. The slash command /redditbot will be followed by a word that is searched for on Reddit. The bot will search Reddit and return a trending post about the word you searched for. Let’s check if the slash command registered. Save your project, open Slack, go to the team and channel you used above, and try your slash command:

slash-command-used-in-Slack

If you don’t see the command auto-complete, then go back to Create New Command, check each step, and try again.

Haskell Code: Building the Slack Bot

Now it’s time to examine some code. You will see a lot of packages and libraries imported into our project at the top of Main.hs. Let’s go over the functions our project already has.

First, we have readSlackFile which takes in our hook file and formats it into a type we want–in this case, one of IO Text. We need to read this file to send our message on Slack. Then configIO actually calls the function.

readSlackFile :: FilePath -> IO Text
readSlackFile filename =
  T.filter (/= '\n') . T.pack <$> Prelude.readFile filename
configIO :: IO Config
configIO =
  Config <$> (readSlackFile "hook")

Then we have parseText and liftMaybe check that there is a word to search following the slash command. Whatever parseText returns is passed to liftMaybe to return the topic we want to search Reddit for. Our printPost function formats the link to display in the message.

parseText :: Text -> Maybe Text
parseText text = case T.strip text of
  "" -> Nothing
  x -> Just x

liftMaybe :: Maybe a -> IO a
liftMaybe = maybe mzero return

printPost :: Post -> T.Text
printPost post = do
  title post <> "\n" <> (T.pack . show . created $ post) <> "\n" <> "http://reddit.com"<> permalink post <> "\n" <> "Score: " <> (T.pack . show . score $ post)

Next, we have findQPosts which takes in the query we return from liftMaybe and passes that to runRedditAnon from the Reddit API, which gets all the posts from the front page of Reddit. FindQPosts searches those for trending posts on a certain topic (in this case, programming).

findQPosts:: Text -> RedditT IO PostListing
findQPosts c = search (Just $ R "programming") (Reddit.Options Nothing (Just 0)) Hot c

Now we need to get Reddit posts and create the message that will be posted to Slack. We do this by calling all the functions we just made above. We also use some of Linklater’s functions to check the command and its passed text, search Reddit for the passed text, and format the information as a message for the Slack channel you configured. The returned Slack message @’s your Slack username in the beginning, too.

messageOfCommandReddit :: Command -> IO Network.Linklater.Message
messageOfCommandReddit (Command "reddit" user channel (Just text)) = do
query <- liftMaybe (parseText text)
posts <- runRedditAnon (findQPosts query)
  case posts of
   Right posts' ->
       return (messageOf [FormatAt user, FormatString (T.intercalate "\n\n". Prelude.map printPost $ contents posts')]) where
      messageOf =
         FormattedMessage(EmojiIcon "gift") "redditbot" channel

This is actually called in messageOfCommandReddit which checks if there’s no parameter passed. In that case, we post “Unrecognized Slack request!”

redditify :: Maybe Command -> IO Text
redditify Nothing =
 return "Unrecognized Slack request!"

redditify(Just command) = do
 Prelude.putStrLn ("+ Incoming command: " <> show command)
 message <- (messageOfCommandReddit) command
 config <- configIO
 case (debug, message) of
   (False,  m) -> do
     _ <- say m config
     return ""
   _ ->
     return ""
 where
   debug = False

Lastly, our main function uses Linklater’s slashSimple method to run on that same port you also call with ngrok. It prints a message out upon successfully running.

main :: IO ()
main =  do
Prelude.putStrLn ("+ Listening on port " <> show port)
run port (slashSimple redditify)
where
port = 3000

To run, go back to your current directory in the terminal, type stack build. This could take some time. So let’s check out Reddit in the meantime.

Sebastian-impatient-gif

Once that’s complete, type stack exec redditbot. In the Slack channel that you configured to handle apps, type in /redditbot ____ where you fill in the underscore with a programming-related topic, like Python, Swift, or Haskell.

 

If you get a 404_client_error, then check your ngrok connection and tunnel. It should be open to 3000. If you get a 500_service_error, then check that the command that is being expected in messageOfCommandReddit is the same command that is expected in your Slack app on the Slack website (in this post, it’s “/redditbot”).

If you see a pop-up asking if you want your application to accept incoming network connections, click “allow”.

If your app is successful, you should see something like this:

Conclusion

Yes, building Slack bots in Haskell is really that easy! Some Haskell APIs that you could use for another bot (or just in general) include:

Questions? Comments? Find me online: