packages feed

slack-web 1.5.0.1 → 1.6.0.0

raw patch · 25 files changed

+520/−62 lines, 25 filesdep ~pretty-simplePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: pretty-simple

API changes (from Hackage documentation)

+ Web.Slack: usersListAll :: SlackConfig -> ListReq -> IO (LoadPage IO User)
+ Web.Slack.Experimental.Events.Types: [$sel:files:MessageEvent] :: MessageEvent -> Maybe [FileObject]
+ Web.Slack.Files.Types: CheckFileInfo :: FileId -> FileObject
+ Web.Slack.Files.Types: External :: FileMode
+ Web.Slack.Files.Types: FileAccess :: FileMode
+ Web.Slack.Files.Types: FileId :: Text -> FileId
+ Web.Slack.Files.Types: FileObjectVisible :: FileId -> UnixTimestamp -> Text -> Text -> Text -> Text -> Bool -> Int -> FileMode -> FileObjectVisible
+ Web.Slack.Files.Types: Hosted :: FileMode
+ Web.Slack.Files.Types: Post :: FileMode
+ Web.Slack.Files.Types: Snippet :: FileMode
+ Web.Slack.Files.Types: VisibleFileObject :: FileObjectVisible -> FileObject
+ Web.Slack.Files.Types: [created] :: FileObjectVisible -> UnixTimestamp
+ Web.Slack.Files.Types: [id] :: FileObjectVisible -> FileId
+ Web.Slack.Files.Types: [isExternal] :: FileObjectVisible -> Bool
+ Web.Slack.Files.Types: [mimetype] :: FileObjectVisible -> Text
+ Web.Slack.Files.Types: [mode] :: FileObjectVisible -> FileMode
+ Web.Slack.Files.Types: [name] :: FileObjectVisible -> Text
+ Web.Slack.Files.Types: [size] :: FileObjectVisible -> Int
+ Web.Slack.Files.Types: [title] :: FileObjectVisible -> Text
+ Web.Slack.Files.Types: [unFileId] :: FileId -> Text
+ Web.Slack.Files.Types: [urlPrivate] :: FileObjectVisible -> Text
+ Web.Slack.Files.Types: data FileMode
+ Web.Slack.Files.Types: data FileObject
+ Web.Slack.Files.Types: data FileObjectVisible
+ Web.Slack.Files.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Files.Types.FileId
+ Web.Slack.Files.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Files.Types.FileMode
+ Web.Slack.Files.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Files.Types.FileObject
+ Web.Slack.Files.Types: instance Data.Aeson.Types.FromJSON.FromJSON Web.Slack.Files.Types.FileObjectVisible
+ Web.Slack.Files.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Files.Types.FileId
+ Web.Slack.Files.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Files.Types.FileMode
+ Web.Slack.Files.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Files.Types.FileObject
+ Web.Slack.Files.Types: instance Data.Aeson.Types.ToJSON.ToJSON Web.Slack.Files.Types.FileObjectVisible
+ Web.Slack.Files.Types: instance GHC.Classes.Eq Web.Slack.Files.Types.FileId
+ Web.Slack.Files.Types: instance GHC.Classes.Eq Web.Slack.Files.Types.FileMode
+ Web.Slack.Files.Types: instance GHC.Classes.Eq Web.Slack.Files.Types.FileObject
+ Web.Slack.Files.Types: instance GHC.Classes.Eq Web.Slack.Files.Types.FileObjectVisible
+ Web.Slack.Files.Types: instance GHC.Show.Show Web.Slack.Files.Types.FileId
+ Web.Slack.Files.Types: instance GHC.Show.Show Web.Slack.Files.Types.FileMode
+ Web.Slack.Files.Types: instance GHC.Show.Show Web.Slack.Files.Types.FileObject
+ Web.Slack.Files.Types: instance GHC.Show.Show Web.Slack.Files.Types.FileObjectVisible
+ Web.Slack.Files.Types: newtype FileId
+ Web.Slack.User: ListReq :: Maybe Cursor -> Maybe Int -> Maybe TeamId -> ListReq
+ Web.Slack.User: [listReqCursor] :: ListReq -> Maybe Cursor
+ Web.Slack.User: [listReqLimit] :: ListReq -> Maybe Int
+ Web.Slack.User: [listReqTeamId] :: ListReq -> Maybe TeamId
+ Web.Slack.User: [listRspResponseMetadata] :: ListRsp -> Maybe ResponseMetadata
+ Web.Slack.User: data ListReq
+ Web.Slack.User: instance Data.Default.Class.Default Web.Slack.User.ListReq
+ Web.Slack.User: instance GHC.Classes.Eq Web.Slack.User.ListReq
+ Web.Slack.User: instance GHC.Generics.Generic Web.Slack.User.ListReq
+ Web.Slack.User: instance GHC.Show.Show Web.Slack.User.ListReq
+ Web.Slack.User: instance Web.Internal.FormUrlEncoded.ToForm Web.Slack.User.ListReq
+ Web.Slack.User: instance Web.Slack.Pager.Types.PagedRequest Web.Slack.User.ListReq
+ Web.Slack.User: instance Web.Slack.Pager.Types.PagedResponse Web.Slack.User.ListRsp
- Web.Slack: usersList :: SlackConfig -> IO (Response ListRsp)
+ Web.Slack: usersList :: SlackConfig -> ListReq -> IO (Response ListRsp)
- Web.Slack.Classy: usersList :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => m (Response ListRsp)
+ Web.Slack.Classy: usersList :: (MonadReader env m, HasManager env, HasToken env, MonadIO m) => ListReq -> m (Response ListRsp)
- Web.Slack.Experimental.Events.Types: MessageEvent :: Maybe [SlackBlock] -> ConversationId -> Text -> ChannelType -> UserId -> Text -> Maybe Text -> Maybe Text -> Maybe Text -> MessageEvent
+ Web.Slack.Experimental.Events.Types: MessageEvent :: Maybe [SlackBlock] -> ConversationId -> Text -> ChannelType -> Maybe [FileObject] -> UserId -> Text -> Maybe Text -> Maybe Text -> Maybe Text -> MessageEvent
- Web.Slack.User: ListRsp :: [User] -> ListRsp
+ Web.Slack.User: ListRsp :: [User] -> Maybe ResponseMetadata -> ListRsp

