From db7d045b1d34dafa1e51c1a7ca6a10f8972388bf Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 12 Jan 2022 18:55:33 +0000 Subject: [PATCH 01/96] initial fix for some breaking changes --- src/Tablebot/Internal/Handler/Command.hs | 8 ++++---- src/Tablebot/Plugins/Quote.hs | 8 ++++---- src/Tablebot/Plugins/Reminder.hs | 2 +- src/Tablebot/Utility/Discord.hs | 20 ++++++++++---------- stack.yaml | 2 +- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Tablebot/Internal/Handler/Command.hs b/src/Tablebot/Internal/Handler/Command.hs index b2805188..ffe68152 100644 --- a/src/Tablebot/Internal/Handler/Command.hs +++ b/src/Tablebot/Internal/Handler/Command.hs @@ -21,7 +21,7 @@ import Data.Maybe (catMaybes) import Data.Set (singleton, toList) import Data.Text (Text) import Data.Void (Void) -import Discord.Types (Message (messageText)) +import Discord.Types (Message (messageContent)) import Tablebot.Internal.Plugins (changeAction) import Tablebot.Internal.Types import Tablebot.Utility.Discord (sendEmbedMessage) @@ -36,7 +36,7 @@ import qualified UnliftIO.Exception as UIOE (tryAny) -- to find inline commands. parseNewMessage :: PluginActions -> Text -> Message -> CompiledDatabaseDiscord () parseNewMessage pl prefix m = - if isCommandCall $ messageText m + if isCommandCall $ messageContent m then parseCommands (compiledCommands pl) m prefix else parseInlineCommands (compiledInlineCommands pl) m where @@ -58,7 +58,7 @@ parseNewMessage pl prefix m = -- If the parser errors, the last error (which is hopefully one created by -- '') is sent to the user as a Discord message. parseCommands :: [CompiledCommand] -> Message -> Text -> CompiledDatabaseDiscord () -parseCommands cs m prefix = case parse (parser cs) "" (messageText m) of +parseCommands cs m prefix = case parse (parser cs) "" (messageContent m) of Right p -> p m Left e -> let (errs, title) = makeBundleReadable e @@ -123,7 +123,7 @@ makeReadable e = (mapParseError (const UnknownError) e, Nothing) -- command's parser on the message text. Errors are not sent to the user, and do -- not halt command attempts (achieved using 'tryAny'). parseInlineCommands :: [CompiledInlineCommand] -> Message -> CompiledDatabaseDiscord () -parseInlineCommands cs m = mapM_ (fromResult . (\cic -> parse (inlineCommandParser cic) "" (messageText m))) cs +parseInlineCommands cs m = mapM_ (fromResult . (\cic -> parse (inlineCommandParser cic) "" (messageContent m))) cs where fromResult (Right p) = UIOE.tryAny (p m) fromResult _ = return $ return () diff --git a/src/Tablebot/Plugins/Quote.hs b/src/Tablebot/Plugins/Quote.hs index cc2e4cd0..47765989 100644 --- a/src/Tablebot/Plugins/Quote.hs +++ b/src/Tablebot/Plugins/Quote.hs @@ -197,7 +197,7 @@ filteredRandomQuote' quoteFilter errorMessage m = do addQ :: Text -> Text -> Message -> DatabaseDiscord () addQ qu author m = do now <- liftIO $ systemToUTCTime <$> getSystemTime - let new = Quote qu author (toMention $ messageAuthor m) (fromIntegral $ messageId m) (fromIntegral $ messageChannel m) now + let new = Quote qu author (toMention $ messageAuthor m) (fromIntegral $ messageId m) (fromIntegral $ messageChannelId m) now added <- insert new let res = pack $ show $ fromSqlKey added renderCustomQuoteMessage ("Quote added as #" `append` res) new (fromSqlKey added) m @@ -227,11 +227,11 @@ addMessageQuote submitter q' m = do now <- liftIO $ systemToUTCTime <$> getSystemTime let new = Quote - (messageText q') + (messageContent q') (toMention $ messageAuthor q') (toMention' submitter) (fromIntegral $ messageId q') - (fromIntegral $ messageChannel q') + (fromIntegral $ messageChannelId q') now added <- insert new let res = pack $ show $ fromSqlKey added @@ -251,7 +251,7 @@ editQ qId qu author m = case oQu of Just Quote {} -> do now <- liftIO $ systemToUTCTime <$> getSystemTime - let new = Quote qu author (toMention $ messageAuthor m) (fromIntegral $ messageId m) (fromIntegral $ messageChannel m) now + let new = Quote qu author (toMention $ messageAuthor m) (fromIntegral $ messageId m) (fromIntegral $ messageChannelId m) now replace k new renderCustomQuoteMessage "Quote updated" new qId m Nothing -> sendMessage m "Couldn't update that quote!" diff --git a/src/Tablebot/Plugins/Reminder.hs b/src/Tablebot/Plugins/Reminder.hs index c3729772..80e6e58d 100644 --- a/src/Tablebot/Plugins/Reminder.hs +++ b/src/Tablebot/Plugins/Reminder.hs @@ -94,7 +94,7 @@ reminderParser (WErr (Qu content, ROI rawString)) m = do -- currently ignores the user's timezone... (TODO fix) addReminder :: UTCTime -> String -> Message -> DatabaseDiscord () addReminder time content m = do - let (Snowflake cid) = messageChannel m + let (Snowflake cid) = messageChannelId m (Snowflake mid) = messageId m (Snowflake uid) = userId $ messageAuthor m added <- insert $ Reminder cid mid uid time content diff --git a/src/Tablebot/Utility/Discord.hs b/src/Tablebot/Utility/Discord.hs index 6b08fc26..0d2b3e38 100644 --- a/src/Tablebot/Utility/Discord.hs +++ b/src/Tablebot/Utility/Discord.hs @@ -67,7 +67,7 @@ sendMessage :: Text -> EnvDatabaseDiscord s () sendMessage m t = do - res <- liftDiscord . restCall $ R.CreateMessage (messageChannel m) t + res <- liftDiscord . restCall $ R.CreateMessage (messageChannelId m) t case res of Left _ -> throw $ MessageSendException "Failed to send message." Right _ -> return () @@ -94,7 +94,7 @@ sendReplyMessage :: EnvDatabaseDiscord s () sendReplyMessage m t = do let ref = MessageReference (Just (messageId m)) Nothing Nothing False - res <- liftDiscord . restCall $ R.CreateMessageDetailed (messageChannel m) (R.MessageDetailedOpts t False Nothing Nothing Nothing (Just ref)) + res <- liftDiscord . restCall $ R.CreateMessageDetailed (messageChannelId m) (R.MessageDetailedOpts t False Nothing Nothing Nothing (Just ref) Nothing Nothing) case res of Left _ -> throw $ MessageSendException "Failed to send message." Right _ -> return () @@ -111,7 +111,7 @@ sendCustomReplyMessage :: EnvDatabaseDiscord s () sendCustomReplyMessage m mid fail' t = do let ref = MessageReference (Just mid) Nothing Nothing fail' - res <- liftDiscord . restCall $ R.CreateMessageDetailed (messageChannel m) (R.MessageDetailedOpts t False Nothing Nothing Nothing (Just ref)) + res <- liftDiscord . restCall $ R.CreateMessageDetailed (messageChannelId m) (R.MessageDetailedOpts t False Nothing Nothing Nothing (Just ref) Nothing Nothing) case res of Left _ -> throw $ MessageSendException "Failed to send message." Right _ -> return () @@ -130,7 +130,7 @@ sendEmbedMessage :: e -> EnvDatabaseDiscord s () sendEmbedMessage m t e = do - res <- liftDiscord . restCall $ TablebotEmbedRequest (messageChannel m) t (asEmbed e) + res <- liftDiscord . restCall $ TablebotEmbedRequest (messageChannelId m) t (asEmbed e) case res of Left _ -> throw $ MessageSendException "Failed to send message." Right _ -> return () @@ -159,12 +159,12 @@ reactToMessage :: EnvDatabaseDiscord s (Either RestCallErrorCode ()) reactToMessage m e = liftDiscord . restCall $ - R.CreateReaction (messageChannel m, messageId m) e + R.CreateReaction (messageChannelId m, messageId m) e -- | @getReplyMessage@ returns the message being replied to (if applicable) getReplyMessage :: Message -> EnvDatabaseDiscord s (Maybe Message) getReplyMessage m = do - let m' = referencedMessage m + let m' = messageReferencedMessage m let mRef = messageReference m case m' of Just msg -> return $ Just msg @@ -183,7 +183,7 @@ getReplyMessage m = do -- | @getPrecedingMessage@ returns the message immediately above the provided message getPrecedingMessage :: Message -> EnvDatabaseDiscord s (Maybe Message) getPrecedingMessage m = do - mlst <- liftDiscord . restCall $ R.GetChannelMessages (messageChannel m) (1, R.BeforeMessage (messageId m)) + mlst <- liftDiscord . restCall $ R.GetChannelMessages (messageChannelId m) (1, R.BeforeMessage (messageId m)) case mlst of Right mlst' -> return $ listToMaybe mlst' @@ -192,7 +192,7 @@ getPrecedingMessage m = do -- | @getMessageMember@ returns the message member object if it was sent from a Discord server, -- or @Nothing@ if it was sent from a DM (or the API fails) getMessageMember :: Message -> EnvDatabaseDiscord s (Maybe GuildMember) -getMessageMember m = gMM (messageGuild m) m +getMessageMember m = gMM (messageGuildId m) m where maybeRight :: Either a b -> Maybe b maybeRight (Left _) = Nothing @@ -204,10 +204,10 @@ getMessageMember m = gMM (messageGuild m) m return $ maybeRight a findGuild :: Message -> EnvDatabaseDiscord s (Maybe GuildId) -findGuild m = case messageGuild m of +findGuild m = case messageGuildId m of Just a -> pure $ Just a Nothing -> do - let chanId = messageChannel m + let chanId = messageChannelId m channel <- getChannel chanId case fmap channelGuild channel of Right a -> pure $ Just a diff --git a/stack.yaml b/stack.yaml index 7cc38305..9a622c90 100644 --- a/stack.yaml +++ b/stack.yaml @@ -40,7 +40,7 @@ packages: # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a # extra-deps: -- discord-haskell-1.10.0 +- discord-haskell-1.11.0 - emoji-0.1.0.2 - load-env-0.2.1.0 - megaparsec-9.0.1 From 40493b17177345a7d1bfa72874df568aab2a9760 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 20 Jan 2022 19:05:23 +0000 Subject: [PATCH 02/96] renamed some vars --- src/Tablebot/Utility/Types.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tablebot/Utility/Types.hs b/src/Tablebot/Utility/Types.hs index 0409b276..635ca856 100644 --- a/src/Tablebot/Utility/Types.hs +++ b/src/Tablebot/Utility/Types.hs @@ -87,12 +87,12 @@ newtype StartUp d = StartUp -- It will first try to match against any subcommands, and if that fails it runs the commandParser data EnvCommand d = Command { -- | The name of the command. - name :: Text, + commandName :: Text, -- | A parser to run on the command arguments, returning a computation to -- run in 'DatabaseDiscord'. commandParser :: Parser (Message -> EnvDatabaseDiscord d ()), -- | A list of subcommands to attempt to parse before the bare command, matching their name. - subcommands :: [EnvCommand d] + commandSubcommands :: [EnvCommand d] } type Command = EnvCommand () From 39b41e424bedd9bee963542458c67c5f4bfba871 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 21 Jan 2022 15:32:18 +0000 Subject: [PATCH 03/96] added the basics for application commands and components, and interactions --- .env.example | 1 + src/Tablebot.hs | 15 ++++++- src/Tablebot/Handler.hs | 7 +++- src/Tablebot/Internal/Handler/Event.hs | 23 +++++++++++ src/Tablebot/Internal/Plugins.hs | 19 +++++---- src/Tablebot/Internal/Types.hs | 21 ++++++++++ src/Tablebot/Utility/Discord.hs | 19 ++++++++- src/Tablebot/Utility/Help.hs | 3 +- src/Tablebot/Utility/Types.hs | 57 ++++++++++++++++++-------- stack.yaml | 3 +- 10 files changed, 139 insertions(+), 29 deletions(-) diff --git a/.env.example b/.env.example index b83209ae..03e9a9e4 100644 --- a/.env.example +++ b/.env.example @@ -6,4 +6,5 @@ CATAPI_TOKEN=12345678-1234-1234-1234-123456789012 EXEC_GROUP=123456789123456789 MODERATOR_GROUP=321654987321654987 SUPERUSER_GROUP=147258369147258369 +SERVER_ID=314159265358979323 # NOTE: YOU MUST HAVE A NEWLINE AT THE END OF THE FILE diff --git a/src/Tablebot.hs b/src/Tablebot.hs index a1a653f8..75bae9ef 100644 --- a/src/Tablebot.hs +++ b/src/Tablebot.hs @@ -37,13 +37,18 @@ import Database.Persist.Sqlite runSqlPool, ) import Discord +import Discord.Interactions (ApplicationCommand (applicationCommandId)) +import Discord.Internal.Rest (PartialApplication (partialApplicationID)) +import System.Environment (getEnv) import Tablebot.Handler (eventHandler, killCron, runCron) import Tablebot.Internal.Administration (adminMigration, currentBlacklist, removeBlacklisted) import Tablebot.Internal.Plugins import Tablebot.Internal.Types +import Tablebot.Utility.Discord (createApplicationCommand, removeApplicationCommandsNotInList) import Tablebot.Utility.Help import Tablebot.Utility.Types (TablebotCache (..)) import Tablebot.Utility.Utils (debugPrint) +import Text.Read (readMaybe) -- | runTablebot @dToken@ @prefix@ @dbpath@ @plugins@ runs the bot using the -- given Discord API token @dToken@ and SQLite connection string @dbpath@. Only @@ -68,6 +73,7 @@ runTablebot dToken prefix dbpath plugins = let filteredPlugins = removeBlacklisted blacklist plugins -- Combine the list of plugins into both a combined plugin let !plugin = generateHelp $ combinePlugins filteredPlugins + compiledAppComms = combinedApplicationCommands plugin -- Run the setup actions of each plugin and collect the plugin actions into a single @PluginActions@ instance allActions <- mapM (runResourceT . runNoLoggingT . flip runSqlPool pool) (combinedSetupAction plugin) let !actions = combineActions allActions @@ -77,7 +83,7 @@ runTablebot dToken prefix dbpath plugins = mapM_ (\migration -> runSqlPool (runMigration migration) pool) $ combinedMigrations plugin -- Create a var to kill any ongoing tasks. mvar <- newEmptyMVar :: IO (MVar [ThreadId]) - cacheMVar <- newMVar (TCache M.empty) :: IO (MVar TablebotCache) + cacheMVar <- newMVar def :: IO (MVar TablebotCache) userFacingError <- runDiscord $ def @@ -91,6 +97,13 @@ runTablebot dToken prefix dbpath plugins = -- (which can just happen due to databases being unavailable -- sometimes). runReaderT (mapM (runCron pool) (compiledCronJobs actions) >>= liftIO . putMVar mvar) cacheMVar + + serverIdStr <- liftIO $ getEnv "SERVER_ID" + serverId <- maybe (fail "could not read server id") return (readMaybe serverIdStr) + aid <- partialApplicationID . cacheApplication <$> readCache + applicationCommands <- mapM (\(CApplicationComand pname cac) -> createApplicationCommand aid serverId cac >>= \ac -> return (applicationCommandId ac, pname)) compiledAppComms + removeApplicationCommandsNotInList aid serverId (fst <$> applicationCommands) + liftIO $ takeMVar cacheMVar >>= \tcache -> putMVar cacheMVar $ tcache {cacheApplicationCommands = M.fromList applicationCommands} liftIO $ putStrLn "Tablebot lives!", -- Kill every cron job in the mvar. discordOnEnd = takeMVar mvar >>= killCron diff --git a/src/Tablebot/Handler.hs b/src/Tablebot/Handler.hs index 8dbb004c..e36255b2 100644 --- a/src/Tablebot/Handler.hs +++ b/src/Tablebot/Handler.hs @@ -25,12 +25,14 @@ import Data.Pool (Pool) import Data.Text (Text) import Database.Persist.Sqlite (SqlBackend, runSqlPool) import Discord (DiscordHandler) +import Discord.Interactions (Interaction (..)) import Discord.Types import Tablebot.Internal.Handler.Command ( parseNewMessage, ) import Tablebot.Internal.Handler.Event - ( parseMessageChange, + ( parseInteractionRecvComponent, + parseMessageChange, parseOther, parseReactionAdd, parseReactionDel, @@ -69,6 +71,9 @@ eventHandler pl prefix = \case -- Similar with MessageReactionRemoveEmoji (removes all of one type). MessageReactionRemoveAll _cid _mid -> pure () MessageReactionRemoveEmoji _rri -> pure () + InteractionCreate i@InteractionComponent {} -> parseInteractionRecvComponent (compiledOnInteractionRecvs pl) i + InteractionCreate i@InteractionApplicationCommand {} -> parseInteractionRecvComponent (compiledOnInteractionRecvs pl) i + InteractionCreate i@InteractionApplicationCommandAutocomplete {} -> parseInteractionRecvComponent (compiledOnInteractionRecvs pl) i e -> parseOther (compiledOtherEvents pl) e where ifNotBot m = unless (userIsBot (messageAuthor m)) diff --git a/src/Tablebot/Internal/Handler/Event.hs b/src/Tablebot/Internal/Handler/Event.hs index 33cbfbe1..25c78f13 100644 --- a/src/Tablebot/Internal/Handler/Event.hs +++ b/src/Tablebot/Internal/Handler/Event.hs @@ -12,12 +12,20 @@ module Tablebot.Internal.Handler.Event ( parseMessageChange, parseReactionAdd, parseReactionDel, + parseInteractionRecvComponent, + parseInteractionRecvApplicationCommand, parseOther, ) where +import Control.Concurrent (readMVar) +import Control.Monad.RWS (MonadIO (liftIO), MonadReader (ask)) +import Data.Map (findWithDefault) +import Data.Text (isPrefixOf) +import Discord.Interactions (Interaction (..), InteractionDataComponent (interactionDataComponentCustomId)) import Discord.Types (ChannelId, Event, MessageId, ReactionInfo) import Tablebot.Internal.Types +import Tablebot.Utility.Types (TablebotCache (cacheApplicationCommands)) -- | This runs each 'MessageChange' feature in @cs@ with the information from a -- Discord 'MessageUpdate' or 'MessageDelete' event - whether it is an update @@ -47,6 +55,21 @@ parseReactionDel cs info = mapM_ doReactionAdd cs where doReactionAdd c = onReactionDelete c info +parseInteractionRecvComponent :: [CompiledInteractionRecv] -> Interaction -> CompiledDatabaseDiscord () +parseInteractionRecvComponent cs info@InteractionComponent {interactionDataComponent = Just idc} = mapM_ (`onInteractionRecv` info) cs' + where + cs' = filter (\cir -> interactionRecvPluginName cir `isPrefixOf` interactionDataComponentCustomId idc) cs +parseInteractionRecvComponent _ _ = return () + +parseInteractionRecvApplicationCommand :: [CompiledInteractionRecv] -> Interaction -> CompiledDatabaseDiscord () +parseInteractionRecvApplicationCommand cs info = do + tvar <- ask + cache <- liftIO $ readMVar tvar + let validPlugin = findWithDefault "" (interactionId info) $ cacheApplicationCommands cache + mapM_ (`onInteractionRecv` info) (cs' validPlugin) + where + cs' plname = filter (\cir -> interactionRecvPluginName cir == plname) cs + -- | This runs each 'Other' feature in @cs@ with the Discord 'Event' provided. -- Note that any events covered by other feature types will /not/ be run -- through this. diff --git a/src/Tablebot/Internal/Plugins.hs b/src/Tablebot/Internal/Plugins.hs index 88fe25e0..7d3114fd 100644 --- a/src/Tablebot/Internal/Plugins.hs +++ b/src/Tablebot/Internal/Plugins.hs @@ -10,28 +10,30 @@ module Tablebot.Internal.Plugins where import Control.Monad.Trans.Reader (runReaderT) +import Data.Default (Default (def)) import Discord.Types (Message) import Tablebot.Internal.Types hiding (helpPages, migrations) -import qualified Tablebot.Internal.Types as HT -import Tablebot.Utility.Types +import qualified Tablebot.Internal.Types as IT +import Tablebot.Utility.Types as UT -- | Combines a list of plugins into a single plugin with the combined -- functionality. The bot actually runs a single plugin, which is just the -- combined version of all input plugins. combinePlugins :: [CompiledPlugin] -> CombinedPlugin -combinePlugins [] = CmPl [] [] [] +combinePlugins [] = def combinePlugins (p : ps) = let p' = combinePlugins ps in CmPl { combinedSetupAction = setupAction p : combinedSetupAction p', - combinedMigrations = HT.migrations p ++ combinedMigrations p', - combinedHelpPages = HT.helpPages p ++ combinedHelpPages p' + combinedApplicationCommands = IT.applicationCommands p ++ combinedApplicationCommands p', + combinedMigrations = IT.migrations p ++ combinedMigrations p', + combinedHelpPages = IT.helpPages p ++ combinedHelpPages p' } -- | Combines a list of plugins actions into a single pa with the combined -- functionality. combineActions :: [PluginActions] -> PluginActions -combineActions [] = PA [] [] [] [] [] [] [] +combineActions [] = def combineActions (p : ps) = let p' = combineActions ps in PA @@ -40,6 +42,7 @@ combineActions (p : ps) = compiledOnMessageChanges = compiledOnMessageChanges p +++ compiledOnMessageChanges p', compiledOnReactionAdds = compiledOnReactionAdds p +++ compiledOnReactionAdds p', compiledOnReactionDeletes = compiledOnReactionDeletes p +++ compiledOnReactionDeletes p', + compiledOnInteractionRecvs = compiledOnInteractionRecvs p +++ compiledOnInteractionRecvs p', compiledOtherEvents = compiledOtherEvents p +++ compiledOtherEvents p', compiledCronJobs = compiledCronJobs p +++ compiledCronJobs p' } @@ -51,7 +54,7 @@ combineActions (p : ps) = a +++ b = a ++ b compilePlugin :: EnvPlugin b -> CompiledPlugin -compilePlugin p = CPl (pluginName p) sa (helpPages p) (migrations p) +compilePlugin p = CPl (pluginName p) sa (CApplicationComand (pluginName p) <$> UT.applicationCommands p) (helpPages p) (migrations p) where sa :: Database PluginActions sa = do @@ -64,6 +67,7 @@ compilePlugin p = CPl (pluginName p) sa (helpPages p) (migrations p) (map (fixOnMessageChanges state) $ onMessageChanges p) (map (fixOnReactionAdd state) $ onReactionAdds p) (map (fixOnReactionDelete state) $ onReactionDeletes p) + (map (fixOnInteractionRecv state) $ onInteractionRecvs p) (map (fixOther state) $ otherEvents p) (map (fixCron state) $ cronJobs p) @@ -73,6 +77,7 @@ compilePlugin p = CPl (pluginName p) sa (helpPages p) (migrations p) fixOnMessageChanges state' (MessageChange action') = CMessageChange (((changeAction state' .) .) . action') fixOnReactionAdd state' (ReactionAdd action') = CReactionAdd (changeAction state' . action') fixOnReactionDelete state' (ReactionDel action') = CReactionDel (changeAction state' . action') + fixOnInteractionRecv state' (InteractionRecv action') = CInteractionRecv (pluginName p) (changeAction state' . action') fixOther state' (Other action') = COther (changeAction state' . action') fixCron state' (CronJob time action') = CCronJob time (changeAction state' action') diff --git a/src/Tablebot/Internal/Types.hs b/src/Tablebot/Internal/Types.hs index c8780087..1b548aec 100644 --- a/src/Tablebot/Internal/Types.hs +++ b/src/Tablebot/Internal/Types.hs @@ -13,9 +13,11 @@ module Tablebot.Internal.Types where import Control.Concurrent.MVar (MVar) import Control.Monad.Reader (ReaderT) +import Data.Default (Default) import Data.Text (Text) import Database.Persist.Sqlite (Migration, SqlPersistT) import Discord +import Discord.Interactions (CreateApplicationCommand, Interaction) import Discord.Types import Tablebot.Utility.Types @@ -27,6 +29,7 @@ type CompiledDatabaseDiscord = ReaderT (MVar TablebotCache) (SqlPersistT Discord data CompiledPlugin = CPl { compiledName :: Text, setupAction :: Database PluginActions, + applicationCommands :: [CompiledApplicationCommand], helpPages :: [HelpPage], migrations :: [Migration] } @@ -37,20 +40,33 @@ data PluginActions = PA compiledOnMessageChanges :: [CompiledMessageChange], compiledOnReactionAdds :: [CompiledReactionAdd], compiledOnReactionDeletes :: [CompiledReactionDel], + compiledOnInteractionRecvs :: [CompiledInteractionRecv], compiledOtherEvents :: [CompiledOther], compiledCronJobs :: [CompiledCronJob] } +instance Default PluginActions where + def = PA [] [] [] [] [] [] [] [] + data CombinedPlugin = CmPl { combinedSetupAction :: [Database PluginActions], + combinedApplicationCommands :: [CompiledApplicationCommand], combinedHelpPages :: [HelpPage], combinedMigrations :: [Migration] } +instance Default CombinedPlugin where + def = CmPl [] [] [] [] + -- * Compiled Items -- These are compiled forms of the actions from the public types that remove the reader. +data CompiledApplicationCommand = CApplicationComand + { applicationCommandPluginName :: Text, + applicationCommand :: CreateApplicationCommand + } + data CompiledCommand = CCommand { commandName :: Text, commandParser :: Parser (Message -> CompiledDatabaseDiscord ()), @@ -73,6 +89,11 @@ newtype CompiledReactionDel = CReactionDel { onReactionDelete :: ReactionInfo -> CompiledDatabaseDiscord () } +data CompiledInteractionRecv = CInteractionRecv + { interactionRecvPluginName :: Text, + onInteractionRecv :: Interaction -> CompiledDatabaseDiscord () + } + newtype CompiledOther = COther { onOtherEvent :: Event -> CompiledDatabaseDiscord () } diff --git a/src/Tablebot/Utility/Discord.hs b/src/Tablebot/Utility/Discord.hs index 0d2b3e38..eb86edb3 100644 --- a/src/Tablebot/Utility/Discord.hs +++ b/src/Tablebot/Utility/Discord.hs @@ -37,21 +37,25 @@ module Tablebot.Utility.Discord formatInput, TimeFormat, extractFromSnowflake, + createApplicationCommand, + removeApplicationCommandsNotInList, ) where import Control.Monad.Exception (MonadException (throw)) import Data.Char (isDigit) import Data.Foldable (msum) +import Data.List import Data.Map.Strict (keys) import Data.Maybe (listToMaybe) import Data.String (IsString (fromString)) import Data.Text (Text, pack, unpack) import Data.Time.Clock (nominalDiffTimeToSeconds) import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) -import Discord (RestCallErrorCode, readCache, restCall) +import Discord (DiscordHandler, RestCallErrorCode, readCache, restCall) +import Discord.Interactions (ApplicationCommand (applicationCommandId), CreateApplicationCommand) import Discord.Internal.Gateway.Cache -import qualified Discord.Requests as R +import Discord.Requests qualified as R import Discord.Types import GHC.Word (Word64) import Tablebot.Internal.Cache @@ -313,3 +317,14 @@ formatText CodeBlock s = "```" <> s <> "```" extractFromSnowflake :: Snowflake -> Word64 extractFromSnowflake (Snowflake w) = w + +createApplicationCommand :: ApplicationId -> GuildId -> CreateApplicationCommand -> DiscordHandler ApplicationCommand +createApplicationCommand aid gid cac = do + a <- restCall $ R.CreateGuildApplicationCommand aid gid cac + either (const (fail "could not create guild application command")) return a + +removeApplicationCommandsNotInList :: ApplicationId -> GuildId -> [ApplicationCommandId] -> DiscordHandler () +removeApplicationCommandsNotInList aid gid aciToKeep = do + allACs' <- restCall $ R.GetGuildApplicationCommands aid gid + allACs <- (applicationCommandId <$>) <$> either (const (fail "could not get all applicationCommands")) return allACs' + mapM_ (restCall . R.DeleteGuildApplicationCommand aid gid) (allACs \\ aciToKeep) diff --git a/src/Tablebot/Utility/Help.hs b/src/Tablebot/Utility/Help.hs index 88a015ed..dba57c6f 100644 --- a/src/Tablebot/Utility/Help.hs +++ b/src/Tablebot/Utility/Help.hs @@ -9,6 +9,7 @@ -- This module creates functions and data structures to help generate help text for commands module Tablebot.Utility.Help where +import Data.Default (Default (def)) import Data.Functor (($>)) import Data.Text (Text) import qualified Data.Text as T @@ -33,7 +34,7 @@ helpHelpPage = HelpPage "help" [] "show information about commands" "**Help**\nS generateHelp :: CombinedPlugin -> CombinedPlugin generateHelp p = p - { combinedSetupAction = return (PA [CCommand "help" (handleHelp (helpHelpPage : combinedHelpPages p)) []] [] [] [] [] [] []) : combinedSetupAction p + { combinedSetupAction = return (def {compiledCommands = [CCommand "help" (handleHelp (helpHelpPage : combinedHelpPages p)) []]}) : combinedSetupAction p } handleHelp :: [HelpPage] -> Parser (Message -> CompiledDatabaseDiscord ()) diff --git a/src/Tablebot/Utility/Types.hs b/src/Tablebot/Utility/Types.hs index 635ca856..e1ff8607 100644 --- a/src/Tablebot/Utility/Types.hs +++ b/src/Tablebot/Utility/Types.hs @@ -16,14 +16,17 @@ import Control.Concurrent.MVar (MVar) import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Reader (ReaderT) import Data.Char (toLower) -import Data.Map (Map) +import Data.Default (Default (def)) +import Data.Map (Map, empty) import Data.Maybe (fromMaybe) import Data.Text (Text) import Data.Void (Void) import Database.Persist.Sqlite (Migration, SqlPersistM, SqlPersistT) import Discord (DiscordHandler) +import Discord.Interactions (CreateApplicationCommand, Interaction) import Discord.Types - ( ChannelId, + ( ApplicationCommandId, + ChannelId, Emoji, Event (..), Message, @@ -51,10 +54,14 @@ type DatabaseDiscord = EnvDatabaseDiscord () -- the just the database for startup actions. type Database d = SqlPersistM d -newtype TablebotCache = TCache - { cacheKnownEmoji :: Map Text Emoji +data TablebotCache = TCache + { cacheKnownEmoji :: Map Text Emoji, + cacheApplicationCommands :: Map ApplicationCommandId Text } +instance Default TablebotCache where + def = TCache empty empty + -- * Parser -- | A simple definition for parsers on Text. @@ -75,8 +82,8 @@ liftDiscord = lift . lift . lift -- Each feature is its own type, and the features are combined via records into -- full plugins. --- | For when you get a 'MessageCreate'. Checks that the @name@ is directly --- after the bot prefix, and then runs @commandParser@ on it. +-- | For when the plugin is first used, to ensure that certain data is +-- available. newtype StartUp d = StartUp { -- | An action to run at startup startAction :: Database d @@ -84,20 +91,23 @@ newtype StartUp d = StartUp -- | For when you get a 'MessageCreate'. Checks that the @name@ is directly -- after the bot prefix, and then runs @commandParser@ on it. --- It will first try to match against any subcommands, and if that fails it runs the commandParser +-- It will first try to match against any subcommands, and if that fails it runs +-- the commandParser. data EnvCommand d = Command { -- | The name of the command. commandName :: Text, -- | A parser to run on the command arguments, returning a computation to -- run in 'DatabaseDiscord'. commandParser :: Parser (Message -> EnvDatabaseDiscord d ()), - -- | A list of subcommands to attempt to parse before the bare command, matching their name. + -- | A list of subcommands to attempt to parse before the bare command, + -- matching their name. commandSubcommands :: [EnvCommand d] } type Command = EnvCommand () --- | Construct an aliased command that behaves the same as another command (for things like short forms) +-- | Construct an aliased command that behaves the same as another command (for +-- things like short forms). commandAlias :: Text -> EnvCommand d -> EnvCommand d commandAlias name' (Command _ cp sc) = Command name' cp sc @@ -142,7 +152,18 @@ newtype EnvReactionDel d = ReactionDel onReactionDelete :: ReactionInfo -> EnvDatabaseDiscord d () } -type ReactionDel = EnvReactionAdd () +type ReactionDel = EnvReactionDel () + +-- | Handles recieving of interactions, such as for application commands (slash +-- commands, user commands, message commands), as well as components from +-- messages. +newtype EnvInteractionRecv d = InteractionRecv + { -- | A function to call on every interaction, which takes in details of that + -- interaction + onInteractionRecv :: Interaction -> EnvDatabaseDiscord d () + } + +type InteractionRecv = EnvInteractionRecv () -- | Handles events not covered by the other kinds of features. This is only -- relevant to specific admin functionality, such as the deletion of channels. @@ -170,16 +191,18 @@ data EnvCronJob d = CronJob type CronJob = EnvCronJob () -- | A feature for generating help text --- Each help text page consists of a explanation body, as well as a list of sub-pages --- that display the short text for its page +-- Each help text page consists of a explanation body, as well as a list of +-- sub-pages that display the short text for its page data HelpPage = HelpPage { -- | The [sub]command name helpName :: Text, -- | List of aliases for this command helpAliases :: [Text], - -- | The text to show when listed in a subpage list. Will be prefixed by its helpName + -- | The text to show when listed in a subpage list. Will be prefixed by its + -- helpName helpShortText :: Text, - -- | The text to show when specifically listed. Appears above the list of subpages + -- | The text to show when specifically listed. Appears above the list of + -- subpages helpBody :: Text, -- | A list of help pages that can be recursively accessed helpSubpages :: [HelpPage], @@ -292,11 +315,13 @@ data RequiredPermission = None | Any | Exec | Moderator | Both | Superuser deriv data EnvPlugin d = Pl { pluginName :: Text, startUp :: StartUp d, + applicationCommands :: [CreateApplicationCommand], commands :: [EnvCommand d], inlineCommands :: [EnvInlineCommand d], onMessageChanges :: [EnvMessageChange d], onReactionAdds :: [EnvReactionAdd d], onReactionDeletes :: [EnvReactionDel d], + onInteractionRecvs :: [EnvInteractionRecv d], otherEvents :: [EnvOther d], cronJobs :: [EnvCronJob d], helpPages :: [HelpPage], @@ -312,7 +337,7 @@ type Plugin = EnvPlugin () -- Examples of this in use can be found in the imports of -- "Tablebot.Plugins". plug :: Text -> Plugin -plug name' = Pl name' (StartUp (return ())) [] [] [] [] [] [] [] [] [] +plug name' = Pl name' (StartUp (return ())) [] [] [] [] [] [] [] [] [] [] [] envPlug :: Text -> StartUp d -> EnvPlugin d -envPlug name' startup = Pl name' startup [] [] [] [] [] [] [] [] [] +envPlug name' startup = Pl name' startup [] [] [] [] [] [] [] [] [] [] [] diff --git a/stack.yaml b/stack.yaml index 9a622c90..26bf51ab 100644 --- a/stack.yaml +++ b/stack.yaml @@ -40,7 +40,8 @@ packages: # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a # extra-deps: -- discord-haskell-1.11.0 +- git: https://github.com/L0neGamer/discord-haskell.git + commit: 32ae2d60902b910c929bb207b5914eaba7a4e73d - emoji-0.1.0.2 - load-env-0.2.1.0 - megaparsec-9.0.1 From 263d3ba5776927dd2679c0dc1384212bdb7972cd Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 21 Jan 2022 15:32:36 +0000 Subject: [PATCH 04/96] fix formatting --- src/Tablebot/Utility/Discord.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tablebot/Utility/Discord.hs b/src/Tablebot/Utility/Discord.hs index eb86edb3..b09faf2d 100644 --- a/src/Tablebot/Utility/Discord.hs +++ b/src/Tablebot/Utility/Discord.hs @@ -55,7 +55,7 @@ import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) import Discord (DiscordHandler, RestCallErrorCode, readCache, restCall) import Discord.Interactions (ApplicationCommand (applicationCommandId), CreateApplicationCommand) import Discord.Internal.Gateway.Cache -import Discord.Requests qualified as R +import qualified Discord.Requests as R import Discord.Types import GHC.Word (Word64) import Tablebot.Internal.Cache From c91d85c0fe58653ab0990f63994899c66e0c840d Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 21 Jan 2022 15:58:54 +0000 Subject: [PATCH 05/96] adding convenient interaction options --- src/Tablebot/Utility/Discord.hs | 67 ++++++++++++++++++++++--------- src/Tablebot/Utility/Exception.hs | 2 + 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/Tablebot/Utility/Discord.hs b/src/Tablebot/Utility/Discord.hs index b09faf2d..744f3617 100644 --- a/src/Tablebot/Utility/Discord.hs +++ b/src/Tablebot/Utility/Discord.hs @@ -10,6 +10,7 @@ -- without having to lift Discord operations constantly. module Tablebot.Utility.Discord ( sendMessage, + sendCustomMessage, sendChannelMessage, sendReplyMessage, sendCustomReplyMessage, @@ -53,9 +54,9 @@ import Data.Text (Text, pack, unpack) import Data.Time.Clock (nominalDiffTimeToSeconds) import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) import Discord (DiscordHandler, RestCallErrorCode, readCache, restCall) -import Discord.Interactions (ApplicationCommand (applicationCommandId), CreateApplicationCommand) +import Discord.Interactions import Discord.Internal.Gateway.Cache -import qualified Discord.Requests as R +import Discord.Requests qualified as R import Discord.Types import GHC.Word (Word64) import Tablebot.Internal.Cache @@ -64,8 +65,7 @@ import Tablebot.Utility (EnvDatabaseDiscord, liftDiscord) import Tablebot.Utility.Exception (BotException (..)) -- | @sendMessage@ sends the input message @t@ in the same channel as message --- @m@. This returns an @Either RestCallErrorCode Message@ to denote failure or --- return the 'Message' that was just sent. +-- @m@. sendMessage :: Message -> Text -> @@ -76,9 +76,24 @@ sendMessage m t = do Left _ -> throw $ MessageSendException "Failed to send message." Right _ -> return () +-- | @sendCustomMessage@ sends the input message @mdo@ in the same channel as +-- message @m@. +-- +-- As opposed to @sendMessage@, this function takes in a MessageDetailedOpts, to +-- allow full functionality. Unless you are dealing with components or some +-- other specific message data, you shouldn't use this function. +sendCustomMessage :: + Message -> + R.MessageDetailedOpts -> + EnvDatabaseDiscord s () +sendCustomMessage m t = do + res <- liftDiscord . restCall $ R.CreateMessageDetailed (messageChannelId m) t + case res of + Left _ -> throw $ MessageSendException "Failed to send message." + Right _ -> return () + -- | @sendChannelMessage@ sends the input message @t@ into the provided channel --- @m@. This returns an @Either RestCallErrorCode Message@ to denote failure or --- return the 'Message' that was just sent. +-- @m@. sendChannelMessage :: ChannelId -> Text -> @@ -89,9 +104,8 @@ sendChannelMessage c t = do Left _ -> throw $ MessageSendException "Failed to send message." Right _ -> return () --- | @sendReplyMessage@ sends the input message @t@ as a reply to the triggering message --- @m@. This returns an @Either RestCallErrorCode Message@ to denote failure or --- return the 'Message' that was just sent. +-- | @sendReplyMessage@ sends the input message @t@ as a reply to the triggering +-- message @m@. sendReplyMessage :: Message -> Text -> @@ -103,9 +117,9 @@ sendReplyMessage m t = do Left _ -> throw $ MessageSendException "Failed to send message." Right _ -> return () --- | @sendCustomReplyMessage@ sends the input message @t@ as a reply to a provided message id --- @m@. This returns an @Either RestCallErrorCode Message@ to denote failure or --- return the 'Message' that was just sent. +-- | @sendCustomReplyMessage@ sends the input message @t@ as a reply to a +-- provided message id @m@. +-- -- @fail'@ indicates whether the message should still send if the provided message id is invalid sendCustomReplyMessage :: Message -> @@ -121,8 +135,8 @@ sendCustomReplyMessage m mid fail' t = do Right _ -> return () -- | @sendEmbedMessage@ sends the input message @t@ in the same channel as message --- @m@ with an additional full Embed. This returns an @Either RestCallErrorCode Message@ to denote failure or --- return the 'Message' that was just sent. +-- @m@ with an additional full Embed. +-- -- This is *really* janky. The library exposes *no way* to create a coloured embed through its main api, -- so I'm having to manually reimplement the sending logic just to add this in. -- If you suffer from nightmares, don't look in 'Tablebot.Handler.Embed'. Nothing good lives there. @@ -320,11 +334,28 @@ extractFromSnowflake (Snowflake w) = w createApplicationCommand :: ApplicationId -> GuildId -> CreateApplicationCommand -> DiscordHandler ApplicationCommand createApplicationCommand aid gid cac = do - a <- restCall $ R.CreateGuildApplicationCommand aid gid cac - either (const (fail "could not create guild application command")) return a + res <- restCall $ R.CreateGuildApplicationCommand aid gid cac + case res of + Left _ -> throw $ InteractionException "Failed to create application command." + Right a -> return a removeApplicationCommandsNotInList :: ApplicationId -> GuildId -> [ApplicationCommandId] -> DiscordHandler () removeApplicationCommandsNotInList aid gid aciToKeep = do allACs' <- restCall $ R.GetGuildApplicationCommands aid gid - allACs <- (applicationCommandId <$>) <$> either (const (fail "could not get all applicationCommands")) return allACs' - mapM_ (restCall . R.DeleteGuildApplicationCommand aid gid) (allACs \\ aciToKeep) + case allACs' of + Left _ -> throw $ InteractionException "Failed to defer get all application commands." + Right aacs -> + let allACs = applicationCommandId <$> aacs + in mapM_ (restCall . R.DeleteGuildApplicationCommand aid gid) (allACs \\ aciToKeep) + +interactionResponseDefer :: Interaction -> EnvDatabaseDiscord s () +interactionResponseDefer i = do + res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponse (typ i) Nothing) + case res of + Left _ -> throw $ InteractionException "Failed to defer interaction." + Right _ -> return () + where + typ InteractionComponent {} = InteractionCallbackTypeDeferredUpdateMessage + typ _ = InteractionCallbackTypeDeferredChannelMessageWithSource + +interactionResponseMessage :: Interaction -> diff --git a/src/Tablebot/Utility/Exception.hs b/src/Tablebot/Utility/Exception.hs index 2c4549f3..06470f8d 100644 --- a/src/Tablebot/Utility/Exception.hs +++ b/src/Tablebot/Utility/Exception.hs @@ -37,6 +37,7 @@ data BotException | EvaluationException String [String] | IOException String | NetrunnerException String + | InteractionException String deriving (Show, Eq) instance Exception BotException @@ -126,3 +127,4 @@ errorInfo (EvaluationException msg' locs) = ErrorInfo "EvaluationException" $ ms else connectVs (reverse locs) errorInfo (IOException msg') = ErrorInfo "IOException" msg' errorInfo (NetrunnerException msg') = ErrorInfo "NetrunnerException" msg' +errorInfo (InteractionException msg') = ErrorInfo "InteractionException" msg' From c8d7f58ac855370241801e52c587c9cdf23e8110 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 21 Jan 2022 15:59:11 +0000 Subject: [PATCH 06/96] formatting --- src/Tablebot/Utility/Discord.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tablebot/Utility/Discord.hs b/src/Tablebot/Utility/Discord.hs index 744f3617..f7242f95 100644 --- a/src/Tablebot/Utility/Discord.hs +++ b/src/Tablebot/Utility/Discord.hs @@ -56,7 +56,7 @@ import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) import Discord (DiscordHandler, RestCallErrorCode, readCache, restCall) import Discord.Interactions import Discord.Internal.Gateway.Cache -import Discord.Requests qualified as R +import qualified Discord.Requests as R import Discord.Types import GHC.Word (Word64) import Tablebot.Internal.Cache From 8b5d7853ea95b76e62d60d7190cbe23e872ee63c Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 21 Jan 2022 19:05:11 +0000 Subject: [PATCH 07/96] trying to get a working example in Roll --- src/Tablebot.hs | 3 +- src/Tablebot/Plugins/Quote.hs | 6 +- src/Tablebot/Plugins/Roll/Plugin.hs | 86 ++++++++++++++++++++++++---- src/Tablebot/Utility/Discord.hs | 89 ++++++++++++++++++++++++----- src/Tablebot/Utility/SmartParser.hs | 2 +- stack.yaml | 2 +- 6 files changed, 158 insertions(+), 30 deletions(-) diff --git a/src/Tablebot.hs b/src/Tablebot.hs index 75bae9ef..ea8ddf27 100644 --- a/src/Tablebot.hs +++ b/src/Tablebot.hs @@ -36,6 +36,7 @@ import Database.Persist.Sqlite runMigration, runSqlPool, ) +import Debug.Trace (trace) import Discord import Discord.Interactions (ApplicationCommand (applicationCommandId)) import Discord.Internal.Rest (PartialApplication (partialApplicationID)) @@ -101,7 +102,7 @@ runTablebot dToken prefix dbpath plugins = serverIdStr <- liftIO $ getEnv "SERVER_ID" serverId <- maybe (fail "could not read server id") return (readMaybe serverIdStr) aid <- partialApplicationID . cacheApplication <$> readCache - applicationCommands <- mapM (\(CApplicationComand pname cac) -> createApplicationCommand aid serverId cac >>= \ac -> return (applicationCommandId ac, pname)) compiledAppComms + applicationCommands <- trace ("doing app comms" <> show aid) mapM (\(CApplicationComand pname cac) -> createApplicationCommand aid serverId cac >>= \ac -> return (applicationCommandId ac, pname)) compiledAppComms removeApplicationCommandsNotInList aid serverId (fst <$> applicationCommands) liftIO $ takeMVar cacheMVar >>= \tcache -> putMVar cacheMVar $ tcache {cacheApplicationCommands = M.fromList applicationCommands} liftIO $ putStrLn "Tablebot lives!", diff --git a/src/Tablebot/Plugins/Quote.hs b/src/Tablebot/Plugins/Quote.hs index 47765989..a354466e 100644 --- a/src/Tablebot/Plugins/Quote.hs +++ b/src/Tablebot/Plugins/Quote.hs @@ -71,8 +71,8 @@ quoteReactionAdd = ReactionAdd quoteReaction | otherwise = return () -- | Our quote command, which combines various functions to create, display and update quotes. -quote :: Command -quote = +quoteCommand :: Command +quoteCommand = Command "quote" (parseComm quoteComm) @@ -386,7 +386,7 @@ Calling without arguments returns a random quote. Calling with a number returns quotePlugin :: Plugin quotePlugin = (plug "quote") - { commands = [quote, commandAlias "q" quote], + { commands = [quoteCommand, commandAlias "q" quoteCommand], onReactionAdds = [quoteReactionAdd], migrations = [quoteMigration], helpPages = [quoteHelp] diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 3bc84493..993dfc4a 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -11,34 +11,38 @@ module Tablebot.Plugins.Roll.Plugin (rollPlugin) where import Control.Monad.Writer (MonadIO (liftIO)) import Data.Bifunctor (Bifunctor (first)) +import Data.Default (Default (def)) import Data.Maybe (fromMaybe) import Data.Text (Text, intercalate, pack, replicate, unpack) import qualified Data.Text as T -import Discord.Types (Message (messageAuthor)) +import Discord.Interactions +import Discord.Requests +import Discord.Types (ButtonStyle (ButtonStyleSecondary), ComponentActionRow (ComponentActionRowButton), ComponentButton (ComponentButton), Message (messageAuthor)) import Tablebot.Plugins.Roll.Dice import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Utility -import Tablebot.Utility.Discord (sendMessage, toMention) +import Tablebot.Utility.Discord (interactionResponseComponentsUpdateMessage, sendCustomMessage, toMention) +import Tablebot.Utility.Exception (BotException (InteractionException), throwBot) import Tablebot.Utility.Parser (inlineCommandHelper) -import Tablebot.Utility.SmartParser (PComm (parseComm), Quoted (Qu), pars) -import Text.Megaparsec (MonadParsec (try), choice, ()) +import Tablebot.Utility.SmartParser (PComm (parseComm), Quoted (Qu, quote), pars) +import Text.Megaparsec (MonadParsec (try), choice, parse, ()) import Text.RawString.QQ (r) -- | The basic execution function for rolling dice. Both the expression and message are -- optional. If the expression is not given, then the default roll is used. -rollDice' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> Message -> DatabaseDiscord () -rollDice' e' t m = do +rollDice'' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> Text -> DatabaseDiscord Text +rollDice'' e' t u = do let e = fromMaybe (Right defaultRoll) e' (vs, ss) <- case e of (Left a) -> liftIO $ first Left <$> evalList a (Right b) -> liftIO $ first Right <$> evalInteger b let msg = makeMsg vs ss if countFormatting msg < 199 - then sendMessage m msg - else sendMessage m (makeMsg (simplify vs) (prettyShow e <> " `[could not display rolls]`")) + then return msg + else return (makeMsg (simplify vs) (prettyShow e <> " `[could not display rolls]`")) where dsc = maybe ": " (\(Qu t') -> " \"" <> t' <> "\": ") t - baseMsg = toMention (messageAuthor m) <> " rolled" <> dsc + baseMsg = u <> " rolled" <> dsc makeLine (i, s) = pack (show i) <> Data.Text.replicate (max 0 (6 - length (show i))) " " <> " ⟵ " <> s makeMsg (Right v) s = baseMsg <> s <> ".\nOutput: " <> pack (show v) makeMsg (Left []) _ = baseMsg <> "No output." @@ -49,6 +53,67 @@ rollDice' e' t m = do simplify li = li countFormatting s = (`div` 4) $ T.foldr (\c cf -> cf + (2 * fromEnum (c == '`')) + fromEnum (c `elem` ['~', '_', '*'])) 0 s +rollDice' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> Message -> DatabaseDiscord () +rollDice' e t m = do + (msg, buttons) <- getMessagePieces e t (toMention $ messageAuthor m) + sendCustomMessage m (def {messageDetailedContent = msg, messageDetailedComponents = buttons}) + +-- sendCustomMessage +-- m +-- ( def +-- { messageDetailedContent = msg, +-- messageDetailedComponents = Just [ComponentActionRowButton [ +-- ComponentButton (("roll" `appendIf` (prettyShow <$> e)) `appendIf` (quote <$> t)) False ButtonStyleSecondary "Reroll" Nothing +-- ]] +-- } +-- ) +-- where +-- appendIf t' Nothing = t' +-- appendIf t' (Just e') = t' <> "`" <> e' + +getMessagePieces :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> Text -> DatabaseDiscord (Text, Maybe [ComponentActionRow]) +getMessagePieces e t u = do + msg <- rollDice'' e t u + return + ( msg, + Just + [ ComponentActionRowButton + [ ComponentButton ((("roll`" <> u) `appendIf` (prettyShow <$> e)) `appendIf` (quote <$> t)) False ButtonStyleSecondary "Reroll" Nothing + ] + ] + ) + where + appendIf t' Nothing = t' <> "`" + appendIf t' (Just e') = t' <> "`" <> e' + +rollInteraction :: Interaction -> DatabaseDiscord () +rollInteraction i@InteractionComponent {interactionDataComponent = Just (InteractionDataComponentButton cid)} = + case opts of + [_, uid, "", ""] -> do + (msg, button) <- getMessagePieces Nothing Nothing uid + interactionResponseComponentsUpdateMessage i ((interactionCallbackMessagesBasic msg) {interactionCallbackMessagesComponents = button}) + [_, uid, "", qt] -> do + (msg, button) <- getMessagePieces Nothing (Just (Qu qt)) uid + interactionResponseComponentsUpdateMessage i ((interactionCallbackMessagesBasic msg) {interactionCallbackMessagesComponents = button}) + [_, uid, e, ""] -> do + let e' = parse pars "" e + case e' of + Left _ -> throwBot $ InteractionException "could not process button click" + Right e'' -> do + (msg, button) <- getMessagePieces (Just e'') Nothing uid + interactionResponseComponentsUpdateMessage i ((interactionCallbackMessagesBasic msg) {interactionCallbackMessagesComponents = button}) + [_, uid, e, qt] -> do + let e' = parse pars "" e + case e' of + Left _ -> throwBot $ InteractionException "could not process button click" + Right e'' -> do + (msg, button) <- getMessagePieces (Just e'') (Just (Qu qt)) uid + interactionResponseComponentsUpdateMessage i ((interactionCallbackMessagesBasic msg) {interactionCallbackMessagesComponents = button}) + _ -> throwBot $ InteractionException "could not process button click" + where + opts = T.split (== '`') cid +rollInteraction _ = return () + -- | Manually creating parser for this command, since SmartCommand doesn't work fully for -- multiple Maybe values rollDiceParser :: Parser (Message -> DatabaseDiscord ()) @@ -136,5 +201,6 @@ rollPlugin = (plug "roll") { commands = [rollDice, commandAlias "r" rollDice, genchar], helpPages = [rollHelp, gencharHelp], - inlineCommands = [rollDiceInline] + inlineCommands = [rollDiceInline], + onInteractionRecvs = [InteractionRecv rollInteraction] } diff --git a/src/Tablebot/Utility/Discord.hs b/src/Tablebot/Utility/Discord.hs index f7242f95..91a4de4e 100644 --- a/src/Tablebot/Utility/Discord.hs +++ b/src/Tablebot/Utility/Discord.hs @@ -40,6 +40,11 @@ module Tablebot.Utility.Discord extractFromSnowflake, createApplicationCommand, removeApplicationCommandsNotInList, + interactionResponseDefer, + interactionResponseDeferUpdateMessage, + interactionResponseMessage, + interactionResponseComponentsUpdateMessage, + interactionResponseAutocomplete, ) where @@ -53,6 +58,7 @@ import Data.String (IsString (fromString)) import Data.Text (Text, pack, unpack) import Data.Time.Clock (nominalDiffTimeToSeconds) import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) +import Debug.Trace (trace) import Discord (DiscordHandler, RestCallErrorCode, readCache, restCall) import Discord.Interactions import Discord.Internal.Gateway.Cache @@ -76,11 +82,11 @@ sendMessage m t = do Left _ -> throw $ MessageSendException "Failed to send message." Right _ -> return () --- | @sendCustomMessage@ sends the input message @mdo@ in the same channel as --- message @m@. +-- | @sendCustomMessage@ sends the input message @mdo@ in the same channel as +-- message @m@. -- -- As opposed to @sendMessage@, this function takes in a MessageDetailedOpts, to --- allow full functionality. Unless you are dealing with components or some +-- allow full functionality. Unless you are dealing with components or some -- other specific message data, you shouldn't use this function. sendCustomMessage :: Message -> @@ -105,7 +111,7 @@ sendChannelMessage c t = do Right _ -> return () -- | @sendReplyMessage@ sends the input message @t@ as a reply to the triggering --- message @m@. +-- message @m@. sendReplyMessage :: Message -> Text -> @@ -117,8 +123,8 @@ sendReplyMessage m t = do Left _ -> throw $ MessageSendException "Failed to send message." Right _ -> return () --- | @sendCustomReplyMessage@ sends the input message @t@ as a reply to a --- provided message id @m@. +-- | @sendCustomReplyMessage@ sends the input message @t@ as a reply to a +-- provided message id @m@. -- -- @fail'@ indicates whether the message should still send if the provided message id is invalid sendCustomReplyMessage :: @@ -135,7 +141,7 @@ sendCustomReplyMessage m mid fail' t = do Right _ -> return () -- | @sendEmbedMessage@ sends the input message @t@ in the same channel as message --- @m@ with an additional full Embed. +-- @m@ with an additional full Embed. -- -- This is *really* janky. The library exposes *no way* to create a coloured embed through its main api, -- so I'm having to manually reimplement the sending logic just to add this in. @@ -281,6 +287,7 @@ toMention' u = "<@!" <> pack (show u) <> ">" fromMention :: Text -> Maybe UserId fromMention = fromMentionStr . unpack +-- | Try to get the userid from a given string. fromMentionStr :: String -> Maybe UserId fromMentionStr user | length user < 4 || head user /= '<' || last user /= '>' || (head . tail) user /= '@' || (head stripToNum /= '!' && (not . isDigit) (head stripToNum)) = Nothing @@ -289,8 +296,10 @@ fromMentionStr user where stripToNum = (init . tail . tail) user +-- | Data types for different time formats. data TimeFormat = Default | ShortTime | LongTime | ShortDate | LongDate | ShortDateTime | LongDateTime | Relative deriving (Show, Enum, Eq) +-- | Turn some UTCTime into the given TimeFormat. toTimestamp' :: TimeFormat -> UTCTime -> Text toTimestamp' format t = " pack (show $ toUtcSeconds t) <> toSuffix format <> ">" where @@ -306,21 +315,32 @@ toTimestamp' format t = " pack (show $ toUtcSeconds t) <> toSuffix format toSuffix LongDateTime = ":F" toSuffix Relative = ":R" +-- | Turn some UTCTime into the default time format. toTimestamp :: UTCTime -> Text toTimestamp = toTimestamp' Default +-- | Turn some UTCTime into a relative time format toRelativeTime :: UTCTime -> Text toRelativeTime = toTimestamp' Relative +-- | Create a link to a message when given the server id, channel id, and +-- message id. getMessageLink :: GuildId -> ChannelId -> MessageId -> Text getMessageLink g c m = pack $ "https://discord.com/channels/" ++ show g ++ "/" ++ show c ++ "/" ++ show m +-- | The data types of different formatting options. +-- +-- Note that repeatedly applying certain formatting options (such as `Italics`, +-- `Code`, and a few others) will result in other formats. data Format = Bold | Underline | Strikethrough | Italics | Code | CodeBlock deriving (Show, Eq) +-- | Format some `a` (that can be turned into a string format) with the given +-- formatting option. formatInput :: (IsString a, Show b, Semigroup a) => Format -> b -> a formatInput f b = formatText f (fromString $ show b) +-- | Format the given string-like object with the given format. formatText :: (IsString a, Semigroup a) => Format -> a -> a formatText Bold s = "**" <> s <> "**" formatText Underline s = "__" <> s <> "__" @@ -329,9 +349,12 @@ formatText Italics s = "*" <> s <> "*" formatText Code s = "`" <> s <> "`" formatText CodeBlock s = "```" <> s <> "```" +-- | Get the `Word64` within a `Snowflake`. extractFromSnowflake :: Snowflake -> Word64 extractFromSnowflake (Snowflake w) = w +-- | When given an application id, server id, and a CreateApplicationCommand +-- object, create the application command. createApplicationCommand :: ApplicationId -> GuildId -> CreateApplicationCommand -> DiscordHandler ApplicationCommand createApplicationCommand aid gid cac = do res <- restCall $ R.CreateGuildApplicationCommand aid gid cac @@ -339,23 +362,61 @@ createApplicationCommand aid gid cac = do Left _ -> throw $ InteractionException "Failed to create application command." Right a -> return a +-- | Remove all application commands that are active in the given server that +-- aren't in the given list. removeApplicationCommandsNotInList :: ApplicationId -> GuildId -> [ApplicationCommandId] -> DiscordHandler () removeApplicationCommandsNotInList aid gid aciToKeep = do allACs' <- restCall $ R.GetGuildApplicationCommands aid gid case allACs' of - Left _ -> throw $ InteractionException "Failed to defer get all application commands." + Left _ -> throw $ InteractionException "Failed to get all application commands." Right aacs -> - let allACs = applicationCommandId <$> aacs + let allACs = trace (show aacs) applicationCommandId <$> aacs in mapM_ (restCall . R.DeleteGuildApplicationCommand aid gid) (allACs \\ aciToKeep) +-- | Defer an interaction response, extending the window of time to respond to +-- 15 minutes (from 3 seconds). interactionResponseDefer :: Interaction -> EnvDatabaseDiscord s () interactionResponseDefer i = do - res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponse (typ i) Nothing) + res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponse InteractionCallbackTypeDeferredChannelMessageWithSource Nothing) case res of Left _ -> throw $ InteractionException "Failed to defer interaction." Right _ -> return () - where - typ InteractionComponent {} = InteractionCallbackTypeDeferredUpdateMessage - typ _ = InteractionCallbackTypeDeferredChannelMessageWithSource -interactionResponseMessage :: Interaction -> +-- | Defer an interaction response, extending the window of time to respond to +-- 15 minutes (from 3 seconds). +-- +-- Used when updating a component message. +interactionResponseDeferUpdateMessage :: Interaction -> EnvDatabaseDiscord s () +interactionResponseDeferUpdateMessage i = do + res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponse InteractionCallbackTypeDeferredUpdateMessage Nothing) + case res of + Left _ -> throw $ InteractionException "Failed to defer interaction." + Right _ -> return () + +-- | Respond to the given interaction with the given text. +interactionResponseMessage :: Interaction -> Text -> EnvDatabaseDiscord s () +interactionResponseMessage i t = interactionResponseCustomMessage i (InteractionCallbackMessages Nothing (Just t) Nothing Nothing Nothing Nothing Nothing) + +-- | Respond to the given interaction with a custom messages object. +interactionResponseCustomMessage :: Interaction -> InteractionCallbackMessages -> EnvDatabaseDiscord s () +interactionResponseCustomMessage i t = do + res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponse InteractionCallbackTypeChannelMessageWithSource (Just $ InteractionCallbackDataMessages t)) + case res of + Left _ -> throw $ InteractionException "Failed to respond to interaction." + Right _ -> return () + +-- | Respond to the given interaction by updating the component's message. +interactionResponseComponentsUpdateMessage :: Interaction -> InteractionCallbackMessages -> EnvDatabaseDiscord s () +interactionResponseComponentsUpdateMessage i t = do + res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponse InteractionCallbackTypeUpdateMessage (Just $ InteractionCallbackDataMessages t)) + case res of + Left _ -> throw $ InteractionException "Failed to respond to interaction with components update." + Right _ -> return () + +-- | Respond to the given interaction by sending a list of choices back. +interactionResponseAutocomplete :: Interaction -> InteractionCallbackAutocomplete -> EnvDatabaseDiscord s () +interactionResponseAutocomplete i ac = do + res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponse InteractionCallbackTypeApplicationCommandAutocompleteResult (Just $ InteractionCallbackDataAutocomplete ac)) + case res of + Left _ -> throw $ InteractionException "Failed to respond to interaction with autocomplete response." + Right _ -> return () diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index a7104deb..0982a110 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -65,7 +65,7 @@ instance {-# OVERLAPPING #-} CanParse String where pars = word -- | @Quoted a@ defines an input of type @a@ that is contained within quotes. -newtype Quoted a = Qu a deriving (Show) +newtype Quoted a = Qu {quote :: a} deriving (Show) instance IsString a => CanParse (Quoted a) where pars = Qu . fromString <$> quoted diff --git a/stack.yaml b/stack.yaml index 26bf51ab..1189d791 100644 --- a/stack.yaml +++ b/stack.yaml @@ -41,7 +41,7 @@ packages: # extra-deps: - git: https://github.com/L0neGamer/discord-haskell.git - commit: 32ae2d60902b910c929bb207b5914eaba7a4e73d + commit: e5e0ba8c8f54117f6f53c6c0c6d4796d3915ada9 - emoji-0.1.0.2 - load-env-0.2.1.0 - megaparsec-9.0.1 From f54346bd8edc779556aa447f48bb5b786b1a4fc8 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 22 Jan 2022 00:48:26 +0000 Subject: [PATCH 08/96] cleaning up and pinning a new discord-haskell version (from my repo) --- package.yaml | 1 - src/Tablebot.hs | 3 +-- src/Tablebot/Utility/Discord.hs | 4 ++-- stack.yaml | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/package.yaml b/package.yaml index 012e76fe..2fd4a403 100644 --- a/package.yaml +++ b/package.yaml @@ -64,7 +64,6 @@ library: source-dirs: src default-extensions: - OverloadedStrings - - ImportQualifiedPost - LambdaCase - EmptyDataDecls - FlexibleContexts diff --git a/src/Tablebot.hs b/src/Tablebot.hs index ea8ddf27..75bae9ef 100644 --- a/src/Tablebot.hs +++ b/src/Tablebot.hs @@ -36,7 +36,6 @@ import Database.Persist.Sqlite runMigration, runSqlPool, ) -import Debug.Trace (trace) import Discord import Discord.Interactions (ApplicationCommand (applicationCommandId)) import Discord.Internal.Rest (PartialApplication (partialApplicationID)) @@ -102,7 +101,7 @@ runTablebot dToken prefix dbpath plugins = serverIdStr <- liftIO $ getEnv "SERVER_ID" serverId <- maybe (fail "could not read server id") return (readMaybe serverIdStr) aid <- partialApplicationID . cacheApplication <$> readCache - applicationCommands <- trace ("doing app comms" <> show aid) mapM (\(CApplicationComand pname cac) -> createApplicationCommand aid serverId cac >>= \ac -> return (applicationCommandId ac, pname)) compiledAppComms + applicationCommands <- mapM (\(CApplicationComand pname cac) -> createApplicationCommand aid serverId cac >>= \ac -> return (applicationCommandId ac, pname)) compiledAppComms removeApplicationCommandsNotInList aid serverId (fst <$> applicationCommands) liftIO $ takeMVar cacheMVar >>= \tcache -> putMVar cacheMVar $ tcache {cacheApplicationCommands = M.fromList applicationCommands} liftIO $ putStrLn "Tablebot lives!", diff --git a/src/Tablebot/Utility/Discord.hs b/src/Tablebot/Utility/Discord.hs index 91a4de4e..d06bb6e3 100644 --- a/src/Tablebot/Utility/Discord.hs +++ b/src/Tablebot/Utility/Discord.hs @@ -43,6 +43,7 @@ module Tablebot.Utility.Discord interactionResponseDefer, interactionResponseDeferUpdateMessage, interactionResponseMessage, + interactionResponseCustomMessage, interactionResponseComponentsUpdateMessage, interactionResponseAutocomplete, ) @@ -58,7 +59,6 @@ import Data.String (IsString (fromString)) import Data.Text (Text, pack, unpack) import Data.Time.Clock (nominalDiffTimeToSeconds) import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) -import Debug.Trace (trace) import Discord (DiscordHandler, RestCallErrorCode, readCache, restCall) import Discord.Interactions import Discord.Internal.Gateway.Cache @@ -370,7 +370,7 @@ removeApplicationCommandsNotInList aid gid aciToKeep = do case allACs' of Left _ -> throw $ InteractionException "Failed to get all application commands." Right aacs -> - let allACs = trace (show aacs) applicationCommandId <$> aacs + let allACs = applicationCommandId <$> aacs in mapM_ (restCall . R.DeleteGuildApplicationCommand aid gid) (allACs \\ aciToKeep) -- | Defer an interaction response, extending the window of time to respond to diff --git a/stack.yaml b/stack.yaml index 1189d791..561fa921 100644 --- a/stack.yaml +++ b/stack.yaml @@ -41,7 +41,7 @@ packages: # extra-deps: - git: https://github.com/L0neGamer/discord-haskell.git - commit: e5e0ba8c8f54117f6f53c6c0c6d4796d3915ada9 + commit: da2fde90f29e8540ab3c084f86c2f1f46dc25f9c - emoji-0.1.0.2 - load-env-0.2.1.0 - megaparsec-9.0.1 From d143af6834e99df4dd002f2758aca728c0656b64 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 22 Jan 2022 00:51:02 +0000 Subject: [PATCH 09/96] working example of reroll button on dice rolling --- src/Tablebot/Plugins/Roll/Plugin.hs | 35 ++++++++++++++--------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 993dfc4a..bd912578 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -17,11 +17,20 @@ import Data.Text (Text, intercalate, pack, replicate, unpack) import qualified Data.Text as T import Discord.Interactions import Discord.Requests -import Discord.Types (ButtonStyle (ButtonStyleSecondary), ComponentActionRow (ComponentActionRowButton), ComponentButton (ComponentButton), Message (messageAuthor)) + ( MessageDetailedOpts (messageDetailedComponents, messageDetailedContent), + ) +import Discord.Types + ( ButtonStyle (ButtonStyleSecondary), + ComponentActionRow (ComponentActionRowButton), + ComponentButton (ComponentButton), + Emoji (Emoji), + GuildMember (memberUser), + Message (messageAuthor), + ) import Tablebot.Plugins.Roll.Dice import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Utility -import Tablebot.Utility.Discord (interactionResponseComponentsUpdateMessage, sendCustomMessage, toMention) +import Tablebot.Utility.Discord (interactionResponseComponentsUpdateMessage, interactionResponseCustomMessage, sendCustomMessage, toMention) import Tablebot.Utility.Exception (BotException (InteractionException), throwBot) import Tablebot.Utility.Parser (inlineCommandHelper) import Tablebot.Utility.SmartParser (PComm (parseComm), Quoted (Qu, quote), pars) @@ -58,19 +67,6 @@ rollDice' e t m = do (msg, buttons) <- getMessagePieces e t (toMention $ messageAuthor m) sendCustomMessage m (def {messageDetailedContent = msg, messageDetailedComponents = buttons}) --- sendCustomMessage --- m --- ( def --- { messageDetailedContent = msg, --- messageDetailedComponents = Just [ComponentActionRowButton [ --- ComponentButton (("roll" `appendIf` (prettyShow <$> e)) `appendIf` (quote <$> t)) False ButtonStyleSecondary "Reroll" Nothing --- ]] --- } --- ) --- where --- appendIf t' Nothing = t' --- appendIf t' (Just e') = t' <> "`" <> e' - getMessagePieces :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> Text -> DatabaseDiscord (Text, Maybe [ComponentActionRow]) getMessagePieces e t u = do msg <- rollDice'' e t u @@ -78,7 +74,7 @@ getMessagePieces e t u = do ( msg, Just [ ComponentActionRowButton - [ ComponentButton ((("roll`" <> u) `appendIf` (prettyShow <$> e)) `appendIf` (quote <$> t)) False ButtonStyleSecondary "Reroll" Nothing + [ ComponentButton ((("roll`" <> u) `appendIf` (prettyShow <$> e)) `appendIf` (quote <$> t)) False ButtonStyleSecondary "Reroll" (Just (Emoji (Just 0) "🎲" Nothing Nothing Nothing (Just False))) ] ] ) @@ -87,8 +83,10 @@ getMessagePieces e t u = do appendIf t' (Just e') = t' <> "`" <> e' rollInteraction :: Interaction -> DatabaseDiscord () -rollInteraction i@InteractionComponent {interactionDataComponent = Just (InteractionDataComponentButton cid)} = - case opts of +rollInteraction i@InteractionComponent {interactionDataComponent = Just (InteractionDataComponentButton cid)} + | length opts /= 4 = throwBot $ InteractionException "could not process button click" + | maybe True (\u -> toMention u /= opts !! 1) getUser = interactionResponseCustomMessage i ((interactionCallbackMessagesBasic "Hey, that isn't your button to press!") {interactionCallbackMessagesFlags = Just $ InteractionCallbackDataFlags [InteractionCallbackDataFlagEphermeral]}) + | otherwise = case opts of [_, uid, "", ""] -> do (msg, button) <- getMessagePieces Nothing Nothing uid interactionResponseComponentsUpdateMessage i ((interactionCallbackMessagesBasic msg) {interactionCallbackMessagesComponents = button}) @@ -112,6 +110,7 @@ rollInteraction i@InteractionComponent {interactionDataComponent = Just (Interac _ -> throwBot $ InteractionException "could not process button click" where opts = T.split (== '`') cid + getUser = maybe (interactionUser i) memberUser (interactionMember i) rollInteraction _ = return () -- | Manually creating parser for this command, since SmartCommand doesn't work fully for From f3d28a91ada12e195e993bdd4c60ff444d2b8a87 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 22 Jan 2022 02:32:59 +0000 Subject: [PATCH 10/96] added working slash commands example. raised a couple of things I'd like to change. needs commenting. --- src/Tablebot/Handler.hs | 7 +-- src/Tablebot/Internal/Handler/Command.hs | 1 + src/Tablebot/Internal/Handler/Event.hs | 7 +-- src/Tablebot/Internal/Plugins.hs | 3 +- src/Tablebot/Plugins/Roll/Plugin.hs | 56 +++++++++++++++++++++--- src/Tablebot/Utility/Types.hs | 3 +- 6 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/Tablebot/Handler.hs b/src/Tablebot/Handler.hs index e36255b2..28fb86b2 100644 --- a/src/Tablebot/Handler.hs +++ b/src/Tablebot/Handler.hs @@ -31,7 +31,8 @@ import Tablebot.Internal.Handler.Command ( parseNewMessage, ) import Tablebot.Internal.Handler.Event - ( parseInteractionRecvComponent, + ( parseInteractionRecvApplicationCommand, + parseInteractionRecvComponent, parseMessageChange, parseOther, parseReactionAdd, @@ -72,8 +73,8 @@ eventHandler pl prefix = \case MessageReactionRemoveAll _cid _mid -> pure () MessageReactionRemoveEmoji _rri -> pure () InteractionCreate i@InteractionComponent {} -> parseInteractionRecvComponent (compiledOnInteractionRecvs pl) i - InteractionCreate i@InteractionApplicationCommand {} -> parseInteractionRecvComponent (compiledOnInteractionRecvs pl) i - InteractionCreate i@InteractionApplicationCommandAutocomplete {} -> parseInteractionRecvComponent (compiledOnInteractionRecvs pl) i + InteractionCreate i@InteractionApplicationCommand {} -> parseInteractionRecvApplicationCommand (compiledOnInteractionRecvs pl) i + -- TODO: add application command autocomplete as an option e -> parseOther (compiledOtherEvents pl) e where ifNotBot m = unless (userIsBot (messageAuthor m)) diff --git a/src/Tablebot/Internal/Handler/Command.hs b/src/Tablebot/Internal/Handler/Command.hs index ffe68152..89ef72ad 100644 --- a/src/Tablebot/Internal/Handler/Command.hs +++ b/src/Tablebot/Internal/Handler/Command.hs @@ -13,6 +13,7 @@ module Tablebot.Internal.Handler.Command ( parseNewMessage, parseCommands, parseInlineCommands, + makeBundleReadable, ) where diff --git a/src/Tablebot/Internal/Handler/Event.hs b/src/Tablebot/Internal/Handler/Event.hs index 25c78f13..813fcc76 100644 --- a/src/Tablebot/Internal/Handler/Event.hs +++ b/src/Tablebot/Internal/Handler/Event.hs @@ -22,7 +22,7 @@ import Control.Concurrent (readMVar) import Control.Monad.RWS (MonadIO (liftIO), MonadReader (ask)) import Data.Map (findWithDefault) import Data.Text (isPrefixOf) -import Discord.Interactions (Interaction (..), InteractionDataComponent (interactionDataComponentCustomId)) +import Discord.Interactions (Interaction (..), InteractionDataApplicationCommand (interactionDataApplicationCommandId), InteractionDataComponent (interactionDataComponentCustomId)) import Discord.Types (ChannelId, Event, MessageId, ReactionInfo) import Tablebot.Internal.Types import Tablebot.Utility.Types (TablebotCache (cacheApplicationCommands)) @@ -62,13 +62,14 @@ parseInteractionRecvComponent cs info@InteractionComponent {interactionDataCompo parseInteractionRecvComponent _ _ = return () parseInteractionRecvApplicationCommand :: [CompiledInteractionRecv] -> Interaction -> CompiledDatabaseDiscord () -parseInteractionRecvApplicationCommand cs info = do +parseInteractionRecvApplicationCommand cs info@InteractionApplicationCommand {interactionDataApplicationCommand = Just idac} = do tvar <- ask cache <- liftIO $ readMVar tvar - let validPlugin = findWithDefault "" (interactionId info) $ cacheApplicationCommands cache + let validPlugin = findWithDefault "" (interactionDataApplicationCommandId idac) $ cacheApplicationCommands cache mapM_ (`onInteractionRecv` info) (cs' validPlugin) where cs' plname = filter (\cir -> interactionRecvPluginName cir == plname) cs +parseInteractionRecvApplicationCommand _ _ = return () -- | This runs each 'Other' feature in @cs@ with the Discord 'Event' provided. -- Note that any events covered by other feature types will /not/ be run diff --git a/src/Tablebot/Internal/Plugins.hs b/src/Tablebot/Internal/Plugins.hs index 7d3114fd..317ba91f 100644 --- a/src/Tablebot/Internal/Plugins.hs +++ b/src/Tablebot/Internal/Plugins.hs @@ -11,6 +11,7 @@ module Tablebot.Internal.Plugins where import Control.Monad.Trans.Reader (runReaderT) import Data.Default (Default (def)) +import Data.Maybe (catMaybes) import Discord.Types (Message) import Tablebot.Internal.Types hiding (helpPages, migrations) import qualified Tablebot.Internal.Types as IT @@ -54,7 +55,7 @@ combineActions (p : ps) = a +++ b = a ++ b compilePlugin :: EnvPlugin b -> CompiledPlugin -compilePlugin p = CPl (pluginName p) sa (CApplicationComand (pluginName p) <$> UT.applicationCommands p) (helpPages p) (migrations p) +compilePlugin p = CPl (pluginName p) sa (CApplicationComand (pluginName p) <$> catMaybes (UT.applicationCommands p)) (helpPages p) (migrations p) where sa :: Database PluginActions sa = do diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index bd912578..21daf9d3 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -26,15 +26,17 @@ import Discord.Types Emoji (Emoji), GuildMember (memberUser), Message (messageAuthor), + User (userId), ) +import Tablebot.Internal.Handler.Command (makeBundleReadable) import Tablebot.Plugins.Roll.Dice import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Utility -import Tablebot.Utility.Discord (interactionResponseComponentsUpdateMessage, interactionResponseCustomMessage, sendCustomMessage, toMention) -import Tablebot.Utility.Exception (BotException (InteractionException), throwBot) +import Tablebot.Utility.Discord (interactionResponseComponentsUpdateMessage, interactionResponseCustomMessage, sendCustomMessage, toMention, toMention') +import Tablebot.Utility.Exception (BotException (InteractionException, ParserException), throwBot) import Tablebot.Utility.Parser (inlineCommandHelper) import Tablebot.Utility.SmartParser (PComm (parseComm), Quoted (Qu, quote), pars) -import Text.Megaparsec (MonadParsec (try), choice, parse, ()) +import Text.Megaparsec (MonadParsec (try), choice, errorBundlePretty, parse, ()) import Text.RawString.QQ (r) -- | The basic execution function for rolling dice. Both the expression and message are @@ -82,8 +84,8 @@ getMessagePieces e t u = do appendIf t' Nothing = t' <> "`" appendIf t' (Just e') = t' <> "`" <> e' -rollInteraction :: Interaction -> DatabaseDiscord () -rollInteraction i@InteractionComponent {interactionDataComponent = Just (InteractionDataComponentButton cid)} +rerollInteraction :: Interaction -> DatabaseDiscord () +rerollInteraction i@InteractionComponent {interactionDataComponent = Just (InteractionDataComponentButton cid)} | length opts /= 4 = throwBot $ InteractionException "could not process button click" | maybe True (\u -> toMention u /= opts !! 1) getUser = interactionResponseCustomMessage i ((interactionCallbackMessagesBasic "Hey, that isn't your button to press!") {interactionCallbackMessagesFlags = Just $ InteractionCallbackDataFlags [InteractionCallbackDataFlagEphermeral]}) | otherwise = case opts of @@ -111,7 +113,42 @@ rollInteraction i@InteractionComponent {interactionDataComponent = Just (Interac where opts = T.split (== '`') cid getUser = maybe (interactionUser i) memberUser (interactionMember i) -rollInteraction _ = return () +rerollInteraction _ = return () + +rollSlashCommandInteraction :: Interaction -> DatabaseDiscord () +rollSlashCommandInteraction i@InteractionApplicationCommand {interactionDataApplicationCommand = Just InteractionDataApplicationCommandChatInput {interactionDataApplicationCommandName = "roll", interactionDataApplicationCommandOptions = opts}} = do + e <- mapM parseExpr expr + (msg, buttons) <- getMessagePieces e (Qu <$> qt) (toMention' getUser) + interactionResponseCustomMessage i ((interactionCallbackMessagesBasic msg) {interactionCallbackMessagesComponents = buttons}) + where + findWhere s = opts >>= \(InteractionDataApplicationCommandOptionsValues values) -> lookup s $ (\v -> (interactionDataApplicationCommandOptionValueName v, interactionDataApplicationCommandOptionValueValue v)) <$> values + expr = findWhere "expression" >>= getText + qt = findWhere "quote" >>= getText + getText (ApplicationCommandInteractionDataValueString t) = Just t + getText _ = Nothing + -- TODO: work out why this exception handling isn't working + parseExpr expr' = case parse pars "" expr' of + Right p -> return p + Left e -> + let (errs, title) = makeBundleReadable e + in throwBot $ ParserException title $ "```\n" ++ errorBundlePretty errs ++ "```" + getUser = maybe 0 userId $ maybe (interactionUser i) memberUser (interactionMember i) +rollSlashCommandInteraction _ = return () + +-- TODO: tie together creating the application command and the handler for it so that they cannot be separated +-- TODO: comment +rollSlashCommand :: Maybe CreateApplicationCommand +rollSlashCommand = + createApplicationCommandChatInput "roll" "roll some dice with a description" >>= \cac -> + return $ + cac + { createApplicationCommandOptions = + Just $ + ApplicationCommandOptionsValues + [ ApplicationCommandOptionValueString "expression" "What expression is being evaluated (list or integer)" Nothing Nothing Nothing, + ApplicationCommandOptionValueString "quote" "What message is associated with this roll" Nothing Nothing Nothing + ] + } -- | Manually creating parser for this command, since SmartCommand doesn't work fully for -- multiple Maybe values @@ -201,5 +238,10 @@ rollPlugin = { commands = [rollDice, commandAlias "r" rollDice, genchar], helpPages = [rollHelp, gencharHelp], inlineCommands = [rollDiceInline], - onInteractionRecvs = [InteractionRecv rollInteraction] + onInteractionRecvs = + InteractionRecv + <$> [ rerollInteraction, + rollSlashCommandInteraction + ], + applicationCommands = [rollSlashCommand] } diff --git a/src/Tablebot/Utility/Types.hs b/src/Tablebot/Utility/Types.hs index e1ff8607..1d1f13da 100644 --- a/src/Tablebot/Utility/Types.hs +++ b/src/Tablebot/Utility/Types.hs @@ -58,6 +58,7 @@ data TablebotCache = TCache { cacheKnownEmoji :: Map Text Emoji, cacheApplicationCommands :: Map ApplicationCommandId Text } + deriving (Show) instance Default TablebotCache where def = TCache empty empty @@ -315,7 +316,7 @@ data RequiredPermission = None | Any | Exec | Moderator | Both | Superuser deriv data EnvPlugin d = Pl { pluginName :: Text, startUp :: StartUp d, - applicationCommands :: [CreateApplicationCommand], + applicationCommands :: [Maybe CreateApplicationCommand], commands :: [EnvCommand d], inlineCommands :: [EnvInlineCommand d], onMessageChanges :: [EnvMessageChange d], From 8637e7a47bbc61e8af4e61cae84513e48617c6f5 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 22 Jan 2022 15:44:57 +0000 Subject: [PATCH 11/96] associated an application command with a handler --- src/Tablebot.hs | 12 ++++++++++-- src/Tablebot/Handler.hs | 4 ++-- src/Tablebot/Internal/Handler/Event.hs | 21 +++++++++++---------- src/Tablebot/Internal/Plugins.hs | 22 +++++++++++++++------- src/Tablebot/Internal/Types.hs | 15 +++++++-------- src/Tablebot/Plugins/Roll/Plugin.hs | 10 ++++------ src/Tablebot/Utility/Types.hs | 7 +++---- 7 files changed, 52 insertions(+), 39 deletions(-) diff --git a/src/Tablebot.hs b/src/Tablebot.hs index 75bae9ef..518d35e1 100644 --- a/src/Tablebot.hs +++ b/src/Tablebot.hs @@ -73,10 +73,10 @@ runTablebot dToken prefix dbpath plugins = let filteredPlugins = removeBlacklisted blacklist plugins -- Combine the list of plugins into both a combined plugin let !plugin = generateHelp $ combinePlugins filteredPlugins - compiledAppComms = combinedApplicationCommands plugin -- Run the setup actions of each plugin and collect the plugin actions into a single @PluginActions@ instance allActions <- mapM (runResourceT . runNoLoggingT . flip runSqlPool pool) (combinedSetupAction plugin) let !actions = combineActions allActions + compiledAppComms = compiledApplicationCommands actions -- TODO: this might have issues with duplicates? -- TODO: in production, this should probably run once and then never again. @@ -98,12 +98,20 @@ runTablebot dToken prefix dbpath plugins = -- sometimes). runReaderT (mapM (runCron pool) (compiledCronJobs actions) >>= liftIO . putMVar mvar) cacheMVar + -- generate the application commands, cleaning up any application commands we don't like serverIdStr <- liftIO $ getEnv "SERVER_ID" serverId <- maybe (fail "could not read server id") return (readMaybe serverIdStr) aid <- partialApplicationID . cacheApplication <$> readCache - applicationCommands <- mapM (\(CApplicationComand pname cac) -> createApplicationCommand aid serverId cac >>= \ac -> return (applicationCommandId ac, pname)) compiledAppComms + applicationCommands <- + mapM + ( \(CApplicationComand cac action) -> do + ac <- createApplicationCommand aid serverId cac + return (applicationCommandId ac, action) + ) + compiledAppComms removeApplicationCommandsNotInList aid serverId (fst <$> applicationCommands) liftIO $ takeMVar cacheMVar >>= \tcache -> putMVar cacheMVar $ tcache {cacheApplicationCommands = M.fromList applicationCommands} + liftIO $ putStrLn "Tablebot lives!", -- Kill every cron job in the mvar. discordOnEnd = takeMVar mvar >>= killCron diff --git a/src/Tablebot/Handler.hs b/src/Tablebot/Handler.hs index 28fb86b2..b90e21ad 100644 --- a/src/Tablebot/Handler.hs +++ b/src/Tablebot/Handler.hs @@ -72,8 +72,8 @@ eventHandler pl prefix = \case -- Similar with MessageReactionRemoveEmoji (removes all of one type). MessageReactionRemoveAll _cid _mid -> pure () MessageReactionRemoveEmoji _rri -> pure () - InteractionCreate i@InteractionComponent {} -> parseInteractionRecvComponent (compiledOnInteractionRecvs pl) i - InteractionCreate i@InteractionApplicationCommand {} -> parseInteractionRecvApplicationCommand (compiledOnInteractionRecvs pl) i + InteractionCreate i@InteractionComponent {} -> parseInteractionRecvComponent (compiledOnComponentInteractionRecvs pl) i + InteractionCreate i@InteractionApplicationCommand {} -> parseInteractionRecvApplicationCommand i -- TODO: add application command autocomplete as an option e -> parseOther (compiledOtherEvents pl) e where diff --git a/src/Tablebot/Internal/Handler/Event.hs b/src/Tablebot/Internal/Handler/Event.hs index 813fcc76..caee061e 100644 --- a/src/Tablebot/Internal/Handler/Event.hs +++ b/src/Tablebot/Internal/Handler/Event.hs @@ -20,12 +20,13 @@ where import Control.Concurrent (readMVar) import Control.Monad.RWS (MonadIO (liftIO), MonadReader (ask)) -import Data.Map (findWithDefault) +import qualified Data.Map as M import Data.Text (isPrefixOf) import Discord.Interactions (Interaction (..), InteractionDataApplicationCommand (interactionDataApplicationCommandId), InteractionDataComponent (interactionDataComponentCustomId)) import Discord.Types (ChannelId, Event, MessageId, ReactionInfo) -import Tablebot.Internal.Types -import Tablebot.Utility.Types (TablebotCache (cacheApplicationCommands)) +import Tablebot.Internal.Plugins (changeAction) +import Tablebot.Internal.Types as IT +import qualified Tablebot.Utility.Types as UT -- | This runs each 'MessageChange' feature in @cs@ with the information from a -- Discord 'MessageUpdate' or 'MessageDelete' event - whether it is an update @@ -61,15 +62,15 @@ parseInteractionRecvComponent cs info@InteractionComponent {interactionDataCompo cs' = filter (\cir -> interactionRecvPluginName cir `isPrefixOf` interactionDataComponentCustomId idc) cs parseInteractionRecvComponent _ _ = return () -parseInteractionRecvApplicationCommand :: [CompiledInteractionRecv] -> Interaction -> CompiledDatabaseDiscord () -parseInteractionRecvApplicationCommand cs info@InteractionApplicationCommand {interactionDataApplicationCommand = Just idac} = do +parseInteractionRecvApplicationCommand :: Interaction -> CompiledDatabaseDiscord () +parseInteractionRecvApplicationCommand info@InteractionApplicationCommand {interactionDataApplicationCommand = Just idac} = do tvar <- ask cache <- liftIO $ readMVar tvar - let validPlugin = findWithDefault "" (interactionDataApplicationCommandId idac) $ cacheApplicationCommands cache - mapM_ (`onInteractionRecv` info) (cs' validPlugin) - where - cs' plname = filter (\cir -> interactionRecvPluginName cir == plname) cs -parseInteractionRecvApplicationCommand _ _ = return () + let action = UT.cacheApplicationCommands cache M.!? interactionDataApplicationCommandId idac + case action of + Nothing -> return () + Just act -> changeAction () $ UT.onInteractionRecv act info +parseInteractionRecvApplicationCommand _ = return () -- | This runs each 'Other' feature in @cs@ with the Discord 'Event' provided. -- Note that any events covered by other feature types will /not/ be run diff --git a/src/Tablebot/Internal/Plugins.hs b/src/Tablebot/Internal/Plugins.hs index 317ba91f..42b8d9c0 100644 --- a/src/Tablebot/Internal/Plugins.hs +++ b/src/Tablebot/Internal/Plugins.hs @@ -9,9 +9,8 @@ -- This contains some functions to combine and compile plugins module Tablebot.Internal.Plugins where -import Control.Monad.Trans.Reader (runReaderT) +import Control.Monad.Reader (MonadTrans (lift), ReaderT (runReaderT)) import Data.Default (Default (def)) -import Data.Maybe (catMaybes) import Discord.Types (Message) import Tablebot.Internal.Types hiding (helpPages, migrations) import qualified Tablebot.Internal.Types as IT @@ -26,7 +25,6 @@ combinePlugins (p : ps) = let p' = combinePlugins ps in CmPl { combinedSetupAction = setupAction p : combinedSetupAction p', - combinedApplicationCommands = IT.applicationCommands p ++ combinedApplicationCommands p', combinedMigrations = IT.migrations p ++ combinedMigrations p', combinedHelpPages = IT.helpPages p ++ combinedHelpPages p' } @@ -38,12 +36,13 @@ combineActions [] = def combineActions (p : ps) = let p' = combineActions ps in PA - { compiledCommands = compiledCommands p +++ compiledCommands p', + { compiledApplicationCommands = compiledApplicationCommands p +++ compiledApplicationCommands p', + compiledCommands = compiledCommands p +++ compiledCommands p', compiledInlineCommands = compiledInlineCommands p +++ compiledInlineCommands p', compiledOnMessageChanges = compiledOnMessageChanges p +++ compiledOnMessageChanges p', compiledOnReactionAdds = compiledOnReactionAdds p +++ compiledOnReactionAdds p', compiledOnReactionDeletes = compiledOnReactionDeletes p +++ compiledOnReactionDeletes p', - compiledOnInteractionRecvs = compiledOnInteractionRecvs p +++ compiledOnInteractionRecvs p', + compiledOnComponentInteractionRecvs = compiledOnComponentInteractionRecvs p +++ compiledOnComponentInteractionRecvs p', compiledOtherEvents = compiledOtherEvents p +++ compiledOtherEvents p', compiledCronJobs = compiledCronJobs p +++ compiledCronJobs p' } @@ -55,7 +54,7 @@ combineActions (p : ps) = a +++ b = a ++ b compilePlugin :: EnvPlugin b -> CompiledPlugin -compilePlugin p = CPl (pluginName p) sa (CApplicationComand (pluginName p) <$> catMaybes (UT.applicationCommands p)) (helpPages p) (migrations p) +compilePlugin p = CPl (pluginName p) sa (helpPages p) (migrations p) where sa :: Database PluginActions sa = do @@ -63,12 +62,13 @@ compilePlugin p = CPl (pluginName p) sa (CApplicationComand (pluginName p) <$> c return $ PA + (fixApplicationCommands state (UT.applicationCommands p)) (map (fixCommand state) $ commands p) (map (fixInlineCommand state) $ inlineCommands p) (map (fixOnMessageChanges state) $ onMessageChanges p) (map (fixOnReactionAdd state) $ onReactionAdds p) (map (fixOnReactionDelete state) $ onReactionDeletes p) - (map (fixOnInteractionRecv state) $ onInteractionRecvs p) + (map (fixOnInteractionRecv state) $ onComponentInteractionRecvs p) (map (fixOther state) $ otherEvents p) (map (fixCron state) $ cronJobs p) @@ -81,6 +81,14 @@ compilePlugin p = CPl (pluginName p) sa (CApplicationComand (pluginName p) <$> c fixOnInteractionRecv state' (InteractionRecv action') = CInteractionRecv (pluginName p) (changeAction state' . action') fixOther state' (Other action') = COther (changeAction state' . action') fixCron state' (CronJob time action') = CCronJob time (changeAction state' action') + fixApplicationCommands state' = + concat + . ( ( \case + (Just ac, action) -> [CApplicationComand ac (InteractionRecv $ \i -> lift (changeAction state' (UT.onInteractionRecv action i)))] + (Nothing, _) -> [] + ) + <$> + ) -- * Helper converters diff --git a/src/Tablebot/Internal/Types.hs b/src/Tablebot/Internal/Types.hs index 1b548aec..8f70b9d8 100644 --- a/src/Tablebot/Internal/Types.hs +++ b/src/Tablebot/Internal/Types.hs @@ -29,42 +29,41 @@ type CompiledDatabaseDiscord = ReaderT (MVar TablebotCache) (SqlPersistT Discord data CompiledPlugin = CPl { compiledName :: Text, setupAction :: Database PluginActions, - applicationCommands :: [CompiledApplicationCommand], helpPages :: [HelpPage], migrations :: [Migration] } data PluginActions = PA - { compiledCommands :: [CompiledCommand], + { compiledApplicationCommands :: [CompiledApplicationCommand], + compiledCommands :: [CompiledCommand], compiledInlineCommands :: [CompiledInlineCommand], compiledOnMessageChanges :: [CompiledMessageChange], compiledOnReactionAdds :: [CompiledReactionAdd], compiledOnReactionDeletes :: [CompiledReactionDel], - compiledOnInteractionRecvs :: [CompiledInteractionRecv], + compiledOnComponentInteractionRecvs :: [CompiledInteractionRecv], compiledOtherEvents :: [CompiledOther], compiledCronJobs :: [CompiledCronJob] } instance Default PluginActions where - def = PA [] [] [] [] [] [] [] [] + def = PA [] [] [] [] [] [] [] [] [] data CombinedPlugin = CmPl { combinedSetupAction :: [Database PluginActions], - combinedApplicationCommands :: [CompiledApplicationCommand], combinedHelpPages :: [HelpPage], combinedMigrations :: [Migration] } instance Default CombinedPlugin where - def = CmPl [] [] [] [] + def = CmPl [] [] [] -- * Compiled Items -- These are compiled forms of the actions from the public types that remove the reader. data CompiledApplicationCommand = CApplicationComand - { applicationCommandPluginName :: Text, - applicationCommand :: CreateApplicationCommand + { applicationCommand :: CreateApplicationCommand, + applicationCommandAction :: EnvInteractionRecv () } data CompiledCommand = CCommand diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 21daf9d3..cd4046d0 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -238,10 +238,8 @@ rollPlugin = { commands = [rollDice, commandAlias "r" rollDice, genchar], helpPages = [rollHelp, gencharHelp], inlineCommands = [rollDiceInline], - onInteractionRecvs = - InteractionRecv - <$> [ rerollInteraction, - rollSlashCommandInteraction - ], - applicationCommands = [rollSlashCommand] + onComponentInteractionRecvs = + [ InteractionRecv rerollInteraction + ], + applicationCommands = [(rollSlashCommand, InteractionRecv rollSlashCommandInteraction)] } diff --git a/src/Tablebot/Utility/Types.hs b/src/Tablebot/Utility/Types.hs index 1d1f13da..a702ed4e 100644 --- a/src/Tablebot/Utility/Types.hs +++ b/src/Tablebot/Utility/Types.hs @@ -56,9 +56,8 @@ type Database d = SqlPersistM d data TablebotCache = TCache { cacheKnownEmoji :: Map Text Emoji, - cacheApplicationCommands :: Map ApplicationCommandId Text + cacheApplicationCommands :: Map ApplicationCommandId (EnvInteractionRecv ()) } - deriving (Show) instance Default TablebotCache where def = TCache empty empty @@ -316,13 +315,13 @@ data RequiredPermission = None | Any | Exec | Moderator | Both | Superuser deriv data EnvPlugin d = Pl { pluginName :: Text, startUp :: StartUp d, - applicationCommands :: [Maybe CreateApplicationCommand], + applicationCommands :: [(Maybe CreateApplicationCommand, EnvInteractionRecv d)], commands :: [EnvCommand d], inlineCommands :: [EnvInlineCommand d], onMessageChanges :: [EnvMessageChange d], onReactionAdds :: [EnvReactionAdd d], onReactionDeletes :: [EnvReactionDel d], - onInteractionRecvs :: [EnvInteractionRecv d], + onComponentInteractionRecvs :: [EnvInteractionRecv d], otherEvents :: [EnvOther d], cronJobs :: [EnvCronJob d], helpPages :: [HelpPage], From 91406508417846c17870d204023fe20142a6da96 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 22 Jan 2022 18:22:58 +0000 Subject: [PATCH 12/96] Beginning of automated application command argument creation, adding a generic message send object --- src/Tablebot/Plugins/Roll/Plugin.hs | 22 +++---- src/Tablebot/Utility/Discord.hs | 16 ++--- src/Tablebot/Utility/SmartParser.hs | 95 ++++++++++++++++++++++++++--- src/Tablebot/Utility/Types.hs | 51 +++++++++++++++- 4 files changed, 153 insertions(+), 31 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index cd4046d0..abc8c00a 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -11,14 +11,10 @@ module Tablebot.Plugins.Roll.Plugin (rollPlugin) where import Control.Monad.Writer (MonadIO (liftIO)) import Data.Bifunctor (Bifunctor (first)) -import Data.Default (Default (def)) import Data.Maybe (fromMaybe) import Data.Text (Text, intercalate, pack, replicate, unpack) import qualified Data.Text as T import Discord.Interactions -import Discord.Requests - ( MessageDetailedOpts (messageDetailedComponents, messageDetailedContent), - ) import Discord.Types ( ButtonStyle (ButtonStyleSecondary), ComponentActionRow (ComponentActionRowButton), @@ -64,10 +60,10 @@ rollDice'' e' t u = do simplify li = li countFormatting s = (`div` 4) $ T.foldr (\c cf -> cf + (2 * fromEnum (c == '`')) + fromEnum (c `elem` ['~', '_', '*'])) 0 s -rollDice' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> Message -> DatabaseDiscord () +rollDice' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> Message -> DatabaseDiscord MessageDetails rollDice' e t m = do (msg, buttons) <- getMessagePieces e t (toMention $ messageAuthor m) - sendCustomMessage m (def {messageDetailedContent = msg, messageDetailedComponents = buttons}) + return ((messageJustText msg) {messageDetailsComponents = buttons}) getMessagePieces :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> Text -> DatabaseDiscord (Text, Maybe [ComponentActionRow]) getMessagePieces e t u = do @@ -87,28 +83,28 @@ getMessagePieces e t u = do rerollInteraction :: Interaction -> DatabaseDiscord () rerollInteraction i@InteractionComponent {interactionDataComponent = Just (InteractionDataComponentButton cid)} | length opts /= 4 = throwBot $ InteractionException "could not process button click" - | maybe True (\u -> toMention u /= opts !! 1) getUser = interactionResponseCustomMessage i ((interactionCallbackMessagesBasic "Hey, that isn't your button to press!") {interactionCallbackMessagesFlags = Just $ InteractionCallbackDataFlags [InteractionCallbackDataFlagEphermeral]}) + | maybe True (\u -> toMention u /= opts !! 1) getUser = interactionResponseCustomMessage i ((messageJustText "Hey, that isn't your button to press!") {messageDetailsFlags = Just $ InteractionCallbackDataFlags [InteractionCallbackDataFlagEphermeral]}) | otherwise = case opts of [_, uid, "", ""] -> do (msg, button) <- getMessagePieces Nothing Nothing uid - interactionResponseComponentsUpdateMessage i ((interactionCallbackMessagesBasic msg) {interactionCallbackMessagesComponents = button}) + interactionResponseComponentsUpdateMessage i ((messageJustText msg) {messageDetailsComponents = button}) [_, uid, "", qt] -> do (msg, button) <- getMessagePieces Nothing (Just (Qu qt)) uid - interactionResponseComponentsUpdateMessage i ((interactionCallbackMessagesBasic msg) {interactionCallbackMessagesComponents = button}) + interactionResponseComponentsUpdateMessage i ((messageJustText msg) {messageDetailsComponents = button}) [_, uid, e, ""] -> do let e' = parse pars "" e case e' of Left _ -> throwBot $ InteractionException "could not process button click" Right e'' -> do (msg, button) <- getMessagePieces (Just e'') Nothing uid - interactionResponseComponentsUpdateMessage i ((interactionCallbackMessagesBasic msg) {interactionCallbackMessagesComponents = button}) + interactionResponseComponentsUpdateMessage i ((messageJustText msg) {messageDetailsComponents = button}) [_, uid, e, qt] -> do let e' = parse pars "" e case e' of Left _ -> throwBot $ InteractionException "could not process button click" Right e'' -> do (msg, button) <- getMessagePieces (Just e'') (Just (Qu qt)) uid - interactionResponseComponentsUpdateMessage i ((interactionCallbackMessagesBasic msg) {interactionCallbackMessagesComponents = button}) + interactionResponseComponentsUpdateMessage i ((messageJustText msg) {messageDetailsComponents = button}) _ -> throwBot $ InteractionException "could not process button click" where opts = T.split (== '`') cid @@ -119,7 +115,7 @@ rollSlashCommandInteraction :: Interaction -> DatabaseDiscord () rollSlashCommandInteraction i@InteractionApplicationCommand {interactionDataApplicationCommand = Just InteractionDataApplicationCommandChatInput {interactionDataApplicationCommandName = "roll", interactionDataApplicationCommandOptions = opts}} = do e <- mapM parseExpr expr (msg, buttons) <- getMessagePieces e (Qu <$> qt) (toMention' getUser) - interactionResponseCustomMessage i ((interactionCallbackMessagesBasic msg) {interactionCallbackMessagesComponents = buttons}) + interactionResponseCustomMessage i ((messageJustText msg) {messageDetailsComponents = buttons}) where findWhere s = opts >>= \(InteractionDataApplicationCommandOptionsValues values) -> lookup s $ (\v -> (interactionDataApplicationCommandOptionValueName v, interactionDataApplicationCommandOptionValueValue v)) <$> values expr = findWhere "expression" >>= getText @@ -168,7 +164,7 @@ rollDice = Command "roll" rollDiceParser [] -- | Rolling dice inline. rollDiceInline :: InlineCommand -rollDiceInline = inlineCommandHelper "[|" "|]" pars (\e m -> rollDice' (Just e) Nothing m) +rollDiceInline = inlineCommandHelper "[|" "|]" pars (\e m -> rollDice' (Just e) Nothing m >>= sendCustomMessage m) -- | Help page for rolling dice, with a link to the help page. rollHelp :: HelpPage diff --git a/src/Tablebot/Utility/Discord.hs b/src/Tablebot/Utility/Discord.hs index d06bb6e3..f6bc4d6a 100644 --- a/src/Tablebot/Utility/Discord.hs +++ b/src/Tablebot/Utility/Discord.hs @@ -67,7 +67,7 @@ import Discord.Types import GHC.Word (Word64) import Tablebot.Internal.Cache import Tablebot.Internal.Embed -import Tablebot.Utility (EnvDatabaseDiscord, liftDiscord) +import Tablebot.Utility (EnvDatabaseDiscord, MessageDetails, convertMessageFormatBasic, convertMessageFormatInteraction, liftDiscord, messageJustText) import Tablebot.Utility.Exception (BotException (..)) -- | @sendMessage@ sends the input message @t@ in the same channel as message @@ -90,10 +90,10 @@ sendMessage m t = do -- other specific message data, you shouldn't use this function. sendCustomMessage :: Message -> - R.MessageDetailedOpts -> + MessageDetails -> EnvDatabaseDiscord s () sendCustomMessage m t = do - res <- liftDiscord . restCall $ R.CreateMessageDetailed (messageChannelId m) t + res <- liftDiscord . restCall $ R.CreateMessageDetailed (messageChannelId m) (convertMessageFormatBasic t) case res of Left _ -> throw $ MessageSendException "Failed to send message." Right _ -> return () @@ -395,20 +395,20 @@ interactionResponseDeferUpdateMessage i = do -- | Respond to the given interaction with the given text. interactionResponseMessage :: Interaction -> Text -> EnvDatabaseDiscord s () -interactionResponseMessage i t = interactionResponseCustomMessage i (InteractionCallbackMessages Nothing (Just t) Nothing Nothing Nothing Nothing Nothing) +interactionResponseMessage i t = interactionResponseCustomMessage i (messageJustText t) -- | Respond to the given interaction with a custom messages object. -interactionResponseCustomMessage :: Interaction -> InteractionCallbackMessages -> EnvDatabaseDiscord s () +interactionResponseCustomMessage :: Interaction -> MessageDetails -> EnvDatabaseDiscord s () interactionResponseCustomMessage i t = do - res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponse InteractionCallbackTypeChannelMessageWithSource (Just $ InteractionCallbackDataMessages t)) + res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponse InteractionCallbackTypeChannelMessageWithSource (Just $ InteractionCallbackDataMessages $ convertMessageFormatInteraction t)) case res of Left _ -> throw $ InteractionException "Failed to respond to interaction." Right _ -> return () -- | Respond to the given interaction by updating the component's message. -interactionResponseComponentsUpdateMessage :: Interaction -> InteractionCallbackMessages -> EnvDatabaseDiscord s () +interactionResponseComponentsUpdateMessage :: Interaction -> MessageDetails -> EnvDatabaseDiscord s () interactionResponseComponentsUpdateMessage i t = do - res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponse InteractionCallbackTypeUpdateMessage (Just $ InteractionCallbackDataMessages t)) + res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponse InteractionCallbackTypeUpdateMessage (Just $ InteractionCallbackDataMessages $ convertMessageFormatInteraction t)) case res of Left _ -> throw $ InteractionException "Failed to respond to interaction with components update." Right _ -> return () diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index 0982a110..3b1ba79c 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -14,13 +14,17 @@ -- build a parser that reads in that Int and then runs the command. module Tablebot.Utility.SmartParser where -import Data.Proxy +import Data.Proxy (Proxy (..)) import Data.String (IsString (fromString)) import Data.Text (Text, pack) +import Discord.Interactions import Discord.Types (Message) +import GHC.OldList (find) import GHC.TypeLits +import Tablebot.Utility.Discord (interactionResponseCustomMessage, sendCustomMessage) +import Tablebot.Utility.Exception (BotException (InteractionException), catchBot, throwBot) import Tablebot.Utility.Parser -import Tablebot.Utility.Types (EnvDatabaseDiscord, Parser) +import Tablebot.Utility.Types (EnvDatabaseDiscord, MessageDetails, Parser) import Text.Megaparsec -- | @PComm@ defines function types that we can automatically turn into parsers @@ -36,12 +40,19 @@ class PComm commandty s where instance {-# OVERLAPPING #-} PComm (Message -> EnvDatabaseDiscord s ()) s where parseComm comm = skipSpace >> eof >> return comm --- Second base case is the single argument - no trailing space is wanted so we --- have to specify this case. -instance {-# OVERLAPPING #-} CanParse a => PComm (a -> Message -> EnvDatabaseDiscord s ()) s where - parseComm comm = do - this <- pars @a - parseComm (comm this) +instance {-# OVERLAPPING #-} PComm (EnvDatabaseDiscord s MessageDetails) s where + parseComm comm = skipSpace >> eof >> return (\m -> comm >>= sendCustomMessage m) + +instance {-# OVERLAPPING #-} PComm (Message -> EnvDatabaseDiscord s MessageDetails) s where + parseComm comm = skipSpace >> eof >> return (\m -> comm m >>= sendCustomMessage m) + +-- TODO: verify that this second base case is no longer needed +-- -- Second base case is the single argument - no trailing space is wanted so we +-- -- have to specify this case. +-- instance {-# OVERLAPPING #-} CanParse a => PComm (a -> Message -> EnvDatabaseDiscord s ()) s where +-- parseComm comm = do +-- this <- pars @a +-- parseComm (comm this) -- Recursive case is to parse the domain of the function type, then the rest. instance {-# OVERLAPPABLE #-} (CanParse a, PComm as s) => PComm (a -> as) s where @@ -172,3 +183,71 @@ instance IsString a => CanParse (RestOfInput1 a) where -- have no arguments (thus making it extremely clear). noArguments :: (Message -> EnvDatabaseDiscord d ()) -> Parser (Message -> EnvDatabaseDiscord d ()) noArguments = parseComm + +-- | Labelled value for use with smart commands. +newtype Labelled (name :: Symbol) (desc :: Symbol) a = Labelled a + +-- | Easily make a labelled value. +labelValue :: forall n d a. a -> Labelled n d a +labelValue = Labelled @n @d + +getLabelValues :: forall n d a. (KnownSymbol n, KnownSymbol d) => Proxy (Labelled n d a) -> (Text, Text) +getLabelValues _ = (pack (symbolVal (Proxy :: Proxy n)), pack (symbolVal (Proxy :: Proxy d))) + +class MakeAppComm commandty where + makeAppComm :: Proxy commandty -> [ApplicationCommandOptionValue] + +-- As a base case, no more arguments +instance {-# OVERLAPPING #-} MakeAppComm (EnvDatabaseDiscord s MessageDetails) where + makeAppComm _ = [] + +instance {-# OVERLAPPABLE #-} (MakeAppComm mac, MakeAppCommArg ty) => MakeAppComm (ty -> mac) where + makeAppComm _ = makeAppCommArg (Proxy :: Proxy ty) : makeAppComm (Proxy :: Proxy mac) + +class MakeAppCommArg commandty where + makeAppCommArg :: Proxy commandty -> ApplicationCommandOptionValue + +instance (KnownSymbol name, KnownSymbol desc) => MakeAppCommArg (Labelled name desc Text) where + makeAppCommArg l = ApplicationCommandOptionValueString n d (Just True) Nothing Nothing + where + (n, d) = getLabelValues l + +instance (KnownSymbol name, KnownSymbol desc, MakeAppCommArg (Labelled name desc t)) => MakeAppCommArg (Labelled name desc (Maybe t)) where + makeAppCommArg _ = + (makeAppCommArg (Proxy :: Proxy (Labelled name desc t))) + { applicationCommandOptionValueRequired = Just False + } + +-- As a base case, send the message produced + +class ProcessAppComm commandty s where + processAppComm :: commandty -> Interaction -> EnvDatabaseDiscord s () + +instance {-# OVERLAPPING #-} ProcessAppComm (EnvDatabaseDiscord s MessageDetails) s where + processAppComm comm i = comm >>= interactionResponseCustomMessage i + +instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s, ProcessAppCommArg ty s) => ProcessAppComm (ty -> pac) s where + processAppComm comm i@InteractionApplicationCommand {interactionDataApplicationCommand = Just InteractionDataApplicationCommandChatInput {interactionDataApplicationCommandOptions = (Just (InteractionDataApplicationCommandOptionsValues values))}} = do + t <- processAppCommArg values + processAppComm (comm t) i + processAppComm _ _ = throwBot $ InteractionException "could not process args to application command" + +class ProcessAppCommArg t s where + processAppCommArg :: [InteractionDataApplicationCommandOptionValue] -> EnvDatabaseDiscord s t + +getValue :: String -> [InteractionDataApplicationCommandOptionValue] -> Maybe ApplicationCommandInteractionDataValue +getValue t is = interactionDataApplicationCommandOptionValueValue <$> find ((== pack t) . interactionDataApplicationCommandOptionValueName) is + +instance (KnownSymbol name) => ProcessAppCommArg (Labelled name desc Text) s where + processAppCommArg is = case getValue (symbolVal (Proxy :: Proxy name)) is of + Just (ApplicationCommandInteractionDataValueString t) -> return $ labelValue t + _ -> throwBot $ InteractionException "could not find required parameter" + +instance (KnownSymbol name, ProcessAppCommArg (Labelled name description t) s) => ProcessAppCommArg (Labelled name description (Maybe t)) s where + processAppCommArg is = do + let result = processAppCommArg is :: EnvDatabaseDiscord s (Labelled name description t) + ( do + (Labelled l) <- result + return (labelValue (Just l)) + ) + `catchBot` const (return $ labelValue Nothing) diff --git a/src/Tablebot/Utility/Types.hs b/src/Tablebot/Utility/Types.hs index a702ed4e..adc1c0e5 100644 --- a/src/Tablebot/Utility/Types.hs +++ b/src/Tablebot/Utility/Types.hs @@ -15,6 +15,7 @@ module Tablebot.Utility.Types where import Control.Concurrent.MVar (MVar) import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Reader (ReaderT) +import Data.ByteString (ByteString) import Data.Char (toLower) import Data.Default (Default (def)) import Data.Map (Map, empty) @@ -23,15 +24,22 @@ import Data.Text (Text) import Data.Void (Void) import Database.Persist.Sqlite (Migration, SqlPersistM, SqlPersistT) import Discord (DiscordHandler) -import Discord.Interactions (CreateApplicationCommand, Interaction) +import Discord.Interactions (CreateApplicationCommand, Interaction, InteractionCallbackDataFlags, InteractionCallbackMessages (InteractionCallbackMessages)) +import Discord.Internal.Rest.Channel (MessageDetailedOpts (MessageDetailedOpts)) import Discord.Types - ( ApplicationCommandId, + ( AllowedMentions, + ApplicationCommandId, + Attachment, ChannelId, + ComponentActionRow, + Embed, Emoji, Event (..), Message, MessageId, + MessageReference, ReactionInfo, + StickerId, ) import Safe.Exact (dropExactMay, takeExactMay) import Text.Megaparsec (Parsec) @@ -341,3 +349,42 @@ plug name' = Pl name' (StartUp (return ())) [] [] [] [] [] [] [] [] [] [] [] envPlug :: Text -> StartUp d -> EnvPlugin d envPlug name' startup = Pl name' startup [] [] [] [] [] [] [] [] [] [] [] + +messageJustText :: Text -> MessageDetails +messageJustText t = MessageDetails Nothing (Just t) Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing + +data MessageDetails = MessageDetails + { messageDetailsTTS :: Maybe Bool, + messageDetailsContent :: Maybe Text, + messageDetailsEmbeds :: Maybe [Embed], + messageDetailsFile :: Maybe (Text, ByteString), + messageDetailsAllowedMentions :: Maybe AllowedMentions, + messageDetailsFlags :: Maybe InteractionCallbackDataFlags, + messageDetailsReference :: Maybe MessageReference, + messageDetailsComponents :: Maybe [ComponentActionRow], + messageDetailsAttachments :: Maybe [Attachment], + messageDetailsStickerIds :: Maybe [StickerId] + } + +convertMessageFormatInteraction :: MessageDetails -> InteractionCallbackMessages +convertMessageFormatInteraction MessageDetails {..} = + InteractionCallbackMessages + messageDetailsTTS + messageDetailsContent + messageDetailsEmbeds + messageDetailsAllowedMentions + messageDetailsFlags + messageDetailsComponents + messageDetailsAttachments + +convertMessageFormatBasic :: MessageDetails -> MessageDetailedOpts +convertMessageFormatBasic MessageDetails {..} = + MessageDetailedOpts + (fromMaybe "" messageDetailsContent) + (fromMaybe False messageDetailsTTS) + Nothing + messageDetailsFile + messageDetailsAllowedMentions + messageDetailsReference + messageDetailsComponents + messageDetailsStickerIds From b437b475d42891718a6cc79e1503b5f1da77d79a Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 22 Jan 2022 18:42:24 +0000 Subject: [PATCH 13/96] moved dice functions over to using proxies instead --- .../Plugins/Roll/Dice/DiceFunctions.hs | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs index d0c80295..82ccbe2c 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs @@ -24,6 +24,7 @@ import Control.Monad.Exception (MonadException) import Data.List (genericDrop, genericLength, genericTake, sort) import Data.Map as M (Map, fromList, keys) import Data.Maybe (fromJust) +import Data.Proxy (Proxy (..)) import Data.Text (Text, unpack) import Tablebot.Utility.Exception (BotException (EvaluationException), throwBot) @@ -105,7 +106,7 @@ constructFuncInfo s f = constructFuncInfo' s f (Nothing, Nothing, const False) constructFuncInfo' :: forall j f m. (MonadException m, ApplyFunc m f, Returns f ~ j) => Text -> f -> (Maybe Integer, Maybe Integer, Integer -> Bool) -> FuncInfoBase m j constructFuncInfo' s f bs = FuncInfo s params (last types) (applyFunc f (fromIntegral (length params)) bs) where - types = getTypes f + types = getTypes (Proxy :: Proxy f) params = init types -- | Some evaluated values, either an integer or a list of values with their @@ -121,11 +122,11 @@ data ArgType = ATInteger | ATIntegerList -- types. Only supports integers and integer lists currently. class ArgCount f where -- | Get the number of arguments to a function. - getArgs :: f -> Integer + getArgs :: Proxy f -> Integer getArgs = (+ (-1)) . fromIntegral . length . getTypes -- | Get the types of arguments to a function. - getTypes :: f -> [ArgType] + getTypes :: Proxy f -> [ArgType] instance ArgCount Integer where getTypes _ = [ATInteger] @@ -134,10 +135,10 @@ instance ArgCount [Integer] where getTypes _ = [ATIntegerList] instance ArgCount f => ArgCount (Integer -> f) where - getTypes f = ATInteger : getTypes (f 1) + getTypes _ = ATInteger : getTypes (Proxy :: Proxy f) instance ArgCount f => ArgCount ([Integer] -> f) where - getTypes f = ATIntegerList : getTypes (f [1]) + getTypes _ = ATIntegerList : getTypes (Proxy :: Proxy f) -- | Type class which represents applying a function f to some inputs when given -- the bounds for the function and some number of inputs. @@ -168,18 +169,18 @@ instance {-# OVERLAPPING #-} ApplyFunc m [Integer] where applyFunc _ args _ _ = throwBot $ EvaluationException ("incorrect number of arguments to function. expected " <> show args <> ", got more than that") [] instance {-# OVERLAPPABLE #-} (ApplyFunc m f) => ApplyFunc m (Integer -> f) where - applyFunc f args _ [] = throwBot $ EvaluationException ("incorrect number of arguments to function. got " <> show dif <> ", expected " <> show args) [] - where - dif = args - getArgs f applyFunc f args bs ((LIInteger x) : xs) = checkBounds x bs >>= \x' -> applyFunc (f x') args bs xs - applyFunc _ _ _ (_ : _) = throwBot $ EvaluationException "incorrect type given to function. expected an integer, got a list" [] + applyFunc _ args _ [] = throwBot $ EvaluationException ("incorrect number of arguments to function. got " <> show dif <> ", expected " <> show args) [] + where + dif = args - getArgs (Proxy :: Proxy f) + applyFunc _ _ _ _ = throwBot $ EvaluationException "incorrect type given to function. expected an integer, got a list" [] instance {-# OVERLAPPABLE #-} (ApplyFunc m f) => ApplyFunc m ([Integer] -> f) where - applyFunc f args _ [] = throwBot $ EvaluationException ("incorrect number of arguments to function. got " <> show dif <> ", expected " <> show args) [] - where - dif = args - getArgs f applyFunc f args bs ((LIList x) : xs) = applyFunc (f x) args bs xs - applyFunc _ _ _ (_ : _) = throwBot $ EvaluationException "incorrect type given to function. expected a list, got an integer" [] + applyFunc _ args _ [] = throwBot $ EvaluationException ("incorrect number of arguments to function. got " <> show dif <> ", expected " <> show args) [] + where + dif = args - getArgs (Proxy :: Proxy f) + applyFunc _ _ _ _ = throwBot $ EvaluationException "incorrect type given to function. expected a list, got an integer" [] -- | Simple type family that gets the return type of whatever function or value -- is given From a78e5a5ef3ffeb8806669ff81f79a0f9324fff3d Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sun, 23 Jan 2022 01:17:09 +0000 Subject: [PATCH 14/96] further worked on making slash commands as painless as possible --- src/Tablebot/Handler.hs | 17 +++-- src/Tablebot/Internal/Handler/Command.hs | 13 +++- src/Tablebot/Plugins/Roll/Plugin.hs | 96 +++++++++--------------- src/Tablebot/Utility/Discord.hs | 22 ++++-- src/Tablebot/Utility/SmartParser.hs | 77 ++++++++++++++++--- src/Tablebot/Utility/Utils.hs | 4 + 6 files changed, 143 insertions(+), 86 deletions(-) diff --git a/src/Tablebot/Handler.hs b/src/Tablebot/Handler.hs index b90e21ad..b3c08a2f 100644 --- a/src/Tablebot/Handler.hs +++ b/src/Tablebot/Handler.hs @@ -18,7 +18,7 @@ where import Control.Concurrent (MVar) import Control.Monad (unless) -import Control.Monad.Exception +import Control.Monad.Exception (MonadException (catch)) import Control.Monad.IO.Class (MonadIO (liftIO)) import Control.Monad.Reader (ReaderT, ask, lift, runReaderT) import Data.Pool (Pool) @@ -40,9 +40,13 @@ import Tablebot.Internal.Handler.Event ) import Tablebot.Internal.Plugins (changeAction) import Tablebot.Internal.Types -import Tablebot.Utility.Discord (sendEmbedMessage) -import Tablebot.Utility.Exception -import Tablebot.Utility.Types (TablebotCache) +import Tablebot.Utility.Discord (interactionResponseCustomMessage, sendEmbedMessage) +import Tablebot.Utility.Exception (BotException, embedError) +import Tablebot.Utility.Types + ( MessageDetails (messageDetailsEmbeds), + TablebotCache, + messageJustText, + ) import UnliftIO.Concurrent ( ThreadId, forkIO, @@ -72,12 +76,13 @@ eventHandler pl prefix = \case -- Similar with MessageReactionRemoveEmoji (removes all of one type). MessageReactionRemoveAll _cid _mid -> pure () MessageReactionRemoveEmoji _rri -> pure () - InteractionCreate i@InteractionComponent {} -> parseInteractionRecvComponent (compiledOnComponentInteractionRecvs pl) i - InteractionCreate i@InteractionApplicationCommand {} -> parseInteractionRecvApplicationCommand i + InteractionCreate i@InteractionComponent {} -> parseInteractionRecvComponent (compiledOnComponentInteractionRecvs pl) i `interactionErrorCatch` i + InteractionCreate i@InteractionApplicationCommand {} -> parseInteractionRecvApplicationCommand i `interactionErrorCatch` i -- TODO: add application command autocomplete as an option e -> parseOther (compiledOtherEvents pl) e where ifNotBot m = unless (userIsBot (messageAuthor m)) + interactionErrorCatch action i = action `catch` (\e -> changeAction () . interactionResponseCustomMessage i $ (messageJustText "") {messageDetailsEmbeds = Just [embedError (e :: BotException)]}) -- | @runCron@ takes an individual @CronJob@ and runs it in a separate thread. -- The @ThreadId@ is returned so it can be killed later. diff --git a/src/Tablebot/Internal/Handler/Command.hs b/src/Tablebot/Internal/Handler/Command.hs index 89ef72ad..6075d9f5 100644 --- a/src/Tablebot/Internal/Handler/Command.hs +++ b/src/Tablebot/Internal/Handler/Command.hs @@ -13,7 +13,7 @@ module Tablebot.Internal.Handler.Command ( parseNewMessage, parseCommands, parseInlineCommands, - makeBundleReadable, + parseValue, ) where @@ -26,9 +26,9 @@ import Discord.Types (Message (messageContent)) import Tablebot.Internal.Plugins (changeAction) import Tablebot.Internal.Types import Tablebot.Utility.Discord (sendEmbedMessage) -import Tablebot.Utility.Exception (BotException (ParserException), embedError) +import Tablebot.Utility.Exception (BotException (ParserException), embedError, throwBot) import Tablebot.Utility.Parser (skipSpace1, space, word) -import Tablebot.Utility.Types (Parser) +import Tablebot.Utility.Types (EnvDatabaseDiscord, Parser) import Text.Megaparsec import qualified UnliftIO.Exception as UIOE (tryAny) @@ -128,3 +128,10 @@ parseInlineCommands cs m = mapM_ (fromResult . (\cic -> parse (inlineCommandPars where fromResult (Right p) = UIOE.tryAny (p m) fromResult _ = return $ return () + +parseValue :: Parser a -> Text -> EnvDatabaseDiscord s a +parseValue par t = case parse par "" t of + Right p -> return p + Left e -> + let (errs, title) = makeBundleReadable e + in throwBot $ ParserException title $ "```\n" ++ errorBundlePretty errs ++ "```" diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index abc8c00a..febaf41f 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -15,6 +15,11 @@ import Data.Maybe (fromMaybe) import Data.Text (Text, intercalate, pack, replicate, unpack) import qualified Data.Text as T import Discord.Interactions + ( Interaction (..), + InteractionCallbackDataFlag (..), + InteractionCallbackDataFlags (..), + InteractionDataComponent (..), + ) import Discord.Types ( ButtonStyle (ButtonStyleSecondary), ComponentActionRow (ComponentActionRowButton), @@ -23,21 +28,22 @@ import Discord.Types GuildMember (memberUser), Message (messageAuthor), User (userId), + UserId, ) -import Tablebot.Internal.Handler.Command (makeBundleReadable) +import Tablebot.Internal.Handler.Command (parseValue) import Tablebot.Plugins.Roll.Dice import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Utility import Tablebot.Utility.Discord (interactionResponseComponentsUpdateMessage, interactionResponseCustomMessage, sendCustomMessage, toMention, toMention') -import Tablebot.Utility.Exception (BotException (InteractionException, ParserException), throwBot) +import Tablebot.Utility.Exception (BotException (InteractionException), throwBot) import Tablebot.Utility.Parser (inlineCommandHelper) -import Tablebot.Utility.SmartParser (PComm (parseComm), Quoted (Qu, quote), pars) -import Text.Megaparsec (MonadParsec (try), choice, errorBundlePretty, parse, ()) +import Tablebot.Utility.SmartParser +import Text.Megaparsec (MonadParsec (eof, try), choice, parse, ()) import Text.RawString.QQ (r) -- | The basic execution function for rolling dice. Both the expression and message are -- optional. If the expression is not given, then the default roll is used. -rollDice'' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> Text -> DatabaseDiscord Text +rollDice'' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> UserId -> DatabaseDiscord Text rollDice'' e' t u = do let e = fromMaybe (Right defaultRoll) e' (vs, ss) <- case e of @@ -49,7 +55,7 @@ rollDice'' e' t u = do else return (makeMsg (simplify vs) (prettyShow e <> " `[could not display rolls]`")) where dsc = maybe ": " (\(Qu t') -> " \"" <> t' <> "\": ") t - baseMsg = u <> " rolled" <> dsc + baseMsg = toMention' u <> " rolled" <> dsc makeLine (i, s) = pack (show i) <> Data.Text.replicate (max 0 (6 - length (show i))) " " <> " ⟵ " <> s makeMsg (Right v) s = baseMsg <> s <> ".\nOutput: " <> pack (show v) makeMsg (Left []) _ = baseMsg <> "No output." @@ -61,20 +67,25 @@ rollDice'' e' t u = do countFormatting s = (`div` 4) $ T.foldr (\c cf -> cf + (2 * fromEnum (c == '`')) + fromEnum (c `elem` ['~', '_', '*'])) 0 s rollDice' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> Message -> DatabaseDiscord MessageDetails -rollDice' e t m = do - (msg, buttons) <- getMessagePieces e t (toMention $ messageAuthor m) - return ((messageJustText msg) {messageDetailsComponents = buttons}) +rollDice' e t m = getMessagePieces e t (userId $ messageAuthor m) + +rollSlashCommandFunction :: Labelled "expression" "what's being evaluated" (Maybe Text) -> Labelled "quote" "associated message" (Maybe (Quoted Text)) -> ParseUserId -> DatabaseDiscord MessageDetails +rollSlashCommandFunction (Labelled mt) (Labelled qt) (ParseUserId uid) = do + lve <- mapM (parseValue (pars <* eof)) mt + getMessagePieces lve qt uid -getMessagePieces :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> Text -> DatabaseDiscord (Text, Maybe [ComponentActionRow]) +getMessagePieces :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> UserId -> DatabaseDiscord MessageDetails getMessagePieces e t u = do msg <- rollDice'' e t u return - ( msg, - Just - [ ComponentActionRowButton - [ ComponentButton ((("roll`" <> u) `appendIf` (prettyShow <$> e)) `appendIf` (quote <$> t)) False ButtonStyleSecondary "Reroll" (Just (Emoji (Just 0) "🎲" Nothing Nothing Nothing (Just False))) - ] - ] + ( (messageJustText msg) + { messageDetailsComponents = + Just + [ ComponentActionRowButton + [ ComponentButton ((("roll`" <> pack (show u)) `appendIf` (prettyShow <$> e)) `appendIf` (quote <$> t)) False ButtonStyleSecondary "Reroll" (Just (Emoji (Just 0) "🎲" Nothing Nothing Nothing (Just False))) + ] + ] + } ) where appendIf t' Nothing = t' <> "`" @@ -86,66 +97,31 @@ rerollInteraction i@InteractionComponent {interactionDataComponent = Just (Inter | maybe True (\u -> toMention u /= opts !! 1) getUser = interactionResponseCustomMessage i ((messageJustText "Hey, that isn't your button to press!") {messageDetailsFlags = Just $ InteractionCallbackDataFlags [InteractionCallbackDataFlagEphermeral]}) | otherwise = case opts of [_, uid, "", ""] -> do - (msg, button) <- getMessagePieces Nothing Nothing uid - interactionResponseComponentsUpdateMessage i ((messageJustText msg) {messageDetailsComponents = button}) + msgdetails <- getMessagePieces Nothing Nothing (read $ unpack uid) + interactionResponseComponentsUpdateMessage i msgdetails [_, uid, "", qt] -> do - (msg, button) <- getMessagePieces Nothing (Just (Qu qt)) uid - interactionResponseComponentsUpdateMessage i ((messageJustText msg) {messageDetailsComponents = button}) + msgdetails <- getMessagePieces Nothing (Just (Qu qt)) (read $ unpack uid) + interactionResponseComponentsUpdateMessage i msgdetails [_, uid, e, ""] -> do let e' = parse pars "" e case e' of Left _ -> throwBot $ InteractionException "could not process button click" Right e'' -> do - (msg, button) <- getMessagePieces (Just e'') Nothing uid - interactionResponseComponentsUpdateMessage i ((messageJustText msg) {messageDetailsComponents = button}) + msgdetails <- getMessagePieces (Just e'') Nothing (read $ unpack uid) + interactionResponseComponentsUpdateMessage i msgdetails [_, uid, e, qt] -> do let e' = parse pars "" e case e' of Left _ -> throwBot $ InteractionException "could not process button click" Right e'' -> do - (msg, button) <- getMessagePieces (Just e'') (Just (Qu qt)) uid - interactionResponseComponentsUpdateMessage i ((messageJustText msg) {messageDetailsComponents = button}) + msgdetails <- getMessagePieces (Just e'') (Just (Qu qt)) (read $ unpack uid) + interactionResponseComponentsUpdateMessage i msgdetails _ -> throwBot $ InteractionException "could not process button click" where opts = T.split (== '`') cid getUser = maybe (interactionUser i) memberUser (interactionMember i) rerollInteraction _ = return () -rollSlashCommandInteraction :: Interaction -> DatabaseDiscord () -rollSlashCommandInteraction i@InteractionApplicationCommand {interactionDataApplicationCommand = Just InteractionDataApplicationCommandChatInput {interactionDataApplicationCommandName = "roll", interactionDataApplicationCommandOptions = opts}} = do - e <- mapM parseExpr expr - (msg, buttons) <- getMessagePieces e (Qu <$> qt) (toMention' getUser) - interactionResponseCustomMessage i ((messageJustText msg) {messageDetailsComponents = buttons}) - where - findWhere s = opts >>= \(InteractionDataApplicationCommandOptionsValues values) -> lookup s $ (\v -> (interactionDataApplicationCommandOptionValueName v, interactionDataApplicationCommandOptionValueValue v)) <$> values - expr = findWhere "expression" >>= getText - qt = findWhere "quote" >>= getText - getText (ApplicationCommandInteractionDataValueString t) = Just t - getText _ = Nothing - -- TODO: work out why this exception handling isn't working - parseExpr expr' = case parse pars "" expr' of - Right p -> return p - Left e -> - let (errs, title) = makeBundleReadable e - in throwBot $ ParserException title $ "```\n" ++ errorBundlePretty errs ++ "```" - getUser = maybe 0 userId $ maybe (interactionUser i) memberUser (interactionMember i) -rollSlashCommandInteraction _ = return () - --- TODO: tie together creating the application command and the handler for it so that they cannot be separated --- TODO: comment -rollSlashCommand :: Maybe CreateApplicationCommand -rollSlashCommand = - createApplicationCommandChatInput "roll" "roll some dice with a description" >>= \cac -> - return $ - cac - { createApplicationCommandOptions = - Just $ - ApplicationCommandOptionsValues - [ ApplicationCommandOptionValueString "expression" "What expression is being evaluated (list or integer)" Nothing Nothing Nothing, - ApplicationCommandOptionValueString "quote" "What message is associated with this roll" Nothing Nothing Nothing - ] - } - -- | Manually creating parser for this command, since SmartCommand doesn't work fully for -- multiple Maybe values rollDiceParser :: Parser (Message -> DatabaseDiscord ()) @@ -237,5 +213,5 @@ rollPlugin = onComponentInteractionRecvs = [ InteractionRecv rerollInteraction ], - applicationCommands = [(rollSlashCommand, InteractionRecv rollSlashCommandInteraction)] + applicationCommands = [makeApplicationCommandPair "roll" "roll some dice with a description" rollSlashCommandFunction] } diff --git a/src/Tablebot/Utility/Discord.hs b/src/Tablebot/Utility/Discord.hs index f6bc4d6a..18d0bbe5 100644 --- a/src/Tablebot/Utility/Discord.hs +++ b/src/Tablebot/Utility/Discord.hs @@ -15,6 +15,7 @@ module Tablebot.Utility.Discord sendReplyMessage, sendCustomReplyMessage, sendEmbedMessage, + sendChannelEmbedMessage, reactToMessage, findGuild, findEmoji, @@ -52,21 +53,20 @@ where import Control.Monad.Exception (MonadException (throw)) import Data.Char (isDigit) import Data.Foldable (msum) -import Data.List +import Data.List ((\\)) import Data.Map.Strict (keys) import Data.Maybe (listToMaybe) import Data.String (IsString (fromString)) import Data.Text (Text, pack, unpack) import Data.Time.Clock (nominalDiffTimeToSeconds) import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) -import Discord (DiscordHandler, RestCallErrorCode, readCache, restCall) +import Discord (Cache (cacheGuilds), DiscordHandler, RestCallErrorCode, readCache, restCall) import Discord.Interactions -import Discord.Internal.Gateway.Cache import qualified Discord.Requests as R import Discord.Types import GHC.Word (Word64) -import Tablebot.Internal.Cache -import Tablebot.Internal.Embed +import Tablebot.Internal.Cache (fillEmojiCache, lookupEmojiCache) +import Tablebot.Internal.Embed (Embeddable (..), TablebotEmbedRequest (TablebotEmbedRequest)) import Tablebot.Utility (EnvDatabaseDiscord, MessageDetails, convertMessageFormatBasic, convertMessageFormatInteraction, liftDiscord, messageJustText) import Tablebot.Utility.Exception (BotException (..)) @@ -153,8 +153,16 @@ sendEmbedMessage :: Text -> e -> EnvDatabaseDiscord s () -sendEmbedMessage m t e = do - res <- liftDiscord . restCall $ TablebotEmbedRequest (messageChannelId m) t (asEmbed e) +sendEmbedMessage m = sendChannelEmbedMessage (messageChannelId m) + +sendChannelEmbedMessage :: + Embeddable e => + ChannelId -> + Text -> + e -> + EnvDatabaseDiscord s () +sendChannelEmbedMessage cid t e = do + res <- liftDiscord . restCall $ TablebotEmbedRequest cid t (asEmbed e) case res of Left _ -> throw $ MessageSendException "Failed to send message." Right _ -> return () diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index 3b1ba79c..a227b205 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -18,14 +18,19 @@ import Data.Proxy (Proxy (..)) import Data.String (IsString (fromString)) import Data.Text (Text, pack) import Discord.Interactions -import Discord.Types (Message) +import Discord.Types import GHC.OldList (find) -import GHC.TypeLits +import GHC.TypeLits (KnownSymbol, Symbol, symbolVal) import Tablebot.Utility.Discord (interactionResponseCustomMessage, sendCustomMessage) import Tablebot.Utility.Exception (BotException (InteractionException), catchBot, throwBot) import Tablebot.Utility.Parser -import Tablebot.Utility.Types (EnvDatabaseDiscord, MessageDetails, Parser) -import Text.Megaparsec +import Tablebot.Utility.Types + ( EnvDatabaseDiscord, + EnvInteractionRecv (InteractionRecv), + MessageDetails, + Parser, + ) +import Text.Megaparsec (MonadParsec (eof, try), chunk, many, optional, (), (<|>)) -- | @PComm@ defines function types that we can automatically turn into parsers -- by composing a parser per input of the function provided. @@ -54,12 +59,25 @@ instance {-# OVERLAPPING #-} PComm (Message -> EnvDatabaseDiscord s MessageDetai -- this <- pars @a -- parseComm (comm this) +instance (PComm (Message -> as) s) => PComm (Message -> Message -> as) s where + parseComm comm = parseComm (\m -> comm m m) + +instance (CanParse a, PComm (Message -> as) s) => PComm (Message -> a -> as) s where + parseComm comm = do + this <- parsThenMoveToNext @a + parseComm (`comm` this) + -- Recursive case is to parse the domain of the function type, then the rest. instance {-# OVERLAPPABLE #-} (CanParse a, PComm as s) => PComm (a -> as) s where parseComm comm = do this <- parsThenMoveToNext @a parseComm (comm this) +-- Recursive case is to parse the domain of the function type, then the rest. +instance {-# OVERLAPPABLE #-} (PComm (Message -> as) s) => PComm (ParseUserId -> as) s where + parseComm comm = do + parseComm $ \m -> comm (ParseUserId (userId $ messageAuthor m)) + -- | @CanParse@ defines types from which we can generate parsers. class CanParse a where pars :: Parser a @@ -179,10 +197,7 @@ newtype RestOfInput1 a = ROI1 a instance IsString a => CanParse (RestOfInput1 a) where pars = ROI1 . fromString <$> untilEnd1 --- | @noArguments@ is a type-specific alias for @parseComm@ for commands that --- have no arguments (thus making it extremely clear). -noArguments :: (Message -> EnvDatabaseDiscord d ()) -> Parser (Message -> EnvDatabaseDiscord d ()) -noArguments = parseComm +newtype ParseUserId = ParseUserId UserId -- | Labelled value for use with smart commands. newtype Labelled (name :: Symbol) (desc :: Symbol) a = Labelled a @@ -194,6 +209,29 @@ labelValue = Labelled @n @d getLabelValues :: forall n d a. (KnownSymbol n, KnownSymbol d) => Proxy (Labelled n d a) -> (Text, Text) getLabelValues _ = (pack (symbolVal (Proxy :: Proxy n)), pack (symbolVal (Proxy :: Proxy d))) +instance (CanParse a) => CanParse (Labelled n d a) where + pars = labelValue <$> pars + +-- | @noArguments@ is a type-specific alias for @parseComm@ for commands that +-- have no arguments (thus making it extremely clear). +noArguments :: (Message -> EnvDatabaseDiscord d ()) -> Parser (Message -> EnvDatabaseDiscord d ()) +noArguments = parseComm + +-------------------------------------------------------------------------------- +-- Interactions stuff +---- + +makeApplicationCommandPair :: forall t s. (MakeAppComm t, ProcessAppComm t s) => Text -> Text -> t -> (Maybe CreateApplicationCommand, EnvInteractionRecv s) +makeApplicationCommandPair name desc f = (makeSlashCommand name desc (Proxy :: Proxy t), InteractionRecv (processAppComm f)) + +makeSlashCommand :: (MakeAppComm t) => Text -> Text -> Proxy t -> Maybe CreateApplicationCommand +makeSlashCommand name desc p = + createApplicationCommandChatInput name desc >>= \cac -> + return $ + cac + { createApplicationCommandOptions = Just $ ApplicationCommandOptionsValues $ makeAppComm p + } + class MakeAppComm commandty where makeAppComm :: Proxy commandty -> [ApplicationCommandOptionValue] @@ -204,6 +242,9 @@ instance {-# OVERLAPPING #-} MakeAppComm (EnvDatabaseDiscord s MessageDetails) w instance {-# OVERLAPPABLE #-} (MakeAppComm mac, MakeAppCommArg ty) => MakeAppComm (ty -> mac) where makeAppComm _ = makeAppCommArg (Proxy :: Proxy ty) : makeAppComm (Proxy :: Proxy mac) +instance {-# OVERLAPPABLE #-} (MakeAppComm mac) => MakeAppComm (ParseUserId -> mac) where + makeAppComm _ = makeAppComm (Proxy :: Proxy mac) + class MakeAppCommArg commandty where makeAppCommArg :: Proxy commandty -> ApplicationCommandOptionValue @@ -218,10 +259,14 @@ instance (KnownSymbol name, KnownSymbol desc, MakeAppCommArg (Labelled name desc { applicationCommandOptionValueRequired = Just False } +instance (KnownSymbol name, KnownSymbol desc, MakeAppCommArg (Labelled name desc t)) => MakeAppCommArg (Labelled name desc (Quoted t)) where + makeAppCommArg _ = makeAppCommArg (Proxy :: Proxy (Labelled name desc t)) + -- As a base case, send the message produced class ProcessAppComm commandty s where processAppComm :: commandty -> Interaction -> EnvDatabaseDiscord s () + processAppComm _ _ = throwBot $ InteractionException "could not process args to application command" instance {-# OVERLAPPING #-} ProcessAppComm (EnvDatabaseDiscord s MessageDetails) s where processAppComm comm i = comm >>= interactionResponseCustomMessage i @@ -232,6 +277,15 @@ instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s, ProcessAppCommArg ty s) => processAppComm (comm t) i processAppComm _ _ = throwBot $ InteractionException "could not process args to application command" +instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s) => ProcessAppComm (ParseUserId -> pac) s where + processAppComm comm i@InteractionApplicationCommand {} = + case getUser of + Nothing -> throwBot $ InteractionException "could not process args to application command" + Just uid -> processAppComm (comm (ParseUserId uid)) i + where + getUser = userId <$> maybe (interactionUser i) memberUser (interactionMember i) + processAppComm _ _ = throwBot $ InteractionException "could not process args to application command" + class ProcessAppCommArg t s where processAppCommArg :: [InteractionDataApplicationCommandOptionValue] -> EnvDatabaseDiscord s t @@ -243,9 +297,12 @@ instance (KnownSymbol name) => ProcessAppCommArg (Labelled name desc Text) s whe Just (ApplicationCommandInteractionDataValueString t) -> return $ labelValue t _ -> throwBot $ InteractionException "could not find required parameter" -instance (KnownSymbol name, ProcessAppCommArg (Labelled name description t) s) => ProcessAppCommArg (Labelled name description (Maybe t)) s where +instance (KnownSymbol name, KnownSymbol desc, ProcessAppCommArg (Labelled name desc t) s) => ProcessAppCommArg (Labelled name desc (Quoted t)) s where + processAppCommArg is = processAppCommArg @(Labelled name desc t) is >>= \(Labelled a) -> return (labelValue (Qu a)) + +instance (KnownSymbol name, ProcessAppCommArg (Labelled name desc t) s) => ProcessAppCommArg (Labelled name desc (Maybe t)) s where processAppCommArg is = do - let result = processAppCommArg is :: EnvDatabaseDiscord s (Labelled name description t) + let result = processAppCommArg is :: EnvDatabaseDiscord s (Labelled name desc t) ( do (Labelled l) <- result return (labelValue (Just l)) diff --git a/src/Tablebot/Utility/Utils.hs b/src/Tablebot/Utility/Utils.hs index b402b4a3..3d0e2582 100644 --- a/src/Tablebot/Utility/Utils.hs +++ b/src/Tablebot/Utility/Utils.hs @@ -10,6 +10,7 @@ module Tablebot.Utility.Utils where import Control.Monad (when) +import Data.Proxy (Proxy (Proxy)) import Data.Text (Text, filter, toLower) import Data.Text.ICU.Char (Bool_ (Diacritic), property) import Data.Text.ICU.Normalize (NormalizationMode (NFD), normalize) @@ -47,3 +48,6 @@ standardise x = filter (not . property Diacritic) normalizedText -- the empty Text. maybeEmptyPrepend :: Text -> Maybe Text -> Text maybeEmptyPrepend s = maybe "" (s <>) + +mkProxy :: forall a. a -> Proxy a +mkProxy _ = Proxy :: Proxy a From f136175d2ffaa2b2f98c369e40e7d4bc2003c1e0 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sun, 23 Jan 2022 14:00:33 +0000 Subject: [PATCH 15/96] renamed and moved some stuff around --- src/Tablebot/Handler.hs | 4 +- src/Tablebot/Plugins/Roll/Dice.hs | 4 +- src/Tablebot/Plugins/Roll/Dice/DiceEval.hs | 179 ++++++++++----------- src/Tablebot/Plugins/Roll/Plugin.hs | 8 +- src/Tablebot/Utility/Discord.hs | 4 +- src/Tablebot/Utility/Parser.hs | 11 ++ src/Tablebot/Utility/SmartParser.hs | 3 + src/Tablebot/Utility/Types.hs | 4 +- stack.yaml | 2 +- 9 files changed, 113 insertions(+), 106 deletions(-) diff --git a/src/Tablebot/Handler.hs b/src/Tablebot/Handler.hs index b3c08a2f..982c0479 100644 --- a/src/Tablebot/Handler.hs +++ b/src/Tablebot/Handler.hs @@ -45,7 +45,7 @@ import Tablebot.Utility.Exception (BotException, embedError) import Tablebot.Utility.Types ( MessageDetails (messageDetailsEmbeds), TablebotCache, - messageJustText, + messageDetailsBasic, ) import UnliftIO.Concurrent ( ThreadId, @@ -82,7 +82,7 @@ eventHandler pl prefix = \case e -> parseOther (compiledOtherEvents pl) e where ifNotBot m = unless (userIsBot (messageAuthor m)) - interactionErrorCatch action i = action `catch` (\e -> changeAction () . interactionResponseCustomMessage i $ (messageJustText "") {messageDetailsEmbeds = Just [embedError (e :: BotException)]}) + interactionErrorCatch action i = action `catch` (\e -> changeAction () . interactionResponseCustomMessage i $ (messageDetailsBasic "") {messageDetailsEmbeds = Just [embedError (e :: BotException)]}) -- | @runCron@ takes an individual @CronJob@ and runs it in a separate thread. -- The @ThreadId@ is returned so it can be killed later. diff --git a/src/Tablebot/Plugins/Roll/Dice.hs b/src/Tablebot/Plugins/Roll/Dice.hs index 06a4b0f8..3f44d9f8 100644 --- a/src/Tablebot/Plugins/Roll/Dice.hs +++ b/src/Tablebot/Plugins/Roll/Dice.hs @@ -55,7 +55,7 @@ -- ords (AdvancedOrdering and NumBase) - representing a more complex ordering operation than a basic `Ordering`, when compared to a `NumBase` -- argv (ArgValue) - representing an argument to a function -- funcBasics - a generic regex representation for a general function parser -module Tablebot.Plugins.Roll.Dice (evalInteger, evalList, ListValues (..), defaultRoll, PrettyShow (prettyShow), integerFunctionsList, listFunctionsList, Converter (promote)) where +module Tablebot.Plugins.Roll.Dice (evalInteger, evalList, ListValues (..), defaultRoll, ParseShow (parseShow), integerFunctionsList, listFunctionsList, Converter (promote)) where import Tablebot.Plugins.Roll.Dice.DiceData ( Converter (promote), @@ -64,7 +64,7 @@ import Tablebot.Plugins.Roll.Dice.DiceData ListValues (..), NumBase (Value), ) -import Tablebot.Plugins.Roll.Dice.DiceEval (PrettyShow (prettyShow), evalInteger, evalList) +import Tablebot.Plugins.Roll.Dice.DiceEval (ParseShow (parseShow), evalInteger, evalList) import Tablebot.Plugins.Roll.Dice.DiceFunctions (integerFunctionsList, listFunctionsList) import Tablebot.Plugins.Roll.Dice.DiceParsing () diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index 74cf08e4..9c6c7f51 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-orphans #-} + -- | -- Module : Tablebot.Plugins.Roll.Dice.DiceEval -- Description : How to evaluate dice and expressions @@ -8,7 +10,7 @@ -- -- Functions, type classes, and other utilities to evaluate dice values and -- expressions. -module Tablebot.Plugins.Roll.Dice.DiceEval (PrettyShow (prettyShow), evalList, evalInteger) where +module Tablebot.Plugins.Roll.Dice.DiceEval (ParseShow (parseShow), evalList, evalInteger) where import Control.Monad (when) import Control.Monad.Exception (MonadException) @@ -24,6 +26,7 @@ import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceFunctions (FuncInfoBase (..), ListInteger (..)) import Tablebot.Utility.Discord (Format (..), formatInput, formatText) import Tablebot.Utility.Exception (BotException (EvaluationException), catchBot, throwBot) +import Tablebot.Utility.Parser (ParseShow (parseShow)) import Tablebot.Utility.Random (chooseOne) -- | A wrapper type to differentiate between the RNGCount and other Integers. @@ -54,14 +57,14 @@ evaluationException nm locs = throwBot $ EvaluationException (unpack nm) (unpack -- | Given a list expression, evaluate it, getting the pretty printed string and -- the value of the result. -evalList :: (IOEvalList a, PrettyShow a) => a -> IO ([(Integer, Text)], Text) +evalList :: (IOEvalList a, ParseShow a) => a -> IO ([(Integer, Text)], Text) evalList a = do (is, ss, _) <- evalShowL (RNGCount 0) a - return (is, fromMaybe (prettyShow a) ss) + return (is, fromMaybe (parseShow a) ss) -- | Given an integer expression, evaluate it, getting the pretty printed string -- and the value of the result. -evalInteger :: (IOEval a, PrettyShow a) => a -> IO (Integer, Text) +evalInteger :: (IOEval a, ParseShow a) => a -> IO (Integer, Text) evalInteger a = do (is, ss, _) <- evalShow (RNGCount 0) a return (is, ss) @@ -69,7 +72,7 @@ evalInteger a = do -- | Utility function to display dice. -- -- The tuple of integers denotes what the critvalues of this dice value are. The --- `a` denotes the value that is being printed, and needs to have `PrettyShow` +-- `a` denotes the value that is being printed, and needs to have `ParseShow` -- defined for it. -- -- Finally, the list of tuples denotes all the values that the `a` value has @@ -77,9 +80,9 @@ evalInteger a = do -- as normal. If the value is `Just False`, the value has been rerolled over, -- and is displayed crossed out. If the value is `Just True`, the value has been -- dropped, and the number is crossed out and underlined. -dieShow :: (PrettyShow a, MonadException m) => Maybe (Integer, Integer) -> a -> [(Integer, Maybe Bool)] -> m Text -dieShow _ a [] = evaluationException "tried to show empty set of results" [prettyShow a] -dieShow lchc d ls = return $ prettyShow d <> " [" <> intercalate ", " adjustList <> "]" +dieShow :: (ParseShow a, MonadException m) => Maybe (Integer, Integer) -> a -> [(Integer, Maybe Bool)] -> m Text +dieShow _ a [] = evaluationException "tried to show empty set of results" [parseShow a] +dieShow lchc d ls = return $ parseShow d <> " [" <> intercalate ", " adjustList <> "]" where toCrit = pack @@ -97,14 +100,14 @@ dieShow lchc d ls = return $ prettyShow d <> " [" <> intercalate ", " adjustList -- | Evaluate a series of values, combining the text output into a comma -- separated list. -evalShowList :: (IOEval a, PrettyShow a) => RNGCount -> [a] -> IO ([Integer], Text, RNGCount) +evalShowList :: (IOEval a, ParseShow a) => RNGCount -> [a] -> IO ([Integer], Text, RNGCount) evalShowList rngCount as = do (vs, rngCount') <- evalShowList' rngCount as let (is, ts) = unzip vs return (is, intercalate ", " ts, rngCount') -- | Evaluate a series of values, combining the text output a list. -evalShowList' :: (IOEval a, PrettyShow a) => RNGCount -> [a] -> IO ([(Integer, Text)], RNGCount) +evalShowList' :: (IOEval a, ParseShow a) => RNGCount -> [a] -> IO ([(Integer, Text)], RNGCount) evalShowList' = evalShowList'' evalShow -- | Evaluate (using a custom evaluator function) a series of values, getting @@ -135,10 +138,10 @@ class IOEvalList a where -- it took. If the `a` value is a dice value, the values of the dice should be -- displayed. This function adds the current location to the exception -- callstack. - evalShowL :: PrettyShow a => RNGCount -> a -> IO ([(Integer, Text)], Maybe Text, RNGCount) - evalShowL rngCount a = propagateException (prettyShow a) (evalShowL' rngCount a) + evalShowL :: ParseShow a => RNGCount -> a -> IO ([(Integer, Text)], Maybe Text, RNGCount) + evalShowL rngCount a = propagateException (parseShow a) (evalShowL' rngCount a) - evalShowL' :: PrettyShow a => RNGCount -> a -> IO ([(Integer, Text)], Maybe Text, RNGCount) + evalShowL' :: ParseShow a => RNGCount -> a -> IO ([(Integer, Text)], Maybe Text, RNGCount) evalArgValue :: RNGCount -> ArgValue -> IO (ListInteger, RNGCount) evalArgValue rngCount (AVExpr e) = do @@ -169,10 +172,10 @@ class IOEval a where -- value, and the number of RNG calls it took. If the `a` value is a dice -- value, the values of the dice should be displayed. This function adds -- the current location to the exception callstack. - evalShow :: PrettyShow a => RNGCount -> a -> IO (Integer, Text, RNGCount) - evalShow rngCount a = propagateException (prettyShow a) (evalShow' rngCount a) + evalShow :: ParseShow a => RNGCount -> a -> IO (Integer, Text, RNGCount) + evalShow rngCount a = propagateException (parseShow a) (evalShow' rngCount a) - evalShow' :: PrettyShow a => RNGCount -> a -> IO (Integer, Text, RNGCount) + evalShow' :: ParseShow a => RNGCount -> a -> IO (Integer, Text, RNGCount) instance IOEval Base where evalShow' rngCount (NBase nb) = evalShow rngCount nb @@ -198,7 +201,7 @@ instance IOEval Die where evalShow' rngCount d@(Die b) = do (bound, _, rngCount') <- evalShow rngCount b if bound < 1 - then evaluationException ("Cannot roll a < 1 sided die (" <> formatText Code (prettyShow b) <> ")") [] + then evaluationException ("Cannot roll a < 1 sided die (" <> formatText Code (parseShow b) <> ")") [] else do i <- randomRIO (1, bound) ds <- dieShow Nothing d [(i, Nothing)] @@ -230,10 +233,10 @@ evalDieOp :: RNGCount -> Dice -> IO ([(NonEmpty Integer, Bool)], Maybe (Integer, evalDieOp rngCount (Dice b ds dopo) = do (nbDice, _, rngCountB) <- evalShow rngCount b if RNGCount nbDice > maximumRNG - then evaluationException ("tried to roll more than " <> formatInput Code (getRNGCount maximumRNG) <> " dice: " <> formatInput Code nbDice) [prettyShow b] + then evaluationException ("tried to roll more than " <> formatInput Code (getRNGCount maximumRNG) <> " dice: " <> formatInput Code nbDice) [parseShow b] else do if nbDice < 0 - then evaluationException ("tried to give a negative value to the number of dice: " <> formatInput Code nbDice) [prettyShow b] + then evaluationException ("tried to give a negative value to the number of dice: " <> formatInput Code nbDice) [parseShow b] else do (ds', rngCountCondense, crits) <- condenseDie rngCountB ds (rolls, _, rngCountRolls) <- evalShowList rngCountCondense (genericReplicate nbDice ds') @@ -333,7 +336,7 @@ evalDieOpHelpKD rngCount kd lh is = do -- Was previously its own type class that wouldn't work for evaluating Base values. -- | Utility function to evaluate a binary operator. -binOpHelp :: (IOEval a, IOEval b, PrettyShow a, PrettyShow b) => RNGCount -> a -> b -> Text -> (Integer -> Integer -> Integer) -> IO (Integer, Text, RNGCount) +binOpHelp :: (IOEval a, IOEval b, ParseShow a, ParseShow b) => RNGCount -> a -> b -> Text -> (Integer -> Integer -> Integer) -> IO (Integer, Text, RNGCount) binOpHelp rngCount a b opS op = do (a', a's, rngCount') <- evalShow rngCount a (b', b's, rngCount'') <- evalShow rngCount' b @@ -351,7 +354,7 @@ instance IOEval Term where (f', f's, rngCount') <- evalShow rngCount f (t', t's, rngCount'') <- evalShow rngCount' t if t' == 0 - then evaluationException "division by zero" [prettyShow t] + then evaluationException "division by zero" [parseShow t] else return (div f' t', f's <> " / " <> t's, rngCount'') instance IOEval Func where @@ -363,7 +366,7 @@ evaluateFunction :: RNGCount -> FuncInfoBase IO j -> [ArgValue] -> IO (j, Text, evaluateFunction rngCount fi exprs = do (exprs', rngCount') <- evalShowList'' (\r a -> evalArgValue r a >>= \(i, r') -> return (i, "", r')) rngCount exprs f <- funcInfoFunc fi (fst <$> exprs') - return (f, funcInfoName fi <> "(" <> intercalate ", " (prettyShow <$> exprs) <> ")", rngCount') + return (f, funcInfoName fi <> "(" <> intercalate ", " (parseShow <$> exprs) <> ")", rngCount') instance IOEval Negation where evalShow' rngCount (NoNeg expo) = evalShow rngCount expo @@ -376,7 +379,7 @@ instance IOEval Expo where evalShow' rngCount (Expo b expo) = do (expo', expo's, rngCount') <- evalShow rngCount expo if expo' < 0 - then evaluationException ("the exponent is negative: " <> formatInput Code expo') [prettyShow expo] + then evaluationException ("the exponent is negative: " <> formatInput Code expo') [parseShow expo] else do (b', b's, rngCount'') <- evalShow rngCount' b return (b' ^ expo', b's <> " ^ " <> expo's, rngCount'') @@ -388,80 +391,70 @@ instance IOEval NumBase where evalShow' rngCount (Value i) = return (i, pack (show i), rngCount) --- Pretty printing the AST --- The output from this should be parseable - --- | Type class to display an expression prettily (not neccessarily accurately). -class PrettyShow a where - -- | Print the given value prettily. - prettyShow :: a -> Text - -instance PrettyShow ArgValue where - prettyShow (AVExpr e) = prettyShow e - prettyShow (AVListValues lv) = prettyShow lv - -instance PrettyShow ListValues where - prettyShow (LVBase e) = prettyShow e - prettyShow (MultipleValues nb b) = prettyShow nb <> "#" <> prettyShow b - prettyShow (LVFunc s n) = funcInfoName s <> "(" <> intercalate "," (prettyShow <$> n) <> ")" - -instance PrettyShow ListValuesBase where - prettyShow (LVBList es) = "{" <> intercalate ", " (prettyShow <$> es) <> "}" - prettyShow (LVBParen p) = prettyShow p - -instance PrettyShow Expr where - prettyShow (Add t e) = prettyShow t <> " + " <> prettyShow e - prettyShow (Sub t e) = prettyShow t <> " - " <> prettyShow e - prettyShow (NoExpr t) = prettyShow t - -instance PrettyShow Term where - prettyShow (Multi f t) = prettyShow f <> " * " <> prettyShow t - prettyShow (Div f t) = prettyShow f <> " / " <> prettyShow t - prettyShow (NoTerm f) = prettyShow f - -instance PrettyShow Func where - prettyShow (Func s n) = funcInfoName s <> "(" <> intercalate "," (prettyShow <$> n) <> ")" - prettyShow (NoFunc b) = prettyShow b - -instance PrettyShow Negation where - prettyShow (Neg expo) = "-" <> prettyShow expo - prettyShow (NoNeg expo) = prettyShow expo - -instance PrettyShow Expo where - prettyShow (NoExpo b) = prettyShow b - prettyShow (Expo b expo) = prettyShow b <> " ^ " <> prettyShow expo - -instance PrettyShow NumBase where - prettyShow (NBParen p) = prettyShow p - prettyShow (Value i) = fromString $ show i - -instance (PrettyShow a) => PrettyShow (Paren a) where - prettyShow (Paren a) = "(" <> prettyShow a <> ")" - -instance PrettyShow Base where - prettyShow (NBase nb) = prettyShow nb - prettyShow (DiceBase dop) = prettyShow dop - -instance PrettyShow Die where - prettyShow (Die b) = "d" <> prettyShow b - prettyShow (CustomDie lv) = "d" <> prettyShow lv - -- prettyShow (CustomDie is) = "d{" <> intercalate ", " (prettyShow <$> is) <> "}" - prettyShow (LazyDie d) = "d!" <> T.tail (prettyShow d) - -instance PrettyShow Dice where - prettyShow (Dice b d dor) = prettyShow b <> prettyShow d <> helper' dor + +instance ParseShow ArgValue where + parseShow (AVExpr e) = parseShow e + parseShow (AVListValues lv) = parseShow lv + +instance ParseShow ListValues where + parseShow (LVBase e) = parseShow e + parseShow (MultipleValues nb b) = parseShow nb <> "#" <> parseShow b + parseShow (LVFunc s n) = funcInfoName s <> "(" <> intercalate "," (parseShow <$> n) <> ")" + +instance ParseShow ListValuesBase where + parseShow (LVBList es) = "{" <> intercalate ", " (parseShow <$> es) <> "}" + parseShow (LVBParen p) = parseShow p + +instance ParseShow Expr where + parseShow (Add t e) = parseShow t <> " + " <> parseShow e + parseShow (Sub t e) = parseShow t <> " - " <> parseShow e + parseShow (NoExpr t) = parseShow t + +instance ParseShow Term where + parseShow (Multi f t) = parseShow f <> " * " <> parseShow t + parseShow (Div f t) = parseShow f <> " / " <> parseShow t + parseShow (NoTerm f) = parseShow f + +instance ParseShow Func where + parseShow (Func s n) = funcInfoName s <> "(" <> intercalate "," (parseShow <$> n) <> ")" + parseShow (NoFunc b) = parseShow b + +instance ParseShow Negation where + parseShow (Neg expo) = "-" <> parseShow expo + parseShow (NoNeg expo) = parseShow expo + +instance ParseShow Expo where + parseShow (NoExpo b) = parseShow b + parseShow (Expo b expo) = parseShow b <> " ^ " <> parseShow expo + +instance ParseShow NumBase where + parseShow (NBParen p) = parseShow p + parseShow (Value i) = fromString $ show i + +instance (ParseShow a) => ParseShow (Paren a) where + parseShow (Paren a) = "(" <> parseShow a <> ")" + +instance ParseShow Base where + parseShow (NBase nb) = parseShow nb + parseShow (DiceBase dop) = parseShow dop + +instance ParseShow Die where + parseShow (Die b) = "d" <> parseShow b + parseShow (CustomDie lv) = "d" <> parseShow lv + -- parseShow (CustomDie is) = "d{" <> intercalate ", " (parseShow <$> is) <> "}" + parseShow (LazyDie d) = "d!" <> T.tail (parseShow d) + +instance ParseShow Dice where + parseShow (Dice b d dor) = parseShow b <> parseShow d <> helper' dor where fromOrdering ao = M.findWithDefault "??" ao $ snd advancedOrderingMapping - fromLHW (Where o i) = "w" <> fromOrdering o <> prettyShow i - fromLHW (Low i) = "l" <> prettyShow i - fromLHW (High i) = "h" <> prettyShow i + fromLHW (Where o i) = "w" <> fromOrdering o <> parseShow i + fromLHW (Low i) = "l" <> parseShow i + fromLHW (High i) = "h" <> parseShow i helper' Nothing = "" helper' (Just (DieOpRecur dopo' dor')) = helper dopo' <> helper' dor' helper (DieOpOptionLazy doo) = "!" <> helper doo - helper (Reroll True o i) = "ro" <> fromOrdering o <> prettyShow i - helper (Reroll False o i) = "rr" <> fromOrdering o <> prettyShow i + helper (Reroll True o i) = "ro" <> fromOrdering o <> parseShow i + helper (Reroll False o i) = "rr" <> fromOrdering o <> parseShow i helper (DieOpOptionKD Keep lhw) = "k" <> fromLHW lhw helper (DieOpOptionKD Drop lhw) = "d" <> fromLHW lhw - -instance (PrettyShow a, PrettyShow b) => PrettyShow (Either a b) where - prettyShow (Left a) = prettyShow a - prettyShow (Right b) = prettyShow b diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index febaf41f..821923e1 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -52,7 +52,7 @@ rollDice'' e' t u = do let msg = makeMsg vs ss if countFormatting msg < 199 then return msg - else return (makeMsg (simplify vs) (prettyShow e <> " `[could not display rolls]`")) + else return (makeMsg (simplify vs) (parseShow e <> " `[could not display rolls]`")) where dsc = maybe ": " (\(Qu t') -> " \"" <> t' <> "\": ") t baseMsg = toMention' u <> " rolled" <> dsc @@ -78,11 +78,11 @@ getMessagePieces :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> Use getMessagePieces e t u = do msg <- rollDice'' e t u return - ( (messageJustText msg) + ( (messageDetailsBasic msg) { messageDetailsComponents = Just [ ComponentActionRowButton - [ ComponentButton ((("roll`" <> pack (show u)) `appendIf` (prettyShow <$> e)) `appendIf` (quote <$> t)) False ButtonStyleSecondary "Reroll" (Just (Emoji (Just 0) "🎲" Nothing Nothing Nothing (Just False))) + [ ComponentButton ((("roll`" <> pack (show u)) `appendIf` (parseShow <$> e)) `appendIf` (quote <$> t)) False ButtonStyleSecondary "Reroll" (Just (Emoji (Just 0) "🎲" Nothing Nothing Nothing (Just False))) ] ] } @@ -94,7 +94,7 @@ getMessagePieces e t u = do rerollInteraction :: Interaction -> DatabaseDiscord () rerollInteraction i@InteractionComponent {interactionDataComponent = Just (InteractionDataComponentButton cid)} | length opts /= 4 = throwBot $ InteractionException "could not process button click" - | maybe True (\u -> toMention u /= opts !! 1) getUser = interactionResponseCustomMessage i ((messageJustText "Hey, that isn't your button to press!") {messageDetailsFlags = Just $ InteractionCallbackDataFlags [InteractionCallbackDataFlagEphermeral]}) + | maybe True (\u -> toMention u /= opts !! 1) getUser = interactionResponseCustomMessage i ((messageDetailsBasic "Hey, that isn't your button to press!") {messageDetailsFlags = Just $ InteractionCallbackDataFlags [InteractionCallbackDataFlagEphermeral]}) | otherwise = case opts of [_, uid, "", ""] -> do msgdetails <- getMessagePieces Nothing Nothing (read $ unpack uid) diff --git a/src/Tablebot/Utility/Discord.hs b/src/Tablebot/Utility/Discord.hs index 18d0bbe5..92a3bcd8 100644 --- a/src/Tablebot/Utility/Discord.hs +++ b/src/Tablebot/Utility/Discord.hs @@ -67,7 +67,7 @@ import Discord.Types import GHC.Word (Word64) import Tablebot.Internal.Cache (fillEmojiCache, lookupEmojiCache) import Tablebot.Internal.Embed (Embeddable (..), TablebotEmbedRequest (TablebotEmbedRequest)) -import Tablebot.Utility (EnvDatabaseDiscord, MessageDetails, convertMessageFormatBasic, convertMessageFormatInteraction, liftDiscord, messageJustText) +import Tablebot.Utility (EnvDatabaseDiscord, MessageDetails, convertMessageFormatBasic, convertMessageFormatInteraction, liftDiscord, messageDetailsBasic) import Tablebot.Utility.Exception (BotException (..)) -- | @sendMessage@ sends the input message @t@ in the same channel as message @@ -403,7 +403,7 @@ interactionResponseDeferUpdateMessage i = do -- | Respond to the given interaction with the given text. interactionResponseMessage :: Interaction -> Text -> EnvDatabaseDiscord s () -interactionResponseMessage i t = interactionResponseCustomMessage i (messageJustText t) +interactionResponseMessage i t = interactionResponseCustomMessage i (messageDetailsBasic t) -- | Respond to the given interaction with a custom messages object. interactionResponseCustomMessage :: Interaction -> MessageDetails -> EnvDatabaseDiscord s () diff --git a/src/Tablebot/Utility/Parser.hs b/src/Tablebot/Utility/Parser.hs index 49924bc1..cc5cd45b 100644 --- a/src/Tablebot/Utility/Parser.hs +++ b/src/Tablebot/Utility/Parser.hs @@ -192,3 +192,14 @@ parseCommaSeparated p = do parseCommaSeparated1 :: Parser a -> Parser [a] parseCommaSeparated1 p = do p >>= (\first' -> (first' :) <$> many (try (skipSpace *> char ',' *> skipSpace) *> p)) + +-- | Type class to display a value in a way that can be parsed. +-- +-- `Right a === parse (pars :: Parser a) "" (parseShow a)` +class ParseShow a where + -- | Represent the value + parseShow :: a -> Text + +instance (ParseShow a, ParseShow b) => ParseShow (Either a b) where + parseShow (Left a) = parseShow a + parseShow (Right b) = parseShow b diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index a227b205..7f2331fd 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -271,6 +271,9 @@ class ProcessAppComm commandty s where instance {-# OVERLAPPING #-} ProcessAppComm (EnvDatabaseDiscord s MessageDetails) s where processAppComm comm i = comm >>= interactionResponseCustomMessage i +instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s) => ProcessAppComm (Interaction -> pac) s where + processAppComm comm i = processAppComm (comm i) i + instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s, ProcessAppCommArg ty s) => ProcessAppComm (ty -> pac) s where processAppComm comm i@InteractionApplicationCommand {interactionDataApplicationCommand = Just InteractionDataApplicationCommandChatInput {interactionDataApplicationCommandOptions = (Just (InteractionDataApplicationCommandOptionsValues values))}} = do t <- processAppCommArg values diff --git a/src/Tablebot/Utility/Types.hs b/src/Tablebot/Utility/Types.hs index adc1c0e5..1c3e2131 100644 --- a/src/Tablebot/Utility/Types.hs +++ b/src/Tablebot/Utility/Types.hs @@ -350,8 +350,8 @@ plug name' = Pl name' (StartUp (return ())) [] [] [] [] [] [] [] [] [] [] [] envPlug :: Text -> StartUp d -> EnvPlugin d envPlug name' startup = Pl name' startup [] [] [] [] [] [] [] [] [] [] [] -messageJustText :: Text -> MessageDetails -messageJustText t = MessageDetails Nothing (Just t) Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing +messageDetailsBasic :: Text -> MessageDetails +messageDetailsBasic t = MessageDetails Nothing (Just t) Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing data MessageDetails = MessageDetails { messageDetailsTTS :: Maybe Bool, diff --git a/stack.yaml b/stack.yaml index 561fa921..17fb9a29 100644 --- a/stack.yaml +++ b/stack.yaml @@ -41,7 +41,7 @@ packages: # extra-deps: - git: https://github.com/L0neGamer/discord-haskell.git - commit: da2fde90f29e8540ab3c084f86c2f1f46dc25f9c + commit: 403ebc9cc0cf1f2367c0ad2677fcf2ad0c76e133 - emoji-0.1.0.2 - load-env-0.2.1.0 - megaparsec-9.0.1 From ac960ebb5ef8df61ec5f52ad6f0558c2c8992d8b Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sun, 23 Jan 2022 23:29:30 +0000 Subject: [PATCH 16/96] comandeering the smart parser to use with components --- src/Tablebot.hs | 7 ++-- src/Tablebot/Handler.hs | 11 ++--- src/Tablebot/Internal/Handler/Event.hs | 20 ++++----- src/Tablebot/Internal/Plugins.hs | 30 ++++++++------ src/Tablebot/Internal/Types.hs | 13 +++--- src/Tablebot/Plugins/Roll/Plugin.hs | 12 +++--- src/Tablebot/Utility/SmartParser.hs | 57 +++++++++++++++----------- src/Tablebot/Utility/Types.hs | 37 +++++++++++++---- 8 files changed, 112 insertions(+), 75 deletions(-) diff --git a/src/Tablebot.hs b/src/Tablebot.hs index 518d35e1..83e06a88 100644 --- a/src/Tablebot.hs +++ b/src/Tablebot.hs @@ -26,8 +26,9 @@ import Control.Concurrent ) import Control.Monad.IO.Class (MonadIO (liftIO)) import Control.Monad.Logger (NoLoggingT (runNoLoggingT)) -import Control.Monad.Reader (runReaderT) +import Control.Monad.Reader (MonadTrans (lift), runReaderT) import Control.Monad.Trans.Resource (runResourceT) +import Data.Bifunctor (Bifunctor (second)) import qualified Data.Map as M import Data.Text (Text, pack) import qualified Data.Text.IO as TIO (putStrLn) @@ -104,13 +105,13 @@ runTablebot dToken prefix dbpath plugins = aid <- partialApplicationID . cacheApplication <$> readCache applicationCommands <- mapM - ( \(CApplicationComand cac action) -> do + ( \(CApplicationCommand cac action) -> do ac <- createApplicationCommand aid serverId cac return (applicationCommandId ac, action) ) compiledAppComms removeApplicationCommandsNotInList aid serverId (fst <$> applicationCommands) - liftIO $ takeMVar cacheMVar >>= \tcache -> putMVar cacheMVar $ tcache {cacheApplicationCommands = M.fromList applicationCommands} + liftIO $ takeMVar cacheMVar >>= \tcache -> putMVar cacheMVar $ tcache {cacheApplicationCommands = M.fromList (second (lift .) <$> applicationCommands)} liftIO $ putStrLn "Tablebot lives!", -- Kill every cron job in the mvar. diff --git a/src/Tablebot/Handler.hs b/src/Tablebot/Handler.hs index 982c0479..346dd1fc 100644 --- a/src/Tablebot/Handler.hs +++ b/src/Tablebot/Handler.hs @@ -31,8 +31,8 @@ import Tablebot.Internal.Handler.Command ( parseNewMessage, ) import Tablebot.Internal.Handler.Event - ( parseInteractionRecvApplicationCommand, - parseInteractionRecvComponent, + ( parseApplicationCommandRecv, + parseComponentRecv, parseMessageChange, parseOther, parseReactionAdd, @@ -62,7 +62,7 @@ import UnliftIO.Exception (catchAny) eventHandler :: PluginActions -> Text -> Event -> CompiledDatabaseDiscord () eventHandler pl prefix = \case MessageCreate m -> - ifNotBot m $ parseNewMessage pl prefix m `catch` \e -> changeAction () . sendEmbedMessage m "" $ embedError (e :: BotException) + ifNotBot m $ catchErrors m $ parseNewMessage pl prefix m MessageUpdate cid mid -> parseMessageChange (compiledOnMessageChanges pl) True cid mid MessageDelete cid mid -> @@ -76,13 +76,14 @@ eventHandler pl prefix = \case -- Similar with MessageReactionRemoveEmoji (removes all of one type). MessageReactionRemoveAll _cid _mid -> pure () MessageReactionRemoveEmoji _rri -> pure () - InteractionCreate i@InteractionComponent {} -> parseInteractionRecvComponent (compiledOnComponentInteractionRecvs pl) i `interactionErrorCatch` i - InteractionCreate i@InteractionApplicationCommand {} -> parseInteractionRecvApplicationCommand i `interactionErrorCatch` i + InteractionCreate i@InteractionComponent {} -> parseComponentRecv (compiledOnComponentInteractionRecvs pl) i `interactionErrorCatch` i + InteractionCreate i@InteractionApplicationCommand {} -> parseApplicationCommandRecv i `interactionErrorCatch` i -- TODO: add application command autocomplete as an option e -> parseOther (compiledOtherEvents pl) e where ifNotBot m = unless (userIsBot (messageAuthor m)) interactionErrorCatch action i = action `catch` (\e -> changeAction () . interactionResponseCustomMessage i $ (messageDetailsBasic "") {messageDetailsEmbeds = Just [embedError (e :: BotException)]}) + catchErrors m = (`catch` (\e -> changeAction () . sendEmbedMessage m "" $ embedError (e :: BotException))) -- | @runCron@ takes an individual @CronJob@ and runs it in a separate thread. -- The @ThreadId@ is returned so it can be killed later. diff --git a/src/Tablebot/Internal/Handler/Event.hs b/src/Tablebot/Internal/Handler/Event.hs index caee061e..5d454512 100644 --- a/src/Tablebot/Internal/Handler/Event.hs +++ b/src/Tablebot/Internal/Handler/Event.hs @@ -12,8 +12,8 @@ module Tablebot.Internal.Handler.Event ( parseMessageChange, parseReactionAdd, parseReactionDel, - parseInteractionRecvComponent, - parseInteractionRecvApplicationCommand, + parseComponentRecv, + parseApplicationCommandRecv, parseOther, ) where @@ -56,21 +56,21 @@ parseReactionDel cs info = mapM_ doReactionAdd cs where doReactionAdd c = onReactionDelete c info -parseInteractionRecvComponent :: [CompiledInteractionRecv] -> Interaction -> CompiledDatabaseDiscord () -parseInteractionRecvComponent cs info@InteractionComponent {interactionDataComponent = Just idc} = mapM_ (`onInteractionRecv` info) cs' +parseComponentRecv :: [CompiledComponentRecv] -> Interaction -> CompiledDatabaseDiscord () +parseComponentRecv cs info@InteractionComponent {interactionDataComponent = Just idc} = mapM_ (`onComponentRecv` info) cs' where - cs' = filter (\cir -> interactionRecvPluginName cir `isPrefixOf` interactionDataComponentCustomId idc) cs -parseInteractionRecvComponent _ _ = return () + cs' = filter (\cir -> (componentPluginName cir <> componentName cir) `isPrefixOf` interactionDataComponentCustomId idc) cs +parseComponentRecv _ _ = return () -parseInteractionRecvApplicationCommand :: Interaction -> CompiledDatabaseDiscord () -parseInteractionRecvApplicationCommand info@InteractionApplicationCommand {interactionDataApplicationCommand = Just idac} = do +parseApplicationCommandRecv :: Interaction -> CompiledDatabaseDiscord () +parseApplicationCommandRecv info@InteractionApplicationCommand {interactionDataApplicationCommand = Just idac} = do tvar <- ask cache <- liftIO $ readMVar tvar let action = UT.cacheApplicationCommands cache M.!? interactionDataApplicationCommandId idac case action of Nothing -> return () - Just act -> changeAction () $ UT.onInteractionRecv act info -parseInteractionRecvApplicationCommand _ = return () + Just act -> changeAction () $ act info +parseApplicationCommandRecv _ = return () -- | This runs each 'Other' feature in @cs@ with the Discord 'Event' provided. -- Note that any events covered by other feature types will /not/ be run diff --git a/src/Tablebot/Internal/Plugins.hs b/src/Tablebot/Internal/Plugins.hs index 42b8d9c0..848dc0d9 100644 --- a/src/Tablebot/Internal/Plugins.hs +++ b/src/Tablebot/Internal/Plugins.hs @@ -9,7 +9,7 @@ -- This contains some functions to combine and compile plugins module Tablebot.Internal.Plugins where -import Control.Monad.Reader (MonadTrans (lift), ReaderT (runReaderT)) +import Control.Monad.Reader (ReaderT (runReaderT)) import Data.Default (Default (def)) import Discord.Types (Message) import Tablebot.Internal.Types hiding (helpPages, migrations) @@ -62,13 +62,13 @@ compilePlugin p = CPl (pluginName p) sa (helpPages p) (migrations p) return $ PA - (fixApplicationCommands state (UT.applicationCommands p)) + (map (fixApplicationCommand state) $ applicationCommands p) (map (fixCommand state) $ commands p) (map (fixInlineCommand state) $ inlineCommands p) (map (fixOnMessageChanges state) $ onMessageChanges p) (map (fixOnReactionAdd state) $ onReactionAdds p) (map (fixOnReactionDelete state) $ onReactionDeletes p) - (map (fixOnInteractionRecv state) $ onComponentInteractionRecvs p) + (map (fixOnComponentRecv state) $ onComponentInteractionRecvs p) (map (fixOther state) $ otherEvents p) (map (fixCron state) $ cronJobs p) @@ -78,17 +78,18 @@ compilePlugin p = CPl (pluginName p) sa (helpPages p) (migrations p) fixOnMessageChanges state' (MessageChange action') = CMessageChange (((changeAction state' .) .) . action') fixOnReactionAdd state' (ReactionAdd action') = CReactionAdd (changeAction state' . action') fixOnReactionDelete state' (ReactionDel action') = CReactionDel (changeAction state' . action') - fixOnInteractionRecv state' (InteractionRecv action') = CInteractionRecv (pluginName p) (changeAction state' . action') + fixOnComponentRecv state' (ComponentRecv name' action') = CComponentRecv (pluginName p) name' (changeAction state' . action') fixOther state' (Other action') = COther (changeAction state' . action') fixCron state' (CronJob time action') = CCronJob time (changeAction state' action') - fixApplicationCommands state' = - concat - . ( ( \case - (Just ac, action) -> [CApplicationComand ac (InteractionRecv $ \i -> lift (changeAction state' (UT.onInteractionRecv action i)))] - (Nothing, _) -> [] - ) - <$> - ) + fixApplicationCommand state' (ApplicationCommandRecv cac action') = CApplicationCommand cac (changeAction state' . action') + +-- concat +-- . ( ( \case +-- (Just ac, action) -> [CApplicationComand ac (InteractionRecv $ \i -> lift (changeAction state' (UT.onComponentRecv action i)))] +-- (Nothing, _) -> [] +-- ) +-- <$> +-- ) -- * Helper converters @@ -96,7 +97,10 @@ compileParser :: s -> Parser (Message -> EnvDatabaseDiscord s a) -> Parser (Mess compileParser s = fmap (changeMessageAction s) changeMessageAction :: s -> (Message -> EnvDatabaseDiscord s a) -> Message -> CompiledDatabaseDiscord a -changeMessageAction s action message = runReaderT (action message) s +changeMessageAction = changeAnyAction + +changeAnyAction :: s -> (m -> EnvDatabaseDiscord s a) -> m -> CompiledDatabaseDiscord a +changeAnyAction s action m = runReaderT (action m) s changeAction :: s -> EnvDatabaseDiscord s a -> CompiledDatabaseDiscord a changeAction s action = runReaderT action s diff --git a/src/Tablebot/Internal/Types.hs b/src/Tablebot/Internal/Types.hs index 8f70b9d8..8f382a8a 100644 --- a/src/Tablebot/Internal/Types.hs +++ b/src/Tablebot/Internal/Types.hs @@ -40,7 +40,7 @@ data PluginActions = PA compiledOnMessageChanges :: [CompiledMessageChange], compiledOnReactionAdds :: [CompiledReactionAdd], compiledOnReactionDeletes :: [CompiledReactionDel], - compiledOnComponentInteractionRecvs :: [CompiledInteractionRecv], + compiledOnComponentInteractionRecvs :: [CompiledComponentRecv], compiledOtherEvents :: [CompiledOther], compiledCronJobs :: [CompiledCronJob] } @@ -61,9 +61,9 @@ instance Default CombinedPlugin where -- These are compiled forms of the actions from the public types that remove the reader. -data CompiledApplicationCommand = CApplicationComand +data CompiledApplicationCommand = CApplicationCommand { applicationCommand :: CreateApplicationCommand, - applicationCommandAction :: EnvInteractionRecv () + applicationCommandAction :: Interaction -> CompiledDatabaseDiscord () } data CompiledCommand = CCommand @@ -88,9 +88,10 @@ newtype CompiledReactionDel = CReactionDel { onReactionDelete :: ReactionInfo -> CompiledDatabaseDiscord () } -data CompiledInteractionRecv = CInteractionRecv - { interactionRecvPluginName :: Text, - onInteractionRecv :: Interaction -> CompiledDatabaseDiscord () +data CompiledComponentRecv = CComponentRecv + { componentPluginName :: Text, + componentName :: Text, + onComponentRecv :: Interaction -> CompiledDatabaseDiscord () } newtype CompiledOther = COther diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 821923e1..e22286db 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -11,7 +11,7 @@ module Tablebot.Plugins.Roll.Plugin (rollPlugin) where import Control.Monad.Writer (MonadIO (liftIO)) import Data.Bifunctor (Bifunctor (first)) -import Data.Maybe (fromMaybe) +import Data.Maybe (catMaybes, fromMaybe) import Data.Text (Text, intercalate, pack, replicate, unpack) import qualified Data.Text as T import Discord.Interactions @@ -34,7 +34,7 @@ import Tablebot.Internal.Handler.Command (parseValue) import Tablebot.Plugins.Roll.Dice import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Utility -import Tablebot.Utility.Discord (interactionResponseComponentsUpdateMessage, interactionResponseCustomMessage, sendCustomMessage, toMention, toMention') +import Tablebot.Utility.Discord (interactionResponseComponentsUpdateMessage, interactionResponseCustomMessage, sendCustomMessage, toMention') import Tablebot.Utility.Exception (BotException (InteractionException), throwBot) import Tablebot.Utility.Parser (inlineCommandHelper) import Tablebot.Utility.SmartParser @@ -82,7 +82,7 @@ getMessagePieces e t u = do { messageDetailsComponents = Just [ ComponentActionRowButton - [ ComponentButton ((("roll`" <> pack (show u)) `appendIf` (parseShow <$> e)) `appendIf` (quote <$> t)) False ButtonStyleSecondary "Reroll" (Just (Emoji (Just 0) "🎲" Nothing Nothing Nothing (Just False))) + [ ComponentButton ((("rollreroll`" <> pack (show u)) `appendIf` (parseShow <$> e)) `appendIf` (quote <$> t)) False ButtonStyleSecondary "Reroll" (Just (Emoji (Just 0) "🎲" Nothing Nothing Nothing (Just False))) ] ] } @@ -94,7 +94,7 @@ getMessagePieces e t u = do rerollInteraction :: Interaction -> DatabaseDiscord () rerollInteraction i@InteractionComponent {interactionDataComponent = Just (InteractionDataComponentButton cid)} | length opts /= 4 = throwBot $ InteractionException "could not process button click" - | maybe True (\u -> toMention u /= opts !! 1) getUser = interactionResponseCustomMessage i ((messageDetailsBasic "Hey, that isn't your button to press!") {messageDetailsFlags = Just $ InteractionCallbackDataFlags [InteractionCallbackDataFlagEphermeral]}) + | maybe True (\u -> pack (show (userId u)) /= opts !! 1) getUser = interactionResponseCustomMessage i ((messageDetailsBasic "Hey, that isn't your button to press!") {messageDetailsFlags = Just $ InteractionCallbackDataFlags [InteractionCallbackDataFlagEphermeral]}) | otherwise = case opts of [_, uid, "", ""] -> do msgdetails <- getMessagePieces Nothing Nothing (read $ unpack uid) @@ -211,7 +211,7 @@ rollPlugin = helpPages = [rollHelp, gencharHelp], inlineCommands = [rollDiceInline], onComponentInteractionRecvs = - [ InteractionRecv rerollInteraction + [ ComponentRecv "reroll" rerollInteraction ], - applicationCommands = [makeApplicationCommandPair "roll" "roll some dice with a description" rollSlashCommandFunction] + applicationCommands = catMaybes [makeApplicationCommandPair "roll" "roll some dice with a description" rollSlashCommandFunction] } diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index 7f2331fd..efe32380 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -14,6 +14,7 @@ -- build a parser that reads in that Int and then runs the command. module Tablebot.Utility.SmartParser where +import Control.Monad.Exception import Data.Proxy (Proxy (..)) import Data.String (IsString (fromString)) import Data.Text (Text, pack) @@ -25,30 +26,38 @@ import Tablebot.Utility.Discord (interactionResponseCustomMessage, sendCustomMes import Tablebot.Utility.Exception (BotException (InteractionException), catchBot, throwBot) import Tablebot.Utility.Parser import Tablebot.Utility.Types - ( EnvDatabaseDiscord, - EnvInteractionRecv (InteractionRecv), - MessageDetails, - Parser, - ) import Text.Megaparsec (MonadParsec (eof, try), chunk, many, optional, (), (<|>)) +class Context a where + contextContent :: MonadException m => a -> m Text + contextUserId :: a -> UserId + +instance Context Message where + contextContent = return . messageContent + contextUserId = userId . messageAuthor + +instance Context Interaction where + contextUserId i = maybe 0 userId (maybe (interactionUser i) memberUser (interactionMember i)) + contextContent InteractionComponent {interactionDataComponent = Just dc} = return $ interactionDataComponentCustomId dc + contextContent _ = throwBot $ InteractionException "could not get content of interactions other than components" + -- | @PComm@ defines function types that we can automatically turn into parsers -- by composing a parser per input of the function provided. -- For example, @Int -> Maybe Text -> Message -> DatabaseDiscord s ()@ builds a -- parser that reads in an @Int@, then some optional @Text@, and then uses -- those to run the provided function with the arguments parsed and the message -- itself. -class PComm commandty s where - parseComm :: commandty -> Parser (Message -> EnvDatabaseDiscord s ()) +class PComm commandty s t where + parseComm :: (Context t) => commandty -> Parser (t -> EnvDatabaseDiscord s ()) -- As a base case, remove the spacing and check for eof. -instance {-# OVERLAPPING #-} PComm (Message -> EnvDatabaseDiscord s ()) s where +instance {-# OVERLAPPING #-} PComm (t -> EnvDatabaseDiscord s ()) s t where parseComm comm = skipSpace >> eof >> return comm -instance {-# OVERLAPPING #-} PComm (EnvDatabaseDiscord s MessageDetails) s where +instance {-# OVERLAPPING #-} PComm (EnvDatabaseDiscord s MessageDetails) s Message where parseComm comm = skipSpace >> eof >> return (\m -> comm >>= sendCustomMessage m) -instance {-# OVERLAPPING #-} PComm (Message -> EnvDatabaseDiscord s MessageDetails) s where +instance {-# OVERLAPPING #-} PComm (Message -> EnvDatabaseDiscord s MessageDetails) s Message where parseComm comm = skipSpace >> eof >> return (\m -> comm m >>= sendCustomMessage m) -- TODO: verify that this second base case is no longer needed @@ -59,24 +68,22 @@ instance {-# OVERLAPPING #-} PComm (Message -> EnvDatabaseDiscord s MessageDetai -- this <- pars @a -- parseComm (comm this) -instance (PComm (Message -> as) s) => PComm (Message -> Message -> as) s where - parseComm comm = parseComm (\m -> comm m m) - -instance (CanParse a, PComm (Message -> as) s) => PComm (Message -> a -> as) s where - parseComm comm = do - this <- parsThenMoveToNext @a - parseComm (`comm` this) - -- Recursive case is to parse the domain of the function type, then the rest. -instance {-# OVERLAPPABLE #-} (CanParse a, PComm as s) => PComm (a -> as) s where +instance {-# OVERLAPPABLE #-} (CanParse a, PComm as s t) => PComm (a -> as) s t where parseComm comm = do this <- parsThenMoveToNext @a parseComm (comm this) --- Recursive case is to parse the domain of the function type, then the rest. -instance {-# OVERLAPPABLE #-} (PComm (Message -> as) s) => PComm (ParseUserId -> as) s where +instance {-# OVERLAPPING #-} (PComm (t -> as) s t) => PComm (t -> t -> as) s t where + parseComm comm = parseComm (\m -> comm m m) + +instance {-# OVERLAPPING #-} (CanParse a, PComm (t -> as) s t) => PComm (t -> a -> as) s t where parseComm comm = do - parseComm $ \m -> comm (ParseUserId (userId $ messageAuthor m)) + this <- parsThenMoveToNext @a + parseComm (`comm` this) + +instance {-# OVERLAPPABLE #-} (PComm (t -> as) s t) => PComm (ParseUserId -> as) s t where + parseComm comm = parseComm $ \(m :: t) -> comm (ParseUserId (contextUserId m)) -- | @CanParse@ defines types from which we can generate parsers. class CanParse a where @@ -221,8 +228,10 @@ noArguments = parseComm -- Interactions stuff ---- -makeApplicationCommandPair :: forall t s. (MakeAppComm t, ProcessAppComm t s) => Text -> Text -> t -> (Maybe CreateApplicationCommand, EnvInteractionRecv s) -makeApplicationCommandPair name desc f = (makeSlashCommand name desc (Proxy :: Proxy t), InteractionRecv (processAppComm f)) +makeApplicationCommandPair :: forall t s. (MakeAppComm t, ProcessAppComm t s) => Text -> Text -> t -> Maybe (EnvApplicationCommandRecv s) +makeApplicationCommandPair name desc f = do + cac <- makeSlashCommand name desc (Proxy :: Proxy t) + return $ ApplicationCommandRecv cac (processAppComm f) makeSlashCommand :: (MakeAppComm t) => Text -> Text -> Proxy t -> Maybe CreateApplicationCommand makeSlashCommand name desc p = diff --git a/src/Tablebot/Utility/Types.hs b/src/Tablebot/Utility/Types.hs index 1c3e2131..cfceb1f8 100644 --- a/src/Tablebot/Utility/Types.hs +++ b/src/Tablebot/Utility/Types.hs @@ -64,7 +64,7 @@ type Database d = SqlPersistM d data TablebotCache = TCache { cacheKnownEmoji :: Map Text Emoji, - cacheApplicationCommands :: Map ApplicationCommandId (EnvInteractionRecv ()) + cacheApplicationCommands :: Map ApplicationCommandId (Interaction -> EnvDatabaseDiscord () ()) } instance Default TablebotCache where @@ -165,13 +165,34 @@ type ReactionDel = EnvReactionDel () -- | Handles recieving of interactions, such as for application commands (slash -- commands, user commands, message commands), as well as components from -- messages. -newtype EnvInteractionRecv d = InteractionRecv - { -- | A function to call on every interaction, which takes in details of that - -- interaction - onInteractionRecv :: Interaction -> EnvDatabaseDiscord d () +-- +-- Rarely used by itself. +-- newtype EnvInteractionRecv d = InteractionRecv +-- { -- | A function to call on an interaction, which takes in details of that +-- -- interaction +-- onInteractionRecv :: Interaction -> EnvDatabaseDiscord d () +-- } + +-- | Handles the creation of an application command and of the action to be +-- performed once that application command is received. +data EnvApplicationCommandRecv d = ApplicationCommandRecv + { -- | The application command to be created. + applicationCommand :: CreateApplicationCommand, + -- | The action to run when the application command is received. + applicationCommandRecv :: Interaction -> EnvDatabaseDiscord d () + } + +type ApplicationCommandRecv = EnvApplicationCommandRecv () + +-- | Handles recieving of interactions, such as for application commands (slash +-- commands, user commands, message commands), as well as components from +-- messages. +data EnvComponentRecv d = ComponentRecv + { componentName :: Text, + onComponentRecv :: Interaction -> EnvDatabaseDiscord d () } -type InteractionRecv = EnvInteractionRecv () +type ComponentRecv = EnvComponentRecv () -- | Handles events not covered by the other kinds of features. This is only -- relevant to specific admin functionality, such as the deletion of channels. @@ -323,13 +344,13 @@ data RequiredPermission = None | Any | Exec | Moderator | Both | Superuser deriv data EnvPlugin d = Pl { pluginName :: Text, startUp :: StartUp d, - applicationCommands :: [(Maybe CreateApplicationCommand, EnvInteractionRecv d)], + applicationCommands :: [EnvApplicationCommandRecv d], commands :: [EnvCommand d], inlineCommands :: [EnvInlineCommand d], onMessageChanges :: [EnvMessageChange d], onReactionAdds :: [EnvReactionAdd d], onReactionDeletes :: [EnvReactionDel d], - onComponentInteractionRecvs :: [EnvInteractionRecv d], + onComponentInteractionRecvs :: [EnvComponentRecv d], otherEvents :: [EnvOther d], cronJobs :: [EnvCronJob d], helpPages :: [HelpPage], From 56965827cb41c6105c2c66270bf8d056842db607 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Mon, 24 Jan 2022 01:27:27 +0000 Subject: [PATCH 17/96] smarter components --- src/Tablebot/Handler.hs | 2 +- src/Tablebot/Internal/Handler/Event.hs | 8 +- src/Tablebot/Internal/Plugins.hs | 4 +- src/Tablebot/Internal/Types.hs | 2 +- src/Tablebot/Plugins/Roll/Plugin.hs | 102 +++++++++++-------------- src/Tablebot/Utility/SmartParser.hs | 70 ++++++++++++----- src/Tablebot/Utility/Types.hs | 2 +- 7 files changed, 104 insertions(+), 86 deletions(-) diff --git a/src/Tablebot/Handler.hs b/src/Tablebot/Handler.hs index 346dd1fc..44f4c30b 100644 --- a/src/Tablebot/Handler.hs +++ b/src/Tablebot/Handler.hs @@ -76,7 +76,7 @@ eventHandler pl prefix = \case -- Similar with MessageReactionRemoveEmoji (removes all of one type). MessageReactionRemoveAll _cid _mid -> pure () MessageReactionRemoveEmoji _rri -> pure () - InteractionCreate i@InteractionComponent {} -> parseComponentRecv (compiledOnComponentInteractionRecvs pl) i `interactionErrorCatch` i + InteractionCreate i@InteractionComponent {} -> parseComponentRecv (compiledOnComponentRecvs pl) i `interactionErrorCatch` i InteractionCreate i@InteractionApplicationCommand {} -> parseApplicationCommandRecv i `interactionErrorCatch` i -- TODO: add application command autocomplete as an option e -> parseOther (compiledOtherEvents pl) e diff --git a/src/Tablebot/Internal/Handler/Event.hs b/src/Tablebot/Internal/Handler/Event.hs index 5d454512..1ddc205e 100644 --- a/src/Tablebot/Internal/Handler/Event.hs +++ b/src/Tablebot/Internal/Handler/Event.hs @@ -21,7 +21,7 @@ where import Control.Concurrent (readMVar) import Control.Monad.RWS (MonadIO (liftIO), MonadReader (ask)) import qualified Data.Map as M -import Data.Text (isPrefixOf) +import Data.Text as T (drop, isPrefixOf, length) import Discord.Interactions (Interaction (..), InteractionDataApplicationCommand (interactionDataApplicationCommandId), InteractionDataComponent (interactionDataComponentCustomId)) import Discord.Types (ChannelId, Event, MessageId, ReactionInfo) import Tablebot.Internal.Plugins (changeAction) @@ -57,9 +57,11 @@ parseReactionDel cs info = mapM_ doReactionAdd cs doReactionAdd c = onReactionDelete c info parseComponentRecv :: [CompiledComponentRecv] -> Interaction -> CompiledDatabaseDiscord () -parseComponentRecv cs info@InteractionComponent {interactionDataComponent = Just idc} = mapM_ (`onComponentRecv` info) cs' +parseComponentRecv cs info@InteractionComponent {interactionDataComponent = Just idc} = mapM_ removePrefix cs' where - cs' = filter (\cir -> (componentPluginName cir <> componentName cir) `isPrefixOf` interactionDataComponentCustomId idc) cs + getPrefix ccr = componentPluginName ccr <> componentName ccr + cs' = filter (\ccr -> getPrefix ccr `isPrefixOf` interactionDataComponentCustomId idc) cs + removePrefix ccr = ccr `onComponentRecv` (info {interactionDataComponent = Just (idc {interactionDataComponentCustomId = T.drop (T.length (getPrefix ccr)) (interactionDataComponentCustomId idc)})}) parseComponentRecv _ _ = return () parseApplicationCommandRecv :: Interaction -> CompiledDatabaseDiscord () diff --git a/src/Tablebot/Internal/Plugins.hs b/src/Tablebot/Internal/Plugins.hs index 848dc0d9..e633126e 100644 --- a/src/Tablebot/Internal/Plugins.hs +++ b/src/Tablebot/Internal/Plugins.hs @@ -42,7 +42,7 @@ combineActions (p : ps) = compiledOnMessageChanges = compiledOnMessageChanges p +++ compiledOnMessageChanges p', compiledOnReactionAdds = compiledOnReactionAdds p +++ compiledOnReactionAdds p', compiledOnReactionDeletes = compiledOnReactionDeletes p +++ compiledOnReactionDeletes p', - compiledOnComponentInteractionRecvs = compiledOnComponentInteractionRecvs p +++ compiledOnComponentInteractionRecvs p', + compiledOnComponentRecvs = compiledOnComponentRecvs p +++ compiledOnComponentRecvs p', compiledOtherEvents = compiledOtherEvents p +++ compiledOtherEvents p', compiledCronJobs = compiledCronJobs p +++ compiledCronJobs p' } @@ -68,7 +68,7 @@ compilePlugin p = CPl (pluginName p) sa (helpPages p) (migrations p) (map (fixOnMessageChanges state) $ onMessageChanges p) (map (fixOnReactionAdd state) $ onReactionAdds p) (map (fixOnReactionDelete state) $ onReactionDeletes p) - (map (fixOnComponentRecv state) $ onComponentInteractionRecvs p) + (map (fixOnComponentRecv state) $ onComponentRecvs p) (map (fixOther state) $ otherEvents p) (map (fixCron state) $ cronJobs p) diff --git a/src/Tablebot/Internal/Types.hs b/src/Tablebot/Internal/Types.hs index 8f382a8a..a2c36b3e 100644 --- a/src/Tablebot/Internal/Types.hs +++ b/src/Tablebot/Internal/Types.hs @@ -40,7 +40,7 @@ data PluginActions = PA compiledOnMessageChanges :: [CompiledMessageChange], compiledOnReactionAdds :: [CompiledReactionAdd], compiledOnReactionDeletes :: [CompiledReactionDel], - compiledOnComponentInteractionRecvs :: [CompiledComponentRecv], + compiledOnComponentRecvs :: [CompiledComponentRecv], compiledOtherEvents :: [CompiledOther], compiledCronJobs :: [CompiledCronJob] } diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index e22286db..146fdd06 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -18,33 +18,30 @@ import Discord.Interactions ( Interaction (..), InteractionCallbackDataFlag (..), InteractionCallbackDataFlags (..), - InteractionDataComponent (..), ) import Discord.Types ( ButtonStyle (ButtonStyleSecondary), ComponentActionRow (ComponentActionRowButton), ComponentButton (ComponentButton), Emoji (Emoji), - GuildMember (memberUser), Message (messageAuthor), User (userId), - UserId, ) import Tablebot.Internal.Handler.Command (parseValue) import Tablebot.Plugins.Roll.Dice import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Utility -import Tablebot.Utility.Discord (interactionResponseComponentsUpdateMessage, interactionResponseCustomMessage, sendCustomMessage, toMention') -import Tablebot.Utility.Exception (BotException (InteractionException), throwBot) +import Tablebot.Utility.Discord (interactionResponseCustomMessage, sendCustomMessage, toMention') import Tablebot.Utility.Parser (inlineCommandHelper) import Tablebot.Utility.SmartParser -import Text.Megaparsec (MonadParsec (eof, try), choice, parse, ()) +import Text.Megaparsec (MonadParsec (eof, try), choice) import Text.RawString.QQ (r) -- | The basic execution function for rolling dice. Both the expression and message are -- optional. If the expression is not given, then the default roll is used. -rollDice'' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> UserId -> DatabaseDiscord Text -rollDice'' e' t u = do +-- The userid of the user that called this command is also given. +rollDice'' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> ParseUserId -> DatabaseDiscord Text +rollDice'' e' t (ParseUserId u) = do let e = fromMaybe (Right defaultRoll) e' (vs, ss) <- case e of (Left a) -> liftIO $ first Left <$> evalList a @@ -66,61 +63,30 @@ rollDice'' e' t u = do simplify li = li countFormatting s = (`div` 4) $ T.foldr (\c cf -> cf + (2 * fromEnum (c == '`')) + fromEnum (c `elem` ['~', '_', '*'])) 0 s -rollDice' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> Message -> DatabaseDiscord MessageDetails -rollDice' e t m = getMessagePieces e t (userId $ messageAuthor m) - -rollSlashCommandFunction :: Labelled "expression" "what's being evaluated" (Maybe Text) -> Labelled "quote" "associated message" (Maybe (Quoted Text)) -> ParseUserId -> DatabaseDiscord MessageDetails -rollSlashCommandFunction (Labelled mt) (Labelled qt) (ParseUserId uid) = do - lve <- mapM (parseValue (pars <* eof)) mt - getMessagePieces lve qt uid - -getMessagePieces :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> UserId -> DatabaseDiscord MessageDetails -getMessagePieces e t u = do +rollDice' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> ParseUserId -> DatabaseDiscord MessageDetails +rollDice' e t u@(ParseUserId uid) = do msg <- rollDice'' e t u return ( (messageDetailsBasic msg) { messageDetailsComponents = Just [ ComponentActionRowButton - [ ComponentButton ((("rollreroll`" <> pack (show u)) `appendIf` (parseShow <$> e)) `appendIf` (quote <$> t)) False ButtonStyleSecondary "Reroll" (Just (Emoji (Just 0) "🎲" Nothing Nothing Nothing (Just False))) + [ ComponentButton ((("rollreroll " <> pack (show uid)) `appendIf` (parseShow <$> e)) `appendIf` (quote <$> t)) False ButtonStyleSecondary "Reroll" (Just (Emoji (Just 0) "🎲" Nothing Nothing Nothing (Just False))) ] ] } ) where - appendIf t' Nothing = t' <> "`" - appendIf t' (Just e') = t' <> "`" <> e' - -rerollInteraction :: Interaction -> DatabaseDiscord () -rerollInteraction i@InteractionComponent {interactionDataComponent = Just (InteractionDataComponentButton cid)} - | length opts /= 4 = throwBot $ InteractionException "could not process button click" - | maybe True (\u -> pack (show (userId u)) /= opts !! 1) getUser = interactionResponseCustomMessage i ((messageDetailsBasic "Hey, that isn't your button to press!") {messageDetailsFlags = Just $ InteractionCallbackDataFlags [InteractionCallbackDataFlagEphermeral]}) - | otherwise = case opts of - [_, uid, "", ""] -> do - msgdetails <- getMessagePieces Nothing Nothing (read $ unpack uid) - interactionResponseComponentsUpdateMessage i msgdetails - [_, uid, "", qt] -> do - msgdetails <- getMessagePieces Nothing (Just (Qu qt)) (read $ unpack uid) - interactionResponseComponentsUpdateMessage i msgdetails - [_, uid, e, ""] -> do - let e' = parse pars "" e - case e' of - Left _ -> throwBot $ InteractionException "could not process button click" - Right e'' -> do - msgdetails <- getMessagePieces (Just e'') Nothing (read $ unpack uid) - interactionResponseComponentsUpdateMessage i msgdetails - [_, uid, e, qt] -> do - let e' = parse pars "" e - case e' of - Left _ -> throwBot $ InteractionException "could not process button click" - Right e'' -> do - msgdetails <- getMessagePieces (Just e'') (Just (Qu qt)) (read $ unpack uid) - interactionResponseComponentsUpdateMessage i msgdetails - _ -> throwBot $ InteractionException "could not process button click" - where - opts = T.split (== '`') cid - getUser = maybe (interactionUser i) memberUser (interactionMember i) -rerollInteraction _ = return () + appendIf t' Nothing = t' + appendIf t' (Just e') = t' <> " " <> e' + +rollSlashCommandFunction :: Labelled "expression" "what's being evaluated" (Maybe Text) -> Labelled "quote" "associated message" (Maybe (Quoted Text)) -> ParseUserId -> DatabaseDiscord MessageDetails +rollSlashCommandFunction (Labelled mt) (Labelled qt) uid = do + lve <- mapM (parseValue (pars <* eof)) mt + rollDice' lve qt uid + +rerollComponentRecv :: ComponentRecv +rerollComponentRecv = ComponentRecv "reroll" (processComponentInteraction' rollDiceParserI True) -- | Manually creating parser for this command, since SmartCommand doesn't work fully for -- multiple Maybe values @@ -130,8 +96,28 @@ rollDiceParser = choice (try <$> options) options = [ parseComm (\lv -> rollDice' (Just lv) Nothing), parseComm (rollDice' Nothing Nothing), - try (parseComm (\lv qt -> rollDice' (Just lv) (Just qt))) "", - try (parseComm (rollDice' Nothing . Just)) "" + try (parseComm (\lv qt -> rollDice' (Just lv) (Just qt))), + try (parseComm (rollDice' Nothing . Just)) + ] + +-- | Manually creating parser for this command, since SmartCommand doesn't work fully for +-- multiple Maybe values +rollDiceParserI :: Parser (Interaction -> DatabaseDiscord MessageDetails) +rollDiceParserI = choice (try <$> options) + where + localRollDice uid lv qt u@(ParseUserId uid') i + | uid == uid' = rollDice' lv qt u + | otherwise = + interactionResponseCustomMessage + i + ( (messageDetailsBasic "Hey, that isn't your button to press!") {messageDetailsFlags = Just $ InteractionCallbackDataFlags [InteractionCallbackDataFlagEphermeral]} + ) + >> return ((messageDetailsBasic "") {messageDetailsContent = Nothing}) + options = + [ parseComm (\uid lv -> localRollDice uid (Just lv) Nothing), + parseComm (\uid -> localRollDice uid Nothing Nothing), + try (parseComm (\uid lv qt -> localRollDice uid (Just lv) (Just qt))), + try (parseComm (\uid qt -> localRollDice uid Nothing (Just qt))) ] -- | Basic command for rolling dice. @@ -140,7 +126,9 @@ rollDice = Command "roll" rollDiceParser [] -- | Rolling dice inline. rollDiceInline :: InlineCommand -rollDiceInline = inlineCommandHelper "[|" "|]" pars (\e m -> rollDice' (Just e) Nothing m >>= sendCustomMessage m) +rollDiceInline = inlineCommandHelper "[|" "|]" pars (\e m -> runFunc e m >>= sendCustomMessage m) + where + runFunc e m = rollDice' (Just e) Nothing (ParseUserId $ userId $ messageAuthor m) -- | Help page for rolling dice, with a link to the help page. rollHelp :: HelpPage @@ -210,8 +198,6 @@ rollPlugin = { commands = [rollDice, commandAlias "r" rollDice, genchar], helpPages = [rollHelp, gencharHelp], inlineCommands = [rollDiceInline], - onComponentInteractionRecvs = - [ ComponentRecv "reroll" rerollInteraction - ], + onComponentRecvs = [rerollComponentRecv], applicationCommands = catMaybes [makeApplicationCommandPair "roll" "roll some dice with a description" rollSlashCommandFunction] } diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index efe32380..9258941d 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -14,7 +14,7 @@ -- build a parser that reads in that Int and then runs the command. module Tablebot.Utility.SmartParser where -import Control.Monad.Exception +import Control.Monad.Exception (MonadException (catch)) import Data.Proxy (Proxy (..)) import Data.String (IsString (fromString)) import Data.Text (Text, pack) @@ -22,24 +22,21 @@ import Discord.Interactions import Discord.Types import GHC.OldList (find) import GHC.TypeLits (KnownSymbol, Symbol, symbolVal) -import Tablebot.Utility.Discord (interactionResponseCustomMessage, sendCustomMessage) -import Tablebot.Utility.Exception (BotException (InteractionException), catchBot, throwBot) +import Tablebot.Internal.Handler.Command (parseValue) +import Tablebot.Utility.Discord (interactionResponseComponentsUpdateMessage, interactionResponseCustomMessage, sendCustomMessage) +import Tablebot.Utility.Exception (BotException (InteractionException, ParserException), catchBot, embedError, throwBot) import Tablebot.Utility.Parser import Tablebot.Utility.Types import Text.Megaparsec (MonadParsec (eof, try), chunk, many, optional, (), (<|>)) class Context a where - contextContent :: MonadException m => a -> m Text - contextUserId :: a -> UserId + contextUserId :: a -> ParseUserId instance Context Message where - contextContent = return . messageContent - contextUserId = userId . messageAuthor + contextUserId = ParseUserId . userId . messageAuthor instance Context Interaction where - contextUserId i = maybe 0 userId (maybe (interactionUser i) memberUser (interactionMember i)) - contextContent InteractionComponent {interactionDataComponent = Just dc} = return $ interactionDataComponentCustomId dc - contextContent _ = throwBot $ InteractionException "could not get content of interactions other than components" + contextUserId i = ParseUserId $ maybe 0 userId (maybe (interactionUser i) memberUser (interactionMember i)) -- | @PComm@ defines function types that we can automatically turn into parsers -- by composing a parser per input of the function provided. @@ -47,17 +44,21 @@ instance Context Interaction where -- parser that reads in an @Int@, then some optional @Text@, and then uses -- those to run the provided function with the arguments parsed and the message -- itself. -class PComm commandty s t where - parseComm :: (Context t) => commandty -> Parser (t -> EnvDatabaseDiscord s ()) +class PComm commandty s context returns where + parseComm :: (Context context) => commandty -> Parser (context -> EnvDatabaseDiscord s returns) +-- TODO: verify that all the parsers for PComm actually work -- As a base case, remove the spacing and check for eof. -instance {-# OVERLAPPING #-} PComm (t -> EnvDatabaseDiscord s ()) s t where +instance {-# OVERLAPPING #-} PComm (t -> EnvDatabaseDiscord s r) s t r where parseComm comm = skipSpace >> eof >> return comm -instance {-# OVERLAPPING #-} PComm (EnvDatabaseDiscord s MessageDetails) s Message where +instance {-# OVERLAPPING #-} PComm (EnvDatabaseDiscord s MessageDetails) s Message () where parseComm comm = skipSpace >> eof >> return (\m -> comm >>= sendCustomMessage m) -instance {-# OVERLAPPING #-} PComm (Message -> EnvDatabaseDiscord s MessageDetails) s Message where +instance {-# OVERLAPPING #-} PComm (EnvDatabaseDiscord s r) s t r where + parseComm comm = skipSpace >> eof >> return (const comm) + +instance {-# OVERLAPPING #-} PComm (Message -> EnvDatabaseDiscord s MessageDetails) s Message () where parseComm comm = skipSpace >> eof >> return (\m -> comm m >>= sendCustomMessage m) -- TODO: verify that this second base case is no longer needed @@ -69,21 +70,21 @@ instance {-# OVERLAPPING #-} PComm (Message -> EnvDatabaseDiscord s MessageDetai -- parseComm (comm this) -- Recursive case is to parse the domain of the function type, then the rest. -instance {-# OVERLAPPABLE #-} (CanParse a, PComm as s t) => PComm (a -> as) s t where +instance {-# OVERLAPPABLE #-} (CanParse a, PComm as s t r) => PComm (a -> as) s t r where parseComm comm = do this <- parsThenMoveToNext @a parseComm (comm this) -instance {-# OVERLAPPING #-} (PComm (t -> as) s t) => PComm (t -> t -> as) s t where +instance {-# OVERLAPPABLE #-} (PComm (t -> as) s t r) => PComm (t -> t -> as) s t r where parseComm comm = parseComm (\m -> comm m m) -instance {-# OVERLAPPING #-} (CanParse a, PComm (t -> as) s t) => PComm (t -> a -> as) s t where +instance {-# OVERLAPPABLE #-} (Context t, CanParse a, PComm (t -> as) s t r) => PComm (t -> a -> as) s t r where parseComm comm = do this <- parsThenMoveToNext @a parseComm (`comm` this) -instance {-# OVERLAPPABLE #-} (PComm (t -> as) s t) => PComm (ParseUserId -> as) s t where - parseComm comm = parseComm $ \(m :: t) -> comm (ParseUserId (contextUserId m)) +instance {-# OVERLAPPABLE #-} (PComm (t -> as) s t r) => PComm (ParseUserId -> as) s t r where + parseComm comm = parseComm $ \(m :: t) -> comm (contextUserId m) -- | @CanParse@ defines types from which we can generate parsers. class CanParse a where @@ -192,6 +193,9 @@ instance CanParse Double where instance CanParse () where pars = eof +instance CanParse Snowflake where + pars = Snowflake . fromInteger <$> pars + -- | @RestOfInput a@ parses the rest of the input, giving a value of type @a@. newtype RestOfInput a = ROI a @@ -320,3 +324,29 @@ instance (KnownSymbol name, ProcessAppCommArg (Labelled name desc t) s) => Proce return (labelValue (Just l)) ) `catchBot` const (return $ labelValue Nothing) + +-- | Given a function that can be processed to create a parser, create an action +-- for it using the helper. +-- +-- If the boolean is true, the message the component is from is updated. Else, +-- a message is sent as the interaction response. +processComponentInteraction :: (PComm f s Interaction MessageDetails) => f -> Bool -> Interaction -> EnvDatabaseDiscord s () +processComponentInteraction f = processComponentInteraction' (parseComm f) + +-- | Given a parser that, when run, returns a function taking an interaction +-- and returns a database action on some MessageDetails, run the action. +-- +-- If the boolean is true, the message the component is from is updated. Else, +-- a message is sent as the interaction response. +processComponentInteraction' :: Parser (Interaction -> EnvDatabaseDiscord s MessageDetails) -> Bool -> Interaction -> EnvDatabaseDiscord s () +processComponentInteraction' compParser updateOriginal i@InteractionComponent {interactionDataComponent = Just idc} = errorCatch $ do + let componentSend + | updateOriginal = interactionResponseComponentsUpdateMessage i + | otherwise = interactionResponseCustomMessage i + action <- parseValue (skipSpace *> compParser) (interactionDataComponentCustomId idc) >>= ($ i) + componentSend action + where + catchParserException e@(ParserException _ _) = interactionResponseCustomMessage i $ (messageDetailsBasic "something (likely) went wrong when processing a component interaction") {messageDetailsEmbeds = Just [embedError (e :: BotException)]} + catchParserException e = interactionResponseCustomMessage i $ (messageDetailsBasic "") {messageDetailsEmbeds = Just [embedError (e :: BotException)]} + errorCatch = (`catch` catchParserException) +processComponentInteraction' _ _ _ = throwBot $ InteractionException "could not process component interaction" diff --git a/src/Tablebot/Utility/Types.hs b/src/Tablebot/Utility/Types.hs index cfceb1f8..7a082b24 100644 --- a/src/Tablebot/Utility/Types.hs +++ b/src/Tablebot/Utility/Types.hs @@ -350,7 +350,7 @@ data EnvPlugin d = Pl onMessageChanges :: [EnvMessageChange d], onReactionAdds :: [EnvReactionAdd d], onReactionDeletes :: [EnvReactionDel d], - onComponentInteractionRecvs :: [EnvComponentRecv d], + onComponentRecvs :: [EnvComponentRecv d], otherEvents :: [EnvOther d], cronJobs :: [EnvCronJob d], helpPages :: [HelpPage], From 7551af009714d3fb80e9fb3ff1f38058a593775e Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Mon, 24 Jan 2022 19:17:48 +0000 Subject: [PATCH 18/96] moving some stuff around, commenting a bit --- README.md | 1 + src/Tablebot.hs | 26 +------ src/Tablebot/Handler.hs | 46 ++++++++---- src/Tablebot/Internal/Plugins.hs | 11 +-- src/Tablebot/Plugins/Roll/Dice/DiceEval.hs | 73 +------------------ src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 72 +++++++++++++++++- src/Tablebot/Utility/SmartParser.hs | 2 + 7 files changed, 111 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index b3373169..7ca761ee 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Create a `.env` file containing the following keys. Consult `.env.example` if yo * `EXEC_GROUP` (optional) - the group ID assigned to exec members. * `MODERATOR_GROUP` (optional) - the group ID assigned to moderator members. * `SUPERUSER_GROUP` (optional) - the group ID assigned to the superuser. Strongly recommended +* `SERVER_ID` (optional) - the id of the server the bot will mainly be deployed in. Application commands will be registered here. The three Group settings are optional, but without them any commands that require elevated permissions will not be able to be called when DEBUG is false. Users with the superuser group are able to run every command (including some dangerous diff --git a/src/Tablebot.hs b/src/Tablebot.hs index 83e06a88..67656a17 100644 --- a/src/Tablebot.hs +++ b/src/Tablebot.hs @@ -26,10 +26,8 @@ import Control.Concurrent ) import Control.Monad.IO.Class (MonadIO (liftIO)) import Control.Monad.Logger (NoLoggingT (runNoLoggingT)) -import Control.Monad.Reader (MonadTrans (lift), runReaderT) +import Control.Monad.Reader (runReaderT) import Control.Monad.Trans.Resource (runResourceT) -import Data.Bifunctor (Bifunctor (second)) -import qualified Data.Map as M import Data.Text (Text, pack) import qualified Data.Text.IO as TIO (putStrLn) import Database.Persist.Sqlite @@ -38,18 +36,13 @@ import Database.Persist.Sqlite runSqlPool, ) import Discord -import Discord.Interactions (ApplicationCommand (applicationCommandId)) -import Discord.Internal.Rest (PartialApplication (partialApplicationID)) -import System.Environment (getEnv) -import Tablebot.Handler (eventHandler, killCron, runCron) +import Tablebot.Handler (eventHandler, killCron, runCron, submitApplicationCommands) import Tablebot.Internal.Administration (adminMigration, currentBlacklist, removeBlacklisted) import Tablebot.Internal.Plugins import Tablebot.Internal.Types -import Tablebot.Utility.Discord (createApplicationCommand, removeApplicationCommandsNotInList) import Tablebot.Utility.Help import Tablebot.Utility.Types (TablebotCache (..)) import Tablebot.Utility.Utils (debugPrint) -import Text.Read (readMaybe) -- | runTablebot @dToken@ @prefix@ @dbpath@ @plugins@ runs the bot using the -- given Discord API token @dToken@ and SQLite connection string @dbpath@. Only @@ -77,7 +70,6 @@ runTablebot dToken prefix dbpath plugins = -- Run the setup actions of each plugin and collect the plugin actions into a single @PluginActions@ instance allActions <- mapM (runResourceT . runNoLoggingT . flip runSqlPool pool) (combinedSetupAction plugin) let !actions = combineActions allActions - compiledAppComms = compiledApplicationCommands actions -- TODO: this might have issues with duplicates? -- TODO: in production, this should probably run once and then never again. @@ -99,19 +91,7 @@ runTablebot dToken prefix dbpath plugins = -- sometimes). runReaderT (mapM (runCron pool) (compiledCronJobs actions) >>= liftIO . putMVar mvar) cacheMVar - -- generate the application commands, cleaning up any application commands we don't like - serverIdStr <- liftIO $ getEnv "SERVER_ID" - serverId <- maybe (fail "could not read server id") return (readMaybe serverIdStr) - aid <- partialApplicationID . cacheApplication <$> readCache - applicationCommands <- - mapM - ( \(CApplicationCommand cac action) -> do - ac <- createApplicationCommand aid serverId cac - return (applicationCommandId ac, action) - ) - compiledAppComms - removeApplicationCommandsNotInList aid serverId (fst <$> applicationCommands) - liftIO $ takeMVar cacheMVar >>= \tcache -> putMVar cacheMVar $ tcache {cacheApplicationCommands = M.fromList (second (lift .) <$> applicationCommands)} + submitApplicationCommands (compiledApplicationCommands actions) cacheMVar liftIO $ putStrLn "Tablebot lives!", -- Kill every cron job in the mvar. diff --git a/src/Tablebot/Handler.hs b/src/Tablebot/Handler.hs index 44f4c30b..a27d216c 100644 --- a/src/Tablebot/Handler.hs +++ b/src/Tablebot/Handler.hs @@ -13,23 +13,25 @@ module Tablebot.Handler ( eventHandler, runCron, killCron, + submitApplicationCommands, ) where -import Control.Concurrent (MVar) +import Control.Concurrent (MVar, putMVar, takeMVar) import Control.Monad (unless) import Control.Monad.Exception (MonadException (catch)) import Control.Monad.IO.Class (MonadIO (liftIO)) import Control.Monad.Reader (ReaderT, ask, lift, runReaderT) +import Data.Bifunctor (Bifunctor (second)) +import Data.Map as M (fromList) import Data.Pool (Pool) import Data.Text (Text) import Database.Persist.Sqlite (SqlBackend, runSqlPool) -import Discord (DiscordHandler) -import Discord.Interactions (Interaction (..)) +import Discord (Cache (cacheApplication), DiscordHandler, readCache) +import Discord.Interactions (ApplicationCommand (..), Interaction (..)) import Discord.Types -import Tablebot.Internal.Handler.Command - ( parseNewMessage, - ) +import System.Environment (getEnv) +import Tablebot.Internal.Handler.Command (parseNewMessage) import Tablebot.Internal.Handler.Event ( parseApplicationCommandRecv, parseComponentRecv, @@ -40,13 +42,10 @@ import Tablebot.Internal.Handler.Event ) import Tablebot.Internal.Plugins (changeAction) import Tablebot.Internal.Types -import Tablebot.Utility.Discord (interactionResponseCustomMessage, sendEmbedMessage) +import Tablebot.Utility.Discord (createApplicationCommand, interactionResponseCustomMessage, removeApplicationCommandsNotInList, sendChannelEmbedMessage, sendEmbedMessage) import Tablebot.Utility.Exception (BotException, embedError) -import Tablebot.Utility.Types - ( MessageDetails (messageDetailsEmbeds), - TablebotCache, - messageDetailsBasic, - ) +import Tablebot.Utility.Types (MessageDetails (messageDetailsEmbeds), TablebotCache (cacheApplicationCommands), messageDetailsBasic) +import Text.Read (readMaybe) import UnliftIO.Concurrent ( ThreadId, forkIO, @@ -62,7 +61,7 @@ import UnliftIO.Exception (catchAny) eventHandler :: PluginActions -> Text -> Event -> CompiledDatabaseDiscord () eventHandler pl prefix = \case MessageCreate m -> - ifNotBot m $ catchErrors m $ parseNewMessage pl prefix m + ifNotBot m $ catchErrors (messageChannelId m) $ parseNewMessage pl prefix m MessageUpdate cid mid -> parseMessageChange (compiledOnMessageChanges pl) True cid mid MessageDelete cid mid -> @@ -83,7 +82,7 @@ eventHandler pl prefix = \case where ifNotBot m = unless (userIsBot (messageAuthor m)) interactionErrorCatch action i = action `catch` (\e -> changeAction () . interactionResponseCustomMessage i $ (messageDetailsBasic "") {messageDetailsEmbeds = Just [embedError (e :: BotException)]}) - catchErrors m = (`catch` (\e -> changeAction () . sendEmbedMessage m "" $ embedError (e :: BotException))) + catchErrors m = (`catch` (\e -> changeAction () . sendChannelEmbedMessage m "" $ embedError (e :: BotException))) -- | @runCron@ takes an individual @CronJob@ and runs it in a separate thread. -- The @ThreadId@ is returned so it can be killed later. @@ -111,3 +110,22 @@ runCron pool (CCronJob delay fn) = do -- | @killCron@ takes a list of @ThreadId@ and kills each thread. killCron :: [ThreadId] -> IO () killCron = mapM_ killThread + +submitApplicationCommands :: [CompiledApplicationCommand] -> MVar TablebotCache -> DiscordHandler () +submitApplicationCommands compiledAppComms cacheMVar = + ( do + -- generate the application commands, cleaning up any application commands we don't like + serverIdStr <- liftIO $ getEnv "SERVER_ID" + serverId <- maybe (fail "could not read server id") return (readMaybe serverIdStr) + aid <- partialApplicationID . cacheApplication <$> readCache + applicationCommands <- + mapM + ( \(CApplicationCommand cac action) -> do + ac <- createApplicationCommand aid serverId cac + return (applicationCommandId ac, action) + ) + compiledAppComms + removeApplicationCommandsNotInList aid serverId (fst <$> applicationCommands) + liftIO $ takeMVar cacheMVar >>= \tcache -> putMVar cacheMVar $ tcache {cacheApplicationCommands = M.fromList (second (lift .) <$> applicationCommands)} + ) + `catch` \(_ :: IOError) -> liftIO $ putStrLn "There was an error of some sort when submitting the application commands - verify that `SERVER_ID` is set." diff --git a/src/Tablebot/Internal/Plugins.hs b/src/Tablebot/Internal/Plugins.hs index e633126e..8d384ac7 100644 --- a/src/Tablebot/Internal/Plugins.hs +++ b/src/Tablebot/Internal/Plugins.hs @@ -48,7 +48,6 @@ combineActions (p : ps) = } where -- copy across Finnbar's +++ optimisation for empty lists from the old system, as it applies here. - [] +++ [] = [] a +++ [] = a [] +++ a = a a +++ b = a ++ b @@ -83,14 +82,6 @@ compilePlugin p = CPl (pluginName p) sa (helpPages p) (migrations p) fixCron state' (CronJob time action') = CCronJob time (changeAction state' action') fixApplicationCommand state' (ApplicationCommandRecv cac action') = CApplicationCommand cac (changeAction state' . action') --- concat --- . ( ( \case --- (Just ac, action) -> [CApplicationComand ac (InteractionRecv $ \i -> lift (changeAction state' (UT.onComponentRecv action i)))] --- (Nothing, _) -> [] --- ) --- <$> --- ) - -- * Helper converters compileParser :: s -> Parser (Message -> EnvDatabaseDiscord s a) -> Parser (Message -> CompiledDatabaseDiscord a) @@ -100,7 +91,7 @@ changeMessageAction :: s -> (Message -> EnvDatabaseDiscord s a) -> Message -> Co changeMessageAction = changeAnyAction changeAnyAction :: s -> (m -> EnvDatabaseDiscord s a) -> m -> CompiledDatabaseDiscord a -changeAnyAction s action m = runReaderT (action m) s +changeAnyAction s action m = changeAction s (action m) changeAction :: s -> EnvDatabaseDiscord s a -> CompiledDatabaseDiscord a changeAction s action = runReaderT action s diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index 9c6c7f51..a6d143fb 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs @@ -16,14 +16,12 @@ import Control.Monad (when) import Control.Monad.Exception (MonadException) import Data.List (genericDrop, genericReplicate, genericTake, sortBy) import Data.List.NonEmpty as NE (NonEmpty ((:|)), head, tail, (<|)) -import Data.Map as M (findWithDefault) import Data.Maybe (fromMaybe, isNothing) -import Data.String (IsString (fromString)) import Data.Text (Text, intercalate, pack, unpack) -import qualified Data.Text as T import System.Random (randomRIO) import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceFunctions (FuncInfoBase (..), ListInteger (..)) +import Tablebot.Plugins.Roll.Dice.DiceParsing () import Tablebot.Utility.Discord (Format (..), formatInput, formatText) import Tablebot.Utility.Exception (BotException (EvaluationException), catchBot, throwBot) import Tablebot.Utility.Parser (ParseShow (parseShow)) @@ -389,72 +387,3 @@ instance IOEval NumBase where (r, s, rngCount') <- evalShow rngCount e return (r, "(" <> s <> ")", rngCount') evalShow' rngCount (Value i) = return (i, pack (show i), rngCount) - ---- Pretty printing the AST - -instance ParseShow ArgValue where - parseShow (AVExpr e) = parseShow e - parseShow (AVListValues lv) = parseShow lv - -instance ParseShow ListValues where - parseShow (LVBase e) = parseShow e - parseShow (MultipleValues nb b) = parseShow nb <> "#" <> parseShow b - parseShow (LVFunc s n) = funcInfoName s <> "(" <> intercalate "," (parseShow <$> n) <> ")" - -instance ParseShow ListValuesBase where - parseShow (LVBList es) = "{" <> intercalate ", " (parseShow <$> es) <> "}" - parseShow (LVBParen p) = parseShow p - -instance ParseShow Expr where - parseShow (Add t e) = parseShow t <> " + " <> parseShow e - parseShow (Sub t e) = parseShow t <> " - " <> parseShow e - parseShow (NoExpr t) = parseShow t - -instance ParseShow Term where - parseShow (Multi f t) = parseShow f <> " * " <> parseShow t - parseShow (Div f t) = parseShow f <> " / " <> parseShow t - parseShow (NoTerm f) = parseShow f - -instance ParseShow Func where - parseShow (Func s n) = funcInfoName s <> "(" <> intercalate "," (parseShow <$> n) <> ")" - parseShow (NoFunc b) = parseShow b - -instance ParseShow Negation where - parseShow (Neg expo) = "-" <> parseShow expo - parseShow (NoNeg expo) = parseShow expo - -instance ParseShow Expo where - parseShow (NoExpo b) = parseShow b - parseShow (Expo b expo) = parseShow b <> " ^ " <> parseShow expo - -instance ParseShow NumBase where - parseShow (NBParen p) = parseShow p - parseShow (Value i) = fromString $ show i - -instance (ParseShow a) => ParseShow (Paren a) where - parseShow (Paren a) = "(" <> parseShow a <> ")" - -instance ParseShow Base where - parseShow (NBase nb) = parseShow nb - parseShow (DiceBase dop) = parseShow dop - -instance ParseShow Die where - parseShow (Die b) = "d" <> parseShow b - parseShow (CustomDie lv) = "d" <> parseShow lv - -- parseShow (CustomDie is) = "d{" <> intercalate ", " (parseShow <$> is) <> "}" - parseShow (LazyDie d) = "d!" <> T.tail (parseShow d) - -instance ParseShow Dice where - parseShow (Dice b d dor) = parseShow b <> parseShow d <> helper' dor - where - fromOrdering ao = M.findWithDefault "??" ao $ snd advancedOrderingMapping - fromLHW (Where o i) = "w" <> fromOrdering o <> parseShow i - fromLHW (Low i) = "l" <> parseShow i - fromLHW (High i) = "h" <> parseShow i - helper' Nothing = "" - helper' (Just (DieOpRecur dopo' dor')) = helper dopo' <> helper' dor' - helper (DieOpOptionLazy doo) = "!" <> helper doo - helper (Reroll True o i) = "ro" <> fromOrdering o <> parseShow i - helper (Reroll False o i) = "rr" <> fromOrdering o <> parseShow i - helper (DieOpOptionKD Keep lhw) = "k" <> fromLHW lhw - helper (DieOpOptionKD Drop lhw) = "d" <> fromLHW lhw diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index 977aa876..498a1a0a 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -18,6 +18,7 @@ import Data.Map as M (Map, findWithDefault, keys, map, (!)) import Data.Maybe (fromMaybe) import Data.Set as S (Set, fromList, map) import Data.Text (Text, singleton, unpack) +import qualified Data.Text as T import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceFunctions ( ArgType (..), @@ -25,7 +26,7 @@ import Tablebot.Plugins.Roll.Dice.DiceFunctions integerFunctions, listFunctions, ) -import Tablebot.Utility.Parser (integer, parseCommaSeparated1, skipSpace) +import Tablebot.Utility.Parser (ParseShow (parseShow), integer, parseCommaSeparated1, skipSpace) import Tablebot.Utility.SmartParser (CanParse (..)) import Tablebot.Utility.Types (Parser) import Text.Megaparsec (MonadParsec (try), choice, failure, optional, (), (<|>)) @@ -188,3 +189,72 @@ parseArgValues :: [ArgType] -> Parser [ArgValue] parseArgValues [] = return [] parseArgValues [at] = (: []) <$> parseArgValue at parseArgValues (at : ats) = parseArgValue at >>= \av -> skipSpace *> (try (char ',') "expected " ++ show (length ats) ++ " more arguments") *> skipSpace *> ((av :) <$> parseArgValues ats) + +--- Pretty printing the AST + +instance ParseShow ArgValue where + parseShow (AVExpr e) = parseShow e + parseShow (AVListValues lv) = parseShow lv + +instance ParseShow ListValues where + parseShow (LVBase e) = parseShow e + parseShow (MultipleValues nb b) = parseShow nb <> "#" <> parseShow b + parseShow (LVFunc s n) = funcInfoName s <> "(" <> T.intercalate "," (parseShow <$> n) <> ")" + +instance ParseShow ListValuesBase where + parseShow (LVBList es) = "{" <> T.intercalate ", " (parseShow <$> es) <> "}" + parseShow (LVBParen p) = parseShow p + +instance ParseShow Expr where + parseShow (Add t e) = parseShow t <> " + " <> parseShow e + parseShow (Sub t e) = parseShow t <> " - " <> parseShow e + parseShow (NoExpr t) = parseShow t + +instance ParseShow Term where + parseShow (Multi f t) = parseShow f <> " * " <> parseShow t + parseShow (Div f t) = parseShow f <> " / " <> parseShow t + parseShow (NoTerm f) = parseShow f + +instance ParseShow Func where + parseShow (Func s n) = funcInfoName s <> "(" <> T.intercalate "," (parseShow <$> n) <> ")" + parseShow (NoFunc b) = parseShow b + +instance ParseShow Negation where + parseShow (Neg expo) = "-" <> parseShow expo + parseShow (NoNeg expo) = parseShow expo + +instance ParseShow Expo where + parseShow (NoExpo b) = parseShow b + parseShow (Expo b expo) = parseShow b <> " ^ " <> parseShow expo + +instance ParseShow NumBase where + parseShow (NBParen p) = parseShow p + parseShow (Value i) = T.pack $ show i + +instance (ParseShow a) => ParseShow (Paren a) where + parseShow (Paren a) = "(" <> parseShow a <> ")" + +instance ParseShow Base where + parseShow (NBase nb) = parseShow nb + parseShow (DiceBase dop) = parseShow dop + +instance ParseShow Die where + parseShow (Die b) = "d" <> parseShow b + parseShow (CustomDie lv) = "d" <> parseShow lv + -- parseShow (CustomDie is) = "d{" <> intercalate ", " (parseShow <$> is) <> "}" + parseShow (LazyDie d) = "d!" <> T.tail (parseShow d) + +instance ParseShow Dice where + parseShow (Dice b d dor) = parseShow b <> parseShow d <> helper' dor + where + fromOrdering ao = M.findWithDefault "??" ao $ snd advancedOrderingMapping + fromLHW (Where o i) = "w" <> fromOrdering o <> parseShow i + fromLHW (Low i) = "l" <> parseShow i + fromLHW (High i) = "h" <> parseShow i + helper' Nothing = "" + helper' (Just (DieOpRecur dopo' dor')) = helper dopo' <> helper' dor' + helper (DieOpOptionLazy doo) = "!" <> helper doo + helper (Reroll True o i) = "ro" <> fromOrdering o <> parseShow i + helper (Reroll False o i) = "rr" <> fromOrdering o <> parseShow i + helper (DieOpOptionKD Keep lhw) = "k" <> fromLHW lhw + helper (DieOpOptionKD Drop lhw) = "d" <> fromLHW lhw diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index 9258941d..9003ccbb 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -333,6 +333,8 @@ instance (KnownSymbol name, ProcessAppCommArg (Labelled name desc t) s) => Proce processComponentInteraction :: (PComm f s Interaction MessageDetails) => f -> Bool -> Interaction -> EnvDatabaseDiscord s () processComponentInteraction f = processComponentInteraction' (parseComm f) +-- TODO: comment what is given to the parser + -- | Given a parser that, when run, returns a function taking an interaction -- and returns a database action on some MessageDetails, run the action. -- From 45e6d6ea42869ed7586413c887649a8e16c52a2a Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Mon, 24 Jan 2022 20:19:04 +0000 Subject: [PATCH 19/96] somre reformatting, fixes, and comments --- src/Tablebot/Handler.hs | 6 +++++- src/Tablebot/Internal/Handler/Command.hs | 1 + src/Tablebot/Internal/Handler/Event.hs | 7 ++++++- src/Tablebot/Internal/Plugins.hs | 2 +- src/Tablebot/Plugins/Roll/Dice.hs | 4 ++-- src/Tablebot/Plugins/Roll/Dice/DiceEval.hs | 2 -- src/Tablebot/Plugins/Roll/Plugin.hs | 11 ++++++----- src/Tablebot/Utility/Parser.hs | 3 +++ src/Tablebot/Utility/SmartParser.hs | 3 +++ 9 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/Tablebot/Handler.hs b/src/Tablebot/Handler.hs index a27d216c..96c1b552 100644 --- a/src/Tablebot/Handler.hs +++ b/src/Tablebot/Handler.hs @@ -42,7 +42,7 @@ import Tablebot.Internal.Handler.Event ) import Tablebot.Internal.Plugins (changeAction) import Tablebot.Internal.Types -import Tablebot.Utility.Discord (createApplicationCommand, interactionResponseCustomMessage, removeApplicationCommandsNotInList, sendChannelEmbedMessage, sendEmbedMessage) +import Tablebot.Utility.Discord (createApplicationCommand, interactionResponseCustomMessage, removeApplicationCommandsNotInList, sendChannelEmbedMessage) import Tablebot.Utility.Exception (BotException, embedError) import Tablebot.Utility.Types (MessageDetails (messageDetailsEmbeds), TablebotCache (cacheApplicationCommands), messageDetailsBasic) import Text.Read (readMaybe) @@ -111,6 +111,10 @@ runCron pool (CCronJob delay fn) = do killCron :: [ThreadId] -> IO () killCron = mapM_ killThread +-- | Given a list of compiled application commands and a pointer to the +-- tablebot cache, create the given application commands, purge ones that +-- weren't created by us, and place the application command id's and their +-- actions in the cache. submitApplicationCommands :: [CompiledApplicationCommand] -> MVar TablebotCache -> DiscordHandler () submitApplicationCommands compiledAppComms cacheMVar = ( do diff --git a/src/Tablebot/Internal/Handler/Command.hs b/src/Tablebot/Internal/Handler/Command.hs index 6075d9f5..789e9e2a 100644 --- a/src/Tablebot/Internal/Handler/Command.hs +++ b/src/Tablebot/Internal/Handler/Command.hs @@ -129,6 +129,7 @@ parseInlineCommands cs m = mapM_ (fromResult . (\cic -> parse (inlineCommandPars fromResult (Right p) = UIOE.tryAny (p m) fromResult _ = return $ return () +-- | Turn the parsing of a value into an exception when given text to parse. parseValue :: Parser a -> Text -> EnvDatabaseDiscord s a parseValue par t = case parse par "" t of Right p -> return p diff --git a/src/Tablebot/Internal/Handler/Event.hs b/src/Tablebot/Internal/Handler/Event.hs index 1ddc205e..927e89f5 100644 --- a/src/Tablebot/Internal/Handler/Event.hs +++ b/src/Tablebot/Internal/Handler/Event.hs @@ -26,6 +26,7 @@ import Discord.Interactions (Interaction (..), InteractionDataApplicationCommand import Discord.Types (ChannelId, Event, MessageId, ReactionInfo) import Tablebot.Internal.Plugins (changeAction) import Tablebot.Internal.Types as IT +import Tablebot.Utility.Exception (BotException (InteractionException), throwBot) import qualified Tablebot.Utility.Types as UT -- | This runs each 'MessageChange' feature in @cs@ with the information from a @@ -56,6 +57,8 @@ parseReactionDel cs info = mapM_ doReactionAdd cs where doReactionAdd c = onReactionDelete c info +-- | When given the compiled component recv actions and a component interaction, +-- find and run the correct action. parseComponentRecv :: [CompiledComponentRecv] -> Interaction -> CompiledDatabaseDiscord () parseComponentRecv cs info@InteractionComponent {interactionDataComponent = Just idc} = mapM_ removePrefix cs' where @@ -64,13 +67,15 @@ parseComponentRecv cs info@InteractionComponent {interactionDataComponent = Just removePrefix ccr = ccr `onComponentRecv` (info {interactionDataComponent = Just (idc {interactionDataComponentCustomId = T.drop (T.length (getPrefix ccr)) (interactionDataComponentCustomId idc)})}) parseComponentRecv _ _ = return () +-- | When given an application command interaction, find and run the correct +-- action. parseApplicationCommandRecv :: Interaction -> CompiledDatabaseDiscord () parseApplicationCommandRecv info@InteractionApplicationCommand {interactionDataApplicationCommand = Just idac} = do tvar <- ask cache <- liftIO $ readMVar tvar let action = UT.cacheApplicationCommands cache M.!? interactionDataApplicationCommandId idac case action of - Nothing -> return () + Nothing -> throwBot $ InteractionException "could not find the given application command" Just act -> changeAction () $ act info parseApplicationCommandRecv _ = return () diff --git a/src/Tablebot/Internal/Plugins.hs b/src/Tablebot/Internal/Plugins.hs index 8d384ac7..a6747ab8 100644 --- a/src/Tablebot/Internal/Plugins.hs +++ b/src/Tablebot/Internal/Plugins.hs @@ -72,6 +72,7 @@ compilePlugin p = CPl (pluginName p) sa (helpPages p) (migrations p) (map (fixCron state) $ cronJobs p) -- Command converters + fixApplicationCommand state' (ApplicationCommandRecv cac action') = CApplicationCommand cac (changeAction state' . action') fixCommand state' (Command name' action' subcommands') = CCommand name' (compileParser state' action') (map (fixCommand state') subcommands') fixInlineCommand state' (InlineCommand action') = CInlineCommand (compileParser state' action') fixOnMessageChanges state' (MessageChange action') = CMessageChange (((changeAction state' .) .) . action') @@ -80,7 +81,6 @@ compilePlugin p = CPl (pluginName p) sa (helpPages p) (migrations p) fixOnComponentRecv state' (ComponentRecv name' action') = CComponentRecv (pluginName p) name' (changeAction state' . action') fixOther state' (Other action') = COther (changeAction state' . action') fixCron state' (CronJob time action') = CCronJob time (changeAction state' action') - fixApplicationCommand state' (ApplicationCommandRecv cac action') = CApplicationCommand cac (changeAction state' . action') -- * Helper converters diff --git a/src/Tablebot/Plugins/Roll/Dice.hs b/src/Tablebot/Plugins/Roll/Dice.hs index 3f44d9f8..27678c57 100644 --- a/src/Tablebot/Plugins/Roll/Dice.hs +++ b/src/Tablebot/Plugins/Roll/Dice.hs @@ -55,7 +55,7 @@ -- ords (AdvancedOrdering and NumBase) - representing a more complex ordering operation than a basic `Ordering`, when compared to a `NumBase` -- argv (ArgValue) - representing an argument to a function -- funcBasics - a generic regex representation for a general function parser -module Tablebot.Plugins.Roll.Dice (evalInteger, evalList, ListValues (..), defaultRoll, ParseShow (parseShow), integerFunctionsList, listFunctionsList, Converter (promote)) where +module Tablebot.Plugins.Roll.Dice (evalInteger, evalList, ListValues (..), defaultRoll, integerFunctionsList, listFunctionsList, Converter (promote)) where import Tablebot.Plugins.Roll.Dice.DiceData ( Converter (promote), @@ -64,7 +64,7 @@ import Tablebot.Plugins.Roll.Dice.DiceData ListValues (..), NumBase (Value), ) -import Tablebot.Plugins.Roll.Dice.DiceEval (ParseShow (parseShow), evalInteger, evalList) +import Tablebot.Plugins.Roll.Dice.DiceEval (evalInteger, evalList) import Tablebot.Plugins.Roll.Dice.DiceFunctions (integerFunctionsList, listFunctionsList) import Tablebot.Plugins.Roll.Dice.DiceParsing () diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index a6d143fb..491ae2e7 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs @@ -1,5 +1,3 @@ -{-# OPTIONS_GHC -Wno-orphans #-} - -- | -- Module : Tablebot.Plugins.Roll.Dice.DiceEval -- Description : How to evaluate dice and expressions diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 146fdd06..467c0085 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -32,7 +32,7 @@ import Tablebot.Plugins.Roll.Dice import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Utility import Tablebot.Utility.Discord (interactionResponseCustomMessage, sendCustomMessage, toMention') -import Tablebot.Utility.Parser (inlineCommandHelper) +import Tablebot.Utility.Parser (ParseShow (parseShow), inlineCommandHelper) import Tablebot.Utility.SmartParser import Text.Megaparsec (MonadParsec (eof, try), choice) import Text.RawString.QQ (r) @@ -71,14 +71,14 @@ rollDice' e t u@(ParseUserId uid) = do { messageDetailsComponents = Just [ ComponentActionRowButton - [ ComponentButton ((("rollreroll " <> pack (show uid)) `appendIf` (parseShow <$> e)) `appendIf` (quote <$> t)) False ButtonStyleSecondary "Reroll" (Just (Emoji (Just 0) "🎲" Nothing Nothing Nothing (Just False))) + [ ComponentButton ((("rollreroll " <> pack (show uid)) `appendIf` e) `appendIf` t) False ButtonStyleSecondary "Reroll" (Just (Emoji (Just 0) "🎲" Nothing Nothing Nothing (Just False))) ] ] } ) where appendIf t' Nothing = t' - appendIf t' (Just e') = t' <> " " <> e' + appendIf t' (Just e') = t' <> " " <> parseShow e' rollSlashCommandFunction :: Labelled "expression" "what's being evaluated" (Maybe Text) -> Labelled "quote" "associated message" (Maybe (Quoted Text)) -> ParseUserId -> DatabaseDiscord MessageDetails rollSlashCommandFunction (Labelled mt) (Labelled qt) uid = do @@ -100,8 +100,9 @@ rollDiceParser = choice (try <$> options) try (parseComm (rollDice' Nothing . Just)) ] --- | Manually creating parser for this command, since SmartCommand doesn't work fully for --- multiple Maybe values +-- | Creating a parser for the component interactions stuff. Needs to be +-- manually made since I think the maybe parser stuff doesn't work properly +-- still? rollDiceParserI :: Parser (Interaction -> DatabaseDiscord MessageDetails) rollDiceParserI = choice (try <$> options) where diff --git a/src/Tablebot/Utility/Parser.hs b/src/Tablebot/Utility/Parser.hs index cc5cd45b..b21f2ce0 100644 --- a/src/Tablebot/Utility/Parser.hs +++ b/src/Tablebot/Utility/Parser.hs @@ -203,3 +203,6 @@ class ParseShow a where instance (ParseShow a, ParseShow b) => ParseShow (Either a b) where parseShow (Left a) = parseShow a parseShow (Right b) = parseShow b + +instance ParseShow Text where + parseShow t = t diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index 9003ccbb..576e8e6c 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -107,6 +107,9 @@ newtype Quoted a = Qu {quote :: a} deriving (Show) instance IsString a => CanParse (Quoted a) where pars = Qu . fromString <$> quoted +instance (ParseShow a) => ParseShow (Quoted a) where + parseShow (Qu a) = "\"" <> parseShow a <> "\"" + -- A parser for @Maybe a@ attempts to parse @a@, returning @Just x@ if -- correctly parsed, else @Nothing@. instance CanParse a => CanParse (Maybe a) where From 46da6746c88c751e7171eb5a211deaf0777aee1c Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Tue, 25 Jan 2022 11:58:44 +0000 Subject: [PATCH 20/96] added default for message details --- src/Tablebot/Utility/Types.hs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Tablebot/Utility/Types.hs b/src/Tablebot/Utility/Types.hs index 7a082b24..03c8962a 100644 --- a/src/Tablebot/Utility/Types.hs +++ b/src/Tablebot/Utility/Types.hs @@ -374,6 +374,9 @@ envPlug name' startup = Pl name' startup [] [] [] [] [] [] [] [] [] [] [] messageDetailsBasic :: Text -> MessageDetails messageDetailsBasic t = MessageDetails Nothing (Just t) Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing +instance Default MessageDetails where + def = MessageDetails Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing + data MessageDetails = MessageDetails { messageDetailsTTS :: Maybe Bool, messageDetailsContent :: Maybe Text, From c305bab9394efcc1fe570af3b559f94aa64554a2 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Tue, 25 Jan 2022 12:01:32 +0000 Subject: [PATCH 21/96] commenting, shifting, cleaning, and clarifying --- src/Tablebot/Plugins/Roll/Plugin.hs | 28 +++----- src/Tablebot/Utility/SmartParser.hs | 99 +++++++++++++++++++++++------ 2 files changed, 89 insertions(+), 38 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 467c0085..183888a4 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -16,22 +16,20 @@ import Data.Text (Text, intercalate, pack, replicate, unpack) import qualified Data.Text as T import Discord.Interactions ( Interaction (..), - InteractionCallbackDataFlag (..), - InteractionCallbackDataFlags (..), ) import Discord.Types - ( ButtonStyle (ButtonStyleSecondary), - ComponentActionRow (ComponentActionRowButton), - ComponentButton (ComponentButton), + ( ComponentActionRow (ComponentActionRowButton), + ComponentButton (componentButtonEmoji), Emoji (Emoji), Message (messageAuthor), User (userId), + mkButton, ) import Tablebot.Internal.Handler.Command (parseValue) import Tablebot.Plugins.Roll.Dice import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Utility -import Tablebot.Utility.Discord (interactionResponseCustomMessage, sendCustomMessage, toMention') +import Tablebot.Utility.Discord (sendCustomMessage, toMention') import Tablebot.Utility.Parser (ParseShow (parseShow), inlineCommandHelper) import Tablebot.Utility.SmartParser import Text.Megaparsec (MonadParsec (eof, try), choice) @@ -71,7 +69,7 @@ rollDice' e t u@(ParseUserId uid) = do { messageDetailsComponents = Just [ ComponentActionRowButton - [ ComponentButton ((("rollreroll " <> pack (show uid)) `appendIf` e) `appendIf` t) False ButtonStyleSecondary "Reroll" (Just (Emoji (Just 0) "🎲" Nothing Nothing Nothing (Just False))) + [ (mkButton "Reroll" ((("rollreroll " <> pack (show uid)) `appendIf` e) `appendIf` t)) {componentButtonEmoji = Just (Emoji (Just 0) "🎲" Nothing Nothing Nothing (Just False))} ] ] } @@ -106,19 +104,11 @@ rollDiceParser = choice (try <$> options) rollDiceParserI :: Parser (Interaction -> DatabaseDiscord MessageDetails) rollDiceParserI = choice (try <$> options) where - localRollDice uid lv qt u@(ParseUserId uid') i - | uid == uid' = rollDice' lv qt u - | otherwise = - interactionResponseCustomMessage - i - ( (messageDetailsBasic "Hey, that isn't your button to press!") {messageDetailsFlags = Just $ InteractionCallbackDataFlags [InteractionCallbackDataFlagEphermeral]} - ) - >> return ((messageDetailsBasic "") {messageDetailsContent = Nothing}) options = - [ parseComm (\uid lv -> localRollDice uid (Just lv) Nothing), - parseComm (\uid -> localRollDice uid Nothing Nothing), - try (parseComm (\uid lv qt -> localRollDice uid (Just lv) (Just qt))), - try (parseComm (\uid qt -> localRollDice uid Nothing (Just qt))) + [ onlyAllowRequestor (\lv -> rollDice' (Just lv) Nothing), + onlyAllowRequestor (rollDice' Nothing Nothing), + try (onlyAllowRequestor (\lv qt -> rollDice' (Just lv) (Just qt))), + try (onlyAllowRequestor (rollDice' Nothing . Just)) ] -- | Basic command for rolling dice. diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index 576e8e6c..22ba4039 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -15,6 +15,7 @@ module Tablebot.Utility.SmartParser where import Control.Monad.Exception (MonadException (catch)) +import Data.Default (Default (def)) import Data.Proxy (Proxy (..)) import Data.String (IsString (fromString)) import Data.Text (Text, pack) @@ -35,6 +36,7 @@ class Context a where instance Context Message where contextUserId = ParseUserId . userId . messageAuthor +-- this is safe to do because we are guaranteed to get either a user or a member instance Context Interaction where contextUserId i = ParseUserId $ maybe 0 userId (maybe (interactionUser i) memberUser (interactionMember i)) @@ -48,26 +50,37 @@ class PComm commandty s context returns where parseComm :: (Context context) => commandty -> Parser (context -> EnvDatabaseDiscord s returns) -- TODO: verify that all the parsers for PComm actually work --- As a base case, remove the spacing and check for eof. + +-- If there is the general case where we have just what we want to parse, then +-- return it instance {-# OVERLAPPING #-} PComm (t -> EnvDatabaseDiscord s r) s t r where + parseComm comm = skipSpace >> return comm + +-- If we have the specific case where we are returning `()`, parse eof as well. +-- This should cover the base case for the rest of the program that doesn't use +-- more complex stuff. +instance {-# OVERLAPPING #-} PComm (t -> EnvDatabaseDiscord s ()) s t () where parseComm comm = skipSpace >> eof >> return comm -instance {-# OVERLAPPING #-} PComm (EnvDatabaseDiscord s MessageDetails) s Message () where - parseComm comm = skipSpace >> eof >> return (\m -> comm >>= sendCustomMessage m) +-- If an action takes a message and returns a message details and we want it to +-- return unit, assume that it wants to be sent, and send it. eof this as well +instance {-# OVERLAPPING #-} PComm (Message -> EnvDatabaseDiscord s MessageDetails) s Message () where + parseComm comm = skipSpace >> eof >> return (\m -> comm m >>= sendCustomMessage m) +-- Just the action. effectively the function hasn't interacted with the `t`. +-- don't parse eof cause we may wanna return instance {-# OVERLAPPING #-} PComm (EnvDatabaseDiscord s r) s t r where - parseComm comm = skipSpace >> eof >> return (const comm) + parseComm comm = skipSpace >> return (const comm) -instance {-# OVERLAPPING #-} PComm (Message -> EnvDatabaseDiscord s MessageDetails) s Message () where - parseComm comm = skipSpace >> eof >> return (\m -> comm m >>= sendCustomMessage m) +-- Just the action. effectively the function hasn't interacted with the `t`. +-- parse eof because we have unit here +instance {-# OVERLAPPING #-} PComm (EnvDatabaseDiscord s ()) s t () where + parseComm comm = skipSpace >> eof >> return (const comm) --- TODO: verify that this second base case is no longer needed --- -- Second base case is the single argument - no trailing space is wanted so we --- -- have to specify this case. --- instance {-# OVERLAPPING #-} CanParse a => PComm (a -> Message -> EnvDatabaseDiscord s ()) s where --- parseComm comm = do --- this <- pars @a --- parseComm (comm this) +-- if we're in a message context and have a message details but want to return +-- unit, assume that we want to send it, and send it. +instance {-# OVERLAPPING #-} PComm (EnvDatabaseDiscord s MessageDetails) s Message () where + parseComm comm = skipSpace >> eof >> return (\m -> comm >>= sendCustomMessage m) -- Recursive case is to parse the domain of the function type, then the rest. instance {-# OVERLAPPABLE #-} (CanParse a, PComm as s t r) => PComm (a -> as) s t r where @@ -75,14 +88,20 @@ instance {-# OVERLAPPABLE #-} (CanParse a, PComm as s t r) => PComm (a -> as) s this <- parsThenMoveToNext @a parseComm (comm this) +-- if we have two contexts for some reason, collapse them if the resultant can +-- be parsed instance {-# OVERLAPPABLE #-} (PComm (t -> as) s t r) => PComm (t -> t -> as) s t r where parseComm comm = parseComm (\m -> comm m m) +-- if we have a context and then some parseable value, effectively juggle the +-- context so that parsing continues (and the context is passed on) instance {-# OVERLAPPABLE #-} (Context t, CanParse a, PComm (t -> as) s t r) => PComm (t -> a -> as) s t r where parseComm comm = do this <- parsThenMoveToNext @a parseComm (`comm` this) +-- special value case - if we get ParseUserId, we need to get the value from +-- the context. so, get the value from the context, and then continue parsing. instance {-# OVERLAPPABLE #-} (PComm (t -> as) s t r) => PComm (ParseUserId -> as) s t r where parseComm comm = parseComm $ \(m :: t) -> comm (contextUserId m) @@ -197,7 +216,7 @@ instance CanParse () where pars = eof instance CanParse Snowflake where - pars = Snowflake . fromInteger <$> pars + pars = Snowflake . fromInteger <$> posInteger -- | @RestOfInput a@ parses the rest of the input, giving a value of type @a@. newtype RestOfInput a = ROI a @@ -329,20 +348,20 @@ instance (KnownSymbol name, ProcessAppCommArg (Labelled name desc t) s) => Proce `catchBot` const (return $ labelValue Nothing) -- | Given a function that can be processed to create a parser, create an action --- for it using the helper. +-- for it using the helper. Uses `parseComm` to generate the required parser. -- --- If the boolean is true, the message the component is from is updated. Else, --- a message is sent as the interaction response. +-- For more information, check the helper `processComponentInteraction'`. processComponentInteraction :: (PComm f s Interaction MessageDetails) => f -> Bool -> Interaction -> EnvDatabaseDiscord s () processComponentInteraction f = processComponentInteraction' (parseComm f) --- TODO: comment what is given to the parser - -- | Given a parser that, when run, returns a function taking an interaction -- and returns a database action on some MessageDetails, run the action. -- -- If the boolean is true, the message the component is from is updated. Else, -- a message is sent as the interaction response. +-- +-- The format of the Text being given should be of space separated values, +-- similar to the command structure. processComponentInteraction' :: Parser (Interaction -> EnvDatabaseDiscord s MessageDetails) -> Bool -> Interaction -> EnvDatabaseDiscord s () processComponentInteraction' compParser updateOriginal i@InteractionComponent {interactionDataComponent = Just idc} = errorCatch $ do let componentSend @@ -355,3 +374,45 @@ processComponentInteraction' compParser updateOriginal i@InteractionComponent {i catchParserException e = interactionResponseCustomMessage i $ (messageDetailsBasic "") {messageDetailsEmbeds = Just [embedError (e :: BotException)]} errorCatch = (`catch` catchParserException) processComponentInteraction' _ _ _ = throwBot $ InteractionException "could not process component interaction" + +-- | Function to only allow use of an interaction if the requestor matches +-- a Snowflake at the beginning of the input. This uses a helper, and by default +-- sends an ephermeral message with the text "You don't have permission to use +-- this component." +-- +-- Helper is `onlyAllowRequestor'`. +onlyAllowRequestor :: forall f. (PComm f () Interaction MessageDetails) => f -> Parser (Interaction -> DatabaseDiscord MessageDetails) +onlyAllowRequestor = + onlyAllowRequestor' + ( (messageDetailsBasic "You don't have permission to use this component.") {messageDetailsFlags = Just $ InteractionCallbackDataFlags [InteractionCallbackDataFlagEphermeral]} + ) + +-- | Take a message to send when a user that is not the one that created a +-- component, and then parse out a user id, and then get the interaction +-- requestor's userid, check if they match, and if they don't then send a +-- message. Regardless, parse out the given function. If it _does_ match, run +-- the parsed function. +-- +-- Adds eof to the end to ensure all the data is parsed. +onlyAllowRequestor' :: forall f. (PComm f () Interaction MessageDetails) => MessageDetails -> f -> Parser (Interaction -> DatabaseDiscord MessageDetails) +onlyAllowRequestor' msg f = do + pre <- parseComm prefunc + f' <- parseComm @f @() @Interaction @MessageDetails f + parseComm + ( \i -> do + isEqual <- pre i + case isEqual of + Nothing -> f' i + Just d -> return d + ) + <* eof + where + prefunc :: UserId -> ParseUserId -> Interaction -> DatabaseDiscord (Maybe MessageDetails) + prefunc uid (ParseUserId u) i = + if uid == u + then return Nothing + else + interactionResponseCustomMessage + i + msg + >> return (Just def) From 24acbeaeeee6537cbfaa3118018c2b632fd6518f Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Tue, 25 Jan 2022 12:14:29 +0000 Subject: [PATCH 22/96] finished tidying. todo: add stuff to quote, make stack.yaml imports proper --- src/Tablebot/Utility/Discord.hs | 7 ++++--- src/Tablebot/Utility/Types.hs | 11 ----------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/Tablebot/Utility/Discord.hs b/src/Tablebot/Utility/Discord.hs index 92a3bcd8..ccc954cc 100644 --- a/src/Tablebot/Utility/Discord.hs +++ b/src/Tablebot/Utility/Discord.hs @@ -85,7 +85,7 @@ sendMessage m t = do -- | @sendCustomMessage@ sends the input message @mdo@ in the same channel as -- message @m@. -- --- As opposed to @sendMessage@, this function takes in a MessageDetailedOpts, to +-- As opposed to @sendMessage@, this function takes in a MessageDetails, to -- allow full functionality. Unless you are dealing with components or some -- other specific message data, you shouldn't use this function. sendCustomMessage :: @@ -197,7 +197,7 @@ reactToMessage m e = getReplyMessage :: Message -> EnvDatabaseDiscord s (Maybe Message) getReplyMessage m = do let m' = messageReferencedMessage m - let mRef = messageReference m + mRef = messageReference m case m' of Just msg -> return $ Just msg Nothing -> case mRef of @@ -393,7 +393,8 @@ interactionResponseDefer i = do -- | Defer an interaction response, extending the window of time to respond to -- 15 minutes (from 3 seconds). -- --- Used when updating a component message. +-- Used when updating a component message. Does not show that the bot is +-- thinking about the interaction. interactionResponseDeferUpdateMessage :: Interaction -> EnvDatabaseDiscord s () interactionResponseDeferUpdateMessage i = do res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponse InteractionCallbackTypeDeferredUpdateMessage Nothing) diff --git a/src/Tablebot/Utility/Types.hs b/src/Tablebot/Utility/Types.hs index 03c8962a..8e4f804e 100644 --- a/src/Tablebot/Utility/Types.hs +++ b/src/Tablebot/Utility/Types.hs @@ -162,17 +162,6 @@ newtype EnvReactionDel d = ReactionDel type ReactionDel = EnvReactionDel () --- | Handles recieving of interactions, such as for application commands (slash --- commands, user commands, message commands), as well as components from --- messages. --- --- Rarely used by itself. --- newtype EnvInteractionRecv d = InteractionRecv --- { -- | A function to call on an interaction, which takes in details of that --- -- interaction --- onInteractionRecv :: Interaction -> EnvDatabaseDiscord d () --- } - -- | Handles the creation of an application command and of the action to be -- performed once that application command is received. data EnvApplicationCommandRecv d = ApplicationCommandRecv From 381b7725aeddfc77d78f5424bf6c6c72f630dd6f Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 26 Jan 2022 23:18:48 +0000 Subject: [PATCH 23/96] made some changes to be in line with changes to discord-haskell --- src/Tablebot/Internal/Handler/Event.hs | 6 +++--- src/Tablebot/Utility/Discord.hs | 12 +++++------ src/Tablebot/Utility/SmartParser.hs | 30 +++++++++++++++----------- src/Tablebot/Utility/Types.hs | 8 +++---- stack.yaml | 2 +- 5 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/Tablebot/Internal/Handler/Event.hs b/src/Tablebot/Internal/Handler/Event.hs index 927e89f5..83cc54fe 100644 --- a/src/Tablebot/Internal/Handler/Event.hs +++ b/src/Tablebot/Internal/Handler/Event.hs @@ -60,17 +60,17 @@ parseReactionDel cs info = mapM_ doReactionAdd cs -- | When given the compiled component recv actions and a component interaction, -- find and run the correct action. parseComponentRecv :: [CompiledComponentRecv] -> Interaction -> CompiledDatabaseDiscord () -parseComponentRecv cs info@InteractionComponent {interactionDataComponent = Just idc} = mapM_ removePrefix cs' +parseComponentRecv cs info@InteractionComponent {interactionDataComponent = idc} = mapM_ removePrefix cs' where getPrefix ccr = componentPluginName ccr <> componentName ccr cs' = filter (\ccr -> getPrefix ccr `isPrefixOf` interactionDataComponentCustomId idc) cs - removePrefix ccr = ccr `onComponentRecv` (info {interactionDataComponent = Just (idc {interactionDataComponentCustomId = T.drop (T.length (getPrefix ccr)) (interactionDataComponentCustomId idc)})}) + removePrefix ccr = ccr `onComponentRecv` (info {interactionDataComponent = (idc {interactionDataComponentCustomId = T.drop (T.length (getPrefix ccr)) (interactionDataComponentCustomId idc)})}) parseComponentRecv _ _ = return () -- | When given an application command interaction, find and run the correct -- action. parseApplicationCommandRecv :: Interaction -> CompiledDatabaseDiscord () -parseApplicationCommandRecv info@InteractionApplicationCommand {interactionDataApplicationCommand = Just idac} = do +parseApplicationCommandRecv info@InteractionApplicationCommand {interactionDataApplicationCommand = idac} = do tvar <- ask cache <- liftIO $ readMVar tvar let action = UT.cacheApplicationCommands cache M.!? interactionDataApplicationCommandId idac diff --git a/src/Tablebot/Utility/Discord.hs b/src/Tablebot/Utility/Discord.hs index ccc954cc..21d60f38 100644 --- a/src/Tablebot/Utility/Discord.hs +++ b/src/Tablebot/Utility/Discord.hs @@ -385,7 +385,7 @@ removeApplicationCommandsNotInList aid gid aciToKeep = do -- 15 minutes (from 3 seconds). interactionResponseDefer :: Interaction -> EnvDatabaseDiscord s () interactionResponseDefer i = do - res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponse InteractionCallbackTypeDeferredChannelMessageWithSource Nothing) + res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) InteractionResponseDeferChannelMessage case res of Left _ -> throw $ InteractionException "Failed to defer interaction." Right _ -> return () @@ -397,7 +397,7 @@ interactionResponseDefer i = do -- thinking about the interaction. interactionResponseDeferUpdateMessage :: Interaction -> EnvDatabaseDiscord s () interactionResponseDeferUpdateMessage i = do - res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponse InteractionCallbackTypeDeferredUpdateMessage Nothing) + res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) InteractionResponseDeferUpdateMessage case res of Left _ -> throw $ InteractionException "Failed to defer interaction." Right _ -> return () @@ -409,7 +409,7 @@ interactionResponseMessage i t = interactionResponseCustomMessage i (messageDeta -- | Respond to the given interaction with a custom messages object. interactionResponseCustomMessage :: Interaction -> MessageDetails -> EnvDatabaseDiscord s () interactionResponseCustomMessage i t = do - res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponse InteractionCallbackTypeChannelMessageWithSource (Just $ InteractionCallbackDataMessages $ convertMessageFormatInteraction t)) + res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponseChannelMessage (convertMessageFormatInteraction t)) case res of Left _ -> throw $ InteractionException "Failed to respond to interaction." Right _ -> return () @@ -417,15 +417,15 @@ interactionResponseCustomMessage i t = do -- | Respond to the given interaction by updating the component's message. interactionResponseComponentsUpdateMessage :: Interaction -> MessageDetails -> EnvDatabaseDiscord s () interactionResponseComponentsUpdateMessage i t = do - res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponse InteractionCallbackTypeUpdateMessage (Just $ InteractionCallbackDataMessages $ convertMessageFormatInteraction t)) + res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponseUpdateMessage (convertMessageFormatInteraction t)) case res of Left _ -> throw $ InteractionException "Failed to respond to interaction with components update." Right _ -> return () -- | Respond to the given interaction by sending a list of choices back. -interactionResponseAutocomplete :: Interaction -> InteractionCallbackAutocomplete -> EnvDatabaseDiscord s () +interactionResponseAutocomplete :: Interaction -> InteractionResponseAutocomplete -> EnvDatabaseDiscord s () interactionResponseAutocomplete i ac = do - res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponse InteractionCallbackTypeApplicationCommandAutocompleteResult (Just $ InteractionCallbackDataAutocomplete ac)) + res <- liftDiscord $ restCall $ R.CreateInteractionResponse (interactionId i) (interactionToken i) (InteractionResponseAutocompleteResult ac) case res of Left _ -> throw $ InteractionException "Failed to respond to interaction with autocomplete response." Right _ -> return () diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index 22ba4039..a349920f 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -38,7 +38,9 @@ instance Context Message where -- this is safe to do because we are guaranteed to get either a user or a member instance Context Interaction where - contextUserId i = ParseUserId $ maybe 0 userId (maybe (interactionUser i) memberUser (interactionMember i)) + contextUserId i = ParseUserId $ maybe 0 userId (either memberUser Just mor) + where + (MemberOrUser mor) = interactionUser i -- | @PComm@ defines function types that we can automatically turn into parsers -- by composing a parser per input of the function provided. @@ -284,14 +286,14 @@ class MakeAppCommArg commandty where makeAppCommArg :: Proxy commandty -> ApplicationCommandOptionValue instance (KnownSymbol name, KnownSymbol desc) => MakeAppCommArg (Labelled name desc Text) where - makeAppCommArg l = ApplicationCommandOptionValueString n d (Just True) Nothing Nothing + makeAppCommArg l = ApplicationCommandOptionValueString n d True (Left False) where (n, d) = getLabelValues l instance (KnownSymbol name, KnownSymbol desc, MakeAppCommArg (Labelled name desc t)) => MakeAppCommArg (Labelled name desc (Maybe t)) where makeAppCommArg _ = (makeAppCommArg (Proxy :: Proxy (Labelled name desc t))) - { applicationCommandOptionValueRequired = Just False + { applicationCommandOptionValueRequired = False } instance (KnownSymbol name, KnownSymbol desc, MakeAppCommArg (Labelled name desc t)) => MakeAppCommArg (Labelled name desc (Quoted t)) where @@ -303,36 +305,40 @@ class ProcessAppComm commandty s where processAppComm :: commandty -> Interaction -> EnvDatabaseDiscord s () processAppComm _ _ = throwBot $ InteractionException "could not process args to application command" +-- One base case instance {-# OVERLAPPING #-} ProcessAppComm (EnvDatabaseDiscord s MessageDetails) s where processAppComm comm i = comm >>= interactionResponseCustomMessage i +-- One simple recursive case instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s) => ProcessAppComm (Interaction -> pac) s where processAppComm comm i = processAppComm (comm i) i -instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s, ProcessAppCommArg ty s) => ProcessAppComm (ty -> pac) s where - processAppComm comm i@InteractionApplicationCommand {interactionDataApplicationCommand = Just InteractionDataApplicationCommandChatInput {interactionDataApplicationCommandOptions = (Just (InteractionDataApplicationCommandOptionsValues values))}} = do +-- one overarching recursive case +instance {-# OVERLAPPABLE #-} (ProcessAppCommArg ty s, ProcessAppComm pac s) => ProcessAppComm (ty -> pac) s where + processAppComm comm i@InteractionApplicationCommand {interactionDataApplicationCommand = InteractionDataApplicationCommandChatInput {interactionDataApplicationCommandOptions = (Just (InteractionDataApplicationCommandOptionsValues values))}} = do t <- processAppCommArg values processAppComm (comm t) i processAppComm _ _ = throwBot $ InteractionException "could not process args to application command" +-- one specific implementation case instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s) => ProcessAppComm (ParseUserId -> pac) s where - processAppComm comm i@InteractionApplicationCommand {} = + processAppComm comm i@InteractionApplicationCommand {interactionUser = MemberOrUser u} = case getUser of Nothing -> throwBot $ InteractionException "could not process args to application command" Just uid -> processAppComm (comm (ParseUserId uid)) i where - getUser = userId <$> maybe (interactionUser i) memberUser (interactionMember i) + getUser = userId <$> either memberUser Just u processAppComm _ _ = throwBot $ InteractionException "could not process args to application command" class ProcessAppCommArg t s where processAppCommArg :: [InteractionDataApplicationCommandOptionValue] -> EnvDatabaseDiscord s t -getValue :: String -> [InteractionDataApplicationCommandOptionValue] -> Maybe ApplicationCommandInteractionDataValue -getValue t is = interactionDataApplicationCommandOptionValueValue <$> find ((== pack t) . interactionDataApplicationCommandOptionValueName) is +getValue :: String -> [InteractionDataApplicationCommandOptionValue] -> Maybe InteractionDataApplicationCommandOptionValue +getValue t = find ((== pack t) . interactionDataApplicationCommandOptionValueName) instance (KnownSymbol name) => ProcessAppCommArg (Labelled name desc Text) s where processAppCommArg is = case getValue (symbolVal (Proxy :: Proxy name)) is of - Just (ApplicationCommandInteractionDataValueString t) -> return $ labelValue t + Just (InteractionDataApplicationCommandOptionValueString _ (Right t)) -> return $ labelValue t _ -> throwBot $ InteractionException "could not find required parameter" instance (KnownSymbol name, KnownSymbol desc, ProcessAppCommArg (Labelled name desc t) s) => ProcessAppCommArg (Labelled name desc (Quoted t)) s where @@ -363,7 +369,7 @@ processComponentInteraction f = processComponentInteraction' (parseComm f) -- The format of the Text being given should be of space separated values, -- similar to the command structure. processComponentInteraction' :: Parser (Interaction -> EnvDatabaseDiscord s MessageDetails) -> Bool -> Interaction -> EnvDatabaseDiscord s () -processComponentInteraction' compParser updateOriginal i@InteractionComponent {interactionDataComponent = Just idc} = errorCatch $ do +processComponentInteraction' compParser updateOriginal i@InteractionComponent {interactionDataComponent = idc} = errorCatch $ do let componentSend | updateOriginal = interactionResponseComponentsUpdateMessage i | otherwise = interactionResponseCustomMessage i @@ -384,7 +390,7 @@ processComponentInteraction' _ _ _ = throwBot $ InteractionException "could not onlyAllowRequestor :: forall f. (PComm f () Interaction MessageDetails) => f -> Parser (Interaction -> DatabaseDiscord MessageDetails) onlyAllowRequestor = onlyAllowRequestor' - ( (messageDetailsBasic "You don't have permission to use this component.") {messageDetailsFlags = Just $ InteractionCallbackDataFlags [InteractionCallbackDataFlagEphermeral]} + ( (messageDetailsBasic "You don't have permission to use this component.") {messageDetailsFlags = Just $ InteractionResponseMessageFlags [InteractionResponseMessageFlagEphermeral]} ) -- | Take a message to send when a user that is not the one that created a diff --git a/src/Tablebot/Utility/Types.hs b/src/Tablebot/Utility/Types.hs index 8e4f804e..b073f05c 100644 --- a/src/Tablebot/Utility/Types.hs +++ b/src/Tablebot/Utility/Types.hs @@ -24,7 +24,7 @@ import Data.Text (Text) import Data.Void (Void) import Database.Persist.Sqlite (Migration, SqlPersistM, SqlPersistT) import Discord (DiscordHandler) -import Discord.Interactions (CreateApplicationCommand, Interaction, InteractionCallbackDataFlags, InteractionCallbackMessages (InteractionCallbackMessages)) +import Discord.Interactions (CreateApplicationCommand, Interaction, InteractionResponseMessage (InteractionResponseMessage), InteractionResponseMessageFlags) import Discord.Internal.Rest.Channel (MessageDetailedOpts (MessageDetailedOpts)) import Discord.Types ( AllowedMentions, @@ -372,16 +372,16 @@ data MessageDetails = MessageDetails messageDetailsEmbeds :: Maybe [Embed], messageDetailsFile :: Maybe (Text, ByteString), messageDetailsAllowedMentions :: Maybe AllowedMentions, - messageDetailsFlags :: Maybe InteractionCallbackDataFlags, + messageDetailsFlags :: Maybe InteractionResponseMessageFlags, messageDetailsReference :: Maybe MessageReference, messageDetailsComponents :: Maybe [ComponentActionRow], messageDetailsAttachments :: Maybe [Attachment], messageDetailsStickerIds :: Maybe [StickerId] } -convertMessageFormatInteraction :: MessageDetails -> InteractionCallbackMessages +convertMessageFormatInteraction :: MessageDetails -> InteractionResponseMessage convertMessageFormatInteraction MessageDetails {..} = - InteractionCallbackMessages + InteractionResponseMessage messageDetailsTTS messageDetailsContent messageDetailsEmbeds diff --git a/stack.yaml b/stack.yaml index 17fb9a29..64408564 100644 --- a/stack.yaml +++ b/stack.yaml @@ -41,7 +41,7 @@ packages: # extra-deps: - git: https://github.com/L0neGamer/discord-haskell.git - commit: 403ebc9cc0cf1f2367c0ad2677fcf2ad0c76e133 + commit: 8ac23e4598f501136109090a8ddb073eaac59889 - emoji-0.1.0.2 - load-env-0.2.1.0 - megaparsec-9.0.1 From 94ac8b937c4aa5ff2db19bac55f943cf72f0261a Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 27 Jan 2022 15:16:29 +0000 Subject: [PATCH 24/96] updating to base docs and moving to CreateEmbed because it's honestly pretty cool (also I borrowed your code and put it in the library sorry anna) --- src/Tablebot/Internal/Embed.hs | 78 +----------------- src/Tablebot/Plugins/Netrunner/Plugin.hs | 14 ++-- .../Plugins/Netrunner/Utility/Card.hs | 4 +- .../Plugins/Netrunner/Utility/Embed.hs | 44 +++++----- src/Tablebot/Plugins/Quote.hs | 17 +++- src/Tablebot/Utility/Discord.hs | 5 +- src/Tablebot/Utility/Embed.hs | 43 +++++----- src/Tablebot/Utility/Exception.hs | 5 +- src/Tablebot/Utility/Types.hs | 81 +------------------ stack.yaml | 2 +- 10 files changed, 81 insertions(+), 212 deletions(-) diff --git a/src/Tablebot/Internal/Embed.hs b/src/Tablebot/Internal/Embed.hs index e533b2bd..d8109f9e 100644 --- a/src/Tablebot/Internal/Embed.hs +++ b/src/Tablebot/Internal/Embed.hs @@ -9,84 +9,14 @@ -- This module contains some behind the scenes logic to allow creation of coloured embeds module Tablebot.Internal.Embed where -import Data.Aeson -import qualified Data.ByteString.Lazy as BL import Data.Text (Text) -import Discord.Internal.Rest.Prelude -import Discord.Internal.Types -import Network.HTTP.Client.MultipartFormData (partBS) -import Network.HTTP.Req ((/:)) -import qualified Network.HTTP.Req as R -import Tablebot.Utility.Types - -colourToInternal :: DiscordColour -> Integer -colourToInternal (RGB r g b) = ((r * 256) + g) * 256 + b -colourToInternal Default = 0 -colourToInternal Aqua = 1752220 -colourToInternal DarkAqua = 1146986 -colourToInternal Green = 3066993 -colourToInternal DarkGreen = 2067276 -colourToInternal Blue = 3447003 -colourToInternal DarkBlue = 2123412 -colourToInternal Purple = 10181046 -colourToInternal DarkPurple = 7419530 -colourToInternal LuminousVividPink = 15277667 -colourToInternal DarkVividPink = 11342935 -colourToInternal Gold = 15844367 -colourToInternal DarkGold = 12745742 -colourToInternal Orange = 15105570 -colourToInternal DarkOrange = 11027200 -colourToInternal Red = 15158332 -colourToInternal DarkRed = 10038562 -colourToInternal Gray = 9807270 -colourToInternal DarkGray = 9936031 -colourToInternal DarkerGray = 8359053 -colourToInternal LightGray = 12370112 -colourToInternal Navy = 3426654 -colourToInternal DarkNavy = 2899536 -colourToInternal Yellow = 16776960 -colourToInternal DiscordWhite = 16777215 -colourToInternal DiscordBlurple = 5793266 -colourToInternal DiscordGrayple = 10070709 -colourToInternal DiscordDarkButNotBlack = 2895667 -colourToInternal DiscordNotQuiteBlack = 2303786 -colourToInternal DiscordGreen = 5763719 -colourToInternal DiscordYellow = 16705372 -colourToInternal DiscordFuschia = 15418782 -colourToInternal DiscordRed = 15548997 -colourToInternal DiscordBlack = 16777215 - --- | TablebotEmbedRequest is a request object that mimics various bits of the discord api, just so we can add colours. --- This is *really* janky. The library exposes *no way* to create a coloured embed through its main api, --- so I'm having to manually reimplement the sending logic just to add this in. --- If you suffer from nightmares, don't look in 'Tablebot.Handler.Embed'. Nothing good lives there. --- In the future, I may actually submit a PR to discord-haskell with a fix to allow colours properly. -channels :: R.Url 'R.Https -channels = baseUrl /: "channels" - -data TablebotEmbedRequest a where TablebotEmbedRequest :: ChannelId -> Text -> Embed -> TablebotEmbedRequest Message - -instance Request (TablebotEmbedRequest a) where - jsonRequest = createEmbedJson - majorRoute = embedMajorRoute - -embedMajorRoute :: TablebotEmbedRequest a -> String -embedMajorRoute (TablebotEmbedRequest chan _ _) = "msg " <> show chan - -createEmbedJson :: TablebotEmbedRequest a -> JsonRequest -createEmbedJson (TablebotEmbedRequest chan msg embed) = - let partJson = partBS "payload_json" $ BL.toStrict $ encode $ toJSON $ object ["content" .= msg, "embed" .= embed] - body = R.reqBodyMultipart [partJson] - in Post (channels // chan /: "messages") body mempty +import Discord.Internal.Types (CreateEmbed (CreateEmbed)) class Embeddable e where - asEmbed :: e -> Embed - -instance Embeddable Embed where - asEmbed = id + asEmbed :: e -> CreateEmbed instance Embeddable CreateEmbed where - asEmbed = createEmbed + asEmbed = id instance Embeddable Text where - asEmbed t = createEmbed $ CreateEmbed "" "" Nothing "" "" Nothing t [] Nothing "" Nothing Nothing + asEmbed t = CreateEmbed "" "" Nothing "" "" Nothing t [] Nothing "" Nothing Nothing Nothing diff --git a/src/Tablebot/Plugins/Netrunner/Plugin.hs b/src/Tablebot/Plugins/Netrunner/Plugin.hs index b8d071e9..cf18c490 100644 --- a/src/Tablebot/Plugins/Netrunner/Plugin.hs +++ b/src/Tablebot/Plugins/Netrunner/Plugin.hs @@ -192,8 +192,8 @@ nrRules = Command "rules" (parseComm rulesComm) [] rulesComm :: RestOfInput Text -> Message -> EnvDatabaseDiscord NrApi () rulesComm (ROI q) m = do let (rTitle, rBody, colour) = case getRuling q of - Left (Ruling t b) -> (t, b, Red) - Right (Ruling t b) -> (t, b, Blue) + Left (Ruling t b) -> (t, b, DiscordColorRed) + Right (Ruling t b) -> (t, b, DiscordColorBlue) sendEmbedMessage m "" $ addColour colour $ embedText rTitle rBody -- | @embedCard@ takes a card and embeds it in a message. @@ -234,9 +234,9 @@ embedBanHistory card m = do api <- ask embed <- cardToEmbedWithText api card $ listBanHistory api card let colour = case toMwlStatus api (activeBanList api) card of - Banned -> Red - Legal -> Green - _ -> Yellow + Banned -> DiscordColorRed + Legal -> DiscordColorGreen + _ -> DiscordColorYellow sendEmbedMessage m "" $ addColour colour embed -- | @embedBanLists@ embeds all banlists in Netrunner history. @@ -244,7 +244,7 @@ embedBanLists :: Message -> EnvDatabaseDiscord NrApi () embedBanLists m = do api <- ask let embed = embedTextWithUrl "Standard Banlists" "https://netrunnerdb.com/en/banlists" $ listBanLists api - colour = if latestBanListActive api then Red else Yellow + colour = if latestBanListActive api then DiscordColorRed else DiscordColorYellow sendEmbedMessage m "" $ addColour colour embed -- | @embedBanList@ embeds the list of cards affected by a given banlist. @@ -253,7 +253,7 @@ embedBanList banList m = do api <- ask let (pre, cCards, rCards) = listAffectedCards api banList header = BanList.name banList <> if active banList then " (active)" else "" - colour = if active banList then Red else Yellow + colour = if active banList then DiscordColorRed else DiscordColorYellow sendEmbedMessage m "" $ addColour colour $ embedColumns header pre [("Corp Cards", cCards), ("Runner Cards", rCards)] beginnerText :: EnvDatabaseDiscord NrApi Text diff --git a/src/Tablebot/Plugins/Netrunner/Utility/Card.hs b/src/Tablebot/Plugins/Netrunner/Utility/Card.hs index f7c9523b..b55e0416 100644 --- a/src/Tablebot/Plugins/Netrunner/Utility/Card.hs +++ b/src/Tablebot/Plugins/Netrunner/Utility/Card.hs @@ -174,8 +174,8 @@ toReleaseData api card = fromMaybe "" helper return $ faction <> " • " <> expansion <> pos -- | @toColour@ gets the factional colour of a card to use in its embed. -toColour :: NrApi -> Card -> DiscordColour -toColour api card = maybe Default (hexToDiscordColour . unpack . Faction.colour) (toFaction api card) +toColour :: NrApi -> Card -> DiscordColor +toColour api card = maybe DiscordColorDefault (hexToDiscordColor . unpack . Faction.colour) (toFaction api card) -- | @toFlavour@ gets a cards flavour text (and makes it italic). toFlavour :: Card -> EnvDatabaseDiscord NrApi (Maybe Text) diff --git a/src/Tablebot/Plugins/Netrunner/Utility/Embed.hs b/src/Tablebot/Plugins/Netrunner/Utility/Embed.hs index 45b6a22f..f63ccbb6 100644 --- a/src/Tablebot/Plugins/Netrunner/Utility/Embed.hs +++ b/src/Tablebot/Plugins/Netrunner/Utility/Embed.hs @@ -37,7 +37,7 @@ import Tablebot.Utility.Types () import Prelude hiding (unwords) -- | @cardToEmbed@ takes a card and generates an embed message representing it. -cardToEmbed :: NrApi -> Card -> EnvDatabaseDiscord NrApi Embed +cardToEmbed :: NrApi -> Card -> EnvDatabaseDiscord NrApi CreateEmbed cardToEmbed api card = do let eTitle = toTitle card eURL = toLink card @@ -45,25 +45,25 @@ cardToEmbed api card = do eImg = toImage api card eColour = toColour api card eText <- toText card - return $ addColour eColour $ createEmbed $ CreateEmbed "" "" Nothing eTitle eURL eImg eText [] Nothing eFoot Nothing Nothing + return $ addColour eColour $ CreateEmbed "" "" Nothing eTitle eURL eImg eText [] Nothing eFoot Nothing Nothing Nothing -- | @cardToEmbedWithText@ embeds some text and decorates it with a given card. -cardToEmbedWithText :: NrApi -> Card -> Text -> EnvDatabaseDiscord NrApi Embed +cardToEmbedWithText :: NrApi -> Card -> Text -> EnvDatabaseDiscord NrApi CreateEmbed cardToEmbedWithText api card text = do let eTitle = toTitle card eURL = toLink card eColour = toColour api card eImg = toImage api card - return $ addColour eColour $ createEmbed $ CreateEmbed "" "" Nothing eTitle eURL eImg text [] Nothing "" Nothing Nothing + return $ addColour eColour $ CreateEmbed "" "" Nothing eTitle eURL eImg text [] Nothing "" Nothing Nothing Nothing -- | @cardsToEmbed@ takes a list of cards and embeds their names with links. -cardsToEmbed :: NrApi -> Text -> [Card] -> Text -> EnvDatabaseDiscord NrApi Embed +cardsToEmbed :: NrApi -> Text -> [Card] -> Text -> EnvDatabaseDiscord NrApi CreateEmbed cardsToEmbed api pre cards err = do formatted <- mapM formatCard $ take 10 cards let cards' = "**" <> intercalate "\n" formatted <> "**" eTitle = "**" <> pack (show $ length cards) <> " results**" eText = pre <> "\n" <> cards' <> if length cards > 10 then "\n" <> err else "" - return $ createEmbed $ CreateEmbed "" "" Nothing eTitle "" Nothing eText [] Nothing "" Nothing Nothing + return $ CreateEmbed "" "" Nothing eTitle "" Nothing eText [] Nothing "" Nothing Nothing Nothing where formatCard :: Card -> EnvDatabaseDiscord NrApi Text formatCard card = do @@ -75,49 +75,49 @@ cardsToEmbed api pre cards err = do return $ icon <> " [" <> title' <> "](" <> link <> ")" -- | @cardToImgEmbed@ takes a card and attempts to embed a picture of it. -cardToImgEmbed :: NrApi -> Card -> Embed +cardToImgEmbed :: NrApi -> Card -> CreateEmbed cardToImgEmbed api card = let eTitle = toTitle card eURL = toLink card eColour = toColour api card in addColour eColour $ - createEmbed $ case toImage api card of - Nothing -> CreateEmbed "" "" Nothing eTitle eURL Nothing "`Could not find card art`" [] Nothing "" Nothing Nothing - eImg -> CreateEmbed "" "" Nothing eTitle eURL Nothing "" [] eImg "" Nothing Nothing + case toImage api card of + Nothing -> CreateEmbed "" "" Nothing eTitle eURL Nothing "`Could not find card art`" [] Nothing "" Nothing Nothing Nothing + eImg -> CreateEmbed "" "" Nothing eTitle eURL Nothing "" [] eImg "" Nothing Nothing Nothing -- | @cardToFlavourEmbed@ takes a card and attempts to embed its flavour text. -cardToFlavourEmbed :: NrApi -> Card -> EnvDatabaseDiscord NrApi Embed +cardToFlavourEmbed :: NrApi -> Card -> EnvDatabaseDiscord NrApi CreateEmbed cardToFlavourEmbed api card = do let eTitle = toTitle card eURL = toLink card eColour = toColour api card eImg = toImage api card - fallback = CreateEmbed "" "" Nothing eTitle eURL eImg "`Card has no flavour text`" [] Nothing "" Nothing Nothing + fallback = CreateEmbed "" "" Nothing eTitle eURL eImg "`Card has no flavour text`" [] Nothing "" Nothing Nothing Nothing flavor <- toFlavour card return $ addColour eColour $ - createEmbed $ case flavor of + case flavor of Nothing -> fallback Just "" -> fallback - Just eFlavour -> CreateEmbed "" "" Nothing eTitle eURL eImg eFlavour [] Nothing "" Nothing Nothing + Just eFlavour -> CreateEmbed "" "" Nothing eTitle eURL eImg eFlavour [] Nothing "" Nothing Nothing Nothing -- | @embedText@ just embeds the given text. -embedText :: Text -> Text -> Embed -embedText title text = createEmbed $ CreateEmbed "" "" Nothing title "" Nothing text [] Nothing "" Nothing Nothing +embedText :: Text -> Text -> CreateEmbed +embedText title text = CreateEmbed "" "" Nothing title "" Nothing text [] Nothing "" Nothing Nothing Nothing -- | @embedTextWithUrl@ is @embedText@ but you can set the title URL. -embedTextWithUrl :: Text -> Text -> Text -> Embed -embedTextWithUrl title url text = createEmbed $ CreateEmbed "" "" Nothing title url Nothing text [] Nothing "" Nothing Nothing +embedTextWithUrl :: Text -> Text -> Text -> CreateEmbed +embedTextWithUrl title url text = CreateEmbed "" "" Nothing title url Nothing text [] Nothing "" Nothing Nothing Nothing -- | @embedColumns@ embeds Text into columns. -embedColumns :: Text -> Text -> [(Text, [Text])] -> Embed +embedColumns :: Text -> Text -> [(Text, [Text])] -> CreateEmbed embedColumns title pre cols = let fields = map (\x -> EmbedField (fst x) (intercalate "\n" $ snd x) $ Just True) cols - in createEmbed $ CreateEmbed "" "" Nothing title "" Nothing pre fields Nothing "" Nothing Nothing + in CreateEmbed "" "" Nothing title "" Nothing pre fields Nothing "" Nothing Nothing Nothing -- | @embedLines@ embeds a list of lines, splitting them into columns as needed. -- NOTE: does not preserve order -embedLines :: Text -> Text -> [Text] -> Embed +embedLines :: Text -> Text -> [Text] -> CreateEmbed embedLines title pre xs = let cumLength = scanl (\l x -> 1 + T.length x + l) (T.length title + 2) xs -- +1 for each newline title characters safeIndex = length $ takeWhile (< 1900) cumLength -- 1900 instead of 2000 because I gave up trying to be exact @@ -128,4 +128,4 @@ embedLines title pre xs = heights = replicate m (d + 1) ++ replicate (c - m) d cols = splitPlaces heights xs' fields = map (\x -> EmbedField "⠀" (intercalate "\n" x) $ Just True) cols - in createEmbed $ CreateEmbed "" "" Nothing title "" Nothing pre fields Nothing "" Nothing Nothing + in CreateEmbed "" "" Nothing title "" Nothing pre fields Nothing "" Nothing Nothing Nothing diff --git a/src/Tablebot/Plugins/Quote.hs b/src/Tablebot/Plugins/Quote.hs index a354466e..f7cd6d0d 100644 --- a/src/Tablebot/Plugins/Quote.hs +++ b/src/Tablebot/Plugins/Quote.hs @@ -281,7 +281,7 @@ renderCustomQuoteMessage t (Quote txt author submitter msgId cnlId dtm) qId m = sendEmbedMessage m t - ( addColour Blue $ + ( addColour DiscordColorBlue $ addTimestamp dtm $ addFooter (pack $ "Quote #" ++ show qId) $ simpleEmbed (txt <> "\n - " <> author <> maybeAddFooter link) @@ -293,6 +293,21 @@ renderCustomQuoteMessage t (Quote txt author submitter msgId cnlId dtm) qId m = maybeAddFooter (Just l) = "\n[source](" <> l <> ") - added by " <> submitter maybeAddFooter Nothing = "" +-- renderCustomQuoteMessage' :: Text -> Quote -> Int64 -> GuildId -> MessageDetails +-- renderCustomQuoteMessage' t (Quote txt author submitter msgId cnlId dtm) qId gid = do +-- (messageDetailsBasic t) +-- { messageDetailsEmbeds = +-- Just +-- [ addColour DiscordColorBlue $ +-- addTimestamp dtm $ +-- addFooter (pack $ "Quote #" ++ show qId) $ +-- simpleEmbed (txt <> "\n - " <> author <> addFooter') +-- ] +-- } +-- where +-- link = getMessageLink gid (fromIntegral cnlId) (fromIntegral msgId) +-- addFooter' = "\n[source](" <> link <> ") - added by " <> submitter + showQuoteHelp :: HelpPage showQuoteHelp = HelpPage diff --git a/src/Tablebot/Utility/Discord.hs b/src/Tablebot/Utility/Discord.hs index 21d60f38..a6e4da1f 100644 --- a/src/Tablebot/Utility/Discord.hs +++ b/src/Tablebot/Utility/Discord.hs @@ -52,6 +52,7 @@ where import Control.Monad.Exception (MonadException (throw)) import Data.Char (isDigit) +import Data.Default (Default (def)) import Data.Foldable (msum) import Data.List ((\\)) import Data.Map.Strict (keys) @@ -66,7 +67,7 @@ import qualified Discord.Requests as R import Discord.Types import GHC.Word (Word64) import Tablebot.Internal.Cache (fillEmojiCache, lookupEmojiCache) -import Tablebot.Internal.Embed (Embeddable (..), TablebotEmbedRequest (TablebotEmbedRequest)) +import Tablebot.Internal.Embed (Embeddable (..)) import Tablebot.Utility (EnvDatabaseDiscord, MessageDetails, convertMessageFormatBasic, convertMessageFormatInteraction, liftDiscord, messageDetailsBasic) import Tablebot.Utility.Exception (BotException (..)) @@ -162,7 +163,7 @@ sendChannelEmbedMessage :: e -> EnvDatabaseDiscord s () sendChannelEmbedMessage cid t e = do - res <- liftDiscord . restCall $ TablebotEmbedRequest cid t (asEmbed e) + res <- liftDiscord . restCall $ R.CreateMessageDetailed cid (def {R.messageDetailedContent = t, R.messageDetailedEmbeds = Just [asEmbed e]}) case res of Left _ -> throw $ MessageSendException "Failed to send message." Right _ -> return () diff --git a/src/Tablebot/Utility/Embed.hs b/src/Tablebot/Utility/Embed.hs index 7330ddf3..b940baf6 100644 --- a/src/Tablebot/Utility/Embed.hs +++ b/src/Tablebot/Utility/Embed.hs @@ -11,60 +11,59 @@ module Tablebot.Utility.Embed where import Data.Text (Text) import Discord.Internal.Types -import Tablebot.Internal.Embed (Embeddable, asEmbed, colourToInternal) -import Tablebot.Utility.Types (DiscordColour) +import Tablebot.Internal.Embed (Embeddable, asEmbed) -- | Some helper functions to allow progressively building up an embed -- If you need something more complex, you can still use the createEmbed flow provided by discord-haskell, -- its not bad (once you realise that it turns empty strings into Nothing for you...) but it can't do colours. -- To add a colour run createEmbed on it and then use one of these functions to manipulate it -simpleEmbed :: Text -> Embed -simpleEmbed t = createEmbed $ CreateEmbed "" "" Nothing "" "" Nothing t [] Nothing "" Nothing Nothing +simpleEmbed :: Text -> CreateEmbed +simpleEmbed t = CreateEmbed "" "" Nothing "" "" Nothing t [] Nothing "" Nothing Nothing Nothing -addTitle :: Embeddable e => Text -> e -> Embed +addTitle :: Embeddable e => Text -> e -> CreateEmbed addTitle t e = (asEmbed e) - { embedTitle = Just t + { createEmbedTitle = t } -addFooter :: Embeddable e => Text -> e -> Embed +addFooter :: Embeddable e => Text -> e -> CreateEmbed addFooter t e = (asEmbed e) - { embedFooter = Just $ EmbedFooter t Nothing Nothing + { createEmbedFooterText = t } -addTimestamp :: UTCTime -> Embed -> Embed +addTimestamp :: Embeddable e => UTCTime -> e -> CreateEmbed addTimestamp t e = - e - { embedTimestamp = Just t + (asEmbed e) + { createEmbedTimestamp = Just t } -addAuthor :: Text -> Embed -> Embed +addAuthor :: Embeddable e => Text -> e -> CreateEmbed addAuthor t e = (asEmbed e) - { embedAuthor = Just $ EmbedAuthor (Just t) Nothing Nothing Nothing + { createEmbedAuthorName = t } -addLink :: Text -> Embed -> Embed +addLink :: Embeddable e => Text -> e -> CreateEmbed addLink t e = - e - { embedUrl = Just t + (asEmbed e) + { createEmbedUrl = t } -addColour :: DiscordColour -> Embed -> Embed +addColour :: Embeddable e => DiscordColor -> e -> CreateEmbed addColour c e = (asEmbed e) - { embedColor = Just $ colourToInternal c + { createEmbedColor = Just c } -addImage :: Embeddable e => Text -> e -> Embed +addImage :: Embeddable e => Text -> e -> CreateEmbed addImage url e = (asEmbed e) - { embedImage = Just $ EmbedImage (Just url) Nothing Nothing Nothing + { createEmbedImage = Just $ CreateEmbedImageUrl url } -addThumbnail :: Embeddable e => Text -> e -> Embed +addThumbnail :: Embeddable e => Text -> e -> CreateEmbed addThumbnail url e = (asEmbed e) - { embedThumbnail = Just $ EmbedThumbnail (Just url) Nothing Nothing Nothing + { createEmbedThumbnail = Just $ CreateEmbedImageUrl url } diff --git a/src/Tablebot/Utility/Exception.hs b/src/Tablebot/Utility/Exception.hs index 06470f8d..a1c560ce 100644 --- a/src/Tablebot/Utility/Exception.hs +++ b/src/Tablebot/Utility/Exception.hs @@ -24,7 +24,6 @@ import Data.List (intercalate) import Data.Text (pack) import Discord.Internal.Types import Tablebot.Utility.Embed -import Tablebot.Utility.Types (DiscordColour (..)) -- | @BotException@ is the type for errors caught in TableBot. -- Declare new errors here, and define them at the bottom of the file. @@ -95,10 +94,10 @@ showUserError :: BotException -> String showUserError e = formatUserError (errorName e) (errorMsg e) -- | @embedError@ takes an error and makes it into an embed. -embedError :: BotException -> Embed +embedError :: BotException -> CreateEmbed embedError e = addTitle (pack $ errorEmoji ++ " **" ++ errorName e ++ "** " ++ errorEmoji) $ - addColour Red $ + addColour DiscordColorRed $ simpleEmbed (pack $ errorMsg e) -- | @errorInfo@ takes a BotException and converts it into an ErrorInfo struct. diff --git a/src/Tablebot/Utility/Types.hs b/src/Tablebot/Utility/Types.hs index b073f05c..72b8cb56 100644 --- a/src/Tablebot/Utility/Types.hs +++ b/src/Tablebot/Utility/Types.hs @@ -16,7 +16,6 @@ import Control.Concurrent.MVar (MVar) import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Reader (ReaderT) import Data.ByteString (ByteString) -import Data.Char (toLower) import Data.Default (Default (def)) import Data.Map (Map, empty) import Data.Maybe (fromMaybe) @@ -32,7 +31,7 @@ import Discord.Types Attachment, ChannelId, ComponentActionRow, - Embed, + CreateEmbed, Emoji, Event (..), Message, @@ -41,9 +40,7 @@ import Discord.Types ReactionInfo, StickerId, ) -import Safe.Exact (dropExactMay, takeExactMay) import Text.Megaparsec (Parsec) -import Text.Read (readMaybe) -- * DatabaseDiscord @@ -229,78 +226,6 @@ data HelpPage = HelpPage } deriving (Show) --- | Colour names --- Colour is a bit of a mess on discord embeds. --- I've here stolen the pallet list from https://gist.github.com/thomasbnt/b6f455e2c7d743b796917fa3c205f812 -data DiscordColour - = RGB Integer Integer Integer - | Default - | Aqua - | DarkAqua - | Green - | DarkGreen - | Blue - | DarkBlue - | Purple - | DarkPurple - | LuminousVividPink - | DarkVividPink - | Gold - | DarkGold - | Orange - | DarkOrange - | Red - | DarkRed - | Gray - | DarkGray - | DarkerGray - | LightGray - | Navy - | DarkNavy - | Yellow - | DiscordWhite - | DiscordBlurple - | DiscordGrayple - | DiscordDarkButNotBlack - | DiscordNotQuiteBlack - | DiscordGreen - | DiscordYellow - | DiscordFuschia - | DiscordRed - | DiscordBlack - --- | @hexToRGB@ attempts to convert a potential hex string into its decimal RGB --- components. -hexToRGB :: String -> Maybe (Integer, Integer, Integer) -hexToRGB hex = do - let h = map toLower hex - r <- takeExactMay 2 h >>= toDec - g <- dropExactMay 2 h >>= takeExactMay 2 >>= toDec - b <- dropExactMay 4 h >>= toDec - return (r, g, b) - where - toDec :: String -> Maybe Integer - toDec [s, u] = do - a <- charToDec s - b <- charToDec u - return $ a * 16 + b - toDec _ = Nothing - charToDec :: Char -> Maybe Integer - charToDec 'a' = Just 10 - charToDec 'b' = Just 11 - charToDec 'c' = Just 12 - charToDec 'd' = Just 13 - charToDec 'e' = Just 14 - charToDec 'f' = Just 15 - charToDec c = readMaybe [c] - --- | @hexToDiscordColour@ converts a potential hex string into a DiscordColour, --- evaluating to Default if it fails. -hexToDiscordColour :: String -> DiscordColour -hexToDiscordColour hex = - let (r, g, b) = fromMaybe (0, 0, 0) $ hexToRGB hex - in RGB r g b - -- | Automatic handling of command permissions -- @UserPermission@ models the current permissions of the user -- @RequiredPermission@ models the permissions required to run a command. @@ -369,7 +294,7 @@ instance Default MessageDetails where data MessageDetails = MessageDetails { messageDetailsTTS :: Maybe Bool, messageDetailsContent :: Maybe Text, - messageDetailsEmbeds :: Maybe [Embed], + messageDetailsEmbeds :: Maybe [CreateEmbed], messageDetailsFile :: Maybe (Text, ByteString), messageDetailsAllowedMentions :: Maybe AllowedMentions, messageDetailsFlags :: Maybe InteractionResponseMessageFlags, @@ -395,7 +320,7 @@ convertMessageFormatBasic MessageDetails {..} = MessageDetailedOpts (fromMaybe "" messageDetailsContent) (fromMaybe False messageDetailsTTS) - Nothing + messageDetailsEmbeds messageDetailsFile messageDetailsAllowedMentions messageDetailsReference diff --git a/stack.yaml b/stack.yaml index 64408564..78c1879f 100644 --- a/stack.yaml +++ b/stack.yaml @@ -41,7 +41,7 @@ packages: # extra-deps: - git: https://github.com/L0neGamer/discord-haskell.git - commit: 8ac23e4598f501136109090a8ddb073eaac59889 + commit: bdc81ab1fa3816f343b337d5dfb0910a9538ae5c - emoji-0.1.0.2 - load-env-0.2.1.0 - megaparsec-9.0.1 From 1ff267ac135c12ee8feadaf053aed3dd92501498 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 27 Jan 2022 17:37:49 +0000 Subject: [PATCH 25/96] started adding application commands in quote - first and foremost, right clicking a message to quote it --- src/Tablebot/Internal/Permission.hs | 15 +++- src/Tablebot/Plugins/Quote.hs | 125 +++++++++++++++------------- src/Tablebot/Utility/Discord.hs | 1 + src/Tablebot/Utility/Exception.hs | 2 + src/Tablebot/Utility/Permission.hs | 8 +- src/Tablebot/Utility/SmartParser.hs | 26 +++++- stack.yaml | 2 +- 7 files changed, 110 insertions(+), 69 deletions(-) diff --git a/src/Tablebot/Internal/Permission.hs b/src/Tablebot/Internal/Permission.hs index ef266be9..b074a87e 100644 --- a/src/Tablebot/Internal/Permission.hs +++ b/src/Tablebot/Internal/Permission.hs @@ -10,9 +10,9 @@ module Tablebot.Internal.Permission where import Control.Monad.IO.Class (liftIO) -import Discord.Types (GuildMember, Message, RoleId, memberRoles) +import Discord.Types (GuildMember, RoleId, memberRoles) import System.Environment (lookupEnv) -import Tablebot.Utility.Discord (getMessageMember) +import Tablebot.Utility.SmartParser (Context (contextMember)) import Tablebot.Utility.Types import Tablebot.Utility.Utils (isDebug) import Text.Read (readMaybe) @@ -57,9 +57,16 @@ permsFromGroups debug krls gps = elemish (Just a) b = a `elem` b elemish Nothing _ = False -getSenderPermission :: Message -> EnvDatabaseDiscord s UserPermission +-- getSenderPermission :: Message -> EnvDatabaseDiscord s UserPermission +-- getSenderPermission m = do +-- member <- getMessageMember m +-- knownroles <- liftIO getKnownRoles +-- debug <- liftIO isDebug +-- return $ permsFromGroups debug knownroles $ getMemberGroups member + +getSenderPermission :: Context m => m -> EnvDatabaseDiscord s UserPermission getSenderPermission m = do - member <- getMessageMember m + let member = contextMember m knownroles <- liftIO getKnownRoles debug <- liftIO isDebug return $ permsFromGroups debug knownroles $ getMemberGroups member diff --git a/src/Tablebot/Plugins/Quote.hs b/src/Tablebot/Plugins/Quote.hs index f7cd6d0d..30b220e0 100644 --- a/src/Tablebot/Plugins/Quote.hs +++ b/src/Tablebot/Plugins/Quote.hs @@ -12,13 +12,15 @@ -- quotes and then @!quote show n@ a particular quote. module Tablebot.Plugins.Quote (quotePlugin) where -import Control.Monad (void) import Control.Monad.IO.Class (liftIO) import Data.Aeson +import Data.Default (Default (def)) +import Data.Maybe (catMaybes) import Data.Text (Text, append, pack, unpack) import Data.Time.Clock.System (SystemTime (systemSeconds), getSystemTime, systemToUTCTime) import Database.Persist.Sqlite (Filter, SelectOpt (LimitTo, OffsetBy), entityVal, (==.)) import Database.Persist.TH +import Discord.Interactions import Discord.Types import GHC.Generics (Generic) import GHC.Int (Int64) @@ -26,18 +28,18 @@ import System.Random (randomRIO) import Tablebot.Utility import Tablebot.Utility.Database import Tablebot.Utility.Discord - ( findGuild, - getMessage, + ( getMessage, getMessageLink, getPrecedingMessage, getReplyMessage, - sendEmbedMessage, + interactionResponseCustomMessage, + sendCustomMessage, sendMessage, toMention, toMention', ) import Tablebot.Utility.Embed -import Tablebot.Utility.Exception (BotException (GenericException), catchBot, throwBot) +import Tablebot.Utility.Exception (BotException (GenericException, InteractionException), catchBot, throwBot) import Tablebot.Utility.Permission (requirePermission) import Tablebot.Utility.SmartParser import Text.RawString.QQ (r) @@ -67,7 +69,7 @@ quoteReactionAdd = ReactionAdd quoteReaction m <- getMessage (reactionChannelId ri) (reactionMessageId ri) case m of Left _ -> pure () - Right mes -> addMessageQuote (reactionUserId ri) mes mes + Right mes -> addMessageQuote (reactionUserId ri) mes mes >>= sendCustomMessage mes | otherwise = return () -- | Our quote command, which combines various functions to create, display and update quotes. @@ -84,9 +86,9 @@ quoteCommand = (Either () (Either Int64 (RestOfInput Text))) -> Message -> DatabaseDiscord () - quoteComm (WErr (Left ())) = randomQ - quoteComm (WErr (Right (Left t))) = showQ t - quoteComm (WErr (Right (Right (ROI t)))) = authorQ t + quoteComm (WErr (Left ())) m = randomQ m >>= sendCustomMessage m + quoteComm (WErr (Right (Left t))) m = showQ t m >>= sendCustomMessage m + quoteComm (WErr (Right (Right (ROI t)))) m = authorQ t m >>= sendCustomMessage m addQuote :: Command addQuote = Command "add" (parseComm addComm) [] @@ -95,7 +97,7 @@ addQuote = Command "add" (parseComm addComm) [] WithError "Quote format incorrect!\nFormat is: .quote \"quote\" - author" (Quoted Text, Exactly "-", RestOfInput Text) -> Message -> DatabaseDiscord () - addComm (WErr (Qu qu, _, ROI author)) = addQ qu author + addComm (WErr (Qu qu, _, ROI author)) m = addQ qu author m >>= sendCustomMessage m editQuote :: Command editQuote = Command "edit" (parseComm editComm) [] @@ -114,6 +116,24 @@ thisQuote = Command "this" (parseComm thisComm) [] thisComm :: Message -> DatabaseDiscord () thisComm = thisQ +quoteMessageAppComm :: Maybe ApplicationCommandRecv +quoteMessageAppComm = appcomm >>= return . (`ApplicationCommandRecv` recv) + where + appcomm = createApplicationCommandMessage "quote" + recv i@InteractionApplicationCommand {interactionDataApplicationCommand = InteractionDataApplicationCommandMessage {..}, ..} = do + let mid = interactionDataApplicationCommandTargetId + case interactionChannelId of + Nothing -> throwBot $ InteractionException "no channel id in quote interaction" + Just cid -> do + m <- getMessage cid mid + case m of + Left _ -> throwBot $ InteractionException "could not get message to quote" + Right msg -> interactionResponseCustomMessage i =<< addQ' (messageContent msg) (toMention $ messageAuthor msg) (toMention' $ parseUserId $ contextUserId i) mid cid i + recv _ = return def + +-- return $ messageDetailsBasic (pack $ show idac) +-- content <- + authorQuote :: Command authorQuote = Command "author" (parseComm authorComm) [] where @@ -121,7 +141,7 @@ authorQuote = Command "author" (parseComm authorComm) [] WithError "Quote format incorrect!\nExpected author name to find quotes for after .quote author" (RestOfInput Text) -> Message -> DatabaseDiscord () - authorComm (WErr (ROI author)) = authorQ author + authorComm (WErr (ROI author)) m = authorQ author m >>= sendCustomMessage m showQuote :: Command showQuote = Command "show" (parseComm showComm) [] @@ -130,7 +150,7 @@ showQuote = Command "show" (parseComm showComm) [] WithError "Quote format incorrect!\nExpected quote number to show, e.g. .quote show 420" Int64 -> Message -> DatabaseDiscord () - showComm (WErr qId) = showQ qId + showComm (WErr qId) m = showQ qId m >>= sendCustomMessage m deleteQuote :: Command deleteQuote = Command "delete" (parseComm deleteComm) [] @@ -145,40 +165,40 @@ randomQuote :: Command randomQuote = Command "random" (parseComm randomComm) [] where randomComm :: Message -> DatabaseDiscord () - randomComm = randomQ + randomComm m = randomQ m >>= sendCustomMessage m -- | @showQuote@, which looks for a message of the form @!quote show n@, looks -- that quote up in the database and responds with that quote. -showQ :: Int64 -> Message -> DatabaseDiscord () +showQ :: Context m => Int64 -> m -> DatabaseDiscord MessageDetails showQ qId m = do qu <- get $ toSqlKey qId case qu of Just q -> renderQuoteMessage q qId m - Nothing -> sendMessage m "Couldn't get that quote!" + Nothing -> return $ messageDetailsBasic "Couldn't get that quote!" -- | @randomQuote@, which looks for a message of the form @!quote random@, -- selects a random quote from the database and responds with that quote. -randomQ :: Message -> DatabaseDiscord () +randomQ :: Context m => m -> DatabaseDiscord MessageDetails randomQ = filteredRandomQuote [] "Couldn't find any quotes!" -- | @authorQuote@, which looks for a message of the form @!quote author u@, -- selects a random quote from the database attributed to u and responds with that quote. -authorQ :: Text -> Message -> DatabaseDiscord () +authorQ :: Context m => Text -> m -> DatabaseDiscord MessageDetails authorQ t = filteredRandomQuote [QuoteAuthor ==. t] "Couldn't find any quotes with that author!" -- | @filteredRandomQuote@ selects a random quote that meets a -- given criteria, and returns that as the response, sending the user a message if the -- quote cannot be found. -filteredRandomQuote :: [Filter Quote] -> Text -> Message -> DatabaseDiscord () +filteredRandomQuote :: Context m => [Filter Quote] -> Text -> m -> DatabaseDiscord MessageDetails filteredRandomQuote quoteFilter errorMessage m = catchBot (filteredRandomQuote' quoteFilter errorMessage m) catchBot' where - catchBot' (GenericException "quote exception" _) = sendMessage m errorMessage + catchBot' (GenericException "quote exception" _) = return $ messageDetailsBasic errorMessage catchBot' e = throwBot e -- | @filteredRandomQuote'@ selects a random quote that meets a -- given criteria, and returns that as the response, throwing an exception if something -- goes wrong. -filteredRandomQuote' :: [Filter Quote] -> Text -> Message -> DatabaseDiscord () +filteredRandomQuote' :: Context m => [Filter Quote] -> Text -> m -> DatabaseDiscord MessageDetails filteredRandomQuote' quoteFilter errorMessage m = do num <- count quoteFilter if num == 0 @@ -194,10 +214,13 @@ filteredRandomQuote' quoteFilter errorMessage m = do -- | @addQuote@, which looks for a message of the form -- @!quote add "quoted text" - author@, and then stores said quote in the -- database, returning the ID used. -addQ :: Text -> Text -> Message -> DatabaseDiscord () -addQ qu author m = do +addQ :: Text -> Text -> Message -> DatabaseDiscord MessageDetails +addQ qu author m = addQ' qu author (toMention $ messageAuthor m) (messageId m) (messageChannelId m) m + +addQ' :: Context m => Text -> Text -> Text -> MessageId -> ChannelId -> m -> DatabaseDiscord MessageDetails +addQ' qu author requestor sourceMsg sourceChannel m = do now <- liftIO $ systemToUTCTime <$> getSystemTime - let new = Quote qu author (toMention $ messageAuthor m) (fromIntegral $ messageId m) (fromIntegral $ messageChannelId m) now + let new = Quote qu author requestor (fromIntegral sourceMsg) (fromIntegral sourceChannel) now added <- insert new let res = pack $ show $ fromSqlKey added renderCustomQuoteMessage ("Quote added as #" `append` res) new (fromSqlKey added) m @@ -209,15 +232,15 @@ thisQ :: Message -> DatabaseDiscord () thisQ m = do q <- getReplyMessage m case q of - (Just q') -> addMessageQuote (userId $ messageAuthor m) q' m + (Just q') -> addMessageQuote (userId $ messageAuthor m) q' m >>= sendCustomMessage m Nothing -> do q2 <- getPrecedingMessage m case q2 of - (Just q') -> addMessageQuote (userId $ messageAuthor m) q' m + (Just q') -> addMessageQuote (userId $ messageAuthor m) q' m >>= sendCustomMessage m Nothing -> sendMessage m "Unable to add quote" -- | @addMessageQuote@, adds a message as a quote to the database, checking that it passes the relevant tests -addMessageQuote :: UserId -> Message -> Message -> DatabaseDiscord () +addMessageQuote :: Context m => UserId -> Message -> m -> DatabaseDiscord MessageDetails addMessageQuote submitter q' m = do num <- count [QuoteMsgId ==. fromIntegral (messageId q')] if num == 0 @@ -236,8 +259,8 @@ addMessageQuote submitter q' m = do added <- insert new let res = pack $ show $ fromSqlKey added renderCustomQuoteMessage ("Quote added as #" `append` res) new (fromSqlKey added) m - else sendMessage m "Can't quote a bot" - else sendMessage m "Message already quoted" + else return $ messageDetailsBasic "Can't quote a bot" + else return $ messageDetailsBasic "Message already quoted" -- | @editQuote@, which looks for a message of the form -- @!quote edit n "quoted text" - author@, and then updates quote with id n in the @@ -253,7 +276,7 @@ editQ qId qu author m = now <- liftIO $ systemToUTCTime <$> getSystemTime let new = Quote qu author (toMention $ messageAuthor m) (fromIntegral $ messageId m) (fromIntegral $ messageChannelId m) now replace k new - renderCustomQuoteMessage "Quote updated" new qId m + renderCustomQuoteMessage "Quote updated" new qId m >>= sendCustomMessage m Nothing -> sendMessage m "Couldn't update that quote!" -- | @deleteQuote@, which looks for a message of the form @!quote delete n@, @@ -270,22 +293,24 @@ deleteQ qId m = sendMessage m "Quote deleted" Nothing -> sendMessage m "Couldn't delete that quote!" -renderQuoteMessage :: Quote -> Int64 -> Message -> DatabaseDiscord () +renderQuoteMessage :: Context m => Quote -> Int64 -> m -> DatabaseDiscord MessageDetails renderQuoteMessage = renderCustomQuoteMessage "" -renderCustomQuoteMessage :: Text -> Quote -> Int64 -> Message -> DatabaseDiscord () +renderCustomQuoteMessage :: Context m => Text -> Quote -> Int64 -> m -> DatabaseDiscord MessageDetails renderCustomQuoteMessage t (Quote txt author submitter msgId cnlId dtm) qId m = do - guild <- findGuild m + guild <- contextGuildId m let link = getLink guild - void $ - sendEmbedMessage - m - t - ( addColour DiscordColorBlue $ - addTimestamp dtm $ - addFooter (pack $ "Quote #" ++ show qId) $ - simpleEmbed (txt <> "\n - " <> author <> maybeAddFooter link) - ) + return + ( (messageDetailsBasic t) + { messageDetailsEmbeds = + Just + [ addColour DiscordColorBlue $ + addTimestamp dtm $ + addFooter (pack $ "Quote #" ++ show qId) $ + simpleEmbed (txt <> "\n - " <> author <> maybeAddFooter link) + ] + } + ) where getLink :: Maybe GuildId -> Maybe Text getLink = fmap (\x -> getMessageLink x (fromIntegral cnlId) (fromIntegral msgId)) @@ -293,21 +318,6 @@ renderCustomQuoteMessage t (Quote txt author submitter msgId cnlId dtm) qId m = maybeAddFooter (Just l) = "\n[source](" <> l <> ") - added by " <> submitter maybeAddFooter Nothing = "" --- renderCustomQuoteMessage' :: Text -> Quote -> Int64 -> GuildId -> MessageDetails --- renderCustomQuoteMessage' t (Quote txt author submitter msgId cnlId dtm) qId gid = do --- (messageDetailsBasic t) --- { messageDetailsEmbeds = --- Just --- [ addColour DiscordColorBlue $ --- addTimestamp dtm $ --- addFooter (pack $ "Quote #" ++ show qId) $ --- simpleEmbed (txt <> "\n - " <> author <> addFooter') --- ] --- } --- where --- link = getMessageLink gid (fromIntegral cnlId) (fromIntegral msgId) --- addFooter' = "\n[source](" <> link <> ") - added by " <> submitter - showQuoteHelp :: HelpPage showQuoteHelp = HelpPage @@ -404,7 +414,8 @@ quotePlugin = { commands = [quoteCommand, commandAlias "q" quoteCommand], onReactionAdds = [quoteReactionAdd], migrations = [quoteMigration], - helpPages = [quoteHelp] + helpPages = [quoteHelp], + applicationCommands = catMaybes [quoteMessageAppComm] } deriving instance Generic Quote diff --git a/src/Tablebot/Utility/Discord.hs b/src/Tablebot/Utility/Discord.hs index a6e4da1f..d9d4dc34 100644 --- a/src/Tablebot/Utility/Discord.hs +++ b/src/Tablebot/Utility/Discord.hs @@ -19,6 +19,7 @@ module Tablebot.Utility.Discord reactToMessage, findGuild, findEmoji, + getChannel, getMessage, getMessageMember, getReplyMessage, diff --git a/src/Tablebot/Utility/Exception.hs b/src/Tablebot/Utility/Exception.hs index a1c560ce..662d4816 100644 --- a/src/Tablebot/Utility/Exception.hs +++ b/src/Tablebot/Utility/Exception.hs @@ -37,6 +37,7 @@ data BotException | IOException String | NetrunnerException String | InteractionException String + | PermissionException String deriving (Show, Eq) instance Exception BotException @@ -127,3 +128,4 @@ errorInfo (EvaluationException msg' locs) = ErrorInfo "EvaluationException" $ ms errorInfo (IOException msg') = ErrorInfo "IOException" msg' errorInfo (NetrunnerException msg') = ErrorInfo "NetrunnerException" msg' errorInfo (InteractionException msg') = ErrorInfo "InteractionException" msg' +errorInfo (PermissionException msg') = ErrorInfo "InteractionException" msg' diff --git a/src/Tablebot/Utility/Permission.hs b/src/Tablebot/Utility/Permission.hs index e6b56def..75233799 100644 --- a/src/Tablebot/Utility/Permission.hs +++ b/src/Tablebot/Utility/Permission.hs @@ -9,15 +9,15 @@ -- This contains a simple interface for plugin authors to require a specific level of privilege. module Tablebot.Utility.Permission where -import Discord.Internal.Rest (Message) import Tablebot.Internal.Permission -import Tablebot.Utility.Discord (sendMessage) +import Tablebot.Utility.Exception (BotException (PermissionException), throwBot) +import Tablebot.Utility.SmartParser (Context) import Tablebot.Utility.Types -- | @requirePermission@ only runs the inputted effect if permissions are matched. Otherwise it returns an error. -requirePermission :: RequiredPermission -> Message -> EnvDatabaseDiscord s () -> EnvDatabaseDiscord s () +requirePermission :: Context m => RequiredPermission -> m -> EnvDatabaseDiscord s a -> EnvDatabaseDiscord s a requirePermission perm m a = do p <- getSenderPermission m if userHasPermission perm p then a - else sendMessage m "Sorry, you don't have permission to do that." + else throwBot $ PermissionException "Sorry, you don't have permission to do that." diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index a349920f..e7ebdc5f 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -24,7 +24,7 @@ import Discord.Types import GHC.OldList (find) import GHC.TypeLits (KnownSymbol, Symbol, symbolVal) import Tablebot.Internal.Handler.Command (parseValue) -import Tablebot.Utility.Discord (interactionResponseComponentsUpdateMessage, interactionResponseCustomMessage, sendCustomMessage) +import Tablebot.Utility.Discord (getChannel, interactionResponseComponentsUpdateMessage, interactionResponseCustomMessage, sendCustomMessage) import Tablebot.Utility.Exception (BotException (InteractionException, ParserException), catchBot, embedError, throwBot) import Tablebot.Utility.Parser import Tablebot.Utility.Types @@ -32,15 +32,35 @@ import Text.Megaparsec (MonadParsec (eof, try), chunk, many, optional, (), (< class Context a where contextUserId :: a -> ParseUserId + contextGuildId :: a -> EnvDatabaseDiscord s (Maybe GuildId) + contextMember :: a -> Maybe GuildMember + contextMessageId :: a -> Maybe MessageId instance Context Message where contextUserId = ParseUserId . userId . messageAuthor + contextGuildId m = case messageGuildId m of + Just a -> pure $ Just a + Nothing -> do + let chanId = messageChannelId m + channel <- getChannel chanId + case fmap channelGuild channel of + Right a -> pure $ Just a + Left _ -> pure Nothing + contextMember = messageMember + contextMessageId = return . messageId --- this is safe to do because we are guaranteed to get either a user or a member instance Context Interaction where + -- this is safe to do because we are guaranteed to get either a user or a member contextUserId i = ParseUserId $ maybe 0 userId (either memberUser Just mor) where (MemberOrUser mor) = interactionUser i + contextGuildId i = return $ interactionGuildId i + contextMember i = case interactionUser i of + (MemberOrUser (Left m)) -> return m + (MemberOrUser (Right _)) -> Nothing + contextMessageId InteractionComponent {interactionMessage = m} = return $ messageId m + contextMessageId InteractionApplicationCommand {interactionDataApplicationCommand = InteractionDataApplicationCommandMessage {..}} = return interactionDataApplicationCommandTargetId + contextMessageId _ = Nothing -- | @PComm@ defines function types that we can automatically turn into parsers -- by composing a parser per input of the function provided. @@ -232,7 +252,7 @@ newtype RestOfInput1 a = ROI1 a instance IsString a => CanParse (RestOfInput1 a) where pars = ROI1 . fromString <$> untilEnd1 -newtype ParseUserId = ParseUserId UserId +newtype ParseUserId = ParseUserId {parseUserId :: UserId} -- | Labelled value for use with smart commands. newtype Labelled (name :: Symbol) (desc :: Symbol) a = Labelled a diff --git a/stack.yaml b/stack.yaml index 78c1879f..9b440403 100644 --- a/stack.yaml +++ b/stack.yaml @@ -41,7 +41,7 @@ packages: # extra-deps: - git: https://github.com/L0neGamer/discord-haskell.git - commit: bdc81ab1fa3816f343b337d5dfb0910a9538ae5c + commit: 430979b51158f7a1e3045ac1d9708da204d544ab - emoji-0.1.0.2 - load-env-0.2.1.0 - megaparsec-9.0.1 From 0c5d286b162d33ebb29842076958657aacddcc29 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 27 Jan 2022 19:43:46 +0000 Subject: [PATCH 26/96] added random, show, and add to the application command quote stuff --- src/Tablebot/Handler.hs | 1 + src/Tablebot/Internal/Handler/Event.hs | 7 ++ src/Tablebot/Plugins/Quote.hs | 97 ++++++++++++++++++++++---- src/Tablebot/Utility/Discord.hs | 2 +- stack.yaml | 2 +- 5 files changed, 95 insertions(+), 14 deletions(-) diff --git a/src/Tablebot/Handler.hs b/src/Tablebot/Handler.hs index 96c1b552..c584ba94 100644 --- a/src/Tablebot/Handler.hs +++ b/src/Tablebot/Handler.hs @@ -77,6 +77,7 @@ eventHandler pl prefix = \case MessageReactionRemoveEmoji _rri -> pure () InteractionCreate i@InteractionComponent {} -> parseComponentRecv (compiledOnComponentRecvs pl) i `interactionErrorCatch` i InteractionCreate i@InteractionApplicationCommand {} -> parseApplicationCommandRecv i `interactionErrorCatch` i + InteractionCreate i@InteractionApplicationCommandAutocomplete {} -> parseApplicationCommandRecv i `interactionErrorCatch` i -- TODO: add application command autocomplete as an option e -> parseOther (compiledOtherEvents pl) e where diff --git a/src/Tablebot/Internal/Handler/Event.hs b/src/Tablebot/Internal/Handler/Event.hs index 83cc54fe..f07d8458 100644 --- a/src/Tablebot/Internal/Handler/Event.hs +++ b/src/Tablebot/Internal/Handler/Event.hs @@ -77,6 +77,13 @@ parseApplicationCommandRecv info@InteractionApplicationCommand {interactionDataA case action of Nothing -> throwBot $ InteractionException "could not find the given application command" Just act -> changeAction () $ act info +parseApplicationCommandRecv info@InteractionApplicationCommandAutocomplete {interactionDataApplicationCommand = idac} = do + tvar <- ask + cache <- liftIO $ readMVar tvar + let action = UT.cacheApplicationCommands cache M.!? interactionDataApplicationCommandId idac + case action of + Nothing -> throwBot $ InteractionException "could not find the given application command" + Just act -> changeAction () $ act info parseApplicationCommandRecv _ = return () -- | This runs each 'Other' feature in @cs@ with the Discord 'Event' provided. diff --git a/src/Tablebot/Plugins/Quote.hs b/src/Tablebot/Plugins/Quote.hs index 30b220e0..e6f0806b 100644 --- a/src/Tablebot/Plugins/Quote.hs +++ b/src/Tablebot/Plugins/Quote.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE LambdaCase #-} {-# OPTIONS_GHC -Wno-unused-top-binds #-} -- | @@ -15,12 +16,15 @@ module Tablebot.Plugins.Quote (quotePlugin) where import Control.Monad.IO.Class (liftIO) import Data.Aeson import Data.Default (Default (def)) +import Data.Functor ((<&>)) import Data.Maybe (catMaybes) import Data.Text (Text, append, pack, unpack) import Data.Time.Clock.System (SystemTime (systemSeconds), getSystemTime, systemToUTCTime) import Database.Persist.Sqlite (Filter, SelectOpt (LimitTo, OffsetBy), entityVal, (==.)) import Database.Persist.TH +import Discord (restCall) import Discord.Interactions +import qualified Discord.Internal.Rest.Interactions as R import Discord.Types import GHC.Generics (Generic) import GHC.Int (Int64) @@ -117,7 +121,7 @@ thisQuote = Command "this" (parseComm thisComm) [] thisComm = thisQ quoteMessageAppComm :: Maybe ApplicationCommandRecv -quoteMessageAppComm = appcomm >>= return . (`ApplicationCommandRecv` recv) +quoteMessageAppComm = appcomm <&> (`ApplicationCommandRecv` recv) where appcomm = createApplicationCommandMessage "quote" recv i@InteractionApplicationCommand {interactionDataApplicationCommand = InteractionDataApplicationCommandMessage {..}, ..} = do @@ -128,12 +132,9 @@ quoteMessageAppComm = appcomm >>= return . (`ApplicationCommandRecv` recv) m <- getMessage cid mid case m of Left _ -> throwBot $ InteractionException "could not get message to quote" - Right msg -> interactionResponseCustomMessage i =<< addQ' (messageContent msg) (toMention $ messageAuthor msg) (toMention' $ parseUserId $ contextUserId i) mid cid i + Right msg -> interactionResponseCustomMessage i =<< fst <$> addQ' (messageContent msg) (toMention $ messageAuthor msg) (toMention' $ parseUserId $ contextUserId i) mid cid i recv _ = return def --- return $ messageDetailsBasic (pack $ show idac) --- content <- - authorQuote :: Command authorQuote = Command "author" (parseComm authorComm) [] where @@ -215,15 +216,15 @@ filteredRandomQuote' quoteFilter errorMessage m = do -- @!quote add "quoted text" - author@, and then stores said quote in the -- database, returning the ID used. addQ :: Text -> Text -> Message -> DatabaseDiscord MessageDetails -addQ qu author m = addQ' qu author (toMention $ messageAuthor m) (messageId m) (messageChannelId m) m +addQ qu author m = fst <$> addQ' qu author (toMention $ messageAuthor m) (messageId m) (messageChannelId m) m -addQ' :: Context m => Text -> Text -> Text -> MessageId -> ChannelId -> m -> DatabaseDiscord MessageDetails +addQ' :: Context m => Text -> Text -> Text -> MessageId -> ChannelId -> m -> DatabaseDiscord (MessageDetails, Int64) addQ' qu author requestor sourceMsg sourceChannel m = do now <- liftIO $ systemToUTCTime <$> getSystemTime let new = Quote qu author requestor (fromIntegral sourceMsg) (fromIntegral sourceChannel) now added <- insert new let res = pack $ show $ fromSqlKey added - renderCustomQuoteMessage ("Quote added as #" `append` res) new (fromSqlKey added) m + renderCustomQuoteMessage ("Quote added as #" `append` res) new (fromSqlKey added) m <&> (,fromSqlKey added) -- | @thisQuote@, which takes the replied message or the -- previous message and stores said message as a quote in the database, @@ -318,6 +319,78 @@ renderCustomQuoteMessage t (Quote txt author submitter msgId cnlId dtm) qId m = maybeAddFooter (Just l) = "\n[source](" <> l <> ") - added by " <> submitter maybeAddFooter Nothing = "" +quoteApplicationCommand :: CreateApplicationCommand +quoteApplicationCommand = CreateApplicationCommandChatInput "quote" "store and retrieve quotes" (Just opts) True + where + opts = + ApplicationCommandOptionsSubcommands $ + ApplicationCommandOptionSubcommandOrGroupSubcommand + <$> [ addQuoteAppComm, + showQuoteAppComm, + randomQuoteAppComm + ] + addQuoteAppComm = + ApplicationCommandOptionSubcommand + "add" + "add a new quote" + [ ApplicationCommandOptionValueString "quote" "what the actual quote is" True (Left False), + ApplicationCommandOptionValueString "author" "who authored this quote" True (Left False) + ] + showQuoteAppComm = + ApplicationCommandOptionSubcommand + "show" + "show a quote by number" + [ ApplicationCommandOptionValueInteger "id" "the quote's number" True (Left True) (Just 1) Nothing + ] + randomQuoteAppComm = + ApplicationCommandOptionSubcommand + "random" + "show a random quote" + [] + +quoteApplicationCommandRecv :: Interaction -> DatabaseDiscord () +quoteApplicationCommandRecv i@InteractionApplicationCommand {interactionDataApplicationCommand = InteractionDataApplicationCommandChatInput {interactionDataApplicationCommandOptions = Just (InteractionDataApplicationCommandOptionsSubcommands [InteractionDataApplicationCommandOptionSubcommandOrGroupSubcommand subc])}} = case subcname of + "random" -> randomQ i >>= interactionResponseCustomMessage i + "show" -> + handleNothing + (getValue "id" vals) + ( \case + InteractionDataApplicationCommandOptionValueInteger _ (Right showid') -> showQ (fromIntegral showid') i >>= interactionResponseCustomMessage i + _ -> return () + ) + "add" -> + handleNothing + (getValue "quote" vals >>= \q -> getValue "author" vals <&> (q,)) + ( \(qt, author) -> do + let qt' = either id id $ interactionDataApplicationCommandOptionValueStringValue qt + author' = either id id $ interactionDataApplicationCommandOptionValueStringValue author + requestor = toMention' $ parseUserId $ contextUserId i + (msg, qid) <- addQ' qt' author' requestor 0 0 i + interactionResponseCustomMessage i msg + -- to get the message to display as wanted, we have to do some trickery + -- we have already sent off the message above with the broken message id + -- and channel id, but now we have sent off this message we can refer + -- to it! We just have to get that message, overwrite the quote, and + -- hope no one cares about the edit message + v <- liftDiscord $ restCall $ R.GetOriginalInteractionResponse (interactionApplicationId i) (interactionToken i) + case v of + Left _ -> return () + Right m -> do + now <- liftIO $ systemToUTCTime <$> getSystemTime + let new = Quote qt' author' requestor (fromIntegral $ messageId m) (fromIntegral $ messageChannelId m) now + replace (toSqlKey qid) new + newMsg <- renderCustomQuoteMessage (messageContent m) new qid i + _ <- liftDiscord $ restCall $ R.EditOriginalInteractionResponse (interactionApplicationId i) (interactionToken i) (convertMessageFormatInteraction newMsg) + return () + ) + _ -> undefined + where + subcname = interactionDataApplicationCommandOptionSubcommandName subc + vals = interactionDataApplicationCommandOptionSubcommandOptions subc + handleNothing Nothing _ = return () + handleNothing (Just a) f = f a +quoteApplicationCommandRecv _ = return () + showQuoteHelp :: HelpPage showQuoteHelp = HelpPage @@ -341,10 +414,10 @@ randomQuoteHelp = authorQuoteHelp :: HelpPage authorQuoteHelp = HelpPage - "user" + "author" [] - "show a random quote by a user" - "**Random User Quote**\nDisplays a random quote attributed to a particular user\n\n*Usage:* `quote user `" + "show a random quote by a author" + "**Random User Quote**\nDisplays a random quote attributed to a particular author\n\n*Usage:* `quote author `" [] Superuser @@ -415,7 +488,7 @@ quotePlugin = onReactionAdds = [quoteReactionAdd], migrations = [quoteMigration], helpPages = [quoteHelp], - applicationCommands = catMaybes [quoteMessageAppComm] + applicationCommands = [ApplicationCommandRecv quoteApplicationCommand quoteApplicationCommandRecv] ++ catMaybes [quoteMessageAppComm] } deriving instance Generic Quote diff --git a/src/Tablebot/Utility/Discord.hs b/src/Tablebot/Utility/Discord.hs index d9d4dc34..7bedd77a 100644 --- a/src/Tablebot/Utility/Discord.hs +++ b/src/Tablebot/Utility/Discord.hs @@ -369,7 +369,7 @@ createApplicationCommand :: ApplicationId -> GuildId -> CreateApplicationCommand createApplicationCommand aid gid cac = do res <- restCall $ R.CreateGuildApplicationCommand aid gid cac case res of - Left _ -> throw $ InteractionException "Failed to create application command." + Left e -> throw $ InteractionException $ "Failed to create application command :" ++ show e Right a -> return a -- | Remove all application commands that are active in the given server that diff --git a/stack.yaml b/stack.yaml index 9b440403..a88183e2 100644 --- a/stack.yaml +++ b/stack.yaml @@ -41,7 +41,7 @@ packages: # extra-deps: - git: https://github.com/L0neGamer/discord-haskell.git - commit: 430979b51158f7a1e3045ac1d9708da204d544ab + commit: cd282b186bd0ade64980ef020501a442dd206f3e - emoji-0.1.0.2 - load-env-0.2.1.0 - megaparsec-9.0.1 From 71fee908d27c86570ab471de5136b7e415956a02 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 27 Jan 2022 19:50:13 +0000 Subject: [PATCH 27/96] beginning autocomplete for show --- src/Tablebot/Plugins/Quote.hs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Tablebot/Plugins/Quote.hs b/src/Tablebot/Plugins/Quote.hs index e6f0806b..9ac4570a 100644 --- a/src/Tablebot/Plugins/Quote.hs +++ b/src/Tablebot/Plugins/Quote.hs @@ -389,6 +389,21 @@ quoteApplicationCommandRecv i@InteractionApplicationCommand {interactionDataAppl vals = interactionDataApplicationCommandOptionSubcommandOptions subc handleNothing Nothing _ = return () handleNothing (Just a) f = f a +quoteApplicationCommandRecv i@InteractionApplicationCommandAutocomplete {interactionDataApplicationCommand = InteractionDataApplicationCommandChatInput {interactionDataApplicationCommandOptions = Just (InteractionDataApplicationCommandOptionsSubcommands [InteractionDataApplicationCommandOptionSubcommandOrGroupSubcommand subc])}} = case subcname of + "show" -> + handleNothing + (getValue "id" vals) + ( \case + InteractionDataApplicationCommandOptionValueInteger _ (Right showid') -> interactionResponseAutocomplete i $ InteractionResponseAutocompleteInteger [Choice (pack $ show showid') showid'] + InteractionDataApplicationCommandOptionValueInteger _ (Left showid') -> + -- interactionResponseAutocomplete i $ InteractionResponseAutocompleteInteger [Choice (pack $ show showid') showid'] + _ -> return () + ) + where + subcname = interactionDataApplicationCommandOptionSubcommandName subc + vals = interactionDataApplicationCommandOptionSubcommandOptions subc + handleNothing Nothing _ = return () + handleNothing (Just a) f = f a quoteApplicationCommandRecv _ = return () showQuoteHelp :: HelpPage From 25cba32c3cb59171ea775ddb480c867a4f213fa9 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 28 Jan 2022 01:38:33 +0000 Subject: [PATCH 28/96] added an autocomplete option for quote show application command --- src/Tablebot/Plugins/Quote.hs | 20 +++++++++++++------- src/Tablebot/Utility/Search.hs | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/Tablebot/Plugins/Quote.hs b/src/Tablebot/Plugins/Quote.hs index 9ac4570a..fa18c299 100644 --- a/src/Tablebot/Plugins/Quote.hs +++ b/src/Tablebot/Plugins/Quote.hs @@ -20,7 +20,7 @@ import Data.Functor ((<&>)) import Data.Maybe (catMaybes) import Data.Text (Text, append, pack, unpack) import Data.Time.Clock.System (SystemTime (systemSeconds), getSystemTime, systemToUTCTime) -import Database.Persist.Sqlite (Filter, SelectOpt (LimitTo, OffsetBy), entityVal, (==.)) +import Database.Persist.Sqlite (Entity (entityKey), Filter, SelectOpt (LimitTo, OffsetBy), entityVal, (==.)) import Database.Persist.TH import Discord (restCall) import Discord.Interactions @@ -36,6 +36,7 @@ import Tablebot.Utility.Discord getMessageLink, getPrecedingMessage, getReplyMessage, + interactionResponseAutocomplete, interactionResponseCustomMessage, sendCustomMessage, sendMessage, @@ -45,6 +46,7 @@ import Tablebot.Utility.Discord import Tablebot.Utility.Embed import Tablebot.Utility.Exception (BotException (GenericException, InteractionException), catchBot, throwBot) import Tablebot.Utility.Permission (requirePermission) +import Tablebot.Utility.Search import Tablebot.Utility.SmartParser import Text.RawString.QQ (r) @@ -390,15 +392,19 @@ quoteApplicationCommandRecv i@InteractionApplicationCommand {interactionDataAppl handleNothing Nothing _ = return () handleNothing (Just a) f = f a quoteApplicationCommandRecv i@InteractionApplicationCommandAutocomplete {interactionDataApplicationCommand = InteractionDataApplicationCommandChatInput {interactionDataApplicationCommandOptions = Just (InteractionDataApplicationCommandOptionsSubcommands [InteractionDataApplicationCommandOptionSubcommandOrGroupSubcommand subc])}} = case subcname of - "show" -> + "show" -> handleNothing (getValue "id" vals) ( \case InteractionDataApplicationCommandOptionValueInteger _ (Right showid') -> interactionResponseAutocomplete i $ InteractionResponseAutocompleteInteger [Choice (pack $ show showid') showid'] - InteractionDataApplicationCommandOptionValueInteger _ (Left showid') -> - -- interactionResponseAutocomplete i $ InteractionResponseAutocompleteInteger [Choice (pack $ show showid') showid'] + InteractionDataApplicationCommandOptionValueInteger _ (Left showid') -> do + allQ <- allQuotes () + let allQ' = (\qe -> (show (fromSqlKey $ entityKey qe), (fromSqlKey $ entityKey qe, (\(Quote q _ _ _ _ _) -> q) (entityVal qe)))) <$> allQ + options = take 25 $ closestPairsWithCosts (def {deletion = 100, substitution = 100, transposition = 5}) allQ' (unpack showid') + interactionResponseAutocomplete i $ InteractionResponseAutocompleteInteger ((\(qids, (qid, _)) -> Choice (pack qids) (toInteger qid)) <$> options) _ -> return () ) + _ -> return () where subcname = interactionDataApplicationCommandOptionSubcommandName subc vals = interactionDataApplicationCommandOptionSubcommandOptions subc @@ -513,8 +519,8 @@ instance FromJSON Quote instance ToJSON Quote -- | Get all the quotes in the database. -allQuotes :: DatabaseDiscord [Quote] -allQuotes = fmap entityVal <$> selectList [] [] +allQuotes :: () -> DatabaseDiscord [Entity Quote] +allQuotes _ = selectList [] [] -- | Export all the quotes in the database to either a default quotes file or to a given -- file name that is quoted in the command. Superuser only. @@ -525,7 +531,7 @@ exportQ :: Maybe (Quoted FilePath) -> Message -> DatabaseDiscord () exportQ qfp m = requirePermission Superuser m $ do let defFileName = getSystemTime >>= \now -> return $ "quotes_" <> show (systemSeconds now) <> ".json" (Qu fp) <- liftIO $ maybe (Qu <$> defFileName) return qfp - aq <- allQuotes + aq <- fmap entityVal <$> allQuotes () _ <- liftIO $ encodeFile fp aq sendMessage m ("Succesfully exported all " <> (pack . show . length) aq <> " quotes to `" <> pack fp <> "`") diff --git a/src/Tablebot/Utility/Search.hs b/src/Tablebot/Utility/Search.hs index 02bd0832..7c6f8640 100644 --- a/src/Tablebot/Utility/Search.hs +++ b/src/Tablebot/Utility/Search.hs @@ -11,8 +11,12 @@ module Tablebot.Utility.Search ( FuzzyCosts (..), closestMatch, closestMatchWithCosts, + closestMatches, + closestMatchesWithCosts, closestPair, closestPairWithCosts, + closestPairs, + closestPairsWithCosts, closestValue, closestValueWithCosts, shortestSuperString, @@ -21,7 +25,8 @@ module Tablebot.Utility.Search where import Data.Char (toLower) -import Data.List (minimumBy) +import Data.Default +import Data.List (minimumBy, sortBy) import Data.Text (Text, isInfixOf, length, take) import Text.EditDistance @@ -38,6 +43,9 @@ data FuzzyCosts = FuzzyCosts transposition :: Int } +instance Default FuzzyCosts where + def = defaultFuzzyCosts + -- | @convertCosts@ turns the custom FuzzyCosts into Text.EditDistance's -- EditCosts. convertCosts :: FuzzyCosts -> EditCosts @@ -72,6 +80,18 @@ closestMatchWithCosts editCosts strings query = minimumBy (compareOn score) stri score :: String -> Int score = levenshteinDistance (convertCosts editCosts) (map toLower query) +-- | @closestMatches@ takes a list of strings and a query and orders the strings +-- by which most closely matches the query (closest matches first). +closestMatches :: [String] -> String -> [String] +closestMatches = closestMatchesWithCosts defaultFuzzyCosts + +-- | @closestMatchesWithCosts@ is @closestMatches@ with customisable edit costs. +closestMatchesWithCosts :: FuzzyCosts -> [String] -> String -> [String] +closestMatchesWithCosts editCosts strings query = sortBy (compareOn score) strings + where + score :: String -> Int + score = levenshteinDistance (convertCosts editCosts) (map toLower query) + -- | @closestPair@ takes a set of pairs and a query and finds the pair whose key -- most closely matches the query. closestPair :: [(String, a)] -> String -> (String, a) @@ -84,6 +104,18 @@ closestPairWithCosts editCosts pairs query = minimumBy (compareOn $ score . fst) score :: String -> Int score = levenshteinDistance (convertCosts editCosts) (map toLower query) +-- | @closestPairs@ takes a list of strings and a query and orders the strings +-- by which most closely matches the query (closest matches first). +closestPairs :: [(String, a)] -> String -> [(String, a)] +closestPairs = closestPairsWithCosts defaultFuzzyCosts + +-- | @closestMatchesWithCosts@ is @closestMatches@ with customisable edit costs. +closestPairsWithCosts :: FuzzyCosts -> [(String, a)] -> String -> [(String, a)] +closestPairsWithCosts editCosts pairs query = sortBy (compareOn (score . fst)) pairs + where + score :: String -> Int + score = levenshteinDistance (convertCosts editCosts) (map toLower query) + -- | @closestValue@ is @closestPair@ but it only returns the value of the -- matched pair. closestValue :: [(String, a)] -> String -> a From 31ec7a378f7542f0ed3347db1965a65551999421 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 28 Jan 2022 01:56:04 +0000 Subject: [PATCH 29/96] fixed up message click option --- src/Tablebot/Plugins/Quote.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Tablebot/Plugins/Quote.hs b/src/Tablebot/Plugins/Quote.hs index fa18c299..2034e2df 100644 --- a/src/Tablebot/Plugins/Quote.hs +++ b/src/Tablebot/Plugins/Quote.hs @@ -134,7 +134,7 @@ quoteMessageAppComm = appcomm <&> (`ApplicationCommandRecv` recv) m <- getMessage cid mid case m of Left _ -> throwBot $ InteractionException "could not get message to quote" - Right msg -> interactionResponseCustomMessage i =<< fst <$> addQ' (messageContent msg) (toMention $ messageAuthor msg) (toMention' $ parseUserId $ contextUserId i) mid cid i + Right msg -> interactionResponseCustomMessage i =<< addMessageQuote (parseUserId $ contextUserId i) msg i recv _ = return def authorQuote :: Command @@ -262,8 +262,8 @@ addMessageQuote submitter q' m = do added <- insert new let res = pack $ show $ fromSqlKey added renderCustomQuoteMessage ("Quote added as #" `append` res) new (fromSqlKey added) m - else return $ messageDetailsBasic "Can't quote a bot" - else return $ messageDetailsBasic "Message already quoted" + else return $ (messageDetailsBasic "Can't quote a bot") {messageDetailsFlags = Just $ InteractionResponseMessageFlags [InteractionResponseMessageFlagEphermeral]} + else return $ (messageDetailsBasic "Message already quoted") {messageDetailsFlags = Just $ InteractionResponseMessageFlags [InteractionResponseMessageFlagEphermeral]} -- | @editQuote@, which looks for a message of the form -- @!quote edit n "quoted text" - author@, and then updates quote with id n in the From 770fcb9638537146e5a0036bdd463b1670624fb9 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 28 Jan 2022 17:25:48 +0000 Subject: [PATCH 30/96] added getters for values, and added quote author options --- package.yaml | 1 + src/Tablebot/Internal/Handler/Event.hs | 2 +- src/Tablebot/Plugins/Quote.hs | 46 ++++++++++++++++++-------- src/Tablebot/Plugins/Roll/Plugin.hs | 2 +- src/Tablebot/Utility/SmartParser.hs | 13 ++++++++ 5 files changed, 48 insertions(+), 16 deletions(-) diff --git a/package.yaml b/package.yaml index 39b4b9d3..d7cd2e63 100644 --- a/package.yaml +++ b/package.yaml @@ -59,6 +59,7 @@ dependencies: - unliftio - split - regex-pcre +- scientific library: source-dirs: src diff --git a/src/Tablebot/Internal/Handler/Event.hs b/src/Tablebot/Internal/Handler/Event.hs index f07d8458..bd25acb4 100644 --- a/src/Tablebot/Internal/Handler/Event.hs +++ b/src/Tablebot/Internal/Handler/Event.hs @@ -62,7 +62,7 @@ parseReactionDel cs info = mapM_ doReactionAdd cs parseComponentRecv :: [CompiledComponentRecv] -> Interaction -> CompiledDatabaseDiscord () parseComponentRecv cs info@InteractionComponent {interactionDataComponent = idc} = mapM_ removePrefix cs' where - getPrefix ccr = componentPluginName ccr <> componentName ccr + getPrefix ccr = componentPluginName ccr <> " " <> componentName ccr cs' = filter (\ccr -> getPrefix ccr `isPrefixOf` interactionDataComponentCustomId idc) cs removePrefix ccr = ccr `onComponentRecv` (info {interactionDataComponent = (idc {interactionDataComponentCustomId = T.drop (T.length (getPrefix ccr)) (interactionDataComponentCustomId idc)})}) parseComponentRecv _ _ = return () diff --git a/src/Tablebot/Plugins/Quote.hs b/src/Tablebot/Plugins/Quote.hs index 2034e2df..393c8b47 100644 --- a/src/Tablebot/Plugins/Quote.hs +++ b/src/Tablebot/Plugins/Quote.hs @@ -182,12 +182,22 @@ showQ qId m = do -- | @randomQuote@, which looks for a message of the form @!quote random@, -- selects a random quote from the database and responds with that quote. randomQ :: Context m => m -> DatabaseDiscord MessageDetails -randomQ = filteredRandomQuote [] "Couldn't find any quotes!" +randomQ c = filteredRandomQuote [] "Couldn't find any quotes!" c >>= \m -> return (m {messageDetailsComponents = Just [ComponentActionRowButton [randomButton]]}) + where + randomButton = mkButton "Random quote" "quote random" + +randomQuoteComponentRecv :: ComponentRecv +randomQuoteComponentRecv = ComponentRecv "random" (processComponentInteraction (randomQ @Interaction) True) -- | @authorQuote@, which looks for a message of the form @!quote author u@, -- selects a random quote from the database attributed to u and responds with that quote. authorQ :: Context m => Text -> m -> DatabaseDiscord MessageDetails -authorQ t = filteredRandomQuote [QuoteAuthor ==. t] "Couldn't find any quotes with that author!" +authorQ t c = filteredRandomQuote [QuoteAuthor ==. t] "Couldn't find any quotes with that author!" c >>= \m -> return (m {messageDetailsComponents = Just [ComponentActionRowButton [authorButton]]}) + where + authorButton = mkButton "Random author quote" ("quote author " <> t) + +authorQuoteComponentRecv :: ComponentRecv +authorQuoteComponentRecv = ComponentRecv "author" (processComponentInteraction (\(ROI t) -> (authorQ @Interaction t)) True) -- | @filteredRandomQuote@ selects a random quote that meets a -- given criteria, and returns that as the response, sending the user a message if the @@ -329,7 +339,8 @@ quoteApplicationCommand = CreateApplicationCommandChatInput "quote" "store and r ApplicationCommandOptionSubcommandOrGroupSubcommand <$> [ addQuoteAppComm, showQuoteAppComm, - randomQuoteAppComm + randomQuoteAppComm, + authorQuoteAppComm ] addQuoteAppComm = ApplicationCommandOptionSubcommand @@ -349,25 +360,31 @@ quoteApplicationCommand = CreateApplicationCommandChatInput "quote" "store and r "random" "show a random quote" [] + authorQuoteAppComm = + ApplicationCommandOptionSubcommand + "author" + "show a random quote by an author" + [ApplicationCommandOptionValueString "author" "whose quotes do you want to see" True (Left False)] quoteApplicationCommandRecv :: Interaction -> DatabaseDiscord () quoteApplicationCommandRecv i@InteractionApplicationCommand {interactionDataApplicationCommand = InteractionDataApplicationCommandChatInput {interactionDataApplicationCommandOptions = Just (InteractionDataApplicationCommandOptionsSubcommands [InteractionDataApplicationCommandOptionSubcommandOrGroupSubcommand subc])}} = case subcname of "random" -> randomQ i >>= interactionResponseCustomMessage i + "author" -> + handleNothing + (getValue "author" vals >>= stringFromOptionValue) + ( \author -> authorQ author i >>= interactionResponseCustomMessage i + ) "show" -> handleNothing - (getValue "id" vals) - ( \case - InteractionDataApplicationCommandOptionValueInteger _ (Right showid') -> showQ (fromIntegral showid') i >>= interactionResponseCustomMessage i - _ -> return () + (getValue "id" vals >>= integerFromOptionValue) + ( \showid -> showQ (fromIntegral showid) i >>= interactionResponseCustomMessage i ) "add" -> handleNothing - (getValue "quote" vals >>= \q -> getValue "author" vals <&> (q,)) + ((getValue "quote" vals >>= stringFromOptionValue) >>= \q -> (getValue "author" vals >>= stringFromOptionValue) <&> (q,)) ( \(qt, author) -> do - let qt' = either id id $ interactionDataApplicationCommandOptionValueStringValue qt - author' = either id id $ interactionDataApplicationCommandOptionValueStringValue author - requestor = toMention' $ parseUserId $ contextUserId i - (msg, qid) <- addQ' qt' author' requestor 0 0 i + let requestor = toMention' $ parseUserId $ contextUserId i + (msg, qid) <- addQ' qt author requestor 0 0 i interactionResponseCustomMessage i msg -- to get the message to display as wanted, we have to do some trickery -- we have already sent off the message above with the broken message id @@ -379,7 +396,7 @@ quoteApplicationCommandRecv i@InteractionApplicationCommand {interactionDataAppl Left _ -> return () Right m -> do now <- liftIO $ systemToUTCTime <$> getSystemTime - let new = Quote qt' author' requestor (fromIntegral $ messageId m) (fromIntegral $ messageChannelId m) now + let new = Quote qt author requestor (fromIntegral $ messageId m) (fromIntegral $ messageChannelId m) now replace (toSqlKey qid) new newMsg <- renderCustomQuoteMessage (messageContent m) new qid i _ <- liftDiscord $ restCall $ R.EditOriginalInteractionResponse (interactionApplicationId i) (interactionToken i) (convertMessageFormatInteraction newMsg) @@ -509,7 +526,8 @@ quotePlugin = onReactionAdds = [quoteReactionAdd], migrations = [quoteMigration], helpPages = [quoteHelp], - applicationCommands = [ApplicationCommandRecv quoteApplicationCommand quoteApplicationCommandRecv] ++ catMaybes [quoteMessageAppComm] + applicationCommands = [ApplicationCommandRecv quoteApplicationCommand quoteApplicationCommandRecv] ++ catMaybes [quoteMessageAppComm], + onComponentRecvs = [randomQuoteComponentRecv, authorQuoteComponentRecv] } deriving instance Generic Quote diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 183888a4..90678d84 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -69,7 +69,7 @@ rollDice' e t u@(ParseUserId uid) = do { messageDetailsComponents = Just [ ComponentActionRowButton - [ (mkButton "Reroll" ((("rollreroll " <> pack (show uid)) `appendIf` e) `appendIf` t)) {componentButtonEmoji = Just (Emoji (Just 0) "🎲" Nothing Nothing Nothing (Just False))} + [ (mkButton "Reroll" ((("roll reroll " <> pack (show uid)) `appendIf` e) `appendIf` t)) {componentButtonEmoji = Just (Emoji (Just 0) "🎲" Nothing Nothing Nothing (Just False))} ] ] } diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index e7ebdc5f..73959528 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -17,6 +17,7 @@ module Tablebot.Utility.SmartParser where import Control.Monad.Exception (MonadException (catch)) import Data.Default (Default (def)) import Data.Proxy (Proxy (..)) +import Data.Scientific import Data.String (IsString (fromString)) import Data.Text (Text, pack) import Discord.Interactions @@ -356,6 +357,18 @@ class ProcessAppCommArg t s where getValue :: String -> [InteractionDataApplicationCommandOptionValue] -> Maybe InteractionDataApplicationCommandOptionValue getValue t = find ((== pack t) . interactionDataApplicationCommandOptionValueName) +integerFromOptionValue :: InteractionDataApplicationCommandOptionValue -> Maybe Integer +integerFromOptionValue InteractionDataApplicationCommandOptionValueInteger {interactionDataApplicationCommandOptionValueIntegerValue = Right i} = Just i +integerFromOptionValue _ = Nothing + +scientificFromOptionValue :: InteractionDataApplicationCommandOptionValue -> Maybe Scientific +scientificFromOptionValue InteractionDataApplicationCommandOptionValueNumber {interactionDataApplicationCommandOptionValueNumberValue = Right i} = Just i +scientificFromOptionValue _ = Nothing + +stringFromOptionValue :: InteractionDataApplicationCommandOptionValue -> Maybe Text +stringFromOptionValue InteractionDataApplicationCommandOptionValueString {interactionDataApplicationCommandOptionValueStringValue = Right i} = Just i +stringFromOptionValue _ = Nothing + instance (KnownSymbol name) => ProcessAppCommArg (Labelled name desc Text) s where processAppCommArg is = case getValue (symbolVal (Proxy :: Proxy name)) is of Just (InteractionDataApplicationCommandOptionValueString _ (Right t)) -> return $ labelValue t From 16d5c49452e21b48e1a5eb32eb45c33945bdb997 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 28 Jan 2022 18:28:24 +0000 Subject: [PATCH 31/96] added edit quote option --- src/Tablebot/Plugins/Quote.hs | 56 +++++++++++++++++++++++++++-------- src/Tablebot/Utility/Types.hs | 11 ++++++- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/src/Tablebot/Plugins/Quote.hs b/src/Tablebot/Plugins/Quote.hs index 393c8b47..a1621133 100644 --- a/src/Tablebot/Plugins/Quote.hs +++ b/src/Tablebot/Plugins/Quote.hs @@ -17,7 +17,7 @@ import Control.Monad.IO.Class (liftIO) import Data.Aeson import Data.Default (Default (def)) import Data.Functor ((<&>)) -import Data.Maybe (catMaybes) +import Data.Maybe (catMaybes, fromMaybe) import Data.Text (Text, append, pack, unpack) import Data.Time.Clock.System (SystemTime (systemSeconds), getSystemTime, systemToUTCTime) import Database.Persist.Sqlite (Entity (entityKey), Filter, SelectOpt (LimitTo, OffsetBy), entityVal, (==.)) @@ -197,7 +197,7 @@ authorQ t c = filteredRandomQuote [QuoteAuthor ==. t] "Couldn't find any quotes authorButton = mkButton "Random author quote" ("quote author " <> t) authorQuoteComponentRecv :: ComponentRecv -authorQuoteComponentRecv = ComponentRecv "author" (processComponentInteraction (\(ROI t) -> (authorQ @Interaction t)) True) +authorQuoteComponentRecv = ComponentRecv "author" (processComponentInteraction (\(ROI t) -> authorQ @Interaction t) True) -- | @filteredRandomQuote@ selects a random quote that meets a -- given criteria, and returns that as the response, sending the user a message if the @@ -272,25 +272,28 @@ addMessageQuote submitter q' m = do added <- insert new let res = pack $ show $ fromSqlKey added renderCustomQuoteMessage ("Quote added as #" `append` res) new (fromSqlKey added) m - else return $ (messageDetailsBasic "Can't quote a bot") {messageDetailsFlags = Just $ InteractionResponseMessageFlags [InteractionResponseMessageFlagEphermeral]} - else return $ (messageDetailsBasic "Message already quoted") {messageDetailsFlags = Just $ InteractionResponseMessageFlags [InteractionResponseMessageFlagEphermeral]} + else return $ makeEphermeral (messageDetailsBasic "Can't quote a bot") + else return $ makeEphermeral (messageDetailsBasic "Message already quoted") -- | @editQuote@, which looks for a message of the form -- @!quote edit n "quoted text" - author@, and then updates quote with id n in the -- database, to match the provided quote. editQ :: Int64 -> Text -> Text -> Message -> DatabaseDiscord () -editQ qId qu author m = +editQ qId qu author m = editQ' qId (Just qu) (Just author) (toMention $ messageAuthor m) (fromIntegral $ messageId m) (fromIntegral $ messageChannelId m) m >>= sendCustomMessage m + +editQ' :: Context m => Int64 -> Maybe Text -> Maybe Text -> Text -> MessageId -> ChannelId -> m -> DatabaseDiscord MessageDetails +editQ' qId qu author requestor mid cid m = requirePermission Any m $ let k = toSqlKey qId in do - oQu <- get k + (oQu :: Maybe Quote) <- get k case oQu of - Just Quote {} -> do + Just (Quote qu' author' _ _ _ _) -> do now <- liftIO $ systemToUTCTime <$> getSystemTime - let new = Quote qu author (toMention $ messageAuthor m) (fromIntegral $ messageId m) (fromIntegral $ messageChannelId m) now + let new = Quote (fromMaybe qu' qu) (fromMaybe author' author) requestor (fromIntegral mid) (fromIntegral cid) now replace k new - renderCustomQuoteMessage "Quote updated" new qId m >>= sendCustomMessage m - Nothing -> sendMessage m "Couldn't update that quote!" + renderCustomQuoteMessage "Quote updated" new qId m + Nothing -> return $ messageDetailsBasic "Couldn't update that quote!" -- | @deleteQuote@, which looks for a message of the form @!quote delete n@, -- and removes it from the database. @@ -340,7 +343,8 @@ quoteApplicationCommand = CreateApplicationCommandChatInput "quote" "store and r <$> [ addQuoteAppComm, showQuoteAppComm, randomQuoteAppComm, - authorQuoteAppComm + authorQuoteAppComm, + editQuoteAppComm ] addQuoteAppComm = ApplicationCommandOptionSubcommand @@ -365,6 +369,14 @@ quoteApplicationCommand = CreateApplicationCommandChatInput "quote" "store and r "author" "show a random quote by an author" [ApplicationCommandOptionValueString "author" "whose quotes do you want to see" True (Left False)] + editQuoteAppComm = + ApplicationCommandOptionSubcommand + "edit" + "edit a quote" + [ ApplicationCommandOptionValueInteger "quoteid" "the id of the quote to edit" True (Left False) Nothing Nothing, + ApplicationCommandOptionValueString "quote" "what the actual quote is" False (Left False), + ApplicationCommandOptionValueString "author" "who authored this quote" False (Left False) + ] quoteApplicationCommandRecv :: Interaction -> DatabaseDiscord () quoteApplicationCommandRecv i@InteractionApplicationCommand {interactionDataApplicationCommand = InteractionDataApplicationCommandChatInput {interactionDataApplicationCommandOptions = Just (InteractionDataApplicationCommandOptionsSubcommands [InteractionDataApplicationCommandOptionSubcommandOrGroupSubcommand subc])}} = case subcname of @@ -402,7 +414,27 @@ quoteApplicationCommandRecv i@InteractionApplicationCommand {interactionDataAppl _ <- liftDiscord $ restCall $ R.EditOriginalInteractionResponse (interactionApplicationId i) (interactionToken i) (convertMessageFormatInteraction newMsg) return () ) - _ -> undefined + "edit" -> + handleNothing + (getValue "quoteid" vals >>= integerFromOptionValue) + ( \qid' -> do + let qid = fromIntegral qid' + qt = getValue "quote" vals >>= stringFromOptionValue + author = getValue "author" vals >>= stringFromOptionValue + case (qt, author) of + (Nothing, Nothing) -> interactionResponseCustomMessage i (makeEphermeral (messageDetailsBasic "No edits made to quote.")) + _ -> do + msg <- editQ' qid qt author (toMention' $ parseUserId $ contextUserId i) 0 0 i + interactionResponseCustomMessage i msg + v <- liftDiscord $ restCall $ R.GetOriginalInteractionResponse (interactionApplicationId i) (interactionToken i) + case v of + Left _ -> return () + Right m -> do + msg' <- editQ' qid qt author (toMention' $ parseUserId $ contextUserId i) (fromIntegral $ messageId m) (fromIntegral $ messageChannelId m) i + _ <- liftDiscord $ restCall $ R.EditOriginalInteractionResponse (interactionApplicationId i) (interactionToken i) (convertMessageFormatInteraction msg') + return () + ) + _ -> throwBot $ InteractionException "unexpected quote interaction" where subcname = interactionDataApplicationCommandOptionSubcommandName subc vals = interactionDataApplicationCommandOptionSubcommandOptions subc diff --git a/src/Tablebot/Utility/Types.hs b/src/Tablebot/Utility/Types.hs index 72b8cb56..ca6e56e5 100644 --- a/src/Tablebot/Utility/Types.hs +++ b/src/Tablebot/Utility/Types.hs @@ -23,7 +23,13 @@ import Data.Text (Text) import Data.Void (Void) import Database.Persist.Sqlite (Migration, SqlPersistM, SqlPersistT) import Discord (DiscordHandler) -import Discord.Interactions (CreateApplicationCommand, Interaction, InteractionResponseMessage (InteractionResponseMessage), InteractionResponseMessageFlags) +import Discord.Interactions + ( CreateApplicationCommand, + Interaction, + InteractionResponseMessage (InteractionResponseMessage), + InteractionResponseMessageFlag (..), + InteractionResponseMessageFlags (..), + ) import Discord.Internal.Rest.Channel (MessageDetailedOpts (MessageDetailedOpts)) import Discord.Types ( AllowedMentions, @@ -304,6 +310,9 @@ data MessageDetails = MessageDetails messageDetailsStickerIds :: Maybe [StickerId] } +makeEphermeral :: MessageDetails -> MessageDetails +makeEphermeral m = m {messageDetailsFlags = Just $ InteractionResponseMessageFlags [InteractionResponseMessageFlagEphermeral]} + convertMessageFormatInteraction :: MessageDetails -> InteractionResponseMessage convertMessageFormatInteraction MessageDetails {..} = InteractionResponseMessage From c181cac7deda032d2bd6622f638a143d0827d799 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Tue, 1 Feb 2022 17:52:33 +0000 Subject: [PATCH 32/96] initial (kinda working) variables in dice expressions --- src/Tablebot/Internal/Handler/Command.hs | 12 +- src/Tablebot/Plugins/Roll/Dice.hs | 4 +- src/Tablebot/Plugins/Roll/Dice/DiceData.hs | 13 +- src/Tablebot/Plugins/Roll/Dice/DiceEval.hs | 129 ++++++++++++++---- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 45 ++++-- src/Tablebot/Plugins/Roll/Plugin.hs | 15 +- 6 files changed, 163 insertions(+), 55 deletions(-) diff --git a/src/Tablebot/Internal/Handler/Command.hs b/src/Tablebot/Internal/Handler/Command.hs index b2805188..9205fc6b 100644 --- a/src/Tablebot/Internal/Handler/Command.hs +++ b/src/Tablebot/Internal/Handler/Command.hs @@ -21,6 +21,7 @@ import Data.Maybe (catMaybes) import Data.Set (singleton, toList) import Data.Text (Text) import Data.Void (Void) +import Debug.Trace (trace) import Discord.Types (Message (messageText)) import Tablebot.Internal.Plugins (changeAction) import Tablebot.Internal.Types @@ -106,11 +107,12 @@ makeBundleReadable (ParseErrorBundle errs state) = -- This uses the Label hidden within each error to build an error message, -- as we have used labels to give parsers user-facing errors. makeReadable :: ParseError Text Void -> (ParseError Text ReadableError, Maybe String) -makeReadable (TrivialError i _ good) = - let (lab, others) = getLabel (toList good) - in case lab of - Just l -> (FancyError i . singleton . ErrorCustom $ KnownError l others, Just l) - Nothing -> (FancyError i . singleton $ ErrorCustom UnknownError, Nothing) +makeReadable te@(TrivialError i _ good) = + trace (show te) $ + let (lab, others) = getLabel (toList good) + in case lab of + Just l -> (FancyError i . singleton . ErrorCustom $ KnownError l others, Just l) + Nothing -> (FancyError i . singleton $ ErrorCustom UnknownError, Nothing) where getLabel :: [ErrorItem (Token Text)] -> (Maybe String, [String]) getLabel [] = (Nothing, []) diff --git a/src/Tablebot/Plugins/Roll/Dice.hs b/src/Tablebot/Plugins/Roll/Dice.hs index 06a4b0f8..66a995c1 100644 --- a/src/Tablebot/Plugins/Roll/Dice.hs +++ b/src/Tablebot/Plugins/Roll/Dice.hs @@ -55,7 +55,7 @@ -- ords (AdvancedOrdering and NumBase) - representing a more complex ordering operation than a basic `Ordering`, when compared to a `NumBase` -- argv (ArgValue) - representing an argument to a function -- funcBasics - a generic regex representation for a general function parser -module Tablebot.Plugins.Roll.Dice (evalInteger, evalList, ListValues (..), defaultRoll, PrettyShow (prettyShow), integerFunctionsList, listFunctionsList, Converter (promote)) where +module Tablebot.Plugins.Roll.Dice (evalProgram, evalInteger, evalList, ListValues (..), defaultRoll, PrettyShow (prettyShow), integerFunctionsList, listFunctionsList, Converter (promote)) where import Tablebot.Plugins.Roll.Dice.DiceData ( Converter (promote), @@ -64,7 +64,7 @@ import Tablebot.Plugins.Roll.Dice.DiceData ListValues (..), NumBase (Value), ) -import Tablebot.Plugins.Roll.Dice.DiceEval (PrettyShow (prettyShow), evalInteger, evalList) +import Tablebot.Plugins.Roll.Dice.DiceEval (PrettyShow (prettyShow), evalInteger, evalList, evalProgram) import Tablebot.Plugins.Roll.Dice.DiceFunctions (integerFunctionsList, listFunctionsList) import Tablebot.Plugins.Roll.Dice.DiceParsing () diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs index 1280e4de..68836fd5 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs @@ -17,6 +17,12 @@ import Data.Text (Text) import Data.Tuple (swap) import Tablebot.Plugins.Roll.Dice.DiceFunctions (FuncInfo, FuncInfoBase) +data Let a = Let Text a | LetLazy Text a + +data Statement = LetExpr (Let Expr) | LetList (Let ListValues) + +data Program = Program [Statement] (Either ListValues Expr) + -- | The value of an argument given to a function. data ArgValue = AVExpr Expr | AVListValues ListValues deriving (Show) @@ -26,7 +32,7 @@ data ListValues = MultipleValues NumBase Base | LVFunc (FuncInfoBase [Integer]) deriving (Show) -- | The type for basic list values (that can be used as is for custom dice). -data ListValuesBase = LVBParen (Paren ListValues) | LVBList [Expr] +data ListValuesBase = LVBParen (Paren ListValues) | LVBList [Expr] | LVBVar Text deriving (Show) -- | The type of the top level expression. Represents one of addition, @@ -59,7 +65,7 @@ newtype Paren a = Paren a deriving (Show) -- | The type representing a numeric base value value or a dice value. -data Base = NBase NumBase | DiceBase Dice +data Base = NBase NumBase | DiceBase Dice | Var Text deriving (Show) -- Dice Operations after this point @@ -165,3 +171,6 @@ instance Converter Dice Base where instance Converter Die Base where promote d = promote $ Dice (promote (1 :: Integer)) d Nothing + +instance Converter [Integer] ListValues where + promote = LVBase . LVBList . (promote <$>) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index dc43d99d..e065d17a 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs @@ -8,13 +8,14 @@ -- -- Functions, type classes, and other utilities to evaluate dice values and -- expressions. -module Tablebot.Plugins.Roll.Dice.DiceEval (PrettyShow (prettyShow), evalList, evalInteger, evaluationException, propagateException) where +module Tablebot.Plugins.Roll.Dice.DiceEval (PrettyShow (prettyShow), evalProgram, evalList, evalInteger, evaluationException, propagateException) where import Control.Monad (when) import Control.Monad.Exception (MonadException) -import Data.List (genericDrop, genericReplicate, genericTake, sortBy) +import Data.List (foldl', genericDrop, genericReplicate, genericTake, sortBy) import Data.List.NonEmpty as NE (NonEmpty ((:|)), head, tail, (<|)) -import Data.Map as M (findWithDefault) +import Data.Map (Map, empty) +import qualified Data.Map as M import Data.Maybe (fromMaybe, isNothing) import Data.String (IsString (fromString)) import Data.Text (Text, intercalate, pack, unpack) @@ -26,25 +27,33 @@ import Tablebot.Utility.Discord (Format (..), formatInput, formatText) import Tablebot.Utility.Exception (BotException (EvaluationException), catchBot, throwBot) import Tablebot.Utility.Random (chooseOne) --- | A wrapper type to differentiate between the RNGCount and other Integers. +-- | A wrapper type to differentiate between the RNGCount and other Integers, +-- as well as store variables throughout the program. -- -- Represents the total number of calls to the RNG throughout the program -- (effectively, how many die rolls have occured). -newtype RNGCount = RNGCount {getRNGCount :: Integer} deriving (Eq, Ord) +data ProgramState = ProgramState + { getRNGCount :: Integer, + getVariables :: Map Text (Either ListValues Expr) + } + deriving (Show) + +addVariable :: ProgramState -> Text -> Either ListValues Expr -> ProgramState +addVariable (ProgramState i vs) t val = ProgramState i (M.insert t val vs) -- | The maximum depth that should be permitted. Used to limit number of dice -- and rerolls. -maximumRNG :: RNGCount -maximumRNG = RNGCount 150 +maximumRNG :: Integer +maximumRNG = 150 -- | Increment the rngcount by 1. -incRNGCount :: RNGCount -> RNGCount -incRNGCount (RNGCount i) = RNGCount (i + 1) +incRNGCount :: ProgramState -> ProgramState +incRNGCount ps = ps {getRNGCount = 1 + getRNGCount ps} -- | Check whether the RNG count has been exceeded by the integer given. -checkRNGCount :: RNGCount -> IO () +checkRNGCount :: ProgramState -> IO () checkRNGCount i = - when (i > maximumRNG) $ throwBot $ EvaluationException ("exceeded maximum rng count (" <> show (getRNGCount maximumRNG) <> ")") [] + when (getRNGCount i > maximumRNG) $ throwBot $ EvaluationException ("exceeded maximum rng count (" <> show maximumRNG <> ")") [] -- | Utility function to throw an `EvaluationException` when using `Text`. evaluationException :: (MonadException m) => Text -> [Text] -> m a @@ -52,18 +61,39 @@ evaluationException nm locs = throwBot $ EvaluationException (unpack nm) (unpack --- Evaluating an expression. Uses IO because dice are random +-- instance IOEval (Program Expr) where +-- evalShow' rngCount (Program ss e) = do +-- (t, ps) <- foldr (\s b -> b >>= \(t, ps) -> evalStatement ps s >>= \(st, ps') -> return (t <> st, ps')) (return ("", rngCount)) ss +-- (i, t', ps') <- evalShow ps e +-- return (i, t <> t', ps') + +-- instance IOEvalList (Program ListValues) where +-- evalShowL' rngCount (Program ss e) = do +-- (t, ps) <- foldr (\s b -> b >>= \(t, ps) -> evalStatement ps s >>= \(st, ps') -> return (t <> st, ps')) (return ("", rngCount)) ss +-- (i, t', ps') <- evalShowL ps e +-- return (i, (t <>) <$> t', ps') + +evalProgram :: Program -> IO (Either [(Integer, Text)] Integer, Text) +evalProgram (Program ss elve) = do + let rngCount = ProgramState 0 empty + (t, ps) <- foldl' (\b s -> b >>= \(t, ps) -> evalStatement ps s >>= \(st, ps') -> return (t <> st, ps')) (return ("", rngCount)) ss + r <- either ((Left <$>) . evalShowL ps) ((Right <$>) . evalShow ps) elve + case r of + Left (is, mt, _) -> return (Left is, t <> fromMaybe (prettyShow elve) mt) + Right (is, mt, _) -> return (Right is, t <> mt) + -- | Given a list expression, evaluate it, getting the pretty printed string and -- the value of the result. evalList :: (IOEvalList a, PrettyShow a) => a -> IO ([(Integer, Text)], Text) evalList a = do - (is, ss, _) <- evalShowL (RNGCount 0) a + (is, ss, _) <- evalShowL (ProgramState 0 empty) a return (is, fromMaybe (prettyShow a) ss) -- | Given an integer expression, evaluate it, getting the pretty printed string -- and the value of the result. evalInteger :: (IOEval a, PrettyShow a) => a -> IO (Integer, Text) evalInteger a = do - (is, ss, _) <- evalShow (RNGCount 0) a + (is, ss, _) <- evalShow (ProgramState 0 empty) a return (is, ss) -- | Utility function to display dice. @@ -97,19 +127,19 @@ dieShow lchc d ls = return $ prettyShow d <> " [" <> intercalate ", " adjustList -- | Evaluate a series of values, combining the text output into a comma -- separated list. -evalShowList :: (IOEval a, PrettyShow a) => RNGCount -> [a] -> IO ([Integer], Text, RNGCount) +evalShowList :: (IOEval a, PrettyShow a) => ProgramState -> [a] -> IO ([Integer], Text, ProgramState) evalShowList rngCount as = do (vs, rngCount') <- evalShowList' rngCount as let (is, ts) = unzip vs return (is, intercalate ", " ts, rngCount') -- | Evaluate a series of values, combining the text output a list. -evalShowList' :: (IOEval a, PrettyShow a) => RNGCount -> [a] -> IO ([(Integer, Text)], RNGCount) +evalShowList' :: (IOEval a, PrettyShow a) => ProgramState -> [a] -> IO ([(Integer, Text)], ProgramState) evalShowList' = evalShowList'' evalShow -- | Evaluate (using a custom evaluator function) a series of values, getting -- strings and values as a result. -evalShowList'' :: (RNGCount -> a -> IO (i, Text, RNGCount)) -> RNGCount -> [a] -> IO ([(i, Text)], RNGCount) +evalShowList'' :: (ProgramState -> a -> IO (i, Text, ProgramState)) -> ProgramState -> [a] -> IO ([(i, Text)], ProgramState) evalShowList'' customEvalShow rngCount = foldr foldF (return ([], rngCount)) where foldF a sumrngcount = do @@ -135,12 +165,12 @@ class IOEvalList a where -- it took. If the `a` value is a dice value, the values of the dice should be -- displayed. This function adds the current location to the exception -- callstack. - evalShowL :: PrettyShow a => RNGCount -> a -> IO ([(Integer, Text)], Maybe Text, RNGCount) + evalShowL :: PrettyShow a => ProgramState -> a -> IO ([(Integer, Text)], Maybe Text, ProgramState) evalShowL rngCount a = propagateException (prettyShow a) (evalShowL' rngCount a) - evalShowL' :: PrettyShow a => RNGCount -> a -> IO ([(Integer, Text)], Maybe Text, RNGCount) + evalShowL' :: PrettyShow a => ProgramState -> a -> IO ([(Integer, Text)], Maybe Text, ProgramState) -evalArgValue :: RNGCount -> ArgValue -> IO (ListInteger, RNGCount) +evalArgValue :: ProgramState -> ArgValue -> IO (ListInteger, ProgramState) evalArgValue rngCount (AVExpr e) = do (i, _, rngCount') <- evalShow rngCount e return (LIInteger i, rngCount') @@ -161,6 +191,9 @@ instance IOEvalList ListValuesBase where (vs, rc) <- evalShowList' rngCount es return (vs, Nothing, rc) evalShowL' rngCount (LVBParen (Paren lv)) = evalShowL rngCount lv + evalShowL' rngCount (LVBVar t) = case M.lookup t (getVariables rngCount) of + Just (Left e) -> evalShowL rngCount e >>= \(i, _, rngCount') -> return (i, Just t, rngCount') + _ -> evaluationException ("could not find list variable `" <> t <> "`") [] -- | This type class gives a function which evaluates the value to an integer -- and a string. @@ -169,14 +202,17 @@ class IOEval a where -- value, and the number of RNG calls it took. If the `a` value is a dice -- value, the values of the dice should be displayed. This function adds -- the current location to the exception callstack. - evalShow :: PrettyShow a => RNGCount -> a -> IO (Integer, Text, RNGCount) + evalShow :: PrettyShow a => ProgramState -> a -> IO (Integer, Text, ProgramState) evalShow rngCount a = propagateException (prettyShow a) (evalShow' rngCount a) - evalShow' :: PrettyShow a => RNGCount -> a -> IO (Integer, Text, RNGCount) + evalShow' :: PrettyShow a => ProgramState -> a -> IO (Integer, Text, ProgramState) instance IOEval Base where evalShow' rngCount (NBase nb) = evalShow rngCount nb evalShow' rngCount (DiceBase dice) = evalShow rngCount dice + evalShow' rngCount (Var t) = case M.lookup t (getVariables rngCount) of + Just (Right e) -> evalShow rngCount e >>= \(i, _, rngCount') -> return (i, t, rngCount') + _ -> evaluationException ("could not find integer variable `" <> t <> "`") [] instance IOEval Die where evalShow' rngCount ld@(LazyDie d) = do @@ -226,11 +262,11 @@ fromEvalDieOpList = foldr foldF [] -- -- The function itself checks to make sure the number of dice being rolled is -- less than the maximum recursion and is non-negative. -evalDieOp :: RNGCount -> Dice -> IO ([(NonEmpty Integer, Bool)], Maybe (Integer, Integer), RNGCount) +evalDieOp :: ProgramState -> Dice -> IO ([(NonEmpty Integer, Bool)], Maybe (Integer, Integer), ProgramState) evalDieOp rngCount (Dice b ds dopo) = do (nbDice, _, rngCountB) <- evalShow rngCount b - if RNGCount nbDice > maximumRNG - then evaluationException ("tried to roll more than " <> formatInput Code (getRNGCount maximumRNG) <> " dice: " <> formatInput Code nbDice) [prettyShow b] + if nbDice > maximumRNG + then evaluationException ("tried to roll more than " <> formatInput Code maximumRNG <> " dice: " <> formatInput Code nbDice) [prettyShow b] else do if nbDice < 0 then evaluationException ("tried to give a negative value to the number of dice: " <> formatInput Code nbDice) [prettyShow b] @@ -254,7 +290,7 @@ evalDieOp rngCount (Dice b ds dopo) = do -- | Utility function that processes a `Maybe DieOpRecur`, when given a die, and -- dice that have already been processed. -evalDieOp' :: RNGCount -> Maybe DieOpRecur -> Die -> [(NonEmpty Integer, Bool)] -> IO ([(NonEmpty Integer, Bool)], RNGCount) +evalDieOp' :: ProgramState -> Maybe DieOpRecur -> Die -> [(NonEmpty Integer, Bool)] -> IO ([(NonEmpty Integer, Bool)], ProgramState) evalDieOp' rngCount Nothing _ is = return (is, rngCount) evalDieOp' rngCount (Just (DieOpRecur doo mdor)) die is = do (doo', rngCount') <- processDOO rngCount doo @@ -280,7 +316,7 @@ evalDieOp' rngCount (Just (DieOpRecur doo mdor)) die is = do -- | Utility function that processes a `DieOpOption`, when given a die, and dice -- that have already been processed. -evalDieOp'' :: RNGCount -> DieOpOption -> Die -> [(NonEmpty Integer, Bool)] -> IO ([(NonEmpty Integer, Bool)], RNGCount) +evalDieOp'' :: ProgramState -> DieOpOption -> Die -> [(NonEmpty Integer, Bool)] -> IO ([(NonEmpty Integer, Bool)], ProgramState) evalDieOp'' rngCount (DieOpOptionLazy doo) die is = evalDieOp'' rngCount doo die is evalDieOp'' rngCount (DieOpOptionKD kd lhw) _ is = evalDieOpHelpKD rngCount kd lhw is evalDieOp'' rngCount (Reroll once o i) die is = foldr rerollF (return ([], rngCount)) is @@ -310,7 +346,7 @@ setToDropped :: [(NonEmpty Integer, Bool)] -> [(NonEmpty Integer, Bool)] setToDropped = fmap (\(is, _) -> (is, False)) -- | Helper function that executes the keep/drop commands on dice. -evalDieOpHelpKD :: RNGCount -> KeepDrop -> LowHighWhere -> [(NonEmpty Integer, Bool)] -> IO ([(NonEmpty Integer, Bool)], RNGCount) +evalDieOpHelpKD :: ProgramState -> KeepDrop -> LowHighWhere -> [(NonEmpty Integer, Bool)] -> IO ([(NonEmpty Integer, Bool)], ProgramState) evalDieOpHelpKD rngCount kd (Where cmp i) is = foldr foldF (return ([], rngCount)) is where isKeep = if kd == Keep then id else not @@ -333,7 +369,7 @@ evalDieOpHelpKD rngCount kd lh is = do -- Was previously its own type class that wouldn't work for evaluating Base values. -- | Utility function to evaluate a binary operator. -binOpHelp :: (IOEval a, IOEval b, PrettyShow a, PrettyShow b) => RNGCount -> a -> b -> Text -> (Integer -> Integer -> Integer) -> IO (Integer, Text, RNGCount) +binOpHelp :: (IOEval a, IOEval b, PrettyShow a, PrettyShow b) => ProgramState -> a -> b -> Text -> (Integer -> Integer -> Integer) -> IO (Integer, Text, ProgramState) binOpHelp rngCount a b opS op = do (a', a's, rngCount') <- evalShow rngCount a (b', b's, rngCount'') <- evalShow rngCount' b @@ -359,7 +395,7 @@ instance IOEval Func where evalShow' rngCount (NoFunc b) = evalShow rngCount b -- | Evaluate a function when given a list of parameters -evaluateFunction :: RNGCount -> FuncInfoBase j -> [ArgValue] -> IO (j, Text, RNGCount) +evaluateFunction :: ProgramState -> FuncInfoBase j -> [ArgValue] -> IO (j, Text, ProgramState) evaluateFunction rngCount fi exprs = do (exprs', rngCount') <- evalShowList'' (\r a -> evalArgValue r a >>= \(i, r') -> return (i, "", r')) rngCount exprs f <- funcInfoFunc fi (fst <$> exprs') @@ -387,6 +423,28 @@ instance IOEval NumBase where return (r, "(" <> s <> ")", rngCount') evalShow' rngCount (Value i) = return (i, pack (show i), rngCount) +instance IOEval (Let Expr) where + evalShow' rngCount (Let t a) = do + (v, lt, rngCount') <- evalShow rngCount a + return (v, "let " <> t <> " = " <> lt, addVariable rngCount' t (Right $ promote v)) + evalShow' rngCount l@(LetLazy t a) = do + (v, _, rngCount') <- evalShow rngCount a + return (v, prettyShow l, addVariable rngCount' t (Right a)) + +instance IOEvalList (Let ListValues) where + evalShowL' rngCount l@(Let t a) = do + (v, _, rngCount') <- evalShowL rngCount a + return (v, Just (prettyShow l), addVariable rngCount' t (Left $ promote $ fst <$> v)) + evalShowL' rngCount l@(LetLazy t a) = do + (v, _, rngCount') <- evalShowL rngCount a + return (v, Just (prettyShow l), addVariable rngCount' t (Left a)) + +evalStatement :: ProgramState -> Statement -> IO (Text, ProgramState) +evalStatement ps (LetExpr l) = evalShow ps l >>= \(_, t, ps') -> return (t <> "; ", ps') +evalStatement ps (LetList l) = evalShowL ps l >>= \(_, t, ps') -> return (fromMaybe (prettyShow l) t <> "; ", ps') + +-- instance IOEval (Program Expr) where + --- Pretty printing the AST -- The output from this should be parseable @@ -407,6 +465,7 @@ instance PrettyShow ListValues where instance PrettyShow ListValuesBase where prettyShow (LVBList es) = "{" <> intercalate ", " (prettyShow <$> es) <> "}" prettyShow (LVBParen p) = prettyShow p + prettyShow (LVBVar t) = t instance PrettyShow Expr where prettyShow (Add t e) = prettyShow t <> " + " <> prettyShow e @@ -440,6 +499,7 @@ instance (PrettyShow a) => PrettyShow (Paren a) where instance PrettyShow Base where prettyShow (NBase nb) = prettyShow nb prettyShow (DiceBase dop) = prettyShow dop + prettyShow (Var t) = t instance PrettyShow Die where prettyShow (Die b) = "d" <> prettyShow b @@ -465,3 +525,14 @@ instance PrettyShow Dice where instance (PrettyShow a, PrettyShow b) => PrettyShow (Either a b) where prettyShow (Left a) = prettyShow a prettyShow (Right b) = prettyShow b + +instance (PrettyShow a) => PrettyShow (Let a) where + prettyShow (Let t a) = "let " <> t <> " = " <> prettyShow a + prettyShow (LetLazy t a) = "let !" <> t <> " = " <> prettyShow a + +instance PrettyShow Statement where + prettyShow (LetExpr l) = prettyShow l <> "; " + prettyShow (LetList l) = prettyShow l <> "; " + +instance PrettyShow Program where + prettyShow (Program ss a) = foldr ((<>) . prettyShow) (prettyShow a) ss diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index fa4366b5..1347f829 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE LiberalTypeSynonyms #-} {-# OPTIONS_GHC -Wno-orphans #-} -- | @@ -28,7 +29,7 @@ import Tablebot.Plugins.Roll.Dice.DiceFunctions import Tablebot.Utility.Parser (integer, parseCommaSeparated1, skipSpace) import Tablebot.Utility.SmartParser (CanParse (..)) import Tablebot.Utility.Types (Parser) -import Text.Megaparsec (MonadParsec (observing, try), choice, failure, optional, (), (<|>)) +import Text.Megaparsec (MonadParsec (observing, try), choice, failure, optional, some, (), (<|>)) import Text.Megaparsec.Char (char, string) import Text.Megaparsec.Error (ErrorItem (Tokens)) @@ -43,16 +44,42 @@ failure' s ss = failure (Just $ Tokens $ NE.fromList $ T.unpack s) (S.map (Token Left _ -> fail s Right a -> return a +varName :: Parser T.Text +varName = T.pack <$> some (choice $ char <$> '_' : ['a' .. 'z']) + +-- instance CanParse a => CanParse (Let a) where +parseLet :: Parser (a -> Let a) +parseLet = do + _ <- try (string "let") <* skipSpace + letCon <- try (char '!' $> LetLazy) <|> return Let + varName' <- varName + _ <- skipSpace >> char '=' >> skipSpace + return $ letCon varName' + +instance CanParse Statement where + pars = + ((LetList <$> (try (parseLet <*> pars) >>= \l@(Let t _) -> if T.isPrefixOf "l_" t then return l else fail "list variables must be prepended with l_")) <|> LetExpr <$> (parseLet <*> pars)) <* skipSpace <* char ';' <* skipSpace + +-- do +-- letP <- parseLet :: Parser (forall a. a -> Let a) +-- val <- (Left <$> pars <|> Right <$> pars) <* skipSpace <* char ';' <* skipSpace +-- return $ either (LetList . letP) (LetExpr . letP) val + +instance CanParse Program where + pars = pars >>= \ss -> Program ss <$> pars + instance CanParse ListValues where pars = do - LVBase <$> pars - <|> functionParser listFunctions LVFunc - <|> ( do - nb <- pars - _ <- char '#' - MultipleValues nb <$> pars - ) + functionParser listFunctions LVFunc + <|> LVBase <$> pars + <|> (try (pars <* char '#') >>= \nb -> MultipleValues nb <$> pars) + +-- ( do +-- nb <- pars +-- _ <- char '#' +-- MultipleValues nb <$> pars +-- ) instance CanParse ListValuesBase where pars = do @@ -64,6 +91,7 @@ instance CanParse ListValuesBase where ) <|> LVBParen . unnest <$> pars + <|> (LVBVar . ("l_" <>) <$> try (string "l_" *> varName)) where unnest (Paren (LVBase (LVBParen e))) = e unnest e = e @@ -129,6 +157,7 @@ instance CanParse Base where -- <|> return (NBase nb) ) <|> DiceBase <$> parseDice (Value 1) + <|> (Var <$> try varName) instance CanParse Die where pars = do diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index eb03e1c8..bc0c0fda 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -10,7 +10,6 @@ module Tablebot.Plugins.Roll.Plugin (rollPlugin) where import Control.Monad.Writer (MonadIO (liftIO), void) -import Data.Bifunctor (Bifunctor (first)) import Data.ByteString.Lazy (toStrict) import Data.Distribution (isValid) import Data.Maybe (fromMaybe) @@ -34,12 +33,10 @@ import Text.RawString.QQ (r) -- | The basic execution function for rolling dice. Both the expression and message are -- optional. If the expression is not given, then the default roll is used. -rollDice' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> Message -> DatabaseDiscord () +rollDice' :: Maybe Program -> Maybe (Quoted Text) -> Message -> DatabaseDiscord () rollDice' e' t m = do - let e = fromMaybe (Right defaultRoll) e' - (vs, ss) <- case e of - (Left a) -> liftIO $ first Left <$> evalList a - (Right b) -> liftIO $ first Right <$> evalInteger b + let e = fromMaybe (Program [] (Right defaultRoll)) e' + (vs, ss) <- liftIO $ evalProgram e let msg = makeMsg vs ss if countFormatting msg < 199 then sendMessage m msg @@ -62,11 +59,11 @@ rollDice' e' t m = do rollDiceParser :: Parser (Message -> DatabaseDiscord ()) rollDiceParser = choice (try <$> options) where - justEither :: WithError "Incorrect expression/list value. Please check the expression" (Either ListValues Expr) -> Message -> DatabaseDiscord () + justEither :: WithError "Incorrect expression/list value. Please check the expression" Program -> Message -> DatabaseDiscord () justEither (WErr x) = rollDice' (Just x) Nothing nothingAtAll :: WithError "Expected eof" () -> Message -> DatabaseDiscord () nothingAtAll (WErr _) = rollDice' Nothing Nothing - bothVals :: WithError "Incorrect format. Please check the expression and quote" (Either ListValues Expr, Quoted Text) -> Message -> DatabaseDiscord () + bothVals :: WithError "Incorrect format. Please check the expression and quote" (Program, Quoted Text) -> Message -> DatabaseDiscord () bothVals (WErr (x, y)) = rollDice' (Just x) (Just y) justText :: WithError "Incorrect quote. Please check the quote format" (Quoted Text) -> Message -> DatabaseDiscord () justText (WErr x) = rollDice' Nothing (Just x) @@ -130,7 +127,7 @@ To see a full list of uses and options, please go to rpgSystems') where - doDiceRoll (nm, lv) = (nm, parseComm $ rollDice' (Just (Left lv)) (Just (Qu ("genchar for " <> nm)))) + doDiceRoll (nm, lv) = (nm, parseComm $ rollDice' (Just (Program [] (Left lv))) (Just (Qu ("genchar for " <> nm)))) rpgSystems' = doDiceRoll <$> rpgSystems toCommand (nm, ps) = Command nm ps [] From f54d279ceb412f257aeb3b8216d36019afc7fe62 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 2 Feb 2022 16:55:43 +0000 Subject: [PATCH 33/96] tinkered with let statements a bit more --- src/Tablebot/Plugins/Roll/Dice/DiceData.hs | 8 ++--- src/Tablebot/Plugins/Roll/Dice/DiceEval.hs | 23 +++++++++----- .../Plugins/Roll/Dice/DiceFunctions.hs | 3 +- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 31 ++++++++++++++----- src/Tablebot/Plugins/Roll/Plugin.hs | 15 ++++++--- 5 files changed, 56 insertions(+), 24 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs index 68836fd5..4de9a4cd 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs @@ -17,11 +17,11 @@ import Data.Text (Text) import Data.Tuple (swap) import Tablebot.Plugins.Roll.Dice.DiceFunctions (FuncInfo, FuncInfoBase) -data Let a = Let Text a | LetLazy Text a +data Let a = Let Text a | LetLazy Text a deriving (Show) -data Statement = LetExpr (Let Expr) | LetList (Let ListValues) +data Statement = LetExpr (Let Expr) | LetList (Let ListValues) deriving (Show) -data Program = Program [Statement] (Either ListValues Expr) +data Program = Program [Statement] (Either ListValues Expr) deriving (Show) -- | The value of an argument given to a function. data ArgValue = AVExpr Expr | AVListValues ListValues @@ -37,7 +37,7 @@ data ListValuesBase = LVBParen (Paren ListValues) | LVBList [Expr] | LVBVar Text -- | The type of the top level expression. Represents one of addition, -- subtraction, or a single term. -data Expr = Add Term Expr | Sub Term Expr | NoExpr Term +data Expr = ExprLet (Let Expr) | Add Term Expr | Sub Term Expr | NoExpr Term deriving (Show) -- | The type representing multiplication, division, or a single negated term. diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index e065d17a..9248e935 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs @@ -20,6 +20,7 @@ import Data.Maybe (fromMaybe, isNothing) import Data.String (IsString (fromString)) import Data.Text (Text, intercalate, pack, unpack) import qualified Data.Text as T +import Debug.Trace (trace) import System.Random (randomRIO) import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceFunctions (FuncInfoBase (..), ListInteger (..)) @@ -38,6 +39,7 @@ data ProgramState = ProgramState } deriving (Show) +-- | Add the given variable to the `ProgramState` addVariable :: ProgramState -> Text -> Either ListValues Expr -> ProgramState addVariable (ProgramState i vs) t val = ProgramState i (M.insert t val vs) @@ -73,6 +75,7 @@ evaluationException nm locs = throwBot $ EvaluationException (unpack nm) (unpack -- (i, t', ps') <- evalShowL ps e -- return (i, (t <>) <$> t', ps') +-- | Evaluating a full program evalProgram :: Program -> IO (Either [(Integer, Text)] Integer, Text) evalProgram (Program ss elve) = do let rngCount = ProgramState 0 empty @@ -140,7 +143,7 @@ evalShowList' = evalShowList'' evalShow -- | Evaluate (using a custom evaluator function) a series of values, getting -- strings and values as a result. evalShowList'' :: (ProgramState -> a -> IO (i, Text, ProgramState)) -> ProgramState -> [a] -> IO ([(i, Text)], ProgramState) -evalShowList'' customEvalShow rngCount = foldr foldF (return ([], rngCount)) +evalShowList'' customEvalShow rngCount as = foldl' (flip foldF) (return ([], rngCount)) as >>= \(lst, ps) -> return (reverse lst, ps) where foldF a sumrngcount = do (diceSoFar, rngCountTotal) <- sumrngcount @@ -377,6 +380,7 @@ binOpHelp rngCount a b opS op = do instance IOEval Expr where evalShow' rngCount (NoExpr t) = evalShow rngCount t + evalShow' rngCount (ExprLet e) = evalShow rngCount e evalShow' rngCount (Add t e) = binOpHelp rngCount t e "+" (+) evalShow' rngCount (Sub t e) = binOpHelp rngCount t e "-" (-) @@ -429,7 +433,7 @@ instance IOEval (Let Expr) where return (v, "let " <> t <> " = " <> lt, addVariable rngCount' t (Right $ promote v)) evalShow' rngCount l@(LetLazy t a) = do (v, _, rngCount') <- evalShow rngCount a - return (v, prettyShow l, addVariable rngCount' t (Right a)) + return $ v `seq` (v, prettyShow l, addVariable rngCount' t (Right a)) instance IOEvalList (Let ListValues) where evalShowL' rngCount l@(Let t a) = do @@ -440,10 +444,14 @@ instance IOEvalList (Let ListValues) where return (v, Just (prettyShow l), addVariable rngCount' t (Left a)) evalStatement :: ProgramState -> Statement -> IO (Text, ProgramState) -evalStatement ps (LetExpr l) = evalShow ps l >>= \(_, t, ps') -> return (t <> "; ", ps') -evalStatement ps (LetList l) = evalShowL ps l >>= \(_, t, ps') -> return (fromMaybe (prettyShow l) t <> "; ", ps') - --- instance IOEval (Program Expr) where +evalStatement ps (LetExpr l) = evalShowStatement l >>= \(_, t, ps') -> return (t <> "; ", ps') + where + evalShowStatement l'@(Let _ _) = evalShow ps l' + evalShowStatement l'@(LetLazy t a) = return (0, prettyShow l', addVariable ps t (Right a)) +evalStatement ps (LetList l) = evalShowStatement l >>= \(_, t, ps') -> return (fromMaybe (prettyShow l) t <> "; ", ps') + where + evalShowStatement l'@(Let _ _) = evalShowL ps l' + evalShowStatement l'@(LetLazy t a) = return ([], Just (prettyShow l'), addVariable ps t (Left a)) --- Pretty printing the AST -- The output from this should be parseable @@ -471,6 +479,7 @@ instance PrettyShow Expr where prettyShow (Add t e) = prettyShow t <> " + " <> prettyShow e prettyShow (Sub t e) = prettyShow t <> " - " <> prettyShow e prettyShow (NoExpr t) = prettyShow t + prettyShow (ExprLet e) = prettyShow e instance PrettyShow Term where prettyShow (Multi f t) = prettyShow f <> " * " <> prettyShow t @@ -478,7 +487,7 @@ instance PrettyShow Term where prettyShow (NoTerm f) = prettyShow f instance PrettyShow Func where - prettyShow (Func s n) = funcInfoName s <> "(" <> intercalate "," (prettyShow <$> n) <> ")" + prettyShow (Func s n) = funcInfoName s <> "(" <> intercalate ", " (prettyShow <$> n) <> ")" prettyShow (NoFunc b) = prettyShow b instance PrettyShow Negation where diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs index 58bba1f9..7ef5419c 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs @@ -25,6 +25,7 @@ import Data.List (genericDrop, genericLength, genericTake, sort) import Data.Map as M (Map, fromList, keys) import Data.Maybe (fromJust) import Data.Text (Text, unpack) +import Debug.Trace (trace) import Tablebot.Utility.Exception (BotException (EvaluationException), throwBot) -- | The limit to how big a factorial value is permitted. Notably, the factorial @@ -90,7 +91,7 @@ funcInfoIndex = FuncInfo "index" [ATInteger, ATIntegerList] ATInteger fiIndex fiIndex (LIInteger i : [LIList is]) | i < 0 || i >= genericLength is = throwBot $ EvaluationException ("index out of range: " ++ show i) [] | otherwise = return (is !! fromInteger i) - fiIndex is = throwBot $ EvaluationException ("incorrect number of arguments. expected 2, got " ++ show (length is)) [] + fiIndex is = trace (show is) throwBot $ EvaluationException ("incorrect number of arguments. expected 2, got " ++ show (length is)) [] -- | A data structure to contain the information about a given function, -- including types, the function name, and the function itself. diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index 1347f829..dc288fa1 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -60,10 +60,26 @@ instance CanParse Statement where pars = ((LetList <$> (try (parseLet <*> pars) >>= \l@(Let t _) -> if T.isPrefixOf "l_" t then return l else fail "list variables must be prepended with l_")) <|> LetExpr <$> (parseLet <*> pars)) <* skipSpace <* char ';' <* skipSpace --- do --- letP <- parseLet :: Parser (forall a. a -> Let a) --- val <- (Left <$> pars <|> Right <$> pars) <* skipSpace <* char ';' <* skipSpace --- return $ either (LetList . letP) (LetExpr . letP) val +{- +-- alternative method to the above. +-- from https://canary.discord.com/channels/280033776820813825/280036215477239809/938154455612919838 +-- - Morrow#1157 +newtype LetCon = LetCon (forall a. a -> Let a) + +parseLet :: Parser LetCon +parseLet = do + _ <- try (string "let") <* skipSpace + lazy <- try (char '!' $> True) <|> return False + varName' <- varName + _ <- skipSpace >> char '=' >> skipSpace + return $ LetCon (\a -> if lazy then LetLazy varName' a else Let varName' a) + +instance CanParse Statement where + pars = do + LetCon letP <- parseLet + val <- (Left <$> pars <|> Right <$> pars) <* skipSpace <* char ';' <* skipSpace + return $ either (LetList . letP) (LetExpr . letP) val +-} instance CanParse Program where pars = pars >>= \ss -> Program ss <$> pars @@ -101,9 +117,10 @@ binOpParseHelp :: (CanParse a) => Char -> (a -> a) -> Parser a binOpParseHelp c con = try (skipSpace *> char c) *> skipSpace *> (con <$> pars) instance CanParse Expr where - pars = do - t <- pars - binOpParseHelp '+' (Add t) <|> binOpParseHelp '-' (Sub t) <|> (return . NoExpr) t + pars = + (ExprLet <$> (parseLet <*> pars)) <|> do + t <- pars + binOpParseHelp '+' (Add t) <|> binOpParseHelp '-' (Sub t) <|> (return . NoExpr) t instance CanParse Term where pars = do diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index bc0c0fda..7086707b 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -21,6 +21,7 @@ import Discord.Types (Message (messageAuthor, messageChannel)) import System.Timeout (timeout) import Tablebot.Plugins.Roll.Dice import Tablebot.Plugins.Roll.Dice.DiceData +import Tablebot.Plugins.Roll.Dice.DiceEval (evaluationException) import Tablebot.Plugins.Roll.Dice.DiceStats (getStats, rangeExpr) import Tablebot.Plugins.Roll.Dice.DiceStatsBase (distributionByteString) import Tablebot.Utility @@ -36,11 +37,15 @@ import Text.RawString.QQ (r) rollDice' :: Maybe Program -> Maybe (Quoted Text) -> Message -> DatabaseDiscord () rollDice' e' t m = do let e = fromMaybe (Program [] (Right defaultRoll)) e' - (vs, ss) <- liftIO $ evalProgram e - let msg = makeMsg vs ss - if countFormatting msg < 199 - then sendMessage m msg - else sendMessage m (makeMsg (simplify vs) (prettyShow e <> " `[could not display rolls]`")) + liftIO $ putStrLn (unpack $ prettyShow e) + maybemsss <- liftIO $ timeout 1000000 $ evalProgram e + case maybemsss of + Nothing -> evaluationException "Could not process expression in one second" [] + Just (vs, ss) -> do + let msg = makeMsg vs ss + if countFormatting msg < 199 + then sendMessage m msg + else sendMessage m (makeMsg (simplify vs) (prettyShow e <> " `[could not display rolls]`")) where dsc = maybe ": " (\(Qu t') -> " \"" <> t' <> "\": ") t baseMsg = toMention (messageAuthor m) <> " rolled" <> dsc From e3411c1155dcbcc664c3c93f5d3b8aa095df0366 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 2 Feb 2022 17:13:47 +0000 Subject: [PATCH 34/96] tinkered with let statements a even more - broadening statements as a whole --- src/Tablebot/Plugins/Roll/Dice/DiceData.hs | 10 ++++--- src/Tablebot/Plugins/Roll/Dice/DiceEval.hs | 27 ++++++++++--------- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 27 ++++++++++--------- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs index 4de9a4cd..17af7852 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs @@ -17,9 +17,11 @@ import Data.Text (Text) import Data.Tuple (swap) import Tablebot.Plugins.Roll.Dice.DiceFunctions (FuncInfo, FuncInfoBase) -data Let a = Let Text a | LetLazy Text a deriving (Show) +data Let a = Let {letName :: Text, letValue :: a} | LetLazy {letName :: Text, letValue :: a} deriving (Show) -data Statement = LetExpr (Let Expr) | LetList (Let ListValues) deriving (Show) +-- data Statement = LetExpr (Let Expr) | LetList (Let ListValues) deriving (Show) + +data Statement = StatementExpr Expr | StatementListValues ListValues deriving (Show) data Program = Program [Statement] (Either ListValues Expr) deriving (Show) @@ -28,11 +30,11 @@ data ArgValue = AVExpr Expr | AVListValues ListValues deriving (Show) -- | The type for list values. -data ListValues = MultipleValues NumBase Base | LVFunc (FuncInfoBase [Integer]) [ArgValue] | LVBase ListValuesBase +data ListValues = MultipleValues NumBase Base | LVFunc (FuncInfoBase [Integer]) [ArgValue] | LVVar Text | LVLet (Let ListValues) | LVBase ListValuesBase deriving (Show) -- | The type for basic list values (that can be used as is for custom dice). -data ListValuesBase = LVBParen (Paren ListValues) | LVBList [Expr] | LVBVar Text +data ListValuesBase = LVBParen (Paren ListValues) | LVBList [Expr] deriving (Show) -- | The type of the top level expression. Represents one of addition, diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index 9248e935..b7b7ca3f 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs @@ -20,7 +20,6 @@ import Data.Maybe (fromMaybe, isNothing) import Data.String (IsString (fromString)) import Data.Text (Text, intercalate, pack, unpack) import qualified Data.Text as T -import Debug.Trace (trace) import System.Random (randomRIO) import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceFunctions (FuncInfoBase (..), ListInteger (..)) @@ -188,15 +187,16 @@ instance IOEvalList ListValues where return (vs, Nothing, rc) evalShowL' rngCount (LVFunc fi exprs) = evaluateFunction rngCount fi exprs >>= \(i, s, rc) -> return ((,"") <$> i, Just s, rc) evalShowL' rngCount (LVBase lvb) = evalShowL rngCount lvb + evalShowL' rngCount (LVVar t) = case M.lookup t (getVariables rngCount) of + Just (Left e) -> evalShowL rngCount e >>= \(i, _, rngCount') -> return (i, Just t, rngCount') + _ -> evaluationException ("could not find list variable `" <> t <> "`") [] + evalShowL' rngCount (LVLet l) = evalShowL rngCount l instance IOEvalList ListValuesBase where evalShowL' rngCount (LVBList es) = do (vs, rc) <- evalShowList' rngCount es return (vs, Nothing, rc) evalShowL' rngCount (LVBParen (Paren lv)) = evalShowL rngCount lv - evalShowL' rngCount (LVBVar t) = case M.lookup t (getVariables rngCount) of - Just (Left e) -> evalShowL rngCount e >>= \(i, _, rngCount') -> return (i, Just t, rngCount') - _ -> evaluationException ("could not find list variable `" <> t <> "`") [] -- | This type class gives a function which evaluates the value to an integer -- and a string. @@ -444,14 +444,14 @@ instance IOEvalList (Let ListValues) where return (v, Just (prettyShow l), addVariable rngCount' t (Left a)) evalStatement :: ProgramState -> Statement -> IO (Text, ProgramState) -evalStatement ps (LetExpr l) = evalShowStatement l >>= \(_, t, ps') -> return (t <> "; ", ps') +evalStatement ps (StatementExpr l) = evalShowStatement l >>= \(_, t, ps') -> return (t <> "; ", ps') where - evalShowStatement l'@(Let _ _) = evalShow ps l' - evalShowStatement l'@(LetLazy t a) = return (0, prettyShow l', addVariable ps t (Right a)) -evalStatement ps (LetList l) = evalShowStatement l >>= \(_, t, ps') -> return (fromMaybe (prettyShow l) t <> "; ", ps') + evalShowStatement (ExprLet l'@(LetLazy t a)) = return (0, prettyShow l', addVariable ps t (Right a)) + evalShowStatement l' = evalShow ps l' +evalStatement ps (StatementListValues l) = evalShowStatement l >>= \(_, t, ps') -> return (fromMaybe (prettyShow l) t <> "; ", ps') where - evalShowStatement l'@(Let _ _) = evalShowL ps l' - evalShowStatement l'@(LetLazy t a) = return ([], Just (prettyShow l'), addVariable ps t (Left a)) + evalShowStatement (LVLet l'@(LetLazy t a)) = return ([], Just (prettyShow l'), addVariable ps t (Left a)) + evalShowStatement l' = evalShowL ps l' --- Pretty printing the AST -- The output from this should be parseable @@ -469,11 +469,12 @@ instance PrettyShow ListValues where prettyShow (LVBase e) = prettyShow e prettyShow (MultipleValues nb b) = prettyShow nb <> "#" <> prettyShow b prettyShow (LVFunc s n) = funcInfoName s <> "(" <> intercalate "," (prettyShow <$> n) <> ")" + prettyShow (LVVar t) = t + prettyShow (LVLet l) = prettyShow l instance PrettyShow ListValuesBase where prettyShow (LVBList es) = "{" <> intercalate ", " (prettyShow <$> es) <> "}" prettyShow (LVBParen p) = prettyShow p - prettyShow (LVBVar t) = t instance PrettyShow Expr where prettyShow (Add t e) = prettyShow t <> " + " <> prettyShow e @@ -540,8 +541,8 @@ instance (PrettyShow a) => PrettyShow (Let a) where prettyShow (LetLazy t a) = "let !" <> t <> " = " <> prettyShow a instance PrettyShow Statement where - prettyShow (LetExpr l) = prettyShow l <> "; " - prettyShow (LetList l) = prettyShow l <> "; " + prettyShow (StatementExpr l) = prettyShow l <> "; " + prettyShow (StatementListValues l) = prettyShow l <> "; " instance PrettyShow Program where prettyShow (Program ss a) = foldr ((<>) . prettyShow) (prettyShow a) ss diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index dc288fa1..57a14c62 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -47,18 +47,16 @@ failure' s ss = failure (Just $ Tokens $ NE.fromList $ T.unpack s) (S.map (Token varName :: Parser T.Text varName = T.pack <$> some (choice $ char <$> '_' : ['a' .. 'z']) --- instance CanParse a => CanParse (Let a) where -parseLet :: Parser (a -> Let a) -parseLet = do - _ <- try (string "let") <* skipSpace - letCon <- try (char '!' $> LetLazy) <|> return Let - varName' <- varName - _ <- skipSpace >> char '=' >> skipSpace - return $ letCon varName' +instance CanParse a => CanParse (Let a) where + pars = do + _ <- try (string "let") <* skipSpace + letCon <- try (char '!' $> LetLazy) <|> return Let + varName' <- varName + _ <- skipSpace >> char '=' >> skipSpace + letCon varName' <$> pars instance CanParse Statement where - pars = - ((LetList <$> (try (parseLet <*> pars) >>= \l@(Let t _) -> if T.isPrefixOf "l_" t then return l else fail "list variables must be prepended with l_")) <|> LetExpr <$> (parseLet <*> pars)) <* skipSpace <* char ';' <* skipSpace + pars = (StatementListValues <$> pars) <|> (StatementExpr <$> pars) {- -- alternative method to the above. @@ -89,7 +87,13 @@ instance CanParse ListValues where do functionParser listFunctions LVFunc <|> LVBase <$> pars + <|> (LVVar . ("l_" <>) <$> try (string "l_" *> varName)) + <|> (LVLet <$> (pars >>= checkLet)) <|> (try (pars <* char '#') >>= \nb -> MultipleValues nb <$> pars) + where + checkLet l + | T.isPrefixOf "l_" (letName l) = return l + | otherwise = fail "list variables must be prepended with l_" -- ( do -- nb <- pars @@ -107,7 +111,6 @@ instance CanParse ListValuesBase where ) <|> LVBParen . unnest <$> pars - <|> (LVBVar . ("l_" <>) <$> try (string "l_" *> varName)) where unnest (Paren (LVBase (LVBParen e))) = e unnest e = e @@ -118,7 +121,7 @@ binOpParseHelp c con = try (skipSpace *> char c) *> skipSpace *> (con <$> pars) instance CanParse Expr where pars = - (ExprLet <$> (parseLet <*> pars)) <|> do + (ExprLet <$> pars) <|> do t <- pars binOpParseHelp '+' (Add t) <|> binOpParseHelp '-' (Sub t) <|> (return . NoExpr) t From 033d5e884bd15a46bcfe7d621b7db0bbfb794ecc Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 2 Feb 2022 17:45:44 +0000 Subject: [PATCH 35/96] more fiddling to get things to work again --- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index 57a14c62..a88b4569 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -56,7 +56,7 @@ instance CanParse a => CanParse (Let a) where letCon varName' <$> pars instance CanParse Statement where - pars = (StatementListValues <$> pars) <|> (StatementExpr <$> pars) + pars = ((StatementListValues <$> try pars) <|> (StatementExpr <$> pars)) <* skipSpace <* char ';' <* skipSpace {- -- alternative method to the above. @@ -79,8 +79,15 @@ instance CanParse Statement where return $ either (LetList . letP) (LetExpr . letP) val -} +parseStatements :: Parser [Statement] +parseStatements = do + s <- optional $ try pars + case s of + Nothing -> return [] + Just s' -> (s' :) <$> parseStatements + instance CanParse Program where - pars = pars >>= \ss -> Program ss <$> pars + pars = parseStatements >>= \ss -> Program ss <$> pars instance CanParse ListValues where pars = @@ -121,9 +128,11 @@ binOpParseHelp c con = try (skipSpace *> char c) *> skipSpace *> (con <$> pars) instance CanParse Expr where pars = - (ExprLet <$> pars) <|> do - t <- pars - binOpParseHelp '+' (Add t) <|> binOpParseHelp '-' (Sub t) <|> (return . NoExpr) t + (ExprLet <$> pars) + <|> ( do + t <- pars + binOpParseHelp '+' (Add t) <|> binOpParseHelp '-' (Sub t) <|> (return . NoExpr) t + ) instance CanParse Term where pars = do @@ -169,7 +178,7 @@ instance (CanParse a) => CanParse (Paren a) where instance CanParse Base where pars = ( do - nb <- try pars + nb <- try pars "could not parse numbase in base" (DiceBase <$> parseDice nb) <|> return (NBase nb) -- try pars >>= \nb -> From bb1b89d35106cec4e46878d791425516b6d42ace Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 2 Feb 2022 19:36:06 +0000 Subject: [PATCH 36/96] added if expressions and moved some stuff around --- src/Tablebot/Plugins/Roll/Dice/DiceData.hs | 14 ++++-- src/Tablebot/Plugins/Roll/Dice/DiceEval.hs | 50 +++++++++++++++++-- .../Plugins/Roll/Dice/DiceFunctions.hs | 3 +- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 14 +++++- src/Tablebot/Plugins/Roll/Plugin.hs | 2 +- 5 files changed, 72 insertions(+), 11 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs index 17af7852..2111d802 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs @@ -19,7 +19,11 @@ import Tablebot.Plugins.Roll.Dice.DiceFunctions (FuncInfo, FuncInfoBase) data Let a = Let {letName :: Text, letValue :: a} | LetLazy {letName :: Text, letValue :: a} deriving (Show) --- data Statement = LetExpr (Let Expr) | LetList (Let ListValues) deriving (Show) +data If a b = If {ifCond :: a, thenValue :: b, elseValue :: b} deriving (Show) + +type IfExpr b = If Expr b + +type IfList b = If ListValues b data Statement = StatementExpr Expr | StatementListValues ListValues deriving (Show) @@ -37,9 +41,13 @@ data ListValues = MultipleValues NumBase Base | LVFunc (FuncInfoBase [Integer]) data ListValuesBase = LVBParen (Paren ListValues) | LVBList [Expr] deriving (Show) +-- | Miscellaneous expressions statements. +data ExprMisc = ExprLet (Let Expr) | ExprIfExpr (IfExpr Expr) | ExprIfList (IfList Expr) + deriving (Show) + -- | The type of the top level expression. Represents one of addition, --- subtraction, or a single term. -data Expr = ExprLet (Let Expr) | Add Term Expr | Sub Term Expr | NoExpr Term +-- subtraction, or a single term; or some misc expression statement. +data Expr = ExprMisc ExprMisc | Add Term Expr | Sub Term Expr | NoExpr Term deriving (Show) -- | The type representing multiplication, division, or a single negated term. diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index b7b7ca3f..b06a6bf4 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs @@ -378,9 +378,14 @@ binOpHelp rngCount a b opS op = do (b', b's, rngCount'') <- evalShow rngCount' b return (op a' b', a's <> " " <> opS <> " " <> b's, rngCount'') +instance IOEval ExprMisc where + evalShow' rngCount (ExprLet l) = evalShow rngCount l + evalShow' rngCount (ExprIfExpr l) = evalShow rngCount l + evalShow' rngCount (ExprIfList l) = evalShow rngCount l + instance IOEval Expr where evalShow' rngCount (NoExpr t) = evalShow rngCount t - evalShow' rngCount (ExprLet e) = evalShow rngCount e + evalShow' rngCount (ExprMisc e) = evalShow rngCount e evalShow' rngCount (Add t e) = binOpHelp rngCount t e "+" (+) evalShow' rngCount (Sub t e) = binOpHelp rngCount t e "-" (-) @@ -446,13 +451,44 @@ instance IOEvalList (Let ListValues) where evalStatement :: ProgramState -> Statement -> IO (Text, ProgramState) evalStatement ps (StatementExpr l) = evalShowStatement l >>= \(_, t, ps') -> return (t <> "; ", ps') where - evalShowStatement (ExprLet l'@(LetLazy t a)) = return (0, prettyShow l', addVariable ps t (Right a)) + evalShowStatement (ExprMisc (ExprLet l'@(LetLazy t a))) = return (0, prettyShow l', addVariable ps t (Right a)) evalShowStatement l' = evalShow ps l' evalStatement ps (StatementListValues l) = evalShowStatement l >>= \(_, t, ps') -> return (fromMaybe (prettyShow l) t <> "; ", ps') where evalShowStatement (LVLet l'@(LetLazy t a)) = return ([], Just (prettyShow l'), addVariable ps t (Left a)) evalShowStatement l' = evalShowL ps l' +class GetTruth a where + getTruthy :: ProgramState -> a -> IO (Bool, ProgramState) + +instance GetTruth Expr where + getTruthy ps a = do + (i, _, ps') <- evalShow ps a + return (i /= 0, ps') + +instance GetTruth ListValues where + getTruthy ps a = do + (i, _, ps') <- evalShowL ps a + return (not $ null i, ps') + +instance GetTruth a => IOEval (If a Expr) where + evalShow' ps if'@(If b t e) = do + (i, ps') <- getTruthy ps b + (i', _, ps'') <- + if i + then evalShow ps' t + else evalShow ps' e + return (i', prettyShow if', ps'') + +instance GetTruth a => IOEvalList (If a ListValues) where + evalShowL' ps if'@(If b t e) = do + (i, ps') <- getTruthy ps b + (i', _, ps'') <- + if i + then evalShowL ps' t + else evalShowL ps' e + return (i', Just $ prettyShow if', ps'') + --- Pretty printing the AST -- The output from this should be parseable @@ -476,11 +512,16 @@ instance PrettyShow ListValuesBase where prettyShow (LVBList es) = "{" <> intercalate ", " (prettyShow <$> es) <> "}" prettyShow (LVBParen p) = prettyShow p +instance PrettyShow ExprMisc where + prettyShow (ExprLet l) = prettyShow l + prettyShow (ExprIfExpr l) = prettyShow l + prettyShow (ExprIfList l) = prettyShow l + instance PrettyShow Expr where prettyShow (Add t e) = prettyShow t <> " + " <> prettyShow e prettyShow (Sub t e) = prettyShow t <> " - " <> prettyShow e prettyShow (NoExpr t) = prettyShow t - prettyShow (ExprLet e) = prettyShow e + prettyShow (ExprMisc e) = prettyShow e instance PrettyShow Term where prettyShow (Multi f t) = prettyShow f <> " * " <> prettyShow t @@ -540,6 +581,9 @@ instance (PrettyShow a) => PrettyShow (Let a) where prettyShow (Let t a) = "let " <> t <> " = " <> prettyShow a prettyShow (LetLazy t a) = "let !" <> t <> " = " <> prettyShow a +instance (PrettyShow a, PrettyShow b) => PrettyShow (If a b) where + prettyShow (If b t e) = "if " <> prettyShow b <> " then " <> prettyShow t <> " else " <> prettyShow e + instance PrettyShow Statement where prettyShow (StatementExpr l) = prettyShow l <> "; " prettyShow (StatementListValues l) = prettyShow l <> "; " diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs index 7ef5419c..58bba1f9 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs @@ -25,7 +25,6 @@ import Data.List (genericDrop, genericLength, genericTake, sort) import Data.Map as M (Map, fromList, keys) import Data.Maybe (fromJust) import Data.Text (Text, unpack) -import Debug.Trace (trace) import Tablebot.Utility.Exception (BotException (EvaluationException), throwBot) -- | The limit to how big a factorial value is permitted. Notably, the factorial @@ -91,7 +90,7 @@ funcInfoIndex = FuncInfo "index" [ATInteger, ATIntegerList] ATInteger fiIndex fiIndex (LIInteger i : [LIList is]) | i < 0 || i >= genericLength is = throwBot $ EvaluationException ("index out of range: " ++ show i) [] | otherwise = return (is !! fromInteger i) - fiIndex is = trace (show is) throwBot $ EvaluationException ("incorrect number of arguments. expected 2, got " ++ show (length is)) [] + fiIndex is = throwBot $ EvaluationException ("incorrect number of arguments. expected 2, got " ++ show (length is)) [] -- | A data structure to contain the information about a given function, -- including types, the function name, and the function itself. diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index a88b4569..9bd0be1f 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -26,7 +26,7 @@ import Tablebot.Plugins.Roll.Dice.DiceFunctions integerFunctions, listFunctions, ) -import Tablebot.Utility.Parser (integer, parseCommaSeparated1, skipSpace) +import Tablebot.Utility.Parser (integer, parseCommaSeparated1, skipSpace, skipSpace1) import Tablebot.Utility.SmartParser (CanParse (..)) import Tablebot.Utility.Types (Parser) import Text.Megaparsec (MonadParsec (observing, try), choice, failure, optional, some, (), (<|>)) @@ -126,9 +126,19 @@ instance CanParse ListValuesBase where binOpParseHelp :: (CanParse a) => Char -> (a -> a) -> Parser a binOpParseHelp c con = try (skipSpace *> char c) *> skipSpace *> (con <$> pars) +instance (CanParse a, CanParse b) => CanParse (If a b) where + pars = do + a <- string "if" *> skipSpace1 *> pars <* skipSpace1 + t <- string "then" *> skipSpace1 *> pars <* skipSpace1 + e <- string "else" *> skipSpace1 *> pars + return $ If a t e + +instance CanParse ExprMisc where + pars = (ExprLet <$> pars) <|> (ExprIfExpr <$> pars) <|> (ExprIfList <$> pars) + instance CanParse Expr where pars = - (ExprLet <$> pars) + (ExprMisc <$> pars) <|> ( do t <- pars binOpParseHelp '+' (Add t) <|> binOpParseHelp '-' (Sub t) <|> (return . NoExpr) t diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 7086707b..d82c03f9 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -37,7 +37,7 @@ import Text.RawString.QQ (r) rollDice' :: Maybe Program -> Maybe (Quoted Text) -> Message -> DatabaseDiscord () rollDice' e' t m = do let e = fromMaybe (Program [] (Right defaultRoll)) e' - liftIO $ putStrLn (unpack $ prettyShow e) + -- liftIO $ putStrLn (unpack $ prettyShow e) maybemsss <- liftIO $ timeout 1000000 $ evalProgram e case maybemsss of Nothing -> evaluationException "Could not process expression in one second" [] From 04af9c5a7f960c09e2b1f0f512bd20d4957988b3 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 2 Feb 2022 23:56:01 +0000 Subject: [PATCH 37/96] added replicate function and set value function --- src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs index 58bba1f9..ab48b07b 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs @@ -21,7 +21,7 @@ module Tablebot.Plugins.Roll.Dice.DiceFunctions where import Control.Monad.Exception (MonadException) -import Data.List (genericDrop, genericLength, genericTake, sort) +import Data.List (genericDrop, genericLength, genericReplicate, genericTake, sort) import Data.Map as M (Map, fromList, keys) import Data.Maybe (fromJust) import Data.Text (Text, unpack) @@ -76,6 +76,8 @@ listFunctionsList = M.keys listFunctions -- each function that returns an integer. listFunctions' :: [FuncInfoBase [Integer]] listFunctions' = + constructFuncInfo "replicate" (genericReplicate @Integer) : + funcInfoSet : constructFuncInfo "between" between : constructFuncInfo "drop" (genericDrop @Integer) : constructFuncInfo "take" (genericTake @Integer) : @@ -90,7 +92,15 @@ funcInfoIndex = FuncInfo "index" [ATInteger, ATIntegerList] ATInteger fiIndex fiIndex (LIInteger i : [LIList is]) | i < 0 || i >= genericLength is = throwBot $ EvaluationException ("index out of range: " ++ show i) [] | otherwise = return (is !! fromInteger i) - fiIndex is = throwBot $ EvaluationException ("incorrect number of arguments. expected 2, got " ++ show (length is)) [] + fiIndex is = throwBot $ EvaluationException ("incorrect number/type of arguments. expected 2, got " ++ show (length is)) [] + +funcInfoSet :: FuncInfoBase [Integer] +funcInfoSet = FuncInfo "set" [ATInteger, ATInteger, ATIntegerList] ATIntegerList fiSet + where + fiSet (LIInteger i : LIInteger j : [LIList js]) + | i < 0 || i >= genericLength js = throwBot $ EvaluationException ("index out of range: " ++ show i) [] + | otherwise = return $ genericTake i js ++ j : genericDrop (i + 1) js + fiSet is = throwBot $ EvaluationException ("incorrect number/type of arguments. expected 3, got " ++ show (length is)) [] -- | A data structure to contain the information about a given function, -- including types, the function name, and the function itself. From 567a390a58287768f7fb834a880b490febd51ada Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 10 Feb 2022 12:30:09 +0000 Subject: [PATCH 38/96] added discord haskell update --- stack.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stack.yaml b/stack.yaml index a88183e2..86d6de90 100644 --- a/stack.yaml +++ b/stack.yaml @@ -40,8 +40,7 @@ packages: # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a # extra-deps: -- git: https://github.com/L0neGamer/discord-haskell.git - commit: cd282b186bd0ade64980ef020501a442dd206f3e +- discord-haskell-1.12.0 - emoji-0.1.0.2 - load-env-0.2.1.0 - megaparsec-9.0.1 From 878170cc96a9eaecbfcad631337ac0af716085a5 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 10 Feb 2022 13:04:40 +0000 Subject: [PATCH 39/96] more merging --- docs/resources/dicestats_2d20kh1.jpg | Bin 0 -> 32963 bytes docs/resources/dicestats_2d20kh1_4d6dl1.jpg | Bin 0 -> 32693 bytes src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 246 ++++++++++++++++++ .../Plugins/Roll/Dice/DiceStatsBase.hs | 138 ++++++++++ 4 files changed, 384 insertions(+) create mode 100755 docs/resources/dicestats_2d20kh1.jpg create mode 100755 docs/resources/dicestats_2d20kh1_4d6dl1.jpg create mode 100644 src/Tablebot/Plugins/Roll/Dice/DiceStats.hs create mode 100644 src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs diff --git a/docs/resources/dicestats_2d20kh1.jpg b/docs/resources/dicestats_2d20kh1.jpg new file mode 100755 index 0000000000000000000000000000000000000000..85eefe42300d5bf64cb1ae6f7362ef598603879c GIT binary patch literal 32963 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 literal 0 HcmV?d00001 diff --git a/docs/resources/dicestats_2d20kh1_4d6dl1.jpg b/docs/resources/dicestats_2d20kh1_4d6dl1.jpg new file mode 100755 index 0000000000000000000000000000000000000000..f321941446bbdc398796c28ffb310f40c7ca264a GIT binary patch literal 32693 zcmb@tcT|(n*De^ONf)G7r7Bga(nX|1r1!2;1JVUTje>%70qISomw?pJdzIdTbO^mC z)Bqve{JuNun_2V6%$>XD&3pdHN?H3k=iU1}=h=JT{kvNQJb0rDR0ZJS;Q`*^UVys= zfHDC8-o5|axQhUH6W%8zBp@IpB_<}iPfkisPDV;bMnOsQfP#{ml8o#D{R8TUv~+ZI zR+;0d7NeBr^Xer1jX#YQ7cO3xg`+&uJl=yg@fP2(<_|$lJ zJpc{>0FMx7?SBmZKQ6p`I2(zG?~{;{;l5D!0B{cvAO9W!{(r2-eLD#EIe>tgkmm7o zC8CGA?}<4*Xhpxo=icXfRnh9_7>mL~WJ2^Ey1E2jjH@}8h-`L#R-r3zlot&PXUtFTEuK$Az z4}kwaVf{C<{~Io99Ikr=1o#BR|G|ZK&lflFsR;-lKPRG5(j|WH@sLyW%YE8c@wru< zBwS*8NV*T66QuOq;%hvp|3LdMWdCcxLjJdq{Wq}x6W2U|93Kz2c=*%+MZgtlq`)p! z1Un`2@Uow$wFz~URJbBOAHXK*EHE2yqD)EK^irP>KhGxpAaW}q&MQ5?k#lp-;;Jq3 zTBC>K{-+gM@B)G(!aGg4p#Bc{24?j2&6diJ^8>#`1jd!?>Dv58VY z{cP8_W|tr*4$-<+sIuU(CWW}QfG=L9gKia_c@?FuThoH@nT_WpZGVL>uWsrk3Bt=D zl=mgNFnmb)l;{cxF-W1ae>8o3USh$Bn=`p7H+?n&lJK~niR1a$TcdVRKkWP#5}%FI zGgsU{$EI~mSZxL&)w;5or{#hqVdZ-c>d5J)fY*XFPx+c+_awdJeBQV+DYeivkiV3^ zSH5!xs9BEkUd!UwI5aYs*D#tg*AKg}R2^l}c=>mPT(9eZ*R#`DxPjE-og{^FLZr(b zz}Ohm4vlD6M`AE;K6)UHSz z5$5tk=C;iYs$5e5(*Luy%ho?^InS6*x~b zLQW{aUC5gk=4VU|+>+BHxWDem!uFB{=J3dfuPZ0ipQ`7@dofdVB%JJ4sT}GJauqyu z;Ur(MxdWIfHU6NH;Wn#Gvi-f!DI^T9m4)qeQ9_3s@(6WQC|bq5G5b4<^w z{GJaN&eIqeek|9Uyr@2Ma`ec~$E#m@#1YmdFV@T4A1lGKJ&Q3fHv@-U&m*Rkljqzx zBRHZ;9@{MmUkLR${rc#tb}#(*)^6pR+bJ_#i^}^BK#XaxXjjm+TJo@4`-av<4wkRa z2x62Kpq?c@g%3iMg#N^5Y%ae1dvLZbU5v3@ZKvFs&xw#zUUyM8n;uz-a64NtIh!K) z8h-C?*N~J(dLI8Q66Yia`u@R+MOTSTPo>z~v9osoPB2t~%+IwjmeIzqFg_&Tqs0D$ zKY&=@s&}pOYa8pV9KLVmTt9+?M!m+8C{!)ry+RfZ$6{-5Ovv8zb)V}W4G~> z!Q2Em=yAGaq{+X$zCC3D{E++d_r?ESI79Hz3K#p`{XNV@-#PSAKTovn++%2SY`z__ zGuk#Tiiy=c6{IaB_5_z&@uQ8`j`6_vDAX*bC79%Z-oC}4ag({$HL@JX{Dsj6HlSHb%qJK$AqQ62MD^QO1UA>jZxND0EQSBCv|Tma(L-UKQ81{5Xwy?RSD#?eG}O zgN!#-zD$HvcbHerrK`qa4)Ql0|EG0JKJhtFoG#o&4}XwS_F89IHzMoW_*y0lBiD zY1}N96>Yb+9S_S`-T|!aITrHItpCQrfIIC;0g%`>3b3mp9XfVP7LIHS{RK+a-#4mv z@_zcUj^wjZ^>^{8mI$ePX4)wP^xPky=qWYiZCsyYEd^JH5mq=uck1lZRQb|om3HaEjI>=Hir3iCmWBg>kBJNoqiwvheNI%JeB-1Nf>SnE zHbpuw1X7kagXV84vDT3ay8mT>?!~S=!$~D%BA5X!)(4?%s1v?ObwL~?T=^dugF{ONl}lW-8vQM8qkU;`hXb8BM1XHSimP>u7b3iAjWT# zHcb4*`O49iqAS5EL-Psq0nB5&eS@3&1u@TLR_?^9*_oMVn zKUAjwR`(9zUyq?)ZI1iSE><`3D_izO{&0#^4WzUdrm`eB8o>XE583Z;+su{yf3-dk zHQPmXA06In#g1bbYQxCwZ|2=J&alf_cDv!_*ZOZ`q>{&mV`&tZN(_e}_XnX3SX!`% znKkL2s(NBKvvz2PX>()ycMufvs6CmRfgPCE2LxQ%ZSa3F2os2Ci^GMza4B4{{TsQp zPz`|yrC{I2r7JA){l*#u{bw!9=H!%+_H*sEa=?;BkODi>yDbakN4WA8(b%prrl1=8 zoYvS&@C%GP7t^oVrn?@Wy{|cW=Aw8}3Pa8m#^hUoy>zBrqLX3i5@lh1$vl*$f3f;0 zSE+gTwDp`U%5zfAPuS{UNp}E|(=&zl|ApLeNtC5kx5zxeRG9p^_@v+S=e^zIM`{+p;{of}|Nm9<@n> zUHiMN=~|QvcjJSyJMDmLp|t{nAdzJGl;9OKg?p{a5-*?g4y~?=4k4}X%m2smDVqzm zm5YF|aL`&RyRheODe3c#_#yAN+*i#ptPyrsM8b=NrBrNs1!tRK3;LlU!R;Oh)8iL++m` z@&B~gdgE?ebzwx=cgFkXOWe9+FFCpE*g!LOox53zU zSuCGYNuY_$RtmFIFvIvco0BZh)T~8==}7<2sw>AgB(F6&D*!&Pf_3~qJH>@!IdjN% z%#x#gb3tv7Co3Hy4;Gwwy-DQW&#*Gus&dgh;|CHok~S7PUN%i9_S?Ad40RHu=zZ_= zQDIK@Rp_|q)ZBIlK;mu)jStWV$82!X7R?qOp&;t4@FcYN!Q$NRc+q#wh$i~+>igKg z8JyHHXdN{jrHfu%_)yhkcv~|QD3ZHB+hhs*ci=6g#3$N)U(k?=#xV9}LG03w$M}Ya z1qGd4ne~1_yAaW*U_w+amT*S6BCo1$`tVqA^G{Pv%PE1E(xdJQ8#_BWcjbHhi#ZP^ zh-#2^%=dUU#oNndTH1lkT}_(!Nz;4 z7jofj?#CDTm!sCAiIOwUzi|DqaXiSx@J)dE^&$4K+v2SeCw4w;;o>%)`?oFZP@?J

MYu)6*ITJKLwhy%tyX?bg$}Hd9ao%@622XiC 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

YG{#d9FD5C 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 literal 0 HcmV?d00001 diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs new file mode 100644 index 00000000..8b6a5022 --- /dev/null +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -0,0 +1,246 @@ +-- | +-- Module : Tablebot.Plugins.Roll.Dice.DiceStats +-- Description : Get statistics on particular expressions. +-- License : MIT +-- Maintainer : tagarople@gmail.com +-- Stability : experimental +-- Portability : POSIX +-- +-- This plugin generates statistics based on the values of dice in given +-- expressions. +module Tablebot.Plugins.Roll.Dice.DiceStats (rangeExpr, rangeListValues, getStats) where + +import Control.Monad +import Control.Monad.Exception +import Data.Bifunctor (Bifunctor (first)) +import Data.Distribution hiding (Distribution, Experiment, fromList) +import qualified Data.Distribution as D +import Data.List +import Tablebot.Plugins.Roll.Dice.DiceData +import Tablebot.Plugins.Roll.Dice.DiceEval +import Tablebot.Plugins.Roll.Dice.DiceFunctions +import Tablebot.Plugins.Roll.Dice.DiceStatsBase (Distribution) +import Tablebot.Utility.Exception (catchBot) + +-- | Alias for an experiment of integers. +-- +-- Where a distribution is a concrete mapping between values and probabilities, +-- an Experiment is more a monadic representation of a Distribution, effectively +-- deferring calculation to the end. +-- +-- I'm not sure if it's more efficient but it certainly makes composing things +-- a lot easier +type Experiment = D.Experiment Integer + +-- | Convenient alias for a experiments of lists of integers. +type ExperimentList = D.Experiment [Integer] + +-- | Get the most common values, the mean, and the standard deviation of a given +-- distribution. +getStats :: Distribution -> ([Integer], Double, Double) +getStats d = (modalOrder, expectation d, standardDeviation d) + where + vals = toList d + modalOrder = fst <$> sortBy (\(_, r) (_, r') -> compare r' r) vals + +-- | Convenience wrapper which gets the range of the given values then applies +-- the function to the resultant distributions. +combineRangesBinOp :: (MonadException m, Range a, Range b, ParseShow a, ParseShow b) => (Integer -> Integer -> Integer) -> a -> b -> m Experiment +combineRangesBinOp f a b = do + d <- range a + d' <- range b + return $ f <$> d <*> d' + +rangeExpr :: (MonadException m) => Expr -> m Distribution +rangeExpr e = do + ex <- range e + return $ run ex + +rangeListValues :: (MonadException m) => ListValues -> m [Distribution] +rangeListValues lv = do + lve <- rangeList lv + let lvd = run lve + lvd' = toList lvd + return $ D.fromList <$> zip' lvd' + where + head' [] = [] + head' (x : _) = [x] + getHeads xs = (\(xs', p) -> (,p) <$> head' xs') =<< xs + getTails xs = first tail <$> xs + zip' xs = getHeads xs : zip' (getTails xs) + +-- | Type class to get the overall range of a value. +-- +-- A `Data.Distribution.Distribution` is a map of values to probabilities, and +-- has a variety of functions that operate on them. +-- +-- An `Data.Distribution.Experiment` is a monadic form of this. +class Range a where + -- | Try and get the `Experiment` of the given value, throwing a + -- `MonadException` on failure. + range :: (MonadException m, ParseShow a) => a -> m Experiment + range a = propagateException (parseShow a) (range' a) + + range' :: (MonadException m, ParseShow a) => a -> m Experiment + +instance Range Expr where + range' (NoExpr t) = range t + range' (Add t e) = combineRangesBinOp (+) t e + range' (Sub t e) = combineRangesBinOp (-) t e + +instance Range Term where + range' (NoTerm t) = range t + range' (Multi t e) = combineRangesBinOp (*) t e + range' (Div t e) = do + d <- range t + d' <- range e + -- If 0 is always the denominator, the distribution will be empty. + return $ div <$> d <*> from (assuming (/= 0) (run d')) + +instance Range Negation where + range' (Neg t) = fmap negate <$> range t + range' (NoNeg t) = range t + +instance Range Expo where + range' (NoExpo t) = range t + range' (Expo t e) = do + d <- range t + d' <- range e + -- if the exponent is always negative, the distribution will be empty + return $ (^) <$> d <*> from (assuming (>= 0) (run d')) + +instance Range Func where + range' (NoFunc t) = range t + range' (Func fi avs) = rangeFunction fi avs + +instance Range NumBase where + range' (Value i) = return $ return i + range' (NBParen (Paren e)) = range e + +instance Range Base where + range' (NBase nb) = range nb + range' (DiceBase d) = range d + +instance Range Die where + range' (LazyDie d) = range d + range' (Die nb) = do + nbr <- range nb + return $ + do + nbV <- nbr + from $ uniform [1 .. nbV] + range' (CustomDie lv) = do + dievs <- rangeList lv + return $ dievs >>= from . uniform + +instance Range Dice where + range' (Dice b d mdor) = do + b' <- range b + d' <- range d + let e = do + diecount <- b' + getDiceExperiment diecount (run d') + res <- rangeDiceExperiment d' mdor e + return $ sum <$> res + +-- | Get the distribution of values from a given number of (identically +-- distributed) values and the distribution of that value. +getDiceExperiment :: Integer -> Distribution -> ExperimentList +getDiceExperiment i = replicateM (fromInteger i) . from + +-- | Go through each operator on dice and modify the `Experiment` representing +-- all possible collections of rolls, returning the `Experiment` produced on +-- finding `Nothing`. +rangeDiceExperiment :: (MonadException m) => Experiment -> Maybe DieOpRecur -> ExperimentList -> m ExperimentList +rangeDiceExperiment _ Nothing is = return is +rangeDiceExperiment die (Just (DieOpRecur doo mdor)) is = rangeDieOpExperiment die doo is >>= rangeDiceExperiment die mdor + +-- | Perform one dice operation on the given `Experiment`, possibly returning +-- a modified experiment representing the distribution of dice rolls. +rangeDieOpExperiment :: MonadException m => Experiment -> DieOpOption -> ExperimentList -> m ExperimentList +rangeDieOpExperiment die (DieOpOptionLazy o) is = rangeDieOpExperiment die o is +rangeDieOpExperiment _ (DieOpOptionKD kd lhw) is = rangeDieOpExperimentKD kd lhw is +rangeDieOpExperiment die (Reroll rro cond lim) is = do + limd <- range lim + return $ do + limit <- limd + let newDie = mkNewDie limit + rolls <- is + let (count, cutdownRolls) = countTriggers limit rolls + if count == 0 + then return cutdownRolls + else (cutdownRolls ++) <$> getDiceExperiment count (run newDie) + where + mkNewDie limitValue + | rro = die + | otherwise = from $ assuming (\i -> not $ applyCompare cond i limitValue) (run die) + countTriggers limitValue = foldr (\i (c, xs') -> if applyCompare cond i limitValue then (c + 1, xs') else (c, i : xs')) (0, []) + +-- | Perform a keep/drop operation on the `Experiment` of dice rolls. +rangeDieOpExperimentKD :: (MonadException m) => KeepDrop -> LowHighWhere -> ExperimentList -> m ExperimentList +rangeDieOpExperimentKD kd (Where cond nb) is = do + nbDis <- range nb + return $ do + wherelimit <- nbDis + filter (\i -> keepDrop $ applyCompare cond i wherelimit) <$> is + where + keepDrop + | kd == Keep = id + | otherwise = not +rangeDieOpExperimentKD kd lhw is = do + let nb = getValueLowHigh lhw + case nb of + Nothing -> whereException + Just nb' -> do + nbd <- range nb' + return $ do + kdlh <- nbd + getKeep kdlh . sortBy' <$> is + where + -- the below exception should never trigger - it is a hold over. it is + -- present so that this thing type checks nicely. + whereException = evaluationException "keep/drop where is unsupported" [] + order l l' = if isLow lhw then compare l l' else compare l' l + sortBy' = sortBy order + getKeep = if kd == Keep then genericTake else genericDrop + +-- | Type class to get the overall range of a list of values. +-- +-- Only used within `DiceStats` as I have no interest in producing statistics on +-- lists +class RangeList a where + -- | Try and get the `DistributionList` of the given value, throwing a + -- `MonadException` on failure. + rangeList :: (MonadException m, ParseShow a) => a -> m ExperimentList + rangeList a = propagateException (parseShow a) (rangeList' a) + + rangeList' :: (MonadException m, ParseShow a) => a -> m ExperimentList + +instance RangeList ListValuesBase where + rangeList' (LVBList es) = do + exprs <- mapM range es + return $ sequence exprs + rangeList' (LVBParen (Paren lv)) = rangeList lv + +instance RangeList ListValues where + rangeList' (LVBase lvb) = rangeList lvb + rangeList' (MultipleValues nb b) = do + nbd <- range nb + bd <- range b + return $ + do + valNum <- nbd + getDiceExperiment valNum (run bd) + rangeList' (LVFunc fi avs) = rangeFunction fi avs + +rangeArgValue :: MonadException m => ArgValue -> m (D.Experiment ListInteger) +rangeArgValue (AVExpr e) = (LIInteger <$>) <$> range e +rangeArgValue (AVListValues lv) = (LIList <$>) <$> rangeList lv + +rangeFunction :: (MonadException m, Ord j) => FuncInfoBase j -> [ArgValue] -> m (D.Experiment j) +rangeFunction fi exprs = do + exprs' <- mapM rangeArgValue exprs + let params = first (funcInfoFunc fi) <$> toList (run $ sequence exprs') + from . D.fromList <$> foldAndIgnoreErrors params + where + foldAndIgnoreErrors = foldr (\(mv, p) mb -> mb >>= \b -> catchBot ((: []) . (,p) <$> mv) (const (return [])) >>= \v -> return (v ++ b)) (return []) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs new file mode 100644 index 00000000..266458eb --- /dev/null +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStatsBase.hs @@ -0,0 +1,138 @@ +-- | +-- Module : Tablebot.Plugins.Roll.Dice.DiceStatsBase +-- Description : The basics for dice stats +-- License : MIT +-- Maintainer : tagarople@gmail.com +-- Stability : experimental +-- Portability : POSIX +-- +-- The basics for dice stats. Functions for creating and manipulating +-- `Distribution`s. +module Tablebot.Plugins.Roll.Dice.DiceStatsBase + ( Distribution, + distributionByteString, + ) +where + +import Codec.Picture (PngSavable (encodePng)) +import Data.Bifunctor +import qualified Data.ByteString.Lazy as B +import qualified Data.Distribution as D +import Data.List (genericLength) +import qualified Data.Map as M +import qualified Data.Set as S +import qualified Data.Text as T +import Diagrams (Diagram, dims2D, renderDia) +import Diagrams.Backend.Rasterific +import Graphics.Rendering.Chart.Axis.Int +import Graphics.Rendering.Chart.Backend.Diagrams (defaultEnv, runBackendR) +import Graphics.Rendering.Chart.Backend.Types +import Graphics.Rendering.Chart.Easy +import Tablebot.Plugins.Roll.Dice.DiceEval (evaluationException) + +-- | A wrapper type for mapping values to their probabilities. +type Distribution = D.Distribution Integer + +-- | Default x and y values for the output chart. +diagramX, diagramY :: Double +(diagramX, diagramY) = (1400.0, 400.0) + +-- | Get the ByteString representation of the given distribution, setting the +-- string as its title. +distributionByteString :: [(Distribution, T.Text)] -> IO B.ByteString +distributionByteString d = encodePng . renderDia Rasterific opts <$> distributionDiagram d + where + opts = RasterificOptions (dims2D diagramX diagramY) + +-- | Get the Diagram representation of the given distribution, setting the +-- string as its title. +distributionDiagram :: [(Distribution, T.Text)] -> IO (Diagram B) +distributionDiagram d = do + if null d + then evaluationException "empty distribution" [] + else do + defEnv <- defaultEnv (AlignmentFns id id) diagramX diagramY + return . fst $ runBackendR defEnv r + where + r = distributionRenderable d + +-- | Get the Renderable representation of the given distribution, setting the +-- string as its title. +distributionRenderable :: [(Distribution, T.Text)] -> Renderable () +distributionRenderable d = toRenderable $ do + layout_title .= T.unpack (title' d) + layout_x_axis . laxis_title .= "value" + layout_y_axis . laxis_title .= "probability (%)" + layout_x_axis . laxis_generate .= scaledIntAxis' r + layout_y_axis . laxis_override .= \ad@AxisData {_axis_labels = axisLabels} -> ad {_axis_labels = (second (\s -> if '.' `elem` s then s else s ++ ".0") <$>) <$> axisLabels} + layout_all_font_styles .= defFontStyle + pb <- (bars @Integer @Double) (barNames d) pts + let pb' = pb {_plot_bars_spacing = BarsFixGap 10 5} + plot $ return $ plotBars pb' + where + removeNullMap m + | M.null m = M.singleton 0 0 + | otherwise = m + ds = removeNullMap . D.toMap . fst <$> d + allIntegers = let s = S.unions $ M.keysSet <$> ds in [S.findMin s .. S.findMax s] + insertEmpty k = M.insertWith (\_ a -> a) k 0 + ds' = M.unionsWith (++) $ M.map (: []) <$> (applyAll (insertEmpty <$> allIntegers) <$> ds) + pts = second (fromRational . (* 100) <$>) <$> M.toList ds' + r = (fst $ M.findMin ds', fst $ M.findMax ds') + applyAll [] = id + applyAll (f : fs) = f . applyAll fs + defFontStyle = def {_font_size = 2 * _font_size def} + barNames [_] = [""] + barNames xs = T.unpack . snd <$> xs + title' [(_, t)] = t + title' xs = "Range of " <> T.intercalate ", " (snd <$> xs) + +-- | Custom scaling function due to some difficulties for drawing charts. +-- +-- Using +-- https://hackage.haskell.org/package/Chart-1.9.3/docs/src/Graphics.Rendering.Chart.Axis.Int.html#scaledIntAxis +-- for pointers. +scaledIntAxis' :: (Integer, Integer) -> AxisFn Integer +scaledIntAxis' r@(minI, maxI) _ = makeAxis (_la_labelf lap) ((minI - 1) : (maxI + 1) : labelvs, tickvs, gridvs) + where + lap = defaultIntAxis + labelvs = stepsInt' (fromIntegral $ _la_nLabels lap) r + tickvs = + stepsInt' + (fromIntegral $ _la_nTicks lap) + ( fromIntegral $ minimum labelvs, + fromIntegral $ maximum labelvs + ) + gridvs = labelvs + +-- | Taken and modified from +-- https://hackage.haskell.org/package/Chart-1.9.3/docs/src/Graphics.Rendering.Chart.Axis.Int.html#stepsInt +stepsInt' :: Integer -> (Integer, Integer) -> [Integer] +stepsInt' nSteps range = bestSize (goodness alt0) alt0 alts + where + bestSize n a (a' : as) = + let n' = goodness a' + in if n' < n then bestSize n' a' as else a + bestSize _ _ [] = [] + + goodness vs = abs (genericLength vs - nSteps) + + (alt0 : alts) = map (`steps` range) sampleSteps' + + -- throw away sampleSteps that are definitely too small as + -- they takes a long time to process + sampleSteps' = + let rangeMag = (snd range - fst range) + + (s1, s2) = span (< (rangeMag `div` nSteps)) sampleSteps + in (reverse . take 5 . reverse) s1 ++ s2 + + -- generate all possible step sizes + sampleSteps = [1, 2, 5] ++ sampleSteps1 + sampleSteps1 = [10, 20, 25, 50] ++ map (* 10) sampleSteps1 + + steps :: Integer -> (Integer, Integer) -> [Integer] + steps size' (minV, maxV) = takeWhile (< b) [a, a + size' ..] ++ [b] + where + a = floor @Double (fromIntegral minV / fromIntegral size') * size' + b = ceiling @Double (fromIntegral maxV / fromIntegral size') * size' From 8fafa2fb792522bed2dcf82980eddae5591caa34 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 10 Feb 2022 18:50:31 +0000 Subject: [PATCH 40/96] started normalising the additional operators (let and if) --- src/Tablebot/Plugins/Roll/Dice/DiceData.hs | 13 +++-- src/Tablebot/Plugins/Roll/Dice/DiceEval.hs | 27 +++++---- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 13 +++-- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 55 ++++++++++++++++++- 4 files changed, 85 insertions(+), 23 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs index b1f7d8a2..ee7d0259 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs @@ -25,14 +25,19 @@ type IfExpr b = If Expr b type IfList b = If ListValues b +data MiscData b = MiscIfExpr (IfExpr b) | MiscIfList (IfList b) | MiscLet (Let b) deriving (Show) + data Statement = StatementExpr Expr | StatementListValues ListValues deriving (Show) +-- | A program is a series of statements followed by data Program = Program [Statement] (Either ListValues Expr) deriving (Show) -- | The value of an argument given to a function. data ArgValue = AVExpr Expr | AVListValues ListValues deriving (Show) +type ListValuesMisc = MiscData ListValues + -- | The type for list values. data ListValues = -- | Represents `N#B`, where N is a NumBase (numbers, parentheses) and B is a Base (numbase or dice value) @@ -41,8 +46,9 @@ data ListValues LVFunc (FuncInfoBase [Integer]) [ArgValue] | -- | A base ListValues value - parentheses or a list of expressions LVBase ListValuesBase - | LVVar Text - | LVLet (Let ListValues) + | -- | A variable that has been defined elsewhere. + LVVar Text + | ListValuesMisc ListValuesMisc deriving (Show) -- | The type for basic list values (that can be used as is for custom dice). @@ -55,8 +61,7 @@ data ListValuesBase = LVBParen (Paren ListValues) | LVBList [Expr] deriving (Show) -- | Miscellaneous expressions statements. -data ExprMisc = ExprLet (Let Expr) | ExprIfExpr (IfExpr Expr) | ExprIfList (IfList Expr) - deriving (Show) +type ExprMisc = MiscData Expr -- | The type of the top level expression. Represents one of addition, -- subtraction, or a single term; or some misc expression statement. diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index aa3b75bb..c14d10f2 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs @@ -195,7 +195,7 @@ instance IOEvalList ListValues where evalShowL' rngCount (LVVar t) = case M.lookup t (getVariables rngCount) of Just (Left e) -> evalShowL rngCount e >>= \(i, _, rngCount') -> return (i, Just t, rngCount') _ -> evaluationException ("could not find list variable `" <> t <> "`") [] - evalShowL' rngCount (LVLet l) = evalShowL rngCount l + evalShowL' rngCount (ListValuesMisc l) = evalShowL rngCount l instance IOEvalList ListValuesBase where evalShowL' rngCount (LVBList es) = do @@ -203,6 +203,11 @@ instance IOEvalList ListValuesBase where return (vs, Nothing, rc) evalShowL' rngCount (LVBParen (Paren lv)) = evalShowL rngCount lv +instance IOEvalList ListValuesMisc where + evalShowL' rngCount (MiscLet l) = evalShowL rngCount l + evalShowL' rngCount (MiscIfExpr l) = evalShowL rngCount l + evalShowL' rngCount (MiscIfList l) = evalShowL rngCount l + -- | This type class gives a function which evaluates the value to an integer -- and a string. class IOEval a where @@ -384,9 +389,9 @@ binOpHelp rngCount a b opS op = do return (op a' b', a's <> " " <> opS <> " " <> b's, rngCount'') instance IOEval ExprMisc where - evalShow' rngCount (ExprLet l) = evalShow rngCount l - evalShow' rngCount (ExprIfExpr l) = evalShow rngCount l - evalShow' rngCount (ExprIfList l) = evalShow rngCount l + evalShow' rngCount (MiscLet l) = evalShow rngCount l + evalShow' rngCount (MiscIfExpr l) = evalShow rngCount l + evalShow' rngCount (MiscIfList l) = evalShow rngCount l instance IOEval Expr where evalShow' rngCount (NoExpr t) = evalShow rngCount t @@ -456,11 +461,11 @@ instance IOEvalList (Let ListValues) where evalStatement :: ProgramState -> Statement -> IO (Text, ProgramState) evalStatement ps (StatementExpr l) = evalShowStatement l >>= \(_, t, ps') -> return (t <> "; ", ps') where - evalShowStatement (ExprMisc (ExprLet l'@(LetLazy t a))) = return (0, prettyShow l', addVariable ps t (Right a)) + evalShowStatement (ExprMisc (MiscLet l'@(LetLazy t a))) = return (0, prettyShow l', addVariable ps t (Right a)) evalShowStatement l' = evalShow ps l' evalStatement ps (StatementListValues l) = evalShowStatement l >>= \(_, t, ps') -> return (fromMaybe (prettyShow l) t <> "; ", ps') where - evalShowStatement (LVLet l'@(LetLazy t a)) = return ([], Just (prettyShow l'), addVariable ps t (Left a)) + evalShowStatement (ListValuesMisc (MiscLet l'@(LetLazy t a))) = return ([], Just (prettyShow l'), addVariable ps t (Left a)) evalShowStatement l' = evalShowL ps l' class GetTruth a where @@ -511,16 +516,16 @@ instance PrettyShow ListValues where prettyShow (MultipleValues nb b) = prettyShow nb <> "#" <> prettyShow b prettyShow (LVFunc s n) = funcInfoName s <> "(" <> intercalate "," (prettyShow <$> n) <> ")" prettyShow (LVVar t) = t - prettyShow (LVLet l) = prettyShow l + prettyShow (ListValuesMisc l) = prettyShow l instance PrettyShow ListValuesBase where prettyShow (LVBList es) = "{" <> intercalate ", " (prettyShow <$> es) <> "}" prettyShow (LVBParen p) = prettyShow p -instance PrettyShow ExprMisc where - prettyShow (ExprLet l) = prettyShow l - prettyShow (ExprIfExpr l) = prettyShow l - prettyShow (ExprIfList l) = prettyShow l +instance PrettyShow a => PrettyShow (MiscData a) where + prettyShow (MiscLet l) = prettyShow l + prettyShow (MiscIfExpr l) = prettyShow l + prettyShow (MiscIfList l) = prettyShow l instance PrettyShow Expr where prettyShow (Add t e) = prettyShow t <> " + " <> prettyShow e diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index 58e65a3f..c3fe643f 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -29,7 +29,7 @@ import Tablebot.Plugins.Roll.Dice.DiceFunctions import Tablebot.Utility.Parser (integer, parseCommaSeparated1, skipSpace, skipSpace1) import Tablebot.Utility.SmartParser (CanParse (..), ()) import Tablebot.Utility.Types (Parser) -import Text.Megaparsec (MonadParsec (observing, try), choice, failure, optional, some, (), (<|>)) +import Text.Megaparsec (MonadParsec (try), choice, failure, optional, some, (), (<|>)) import Text.Megaparsec.Char (char, string) import Text.Megaparsec.Error (ErrorItem (Tokens)) @@ -88,12 +88,13 @@ instance CanParse ListValues where functionParser listFunctions LVFunc <|> LVBase <$> pars <|> (LVVar . ("l_" <>) <$> try (string "l_" *> varName)) - <|> (LVLet <$> (pars >>= checkLet)) + <|> ListValuesMisc <$> (pars >>= checkLet) <|> (try (pars <* char '#') >>= \nb -> MultipleValues nb <$> pars) where - checkLet l - | T.isPrefixOf "l_" (letName l) = return l + checkLet (MiscLet l) + | T.isPrefixOf "l_" (letName l) = return (MiscLet l) | otherwise = fail "list variables must be prepended with l_" + checkLet l = return l -- ( do -- nb <- pars @@ -126,8 +127,8 @@ instance (CanParse a, CanParse b) => CanParse (If a b) where e <- string "else" *> skipSpace1 *> pars return $ If a t e -instance CanParse ExprMisc where - pars = (ExprLet <$> pars) <|> (ExprIfExpr <$> pars) <|> (ExprIfList <$> pars) +instance CanParse a => CanParse (MiscData a) where + pars = (MiscLet <$> pars) <|> (MiscIfExpr <$> pars) <|> (MiscIfList <$> pars) instance CanParse Expr where pars = diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index fba5dbbb..c9eeaa00 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -16,6 +16,7 @@ import Data.Bifunctor (Bifunctor (first)) import Data.Distribution hiding (Distribution, Experiment, fromList) import qualified Data.Distribution as D import Data.List +import qualified Data.Map as M import Tablebot.Plugins.Roll.Dice.DiceData import Tablebot.Plugins.Roll.Dice.DiceEval import Tablebot.Plugins.Roll.Dice.DiceFunctions @@ -75,7 +76,7 @@ rangeListValues lv = do -- has a variety of functions that operate on them. -- -- An `Data.Distribution.Experiment` is a monadic form of this. -class Range a where +class PrettyShow a => Range a where -- | Try and get the `Experiment` of the given value, throwing a -- `MonadException` on failure. range :: (MonadException m, PrettyShow a) => a -> m Experiment @@ -83,10 +84,57 @@ class Range a where range' :: (MonadException m, PrettyShow a) => a -> m Experiment +instance (Range a) => Range (MiscData a) where + range' (MiscLet l) = range l + range' (MiscIfExpr i) = rangeIfExpr range i + range' (MiscIfList i) = rangeIfList range i + +instance (RangeList a) => RangeList (MiscData a) where + rangeList' (MiscLet l) = rangeList l + rangeList' (MiscIfExpr i) = rangeIfExpr rangeList i + rangeList' (MiscIfList i) = rangeIfList rangeList i + +rangeIfExpr :: (MonadException m, Ord b) => (a -> m (D.Experiment b)) -> If Expr a -> m (D.Experiment b) +rangeIfExpr func (If b t f) = do + b' <- range b + let mp = toMap $ run b' + canBeFalse = M.member 0 mp + canBeTrue = M.null $ M.filterWithKey (\k _ -> k /= 0) mp + emptyExp = from $ D.fromList @_ @Integer [] + t' <- if canBeTrue then func t else return emptyExp + f' <- if canBeFalse then func f else return emptyExp + return $ + do + b'' <- b' + if b'' /= 0 then t' else f' + +rangeIfList :: (MonadException m, Ord b) => (a -> m (D.Experiment b)) -> If ListValues a -> m (D.Experiment b) +rangeIfList func (If b t f) = do + b' <- rangeList b + let mp = toMap $ run b' + canBeFalse = M.member [] mp + canBeTrue = M.null $ M.filterWithKey (\k _ -> k /= []) mp + emptyExp = from $ D.fromList @_ @Integer [] + t' <- if canBeTrue then func t else return emptyExp + f' <- if canBeFalse then func f else return emptyExp + return $ + do + b'' <- b' + if b'' /= [] then t' else f' + +instance (Range a) => Range (Let a) where + range' (Let _ a) = range a + range' (LetLazy _ a) = range a + +instance (RangeList a) => RangeList (Let a) where + rangeList' (Let _ a) = rangeList a + rangeList' (LetLazy _ a) = rangeList a + instance Range Expr where range' (NoExpr t) = range t range' (Add t e) = combineRangesBinOp (+) t e range' (Sub t e) = combineRangesBinOp (-) t e + range' (ExprMisc t) = range t instance Range Term where range' (NoTerm t) = range t @@ -120,6 +168,7 @@ instance Range NumBase where instance Range Base where range' (NBase nb) = range nb range' (DiceBase d) = range d + range' b@(Var _) = evaluationException "cannot find range of variable" [prettyShow b] instance Range Die where range' (LazyDie d) = range d @@ -208,7 +257,7 @@ rangeDieOpExperimentKD kd lhw is = do -- -- Only used within `DiceStats` as I have no interest in producing statistics on -- lists -class RangeList a where +class PrettyShow a => RangeList a where -- | Try and get the `DistributionList` of the given value, throwing a -- `MonadException` on failure. rangeList :: (MonadException m, PrettyShow a) => a -> m ExperimentList @@ -232,6 +281,8 @@ instance RangeList ListValues where valNum <- nbd getDiceExperiment valNum (run bd) rangeList' (LVFunc fi avs) = rangeFunction fi avs + rangeList' (ListValuesMisc m) = rangeList m + rangeList' b@(LVVar _) = evaluationException "cannot find range of variable" [prettyShow b] rangeArgValue :: MonadException m => ArgValue -> m (D.Experiment ListInteger) rangeArgValue (AVExpr e) = (LIInteger <$>) <$> range e From 699a5542c17c511aec6df9d0ae830dfec80975d1 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 10 Feb 2022 19:00:54 +0000 Subject: [PATCH 41/96] some commenting --- src/Tablebot/Plugins/Roll/Dice/DiceData.hs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs index ee7d0259..1b5d0201 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs @@ -17,25 +17,39 @@ import Data.Text (Text) import Data.Tuple (swap) import Tablebot.Plugins.Roll.Dice.DiceFunctions (FuncInfo, FuncInfoBase) +-- | Set the variable `letName` to the value `letValue`. This also returns the +-- evaluated `letValue`. +-- +-- List variables have to be prefixed with `l_`. This really helps with parsing. data Let a = Let {letName :: Text, letValue :: a} | LetLazy {letName :: Text, letValue :: a} deriving (Show) +-- | If the first value is truthy (non-zero or a non-empty list) then return +-- the `thenValue`, else return the `elseValue`. data If a b = If {ifCond :: a, thenValue :: b, elseValue :: b} deriving (Show) type IfExpr b = If Expr b type IfList b = If ListValues b +-- | Either an If or a Let that returns a `b`. data MiscData b = MiscIfExpr (IfExpr b) | MiscIfList (IfList b) | MiscLet (Let b) deriving (Show) +-- | An expression is just an Expr or a ListValues with a semicolon on the end. +-- +-- When evaluating, LetLazy expressions are handled with a special case - they +-- are not evaluated until the value is first referenced. Otherwise, the value +-- is evaluated as the statement is encountered data Statement = StatementExpr Expr | StatementListValues ListValues deriving (Show) --- | A program is a series of statements followed by +-- | A program is a series of `Statement`s followed by either a `ListValues` or +-- an Expr. data Program = Program [Statement] (Either ListValues Expr) deriving (Show) -- | The value of an argument given to a function. data ArgValue = AVExpr Expr | AVListValues ListValues deriving (Show) +-- | Alias for `MiscData` that returns a `ListValues`. type ListValuesMisc = MiscData ListValues -- | The type for list values. @@ -48,7 +62,8 @@ data ListValues LVBase ListValuesBase | -- | A variable that has been defined elsewhere. LVVar Text - | ListValuesMisc ListValuesMisc + | -- | A misc list values expression. + ListValuesMisc ListValuesMisc deriving (Show) -- | The type for basic list values (that can be used as is for custom dice). @@ -60,7 +75,7 @@ data ListValues data ListValuesBase = LVBParen (Paren ListValues) | LVBList [Expr] deriving (Show) --- | Miscellaneous expressions statements. +-- | Alias for `MiscData` that returns an `Expr`. type ExprMisc = MiscData Expr -- | The type of the top level expression. Represents one of addition, From a31da8b45260cc8babc2943559b71db90c1b87ad Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 10 Feb 2022 19:47:27 +0000 Subject: [PATCH 42/96] more commenting and documenting --- docs/Roll.md | 8 +++++- src/Tablebot/Plugins/Roll/Dice.hs | 29 +++++++++++++++------- src/Tablebot/Plugins/Roll/Dice/DiceEval.hs | 2 +- src/Tablebot/Plugins/Roll/Plugin.hs | 9 ++++--- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/docs/Roll.md b/docs/Roll.md index 1b5c6127..fb4f1eac 100644 --- a/docs/Roll.md +++ b/docs/Roll.md @@ -66,9 +66,15 @@ As an addendum to custom dice, if a list value is bracketed then it can be used Lists are limited to 50 items long currently (which is configurable). +## Complex Operations + +There are two operators that are more complex and have specific organisational requirements, that allow for a great deal of control in the program. + +If statements take an expression or list to check for falseyness, + ## Functions -Here are all the functions, what they take, and what they return. +Here are all the functions, what they take, and what they return. They are called with `name(arg1, arg2)`. ### Returns an Integer - abs (integer) - the absolute value of an integer diff --git a/src/Tablebot/Plugins/Roll/Dice.hs b/src/Tablebot/Plugins/Roll/Dice.hs index 66a995c1..c8f1dd9f 100644 --- a/src/Tablebot/Plugins/Roll/Dice.hs +++ b/src/Tablebot/Plugins/Roll/Dice.hs @@ -17,27 +17,38 @@ -- - DiceEval - methods for evaluating elements from DiceData -- -- Below is the regex representing the parsing for the expressions, and --- explanations for each component +-- explanations for each component. It's not 100% accurate to the actual data +-- representation, but it's close enough that you can start reading `DiceData`, +-- which is the canonical representation of the AST, and then DiceParsing. -- --- If there is a gap between terms, any number of spaces (including none) is valid, barring in lstv, dice, die, dopr, ords; spaces are added manually in those. +-- If there is a gap between terms, any number of spaces (including none) is +-- valid, barring in lstv, dice, die, dopr, ords, funcBasics, misc; spaces are +-- added manually in those. -- --- lstv - nbse "#" base | funcBasics | lstb +-- prog - stat* (lstv | expr) +-- stat - (lstv | expr) ";" +-- misc - ifst | lets +-- ifst - "if" spc1 (lstv | expr) spc1 "then" spc1 (lstv | expr) spc1 "else" spc1 (lstv | expr) +-- lets - "let" spc1 ("l_" name spcs "=" spcs lstv | name spcs "=" spcs expr) +-- lstv - nbse "#" base | funcBasics | lstb | name | misc -- lstb - "{" expr ("," expr)* "}" | "(" lstv ")" --- expr - term ([+-] expr)? +-- expr - term ([+-] expr)? | misc -- term - nega ([*/] term)? -- nega - "-" expo | expo -- expo - func "^" expo | func -- func - funcBasics | base --- base - dice | nbse +-- base - dice | nbse | name -- nbse - "(" expr ")" | [0-9]+ -- dice - base die dopr? --- die - "d" "!"? (bse | lstb) +-- die - "d" "!"? (base | lstb) -- dopr - dopo+ -- dopo - "!"? (("rr" | "ro") ords | ("k"|"d") (("l" | "h") nbse | "w" ords)) -- ords - ("/=" | "<=" | ">=" | "<" | "=" | ">") nbase -- spcs - " "* +-- spc1 - " "+ -- argv - lstv | expr --- funcBasics - {some string identifier} "(" (argv ("," argv)*)? ")" +-- funcBasics - {some string identifier} "(" spcs (argv (spcs "," spcs argv)*)? spcs ")" +-- name - [a-z_]* -- -- lstv (ListValues) - representing all possible list values (basic list values, functions that return lists, and values which are lists of length N that consist of `Base`s) -- lstb (ListValuesBase) - representing some basic list values (those that can be used in dice expressions, such as manually created lists and bracketed `ListValues`) @@ -55,7 +66,7 @@ -- ords (AdvancedOrdering and NumBase) - representing a more complex ordering operation than a basic `Ordering`, when compared to a `NumBase` -- argv (ArgValue) - representing an argument to a function -- funcBasics - a generic regex representation for a general function parser -module Tablebot.Plugins.Roll.Dice (evalProgram, evalInteger, evalList, ListValues (..), defaultRoll, PrettyShow (prettyShow), integerFunctionsList, listFunctionsList, Converter (promote)) where +module Tablebot.Plugins.Roll.Dice (evalProgram, evalInteger, evalList, ListValues (..), defaultRoll, PrettyShow (prettyShow), integerFunctionsList, listFunctionsList, maximumListLength, maximumRNG, Converter (promote)) where import Tablebot.Plugins.Roll.Dice.DiceData ( Converter (promote), @@ -64,7 +75,7 @@ import Tablebot.Plugins.Roll.Dice.DiceData ListValues (..), NumBase (Value), ) -import Tablebot.Plugins.Roll.Dice.DiceEval (PrettyShow (prettyShow), evalInteger, evalList, evalProgram) +import Tablebot.Plugins.Roll.Dice.DiceEval (PrettyShow (prettyShow), evalInteger, evalList, evalProgram, maximumListLength, maximumRNG) import Tablebot.Plugins.Roll.Dice.DiceFunctions (integerFunctionsList, listFunctionsList) import Tablebot.Plugins.Roll.Dice.DiceParsing () diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index c14d10f2..d006aa20 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs @@ -8,7 +8,7 @@ -- -- Functions, type classes, and other utilities to evaluate dice values and -- expressions. -module Tablebot.Plugins.Roll.Dice.DiceEval (PrettyShow (prettyShow), evalProgram, evalList, evalInteger, evaluationException, propagateException) where +module Tablebot.Plugins.Roll.Dice.DiceEval (PrettyShow (prettyShow), evalProgram, evalList, evalInteger, evaluationException, propagateException, maximumRNG, maximumListLength) where import Control.Monad (when) import Control.Monad.Exception (MonadException) diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 3d945924..6d40d043 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -21,7 +21,6 @@ import Discord.Types (Message (messageAuthor, messageChannel)) import System.Timeout (timeout) import Tablebot.Plugins.Roll.Dice import Tablebot.Plugins.Roll.Dice.DiceData -import Tablebot.Plugins.Roll.Dice.DiceEval (evaluationException) import Tablebot.Plugins.Roll.Dice.DiceStats (getStats, rangeExpr) import Tablebot.Plugins.Roll.Dice.DiceStatsBase (distributionByteString) import Tablebot.Utility @@ -40,7 +39,7 @@ rollDice' e' t m = do -- liftIO $ putStrLn (unpack $ prettyShow e) maybemsss <- liftIO $ timeout 1000000 $ evalProgram e case maybemsss of - Nothing -> evaluationException "Could not process expression in one second" [] + Nothing -> throwBot (EvaluationException "Could not process expression in one second" []) Just (vs, ss) -> do let msg = makeMsg vs ss if countFormatting msg < 199 @@ -116,7 +115,11 @@ Given an expression, evaluate the expression. Can roll inline using |] ++ "`[|to roll|]`." ++ [r| Can use `r` instead of `roll`. -This supports addition, subtraction, multiplication, integer division, exponentiation, parentheses, dice of arbitrary size, dice with custom sides, rerolling dice once on a condition, rerolling dice indefinitely on a condition, keeping or dropping the highest or lowest dice, keeping or dropping dice based on a condition, operating on lists (which have a maximum, configurable size of 50), and using functions like |] +This supports addition, subtraction, multiplication, integer division, exponentiation, parentheses, rolling dice of arbitrary size (up to |] + ++ show maximumRNG + ++ [r| RNG calls), dice with custom sides, rerolling dice once on a condition, rerolling dice indefinitely on a condition, keeping or dropping the highest or lowest dice, keeping or dropping dice based on a condition, operating on lists (which have a maximum size of |] + ++ show maximumListLength + ++ [r|), if statements, let statements, and using functions like |] ++ unpack (intercalate ", " integerFunctionsList) ++ [r| (which return integers), or functions like |] ++ unpack (intercalate ", " listFunctionsList) From ab8272b86bf7d26bff8f2d5eb38c39b82d3f2d4f Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 10 Feb 2022 20:09:39 +0000 Subject: [PATCH 43/96] reduced if's dev space to only work with Expr --- src/Tablebot/Plugins/Roll/Dice.hs | 13 ++++--- src/Tablebot/Plugins/Roll/Dice/DiceData.hs | 8 ++--- src/Tablebot/Plugins/Roll/Dice/DiceEval.hs | 36 ++++++------------- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 4 +-- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 34 +++++++++--------- 5 files changed, 39 insertions(+), 56 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice.hs b/src/Tablebot/Plugins/Roll/Dice.hs index c8f1dd9f..1dd7cb28 100644 --- a/src/Tablebot/Plugins/Roll/Dice.hs +++ b/src/Tablebot/Plugins/Roll/Dice.hs @@ -28,8 +28,8 @@ -- prog - stat* (lstv | expr) -- stat - (lstv | expr) ";" -- misc - ifst | lets --- ifst - "if" spc1 (lstv | expr) spc1 "then" spc1 (lstv | expr) spc1 "else" spc1 (lstv | expr) --- lets - "let" spc1 ("l_" name spcs "=" spcs lstv | name spcs "=" spcs expr) +-- ifst - "if" spc1 expr spc1 "then" spc1 (lstv | expr) spc1 "else" spc1 (lstv | expr) +-- lets - "let" spc1 "!"? ("l_" name spcs "=" spcs lstv | name spcs "=" spcs expr) -- lstv - nbse "#" base | funcBasics | lstb | name | misc -- lstb - "{" expr ("," expr)* "}" | "(" lstv ")" -- expr - term ([+-] expr)? | misc @@ -50,9 +50,14 @@ -- funcBasics - {some string identifier} "(" spcs (argv (spcs "," spcs argv)*)? spcs ")" -- name - [a-z_]* -- --- lstv (ListValues) - representing all possible list values (basic list values, functions that return lists, and values which are lists of length N that consist of `Base`s) +-- prog (Program) - representing a complete program - a series of statements and a value to output at the end. +-- stat (Statement) - representing a single statement - an expression or list value +-- misc (MiscData) - either an if or a let +-- ifst (If) - representing one of two values depending on the outcome of an expression +-- lets (Let) - setting a variable to a certain value +-- lstv (ListValues) - representing all possible list values (basic list values, functions that return lists, and values which are lists of length N that consist of `Base`s, as well as a MiscData value) -- lstb (ListValuesBase) - representing some basic list values (those that can be used in dice expressions, such as manually created lists and bracketed `ListValues`) --- expr (Expr) - representing addition, subtraction, or a single `Term` value +-- expr (Expr) - representing addition, subtraction, or a single `Term` value, or a MiscData value -- term (Term) - representing multiplication, division, or a single `Negation` value -- nega (Negation) - representing a negation, or a single `Expo` value -- expo (Expo) - representing exponentiation or a single `Func` value diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs index 1b5d0201..3c342e0b 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs @@ -25,14 +25,10 @@ data Let a = Let {letName :: Text, letValue :: a} | LetLazy {letName :: Text, le -- | If the first value is truthy (non-zero or a non-empty list) then return -- the `thenValue`, else return the `elseValue`. -data If a b = If {ifCond :: a, thenValue :: b, elseValue :: b} deriving (Show) - -type IfExpr b = If Expr b - -type IfList b = If ListValues b +data If b = If {ifCond :: Expr, thenValue :: b, elseValue :: b} deriving (Show) -- | Either an If or a Let that returns a `b`. -data MiscData b = MiscIfExpr (IfExpr b) | MiscIfList (IfList b) | MiscLet (Let b) deriving (Show) +data MiscData b = MiscIf (If b) | MiscLet (Let b) deriving (Show) -- | An expression is just an Expr or a ListValues with a semicolon on the end. -- diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index d006aa20..9a2640ab 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs @@ -205,8 +205,7 @@ instance IOEvalList ListValuesBase where instance IOEvalList ListValuesMisc where evalShowL' rngCount (MiscLet l) = evalShowL rngCount l - evalShowL' rngCount (MiscIfExpr l) = evalShowL rngCount l - evalShowL' rngCount (MiscIfList l) = evalShowL rngCount l + evalShowL' rngCount (MiscIf l) = evalShowL rngCount l -- | This type class gives a function which evaluates the value to an integer -- and a string. @@ -390,8 +389,7 @@ binOpHelp rngCount a b opS op = do instance IOEval ExprMisc where evalShow' rngCount (MiscLet l) = evalShow rngCount l - evalShow' rngCount (MiscIfExpr l) = evalShow rngCount l - evalShow' rngCount (MiscIfList l) = evalShow rngCount l + evalShow' rngCount (MiscIf l) = evalShow rngCount l instance IOEval Expr where evalShow' rngCount (NoExpr t) = evalShow rngCount t @@ -468,33 +466,20 @@ evalStatement ps (StatementListValues l) = evalShowStatement l >>= \(_, t, ps') evalShowStatement (ListValuesMisc (MiscLet l'@(LetLazy t a))) = return ([], Just (prettyShow l'), addVariable ps t (Left a)) evalShowStatement l' = evalShowL ps l' -class GetTruth a where - getTruthy :: ProgramState -> a -> IO (Bool, ProgramState) - -instance GetTruth Expr where - getTruthy ps a = do - (i, _, ps') <- evalShow ps a - return (i /= 0, ps') - -instance GetTruth ListValues where - getTruthy ps a = do - (i, _, ps') <- evalShowL ps a - return (not $ null i, ps') - -instance GetTruth a => IOEval (If a Expr) where +instance IOEval (If Expr) where evalShow' ps if'@(If b t e) = do - (i, ps') <- getTruthy ps b + (i, _, ps') <- evalShow ps b (i', _, ps'') <- - if i + if i /= 0 then evalShow ps' t else evalShow ps' e return (i', prettyShow if', ps'') -instance GetTruth a => IOEvalList (If a ListValues) where +instance IOEvalList (If ListValues) where evalShowL' ps if'@(If b t e) = do - (i, ps') <- getTruthy ps b + (i, _, ps') <- evalShow ps b (i', _, ps'') <- - if i + if i /= 0 then evalShowL ps' t else evalShowL ps' e return (i', Just $ prettyShow if', ps'') @@ -524,8 +509,7 @@ instance PrettyShow ListValuesBase where instance PrettyShow a => PrettyShow (MiscData a) where prettyShow (MiscLet l) = prettyShow l - prettyShow (MiscIfExpr l) = prettyShow l - prettyShow (MiscIfList l) = prettyShow l + prettyShow (MiscIf l) = prettyShow l instance PrettyShow Expr where prettyShow (Add t e) = prettyShow t <> " + " <> prettyShow e @@ -591,7 +575,7 @@ instance (PrettyShow a) => PrettyShow (Let a) where prettyShow (Let t a) = "let " <> t <> " = " <> prettyShow a prettyShow (LetLazy t a) = "let !" <> t <> " = " <> prettyShow a -instance (PrettyShow a, PrettyShow b) => PrettyShow (If a b) where +instance (PrettyShow b) => PrettyShow (If b) where prettyShow (If b t e) = "if " <> prettyShow b <> " then " <> prettyShow t <> " else " <> prettyShow e instance PrettyShow Statement where diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index c3fe643f..aa6a2e2c 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -120,7 +120,7 @@ instance CanParse ListValuesBase where binOpParseHelp :: (CanParse a) => Char -> (a -> a) -> Parser a binOpParseHelp c con = try (skipSpace *> char c) *> skipSpace *> (con <$> pars) -instance (CanParse a, CanParse b) => CanParse (If a b) where +instance (CanParse b) => CanParse (If b) where pars = do a <- string "if" *> skipSpace1 *> pars <* skipSpace1 t <- string "then" *> skipSpace1 *> pars <* skipSpace1 @@ -128,7 +128,7 @@ instance (CanParse a, CanParse b) => CanParse (If a b) where return $ If a t e instance CanParse a => CanParse (MiscData a) where - pars = (MiscLet <$> pars) <|> (MiscIfExpr <$> pars) <|> (MiscIfList <$> pars) + pars = (MiscLet <$> pars) <|> (MiscIf <$> pars) instance CanParse Expr where pars = diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index c9eeaa00..9bd29f6c 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -86,15 +86,13 @@ class PrettyShow a => Range a where instance (Range a) => Range (MiscData a) where range' (MiscLet l) = range l - range' (MiscIfExpr i) = rangeIfExpr range i - range' (MiscIfList i) = rangeIfList range i + range' (MiscIf i) = rangeIfExpr range i instance (RangeList a) => RangeList (MiscData a) where rangeList' (MiscLet l) = rangeList l - rangeList' (MiscIfExpr i) = rangeIfExpr rangeList i - rangeList' (MiscIfList i) = rangeIfList rangeList i + rangeList' (MiscIf i) = rangeIfExpr rangeList i -rangeIfExpr :: (MonadException m, Ord b) => (a -> m (D.Experiment b)) -> If Expr a -> m (D.Experiment b) +rangeIfExpr :: (MonadException m, Ord b) => (a -> m (D.Experiment b)) -> If a -> m (D.Experiment b) rangeIfExpr func (If b t f) = do b' <- range b let mp = toMap $ run b' @@ -108,19 +106,19 @@ rangeIfExpr func (If b t f) = do b'' <- b' if b'' /= 0 then t' else f' -rangeIfList :: (MonadException m, Ord b) => (a -> m (D.Experiment b)) -> If ListValues a -> m (D.Experiment b) -rangeIfList func (If b t f) = do - b' <- rangeList b - let mp = toMap $ run b' - canBeFalse = M.member [] mp - canBeTrue = M.null $ M.filterWithKey (\k _ -> k /= []) mp - emptyExp = from $ D.fromList @_ @Integer [] - t' <- if canBeTrue then func t else return emptyExp - f' <- if canBeFalse then func f else return emptyExp - return $ - do - b'' <- b' - if b'' /= [] then t' else f' +-- rangeIfList :: (MonadException m, Ord b) => (a -> m (D.Experiment b)) -> If ListValues a -> m (D.Experiment b) +-- rangeIfList func (If b t f) = do +-- b' <- rangeList b +-- let mp = toMap $ run b' +-- canBeFalse = M.member [] mp +-- canBeTrue = M.null $ M.filterWithKey (\k _ -> k /= []) mp +-- emptyExp = from $ D.fromList @_ @Integer [] +-- t' <- if canBeTrue then func t else return emptyExp +-- f' <- if canBeFalse then func f else return emptyExp +-- return $ +-- do +-- b'' <- b' +-- if b'' /= [] then t' else f' instance (Range a) => Range (Let a) where range' (Let _ a) = range a From 7e853c7dd1eec8cb8b771ad65c3a05428e859122 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 10 Feb 2022 20:49:04 +0000 Subject: [PATCH 44/96] describing more complex stuff in roll.md --- docs/Roll.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/Roll.md b/docs/Roll.md index fb4f1eac..6f1b174c 100644 --- a/docs/Roll.md +++ b/docs/Roll.md @@ -68,9 +68,15 @@ Lists are limited to 50 items long currently (which is configurable). ## Complex Operations -There are two operators that are more complex and have specific organisational requirements, that allow for a great deal of control in the program. +There are two operators that are more complex and have specific organisational requirements, that allow for a great deal of control in the program. With them comes more complex structures for expressions as a whole. -If statements take an expression or list to check for falseyness, +If statements take an expression, and then two either integer values or list values. If the expression is non-zero, the first value is returned. If the expression is zero, the second value is returned. The syntax for it is `if expression then t else f`, where `expression` is an integer value, and `t` and `f` are both either integer values or list values. + +Let statements take a name and either an integer value or a list, and set a variable with that name to that value. If the let statement is lazy (with an exclamation mark before the variable name) instead, the value is recalculated every time the variable is used. A let statement returns the value on the left side. To create and use list variables, they must be prepended with `l_`. + +As well as normal expressions, statements can be used now. A statement is an integer value or list value followed by a semicolon. Below is a couple example programs. One small quirk is that a lazy let expression won't be evaluated until the variable is first used. + +- Get the minimum, maximum, and average value of a random list `let l_list = (2d6)#3d6 ; {minimum(l_list), maximum(l_list), sum(l_list)/length(l_list)}` ## Functions From e623ddc4a57203b6463d46b2bf549ca97698b508 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 10 Feb 2022 22:33:04 +0000 Subject: [PATCH 45/96] updated docs to include at least some info about let, if, and programs as a whole. --- docs/Roll.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/Roll.md b/docs/Roll.md index 6f1b174c..875c718d 100644 --- a/docs/Roll.md +++ b/docs/Roll.md @@ -70,13 +70,16 @@ Lists are limited to 50 items long currently (which is configurable). There are two operators that are more complex and have specific organisational requirements, that allow for a great deal of control in the program. With them comes more complex structures for expressions as a whole. -If statements take an expression, and then two either integer values or list values. If the expression is non-zero, the first value is returned. If the expression is zero, the second value is returned. The syntax for it is `if expression then t else f`, where `expression` is an integer value, and `t` and `f` are both either integer values or list values. +If statements take an expression, and then two either integer values or list values. If the expression is non-zero, the first value is returned. If the expression is zero, the second value is returned. The syntax for it is `if expression then t else f`, where `expression` is an integer value, and `t` and `f` are both either integer values or list values. Only one of `t` or `f` is ever evaluated. Let statements take a name and either an integer value or a list, and set a variable with that name to that value. If the let statement is lazy (with an exclamation mark before the variable name) instead, the value is recalculated every time the variable is used. A let statement returns the value on the left side. To create and use list variables, they must be prepended with `l_`. -As well as normal expressions, statements can be used now. A statement is an integer value or list value followed by a semicolon. Below is a couple example programs. One small quirk is that a lazy let expression won't be evaluated until the variable is first used. +As well as normal expressions, statements can be used now. A statement is an integer value or list value followed by a semicolon. Below are a couple example programs. One small quirk is that a lazy let expression won't be evaluated until the variable is first used. -- Get the minimum, maximum, and average value of a random list `let l_list = (2d6)#3d6 ; {minimum(l_list), maximum(l_list), sum(l_list)/length(l_list)}` +- `let l_list = (2d6)#3d6 ; {length(l_list), minimum(l_list), maximum(l_list), sum(l_list)/length(l_list)}` + - Get the length, minimum, maximum, and average value of a random list +- `let !k = 1d20; let t = k; let !t_iseven = if mod(t,2) then 0 else 1; if t_iseven then k * t else t` + - Create a lazy variable k. Evaluate it into a variable t. Check whether t is even, and place in a variable. Depending on whether t is even or not, either output another random number times by t, or just output t. ## Functions From df8ab31bd67b03bcf0ff5e7cbec3eaf26f668638 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 10 Feb 2022 22:34:52 +0000 Subject: [PATCH 46/96] more let description --- docs/Roll.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Roll.md b/docs/Roll.md index 875c718d..e74f138f 100644 --- a/docs/Roll.md +++ b/docs/Roll.md @@ -72,7 +72,7 @@ There are two operators that are more complex and have specific organisational r If statements take an expression, and then two either integer values or list values. If the expression is non-zero, the first value is returned. If the expression is zero, the second value is returned. The syntax for it is `if expression then t else f`, where `expression` is an integer value, and `t` and `f` are both either integer values or list values. Only one of `t` or `f` is ever evaluated. -Let statements take a name and either an integer value or a list, and set a variable with that name to that value. If the let statement is lazy (with an exclamation mark before the variable name) instead, the value is recalculated every time the variable is used. A let statement returns the value on the left side. To create and use list variables, they must be prepended with `l_`. +Let statements take a name and either an integer value or a list, and set a variable with that name to that value. If the let statement is lazy (with an exclamation mark before the variable name) instead, the value is recalculated every time the variable is used. A let statement returns the value on the left side. To create and use list variables, they must be prepended with `l_`. The syntax can be something like `let name = value`, `let !name = value`, or `let l_name = value`, or so on. As well as normal expressions, statements can be used now. A statement is an integer value or list value followed by a semicolon. Below are a couple example programs. One small quirk is that a lazy let expression won't be evaluated until the variable is first used. From aa75919ce8395109d2fcff59658191bb5b020793 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 11 Feb 2022 12:08:12 +0000 Subject: [PATCH 47/96] updated functions list and added some more functions --- docs/Roll.md | 3 +++ src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/docs/Roll.md b/docs/Roll.md index e74f138f..71a6c8ad 100644 --- a/docs/Roll.md +++ b/docs/Roll.md @@ -106,6 +106,9 @@ Here are all the functions, what they take, and what they return. They are calle - take (integer, list) - take the first `n` values from a list, where `n` is the integer given - between (integer, integer) - generate a list between the two given integers (inclusive) - concat (list, list) - concatenate two lists together +- replicate (integer, integer) - create a list of length the first integer, consisting of elements of only the second element +- set (integer, integer, list) - set the item at the index of the first integer to the value of the second integer in the given list +- insert (integer, integer, list) - insert the item at the index of the first integer to the value of the second integer in the given list # Statistics diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs index 05f9ba56..a16e5ce7 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs @@ -76,6 +76,8 @@ listFunctionsList = M.keys listFunctions -- each function that returns an integer. listFunctions' :: [FuncInfoBase [Integer]] listFunctions' = + funcInfoInsert : + constructFuncInfo "prepend" (:) : constructFuncInfo "replicate" (genericReplicate @Integer) : funcInfoSet : constructFuncInfo "concat" (++) : @@ -103,6 +105,14 @@ funcInfoSet = FuncInfo "set" [ATInteger, ATInteger, ATIntegerList] ATIntegerList | otherwise = return $ genericTake i js ++ j : genericDrop (i + 1) js fiSet is = throwBot $ EvaluationException ("incorrect number/type of arguments. expected 3, got " ++ show (length is)) [] +funcInfoInsert :: FuncInfoBase [Integer] +funcInfoInsert = FuncInfo "insert" [ATInteger, ATInteger, ATIntegerList] ATIntegerList fiSet + where + fiSet (LIInteger i : LIInteger j : [LIList js]) + | i < 0 || i >= genericLength js = throwBot $ EvaluationException ("index out of range: " ++ show i) [] + | otherwise = return $ genericTake i js ++ j : genericDrop i js + fiSet is = throwBot $ EvaluationException ("incorrect number/type of arguments. expected 3, got " ++ show (length is)) [] + -- | A data structure to contain the information about a given function, -- including types, the function name, and the function itself. data FuncInfoBase j = FuncInfo {funcInfoName :: Text, funcInfoParameters :: [ArgType], funcReturnType :: ArgType, funcInfoFunc :: forall m. (MonadException m) => [ListInteger] -> m j} From 7cfa0208d5504254e4af7f8d9a38081f38e3ff06 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 11 Feb 2022 12:46:59 +0000 Subject: [PATCH 48/96] spacing issues --- docs/Roll.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Roll.md b/docs/Roll.md index 71a6c8ad..e9942b5d 100644 --- a/docs/Roll.md +++ b/docs/Roll.md @@ -22,7 +22,7 @@ As well as the arithmetic operators above, dice can be rolled, hence the name of The basic format for this is `dX` where X is some number, meaning a single die of size X. Multiple dice can be rolled using `YdX`, meaning that Y dice are rolled of size X. Parentheses can be used for both Y and X in this case. If Y is greater than a number determined by the bot owner (150 by default), the roll will not be executed. This is the same number that governs the total amount of RNG calls allowed within a command's execution. -In addition to the above, there is syntax for rolling dice with arbitrary sides - `d{4,7,19,-5}`. This results in a die that is equally likely to result in four, seven, nineteen, or minus five. These numbers could be any expression instead. +In addition to the above, there is syntax for rolling dice with arbitrary sides - `d{4, 7, 19, -5}`. This results in a die that is equally likely to result in four, seven, nineteen, or minus five. These numbers could be any expression instead. There is support for stacking dice. This means that if you write `2d4d5d6`, it will be parsed and executed as `((2d4)d5)d6`. Operations can be applied to the dice in this stack. @@ -60,7 +60,7 @@ With the introduction of this notation, it is worth noting that the normal (with ## Lists -As well as simple expressions, basic list expressions can be formed. You can form a basic list using `{e,f,g}`, where `e`, `f`, and `g` are expressions as seen before. Additionally, by using `N#YdX` syntax, you can roll `N` amount of dice following `YdX`. +As well as simple expressions, basic list expressions can be formed. You can form a basic list using `{e, f, g}`, where `e`, `f`, and `g` are expressions as seen before. Additionally, by using `N#YdX` syntax, you can roll `N` amount of dice following `YdX`. As an addendum to custom dice, if a list value is bracketed then it can be used in custom dice. For example, `5d(4#4d6)` rolls five dice, whose sides are determined by rolling 4d6 4 times. Do note that laziness still applies here, meaning that the RNG cap can be very quickly reached. @@ -76,9 +76,9 @@ Let statements take a name and either an integer value or a list, and set a vari As well as normal expressions, statements can be used now. A statement is an integer value or list value followed by a semicolon. Below are a couple example programs. One small quirk is that a lazy let expression won't be evaluated until the variable is first used. -- `let l_list = (2d6)#3d6 ; {length(l_list), minimum(l_list), maximum(l_list), sum(l_list)/length(l_list)}` +- `let l_list = (2d6)#3d6; {length(l_list), minimum(l_list), maximum(l_list), sum(l_list)/length(l_list)}` - Get the length, minimum, maximum, and average value of a random list -- `let !k = 1d20; let t = k; let !t_iseven = if mod(t,2) then 0 else 1; if t_iseven then k * t else t` +- `let !k = 1d20; let t = k; let !t_iseven = if mod(t, 2) then 0 else 1; if t_iseven then k * t else t` - Create a lazy variable k. Evaluate it into a variable t. Check whether t is even, and place in a variable. Depending on whether t is even or not, either output another random number times by t, or just output t. ## Functions From db3adef26fb976770952fc366dcb9cc26035c07e Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sun, 13 Feb 2022 14:48:17 +0000 Subject: [PATCH 49/96] adjusting comments --- src/Tablebot/Plugins/Roll/Dice.hs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Tablebot/Plugins/Roll/Dice.hs b/src/Tablebot/Plugins/Roll/Dice.hs index 1dd7cb28..d9d2c3e6 100644 --- a/src/Tablebot/Plugins/Roll/Dice.hs +++ b/src/Tablebot/Plugins/Roll/Dice.hs @@ -9,12 +9,16 @@ -- This plugin contains the neccessary parsers and stucture to get the AST for an -- expression that contains dice, as well as evaluate that expression. -- --- The behind the scenes for the dice is split into four files. +-- The behind the scenes for the dice is split into six files, two of which +-- are for generating dice statistics. -- - DiceData - the data structures for the AST for dice -- - DiceFunctions - functionality for dealing with functions and processing -- them -- - DiceParsing - parsers for getting all the DiceData items -- - DiceEval - methods for evaluating elements from DiceData +-- - DiceStats - filling the type classes and function needed to generate +-- statistics on dice +-- - DiceStatsBase - functions to process completed dice ranges -- -- Below is the regex representing the parsing for the expressions, and -- explanations for each component. It's not 100% accurate to the actual data From 7d8002cd39bf92aeafd6fa57544fda0f60bc4681 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sun, 13 Feb 2022 14:50:57 +0000 Subject: [PATCH 50/96] added full stop --- docs/Roll.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Roll.md b/docs/Roll.md index e9942b5d..393bc0d9 100644 --- a/docs/Roll.md +++ b/docs/Roll.md @@ -77,7 +77,7 @@ Let statements take a name and either an integer value or a list, and set a vari As well as normal expressions, statements can be used now. A statement is an integer value or list value followed by a semicolon. Below are a couple example programs. One small quirk is that a lazy let expression won't be evaluated until the variable is first used. - `let l_list = (2d6)#3d6; {length(l_list), minimum(l_list), maximum(l_list), sum(l_list)/length(l_list)}` - - Get the length, minimum, maximum, and average value of a random list + - Get the length, minimum, maximum, and average value of a random list. - `let !k = 1d20; let t = k; let !t_iseven = if mod(t, 2) then 0 else 1; if t_iseven then k * t else t` - Create a lazy variable k. Evaluate it into a variable t. Check whether t is even, and place in a variable. Depending on whether t is even or not, either output another random number times by t, or just output t. From 042f51a166b50c9f5ed6cf06f62b544dcea1a66e Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sun, 13 Feb 2022 15:10:38 +0000 Subject: [PATCH 51/96] modified language --- docs/Roll.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Roll.md b/docs/Roll.md index 393bc0d9..f95a8fa5 100644 --- a/docs/Roll.md +++ b/docs/Roll.md @@ -74,7 +74,7 @@ If statements take an expression, and then two either integer values or list val Let statements take a name and either an integer value or a list, and set a variable with that name to that value. If the let statement is lazy (with an exclamation mark before the variable name) instead, the value is recalculated every time the variable is used. A let statement returns the value on the left side. To create and use list variables, they must be prepended with `l_`. The syntax can be something like `let name = value`, `let !name = value`, or `let l_name = value`, or so on. -As well as normal expressions, statements can be used now. A statement is an integer value or list value followed by a semicolon. Below are a couple example programs. One small quirk is that a lazy let expression won't be evaluated until the variable is first used. +As well as normal expressions, statements can be used now. A statement is an integer value or list value followed by a semicolon. Below are a couple example programs. One quality of life feature is that a lazy let expression won't be evaluated until the variable is first used. - `let l_list = (2d6)#3d6; {length(l_list), minimum(l_list), maximum(l_list), sum(l_list)/length(l_list)}` - Get the length, minimum, maximum, and average value of a random list. From 3f3e18080ade735db3e2e5917fe244cdc111e5ff Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sun, 13 Feb 2022 15:25:50 +0000 Subject: [PATCH 52/96] removed old comment --- src/Tablebot/Plugins/Roll/Plugin.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 6d40d043..64f80b0a 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -36,7 +36,6 @@ import Text.RawString.QQ (r) rollDice' :: Maybe Program -> Maybe (Quoted Text) -> Message -> DatabaseDiscord () rollDice' e' t m = do let e = fromMaybe (Program [] (Right defaultRoll)) e' - -- liftIO $ putStrLn (unpack $ prettyShow e) maybemsss <- liftIO $ timeout 1000000 $ evalProgram e case maybemsss of Nothing -> throwBot (EvaluationException "Could not process expression in one second" []) From 0462346f3d23b49312ecee0ebfb16866c2b6c326 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 2 Mar 2022 22:45:27 +0000 Subject: [PATCH 53/96] updated roll link --- src/Tablebot/Plugins/Roll/Plugin.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 7d49b0a7..318946f9 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -120,7 +120,7 @@ This supports addition, subtraction, multiplication, integer division, exponenti ++ unpack (intercalate ", " listFunctionsList) ++ [r| (which return lists). -To see a full list of uses, options and limitations, please go to . +To see a full list of uses, options and limitations, please go to . *Usage:* - `roll 1d20` -> rolls a twenty sided die and returns the outcome From 5a6a524464c2df3859a3e4252a0efcd96fbe4239 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sun, 20 Mar 2022 10:43:38 +0000 Subject: [PATCH 54/96] updated discord-haskell version and fixed hole --- src/Tablebot.hs | 2 +- stack.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tablebot.hs b/src/Tablebot.hs index ecec3fdf..214eac98 100644 --- a/src/Tablebot.hs +++ b/src/Tablebot.hs @@ -106,7 +106,7 @@ runTablebot vinfo dToken prefix dbpath plugins = { updateStatusOptsSince = Nothing, updateStatusOptsGame = Just - ( Activity + ( def { activityName = "with dice. Prefix is `" <> prefix <> "`. Call `" <> prefix <> "help` for help", activityType = ActivityTypeGame, activityUrl = Nothing diff --git a/stack.yaml b/stack.yaml index 8e53008f..f06ea69c 100644 --- a/stack.yaml +++ b/stack.yaml @@ -42,7 +42,7 @@ packages: # allow-newer: true extra-deps: -- discord-haskell-1.12.0 +- discord-haskell-1.12.4 - emoji-0.1.0.2 - load-env-0.2.1.0 - megaparsec-9.0.1 From 2706ba480f2e4a168d80e07d154620d461f1c8b1 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sun, 20 Mar 2022 14:55:41 +0000 Subject: [PATCH 55/96] tablebot will now join threads whenever they are created --- src/Tablebot/Handler.hs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Tablebot/Handler.hs b/src/Tablebot/Handler.hs index c584ba94..38a98c95 100644 --- a/src/Tablebot/Handler.hs +++ b/src/Tablebot/Handler.hs @@ -18,7 +18,7 @@ module Tablebot.Handler where import Control.Concurrent (MVar, putMVar, takeMVar) -import Control.Monad (unless) +import Control.Monad (unless, void) import Control.Monad.Exception (MonadException (catch)) import Control.Monad.IO.Class (MonadIO (liftIO)) import Control.Monad.Reader (ReaderT, ask, lift, runReaderT) @@ -27,8 +27,9 @@ import Data.Map as M (fromList) import Data.Pool (Pool) import Data.Text (Text) import Database.Persist.Sqlite (SqlBackend, runSqlPool) -import Discord (Cache (cacheApplication), DiscordHandler, readCache) +import Discord (Cache (cacheApplication), DiscordHandler, readCache, restCall) import Discord.Interactions (ApplicationCommand (..), Interaction (..)) +import Discord.Requests (ChannelRequest (JoinThread)) import Discord.Types import System.Environment (getEnv) import Tablebot.Internal.Handler.Command (parseNewMessage) @@ -44,7 +45,7 @@ import Tablebot.Internal.Plugins (changeAction) import Tablebot.Internal.Types import Tablebot.Utility.Discord (createApplicationCommand, interactionResponseCustomMessage, removeApplicationCommandsNotInList, sendChannelEmbedMessage) import Tablebot.Utility.Exception (BotException, embedError) -import Tablebot.Utility.Types (MessageDetails (messageDetailsEmbeds), TablebotCache (cacheApplicationCommands), messageDetailsBasic) +import Tablebot.Utility.Types (MessageDetails (messageDetailsEmbeds), TablebotCache (cacheApplicationCommands), liftDiscord, messageDetailsBasic) import Text.Read (readMaybe) import UnliftIO.Concurrent ( ThreadId, @@ -79,6 +80,7 @@ eventHandler pl prefix = \case InteractionCreate i@InteractionApplicationCommand {} -> parseApplicationCommandRecv i `interactionErrorCatch` i InteractionCreate i@InteractionApplicationCommandAutocomplete {} -> parseApplicationCommandRecv i `interactionErrorCatch` i -- TODO: add application command autocomplete as an option + ThreadCreate c -> changeAction () $ void $ liftDiscord $ restCall $ JoinThread (channelId c) e -> parseOther (compiledOtherEvents pl) e where ifNotBot m = unless (userIsBot (messageAuthor m)) From 2ac3a1a2982b27996dd2754975bfd949806576f6 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sun, 27 Mar 2022 14:54:41 +0100 Subject: [PATCH 56/96] fixed documnetation slightly --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ca761ee..0985e0d9 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Create a `.env` file containing the following keys. Consult `.env.example` if yo * `EXEC_GROUP` (optional) - the group ID assigned to exec members. * `MODERATOR_GROUP` (optional) - the group ID assigned to moderator members. * `SUPERUSER_GROUP` (optional) - the group ID assigned to the superuser. Strongly recommended -* `SERVER_ID` (optional) - the id of the server the bot will mainly be deployed in. Application commands will be registered here. +* `SERVER_ID` - the id of the server the bot will mainly be deployed in. Application commands will be registered here. The three Group settings are optional, but without them any commands that require elevated permissions will not be able to be called when DEBUG is false. Users with the superuser group are able to run every command (including some dangerous From e7e4a35f8e70604fae0ffda618f80525cfa25768 Mon Sep 17 00:00:00 2001 From: Finnbar Keating Date: Sat, 2 Apr 2022 16:39:51 +0100 Subject: [PATCH 57/96] Made bot easier to import. * Moved setup out of Main.hs and into `runTablebotWithEnv`. * Allowed plugins to be chosen at startup. --- app/Main.hs | 36 ++-------------- src/Tablebot.hs | 41 ++++++++++++++---- src/Tablebot/Plugins.hs | 69 ++++++++++++++----------------- src/Tablebot/Plugins/Basic.hs | 21 +++------- src/Tablebot/Plugins/Cats.hs | 24 ++++------- src/Tablebot/Plugins/Dogs.hs | 24 ++++------- src/Tablebot/Plugins/Flip.hs | 5 ++- src/Tablebot/Plugins/Fox.hs | 24 ++++------- src/Tablebot/Plugins/Netrunner.hs | 6 ++- src/Tablebot/Plugins/Ping.hs | 5 ++- src/Tablebot/Plugins/Quote.hs | 5 ++- src/Tablebot/Plugins/Reminder.hs | 8 ++-- src/Tablebot/Plugins/Roll.hs | 6 ++- src/Tablebot/Plugins/Say.hs | 5 ++- src/Tablebot/Plugins/Shibe.hs | 23 ++++------- src/Tablebot/Plugins/Suggest.hs | 5 ++- src/Tablebot/Plugins/Welcome.hs | 5 ++- src/Tablebot/Utility.hs | 4 ++ 18 files changed, 149 insertions(+), 167 deletions(-) diff --git a/app/Main.hs b/app/Main.hs index 5975eea9..2907271c 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -1,38 +1,8 @@ module Main where -import Control.Concurrent.MVar (MVar, newMVar, swapMVar) -import Control.Monad (forever, unless) -import Control.Monad.Extra -import Data.Maybe (fromMaybe) -import Data.Text (pack) -import Data.Text.Encoding (encodeUtf8) -import LoadEnv (loadEnv) -import Paths_tablebot (version) -import System.Environment (getEnv, lookupEnv) -import System.Exit (die) -import Tablebot (runTablebot) -import Tablebot.Internal.Administration -import Tablebot.Plugins (plugins) -import Tablebot.Utility.Types -import Text.Regex.PCRE +import Tablebot (runTablebotWithEnv) +import Tablebot.Plugins (allPlugins) -- @main@ runs forever. This allows bot reloading by fully shutting down the bot and letting it restart. main :: IO () -main = do - -- fetch the version info as soon after building to reduce the likelihood that it changes between build and run - gv <- gitVersion - let vInfo = VInfo gv version - rFlag <- newMVar Reload :: IO (MVar ShutdownReason) - whileM $ do - _ <- swapMVar rFlag Reload - loadEnv - dToken <- pack <$> getEnv "DISCORD_TOKEN" - unless (encodeUtf8 dToken =~ ("^[A-Za-z0-9_-]{24}[.][A-Za-z0-9_-]{6}[.][A-Za-z0-9_-]{27}$" :: String)) $ - die "Invalid token format. Please check it is a bot token" - prefix <- pack . fromMaybe "!" <$> lookupEnv "PREFIX" - dbpath <- getEnv "SQLITE_FILENAME" - runTablebot vInfo dToken prefix dbpath (plugins rFlag) - exit <- swapMVar rFlag Reload - restartAction exit - pure $ not (restartIsTerminal exit) - putStrLn "Tablebot closed" +main = runTablebotWithEnv allPlugins diff --git a/src/Tablebot.hs b/src/Tablebot.hs index 4338a95a..a55527a3 100644 --- a/src/Tablebot.hs +++ b/src/Tablebot.hs @@ -13,23 +13,20 @@ -- from "Tablebot.Plugins". module Tablebot ( runTablebot, + runTablebotWithEnv, ) where import Control.Concurrent - ( MVar, - ThreadId, - newEmptyMVar, - newMVar, - putMVar, - takeMVar, - ) +import Control.Monad.Extra import Control.Monad.IO.Class (MonadIO (liftIO)) import Control.Monad.Logger (NoLoggingT (runNoLoggingT)) import Control.Monad.Reader (runReaderT) import Control.Monad.Trans.Resource (runResourceT) import qualified Data.Map as M +import Data.Maybe (fromMaybe) import Data.Text (Text, pack) +import Data.Text.Encoding (encodeUtf8) import qualified Data.Text.IO as TIO (putStrLn) import Database.Persist.Sqlite ( createSqlitePool, @@ -38,12 +35,38 @@ import Database.Persist.Sqlite ) import Discord import Discord.Internal.Rest +import LoadEnv (loadEnv) +import Paths_tablebot (version) +import System.Environment (getEnv, lookupEnv) +import System.Exit (die) import Tablebot.Handler (eventHandler, killCron, runCron) -import Tablebot.Internal.Administration (adminMigration, currentBlacklist, removeBlacklisted) +import Tablebot.Internal.Administration import Tablebot.Internal.Plugins import Tablebot.Internal.Types +import Tablebot.Plugins (addAdministrationPlugin) import Tablebot.Utility import Tablebot.Utility.Help +import Text.Regex.PCRE ((=~)) + +runTablebotWithEnv :: [CompiledPlugin] -> IO () +runTablebotWithEnv plugins = do + -- fetch the version info as soon after building to reduce the likelihood that it changes between build and run + gv <- gitVersion + let vInfo = VInfo gv version + rFlag <- newMVar Reload :: IO (MVar ShutdownReason) + whileM $ do + _ <- swapMVar rFlag Reload + loadEnv + dToken <- pack <$> getEnv "DISCORD_TOKEN" + unless (encodeUtf8 dToken =~ ("^[A-Za-z0-9_-]{24}[.][A-Za-z0-9_-]{6}[.][A-Za-z0-9_-]{27}$" :: String)) $ + die "Invalid token format. Please check it is a bot token" + prefix <- pack . fromMaybe "!" <$> lookupEnv "PREFIX" + dbpath <- getEnv "SQLITE_FILENAME" + runTablebot vInfo dToken prefix dbpath (addAdministrationPlugin rFlag plugins) + exit <- swapMVar rFlag Reload + restartAction exit + pure $ not (restartIsTerminal exit) + putStrLn "Bot closed" -- | runTablebot @dToken@ @prefix@ @dbpath@ @plugins@ runs the bot using the -- given Discord API token @dToken@ and SQLite connection string @dbpath@. Only @@ -91,7 +114,7 @@ runTablebot vinfo dToken prefix dbpath plugins = -- (which can just happen due to databases being unavailable -- sometimes). runReaderT (mapM (runCron pool) (compiledCronJobs actions) >>= liftIO . putMVar mvar) cacheMVar - liftIO $ putStrLn "Tablebot lives!" + liftIO $ putStrLn "The bot lives!" sendCommand (UpdateStatus activityStatus), -- Kill every cron job in the mvar. discordOnEnd = takeMVar mvar >>= killCron diff --git a/src/Tablebot/Plugins.hs b/src/Tablebot/Plugins.hs index 56e72e3b..76e5f468 100644 --- a/src/Tablebot/Plugins.hs +++ b/src/Tablebot/Plugins.hs @@ -9,51 +9,46 @@ -- Here is a collection of existing plugins for Tablebot. If you add new plugins -- to the Plugins directory, include an import here. This means that users only -- need to import @Tablebot.Plugins@ to import individual plugins. -module Tablebot.Plugins - ( plugins, - ) -where +module Tablebot.Plugins where import Control.Concurrent.MVar (MVar) import Tablebot.Internal.Administration (ShutdownReason) import Tablebot.Internal.Plugins (compilePlugin) import Tablebot.Internal.Types (CompiledPlugin) import Tablebot.Plugins.Administration (administrationPlugin) -import Tablebot.Plugins.Basic (basicPlugin) -import Tablebot.Plugins.Cats (catPlugin) -import Tablebot.Plugins.Dogs (dogPlugin) -import Tablebot.Plugins.Flip (flipPlugin) -import Tablebot.Plugins.Fox (foxPlugin) -import Tablebot.Plugins.Netrunner (netrunnerPlugin) -import Tablebot.Plugins.Ping (pingPlugin) -import Tablebot.Plugins.Quote (quotePlugin) -import Tablebot.Plugins.Reminder (reminderPlugin) -import Tablebot.Plugins.Roll (rollPlugin) -import Tablebot.Plugins.Say (sayPlugin) -import Tablebot.Plugins.Shibe (shibePlugin) -import Tablebot.Plugins.Suggest (suggestPlugin) -import Tablebot.Plugins.Welcome (welcomePlugin) +import Tablebot.Plugins.Basic (basic) +import Tablebot.Plugins.Cats (cat) +import Tablebot.Plugins.Dogs (dog) +import Tablebot.Plugins.Flip (flips) +import Tablebot.Plugins.Fox (fox) +import Tablebot.Plugins.Netrunner (netrunner) +import Tablebot.Plugins.Ping (pingpong) +import Tablebot.Plugins.Quote (quotes) +import Tablebot.Plugins.Reminder (reminder) +import Tablebot.Plugins.Roll (roll) +import Tablebot.Plugins.Say (says) +import Tablebot.Plugins.Shibe (shibe) +import Tablebot.Plugins.Suggest (suggests) +import Tablebot.Plugins.Welcome (welcome) -- Use long list format to make additions and removals non-conflicting on git PRs -plugins :: MVar ShutdownReason -> [CompiledPlugin] -plugins rFlag = - addAdministrationPlugin - rFlag - [ compilePlugin pingPlugin, - compilePlugin basicPlugin, - compilePlugin catPlugin, - compilePlugin dogPlugin, - compilePlugin shibePlugin, - compilePlugin flipPlugin, - compilePlugin foxPlugin, - compilePlugin netrunnerPlugin, - compilePlugin quotePlugin, - compilePlugin reminderPlugin, - compilePlugin sayPlugin, - compilePlugin suggestPlugin, - compilePlugin rollPlugin, - compilePlugin welcomePlugin - ] +allPlugins :: [CompiledPlugin] +allPlugins = + [ pingpong, + basic, + cat, + dog, + shibe, + flips, + fox, + netrunner, + quotes, + reminder, + says, + suggests, + roll, + welcome + ] -- | @addAdministrationPlugin@ is needed to allow the administration plugin to be aware of the list of current plugins addAdministrationPlugin :: MVar ShutdownReason -> [CompiledPlugin] -> [CompiledPlugin] diff --git a/src/Tablebot/Plugins/Basic.hs b/src/Tablebot/Plugins/Basic.hs index bcfba4b4..9962b643 100644 --- a/src/Tablebot/Plugins/Basic.hs +++ b/src/Tablebot/Plugins/Basic.hs @@ -7,25 +7,13 @@ -- Portability : POSIX -- -- This is an example plugin which responds to certain calls with specific responses. -module Tablebot.Plugins.Basic (basicPlugin) where +module Tablebot.Plugins.Basic (basic) where import Data.Text as T (Text, toTitle) -import Discord.Internal.Rest (Message) +import Discord.Types (Message) +import Tablebot.Utility import Tablebot.Utility.Discord (sendMessage) import Tablebot.Utility.SmartParser (parseComm) -import Tablebot.Utility.Types - ( Command, - DatabaseDiscord, - EnvCommand (Command), - EnvInlineCommand (InlineCommand), - EnvPlugin (commands, inlineCommands), - HelpPage (HelpPage), - InlineCommand, - Plugin, - RequiredPermission (None), - helpPages, - plug, - ) import Text.Megaparsec (anySingle, skipManyTill) import Text.Megaparsec.Char (string') @@ -100,3 +88,6 @@ basicPlugin = helpPages = map baseHelp basicCommands, inlineCommands = map baseInlineCommand basicInlineCommands } + +basic :: CompiledPlugin +basic = compilePlugin basicPlugin diff --git a/src/Tablebot/Plugins/Cats.hs b/src/Tablebot/Plugins/Cats.hs index 2f373c94..c666b9ff 100644 --- a/src/Tablebot/Plugins/Cats.hs +++ b/src/Tablebot/Plugins/Cats.hs @@ -7,7 +7,7 @@ -- Portability : POSIX -- -- This is an example plugin which just responds with a cat photo to a .cat call -module Tablebot.Plugins.Cats (catPlugin) where +module Tablebot.Plugins.Cats (cat) where import Control.Monad.IO.Class (MonadIO (liftIO)) import Data.Aeson (FromJSON, Object, eitherDecode) @@ -18,18 +18,9 @@ import GHC.Generics (Generic) import Network.HTTP.Conduit (Response (responseBody), parseRequest) import Network.HTTP.Simple (addRequestHeader, httpLBS) import System.Environment (lookupEnv) +import Tablebot.Utility import Tablebot.Utility.Discord (Message, sendMessage) import Tablebot.Utility.SmartParser (parseComm) -import Tablebot.Utility.Types - ( Command, - DatabaseDiscord, - EnvCommand (Command), - EnvPlugin (..), - HelpPage (HelpPage), - Plugin, - RequiredPermission (None), - plug, - ) -- | @CatAPI@ is the basic data type for the JSON object that thecatapi returns data CatAPI = CatAPI @@ -43,10 +34,10 @@ data CatAPI = CatAPI instance FromJSON CatAPI --- | @cat@ is a command that takes no arguments (using 'noArguments') and +-- | @catmmand@ is a command that takes no arguments (using 'noArguments') and -- replies with an image of a cat. Uses https://docs.thecatapi.com/ for cats. -cat :: Command -cat = +catmmand :: Command +catmmand = Command "cat" (parseComm sendCat) @@ -87,4 +78,7 @@ catHelp = HelpPage "cat" [] "displays an image of a cat" "**Cat**\nGets a random -- | @catPlugin@ assembles these commands into a plugin containing cat catPlugin :: Plugin -catPlugin = (plug "cats") {commands = [cat], helpPages = [catHelp]} +catPlugin = (plug "cats") {commands = [catmmand], helpPages = [catHelp]} + +cat :: CompiledPlugin +cat = compilePlugin catPlugin diff --git a/src/Tablebot/Plugins/Dogs.hs b/src/Tablebot/Plugins/Dogs.hs index 177f4607..e70132fa 100644 --- a/src/Tablebot/Plugins/Dogs.hs +++ b/src/Tablebot/Plugins/Dogs.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE DeriveGeneric #-} - -- | -- Module : Tablebot.Plugins.Dog -- Description : A very simple plugin that provides dog pictures. @@ -9,7 +7,7 @@ -- Portability : POSIX -- -- This is an example plugin which just responds with a dog photo to a .dog call -module Tablebot.Plugins.Dogs (dogPlugin) where +module Tablebot.Plugins.Dogs (dog) where import Control.Monad.IO.Class (MonadIO (liftIO)) import Data.Text (Text) @@ -17,23 +15,14 @@ import Data.Text.Lazy (toStrict) import Data.Text.Lazy.Encoding (decodeUtf8) import Network.HTTP.Conduit (Response (responseBody), parseRequest) import Network.HTTP.Simple (httpLBS) +import Tablebot.Utility import Tablebot.Utility.Discord (Message, sendMessage) import Tablebot.Utility.SmartParser (parseComm) -import Tablebot.Utility.Types - ( Command, - DatabaseDiscord, - EnvCommand (Command), - EnvPlugin (..), - HelpPage (HelpPage), - Plugin, - RequiredPermission (None), - plug, - ) -- | @dog@ is a command that takes no arguments (using 'noArguments') and -- replies with an image of a dog. Uses https://randomdog.ca/ for dog images. -dog :: Command -dog = +dogmmand :: Command +dogmmand = Command "dog" (parseComm sendDog) @@ -58,4 +47,7 @@ dogHelp = HelpPage "dog" [] "displays an image of a dog" "**Dog**\nGets a random -- | @dogPlugin@ assembles these commands into a plugin containing dog dogPlugin :: Plugin -dogPlugin = (plug "dog") {commands = [dog], helpPages = [dogHelp]} +dogPlugin = (plug "dog") {commands = [dogmmand], helpPages = [dogHelp]} + +dog :: CompiledPlugin +dog = compilePlugin dogPlugin diff --git a/src/Tablebot/Plugins/Flip.hs b/src/Tablebot/Plugins/Flip.hs index 1b7274c6..66a2d0d3 100644 --- a/src/Tablebot/Plugins/Flip.hs +++ b/src/Tablebot/Plugins/Flip.hs @@ -7,7 +7,7 @@ -- Portability : POSIX -- -- A command that picks one random element from its given arguments. -module Tablebot.Plugins.Flip (flipPlugin) where +module Tablebot.Plugins.Flip (flips) where import Control.Monad.IO.Class (MonadIO (liftIO)) import Data.Text (pack) @@ -51,3 +51,6 @@ Randomly picks one element from its arguments or, if none are provided, picks fr -- | @flipPlugin@ assembles the command into a plugin. flipPlugin :: Plugin flipPlugin = (plug "flip") {commands = [flip], helpPages = [flipHelp]} + +flips :: CompiledPlugin +flips = compilePlugin flipPlugin diff --git a/src/Tablebot/Plugins/Fox.hs b/src/Tablebot/Plugins/Fox.hs index 454f4500..b040901e 100644 --- a/src/Tablebot/Plugins/Fox.hs +++ b/src/Tablebot/Plugins/Fox.hs @@ -9,27 +9,18 @@ -- Portability : POSIX -- -- This is an example plugin which just responds with a fox photo to a .fox call -module Tablebot.Plugins.Fox (foxPlugin) where +module Tablebot.Plugins.Fox (fox) where import Control.Monad.IO.Class (MonadIO (liftIO)) import Data.Aeson (FromJSON, eitherDecode) import Data.Functor ((<&>)) import Data.Text (Text, pack) -import GHC.Generics +import GHC.Generics (Generic) import Network.HTTP.Conduit (Response (responseBody), parseRequest) import Network.HTTP.Simple (httpLBS) +import Tablebot.Utility import Tablebot.Utility.Discord (Message, sendMessage) import Tablebot.Utility.SmartParser (parseComm) -import Tablebot.Utility.Types - ( Command, - DatabaseDiscord, - EnvCommand (Command), - EnvPlugin (..), - HelpPage (HelpPage), - Plugin, - RequiredPermission (None), - plug, - ) -- | @FoxAPI@ is the basic data type for the JSON object that the Fox API returns data FoxAPI = Fox @@ -42,8 +33,8 @@ instance FromJSON FoxAPI -- | @fox@ is a command that takes no arguments (using 'noArguments') and -- replies with an image of a fox. Uses https://randomfox.ca/ for fox images. -fox :: Command -fox = +foxmmand :: Command +foxmmand = Command "fox" (parseComm sendFox) @@ -75,4 +66,7 @@ foxHelp = HelpPage "fox" [] "displays an image of a fox" "**Fox**\nGets a random -- | @foxPlugin@ assembles these commands into a plugin containing fox foxPlugin :: Plugin -foxPlugin = (plug "fox") {commands = [fox], helpPages = [foxHelp]} +foxPlugin = (plug "fox") {commands = [foxmmand], helpPages = [foxHelp]} + +fox :: CompiledPlugin +fox = compilePlugin foxPlugin diff --git a/src/Tablebot/Plugins/Netrunner.hs b/src/Tablebot/Plugins/Netrunner.hs index ca79bbc0..ccf9a999 100644 --- a/src/Tablebot/Plugins/Netrunner.hs +++ b/src/Tablebot/Plugins/Netrunner.hs @@ -7,6 +7,10 @@ -- Portability : POSIX -- -- Commands for interfacing with NetrunnerDB. -module Tablebot.Plugins.Netrunner (netrunnerPlugin) where +module Tablebot.Plugins.Netrunner (netrunner) where import Tablebot.Plugins.Netrunner.Plugin (netrunnerPlugin) +import Tablebot.Utility (CompiledPlugin, compilePlugin) + +netrunner :: CompiledPlugin +netrunner = compilePlugin netrunnerPlugin diff --git a/src/Tablebot/Plugins/Ping.hs b/src/Tablebot/Plugins/Ping.hs index d42e46c6..5fac4692 100644 --- a/src/Tablebot/Plugins/Ping.hs +++ b/src/Tablebot/Plugins/Ping.hs @@ -7,7 +7,7 @@ -- Portability : POSIX -- -- This is an example plugin which just responds "ping" to "!pong" and vice-versa. -module Tablebot.Plugins.Ping (pingPlugin) where +module Tablebot.Plugins.Ping (pingpong) where import Data.Text (Text) import Tablebot.Utility @@ -49,3 +49,6 @@ pongHelp = HelpPage "pong" [] "show a more different debug message" "**Pong**\nS -- and pong. pingPlugin :: Plugin pingPlugin = (plug "ping") {commands = [ping, pong], helpPages = [pingHelp, pongHelp]} + +pingpong :: CompiledPlugin +pingpong = compilePlugin pingPlugin diff --git a/src/Tablebot/Plugins/Quote.hs b/src/Tablebot/Plugins/Quote.hs index cc2e4cd0..a44302d7 100644 --- a/src/Tablebot/Plugins/Quote.hs +++ b/src/Tablebot/Plugins/Quote.hs @@ -10,7 +10,7 @@ -- -- This is an example plugin which allows user to @!quote add@ their favourite -- quotes and then @!quote show n@ a particular quote. -module Tablebot.Plugins.Quote (quotePlugin) where +module Tablebot.Plugins.Quote (quotes) where import Control.Monad (void) import Control.Monad.IO.Class (liftIO) @@ -392,6 +392,9 @@ quotePlugin = helpPages = [quoteHelp] } +quotes :: CompiledPlugin +quotes = compilePlugin quotePlugin + deriving instance Generic Quote instance FromJSON Quote diff --git a/src/Tablebot/Plugins/Reminder.hs b/src/Tablebot/Plugins/Reminder.hs index 7f34b93b..739fe122 100644 --- a/src/Tablebot/Plugins/Reminder.hs +++ b/src/Tablebot/Plugins/Reminder.hs @@ -10,10 +10,7 @@ -- -- This is an example plugin which allows user to ask the bot to remind them about -- something later in time. -module Tablebot.Plugins.Reminder - ( reminderPlugin, - ) -where +module Tablebot.Plugins.Reminder (reminder) where import Control.Monad (forM_) import Control.Monad.IO.Class (MonadIO (liftIO)) @@ -185,3 +182,6 @@ reminderPlugin = migrations = [reminderMigration], helpPages = [reminderHelp] } + +reminder :: CompiledPlugin +reminder = compilePlugin reminderPlugin diff --git a/src/Tablebot/Plugins/Roll.hs b/src/Tablebot/Plugins/Roll.hs index 8ff20b49..1616388e 100644 --- a/src/Tablebot/Plugins/Roll.hs +++ b/src/Tablebot/Plugins/Roll.hs @@ -7,6 +7,10 @@ -- Portability : POSIX -- -- A command that outputs the result of rolling the input dice. -module Tablebot.Plugins.Roll (rollPlugin) where +module Tablebot.Plugins.Roll (roll) where import Tablebot.Plugins.Roll.Plugin (rollPlugin) +import Tablebot.Utility + +roll :: CompiledPlugin +roll = compilePlugin rollPlugin diff --git a/src/Tablebot/Plugins/Say.hs b/src/Tablebot/Plugins/Say.hs index a9fa4863..7194866b 100644 --- a/src/Tablebot/Plugins/Say.hs +++ b/src/Tablebot/Plugins/Say.hs @@ -7,7 +7,7 @@ -- Portability : POSIX -- -- A command that outputs its input. -module Tablebot.Plugins.Say (sayPlugin) where +module Tablebot.Plugins.Say (says) where import Data.Text (pack) import Discord.Types (Message (messageAuthor), User (userId)) @@ -43,3 +43,6 @@ Repeat the input. -- | @sayPlugin@ assembles the command into a plugin. sayPlugin :: Plugin sayPlugin = (plug "say") {commands = [say], helpPages = [sayHelp]} + +says :: CompiledPlugin +says = compilePlugin sayPlugin diff --git a/src/Tablebot/Plugins/Shibe.hs b/src/Tablebot/Plugins/Shibe.hs index c0556fa3..055650aa 100644 --- a/src/Tablebot/Plugins/Shibe.hs +++ b/src/Tablebot/Plugins/Shibe.hs @@ -7,7 +7,7 @@ -- Portability : POSIX -- -- This is an example plugin which just responds with a shibe photo to a .shibe call -module Tablebot.Plugins.Shibe (shibePlugin) where +module Tablebot.Plugins.Shibe (shibe) where import Control.Monad.IO.Class (MonadIO (liftIO)) import Data.Aeson (eitherDecode) @@ -15,27 +15,17 @@ import Data.Functor ((<&>)) import Data.Text (Text, pack) import Network.HTTP.Conduit (Response (responseBody), parseRequest) import Network.HTTP.Simple (httpLBS) +import Tablebot.Utility import Tablebot.Utility.Discord (Message, sendMessage) import Tablebot.Utility.SmartParser (parseComm) -import Tablebot.Utility.Types - ( Command, - DatabaseDiscord, - EnvCommand (Command), - EnvPlugin (..), - HelpPage (HelpPage), - Plugin, - RequiredPermission (None), - commandAlias, - plug, - ) -- | @ShibeAPI@ is the basic data type for the JSON object that the Shibe API returns type ShibeAPI = Text -- | @shibe@ is a command that takes no arguments (using 'noArguments') and -- replies with an image of a shibe. Uses https://shibe.online/ for shibe images. -shibe :: Command -shibe = +shibes :: Command +shibes = Command "shibe" (parseComm sendShibe) @@ -105,4 +95,7 @@ birbHelp = HelpPage "bird" [] "displays an image of a bird" "**Bird**\nGets a ra -- | @shibePlugin@ assembles these commands into a plugin containing shibe shibePlugin :: Plugin -shibePlugin = (plug "shibe") {commands = [birb, commandAlias "bird" birb, shibe], helpPages = [birbHelp, shibeHelp]} +shibePlugin = (plug "shibe") {commands = [birb, commandAlias "bird" birb, shibes], helpPages = [birbHelp, shibeHelp]} + +shibe :: CompiledPlugin +shibe = compilePlugin shibePlugin diff --git a/src/Tablebot/Plugins/Suggest.hs b/src/Tablebot/Plugins/Suggest.hs index bf6b033d..e0ab4387 100644 --- a/src/Tablebot/Plugins/Suggest.hs +++ b/src/Tablebot/Plugins/Suggest.hs @@ -7,7 +7,7 @@ -- Portability : POSIX -- -- A command that shows the link for a user to suggest a new game to buy. -module Tablebot.Plugins.Suggest (suggestPlugin) where +module Tablebot.Plugins.Suggest (suggests) where import Data.Text (pack) import Tablebot.Utility @@ -38,3 +38,6 @@ suggestHelp = HelpPage "suggest" [] "show links to suggest a new game for the so suggestPlugin :: Plugin suggestPlugin = (plug "suggest") {commands = [suggest], helpPages = [suggestHelp]} + +suggests :: CompiledPlugin +suggests = compilePlugin suggestPlugin diff --git a/src/Tablebot/Plugins/Welcome.hs b/src/Tablebot/Plugins/Welcome.hs index 10f8ba5c..df4b1ad6 100644 --- a/src/Tablebot/Plugins/Welcome.hs +++ b/src/Tablebot/Plugins/Welcome.hs @@ -7,7 +7,7 @@ -- Portability : POSIX -- -- Commands for generating welcome messages. -module Tablebot.Plugins.Welcome (welcomePlugin) where +module Tablebot.Plugins.Welcome (welcome) where import Control.Monad.IO.Class import Control.Monad.Trans.Reader (ask) @@ -101,3 +101,6 @@ welcomeStartUp = StartUp $ liftIO readCategories -- | @welcomePlugin@ assembles these commands into a plugin. welcomePlugin :: EnvPlugin SS welcomePlugin = (envPlug "welcome" welcomeStartUp) {commands = [favourite], helpPages = [favouriteHelp]} + +welcome :: CompiledPlugin +welcome = compilePlugin welcomePlugin diff --git a/src/Tablebot/Utility.hs b/src/Tablebot/Utility.hs index 85d7fc18..46f8ff8b 100644 --- a/src/Tablebot/Utility.hs +++ b/src/Tablebot/Utility.hs @@ -12,8 +12,12 @@ module Tablebot.Utility ( module Types, module Utils, + compilePlugin, + CompiledPlugin, ) where +import Tablebot.Internal.Plugins (compilePlugin) +import Tablebot.Internal.Types (CompiledPlugin) import Tablebot.Utility.Types as Types hiding (Pl) import Tablebot.Utility.Utils as Utils From 0fa17b841baa124f1fcadd49ebee10a00fbc5278 Mon Sep 17 00:00:00 2001 From: Finnbar Keating Date: Sat, 2 Apr 2022 16:49:48 +0100 Subject: [PATCH 58/96] Documentation / ormolu --- README.md | 4 ++++ src/Tablebot.hs | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b3373169..f2da23d1 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,7 @@ to be called when DEBUG is false. Users with the superuser group are able to run ones), so caution should be used when setting these up. If you have any difficulties setting it up, see the [contributor's guide](CONTRIBUTING.md) for a walkthrough. + +## Importing this bot and running it yourself. + +If you like, rather than directly running this bot you can run it yourself with minor tweaks. An example of this is in `app/Main.hs` - tweak this to your needs and then run `stack run` as per usual. \ No newline at end of file diff --git a/src/Tablebot.hs b/src/Tablebot.hs index a55527a3..87e377d0 100644 --- a/src/Tablebot.hs +++ b/src/Tablebot.hs @@ -9,7 +9,7 @@ -- Portability : POSIX -- -- This module contains the main runner for Tablebot. If you're just looking to --- run the bot with existing plugins, importing this and your favourite plugins +-- run the bot with existing plugins, import this and your favourite plugins -- from "Tablebot.Plugins". module Tablebot ( runTablebot, @@ -48,6 +48,10 @@ import Tablebot.Utility import Tablebot.Utility.Help import Text.Regex.PCRE ((=~)) +-- | runTablebotWithEnv @plugins@ runs the bot using data found in the .env +-- file with the @[CompiledPlugin]@ given. If you're looking to run the bot as +-- detailed in the README (i.e. using data from .env), you should call this +-- function. runTablebotWithEnv :: [CompiledPlugin] -> IO () runTablebotWithEnv plugins = do -- fetch the version info as soon after building to reduce the likelihood that it changes between build and run From 356a95c35587a64a13d200902f0a384f48941b5a Mon Sep 17 00:00:00 2001 From: Finnbar Keating Date: Sat, 2 Apr 2022 17:55:58 +0100 Subject: [PATCH 59/96] Allow game and root help text to be customised. --- app/Main.hs | 13 +++++++++++-- src/Tablebot.hs | 28 +++++++++++++++++----------- src/Tablebot/Internal/Types.hs | 13 +++++++++++++ src/Tablebot/Utility/Help.hs | 13 +++++++------ 4 files changed, 48 insertions(+), 19 deletions(-) diff --git a/app/Main.hs b/app/Main.hs index 2907271c..7aeb3778 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -1,8 +1,17 @@ +{-# LANGUAGE OverloadedStrings #-} + module Main where -import Tablebot (runTablebotWithEnv) +import Data.Default +import Data.Text (Text) +import Tablebot (BotConfig (..), runTablebotWithEnv) import Tablebot.Plugins (allPlugins) -- @main@ runs forever. This allows bot reloading by fully shutting down the bot and letting it restart. main :: IO () -main = runTablebotWithEnv allPlugins +main = runTablebotWithEnv allPlugins $ def {gamePlaying = Just "Cosmic Encounter", rootHelpText = Just rootBody} + +rootBody :: Text +rootBody = + "**Test Bot**\n\ + \This bot is for testing, so should not be trusted." diff --git a/src/Tablebot.hs b/src/Tablebot.hs index 87e377d0..1cdbcece 100644 --- a/src/Tablebot.hs +++ b/src/Tablebot.hs @@ -14,6 +14,7 @@ module Tablebot ( runTablebot, runTablebotWithEnv, + BotConfig (..), ) where @@ -48,12 +49,15 @@ import Tablebot.Utility import Tablebot.Utility.Help import Text.Regex.PCRE ((=~)) +-- TODO: Make a Config type or something which can be passed in to set various bits. +-- All plugins that rely on it can have that as an Env argument, found via MVar or similar. + -- | runTablebotWithEnv @plugins@ runs the bot using data found in the .env -- file with the @[CompiledPlugin]@ given. If you're looking to run the bot as -- detailed in the README (i.e. using data from .env), you should call this -- function. -runTablebotWithEnv :: [CompiledPlugin] -> IO () -runTablebotWithEnv plugins = do +runTablebotWithEnv :: [CompiledPlugin] -> BotConfig -> IO () +runTablebotWithEnv plugins config = do -- fetch the version info as soon after building to reduce the likelihood that it changes between build and run gv <- gitVersion let vInfo = VInfo gv version @@ -66,24 +70,26 @@ runTablebotWithEnv plugins = do die "Invalid token format. Please check it is a bot token" prefix <- pack . fromMaybe "!" <$> lookupEnv "PREFIX" dbpath <- getEnv "SQLITE_FILENAME" - runTablebot vInfo dToken prefix dbpath (addAdministrationPlugin rFlag plugins) + runTablebot vInfo dToken prefix dbpath (addAdministrationPlugin rFlag plugins) config exit <- swapMVar rFlag Reload restartAction exit pure $ not (restartIsTerminal exit) putStrLn "Bot closed" --- | runTablebot @dToken@ @prefix@ @dbpath@ @plugins@ runs the bot using the --- given Discord API token @dToken@ and SQLite connection string @dbpath@. Only --- the plugins provided by @plugins@ are run, and all commands are prefixed --- with @prefix@. +-- | runTablebot @dToken@ @prefix@ @dbpath@ @plugins@ @config@ runs the bot +-- using the given Discord API token @dToken@ and SQLite connection string +-- @dbpath@. Only the plugins provided by @plugins@ are run, and all commands +-- are prefixed with @prefix@. @config@ details how the bot should present +-- itself to users, allowing programmers to replace the Tablebot-specific text +-- with their own. -- The plugins given are combined into a single plugin with their combined -- functionality. Each migration present in the combined plugin is run, and -- each cron job and handler is set up. -- This creates a small pool of database connections used by the event handler, -- builds an event handler and starts cron jobs. It also kills the cron jobs on -- bot close. -runTablebot :: VersionInfo -> Text -> Text -> FilePath -> [CompiledPlugin] -> IO () -runTablebot vinfo dToken prefix dbpath plugins = +runTablebot :: VersionInfo -> Text -> Text -> FilePath -> [CompiledPlugin] -> BotConfig -> IO () +runTablebot vinfo dToken prefix dbpath plugins config = do debugPrint ("DEBUG enabled. This is strongly not recommended in production!" :: String) -- Create multiple database threads. @@ -94,7 +100,7 @@ runTablebot vinfo dToken prefix dbpath plugins = blacklist <- runResourceT $ runNoLoggingT $ runSqlPool currentBlacklist pool let filteredPlugins = removeBlacklisted blacklist plugins -- Combine the list of plugins into both a combined plugin - let !plugin = generateHelp $ combinePlugins filteredPlugins + let !plugin = generateHelp (rootHelpText config) $ combinePlugins filteredPlugins -- Run the setup actions of each plugin and collect the plugin actions into a single @PluginActions@ instance allActions <- mapM (runResourceT . runNoLoggingT . flip runSqlPool pool) (combinedSetupAction plugin) let !actions = combineActions allActions @@ -131,7 +137,7 @@ runTablebot vinfo dToken prefix dbpath plugins = updateStatusOptsGame = Just ( Activity - { activityName = "with dice. Prefix is `" <> prefix <> "`. Call `" <> prefix <> "help` for help", + { activityName = fromMaybe "with dice" (gamePlaying config) <> ". Prefix is `" <> prefix <> "`. Call `" <> prefix <> "help` for help", activityType = ActivityTypeGame, activityUrl = Nothing } diff --git a/src/Tablebot/Internal/Types.hs b/src/Tablebot/Internal/Types.hs index c8780087..f79a225e 100644 --- a/src/Tablebot/Internal/Types.hs +++ b/src/Tablebot/Internal/Types.hs @@ -13,6 +13,7 @@ module Tablebot.Internal.Types where import Control.Concurrent.MVar (MVar) import Control.Monad.Reader (ReaderT) +import Data.Default (Default) import Data.Text (Text) import Database.Persist.Sqlite (Migration, SqlPersistT) import Discord @@ -81,3 +82,15 @@ data CompiledCronJob = CCronJob { timeframe :: Int, onCron :: CompiledDatabaseDiscord () } + +-- * Configuration type + +-- Allows others to configure the bot. + +data BotConfig = BotConfig + { rootHelpText :: Maybe Text, + gamePlaying :: Maybe Text + } + +instance Default BotConfig where + def = BotConfig Nothing Nothing diff --git a/src/Tablebot/Utility/Help.hs b/src/Tablebot/Utility/Help.hs index 88a015ed..1f02b0b8 100644 --- a/src/Tablebot/Utility/Help.hs +++ b/src/Tablebot/Utility/Help.hs @@ -10,6 +10,7 @@ module Tablebot.Utility.Help where import Data.Functor (($>)) +import Data.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as T import Tablebot.Internal.Permission (getSenderPermission, userHasPermission) @@ -30,16 +31,16 @@ rootBody = helpHelpPage :: HelpPage helpHelpPage = HelpPage "help" [] "show information about commands" "**Help**\nShows information about bot commands\n\n*Usage:* `help `" [] None -generateHelp :: CombinedPlugin -> CombinedPlugin -generateHelp p = +generateHelp :: Maybe Text -> CombinedPlugin -> CombinedPlugin +generateHelp rootText p = p - { combinedSetupAction = return (PA [CCommand "help" (handleHelp (helpHelpPage : combinedHelpPages p)) []] [] [] [] [] [] []) : combinedSetupAction p + { combinedSetupAction = return (PA [CCommand "help" (handleHelp rootText (helpHelpPage : combinedHelpPages p)) []] [] [] [] [] [] []) : combinedSetupAction p } -handleHelp :: [HelpPage] -> Parser (Message -> CompiledDatabaseDiscord ()) -handleHelp hp = parseHelpPage root +handleHelp :: Maybe Text -> [HelpPage] -> Parser (Message -> CompiledDatabaseDiscord ()) +handleHelp rootText hp = parseHelpPage root where - root = HelpPage "" [] "" rootBody hp None + root = HelpPage "" [] "" (fromMaybe rootBody rootText) hp None parseHelpPage :: HelpPage -> Parser (Message -> CompiledDatabaseDiscord ()) parseHelpPage hp = do From 0c46be021bd82025c2afd05a0e569788ad7515b2 Mon Sep 17 00:00:00 2001 From: Finnbar Keating Date: Sat, 2 Apr 2022 18:06:33 +0100 Subject: [PATCH 60/96] Don't use Maybe and hardcoded defaults --- app/Main.hs | 8 ++++---- src/Tablebot.hs | 2 +- src/Tablebot/Internal/Types.hs | 8 ++------ src/Tablebot/Utility/Help.hs | 13 +++---------- 4 files changed, 10 insertions(+), 21 deletions(-) diff --git a/app/Main.hs b/app/Main.hs index 7aeb3778..25607066 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -2,16 +2,16 @@ module Main where -import Data.Default import Data.Text (Text) import Tablebot (BotConfig (..), runTablebotWithEnv) import Tablebot.Plugins (allPlugins) -- @main@ runs forever. This allows bot reloading by fully shutting down the bot and letting it restart. main :: IO () -main = runTablebotWithEnv allPlugins $ def {gamePlaying = Just "Cosmic Encounter", rootHelpText = Just rootBody} +main = runTablebotWithEnv allPlugins $ BotConfig {gamePlaying = "with dice", rootHelpText = rootBody} rootBody :: Text rootBody = - "**Test Bot**\n\ - \This bot is for testing, so should not be trusted." + "**Tabletop Bot**\n\ + \This friendly little bot provides several tools to help with\ + \ the running of the Warwick Tabletop Games and Role-Playing Society Discord server." diff --git a/src/Tablebot.hs b/src/Tablebot.hs index 1cdbcece..0376b488 100644 --- a/src/Tablebot.hs +++ b/src/Tablebot.hs @@ -137,7 +137,7 @@ runTablebot vinfo dToken prefix dbpath plugins config = updateStatusOptsGame = Just ( Activity - { activityName = fromMaybe "with dice" (gamePlaying config) <> ". Prefix is `" <> prefix <> "`. Call `" <> prefix <> "help` for help", + { activityName = gamePlaying config <> ". Prefix is `" <> prefix <> "`. Call `" <> prefix <> "help` for help", activityType = ActivityTypeGame, activityUrl = Nothing } diff --git a/src/Tablebot/Internal/Types.hs b/src/Tablebot/Internal/Types.hs index f79a225e..276d77c8 100644 --- a/src/Tablebot/Internal/Types.hs +++ b/src/Tablebot/Internal/Types.hs @@ -13,7 +13,6 @@ module Tablebot.Internal.Types where import Control.Concurrent.MVar (MVar) import Control.Monad.Reader (ReaderT) -import Data.Default (Default) import Data.Text (Text) import Database.Persist.Sqlite (Migration, SqlPersistT) import Discord @@ -88,9 +87,6 @@ data CompiledCronJob = CCronJob -- Allows others to configure the bot. data BotConfig = BotConfig - { rootHelpText :: Maybe Text, - gamePlaying :: Maybe Text + { rootHelpText :: Text, + gamePlaying :: Text } - -instance Default BotConfig where - def = BotConfig Nothing Nothing diff --git a/src/Tablebot/Utility/Help.hs b/src/Tablebot/Utility/Help.hs index 1f02b0b8..9eecf7c7 100644 --- a/src/Tablebot/Utility/Help.hs +++ b/src/Tablebot/Utility/Help.hs @@ -10,7 +10,6 @@ module Tablebot.Utility.Help where import Data.Functor (($>)) -import Data.Maybe (fromMaybe) import Data.Text (Text) import qualified Data.Text as T import Tablebot.Internal.Permission (getSenderPermission, userHasPermission) @@ -22,25 +21,19 @@ import Tablebot.Utility.Permission (requirePermission) import Tablebot.Utility.Types hiding (helpPages) import Text.Megaparsec (choice, chunk, eof, try, (), (<|>)) -rootBody :: Text -rootBody = - "**Tabletop Bot**\n\ - \This friendly little bot provides several tools to help with\ - \ the running of the Warwick Tabletop Games and Role-Playing Society Discord server." - helpHelpPage :: HelpPage helpHelpPage = HelpPage "help" [] "show information about commands" "**Help**\nShows information about bot commands\n\n*Usage:* `help `" [] None -generateHelp :: Maybe Text -> CombinedPlugin -> CombinedPlugin +generateHelp :: Text -> CombinedPlugin -> CombinedPlugin generateHelp rootText p = p { combinedSetupAction = return (PA [CCommand "help" (handleHelp rootText (helpHelpPage : combinedHelpPages p)) []] [] [] [] [] [] []) : combinedSetupAction p } -handleHelp :: Maybe Text -> [HelpPage] -> Parser (Message -> CompiledDatabaseDiscord ()) +handleHelp :: Text -> [HelpPage] -> Parser (Message -> CompiledDatabaseDiscord ()) handleHelp rootText hp = parseHelpPage root where - root = HelpPage "" [] "" (fromMaybe rootBody rootText) hp None + root = HelpPage "" [] "" rootText hp None parseHelpPage :: HelpPage -> Parser (Message -> CompiledDatabaseDiscord ()) parseHelpPage hp = do From 0683aa1917b6c5e806f19523c443c517c498d228 Mon Sep 17 00:00:00 2001 From: Anna <44057980+the-Bruce@users.noreply.github.com> Date: Mon, 4 Apr 2022 19:01:38 +0100 Subject: [PATCH 61/96] Allow restriction of emoji origination. --- .env.example | 1 + ChangeLog.md | 1 + README.md | 3 ++- src/Tablebot/Utility/Discord.hs | 16 +++++++++++++--- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 31f56b71..2e574686 100644 --- a/.env.example +++ b/.env.example @@ -7,4 +7,5 @@ EXEC_GROUP=123456789123456789 MODERATOR_GROUP=321654987321654987 SUPERUSER_GROUP=147258369147258369 ALLOW_GIT_UPDATE=False +EMOJI_SERVERS=[121213131414151516] # NOTE: YOU MUST HAVE A NEWLINE AT THE END OF THE FILE diff --git a/ChangeLog.md b/ChangeLog.md index 1dd16b9c..9e0d73c3 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,3 +1,4 @@ # Changelog for tablebot ## Unreleased changes +- Allow configuration of which servers are allowed to provide emoji \ No newline at end of file diff --git a/README.md b/README.md index b3373169..61e91898 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,10 @@ Create a `.env` file containing the following keys. Consult `.env.example` if yo * `EXEC_GROUP` (optional) - the group ID assigned to exec members. * `MODERATOR_GROUP` (optional) - the group ID assigned to moderator members. * `SUPERUSER_GROUP` (optional) - the group ID assigned to the superuser. Strongly recommended +* `EMOJI_SERVERS` (optional) - a list of server IDs that the bot will search for emoji within. The three Group settings are optional, but without them any commands that require elevated permissions will not be able -to be called when DEBUG is false. Users with the superuser group are able to run every command (including some dangerous +to be called when `DEBUG` is false. Users with the superuser group are able to run every command (including some dangerous ones), so caution should be used when setting these up. If you have any difficulties setting it up, see the [contributor's guide](CONTRIBUTING.md) for a walkthrough. diff --git a/src/Tablebot/Utility/Discord.hs b/src/Tablebot/Utility/Discord.hs index 6b08fc26..996737aa 100644 --- a/src/Tablebot/Utility/Discord.hs +++ b/src/Tablebot/Utility/Discord.hs @@ -40,6 +40,7 @@ module Tablebot.Utility.Discord ) where +import Control.Monad.Cont (liftIO) import Control.Monad.Exception (MonadException (throw)) import Data.Char (isDigit) import Data.Foldable (msum) @@ -54,6 +55,7 @@ import Discord.Internal.Gateway.Cache import qualified Discord.Requests as R import Discord.Types import GHC.Word (Word64) +import System.Environment (lookupEnv) import Tablebot.Internal.Cache import Tablebot.Internal.Embed import Tablebot.Utility (EnvDatabaseDiscord, liftDiscord) @@ -231,10 +233,18 @@ getGuildEmoji ename gid = do -- | search through all known guilds for an emoji with that name findEmoji :: Text -> EnvDatabaseDiscord s (Maybe Emoji) -findEmoji ename = fmap msum (liftDiscord readCache >>= cacheToEmoji) +findEmoji ename = fmap msum (emojiServers >>= cacheToEmoji) where - cacheToEmoji :: Cache -> EnvDatabaseDiscord s [Maybe Emoji] - cacheToEmoji cache = mapM (getGuildEmoji ename) (keys $ cacheGuilds cache) + cacheToEmoji :: [GuildId] -> EnvDatabaseDiscord s [Maybe Emoji] + cacheToEmoji ids = mapM (getGuildEmoji ename) ids + emojiServers :: EnvDatabaseDiscord s [GuildId] + emojiServers = do + maybeServers <- liftIO $ lookupEnv "EMOJI_SERVERS" + case maybeServers of + Just x -> pure (read x) + Nothing -> do + cache <- liftDiscord readCache + pure $ keys $ cacheGuilds cache -- | Render an Emoji formatEmoji :: Emoji -> Text From 06ede54f8c463a56b52ed5b6767b0aba64939530 Mon Sep 17 00:00:00 2001 From: Finnbar Keating Date: Mon, 4 Apr 2022 20:42:22 +0100 Subject: [PATCH 62/96] Added warning to runTablebot --- src/Tablebot.hs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Tablebot.hs b/src/Tablebot.hs index 0376b488..07fec893 100644 --- a/src/Tablebot.hs +++ b/src/Tablebot.hs @@ -49,9 +49,6 @@ import Tablebot.Utility import Tablebot.Utility.Help import Text.Regex.PCRE ((=~)) --- TODO: Make a Config type or something which can be passed in to set various bits. --- All plugins that rely on it can have that as an Env argument, found via MVar or similar. - -- | runTablebotWithEnv @plugins@ runs the bot using data found in the .env -- file with the @[CompiledPlugin]@ given. If you're looking to run the bot as -- detailed in the README (i.e. using data from .env), you should call this @@ -76,9 +73,12 @@ runTablebotWithEnv plugins config = do pure $ not (restartIsTerminal exit) putStrLn "Bot closed" --- | runTablebot @dToken@ @prefix@ @dbpath@ @plugins@ @config@ runs the bot --- using the given Discord API token @dToken@ and SQLite connection string --- @dbpath@. Only the plugins provided by @plugins@ are run, and all commands +-- | runTablebot @vinfo@ @dToken@ @prefix@ @dbpath@ @plugins@ @config@ runs the +-- bot using the given Discord API token @dToken@ and SQLite connection string +-- @dbpath@. In general, you should prefer @runTablebotWithEnv@ as it gets all +-- of the required data for you, but this is exported for if you have weird +-- setup requirements or don't want to use the administration plugin. +-- Only the plugins provided by @plugins@ are run, and all commands -- are prefixed with @prefix@. @config@ details how the bot should present -- itself to users, allowing programmers to replace the Tablebot-specific text -- with their own. From c05c155f730e9ed199bb9b247f29adea312c5d35 Mon Sep 17 00:00:00 2001 From: Finnbar Keating Date: Mon, 4 Apr 2022 20:47:50 +0100 Subject: [PATCH 63/96] Fixed warning from Netrunner plugin --- src/Tablebot/Plugins/Netrunner/Utility/Card.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Tablebot/Plugins/Netrunner/Utility/Card.hs b/src/Tablebot/Plugins/Netrunner/Utility/Card.hs index f7c9523b..f77f9491 100644 --- a/src/Tablebot/Plugins/Netrunner/Utility/Card.hs +++ b/src/Tablebot/Plugins/Netrunner/Utility/Card.hs @@ -38,7 +38,6 @@ import Tablebot.Plugins.Netrunner.Utility.BanList (activeBanList, isBanned, isRe import Tablebot.Plugins.Netrunner.Utility.Misc (formatNr) import Tablebot.Utility import Tablebot.Utility.Types () -import Tablebot.Utility.Utils (intToText, maybeEmptyPrepend) -- | @toLink@ takes a card and generates a link to its NetrunnerDB page. toLink :: Card -> Text From 0275b106a66dc95e4fce75da2d271bc568a289b0 Mon Sep 17 00:00:00 2001 From: Finnbar Keating Date: Mon, 4 Apr 2022 20:54:02 +0100 Subject: [PATCH 64/96] Added Default to BotConfig --- src/Tablebot/Internal/Types.hs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Tablebot/Internal/Types.hs b/src/Tablebot/Internal/Types.hs index 276d77c8..9bdbd4d1 100644 --- a/src/Tablebot/Internal/Types.hs +++ b/src/Tablebot/Internal/Types.hs @@ -13,6 +13,7 @@ module Tablebot.Internal.Types where import Control.Concurrent.MVar (MVar) import Control.Monad.Reader (ReaderT) +import Data.Default import Data.Text (Text) import Database.Persist.Sqlite (Migration, SqlPersistT) import Discord @@ -90,3 +91,10 @@ data BotConfig = BotConfig { rootHelpText :: Text, gamePlaying :: Text } + +instance Default BotConfig where + def = + BotConfig + { rootHelpText = "This bot is built off the Tablebot framework ().", + gamePlaying = "Kirby: Planet Robobot" + } From 87dd94c4a4140cf6c7236b8455f4261f52cb8760 Mon Sep 17 00:00:00 2001 From: Finnbar Keating Date: Mon, 4 Apr 2022 20:56:20 +0100 Subject: [PATCH 65/96] Renamed a few commands to make them clearer --- src/Tablebot/Plugins/Cats.hs | 8 ++++---- src/Tablebot/Plugins/Dogs.hs | 6 +++--- src/Tablebot/Plugins/Fox.hs | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Tablebot/Plugins/Cats.hs b/src/Tablebot/Plugins/Cats.hs index c666b9ff..9d01f35a 100644 --- a/src/Tablebot/Plugins/Cats.hs +++ b/src/Tablebot/Plugins/Cats.hs @@ -34,10 +34,10 @@ data CatAPI = CatAPI instance FromJSON CatAPI --- | @catmmand@ is a command that takes no arguments (using 'noArguments') and +-- | @catCommand@ is a command that takes no arguments (using 'noArguments') and -- replies with an image of a cat. Uses https://docs.thecatapi.com/ for cats. -catmmand :: Command -catmmand = +catCommand :: Command +catCommand = Command "cat" (parseComm sendCat) @@ -78,7 +78,7 @@ catHelp = HelpPage "cat" [] "displays an image of a cat" "**Cat**\nGets a random -- | @catPlugin@ assembles these commands into a plugin containing cat catPlugin :: Plugin -catPlugin = (plug "cats") {commands = [catmmand], helpPages = [catHelp]} +catPlugin = (plug "cats") {commands = [catCommand], helpPages = [catHelp]} cat :: CompiledPlugin cat = compilePlugin catPlugin diff --git a/src/Tablebot/Plugins/Dogs.hs b/src/Tablebot/Plugins/Dogs.hs index e70132fa..813d315a 100644 --- a/src/Tablebot/Plugins/Dogs.hs +++ b/src/Tablebot/Plugins/Dogs.hs @@ -21,8 +21,8 @@ import Tablebot.Utility.SmartParser (parseComm) -- | @dog@ is a command that takes no arguments (using 'noArguments') and -- replies with an image of a dog. Uses https://randomdog.ca/ for dog images. -dogmmand :: Command -dogmmand = +dogCommand :: Command +dogCommand = Command "dog" (parseComm sendDog) @@ -47,7 +47,7 @@ dogHelp = HelpPage "dog" [] "displays an image of a dog" "**Dog**\nGets a random -- | @dogPlugin@ assembles these commands into a plugin containing dog dogPlugin :: Plugin -dogPlugin = (plug "dog") {commands = [dogmmand], helpPages = [dogHelp]} +dogPlugin = (plug "dog") {commands = [dogCommand], helpPages = [dogHelp]} dog :: CompiledPlugin dog = compilePlugin dogPlugin diff --git a/src/Tablebot/Plugins/Fox.hs b/src/Tablebot/Plugins/Fox.hs index b040901e..7610fb17 100644 --- a/src/Tablebot/Plugins/Fox.hs +++ b/src/Tablebot/Plugins/Fox.hs @@ -33,8 +33,8 @@ instance FromJSON FoxAPI -- | @fox@ is a command that takes no arguments (using 'noArguments') and -- replies with an image of a fox. Uses https://randomfox.ca/ for fox images. -foxmmand :: Command -foxmmand = +foxCommand :: Command +foxCommand = Command "fox" (parseComm sendFox) @@ -66,7 +66,7 @@ foxHelp = HelpPage "fox" [] "displays an image of a fox" "**Fox**\nGets a random -- | @foxPlugin@ assembles these commands into a plugin containing fox foxPlugin :: Plugin -foxPlugin = (plug "fox") {commands = [foxmmand], helpPages = [foxHelp]} +foxPlugin = (plug "fox") {commands = [foxCommand], helpPages = [foxHelp]} fox :: CompiledPlugin fox = compilePlugin foxPlugin From 340494f35644197c79a2a4aff4ccfb7b737ae79b Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Tue, 5 Apr 2022 18:45:33 +0100 Subject: [PATCH 66/96] remove trace --- src/Tablebot/Internal/Handler/Command.hs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Tablebot/Internal/Handler/Command.hs b/src/Tablebot/Internal/Handler/Command.hs index 5388041c..4e7575ce 100644 --- a/src/Tablebot/Internal/Handler/Command.hs +++ b/src/Tablebot/Internal/Handler/Command.hs @@ -21,7 +21,6 @@ import Data.Maybe (catMaybes) import Data.Set (singleton, toList) import Data.Text (Text) import Data.Void (Void) -import Debug.Trace (trace) import Discord.Types (Message (messageText)) import Tablebot.Internal.Plugins (changeAction) import Tablebot.Internal.Types @@ -107,12 +106,11 @@ makeBundleReadable (ParseErrorBundle errs state) = -- This uses the Label hidden within each error to build an error message, -- as we have used labels to give parsers user-facing errors. makeReadable :: ParseError Text Void -> (ParseError Text ReadableError, Maybe String) -makeReadable te@(TrivialError i _ good) = - trace (show te) $ - let (lab, others) = getLabel (toList good) - in case lab of - Just l -> (FancyError i . singleton . ErrorCustom $ KnownError l others, Just l) - Nothing -> (FancyError i . singleton $ ErrorCustom UnknownError, Nothing) +makeReadable (TrivialError i _ good) = + let (lab, others) = getLabel (toList good) + in case lab of + Just l -> (FancyError i . singleton . ErrorCustom $ KnownError l others, Just l) + Nothing -> (FancyError i . singleton $ ErrorCustom UnknownError, Nothing) where getLabel :: [ErrorItem (Token Text)] -> (Maybe String, [String]) getLabel [] = (Nothing, []) From d215282c88830cc261bdf78a179e2dd92f1a8362 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Tue, 5 Apr 2022 19:14:22 +0100 Subject: [PATCH 67/96] moved over to transformer monads for dice evaluation --- src/Tablebot/Plugins/Roll/Dice/DiceEval.hs | 439 +++++++++++---------- 1 file changed, 222 insertions(+), 217 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index 9a2640ab..95c26090 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs @@ -10,8 +10,8 @@ -- expressions. module Tablebot.Plugins.Roll.Dice.DiceEval (PrettyShow (prettyShow), evalProgram, evalList, evalInteger, evaluationException, propagateException, maximumRNG, maximumListLength) where -import Control.Monad (when) import Control.Monad.Exception (MonadException) +import Control.Monad.State (MonadIO (liftIO), StateT, evalStateT, gets, modify, when) import Data.List (foldl', genericDrop, genericReplicate, genericTake, sortBy) import Data.List.NonEmpty as NE (NonEmpty ((:|)), head, tail, (<|)) import Data.Map (Map, empty) @@ -38,9 +38,14 @@ data ProgramState = ProgramState } deriving (Show) +startState :: ProgramState +startState = ProgramState 0 empty + +type ProgramStateM = StateT ProgramState IO + -- | Add the given variable to the `ProgramState` -addVariable :: ProgramState -> Text -> Either ListValues Expr -> ProgramState -addVariable (ProgramState i vs) t val = ProgramState i (M.insert t val vs) +addVariable :: Text -> Either ListValues Expr -> ProgramStateM () +addVariable t val = modify $ \s -> s {getVariables = M.insert t val (getVariables s)} -- | The maximum depth that should be permitted. Used to limit number of dice -- and rerolls. @@ -51,13 +56,14 @@ maximumListLength :: Integer maximumListLength = 50 -- | Increment the rngcount by 1. -incRNGCount :: ProgramState -> ProgramState -incRNGCount ps = ps {getRNGCount = 1 + getRNGCount ps} +incRNGCount :: ProgramStateM () +incRNGCount = modify (\s -> s {getRNGCount = getRNGCount s + 1}) >> checkRNGCount -- | Check whether the RNG count has been exceeded by the integer given. -checkRNGCount :: ProgramState -> IO () -checkRNGCount i = - when (getRNGCount i > maximumRNG) $ throwBot $ EvaluationException ("exceeded maximum rng count (" <> show maximumRNG <> ")") [] +checkRNGCount :: ProgramStateM () +checkRNGCount = do + rngCount <- gets getRNGCount + when (rngCount > maximumRNG) $ evaluationException ("Maximum RNG count exceeded (" <> pack (show maximumRNG) <> ")") [] -- | Utility function to throw an `EvaluationException` when using `Text`. evaluationException :: (MonadException m) => Text -> [Text] -> m a @@ -65,40 +71,31 @@ evaluationException nm locs = throwBot $ EvaluationException (unpack nm) (unpack --- Evaluating an expression. Uses IO because dice are random --- instance IOEval (Program Expr) where --- evalShow' rngCount (Program ss e) = do --- (t, ps) <- foldr (\s b -> b >>= \(t, ps) -> evalStatement ps s >>= \(st, ps') -> return (t <> st, ps')) (return ("", rngCount)) ss --- (i, t', ps') <- evalShow ps e --- return (i, t <> t', ps') - --- instance IOEvalList (Program ListValues) where --- evalShowL' rngCount (Program ss e) = do --- (t, ps) <- foldr (\s b -> b >>= \(t, ps) -> evalStatement ps s >>= \(st, ps') -> return (t <> st, ps')) (return ("", rngCount)) ss --- (i, t', ps') <- evalShowL ps e --- return (i, (t <>) <$> t', ps') - -- | Evaluating a full program evalProgram :: Program -> IO (Either [(Integer, Text)] Integer, Text) -evalProgram (Program ss elve) = do - let rngCount = ProgramState 0 empty - (t, ps) <- foldl' (\b s -> b >>= \(t, ps) -> evalStatement ps s >>= \(st, ps') -> return (t <> st, ps')) (return ("", rngCount)) ss - r <- either ((Left <$>) . evalShowL ps) ((Right <$>) . evalShow ps) elve - case r of - Left (is, mt, _) -> return (Left is, t <> fromMaybe (prettyShow elve) mt) - Right (is, mt, _) -> return (Right is, t <> mt) +evalProgram (Program ss elve) = + evalStateT + ( do + t <- foldl' (\b s -> b >>= \t -> evalStatement s >>= \st -> return (t <> st)) (return "") ss + r <- either ((Left <$>) . evalShowL) ((Right <$>) . evalShow) elve + case r of + Left (is, mt) -> return (Left is, t <> fromMaybe (prettyShow elve) mt) + Right (is, mt) -> return (Right is, t <> mt) + ) + startState -- | Given a list expression, evaluate it, getting the pretty printed string and -- the value of the result. evalList :: (IOEvalList a, PrettyShow a) => a -> IO ([(Integer, Text)], Text) evalList a = do - (is, ss, _) <- evalShowL (ProgramState 0 empty) a + (is, ss) <- evalStateT (evalShowL a) startState return (is, fromMaybe (prettyShow a) ss) -- | Given an integer expression, evaluate it, getting the pretty printed string -- and the value of the result. evalInteger :: (IOEval a, PrettyShow a) => a -> IO (Integer, Text) evalInteger a = do - (is, ss, _) <- evalShow (ProgramState 0 empty) a + (is, ss) <- evalStateT (evalShow a) startState return (is, ss) -- | Utility function to display dice. @@ -132,25 +129,25 @@ dieShow lchc d ls = return $ prettyShow d <> " [" <> intercalate ", " adjustList -- | Evaluate a series of values, combining the text output into a comma -- separated list. -evalShowList :: (IOEval a, PrettyShow a) => ProgramState -> [a] -> IO ([Integer], Text, ProgramState) -evalShowList rngCount as = do - (vs, rngCount') <- evalShowList' rngCount as +evalShowList :: (IOEval a, PrettyShow a) => [a] -> ProgramStateM ([Integer], Text) +evalShowList as = do + vs <- evalShowList' as let (is, ts) = unzip vs - return (is, intercalate ", " ts, rngCount') + return (is, intercalate ", " ts) -- | Evaluate a series of values, combining the text output a list. -evalShowList' :: (IOEval a, PrettyShow a) => ProgramState -> [a] -> IO ([(Integer, Text)], ProgramState) +evalShowList' :: (IOEval a, PrettyShow a) => [a] -> ProgramStateM [(Integer, Text)] evalShowList' = evalShowList'' evalShow -- | Evaluate (using a custom evaluator function) a series of values, getting -- strings and values as a result. -evalShowList'' :: (ProgramState -> a -> IO (i, Text, ProgramState)) -> ProgramState -> [a] -> IO ([(i, Text)], ProgramState) -evalShowList'' customEvalShow rngCount as = foldl' (flip foldF) (return ([], rngCount)) as >>= \(lst, ps) -> return (reverse lst, ps) +evalShowList'' :: (a -> ProgramStateM (i, Text)) -> [a] -> ProgramStateM [(i, Text)] +evalShowList'' customEvalShow as = foldl' (flip foldF) (return []) as >>= \lst -> return (reverse lst) where foldF a sumrngcount = do - (diceSoFar, rngCountTotal) <- sumrngcount - (i, s, rngCountTemp) <- customEvalShow rngCountTotal a - return ((i, s) : diceSoFar, rngCountTemp) + diceSoFar <- sumrngcount + (i, s) <- customEvalShow a + return ((i, s) : diceSoFar) -- | When given a value that may possibly have an `EvaluationException`, add the -- representation of the current value to the exception stack. @@ -170,42 +167,44 @@ class IOEvalList a where -- it took. If the `a` value is a dice value, the values of the dice should be -- displayed. This function adds the current location to the exception -- callstack. - evalShowL :: PrettyShow a => ProgramState -> a -> IO ([(Integer, Text)], Maybe Text, ProgramState) - evalShowL rngCount a = do - (is, mt, rngCount') <- propagateException (prettyShow a) (evalShowL' rngCount a) - return (genericTake maximumListLength is, mt, rngCount') + evalShowL :: PrettyShow a => a -> ProgramStateM ([(Integer, Text)], Maybe Text) + evalShowL a = do + (is, mt) <- propagateException (prettyShow a) (evalShowL' a) + return (genericTake maximumListLength is, mt) - evalShowL' :: PrettyShow a => ProgramState -> a -> IO ([(Integer, Text)], Maybe Text, ProgramState) + evalShowL' :: PrettyShow a => a -> ProgramStateM ([(Integer, Text)], Maybe Text) -evalArgValue :: ProgramState -> ArgValue -> IO (ListInteger, ProgramState) -evalArgValue rngCount (AVExpr e) = do - (i, _, rngCount') <- evalShow rngCount e - return (LIInteger i, rngCount') -evalArgValue rngCount (AVListValues e) = do - (i, _, rngCount') <- evalShowL rngCount e - return (LIList (fst <$> i), rngCount') +evalArgValue :: ArgValue -> ProgramStateM ListInteger +evalArgValue (AVExpr e) = do + (i, _) <- evalShow e + return $ LIInteger i +evalArgValue (AVListValues e) = do + (i, _) <- evalShowL e + return (LIList (fst <$> i)) instance IOEvalList ListValues where - evalShowL' rngCount (MultipleValues nb b) = do - (nb', _, rngCount') <- evalShow rngCount nb - (vs, rc) <- evalShowList' rngCount' (genericReplicate nb' b) - return (vs, Nothing, rc) - evalShowL' rngCount (LVFunc fi exprs) = evaluateFunction rngCount fi exprs >>= \(i, s, rc) -> return ((,"") <$> i, Just s, rc) - evalShowL' rngCount (LVBase lvb) = evalShowL rngCount lvb - evalShowL' rngCount (LVVar t) = case M.lookup t (getVariables rngCount) of - Just (Left e) -> evalShowL rngCount e >>= \(i, _, rngCount') -> return (i, Just t, rngCount') - _ -> evaluationException ("could not find list variable `" <> t <> "`") [] - evalShowL' rngCount (ListValuesMisc l) = evalShowL rngCount l + evalShowL' (MultipleValues nb b) = do + (nb', _) <- evalShow nb + vs <- evalShowList' (genericReplicate nb' b) + return (vs, Nothing) + evalShowL' (LVFunc fi exprs) = evaluateFunction fi exprs >>= \(i, s) -> return ((,"") <$> i, Just s) + evalShowL' (LVBase lvb) = evalShowL lvb + evalShowL' (LVVar t) = do + vars <- gets getVariables + case M.lookup t vars of + Just (Left e) -> evalShowL e >>= \(i, _) -> return (i, Just t) + _ -> evaluationException ("could not find list variable `" <> t <> "`") [] + evalShowL' (ListValuesMisc l) = evalShowL l instance IOEvalList ListValuesBase where - evalShowL' rngCount (LVBList es) = do - (vs, rc) <- evalShowList' rngCount es - return (vs, Nothing, rc) - evalShowL' rngCount (LVBParen (Paren lv)) = evalShowL rngCount lv + evalShowL' (LVBList es) = do + vs <- evalShowList' es + return (vs, Nothing) + evalShowL' (LVBParen (Paren lv)) = evalShowL lv instance IOEvalList ListValuesMisc where - evalShowL' rngCount (MiscLet l) = evalShowL rngCount l - evalShowL' rngCount (MiscIf l) = evalShowL rngCount l + evalShowL' (MiscLet l) = evalShowL l + evalShowL' (MiscIf l) = evalShowL l -- | This type class gives a function which evaluates the value to an integer -- and a string. @@ -214,51 +213,53 @@ class IOEval a where -- value, and the number of RNG calls it took. If the `a` value is a dice -- value, the values of the dice should be displayed. This function adds -- the current location to the exception callstack. - evalShow :: PrettyShow a => ProgramState -> a -> IO (Integer, Text, ProgramState) - evalShow rngCount a = propagateException (prettyShow a) (evalShow' rngCount a) + evalShow :: PrettyShow a => a -> ProgramStateM (Integer, Text) + evalShow a = propagateException (prettyShow a) (evalShow' a) - evalShow' :: PrettyShow a => ProgramState -> a -> IO (Integer, Text, ProgramState) + evalShow' :: PrettyShow a => a -> ProgramStateM (Integer, Text) instance IOEval Base where - evalShow' rngCount (NBase nb) = evalShow rngCount nb - evalShow' rngCount (DiceBase dice) = evalShow rngCount dice - evalShow' rngCount (Var t) = case M.lookup t (getVariables rngCount) of - Just (Right e) -> evalShow rngCount e >>= \(i, _, rngCount') -> return (i, t, rngCount') - _ -> evaluationException ("could not find integer variable `" <> t <> "`") [] + evalShow' (NBase nb) = evalShow nb + evalShow' (DiceBase dice) = evalShow dice + evalShow' (Var t) = do + vars <- gets getVariables + case M.lookup t vars of + Just (Right e) -> evalShow e >>= \(i, _) -> return (i, t) + _ -> evaluationException ("could not find integer variable `" <> t <> "`") [] instance IOEval Die where - evalShow' rngCount ld@(LazyDie d) = do - (i, _, rngCount') <- evalShow rngCount d + evalShow' ld@(LazyDie d) = do + (i, _) <- evalShow d ds <- dieShow Nothing ld [(i, Nothing)] - return (i, ds, rngCount') - evalShow' rngCount d@(CustomDie (LVBList es)) = do - e <- chooseOne es - (i, _, rngCount') <- evalShow rngCount e + return (i, ds) + evalShow' d@(CustomDie (LVBList es)) = do + e <- liftIO $ chooseOne es + (i, _) <- evalShow e ds <- dieShow Nothing d [(i, Nothing)] - checkRNGCount (incRNGCount rngCount') - return (i, ds, incRNGCount rngCount') - evalShow' rngCount d@(CustomDie is) = do - (is', _, rngCount') <- evalShowL rngCount is - i <- chooseOne (fst <$> is') + incRNGCount + return (i, ds) + evalShow' d@(CustomDie is) = do + (is', _) <- evalShowL is + i <- liftIO $ chooseOne (fst <$> is') ds <- dieShow Nothing d [(i, Nothing)] - checkRNGCount (incRNGCount rngCount') - return (i, ds, incRNGCount rngCount') - evalShow' rngCount d@(Die b) = do - (bound, _, rngCount') <- evalShow rngCount b + incRNGCount + return (i, ds) + evalShow' d@(Die b) = do + (bound, _) <- evalShow b if bound < 1 then evaluationException ("Cannot roll a < 1 sided die (" <> formatText Code (prettyShow b) <> ")") [] else do i <- randomRIO (1, bound) ds <- dieShow Nothing d [(i, Nothing)] - checkRNGCount (incRNGCount rngCount') - return (i, ds, incRNGCount rngCount') + incRNGCount + return (i, ds) instance IOEval Dice where - evalShow' rngCount dop = do - (lst, mnmx, rngCount') <- evalDieOp rngCount dop + evalShow' dop = do + (lst, mnmx) <- evalDieOp dop let vs = fromEvalDieOpList lst s <- dieShow mnmx dop vs - return (sum (fst <$> filter (isNothing . snd) vs), s, rngCount') + return (sum (fst <$> filter (isNothing . snd) vs), s) -- | Utility function to transform the output list type of other utility -- functions into one that `dieShow` recognises. @@ -274,76 +275,76 @@ fromEvalDieOpList = foldr foldF [] -- -- The function itself checks to make sure the number of dice being rolled is -- less than the maximum recursion and is non-negative. -evalDieOp :: ProgramState -> Dice -> IO ([(NonEmpty Integer, Bool)], Maybe (Integer, Integer), ProgramState) -evalDieOp rngCount (Dice b ds dopo) = do - (nbDice, _, rngCountB) <- evalShow rngCount b +evalDieOp :: Dice -> ProgramStateM ([(NonEmpty Integer, Bool)], Maybe (Integer, Integer)) +evalDieOp (Dice b ds dopo) = do + (nbDice, _) <- evalShow b if nbDice > maximumRNG then evaluationException ("tried to roll more than " <> formatInput Code maximumRNG <> " dice: " <> formatInput Code nbDice) [prettyShow b] else do if nbDice < 0 then evaluationException ("tried to give a negative value to the number of dice: " <> formatInput Code nbDice) [prettyShow b] else do - (ds', rngCountCondense, crits) <- condenseDie rngCountB ds - (rolls, _, rngCountRolls) <- evalShowList rngCountCondense (genericReplicate nbDice ds') + (ds', crits) <- condenseDie ds + (rolls, _) <- evalShowList (genericReplicate nbDice ds') let vs = fmap (\i -> (i :| [], True)) rolls - (rs, rngCountRs) <- evalDieOp' rngCountRolls dopo ds' vs - return (sortBy sortByOption rs, crits, rngCountRs) + rs <- evalDieOp' dopo ds' vs + return (sortBy sortByOption rs, crits) where - condenseDie rngCount' (Die dBase) = do - (i, _, rngCount'') <- evalShow rngCount' dBase - return (Die (Value i), rngCount'', Just (1, i)) - condenseDie rngCount' (CustomDie is) = do - (is', _, rngCount'') <- evalShowL rngCount' is - return (CustomDie (LVBList (promote . fst <$> is')), rngCount'', Nothing) - condenseDie rngCount' (LazyDie d) = return (d, rngCount', Nothing) + condenseDie (Die dBase) = do + (i, _) <- evalShow dBase + return (Die (Value i), Just (1, i)) + condenseDie (CustomDie is) = do + (is', _) <- evalShowL is + return (CustomDie (LVBList (promote . fst <$> is')), Nothing) + condenseDie (LazyDie d) = return (d, Nothing) sortByOption (e :| es, _) (f :| fs, _) | e == f = compare (length fs) (length es) | otherwise = compare e f -- | Utility function that processes a `Maybe DieOpRecur`, when given a die, and -- dice that have already been processed. -evalDieOp' :: ProgramState -> Maybe DieOpRecur -> Die -> [(NonEmpty Integer, Bool)] -> IO ([(NonEmpty Integer, Bool)], ProgramState) -evalDieOp' rngCount Nothing _ is = return (is, rngCount) -evalDieOp' rngCount (Just (DieOpRecur doo mdor)) die is = do - (doo', rngCount') <- processDOO rngCount doo - (is', rngCount'') <- evalDieOp'' rngCount' doo' die is - evalDieOp' rngCount'' mdor die is' +evalDieOp' :: Maybe DieOpRecur -> Die -> [(NonEmpty Integer, Bool)] -> ProgramStateM [(NonEmpty Integer, Bool)] +evalDieOp' Nothing _ is = return is +evalDieOp' (Just (DieOpRecur doo mdor)) die is = do + doo' <- processDOO doo + is' <- evalDieOp'' doo' die is + evalDieOp' mdor die is' where - processLHW rngCount' (Low i) = do - (i', _, rngCount'') <- evalShow rngCount' i - return (Low (Value i'), rngCount'') - processLHW rngCount' (High i) = do - (i', _, rngCount'') <- evalShow rngCount' i - return (High (Value i'), rngCount'') - processLHW rngCount' (Where o i) = do - (i', _, rngCount'') <- evalShow rngCount' i - return (Where o (Value i'), rngCount'') - processDOO rngCount' (DieOpOptionKD kd lhw) = do - (lhw', rngCount'') <- processLHW rngCount' lhw - return (DieOpOptionKD kd lhw', rngCount'') - processDOO rngCount' (Reroll once o i) = do - (i', _, rngCount'') <- evalShow rngCount' i - return (Reroll once o (Value i'), rngCount'') - processDOO rngCount' (DieOpOptionLazy doo') = return (doo', rngCount') + processLHW (Low i) = do + (i', _) <- evalShow i + return (Low (Value i')) + processLHW (High i) = do + (i', _) <- evalShow i + return (High (Value i')) + processLHW (Where o i) = do + (i', _) <- evalShow i + return (Where o (Value i')) + processDOO (DieOpOptionKD kd lhw) = do + lhw' <- processLHW lhw + return (DieOpOptionKD kd lhw') + processDOO (Reroll once o i) = do + (i', _) <- evalShow i + return (Reroll once o (Value i')) + processDOO (DieOpOptionLazy doo') = return doo' -- | Utility function that processes a `DieOpOption`, when given a die, and dice -- that have already been processed. -evalDieOp'' :: ProgramState -> DieOpOption -> Die -> [(NonEmpty Integer, Bool)] -> IO ([(NonEmpty Integer, Bool)], ProgramState) -evalDieOp'' rngCount (DieOpOptionLazy doo) die is = evalDieOp'' rngCount doo die is -evalDieOp'' rngCount (DieOpOptionKD kd lhw) _ is = evalDieOpHelpKD rngCount kd lhw is -evalDieOp'' rngCount (Reroll once o i) die is = foldr rerollF (return ([], rngCount)) is +evalDieOp'' :: DieOpOption -> Die -> [(NonEmpty Integer, Bool)] -> ProgramStateM [(NonEmpty Integer, Bool)] +evalDieOp'' (DieOpOptionLazy doo) die is = evalDieOp'' doo die is +evalDieOp'' (DieOpOptionKD kd lhw) _ is = evalDieOpHelpKD kd lhw is +evalDieOp'' (Reroll once o i) die is = foldr rerollF (return []) is where rerollF g@(i', b) isRngCount' = do - (is', rngCount') <- isRngCount' - (iEval, _, rngCount'') <- evalShow rngCount' i + is' <- isRngCount' + (iEval, _) <- evalShow i if b && applyCompare o (NE.head i') iEval then do - (v, _, rngCount''') <- evalShow rngCount'' die + (v, _) <- evalShow die let ret = (v <| i', b) if once - then return (ret : is', rngCount''') - else rerollF ret (return (is', rngCount''')) - else return (g : is', rngCount'') + then return (ret : is') + else rerollF ret (return is') + else return (g : is') -- | Given a list of dice values, separate them into kept values and dropped values -- respectively. @@ -358,17 +359,17 @@ setToDropped :: [(NonEmpty Integer, Bool)] -> [(NonEmpty Integer, Bool)] setToDropped = fmap (\(is, _) -> (is, False)) -- | Helper function that executes the keep/drop commands on dice. -evalDieOpHelpKD :: ProgramState -> KeepDrop -> LowHighWhere -> [(NonEmpty Integer, Bool)] -> IO ([(NonEmpty Integer, Bool)], ProgramState) -evalDieOpHelpKD rngCount kd (Where cmp i) is = foldr foldF (return ([], rngCount)) is +evalDieOpHelpKD :: KeepDrop -> LowHighWhere -> [(NonEmpty Integer, Bool)] -> ProgramStateM [(NonEmpty Integer, Bool)] +evalDieOpHelpKD kd (Where cmp i) is = foldr foldF (return []) is where isKeep = if kd == Keep then id else not foldF (iis, b) sumrngcount = do - (diceSoFar, rngCountTotal) <- sumrngcount - (i', _, rngCountTemp) <- evalShow rngCountTotal i - return ((iis, b && isKeep (applyCompare cmp (NE.head iis) i')) : diceSoFar, rngCountTemp) -evalDieOpHelpKD rngCount kd lh is = do - (i', _, rngCount') <- evalShow rngCount i - return (d <> setToDropped (getDrop i' sk) <> getKeep i' sk, rngCount') + diceSoFar <- sumrngcount + (i', _) <- evalShow i + return ((iis, b && isKeep (applyCompare cmp (NE.head iis) i')) : diceSoFar) +evalDieOpHelpKD kd lh is = do + (i', _) <- evalShow i + return (d <> setToDropped (getDrop i' sk) <> getKeep i' sk) where (k, d) = separateKeptDropped is -- Note that lh will always be one of `Low` or `High` @@ -381,108 +382,112 @@ evalDieOpHelpKD rngCount kd lh is = do -- Was previously its own type class that wouldn't work for evaluating Base values. -- | Utility function to evaluate a binary operator. -binOpHelp :: (IOEval a, IOEval b, PrettyShow a, PrettyShow b) => ProgramState -> a -> b -> Text -> (Integer -> Integer -> Integer) -> IO (Integer, Text, ProgramState) -binOpHelp rngCount a b opS op = do - (a', a's, rngCount') <- evalShow rngCount a - (b', b's, rngCount'') <- evalShow rngCount' b - return (op a' b', a's <> " " <> opS <> " " <> b's, rngCount'') +binOpHelp :: (IOEval a, IOEval b, PrettyShow a, PrettyShow b) => a -> b -> Text -> (Integer -> Integer -> Integer) -> ProgramStateM (Integer, Text) +binOpHelp a b opS op = do + (a', a's) <- evalShow a + (b', b's) <- evalShow b + return (op a' b', a's <> " " <> opS <> " " <> b's) instance IOEval ExprMisc where - evalShow' rngCount (MiscLet l) = evalShow rngCount l - evalShow' rngCount (MiscIf l) = evalShow rngCount l + evalShow' (MiscLet l) = evalShow l + evalShow' (MiscIf l) = evalShow l instance IOEval Expr where - evalShow' rngCount (NoExpr t) = evalShow rngCount t - evalShow' rngCount (ExprMisc e) = evalShow rngCount e - evalShow' rngCount (Add t e) = binOpHelp rngCount t e "+" (+) - evalShow' rngCount (Sub t e) = binOpHelp rngCount t e "-" (-) + evalShow' (NoExpr t) = evalShow t + evalShow' (ExprMisc e) = evalShow e + evalShow' (Add t e) = binOpHelp t e "+" (+) + evalShow' (Sub t e) = binOpHelp t e "-" (-) instance IOEval Term where - evalShow' rngCount (NoTerm f) = evalShow rngCount f - evalShow' rngCount (Multi f t) = binOpHelp rngCount f t "*" (*) - evalShow' rngCount (Div f t) = do - (f', f's, rngCount') <- evalShow rngCount f - (t', t's, rngCount'') <- evalShow rngCount' t + evalShow' (NoTerm f) = evalShow f + evalShow' (Multi f t) = binOpHelp f t "*" (*) + evalShow' (Div f t) = do + (f', f's) <- evalShow f + (t', t's) <- evalShow t if t' == 0 then evaluationException "division by zero" [prettyShow t] - else return (div f' t', f's <> " / " <> t's, rngCount'') + else return (div f' t', f's <> " / " <> t's) instance IOEval Func where - evalShow' rngCount (Func s exprs) = evaluateFunction rngCount s exprs - evalShow' rngCount (NoFunc b) = evalShow rngCount b + evalShow' (Func s exprs) = evaluateFunction s exprs + evalShow' (NoFunc b) = evalShow b -- | Evaluate a function when given a list of parameters -evaluateFunction :: ProgramState -> FuncInfoBase j -> [ArgValue] -> IO (j, Text, ProgramState) -evaluateFunction rngCount fi exprs = do - (exprs', rngCount') <- evalShowList'' (\r a -> evalArgValue r a >>= \(i, r') -> return (i, "", r')) rngCount exprs +evaluateFunction :: FuncInfoBase j -> [ArgValue] -> ProgramStateM (j, Text) +evaluateFunction fi exprs = do + exprs' <- evalShowList'' (fmap (,"") . evalArgValue) exprs f <- funcInfoFunc fi (fst <$> exprs') - return (f, funcInfoName fi <> "(" <> intercalate ", " (prettyShow <$> exprs) <> ")", rngCount') + return (f, funcInfoName fi <> "(" <> intercalate ", " (prettyShow <$> exprs) <> ")") instance IOEval Negation where - evalShow' rngCount (NoNeg expo) = evalShow rngCount expo - evalShow' rngCount (Neg expo) = do - (expo', expo's, rngCount') <- evalShow rngCount expo - return (negate expo', "-" <> expo's, rngCount') + evalShow' (NoNeg expo) = evalShow expo + evalShow' (Neg expo) = do + (expo', expo's) <- evalShow expo + return (negate expo', "-" <> expo's) instance IOEval Expo where - evalShow' rngCount (NoExpo b) = evalShow rngCount b - evalShow' rngCount (Expo b expo) = do - (expo', expo's, rngCount') <- evalShow rngCount expo + evalShow' (NoExpo b) = evalShow b + evalShow' (Expo b expo) = do + (expo', expo's) <- evalShow expo if expo' < 0 then evaluationException ("the exponent is negative: " <> formatInput Code expo') [prettyShow expo] else do - (b', b's, rngCount'') <- evalShow rngCount' b - return (b' ^ expo', b's <> " ^ " <> expo's, rngCount'') + (b', b's) <- evalShow b + return (b' ^ expo', b's <> " ^ " <> expo's) instance IOEval NumBase where - evalShow' rngCount (NBParen (Paren e)) = do - (r, s, rngCount') <- evalShow rngCount e - return (r, "(" <> s <> ")", rngCount') - evalShow' rngCount (Value i) = return (i, pack (show i), rngCount) + evalShow' (NBParen (Paren e)) = do + (r, s) <- evalShow e + return (r, "(" <> s <> ")") + evalShow' (Value i) = return (i, pack (show i)) instance IOEval (Let Expr) where - evalShow' rngCount (Let t a) = do - (v, lt, rngCount') <- evalShow rngCount a - return (v, "let " <> t <> " = " <> lt, addVariable rngCount' t (Right $ promote v)) - evalShow' rngCount l@(LetLazy t a) = do - (v, _, rngCount') <- evalShow rngCount a - return $ v `seq` (v, prettyShow l, addVariable rngCount' t (Right a)) + evalShow' (Let t a) = do + (v, lt) <- evalShow a + addVariable t (Right $ promote v) + return (v, "let " <> t <> " = " <> lt) + evalShow' l@(LetLazy t a) = do + (v, _) <- evalShow a + addVariable t (Right a) + return $ v `seq` (v, prettyShow l) instance IOEvalList (Let ListValues) where - evalShowL' rngCount l@(Let t a) = do - (v, _, rngCount') <- evalShowL rngCount a - return (v, Just (prettyShow l), addVariable rngCount' t (Left $ promote $ fst <$> v)) - evalShowL' rngCount l@(LetLazy t a) = do - (v, _, rngCount') <- evalShowL rngCount a - return (v, Just (prettyShow l), addVariable rngCount' t (Left a)) - -evalStatement :: ProgramState -> Statement -> IO (Text, ProgramState) -evalStatement ps (StatementExpr l) = evalShowStatement l >>= \(_, t, ps') -> return (t <> "; ", ps') + evalShowL' l@(Let t a) = do + (v, _) <- evalShowL a + addVariable t (Left $ promote $ fst <$> v) + return (v, Just (prettyShow l)) + evalShowL' l@(LetLazy t a) = do + (v, _) <- evalShowL a + addVariable t (Left a) + return (v, Just (prettyShow l)) + +evalStatement :: Statement -> ProgramStateM Text +evalStatement (StatementExpr l) = evalShowStatement l >>= \(_, t) -> return (t <> "; ") where - evalShowStatement (ExprMisc (MiscLet l'@(LetLazy t a))) = return (0, prettyShow l', addVariable ps t (Right a)) - evalShowStatement l' = evalShow ps l' -evalStatement ps (StatementListValues l) = evalShowStatement l >>= \(_, t, ps') -> return (fromMaybe (prettyShow l) t <> "; ", ps') + evalShowStatement (ExprMisc (MiscLet l'@(LetLazy t a))) = addVariable t (Right a) >> return (0, prettyShow l') + evalShowStatement l' = evalShow l' +evalStatement (StatementListValues l) = evalShowStatement l >>= \(_, t) -> return (fromMaybe (prettyShow l) t <> "; ") where - evalShowStatement (ListValuesMisc (MiscLet l'@(LetLazy t a))) = return ([], Just (prettyShow l'), addVariable ps t (Left a)) - evalShowStatement l' = evalShowL ps l' + evalShowStatement (ListValuesMisc (MiscLet l'@(LetLazy t a))) = addVariable t (Left a) >> return ([], Just (prettyShow l')) + evalShowStatement l' = evalShowL l' instance IOEval (If Expr) where - evalShow' ps if'@(If b t e) = do - (i, _, ps') <- evalShow ps b - (i', _, ps'') <- + evalShow' if'@(If b t e) = do + (i, _) <- evalShow b + (i', _) <- if i /= 0 - then evalShow ps' t - else evalShow ps' e - return (i', prettyShow if', ps'') + then evalShow t + else evalShow e + return (i', prettyShow if') instance IOEvalList (If ListValues) where - evalShowL' ps if'@(If b t e) = do - (i, _, ps') <- evalShow ps b - (i', _, ps'') <- + evalShowL' if'@(If b t e) = do + (i, _) <- evalShow b + (i', _) <- if i /= 0 - then evalShowL ps' t - else evalShowL ps' e - return (i', Just $ prettyShow if', ps'') + then evalShowL t + else evalShowL e + return (i', Just $ prettyShow if') --- Pretty printing the AST -- The output from this should be parseable From 8c29e5c4a7e3f74cf530b0ba9b71b0e9e515feba Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Tue, 5 Apr 2022 19:23:51 +0100 Subject: [PATCH 68/96] added help text to stats about statements and how they don't work --- src/Tablebot/Plugins/Roll/Plugin.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 64f80b0a..f9cb7f2b 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -225,7 +225,7 @@ statsHelp = "stats" [] "calculate and display statistics for expressions." - "**Roll Stats**\nCan be used to display statistics for expressions of dice.\n\n*Usage:* `roll stats 2d20kh1`, `roll stats 4d6rr=1dl1+5`, `roll stats 3d6dl1+6 4d6dl1`" + "**Roll Stats**\nCan be used to display statistics for expressions of dice.\nDoes not work with statements.\n\n*Usage:* `roll stats 2d20kh1`, `roll stats 4d6rr=1dl1+5`, `roll stats 3d6dl1+6 4d6dl1`" [] None From 9a317d9512cd04f8d97e1ee474c3b8ccca09c0fb Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Tue, 5 Apr 2022 19:35:22 +0100 Subject: [PATCH 69/96] updated documentation --- docs/Roll.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/Roll.md b/docs/Roll.md index f95a8fa5..75c3f198 100644 --- a/docs/Roll.md +++ b/docs/Roll.md @@ -72,14 +72,14 @@ There are two operators that are more complex and have specific organisational r If statements take an expression, and then two either integer values or list values. If the expression is non-zero, the first value is returned. If the expression is zero, the second value is returned. The syntax for it is `if expression then t else f`, where `expression` is an integer value, and `t` and `f` are both either integer values or list values. Only one of `t` or `f` is ever evaluated. -Let statements take a name and either an integer value or a list, and set a variable with that name to that value. If the let statement is lazy (with an exclamation mark before the variable name) instead, the value is recalculated every time the variable is used. A let statement returns the value on the left side. To create and use list variables, they must be prepended with `l_`. The syntax can be something like `let name = value`, `let !name = value`, or `let l_name = value`, or so on. +Let statements take a name and either an integer value or a list, and set a variable with that name to that value. If the let statement is lazy (with an exclamation mark before the variable name) instead, the value is recalculated every time the variable is used. A let statement returns the value on the left side. To create and use list variables, they must be prepended with `l_`. The syntax can be something like `let name = value`, `let !name = value`, or `let l_name = value`, or so on. These bound values can then be used in other calculations. -As well as normal expressions, statements can be used now. A statement is an integer value or list value followed by a semicolon. Below are a couple example programs. One quality of life feature is that a lazy let expression won't be evaluated until the variable is first used. +To fully utilise these expression types, statements have been made, which, when constructed together with a value, creates a program. A statement is an integer value or list value followed by a semicolon. Below are a couple example programs (which are multiple statements followed by a single value). One quality of life feature is that a lazy let expression won't be evaluated until the variable is first used. - `let l_list = (2d6)#3d6; {length(l_list), minimum(l_list), maximum(l_list), sum(l_list)/length(l_list)}` - Get the length, minimum, maximum, and average value of a random list. -- `let !k = 1d20; let t = k; let !t_iseven = if mod(t, 2) then 0 else 1; if t_iseven then k * t else t` - - Create a lazy variable k. Evaluate it into a variable t. Check whether t is even, and place in a variable. Depending on whether t is even or not, either output another random number times by t, or just output t. +- `let !k = 1d20; let t = k; let !t_iseven = if mod(t, 2) then 0 else 1; if t_iseven then k * t + 20 else t` + - Create a lazy variable `k`. Evaluate it into a variable `t`. Check whether `t` is even, and place in a variable. Depending on whether `t` is even or not, either output another random number times by `t` (and add 20 to distinguish it), or just output `t`. ## Functions @@ -114,7 +114,7 @@ Here are all the functions, what they take, and what they return. They are calle As well as generating values, statistics based off of expressions can be found. There is a total time limit of 10 seconds for this command, with 5 seconds given to calculations and 5 seconds given to generating the bar chart. -To get these statistics, calling the `roll` command with the `stats` subcommand will generate the requested statistics. The expression given has to return an integer. +To get these statistics, calling the `roll` command with the `stats` subcommand will generate the requested statistics. The expression given has to return an integer. Stats can only be generated on single expressions and not programs. The bot will give the mean, the standard deviation, and the top ten most common values of the distribution, as well as graphing the entire distribution. From 67c6b0458e8e776778b9e5a2c71a7fd168ea15ad Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 6 Apr 2022 21:53:07 +0100 Subject: [PATCH 70/96] aliases work --- src/Tablebot/Internal/Alias.hs | 37 ++++++ src/Tablebot/Internal/Handler/Command.hs | 49 ++++++- src/Tablebot/Internal/Types.hs | 16 ++- src/Tablebot/Plugins.hs | 2 + src/Tablebot/Plugins/Alias.hs | 158 +++++++++++++++++++++++ 5 files changed, 254 insertions(+), 8 deletions(-) create mode 100644 src/Tablebot/Internal/Alias.hs create mode 100644 src/Tablebot/Plugins/Alias.hs diff --git a/src/Tablebot/Internal/Alias.hs b/src/Tablebot/Internal/Alias.hs new file mode 100644 index 00000000..1f4f29fc --- /dev/null +++ b/src/Tablebot/Internal/Alias.hs @@ -0,0 +1,37 @@ +-- | +-- Module : Tablebot.Internal.Alias +-- Description : Alias management +-- License : MIT +-- Maintainer : tagarople@gmail.com +-- Stability : experimental +-- Portability : POSIX +-- +-- Alias management +module Tablebot.Internal.Alias where + +import Control.Monad.Exception (MonadException (catch), SomeException) +import Data.Text (Text) +import Database.Persist.Sqlite (BackendKey (SqlBackendKey)) +import qualified Database.Persist.Sqlite as Sql +import Database.Persist.TH +import Discord.Types (UserId) +import Tablebot.Internal.Types (AliasType (..)) +import Tablebot.Utility.Database (selectList) +import Tablebot.Utility.Types (EnvDatabaseDiscord) + +share + [mkPersist sqlSettings, mkMigrate "aliasMigration"] + [persistLowerCase| +Alias + alias Text + command Text + type AliasType + UniqueAlias alias type + deriving Show + deriving Eq +|] + +getAliases :: UserId -> EnvDatabaseDiscord d (Maybe [Alias]) +getAliases uid = + (Just . fmap Sql.entityVal <$> selectList [AliasType Sql.<-. [AliasPublic, AliasPrivate uid]] []) + `catch` (\(_ :: SomeException) -> return Nothing) diff --git a/src/Tablebot/Internal/Handler/Command.hs b/src/Tablebot/Internal/Handler/Command.hs index 4e7575ce..b3d69683 100644 --- a/src/Tablebot/Internal/Handler/Command.hs +++ b/src/Tablebot/Internal/Handler/Command.hs @@ -16,12 +16,15 @@ module Tablebot.Internal.Handler.Command ) where +import Data.List (find) import qualified Data.List.NonEmpty as NE import Data.Maybe (catMaybes) import Data.Set (singleton, toList) import Data.Text (Text) +import qualified Data.Text as T import Data.Void (Void) -import Discord.Types (Message (messageText)) +import Discord.Types (Message (messageAuthor, messageText), User (userId)) +import Tablebot.Internal.Alias (Alias (aliasAlias, aliasCommand), getAliases) import Tablebot.Internal.Plugins (changeAction) import Tablebot.Internal.Types import Tablebot.Utility.Discord (sendEmbedMessage) @@ -49,6 +52,14 @@ parseNewMessage pl prefix m = checkCommand :: Parser () checkCommand = chunk prefix *> word *> (space <|> eof) +parseCommands :: [CompiledCommand] -> Message -> Text -> CompiledDatabaseDiscord () +parseCommands cs m prefix = do + as <- changeAction () $ getAliases (userId $ messageAuthor m) + res <- parseCommands' cs as m prefix + case res of + Right _ -> return () + Left (title, e) -> changeAction () . sendEmbedMessage m "" $ embedError $ ParserException (T.unpack title) . T.unpack $ "```\n" <> e <> "```" + -- | Given a list of 'Command' @cs@, the 'Message' that triggered the event -- @m@, and a command prefix @prefix@, construct a parser that parses commands. -- We look for the prefix, followed by trying out the name of each command, @@ -57,13 +68,29 @@ parseNewMessage pl prefix m = -- -- If the parser errors, the last error (which is hopefully one created by -- '') is sent to the user as a Discord message. -parseCommands :: [CompiledCommand] -> Message -> Text -> CompiledDatabaseDiscord () -parseCommands cs m prefix = case parse (parser cs) "" (messageText m) of - Right p -> p m - Left e -> - let (errs, title) = makeBundleReadable e - in changeAction () . sendEmbedMessage m "" $ embedError $ ParserException title $ "```\n" ++ errorBundlePretty errs ++ "```" +parseCommands' :: [CompiledCommand] -> Maybe [Alias] -> Message -> Text -> CompiledDatabaseDiscord (Either (Text, Text) ()) +parseCommands' cs as m prefix = case parse (parser cs) "" (messageText m) of + Right p -> Right <$> p m + Left e -> case as of + (Just as'@(_ : _)) -> + case parse (aliasParser as') "" (messageText m) of + -- if the alias parser fails, just give the outer error + Left _ -> mkTitleBody e + -- if we get a valid alias, run the command with the alias + -- the way we do this is by running this function again and edit the + -- message text to be the alias's command + -- we ensure no infinite loops by removing the alias we just used + Right (a', rest) -> do + recur <- parseCommands' cs (Just $ filter (/= a') as') (m {messageText = prefix <> aliasCommand a' <> rest}) prefix + -- if successful, return the result. if not, edit the error we + -- obtained from running the alias to include the alias we tried to + -- use + case recur of + Right _ -> return recur + Left (title, err) -> return $ Left (title, aliasAlias a' <> " -> " <> aliasCommand a' <> "\n" <> err) + _ -> mkTitleBody e where + mkTitleBody e' = let (errs, title) = makeBundleReadable e' in return $ Left (T.pack title, T.pack $ errorBundlePretty errs) parser :: [CompiledCommand] -> Parser (Message -> CompiledDatabaseDiscord ()) parser cs' = do @@ -71,6 +98,14 @@ parseCommands cs m prefix = case parse (parser cs) "" (messageText m) of choice (map toErroringParser cs') "No command with that name was found!" toErroringParser :: CompiledCommand -> Parser (Message -> CompiledDatabaseDiscord ()) toErroringParser c = try (chunk $ commandName c) *> (skipSpace1 <|> eof) *> (try (choice $ map toErroringParser $ commandSubcommands c) <|> commandParser c) + aliasParser :: [Alias] -> Parser (Alias, Text) + aliasParser as' = do + _ <- chunk prefix + a <- choice (map (chunk . aliasAlias) as') "No command with that name was found!" + rst <- many anySingle + case find (\a' -> aliasAlias a' == a) as' of + Just a' -> return (a', T.pack rst) + Nothing -> fail "This should never happen! (aliasParser)" data ReadableError = UnknownError | KnownError String [String] deriving (Show, Eq, Ord) diff --git a/src/Tablebot/Internal/Types.hs b/src/Tablebot/Internal/Types.hs index c8780087..ff02932e 100644 --- a/src/Tablebot/Internal/Types.hs +++ b/src/Tablebot/Internal/Types.hs @@ -14,7 +14,7 @@ module Tablebot.Internal.Types where import Control.Concurrent.MVar (MVar) import Control.Monad.Reader (ReaderT) import Data.Text (Text) -import Database.Persist.Sqlite (Migration, SqlPersistT) +import Database.Persist.Sqlite import Discord import Discord.Types import Tablebot.Utility.Types @@ -81,3 +81,17 @@ data CompiledCronJob = CCronJob { timeframe :: Int, onCron :: CompiledDatabaseDiscord () } + +data AliasType = AliasPublic | AliasPrivate UserId + deriving (Eq, Show, Ord) + +instance PersistField AliasType where + toPersistValue (AliasPrivate (Snowflake wd)) = PersistInt64 (fromIntegral wd) + toPersistValue AliasPublic = PersistInt64 (-1) + fromPersistValue = \case + PersistInt64 (-1) -> Right AliasPublic + PersistInt64 i -> Right $ AliasPrivate (fromIntegral i) + _ -> Left "AliasType: fromPersistValue: Invalid value" + +instance PersistFieldSql AliasType where + sqlType _ = SqlInt64 diff --git a/src/Tablebot/Plugins.hs b/src/Tablebot/Plugins.hs index 56e72e3b..5de55105 100644 --- a/src/Tablebot/Plugins.hs +++ b/src/Tablebot/Plugins.hs @@ -19,6 +19,7 @@ import Tablebot.Internal.Administration (ShutdownReason) import Tablebot.Internal.Plugins (compilePlugin) import Tablebot.Internal.Types (CompiledPlugin) import Tablebot.Plugins.Administration (administrationPlugin) +import Tablebot.Plugins.Alias (aliasPlugin) import Tablebot.Plugins.Basic (basicPlugin) import Tablebot.Plugins.Cats (catPlugin) import Tablebot.Plugins.Dogs (dogPlugin) @@ -40,6 +41,7 @@ plugins rFlag = addAdministrationPlugin rFlag [ compilePlugin pingPlugin, + compilePlugin aliasPlugin, compilePlugin basicPlugin, compilePlugin catPlugin, compilePlugin dogPlugin, diff --git a/src/Tablebot/Plugins/Alias.hs b/src/Tablebot/Plugins/Alias.hs new file mode 100644 index 00000000..94122fb7 --- /dev/null +++ b/src/Tablebot/Plugins/Alias.hs @@ -0,0 +1,158 @@ +module Tablebot.Plugins.Alias (aliasPlugin) where + +import Data.Text (Text) +import qualified Data.Text as T +import qualified Database.Persist.Sqlite as Sql +import Discord.Types (Message (messageAuthor), User (userId)) +import Tablebot.Internal.Alias +import Tablebot.Internal.Types (AliasType (..)) +import Tablebot.Utility +import Tablebot.Utility.Database (deleteBy, exists) +import Tablebot.Utility.Discord (sendMessage) +import Tablebot.Utility.Permission (requirePermission) +import Tablebot.Utility.SmartParser (PComm (parseComm), Quoted (..), WithError (..)) +import Text.RawString.QQ (r) + +aliasTypeToText :: AliasType -> Text +aliasTypeToText AliasPublic = "Public" +aliasTypeToText (AliasPrivate _) = "Private" + +updateAlias :: Alias -> EnvDatabaseDiscord d (Sql.Entity Alias) +updateAlias alias = liftSql $ Sql.upsertBy (UniqueAlias (aliasAlias alias) (aliasType alias)) alias [AliasCommand Sql.=. aliasCommand alias] + +aliasPlugin :: Plugin +aliasPlugin = + (plug "alias") + { commands = [aliasComm], + migrations = [aliasMigration], + helpPages = [aliasHelp] + } + +aliasComm :: Command +aliasComm = + Command + "alias" + (parseComm (\m -> aliasList (AliasPrivate (userId $ messageAuthor m)) m)) + [aliasAddCommand, aliasListCommand, aliasDeleteCommand] + +aliasHelp :: HelpPage +aliasHelp = + HelpPage + "alias" + [] + "alias a command to another command" + [r|**Aliases** +Allows creation, viewing, and deletion of aliases. +Calling without any arguments will show a list of aliases. + +*Usage:* `alias`|] + [aliasAddHelp, aliasListHelp, aliasDeleteHelp] + None + +aliasAddCommand :: Command +aliasAddCommand = + Command + "add" + (parseComm aliasAddPrivateComm) + [ Command "private" (parseComm aliasAddPrivateComm) [], + Command "public" (parseComm aliasAddPublicComm) [] + ] + where + aliasAddPrivateComm :: WithError "Need a single word" Text -> WithError "Need a quoted string" (Quoted Text) -> Message -> DatabaseDiscord () + aliasAddPrivateComm (WErr t) (WErr (Qu t')) m = aliasAdd t t' (AliasPrivate (userId $ messageAuthor m)) m + aliasAddPublicComm :: WithError "Need a single word" Text -> WithError "Need a quoted string" (Quoted Text) -> Message -> DatabaseDiscord () + aliasAddPublicComm (WErr t) (WErr (Qu t')) m = requirePermission Moderator m $ aliasAdd t t' AliasPublic m + +aliasAdd :: Text -> Text -> AliasType -> Message -> DatabaseDiscord () +aliasAdd a b at m = do + let new = Alias a b at + _ <- updateAlias new + sendMessage m ("Added " <> T.toLower (aliasTypeToText at) <> " alias `" <> a <> "` -> `" <> b <> "`") + +aliasAddHelp :: HelpPage +aliasAddHelp = + HelpPage + "add" + [] + "adds an alias" + [r|**Add Alias** +Adds an alias. +You can specify whether the alias is public or private. +Public aliases can only be added by moderators. + +*Usage:* `alias add ""`, `alias add private ""` , `alias add public ""`|] + [] + None + +aliasListCommand :: Command +aliasListCommand = + Command + "list" + (parseComm aliasListPrivateComm) + [ Command "private" (parseComm aliasListPrivateComm) [], + Command "public" (parseComm aliasListPublicComm) [] + ] + where + aliasListPrivateComm :: Message -> DatabaseDiscord () + aliasListPrivateComm m = aliasList (AliasPrivate (userId $ messageAuthor m)) m + aliasListPublicComm :: Message -> DatabaseDiscord () + aliasListPublicComm m = aliasList AliasPublic m + +aliasList :: AliasType -> Message -> DatabaseDiscord () +aliasList at m = do + aliases <- fmap Sql.entityVal <$> liftSql (Sql.selectList [AliasType Sql.==. at] []) + let msg = + aliasTypeToText at <> " aliases:\n" + <> T.unlines (map (\(Alias a b _) -> "\t`" <> a <> "` -> `" <> b <> "`") aliases) + sendMessage m msg + +aliasListHelp :: HelpPage +aliasListHelp = + HelpPage + "list" + [] + "lists aliases" + [r|**List Aliases** +Lists all aliases. +You can specify whether the aliases are public or private. + +*Usage:* `alias list`, `alias list private`, `alias list public`|] + [] + None + +aliasDeleteCommand :: Command +aliasDeleteCommand = + Command + "delete" + (parseComm aliasDeletePrivateComm) + [ Command "private" (parseComm aliasDeletePrivateComm) [], + Command "public" (parseComm aliasDeletePublicComm) [] + ] + where + aliasDeletePrivateComm :: WithError "Need a single word" Text -> Message -> DatabaseDiscord () + aliasDeletePrivateComm (WErr t) m = aliasDelete t (AliasPrivate (userId $ messageAuthor m)) m + aliasDeletePublicComm :: WithError "Need a single word" Text -> Message -> DatabaseDiscord () + aliasDeletePublicComm (WErr t) m = requirePermission Moderator m $ aliasDelete t AliasPublic m + +aliasDelete :: Text -> AliasType -> Message -> DatabaseDiscord () +aliasDelete a at m = do + let toDelete = UniqueAlias a at + itemExists <- exists [AliasAlias Sql.==. a, AliasType Sql.==. at] + if itemExists + then deleteBy toDelete >> sendMessage m ("Deleted alias `" <> a <> "`") + else sendMessage m ("No such alias `" <> a <> "`") + +aliasDeleteHelp :: HelpPage +aliasDeleteHelp = + HelpPage + "delete" + [] + "deletes an alias" + [r|**Delete Alias** +Deletes an alias. +You can specify whether the alias to delete is public or private. +Public aliases can only be deleted by moderators. + +*Usage:* `alias delete `, `alias delete private `, `alias delete public `|] + [] + None From 441418d884cabe53cc10683ad9a4c3960d8be2ad Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 6 Apr 2022 22:37:41 +0100 Subject: [PATCH 71/96] changed let values to be called var values instead --- src/Tablebot/Plugins/Roll/Dice.hs | 6 ++-- src/Tablebot/Plugins/Roll/Dice/DiceData.hs | 10 +++--- src/Tablebot/Plugins/Roll/Dice/DiceEval.hs | 34 +++++++++---------- src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 30 ++++++++-------- src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 18 +++++----- 5 files changed, 49 insertions(+), 49 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Dice.hs b/src/Tablebot/Plugins/Roll/Dice.hs index d9d2c3e6..0e483884 100644 --- a/src/Tablebot/Plugins/Roll/Dice.hs +++ b/src/Tablebot/Plugins/Roll/Dice.hs @@ -31,9 +31,9 @@ -- -- prog - stat* (lstv | expr) -- stat - (lstv | expr) ";" --- misc - ifst | lets +-- misc - ifst | vars -- ifst - "if" spc1 expr spc1 "then" spc1 (lstv | expr) spc1 "else" spc1 (lstv | expr) --- lets - "let" spc1 "!"? ("l_" name spcs "=" spcs lstv | name spcs "=" spcs expr) +-- vars - "var" spc1 "!"? ("l_" name spcs "=" spcs lstv | name spcs "=" spcs expr) -- lstv - nbse "#" base | funcBasics | lstb | name | misc -- lstb - "{" expr ("," expr)* "}" | "(" lstv ")" -- expr - term ([+-] expr)? | misc @@ -58,7 +58,7 @@ -- stat (Statement) - representing a single statement - an expression or list value -- misc (MiscData) - either an if or a let -- ifst (If) - representing one of two values depending on the outcome of an expression --- lets (Let) - setting a variable to a certain value +-- vars (Var) - setting a variable to a certain value -- lstv (ListValues) - representing all possible list values (basic list values, functions that return lists, and values which are lists of length N that consist of `Base`s, as well as a MiscData value) -- lstb (ListValuesBase) - representing some basic list values (those that can be used in dice expressions, such as manually created lists and bracketed `ListValues`) -- expr (Expr) - representing addition, subtraction, or a single `Term` value, or a MiscData value diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs index 3c342e0b..f844de0c 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs @@ -21,18 +21,18 @@ import Tablebot.Plugins.Roll.Dice.DiceFunctions (FuncInfo, FuncInfoBase) -- evaluated `letValue`. -- -- List variables have to be prefixed with `l_`. This really helps with parsing. -data Let a = Let {letName :: Text, letValue :: a} | LetLazy {letName :: Text, letValue :: a} deriving (Show) +data Var a = Var {letName :: Text, letValue :: a} | VarLazy {letName :: Text, letValue :: a} deriving (Show) -- | If the first value is truthy (non-zero or a non-empty list) then return -- the `thenValue`, else return the `elseValue`. data If b = If {ifCond :: Expr, thenValue :: b, elseValue :: b} deriving (Show) --- | Either an If or a Let that returns a `b`. -data MiscData b = MiscIf (If b) | MiscLet (Let b) deriving (Show) +-- | Either an If or a Var that returns a `b`. +data MiscData b = MiscIf (If b) | MiscVar (Var b) deriving (Show) -- | An expression is just an Expr or a ListValues with a semicolon on the end. -- --- When evaluating, LetLazy expressions are handled with a special case - they +-- When evaluating, VarLazy expressions are handled with a special case - they -- are not evaluated until the value is first referenced. Otherwise, the value -- is evaluated as the statement is encountered data Statement = StatementExpr Expr | StatementListValues ListValues deriving (Show) @@ -104,7 +104,7 @@ newtype Paren a = Paren a deriving (Show) -- | The type representing a numeric base value value or a dice value. -data Base = NBase NumBase | DiceBase Dice | Var Text +data Base = NBase NumBase | DiceBase Dice | NumVar Text deriving (Show) -- Dice Operations after this point diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index 95c26090..e8132f25 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs @@ -203,7 +203,7 @@ instance IOEvalList ListValuesBase where evalShowL' (LVBParen (Paren lv)) = evalShowL lv instance IOEvalList ListValuesMisc where - evalShowL' (MiscLet l) = evalShowL l + evalShowL' (MiscVar l) = evalShowL l evalShowL' (MiscIf l) = evalShowL l -- | This type class gives a function which evaluates the value to an integer @@ -221,7 +221,7 @@ class IOEval a where instance IOEval Base where evalShow' (NBase nb) = evalShow nb evalShow' (DiceBase dice) = evalShow dice - evalShow' (Var t) = do + evalShow' (NumVar t) = do vars <- gets getVariables case M.lookup t vars of Just (Right e) -> evalShow e >>= \(i, _) -> return (i, t) @@ -389,7 +389,7 @@ binOpHelp a b opS op = do return (op a' b', a's <> " " <> opS <> " " <> b's) instance IOEval ExprMisc where - evalShow' (MiscLet l) = evalShow l + evalShow' (MiscVar l) = evalShow l evalShow' (MiscIf l) = evalShow l instance IOEval Expr where @@ -441,22 +441,22 @@ instance IOEval NumBase where return (r, "(" <> s <> ")") evalShow' (Value i) = return (i, pack (show i)) -instance IOEval (Let Expr) where - evalShow' (Let t a) = do +instance IOEval (Var Expr) where + evalShow' (Var t a) = do (v, lt) <- evalShow a addVariable t (Right $ promote v) - return (v, "let " <> t <> " = " <> lt) - evalShow' l@(LetLazy t a) = do + return (v, "var " <> t <> " = " <> lt) + evalShow' l@(VarLazy t a) = do (v, _) <- evalShow a addVariable t (Right a) return $ v `seq` (v, prettyShow l) -instance IOEvalList (Let ListValues) where - evalShowL' l@(Let t a) = do +instance IOEvalList (Var ListValues) where + evalShowL' l@(Var t a) = do (v, _) <- evalShowL a addVariable t (Left $ promote $ fst <$> v) return (v, Just (prettyShow l)) - evalShowL' l@(LetLazy t a) = do + evalShowL' l@(VarLazy t a) = do (v, _) <- evalShowL a addVariable t (Left a) return (v, Just (prettyShow l)) @@ -464,11 +464,11 @@ instance IOEvalList (Let ListValues) where evalStatement :: Statement -> ProgramStateM Text evalStatement (StatementExpr l) = evalShowStatement l >>= \(_, t) -> return (t <> "; ") where - evalShowStatement (ExprMisc (MiscLet l'@(LetLazy t a))) = addVariable t (Right a) >> return (0, prettyShow l') + evalShowStatement (ExprMisc (MiscVar l'@(VarLazy t a))) = addVariable t (Right a) >> return (0, prettyShow l') evalShowStatement l' = evalShow l' evalStatement (StatementListValues l) = evalShowStatement l >>= \(_, t) -> return (fromMaybe (prettyShow l) t <> "; ") where - evalShowStatement (ListValuesMisc (MiscLet l'@(LetLazy t a))) = addVariable t (Left a) >> return ([], Just (prettyShow l')) + evalShowStatement (ListValuesMisc (MiscVar l'@(VarLazy t a))) = addVariable t (Left a) >> return ([], Just (prettyShow l')) evalShowStatement l' = evalShowL l' instance IOEval (If Expr) where @@ -513,7 +513,7 @@ instance PrettyShow ListValuesBase where prettyShow (LVBParen p) = prettyShow p instance PrettyShow a => PrettyShow (MiscData a) where - prettyShow (MiscLet l) = prettyShow l + prettyShow (MiscVar l) = prettyShow l prettyShow (MiscIf l) = prettyShow l instance PrettyShow Expr where @@ -549,7 +549,7 @@ instance (PrettyShow a) => PrettyShow (Paren a) where instance PrettyShow Base where prettyShow (NBase nb) = prettyShow nb prettyShow (DiceBase dop) = prettyShow dop - prettyShow (Var t) = t + prettyShow (NumVar t) = t instance PrettyShow Die where prettyShow (Die b) = "d" <> prettyShow b @@ -576,9 +576,9 @@ instance (PrettyShow a, PrettyShow b) => PrettyShow (Either a b) where prettyShow (Left a) = prettyShow a prettyShow (Right b) = prettyShow b -instance (PrettyShow a) => PrettyShow (Let a) where - prettyShow (Let t a) = "let " <> t <> " = " <> prettyShow a - prettyShow (LetLazy t a) = "let !" <> t <> " = " <> prettyShow a +instance (PrettyShow a) => PrettyShow (Var a) where + prettyShow (Var t a) = "var " <> t <> " = " <> prettyShow a + prettyShow (VarLazy t a) = "var !" <> t <> " = " <> prettyShow a instance (PrettyShow b) => PrettyShow (If b) where prettyShow (If b t e) = "if " <> prettyShow b <> " then " <> prettyShow t <> " else " <> prettyShow e diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index f8971e68..d7d81184 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -40,10 +40,10 @@ failure' s ss = failure (Just $ Tokens $ NE.fromList $ T.unpack s) (S.map (Token varName :: Parser T.Text varName = T.pack <$> some (choice $ char <$> '_' : ['a' .. 'z']) -instance CanParse a => CanParse (Let a) where +instance CanParse a => CanParse (Var a) where pars = do - _ <- try (string "let") <* skipSpace - letCon <- try (char '!' $> LetLazy) <|> return Let + _ <- try (string "var") <* skipSpace + letCon <- try (char '!' $> VarLazy) <|> return Var varName' <- varName _ <- skipSpace >> char '=' >> skipSpace letCon varName' <$> pars @@ -55,21 +55,21 @@ instance CanParse Statement where -- alternative method to the above. -- from https://canary.discord.com/channels/280033776820813825/280036215477239809/938154455612919838 -- - Morrow#1157 -newtype LetCon = LetCon (forall a. a -> Let a) +newtype VarCon = VarCon (forall a. a -> Var a) -parseLet :: Parser LetCon +parseLet :: Parser VarCon parseLet = do - _ <- try (string "let") <* skipSpace + _ <- try (string "var") <* skipSpace lazy <- try (char '!' $> True) <|> return False varName' <- varName _ <- skipSpace >> char '=' >> skipSpace - return $ LetCon (\a -> if lazy then LetLazy varName' a else Let varName' a) + return $ VarCon (\a -> if lazy then VarLazy varName' a else Var varName' a) instance CanParse Statement where pars = do - LetCon letP <- parseLet + VarCon letP <- parseVar val <- (Left <$> pars <|> Right <$> pars) <* skipSpace <* char ';' <* skipSpace - return $ either (LetList . letP) (LetExpr . letP) val + return $ either (VarList . letP) (VarExpr . letP) val -} parseStatements :: Parser [Statement] @@ -87,14 +87,14 @@ instance CanParse ListValues where do functionParser listFunctions LVFunc <|> (LVVar . ("l_" <>) <$> try (string "l_" *> varName)) - <|> ListValuesMisc <$> (pars >>= checkLet) + <|> ListValuesMisc <$> (pars >>= checkVar) <|> (try (pars <* char '#') >>= \nb -> MultipleValues nb <$> pars) <|> LVBase <$> pars where - checkLet (MiscLet l) - | T.isPrefixOf "l_" (letName l) = return (MiscLet l) + checkVar (MiscVar l) + | T.isPrefixOf "l_" (letName l) = return (MiscVar l) | otherwise = fail "list variables must be prepended with l_" - checkLet l = return l + checkVar l = return l instance CanParse ListValuesBase where pars = do @@ -122,7 +122,7 @@ instance (CanParse b) => CanParse (If b) where return $ If a t e instance CanParse a => CanParse (MiscData a) where - pars = (MiscLet <$> pars) <|> (MiscIf <$> pars) + pars = (MiscVar <$> pars) <|> (MiscIf <$> pars) instance CanParse Expr where pars = @@ -183,7 +183,7 @@ instance CanParse Base where <|> return (NBase nb) ) <|> DiceBase <$> parseDice (Value 1) - <|> (Var <$> try varName) + <|> (NumVar <$> try varName) instance CanParse Die where pars = do diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 9bd29f6c..6090be29 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -85,11 +85,11 @@ class PrettyShow a => Range a where range' :: (MonadException m, PrettyShow a) => a -> m Experiment instance (Range a) => Range (MiscData a) where - range' (MiscLet l) = range l + range' (MiscVar l) = range l range' (MiscIf i) = rangeIfExpr range i instance (RangeList a) => RangeList (MiscData a) where - rangeList' (MiscLet l) = rangeList l + rangeList' (MiscVar l) = rangeList l rangeList' (MiscIf i) = rangeIfExpr rangeList i rangeIfExpr :: (MonadException m, Ord b) => (a -> m (D.Experiment b)) -> If a -> m (D.Experiment b) @@ -120,13 +120,13 @@ rangeIfExpr func (If b t f) = do -- b'' <- b' -- if b'' /= [] then t' else f' -instance (Range a) => Range (Let a) where - range' (Let _ a) = range a - range' (LetLazy _ a) = range a +instance (Range a) => Range (Var a) where + range' (Var _ a) = range a + range' (VarLazy _ a) = range a -instance (RangeList a) => RangeList (Let a) where - rangeList' (Let _ a) = rangeList a - rangeList' (LetLazy _ a) = rangeList a +instance (RangeList a) => RangeList (Var a) where + rangeList' (Var _ a) = rangeList a + rangeList' (VarLazy _ a) = rangeList a instance Range Expr where range' (NoExpr t) = range t @@ -166,7 +166,7 @@ instance Range NumBase where instance Range Base where range' (NBase nb) = range nb range' (DiceBase d) = range d - range' b@(Var _) = evaluationException "cannot find range of variable" [prettyShow b] + range' b@(NumVar _) = evaluationException "cannot find range of variable" [prettyShow b] instance Range Die where range' (LazyDie d) = range d From edee8de10ea69b9ae90921abd4d7188d4bfe5d6b Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Wed, 6 Apr 2022 23:59:23 +0100 Subject: [PATCH 72/96] add options to either ignore appcomms or make it global. also fixed a bug with slash command processing --- README.md | 3 ++- src/Tablebot/Handler.hs | 37 +++++++++++++++++------------ src/Tablebot/Utility/Discord.hs | 26 +++++++++++++------- src/Tablebot/Utility/SmartParser.hs | 8 ++++--- 4 files changed, 46 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 0985e0d9..40157787 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ Create a `.env` file containing the following keys. Consult `.env.example` if yo * `EXEC_GROUP` (optional) - the group ID assigned to exec members. * `MODERATOR_GROUP` (optional) - the group ID assigned to moderator members. * `SUPERUSER_GROUP` (optional) - the group ID assigned to the superuser. Strongly recommended -* `SERVER_ID` - the id of the server the bot will mainly be deployed in. Application commands will be registered here. +* `SERVER_ID` (optional) - either `global` or the id of the server the bot will mainly be deployed in. Application commands will be + registered here. If absent, application commands won't be registered. The three Group settings are optional, but without them any commands that require elevated permissions will not be able to be called when DEBUG is false. Users with the superuser group are able to run every command (including some dangerous diff --git a/src/Tablebot/Handler.hs b/src/Tablebot/Handler.hs index 38a98c95..d4848578 100644 --- a/src/Tablebot/Handler.hs +++ b/src/Tablebot/Handler.hs @@ -31,7 +31,7 @@ import Discord (Cache (cacheApplication), DiscordHandler, readCache, restCall) import Discord.Interactions (ApplicationCommand (..), Interaction (..)) import Discord.Requests (ChannelRequest (JoinThread)) import Discord.Types -import System.Environment (getEnv) +import System.Environment (lookupEnv) import Tablebot.Internal.Handler.Command (parseNewMessage) import Tablebot.Internal.Handler.Event ( parseApplicationCommandRecv, @@ -116,23 +116,30 @@ killCron = mapM_ killThread -- | Given a list of compiled application commands and a pointer to the -- tablebot cache, create the given application commands, purge ones that --- weren't created by us, and place the application command id's and their +-- weren't created by us, and place the application command id's and their -- actions in the cache. submitApplicationCommands :: [CompiledApplicationCommand] -> MVar TablebotCache -> DiscordHandler () submitApplicationCommands compiledAppComms cacheMVar = ( do -- generate the application commands, cleaning up any application commands we don't like - serverIdStr <- liftIO $ getEnv "SERVER_ID" - serverId <- maybe (fail "could not read server id") return (readMaybe serverIdStr) - aid <- partialApplicationID . cacheApplication <$> readCache - applicationCommands <- - mapM - ( \(CApplicationCommand cac action) -> do - ac <- createApplicationCommand aid serverId cac - return (applicationCommandId ac, action) - ) - compiledAppComms - removeApplicationCommandsNotInList aid serverId (fst <$> applicationCommands) - liftIO $ takeMVar cacheMVar >>= \tcache -> putMVar cacheMVar $ tcache {cacheApplicationCommands = M.fromList (second (lift .) <$> applicationCommands)} + serverIdStr' <- liftIO $ lookupEnv "SERVER_ID" + case serverIdStr' of + Nothing -> pure () + Just serverIdStr -> do + serverId <- readServerStr serverIdStr + aid <- partialApplicationID . cacheApplication <$> readCache + applicationCommands <- + mapM + ( \(CApplicationCommand cac action) -> do + ac <- createApplicationCommand aid serverId cac + return (applicationCommandId ac, action) + ) + compiledAppComms + removeApplicationCommandsNotInList aid serverId (fst <$> applicationCommands) + liftIO $ takeMVar cacheMVar >>= \tcache -> putMVar cacheMVar $ tcache {cacheApplicationCommands = M.fromList (second (lift .) <$> applicationCommands)} ) - `catch` \(_ :: IOError) -> liftIO $ putStrLn "There was an error of some sort when submitting the application commands - verify that `SERVER_ID` is set." + `catch` \(e :: IOError) -> liftIO $ putStrLn $ "There was an error of some sort when submitting the application commands - verify that `SERVER_ID` is set properly. (" <> show e <> ")" + where + readServerStr :: String -> DiscordHandler (Maybe GuildId) + readServerStr "global" = return Nothing + readServerStr s = maybe (fail $ "could not read server id: " <> show s) (return . Just) (readMaybe s) diff --git a/src/Tablebot/Utility/Discord.hs b/src/Tablebot/Utility/Discord.hs index 7bedd77a..a24cd5e5 100644 --- a/src/Tablebot/Utility/Discord.hs +++ b/src/Tablebot/Utility/Discord.hs @@ -363,25 +363,33 @@ formatText CodeBlock s = "```" <> s <> "```" extractFromSnowflake :: Snowflake -> Word64 extractFromSnowflake (Snowflake w) = w --- | When given an application id, server id, and a CreateApplicationCommand --- object, create the application command. -createApplicationCommand :: ApplicationId -> GuildId -> CreateApplicationCommand -> DiscordHandler ApplicationCommand +-- | When given an application id, an optional server id, and a +-- CreateApplicationCommand object, create the application command. +createApplicationCommand :: ApplicationId -> Maybe GuildId -> CreateApplicationCommand -> DiscordHandler ApplicationCommand createApplicationCommand aid gid cac = do - res <- restCall $ R.CreateGuildApplicationCommand aid gid cac + res <- createAppComm case res of Left e -> throw $ InteractionException $ "Failed to create application command :" ++ show e Right a -> return a + where + createAppComm = case gid of + Nothing -> restCall $ R.CreateGlobalApplicationCommand aid cac + Just gid' -> restCall $ R.CreateGuildApplicationCommand aid gid' cac --- | Remove all application commands that are active in the given server that --- aren't in the given list. -removeApplicationCommandsNotInList :: ApplicationId -> GuildId -> [ApplicationCommandId] -> DiscordHandler () +-- | Remove all application commands that are active (optionally in the given +-- server) that aren't in the given list. +removeApplicationCommandsNotInList :: ApplicationId -> Maybe GuildId -> [ApplicationCommandId] -> DiscordHandler () removeApplicationCommandsNotInList aid gid aciToKeep = do - allACs' <- restCall $ R.GetGuildApplicationCommands aid gid + allACs' <- getAppComm case allACs' of Left _ -> throw $ InteractionException "Failed to get all application commands." Right aacs -> let allACs = applicationCommandId <$> aacs - in mapM_ (restCall . R.DeleteGuildApplicationCommand aid gid) (allACs \\ aciToKeep) + in mapM_ deleteAppComm (allACs \\ aciToKeep) + where + (getAppComm, deleteAppComm) = case gid of + Nothing -> (restCall $ R.GetGlobalApplicationCommands aid, restCall . R.DeleteGlobalApplicationCommand aid) + Just gid' -> (restCall $ R.GetGuildApplicationCommands aid gid', restCall . R.DeleteGuildApplicationCommand aid gid') -- | Defer an interaction response, extending the window of time to respond to -- 15 minutes (from 3 seconds). diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index cf42eb46..738ac107 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -328,7 +328,6 @@ instance (KnownSymbol name, KnownSymbol desc, MakeAppCommArg (Labelled name desc class ProcessAppComm commandty s where processAppComm :: commandty -> Interaction -> EnvDatabaseDiscord s () - processAppComm _ _ = throwBot $ InteractionException "could not process args to application command" -- One base case instance {-# OVERLAPPING #-} ProcessAppComm (EnvDatabaseDiscord s MessageDetails) s where @@ -340,9 +339,12 @@ instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s) => ProcessAppComm (Interact -- one overarching recursive case instance {-# OVERLAPPABLE #-} (ProcessAppCommArg ty s, ProcessAppComm pac s) => ProcessAppComm (ty -> pac) s where - processAppComm comm i@InteractionApplicationCommand {interactionDataApplicationCommand = InteractionDataApplicationCommandChatInput {interactionDataApplicationCommandOptions = (Just (InteractionDataApplicationCommandOptionsValues values))}} = do - t <- processAppCommArg values + processAppComm comm i@InteractionApplicationCommand {interactionDataApplicationCommand = InteractionDataApplicationCommandChatInput {interactionDataApplicationCommandOptions = opts}} = do + t <- processAppCommArg (getVs opts) processAppComm (comm t) i + where + getVs (Just (InteractionDataApplicationCommandOptionsValues vs)) = vs + getVs _ = [] processAppComm _ _ = throwBot $ InteractionException "could not process args to application command" -- one specific implementation case From dc11340e4d030f625d27c0c582933e779d69349f Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 7 Apr 2022 19:10:02 +0100 Subject: [PATCH 73/96] update discord-haskell, limit reroll button --- src/Tablebot/Plugins/Roll/Plugin.hs | 11 ++++++++--- stack.yaml | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 79ad3f7a..067ae4ef 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -20,8 +20,8 @@ import Discord (restCall) import Discord.Interactions ( Interaction (..), ) -import Discord.Internal.Rest.Channel (ChannelRequest (CreateMessageDetailed), MessageDetailedOpts (MessageDetailedOpts)) -import Discord.Types (ComponentActionRow (ComponentActionRowButton), ComponentButton (componentButtonEmoji), Emoji (Emoji), Message (messageAuthor, messageChannelId), User (userId), mkButton) +import Discord.Internal.Rest.Channel (ChannelRequest (..), MessageDetailedOpts (..)) +import Discord.Types (ComponentActionRow (..), ComponentButton (..), Emoji (..), Message (..), User (..), mkButton) import System.Timeout (timeout) import Tablebot.Internal.Handler.Command (parseValue) import Tablebot.Plugins.Roll.Dice @@ -70,7 +70,10 @@ rollDice' e t u@(ParseUserId uid) = do { messageDetailsComponents = Just [ ComponentActionRowButton - [ (mkButton "Reroll" ((("roll reroll " <> pack (show uid)) `appendIf` e) `appendIf` t)) {componentButtonEmoji = Just (Emoji (Just 0) "🎲" Nothing Nothing Nothing (Just False))} + [ (mkButton buttonName (T.take 100 buttonCustomId)) + { componentButtonEmoji = Just (Emoji (Just 0) "🎲" Nothing Nothing Nothing (Just False)), + componentButtonDisabled = buttonDisabled + } ] ] } @@ -78,6 +81,8 @@ rollDice' e t u@(ParseUserId uid) = do where appendIf t' Nothing = t' appendIf t' (Just e') = t' <> " " <> parseShow e' + buttonCustomId = (("roll reroll " <> pack (show uid)) `appendIf` e) `appendIf` t + (buttonName, buttonDisabled) = if T.length buttonCustomId > 100 then ("Expr too long", True) else ("Reroll", False) rollSlashCommandFunction :: Labelled "expression" "what's being evaluated" (Maybe Text) -> Labelled "quote" "associated message" (Maybe (Quoted Text)) -> ParseUserId -> DatabaseDiscord MessageDetails rollSlashCommandFunction (Labelled mt) (Labelled qt) uid = do diff --git a/stack.yaml b/stack.yaml index f06ea69c..c3a22f23 100644 --- a/stack.yaml +++ b/stack.yaml @@ -42,7 +42,7 @@ packages: # allow-newer: true extra-deps: -- discord-haskell-1.12.4 +- discord-haskell-1.12.5 - emoji-0.1.0.2 - load-env-0.2.1.0 - megaparsec-9.0.1 From fe15ebef0b7fe7e22782222ce97966f3e97093c8 Mon Sep 17 00:00:00 2001 From: Finnbar Keating Date: Wed, 4 May 2022 09:36:35 +0100 Subject: [PATCH 74/96] Removed .issue and .pr, added more to .about --- src/Tablebot/Plugins/Basic.hs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Tablebot/Plugins/Basic.hs b/src/Tablebot/Plugins/Basic.hs index 9962b643..5d81945f 100644 --- a/src/Tablebot/Plugins/Basic.hs +++ b/src/Tablebot/Plugins/Basic.hs @@ -16,6 +16,7 @@ import Tablebot.Utility.Discord (sendMessage) import Tablebot.Utility.SmartParser (parseComm) import Text.Megaparsec (anySingle, skipManyTill) import Text.Megaparsec.Char (string') +import Text.RawString.QQ (r) -- * Some types to help clarify what's going on @@ -31,20 +32,12 @@ type BasicCommand = (Text, Text, MiniHelpPage) -- | The basic commands. basicCommands :: [BasicCommand] basicCommands = - [ ( "pr", - "You can make a pull request for that!", - Simple ("you know what to do", "You know what to do") - ), - ( "issue", - "You can submit an issue for that!", - Simple ("you know what you want someone else to do", "You know what you want someone else to do") - ), - ( "benji", + [ ( "benji", "<:benji_sit:920000993721196654>", Simple ("the almost mascot", "Though he may sit, when put to test, the gender cube proved it was best") ), ( "about", - "This bot was created by finnbar to replace a couple of other bots in Tabletop. It's written in Haskell, and you can find the github here: . There are setup guides and a contributor's guide to help you get started.", + aboutStr, Simple ("some information about the bot", "Some information about the bot, including how you can get involved") ), ( "inventory", @@ -52,6 +45,13 @@ basicCommands = Simple ("our board games inventory", "Our board games inventory, with a link to the actual inventory") ) ] + where + aboutStr = + [r|This bot was created by finnbar to replace a couple of other bots in Tabletop. +It's written in Haskell, and you can find the code here: . +If you would like to contribute, there are setup guides and a contributor's guide to help you get started! + +If you have found a bug, please report it on Github () or inform one of the maintainers.|] -- | @echo@ pulled out to help resolve parser overlapping instances errors. -- Sends the provided text, regardless of received message. From 272b6d5b5a2be764474a4a13bece2cb99069b5ac Mon Sep 17 00:00:00 2001 From: Finnbar Keating Date: Wed, 4 May 2022 09:52:26 +0100 Subject: [PATCH 75/96] Remove `ping` and `pong` Note that the code for the commands is kept for tutorial reasons. Implements this with `minusPl`, which subtracts from `allPlugins` --- app/Main.hs | 4 ++-- src/Tablebot/Plugins.hs | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/Main.hs b/app/Main.hs index 25607066..b1ee0e3b 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -4,11 +4,11 @@ module Main where import Data.Text (Text) import Tablebot (BotConfig (..), runTablebotWithEnv) -import Tablebot.Plugins (allPlugins) +import Tablebot.Plugins (allPlugins, minusPl) -- @main@ runs forever. This allows bot reloading by fully shutting down the bot and letting it restart. main :: IO () -main = runTablebotWithEnv allPlugins $ BotConfig {gamePlaying = "with dice", rootHelpText = rootBody} +main = runTablebotWithEnv (allPlugins `minusPl` ["ping"]) $ BotConfig {gamePlaying = "with dice", rootHelpText = rootBody} rootBody :: Text rootBody = diff --git a/src/Tablebot/Plugins.hs b/src/Tablebot/Plugins.hs index 76e5f468..1ad84ebe 100644 --- a/src/Tablebot/Plugins.hs +++ b/src/Tablebot/Plugins.hs @@ -12,9 +12,10 @@ module Tablebot.Plugins where import Control.Concurrent.MVar (MVar) +import Data.Text (Text) import Tablebot.Internal.Administration (ShutdownReason) import Tablebot.Internal.Plugins (compilePlugin) -import Tablebot.Internal.Types (CompiledPlugin) +import Tablebot.Internal.Types (CompiledPlugin (..)) import Tablebot.Plugins.Administration (administrationPlugin) import Tablebot.Plugins.Basic (basic) import Tablebot.Plugins.Cats (cat) @@ -53,3 +54,7 @@ allPlugins = -- | @addAdministrationPlugin@ is needed to allow the administration plugin to be aware of the list of current plugins addAdministrationPlugin :: MVar ShutdownReason -> [CompiledPlugin] -> [CompiledPlugin] addAdministrationPlugin rFlag cps = compilePlugin (administrationPlugin rFlag cps) : cps + +-- | @plugs `minusPl` names@ removes all plugins with the given names. +minusPl :: [CompiledPlugin] -> [Text] -> [CompiledPlugin] +minusPl = foldr (\n plugs -> filter ((/= n) . compiledName) plugs) From 6c46188a800df7d5a9e9d5832d5dfacb01a9e46f Mon Sep 17 00:00:00 2001 From: Finnbar Keating Date: Wed, 4 May 2022 10:34:02 +0100 Subject: [PATCH 76/96] Improvements to CONTRIBUTING.md (fixes #136) --- CONTRIBUTING.md | 49 +++++++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f72e1d08..a6a6efe1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,24 +4,43 @@ Welcome to the [University of Warwick Tabletop and Roleplaying Society](https:// Please feel free to contribute to the code in whatever way you are able. We're more than happy to accept new code, bug fixes, documentation, issues, and anything else you think will improve the bot! If you do start work on an issue, please first let us know in the issue's thread or in our Discord server to avoid competing pull requests. -## Code of Conduct +**Our society has a [Code of Conduct](https://www.warwicktabletop.co.uk/page/codeofconduct/). We expect it to be upheld by contributors in all our online spaces the same way we'd expect it to be upheld by attendees of our events.** -Our society has a [Code of Conduct](https://www.warwicktabletop.co.uk/page/codeofconduct/). We expect it to be upheld by contributors in all our online spaces the same way we'd expect it to be upheld by attendees of our events. +## What can I contribute? -## Good Practice Recommendations +We're happy to accept any contribution, big or small. You can find our list of issues [here](https://github.com/WarwickTabletop/tablebot/issues), which details bugs and feature requests. Both code (with documentation) and documentation alone is accepted - we want this project to be as accessible as possible, so any contributions must come with documentation or add documentation. -Here are some good practice tips for git and this project. +If you think of a feature you'd like added or find a bug in the current implementation please do create a new ticket! There's no obligation to implement the issue. If you don't have any ideas but do want to get involved with programming you can check the issues page for new features and fixes to work on. If you're not too familiar with Haskell or our codebase, look out for the "good first issue" label. We put this on issues that we think would be good for newcomers to the language/project to get started on. -* Whenever a new feature is being worked on, create a new branch on your forked repo -* When a feature is ready to be merged into the bot, make a pull request from the feature branch to the main repo -* Before making a pull request, make sure your branch is up to date with main (and that it compiles and complies with `ormolu` - see below for details) so that it can be merged without hassle -* Write comments! This project will be maintained by many people, and it can be difficult to work out what others' code does -* To communicate with the maintainers, please join the [Tabletop Discord server](https://warwicktabletop.co.uk/discord) and give yourself the @computer_person role to join the developer channel -* If you need help finding a function to do a particular task, you can search on [Hoogle](https://hoogle.haskell.org/). The two libraries that deal with parsing are `Text.Megaparsec` and `Control.Monad.Combinators`, and `Discord` is the package that deals with Discord itself. You can filter by package if that helps to find certain functions +If you have trouble at any time, please do ask for help in an issue thread or on our Discord. The maintainers generally communicate in the [Tabletop Discord server](https://warwicktabletop.co.uk/discord). The channel we use, #computers-were-a-mistake, is opt-in so you'll need to give yourself the @computer_person role in #roles. Finally, you can also check out the [tutorials](tutorials) in the repository and pre-existing solutions in the code for guidance. + +## How to contribute + +### The basics + +If you'd like to contribute code, these are the steps you'll need to follow along with some tips. (If you've contributed to an open-source Haskell project before, you likely know all of this.) + +* You'll need to fork this repo if you haven't already, and then create a branch for the feature you're implementing. Please split up features into different branches where you can. +* When a feature is ready to be merged into the bot, make a pull request from the feature branch to the main repo. In the next section we'll talk about pull requests. +* Before making a pull request (PR), **make sure your branch is up to date with main**. The CI must also not complain, so the code must compile and not throw any formatting errors. We use `ormolu` to check for the latter, which is detailed later. +* If you need help finding a function to do a particular task, you can search on [Hoogle](https://hoogle.haskell.org/). The two libraries that deal with parsing are `Text.Megaparsec` and `Control.Monad.Combinators`, and `Discord` is the package that deals with Discord itself. You can filter by package if that helps to find certain functions. You can check out the [README](README.md) for a brief overview on how to set up a local bot for testing. If you've never done something like this before, see the bottom of this document for a walkthrough. -## Ormolu +### Writing good Pull Requests (PRs) + +Writing good PRs is hard. As such, here are some important points to consider when writing your PR to make it easier for the reviewer. + +* **Ask yourself: what does this PR contribute?** It is very important that you're clear about all of the features being added, because they justify the changes you have made and point out what exactly needs testing. Mention _everything_ even if it is just cleaning up a file - this makes it clear what the point is of each change and doesn't leave the reviewer guessing why you've added a certain change. +* **Documentation is key.** PRs without documentation will be rejected. A few points to consider about documentation: + * Functions should have top-level documentation explaining what they do (even if it is very brief) unless they are self-evident. Classes should be justified in the same way. Use Haddock style. + * In larger implementations, it may help to talk about the high-level structure of your implementation - e.g. you might have a section of your plugin that deals with parsing messages, another which deals with some specific case, another which deals with the general case and so on. Make it clear how these parts interact. Splitting your implementation into multiple files may help here, with a base file that imports each auxiliary file and puts the results within those files together. + * Haskell code has a habit of being extremely abstract, and talking about it in the abstract does not aid understanding. Give concrete examples of how the abstract is used to justify its existence - instead of solely saying "we have a parser that doesn't look at `t`", back it up with an example like "this is used within a Discord interaction, so doesn't necessarily have an associated message". +* **Remember the plugin writer as well as the end user.** If you've written something that changes how plugins are written, update existing tutorials or add new ones. Make sure that the API you're defining is clear and easy to use, so doesn't put too much burden on someone writing a plugin. + +If you follow these steps, it becomes much easier for the reviewer to understand your code and thus feel confident about accepting it. This also allows the reviewer to make more helpful suggestions about the code itself - both allowing them to verify that the code does what you say it does, and that you've implemented it in a helpful way. The review process should help you write better code as well as making Tablebot as a whole better. + +### Ormolu To maintain consistent formatting you must use Ormolu, which can be installed via stack: @@ -33,7 +52,7 @@ Then you can run it on every file via: You can see full documentation on the [Ormolu repo](https://github.com/tweag/ormolu#usage). -### Running Ormolu automatically with git +#### Running Ormolu automatically with git You may also wish to set up Ormolu to run when you stage a file (get ready to commit it) - this can be done using `.gitattributes` and `.gitconfig` as follows. @@ -55,12 +74,6 @@ You may also wish to set up Ormolu to run when you stage a file (get ready to co That's it! (With thanks to Sam Coy for explaining this process) -## What can I work on? - -We're happy to accept any contribution, big or small. You can find our list of issues [here](https://github.com/WarwickTabletop/tablebot/issues). If you think of a feature you'd like added or a bug in the current implementation please do create a new ticket! There's no obligation to implement the issue. If you don't have any ideas but do want to get involved with programming you can check the issues page for new features and fixes to work on. If you're not too familiar with Haskell or our codebase, look out for the "good first issue" label. We put this on issues that we think would be good for newcomers to the language/project to get started on. - -If you have trouble at any time, please do ask for help in an issue thread or on our Discord. You can also check out the [tutorials](tutorials) in the repository and pre-existing solutions in the code for guidance. - ## Setup from Scratch If at any point something doesn't work, restart your computer first and try it again. If the problem persists please feel free to ask for help in the [Discord server](https://www.warwicktabletop.co.uk/discord/). Sections are marked depending on what OS they rely on, if any. From a0b425175488ae89c7112c874efdc02e3473744f Mon Sep 17 00:00:00 2001 From: Finnbar Keating Date: Wed, 4 May 2022 10:41:31 +0100 Subject: [PATCH 77/96] Moved setup into its own file. --- CONTRIBUTING.md | 45 +-------------------------------------------- README.md | 2 +- SETUP.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 45 deletions(-) create mode 100644 SETUP.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a6a6efe1..a9bf7c0a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,7 @@ If you'd like to contribute code, these are the steps you'll need to follow alon * Before making a pull request (PR), **make sure your branch is up to date with main**. The CI must also not complain, so the code must compile and not throw any formatting errors. We use `ormolu` to check for the latter, which is detailed later. * If you need help finding a function to do a particular task, you can search on [Hoogle](https://hoogle.haskell.org/). The two libraries that deal with parsing are `Text.Megaparsec` and `Control.Monad.Combinators`, and `Discord` is the package that deals with Discord itself. You can filter by package if that helps to find certain functions. -You can check out the [README](README.md) for a brief overview on how to set up a local bot for testing. If you've never done something like this before, see the bottom of this document for a walkthrough. +You can check out the [README](README.md) for a brief overview on how to set up a local bot for testing. If you've never done something like this before, see [SETUP.md](SETUP.md). ### Writing good Pull Requests (PRs) @@ -74,46 +74,3 @@ You may also wish to set up Ormolu to run when you stage a file (get ready to co That's it! (With thanks to Sam Coy for explaining this process) -## Setup from Scratch - -If at any point something doesn't work, restart your computer first and try it again. If the problem persists please feel free to ask for help in the [Discord server](https://www.warwicktabletop.co.uk/discord/). Sections are marked depending on what OS they rely on, if any. - -1. git, wsl, and vscode setup - 1. github - 1. Create a GitHub account - 2. Go to - 3. Click fork on the repo (should be top right) (this creates your own version of the repo) - 4. Take note of the url that your forked repo is on - 2. wsl and git (Windows) - 1. Install wsl by going to , and make sure it's in the right click context menu of folders - 2. Navigate to an empty folder on your computer that you want to do your programming from (the project folder) - 3. Shift-right click in the project folder, and click "open linux shell here" - 4. Type `git clone ` into the terminal - 5. The folder should be filled with a bunch of files and folders - 3. terminal and git (Linux) - 1. Navigate to an empty folder on your computer that you want to do your programming from (the project folder) - 2. Shift-right click in the project folder and press "open in terminal" - 3. Type `git clone ` into the terminal - 4. The folder should be filled with a bunch of files and folders - 4. vscode - 1. Install vscode from - 2. Install this - 3. From a terminal opened in the project folder, type `code .` - 4. There should soon be a window which has all the folders and files open on the left hand side -2. Haskell setup - 1. In any linux terminal window (wsl or the linux terminal itself), type `curl -sSL https://get.haskellstack.org/ | sh`, allowing sudo access and providing passwords as needed - 2. In the linux terminal window opened from the project folder (or the terminal open in the vscode window) run `stack build`, and then wait until it's done - 3. This will take a long time - 4. Make some tea, or maybe some coffee or hot chocolate - 5. If it didn't work, reopen all terminal windows and try again. if that doesn't work, restart your computer and try again - 6. Install this - 7. Open a file and marvel at the colours, and the fact you can hover over things and see values and stuff -3. Discord and Environment variables - 1. Create a file in the top level of the project folder called `.env`, based on the template in `.env.example` - 2. Follow the instructions in [Environment File Setup](README.md#environment-file-setup) to fill in the `.env`. Make sure to get a `DISCORD_TOKEN` and a `SQLITE_FILENAME` (which can be named anything, but use something like `database.db`) - 3. To run the bot, type `stack run` into the terminal, and the bot will start to run - 4. Make sure to invite the bot to a server so you can test it out! - -Congratulations, you now know the very basics needed to set up your own tablebot! - -To learn more about git, you should look up a tutorial or watch this video: diff --git a/README.md b/README.md index fd476758..021396ea 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ An extendable Discord bot framework written on top of [`discord-haskell`](https://github.com/aquarial/discord-haskell). -If you're new to this project, or completely new to git, and Haskell, you might be interested in looking at the [Setup from Scratch](CONTRIBUTING.md#setup-from-scratch) section in the [contributor's guide](CONTRIBUTING.md). If you want tutorials on making your first plugin or how exceptions work, checkout the tutorials in the [tutorials](tutorials) folder. +If you're new to this project, or completely new to git, and Haskell, you might be interested in looking at the [Setup from Scratch](SETUP.md) guide. If you want to contribute, please consult the [contributor's guide](CONTRIBUTING.md). If you want tutorials on making your first plugin or how exceptions work, checkout the tutorials in the [tutorials](tutorials) folder. ## Environment file setup diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 00000000..0b3c6e10 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,46 @@ +# Setup from Scratch + +This document details the setup process of downloading a development copy of Tablebot. +This was originally authored by Benji (Git won't reflect this as it was taken out of CONTRIBUTING.md). + +If at any point something doesn't work, restart your computer first and try it again. If the problem persists please feel free to ask for help in the [Discord server](https://www.warwicktabletop.co.uk/discord/). Sections are marked depending on what OS they rely on, if any. + +1. git, wsl, and vscode setup + 1. github + 1. Create a GitHub account + 2. Go to + 3. Click fork on the repo (should be top right) (this creates your own version of the repo) + 4. Take note of the url that your forked repo is on + 2. wsl and git (Windows) + 1. Install wsl by going to , and make sure it's in the right click context menu of folders + 2. Navigate to an empty folder on your computer that you want to do your programming from (the project folder) + 3. Shift-right click in the project folder, and click "open linux shell here" + 4. Type `git clone ` into the terminal + 5. The folder should be filled with a bunch of files and folders + 3. terminal and git (Linux) + 1. Navigate to an empty folder on your computer that you want to do your programming from (the project folder) + 2. Shift-right click in the project folder and press "open in terminal" + 3. Type `git clone ` into the terminal + 4. The folder should be filled with a bunch of files and folders + 4. vscode + 1. Install vscode from + 2. Install this + 3. From a terminal opened in the project folder, type `code .` + 4. There should soon be a window which has all the folders and files open on the left hand side +2. Haskell setup + 1. In any linux terminal window (wsl or the linux terminal itself), type `curl -sSL https://get.haskellstack.org/ | sh`, allowing sudo access and providing passwords as needed + 2. In the linux terminal window opened from the project folder (or the terminal open in the vscode window) run `stack build`, and then wait until it's done + 3. This will take a long time + 4. Make some tea, or maybe some coffee or hot chocolate + 5. If it didn't work, reopen all terminal windows and try again. if that doesn't work, restart your computer and try again + 6. Install this + 7. Open a file and marvel at the colours, and the fact you can hover over things and see values and stuff +3. Discord and Environment variables + 1. Create a file in the top level of the project folder called `.env`, based on the template in `.env.example` + 2. Follow the instructions in [Environment File Setup](README.md#environment-file-setup) to fill in the `.env`. Make sure to get a `DISCORD_TOKEN` and a `SQLITE_FILENAME` (which can be named anything, but use something like `database.db`) + 3. To run the bot, type `stack run` into the terminal, and the bot will start to run + 4. Make sure to invite the bot to a server so you can test it out! + +Congratulations, you now know the very basics needed to set up your own tablebot! + +To learn more about git, you should look up a tutorial or watch this video: From 7725e0bd7a40cc93f717c37af913749a66e71394 Mon Sep 17 00:00:00 2001 From: Finnbar Keating Date: Wed, 4 May 2022 10:42:04 +0100 Subject: [PATCH 78/96] Whoops, missed a link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 021396ea..416181ce 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The three Group settings are optional, but without them any commands that requir to be called when `DEBUG` is false. Users with the superuser group are able to run every command (including some dangerous ones), so caution should be used when setting these up. -If you have any difficulties setting it up, see the [contributor's guide](CONTRIBUTING.md) for a walkthrough. +If you have any difficulties setting it up, see the [setup guide](SETUP.md) for a walkthrough. ## Importing this bot and running it yourself. From 1588ac578cbec448f41399e18565a53c75e3b760 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 6 May 2022 19:52:30 +0100 Subject: [PATCH 79/96] added a bunch of comments --- .../Plugins/Netrunner/Utility/Embed.hs | 2 +- src/Tablebot/Plugins/Quote.hs | 180 ++++++++++-------- src/Tablebot/Plugins/Roll/Plugin.hs | 17 +- src/Tablebot/Utility/Random.hs | 2 +- src/Tablebot/Utility/SmartParser.hs | 97 +++++++++- src/Tablebot/Utility/Types.hs | 15 +- 6 files changed, 217 insertions(+), 96 deletions(-) diff --git a/src/Tablebot/Plugins/Netrunner/Utility/Embed.hs b/src/Tablebot/Plugins/Netrunner/Utility/Embed.hs index f63ccbb6..4dc7b4fb 100644 --- a/src/Tablebot/Plugins/Netrunner/Utility/Embed.hs +++ b/src/Tablebot/Plugins/Netrunner/Utility/Embed.hs @@ -122,7 +122,7 @@ embedLines title pre xs = let cumLength = scanl (\l x -> 1 + T.length x + l) (T.length title + 2) xs -- +1 for each newline title characters safeIndex = length $ takeWhile (< 1900) cumLength -- 1900 instead of 2000 because I gave up trying to be exact xs' = take safeIndex xs - c = if length xs' < 12 then 1 else 2 --if length xs' < 27 then 2 else 3 + c = if length xs' < 12 then 1 else 2 d = length xs' `div` c m = length xs' `mod` c heights = replicate m (d + 1) ++ replicate (c - m) d diff --git a/src/Tablebot/Plugins/Quote.hs b/src/Tablebot/Plugins/Quote.hs index 3105610b..919b028b 100644 --- a/src/Tablebot/Plugins/Quote.hs +++ b/src/Tablebot/Plugins/Quote.hs @@ -72,10 +72,10 @@ quoteReactionAdd = ReactionAdd quoteReaction where quoteReaction ri | emojiName (reactionEmoji ri) == "\x1F4AC" = do - m <- getMessage (reactionChannelId ri) (reactionMessageId ri) - case m of - Left _ -> pure () - Right mes -> addMessageQuote (reactionUserId ri) mes mes >>= sendCustomMessage mes + m <- getMessage (reactionChannelId ri) (reactionMessageId ri) + case m of + Left _ -> pure () + Right mes -> addMessageQuote (reactionUserId ri) mes mes >>= sendCustomMessage mes | otherwise = return () -- | Our quote command, which combines various functions to create, display and update quotes. @@ -379,86 +379,110 @@ quoteApplicationCommand = CreateApplicationCommandChatInput "quote" "store and r ] quoteApplicationCommandRecv :: Interaction -> DatabaseDiscord () -quoteApplicationCommandRecv i@InteractionApplicationCommand {interactionDataApplicationCommand = InteractionDataApplicationCommandChatInput {interactionDataApplicationCommandOptions = Just (InteractionDataApplicationCommandOptionsSubcommands [InteractionDataApplicationCommandOptionSubcommandOrGroupSubcommand subc])}} = case subcname of - "random" -> randomQ i >>= interactionResponseCustomMessage i - "author" -> - handleNothing - (getValue "author" vals >>= stringFromOptionValue) - ( \author -> authorQ author i >>= interactionResponseCustomMessage i - ) - "show" -> - handleNothing - (getValue "id" vals >>= integerFromOptionValue) - ( \showid -> showQ (fromIntegral showid) i >>= interactionResponseCustomMessage i - ) - "add" -> - handleNothing - ((getValue "quote" vals >>= stringFromOptionValue) >>= \q -> (getValue "author" vals >>= stringFromOptionValue) <&> (q,)) - ( \(qt, author) -> do - let requestor = toMention' $ parseUserId $ contextUserId i - (msg, qid) <- addQ' qt author requestor 0 0 i - interactionResponseCustomMessage i msg - -- to get the message to display as wanted, we have to do some trickery - -- we have already sent off the message above with the broken message id - -- and channel id, but now we have sent off this message we can refer - -- to it! We just have to get that message, overwrite the quote, and - -- hope no one cares about the edit message - v <- liftDiscord $ restCall $ R.GetOriginalInteractionResponse (interactionApplicationId i) (interactionToken i) - case v of - Left _ -> return () - Right m -> do - now <- liftIO $ systemToUTCTime <$> getSystemTime - let new = Quote qt author requestor (fromIntegral $ messageId m) (fromIntegral $ messageChannelId m) now - replace (toSqlKey qid) new - newMsg <- renderCustomQuoteMessage (messageContent m) new qid i - _ <- liftDiscord $ restCall $ R.EditOriginalInteractionResponse (interactionApplicationId i) (interactionToken i) (convertMessageFormatInteraction newMsg) - return () - ) - "edit" -> - handleNothing - (getValue "quoteid" vals >>= integerFromOptionValue) - ( \qid' -> do - let qid = fromIntegral qid' - qt = getValue "quote" vals >>= stringFromOptionValue - author = getValue "author" vals >>= stringFromOptionValue - case (qt, author) of - (Nothing, Nothing) -> interactionResponseCustomMessage i (makeEphermeral (messageDetailsBasic "No edits made to quote.")) - _ -> do - msg <- editQ' qid qt author (toMention' $ parseUserId $ contextUserId i) 0 0 i +quoteApplicationCommandRecv + i@InteractionApplicationCommand + { interactionDataApplicationCommand = + InteractionDataApplicationCommandChatInput + { interactionDataApplicationCommandOptions = + Just + ( InteractionDataApplicationCommandOptionsSubcommands + [ InteractionDataApplicationCommandOptionSubcommandOrGroupSubcommand subc + ] + ) + } + } = + case subcname of + "random" -> randomQ i >>= interactionResponseCustomMessage i + "author" -> + handleNothing + (getValue "author" vals >>= stringFromOptionValue) + ( \author -> authorQ author i >>= interactionResponseCustomMessage i + ) + "show" -> + handleNothing + (getValue "id" vals >>= integerFromOptionValue) + ( \showid -> showQ (fromIntegral showid) i >>= interactionResponseCustomMessage i + ) + "add" -> + handleNothing + ((getValue "quote" vals >>= stringFromOptionValue) >>= \q -> (getValue "author" vals >>= stringFromOptionValue) <&> (q,)) + ( \(qt, author) -> do + let requestor = toMention' $ parseUserId $ contextUserId i + (msg, qid) <- addQ' qt author requestor 0 0 i interactionResponseCustomMessage i msg + -- to get the message to display as wanted, we have to do some trickery + -- we have already sent off the message above with the broken message id + -- and channel id, but now we have sent off this message we can refer + -- to it! We just have to get that message, overwrite the quote, and + -- hope no one cares about the edit message v <- liftDiscord $ restCall $ R.GetOriginalInteractionResponse (interactionApplicationId i) (interactionToken i) case v of Left _ -> return () Right m -> do - msg' <- editQ' qid qt author (toMention' $ parseUserId $ contextUserId i) (fromIntegral $ messageId m) (fromIntegral $ messageChannelId m) i - _ <- liftDiscord $ restCall $ R.EditOriginalInteractionResponse (interactionApplicationId i) (interactionToken i) (convertMessageFormatInteraction msg') + now <- liftIO $ systemToUTCTime <$> getSystemTime + let new = Quote qt author requestor (fromIntegral $ messageId m) (fromIntegral $ messageChannelId m) now + replace (toSqlKey qid) new + newMsg <- renderCustomQuoteMessage (messageContent m) new qid i + _ <- liftDiscord $ restCall $ R.EditOriginalInteractionResponse (interactionApplicationId i) (interactionToken i) (convertMessageFormatInteraction newMsg) return () - ) - _ -> throwBot $ InteractionException "unexpected quote interaction" - where - subcname = interactionDataApplicationCommandOptionSubcommandName subc - vals = interactionDataApplicationCommandOptionSubcommandOptions subc - handleNothing Nothing _ = return () - handleNothing (Just a) f = f a -quoteApplicationCommandRecv i@InteractionApplicationCommandAutocomplete {interactionDataApplicationCommand = InteractionDataApplicationCommandChatInput {interactionDataApplicationCommandOptions = Just (InteractionDataApplicationCommandOptionsSubcommands [InteractionDataApplicationCommandOptionSubcommandOrGroupSubcommand subc])}} = case subcname of - "show" -> - handleNothing - (getValue "id" vals) - ( \case - InteractionDataApplicationCommandOptionValueInteger _ (Right showid') -> interactionResponseAutocomplete i $ InteractionResponseAutocompleteInteger [Choice (pack $ show showid') showid'] - InteractionDataApplicationCommandOptionValueInteger _ (Left showid') -> do - allQ <- allQuotes () - let allQ' = (\qe -> (show (fromSqlKey $ entityKey qe), (fromSqlKey $ entityKey qe, (\(Quote q _ _ _ _ _) -> q) (entityVal qe)))) <$> allQ - options = take 25 $ closestPairsWithCosts (def {deletion = 100, substitution = 100, transposition = 5}) allQ' (unpack showid') - interactionResponseAutocomplete i $ InteractionResponseAutocompleteInteger ((\(qids, (qid, _)) -> Choice (pack qids) (toInteger qid)) <$> options) - _ -> return () - ) - _ -> return () - where - subcname = interactionDataApplicationCommandOptionSubcommandName subc - vals = interactionDataApplicationCommandOptionSubcommandOptions subc - handleNothing Nothing _ = return () - handleNothing (Just a) f = f a + ) + "edit" -> + handleNothing + (getValue "quoteid" vals >>= integerFromOptionValue) + ( \qid' -> do + let qid = fromIntegral qid' + qt = getValue "quote" vals >>= stringFromOptionValue + author = getValue "author" vals >>= stringFromOptionValue + case (qt, author) of + (Nothing, Nothing) -> interactionResponseCustomMessage i (makeEphermeral (messageDetailsBasic "No edits made to quote.")) + _ -> do + msg <- editQ' qid qt author (toMention' $ parseUserId $ contextUserId i) 0 0 i + interactionResponseCustomMessage i msg + v <- liftDiscord $ restCall $ R.GetOriginalInteractionResponse (interactionApplicationId i) (interactionToken i) + case v of + Left _ -> return () + Right m -> do + msg' <- editQ' qid qt author (toMention' $ parseUserId $ contextUserId i) (fromIntegral $ messageId m) (fromIntegral $ messageChannelId m) i + _ <- liftDiscord $ restCall $ R.EditOriginalInteractionResponse (interactionApplicationId i) (interactionToken i) (convertMessageFormatInteraction msg') + return () + ) + _ -> throwBot $ InteractionException "unexpected quote interaction" + where + subcname = interactionDataApplicationCommandOptionSubcommandName subc + vals = interactionDataApplicationCommandOptionSubcommandOptions subc + handleNothing Nothing _ = return () + handleNothing (Just a) f = f a +quoteApplicationCommandRecv + i@InteractionApplicationCommandAutocomplete + { interactionDataApplicationCommand = + InteractionDataApplicationCommandChatInput + { interactionDataApplicationCommandOptions = + Just + ( InteractionDataApplicationCommandOptionsSubcommands + [ InteractionDataApplicationCommandOptionSubcommandOrGroupSubcommand subc + ] + ) + } + } = + case subcname of + "show" -> + handleNothing + (getValue "id" vals) + ( \case + InteractionDataApplicationCommandOptionValueInteger _ (Right showid') -> interactionResponseAutocomplete i $ InteractionResponseAutocompleteInteger [Choice (pack $ show showid') showid'] + InteractionDataApplicationCommandOptionValueInteger _ (Left showid') -> do + allQ <- allQuotes () + let allQ' = (\qe -> (show (fromSqlKey $ entityKey qe), (fromSqlKey $ entityKey qe, (\(Quote q _ _ _ _ _) -> q) (entityVal qe)))) <$> allQ + options = take 25 $ closestPairsWithCosts (def {deletion = 100, substitution = 100, transposition = 5}) allQ' (unpack showid') + interactionResponseAutocomplete i $ InteractionResponseAutocompleteInteger ((\(qids, (qid, _)) -> Choice (pack qids) (toInteger qid)) <$> options) + _ -> return () + ) + _ -> return () + where + subcname = interactionDataApplicationCommandOptionSubcommandName subc + vals = interactionDataApplicationCommandOptionSubcommandOptions subc + handleNothing Nothing _ = return () + handleNothing (Just a) f = f a quoteApplicationCommandRecv _ = return () showQuoteHelp :: HelpPage diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 067ae4ef..e17f1b70 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -12,6 +12,7 @@ module Tablebot.Plugins.Roll.Plugin (rollPlugin) where import Control.Monad.Writer (MonadIO (liftIO), void) import Data.Bifunctor (Bifunctor (first)) import Data.ByteString.Lazy (toStrict) +import Data.Default (Default (def)) import Data.Distribution (isValid) import Data.Maybe (catMaybes, fromMaybe) import Data.Text (Text, intercalate, pack, replicate, unpack) @@ -21,7 +22,7 @@ import Discord.Interactions ( Interaction (..), ) import Discord.Internal.Rest.Channel (ChannelRequest (..), MessageDetailedOpts (..)) -import Discord.Types (ComponentActionRow (..), ComponentButton (..), Emoji (..), Message (..), User (..), mkButton) +import Discord.Types (ComponentActionRow (..), ComponentButton (..), Message (..), User (..), mkButton, mkEmoji) import System.Timeout (timeout) import Tablebot.Internal.Handler.Command (parseValue) import Tablebot.Plugins.Roll.Dice @@ -70,8 +71,12 @@ rollDice' e t u@(ParseUserId uid) = do { messageDetailsComponents = Just [ ComponentActionRowButton + -- we take the first 100 characters of the button customid + -- because they're only allowed to be 100 characters long. + -- the button is disabled if it's meant to be more than 100 + -- characters so we don't have to worry about this. [ (mkButton buttonName (T.take 100 buttonCustomId)) - { componentButtonEmoji = Just (Emoji (Just 0) "🎲" Nothing Nothing Nothing (Just False)), + { componentButtonEmoji = Just (mkEmoji "🎲"), componentButtonDisabled = buttonDisabled } ] @@ -231,7 +236,13 @@ statsCommand = Command "stats" statsCommandParser [] liftDiscord $ void $ restCall - ( CreateMessageDetailed (messageChannelId m) (MessageDetailedOpts (msg range') False Nothing (Just (T.unwords (snd <$> range') <> ".png", toStrict image)) Nothing Nothing Nothing Nothing) + ( CreateMessageDetailed + (messageChannelId m) + ( def + { messageDetailedContent = msg range', + messageDetailedFile = Just (T.unwords (snd <$> range') <> ".png", toStrict image) + } + ) ) where msg [(d, t)] = diff --git a/src/Tablebot/Utility/Random.hs b/src/Tablebot/Utility/Random.hs index 68dac0a6..9eee72aa 100644 --- a/src/Tablebot/Utility/Random.hs +++ b/src/Tablebot/Utility/Random.hs @@ -36,7 +36,7 @@ chooseOneWeighted weight xs | any ((< 0) . weight) xs = throw $ RandomException "Probability weightings cannot be negative." | all ((== 0) . weight) xs = throw $ RandomException "At least one weighting must be positive." | otherwise = - fst . fromJust . (\i -> find ((> i) . snd) (zip xs' $ scanl1 (+) $ weight <$> xs')) <$> randomRIO (0, totalWeight - 1) + fst . fromJust . (\i -> find ((> i) . snd) (zip xs' $ scanl1 (+) $ weight <$> xs')) <$> randomRIO (0, totalWeight - 1) where xs' = filter ((> 0) . weight) xs -- removes elements with a weight of zero totalWeight = sum $ weight <$> xs' diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index 738ac107..885cc426 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -31,6 +31,10 @@ import Tablebot.Utility.Parser import Tablebot.Utility.Types import Text.Megaparsec (MonadParsec (eof, try), chunk, many, observing, optional, (), (<|>)) +-- | The type class representing some data we can extract data from. +-- Needed for things like getting a GuildMember, message id, guild id. +-- +-- Only defined for Message and Interaction. class Context a where contextUserId :: a -> ParseUserId contextGuildId :: a -> EnvDatabaseDiscord s (Maybe GuildId) @@ -83,6 +87,11 @@ instance Context Interaction where -- parser that reads in an @Int@, then some optional @Text@, and then uses -- those to run the provided function with the arguments parsed and the message -- itself. +-- +-- The arguments to this class are the type of the function, the type of the +-- environment, the type of the context (either Message or Interaction), and the +-- type of the result of the function (which is either () or MessageDetails +-- usually). class PComm commandty s context returns where parseComm :: (Context context) => commandty -> Parser (context -> EnvDatabaseDiscord s returns) @@ -90,36 +99,45 @@ class PComm commandty s context returns where -- If there is the general case where we have just what we want to parse, then -- return it +-- (1) instance {-# OVERLAPPING #-} PComm (t -> EnvDatabaseDiscord s r) s t r where parseComm comm = skipSpace >> return comm -- If we have the specific case where we are returning `()`, parse eof as well. -- This should cover the base case for the rest of the program that doesn't use -- more complex stuff. +-- (2) instance {-# OVERLAPPING #-} PComm (t -> EnvDatabaseDiscord s ()) s t () where parseComm comm = skipSpace >> eof >> return comm -- If an action takes a message and returns a message details and we want it to -- return unit, assume that it wants to be sent, and send it. eof this as well +-- (3) instance {-# OVERLAPPING #-} PComm (Message -> EnvDatabaseDiscord s MessageDetails) s Message () where parseComm comm = skipSpace >> eof >> return (\m -> comm m >>= sendCustomMessage m) --- Just the action. effectively the function hasn't interacted with the `t`. --- don't parse eof cause we may wanna return -instance {-# OVERLAPPING #-} PComm (EnvDatabaseDiscord s r) s t r where +-- When there is no context to the function (eg no Message or Interaction), +-- just run the action. don't parse eof cause we may wanna return. +-- similar to (1) +-- (4) +instance PComm (EnvDatabaseDiscord s r) s t r where parseComm comm = skipSpace >> return (const comm) --- Just the action. effectively the function hasn't interacted with the `t`. --- parse eof because we have unit here +-- When there is no context to the function (eg no Message or Interaction), +-- just run the action. effectively the function hasn't interacted with the `t`. +-- parse eof because we have unit here. similar to (2) +-- (5) instance {-# OVERLAPPING #-} PComm (EnvDatabaseDiscord s ()) s t () where parseComm comm = skipSpace >> eof >> return (const comm) -- if we're in a message context and have a message details but want to return --- unit, assume that we want to send it, and send it. +-- unit, assume that we want to send it, and send it. similar to (3) +-- (6) instance {-# OVERLAPPING #-} PComm (EnvDatabaseDiscord s MessageDetails) s Message () where parseComm comm = skipSpace >> eof >> return (\m -> comm >>= sendCustomMessage m) -- Recursive case is to parse the domain of the function type, then the rest. +-- (7) instance {-# OVERLAPPABLE #-} (CanParse a, PComm as s t r) => PComm (a -> as) s t r where parseComm comm = do this <- parsThenMoveToNext @a @@ -127,11 +145,13 @@ instance {-# OVERLAPPABLE #-} (CanParse a, PComm as s t r) => PComm (a -> as) s -- if we have two contexts for some reason, collapse them if the resultant can -- be parsed +-- (8) instance {-# OVERLAPPABLE #-} (PComm (t -> as) s t r) => PComm (t -> t -> as) s t r where parseComm comm = parseComm (\m -> comm m m) -- if we have a context and then some parseable value, effectively juggle the -- context so that parsing continues (and the context is passed on) +-- (9) instance {-# OVERLAPPABLE #-} (Context t, CanParse a, PComm (t -> as) s t r) => PComm (t -> a -> as) s t r where parseComm comm = do this <- parsThenMoveToNext @a @@ -139,6 +159,7 @@ instance {-# OVERLAPPABLE #-} (Context t, CanParse a, PComm (t -> as) s t r) => -- special value case - if we get ParseUserId, we need to get the value from -- the context. so, get the value from the context, and then continue parsing. +-- (10) instance {-# OVERLAPPABLE #-} (PComm (t -> as) s t r) => PComm (ParseUserId -> as) s t r where parseComm comm = parseComm $ \(m :: t) -> comm (contextUserId m) @@ -257,18 +278,25 @@ newtype RestOfInput1 a = ROI1 a instance IsString a => CanParse (RestOfInput1 a) where pars = ROI1 . fromString <$> untilEnd1 +-- | Data type to represent parsing a user id from the context. newtype ParseUserId = ParseUserId {parseUserId :: UserId} -- | Labelled value for use with smart commands. +-- +-- This is for use with slash commands, where there is a name and description +-- required. newtype Labelled (name :: Symbol) (desc :: Symbol) a = Labelled a -- | Easily make a labelled value. labelValue :: forall n d a. a -> Labelled n d a labelValue = Labelled @n @d +-- | Get the name and description of a labelled value. getLabelValues :: forall n d a. (KnownSymbol n, KnownSymbol d) => Proxy (Labelled n d a) -> (Text, Text) getLabelValues _ = (pack (symbolVal (Proxy :: Proxy n)), pack (symbolVal (Proxy :: Proxy d))) +-- | Parse a labelled value, by parsing the base value and adding the label +-- values. instance (CanParse a) => CanParse (Labelled n d a) where pars = labelValue <$> pars @@ -281,11 +309,16 @@ noArguments = parseComm -- Interactions stuff ---- +-- | Creates both the slash command creation data structure and the parser for +-- the command, and creates the EnvApplicationCommandRecv for the command by +-- combining them. makeApplicationCommandPair :: forall t s. (MakeAppComm t, ProcessAppComm t s) => Text -> Text -> t -> Maybe (EnvApplicationCommandRecv s) makeApplicationCommandPair name desc f = do cac <- makeSlashCommand name desc (Proxy :: Proxy t) return $ ApplicationCommandRecv cac (processAppComm f) +-- | Make the creation data structure for a slash command when given a proxy for +-- a function's type. makeSlashCommand :: (MakeAppComm t) => Text -> Text -> Proxy t -> Maybe CreateApplicationCommand makeSlashCommand name desc p = createApplicationCommandChatInput name desc >>= \cac -> @@ -294,6 +327,10 @@ makeSlashCommand name desc p = { createApplicationCommandOptions = Just $ ApplicationCommandOptionsValues $ makeAppComm p } +-- | Create a series of command option values from the given types. +-- +-- This is making the arguments for a text input/slash command from +-- a proxy of the given function. class MakeAppComm commandty where makeAppComm :: Proxy commandty -> [ApplicationCommandOptionValue] @@ -301,43 +338,54 @@ class MakeAppComm commandty where instance {-# OVERLAPPING #-} MakeAppComm (EnvDatabaseDiscord s MessageDetails) where makeAppComm _ = [] +-- If there is a way to get an argument from a `ty`, then get that arg and continue recursion. instance {-# OVERLAPPABLE #-} (MakeAppComm mac, MakeAppCommArg ty) => MakeAppComm (ty -> mac) where makeAppComm _ = makeAppCommArg (Proxy :: Proxy ty) : makeAppComm (Proxy :: Proxy mac) instance {-# OVERLAPPABLE #-} (MakeAppComm mac) => MakeAppComm (ParseUserId -> mac) where makeAppComm _ = makeAppComm (Proxy :: Proxy mac) +-- | From a single value, make an argument for a slash command command. class MakeAppCommArg commandty where makeAppCommArg :: Proxy commandty -> ApplicationCommandOptionValue +-- Create a labelled text argument. By default it is required and does not +-- have autocompeletion. instance (KnownSymbol name, KnownSymbol desc) => MakeAppCommArg (Labelled name desc Text) where makeAppCommArg l = ApplicationCommandOptionValueString n d True (Left False) where (n, d) = getLabelValues l +-- Create a labelled argument that is optional. instance (KnownSymbol name, KnownSymbol desc, MakeAppCommArg (Labelled name desc t)) => MakeAppCommArg (Labelled name desc (Maybe t)) where makeAppCommArg _ = (makeAppCommArg (Proxy :: Proxy (Labelled name desc t))) { applicationCommandOptionValueRequired = False } +-- When quoted text is required, just fake it and get a sub layer. instance (KnownSymbol name, KnownSymbol desc, MakeAppCommArg (Labelled name desc t)) => MakeAppCommArg (Labelled name desc (Quoted t)) where makeAppCommArg _ = makeAppCommArg (Proxy :: Proxy (Labelled name desc t)) -- As a base case, send the message produced +-- | Process an application command when given a function/value. class ProcessAppComm commandty s where processAppComm :: commandty -> Interaction -> EnvDatabaseDiscord s () --- One base case +-- When left with just a MessageDetails, just send the message as an +-- interaction response. instance {-# OVERLAPPING #-} ProcessAppComm (EnvDatabaseDiscord s MessageDetails) s where processAppComm comm i = comm >>= interactionResponseCustomMessage i --- One simple recursive case +-- If there is already an interaction in this function call, apply it and +-- recurse. instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s) => ProcessAppComm (Interaction -> pac) s where processAppComm comm i = processAppComm (comm i) i --- one overarching recursive case +-- This is the main recursion case. +-- +-- If the argument is a ProcessAppCommArg, then parse it and recurse. instance {-# OVERLAPPABLE #-} (ProcessAppCommArg ty s, ProcessAppComm pac s) => ProcessAppComm (ty -> pac) s where processAppComm comm i@InteractionApplicationCommand {interactionDataApplicationCommand = InteractionDataApplicationCommandChatInput {interactionDataApplicationCommandOptions = opts}} = do t <- processAppCommArg (getVs opts) @@ -347,7 +395,7 @@ instance {-# OVERLAPPABLE #-} (ProcessAppCommArg ty s, ProcessAppComm pac s) => getVs _ = [] processAppComm _ _ = throwBot $ InteractionException "could not process args to application command" --- one specific implementation case +-- one specific implementation case when we want to parse out a user id. instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s) => ProcessAppComm (ParseUserId -> pac) s where processAppComm comm i@InteractionApplicationCommand {interactionUser = MemberOrUser u} = case getUser of @@ -357,32 +405,58 @@ instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s) => ProcessAppComm (ParseUse getUser = userId <$> either memberUser Just u processAppComm _ _ = throwBot $ InteractionException "could not process args to application command" +-- | Process an argument for an application command. +-- +-- Given a type `t`, parse a value of that type from the given list of option +-- values. class ProcessAppCommArg t s where processAppCommArg :: [InteractionDataApplicationCommandOptionValue] -> EnvDatabaseDiscord s t +-- | Given a string, find the first option value with that name in the list, +-- returning Nothing if none is found. getValue :: String -> [InteractionDataApplicationCommandOptionValue] -> Maybe InteractionDataApplicationCommandOptionValue getValue t = find ((== pack t) . interactionDataApplicationCommandOptionValueName) +-- | Tries to extract an integer from a given option value. integerFromOptionValue :: InteractionDataApplicationCommandOptionValue -> Maybe Integer integerFromOptionValue InteractionDataApplicationCommandOptionValueInteger {interactionDataApplicationCommandOptionValueIntegerValue = Right i} = Just i integerFromOptionValue _ = Nothing +-- | Tries to extract a scientific number from a given option value. scientificFromOptionValue :: InteractionDataApplicationCommandOptionValue -> Maybe Scientific scientificFromOptionValue InteractionDataApplicationCommandOptionValueNumber {interactionDataApplicationCommandOptionValueNumberValue = Right i} = Just i scientificFromOptionValue _ = Nothing +-- | Tries to extract a string from a given option value. stringFromOptionValue :: InteractionDataApplicationCommandOptionValue -> Maybe Text stringFromOptionValue InteractionDataApplicationCommandOptionValueString {interactionDataApplicationCommandOptionValueStringValue = Right i} = Just i stringFromOptionValue _ = Nothing +-- there are a number of missing slash command argument types missing here, which I've not added yet. + +-- extract a string of the given type from the arguments instance (KnownSymbol name) => ProcessAppCommArg (Labelled name desc Text) s where processAppCommArg is = case getValue (symbolVal (Proxy :: Proxy name)) is of Just (InteractionDataApplicationCommandOptionValueString _ (Right t)) -> return $ labelValue t _ -> throwBot $ InteractionException "could not find required parameter" +-- extract an integer of the given type from the arguments +instance (KnownSymbol name) => ProcessAppCommArg (Labelled name desc Integer) s where + processAppCommArg is = case getValue (symbolVal (Proxy :: Proxy name)) is of + Just (InteractionDataApplicationCommandOptionValueInteger _ (Right i)) -> return $ labelValue i + _ -> throwBot $ InteractionException "could not find required parameter" + +-- extract a scientific number of the given type from the arguments +instance (KnownSymbol name) => ProcessAppCommArg (Labelled name desc Scientific) s where + processAppCommArg is = case getValue (symbolVal (Proxy :: Proxy name)) is of + Just (InteractionDataApplicationCommandOptionValueNumber _ (Right i)) -> return $ labelValue i + _ -> throwBot $ InteractionException "could not find required parameter" + +-- extract a quote of the given type from the arguments instance (KnownSymbol name, KnownSymbol desc, ProcessAppCommArg (Labelled name desc t) s) => ProcessAppCommArg (Labelled name desc (Quoted t)) s where processAppCommArg is = processAppCommArg @(Labelled name desc t) is >>= \(Labelled a) -> return (labelValue (Qu a)) +-- extract an optional data type from the arguments instance (KnownSymbol name, ProcessAppCommArg (Labelled name desc t) s) => ProcessAppCommArg (Labelled name desc (Maybe t)) s where processAppCommArg is = do let result = processAppCommArg is :: EnvDatabaseDiscord s (Labelled name desc t) @@ -395,6 +469,9 @@ instance (KnownSymbol name, ProcessAppCommArg (Labelled name desc t) s) => Proce -- | Given a function that can be processed to create a parser, create an action -- for it using the helper. Uses `parseComm` to generate the required parser. -- +-- Components use a unique string as their identifier. We can use this to +-- run the normal command parser on, hence the use of PComm. +-- -- For more information, check the helper `processComponentInteraction'`. processComponentInteraction :: (PComm f s Interaction MessageDetails) => f -> Bool -> Interaction -> EnvDatabaseDiscord s () processComponentInteraction f = processComponentInteraction' (parseComm f) diff --git a/src/Tablebot/Utility/Types.hs b/src/Tablebot/Utility/Types.hs index 9fda1b75..183ef1c3 100644 --- a/src/Tablebot/Utility/Types.hs +++ b/src/Tablebot/Utility/Types.hs @@ -172,6 +172,11 @@ type ReactionDel = EnvReactionDel () -- | Handles the creation of an application command and of the action to be -- performed once that application command is received. +-- +-- This handles things like chat input (slash commands), message commands, or +-- user commands. The `applicationCommand` is the data structure that +-- represents the application command, and the `applicationCommandRecv` is the +-- action to be performed when this application command is received. data EnvApplicationCommandRecv d = ApplicationCommandRecv { -- | The application command to be created. applicationCommand :: CreateApplicationCommand, @@ -181,9 +186,10 @@ data EnvApplicationCommandRecv d = ApplicationCommandRecv type ApplicationCommandRecv = EnvApplicationCommandRecv () --- | Handles recieving of interactions, such as for application commands (slash --- commands, user commands, message commands), as well as components from --- messages. +-- | Handles recieving of components, such as buttons or select menus. +-- +-- The name is the name of the component within a plugin. Choose something +-- unique within the plugin. data EnvComponentRecv d = ComponentRecv { componentName :: Text, onComponentRecv :: Interaction -> EnvDatabaseDiscord d () @@ -302,6 +308,9 @@ messageDetailsBasic t = MessageDetails Nothing (Just t) Nothing Nothing Nothing instance Default MessageDetails where def = MessageDetails Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing +-- | This data structure as a convenient way to make either interaction responses +-- or just plain messages. It is used in cases that we're either gonna return +-- an interaction or a message. data MessageDetails = MessageDetails { messageDetailsTTS :: Maybe Bool, messageDetailsContent :: Maybe Text, From b0685f574cc3315df1e668a0a3a79bbc3685050c Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 7 May 2022 12:31:25 +0100 Subject: [PATCH 80/96] made requested changes --- src/Tablebot/Plugins/Alias.hs | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/Tablebot/Plugins/Alias.hs b/src/Tablebot/Plugins/Alias.hs index a90d0cae..5ae0c4ec 100644 --- a/src/Tablebot/Plugins/Alias.hs +++ b/src/Tablebot/Plugins/Alias.hs @@ -37,6 +37,9 @@ Alias deriving Eq |] +publicAliasPerms :: RequiredPermission +publicAliasPerms = Moderator + getAliases :: UserId -> EnvDatabaseDiscord d (Maybe [Alias]) getAliases uid = (Just . fmap Sql.entityVal <$> selectList [AliasType Sql.<-. [AliasPublic, AliasPrivate uid]] []) @@ -65,7 +68,7 @@ aliasComm = Command "alias" (parseComm (\m -> aliasList (AliasPrivate (userId $ messageAuthor m)) m)) - [aliasAddCommand, aliasListCommand, aliasDeleteCommand] + [aliasAddCommand, aliasListCommand, aliasDeleteCommand, commandAlias "remove" aliasDeleteCommand] aliasHelp :: HelpPage aliasHelp = @@ -93,7 +96,7 @@ aliasAddCommand = aliasAddPrivateComm :: WithError "Need a single word" Text -> WithError "Need a quoted string" (Quoted Text) -> Message -> DatabaseDiscord () aliasAddPrivateComm (WErr t) (WErr (Qu t')) m = aliasAdd t t' (AliasPrivate (userId $ messageAuthor m)) m aliasAddPublicComm :: WithError "Need a single word" Text -> WithError "Need a quoted string" (Quoted Text) -> Message -> DatabaseDiscord () - aliasAddPublicComm (WErr t) (WErr (Qu t')) m = requirePermission Moderator m $ aliasAdd t t' AliasPublic m + aliasAddPublicComm (WErr t) (WErr (Qu t')) m = requirePermission publicAliasPerms m $ aliasAdd t t' AliasPublic m aliasAdd :: Text -> Text -> AliasType -> Message -> DatabaseDiscord () aliasAdd a b at m = do @@ -109,11 +112,11 @@ aliasAddHelp = "adds an alias" [r|**Add Alias** Adds an alias. -You can specify whether the alias is public or private. -Public aliases can only be added by moderators. -*Usage:* `alias add ""`, `alias add private ""` , `alias add public ""`|] - [] +*Usage:* `alias add ""`|] + [ HelpPage "private" [] "adds a private alias" "**Add Private Alias**\nAdds a private alias.\n\n*Usage:* `alias add private \"\"`" [] None, + HelpPage "public" [] "adds a public alias" "**Add Public Alias**\nAdds a public alias.\n\n*Usage:* `alias add public \"\"`" [] publicAliasPerms + ] None aliasListCommand :: Command @@ -164,7 +167,7 @@ aliasDeleteCommand = aliasDeletePrivateComm :: WithError "Need a single word" Text -> Message -> DatabaseDiscord () aliasDeletePrivateComm (WErr t) m = aliasDelete t (AliasPrivate (userId $ messageAuthor m)) m aliasDeletePublicComm :: WithError "Need a single word" Text -> Message -> DatabaseDiscord () - aliasDeletePublicComm (WErr t) m = requirePermission Moderator m $ aliasDelete t AliasPublic m + aliasDeletePublicComm (WErr t) m = requirePermission publicAliasPerms m $ aliasDelete t AliasPublic m aliasDelete :: Text -> AliasType -> Message -> DatabaseDiscord () aliasDelete a at m = do @@ -178,13 +181,20 @@ aliasDeleteHelp :: HelpPage aliasDeleteHelp = HelpPage "delete" - [] + ["remove"] "deletes an alias" [r|**Delete Alias** -Deletes an alias. +Deletes a private alias. + +*Usage:* `alias delete `|] + [ HelpPage "private" [] "deletes a private alias" "**Delete Private Alias**\nDeletes a private alias.\n\n*Usage:* `alias delete private `" [] None, + HelpPage "public" [] "deletes a public alias" "**Delete Public Alias**\nDeletes a public alias.\n\n*Usage:* `alias delete public `" [] publicAliasPerms + ] + None + +{- You can specify whether the alias to delete is public or private. Public aliases can only be deleted by moderators. -*Usage:* `alias delete `, `alias delete private `, `alias delete public `|] - [] - None +, `alias delete private `, `alias delete public ` +-} From 8170d71e829b1e8e839719e9bbbf9bb2ec6143f1 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 7 May 2022 12:32:42 +0100 Subject: [PATCH 81/96] removed commented content --- src/Tablebot/Plugins/Alias.hs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Tablebot/Plugins/Alias.hs b/src/Tablebot/Plugins/Alias.hs index 5ae0c4ec..c23c1313 100644 --- a/src/Tablebot/Plugins/Alias.hs +++ b/src/Tablebot/Plugins/Alias.hs @@ -191,10 +191,3 @@ Deletes a private alias. HelpPage "public" [] "deletes a public alias" "**Delete Public Alias**\nDeletes a public alias.\n\n*Usage:* `alias delete public `" [] publicAliasPerms ] None - -{- -You can specify whether the alias to delete is public or private. -Public aliases can only be deleted by moderators. - -, `alias delete private `, `alias delete public ` --} From 47af85e1924fccb96e5567ec1343a525b97c0884 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sun, 8 May 2022 11:08:01 +0100 Subject: [PATCH 82/96] made requested changes --- .gitignore | 1 + docs/Roll.md | 8 ++++---- src/Tablebot/Plugins/Roll/Dice.hs | 8 ++++++-- src/Tablebot/Plugins/Roll/Dice/DiceData.hs | 6 +++--- src/Tablebot/Plugins/Roll/Dice/DiceEval.hs | 13 ++++++++++--- src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs | 10 ++++++++++ src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs | 12 ++++++------ src/Tablebot/Plugins/Roll/Dice/DiceStats.hs | 16 +--------------- src/Tablebot/Plugins/Roll/Plugin.hs | 6 ++++-- 9 files changed, 45 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index 6f78a42e..57468f69 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ database* *.cabal stack.yaml.lock .gitattributes +.vscode diff --git a/docs/Roll.md b/docs/Roll.md index 75c3f198..d8d11774 100644 --- a/docs/Roll.md +++ b/docs/Roll.md @@ -72,13 +72,13 @@ There are two operators that are more complex and have specific organisational r If statements take an expression, and then two either integer values or list values. If the expression is non-zero, the first value is returned. If the expression is zero, the second value is returned. The syntax for it is `if expression then t else f`, where `expression` is an integer value, and `t` and `f` are both either integer values or list values. Only one of `t` or `f` is ever evaluated. -Let statements take a name and either an integer value or a list, and set a variable with that name to that value. If the let statement is lazy (with an exclamation mark before the variable name) instead, the value is recalculated every time the variable is used. A let statement returns the value on the left side. To create and use list variables, they must be prepended with `l_`. The syntax can be something like `let name = value`, `let !name = value`, or `let l_name = value`, or so on. These bound values can then be used in other calculations. +Var statements take a name and either an integer value or a list, and set a variable with that name to that value. If the var statement is lazy (with an exclamation mark before the variable name) the value is recalculated every time the variable is used. A var statement returns the value on the left side. To create and use list variables, they must be prepended with `l_`. The syntax can be something like `var name = value`, `var !name = value`, or `var l_name = value`, or so on. These bound values can then be used in other calculations. Variable names consist only have lower case letters and underscores. -To fully utilise these expression types, statements have been made, which, when constructed together with a value, creates a program. A statement is an integer value or list value followed by a semicolon. Below are a couple example programs (which are multiple statements followed by a single value). One quality of life feature is that a lazy let expression won't be evaluated until the variable is first used. +To fully utilise these expression types, statements have been made, which, when constructed together with a value, creates a program. A statement is an integer value or list value followed by a semicolon. Below are a couple example programs (which are multiple statements followed by a single value). One quality of life feature is that a lazy var expression won't be evaluated until the variable is first used. -- `let l_list = (2d6)#3d6; {length(l_list), minimum(l_list), maximum(l_list), sum(l_list)/length(l_list)}` +- `var l_list = (2d6)#3d6; {length(l_list), minimum(l_list), maximum(l_list), sum(l_list)/length(l_list)}` - Get the length, minimum, maximum, and average value of a random list. -- `let !k = 1d20; let t = k; let !t_iseven = if mod(t, 2) then 0 else 1; if t_iseven then k * t + 20 else t` +- `var !k = 1d20; var t = k; var !t_iseven = if mod(t, 2) then 0 else 1; if t_iseven then k * t + 20 else t` - Create a lazy variable `k`. Evaluate it into a variable `t`. Check whether `t` is even, and place in a variable. Depending on whether `t` is even or not, either output another random number times by `t` (and add 20 to distinguish it), or just output `t`. ## Functions diff --git a/src/Tablebot/Plugins/Roll/Dice.hs b/src/Tablebot/Plugins/Roll/Dice.hs index 0e483884..5423c526 100644 --- a/src/Tablebot/Plugins/Roll/Dice.hs +++ b/src/Tablebot/Plugins/Roll/Dice.hs @@ -18,7 +18,7 @@ -- - DiceEval - methods for evaluating elements from DiceData -- - DiceStats - filling the type classes and function needed to generate -- statistics on dice --- - DiceStatsBase - functions to process completed dice ranges +-- - DiceStatsBase - functions to process dice value distributions -- -- Below is the regex representing the parsing for the expressions, and -- explanations for each component. It's not 100% accurate to the actual data @@ -29,6 +29,10 @@ -- valid, barring in lstv, dice, die, dopr, ords, funcBasics, misc; spaces are -- added manually in those. -- +-- TODO: it's usually safer to put these kinds of grammars in Backus-Naur form +-- rather than regex due to the sheer number of regex standards and possible +-- overloading of ?. +-- -- prog - stat* (lstv | expr) -- stat - (lstv | expr) ";" -- misc - ifst | vars @@ -56,7 +60,7 @@ -- -- prog (Program) - representing a complete program - a series of statements and a value to output at the end. -- stat (Statement) - representing a single statement - an expression or list value --- misc (MiscData) - either an if or a let +-- misc (MiscData) - either an if or a var -- ifst (If) - representing one of two values depending on the outcome of an expression -- vars (Var) - setting a variable to a certain value -- lstv (ListValues) - representing all possible list values (basic list values, functions that return lists, and values which are lists of length N that consist of `Base`s, as well as a MiscData value) diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs index f844de0c..bb9d2e94 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceData.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceData.hs @@ -17,11 +17,11 @@ import Data.Text (Text) import Data.Tuple (swap) import Tablebot.Plugins.Roll.Dice.DiceFunctions (FuncInfo, FuncInfoBase) --- | Set the variable `letName` to the value `letValue`. This also returns the --- evaluated `letValue`. +-- | Set the variable `varName` to the value `varValue`. This also returns the +-- evaluated `varValue`. -- -- List variables have to be prefixed with `l_`. This really helps with parsing. -data Var a = Var {letName :: Text, letValue :: a} | VarLazy {letName :: Text, letValue :: a} deriving (Show) +data Var a = Var {varName :: Text, varValue :: a} | VarLazy {varName :: Text, varValue :: a} deriving (Show) -- | If the first value is truthy (non-zero or a non-empty list) then return -- the `thenValue`, else return the `elseValue`. diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs index e8132f25..bc92d27c 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceEval.hs @@ -76,13 +76,20 @@ evalProgram :: Program -> IO (Either [(Integer, Text)] Integer, Text) evalProgram (Program ss elve) = evalStateT ( do - t <- foldl' (\b s -> b >>= \t -> evalStatement s >>= \st -> return (t <> st)) (return "") ss + -- evaluate all the statements + stmts <- foldl' folder (return "") ss + -- evaluate the expression r <- either ((Left <$>) . evalShowL) ((Right <$>) . evalShow) elve case r of - Left (is, mt) -> return (Left is, t <> fromMaybe (prettyShow elve) mt) - Right (is, mt) -> return (Right is, t <> mt) + Left (is, mtxt) -> return (Left is, stmts <> fromMaybe (prettyShow elve) mtxt) + Right (i, txt) -> return (Right i, stmts <> txt) ) startState + where + folder b s = do + stmts <- b + st <- evalStatement s + return (stmts <> st) -- | Given a list expression, evaluate it, getting the pretty printed string and -- the value of the result. diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs index a16e5ce7..05636940 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceFunctions.hs @@ -89,6 +89,8 @@ listFunctions' = between i i' = let (mi, ma, rev) = (min i i', max i i', if i > i' then reverse else id) in rev [mi .. ma] -- | The `FuncInfo` of the function that indexes into a list. +-- +-- Creates a function that takes an integer and a list and returns an integer. funcInfoIndex :: FuncInfo funcInfoIndex = FuncInfo "index" [ATInteger, ATIntegerList] ATInteger fiIndex where @@ -97,6 +99,10 @@ funcInfoIndex = FuncInfo "index" [ATInteger, ATIntegerList] ATInteger fiIndex | otherwise = return (is !! fromInteger i) fiIndex is = throwBot $ EvaluationException ("incorrect number/type of arguments. expected 2, got " ++ show (length is)) [] +-- | The `FuncInfo` of the function that sets an element in a list. +-- +-- Creates a function that takes an index, an integer and a list and returns a +-- list. funcInfoSet :: FuncInfoBase [Integer] funcInfoSet = FuncInfo "set" [ATInteger, ATInteger, ATIntegerList] ATIntegerList fiSet where @@ -105,6 +111,10 @@ funcInfoSet = FuncInfo "set" [ATInteger, ATInteger, ATIntegerList] ATIntegerList | otherwise = return $ genericTake i js ++ j : genericDrop (i + 1) js fiSet is = throwBot $ EvaluationException ("incorrect number/type of arguments. expected 3, got " ++ show (length is)) [] +-- | The `FuncInfo` of the function that inserts an integer into a list. +-- +-- Creates a function that takes an index, an integer and a list and returns a +-- list. funcInfoInsert :: FuncInfoBase [Integer] funcInfoInsert = FuncInfo "insert" [ATInteger, ATInteger, ATIntegerList] ATIntegerList fiSet where diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs index d7d81184..500f930d 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceParsing.hs @@ -37,14 +37,14 @@ import Text.Megaparsec.Error (ErrorItem (Tokens)) failure' :: T.Text -> Set T.Text -> Parser a failure' s ss = failure (Just $ Tokens $ NE.fromList $ T.unpack s) (S.map (Tokens . NE.fromList . T.unpack) ss) -varName :: Parser T.Text -varName = T.pack <$> some (choice $ char <$> '_' : ['a' .. 'z']) +variableName :: Parser T.Text +variableName = T.pack <$> some (choice $ char <$> '_' : ['a' .. 'z']) instance CanParse a => CanParse (Var a) where pars = do _ <- try (string "var") <* skipSpace letCon <- try (char '!' $> VarLazy) <|> return Var - varName' <- varName + varName' <- variableName _ <- skipSpace >> char '=' >> skipSpace letCon varName' <$> pars @@ -86,13 +86,13 @@ instance CanParse ListValues where pars = do functionParser listFunctions LVFunc - <|> (LVVar . ("l_" <>) <$> try (string "l_" *> varName)) + <|> (LVVar . ("l_" <>) <$> try (string "l_" *> variableName)) <|> ListValuesMisc <$> (pars >>= checkVar) <|> (try (pars <* char '#') >>= \nb -> MultipleValues nb <$> pars) <|> LVBase <$> pars where checkVar (MiscVar l) - | T.isPrefixOf "l_" (letName l) = return (MiscVar l) + | T.isPrefixOf "l_" (varName l) = return (MiscVar l) | otherwise = fail "list variables must be prepended with l_" checkVar l = return l @@ -183,7 +183,7 @@ instance CanParse Base where <|> return (NBase nb) ) <|> DiceBase <$> parseDice (Value 1) - <|> (NumVar <$> try varName) + <|> (NumVar <$> try variableName) instance CanParse Die where pars = do diff --git a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs index 6090be29..08eaffef 100644 --- a/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs +++ b/src/Tablebot/Plugins/Roll/Dice/DiceStats.hs @@ -97,7 +97,7 @@ rangeIfExpr func (If b t f) = do b' <- range b let mp = toMap $ run b' canBeFalse = M.member 0 mp - canBeTrue = M.null $ M.filterWithKey (\k _ -> k /= 0) mp + canBeTrue = not $ M.null $ M.filterWithKey (\k _ -> k /= 0) mp emptyExp = from $ D.fromList @_ @Integer [] t' <- if canBeTrue then func t else return emptyExp f' <- if canBeFalse then func f else return emptyExp @@ -106,20 +106,6 @@ rangeIfExpr func (If b t f) = do b'' <- b' if b'' /= 0 then t' else f' --- rangeIfList :: (MonadException m, Ord b) => (a -> m (D.Experiment b)) -> If ListValues a -> m (D.Experiment b) --- rangeIfList func (If b t f) = do --- b' <- rangeList b --- let mp = toMap $ run b' --- canBeFalse = M.member [] mp --- canBeTrue = M.null $ M.filterWithKey (\k _ -> k /= []) mp --- emptyExp = from $ D.fromList @_ @Integer [] --- t' <- if canBeTrue then func t else return emptyExp --- f' <- if canBeFalse then func f else return emptyExp --- return $ --- do --- b'' <- b' --- if b'' /= [] then t' else f' - instance (Range a) => Range (Var a) where range' (Var _ a) = range a range' (VarLazy _ a) = range a diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index aa473b92..256b75a0 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -39,6 +39,8 @@ rollDice' e' t m = do maybemsss <- liftIO $ timeout 1000000 $ evalProgram e case maybemsss of Nothing -> throwBot (EvaluationException "Could not process expression in one second" []) + -- vs is either a list of integers and their textual representation, or + -- a single integer. ss is Just (vs, ss) -> do let msg = makeMsg vs ss if countFormatting msg < 199 @@ -118,7 +120,7 @@ This supports addition, subtraction, multiplication, integer division, exponenti ++ show maximumRNG ++ [r| RNG calls), dice with custom sides, rerolling dice once on a condition, rerolling dice indefinitely on a condition, keeping or dropping the highest or lowest dice, keeping or dropping dice based on a condition, operating on lists (which have a maximum size of |] ++ show maximumListLength - ++ [r|), if statements, let statements, and using functions like |] + ++ [r|), if statements, var statements, and using functions like |] ++ unpack (intercalate ", " integerFunctionsList) ++ [r| (which return integers), or functions like |] ++ unpack (intercalate ", " listFunctionsList) @@ -225,7 +227,7 @@ statsHelp = "stats" [] "calculate and display statistics for expressions." - "**Roll Stats**\nCan be used to display statistics for expressions of dice.\nDoes not work with statements.\n\n*Usage:* `roll stats 2d20kh1`, `roll stats 4d6rr=1dl1+5`, `roll stats 3d6dl1+6 4d6dl1`" + "**Roll Stats**\nCan be used to display statistics for expressions of dice.\nDoes not work with \"programs\" ie multiple statements one after the other, or with accessing variables.\n\n*Usage:* `roll stats 2d20kh1`, `roll stats 4d6rr=1dl1+5`, `roll stats 3d6dl1+6 4d6dl1`" [] None From 1a2d3ec97fbbdbc5ad53d6f79d669d1d5e76eec4 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Tue, 10 May 2022 20:17:53 +0100 Subject: [PATCH 83/96] interaction tutorial --- src/Tablebot/Plugins/Quote.hs | 8 +- src/Tablebot/Utility/Random.hs | 2 +- src/Tablebot/Utility/SmartParser.hs | 9 +- tutorials/3.Interactions.md | 348 ++++++++++++++++++++++++++++ 4 files changed, 360 insertions(+), 7 deletions(-) create mode 100644 tutorials/3.Interactions.md diff --git a/src/Tablebot/Plugins/Quote.hs b/src/Tablebot/Plugins/Quote.hs index 919b028b..04625bc8 100644 --- a/src/Tablebot/Plugins/Quote.hs +++ b/src/Tablebot/Plugins/Quote.hs @@ -72,10 +72,10 @@ quoteReactionAdd = ReactionAdd quoteReaction where quoteReaction ri | emojiName (reactionEmoji ri) == "\x1F4AC" = do - m <- getMessage (reactionChannelId ri) (reactionMessageId ri) - case m of - Left _ -> pure () - Right mes -> addMessageQuote (reactionUserId ri) mes mes >>= sendCustomMessage mes + m <- getMessage (reactionChannelId ri) (reactionMessageId ri) + case m of + Left _ -> pure () + Right mes -> addMessageQuote (reactionUserId ri) mes mes >>= sendCustomMessage mes | otherwise = return () -- | Our quote command, which combines various functions to create, display and update quotes. diff --git a/src/Tablebot/Utility/Random.hs b/src/Tablebot/Utility/Random.hs index 9eee72aa..68dac0a6 100644 --- a/src/Tablebot/Utility/Random.hs +++ b/src/Tablebot/Utility/Random.hs @@ -36,7 +36,7 @@ chooseOneWeighted weight xs | any ((< 0) . weight) xs = throw $ RandomException "Probability weightings cannot be negative." | all ((== 0) . weight) xs = throw $ RandomException "At least one weighting must be positive." | otherwise = - fst . fromJust . (\i -> find ((> i) . snd) (zip xs' $ scanl1 (+) $ weight <$> xs')) <$> randomRIO (0, totalWeight - 1) + fst . fromJust . (\i -> find ((> i) . snd) (zip xs' $ scanl1 (+) $ weight <$> xs')) <$> randomRIO (0, totalWeight - 1) where xs' = filter ((> 0) . weight) xs -- removes elements with a weight of zero totalWeight = sum $ weight <$> xs' diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index 885cc426..96e1ad05 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -267,7 +267,7 @@ instance CanParse Snowflake where pars = Snowflake . fromInteger <$> posInteger -- | @RestOfInput a@ parses the rest of the input, giving a value of type @a@. -newtype RestOfInput a = ROI a +newtype RestOfInput a = ROI {unROI :: a} instance IsString a => CanParse (RestOfInput a) where pars = ROI . fromString <$> untilEnd @@ -285,7 +285,7 @@ newtype ParseUserId = ParseUserId {parseUserId :: UserId} -- -- This is for use with slash commands, where there is a name and description -- required. -newtype Labelled (name :: Symbol) (desc :: Symbol) a = Labelled a +newtype Labelled (name :: Symbol) (desc :: Symbol) a = Labelled {unLabel :: a} -- | Easily make a labelled value. labelValue :: forall n d a. a -> Labelled n d a @@ -312,6 +312,8 @@ noArguments = parseComm -- | Creates both the slash command creation data structure and the parser for -- the command, and creates the EnvApplicationCommandRecv for the command by -- combining them. +-- +-- Takes the name and description for a slash command, and its function. makeApplicationCommandPair :: forall t s. (MakeAppComm t, ProcessAppComm t s) => Text -> Text -> t -> Maybe (EnvApplicationCommandRecv s) makeApplicationCommandPair name desc f = do cac <- makeSlashCommand name desc (Proxy :: Proxy t) @@ -472,6 +474,9 @@ instance (KnownSymbol name, ProcessAppCommArg (Labelled name desc t) s) => Proce -- Components use a unique string as their identifier. We can use this to -- run the normal command parser on, hence the use of PComm. -- +-- If the boolean is False, a reply is sent to the interaction message. If the +-- boolean is True, the original message is updated. +-- -- For more information, check the helper `processComponentInteraction'`. processComponentInteraction :: (PComm f s Interaction MessageDetails) => f -> Bool -> Interaction -> EnvDatabaseDiscord s () processComponentInteraction f = processComponentInteraction' (parseComm f) diff --git a/tutorials/3.Interactions.md b/tutorials/3.Interactions.md new file mode 100644 index 00000000..10eb40c4 --- /dev/null +++ b/tutorials/3.Interactions.md @@ -0,0 +1,348 @@ +# Making and managing Interactions + +A recent change in Discord added some pretty neat ways of interacting. Unfortunately, it requires a fair amount of fiddling, so let's go through how to use this! + +## Setup + +We'll start where the [Plugins](./1.Plugins.md) tutorial left off, with the below file. I've removed `myping'`, and commented out most of the file, so we can start from basics and build back up again, as well as added a line to `pingPlugin'` which will help us later. + +```haskell +module Tablebot.Plugins.MyPing (pingPlugin') where + +import Data.Text +import Tablebot.Utility +import Tablebot.Utility.Discord +import Tablebot.Utility.SmartParser +import Database.Persist.TH +import Discord.Types +import Database.Esqueleto +import GHC.Word +import Data.Maybe (catMaybes) +import Discord.Interactions +import Data.Default + +-- share +-- [mkPersist sqlSettings, mkMigrate "pingMigration"] +-- [persistLowerCase| +-- PingCount +-- Primary uid +-- uid Word64 +-- counter Int +-- deriving Show +-- |] + +myPing :: Command +myPing = Command "myping" (noArguments $ \m -> do + sendMessage m "pong") [] + +-- myEcho :: Command +-- myEcho = Command "myecho" (parseComm echoHelp) [] +-- where echoHelp :: RestOfInput Text -> Message -> DatabaseDiscord () +-- echoHelp (ROI t) m = sendMessage m t + +-- myPing'' :: Command +-- myPing'' = Command "cmyping" (parseComm pingDB) [] +-- where pingDB :: Message -> DatabaseDiscord () +-- pingDB m = do +-- let uid = extractFromSnowflake $ userId $ messageAuthor m +-- user <- liftSql $ select $ from $ \p -> do +-- where_ (p ^. PingCountUid ==. val uid) +-- return p +-- c <- case user of +-- [] -> do +-- _ <- liftSql $ insert (PingCount uid 1) +-- return 1 +-- (x : _) -> do +-- let (PingCount uid' count) = entityVal x +-- record' = PingCount uid' (count+1) +-- liftSql $ repsert (entityKey x) record' +-- return (count+1) +-- sendMessage m (pack $ show c) + +pingPlugin' :: Plugin +pingPlugin' = (plug "myping") {commands = [ + myPing + -- , myPing'' + -- , myEcho + ] + -- , migrations = [pingMigration] + , onComponentRecvs = [] + , applicationCommands = catMaybes [] + } +``` + +## Slash command basics + +As before, let's start with the simplest type of command - replying to a given command. + +We're going to change up how `myPing` is formed, so we can see how to make both a slash command and a text command. + +First, we separate out the function that responds with `pong`, and make it so that instead of instantly sending the message, it generates a `MessageDetails` data structure that represents such a message. + +```haskell +myPing :: Command +myPing = Command "myping" (parseComm myPingAction) [] + +myPingAction :: DatabaseDiscord MessageDetails +myPingAction = return $ messageDetailsBasic "pong" +``` + +Due to some of the setup that's been done separately, this will work as before, replying with "pong" when we call "myping". + +We can now create our first application command! + +To do this, we have to tell Discord what application command to create, and then we have to come up with a way to answer that. Luckily, we've made some magic to make some of this somewhat easier, and package it all up for us. + +```haskell +myPingInteraction :: Maybe ApplicationCommandRecv +myPingInteraction = makeApplicationCommandPair "myping" "my unique ping" myPingAction +``` + +Here we've (possibly) created an application command called "myping", with a description of "my unique ping" that is created from `myPingAction`. + +If we add `myPingInteraction` to the `applicationCommands` list in `pingPlugin'`, and run our bot, we can see that we can now (after a short delay) type `/myping` into discord, press enter, and get a special response in return. + +Well, that's pretty exciting. But we could do that before. What do we need to do to get the rest of our ping functions up to scratch? + +Turns out there's a fair amount of information that we took for granted before that we're now going to have to work through, but don't worry, we're gonna get through it step by step. + +### Labelled arguments + +Slash commands unfortunately need to have each of their parameters named, which means that anything we give to our function has to be named. + +Let's uncomment `myEcho` and roll up our sleeves to see what we want to do here. + +First we pull out `echoHelp` into its own top level function, modify the return and arguments so it returns a message that is just the text we pass in - and we run into an issue. Having a `RestOfInput` doesn't make any sense in a slash command, so we can't use `echoHelp` as is. We're going to have to construct two subtly different functions that we can process differently to get what we want. + +```haskell +-- current `echoHelp` +echoHelp :: RestOfInput Text -> DatabaseDiscord MessageDetails +echoHelp (ROI t) = return $ messageDetailsBasic t +``` + +We take away the restriction of `RestOfInput` for now, breaking `myecho`, but we'll fix it shortly. + +We then change `myEcho` so that instead of having `(parseComm echoHelp)`, we instead have `(parseComm (echoHelp . unROI))`. This function is now the same as it originally was, and we can reuse our new `echoHelp` for our interactions. + +```haskell +myEcho :: Command +myEcho = Command "myecho" (parseComm (echoHelp . unROI)) [] + +echoHelp :: Text -> DatabaseDiscord MessageDetails +echoHelp t = return $ messageDetailsBasic t +``` + +Ok, great, we're back to where we started. Now we have to make this labelled interactiony thingummy. + +Let's see if we can just do the same thing we just did, using the tools we had before. + +```haskell +myEchoInteraction :: Maybe ApplicationCommandRecv +myEchoInteraction = makeApplicationCommandPair "myecho" "echo your input" (echoHelp . unLabel) +``` + +Alright, and compi- + +``` +No instance for (GHC.TypeLits.KnownSymbol name0) + arising from a use of ‘makeApplicationCommandPair’ + • In the expression: + makeApplicationCommandPair + "myecho" "echo your input" (echoHelp . unLabel) + In an equation for ‘myEchoInteraction’: + myEchoInteraction + = makeApplicationCommandPair + "myecho" "echo your input" (echoHelp . unLabel) +``` + +Oh dear that's hideous. Oh yeah, we forget to actually label anything! + +We can achieve this in one of two ways. Firstly, we could create a function which has the labels we want and then make the slash command like that; secondly, we could use type applications to add the labels we want here. I'm going to go with the latter in this case, but I'll show both here. + +```haskell +myEchoInteraction1 :: Maybe ApplicationCommandRecv +myEchoInteraction1 = makeApplicationCommandPair "myecho" "echo your input" echoHelp' + where + echoHelp' :: Labelled "message" "the message to echo back" Text -> DatabaseDiscord MessageDetails + echoHelp' (Labelled t) = echoHelp t + +myEchoInteraction2 :: Maybe ApplicationCommandRecv +myEchoInteraction2 = makeApplicationCommandPair "myecho" "echo your input" (echoHelp . unLabel @"message" @"the message to echo back") +``` + +Adding this new construction to our `applicationCommands` list and running the bot results in a new application command, one which can a single text input which the bot then throws right back at us. + +### Users + +One of the most useful bits of information that we would want to get is the user id of the user that called a command. Removing the reliance on `Message` means that we, unfortunately, don't have that information any more. We have a solution to this though! And we'll work through using that solution with `myping''`, which was the ping example that counted the number of times it had been pinged by the user before. + +As before, we'll start by moving the helper function (`pingDB`) to its own top level function, changing any message sending to returning a `MessageDetails`, and then removing `Message` from the signature. + +Doing this though, we immediately come up on the snag. Even if we aren't sending a message using `m` any more, we are still getting the user id of the user that sent the message. To solve this, we have to introduce a new type, `ParseUserId`. This is a special type that tells our automated stuff "hey I would like a user id here please". This changes the first couple lines of `pingDB` to the following. + +```haskell +pingDB :: ParseUserId -> DatabaseDiscord MessageDetails +pingDB (ParseUserId u) = do + let uid = extractFromSnowflake u +``` + +We then construct the interaction as we have done before, add it to the interactions, and boom, we have another slash command to work with! + +### Closing off and caveats + +There are some more complex constructions with slash commands (such as subcommands), but as they are currently a bit fiddly we won't cover them in this tutorial. + +Discord also offers user and message application commands, which we haven't created nice interfaces for just yet, but are usable if you do create them. + +## Components + +Another cool thing added was a variety of widgets and gizmos that bots can add to messages called "components". These also use the interaction system to process, but we've abstracted that again. + +First, let's decide what we want to do with this. How about we add to the basic `myping`, and make it so that there's a button that says "Ping!", which people can click and it'll reply "pong"? + +Yes it's contrived, I'm sorry. + +Like before, this is a two stage process. In one place, we have to create the component itself, and in another we have to handle the interaction the component generates. + +Let's create the component itself. We need to add it to the button to be pressed. This isn't too streamlined, but it makes some level of sense. + +A button needs the text it will display, and a unique identifier that we'll use to differentiate and process a button. In this case, those are "Ping!" and "myping pingbutton" (why that exactly I'll explain later). + +```haskell +myPingAction :: DatabaseDiscord MessageDetails +myPingAction = return $ (messageDetailsBasic "pong") { messageDetailsComponents = Just [cps] } + where cps = ComponentActionRowButton [mkButton "Ping!" "myping pingbutton"] +``` + +Running the bot, we see that we get the button now! But it just loads for a couple seconds then errors. We need our bot to actually handle this button press. + +We can construct and add this component handler by using `processComponentInteraction` and `ComponentRecv`. The first creates a parser like `parseComm` did in the [Plugins](./1.Plugins.md) tutorial and the latter creates the data structure this interaction processor works in. + +The action we use in response to the button will be `myPingAction` as it was before, so we feed that and `False` to `processComponentInteraction`. The `False` means that we send a message, instead of updating a message (see [More complex components](#more-complex-components)). + +The reason I chose "myping pingbutton" as the identifier before is because of how we process and distribute components. The first word of the unique identifier has to be the plugin name, and the second has to be identifier of the particular component being processed. + +```haskell +myPingButton :: ComponentRecv +myPingButton = ComponentRecv "pingbutton" (processComponentInteraction myPingAction False) +``` + +Now we just load `myPingButton` into `onComponentRecvs` in our plugin creation, run the bot again, run the command, and press our sparkly button, and we get a message from the bot saying "pong"! + +But can we do better? + +### More complex components + +In the [Plugins](./1.Plugins.md) tutorial we created a ping command that stored the ping count of a user in the database. Wouldn't it be useful if we could do the same, but in a button? Well even if it isn't useful, we can! + +For this we're going to have to make a more complex button action, but it should be fine, right? + +First we make it so that the button identifier includes a number at the end, like `"myping pingbutton 0"`. Now we have to update the action on receiving a button press. + +We create a function `myPingButtonAction` that takes a number and the interaction, and with those updates the original message component with that number, and sends a message that says "pong" and the number it is up to. + +```haskell +myPingButtonAction :: Integer -> Interaction -> DatabaseDiscord MessageDetails +myPingButtonAction i inter = do + sendReplyMessage (interactionMessage inter) ("pong " <> pack (show i)) -- respond to the message with a pong + return $ def { messageDetailsComponents = Just [cps] } -- the message to update the original with + where cps = ComponentActionRowButton [mkButton "Ping!" ("myping pingbutton " <> pack (show (i + 1)))] +``` + +And then we edit `myPingButton` so that it uses the above function and also uses the output from it to update the original message: `ComponentRecv "pingbutton" (processComponentInteraction myPingButtonAction True)`. + +Now when we press the button, we get a pong with a number after it for each time the button has been pressed! Pretty neat, huh? + +## Conclusions + +A lot was missed out of this tutorial, and there's a lot more that we haven't (yet!) made easier to do within `tablebot`, but we hope that this lets you get started on your bot development in Haskell! + +In case you just want the complete working code from this tutorial, here it is. + +```haskell +module Tablebot.Plugins.MyPing (pingPlugin') where + +import Data.Text +import Tablebot.Utility +import Tablebot.Utility.Discord +import Tablebot.Utility.SmartParser +import Database.Persist.TH +import Discord.Types +import Database.Esqueleto +import GHC.Word +import Data.Maybe (catMaybes) +import Discord.Interactions +import Data.Default + +share + [mkPersist sqlSettings, mkMigrate "pingMigration"] + [persistLowerCase| +PingCount + Primary uid + uid Word64 + counter Int + deriving Show +|] + +myPing :: Command +myPing = Command "myping" (parseComm myPingAction) [] + +myPingAction :: DatabaseDiscord MessageDetails +myPingAction = return $ (messageDetailsBasic "pong") { messageDetailsComponents = Just [cps] } + where cps = ComponentActionRowButton [mkButton "Ping!" "myping pingbutton 0"] + +myPingInteraction :: Maybe ApplicationCommandRecv +myPingInteraction = makeApplicationCommandPair "myping" "my unique ping" myPingAction + +myPingButton :: ComponentRecv +myPingButton = ComponentRecv "pingbutton" (processComponentInteraction myPingButtonAction True) + +myPingButtonAction :: Integer -> Interaction -> DatabaseDiscord MessageDetails +myPingButtonAction i inter = do + sendReplyMessage (interactionMessage inter) ("pong " <> pack (show i)) + return $ def { messageDetailsComponents = Just [cps] } + where cps = ComponentActionRowButton [mkButton "Ping!" ("myping pingbutton " <> pack (show (i + 1)))] + +myEcho :: Command +myEcho = Command "myecho" (parseComm (echoHelp . unROI)) [] + +echoHelp :: Text -> DatabaseDiscord MessageDetails +echoHelp t = return $ messageDetailsBasic t + +myEchoInteraction :: Maybe ApplicationCommandRecv +myEchoInteraction = makeApplicationCommandPair "myecho" "echo your input" (echoHelp . unLabel @"message" @"the message to echo back") + +myPing'' :: Command +myPing'' = Command "cmyping" (parseComm pingDB) [] + +pingDB :: ParseUserId -> DatabaseDiscord MessageDetails +pingDB (ParseUserId u) = do + let uid = extractFromSnowflake u + user <- liftSql $ select $ from $ \p -> do + where_ (p ^. PingCountUid ==. val uid) + return p + c <- case user of + [] -> do + _ <- liftSql $ insert (PingCount uid 1) + return 1 + (x : _) -> do + let (PingCount uid' count) = entityVal x + record' = PingCount uid' (count+1) + liftSql $ repsert (entityKey x) record' + return (count+1) + return $ messageDetailsBasic (pack $ show c) + +myPingInteraction'' :: Maybe ApplicationCommandRecv +myPingInteraction'' = makeApplicationCommandPair "cmyping" "counting pings" pingDB + +pingPlugin' :: Plugin +pingPlugin' = (plug "myping") {commands = [ + myPing + , myPing'' + , myEcho + ] + , migrations = [pingMigration] + , onComponentRecvs = [myPingButton] + , applicationCommands = catMaybes [ myPingInteraction, myEchoInteraction, myPingInteraction'' ] + } +``` From b221b2272de8aa50d6fd19632f42420a7571be80 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Thu, 12 May 2022 22:14:07 +0100 Subject: [PATCH 84/96] moved to discord haskell 13 - no major feature changes but a bunch of syntax ones --- src/Tablebot/Internal/Handler/Event.hs | 16 +++--- src/Tablebot/Plugins/Quote.hs | 72 +++++++++++++------------- src/Tablebot/Plugins/Reminder.hs | 10 ++-- src/Tablebot/Plugins/Roll/Plugin.hs | 8 +-- src/Tablebot/Utility/SmartParser.hs | 46 ++++++++-------- src/Tablebot/Utility/Types.hs | 6 +-- stack.yaml | 2 +- tutorials/3.Interactions.md | 12 ++--- 8 files changed, 85 insertions(+), 87 deletions(-) diff --git a/src/Tablebot/Internal/Handler/Event.hs b/src/Tablebot/Internal/Handler/Event.hs index bd25acb4..9ada3821 100644 --- a/src/Tablebot/Internal/Handler/Event.hs +++ b/src/Tablebot/Internal/Handler/Event.hs @@ -22,7 +22,7 @@ import Control.Concurrent (readMVar) import Control.Monad.RWS (MonadIO (liftIO), MonadReader (ask)) import qualified Data.Map as M import Data.Text as T (drop, isPrefixOf, length) -import Discord.Interactions (Interaction (..), InteractionDataApplicationCommand (interactionDataApplicationCommandId), InteractionDataComponent (interactionDataComponentCustomId)) +import Discord.Interactions (ApplicationCommandData (applicationCommandDataId), ComponentData (componentDataCustomId), Interaction (..)) import Discord.Types (ChannelId, Event, MessageId, ReactionInfo) import Tablebot.Internal.Plugins (changeAction) import Tablebot.Internal.Types as IT @@ -60,27 +60,27 @@ parseReactionDel cs info = mapM_ doReactionAdd cs -- | When given the compiled component recv actions and a component interaction, -- find and run the correct action. parseComponentRecv :: [CompiledComponentRecv] -> Interaction -> CompiledDatabaseDiscord () -parseComponentRecv cs info@InteractionComponent {interactionDataComponent = idc} = mapM_ removePrefix cs' +parseComponentRecv cs info@InteractionComponent {componentData = idc} = mapM_ removePrefix cs' where getPrefix ccr = componentPluginName ccr <> " " <> componentName ccr - cs' = filter (\ccr -> getPrefix ccr `isPrefixOf` interactionDataComponentCustomId idc) cs - removePrefix ccr = ccr `onComponentRecv` (info {interactionDataComponent = (idc {interactionDataComponentCustomId = T.drop (T.length (getPrefix ccr)) (interactionDataComponentCustomId idc)})}) + cs' = filter (\ccr -> getPrefix ccr `isPrefixOf` componentDataCustomId idc) cs + removePrefix ccr = ccr `onComponentRecv` (info {componentData = (idc {componentDataCustomId = T.drop (T.length (getPrefix ccr)) (componentDataCustomId idc)})}) parseComponentRecv _ _ = return () -- | When given an application command interaction, find and run the correct -- action. parseApplicationCommandRecv :: Interaction -> CompiledDatabaseDiscord () -parseApplicationCommandRecv info@InteractionApplicationCommand {interactionDataApplicationCommand = idac} = do +parseApplicationCommandRecv info@InteractionApplicationCommand {applicationCommandData = idac} = do tvar <- ask cache <- liftIO $ readMVar tvar - let action = UT.cacheApplicationCommands cache M.!? interactionDataApplicationCommandId idac + let action = UT.cacheApplicationCommands cache M.!? applicationCommandDataId idac case action of Nothing -> throwBot $ InteractionException "could not find the given application command" Just act -> changeAction () $ act info -parseApplicationCommandRecv info@InteractionApplicationCommandAutocomplete {interactionDataApplicationCommand = idac} = do +parseApplicationCommandRecv info@InteractionApplicationCommandAutocomplete {applicationCommandData = idac} = do tvar <- ask cache <- liftIO $ readMVar tvar - let action = UT.cacheApplicationCommands cache M.!? interactionDataApplicationCommandId idac + let action = UT.cacheApplicationCommands cache M.!? applicationCommandDataId idac case action of Nothing -> throwBot $ InteractionException "could not find the given application command" Just act -> changeAction () $ act info diff --git a/src/Tablebot/Plugins/Quote.hs b/src/Tablebot/Plugins/Quote.hs index 04625bc8..c64e98c4 100644 --- a/src/Tablebot/Plugins/Quote.hs +++ b/src/Tablebot/Plugins/Quote.hs @@ -125,9 +125,9 @@ thisQuote = Command "this" (parseComm thisComm) [] quoteMessageAppComm :: Maybe ApplicationCommandRecv quoteMessageAppComm = appcomm <&> (`ApplicationCommandRecv` recv) where - appcomm = createApplicationCommandMessage "quote" - recv i@InteractionApplicationCommand {interactionDataApplicationCommand = InteractionDataApplicationCommandMessage {..}, ..} = do - let mid = interactionDataApplicationCommandTargetId + appcomm = createMessage "quote" + recv i@InteractionApplicationCommand {applicationCommandData = ApplicationCommandDataMessage {..}, ..} = do + let mid = applicationCommandDataTargetMessageId case interactionChannelId of Nothing -> throwBot $ InteractionException "no channel id in quote interaction" Just cid -> do @@ -182,7 +182,7 @@ showQ qId m = do -- | @randomQuote@, which looks for a message of the form @!quote random@, -- selects a random quote from the database and responds with that quote. randomQ :: Context m => m -> DatabaseDiscord MessageDetails -randomQ c = filteredRandomQuote [] "Couldn't find any quotes!" c >>= \m -> return (m {messageDetailsComponents = Just [ComponentActionRowButton [randomButton]]}) +randomQ c = filteredRandomQuote [] "Couldn't find any quotes!" c >>= \m -> return (m {messageDetailsComponents = Just [ActionRowButtons [randomButton]]}) where randomButton = mkButton "Random quote" "quote random" @@ -192,7 +192,7 @@ randomQuoteComponentRecv = ComponentRecv "random" (processComponentInteraction ( -- | @authorQuote@, which looks for a message of the form @!quote author u@, -- selects a random quote from the database attributed to u and responds with that quote. authorQ :: Context m => Text -> m -> DatabaseDiscord MessageDetails -authorQ t c = filteredRandomQuote [QuoteAuthor ==. t] "Couldn't find any quotes with that author!" c >>= \m -> return (m {messageDetailsComponents = Just [ComponentActionRowButton [authorButton]]}) +authorQ t c = filteredRandomQuote [QuoteAuthor ==. t] "Couldn't find any quotes with that author!" c >>= \m -> return (m {messageDetailsComponents = Just [ActionRowButtons [authorButton]]}) where authorButton = mkButton "Random author quote" ("quote author " <> t) @@ -338,8 +338,8 @@ quoteApplicationCommand :: CreateApplicationCommand quoteApplicationCommand = CreateApplicationCommandChatInput "quote" "store and retrieve quotes" (Just opts) True where opts = - ApplicationCommandOptionsSubcommands $ - ApplicationCommandOptionSubcommandOrGroupSubcommand + OptionsSubcommands $ + OptionSubcommandOrGroupSubcommand <$> [ addQuoteAppComm, showQuoteAppComm, randomQuoteAppComm, @@ -347,47 +347,46 @@ quoteApplicationCommand = CreateApplicationCommandChatInput "quote" "store and r editQuoteAppComm ] addQuoteAppComm = - ApplicationCommandOptionSubcommand + OptionSubcommand "add" "add a new quote" - [ ApplicationCommandOptionValueString "quote" "what the actual quote is" True (Left False), - ApplicationCommandOptionValueString "author" "who authored this quote" True (Left False) + [ OptionValueString "quote" "what the actual quote is" True (Left False), + OptionValueString "author" "who authored this quote" True (Left False) ] showQuoteAppComm = - ApplicationCommandOptionSubcommand + OptionSubcommand "show" "show a quote by number" - [ ApplicationCommandOptionValueInteger "id" "the quote's number" True (Left True) (Just 1) Nothing + [ OptionValueInteger "id" "the quote's number" True (Left True) (Just 1) Nothing ] randomQuoteAppComm = - ApplicationCommandOptionSubcommand + OptionSubcommand "random" "show a random quote" [] authorQuoteAppComm = - ApplicationCommandOptionSubcommand + OptionSubcommand "author" "show a random quote by an author" - [ApplicationCommandOptionValueString "author" "whose quotes do you want to see" True (Left False)] + [OptionValueString "author" "whose quotes do you want to see" True (Left False)] editQuoteAppComm = - ApplicationCommandOptionSubcommand + OptionSubcommand "edit" "edit a quote" - [ ApplicationCommandOptionValueInteger "quoteid" "the id of the quote to edit" True (Left False) Nothing Nothing, - ApplicationCommandOptionValueString "quote" "what the actual quote is" False (Left False), - ApplicationCommandOptionValueString "author" "who authored this quote" False (Left False) + [ OptionValueInteger "quoteid" "the id of the quote to edit" True (Left False) Nothing Nothing, + OptionValueString "quote" "what the actual quote is" False (Left False), + OptionValueString "author" "who authored this quote" False (Left False) ] quoteApplicationCommandRecv :: Interaction -> DatabaseDiscord () quoteApplicationCommandRecv i@InteractionApplicationCommand - { interactionDataApplicationCommand = - InteractionDataApplicationCommandChatInput - { interactionDataApplicationCommandOptions = + { applicationCommandData = + ApplicationCommandDataChatInput + { optionsData = Just - ( InteractionDataApplicationCommandOptionsSubcommands - [ InteractionDataApplicationCommandOptionSubcommandOrGroupSubcommand subc - ] + ( OptionsDataSubcommands + [OptionDataSubcommandOrGroupSubcommand subc] ) } } = @@ -448,19 +447,18 @@ quoteApplicationCommandRecv ) _ -> throwBot $ InteractionException "unexpected quote interaction" where - subcname = interactionDataApplicationCommandOptionSubcommandName subc - vals = interactionDataApplicationCommandOptionSubcommandOptions subc + subcname = optionDataSubcommandName subc + vals = optionDataSubcommandOptions subc handleNothing Nothing _ = return () handleNothing (Just a) f = f a quoteApplicationCommandRecv i@InteractionApplicationCommandAutocomplete - { interactionDataApplicationCommand = - InteractionDataApplicationCommandChatInput - { interactionDataApplicationCommandOptions = + { applicationCommandData = + ApplicationCommandDataChatInput + { optionsData = Just - ( InteractionDataApplicationCommandOptionsSubcommands - [ InteractionDataApplicationCommandOptionSubcommandOrGroupSubcommand subc - ] + ( OptionsDataSubcommands + [OptionDataSubcommandOrGroupSubcommand subc] ) } } = @@ -469,8 +467,8 @@ quoteApplicationCommandRecv handleNothing (getValue "id" vals) ( \case - InteractionDataApplicationCommandOptionValueInteger _ (Right showid') -> interactionResponseAutocomplete i $ InteractionResponseAutocompleteInteger [Choice (pack $ show showid') showid'] - InteractionDataApplicationCommandOptionValueInteger _ (Left showid') -> do + OptionDataValueInteger _ (Right showid') -> interactionResponseAutocomplete i $ InteractionResponseAutocompleteInteger [Choice (pack $ show showid') showid'] + OptionDataValueInteger _ (Left showid') -> do allQ <- allQuotes () let allQ' = (\qe -> (show (fromSqlKey $ entityKey qe), (fromSqlKey $ entityKey qe, (\(Quote q _ _ _ _ _) -> q) (entityVal qe)))) <$> allQ options = take 25 $ closestPairsWithCosts (def {deletion = 100, substitution = 100, transposition = 5}) allQ' (unpack showid') @@ -479,8 +477,8 @@ quoteApplicationCommandRecv ) _ -> return () where - subcname = interactionDataApplicationCommandOptionSubcommandName subc - vals = interactionDataApplicationCommandOptionSubcommandOptions subc + subcname = optionDataSubcommandName subc + vals = optionDataSubcommandOptions subc handleNothing Nothing _ = return () handleNothing (Just a) f = f a quoteApplicationCommandRecv _ = return () diff --git a/src/Tablebot/Plugins/Reminder.hs b/src/Tablebot/Plugins/Reminder.hs index 0a03b5b5..528b8dfa 100644 --- a/src/Tablebot/Plugins/Reminder.hs +++ b/src/Tablebot/Plugins/Reminder.hs @@ -90,9 +90,9 @@ reminderParser (WErr (Qu content, ROI rawString)) m = do -- currently ignores the user's timezone... (TODO fix) addReminder :: UTCTime -> String -> Message -> DatabaseDiscord () addReminder time content m = do - let (Snowflake cid) = messageChannelId m - (Snowflake mid) = messageId m - (Snowflake uid) = userId $ messageAuthor m + let (Snowflake cid) = unId $ messageChannelId m + (Snowflake mid) = unId $ messageId m + (Snowflake uid) = unId $ userId $ messageAuthor m added <- insert $ Reminder cid mid uid time content let res = pack $ show $ fromSqlKey added sendMessage m ("Reminder " <> res <> " set for " <> toTimestamp time <> " with message `" <> pack content <> "`") @@ -131,13 +131,13 @@ reminderCron = do let (Reminder cid mid uid _time content) = entityVal re in do liftIO . print $ entityVal re - res <- getMessage (Snowflake cid) (Snowflake mid) + res <- getMessage (DiscordId $ Snowflake cid) (DiscordId $ Snowflake mid) case res of Left _ -> do sendChannelMessage (fromIntegral cid) (pack $ "Reminder to <@" ++ show uid ++ ">! " ++ content) delete (entityKey re) Right mess -> do - sendCustomReplyMessage mess (Snowflake mid) True $ + sendCustomReplyMessage mess (DiscordId $ Snowflake mid) True $ pack $ "Reminder to <@" ++ show uid ++ ">! " ++ content delete (entityKey re) diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index e17f1b70..242fc767 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -22,7 +22,7 @@ import Discord.Interactions ( Interaction (..), ) import Discord.Internal.Rest.Channel (ChannelRequest (..), MessageDetailedOpts (..)) -import Discord.Types (ComponentActionRow (..), ComponentButton (..), Message (..), User (..), mkButton, mkEmoji) +import Discord.Types (ActionRow (..), Button (..), Message (..), User (..), mkButton, mkEmoji) import System.Timeout (timeout) import Tablebot.Internal.Handler.Command (parseValue) import Tablebot.Plugins.Roll.Dice @@ -70,14 +70,14 @@ rollDice' e t u@(ParseUserId uid) = do ( (messageDetailsBasic msg) { messageDetailsComponents = Just - [ ComponentActionRowButton + [ ActionRowButtons -- we take the first 100 characters of the button customid -- because they're only allowed to be 100 characters long. -- the button is disabled if it's meant to be more than 100 -- characters so we don't have to worry about this. [ (mkButton buttonName (T.take 100 buttonCustomId)) - { componentButtonEmoji = Just (mkEmoji "🎲"), - componentButtonDisabled = buttonDisabled + { buttonEmoji = Just (mkEmoji "🎲"), + buttonDisabled = buttonDisabled } ] ] diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index 96e1ad05..7cbfb00c 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -64,7 +64,7 @@ instance Context Interaction where (MemberOrUser (Left m)) -> return m (MemberOrUser (Right _)) -> Nothing contextMessageId InteractionComponent {interactionMessage = m} = return $ messageId m - contextMessageId InteractionApplicationCommand {interactionDataApplicationCommand = InteractionDataApplicationCommandMessage {..}} = return interactionDataApplicationCommandTargetId + contextMessageId InteractionApplicationCommand {applicationCommandData = ApplicationCommandDataMessage {..}} = return applicationCommandDataTargetMessageId contextMessageId _ = Nothing -- | Custom infix operator to replace the error of a failing parser (regardless @@ -323,10 +323,10 @@ makeApplicationCommandPair name desc f = do -- a function's type. makeSlashCommand :: (MakeAppComm t) => Text -> Text -> Proxy t -> Maybe CreateApplicationCommand makeSlashCommand name desc p = - createApplicationCommandChatInput name desc >>= \cac -> + createChatInput name desc >>= \cac -> return $ cac - { createApplicationCommandOptions = Just $ ApplicationCommandOptionsValues $ makeAppComm p + { createOptions = Just $ OptionsValues $ makeAppComm p } -- | Create a series of command option values from the given types. @@ -334,7 +334,7 @@ makeSlashCommand name desc p = -- This is making the arguments for a text input/slash command from -- a proxy of the given function. class MakeAppComm commandty where - makeAppComm :: Proxy commandty -> [ApplicationCommandOptionValue] + makeAppComm :: Proxy commandty -> [OptionValue] -- As a base case, no more arguments instance {-# OVERLAPPING #-} MakeAppComm (EnvDatabaseDiscord s MessageDetails) where @@ -349,12 +349,12 @@ instance {-# OVERLAPPABLE #-} (MakeAppComm mac) => MakeAppComm (ParseUserId -> m -- | From a single value, make an argument for a slash command command. class MakeAppCommArg commandty where - makeAppCommArg :: Proxy commandty -> ApplicationCommandOptionValue + makeAppCommArg :: Proxy commandty -> OptionValue -- Create a labelled text argument. By default it is required and does not -- have autocompeletion. instance (KnownSymbol name, KnownSymbol desc) => MakeAppCommArg (Labelled name desc Text) where - makeAppCommArg l = ApplicationCommandOptionValueString n d True (Left False) + makeAppCommArg l = OptionValueString n d True (Left False) where (n, d) = getLabelValues l @@ -362,7 +362,7 @@ instance (KnownSymbol name, KnownSymbol desc) => MakeAppCommArg (Labelled name d instance (KnownSymbol name, KnownSymbol desc, MakeAppCommArg (Labelled name desc t)) => MakeAppCommArg (Labelled name desc (Maybe t)) where makeAppCommArg _ = (makeAppCommArg (Proxy :: Proxy (Labelled name desc t))) - { applicationCommandOptionValueRequired = False + { optionValueRequired = False } -- When quoted text is required, just fake it and get a sub layer. @@ -389,11 +389,11 @@ instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s) => ProcessAppComm (Interact -- -- If the argument is a ProcessAppCommArg, then parse it and recurse. instance {-# OVERLAPPABLE #-} (ProcessAppCommArg ty s, ProcessAppComm pac s) => ProcessAppComm (ty -> pac) s where - processAppComm comm i@InteractionApplicationCommand {interactionDataApplicationCommand = InteractionDataApplicationCommandChatInput {interactionDataApplicationCommandOptions = opts}} = do + processAppComm comm i@InteractionApplicationCommand {applicationCommandData = ApplicationCommandDataChatInput {optionsData = opts}} = do t <- processAppCommArg (getVs opts) processAppComm (comm t) i where - getVs (Just (InteractionDataApplicationCommandOptionsValues vs)) = vs + getVs (Just (OptionsDataValues vs)) = vs getVs _ = [] processAppComm _ _ = throwBot $ InteractionException "could not process args to application command" @@ -412,26 +412,26 @@ instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s) => ProcessAppComm (ParseUse -- Given a type `t`, parse a value of that type from the given list of option -- values. class ProcessAppCommArg t s where - processAppCommArg :: [InteractionDataApplicationCommandOptionValue] -> EnvDatabaseDiscord s t + processAppCommArg :: [OptionDataValue] -> EnvDatabaseDiscord s t -- | Given a string, find the first option value with that name in the list, -- returning Nothing if none is found. -getValue :: String -> [InteractionDataApplicationCommandOptionValue] -> Maybe InteractionDataApplicationCommandOptionValue -getValue t = find ((== pack t) . interactionDataApplicationCommandOptionValueName) +getValue :: String -> [OptionDataValue] -> Maybe OptionDataValue +getValue t = find ((== pack t) . optionDataValueName) -- | Tries to extract an integer from a given option value. -integerFromOptionValue :: InteractionDataApplicationCommandOptionValue -> Maybe Integer -integerFromOptionValue InteractionDataApplicationCommandOptionValueInteger {interactionDataApplicationCommandOptionValueIntegerValue = Right i} = Just i +integerFromOptionValue :: OptionDataValue -> Maybe Integer +integerFromOptionValue OptionDataValueInteger {optionDataValueInteger = Right i} = Just i integerFromOptionValue _ = Nothing -- | Tries to extract a scientific number from a given option value. -scientificFromOptionValue :: InteractionDataApplicationCommandOptionValue -> Maybe Scientific -scientificFromOptionValue InteractionDataApplicationCommandOptionValueNumber {interactionDataApplicationCommandOptionValueNumberValue = Right i} = Just i +scientificFromOptionValue :: OptionDataValue -> Maybe Scientific +scientificFromOptionValue OptionDataValueNumber {optionDataValueNumber = Right i} = Just i scientificFromOptionValue _ = Nothing -- | Tries to extract a string from a given option value. -stringFromOptionValue :: InteractionDataApplicationCommandOptionValue -> Maybe Text -stringFromOptionValue InteractionDataApplicationCommandOptionValueString {interactionDataApplicationCommandOptionValueStringValue = Right i} = Just i +stringFromOptionValue :: OptionDataValue -> Maybe Text +stringFromOptionValue OptionDataValueString {optionDataValueString = Right i} = Just i stringFromOptionValue _ = Nothing -- there are a number of missing slash command argument types missing here, which I've not added yet. @@ -439,19 +439,19 @@ stringFromOptionValue _ = Nothing -- extract a string of the given type from the arguments instance (KnownSymbol name) => ProcessAppCommArg (Labelled name desc Text) s where processAppCommArg is = case getValue (symbolVal (Proxy :: Proxy name)) is of - Just (InteractionDataApplicationCommandOptionValueString _ (Right t)) -> return $ labelValue t + Just (OptionDataValueString _ (Right t)) -> return $ labelValue t _ -> throwBot $ InteractionException "could not find required parameter" -- extract an integer of the given type from the arguments instance (KnownSymbol name) => ProcessAppCommArg (Labelled name desc Integer) s where processAppCommArg is = case getValue (symbolVal (Proxy :: Proxy name)) is of - Just (InteractionDataApplicationCommandOptionValueInteger _ (Right i)) -> return $ labelValue i + Just (OptionDataValueInteger _ (Right i)) -> return $ labelValue i _ -> throwBot $ InteractionException "could not find required parameter" -- extract a scientific number of the given type from the arguments instance (KnownSymbol name) => ProcessAppCommArg (Labelled name desc Scientific) s where processAppCommArg is = case getValue (symbolVal (Proxy :: Proxy name)) is of - Just (InteractionDataApplicationCommandOptionValueNumber _ (Right i)) -> return $ labelValue i + Just (OptionDataValueNumber _ (Right i)) -> return $ labelValue i _ -> throwBot $ InteractionException "could not find required parameter" -- extract a quote of the given type from the arguments @@ -490,11 +490,11 @@ processComponentInteraction f = processComponentInteraction' (parseComm f) -- The format of the Text being given should be of space separated values, -- similar to the command structure. processComponentInteraction' :: Parser (Interaction -> EnvDatabaseDiscord s MessageDetails) -> Bool -> Interaction -> EnvDatabaseDiscord s () -processComponentInteraction' compParser updateOriginal i@InteractionComponent {interactionDataComponent = idc} = errorCatch $ do +processComponentInteraction' compParser updateOriginal i@InteractionComponent {componentData = idc} = errorCatch $ do let componentSend | updateOriginal = interactionResponseComponentsUpdateMessage i | otherwise = interactionResponseCustomMessage i - action <- parseValue (skipSpace *> compParser) (interactionDataComponentCustomId idc) >>= ($ i) + action <- parseValue (skipSpace *> compParser) (componentDataCustomId idc) >>= ($ i) componentSend action where catchParserException e@(ParserException _ _) = interactionResponseCustomMessage i $ (messageDetailsBasic "something (likely) went wrong when processing a component interaction") {messageDetailsEmbeds = Just [embedError (e :: BotException)]} diff --git a/src/Tablebot/Utility/Types.hs b/src/Tablebot/Utility/Types.hs index 183ef1c3..970acf2b 100644 --- a/src/Tablebot/Utility/Types.hs +++ b/src/Tablebot/Utility/Types.hs @@ -33,11 +33,11 @@ import Discord.Interactions ) import Discord.Internal.Rest.Channel (MessageDetailedOpts (MessageDetailedOpts)) import Discord.Types - ( AllowedMentions, + ( ActionRow, + AllowedMentions, ApplicationCommandId, Attachment, ChannelId, - ComponentActionRow, CreateEmbed, Emoji, Event (..), @@ -319,7 +319,7 @@ data MessageDetails = MessageDetails messageDetailsAllowedMentions :: Maybe AllowedMentions, messageDetailsFlags :: Maybe InteractionResponseMessageFlags, messageDetailsReference :: Maybe MessageReference, - messageDetailsComponents :: Maybe [ComponentActionRow], + messageDetailsComponents :: Maybe [ActionRow], messageDetailsAttachments :: Maybe [Attachment], messageDetailsStickerIds :: Maybe [StickerId] } diff --git a/stack.yaml b/stack.yaml index c3a22f23..97d38292 100644 --- a/stack.yaml +++ b/stack.yaml @@ -42,7 +42,7 @@ packages: # allow-newer: true extra-deps: -- discord-haskell-1.12.5 +- discord-haskell-1.13.0 - emoji-0.1.0.2 - load-env-0.2.1.0 - megaparsec-9.0.1 diff --git a/tutorials/3.Interactions.md b/tutorials/3.Interactions.md index 10eb40c4..70bf3af9 100644 --- a/tutorials/3.Interactions.md +++ b/tutorials/3.Interactions.md @@ -183,7 +183,7 @@ Doing this though, we immediately come up on the snag. Even if we aren't sending ```haskell pingDB :: ParseUserId -> DatabaseDiscord MessageDetails pingDB (ParseUserId u) = do - let uid = extractFromSnowflake u + let uid = extractFromSnowflake $ unId u ``` We then construct the interaction as we have done before, add it to the interactions, and boom, we have another slash command to work with! @@ -211,7 +211,7 @@ A button needs the text it will display, and a unique identifier that we'll use ```haskell myPingAction :: DatabaseDiscord MessageDetails myPingAction = return $ (messageDetailsBasic "pong") { messageDetailsComponents = Just [cps] } - where cps = ComponentActionRowButton [mkButton "Ping!" "myping pingbutton"] + where cps = ActionRowButtons [mkButton "Ping!" "myping pingbutton"] ``` Running the bot, we see that we get the button now! But it just loads for a couple seconds then errors. We need our bot to actually handle this button press. @@ -246,7 +246,7 @@ myPingButtonAction :: Integer -> Interaction -> DatabaseDiscord MessageDetails myPingButtonAction i inter = do sendReplyMessage (interactionMessage inter) ("pong " <> pack (show i)) -- respond to the message with a pong return $ def { messageDetailsComponents = Just [cps] } -- the message to update the original with - where cps = ComponentActionRowButton [mkButton "Ping!" ("myping pingbutton " <> pack (show (i + 1)))] + where cps = ActionRowButtons [mkButton "Ping!" ("myping pingbutton " <> pack (show (i + 1)))] ``` And then we edit `myPingButton` so that it uses the above function and also uses the output from it to update the original message: `ComponentRecv "pingbutton" (processComponentInteraction myPingButtonAction True)`. @@ -289,7 +289,7 @@ myPing = Command "myping" (parseComm myPingAction) [] myPingAction :: DatabaseDiscord MessageDetails myPingAction = return $ (messageDetailsBasic "pong") { messageDetailsComponents = Just [cps] } - where cps = ComponentActionRowButton [mkButton "Ping!" "myping pingbutton 0"] + where cps = ActionRowButtons [mkButton "Ping!" "myping pingbutton 0"] myPingInteraction :: Maybe ApplicationCommandRecv myPingInteraction = makeApplicationCommandPair "myping" "my unique ping" myPingAction @@ -301,7 +301,7 @@ myPingButtonAction :: Integer -> Interaction -> DatabaseDiscord MessageDetails myPingButtonAction i inter = do sendReplyMessage (interactionMessage inter) ("pong " <> pack (show i)) return $ def { messageDetailsComponents = Just [cps] } - where cps = ComponentActionRowButton [mkButton "Ping!" ("myping pingbutton " <> pack (show (i + 1)))] + where cps = ActionRowButtons [mkButton "Ping!" ("myping pingbutton " <> pack (show (i + 1)))] myEcho :: Command myEcho = Command "myecho" (parseComm (echoHelp . unROI)) [] @@ -317,7 +317,7 @@ myPing'' = Command "cmyping" (parseComm pingDB) [] pingDB :: ParseUserId -> DatabaseDiscord MessageDetails pingDB (ParseUserId u) = do - let uid = extractFromSnowflake u + let uid = extractFromSnowflake $ unId u user <- liftSql $ select $ from $ \p -> do where_ (p ^. PingCountUid ==. val uid) return p From 074501b3eb030e41d247a6271fffd28738eea44e Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 28 May 2022 14:03:16 +0100 Subject: [PATCH 85/96] changes to take advantge of some new discord-haskell type system stuff --- src/Tablebot/Plugins/Quote.hs | 8 ++++---- src/Tablebot/Plugins/Roll/Plugin.hs | 24 ++++++++++++------------ src/Tablebot/Utility/SmartParser.hs | 27 ++++++++++++++------------- tutorials/3.Interactions.md | 10 +++++----- 4 files changed, 35 insertions(+), 34 deletions(-) diff --git a/src/Tablebot/Plugins/Quote.hs b/src/Tablebot/Plugins/Quote.hs index c64e98c4..67fa71df 100644 --- a/src/Tablebot/Plugins/Quote.hs +++ b/src/Tablebot/Plugins/Quote.hs @@ -134,7 +134,7 @@ quoteMessageAppComm = appcomm <&> (`ApplicationCommandRecv` recv) m <- getMessage cid mid case m of Left _ -> throwBot $ InteractionException "could not get message to quote" - Right msg -> interactionResponseCustomMessage i =<< addMessageQuote (parseUserId $ contextUserId i) msg i + Right msg -> interactionResponseCustomMessage i =<< addMessageQuote (contextUserId i) msg i recv _ = return def authorQuote :: Command @@ -406,7 +406,7 @@ quoteApplicationCommandRecv handleNothing ((getValue "quote" vals >>= stringFromOptionValue) >>= \q -> (getValue "author" vals >>= stringFromOptionValue) <&> (q,)) ( \(qt, author) -> do - let requestor = toMention' $ parseUserId $ contextUserId i + let requestor = toMention' $ contextUserId i (msg, qid) <- addQ' qt author requestor 0 0 i interactionResponseCustomMessage i msg -- to get the message to display as wanted, we have to do some trickery @@ -435,13 +435,13 @@ quoteApplicationCommandRecv case (qt, author) of (Nothing, Nothing) -> interactionResponseCustomMessage i (makeEphermeral (messageDetailsBasic "No edits made to quote.")) _ -> do - msg <- editQ' qid qt author (toMention' $ parseUserId $ contextUserId i) 0 0 i + msg <- editQ' qid qt author (toMention' $ contextUserId i) 0 0 i interactionResponseCustomMessage i msg v <- liftDiscord $ restCall $ R.GetOriginalInteractionResponse (interactionApplicationId i) (interactionToken i) case v of Left _ -> return () Right m -> do - msg' <- editQ' qid qt author (toMention' $ parseUserId $ contextUserId i) (fromIntegral $ messageId m) (fromIntegral $ messageChannelId m) i + msg' <- editQ' qid qt author (toMention' $ contextUserId i) (fromIntegral $ messageId m) (fromIntegral $ messageChannelId m) i _ <- liftDiscord $ restCall $ R.EditOriginalInteractionResponse (interactionApplicationId i) (interactionToken i) (convertMessageFormatInteraction msg') return () ) diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index 242fc767..bf0df806 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -22,7 +22,7 @@ import Discord.Interactions ( Interaction (..), ) import Discord.Internal.Rest.Channel (ChannelRequest (..), MessageDetailedOpts (..)) -import Discord.Types (ActionRow (..), Button (..), Message (..), User (..), mkButton, mkEmoji) +import Discord.Types (ActionRow (..), Button (..), Message (..), User (..), UserId, mkButton, mkEmoji) import System.Timeout (timeout) import Tablebot.Internal.Handler.Command (parseValue) import Tablebot.Plugins.Roll.Dice @@ -40,8 +40,8 @@ import Text.RawString.QQ (r) -- | The basic execution function for rolling dice. Both the expression and message are -- optional. If the expression is not given, then the default roll is used. -- The userid of the user that called this command is also given. -rollDice'' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> ParseUserId -> DatabaseDiscord Text -rollDice'' e' t (ParseUserId u) = do +rollDice'' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> UserId -> DatabaseDiscord Text +rollDice'' e' t u = do let e = fromMaybe (Right defaultRoll) e' (vs, ss) <- case e of (Left a) -> liftIO $ first Left <$> evalList a @@ -63,9 +63,9 @@ rollDice'' e' t (ParseUserId u) = do simplify li = li countFormatting s = (`div` 4) $ T.foldr (\c cf -> cf + (2 * fromEnum (c == '`')) + fromEnum (c `elem` ['~', '_', '*'])) 0 s -rollDice' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> ParseUserId -> DatabaseDiscord MessageDetails -rollDice' e t u@(ParseUserId uid) = do - msg <- rollDice'' e t u +rollDice' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> UserId -> DatabaseDiscord MessageDetails +rollDice' e t uid = do + msg <- rollDice'' e t uid return ( (messageDetailsBasic msg) { messageDetailsComponents = @@ -89,7 +89,7 @@ rollDice' e t u@(ParseUserId uid) = do buttonCustomId = (("roll reroll " <> pack (show uid)) `appendIf` e) `appendIf` t (buttonName, buttonDisabled) = if T.length buttonCustomId > 100 then ("Expr too long", True) else ("Reroll", False) -rollSlashCommandFunction :: Labelled "expression" "what's being evaluated" (Maybe Text) -> Labelled "quote" "associated message" (Maybe (Quoted Text)) -> ParseUserId -> DatabaseDiscord MessageDetails +rollSlashCommandFunction :: Labelled "expression" "what's being evaluated" (Maybe Text) -> Labelled "quote" "associated message" (Maybe (Quoted Text)) -> UserId -> DatabaseDiscord MessageDetails rollSlashCommandFunction (Labelled mt) (Labelled qt) uid = do lve <- mapM (parseValue (pars <* eof)) mt rollDice' lve qt uid @@ -103,16 +103,16 @@ rollDiceParser :: Parser (Message -> DatabaseDiscord ()) rollDiceParser = choice (try <$> options) where -- Just the value is given to the command, no quote. - justEither :: WithError "Incorrect expression/list value. Please check the expression" (Either ListValues Expr) -> ParseUserId -> DatabaseDiscord MessageDetails + justEither :: WithError "Incorrect expression/list value. Please check the expression" (Either ListValues Expr) -> UserId -> DatabaseDiscord MessageDetails justEither (WErr x) = rollDice' (Just x) Nothing -- Nothing is given to the command, a default case. - nothingAtAll :: WithError "Expected eof" () -> ParseUserId -> DatabaseDiscord MessageDetails + nothingAtAll :: WithError "Expected eof" () -> UserId -> DatabaseDiscord MessageDetails nothingAtAll (WErr _) = rollDice' Nothing Nothing -- Both the value and the quote are present. - bothVals :: WithError "Incorrect format. Please check the expression and quote" (Either ListValues Expr, Quoted Text) -> ParseUserId -> DatabaseDiscord MessageDetails + bothVals :: WithError "Incorrect format. Please check the expression and quote" (Either ListValues Expr, Quoted Text) -> UserId -> DatabaseDiscord MessageDetails bothVals (WErr (x, y)) = rollDice' (Just x) (Just y) -- Just the quote is given to the command. - justText :: WithError "Incorrect quote. Please check the quote format" (Quoted Text) -> ParseUserId -> DatabaseDiscord MessageDetails + justText :: WithError "Incorrect quote. Please check the quote format" (Quoted Text) -> UserId -> DatabaseDiscord MessageDetails justText (WErr x) = rollDice' Nothing (Just x) options = [ parseComm justEither, @@ -147,7 +147,7 @@ rollDice = Command "roll" rollDiceParser [statsCommand] rollDiceInline :: InlineCommand rollDiceInline = inlineCommandHelper "[|" "|]" pars (\e m -> runFunc e m >>= sendCustomMessage m) where - runFunc e m = rollDice' (Just e) Nothing (ParseUserId $ userId $ messageAuthor m) + runFunc e m = rollDice' (Just e) Nothing (userId $ messageAuthor m) -- | Help page for rolling dice, with a link to the help page. rollHelp :: HelpPage diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index 7cbfb00c..d255924c 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -36,13 +36,13 @@ import Text.Megaparsec (MonadParsec (eof, try), chunk, many, observing, optional -- -- Only defined for Message and Interaction. class Context a where - contextUserId :: a -> ParseUserId + contextUserId :: a -> UserId contextGuildId :: a -> EnvDatabaseDiscord s (Maybe GuildId) contextMember :: a -> Maybe GuildMember contextMessageId :: a -> Maybe MessageId instance Context Message where - contextUserId = ParseUserId . userId . messageAuthor + contextUserId = userId . messageAuthor contextGuildId m = case messageGuildId m of Just a -> pure $ Just a Nothing -> do @@ -56,7 +56,7 @@ instance Context Message where instance Context Interaction where -- this is safe to do because we are guaranteed to get either a user or a member - contextUserId i = ParseUserId $ maybe 0 userId (either memberUser Just mor) + contextUserId i = maybe 0 userId (either memberUser Just mor) where (MemberOrUser mor) = interactionUser i contextGuildId i = return $ interactionGuildId i @@ -157,10 +157,10 @@ instance {-# OVERLAPPABLE #-} (Context t, CanParse a, PComm (t -> as) s t r) => this <- parsThenMoveToNext @a parseComm (`comm` this) --- special value case - if we get ParseUserId, we need to get the value from +-- special value case - if we get UserId, we need to get the value from -- the context. so, get the value from the context, and then continue parsing. -- (10) -instance {-# OVERLAPPABLE #-} (PComm (t -> as) s t r) => PComm (ParseUserId -> as) s t r where +instance {-# OVERLAPPABLE #-} (PComm (t -> as) s t r) => PComm (UserId -> as) s t r where parseComm comm = parseComm $ \(m :: t) -> comm (contextUserId m) -- | @CanParse@ defines types from which we can generate parsers. @@ -278,9 +278,6 @@ newtype RestOfInput1 a = ROI1 a instance IsString a => CanParse (RestOfInput1 a) where pars = ROI1 . fromString <$> untilEnd1 --- | Data type to represent parsing a user id from the context. -newtype ParseUserId = ParseUserId {parseUserId :: UserId} - -- | Labelled value for use with smart commands. -- -- This is for use with slash commands, where there is a name and description @@ -344,7 +341,8 @@ instance {-# OVERLAPPING #-} MakeAppComm (EnvDatabaseDiscord s MessageDetails) w instance {-# OVERLAPPABLE #-} (MakeAppComm mac, MakeAppCommArg ty) => MakeAppComm (ty -> mac) where makeAppComm _ = makeAppCommArg (Proxy :: Proxy ty) : makeAppComm (Proxy :: Proxy mac) -instance {-# OVERLAPPABLE #-} (MakeAppComm mac) => MakeAppComm (ParseUserId -> mac) where +-- we don't get the user id from the command itself, so ignore it +instance {-# OVERLAPPABLE #-} (MakeAppComm mac) => MakeAppComm (UserId -> mac) where makeAppComm _ = makeAppComm (Proxy :: Proxy mac) -- | From a single value, make an argument for a slash command command. @@ -372,6 +370,8 @@ instance (KnownSymbol name, KnownSymbol desc, MakeAppCommArg (Labelled name desc -- As a base case, send the message produced -- | Process an application command when given a function/value. +-- +-- `s` is the context of the environment. class ProcessAppComm commandty s where processAppComm :: commandty -> Interaction -> EnvDatabaseDiscord s () @@ -398,11 +398,11 @@ instance {-# OVERLAPPABLE #-} (ProcessAppCommArg ty s, ProcessAppComm pac s) => processAppComm _ _ = throwBot $ InteractionException "could not process args to application command" -- one specific implementation case when we want to parse out a user id. -instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s) => ProcessAppComm (ParseUserId -> pac) s where +instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s) => ProcessAppComm (UserId -> pac) s where processAppComm comm i@InteractionApplicationCommand {interactionUser = MemberOrUser u} = case getUser of Nothing -> throwBot $ InteractionException "could not process args to application command" - Just uid -> processAppComm (comm (ParseUserId uid)) i + Just uid -> processAppComm (comm uid) i where getUser = userId <$> either memberUser Just u processAppComm _ _ = throwBot $ InteractionException "could not process args to application command" @@ -435,6 +435,7 @@ stringFromOptionValue OptionDataValueString {optionDataValueString = Right i} = stringFromOptionValue _ = Nothing -- there are a number of missing slash command argument types missing here, which I've not added yet. +-- we can add ids of various sorts -- extract a string of the given type from the arguments instance (KnownSymbol name) => ProcessAppCommArg (Labelled name desc Text) s where @@ -534,8 +535,8 @@ onlyAllowRequestor' msg f = do ) <* eof where - prefunc :: UserId -> ParseUserId -> Interaction -> DatabaseDiscord (Maybe MessageDetails) - prefunc uid (ParseUserId u) i = + prefunc :: UserId -> UserId -> Interaction -> DatabaseDiscord (Maybe MessageDetails) + prefunc uid u i = if uid == u then return Nothing else diff --git a/tutorials/3.Interactions.md b/tutorials/3.Interactions.md index 70bf3af9..c8c9a9f9 100644 --- a/tutorials/3.Interactions.md +++ b/tutorials/3.Interactions.md @@ -178,11 +178,11 @@ One of the most useful bits of information that we would want to get is the user As before, we'll start by moving the helper function (`pingDB`) to its own top level function, changing any message sending to returning a `MessageDetails`, and then removing `Message` from the signature. -Doing this though, we immediately come up on the snag. Even if we aren't sending a message using `m` any more, we are still getting the user id of the user that sent the message. To solve this, we have to introduce a new type, `ParseUserId`. This is a special type that tells our automated stuff "hey I would like a user id here please". This changes the first couple lines of `pingDB` to the following. +Doing this though, we immediately come up on the snag. Even if we aren't sending a message using `m` any more, we are still getting the user id of the user that sent the message. To solve this, we have to perform a special kind of parsing, using the context we have. This changes the first couple lines of `pingDB` to the following. ```haskell -pingDB :: ParseUserId -> DatabaseDiscord MessageDetails -pingDB (ParseUserId u) = do +pingDB :: UserId -> DatabaseDiscord MessageDetails +pingDB u = do let uid = extractFromSnowflake $ unId u ``` @@ -315,8 +315,8 @@ myEchoInteraction = makeApplicationCommandPair "myecho" "echo your input" (echoH myPing'' :: Command myPing'' = Command "cmyping" (parseComm pingDB) [] -pingDB :: ParseUserId -> DatabaseDiscord MessageDetails -pingDB (ParseUserId u) = do +pingDB :: UserId -> DatabaseDiscord MessageDetails +pingDB u = do let uid = extractFromSnowflake $ unId u user <- liftSql $ select $ from $ \p -> do where_ (p ^. PingCountUid ==. val uid) From f8fbef26d3297678c283787be8a4b7f6c953d58b Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Fri, 15 Jul 2022 16:17:43 +0100 Subject: [PATCH 86/96] made changes and split some things up --- src/Tablebot/Plugins/Quote.hs | 2 +- src/Tablebot/Plugins/Roll/Plugin.hs | 29 +- src/Tablebot/Utility/SmartParser.hs | 545 +----------------- .../Utility/SmartParser/Interactions.hs | 286 +++++++++ .../Utility/SmartParser/SmartParser.hs | 220 +++++++ src/Tablebot/Utility/SmartParser/Types.hs | 102 ++++ src/Tablebot/Utility/Types.hs | 1 + stack.yaml | 2 +- tutorials/3.Interactions.md | 8 +- 9 files changed, 640 insertions(+), 555 deletions(-) create mode 100644 src/Tablebot/Utility/SmartParser/Interactions.hs create mode 100644 src/Tablebot/Utility/SmartParser/SmartParser.hs create mode 100644 src/Tablebot/Utility/SmartParser/Types.hs diff --git a/src/Tablebot/Plugins/Quote.hs b/src/Tablebot/Plugins/Quote.hs index 67fa71df..e2d5742b 100644 --- a/src/Tablebot/Plugins/Quote.hs +++ b/src/Tablebot/Plugins/Quote.hs @@ -335,7 +335,7 @@ renderCustomQuoteMessage t (Quote txt author submitter msgId cnlId dtm) qId m = maybeAddFooter Nothing = "" quoteApplicationCommand :: CreateApplicationCommand -quoteApplicationCommand = CreateApplicationCommandChatInput "quote" "store and retrieve quotes" (Just opts) True +quoteApplicationCommand = CreateApplicationCommandChatInput "quote" "store and retrieve quotes" (Just opts) Nothing True where opts = OptionsSubcommands $ diff --git a/src/Tablebot/Plugins/Roll/Plugin.hs b/src/Tablebot/Plugins/Roll/Plugin.hs index bf0df806..11b0faa5 100644 --- a/src/Tablebot/Plugins/Roll/Plugin.hs +++ b/src/Tablebot/Plugins/Roll/Plugin.hs @@ -41,7 +41,7 @@ import Text.RawString.QQ (r) -- optional. If the expression is not given, then the default roll is used. -- The userid of the user that called this command is also given. rollDice'' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> UserId -> DatabaseDiscord Text -rollDice'' e' t u = do +rollDice'' e' t uid = do let e = fromMaybe (Right defaultRoll) e' (vs, ss) <- case e of (Left a) -> liftIO $ first Left <$> evalList a @@ -52,7 +52,7 @@ rollDice'' e' t u = do else return (makeMsg (simplify vs) (parseShow e <> " `[could not display rolls]`")) where dsc = maybe ": " (\(Qu t') -> " \"" <> t' <> "\": ") t - baseMsg = toMention' u <> " rolled" <> dsc + baseMsg = toMention' uid <> " rolled" <> dsc makeLine (i, s) = pack (show i) <> Data.Text.replicate (max 0 (6 - length (show i))) " " <> " ⟵ " <> s makeMsg (Right v) s = baseMsg <> s <> ".\nOutput: " <> pack (show v) makeMsg (Left []) _ = baseMsg <> "No output." @@ -63,8 +63,9 @@ rollDice'' e' t u = do simplify li = li countFormatting s = (`div` 4) $ T.foldr (\c cf -> cf + (2 * fromEnum (c == '`')) + fromEnum (c `elem` ['~', '_', '*'])) 0 s -rollDice' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> UserId -> DatabaseDiscord MessageDetails -rollDice' e t uid = do +-- | A version of rollDice'' that is nicer to parse and has a constructed message. +rollDice' :: Maybe (Either ListValues Expr) -> Maybe (Quoted Text) -> SenderUserId -> DatabaseDiscord MessageDetails +rollDice' e t (SenderUserId uid) = do msg <- rollDice'' e t uid return ( (messageDetailsBasic msg) @@ -89,10 +90,10 @@ rollDice' e t uid = do buttonCustomId = (("roll reroll " <> pack (show uid)) `appendIf` e) `appendIf` t (buttonName, buttonDisabled) = if T.length buttonCustomId > 100 then ("Expr too long", True) else ("Reroll", False) -rollSlashCommandFunction :: Labelled "expression" "what's being evaluated" (Maybe Text) -> Labelled "quote" "associated message" (Maybe (Quoted Text)) -> UserId -> DatabaseDiscord MessageDetails -rollSlashCommandFunction (Labelled mt) (Labelled qt) uid = do +rollSlashCommandFunction :: Labelled "expression" "what's being evaluated" (Maybe Text) -> Labelled "quote" "associated message" (Maybe (Quoted Text)) -> SenderUserId -> DatabaseDiscord MessageDetails +rollSlashCommandFunction (Labelled mt) (Labelled qt) suid = do lve <- mapM (parseValue (pars <* eof)) mt - rollDice' lve qt uid + rollDice' lve qt suid rerollComponentRecv :: ComponentRecv rerollComponentRecv = ComponentRecv "reroll" (processComponentInteraction' rollDiceParserI True) @@ -103,16 +104,16 @@ rollDiceParser :: Parser (Message -> DatabaseDiscord ()) rollDiceParser = choice (try <$> options) where -- Just the value is given to the command, no quote. - justEither :: WithError "Incorrect expression/list value. Please check the expression" (Either ListValues Expr) -> UserId -> DatabaseDiscord MessageDetails + justEither :: WithError "Incorrect expression/list value. Please check the expression" (Either ListValues Expr) -> SenderUserId -> DatabaseDiscord MessageDetails justEither (WErr x) = rollDice' (Just x) Nothing -- Nothing is given to the command, a default case. - nothingAtAll :: WithError "Expected eof" () -> UserId -> DatabaseDiscord MessageDetails + nothingAtAll :: WithError "Expected eof" () -> SenderUserId -> DatabaseDiscord MessageDetails nothingAtAll (WErr _) = rollDice' Nothing Nothing -- Both the value and the quote are present. - bothVals :: WithError "Incorrect format. Please check the expression and quote" (Either ListValues Expr, Quoted Text) -> UserId -> DatabaseDiscord MessageDetails + bothVals :: WithError "Incorrect format. Please check the expression and quote" (Either ListValues Expr, Quoted Text) -> SenderUserId -> DatabaseDiscord MessageDetails bothVals (WErr (x, y)) = rollDice' (Just x) (Just y) -- Just the quote is given to the command. - justText :: WithError "Incorrect quote. Please check the quote format" (Quoted Text) -> UserId -> DatabaseDiscord MessageDetails + justText :: WithError "Incorrect quote. Please check the quote format" (Quoted Text) -> SenderUserId -> DatabaseDiscord MessageDetails justText (WErr x) = rollDice' Nothing (Just x) options = [ parseComm justEither, @@ -130,8 +131,8 @@ rollDiceParserI = choice (try <$> options) options = [ onlyAllowRequestor (\lv -> rollDice' (Just lv) Nothing), onlyAllowRequestor (rollDice' Nothing Nothing), - try (onlyAllowRequestor (\lv qt -> rollDice' (Just lv) (Just qt))), - try (onlyAllowRequestor (rollDice' Nothing . Just)) + onlyAllowRequestor (\lv qt -> rollDice' (Just lv) (Just qt)), + onlyAllowRequestor (rollDice' Nothing . Just) ] -- | Basic command for rolling dice. @@ -147,7 +148,7 @@ rollDice = Command "roll" rollDiceParser [statsCommand] rollDiceInline :: InlineCommand rollDiceInline = inlineCommandHelper "[|" "|]" pars (\e m -> runFunc e m >>= sendCustomMessage m) where - runFunc e m = rollDice' (Just e) Nothing (userId $ messageAuthor m) + runFunc e m = rollDice' (Just e) Nothing (SenderUserId $ userId $ messageAuthor m) -- | Help page for rolling dice, with a link to the help page. rollHelp :: HelpPage diff --git a/src/Tablebot/Utility/SmartParser.hs b/src/Tablebot/Utility/SmartParser.hs index d255924c..c69f9ac4 100644 --- a/src/Tablebot/Utility/SmartParser.hs +++ b/src/Tablebot/Utility/SmartParser.hs @@ -1,6 +1,3 @@ -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeApplications #-} - -- | -- Module : Tablebot.Utility.SmartParser -- Description : Automatic parser generation from function types. @@ -12,535 +9,13 @@ -- Generates a parser based on the shape of the command function. -- For example, if you have a command that takes in an Int as argument, we -- build a parser that reads in that Int and then runs the command. -module Tablebot.Utility.SmartParser where - -import Control.Monad.Exception (MonadException (catch)) -import Data.Default (Default (def)) -import Data.Proxy (Proxy (..)) -import Data.Scientific -import Data.String (IsString (fromString)) -import Data.Text (Text, pack) -import Discord.Interactions -import Discord.Types -import GHC.OldList (find) -import GHC.TypeLits (KnownSymbol, Symbol, symbolVal) -import Tablebot.Internal.Handler.Command (parseValue) -import Tablebot.Utility.Discord (getChannel, interactionResponseComponentsUpdateMessage, interactionResponseCustomMessage, sendCustomMessage) -import Tablebot.Utility.Exception (BotException (InteractionException, ParserException), catchBot, embedError, throwBot) -import Tablebot.Utility.Parser -import Tablebot.Utility.Types -import Text.Megaparsec (MonadParsec (eof, try), chunk, many, observing, optional, (), (<|>)) - --- | The type class representing some data we can extract data from. --- Needed for things like getting a GuildMember, message id, guild id. --- --- Only defined for Message and Interaction. -class Context a where - contextUserId :: a -> UserId - contextGuildId :: a -> EnvDatabaseDiscord s (Maybe GuildId) - contextMember :: a -> Maybe GuildMember - contextMessageId :: a -> Maybe MessageId - -instance Context Message where - contextUserId = userId . messageAuthor - contextGuildId m = case messageGuildId m of - Just a -> pure $ Just a - Nothing -> do - let chanId = messageChannelId m - channel <- getChannel chanId - case fmap channelGuild channel of - Right a -> pure $ Just a - Left _ -> pure Nothing - contextMember = messageMember - contextMessageId = return . messageId - -instance Context Interaction where - -- this is safe to do because we are guaranteed to get either a user or a member - contextUserId i = maybe 0 userId (either memberUser Just mor) - where - (MemberOrUser mor) = interactionUser i - contextGuildId i = return $ interactionGuildId i - contextMember i = case interactionUser i of - (MemberOrUser (Left m)) -> return m - (MemberOrUser (Right _)) -> Nothing - contextMessageId InteractionComponent {interactionMessage = m} = return $ messageId m - contextMessageId InteractionApplicationCommand {applicationCommandData = ApplicationCommandDataMessage {..}} = return applicationCommandDataTargetMessageId - contextMessageId _ = Nothing - --- | Custom infix operator to replace the error of a failing parser (regardless --- of parser position) with a user given error message. --- --- Has some effects on other error parsing. Use if you want the error you give --- to be the one that is reported (unless this is used at a higher level.) --- --- Overwrites/overpowers WithError errors. -() :: Parser a -> String -> Parser a -() p s = do - r <- observing p - case r of - Left _ -> fail s - Right a -> return a - --- | @PComm@ defines function types that we can automatically turn into parsers --- by composing a parser per input of the function provided. --- For example, @Int -> Maybe Text -> Message -> DatabaseDiscord s ()@ builds a --- parser that reads in an @Int@, then some optional @Text@, and then uses --- those to run the provided function with the arguments parsed and the message --- itself. --- --- The arguments to this class are the type of the function, the type of the --- environment, the type of the context (either Message or Interaction), and the --- type of the result of the function (which is either () or MessageDetails --- usually). -class PComm commandty s context returns where - parseComm :: (Context context) => commandty -> Parser (context -> EnvDatabaseDiscord s returns) - --- TODO: verify that all the parsers for PComm actually work - --- If there is the general case where we have just what we want to parse, then --- return it --- (1) -instance {-# OVERLAPPING #-} PComm (t -> EnvDatabaseDiscord s r) s t r where - parseComm comm = skipSpace >> return comm - --- If we have the specific case where we are returning `()`, parse eof as well. --- This should cover the base case for the rest of the program that doesn't use --- more complex stuff. --- (2) -instance {-# OVERLAPPING #-} PComm (t -> EnvDatabaseDiscord s ()) s t () where - parseComm comm = skipSpace >> eof >> return comm - --- If an action takes a message and returns a message details and we want it to --- return unit, assume that it wants to be sent, and send it. eof this as well --- (3) -instance {-# OVERLAPPING #-} PComm (Message -> EnvDatabaseDiscord s MessageDetails) s Message () where - parseComm comm = skipSpace >> eof >> return (\m -> comm m >>= sendCustomMessage m) - --- When there is no context to the function (eg no Message or Interaction), --- just run the action. don't parse eof cause we may wanna return. --- similar to (1) --- (4) -instance PComm (EnvDatabaseDiscord s r) s t r where - parseComm comm = skipSpace >> return (const comm) - --- When there is no context to the function (eg no Message or Interaction), --- just run the action. effectively the function hasn't interacted with the `t`. --- parse eof because we have unit here. similar to (2) --- (5) -instance {-# OVERLAPPING #-} PComm (EnvDatabaseDiscord s ()) s t () where - parseComm comm = skipSpace >> eof >> return (const comm) - --- if we're in a message context and have a message details but want to return --- unit, assume that we want to send it, and send it. similar to (3) --- (6) -instance {-# OVERLAPPING #-} PComm (EnvDatabaseDiscord s MessageDetails) s Message () where - parseComm comm = skipSpace >> eof >> return (\m -> comm >>= sendCustomMessage m) - --- Recursive case is to parse the domain of the function type, then the rest. --- (7) -instance {-# OVERLAPPABLE #-} (CanParse a, PComm as s t r) => PComm (a -> as) s t r where - parseComm comm = do - this <- parsThenMoveToNext @a - parseComm (comm this) - --- if we have two contexts for some reason, collapse them if the resultant can --- be parsed --- (8) -instance {-# OVERLAPPABLE #-} (PComm (t -> as) s t r) => PComm (t -> t -> as) s t r where - parseComm comm = parseComm (\m -> comm m m) - --- if we have a context and then some parseable value, effectively juggle the --- context so that parsing continues (and the context is passed on) --- (9) -instance {-# OVERLAPPABLE #-} (Context t, CanParse a, PComm (t -> as) s t r) => PComm (t -> a -> as) s t r where - parseComm comm = do - this <- parsThenMoveToNext @a - parseComm (`comm` this) - --- special value case - if we get UserId, we need to get the value from --- the context. so, get the value from the context, and then continue parsing. --- (10) -instance {-# OVERLAPPABLE #-} (PComm (t -> as) s t r) => PComm (UserId -> as) s t r where - parseComm comm = parseComm $ \(m :: t) -> comm (contextUserId m) - --- | @CanParse@ defines types from which we can generate parsers. -class CanParse a where - pars :: Parser a - parsThenMoveToNext :: Parser a - parsThenMoveToNext = pars <* (eof <|> skipSpace1) - --- Note: since FromString and (Read, Integral) can overlap, we cannot specify --- this instance as FromString a => CanParse a. -instance CanParse Text where - pars = pack <$> word - --- This overlaps CanParse [a], since String = [Char]. -instance {-# OVERLAPPING #-} CanParse String where - pars = word - --- | @Quoted a@ defines an input of type @a@ that is contained within quotes. -newtype Quoted a = Qu {quote :: a} deriving (Show) - -instance IsString a => CanParse (Quoted a) where - pars = Qu . fromString <$> quoted - -instance (ParseShow a) => ParseShow (Quoted a) where - parseShow (Qu a) = "\"" <> parseShow a <> "\"" - --- A parser for @Maybe a@ attempts to parse @a@, returning @Just x@ if --- correctly parsed, else @Nothing@. -instance CanParse a => CanParse (Maybe a) where - pars = optional $ try (pars @a) - - -- Note: we override @parsThenMoveToNext@: - -- there will be no spaces to parse if the argument isn't present. - parsThenMoveToNext = - pars >>= \case - Nothing -> return Nothing - Just val -> Just val <$ (eof <|> skipSpace1) - --- A parser for @[a]@ parses any number of @a@s. -instance {-# OVERLAPPABLE #-} CanParse a => CanParse [a] where - pars = many pars - --- A parser for @Either a b@ attempts to parse @a@, and if that fails then --- attempts to parse @b@. -instance (CanParse a, CanParse b) => CanParse (Either a b) where - pars = (Left <$> try (pars @a)) <|> (Right <$> pars @b) - --- TODO: automate creation of tuple instances using TemplateHaskell -instance (CanParse a, CanParse b) => CanParse (a, b) where - pars = do - x <- parsThenMoveToNext @a - y <- pars @b - return (x, y) - -instance (CanParse a, CanParse b, CanParse c) => CanParse (a, b, c) where - pars = do - x <- parsThenMoveToNext @a - y <- parsThenMoveToNext @b - z <- pars @c - return (x, y, z) - -instance (CanParse a, CanParse b, CanParse c, CanParse d) => CanParse (a, b, c, d) where - pars = do - x <- parsThenMoveToNext @a - y <- parsThenMoveToNext @b - z <- parsThenMoveToNext @c - w <- pars @d - return (x, y, z, w) - -instance (CanParse a, CanParse b, CanParse c, CanParse d, CanParse e) => CanParse (a, b, c, d, e) where - pars = do - x <- parsThenMoveToNext @a - y <- parsThenMoveToNext @b - z <- parsThenMoveToNext @c - w <- parsThenMoveToNext @d - v <- pars @e - return (x, y, z, w, v) - --- | @Exactly s@ defines an input exactly matching @s@ and nothing else. -data Exactly (s :: Symbol) = Ex - -instance KnownSymbol s => CanParse (Exactly s) where - pars = chunk (pack $ symbolVal (Proxy :: Proxy s)) >> return Ex - --- | @WithError err x@ parses an @x@, reporting @err@ if the parsing of @x@ --- fails. -newtype WithError (err :: Symbol) x = WErr x - -instance (KnownSymbol err, CanParse x) => CanParse (WithError err x) where - pars = (WErr <$> try (pars @x)) symbolVal (Proxy :: Proxy err) - --- | Parsing implementation for all integral types --- Overlappable due to the really flexible head state -instance {-# OVERLAPPABLE #-} (Integral a, Read a) => CanParse a where - pars = integer - -instance CanParse Double where - pars = double - -instance CanParse () where - pars = eof - -instance CanParse Snowflake where - pars = Snowflake . fromInteger <$> posInteger - --- | @RestOfInput a@ parses the rest of the input, giving a value of type @a@. -newtype RestOfInput a = ROI {unROI :: a} - -instance IsString a => CanParse (RestOfInput a) where - pars = ROI . fromString <$> untilEnd - --- | @RestOfInput a@ parses the rest of the input, giving a value of type @a@. -newtype RestOfInput1 a = ROI1 a - -instance IsString a => CanParse (RestOfInput1 a) where - pars = ROI1 . fromString <$> untilEnd1 - --- | Labelled value for use with smart commands. --- --- This is for use with slash commands, where there is a name and description --- required. -newtype Labelled (name :: Symbol) (desc :: Symbol) a = Labelled {unLabel :: a} - --- | Easily make a labelled value. -labelValue :: forall n d a. a -> Labelled n d a -labelValue = Labelled @n @d - --- | Get the name and description of a labelled value. -getLabelValues :: forall n d a. (KnownSymbol n, KnownSymbol d) => Proxy (Labelled n d a) -> (Text, Text) -getLabelValues _ = (pack (symbolVal (Proxy :: Proxy n)), pack (symbolVal (Proxy :: Proxy d))) - --- | Parse a labelled value, by parsing the base value and adding the label --- values. -instance (CanParse a) => CanParse (Labelled n d a) where - pars = labelValue <$> pars - --- | @noArguments@ is a type-specific alias for @parseComm@ for commands that --- have no arguments (thus making it extremely clear). -noArguments :: (Message -> EnvDatabaseDiscord d ()) -> Parser (Message -> EnvDatabaseDiscord d ()) -noArguments = parseComm - --------------------------------------------------------------------------------- --- Interactions stuff ----- - --- | Creates both the slash command creation data structure and the parser for --- the command, and creates the EnvApplicationCommandRecv for the command by --- combining them. --- --- Takes the name and description for a slash command, and its function. -makeApplicationCommandPair :: forall t s. (MakeAppComm t, ProcessAppComm t s) => Text -> Text -> t -> Maybe (EnvApplicationCommandRecv s) -makeApplicationCommandPair name desc f = do - cac <- makeSlashCommand name desc (Proxy :: Proxy t) - return $ ApplicationCommandRecv cac (processAppComm f) - --- | Make the creation data structure for a slash command when given a proxy for --- a function's type. -makeSlashCommand :: (MakeAppComm t) => Text -> Text -> Proxy t -> Maybe CreateApplicationCommand -makeSlashCommand name desc p = - createChatInput name desc >>= \cac -> - return $ - cac - { createOptions = Just $ OptionsValues $ makeAppComm p - } - --- | Create a series of command option values from the given types. --- --- This is making the arguments for a text input/slash command from --- a proxy of the given function. -class MakeAppComm commandty where - makeAppComm :: Proxy commandty -> [OptionValue] - --- As a base case, no more arguments -instance {-# OVERLAPPING #-} MakeAppComm (EnvDatabaseDiscord s MessageDetails) where - makeAppComm _ = [] - --- If there is a way to get an argument from a `ty`, then get that arg and continue recursion. -instance {-# OVERLAPPABLE #-} (MakeAppComm mac, MakeAppCommArg ty) => MakeAppComm (ty -> mac) where - makeAppComm _ = makeAppCommArg (Proxy :: Proxy ty) : makeAppComm (Proxy :: Proxy mac) - --- we don't get the user id from the command itself, so ignore it -instance {-# OVERLAPPABLE #-} (MakeAppComm mac) => MakeAppComm (UserId -> mac) where - makeAppComm _ = makeAppComm (Proxy :: Proxy mac) - --- | From a single value, make an argument for a slash command command. -class MakeAppCommArg commandty where - makeAppCommArg :: Proxy commandty -> OptionValue - --- Create a labelled text argument. By default it is required and does not --- have autocompeletion. -instance (KnownSymbol name, KnownSymbol desc) => MakeAppCommArg (Labelled name desc Text) where - makeAppCommArg l = OptionValueString n d True (Left False) - where - (n, d) = getLabelValues l - --- Create a labelled argument that is optional. -instance (KnownSymbol name, KnownSymbol desc, MakeAppCommArg (Labelled name desc t)) => MakeAppCommArg (Labelled name desc (Maybe t)) where - makeAppCommArg _ = - (makeAppCommArg (Proxy :: Proxy (Labelled name desc t))) - { optionValueRequired = False - } - --- When quoted text is required, just fake it and get a sub layer. -instance (KnownSymbol name, KnownSymbol desc, MakeAppCommArg (Labelled name desc t)) => MakeAppCommArg (Labelled name desc (Quoted t)) where - makeAppCommArg _ = makeAppCommArg (Proxy :: Proxy (Labelled name desc t)) - --- As a base case, send the message produced - --- | Process an application command when given a function/value. --- --- `s` is the context of the environment. -class ProcessAppComm commandty s where - processAppComm :: commandty -> Interaction -> EnvDatabaseDiscord s () - --- When left with just a MessageDetails, just send the message as an --- interaction response. -instance {-# OVERLAPPING #-} ProcessAppComm (EnvDatabaseDiscord s MessageDetails) s where - processAppComm comm i = comm >>= interactionResponseCustomMessage i - --- If there is already an interaction in this function call, apply it and --- recurse. -instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s) => ProcessAppComm (Interaction -> pac) s where - processAppComm comm i = processAppComm (comm i) i - --- This is the main recursion case. --- --- If the argument is a ProcessAppCommArg, then parse it and recurse. -instance {-# OVERLAPPABLE #-} (ProcessAppCommArg ty s, ProcessAppComm pac s) => ProcessAppComm (ty -> pac) s where - processAppComm comm i@InteractionApplicationCommand {applicationCommandData = ApplicationCommandDataChatInput {optionsData = opts}} = do - t <- processAppCommArg (getVs opts) - processAppComm (comm t) i - where - getVs (Just (OptionsDataValues vs)) = vs - getVs _ = [] - processAppComm _ _ = throwBot $ InteractionException "could not process args to application command" - --- one specific implementation case when we want to parse out a user id. -instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s) => ProcessAppComm (UserId -> pac) s where - processAppComm comm i@InteractionApplicationCommand {interactionUser = MemberOrUser u} = - case getUser of - Nothing -> throwBot $ InteractionException "could not process args to application command" - Just uid -> processAppComm (comm uid) i - where - getUser = userId <$> either memberUser Just u - processAppComm _ _ = throwBot $ InteractionException "could not process args to application command" - --- | Process an argument for an application command. --- --- Given a type `t`, parse a value of that type from the given list of option --- values. -class ProcessAppCommArg t s where - processAppCommArg :: [OptionDataValue] -> EnvDatabaseDiscord s t - --- | Given a string, find the first option value with that name in the list, --- returning Nothing if none is found. -getValue :: String -> [OptionDataValue] -> Maybe OptionDataValue -getValue t = find ((== pack t) . optionDataValueName) - --- | Tries to extract an integer from a given option value. -integerFromOptionValue :: OptionDataValue -> Maybe Integer -integerFromOptionValue OptionDataValueInteger {optionDataValueInteger = Right i} = Just i -integerFromOptionValue _ = Nothing - --- | Tries to extract a scientific number from a given option value. -scientificFromOptionValue :: OptionDataValue -> Maybe Scientific -scientificFromOptionValue OptionDataValueNumber {optionDataValueNumber = Right i} = Just i -scientificFromOptionValue _ = Nothing - --- | Tries to extract a string from a given option value. -stringFromOptionValue :: OptionDataValue -> Maybe Text -stringFromOptionValue OptionDataValueString {optionDataValueString = Right i} = Just i -stringFromOptionValue _ = Nothing - --- there are a number of missing slash command argument types missing here, which I've not added yet. --- we can add ids of various sorts - --- extract a string of the given type from the arguments -instance (KnownSymbol name) => ProcessAppCommArg (Labelled name desc Text) s where - processAppCommArg is = case getValue (symbolVal (Proxy :: Proxy name)) is of - Just (OptionDataValueString _ (Right t)) -> return $ labelValue t - _ -> throwBot $ InteractionException "could not find required parameter" - --- extract an integer of the given type from the arguments -instance (KnownSymbol name) => ProcessAppCommArg (Labelled name desc Integer) s where - processAppCommArg is = case getValue (symbolVal (Proxy :: Proxy name)) is of - Just (OptionDataValueInteger _ (Right i)) -> return $ labelValue i - _ -> throwBot $ InteractionException "could not find required parameter" - --- extract a scientific number of the given type from the arguments -instance (KnownSymbol name) => ProcessAppCommArg (Labelled name desc Scientific) s where - processAppCommArg is = case getValue (symbolVal (Proxy :: Proxy name)) is of - Just (OptionDataValueNumber _ (Right i)) -> return $ labelValue i - _ -> throwBot $ InteractionException "could not find required parameter" - --- extract a quote of the given type from the arguments -instance (KnownSymbol name, KnownSymbol desc, ProcessAppCommArg (Labelled name desc t) s) => ProcessAppCommArg (Labelled name desc (Quoted t)) s where - processAppCommArg is = processAppCommArg @(Labelled name desc t) is >>= \(Labelled a) -> return (labelValue (Qu a)) - --- extract an optional data type from the arguments -instance (KnownSymbol name, ProcessAppCommArg (Labelled name desc t) s) => ProcessAppCommArg (Labelled name desc (Maybe t)) s where - processAppCommArg is = do - let result = processAppCommArg is :: EnvDatabaseDiscord s (Labelled name desc t) - ( do - (Labelled l) <- result - return (labelValue (Just l)) - ) - `catchBot` const (return $ labelValue Nothing) - --- | Given a function that can be processed to create a parser, create an action --- for it using the helper. Uses `parseComm` to generate the required parser. --- --- Components use a unique string as their identifier. We can use this to --- run the normal command parser on, hence the use of PComm. --- --- If the boolean is False, a reply is sent to the interaction message. If the --- boolean is True, the original message is updated. --- --- For more information, check the helper `processComponentInteraction'`. -processComponentInteraction :: (PComm f s Interaction MessageDetails) => f -> Bool -> Interaction -> EnvDatabaseDiscord s () -processComponentInteraction f = processComponentInteraction' (parseComm f) - --- | Given a parser that, when run, returns a function taking an interaction --- and returns a database action on some MessageDetails, run the action. --- --- If the boolean is true, the message the component is from is updated. Else, --- a message is sent as the interaction response. --- --- The format of the Text being given should be of space separated values, --- similar to the command structure. -processComponentInteraction' :: Parser (Interaction -> EnvDatabaseDiscord s MessageDetails) -> Bool -> Interaction -> EnvDatabaseDiscord s () -processComponentInteraction' compParser updateOriginal i@InteractionComponent {componentData = idc} = errorCatch $ do - let componentSend - | updateOriginal = interactionResponseComponentsUpdateMessage i - | otherwise = interactionResponseCustomMessage i - action <- parseValue (skipSpace *> compParser) (componentDataCustomId idc) >>= ($ i) - componentSend action - where - catchParserException e@(ParserException _ _) = interactionResponseCustomMessage i $ (messageDetailsBasic "something (likely) went wrong when processing a component interaction") {messageDetailsEmbeds = Just [embedError (e :: BotException)]} - catchParserException e = interactionResponseCustomMessage i $ (messageDetailsBasic "") {messageDetailsEmbeds = Just [embedError (e :: BotException)]} - errorCatch = (`catch` catchParserException) -processComponentInteraction' _ _ _ = throwBot $ InteractionException "could not process component interaction" - --- | Function to only allow use of an interaction if the requestor matches --- a Snowflake at the beginning of the input. This uses a helper, and by default --- sends an ephermeral message with the text "You don't have permission to use --- this component." --- --- Helper is `onlyAllowRequestor'`. -onlyAllowRequestor :: forall f. (PComm f () Interaction MessageDetails) => f -> Parser (Interaction -> DatabaseDiscord MessageDetails) -onlyAllowRequestor = - onlyAllowRequestor' - ( (messageDetailsBasic "You don't have permission to use this component.") {messageDetailsFlags = Just $ InteractionResponseMessageFlags [InteractionResponseMessageFlagEphermeral]} - ) - --- | Take a message to send when a user that is not the one that created a --- component, and then parse out a user id, and then get the interaction --- requestor's userid, check if they match, and if they don't then send a --- message. Regardless, parse out the given function. If it _does_ match, run --- the parsed function. --- --- Adds eof to the end to ensure all the data is parsed. -onlyAllowRequestor' :: forall f. (PComm f () Interaction MessageDetails) => MessageDetails -> f -> Parser (Interaction -> DatabaseDiscord MessageDetails) -onlyAllowRequestor' msg f = do - pre <- parseComm prefunc - f' <- parseComm @f @() @Interaction @MessageDetails f - parseComm - ( \i -> do - isEqual <- pre i - case isEqual of - Nothing -> f' i - Just d -> return d - ) - <* eof - where - prefunc :: UserId -> UserId -> Interaction -> DatabaseDiscord (Maybe MessageDetails) - prefunc uid u i = - if uid == u - then return Nothing - else - interactionResponseCustomMessage - i - msg - >> return (Just def) +module Tablebot.Utility.SmartParser + ( module Tablebot.Utility.SmartParser.SmartParser, + module Tablebot.Utility.SmartParser.Interactions, + module Tablebot.Utility.SmartParser.Types, + ) +where + +import Tablebot.Utility.SmartParser.Interactions +import Tablebot.Utility.SmartParser.SmartParser +import Tablebot.Utility.SmartParser.Types diff --git a/src/Tablebot/Utility/SmartParser/Interactions.hs b/src/Tablebot/Utility/SmartParser/Interactions.hs new file mode 100644 index 00000000..72ea2b08 --- /dev/null +++ b/src/Tablebot/Utility/SmartParser/Interactions.hs @@ -0,0 +1,286 @@ +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +-- | +-- Module : Tablebot.Utility.Interactions +-- Description : Automatic parser generation from function types. +-- License : MIT +-- Maintainer : tagarople@gmail.com +-- Stability : experimental +-- Portability : POSIX +-- +-- Generates a parser based on the shape of the command function. +-- For example, if you have a command that takes in an Int as argument, we +-- build a parser that reads in that Int and then runs the command. +module Tablebot.Utility.SmartParser.Interactions where + +import Control.Monad.Exception (MonadException (catch)) +import Data.Default (Default (def)) +import Data.Proxy (Proxy (..)) +import Data.Scientific +import Data.Text (Text, pack) +import Discord.Interactions +import Discord.Types +import GHC.OldList (find) +import GHC.TypeLits (KnownSymbol, symbolVal) +import Tablebot.Internal.Handler.Command (parseValue) +import Tablebot.Utility.Discord (interactionResponseComponentsUpdateMessage, interactionResponseCustomMessage) +import Tablebot.Utility.Exception (BotException (InteractionException, ParserException), catchBot, embedError, throwBot) +import Tablebot.Utility.Parser +import Tablebot.Utility.SmartParser.SmartParser (PComm (..)) +import Tablebot.Utility.SmartParser.Types +import Tablebot.Utility.Types +import Text.Megaparsec (MonadParsec (eof)) + +-- | Creates both the slash command creation data structure and the parser for +-- the command, and creates the EnvApplicationCommandRecv for the command by +-- combining them. +-- +-- Takes the name and description for a slash command, and its function. +makeApplicationCommandPair :: forall t s. (MakeAppComm t, ProcessAppComm t s) => Text -> Text -> t -> Maybe (EnvApplicationCommandRecv s) +makeApplicationCommandPair name desc f = do + cac <- makeSlashCommand name desc (Proxy :: Proxy t) + return $ ApplicationCommandRecv cac (processAppComm f) + +-- | Make the creation data structure for a slash command when given a proxy for +-- a function's type. +makeSlashCommand :: (MakeAppComm t) => Text -> Text -> Proxy t -> Maybe CreateApplicationCommand +makeSlashCommand name desc p = + createChatInput name desc >>= \cac -> + return $ + cac + { createOptions = Just $ OptionsValues $ makeAppComm p + } + +-- | Create a series of command option values from the given types. +-- +-- This is making the arguments for a text input/slash command from +-- a proxy of the given function. +class MakeAppComm commandty where + makeAppComm :: Proxy commandty -> [OptionValue] + +-- As a base case, no more arguments +instance {-# OVERLAPPING #-} MakeAppComm (EnvDatabaseDiscord s MessageDetails) where + makeAppComm _ = [] + +-- If there is a way to get an argument from a `ty`, then get that arg and continue recursion. +instance {-# OVERLAPPABLE #-} (MakeAppComm mac, MakeAppCommArg ty) => MakeAppComm (ty -> mac) where + makeAppComm _ = makeAppCommArg (Proxy :: Proxy ty) : makeAppComm (Proxy :: Proxy mac) + +-- we don't get the sender user id from the command itself, so ignore it +instance {-# OVERLAPPABLE #-} (MakeAppComm mac) => MakeAppComm (SenderUserId -> mac) where + makeAppComm _ = makeAppComm (Proxy :: Proxy mac) + +-- | From a single value, make an argument for a slash command command. +class MakeAppCommArg commandty where + makeAppCommArg :: Proxy commandty -> OptionValue + +-- | Create a labelled text argument. By default it is required and does not +-- have autocompeletion. +instance (KnownSymbol name, KnownSymbol desc) => MakeAppCommArg (Labelled name desc Text) where + makeAppCommArg l = OptionValueString n d True (Left False) + where + (n, d) = getLabelValues l + +-- | Create a labelled integer argument. By default it is required and does not +-- have autocompeletion, and does not have bounds. +instance (KnownSymbol name, KnownSymbol desc) => MakeAppCommArg (Labelled name desc Integer) where + makeAppCommArg l = OptionValueInteger n d True (Left False) Nothing Nothing + where + (n, d) = getLabelValues l + +-- | Create a labelled scientific argument. By default it is required and does not +-- have autocompeletion, and does not have bounds. +instance (KnownSymbol name, KnownSymbol desc) => MakeAppCommArg (Labelled name desc Scientific) where + makeAppCommArg l = OptionValueNumber n d True (Left False) Nothing Nothing + where + (n, d) = getLabelValues l + +-- | Create a labelled argument that is optional. +instance (KnownSymbol name, KnownSymbol desc, MakeAppCommArg (Labelled name desc t)) => MakeAppCommArg (Labelled name desc (Maybe t)) where + makeAppCommArg _ = + (makeAppCommArg (Proxy :: Proxy (Labelled name desc t))) + { optionValueRequired = False + } + +-- | When quoted text is required, just fake it and get a sub layer. +instance (KnownSymbol name, KnownSymbol desc, MakeAppCommArg (Labelled name desc t)) => MakeAppCommArg (Labelled name desc (Quoted t)) where + makeAppCommArg _ = makeAppCommArg (Proxy :: Proxy (Labelled name desc t)) + +-- As a base case, send the message produced + +-- | Process an application command when given a function/value. +-- +-- `s` is the context of the environment. +class ProcessAppComm commandty s where + processAppComm :: commandty -> Interaction -> EnvDatabaseDiscord s () + +-- When left with just a MessageDetails, just send the message as an +-- interaction response. +instance {-# OVERLAPPING #-} ProcessAppComm (EnvDatabaseDiscord s MessageDetails) s where + processAppComm comm i = comm >>= interactionResponseCustomMessage i + +-- If there is already an interaction in this function call, apply it and +-- recurse. +instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s) => ProcessAppComm (Interaction -> pac) s where + processAppComm comm i = processAppComm (comm i) i + +-- This is the main recursion case. +-- +-- If the argument is a ProcessAppCommArg, then parse it and recurse. +instance {-# OVERLAPPABLE #-} (ProcessAppCommArg ty s, ProcessAppComm pac s) => ProcessAppComm (ty -> pac) s where + processAppComm comm i@InteractionApplicationCommand {applicationCommandData = ApplicationCommandDataChatInput {optionsData = opts}} = do + t <- processAppCommArg (getVs opts) + processAppComm (comm t) i + where + getVs (Just (OptionsDataValues vs)) = vs + getVs _ = [] + processAppComm _ _ = throwBot $ InteractionException "could not process args to application command" + +-- one specific implementation case when we want to parse out a user id. +instance {-# OVERLAPPABLE #-} (ProcessAppComm pac s) => ProcessAppComm (SenderUserId -> pac) s where + processAppComm comm i@InteractionApplicationCommand {interactionUser = MemberOrUser u} = + case getUser of + Nothing -> throwBot $ InteractionException "could not process args to application command" + Just uid -> processAppComm (comm (SenderUserId uid)) i + where + getUser = userId <$> either memberUser Just u + processAppComm _ _ = throwBot $ InteractionException "could not process args to application command" + +-- | Process an argument for an application command. +-- +-- Given a type `t`, parse a value of that type from the given list of option +-- values. +class ProcessAppCommArg t s where + processAppCommArg :: [OptionDataValue] -> EnvDatabaseDiscord s t + +-- | Given a string, find the first option value with that name in the list, +-- returning Nothing if none is found. +getValue :: String -> [OptionDataValue] -> Maybe OptionDataValue +getValue t = find ((== pack t) . optionDataValueName) + +-- | Tries to extract an integer from a given option value. +integerFromOptionValue :: OptionDataValue -> Maybe Integer +integerFromOptionValue OptionDataValueInteger {optionDataValueInteger = Right i} = Just i +integerFromOptionValue _ = Nothing + +-- | Tries to extract a scientific number from a given option value. +scientificFromOptionValue :: OptionDataValue -> Maybe Scientific +scientificFromOptionValue OptionDataValueNumber {optionDataValueNumber = Right i} = Just i +scientificFromOptionValue _ = Nothing + +-- | Tries to extract a string from a given option value. +stringFromOptionValue :: OptionDataValue -> Maybe Text +stringFromOptionValue OptionDataValueString {optionDataValueString = Right i} = Just i +stringFromOptionValue _ = Nothing + +-- there are a number of missing slash command argument types missing here, which I've not added yet. +-- we can add ids of various sorts + +-- extract a string of the given type from the arguments +instance (KnownSymbol name) => ProcessAppCommArg (Labelled name desc Text) s where + processAppCommArg is = case getValue (symbolVal (Proxy :: Proxy name)) is of + Just (OptionDataValueString _ (Right t)) -> return $ labelValue t + _ -> throwBot $ InteractionException "could not find required parameter" + +-- extract an integer of the given type from the arguments +instance (KnownSymbol name) => ProcessAppCommArg (Labelled name desc Integer) s where + processAppCommArg is = case getValue (symbolVal (Proxy :: Proxy name)) is of + Just (OptionDataValueInteger _ (Right i)) -> return $ labelValue i + _ -> throwBot $ InteractionException "could not find required parameter" + +-- extract a scientific number of the given type from the arguments +instance (KnownSymbol name) => ProcessAppCommArg (Labelled name desc Scientific) s where + processAppCommArg is = case getValue (symbolVal (Proxy :: Proxy name)) is of + Just (OptionDataValueNumber _ (Right i)) -> return $ labelValue i + _ -> throwBot $ InteractionException "could not find required parameter" + +-- extract a quote of the given type from the arguments +instance (KnownSymbol name, KnownSymbol desc, ProcessAppCommArg (Labelled name desc t) s) => ProcessAppCommArg (Labelled name desc (Quoted t)) s where + processAppCommArg is = processAppCommArg @(Labelled name desc t) is >>= \(Labelled a) -> return (labelValue (Qu a)) + +-- extract an optional data type from the arguments +instance (KnownSymbol name, ProcessAppCommArg (Labelled name desc t) s) => ProcessAppCommArg (Labelled name desc (Maybe t)) s where + processAppCommArg is = do + let result = processAppCommArg is :: EnvDatabaseDiscord s (Labelled name desc t) + ( do + (Labelled l) <- result + return (labelValue (Just l)) + ) + `catchBot` const (return $ labelValue Nothing) + +-- | Given a function that can be processed to create a parser, create an action +-- for it using the helper. Uses `parseComm` to generate the required parser. +-- +-- Components use a unique string as their identifier. We can use this to +-- run the normal command parser on, hence the use of PComm. +-- +-- If the boolean is False, a reply is sent to the interaction message. If the +-- boolean is True, the original message is updated. +-- +-- For more information, check the helper `processComponentInteraction'`. +processComponentInteraction :: (PComm f s Interaction MessageDetails) => f -> Bool -> Interaction -> EnvDatabaseDiscord s () +processComponentInteraction f = processComponentInteraction' (parseComm f) + +-- | Given a parser that, when run, returns a function taking an interaction +-- and returns a database action on some MessageDetails, run the action. +-- +-- If the boolean is true, the message the component is from is updated. Else, +-- a message is sent as the interaction response. +-- +-- The format of the Text being given should be of space separated values, +-- similar to the command structure. +processComponentInteraction' :: Parser (Interaction -> EnvDatabaseDiscord s MessageDetails) -> Bool -> Interaction -> EnvDatabaseDiscord s () +processComponentInteraction' compParser updateOriginal i@InteractionComponent {componentData = idc} = errorCatch $ do + let componentSend + | updateOriginal = interactionResponseComponentsUpdateMessage i + | otherwise = interactionResponseCustomMessage i + action <- parseValue (skipSpace *> compParser) (componentDataCustomId idc) >>= ($ i) + componentSend action + where + catchParserException e@(ParserException _ _) = interactionResponseCustomMessage i $ (messageDetailsBasic "something (likely) went wrong when processing a component interaction") {messageDetailsEmbeds = Just [embedError (e :: BotException)]} + catchParserException e = interactionResponseCustomMessage i $ (messageDetailsBasic "") {messageDetailsEmbeds = Just [embedError (e :: BotException)]} + errorCatch = (`catch` catchParserException) +processComponentInteraction' _ _ _ = throwBot $ InteractionException "could not process component interaction" + +-- | Function to only allow use of an interaction if the requestor matches +-- a Snowflake at the beginning of the input. This uses a helper, and by default +-- sends an ephermeral message with the text "You don't have permission to use +-- this component." +-- +-- Helper is `onlyAllowRequestor'`. +onlyAllowRequestor :: forall f. (PComm f () Interaction MessageDetails) => f -> Parser (Interaction -> DatabaseDiscord MessageDetails) +onlyAllowRequestor = + onlyAllowRequestor' + ( (messageDetailsBasic "You don't have permission to use this component.") {messageDetailsFlags = Just $ InteractionResponseMessageFlags [InteractionResponseMessageFlagEphermeral]} + ) + +-- | Take a message to send when a user that is not the one that created a +-- component, and then parse out a user id, and then get the interaction +-- requestor's userid, check if they match, and if they don't then send a +-- message. Regardless, parse out the given function. If it _does_ match, run +-- the parsed function. +-- +-- Adds eof to the end to ensure all the data is parsed. +onlyAllowRequestor' :: forall f. (PComm f () Interaction MessageDetails) => MessageDetails -> f -> Parser (Interaction -> DatabaseDiscord MessageDetails) +onlyAllowRequestor' msg f = do + pre <- parseComm prefunc + f' <- parseComm @f @() @Interaction @MessageDetails f + parseComm + ( \i -> do + isEqual <- pre i + case isEqual of + Nothing -> f' i + Just d -> return d + ) + <* eof + where + prefunc :: UserId -> SenderUserId -> Interaction -> DatabaseDiscord (Maybe MessageDetails) + prefunc uid (SenderUserId u) i = + if uid == u + then return Nothing + else + interactionResponseCustomMessage + i + msg + >> return (Just def) diff --git a/src/Tablebot/Utility/SmartParser/SmartParser.hs b/src/Tablebot/Utility/SmartParser/SmartParser.hs new file mode 100644 index 00000000..e28b477f --- /dev/null +++ b/src/Tablebot/Utility/SmartParser/SmartParser.hs @@ -0,0 +1,220 @@ +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -Wno-orphans #-} + +-- | +-- Module : Tablebot.Utility.SmartParser.SmartParser +-- Description : Automatic parser generation from function types. +-- License : MIT +-- Maintainer : tagarople@gmail.com +-- Stability : experimental +-- Portability : POSIX +-- +-- Generates a parser based on the shape of the command function. +-- For example, if you have a command that takes in an Int as argument, we +-- build a parser that reads in that Int and then runs the command. +module Tablebot.Utility.SmartParser.SmartParser where + +import Data.Proxy (Proxy (..)) +import Data.Scientific () +import Data.String (IsString (fromString)) +import Data.Text (Text, pack) +import Discord.Interactions () +import Discord.Types (Message, Snowflake (Snowflake)) +import GHC.TypeLits (KnownSymbol, symbolVal) +import Tablebot.Utility.Discord (sendCustomMessage) +import Tablebot.Utility.Parser +import Tablebot.Utility.SmartParser.Types +import Tablebot.Utility.Types (EnvDatabaseDiscord, MessageDetails, Parser) +import Text.Megaparsec (MonadParsec (eof, try), chunk, many, optional, (), (<|>)) + +-- | @PComm@ defines function types that we can automatically turn into parsers +-- by composing a parser per input of the function provided. +-- For example, @Int -> Maybe Text -> Message -> DatabaseDiscord s ()@ builds a +-- parser that reads in an @Int@, then some optional @Text@, and then uses +-- those to run the provided function with the arguments parsed and the message +-- itself. +-- +-- The arguments to this class are the type of the function, the type of the +-- environment, the type of the context (either Message or Interaction), and the +-- type of the result of the function (which is either () or MessageDetails +-- usually). +class PComm commandty s context returns where + parseComm :: (Context context) => commandty -> Parser (context -> EnvDatabaseDiscord s returns) + +-- TODO: verify that all the parsers for PComm actually work + +-- If there is the general case where we have just what we want to parse, then +-- return it +-- (1) +instance {-# OVERLAPPING #-} PComm (t -> EnvDatabaseDiscord s r) s t r where + parseComm comm = skipSpace >> return comm + +-- If we have the specific case where we are returning `()`, parse eof as well. +-- This should cover the base case for the rest of the program that doesn't use +-- more complex stuff. +-- (2) +instance {-# OVERLAPPING #-} PComm (t -> EnvDatabaseDiscord s ()) s t () where + parseComm comm = skipSpace >> eof >> return comm + +-- If an action takes a message and returns a message details and we want it to +-- return unit, assume that it wants to be sent, and send it. eof this as well +-- (3) +instance {-# OVERLAPPING #-} PComm (Message -> EnvDatabaseDiscord s MessageDetails) s Message () where + parseComm comm = skipSpace >> eof >> return (\m -> comm m >>= sendCustomMessage m) + +-- When there is no context to the function (eg no Message or Interaction), +-- just run the action. don't parse eof cause we may wanna return. +-- similar to (1) +-- (4) +instance PComm (EnvDatabaseDiscord s r) s t r where + parseComm comm = skipSpace >> return (const comm) + +-- When there is no context to the function (eg no Message or Interaction), +-- just run the action. effectively the function hasn't interacted with the `t`. +-- parse eof because we have unit here. similar to (2) +-- (5) +instance {-# OVERLAPPING #-} PComm (EnvDatabaseDiscord s ()) s t () where + parseComm comm = skipSpace >> eof >> return (const comm) + +-- if we're in a message context and have a message details but want to return +-- unit, assume that we want to send it, and send it. similar to (3) +-- (6) +instance {-# OVERLAPPING #-} PComm (EnvDatabaseDiscord s MessageDetails) s Message () where + parseComm comm = skipSpace >> eof >> return (\m -> comm >>= sendCustomMessage m) + +-- Recursive case is to parse the domain of the function type, then the rest. +-- (7) +instance {-# OVERLAPPABLE #-} (CanParse a, PComm as s t r) => PComm (a -> as) s t r where + parseComm comm = do + this <- parsThenMoveToNext @a + parseComm (comm this) + +-- if we have two contexts for some reason, collapse them if the resultant can +-- be parsed +-- (8) +instance {-# OVERLAPPABLE #-} (PComm (t -> as) s t r) => PComm (t -> t -> as) s t r where + parseComm comm = parseComm (\m -> comm m m) + +-- if we have a context and then some parseable value, effectively juggle the +-- context so that parsing continues (and the context is passed on) +-- (9) +instance {-# OVERLAPPABLE #-} (Context t, CanParse a, PComm (t -> as) s t r) => PComm (t -> a -> as) s t r where + parseComm comm = do + this <- parsThenMoveToNext @a + parseComm (`comm` this) + +-- special value case - if we get SenderUserId, we need to get the value from +-- the context. so, get the value from the context, and then continue parsing. +-- (10) +instance {-# OVERLAPPABLE #-} (PComm (t -> as) s t r) => PComm (SenderUserId -> as) s t r where + parseComm comm = parseComm $ \(m :: t) -> comm (SenderUserId $ contextUserId m) + +-- | @CanParse@ defines types from which we can generate parsers. +class CanParse a where + pars :: Parser a + parsThenMoveToNext :: Parser a + parsThenMoveToNext = pars <* (eof <|> skipSpace1) + +-- Note: since FromString and (Read, Integral) can overlap, we cannot specify +-- this instance as FromString a => CanParse a. +instance CanParse Text where + pars = pack <$> word + +-- This overlaps CanParse [a], since String = [Char]. +instance {-# OVERLAPPING #-} CanParse String where + pars = word + +instance IsString a => CanParse (Quoted a) where + pars = Qu . fromString <$> quoted + +instance (ParseShow a) => ParseShow (Quoted a) where + parseShow (Qu a) = "\"" <> parseShow a <> "\"" + +-- A parser for @Maybe a@ attempts to parse @a@, returning @Just x@ if +-- correctly parsed, else @Nothing@. +instance CanParse a => CanParse (Maybe a) where + pars = optional $ try (pars @a) + + -- Note: we override @parsThenMoveToNext@: + -- there will be no spaces to parse if the argument isn't present. + parsThenMoveToNext = + pars >>= \case + Nothing -> return Nothing + Just val -> Just val <$ (eof <|> skipSpace1) + +-- A parser for @[a]@ parses any number of @a@s. +instance {-# OVERLAPPABLE #-} CanParse a => CanParse [a] where + pars = many pars + +-- A parser for @Either a b@ attempts to parse @a@, and if that fails then +-- attempts to parse @b@. +instance (CanParse a, CanParse b) => CanParse (Either a b) where + pars = (Left <$> try (pars @a)) <|> (Right <$> pars @b) + +-- TODO: automate creation of tuple instances using TemplateHaskell +instance (CanParse a, CanParse b) => CanParse (a, b) where + pars = do + x <- parsThenMoveToNext @a + y <- pars @b + return (x, y) + +instance (CanParse a, CanParse b, CanParse c) => CanParse (a, b, c) where + pars = do + x <- parsThenMoveToNext @a + y <- parsThenMoveToNext @b + z <- pars @c + return (x, y, z) + +instance (CanParse a, CanParse b, CanParse c, CanParse d) => CanParse (a, b, c, d) where + pars = do + x <- parsThenMoveToNext @a + y <- parsThenMoveToNext @b + z <- parsThenMoveToNext @c + w <- pars @d + return (x, y, z, w) + +instance (CanParse a, CanParse b, CanParse c, CanParse d, CanParse e) => CanParse (a, b, c, d, e) where + pars = do + x <- parsThenMoveToNext @a + y <- parsThenMoveToNext @b + z <- parsThenMoveToNext @c + w <- parsThenMoveToNext @d + v <- pars @e + return (x, y, z, w, v) + +instance KnownSymbol s => CanParse (Exactly s) where + pars = chunk (pack $ symbolVal (Proxy :: Proxy s)) >> return Ex + +instance (KnownSymbol err, CanParse x) => CanParse (WithError err x) where + pars = (WErr <$> try (pars @x)) symbolVal (Proxy :: Proxy err) + +-- | Parsing implementation for all integral types +-- Overlappable due to the really flexible head state +instance {-# OVERLAPPABLE #-} (Integral a, Read a) => CanParse a where + pars = integer + +instance CanParse Double where + pars = double + +instance CanParse () where + pars = eof + +instance CanParse Snowflake where + pars = Snowflake . fromInteger <$> posInteger + +instance IsString a => CanParse (RestOfInput a) where + pars = ROI . fromString <$> untilEnd + +instance IsString a => CanParse (RestOfInput1 a) where + pars = ROI1 . fromString <$> untilEnd1 + +-- | Parse a labelled value, by parsing the base value and adding the label +-- values. +instance (CanParse a) => CanParse (Labelled n d a) where + pars = labelValue <$> pars + +-- | @noArguments@ is a type-specific alias for @parseComm@ for commands that +-- have no arguments (thus making it extremely clear). +noArguments :: (Message -> EnvDatabaseDiscord d ()) -> Parser (Message -> EnvDatabaseDiscord d ()) +noArguments = parseComm diff --git a/src/Tablebot/Utility/SmartParser/Types.hs b/src/Tablebot/Utility/SmartParser/Types.hs new file mode 100644 index 00000000..f399d3ac --- /dev/null +++ b/src/Tablebot/Utility/SmartParser/Types.hs @@ -0,0 +1,102 @@ +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +-- | +-- Module : Tablebot.Utility.SmartParser.Types +-- Description : Some of the types or typeclasses for smart parsers. +-- License : MIT +-- Maintainer : tagarople@gmail.com +-- Stability : experimental +-- Portability : POSIX +module Tablebot.Utility.SmartParser.Types where + +import Data.Proxy (Proxy (..)) +import Data.Text (Text, pack) +import Discord.Interactions +import Discord.Types +import GHC.TypeLits (KnownSymbol, Symbol, symbolVal) +import Tablebot.Utility.Discord (getChannel) +import Tablebot.Utility.Types +import Text.Megaparsec (observing) + +-- | The type class representing some data we can extract data from. +-- Needed for things like getting a GuildMember, message id, guild id. +-- +-- Only defined for Message and Interaction. +class Context a where + contextUserId :: a -> UserId + contextGuildId :: a -> EnvDatabaseDiscord s (Maybe GuildId) + contextMember :: a -> Maybe GuildMember + contextMessageId :: a -> Maybe MessageId + +newtype SenderUserId = SenderUserId UserId deriving (Show, Eq) + +instance Context Message where + contextUserId = userId . messageAuthor + contextGuildId m = case messageGuildId m of + Just a -> pure $ Just a + Nothing -> do + let chanId = messageChannelId m + channel <- getChannel chanId + case fmap channelGuild channel of + Right a -> pure $ Just a + Left _ -> pure Nothing + contextMember = messageMember + contextMessageId = return . messageId + +instance Context Interaction where + -- this is safe to do because we are guaranteed to get either a user or a member + contextUserId i = maybe 0 userId (either memberUser Just mor) + where + (MemberOrUser mor) = interactionUser i + contextGuildId i = return $ interactionGuildId i + contextMember i = case interactionUser i of + (MemberOrUser (Left m)) -> return m + (MemberOrUser (Right _)) -> Nothing + contextMessageId InteractionComponent {interactionMessage = m} = return $ messageId m + contextMessageId InteractionApplicationCommand {applicationCommandData = ApplicationCommandDataMessage {..}} = return applicationCommandDataTargetMessageId + contextMessageId _ = Nothing + +-- | Custom infix operator to replace the error of a failing parser (regardless +-- of parser position) with a user given error message. +-- +-- Has some effects on other error parsing. Use if you want the error you give +-- to be the one that is reported (unless this is used at a higher level.) +-- +-- Overwrites/overpowers WithError errors. +() :: Parser a -> String -> Parser a +() p s = do + r <- observing p + case r of + Left _ -> fail s + Right a -> return a + +-- | @Quoted a@ defines an input of type @a@ that is contained within quotes. +newtype Quoted a = Qu {quote :: a} deriving (Show) + +-- | @RestOfInput a@ parses the rest of the input, giving a value of type @a@. +newtype RestOfInput a = ROI {unROI :: a} + +-- | @Exactly s@ defines an input exactly matching @s@ and nothing else. +data Exactly (s :: Symbol) = Ex + +-- | @RestOfInput a@ parses the rest of the input, giving a value of type @a@. +newtype RestOfInput1 a = ROI1 a + +-- | @WithError err x@ parses an @x@, reporting @err@ if the parsing of @x@ +-- fails. +newtype WithError (err :: Symbol) x = WErr x + +-- | Labelled value for use with smart commands. +-- +-- This is for use with slash commands, where there is a name and description +-- required. +newtype Labelled (name :: Symbol) (desc :: Symbol) a = Labelled {unLabel :: a} + +-- | Easily make a labelled value. +labelValue :: forall n d a. a -> Labelled n d a +labelValue = Labelled @n @d + +-- | Get the name and description of a labelled value. +getLabelValues :: forall n d a. (KnownSymbol n, KnownSymbol d) => Proxy (Labelled n d a) -> (Text, Text) +getLabelValues _ = (pack (symbolVal (Proxy :: Proxy n)), pack (symbolVal (Proxy :: Proxy d))) diff --git a/src/Tablebot/Utility/Types.hs b/src/Tablebot/Utility/Types.hs index 970acf2b..6eb51b6d 100644 --- a/src/Tablebot/Utility/Types.hs +++ b/src/Tablebot/Utility/Types.hs @@ -323,6 +323,7 @@ data MessageDetails = MessageDetails messageDetailsAttachments :: Maybe [Attachment], messageDetailsStickerIds :: Maybe [StickerId] } + deriving (Show) makeEphermeral :: MessageDetails -> MessageDetails makeEphermeral m = m {messageDetailsFlags = Just $ InteractionResponseMessageFlags [InteractionResponseMessageFlagEphermeral]} diff --git a/stack.yaml b/stack.yaml index 97d38292..619f14bd 100644 --- a/stack.yaml +++ b/stack.yaml @@ -42,7 +42,7 @@ packages: # allow-newer: true extra-deps: -- discord-haskell-1.13.0 +- discord-haskell-1.14.0 - emoji-0.1.0.2 - load-env-0.2.1.0 - megaparsec-9.0.1 diff --git a/tutorials/3.Interactions.md b/tutorials/3.Interactions.md index c8c9a9f9..61aff4ff 100644 --- a/tutorials/3.Interactions.md +++ b/tutorials/3.Interactions.md @@ -181,8 +181,8 @@ As before, we'll start by moving the helper function (`pingDB`) to its own top l Doing this though, we immediately come up on the snag. Even if we aren't sending a message using `m` any more, we are still getting the user id of the user that sent the message. To solve this, we have to perform a special kind of parsing, using the context we have. This changes the first couple lines of `pingDB` to the following. ```haskell -pingDB :: UserId -> DatabaseDiscord MessageDetails -pingDB u = do +pingDB :: SenderUserId -> DatabaseDiscord MessageDetails +pingDB (SenderUserId u) = do let uid = extractFromSnowflake $ unId u ``` @@ -315,8 +315,8 @@ myEchoInteraction = makeApplicationCommandPair "myecho" "echo your input" (echoH myPing'' :: Command myPing'' = Command "cmyping" (parseComm pingDB) [] -pingDB :: UserId -> DatabaseDiscord MessageDetails -pingDB u = do +pingDB :: SenderUserId -> DatabaseDiscord MessageDetails +pingDB (SenderUserId u) = do let uid = extractFromSnowflake $ unId u user <- liftSql $ select $ from $ \p -> do where_ (p ^. PingCountUid ==. val uid) From 02ae70fdcace2e0739665eb037c6413cd0877ffa Mon Sep 17 00:00:00 2001 From: Finnbar Keating Date: Sun, 17 Jul 2022 18:21:52 +0100 Subject: [PATCH 87/96] Reinstate `ping` --- app/Main.hs | 4 ++-- src/Tablebot.hs | 2 +- src/Tablebot/Plugins/Ping.hs | 15 +-------------- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/app/Main.hs b/app/Main.hs index b1ee0e3b..25607066 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -4,11 +4,11 @@ module Main where import Data.Text (Text) import Tablebot (BotConfig (..), runTablebotWithEnv) -import Tablebot.Plugins (allPlugins, minusPl) +import Tablebot.Plugins (allPlugins) -- @main@ runs forever. This allows bot reloading by fully shutting down the bot and letting it restart. main :: IO () -main = runTablebotWithEnv (allPlugins `minusPl` ["ping"]) $ BotConfig {gamePlaying = "with dice", rootHelpText = rootBody} +main = runTablebotWithEnv allPlugins $ BotConfig {gamePlaying = "with dice", rootHelpText = rootBody} rootBody :: Text rootBody = diff --git a/src/Tablebot.hs b/src/Tablebot.hs index 07fec893..d8a54744 100644 --- a/src/Tablebot.hs +++ b/src/Tablebot.hs @@ -63,7 +63,7 @@ runTablebotWithEnv plugins config = do _ <- swapMVar rFlag Reload loadEnv dToken <- pack <$> getEnv "DISCORD_TOKEN" - unless (encodeUtf8 dToken =~ ("^[A-Za-z0-9_-]{24}[.][A-Za-z0-9_-]{6}[.][A-Za-z0-9_-]{27}$" :: String)) $ + unless (encodeUtf8 dToken =~ ("^[A-Za-z0-9_-]{24}[.][A-Za-z0-9_-]{6}[.][A-Za-z0-9_-]{38}$" :: String)) $ die "Invalid token format. Please check it is a bot token" prefix <- pack . fromMaybe "!" <$> lookupEnv "PREFIX" dbpath <- getEnv "SQLITE_FILENAME" diff --git a/src/Tablebot/Plugins/Ping.hs b/src/Tablebot/Plugins/Ping.hs index 5fac4692..247ff4a3 100644 --- a/src/Tablebot/Plugins/Ping.hs +++ b/src/Tablebot/Plugins/Ping.hs @@ -29,26 +29,13 @@ ping = ) [] --- | @pong@ is a command that takes no arguments (using 'noArguments') and --- replies with "ping". It is the younger sibling of @ping@. -pong :: Command -pong = - Command - "pong" - ( parseComm $ echo "ping" - ) - [] - pingHelp :: HelpPage pingHelp = HelpPage "ping" [] "show a debug message" "**Ping**\nShows a debug message\n\n*Usage:* `ping`" [] None -pongHelp :: HelpPage -pongHelp = HelpPage "pong" [] "show a more different debug message" "**Pong**\nShows a different debug message\n\n*Usage:* `pong`" [] None - -- | @pingPlugin@ assembles these commands into a plugin containing both ping -- and pong. pingPlugin :: Plugin -pingPlugin = (plug "ping") {commands = [ping, pong], helpPages = [pingHelp, pongHelp]} +pingPlugin = (plug "ping") {commands = [ping], helpPages = [pingHelp]} pingpong :: CompiledPlugin pingpong = compilePlugin pingPlugin From aa812fe7b2334ed208ef927f02a3bb7d8296ca7a Mon Sep 17 00:00:00 2001 From: Finnbar Keating Date: Sun, 17 Jul 2022 18:23:28 +0100 Subject: [PATCH 88/96] Please don't make big PRs --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9bf7c0a..0bc1c47e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,6 +37,7 @@ Writing good PRs is hard. As such, here are some important points to consider wh * In larger implementations, it may help to talk about the high-level structure of your implementation - e.g. you might have a section of your plugin that deals with parsing messages, another which deals with some specific case, another which deals with the general case and so on. Make it clear how these parts interact. Splitting your implementation into multiple files may help here, with a base file that imports each auxiliary file and puts the results within those files together. * Haskell code has a habit of being extremely abstract, and talking about it in the abstract does not aid understanding. Give concrete examples of how the abstract is used to justify its existence - instead of solely saying "we have a parser that doesn't look at `t`", back it up with an example like "this is used within a Discord interaction, so doesn't necessarily have an associated message". * **Remember the plugin writer as well as the end user.** If you've written something that changes how plugins are written, update existing tutorials or add new ones. Make sure that the API you're defining is clear and easy to use, so doesn't put too much burden on someone writing a plugin. +* **Try to keep your PR small.** If you can split your work into multiple PRs, please do - the smaller a PR is, the more likely your reviewer will be able to understand it and thus accept it. If you follow these steps, it becomes much easier for the reviewer to understand your code and thus feel confident about accepting it. This also allows the reviewer to make more helpful suggestions about the code itself - both allowing them to verify that the code does what you say it does, and that you've implemented it in a helpful way. The review process should help you write better code as well as making Tablebot as a whole better. From 731d5546cc4b412e21b1bac476802a600e1468f2 Mon Sep 17 00:00:00 2001 From: Finnbar Keating Date: Sun, 17 Jul 2022 18:26:57 +0100 Subject: [PATCH 89/96] Tiny rename to improve understanding --- src/Tablebot/Internal/Handler/Command.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Tablebot/Internal/Handler/Command.hs b/src/Tablebot/Internal/Handler/Command.hs index 4e7575ce..66ee3343 100644 --- a/src/Tablebot/Internal/Handler/Command.hs +++ b/src/Tablebot/Internal/Handler/Command.hs @@ -68,9 +68,9 @@ parseCommands cs m prefix = case parse (parser cs) "" (messageText m) of parser cs' = do _ <- chunk prefix - choice (map toErroringParser cs') "No command with that name was found!" - toErroringParser :: CompiledCommand -> Parser (Message -> CompiledDatabaseDiscord ()) - toErroringParser c = try (chunk $ commandName c) *> (skipSpace1 <|> eof) *> (try (choice $ map toErroringParser $ commandSubcommands c) <|> commandParser c) + choice (map commandToParser cs') "No command with that name was found!" + commandToParser :: CompiledCommand -> Parser (Message -> CompiledDatabaseDiscord ()) + commandToParser c = try (chunk $ commandName c) *> (skipSpace1 <|> eof) *> (try (choice $ map commandToParser $ commandSubcommands c) <|> commandParser c) data ReadableError = UnknownError | KnownError String [String] deriving (Show, Eq, Ord) From e1198eb95d69a724d3c0e866bd071d601769b16b Mon Sep 17 00:00:00 2001 From: Finnbar Keating Date: Sun, 17 Jul 2022 18:40:38 +0100 Subject: [PATCH 90/96] Made "played game" more generic --- app/Main.hs | 10 +++++++++- src/Tablebot.hs | 2 +- src/Tablebot/Internal/Types.hs | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/Main.hs b/app/Main.hs index 25607066..d14d8e3d 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -8,7 +8,15 @@ import Tablebot.Plugins (allPlugins) -- @main@ runs forever. This allows bot reloading by fully shutting down the bot and letting it restart. main :: IO () -main = runTablebotWithEnv allPlugins $ BotConfig {gamePlaying = "with dice", rootHelpText = rootBody} +main = + runTablebotWithEnv allPlugins $ + BotConfig + { gamePlaying = game, + rootHelpText = rootBody + } + +game :: Text -> Text +game prefix = "with dice. Prefix is `" <> prefix <> "`. Call `" <> prefix <> "help` for help" rootBody :: Text rootBody = diff --git a/src/Tablebot.hs b/src/Tablebot.hs index d8a54744..24490ff9 100644 --- a/src/Tablebot.hs +++ b/src/Tablebot.hs @@ -137,7 +137,7 @@ runTablebot vinfo dToken prefix dbpath plugins config = updateStatusOptsGame = Just ( Activity - { activityName = gamePlaying config <> ". Prefix is `" <> prefix <> "`. Call `" <> prefix <> "help` for help", + { activityName = gamePlaying config prefix, activityType = ActivityTypeGame, activityUrl = Nothing } diff --git a/src/Tablebot/Internal/Types.hs b/src/Tablebot/Internal/Types.hs index 9a1e8b5b..d477fbd5 100644 --- a/src/Tablebot/Internal/Types.hs +++ b/src/Tablebot/Internal/Types.hs @@ -89,12 +89,12 @@ data CompiledCronJob = CCronJob data BotConfig = BotConfig { rootHelpText :: Text, - gamePlaying :: Text + gamePlaying :: Text -> Text } instance Default BotConfig where def = BotConfig { rootHelpText = "This bot is built off the Tablebot framework ().", - gamePlaying = "Kirby: Planet Robobot" + gamePlaying = const "Kirby: Planet Robobot" } From 101a4e63d2288edc666c163ae919aed48cd769f3 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Mon, 18 Jul 2022 17:16:06 +0100 Subject: [PATCH 91/96] added some more stuff to tutorial --- tutorials/3.Interactions.md | 26 ++++++++++++++---- .../resources/interactions_example_labels.jpg | Bin 0 -> 10778 bytes .../resources/interactions_example_result.jpg | Bin 0 -> 38105 bytes 3 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 tutorials/resources/interactions_example_labels.jpg create mode 100644 tutorials/resources/interactions_example_result.jpg diff --git a/tutorials/3.Interactions.md b/tutorials/3.Interactions.md index 61aff4ff..f7f7a815 100644 --- a/tutorials/3.Interactions.md +++ b/tutorials/3.Interactions.md @@ -2,6 +2,10 @@ A recent change in Discord added some pretty neat ways of interacting. Unfortunately, it requires a fair amount of fiddling, so let's go through how to use this! +Below is an example of what we'll be able to produce at the end of this tutorial. + +!["showing the usage of various interactions"](./resources/interactions_example_result.jpg "showing the usage of various interactions") + ## Setup We'll start where the [Plugins](./1.Plugins.md) tutorial left off, with the below file. I've removed `myping'`, and commented out most of the file, so we can start from basics and build back up again, as well as added a line to `pingPlugin'` which will help us later. @@ -108,7 +112,9 @@ Turns out there's a fair amount of information that we took for granted before t ### Labelled arguments -Slash commands unfortunately need to have each of their parameters named, which means that anything we give to our function has to be named. +Slash commands unfortunately need to have each of their parameters named and described, which means that anything we give to our function has to be named. This can be seen below. + +!["showing the an example slash command"](./resources/interactions_example_labels.jpg "showing an example slash command") Let's uncomment `myEcho` and roll up our sleeves to see what we want to do here. @@ -122,7 +128,7 @@ echoHelp (ROI t) = return $ messageDetailsBasic t We take away the restriction of `RestOfInput` for now, breaking `myecho`, but we'll fix it shortly. -We then change `myEcho` so that instead of having `(parseComm echoHelp)`, we instead have `(parseComm (echoHelp . unROI))`. This function is now the same as it originally was, and we can reuse our new `echoHelp` for our interactions. +We then change `myEcho` so that instead of having `(parseComm echoHelp)`, we instead have `(parseComm (echoHelp . unROI))`. This function is now the same as it originally was, and we can reuse our new `echoHelp` for our interactions. `unROI` lets us get the value within a `RestOfInput` value; its usage here effectively tells `parseComm` "Hey, get a `RestOfInput`, unwrap it, and feed it to `echoHelp`". ```haskell myEcho :: Command @@ -155,9 +161,9 @@ No instance for (GHC.TypeLits.KnownSymbol name0) "myecho" "echo your input" (echoHelp . unLabel) ``` -Oh dear that's hideous. Oh yeah, we forget to actually label anything! +Oh dear that's hideous. Oh yeah, we forget to actually label anything! Labelling means we name and describe each argument we're giving to this function. -We can achieve this in one of two ways. Firstly, we could create a function which has the labels we want and then make the slash command like that; secondly, we could use type applications to add the labels we want here. I'm going to go with the latter in this case, but I'll show both here. +We can achieve this in one of two ways. Firstly, we could create a function which has the labels we want and then make the slash command like that; secondly, we could use type applications to add the labels we want here. I'm going to go with the latter in this case, but I'll show both here. `Labelled` is provided by `Tablebot` to do some clever things in the command parsing - it groups a value with a label and a description. ```haskell myEchoInteraction1 :: Maybe ApplicationCommandRecv @@ -227,7 +233,17 @@ myPingButton :: ComponentRecv myPingButton = ComponentRecv "pingbutton" (processComponentInteraction myPingAction False) ``` -Now we just load `myPingButton` into `onComponentRecvs` in our plugin creation, run the bot again, run the command, and press our sparkly button, and we get a message from the bot saying "pong"! +Now we just load `myPingButton` into `onComponentRecvs` in our plugin creation (similar to what we do with other commands), run the bot again, run the command, and press our sparkly button, and we get a message from the bot saying "pong"! + +```haskell +pingPlugin' = (plug "myping") {commands = [ + ... + ] + ... + , onComponentRecvs = [myPingButton] -- This right here, add this + ... + } +``` But can we do better? diff --git a/tutorials/resources/interactions_example_labels.jpg b/tutorials/resources/interactions_example_labels.jpg new file mode 100644 index 0000000000000000000000000000000000000000..42499e8ae91527392a37c730ca4292d88842c2aa GIT binary patch literal 10778 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�Tuu9|@rBL?MHLj9hhrgPHjhYf*JSn3` z-*ba)uB`sh`?Y`c&)@MzTzxy%t9kP-+`O_@45MP+mLc{nuu&@=7;i<&`?gWeGn~D< zPUAxG=R4icvaU@tcA3>Ksu#>Sk1`wTh~B=fW%u|YxvbPJL3WWLZGj|Loxjle%LRd( zFj^Dqb9g~WjASd4N+ECyd5fos?$_c?KdhxU8ojYmXEL~=c406D#tLK z1MGZKBbvH8@WKH19=f`Obph9dw+WRo9+)bXe#jyYb%BnH|N9=q-G}>n=Y)G;0`#O9 zM<|*Ltmy5wZHy*a?4FER7?)%Cz*)u^;`@;v>QvQ~#gLe+SRtXa5Ibukknl literal 0 HcmV?d00001 diff --git a/tutorials/resources/interactions_example_result.jpg b/tutorials/resources/interactions_example_result.jpg new file mode 100644 index 0000000000000000000000000000000000000000..edb1e18114f8d9dab4836518292beb89ca37a96f GIT binary patch literal 38105 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 literal 0 HcmV?d00001 From a2dbafdd79ff3cd6041f568d21e3fda7cc792abb Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Tue, 19 Jul 2022 19:45:06 +0100 Subject: [PATCH 92/96] minor changes and fixes, mostly adding descriptions and modifying program flow --- README.md | 1 + src/Tablebot/Internal/Administration.hs | 14 +++++---- src/Tablebot/Plugins/Administration.hs | 40 ++++++++++++++----------- src/Tablebot/Plugins/Alias.hs | 6 ++-- src/Tablebot/Utility/Exception.hs | 2 +- tutorials/3.Interactions.md | 2 ++ 6 files changed, 39 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index b0734af3..dc0dc6a0 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Create a `.env` file containing the following keys. Consult `.env.example` if yo * `SERVER_ID` (optional) - either `global` or the id of the server the bot will mainly be deployed in. Application commands will be registered here. If absent, application commands won't be registered. * `EMOJI_SERVERS` (optional) - a list of server IDs that the bot will search for emoji within. +* `ALLOW_GIT_UPDATE` (optional) - a `true` or `false` value that determines whether the bot can automatically load data from the repository. The three Group settings are optional, but without them any commands that require elevated permissions will not be able to be called when `DEBUG` is false. Users with the superuser group are able to run every command (including some dangerous diff --git a/src/Tablebot/Internal/Administration.hs b/src/Tablebot/Internal/Administration.hs index ed5319e1..e0bb9874 100644 --- a/src/Tablebot/Internal/Administration.hs +++ b/src/Tablebot/Internal/Administration.hs @@ -61,22 +61,26 @@ restartIsTerminal :: ShutdownReason -> Bool restartIsTerminal Reload = False restartIsTerminal _ = True +gitUpdateEnabled :: IO Bool +gitUpdateEnabled = do + maybeEnabled <- lookupEnv "ALLOW_GIT_UPDATE" + return $ maybe False ((== "true") . lower . trim) maybeEnabled + updateGit :: IO () updateGit = do - maybeEnabled <- lookupEnv "ALLOW_GIT_UPDATE" - let enabled = maybe False ((== "true") . lower . trim) maybeEnabled + enabled <- gitUpdateEnabled when enabled $ do status <- readProcess "git" ["status"] "" let pattern :: String pattern = "working tree clean" clean :: Bool - clean = isInfixOf pattern status + clean = pattern `isInfixOf` status if clean then do callProcess "git" ["pull", "--rebase"] pullStatus <- readProcess "git" ["status"] "" let pullClean :: Bool - pullClean = isInfixOf pattern pullStatus + pullClean = pattern `isInfixOf` pullStatus if pullClean then putStrLn "Git pulled successfully. Restarting" else do @@ -85,4 +89,4 @@ updateGit = do else putStrLn "Git directory not clean. Not updating" gitVersion :: IO Text -gitVersion = (pack . trim) <$> readProcess "git" ["rev-parse", "HEAD"] "" +gitVersion = pack . trim <$> readProcess "git" ["rev-parse", "HEAD"] "" diff --git a/src/Tablebot/Plugins/Administration.hs b/src/Tablebot/Plugins/Administration.hs index c595a1cc..6de41e6c 100644 --- a/src/Tablebot/Plugins/Administration.hs +++ b/src/Tablebot/Plugins/Administration.hs @@ -52,14 +52,14 @@ blacklistComm :: EnvDatabaseDiscord SS () blacklistComm (WErr (Left (Left (_, pLabel)))) = addBlacklist pLabel blacklistComm (WErr (Left (Right (_, pLabel)))) = removeBlacklist pLabel -blacklistComm (WErr (Right (_))) = listBlacklist +blacklistComm (WErr (Right _)) = listBlacklist addBlacklist :: String -> Message -> EnvDatabaseDiscord SS () addBlacklist pLabel m = requirePermission Superuser m $ do known <- ask -- It's not an error to add an unknown plugin (so that you can pre-disable a plugin you know you're about to add), -- but emmit a warning so people know if it wasn't deliberate - when ((pack pLabel) `notElem` known) $ sendMessage m "Warning, unknown plugin" + when (pack pLabel `notElem` known) $ sendMessage m "Warning, unknown plugin" extant <- exists [PluginBlacklistLabel ==. pLabel] if not extant then do @@ -132,7 +132,7 @@ version = Command "version" noCommand [] gVersion <- getVersionInfo sendMessage m $ formatVersions gVersion formatVersions :: VersionInfo -> Text - formatVersions vi = "Tablebot version " <> (pack $ showVersion $ procVersion vi) <> "\nGit Hash: `" <> (gitHash vi) <> "`" + formatVersions vi = "Tablebot version " <> pack (showVersion $ procVersion vi) <> "\nGit Hash: `" <> gitHash vi <> "`" -- | @botcontrol@ reloads the bot with any new configuration changes. botControl :: MVar ShutdownReason -> EnvCommand SS @@ -144,13 +144,13 @@ botControl rFlag = Command "botcontrol" noCommand [reload rFlag, restart rFlag, -- | @reload@ reloads the bot with any new configuration changes. reload :: MVar ShutdownReason -> EnvCommand SS -reload rFlag = Command "reload" restartCommand [] +reload rFlag = Command "reload" reloadCommand [] where - restartCommand :: Parser (Message -> EnvDatabaseDiscord SS ()) - restartCommand = noArguments $ \m -> requirePermission Superuser m $ do + reloadCommand :: Parser (Message -> EnvDatabaseDiscord SS ()) + reloadCommand = noArguments $ \m -> requirePermission Superuser m $ do sendMessage m "Reloading bot..." _ <- liftIO $ swapMVar rFlag Reload - liftDiscord $ stopDiscord + liftDiscord stopDiscord -- | @reload@ reloads the bot with any new configuration changes. restart :: MVar ShutdownReason -> EnvCommand SS @@ -160,17 +160,17 @@ restart rFlag = Command "restart" restartCommand [] restartCommand = noArguments $ \m -> requirePermission Superuser m $ do sendMessage m "Restarting bot... (this may take some time)" _ <- liftIO $ swapMVar rFlag Restart - liftDiscord $ stopDiscord + liftDiscord stopDiscord -- | @halt@ stops the bot. halt :: MVar ShutdownReason -> EnvCommand SS -halt rFlag = Command "halt" restartCommand [] +halt rFlag = Command "halt" haltCommand [] where - restartCommand :: Parser (Message -> EnvDatabaseDiscord SS ()) - restartCommand = noArguments $ \m -> requirePermission Superuser m $ do + haltCommand :: Parser (Message -> EnvDatabaseDiscord SS ()) + haltCommand = noArguments $ \m -> requirePermission Superuser m $ do sendMessage m "Halting bot! (Goodnight, cruel world)" _ <- liftIO $ swapMVar rFlag Halt - liftDiscord $ stopDiscord + liftDiscord stopDiscord -- | @gitupdate@ pulls the latest version from the git. gitprompt :: MVar ShutdownReason -> EnvCommand SS @@ -181,13 +181,17 @@ gitprompt rFlag = Command "gitupdate" promptCommand [gitupdate rFlag] sendMessage m "Please confirm you want to do this by appending the following to your command:\n`yes I'm sure I want to do this and understand it's potentially dangerous`" gitupdate :: MVar ShutdownReason -> EnvCommand SS -gitupdate rFlag = Command "yes I'm sure I want to do this and understand it's potentially dangerous" restartCommand [] +gitupdate rFlag = Command "yes I'm sure I want to do this and understand it's potentially dangerous" updateCommand [] where - restartCommand :: Parser (Message -> EnvDatabaseDiscord SS ()) - restartCommand = noArguments $ \m -> requirePermission Superuser m $ do - sendMessage m "Attempting to update bot from the git. Please wait" - _ <- liftIO $ swapMVar rFlag GitUpdate - liftDiscord $ stopDiscord + updateCommand :: Parser (Message -> EnvDatabaseDiscord SS ()) + updateCommand = noArguments $ \m -> requirePermission Superuser m $ do + enabled <- liftIO gitUpdateEnabled + if not enabled + then sendMessage m "Git update is not enabled; set `ALLOW_GIT_UPDATE` to `true`." + else do + sendMessage m "Attempting to update bot from the git. Please wait" + _ <- liftIO $ swapMVar rFlag GitUpdate + liftDiscord stopDiscord versionHelp :: HelpPage versionHelp = diff --git a/src/Tablebot/Plugins/Alias.hs b/src/Tablebot/Plugins/Alias.hs index bb635228..d0cee7bd 100644 --- a/src/Tablebot/Plugins/Alias.hs +++ b/src/Tablebot/Plugins/Alias.hs @@ -133,8 +133,10 @@ aliasListHelp = Lists all aliases. You can specify whether the aliases are public or private. -*Usage:* `alias list`, `alias list private`, `alias list public`|] - [] +*Usage:* `alias list`|] + [ HelpPage "private" [] "lists your private aliases" "**List Private Aliases**\nLists your private aliases.\n\n*Usage:* `alias list private`" [] None, + HelpPage "public" [] "lists the public aliases" "**List Public Aliases**\nLists the public aliases.\n\n*Usage:* `alias list public`" [] None + ] None aliasDeleteCommand :: Command diff --git a/src/Tablebot/Utility/Exception.hs b/src/Tablebot/Utility/Exception.hs index 854ef566..3e8231be 100644 --- a/src/Tablebot/Utility/Exception.hs +++ b/src/Tablebot/Utility/Exception.hs @@ -128,4 +128,4 @@ errorInfo (EvaluationException msg' locs) = ErrorInfo "EvaluationException" $ ms errorInfo (IOException msg') = ErrorInfo "IOException" msg' errorInfo (NetrunnerException msg') = ErrorInfo "NetrunnerException" msg' errorInfo (InteractionException msg') = ErrorInfo "InteractionException" msg' -errorInfo (PermissionException msg') = ErrorInfo "InteractionException" msg' +errorInfo (PermissionException msg') = ErrorInfo "PermissionException" msg' diff --git a/tutorials/3.Interactions.md b/tutorials/3.Interactions.md index f7f7a815..47165067 100644 --- a/tutorials/3.Interactions.md +++ b/tutorials/3.Interactions.md @@ -75,6 +75,8 @@ pingPlugin' = (plug "myping") {commands = [ } ``` +Additionally, we'll need to choose how our application commands are registered. The quickest and easiest way to do this is to add `SERVER_ID=` to your `.env` file. + ## Slash command basics As before, let's start with the simplest type of command - replying to a given command. From f0ac35406d5eee4af324ad5a307e881c9266f446 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Tue, 19 Jul 2022 20:46:01 +0100 Subject: [PATCH 93/96] made changes that mean buttons should only appear when needed, and quote errors replace embeds --- src/Tablebot/Plugins/Quote.hs | 39 +++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/Tablebot/Plugins/Quote.hs b/src/Tablebot/Plugins/Quote.hs index e2d5742b..d9e31062 100644 --- a/src/Tablebot/Plugins/Quote.hs +++ b/src/Tablebot/Plugins/Quote.hs @@ -176,13 +176,13 @@ showQ :: Context m => Int64 -> m -> DatabaseDiscord MessageDetails showQ qId m = do qu <- get $ toSqlKey qId case qu of - Just q -> renderQuoteMessage q qId m + Just q -> renderQuoteMessage q qId Nothing m Nothing -> return $ messageDetailsBasic "Couldn't get that quote!" -- | @randomQuote@, which looks for a message of the form @!quote random@, -- selects a random quote from the database and responds with that quote. randomQ :: Context m => m -> DatabaseDiscord MessageDetails -randomQ c = filteredRandomQuote [] "Couldn't find any quotes!" c >>= \m -> return (m {messageDetailsComponents = Just [ActionRowButtons [randomButton]]}) +randomQ = filteredRandomQuote [] "Couldn't find any quotes!" (Just randomButton) where randomButton = mkButton "Random quote" "quote random" @@ -192,7 +192,7 @@ randomQuoteComponentRecv = ComponentRecv "random" (processComponentInteraction ( -- | @authorQuote@, which looks for a message of the form @!quote author u@, -- selects a random quote from the database attributed to u and responds with that quote. authorQ :: Context m => Text -> m -> DatabaseDiscord MessageDetails -authorQ t c = filteredRandomQuote [QuoteAuthor ==. t] "Couldn't find any quotes with that author!" c >>= \m -> return (m {messageDetailsComponents = Just [ActionRowButtons [authorButton]]}) +authorQ t = filteredRandomQuote [QuoteAuthor ==. t] "Couldn't find any quotes with that author!" (Just authorButton) where authorButton = mkButton "Random author quote" ("quote author " <> t) @@ -202,28 +202,30 @@ authorQuoteComponentRecv = ComponentRecv "author" (processComponentInteraction ( -- | @filteredRandomQuote@ selects a random quote that meets a -- given criteria, and returns that as the response, sending the user a message if the -- quote cannot be found. -filteredRandomQuote :: Context m => [Filter Quote] -> Text -> m -> DatabaseDiscord MessageDetails -filteredRandomQuote quoteFilter errorMessage m = catchBot (filteredRandomQuote' quoteFilter errorMessage m) catchBot' +filteredRandomQuote :: Context m => [Filter Quote] -> Text -> Maybe Button -> m -> DatabaseDiscord MessageDetails +filteredRandomQuote quoteFilter errorMessage mb m = catchBot (filteredRandomQuote' quoteFilter errorMessage mb m) catchBot' where - catchBot' (GenericException "quote exception" _) = return $ messageDetailsBasic errorMessage + catchBot' (GenericException "quote exception" _) = return $ (messageDetailsBasic errorMessage) {messageDetailsEmbeds = Just []} catchBot' e = throwBot e -- | @filteredRandomQuote'@ selects a random quote that meets a -- given criteria, and returns that as the response, throwing an exception if something -- goes wrong. -filteredRandomQuote' :: Context m => [Filter Quote] -> Text -> m -> DatabaseDiscord MessageDetails -filteredRandomQuote' quoteFilter errorMessage m = do +filteredRandomQuote' :: Context m => [Filter Quote] -> Text -> Maybe Button -> m -> DatabaseDiscord MessageDetails +filteredRandomQuote' quoteFilter errorMessage mb m = do num <- count quoteFilter - if num == 0 + if num == 0 -- we can't find any quotes meeting the filter then throwBot (GenericException "quote exception" (unpack errorMessage)) else do rindex <- liftIO $ randomRIO (0, num - 1) key <- selectKeysList quoteFilter [OffsetBy rindex, LimitTo 1] qu <- get $ head key case qu of - Just q -> renderQuoteMessage q (fromSqlKey $ head key) m + Just q -> renderQuoteMessage q (fromSqlKey $ head key) mb m Nothing -> throwBot (GenericException "quote exception" (unpack errorMessage)) +-- we somehow can't get the quote we described + -- | @addQuote@, which looks for a message of the form -- @!quote add "quoted text" - author@, and then stores said quote in the -- database, returning the ID used. @@ -236,7 +238,7 @@ addQ' qu author requestor sourceMsg sourceChannel m = do let new = Quote qu author requestor (fromIntegral sourceMsg) (fromIntegral sourceChannel) now added <- insert new let res = pack $ show $ fromSqlKey added - renderCustomQuoteMessage ("Quote added as #" `append` res) new (fromSqlKey added) m <&> (,fromSqlKey added) + renderCustomQuoteMessage ("Quote added as #" `append` res) new (fromSqlKey added) Nothing m <&> (,fromSqlKey added) -- | @thisQuote@, which takes the replied message or the -- previous message and stores said message as a quote in the database, @@ -271,7 +273,7 @@ addMessageQuote submitter q' m = do now added <- insert new let res = pack $ show $ fromSqlKey added - renderCustomQuoteMessage ("Quote added as #" `append` res) new (fromSqlKey added) m + renderCustomQuoteMessage ("Quote added as #" `append` res) new (fromSqlKey added) Nothing m else return $ makeEphermeral (messageDetailsBasic "Can't quote a bot") else return $ makeEphermeral (messageDetailsBasic "Message already quoted") @@ -292,7 +294,7 @@ editQ' qId qu author requestor mid cid m = now <- liftIO $ systemToUTCTime <$> getSystemTime let new = Quote (fromMaybe qu' qu) (fromMaybe author' author) requestor (fromIntegral mid) (fromIntegral cid) now replace k new - renderCustomQuoteMessage "Quote updated" new qId m + renderCustomQuoteMessage "Quote updated" new qId Nothing m Nothing -> return $ messageDetailsBasic "Couldn't update that quote!" -- | @deleteQuote@, which looks for a message of the form @!quote delete n@, @@ -309,11 +311,11 @@ deleteQ qId m = sendMessage m "Quote deleted" Nothing -> sendMessage m "Couldn't delete that quote!" -renderQuoteMessage :: Context m => Quote -> Int64 -> m -> DatabaseDiscord MessageDetails +renderQuoteMessage :: Context m => Quote -> Int64 -> Maybe Button -> m -> DatabaseDiscord MessageDetails renderQuoteMessage = renderCustomQuoteMessage "" -renderCustomQuoteMessage :: Context m => Text -> Quote -> Int64 -> m -> DatabaseDiscord MessageDetails -renderCustomQuoteMessage t (Quote txt author submitter msgId cnlId dtm) qId m = do +renderCustomQuoteMessage :: Context m => Text -> Quote -> Int64 -> Maybe Button -> m -> DatabaseDiscord MessageDetails +renderCustomQuoteMessage t (Quote txt author submitter msgId cnlId dtm) qId mb m = do guild <- contextGuildId m let link = getLink guild return @@ -324,7 +326,8 @@ renderCustomQuoteMessage t (Quote txt author submitter msgId cnlId dtm) qId m = addTimestamp dtm $ addFooter (pack $ "Quote #" ++ show qId) $ simpleEmbed (txt <> "\n - " <> author <> maybeAddFooter link) - ] + ], + messageDetailsComponents = mb >>= \b -> Just [ActionRowButtons [b]] } ) where @@ -421,7 +424,7 @@ quoteApplicationCommandRecv now <- liftIO $ systemToUTCTime <$> getSystemTime let new = Quote qt author requestor (fromIntegral $ messageId m) (fromIntegral $ messageChannelId m) now replace (toSqlKey qid) new - newMsg <- renderCustomQuoteMessage (messageContent m) new qid i + newMsg <- renderCustomQuoteMessage (messageContent m) new qid Nothing i _ <- liftDiscord $ restCall $ R.EditOriginalInteractionResponse (interactionApplicationId i) (interactionToken i) (convertMessageFormatInteraction newMsg) return () ) From 74f13e8b61498affd03cdb003f6ce5cf8d3854c4 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Tue, 19 Jul 2022 23:51:12 +0100 Subject: [PATCH 94/96] added warning about allowing git update --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index dc0dc6a0..e8ee3b9a 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Create a `.env` file containing the following keys. Consult `.env.example` if yo registered here. If absent, application commands won't be registered. * `EMOJI_SERVERS` (optional) - a list of server IDs that the bot will search for emoji within. * `ALLOW_GIT_UPDATE` (optional) - a `true` or `false` value that determines whether the bot can automatically load data from the repository. + **Warning!** Be very careful with setting this to true; if you haven't set up permissions properly on your repo and your discord servers then things can go wrong! The three Group settings are optional, but without them any commands that require elevated permissions will not be able to be called when `DEBUG` is false. Users with the superuser group are able to run every command (including some dangerous From 818ff51407948ea83ca4a5cf72115dc6de70a2c7 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Sat, 23 Jul 2022 17:17:17 +0100 Subject: [PATCH 95/96] =?UTF-8?q?removed=20button=20on=20quote=20failure,?= =?UTF-8?q?=20stop=20aliasing=20when=20alias=20is=20blacklisted=20=C2=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Tablebot/Internal/Administration.hs | 6 +++--- src/Tablebot/Internal/Alias.hs | 13 +++++++++---- src/Tablebot/Plugins/Quote.hs | 4 +--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Tablebot/Internal/Administration.hs b/src/Tablebot/Internal/Administration.hs index e0bb9874..7d5ba552 100644 --- a/src/Tablebot/Internal/Administration.hs +++ b/src/Tablebot/Internal/Administration.hs @@ -14,11 +14,11 @@ module Tablebot.Internal.Administration ) where -import Control.Monad.Cont (void, when) +import Control.Monad.Cont (MonadIO, void, when) import Data.List.Extra (isInfixOf, lower, trim) import Data.Text (Text, pack) import Database.Persist -import Database.Persist.Sqlite (SqlPersistM) +import Database.Persist.Sqlite (SqlPersistT) import Database.Persist.TH import System.Environment (lookupEnv) import System.Process @@ -32,7 +32,7 @@ PluginBlacklist deriving Show |] -currentBlacklist :: SqlPersistM [Text] +currentBlacklist :: MonadIO m => SqlPersistT m [Text] currentBlacklist = do bl <- selectList allBlacklisted [] return $ fmap (pack . pluginBlacklistLabel . entityVal) bl diff --git a/src/Tablebot/Internal/Alias.hs b/src/Tablebot/Internal/Alias.hs index be929665..7782c701 100644 --- a/src/Tablebot/Internal/Alias.hs +++ b/src/Tablebot/Internal/Alias.hs @@ -15,8 +15,9 @@ import Database.Persist.Sqlite (BackendKey (SqlBackendKey)) import qualified Database.Persist.Sqlite as Sql import Database.Persist.TH import Discord.Types +import Tablebot.Internal.Administration (currentBlacklist) import Tablebot.Internal.Types -import Tablebot.Utility.Database (selectList) +import Tablebot.Utility.Database (liftSql, selectList) import Tablebot.Utility.Types (EnvDatabaseDiscord) share @@ -32,6 +33,10 @@ Alias |] getAliases :: UserId -> EnvDatabaseDiscord d (Maybe [Alias]) -getAliases uid = - (Just . fmap Sql.entityVal <$> selectList [AliasType Sql.<-. [AliasPublic, AliasPrivate uid]] []) - `catch` (\(_ :: SomeException) -> return Nothing) +getAliases uid = do + blacklist <- liftSql currentBlacklist + if "alias" `elem` blacklist + then return Nothing + else + (Just . fmap Sql.entityVal <$> selectList [AliasType Sql.<-. [AliasPublic, AliasPrivate uid]] []) + `catch` (\(_ :: SomeException) -> return Nothing) diff --git a/src/Tablebot/Plugins/Quote.hs b/src/Tablebot/Plugins/Quote.hs index d9e31062..01a4f2cb 100644 --- a/src/Tablebot/Plugins/Quote.hs +++ b/src/Tablebot/Plugins/Quote.hs @@ -205,7 +205,7 @@ authorQuoteComponentRecv = ComponentRecv "author" (processComponentInteraction ( filteredRandomQuote :: Context m => [Filter Quote] -> Text -> Maybe Button -> m -> DatabaseDiscord MessageDetails filteredRandomQuote quoteFilter errorMessage mb m = catchBot (filteredRandomQuote' quoteFilter errorMessage mb m) catchBot' where - catchBot' (GenericException "quote exception" _) = return $ (messageDetailsBasic errorMessage) {messageDetailsEmbeds = Just []} + catchBot' (GenericException "quote exception" _) = return $ (messageDetailsBasic errorMessage) {messageDetailsEmbeds = Just [], messageDetailsComponents = Just []} catchBot' e = throwBot e -- | @filteredRandomQuote'@ selects a random quote that meets a @@ -224,8 +224,6 @@ filteredRandomQuote' quoteFilter errorMessage mb m = do Just q -> renderQuoteMessage q (fromSqlKey $ head key) mb m Nothing -> throwBot (GenericException "quote exception" (unpack errorMessage)) --- we somehow can't get the quote we described - -- | @addQuote@, which looks for a message of the form -- @!quote add "quoted text" - author@, and then stores said quote in the -- database, returning the ID used. From af70a373bad29f9a2ab5fc4df426fa915baca474 Mon Sep 17 00:00:00 2001 From: L0neGamer Date: Tue, 26 Jul 2022 20:50:45 +0100 Subject: [PATCH 96/96] made command parser more forgiving --- src/Tablebot/Internal/Handler/Command.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tablebot/Internal/Handler/Command.hs b/src/Tablebot/Internal/Handler/Command.hs index 94379b03..2adc979a 100644 --- a/src/Tablebot/Internal/Handler/Command.hs +++ b/src/Tablebot/Internal/Handler/Command.hs @@ -98,7 +98,7 @@ parseCommands' cs as m prefix = case parse (parser cs) "" (messageContent m) of _ <- chunk prefix choice (map commandToParser cs') "No command with that name was found!" commandToParser :: CompiledCommand -> Parser (Message -> CompiledDatabaseDiscord ()) - commandToParser c = try (chunk $ commandName c) *> (skipSpace1 <|> eof) *> (try (choice $ map commandToParser $ commandSubcommands c) <|> commandParser c) + commandToParser c = try (chunk (commandName c) *> (skipSpace1 <|> eof)) *> (try (choice $ map commandToParser $ commandSubcommands c) <|> commandParser c) aliasParser :: [Alias] -> Parser (Alias, Text) aliasParser as' = do _ <- chunk prefix