{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE MultiWayIf #-} -- | Provide HTTP primitives module Discord.Rest.HTTP ( restLoop , Request(..) , JsonRequest(..) ) where import Prelude hiding (log) import Data.Semigroup ((<>)) import Control.Monad.IO.Class (liftIO) import Control.Concurrent (threadDelay) import Control.Concurrent.MVar import Control.Concurrent.Chan import Data.Ix (inRange) import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime) import qualified Data.ByteString.Char8 as Q import qualified Data.ByteString.Lazy.Char8 as QL import Data.Maybe (fromMaybe) import Text.Read (readMaybe) import qualified Network.HTTP.Req as R import qualified Data.Map.Strict as M import Discord.Types import Discord.Rest.Prelude unpackResp :: Either String QL.ByteString -> String unpackResp r = case r of Right a -> "Resp " <> QL.unpack a Left s -> "BadResp " <> s restLoop :: Auth -> Chan (String, JsonRequest, MVar (Either String QL.ByteString)) -> Chan String -> IO () restLoop auth urls log = loop M.empty where loop ratelocker = do threadDelay (40 * 1000) (route, request, thread) <- readChan urls curtime <- getPOSIXTime case compareRate ratelocker route curtime of Locked -> do writeChan urls (route, request, thread) loop ratelocker Available -> do let action = compileRequest auth request (resp, retry) <- restIOtoIO (tryRequest action log) writeChan log ("rest - response " <> unpackResp resp) case resp of Right "" -> putMVar thread (Right "[]") -- empty should be () Right bs -> putMVar thread (Right bs) Left "Try Again" -> writeChan urls (route, request, thread) Left r -> putMVar thread (Left r) case retry of GlobalWait i -> do writeChan log ("rest - GLOBAL WAIT LIMIT: " <> show ((i - curtime) * 1000)) threadDelay $ round ((i - curtime + 0.1) * 1000) loop ratelocker PathWait i -> loop $ M.insert route i ratelocker NoLimit -> loop ratelocker compareRate :: (Ord k, Ord v) => M.Map k v -> k -> v -> RateLimited compareRate ratelocker route curtime = case M.lookup route ratelocker of Just unlockTime -> if curtime < unlockTime then Locked else Available Nothing -> Available data RateLimited = Available | Locked data Timeout = GlobalWait POSIXTime | PathWait POSIXTime | NoLimit tryRequest :: RestIO R.LbsResponse -> Chan String -> RestIO (Either String QL.ByteString, Timeout) tryRequest action log = do resp <- action next10 <- liftIO (round . (+10) <$> getPOSIXTime) let code = R.responseStatusCode resp status = R.responseStatusMessage resp remain = fromMaybe 1 $ readMaybeBS =<< R.responseHeader resp "X-Ratelimit-Remaining" global = fromMaybe False $ readMaybeBS =<< R.responseHeader resp "X-RateLimit-Global" resetInt = fromMaybe next10 $ readMaybeBS =<< R.responseHeader resp "X-RateLimit-Reset" reset = fromIntegral resetInt if | code == 429 -> do liftIO $ writeChan log ("rest - 429 RATE LIMITED global:" <> show global <> " reset:" <> show reset) pure (Left "Try Again", if global then GlobalWait reset else PathWait reset) | code `elem` [500,502] -> pure (Left "Try Again", NoLimit) | inRange (200,299) code -> pure ( Right (R.responseBody resp) , if remain > 0 then NoLimit else PathWait reset ) | inRange (400,499) code -> pure ( Left (show code <> " - " <> Q.unpack status <> QL.unpack (R.responseBody resp)) , if remain > 0 then NoLimit else PathWait reset ) | otherwise -> let err = "Unexpected code: " ++ show code ++ " - " ++ Q.unpack status in pure (Left err, NoLimit) readMaybeBS :: Read a => Q.ByteString -> Maybe a readMaybeBS = readMaybe . Q.unpack compileRequest :: Auth -> JsonRequest -> RestIO R.LbsResponse compileRequest auth request = action where authopt = authHeader auth action = case request of (Delete url opts) -> R.req R.DELETE url R.NoReqBody R.lbsResponse (authopt <> opts) (Get url opts) -> R.req R.GET url R.NoReqBody R.lbsResponse (authopt <> opts) (Patch url body opts) -> R.req R.PATCH url body R.lbsResponse (authopt <> opts) (Put url body opts) -> R.req R.PUT url body R.lbsResponse (authopt <> opts) (Post url body opts) -> do b <- body R.req R.POST url b R.lbsResponse (authopt <> opts)