-- 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.Federation.API.Galley
  ( module Wire.API.Federation.API.Galley,
    module Notifications,
  )
where

import Data.Aeson (FromJSON, ToJSON)
import Data.Domain
import Data.Id
import Data.Json.Util
import Data.Misc (Milliseconds)
import Data.OpenApi (OpenApi, ToSchema)
import Data.Proxy (Proxy (Proxy))
import Data.Qualified
import Data.Time.Clock (UTCTime)
import Imports
import Network.Wai.Utilities.JSONResponse
import Servant.API
import Servant.OpenApi (HasOpenApi (toOpenApi))
import Wire.API.Conversation
import Wire.API.Conversation.Action
import Wire.API.Conversation.Protocol
import Wire.API.Conversation.Role (RoleName)
import Wire.API.Conversation.Typing
import Wire.API.Error.Galley
import Wire.API.Federation.API.Common
import Wire.API.Federation.API.Galley.Notifications as Notifications
import Wire.API.Federation.Endpoint
import Wire.API.Federation.Version
import Wire.API.History
import Wire.API.MLS.Keys
import Wire.API.MLS.OutOfSync
import Wire.API.MLS.SubConversation
import Wire.API.Message
import Wire.API.Routes.Named
import Wire.API.Routes.Public.Galley.Messaging
import Wire.API.Routes.SpecialiseToVersion
import Wire.API.Routes.Version qualified as ClientAPI
import Wire.API.Routes.Versioned qualified as ClientAPI
import Wire.API.Util.Aeson (CustomEncoded (..))
import Wire.API.VersionInfo
import Wire.Arbitrary (Arbitrary (..), GenericUniform (..))

-- FUTUREWORK: data types, json instances, more endpoints. See
-- https://wearezeta.atlassian.net/wiki/spaces/CORE/pages/356090113/Federation+Galley+Conversation+API
-- for the current list we need.

-- | For conventions see /docs/developer/federation-api-conventions.md
type GalleyApi =
  -- | Register a new conversation. This is only called on backends of users
  -- that are part of a conversation at creation time. Since MLS conversations
  -- are always created empty (i.e. they only contain the creator), this RPC is
  -- never invoked for such conversations.
  FedEndpoint "on-conversation-created" (ConversationCreated ConvId) EmptyResponse
    -- This endpoint is called the first time a user from this backend is
    -- added to a remote conversation.
    :<|> Named
           "get-conversations@v1"
           ( UnnamedFedEndpointWithMods
               '[Until 'V2]
               "get-conversations"
               GetConversationsRequest
               GetConversationsResponse
           )
    :<|> FedEndpointWithMods
           '[From 'V2]
           "get-conversations"
           GetConversationsRequest
           GetRemoteConversationViewsResponse
    :<|> FedEndpoint
           "leave-conversation"
           LeaveConversationRequest
           LeaveConversationResponse
    -- used by a remote backend to send a message to a conversation owned by
    -- this backend
    :<|> FedEndpoint
           "send-message"
           ProteusMessageSendRequest
           MessageSendResponse
    :<|> FedEndpoint
           "update-conversation"
           ConversationUpdateRequest
           ConversationUpdateResponse
    :<|> FedEndpoint "mls-welcome" MLSWelcomeRequest MLSWelcomeResponse
    :<|> FedEndpoint
           "send-mls-message"
           MLSMessageSendRequest
           MLSMessageResponse
    :<|> FedEndpoint
           "send-mls-commit-bundle"
           MLSMessageSendRequest
           MLSMessageResponse
    :<|> FedEndpoint "query-group-info" GetGroupInfoRequest GetGroupInfoResponse
    :<|> FedEndpointWithMods
           '[
            ]
           "update-typing-indicator"
           TypingDataUpdateRequest
           TypingDataUpdateResponse
    :<|> FedEndpoint "on-typing-indicator-updated" TypingDataUpdated EmptyResponse
    :<|> FedEndpointWithMods
           '[ From 'V1
            ]
           "get-sub-conversation"
           GetSubConversationsRequest
           GetSubConversationsResponse
    :<|> FedEndpointWithMods
           '[ From 'V1
            ]
           "delete-sub-conversation"
           DeleteSubConversationFedRequest
           DeleteSubConversationResponse
    :<|> FedEndpointWithMods
           '[ From 'V1
            ]
           "leave-sub-conversation"
           LeaveSubConversationRequest
           LeaveSubConversationResponse
    :<|> Named
           "get-one2one-conversation@v1"
           ( UnnamedFedEndpointWithMods
               '[From 'V1, Until 'V2]
               "get-one2one-conversation"
               GetOne2OneConversationRequest
               GetOne2OneConversationResponse
           )
    :<|> FedEndpointWithMods
           '[From 'V2]
           "get-one2one-conversation"
           GetOne2OneConversationRequest
           GetOne2OneConversationResponseV2
    -- All the notification endpoints that go through the queue-based
    -- federation client ('fedQueueClient').
    :<|> GalleyNotificationAPI

