packages feed

notion-client-0.8.0.0: src/Notion/V1/Comments.hs

-- | @\/v1\/comments@
module Notion.V1.Comments
  ( -- * Main types
    CommentID,
    CommentObject (..),
    CommentAttachment (..),
    CommentDisplayName (..),
    CommentResponse (..),
    commentResponseId,
    commentResponseObject,

    -- * Requests
    CreateComment (..),
    CommentTarget (..),
    CommentContent (..),
    CommentAttachmentRequest (..),
    CommentDisplayNameRequest (..),
    mkCreateComment,
    mkReplyComment,

    -- * Servant
    API,
  )
where

import Data.Aeson ((.:), (.:?), (.=))
import Data.Aeson qualified as Aeson
import Data.Aeson.KeyMap qualified as KeyMap
import Data.Aeson.Types (Pair)
import Data.Maybe (catMaybes)
import Notion.Prelude
import Notion.V1.Common (BlockID, ExternalFile, File, ObjectType (..), Parent, UUID)
import Notion.V1.ListOf (ListOf)
import Notion.V1.RichText (RichText)
import Notion.V1.Users (UserReference)
import Prelude hiding (id)

-- | Comment ID
type CommentID = UUID

-- | Comment attachment (files attached to comments), as read from responses.
--
-- Read responses (@GET \/v1\/comments@) contain @category@ + @file@; an
-- older @name@ + @type@ + @external@\/@file@ shape is also accepted. To attach
-- files to a new comment use 'CommentAttachmentRequest'.
data CommentAttachment = CommentAttachment
  { name :: Maybe Text,
    type_ :: Maybe Text,
    category :: Maybe Text,
    external :: Maybe ExternalFile,
    file :: Maybe File
  }
  deriving stock (Generic, Show)

instance FromJSON CommentAttachment where
  parseJSON = Aeson.withObject "CommentAttachment" $ \o ->
    CommentAttachment
      <$> o .:? "name"
      <*> o .:? "type"
      <*> o .:? "category"
      <*> o .:? "external"
      <*> o .:? "file"

instance ToJSON CommentAttachment where
  toJSON CommentAttachment {..} =
    Aeson.object $
      catMaybes
        [ ("name" .=) <$> name,
          ("type" .=) <$> type_,
          ("category" .=) <$> category,
          ("external" .=) <$> external,
          ("file" .=) <$> file
        ]

-- | Comment display name, as read from responses.
--
-- Read responses include @resolved_name@ (the rendered author label). To
-- choose the display name of a new comment use 'CommentDisplayNameRequest'.
data CommentDisplayName = CommentDisplayName
  { type_ :: Text,
    emoji :: Maybe Text,
    displayName :: Maybe Text,
    resolvedName :: Maybe Text
  }
  deriving stock (Generic, Show)

instance FromJSON CommentDisplayName where
  parseJSON = Aeson.withObject "CommentDisplayName" $ \o ->
    CommentDisplayName
      <$> o .: "type"
      <*> o .:? "emoji"
      <*> o .:? "display_name"
      <*> o .:? "resolved_name"

instance ToJSON CommentDisplayName where
  toJSON CommentDisplayName {..} =
    Aeson.object $
      ("type" .= type_)
        : catMaybes
          [ ("emoji" .=) <$> emoji,
            ("display_name" .=) <$> displayName,
            ("resolved_name" .=) <$> resolvedName
          ]

-- | Notion comment object
data CommentObject = CommentObject
  { id :: CommentID,
    parent :: Parent,
    discussionId :: UUID,
    createdTime :: POSIXTime,
    lastEditedTime :: POSIXTime,
    createdBy :: UserReference,
    richText :: Vector RichText,
    attachments :: Maybe (Vector CommentAttachment),
    displayName :: Maybe CommentDisplayName,
    object :: ObjectType
  }
  deriving stock (Generic, Show)

instance FromJSON CommentObject where
  parseJSON = \case
    Object o -> do
      id <- o .: "id"
      parent <- o .: "parent"
      discussionId <- o .: "discussion_id"
      createdTimeStr <- o .: "created_time"
      createdTime <- parseISO8601 createdTimeStr
      lastEditedTimeStr <- o .: "last_edited_time"
      lastEditedTime <- parseISO8601 lastEditedTimeStr
      createdBy <- o .: "created_by"
      richText <- o .: "rich_text"
      attachments <- o .:? "attachments"
      displayName <- o .:? "display_name"
      object <- o .: "object"
      return CommentObject {..}
    _ -> fail "Expected object for CommentObject"

-- | A comment endpoint response: Notion may return the full comment or only
-- its id.
data CommentResponse
  = FullComment CommentObject
  | PartialComment CommentID
  deriving stock (Generic, Show)

