packages feed

slack-web 0.4.0.0 → 1.5.0.0

raw patch · 67 files changed

+4615/−1499 lines, 67 filesdep +aeson-prettydep +base16-bytestringdep +classy-preludedep ~aesondep ~basedep ~http-api-datasetup-changednew-uploaderPVP ok

version bump matches the API change (PVP)

Dependencies added: aeson-pretty, base16-bytestring, classy-prelude, cryptonite, data-default-class, either, hspec-core, hspec-golden, mono-traversable, refined, string-conversions, string-variants, template-haskell, th-compat, vector

Dependency ranges changed: aeson, base, http-api-data, http-client, servant, servant-client, servant-client-core, text

API changes (from Hackage documentation)

- Web.Slack: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Web.Slack.ResponseJSON a)
- Web.Slack: instance GHC.Classes.Eq Web.Slack.ResponseSlackError
- Web.Slack: instance GHC.Show.Show Web.Slack.ResponseSlackError
- Web.Slack.Classy: instance Web.Slack.Classy.HasManager Web.Slack.SlackConfig
- Web.Slack.Classy: instance Web.Slack.Classy.HasToken Web.Slack.SlackConfig
- Web.Slack.Conversation: instance Control.DeepSeq.NFData Web.Slack.Conversation.ResponseMetadata
- Web.Slack.Conversation: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Conversation.ResponseMetadata
- Web.Slack.Conversation: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Conversation.ResponseMetadata
- Web.Slack.Conversation: instance GHC.Classes.Eq Web.Slack.Conversation.ResponseMetadata
- Web.Slack.Conversation: instance GHC.Generics.Generic Web.Slack.Conversation.ResponseMetadata
- Web.Slack.Conversation: instance GHC.Show.Show Web.Slack.Conversation.ResponseMetadata
- Web.Slack.Conversation: newtype ListRsp
- Web.Slack.Pager: conversationsHistoryAllBy :: MonadIO m => (HistoryReq -> m (Response HistoryRsp)) -> HistoryReq -> m (LoadPage m Message)
- Web.Slack.Pager: repliesFetchAllBy :: MonadIO m => (RepliesReq -> m (Response HistoryRsp)) -> RepliesReq -> m (LoadPage m Message)
- Web.Slack.Types: instance Control.DeepSeq.NFData Web.Slack.Types.Cursor
- Web.Slack.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Types.Cursor
- Web.Slack.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Types.Cursor
- Web.Slack.Types: instance Data.Hashable.Class.Hashable Web.Slack.Types.Cursor
- Web.Slack.Types: instance GHC.Classes.Eq Web.Slack.Types.Cursor
- Web.Slack.Types: instance GHC.Generics.Generic Web.Slack.Types.Cursor
- Web.Slack.Types: instance GHC.Show.Show Web.Slack.Types.Cursor
- Web.Slack.Types: instance Web.Internal.HttpApiData.ToHttpApiData Web.Slack.Types.Cursor
+ Web.Slack: chatUpdate :: SlackConfig -> UpdateReq -> IO (Response UpdateRsp)
+ Web.Slack: conversationsListAll :: SlackConfig -> ListReq -> IO (LoadPage IO Conversation)
+ Web.Slack: usersConversations :: SlackConfig -> UsersConversationsRequest -> IO (Response UsersConversationsResponse)
+ Web.Slack: usersConversationsAll :: SlackConfig -> UsersConversationsRequest -> IO (LoadPage IO Conversation)
+ Web.Slack.Chat: UpdateReq :: ConversationId -> Text -> Maybe Bool -> Maybe Text -> Maybe Bool -> Maybe Text -> Maybe Text -> Maybe Bool -> Maybe Text -> UpdateReq
+ Web.Slack.Chat: UpdateRsp :: ConversationId -> Text -> Text -> UpdateRsp
+ Web.Slack.Chat: [updateReqAsUser] :: UpdateReq -> Maybe Bool
+ Web.Slack.Chat: [updateReqAttachments] :: UpdateReq -> Maybe Text
+ Web.Slack.Chat: [updateReqChannel] :: UpdateReq -> ConversationId
+ Web.Slack.Chat: [updateReqLinkNames] :: UpdateReq -> Maybe Bool
+ Web.Slack.Chat: [updateReqMetadata] :: UpdateReq -> Maybe Text
+ Web.Slack.Chat: [updateReqParse] :: UpdateReq -> Maybe Text
+ Web.Slack.Chat: [updateReqReplyBroadcast] :: UpdateReq -> Maybe Bool
+ Web.Slack.Chat: [updateReqText] :: UpdateReq -> Maybe Text
+ Web.Slack.Chat: [updateReqTs] :: UpdateReq -> Text
+ Web.Slack.Chat: [updateRspChannel] :: UpdateRsp -> ConversationId
+ Web.Slack.Chat: [updateRspText] :: UpdateRsp -> Text
+ Web.Slack.Chat: [updateRspTs] :: UpdateRsp -> Text
+ Web.Slack.Chat: data UpdateReq
+ Web.Slack.Chat: data UpdateRsp
+ Web.Slack.Chat: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Chat.UpdateRsp
+ Web.Slack.Chat: instance GHC.Classes.Eq Web.Slack.Chat.UpdateReq
+ Web.Slack.Chat: instance GHC.Classes.Eq Web.Slack.Chat.UpdateRsp
+ Web.Slack.Chat: instance GHC.Generics.Generic Web.Slack.Chat.UpdateReq
+ Web.Slack.Chat: instance GHC.Generics.Generic Web.Slack.Chat.UpdateRsp
+ Web.Slack.Chat: instance GHC.Show.Show Web.Slack.Chat.UpdateReq
+ Web.Slack.Chat: instance GHC.Show.Show Web.Slack.Chat.UpdateRsp
+ Web.Slack.Chat: instance Web.Internal.FormUrlEncoded.ToForm Web.Slack.Chat.UpdateReq
+ Web.Slack.Chat: mkUpdateReq :: ConversationId -> Text -> UpdateReq
+ Web.Slack.Classy: instance Web.Slack.Classy.HasManager Web.Slack.Internal.SlackConfig
+ Web.Slack.Classy: instance Web.Slack.Classy.HasToken Web.Slack.Internal.SlackConfig
+ Web.Slack.Conversation: [listReqCursor] :: ListReq -> Maybe Cursor
+ Web.Slack.Conversation: [listReqLimit] :: ListReq -> Maybe Int
+ Web.Slack.Conversation: [listReqTeamId] :: ListReq -> Maybe TeamId
+ Web.Slack.Conversation: [listRspResponseMetadata] :: ListRsp -> Maybe ResponseMetadata
+ Web.Slack.Conversation: data ListRsp
+ Web.Slack.Conversation: instance Web.Slack.Pager.Types.PagedRequest Web.Slack.Conversation.HistoryReq
+ Web.Slack.Conversation: instance Web.Slack.Pager.Types.PagedRequest Web.Slack.Conversation.ListReq
+ Web.Slack.Conversation: instance Web.Slack.Pager.Types.PagedRequest Web.Slack.Conversation.RepliesReq
+ Web.Slack.Conversation: instance Web.Slack.Pager.Types.PagedResponse Web.Slack.Conversation.HistoryRsp
+ Web.Slack.Conversation: instance Web.Slack.Pager.Types.PagedResponse Web.Slack.Conversation.ListRsp
+ Web.Slack.Experimental.Blocks: (<+>) :: SlackText -> SlackText -> SlackText
+ Web.Slack.Experimental.Blocks: DeleteOriginal :: SlackInteractiveResponse
+ Web.Slack.Experimental.Blocks: Ephemeral :: SlackMessage -> SlackInteractiveResponse
+ Web.Slack.Experimental.Blocks: RenderedSlackMessage :: SlackMessage -> Text -> Bool -> RenderedSlackMessage
+ Web.Slack.Experimental.Blocks: ReplaceOriginal :: SlackMessage -> SlackInteractiveResponse
+ Web.Slack.Experimental.Blocks: RichItemChannel :: ConversationId -> RichItem
+ Web.Slack.Experimental.Blocks: RichItemEmoji :: Text -> RichItem
+ Web.Slack.Experimental.Blocks: RichItemLink :: RichLinkAttrs -> RichItem
+ Web.Slack.Experimental.Blocks: RichItemOther :: Text -> Value -> RichItem
+ Web.Slack.Experimental.Blocks: RichItemText :: Text -> RichStyle -> RichItem
+ Web.Slack.Experimental.Blocks: RichItemUser :: UserId -> RichStyle -> RichItem
+ Web.Slack.Experimental.Blocks: RichLinkAttrs :: RichStyle -> Text -> Maybe Text -> RichLinkAttrs
+ Web.Slack.Experimental.Blocks: RichStyle :: Bool -> Bool -> RichStyle
+ Web.Slack.Experimental.Blocks: RichText :: Maybe SlackBlockId -> [RichTextSectionItem] -> RichText
+ Web.Slack.Experimental.Blocks: RichTextSectionItemRichText :: [RichItem] -> RichTextSectionItem
+ Web.Slack.Experimental.Blocks: RichTextSectionItemUnknown :: Text -> Value -> RichTextSectionItem
+ Web.Slack.Experimental.Blocks: SlackActionId :: NonEmptyText 255 -> SlackActionId
+ Web.Slack.Experimental.Blocks: SlackBlockActions :: Maybe SlackBlockId -> SlackActionList -> SlackBlock
+ Web.Slack.Experimental.Blocks: SlackBlockContext :: SlackContext -> SlackBlock
+ Web.Slack.Experimental.Blocks: SlackBlockDivider :: SlackBlock
+ Web.Slack.Experimental.Blocks: SlackBlockHeader :: SlackPlainTextOnly -> SlackBlock
+ Web.Slack.Experimental.Blocks: SlackBlockImage :: SlackImage -> SlackBlock
+ Web.Slack.Experimental.Blocks: SlackBlockRichText :: RichText -> SlackBlock
+ Web.Slack.Experimental.Blocks: SlackBlockSection :: SlackText -> Maybe SlackAccessory -> SlackBlock
+ Web.Slack.Experimental.Blocks: SlackImage :: !Maybe Text -> !Text -> !Text -> SlackImage
+ Web.Slack.Experimental.Blocks: SlackInteractiveResponse :: SlackMessage -> SlackInteractiveResponse
+ Web.Slack.Experimental.Blocks: SlackStyleDanger :: SlackStyle
+ Web.Slack.Experimental.Blocks: SlackStylePrimary :: SlackStyle
+ Web.Slack.Experimental.Blocks: [_originalMessage] :: RenderedSlackMessage -> SlackMessage
+ Web.Slack.Experimental.Blocks: [_renderedMessage] :: RenderedSlackMessage -> Text
+ Web.Slack.Experimental.Blocks: [_truncated] :: RenderedSlackMessage -> Bool
+ Web.Slack.Experimental.Blocks: [blockId] :: RichText -> Maybe SlackBlockId
+ Web.Slack.Experimental.Blocks: [elements] :: RichText -> [RichTextSectionItem]
+ Web.Slack.Experimental.Blocks: [rsBold] :: RichStyle -> Bool
+ Web.Slack.Experimental.Blocks: [rsItalic] :: RichStyle -> Bool
+ Web.Slack.Experimental.Blocks: [slackImageAltText] :: SlackImage -> !Text
+ Web.Slack.Experimental.Blocks: [slackImageTitle] :: SlackImage -> !Maybe Text
+ Web.Slack.Experimental.Blocks: [slackImageUrl] :: SlackImage -> !Text
+ Web.Slack.Experimental.Blocks: [style] :: RichLinkAttrs -> RichStyle
+ Web.Slack.Experimental.Blocks: [text] :: RichLinkAttrs -> Maybe Text
+ Web.Slack.Experimental.Blocks: [unSlackActionId] :: SlackActionId -> NonEmptyText 255
+ Web.Slack.Experimental.Blocks: [url] :: RichLinkAttrs -> Text
+ Web.Slack.Experimental.Blocks: actions :: ToSlackActionList as => as -> SlackMessage
+ Web.Slack.Experimental.Blocks: actionsWithBlockId :: ToSlackActionList as => SlackBlockId -> as -> SlackMessage
+ Web.Slack.Experimental.Blocks: angleBrackets :: SlackText -> SlackText
+ Web.Slack.Experimental.Blocks: bold :: SlackText -> SlackText
+ Web.Slack.Experimental.Blocks: brackets :: SlackText -> SlackText
+ Web.Slack.Experimental.Blocks: button :: SlackActionId -> SlackButtonText -> ButtonSettings -> SlackAction
+ Web.Slack.Experimental.Blocks: buttonSettings :: ButtonSettings
+ Web.Slack.Experimental.Blocks: class Image a
+ Web.Slack.Experimental.Blocks: class Markdown a
+ Web.Slack.Experimental.Blocks: codeBlock :: SlackText -> SlackText
+ Web.Slack.Experimental.Blocks: confirm :: ConfirmSettings -> SlackConfirmObject
+ Web.Slack.Experimental.Blocks: confirmAreYouSure :: ConfirmSettings
+ Web.Slack.Experimental.Blocks: context :: SlackContext -> SlackMessage
+ Web.Slack.Experimental.Blocks: data ButtonSettings
+ Web.Slack.Experimental.Blocks: data ConfirmSettings
+ Web.Slack.Experimental.Blocks: data RenderedSlackMessage
+ Web.Slack.Experimental.Blocks: data RichItem
+ Web.Slack.Experimental.Blocks: data RichLinkAttrs
+ Web.Slack.Experimental.Blocks: data RichStyle
+ Web.Slack.Experimental.Blocks: data RichText
+ Web.Slack.Experimental.Blocks: data RichTextSectionItem
+ Web.Slack.Experimental.Blocks: data SlackBlock
+ Web.Slack.Experimental.Blocks: data SlackImage
+ Web.Slack.Experimental.Blocks: data SlackInteractiveResponse
+ Web.Slack.Experimental.Blocks: data SlackMessage
+ Web.Slack.Experimental.Blocks: data SlackStyle
+ Web.Slack.Experimental.Blocks: data SlackText
+ Web.Slack.Experimental.Blocks: emptySetting :: OptionalSetting a
+ Web.Slack.Experimental.Blocks: image :: Image a => SlackImage -> a
+ Web.Slack.Experimental.Blocks: isSubStringOf :: Text -> SlackText -> Bool
+ Web.Slack.Experimental.Blocks: italic :: SlackText -> SlackText
+ Web.Slack.Experimental.Blocks: link :: Text -> Maybe Text -> SlackText
+ Web.Slack.Experimental.Blocks: markdown :: Markdown a => SlackText -> a
+ Web.Slack.Experimental.Blocks: mentionUser :: UserId -> SlackText
+ Web.Slack.Experimental.Blocks: mentionUserGroupById :: SlackText -> SlackText
+ Web.Slack.Experimental.Blocks: monospaced :: Slack a => a -> SlackText
+ Web.Slack.Experimental.Blocks: mrkdwn :: Slack a => a -> SlackTextObject
+ Web.Slack.Experimental.Blocks: newline :: SlackText -> SlackText
+ Web.Slack.Experimental.Blocks: newtype SlackActionId
+ Web.Slack.Experimental.Blocks: parens :: SlackText -> SlackText
+ Web.Slack.Experimental.Blocks: plaintext :: Slack a => a -> SlackTextObject
+ Web.Slack.Experimental.Blocks: plaintextonly :: Slack a => a -> SlackPlainTextOnly
+ Web.Slack.Experimental.Blocks: prefixFirstSlackMessage :: Text -> [SlackMessage] -> [SlackMessage]
+ Web.Slack.Experimental.Blocks: render :: SlackMessage -> RenderedSlackMessage
+ Web.Slack.Experimental.Blocks: setting :: a -> OptionalSetting a
+ Web.Slack.Experimental.Blocks: slackMessage :: [SlackText] -> SlackMessage
+ Web.Slack.Experimental.Blocks: textToContext :: Text -> SlackMessage
+ Web.Slack.Experimental.Blocks: textToMessage :: Text -> SlackMessage
+ Web.Slack.Experimental.Blocks: ticks :: SlackText -> SlackText
+ Web.Slack.Experimental.Blocks: type SlackBlockId = NonEmptyText 255
+ Web.Slack.Experimental.Blocks: unorderedList :: [SlackText] -> SlackText
+ Web.Slack.Experimental.Blocks.Types: ButtonSettings :: OptionalSetting (NonEmptyText 3000) -> OptionalSetting (NonEmptyText 2000) -> OptionalSetting SlackStyle -> OptionalSetting SlackConfirmObject -> ButtonSettings
+ Web.Slack.Experimental.Blocks.Types: ConfirmSettings :: Text -> Text -> Text -> Text -> OptionalSetting SlackStyle -> ConfirmSettings
+ Web.Slack.Experimental.Blocks.Types: DeleteOriginal :: SlackInteractiveResponse
+ Web.Slack.Experimental.Blocks.Types: Ephemeral :: SlackMessage -> SlackInteractiveResponse
+ Web.Slack.Experimental.Blocks.Types: OptionalSetting :: Maybe a -> OptionalSetting a
+ Web.Slack.Experimental.Blocks.Types: ReplaceOriginal :: SlackMessage -> SlackInteractiveResponse
+ Web.Slack.Experimental.Blocks.Types: RichItemChannel :: ConversationId -> RichItem
+ Web.Slack.Experimental.Blocks.Types: RichItemEmoji :: Text -> RichItem
+ Web.Slack.Experimental.Blocks.Types: RichItemLink :: RichLinkAttrs -> RichItem
+ Web.Slack.Experimental.Blocks.Types: RichItemOther :: Text -> Value -> RichItem
+ Web.Slack.Experimental.Blocks.Types: RichItemText :: Text -> RichStyle -> RichItem
+ Web.Slack.Experimental.Blocks.Types: RichItemUser :: UserId -> RichStyle -> RichItem
+ Web.Slack.Experimental.Blocks.Types: RichLinkAttrs :: RichStyle -> Text -> Maybe Text -> RichLinkAttrs
+ Web.Slack.Experimental.Blocks.Types: RichStyle :: Bool -> Bool -> RichStyle
+ Web.Slack.Experimental.Blocks.Types: RichText :: Maybe SlackBlockId -> [RichTextSectionItem] -> RichText
+ Web.Slack.Experimental.Blocks.Types: RichTextSectionItemRichText :: [RichItem] -> RichTextSectionItem
+ Web.Slack.Experimental.Blocks.Types: RichTextSectionItemUnknown :: Text -> Value -> RichTextSectionItem
+ Web.Slack.Experimental.Blocks.Types: SlackAction :: SlackActionId -> SlackActionComponent -> SlackAction
+ Web.Slack.Experimental.Blocks.Types: SlackActionId :: NonEmptyText 255 -> SlackActionId
+ Web.Slack.Experimental.Blocks.Types: SlackActionList :: Refined SlackActionListConstraints [SlackAction] -> SlackActionList
+ Web.Slack.Experimental.Blocks.Types: SlackActionResponse :: SlackBlockId -> SlackActionId -> SlackActionComponent -> SlackActionResponse
+ Web.Slack.Experimental.Blocks.Types: SlackBlockActions :: Maybe SlackBlockId -> SlackActionList -> SlackBlock
+ Web.Slack.Experimental.Blocks.Types: SlackBlockContext :: SlackContext -> SlackBlock
+ Web.Slack.Experimental.Blocks.Types: SlackBlockDivider :: SlackBlock
+ Web.Slack.Experimental.Blocks.Types: SlackBlockHeader :: SlackPlainTextOnly -> SlackBlock
+ Web.Slack.Experimental.Blocks.Types: SlackBlockImage :: SlackImage -> SlackBlock
+ Web.Slack.Experimental.Blocks.Types: SlackBlockRichText :: RichText -> SlackBlock
+ Web.Slack.Experimental.Blocks.Types: SlackBlockSection :: SlackText -> Maybe SlackAccessory -> SlackBlock
+ Web.Slack.Experimental.Blocks.Types: SlackButton :: SlackButtonText -> Maybe (NonEmptyText 3000) -> Maybe (NonEmptyText 2000) -> Maybe SlackStyle -> Maybe SlackConfirmObject -> SlackActionComponent
+ Web.Slack.Experimental.Blocks.Types: SlackButtonAccessory :: SlackAction -> SlackAccessory
+ Web.Slack.Experimental.Blocks.Types: SlackButtonText :: NonEmptyText 75 -> SlackButtonText
+ Web.Slack.Experimental.Blocks.Types: SlackConfirmObject :: SlackPlainTextOnly -> SlackTextObject -> SlackPlainTextOnly -> SlackPlainTextOnly -> Maybe SlackStyle -> SlackConfirmObject
+ Web.Slack.Experimental.Blocks.Types: SlackContentImage :: SlackImage -> SlackContent
+ Web.Slack.Experimental.Blocks.Types: SlackContentText :: SlackText -> SlackContent
+ Web.Slack.Experimental.Blocks.Types: SlackContext :: [SlackContent] -> SlackContext
+ Web.Slack.Experimental.Blocks.Types: SlackImage :: !Maybe Text -> !Text -> !Text -> SlackImage
+ Web.Slack.Experimental.Blocks.Types: SlackInteractivePayload :: Text -> Text -> Text -> Maybe SlackResponseUrl -> Maybe Text -> [SlackActionResponse] -> SlackInteractivePayload
+ Web.Slack.Experimental.Blocks.Types: SlackInteractiveResponse :: SlackMessage -> SlackInteractiveResponse
+ Web.Slack.Experimental.Blocks.Types: SlackInteractiveResponseResponse :: Bool -> SlackInteractiveResponseResponse
+ Web.Slack.Experimental.Blocks.Types: SlackMarkdownText :: SlackText -> SlackTextObject
+ Web.Slack.Experimental.Blocks.Types: SlackMessage :: [SlackBlock] -> SlackMessage
+ Web.Slack.Experimental.Blocks.Types: SlackPlainText :: SlackText -> SlackTextObject
+ Web.Slack.Experimental.Blocks.Types: SlackPlainTextOnly :: SlackText -> SlackPlainTextOnly
+ Web.Slack.Experimental.Blocks.Types: SlackResponseUrl :: Text -> SlackResponseUrl
+ Web.Slack.Experimental.Blocks.Types: SlackStyleDanger :: SlackStyle
+ Web.Slack.Experimental.Blocks.Types: SlackStylePrimary :: SlackStyle
+ Web.Slack.Experimental.Blocks.Types: SlackText :: [Text] -> SlackText
+ Web.Slack.Experimental.Blocks.Types: [blockId] :: RichText -> Maybe SlackBlockId
+ Web.Slack.Experimental.Blocks.Types: [buttonConfirm] :: ButtonSettings -> OptionalSetting SlackConfirmObject
+ Web.Slack.Experimental.Blocks.Types: [buttonStyle] :: ButtonSettings -> OptionalSetting SlackStyle
+ Web.Slack.Experimental.Blocks.Types: [buttonUrl] :: ButtonSettings -> OptionalSetting (NonEmptyText 3000)
+ Web.Slack.Experimental.Blocks.Types: [buttonValue] :: ButtonSettings -> OptionalSetting (NonEmptyText 2000)
+ Web.Slack.Experimental.Blocks.Types: [confirmConfirm] :: ConfirmSettings -> Text
+ Web.Slack.Experimental.Blocks.Types: [confirmDeny] :: ConfirmSettings -> Text
+ Web.Slack.Experimental.Blocks.Types: [confirmStyle] :: ConfirmSettings -> OptionalSetting SlackStyle
+ Web.Slack.Experimental.Blocks.Types: [confirmText] :: ConfirmSettings -> Text
+ Web.Slack.Experimental.Blocks.Types: [confirmTitle] :: ConfirmSettings -> Text
+ Web.Slack.Experimental.Blocks.Types: [elements] :: RichText -> [RichTextSectionItem]
+ Web.Slack.Experimental.Blocks.Types: [rsBold] :: RichStyle -> Bool
+ Web.Slack.Experimental.Blocks.Types: [rsItalic] :: RichStyle -> Bool
+ Web.Slack.Experimental.Blocks.Types: [sarActionComponent] :: SlackActionResponse -> SlackActionComponent
+ Web.Slack.Experimental.Blocks.Types: [sarActionId] :: SlackActionResponse -> SlackActionId
+ Web.Slack.Experimental.Blocks.Types: [sarBlockId] :: SlackActionResponse -> SlackBlockId
+ Web.Slack.Experimental.Blocks.Types: [sipActions] :: SlackInteractivePayload -> [SlackActionResponse]
+ Web.Slack.Experimental.Blocks.Types: [sipName] :: SlackInteractivePayload -> Text
+ Web.Slack.Experimental.Blocks.Types: [sipResponseUrl] :: SlackInteractivePayload -> Maybe SlackResponseUrl
+ Web.Slack.Experimental.Blocks.Types: [sipTriggerId] :: SlackInteractivePayload -> Maybe Text
+ Web.Slack.Experimental.Blocks.Types: [sipUserId] :: SlackInteractivePayload -> Text
+ Web.Slack.Experimental.Blocks.Types: [sipUsername] :: SlackInteractivePayload -> Text
+ Web.Slack.Experimental.Blocks.Types: [slackButtonConfirm] :: SlackActionComponent -> Maybe SlackConfirmObject
+ Web.Slack.Experimental.Blocks.Types: [slackButtonStyle] :: SlackActionComponent -> Maybe SlackStyle
+ Web.Slack.Experimental.Blocks.Types: [slackButtonText] :: SlackActionComponent -> SlackButtonText
+ Web.Slack.Experimental.Blocks.Types: [slackButtonUrl] :: SlackActionComponent -> Maybe (NonEmptyText 3000)
+ Web.Slack.Experimental.Blocks.Types: [slackButtonValue] :: SlackActionComponent -> Maybe (NonEmptyText 2000)
+ Web.Slack.Experimental.Blocks.Types: [slackConfirmConfirm] :: SlackConfirmObject -> SlackPlainTextOnly
+ Web.Slack.Experimental.Blocks.Types: [slackConfirmDeny] :: SlackConfirmObject -> SlackPlainTextOnly
+ Web.Slack.Experimental.Blocks.Types: [slackConfirmStyle] :: SlackConfirmObject -> Maybe SlackStyle
+ Web.Slack.Experimental.Blocks.Types: [slackConfirmText] :: SlackConfirmObject -> SlackTextObject
+ Web.Slack.Experimental.Blocks.Types: [slackConfirmTitle] :: SlackConfirmObject -> SlackPlainTextOnly
+ Web.Slack.Experimental.Blocks.Types: [slackImageAltText] :: SlackImage -> !Text
+ Web.Slack.Experimental.Blocks.Types: [slackImageTitle] :: SlackImage -> !Maybe Text
+ Web.Slack.Experimental.Blocks.Types: [slackImageUrl] :: SlackImage -> !Text
+ Web.Slack.Experimental.Blocks.Types: [style] :: RichLinkAttrs -> RichStyle
+ Web.Slack.Experimental.Blocks.Types: [text] :: RichLinkAttrs -> Maybe Text
+ Web.Slack.Experimental.Blocks.Types: [unOptionalSetting] :: OptionalSetting a -> Maybe a
+ Web.Slack.Experimental.Blocks.Types: [unSlackActionId] :: SlackActionId -> NonEmptyText 255
+ Web.Slack.Experimental.Blocks.Types: [unSlackActionList] :: SlackActionList -> Refined SlackActionListConstraints [SlackAction]
+ Web.Slack.Experimental.Blocks.Types: [unSlackInteractiveResponseResponse] :: SlackInteractiveResponseResponse -> Bool
+ Web.Slack.Experimental.Blocks.Types: [unSlackResponseUrl] :: SlackResponseUrl -> Text
+ Web.Slack.Experimental.Blocks.Types: [unSlackTexts] :: SlackText -> [Text]
+ Web.Slack.Experimental.Blocks.Types: [url] :: RichLinkAttrs -> Text
+ Web.Slack.Experimental.Blocks.Types: actions :: ToSlackActionList as => as -> SlackMessage
+ Web.Slack.Experimental.Blocks.Types: actionsWithBlockId :: ToSlackActionList as => SlackBlockId -> as -> SlackMessage
+ Web.Slack.Experimental.Blocks.Types: button :: SlackActionId -> SlackButtonText -> ButtonSettings -> SlackAction
+ Web.Slack.Experimental.Blocks.Types: buttonSettings :: ButtonSettings
+ Web.Slack.Experimental.Blocks.Types: class Image a
+ Web.Slack.Experimental.Blocks.Types: class Markdown a
+ Web.Slack.Experimental.Blocks.Types: class Slack a
+ Web.Slack.Experimental.Blocks.Types: class ToSlackActionList a
+ Web.Slack.Experimental.Blocks.Types: confirm :: ConfirmSettings -> SlackConfirmObject
+ Web.Slack.Experimental.Blocks.Types: confirmAreYouSure :: ConfirmSettings
+ Web.Slack.Experimental.Blocks.Types: context :: SlackContext -> SlackMessage
+ Web.Slack.Experimental.Blocks.Types: data ButtonSettings
+ Web.Slack.Experimental.Blocks.Types: data ConfirmSettings
+ Web.Slack.Experimental.Blocks.Types: data RichItem
+ Web.Slack.Experimental.Blocks.Types: data RichLinkAttrs
+ Web.Slack.Experimental.Blocks.Types: data RichStyle
+ Web.Slack.Experimental.Blocks.Types: data RichText
+ Web.Slack.Experimental.Blocks.Types: data RichTextSectionItem
+ Web.Slack.Experimental.Blocks.Types: data SlackAccessory
+ Web.Slack.Experimental.Blocks.Types: data SlackAction
+ Web.Slack.Experimental.Blocks.Types: data SlackActionComponent
+ Web.Slack.Experimental.Blocks.Types: data SlackActionResponse
+ Web.Slack.Experimental.Blocks.Types: data SlackBlock
+ Web.Slack.Experimental.Blocks.Types: data SlackConfirmObject
+ Web.Slack.Experimental.Blocks.Types: data SlackContent
+ Web.Slack.Experimental.Blocks.Types: data SlackImage
+ Web.Slack.Experimental.Blocks.Types: data SlackInteractivePayload
+ Web.Slack.Experimental.Blocks.Types: data SlackInteractiveResponse
+ Web.Slack.Experimental.Blocks.Types: data SlackInteractiveResponseResponse
+ Web.Slack.Experimental.Blocks.Types: data SlackPlainTextOnly
+ Web.Slack.Experimental.Blocks.Types: data SlackStyle
+ Web.Slack.Experimental.Blocks.Types: data SlackTextObject
+ Web.Slack.Experimental.Blocks.Types: emptySetting :: OptionalSetting a
+ Web.Slack.Experimental.Blocks.Types: image :: Image a => SlackImage -> a
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.RichItem
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.RichStyle
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.RichText
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.RichTextSectionItem
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.SlackAccessory
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.SlackAction
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.SlackActionComponent
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.SlackActionId
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.SlackActionList
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.SlackActionResponse
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.SlackBlock
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.SlackButtonText
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.SlackConfirmObject
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.SlackContent
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.SlackContext
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.SlackInteractivePayload
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.SlackInteractiveResponseResponse
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.SlackMessage
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.SlackPlainTextOnly
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.SlackResponseUrl
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.SlackStyle
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.SlackTextObject
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Blocks.Types.SlackAccessory
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Blocks.Types.SlackAction
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Blocks.Types.SlackActionId
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Blocks.Types.SlackActionList
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Blocks.Types.SlackBlock
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Blocks.Types.SlackConfirmObject
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Blocks.Types.SlackContent
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Blocks.Types.SlackContext
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Blocks.Types.SlackInteractiveResponse
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Blocks.Types.SlackMessage
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Blocks.Types.SlackPlainTextOnly
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Blocks.Types.SlackResponseUrl
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Blocks.Types.SlackStyle
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Blocks.Types.SlackText
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Blocks.Types.SlackTextObject
+ Web.Slack.Experimental.Blocks.Types: instance Data.String.IsString (Web.Slack.Experimental.Blocks.Types.OptionalSetting Data.Text.Internal.Text)
+ Web.Slack.Experimental.Blocks.Types: instance Data.String.IsString (Web.Slack.Experimental.Blocks.Types.OptionalSetting GHC.Base.String)
+ Web.Slack.Experimental.Blocks.Types: instance Data.String.IsString Web.Slack.Experimental.Blocks.Types.SlackButtonText
+ Web.Slack.Experimental.Blocks.Types: instance Data.String.IsString Web.Slack.Experimental.Blocks.Types.SlackPlainTextOnly
+ Web.Slack.Experimental.Blocks.Types: instance Data.String.IsString Web.Slack.Experimental.Blocks.Types.SlackText
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Base.Monoid Web.Slack.Experimental.Blocks.Types.RichStyle
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Base.Monoid Web.Slack.Experimental.Blocks.Types.SlackContext
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Base.Monoid Web.Slack.Experimental.Blocks.Types.SlackMessage
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Base.Monoid Web.Slack.Experimental.Blocks.Types.SlackText
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Base.Semigroup Web.Slack.Experimental.Blocks.Types.RichStyle
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Base.Semigroup Web.Slack.Experimental.Blocks.Types.SlackContext
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Base.Semigroup Web.Slack.Experimental.Blocks.Types.SlackMessage
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Base.Semigroup Web.Slack.Experimental.Blocks.Types.SlackText
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.RichItem
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.RichLinkAttrs
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.RichStyle
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.RichText
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.RichTextSectionItem
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.SlackAccessory
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.SlackAction
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.SlackActionComponent
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.SlackActionId
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.SlackActionList
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.SlackBlock
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.SlackButtonText
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.SlackConfirmObject
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.SlackContent
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.SlackContext
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.SlackImage
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.SlackMessage
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.SlackPlainTextOnly
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.SlackResponseUrl
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.SlackStyle
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.SlackText
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.SlackTextObject
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq a => GHC.Classes.Eq (Web.Slack.Experimental.Blocks.Types.OptionalSetting a)
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.RichItem
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.RichLinkAttrs
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.RichStyle
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.RichText
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.RichTextSectionItem
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.SlackAccessory
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.SlackAction
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.SlackActionComponent
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.SlackActionId
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.SlackActionList
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.SlackActionResponse
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.SlackBlock
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.SlackButtonText
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.SlackContent
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.SlackContext
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.SlackImage
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.SlackInteractivePayload
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.SlackInteractiveResponse
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.SlackMessage
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.SlackPlainTextOnly
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.SlackResponseUrl
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.SlackStyle
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.SlackText
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.SlackTextObject
+ Web.Slack.Experimental.Blocks.Types: instance Web.Slack.Experimental.Blocks.Types.Image Web.Slack.Experimental.Blocks.Types.SlackContext
+ Web.Slack.Experimental.Blocks.Types: instance Web.Slack.Experimental.Blocks.Types.Image Web.Slack.Experimental.Blocks.Types.SlackMessage
+ Web.Slack.Experimental.Blocks.Types: instance Web.Slack.Experimental.Blocks.Types.Markdown Web.Slack.Experimental.Blocks.Types.SlackContext
+ Web.Slack.Experimental.Blocks.Types: instance Web.Slack.Experimental.Blocks.Types.Markdown Web.Slack.Experimental.Blocks.Types.SlackMessage
+ Web.Slack.Experimental.Blocks.Types: instance Web.Slack.Experimental.Blocks.Types.Slack Data.Text.Internal.Text
+ Web.Slack.Experimental.Blocks.Types: instance Web.Slack.Experimental.Blocks.Types.Slack GHC.Base.String
+ Web.Slack.Experimental.Blocks.Types: instance Web.Slack.Experimental.Blocks.Types.Slack GHC.Types.Int
+ Web.Slack.Experimental.Blocks.Types: instance Web.Slack.Experimental.Blocks.Types.Slack Web.Slack.Experimental.Blocks.Types.SlackButtonText
+ Web.Slack.Experimental.Blocks.Types: instance Web.Slack.Experimental.Blocks.Types.ToSlackActionList (Web.Slack.Experimental.Blocks.Types.SlackAction, Web.Slack.Experimental.Blocks.Types.SlackAction)
+ Web.Slack.Experimental.Blocks.Types: instance Web.Slack.Experimental.Blocks.Types.ToSlackActionList (Web.Slack.Experimental.Blocks.Types.SlackAction, Web.Slack.Experimental.Blocks.Types.SlackAction, Web.Slack.Experimental.Blocks.Types.SlackAction)
+ Web.Slack.Experimental.Blocks.Types: instance Web.Slack.Experimental.Blocks.Types.ToSlackActionList (Web.Slack.Experimental.Blocks.Types.SlackAction, Web.Slack.Experimental.Blocks.Types.SlackAction, Web.Slack.Experimental.Blocks.Types.SlackAction, Web.Slack.Experimental.Blocks.Types.SlackAction)
+ Web.Slack.Experimental.Blocks.Types: instance Web.Slack.Experimental.Blocks.Types.ToSlackActionList (Web.Slack.Experimental.Blocks.Types.SlackAction, Web.Slack.Experimental.Blocks.Types.SlackAction, Web.Slack.Experimental.Blocks.Types.SlackAction, Web.Slack.Experimental.Blocks.Types.SlackAction, Web.Slack.Experimental.Blocks.Types.SlackAction)
+ Web.Slack.Experimental.Blocks.Types: instance Web.Slack.Experimental.Blocks.Types.ToSlackActionList Web.Slack.Experimental.Blocks.Types.SlackAction
+ Web.Slack.Experimental.Blocks.Types: instance Web.Slack.Experimental.Blocks.Types.ToSlackActionList Web.Slack.Experimental.Blocks.Types.SlackActionList
+ Web.Slack.Experimental.Blocks.Types: link :: Text -> Maybe Text -> SlackText
+ Web.Slack.Experimental.Blocks.Types: markdown :: Markdown a => SlackText -> a
+ Web.Slack.Experimental.Blocks.Types: message :: Slack a => a -> SlackText
+ Web.Slack.Experimental.Blocks.Types: mrkdwn :: Slack a => a -> SlackTextObject
+ Web.Slack.Experimental.Blocks.Types: newtype OptionalSetting a
+ Web.Slack.Experimental.Blocks.Types: newtype SlackActionId
+ Web.Slack.Experimental.Blocks.Types: newtype SlackActionList
+ Web.Slack.Experimental.Blocks.Types: newtype SlackButtonText
+ Web.Slack.Experimental.Blocks.Types: newtype SlackContext
+ Web.Slack.Experimental.Blocks.Types: newtype SlackMessage
+ Web.Slack.Experimental.Blocks.Types: newtype SlackResponseUrl
+ Web.Slack.Experimental.Blocks.Types: newtype SlackText
+ Web.Slack.Experimental.Blocks.Types: plaintext :: Slack a => a -> SlackTextObject
+ Web.Slack.Experimental.Blocks.Types: plaintextonly :: Slack a => a -> SlackPlainTextOnly
+ Web.Slack.Experimental.Blocks.Types: sectionWithButtonAccessory :: SlackAction -> SlackText -> SlackBlock
+ Web.Slack.Experimental.Blocks.Types: setting :: a -> OptionalSetting a
+ Web.Slack.Experimental.Blocks.Types: textToContext :: Text -> SlackMessage
+ Web.Slack.Experimental.Blocks.Types: textToMessage :: Text -> SlackMessage
+ Web.Slack.Experimental.Blocks.Types: toSlackActionList :: ToSlackActionList a => a -> SlackActionList
+ Web.Slack.Experimental.Blocks.Types: type SlackActionListConstraints = SizeGreaterThan 0 && SizeLessThan 6
+ Web.Slack.Experimental.Blocks.Types: type SlackBlockId = NonEmptyText 255
+ Web.Slack.Experimental.Events.Types: Channel :: ChannelType
+ Web.Slack.Experimental.Events.Types: ChannelCreatedEvent :: CreatedChannel -> ChannelCreatedEvent
+ Web.Slack.Experimental.Events.Types: ChannelLeftEvent :: UserId -> ConversationId -> Text -> ChannelLeftEvent
+ Web.Slack.Experimental.Events.Types: CreatedChannel :: ConversationId -> Bool -> Text -> Text -> UserId -> SystemTime -> Bool -> Bool -> TeamId -> CreatedChannel
+ Web.Slack.Experimental.Events.Types: EventCallback :: EventId -> TeamId -> SystemTime -> Event -> EventCallback
+ Web.Slack.Experimental.Events.Types: EventChannelCreated :: ChannelCreatedEvent -> Event
+ Web.Slack.Experimental.Events.Types: EventChannelJoinMessage :: Event
+ Web.Slack.Experimental.Events.Types: EventChannelLeft :: ChannelLeftEvent -> Event
+ Web.Slack.Experimental.Events.Types: EventEventCallback :: EventCallback -> SlackWebhookEvent
+ Web.Slack.Experimental.Events.Types: EventId :: Text -> EventId
+ Web.Slack.Experimental.Events.Types: EventMessage :: MessageEvent -> Event
+ Web.Slack.Experimental.Events.Types: EventMessageChanged :: Event
+ Web.Slack.Experimental.Events.Types: EventUnknown :: Value -> Event
+ Web.Slack.Experimental.Events.Types: EventUnknownWebhook :: Value -> SlackWebhookEvent
+ Web.Slack.Experimental.Events.Types: EventUrlVerification :: UrlVerificationPayload -> SlackWebhookEvent
+ Web.Slack.Experimental.Events.Types: Group :: ChannelType
+ Web.Slack.Experimental.Events.Types: Im :: ChannelType
+ Web.Slack.Experimental.Events.Types: MessageEvent :: Maybe [SlackBlock] -> ConversationId -> Text -> ChannelType -> UserId -> Text -> Maybe Text -> Maybe Text -> Maybe Text -> MessageEvent
+ Web.Slack.Experimental.Events.Types: MessageId :: Text -> MessageId
+ Web.Slack.Experimental.Events.Types: MessageUpdateEvent :: MessageEvent -> MessageUpdateEvent
+ Web.Slack.Experimental.Events.Types: UrlVerificationPayload :: Text -> UrlVerificationPayload
+ Web.Slack.Experimental.Events.Types: UrlVerificationResponse :: Text -> UrlVerificationResponse
+ Web.Slack.Experimental.Events.Types: [$sel:actorId:ChannelLeftEvent] :: ChannelLeftEvent -> UserId
+ Web.Slack.Experimental.Events.Types: [$sel:appId:MessageEvent] :: MessageEvent -> Maybe Text
+ Web.Slack.Experimental.Events.Types: [$sel:blocks:MessageEvent] :: MessageEvent -> Maybe [SlackBlock]
+ Web.Slack.Experimental.Events.Types: [$sel:botId:MessageEvent] :: MessageEvent -> Maybe Text
+ Web.Slack.Experimental.Events.Types: [$sel:challenge:UrlVerificationPayload] :: UrlVerificationPayload -> Text
+ Web.Slack.Experimental.Events.Types: [$sel:challenge:UrlVerificationResponse] :: UrlVerificationResponse -> Text
+ Web.Slack.Experimental.Events.Types: [$sel:channel:ChannelCreatedEvent] :: ChannelCreatedEvent -> CreatedChannel
+ Web.Slack.Experimental.Events.Types: [$sel:channel:ChannelLeftEvent] :: ChannelLeftEvent -> ConversationId
+ Web.Slack.Experimental.Events.Types: [$sel:channel:MessageEvent] :: MessageEvent -> ConversationId
+ Web.Slack.Experimental.Events.Types: [$sel:channelType:MessageEvent] :: MessageEvent -> ChannelType
+ Web.Slack.Experimental.Events.Types: [$sel:contextTeamId:CreatedChannel] :: CreatedChannel -> TeamId
+ Web.Slack.Experimental.Events.Types: [$sel:created:CreatedChannel] :: CreatedChannel -> SystemTime
+ Web.Slack.Experimental.Events.Types: [$sel:creator:CreatedChannel] :: CreatedChannel -> UserId
+ Web.Slack.Experimental.Events.Types: [$sel:event:EventCallback] :: EventCallback -> Event
+ Web.Slack.Experimental.Events.Types: [$sel:eventId:EventCallback] :: EventCallback -> EventId
+ Web.Slack.Experimental.Events.Types: [$sel:eventTime:EventCallback] :: EventCallback -> SystemTime
+ Web.Slack.Experimental.Events.Types: [$sel:eventTs:ChannelLeftEvent] :: ChannelLeftEvent -> Text
+ Web.Slack.Experimental.Events.Types: [$sel:id:CreatedChannel] :: CreatedChannel -> ConversationId
+ Web.Slack.Experimental.Events.Types: [$sel:isChannel:CreatedChannel] :: CreatedChannel -> Bool
+ Web.Slack.Experimental.Events.Types: [$sel:isOrgShared:CreatedChannel] :: CreatedChannel -> Bool
+ Web.Slack.Experimental.Events.Types: [$sel:isShared:CreatedChannel] :: CreatedChannel -> Bool
+ Web.Slack.Experimental.Events.Types: [$sel:message:MessageUpdateEvent] :: MessageUpdateEvent -> MessageEvent
+ Web.Slack.Experimental.Events.Types: [$sel:name:CreatedChannel] :: CreatedChannel -> Text
+ Web.Slack.Experimental.Events.Types: [$sel:nameNormalized:CreatedChannel] :: CreatedChannel -> Text
+ Web.Slack.Experimental.Events.Types: [$sel:teamId:EventCallback] :: EventCallback -> TeamId
+ Web.Slack.Experimental.Events.Types: [$sel:text:MessageEvent] :: MessageEvent -> Text
+ Web.Slack.Experimental.Events.Types: [$sel:threadTs:MessageEvent] :: MessageEvent -> Maybe Text
+ Web.Slack.Experimental.Events.Types: [$sel:ts:MessageEvent] :: MessageEvent -> Text
+ Web.Slack.Experimental.Events.Types: [$sel:unEventId:EventId] :: EventId -> Text
+ Web.Slack.Experimental.Events.Types: [$sel:unMessageId:MessageId] :: MessageId -> Text
+ Web.Slack.Experimental.Events.Types: [$sel:user:MessageEvent] :: MessageEvent -> UserId
+ Web.Slack.Experimental.Events.Types: data ChannelCreatedEvent
+ Web.Slack.Experimental.Events.Types: data ChannelLeftEvent
+ Web.Slack.Experimental.Events.Types: data ChannelType
+ Web.Slack.Experimental.Events.Types: data CreatedChannel
+ Web.Slack.Experimental.Events.Types: data Event
+ Web.Slack.Experimental.Events.Types: data EventCallback
+ Web.Slack.Experimental.Events.Types: data MessageEvent
+ Web.Slack.Experimental.Events.Types: data MessageUpdateEvent
+ Web.Slack.Experimental.Events.Types: data SlackWebhookEvent
+ Web.Slack.Experimental.Events.Types: data UrlVerificationPayload
+ Web.Slack.Experimental.Events.Types: data UrlVerificationResponse
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Events.Types.ChannelCreatedEvent
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Events.Types.ChannelLeftEvent
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Events.Types.ChannelType
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Events.Types.CreatedChannel
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Events.Types.Event
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Events.Types.EventCallback
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Events.Types.EventId
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Events.Types.MessageEvent
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Events.Types.MessageId
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Events.Types.MessageUpdateEvent
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Events.Types.SlackWebhookEvent
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Events.Types.UrlVerificationPayload
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Events.Types.ChannelType
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Events.Types.UrlVerificationResponse
+ Web.Slack.Experimental.Events.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Events.Types.ChannelType
+ Web.Slack.Experimental.Events.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Events.Types.MessageId
+ Web.Slack.Experimental.Events.Types: instance GHC.Generics.Generic Web.Slack.Experimental.Events.Types.Event
+ Web.Slack.Experimental.Events.Types: instance GHC.Generics.Generic Web.Slack.Experimental.Events.Types.SlackWebhookEvent
+ Web.Slack.Experimental.Events.Types: instance GHC.Show.Show Web.Slack.Experimental.Events.Types.ChannelCreatedEvent
+ Web.Slack.Experimental.Events.Types: instance GHC.Show.Show Web.Slack.Experimental.Events.Types.ChannelLeftEvent
+ Web.Slack.Experimental.Events.Types: instance GHC.Show.Show Web.Slack.Experimental.Events.Types.ChannelType
+ Web.Slack.Experimental.Events.Types: instance GHC.Show.Show Web.Slack.Experimental.Events.Types.CreatedChannel
+ Web.Slack.Experimental.Events.Types: instance GHC.Show.Show Web.Slack.Experimental.Events.Types.Event
+ Web.Slack.Experimental.Events.Types: instance GHC.Show.Show Web.Slack.Experimental.Events.Types.EventCallback
+ Web.Slack.Experimental.Events.Types: instance GHC.Show.Show Web.Slack.Experimental.Events.Types.EventId
+ Web.Slack.Experimental.Events.Types: instance GHC.Show.Show Web.Slack.Experimental.Events.Types.MessageEvent
+ Web.Slack.Experimental.Events.Types: instance GHC.Show.Show Web.Slack.Experimental.Events.Types.MessageId
+ Web.Slack.Experimental.Events.Types: instance GHC.Show.Show Web.Slack.Experimental.Events.Types.MessageUpdateEvent
+ Web.Slack.Experimental.Events.Types: instance GHC.Show.Show Web.Slack.Experimental.Events.Types.SlackWebhookEvent
+ Web.Slack.Experimental.Events.Types: instance GHC.Show.Show Web.Slack.Experimental.Events.Types.UrlVerificationPayload
+ Web.Slack.Experimental.Events.Types: instance GHC.Show.Show Web.Slack.Experimental.Events.Types.UrlVerificationResponse
+ Web.Slack.Experimental.Events.Types: newtype EventId
+ Web.Slack.Experimental.Events.Types: newtype MessageId
+ Web.Slack.Experimental.RequestVerification: SlackRequestTimestamp :: ByteString -> SlackRequestTimestamp
+ Web.Slack.Experimental.RequestVerification: SlackSignature :: ByteString -> SlackSignature
+ Web.Slack.Experimental.RequestVerification: SlackSigningSecret :: ByteString -> SlackSigningSecret
+ Web.Slack.Experimental.RequestVerification: VerificationCannotParse :: Text -> SlackVerificationFailed
+ Web.Slack.Experimental.RequestVerification: VerificationMalformedSignature :: String -> SlackVerificationFailed
+ Web.Slack.Experimental.RequestVerification: VerificationMalformedTimestamp :: ByteString -> SlackVerificationFailed
+ Web.Slack.Experimental.RequestVerification: VerificationMissingSignature :: SlackVerificationFailed
+ Web.Slack.Experimental.RequestVerification: VerificationMissingTimestamp :: SlackVerificationFailed
+ Web.Slack.Experimental.RequestVerification: VerificationSignatureMismatch :: SlackVerificationFailed
+ Web.Slack.Experimental.RequestVerification: VerificationTimestampOutOfRange :: Int -> SlackVerificationFailed
+ Web.Slack.Experimental.RequestVerification: VerificationUndecodableSignature :: ByteString -> SlackVerificationFailed
+ Web.Slack.Experimental.RequestVerification: VerificationUnknownSignatureVersion :: ByteString -> SlackVerificationFailed
+ Web.Slack.Experimental.RequestVerification: data SlackVerificationFailed
+ Web.Slack.Experimental.RequestVerification: instance GHC.Classes.Eq Web.Slack.Experimental.RequestVerification.SlackRequestTimestamp
+ Web.Slack.Experimental.RequestVerification: instance GHC.Classes.Eq Web.Slack.Experimental.RequestVerification.SlackSignature
+ Web.Slack.Experimental.RequestVerification: instance GHC.Classes.Eq Web.Slack.Experimental.RequestVerification.SlackSigningSecret
+ Web.Slack.Experimental.RequestVerification: instance GHC.Classes.Eq Web.Slack.Experimental.RequestVerification.SlackVerificationFailed
+ Web.Slack.Experimental.RequestVerification: instance GHC.Exception.Type.Exception Web.Slack.Experimental.RequestVerification.SlackVerificationFailed
+ Web.Slack.Experimental.RequestVerification: instance GHC.Show.Show Web.Slack.Experimental.RequestVerification.SlackRequestTimestamp
+ Web.Slack.Experimental.RequestVerification: instance GHC.Show.Show Web.Slack.Experimental.RequestVerification.SlackSignature
+ Web.Slack.Experimental.RequestVerification: instance GHC.Show.Show Web.Slack.Experimental.RequestVerification.SlackSigningSecret
+ Web.Slack.Experimental.RequestVerification: instance GHC.Show.Show Web.Slack.Experimental.RequestVerification.SlackVerificationFailed
+ Web.Slack.Experimental.RequestVerification: instance Web.Internal.HttpApiData.FromHttpApiData Web.Slack.Experimental.RequestVerification.SlackRequestTimestamp
+ Web.Slack.Experimental.RequestVerification: instance Web.Internal.HttpApiData.FromHttpApiData Web.Slack.Experimental.RequestVerification.SlackSignature
+ Web.Slack.Experimental.RequestVerification: newtype SlackRequestTimestamp
+ Web.Slack.Experimental.RequestVerification: newtype SlackSignature
+ Web.Slack.Experimental.RequestVerification: newtype SlackSigningSecret
+ Web.Slack.Experimental.RequestVerification: validateRequest :: (MonadIO m, FromJSON a) => SlackSigningSecret -> SlackSignature -> SlackRequestTimestamp -> ByteString -> m (Either SlackVerificationFailed a)
+ Web.Slack.Experimental.RequestVerification: validateRequest' :: FromJSON a => NominalDiffTime -> SlackSigningSecret -> SlackSignature -> SlackRequestTimestamp -> ByteString -> Either SlackVerificationFailed a
+ Web.Slack.Internal: ResponseJSON :: Either ResponseSlackError a -> ResponseJSON a
+ Web.Slack.Internal: ResponseSlackError :: Text -> ResponseSlackError
+ Web.Slack.Internal: SlackConfig :: Manager -> Text -> SlackConfig
+ Web.Slack.Internal: [slackConfigManager] :: SlackConfig -> Manager
+ Web.Slack.Internal: [slackConfigToken] :: SlackConfig -> Text
+ Web.Slack.Internal: authenticateReq :: Text -> Request -> Request
+ Web.Slack.Internal: data ResponseSlackError
+ Web.Slack.Internal: data SlackConfig
+ Web.Slack.Internal: instance Data.Aeson.Types.FromJSON.FromJSON a => Data.Aeson.Types.FromJSON.FromJSON (Web.Slack.Internal.ResponseJSON a)
+ Web.Slack.Internal: instance GHC.Classes.Eq Web.Slack.Internal.ResponseSlackError
+ Web.Slack.Internal: instance GHC.Show.Show Web.Slack.Internal.ResponseSlackError
+ Web.Slack.Internal: mkSlackAuthenticateReq :: SlackConfig -> AuthenticatedRequest (AuthProtect "token")
+ Web.Slack.Internal: newtype ResponseJSON a
+ Web.Slack.Internal: run :: ClientM (ResponseJSON a) -> Manager -> IO (Response a)
+ Web.Slack.Internal: unnestErrors :: Either ClientError (ResponseJSON a) -> Response a
+ Web.Slack.Pager: fetchAllBy :: (MonadIO m, PagedRequest req, PagedResponse resp) => (req -> m (Response resp)) -> req -> m (LoadPage m (ResponseObject resp))
+ Web.Slack.Pager.Types: Cursor :: Text -> Cursor
+ Web.Slack.Pager.Types: ResponseMetadata :: Maybe Cursor -> ResponseMetadata
+ Web.Slack.Pager.Types: [responseMetadataNextCursor] :: ResponseMetadata -> Maybe Cursor
+ Web.Slack.Pager.Types: [unCursor] :: Cursor -> Text
+ Web.Slack.Pager.Types: class PagedRequest a
+ Web.Slack.Pager.Types: class PagedResponse a where {
+ Web.Slack.Pager.Types: getResponseData :: PagedResponse a => a -> [ResponseObject a]
+ Web.Slack.Pager.Types: getResponseMetadata :: PagedResponse a => a -> Maybe ResponseMetadata
+ Web.Slack.Pager.Types: instance Control.DeepSeq.NFData Web.Slack.Pager.Types.Cursor
+ Web.Slack.Pager.Types: instance Control.DeepSeq.NFData Web.Slack.Pager.Types.ResponseMetadata
+ Web.Slack.Pager.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Pager.Types.Cursor
+ Web.Slack.Pager.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Pager.Types.ResponseMetadata
+ Web.Slack.Pager.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Pager.Types.Cursor
+ Web.Slack.Pager.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Pager.Types.ResponseMetadata
+ Web.Slack.Pager.Types: instance Data.Hashable.Class.Hashable Web.Slack.Pager.Types.Cursor
+ Web.Slack.Pager.Types: instance GHC.Classes.Eq Web.Slack.Pager.Types.Cursor
+ Web.Slack.Pager.Types: instance GHC.Classes.Eq Web.Slack.Pager.Types.ResponseMetadata
+ Web.Slack.Pager.Types: instance GHC.Generics.Generic Web.Slack.Pager.Types.Cursor
+ Web.Slack.Pager.Types: instance GHC.Generics.Generic Web.Slack.Pager.Types.ResponseMetadata
+ Web.Slack.Pager.Types: instance GHC.Show.Show Web.Slack.Pager.Types.Cursor
+ Web.Slack.Pager.Types: instance GHC.Show.Show Web.Slack.Pager.Types.ResponseMetadata
+ Web.Slack.Pager.Types: instance Web.Internal.HttpApiData.ToHttpApiData Web.Slack.Pager.Types.Cursor
+ Web.Slack.Pager.Types: newtype Cursor
+ Web.Slack.Pager.Types: newtype ResponseMetadata
+ Web.Slack.Pager.Types: setCursor :: PagedRequest a => Maybe Cursor -> a -> a
+ Web.Slack.Pager.Types: type ResponseObject a;
+ Web.Slack.Pager.Types: }
+ Web.Slack.UsersConversations: UsersConversationsRequest :: Maybe Cursor -> Maybe Bool -> Maybe Int -> Maybe TeamId -> Maybe ConversationTypesList -> Maybe UserId -> UsersConversationsRequest
+ Web.Slack.UsersConversations: UsersConversationsResponse :: [Conversation] -> Maybe ResponseMetadata -> UsersConversationsResponse
+ Web.Slack.UsersConversations: [channels] :: UsersConversationsResponse -> [Conversation]
+ Web.Slack.UsersConversations: [cursor] :: UsersConversationsRequest -> Maybe Cursor
+ Web.Slack.UsersConversations: [excludeArchived] :: UsersConversationsRequest -> Maybe Bool
+ Web.Slack.UsersConversations: [limit] :: UsersConversationsRequest -> Maybe Int
+ Web.Slack.UsersConversations: [responseMetadata] :: UsersConversationsResponse -> Maybe ResponseMetadata
+ Web.Slack.UsersConversations: [teamId] :: UsersConversationsRequest -> Maybe TeamId
+ Web.Slack.UsersConversations: [types] :: UsersConversationsRequest -> Maybe ConversationTypesList
+ Web.Slack.UsersConversations: [user] :: UsersConversationsRequest -> Maybe UserId
+ Web.Slack.UsersConversations: data UsersConversationsRequest
+ Web.Slack.UsersConversations: data UsersConversationsResponse
+ Web.Slack.UsersConversations: instance Control.DeepSeq.NFData Web.Slack.UsersConversations.ConversationTypesList
+ Web.Slack.UsersConversations: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.UsersConversations.UsersConversationsResponse
+ Web.Slack.UsersConversations: instance Data.Default.Class.Default Web.Slack.UsersConversations.UsersConversationsRequest
+ Web.Slack.UsersConversations: instance GHC.Classes.Eq Web.Slack.UsersConversations.ConversationTypesList
+ Web.Slack.UsersConversations: instance GHC.Classes.Eq Web.Slack.UsersConversations.UsersConversationsRequest
+ Web.Slack.UsersConversations: instance GHC.Classes.Eq Web.Slack.UsersConversations.UsersConversationsResponse
+ Web.Slack.UsersConversations: instance GHC.Generics.Generic Web.Slack.UsersConversations.UsersConversationsRequest
+ Web.Slack.UsersConversations: instance GHC.Show.Show Web.Slack.UsersConversations.ConversationTypesList
+ Web.Slack.UsersConversations: instance GHC.Show.Show Web.Slack.UsersConversations.UsersConversationsRequest
+ Web.Slack.UsersConversations: instance GHC.Show.Show Web.Slack.UsersConversations.UsersConversationsResponse
+ Web.Slack.UsersConversations: instance Web.Internal.FormUrlEncoded.ToForm Web.Slack.UsersConversations.UsersConversationsRequest
+ Web.Slack.UsersConversations: instance Web.Internal.HttpApiData.ToHttpApiData Web.Slack.UsersConversations.ConversationTypesList
+ Web.Slack.UsersConversations: instance Web.Slack.Pager.Types.PagedRequest Web.Slack.UsersConversations.UsersConversationsRequest
+ Web.Slack.UsersConversations: instance Web.Slack.Pager.Types.PagedResponse Web.Slack.UsersConversations.UsersConversationsResponse
+ Web.Slack.UsersConversations: usersConversations :: SlackConfig -> UsersConversationsRequest -> IO (Response UsersConversationsResponse)
+ Web.Slack.UsersConversations: usersConversationsAll :: SlackConfig -> UsersConversationsRequest -> IO (LoadPage IO Conversation)
- Web.Slack.Chat: PostMsgReq :: Text -> Text -> Maybe Text -> Maybe Bool -> Maybe Text -> Maybe Text -> Maybe Bool -> Maybe Bool -> Maybe Text -> Maybe Bool -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Bool -> PostMsgReq
+ Web.Slack.Chat: PostMsgReq :: Text -> Maybe Text -> Maybe Text -> Maybe Bool -> Maybe Text -> Maybe Text -> Maybe Bool -> Maybe Bool -> Maybe Text -> Maybe Bool -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Bool -> PostMsgReq
- Web.Slack.Chat: PostMsgRsp :: String -> PostMsg -> PostMsgRsp
+ Web.Slack.Chat: PostMsgRsp :: Text -> PostMsg -> PostMsgRsp
- Web.Slack.Chat: [postMsgReqText] :: PostMsgReq -> Text
+ Web.Slack.Chat: [postMsgReqText] :: PostMsgReq -> Maybe Text
- Web.Slack.Chat: [postMsgRspTs] :: PostMsgRsp -> String
+ Web.Slack.Chat: [postMsgRspTs] :: PostMsgRsp -> Text
- Web.Slack.Conversation: ChannelConversation :: ConversationId -> Text -> Integer -> Bool -> Bool -> Integer -> Text -> Bool -> UserId -> Bool -> Bool -> [TeamId] -> Bool -> Bool -> Topic -> Purpose -> [Text] -> Integer -> ChannelConversation
+ Web.Slack.Conversation: ChannelConversation :: ConversationId -> Text -> Integer -> Bool -> Bool -> Integer -> Text -> Bool -> UserId -> Bool -> Bool -> Maybe [TeamId] -> Bool -> Maybe Bool -> Topic -> Purpose -> [Text] -> Maybe Integer -> ChannelConversation
- Web.Slack.Conversation: ListReq :: Maybe Bool -> [ConversationType] -> ListReq
+ Web.Slack.Conversation: ListReq :: Maybe Bool -> [ConversationType] -> Maybe Cursor -> Maybe Int -> Maybe TeamId -> ListReq
- Web.Slack.Conversation: ListRsp :: [Conversation] -> ListRsp
+ Web.Slack.Conversation: ListRsp :: [Conversation] -> Maybe ResponseMetadata -> ListRsp
- Web.Slack.Conversation: [channelIsMember] :: ChannelConversation -> Bool
+ Web.Slack.Conversation: [channelIsMember] :: ChannelConversation -> Maybe Bool
- Web.Slack.Conversation: [channelNumMembers] :: ChannelConversation -> Integer
+ Web.Slack.Conversation: [channelNumMembers] :: ChannelConversation -> Maybe Integer
- Web.Slack.Conversation: [channelSharedTeamIds] :: ChannelConversation -> [TeamId]
+ Web.Slack.Conversation: [channelSharedTeamIds] :: ChannelConversation -> Maybe [TeamId]

