Tweeting a Blog Post via command line

Posted on January 6, 2020 by Riccardo.

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:

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:

The whole script can be found on Github.


Support my work by tweeting this article! 🙏