calamity-0.11.0.0: Calamity/Client/Types.hs
{-# LANGUAGE TemplateHaskell #-}
-- | Types for the client
module Calamity.Client.Types (
Client (..),
StartupError (..),
EventType (..),
GuildCreateStatus (..),
GuildDeleteStatus (..),
EHType,
BotC,
SetupEff,
ReactConstraints,
EventHandlers (..),
InsertEventHandler (..),
RemoveEventHandler (..),
getEventHandlers,
getCustomEventHandlers,
) where
import Calamity.Cache.Eff
import Calamity.Gateway.DispatchEvents (CalamityEvent (..), InviteCreateData, InviteDeleteData, ReactionEvtData, ReadyData)
import Calamity.Gateway.Types (ControlMessage)
import Calamity.HTTP.Internal.Ratelimit
import Calamity.HTTP.Internal.Types
import Calamity.Metrics.Eff
import Calamity.Types.LogEff
import Calamity.Types.Model.Channel
import Calamity.Types.Model.Channel.UpdatedMessage
import Calamity.Types.Model.Guild
import Calamity.Types.Model.Interaction (Interaction)
import Calamity.Types.Model.User
import Calamity.Types.Model.Voice
import Calamity.Types.Snowflake
import Calamity.Types.Token
import Calamity.Types.TokenEff
import Control.Concurrent.Async
import Control.Concurrent.Chan.Unagi
import Control.Concurrent.MVar
import Control.Concurrent.STM.TVar
import Data.Default.Class
import Data.Dynamic
import Data.IORef
import Data.Kind (Type)
import Data.Maybe
import Data.Time
import Data.TypeRepMap (TypeRepMap, WrapTypeable (..))
import Data.TypeRepMap qualified as TM
import Data.Typeable
import Data.Void (Void)
import Df1 qualified
import Di.Core qualified as DC
import GHC.Exts (fromList)
import Optics.TH
import Polysemy qualified as P
import Polysemy.Async qualified as P
import Polysemy.AtomicState qualified as P
import Polysemy.Reader qualified as P
data Client = Client
{ shards :: TVar [(InChan ControlMessage, Async (Maybe ()))]
, numShards :: MVar Int
, token :: Token
, rlState :: RateLimitState
, eventsIn :: InChan CalamityEvent
, eventsOut :: OutChan CalamityEvent
, ehidCounter :: IORef Integer
, initialDi :: Maybe (DC.Di Df1.Level Df1.Path Df1.Message)
}
-- | Constraints required by the bot client
type BotC r =
( P.Members
'[ LogEff
, MetricEff
, CacheEff
, RatelimitEff
, TokenEff
, P.Reader Client
, P.AtomicState EventHandlers
, P.Embed IO
, P.Final IO
, P.Async
]
r
)
-- | A concrete effect stack used inside the bot
type SetupEff r = (RatelimitEff ': TokenEff ': P.Reader Client ': P.AtomicState EventHandlers ': P.Async ': r)
{- | Some constraints that 'Calamity.Client.Client.react' needs to work. Don't
worry about these since they are satisfied for any type @s@ can be
-}
type ReactConstraints s =
( InsertEventHandler s
, RemoveEventHandler s
)
newtype StartupError = StartupError String
deriving stock (Show)
-- | A Data Kind used to fire custom events
data EventType
= ReadyEvt
| ChannelCreateEvt
| ChannelUpdateEvt
| ChannelDeleteEvt
| ChannelpinsUpdateEvt
| GuildCreateEvt
| GuildUpdateEvt
| GuildDeleteEvt
| GuildBanAddEvt
| GuildBanRemoveEvt
| GuildEmojisUpdateEvt
| GuildIntegrationsUpdateEvt
| GuildMemberAddEvt
| GuildMemberRemoveEvt
| GuildMemberUpdateEvt
| GuildMembersChunkEvt
| GuildRoleCreateEvt
| GuildRoleUpdateEvt
| GuildRoleDeleteEvt
| InviteCreateEvt
| InviteDeleteEvt
| MessageCreateEvt
| -- | Fired when a cached message is updated, use 'RawMessageUpdateEvt' to see
-- updates of uncached messages
MessageUpdateEvt
| -- | Fired when a message is updated
RawMessageUpdateEvt
| -- | Fired when a cached message is deleted, use 'RawMessageDeleteEvt' to see
-- deletes of uncached messages.
--
-- Does not include messages deleted through bulk deletes, use
-- 'MessageDeleteBulkEvt' for those
MessageDeleteEvt
| -- | Fired when a message is deleted.
--
-- Does not include messages deleted through bulk deletes, use
-- 'RawMessageDeleteBulkEvt' for those
RawMessageDeleteEvt
| -- | Fired when messages are bulk deleted. Only includes cached messages, use
-- 'RawMessageDeleteBulkEvt' to see deletes of uncached messages.
MessageDeleteBulkEvt
| -- | Fired when messages are bulk deleted.
RawMessageDeleteBulkEvt
| -- | Fired when a reaction is added to a cached message, use
-- 'RawMessageReactionAddEvt' to see reactions on uncached messages.
MessageReactionAddEvt
| -- | Fired when a reaction is added to a message.
RawMessageReactionAddEvt
| -- | Fired when a reaction is removed from a cached message, use
-- 'RawMessageReactionRemoveEvt' to see reactions on uncached messages.
MessageReactionRemoveEvt
| -- | Fired when a reaction is removed from a message.
RawMessageReactionRemoveEvt
| -- | Fired when all reactions are removed from a cached message, use
-- 'RawMessageReactionRemoveEvt' to see reactions on uncached messages.
--
-- The message passed will contain the removed events.
MessageReactionRemoveAllEvt
| -- | Fired when all reactions are removed from a message.
RawMessageReactionRemoveAllEvt
| TypingStartEvt
| UserUpdateEvt
| -- | Sent when someone joins/leaves/moves voice channels
VoiceStateUpdateEvt
| -- | Fired when the bot receives an interaction
InteractionEvt
| -- | A custom event, @a@ is the data sent to the handler and should probably
-- be a newtype to disambiguate events
forall (a :: Type). CustomEvt a
data GuildCreateStatus
= -- | The guild was just joined
GuildCreateNew
| -- | The guild is becoming available
GuildCreateAvailable
deriving (Show)
data GuildDeleteStatus
= -- | The guild became unavailable
GuildDeleteUnavailable
| -- | The bot was removed from the guild
GuildDeleteRemoved
deriving (Show)
{- | A type family to decide what the parameters for an event handler should be
determined by the type of event it is handling.
-}
type family EHType (d :: EventType) where
EHType 'ReadyEvt = ReadyData
EHType 'ChannelCreateEvt = Channel
EHType 'ChannelUpdateEvt = (Channel, Channel)
EHType 'ChannelDeleteEvt = Channel
EHType 'ChannelpinsUpdateEvt = (Channel, Maybe UTCTime)
EHType 'GuildCreateEvt = (Guild, GuildCreateStatus)
EHType 'GuildUpdateEvt = (Guild, Guild)
EHType 'GuildDeleteEvt = (Guild, GuildDeleteStatus)
EHType 'GuildBanAddEvt = (Guild, User)
EHType 'GuildBanRemoveEvt = (Guild, User)
EHType 'GuildEmojisUpdateEvt = (Guild, [Emoji])
EHType 'GuildIntegrationsUpdateEvt = Guild
EHType 'GuildMemberAddEvt = (Guild, Member)
EHType 'GuildMemberRemoveEvt = (Guild, Member)
EHType 'GuildMemberUpdateEvt = (Guild, Member, Member)
EHType 'GuildMembersChunkEvt = (Guild, [Member])
EHType 'GuildRoleCreateEvt = (Guild, Role)
EHType 'GuildRoleUpdateEvt = (Guild, Role, Role)
EHType 'GuildRoleDeleteEvt = (Guild, Role)
EHType 'InviteCreateEvt = InviteCreateData
EHType 'InviteDeleteEvt = InviteDeleteData
EHType 'MessageCreateEvt = (Message, Maybe User, Maybe Member)
EHType 'MessageUpdateEvt = (Message, Message, Maybe User, Maybe Member)
EHType 'MessageDeleteEvt = Message
EHType 'MessageDeleteBulkEvt = [Message]
EHType 'MessageReactionAddEvt = (Message, User, Channel, RawEmoji)
EHType 'MessageReactionRemoveEvt = (Message, User, Channel, RawEmoji)
EHType 'MessageReactionRemoveAllEvt = Message
EHType 'RawMessageUpdateEvt = (UpdatedMessage, Maybe User, Maybe Member)
EHType 'RawMessageDeleteEvt = Snowflake Message
EHType 'RawMessageDeleteBulkEvt = [Snowflake Message]
EHType 'RawMessageReactionAddEvt = ReactionEvtData
EHType 'RawMessageReactionRemoveEvt = ReactionEvtData
EHType 'RawMessageReactionRemoveAllEvt = Snowflake Message
EHType 'TypingStartEvt = (Channel, Snowflake User, UTCTime)
EHType 'UserUpdateEvt = (User, User)
EHType 'VoiceStateUpdateEvt = (Maybe VoiceState, VoiceState)
EHType 'InteractionEvt = Interaction
EHType ('CustomEvt a) = a
type StoredEHType t = EHType t -> IO ()
newtype EventHandlers = EventHandlers (TypeRepMap EventHandler)
data EventHandlerWithID (a :: Type) = EventHandlerWithID
{ ehID :: Integer
, eh :: a
}
newtype CustomEHTypeStorage (a :: Type) = CustomEHTypeStorage
{ unwrapCustomEHTypeStorage :: [EventHandlerWithID (a -> IO ())]
}
deriving newtype (Monoid, Semigroup)
type family EHStorageType (t :: EventType) where
EHStorageType ('CustomEvt _) = TypeRepMap CustomEHTypeStorage
EHStorageType t = [EventHandlerWithID (StoredEHType t)]
newtype EventHandler (t :: EventType) = EH
{ unwrapEventHandler :: (Semigroup (EHStorageType t), Monoid (EHStorageType t)) => EHStorageType t
}
instance Semigroup (EventHandler t) where
EH a <> EH b = EH $ a <> b
instance Monoid (EventHandler t) where
mempty = EH mempty
instance Default EventHandlers where
def =
EventHandlers $
fromList
[ WrapTypeable $ EH @'ReadyEvt []
, WrapTypeable $ EH @'ChannelCreateEvt []
, WrapTypeable $ EH @'ChannelUpdateEvt []
, WrapTypeable $ EH @'ChannelDeleteEvt []
, WrapTypeable $ EH @'ChannelpinsUpdateEvt []
, WrapTypeable $ EH @'GuildCreateEvt []
, WrapTypeable $ EH @'GuildUpdateEvt []
, WrapTypeable $ EH @'GuildDeleteEvt []
, WrapTypeable $ EH @'GuildBanAddEvt []
, WrapTypeable $ EH @'GuildBanRemoveEvt []
, WrapTypeable $ EH @'GuildEmojisUpdateEvt []
, WrapTypeable $ EH @'GuildIntegrationsUpdateEvt []
, WrapTypeable $ EH @'GuildMemberAddEvt []
, WrapTypeable $ EH @'GuildMemberRemoveEvt []
, WrapTypeable $ EH @'GuildMemberUpdateEvt []
, WrapTypeable $ EH @'GuildMembersChunkEvt []
, WrapTypeable $ EH @'GuildRoleCreateEvt []
, WrapTypeable $ EH @'GuildRoleUpdateEvt []
, WrapTypeable $ EH @'GuildRoleDeleteEvt []
, WrapTypeable $ EH @'MessageCreateEvt []
, WrapTypeable $ EH @'MessageUpdateEvt []
, WrapTypeable $ EH @'MessageDeleteEvt []
, WrapTypeable $ EH @'MessageDeleteBulkEvt []
, WrapTypeable $ EH @'MessageReactionAddEvt []
, WrapTypeable $ EH @'MessageReactionRemoveEvt []
, WrapTypeable $ EH @'MessageReactionRemoveAllEvt []
, WrapTypeable $ EH @'TypingStartEvt []
, WrapTypeable $ EH @'UserUpdateEvt []
, WrapTypeable $ EH @'InteractionEvt []
, WrapTypeable $ EH @('CustomEvt Void) TM.empty
]
instance Semigroup EventHandlers where
(EventHandlers a) <> (EventHandlers b) = EventHandlers $ TM.unionWith (<>) a b
instance Monoid EventHandlers where
mempty = def
-- not sure what to think of this
type family EHInstanceSelector (d :: EventType) :: Bool where
EHInstanceSelector ('CustomEvt _) = 'True
EHInstanceSelector _ = 'False
{- | A helper typeclass that is used to decide how to register regular
events, and custom events which require storing in a map at runtime.
-}
class InsertEventHandler (a :: EventType) where
makeEventHandlers :: Proxy a -> Integer -> StoredEHType a -> EventHandlers
instance (EHInstanceSelector a ~ flag, InsertEventHandler' flag a) => InsertEventHandler a where
makeEventHandlers = makeEventHandlers' (Proxy @flag)
class InsertEventHandler' (flag :: Bool) a where
makeEventHandlers' :: Proxy flag -> Proxy a -> Integer -> StoredEHType a -> EventHandlers
instance forall (x :: Type). (Typeable (EHType ('CustomEvt x))) => InsertEventHandler' 'True ('CustomEvt x) where
makeEventHandlers' _ _ id' handler =
EventHandlers . TM.one $
EH @('CustomEvt Void)
(TM.one @x $ CustomEHTypeStorage [EventHandlerWithID id' handler])
instance (Typeable s, EHStorageType s ~ [EventHandlerWithID (StoredEHType s)], Typeable (StoredEHType s)) => InsertEventHandler' 'False s where
makeEventHandlers' _ _ id' handler = EventHandlers . TM.one $ EH @s [EventHandlerWithID id' handler]
class GetEventHandlers a where
getEventHandlers :: EventHandlers -> [StoredEHType a]
instance (EHInstanceSelector a ~ flag, GetEventHandlers' flag a) => GetEventHandlers a where
getEventHandlers = getEventHandlers' (Proxy @a) (Proxy @flag)
class GetEventHandlers' (flag :: Bool) a where
getEventHandlers' :: Proxy a -> Proxy flag -> EventHandlers -> [StoredEHType a]
instance GetEventHandlers' 'True ('CustomEvt a) where
getEventHandlers' _ _ _ = error "use getCustomEventHandlers instead"
instance (Typeable s, Typeable (StoredEHType s), EHStorageType s ~ [EventHandlerWithID (StoredEHType s)]) => GetEventHandlers' 'False s where
getEventHandlers' _ _ (EventHandlers handlers) =
let theseHandlers = unwrapEventHandler @s $ fromMaybe mempty (TM.lookup handlers :: Maybe (EventHandler s))
in map eh theseHandlers
class RemoveEventHandler a where
removeEventHandler :: Proxy a -> Integer -> EventHandlers -> EventHandlers
instance (EHInstanceSelector a ~ flag, RemoveEventHandler' flag a) => RemoveEventHandler a where
removeEventHandler = removeEventHandler' (Proxy @flag)
class RemoveEventHandler' (flag :: Bool) a where
removeEventHandler' :: Proxy flag -> Proxy a -> Integer -> EventHandlers -> EventHandlers
instance forall (a :: Type). (Typeable a) => RemoveEventHandler' 'True ('CustomEvt a) where
removeEventHandler' _ _ id' (EventHandlers handlers) =
EventHandlers $
TM.adjust @('CustomEvt Void)
( \(EH ehs) ->
EH
( TM.adjust @a
(CustomEHTypeStorage . filter ((/= id') . ehID) . unwrapCustomEHTypeStorage)
ehs
)
)
handlers
instance
(Typeable s, Typeable (StoredEHType s), EHStorageType s ~ [EventHandlerWithID (StoredEHType s)]) =>
RemoveEventHandler' 'False s
where
removeEventHandler' _ _ id' (EventHandlers handlers) =
EventHandlers $
TM.adjust @s
(\(EH ehs) -> EH $ filter ((/= id') . ehID) ehs)
handlers
getCustomEventHandlers :: forall a. (Typeable a) => EventHandlers -> [a -> IO ()]
getCustomEventHandlers (EventHandlers handlers) =
let handlerMap =
unwrapEventHandler @('CustomEvt Void) $
fromMaybe mempty (TM.lookup handlers :: Maybe (EventHandler ('CustomEvt Void)))
in maybe mempty (map eh . unwrapCustomEHTypeStorage) $ TM.lookup @a handlerMap
$(makeFieldLabelsNoPrefix ''Client)