Files

CHANGELOG.md view
@@ -1,3 +1,62 @@+# 1.5.0.0++## Breaking changes++- `PostMsgRsp` uses a `Text` instead of a `String` for the ts.+- `PostMsgReq` uses `Maybe` for the `text` since it is optional if blocks or+  attachments are provided.++The following fields are now Maybe since they have been found to be absent in+some responses:++- `ChannelConversation.channelSharedTeamIds`+- `ChannelConversation.channelIsMember`+- `ChannelConversation.channelNumMembers`++### Breaking changes since development versions++- The `SlackBlockSection` constructor has a new `Maybe` field for accessories.++## New features++- `chat.update` bindings [#112](https://github.com/MercuryTechnologies/slack-web/pull/112)+- `users.conversations` bindings [#109](https://github.com/MercuryTechnologies/slack-web/pull/109)+- Generic pagination, making new requests easy to paginate+  [#105](https://github.com/MercuryTechnologies/slack-web/pull/105)+- Expose an Internal module, allowing for new endpoints to be defined without+  changing slack-web+  [#99](https://github.com/MercuryTechnologies/slack-web/pull/99)+- Three experimental features:+  - Schemas for the Events API+    [#107](https://github.com/MercuryTechnologies/slack-web/pull/107)+  - Request verification for webhooks+    [#106](https://github.com/MercuryTechnologies/slack-web/pull/106)+  - Support for [Blocks](https://api.slack.com/block-kit) including a builder,+    headers, and accessories+    [#100](https://github.com/MercuryTechnologies/slack-web/pull/100),+    [#115](https://github.com/MercuryTechnologies/slack-web/pull/115),+    [#116](https://github.com/MercuryTechnologies/slack-web/pull/116)++## Bug fixes++- Conversation parse errors will pick the variant first, *then* let it fail.+  Messages now identify the correct variant and missing fields.+  [#108](https://github.com/MercuryTechnologies/slack-web/pull/108)++## Project updates++[Mercury](https://mercury.com) now maintains this library.++CI has been rewritten, and a Nix flake is now provided.++We have gathered a fair corpus of sample data from Slack through the course of+writing [Slacklinker](https://github.com/MercuryTechnologies/Slacklinker),+which has been used to write a new snapshot test suite.++Thanks to Daniel Brice, Dennis Hennen, Claire Violet, Evan Relf, Jake Keuhlen,+Rebecca Turner, Matthew Mongeau, and Kevin Tang for their contributions to this+release!+ # 0.3.0.1  - Use Authorization header (44c3cf1)
LICENSE.md view
@@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 Juan Pedro Villa Isaza+Copyright (c) 2017 Juan Pedro Villa Isaza, (c) 2022 Mercury Technologies, Inc  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the
README.md view
@@ -1,17 +1,5 @@ # Haskell bindings for the Slack web API -[![][1]][0]--[![][3]][2]-[![][5]][4]--[0]: https://circleci.com/gh/jpvillaisaza/slack-web-[1]: https://circleci.com/gh/jpvillaisaza/slack-web.svg?style=svg-[2]: https://www.stackage.org/lts/package/slack-web-[3]: https://www.stackage.org/package/slack-web/badge/lts-[4]: https://www.stackage.org/nightly/package/slack-web-[5]: https://www.stackage.org/package/slack-web/badge/nightly- - Hackage: <https://hackage.haskell.org/package/slack-web>  - Slack web API: <https://api.slack.com/web>@@ -27,10 +15,6 @@ ```  ```-> import Control.Monad.Reader-```--``` > :set -XOverloadedStrings ``` @@ -39,14 +23,25 @@ ```  ```-> flip runReaderT slackConfig (Slack.apiTest Api.mkTestReq)+> Slack.apiTest (Slack.slackConfigManager slackConfig) Api.mkTestReq Right ... ```  ```-> flip runReaderT slackConfig (Slack.apiTest Api.mkTestReq { Api.testReqFoo = Just "bar" })+> Slack.apiTest (Slack.slackConfigManager slackConfig) Api.mkTestReq { Api.testReqFoo = Just "bar" } Right ... ```++## Contributing++This repository provides a `flake.nix` file which offers pre-commit hooks and+haskell-language-server. We use `fourmolu` for formatting, which is verified in+CI.++To use the flake, run `nix develop`, which will get you `cabal` and everything+else you need to work on `slack-web`.++Pull requests are welcome!  ## License 
− Setup.hs
@@ -1,2 +0,0 @@-import           Distribution.Simple-main = defaultMain
main/cli.hs view
@@ -1,47 +1,40 @@-{-# LANGUAGE TypeApplications #-} {-# LANGUAGE LambdaCase #-}-+{-# LANGUAGE TypeApplications #-}  -- base import Control.Exception (throwIO) import Control.Monad.IO.Class (liftIO)-import Data.Functor (void)-import System.Environment (getEnv)-import System.Exit (die)-import System.IO (hPutStrLn, stderr)-import Text.Read (readMaybe)- -- butcher-import UI.Butcher.Monadic  -- bytestring-import qualified Data.ByteString.Lazy.Char8 as BL  -- monad-loops import Control.Monad.Loops (iterateUntil)- -- mtl import Control.Monad.Reader (runReaderT)-+import Data.ByteString.Lazy.Char8 qualified as BL+import Data.Functor (void) -- pretty-simple-import Text.Pretty.Simple (pPrint, pShow)  -- slack-web-import qualified Web.Slack.Classy as Slack-import qualified Web.Slack.Common as Slack-import qualified Web.Slack.Conversation as SlackConversation  -- text-import qualified Data.Text as Text-import qualified Data.Text.Lazy as TextLazy-+import Data.Text qualified as Text+import Data.Text.Lazy qualified as TextLazy -- time-import Data.Time.Clock (getCurrentTime, nominalDay, addUTCTime)+import Data.Time.Clock (addUTCTime, getCurrentTime, nominalDay) import Data.Time.Clock.POSIX (posixSecondsToUTCTime)- -- servant-client-core-import Servant.Client.Core (ClientError(..), Response, ResponseF(..))-+import Servant.Client.Core (ClientError (..), Response, ResponseF (..))+import System.Environment (getEnv)+import System.Exit (die)+import System.IO (hPutStrLn, stderr)+import Text.Pretty.Simple (pPrint, pShow)+import Text.Read (readMaybe)+import UI.Butcher.Monadic+import Web.Slack.Classy qualified as Slack+import Web.Slack.Common qualified as Slack+import Web.Slack.Conversation qualified as SlackConversation  main :: IO () main = do@@ -49,17 +42,19 @@   mainFromCmdParserWithHelpDesc $ \helpDesc -> do     addHelpCommand helpDesc     addCmd "conversations.list" . addCmdImpl $ do-      let listReq = SlackConversation.ListReq-            { SlackConversation.listReqExcludeArchived = Just True-            , SlackConversation.listReqTypes =-              [ SlackConversation.PublicChannelType-              , SlackConversation.PrivateChannelType-              , SlackConversation.MpimType-              , SlackConversation.ImType-              ]-            }+      let listReq =+            SlackConversation.ListReq+              { SlackConversation.listReqExcludeArchived = Just True+              , SlackConversation.listReqTypes =+                  [ SlackConversation.PublicChannelType+                  , SlackConversation.PrivateChannelType+                  , SlackConversation.MpimType+                  , SlackConversation.ImType+                  ]+              }       Slack.conversationsList listReq-        `runReaderT` apiConfig >>= \case+        `runReaderT` apiConfig+        >>= \case           Right (SlackConversation.ListRsp cs) -> do             pPrint cs           Left err -> do@@ -76,7 +71,7 @@         if getsAll           then do             (`runReaderT` apiConfig) $ do-              fetchPage <- Slack.conversationsHistoryAll $ (SlackConversation.mkHistoryReq conversationId) { SlackConversation.historyReqCount = 2 }+              fetchPage <- Slack.conversationsHistoryAll $ (SlackConversation.mkHistoryReq conversationId) {SlackConversation.historyReqCount = 2}               void . iterateUntil null $ do                 result <- either (liftIO . throwIO) return =<< fetchPage                 liftIO $ pPrint result@@ -85,16 +80,18 @@             nowUtc <- getCurrentTime             let now = Slack.mkSlackTimestamp nowUtc                 thirtyDaysAgo = Slack.mkSlackTimestamp $ addUTCTime (nominalDay * negate 30) nowUtc-                histReq = SlackConversation.HistoryReq-                  { SlackConversation.historyReqChannel = conversationId-                  , SlackConversation.historyReqCount = 5-                  , SlackConversation.historyReqLatest = Just now-                  , SlackConversation.historyReqOldest = Just thirtyDaysAgo-                  , SlackConversation.historyReqInclusive = True-                  , SlackConversation.historyReqCursor = Nothing-                  }+                histReq =+                  SlackConversation.HistoryReq+                    { SlackConversation.historyReqChannel = conversationId+                    , SlackConversation.historyReqCount = 5+                    , SlackConversation.historyReqLatest = Just now+                    , SlackConversation.historyReqOldest = Just thirtyDaysAgo+                    , SlackConversation.historyReqInclusive = True+                    , SlackConversation.historyReqCursor = Nothing+                    }             Slack.conversationsHistory histReq-              `runReaderT` apiConfig >>= \case+              `runReaderT` apiConfig+              >>= \case                 Right rsp ->                   pPrint rsp                 Left err -> do@@ -111,25 +108,26 @@       pageSize <- addParamRead "PAGE_SIZE" (paramHelpStr "How many messages to get by a request.")       addCmdImpl $ do         -- NOTE: butcher's CmdParser isn't a MonadFail-        threadTimeStamp <- either-          (\emsg -> fail $ "Invalid timestamp " ++ show threadTimeStampStr ++ ": " ++ emsg)-          return-          ethreadTimeStamp+        threadTimeStamp <-+          either+            (\emsg -> fail $ "Invalid timestamp " ++ show threadTimeStampStr ++ ": " ++ emsg)+            return+            ethreadTimeStamp         nowUtc <- getCurrentTime         let now = Slack.mkSlackTimestamp nowUtc             tenDaysAgo = Slack.mkSlackTimestamp $ addUTCTime (nominalDay * negate 10) nowUtc-            req = (SlackConversation.mkRepliesReq conversationId threadTimeStamp)-              { SlackConversation.repliesReqLimit = pageSize-              , SlackConversation.repliesReqLatest = Just now-              , SlackConversation.repliesReqOldest = Just tenDaysAgo-              }+            req =+              (SlackConversation.mkRepliesReq conversationId threadTimeStamp)+                { SlackConversation.repliesReqLimit = pageSize+                , SlackConversation.repliesReqLatest = Just now+                , SlackConversation.repliesReqOldest = Just tenDaysAgo+                }         (`runReaderT` apiConfig) $ do           fetchPage <- Slack.repliesFetchAll req           void . iterateUntil null $ do             result <- either (liftIO . throwIO) return =<< fetchPage             liftIO $ pPrint result             return result-  peepInResponseBody err = do   {- Uncomment these lines when you want to see the JSON in the reponse body.
slack-web.cabal view
@@ -1,32 +1,108 @@+cabal-version: 2.2 name: slack-web-version: 0.4.0.0+version: 1.5.0.0  build-type: Simple-cabal-version: 1.20  license: MIT license-file: LICENSE.md -copyright: 2017 Juan Pedro Villa Isaza-author: Juan Pedro Villa Isaza <jpvillaisaza@gmail.com>-maintainer: Juan Pedro Villa Isaza <jpvillaisaza@gmail.com>+copyright: 2017 Juan Pedro Villa Isaza, 2022 Mercury Technologies, Inc+author: Juan Pedro Villa Isaza <jpvillaisaza@gmail.com>, Jade Lovelace <jadel@mercury.com>, Dennis Hennen <dennis@mercury.com>+maintainer: Jade Lovelace <jadel@mercury.com> -homepage: https://github.com/jpvillaisaza/slack-web-bug-reports: https://github.com/jpvillaisaza/slack-web/issues+homepage: https://github.com/MercuryTechnologies/slack-web+bug-reports: https://github.com/MercuryTechnologies/slack-web/issues  synopsis: Bindings for the Slack web API description: Haskell bindings for the Slack web API. +extra-source-files:+  tests/golden/SlackWebhookEvent/*.json+  tests/golden/SlackWebhookEvent/*.golden+  tests/golden/Conversation/*.json+  tests/golden/Conversation/*.golden+  tests/golden/UsersConversationsResponse/*.json+  tests/golden/UsersConversationsResponse/*.golden+  tests/golden/UpdateRsp/*.json+  tests/golden/UpdateRsp/*.golden+  tests/golden/BlockKitBuilderMessage/*.golden.json+ category: Web -tested-with: GHC == 8.10.7+tested-with: GHC == 8.10.7 || ==9.2.4 || == 9.4.2  extra-source-files:   CHANGELOG.md   README.md +common build-opts+  ghc-options:+    -Weverything+    -- missing-exported-signatures turns off the more strict -Wmissing-signatures. See https://ghc.haskell.org/trac/ghc/ticket/14794#ticket+    -Wno-missing-exported-signatures+    -- Requires explicit export lists for every module, a pain for large modules+    -Wno-missing-export-lists+    -- Requires explicit imports of _every_ function (e.g. '$'); too strict+    -Wno-missing-import-lists+    -- When GHC can't specialize a polymorphic function. No big deal and requires fixing underlying libraries to solve.+    -Wno-missed-specialisations+    -- See missed-specialisations+    -Wno-all-missed-specialisations+    -- Don't use Safe Haskell warnings+    -Wno-unsafe+    -- Warning for polymorphic local bindings. Don't think this is an issue+    -Wno-missing-local-signatures+    -- Don't warn if the monomorphism restriction is used+    -Wno-monomorphism-restriction+    --  Cabal isn’t setting this currently (introduced in GHC 8.10)+    -Wno-missing-safe-haskell-mode+    --  Cabal’s generate Path_*.hs doesn’t do this (fixed in https://github.com/haskell/cabal/pull/7352)+    -Wno-prepositive-qualified-module+    -- Some tooling gives this error+    -Wno-unused-packages+    -- Warns on every single data declaration+    -Wno-missing-kind-signatures +  default-extensions:+    AllowAmbiguousTypes+    ApplicativeDo+    BlockArguments+    DataKinds+    DeriveAnyClass+    DeriveFoldable+    DeriveFunctor+    DeriveGeneric+    DeriveTraversable+    DerivingVia+    FlexibleContexts+    FlexibleInstances+    FunctionalDependencies+    GADTs+    GeneralizedNewtypeDeriving+    ImportQualifiedPost+    InstanceSigs+    LambdaCase+    MonoLocalBinds+    MultiWayIf+    NamedFieldPuns+    NumericUnderscores+    NoImplicitPrelude+    OverloadedStrings+    PatternSynonyms+    PolyKinds+    RankNTypes+    RecordWildCards+    RecursiveDo+    ScopedTypeVariables+    StandaloneDeriving+    StandaloneKindSignatures+    TypeApplications+    TypeFamilies+    ViewPatterns+ library+  import: build-opts   hs-source-dirs:       src   exposed-modules:@@ -37,41 +113,59 @@       Web.Slack.Classy       Web.Slack.Common       Web.Slack.Conversation+      Web.Slack.Internal       Web.Slack.MessageParser       Web.Slack.Pager+      Web.Slack.Pager.Types       Web.Slack.Types       Web.Slack.User+      Web.Slack.UsersConversations+      Web.Slack.Experimental.Blocks+      Web.Slack.Experimental.Blocks.Types+      Web.Slack.Experimental.Events.Types+      Web.Slack.Experimental.RequestVerification   other-modules:       Web.Slack.Util+      Web.Slack.Prelude+      Web.Slack.AesonUtils   build-depends:-      aeson >= 1.0 && < 1.6-    , base >= 4.11 && < 4.16-    , scientific+      aeson >= 2.0 && < 2.2+    , base >= 4.11 && < 4.18+    , base16-bytestring+    , bytestring+    , classy-prelude     , containers+    , cryptonite+    , data-default-class     , deepseq-    , unordered-containers+    , either+    , errors     , hashable-    , http-api-data >= 0.3 && < 0.5-    , http-client >= 0.5 && < 0.7+    , http-api-data >= 0.3 && < 0.6+    , http-client >= 0.5 && < 0.8     , http-client-tls >= 0.3 && < 0.4-    , servant >= 0.12 && < 0.19-    , servant-client >= 0.12 && < 0.19-    , servant-client-core >= 0.12 && < 0.19-    , text >= 1.2 && < 1.3-    , transformers+    , megaparsec >= 5.0 && < 10+    , mono-traversable     , mtl+    , refined+    , scientific+    , servant >= 0.16 && < 0.20+    , servant-client >= 0.16 && < 0.20+    , servant-client-core >= 0.16 && < 0.20+    , string-conversions+    , string-variants >= 0.1.0.1+    , text (>= 1.2 && < 1.3) || (>= 2.0 && < 2.1)     , time-    , errors-    , megaparsec >= 5.0 && < 10+    , transformers+    , unordered-containers+    , vector   default-language:       Haskell2010   ghc-options:       -Wall --- the test suite doesn't depend on slack-web--- but rather has the src as an extra source directory.--- that allows us to refer to non exported modules from tests. test-suite tests+  import: build-opts   main-is:       Spec.hs   hs-source-dirs:@@ -79,19 +173,39 @@   type:       exitcode-stdio-1.0   other-modules:+      JSONGolden       Web.Slack.PagerSpec       Web.Slack.MessageParserSpec       Web.Slack.ConversationSpec+      Web.Slack.ChatSpec+      Web.Slack.UsersConversationsSpec+      Web.Slack.Experimental.RequestVerificationSpec+      Web.Slack.Experimental.Events.TypesSpec+      Web.Slack.Experimental.BlocksSpec+      TestImport+  build-tool-depends:+    hspec-discover:hspec-discover >=2.6.0 && <2.11   build-depends:       base-    , slack-web+    , QuickCheck     , aeson+    , aeson-pretty+    , bytestring+    , classy-prelude     , fakepull     , hspec+    , hspec-core+    , hspec-golden+    , mtl+    , pretty-simple+    , quickcheck-instances+    , slack-web+    , string-conversions+    , string-variants+    , template-haskell     , text+    , th-compat     , time-    , QuickCheck-    , quickcheck-instances   default-language:     Haskell2010   ghc-options:@@ -103,6 +217,7 @@   manual:      True  executable slack-web-cli+  import: build-opts   if flag(cli)     buildable: True   else@@ -125,4 +240,4 @@  source-repository head   type: git-  location: https://github.com/jpvillaisaza/slack-web+  location: https://github.com/MercuryTechnologies/slack-web
src/Web/Slack.hs view
@@ -1,212 +1,165 @@ {-# LANGUAGE DataKinds #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE CPP #-} {-# LANGUAGE TypeOperators #-}  ----------------------------------------------------------------------++----------------------------------------------------------------------+ -- | -- Module: Web.Slack -- Description: Provides Slack's Web API functions. -- *Since 0.4.0.0*: The API functions is now more intuitive for newbies -- than before. If you need compatiblity with the previous version, use -- 'Web.Slack.Classy' instead.--------------------------------------------------------------------------- module Web.Slack-  ( SlackConfig(..)-  , mkSlackConfig-  , apiTest-  , authTest-  , chatPostMessage-  , conversationsList-  , conversationsHistory-  , conversationsHistoryAll-  , conversationsReplies-  , repliesFetchAll-  , getUserDesc-  , usersList-  , userLookupByEmail-  , authenticateReq-  , Response-  , LoadPage+  ( SlackConfig (..),+    mkSlackConfig,++    -- * Endpoints+    apiTest,+    authTest,+    chatPostMessage,+    chatUpdate,+    conversationsList,+    conversationsListAll,+    conversationsHistory,+    conversationsHistoryAll,+    conversationsReplies,+    repliesFetchAll,+    getUserDesc,+    usersList,+    userLookupByEmail,+    UsersConversations.usersConversations,+    UsersConversations.usersConversationsAll,++    -- * Requests and responses+    authenticateReq,+    Response,+    LoadPage,   )-  where+where --- aeson-import Data.Aeson+-- FIXME: Web.Slack.Prelude --- base import Control.Arrow ((&&&))+import Data.Map qualified as Map import Data.Maybe-import Data.Proxy (Proxy(..))---- containers-import qualified Data.Map as Map---- http-client+import Data.Proxy (Proxy (..))+import Data.Text (Text) import Network.HTTP.Client (Manager, newManager)---- http-client-tls import Network.HTTP.Client.TLS (tlsManagerSettings)---- mtl-import Control.Monad.Reader---- servant import Servant.API hiding (addHeader)---- servant-client import Servant.Client hiding (Response, baseUrl)--#if MIN_VERSION_servant(0,16,0)-import Servant.Client.Core (AuthClientData, AuthenticatedRequest, Request, mkAuthenticatedRequest, addHeader)-#else-import Servant.Client.Core.Internal.Auth-import Servant.Client.Core (Request, addHeader)-#endif---- slack-web-import qualified Web.Slack.Api as Api-import qualified Web.Slack.Auth as Auth-import qualified Web.Slack.Conversation as Conversation-import qualified Web.Slack.Chat as Chat-import qualified Web.Slack.Common as Common-import qualified Web.Slack.User as User-import           Web.Slack.Pager---- text-import Data.Text (Text)--#if !MIN_VERSION_servant(0,13,0)-mkClientEnv :: Manager -> BaseUrl -> ClientEnv-mkClientEnv = ClientEnv-#endif---data SlackConfig-  = SlackConfig-  { slackConfigManager :: Manager-  , slackConfigToken :: Text-  }----- contains errors that can be returned by the slack API.--- constrast with 'SlackClientError' which additionally--- contains errors which occured during the network communication.-data ResponseSlackError = ResponseSlackError Text-  deriving (Eq, Show)----- |--- Internal type!----newtype ResponseJSON a = ResponseJSON (Either ResponseSlackError a)--instance FromJSON a => FromJSON (ResponseJSON a) where-    parseJSON = withObject "Response" $ \o -> do-        ok <- o .: "ok"-        ResponseJSON <$> if ok-           then Right <$> parseJSON (Object o)-           else Left . ResponseSlackError <$> o .: "error"-+import Servant.Client.Core (AuthenticatedRequest)+import Web.Slack.Api qualified as Api+import Web.Slack.Auth qualified as Auth+import Web.Slack.Chat qualified as Chat+import Web.Slack.Common qualified as Common+import Web.Slack.Conversation qualified as Conversation+import Web.Slack.Internal+import Web.Slack.Pager+import Web.Slack.User qualified as User+import Web.Slack.UsersConversations qualified as UsersConversations+import Prelude --- |------ type Api =-    "api.test"-      :> ReqBody '[FormUrlEncoded] Api.TestReq-      :> Post '[JSON] (ResponseJSON Api.TestRsp)-  :<|>-    "auth.test"+  "api.test"+    :> ReqBody '[FormUrlEncoded] Api.TestReq+    :> Post '[JSON] (ResponseJSON Api.TestRsp)+    :<|> "auth.test"       :> AuthProtect "token"       :> Post '[JSON] (ResponseJSON Auth.TestRsp)-  :<|>-    "conversations.list"+    :<|> "conversations.list"       :> AuthProtect "token"       :> ReqBody '[FormUrlEncoded] Conversation.ListReq       :> Post '[JSON] (ResponseJSON Conversation.ListRsp)-  :<|>-    "conversations.history"+    :<|> "conversations.history"       :> AuthProtect "token"       :> ReqBody '[FormUrlEncoded] Conversation.HistoryReq       :> Post '[JSON] (ResponseJSON Conversation.HistoryRsp)-  :<|>-    "conversations.replies"+    :<|> "conversations.replies"       :> AuthProtect "token"       :> ReqBody '[FormUrlEncoded] Conversation.RepliesReq       :> Post '[JSON] (ResponseJSON Conversation.HistoryRsp)-  :<|>-    "chat.postMessage"+    :<|> "chat.postMessage"       :> AuthProtect "token"       :> ReqBody '[FormUrlEncoded] Chat.PostMsgReq       :> Post '[JSON] (ResponseJSON Chat.PostMsgRsp)-  :<|>-    "users.list"+    :<|> "chat.update"       :> AuthProtect "token"+      :> ReqBody '[FormUrlEncoded] Chat.UpdateReq+      :> Post '[JSON] (ResponseJSON Chat.UpdateRsp)+    :<|> "users.list"+      :> AuthProtect "token"       :> Post '[JSON] (ResponseJSON User.ListRsp)-  :<|>-    "users.lookupByEmail"+    :<|> "users.lookupByEmail"       :> AuthProtect "token"       :> ReqBody '[FormUrlEncoded] User.Email       :> Post '[JSON] (ResponseJSON User.UserRsp) - -- | -- -- Check API calling code. -- -- <https://api.slack.com/methods/api.test>--apiTest-  :: Manager-  -> Api.TestReq-  -> IO (Response Api.TestRsp)+apiTest ::+  Manager ->+  Api.TestReq ->+  IO (Response Api.TestRsp) apiTest mgr req = run (apiTest_ req) mgr -apiTest_-  :: Api.TestReq-  -> ClientM (ResponseJSON Api.TestRsp)-+apiTest_ ::+  Api.TestReq ->+  ClientM (ResponseJSON Api.TestRsp)  -- | -- -- Check authentication and identity. -- -- <https://api.slack.com/methods/auth.test>--authTest-  :: SlackConfig-  -> IO (Response Auth.TestRsp)+authTest ::+  SlackConfig ->+  IO (Response Auth.TestRsp) authTest = do   authR <- mkSlackAuthenticateReq   run (authTest_ authR) . slackConfigManager -authTest_-  :: AuthenticatedRequest (AuthProtect "token")-  -> ClientM (ResponseJSON Auth.TestRsp)+authTest_ ::+  AuthenticatedRequest (AuthProtect "token") ->+  ClientM (ResponseJSON Auth.TestRsp)  -- | -- -- Retrieve conversations list. -- -- <https://api.slack.com/methods/conversations.list>--conversationsList-  :: SlackConfig-  -> Conversation.ListReq-  -> IO (Response Conversation.ListRsp)+conversationsList ::+  SlackConfig ->+  Conversation.ListReq ->+  IO (Response Conversation.ListRsp) conversationsList = flip $ \listReq -> do   authR <- mkSlackAuthenticateReq   run (conversationsList_ authR listReq) . slackConfigManager -conversationsList_-  :: AuthenticatedRequest (AuthProtect "token")-  -> Conversation.ListReq-  -> ClientM (ResponseJSON Conversation.ListRsp)+conversationsList_ ::+  AuthenticatedRequest (AuthProtect "token") ->+  Conversation.ListReq ->+  ClientM (ResponseJSON Conversation.ListRsp) +-- | Returns an action to send a request to get the list of conversations in the+--   workspace.+--+--   To fetch all replies in the conversation, run the returned 'LoadPage' action+--   repeatedly until it returns an empty list.+conversationsListAll ::+  SlackConfig ->+  -- | The first request to send. _NOTE_: 'Conversation.listReqCursor' is silently ignored.+  Conversation.ListReq ->+  -- | An action which returns a new page of messages every time called.+  --   If there are no pages anymore, it returns an empty list.+  IO (LoadPage IO Conversation.Conversation)+conversationsListAll = fetchAllBy . conversationsList  -- | --@@ -214,20 +167,18 @@ -- Consider using 'historyFetchAll' in combination with this function. -- -- <https://api.slack.com/methods/conversations.history>--conversationsHistory-  :: SlackConfig-  -> Conversation.HistoryReq-  -> IO (Response Conversation.HistoryRsp)+conversationsHistory ::+  SlackConfig ->+  Conversation.HistoryReq ->+  IO (Response Conversation.HistoryRsp) conversationsHistory = flip $ \histReq -> do   authR <- mkSlackAuthenticateReq   run (conversationsHistory_ authR histReq) . slackConfigManager -conversationsHistory_-  :: AuthenticatedRequest (AuthProtect "token")-  -> Conversation.HistoryReq-  -> ClientM (ResponseJSON Conversation.HistoryRsp)-+conversationsHistory_ ::+  AuthenticatedRequest (AuthProtect "token") ->+  Conversation.HistoryReq ->+  ClientM (ResponseJSON Conversation.HistoryRsp)  -- | --@@ -236,58 +187,69 @@ -- of a conversation. -- -- <https://api.slack.com/methods/conversations.replies>--conversationsReplies-  :: SlackConfig-  -> Conversation.RepliesReq-  -> IO (Response Conversation.HistoryRsp)+conversationsReplies ::+  SlackConfig ->+  Conversation.RepliesReq ->+  IO (Response Conversation.HistoryRsp) conversationsReplies = flip $ \repliesReq -> do   authR <- mkSlackAuthenticateReq   run (conversationsReplies_ authR repliesReq) . slackConfigManager -conversationsReplies_-  :: AuthenticatedRequest (AuthProtect "token")-  -> Conversation.RepliesReq-  -> ClientM (ResponseJSON Conversation.HistoryRsp)-+conversationsReplies_ ::+  AuthenticatedRequest (AuthProtect "token") ->+  Conversation.RepliesReq ->+  ClientM (ResponseJSON Conversation.HistoryRsp)  -- | -- -- Send a message to a channel. -- -- <https://api.slack.com/methods/chat.postMessage>--chatPostMessage-  :: SlackConfig-  -> Chat.PostMsgReq-  -> IO (Response Chat.PostMsgRsp)+chatPostMessage ::+  SlackConfig ->+  Chat.PostMsgReq ->+  IO (Response Chat.PostMsgRsp) chatPostMessage = flip $ \postReq -> do   authR <- mkSlackAuthenticateReq   run (chatPostMessage_ authR postReq) . slackConfigManager -chatPostMessage_-  :: AuthenticatedRequest (AuthProtect "token")-  -> Chat.PostMsgReq-  -> ClientM (ResponseJSON Chat.PostMsgRsp)+chatPostMessage_ ::+  AuthenticatedRequest (AuthProtect "token") ->+  Chat.PostMsgReq ->+  ClientM (ResponseJSON Chat.PostMsgRsp) +-- | Updates a message.+--+-- <https://api.slack.com/methods/chat.update>+chatUpdate ::+  SlackConfig ->+  Chat.UpdateReq ->+  IO (Response Chat.UpdateRsp)+chatUpdate = flip $ \updateReq -> do+  authR <- mkSlackAuthenticateReq+  run (chatUpdate_ authR updateReq) . slackConfigManager +chatUpdate_ ::+  AuthenticatedRequest (AuthProtect "token") ->+  Chat.UpdateReq ->+  ClientM (ResponseJSON Chat.UpdateRsp)+ -- | -- -- This method returns a list of all users in the team. -- This includes deleted/deactivated users. -- -- <https://api.slack.com/methods/users.list>--usersList-  :: SlackConfig-  -> IO (Response User.ListRsp)+usersList ::+  SlackConfig ->+  IO (Response User.ListRsp) usersList = do   authR <- mkSlackAuthenticateReq   run (usersList_ authR) . slackConfigManager -usersList_-  :: AuthenticatedRequest (AuthProtect "token")-  -> ClientM (ResponseJSON User.ListRsp)+usersList_ ::+  AuthenticatedRequest (AuthProtect "token") ->+  ClientM (ResponseJSON User.ListRsp)  -- | --@@ -295,49 +257,44 @@ -- This includes deleted/deactivated users. -- -- <https://api.slack.com/methods/users.lookupByEmail>--userLookupByEmail-  :: SlackConfig-  -> User.Email-  -> IO (Response User.UserRsp)+userLookupByEmail ::+  SlackConfig ->+  User.Email ->+  IO (Response User.UserRsp) userLookupByEmail = flip $ \email -> do   authR <- mkSlackAuthenticateReq   run (userLookupByEmail_ authR email) . slackConfigManager -userLookupByEmail_-  :: AuthenticatedRequest (AuthProtect "token")-  -> User.Email-  -> ClientM (ResponseJSON User.UserRsp)-+userLookupByEmail_ ::+  AuthenticatedRequest (AuthProtect "token") ->+  User.Email ->+  ClientM (ResponseJSON User.UserRsp)  -- | Returns a function to get a username from a 'Common.UserId'. -- Comes in handy to use 'Web.Slack.MessageParser.messageToHtml'-getUserDesc-  :: (Common.UserId -> Text)-  -- ^ A function to give a default username in case the username is unknown-  -> User.ListRsp-  -- ^ List of users as known by the slack server. See 'usersList'.-  -> (Common.UserId -> Text)-  -- ^ A function from 'Common.UserId' to username.+getUserDesc ::+  -- | A function to give a default username in case the username is unknown+  (Common.UserId -> Text) ->+  -- | List of users as known by the slack server. See 'usersList'.+  User.ListRsp ->+  -- | A function from 'Common.UserId' to username.+  (Common.UserId -> Text) getUserDesc unknownUserFn users =   let userMap = Map.fromList $ (User.userId &&& User.userName) <$> User.listRspMembers users-  in-    \userId -> fromMaybe (unknownUserFn userId) $ Map.lookup userId userMap-+   in \userId -> fromMaybe (unknownUserFn userId) $ Map.lookup userId userMap  -- | Returns an action to send a request to get the history of a conversation. -- --   To fetch all messages in the conversation, run the returned 'LoadPage' action --   repeatedly until it returns an empty list.-conversationsHistoryAll-  :: SlackConfig-  -> Conversation.HistoryReq-  -- ^ The first request to send. _NOTE_: 'Conversation.historyReqCursor' is silently ignored.-  -> IO (LoadPage IO Common.Message)-  -- ^ An action which returns a new page of messages every time called.+conversationsHistoryAll ::+  SlackConfig ->+  -- | The first request to send. _NOTE_: 'Conversation.historyReqCursor' is silently ignored.+  Conversation.HistoryReq ->+  -- | An action which returns a new page of messages every time called.   --   If there are no pages anymore, it returns an empty list.-conversationsHistoryAll = conversationsHistoryAllBy . conversationsHistory-+  IO (LoadPage IO Common.Message)+conversationsHistoryAll = fetchAllBy . conversationsHistory  -- | Returns an action to send a request to get the replies of a conversation. --@@ -348,15 +305,14 @@ --           of the thread. So every page returned by the 'LoadPage' action includes --           the first message of the thread. You should drop it if you want to --           collect messages in a thread without duplicates.-repliesFetchAll-  :: SlackConfig-  ->  Conversation.RepliesReq-  -- ^ The first request to send. _NOTE_: 'Conversation.repliesReqCursor' is silently ignored.-  -> IO (LoadPage IO Common.Message)-  -- ^ An action which returns a new page of messages every time called.+repliesFetchAll ::+  SlackConfig ->+  -- | The first request to send. _NOTE_: 'Conversation.repliesReqCursor' is silently ignored.+  Conversation.RepliesReq ->+  -- | An action which returns a new page of messages every time called.   --   If there are no pages anymore, it returns an empty list.-repliesFetchAll = repliesFetchAllBy . conversationsReplies-+  IO (LoadPage IO Common.Message)+repliesFetchAll = fetchAllBy . conversationsReplies  apiTest_   :<|> authTest_@@ -364,57 +320,12 @@   :<|> conversationsHistory_   :<|> conversationsReplies_   :<|> chatPostMessage_+  :<|> chatUpdate_   :<|> usersList_-  :<|> userLookupByEmail_-  =-  client (Proxy :: Proxy Api)----- |--------type instance AuthClientData (AuthProtect "token") =-  Text----- |-------authenticateReq-  :: Text-  -> Request-  -> Request-authenticateReq token =-  addHeader "Authorization" $ "Bearer " <> token----- |--------run-  :: ClientM (ResponseJSON a)-  -> Manager-  -> IO (Response a)-run clientAction mgr = do-  let baseUrl = BaseUrl Https "slack.com" 443 "/api"-  unnestErrors <$> liftIO (runClientM clientAction $ mkClientEnv mgr baseUrl)---mkSlackAuthenticateReq :: SlackConfig -> AuthenticatedRequest (AuthProtect "token")-mkSlackAuthenticateReq = (`mkAuthenticatedRequest` authenticateReq) . slackConfigToken---unnestErrors :: Either ClientError (ResponseJSON a) -> Response a-unnestErrors (Right (ResponseJSON (Right a))) = Right a-unnestErrors (Right (ResponseJSON (Left (ResponseSlackError serv))))-    = Left (Common.SlackError serv)-unnestErrors (Left slackErr) = Left (Common.ServantError slackErr)-+  :<|> userLookupByEmail_ =+    client (Proxy :: Proxy Api)  -- | Prepare a SlackConfig from a slack token. -- You can then call the other functions providing this in a reader context.--- mkSlackConfig :: Text -> IO SlackConfig mkSlackConfig token = SlackConfig <$> newManager tlsManagerSettings <*> pure token
+ src/Web/Slack/AesonUtils.hs view
@@ -0,0 +1,114 @@+module Web.Slack.AesonUtils where++import Data.Aeson+import Data.Aeson qualified as J+import Data.Aeson.Types (Pair)+import Data.Char qualified as Char+import Data.Text qualified as T+import Web.FormUrlEncoded qualified as F+import Web.Slack.Prelude++-- | Checks that a record's field labels each start with the given 'prefix',+-- then uses a given 'drop (length prefix)' derivingStrategy to drop that prefix from generated JSON.+--+-- If used in a Template Haskell splice, gives a compile-time error if the prefixes don't match up.+-- Warning: This function should not be used outside of a Template Haskell splice, as it calls `error` in the case that the prefixes don't match up!+--+-- Example usage:+--+-- data PrefixedRecord = PrefixedRecord { prefixedRecordOne :: Int, prefixedRecordTwo :: Char }++-- $(deriveFromJSON (jsonDeriveWithAffix "prefixedRecord" jsonDeriveOptionsSnakeCase) ''PrefixedRecord)++jsonDeriveWithAffix :: Text -> (Int -> Options) -> Options+jsonDeriveWithAffix prefix derivingStrategy =+  originalOptions+    { fieldLabelModifier = \fieldLabel ->+        if prefix `isPrefixOf` T.pack fieldLabel+          then originalModifier fieldLabel+          else error $ "Prefixes don't match: `" <> T.unpack prefix <> "` isn't a prefix of `" <> fieldLabel <> "`. Search for jsonDeriveWithAffix to learn more."+    }+  where+    originalOptions = derivingStrategy $ T.length prefix+    originalModifier = fieldLabelModifier originalOptions++camelToSnake :: String -> String+camelToSnake = camelTo2 '_'++lowerFirst :: String -> String+lowerFirst [] = []+lowerFirst (c : chars) = Char.toLower c : chars++jsonDeriveOptionsSnakeCase :: Int -> Options+jsonDeriveOptionsSnakeCase n =+  defaultOptions+    { fieldLabelModifier = camelToSnake . lowerFirst . drop n+    , omitNothingFields = True+    , constructorTagModifier = camelToSnake . lowerFirst . drop n+    }++-- | Create a 'Value' from a list of name\/value @Maybe Pair@'s.+-- For 'Nothing', instead of outputting @null@, that field will not be output at all.+-- If duplicate keys arise, later keys and their associated values win.+--+-- Example:+--+-- @+-- objectOptional+--   [ "always" .=! 1+--   , "just" .=? Just 2+--   , "nothing" .=? Nothing+--   ]+-- @+--+-- will result in the JSON+--+-- @+-- {+--   "always": 1,+--   "just": 2+-- }+-- @+--+-- The field @nothing@ is ommited because it was 'Nothing'.+objectOptional :: [Maybe Pair] -> Value+objectOptional = J.object . catMaybes++-- | Encode a value for 'objectOptional'+(.=!) :: ToJSON v => Key -> v -> Maybe Pair+key .=! val = Just (key .= val)++infixr 8 .=!++-- | Encode a Maybe value for 'objectOptional'+(.=?) :: ToJSON v => Key -> Maybe v -> Maybe Pair+key .=? mVal = fmap (key .=) mVal++infixr 8 .=?++-- | Conditionally encode a value for 'objectOptional'+(?.>) :: Bool -> Pair -> Maybe Pair+True ?.> pair = Just pair+False ?.> _ = Nothing++infixr 7 ?.>++-- | Conditionally express a pair in a JSON series+thenPair :: Bool -> J.Series -> J.Series+thenPair True s = s+thenPair False _ = mempty++infixr 7 `thenPair`++snakeCaseOptions :: Options+snakeCaseOptions =+  defaultOptions+    { fieldLabelModifier = camelTo2 '_'+    , constructorTagModifier = camelTo2 '_'+    }++snakeCaseFormOptions :: F.FormOptions+snakeCaseFormOptions =+  F.defaultFormOptions+    { F.fieldLabelModifier = camelTo2 '_'+    }
src/Web/Slack/Api.hs view
@@ -3,75 +3,53 @@ {-# LANGUAGE TemplateHaskell #-}  ----------------------------------------------------------------------++----------------------------------------------------------------------+ -- | -- Module: Web.Slack.Api -- Description:---------------------------------------------------------------------------------- module Web.Slack.Api-  ( TestReq(..)-  , mkTestReq-  , TestRsp(..)+  ( TestReq (..),+    mkTestReq,+    TestRsp (..),   )-  where+where +-- FIXME: Web.Slack.Prelude+ -- aeson-import Data.Aeson.TH  -- base-import GHC.Generics (Generic)  -- deepseq import Control.DeepSeq (NFData)-+import Data.Aeson.TH -- http-api-data-import Web.FormUrlEncoded  -- slack-web-import Web.Slack.Util  -- text import Data.Text (Text)----- |------+import GHC.Generics (Generic)+import Web.FormUrlEncoded+import Web.Slack.Util+import Prelude -data TestReq =-  TestReq-    { testReqError :: Maybe Text-    , testReqFoo :: Maybe Text-    }-  deriving (Eq, Generic, Show)+data TestReq = TestReq+  { testReqError :: Maybe Text+  , testReqFoo :: Maybe Text+  }+  deriving stock (Eq, Generic, Show)  instance NFData TestReq ---- |------- $(deriveJSON (jsonOpts "testReq") ''TestReq) ---- |------- instance ToForm TestReq where   toForm =     genericToForm (formOpts "testReq") ---- |------- mkTestReq :: TestReq mkTestReq =   TestReq@@ -79,20 +57,11 @@     , testReqFoo = Nothing     } ---- |--------data TestRsp =-  TestRsp-    { testRspArgs :: Maybe TestReq-    }-  deriving (Eq, Generic, Show)+data TestRsp = TestRsp+  { testRspArgs :: Maybe TestReq+  }+  deriving stock (Eq, Generic, Show)  instance NFData TestRsp --- |------ $(deriveFromJSON (jsonOpts "testRsp") ''TestRsp)
src/Web/Slack/Auth.hs view
@@ -3,50 +3,41 @@ {-# LANGUAGE TemplateHaskell #-}  ----------------------------------------------------------------------++----------------------------------------------------------------------+ -- | -- Module: Web.Slack.Auth -- Description:---------------------------------------------------------------------------------+module Web.Slack.Auth where -module Web.Slack.Auth-  where+-- FIXME: Web.Slack.Prelude  -- aeson-import Data.Aeson.TH  -- base-import GHC.Generics (Generic)  -- deepseq import Control.DeepSeq (NFData)-+import Data.Aeson.TH -- slack-web-import Web.Slack.Util  -- text import Data.Text (Text)----- |------+import GHC.Generics (Generic)+import Web.Slack.Util+import Prelude -data TestRsp =-  TestRsp-    { testRspUrl :: Text-    , testRspTeam :: Text-    , testRspUser :: Text-    , testRspTeamId :: Text-    , testRspUserId :: Text-    , testRspEnterpriseId :: Maybe Text-    }-  deriving (Eq, Generic, Show)+data TestRsp = TestRsp+  { testRspUrl :: Text+  , testRspTeam :: Text+  , testRspUser :: Text+  , testRspTeamId :: Text+  , testRspUserId :: Text+  , testRspEnterpriseId :: Maybe Text+  }+  deriving stock (Eq, Generic, Show)  instance NFData TestRsp-  $(deriveJSON (jsonOpts "testRsp") ''TestRsp)
src/Web/Slack/Chat.hs view
@@ -2,123 +2,78 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} -------------------------------------------------------------------------- |--- Module: Web.Slack.Chat--- Description:--------------------------------------------------------------------------------- module Web.Slack.Chat-  ( PostMsg(..)-  , PostMsgReq(..)-  , mkPostMsgReq-  , PostMsgRsp(..)+  ( PostMsg (..),+    PostMsgReq (..),+    mkPostMsgReq,+    PostMsgRsp (..),+    UpdateReq (..),+    mkUpdateReq,+    UpdateRsp (..),   )-  where---- aeson-import Data.Aeson.TH---- base-import GHC.Generics (Generic)---- deepseq-import Control.DeepSeq (NFData)+where --- http-api-data import Web.FormUrlEncoded---- slack-web+import Web.Slack.Conversation (ConversationId)+import Web.Slack.Prelude import Web.Slack.Util --- text-import Data.Text (Text)---data PostMsg =-  PostMsg-    { postMsgText :: Text-    , postMsgParse :: Maybe Text-    , postMsgLinkNames :: Maybe Bool-    , postMsgAttachments :: Maybe Text-    , postMsgUnfurlLinks :: Maybe Bool-    , postMsgUnfurlMedia :: Maybe Bool-    , postMsgUsername :: Maybe Text-    , postMsgAsUser :: Maybe Bool-    , postMsgIconUrl :: Maybe Text-    , postMsgIconEmoji :: Maybe Text-    , postMsgThreadTs :: Maybe Text-    , postMsgReplyBroadcast :: Maybe Bool-    }-  deriving (Eq, Generic, Show)+data PostMsg = PostMsg+  { postMsgText :: Text+  , postMsgParse :: Maybe Text+  , postMsgLinkNames :: Maybe Bool+  , postMsgAttachments :: Maybe Text+  , postMsgUnfurlLinks :: Maybe Bool+  , postMsgUnfurlMedia :: Maybe Bool+  , postMsgUsername :: Maybe Text+  , postMsgAsUser :: Maybe Bool+  , postMsgIconUrl :: Maybe Text+  , postMsgIconEmoji :: Maybe Text+  , postMsgThreadTs :: Maybe Text+  , postMsgReplyBroadcast :: Maybe Bool+  }+  deriving stock (Eq, Generic, Show)  instance NFData PostMsg ---- |------- $(deriveJSON (jsonOpts "postMsg") ''PostMsg) ---- |--------data PostMsgReq =-  PostMsgReq-    { postMsgReqChannel :: Text-    , postMsgReqText :: Text-    , postMsgReqParse :: Maybe Text-    , postMsgReqLinkNames :: Maybe Bool-    , postMsgReqAttachments :: Maybe Text-    , postMsgReqBlocks :: Maybe Text-    , postMsgReqUnfurlLinks :: Maybe Bool-    , postMsgReqUnfurlMedia :: Maybe Bool-    , postMsgReqUsername :: Maybe Text-    , postMsgReqAsUser :: Maybe Bool-    , postMsgReqIconUrl :: Maybe Text-    , postMsgReqIconEmoji :: Maybe Text-    , postMsgReqThreadTs :: Maybe Text-    , postMsgReqReplyBroadcast :: Maybe Bool-    }-  deriving (Eq, Generic, Show)+data PostMsgReq = PostMsgReq+  { postMsgReqChannel :: Text+  , postMsgReqText :: Maybe Text+  -- ^ One of 'postMsgReqText', 'postMsgReqAttachments', or 'postMsgReqBlocks'+  -- is required.+  , postMsgReqParse :: Maybe Text+  , postMsgReqLinkNames :: Maybe Bool+  , postMsgReqAttachments :: Maybe Text+  , postMsgReqBlocks :: Maybe Text+  , postMsgReqUnfurlLinks :: Maybe Bool+  , postMsgReqUnfurlMedia :: Maybe Bool+  , postMsgReqUsername :: Maybe Text+  , postMsgReqAsUser :: Maybe Bool+  , postMsgReqIconUrl :: Maybe Text+  , postMsgReqIconEmoji :: Maybe Text+  , postMsgReqThreadTs :: Maybe Text+  , postMsgReqReplyBroadcast :: Maybe Bool+  }+  deriving stock (Eq, Generic, Show)  instance NFData PostMsgReq ---- |------- $(deriveJSON (jsonOpts "postMsgReq") ''PostMsgReq) ---- |------- instance ToForm PostMsgReq where   toForm =     genericToForm (formOpts "postMsgReq") ---- |--------mkPostMsgReq-  :: Text-  -> Text-  -> PostMsgReq+mkPostMsgReq ::+  Text ->+  Text ->+  PostMsgReq mkPostMsgReq channel text =   PostMsgReq     { postMsgReqChannel = channel-    , postMsgReqText = text+    , postMsgReqText = Just text     , postMsgReqParse = Nothing     , postMsgReqLinkNames = Nothing     , postMsgReqAttachments = Nothing@@ -133,18 +88,61 @@     , postMsgReqReplyBroadcast = Nothing     } +data PostMsgRsp = PostMsgRsp+  { postMsgRspTs :: Text+  , postMsgRspMessage :: PostMsg+  }+  deriving stock (Eq, Generic, Show) --- |------+instance NFData PostMsgRsp -data PostMsgRsp =-  PostMsgRsp-    { postMsgRspTs :: String-    , postMsgRspMessage :: PostMsg+$(deriveFromJSON (jsonOpts "postMsgRsp") ''PostMsgRsp)++-- | <https://api.slack.com/methods/chat.update>+data UpdateReq = UpdateReq+  { updateReqChannel :: ConversationId+  , updateReqTs :: Text+  -- ^ \"Timestamp of the message to be updated.\"+  , updateReqAsUser :: Maybe Bool+  -- ^ \"Pass true to update the message as the authed user. Bot users in this context are considered authed users.\"+  , updateReqAttachments :: Maybe Text+  -- ^ \"A JSON-based array of structured attachments, presented as a URL-encoded string. This field is required when not presenting text. If you don't include this field, the message's previous attachments will be retained. To remove previous attachments, include an empty array for this field.\"+  , updateReqLinkNames :: Maybe Bool+  , updateReqMetadata :: Maybe Text+  , updateReqParse :: Maybe Text+  , updateReqReplyBroadcast :: Maybe Bool+  -- ^ \"Broadcast an existing thread reply to make it visible to everyone in the channel or conversation.\"+  , updateReqText :: Maybe Text+  -- ^ \"New text for the message, using the default formatting rules. It's not required when presenting blocks or attachments.\"+  }+  deriving stock (Eq, Generic, Show)++instance ToForm UpdateReq where+  toForm = genericToForm (formOpts "updateReq")++mkUpdateReq :: ConversationId -> Text -> UpdateReq+mkUpdateReq channel ts =+  UpdateReq+    { updateReqChannel = channel+    , updateReqTs = ts+    , updateReqAsUser = Nothing+    , updateReqAttachments = Nothing+    , updateReqLinkNames = Nothing+    , updateReqMetadata = Nothing+    , updateReqParse = Nothing+    , updateReqReplyBroadcast = Nothing+    , updateReqText = Nothing     }-  deriving (Eq, Generic, Show) -instance NFData PostMsgRsp+data UpdateRsp = UpdateRsp+  { updateRspChannel :: ConversationId+  , updateRspTs :: Text+  , updateRspText :: Text+  -- FIXME(jadel): this does look suspiciously like the same schema as+  -- MessageEvent based on the example I received, but Slack hasn't documented+  -- what it actually is, so let's not try to parse it for now.+  -- , message :: MessageEvent+  }+  deriving stock (Eq, Generic, Show) -$(deriveFromJSON (jsonOpts "postMsgRsp") ''PostMsgRsp)+$(deriveFromJSON (jsonOpts "updateRsp") ''UpdateRsp)
src/Web/Slack/Classy.hs view
@@ -1,65 +1,61 @@-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE CPP #-}  ----------------------------------------------------------------------++----------------------------------------------------------------------+ -- | -- Module: Web.Slack.Classy -- Description: For compatibility with Web.Slack prior to v0.4.0.0.--------------------------------------------------------------------------------- module Web.Slack.Classy-  ( SlackConfig(..)-  , mkSlackConfig-  , apiTest-  , authTest-  , chatPostMessage-  , conversationsList-  , conversationsHistory-  , conversationsHistoryAll-  , conversationsReplies-  , repliesFetchAll-  , getUserDesc-  , usersList-  , userLookupByEmail-  , authenticateReq-  , Response-  , LoadPage-  , HasManager(..)-  , HasToken(..)+  ( SlackConfig (..),+    mkSlackConfig,+    apiTest,+    authTest,+    chatPostMessage,+    conversationsList,+    conversationsHistory,+    conversationsHistoryAll,+    conversationsReplies,+    repliesFetchAll,+    getUserDesc,+    usersList,+    userLookupByEmail,+    authenticateReq,+    Response,+    LoadPage,+    HasManager (..),+    HasToken (..),   )-  where+where +-- FIXME: Web.Slack.Prelude+ -- base import Control.Arrow ((&&&))-import Data.Maybe- -- containers-import qualified Data.Map as Map  -- http-client-import Network.HTTP.Client (Manager)  -- mtl import Control.Monad.Reader-+import Data.Map qualified as Map+import Data.Maybe -- slack-web-import qualified Web.Slack.Api as Api-import qualified Web.Slack.Auth as Auth-import qualified Web.Slack.Conversation as Conversation-import qualified Web.Slack.Chat as Chat-import qualified Web.Slack.Common as Common-import qualified Web.Slack.User as User-import           Web.Slack.Pager-import qualified Web.Slack as NonClassy-import           Web.Slack (SlackConfig (..), authenticateReq, mkSlackConfig)  -- text import Data.Text (Text)+import Network.HTTP.Client (Manager)+import Web.Slack (SlackConfig (..), authenticateReq, mkSlackConfig)+import Web.Slack qualified as NonClassy+import Web.Slack.Api qualified as Api+import Web.Slack.Auth qualified as Auth+import Web.Slack.Chat qualified as Chat+import Web.Slack.Common qualified as Common+import Web.Slack.Conversation qualified as Conversation+import Web.Slack.Pager+import Web.Slack.User qualified as User+import Prelude  #if !MIN_VERSION_servant(0,13,0) mkClientEnv :: Manager -> BaseUrl -> ClientEnv@@ -68,70 +64,62 @@  -- | Implemented by 'SlackConfig' class HasManager a where-    getManager :: a -> Manager+  getManager :: a -> Manager  -- | Implemented by 'SlackConfig' class HasToken a where-    getToken :: a -> Text+  getToken :: a -> Text  instance HasManager SlackConfig where-    getManager = slackConfigManager-instance HasToken SlackConfig where-    getToken = slackConfigToken+  getManager = slackConfigManager +instance HasToken SlackConfig where+  getToken = slackConfigToken  -- | -- -- Check API calling code. -- -- <https://api.slack.com/methods/api.test>--apiTest-  :: (MonadReader env m, HasManager env, MonadIO m)-  => Api.TestReq-  -> m (Response Api.TestRsp)+apiTest ::+  (MonadReader env m, HasManager env, MonadIO m) =>+  Api.TestReq ->+  m (Response Api.TestRsp) apiTest = liftToReader . flip (NonClassy.apiTest . getManager) - -- | -- -- Check authentication and identity. -- -- <https://api.slack.com/methods/auth.test>--authTest-  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)-  => m (Response Auth.TestRsp)+authTest ::+  (MonadReader env m, HasManager env, HasToken env, MonadIO m) =>+  m (Response Auth.TestRsp) authTest = liftNonClassy NonClassy.authTest - -- | -- -- Retrieve conversations list. -- -- <https://api.slack.com/methods/conversations.list>--conversationsList-  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)-  => Conversation.ListReq-  -> m (Response Conversation.ListRsp)+conversationsList ::+  (MonadReader env m, HasManager env, HasToken env, MonadIO m) =>+  Conversation.ListReq ->+  m (Response Conversation.ListRsp) conversationsList = liftNonClassy . flip NonClassy.conversationsList - -- | -- -- Retrieve ceonversation history. -- Consider using 'historyFetchAll' in combination with this function. -- -- <https://api.slack.com/methods/conversations.history>--conversationsHistory-  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)-  => Conversation.HistoryReq-  -> m (Response Conversation.HistoryRsp)+conversationsHistory ::+  (MonadReader env m, HasManager env, HasToken env, MonadIO m) =>+  Conversation.HistoryReq ->+  m (Response Conversation.HistoryRsp) conversationsHistory = liftNonClassy . flip NonClassy.conversationsHistory - -- | -- -- Retrieve replies of a conversation.@@ -139,82 +127,71 @@ -- of a conversation. -- -- <https://api.slack.com/methods/conversations.replies>--conversationsReplies-  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)-  => Conversation.RepliesReq-  -> m (Response Conversation.HistoryRsp)+conversationsReplies ::+  (MonadReader env m, HasManager env, HasToken env, MonadIO m) =>+  Conversation.RepliesReq ->+  m (Response Conversation.HistoryRsp) conversationsReplies = liftNonClassy . flip NonClassy.conversationsReplies - -- | -- -- Send a message to a channel. -- -- <https://api.slack.com/methods/chat.postMessage>--chatPostMessage-  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)-  => Chat.PostMsgReq-  -> m (Response Chat.PostMsgRsp)+chatPostMessage ::+  (MonadReader env m, HasManager env, HasToken env, MonadIO m) =>+  Chat.PostMsgReq ->+  m (Response Chat.PostMsgRsp) chatPostMessage = liftNonClassy . flip NonClassy.chatPostMessage - -- | -- -- This method returns a list of all users in the team. -- This includes deleted/deactivated users. -- -- <https://api.slack.com/methods/users.list>--usersList-  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)-  => m (Response User.ListRsp)+usersList ::+  (MonadReader env m, HasManager env, HasToken env, MonadIO m) =>+  m (Response User.ListRsp) usersList = liftNonClassy NonClassy.usersList - -- | -- -- This method returns a list of all users in the team. -- This includes deleted/deactivated users. -- -- <https://api.slack.com/methods/users.lookupByEmail>--userLookupByEmail-  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)-  => User.Email-  -> m (Response User.UserRsp)+userLookupByEmail ::+  (MonadReader env m, HasManager env, HasToken env, MonadIO m) =>+  User.Email ->+  m (Response User.UserRsp) userLookupByEmail = liftNonClassy . flip NonClassy.userLookupByEmail - -- | Returns a function to get a username from a 'Common.UserId'. -- Comes in handy to use 'Web.Slack.MessageParser.messageToHtml'-getUserDesc-  :: (Common.UserId -> Text)-  -- ^ A function to give a default username in case the username is unknown-  -> User.ListRsp-  -- ^ List of users as known by the slack server. See 'usersList'.-  -> (Common.UserId -> Text)-  -- ^ A function from 'Common.UserId' to username.+getUserDesc ::+  -- | A function to give a default username in case the username is unknown+  (Common.UserId -> Text) ->+  -- | List of users as known by the slack server. See 'usersList'.+  User.ListRsp ->+  -- | A function from 'Common.UserId' to username.+  (Common.UserId -> Text) getUserDesc unknownUserFn users =   let userMap = Map.fromList $ (User.userId &&& User.userName) <$> User.listRspMembers users-  in-    \userId -> fromMaybe (unknownUserFn userId) $ Map.lookup userId userMap-+   in \userId -> fromMaybe (unknownUserFn userId) $ Map.lookup userId userMap  -- | Returns an action to send a request to get the history of a conversation. -- --   To fetch all messages in the conversation, run the returned 'LoadPage' action --   repeatedly until it returns an empty list.-conversationsHistoryAll-  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)-  =>  Conversation.HistoryReq-  -- ^ The first request to send. _NOTE_: 'Conversation.historyReqCursor' is silently ignored.-  -> m (LoadPage m Common.Message)-  -- ^ An action which returns a new page of messages every time called.+conversationsHistoryAll ::+  (MonadReader env m, HasManager env, HasToken env, MonadIO m) =>+  -- | The first request to send. _NOTE_: 'Conversation.historyReqCursor' is silently ignored.+  Conversation.HistoryReq ->+  -- | An action which returns a new page of messages every time called.   --   If there are no pages anymore, it returns an empty list.-conversationsHistoryAll = conversationsHistoryAllBy conversationsHistory-+  m (LoadPage m Common.Message)+conversationsHistoryAll = fetchAllBy conversationsHistory  -- | Returns an action to send a request to get the replies of a conversation. --@@ -225,26 +202,26 @@ --           of the thread. So every page returned by the 'LoadPage' action includes --           the first message of the thread. You should drop it if you want to --           collect messages in a thread without duplicates.-repliesFetchAll-  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)-  =>  Conversation.RepliesReq-  -- ^ The first request to send. _NOTE_: 'Conversation.repliesReqCursor' is silently ignored.-  -> m (LoadPage m Common.Message)-  -- ^ An action which returns a new page of messages every time called.+repliesFetchAll ::+  (MonadReader env m, HasManager env, HasToken env, MonadIO m) =>+  -- | The first request to send. _NOTE_: 'Conversation.repliesReqCursor' is silently ignored.+  Conversation.RepliesReq ->+  -- | An action which returns a new page of messages every time called.   --   If there are no pages anymore, it returns an empty list.-repliesFetchAll = repliesFetchAllBy conversationsReplies-+  m (LoadPage m Common.Message)+repliesFetchAll = fetchAllBy conversationsReplies -liftNonClassy-  :: (MonadReader env m, HasManager env, HasToken env, MonadIO m)-  => (SlackConfig -> IO a) -> m a+liftNonClassy ::+  (MonadReader env m, HasManager env, HasToken env, MonadIO m) =>+  (SlackConfig -> IO a) ->+  m a liftNonClassy f =   liftToReader $ \env -> f $ SlackConfig (getManager env) (getToken env) --liftToReader-  :: (MonadReader env m, MonadIO m)-  => (env -> IO a) -> m a+liftToReader ::+  (MonadReader env m, MonadIO m) =>+  (env -> IO a) ->+  m a liftToReader f = do   env <- ask   liftIO $ f env
src/Web/Slack/Common.hs view
@@ -1,66 +1,65 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingStrategies #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE StrictData #-}-{-# LANGUAGE OverloadedLists #-}-{-# LANGUAGE CPP #-}+{-# LANGUAGE TemplateHaskell #-}  ----------------------------------------------------------------------++----------------------------------------------------------------------+ -- | -- Module: Web.Slack.Common -- Description:--------------------------------------------------------------------------------- module Web.Slack.Common-  ( Color(..)-  , UserId(..)-  , ConversationId (..)-  , TeamId (..)-  , Cursor (..)-  , SlackTimestamp(..)-  , mkSlackTimestamp-  , timestampFromText-  , Message(..)-  , MessageType(..)-  , SlackClientError(..)-  , SlackMessageText(..)+  ( Color (..),+    UserId (..),+    ConversationId (..),+    TeamId (..),+    Cursor (..),+    SlackTimestamp (..),+    mkSlackTimestamp,+    timestampFromText,+    Message (..),+    MessageType (..),+    SlackClientError (..),+    SlackMessageText (..),   )-  where+where +-- FIXME: Web.Slack.Prelude+ -- aeson-import Data.Aeson-import Data.Aeson.TH  -- base-import Control.Exception-import GHC.Generics (Generic)  -- deepseq import Control.DeepSeq (NFData)--+import Control.Exception+import Data.Aeson+import Data.Aeson.TH -- servant-client-import Servant.Client  -- slack-web-import Web.Slack.Types-import Web.Slack.Util  -- text import Data.Text (Text)+import GHC.Generics (Generic)+import Servant.Client+import Web.Slack.Types+import Web.Slack.Util+import Prelude  #if !MIN_VERSION_servant(0,16,0) type ClientError = ServantError #endif  data MessageType = MessageTypeMessage-  deriving (Eq, Show, Generic)+  deriving stock (Eq, Show, Generic)  instance NFData MessageType @@ -71,16 +70,16 @@ instance ToJSON MessageType where   toJSON _ = String "message" -data Message =-  Message-    { messageType :: MessageType-    , messageUser :: Maybe UserId -- ^ not present for bot messages at least-    , messageText :: SlackMessageText-    -- ^ the message text is in a markdown-like slack-specific format.-    -- Use 'Web.Slack.MessageParser.messageToHtml' to convert it to HTML.-    , messageTs :: SlackTimestamp-    }-  deriving (Eq, Generic, Show)+data Message = Message+  { messageType :: MessageType+  , messageUser :: Maybe UserId+  -- ^ not present for bot messages at least+  , messageText :: SlackMessageText+  -- ^ the message text is in a markdown-like slack-specific format.+  -- Use 'Web.Slack.MessageParser.messageToHtml' to convert it to HTML.+  , messageTs :: SlackTimestamp+  }+  deriving stock (Eq, Generic, Show)  instance NFData Message @@ -89,11 +88,11 @@ -- | -- Errors that can be triggered by a slack request. data SlackClientError-    = ServantError ClientError-    -- ^ errors from the network connection-    | SlackError Text-    -- ^ errors returned by the slack API-  deriving (Eq, Generic, Show)+  = -- | errors from the network connection+    ServantError ClientError+  | -- | errors returned by the slack API+    SlackError Text+  deriving stock (Eq, Generic, Show)  instance NFData SlackClientError 
src/Web/Slack/Conversation.hs view
@@ -2,127 +2,99 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE RecordWildCards #-}+-- FIXME: squashes warnings+{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}  ----------------------------------------------------------------------++----------------------------------------------------------------------+ -- | -- Module: Web.Slack.Channel -- Description: Types and functions related to <https://api.slack.com/docs/conversations-api Conversation API>--------------------------------------------------------------------------------- module Web.Slack.Conversation-  ( Conversation(..)-  , ConversationId(..)-  , ConversationType(..)-  , ChannelConversation(..)-  , GroupConversation(..)-  , ImConversation(..)-  , TeamId(..)-  , Purpose(..)-  , Topic(..)-  , ListReq(..)-  , mkListReq-  , ListRsp(..)-  , HistoryReq (..)-  , mkHistoryReq-  , HistoryRsp (..)-  , RepliesReq (..)-  , mkRepliesReq-  , ResponseMetadata (..)-  ) where+  ( Conversation (..),+    ConversationId (..),+    ConversationType (..),+    ChannelConversation (..),+    GroupConversation (..),+    ImConversation (..),+    TeamId (..),+    Purpose (..),+    Topic (..),+    ListReq (..),+    mkListReq,+    ListRsp (..),+    HistoryReq (..),+    mkHistoryReq,+    HistoryRsp (..),+    RepliesReq (..),+    mkRepliesReq,+    ResponseMetadata (..),+  )+where --- aeson import Data.Aeson import Data.Aeson.Encoding+import Data.Aeson.KeyMap qualified as KM import Data.Aeson.TH import Data.Aeson.Types---- unordered-containers-import qualified Data.HashMap.Strict as HM---- base-import Control.Applicative (empty, (<|>))-import GHC.Generics (Generic)---- deepseq-import Control.DeepSeq (NFData)---- http-api-data+import Data.Scientific+import Data.Text qualified as T import Web.FormUrlEncoded import Web.HttpApiData---- slack-web import Web.Slack.Common+import Web.Slack.Pager.Types (PagedRequest (..), PagedResponse (..), ResponseMetadata (..))+import Web.Slack.Prelude import Web.Slack.Util --- scientific-import Data.Scientific---- text-import Data.Text (Text)-import qualified Data.Text as T----- |-------data Topic =-  Topic-    { topicValue :: Text-    , topicCreator :: Text-    , topicLastSet :: Integer-    }-  deriving (Eq, Show, Generic)+data Topic = Topic+  { topicValue :: Text+  , topicCreator :: Text+  , topicLastSet :: Integer+  }+  deriving stock (Eq, Show, Generic)  instance NFData Topic  $(deriveJSON (jsonOpts "topic") ''Topic) ---- |-------data Purpose =-  Purpose-    { purposeValue :: Text-    , purposeCreator :: Text-    , purposeLastSet :: Integer-    }-  deriving (Eq, Show, Generic)+data Purpose = Purpose+  { purposeValue :: Text+  , purposeCreator :: Text+  , purposeLastSet :: Integer+  }+  deriving stock (Eq, Show, Generic)  instance NFData Purpose  $(deriveJSON (jsonOpts "purpose") ''Purpose) - -- | Conversation object representing a public channel, --   which any people in the team can join in and see.-data ChannelConversation =-  ChannelConversation-    { channelId :: ConversationId-    , channelName :: Text-    , channelCreated :: Integer-    , channelIsArchived :: Bool-    , channelIsGeneral :: Bool-    , channelUnlinked :: Integer-    , channelNameNormalized :: Text-    , channelIsShared :: Bool--    -- FIXME:+data ChannelConversation = ChannelConversation+  { channelId :: ConversationId+  , channelName :: Text+  , channelCreated :: Integer+  , channelIsArchived :: Bool+  , channelIsGeneral :: Bool+  , channelUnlinked :: Integer+  , channelNameNormalized :: Text+  , channelIsShared :: Bool+  , -- FIXME:     -- I'm not sure the correct type of this field, because I only found     -- example responses whose @parent_conversation@ is @null@     -- , channelParentConversation: null-    , channelCreator :: UserId-    , channelIsExtShared :: Bool-    , channelIsOrgShared :: Bool-    , channelSharedTeamIds :: [TeamId]--    -- FIXME:+    channelCreator :: UserId+  , channelIsExtShared :: Bool+  , channelIsOrgShared :: Bool+  , channelSharedTeamIds :: Maybe [TeamId]+  -- ^ Ironically this has been observed to be absent on real shared-channel+  -- responses.+  , -- FIXME:     -- I'm not sure the correct type of these fields, because I only found     -- example responses whose @pending_connected_team_ids@ and     -- @pending_shared@ are empty arrays. (Perhaps this is because@@ -131,45 +103,43 @@     -- , channelPendingShared :: [TeamId]     -- , channelPendingConnectedTeamIds :: [TeamId] -    , channelIsPendingExtShared :: Bool-    , channelIsMember :: Bool-    , channelTopic :: Topic-    , channelPurpose :: Purpose-    , channelPreviousNames :: [Text]-    , channelNumMembers :: Integer-    }-  deriving (Eq, Show, Generic)+    channelIsPendingExtShared :: Bool+  , channelIsMember :: Maybe Bool+  -- ^ Absent from @users.conversations@ response+  , channelTopic :: Topic+  , channelPurpose :: Purpose+  , channelPreviousNames :: [Text]+  , channelNumMembers :: Maybe Integer+  -- ^ Absent from @conversations.join@ response+  }+  deriving stock (Eq, Show, Generic)  instance NFData ChannelConversation  $(deriveJSON (jsonOpts "channel") ''ChannelConversation) - -- | Conversation object representing a private channel or --   _a multi-party instant message (mpim)*, which only invited people in the --  team can join in and see.-data GroupConversation =-  GroupConversation-    { groupId :: ConversationId-    , groupName :: Text-    , groupCreated :: Integer-    , groupIsArchived :: Bool-    , groupIsGeneral :: Bool-    , groupUnlinked :: Integer-    , groupNameNormalized :: Text-    , groupIsShared :: Bool--    -- FIXME:+data GroupConversation = GroupConversation+  { groupId :: ConversationId+  , groupName :: Text+  , groupCreated :: Integer+  , groupIsArchived :: Bool+  , groupIsGeneral :: Bool+  , groupUnlinked :: Integer+  , groupNameNormalized :: Text+  , groupIsShared :: Bool+  , -- FIXME:     -- I'm not sure the correct type of this field, because I only found     -- example responses whose @parent_conversation@ is @null@     -- , groupParentConversation :: null -    , groupCreator :: UserId-    , groupIsExtShared :: Bool-    , groupIsOrgShared :: Bool-    , groupSharedTeamIds :: [TeamId]--    -- FIXME:+    groupCreator :: UserId+  , groupIsExtShared :: Bool+  , groupIsOrgShared :: Bool+  , groupSharedTeamIds :: [TeamId]+  , -- FIXME:     -- I'm not sure the correct type of these fields, because I only found     -- example responses whose @pending_connected_team_ids@ and     -- @pending_shared@ are empty arrays. (Perhaps this is because@@ -178,97 +148,101 @@     -- , group_pending_shared :: []     -- , group_pending_connected_team_ids :: [] -    , groupIsPendingExtShared :: Bool-    , groupIsMember :: Bool-    , groupIsPrivate :: Bool-    , groupIsMpim :: Bool-    , groupLastRead :: SlackTimestamp-    , groupIsOpen :: Bool-    , groupTopic :: Topic-    , groupPurpose :: Purpose-    , groupPriority :: Scientific-    }-  deriving (Eq, Show, Generic)+    groupIsPendingExtShared :: Bool+  , groupIsMember :: Bool+  , groupIsPrivate :: Bool+  , groupIsMpim :: Bool+  , groupLastRead :: SlackTimestamp+  , groupIsOpen :: Bool+  , groupTopic :: Topic+  , groupPurpose :: Purpose+  , groupPriority :: Scientific+  }+  deriving stock (Eq, Show, Generic)  instance NFData GroupConversation  $(deriveJSON (jsonOpts "group") ''GroupConversation) - -- | Conversation object representing a (single-party) instance message, --   where only two people talk.-data ImConversation =-  ImConversation-    { imId :: ConversationId-    , imCreated :: Integer-    , imIsArchived :: Bool-    , imIsOrgShared :: Bool-    , imUser :: UserId-    , imIsUserDeleted :: Bool-    , imPriority :: Scientific-    }-  deriving (Eq, Show, Generic)+data ImConversation = ImConversation+  { imId :: ConversationId+  , imCreated :: Integer+  , imIsArchived :: Bool+  , imIsOrgShared :: Bool+  , imUser :: UserId+  , imIsUserDeleted :: Bool+  , imPriority :: Scientific+  }+  deriving stock (Eq, Show, Generic)  instance NFData ImConversation  $(deriveJSON (jsonOpts "im") ''ImConversation) - -- | Ref. https://api.slack.com/types/conversation-------data Conversation =-      Channel ChannelConversation-    | Group GroupConversation-    | Im ImConversation-  deriving (Eq, Show, Generic)+data Conversation+  = Channel ChannelConversation+  | Group GroupConversation+  | Im ImConversation+  deriving stock (Eq, Show, Generic)  instance NFData Conversation - instance FromJSON Conversation where   parseJSON = withObject "Conversation" $ \o ->-    parseWhen "is_channel" Channel o-      <|> parseWhen "is_group" Group o-      <|> parseWhen "is_im" Im o-      <|> prependFailure-            "parsing a Conversation failed: neither channel, group, nor im, "-            (typeMismatch "Conversation" (Object o))-   where-    parseWhen key con o = do-      is <- (o .: key) <|> empty-      if is-        then con <$> parseJSON (Object o)-        else empty+    fromMaybe (noneMatched o)+      =<< parseWhen "is_channel" Channel o+      `parseOr` parseWhen "is_group" Group o+      `parseOr` parseWhen "is_im" Im o+    where+      noneMatched o =+        prependFailure+          "parsing a Conversation failed: neither channel, group, nor im: "+          (typeMismatch "Conversation" (Object o)) +      -- '(<|>)' that pierces one layer of 'Monad' first+      parseOr :: (Monad m, Alternative a) => m (a b) -> m (a b) -> m (a b)+      parseOr = liftM2 (<|>) +      -- This uses the outer Parser monad since deciding which parser to use+      -- is monadic, then the Maybe to decide which parser is picked, then+      -- finally the inner parser to actually run it+      parseWhen :: FromJSON a => Key -> (a -> b) -> Object -> Parser (Maybe (Parser b))+      parseWhen key con o = do+        -- Slack only inconsistently includes the is_* attributes if false.+        is <- o .:? key .!= False+        if is+          then pure . Just $ con <$> parseJSON (Object o)+          else pure $ Nothing+ instance ToJSON Conversation where   toJSON (Channel channel) =     let (Object obj) = toJSON channel      in Object-          . HM.insert "is_channel" (Bool True)-          . HM.insert "is_group" (Bool False)-          $ HM.insert "is_im" (Bool False) obj-  toJSON (Group group) =-    let (Object obj) = toJSON group+          . KM.insert "is_channel" (Bool True)+          . KM.insert "is_group" (Bool False)+          $ KM.insert "is_im" (Bool False) obj+  toJSON (Group theGroup) =+    let (Object obj) = toJSON theGroup      in Object-          . HM.insert "is_channel" (Bool False)-          . HM.insert "is_group" (Bool True)-          $ HM.insert "is_im" (Bool False) obj+          . KM.insert "is_channel" (Bool False)+          . KM.insert "is_group" (Bool True)+          $ KM.insert "is_im" (Bool False) obj   toJSON (Im im) =     let (Object obj) = toJSON im      in Object-          . HM.insert "is_channel" (Bool False)-          . HM.insert "is_group" (Bool False)-          $ HM.insert "is_im" (Bool True) obj-+          . KM.insert "is_channel" (Bool False)+          . KM.insert "is_group" (Bool False)+          $ KM.insert "is_im" (Bool True) obj -data ConversationType =-    PublicChannelType+data ConversationType+  = PublicChannelType   | PrivateChannelType   | MpimType   | ImType-  deriving (Eq, Show, Generic)+  deriving stock (Eq, Show, Generic)  instance NFData ConversationType @@ -290,99 +264,91 @@     "im" -> pure ImType     actual ->       prependFailure "must be either \"public_channel\", \"private_channel\", \"mpim\" or \"im\"!"-        . typeMismatch "ConversationType" $ String actual--+        . typeMismatch "ConversationType"+        $ String actual -data ListReq =-  ListReq-    { listReqExcludeArchived :: Maybe Bool-    , listReqTypes :: [ConversationType]-    }-  deriving (Eq, Show, Generic)+data ListReq = ListReq+  { listReqExcludeArchived :: Maybe Bool+  , listReqTypes :: [ConversationType]+  , listReqCursor :: Maybe Cursor+  , listReqLimit :: Maybe Int+  , listReqTeamId :: Maybe TeamId+  }+  deriving stock (Eq, Show, Generic)  instance NFData ListReq ---- |------- $(deriveJSON (jsonOpts "listReq") ''ListReq) --- |--------mkListReq-  :: ListReq+mkListReq ::+  ListReq mkListReq =   ListReq     { listReqExcludeArchived = Nothing     , listReqTypes = []+    , listReqLimit = Nothing+    , listReqTeamId = Nothing+    , listReqCursor = Nothing     } ---- |------- instance ToForm ListReq where-  toForm (ListReq archived types) =-    archivedForm <> typesForm-   where-    archivedForm =-      maybe mempty (\val -> [("archived", toUrlPiece val)]) archived-    typesForm =-      if null types-        then mempty-        else [("types", T.intercalate "," $ map toUrlPiece types)]----- |---+  toForm+    ( ListReq+        { listReqExcludeArchived+        , listReqTypes = types+        , listReqTeamId+        , listReqCursor+        , listReqLimit+        }+      ) =+      archivedForm+        <> typesForm+        <> toQueryParamIfJust "team_id" listReqTeamId+        <> toQueryParamIfJust "cursor" listReqCursor+        <> toQueryParamIfJust "limit" listReqLimit+      where+        archivedForm =+          maybe mempty (\val -> [("exclude_archived", toUrlPiece val)]) listReqExcludeArchived+        typesForm =+          if null types+            then mempty+            else [("types", T.intercalate "," $ map toUrlPiece types)] -newtype ListRsp =-  ListRsp-    { listRspChannels :: [Conversation]-    }-  deriving (Eq, Show, Generic)+data ListRsp = ListRsp+  { listRspChannels :: [Conversation]+  , listRspResponseMetadata :: Maybe ResponseMetadata+  }+  deriving stock (Eq, Show, Generic)  instance NFData ListRsp  $(deriveFromJSON (jsonOpts "listRsp") ''ListRsp) --- |------+instance PagedRequest ListReq where+  setCursor c r = r {listReqCursor = c} -data HistoryReq =-  HistoryReq-    { historyReqChannel :: ConversationId-    , historyReqCursor :: Maybe Cursor-    , historyReqCount :: Int-    , historyReqLatest :: Maybe SlackTimestamp-    , historyReqOldest :: Maybe SlackTimestamp-    , historyReqInclusive :: Bool-    }-  deriving (Eq, Show, Generic)+instance PagedResponse ListRsp where+  type ResponseObject ListRsp = Conversation+  getResponseData ListRsp {listRspChannels} = listRspChannels+  getResponseMetadata ListRsp {listRspResponseMetadata} = listRspResponseMetadata -instance NFData HistoryReq+data HistoryReq = HistoryReq+  { historyReqChannel :: ConversationId+  , historyReqCursor :: Maybe Cursor+  , historyReqCount :: Int+  , historyReqLatest :: Maybe SlackTimestamp+  , historyReqOldest :: Maybe SlackTimestamp+  , historyReqInclusive :: Bool+  }+  deriving stock (Eq, Show, Generic) --- |------+instance NFData HistoryReq  $(deriveJSON (jsonOpts "historyReq") ''HistoryReq) ---- |--------mkHistoryReq-  :: ConversationId-  -> HistoryReq+mkHistoryReq ::+  ConversationId ->+  HistoryReq mkHistoryReq channel =   HistoryReq     { historyReqChannel = channel@@ -393,13 +359,9 @@     , historyReqInclusive = True     } --- |------- instance ToForm HistoryReq where   -- can't use genericToForm because slack expects booleans as 0/1-  toForm HistoryReq{..} =+  toForm HistoryReq {..} =     [("channel", toQueryParam historyReqChannel)]       <> toQueryParamIfJust "cursor" historyReqCursor       <> [("count", toQueryParam historyReqCount)]@@ -407,46 +369,35 @@       <> toQueryParamIfJust "oldest" historyReqOldest       <> [("inclusive", toQueryParam (if historyReqInclusive then 1 :: Int else 0))] ---- |-------newtype ResponseMetadata = ResponseMetadata { responseMetadataNextCursor :: Maybe Cursor }-  deriving (Eq, Show, Generic)--instance NFData ResponseMetadata--$(deriveJSON (jsonOpts "responseMetadata") ''ResponseMetadata)----- |--------data HistoryRsp =-  HistoryRsp-    { historyRspMessages :: [Message]-    , historyRspResponseMetadata :: Maybe ResponseMetadata-    }-  deriving (Eq, Show, Generic)+data HistoryRsp = HistoryRsp+  { historyRspMessages :: [Message]+  , historyRspResponseMetadata :: Maybe ResponseMetadata+  }+  deriving stock (Eq, Show, Generic)  instance NFData HistoryRsp  $(deriveJSON (jsonOpts "historyRsp") ''HistoryRsp) +instance PagedRequest HistoryReq where+  setCursor c r = r {historyReqCursor = c} -data RepliesReq =-  RepliesReq-    { repliesReqTs :: SlackTimestamp-    , repliesReqCursor :: Maybe Cursor-    , repliesReqChannel :: ConversationId-    , repliesReqLimit :: Int-    , repliesReqLatest :: Maybe SlackTimestamp-    , repliesReqOldest :: Maybe SlackTimestamp-    , repliesReqInclusive :: Bool-    }-  deriving (Eq, Show, Generic)+instance PagedResponse HistoryRsp where+  type ResponseObject HistoryRsp = Message+  getResponseMetadata HistoryRsp {historyRspResponseMetadata} = historyRspResponseMetadata+  getResponseData HistoryRsp {historyRspMessages} = historyRspMessages +data RepliesReq = RepliesReq+  { repliesReqTs :: SlackTimestamp+  , repliesReqCursor :: Maybe Cursor+  , repliesReqChannel :: ConversationId+  , repliesReqLimit :: Int+  , repliesReqLatest :: Maybe SlackTimestamp+  , repliesReqOldest :: Maybe SlackTimestamp+  , repliesReqInclusive :: Bool+  }+  deriving stock (Eq, Show, Generic)+ instance NFData RepliesReq  $(deriveJSON (jsonOpts "repliesReq") ''RepliesReq)@@ -462,15 +413,13 @@       <> toQueryParamIfJust "oldest" repliesReqOldest       <> [("inclusive", toQueryParam (if repliesReqInclusive then 1 :: Int else 0))] ---- |------+instance PagedRequest RepliesReq where+  setCursor c r = r {repliesReqCursor = c} -mkRepliesReq-  :: ConversationId-  -> SlackTimestamp-  -> RepliesReq+mkRepliesReq ::+  ConversationId ->+  SlackTimestamp ->+  RepliesReq mkRepliesReq channel ts =   RepliesReq     { repliesReqChannel = channel
+ src/Web/Slack/Experimental/Blocks.hs view
@@ -0,0 +1,204 @@+module Web.Slack.Experimental.Blocks+  ( -- * General Slack Messages+    SlackText,+    (<+>),+    parens,+    brackets,+    angleBrackets,+    ticks,+    codeBlock,+    bold,+    italic,+    newline,+    unorderedList,+    link,+    monospaced,+    mentionUser,+    isSubStringOf,+    SlackImage (..),+    SlackMessage,+    Markdown (..),+    Image (..),+    context,+    textToMessage,+    prefixFirstSlackMessage,+    mentionUserGroupById,+    textToContext,+    slackMessage,+    SlackBlock (..),++    -- ** Blocks' rich text formatting (receive only!)+    RichItem (..),+    RichStyle (..),+    RichLinkAttrs (..),+    RichTextSectionItem (..),+    RichText (..),++    -- * Rendered messages+    RenderedSlackMessage (..),+    render,++    -- * Introduction to Slack Interactive Messages+    -- $interactive++    -- * Creating Slack Interactive Messages+    actions,+    actionsWithBlockId,+    SlackActionId (..),+    SlackBlockId,+    setting,+    emptySetting,+    SlackStyle (..),+    plaintext,+    plaintextonly,+    mrkdwn,+    button,+    buttonSettings,+    ButtonSettings+      ( buttonUrl,+        buttonValue,+        buttonStyle,+        buttonConfirm+      ),+    confirm,+    confirmAreYouSure,+    ConfirmSettings+      ( confirmTitle,+        confirmText,+        confirmConfirm,+        confirmDeny,+        confirmStyle+      ),++    -- * Responding to Slack Interactive Messages+    SlackInteractiveResponse (..),+  )+where++import Data.Aeson.Text (encodeToLazyText)+import Data.Text qualified as T+import Web.Slack.Experimental.Blocks.Types+import Web.Slack.Prelude+import Web.Slack.Types++(<+>) :: SlackText -> SlackText -> SlackText+x <+> y = x <> " " <> y++parens :: SlackText -> SlackText+parens x = "(" <> x <> ")"++brackets :: SlackText -> SlackText+brackets x = "[" <> x <> "]"++angleBrackets :: SlackText -> SlackText+angleBrackets x = "<" <> x <> ">"++ticks :: SlackText -> SlackText+ticks x = "`" <> x <> "`"++-- | Render a 'Slack' renderable value with ticks around it. Alias for @ticks . message@+monospaced :: Slack a => a -> SlackText+monospaced = ticks . message++codeBlock :: SlackText -> SlackText+codeBlock x = "```\n" <> x <> "\n```"++bold :: SlackText -> SlackText+bold x = "*" <> x <> "*"++italic :: SlackText -> SlackText+italic x = "_" <> x <> "_"++newline :: SlackText -> SlackText+newline x = "\n" <> x++-- | Render an unordered (bulleted) list+unorderedList :: [SlackText] -> SlackText+unorderedList = mconcat . fmap (\t -> newline (message @Text "- " <> t))++-- | https://api.slack.com/reference/surfaces/formatting#mentioning-users+mentionUser :: UserId -> SlackText+mentionUser slackUserId = message $ "<@" <> unUserId slackUserId <> ">"++-- | https://api.slack.com/reference/surfaces/formatting#mentioning-groups+mentionUserGroupById :: SlackText -> SlackText+mentionUserGroupById userGroupId = angleBrackets $ "!subteam^" <> userGroupId++isSubStringOf :: Text -> SlackText -> Bool+needle `isSubStringOf` (SlackText haystack) = needle `isInfixOf` concat haystack++-- | RenderedSlackMessage contains the original SlackMessage, the rendered version, and a+--   boolean indicating whether truncation was done.+--+--   Usage:+--+--   @+--      let+--        msg =+--        (mkPostMsgReq channel "")+--          { postMsgReqBlocks = Just blocks+--          , postMsgReqThreadTs = mThreadTs+--          }+--    chatPostMessage msg+--   @+data RenderedSlackMessage = RenderedSlackMessage+  { _originalMessage :: SlackMessage+  , _renderedMessage :: Text+  , _truncated :: Bool+  }++render :: SlackMessage -> RenderedSlackMessage+render sm =+  let (truncatedSm, isTruncated) = truncateSlackMessage sm+   in RenderedSlackMessage sm (cs . encodeToLazyText . toJSON $ truncatedSm) isTruncated++-- | Slack will fail if any text block is over 3000 chars.+--   truncateSlackMessage will truncate all text blocks and also return an bool+--   indicating whether any truncation was done.+truncateSlackMessage :: SlackMessage -> (SlackMessage, Bool)+truncateSlackMessage (SlackMessage blocks) =+  let (truncatedBlocks, isTruncateds) = unzip $ map truncateSlackBlock blocks+   in (SlackMessage truncatedBlocks, or isTruncateds)++truncateSlackBlock :: SlackBlock -> (SlackBlock, Bool)+truncateSlackBlock sb@(SlackBlockSection (SlackText texts) mAccessory) =+  let messageLength = sum $ map T.length texts+      lengthLimit = 3000+      truncationMessage = "\n...Rest of message truncated for slack\n"+      truncationMessageLength = T.length truncationMessage+      truncateTexts ts = take (lengthLimit - truncationMessageLength) (concat ts)+   in if messageLength > lengthLimit+        then (SlackBlockSection (SlackText [truncateTexts texts <> "\n...Rest of message truncated for slack\n"]) mAccessory, True)+        else (sb, False)+-- possible to also truncate SlackContexts, but we never put long strings in there.+truncateSlackBlock x = (x, False)++prefixFirstSlackBlockSection :: Text -> [SlackBlock] -> ([SlackBlock], Bool)+prefixFirstSlackBlockSection prefix (SlackBlockSection text mAccessory : sbs) = (SlackBlockSection (message prefix <> text) mAccessory : sbs, True)+prefixFirstSlackBlockSection prefix (sb : sbs) = let (prefixedSbs, match) = prefixFirstSlackBlockSection prefix sbs in (sb : prefixedSbs, match)+prefixFirstSlackBlockSection _ [] = ([], False)++prefixFirstSlackMessage :: Text -> [SlackMessage] -> [SlackMessage]+prefixFirstSlackMessage prefix (sm : sms) =+  let SlackMessage slackBlocks = sm+      (prefixedSlackBlocks, match) = prefixFirstSlackBlockSection prefix slackBlocks+   in if match+        then SlackMessage prefixedSlackBlocks : sms+        else sm : prefixFirstSlackMessage prefix sms+prefixFirstSlackMessage _ [] = []++-- | Concatenate a list of 'SlackText' into a single block, and wrap it up as a full message+slackMessage :: [SlackText] -> SlackMessage+slackMessage = SlackMessage . pure . (`SlackBlockSection` Nothing) . mconcat++-- $interactive+--+-- = Slack Interactive Messages+--+-- First, familiarize yourself with [Slack's Interactivity documentation](https://api.slack.com/interactivity).+-- Currently we only support [Block Kit interactive components](https://api.slack.com/interactivity/components),+-- i.e. components like buttons attached to messages.+--+-- To make an Slack message interactive, you need to include an \"Actions\" block that has one or more interactive components.+-- These should generally only be created using the builder functions such as 'actions' and 'button'. Consumers of this module+-- should avoid directly importing "Web.Slack.Experimental.Blocks.Types".
+ src/Web/Slack/Experimental/Blocks/Types.hs view
@@ -0,0 +1,756 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeOperators #-}++module Web.Slack.Experimental.Blocks.Types where++import Control.Monad (MonadFail (..))+import Data.Aeson (Object, Value (..), withArray)+import Data.Aeson.Types ((.!=))+import Data.StringVariants+import Data.Vector qualified as V+import Refined+import Refined.Unsafe (reallyUnsafeRefine)+import Web.Slack.AesonUtils+import Web.Slack.Common (ConversationId, UserId)+import Web.Slack.Prelude++-- | Class of types that can be turned into part of a Slack Message. 'message'+-- is the primary way of converting primitive and domain-level types into things+-- that can be shown in a slack message.+class Slack a where+  message :: a -> SlackText++newtype SlackText = SlackText {unSlackTexts :: [Text]}+  deriving newtype (Semigroup, Monoid, Eq)++-- | Render a link with optional link text+link :: Text -> Maybe Text -> SlackText+link uri = \case+  Nothing -> message $ "<" <> uri <> ">"+  Just linkText -> message $ "<" <> uri <> "|" <> linkText <> ">"++data SlackPlainTextOnly = SlackPlainTextOnly SlackText+  deriving stock (Eq, Show)++instance IsString SlackPlainTextOnly where+  fromString = SlackPlainTextOnly . message++instance ToJSON SlackPlainTextOnly where+  toJSON (SlackPlainTextOnly (SlackText arr)) =+    object+      [ "type" .= ("plain_text" :: Text)+      , "text" .= intercalate "\n" arr+      ]++instance FromJSON SlackPlainTextOnly where+  parseJSON = withObject "SlackPlainTextOnly" $ \obj -> do+    text <- obj .: "text"+    pure . SlackPlainTextOnly . SlackText $ lines text++-- | Create a 'SlackPlainTextOnly'. Some API points can can take either markdown or plain text,+-- but some can take only plain text. This enforces the latter.+plaintextonly :: Slack a => a -> SlackPlainTextOnly+plaintextonly a = SlackPlainTextOnly $ message a++data SlackTextObject+  = SlackPlainText SlackText+  | SlackMarkdownText SlackText+  deriving stock (Eq, Show)++instance ToJSON SlackTextObject where+  toJSON (SlackPlainText (SlackText arr)) =+    object+      [ "type" .= ("plain_text" :: Text)+      , "text" .= intercalate "\n" arr+      ]+  toJSON (SlackMarkdownText (SlackText arr)) =+    object+      [ "type" .= ("mrkdwn" :: Text)+      , "text" .= intercalate "\n" arr+      ]++-- | Create a plain text 'SlackTextObject' where the API allows either markdown or plain text.+plaintext :: Slack a => a -> SlackTextObject+plaintext = SlackPlainText . message++-- | Create a markdown 'SlackTextObject' where the API allows either markdown or plain text.+mrkdwn :: Slack a => a -> SlackTextObject+mrkdwn = SlackMarkdownText . message++instance FromJSON SlackTextObject where+  parseJSON = withObject "SlackTextObject" $ \obj -> do+    (slackTextType :: Text) <- obj .: "type"+    case slackTextType of+      "text" -> do+        text <- obj .: "text"+        pure . SlackPlainText . SlackText $ lines text+      "mrkdwn" -> do+        text <- obj .: "text"+        pure . SlackMarkdownText . SlackText $ lines text+      _ -> fail "Unknown SlackTextObject type, must be one of ['text', 'mrkdwn']"++instance Show SlackText where+  show (SlackText arr) = show $ concat arr++instance ToJSON SlackText where+  toJSON (SlackText arr) = toJSON $ concat arr++instance IsString SlackText where+  fromString = message++instance Slack Text where+  message text = SlackText [text]++instance Slack String where+  message = message @Text . pack++instance Slack Int where+  message = message . show++-- | Represents an optional setting for some Slack Setting.+newtype OptionalSetting a = OptionalSetting {unOptionalSetting :: Maybe a}+  deriving newtype (Eq)++-- | Allows using bare Strings without having to use 'setting'+instance IsString (OptionalSetting String) where+  fromString = OptionalSetting . Just++-- | Allows using bare Texts without having to use 'setting'+instance IsString (OptionalSetting Text) where+  fromString = OptionalSetting . Just . pack++-- | Sets a setting.+setting :: a -> OptionalSetting a+setting = OptionalSetting . Just++-- | Sets the empty setting.+emptySetting :: OptionalSetting a+emptySetting = OptionalSetting Nothing++-- | Styles for Slack [buttons](https://api.slack.com/reference/block-kit/block-elements#button).+-- If no style is given, the default style (black) is used.+data SlackStyle+  = -- | Green button+    SlackStylePrimary+  | -- | Red button+    SlackStyleDanger+  deriving stock (Eq, Show)++$(deriveJSON (jsonDeriveWithAffix "SlackStyle" jsonDeriveOptionsSnakeCase) ''SlackStyle)++-- | Used to identify an action. The ID used should be unique among all actions in the block.+--+--   This is limited to 255 characters, per the Slack documentation at+--   <https://api.slack.com/reference/block-kit/block-elements#button>+newtype SlackActionId = SlackActionId {unSlackActionId :: NonEmptyText 255}+  deriving stock (Show, Eq)+  deriving newtype (FromJSON, ToJSON)++-- FIXME(jadel): SlackActionId might be worth turning into something more type+-- safe: possibly parameterize SlackAction over a sum type parameter++data SlackImage = SlackImage+  { slackImageTitle :: !(Maybe Text)+  , slackImageAltText :: !Text+  -- ^ Optional Title+  , slackImageUrl :: !Text+  }+  deriving stock (Eq)++instance Show SlackImage where+  show (SlackImage _ altText _) = unpack altText++data SlackContent+  = SlackContentText SlackText+  | SlackContentImage SlackImage+  deriving stock (Eq)++instance Show SlackContent where+  show (SlackContentText t) = show t+  show (SlackContentImage i) = show i++instance ToJSON SlackContent where+  toJSON (SlackContentText t) =+    object+      [ "type" .= ("mrkdwn" :: Text)+      , "text" .= t+      ]+  toJSON (SlackContentImage (SlackImage mtitle altText url)) =+    object $+      [ "type" .= ("image" :: Text)+      , "image_url" .= url+      , "alt_text" .= altText+      ]+        <> maybe [] mkTitle mtitle+    where+      mkTitle title =+        [ "title"+            .= object+              [ "type" .= ("plain_text" :: Text)+              , "text" .= title+              ]+        ]++instance FromJSON SlackContent where+  parseJSON = withObject "SlackContent" $ \obj -> do+    (slackContentType :: Text) <- obj .: "type"+    case slackContentType of+      "mrkdwn" -> do+        (slackContentText :: String) <- obj .: "text"+        pure $ SlackContentText $ fromString slackContentText+      "image" -> do+        (slackImageUrl :: Text) <- obj .: "image_url"+        (slackImageAltText :: Text) <- obj .: "alt_text"+        (slackImageTitleObj :: Maybe Object) <- obj .:? "title"+        (slackImageTitleText :: Maybe Text) <- case slackImageTitleObj of+          Just innerObj -> innerObj .: "text"+          Nothing -> pure Nothing+        pure $ SlackContentImage $ SlackImage slackImageTitleText slackImageAltText slackImageUrl+      _ -> fail "Unknown SlackContent type, must be one of ['mrkdwn', 'image']"++newtype SlackContext = SlackContext [SlackContent]+  deriving newtype (Semigroup, Monoid, Eq)++instance Show SlackContext where+  show (SlackContext arr) = show arr++instance ToJSON SlackContext where+  toJSON (SlackContext arr) = toJSON arr++instance FromJSON SlackContext where+  parseJSON = withArray "SlackContext" $ \arr -> do+    (parsedAsArrayOfSlackContents :: V.Vector SlackContent) <- traverse parseJSON arr+    let slackContentList = V.toList parsedAsArrayOfSlackContents+    pure $ SlackContext slackContentList++type SlackActionListConstraints = SizeGreaterThan 0 && SizeLessThan 6++-- | List that enforces that Slack actions must have between 1 and 5 actions.+newtype SlackActionList = SlackActionList {unSlackActionList :: Refined SlackActionListConstraints [SlackAction]}+  deriving stock (Show)+  deriving newtype (Eq, FromJSON, ToJSON)++-- | Helper to allow using up to a 5-tuple for a 'SlackActionList'+class ToSlackActionList a where+  toSlackActionList :: a -> SlackActionList++instance ToSlackActionList SlackActionList where+  toSlackActionList = id++instance ToSlackActionList SlackAction where+  toSlackActionList a = SlackActionList $ reallyUnsafeRefine [a]++instance ToSlackActionList (SlackAction, SlackAction) where+  toSlackActionList (a, b) = SlackActionList $ reallyUnsafeRefine [a, b]++instance ToSlackActionList (SlackAction, SlackAction, SlackAction) where+  toSlackActionList (a, b, c) = SlackActionList $ reallyUnsafeRefine [a, b, c]++instance ToSlackActionList (SlackAction, SlackAction, SlackAction, SlackAction) where+  toSlackActionList (a, b, c, d) = SlackActionList $ reallyUnsafeRefine [a, b, c, d]++instance ToSlackActionList (SlackAction, SlackAction, SlackAction, SlackAction, SlackAction) where+  toSlackActionList (a, b, c, d, e) = SlackActionList $ reallyUnsafeRefine [a, b, c, d, e]++-- | A rich text style. You can't actually send these, for some reason.+data RichStyle = RichStyle+  { rsBold :: Bool+  , rsItalic :: Bool+  }+  deriving stock (Eq, Show)++instance Semigroup RichStyle where+  a <> b = RichStyle {rsBold = rsBold a || rsBold b, rsItalic = rsItalic a || rsItalic b}++instance Monoid RichStyle where+  mempty = RichStyle {rsBold = False, rsItalic = False}++instance FromJSON RichStyle where+  parseJSON = withObject "RichStyle" \obj -> do+    rsBold <- obj .:? "bold" .!= False+    rsItalic <- obj .:? "italic" .!= False+    pure RichStyle {..}++data RichLinkAttrs = RichLinkAttrs+  { style :: RichStyle+  , url :: Text+  , text :: Maybe Text+  -- ^ Probably is empty in the case of links that are just the URL+  }+  deriving stock (Eq, Show)++-- | Seemingly only documented at+--  <https://api.slack.com/changelog/2019-09-what-they-see-is-what-you-get-and-more-and-less>+--+--  They warn of undocumented element types. Joy.+data RichItem+  = RichItemText Text RichStyle+  | RichItemChannel ConversationId+  | RichItemUser UserId RichStyle+  | RichItemLink RichLinkAttrs+  | RichItemEmoji Text+  | RichItemOther Text Value+  -- FIXME(jadel): date, usergroup, team, broadcast+  deriving stock (Eq, Show)++instance FromJSON RichItem where+  parseJSON = withObject "RichItem" \obj -> do+    kind :: Text <- obj .: "type"+    case kind of+      "text" -> do+        style <- obj .:? "style" .!= mempty+        text <- obj .: "text"+        pure $ RichItemText text style+      "channel" -> do+        channelId <- obj .: "chanel_id"+        pure $ RichItemChannel channelId+      "emoji" -> do+        name <- obj .: "name"+        pure $ RichItemEmoji name+      "link" -> do+        url <- obj .: "url"+        text <- obj .:? "text"+        style <- obj .:? "style" .!= mempty+        pure $ RichItemLink RichLinkAttrs {..}+      "user" -> do+        userId <- obj .: "user_id"+        style <- obj .:? "style" .!= mempty+        pure $ RichItemUser userId style+      _ -> pure $ RichItemOther kind (Object obj)++data RichTextSectionItem+  = RichTextSectionItemRichText [RichItem]+  | RichTextSectionItemUnknown Text Value+  deriving stock (Eq, Show)++instance FromJSON RichTextSectionItem where+  parseJSON = withObject "RichTextSectionItem" \obj -> do+    kind <- obj .: "type"+    case kind of+      "rich_text_section" -> do+        elts <- obj .: "elements"+        pure $ RichTextSectionItemRichText elts+      _ -> pure $ RichTextSectionItemUnknown kind (Object obj)++data RichText = RichText+  { blockId :: Maybe SlackBlockId+  , elements :: [RichTextSectionItem]+  }+  deriving stock (Eq, Show)++instance FromJSON RichText where+  parseJSON = withObject "RichText" \obj -> do+    blockId <- obj .:? "block_id"+    elements <- obj .: "elements"+    pure RichText {..}++-- | Accessory is a type of optional block element that floats to the right of text in a BlockSection.+--   <https://api.slack.com/reference/block-kit/blocks#section_fields>+data SlackAccessory+  = SlackButtonAccessory SlackAction -- button+  deriving stock (Eq)++instance ToJSON SlackAccessory where+  toJSON (SlackButtonAccessory btn) = toJSON btn++instance FromJSON SlackAccessory where+  parseJSON v = SlackButtonAccessory <$> parseJSON v++instance Show SlackAccessory where+  show (SlackButtonAccessory btn) = show btn++-- | Small helper function for constructing a section with a button accessory out of a button and text components+sectionWithButtonAccessory :: SlackAction -> SlackText -> SlackBlock+sectionWithButtonAccessory btn txt = SlackBlockSection txt (Just $ SlackButtonAccessory btn)++data SlackBlock+  = SlackBlockSection SlackText (Maybe SlackAccessory)+  | SlackBlockImage SlackImage+  | SlackBlockContext SlackContext+  | SlackBlockDivider+  | SlackBlockRichText RichText+  | SlackBlockActions (Maybe SlackBlockId) SlackActionList -- 1 to 5 elements+  | SlackBlockHeader SlackPlainTextOnly -- max length 150+  deriving stock (Eq)++instance Show SlackBlock where+  show (SlackBlockSection t Nothing) = show t+  show (SlackBlockSection t (Just mAccessory)) =+    show $ mconcat [show t, " [", show mAccessory, "]"]+  show (SlackBlockImage i) = show i+  show (SlackBlockContext contents) = show contents+  show SlackBlockDivider = "|"+  show (SlackBlockActions mBlockId as) =+    show $+      mconcat+        [ "actions("+        , show mBlockId+        , ") = ["+        , show $ intercalate ", " (map show (unrefine $ unSlackActionList as))+        , "]"+        ]+  show (SlackBlockRichText rt) = show rt+  show (SlackBlockHeader p) = show p++instance ToJSON SlackBlock where+  toJSON (SlackBlockSection slackText mSectionAccessory) =+    objectOptional+      [ "type" .=! ("section" :: Text)+      , "text" .=! SlackContentText slackText+      , "accessory" .=? mSectionAccessory+      ]+  toJSON (SlackBlockImage i) = toJSON (SlackContentImage i)+  toJSON (SlackBlockContext contents) =+    object+      [ "type" .= ("context" :: Text)+      , "elements" .= contents+      ]+  toJSON SlackBlockDivider =+    object+      [ "type" .= ("divider" :: Text)+      ]+  toJSON (SlackBlockActions mBlockId as) =+    objectOptional+      [ "type" .=! ("actions" :: Text)+      , "block_id" .=? mBlockId+      , "elements" .=! as+      ]+  -- FIXME(jadel): should this be an error? Slack doesn't accept these+  toJSON (SlackBlockRichText _) =+    object []+  toJSON (SlackBlockHeader slackPlainText) =+    object+      [ "type" .= ("header" :: Text)+      , "text" .= slackPlainText+      ]++instance FromJSON SlackBlock where+  parseJSON = withObject "SlackBlock" $ \obj -> do+    (slackBlockType :: Text) <- obj .: "type"+    case slackBlockType of+      "section" -> do+        (sectionContentObj :: Value) <- obj .: "text"+        SlackContentText sectionContentText <- parseJSON sectionContentObj+        mSectionAccessory <- obj .:? "accessory"+        pure $ SlackBlockSection sectionContentText mSectionAccessory+      "context" -> do+        (contextElementsObj :: Value) <- obj .: "elements"+        slackContent <- parseJSON contextElementsObj+        pure $ SlackBlockContext slackContent+      "image" -> do+        SlackContentImage i <- parseJSON $ Object obj+        pure $ SlackBlockImage i+      "divider" -> pure SlackBlockDivider+      "actions" -> do+        slackActions <- obj .: "elements"+        mBlockId <- obj .:? "block_id"+        pure $ SlackBlockActions mBlockId slackActions+      "rich_text" -> do+        elements <- obj .: "elements"+        mBlockId <- obj .:? "block_id"+        pure . SlackBlockRichText $+          RichText+            { blockId = mBlockId+            , elements+            }+      "header" -> do+        (headerContentObj :: Value) <- obj .: "text"+        headerContentText <- parseJSON headerContentObj+        pure $ SlackBlockHeader headerContentText+      _ -> fail "Unknown SlackBlock type, must be one of ['section', 'context', 'image', 'divider', 'actions', 'rich_text', 'header']"++newtype SlackMessage = SlackMessage [SlackBlock]+  deriving newtype (Semigroup, Monoid, Eq)++instance Show SlackMessage where+  show (SlackMessage arr) = intercalate " " (map show arr)++instance ToJSON SlackMessage where+  toJSON (SlackMessage arr) = toJSON arr++instance FromJSON SlackMessage where+  parseJSON = withArray "SlackMessage" $ \arr -> do+    (parsedAsArrayOfSlackBlocks :: V.Vector SlackBlock) <- traverse parseJSON arr+    let slackBlockList = V.toList parsedAsArrayOfSlackBlocks+    pure $ SlackMessage slackBlockList++textToMessage :: Text -> SlackMessage+textToMessage = markdown . message++class Markdown a where+  markdown :: SlackText -> a++instance Markdown SlackMessage where+  markdown t = SlackMessage [SlackBlockSection t Nothing]++instance Markdown SlackContext where+  markdown t = SlackContext [SlackContentText t]++class Image a where+  image :: SlackImage -> a++instance Image SlackMessage where+  image i = SlackMessage [SlackBlockImage i]++instance Image SlackContext where+  image i = SlackContext [SlackContentImage i]++context :: SlackContext -> SlackMessage+context c = SlackMessage [SlackBlockContext c]++textToContext :: Text -> SlackMessage+textToContext = context . markdown . message++-- | Generates interactive components such as buttons.+actions :: ToSlackActionList as => as -> SlackMessage+actions as = SlackMessage [SlackBlockActions Nothing $ toSlackActionList as]++-- | Generates interactive components such as buttons with a 'SlackBlockId'.+actionsWithBlockId :: ToSlackActionList as => SlackBlockId -> as -> SlackMessage+actionsWithBlockId slackBlockId as = SlackMessage [SlackBlockActions (Just slackBlockId) $ toSlackActionList as]++-- | Settings for [button elements](https://api.slack.com/reference/block-kit/block-elements#button).+data ButtonSettings = ButtonSettings+  { buttonUrl :: OptionalSetting (NonEmptyText 3000)+  -- ^ Optional URL to load into the user's browser.+  -- However, Slack will still call the webhook and you must send an acknowledgement response.+  , buttonValue :: OptionalSetting (NonEmptyText 2000)+  -- ^ Optional value to send with the interaction payload.+  -- One commoon use is to send state via JSON encoding.+  , buttonStyle :: OptionalSetting SlackStyle+  -- ^ Optional 'SlackStyle'. If not provided, uses the default style which is a black button.+  , buttonConfirm :: OptionalSetting SlackConfirmObject+  -- ^ An optional confirmation dialog to display.+  }++-- | Default button settings.+buttonSettings :: ButtonSettings+buttonSettings =+  ButtonSettings+    { buttonUrl = emptySetting+    , buttonValue = emptySetting+    , buttonStyle = emptySetting+    , buttonConfirm = emptySetting+    }++-- | Button builder.+button :: SlackActionId -> SlackButtonText -> ButtonSettings -> SlackAction+button actionId buttonText ButtonSettings {..} =+  SlackAction actionId $+    SlackButton+      { slackButtonText = buttonText+      , slackButtonUrl = unOptionalSetting buttonUrl+      , slackButtonValue = unOptionalSetting buttonValue+      , slackButtonStyle = unOptionalSetting buttonStyle+      , slackButtonConfirm = unOptionalSetting buttonConfirm+      }++-- | Settings for [confirmation dialog objects](https://api.slack.com/reference/block-kit/composition-objects#confirm).+data ConfirmSettings = ConfirmSettings+  { confirmTitle :: Text+  -- ^ Plain text title for the dialog window. Max length 100 characters.+  , confirmText :: Text+  -- ^ Markdown explanatory text that appears in the confirm dialog.+  -- Max length is 300 characters.+  , confirmConfirm :: Text+  -- ^ Plain text to display in the \"confirm\" button.+  -- Max length is 30 characters.+  , confirmDeny :: Text+  -- ^ Plain text to display in the \"deny\" button.+  -- Max length is 30 characters.+  , confirmStyle :: OptionalSetting SlackStyle+  -- ^ Optional 'SlackStyle' to use for the \"confirm\" button.+  }++-- | Default settings for a \"Are you sure?\" confirmation dialog.+confirmAreYouSure :: ConfirmSettings+confirmAreYouSure =+  ConfirmSettings+    { confirmTitle = "Are You Sure?"+    , confirmText = "Are you sure you wish to perform this operation?"+    , confirmConfirm = "Yes"+    , confirmDeny = "No"+    , confirmStyle = emptySetting+    }++-- | Confirm dialog builder.+confirm :: ConfirmSettings -> SlackConfirmObject+confirm ConfirmSettings {..} =+  SlackConfirmObject+    { slackConfirmTitle = plaintextonly confirmTitle+    , slackConfirmText = mrkdwn confirmText+    , slackConfirmConfirm = plaintextonly confirmConfirm+    , slackConfirmDeny = plaintextonly confirmDeny+    , slackConfirmStyle = unOptionalSetting confirmStyle+    }++-- | 'SlackBlockId' should be unique for each message and each iteration+-- of a message. If a message is updated, use a new block_id.+type SlackBlockId = NonEmptyText 255++-- | All Slack Actions must have a 'SlackActionId' and one 'SlackActionComponent' (such as a button).+data SlackAction = SlackAction SlackActionId SlackActionComponent+  deriving stock (Eq)++instance Show SlackAction where+  show (SlackAction actionId component) = show actionId <> " " <> show component++-- | [Confirm dialog object](https://api.slack.com/reference/block-kit/composition-objects#confirm).+data SlackConfirmObject = SlackConfirmObject+  { slackConfirmTitle :: SlackPlainTextOnly -- max length 100+  , slackConfirmText :: SlackTextObject -- max length 300+  , slackConfirmConfirm :: SlackPlainTextOnly -- max length 30+  , slackConfirmDeny :: SlackPlainTextOnly -- max length 30+  , slackConfirmStyle :: Maybe SlackStyle+  }+  deriving stock (Eq)++instance ToJSON SlackConfirmObject where+  toJSON SlackConfirmObject {..} =+    objectOptional+      [ "title" .=! slackConfirmTitle+      , "text" .=! slackConfirmText+      , "confirm" .=! slackConfirmConfirm+      , "deny" .=! slackConfirmDeny+      , "style" .=? slackConfirmStyle+      ]++instance FromJSON SlackConfirmObject where+  parseJSON = withObject "SlackConfirmObject" $ \obj -> do+    slackConfirmTitle <- obj .: "title"+    slackConfirmText <- obj .: "text"+    slackConfirmConfirm <- obj .: "confirm"+    slackConfirmDeny <- obj .: "deny"+    slackConfirmStyle <- obj .:? "style"+    pure SlackConfirmObject {..}++newtype SlackResponseUrl = SlackResponseUrl {unSlackResponseUrl :: Text}+  deriving stock (Eq, Show)+  deriving newtype (FromJSON, ToJSON)++-- | Represents the data we get from a callback from Slack for interactive+-- operations. See https://api.slack.com/interactivity/handling#payloads+data SlackInteractivePayload = SlackInteractivePayload+  { sipUserId :: Text+  , sipUsername :: Text+  , sipName :: Text+  , sipResponseUrl :: Maybe SlackResponseUrl+  , sipTriggerId :: Maybe Text+  , sipActions :: [SlackActionResponse]+  }+  deriving stock (Show)++instance FromJSON SlackInteractivePayload where+  parseJSON = withObject "SlackInteractivePayload" $ \obj -> do+    user <- obj .: "user"+    sipUserId <- user .: "id"+    sipUsername <- user .: "username"+    sipName <- user .: "name"+    sipResponseUrl <- obj .:? "response_url"+    sipTriggerId <- obj .:? "trigger_id"+    actionsObj <- obj .: "actions"+    sipActions <- parseJSON actionsObj+    pure $ SlackInteractivePayload {..}++-- | Which component and it's IDs that triggered an interactive webhook call.+data SlackActionResponse = SlackActionResponse+  { sarBlockId :: SlackBlockId+  , sarActionId :: SlackActionId+  , sarActionComponent :: SlackActionComponent+  }+  deriving stock (Show)++instance FromJSON SlackActionResponse where+  parseJSON = withObject "SlackActionResponse" $ \obj -> do+    sarBlockId <- obj .: "block_id"+    sarActionId <- obj .: "action_id"+    sarActionComponent <- parseJSON $ Object obj+    pure $ SlackActionResponse {..}++data SlackInteractiveResponseResponse = SlackInteractiveResponseResponse {unSlackInteractiveResponseResponse :: Bool}++instance FromJSON SlackInteractiveResponseResponse where+  parseJSON = withObject "SlackInteractiveResponseResponse" $ \obj -> do+    res <- obj .: "ok"+    pure $ SlackInteractiveResponseResponse res++-- | Type of message to send in response to an interactive webhook.+-- See Slack's [Handling user interaction in your Slack apps](https://api.slack.com/interactivity/handling#responses)+-- for a description of these fieldds.+data SlackInteractiveResponse+  = -- | Respond with a new message.+    SlackInteractiveResponse SlackMessage+  | -- | Respond with a message that only the interacting user can usee.+    Ephemeral SlackMessage+  | -- | Replace the original message.+    ReplaceOriginal SlackMessage+  | -- | Delete the original message.+    DeleteOriginal+  deriving stock (Show)++instance ToJSON SlackInteractiveResponse where+  toJSON (SlackInteractiveResponse msg) = object ["blocks" .= msg]+  toJSON (Ephemeral msg) = object ["blocks" .= msg, "replace_original" .= False, "response_type" .= ("ephemeral" :: Text)]+  toJSON (ReplaceOriginal msg) = object ["blocks" .= msg, "replace_original" .= True]+  toJSON DeleteOriginal = object ["delete_original" .= True]++-- | Text to be displayed in a 'SlackButton'.+-- Up to 75 characters, but may be truncated to 30 characters.+newtype SlackButtonText = SlackButtonText (NonEmptyText 75)+  deriving stock (Show)+  deriving newtype (Eq, FromJSON)++instance Slack SlackButtonText where+  message (SlackButtonText m) = SlackText [nonEmptyTextToText m]++-- It isn't the end of the world if this gets truncated (slack may truncate it+-- to about 30 characters anyway) so we have this convenience instance to+-- use plain strings for button text.+instance IsString SlackButtonText where+  fromString s = SlackButtonText . unsafeMkNonEmptyText . cs $ take 75 s++-- | The component in a 'SlackAction'. Do not use directly.+-- Use the builder functions such as 'button' instead.+data SlackActionComponent = SlackButton+  { slackButtonText :: SlackButtonText -- max length 75, may truncate to ~30+  , slackButtonUrl :: Maybe (NonEmptyText 3000) -- max length 3000+  , slackButtonValue :: Maybe (NonEmptyText 2000) -- max length 2000+  , slackButtonStyle :: Maybe SlackStyle+  , slackButtonConfirm :: Maybe SlackConfirmObject+  }+  deriving stock (Eq)++instance FromJSON SlackActionComponent where+  parseJSON = withObject "SlactActionComponent" $ \obj -> do+    (slackActionType :: Text) <- obj .: "type"+    case slackActionType of+      "button" -> do+        text <- obj .: "text"+        slackButtonText <- text .: "text"+        slackButtonUrl <- obj .:? "url"+        slackButtonValue <- obj .:? "value"+        slackButtonStyle <- obj .:? "style"+        slackButtonConfirm <- obj .:? "confirm"+        pure $ SlackButton {..}+      _ -> fail "Unknown SlackActionComponent type, must be one of ['button']"++instance Show SlackActionComponent where+  show SlackButton {..} = "[button " <> show slackButtonText <> "]"++instance ToJSON SlackAction where+  toJSON (SlackAction actionId SlackButton {..}) =+    objectOptional+      [ "type" .=! ("button" :: Text)+      , "action_id" .=! actionId+      , "text" .=! plaintext slackButtonText+      , "url" .=? slackButtonUrl+      , "value" .=? slackButtonValue+      , "style" .=? slackButtonStyle+      , "confirm" .=? slackButtonConfirm+      ]++instance FromJSON SlackAction where+  parseJSON = withObject "SlackAction" $ \obj -> do+    actionId <- obj .: "action_id"+    slackActionComponent <- parseJSON $ Object obj+    pure $ SlackAction actionId slackActionComponent
+ src/Web/Slack/Experimental/Events/Types.hs view
@@ -0,0 +1,184 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE TemplateHaskell #-}++-- FIXME(jadel): Use NoFieldSelectors when we drop everything before 9.2.++-- | Types for the [Slack Events API](https://api.slack.com/events).+module Web.Slack.Experimental.Events.Types where++import Data.Aeson+import Data.Aeson.TH+import Data.Time.Clock.System (SystemTime)+import Web.Slack.AesonUtils+import Web.Slack.Experimental.Blocks (SlackBlock)+import Web.Slack.Prelude+import Web.Slack.Types (ConversationId, TeamId, UserId)++-- | Not a ConversationType for some mysterious reason; this one has Channel as+-- an option, which does not exist as a ConversationType. Slack, why?+data ChannelType = Channel | Group | Im+  deriving stock (Show, Eq)++$(deriveJSON snakeCaseOptions ''ChannelType)++-- | <https://api.slack.com/events/message>+data MessageEvent = MessageEvent+  { blocks :: Maybe [SlackBlock]+  , channel :: ConversationId+  , text :: Text+  , channelType :: ChannelType+  , -- FIXME(jadel): clientMsgId??+    user :: UserId+  , ts :: Text+  , threadTs :: Maybe Text+  -- ^ Present if the message is in a thread+  , appId :: Maybe Text+  -- ^ Present if it's sent by an app+  --+  --   For mysterious reasons, this is NOT+  --   <https://api.slack.com/events/message/bot_message>, this is a regular+  --   message but with bot metadata. I Think it's because there *is* an+  --   associated user.+  --+  --   See @botMessage.json@ golden parser test.+  , botId :: Maybe Text+  -- ^ Present if it's sent by a bot user+  }+  deriving stock (Show)++-- | <https://api.slack.com/events/message/message_changed>+--+-- FIXME(jadel): implement. This is mega cursed! in the normal message event+-- the channel is called "channel" but here it is called "channelId" and also+-- has a "channel_name" and "channel_team". Why?!+--+-- We don't decode these on this basis.+data MessageUpdateEvent = MessageUpdateEvent+  { message :: MessageEvent+  }+  deriving stock (Show)++$(deriveFromJSON snakeCaseOptions ''MessageEvent)+$(deriveFromJSON snakeCaseOptions ''MessageUpdateEvent)++-- | FIXME: this might be a Channel, but may also be missing some fields/have bonus+-- because Slack is cursed.+data CreatedChannel = CreatedChannel+  { id :: ConversationId+  , isChannel :: Bool+  , name :: Text+  , nameNormalized :: Text+  , creator :: UserId+  , created :: SystemTime+  , isShared :: Bool+  , isOrgShared :: Bool+  , -- what is this?+    contextTeamId :: TeamId+  }+  deriving stock (Show)++-- | A channel was created.+--+-- <https://api.slack.com/events/channel_created>+data ChannelCreatedEvent = ChannelCreatedEvent+  { channel :: CreatedChannel+  }+  deriving stock (Show)++$(deriveFromJSON snakeCaseOptions ''CreatedChannel)+$(deriveFromJSON snakeCaseOptions ''ChannelCreatedEvent)++-- | You left a channel.+--+-- <https://api.slack.com/events/channel_left>+data ChannelLeftEvent = ChannelLeftEvent+  { actorId :: UserId+  , channel :: ConversationId+  , eventTs :: Text+  }+  deriving stock (Show)++$(deriveFromJSON snakeCaseOptions ''ChannelLeftEvent)++-- | <https://api.slack.com/events/url_verification>+data UrlVerificationPayload = UrlVerificationPayload+  { challenge :: Text+  }+  deriving stock (Show)++$(deriveFromJSON snakeCaseOptions ''UrlVerificationPayload)++newtype EventId = EventId {unEventId :: Text}+  deriving newtype (FromJSON)+  deriving stock (Show)++newtype MessageId = MessageId {unMessageId :: Text}+  deriving newtype (FromJSON)+  deriving stock (Show, Eq)++data Event+  = EventMessage MessageEvent+  | EventMessageChanged+  | -- | Weird message event of subtype channel_join. Sent "sometimes", according+    -- to a random Slack blog post from 2017:+    -- <https://api.slack.com/changelog/2017-05-rethinking-channel-entrance-and-exit-events-and-messages>+    --+    -- Documentation: <https://api.slack.com/events/message/channel_join>+    EventChannelJoinMessage+  | EventChannelCreated ChannelCreatedEvent+  | EventChannelLeft ChannelLeftEvent+  | EventUnknown Value+  deriving stock (Show, Generic)++instance FromJSON Event where+  parseJSON = withObject "MessageEvent" \obj -> do+    tag :: Text <- obj .: "type"+    subtype :: Maybe Text <- obj .:? "subtype"+    case (tag, subtype) of+      ("message", Nothing) -> EventMessage <$> parseJSON @MessageEvent (Object obj)+      ("message", Just "message_changed") -> pure EventMessageChanged+      ("message", Just "channel_join") -> pure EventChannelJoinMessage+      ("channel_created", Nothing) -> EventChannelCreated <$> parseJSON (Object obj)+      ("channel_left", Nothing) -> EventChannelLeft <$> parseJSON (Object obj)+      _ -> pure $ EventUnknown (Object obj)++data EventCallback = EventCallback+  { eventId :: EventId+  , teamId :: TeamId+  , eventTime :: SystemTime+  , event :: Event+  }+  deriving stock (Show)++$(deriveFromJSON snakeCaseOptions ''EventCallback)++data SlackWebhookEvent+  = EventUrlVerification UrlVerificationPayload+  | EventEventCallback EventCallback+  | EventUnknownWebhook Value+  deriving stock (Show, Generic)++instance FromJSON SlackWebhookEvent where+  parseJSON = withObject "SlackWebhookEvent" \obj -> do+    tag :: Text <- obj .: "type"+    case tag of+      "url_verification" -> EventUrlVerification <$> parseJSON (Object obj)+      "event_callback" -> EventEventCallback <$> parseJSON (Object obj)+      _ -> pure $ EventUnknownWebhook (Object obj)++-- * Event responses++-- $eventResponses+--+-- By and large, Slack does not care about the response returned from event+-- handlers, at least for the 'EventEventCallback' as long as your service+-- 200s. The exception is 'EventUrlVerification', which is expected to return a+-- 'UrlVerificationResponse'++-- | Response for @url_verification@.+data UrlVerificationResponse = UrlVerificationResponse+  { challenge :: Text+  }+  deriving stock (Show)++$(deriveToJSON defaultOptions ''UrlVerificationResponse)
+ src/Web/Slack/Experimental/RequestVerification.hs view
@@ -0,0 +1,101 @@+module Web.Slack.Experimental.RequestVerification+  ( SlackSigningSecret (..),+    SlackSignature (..),+    SlackRequestTimestamp (..),+    SlackVerificationFailed (..),+    validateRequest,+    validateRequest',+  )+where++import Crypto.Hash (SHA256, digestFromByteString)+import Crypto.MAC.HMAC+import Data.Aeson (eitherDecodeStrict)+import Data.ByteString.Base16 qualified as B16+import Data.ByteString.Char8 (readInt)+import Data.Either.Combinators (mapLeft, maybeToRight)+import Data.Time (NominalDiffTime)+import Data.Time.Clock.POSIX (getPOSIXTime)+import Web.HttpApiData (FromHttpApiData (..))+import Web.Slack.Prelude++-- | Slack generated Signing Secret placed into configuration.+-- See https://api.slack.com/authentication/verifying-requests-from-slack#signing_secrets_admin_page+newtype SlackSigningSecret+  = SlackSigningSecret ByteString+  deriving stock (Eq)++instance Show SlackSigningSecret where+  show _ = "<SlackSigningSecret>"++newtype SlackSignature = SlackSignature ByteString+  deriving newtype (Eq, Show)++newtype SlackRequestTimestamp = SlackRequestTimestamp ByteString+  deriving newtype (Eq, Show)++instance FromHttpApiData SlackRequestTimestamp where+  parseQueryParam _ = error "SlackRequestTimestamp should not be in a query param"+  parseUrlPiece _ = error "SlackRequestTimestamp should not be in a url piece"+  parseHeader = Right . SlackRequestTimestamp++instance FromHttpApiData SlackSignature where+  parseQueryParam _ = error "SlackSignature should not be in a query param"+  parseUrlPiece _ = error "SlackSignature should not be in a url piece"+  parseHeader = Right . SlackSignature++data SlackVerificationFailed+  = VerificationMissingTimestamp+  | VerificationMalformedTimestamp ByteString+  | VerificationTimestampOutOfRange Int+  | VerificationMissingSignature+  | VerificationUnknownSignatureVersion ByteString+  | VerificationMalformedSignature String+  | VerificationUndecodableSignature ByteString+  | VerificationSignatureMismatch+  | VerificationCannotParse Text+  deriving stock (Show, Eq)++instance Exception SlackVerificationFailed++validateRequest ::+  (MonadIO m, FromJSON a) =>+  SlackSigningSecret ->+  SlackSignature ->+  SlackRequestTimestamp ->+  ByteString ->+  m (Either SlackVerificationFailed a)+validateRequest secret sig reqTs body =+  liftIO getPOSIXTime >>= \time -> pure $ validateRequest' time secret sig reqTs body++-- | Pure version of 'validateRequest'. Probably only useful for tests.+validateRequest' ::+  FromJSON a =>+  NominalDiffTime ->+  SlackSigningSecret ->+  SlackSignature ->+  SlackRequestTimestamp ->+  ByteString ->+  Either SlackVerificationFailed a+validateRequest' now (SlackSigningSecret secret) (SlackSignature sigHeader) (SlackRequestTimestamp timestampString) body = do+  let fiveMinutes = 5 * 60+  -- timestamp must be an Int for proper basestring construction below+  timestamp <-+    maybeToRight (VerificationMalformedTimestamp timestampString) $+      fst <$> readInt timestampString+  if abs (now - fromIntegral timestamp) > fiveMinutes+    then Left $ VerificationTimestampOutOfRange timestamp+    else Right ()+  sigHeaderStripped <-+    maybeToRight (VerificationUnknownSignatureVersion sigHeader) $+      stripPrefix "v0=" sigHeader+  sigDecoded <-+    mapLeft VerificationMalformedSignature $+      B16.decode sigHeaderStripped+  sig :: HMAC SHA256 <-+    maybeToRight (VerificationUndecodableSignature sigDecoded) $+      HMAC <$> digestFromByteString sigDecoded+  let basestring = encodeUtf8 ("v0:" <> tshow timestamp <> ":") <> body+  when (hmac secret basestring /= sig) $+    Left VerificationSignatureMismatch+  mapLeft (VerificationCannotParse . pack) $ eitherDecodeStrict body
+ src/Web/Slack/Internal.hs view
@@ -0,0 +1,64 @@+-- | Internal things in slack-web. May be changed arbitrarily!+module Web.Slack.Internal where++import Data.Aeson (Value (..))+import Network.HTTP.Client (Manager)+import Servant.API hiding (addHeader)+-- import Servant.Client.Core++import Servant.Client (BaseUrl (..), ClientError, ClientM, Scheme (..), mkClientEnv, runClientM)+import Servant.Client.Core (AuthClientData, AuthenticatedRequest, Request, addHeader, mkAuthenticatedRequest)+import Web.Slack.Common qualified as Common+import Web.Slack.Pager (Response)+import Web.Slack.Prelude++data SlackConfig = SlackConfig+  { slackConfigManager :: Manager+  , slackConfigToken :: Text+  }++-- contains errors that can be returned by the slack API.+-- constrast with 'SlackClientError' which additionally+-- contains errors which occured during the network communication.+data ResponseSlackError = ResponseSlackError Text+  deriving stock (Eq, Show)++-- |+-- Internal type!+newtype ResponseJSON a = ResponseJSON (Either ResponseSlackError a)++instance FromJSON a => FromJSON (ResponseJSON a) where+  parseJSON = withObject "Response" $ \o -> do+    ok <- o .: "ok"+    ResponseJSON+      <$> if ok+        then Right <$> parseJSON (Object o)+        else Left . ResponseSlackError <$> o .: "error"++mkSlackAuthenticateReq :: SlackConfig -> AuthenticatedRequest (AuthProtect "token")+mkSlackAuthenticateReq = (`mkAuthenticatedRequest` authenticateReq) . slackConfigToken++type instance+  AuthClientData (AuthProtect "token") =+    Text++authenticateReq ::+  Text ->+  Request ->+  Request+authenticateReq token =+  addHeader "Authorization" $ "Bearer " <> token++run ::+  ClientM (ResponseJSON a) ->+  Manager ->+  IO (Response a)+run clientAction mgr = do+  let baseUrl = BaseUrl Https "slack.com" 443 "/api"+  unnestErrors <$> liftIO (runClientM clientAction $ mkClientEnv mgr baseUrl)++unnestErrors :: Either ClientError (ResponseJSON a) -> Response a+unnestErrors (Right (ResponseJSON (Right a))) = Right a+unnestErrors (Right (ResponseJSON (Left (ResponseSlackError serv)))) =+  Left (Common.SlackError serv)+unnestErrors (Left slackErr) = Left (Common.ServantError slackErr)
src/Web/Slack/MessageParser.hs view
@@ -1,39 +1,39 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-} {-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE CPP #-}  -- | See https://api.slack.com/docs/message-formatting--- module Web.Slack.MessageParser-  ( messageToHtml-  , HtmlRenderers(..)-  , defaultHtmlRenderers+  ( messageToHtml,+    HtmlRenderers (..),+    defaultHtmlRenderers,   )-  where+where +-- FIXME: Web.Slack.Prelude+ -- base import Control.Monad-import Data.List-import Data.Maybe-import Data.Void- -- megaparsec-import Text.Megaparsec-import Text.Megaparsec.Char  -- mtl import Data.Functor.Identity-+import Data.List (intercalate)+import Data.Maybe -- slack-web-import Web.Slack.Types  -- text import Data.Text (Text)-import qualified Data.Text as T+import Data.Text qualified as T+import Data.Void+import Text.Megaparsec+import Text.Megaparsec.Char+import Web.Slack.Types+import Prelude -newtype SlackUrl = SlackUrl { unSlackUrl :: Text }-  deriving (Show, Eq)+newtype SlackUrl = SlackUrl {unSlackUrl :: Text}+  deriving stock (Show, Eq)  data SlackMsgItem   = SlackMsgItemPlainText Text@@ -46,7 +46,7 @@   | SlackMsgItemCodeSection Text   | SlackMsgItemQuoted [SlackMsgItem]   | SlackMsgItemEmoticon Text-  deriving (Show, Eq)+  deriving stock (Show, Eq)  #if MIN_VERSION_megaparsec(6,0,0) type MegaparsecError = Void@@ -63,29 +63,35 @@ type SlackParser a = ParsecT MegaparsecError T.Text Identity a  parseMessage :: Text -> [SlackMsgItem]-parseMessage input = fromMaybe [SlackMsgItemPlainText input] $-  parseMaybe (some $ parseMessageItem True) input+parseMessage input =+  fromMaybe [SlackMsgItemPlainText input] $+    parseMaybe (some $ parseMessageItem True) input  parseMessageItem :: Bool -> SlackParser SlackMsgItem-parseMessageItem acceptNewlines-  = parseBoldSection-  <|> parseItalicsSection-  <|> parseStrikethroughSection-  <|> try parseEmoticon-  <|> parseCode-  <|> parseInlineCode-  <|> parseUserLink-  <|> parseLink-  <|> parseBlockQuote-  <|> parsePlainText-  <|> parseWhitespace acceptNewlines+parseMessageItem acceptNewlines =+  parseBoldSection+    <|> parseItalicsSection+    <|> parseStrikethroughSection+    <|> try parseEmoticon+    <|> parseCode+    <|> parseInlineCode+    <|> parseUserLink+    <|> parseLink+    <|> parseBlockQuote+    <|> parsePlainText+    <|> parseWhitespace acceptNewlines  parsePlainText :: SlackParser SlackMsgItem-parsePlainText = SlackMsgItemPlainText . T.pack <$>-    someTill (noneOf stopChars) (void (lookAhead $ try $ oneOf stopChars)-                                   <|> lookAhead (try $ choice $ sectionEndSymbol <$> ['*', '_', ':', '~'])-                                   <|> lookAhead eof)-    where stopChars = [' ', '\n']+parsePlainText =+  SlackMsgItemPlainText . T.pack+    <$> someTill+      (noneOf stopChars)+      ( void (lookAhead $ try $ oneOf stopChars)+          <|> lookAhead (try $ choice $ sectionEndSymbol <$> ['*', '_', ':', '~'])+          <|> lookAhead eof+      )+  where+    stopChars = [' ', '\n']  -- slack accepts bold/italics modifiers -- only at word boundary. for instance 'my_word'@@ -99,7 +105,7 @@ sectionEndSymbol chr = void $ char chr >> lookAhead wordBoundary  parseCharDelimitedSection :: Char -> SlackParser [SlackMsgItem]-parseCharDelimitedSection chr = +parseCharDelimitedSection chr =   char chr *> someTill (parseMessageItem False) (sectionEndSymbol chr)  wordBoundary :: SlackParser ()@@ -118,75 +124,84 @@   fmap SlackMsgItemStrikethroughSection (parseCharDelimitedSection '~')  parseEmoticon :: SlackParser SlackMsgItem-parseEmoticon = fmap (SlackMsgItemEmoticon . T.pack) $-  char ':' *> someTill (alphaNumChar <|> char '_' <|> char '+') (sectionEndSymbol ':')+parseEmoticon =+  fmap (SlackMsgItemEmoticon . T.pack) $+    char ':' *> someTill (alphaNumChar <|> char '_' <|> char '+') (sectionEndSymbol ':')  parseUserLink :: SlackParser SlackMsgItem parseUserLink = do   void (string "<@")   userId <- UserId . T.pack <$> some (noneOf ['|', '>'])-  let linkWithoutDesc = char '>' >>-          pure (SlackMsgItemUserLink userId Nothing)-  let linkWithDesc = char '|' >>-          SlackMsgItemUserLink <$> pure userId <*> (Just <$> ((T.pack <$> some (noneOf ['>'])) <* char '>'))+  let linkWithoutDesc =+        char '>'+          >> pure (SlackMsgItemUserLink userId Nothing)+  let linkWithDesc =+        char '|'+          >> SlackMsgItemUserLink <$> pure userId <*> (Just <$> ((T.pack <$> some (noneOf ['>'])) <* char '>'))   linkWithDesc <|> linkWithoutDesc  parseLink :: SlackParser SlackMsgItem parseLink = do   void (char '<')   url <- SlackUrl . T.pack <$> some (noneOf ['|', '>'])-  let linkWithoutDesc = char '>' >>-          pure (SlackMsgItemLink (unSlackUrl url) url)-  let linkWithDesc = char '|' >>-          SlackMsgItemLink <$> ((T.pack <$> some (noneOf ['>'])) <* char '>') <*> pure url+  let linkWithoutDesc =+        char '>'+          >> pure (SlackMsgItemLink (unSlackUrl url) url)+  let linkWithDesc =+        char '|'+          >> SlackMsgItemLink <$> ((T.pack <$> some (noneOf ['>'])) <* char '>') <*> pure url   linkWithDesc <|> linkWithoutDesc  parseCode :: SlackParser SlackMsgItem-parseCode = SlackMsgItemCodeSection . T.pack <$>-  (string "```" >> manyTill anySingle (string "```"))+parseCode =+  SlackMsgItemCodeSection . T.pack+    <$> (string "```" >> manyTill anySingle (string "```"))  parseInlineCode :: SlackParser SlackMsgItem-parseInlineCode = SlackMsgItemInlineCodeSection . T.pack <$>-  (char '`' *> some (noneOf ['`']) <* char '`')+parseInlineCode =+  SlackMsgItemInlineCodeSection . T.pack+    <$> (char '`' *> some (noneOf ['`']) <* char '`')  parseBlockQuote :: SlackParser SlackMsgItem parseBlockQuote = SlackMsgItemQuoted . intercalate [SlackMsgItemPlainText "<br/>"] <$> some blockQuoteLine  blockQuoteLine :: SlackParser [SlackMsgItem]-blockQuoteLine = string "&gt;" *> optional (char ' ') *>-    manyTill (parseMessageItem False) (eof <|> void newline)+blockQuoteLine =+  string "&gt;"+    *> optional (char ' ')+    *> manyTill (parseMessageItem False) (eof <|> void newline)  -- | -- Convert the slack format for messages (markdown like, see -- https://api.slack.com/docs/message-formatting ) to HTML.-messageToHtml-  :: HtmlRenderers-  -- ^ Renderers allow you to customize the message rendering.+messageToHtml ::+  -- | Renderers allow you to customize the message rendering.   -- Give 'defaultHtmlRenderers' for a default implementation.-  -> (UserId -> Text)-  -- ^ A function giving a user name for a user id. You can use 'Web.Slack.getUserDesc'-  -> SlackMessageText-  -- ^ A slack message to convert to HTML-  -> Text-  -- ^ The HTML-formatted slack message+  HtmlRenderers ->+  -- | A function giving a user name for a user id. You can use 'Web.Slack.getUserDesc'+  (UserId -> Text) ->+  -- | A slack message to convert to HTML+  SlackMessageText ->+  -- | The HTML-formatted slack message+  Text messageToHtml htmlRenderers getUserDesc =   messageToHtml' htmlRenderers getUserDesc . parseMessage . unSlackMessageText  messageToHtml' :: HtmlRenderers -> (UserId -> Text) -> [SlackMsgItem] -> Text messageToHtml' htmlRenderers getUserDesc = foldr ((<>) . msgItemToHtml htmlRenderers getUserDesc) "" -data HtmlRenderers-  = HtmlRenderers+data HtmlRenderers = HtmlRenderers   { emoticonRenderer :: Text -> Text   }  defaultHtmlRenderers :: HtmlRenderers-defaultHtmlRenderers = HtmlRenderers-  { emoticonRenderer = \code -> ":" <> code <> ":"-  }-  +defaultHtmlRenderers =+  HtmlRenderers+    { emoticonRenderer = \code -> ":" <> code <> ":"+    }+ msgItemToHtml :: HtmlRenderers -> (UserId -> Text) -> SlackMsgItem -> Text-msgItemToHtml htmlRenderers@HtmlRenderers{..} getUserDesc = \case+msgItemToHtml htmlRenderers@HtmlRenderers {..} getUserDesc = \case   SlackMsgItemPlainText txt -> T.replace "\n" "<br/>" txt   SlackMsgItemBoldSection cts ->     "<b>" <> messageToHtml' htmlRenderers getUserDesc cts <> "</b>"
src/Web/Slack/Pager.hs view
@@ -1,112 +1,65 @@-{-# LANGUAGE DeriveFoldable    #-}-{-# LANGUAGE DeriveFunctor     #-}-{-# LANGUAGE DeriveTraversable #-}-{-# LANGUAGE NamedFieldPuns    #-}-{-# LANGUAGE OverloadedStrings #-}- module Web.Slack.Pager-  ( Response-  , conversationsHistoryAllBy-  , repliesFetchAllBy-  , LoadPage-  , loadingPage-  ) where---- base-import           Control.Monad.IO.Class (MonadIO, liftIO)-import           Data.IORef             (newIORef, readIORef, writeIORef)-import           Data.Maybe             (isNothing)---- slack-web-import qualified Web.Slack.Common       as Common-import qualified Web.Slack.Conversation as Conversation-import           Web.Slack.Types        (Cursor)----- | Public only for testing.-conversationsHistoryAllBy-  :: MonadIO m-  => (Conversation.HistoryReq -> m (Response Conversation.HistoryRsp))-  -- ^ Response generator-  -> Conversation.HistoryReq-  -- ^ The first request to send. _NOTE_: 'Conversation.historyReqCursor' is silently ignored.-  -> m (LoadPage m Common.Message)-  -- ^ An action which returns a new page of messages every time called.-  --   If there are no pages anymore, it returns an empty list.-conversationsHistoryAllBy sendRequest initialRequest =-  genericFetchAllBy-    sendRequest-    (\cursor -> initialRequest { Conversation.historyReqCursor = cursor })----- | Public only for testing.-repliesFetchAllBy-  :: MonadIO m-  => (Conversation.RepliesReq -> m (Response Conversation.HistoryRsp))-  -- ^ Response generator-  -> Conversation.RepliesReq-  -- ^ The first request to send. _NOTE_: 'Conversation.historyReqCursor' is silently ignored.-  -> m (LoadPage m Common.Message)-  -- ^ An action which returns a new page of messages every time called.-  --   If there are no pages anymore, it returns an empty list.-repliesFetchAllBy sendRequest initialRequest =-  genericFetchAllBy-    sendRequest-    (\cursor -> initialRequest { Conversation.repliesReqCursor = cursor })+  ( Response,+    LoadPage,+    loadingPage,+    fetchAllBy,+    module Web.Slack.Pager.Types,+  )+where +import Web.Slack.Common qualified as Common+import Web.Slack.Conversation qualified as Conversation+import Web.Slack.Pager.Types+import Web.Slack.Prelude  type Response a = Either Common.SlackClientError a - -- | Represents an action which returns a paginated response from Slack. --   Every time calling the action, it performs a request with a new cursor --   to get the next page. --   If there is no more response, the action returns an empty list. type LoadPage m a = m (Response [a]) - -- | Utility function for 'LoadPage'. Perform the 'LoadPage' action to call --   the function with the loaded page, until an empty page is loaded. loadingPage :: (Monad m, Monoid n) => LoadPage m a -> (Response [a] -> m n) -> m n loadingPage loadPage usePage = go mempty- where-  go result = do-    epage <- loadPage-    case epage of+  where+    go result = do+      epage <- loadPage+      case epage of         Right page ->           if null page             then return result             else (go $!) . (result <>) =<< usePage epage         Left e -> (result <>) <$> usePage (Left e) --genericFetchAllBy-  :: MonadIO m-  => (a -> m (Response Conversation.HistoryRsp))-  -> (Maybe Cursor -> a)-  -> m (LoadPage m Common.Message)-genericFetchAllBy sendRequest requestFromCursor = do+fetchAllBy ::+  ( MonadIO m+  , PagedRequest req+  , PagedResponse resp+  ) =>+  (req -> m (Response resp)) ->+  req ->+  m (LoadPage m (ResponseObject resp))+fetchAllBy sendRequest initialRequest = do   cursorRef <- liftIO $ newIORef Nothing -  let collectAndUpdateCursor-        Conversation.HistoryRsp-          { Conversation.historyRspMessages-          , Conversation.historyRspResponseMetadata-          } = do-        let newCursor = Conversation.responseMetadataNextCursor =<< historyRspResponseMetadata+  let requestFromCursor cursor = setCursor cursor initialRequest+      collectAndUpdateCursor resp = do+        let newCursor = Conversation.responseMetadataNextCursor =<< getResponseMetadata resp             -- emptyCursor is used for the marker to show that there are no more pages.             cursorToSave = if isNothing newCursor then emptyCursor else newCursor         writeIORef cursorRef cursorToSave-        return historyRspMessages+        return $ getResponseData resp    return $ do     cursor <- liftIO $ readIORef cursorRef     if cursor == emptyCursor-      then-        return $ Right []+      then return $ Right []       else         traverse (liftIO . collectAndUpdateCursor)           =<< sendRequest (requestFromCursor cursor)- where-  -- Used for the marker to show that there are no more pages.-  emptyCursor = Just $ Common.Cursor ""+  where+    -- Used for the marker to show that there are no more pages.+    emptyCursor = Just $ Common.Cursor ""
+ src/Web/Slack/Pager/Types.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE TemplateHaskell #-}++module Web.Slack.Pager.Types where++import Web.Slack.Prelude+import Web.Slack.Util++newtype Cursor = Cursor {unCursor :: Text}+  deriving stock (Eq, Generic, Show)+  deriving newtype (NFData, Hashable, FromJSON, ToJSON, ToHttpApiData)++newtype ResponseMetadata = ResponseMetadata {responseMetadataNextCursor :: Maybe Cursor}+  deriving stock (Eq, Show, Generic)++instance NFData ResponseMetadata++$(deriveJSON (jsonOpts "responseMetadata") ''ResponseMetadata)++class PagedRequest a where+  setCursor :: Maybe Cursor -> a -> a++class PagedResponse a where+  type ResponseObject a+  getResponseMetadata :: a -> Maybe ResponseMetadata+  getResponseData :: a -> [ResponseObject a]
+ src/Web/Slack/Prelude.hs view
@@ -0,0 +1,20 @@+module Web.Slack.Prelude+  ( module ClassyPrelude,+    module Data.Aeson,+    module Data.Aeson.TH,+    cs,+    ToHttpApiData,+    NonEmpty (..),+    Default (..),+    Proxy (..),+  )+where++import ClassyPrelude hiding (link)+import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.:?), (.=))+import Data.Aeson.TH (deriveFromJSON, deriveJSON, deriveToJSON)+import Data.Default.Class (Default (..))+import Data.List.NonEmpty (NonEmpty (..))+import Data.Proxy (Proxy (..))+import Data.String.Conversions (cs)+import Web.HttpApiData (ToHttpApiData)
src/Web/Slack/Types.hs view
@@ -1,94 +1,66 @@-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE DerivingStrategies #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE TypeApplications #-}+----------------------------------------------------------------------  ----------------------------------------------------------------------+ -- | -- Module: Web.Slack.Types -- Description:--------------------------------------------------------------------------------- module Web.Slack.Types-  ( Color(..)-  , UserId(..)-  , ConversationId(..)-  , TeamId(..)-  , Cursor(..)-  , SlackTimestamp(..)-  , mkSlackTimestamp-  , timestampFromText-  , SlackMessageText(..)+  ( Color (..),+    UserId (..),+    ConversationId (..),+    TeamId (..),+    Cursor (..),+    SlackTimestamp (..),+    mkSlackTimestamp,+    timestampFromText,+    SlackMessageText (..),   )-  where+where --- aeson+import Control.Monad (MonadFail (..)) import Data.Aeson---- base-import Data.Bifunctor (second)-import GHC.Generics (Generic)---- deepseq-import Control.DeepSeq (NFData)---- hashable-import Data.Hashable (Hashable)---- http-api-data-import Web.HttpApiData---- text-import Data.Text (Text)-import qualified Data.Text as T+import Data.Text qualified as T import Data.Text.Read (rational)---- time import Data.Time.Clock import Data.Time.Clock.POSIX+import Web.HttpApiData+import Web.Slack.Pager.Types+import Web.Slack.Prelude  -- Ord to allow it to be a key of a Map-newtype Color = Color { unColor :: Text }+newtype Color = Color {unColor :: Text}   deriving stock (Eq, Ord, Generic, Show)   deriving newtype (NFData, Hashable, FromJSON, ToJSON)  -- Ord to allow it to be a key of a Map-newtype UserId = UserId { unUserId :: Text }+newtype UserId = UserId {unUserId :: Text}   deriving stock (Eq, Ord, Generic, Show)   deriving newtype (NFData, Hashable, FromJSON, ToJSON, ToHttpApiData)  -- | Common identifier for every type of 'Conversation'. --   Unique to the team which the conversation belongs to. -- Ord to allow it to be a key of a Map-newtype ConversationId = ConversationId { unConversationId :: Text }+newtype ConversationId = ConversationId {unConversationId :: Text}   deriving stock (Eq, Ord, Generic, Show)   deriving newtype (NFData, Hashable, FromJSON, ToJSON, ToHttpApiData)  -- Ord to allow it to be a key of a Map-newtype TeamId = TeamId { unTeamId :: Text }+newtype TeamId = TeamId {unTeamId :: Text}   deriving stock (Eq, Ord, Generic, Show)   deriving newtype (NFData, Hashable, FromJSON, ToJSON, ToHttpApiData) -newtype Cursor = Cursor { unCursor :: Text }-  deriving stock (Eq, Generic, Show)-  deriving newtype (NFData, Hashable, FromJSON, ToJSON, ToHttpApiData)- -- | Message text in the format returned by Slack, -- see https://api.slack.com/docs/message-formatting -- Consider using 'messageToHtml' for displaying.-newtype SlackMessageText = SlackMessageText { unSlackMessageText :: Text}+newtype SlackMessageText = SlackMessageText {unSlackMessageText :: Text}   deriving stock (Eq, Ord, Generic, Show)   deriving newtype (NFData, Hashable, FromJSON, ToJSON) -data SlackTimestamp =-  SlackTimestamp-    { slackTimestampTs :: Text-    , slackTimestampTime :: UTCTime-    }+data SlackTimestamp = SlackTimestamp+  { slackTimestampTs :: Text+  , slackTimestampTime :: UTCTime+  }   deriving stock (Eq, Show, Generic)  instance NFData SlackTimestamp@@ -99,24 +71,25 @@ -- | Convert timestamp texts e.g. "1595719220.011100" into 'SlackTimestamp' timestampFromText :: Text -> Either String SlackTimestamp timestampFromText t = f =<< rational t- where-  f (posixTime, "") =-    Right . SlackTimestamp t $ posixSecondsToUTCTime posixTime-  f (_, _left) = Left "Unexpected text left after timestamp"+  where+    f (posixTime, "") =+      Right . SlackTimestamp t $ posixSecondsToUTCTime posixTime+    f (_, _left) = Left "Unexpected text left after timestamp"  mkSlackTimestamp :: UTCTime -> SlackTimestamp mkSlackTimestamp utctime = SlackTimestamp (take6DigitsAfterPoint $ T.pack (show unixts)) utctime- where-  unixts = nominalDiffTimeToSeconds $ utcTimeToPOSIXSeconds utctime-  take6DigitsAfterPoint = uncurry (<>) . second (T.take 7) . T.break (== '.')+  where+    unixts = nominalDiffTimeToSeconds $ utcTimeToPOSIXSeconds utctime+    take6DigitsAfterPoint = uncurry (<>) . second (T.take 7) . T.break (== '.')  instance ToHttpApiData SlackTimestamp where   toQueryParam (SlackTimestamp contents _) = contents  instance FromJSON SlackTimestamp where-  parseJSON = withText "Slack ts"-    $ either (fail . ("Invalid Slack ts: " ++)) pure-    . timestampFromText+  parseJSON =+    withText "Slack ts" $+      either (fail . ("Invalid Slack ts: " ++)) pure+        . timestampFromText  instance ToJSON SlackTimestamp where   toJSON = String . slackTimestampTs
src/Web/Slack/User.hs view
@@ -1,106 +1,100 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE OverloadedLists #-}  ----------------------------------------------------------------------++----------------------------------------------------------------------+ -- | -- Module: Web.Slack.User -- Description:--------------------------------------------------------------------------------- module Web.Slack.User-  ( Profile(..)-  , User(..)-  , ListRsp(..)-  , Email(..)-  , UserRsp(..)+  ( Profile (..),+    User (..),+    ListRsp (..),+    Email (..),+    UserRsp (..),   )-  where+where +-- FIXME: Web.Slack.Prelude+ -- aeson import Data.Aeson.TH- -- base-import GHC.Generics (Generic)  -- slack-web-import Web.Slack.Common-import Web.Slack.Util  -- text import Data.Text (Text)- -- time import Data.Time.Clock.POSIX-+import GHC.Generics (Generic) -- http-api-data-import Web.HttpApiData+ import Web.FormUrlEncoded+import Web.HttpApiData+import Web.Slack.Common+import Web.Slack.Util+import Prelude  -- See https://api.slack.com/types/user -data Profile = -  Profile -    { profileAvatarHash :: Maybe Text-    , profileStatusText :: Maybe Text-    , profileStatusEmoji :: Maybe Text -    , profileRealName :: Maybe Text -    , profileDisplayName :: Maybe Text -    , profileRealNameNormalized :: Maybe Text -    , profileDisplayNameNormalized :: Maybe Text -    , profileEmail :: Maybe Text -    , profileImage_24 :: Text -    , profileImage_32 :: Text -    , profileImage_48 :: Text -    , profileImage_72 :: Text -    , profileImage_192 :: Text -    , profileImage_512 :: Text -    , profileTeam :: Maybe Text -    }-  deriving (Eq, Generic, Show)+data Profile = Profile+  { profileAvatarHash :: Maybe Text+  , profileStatusText :: Maybe Text+  , profileStatusEmoji :: Maybe Text+  , profileRealName :: Maybe Text+  , profileDisplayName :: Maybe Text+  , profileRealNameNormalized :: Maybe Text+  , profileDisplayNameNormalized :: Maybe Text+  , profileEmail :: Maybe Text+  , profileImage_24 :: Text+  , profileImage_32 :: Text+  , profileImage_48 :: Text+  , profileImage_72 :: Text+  , profileImage_192 :: Text+  , profileImage_512 :: Text+  , profileTeam :: Maybe Text+  }+  deriving stock (Eq, Generic, Show)  $(deriveFromJSON (jsonOpts "profile") ''Profile) -data User =-  User-    { userId :: UserId-    , userName :: Text-    , userDeleted :: Bool-    , userColor :: Maybe Color-    , userProfile :: Maybe Profile-    , userIsAdmin :: Maybe Bool-    , userIsOwner :: Maybe Bool-    , userIsPrimaryOwner :: Maybe Bool-    , userIsRestricted :: Maybe Bool-    , userIsUltraRestricted :: Maybe Bool-    , userUpdated :: POSIXTime-    }-  deriving (Eq, Generic, Show)+data User = User+  { userId :: UserId+  , userName :: Text+  , userDeleted :: Bool+  , userColor :: Maybe Color+  , userProfile :: Maybe Profile+  , userIsAdmin :: Maybe Bool+  , userIsOwner :: Maybe Bool+  , userIsPrimaryOwner :: Maybe Bool+  , userIsRestricted :: Maybe Bool+  , userIsUltraRestricted :: Maybe Bool+  , userUpdated :: POSIXTime+  }+  deriving stock (Eq, Generic, Show)  $(deriveFromJSON (jsonOpts "user") ''User) -data ListRsp =-  ListRsp-    { listRspMembers :: [User]-    }-  deriving (Eq, Generic, Show)+data ListRsp = ListRsp+  { listRspMembers :: [User]+  }+  deriving stock (Eq, Generic, Show)  $(deriveFromJSON (jsonOpts "listRsp") ''ListRsp) --data UserRsp =-  UserRsp-    { userRspUser :: User-    }-  deriving (Eq, Generic, Show)+data UserRsp = UserRsp+  { userRspUser :: User+  }+  deriving stock (Eq, Generic, Show)  $(deriveFromJSON (jsonOpts "UserRsp") ''UserRsp) -newtype Email = Email Text deriving (Eq, Generic, Show)-instance ToForm Email where -  toForm (Email txt) = [("email", toQueryParam txt)]+newtype Email = Email Text deriving stock (Eq, Generic, Show) +instance ToForm Email where+  toForm (Email txt) = [("email", toQueryParam txt)]
+ src/Web/Slack/UsersConversations.hs view
@@ -0,0 +1,88 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeOperators #-}++-- | Client for Slack's @users.conversations@ endpoint.+-- <https://api.slack.com/methods/users.conversations>+module Web.Slack.UsersConversations+  ( UsersConversationsRequest (..),+    UsersConversationsResponse (..),+    usersConversations,+    usersConversationsAll,+  )+where++import Servant.API+import Servant.Client hiding (Response)+import Servant.Client.Core hiding (Response)+import Web.FormUrlEncoded (ToForm (..), genericToForm)+import Web.Slack.AesonUtils+import Web.Slack.Common (Cursor, TeamId, UserId)+import Web.Slack.Conversation (Conversation, ConversationType)+import Web.Slack.Internal+import Web.Slack.Pager (LoadPage, PagedRequest (..), PagedResponse (..), Response, ResponseMetadata, fetchAllBy)+import Web.Slack.Prelude++newtype ConversationTypesList = ConversationTypesList {unConversationTypesList :: NonEmpty ConversationType}+  deriving stock (Show, Eq)+  deriving newtype (NFData)++instance ToHttpApiData ConversationTypesList where+  toQueryParam (ConversationTypesList list) = intercalate "," $ toUrlPiece <$> list++-- | <https://api.slack.com/methods/users.conversations>+data UsersConversationsRequest = UsersConversationsRequest+  { cursor :: Maybe Cursor+  , excludeArchived :: Maybe Bool+  , limit :: Maybe Int+  , teamId :: Maybe TeamId+  , types :: Maybe ConversationTypesList+  -- ^ Specify some specific conversation types. Defaults on Slack's end to+  -- @public_channel@, aka 'PublicChannelType'.+  , user :: Maybe UserId+  -- ^ Look at the conversations of a specified user rather than the calling+  -- user+  }+  deriving stock (Show, Eq, Generic)+  deriving anyclass (Default)++instance ToForm UsersConversationsRequest where+  toForm = genericToForm snakeCaseFormOptions++instance PagedRequest UsersConversationsRequest where+  setCursor c req = req {cursor = c}++data UsersConversationsResponse = UsersConversationsResponse+  { channels :: [Conversation]+  , responseMetadata :: Maybe ResponseMetadata+  }+  deriving stock (Show, Eq)++instance PagedResponse UsersConversationsResponse where+  type ResponseObject UsersConversationsResponse = Conversation+  getResponseData UsersConversationsResponse {channels} = channels+  getResponseMetadata UsersConversationsResponse {responseMetadata} = responseMetadata++$(deriveFromJSON snakeCaseOptions ''UsersConversationsResponse)++type Api =+  "users.conversations"+    :> AuthProtect "token"+    :> ReqBody '[FormUrlEncoded] UsersConversationsRequest+    :> Post '[JSON] (ResponseJSON UsersConversationsResponse)++-- | Client for Slack's @users.conversations@ endpoint.+-- <https://api.slack.com/methods/users.conversations>+usersConversations :: SlackConfig -> UsersConversationsRequest -> IO (Response UsersConversationsResponse)+usersConversations slackConfig req = do+  let authR = mkSlackAuthenticateReq slackConfig+  run (usersConversations_ authR req) . slackConfigManager $ slackConfig++usersConversations_ ::+  AuthenticatedRequest (AuthProtect "token") ->+  UsersConversationsRequest ->+  ClientM (ResponseJSON UsersConversationsResponse)+usersConversations_ = client (Proxy @Api)++-- | Fetch all user conversations.+usersConversationsAll :: SlackConfig -> UsersConversationsRequest -> IO (LoadPage IO Conversation)+usersConversationsAll = fetchAllBy . usersConversations
src/Web/Slack/Util.hs view
@@ -1,85 +1,63 @@ ----------------------------------------------------------------------++----------------------------------------------------------------------+ -- | -- Module: Web.Slack.Util -- Description:--------------------------------------------------------------------------------- module Web.Slack.Util-  ( formOpts-  , jsonOpts-  , toQueryParamIfJust+  ( formOpts,+    jsonOpts,+    toQueryParamIfJust,   )-  where+where +-- FIXME: Web.Slack.Prelude+ -- aeson import Data.Aeson.TH import Data.Aeson.Types- -- base import Data.Char import Data.Maybe (maybeToList)-import GHC.Exts (fromList)- -- http-api-data-import Web.HttpApiData (toQueryParam, ToHttpApiData)-import Web.FormUrlEncoded (Form, FormOptions(FormOptions))  -- text import Data.Text (Text)-import qualified Data.Text as Text----- |------+import Data.Text qualified as Text+import GHC.Exts (fromList)+import Web.FormUrlEncoded (Form, FormOptions (FormOptions))+import Web.HttpApiData (ToHttpApiData, toQueryParam)+import Prelude -formOpts-  :: Text-  -> FormOptions+formOpts ::+  Text ->+  FormOptions formOpts prefix =   FormOptions (modifyLabel prefix) ---- |--------jsonOpts-  :: Text-  -> Options+jsonOpts ::+  Text ->+  Options jsonOpts prefix =   defaultOptions     { fieldLabelModifier = modifyLabel prefix     } ---- |--------modifyLabel-  :: Text-  -> String-  -> String+modifyLabel ::+  Text ->+  String ->+  String modifyLabel prefix =   fmap toLower     . addUnderscores     . drop (Text.length prefix) ---- |--------addUnderscores-  :: String-  -> String+addUnderscores ::+  String ->+  String addUnderscores =   camelTo2 '_'-  toQueryParamIfJust :: ToHttpApiData a => Text -> Maybe a -> Form toQueryParamIfJust key =
+ tests/JSONGolden.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE TemplateHaskell #-}++module JSONGolden (oneGoldenTestDecode, oneGoldenTestEncode) where++import Data.Aeson (eitherDecode)+import Data.Aeson.Encode.Pretty qualified as AP+import Data.ByteString.Lazy qualified as LBS+import Data.Text (stripEnd)+import Data.Text.IO qualified as T+import Language.Haskell.TH (Exp (..), Lit (..))+import Language.Haskell.TH.Syntax.Compat (makeRelativeToProject)+import Test.Hspec.Core.Spec (SpecM)+import Test.Hspec.Golden+import TestImport+import Text.Pretty.Simple (pShowNoColor)+import Type.Reflection++-- this requires the filepath hacking like this so it can be run from arbitrary+-- working directories+filename :: Text -> Text -> FilePath+filename tycon name = $(LitE . StringL <$> makeRelativeToProject "tests/golden") </> unpack tycon </> unpack name++typeName :: forall a. Typeable a => Text+typeName = pack . tyConName . typeRepTyCon $ typeRep @a++goldenTestDecode :: forall a. (FromJSON a, Show a, Typeable a) => Text -> LByteString -> Golden Text+goldenTestDecode name rawInput = do+  let output = either error id $ eitherDecode @a rawInput+      theTypeName = typeName @a+   in Golden+        { output = toStrict . pShowNoColor $ output+        , encodePretty = unpack+        , writeToFile = T.writeFile+        , -- deal with vim related EOF+          readFromFile = \fname -> stripEnd <$> T.readFile fname+        , goldenFile = filename theTypeName name ++ ".golden"+        , actualFile = Just $ filename theTypeName name ++ ".actual"+        , failFirstTime = True+        }++goldenTestEncode :: forall a. (ToJSON a, Typeable a) => Text -> a -> Golden Text+goldenTestEncode name value = do+  let output = AP.encodePretty @a value+      theTypeName = typeName @a+   in Golden+        { output = cs output+        , encodePretty = unpack+        , writeToFile = T.writeFile+        , -- deal with vim related EOF+          readFromFile = \fname -> stripEnd <$> T.readFile fname+        , goldenFile = filename theTypeName name ++ ".golden.json"+        , actualFile = Just $ filename theTypeName name ++ ".actual.json"+        , failFirstTime = True+        }++oneGoldenTestEncode :: forall a. (ToJSON a, Typeable a) => Text -> a -> SpecM () ()+oneGoldenTestEncode name value = do+  it (unpack name) $ goldenTestEncode @a name value++oneGoldenTestDecode :: forall a. (FromJSON a, Show a, Typeable a) => Text -> SpecM () ()+oneGoldenTestDecode name = do+  input <- runIO . LBS.readFile $ filename (typeName @a) name <> ".json"+  it (unpack name) $ goldenTestDecode @a name input
+ tests/TestImport.hs view
@@ -0,0 +1,18 @@+module TestImport+  ( fromJust,+    module Control.Monad.Fail,+    module Test.Hspec,+    module ClassyPrelude,+    module Data.Aeson,+    module Data.Aeson.TH,+    cs,+  )+where++import ClassyPrelude hiding (link)+import Control.Monad.Fail+import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.:?), (.=))+import Data.Aeson.TH (deriveJSON)+import Data.Maybe (fromJust)+import Data.String.Conversions (cs)+import Test.Hspec
+ tests/Web/Slack/ChatSpec.hs view
@@ -0,0 +1,11 @@+module Web.Slack.ChatSpec (spec) where++import JSONGolden+import TestImport+import Web.Slack.Chat (UpdateRsp)++spec :: Spec+spec = describe "Chat methods" do+  describe "chat.update" do+    describe "Response FromJSON" do+      mapM_ (oneGoldenTestDecode @UpdateRsp) ["sample", "actual"]
tests/Web/Slack/ConversationSpec.hs view
@@ -4,36 +4,22 @@ {-# OPTIONS_GHC -fno-warn-orphans #-}  module Web.Slack.ConversationSpec-  ( spec-  ) where+  ( spec,+  )+where  -- aeson import Data.Aeson---- base-import Data.Maybe---- hspec-import Test.Hspec+-- time+import Data.Time.Clock.POSIX+import JSONGolden import Test.Hspec.QuickCheck---- QuickCheck import Test.QuickCheck---- quickcheck-instances import Test.QuickCheck.Instances ()---- slack-web+import TestImport import Web.Slack.Common import Web.Slack.Conversation --- text-import Data.Text (Text)---- time-import Data.Time.Clock.POSIX-- instance Arbitrary MessageType where   arbitrary = return MessageTypeMessage @@ -55,71 +41,97 @@   arbitrary = Purpose <$> arbitrary <*> arbitrary <*> arbitrary  instance Arbitrary ChannelConversation where-  arbitrary = ChannelConversation-    <$> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary+  arbitrary =+    ChannelConversation+      <$> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary  instance Arbitrary GroupConversation where-  arbitrary = GroupConversation-    <$> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary+  arbitrary =+    GroupConversation+      <$> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary  instance Arbitrary ImConversation where-  arbitrary = ImConversation-    <$> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary-    <*> arbitrary+  arbitrary =+    ImConversation+      <$> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary+      <*> arbitrary  instance Arbitrary Conversation where   arbitrary = oneof [Channel <$> arbitrary, Group <$> arbitrary, Im <$> arbitrary] +deriving newtype instance Arbitrary ConversationId -deriving instance Arbitrary ConversationId-deriving instance Arbitrary SlackMessageText-deriving instance Arbitrary TeamId+deriving newtype instance Arbitrary SlackMessageText +deriving newtype instance Arbitrary TeamId  spec :: Spec spec = describe "ToJSON and FromJSON for Conversation" $ do-    prop "the encoded json is decoded as " $ \conversation -> do-      actual <- either fail return . eitherDecode $ encode conversation-      actual `shouldBe` (conversation :: Conversation)+  prop "the encoded json is decoded as " $ \conversation -> do+    actual <- either fail return . eitherDecode $ encode conversation+    actual `shouldBe` (conversation :: Conversation)++  describe "Golden tests" $ do+    mapM_ (oneGoldenTestDecode @Conversation) ["shared_channel"]++  it "errors accurately if no variant matches" $ do+    let badData =+          object+            [ ("is_group", Bool False)+            , ("is_im", Bool False)+            , ("is_channel", Bool False)+            ]+    fromJSON @Conversation badData+      `shouldBe` Error "parsing a Conversation failed: neither channel, group, nor im: expected Conversation, but encountered Object"++  it "has good errors if a variant matches but is missing fields" $ do+    let badData =+          object+            [ ("is_group", Bool False)+            , ("is_im", Bool False)+            , ("is_channel", Bool True)+            ]+    fromJSON @Conversation badData+      `shouldBe` Error "When parsing the record ChannelConversation of type Web.Slack.Conversation.ChannelConversation the key id was not present."
+ tests/Web/Slack/Experimental/BlocksSpec.hs view
@@ -0,0 +1,89 @@+module Web.Slack.Experimental.BlocksSpec where++import Control.Monad.Writer.Strict+import Data.StringVariants (mkNonEmptyText)+import JSONGolden (oneGoldenTestEncode)+import TestImport+import Web.Slack.Experimental.Blocks+import Web.Slack.Experimental.Blocks.Types++-- FIXME(jadel, violet): These builder things should probably be put into a+-- module to make them available, but that's later work++headerBlock :: SlackText -> WriterT [SlackBlock] Identity ()+headerBlock = tell . pure . SlackBlockHeader . SlackPlainTextOnly++sectionBlock :: SlackText -> WriterT [SlackBlock] Identity ()+sectionBlock text = tell . pure $ SlackBlockSection text Nothing++sectionBlockWithAccessory ::+  SlackText ->+  SlackAction ->+  WriterT [SlackBlock] Identity ()+sectionBlockWithAccessory a b = tell . pure $ SlackBlockSection a (Just . SlackButtonAccessory $ b)++dividerBlock :: WriterT [SlackBlock] Identity ()+dividerBlock = tell . pure $ SlackBlockDivider++contextBlock :: [SlackContent] -> WriterT [SlackBlock] Identity ()+contextBlock = tell . pure . SlackBlockContext . SlackContext++actionsBlock ::+  Maybe SlackBlockId ->+  SlackActionList ->+  WriterT [SlackBlock] Identity ()+actionsBlock a b = tell . pure $ SlackBlockActions a b++slackActionDoNothing :: SlackActionId+slackActionDoNothing = SlackActionId . fromJust . mkNonEmptyText $ "doNothing"++slackActionDoNothing2 :: SlackActionId+slackActionDoNothing2 = SlackActionId . fromJust . mkNonEmptyText $ "doNothing2"++slackActionDoNothing3 :: SlackActionId+slackActionDoNothing3 = SlackActionId . fromJust . mkNonEmptyText $ "doNothing3"++slackShowBlockFormat :: BlockKitBuilderMessage+slackShowBlockFormat =+  BlockKitBuilderMessage $+    execWriter $ do+      headerBlock ("Blah: " <> list ["a", "b", "c"])+      dividerBlock+      sectionBlock (bold $ list ["blah", "blah2", "blah3"])+      sectionBlockWithAccessory+        (monospaced @Text "blah")+        (button slackActionDoNothing ":mag: Look at it" buttonSettings {buttonUrl = google})+      sectionBlockWithAccessory+        (bold $ list ["blah", "blah2", "blah3"])+        (button slackActionDoNothing ":office: Look at it but different" buttonSettings {buttonUrl = google})+      sectionBlock (bold (textMessage "Letters:") <> newline (list ["a", "b", "c"]))+      sectionBlock (list ["blah1", "blah2", "blah3"])+      dividerBlock+      sectionBlock (bold . textMessage $ "blah")+      sectionBlockWithAccessory+        (bold . textMessage $ "blah")+        (button slackActionDoNothing ":bank: Look at it!" buttonSettings {buttonUrl = google})+      contextBlock [SlackContentText ":key: Context!"]+      dividerBlock+      actionsBlock Nothing $+        toSlackActionList+          ( button slackActionDoNothing ":mag: View" buttonSettings {buttonUrl = google}+          , button slackActionDoNothing2 ":office: View" buttonSettings {buttonUrl = google}+          , button slackActionDoNothing3 ":bank: View" buttonSettings {buttonUrl = google}+          )+  where+    textMessage = message @Text+    list x = textMessage (intercalate ", " x)+    google = OptionalSetting . mkNonEmptyText @3000 $ "https://google.com"++-- | This is used so that we can paste the output of these tests directly into+-- the block kit builder:+-- <https://app.slack.com/block-kit-builder>+newtype BlockKitBuilderMessage = BlockKitBuilderMessage {blocks :: [SlackBlock]}++instance ToJSON BlockKitBuilderMessage where+  toJSON BlockKitBuilderMessage {blocks} = object [("blocks", toJSON blocks)]++spec :: Spec+spec = describe "Slack block builder" do+  oneGoldenTestEncode "simple_blocks" slackShowBlockFormat
+ tests/Web/Slack/Experimental/Events/TypesSpec.hs view
@@ -0,0 +1,23 @@+module Web.Slack.Experimental.Events.TypesSpec (spec) where++import JSONGolden+import TestImport+import Web.Slack.Experimental.Events.Types++spec :: Spec+spec = describe "Types for Slack events" do+  describe "SlackWebhookEvent" do+    describe "FromJSON" do+      mapM_+        (oneGoldenTestDecode @SlackWebhookEvent)+        [ "messageExample"+        , "messageChange"+        , "link"+        , "botMessage"+        , "joinChannel"+        , "createChannel"+        , "messageIm"+        , "slackbotIm"+        , "channel_left"+        , "share_without_message"+        ]
+ tests/Web/Slack/Experimental/RequestVerificationSpec.hs view
@@ -0,0 +1,50 @@+module Web.Slack.Experimental.RequestVerificationSpec where++import Data.Aeson+import Data.Aeson.KeyMap qualified as KeyMap+import Data.Time.Clock.POSIX+import TestImport+import Web.Slack.Experimental.RequestVerification++spec :: Spec+spec = do+  let timestamp = SlackRequestTimestamp "1665615887" -- Oct 12 2022 23:04:47 GMT+0000+  let sig = SlackSignature "v0=f7f0a2f48ae5fe2c923813af0a7c7636396fea6e03edb3d73580a99cd1913575"+  let slackSigningSecret = SlackSigningSecret "SIGNING_SECRET"+      -- matches the test timestamp within one minute+      goodUtcTime = utcTimeToPOSIXSeconds $ UTCTime (fromGregorian 2022 10 12) (23 * 60 * 60 + 5 * 60)+      -- back to the future+      someoneGotATimeMachineUtcTime = utcTimeToPOSIXSeconds $ UTCTime (fromGregorian 2020 1 1) 0+      oldUtcTime = utcTimeToPOSIXSeconds $ UTCTime (fromGregorian 2025 10 12) (23 * 60 * 60 + 5 * 60)++      -- the simplest possible JSON; this thing just parses it straight through+      -- anyway+      requestBody = "{}"++  describe "Slack Webhook" $ do+    it "Correctly validates webhooks" $ do+      validateRequest' goodUtcTime slackSigningSecret sig timestamp requestBody+        `shouldBe` (Right $ Object KeyMap.empty)++    it "Handles incorrect timestamp" $ do+      validateRequest' @Value goodUtcTime slackSigningSecret sig (SlackRequestTimestamp "bad") requestBody+        `shouldBe` Left (VerificationMalformedTimestamp "bad")++    it "Handles invalid signature" $ do+      validateRequest' @Value goodUtcTime slackSigningSecret (SlackSignature "the_v0_prefix_missing") timestamp requestBody+        `shouldBe` Left (VerificationUnknownSignatureVersion "the_v0_prefix_missing")++    it "Handles signature mismatch" $ do+      let badSignature = SlackSignature "v0=bad0bad0bad0bad0bad0bad0bad0bad0bad0bad0bad0bad0bad0bad0bad0bad0"+      validateRequest' @Value goodUtcTime slackSigningSecret badSignature timestamp requestBody+        `shouldBe` Left VerificationSignatureMismatch++    it "Handles timestamp in the future" $ do+      -- it's "in 2020" and someone gave us a 2022 timestamp+      validateRequest' @Value someoneGotATimeMachineUtcTime slackSigningSecret sig timestamp requestBody+        `shouldBe` Left (VerificationTimestampOutOfRange 1665615887)++    it "Handles timestamp in the past" $ do+      -- it's "in 2025" and someone gave us a 2022 timestamp+      validateRequest' @Value oldUtcTime slackSigningSecret sig timestamp requestBody+        `shouldBe` (Left (VerificationTimestampOutOfRange 1665615887))
tests/Web/Slack/MessageParserSpec.hs view
@@ -1,25 +1,18 @@-{-# LANGUAGE OverloadedStrings #-}- module Web.Slack.MessageParserSpec (spec) where --- hspec-import Test.Hspec---- slack-web+import TestImport import Web.Slack.MessageParser import Web.Slack.Types --- text-import Data.Text (Text)- testGetUserDesc :: UserId -> Text testGetUserDesc (UserId "USER1") = "user_one" testGetUserDesc x = unUserId x  testHtmlRenderers :: HtmlRenderers-testHtmlRenderers = HtmlRenderers-  { emoticonRenderer = \x -> ":>" <> x <> "<:"-  }+testHtmlRenderers =+  HtmlRenderers+    { emoticonRenderer = \x -> ":>" <> x <> "<:"+    }  msgToHtml :: Text -> Text msgToHtml = messageToHtml testHtmlRenderers testGetUserDesc . SlackMessageText@@ -31,12 +24,10 @@       msgToHtml "hello" `shouldBe` "hello"     it "converts a simple message to HTML correctly" $       msgToHtml "_hello_ *world* <http://www.google.com|google> `code` ```longer\ncode```"-        `shouldBe`-        "<i>hello</i> <b>world</b> <a href='http://www.google.com'>google</a> <code>code</code> <pre>longer\ncode</pre>"+        `shouldBe` "<i>hello</i> <b>world</b> <a href='http://www.google.com'>google</a> <code>code</code> <pre>longer\ncode</pre>"     it "degrades properly to return the input message if it's incorrect" $       msgToHtml "link not closed <bad"-        `shouldBe`-        "link not closed <bad"+        `shouldBe` "link not closed <bad"     it "creates italics sections only at word boundaries" $       msgToHtml "false_positive" `shouldBe` "false_positive"     it "handles bold, strikethrough & italics simultaneously" $@@ -54,12 +45,10 @@       msgToHtml "a:\n&gt;b.\n:slightly_smiling_face:" `shouldBe` "a:<br/><blockquote>b.</blockquote>:>slightly_smiling_face<:"     it "properly parses multiline blockquotes" $       msgToHtml "&gt; first row\n&gt; second row\nthird row\n&gt; fourth row"-        `shouldBe`-        "<blockquote>first row<br/>second row</blockquote>third row<br/><blockquote>fourth row</blockquote>"+        `shouldBe` "<blockquote>first row<br/>second row</blockquote>third row<br/><blockquote>fourth row</blockquote>"     it "converts usernames" $       msgToHtml "<@USER1> should be converted, <@USER1|default> stay default"-        `shouldBe`-        "@user_one should be converted, @default stay default"+        `shouldBe` "@user_one should be converted, @default stay default"     it "converts carriage returns" $       msgToHtml "a\nb" `shouldBe` "a<br/>b"     it "handles full stops as punctuation" $
tests/Web/Slack/PagerSpec.hs view
@@ -1,48 +1,30 @@-{-# LANGUAGE OverloadedStrings #-}- module Web.Slack.PagerSpec (spec) where --- base-import Control.Exception (throwIO)-import Data.Maybe (fromJust)---- fakepull-import Test.Pull.Fake.IO (FakeStream, pull, newFakeStream)---- hspec-import Test.Hspec---- slack-web-import Web.Slack.Pager+import Data.Text qualified as Text+-- time+import Data.Time.Clock (addUTCTime, nominalDay)+import Test.Pull.Fake.IO (FakeStream, newFakeStream, pull)+import TestImport import Web.Slack.Common-  ( mkSlackTimestamp-  , Cursor (..)-  , Message (..)-  , MessageType (..)-  , SlackMessageText (..)+  ( Message (..),+    MessageType (..),+    SlackMessageText (..),+    mkSlackTimestamp,   ) import Web.Slack.Conversation-  ( ConversationId (ConversationId)-  , mkHistoryReq-  , mkRepliesReq-  , HistoryReq (..)-  , HistoryRsp (..)-  , RepliesReq (..)-  , ResponseMetadata (..)+  ( ConversationId (ConversationId),+    HistoryReq (..),+    HistoryRsp (..),+    RepliesReq (..),+    mkHistoryReq,+    mkRepliesReq,   )+import Web.Slack.Pager import Web.Slack.Types (UserId (..)) --- text-import qualified Data.Text as Text---- time-import Data.Time.Clock (getCurrentTime, nominalDay, addUTCTime)-- stubbedSendRequest :: FakeStream (Response HistoryRsp) -> a -> IO (Response HistoryRsp) stubbedSendRequest stream _request = fromJust <$> pull stream - spec :: Spec spec = do   let prepare = do@@ -54,35 +36,38 @@             allResponses = do               -- According to https://api.slack.com/docs/pagination,               -- The last page's cursor can be either an empty string, null, or non-exisitent in the object.-              (pageN, cursor) <- zip [1..3] ["cursor1=", "cursor2=", ""]+              (pageN, cursor) <- zip [1 .. 3] ["cursor1=", "cursor2=", ""]               let pageNT = Text.pack (show pageN)-              pure . Right $ HistoryRsp-                { historyRspMessages = do-                    messageN <- [1..messagesPerPage]-                    let messagesPerPageNDT = fromIntegral messagesPerPage-                        messageNDT = fromIntegral messageN-                        messageNT = Text.pack (show messageN)-                        createdBefore = negate $ nominalDay * ((pageN - 1) * messagesPerPageNDT + messageNDT)-                    pure $ Message-                        MessageTypeMessage-                        (Just . UserId $ "U" <> pageNT <> messageNT)-                        (SlackMessageText $ "message " <> pageNT <> "-" <> messageNT)-                        (mkSlackTimestamp $ addUTCTime createdBefore nowUtc)-                , historyRspResponseMetadata = Just . ResponseMetadata . Just $ Cursor cursor-                }+              pure . Right $+                HistoryRsp+                  { historyRspMessages = do+                      messageN <- [1 .. messagesPerPage]+                      let messagesPerPageNDT = fromIntegral messagesPerPage+                          messageNDT = fromIntegral messageN+                          messageNT = Text.pack (show messageN)+                          createdBefore = negate $ nominalDay * ((pageN - 1) * messagesPerPageNDT + messageNDT)+                      pure $+                        Message+                          MessageTypeMessage+                          (Just . UserId $ "U" <> pageNT <> messageNT)+                          (SlackMessageText $ "message " <> pageNT <> "-" <> messageNT)+                          (mkSlackTimestamp $ addUTCTime createdBefore nowUtc)+                  , historyRspResponseMetadata = Just . ResponseMetadata . Just $ Cursor cursor+                  }         responsesToReturn <- newFakeStream allResponses         return (now, oldest, messagesPerPage, allResponses, responsesToReturn)    describe "conversationsHistoryAllBy" $     it "collect all results by sending requests" $ do       (now, oldest, messagesPerPage, allResponses, responsesToReturn) <- prepare-      let initialRequest = (mkHistoryReq (ConversationId "C01234567"))-            { historyReqCount = messagesPerPage-            , historyReqLatest = Just now-            , historyReqOldest = Just oldest-            , historyReqInclusive = False-            }-      loadPage <- conversationsHistoryAllBy (stubbedSendRequest responsesToReturn) initialRequest+      let initialRequest =+            (mkHistoryReq (ConversationId "C01234567"))+              { historyReqCount = messagesPerPage+              , historyReqLatest = Just now+              , historyReqOldest = Just oldest+              , historyReqInclusive = False+              }+      loadPage <- fetchAllBy (stubbedSendRequest responsesToReturn) initialRequest       let actual = unfoldPageM $ either throwIO return =<< loadPage       expected <- fmap (map historyRspMessages) . either throwIO return $ sequenceA allResponses       actual `shouldReturn` expected@@ -90,24 +75,24 @@   describe "repliesFetchAllBy" $     it "collect all results by sending requests" $ do       (now, oldest, messagesPerPage, allResponses, responsesToReturn) <- prepare-      let initialRequest = (mkRepliesReq (ConversationId "C98765432") oldest)-            { repliesReqLimit = messagesPerPage-            , repliesReqLatest = Just now-            , repliesReqOldest = Just oldest-            , repliesReqInclusive = False-            }-      loadPage <- repliesFetchAllBy (stubbedSendRequest responsesToReturn) initialRequest+      let initialRequest =+            (mkRepliesReq (ConversationId "C98765432") oldest)+              { repliesReqLimit = messagesPerPage+              , repliesReqLatest = Just now+              , repliesReqOldest = Just oldest+              , repliesReqInclusive = False+              }+      loadPage <- fetchAllBy (stubbedSendRequest responsesToReturn) initialRequest       let actual = unfoldPageM $ either throwIO return =<< loadPage       expected <- fmap (map historyRspMessages) . either throwIO return $ sequenceA allResponses       actual `shouldReturn` expected - -- | Runs the given action repeatedly until it returns an empty list. unfoldPageM :: Monad m => m [a] -> m [[a]] unfoldPageM act = reverse <$> go []- where-  go accum = do-    x <- act-    case x of+  where+    go accum = do+      x <- act+      case x of         [] -> return accum         xs -> go $! xs : accum
+ tests/Web/Slack/UsersConversationsSpec.hs view
@@ -0,0 +1,10 @@+module Web.Slack.UsersConversationsSpec (spec) where++import JSONGolden+import TestImport+import Web.Slack.UsersConversations (UsersConversationsResponse)++spec :: Spec+spec = describe "User conversations method" do+  describe "Response FromJSON" do+    mapM_ (oneGoldenTestDecode @UsersConversationsResponse) ["im_and_channels"]
+ tests/golden/BlockKitBuilderMessage/simple_blocks.golden.json view
@@ -0,0 +1,137 @@+{+    "blocks": [+        {+            "text": {+                "text": "Blah: \na, b, c",+                "type": "plain_text"+            },+            "type": "header"+        },+        {+            "type": "divider"+        },+        {+            "text": {+                "text": "*blah, blah2, blah3*",+                "type": "mrkdwn"+            },+            "type": "section"+        },+        {+            "accessory": {+                "action_id": "doNothing",+                "text": {+                    "text": ":mag: Look at it",+                    "type": "plain_text"+                },+                "type": "button",+                "url": "https://google.com"+            },+            "text": {+                "text": "`blah`",+                "type": "mrkdwn"+            },+            "type": "section"+        },+        {+            "accessory": {+                "action_id": "doNothing",+                "text": {+                    "text": ":office: Look at it but different",+                    "type": "plain_text"+                },+                "type": "button",+                "url": "https://google.com"+            },+            "text": {+                "text": "*blah, blah2, blah3*",+                "type": "mrkdwn"+            },+            "type": "section"+        },+        {+            "text": {+                "text": "*Letters:*\na, b, c",+                "type": "mrkdwn"+            },+            "type": "section"+        },+        {+            "text": {+                "text": "blah1, blah2, blah3",+                "type": "mrkdwn"+            },+            "type": "section"+        },+        {+            "type": "divider"+        },+        {+            "text": {+                "text": "*blah*",+                "type": "mrkdwn"+            },+            "type": "section"+        },+        {+            "accessory": {+                "action_id": "doNothing",+                "text": {+                    "text": ":bank: Look at it!",+                    "type": "plain_text"+                },+                "type": "button",+                "url": "https://google.com"+            },+            "text": {+                "text": "*blah*",+                "type": "mrkdwn"+            },+            "type": "section"+        },+        {+            "elements": [+                {+                    "text": ":key: Context!",+                    "type": "mrkdwn"+                }+            ],+            "type": "context"+        },+        {+            "type": "divider"+        },+        {+            "elements": [+                {+                    "action_id": "doNothing",+                    "text": {+                        "text": ":mag: View",+                        "type": "plain_text"+                    },+                    "type": "button",+                    "url": "https://google.com"+                },+                {+                    "action_id": "doNothing2",+                    "text": {+                        "text": ":office: View",+                        "type": "plain_text"+                    },+                    "type": "button",+                    "url": "https://google.com"+                },+                {+                    "action_id": "doNothing3",+                    "text": {+                        "text": ":bank: View",+                        "type": "plain_text"+                    },+                    "type": "button",+                    "url": "https://google.com"+                }+            ],+            "type": "actions"+        }+    ]+}
+ tests/golden/Conversation/shared_channel.golden view
@@ -0,0 +1,32 @@+Channel+    ( ChannelConversation+        { channelId = ConversationId+            { unConversationId = "C0000000000" }+        , channelName = "shared-channel"+        , channelCreated = 1667519517+        , channelIsArchived = False+        , channelIsGeneral = False+        , channelUnlinked = 0+        , channelNameNormalized = "shared-channel"+        , channelIsShared = True+        , channelCreator = UserId+            { unUserId = "UAAAAAAAA" }+        , channelIsExtShared = True+        , channelIsOrgShared = False+        , channelSharedTeamIds = Nothing+        , channelIsPendingExtShared = False+        , channelIsMember = Just False+        , channelTopic = Topic+            { topicValue = ""+            , topicCreator = ""+            , topicLastSet = 0+            }+        , channelPurpose = Purpose+            { purposeValue = ""+            , purposeCreator = ""+            , purposeLastSet = 0+            }+        , channelPreviousNames = []+        , channelNumMembers = Just 12+        }+    )
+ tests/golden/Conversation/shared_channel.json view
@@ -0,0 +1,37 @@+{+  "id": "C0000000000",+  "name": "shared-channel",+  "is_channel": true,+  "is_group": false,+  "is_im": false,+  "is_mpim": false,+  "is_private": false,+  "created": 1667519517,+  "is_archived": false,+  "is_general": false,+  "unlinked": 0,+  "name_normalized": "shared-channel",+  "is_shared": true,+  "is_org_shared": false,+  "is_pending_ext_shared": false,+  "pending_shared": [],+  "context_team_id": "TAAAAAAAA",+  "parent_conversation": null,+  "creator": "UAAAAAAAA",+  "is_ext_shared": true,+  "pending_connected_team_ids": [],+  "conversation_host_id": "TAAAAAAAA",+  "is_member": false,+  "topic": {+    "value": "",+    "creator": "",+    "last_set": 0+  },+  "purpose": {+    "value": "",+    "creator": "",+    "last_set": 0+  },+  "previous_names": [],+  "num_members": 12+}
+ tests/golden/SlackWebhookEvent/botMessage.golden view
@@ -0,0 +1,42 @@+EventEventCallback+    ( EventCallback+        { eventId = EventId+            { unEventId = "Ev043VFLT545" }+        , teamId = TeamId+            { unTeamId = "T043DB835ML" }+        , eventTime = MkSystemTime+            { systemSeconds = 1664216870+            , systemNanoseconds = 0+            }+        , event = EventMessage+            ( MessageEvent+                { blocks = Just+                    [ RichText+                        { blockId = Just+                            ( NonEmptyText "FVI" )+                        , elements =+                            [ RichTextSectionItemRichText+                                [ RichItemText "TEST"+                                    ( RichStyle+                                        { rsBold = False+                                        , rsItalic = False+                                        }+                                    )+                                ]+                            ]+                        }+                    ]+                , channel = ConversationId+                    { unConversationId = "C043YJGBY49" }+                , text = "TEST"+                , channelType = Channel+                , user = UserId+                    { unUserId = "U0442US8QGH" }+                , ts = "1664216870.477049"+                , threadTs = Just "1664216109.798919"+                , appId = Just "A0442TUPHGR"+                , botId = Just "B0439P161B9"+                }+            )+        }+    )
+ tests/golden/SlackWebhookEvent/botMessage.json view
@@ -0,0 +1,63 @@+{+  "token": "aaaa",+  "team_id": "T043DB835ML",+  "api_app_id": "A0442TUPHGR",+  "event": {+    "bot_id": "B0439P161B9",+    "type": "message",+    "text": "TEST",+    "user": "U0442US8QGH",+    "ts": "1664216870.477049",+    "app_id": "A0442TUPHGR",+    "team": "T043DB835ML",+    "bot_profile": {+      "id": "B0439P161B9",+      "deleted": false,+      "name": "Slacklinker dev",+      "updated": 1663960005,+      "app_id": "A0442TUPHGR",+      "icons": {+        "image_36": "https://a.slack-edge.com/80588/img/plugins/app/bot_36.png",+        "image_48": "https://a.slack-edge.com/80588/img/plugins/app/bot_48.png",+        "image_72": "https://a.slack-edge.com/80588/img/plugins/app/service_72.png"+      },+      "team_id": "T043DB835ML"+    },+    "blocks": [+      {+        "type": "rich_text",+        "block_id": "FVI",+        "elements": [+          {+            "type": "rich_text_section",+            "elements": [+              {+                "type": "text",+                "text": "TEST"+              }+            ]+          }+        ]+      }+    ],+    "thread_ts": "1664216109.798919",+    "parent_user_id": "U043H11ES4V",+    "channel": "C043YJGBY49",+    "event_ts": "1664216870.477049",+    "channel_type": "channel"+  },+  "type": "event_callback",+  "event_id": "Ev043VFLT545",+  "event_time": 1664216870,+  "authorizations": [+    {+      "enterprise_id": null,+      "team_id": "T043DB835ML",+      "user_id": "U0442US8QGH",+      "is_bot": true,+      "is_enterprise_install": false+    }+  ],+  "is_ext_shared_channel": false,+  "event_context": "aaaa"+}
+ tests/golden/SlackWebhookEvent/channel_left.golden view
@@ -0,0 +1,21 @@+EventEventCallback+    ( EventCallback+        { eventId = EventId+            { unEventId = "Ev044RKJHB8W" }+        , teamId = TeamId+            { unTeamId = "T043DB835ML" }+        , eventTime = MkSystemTime+            { systemSeconds = 1664827321+            , systemNanoseconds = 0+            }+        , event = EventChannelLeft+            ( ChannelLeftEvent+                { actorId = UserId+                    { unUserId = "U043H11ES4V" }+                , channel = ConversationId+                    { unConversationId = "C043KSKGJUB" }+                , eventTs = "1664827321.000300"+                }+            )+        }+    )
+ tests/golden/SlackWebhookEvent/channel_left.json view
@@ -0,0 +1,24 @@+{+  "token": "aaaa",+  "team_id": "T043DB835ML",+  "api_app_id": "A0442TUPHGR",+  "event": {+    "type": "channel_left",+    "channel": "C043KSKGJUB",+    "actor_id": "U043H11ES4V",+    "event_ts": "1664827321.000300"+  },+  "type": "event_callback",+  "event_id": "Ev044RKJHB8W",+  "event_time": 1664827321,+  "authorizations": [+    {+      "enterprise_id": null,+      "team_id": "T043DB835ML",+      "user_id": "U0442US8QGH",+      "is_bot": true,+      "is_enterprise_install": false+    }+  ],+  "is_ext_shared_channel": false+}
+ tests/golden/SlackWebhookEvent/createChannel.golden view
@@ -0,0 +1,33 @@+EventEventCallback+    ( EventCallback+        { eventId = EventId+            { unEventId = "Ev0445DETPAA" }+        , teamId = TeamId+            { unTeamId = "T043DB835ML" }+        , eventTime = MkSystemTime+            { systemSeconds = 1664404606+            , systemNanoseconds = 0+            }+        , event = EventChannelCreated+            ( ChannelCreatedEvent+                { channel = CreatedChannel+                    { id = ConversationId+                        { unConversationId = "C04493BRXEZ" }+                    , isChannel = True+                    , name = "new-channel"+                    , nameNormalized = "new-channel"+                    , creator = UserId+                        { unUserId = "U043H11ES4V" }+                    , created = MkSystemTime+                        { systemSeconds = 1664404606+                        , systemNanoseconds = 0+                        }+                    , isShared = False+                    , isOrgShared = False+                    , contextTeamId = TeamId+                        { unTeamId = "T043DB835ML" }+                    }+                }+            )+        }+    )
+ tests/golden/SlackWebhookEvent/createChannel.json view
@@ -0,0 +1,33 @@+{+  "token": "aaaa",+  "team_id": "T043DB835ML",+  "api_app_id": "A0442TUPHGR",+  "event": {+    "type": "channel_created",+    "channel": {+      "id": "C04493BRXEZ",+      "is_channel": true,+      "name": "new-channel",+      "name_normalized": "new-channel",+      "created": 1664404606,+      "creator": "U043H11ES4V",+      "is_shared": false,+      "is_org_shared": false,+      "context_team_id": "T043DB835ML"+    },+    "event_ts": "1664404606.000700"+  },+  "type": "event_callback",+  "event_id": "Ev0445DETPAA",+  "event_time": 1664404606,+  "authorizations": [+    {+      "enterprise_id": null,+      "team_id": "T043DB835ML",+      "user_id": "U0442US8QGH",+      "is_bot": true,+      "is_enterprise_install": false+    }+  ],+  "is_ext_shared_channel": false+}
+ tests/golden/SlackWebhookEvent/joinChannel.golden view
@@ -0,0 +1,13 @@+EventEventCallback+    ( EventCallback+        { eventId = EventId+            { unEventId = "Ev043W120WSH" }+        , teamId = TeamId+            { unTeamId = "T043DB835ML" }+        , eventTime = MkSystemTime+            { systemSeconds = 1664222685+            , systemNanoseconds = 0+            }+        , event = EventChannelJoinMessage+        }+    )
+ tests/golden/SlackWebhookEvent/joinChannel.json view
@@ -0,0 +1,30 @@+{+  "token": "aaaa",+  "team_id": "T043DB835ML",+  "api_app_id": "A0442TUPHGR",+  "event": {+    "type": "message",+    "subtype": "channel_join",+    "ts": "1664222685.728809",+    "user": "U0442US8QGH",+    "text": "<@U0442US8QGH> has joined the channel",+    "inviter": "U043H11ES4V",+    "channel": "C043KSKGJUB",+    "event_ts": "1664222685.728809",+    "channel_type": "channel"+  },+  "type": "event_callback",+  "event_id": "Ev043W120WSH",+  "event_time": 1664222685,+  "authorizations": [+    {+      "enterprise_id": null,+      "team_id": "T043DB835ML",+      "user_id": "U0442US8QGH",+      "is_bot": true,+      "is_enterprise_install": false+    }+  ],+  "is_ext_shared_channel": false,+  "event_context": "aaaa"+}
+ tests/golden/SlackWebhookEvent/link.golden view
@@ -0,0 +1,46 @@+EventEventCallback+    ( EventCallback+        { eventId = EventId+            { unEventId = "Ev043R67DQ1H" }+        , teamId = TeamId+            { unTeamId = "T043DB835ML" }+        , eventTime = MkSystemTime+            { systemSeconds = 1663978925+            , systemNanoseconds = 0+            }+        , event = EventMessage+            ( MessageEvent+                { blocks = Just+                    [ RichText+                        { blockId = Just+                            ( NonEmptyText "MEdM" )+                        , elements =+                            [ RichTextSectionItemRichText+                                [ RichItemLink+                                    ( RichLinkAttrs+                                        { style = RichStyle+                                            { rsBold = False+                                            , rsItalic = False+                                            }+                                        , url = "https://jadeapptesting.slack.com/archives/C043YJGBY49/p1663961604007869"+                                        , text = Nothing+                                        }+                                    )+                                ]+                            ]+                        }+                    ]+                , channel = ConversationId+                    { unConversationId = "C043YJGBY49" }+                , text = "<https://jadeapptesting.slack.com/archives/C043YJGBY49/p1663961604007869>"+                , channelType = Channel+                , user = UserId+                    { unUserId = "U043H11ES4V" }+                , ts = "1663978925.099999"+                , threadTs = Nothing+                , appId = Nothing+                , botId = Nothing+                }+            )+        }+    )
+ tests/golden/SlackWebhookEvent/link.json view
@@ -0,0 +1,47 @@+{+  "token": "aaaa",+  "team_id": "T043DB835ML",+  "api_app_id": "A0442TUPHGR",+  "event": {+    "client_msg_id": "ade2a33a-3aa8-4fcf-9b93-2e44e4a92899",+    "type": "message",+    "text": "<https://jadeapptesting.slack.com/archives/C043YJGBY49/p1663961604007869>",+    "user": "U043H11ES4V",+    "ts": "1663978925.099999",+    "team": "T043DB835ML",+    "blocks": [+      {+        "type": "rich_text",+        "block_id": "MEdM",+        "elements": [+          {+            "type": "rich_text_section",+            "elements": [+              {+                "type": "link",+                "url": "https://jadeapptesting.slack.com/archives/C043YJGBY49/p1663961604007869"+              }+            ]+          }+        ]+      }+    ],+    "channel": "C043YJGBY49",+    "event_ts": "1663978925.099999",+    "channel_type": "channel"+  },+  "type": "event_callback",+  "event_id": "Ev043R67DQ1H",+  "event_time": 1663978925,+  "authorizations": [+    {+      "enterprise_id": null,+      "team_id": "T043DB835ML",+      "user_id": "U0442US8QGH",+      "is_bot": true,+      "is_enterprise_install": false+    }+  ],+  "is_ext_shared_channel": false,+  "event_context": "aaaa"+}
+ tests/golden/SlackWebhookEvent/messageChange.golden view
@@ -0,0 +1,13 @@+EventEventCallback+    ( EventCallback+        { eventId = EventId+            { unEventId = "Ev043QRVT9FD" }+        , teamId = TeamId+            { unTeamId = "T043DB835ML" }+        , eventTime = MkSystemTime+            { systemSeconds = 1663969334+            , systemNanoseconds = 0+            }+        , event = EventMessageChanged+        }+    )
+ tests/golden/SlackWebhookEvent/messageChange.json view
@@ -0,0 +1,127 @@+{+  "token": "aaaa",+  "team_id": "T043DB835ML",+  "api_app_id": "A0442TUPHGR",+  "event": {+    "type": "message",+    "subtype": "message_changed",+    "message": {+      "client_msg_id": "de349a9f-62a4-4293-b64d-701a1f566e8a",+      "type": "message",+      "text": "<https://jadeapptesting.slack.com/archives/C043YJGBY49/p1663966292304369>",+      "user": "U043H11ES4V",+      "team": "T043DB835ML",+      "attachments": [+        {+          "from_url": "https://jadeapptesting.slack.com/archives/C043YJGBY49/p1663966292304369",+          "ts": "1663966292.304369",+          "author_id": "U043H11ES4V",+          "channel_team": "T043DB835ML",+          "channel_id": "C043YJGBY49",+          "channel_name": "general",+          "is_msg_unfurl": true,+          "message_blocks": [+            {+              "team": "T043DB835ML",+              "channel": "C043YJGBY49",+              "ts": "1663966292.304369",+              "message": {+                "blocks": [+                  {+                    "type": "rich_text",+                    "block_id": "hn8/C",+                    "elements": [+                      {+                        "type": "rich_text_section",+                        "elements": [+                          {+                            "type": "text",+                            "text": "asdfhlkfasdlhsdf"+                          }+                        ]+                      }+                    ]+                  }+                ]+              }+            }+          ],+          "id": 1,+          "original_url": "https://jadeapptesting.slack.com/archives/C043YJGBY49/p1663966292304369",+          "fallback": "[September 23rd, 2022 1:51 PM] jadel: asdfhlkfasdlhsdf",+          "text": "asdfhlkfasdlhsdf",+          "author_name": "jadel",+          "author_link": "https://jadeapptesting.slack.com/team/U043H11ES4V",+          "author_icon": "https://secure.gravatar.com/avatar/dcd5bc53dcfaca62ddc3f5726d07ba13.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2Fdf10d%2Fimg%2Favatars%2Fava_0019-48.png",+          "author_subname": "jadel",+          "mrkdwn_in": [+            "text"+          ],+          "footer": "Posted in #general"+        }+      ],+      "blocks": [+        {+          "type": "rich_text",+          "block_id": "FXy3n",+          "elements": [+            {+              "type": "rich_text_section",+              "elements": [+                {+                  "type": "link",+                  "url": "https://jadeapptesting.slack.com/archives/C043YJGBY49/p1663966292304369"+                }+              ]+            }+          ]+        }+      ],+      "ts": "1663969333.524439"+    },+    "previous_message": {+      "client_msg_id": "de349a9f-62a4-4293-b64d-701a1f566e8a",+      "type": "message",+      "text": "<https://jadeapptesting.slack.com/archives/C043YJGBY49/p1663966292304369>",+      "user": "U043H11ES4V",+      "ts": "1663969333.524439",+      "team": "T043DB835ML",+      "blocks": [+        {+          "type": "rich_text",+          "block_id": "FXy3n",+          "elements": [+            {+              "type": "rich_text_section",+              "elements": [+                {+                  "type": "link",+                  "url": "https://jadeapptesting.slack.com/archives/C043YJGBY49/p1663966292304369"+                }+              ]+            }+          ]+        }+      ]+    },+    "channel": "C043YJGBY49",+    "hidden": true,+    "ts": "1663969334.001500",+    "event_ts": "1663969334.001500",+    "channel_type": "channel"+  },+  "type": "event_callback",+  "event_id": "Ev043QRVT9FD",+  "event_time": 1663969334,+  "authorizations": [+    {+      "enterprise_id": null,+      "team_id": "T043DB835ML",+      "user_id": "U0442US8QGH",+      "is_bot": true,+      "is_enterprise_install": false+    }+  ],+  "is_ext_shared_channel": false,+  "event_context": "aaaa"+}
+ tests/golden/SlackWebhookEvent/messageExample.golden view
@@ -0,0 +1,42 @@+EventEventCallback+    ( EventCallback+        { eventId = EventId+            { unEventId = "Ev043T7CKN84" }+        , teamId = TeamId+            { unTeamId = "T043DB835ML" }+        , eventTime = MkSystemTime+            { systemSeconds = 1663966382+            , systemNanoseconds = 0+            }+        , event = EventMessage+            ( MessageEvent+                { blocks = Just+                    [ RichText+                        { blockId = Just+                            ( NonEmptyText "u6n" )+                        , elements =+                            [ RichTextSectionItemRichText+                                [ RichItemText "dgsfklsdgf"+                                    ( RichStyle+                                        { rsBold = False+                                        , rsItalic = False+                                        }+                                    )+                                ]+                            ]+                        }+                    ]+                , channel = ConversationId+                    { unConversationId = "C043YJGBY49" }+                , text = "dgsfklsdgf"+                , channelType = Channel+                , user = UserId+                    { unUserId = "U043H11ES4V" }+                , ts = "1663966382.046509"+                , threadTs = Nothing+                , appId = Nothing+                , botId = Nothing+                }+            )+        }+    )
+ tests/golden/SlackWebhookEvent/messageExample.json view
@@ -0,0 +1,43 @@+{+  "token": "aaaaaaaaaaaaaaaaaaaaabbb",+  "team_id": "T043DB835ML",+  "api_app_id": "A0442TUPHGR",+  "event": {+    "client_msg_id": "62ee8aaa-b28-4959-b2dc-c0ba4ecbfc7c",+    "type": "message",+    "text": "dgsfklsdgf",+    "user": "U043H11ES4V",+    "ts": "1663966382.046509",+    "team": "T43DB835ML",+    "blocks": [+      {+        "type": "rich_text",+        "block_id": "u6n",+        "elements": [+          {+            "type": "rich_text_section",+            "elements": [{ "type": "text", "text": "dgsfklsdgf" }]+          }+        ]+      }+    ],+    "channel": "C043YJGBY49",+    "event_ts": "1663966382.046509",+    "channel_type": "channel"+  },+  "type": "event_callback",+  "event_id": "Ev043T7CKN84",+  "event_time": 1663966382,+  "authorizations": [+    {+      "enterprise_id": null,+      "team_id": "T043DB835L",+      "user_id": "U0442US8QGH",+      "is_bot": true,+      "is_enterprise_install": false+    }+  ],+  "is_ext_shared_channel": false,+  "event_context": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"+}+
+ tests/golden/SlackWebhookEvent/messageIm.golden view
@@ -0,0 +1,42 @@+EventEventCallback+    ( EventCallback+        { eventId = EventId+            { unEventId = "Ev044C51K43V" }+        , teamId = TeamId+            { unTeamId = "T043DB835ML" }+        , eventTime = MkSystemTime+            { systemSeconds = 1664408649+            , systemNanoseconds = 0+            }+        , event = EventMessage+            ( MessageEvent+                { blocks = Just+                    [ RichText+                        { blockId = Just+                            ( NonEmptyText "LSo" )+                        , elements =+                            [ RichTextSectionItemRichText+                                [ RichItemText "test"+                                    ( RichStyle+                                        { rsBold = False+                                        , rsItalic = False+                                        }+                                    )+                                ]+                            ]+                        }+                    ]+                , channel = ConversationId+                    { unConversationId = "D0442US94JD" }+                , text = "test"+                , channelType = Im+                , user = UserId+                    { unUserId = "U043H11ES4V" }+                , ts = "1664408649.009629"+                , threadTs = Nothing+                , appId = Nothing+                , botId = Nothing+                }+            )+        }+    )
+ tests/golden/SlackWebhookEvent/messageIm.json view
@@ -0,0 +1,47 @@+{+  "token": "aaaa",+  "team_id": "T043DB835ML",+  "api_app_id": "A0442TUPHGR",+  "event": {+    "client_msg_id": "1efd76a1-5075-4c2c-ad38-1284820b429f",+    "type": "message",+    "text": "test",+    "user": "U043H11ES4V",+    "ts": "1664408649.009629",+    "team": "T043DB835ML",+    "blocks": [+      {+        "type": "rich_text",+        "block_id": "LSo",+        "elements": [+          {+            "type": "rich_text_section",+            "elements": [+              {+                "type": "text",+                "text": "test"+              }+            ]+          }+        ]+      }+    ],+    "channel": "D0442US94JD",+    "event_ts": "1664408649.009629",+    "channel_type": "im"+  },+  "type": "event_callback",+  "event_id": "Ev044C51K43V",+  "event_time": 1664408649,+  "authorizations": [+    {+      "enterprise_id": null,+      "team_id": "T043DB835ML",+      "user_id": "U0442US8QGH",+      "is_bot": true,+      "is_enterprise_install": false+    }+  ],+  "is_ext_shared_channel": false,+  "event_context": "aaaa"+}
+ tests/golden/SlackWebhookEvent/share_without_message.golden view
@@ -0,0 +1,27 @@+EventEventCallback+    ( EventCallback+        { eventId = EventId+            { unEventId = "Ev04BEN3QMSM" }+        , teamId = TeamId+            { unTeamId = "T043DB835ML" }+        , eventTime = MkSystemTime+            { systemSeconds = 1668537593+            , systemNanoseconds = 0+            }+        , event = EventMessage+            ( MessageEvent+                { blocks = Nothing+                , channel = ConversationId+                    { unConversationId = "C043YJGBY49" }+                , text = ""+                , channelType = Channel+                , user = UserId+                    { unUserId = "U043H11ES4V" }+                , ts = "1668537593.598469"+                , threadTs = Nothing+                , appId = Nothing+                , botId = Nothing+                }+            )+        }+    )
+ tests/golden/SlackWebhookEvent/share_without_message.json view
@@ -0,0 +1,78 @@+{+  "token": "aaaa",+  "team_id": "T043DB835ML",+  "api_app_id": "A0442TUPHGR",+  "event": {+    "type": "message",+    "text": "",+    "user": "U043H11ES4V",+    "ts": "1668537593.598469",+    "team": "T043DB835ML",+    "attachments": [+      {+        "from_url": "https://jadeapptesting.slack.com/archives/C043KSKGJUB/p1665615886364019",+        "ts": "1665615886.364019",+        "author_id": "U043H11ES4V",+        "channel_id": "C043KSKGJUB",+        "channel_team": "T043DB835ML",+        "channel_name": "testing-slack-app",+        "is_msg_unfurl": true,+        "message_blocks": [+          {+            "team": "T043DB835ML",+            "channel": "C043KSKGJUB",+            "ts": "1665615886.364019",+            "message": {+              "blocks": [+                {+                  "type": "rich_text",+                  "block_id": "TNHa4",+                  "elements": [+                    {+                      "type": "rich_text_section",+                      "elements": [+                        {+                          "type": "text",+                          "text": "blahblahblahblahblah"+                        }+                      ]+                    }+                  ]+                }+              ]+            }+          }+        ],+        "color": "D0D0D0",+        "is_share": true,+        "fallback": "[October 12th, 2022 4:04 PM] jadel: blahblahblahblahblah",+        "text": "blahblahblahblahblah",+        "author_name": "jadel",+        "author_link": "https://jadeapptesting.slack.com/team/U043H11ES4V",+        "author_icon": "https://secure.gravatar.com/avatar/dcd5bc53dcfaca62ddc3f5726d07ba13.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2Fdf10d%2Fimg%2Favatars%2Fava_0019-48.png",+        "author_subname": "jadel",+        "mrkdwn_in": [+          "text"+        ],+        "footer": "Posted in #testing-slack-app"+      }+    ],+    "channel": "C043YJGBY49",+    "event_ts": "1668537593.598469",+    "channel_type": "channel"+  },+  "type": "event_callback",+  "event_id": "Ev04BEN3QMSM",+  "event_time": 1668537593,+  "authorizations": [+    {+      "enterprise_id": null,+      "team_id": "T043DB835ML",+      "user_id": "U0442US8QGH",+      "is_bot": true,+      "is_enterprise_install": false+    }+  ],+  "is_ext_shared_channel": false,+  "event_context": "aaaa"+}
+ tests/golden/SlackWebhookEvent/shared_message_no_content.json view
@@ -0,0 +1,78 @@+{+  "token": "aaaa",+  "team_id": "T043DB835ML",+  "api_app_id": "A0442TUPHGR",+  "event": {+    "type": "message",+    "text": "",+    "user": "U043H11ES4V",+    "ts": "1668537593.598469",+    "team": "T043DB835ML",+    "attachments": [+      {+        "from_url": "https://jadeapptesting.slack.com/archives/C043KSKGJUB/p1665615886364019",+        "ts": "1665615886.364019",+        "author_id": "U043H11ES4V",+        "channel_id": "C043KSKGJUB",+        "channel_team": "T043DB835ML",+        "channel_name": "testing-slack-app",+        "is_msg_unfurl": true,+        "message_blocks": [+          {+            "team": "T043DB835ML",+            "channel": "C043KSKGJUB",+            "ts": "1665615886.364019",+            "message": {+              "blocks": [+                {+                  "type": "rich_text",+                  "block_id": "TNHa4",+                  "elements": [+                    {+                      "type": "rich_text_section",+                      "elements": [+                        {+                          "type": "text",+                          "text": "blahblahblahblahblah"+                        }+                      ]+                    }+                  ]+                }+              ]+            }+          }+        ],+        "color": "D0D0D0",+        "is_share": true,+        "fallback": "[October 12th, 2022 4:04 PM] jadel: blahblahblahblahblah",+        "text": "blahblahblahblahblah",+        "author_name": "jadel",+        "author_link": "https://jadeapptesting.slack.com/team/U043H11ES4V",+        "author_icon": "https://secure.gravatar.com/avatar/dcd5bc53dcfaca62ddc3f5726d07ba13.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2Fdf10d%2Fimg%2Favatars%2Fava_0019-48.png",+        "author_subname": "jadel",+        "mrkdwn_in": [+          "text"+        ],+        "footer": "Posted in #testing-slack-app"+      }+    ],+    "channel": "C043YJGBY49",+    "event_ts": "1668537593.598469",+    "channel_type": "channel"+  },+  "type": "event_callback",+  "event_id": "Ev04BEN3QMSM",+  "event_time": 1668537593,+  "authorizations": [+    {+      "enterprise_id": null,+      "team_id": "T043DB835ML",+      "user_id": "U0442US8QGH",+      "is_bot": true,+      "is_enterprise_install": false+    }+  ],+  "is_ext_shared_channel": false,+  "event_context": "aaaa"+}
+ tests/golden/SlackWebhookEvent/slackbotIm.golden view
@@ -0,0 +1,51 @@+EventEventCallback+    ( EventCallback+        { eventId = EventId+            { unEventId = "Ev044Y3D66M9" }+        , teamId = TeamId+            { unTeamId = "T043DB835ML" }+        , eventTime = MkSystemTime+            { systemSeconds = 1664826477+            , systemNanoseconds = 0+            }+        , event = EventMessage+            ( MessageEvent+                { blocks = Just+                    [ RichText+                        { blockId = Just+                            ( NonEmptyText "ZRe" )+                        , elements =+                            [ RichTextSectionItemRichText+                                [ RichItemText "You have been removed from #testing-slack-app by "+                                    ( RichStyle+                                        { rsBold = False+                                        , rsItalic = False+                                        }+                                    )+                                , RichItemUser+                                    ( UserId+                                        { unUserId = "U043H11ES4V" }+                                    )+                                    ( RichStyle+                                        { rsBold = False+                                        , rsItalic = False+                                        }+                                    )+                                ]+                            ]+                        }+                    ]+                , channel = ConversationId+                    { unConversationId = "D043HMJ0WDU" }+                , text = "You have been removed from #testing-slack-app by <@U043H11ES4V>"+                , channelType = Im+                , user = UserId+                    { unUserId = "USLACKBOT" }+                , ts = "1664826477.425039"+                , threadTs = Nothing+                , appId = Nothing+                , botId = Nothing+                }+            )+        }+    )
+ tests/golden/SlackWebhookEvent/slackbotIm.json view
@@ -0,0 +1,50 @@+{+  "token": "aaaa",+  "team_id": "T043DB835ML",+  "api_app_id": "A0442TUPHGR",+  "event": {+    "type": "message",+    "text": "You have been removed from #testing-slack-app by <@U043H11ES4V>",+    "user": "USLACKBOT",+    "ts": "1664826477.425039",+    "team": "T043DB835ML",+    "blocks": [+      {+        "type": "rich_text",+        "block_id": "ZRe",+        "elements": [+          {+            "type": "rich_text_section",+            "elements": [+              {+                "type": "text",+                "text": "You have been removed from #testing-slack-app by "+              },+              {+                "type": "user",+                "user_id": "U043H11ES4V"+              }+            ]+          }+        ]+      }+    ],+    "channel": "D043HMJ0WDU",+    "event_ts": "1664826477.425039",+    "channel_type": "im"+  },+  "type": "event_callback",+  "event_id": "Ev044Y3D66M9",+  "event_time": 1664826477,+  "authorizations": [+    {+      "enterprise_id": null,+      "team_id": "T043DB835ML",+      "user_id": "U0442US8QGH",+      "is_bot": true,+      "is_enterprise_install": false+    }+  ],+  "is_ext_shared_channel": false,+  "event_context": "aaaa"+}
+ tests/golden/UpdateRsp/actual.golden view
@@ -0,0 +1,6 @@+UpdateRsp+    { updateRspChannel = ConversationId+        { unConversationId = "D0442US94JD" }+    , updateRspTs = "1668717810.728689"+    , updateRspText = "blahblahblah"+    }
+ tests/golden/UpdateRsp/actual.json view
@@ -0,0 +1,48 @@+{+    "ok": true,+    "channel": "D0442US94JD",+    "ts": "1668717810.728689",+    "text": "blahblahblah",+    "message": {+        "bot_id": "B0439P161B9",+        "type": "message",+        "text": "blahblahblah",+        "user": "U0442US8QGH",+        "app_id": "A0442TUPHGR",+        "blocks": [+            {+                "type": "rich_text",+                "block_id": "j1Bvn",+                "elements": [+                    {+                        "type": "rich_text_section",+                        "elements": [+                            {+                                "type": "text",+                                "text": "blahblahblah"+                            }+                        ]+                    }+                ]+            }+        ],+        "team": "T043DB835ML",+        "bot_profile": {+            "id": "B0439P161B9",+            "app_id": "A0442TUPHGR",+            "name": "Slacklinker dev",+            "icons": {+                "image_36": "https://a.slack-edge.com/80588/img/plugins/app/bot_36.png",+                "image_48": "https://a.slack-edge.com/80588/img/plugins/app/bot_48.png",+                "image_72": "https://a.slack-edge.com/80588/img/plugins/app/service_72.png"+            },+            "deleted": false,+            "updated": 1663960005,+            "team_id": "T043DB835ML"+        },+        "edited": {+            "user": "B0439P161B9",+            "ts": "1668721589.000000"+        }+    }+}
+ tests/golden/UpdateRsp/sample.golden view
@@ -0,0 +1,6 @@+UpdateRsp+    { updateRspChannel = ConversationId+        { unConversationId = "C024BE91L" }+    , updateRspTs = "1401383885.000061"+    , updateRspText = "Updated text you carefully authored"+    }
+ tests/golden/UpdateRsp/sample.json view
@@ -0,0 +1,10 @@+{+    "ok": true,+    "channel": "C024BE91L",+    "ts": "1401383885.000061",+    "text": "Updated text you carefully authored",+    "message": {+        "text": "Updated text you carefully authored",+        "user": "U34567890"+    }+}
+ tests/golden/UsersConversationsResponse/im_and_channels.golden view
@@ -0,0 +1,108 @@+UsersConversationsResponse+    { channels =+        [ Channel+            ( ChannelConversation+                { channelId = ConversationId+                    { unConversationId = "C043YJGBY49" }+                , channelName = "general"+                , channelCreated = 1663890553+                , channelIsArchived = False+                , channelIsGeneral = True+                , channelUnlinked = 0+                , channelNameNormalized = "general"+                , channelIsShared = False+                , channelCreator = UserId+                    { unUserId = "U043H11ES4V" }+                , channelIsExtShared = False+                , channelIsOrgShared = False+                , channelSharedTeamIds = Just+                    [ TeamId+                        { unTeamId = "T043DB835ML" }+                    ]+                , channelIsPendingExtShared = False+                , channelIsMember = Nothing+                , channelTopic = Topic+                    { topicValue = ""+                    , topicCreator = ""+                    , topicLastSet = 0+                    }+                , channelPurpose = Purpose+                    { purposeValue = "This is the one channel that will always include everyone. It’s a great spot for announcements and team-wide conversations."+                    , purposeCreator = "U043H11ES4V"+                    , purposeLastSet = 1663890553+                    }+                , channelPreviousNames = []+                , channelNumMembers = Nothing+                }+            )+        , Channel+            ( ChannelConversation+                { channelId = ConversationId+                    { unConversationId = "C0451SKQN72" }+                , channelName = "new-channel-2"+                , channelCreated = 1664407230+                , channelIsArchived = False+                , channelIsGeneral = False+                , channelUnlinked = 0+                , channelNameNormalized = "new-channel-2"+                , channelIsShared = False+                , channelCreator = UserId+                    { unUserId = "U043H11ES4V" }+                , channelIsExtShared = False+                , channelIsOrgShared = False+                , channelSharedTeamIds = Just+                    [ TeamId+                        { unTeamId = "T043DB835ML" }+                    ]+                , channelIsPendingExtShared = False+                , channelIsMember = Nothing+                , channelTopic = Topic+                    { topicValue = ""+                    , topicCreator = ""+                    , topicLastSet = 0+                    }+                , channelPurpose = Purpose+                    { purposeValue = ""+                    , purposeCreator = ""+                    , purposeLastSet = 0+                    }+                , channelPreviousNames = []+                , channelNumMembers = Nothing+                }+            )+        , Im+            ( ImConversation+                { imId = ConversationId+                    { unConversationId = "D0442US94JD" }+                , imCreated = 1663960005+                , imIsArchived = False+                , imIsOrgShared = False+                , imUser = UserId+                    { unUserId = "U043H11ES4V" }+                , imIsUserDeleted = False+                , imPriority = 0.0+                }+            )+        , Im+            ( ImConversation+                { imId = ConversationId+                    { unConversationId = "D043HMJ0WDU" }+                , imCreated = 1663960005+                , imIsArchived = False+                , imIsOrgShared = False+                , imUser = UserId+                    { unUserId = "USLACKBOT" }+                , imIsUserDeleted = False+                , imPriority = 0.0+                }+            )+        ]+    , responseMetadata = Just+        ( ResponseMetadata+            { responseMetadataNextCursor = Just+                ( Cursor+                    { unCursor = "" }+                )+            }+        )+    }
+ tests/golden/UsersConversationsResponse/im_and_channels.json view
@@ -0,0 +1,104 @@+{+    "ok": true,+    "channels": [+        {+            "id": "C043YJGBY49",+            "name": "general",+            "is_channel": true,+            "is_group": false,+            "is_im": false,+            "is_mpim": false,+            "is_private": false,+            "created": 1663890553,+            "is_archived": false,+            "is_general": true,+            "unlinked": 0,+            "name_normalized": "general",+            "is_shared": false,+            "is_org_shared": false,+            "is_pending_ext_shared": false,+            "pending_shared": [],+            "context_team_id": "T043DB835ML",+            "parent_conversation": null,+            "creator": "U043H11ES4V",+            "is_ext_shared": false,+            "shared_team_ids": [+                "T043DB835ML"+            ],+            "pending_connected_team_ids": [],+            "topic": {+                "value": "",+                "creator": "",+                "last_set": 0+            },+            "purpose": {+                "value": "This is the one channel that will always include everyone. It’s a great spot for announcements and team-wide conversations.",+                "creator": "U043H11ES4V",+                "last_set": 1663890553+            },+            "previous_names": []+        },+        {+            "id": "C0451SKQN72",+            "name": "new-channel-2",+            "is_channel": true,+            "is_group": false,+            "is_im": false,+            "is_mpim": false,+            "is_private": false,+            "created": 1664407230,+            "is_archived": false,+            "is_general": false,+            "unlinked": 0,+            "name_normalized": "new-channel-2",+            "is_shared": false,+            "is_org_shared": false,+            "is_pending_ext_shared": false,+            "pending_shared": [],+            "context_team_id": "T043DB835ML",+            "parent_conversation": null,+            "creator": "U043H11ES4V",+            "is_ext_shared": false,+            "shared_team_ids": [+                "T043DB835ML"+            ],+            "pending_connected_team_ids": [],+            "topic": {+                "value": "",+                "creator": "",+                "last_set": 0+            },+            "purpose": {+                "value": "",+                "creator": "",+                "last_set": 0+            },+            "previous_names": []+        },+        {+            "id": "D0442US94JD",+            "created": 1663960005,+            "is_archived": false,+            "is_im": true,+            "is_org_shared": false,+            "context_team_id": "T043DB835ML",+            "user": "U043H11ES4V",+            "is_user_deleted": false,+            "priority": 0+        },+        {+            "id": "D043HMJ0WDU",+            "created": 1663960005,+            "is_archived": false,+            "is_im": true,+            "is_org_shared": false,+            "context_team_id": "T043DB835ML",+            "user": "USLACKBOT",+            "is_user_deleted": false,+            "priority": 0+        }+    ],+    "response_metadata": {+        "next_cursor": ""+    }+}