data TypingDataUpdateRequest = TypingDataUpdateRequest
  { typingStatus :: TypingStatus,
    userId :: UserId,
    convId :: ConvId
  }
  deriving stock (Eq, Show, Generic)
  deriving (FromJSON, ToJSON) via (CustomEncoded TypingDataUpdateRequest)

instance ToSchema TypingDataUpdateRequest

data TypingDataUpdateResponse
  = TypingDataUpdateSuccess TypingDataUpdated
  | TypingDataUpdateError GalleyError
  deriving stock (Eq, Show, Generic)
  deriving (FromJSON, ToJSON) via (CustomEncoded TypingDataUpdateResponse)

instance ToSchema TypingDataUpdateResponse

data TypingDataUpdated = TypingDataUpdated
  { time :: UTCTime,
    origUserId :: Qualified UserId,
    -- | Implicitely qualified by sender's domain
    convId :: ConvId,
    -- | Implicitely qualified by receiver's domain
    usersInConv :: [UserId],
    typingStatus :: TypingStatus
  }
  deriving stock (Eq, Show, Generic)
  deriving (FromJSON, ToJSON) via (CustomEncoded TypingDataUpdated)

instance ToSchema TypingDataUpdated

data GetConversationsRequest = GetConversationsRequest
  { userId :: UserId,
    convIds :: [ConvId]
  }
  deriving stock (Eq, Show, Generic)
  deriving (Arbitrary) via (GenericUniform GetConversationsRequest)
  deriving (ToJSON, FromJSON) via (CustomEncoded GetConversationsRequest)

instance ToSchema GetConversationsRequest

data GetOne2OneConversationRequest = GetOne2OneConversationRequest
  { -- The user on the sender's domain
    goocSenderUser :: UserId,
    -- The user on the receiver's domain
    goocReceiverUser :: UserId
  }
  deriving stock (Eq, Show, Generic)
  deriving (Arbitrary) via (GenericUniform GetOne2OneConversationRequest)
  deriving (ToJSON, FromJSON) via (CustomEncoded GetOne2OneConversationRequest)

instance ToSchema GetOne2OneConversationRequest

data RemoteConvMembers = RemoteConvMembers
  { selfRole :: RoleName,
    others :: [OtherMember]
  }
  deriving stock (Eq, Show, Generic)
  deriving (Arbitrary) via (GenericUniform RemoteConvMembers)
  deriving (FromJSON, ToJSON) via (CustomEncoded RemoteConvMembers)

instance ToSchema RemoteConvMembers

-- | A conversation hosted on a remote backend. This contains the same
-- information as a 'Conversation', with the exception that conversation status
-- fields (muted\/archived\/hidden) are omitted, since they are not known by the
-- remote backend.
data RemoteConversation = RemoteConversation
  { -- | Id of the conversation, implicitly qualified with the domain of the
    -- backend that created this value.
    id :: ConvId,
    metadata :: ConversationMetadata GroupConvType,
    members :: RemoteConvMembers,
    protocol :: ClientAPI.Versioned 'ClientAPI.V5 Protocol
  }
  deriving stock (Eq, Show, Generic)
  deriving (Arbitrary) via (GenericUniform RemoteConversation)
  deriving (FromJSON, ToJSON) via (CustomEncoded RemoteConversation)

instance ToSchema RemoteConversation

