packages feed

discord-haskell-1.15.2: examples/interaction-commands.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns #-}

import Control.Monad (when)
import qualified Data.ByteString as B
import Data.Char (isDigit)
import Data.Functor ((<&>))
import Data.List (transpose)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import Discord
import Discord.Interactions
import qualified Discord.Requests as R
import Discord.Types
import UnliftIO (liftIO)
import UnliftIO.Concurrent

import ExampleUtils (getToken, getGuildId, actionWithChannelId)

main :: IO ()
main = interactionCommandExample

void :: DiscordHandler (Either RestCallErrorCode b) -> DiscordHandler ()
void =
  ( >>=
      ( \case
          Left e -> liftIO $ print e
          Right _ -> return ()
      )
  )

-- | Creates and manages a variety of interactions, including a tic tac toe example.
interactionCommandExample :: IO ()
interactionCommandExample = do
  tok <- getToken
  testserverid <- getGuildId

  -- open ghci and run  [[ :info RunDiscordOpts ]] to see available fields
  t <-
    runDiscord $
      def
        { discordToken = tok,
          discordOnStart = startHandler testserverid,
          discordOnEnd = liftIO $ putStrLn "Ended",
          discordOnEvent = eventHandler testserverid,
          discordOnLog = \s -> TIO.putStrLn s >> TIO.putStrLn "",
          discordGatewayIntent = def {gatewayIntentMembers = True, gatewayIntentPresences = True}
        }
  TIO.putStrLn t

-- If the start handler throws an exception, discord-haskell will gracefully shutdown
--     Use place to execute commands you know you want to complete
startHandler :: GuildId -> DiscordHandler ()
startHandler testserverid = do
  let activity =
        def
          { activityName = "ping-pong",
            activityType = ActivityTypeGame
          }
  let opts =
        UpdateStatusOpts
          { updateStatusOptsSince = Nothing,
            updateStatusOptsGame = Just activity,
            updateStatusOptsNewStatus = UpdateStatusOnline,
            updateStatusOptsAFK = False
          }
  sendCommand (UpdateStatus opts)

  actionWithChannelId testserverid $ \cid ->
    void $
      restCall $
        R.CreateMessage
          cid
          "Hello! I will reply to pings with pongs"

-- | Example user command
exampleUserCommand :: Maybe CreateApplicationCommand
exampleUserCommand = createUser "usercomm"

-- | Example slash command that has subcommands and multiple types of fields.
newExampleSlashCommand :: Maybe CreateApplicationCommand
newExampleSlashCommand =
  createChatInput
    "subtest"
    "testing out subcommands"
    >>= \d ->
      Just $
        d
          { createOptions =
              Just
                ( OptionsSubcommands
                    [ OptionSubcommandGroup
                        "frstsubcmdgrp"
                        Nothing
                        "the sub command group"
                        Nothing
                        [ OptionSubcommand
                            "frstsubcmd"
                            Nothing
                            "the first sub sub command"
                            Nothing
                            [ OptionValueString
                                "onestringinput"
                                Nothing
                                "two options"
                                Nothing
                                True
                                ( Right
                                    [ Choice "green" Nothing "green",
                                      Choice "red" Nothing "red"
                                    ]
                                )
                                Nothing
                                Nothing,
                              OptionValueInteger "oneintinput" Nothing "choices galore" Nothing False (Left False) Nothing Nothing
                            ]
                        ],
                      OptionSubcommandOrGroupSubcommand $
                        OptionSubcommand
                          "frstsubcmd"
                          Nothing
                          "the first subcommand"
                          Nothing
                          [ OptionValueString
                              "onestringinput"
                              Nothing
                              "two options"
                              Nothing
                              True
                              ( Right
                                  [ Choice "yellow" Nothing "yellow",
                                    Choice "blue" Nothing "blue"
                                  ]
                              )
                              Nothing
                              Nothing
                          ],
                      OptionSubcommandOrGroupSubcommand $
                        OptionSubcommand
                          "sndsubcmd"
                          Nothing
                          "the second subcommand"
                          Nothing
                          [ OptionValueBoolean
                              "trueorfalse"
                              Nothing
                              "true or false"
                              Nothing
                              True,
                            OptionValueNumber
                              "numbercomm"
                              Nothing
                              "number option"
                              Nothing
                              False
                              (Left True)
                              (Just 3.1415)
                              (Just 101),
                            OptionValueInteger
                              "numbercomm2"
                              Nothing
                              "another number option"
                              Nothing
                              False
                              (Right [Choice "one" Nothing 1, Choice "two" Nothing 2, Choice "minus 1" Nothing (-1)])
                              (Just $ -1)
                              (Just $ -2),
                            OptionValueInteger
                              "numbercomm3"
                              Nothing
                              "another another number option"
                              Nothing
                              False
                              (Left True)
                              (Just $ -50)
                              (Just 50),
                            OptionValueUser
                              "user"
                              Nothing
                              "testing asking for a user"
                              Nothing
                              False,
                            OptionValueChannel
                              "channel"
                              Nothing
                              "testing asking for a channel"
                              Nothing
                              False
                              (Just [ApplicationCommandChannelTypeGuildVoice]),
                            OptionValueMentionable
                              "mentionable"
                              Nothing
                              "testing asking for a mentionable"
                              Nothing
                              False
                          ]
                    ]
                )
          }

