packages feed

slack-web-1.5.0.0: src/Web/Slack/Experimental/Blocks.hs

module Web.Slack.Experimental.Blocks
  ( -- * General Slack Messages
    SlackText,
    (<+>),
    parens,
    brackets,
    angleBrackets,
    ticks,
    codeBlock,
    bold,
    italic,
    newline,
    unorderedList,
    link,
    monospaced,
    mentionUser,
    isSubStringOf,
    SlackImage (..),
    SlackMessage,
    Markdown (..),
    Image (..),
    context,
    textToMessage,
    prefixFirstSlackMessage,
    mentionUserGroupById,
    textToContext,
    slackMessage,
    SlackBlock (..),

    -- ** Blocks' rich text formatting (receive only!)
    RichItem (..),
    RichStyle (..),
    RichLinkAttrs (..),
    RichTextSectionItem (..),
    RichText (..),

    -- * Rendered messages
    RenderedSlackMessage (..),
    render,

    -- * Introduction to Slack Interactive Messages
    -- $interactive

    -- * Creating Slack Interactive Messages
    actions,
    actionsWithBlockId,
    SlackActionId (..),
    SlackBlockId,
    setting,
    emptySetting,
    SlackStyle (..),
    plaintext,
    plaintextonly,
    mrkdwn,
    button,
    buttonSettings,
    ButtonSettings
      ( buttonUrl,
        buttonValue,
        buttonStyle,
        buttonConfirm
      ),
    confirm,
    confirmAreYouSure,
    ConfirmSettings
      ( confirmTitle,
        confirmText,
        confirmConfirm,
        confirmDeny,
        confirmStyle
      ),

    -- * Responding to Slack Interactive Messages
    SlackInteractiveResponse (..),
  )
where

import Data.Aeson.Text (encodeToLazyText)
import Data.Text qualified as T
import Web.Slack.Experimental.Blocks.Types
import Web.Slack.Prelude
import Web.Slack.Types

(<+>) :: SlackText -> SlackText -> SlackText
x <+> y = x <> " " <> y

parens :: SlackText -> SlackText
parens x = "(" <> x <> ")"

brackets :: SlackText -> SlackText
brackets x = "[" <> x <> "]"

angleBrackets :: SlackText -> SlackText
angleBrackets x = "<" <> x <> ">"

ticks :: SlackText -> SlackText
ticks x = "`" <> x <> "`"

-- | Render a 'Slack' renderable value with ticks around it. Alias for @ticks . message@
monospaced :: Slack a => a -> SlackText
monospaced = ticks . message

codeBlock :: SlackText -> SlackText
codeBlock x = "```\n" <> x <> "\n```"

bold :: SlackText -> SlackText
bold x = "*" <> x <> "*"

italic :: SlackText -> SlackText
italic x = "_" <> x <> "_"

newline :: SlackText -> SlackText
newline x = "\n" <> x

-- | Render an unordered (bulleted) list
unorderedList :: [SlackText] -> SlackText
unorderedList = mconcat . fmap (\t -> newline (message @Text "- " <> t))

-- | https://api.slack.com/reference/surfaces/formatting#mentioning-users
mentionUser :: UserId -> SlackText
mentionUser slackUserId = message $ "<@" <> unUserId slackUserId <> ">"

-- | https://api.slack.com/reference/surfaces/formatting#mentioning-groups
mentionUserGroupById :: SlackText -> SlackText
mentionUserGroupById userGroupId = angleBrackets $ "!subteam^" <> userGroupId

isSubStringOf :: Text -> SlackText -> Bool
needle `isSubStringOf` (SlackText haystack) = needle `isInfixOf` concat haystack

-- | RenderedSlackMessage contains the original SlackMessage, the rendered version, and a
--   boolean indicating whether truncation was done.
--
--   Usage:
--
--   @
--      let
--        msg =
--        (mkPostMsgReq channel "")
--          { postMsgReqBlocks = Just blocks
--          , postMsgReqThreadTs = mThreadTs
--          }
--    chatPostMessage msg
--   @
data RenderedSlackMessage = RenderedSlackMessage
  { _originalMessage :: SlackMessage
  , _renderedMessage :: Text
  , _truncated :: Bool
  }

render :: SlackMessage -> RenderedSlackMessage
render sm =
  let (truncatedSm, isTruncated) = truncateSlackMessage sm
   in RenderedSlackMessage sm (cs . encodeToLazyText . toJSON $ truncatedSm) isTruncated

-- | Slack will fail if any text block is over 3000 chars.
--   truncateSlackMessage will truncate all text blocks and also return an bool
--   indicating whether any truncation was done.
truncateSlackMessage :: SlackMessage -> (SlackMessage, Bool)
truncateSlackMessage (SlackMessage blocks) =
  let (truncatedBlocks, isTruncateds) = unzip $ map truncateSlackBlock blocks
   in (SlackMessage truncatedBlocks, or isTruncateds)

truncateSlackBlock :: SlackBlock -> (SlackBlock, Bool)
truncateSlackBlock sb@(SlackBlockSection (SlackText texts) mAccessory) =
  let messageLength = sum $ map T.length texts
      lengthLimit = 3000
      truncationMessage = "\n...Rest of message truncated for slack\n"
      truncationMessageLength = T.length truncationMessage
      truncateTexts ts = take (lengthLimit - truncationMessageLength) (concat ts)
   in if messageLength > lengthLimit
        then (SlackBlockSection (SlackText [truncateTexts texts <> "\n...Rest of message truncated for slack\n"]) mAccessory, True)
        else (sb, False)
-- possible to also truncate SlackContexts, but we never put long strings in there.
truncateSlackBlock x = (x, False)

prefixFirstSlackBlockSection :: Text -> [SlackBlock] -> ([SlackBlock], Bool)
prefixFirstSlackBlockSection prefix (SlackBlockSection text mAccessory : sbs) = (SlackBlockSection (message prefix <> text) mAccessory : sbs, True)
prefixFirstSlackBlockSection prefix (sb : sbs) = let (prefixedSbs, match) = prefixFirstSlackBlockSection prefix sbs in (sb : prefixedSbs, match)
prefixFirstSlackBlockSection _ [] = ([], False)

prefixFirstSlackMessage :: Text -> [SlackMessage] -> [SlackMessage]
prefixFirstSlackMessage prefix (sm : sms) =
  let SlackMessage slackBlocks = sm
      (prefixedSlackBlocks, match) = prefixFirstSlackBlockSection prefix slackBlocks
   in if match
        then SlackMessage prefixedSlackBlocks : sms
        else sm : prefixFirstSlackMessage prefix sms
prefixFirstSlackMessage _ [] = []

-- | Concatenate a list of 'SlackText' into a single block, and wrap it up as a full message
slackMessage :: [SlackText] -> SlackMessage
slackMessage = SlackMessage . pure . (`SlackBlockSection` Nothing) . mconcat

-- $interactive
--
-- = Slack Interactive Messages
--
-- First, familiarize yourself with [Slack's Interactivity documentation](https://api.slack.com/interactivity).
-- Currently we only support [Block Kit interactive components](https://api.slack.com/interactivity/components),
-- i.e. components like buttons attached to messages.
--
-- To make an Slack message interactive, you need to include an \"Actions\" block that has one or more interactive components.
-- These should generally only be created using the builder functions such as 'actions' and 'button'. Consumers of this module
-- should avoid directly importing "Web.Slack.Experimental.Blocks.Types".