hstratus-drive-0.1.0.0: src-internal/Network/HStratus/Internal/Drive/Endpoints.hs
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_HADDOCK prune #-}
{- |
Module : Network.HStratus.Internal.Drive.Endpoints
Copyright : (c) 2026 Tim Emiola
Maintainer : Tim Emiola <adetokunbo@emio.la>
SPDX-License-Identifier: BSD-3-Clause
Request builders and endpoint configuration for the iCloud Drive API.
-}
module Network.HStratus.Internal.Drive.Endpoints
( DriveEndpoints
, mkDriveEndpoints
, nodeDetailsReq
, nodeDetailsBody
, downloadTokenReq
, createFolderReq
, createFolderBody
, renameNodeReq
, renameNodeBody
, deleteNodeReq
, deleteNodeBody
, uploadTokenReq
, commitUploadReq
)
where
import Data.Aeson (encode, object, (.=))
import qualified Data.ByteString.Lazy as LBS
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)
import Network.HStratus.Http.Common
( icloudBrowserHeaders
, lookupWebservice
, stripTrailingSlash
, withHeaders
)
import Network.HStratus.Internal.Drive.Node (DriveNodeId (..))
import Network.HStratus.Session (AccountData (..), Session (..))
import Network.HTTP.Client
( Request (..)
)
import Network.HTTP.Types (methodGet, methodPost, urlEncode)
-- | Base requests and client ID needed to call the iCloud Drive API.
data DriveEndpoints = DriveEndpoints
{ deServiceReq :: !Request
-- ^ base request targeting the Drive service root (@drivews@)
, deDocReq :: !Request
-- ^ base request targeting the document root (@docws@)
, deClientId :: !Text
-- ^ client ID sent as a query parameter with every request
}
{- | Construct 'DriveEndpoints' from the account data returned after login.
Fails if the @drivews@ or @docws@ service URLs are absent from the account
data.
-}
mkDriveEndpoints :: AccountData -> Session -> IO DriveEndpoints
mkDriveEndpoints ad sess = do
svcReq <- lookupWebservice "drivews" (adWebservices ad)
docReq <- lookupWebservice "docws" (adWebservices ad)
let deServiceReq = withHeaders icloudBrowserHeaders svcReq
deDocReq = withHeaders icloudBrowserHeaders docReq
deClientId = sessionClientId sess
pure DriveEndpoints{deServiceReq, deDocReq, deClientId}
-- | Build the @POST retrieveItemDetailsInFolders@ request.
nodeDetailsReq :: DriveEndpoints -> Request
nodeDetailsReq ep =
withClientId ep $
(deServiceReq ep)
{ path = stripTrailingSlash (path (deServiceReq ep)) <> "/retrieveItemDetailsInFolders"
, method = methodPost
}
-- | Build the JSON request body for @retrieveItemDetailsInFolders@.
nodeDetailsBody :: DriveNodeId -> LBS.ByteString
nodeDetailsBody (DriveNodeId nid) =
encode [object ["drivewsid" .= nid, "partialData" .= False]]
-- | Build the @GET download/by_id@ request for a file in the given zone.
downloadTokenReq :: Text -> Text -> DriveEndpoints -> Request
downloadTokenReq docId zone ep =
(deDocReq ep)
{ path = stripTrailingSlash (path (deDocReq ep)) <> "/ws/" <> urlEncode False (encodeUtf8 zone) <> "/download/by_id"
, method = methodGet
, queryString =
"clientId="
<> urlEncode True (encodeUtf8 (deClientId ep))
<> "&document_id="
<> urlEncode True (encodeUtf8 docId)
}
-- | Build the @POST createFolders@ request.
createFolderReq :: DriveEndpoints -> Request
createFolderReq ep =
withClientId ep $
(deServiceReq ep)
{ path = stripTrailingSlash (path (deServiceReq ep)) <> "/createFolders"
, method = methodPost
}
-- | Build the JSON request body for @createFolders@.
createFolderBody :: DriveEndpoints -> DriveNodeId -> Text -> LBS.ByteString
createFolderBody ep (DriveNodeId parentId) name =
encode $
object
[ "destinationDrivewsId" .= parentId
, "folders" .= [object ["clientId" .= deClientId ep, "name" .= name]]
]
-- | Build the @POST renameItems@ request.
renameNodeReq :: DriveEndpoints -> Request
renameNodeReq ep =
withClientId ep $
(deServiceReq ep)
{ path = stripTrailingSlash (path (deServiceReq ep)) <> "/renameItems"
, method = methodPost
}
-- | Build the JSON request body for @renameItems@.
renameNodeBody :: DriveNodeId -> Text -> Text -> LBS.ByteString
renameNodeBody (DriveNodeId nid) etag name =
encode $
object
["items" .= [object ["drivewsid" .= nid, "etag" .= etag, "name" .= name]]]
-- | Build the @POST moveItemsToTrash@ request.
deleteNodeReq :: DriveEndpoints -> Request
deleteNodeReq ep =
withClientId ep $
(deServiceReq ep)
{ path = stripTrailingSlash (path (deServiceReq ep)) <> "/moveItemsToTrash"
, method = methodPost
}
-- | Build the JSON request body for @moveItemsToTrash@.
deleteNodeBody :: DriveEndpoints -> DriveNodeId -> Text -> LBS.ByteString
deleteNodeBody ep (DriveNodeId nid) etag =
encode $
object
["items" .= [object ["drivewsid" .= nid, "etag" .= etag, "clientId" .= deClientId ep]]]
-- | Build the @POST upload/web@ request for the given zone.
uploadTokenReq :: Text -> DriveEndpoints -> Request
uploadTokenReq zone ep =
withClientId ep $
(deDocReq ep)
{ path =
stripTrailingSlash (path (deDocReq ep))
<> "/ws/"
<> urlEncode False (encodeUtf8 zone)
<> "/upload/web"
, method = methodPost
}
-- | Build the @POST update/documents@ commit request for the given zone.
commitUploadReq :: Text -> DriveEndpoints -> Request
commitUploadReq zone ep =
withClientId ep $
(deDocReq ep)
{ path =
stripTrailingSlash (path (deDocReq ep))
<> "/ws/"
<> urlEncode False (encodeUtf8 zone)
<> "/update/documents"
, method = methodPost
}
withClientId :: DriveEndpoints -> Request -> Request
withClientId ep req =
let cid = "clientId=" <> urlEncode True (encodeUtf8 (deClientId ep))
qs = queryString req
in req{queryString = cid <> (if qs == "" then "" else "&" <> qs)}