-- | An example slash command.
exampleSlashCommand :: Maybe CreateApplicationCommand
exampleSlashCommand =
  createChatInput
    "test"
    "here is a description"
    >>= \cac ->
      return $
        cac
          { createOptions =
              Just $
                OptionsValues
                  [ OptionValueString
                      "randominput"
                      Nothing
                      "I shall not"
                      Nothing
                      True
                      (Right [Choice "firstOpt" Nothing "yay", Choice "secondOpt" Nothing "nay"])
                      Nothing
                      Nothing
                  ]
          }

exampleInteractionResponse :: OptionsData -> InteractionResponse
exampleInteractionResponse (OptionsDataValues [OptionDataValueString {optionDataValueString = s}]) =
  interactionResponseBasic (T.pack $ "Here's the reply! You chose: " ++ show s)
exampleInteractionResponse _ =
  interactionResponseBasic
    "Something unexpected happened - the value was not what I expected!"

getImage :: IO B.ByteString
getImage = return "\137PNG\r\n\SUB\n\NUL\NUL\NUL\rIHDR\NUL\NUL\NUL\SOH\NUL\NUL\NUL\SOH\b\STX\NUL\NUL\NUL\144wS\222\NUL\NUL\NUL\SOHsRGB\NUL\174\206\FS\233\NUL\NUL\NUL\EOTgAMA\NUL\NUL\177\143\v\252a\ENQ\NUL\NUL\NUL\tpHYs\NUL\NUL\SO\195\NUL\NUL\SO\195\SOH\199o\168d\NUL\NUL\NUL\fIDAT\CANWc\248\239\180\t\NUL\EOT7\SOH\244\162\155\ENQ\235\NUL\NUL\NUL\NULIEND\174B`\130"


