Posting to Twitter via HTTP in Haskell
When working on rapcandy, I had a bit of trouble getting the twitter connector bit working. I’ll detail here what worked – if you need to connect to Twitter via one of your Haskell applications, this should help you out.
Setting up a Twitter Application
In order to interact with Twitter programmatically, you’ll first need to set up an application via dev.twitter.com. You can create a new app here and generate new API keys for it. Once you do this, you’ll be able to read from twitter. If you want to write to twitter (like I did, with @_rapcandy), you can go to the API Keys tab, click “modify app permissions” and give it write access. You can then generate new API keys which will permit writing.
Copy down your API key, API secret, Access token, and Access token secret into a JSON file called config.json
that looks like this:
{
"apiKey": "<api key>",
"apiSecret": "<api secret>",
"userKey": "<user key>",
"userSecret": "<user secret>"
}
Now everything’s in place to be able to start interacting with Twitter via Haskell.
Posting to Twitter
We’ll be using aeson
, HTTP
, and authenticate-oauth
(Twitter uses OAuth to authenticate its users) to handle the transaction. A bit of boilerplate:
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
import Control.Monad
import Data.Aeson
import GHC.Generics
import Data.ByteString
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy as BL
import qualified Network.HTTP.Base as HTTP
import Network.HTTP.Client
import Network.HTTP.Client.TLS
import Network.HTTP.Types
import Web.Authenticate.OAuth
First thing’s first, we need to be able to pull in our config file in order to access the keys for our application. We “magically” do this using Aeson
, deriving Generic
and making automatic instances of {To, From}JSON
for our user-defined Config
type.
data Config = Config {
apiKey :: String,
apiSecret :: String,
userKey :: String,
userSecret :: String
} deriving (Show, Generic)
instance FromJSON Config
instance ToJSON Config
We can pull in a Config
from a file with a basic function configFromFile
:
configFromFile :: FilePath -> IO (Either String Config)
configFromFile path = do
contents <- BL.readFile path
return $ eitherDecode contents
Now calling configFromFile "config.json"
should return something like Right (Config{...})
. Now we can start authenticating requests to the Twitter API. The following function is adapted from the Yesod source code to be less specific to Yesod:
oauthTwitter :: ByteString -> ByteString -> OAuth
oauthTwitter key secret =
newOAuth { oauthServerName = "twitter"
, oauthRequestUri = "https://api.twitter.com/oauth/request_token"
, oauthAccessTokenUri = "https://api.twitter.com/oauth/access_token"
, oauthAuthorizeUri = "https://api.twitter.com/oauth/authorize"
, oauthSignatureMethod = HMACSHA1
, oauthConsumerKey = key
, oauthConsumerSecret = secret
, oauthVersion = OAuth10a
}
Here we pass in our OAuth consumer key and secret to build an OAuth
; these correspond to the Twitter API key/secret, and is one half of what we need to fully authenticate Twitter requests. The other half is a Credential
, which we can build with newCredential
using our user key and secret. We can fully sign an arbitrary request using a Config
:
signWithConfig :: Config -> Request -> IO Request
signWithConfig Config{..} = signOAuth
(oauthTwitter (B.pack apiKey) (B.pack apiSecret))
(newCredential (B.pack userKey) (B.pack userSecret))
Now all we have to do is actually send a request (post a status!), which is simple but took me a while to finagle into place. There are three things to keep in mind here:
- We must
urlEncode
the status we want to send. - We must
POST
; notGET
. - We must use
tlsManagerSettings
to enable TLS for our request (otherwise, the request won’t go through)
tweet :: Config -> String -> IO (Response BL.ByteString)
tweet config status = do
url <- parseUrl $ "https://api.twitter.com/1.1/statuses/update.json?status=" ++ HTTP.urlEncode status
req <- signWithConfig config url{ method = "POST" }
manager <- newManager tlsManagerSettings
httpLbs req manager
Using this code and a sufficiently permissive Twitter application, you should be able to adapt this code to send requests to any of the Twitter REST API endpoints from Haskell.
Ben