-- | A conversation hosted on a remote backend. This contains the same
-- information as a 'Conversation', with the exception that conversation status
-- fields (muted\/archived\/hidden) are omitted, since they are not known by the
-- remote backend.
data RemoteConversationView = RemoteConversationView
  { -- | Id of the conversation, implicitly qualified with the domain of the
    -- backend that created this value.
    id :: ConvId,
    metadata :: ConversationMetadata GroupConvType,
    members :: RemoteConvMembers,
    protocol :: Protocol
  }
  deriving stock (Eq, Show, Generic)
  deriving (Arbitrary) via (GenericUniform RemoteConversationView)
  deriving (FromJSON, ToJSON) via (CustomEncoded RemoteConversationView)

instance ToSchema RemoteConversationView

remoteConversationFromView :: RemoteConversationView -> RemoteConversation
remoteConversationFromView rc =
  RemoteConversation
    { id = rc.id,
      metadata = rc.metadata,
      members = rc.members,
      protocol = ClientAPI.Versioned rc.protocol
    }

remoteConversationToView :: RemoteConversation -> RemoteConversationView
remoteConversationToView rc =
  RemoteConversationView
    { id = rc.id,
      metadata = rc.metadata,
      members = rc.members,
      protocol = rc.protocol.unVersioned
    }

newtype GetConversationsResponse = GetConversationsResponse
  { convs :: [RemoteConversation]
  }
  deriving stock (Eq, Show, Generic)
  deriving (Arbitrary) via (GenericUniform GetConversationsResponse)
  deriving (ToJSON, FromJSON) via (CustomEncoded GetConversationsResponse)

instance ToSchema GetConversationsResponse

newtype GetRemoteConversationViewsResponse = GetRemoteConversationViewsResponse
  { convs :: [RemoteConversationView]
  }
  deriving stock (Eq, Show, Generic)
  deriving (Arbitrary) via (GenericUniform GetRemoteConversationViewsResponse)
  deriving (ToJSON, FromJSON) via (CustomEncoded GetRemoteConversationViewsResponse)

instance ToSchema GetRemoteConversationViewsResponse

getConversationsResponseToView :: GetConversationsResponse -> GetRemoteConversationViewsResponse
getConversationsResponseToView res = GetRemoteConversationViewsResponse (map remoteConversationToView res.convs)

getConversationsResponseFromView :: GetRemoteConversationViewsResponse -> GetConversationsResponse
getConversationsResponseFromView res = GetConversationsResponse (map remoteConversationFromView res.convs)

data GetOne2OneConversationResponse
  = GetOne2OneConversationOk RemoteConversation
  | -- | This is returned when the local backend is asked for a 1-1 conversation
    -- that should reside on the other backend.
    GetOne2OneConversationBackendMismatch
  | -- | This is returned when a 1-1 conversation between two unconnected users
    -- is requested.
    GetOne2OneConversationNotConnected
  deriving stock (Eq, Show, Generic)
  deriving (Arbitrary) via (GenericUniform GetOne2OneConversationResponse)
  deriving (ToJSON, FromJSON) via (CustomEncoded GetOne2OneConversationResponse)

instance ToSchema GetOne2OneConversationResponse

data GetOne2OneConversationResponseV2
  = GetOne2OneConversationV2Ok RemoteMLSOne2OneConversation
  | -- | This is returned when the local backend is asked for a 1-1 conversation
    -- that should reside on the other backend.
    GetOne2OneConversationV2BackendMismatch
  | -- | This is returned when a 1-1 conversation between two unconnected users
    -- is requested.
    GetOne2OneConversationV2NotConnected
  | GetOne2OneConversationV2MLSNotEnabled
  deriving stock (Eq, Show, Generic)
  deriving (ToJSON, FromJSON) via (CustomEncoded GetOne2OneConversationResponseV2)

instance ToSchema GetOne2OneConversationResponseV2

data RemoteMLSOne2OneConversation = RemoteMLSOne2OneConversation
  { conversation :: RemoteConversationView,
    publicKeys :: MLSKeysByPurpose MLSPublicKeys
  }
  deriving stock (Eq, Show, Generic)
  deriving (ToJSON, FromJSON) via (CustomEncoded RemoteMLSOne2OneConversation)

instance ToSchema RemoteMLSOne2OneConversation

