slack-web 2.0.1.0 → 2.1.0.0
raw patch · 48 files changed
+1056/−95 lines, 48 filesdep ~aesondep ~basedep ~http-api-dataPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: aeson, base, http-api-data, text
API changes (from Hackage documentation)
- Web.Slack.Internal: ResponseSlackError :: Text -> ResponseSlackError
- Web.Slack.Internal: data ResponseSlackError
- Web.Slack.Internal: instance GHC.Classes.Eq Web.Slack.Internal.ResponseSlackError
- Web.Slack.Internal: instance GHC.Show.Show Web.Slack.Internal.ResponseSlackError
+ Web.Slack.Common: ResponseSlackError :: Text -> Object -> ResponseSlackError
+ Web.Slack.Common: [errorText] :: ResponseSlackError -> Text
+ Web.Slack.Common: [responseMetadata] :: ResponseSlackError -> Object
+ Web.Slack.Common: data ResponseSlackError
+ Web.Slack.Common: instance Control.DeepSeq.NFData Web.Slack.Common.ResponseSlackError
+ Web.Slack.Common: instance GHC.Classes.Eq Web.Slack.Common.ResponseSlackError
+ Web.Slack.Common: instance GHC.Generics.Generic Web.Slack.Common.ResponseSlackError
+ Web.Slack.Common: instance GHC.Show.Show Web.Slack.Common.ResponseSlackError
+ Web.Slack.Experimental.Blocks: SlackBlockOther :: Object -> SlackBlock
+ Web.Slack.Experimental.Blocks.Builder: BlockKitBuilderMessage :: [SlackBlock] -> BlockKitBuilderMessage
+ Web.Slack.Experimental.Blocks.Builder: [blocks] :: BlockKitBuilderMessage -> [SlackBlock]
+ Web.Slack.Experimental.Blocks.Builder: actionsBlock :: Monad m => Maybe SlackBlockId -> SlackActionList -> BlockBuilder m ()
+ Web.Slack.Experimental.Blocks.Builder: contextBlock :: Monad m => [SlackContent] -> BlockBuilder m ()
+ Web.Slack.Experimental.Blocks.Builder: dividerBlock :: Monad m => BlockBuilder m ()
+ Web.Slack.Experimental.Blocks.Builder: headerBlock :: Monad m => SlackText -> BlockBuilder m ()
+ Web.Slack.Experimental.Blocks.Builder: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Blocks.Builder.BlockKitBuilderMessage
+ Web.Slack.Experimental.Blocks.Builder: newtype BlockKitBuilderMessage
+ Web.Slack.Experimental.Blocks.Builder: runBlockBuilder :: Monad m => BlockBuilder m () -> m [SlackBlock]
+ Web.Slack.Experimental.Blocks.Builder: sectionBlock :: Monad m => SlackText -> BlockBuilder m ()
+ Web.Slack.Experimental.Blocks.Builder: sectionBlockWithAccessory :: Monad m => SlackText -> SlackAction -> BlockBuilder m ()
+ Web.Slack.Experimental.Blocks.Builder: sectionBlockWithFields :: Monad m => SlackText -> [SlackText] -> BlockBuilder m ()
+ Web.Slack.Experimental.Blocks.Builder: type BlockBuilder = WriterT [SlackBlock]
+ Web.Slack.Experimental.Blocks.Types: SlackBlockOther :: Object -> SlackBlock
+ Web.Slack.Experimental.Events.Types: AppHomeOpenedEvent :: UserId -> ConversationId -> AppHomeTab -> Text -> AppHomeOpenedEvent
+ Web.Slack.Experimental.Events.Types: AppHomeTab :: Text -> AppHomeTab
+ Web.Slack.Experimental.Events.Types: BotProfile :: Text -> Text -> Text -> TeamId -> BotProfile
+ Web.Slack.Experimental.Events.Types: EventAppHomeOpened :: AppHomeOpenedEvent -> Event
+ Web.Slack.Experimental.Events.Types: [$sel:appId:BotProfile] :: BotProfile -> Text
+ Web.Slack.Experimental.Events.Types: [$sel:botProfile:MessageEvent] :: MessageEvent -> Maybe BotProfile
+ Web.Slack.Experimental.Events.Types: [$sel:channel:AppHomeOpenedEvent] :: AppHomeOpenedEvent -> ConversationId
+ Web.Slack.Experimental.Events.Types: [$sel:eventTs:AppHomeOpenedEvent] :: AppHomeOpenedEvent -> Text
+ Web.Slack.Experimental.Events.Types: [$sel:id:BotProfile] :: BotProfile -> Text
+ Web.Slack.Experimental.Events.Types: [$sel:name:BotProfile] :: BotProfile -> Text
+ Web.Slack.Experimental.Events.Types: [$sel:tab:AppHomeOpenedEvent] :: AppHomeOpenedEvent -> AppHomeTab
+ Web.Slack.Experimental.Events.Types: [$sel:teamId:BotProfile] :: BotProfile -> TeamId
+ Web.Slack.Experimental.Events.Types: [$sel:unAppHomeTab:AppHomeTab] :: AppHomeTab -> Text
+ Web.Slack.Experimental.Events.Types: [$sel:user:AppHomeOpenedEvent] :: AppHomeOpenedEvent -> UserId
+ Web.Slack.Experimental.Events.Types: appHomeTabHome :: AppHomeTab
+ Web.Slack.Experimental.Events.Types: appHomeTabMessages :: AppHomeTab
+ Web.Slack.Experimental.Events.Types: data AppHomeOpenedEvent
+ Web.Slack.Experimental.Events.Types: data BotProfile
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Events.Types.AppHomeOpenedEvent
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Events.Types.AppHomeTab
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Events.Types.BotProfile
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Events.Types.AppHomeTab
+ Web.Slack.Experimental.Events.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Events.Types.AppHomeTab
+ Web.Slack.Experimental.Events.Types: instance GHC.Show.Show Web.Slack.Experimental.Events.Types.AppHomeOpenedEvent
+ Web.Slack.Experimental.Events.Types: instance GHC.Show.Show Web.Slack.Experimental.Events.Types.AppHomeTab
+ Web.Slack.Experimental.Events.Types: instance GHC.Show.Show Web.Slack.Experimental.Events.Types.BotProfile
+ Web.Slack.Experimental.Events.Types: newtype AppHomeTab
+ Web.Slack.Experimental.Views: Expected :: Expected (lit :: Symbol)
+ Web.Slack.Experimental.Views: HomeTabView :: Expected "home" -> HomeTabView
+ Web.Slack.Experimental.Views: ModalView :: Expected "modal" -> SlackPlainTextOnly -> Maybe SlackPlainTextOnly -> Maybe SlackPlainTextOnly -> Maybe Bool -> ModalView
+ Web.Slack.Experimental.Views: PublishReq :: UserId -> SlackView HomeTabView -> PublishReq
+ Web.Slack.Experimental.Views: PublishResp :: SlackView HomeTabView -> PublishResp
+ Web.Slack.Experimental.Views: SlackView :: Vector SlackBlock -> Maybe Text -> Maybe Text -> Maybe Text -> inner -> SlackView inner
+ Web.Slack.Experimental.Views: [$sel:blocks:SlackView] :: SlackView inner -> Vector SlackBlock
+ Web.Slack.Experimental.Views: [$sel:callbackId:SlackView] :: SlackView inner -> Maybe Text
+ Web.Slack.Experimental.Views: [$sel:close:ModalView] :: ModalView -> Maybe SlackPlainTextOnly
+ Web.Slack.Experimental.Views: [$sel:externalId:SlackView] :: SlackView inner -> Maybe Text
+ Web.Slack.Experimental.Views: [$sel:inner:SlackView] :: SlackView inner -> inner
+ Web.Slack.Experimental.Views: [$sel:privateMetadata:SlackView] :: SlackView inner -> Maybe Text
+ Web.Slack.Experimental.Views: [$sel:submit:ModalView] :: ModalView -> Maybe SlackPlainTextOnly
+ Web.Slack.Experimental.Views: [$sel:submitDisabled:ModalView] :: ModalView -> Maybe Bool
+ Web.Slack.Experimental.Views: [$sel:title:ModalView] :: ModalView -> SlackPlainTextOnly
+ Web.Slack.Experimental.Views: [$sel:type_:HomeTabView] :: HomeTabView -> Expected "home"
+ Web.Slack.Experimental.Views: [$sel:type_:ModalView] :: ModalView -> Expected "modal"
+ Web.Slack.Experimental.Views: [$sel:userId:PublishReq] :: PublishReq -> UserId
+ Web.Slack.Experimental.Views: [$sel:view:PublishReq] :: PublishReq -> SlackView HomeTabView
+ Web.Slack.Experimental.Views: [$sel:view:PublishResp] :: PublishResp -> SlackView HomeTabView
+ Web.Slack.Experimental.Views: data Expected (lit :: Symbol)
+ Web.Slack.Experimental.Views: data HomeTabView
+ Web.Slack.Experimental.Views: data ModalView
+ Web.Slack.Experimental.Views: data PublishReq
+ Web.Slack.Experimental.Views: data PublishResp
+ Web.Slack.Experimental.Views: data SlackView inner
+ Web.Slack.Experimental.Views: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Views.HomeTabView
+ Web.Slack.Experimental.Views: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Views.ModalView
+ Web.Slack.Experimental.Views: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Views.PublishResp
+ Web.Slack.Experimental.Views: instance Data.Aeson.Types.FromJSON.FromJSON inner => Data.Aeson.Types.FromJSON.FromJSON (Web.Slack.Experimental.Views.SlackView inner)
+ Web.Slack.Experimental.Views: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Views.HomeTabView
+ Web.Slack.Experimental.Views: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Views.ModalView
+ Web.Slack.Experimental.Views: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Experimental.Views.PublishResp
+ Web.Slack.Experimental.Views: instance Data.Aeson.Types.ToJSON.ToJSON inner => Data.Aeson.Types.ToJSON.ToJSON (Web.Slack.Experimental.Views.SlackView inner)
+ Web.Slack.Experimental.Views: instance GHC.Classes.Eq Web.Slack.Experimental.Views.HomeTabView
+ Web.Slack.Experimental.Views: instance GHC.Classes.Eq Web.Slack.Experimental.Views.ModalView
+ Web.Slack.Experimental.Views: instance GHC.Classes.Eq inner => GHC.Classes.Eq (Web.Slack.Experimental.Views.SlackView inner)
+ Web.Slack.Experimental.Views: instance GHC.Generics.Generic (Web.Slack.Experimental.Views.SlackView inner)
+ Web.Slack.Experimental.Views: instance GHC.Generics.Generic Web.Slack.Experimental.Views.HomeTabView
+ Web.Slack.Experimental.Views: instance GHC.Generics.Generic Web.Slack.Experimental.Views.ModalView
+ Web.Slack.Experimental.Views: instance GHC.Show.Show Web.Slack.Experimental.Views.HomeTabView
+ Web.Slack.Experimental.Views: instance GHC.Show.Show Web.Slack.Experimental.Views.ModalView
+ Web.Slack.Experimental.Views: instance GHC.Show.Show Web.Slack.Experimental.Views.PublishReq
+ Web.Slack.Experimental.Views: instance GHC.Show.Show Web.Slack.Experimental.Views.PublishResp
+ Web.Slack.Experimental.Views: instance GHC.Show.Show inner => GHC.Show.Show (Web.Slack.Experimental.Views.SlackView inner)
+ Web.Slack.Experimental.Views: instance Web.Internal.FormUrlEncoded.ToForm Web.Slack.Experimental.Views.PublishReq
+ Web.Slack.Experimental.Views: type Api = "views.publish" :> AuthProtect "token" :> ReqBody '[FormUrlEncoded] PublishReq :> Post '[JSON] (ResponseJSON PublishResp)
+ Web.Slack.Experimental.Views: viewsPublish :: SlackConfig -> PublishReq -> IO (Response PublishResp)
+ Web.Slack.Internal: instance GHC.Show.Show a => GHC.Show.Show (Web.Slack.Internal.ResponseJSON a)
+ Web.Slack.Reactions: AddReq :: ConversationId -> Emoji -> Text -> AddReq
+ Web.Slack.Reactions: AddResp :: AddResp
+ Web.Slack.Reactions: [$sel:channel:AddReq] :: AddReq -> ConversationId
+ Web.Slack.Reactions: [$sel:name:AddReq] :: AddReq -> Emoji
+ Web.Slack.Reactions: [$sel:timestamp:AddReq] :: AddReq -> Text
+ Web.Slack.Reactions: data AddReq
+ Web.Slack.Reactions: data AddResp
+ Web.Slack.Reactions: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Reactions.AddResp
+ Web.Slack.Reactions: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Reactions.AddResp
+ Web.Slack.Reactions: instance GHC.Classes.Eq Web.Slack.Reactions.AddReq
+ Web.Slack.Reactions: instance GHC.Classes.Eq Web.Slack.Reactions.AddResp
+ Web.Slack.Reactions: instance GHC.Show.Show Web.Slack.Reactions.AddReq
+ Web.Slack.Reactions: instance GHC.Show.Show Web.Slack.Reactions.AddResp
+ Web.Slack.Reactions: instance Web.Internal.FormUrlEncoded.ToForm Web.Slack.Reactions.AddReq
+ Web.Slack.Reactions: reactionsAdd :: SlackConfig -> AddReq -> IO (Response AddResp)
+ Web.Slack.Reactions: reactionsAdd_ :: AuthenticatedRequest (AuthProtect "token") -> AddReq -> ClientM (ResponseJSON AddResp)
+ Web.Slack.Reactions: type Api = "reactions.add" :> AuthProtect "token" :> ReqBody '[FormUrlEncoded] AddReq :> Post '[JSON] (ResponseJSON AddResp)
+ Web.Slack.Types: Emoji :: Text -> Emoji
+ Web.Slack.Types: [unEmoji] :: Emoji -> Text
+ Web.Slack.Types: instance Control.DeepSeq.NFData Web.Slack.Types.Emoji
+ Web.Slack.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Types.Emoji
+ Web.Slack.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Types.Emoji
+ Web.Slack.Types: instance Data.Hashable.Class.Hashable Web.Slack.Types.Emoji
+ Web.Slack.Types: instance Data.String.IsString Web.Slack.Types.Emoji
+ Web.Slack.Types: instance GHC.Classes.Eq Web.Slack.Types.Emoji
+ Web.Slack.Types: instance GHC.Show.Show Web.Slack.Types.Emoji
+ Web.Slack.Types: instance Web.Internal.HttpApiData.ToHttpApiData Web.Slack.Types.Emoji
+ Web.Slack.Types: newtype Emoji
- Web.Slack.Chat: PostMsg :: Text -> Maybe Text -> Maybe Bool -> Maybe Text -> Maybe Bool -> Maybe Bool -> Maybe Text -> Maybe Bool -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Bool -> PostMsg
+ Web.Slack.Chat: PostMsg :: Text -> Maybe Text -> Maybe Bool -> Maybe Text -> Maybe Bool -> Maybe Bool -> Maybe Text -> Maybe Bool -> Maybe Text -> Maybe Emoji -> Maybe Text -> Maybe Bool -> PostMsg
- 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: PostMsgReq :: Text -> Maybe Text -> Maybe Text -> Maybe Bool -> Maybe Text -> Maybe Text -> Maybe Bool -> Maybe Bool -> Maybe Text -> Maybe Bool -> Maybe Text -> Maybe Emoji -> Maybe Text -> Maybe Bool -> PostMsgReq
- Web.Slack.Chat: [postMsgIconEmoji] :: PostMsg -> Maybe Text
+ Web.Slack.Chat: [postMsgIconEmoji] :: PostMsg -> Maybe Emoji
- Web.Slack.Chat: [postMsgReqIconEmoji] :: PostMsgReq -> Maybe Text
+ Web.Slack.Chat: [postMsgReqIconEmoji] :: PostMsgReq -> Maybe Emoji
- Web.Slack.Common: SlackError :: Text -> SlackClientError
+ Web.Slack.Common: SlackError :: ResponseSlackError -> SlackClientError
- Web.Slack.Experimental.Blocks: RichItemEmoji :: Text -> RichItem
+ Web.Slack.Experimental.Blocks: RichItemEmoji :: Emoji -> RichItem
- Web.Slack.Experimental.Blocks.Types: RichItemEmoji :: Text -> RichItem
+ Web.Slack.Experimental.Blocks.Types: RichItemEmoji :: Emoji -> RichItem
- Web.Slack.Experimental.Events.Types: MessageEvent :: Maybe [SlackBlock] -> ConversationId -> Text -> ChannelType -> Maybe [FileObject] -> UserId -> Text -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe [MessageAttachment] -> MessageEvent
+ Web.Slack.Experimental.Events.Types: MessageEvent :: Maybe [SlackBlock] -> ConversationId -> Text -> ChannelType -> Maybe [FileObject] -> UserId -> Text -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe BotProfile -> Maybe [MessageAttachment] -> MessageEvent
- Web.Slack.User: Profile :: Maybe Text -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Text -> Text -> Text -> Text -> Text -> Text -> Text -> Maybe Text -> Profile
+ Web.Slack.User: Profile :: Maybe Text -> Maybe Text -> Maybe Emoji -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Text -> Text -> Text -> Text -> Text -> Text -> Text -> Maybe Text -> Profile
- Web.Slack.User: [profileStatusEmoji] :: Profile -> Maybe Text
+ Web.Slack.User: [profileStatusEmoji] :: Profile -> Maybe Emoji
Files
- CHANGELOG.md +19/−0
- slack-web.cabal +20/−8
- src/Web/Slack/AesonUtils.hs +32/−1
- src/Web/Slack/Chat.hs +3/−2
- src/Web/Slack/Common.hs +17/−1
- src/Web/Slack/Experimental/Blocks/Builder.hs +105/−0
- src/Web/Slack/Experimental/Blocks/Types.hs +30/−7
- src/Web/Slack/Experimental/Events/Types.hs +69/−4
- src/Web/Slack/Experimental/Views.hs +160/−0
- src/Web/Slack/Internal.hs +11/−12
- src/Web/Slack/Reactions.hs +79/−0
- src/Web/Slack/Types.hs +8/−0
- src/Web/Slack/User.hs +2/−1
- tests/Web/Slack/Experimental/BlocksSpec.hs +3/−58
- tests/Web/Slack/Experimental/Events/TypesSpec.hs +2/−0
- tests/Web/Slack/Experimental/ViewsSpec.hs +25/−0
- tests/Web/Slack/InternalSpec.hs +20/−0
- tests/golden/PublishResp/views.publish example.golden +18/−0
- tests/golden/PublishResp/views.publish example.json +34/−0
- tests/golden/ResponseJSON/failed_view_publish.golden +17/−0
- tests/golden/ResponseJSON/failed_view_publish.json +11/−0
- tests/golden/ResponseJSON/metadata_example.golden +1/−0
- tests/golden/ResponseJSON/metadata_example.json +14/−0
- tests/golden/SlackView/homeTabView.golden +16/−0
- tests/golden/SlackView/homeTabView.json +35/−0
- tests/golden/SlackView/modalView.golden +86/−0
- tests/golden/SlackView/modalView.json +54/−0
- tests/golden/SlackWebhookEvent/app_home_opened_home.golden +23/−0
- tests/golden/SlackWebhookEvent/app_home_opened_home.json +25/−0
- tests/golden/SlackWebhookEvent/app_home_opened_messages.golden +23/−0
- tests/golden/SlackWebhookEvent/app_home_opened_messages.json +25/−0
- tests/golden/SlackWebhookEvent/botMessage.golden +9/−0
- tests/golden/SlackWebhookEvent/circleci.golden +9/−0
- tests/golden/SlackWebhookEvent/email_message.golden +1/−0
- tests/golden/SlackWebhookEvent/forwarded_message.golden +1/−0
- tests/golden/SlackWebhookEvent/github_notification.golden +9/−0
- tests/golden/SlackWebhookEvent/github_notification_ts_string.golden +9/−0
- tests/golden/SlackWebhookEvent/github_with_link.golden +9/−0
- tests/golden/SlackWebhookEvent/link.golden +1/−0
- tests/golden/SlackWebhookEvent/messageExample.golden +1/−0
- tests/golden/SlackWebhookEvent/messageIm.golden +1/−0
- tests/golden/SlackWebhookEvent/message_file_share.golden +1/−0
- tests/golden/SlackWebhookEvent/message_file_share_slack_connect.golden +1/−0
- tests/golden/SlackWebhookEvent/message_rich_text.golden +5/−1
- tests/golden/SlackWebhookEvent/non_spec_attachment.golden +9/−0
- tests/golden/SlackWebhookEvent/share_with_message.golden +1/−0
- tests/golden/SlackWebhookEvent/share_without_message.golden +1/−0
- tests/golden/SlackWebhookEvent/slackbotIm.golden +1/−0
CHANGELOG.md view
@@ -1,3 +1,22 @@+# 2.1.0.0 (2025-03-06)+* [#138](https://github.com/MercuryTechnologies/slack-web/pull/138)+ Implement `views.publish` method and App Home tab events.+* [#140](https://github.com/MercuryTechnologies/slack-web/pull/140)+ Implement `reactions.add` method.++ Breaking change: various places in the API using emoji now use an Emoji+ newtype.+* [#141](https://github.com/MercuryTechnologies/slack-web/pull/141)+ Include `response_metadata` in errors.+ This is a breaking change since it changes the type of `ResponseSlackError` and friends to add that field.+* [#142](https://github.com/MercuryTechnologies/slack-web/pull/142)+ Make the block builder previously used in the test suite public as an+ experimental module `Web.Slack.Experimental.Blocks.Builder`.++ This makes the DSL for specifying messages built with blocks a bit nicer.+* [#143](https://github.com/MercuryTechnologies/slack-web/pull/143)+ Add the `bot_profile` field to messages.+ # 2.0.1.0 (2025-01-09) * [#136](https://github.com/MercuryTechnologies/slack-web/pull/136) Bumps the version ranges for dependencies to be compatible with LTS 22.43.
slack-web.cabal view
@@ -1,6 +1,6 @@-cabal-version: 2.2+cabal-version: 3.0 name: slack-web-version: 2.0.1.0+version: 2.1.0.0 build-type: Simple @@ -29,10 +29,17 @@ tests/golden/BlockKitBuilderMessage/*.golden.json tests/golden/FileObject/*.json tests/golden/FileObject/*.golden+ tests/golden/SlackView/*.json+ tests/golden/SlackView/*.golden+ tests/golden/PublishResp/*.json+ tests/golden/PublishResp/*.golden+ tests/golden/ResponseJSON/*.json+ tests/golden/ResponseJSON/*.golden category: Web -tested-with: GHC == 8.10.7 || ==9.2.4 || == 9.4.2 || == 9.6.6+-- Please keep in sync with .github/workflows/haskell.yml :)+tested-with: GHC == { 9.2, 9.4, 9.6, 9.8, 9.10 } extra-source-files: CHANGELOG.md@@ -68,7 +75,6 @@ default-extensions: AllowAmbiguousTypes- ApplicativeDo BlockArguments DataKinds DeriveAnyClass@@ -96,6 +102,7 @@ RankNTypes RecordWildCards RecursiveDo+ RoleAnnotations ScopedTypeVariables StandaloneDeriving StandaloneKindSignatures@@ -118,22 +125,25 @@ Web.Slack.Files.Types Web.Slack.Internal Web.Slack.MessageParser+ Web.Slack.Reactions Web.Slack.Pager Web.Slack.Pager.Types Web.Slack.Types Web.Slack.User Web.Slack.UsersConversations Web.Slack.Experimental.Blocks+ Web.Slack.Experimental.Blocks.Builder Web.Slack.Experimental.Blocks.Types Web.Slack.Experimental.Events.Types+ Web.Slack.Experimental.Views Web.Slack.Experimental.RequestVerification other-modules: Web.Slack.Util Web.Slack.Prelude Web.Slack.AesonUtils build-depends:- aeson >= 2.0 && < 2.2- , base >= 4.11 && < 4.19+ aeson >= 2.0 && < 2.3+ , base >= 4.11 && < 4.22 , base16-bytestring , bytestring , classy-prelude@@ -144,7 +154,7 @@ , either , errors , hashable- , http-api-data >= 0.3 && < 0.6+ , http-api-data >= 0.3 && < 0.7 , http-client >= 0.5 && < 0.8 , http-client-tls >= 0.3 && < 0.4 , megaparsec >= 5.0 && < 10@@ -157,7 +167,7 @@ , servant-client-core >= 0.16 && < 0.21 , string-conversions , string-variants >= 0.1.0.1- , text (>= 1.2 && < 1.3) || (>= 2.0 && < 2.1)+ , text (>= 1.2 && < 1.3) || (>= 2.0 && < 2.2) , time , transformers , unordered-containers@@ -182,11 +192,13 @@ Web.Slack.ConversationSpec Web.Slack.ChatSpec Web.Slack.Files.TypesSpec+ Web.Slack.InternalSpec Web.Slack.UsersConversationsSpec Web.Slack.Experimental.RequestVerificationSpec Web.Slack.Experimental.Events.TypesSpec Web.Slack.Experimental.BlocksSpec Web.Slack.Experimental.Blocks.TypesSpec+ Web.Slack.Experimental.ViewsSpec TestImport TestImport.Aeson build-tool-depends:
src/Web/Slack/AesonUtils.hs view
@@ -2,10 +2,12 @@ import Data.Aeson import Data.Aeson qualified as J-import Data.Aeson.Types (Pair)+import Data.Aeson.Types (Pair, parseFail) import Data.Char qualified as Char+import Data.List (dropWhileEnd) import Data.Text qualified as T import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds)+import GHC.TypeLits (KnownSymbol, Symbol, symbolVal) import Web.FormUrlEncoded qualified as F import Web.Slack.Prelude @@ -108,6 +110,15 @@ , constructorTagModifier = camelTo2 '_' } +-- | 'snakeCaseOptions' that eats trailing underscores. This is so that you can+-- have a field called "type_".+snakeCaseOptionsEatTrailingUnderscore :: Options+snakeCaseOptionsEatTrailingUnderscore =+ defaultOptions+ { fieldLabelModifier = camelTo2 '_' . dropWhileEnd (== '_')+ , constructorTagModifier = camelTo2 '_'+ }+ snakeCaseFormOptions :: F.FormOptions snakeCaseFormOptions = F.defaultFormOptions@@ -122,3 +133,23 @@ instance ToJSON UnixTimestamp where toJSON (UnixTimestamp a) = toJSON (utcTimeToPOSIXSeconds a)++-- | Expected a given value as a string. Useful for @type@ fields in json.+data Expected (lit :: Symbol) = Expected++type role Expected phantom++instance (KnownSymbol lit) => Eq (Expected lit) where+ -- If the types match the values are always equal+ _ == _ = True++instance (KnownSymbol lit) => FromJSON (Expected lit) where+ parseJSON = withText "Expected" \s -> do+ unless (s == T.pack (symbolVal (Proxy @lit))) $ parseFail ("should be " <> symbolVal (Proxy @lit))+ pure Expected++instance (KnownSymbol lit) => ToJSON (Expected lit) where+ toJSON _ = String . T.pack $ symbolVal (Proxy @lit)++instance (KnownSymbol lit) => Show (Expected lit) where+ show _ = show $ symbolVal (Proxy @lit)
src/Web/Slack/Chat.hs view
@@ -15,6 +15,7 @@ import Web.FormUrlEncoded import Web.Slack.Conversation (ConversationId) import Web.Slack.Prelude+import Web.Slack.Types (Emoji) import Web.Slack.Util data PostMsg = PostMsg@@ -27,7 +28,7 @@ , postMsgUsername :: Maybe Text , postMsgAsUser :: Maybe Bool , postMsgIconUrl :: Maybe Text- , postMsgIconEmoji :: Maybe Text+ , postMsgIconEmoji :: Maybe Emoji , postMsgThreadTs :: Maybe Text , postMsgReplyBroadcast :: Maybe Bool }@@ -51,7 +52,7 @@ , postMsgReqUsername :: Maybe Text , postMsgReqAsUser :: Maybe Bool , postMsgReqIconUrl :: Maybe Text- , postMsgReqIconEmoji :: Maybe Text+ , postMsgReqIconEmoji :: Maybe Emoji , postMsgReqThreadTs :: Maybe Text , postMsgReqReplyBroadcast :: Maybe Bool }
src/Web/Slack/Common.hs view
@@ -27,6 +27,7 @@ Message (..), MessageType (..), SlackClientError (..),+ ResponseSlackError (..), SlackMessageText (..), ) where @@ -84,13 +85,28 @@ $(deriveJSON (jsonOpts "message") ''Message) +-- | Contains errors that can be returned by the slack API.+-- constrast with 'SlackClientError' which additionally+-- contains errors which occured during the network communication.+--+-- Includes an Object correponding to the @response_metadata@ field.+--+-- @since 2.1.0.0+data ResponseSlackError = ResponseSlackError+ { errorText :: Text+ , responseMetadata :: Object+ }+ deriving stock (Eq, Show, Generic)++instance NFData ResponseSlackError+ -- | -- Errors that can be triggered by a slack request. data SlackClientError = -- | errors from the network connection ServantError ClientError | -- | errors returned by the slack API- SlackError Text+ SlackError ResponseSlackError deriving stock (Eq, Generic, Show) instance NFData SlackClientError
+ src/Web/Slack/Experimental/Blocks/Builder.hs view
@@ -0,0 +1,105 @@+-- | A builder for Slack blocks+--+-- @since 2.1.0.0+module Web.Slack.Experimental.Blocks.Builder where++import Control.Monad.Writer.Strict+import Web.Slack.Experimental.Blocks.Types (SlackAccessory (..), SlackAction, SlackActionList, SlackBlock (..), SlackBlockId, SlackContent, SlackContext (..), SlackPlainTextOnly (..), SlackSection (..), SlackText, slackSectionWithText)+import Web.Slack.Prelude++type BlockBuilder = WriterT [SlackBlock]++-- | Runs a block builder, yielding a result+runBlockBuilder :: (Monad m) => BlockBuilder m () -> m [SlackBlock]+runBlockBuilder = execWriterT++-- | Header block.+--+-- The text is max 150 characters long.+--+-- <https://api.slack.com/reference/block-kit/blocks#header>+--+-- @since 2.1.0.0+headerBlock :: (Monad m) => SlackText -> BlockBuilder m ()+headerBlock = tell . pure . SlackBlockHeader . SlackPlainTextOnly++-- | Section block. Similar in concept to a p or div tag in HTML.+--+-- <https://api.slack.com/reference/block-kit/blocks#section>+--+-- @since 2.1.0.0+sectionBlock :: (Monad m) => SlackText -> BlockBuilder m ()+sectionBlock = tell . pure . SlackBlockSection . slackSectionWithText++-- | Section block. Similar in concept to a p or div tag in HTML.+--+-- <https://api.slack.com/reference/block-kit/blocks#section>+--+-- @since 2.1.0.0+sectionBlockWithFields :: (Monad m) => SlackText -> [SlackText] -> BlockBuilder m ()+sectionBlockWithFields text fields =+ tell+ $ pure+ $ SlackBlockSection+ $ SlackSection+ { slackSectionText = Just text+ , slackSectionBlockId = Nothing+ , slackSectionFields = Just fields+ , slackSectionAccessory = Nothing+ }++-- | Section block. Similar in concept to a p or div tag in HTML.+--+-- <https://api.slack.com/reference/block-kit/blocks#section>+--+-- @since 2.1.0.0+sectionBlockWithAccessory ::+ (Monad m) =>+ SlackText ->+ SlackAction ->+ BlockBuilder m ()+sectionBlockWithAccessory t b =+ tell+ $ pure+ $ SlackBlockSection+ SlackSection+ { slackSectionText = Just t+ , slackSectionBlockId = Nothing+ , slackSectionFields = Nothing+ , slackSectionAccessory = Just $ SlackButtonAccessory b+ }++-- | Horizontal line. Similar to an html @hr@ tag.+--+-- <https://api.slack.com/reference/block-kit/blocks#divider>+--+-- @since 2.1.0.0+dividerBlock :: (Monad m) => BlockBuilder m ()+dividerBlock = tell . pure $ SlackBlockDivider++-- | Context block: smaller, grey, text, and rendered inline. Like a @span@ in HTML.+--+-- <https://api.slack.com/reference/block-kit/blocks#context>+contextBlock :: (Monad m) => [SlackContent] -> BlockBuilder m ()+contextBlock = tell . pure . SlackBlockContext . SlackContext++-- | Inline container for interactive actions.+--+-- <https://api.slack.com/reference/block-kit/blocks#actions>+actionsBlock ::+ (Monad m) =>+ Maybe SlackBlockId ->+ SlackActionList ->+ BlockBuilder m ()+actionsBlock a b = tell . pure $ SlackBlockActions a b++-- | This is used for testing purposes so that you can paste these into the+-- Block Kit builder and have the expected format:+--+-- <https://app.slack.com/block-kit-builder>+--+-- @since 2.1.0.0+newtype BlockKitBuilderMessage = BlockKitBuilderMessage {blocks :: [SlackBlock]}++instance ToJSON BlockKitBuilderMessage where+ toJSON BlockKitBuilderMessage {blocks} = object [("blocks", toJSON blocks)]
src/Web/Slack/Experimental/Blocks/Types.hs view
@@ -13,6 +13,7 @@ import Web.Slack.AesonUtils import Web.Slack.Common (ConversationId, UserId) import Web.Slack.Prelude+import Web.Slack.Types (Emoji) -- | 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@@ -111,6 +112,8 @@ newtype OptionalSetting a = OptionalSetting {unOptionalSetting :: Maybe a} deriving newtype (Eq) +type role OptionalSetting representational+ -- | Allows using bare Strings without having to use 'setting' instance IsString (OptionalSetting String) where fromString = OptionalSetting . Just@@ -295,7 +298,7 @@ | RichItemChannel ConversationId | RichItemUser UserId RichStyle | RichItemLink RichLinkAttrs- | RichItemEmoji Text+ | RichItemEmoji Emoji | RichItemOther Text Value -- FIXME(jadel): date, usergroup, team, broadcast deriving stock (Eq, Show)@@ -395,13 +398,31 @@ } data SlackBlock- = SlackBlockSection SlackSection+ = -- | Section block. Similar in concept to a @p@ or @div@ tag in HTML.+ --+ -- <https://api.slack.com/reference/block-kit/blocks#section>+ SlackBlockSection SlackSection | SlackBlockImage SlackImage- | SlackBlockContext SlackContext- | SlackBlockDivider+ | -- | Context block: smaller, grey, text, and rendered inline. Like a @span@ in HTML.+ --+ -- <https://api.slack.com/reference/block-kit/blocks#context>+ SlackBlockContext SlackContext+ | -- | Horizontal line. Similar to an html @hr@ tag.+ --+ -- <https://api.slack.com/reference/block-kit/blocks#divider>+ SlackBlockDivider | SlackBlockRichText RichText- | SlackBlockActions (Maybe SlackBlockId) SlackActionList -- 1 to 5 elements- | SlackBlockHeader SlackPlainTextOnly -- max length 150+ | -- | Inline container for interactive actions.+ --+ -- <https://api.slack.com/reference/block-kit/blocks#actions>+ SlackBlockActions (Maybe SlackBlockId) SlackActionList -- 1 to 5 elements+ | -- | Header block.+ --+ -- The text is max 150 characters long.+ --+ -- <https://api.slack.com/reference/block-kit/blocks#header>+ SlackBlockHeader SlackPlainTextOnly+ | SlackBlockOther Object deriving stock (Eq) instance Show SlackBlock where@@ -420,6 +441,7 @@ ] show (SlackBlockRichText rt) = show rt show (SlackBlockHeader p) = show p+ show (SlackBlockOther o) = "SlackBlockOther " <> show o instance ToJSON SlackBlock where toJSON (SlackBlockSection SlackSection {..}) =@@ -454,6 +476,7 @@ [ "type" .= ("header" :: Text) , "text" .= slackPlainText ]+ toJSON (SlackBlockOther o) = Object o instance FromJSON SlackBlock where parseJSON = withObject "SlackBlock" $ \obj -> do@@ -500,7 +523,7 @@ (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']"+ _unk -> pure $ SlackBlockOther obj newtype SlackMessage = SlackMessage [SlackBlock] deriving newtype (Semigroup, Monoid, Eq)
src/Web/Slack/Experimental/Events/Types.hs view
@@ -144,6 +144,19 @@ -- Return the structured data with raw JSON preserved pure MessageAttachment {decoded = decodedContent, raw = ov} +-- | @bot_profile@ field of message events.+--+-- @since 2.1.0.0+data BotProfile = BotProfile+ { id :: Text+ , name :: Text+ , appId :: Text+ , teamId :: TeamId+ }+ deriving stock (Show)++$(deriveFromJSON snakeCaseOptions ''BotProfile)+ -- | <https://api.slack.com/events/message> -- and -- <https://api.slack.com/events/message/file_share>@@ -162,14 +175,20 @@ , 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.+ -- This is always the case for messages sent by modern Slack Apps, as+ -- opposed to legacy bots, which send 'BotMessageEvent' so they do not+ -- require an associated user ID. Modern Slack Apps have user IDs *and* App+ -- IDs *and* Bot IDs. -- -- See @botMessage.json@ golden parser test. , botId :: Maybe Text -- ^ Present if it's sent by a bot user+ , botProfile :: Maybe BotProfile+ -- ^ The bot profile of the app sending the message, if any.+ --+ -- This is (ill)-documented, sorta, at <https://api.slack.com/events/message/bot_message>.+ --+ -- @since 2.1.0.0 , attachments :: Maybe [MessageAttachment] -- ^ @since 2.0.0.3 -- Present if the message has attachments@@ -182,6 +201,13 @@ -- This is similar to a MessageEvent but sent by a bot, for example -- messages that Reacji Channeler sends. --+-- This only applies to *legacy* bots. Modern Slack apps do not send+-- @bot_message@ events; see the Slack documentation.+--+-- FIXME(jadel): remove this completely after March 31 2025 (?) or+-- March 2026 (?) since the things that send these will likely vanish.+-- <https://api.slack.com/changelog/2024-09-legacy-custom-bots-classic-apps-deprecation>+-- -- @since 2.0.0.2 data BotMessageEvent = BotMessageEvent { blocks :: Maybe [SlackBlock]@@ -273,6 +299,42 @@ deriving newtype (FromJSON) deriving stock (Show, Eq) +-- | Which tab of App Home is open.+--+-- @since 2.1.0.0+newtype AppHomeTab = AppHomeTab {unAppHomeTab :: Text}+ deriving stock (Show, Eq)+ deriving newtype (FromJSON, ToJSON)++-- | Home tab of App Home.+--+-- @since 2.1.0.0+appHomeTabHome :: AppHomeTab+appHomeTabHome = AppHomeTab "home"++-- | Messages tab of App Home.+--+-- @since 2.1.0.0+appHomeTabMessages :: AppHomeTab+appHomeTabMessages = AppHomeTab "messages"++-- | App Home Opened event: when a Slack bot's home page is opened.+--+-- <https://api.slack.com/events/app_home_opened>+--+-- See also: <https://api.slack.com/reference/app-home>+--+-- @since 2.1.0.0+data AppHomeOpenedEvent = AppHomeOpenedEvent+ { user :: UserId+ , channel :: ConversationId+ , tab :: AppHomeTab+ , eventTs :: Text+ }+ deriving stock (Show)++$(deriveFromJSON snakeCaseOptions ''AppHomeOpenedEvent)+ data Event = EventMessage MessageEvent | EventBotMessage BotMessageEvent@@ -285,6 +347,8 @@ EventChannelJoinMessage | EventChannelCreated ChannelCreatedEvent | EventChannelLeft ChannelLeftEvent+ | -- | @since 2.1.0.0+ EventAppHomeOpened AppHomeOpenedEvent | EventUnknown Value deriving stock (Show, Generic) @@ -302,6 +366,7 @@ ("message", Just "file_share") -> EventMessage <$> parseJSON @MessageEvent (Object obj) ("channel_created", Nothing) -> EventChannelCreated <$> parseJSON (Object obj) ("channel_left", Nothing) -> EventChannelLeft <$> parseJSON (Object obj)+ ("app_home_opened", Nothing) -> EventAppHomeOpened <$> parseJSON (Object obj) _ -> pure $ EventUnknown (Object obj) data EventCallback = EventCallback
+ src/Web/Slack/Experimental/Views.hs view
@@ -0,0 +1,160 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeOperators #-}++-- | Publishing views using the Blocks API.+--+-- This is used for App Home and modals.+--+-- <https://api.slack.com/methods/views.publish>+--+-- @since 2.1.0.0+module Web.Slack.Experimental.Views (+ -- * Types+ SlackView (..),+ HomeTabView (..),+ ModalView (..),+ Expected (..),++ -- * Requests and responses+ PublishReq (..),+ PublishResp (..),+ Api,+ viewsPublish,+) where++import Data.Aeson qualified as A+import Data.Aeson.KeyMap qualified as KM+import Servant.API (AuthProtect, FormUrlEncoded, JSON, Post, ReqBody, (:>))+import Servant.Client (ClientM, client)+import Servant.Client.Core (AuthenticatedRequest)+import Web.FormUrlEncoded (ToForm (..))+import Web.HttpApiData (ToHttpApiData (..))+import Web.Slack.AesonUtils (Expected (..), snakeCaseOptions, snakeCaseOptionsEatTrailingUnderscore, (.=!), (.=?))+import Web.Slack.Experimental.Blocks qualified as B+import Web.Slack.Experimental.Blocks.Types (SlackPlainTextOnly)+import Web.Slack.Internal (ResponseJSON (..), SlackConfig (..), mkSlackAuthenticateReq, run)+import Web.Slack.Pager (Response)+import Web.Slack.Prelude+import Web.Slack.Types (UserId)++-- | View definition for some Slack surface. Has an inner type of either+-- 'ModalView' or 'HomeTabView'.+--+-- <https://api.slack.com/reference/surfaces/views>+--+-- @since 2.1.0.0+data SlackView inner = SlackView+ { blocks :: Vector B.SlackBlock+ , privateMetadata :: Maybe Text+ , callbackId :: Maybe Text+ , externalId :: Maybe Text+ , inner :: inner+ }+ deriving stock (Show, Eq, Generic)++type role SlackView representational++instance (FromJSON inner) => FromJSON (SlackView inner) where+ parseJSON = withObject "SlackView" \o -> do+ blocks <- o .: "blocks"+ privateMetadata <- o .:? "private_metadata"+ callbackId <- o .:? "callback_id"+ externalId <- o .:? "external_id"+ inner <- parseJSON @inner (A.Object o)+ pure SlackView {..}++instance (ToJSON inner) => ToJSON (SlackView inner) where+ toJSON SlackView {..} = case toJSON inner of+ A.Object innerObj ->+ A.Object+ ( KM.fromList+ ( catMaybes+ [ "blocks" .=! blocks+ , "private_metadata" .=? privateMetadata+ , "callback_id" .=? callbackId+ , "external_id" .=? externalId+ ]+ )+ <> innerObj+ )+ _ -> error "inner of SlackView is not an object"++-- | @since 2.1.0.0+data HomeTabView = HomeTabView+ { type_ :: Expected "home"+ }+ deriving stock (Show, Eq, Generic)++$(deriveJSON snakeCaseOptionsEatTrailingUnderscore ''HomeTabView)++-- | Modal view+--+-- <https://api.slack.com/reference/surfaces/views#modal>+--+-- @since 2.1.0.0+data ModalView = ModalView+ { type_ :: Expected "modal"+ , title :: SlackPlainTextOnly+ -- ^ Title of the modal on the top left. Maximum length of 24 characters.+ , close :: Maybe SlackPlainTextOnly+ -- ^ Text appearing on the close button on the bottom-right of the modal.+ -- Maximum length of 24 characters.+ , submit :: Maybe SlackPlainTextOnly+ -- ^ Text appearing on the submit buttonn. Maximum length of 24 characters.+ -- Must be 'SlackPlainTextOnly'.+ , submitDisabled :: Maybe Bool+ -- ^ Whether one or more inputs must be filled before enabling the submit button.+ -- For configuration modals: <https://api.slack.com/reference/workflows/configuration-view>+ }+ deriving stock (Show, Eq, Generic)++$(deriveJSON snakeCaseOptionsEatTrailingUnderscore ''ModalView)++-- | Publishes the App Home view for a user.+--+-- <https://api.slack.com/methods/views.publish>+--+-- @since 2.1.0.0+data PublishReq = PublishReq+ { userId :: UserId+ -- ^ User to whom the view is being published.+ , view :: SlackView HomeTabView+ -- ^ View payload.+ }+ deriving stock (Show)++instance ToForm PublishReq where+ toForm PublishReq {..} =+ [ ("user_id" :: Text, toQueryParam userId)+ , ("view", toQueryParam . decodeUtf8 $ A.encode view)+ ]++-- | @since 2.1.0.0+data PublishResp = PublishResp+ { view :: SlackView HomeTabView+ }+ deriving stock (Show)++$(deriveJSON snakeCaseOptions ''PublishResp)++-- | @since 2.1.0.0+type Api =+ "views.publish"+ :> AuthProtect "token"+ :> ReqBody '[FormUrlEncoded] PublishReq+ :> Post '[JSON] (ResponseJSON PublishResp)++-- | Publishes the App Home view for a user.+--+-- <https://api.slack.com/methods/views.publish>+--+-- @since 2.1.0.0+viewsPublish :: SlackConfig -> PublishReq -> IO (Response PublishResp)+viewsPublish slackConfig req = do+ let authR = mkSlackAuthenticateReq slackConfig+ run (viewsPublish_ authR req) . slackConfigManager $ slackConfig++viewsPublish_ :: AuthenticatedRequest (AuthProtect "token") -> PublishReq -> ClientM (ResponseJSON PublishResp)+viewsPublish_ = client (Proxy @Api)
src/Web/Slack/Internal.hs view
@@ -2,10 +2,11 @@ module Web.Slack.Internal where import Data.Aeson (Value (..))-import Network.HTTP.Client (Manager)-import Servant.API hiding (addHeader) -- import Servant.Client.Core +import Data.Aeson.KeyMap qualified as KM+import Network.HTTP.Client (Manager)+import Servant.API hiding (addHeader) 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@@ -17,15 +18,10 @@ , 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)+newtype ResponseJSON a = ResponseJSON (Either Common.ResponseSlackError a)+ deriving stock (Show) instance (FromJSON a) => FromJSON (ResponseJSON a) where parseJSON = withObject "Response" $ \o -> do@@ -33,7 +29,10 @@ ResponseJSON <$> if ok then Right <$> parseJSON (Object o)- else Left . ResponseSlackError <$> o .: "error"+ else do+ err <- o .: "error"+ meta <- o .:? "response_metadata"+ pure $ Left $ Common.ResponseSlackError {errorText = err, responseMetadata = (fromMaybe KM.empty meta)} mkSlackAuthenticateReq :: SlackConfig -> AuthenticatedRequest (AuthProtect "token") mkSlackAuthenticateReq = (`mkAuthenticatedRequest` authenticateReq) . slackConfigToken@@ -59,6 +58,6 @@ 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 (Right (ResponseJSON (Left err))) =+ Left (Common.SlackError err) unnestErrors (Left slackErr) = Left (Common.ServantError slackErr)
+ src/Web/Slack/Reactions.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeOperators #-}++-- | API methods relating to reactions:+--+-- - <https://api.slack.com/methods/reactions.add>+-- - <https://api.slack.com/methods/reactions.get>+-- - <https://api.slack.com/methods/reactions.list>+-- - <https://api.slack.com/methods/reactions.remove>+--+-- @since 2.1.0.0+module Web.Slack.Reactions (AddReq (..), AddResp (..), Api, reactionsAdd, reactionsAdd_) where++import Data.Aeson qualified as A+import Servant.API (AuthProtect, FormUrlEncoded, JSON, Post, ReqBody, (:>))+import Servant.Client (ClientM, client)+import Servant.Client.Core (AuthenticatedRequest)+import Web.FormUrlEncoded (ToForm (..))+import Web.Slack.Internal (ResponseJSON (..), SlackConfig (..), mkSlackAuthenticateReq, run)+import Web.Slack.Pager (Response)+import Web.Slack.Prelude+import Web.Slack.Types (ConversationId (..), Emoji (..))++-- | Add a reaction to a message.+--+-- <https://api.slack.com/methods/reactions.add>+--+-- @since 2.1.0.0+data AddReq = AddReq+ { channel :: ConversationId+ -- ^ Conversation in which to react.+ , name :: Emoji+ -- ^ Emoji name. For Unicode emoji, this is the name, not the character.+ , timestamp :: Text+ -- ^ Message @ts@ to react to.+ }+ deriving stock (Show, Eq)++instance ToForm AddReq where+ toForm AddReq {..} =+ [ ("channel", unConversationId channel)+ , ("name", unEmoji name)+ , ("timestamp", timestamp)+ ]++-- | Response to @reactions.add@. Slack doesn't send us anything here.+--+-- @since 2.1.0.0+data AddResp = AddResp+ deriving stock (Show, Eq)++instance ToJSON AddResp where+ toJSON _ = A.object []++instance FromJSON AddResp where+ parseJSON = A.withObject "Reactions.AddResp" \_ -> pure AddResp++type Api =+ "reactions.add"+ :> AuthProtect "token"+ :> ReqBody '[FormUrlEncoded] AddReq+ :> Post '[JSON] (ResponseJSON AddResp)++-- | Adds a reaction to a message.+--+-- <https://api.slack.com/methods/reactions.add>+--+-- @since 2.1.0.0+reactionsAdd :: SlackConfig -> AddReq -> IO (Response AddResp)+reactionsAdd slackConfig req = do+ let authR = mkSlackAuthenticateReq slackConfig+ run (reactionsAdd_ authR req) . slackConfigManager $ slackConfig++reactionsAdd_ :: AuthenticatedRequest (AuthProtect "token") -> AddReq -> ClientM (ResponseJSON AddResp)+reactionsAdd_ = client (Proxy @Api)++-- FIXME(jadel): reactions.remove, reactions.get, reactions.list
src/Web/Slack/Types.hs view
@@ -10,6 +10,7 @@ UserId (..), ConversationId (..), TeamId (..),+ Emoji (..), Cursor (..), SlackTimestamp (..), mkSlackTimestamp,@@ -48,6 +49,13 @@ newtype TeamId = TeamId {unTeamId :: Text} deriving stock (Eq, Ord, Generic, Show) deriving newtype (NFData, Hashable, FromJSON, ToJSON, ToHttpApiData)++-- | Slack emoji name.+--+-- @since 2.1.0.0+newtype Emoji = Emoji {unEmoji :: Text}+ deriving stock (Show, Eq)+ deriving newtype (NFData, Hashable, IsString, FromJSON, ToJSON, ToHttpApiData) -- | Message text in the format returned by Slack, -- see https://api.slack.com/docs/message-formatting
src/Web/Slack/User.hs view
@@ -19,13 +19,14 @@ import Web.Slack.Common import Web.Slack.Pager.Types (PagedRequest (..), PagedResponse (..), ResponseMetadata) import Web.Slack.Prelude+import Web.Slack.Types (Emoji) import Web.Slack.Util -- | See <https://api.slack.com/types/user> data Profile = Profile { profileAvatarHash :: Maybe Text , profileStatusText :: Maybe Text- , profileStatusEmoji :: Maybe Text+ , profileStatusEmoji :: Maybe Emoji , profileRealName :: Maybe Text , profileDisplayName :: Maybe Text , profileRealNameNormalized :: Maybe Text
tests/Web/Slack/Experimental/BlocksSpec.hs view
@@ -1,60 +1,12 @@ 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.Builder (BlockKitBuilderMessage (..), actionsBlock, contextBlock, dividerBlock, headerBlock, runBlockBuilder, sectionBlock, sectionBlockWithAccessory, sectionBlockWithFields) 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 = tell . pure . SlackBlockSection . slackSectionWithText--sectionBlockWithFields :: SlackText -> [SlackText] -> WriterT [SlackBlock] Identity ()-sectionBlockWithFields text fields =- tell- $ pure- $ SlackBlockSection- $ SlackSection- { slackSectionText = Just text- , slackSectionBlockId = Nothing- , slackSectionFields = Just fields- , slackSectionAccessory = Nothing- }--sectionBlockWithAccessory ::- SlackText ->- SlackAction ->- WriterT [SlackBlock] Identity ()-sectionBlockWithAccessory t b =- tell- $ pure- $ SlackBlockSection- SlackSection- { slackSectionText = Just t- , slackSectionBlockId = Nothing- , slackSectionFields = Nothing- , slackSectionAccessory = 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" @@ -67,7 +19,8 @@ slackShowBlockFormat :: BlockKitBuilderMessage slackShowBlockFormat = BlockKitBuilderMessage- $ execWriter+ $ runIdentity+ . runBlockBuilder $ do headerBlock ("Blah: " <> list ["a", "b", "c"]) dividerBlock@@ -104,14 +57,6 @@ 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
tests/Web/Slack/Experimental/Events/TypesSpec.hs view
@@ -33,4 +33,6 @@ , "github_notification_ts_string" , "github_with_link" , "non_spec_attachment"+ , "app_home_opened_home"+ , "app_home_opened_messages" ]
+ tests/Web/Slack/Experimental/ViewsSpec.hs view
@@ -0,0 +1,25 @@+{-# OPTIONS_GHC -Wno-orphans #-}++module Web.Slack.Experimental.ViewsSpec where++import JSONGolden+import TestImport+import Web.Slack.Experimental.Views++spec :: Spec+spec = describe "Slack views" do+ describe "SlackView ModalView" do+ describe "FromJSON" do+ mapM_+ (oneGoldenTestDecode @(SlackView ModalView))+ -- Taken from: https://api.slack.com/reference/surfaces/views#modal__modal-view-example+ ["modalView"]++ describe "SlackView HomeTabView" do+ mapM_+ (oneGoldenTestDecode @(SlackView HomeTabView))+ -- https://api.slack.com/reference/surfaces/views#home-example+ ["homeTabView"]++ describe "views.publish response" do+ oneGoldenTestDecode @PublishResp "views.publish example"
+ tests/Web/Slack/InternalSpec.hs view
@@ -0,0 +1,20 @@+module Web.Slack.InternalSpec where++import JSONGolden+import TestImport+import Web.Slack.Internal++-- | Parses nothing and succeeds!+data NoJSONExpectations = NoJSONExpectations+ deriving stock (Show)++instance FromJSON NoJSONExpectations where+ parseJSON _ = pure NoJSONExpectations++spec :: Spec+spec = describe "Common infrastructure" do+ describe "Response parsing" do+ -- FIXME(jadel): discards warnings for successful responses! seems like we+ -- need to improve this API+ oneGoldenTestDecode @(ResponseJSON NoJSONExpectations) "metadata_example"+ oneGoldenTestDecode @(ResponseJSON NoJSONExpectations) "failed_view_publish"
+ tests/golden/PublishResp/views.publish example.golden view
@@ -0,0 +1,18 @@+PublishResp+ { view = SlackView+ { blocks =+ [ SlackSection+ { slackSectionText = Just "A simple section with some sample sentence."+ , slackSectionBlockId = Just+ ( NonEmptyText "2WGp9" )+ , slackSectionFields = Nothing+ , slackSectionAccessory = Nothing+ }+ ]+ , privateMetadata = Just "Shh it is a secret"+ , callbackId = Just "identify_your_home_tab"+ , externalId = Just ""+ , inner = HomeTabView+ { type_ = "home" }+ }+ }
+ tests/golden/PublishResp/views.publish example.json view
@@ -0,0 +1,34 @@+{+ "ok": true,+ "view": {+ "id": "VMHU10V25",+ "team_id": "T8N4K1JN",+ "type": "home",+ "close": null,+ "submit": null,+ "blocks": [+ {+ "type": "section",+ "block_id": "2WGp9",+ "text": {+ "type": "mrkdwn",+ "text": "A simple section with some sample sentence.",+ "verbatim": false+ }+ }+ ],+ "private_metadata": "Shh it is a secret",+ "callback_id": "identify_your_home_tab",+ "state": {+ "values": {}+ },+ "hash": "156772938.1827394",+ "clear_on_close": false,+ "notify_on_close": false,+ "root_view_id": "VMHU10V25",+ "previous_view_id": null,+ "app_id": "AA4928AQ",+ "external_id": "",+ "bot_id": "BA13894H"+ }+}
+ tests/golden/ResponseJSON/failed_view_publish.golden view
@@ -0,0 +1,17 @@+ResponseJSON+ ( Left+ ( ResponseSlackError+ { errorText = "invalid_arguments"+ , responseMetadata = fromList+ [+ ( "messages"+ , Array+ [ String "[ERROR] failed to match all allowed schemas [json-pointer:/view]"+ , String "[ERROR] must provide an object [json-pointer:/view]"+ , String "[ERROR] must provide an object [json-pointer:/view]"+ ]+ )+ ]+ }+ )+ )
+ tests/golden/ResponseJSON/failed_view_publish.json view
@@ -0,0 +1,11 @@+{+ "ok": false,+ "error": "invalid_arguments",+ "response_metadata": {+ "messages": [+ "[ERROR] failed to match all allowed schemas [json-pointer:/view]",+ "[ERROR] must provide an object [json-pointer:/view]",+ "[ERROR] must provide an object [json-pointer:/view]"+ ]+ }+}
+ tests/golden/ResponseJSON/metadata_example.golden view
@@ -0,0 +1,1 @@+ResponseJSON ( Right NoJSONExpectations )
+ tests/golden/ResponseJSON/metadata_example.json view
@@ -0,0 +1,14 @@+{+ "ok": true,+ "warnings": [+ "missing_charset"+ ],+ "response_metadata": {+ "warnings": [+ "missing_charset"+ ],+ "messages": [+ "[WARN] A Content-Type HTTP header was presented but did not declare a charset, such as a 'utf-8'"+ ]+ }+}
+ tests/golden/SlackView/homeTabView.golden view
@@ -0,0 +1,16 @@+SlackView+ { blocks =+ [ SlackSection+ { slackSectionText = Just "A stack of blocks for the sample Block Kit Home tab."+ , slackSectionBlockId = Nothing+ , slackSectionFields = Nothing+ , slackSectionAccessory = Nothing+ }+ , "actions(Nothing) = ["SlackActionId {unSlackActionId = NonEmptyText \"oops our json decoder requires this\"} [button SlackButtonText (NonEmptyText \"Action A\")], SlackActionId {unSlackActionId = NonEmptyText \"oops our json decoder requires this 2\"} [button SlackButtonText (NonEmptyText \"Action B\")]"]"+ ]+ , privateMetadata = Nothing+ , callbackId = Nothing+ , externalId = Nothing+ , inner = HomeTabView+ { type_ = "home" }+ }
+ tests/golden/SlackView/homeTabView.json view
@@ -0,0 +1,35 @@+{+ "type": "home",+ "blocks": [+ {+ "type": "section",+ "text": {+ "type": "mrkdwn",+ "text": "A stack of blocks for the sample Block Kit Home tab."+ }+ },+ {+ "type": "actions",+ "elements": [+ {+ "type": "button",+ "action_id": "oops our json decoder requires this",+ "text": {+ "type": "plain_text",+ "text": "Action A",+ "emoji": true+ }+ },+ {+ "type": "button",+ "action_id": "oops our json decoder requires this 2",+ "text": {+ "type": "plain_text",+ "text": "Action B",+ "emoji": true+ }+ }+ ]+ }+ ]+}
+ tests/golden/SlackView/modalView.golden view
@@ -0,0 +1,86 @@+SlackView+ { blocks =+ [ SlackSection+ { slackSectionText = Just "It's Block Kit...but _in a modal_"+ , slackSectionBlockId = Just+ ( NonEmptyText "section1" )+ , slackSectionFields = Nothing+ , slackSectionAccessory = Just SlackActionId+ { unSlackActionId = NonEmptyText "button_abc" }+ [ button SlackButtonText+ ( NonEmptyText "Click me" )+ ]+ }+ , SlackBlockOther fromList+ [+ ( "element"+ , Object+ ( fromList+ [+ ( "action_id"+ , String "input1"+ )+ ,+ ( "multiline"+ , Bool False+ )+ ,+ ( "placeholder"+ , Object+ ( fromList+ [+ ( "text"+ , String "Type in here"+ )+ ,+ ( "type"+ , String "plain_text"+ )+ ]+ )+ )+ ,+ ( "type"+ , String "plain_text_input"+ )+ ]+ )+ )+ ,+ ( "label"+ , Object+ ( fromList+ [+ ( "text"+ , String "Input label"+ )+ ,+ ( "type"+ , String "plain_text"+ )+ ]+ )+ )+ ,+ ( "optional"+ , Bool False+ )+ ,+ ( "type"+ , String "input"+ )+ ]+ ]+ , privateMetadata = Just "Shhhhhhhh"+ , callbackId = Just "view_identifier_12"+ , externalId = Nothing+ , inner = ModalView+ { type_ = "modal"+ , title = SlackPlainTextOnly "Modal title"+ , close = Just+ ( SlackPlainTextOnly "Cancel" )+ , submit = Just+ ( SlackPlainTextOnly "Save" )+ , submitDisabled = Nothing+ }+ }
+ tests/golden/SlackView/modalView.json view
@@ -0,0 +1,54 @@+{+ "type": "modal",+ "title": {+ "type": "plain_text",+ "text": "Modal title"+ },+ "blocks": [+ {+ "type": "section",+ "text": {+ "type": "mrkdwn",+ "text": "It's Block Kit...but _in a modal_"+ },+ "block_id": "section1",+ "accessory": {+ "type": "button",+ "text": {+ "type": "plain_text",+ "text": "Click me"+ },+ "action_id": "button_abc",+ "value": "Button value",+ "style": "danger"+ }+ },+ {+ "type": "input",+ "label": {+ "type": "plain_text",+ "text": "Input label"+ },+ "element": {+ "type": "plain_text_input",+ "action_id": "input1",+ "placeholder": {+ "type": "plain_text",+ "text": "Type in here"+ },+ "multiline": false+ },+ "optional": false+ }+ ],+ "close": {+ "type": "plain_text",+ "text": "Cancel"+ },+ "submit": {+ "type": "plain_text",+ "text": "Save"+ },+ "private_metadata": "Shhhhhhhh",+ "callback_id": "view_identifier_12"+}
+ tests/golden/SlackWebhookEvent/app_home_opened_home.golden view
@@ -0,0 +1,23 @@+EventEventCallback+ ( EventCallback+ { eventId = EventId+ { unEventId = "Ev08EUKK3342" }+ , teamId = TeamId+ { unTeamId = "T043DB835ML" }+ , eventTime = MkSystemTime+ { systemSeconds = 1740431544+ , systemNanoseconds = 0+ }+ , event = EventAppHomeOpened+ ( AppHomeOpenedEvent+ { user = UserId+ { unUserId = "U043H11ES4V" }+ , channel = ConversationId+ { unConversationId = "D0442US94JD" }+ , tab = AppHomeTab+ { unAppHomeTab = "home" }+ , eventTs = "1740431544.556817"+ }+ )+ }+ )
+ tests/golden/SlackWebhookEvent/app_home_opened_home.json view
@@ -0,0 +1,25 @@+{+ "token": "aaaa",+ "team_id": "T043DB835ML",+ "api_app_id": "A0442TUPHGR",+ "event": {+ "type": "app_home_opened",+ "user": "U043H11ES4V",+ "channel": "D0442US94JD",+ "tab": "home",+ "event_ts": "1740431544.556817"+ },+ "type": "event_callback",+ "event_id": "Ev08EUKK3342",+ "event_time": 1740431544,+ "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/app_home_opened_messages.golden view
@@ -0,0 +1,23 @@+EventEventCallback+ ( EventCallback+ { eventId = EventId+ { unEventId = "Ev08EUG3TZ0S" }+ , teamId = TeamId+ { unTeamId = "T043DB835ML" }+ , eventTime = MkSystemTime+ { systemSeconds = 1740430328+ , systemNanoseconds = 0+ }+ , event = EventAppHomeOpened+ ( AppHomeOpenedEvent+ { user = UserId+ { unUserId = "U043H11ES4V" }+ , channel = ConversationId+ { unConversationId = "D0442US94JD" }+ , tab = AppHomeTab+ { unAppHomeTab = "messages" }+ , eventTs = "1740430328.659101"+ }+ )+ }+ )
+ tests/golden/SlackWebhookEvent/app_home_opened_messages.json view
@@ -0,0 +1,25 @@+{+ "token": "aaaa",+ "team_id": "T043DB835ML",+ "api_app_id": "A0442TUPHGR",+ "event": {+ "type": "app_home_opened",+ "user": "U043H11ES4V",+ "channel": "D0442US94JD",+ "tab": "messages",+ "event_ts": "1740430328.659101"+ },+ "type": "event_callback",+ "event_id": "Ev08EUG3TZ0S",+ "event_time": 1740430328,+ "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/botMessage.golden view
@@ -37,6 +37,15 @@ , threadTs = Just "1664216109.798919" , appId = Just "A0442TUPHGR" , botId = Just "B0439P161B9"+ , botProfile = Just+ ( BotProfile+ { id = "B0439P161B9"+ , name = "Slacklinker dev"+ , appId = "A0442TUPHGR"+ , teamId = TeamId+ { unTeamId = "T043DB835ML" }+ }+ ) , attachments = Nothing } )
tests/golden/SlackWebhookEvent/circleci.golden view
@@ -50,6 +50,15 @@ , threadTs = Nothing , appId = Just "A56789123" , botId = Just "B01234687"+ , botProfile = Just+ ( BotProfile+ { id = "B01234687"+ , name = "CircleCI integration"+ , appId = "A56789123"+ , teamId = TeamId+ { unTeamId = "T0123456789" }+ }+ ) , attachments = Nothing } )
tests/golden/SlackWebhookEvent/email_message.golden view
@@ -37,6 +37,7 @@ , threadTs = Nothing , appId = Nothing , botId = Just "B012346789"+ , botProfile = Nothing , attachments = Nothing } )
tests/golden/SlackWebhookEvent/forwarded_message.golden view
@@ -22,6 +22,7 @@ , threadTs = Nothing , appId = Nothing , botId = Nothing+ , botProfile = Nothing , attachments = Just [ MessageAttachment { decoded = Just
tests/golden/SlackWebhookEvent/github_notification.golden view
@@ -22,6 +22,15 @@ , threadTs = Nothing , appId = Just "A01BP7R4KNY" , botId = Just "B07LDR01Z63"+ , botProfile = Just+ ( BotProfile+ { id = "B07LDR01Z63"+ , name = "GitHub"+ , appId = "A01BP7R4KNY"+ , teamId = TeamId+ { unTeamId = "T043DB835ML" }+ }+ ) , attachments = Just [ MessageAttachment { decoded = Just
tests/golden/SlackWebhookEvent/github_notification_ts_string.golden view
@@ -22,6 +22,15 @@ , threadTs = Nothing , appId = Just "A01BP7R4KNY" , botId = Just "B07LDR01Z63"+ , botProfile = Just+ ( BotProfile+ { id = "B07LDR01Z63"+ , name = "GitHub"+ , appId = "A01BP7R4KNY"+ , teamId = TeamId+ { unTeamId = "T043DB835ML" }+ }+ ) , attachments = Just [ MessageAttachment { decoded = Just
tests/golden/SlackWebhookEvent/github_with_link.golden view
@@ -22,6 +22,15 @@ , threadTs = Nothing , appId = Just "A01BP7R4KNY" , botId = Just "B07LDR01Z63"+ , botProfile = Just+ ( BotProfile+ { id = "B07LDR01Z63"+ , name = "GitHub"+ , appId = "A01BP7R4KNY"+ , teamId = TeamId+ { unTeamId = "T043DB835ML" }+ }+ ) , attachments = Just [ MessageAttachment { decoded = Just
tests/golden/SlackWebhookEvent/link.golden view
@@ -41,6 +41,7 @@ , threadTs = Nothing , appId = Nothing , botId = Nothing+ , botProfile = Nothing , attachments = Nothing } )
tests/golden/SlackWebhookEvent/messageExample.golden view
@@ -37,6 +37,7 @@ , threadTs = Nothing , appId = Nothing , botId = Nothing+ , botProfile = Nothing , attachments = Nothing } )
tests/golden/SlackWebhookEvent/messageIm.golden view
@@ -37,6 +37,7 @@ , threadTs = Nothing , appId = Nothing , botId = Nothing+ , botProfile = Nothing , attachments = Nothing } )
@@ -52,6 +52,7 @@ , threadTs = Nothing , appId = Nothing , botId = Nothing+ , botProfile = Nothing , attachments = Nothing } )
@@ -27,6 +27,7 @@ , threadTs = Nothing , appId = Nothing , botId = Nothing+ , botProfile = Nothing , attachments = Nothing } )
tests/golden/SlackWebhookEvent/message_rich_text.golden view
@@ -26,7 +26,10 @@ , rsItalic = False } )- , RichItemEmoji "cat"+ , RichItemEmoji+ ( Emoji+ { unEmoji = "cat" }+ ) , RichItemText " " ( RichStyle { rsBold = False@@ -79,6 +82,7 @@ , threadTs = Nothing , appId = Nothing , botId = Nothing+ , botProfile = Nothing , attachments = Nothing } )
tests/golden/SlackWebhookEvent/non_spec_attachment.golden view
@@ -22,6 +22,15 @@ , threadTs = Nothing , appId = Just "A01BP7R4KNY" , botId = Just "B07LDR01Z63"+ , botProfile = Just+ ( BotProfile+ { id = "B07LDR01Z63"+ , name = "GitHub"+ , appId = "A01BP7R4KNY"+ , teamId = TeamId+ { unTeamId = "T043DB835ML" }+ }+ ) , attachments = Just [ MessageAttachment { decoded = Just
@@ -37,6 +37,7 @@ , threadTs = Nothing , appId = Nothing , botId = Nothing+ , botProfile = Nothing , attachments = Just [ MessageAttachment { decoded = Just
@@ -22,6 +22,7 @@ , threadTs = Nothing , appId = Nothing , botId = Nothing+ , botProfile = Nothing , attachments = Just [ MessageAttachment { decoded = Just
tests/golden/SlackWebhookEvent/slackbotIm.golden view
@@ -46,6 +46,7 @@ , threadTs = Nothing , appId = Nothing , botId = Nothing+ , botProfile = Nothing , attachments = Nothing } )