diff options
| author | LLLL Colonq <llll@colonq> | 2023-11-16 19:06:43 -0500 |
|---|---|---|
| committer | LLLL Colonq <llll@colonq> | 2023-11-16 19:06:43 -0500 |
| commit | dcef0b65069fb38fd0f6c4382353167f603ebff1 (patch) | |
| tree | 45954ffe308c3dd056e6af4f734e6d2af89e5856 /deps/discord-haskell/src/Discord/Internal/Rest | |
Initial commit
Diffstat (limited to 'deps/discord-haskell/src/Discord/Internal/Rest')
12 files changed, 2206 insertions, 0 deletions
diff --git a/deps/discord-haskell/src/Discord/Internal/Rest/ApplicationCommands.hs b/deps/discord-haskell/src/Discord/Internal/Rest/ApplicationCommands.hs new file mode 100644 index 0000000..9ed33b3 --- /dev/null +++ b/deps/discord-haskell/src/Discord/Internal/Rest/ApplicationCommands.hs @@ -0,0 +1,172 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeApplications #-} + +module Discord.Internal.Rest.ApplicationCommands where + +import Data.Aeson (Value) +import Discord.Internal.Rest.Prelude +import Discord.Internal.Types +import Discord.Internal.Types.ApplicationCommands + ( ApplicationCommandPermissions, + GuildApplicationCommandPermissions(GuildApplicationCommandPermissions), + EditApplicationCommand, + CreateApplicationCommand, + ApplicationCommand ) +import Network.HTTP.Req as R + +instance Request (ApplicationCommandRequest a) where + jsonRequest = applicationCommandJsonRequest + majorRoute = applicationCommandMajorRoute + +-- | Requests related to application commands +data ApplicationCommandRequest a where + -- | Fetch all of the global commands for your application. Returns an list of 'ApplicationCommand's. + GetGlobalApplicationCommands :: ApplicationId + -> ApplicationCommandRequest [ApplicationCommand] + -- | Create a new global command. Returns an 'ApplicationCommand'. + -- + -- __Note__: Creating a command with the same name as an existing command for your application will overwrite the old command. + CreateGlobalApplicationCommand :: ApplicationId + -> CreateApplicationCommand + -> ApplicationCommandRequest ApplicationCommand + -- | Fetch a global command for your application. Returns an 'ApplicationCommand'. + GetGlobalApplicationCommand :: ApplicationId + -> ApplicationCommandId + -> ApplicationCommandRequest ApplicationCommand + -- | Edit a global command. Returns an 'ApplicationCommand'. + -- + -- All fields are optional, but any fields provided will entirely overwrite the existing values of those fields. + EditGlobalApplicationCommand :: ApplicationId + -> ApplicationCommandId + -> EditApplicationCommand + -> ApplicationCommandRequest ApplicationCommand + -- | Delete a global command. + DeleteGlobalApplicationCommand :: ApplicationId + -> ApplicationCommandId + -> ApplicationCommandRequest () + -- | Takes a list of 'CreateApplicationCommand', overwriting the existing global command list for this application. + -- + -- __Note__: This will overwrite __all__ types of application commands: slash commands, user commands, and message commands. + BulkOverWriteGlobalApplicationCommand :: ApplicationId + -> [CreateApplicationCommand] + -> ApplicationCommandRequest () + -- | Fetch all of the guild commands for your application for a specific guild. Returns an list of 'ApplicationCommands'. + GetGuildApplicationCommands :: ApplicationId + -> GuildId + -> ApplicationCommandRequest [ApplicationCommand] + -- | Create a new guild command. New guild commands will be available in the guild immediately. + -- Returns an 'ApplicationCommand'. + -- If the command did not already exist, it will count toward daily application command create limits. + -- + -- __Note__: Creating a command with the same name as an existing command for your application will overwrite the old command. + CreateGuildApplicationCommand :: ApplicationId + -> GuildId + -> CreateApplicationCommand + -> ApplicationCommandRequest ApplicationCommand + -- | Fetch a guild command for your application. Returns an 'ApplicationCommand' + GetGuildApplicationCommand :: ApplicationId + -> GuildId + -> ApplicationCommandId + -> ApplicationCommandRequest ApplicationCommand + -- | Edit a guild command. Updates for guild commands will be available immediately. Returns an 'ApplicationCommand'. + -- All fields are optional, but any fields provided will entirely overwrite the existing values of those fields. + EditGuildApplicationCommand :: ApplicationId + -> GuildId + -> ApplicationCommandId + -> CreateApplicationCommand + -> ApplicationCommandRequest ApplicationCommand + -- | Delete a guild command. + DeleteGuildApplicationCommand :: ApplicationId + -> GuildId + -> ApplicationCommandId + -> ApplicationCommandRequest () + -- | Takes a list of `CreateApplicationCommand`, overwriting the existing command list for this application for the targeted guild. + -- + -- __Note__: This will overwrite __all__ types of application commands: slash commands, user commands, and message commands. + BulkOverWriteGuildApplicationCommand :: ApplicationId + -> GuildId + -> [CreateApplicationCommand] + -> ApplicationCommandRequest () + -- | Fetches permissions for all commands for your application in a guild. + GetGuildApplicationCommandPermissions :: ApplicationId + -> GuildId + -> ApplicationCommandRequest GuildApplicationCommandPermissions + -- | Fetches permissions for a specific command for your application in a guild. + GetApplicationCommandPermissions :: ApplicationId + -> GuildId + -> ApplicationCommandId + -> ApplicationCommandRequest GuildApplicationCommandPermissions + -- | Edits command permissions for a specific command for your application. + -- You can add up to 100 permission overwrites for a command. + -- __Notes__: + -- + -- * This endpoint will overwrite existing permissions for the command in that guild + -- * This endpoint requires authentication with a Bearer token that has permission to manage the guild and its roles. + -- * Deleting or renaming a command will permanently delete all permissions for the command + EditApplicationCommandPermissions :: ApplicationId + -> GuildId + -> ApplicationCommandId + -> [ApplicationCommandPermissions] + -> ApplicationCommandRequest GuildApplicationCommandPermissions + +-- | The base url for application commands +applications :: ApplicationId -> R.Url 'R.Https +applications s = baseUrl /: "applications" /~ s + +-- | The major routes identifiers for `ApplicationCommandRequest`s +applicationCommandMajorRoute :: ApplicationCommandRequest a -> String +applicationCommandMajorRoute a = case a of + (GetGlobalApplicationCommands aid) -> "get_glob_appcomm" <> show aid + (CreateGlobalApplicationCommand aid _) -> "write_glob_appcomm" <> show aid + (GetGlobalApplicationCommand aid _) -> "get_glob_appcomm" <> show aid + (EditGlobalApplicationCommand aid _ _) -> "write_glob_appcomm" <> show aid + (DeleteGlobalApplicationCommand aid _) -> "write_glob_appcomm" <> show aid + (BulkOverWriteGlobalApplicationCommand aid _) -> "write_glob_appcomm" <> show aid + (GetGuildApplicationCommands aid _) -> "get_appcomm" <> show aid + (CreateGuildApplicationCommand aid _ _) -> "write_appcomm" <> show aid + (GetGuildApplicationCommand aid _ _) -> "get_appcomm" <> show aid + (EditGuildApplicationCommand aid _ _ _) -> "write_appcomm" <> show aid + (DeleteGuildApplicationCommand aid _ _) -> "write_appcomm" <> show aid + (BulkOverWriteGuildApplicationCommand aid _ _) -> "write_appcomm" <> show aid + (GetGuildApplicationCommandPermissions aid _) -> "appcom_perm " <> show aid + (GetApplicationCommandPermissions aid _ _) -> "appcom_perm " <> show aid + (EditApplicationCommandPermissions aid _ _ _) -> "appcom_perm " <> show aid + +-- | The `JsonRequest`s for `ApplicationCommandRequest`s +applicationCommandJsonRequest :: ApplicationCommandRequest a -> JsonRequest +applicationCommandJsonRequest a = case a of + (GetGlobalApplicationCommands aid) -> + Get (applications aid /: "commands") mempty + (CreateGlobalApplicationCommand aid cac) -> + Post (applications aid /: "commands") (convert cac) mempty + (GetGlobalApplicationCommand aid aci) -> + Get (applications aid /: "commands" /~ aci) mempty + (EditGlobalApplicationCommand aid aci eac) -> + Patch (applications aid /: "commands" /~ aci) (convert eac) mempty + (DeleteGlobalApplicationCommand aid aci) -> + Delete (applications aid /: "commands" /~ aci) mempty + (BulkOverWriteGlobalApplicationCommand aid cacs) -> + Put (applications aid /: "commands") (R.ReqBodyJson $ toJSON cacs) mempty + (GetGuildApplicationCommands aid gid) -> + Get (applications aid /: "guilds" /~ gid /: "commands") mempty + (CreateGuildApplicationCommand aid gid cac) -> + Post (applications aid /: "guilds" /~ gid /: "commands") (convert cac) mempty + (GetGuildApplicationCommand aid gid aci) -> + Get (applications aid /: "guilds" /~ gid /: "commands" /~ aci) mempty + (EditGuildApplicationCommand aid gid aci eac) -> + Patch (applications aid /: "guilds" /~ gid /: "commands" /~ aci) (convert eac) mempty + (DeleteGuildApplicationCommand aid gid aci) -> + Delete (applications aid /: "guilds" /~ gid /: "commands" /~ aci) mempty + (BulkOverWriteGuildApplicationCommand aid gid cacs) -> + Put (applications aid /: "guilds" /~ gid /: "commands") (R.ReqBodyJson $ toJSON cacs) mempty + (GetGuildApplicationCommandPermissions aid gid) -> + Get (applications aid /: "guilds" /~ gid /: "commands" /: "permissions") mempty + (GetApplicationCommandPermissions aid gid cid) -> + Get (applications aid /: "guilds" /~ gid /: "commands" /~ cid /: "permissions") mempty + (EditApplicationCommandPermissions aid gid cid ps) -> + Put (applications aid /: "guilds" /~ gid /: "commands" /~ cid /: "permissions") (R.ReqBodyJson $ toJSON (GuildApplicationCommandPermissions cid aid gid ps)) mempty + where + convert :: (ToJSON a) => a -> RestIO (ReqBodyJson Value) + convert = (pure @RestIO) . R.ReqBodyJson . toJSON diff --git a/deps/discord-haskell/src/Discord/Internal/Rest/Channel.hs b/deps/discord-haskell/src/Discord/Internal/Rest/Channel.hs new file mode 100644 index 0000000..1024d9d --- /dev/null +++ b/deps/discord-haskell/src/Discord/Internal/Rest/Channel.hs @@ -0,0 +1,607 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | Provides actions for Channel API interactions +module Discord.Internal.Rest.Channel + ( ChannelRequest(..) + , MessageDetailedOpts(..) + , AllowedMentions(..) + , ReactionTiming(..) + , MessageTiming(..) + , ChannelInviteOpts(..) + , ModifyChannelOpts(..) + , ChannelPermissionsOpts(..) + , GroupDMAddRecipientOpts(..) + , StartThreadOpts(..) + , StartThreadNoMessageOpts(..) + , ListThreads(..) + ) where + + +import Data.Aeson +import Data.Default (Default, def) +import Data.Emoji (unicodeByName) +import qualified Data.Text as T +import qualified Data.ByteString as B +import qualified Data.ByteString.Lazy as BL +import Network.HTTP.Client (RequestBody (RequestBodyBS)) +import Network.HTTP.Client.MultipartFormData (partFileRequestBody, partBS) +import Network.HTTP.Req ((/:), (/~)) +import qualified Network.HTTP.Req as R + +import Discord.Internal.Rest.Prelude +import Discord.Internal.Types +import Control.Monad (join) + +instance Request (ChannelRequest a) where + majorRoute = channelMajorRoute + jsonRequest = channelJsonRequest + +-- | Data constructor for requests. See <https://discord.com/developers/docs/resources/ API> +data ChannelRequest a where + -- | Gets a channel by its id. + GetChannel :: ChannelId -> ChannelRequest Channel + -- | Edits channels options. + ModifyChannel :: ChannelId -> ModifyChannelOpts -> ChannelRequest Channel + -- | Deletes a channel if its id doesn't equal to the id of guild. + DeleteChannel :: ChannelId -> ChannelRequest Channel + -- | Gets a messages from a channel with limit of 100 per request. + GetChannelMessages :: ChannelId -> (Int, MessageTiming) -> ChannelRequest [Message] + -- | Gets a message in a channel by its id. + GetChannelMessage :: (ChannelId, MessageId) -> ChannelRequest Message + -- | Sends a message to a channel. + CreateMessage :: ChannelId -> T.Text -> ChannelRequest Message + -- | Sends a message with granular controls. + CreateMessageDetailed :: ChannelId -> MessageDetailedOpts -> ChannelRequest Message + -- | Add an emoji reaction to a message. ID must be present for custom emoji + CreateReaction :: (ChannelId, MessageId) -> T.Text -> ChannelRequest () + -- | Remove a Reaction this bot added + DeleteOwnReaction :: (ChannelId, MessageId) -> T.Text -> ChannelRequest () + -- | Remove a Reaction someone else added + DeleteUserReaction :: (ChannelId, MessageId) -> UserId -> T.Text -> ChannelRequest () + -- | Deletes all reactions of a single emoji on a message + DeleteSingleReaction :: (ChannelId, MessageId) -> T.Text -> ChannelRequest () + -- | List of users that reacted with this emoji + GetReactions :: (ChannelId, MessageId) -> T.Text -> (Int, ReactionTiming) -> ChannelRequest [User] + -- | Delete all reactions on a message + DeleteAllReactions :: (ChannelId, MessageId) -> ChannelRequest () + -- | Edits a message content. + EditMessage :: (ChannelId, MessageId) -> MessageDetailedOpts + -> ChannelRequest Message + -- | Deletes a message. + DeleteMessage :: (ChannelId, MessageId) -> ChannelRequest () + -- | Deletes a group of messages. + BulkDeleteMessage :: (ChannelId, [MessageId]) -> ChannelRequest () + -- | Edits a permission overrides for a channel. + EditChannelPermissions :: ChannelId -> Either RoleId UserId -> ChannelPermissionsOpts -> ChannelRequest () + -- | Gets all instant invites to a channel. + GetChannelInvites :: ChannelId -> ChannelRequest Object + -- | Creates an instant invite to a channel. + CreateChannelInvite :: ChannelId -> ChannelInviteOpts -> ChannelRequest Invite + -- | Deletes a permission override from a channel. + DeleteChannelPermission :: ChannelId -> Either RoleId UserId -> ChannelRequest () + -- | Sends a typing indicator a channel which lasts 10 seconds. + TriggerTypingIndicator :: ChannelId -> ChannelRequest () + -- | Gets all pinned messages of a channel. + GetPinnedMessages :: ChannelId -> ChannelRequest [Message] + -- | Pins a message. + AddPinnedMessage :: (ChannelId, MessageId) -> ChannelRequest () + -- | Unpins a message. + DeletePinnedMessage :: (ChannelId, MessageId) -> ChannelRequest () + -- | Adds a recipient to a Group DM using their access token + GroupDMAddRecipient :: ChannelId -> GroupDMAddRecipientOpts -> ChannelRequest () + -- | Removes a recipient from a Group DM + GroupDMRemoveRecipient :: ChannelId -> UserId -> ChannelRequest () + -- | Start a thread from a message + StartThreadFromMessage :: ChannelId -> MessageId -> StartThreadOpts -> ChannelRequest Channel + -- | Start a thread without a message + StartThreadNoMessage :: ChannelId -> StartThreadNoMessageOpts -> ChannelRequest Channel + -- | Join a thread + JoinThread :: ChannelId -> ChannelRequest () + -- | Add a thread member + AddThreadMember :: ChannelId -> UserId -> ChannelRequest () + -- | Leave a thread + LeaveThread :: ChannelId -> ChannelRequest () + -- | Remove a thread member + RemoveThreadMember :: ChannelId -> UserId -> ChannelRequest () + -- | Get a thread member + GetThreadMember :: ChannelId -> UserId -> ChannelRequest ThreadMember + -- | List the thread members + ListThreadMembers :: ChannelId -> ChannelRequest [ThreadMember] + -- | List public archived threads in the given channel. Optionally before a + -- given time, and optional maximum number of threads. Returns the threads, + -- thread members, and whether there are more to collect. + -- Requires the READ_MESSAGE_HISTORY permission. + ListPublicArchivedThreads :: ChannelId -> (Maybe UTCTime, Maybe Integer) -> ChannelRequest ListThreads + -- | List private archived threads in the given channel. Optionally before a + -- given time, and optional maximum number of threads. Returns the threads, + -- thread members, and whether there are more to collect. + -- Requires both the READ_MESSAGE_HISTORY and MANAGE_THREADS permissions. + ListPrivateArchivedThreads :: ChannelId -> (Maybe UTCTime, Maybe Integer) -> ChannelRequest ListThreads + -- | List joined private archived threads in the given channel. Optionally + -- before a given time, and optional maximum number of threads. Returns the + -- threads, thread members, and whether there are more to collect. + -- Requires both the READ_MESSAGE_HISTORY and MANAGE_THREADS permissions. + ListJoinedPrivateArchivedThreads :: ChannelId -> (Maybe UTCTime, Maybe Integer) -> ChannelRequest ListThreads + + +-- | Options for `CreateMessageDetailed` requests. +data MessageDetailedOpts = MessageDetailedOpts + { -- | The message contents (up to 2000 characters) + messageDetailedContent :: T.Text + , -- | `True` if this is a TTS message + messageDetailedTTS :: Bool + , -- | embedded rich content (up to 6000 characters) + messageDetailedEmbeds :: Maybe [CreateEmbed] + , -- | the contents of the file being sent + messageDetailedFile :: Maybe (T.Text, B.ByteString) + , -- | allowed mentions for the message + messageDetailedAllowedMentions :: Maybe AllowedMentions + , -- | If `Just`, reply to the message referenced + messageDetailedReference :: Maybe MessageReference + , -- | Message components for the message + messageDetailedComponents :: Maybe [ActionRow] + , -- | IDs of up to 3 `Sticker` in the server to send with the message + messageDetailedStickerIds :: Maybe [StickerId] + } deriving (Show, Read, Eq, Ord) + +instance Default MessageDetailedOpts where + def = MessageDetailedOpts { messageDetailedContent = "" + , messageDetailedTTS = False + , messageDetailedEmbeds = Nothing + , messageDetailedFile = Nothing + , messageDetailedAllowedMentions = Nothing + , messageDetailedReference = Nothing + , messageDetailedComponents = Nothing + , messageDetailedStickerIds = Nothing + } + +-- | Data constructor for `GetReactions` requests +data ReactionTiming = BeforeReaction MessageId + | AfterReaction MessageId + | LatestReaction + deriving (Show, Read, Eq, Ord) + +reactionTimingToQuery :: ReactionTiming -> R.Option 'R.Https +reactionTimingToQuery t = case t of + (BeforeReaction snow) -> "before" R.=: show snow + (AfterReaction snow) -> "after" R.=: show snow + LatestReaction -> mempty + +-- | Data constructor for `GetChannelMessages` requests. +-- +-- See <https://discord.com/developers/docs/resources/channel#get-channel-messages> +data MessageTiming = AroundMessage MessageId + | BeforeMessage MessageId + | AfterMessage MessageId + | LatestMessages + deriving (Show, Read, Eq, Ord) + +messageTimingToQuery :: MessageTiming -> R.Option 'R.Https +messageTimingToQuery t = case t of + (AroundMessage snow) -> "around" R.=: show snow + (BeforeMessage snow) -> "before" R.=: show snow + (AfterMessage snow) -> "after" R.=: show snow + LatestMessages -> mempty + +-- | Options for `CreateChannelInvite` requests +data ChannelInviteOpts = ChannelInviteOpts + { -- | How long the invite is valid for (in seconds) + channelInviteOptsMaxAgeSeconds :: Maybe Integer + , -- | How many uses the invite is valid for + channelInviteOptsMaxUsages :: Maybe Integer + , -- | Whether this invite only grants temporary membership + channelInviteOptsIsTemporary :: Maybe Bool + , -- | Don't reuse a similar invite. Useful for creating many unique one time + -- use invites + channelInviteOptsDontReuseSimilarInvite :: Maybe Bool + } deriving (Show, Read, Eq, Ord) + +instance ToJSON ChannelInviteOpts where + toJSON ChannelInviteOpts{..} = objectFromMaybes + ["max_age" .=? channelInviteOptsMaxAgeSeconds, + "max_uses" .=? channelInviteOptsMaxUsages, + "temporary" .=? channelInviteOptsIsTemporary, + "unique" .=? channelInviteOptsDontReuseSimilarInvite ] + +-- | Options for `ModifyChannel` requests +data ModifyChannelOpts = ModifyChannelOpts + { -- | (All) The name of the channel (max 100 characters) + modifyChannelName :: Maybe T.Text + , -- | (All) Position of the channel in the listing + modifyChannelPosition :: Maybe Integer + , -- | (Text) The channel topic text (max 1024 characters) + modifyChannelTopic :: Maybe T.Text + , -- | (Text) Wether the channel is tagged as NSFW + modifyChannelNSFW :: Maybe Bool + , -- | (Voice) Bitrate (in bps) of a voice channel. Min 8000, max 96000 + -- (128000 for boosted servers) + modifyChannelBitrate :: Maybe Integer + , -- | (Text) The rate limit of the channel, in seconds (0-21600), does not + -- affect bots and users with @manage_channel@ or @manage_messages@ + -- permissons + modifyChannelUserRateLimit :: Maybe Integer + , -- | (Voice) the user limit of the voice channel, max 99 + modifyChannelUserLimit :: Maybe Integer + , -- | (All) The channel permissions + modifyChannelPermissionOverwrites :: Maybe [Overwrite] + , -- | (All) The parent category of the channel + modifyChannelParentId :: Maybe ChannelId + , -- | (Text) Auto-archive duration for Threads + modifyChannelDefaultAutoArchive :: Maybe Integer + , -- | (Thread) Whether the thread is archived + modifyChannelThreadArchived :: Maybe Bool + , -- | (Thread) duration in minutes to automatically archive the thread after + -- recent activity, can be set to: 60, 1440, 4320 or 10080 + modifyChannelThreadAutoArchive :: Maybe Integer + , -- | (Thread) Whether the thread is locked. When a thread is locked, only + -- users with @manage_threads@ can unarchive it + modifyChannelThreadLocked :: Maybe Bool + , -- | (Thread) Whether non-moderators can add other non-moderators to a + -- thread. Only available on private threads + modifyChannelThreadInvitable :: Maybe Bool + } deriving (Show, Read, Eq, Ord) + +instance Default ModifyChannelOpts where + def = ModifyChannelOpts Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing + +instance ToJSON ModifyChannelOpts where + toJSON ModifyChannelOpts{..} = objectFromMaybes + ["name" .=? modifyChannelName, + "position" .=? modifyChannelPosition, + "topic" .=? modifyChannelTopic, + "nsfw" .=? modifyChannelNSFW, + "bitrate" .=? modifyChannelBitrate, + "rate_limit_per_user" .=? modifyChannelUserRateLimit, + "user_limit" .=? modifyChannelUserLimit, + "permission_overwrites" .=? modifyChannelPermissionOverwrites, + "parent_id" .=? modifyChannelParentId, + "default_auto_archive_duration" .=? modifyChannelDefaultAutoArchive, + "archived" .=? modifyChannelThreadArchived, + "auto_archive_duration" .=? modifyChannelThreadAutoArchive, + "locked" .=? modifyChannelThreadLocked, + "invitable" .=? modifyChannelThreadInvitable ] + +-- | Options for The `EditChannelPermissions` request +-- +-- Since the JSON encoding of this datatype will require information in the +-- route (the Either decides whether the overwrite is for a user or a role), we +-- do not provide a ToJSON instance. Instead, the JSON is manually constructed +-- in the 'channelJsonRequest' function. +data ChannelPermissionsOpts = ChannelPermissionsOpts + { -- | The permission integer for the explicitly allowed permissions + channelPermissionsOptsAllow :: Integer + , -- | The permission integer for the explicitly denied permissions + channelPermissionsOptsDeny :: Integer + } deriving (Show, Read, Eq, Ord) + +-- | Options for `GroupDMAddRecipient` request +-- +-- See <https://discord.com/developers/docs/resources/channel#group-dm-add-recipient> +data GroupDMAddRecipientOpts = GroupDMAddRecipientOpts + { -- | The id of the user to add to the Group DM + groupDMAddRecipientUserToAdd :: UserId + , -- | The nickname given to the user being added + groupDMAddRecipientUserToAddNickName :: T.Text + , -- | Access token of the user. That user must have granted your app the + -- @gdm.join@ scope. + groupDMAddRecipientGDMJoinAccessToken :: T.Text + } deriving (Show, Read, Eq, Ord) + +-- | Options for `StartThreadFromMessage` request +data StartThreadOpts = StartThreadOpts + { -- | Name of the thread + startThreadName :: T.Text + , -- | Period of innactivity after which the thread gets archived in minutes. + -- + -- Can be one of 60, 1440, 4320, 10080 + startThreadAutoArchive :: Maybe Integer + , -- | Amount of seconds a user has to wait before sending another message + -- (0-21600) + startThreadRateLimit :: Maybe Integer + } deriving (Show, Read, Eq, Ord) + +instance ToJSON StartThreadOpts where + toJSON StartThreadOpts{..} = objectFromMaybes + [ "name" .== startThreadName + , "auto_archive_duration" .=? startThreadAutoArchive + , "rate_limit_per_user" .=? startThreadRateLimit + ] + +-- | Options for `StartThreadNoMessage` request +data StartThreadNoMessageOpts = StartThreadNoMessageOpts + { -- | Base options for the thread + startThreadNoMessageBaseOpts :: StartThreadOpts + , -- | The type of thread to create + -- + -- Can be @10@, @11@, or @12@. See + -- <https://discord.com/developers/docs/resources/channel#channel-object-channel-types> + startThreadNoMessageType :: Integer + , -- | Whether non-moderators can add other non-moderators to a thread. Only + -- available when creating a private thread. + startThreadNoMessageInvitable :: Maybe Bool + } deriving (Show, Read, Eq, Ord) + +instance ToJSON StartThreadNoMessageOpts where + toJSON StartThreadNoMessageOpts{..} = objectFromMaybes + [ "name" .== startThreadName startThreadNoMessageBaseOpts + , "auto_archive_duration" .=? startThreadAutoArchive startThreadNoMessageBaseOpts + , "rate_limit_per_user" .=? startThreadRateLimit startThreadNoMessageBaseOpts + , "type" .== startThreadNoMessageType + , "invitable" .=? startThreadNoMessageInvitable + ] + +-- | Result type of `ListJoinedPrivateArchivedThreads`, +-- `ListPrivateArchivedThreads` and `ListPublicArchivedThreads` +data ListThreads = ListThreads + { -- | The returned threads + listThreadsThreads :: [Channel] + , -- | A thread member object for each returned thread the current user has + -- joined + listThreadsMembers :: [ThreadMember] + , -- | Whether there is more data to retrieve + listThreadsHasMore :: Bool + } deriving (Show, Read, Eq, Ord) + +instance ToJSON ListThreads where + toJSON ListThreads{..} = object + [ ("threads", toJSON listThreadsThreads) + , ("members", toJSON listThreadsMembers) + , ("has_more", toJSON listThreadsHasMore) + ] + +instance FromJSON ListThreads where + parseJSON = withObject "ListThreads" $ \o -> + ListThreads <$> o .: "threads" + <*> o .: "members" + <*> o .: "has_more" + +channelMajorRoute :: ChannelRequest a -> String +channelMajorRoute c = case c of + (GetChannel chan) -> "get_chan " <> show chan + (ModifyChannel chan _) -> "mod_chan " <> show chan + (DeleteChannel chan) -> "mod_chan " <> show chan + (GetChannelMessages chan _) -> "msg " <> show chan + (GetChannelMessage (chan, _)) -> "get_msg " <> show chan + (CreateMessage chan _) -> "msg " <> show chan + (CreateMessageDetailed chan _) -> "msg " <> show chan + (CreateReaction (chan, _) _) -> "add_react " <> show chan + (DeleteOwnReaction (chan, _) _) -> "react " <> show chan + (DeleteUserReaction (chan, _) _ _) -> "react " <> show chan + (DeleteSingleReaction (chan, _) _) -> "react " <> show chan + (GetReactions (chan, _) _ _) -> "react " <> show chan + (DeleteAllReactions (chan, _)) -> "react " <> show chan + (EditMessage (chan, _) _) -> "get_msg " <> show chan + (DeleteMessage (chan, _)) -> "get_msg " <> show chan + (BulkDeleteMessage (chan, _)) -> "del_msgs " <> show chan + (EditChannelPermissions chan _ _) -> "perms " <> show chan + (GetChannelInvites chan) -> "invites " <> show chan + (CreateChannelInvite chan _) -> "invites " <> show chan + (DeleteChannelPermission chan _) -> "perms " <> show chan + (TriggerTypingIndicator chan) -> "tti " <> show chan + (GetPinnedMessages chan) -> "pins " <> show chan + (AddPinnedMessage (chan, _)) -> "pin " <> show chan + (DeletePinnedMessage (chan, _)) -> "pin " <> show chan + (GroupDMAddRecipient chan _) -> "groupdm " <> show chan + (GroupDMRemoveRecipient chan _) -> "groupdm " <> show chan + (StartThreadFromMessage chan _ _) -> "thread " <> show chan + (StartThreadNoMessage chan _) -> "thread " <> show chan + (JoinThread chan) -> "thread " <> show chan + (AddThreadMember chan _) -> "thread " <> show chan + (LeaveThread chan) -> "thread " <> show chan + (RemoveThreadMember chan _) -> "thread " <> show chan + (GetThreadMember chan _) -> "thread " <> show chan + (ListThreadMembers chan) -> "thread " <> show chan + (ListPublicArchivedThreads chan _) -> "thread " <> show chan + (ListPrivateArchivedThreads chan _) -> "thread " <> show chan + (ListJoinedPrivateArchivedThreads chan _) -> "thread " <> show chan + +cleanupEmoji :: T.Text -> T.Text +cleanupEmoji emoji = + let noAngles = T.replace "<" "" (T.replace ">" "" emoji) + byName = T.pack <$> unicodeByName (T.unpack (T.replace ":" "" emoji)) + in case (byName, T.stripPrefix ":" noAngles) of + (Just e, _) -> e + (_, Just a) -> "custom:" <> a + (_, Nothing) -> noAngles + +channels :: R.Url 'R.Https +channels = baseUrl /: "channels" + +channelJsonRequest :: ChannelRequest r -> JsonRequest +channelJsonRequest c = case c of + (GetChannel chan) -> + Get (channels /~ chan) mempty + + (ModifyChannel chan patch) -> + Patch (channels /~ chan) (pure (R.ReqBodyJson patch)) mempty + + (DeleteChannel chan) -> + Delete (channels /~ chan) mempty + + (GetChannelMessages chan (n,timing)) -> + let n' = max 1 (min 100 n) + options = "limit" R.=: n' <> messageTimingToQuery timing + in Get (channels /~ chan /: "messages") options + + (GetChannelMessage (chan, msg)) -> + Get (channels /~ chan /: "messages" /~ msg) mempty + + (CreateMessage chan msg) -> + let content = ["content" .= msg] + body = pure $ R.ReqBodyJson $ object content + in Post (channels /~ chan /: "messages") body mempty + + (CreateMessageDetailed chan msgOpts) -> + let fileUpload = messageDetailedFile msgOpts + filePart = + ( case fileUpload of + Nothing -> [] + Just f -> + [ partFileRequestBody + "file" + (T.unpack $ fst f) + (RequestBodyBS $ snd f) + ] + ) + ++ join (maybe [] (maybeEmbed . Just <$>) (messageDetailedEmbeds msgOpts)) + + payloadData = objectFromMaybes $ + [ "content" .== messageDetailedContent msgOpts + , "tts" .== messageDetailedTTS msgOpts ] ++ + [ "embeds" .=? ((createEmbed <$>) <$> messageDetailedEmbeds msgOpts) + , "allowed_mentions" .=? messageDetailedAllowedMentions msgOpts + , "message_reference" .=? messageDetailedReference msgOpts + , "components" .=? messageDetailedComponents msgOpts + , "sticker_ids" .=? messageDetailedStickerIds msgOpts + ] + payloadPart = partBS "payload_json" $ BL.toStrict $ encode payloadData + + body = R.reqBodyMultipart (payloadPart : filePart) + in Post (channels /~ chan /: "messages") body mempty + + (CreateReaction (chan, msgid) emoji) -> + let e = cleanupEmoji emoji + in Put (channels /~ chan /: "messages" /~ msgid /: "reactions" /: e /: "@me" ) + R.NoReqBody mempty + + (DeleteOwnReaction (chan, msgid) emoji) -> + let e = cleanupEmoji emoji + in Delete (channels /~ chan /: "messages" /~ msgid /: "reactions" /: e /: "@me" ) mempty + + (DeleteUserReaction (chan, msgid) uID emoji) -> + let e = cleanupEmoji emoji + in Delete (channels /~ chan /: "messages" /~ msgid /: "reactions" /: e /~ uID ) mempty + + (DeleteSingleReaction (chan, msgid) emoji) -> + let e = cleanupEmoji emoji + in Delete (channels /~ chan /: "messages" /~ msgid /: "reactions" /: e) mempty + + (GetReactions (chan, msgid) emoji (n, timing)) -> + let e = cleanupEmoji emoji + n' = max 1 (min 100 n) + options = "limit" R.=: n' <> reactionTimingToQuery timing + in Get (channels /~ chan /: "messages" /~ msgid /: "reactions" /: e) options + + (DeleteAllReactions (chan, msgid)) -> + Delete (channels /~ chan /: "messages" /~ msgid /: "reactions" ) mempty + + -- copied from CreateMessageDetailed, should be outsourced to function probably + (EditMessage (chan, msg) msgOpts) -> + let fileUpload = messageDetailedFile msgOpts + filePart = + ( case fileUpload of + Nothing -> [] + Just f -> + [ partFileRequestBody + "file" + (T.unpack $ fst f) + (RequestBodyBS $ snd f) + ] + ) + ++ join (maybe [] (maybeEmbed . Just <$>) (messageDetailedEmbeds msgOpts)) + + payloadData = objectFromMaybes $ + [ "content" .== messageDetailedContent msgOpts + , "tts" .== messageDetailedTTS msgOpts ] ++ + [ "embeds" .=? ((createEmbed <$>) <$> messageDetailedEmbeds msgOpts) + , "allowed_mentions" .=? messageDetailedAllowedMentions msgOpts + , "message_reference" .=? messageDetailedReference msgOpts + , "components" .=? messageDetailedComponents msgOpts + , "sticker_ids" .=? messageDetailedStickerIds msgOpts + ] + payloadPart = partBS "payload_json" $ BL.toStrict $ encode payloadData + + body = R.reqBodyMultipart (payloadPart : filePart) + in Patch (channels /~ chan /: "messages" /~ msg) body mempty + + (DeleteMessage (chan, msg)) -> + Delete (channels /~ chan /: "messages" /~ msg) mempty + + (BulkDeleteMessage (chan, msgs)) -> + let body = pure . R.ReqBodyJson $ object ["messages" .= msgs] + in Post (channels /~ chan /: "messages" /: "bulk-delete") body mempty + + (EditChannelPermissions chan overwriteId (ChannelPermissionsOpts a d)) -> + let body = R.ReqBodyJson $ object [("type", toJSON (either (const 0) (const 1) overwriteId :: Int)) + ,("allow", toJSON a) + ,("deny", toJSON d)] + in Put (channels /~ chan /: "permissions" /~ either unId unId overwriteId) body mempty + + (GetChannelInvites chan) -> + Get (channels /~ chan /: "invites") mempty + + (CreateChannelInvite chan patch) -> + Post (channels /~ chan /: "invites") (pure (R.ReqBodyJson patch)) mempty + + (DeleteChannelPermission chan overwriteId) -> + Delete (channels /~ chan /: "permissions" /~ either unId unId overwriteId) mempty + + (TriggerTypingIndicator chan) -> + Post (channels /~ chan /: "typing") (pure R.NoReqBody) mempty + + (GetPinnedMessages chan) -> + Get (channels /~ chan /: "pins") mempty + + (AddPinnedMessage (chan, msg)) -> + Put (channels /~ chan /: "pins" /~ msg) R.NoReqBody mempty + + (DeletePinnedMessage (chan, msg)) -> + Delete (channels /~ chan /: "pins" /~ msg) mempty + + (GroupDMAddRecipient chan (GroupDMAddRecipientOpts uid nick tok)) -> + Put (channels /~ chan /~ chan /: "recipients" /~ uid) + (R.ReqBodyJson (object [ ("access_token", toJSON tok) + , ("nick", toJSON nick)])) + mempty + + (GroupDMRemoveRecipient chan userid) -> + Delete (channels /~ chan /~ chan /: "recipients" /~ userid) mempty + + (StartThreadFromMessage chan mid sto) -> + Post (channels /~ chan /: "messages" /~ mid /: "threads") + (pure $ R.ReqBodyJson $ toJSON sto) + mempty + + (StartThreadNoMessage chan sto) -> + Post (channels /~ chan /: "messages" /: "threads") + (pure $ R.ReqBodyJson $ toJSON sto) + mempty + + (JoinThread chan) -> + Put (channels /~ chan /: "thread-members" /: "@me") + R.NoReqBody mempty + + (AddThreadMember chan uid) -> + Put (channels /~ chan /: "thread-members" /~ uid) + R.NoReqBody mempty + + (LeaveThread chan) -> + Delete (channels /~ chan /: "thread-members" /: "@me") + mempty + + (RemoveThreadMember chan uid) -> + Delete (channels /~ chan /: "thread-members" /~ uid) + mempty + + (GetThreadMember chan uid) -> + Get (channels /~ chan /: "thread-members" /~ uid) + mempty + + (ListThreadMembers chan) -> + Get (channels /~ chan /: "thread-members") + mempty + + (ListPublicArchivedThreads chan (time, lim)) -> + Get (channels /~ chan /: "threads" /: "archived" /: "public") + (maybe mempty ("limit" R.=:) lim <> maybe mempty ("before" R.=:) time) + + (ListPrivateArchivedThreads chan (time, lim)) -> + Get (channels /~ chan /: "threads" /: "archived" /: "private") + (maybe mempty ("limit" R.=:) lim <> maybe mempty ("before" R.=:) time) + + (ListJoinedPrivateArchivedThreads chan (time, lim)) -> + Get (channels /~ chan /: "users" /: "@me" /: "threads" /: "archived" /: "private") + (maybe mempty ("limit" R.=:) lim <> maybe mempty ("before" R.=:) time) diff --git a/deps/discord-haskell/src/Discord/Internal/Rest/Emoji.hs b/deps/discord-haskell/src/Discord/Internal/Rest/Emoji.hs new file mode 100644 index 0000000..2a52171 --- /dev/null +++ b/deps/discord-haskell/src/Discord/Internal/Rest/Emoji.hs @@ -0,0 +1,201 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} + +-- | Provides actions for Channel API interactions +module Discord.Internal.Rest.Emoji + ( EmojiRequest (..), + ModifyGuildEmojiOpts (..), + parseEmojiImage, + parseStickerImage, + StickerRequest (..), + CreateGuildStickerOpts (..), + EditGuildStickerOpts (..) + ) +where + +import Data.Aeson +import qualified Data.ByteString as B +import qualified Data.ByteString.Base64 as B64 +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE +import Discord.Internal.Rest.Prelude +import Discord.Internal.Types +import Network.HTTP.Req ((/:), (/~)) +import qualified Network.HTTP.Req as R + +instance Request (EmojiRequest a) where + majorRoute = emojiMajorRoute + jsonRequest = emojiJsonRequest + +-- | Data constructor for requests. See <https://discord.com/developers/docs/resources/ API> +data EmojiRequest a where + -- | List of emoji objects for the given guild. Requires MANAGE_EMOJIS permission. + ListGuildEmojis :: GuildId -> EmojiRequest [Emoji] + -- | Emoji object for the given guild and emoji ID + GetGuildEmoji :: GuildId -> EmojiId -> EmojiRequest Emoji + -- | Create a new guild emoji (static&animated). Requires MANAGE_EMOJIS permission. + CreateGuildEmoji :: GuildId -> T.Text -> Base64Image Emoji -> EmojiRequest Emoji + -- | Requires MANAGE_EMOJIS permission + ModifyGuildEmoji :: GuildId -> EmojiId -> ModifyGuildEmojiOpts -> EmojiRequest Emoji + -- | Requires MANAGE_EMOJIS permission + DeleteGuildEmoji :: GuildId -> EmojiId -> EmojiRequest () + +data ModifyGuildEmojiOpts = ModifyGuildEmojiOpts + { modifyGuildEmojiName :: T.Text, + modifyGuildEmojiRoles :: [RoleId] + } + deriving (Show, Read, Eq, Ord) + +instance ToJSON ModifyGuildEmojiOpts where + toJSON (ModifyGuildEmojiOpts name roles) = + object ["name" .= name, "roles" .= roles] + + +-- | @parseEmojiImage bs@ will attempt to convert the given image bytestring @bs@ +-- to the base64 format expected by the Discord API. It may return Left with an +-- error reason if either the bytestring is too large, or if the image format +-- could not be predetermined from the opening few bytes. This function does +-- /not/ validate the rest of the image, nor check that its dimensions are +-- 128x128 as required by Discord. This is up to the library user to check. +-- +-- This function accepts all file types accepted by 'getMimeType'. +parseEmojiImage :: B.ByteString -> Either T.Text (Base64Image Emoji) +parseEmojiImage bs + | B.length bs > 256000 = Left "Cannot create emoji - File is larger than 256kb" + | Just mime <- getMimeType bs = Right (Base64Image mime (TE.decodeUtf8 (B64.encode bs))) + | otherwise = Left "Unsupported image format provided" + +emojiMajorRoute :: EmojiRequest a -> String +emojiMajorRoute c = case c of + (ListGuildEmojis g) -> "emoji " <> show g + (GetGuildEmoji g _) -> "emoji " <> show g + (CreateGuildEmoji g _ _) -> "emoji " <> show g + (ModifyGuildEmoji g _ _) -> "emoji " <> show g + (DeleteGuildEmoji g _) -> "emoji " <> show g + +guilds :: R.Url 'R.Https +guilds = baseUrl /: "guilds" + +emojiJsonRequest :: EmojiRequest r -> JsonRequest +emojiJsonRequest c = case c of + (ListGuildEmojis g) -> Get (guilds /~ g /: "emojis") mempty + (GetGuildEmoji g e) -> Get (guilds /~ g /: "emojis" /~ e) mempty + (CreateGuildEmoji g name b64im) -> + Post + (guilds /~ g /: "emojis") + ( pure + ( R.ReqBodyJson + ( object + [ "name" .= name, + "image" .= b64im + -- todo , "roles" .= ... + ] + ) + ) + ) + mempty + (ModifyGuildEmoji g e o) -> + Patch + (guilds /~ g /: "emojis" /~ e) + (pure (R.ReqBodyJson o)) + mempty + (DeleteGuildEmoji g e) -> Delete (guilds /~ g /: "emojis" /~ e) mempty + +-- | @parseStickerImage bs@ accepts PNG, APNG, or Lottie JSON bytestring @bs@ and +-- will attempt to convert it to the base64 format expected by the Discord API. +-- It may return Left with an error reason if the image format is unexpected. +-- This function does /not/ validate the contents of the image, this is up to +-- the library user to check. +parseStickerImage :: B.ByteString -> Either T.Text (Base64Image Sticker) +parseStickerImage bs + | B.length bs > 512000 + = Left "Cannot create sticker - File is larger than 512kb" + | Just "image/png" <- getMimeType bs + = Right (Base64Image "image/png" (TE.decodeUtf8 (B64.encode bs))) + | not (B.null bs) && B.head bs == 0x7b -- '{' + = Right (Base64Image "application/json" (TE.decodeUtf8 (B64.encode bs))) + | otherwise + = Left "Unsupported image format provided" + +-- | Options for `CreateGuildSticker` +data CreateGuildStickerOpts = CreateGuildStickerOpts + { guildStickerName :: T.Text, + guildStickerDescription :: T.Text, + guildStickerTags :: [T.Text], + guildStickerFile :: Base64Image Sticker + } + deriving (Show, Read, Eq, Ord) + +instance ToJSON CreateGuildStickerOpts where + toJSON (CreateGuildStickerOpts name desc tags b64im) = + object + [ ("name", toJSON name), + ("description", toJSON desc), + ("tags", toJSON $ T.intercalate "," tags), + ("file", toJSON b64im) + ] + +-- | Options for `ModifyGuildSticker` +data EditGuildStickerOpts = EditGuildStickerOpts + { editGuildStickerName :: Maybe T.Text, + editGuildStickerDescription :: Maybe T.Text, + editGuildStickerTags :: Maybe [T.Text] + } + deriving (Show, Read, Eq, Ord) + +instance ToJSON EditGuildStickerOpts where + toJSON EditGuildStickerOpts {..} = + objectFromMaybes + [ "name" .=? editGuildStickerName, + "description" .=? editGuildStickerDescription, + "tags" .=? fmap (T.intercalate ",") editGuildStickerTags + ] + +instance Request (StickerRequest a) where + majorRoute = stickerMajorRoute + jsonRequest = stickerJsonRequest + +-- | Data constructor for requests. See <https://discord.com/developers/docs/resources/ API> +-- +-- Be warned that these are untested due to not having a spare server with +-- boosts. Functionality is at your own risk. +data StickerRequest a where + -- | Returns a sticker object for the given sticker ID. + GetSticker :: StickerId -> StickerRequest Sticker + -- | Returns the list of sticker packs available to Nitro subscribers. + ListNitroStickerPacks :: StickerRequest [StickerPack] + -- | Returns an array of sticker objects for the given guild. + ListGuildStickers :: GuildId -> StickerRequest [Sticker] + -- | Returns a sticker object for the given guild and sticker ID. + GetGuildSticker :: GuildId -> StickerId -> StickerRequest Sticker + -- | Create a new sticker for the guild. + CreateGuildSticker :: GuildId -> CreateGuildStickerOpts -> StickerRequest Sticker + -- | Modify a sticker for a guild. + ModifyGuildSticker :: GuildId -> StickerId -> EditGuildStickerOpts -> StickerRequest Sticker + -- | Delete a guild sticker + DeleteGuildSticker :: GuildId -> StickerId -> StickerRequest () + +stickerMajorRoute :: StickerRequest a -> String +stickerMajorRoute = \case + GetSticker gid -> "sticker " <> show gid + ListNitroStickerPacks -> "sticker" + ListGuildStickers gid -> "sticker " <> show gid + GetGuildSticker gid _ -> "sticker " <> show gid + CreateGuildSticker gid _ -> "sticker " <> show gid + ModifyGuildSticker gid _ _ -> "sticker " <> show gid + DeleteGuildSticker gid _ -> "sticker " <> show gid + +stickerJsonRequest :: StickerRequest a -> JsonRequest +stickerJsonRequest = \case + GetSticker gid -> Get (baseUrl /: "stickers" /~ gid) mempty + ListNitroStickerPacks -> Get (baseUrl /: "sticker-packs") mempty + ListGuildStickers gid -> Get (stickersGuild gid) mempty + GetGuildSticker gid sid -> Get (stickersGuild gid /~ sid) mempty + CreateGuildSticker gid cgso -> Post (stickersGuild gid) (pure $ R.ReqBodyJson $ toJSON cgso) mempty + ModifyGuildSticker gid sid egso -> Patch (stickersGuild gid /~ sid) (pure $ R.ReqBodyJson egso) mempty + DeleteGuildSticker gid sid -> Delete (stickersGuild gid /~ sid) mempty + where + stickersGuild gid = baseUrl /: "guilds" /~ gid /: "stickers" diff --git a/deps/discord-haskell/src/Discord/Internal/Rest/Guild.hs b/deps/discord-haskell/src/Discord/Internal/Rest/Guild.hs new file mode 100644 index 0000000..a0cb3aa --- /dev/null +++ b/deps/discord-haskell/src/Discord/Internal/Rest/Guild.hs @@ -0,0 +1,468 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | Provides actions for Channel API interactions +module Discord.Internal.Rest.Guild + ( GuildRequest(..) + , CreateGuildChannelOpts(..) + , ModifyGuildOpts(..) + , AddGuildMemberOpts(..) + , ModifyGuildMemberOpts(..) + , GuildMembersTiming(..) + , CreateGuildBanOpts(..) + , ModifyGuildRoleOpts(..) + , CreateGuildIntegrationOpts(..) + , ModifyGuildIntegrationOpts(..) + ) where + + +import Data.Aeson +import Network.HTTP.Req ((/:), (/~)) +import qualified Network.HTTP.Req as R +import qualified Data.Text as T + +import Discord.Internal.Rest.Prelude +import Discord.Internal.Types +import Data.Default (Default(..)) + +instance Request (GuildRequest a) where + majorRoute = guildMajorRoute + jsonRequest = guildJsonRequest + +-- | Data constructor for requests. See <https://discord.com/developers/docs/resources/ API> +data GuildRequest a where + -- -- Creating a guild with the API is annoying. Do it manually. + -- -- https://discord.com/developers/docs/resources/guild#create-guild + + -- | Returns the new 'Guild' object for the given id + GetGuild :: GuildId -> GuildRequest Guild + -- | Modify a guild's settings. Returns the updated 'Guild' object on success. Fires a + -- Guild Update 'Event'. + ModifyGuild :: GuildId -> ModifyGuildOpts -> GuildRequest Guild + -- | Delete a guild permanently. User must be owner. Fires a Guild Delete 'Event'. + DeleteGuild :: GuildId -> GuildRequest () + -- | Returns a list of guild 'Channel' objects + GetGuildChannels :: GuildId -> GuildRequest [Channel] + -- | Create a new 'Channel' object for the guild. Requires 'MANAGE_CHANNELS' + -- permission. Returns the new 'Channel' object on success. Fires a Channel Create + -- 'Event' + CreateGuildChannel :: GuildId -> T.Text -> [Overwrite] -> CreateGuildChannelOpts -> GuildRequest Channel + -- | Modify the positions of a set of channel objects for the guild. Requires + -- 'MANAGE_CHANNELS' permission. Returns a list of all of the guild's 'Channel' + -- objects on success. Fires multiple Channel Update 'Event's. + ModifyGuildChannelPositions :: GuildId -> [(ChannelId,Int)] -> GuildRequest [Channel] + -- | Returns a guild 'Member' object for the specified user + GetGuildMember :: GuildId -> UserId -> GuildRequest GuildMember + -- | Returns a list of guild 'Member' objects that are members of the guild. + ListGuildMembers :: GuildId -> GuildMembersTiming -> GuildRequest [GuildMember] + -- | Adds a user to the guild, provided you have a valid oauth2 access token + -- for the user with the guilds.join scope. Returns the guild 'Member' as the body. + -- Fires a Guild Member Add 'Event'. Requires the bot to have the + -- CREATE_INSTANT_INVITE permission. + AddGuildMember :: GuildId -> UserId -> AddGuildMemberOpts + -> GuildRequest () + -- | Modify attributes of a guild 'Member'. Fires a Guild Member Update 'Event'. + ModifyGuildMember :: GuildId -> UserId -> ModifyGuildMemberOpts -> GuildRequest GuildMember + -- | Modify the nickname of the current user + ModifyCurrentUserNick :: GuildId -> T.Text -> GuildRequest () + -- | Add a member to a guild role. Requires 'MANAGE_ROLES' permission. + AddGuildMemberRole :: GuildId -> UserId -> RoleId -> GuildRequest () + -- | Remove a member from a guild role. Requires 'MANAGE_ROLES' permission. + RemoveGuildMemberRole :: GuildId -> UserId -> RoleId -> GuildRequest () + -- | Remove a member from a guild. Requires 'KICK_MEMBER' permission. Fires a + -- Guild Member Remove 'Event'. + RemoveGuildMember :: GuildId -> UserId -> GuildRequest () + -- | Returns a list of 'Ban' objects for users that are banned from this guild. Requires the + -- 'BAN_MEMBERS' permission + GetGuildBans :: GuildId -> GuildRequest [GuildBan] + -- | Returns a 'Ban' object for the user banned from this guild. Requires the + -- 'BAN_MEMBERS' permission + GetGuildBan :: GuildId -> UserId -> GuildRequest GuildBan + -- | Create a guild ban, and optionally Delete previous messages sent by the banned + -- user. Requires the 'BAN_MEMBERS' permission. Fires a Guild Ban Add 'Event'. + CreateGuildBan :: GuildId -> UserId -> CreateGuildBanOpts -> GuildRequest () + -- | Remove the ban for a user. Requires the 'BAN_MEMBERS' permissions. + -- Fires a Guild Ban Remove 'Event'. + RemoveGuildBan :: GuildId -> UserId -> GuildRequest () + -- | Returns a list of 'Role' objects for the guild. Requires the 'MANAGE_ROLES' + -- permission + GetGuildRoles :: GuildId -> GuildRequest [Role] + -- | Create a new 'Role' for the guild. Requires the 'MANAGE_ROLES' permission. + -- Returns the new role object on success. Fires a Guild Role Create 'Event'. + CreateGuildRole :: GuildId -> ModifyGuildRoleOpts -> GuildRequest Role + -- | Modify the positions of a set of role objects for the guild. Requires the + -- 'MANAGE_ROLES' permission. Returns a list of all of the guild's 'Role' objects + -- on success. Fires multiple Guild Role Update 'Event's. + ModifyGuildRolePositions :: GuildId -> [(RoleId, Integer)] -> GuildRequest [Role] + -- | Modify a guild role. Requires the 'MANAGE_ROLES' permission. Returns the + -- updated 'Role' on success. Fires a Guild Role Update 'Event's. + ModifyGuildRole :: GuildId -> RoleId -> ModifyGuildRoleOpts -> GuildRequest Role + -- | Delete a guild role. Requires the 'MANAGE_ROLES' permission. Fires a Guild Role + -- Delete 'Event'. + DeleteGuildRole :: GuildId -> RoleId -> GuildRequest () + -- | Returns an object with one 'pruned' key indicating the number of members + -- that would be removed in a prune operation. Requires the 'KICK_MEMBERS' + -- permission. + GetGuildPruneCount :: GuildId -> Integer -> GuildRequest Object + -- | Begin a prune operation. Requires the 'KICK_MEMBERS' permission. Returns an + -- object with one 'pruned' key indicating the number of members that were removed + -- in the prune operation. Fires multiple Guild Member Remove 'Events'. + BeginGuildPrune :: GuildId -> Integer -> GuildRequest Object + -- | Returns a list of 'VoiceRegion' objects for the guild. Unlike the similar /voice + -- route, this returns VIP servers when the guild is VIP-enabled. + GetGuildVoiceRegions :: GuildId -> GuildRequest [VoiceRegion] + -- | Returns a list of 'Invite' objects for the guild. Requires the 'MANAGE_GUILD' + -- permission. + GetGuildInvites :: GuildId -> GuildRequest [Invite] + -- | Return a list of 'Integration' objects for the guild. Requires the 'MANAGE_GUILD' + -- permission. + GetGuildIntegrations :: GuildId -> GuildRequest [Integration] + -- | Attach an 'Integration' object from the current user to the guild. Requires the + -- 'MANAGE_GUILD' permission. Fires a Guild Integrations Update 'Event'. + CreateGuildIntegration :: GuildId -> IntegrationId -> CreateGuildIntegrationOpts -> GuildRequest () + -- | Modify the behavior and settings of a 'Integration' object for the guild. + -- Requires the 'MANAGE_GUILD' permission. Fires a Guild Integrations Update 'Event'. + ModifyGuildIntegration :: GuildId -> IntegrationId -> ModifyGuildIntegrationOpts + -> GuildRequest () + -- | Delete the attached 'Integration' object for the guild. Requires the + -- 'MANAGE_GUILD' permission. Fires a Guild Integrations Update 'Event'. + DeleteGuildIntegration :: GuildId -> IntegrationId -> GuildRequest () + -- | Sync an 'Integration'. Requires the 'MANAGE_GUILD' permission. + SyncGuildIntegration :: GuildId -> IntegrationId -> GuildRequest () + -- | Returns the 'GuildWidget' object. Requires the 'MANAGE_GUILD' permission. + GetGuildWidget :: GuildId -> GuildRequest GuildWidget + -- | Modify a 'GuildWidget' object for the guild. All attributes may be passed in with + -- JSON and modified. Requires the 'MANAGE_GUILD' permission. Returns the updated + -- 'GuildWidget' object. + ModifyGuildWidget :: GuildId -> GuildWidget -> GuildRequest GuildWidget + -- | Vanity URL + GetGuildVanityURL :: GuildId -> GuildRequest T.Text + +-- | Options for `ModifyGuildIntegration` +data ModifyGuildIntegrationOpts = ModifyGuildIntegrationOpts + { modifyGuildIntegrationOptsExpireBehavior :: Integer + , modifyGuildIntegrationOptsExpireGraceSeconds :: Integer + , modifyGuildIntegrationOptsEmoticonsEnabled :: Bool + } deriving (Show, Read, Eq, Ord) + +instance ToJSON ModifyGuildIntegrationOpts where + toJSON ModifyGuildIntegrationOpts{..} = objectFromMaybes + [ "expire_grace_period" .== modifyGuildIntegrationOptsExpireGraceSeconds + , "expire_behavior" .== modifyGuildIntegrationOptsExpireBehavior + , "enable_emoticons" .== modifyGuildIntegrationOptsEmoticonsEnabled ] + +-- | Options for `CreateGuildIntegration` +newtype CreateGuildIntegrationOpts = CreateGuildIntegrationOpts + { createGuildIntegrationOptsType :: T.Text + } deriving (Show, Read, Eq, Ord) + +instance ToJSON CreateGuildIntegrationOpts where + toJSON CreateGuildIntegrationOpts{..} = objectFromMaybes + ["type" .== createGuildIntegrationOptsType] + +-- | Options for `CreateGuildBan` +data CreateGuildBanOpts = CreateGuildBanOpts + { createGuildBanOptsDeleteLastNMessages :: Maybe Int + , createGuildBanOptsReason :: Maybe T.Text + } deriving (Show, Read, Eq, Ord) + +instance ToJSON CreateGuildBanOpts where + toJSON CreateGuildBanOpts{..} = objectFromMaybes + [ "delete_message_days" + .=? createGuildBanOptsDeleteLastNMessages + , "reason" .=? createGuildBanOptsReason] + +-- | Options for `ModifyGuildRole` +data ModifyGuildRoleOpts = ModifyGuildRoleOpts + { modifyGuildRoleOptsName :: Maybe T.Text + , modifyGuildRoleOptsPermissions :: Maybe RolePermissions + , modifyGuildRoleOptsColor :: Maybe DiscordColor + , modifyGuildRoleOptsSeparateSidebar :: Maybe Bool + , modifyGuildRoleOptsMentionable :: Maybe Bool + , modifyGuildRoleOptsIcon :: Maybe T.Text + } deriving (Show, Read, Eq, Ord) + +instance ToJSON ModifyGuildRoleOpts where + toJSON ModifyGuildRoleOpts{..} = objectFromMaybes + ["name" .=? modifyGuildRoleOptsName, + "permissions" .=? modifyGuildRoleOptsPermissions, + "color" .=? modifyGuildRoleOptsColor, + "hoist" .=? modifyGuildRoleOptsSeparateSidebar, + "mentionable" .=? modifyGuildRoleOptsMentionable, + "icon" .=? modifyGuildRoleOptsIcon] + +-- | Options for `AddGuildMember` +data AddGuildMemberOpts = AddGuildMemberOpts + { addGuildMemberOptsAccessToken :: T.Text + , addGuildMemberOptsNickname :: Maybe T.Text + , addGuildMemberOptsRoles :: Maybe [RoleId] + , addGuildMemberOptsIsMuted :: Maybe Bool + , addGuildMemberOptsIsDeafened :: Maybe Bool + } deriving (Show, Read, Eq, Ord) + +instance ToJSON AddGuildMemberOpts where + toJSON AddGuildMemberOpts{..} = objectFromMaybes + ["access_token" .== addGuildMemberOptsAccessToken, + "nick" .=? addGuildMemberOptsNickname, + "roles" .=? addGuildMemberOptsRoles, + "mute" .=? addGuildMemberOptsIsMuted, + "deaf" .=? addGuildMemberOptsIsDeafened] + +-- | Options for `ModifyGuildMember` +data ModifyGuildMemberOpts = ModifyGuildMemberOpts + { modifyGuildMemberOptsNickname :: Maybe T.Text + , modifyGuildMemberOptsRoles :: Maybe [RoleId] + , modifyGuildMemberOptsIsMuted :: Maybe Bool + , modifyGuildMemberOptsIsDeafened :: Maybe Bool + , modifyGuildMemberOptsMoveToChannel :: Maybe ChannelId + , modifyGuildMemberOptsTimeoutUntil :: Maybe (Maybe UTCTime) -- ^ If `Just Nothing`, the timeout will be removed. + } deriving (Show, Read, Eq, Ord) + +instance Default ModifyGuildMemberOpts where + def = ModifyGuildMemberOpts Nothing Nothing Nothing Nothing Nothing Nothing + +instance ToJSON ModifyGuildMemberOpts where + toJSON ModifyGuildMemberOpts{..} = objectFromMaybes + ["nick" .=? modifyGuildMemberOptsNickname, + "roles" .=? modifyGuildMemberOptsRoles, + "mute" .=? modifyGuildMemberOptsIsMuted, + "deaf" .=? modifyGuildMemberOptsIsDeafened, + "channel_id" .=? modifyGuildMemberOptsMoveToChannel, + "communication_disabled_until" .=? modifyGuildMemberOptsTimeoutUntil] + +-- | Options for `CreateGuildChannel` +data CreateGuildChannelOpts + -- | Create a text channel + = CreateGuildChannelOptsText { + createGuildChannelOptsTopic :: Maybe T.Text + , createGuildChannelOptsUserMessageRateDelay :: Maybe Integer + , createGuildChannelOptsIsNSFW :: Maybe Bool + , createGuildChannelOptsCategoryId :: Maybe ChannelId } + -- | Create a voice channel + | CreateGuildChannelOptsVoice { + createGuildChannelOptsBitrate :: Maybe Integer + , createGuildChannelOptsMaxUsers :: Maybe Integer + , createGuildChannelOptsCategoryId :: Maybe ChannelId } + -- | Create a category + | CreateGuildChannelOptsCategory + deriving (Show, Read, Eq, Ord) + +-- | Converts a channel name, a list of permissions and other channel options into a JSON Value +createChannelOptsToJSON :: T.Text -> [Overwrite] -> CreateGuildChannelOpts -> Value +createChannelOptsToJSON name perms opts = objectFromMaybes optsJSON + where + optsJSON = case opts of + CreateGuildChannelOptsText{..} -> + ["name" .== String name + ,"type" .== Number 0 + ,"permission_overwrites" .== perms + ,"topic" .=? createGuildChannelOptsTopic + ,"rate_limit_per_user" .=? createGuildChannelOptsUserMessageRateDelay + ,"nsfw" .=? createGuildChannelOptsIsNSFW + ,"parent_id" .=? createGuildChannelOptsCategoryId] + CreateGuildChannelOptsVoice{..} -> + ["name" .== String name + ,"type" .== Number 2 + ,"permission_overwrites" .== perms + ,"bitrate" .=? createGuildChannelOptsBitrate + ,"user_limit" .=? createGuildChannelOptsMaxUsers + ,"parent_id" .=? createGuildChannelOptsCategoryId] + CreateGuildChannelOptsCategory -> + ["name" .== String name + ,"type" .== Number 4 + ,"permission_overwrites" .== perms] + + +-- | Options for `ModifyGuild` +-- +-- See <https://discord.com/developers/docs/resources/guild#modify-guild> +data ModifyGuildOpts = ModifyGuildOpts + { modifyGuildOptsName :: Maybe T.Text + , modifyGuildOptsAFKChannelId :: Maybe ChannelId + , modifyGuildOptsIcon :: Maybe T.Text + , modifyGuildOptsOwnerId :: Maybe UserId + -- Region + -- VerificationLevel + -- DefaultMessageNotification + -- ExplicitContentFilter + } deriving (Show, Read, Eq, Ord) + +instance ToJSON ModifyGuildOpts where + toJSON ModifyGuildOpts{..} = objectFromMaybes + ["name" .=? modifyGuildOptsName, + "afk_channel_id" .=? modifyGuildOptsAFKChannelId, + "icon" .=? modifyGuildOptsIcon, + "owner_id" .=? modifyGuildOptsOwnerId] + +data GuildMembersTiming = GuildMembersTiming + { guildMembersTimingLimit :: Maybe Int + , guildMembersTimingAfter :: Maybe UserId + } deriving (Show, Read, Eq, Ord) + +guildMembersTimingToQuery :: GuildMembersTiming -> R.Option 'R.Https +guildMembersTimingToQuery (GuildMembersTiming mLimit mAfter) = + let limit = case mLimit of + Nothing -> mempty + Just lim -> "limit" R.=: lim + after = case mAfter of + Nothing -> mempty + Just aft -> "after" R.=: show aft + in limit <> after + +guildMajorRoute :: GuildRequest a -> String +guildMajorRoute c = case c of + (GetGuild g) -> "guild " <> show g + (ModifyGuild g _) -> "guild " <> show g + (DeleteGuild g) -> "guild " <> show g + (GetGuildChannels g) -> "guild_chan " <> show g + (CreateGuildChannel g _ _ _) -> "guild_chan " <> show g + (ModifyGuildChannelPositions g _) -> "guild_chan " <> show g + (GetGuildMember g _) -> "guild_memb " <> show g + (ListGuildMembers g _) -> "guild_membs " <> show g + (AddGuildMember g _ _) -> "guild_membs " <> show g + (ModifyGuildMember g _ _) -> "guild_membs " <> show g + (ModifyCurrentUserNick g _) -> "guild_membs " <> show g + (AddGuildMemberRole g _ _) -> "guild_membs " <> show g + (RemoveGuildMemberRole g _ _) -> "guild_membs " <> show g + (RemoveGuildMember g _) -> "guild_membs " <> show g + (GetGuildBan g _) -> "guild_bans " <> show g + (GetGuildBans g) -> "guild_bans " <> show g + (CreateGuildBan g _ _) -> "guild_ban " <> show g + (RemoveGuildBan g _) -> "guild_ban " <> show g + (GetGuildRoles g) -> "guild_roles " <> show g + (CreateGuildRole g _) -> "guild_roles " <> show g + (ModifyGuildRolePositions g _) -> "guild_roles " <> show g + (ModifyGuildRole g _ _) -> "guild_role " <> show g + (DeleteGuildRole g _) -> "guild_role " <> show g + (GetGuildPruneCount g _) -> "guild_prune " <> show g + (BeginGuildPrune g _) -> "guild_prune " <> show g + (GetGuildVoiceRegions g) -> "guild_voice " <> show g + (GetGuildInvites g) -> "guild_invit " <> show g + (GetGuildIntegrations g) -> "guild_integ " <> show g + (CreateGuildIntegration g _ _) -> "guild_integ " <> show g + (ModifyGuildIntegration g _ _) -> "guild_intgr " <> show g + (DeleteGuildIntegration g _) -> "guild_intgr " <> show g + (SyncGuildIntegration g _) -> "guild_sync " <> show g + (GetGuildWidget g) -> "guild_widget " <> show g + (ModifyGuildWidget g _) -> "guild_widget " <> show g + (GetGuildVanityURL g) -> "guild " <> show g + + +guilds :: R.Url 'R.Https +guilds = baseUrl /: "guilds" + +guildJsonRequest :: GuildRequest r -> JsonRequest +guildJsonRequest c = case c of + (GetGuild guild) -> + Get (guilds /~ guild) mempty + + (ModifyGuild guild patch) -> + Patch (guilds /~ guild) (pure (R.ReqBodyJson patch)) mempty + + (DeleteGuild guild) -> + Delete (guilds /~ guild) mempty + + (GetGuildChannels guild) -> + Get (guilds /~ guild /: "channels") mempty + + (CreateGuildChannel guild name perms patch) -> + Post (guilds /~ guild /: "channels") + (pure (R.ReqBodyJson (createChannelOptsToJSON name perms patch))) mempty + + (ModifyGuildChannelPositions guild newlocs) -> + let patch = map (\(a, b) -> object [("id", toJSON a) + ,("position", toJSON b)]) newlocs + in Patch (guilds /~ guild /: "channels") (pure (R.ReqBodyJson patch)) mempty + + (GetGuildMember guild member) -> + Get (guilds /~ guild /: "members" /~ member) mempty + + (ListGuildMembers guild range) -> + Get (guilds /~ guild /: "members") (guildMembersTimingToQuery range) + + (AddGuildMember guild user patch) -> + Put (guilds /~ guild /: "members" /~ user) (R.ReqBodyJson patch) mempty + + (ModifyGuildMember guild member patch) -> + Patch (guilds /~ guild /: "members" /~ member) (pure (R.ReqBodyJson patch)) mempty + + (ModifyCurrentUserNick guild name) -> + let patch = object ["nick" .= name] + in Patch (guilds /~ guild /: "members/@me/nick") (pure (R.ReqBodyJson patch)) mempty + + (AddGuildMemberRole guild user role) -> + let body = R.ReqBodyJson (object []) + in Put (guilds /~ guild /: "members" /~ user /: "roles" /~ role) body mempty + + (RemoveGuildMemberRole guild user role) -> + Delete (guilds /~ guild /: "members" /~ user /: "roles" /~ role) mempty + + (RemoveGuildMember guild user) -> + Delete (guilds /~ guild /: "members" /~ user) mempty + + (GetGuildBan guild user) -> Get (guilds /~ guild /: "bans" /~ user) mempty + + (GetGuildBans guild) -> Get (guilds /~ guild /: "bans") mempty + + (CreateGuildBan guild user patch) -> + Put (guilds /~ guild /: "bans" /~ user) (R.ReqBodyJson patch) mempty + + (RemoveGuildBan guild ban) -> + Delete (guilds /~ guild /: "bans" /~ ban) mempty + + (GetGuildRoles guild) -> + Get (guilds /~ guild /: "roles") mempty + + (CreateGuildRole guild patch) -> + Post (guilds /~ guild /: "roles") (pure (R.ReqBodyJson patch)) mempty + + (ModifyGuildRolePositions guild patch) -> + let body = map (\(role, pos) -> object ["id".=role, "position".=pos]) patch + in Patch (guilds /~ guild /: "roles") (pure (R.ReqBodyJson body)) mempty + + (ModifyGuildRole guild role patch) -> + Patch (guilds /~ guild /: "roles" /~ role) (pure (R.ReqBodyJson patch)) mempty + + (DeleteGuildRole guild role) -> + Delete (guilds /~ guild /: "roles" /~ role) mempty + + (GetGuildPruneCount guild days) -> + Get (guilds /~ guild /: "prune") ("days" R.=: days) + + (BeginGuildPrune guild days) -> + Post (guilds /~ guild /: "prune") (pure R.NoReqBody) ("days" R.=: days) + + (GetGuildVoiceRegions guild) -> + Get (guilds /~ guild /: "regions") mempty + + (GetGuildInvites guild) -> + Get (guilds /~ guild /: "invites") mempty + + (GetGuildIntegrations guild) -> + Get (guilds /~ guild /: "integrations") mempty + + (CreateGuildIntegration guild iid opts) -> + let patch = object ["type" .= createGuildIntegrationOptsType opts, "id" .= iid] + in Post (guilds /~ guild /: "integrations") (pure (R.ReqBodyJson patch)) mempty + + (ModifyGuildIntegration guild iid patch) -> + let body = pure (R.ReqBodyJson patch) + in Patch (guilds /~ guild /: "integrations" /~ iid) body mempty + + (DeleteGuildIntegration guild integ) -> + Delete (guilds /~ guild /: "integrations" /~ integ) mempty + + (SyncGuildIntegration guild integ) -> + Post (guilds /~ guild /: "integrations" /~ integ) (pure R.NoReqBody) mempty + + (GetGuildWidget guild) -> + Get (guilds /~ guild /: "integrations") mempty + + (ModifyGuildWidget guild patch) -> + Patch (guilds /~ guild /: "widget") (pure (R.ReqBodyJson patch)) mempty + + (GetGuildVanityURL guild) -> + Get (guilds /~ guild /: "vanity-url") mempty + diff --git a/deps/discord-haskell/src/Discord/Internal/Rest/HTTP.hs b/deps/discord-haskell/src/Discord/Internal/Rest/HTTP.hs new file mode 100644 index 0000000..f9c0341 --- /dev/null +++ b/deps/discord-haskell/src/Discord/Internal/Rest/HTTP.hs @@ -0,0 +1,140 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE MultiWayIf #-} + +-- | Provide HTTP primitives +module Discord.Internal.Rest.HTTP + ( restLoop + , Request(..) + , JsonRequest(..) + , RestCallInternalException(..) + ) where + +import Prelude hiding (log) + +import Control.Monad.IO.Class (liftIO) +import Control.Concurrent (threadDelay) +import Control.Exception.Safe (try) +import Control.Concurrent.MVar +import Control.Concurrent.Chan +import Data.Ix (inRange) +import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime) +import qualified Data.ByteString as B +import qualified Data.ByteString.Lazy as BL +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE +import Text.Read (readMaybe) +import Data.Maybe (fromMaybe) +import qualified Network.HTTP.Req as R +import qualified Data.Map.Strict as M + +import Discord.Internal.Types +import Discord.Internal.Rest.Prelude + +-- | An exception in a Rest call +data RestCallInternalException + -- | Error code from Discord + = RestCallInternalErrorCode Int B.ByteString B.ByteString + -- | Couldn't parse the response + | RestCallInternalNoParse String BL.ByteString + -- | Something went bad in the HTTP process + | RestCallInternalHttpException R.HttpException + deriving (Show) + +-- | Rest event loop +restLoop :: Auth -> Chan (String, JsonRequest, MVar (Either RestCallInternalException BL.ByteString)) + -> Chan T.Text -> IO () +restLoop auth urls log = loop M.empty + where + loop ratelocker = do + threadDelay (40 * 1000) + (route, request, thread) <- readChan urls + curtime <- getPOSIXTime + case compareRate ratelocker route curtime of + Locked -> do writeChan urls (route, request, thread) + loop ratelocker + Available -> do let action = compileRequest auth request + reqIO <- try $ restIOtoIO (tryRequest log action) + case reqIO :: Either R.HttpException (RequestResponse, Timeout) of + Left e -> do + writeChan log ("rest - http exception " <> T.pack (show e)) + putMVar thread (Left (RestCallInternalHttpException e)) + loop ratelocker + Right (resp, retry) -> do + case resp of + -- decode "[]" == () for expected empty calls + ResponseByteString "" -> putMVar thread (Right "[]") + ResponseByteString bs -> putMVar thread (Right bs) + ResponseErrorCode e s b -> + putMVar thread (Left (RestCallInternalErrorCode e s b)) + ResponseTryAgain -> writeChan urls (route, request, thread) + case retry of + GlobalWait i -> do + writeChan log ("rest - GLOBAL WAIT LIMIT: " + <> T.pack (show ((i - curtime) * 1000))) + threadDelay $ round ((i - curtime + 0.1) * 1000) + loop ratelocker + PathWait i -> loop $ M.insert route i (removeAllExpire ratelocker curtime) + NoLimit -> loop ratelocker + +data RateLimited = Available | Locked + +compareRate :: M.Map String POSIXTime -> String -> POSIXTime -> RateLimited +compareRate ratelocker route curtime = + case M.lookup route ratelocker of + Just unlockTime -> if curtime < unlockTime then Locked else Available + Nothing -> Available + +removeAllExpire :: M.Map String POSIXTime -> POSIXTime -> M.Map String POSIXTime +removeAllExpire ratelocker curtime = + if M.size ratelocker > 100 then M.filter (> curtime) ratelocker + else ratelocker + +data RequestResponse = ResponseTryAgain + | ResponseByteString BL.ByteString + | ResponseErrorCode Int B.ByteString B.ByteString + deriving (Show) + +data Timeout = GlobalWait POSIXTime + | PathWait POSIXTime + | NoLimit + +tryRequest :: Chan T.Text -> RestIO R.LbsResponse -> RestIO (RequestResponse, Timeout) +tryRequest _log action = do + resp <- action + now <- liftIO getPOSIXTime + let body = R.responseBody resp + code = R.responseStatusCode resp + status = R.responseStatusMessage resp + global = (Just ("true" :: String) ==) $ readMaybeBS =<< R.responseHeader resp "X-RateLimit-Global" + remain = fromMaybe 1 $ readMaybeBS =<< R.responseHeader resp "X-RateLimit-Remaining" :: Integer + reset = withDelta . fromMaybe 10 $ readMaybeBS =<< R.responseHeader resp "X-RateLimit-Reset-After" + + withDelta :: Double -> POSIXTime + withDelta dt = now + fromRational (toRational dt) + + if | code == 429 -> pure (ResponseTryAgain, if global then GlobalWait reset + else PathWait reset) + | code `elem` [500,502] -> pure (ResponseTryAgain, NoLimit) + | inRange (200,299) code -> pure ( ResponseByteString body + , if remain > 0 then NoLimit else PathWait reset ) + | inRange (400,499) code -> pure (ResponseErrorCode code status (BL.toStrict body) + , if remain > 0 then NoLimit else PathWait reset ) + | otherwise -> pure (ResponseErrorCode code status (BL.toStrict body), NoLimit) + +readMaybeBS :: Read a => B.ByteString -> Maybe a +readMaybeBS = readMaybe . T.unpack . TE.decodeUtf8 + +compileRequest :: Auth -> JsonRequest -> RestIO R.LbsResponse +compileRequest auth request = action + where + authopt = authHeader auth <> R.header "X-RateLimit-Precision" "millisecond" + + action = case request of + (Delete url opts) -> R.req R.DELETE url R.NoReqBody R.lbsResponse (authopt <> opts) + (Get url opts) -> R.req R.GET url R.NoReqBody R.lbsResponse (authopt <> opts) + (Put url body opts) -> R.req R.PUT url body R.lbsResponse (authopt <> opts) + (Patch url body opts) -> do b <- body + R.req R.PATCH url b R.lbsResponse (authopt <> opts) + (Post url body opts) -> do b <- body + R.req R.POST url b R.lbsResponse (authopt <> opts) diff --git a/deps/discord-haskell/src/Discord/Internal/Rest/Interactions.hs b/deps/discord-haskell/src/Discord/Internal/Rest/Interactions.hs new file mode 100644 index 0000000..44e41d1 --- /dev/null +++ b/deps/discord-haskell/src/Discord/Internal/Rest/Interactions.hs @@ -0,0 +1,90 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} + +module Discord.Internal.Rest.Interactions (InteractionResponseRequest(..)) where + +import Data.Aeson (encode) +import qualified Data.ByteString.Lazy as BL +import Discord.Internal.Rest.Prelude + ( RestIO, + Request(..), + JsonRequest(Delete, Post, Get, Patch), + baseUrl) +import Discord.Internal.Types +import Discord.Internal.Types.Interactions +import Network.HTTP.Client.MultipartFormData (PartM, partBS) +import Network.HTTP.Req ((/:), (/~)) +import qualified Network.HTTP.Req as R + +-- | Data constructor for Interaction response requests +data InteractionResponseRequest a where + -- | Create a response to an Interaction from the gateway. + -- + -- This endpoint also supports file attachments similar to the webhook endpoints. + -- Refer to [Uploading files](https://discord.com/developers/docs/reference#uploading-files) + -- for details on uploading files and @multipart/form-data@ requests. + CreateInteractionResponse :: InteractionId -> InteractionToken -> InteractionResponse -> InteractionResponseRequest () + -- | Returns the initial Interaction response. + GetOriginalInteractionResponse :: ApplicationId -> InteractionToken -> InteractionResponseRequest Message + -- | Edits the initial Interaction response. + EditOriginalInteractionResponse :: ApplicationId -> InteractionToken -> InteractionResponseMessage -> InteractionResponseRequest Message + -- | Deletes the initial Interaction response. + DeleteOriginalInteractionResponse :: ApplicationId -> InteractionToken -> InteractionResponseRequest () + -- | Create a followup message for an Interaction + CreateFollowupInteractionMessage :: ApplicationId -> InteractionToken -> InteractionResponseMessage -> InteractionResponseRequest Message + -- | Returns a followup message for an Interaction. + GetFollowupInteractionMessage :: ApplicationId -> InteractionToken -> MessageId -> InteractionResponseRequest Message + -- | Edits a followup message for an Interaction. + EditFollowupInteractionMessage :: ApplicationId -> InteractionToken -> MessageId -> InteractionResponse -> InteractionResponseRequest Message + -- | Deletes a followup message for an Interaction. + DeleteFollowupInteractionMessage :: ApplicationId -> InteractionToken -> MessageId -> InteractionResponseRequest () + +instance Request (InteractionResponseRequest a) where + jsonRequest = interactionResponseJsonRequest + majorRoute = interactionResponseMajorRoute + +interactionResponseMajorRoute :: InteractionResponseRequest a -> String +interactionResponseMajorRoute a = case a of + (CreateInteractionResponse iid _ _) -> "intresp " <> show iid + (GetOriginalInteractionResponse aid _) -> "intresp " <> show aid + (EditOriginalInteractionResponse aid _ _) -> "intresp " <> show aid + (DeleteOriginalInteractionResponse aid _) -> "intresp " <> show aid + (CreateFollowupInteractionMessage iid _ _) -> "intrespf " <> show iid + (GetFollowupInteractionMessage aid _ _) -> "intrespf " <> show aid + (EditFollowupInteractionMessage aid _ _ _) -> "intrespf " <> show aid + (DeleteFollowupInteractionMessage aid _ _) -> "intrespf " <> show aid + +interaction :: ApplicationId -> InteractionToken -> R.Url 'R.Https +interaction aid it = baseUrl /: "webhooks" /~ aid /~ it /: "messages" + +interactionResponseJsonRequest :: InteractionResponseRequest a -> JsonRequest +interactionResponseJsonRequest a = case a of + (CreateInteractionResponse iid it i) -> + Post (baseUrl /: "interactions" /~ iid /~ it /: "callback") (convert i) mempty + (GetOriginalInteractionResponse aid it) -> + Get (interaction aid it /: "@original") mempty + (EditOriginalInteractionResponse aid it i) -> + Patch (interaction aid it /: "@original") (convertIRM i) mempty + (DeleteOriginalInteractionResponse aid it) -> + Delete (interaction aid it /: "@original") mempty + (CreateFollowupInteractionMessage aid it i) -> + Post (baseUrl /: "webhooks" /~ aid /~ it) (convertIRM i) mempty + (GetFollowupInteractionMessage aid it mid) -> + Get (interaction aid it /~ mid) mempty + (EditFollowupInteractionMessage aid it mid i) -> + Patch (interaction aid it /~ mid) (convert i) mempty + (DeleteFollowupInteractionMessage aid it mid) -> + Delete (interaction aid it /~ mid) mempty + where + convert :: InteractionResponse -> RestIO R.ReqBodyMultipart + convert ir@(InteractionResponseChannelMessage irm) = R.reqBodyMultipart (partBS "payload_json" (BL.toStrict $ encode ir) : convert' irm) + convert ir@(InteractionResponseUpdateMessage irm) = R.reqBodyMultipart (partBS "payload_json" (BL.toStrict $ encode ir) : convert' irm) + convert ir = R.reqBodyMultipart [partBS "payload_json" $ BL.toStrict $ encode ir] + convertIRM :: InteractionResponseMessage -> RestIO R.ReqBodyMultipart + convertIRM irm = R.reqBodyMultipart (partBS "payload_json" (BL.toStrict $ encode irm) : convert' irm) + convert' :: InteractionResponseMessage -> [PartM IO] + convert' InteractionResponseMessage {..} = case interactionResponseMessageEmbeds of + Nothing -> [] + Just f -> (maybeEmbed . Just) =<< f diff --git a/deps/discord-haskell/src/Discord/Internal/Rest/Invite.hs b/deps/discord-haskell/src/Discord/Internal/Rest/Invite.hs new file mode 100644 index 0000000..79b4aa5 --- /dev/null +++ b/deps/discord-haskell/src/Discord/Internal/Rest/Invite.hs @@ -0,0 +1,43 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE MultiWayIf #-} +{-# LANGUAGE InstanceSigs #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | Provides actions for Channel API interactions +module Discord.Internal.Rest.Invite + ( InviteRequest(..) + ) where + +import Network.HTTP.Req ((/:)) +import qualified Network.HTTP.Req as R +import qualified Data.Text as T + +import Discord.Internal.Rest.Prelude +import Discord.Internal.Types + +instance Request (InviteRequest a) where + majorRoute = inviteMajorRoute + jsonRequest = inviteJsonRequest + + +-- | Data constructor for requests. See <https://discord.com/developers/docs/resources/ API> +data InviteRequest a where + -- | Get invite for given code + GetInvite :: T.Text -> InviteRequest Invite + -- | Delete invite by code + DeleteInvite :: T.Text -> InviteRequest Invite + +inviteMajorRoute :: InviteRequest a -> String +inviteMajorRoute c = case c of + (GetInvite _) -> "invite " + (DeleteInvite _) -> "invite " + +invite :: R.Url 'R.Https +invite = baseUrl /: "invites" + +inviteJsonRequest :: InviteRequest r -> JsonRequest +inviteJsonRequest c = case c of + (GetInvite g) -> Get (invite R./: g) mempty + (DeleteInvite g) -> Delete (invite R./: g) mempty diff --git a/deps/discord-haskell/src/Discord/Internal/Rest/Prelude.hs b/deps/discord-haskell/src/Discord/Internal/Rest/Prelude.hs new file mode 100644 index 0000000..4b7d825 --- /dev/null +++ b/deps/discord-haskell/src/Discord/Internal/Rest/Prelude.hs @@ -0,0 +1,74 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE GADTs #-} + +-- | Utility and base types and functions for the Discord Rest API +module Discord.Internal.Rest.Prelude where + +import Prelude hiding (log) +import Control.Exception.Safe (throwIO) +import Control.Monad.IO.Class (MonadIO, liftIO) +import Data.String (IsString(fromString)) +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE + +import qualified Network.HTTP.Req as R +import Web.Internal.HttpApiData (ToHttpApiData) + +import Discord.Internal.Types + +import Paths_discord_haskell (version) +import Data.Version (showVersion) + +-- | The api version to use. +apiVersion :: T.Text +apiVersion = "10" + +-- | The base url (Req) for API requests +baseUrl :: R.Url 'R.Https +baseUrl = R.https "discord.com" R./: "api" R./: apiVersion' + where apiVersion' = "v" <> apiVersion + +-- | Discord requires HTTP headers for authentication. +authHeader :: Auth -> R.Option 'R.Https +authHeader auth = + R.header "Authorization" (TE.encodeUtf8 (authToken auth)) + <> R.header "User-Agent" agent + where + -- | https://discord.com/developers/docs/reference#user-agent + -- Second place where the library version is noted + agent = fromString $ "DiscordBot (https://github.com/discord-haskell/discord-haskell, " <> showVersion version <> ")" + +-- Possibly append to an URL +infixl 5 /? +(/?) :: ToHttpApiData a => R.Url scheme -> Maybe a -> R.Url scheme +(/?) url Nothing = url +(/?) url (Just part) = url R./~ part + + +-- | A compiled HTTP request ready to execute +data JsonRequest where + Delete :: R.Url 'R.Https -> R.Option 'R.Https -> JsonRequest + Get :: R.Url 'R.Https -> R.Option 'R.Https -> JsonRequest + Put :: R.HttpBody a => R.Url 'R.Https -> a -> R.Option 'R.Https -> JsonRequest + Patch :: R.HttpBody a => R.Url 'R.Https -> RestIO a -> R.Option 'R.Https -> JsonRequest + Post :: R.HttpBody a => R.Url 'R.Https -> RestIO a -> R.Option 'R.Https -> JsonRequest + +class Request a where + -- | used for putting a request into a rate limit bucket + -- https://discord.com/developers/docs/topics/rate-limits#rate-limits + majorRoute :: a -> String + + -- | build a JSON http request + jsonRequest :: a -> JsonRequest + +-- | Same Monad as IO. Overwrite Req settings +newtype RestIO a = RestIO { restIOtoIO :: IO a } + deriving (Functor, Applicative, Monad, MonadIO) + +instance R.MonadHttp RestIO where + -- | Throw actual exceptions + handleHttpException = liftIO . throwIO + -- | Don't throw exceptions on http error codes like 404 + getHttpConfig = pure $ R.defaultHttpConfig { R.httpConfigCheckResponse = \_ _ _ -> Nothing } diff --git a/deps/discord-haskell/src/Discord/Internal/Rest/ScheduledEvents.hs b/deps/discord-haskell/src/Discord/Internal/Rest/ScheduledEvents.hs new file mode 100644 index 0000000..bb35d12 --- /dev/null +++ b/deps/discord-haskell/src/Discord/Internal/Rest/ScheduledEvents.hs @@ -0,0 +1,73 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | Provides actions for Scheduled Event API +module Discord.Internal.Rest.ScheduledEvents + ( ScheduledEventRequest(..) + ) where +import Data.Aeson ( ToJSON(toJSON) ) +import Discord.Internal.Rest.Prelude ( JsonRequest(..) + , Request + ( jsonRequest + , majorRoute + ) + , baseUrl + ) +import Discord.Internal.Types.Prelude ( GuildId + , ScheduledEventId + ) +import Discord.Internal.Types.ScheduledEvents + ( CreateScheduledEventData + , ModifyScheduledEventData + , ScheduledEvent + , ScheduledEventUser + ) +import qualified Network.HTTP.Req as R +import Network.HTTP.Req ( (/:), (/~) ) + +-- | Data constructor for requests. +-- See <https://discord.com/developers/docs/resources/guild-scheduled-event> +data ScheduledEventRequest a where + -- | Gets all the Scheduled Events of a Guild + ListScheduledEvents ::GuildId + -> ScheduledEventRequest [ScheduledEvent] + -- | Creates a new ScheduledEvent + CreateScheduledEvent ::GuildId + -> CreateScheduledEventData + -> ScheduledEventRequest ScheduledEvent + -- | Gets the information about an Event + GetScheduledEvent ::GuildId + -> ScheduledEventId + -> ScheduledEventRequest ScheduledEvent + -- | Modifies a Scheduled Event's information + ModifyScheduledEvent ::GuildId + -> ScheduledEventId + -> ModifyScheduledEventData + -> ScheduledEventRequest ScheduledEvent + -- | Delete a ScheduledEvent + DeleteScheduledEvent ::GuildId + -> ScheduledEventId + -> ScheduledEventRequest () + -- | Gets the Users that subscribed to the event + GetScheduledEventUsers ::GuildId + -> ScheduledEventId + -> ScheduledEventRequest [ScheduledEventUser] + +sevEndpoint :: GuildId -> R.Url 'R.Https +sevEndpoint gid = baseUrl /: "guilds" /~ gid /: "scheduled-events" + +instance Request (ScheduledEventRequest a) where + majorRoute = const "scheduledEvent" + jsonRequest rq = case rq of + ListScheduledEvents gid -> Get (sevEndpoint gid) mempty + GetScheduledEvent gid ev -> Get (sevEndpoint gid /~ ev) mempty + CreateScheduledEvent gid ev -> + Post (sevEndpoint gid) (pure $ R.ReqBodyJson $ toJSON ev) mempty + ModifyScheduledEvent gid evi ev -> Patch + (sevEndpoint gid /~ evi) + (pure $ R.ReqBodyJson $ toJSON ev) + mempty + DeleteScheduledEvent gid evi -> Delete (sevEndpoint gid /~ evi) mempty + GetScheduledEventUsers gid evi -> + Get (sevEndpoint gid /~ evi /: "users") mempty diff --git a/deps/discord-haskell/src/Discord/Internal/Rest/User.hs b/deps/discord-haskell/src/Discord/Internal/Rest/User.hs new file mode 100644 index 0000000..28c0505 --- /dev/null +++ b/deps/discord-haskell/src/Discord/Internal/Rest/User.hs @@ -0,0 +1,99 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE InstanceSigs #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | Provides actions for Channel API interactions +module Discord.Internal.Rest.User + ( UserRequest(..) + , parseAvatarImage + ) where + + +import Data.Aeson +import Network.HTTP.Req ((/:), (/~)) +import qualified Network.HTTP.Req as R +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE +import qualified Data.ByteString as B +import qualified Data.ByteString.Base64 as B64 + +import Discord.Internal.Rest.Prelude +import Discord.Internal.Types + +instance Request (UserRequest a) where + majorRoute = userMajorRoute + jsonRequest = userJsonRequest + + +-- | Data constructor for requests. See <https://discord.com/developers/docs/resources/ API> +data UserRequest a where + -- | Returns the 'User' object of the requester's account. For OAuth2, this requires + -- the identify scope, which will return the object without an email, and optionally + -- the email scope, which returns the object with an email. + GetCurrentUser :: UserRequest User + -- | Returns a 'User' for a given user ID + GetUser :: UserId -> UserRequest User + -- | Modify user's username & avatar pic + ModifyCurrentUser :: T.Text -> Base64Image User -> UserRequest User + -- | Returns a list of user 'Guild' objects the current user is a member of. + -- Requires the guilds OAuth2 scope. + GetCurrentUserGuilds :: UserRequest [PartialGuild] + -- | Leave a guild. + LeaveGuild :: GuildId -> UserRequest () + -- | Returns a list of DM 'Channel' objects + GetUserDMs :: UserRequest [Channel] + -- | Create a new DM channel with a user. Returns a DM 'Channel' object. + CreateDM :: UserId -> UserRequest Channel + + GetUserConnections :: UserRequest [ConnectionObject] + +-- | @parseAvatarImage bs@ will attempt to convert the given image bytestring +-- @bs@ to the base64 format expected by the Discord API. It may return Left +-- with an error reason if the image format could not be predetermined from the +-- opening magic bytes. This function does /not/ validate the rest of the image, +-- and this is up to the library user to check themselves. +-- +-- This function accepts all file types accepted by 'getMimeType'. +parseAvatarImage :: B.ByteString -> Either T.Text (Base64Image User) +parseAvatarImage bs + | Just mime <- getMimeType bs = Right (Base64Image mime (TE.decodeUtf8 (B64.encode bs))) + | otherwise = Left "Unsupported image format provided" + +userMajorRoute :: UserRequest a -> String +userMajorRoute c = case c of + (GetCurrentUser) -> "me " + (GetUser _) -> "user " + (ModifyCurrentUser _ _) -> "modify_user " + (GetCurrentUserGuilds) -> "get_user_guilds " + (LeaveGuild g) -> "leave_guild " <> show g + (GetUserDMs) -> "get_dms " + (CreateDM _) -> "make_dm " + (GetUserConnections) -> "connections " + +users :: R.Url 'R.Https +users = baseUrl /: "users" + +userJsonRequest :: UserRequest r -> JsonRequest +userJsonRequest c = case c of + (GetCurrentUser) -> Get (users /: "@me") mempty + + (GetUser user) -> Get (users /~ user ) mempty + + (ModifyCurrentUser name b64im) -> + Patch (users /: "@me") (pure (R.ReqBodyJson (object [ "username" .= name + , "avatar" .= b64im ]))) mempty + + (GetCurrentUserGuilds) -> Get (users /: "@me" /: "guilds") mempty + + (LeaveGuild guild) -> Delete (users /: "@me" /: "guilds" /~ guild) mempty + + (GetUserDMs) -> Get (users /: "@me" /: "channels") mempty + + (CreateDM user) -> + let body = R.ReqBodyJson $ object ["recipient_id" .= user] + in Post (users /: "@me" /: "channels") (pure body) mempty + + (GetUserConnections) -> + Get (users /: "@me" /: "connections") mempty diff --git a/deps/discord-haskell/src/Discord/Internal/Rest/Voice.hs b/deps/discord-haskell/src/Discord/Internal/Rest/Voice.hs new file mode 100644 index 0000000..9966aea --- /dev/null +++ b/deps/discord-haskell/src/Discord/Internal/Rest/Voice.hs @@ -0,0 +1,37 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE InstanceSigs #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | Provides actions for Voice API interactions +module Discord.Internal.Rest.Voice + ( VoiceRequest(..) + ) where + + +import Network.HTTP.Req ((/:)) +import qualified Network.HTTP.Req as R + +import Discord.Internal.Rest.Prelude +import Discord.Internal.Types + +instance Request (VoiceRequest a) where + majorRoute = voiceMajorRoute + jsonRequest = voiceJsonRequest + +-- | Data constructor for requests +data VoiceRequest a where + -- | List all available 'VoiceRegion's. + ListVoiceRegions :: VoiceRequest [VoiceRegion] + +voiceMajorRoute :: VoiceRequest a -> String +voiceMajorRoute c = case c of + (ListVoiceRegions) -> "whatever " + +voices :: R.Url 'R.Https +voices = baseUrl /: "voice" + +voiceJsonRequest :: VoiceRequest r -> JsonRequest +voiceJsonRequest c = case c of + (ListVoiceRegions) -> Get (voices /: "regions") mempty diff --git a/deps/discord-haskell/src/Discord/Internal/Rest/Webhook.hs b/deps/discord-haskell/src/Discord/Internal/Rest/Webhook.hs new file mode 100644 index 0000000..7b4a545 --- /dev/null +++ b/deps/discord-haskell/src/Discord/Internal/Rest/Webhook.hs @@ -0,0 +1,202 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | Provides actions for Webhook API interactions +module Discord.Internal.Rest.Webhook + ( CreateWebhookOpts(..) + , ExecuteWebhookWithTokenOpts(..) + , ModifyWebhookOpts(..) + , WebhookContent(..) + , WebhookRequest(..) + ) where + +import Data.Aeson +import qualified Data.Text as T +import qualified Data.ByteString as B +import qualified Data.ByteString.Lazy as BL +import Network.HTTP.Req ((/:), (/~)) +import qualified Network.HTTP.Req as R +import Network.HTTP.Client (RequestBody (RequestBodyBS)) +import Network.HTTP.Client.MultipartFormData (partBS, partFileRequestBody) + +import Discord.Internal.Rest.Prelude +import Discord.Internal.Types + +instance Request (WebhookRequest a) where + majorRoute = webhookMajorRoute + jsonRequest = webhookJsonRequest + +-- | Data constructors for webhook requests. +data WebhookRequest a where + -- | Creates a new webhook and returns a webhook object on success. Requires the @MANAGE_WEBHOOKS@ permission. + -- An error will be returned if a webhook name (name) is not valid. A webhook name is valid if: + -- + -- * It does not contain the substring @clyde@ (case-insensitive) + -- * It follows the nickname guidelines in the Usernames and Nicknames documentation, + -- with an exception that webhook names can be up to 80 characters + CreateWebhook :: ChannelId + -> CreateWebhookOpts + -> WebhookRequest Webhook + -- | Returns a channel's `Webhook`s as a list. Requires the @MANAGE_WEBHOOKS@ permission. + GetChannelWebhooks :: ChannelId + -> WebhookRequest [Webhook] + -- | Returns a guild's `Webhook`s as a list. Requires the @MANAGE_WEBHOOKS@ permission. + GetGuildWebhooks :: GuildId + -> WebhookRequest [Webhook] + -- | Returns the `Webhook` for the given id. If a token is given, authentication is not required. + GetWebhook :: WebhookId + -> Maybe WebhookToken + -> WebhookRequest Webhook + -- | Modify a webhook. Requires the @MANAGE_WEBHOOKS@ permission. Returns the updated `Webhook` on success. + -- If a token is given, authentication is not required. + ModifyWebhook :: WebhookId + -> Maybe WebhookToken + -> ModifyWebhookOpts + -> WebhookRequest Webhook + -- | Delete a webhook permanently. Requires the @MANAGE_WEBHOOKS@ permission. + -- If a token is given, authentication is not required. + DeleteWebhook :: WebhookId + -> Maybe WebhookToken + -> WebhookRequest () + -- | Executes a Webhook. + -- + -- Refer to [Uploading Files](https://discord.com/developers/docs/reference#uploading-files) + -- for details on attachments and @multipart/form-data@ requests. + ExecuteWebhook :: WebhookId + -> WebhookToken + -> ExecuteWebhookWithTokenOpts + -> WebhookRequest () + -- We don't support slack and github compatible webhooks because you should + -- just use execute webhook. + + -- | Returns a previously-sent webhook message from the same token. + GetWebhookMessage :: WebhookId + -> WebhookToken + -> MessageId + -> WebhookRequest Message + -- | Edits a previously-sent webhook message from the same token. + EditWebhookMessage :: WebhookId + -> WebhookToken + -> MessageId + -> T.Text -- currently we don't support the full range of edits - feel free to PR and fix this + -> WebhookRequest Message + -- | Deletes a previously-sent webhook message from the same token. + DeleteWebhookMessage :: WebhookId + -> WebhookToken + -> MessageId + -> WebhookRequest () + +-- | Options for `ModifyWebhook` and `ModifyWebhookWithToken` +data ModifyWebhookOpts = ModifyWebhookOpts + { modifyWebhookOptsName :: Maybe T.Text + , modifyWebhookOptsAvatar :: Maybe T.Text + , modifyWebhookOptsChannelId :: Maybe ChannelId + } deriving (Show, Read, Eq, Ord) + +instance ToJSON ModifyWebhookOpts where + toJSON ModifyWebhookOpts{..} = objectFromMaybes + ["channel_id" .=? modifyWebhookOptsChannelId, + "name" .=? modifyWebhookOptsName, + "avatar" .=? modifyWebhookOptsAvatar ] + +-- | Options for `CreateWebhook` +data CreateWebhookOpts = CreateWebhookOpts + { createWebhookOptsName :: T.Text + , createWebhookOptsAvatar :: Maybe T.Text + } deriving (Show, Read, Eq, Ord) + +instance ToJSON CreateWebhookOpts where + toJSON CreateWebhookOpts{..} = objectFromMaybes + ["name" .== createWebhookOptsName, + "avatar" .=? createWebhookOptsAvatar ] + +-- | Options for `ExecuteWebhookWithToken` +data ExecuteWebhookWithTokenOpts = ExecuteWebhookWithTokenOpts + { executeWebhookWithTokenOptsUsername :: Maybe T.Text + , executeWebhookWithTokenOptsContent :: WebhookContent + } deriving (Show, Read, Eq, Ord) + +-- | A webhook's content +data WebhookContent = WebhookContentText T.Text + | WebhookContentFile T.Text B.ByteString + | WebhookContentEmbeds [CreateEmbed] + deriving (Show, Read, Eq, Ord) + +webhookContentJson :: WebhookContent -> [(AesonKey, Value)] +webhookContentJson c = case c of + WebhookContentText t -> [("content", toJSON t)] + WebhookContentFile _ _ -> [] + WebhookContentEmbeds e -> [("embeds", toJSON (createEmbed <$> e))] + +instance ToJSON ExecuteWebhookWithTokenOpts where + toJSON ExecuteWebhookWithTokenOpts{..} = objectFromMaybes $ + ["username" .=? executeWebhookWithTokenOptsUsername] + <> fmap Just (webhookContentJson executeWebhookWithTokenOptsContent) + +-- | Major routes for webhook requests +webhookMajorRoute :: WebhookRequest a -> String +webhookMajorRoute ch = case ch of + (CreateWebhook c _) -> "aaaaaahook " <> show c + (GetChannelWebhooks c) -> "aaaaaahook " <> show c + (GetGuildWebhooks g) -> "aaaaaahook " <> show g + (GetWebhook w _) -> "getwebhook " <> show w + (ModifyWebhook w _ _) -> "modifyhook " <> show w + (DeleteWebhook w _) -> "deletehook " <> show w + (ExecuteWebhook w _ _) -> "executehk " <> show w + (GetWebhookMessage w _ _) -> "gethkmsg " <> show w + (EditWebhookMessage w _ _ _) -> "edithkmsg " <> show w + (DeleteWebhookMessage w _ _) -> "delhkmsg " <> show w + +-- | Create a 'JsonRequest' from a `WebhookRequest` +webhookJsonRequest :: WebhookRequest r -> JsonRequest +webhookJsonRequest ch = case ch of + (CreateWebhook channel patch) -> + let body = pure (R.ReqBodyJson patch) + in Post (baseUrl /: "channels" /~ channel /: "webhooks") body mempty + + (GetChannelWebhooks c) -> + Get (baseUrl /: "channels" /~ c /: "webhooks") mempty + + (GetGuildWebhooks g) -> + Get (baseUrl /: "guilds" /~ g /: "webhooks") mempty + + (GetWebhook w t) -> + Get (baseUrl /: "webhooks" /~ w /? t) mempty + + (ModifyWebhook w t p) -> + Patch (baseUrl /: "webhooks" /~ w /? t) (pure (R.ReqBodyJson p)) mempty + + (DeleteWebhook w t) -> + Delete (baseUrl /: "webhooks" /~ w /? t) mempty + + (ExecuteWebhook w tok o) -> + case executeWebhookWithTokenOptsContent o of + WebhookContentFile name text -> + let part = partFileRequestBody "file" (T.unpack name) (RequestBodyBS text) + body = R.reqBodyMultipart [part] + in Post (baseUrl /: "webhooks" /~ w /~ tok) body mempty + WebhookContentText _ -> + let body = pure (R.ReqBodyJson o) + in Post (baseUrl /: "webhooks" /~ w /~ tok) body mempty + WebhookContentEmbeds embeds -> + let mkPart (name,content) = partFileRequestBody name (T.unpack name) (RequestBodyBS content) + uploads CreateEmbed{..} = [(n,c) | (n, Just (CreateEmbedImageUpload c)) <- + [ ("author.png", createEmbedAuthorIcon) + , ("thumbnail.png", createEmbedThumbnail) + , ("image.png", createEmbedImage) + , ("footer.png", createEmbedFooterIcon) ]] + parts = map mkPart (concatMap uploads embeds) + partsJson = [partBS "payload_json" $ BL.toStrict $ encode $ toJSON $ object ["embed" .= createEmbed e] | e <- embeds] + body = R.reqBodyMultipart (partsJson ++ parts) + in Post (baseUrl /: "webhooks" /~ w /: unToken tok) body mempty + + (GetWebhookMessage w t m) -> + Get (baseUrl /: "webhooks" /~ w /~ t /: "messages" /~ m) mempty + + (EditWebhookMessage w t m p) -> + Patch (baseUrl /: "webhooks" /~ w /~ t /: "messages" /~ m) (pure (R.ReqBodyJson $ object ["content" .= p])) mempty + + (DeleteWebhookMessage w t m) -> + Delete (baseUrl /: "webhooks" /~ w /~ t /: "messages" /~ m) mempty |
