packages feed

sauron-0.0.0.0: src/Sauron/Top/Client.hs

{-# LANGUAGE DataKinds     #-}
{-# LANGUAGE TypeOperators #-}

{- |
Copyright: (c) 2022 Dmitrii Kovanikov
SPDX-License-Identifier: MPL-2.0
Maintainer: Dmitrii Kovanikov <kovanikov@gmail.com>

Functions for querying Twitter API.

__Auth:__

@
Authorization: Bearer $TWITTER_TOKEN
@
-}

module Sauron.Top.Client
    ( -- * User
      getUserByUsername
    , getTweets

      -- * Tweets
    , GetTweets

      -- * Internals
    , twitterBaseUrl
    ) where

import Control.Exception (throwIO)
import Servant.API (Capture, Get, Header', JSON, QueryParam, Required, Strict, (:>))
import Servant.Client (BaseUrl (..), ClientM, Scheme (Https), client, mkClientEnv, runClientM)
import Servant.Client.Core (ClientError)

import Sauron.App (App, Env (..))
import Sauron.Top.Json (Data (..), Page (..))
import Sauron.Top.Tweet (Tweet)
import Sauron.Top.User (User, UserId, Username (..))

import qualified Iris


-- | URL for acessing Twitter API v2
twitterBaseUrl :: BaseUrl
twitterBaseUrl = BaseUrl
    { baseUrlScheme = Https
    , baseUrlHost   = "api.twitter.com"
    , baseUrlPort   = 443
    , baseUrlPath   = "/2"
    }

type RequiredHeader = Header' '[Required, Strict]

{- | API type for the following URL:

@
https://api.twitter.com/2/users/by/username/:username
@
-}
type GetUserByUsername
    =  RequiredHeader "Authorization" Text
    :> "users"
    :> "by"
    :> "username"
    :> Capture "username" Username
    :> QueryParam "user.fields" Text
    :> Get '[JSON] (Data User)

getUserByUsernameClient :: Text -> Username -> Maybe Text -> ClientM (Data User)
getUserByUsernameClient = client $ Proxy @GetUserByUsername

getUserByUsername :: Username -> App User
getUserByUsername username = do
    manager <- Iris.asksAppEnv envManager
    let clientEnv = mkClientEnv manager twitterBaseUrl

    token <- Iris.asksAppEnv envToken
    let auth = "Bearer " <> token

    let request = getUserByUsernameClient auth username (Just "public_metrics")

    res <- liftIO $ runClientM request clientEnv
    case res of
      Right (Data userId) -> pure userId
      Left err -> do
          putStrLn $ "Error getting user ID: " ++ show err
          liftIO $ throwIO err

{- | API type for the following URL:

@
https://api.twitter.com/2/users/2164623379/tweets?max_results=5&exclude=retweets&tweet.fields=created_at,public_metrics&end_time=2022-09-01T13%3A52%3A14Z
@
-}
type GetTweets
    =  RequiredHeader "Authorization" Text
    :> "users"
    :> Capture "id" UserId
    :> "tweets"
    :> QueryParam "max_results" Int
    :> QueryParam "exclude" Text
    :> QueryParam "tweet.fields" Text
    :> QueryParam "end_time" String
    :> QueryParam "pagination_token" Text
    :> Get '[JSON] (Page [Tweet])

getTweetsClient
    :: Text
    -> UserId
    -> Maybe Int
    -> Maybe Text
    -> Maybe Text
    -> Maybe String
    -> Maybe Text
    -> ClientM (Page [Tweet])
getTweetsClient = client $ Proxy @GetTweets

getTweetsClientRequest :: Text -> UserId -> String -> Maybe Text -> ClientM (Page [Tweet])
getTweetsClientRequest authToken userId endTime paginationToken = getTweetsClient
    authToken
    userId
    (Just 100)  -- Return 100 requests (max allowed)
    (Just "retweets")  -- exclude retweets & replies
    (Just "created_at,public_metrics")  -- extra fields to return
    (Just endTime)
    paginationToken

getTweets :: UserId -> String -> Maybe Text -> App (Either ClientError (Page [Tweet]))
getTweets userId endTime paginationToken = do
    manager <- Iris.asksAppEnv envManager
    let clientEnv = mkClientEnv manager twitterBaseUrl

    token <- Iris.asksAppEnv envToken
    let auth = "Bearer " <> token

    let request = getTweetsClientRequest
            auth
            userId
            endTime
            paginationToken

    liftIO $ runClientM request clientEnv