-- | A response is full when it carries @parent@; a full response with other
-- required fields missing fails to decode rather than becoming partial.
instance FromJSON CommentResponse where
  parseJSON = Aeson.withObject "CommentResponse" $ \o ->
    if KeyMap.member "parent" o
      then FullComment <$> parseJSON (Object o)
      else PartialComment <$> o .: "id"

-- | The id carried by either response shape.
commentResponseId :: CommentResponse -> CommentID
commentResponseId = \case
  FullComment CommentObject {id} -> id
  PartialComment cid -> cid

-- | The full comment, if Notion returned one.
commentResponseObject :: CommentResponse -> Maybe CommentObject
commentResponseObject = \case
  FullComment c -> Just c
  PartialComment _ -> Nothing

-- | Where a new comment goes.
data CommentTarget
  = -- | Start a new discussion on a page ('Notion.V1.Common.PageParent') or
    -- block ('Notion.V1.Common.BlockParent'). Notion rejects other parents.
    CommentOnParent Parent
  | -- | Reply in an existing discussion.
    CommentInDiscussion UUID
  deriving stock (Generic, Show)

-- | The body of a comment: rich text or inline Markdown.
--
-- Also the request body of @PATCH \/v1\/comments\/{comment_id}@.
data CommentContent
  = CommentRichText (Vector RichText)
  | -- | Inline formatting, equations and mentions only; block-level Markdown
    -- does not become blocks.
    CommentMarkdown Text
  deriving stock (Generic, Show)

instance ToJSON CommentContent where
  toJSON c = Aeson.object [commentContentPair c]

commentContentPair :: CommentContent -> Pair
commentContentPair = \case
  CommentRichText rt -> "rich_text" .= rt
  CommentMarkdown md -> "markdown" .= md

-- | Attach a completed file upload to a new comment. Encodes as
-- @{"file_upload_id": "...", "type": "file_upload"}@.
newtype CommentAttachmentRequest = CommentAttachmentRequest {fileUploadId :: UUID}
  deriving stock (Generic, Show)

instance ToJSON CommentAttachmentRequest where
  toJSON CommentAttachmentRequest {fileUploadId} =
    Aeson.object ["file_upload_id" .= fileUploadId, "type" .= ("file_upload" :: Text)]

-- | How the author of a new comment is displayed.
data CommentDisplayNameRequest
  = -- | @{"type":"integration"}@
    DisplayAsIntegration
  | -- | @{"type":"user"}@
    DisplayAsUser
  | -- | @{"type":"custom","custom":{"name":...}}@
    DisplayAsCustom Text
  deriving stock (Generic, Show)

instance ToJSON CommentDisplayNameRequest where
  toJSON = \case
    DisplayAsIntegration -> Aeson.object ["type" .= ("integration" :: Text)]
    DisplayAsUser -> Aeson.object ["type" .= ("user" :: Text)]
    DisplayAsCustom n ->
      Aeson.object
        [ "type" .= ("custom" :: Text),
          "custom" .= Aeson.object ["name" .= n]
        ]

-- | Create comment request
data CreateComment = CreateComment
  { target :: CommentTarget,
    content :: CommentContent,
    -- | At most three attachments.
    attachments :: Maybe (Vector CommentAttachmentRequest),
    displayName :: Maybe CommentDisplayNameRequest
  }
  deriving stock (Generic, Show)

instance ToJSON CreateComment where
  toJSON CreateComment {..} =
    Aeson.object $
      [ case target of
          CommentOnParent p -> "parent" .= p
          CommentInDiscussion d -> "discussion_id" .= d,
        commentContentPair content
      ]
        <> catMaybes
          [ ("attachments" .=) <$> attachments,
            ("display_name" .=) <$> displayName
          ]

-- | Start a new discussion on a page or block.
mkCreateComment :: Parent -> CommentContent -> CreateComment
mkCreateComment p c = CreateComment (CommentOnParent p) c Nothing Nothing

-- | Reply in an existing discussion.
mkReplyComment :: UUID -> CommentContent -> CreateComment
mkReplyComment d c = CreateComment (CommentInDiscussion d) c Nothing Nothing

-- | Servant API
-- Note: To list comments on a page, use the page ID as block_id (pages are blocks in Notion)
type API =
  "comments"
    :> ( ReqBody '[JSON] CreateComment
           :> Post '[JSON] CommentResponse
           :<|> QueryParam "block_id" BlockID
           :> QueryParam "start_cursor" Text
           :> QueryParam "page_size" Natural
           :> Get '[JSON] (ListOf CommentObject)
           :<|> Capture "comment_id" CommentID
           :> Get '[JSON] CommentResponse
           :<|> Capture "comment_id" CommentID
           :> ReqBody '[JSON] CommentContent
           :> Patch '[JSON] CommentResponse
           :<|> Capture "comment_id" CommentID
           :> Delete '[JSON] CommentResponse
       )