-- | A record type describing a new federated conversation
--
-- FUTUREWORK: Think about extracting common conversation metadata into a
-- separarate data type that can be reused in several data types in this module.
data ConversationCreated conv = ConversationCreated
  { -- | The time when the conversation was created
    time :: UTCTime,
    -- | The user that created the conversation. This is implicitly qualified
    -- by the requesting domain, since it is impossible to create a regular/group
    -- conversation on a remote backend.
    origUserId :: UserId,
    -- | The conversation ID, local to the backend invoking the RPC
    cnvId :: conv,
    -- | The conversation type
    cnvType :: ConvType,
    cnvAccess :: [Access],
    cnvAccessRoles :: Set AccessRole,
    -- | The conversation name,
    cnvName :: Maybe Text,
    -- | Members of the conversation apart from the creator
    nonCreatorMembers :: Set OtherMember,
    messageTimer :: Maybe Milliseconds,
    receiptMode :: Maybe ReceiptMode,
    protocol :: Protocol,
    groupConvType :: Maybe GroupConvType,
    channelAddPermission :: Maybe AddPermission,
    history :: Maybe History
  }
  deriving stock (Eq, Show, Generic, Functor)
  deriving (ToJSON, FromJSON) via (CustomEncoded (ConversationCreated conv))

instance (ToSchema a) => ToSchema (ConversationCreated a)

instance (Arbitrary a) => Arbitrary (ConversationCreated a) where
  arbitrary =
    ConversationCreated
      <$> arbitrary
      <*> arbitrary
      <*> arbitrary
      <*> arbitrary
      <*> arbitrary
      <*> arbitrary
      <*> arbitrary
      <*> arbitrary
      <*> arbitrary
      <*> arbitrary
      <*> arbitrary
      <*> arbitrary
      <*> arbitrary
      <*> (canonicaliseHistory <$> arbitrary)
    where
      -- make sure we use a canonical representation of the history field so
      -- that roundtrip tests make sense
      canonicaliseHistory :: History -> Maybe History
      canonicaliseHistory HistoryPrivate = Nothing
      canonicaliseHistory h = Just h

ccRemoteOrigUserId :: ConversationCreated (Remote ConvId) -> Remote UserId
ccRemoteOrigUserId cc = qualifyAs cc.cnvId cc.origUserId

data LeaveConversationRequest = LeaveConversationRequest
  { -- | The conversation is assumed to be owned by the target domain, which
    -- allows us to protect against relay attacks
    convId :: ConvId,
    -- | The leaver is assumed to be owned by the origin domain, which allows us
    -- to protect against spoofing attacks
    leaver :: UserId
  }
  deriving stock (Generic, Eq, Show)
  deriving (ToJSON, FromJSON) via (CustomEncoded LeaveConversationRequest)

instance ToSchema LeaveConversationRequest

-- | Error outcomes of the leave-conversation RPC.
data RemoveFromConversationError
  = RemoveFromConversationErrorRemovalNotAllowed
  | RemoveFromConversationErrorNotFound
  | RemoveFromConversationErrorUnchanged
  deriving stock (Eq, Show, Generic)
  deriving (ToJSON, FromJSON) via (CustomEncoded RemoveFromConversationError)

instance ToSchema RemoveFromConversationError

data RemoteMLSMessageResponse
  = RemoteMLSMessageOk
  | RemoteMLSMessageMLSNotEnabled
  deriving stock (Eq, Show, Generic)
  deriving (ToJSON, FromJSON) via (CustomEncoded RemoteMLSMessageResponse)

instance ToSchema RemoteMLSMessageResponse

data ProteusMessageSendRequest = ProteusMessageSendRequest
  { -- | Conversation is assumed to be owned by the target domain, this allows
    -- us to protect against relay attacks
    convId :: ConvId,
    -- | Sender is assumed to be owned by the origin domain, this allows us to
    -- protect against spoofing attacks
    sender :: UserId,
    rawMessage :: Base64ByteString
  }
  deriving stock (Eq, Show, Generic)
  deriving (Arbitrary) via (GenericUniform ProteusMessageSendRequest)
  deriving (ToJSON, FromJSON) via (CustomEncoded ProteusMessageSendRequest)

