module Calamity.Client.Client
( Client(..)
, react
, runBotIO
, stopBot ) where
import Calamity.Cache.Eff
import Calamity.Client.ShardManager
import Calamity.Client.Types
import Calamity.Gateway.DispatchEvents
import Calamity.Gateway.Types
import Calamity.HTTP.Internal.Ratelimit
import qualified Calamity.Internal.SnowflakeMap as SM
import Calamity.Internal.Updateable
import Calamity.Internal.Utils
import Calamity.LogEff
import Calamity.Types.Model.Channel
import Calamity.Types.Model.Guild.UnavailableGuild
import Calamity.Types.Model.Presence ( Presence(..) )
import Calamity.Types.Snowflake
import Calamity.Types.Token
import Control.Concurrent.MVar
import Control.Concurrent.STM
import Control.Lens
import Control.Monad
import Data.Default.Class
import Data.Dynamic
import Data.Foldable
import Data.Maybe
import Data.Traversable
import qualified Data.TypeRepMap as TM
import qualified DiPolysemy as Di
import Fmt
import GHC.TypeLits
import Polysemy ( Sem )
import qualified Polysemy as P
import qualified Polysemy.Async as P
import qualified Polysemy.AtomicState as P
import qualified Polysemy.Error as P
import qualified Polysemy.Fail as P
import qualified Polysemy.Reader as P
newClient :: Token -> IO Client
newClient token = do
shards' <- newTVarIO []
numShards' <- newEmptyMVar
rlState' <- newRateLimitState
eventQueue' <- newTQueueIO
pure $ Client shards'
numShards'
token
rlState'
eventQueue'
type SetupEff r = Sem (LogEff ': P.Reader Client ': P.AtomicState EventHandlers ': P.Async ': r) ()
runBotIO :: (P.Members '[P.Embed IO, P.Final IO, CacheEff] r, Typeable r) => Token -> SetupEff r -> Sem r ()
runBotIO token setup = do
client <- P.embed $ newClient token
handlers <- P.embed $ newTVarIO def
P.asyncToIOFinal . P.runAtomicStateTVar handlers . P.runReader client . Di.runDiToStderrIO $ do
setup
shardBot
clientLoop
finishUp
react :: forall (s :: Symbol) r. (KnownSymbol s, BotC r, EHType' s ~ Dynamic, Typeable (EHType s (Sem r))) => EHType s (Sem r) -> Sem r ()
react f =
let handlers = EventHandlers . TM.one $ EH @s [toDyn f]
in P.atomicModify (handlers <>)
stopBot :: BotC r => Sem r ()
stopBot = do
debug "stopping bot"
shards <- P.asks (^. #shards) >>= P.embed . readTVarIO
for_ shards $ \shard ->
P.embed . atomically $ writeTQueue (shard ^. _1 . #cmdQueue) ShutDownShard
eventQueue <- P.asks (^. #eventQueue)
P.embed . atomically $ writeTQueue eventQueue ShutDown
finishUp :: BotC r => Sem r ()
finishUp = do
debug "finishing up"
shards <- P.asks (^. #shards) >>= P.embed . readTVarIO
for_ shards $ \shard -> void . P.await $ (shard ^. _2)
debug "bot has stopped"
clientLoop :: BotC r => Sem r ()
clientLoop = do
evtQueue <- P.asks (^. #eventQueue)
void . P.runError . forever $ do
evt' <- P.embed . atomically $ readTQueue evtQueue
case evt' of
DispatchData' evt -> P.raise $ handleEvent evt
ShutDown -> P.throw ()
debug "leaving client loop"
handleEvent :: BotC r => DispatchData -> Sem r ()
handleEvent data' = do
debug "handling an event"
eventHandlers <- P.atomicGet
actions <- P.runFail $ handleEvent' eventHandlers data'
case actions of
Right actions -> for_ actions P.async
Left err -> debug $ "Failed handling actions for event: " +| err |+ ""
unwrapEvent :: forall s r.
(KnownSymbol s, EHType' s ~ Dynamic, Typeable r, Typeable (EHType s (Sem r)))
=> EventHandlers
-> [EHType s (Sem r)]
unwrapEvent (EventHandlers eh) = map (fromJust . fromDynamic) . unwrapEventHandler @s . fromJust
$ (TM.lookup eh :: Maybe (EventHandler s))
handleEvent' :: BotC r
=> EventHandlers
-> DispatchData
-> Sem (P.Fail ': r) [Sem r ()]
handleEvent' eh evt@(Ready rd@ReadyData { user, guilds }) = do
updateCache evt
pure $ map ($ rd) (unwrapEvent @"ready" eh)
handleEvent' eh evt@(ChannelCreate (DMChannel' chan)) = do
updateCache evt
Just newChan <- DMChannel' <<$>> getDM (getID chan)
pure $ map ($ newChan) (unwrapEvent @"channelcreate" 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) (unwrapEvent @"channelcreate" eh)
handleEvent' eh evt@(ChannelUpdate (DMChannel' chan)) = do
Just oldChan <- DMChannel' <<$>> getDM (getID chan)
updateCache evt
Just newChan <- DMChannel' <<$>> getDM (getID chan)
pure $ map (\f -> f oldChan newChan) (unwrapEvent @"channelupdate" 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 (\f -> f oldChan newChan) (unwrapEvent @"channelupdate" 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 (\f -> f oldChan) (unwrapEvent @"channeldelete" eh)
handleEvent' eh evt@(ChannelDelete (DMChannel' chan)) = do
Just oldChan <- DMChannel' <<$>> getDM (getID chan)
updateCache evt
pure $ map (\f -> f oldChan) (unwrapEvent @"channeldelete" eh)
handleEvent' eh evt@(GuildCreate guild) = do
isNew <- isUnavailableGuild (getID guild)
updateCache evt
Just guild <- getGuild (getID guild)
pure $ map (\f -> f guild isNew) (unwrapEvent @"guildcreate" eh)
handleEvent' eh evt@(GuildUpdate guild) = do
Just oldGuild <- getGuild (getID guild)
updateCache evt
Just newGuild <- getGuild (getID guild)
pure $ map (\f -> f oldGuild newGuild) (unwrapEvent @"guildupdate" eh)
handleEvent' eh evt@(GuildDelete UnavailableGuild { id, unavailable }) = do
Just oldGuild <- getGuild id
updateCache evt
pure $ map (\f -> f oldGuild unavailable) (unwrapEvent @"guilddelete" eh)
handleEvent' eh evt@(GuildBanAdd GuildBanData { guildID, user }) = do
Just guild <- getGuild guildID
updateCache evt
pure $ map (\f -> f guild user) (unwrapEvent @"guildbanadd" eh)
handleEvent' eh evt@(GuildBanRemove GuildBanData { guildID, user }) = do
Just guild <- getGuild guildID
updateCache evt
pure $ map (\f -> f guild user) (unwrapEvent @"guildbanremove" eh)
handleEvent' eh evt@(GuildEmojisUpdate GuildEmojisUpdateData { guildID, emojis }) = do
Just guild <- getGuild guildID
updateCache evt
pure $ map (\f -> f guild emojis) (unwrapEvent @"guildemojisupdate" eh)
handleEvent' eh evt@(GuildIntegrationsUpdate GuildIntegrationsUpdateData { guildID }) = do
updateCache evt
Just guild <- getGuild guildID
pure $ map ($ guild) (unwrapEvent @"guildintegrationsupdate" eh)
handleEvent' eh evt@(GuildMemberAdd member) = do
updateCache evt
Just guild <- getGuild (getID member)
Just member <- pure $ guild ^. #members . at (getID member)
pure $ map ($ member) (unwrapEvent @"guildmemberadd" 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 ($ member) (unwrapEvent @"guildmemberremove" 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 (\f -> f oldMember newMember) (unwrapEvent @"guildmemberupdate" eh)
handleEvent' eh evt@(GuildMembersChunk GuildMembersChunkData { members, guildID }) = do
updateCache evt
Just guild <- getGuild guildID
let members' = guild ^.. #members . foldMap (at . getID) members . _Just
pure $ map (\f -> f guild members') (unwrapEvent @"guildmemberschunk" 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 (\f -> f guild role') (unwrapEvent @"guildrolecreate" 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 (\f -> f newGuild oldRole newRole) (unwrapEvent @"guildroleupdate" eh)
handleEvent' eh evt@(GuildRoleDelete GuildRoleDeleteData { guildID, roleID }) = do
Just guild <- getGuild guildID
Just role <- pure $ guild ^. #roles . at roleID
updateCache evt
pure $ map (\f -> f guild role) (unwrapEvent @"guildroledelete" eh)
handleEvent' eh evt@(MessageCreate msg) = do
updateCache evt
pure $ map ($ msg) (unwrapEvent @"messagecreate" eh)
handleEvent' eh evt@(MessageUpdate msg) = do
Just oldMsg <- getMessage (getID msg)
updateCache evt
Just newMsg <- getMessage (getID msg)
pure $ map (\f -> f oldMsg newMsg) (unwrapEvent @"messageupdate" eh)
handleEvent' eh evt@(MessageDelete MessageDeleteData { id }) = do
Just oldMsg <- getMessage id
updateCache evt
pure $ map ($ oldMsg) (unwrapEvent @"messagedelete" eh)
handleEvent' eh evt@(MessageDeleteBulk MessageDeleteBulkData { ids }) = do
messages <- catMaybes <$> mapM getMessage ids
updateCache evt
join <$> for messages (\msg -> pure $ map ($ msg) (unwrapEvent @"messagedelete" eh))
handleEvent' eh evt@(MessageReactionAdd reaction) = do
updateCache evt
Just msg <- getMessage (getID reaction)
pure $ map (\f -> f msg reaction) (unwrapEvent @"messagereactionadd" eh)
handleEvent' eh evt@(MessageReactionRemove reaction) = do
Just msg <- getMessage (getID reaction)
updateCache evt
pure $ map (\f -> f msg reaction) (unwrapEvent @"messagereactionremove" eh)
handleEvent' eh evt@(MessageReactionRemoveAll MessageReactionRemoveAllData { messageID }) = do
Just msg <- getMessage messageID
updateCache evt
pure $ map ($ msg) (unwrapEvent @"messagereactionremoveall" eh)
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 userUpdates = if oldMember ^. #user /= newMember ^. #user
then map (\f -> f (oldMember ^. #user) (newMember ^. #user)) (unwrapEvent @"userupdate" eh)
else mempty
pure $ userUpdates <> map (\f -> f oldMember newMember) (unwrapEvent @"guildmemberupdate" eh)
handleEvent' eh (TypingStart TypingStartData { channelID, guildID, userID, timestamp }) =
case guildID of
Just gid -> do
Just guild <- getGuild gid
Just member <- pure $ guild ^. #members . at (coerceSnowflake userID)
Just chan <- pure $ GuildChannel' <$> guild ^. #channels . at (coerceSnowflake channelID)
pure $ map (\f -> f chan (Just member) timestamp) (unwrapEvent @"typingstart" eh)
Nothing -> do
Just chan <- DMChannel' <<$>> getDM (coerceSnowflake channelID)
pure $ map (\f -> f chan Nothing timestamp) (unwrapEvent @"typingstart" eh)
handleEvent' eh evt@(UserUpdate _) = do
Just oldUser <- getBotUser
updateCache evt
Just newUser <- getBotUser
pure $ map (\f -> f oldUser newUser) (unwrapEvent @"userupdate" eh)
handleEvent' _ e = fail $ "Unhandled event: " <> show e
updateCache :: P.Members '[CacheEff, P.Fail] r => DispatchData -> Sem r ()
updateCache (Ready ReadyData { user, guilds }) = do
setBotUser user
for_ (map getID guilds) setUnavailableGuild
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 . at (getID chan) . _Just %~ 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.fromList (guild ^.. #members . traverse . #user)) setUser
updateCache (GuildUpdate guild) =
updateGuild (getID guild) (update guild)
updateCache (GuildDelete guild) =
delGuild (getID guild)
updateCache (GuildEmojisUpdate GuildEmojisUpdateData { guildID, emojis }) =
updateGuild guildID (#emojis .~ SM.fromList emojis)
updateCache (GuildMemberAdd member) = do
setUser (member ^. #user)
updateGuild (getID member) (#members . at (getID member) ?~ member)
updateCache (GuildMemberRemove GuildMemberRemoveData { guildID, user }) =
updateGuild guildID (#members %~ sans (getID user))
updateCache (GuildMemberUpdate GuildMemberUpdateData { guildID, roles, user, nick }) = do
setUser user
updateGuild guildID (#members . at (getID user) . _Just %~ (#roles .~ roles) . (#nick .~ nick))
updateCache (GuildMembersChunk GuildMembersChunkData { members }) =
traverse_ (updateCache . GuildMemberAdd) 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) = setMessage msg
updateCache (MessageUpdate msg) =
updateMessage (getID msg) (update msg)
updateCache (MessageDelete MessageDeleteData { id }) = delMessage id
updateCache (MessageDeleteBulk MessageDeleteBulkData { ids }) =
for_ ids delMessage
updateCache (MessageReactionAdd reaction) =
updateMessage (getID reaction) (#reactions %~ cons reaction)
updateCache (MessageReactionRemove reaction) =
updateMessage (getID reaction) (#reactions %~ filter (\r -> r ^. #emoji /= reaction ^. #emoji))
updateCache (MessageReactionRemoveAll MessageReactionRemoveAllData { messageID }) =
updateMessage messageID (#reactions .~ mempty)
updateCache (PresenceUpdate PresenceUpdateData { userID, roles, presence }) =
updateGuild (getID presence) ((#members . at (coerceSnowflake userID) . _Just . #roles .~ roles)
. (#presences . at userID ?~ presence))
updateCache (UserUpdate user) = setBotUser user
updateCache _data' = pure ()