-- If an event handler throws an exception, discord-haskell will continue to run
eventHandler :: GuildId -> Event -> DiscordHandler ()
eventHandler testserverid event = case event of
  MessageCreate m -> when (not (fromBot m) && isPing m) $ do
    void $ restCall (R.CreateReaction (messageChannelId m, messageId m) "eyes")
    threadDelay (2 * 10 ^ (6 :: Int))

    -- A very simple message.
    void $ restCall (R.CreateMessage (messageChannelId m) "Pong!")
    exampleImage <- liftIO getImage

    -- A more complex message. Text-to-speech, does not mention everyone nor
    -- the user, and uses Discord native replies.
    -- Use ":info" in ghci to explore the type
    let opts :: R.MessageDetailedOpts
        opts =
          def
            { R.messageDetailedContent = "Here's a more complex message, but doesn't ping @everyone!",
              R.messageDetailedTTS = True,
              R.messageDetailedAllowedMentions =
                Just $
                  def
                    { R.mentionEveryone = False,
                      R.mentionRepliedUser = False
                    },
              R.messageDetailedReference =
                Just $
                  def {referenceMessageId = Just $ messageId m}
            }
    void $ restCall (R.CreateMessageDetailed (messageChannelId m) opts)
    let opts' :: R.MessageDetailedOpts
        opts' =
          def
            { R.messageDetailedContent = "An example of a message with buttons!",
              R.messageDetailedComponents =
                Just
                  [ ActionRowButtons
                      [ Button "Button 1" False ButtonStylePrimary (Just "Button 1") (Just (mkEmoji "🔥")),
                        Button "Button 2" True ButtonStyleSuccess (Just "Button 2") Nothing,
                        ButtonUrl
                          "https://github.com/discord-haskell/discord-haskell"
                          False
                          (Just "Button 3")
                          Nothing
                      ],
                    ActionRowSelectMenu
                      ( SelectMenu
                          "action select menu"
                          False
                          [ SelectOption "First option" "opt1" (Just "the only desc") Nothing Nothing,
                            SelectOption "Second option" "opt2" Nothing (Just (mkEmoji "😭")) (Just True),
                            SelectOption "third option" "opt3" Nothing Nothing Nothing,
                            SelectOption "fourth option" "opt4" Nothing Nothing Nothing,
                            SelectOption "fifth option" "opt5" Nothing Nothing Nothing
                          ]
                          (Just "this is a place holder")
                          (Just 2)
                          (Just 5)
                      )
                  ],
              R.messageDetailedEmbeds =
                Just
                  [ def
                      { createEmbedTitle = "Title",
                        createEmbedDescription = "the description",
                        createEmbedAuthorName = "Author name is Required",
                        createEmbedImage = Just (CreateEmbedImageUrl "https://media.discordapp.net/attachments/365969021083975681/936055590415921172/Warning.png"),
                        createEmbedColor = Just DiscordColorLuminousVividPink,
                        createEmbedAuthorIcon = Just (CreateEmbedImageUpload exampleImage)
                      },
                    def {createEmbedTitle = "a different Title", createEmbedDescription = "another desc"}
                  ]
            }
        tictactoe :: R.MessageDetailedOpts
        tictactoe =
          def
            { R.messageDetailedContent = "Playing tic tac toe! Player 0",
              R.messageDetailedComponents = Just $ updateTicTacToe Nothing []
            }
    void $ restCall (R.CreateMessageDetailed (messageChannelId m) opts')
    void $ restCall (R.CreateMessageDetailed (messageChannelId m) tictactoe)
  Ready _ _ _ _ _ _ (PartialApplication i _) -> do
    vs <-
      mapM
        (maybe (return (Left $ RestCallErrorCode 0 "" "")) (restCall . R.CreateGuildApplicationCommand i testserverid))
        [exampleSlashCommand, exampleUserCommand, newExampleSlashCommand, createChatInput "modal" "modal test"]
    liftIO (putStrLn $ "number of application commands added " ++ show (length vs))
    acs <- restCall (R.GetGuildApplicationCommands i testserverid)
    case acs of
      Left r -> liftIO $ print r
      Right ls -> liftIO $ putStrLn $ "number of application commands total " ++ show (length ls)
  InteractionCreate InteractionComponent {componentData = cb@ButtonData {componentDataCustomId = (T.take 3 -> "ttt")}, ..} -> case processTicTacToe cb interactionMessage of
    [r] ->
      void
        ( restCall
            ( R.CreateInteractionResponse
                interactionId
                interactionToken
                (InteractionResponseUpdateMessage r)
            )
        )
    r : rs ->
      void
        ( restCall $
            R.CreateInteractionResponse
              interactionId
              interactionToken
              (InteractionResponseUpdateMessage r)
        )
        >> mapM_
          ( restCall
              . R.CreateFollowupInteractionMessage
                interactionApplicationId
                interactionToken
          )
          rs
    _ -> return ()
  InteractionCreate InteractionApplicationCommand {applicationCommandData = ApplicationCommandDataUser {applicationCommandDataName = nm, applicationCommandDataTargetUserId = uid, ..}, ..} ->
    void $
      restCall
        (R.CreateInteractionResponse interactionId interactionToken (interactionResponseBasic $ "Command " <> nm <> T.pack (" selected user: " ++ show uid)))
  InteractionCreate InteractionApplicationCommand {applicationCommandData = ApplicationCommandDataChatInput {applicationCommandDataName = "test", optionsData = Just d, ..}, ..} ->
    void $
      restCall
        (R.CreateInteractionResponse interactionId interactionToken (exampleInteractionResponse d))
  InteractionCreate InteractionApplicationCommand {applicationCommandData = ApplicationCommandDataChatInput {applicationCommandDataName = "subtest", optionsData = Just d, ..}, ..} -> void $ restCall (R.CreateInteractionResponse interactionId interactionToken (interactionResponseBasic (T.pack $ "oh boy, subcommands! welp, here's everything I got from that: " <> show d)))
  InteractionCreate InteractionComponent {componentData = ButtonData {..}, ..} ->
    void $
      restCall
        ( R.CreateInteractionResponse interactionId interactionToken $
            InteractionResponseChannelMessage
              ( ( interactionResponseMessageBasic $ "You pressed the button " <> componentDataCustomId
                )
                  { interactionResponseMessageFlags = Just (InteractionResponseMessageFlags [InteractionResponseMessageFlagEphermeral])
                  }
              )
        )
  InteractionCreate InteractionComponent {componentData = SelectMenuData {componentDataValues = vs}, ..} ->
    void
      ( do
          exampleImage <- liftIO getImage
          aid <- readCache <&> cacheApplication <&> partialApplicationID
          _ <- restCall (R.CreateInteractionResponse interactionId interactionToken InteractionResponseDeferChannelMessage)
          restCall
            ( R.CreateFollowupInteractionMessage
                aid
                interactionToken
                (interactionResponseMessageBasic (T.pack $ "oh dear, select menu. thank you for waiting" <> show vs))
                  { interactionResponseMessageEmbeds =
                      Just
                        [ def
                            { createEmbedTitle = "Select menu title",
                              createEmbedDescription = "Here is the select menu embed desc",
                              createEmbedAuthorName = "someunknownentity",
                              createEmbedImage = Just (CreateEmbedImageUrl "https://media.discordapp.net/attachments/365969021083975681/936055590415921172/Warning.png"),
                              createEmbedColor = Just DiscordColorDiscordBlurple,
                              createEmbedAuthorIcon = Just (CreateEmbedImageUpload exampleImage)
                            }
                        ]
                  }
            )
      )
  InteractionCreate InteractionApplicationCommandAutocomplete {applicationCommandData = ApplicationCommandDataChatInput {applicationCommandDataName = "subtest", optionsData = Just _, ..}, ..} -> void (restCall $ R.CreateInteractionResponse interactionId interactionToken (InteractionResponseAutocompleteResult (InteractionResponseAutocompleteInteger [Choice "five" Nothing 5])))
  InteractionCreate i@InteractionApplicationCommand {applicationCommandData = ApplicationCommandDataChatInput {applicationCommandDataName = "modal"}} ->
    void $
      restCall
        ( R.CreateInteractionResponse
            (interactionId i)
            (interactionToken i)
            ( InteractionResponseModal
                ( InteractionResponseModalData
                    "customidmodal"
                    "modal title"
                    [mkTextInput "textcid" "textlabel"]
                )
            )
        )
  InteractionCreate i@InteractionModalSubmit {modalData = idm} -> void $ restCall (R.CreateInteractionResponse (interactionId i) (interactionToken i) (interactionResponseBasic (T.pack (show idm))))
  _ -> return ()

processTicTacToe :: ComponentData -> Message -> [InteractionResponseMessage]
processTicTacToe (ButtonData cid) m = case messageComponents m of
  Nothing -> [interactionResponseMessageBasic "Sorry, I couldn't get the components on that message."]
  (Just cs) ->
    let newComp = newComp' cs
     in ( ( interactionResponseMessageBasic
              ("Some Tic Tac Toe! Player " <> (if '0' == T.last (messageContent m) then "1" else "0"))
          )
            { interactionResponseMessageComponents = Just ((if checkTicTacToe newComp then (disableAll <$>) else id) newComp)
            }
        ) :
          [interactionResponseMessageBasic ("Player " <> T.singleton player <> " has won!") | checkTicTacToe newComp]
  where
    player = T.last (messageContent m)
    newComp' = updateTicTacToe (Just (cid, '0' == player))
    disableAll (ActionRowButtons cs) = ActionRowButtons $ (\c -> c {buttonDisabled = True}) <$> cs
    disableAll c = c
processTicTacToe _ _ = [interactionResponseMessageBasic "Sorry, I couldn't understand that button."]

checkTicTacToe :: [ActionRow] -> Bool
checkTicTacToe xs = checkRows unwrapped || checkRows unwrappedT || checkRows [diagonal unwrapped, diagonal (reverse <$> unwrapped)]
  where
    checkRows = any (\cbs -> all (\cb -> cb == head cbs && cb /= ButtonStyleSecondary) cbs)
    unwrapped = (\(ActionRowButtons cbs) -> (\Button {buttonStyle = style} -> style) <$> cbs) <$> xs
    unwrappedT = transpose unwrapped
    diagonal [] = []
    diagonal ([] : _) = []
    diagonal (ys : yss) = head ys : diagonal (tail <$> yss)

updateTicTacToe :: Maybe (T.Text, Bool) -> [ActionRow] -> [ActionRow]
updateTicTacToe Nothing _ = (\y -> ActionRowButtons $ (\x -> Button (T.pack $ "ttt " <> show x <> show y) False ButtonStyleSecondary (Just "[ ]") Nothing) <$> [0 .. 4]) <$> [0 .. 4]
updateTicTacToe (Just (tttxy, isFirst)) car
  | not (checkIsValid tttxy) = car
  | otherwise = (\(ActionRowButtons cbs) -> ActionRowButtons (changeIf <$> cbs)) <$> car
  where
    checkIsValid tttxy' = T.length tttxy' == 6 && all isDigit [T.index tttxy' 4, T.index tttxy' 5]
    getxy tttxy' = (T.index tttxy' 4, T.index tttxy' 5)
    (style, symbol) = if isFirst then (ButtonStyleSuccess, "[X]") else (ButtonStyleDanger, "[O]")
    changeIf cb@Button {..}
      | checkIsValid buttonCustomId && getxy tttxy == getxy buttonCustomId = cb {buttonDisabled = True, buttonStyle = style, buttonLabel = Just symbol}
      | otherwise = cb
    changeIf cb = cb

fromBot :: Message -> Bool
fromBot = userIsBot . messageAuthor

isPing :: Message -> Bool
isPing = ("ping" `T.isPrefixOf`) . T.toLower . messageContent