Tweeting a Blog Post via command line
Tags: functional, haskell, hakyll
Published: 2020-01-06 09:46:31

You can keep reading here or [jump to my blog](https://odone.io/posts/2020-01-06-posting-a-tweet-with-haskell.html) to get the full experience, including the wonderful pink, blue and white palette.

---

In the [previous post](https://odone.io/posts/2019-12-26-scaffolding-a-blog-post.html) 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`](http://hackage.haskell.org/package/frontmatter) to parse the frontmatter;
- [`tweet-hs`](http://hackage.haskell.org/package/tweet-hs) to post tweets.

```hs
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`:

```hs
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](https://hackage.haskell.org/package/optparse-applicative). Its readme is awesome, so please refer to that to learn more.

```hs
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:

```bash
$ ./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:

```bash
$ ./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:

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Scaffolding a Blog Post ð Using a Haskell script to bootstrap a file from a template<a href="https://twitter.com/hashtag/FunctionalProgramming?src=hash&amp;ref_src=twsrc%5Etfw">#FunctionalProgramming</a> <a href="https://twitter.com/hashtag/Haskell?src=hash&amp;ref_src=twsrc%5Etfw">#Haskell</a> <a href="https://twitter.com/hashtag/Script?src=hash&amp;ref_src=twsrc%5Etfw">#Script</a><a href="https://t.co/lSaeHXw6EU">https://t.co/lSaeHXw6EU</a></p>&mdash; Riccardo Odone (@RiccardoOdone) <a href="https://twitter.com/RiccardoOdone/status/1210205367155773442?ref_src=twsrc%5Etfw">December 26, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

The whole script can be found on [Github](https://github.com/3v0k4/contact-page/blob/7f9f7c70dd9c0a1970d596da80979e44666b9c7c/blog/tweet.hs).

---

Get the latest content via email from me personally. Reply with your thoughts. Let's learn from each other. Subscribe to my [PinkLetter](https://odone.io#newsletter)!