codeforces-cli-0.1.0: src/Codeforces/Config.hs
--------------------------------------------------------------------------------
module Codeforces.Config
( UserConfig(..)
, AuthQuery(..)
, generateRequestParams
) where
import Codeforces.Types
import qualified Crypto.Hash.SHA512 as SHA512
import Data.Aeson
import qualified Data.ByteString.Base16 as Base16
import Data.ByteString.Char8 ( ByteString )
import qualified Data.ByteString.Char8 as BC
import Data.Text ( Text )
import qualified Data.Text.Encoding as T
import Data.Time.Clock.POSIX
import Network.HTTP.Simple
import Network.HTTP.Types ( renderQuery )
import System.Random
--------------------------------------------------------------------------------
-- | Represents the user's configuration settings.
--
-- The API key must first be generated on
-- <https://codeforces.com/settings/api the Codeforces API page>.
-- An API key consists of two parameters: @key@ and @secret@.
--
-- To use the API key in a request, some parameters need to be generated from
-- this configuration.
--
data UserConfig = UserConfig
{ cfgHandle :: Handle -- ^ Codeforces handle of the user
, cfgKey :: Text -- ^ First part of the API key
, cfgSecret :: Text -- ^ Second part of the API key
}
deriving Show
instance FromJSON UserConfig where
parseJSON = withObject "Config" $ \v ->
UserConfig <$> (v .: "handle") <*> (v .: "key") <*> (v .: "secret")
instance ToJSON UserConfig where
toJSON UserConfig {..} =
object ["handle" .= cfgHandle, "key" .= cfgKey, "secret" .= cfgSecret]
--------------------------------------------------------------------------------
-- | Contains the data needed to make an authorized GET request.
-- See 'generateRequestParams'.
data AuthQuery = AuthQuery
{ aqOriginalQuery :: Query
, aqMethodName :: String
, aqKey :: Text
, aqSecret :: Text
, aqTime :: POSIXTime
, aqRand :: Int
}
-- | 'generateRequestParams' @config path query@ returns a query string with
-- extra query items that are generated to allow for authorized access.
--
-- The parameters are of the form:
-- @
-- ?paramI=valueI&apiKey=<key>&time=<time>&apiSig=<rand><hash>
-- @
--
-- Where
--
-- * @<rand>@ is a random 6-digit integer
-- * @<time>@ is current time since epoch in seconds
-- * @<hash>@ is the SHA-512 hashcode of the UTF-8 encoded string:
--
-- @
-- <rand>/<methodName>?paramI=valueI&apiKey=<key>&time=<time>#<secret>
-- @
generateRequestParams :: UserConfig -> String -> Query -> IO Query
generateRequestParams UserConfig {..} path query = do
(rand :: Int) <- randomRIO (100000, 999999)
time <- getPOSIXTime
let authQuery = AuthQuery { aqOriginalQuery = query
, aqMethodName = path
, aqKey = cfgKey
, aqSecret = cfgSecret
, aqTime = time
, aqRand = rand
}
let apiSig = (BC.pack . show) rand <> generateHash authQuery
pure $ concat
[query, keyAndTimeParams cfgKey time, [("apiSig", Just apiSig)]]
generateHash :: AuthQuery -> ByteString
generateHash AuthQuery {..} = Base16.encode $ SHA512.hash $ BC.concat
[ BC.pack (show aqRand)
, BC.pack aqMethodName
, renderQuery True $ aqOriginalQuery ++ keyAndTimeParams aqKey aqTime
, "#"
, T.encodeUtf8 aqSecret
]
-- | A 'Query' consisting the correctly formatted @apiKey@ and POSIX @time@.
keyAndTimeParams :: Text -> POSIXTime -> Query
keyAndTimeParams key time =
fmap Just
<$> [("apiKey", T.encodeUtf8 key), ("time", posixToByteString time)]
posixToByteString :: POSIXTime -> ByteString
posixToByteString = BC.pack . (show :: Int -> String) . round
--------------------------------------------------------------------------------