todoist-sdk-0.1.2.1: src/Web/Todoist/Domain/Comment.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE RecordWildCards #-}
{- |
Module : Web.Todoist.Domain.Comment
Description : Comment API types and operations for Todoist REST API
Copyright : (c) 2025 Sam Saud Almahri
License : MIT
Maintainer : sam.salmahri@gmail.com
This module provides types and operations for working with Todoist comments. Comments can be
attached to either projects or tasks, and support attachments, user notifications, and pagination.
= Usage Example
@
import Web.Todoist.Domain.Comment
import Web.Todoist.Builder
import Web.Todoist.Runner (todoist, newTodoistConfig)
main :: IO ()
main = do
let config = newTodoistConfig "your-api-token"
-- Create a comment on a project
let projectComment = runBuilder (newCommentBuilder "Great project!")
(withProjectId "project-id-123")
result <- todoist config (addComment projectComment)
-- Create a comment on a task
let taskComment = runBuilder (newCommentBuilder "Don't forget this!")
(withTaskId "task-id-456")
result <- todoist config (addComment taskComment)
-- Get all comments for a project with builder pattern
let params = runBuilder commentParamBuilder (withProjectId "project-id-123" <> withLimit 50)
comments <- todoist config (getComments params)
-- Update a comment
let update = runBuilder (updateCommentBuilder "Updated text!") mempty
updatedComment <- todoist config (updateComment update commentId)
@
For more details on the Todoist Comments API, see:
<https://developer.todoist.com/rest/v2/#comments>
-}
module Web.Todoist.Domain.Comment
( -- * Types
CommentId (..)
, Content (..)
, Comment (..)
, CommentCreate
, CommentUpdate
, CommentParam (..)
-- * Constructors
, newCommentBuilder
, updateCommentBuilder
, commentParamBuilder
-- * Type Class
, TodoistCommentM (..)
-- * Lenses
, commentId
, commentContent
, commentPosterId
, commentPostedAt
, commentTaskId
, commentProjectId
, commentAttachment
) where
import Web.Todoist.Internal.Types (FileAttachment, Params)
import Web.Todoist.Util.Builder
( HasAttachment (..)
, HasContent (..)
, HasCursor (..)
, HasLimit (..)
, HasProjectId (..)
, HasPublicKey (..)
, HasTaskId (..)
, HasUidsToNotify (..)
, Initial
, seed
)
import Web.Todoist.Util.QueryParam (QueryParam (..))
import Control.Applicative ((<$>))
import Control.Monad (Monad)
import Data.Aeson
( FromJSON (..)
, ToJSON (..)
, Value
, genericToJSON
, object
)
import qualified Data.Aeson as Aeson
import Data.Aeson.Types (Parser)
import Data.Bool (Bool (False, True), not)
import Data.Eq (Eq)
import Data.Foldable (null)
import Data.Function (($), (.))
import Data.Int (Int)
import Data.List (filter)
import qualified Data.List as L
import Data.Maybe (Maybe (Just, Nothing), maybe)
import Data.Monoid ((<>))
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Tuple as L
import GHC.Generics (Generic)
import Lens.Micro (to)
import Text.Show (Show (..))
import Web.Todoist.Domain.Types (Attachment, Content (..), ProjectId (..), TaskId (..), Uid)
import Web.Todoist.Lens (Getter)
-- | Newtype wrapper for Comment ID
newtype CommentId = CommentId {getCommentId :: Text}
deriving (Show, Eq, Generic)
instance ToJSON CommentId where
toJSON :: CommentId -> Value
toJSON (CommentId txt) = toJSON txt
instance FromJSON CommentId where
parseJSON :: Value -> Parser CommentId
parseJSON v = CommentId <$> parseJSON v
-- | Comment domain type
data Comment = Comment
{ _id :: CommentId
, _content :: Content
, _poster_id :: Maybe Uid
, _posted_at :: Maybe Uid
, _task_id :: Maybe TaskId
, _project_id :: Maybe ProjectId
, _attachment :: Maybe FileAttachment
}
deriving (Show, Eq, Generic)
-- | Lenses for Comment
commentId :: Getter Comment CommentId
commentId = to (\(Comment {_id = x}) -> x)
{-# INLINE commentId #-}
commentContent :: Getter Comment Content
commentContent = to (\(Comment {_content = x}) -> x)
{-# INLINE commentContent #-}
commentPosterId :: Getter Comment (Maybe Uid)
commentPosterId = to (\(Comment {_poster_id = x}) -> x)
{-# INLINE commentPosterId #-}
commentPostedAt :: Getter Comment (Maybe Uid)
commentPostedAt = to (\(Comment {_posted_at = x}) -> x)
{-# INLINE commentPostedAt #-}
commentTaskId :: Getter Comment (Maybe TaskId)
commentTaskId = to (\(Comment {_task_id = x}) -> x)
{-# INLINE commentTaskId #-}
commentProjectId :: Getter Comment (Maybe ProjectId)
commentProjectId = to (\(Comment {_project_id = x}) -> x)
{-# INLINE commentProjectId #-}
commentAttachment :: Getter Comment (Maybe FileAttachment)
commentAttachment = to (\(Comment {_attachment = x}) -> x)
{-# INLINE commentAttachment #-}
-- | Request body for creating a comment
data CommentCreate = CommentCreate
{ _content :: Content
, _project_id :: Maybe ProjectId
, _task_id :: Maybe TaskId
, _attachment :: Maybe Attachment
, _uids_to_notify :: [Int]
}
deriving (Show, Eq, Generic)
-- Custom ToJSON instance to omit empty lists and Nothing fields
instance ToJSON CommentCreate where
toJSON :: CommentCreate -> Value
toJSON CommentCreate {..} =
object $
filter
(not . isEmptyValue . L.snd)
[ ("content", Aeson.toJSON _content)
, ("project_id", maybe Aeson.Null (Aeson.toJSON . getProjectId) _project_id)
, ("task_id", maybe Aeson.Null (Aeson.toJSON . getTaskId) _task_id)
, ("attachment", Aeson.toJSON _attachment)
, ("uids_to_notify", Aeson.toJSON _uids_to_notify)
]
where
isEmptyValue :: Value -> Bool
isEmptyValue Aeson.Null = True
isEmptyValue (Aeson.Array v) = null v
isEmptyValue _ = False
-- | Request body for updating a comment
newtype CommentUpdate = CommentUpdate
{ _content :: Maybe Text
}
deriving (Show, Eq, Generic)
instance ToJSON CommentUpdate where
toJSON :: CommentUpdate -> Value
toJSON =
genericToJSON
Aeson.defaultOptions {Aeson.fieldLabelModifier = L.drop 1, Aeson.omitNothingFields = True}
-- | Query parameters for fetching comments
data CommentParam = CommentParam
{ project_id :: Maybe Text
, task_id :: Maybe Text
, cursor :: Maybe Text
, limit :: Maybe Int
, public_key :: Maybe Text
}
deriving (Show, Eq, Generic)
instance QueryParam CommentParam where
toQueryParam :: CommentParam -> Params
toQueryParam CommentParam {..} =
let projectParam = maybe [] (\pid -> [("project_id", pid)]) project_id
taskParam = maybe [] (\tid -> [("task_id", tid)]) task_id
cursorParam = maybe [] (\c -> [("cursor", c)]) cursor
limitParam = maybe [] (\l -> [("limit", T.pack $ show l)]) limit
publicKeyParam = maybe [] (\pk -> [("public_key", pk)]) public_key
in projectParam <> taskParam <> cursorParam <> limitParam <> publicKeyParam
{- | Constructor for CommentCreate
Truncates content to 15000 characters (Todoist API limit)
-}
newCommentBuilder :: Text -> Initial CommentCreate
newCommentBuilder content =
let truncated = T.take 15000 content
in seed $
CommentCreate
{ _content = Content truncated
, _project_id = Nothing
, _task_id = Nothing
, _attachment = Nothing
, _uids_to_notify = []
}
{- | Constructor for CommentUpdate
Truncates content to 15000 characters (Todoist API limit)
-}
updateCommentBuilder :: Text -> Initial CommentUpdate
updateCommentBuilder content =
let truncated = T.take 15000 content
in seed $ CommentUpdate {_content = Just truncated}
-- | Create new CommentParam for use with builder pattern
commentParamBuilder :: Initial CommentParam
commentParamBuilder =
seed $
CommentParam
{ project_id = Nothing
, task_id = Nothing
, cursor = Nothing
, limit = Nothing
, public_key = Nothing
}
-- | Type class for comment operations
class (Monad m) => TodoistCommentM m where
{- | Add a new comment to a project or task
Todo: have it return CommentId
-}
addComment :: CommentCreate -> m Comment
-- | Get all comments (automatic pagination)
getComments :: CommentParam -> m [Comment]
-- | Get comments with manual pagination control
getCommentsPaginated :: CommentParam -> m ([Comment], Maybe Text)
-- | Get a single comment by ID
getComment :: CommentId -> m Comment
-- | Update a comment's content
updateComment :: CommentUpdate -> CommentId -> m Comment
-- | Delete a comment
deleteComment :: CommentId -> m ()
-- Builder pattern instances
instance HasContent CommentCreate where
hasContent :: Text -> CommentCreate -> CommentCreate
hasContent content CommentCreate {..} = CommentCreate {_content = Content content, ..}
instance HasProjectId CommentCreate where
hasProjectId :: Text -> CommentCreate -> CommentCreate
hasProjectId pid CommentCreate {..} = CommentCreate {_project_id = Just (ProjectId pid), ..}
instance HasTaskId CommentCreate where
hasTaskId :: Text -> CommentCreate -> CommentCreate
hasTaskId tid CommentCreate {..} = CommentCreate {_task_id = Just (TaskId tid), ..}
instance HasAttachment CommentCreate where
hasAttachment :: Attachment -> CommentCreate -> CommentCreate
hasAttachment att CommentCreate {..} = CommentCreate {_attachment = Just att, ..}
instance HasUidsToNotify CommentCreate where
hasUidsToNotify :: [Int] -> CommentCreate -> CommentCreate
hasUidsToNotify uids CommentCreate {..} = CommentCreate {_uids_to_notify = uids, ..}
-- HasX instances for CommentParam
instance HasProjectId CommentParam where
hasProjectId :: Text -> CommentParam -> CommentParam
hasProjectId pid CommentParam {..} = CommentParam {project_id = Just pid, ..}
instance HasTaskId CommentParam where
hasTaskId :: Text -> CommentParam -> CommentParam
hasTaskId tid CommentParam {..} = CommentParam {task_id = Just tid, ..}
instance HasCursor CommentParam where
hasCursor :: Text -> CommentParam -> CommentParam
hasCursor c CommentParam {..} = CommentParam {cursor = Just c, ..}
instance HasLimit CommentParam where
hasLimit :: Int -> CommentParam -> CommentParam
hasLimit l CommentParam {..} = CommentParam {limit = Just l, ..}
instance HasPublicKey CommentParam where
hasPublicKey :: Text -> CommentParam -> CommentParam
hasPublicKey pk CommentParam {..} = CommentParam {public_key = Just pk, ..}