In the previous post we have seen how to scaffold a blog post with a Haskell script. Today, we are going to automate tweeting.
The heart of the script is the tweet
function which uses:
frontmatter
to parse the frontmatter;tweet-hs
to post tweets.
tweet :: String -> FilePath -> IO ()
tweet creds path =
-- ^ Blog post we want to tweet about.
-- ^ Twitter API credentials.
parseYamlFrontmatter <$> Data.ByteString.readFile path
-- ^ Read the blog post.
-- ^ Parse the frontmatter of the blog post.
>>= \case
Done _post frontmatter ->
-- ^ If the frontmatter was parsed successfully..
basicTweet (mkTweet path frontmatter) creds >> pure ()
-- ^ ..then tweet..
e ->
error $ show e
-- ^ ..else stop execution and display the error `e`.
The content of the tweet comes from mkTweet
:
data Front =
Front
{ title :: String
, description :: String
, tags :: [String]
} deriving (Show, Generic, FromJSON)
mkTweet :: FilePath -> Front -> String
mkTweet path Front{..} = fold [title, " 📒 ", description, "\n\n", htags, "\n\n", url]
-- ^ Same as `{ title = title, description = description, tags = tags }`
-- but using two characters 😎
-- ^ Fold strings to a single one.
-- See previous post for a more sophisticated explanation!
where
base = "https://odone.io/posts"
name = System.FilePath.Posix.takeBaseName path
url = fold [base, "/", name, ".html"]
-- ^ Address to the blog post on odone.io.
htags = Data.List.intercalate " " $ fmap ('#':) tags
-- ^ List of hashtags generated by appending '#'.
in front of each tag coming from the frontmatter.
There's still one piece missing. We want to pass as input the credentials for Twitter and the path to the blog post we want to tweet about. This is super eazyly done using optparse-applicative. Its readme is awesome, so please refer to that to learn more.
data Opts =
-- ^ The input we expect from the command line.
Opts
{ creds :: String
, post :: String
}
main :: IO ()
main = do
cs <- fmap (<> "/.cred.toml") getHomeDirectory
-- ^ Default path to the Twitter credentials file. See tweet-hs's docs for more info.
execParser (opts cs) >>= (\Opts{..} -> tweet creds post)
-- ^ Parse the command line input and..
-- ..if successful then call `tweet`..
-- ..else show an error message and a summary on how to use the script correctly.
where
opts cs = info (parser cs <**> helper)
( fullDesc
<> progDesc "Shares on Twitter a new blog POST using CREDS for authentication"
)
parser :: FilePath -> Options.Applicative.Parser Opts
parser creds = Opts
<$> strOption
( long "creds"
<> metavar "CREDS"
<> help "Path to creds .toml file"
<> value creds
-- ^ `--creds` is optional and will use `~/.cred.toml` if not passed as input.
<> showDefault
)
<*> argument str
-- ^ `post` is mandatory and the only argument to the script.
( metavar "POST"
<> help "Path to blog post file"
)
With that in place, calling the script without the mandatory argument gets us:
$ ./tweet.hs
#
# Missing: POST
#
# Usage: tweet.hs [--creds CREDS] POST
# Shares on Twitter a new blog POST using CREDS for authentication
We can also call it with --help
to get a detailed explanation:
$ ./tweet.hs --help
#
# Usage: tweet.hs [--creds CREDS] POST
# Shares on Twitter a new blog POST using CREDS for authentication
#
# Available options:
# --creds CREDS Path to creds .toml
# file (default: "/Users/rysiek/.cred.toml")
# POST Path to blog post file
# -h,--help Show this help text
Instead, a proper call (e.g. ./tweet.hs posts/2019-12-26-scaffolding-a-blog-post.md
) would tweet successfully:
Scaffolding a Blog Post 📒 Using a Haskell script to bootstrap a file from a template#FunctionalProgramming #Haskell #Scripthttps://t.co/lSaeHXw6EU
— Riccardo Odone (@RiccardoOdone) December 26, 2019
The whole script can be found on Github.