instance ToSchema ProteusMessageSendRequest

data MLSMessageSendRequest = MLSMessageSendRequest
  { -- | Conversation (or sub conversation) is assumed to be owned by the target
    -- domain, this allows us to protect against relay attacks
    convOrSubId :: ConvOrSubConvId,
    -- | Sender is assumed to be owned by the origin domain, this allows us to
    -- protect against spoofing attacks
    sender :: UserId,
    senderClient :: ClientId,
    rawMessage :: Base64ByteString,
    -- | Whether out of sync errors should be enabled
    enableOutOfSyncCheck :: Maybe EnableOutOfSyncCheck
  }
  deriving stock (Eq, Show, Generic)
  deriving (Arbitrary) via (GenericUniform MLSMessageSendRequest)
  deriving (ToJSON, FromJSON) via (CustomEncoded MLSMessageSendRequest)

instance ToSchema MLSMessageSendRequest

newtype MessageSendResponse = MessageSendResponse
  {response :: PostOtrResponse MessageSendingStatus}
  deriving stock (Eq, Show, Generic)
  deriving
    (ToJSON, FromJSON)
    via ( Either
            (CustomEncoded (MessageNotSent MessageSendingStatus))
            MessageSendingStatus
        )

instance ToSchema MessageSendResponse

newtype LeaveConversationResponse = LeaveConversationResponse
  {response :: Either RemoveFromConversationError ()}
  deriving stock (Eq, Show, Generic)
  deriving
    (ToJSON, FromJSON)
    via (Either (CustomEncoded RemoveFromConversationError) ())

instance ToSchema LeaveConversationResponse

data ConversationUpdateRequest = ConversationUpdateRequest
  { -- | The user that is attempting to perform the action. This is qualified
    -- implicitly by the origin domain
    user :: UserId,
    -- | Id of conversation the action should be performed on. The is qualified
    -- implicity by the owning backend which receives this request.
    convId :: ConvId,
    action :: SomeConversationAction
  }
  deriving stock (Eq, Show, Generic)
  deriving (Arbitrary) via (GenericUniform ConversationUpdateRequest)
  deriving (FromJSON, ToJSON) via (CustomEncoded ConversationUpdateRequest)

instance ToSchema ConversationUpdateRequest

data ConversationUpdateResponse
  = ConversationUpdateResponseError GalleyError
  | ConversationUpdateResponseUpdate ConversationUpdate
  | ConversationUpdateResponseNoChanges
  | ConversationUpdateResponseNonFederatingBackends NonFederatingBackends
  | ConversationUpdateResponseUnreachableBackends UnreachableBackends
  deriving stock (Eq, Show, Generic)
  deriving
    (ToJSON, FromJSON)
    via (CustomEncoded ConversationUpdateResponse)

instance ToSchema ConversationUpdateResponse

-- | A wrapper around a raw welcome message
data MLSWelcomeRequest = MLSWelcomeRequest
  { -- | Implicitely qualified by origin domain
    originatingUser :: UserId,
    -- | A serialised welcome message.
    welcomeMessage :: Base64ByteString,
    -- | Recipients local to the target backend.
    recipients :: [(UserId, ClientId)],
    -- | The conversation id, qualified to the owning domain
    qualifiedConvId :: Qualified ConvId
  }
  deriving stock (Eq, Generic, Show)
  deriving (Arbitrary) via (GenericUniform MLSWelcomeRequest)
  deriving (FromJSON, ToJSON) via (CustomEncoded MLSWelcomeRequest)

instance ToSchema MLSWelcomeRequest

data MLSWelcomeResponse
  = MLSWelcomeSent
  | MLSWelcomeMLSNotEnabled
  deriving stock (Eq, Generic, Show)
  deriving (FromJSON, ToJSON) via (CustomEncoded MLSWelcomeResponse)

instance ToSchema MLSWelcomeResponse

