Calamity

Hackage Gitlab pipeline status License Hackage-Deps Discord Invite

Calamity is a Haskell library for writing discord bots, it uses [Polysemy](https://hackage.haskell.org/package/polysemy) as the core library for handling effects, allowing you to pick and choose how to handle certain features of the library. If you're looking for something with a less complicated interface, you might want to take a look at [discord-haskell](https://github.com/aquarial/discord-haskell). The current customisable effects are: * Cache: The default cache handler keeps the cache in memory, however you could write a cache handler that stores cache in a database for example. * Metrics: The library has counters, gauges, and histograms installed to measure useful things, by default these are not used (and cost nothing), but could be combined with [Prometheus](https://hackage.haskell.org/package/prometheus). An example of using prometheus as the metrics handler can be found [here](https://github.com/simmsb/calamity-example). * Logging: The [di-polysemy](https://hackage.haskell.org/package/di-polysemy) library is used to allow the logging effect to be customized, or disabled. # Docs You can find documentation on hackage at: https://hackage.haskell.org/package/calamity # Examples Some example projects can be found at: - [simmsb/calamity-bot](https://github.com/simmsb/calamity-bot): Uses a database, showing modularisation of groups/commands. ``` haskell {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-} module Main where import Calamity import Calamity.Cache.InMemory import Calamity.Commands import qualified Calamity.Commands.Context as CommandContext import Calamity.Metrics.Noop import Control.Concurrent import Control.Concurrent.STM.TVar import Control.Lens import Control.Monad import Data.Text.Lazy ( Text, fromStrict ) import Data.Text.Strict.Lens import qualified Di import qualified DiPolysemy as DiP import qualified Polysemy as P import qualified Polysemy.Async as P import qualified Polysemy.AtomicState as P import qualified Polysemy.Embed as P import qualified Polysemy.Fail as P import Prelude hiding ( error ) import TextShow data Counter m a where GetCounter :: Counter m Int P.makeSem ''Counter runCounterAtomic :: P.Member (P.Embed IO) r => P.Sem (Counter ': r) a -> P.Sem r a runCounterAtomic m = do var <- P.embed $ newTVarIO (0 :: Int) P.runAtomicStateTVar var $ P.reinterpret (\case GetCounter -> P.atomicState (\v -> (v + 1, v))) m handleFailByLogging m = do r <- P.runFail m case r of Left e -> DiP.error (e ^. packed) _ -> pure () info, debug :: BotC r => Text -> P.Sem r () info = DiP.info debug = DiP.info tellt :: (BotC r, Tellable t) => t -> Text -> P.Sem r (Either RestError Message) tellt t m = tell t $ L.toStrict m main :: IO () main = do token <- view packed <$> getEnv "BOT_TOKEN" Di.new $ \di -> void . P.runFinal . P.embedToFinal . DiP.runDiToIO di . runCounterAtomic . runCacheInMemory . runMetricsNoop . useConstantPrefix "!" $ runBotIO (BotToken token) defaultIntents $ do addCommands $ do helpCommand command @'[User] "utest" $ \ctx u -> do void $ tellt ctx $ "got user: " <> showtl u command @'[Named "u" User, Named "u1" User] "utest2" $ \ctx u u1 -> do void $ tellt ctx $ "got user: " <> showtl u <> "\nand: " <> showtl u1 command @'[L.Text, Snowflake User] "test" $ \ctx something aUser -> do info $ "something = " <> showt something <> ", aUser = " <> showt aUser command @'[] "hello" $ \ctx -> do void $ tellt ctx "heya" group "testgroup" $ do command @'[[L.Text]] "test" $ \ctx l -> do void $ tellt ctx ("you sent: " <> showtl l) command @'[] "count" $ \ctx -> do val <- getCounter void $ tellt ctx ("The value is: " <> showtl val) group "say" $ do command @'[KleenePlusConcat L.Text] "this" $ \ctx msg -> do void $ tellt ctx msg command @'[Snowflake Emoji] "etest" $ \ctx e -> do void $ tellt ctx $ "got emoji: " <> showtl e command @'[] "explode" $ \ctx -> do Just x <- pure Nothing debug "unreachable!" command @'[] "bye" $ \ctx -> do void $ tellt ctx "bye!" stopBot command @'[] "fire-evt" $ \ctx -> do fire $ customEvt @"my-event" ("aha" :: L.Text, ctx ^. #message) command @'[L.Text] "wait-for" $ \ctx s -> do void $ tellt ctx ("waiting for !" <> s) waitUntil @'MessageCreateEvt (\msg -> msg ^. #content == ("!" <> s)) void $ tellt ctx ("got !" <> s) react @'MessageCreateEvt $ \msg -> handleFailByLogging $ case msg ^. #content of "!say hi" -> replicateM_ 3 . P.async $ do info "saying heya" Right msg' <- tellt msg "heya" info "sleeping" P.embed $ threadDelay (5 * 1000 * 1000) info "slept" void . invoke $ EditMessage (msg ^. #channelID) msg' (Just "lol") Nothing info "edited" _ -> pure () react @('CustomEvt "command-error" (CommandContext.Context, CommandError)) $ \(ctx, e) -> do info $ "Command failed with reason: " <> showt e case e of ParseError n r -> void . tellt ctx $ "Failed to parse parameter: `" <> L.fromStrict n <> "`, with reason: ```\n" <> r <> "```" react @('CustomEvt "my-event" (L.Text, Message)) $ \(s, m) -> void $ tellt m ("Somebody told me to tell you about: " <> s) ``` ## Disabling library logging The library logs on debug levels by default, if you wish to disable logging you can do something along the lines of: ``` haskell Di.new $ \di -> -- ... . runDiToIO di -- disable logs emitted inside calamity . DiPolysemy.local (Di.Core.filter (\_ _ _ -> False)) . runBotIO -- ... -- re-enable logs emitted inside your bot's code . DiPolysemy.local (const di) ```