packages feed

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 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                 }             )
tests/golden/SlackWebhookEvent/message_file_share.golden view
@@ -52,6 +52,7 @@                 , threadTs = Nothing                 , appId = Nothing                 , botId = Nothing+                , botProfile = Nothing                 , attachments = Nothing                 }             )
tests/golden/SlackWebhookEvent/message_file_share_slack_connect.golden view
@@ -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
tests/golden/SlackWebhookEvent/share_with_message.golden view
@@ -37,6 +37,7 @@                 , threadTs = Nothing                 , appId = Nothing                 , botId = Nothing+                , botProfile = Nothing                 , attachments = Just                     [ MessageAttachment                         { decoded = Just
tests/golden/SlackWebhookEvent/share_without_message.golden view
@@ -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                 }             )