{-# 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