slack-web 2.2.0.0 → 2.3.0.0
raw patch · 32 files changed
+854/−159 lines, 32 filesdep +data-defaultdep −data-default-classnew-uploaderPVP ok
version bump matches the API change (PVP)
Dependencies added: data-default
Dependencies removed: data-default-class
API changes (from Hackage documentation)
+ Web.Slack.Conversation: MembersReq :: ConversationId -> Maybe Cursor -> Maybe Int -> MembersReq
+ Web.Slack.Conversation: MembersRsp :: [UserId] -> Maybe ResponseMetadata -> MembersRsp
+ Web.Slack.Conversation: [membersReqChannel] :: MembersReq -> ConversationId
+ Web.Slack.Conversation: [membersReqCursor] :: MembersReq -> Maybe Cursor
+ Web.Slack.Conversation: [membersReqLimit] :: MembersReq -> Maybe Int
+ Web.Slack.Conversation: [membersRspMembers] :: MembersRsp -> [UserId]
+ Web.Slack.Conversation: [membersRspResponseMetadata] :: MembersRsp -> Maybe ResponseMetadata
+ Web.Slack.Conversation: conversationsMembers :: SlackConfig -> MembersReq -> IO (Response MembersRsp)
+ Web.Slack.Conversation: conversationsMembers_ :: AuthenticatedRequest (AuthProtect "token") -> MembersReq -> ClientM (ResponseJSON MembersRsp)
+ Web.Slack.Conversation: data MembersReq
+ Web.Slack.Conversation: data MembersRsp
+ Web.Slack.Conversation: instance Control.DeepSeq.NFData Web.Slack.Conversation.MembersReq
+ Web.Slack.Conversation: instance Control.DeepSeq.NFData Web.Slack.Conversation.MembersRsp
+ Web.Slack.Conversation: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Conversation.MembersReq
+ Web.Slack.Conversation: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Conversation.MembersRsp
+ Web.Slack.Conversation: instance GHC.Classes.Eq Web.Slack.Conversation.MembersReq
+ Web.Slack.Conversation: instance GHC.Classes.Eq Web.Slack.Conversation.MembersRsp
+ Web.Slack.Conversation: instance GHC.Generics.Generic Web.Slack.Conversation.MembersReq
+ Web.Slack.Conversation: instance GHC.Generics.Generic Web.Slack.Conversation.MembersRsp
+ Web.Slack.Conversation: instance GHC.Show.Show Web.Slack.Conversation.MembersReq
+ Web.Slack.Conversation: instance GHC.Show.Show Web.Slack.Conversation.MembersRsp
+ Web.Slack.Conversation: instance Web.Internal.FormUrlEncoded.ToForm Web.Slack.Conversation.MembersReq
+ Web.Slack.Conversation: instance Web.Slack.Pager.Types.PagedRequest Web.Slack.Conversation.MembersReq
+ Web.Slack.Conversation: instance Web.Slack.Pager.Types.PagedResponse Web.Slack.Conversation.MembersRsp
+ Web.Slack.Conversation: mkMembersReq :: ConversationId -> MembersReq
+ Web.Slack.Experimental.Blocks: RichItemMessageMention :: RichMessageMention -> RichItem
+ Web.Slack.Experimental.Blocks: RichMessageMention :: ConversationId -> Text -> Maybe Text -> Maybe Text -> RichMessageMention
+ Web.Slack.Experimental.Blocks: RichTextSection :: [RichItem] -> RichTextSection
+ Web.Slack.Experimental.Blocks: RichTextSectionItemList :: [RichTextSection] -> RichTextSectionItem
+ Web.Slack.Experimental.Blocks: RichTextSectionItemPreformatted :: [RichItem] -> RichTextSectionItem
+ Web.Slack.Experimental.Blocks: RichTextSectionItemQuote :: [RichItem] -> RichTextSectionItem
+ Web.Slack.Experimental.Blocks: [rmmChannelId] :: RichMessageMention -> ConversationId
+ Web.Slack.Experimental.Blocks: [rmmMessageTs] :: RichMessageMention -> Text
+ Web.Slack.Experimental.Blocks: [rmmThreadTs] :: RichMessageMention -> Maybe Text
+ Web.Slack.Experimental.Blocks: [rmmUrl] :: RichMessageMention -> Maybe Text
+ Web.Slack.Experimental.Blocks: data RichMessageMention
+ Web.Slack.Experimental.Blocks: newtype RichTextSection
+ Web.Slack.Experimental.Blocks.Types: RichItemMessageMention :: RichMessageMention -> RichItem
+ Web.Slack.Experimental.Blocks.Types: RichMessageMention :: ConversationId -> Text -> Maybe Text -> Maybe Text -> RichMessageMention
+ Web.Slack.Experimental.Blocks.Types: RichTextSection :: [RichItem] -> RichTextSection
+ Web.Slack.Experimental.Blocks.Types: RichTextSectionItemList :: [RichTextSection] -> RichTextSectionItem
+ Web.Slack.Experimental.Blocks.Types: RichTextSectionItemPreformatted :: [RichItem] -> RichTextSectionItem
+ Web.Slack.Experimental.Blocks.Types: RichTextSectionItemQuote :: [RichItem] -> RichTextSectionItem
+ Web.Slack.Experimental.Blocks.Types: [rmmChannelId] :: RichMessageMention -> ConversationId
+ Web.Slack.Experimental.Blocks.Types: [rmmMessageTs] :: RichMessageMention -> Text
+ Web.Slack.Experimental.Blocks.Types: [rmmThreadTs] :: RichMessageMention -> Maybe Text
+ Web.Slack.Experimental.Blocks.Types: [rmmUrl] :: RichMessageMention -> Maybe Text
+ Web.Slack.Experimental.Blocks.Types: data RichMessageMention
+ Web.Slack.Experimental.Blocks.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Blocks.Types.RichTextSection
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.RichMessageMention
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Classes.Eq Web.Slack.Experimental.Blocks.Types.RichTextSection
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.RichMessageMention
+ Web.Slack.Experimental.Blocks.Types: instance GHC.Show.Show Web.Slack.Experimental.Blocks.Types.RichTextSection
+ Web.Slack.Experimental.Blocks.Types: newtype RichTextSection
+ Web.Slack.Experimental.Events.Types: ChannelSharedEvent :: TeamId -> ConversationId -> Text -> ChannelSharedEvent
+ Web.Slack.Experimental.Events.Types: ChannelUnsharedEvent :: TeamId -> ConversationId -> Bool -> Text -> ChannelUnsharedEvent
+ Web.Slack.Experimental.Events.Types: EventChannelShared :: ChannelSharedEvent -> Event
+ Web.Slack.Experimental.Events.Types: EventChannelUnshared :: ChannelUnsharedEvent -> Event
+ Web.Slack.Experimental.Events.Types: EventMessageDeleted :: MessageDeletedEvent -> Event
+ Web.Slack.Experimental.Events.Types: MessageDeletedEvent :: ConversationId -> Text -> Text -> MessageDeletedEvent
+ Web.Slack.Experimental.Events.Types: [connectedTeamId] :: ChannelSharedEvent -> TeamId
+ Web.Slack.Experimental.Events.Types: [deletedTs] :: MessageDeletedEvent -> Text
+ Web.Slack.Experimental.Events.Types: [isExtShared] :: ChannelUnsharedEvent -> Bool
+ Web.Slack.Experimental.Events.Types: [previouslyConnectedTeamId] :: ChannelUnsharedEvent -> TeamId
+ Web.Slack.Experimental.Events.Types: data ChannelSharedEvent
+ Web.Slack.Experimental.Events.Types: data ChannelUnsharedEvent
+ Web.Slack.Experimental.Events.Types: data MessageDeletedEvent
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Events.Types.ChannelSharedEvent
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Events.Types.ChannelUnsharedEvent
+ Web.Slack.Experimental.Events.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Experimental.Events.Types.MessageDeletedEvent
+ Web.Slack.Experimental.Events.Types: instance GHC.Show.Show Web.Slack.Experimental.Events.Types.ChannelSharedEvent
+ Web.Slack.Experimental.Events.Types: instance GHC.Show.Show Web.Slack.Experimental.Events.Types.ChannelUnsharedEvent
+ Web.Slack.Experimental.Events.Types: instance GHC.Show.Show Web.Slack.Experimental.Events.Types.MessageDeletedEvent
+ Web.Slack.User: [userIsBot] :: User -> Maybe Bool
- Web.Slack.Conversation: type Api = "conversations.info" :> AuthProtect "token" :> ReqBody '[FormUrlEncoded] InfoReq :> Post '[JSON] ResponseJSON InfoRsp :<|> "conversations.join" :> AuthProtect "token" :> ReqBody '[FormUrlEncoded] JoinReq :> Post '[JSON] ResponseJSON JoinRsp
+ Web.Slack.Conversation: type Api = "conversations.info" :> AuthProtect "token" :> ReqBody '[FormUrlEncoded] InfoReq :> Post '[JSON] ResponseJSON InfoRsp :<|> "conversations.join" :> AuthProtect "token" :> ReqBody '[FormUrlEncoded] JoinReq :> Post '[JSON] ResponseJSON JoinRsp :<|> "conversations.members" :> AuthProtect "token" :> ReqBody '[FormUrlEncoded] MembersReq :> Post '[JSON] ResponseJSON MembersRsp
- Web.Slack.Experimental.Blocks: RichTextSectionItemRichText :: [RichItem] -> RichTextSectionItem
+ Web.Slack.Experimental.Blocks: RichTextSectionItemRichText :: RichTextSection -> RichTextSectionItem
- Web.Slack.Experimental.Blocks.Types: RichTextSectionItemRichText :: [RichItem] -> RichTextSectionItem
+ Web.Slack.Experimental.Blocks.Types: RichTextSectionItemRichText :: RichTextSection -> RichTextSectionItem
- Web.Slack.Experimental.Events.Types: [ts] :: BotMessageEvent -> Text
+ Web.Slack.Experimental.Events.Types: [ts] :: MessageDeletedEvent -> Text
- Web.Slack.User: User :: UserId -> Text -> Bool -> Maybe Color -> Maybe Profile -> Maybe Bool -> Maybe Bool -> Maybe Bool -> Maybe Bool -> Maybe Bool -> POSIXTime -> User
+ Web.Slack.User: User :: UserId -> Text -> Bool -> Maybe Color -> Maybe Profile -> Maybe Bool -> Maybe Bool -> Maybe Bool -> Maybe Bool -> Maybe Bool -> Maybe Bool -> POSIXTime -> User
Files
- CHANGELOG.md +53/−1
- slack-web.cabal +4/−3
- src/Web/Slack/Conversation.hs +78/−1
- src/Web/Slack/Experimental/Blocks.hs +2/−0
- src/Web/Slack/Experimental/Blocks/Types.hs +109/−12
- src/Web/Slack/Experimental/Events/Types.hs +52/−0
- src/Web/Slack/Internal.hs +2/−0
- src/Web/Slack/Prelude.hs +1/−1
- src/Web/Slack/User.hs +1/−0
- tests/Web/Slack/ConversationSpec.hs +2/−0
- tests/Web/Slack/Experimental/Blocks/TypesSpec.hs +55/−0
- tests/Web/Slack/Experimental/Events/TypesSpec.hs +4/−0
- tests/Web/Slack/UserSpec.hs +13/−0
- tests/golden/SlackWebhookEvent/botMessage.golden +9/−7
- tests/golden/SlackWebhookEvent/channel_shared.golden +21/−0
- tests/golden/SlackWebhookEvent/channel_shared.json +24/−0
- tests/golden/SlackWebhookEvent/channel_unshared.golden +22/−0
- tests/golden/SlackWebhookEvent/channel_unshared.json +25/−0
- tests/golden/SlackWebhookEvent/forwarded_message.golden +9/−7
- tests/golden/SlackWebhookEvent/link.golden +12/−10
- tests/golden/SlackWebhookEvent/messageExample.golden +9/−7
- tests/golden/SlackWebhookEvent/messageIm.golden +9/−7
- tests/golden/SlackWebhookEvent/message_deleted.golden +20/−0
- tests/golden/SlackWebhookEvent/message_deleted.json +37/−0
- tests/golden/SlackWebhookEvent/message_file_share.golden +9/−7
- tests/golden/SlackWebhookEvent/message_rich_text.golden +51/−49
- tests/golden/SlackWebhookEvent/message_rich_text_containers.golden +91/−0
- tests/golden/SlackWebhookEvent/message_rich_text_containers.json +73/−0
- tests/golden/SlackWebhookEvent/message_subtype_bot_message.golden +12/−10
- tests/golden/SlackWebhookEvent/share_with_message.golden +18/−14
- tests/golden/SlackWebhookEvent/share_without_message.golden +9/−7
- tests/golden/SlackWebhookEvent/slackbotIm.golden +18/−16
CHANGELOG.md view
@@ -1,4 +1,56 @@-# 2.1.1.0+# 2.3.0.0 (2026-10-09)+* [#158](https://github.com/MercuryTechnologies/slack-web/pull/158),+ [#159](https://github.com/MercuryTechnologies/slack-web/pull/159)+ Decode the optional `is_bot` field on user objects.++ Breaking change: `User` has a new `userIsBot :: Maybe Bool` field.+* [#160](https://github.com/MercuryTechnologies/slack-web/pull/160)+ Add cursor-based pagination and page-size control to `conversationsMembers`.++ Breaking change: `MembersReq` has new `membersReqCursor` and `membersReqLimit`+ fields; `MembersRsp` has a new `membersRspResponseMetadata` field.+* [#155](https://github.com/MercuryTechnologies/slack-web/pull/155)+ Replace the `data-default-class` dependency with `data-default`.+* [#164](https://github.com/MercuryTechnologies/slack-web/pull/164),+ [#165](https://github.com/MercuryTechnologies/slack-web/pull/165)+ Include more information in Block Kit parse errors. If an unexpected value is+ found, it will now include the invalid value as part of the error.++ Before:++ Unknown SlackActionComponent type, must be one of ['button']++ After:++ Unknown SlackActionComponent type 'weird_type', must be one of ['button']++* [#168](https://github.com/MercuryTechnologies/slack-web/pull/168)+ Parse rich-text `message_mention` elements as `RichItemMessageMention`.+ These elements were previously returned as `RichItemOther`.++ Breaking change: `RichItem` has a new `RichItemMessageMention` constructor.+* [#169](https://github.com/MercuryTechnologies/slack-web/pull/169)+ Introduce `RichTextSection` for `rich_text_section` objects.++ Breaking change: `RichTextSectionItemRichText` now wraps `RichTextSection`+ instead of `[RichItem]`.+* [#170](https://github.com/MercuryTechnologies/slack-web/pull/170)+ Breaking change: `RichTextSectionItem` has new `RichTextSectionItemList`,+ `RichTextSectionItemQuote`, and `RichTextSectionItemPreformatted` constructors.++# 2.2.3.0 (2025-09-15)+* [#156](https://github.com/MercuryTechnologies/slack-web/pull/156)+ Implement `message_deleted` event.++# 2.2.2.0 (2025-09-15)+* [#153](https://github.com/MercuryTechnologies/slack-web/pull/153)+ Implement `channel_shared` and `channel_unshared` events.++# 2.2.1.0 (2025-09-15)+* [#151](https://github.com/MercuryTechnologies/slack-web/pull/151)+ Implement `conversations.member` API method.++# 2.2.0.0 (2025-03-21) * [#145](https://github.com/MercuryTechnologies/slack-web/pull/145) Implement `conversations.info` API method.
slack-web.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: slack-web-version: 2.2.0.0+version: 2.3.0.0 build-type: Simple @@ -153,12 +153,12 @@ , classy-prelude , containers , crypton- , data-default-class+ , data-default , deepseq , either , errors , hashable- , http-api-data >= 0.3 && < 0.7+ , http-api-data >= 0.3 && < 0.8 , http-client >= 0.5 && < 0.8 , http-client-tls >= 0.3 && < 0.4 , megaparsec >= 5.0 && < 10@@ -197,6 +197,7 @@ Web.Slack.ChatSpec Web.Slack.Files.TypesSpec Web.Slack.InternalSpec+ Web.Slack.UserSpec Web.Slack.UsersConversationsSpec Web.Slack.Experimental.RequestVerificationSpec Web.Slack.Experimental.Events.TypesSpec
src/Web/Slack/Conversation.hs view
@@ -44,6 +44,11 @@ JoinRsp (..), conversationsJoin, conversationsJoin_,+ MembersReq (..),+ mkMembersReq,+ MembersRsp (..),+ conversationsMembers,+ conversationsMembers_, ) where import Data.Aeson@@ -475,6 +480,62 @@ $(deriveJSON (jsonOpts "infoRsp") ''InfoRsp) +-- | @conversations.members@ request: retrieve a conversation's members.+--+-- <https://api.slack.com/methods/conversations.members>+--+-- @since 2.2.1.0+data MembersReq = MembersReq+ { membersReqChannel :: ConversationId+ , membersReqCursor :: Maybe Cursor+ , membersReqLimit :: Maybe Int+ }+ deriving stock (Eq, Show, Generic)++instance NFData MembersReq++$(deriveFromJSON (jsonOpts "membersReq") ''MembersReq)++instance ToForm MembersReq where+ toForm MembersReq {..} =+ [("channel", toQueryParam membersReqChannel)]+ <> toQueryParamIfJust "cursor" membersReqCursor+ <> toQueryParamIfJust "limit" membersReqLimit++-- | Smart constructor for 'MembersReq' with defaults.+--+-- @since 2.2.1.0+mkMembersReq :: ConversationId -> MembersReq+mkMembersReq channel =+ MembersReq+ { membersReqChannel = channel+ , membersReqCursor = Nothing+ , membersReqLimit = Nothing+ }++instance PagedRequest MembersReq where+ setCursor c r = r {membersReqCursor = c}++-- | @conversation.members@ response+--+-- <https://api.slack.com/methods/conversations.members>+--+-- @since 2.2.1.0+data MembersRsp = MembersRsp+ { membersRspMembers :: [UserId]+ , membersRspResponseMetadata :: Maybe ResponseMetadata+ }+ deriving stock (Eq, Show, Generic)++instance NFData MembersRsp++$(deriveFromJSON (jsonOpts "membersRsp") ''MembersRsp)++instance PagedResponse MembersRsp where+ type ResponseObject MembersRsp = UserId+ getResponseData MembersRsp {membersRspMembers} = membersRspMembers+ getResponseMetadata MembersRsp {membersRspResponseMetadata} = membersRspResponseMetadata+ -- | FIXME(jadel): move the rest of the Conversations API into here since the old "shoving all the API in one spot" is soft deprecated. -- @since 2.2.0.0 type Api =@@ -486,6 +547,10 @@ :> AuthProtect "token" :> ReqBody '[FormUrlEncoded] JoinReq :> Post '[JSON] (ResponseJSON JoinRsp)+ :<|> "conversations.members"+ :> AuthProtect "token"+ :> ReqBody '[FormUrlEncoded] MembersReq+ :> Post '[JSON] (ResponseJSON MembersRsp) -- | Joins a conversation. --@@ -552,12 +617,15 @@ -- | @since 2.2.0.0 conversationsJoin_ :: AuthenticatedRequest (AuthProtect "token") -> JoinReq -> ClientM (ResponseJSON JoinRsp) +-- | @since 2.2.1.0+conversationsMembers_ :: AuthenticatedRequest (AuthProtect "token") -> MembersReq -> ClientM (ResponseJSON MembersRsp)+ -- | @since 2.2.0.0 conversationsInfo_ :: AuthenticatedRequest (AuthProtect "token") -> InfoReq -> ClientM (ResponseJSON InfoRsp)-conversationsInfo_ :<|> conversationsJoin_ = client (Proxy @Api)+conversationsInfo_ :<|> conversationsJoin_ :<|> conversationsMembers_ = client (Proxy @Api) -- | Retrieve a conversation's metadata. --@@ -584,3 +652,12 @@ conversationsJoin slackConfig req = do let authR = mkSlackAuthenticateReq slackConfig run (conversationsJoin_ authR req) . slackConfigManager $ slackConfig++-- | @since 2.2.1.0+conversationsMembers ::+ SlackConfig ->+ MembersReq ->+ IO (Response MembersRsp)+conversationsMembers slackConfig req = do+ let authR = mkSlackAuthenticateReq slackConfig+ run (conversationsMembers_ authR req) . slackConfigManager $ slackConfig
src/Web/Slack/Experimental/Blocks.hs view
@@ -31,6 +31,8 @@ RichItem (..), RichStyle (..), RichLinkAttrs (..),+ RichMessageMention (..),+ RichTextSection (..), RichTextSectionItem (..), RichText (..),
src/Web/Slack/Experimental/Blocks/Types.hs view
@@ -88,7 +88,7 @@ "mrkdwn" -> do text <- obj .: "text" pure . SlackMarkdownText . SlackText $ lines text- _ -> fail "Unknown SlackTextObject type, must be one of ['plain_text', 'mrkdwn']"+ _ -> fail $ "Unknown SlackTextObject type " <> show slackTextType <> ", must be one of ['plain_text', 'mrkdwn']" instance Show SlackText where show (SlackText arr) = show $ concat arr@@ -216,7 +216,7 @@ Just innerObj -> innerObj .: "text" Nothing -> pure Nothing pure $ SlackContentImage $ SlackImage slackImageTitleText slackImageAltText slackImageUrl- _ -> fail "Unknown SlackContent type, must be one of ['mrkdwn', 'image']"+ _ -> fail $ "Unknown SlackContent type " <> show slackContentType <> ", must be one of ['mrkdwn', 'image']" newtype SlackContext = SlackContext [SlackContent] deriving newtype (Semigroup, Monoid, Eq)@@ -289,15 +289,48 @@ } deriving stock (Eq, Show) --- | Seemingly only documented at--- <https://api.slack.com/changelog/2019-09-what-they-see-is-what-you-get-and-more-and-less>+-- | An inline reference to a message. The URL is optional; consumers can build+-- a permalink from the channel and message timestamp in the current workspace. ----- They warn of undocumented element types. Joy.+-- Parsed as part of 'RichItemMessageMention' in incoming rich text.+--+-- <https://docs.slack.dev/reference/block-kit/block-elements/message-mention-element/>+--+-- @since 2.3.0.0+data RichMessageMention = RichMessageMention+ { rmmChannelId :: ConversationId+ -- ^ Channel containing the referenced message.+ --+ -- @since 2.3.0.0+ , rmmMessageTs :: Text+ -- ^ Timestamp identifying the referenced message, preserved as Slack's text.+ --+ -- @since 2.3.0.0+ , rmmThreadTs :: Maybe Text+ -- ^ Optional @thread_ts@ timestamp supplied by Slack.+ --+ -- @since 2.3.0.0+ , rmmUrl :: Maybe Text+ -- ^ URL of the referenced message, when supplied by Slack.+ --+ -- @since 2.3.0.0+ }+ deriving stock (Eq, Show)++-- | Inline content inside rich-text sections, quotes, and preformatted blocks.+--+-- <https://docs.slack.dev/reference/block-kit/block-elements/rich-text-section-element/>+--+-- Unrecognized element types are preserved as 'RichItemOther'. data RichItem = RichItemText Text RichStyle | RichItemChannel ConversationId | RichItemUser UserId RichStyle | RichItemLink RichLinkAttrs+ | -- | An inline @message_mention@ reference to another message.+ --+ -- @since 2.3.0.0+ RichItemMessageMention RichMessageMention | RichItemEmoji Emoji | RichItemOther Text Value -- FIXME(jadel): date, usergroup, team, broadcast@@ -322,14 +355,73 @@ text <- obj .:? "text" style <- obj .:? "style" .!= mempty pure $ RichItemLink RichLinkAttrs {..}+ "message_mention" -> do+ rmmChannelId <- obj .: "channel_id"+ rmmMessageTs <- obj .: "message_ts"+ rmmThreadTs <- obj .:? "thread_ts"+ rmmUrl <- obj .:? "url"+ pure $ RichItemMessageMention RichMessageMention {..} "user" -> do userId <- obj .: "user_id" style <- obj .:? "style" .!= mempty pure $ RichItemUser userId style _ -> pure $ RichItemOther kind (Object obj) +-- | A @rich_text_section@ object containing inline 'RichItem' values.+-- A section can appear directly in a @rich_text@ block or as an entry in a+-- @rich_text_list@. Its JSON decoder requires @type = "rich_text_section"@.+--+-- t'RichTextSectionItem' represents the broader set of structural children of+-- a rich-text block; this type represents only a section. Lists contain+-- sections, so their entries use this narrower type.+--+-- <https://docs.slack.dev/reference/block-kit/block-elements/rich-text-section-element/>+--+-- @since 2.3.0.0+newtype RichTextSection = RichTextSection [RichItem]+ deriving stock (Eq, Show)++instance FromJSON RichTextSection where+ parseJSON = withObject "RichTextSection" \obj -> do+ kind :: Text <- obj .: "type"+ case kind of+ "rich_text_section" -> RichTextSection <$> obj .: "elements"+ _ -> fail $ "Unexpected RichTextSection type " <> show kind <> ", must be 'rich_text_section'"++-- | Structural children of a @rich_text@ block: sections, lists, quotes, and+-- preformatted blocks. The name refers to this broader set of containers;+-- t'RichTextSection' represents a single @rich_text_section@, and 'RichItem'+-- represents inline content such as text, links, and message mentions.+--+-- <https://docs.slack.dev/reference/block-kit/blocks/rich-text-block/> data RichTextSectionItem- = RichTextSectionItemRichText [RichItem]+ = -- | A @rich_text_section@ containing inline items.+ --+ -- <https://docs.slack.dev/reference/block-kit/block-elements/rich-text-section-element/>+ RichTextSectionItemRichText RichTextSection+ | -- | A @rich_text_list@ whose entries are sections. The t'RichTextSection'+ -- decoder rejects entries with any other container type.+ -- The decoder ignores Slack's @style@, @indent@, @offset@, and @border@ fields.+ --+ -- <https://docs.slack.dev/reference/block-kit/block-elements/rich-text-list-element/>+ --+ -- @since 2.3.0.0+ RichTextSectionItemList [RichTextSection]+ | -- | A @rich_text_quote@ containing inline items.+ -- The decoder ignores Slack's optional @border@ field.+ --+ -- <https://docs.slack.dev/reference/block-kit/block-elements/rich-text-quote-element/>+ --+ -- @since 2.3.0.0+ RichTextSectionItemQuote [RichItem]+ | -- | A @rich_text_preformatted@ code block. Slack documents text and link+ -- elements as its contents; 'RichItem' also permits other constructors.+ -- Border and syntax-highlighting language are not retained.+ --+ -- <https://docs.slack.dev/reference/block-kit/block-elements/rich-text-preformatted-element/>+ --+ -- @since 2.3.0.0+ RichTextSectionItemPreformatted [RichItem] | RichTextSectionItemUnknown Text Value deriving stock (Eq, Show) @@ -337,9 +429,10 @@ parseJSON = withObject "RichTextSectionItem" \obj -> do kind <- obj .: "type" case kind of- "rich_text_section" -> do- elts <- obj .: "elements"- pure $ RichTextSectionItemRichText elts+ "rich_text_section" -> RichTextSectionItemRichText <$> parseJSON (Object obj)+ "rich_text_list" -> RichTextSectionItemList <$> obj .: "elements"+ "rich_text_quote" -> RichTextSectionItemQuote <$> obj .: "elements"+ "rich_text_preformatted" -> RichTextSectionItemPreformatted <$> obj .: "elements" _ -> pure $ RichTextSectionItemUnknown kind (Object obj) data RichText = RichText@@ -364,7 +457,11 @@ toJSON (SlackButtonAccessory btn) = toJSON btn instance FromJSON SlackAccessory where- parseJSON v = SlackButtonAccessory <$> parseJSON v+ parseJSON = withObject "SlackAccessory" \obj -> do+ kind :: Text <- obj .: "type"+ case kind of+ "button" -> SlackButtonAccessory <$> parseJSON (Object obj)+ _ -> fail $ "Unknown SlackAccessory type " <> show kind <> ", must be one of ['button']" instance Show SlackAccessory where show (SlackButtonAccessory btn) = show btn@@ -794,7 +891,7 @@ deriving stock (Eq) instance FromJSON SlackActionComponent where- parseJSON = withObject "SlactActionComponent" $ \obj -> do+ parseJSON = withObject "SlackActionComponent" $ \obj -> do (slackActionType :: Text) <- obj .: "type" case slackActionType of "button" -> do@@ -805,7 +902,7 @@ slackButtonStyle <- obj .:? "style" slackButtonConfirm <- obj .:? "confirm" pure $ SlackButton {..}- _ -> fail "Unknown SlackActionComponent type, must be one of ['button']"+ _ -> fail $ "Unknown SlackActionComponent type " <> show slackActionType <> ", must be one of ['button']" instance Show SlackActionComponent where show SlackButton {..} = "[button " <> show slackButtonText <> "]"
src/Web/Slack/Experimental/Events/Types.hs view
@@ -244,6 +244,20 @@ $(deriveFromJSON snakeCaseOptions ''MessageUpdateEvent) +-- | A message was deleted from a channel or DM.+--+-- <https://docs.slack.dev/reference/events/message/message_deleted/>+--+-- @since 2.2.3.0+data MessageDeletedEvent = MessageDeletedEvent+ { channel :: ConversationId+ , ts :: Text+ , deletedTs :: Text+ }+ deriving stock (Show)++$(deriveFromJSON snakeCaseOptions ''MessageDeletedEvent)+ -- | FIXME: this might be a Channel, but may also be missing some fields/have bonus -- because Slack is cursed. data CreatedChannel = CreatedChannel@@ -283,6 +297,35 @@ $(deriveFromJSON snakeCaseOptions ''ChannelLeftEvent) +-- | A channel has been shared with an external workspace.+--+-- <https://docs.slack.dev/reference/events/channel_shared/>+--+-- @since 2.2.2.0+data ChannelSharedEvent = ChannelSharedEvent+ { connectedTeamId :: TeamId+ , channel :: ConversationId+ , eventTs :: Text+ }+ deriving stock (Show)++$(deriveFromJSON snakeCaseOptions ''ChannelSharedEvent)++-- | A channel has been unshared with an external workspace.+--+-- <https://docs.slack.dev/reference/events/channel_unshared/>+--+-- @since 2.2.2.0+data ChannelUnsharedEvent = ChannelUnsharedEvent+ { previouslyConnectedTeamId :: TeamId+ , channel :: ConversationId+ , isExtShared :: Bool+ , eventTs :: Text+ }+ deriving stock (Show)++$(deriveFromJSON snakeCaseOptions ''ChannelUnsharedEvent)+ -- | <https://api.slack.com/events/url_verification> data UrlVerificationPayload = UrlVerificationPayload { challenge :: Text@@ -339,6 +382,8 @@ = EventMessage MessageEvent | EventBotMessage BotMessageEvent | EventMessageChanged+ | -- | @since 2.2.3.0+ EventMessageDeleted MessageDeletedEvent | -- | Weird message event of subtype channel_join. Sent "sometimes", according -- to a random Slack blog post from 2017: -- <https://api.slack.com/changelog/2017-05-rethinking-channel-entrance-and-exit-events-and-messages>@@ -347,6 +392,10 @@ EventChannelJoinMessage | EventChannelCreated ChannelCreatedEvent | EventChannelLeft ChannelLeftEvent+ | -- | @since 2.2.2.0+ EventChannelShared ChannelSharedEvent+ | -- | @since 2.2.2.0+ EventChannelUnshared ChannelUnsharedEvent | -- | @since 2.1.0.0 EventAppHomeOpened AppHomeOpenedEvent | EventUnknown Value@@ -360,12 +409,15 @@ ("message", Nothing) -> EventMessage <$> parseJSON @MessageEvent (Object obj) ("message", Just "bot_message") -> EventBotMessage <$> parseJSON @BotMessageEvent (Object obj) ("message", Just "message_changed") -> pure EventMessageChanged+ ("message", Just "message_deleted") -> EventMessageDeleted <$> parseJSON @MessageDeletedEvent (Object obj) ("message", Just "channel_join") -> pure EventChannelJoinMessage -- n.b. these are unified since it is *identical* to a Message event with -- a bonus files field ("message", Just "file_share") -> EventMessage <$> parseJSON @MessageEvent (Object obj) ("channel_created", Nothing) -> EventChannelCreated <$> parseJSON (Object obj) ("channel_left", Nothing) -> EventChannelLeft <$> parseJSON (Object obj)+ ("channel_shared", Nothing) -> EventChannelShared <$> parseJSON (Object obj)+ ("channel_unshared", Nothing) -> EventChannelUnshared <$> parseJSON (Object obj) ("app_home_opened", Nothing) -> EventAppHomeOpened <$> parseJSON (Object obj) _ -> pure $ EventUnknown (Object obj)
src/Web/Slack/Internal.hs view
@@ -1,3 +1,5 @@+{-# OPTIONS_GHC -Wno-orphans #-}+ -- | Internal things in slack-web. May be changed arbitrarily! module Web.Slack.Internal where
src/Web/Slack/Prelude.hs view
@@ -12,7 +12,7 @@ import ClassyPrelude hiding (link) import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.:?), (.=)) import Data.Aeson.TH (deriveFromJSON, deriveJSON, deriveToJSON)-import Data.Default.Class (Default (..))+import Data.Default (Default (..)) import Data.List.NonEmpty (NonEmpty (..)) import Data.Proxy (Proxy (..)) import Data.String.Conversions (cs)
src/Web/Slack/User.hs view
@@ -55,6 +55,7 @@ , userIsPrimaryOwner :: Maybe Bool , userIsRestricted :: Maybe Bool , userIsUltraRestricted :: Maybe Bool+ , userIsBot :: Maybe Bool , userUpdated :: POSIXTime } deriving stock (Eq, Generic, Show)
tests/Web/Slack/ConversationSpec.hs view
@@ -116,6 +116,8 @@ mapM_ (oneGoldenTestDecode @Conversation) ["shared_channel"] mapM_ (oneGoldenTestDecode @InfoRsp) ["general"] mapM_ (oneGoldenTestDecode @JoinRsp) ["test"]+ mapM_ (oneGoldenTestDecode @MembersReq) ["with_limit"]+ mapM_ (oneGoldenTestDecode @MembersRsp) ["test"] it "errors accurately if no variant matches" $ do let badData =
tests/Web/Slack/Experimental/Blocks/TypesSpec.hs view
@@ -1,9 +1,12 @@ module Web.Slack.Experimental.Blocks.TypesSpec where import Data.Aeson qualified as Aeson+import Data.Aeson.Types (parseEither)+import Data.Either (isLeft) import Data.StringVariants.NonEmptyText.Internal (pattern NonEmptyText) import Refined.Unsafe (reallyUnsafeRefine) import TestImport+import Web.Slack.Common (ConversationId (..)) import Web.Slack.Experimental.Blocks.Types jsonRoundtrips :: (Show a, Eq a, Aeson.ToJSON a, Aeson.FromJSON a) => a -> Spec@@ -13,6 +16,58 @@ spec :: Spec spec = do+ describe "unknown component diagnostics" do+ it "reports the rejected text object type" do+ Aeson.eitherDecode @SlackTextObject "{\"type\":\"unexpected_text\"}"+ `shouldBe` Left "Error in $: Unknown SlackTextObject type \"unexpected_text\", must be one of ['plain_text', 'mrkdwn']"++ it "reports the rejected content type at its nested JSON path" do+ Aeson.eitherDecode @SlackBlock "{\"type\":\"context\",\"elements\":[{\"type\":\"unexpected_content\"}]}"+ `shouldBe` Left "Error in $.elements: Unknown SlackContent type \"unexpected_content\", must be one of ['mrkdwn', 'image']"++ it "reports the rejected action component type at its nested JSON path" do+ Aeson.eitherDecode @SlackBlock "{\"type\":\"actions\",\"elements\":[{\"type\":\"static_select\",\"action_id\":\"select\"}]}"+ `shouldBe` Left "Error in $.elements[0]: Unknown SlackActionComponent type \"static_select\", must be one of ['button']"++ it "reports the rejected accessory type before requiring action fields" do+ Aeson.eitherDecode @SlackAccessory "{\"type\":\"image\",\"image_url\":\"https://example.com/image.png\",\"alt_text\":\"example\"}"+ `shouldBe` Left "Error in $: Unknown SlackAccessory type \"image\", must be one of ['button']"++ it "reports the rejected accessory type even when action fields are present" do+ Aeson.eitherDecode @SlackAccessory "{\"type\":\"static_select\",\"action_id\":\"select\"}"+ `shouldBe` Left "Error in $: Unknown SlackAccessory type \"static_select\", must be one of ['button']"++ describe "incoming Block Kit components" do+ it "parses a message mention without a URL" do+ Aeson.eitherDecode @RichItem "{\"type\":\"message_mention\",\"channel_id\":\"C123ABC456\",\"message_ts\":\"1720710212.123456\"}"+ `shouldBe` Right (RichItemMessageMention (RichMessageMention (ConversationId "C123ABC456") "1720710212.123456" Nothing Nothing))++ describe "rich-text list entries" do+ let listWith entry =+ object+ [ "type" .= ("rich_text_list" :: Text)+ , "style" .= ("bullet" :: Text)+ , "elements" .= [entry]+ ]+ decodeList = parseEither (parseJSON @RichTextSectionItem) . listWith++ for_ ["rich_text_list", "rich_text_quote", "rich_text_preformatted", "text", "future_container"] \(kind :: Text) ->+ it ("rejects a " <> unpack kind <> " as a list entry") do+ decodeList+ (object ["type" .= kind, "elements" .= ([] :: [Aeson.Value]), "style" .= ("bullet" :: Text), "text" .= ("inline" :: Text)])+ `shouldSatisfy` isLeft++ it "requires the section type tag" do+ decodeList (object ["elements" .= ([] :: [Aeson.Value])]) `shouldSatisfy` isLeft++ it "requires the section contents" do+ decodeList (object ["type" .= ("rich_text_section" :: Text)]) `shouldSatisfy` isLeft++ it "preserves unknown inline items inside a section" do+ let inlineItem = object ["type" .= ("future_inline" :: Text), "text" .= ("preserved" :: Text)]+ decodeList (object ["type" .= ("rich_text_section" :: Text), "elements" .= [inlineItem]])+ `shouldBe` Right (RichTextSectionItemList [RichTextSection [RichItemOther "future_inline" inlineItem]])+ let aSlackAccessory = SlackButtonAccessory aSlackAction aSlackAction = SlackAction
tests/Web/Slack/Experimental/Events/TypesSpec.hs view
@@ -12,7 +12,9 @@ (oneGoldenTestDecode @SlackWebhookEvent) [ "messageExample" , "messageChange"+ , "message_deleted" , "message_rich_text"+ , "message_rich_text_containers" , "message_file_share" , "message_file_share_slack_connect" , "link"@@ -22,6 +24,8 @@ , "messageIm" , "slackbotIm" , "channel_left"+ , "channel_shared"+ , "channel_unshared" , "share_without_message" , "share_with_message" , "circleci"
+ tests/Web/Slack/UserSpec.hs view
@@ -0,0 +1,13 @@+module Web.Slack.UserSpec (spec) where++import JSONGolden+import TestImport+import Web.Slack.User (ListRsp)++-- Fixture modeled on the example response at+-- https://api.slack.com/methods/users.list#examples+spec :: Spec+spec = describe "User methods" do+ describe "users.list" do+ describe "Response FromJSON" do+ mapM_ (oneGoldenTestDecode @ListRsp) ["with_bots"]
tests/golden/SlackWebhookEvent/botMessage.golden view
@@ -16,13 +16,15 @@ ( NonEmptyText "FVI" ) , elements = [ RichTextSectionItemRichText- [ RichItemText "TEST"- ( RichStyle- { rsBold = False- , rsItalic = False- }- )- ]+ ( RichTextSection+ [ RichItemText "TEST"+ ( RichStyle+ { rsBold = False+ , rsItalic = False+ }+ )+ ]+ ) ] } ]
@@ -0,0 +1,21 @@+EventEventCallback+ ( EventCallback+ { eventId = EventId+ { unEventId = "Ev044RKJHB8W" }+ , teamId = TeamId+ { unTeamId = "T043DB835ML" }+ , eventTime = MkSystemTime+ { systemSeconds = 1664827321+ , systemNanoseconds = 0+ }+ , event = EventChannelShared+ ( ChannelSharedEvent+ { connectedTeamId = TeamId+ { unTeamId = "E163Q94DX" }+ , channel = ConversationId+ { unConversationId = "C123ABC456" }+ , eventTs = "1561064063.001100"+ }+ )+ }+ )
@@ -0,0 +1,24 @@+{+ "token": "aaaa",+ "team_id": "T043DB835ML",+ "api_app_id": "A0442TUPHGR",+ "event": {+ "type": "channel_shared",+ "connected_team_id": "E163Q94DX",+ "channel": "C123ABC456",+ "event_ts": "1561064063.001100"+ },+ "type": "event_callback",+ "event_id": "Ev044RKJHB8W",+ "event_time": 1664827321,+ "authorizations": [+ {+ "enterprise_id": null,+ "team_id": "T043DB835ML",+ "user_id": "U0442US8QGH",+ "is_bot": false,+ "is_enterprise_install": false+ }+ ],+ "is_ext_shared_channel": true+}
@@ -0,0 +1,22 @@+EventEventCallback+ ( EventCallback+ { eventId = EventId+ { unEventId = "Ev044RKJHB8W" }+ , teamId = TeamId+ { unTeamId = "T043DB835ML" }+ , eventTime = MkSystemTime+ { systemSeconds = 1664827321+ , systemNanoseconds = 0+ }+ , event = EventChannelUnshared+ ( ChannelUnsharedEvent+ { previouslyConnectedTeamId = TeamId+ { unTeamId = "E163Q94DX" }+ , channel = ConversationId+ { unConversationId = "C123ABC456" }+ , isExtShared = False+ , eventTs = "1561064063.001100"+ }+ )+ }+ )
@@ -0,0 +1,25 @@+{+ "token": "aaaa",+ "team_id": "T043DB835ML",+ "api_app_id": "A0442TUPHGR",+ "event": {+ "type": "channel_unshared",+ "previously_connected_team_id": "E163Q94DX",+ "channel": "C123ABC456",+ "is_ext_shared": false,+ "event_ts": "1561064063.001100"+ },+ "type": "event_callback",+ "event_id": "Ev044RKJHB8W",+ "event_time": 1664827321,+ "authorizations": [+ {+ "enterprise_id": null,+ "team_id": "T043DB835ML",+ "user_id": "U0442US8QGH",+ "is_bot": false,+ "is_enterprise_install": false+ }+ ],+ "is_ext_shared_channel": false+}
tests/golden/SlackWebhookEvent/forwarded_message.golden view
@@ -57,13 +57,15 @@ ( NonEmptyText "nXUog" ) , elements = [ RichTextSectionItemRichText- [ RichItemText "123"- ( RichStyle- { rsBold = False- , rsItalic = False- }- )- ]+ ( RichTextSection+ [ RichItemText "123"+ ( RichStyle+ { rsBold = False+ , rsItalic = False+ }+ )+ ]+ ) ] } ]
tests/golden/SlackWebhookEvent/link.golden view
@@ -16,17 +16,19 @@ ( NonEmptyText "MEdM" ) , elements = [ RichTextSectionItemRichText- [ RichItemLink- ( RichLinkAttrs- { style = RichStyle- { rsBold = False- , rsItalic = False+ ( RichTextSection+ [ RichItemLink+ ( RichLinkAttrs+ { style = RichStyle+ { rsBold = False+ , rsItalic = False+ }+ , url = "https://jadeapptesting.slack.com/archives/C043YJGBY49/p1663961604007869"+ , text = Nothing }- , url = "https://jadeapptesting.slack.com/archives/C043YJGBY49/p1663961604007869"- , text = Nothing- }- )- ]+ )+ ]+ ) ] } ]
tests/golden/SlackWebhookEvent/messageExample.golden view
@@ -16,13 +16,15 @@ ( NonEmptyText "u6n" ) , elements = [ RichTextSectionItemRichText- [ RichItemText "dgsfklsdgf"- ( RichStyle- { rsBold = False- , rsItalic = False- }- )- ]+ ( RichTextSection+ [ RichItemText "dgsfklsdgf"+ ( RichStyle+ { rsBold = False+ , rsItalic = False+ }+ )+ ]+ ) ] } ]
tests/golden/SlackWebhookEvent/messageIm.golden view
@@ -16,13 +16,15 @@ ( NonEmptyText "LSo" ) , elements = [ RichTextSectionItemRichText- [ RichItemText "test"- ( RichStyle- { rsBold = False- , rsItalic = False- }- )- ]+ ( RichTextSection+ [ RichItemText "test"+ ( RichStyle+ { rsBold = False+ , rsItalic = False+ }+ )+ ]+ ) ] } ]
+ tests/golden/SlackWebhookEvent/message_deleted.golden view
@@ -0,0 +1,20 @@+EventEventCallback+ ( EventCallback+ { eventId = EventId+ { unEventId = "Ev043QRVT9FE" }+ , teamId = TeamId+ { unTeamId = "T043DB835ML" }+ , eventTime = MkSystemTime+ { systemSeconds = 1663969383+ , systemNanoseconds = 0+ }+ , event = EventMessageDeleted+ ( MessageDeletedEvent+ { channel = ConversationId+ { unConversationId = "C043YJGBY49" }+ , ts = "1663969383.001500"+ , deletedTs = "1663966382.046509"+ }+ )+ }+ )
+ tests/golden/SlackWebhookEvent/message_deleted.json view
@@ -0,0 +1,37 @@+{+ "token": "aaaa",+ "team_id": "T043DB835ML",+ "api_app_id": "A0442TUPHGR",+ "event": {+ "type": "message",+ "subtype": "message_deleted",+ "hidden": true,+ "channel": "C043YJGBY49",+ "previous_message": {+ "client_msg_id": "de349a9f-62a4-4293-b64d-701a1f566e8a",+ "type": "message",+ "text": "This message will be deleted",+ "user": "U043H11ES4V",+ "ts": "1663966382.046509",+ "team": "T043DB835ML"+ },+ "deleted_ts": "1663966382.046509",+ "ts": "1663969383.001500",+ "event_ts": "1663969383.001500",+ "channel_type": "channel"+ },+ "type": "event_callback",+ "event_id": "Ev043QRVT9FE",+ "event_time": 1663969383,+ "authorizations": [+ {+ "enterprise_id": null,+ "team_id": "T043DB835ML",+ "user_id": "U0442US8QGH",+ "is_bot": true,+ "is_enterprise_install": false+ }+ ],+ "is_ext_shared_channel": false,+ "event_context": "aaaa"+}
@@ -16,13 +16,15 @@ ( NonEmptyText "MeAN" ) , elements = [ RichTextSectionItemRichText- [ RichItemText "message message message"- ( RichStyle- { rsBold = False- , rsItalic = False- }- )- ]+ ( RichTextSection+ [ RichItemText "message message message"+ ( RichStyle+ { rsBold = False+ , rsItalic = False+ }+ )+ ]+ ) ] } ]
tests/golden/SlackWebhookEvent/message_rich_text.golden view
@@ -16,58 +16,60 @@ ( NonEmptyText "6r5Zo" ) , elements = [ RichTextSectionItemRichText- [ RichItemChannel- ( ConversationId- { unConversationId = "C043YJGBY49" }- )- , RichItemText " "- ( RichStyle- { rsBold = False- , rsItalic = False- }- )- , RichItemEmoji- ( Emoji- { unEmoji = "cat" }- )- , RichItemText " "- ( RichStyle- { rsBold = False- , rsItalic = False- }- )- , RichItemUser- ( UserId- { unUserId = "U043H11ES4V" }- )- ( RichStyle- { rsBold = False- , rsItalic = False- }- )- , RichItemText " "- ( RichStyle- { rsBold = False- , rsItalic = False- }- )- , RichItemLink- ( RichLinkAttrs- { style = RichStyle+ ( RichTextSection+ [ RichItemChannel+ ( ConversationId+ { unConversationId = "C043YJGBY49" }+ )+ , RichItemText " "+ ( RichStyle { rsBold = False , rsItalic = False }- , url = "https://google.com"- , text = Nothing- }- )- , RichItemText " text text text"- ( RichStyle- { rsBold = False- , rsItalic = False- }- )- ]+ )+ , RichItemEmoji+ ( Emoji+ { unEmoji = "cat" }+ )+ , RichItemText " "+ ( RichStyle+ { rsBold = False+ , rsItalic = False+ }+ )+ , RichItemUser+ ( UserId+ { unUserId = "U043H11ES4V" }+ )+ ( RichStyle+ { rsBold = False+ , rsItalic = False+ }+ )+ , RichItemText " "+ ( RichStyle+ { rsBold = False+ , rsItalic = False+ }+ )+ , RichItemLink+ ( RichLinkAttrs+ { style = RichStyle+ { rsBold = False+ , rsItalic = False+ }+ , url = "https://google.com"+ , text = Nothing+ }+ )+ , RichItemText " text text text"+ ( RichStyle+ { rsBold = False+ , rsItalic = False+ }+ )+ ]+ ) ] } ]
+ tests/golden/SlackWebhookEvent/message_rich_text_containers.golden view
@@ -0,0 +1,91 @@+EventEventCallback+ ( EventCallback+ { eventId = EventId+ { unEventId = "EvRichTextContainers" }+ , teamId = TeamId+ { unTeamId = "T043DB835ML" }+ , eventTime = MkSystemTime+ { systemSeconds = 1720710300+ , systemNanoseconds = 0+ }+ , event = EventMessage+ ( MessageEvent+ { blocks = Just+ [ RichText+ { blockId = Nothing+ , elements =+ [ RichTextSectionItemList+ [ RichTextSection+ [ RichItemMessageMention+ ( RichMessageMention+ { rmmChannelId = ConversationId+ { unConversationId = "C123ABC456" }+ , rmmMessageTs = "1720710212.123456"+ , rmmThreadTs = Nothing+ , rmmUrl = Nothing+ }+ )+ ]+ , RichTextSection+ [ RichItemLink+ ( RichLinkAttrs+ { style = RichStyle+ { rsBold = False+ , rsItalic = False+ }+ , url = "https://example.com/list-item"+ , text = Nothing+ }+ )+ ]+ ]+ , RichTextSectionItemQuote+ [ RichItemMessageMention+ ( RichMessageMention+ { rmmChannelId = ConversationId+ { unConversationId = "C123ABC456" }+ , rmmMessageTs = "1720710250.012345"+ , rmmThreadTs = Nothing+ , rmmUrl = Nothing+ }+ )+ ]+ , RichTextSectionItemPreformatted+ [ RichItemText "See the documentation:+ "+ ( RichStyle+ { rsBold = False+ , rsItalic = False+ }+ )+ , RichItemLink+ ( RichLinkAttrs+ { style = RichStyle+ { rsBold = False+ , rsItalic = False+ }+ , url = "https://example.com/code-reference"+ , text = Nothing+ }+ )+ ]+ ]+ }+ ]+ , channel = ConversationId+ { unConversationId = "C045V0VJT16" }+ , text = "References in a list, a quote, and a code block"+ , channelType = Channel+ , files = Nothing+ , user = UserId+ { unUserId = "U043H11ES4V" }+ , ts = "1720710300.654321"+ , threadTs = Nothing+ , appId = Nothing+ , botId = Nothing+ , botProfile = Nothing+ , attachments = Nothing+ }+ )+ }+ )
+ tests/golden/SlackWebhookEvent/message_rich_text_containers.json view
@@ -0,0 +1,73 @@+{+ "type": "event_callback",+ "team_id": "T043DB835ML",+ "event_id": "EvRichTextContainers",+ "event_time": 1720710300,+ "event": {+ "type": "message",+ "channel": "C045V0VJT16",+ "channel_type": "channel",+ "user": "U043H11ES4V",+ "ts": "1720710300.654321",+ "text": "References in a list, a quote, and a code block",+ "blocks": [+ {+ "type": "rich_text",+ "elements": [+ {+ "type": "rich_text_list",+ "style": "bullet",+ "indent": 0,+ "elements": [+ {+ "type": "rich_text_section",+ "elements": [+ {+ "type": "message_mention",+ "channel_id": "C123ABC456",+ "message_ts": "1720710212.123456"+ }+ ]+ },+ {+ "type": "rich_text_section",+ "elements": [+ {+ "type": "link",+ "url": "https://example.com/list-item"+ }+ ]+ }+ ]+ },+ {+ "type": "rich_text_quote",+ "border": 1,+ "elements": [+ {+ "type": "message_mention",+ "channel_id": "C123ABC456",+ "message_ts": "1720710250.012345"+ }+ ]+ },+ {+ "type": "rich_text_preformatted",+ "language": "text",+ "border": 0,+ "elements": [+ {+ "type": "text",+ "text": "See the documentation:\n"+ },+ {+ "type": "link",+ "url": "https://example.com/code-reference"+ }+ ]+ }+ ]+ }+ ]+ }+}
tests/golden/SlackWebhookEvent/message_subtype_bot_message.golden view
@@ -16,17 +16,19 @@ ( NonEmptyText "nmh" ) , elements = [ RichTextSectionItemRichText- [ RichItemLink- ( RichLinkAttrs- { style = RichStyle- { rsBold = False- , rsItalic = False+ ( RichTextSection+ [ RichItemLink+ ( RichLinkAttrs+ { style = RichStyle+ { rsBold = False+ , rsItalic = False+ }+ , url = "https://myworkspace.slack.com/archives/C07KTH1T4CQ/p1724954997876929"+ , text = Nothing }- , url = "https://myworkspace.slack.com/archives/C07KTH1T4CQ/p1724954997876929"- , text = Nothing- }- )- ]+ )+ ]+ ) ] } ]
@@ -16,13 +16,15 @@ ( NonEmptyText "Yyg6K" ) , elements = [ RichTextSectionItemRichText- [ RichItemText "hello check that out!"- ( RichStyle- { rsBold = False- , rsItalic = False- }- )- ]+ ( RichTextSection+ [ RichItemText "hello check that out!"+ ( RichStyle+ { rsBold = False+ , rsItalic = False+ }+ )+ ]+ ) ] } ]@@ -72,13 +74,15 @@ ( NonEmptyText "BXn1o" ) , elements = [ RichTextSectionItemRichText- [ RichItemText "this will be shared with a message"- ( RichStyle- { rsBold = False- , rsItalic = False- }- )- ]+ ( RichTextSection+ [ RichItemText "this will be shared with a message"+ ( RichStyle+ { rsBold = False+ , rsItalic = False+ }+ )+ ]+ ) ] } ]
@@ -57,13 +57,15 @@ ( NonEmptyText "TNHa4" ) , elements = [ RichTextSectionItemRichText- [ RichItemText "blahblahblahblahblah"- ( RichStyle- { rsBold = False- , rsItalic = False- }- )- ]+ ( RichTextSection+ [ RichItemText "blahblahblahblahblah"+ ( RichStyle+ { rsBold = False+ , rsItalic = False+ }+ )+ ]+ ) ] } ]
tests/golden/SlackWebhookEvent/slackbotIm.golden view
@@ -16,22 +16,24 @@ ( NonEmptyText "ZRe" ) , elements = [ RichTextSectionItemRichText- [ RichItemText "You have been removed from #testing-slack-app by "- ( RichStyle- { rsBold = False- , rsItalic = False- }- )- , RichItemUser- ( UserId- { unUserId = "U043H11ES4V" }- )- ( RichStyle- { rsBold = False- , rsItalic = False- }- )- ]+ ( RichTextSection+ [ RichItemText "You have been removed from #testing-slack-app by "+ ( RichStyle+ { rsBold = False+ , rsItalic = False+ }+ )+ , RichItemUser+ ( UserId+ { unUserId = "U043H11ES4V" }+ )+ ( RichStyle+ { rsBold = False+ , rsItalic = False+ }+ )+ ]+ ) ] } ]