{-# OPTIONS_GHC -Wno-name-shadowing #-} {-# OPTIONS_GHC -fplugin=Polysemy.Plugin #-} -- | The client module Calamity.Client.Client ( react, runBotIO, runBotIO', runBotIO'', stopBot, sendPresence, events, fire, waitUntil, waitUntilM, CalamityEvent (Dispatch, ShutDown), customEvt, ) where import Calamity.Cache.Eff import Calamity.Client.ShardManager import Calamity.Client.Types import Calamity.Gateway.DispatchEvents import Calamity.Gateway.Intents import Calamity.Gateway.Types import Calamity.HTTP.Internal.Ratelimit import Calamity.Internal.ConstructorName import Calamity.Internal.RunIntoIO import Calamity.Internal.SnowflakeMap qualified as SM import Calamity.Internal.UnixTimestamp import Calamity.Internal.Updateable import Calamity.Internal.Utils import Calamity.Metrics.Eff import Calamity.Types.LogEff import Calamity.Types.Model.Channel import Calamity.Types.Model.Guild import Calamity.Types.Model.Presence (Presence (..)) import Calamity.Types.Model.User import Calamity.Types.Model.Voice qualified as V import Calamity.Types.Snowflake import Calamity.Types.Token import Calamity.Types.TokenEff import Control.Concurrent.Chan.Unagi import Control.Concurrent.MVar import Control.Concurrent.STM import Control.Exception (SomeException) import Control.Monad import Data.Default.Class import Data.Dynamic import Data.Foldable import Data.IORef import Data.Maybe import Data.Proxy import Data.Text qualified as T import Data.Time.Clock.POSIX import Df1 qualified import Di.Core qualified as DC import DiPolysemy qualified as Di import Optics import Polysemy qualified as P import Polysemy.Async qualified as P import Polysemy.AtomicState qualified as P import Polysemy.Error qualified as P import Polysemy.Fail qualified as P import Polysemy.Reader qualified as P import Polysemy.Resource qualified as P import TextShow (TextShow (showt)) timeA :: (P.Member (P.Embed IO) r) => P.Sem r a -> P.Sem r (Double, a) timeA m = do start <- P.embed getPOSIXTime res <- m end <- P.embed getPOSIXTime let duration = fromRational . toRational $ end - start pure (duration, res) newClient :: Token -> Maybe (DC.Di Df1.Level Df1.Path Df1.Message) -> IO Client newClient token initialDi = do shards' <- newTVarIO [] numShards' <- newEmptyMVar rlState' <- newRateLimitState (inc, outc) <- newChan ehidCounter <- newIORef 0 pure $ Client shards' numShards' token rlState' inc outc ehidCounter initialDi -- | Create a bot, run your setup action, and then loop until the bot closes. runBotIO :: forall r a. (P.Members '[P.Embed IO, P.Final IO, CacheEff, MetricEff, LogEff] r) => Token -> -- | The intents the bot should use Intents -> P.Sem (SetupEff r) a -> P.Sem r (Maybe StartupError) runBotIO token intents = runBotIO' token intents Nothing resetDi :: (BotC r) => P.Sem r a -> P.Sem r a resetDi m = do initialDi <- P.asks (^. #initialDi) Di.local (`fromMaybe` initialDi) m interpretRatelimitViaClient :: (P.Member (P.Reader Client) r) => P.Sem (RatelimitEff ': r) a -> P.Sem r a interpretRatelimitViaClient = P.interpret ( \case GetRatelimitState -> P.asks (^. #rlState) ) interpretTokenViaClient :: (P.Member (P.Reader Client) r) => P.Sem (TokenEff ': r) a -> P.Sem r a interpretTokenViaClient = P.interpret ( \case GetBotToken -> P.asks (^. #token) ) {- | Create a bot, run your setup action, and then loop until the bot closes. This version allows you to specify the initial status -} runBotIO' :: forall r a. (P.Members '[P.Embed IO, P.Final IO, CacheEff, MetricEff, LogEff] r) => Token -> -- | The intents the bot should use Intents -> -- | The initial status to send to the gateway Maybe StatusUpdateData -> P.Sem (SetupEff r) a -> P.Sem r (Maybe StartupError) runBotIO' token intents status setup = do initialDi <- Di.fetch client <- P.embed $ newClient token initialDi handlers <- P.embed $ newTVarIO def P.asyncToIOFinal . P.runAtomicStateTVar handlers . P.runReader client . interpretTokenViaClient . interpretRatelimitViaClient . Di.push "calamity" $ do void $ Di.push "calamity-setup" setup r <- shardBot status intents case r of Left e -> pure (Just e) Right _ -> do Di.push "calamity-loop" clientLoop Di.push "calamity-stop" finishUp pure Nothing {- | Create a bot, run your setup action, and then loop until the bot closes. This version only handles the @'P.Reader' 'Client'@ effect, allowing you to handle the @'P.AtomicState' 'EventHandlers'@ yourself. -} runBotIO'' :: forall r a. ( P.Members '[ LogEff , MetricEff , CacheEff , P.Reader Client , P.AtomicState EventHandlers , P.Embed IO , P.Final IO , P.Async ] r ) => Token -> -- | The intents the bot should use Intents -> -- | The initial status to send to the gateway Maybe StatusUpdateData -> P.Sem (RatelimitEff ': TokenEff ': P.Reader Client ': r) a -> P.Sem r (Maybe StartupError) runBotIO'' token intents status setup = do initialDi <- Di.fetch client <- P.embed $ newClient token initialDi P.runReader client . interpretTokenViaClient . interpretRatelimitViaClient . Di.push "calamity" $ do void $ Di.push "calamity-setup" setup r <- shardBot status intents case r of Left e -> pure (Just e) Right _ -> do Di.push "calamity-loop" clientLoop Di.push "calamity-stop" finishUp pure Nothing {- | Register an event handler, returning an action that removes the event handler from the bot. Refer to 'EventType' for what events you can register, and 'EHType' for the parameters the event handlers they receive. You'll probably want @TypeApplications@ and need @DataKinds@ enabled to specify the type of @s@. ==== Examples Reacting to every message: @ 'react' @\''MessageCreateEvt' '$' \msg -> 'print' '$' "Got message: " '<>' 'show' msg @ Reacting to a custom event: @ data MyCustomEvt = MyCustomEvt 'Data.Text.Text' 'Message' 'react' @(\''CustomEvt' MyCustomEvt) $ \\(MyCustomEvt s m) -> 'void' $ 'Calamity.Types.Tellable.tell' @'Data.Text.Text' m ("Somebody told me to tell you about: " '<>' s) @ ==== Notes This function is pretty bad for giving nasty type errors, since if something doesn't match then 'EHType' might not get substituted, which will result in errors about parameter counts mismatching. -} react :: forall (s :: EventType) r. (BotC r, ReactConstraints s) => (EHType s -> (P.Sem r) ()) -> P.Sem r (P.Sem r ()) react handler = do handler' <- bindSemToIO handler ehidC <- P.asks (^. #ehidCounter) id' <- P.embed $ atomicModifyIORef ehidC (\i -> (i + 1, i)) let handlers = makeEventHandlers (Proxy @s) id' (const () <.> handler') P.atomicModify (handlers <>) pure $ removeHandler @s id' removeHandler :: forall (s :: EventType) r. (BotC r, RemoveEventHandler s) => Integer -> P.Sem r () removeHandler id' = P.atomicModify (removeEventHandler (Proxy @s) id') {- | Fire an event that the bot will then handle. ==== Examples Firing an event named \"my-event\": @ 'fire' '$' 'customEvt' @"my-event" ("aha" :: 'Data.Text.Text', msg) @ -} fire :: (BotC r) => CalamityEvent -> P.Sem r () fire e = do inc <- P.asks (^. #eventsIn) P.embed $ writeChan inc e {- | Build a Custom CalamityEvent The type of @a@ must match up with the event handler you want to receive it. ==== Examples @ 'customEvt' (MyCustomEvent "lol") @ -} customEvt :: forall a. (Typeable a) => a -> CalamityEvent customEvt = Custom -- | Get a copy of the event stream. events :: (BotC r) => P.Sem r (OutChan CalamityEvent) events = do inc <- P.asks (^. #eventsIn) P.embed $ dupChan inc {- | Wait until an event satisfying a condition happens, then returns its parameters. The check function for this command is pure unlike 'waitUntilM' This is what it would look like with @s ~ \''MessageCreateEvt'@: @ 'waitUntil' :: ('Message' -> 'Bool') -> 'P.Sem' r 'Message' @ And for @s ~ \''MessageUpdateEvt'@: @ 'waitUntil' :: (('Message', 'Message') -> 'Bool') -> 'P.Sem' r ('Message', 'Message') @ ==== Examples Waiting for a message containing the text \"hi\": @ f = do msg \<\- 'waitUntil' @\''MessageCreateEvt' (\\m -> 'Data.Text.isInfixOf' "hi" $ m ^. #content) print $ msg ^. #content @ -} waitUntil :: forall (s :: EventType) r. (BotC r, ReactConstraints s) => (EHType s -> Bool) -> P.Sem r (EHType s) waitUntil f = P.resourceToIOFinal $ do result <- P.embed newEmptyMVar P.bracket (P.raise $ react @s (checker result)) P.raise (const . P.embed $ takeMVar result) where checker :: MVar (EHType s) -> EHType s -> P.Sem r () checker result args = do when (f args) $ do P.embed $ putMVar result args {- | Wait until an event satisfying a condition happens, then returns its parameters This is what it would look like with @s ~ \''MessageCreateEvt'@: @ 'waitUntilM' :: ('Message' -> 'P.Sem' r 'Bool') -> 'P.Sem' r 'Message' @ And for @s ~ \''MessageUpdateEvt'@: @ 'waitUntilM' :: (('Message', 'Message') -> 'P.Sem' r 'Bool') -> 'P.Sem' r ('Message', 'Message') @ ==== Examples Waiting for a message containing the text \"hi\": @ f = do msg \<\- 'waitUntilM' @\''MessageCreateEvt' (\\m -> ('debug' $ "got message: " <> 'showt' msg) >> ('pure' $ 'Data.Text.isInfixOf' "hi" $ m ^. #content)) print $ msg ^. #content @ -} waitUntilM :: forall (s :: EventType) r. (BotC r, ReactConstraints s) => (EHType s -> P.Sem r Bool) -> P.Sem r (EHType s) waitUntilM f = P.resourceToIOFinal $ do result <- P.embed newEmptyMVar P.bracket (P.raise $ react @s (checker result)) P.raise (const . P.embed $ takeMVar result) where checker :: MVar (EHType s) -> EHType s -> P.Sem r () checker result args = do res <- f args when res $ do P.embed $ putMVar result args -- | Set the bot's presence on all shards. sendPresence :: (BotC r) => StatusUpdateData -> P.Sem r () sendPresence s = do shards <- P.asks (^. #shards) >>= P.embed . readTVarIO for_ shards $ \(inc, _) -> P.embed $ writeChan inc (SendPresence s) -- | Initiate shutting down the bot. stopBot :: (BotC r) => P.Sem r () stopBot = do debug "stopping bot" inc <- P.asks (^. #eventsIn) P.embed $ writeChan inc ShutDown finishUp :: (BotC r) => P.Sem r () finishUp = do debug "finishing up" shards <- P.asks (^. #shards) >>= P.embed . readTVarIO for_ shards $ \(inc, _) -> P.embed $ writeChan inc ShutDownShard for_ shards $ \(_, shardThread) -> P.await shardThread debug "bot has stopped" {- | main loop of the client, handles fetching the next event, processing the event and invoking its handler functions -} clientLoop :: (BotC r) => P.Sem r () clientLoop = do outc <- P.asks (^. #eventsOut) whileMFinalIO $ do !evt' <- P.embed $ readChan outc case evt' of Dispatch !sid !evt -> handleEvent sid evt >> pure True Custom d -> handleCustomEvent d >> pure True ShutDown -> pure False debug "leaving client loop" handleCustomEvent :: forall a r. (Typeable a, BotC r) => a -> P.Sem r () handleCustomEvent d = do eventHandlers <- P.atomicGet let handlers = getCustomEventHandlers @a eventHandlers for_ handlers (\h -> P.async . P.embed $ h d) catchAllLogging :: (BotC r) => P.Sem r () -> P.Sem r () catchAllLogging m = do r <- P.errorToIOFinal . P.fromExceptionSem @SomeException $ P.raise m case r of Right _ -> pure () Left e -> debug . T.pack $ "got exception: " <> show e handleEvent :: (BotC r) => Int -> DispatchData -> P.Sem r () handleEvent shardID data' = do debug . T.pack $ "handling an event: " <> ctorName data' eventHandlers <- P.atomicGet actions <- P.runFail $ do evtCounter <- registerCounter "events_received" [("type", T.pack $ ctorName data'), ("shard", showt shardID)] void $ addCounter 1 evtCounter cacheUpdateHisto <- registerHistogram "cache_update" mempty [10, 20 .. 100] (time, res) <- timeA . resetDi $ handleEvent' eventHandlers data' void $ observeHistogram time cacheUpdateHisto pure res eventHandleHisto <- registerHistogram "event_handle" mempty [10, 20 .. 100] case actions of Right actions -> for_ actions $ \action -> P.async $ do (time, _) <- timeA . catchAllLogging $ P.embed action void $ observeHistogram time eventHandleHisto -- pattern match failures are usually stuff like events for uncached guilds, etc Left err -> debug . T.pack $ "Failed handling actions for event: " <> show err handleEvent' :: (BotC r) => EventHandlers -> DispatchData -> P.Sem (P.Fail ': r) [IO ()] handleEvent' eh evt@(Ready rd@ReadyData {}) = do updateCache evt pure $ map ($ rd) (getEventHandlers @'ReadyEvt eh) handleEvent' _ Resumed = pure [] handleEvent' eh evt@(ChannelCreate (DMChannel' chan)) = do updateCache evt Just newChan <- DMChannel' <<$>> getDM (getID chan) pure $ map ($ newChan) (getEventHandlers @'ChannelCreateEvt eh) handleEvent' eh evt@(ChannelCreate (GuildChannel' chan)) = do updateCache evt Just guild <- getGuild (getID chan) Just newChan <- pure $ GuildChannel' <$> guild ^. #channels % at (getID chan) pure $ map ($ newChan) (getEventHandlers @'ChannelCreateEvt eh) handleEvent' eh evt@(ChannelUpdate (DMChannel' chan)) = do Just oldChan <- DMChannel' <<$>> getDM (getID chan) updateCache evt Just newChan <- DMChannel' <<$>> getDM (getID chan) pure $ map ($ (oldChan, newChan)) (getEventHandlers @'ChannelUpdateEvt eh) handleEvent' eh evt@(ChannelUpdate (GuildChannel' chan)) = do Just oldGuild <- getGuild (getID chan) Just oldChan <- pure $ GuildChannel' <$> oldGuild ^. #channels % at (getID chan) updateCache evt Just newGuild <- getGuild (getID chan) Just newChan <- pure $ GuildChannel' <$> newGuild ^. #channels % at (getID chan) pure $ map ($ (oldChan, newChan)) (getEventHandlers @'ChannelUpdateEvt eh) handleEvent' eh evt@(ChannelDelete (GuildChannel' chan)) = do Just oldGuild <- getGuild (getID chan) Just oldChan <- pure $ GuildChannel' <$> oldGuild ^. #channels % at (getID chan) updateCache evt pure $ map ($ oldChan) (getEventHandlers @'ChannelDeleteEvt eh) handleEvent' eh evt@(ChannelDelete (DMChannel' chan)) = do Just oldChan <- DMChannel' <<$>> getDM (getID chan) updateCache evt pure $ map ($ oldChan) (getEventHandlers @'ChannelDeleteEvt eh) -- handleEvent' eh evt@(ChannelPinsUpdate ChannelPinsUpdateData { channelID, lastPinTimestamp }) = do -- chan <- (GuildChannel' <$> os ^? #channels % at (coerceSnowflake channelID) . _Just) -- <|> (DMChannel' <$> os ^? #dms % at (coerceSnowflake channelID) . _Just) -- pure $ map (\f -> f chan lastPinTimestamp) (getEventHandlers @"channelpinsupdate" eh) handleEvent' eh evt@(GuildCreate guild) = do isNew <- not <$> isUnavailableGuild (getID guild) updateCache evt Just guild <- getGuild (getID guild) pure $ map ($ (guild, if isNew then GuildCreateNew else GuildCreateAvailable)) (getEventHandlers @'GuildCreateEvt eh) handleEvent' eh evt@(GuildUpdate guild) = do Just oldGuild <- getGuild (getID guild) updateCache evt Just newGuild <- getGuild (getID guild) pure $ map ($ (oldGuild, newGuild)) (getEventHandlers @'GuildUpdateEvt eh) -- NOTE: Guild will be deleted in the new cache if unavailable was false handleEvent' eh evt@(GuildDelete UnavailableGuild {id, unavailable}) = do Just oldGuild <- getGuild id updateCache evt pure $ map ($ (oldGuild, if unavailable then GuildDeleteUnavailable else GuildDeleteRemoved)) (getEventHandlers @'GuildDeleteEvt eh) handleEvent' eh evt@(GuildBanAdd BanData {guildID, user}) = do Just guild <- getGuild guildID updateCache evt pure $ map ($ (guild, user)) (getEventHandlers @'GuildBanAddEvt eh) handleEvent' eh evt@(GuildBanRemove BanData {guildID, user}) = do Just guild <- getGuild guildID updateCache evt pure $ map ($ (guild, user)) (getEventHandlers @'GuildBanRemoveEvt eh) -- NOTE: we fire this event using the guild data with old emojis handleEvent' eh evt@(GuildEmojisUpdate GuildEmojisUpdateData {guildID, emojis}) = do Just guild <- getGuild guildID updateCache evt pure $ map ($ (guild, emojis)) (getEventHandlers @'GuildEmojisUpdateEvt eh) handleEvent' eh evt@(GuildIntegrationsUpdate GuildIntegrationsUpdateData {guildID}) = do updateCache evt Just guild <- getGuild guildID pure $ map ($ guild) (getEventHandlers @'GuildIntegrationsUpdateEvt eh) handleEvent' eh evt@(GuildMemberAdd gid member) = do updateCache evt Just guild <- getGuild gid Just member <- pure $ guild ^. #members % at (getID member) pure $ map ($ (guild, member)) (getEventHandlers @'GuildMemberAddEvt eh) handleEvent' eh evt@(GuildMemberRemove GuildMemberRemoveData {user, guildID}) = do Just guild <- getGuild guildID Just member <- pure $ guild ^. #members % at (getID user) updateCache evt pure $ map ($ (guild, member)) (getEventHandlers @'GuildMemberRemoveEvt eh) handleEvent' eh evt@(GuildMemberUpdate GuildMemberUpdateData {user, guildID}) = do Just oldGuild <- getGuild guildID Just oldMember <- pure $ oldGuild ^. #members % at (getID user) updateCache evt Just newGuild <- getGuild guildID Just newMember <- pure $ newGuild ^. #members % at (getID user) pure $ map ($ (newGuild, oldMember, newMember)) (getEventHandlers @'GuildMemberUpdateEvt eh) handleEvent' eh evt@(GuildMembersChunk GuildMembersChunkData {members, guildID}) = do updateCache evt Just guild <- getGuild guildID let memberIDs = map (getID @Member) members let members' = mapMaybe (\mid -> guild ^. #members % at mid) memberIDs pure $ map ($ (guild, members')) (getEventHandlers @'GuildMembersChunkEvt eh) handleEvent' eh evt@(GuildRoleCreate GuildRoleData {guildID, role}) = do updateCache evt Just guild <- getGuild guildID Just role' <- pure $ guild ^. #roles % at (getID role) pure $ map ($ (guild, role')) (getEventHandlers @'GuildRoleCreateEvt eh) handleEvent' eh evt@(GuildRoleUpdate GuildRoleData {guildID, role}) = do Just oldGuild <- getGuild guildID Just oldRole <- pure $ oldGuild ^. #roles % at (getID role) updateCache evt Just newGuild <- getGuild guildID Just newRole <- pure $ newGuild ^. #roles % at (getID role) pure $ map ($ (newGuild, oldRole, newRole)) (getEventHandlers @'GuildRoleUpdateEvt eh) handleEvent' eh evt@(GuildRoleDelete GuildRoleDeleteData {guildID, roleID}) = do Just guild <- getGuild guildID Just role <- pure $ guild ^. #roles % at roleID updateCache evt pure $ map ($ (guild, role)) (getEventHandlers @'GuildRoleDeleteEvt eh) handleEvent' eh (InviteCreate d) = do pure $ map ($ d) (getEventHandlers @'InviteCreateEvt eh) handleEvent' eh (InviteDelete d) = do pure $ map ($ d) (getEventHandlers @'InviteDeleteEvt eh) handleEvent' eh evt@(MessageCreate msg user member) = do updateCache evt pure $ map ($ (msg, user, member)) (getEventHandlers @'MessageCreateEvt eh) handleEvent' eh evt@(MessageUpdate msg user member) = do oldMsg <- getMessage (getID msg) updateCache evt newMsg <- getMessage (getID msg) let rawActions = map ($ (msg, user, member)) (getEventHandlers @'RawMessageUpdateEvt eh) let actions = case (oldMsg, newMsg) of (Just oldMsg', Just newMsg') -> map ($ (oldMsg', newMsg', user, member)) (getEventHandlers @'MessageUpdateEvt eh) _ -> [] pure $ rawActions <> actions handleEvent' eh evt@(MessageDelete MessageDeleteData {id}) = do oldMsg <- getMessage id updateCache evt let rawActions = map ($ id) (getEventHandlers @'RawMessageDeleteEvt eh) let actions = case oldMsg of Just oldMsg' -> map ($ oldMsg') (getEventHandlers @'MessageDeleteEvt eh) _ -> [] pure $ rawActions <> actions handleEvent' eh evt@(MessageDeleteBulk MessageDeleteBulkData {ids}) = do messages <- catMaybes <$> traverse getMessage ids updateCache evt let rawActions = map ($ ids) (getEventHandlers @'RawMessageDeleteBulkEvt eh) let actions = map ($ messages) (getEventHandlers @'MessageDeleteBulkEvt eh) pure $ rawActions <> actions handleEvent' eh evt@(MessageReactionAdd reaction) = do updateCache evt msg <- getMessage (getID reaction) user <- getUser (getID reaction) chan <- case reaction ^. #guildID of Just _ -> do chan <- getGuildChannel (coerceSnowflake $ getID @Channel reaction) pure (GuildChannel' <$> chan) Nothing -> do chan <- getDM (coerceSnowflake $ getID @Channel reaction) pure (DMChannel' <$> chan) let rawActions = map ($ reaction) (getEventHandlers @'RawMessageReactionAddEvt eh) let actions = case (msg, user, chan) of (Just msg', Just user', Just chan') -> map ($ (msg', user', chan', reaction ^. #emoji)) (getEventHandlers @'MessageReactionAddEvt eh) _ -> [] pure $ rawActions <> actions handleEvent' eh evt@(MessageReactionRemove reaction) = do msg <- getMessage (getID reaction) updateCache evt user <- getUser (getID reaction) chan <- case reaction ^. #guildID of Just _ -> do chan <- getGuildChannel (coerceSnowflake $ getID @Channel reaction) pure (GuildChannel' <$> chan) Nothing -> do chan <- getDM (coerceSnowflake $ getID @Channel reaction) pure (DMChannel' <$> chan) let rawActions = map ($ reaction) (getEventHandlers @'RawMessageReactionRemoveEvt eh) let actions = case (msg, user, chan) of (Just msg', Just user', Just chan') -> map ($ (msg', user', chan', reaction ^. #emoji)) (getEventHandlers @'MessageReactionRemoveEvt eh) _ -> [] pure $ rawActions <> actions handleEvent' eh evt@(MessageReactionRemoveAll MessageReactionRemoveAllData {messageID}) = do msg <- getMessage messageID updateCache evt let rawActions = map ($ messageID) (getEventHandlers @'RawMessageReactionRemoveAllEvt eh) let actions = case msg of Just msg' -> map ($ msg') (getEventHandlers @'MessageReactionRemoveAllEvt eh) _ -> [] pure $ rawActions <> actions handleEvent' eh evt@(PresenceUpdate PresenceUpdateData {userID, presence = Presence {guildID}}) = do Just oldGuild <- getGuild guildID Just oldMember <- pure $ oldGuild ^. #members % at (coerceSnowflake userID) updateCache evt Just newGuild <- getGuild guildID Just newMember <- pure $ newGuild ^. #members % at (coerceSnowflake userID) let oldUser :: User = let Member {..} = oldMember in User {..} newUser :: User = let Member {..} = newMember in User {..} userUpdates = if oldUser /= newUser then map ($ (oldUser, newUser)) (getEventHandlers @'UserUpdateEvt eh) else mempty pure $ userUpdates <> map ($ (newGuild, oldMember, newMember)) (getEventHandlers @'GuildMemberUpdateEvt eh) handleEvent' eh (TypingStart TypingStartData {channelID, guildID, userID, timestamp = UnixTimestamp timestamp}) = case guildID of Just gid -> do Just guild <- getGuild gid Just chan <- pure $ GuildChannel' <$> guild ^. #channels % at (coerceSnowflake channelID) pure $ map ($ (chan, userID, timestamp)) (getEventHandlers @'TypingStartEvt eh) Nothing -> do Just chan <- DMChannel' <<$>> getDM (coerceSnowflake channelID) pure $ map ($ (chan, userID, timestamp)) (getEventHandlers @'TypingStartEvt eh) handleEvent' eh evt@(UserUpdate _) = do Just oldUser <- getBotUser updateCache evt Just newUser <- getBotUser pure $ map ($ (oldUser, newUser)) (getEventHandlers @'UserUpdateEvt eh) handleEvent' eh evt@(VoiceStateUpdate newVoiceState@V.VoiceState {guildID = Just guildID}) = do oldVoiceState <- ((find ((== V.sessionID newVoiceState) . V.sessionID) . voiceStates) =<<) <$> getGuild guildID updateCache evt pure $ map ($ (oldVoiceState, newVoiceState)) (getEventHandlers @'VoiceStateUpdateEvt eh) handleEvent' eh evt@(InteractionCreate interaction) = do updateCache evt pure $ map ($ interaction) (getEventHandlers @'InteractionEvt eh) handleEvent' _ (UNHANDLED e) = do debug . T.pack $ "Not handling event: " <> show e pure [] handleEvent' _ e = fail $ "Unhandled event: " <> show e updateCache :: (P.Members '[CacheEff, P.Fail] r) => DispatchData -> P.Sem r () updateCache (Ready ReadyData {user, guilds}) = do setBotUser user for_ (map getID guilds) setUnavailableGuild updateCache Resumed = pure () updateCache (ChannelCreate (DMChannel' chan)) = setDM chan updateCache (ChannelCreate (GuildChannel' chan)) = updateGuild (getID chan) (#channels %~ SM.insert chan) updateCache (ChannelUpdate (DMChannel' chan)) = updateDM (getID chan) (update chan) updateCache (ChannelUpdate (GuildChannel' chan)) = updateGuild (getID chan) (#channels % ix (getID chan) %~ update chan) updateCache (ChannelDelete (DMChannel' chan)) = delDM (getID chan) updateCache (ChannelDelete (GuildChannel' chan)) = updateGuild (getID chan) (#channels %~ sans (getID chan)) updateCache (GuildCreate guild) = do isNew <- isUnavailableGuild (getID guild) when isNew $ delUnavailableGuild (getID guild) setGuild guild for_ (SM.elems (guild ^. #members)) (\Member {..} -> setUser User {..}) updateCache (GuildUpdate guild) = updateGuild (getID guild) (update guild) updateCache (GuildDelete UnavailableGuild {id, unavailable}) = if unavailable then setUnavailableGuild id else delGuild id updateCache (GuildEmojisUpdate GuildEmojisUpdateData {guildID, emojis}) = updateGuild guildID (#emojis .~ SM.fromList emojis) updateCache (GuildMemberAdd gid member) = do setUser $ (\Member {..} -> User {..}) member updateGuild gid (#members % at (getID member) ?~ member) updateCache (GuildMemberRemove GuildMemberRemoveData {guildID, user}) = updateGuild guildID (#members %~ sans (getID user)) updateCache (GuildMemberUpdate GuildMemberUpdateData {guildID, roles = AesonVector roles, user, nick}) = do setUser user updateGuild guildID (#members % ix (getID user) %~ (#roles .~ roles) . (#nick .~ nick)) updateCache (GuildMembersChunk GuildMembersChunkData {guildID, members}) = traverse_ (updateCache . GuildMemberAdd guildID) members updateCache (GuildRoleCreate GuildRoleData {guildID, role}) = updateGuild guildID (#roles %~ SM.insert role) updateCache (GuildRoleUpdate GuildRoleData {guildID, role}) = updateGuild guildID (#roles %~ SM.insert role) updateCache (GuildRoleDelete GuildRoleDeleteData {guildID, roleID}) = updateGuild guildID (#roles %~ sans roleID) updateCache (MessageCreate !msg !user !_) = do setMessage msg whenJust user $ \u -> setUser u updateCache (MessageUpdate msg !_ !_) = updateMessage (getID msg) (update msg) updateCache (MessageDelete MessageDeleteData {id}) = delMessage id updateCache (MessageDeleteBulk MessageDeleteBulkData {ids}) = for_ ids delMessage updateCache (MessageReactionAdd reaction) = do isMe <- (\u -> Just (getID @User reaction) == (getID @User <$> u)) <$> getBotUser updateMessage (getID reaction) ( \m -> case m ^. #reactions & filter ((== (reaction ^. #emoji)) . (^. #emoji)) of [] -> m & #reactions %~ (<> [Reaction 1 isMe (reaction ^. #emoji)]) _ -> m & #reactions % traversed %~ updateReactionAdd isMe (reaction ^. #emoji) ) updateCache (MessageReactionRemove reaction) = do isMe <- (\u -> Just (getID @User reaction) == (getID @User <$> u)) <$> getBotUser updateMessage (getID reaction) ( \m -> m & #reactions % traversed %~ updateReactionRemove isMe (reaction ^. #emoji) & #reactions %~ filter (\r -> r ^. #count /= 0) ) updateCache (MessageReactionRemoveAll MessageReactionRemoveAllData {messageID}) = updateMessage messageID (#reactions .~ mempty) updateCache (PresenceUpdate PresenceUpdateData {userID, presence}) = updateGuild (getID presence) (#presences % at userID ?~ presence) updateCache (UserUpdate user) = setBotUser user -- we don't handle group channels currently updateCache (ChannelCreate (GroupChannel' _)) = pure () updateCache (ChannelUpdate (GroupChannel' _)) = pure () updateCache (ChannelDelete (GroupChannel' _)) = pure () -- these don't modify state updateCache (GuildBanAdd _) = pure () updateCache (GuildBanRemove _) = pure () updateCache (GuildIntegrationsUpdate _) = pure () updateCache (TypingStart _) = pure () updateCache (ChannelPinsUpdate _) = pure () updateCache (WebhooksUpdate _) = pure () updateCache (InviteCreate _) = pure () updateCache (InviteDelete _) = pure () updateCache (VoiceStateUpdate voiceState@V.VoiceState {guildID = Just guildID}) = updateGuild guildID (#voiceStates %~ updateVoiceStates) where updateVoiceStates [] = [voiceState] updateVoiceStates (x : xs) | V.sessionID x == V.sessionID voiceState = voiceState : xs | otherwise = x : updateVoiceStates xs -- we don't handle voice server update and direct voice connections currently updateCache (VoiceStateUpdate V.VoiceState {guildID = Nothing}) = pure () updateCache (VoiceServerUpdate _) = pure () -- we don't update the cache from interactions -- TODO: should we? updateCache (InteractionCreate _) = pure () updateCache (UNHANDLED _) = pure () updateReactionAdd :: Bool -> RawEmoji -> Reaction -> Reaction updateReactionAdd isMe emoji reaction = if emoji == reaction ^. #emoji then reaction & #count %~ succ & #me %~ (|| isMe) else reaction updateReactionRemove :: Bool -> RawEmoji -> Reaction -> Reaction updateReactionRemove isMe emoji reaction = if emoji == reaction ^. #emoji then reaction & #count %~ pred & #me %~ (&& not isMe) else reaction