packages feed

calamity-0.12.1.0: Calamity/Commands/CalamityParsers.hs

{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# OPTIONS_GHC -fplugin=Polysemy.Plugin #-}

-- | 'ParameterParser' instances for calamity models
module Calamity.Commands.CalamityParsers () where

import Calamity.Cache.Eff
import Calamity.Commands.Context
import Calamity.Types.Model.Channel (Channel, GuildChannel)
import Calamity.Types.Model.Guild (Emoji, Guild, Member, Partial (PartialEmoji), RawEmoji (..), Role)
import Calamity.Types.Model.User (User)
import Calamity.Types.Partial
import Calamity.Types.Snowflake
import CalamityCommands.ParameterInfo
import CalamityCommands.Parser
import Control.Monad
import Control.Monad.Trans (lift)
import Data.Maybe (fromMaybe, isJust)
import Data.Text qualified as T
import Data.Typeable
import Optics
import Polysemy qualified as P
import Polysemy.Reader qualified as P
import Text.Megaparsec hiding (parse)
import Text.Megaparsec.Char.Lexer (decimal)
import Text.Megaparsec.Error.Builder (errFancy, fancy)

parserName :: forall a c r. (ParameterParser a c r) => T.Text
parserName =
  let ParameterInfo (fromMaybe "" -> name) type_ _ = parameterInfo @a @c @r
   in name <> ":" <> T.pack (show type_)

instance (Typeable (Snowflake a)) => ParameterParser (Snowflake a) c r where
  parse = parseMP (parserName @(Snowflake a)) snowflake
  parameterDescription = "discord id"

-- | Accepts both plain IDs and mentions
instance {-# OVERLAPS #-} ParameterParser (Snowflake User) c r where
  parse = parseMP (parserName @(Snowflake User)) (try (ping "@") <|> snowflake)
  parameterDescription = "user mention or id"

-- | Accepts both plain IDs and mentions
instance {-# OVERLAPS #-} ParameterParser (Snowflake Member) c r where
  parse = parseMP (parserName @(Snowflake Member)) (try (ping "@") <|> snowflake)
  parameterDescription = "user mention or id"

-- | Accepts both plain IDs and mentions
instance {-# OVERLAPS #-} ParameterParser (Snowflake Channel) c r where
  parse = parseMP (parserName @(Snowflake Channel)) (try (ping "#") <|> snowflake)
  parameterDescription = "channel mention or id"

-- | Accepts both plain IDs and mentions
instance {-# OVERLAPS #-} ParameterParser (Snowflake Role) c r where
  parse = parseMP (parserName @(Snowflake Role)) (try (ping "@&") <|> snowflake)
  parameterDescription = "role mention or id"

-- | Accepts both plain IDs and uses of emoji
instance {-# OVERLAPS #-} ParameterParser (Snowflake Emoji) c r where
  parse = parseMP (parserName @(Snowflake Emoji)) (try emoji <|> snowflake)
  parameterDescription = "emoji or id"

mapParserMaybeM :: (Monad m, Stream s) => ParsecT SpannedError s m a -> T.Text -> (a -> m (Maybe b)) -> ParsecT SpannedError s m b
mapParserMaybeM m e f = do
  offs <- getOffset
  r <- m >>= lift . f
  offe <- getOffset
  case r of
    Just r' -> pure r'
    Nothing -> parseError . errFancy offs . fancy . ErrorCustom $ SpannedError e offs offe

{- | ParameterParser for members in the guild the command was invoked in, this only looks
 in the cache. Use @'Snowflake' 'Member'@ and use
 'Calamity.Types.Upgradeable.upgrade' if you want to allow fetching from http.
-}
instance (P.Member CacheEff r, CalamityCommandContext c) => ParameterParser Member c r where
  parse =
    parseMP (parserName @Member @c @r) $
      mapParserMaybeM
        (try (ping "@") <|> snowflake)
        "Couldn't find a Member with this id"
        ( \mid -> do
            ctx <- P.ask
            guild <- join <$> getGuild `traverse` ctxGuildID ctx
            pure $ guild ^? _Just % #members % ix mid
        )
  parameterDescription = "user mention or id"

{- | ParameterParser for users, this only looks in the cache. Use @'Snowflake'
 'User'@ and use 'Calamity.Types.Upgradeable.upgrade' if you want to allow
 fetching from http.
-}
instance (P.Member CacheEff r) => ParameterParser User c r where
  parse =
    parseMP (parserName @User @c @r) $
      mapParserMaybeM
        (try (ping "@") <|> snowflake)
        "Couldn't find a User with this id"
        getUser
  parameterDescription = "user mention or id"

{- | ParameterParser for channels in the guild the command was invoked in, this only
 looks in the cache. Use @'Snowflake' 'Channel'@ and use
 'Calamity.Types.Upgradeable.upgrade' if you want to allow fetching from http.
-}
instance (P.Member CacheEff r, CalamityCommandContext c) => ParameterParser GuildChannel c r where
  parse =
    parseMP (parserName @GuildChannel @c @r) $
      mapParserMaybeM
        (try (ping "#") <|> snowflake)
        "Couldn't find a GuildChannel with this id"
        ( \cid -> do
            ctx <- P.ask
            guild <- join <$> getGuild `traverse` ctxGuildID ctx
            pure $ guild ^? _Just % #channels % ix cid
        )
  parameterDescription = "channel mention or id"

{- | ParameterParser for guilds, this only looks in the cache. Use @'Snowflake' 'Guild'@
 and use 'Calamity.Types.Upgradeable.upgrade' if you want to allow fetching
 from http.
-}
instance (P.Member CacheEff r) => ParameterParser Guild c r where
  parse =
    parseMP (parserName @Guild @c @r) $
      mapParserMaybeM
        snowflake
        "Couldn't find a Guild with this id"
        getGuild
  parameterDescription = "guild id"

{- | ParameterParser for emojis in the guild the command was invoked in, this only
 looks in the cache. Use @'Snowflake' 'Emoji'@ and use
 'Calamity.Types.Upgradeable.upgrade' if you want to allow fetching from http.
-}
instance (P.Member CacheEff r, CalamityCommandContext c) => ParameterParser Emoji c r where
  parse =
    parseMP (parserName @Emoji @c @r) $
      mapParserMaybeM
        (try emoji <|> snowflake)
        "Couldn't find an Emoji with this id"
        ( \eid -> do
            ctx <- P.ask
            guild <- join <$> getGuild `traverse` ctxGuildID ctx
            pure $ guild ^? _Just % #emojis % ix eid
        )
  parameterDescription = "emoji or id"

-- | Parses both discord emojis, and unicode emojis
instance ParameterParser RawEmoji c r where
  parse = parseMP (parserName @RawEmoji) (try parseCustomEmoji <|> UnicodeEmoji <$> takeP (Just "A unicode emoji") 1)
    where
      parseCustomEmoji = CustomEmoji <$> partialEmoji
  parameterDescription = "emoji"

{- | ParameterParser for roles in the guild the command was invoked in, this only
 looks in the cache. Use @'Snowflake' 'Role'@ and use
 'Calamity.Types.Upgradeable.upgrade' if you want to allow fetching from http.
-}
instance (P.Member CacheEff r, CalamityCommandContext c) => ParameterParser Role c r where
  parse =
    parseMP (parserName @Role @c @r) $
      mapParserMaybeM
        (try (ping "@&") <|> snowflake)
        "Couldn't find an Emoji with this id"
        ( \rid -> do
            ctx <- P.ask
            guild <- join <$> getGuild `traverse` ctxGuildID ctx
            pure $ guild ^? _Just % #roles % ix rid
        )
  parameterDescription = "role mention or id"

-- skipN :: (Stream s, Ord e) => Int -> ParsecT e s m ()
-- skipN n = void $ takeP Nothing n

ping :: (MonadParsec e T.Text m) => T.Text -> m (Snowflake a)
ping c = chunk ("<" <> c) *> optional (chunk "!") *> snowflake <* chunk ">"

ping' :: (MonadParsec e T.Text m) => m () -> m (Snowflake a)
ping' m = chunk "<" *> m *> snowflake <* chunk ">"

snowflake :: (MonadParsec e T.Text m) => m (Snowflake a)
snowflake = Snowflake <$> decimal

partialEmoji :: (MonadParsec e T.Text m) => m (Partial Emoji)
partialEmoji = do
  animated <- isJust <$> (chunk "<" *> optional (chunk "a"))
  name <- between (chunk ":") (chunk ":") (takeWhileP (Just "Emoji name") (/= ':'))
  id <- snowflake
  void $ chunk ">"
  pure (PartialEmoji id name animated)

emoji :: (MonadParsec e T.Text m) => m (Snowflake a)
emoji = ping' (optional (chunk "a") *> between (chunk ":") (chunk ":") (void $ takeWhileP Nothing (/= ':')))

-- trackOffsets :: MonadParsec e s m => m a -> m (a, Int)
-- trackOffsets m = do
--   offs <- getOffset
--   a <- m
--   offe <- getOffset
--   pure (a, offe - offs)

-- item :: MonadParsec e L.Text m => m L.Text
-- item = try quotedString <|> someNonWS

-- manySingle :: MonadParsec e s m => m (Tokens s)
-- manySingle = takeWhileP (Just "Any character") (const True)

-- someSingle :: MonadParsec e s m => m (Tokens s)
-- someSingle = takeWhile1P (Just "any character") (const True)

-- quotedString :: MonadParsec e L.Text m => m L.Text
-- quotedString = try (between (chunk "'") (chunk "'") (takeWhileP (Just "any character") (/= '\''))) <|>
--                between (chunk "\"") (chunk "\"") (takeWhileP (Just "any character") (/= '"'))

-- -- manyNonWS :: (Token s ~ Char, MonadParsec e s m) => m (Tokens s)
-- -- manyNonWS = takeWhileP (Just "Any Non-Whitespace") (not . isSpace)

-- someNonWS :: (Token s ~ Char, MonadParsec e s m) => m (Tokens s)
-- someNonWS = takeWhile1P (Just "any non-whitespace") (not . isSpace)