packages feed

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

{-# LANGUAGE LambdaCase #-}

-- | @\/v1\/users@
module Notion.V1.Users
  ( -- * Main types
    UserID,
    UserObject (..),
    UserType (..),
    PersonUser (..),
    BotUser (..),
    UserOwner (..),
    WorkspaceLimits (..),
    UserReference (..),

    -- * Users inside values
    UserValue (..),
    userValueId,
    GroupObject (..),
    PeopleEntry (..),

    -- * Servant
    API,
  )
where

import Control.Applicative ((<|>))
import Data.Aeson ((.:?), (.=))
import Data.Aeson qualified as Aeson
import Data.Aeson.KeyMap qualified as KeyMap
import Data.Aeson.Types ((.:))
import Notion.Prelude
import Notion.V1.Common (ObjectType (..), UUID)
import Notion.V1.ListOf (ListOf)

-- | User ID
type UserID = UUID

-- | Notion user object
data UserObject = UserObject
  { id :: UserID,
    name :: Maybe Text,
    avatarUrl :: Maybe Text,
    type_ :: UserType,
    person :: Maybe PersonUser,
    bot :: Maybe BotUser,
    object :: ObjectType
  }
  deriving stock (Eq, Generic, Show)

instance FromJSON UserObject where
  parseJSON = genericParseJSON aesonOptions {fieldLabelModifier = \s -> if s == "type_" then "type" else labelModifier s}

-- | User type
data UserType
  = Person
  | Bot
  deriving stock (Eq, Generic, Show)

instance FromJSON UserType where
  parseJSON = genericParseJSON aesonOptions

-- | Person user
newtype PersonUser = PersonUser
  { email :: Maybe Text
  }
  deriving stock (Eq, Generic, Show)

instance FromJSON PersonUser where
  parseJSON = genericParseJSON aesonOptions

-- | Workspace limits for bot users.
data WorkspaceLimits = WorkspaceLimits
  { maxFileUploadSizeInBytes :: Maybe Natural
  }
  deriving stock (Eq, Generic, Show)

instance FromJSON WorkspaceLimits where
  parseJSON = genericParseJSON aesonOptions

-- | Bot user
data BotUser = BotUser
  { owner :: Maybe UserOwner,
    workspaceName :: Maybe Text,
    workspaceId :: Maybe Text,
    workspaceLimits :: Maybe WorkspaceLimits
  }
  deriving stock (Eq, Generic, Show)

instance FromJSON BotUser where
  parseJSON = genericParseJSON aesonOptions

-- | User owner
data UserOwner
  = UserOwner {type_ :: Text, user :: UserID}
  | WorkspaceOwner {type_ :: Text, workspace :: Bool}
  | -- | Owner kind not modelled yet; holds the raw owner object.
    UnknownOwner {type_ :: Text, ownerValue :: Value}
  deriving stock (Eq, Generic, Show)

instance FromJSON UserOwner where
  parseJSON = \case
    Object o -> do
      ownerType :: Text <- o .: "type"
      case ownerType of
        "user" -> do
          -- Notion sends the owning user object, not a bare ID.
          userObj <- o .: "user"
          UserOwner ownerType <$> userObj .: "id"
        "workspace" -> WorkspaceOwner ownerType <$> (o .: "workspace")
        _ -> pure (UnknownOwner ownerType (Object o))
    _ -> fail "Expected object for UserOwner"

-- | Simple user reference objects that appear in created_by and last_edited_by fields
data UserReference = UserReference
  { id :: UserID,
    object :: Text
  }
  deriving stock (Generic, Show)

instance FromJSON UserReference where
  parseJSON = \case
    Object o -> UserReference <$> o .: "id" <*> o .: "object"
    _ -> fail "Expected object for UserReference"

instance ToJSON UserReference where
  toJSON = genericToJSON aesonOptions

-- | A user as it appears inside mentions, people values and verification
-- values: either just a reference (@{"object":"user","id":...}@) or a full
-- user object (one with a @type@ key).
data UserValue
  = PartialUser UserID
  | FullUser UserObject
  deriving stock (Eq, Generic, Show)

-- | The ID of a partial or full user.
userValueId :: UserValue -> UserID
userValueId (PartialUser i) = i
userValueId (FullUser UserObject {id = i}) = i

-- | A full user object that this library cannot decode (for example a new
-- user type) is kept as a 'PartialUser' rather than failing the response.
instance FromJSON UserValue where
  parseJSON = \case
    Object o
      | KeyMap.member "type" o -> (FullUser <$> parseJSON (Object o)) <|> (PartialUser <$> o .: "id")
      | otherwise -> PartialUser <$> o .: "id"
    _ -> fail "Expected object for UserValue"

-- | Encodes the request shape only (@object@ and @id@); full user details are
-- not sent back to the API.
instance ToJSON UserValue where
  toJSON u = Aeson.object ["object" .= ("user" :: Text), "id" .= userValueId u]

-- | A group (team) that can appear in a people property.
data GroupObject = GroupObject
  { id :: UUID,
    name :: Maybe Text
  }
  deriving stock (Eq, Generic, Show)

-- | One entry of a people property: a user or a group.
data PeopleEntry
  = PersonEntry UserValue
  | GroupEntry GroupObject
  deriving stock (Eq, Generic, Show)

instance FromJSON PeopleEntry where
  parseJSON = \case
    Object o -> do
      objectType :: Maybe Text <- o .:? "object"
      case objectType of
        Just "group" -> GroupEntry <$> (GroupObject <$> o .: "id" <*> o .:? "name")
        _ -> PersonEntry <$> parseJSON (Object o)
    _ -> fail "Expected object for PeopleEntry"

instance ToJSON PeopleEntry where
  toJSON = \case
    PersonEntry u -> toJSON u
    GroupEntry (GroupObject gid gname) ->
      Aeson.object (["object" .= ("group" :: Text), "id" .= gid] <> maybe [] (\n -> ["name" .= n]) gname)

-- | Servant API
type API =
  "users"
    :> ( Capture "user_id" UserID
           :> Get '[JSON] UserObject
           :<|> QueryParam "page_size" Natural
           :> QueryParam "start_cursor" Text
           :> Get '[JSON] (ListOf UserObject)
           :<|> "me"
           :> Get '[JSON] UserObject
       )