Files

CHANGELOG.md view
@@ -1,3 +1,20 @@+# 1.6.0.0 (2022-12-14)++## Breaking changes++* `usersList` now takes a `User.ListReq` object. You can construct one with+  `data-default`'s `def` function to get the previous behaviour.++## New features++* [#120](https://github.com/MercuryTechnologies/slack-web/pull/120)+  Add file share event parsing support.++  Increase bounds on pretty-simple to 4.1 to mitigate a bug added in 4.0.++* [#121](https://github.com/MercuryTechnologies/slack-web/pull/120)+  Add pagination of `users.list`+ # 1.5.0.1  * [#119](https://github.com/MercuryTechnologies/slack-web/pull/119) Fix a typo
slack-web.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: slack-web-version: 1.5.0.1+version: 1.6.0.0  build-type: Simple @@ -27,6 +27,8 @@   tests/golden/UpdateRsp/*.json   tests/golden/UpdateRsp/*.golden   tests/golden/BlockKitBuilderMessage/*.golden.json+  tests/golden/FileObject/*.json+  tests/golden/FileObject/*.golden  category: Web @@ -113,6 +115,7 @@       Web.Slack.Classy       Web.Slack.Common       Web.Slack.Conversation+      Web.Slack.Files.Types       Web.Slack.Internal       Web.Slack.MessageParser       Web.Slack.Pager@@ -178,6 +181,7 @@       Web.Slack.MessageParserSpec       Web.Slack.ConversationSpec       Web.Slack.ChatSpec+      Web.Slack.Files.TypesSpec       Web.Slack.UsersConversationsSpec       Web.Slack.Experimental.RequestVerificationSpec       Web.Slack.Experimental.Events.TypesSpec@@ -197,7 +201,7 @@     , hspec-core     , hspec-golden     , mtl-    , pretty-simple+    , pretty-simple ^>= 4.1     , quickcheck-instances     , slack-web     , string-conversions@@ -231,7 +235,7 @@     , bytestring     , monad-loops     , mtl-    , pretty-simple+    , pretty-simple ^>= 4.1     , text     , time     , servant-client-core
src/Web/Slack.hs view
@@ -29,6 +29,7 @@     repliesFetchAll,     getUserDesc,     usersList,+    usersListAll,     userLookupByEmail,     UsersConversations.usersConversations,     UsersConversations.usersConversationsAll,@@ -92,6 +93,7 @@       :> Post '[JSON] (ResponseJSON Chat.UpdateRsp)     :<|> "users.list"       :> AuthProtect "token"+      :> ReqBody '[FormUrlEncoded] User.ListReq       :> Post '[JSON] (ResponseJSON User.ListRsp)     :<|> "users.lookupByEmail"       :> AuthProtect "token"@@ -234,27 +236,32 @@   Chat.UpdateReq ->   ClientM (ResponseJSON Chat.UpdateRsp) --- |------ This method returns a list of all users in the team.+-- | This method returns a list of all users in the team. -- This includes deleted/deactivated users. -- -- <https://api.slack.com/methods/users.list> usersList ::   SlackConfig ->+  User.ListReq ->   IO (Response User.ListRsp)-usersList = do-  authR <- mkSlackAuthenticateReq-  run (usersList_ authR) . slackConfigManager+usersList config req = do+  let authR = mkSlackAuthenticateReq config+  run (usersList_ authR req) (slackConfigManager config)  usersList_ ::   AuthenticatedRequest (AuthProtect "token") ->+  User.ListReq ->   ClientM (ResponseJSON User.ListRsp)+usersListAll ::+  SlackConfig ->+  -- | The first request to send. _NOTE_: 'User.listReqCursor' is silently ignored.+  User.ListReq ->+  -- | An action which returns a new page of messages every time called.+  --   If there are no pages anymore, it returns an empty list.+  IO (LoadPage IO User.User)+usersListAll = fetchAllBy . usersList --- |------ This method returns a list of all users in the team.--- This includes deleted/deactivated users.+-- | Find a user by email address. -- -- <https://api.slack.com/methods/users.lookupByEmail> userLookupByEmail ::
src/Web/Slack/AesonUtils.hs view
@@ -5,6 +5,7 @@ import Data.Aeson.Types (Pair) import Data.Char qualified as Char import Data.Text qualified as T+import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds) import Web.FormUrlEncoded qualified as F import Web.Slack.Prelude @@ -112,3 +113,12 @@   F.defaultFormOptions     { F.fieldLabelModifier = camelTo2 '_'     }++newtype UnixTimestamp = UnixTimestamp {unUnixTimestamp :: UTCTime}+  deriving newtype (Show, Eq)++instance FromJSON UnixTimestamp where+  parseJSON a = UnixTimestamp . posixSecondsToUTCTime <$> parseJSON a++instance ToJSON UnixTimestamp where+  toJSON (UnixTimestamp a) = toJSON (utcTimeToPOSIXSeconds a)
src/Web/Slack/Classy.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE CPP #-}- ----------------------------------------------------------------------  ----------------------------------------------------------------------@@ -29,22 +27,7 @@   ) where --- FIXME: Web.Slack.Prelude---- base-import Control.Arrow ((&&&))--- containers---- http-client---- mtl-import Control.Monad.Reader import Data.Map qualified as Map-import Data.Maybe--- slack-web---- text-import Data.Text (Text) import Network.HTTP.Client (Manager) import Web.Slack (SlackConfig (..), authenticateReq, mkSlackConfig) import Web.Slack qualified as NonClassy@@ -54,14 +37,9 @@ import Web.Slack.Common qualified as Common import Web.Slack.Conversation qualified as Conversation import Web.Slack.Pager+import Web.Slack.Prelude import Web.Slack.User qualified as User-import Prelude -#if !MIN_VERSION_servant(0,13,0)-mkClientEnv :: Manager -> BaseUrl -> ClientEnv-mkClientEnv = ClientEnv-#endif- -- | Implemented by 'SlackConfig' class HasManager a where   getManager :: a -> Manager@@ -152,8 +130,9 @@ -- <https://api.slack.com/methods/users.list> usersList ::   (MonadReader env m, HasManager env, HasToken env, MonadIO m) =>+  User.ListReq ->   m (Response User.ListRsp)-usersList = liftNonClassy NonClassy.usersList+usersList = liftNonClassy . flip NonClassy.usersList  -- | --
src/Web/Slack/Experimental/Events/Types.hs view
@@ -11,6 +11,7 @@ import Data.Time.Clock.System (SystemTime) import Web.Slack.AesonUtils import Web.Slack.Experimental.Blocks (SlackBlock)+import Web.Slack.Files.Types (FileObject) import Web.Slack.Prelude import Web.Slack.Types (ConversationId, TeamId, UserId) @@ -22,11 +23,15 @@ $(deriveJSON snakeCaseOptions ''ChannelType)  -- | <https://api.slack.com/events/message>+-- and+-- <https://api.slack.com/events/message/file_share> data MessageEvent = MessageEvent   { blocks :: Maybe [SlackBlock]   , channel :: ConversationId   , text :: Text   , channelType :: ChannelType+  , files :: Maybe [FileObject]+  -- ^ @since 1.6.0.0   , -- FIXME(jadel): clientMsgId??     user :: UserId   , ts :: Text@@ -46,6 +51,8 @@   }   deriving stock (Show) +$(deriveFromJSON snakeCaseOptions ''MessageEvent)+ -- | <https://api.slack.com/events/message/message_changed> -- -- FIXME(jadel): implement. This is mega cursed! in the normal message event@@ -58,7 +65,6 @@   }   deriving stock (Show) -$(deriveFromJSON snakeCaseOptions ''MessageEvent) $(deriveFromJSON snakeCaseOptions ''MessageUpdateEvent)  -- | FIXME: this might be a Channel, but may also be missing some fields/have bonus@@ -138,6 +144,9 @@       ("message", Nothing) -> EventMessage <$> parseJSON @MessageEvent (Object obj)       ("message", Just "message_changed") -> pure EventMessageChanged       ("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)       _ -> pure $ EventUnknown (Object obj)
+ src/Web/Slack/Files/Types.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE TemplateHaskell #-}++-- | Type definitions for the Slack files APIs.+-- See <https://api.slack.com/messaging/files>.+--+-- @since 1.6.0.0+module Web.Slack.Files.Types where++import Control.Monad.Fail (MonadFail (..))+import Data.Aeson ((.!=))+import Data.Aeson qualified as A+import Data.Aeson.KeyMap qualified as KM+import Web.Slack.AesonUtils (UnixTimestamp, snakeCaseOptions)+import Web.Slack.Prelude++-- | ID for a file, which looks something like @F2147483862@.+newtype FileId = FileId {unFileId :: Text}+  deriving stock (Show, Eq)+  deriving newtype (FromJSON, ToJSON)++data FileMode = Hosted | External | Snippet | Post | FileAccess+  deriving stock (Show, Eq)++$(deriveJSON snakeCaseOptions ''FileMode)++-- | <https://api.slack.com/types/file>+data FileObjectVisible = FileObjectVisible+  { id :: FileId+  , created :: UnixTimestamp+  , name :: Text+  , title :: Text+  , mimetype :: Text+  , urlPrivate :: Text+  , isExternal :: Bool+  , size :: Int+  , mode :: FileMode+  }+  deriving stock (Show, Eq)++$(deriveJSON snakeCaseOptions ''FileObjectVisible)++data FileObject+  = -- | File object is visible+    VisibleFileObject FileObjectVisible+  | -- | File object is in a shared channel so @files.info@ must be invoked to+    -- get any further details. See+    -- <https://api.slack.com/types/file#slack_connect_files> for more details.+    CheckFileInfo FileId+  deriving stock (Show, Eq)++instance FromJSON FileObject where+  parseJSON = withObject "FileObject" $ \obj -> do+    -- "visible" is undocumented, thanks Slack!+    ty :: Text <- obj .:? "file_access" .!= "visible"+    case ty of+      "visible" -> VisibleFileObject <$> parseJSON (A.Object obj)+      "check_file_info" -> CheckFileInfo <$> obj .: "id"+      _ -> fail $ "unknown file_access type " <> unpack ty++instance ToJSON FileObject where+  toJSON (VisibleFileObject obj) = case toJSON obj of+    A.Object o -> A.Object $ o <> KM.fromList [("file_access", "visible")]+    _ -> error "impossible"+  toJSON (CheckFileInfo fid) = A.object [("file_access", "check_file_info"), ("id", toJSON fid)]
src/Web/Slack/User.hs view
@@ -3,45 +3,26 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} --------------------------------------------------------------------------------------------------------------------------------------------------- |--- Module: Web.Slack.User--- Description:+-- | @users.*@ methods in the Slack API module Web.Slack.User   ( Profile (..),     User (..),+    ListReq (..),     ListRsp (..),     Email (..),     UserRsp (..),   ) where --- FIXME: Web.Slack.Prelude---- aeson-import Data.Aeson.TH--- base---- slack-web---- text-import Data.Text (Text)--- time import Data.Time.Clock.POSIX-import GHC.Generics (Generic)--- http-api-data- import Web.FormUrlEncoded import Web.HttpApiData import Web.Slack.Common+import Web.Slack.Pager.Types (PagedRequest (..), PagedResponse (..), ResponseMetadata)+import Web.Slack.Prelude import Web.Slack.Util-import Prelude --- See https://api.slack.com/types/user-+-- | See <https://api.slack.com/types/user> data Profile = Profile   { profileAvatarHash :: Maybe Text   , profileStatusText :: Maybe Text@@ -80,12 +61,39 @@  $(deriveFromJSON (jsonOpts "user") ''User) +-- | @users.list@ request. See <https://api.slack.com/methods/users.list#args>+--+-- @since 1.6.0.0+data ListReq = ListReq+  { listReqCursor :: Maybe Cursor+  , listReqLimit :: Maybe Int+  , listReqTeamId :: Maybe TeamId+  }+  deriving stock (Show, Generic, Eq)+  deriving anyclass (Default)++instance ToForm ListReq where+  toForm (ListReq {listReqCursor, listReqLimit, listReqTeamId}) =+    toQueryParamIfJust "cursor" listReqCursor+      <> toQueryParamIfJust "limit" listReqLimit+      <> toQueryParamIfJust "team_id" listReqTeamId++instance PagedRequest ListReq where+  setCursor c r = r {listReqCursor = c}++-- | Response to <https://api.slack.com/methods/users.list> data ListRsp = ListRsp   { listRspMembers :: [User]+  , listRspResponseMetadata :: Maybe ResponseMetadata   }   deriving stock (Eq, Generic, Show)  $(deriveFromJSON (jsonOpts "listRsp") ''ListRsp)++instance PagedResponse ListRsp where+  type ResponseObject ListRsp = User+  getResponseData = listRspMembers+  getResponseMetadata = listRspResponseMetadata  data UserRsp = UserRsp   { userRspUser :: User
tests/Web/Slack/Experimental/Events/TypesSpec.hs view
@@ -13,6 +13,8 @@         [ "messageExample"         , "messageChange"         , "message_rich_text"+        , "message_file_share"+        , "message_file_share_slack_connect"         , "link"         , "botMessage"         , "joinChannel"
+ tests/Web/Slack/Files/TypesSpec.hs view
@@ -0,0 +1,15 @@+module Web.Slack.Files.TypesSpec (spec) where++import JSONGolden+import TestImport+import Web.Slack.Files.Types++spec :: Spec+spec = describe "Types for Slack files" do+  describe "FileObject" do+    describe "FromJSON" do+      mapM_+        (oneGoldenTestDecode @FileObject)+        [ "example"+        , "real"+        ]
+ tests/golden/FileObject/example.golden view
@@ -0,0 +1,14 @@+VisibleFileObject+    ( FileObjectVisible+        { id = FileId+            { unFileId = "F0S43PZDF" }+        , created = 2018-07-16 17:49:02 UTC+        , name = "tedair.gif"+        , title = "tedair.gif"+        , mimetype = "image/gif"+        , urlPrivate = "https://.../tedair.gif"+        , isExternal = False+        , size = 137531+        , mode = Hosted+        }+    )
+ tests/golden/FileObject/example.json view
@@ -0,0 +1,58 @@+{+    "id": "F0S43PZDF",+    "created": 1531763342,+    "timestamp": 1531763342,+    "name": "tedair.gif",+    "title": "tedair.gif",+    "mimetype": "image/gif",+    "filetype": "gif",+    "pretty_type": "GIF",+    "user": "U061F7AUR",+    "editable": false,+    "size": 137531,+    "mode": "hosted",+    "is_external": false,+    "external_type": "",+    "is_public": true,+    "public_url_shared": false,+    "display_as_bot": false,+    "username": "",+    "url_private": "https://.../tedair.gif",+    "url_private_download": "https://.../tedair.gif",+    "thumb_64": "https://.../tedair_64.png",+    "thumb_80": "https://.../tedair_80.png",+    "thumb_360": "https://.../tedair_360.png",+    "thumb_360_w": 176,+    "thumb_360_h": 226,+    "thumb_160": "https://.../tedair_=_160.png",+    "thumb_360_gif": "https://.../tedair_360.gif",+    "image_exif_rotation": 1,+    "original_w": 176,+    "original_h": 226,+    "deanimate_gif": "https://.../tedair_deanimate_gif.png",+    "pjpeg": "https://.../tedair_pjpeg.jpg",+    "permalink": "https://.../tedair.gif",+    "permalink_public": "https://.../...",+    "comments_count": 0,+    "is_starred": false,+    "shares": {+        "public": {+            "C0T8SE4AU": [+                {+                    "reply_users": ["U061F7AUR"],+                    "reply_users_count": 1,+                    "reply_count": 1,+                    "ts": "1531763348.000001",+                    "thread_ts": "1531763273.000015",+                    "latest_reply": "1531763348.000001",+                    "channel_name": "file-under",+                    "team_id": "T061EG9R6"+                }+            ]+        }+    },+    "channels": ["C0T8SE4AU"],+    "groups": [],+    "ims": [],+    "has_rich_preview": false+}
+ tests/golden/FileObject/real.golden view
@@ -0,0 +1,14 @@+VisibleFileObject+    ( FileObjectVisible+        { id = FileId+            { unFileId = "F04EEHX1EES" }+        , created = 2022-12-10 00:18:47 UTC+        , name = "namenamename.txt"+        , title = "namenamename.txt"+        , mimetype = "text/plain"+        , urlPrivate = "https://files.slack.com/files-pri/T043DB835ML-F04EEHX1EES/namenamename.txt"+        , isExternal = False+        , size = 12+        , mode = Snippet+        }+    )
+ tests/golden/FileObject/real.json view
@@ -0,0 +1,33 @@+{+    "id": "F04EEHX1EES",+    "created": 1670631527,+    "timestamp": 1670631527,+    "name": "namenamename.txt",+    "title": "namenamename.txt",+    "mimetype": "text/plain",+    "filetype": "text",+    "pretty_type": "Plain Text",+    "user": "U043H11ES4V",+    "user_team": "T043DB835ML",+    "editable": true,+    "size": 12,+    "mode": "snippet",+    "is_external": false,+    "external_type": "",+    "is_public": false,+    "public_url_shared": false,+    "display_as_bot": false,+    "username": "",+    "url_private": "https://files.slack.com/files-pri/T043DB835ML-F04EEHX1EES/namenamename.txt",+    "url_private_download": "https://files.slack.com/files-pri/T043DB835ML-F04EEHX1EES/download/namenamename.txt",+    "permalink": "https://jadeapptesting.slack.com/files/U043H11ES4V/F04EEHX1EES/namenamename.txt",+    "permalink_public": "https://slack-files.com/T043DB835ML-F04EEHX1EES-0ce8936320",+    "edit_link": "https://jadeapptesting.slack.com/files/U043H11ES4V/F04EEHX1EES/namenamename.txt/edit",+    "preview": "hug a kitten",+    "preview_highlight": "<div class=\"CodeMirror cm-s-default CodeMirrorServer\" oncopy=\"if(event.clipboardData){event.clipboardData.setData('text/plain',window.getSelection().toString().replace(/\\u200b/g,''));event.preventDefault();event.stopPropagation();}\">\n<div class=\"CodeMirror-code\">\n<div><pre>hug a kitten</pre></div>\n</div>\n</div>\n",+    "lines": 1,+    "lines_more": 0,+    "preview_is_truncated": false,+    "has_rich_preview": false,+    "file_access": "visible"+}
tests/golden/SlackWebhookEvent/botMessage.golden view
@@ -30,6 +30,7 @@                     { unConversationId = "C043YJGBY49" }                 , text = "TEST"                 , channelType = Channel+                , files = Nothing                 , user = UserId                     { unUserId = "U0442US8QGH" }                 , ts = "1664216870.477049"
tests/golden/SlackWebhookEvent/link.golden view
@@ -34,6 +34,7 @@                     { unConversationId = "C043YJGBY49" }                 , text = "<https://jadeapptesting.slack.com/archives/C043YJGBY49/p1663961604007869>"                 , channelType = Channel+                , files = Nothing                 , user = UserId                     { unUserId = "U043H11ES4V" }                 , ts = "1663978925.099999"
tests/golden/SlackWebhookEvent/messageExample.golden view
@@ -30,6 +30,7 @@                     { unConversationId = "C043YJGBY49" }                 , text = "dgsfklsdgf"                 , channelType = Channel+                , files = Nothing                 , user = UserId                     { unUserId = "U043H11ES4V" }                 , ts = "1663966382.046509"
tests/golden/SlackWebhookEvent/messageIm.golden view
@@ -30,6 +30,7 @@                     { unConversationId = "D0442US94JD" }                 , text = "test"                 , channelType = Im+                , files = Nothing                 , user = UserId                     { unUserId = "U043H11ES4V" }                 , ts = "1664408649.009629"
+ tests/golden/SlackWebhookEvent/message_file_share.golden view
@@ -0,0 +1,58 @@+EventEventCallback+    ( EventCallback+        { eventId = EventId+            { unEventId = "Ev04EEHX2PSS" }+        , teamId = TeamId+            { unTeamId = "T043DB835ML" }+        , eventTime = MkSystemTime+            { systemSeconds = 1670631527+            , systemNanoseconds = 0+            }+        , event = EventMessage+            ( MessageEvent+                { blocks = Just+                    [ RichText+                        { blockId = Just+                            ( NonEmptyText "MeAN" )+                        , elements =+                            [ RichTextSectionItemRichText+                                [ RichItemText "message message message"+                                    ( RichStyle+                                        { rsBold = False+                                        , rsItalic = False+                                        }+                                    )+                                ]+                            ]+                        }+                    ]+                , channel = ConversationId+                    { unConversationId = "D0442US94JD" }+                , text = "message message message"+                , channelType = Im+                , files = Just+                    [ VisibleFileObject+                        ( FileObjectVisible+                            { id = FileId+                                { unFileId = "F04EEHX1EES" }+                            , created = 2022-12-10 00:18:47 UTC+                            , name = "namenamename.txt"+                            , title = "namenamename.txt"+                            , mimetype = "text/plain"+                            , urlPrivate = "https://files.slack.com/files-pri/T043DB835ML-F04EEHX1EES/namenamename.txt"+                            , isExternal = False+                            , size = 12+                            , mode = Snippet+                            }+                        )+                    ]+                , user = UserId+                    { unUserId = "U043H11ES4V" }+                , ts = "1670631527.874149"+                , threadTs = Nothing+                , appId = Nothing+                , botId = Nothing+                }+            )+        }+    )
+ tests/golden/SlackWebhookEvent/message_file_share.json view
@@ -0,0 +1,84 @@+{+  "token": "aaaa",+  "team_id": "T043DB835ML",+  "api_app_id": "A0442TUPHGR",+  "event": {+    "type": "message",+    "text": "message message message",+    "files": [+      {+        "id": "F04EEHX1EES",+        "created": 1670631527,+        "timestamp": 1670631527,+        "name": "namenamename.txt",+        "title": "namenamename.txt",+        "mimetype": "text/plain",+        "filetype": "text",+        "pretty_type": "Plain Text",+        "user": "U043H11ES4V",+        "user_team": "T043DB835ML",+        "editable": true,+        "size": 12,+        "mode": "snippet",+        "is_external": false,+        "external_type": "",+        "is_public": false,+        "public_url_shared": false,+        "display_as_bot": false,+        "username": "",+        "url_private": "https://files.slack.com/files-pri/T043DB835ML-F04EEHX1EES/namenamename.txt",+        "url_private_download": "https://files.slack.com/files-pri/T043DB835ML-F04EEHX1EES/download/namenamename.txt",+        "permalink": "https://jadeapptesting.slack.com/files/U043H11ES4V/F04EEHX1EES/namenamename.txt",+        "permalink_public": "https://slack-files.com/T043DB835ML-F04EEHX1EES-0ce8936320",+        "edit_link": "https://jadeapptesting.slack.com/files/U043H11ES4V/F04EEHX1EES/namenamename.txt/edit",+        "preview": "hug a kitten",+        "preview_highlight": "<div class=\"CodeMirror cm-s-default CodeMirrorServer\" oncopy=\"if(event.clipboardData){event.clipboardData.setData('text/plain',window.getSelection().toString().replace(/\\u200b/g,''));event.preventDefault();event.stopPropagation();}\">\n<div class=\"CodeMirror-code\">\n<div><pre>hug a kitten</pre></div>\n</div>\n</div>\n",+        "lines": 1,+        "lines_more": 0,+        "preview_is_truncated": false,+        "has_rich_preview": false,+        "file_access": "visible"+      }+    ],+    "upload": true,+    "user": "U043H11ES4V",+    "display_as_bot": false,+    "ts": "1670631527.874149",+    "blocks": [+      {+        "type": "rich_text",+        "block_id": "MeAN",+        "elements": [+          {+            "type": "rich_text_section",+            "elements": [+              {+                "type": "text",+                "text": "message message message"+              }+            ]+          }+        ]+      }+    ],+    "client_msg_id": "a5a11822-d14e-4621-992b-32d561648aa0",+    "channel": "D0442US94JD",+    "subtype": "file_share",+    "event_ts": "1670631527.874149",+    "channel_type": "im"+  },+  "type": "event_callback",+  "event_id": "Ev04EEHX2PSS",+  "event_time": 1670631527,+  "authorizations": [+    {+      "enterprise_id": null,+      "team_id": "T043DB835ML",+      "user_id": "U0442US8QGH",+      "is_bot": true,+      "is_enterprise_install": false+    }+  ],+  "is_ext_shared_channel": false,+  "event_context": "aaaa"+}
+ tests/golden/SlackWebhookEvent/message_file_share_slack_connect.golden view
@@ -0,0 +1,33 @@+EventEventCallback+    ( EventCallback+        { eventId = EventId+            { unEventId = "Ev04EEHX2PSS" }+        , teamId = TeamId+            { unTeamId = "T043DB835ML" }+        , eventTime = MkSystemTime+            { systemSeconds = 1670631527+            , systemNanoseconds = 0+            }+        , event = EventMessage+            ( MessageEvent+                { blocks = Nothing+                , channel = ConversationId+                    { unConversationId = "D0442US94JD" }+                , text = "message message message"+                , channelType = Im+                , files = Just+                    [ CheckFileInfo+                        ( FileId+                            { unFileId = "F12345678" }+                        )+                    ]+                , user = UserId+                    { unUserId = "U043H11ES4V" }+                , ts = "1670631527.874149"+                , threadTs = Nothing+                , appId = Nothing+                , botId = Nothing+                }+            )+        }+    )
+ tests/golden/SlackWebhookEvent/message_file_share_slack_connect.json view
@@ -0,0 +1,42 @@+{+  "token": "aaaa",+  "team_id": "T043DB835ML",+  "api_app_id": "A0442TUPHGR",+  "event": {+    "type": "message",+    "text": "message message message",+    "files": [+      {+        "id": "F12345678",+        "mode": "file_access",+        "file_access": "check_file_info",+        "created": 0,+        "timestamp": 0,+        "user": ""+      }+    ],+    "upload": true,+    "user": "U043H11ES4V",+    "display_as_bot": false,+    "ts": "1670631527.874149",+    "client_msg_id": "a5a11822-d14e-4621-992b-32d561648aa0",+    "channel": "D0442US94JD",+    "subtype": "file_share",+    "event_ts": "1670631527.874149",+    "channel_type": "im"+  },+  "type": "event_callback",+  "event_id": "Ev04EEHX2PSS",+  "event_time": 1670631527,+  "authorizations": [+    {+      "enterprise_id": null,+      "team_id": "T043DB835ML",+      "user_id": "U0442US8QGH",+      "is_bot": true,+      "is_enterprise_install": false+    }+  ],+  "is_ext_shared_channel": false,+  "event_context": "aaaa"+}
tests/golden/SlackWebhookEvent/message_rich_text.golden view
@@ -72,6 +72,7 @@                     { unConversationId = "C045V0VJT16" }                 , text = "<#C043YJGBY49|general> :cat: <@U043H11ES4V> <https://google.com> text text text"                 , channelType = Channel+                , files = Nothing                 , user = UserId                     { unUserId = "U043H11ES4V" }                 , ts = "1670625739.704049"
tests/golden/SlackWebhookEvent/share_without_message.golden view
@@ -15,6 +15,7 @@                     { unConversationId = "C043YJGBY49" }                 , text = ""                 , channelType = Channel+                , files = Nothing                 , user = UserId                     { unUserId = "U043H11ES4V" }                 , ts = "1668537593.598469"
tests/golden/SlackWebhookEvent/slackbotIm.golden view
@@ -39,6 +39,7 @@                     { unConversationId = "D043HMJ0WDU" }                 , text = "You have been removed from #testing-slack-app by <@U043H11ES4V>"                 , channelType = Im+                , files = Nothing                 , user = UserId                     { unUserId = "USLACKBOT" }                 , ts = "1664826477.425039"