packages feed

calamity-0.8.0.0: Calamity/Types/Model/Channel/Component.hs

{-# LANGUAGE TemplateHaskell #-}

-- | Message components
module Calamity.Types.Model.Channel.Component (
  CustomID (..),
  Component (..),
  Button (..),
  LinkButton (..),
  button,
  button',
  lbutton,
  lbutton',
  ButtonStyle (..),
  Select (..),
  select,
  SelectOption (..),
  sopt,
  TextInput (..),
  TextInputStyle (..),
  textInput,
  ComponentType (..),
  componentType,
) where

import Calamity.Internal.Utils (CalamityToJSON (..), CalamityToJSON' (..), (.=), (.?=))
import Calamity.Types.Model.Guild.Emoji
import Control.Monad (replicateM)
import Data.Aeson ((.!=), (.:), (.:?))
import Data.Aeson qualified as Aeson
import Data.Maybe (catMaybes)
import Data.Scientific (toBoundedInteger)
import Data.Text qualified as T
import Optics.TH
import System.Random (Uniform)
import System.Random.Stateful (Uniform (uniformM), UniformRange (uniformRM))
import TextShow.TH

newtype CustomID = CustomID T.Text
  deriving stock (Eq, Ord, Show)
  deriving (Aeson.ToJSON, Aeson.FromJSON) via T.Text

$(deriveTextShow ''CustomID)

instance Uniform CustomID where
  uniformM = ((CustomID . T.pack) <$>) . replicateM 16 . uniformRM ('a', 'z')

data ComponentType
  = ActionRowType
  | ButtonType
  | SelectType
  | TextInputType
  deriving (Eq, Show)

$(deriveTextShow ''ComponentType)

instance Aeson.ToJSON ComponentType where
  toJSON x = Aeson.toJSON @Int $ case x of
    ActionRowType -> 1
    ButtonType -> 2
    SelectType -> 3
    TextInputType -> 4
  toEncoding x = Aeson.toEncoding @Int $ case x of
    ActionRowType -> 1
    ButtonType -> 2
    SelectType -> 3
    TextInputType -> 4

instance Aeson.FromJSON ComponentType where
  parseJSON = Aeson.withScientific "Components.ComponentType" $ \n -> case toBoundedInteger @Int n of
    Just 1 -> pure ActionRowType
    Just 2 -> pure ButtonType
    Just 3 -> pure SelectType
    Just 4 -> pure TextInputType
    _ -> fail $ "Invalid ComponentType: " <> show n

data ButtonStyle
  = ButtonPrimary
  | ButtonSecondary
  | ButtonSuccess
  | ButtonDanger
  | ButtonLink
  deriving (Eq, Show)

$(deriveTextShow ''ButtonStyle)

instance Aeson.ToJSON ButtonStyle where
  toJSON t = Aeson.toJSON @Int $ case t of
    ButtonPrimary -> 1
    ButtonSecondary -> 2
    ButtonSuccess -> 3
    ButtonDanger -> 4
    ButtonLink -> 5
  toEncoding t = Aeson.toEncoding @Int $ case t of
    ButtonPrimary -> 1
    ButtonSecondary -> 2
    ButtonSuccess -> 3
    ButtonDanger -> 4
    ButtonLink -> 5

instance Aeson.FromJSON ButtonStyle where
  parseJSON = Aeson.withScientific "Components.ButtonStyle" $ \n -> case toBoundedInteger @Int n of
    Just v -> case v of
      1 -> pure ButtonPrimary
      2 -> pure ButtonSecondary
      3 -> pure ButtonSuccess
      4 -> pure ButtonDanger
      5 -> pure ButtonLink
      _ -> fail $ "Invalid ButtonStyle: " <> show n
    Nothing -> fail $ "Invalid ButtonStyle: " <> show n

data Button = Button
  { style :: ButtonStyle
  , label :: Maybe T.Text
  , emoji :: Maybe RawEmoji
  , disabled :: Bool
  , customID :: CustomID
  }
  deriving (Show)
  deriving (Aeson.ToJSON) via CalamityToJSON Button

instance CalamityToJSON' Button where
  toPairs Button {..} =
    [ "style" .= style
    , "label" .?= label
    , "emoji" .?= emoji
    , "disabled" .= disabled
    , "custom_id" .= customID
    , "type" .= ButtonType
    ]

$(deriveTextShow ''Button)
$(makeFieldLabelsNoPrefix ''Button)

instance Aeson.FromJSON Button where
  parseJSON = Aeson.withObject "Components.Button" $ \v ->
    Button
      <$> v .: "style"
      <*> v .:? "label"
      <*> v .:? "emoji"
      <*> v .:? "disabled" .!= False
      <*> v .: "custom_id"

data LinkButton = LinkButton
  { style :: ButtonStyle
  , label :: Maybe T.Text
  , emoji :: Maybe RawEmoji
  , url :: T.Text
  , disabled :: Bool
  }
  deriving (Show)
  deriving (Aeson.ToJSON) via CalamityToJSON LinkButton

instance CalamityToJSON' LinkButton where
  toPairs LinkButton {..} =
    [ "style" .= style
    , "label" .?= label
    , "emoji" .?= emoji
    , "url" .= url
    , "disabled" .= disabled
    , "type" .= ButtonType
    ]

instance Aeson.FromJSON LinkButton where
  parseJSON = Aeson.withObject "Components.Linkbutton" $ \v ->
    LinkButton
      <$> v .: "style"
      <*> v .:? "label"
      <*> v .:? "emoji"
      <*> v .: "url"
      <*> v .:? "disabled" .!= False

$(deriveTextShow ''LinkButton)
$(makeFieldLabelsNoPrefix ''LinkButton)

{- | Constuct a non-disabled 'Button' with the given 'ButtonStyle' and 'CustomID',
 all other fields are set to 'Nothing'
-}
button :: ButtonStyle -> CustomID -> Button
button s = Button s Nothing Nothing False

{- | Constuct a non-disabled 'Button' with the given 'ButtonStyle', 'CustomID',
 and label, all other fields are set to 'Nothing'
-}
button' :: ButtonStyle -> T.Text -> CustomID -> Button
button' s l = Button s (Just l) Nothing False

{- | Constuct a non-disabled 'LinkButton' with the given 'ButtonStyle', link, all
   other fields are set to 'Nothing'
-}
lbutton ::
  ButtonStyle ->
  -- | The link to use
  T.Text ->
  LinkButton
lbutton s lnk = LinkButton s Nothing Nothing lnk False

{- | Constuct a non-disabled 'LinkButton' with the given 'ButtonStyle', link,
 and label, all other fields are set to 'Nothing'
-}
lbutton' ::
  ButtonStyle ->
  -- | The link to use
  T.Text ->
  -- | The label to use
  T.Text ->
  LinkButton
lbutton' s lnk lbl = LinkButton s (Just lbl) Nothing lnk False

data SelectOption = SelectOption
  { label :: T.Text
  , value :: T.Text
  , description :: Maybe T.Text
  , emoji :: Maybe RawEmoji
  , default_ :: Bool
  }
  deriving (Show)
  deriving (Aeson.ToJSON) via CalamityToJSON SelectOption

instance CalamityToJSON' SelectOption where
  toPairs SelectOption {..} =
    [ "label" .= label
    , "value" .= value
    , "description" .?= description
    , "emoji" .?= emoji
    , "default" .= default_
    ]

instance Aeson.FromJSON SelectOption where
  parseJSON = Aeson.withObject "Components.SelectOption" $ \v ->
    SelectOption
      <$> v .: "label"
      <*> v .: "value"
      <*> v .:? "description"
      <*> v .:? "emoji"
      <*> v .:? "default" .!= False

$(deriveTextShow ''SelectOption)
$(makeFieldLabelsNoPrefix ''SelectOption)

data Select = Select
  { options :: [SelectOption]
  , placeholder :: Maybe T.Text
  , minValues :: Maybe Int
  , maxValues :: Maybe Int
  , disabled :: Bool
  , customID :: CustomID
  }
  deriving (Show)
  deriving (Aeson.ToJSON) via CalamityToJSON Select

instance CalamityToJSON' Select where
  toPairs Select {..} =
    [ "options" .= options
    , "placeholder" .?= placeholder
    , "min_values" .?= minValues
    , "max_values" .?= maxValues
    , "disabled" .= disabled
    , "custom_id" .= customID
    , "type" .= SelectType
    ]

instance Aeson.FromJSON Select where
  parseJSON = Aeson.withObject "Components.Select" $ \v ->
    Select
      <$> v .: "options"
      <*> v .:? "placeholder"
      <*> v .:? "min_values"
      <*> v .:? "max_values"
      <*> v .:? "disabled" .!= False
      <*> v .: "custom_id"

$(deriveTextShow ''Select)
$(makeFieldLabelsNoPrefix ''Select)

select :: [SelectOption] -> CustomID -> Select
select o = Select o Nothing Nothing Nothing False

sopt ::
  -- | Label
  T.Text ->
  -- | Value
  T.Text ->
  SelectOption
sopt l v = SelectOption l v Nothing Nothing False

data TextInputStyle
  = TextInputShort
  | TextInputParagraph
  deriving (Show)

$(deriveTextShow ''TextInputStyle)

instance Aeson.ToJSON TextInputStyle where
  toJSON t = Aeson.toJSON @Int $ case t of
    TextInputShort -> 1
    TextInputParagraph -> 2
  toEncoding t = Aeson.toEncoding @Int $ case t of
    TextInputShort -> 1
    TextInputParagraph -> 2

instance Aeson.FromJSON TextInputStyle where
  parseJSON = Aeson.withScientific "Components.TextInputStyle" $ \n -> case toBoundedInteger @Int n of
    Just v -> case v of
      1 -> pure TextInputShort
      2 -> pure TextInputParagraph
      _ -> fail $ "Invalid TextInputStyle: " <> show n
    Nothing -> fail $ "Invalid TextInputStyle: " <> show n

data TextInput = TextInput
  { style :: TextInputStyle
  , label :: T.Text
  , minLength :: Maybe Int
  , maxLength :: Maybe Int
  , required :: Bool
  , value :: Maybe T.Text
  , placeholder :: Maybe T.Text
  , customID :: CustomID
  }
  deriving (Show)
  deriving (Aeson.ToJSON) via CalamityToJSON TextInput

instance CalamityToJSON' TextInput where
  toPairs TextInput {..} =
    [ "style" .= style
    , "label" .= label
    , "min_length" .= minLength
    , "max_length" .= maxLength
    , "required" .= required
    , "value" .= value
    , "placeholder" .= placeholder
    , "custom_id" .= customID
    , "type" .= TextInputType
    ]

instance Aeson.FromJSON TextInput where
  parseJSON = Aeson.withObject "Components.TextInput" $ \v ->
    TextInput
      <$> v .: "style"
      <*> v .: "label"
      <*> v .:? "min_length"
      <*> v .:? "max_length"
      <*> v .:? "required" .!= False
      <*> v .:? "value"
      <*> v .:? "placeholder"
      <*> v .: "custom_id"

$(deriveTextShow ''TextInput)
$(makeFieldLabelsNoPrefix ''TextInput)

textInput ::
  TextInputStyle ->
  -- | Label
  T.Text ->
  CustomID ->
  TextInput
textInput s l = TextInput s l Nothing Nothing True Nothing Nothing

data Component
  = ActionRow' [Component]
  | Button' Button
  | LinkButton' LinkButton
  | Select' Select
  | TextInput' TextInput
  deriving (Show)

$(deriveTextShow ''Component)

instance Aeson.ToJSON Component where
  toJSON t =
    case t of
      ActionRow' xs -> Aeson.object . catMaybes $ ["components" .= xs, "type" .= ActionRowType]
      Button' b -> Aeson.toJSON b
      LinkButton' lb -> Aeson.toJSON lb
      Select' s -> Aeson.toJSON s
      TextInput' ti -> Aeson.toJSON ti

  toEncoding t =
    case t of
      ActionRow' xs -> Aeson.pairs . mconcat . catMaybes $ ["components" .= xs, "type" .= ActionRowType]
      Button' b -> Aeson.toEncoding b
      LinkButton' lb -> Aeson.toEncoding lb
      Select' s -> Aeson.toEncoding s
      TextInput' ti -> Aeson.toEncoding ti

instance Aeson.FromJSON Component where
  parseJSON = Aeson.withObject "Component" $ \v -> do
    type_ :: ComponentType <- v .: "type"

    case type_ of
      ActionRowType -> ActionRow' <$> v .: "components"
      ButtonType -> do
        cid :: Maybe CustomID <- v .:? "custom_id"
        url :: Maybe T.Text <- v .:? "url"
        case (cid, url) of
          (Just _, _) -> Button' <$> Aeson.parseJSON (Aeson.Object v)
          (_, Just _) -> LinkButton' <$> Aeson.parseJSON (Aeson.Object v)
          _ -> fail $ "Impossible button: " <> show v
      SelectType -> Select' <$> Aeson.parseJSON (Aeson.Object v)
      TextInputType -> TextInput' <$> Aeson.parseJSON (Aeson.Object v)

componentType :: Component -> ComponentType
componentType (ActionRow' _) = ActionRowType
componentType (Button' _) = ButtonType
componentType (LinkButton' _) = ButtonType
componentType (Select' _) = SelectType
componentType (TextInput' _) = TextInputType