content: -- This file is part of the Wire Server implementation.
  --
  -- Copyright (C) 2022 Wire Swiss GmbH <opensource@wire.com>
  --
  -- This program is free software: you can redistribute it and/or modify it under
  -- the terms of the GNU Affero General Public License as published by the Free
  -- Software Foundation, either version 3 of the License, or (at your option) any
  -- later version.
  --
  -- This program is distributed in the hope that it will be useful, but WITHOUT
  -- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  -- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  -- details.
  --
  -- You should have received a copy of the GNU Affero General Public License along
  -- with this program. If not, see <https://www.gnu.org/licenses/>.
  
  module Wire.API.Routes.Public.Cargohold where
  
  import Data.Id
  import Data.Kind
  import Data.Metrics.Servant
  import Data.Qualified
  import Data.SOP
  import Imports
  import Servant
  import Servant.OpenApi.Internal.Orphans ()
  import URI.ByteString
  import Wire.API.Asset
  import Wire.API.Error
  import Wire.API.Error.Cargohold
  import Wire.API.Routes.API
  import Wire.API.Routes.AssetBody
  import Wire.API.Routes.MultiVerb
  import Wire.API.Routes.Named
  import Wire.API.Routes.Public
  import Wire.API.Routes.QualifiedCapture
  import Wire.API.Routes.Version
  
  data PrincipalTag = UserPrincipalTag | BotPrincipalTag | ProviderPrincipalTag
    deriving (Eq, Show)
  
  instance RenderableSymbol UserPrincipalTag where
    renderSymbol = "user"
  
  instance RenderableSymbol BotPrincipalTag where
    renderSymbol = "bot"
  
  instance RenderableSymbol ProviderPrincipalTag where
    renderSymbol = "provider"
  
  type family PrincipalId (tag :: PrincipalTag) = (id :: Type) | id -> tag where
    PrincipalId 'UserPrincipalTag = Local UserId
    PrincipalId 'BotPrincipalTag = BotId
    PrincipalId 'ProviderPrincipalTag = ProviderId
  
  type family ApplyPrincipalPath (tag :: PrincipalTag) api
  
  type instance
    ApplyPrincipalPath 'UserPrincipalTag api =
      ZLocalUser :> Until 'V2 :> "assets" :> "v3" :> api
  
  type instance ApplyPrincipalPath 'BotPrincipalTag api = ZBot :> "bot" :> "assets" :> api
  
  type instance ApplyPrincipalPath 'ProviderPrincipalTag api = ZProvider :> "provider" :> "assets" :> api
  
  type instance
    SpecialiseToVersion v ((tag :: PrincipalTag) :> api) =
      SpecialiseToVersion v (ApplyPrincipalPath tag api)
  
  instance (HasServer (ApplyPrincipalPath tag api) ctx) => HasServer (tag :> api) ctx where
    type ServerT (tag :> api) m = ServerT (ApplyPrincipalPath tag api) m
    route _ = route (Proxy @(ApplyPrincipalPath tag api))
    hoistServerWithContext _ = hoistServerWithContext (Proxy @(ApplyPrincipalPath tag api))
  
  instance (RoutesToPaths (ApplyPrincipalPath tag api)) => RoutesToPaths (tag :> api) where
    getRoutes = getRoutes @(ApplyPrincipalPath tag api)
  
  type AssetLocationHeader r =
    '[DescHeader "Location" "Asset location" (AssetLocation r)]
  
  type AssetRedirect =
    WithHeaders
      (AssetLocationHeader Absolute)
      (AssetLocation Absolute)
      (RespondEmpty 302 "Asset found")
  
  type AssetStreaming =
    RespondStreaming
      200
      "Asset returned directly with content type `application/octet-stream`"
      OctetStream
  
  type GetAsset =
    MultiVerb
      'GET
      '[JSON]
      '[ErrorResponse 'AssetNotFound, AssetRedirect]
      (Maybe (AssetLocation Absolute))
  
  type CargoholdAPI =
    ( Summary "Renew an asset token"
        :> Until 'V2
        :> CanThrow 'AssetNotFound
        :> CanThrow 'Unauthorised
        :> ZLocalUser
        :> "assets"
        :> "v3"
        :> Capture "key" AssetKey
        :> "token"
        :> Post '[JSON] NewAssetToken
    )
      :<|> ( Summary "Delete an asset token"
               :> Until 'V2
               :> Description "**Note**: deleting the token makes the asset public."
               :> ZLocalUser
               :> "assets"
               :> "v3"
               :> Capture "key" AssetKey
               :> "token"
               :> MultiVerb
                    'DELETE
                    '[JSON]
                    '[RespondEmpty 200 "Asset token deleted"]
                    ()
           )
      :<|> BaseAPIv3 'UserPrincipalTag
      :<|> BaseAPIv3 'BotPrincipalTag
      :<|> BaseAPIv3 'ProviderPrincipalTag
      :<|> QualifiedAPI
      :<|> LegacyAPI
      :<|> MainAPI
  
  -- | Asset API for a given principal (user/bot/provider).
  --
  -- This was introduced before API versioning, and the user endpoints contain a
  -- v3 suffix, which is removed starting from API V2.
  type BaseAPIv3 (tag :: PrincipalTag) =
    Named
      '("assets-upload-v3", tag)
      ( Summary "Upload an asset"
          :> Description AuditUploadDescription
          :> CanThrow 'AssetTooLarge
          :> CanThrow 'InvalidLength
          :> CanThrow 'IncompleteBody
          :> tag
          :> AssetBody
          :> MultiVerb
               'POST
               '[JSON]
               '[ WithHeaders
                    (AssetLocationHeader Relative)
                    (Asset, AssetLocation Relative)
                    (Respond 201 "Asset posted" Asset)
                ]
               (Asset, AssetLocation Relative)
      )
      :<|> Named
             '("assets-download-v3", tag)
             ( Summary "Download an asset"
                 :> tag
                 :> Capture "key" AssetKey
                 :> Header "Asset-Token" AssetToken
                 :> QueryParam "asset_token" AssetToken
                 :> ZHostOpt
                 :> GetAsset
             )
      :<|> Named
             '("assets-delete-v3", tag)
             ( Summary "Delete an asset"
                 :> CanThrow 'AssetNotFound
                 :> CanThrow 'Unauthorised
                 :> tag
                 :> Capture "key" AssetKey
                 :> MultiVerb
                      'DELETE
                      '[JSON]
                      '[RespondEmpty 200 "Asset deleted"]
                      ()
             )
  
  -- | Qualified asset API. Only download and delete endpoints are supported, as
  -- upload has stayed unqualified. These endpoints also predate API versioning,
  -- and contain a v4 suffix.
  type QualifiedAPI =
    Named
      "assets-download-v4"
      ( Summary "Download an asset"
          :> Until 'V2
          :> Description
               "**Note**: local assets result in a redirect, \
               \while remote assets are streamed directly."
          :> ZLocalUser
          :> "assets"
          :> "v4"
          :> QualifiedCapture "key" AssetKey
          :> Header "Asset-Token" AssetToken
          :> QueryParam "asset_token" AssetToken
          :> ZHostOpt
          :> MultiVerb
               'GET
               '()
               '[ ErrorResponse 'AssetNotFound,
                  AssetRedirect,
                  AssetStreaming
                ]
               (Maybe LocalOrRemoteAsset)
      )
      :<|> Named
             "assets-delete-v4"
             ( Summary "Delete an asset"
                 :> Until 'V2
                 :> Description "**Note**: only local assets can be deleted."
                 :> CanThrow 'AssetNotFound
                 :> CanThrow 'Unauthorised
                 :> ZLocalUser
                 :> "assets"
                 :> "v4"
                 :> QualifiedCapture "key" AssetKey
                 :> MultiVerb
                      'DELETE
                      '[JSON]
                      '[RespondEmpty 200 "Asset deleted"]
                      ()
             )
  
  -- Old endpoints, predating BaseAPIv3, and therefore API versioning.
  type LegacyAPI =
    Named
      "assets-download-legacy"
      ( ZLocalUser
          :> Until 'V2
          :> "assets"
          :> QueryParam' [Required, Strict] "conv_id" ConvId
          :> Capture "id" AssetId
          :> GetAsset
      )
      :<|> Named
             "assets-conv-download-legacy"
             ( ZLocalUser
                 :> Until 'V2
                 :> "conversations"
                 :> Capture "cnv" ConvId
                 :> "assets"
                 :> Capture "id" AssetId
                 :> GetAsset
             )
      :<|> Named
             "assets-conv-otr-download-legacy"
             ( ZLocalUser
                 :> Until 'V2
                 :> "conversations"
                 :> Capture "cnv" ConvId
                 :> "otr"
                 :> "assets"
                 :> Capture "id" AssetId
                 :> GetAsset
             )
  
  -- | With API versioning, the previous ad-hoc v3/v4 versioning is abandoned, and
  -- asset endpoints are versioned normally as part of the public API, without any
  -- explicit prefix.
  type MainAPI =
    Named
      "tokens-renew"
      ( Summary "Renew an asset token"
          :> From 'V2
          :> CanThrow 'AssetNotFound
          :> CanThrow 'Unauthorised
          :> ZLocalUser
          :> "assets"
          :> Capture "key" AssetKey
          :> "token"
          :> Post '[JSON] NewAssetToken
      )
      :<|> Named
             "tokens-delete"
             ( Summary "Delete an asset token"
                 :> From 'V2
                 :> Description "**Note**: deleting the token makes the asset public."
                 :> ZLocalUser
                 :> "assets"
                 :> Capture "key" AssetKey
                 :> "token"
                 :> MultiVerb
                      'DELETE
                      '[JSON]
                      '[RespondEmpty 200 "Asset token deleted"]
                      ()
             )
      :<|> Named
             "assets-upload"
             ( Summary "Upload an asset"
                 :> Description AuditUploadDescription
                 :> From 'V2
                 :> CanThrow 'AssetTooLarge
                 :> CanThrow 'InvalidLength
                 :> CanThrow 'IncompleteBody
                 :> ZLocalUser
                 :> "assets"
                 :> AssetBody
                 :> MultiVerb
                      'POST
                      '[JSON]
                      '[ WithHeaders
                           (AssetLocationHeader Relative)
                           (Asset, AssetLocation Relative)
                           (Respond 201 "Asset posted" Asset)
                       ]
                      (Asset, AssetLocation Relative)
             )
      :<|> Named
             "assets-download"
             ( Summary "Download an asset"
                 :> From 'V2
                 :> Description
                      "**Note**: local assets result in a redirect, \
                      \while remote assets are streamed directly."
                 :> CanThrow 'NoMatchingAssetEndpoint
                 :> ZLocalUser
                 :> "assets"
                 :> QualifiedCapture "key" AssetKey
                 :> Header "Asset-Token" AssetToken
                 :> QueryParam "asset_token" AssetToken
                 :> ZHostOpt
                 :> MultiVerb
                      'GET
                      '()
                      '[ ErrorResponse 'AssetNotFound,
                         AssetRedirect,
                         AssetStreaming
                       ]
                      (Maybe LocalOrRemoteAsset)
             )
      :<|> Named
             "assets-delete"
             ( Summary "Delete an asset"
                 :> From 'V2
                 :> Description "**Note**: only local assets can be deleted."
                 :> CanThrow 'AssetNotFound
                 :> CanThrow 'Unauthorised
                 :> ZLocalUser
                 :> "assets"
                 :> QualifiedCapture "key" AssetKey
                 :> MultiVerb
                      'DELETE
                      '[JSON]
                      '[RespondEmpty 200 "Asset deleted"]
                      ()
             )
  
  data CargoholdAPITag
  
  instance ServiceAPI CargoholdAPITag v where
    type ServiceAPIRoutes CargoholdAPITag = CargoholdAPI
  
  type AuditUploadDescription =
    "<p>Construct the request as <code>multipart/mixed</code>; set header \
    \<code>Content-Type: multipart/mixed; boundary=&lt;boundary&gt;</code>.</p> \
    \<p>Use exactly two parts in this order:</p> \
    \<ol><li><code>application/json</code> metadata (AssetSettings)</li><li><code>application/octet-stream</code> asset bytes</li></ol> \
    \<p>Each part must include <code>Content-Type</code> and <code>Content-Length</code>; the second part may include <code>Content-MD5</code>. Use CRLF between headers and bodies.</p> \
    \<p>When asset audit logging is enabled, the JSON metadata must include:</p> \
    \<ul><li><code>convId</code>: object <code>{ id: UUID, domain: String }</code> (qualified conversation ID)</li><li><code>filename</code>: String</li><li><code>filetype</code>: String MIME type (e.g. <code>image/png</code>, <code>application/pdf</code>)</li></ul> \
    \<p>Optional metadata: <code>public</code> (Bool, default <code>false</code>), <code>retention</code> (one of <code>eternal</code>, <code>persistent</code>, <code>volatile</code>, <code>eternal-infrequent_access</code>, <code>expiring</code>).</p> \
    \<p>For profile pictures or team icons without a conversation, set <code>convId.id</code> to <code>00000000-0000-0000-0000-000000000000</code> and <code>convId.domain</code> to the tenant’s domain; use any reasonable filename.</p> \
    \<p>Note: the server treats the asset bytes as <code>application/octet-stream</code>; <code>filetype</code> is used for auditing only.</p> \
    \<p>Example body (boundary=frontier):</p> \
    \<pre><code>Content-Type: multipart/mixed; boundary=frontier<br/><br/>--frontier<br/>Content-Type: application/json<br/>Content-Length: 191<br/><br/>{\"public\":false,\"retention\":\"volatile\",\"convId\":{\"id\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"domain\":\"example.com\"},\"filename\":\"report.pdf\",\"filetype\":\"application/pdf\"}<br/>--frontier<br/>Content-Type: application/octet-stream<br/>Content-Length: 11<br/><br/>Hello Audit<br/>--frontier--</code></pre>"
  