data MLSMessageResponse
  = MLSMessageResponseError GalleyError
  | MLSMessageResponseProtocolError Text
  | MLSMessageResponseProposalFailure JSONResponse
  | -- | The conversation-owning backend could not reach some of the backends that
    -- have users in the conversation when processing a commit.
    MLSMessageResponseUnreachableBackends (Set Domain)
  | -- | If the list of unreachable users is non-empty, it corresponds to users
    -- that an application message could not be sent to.
    MLSMessageResponseUpdates [ConversationUpdate]
  | MLSMessageResponseNonFederatingBackends NonFederatingBackends
  | MLSMessageResponseGroupInfoDiagnostics GroupInfoDiagnostics
  | MLSMessageResponseOutOfSyncError MLSOutOfSyncError
  deriving stock (Eq, Show, Generic)
  deriving (ToJSON, FromJSON) via (CustomEncoded MLSMessageResponse)

instance ToSchema MLSMessageResponse

data GetGroupInfoRequest = GetGroupInfoRequest
  { -- | Conversation (or subconversation) is assumed to be owned by the target
    -- domain, this allows us to protect against relay attacks
    conv :: ConvOrSubConvId,
    -- | Sender is assumed to be owned by the origin domain, this allows us to
    -- protect against spoofing attacks
    sender :: UserId
  }
  deriving stock (Eq, Show, Generic)
  deriving (Arbitrary) via (GenericUniform GetGroupInfoRequest)
  deriving (ToJSON, FromJSON) via (CustomEncoded GetGroupInfoRequest)

instance ToSchema GetGroupInfoRequest

data GetGroupInfoResponse
  = GetGroupInfoResponseError GalleyError
  | GetGroupInfoResponseState Base64ByteString
  deriving stock (Eq, Show, Generic)
  deriving (ToJSON, FromJSON) via (CustomEncoded GetGroupInfoResponse)

instance ToSchema GetGroupInfoResponse

data GetSubConversationsRequest = GetSubConversationsRequest
  { gsreqUser :: UserId,
    gsreqConv :: ConvId,
    gsreqSubConv :: SubConvId
  }
  deriving stock (Eq, Show, Generic)
  deriving (ToJSON, FromJSON) via (CustomEncoded GetSubConversationsRequest)

instance ToSchema GetSubConversationsRequest

data GetSubConversationsResponse
  = GetSubConversationsResponseError GalleyError
  | GetSubConversationsResponseSuccess PublicSubConversation
  deriving stock (Eq, Show, Generic)
  deriving (ToJSON, FromJSON) via (CustomEncoded GetSubConversationsResponse)

instance ToSchema GetSubConversationsResponse

data LeaveSubConversationRequest = LeaveSubConversationRequest
  { lscrUser :: UserId,
    lscrClient :: ClientId,
    lscrConv :: ConvId,
    lscrSubConv :: SubConvId
  }
  deriving stock (Eq, Show, Generic)
  deriving (Arbitrary) via (GenericUniform LeaveSubConversationRequest)
  deriving (ToJSON, FromJSON) via (CustomEncoded LeaveSubConversationRequest)

instance ToSchema LeaveSubConversationRequest

data LeaveSubConversationResponse
  = LeaveSubConversationResponseError GalleyError
  | LeaveSubConversationResponseProtocolError Text
  | LeaveSubConversationResponseOk
  deriving stock (Eq, Show, Generic)
  deriving (ToJSON, FromJSON) via (CustomEncoded LeaveSubConversationResponse)

instance ToSchema LeaveSubConversationResponse

data DeleteSubConversationFedRequest = DeleteSubConversationFedRequest
  { dscreqUser :: UserId,
    dscreqConv :: ConvId,
    dscreqSubConv :: SubConvId,
    dscreqGroupId :: GroupId,
    dscreqEpoch :: Epoch
  }
  deriving stock (Eq, Show, Generic)
  deriving (ToJSON, FromJSON) via (CustomEncoded DeleteSubConversationFedRequest)

instance ToSchema DeleteSubConversationFedRequest

data DeleteSubConversationResponse
  = DeleteSubConversationResponseError GalleyError
  | DeleteSubConversationResponseSuccess
  deriving stock (Eq, Show, Generic)
  deriving (ToJSON, FromJSON) via (CustomEncoded DeleteSubConversationResponse)

instance ToSchema DeleteSubConversationResponse

swaggerDoc :: OpenApi
swaggerDoc = toOpenApi (Proxy @(SpecialiseToVersion 'V1 GalleyApi))
