packages feed

imm-2.1.2.0: src/wallabag/Main.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE UnicodeSyntax #-}

-- Save page related to input RSS/Atom item, into a Wallabag server.
--
--  Meant to be used as a callback for imm.
--  {{{ Imports

import Data.Aeson (FromJSON (..), ToJSON (..), eitherDecode, object, (.=))
import Data.ByteString.Lazy (getContents)
import Imm.Callback
import Imm.Feed
import Imm.Link
import Imm.Pretty
import Network.HTTP.Req
import Options.Applicative (Parser, auto, execParser, forwardOptions, help, helper, info, long, option, progDesc, short, strOption, switch)
import URI.ByteString.Extended

-- }}}

data CliOptions = CliOptions
  { _hostname ∷ Text
  , _port ∷ Int
  , _clientID ∷ String
  , _clientSecret ∷ String
  , _username ∷ String
  , _password ∷ String
  , _dryRun ∷ Bool
  }
  deriving (Eq, Ord, Read, Show)

parseOptions ∷ MonadIO m ⇒ m CliOptions
parseOptions = io $ execParser $ info (cliOptions <**> helper) $ progDesc description <> forwardOptions
 where
  description = "Save page into a Wallabag server, for each new RSS/Atom item."

cliOptions ∷ Parser CliOptions
cliOptions =
  CliOptions
    <$> strOption (long "host" <> short 'H' <> help "Hostname of the Wallabag server.")
    <*> option auto (long "port" <> short 'P' <> help "Port of the Wallabag server.")
    <*> strOption (long "client-id" <> short 'i' <> help "Client ID used to access the Wallabag API.")
    <*> strOption (long "client-secret" <> short 's' <> help "Client secret used to access the Wallabag API.")
    <*> strOption (long "username" <> short 'u' <> help "Username to log into Wallabag.")
    <*> strOption (long "password" <> short 'p' <> help "Password to log into Wallabag.")
    <*> switch (long "dry-run" <> help "Disable all I/Os, except for logs.")

main ∷ IO ()
main = do
  CliOptions wallabagHost wallabagPort clientID clientSecret username password dryRun ← parseOptions
  input ← getContents <&> eitherDecode

  case input ∷ Either String CallbackMessage of
    Right (CallbackMessage _feedLocation _feedDefinition item) → do
      unless dryRun $ do
        case getMainLink item of
          Just link → do
            runReq defaultHttpConfig $ do
              oAuthToken ← retrieveOAuthToken wallabagHost wallabagPort clientID clientSecret username password
              saveWallabag wallabagHost wallabagPort (access_token oAuthToken) $ _linkURI link
          _ → putStrLn ("No main link in item " <> show (prettyName item)) >> exitFailure
    Left e → putStrLn ("Invalid input: " <> e) >> exitFailure
  return ()

data OAuthTokenResponse = OAuthTokenResponse
  { access_token ∷ Text
  , expires_in ∷ Int
  , refresh_token ∷ Text
  , token_type ∷ Text
  }
  deriving (Generic, Show)

instance ToJSON OAuthTokenResponse

instance FromJSON OAuthTokenResponse

retrieveOAuthToken ∷ MonadHttp m ⇒ Text → Int → String → String → String → String → m OAuthTokenResponse
retrieveOAuthToken wallabagHost wallabagPort clientID clientSecret username password = do
  let payload =
        object
          [ "grant_type" .= ("password" ∷ String)
          , "client_id" .= clientID
          , "client_secret" .= clientSecret
          , "username" .= username
          , "password" .= password
          ]

  responseBody <$> req POST (http wallabagHost /: "oauth" /: "v2" /: "token") (ReqBodyJson payload) jsonResponse (port wallabagPort)

saveWallabag ∷ MonadHttp m ⇒ Text → Int → Text → AnyURI → m ()
saveWallabag wallabagHost wallabagPort accessToken uri = do
  let payload = object ["url" .= (decodeUtf8 $ withAnyURI serializeURIRef' uri ∷ Text)]
      options = port wallabagPort <> header "Authorization" ("Bearer " <> encodeUtf8 accessToken)
  responseBody <$> req POST (http wallabagHost /: "api" /: "entries.json") (ReqBodyJson payload) jsonResponse options