diff --git a/package.yaml b/package.yaml index ab02e2e..d0d3e45 100644 --- a/package.yaml +++ b/package.yaml @@ -103,7 +103,6 @@ tests: - katip - nyan-interpolation - servant-client-core - - string-conversions - tasty - tasty-hspec - tasty-hunit diff --git a/src/TzBot/Slack/API/MessageBlock.hs b/src/TzBot/Slack/API/MessageBlock.hs index 74e3caa..ffeffac 100644 --- a/src/TzBot/Slack/API/MessageBlock.hs +++ b/src/TzBot/Slack/API/MessageBlock.hs @@ -202,15 +202,26 @@ extractPieces mBlocks = runWriter $ concat <$> mapM goMessageBlock mBlocks go :: Maybe Builder -> [Text] -> [WithUnknown BlockElementLevel2] -> Writer [ExtractError] [Text] go mbCurPiece prevPieces (e:es) = case unUnknown e of Left val -> do - let _type = fromMaybe "unknown" (val ^? key "type" . _String) - tell [EEUnknownBlockElementLevel2 $ UnknownBlockElementLevel2Error _type val] - go Nothing (prependMbCurrentToPrevious mbCurPiece prevPieces) es + let blockType = fromMaybe "unknown" (val ^? key "type" . _String) + case blockType of + "emoji" -> + -- skip over emoji blocks + go mbCurPiece prevPieces es + _ -> do + -- return an error if we find a block type we don't recognize. + when (blockType `notElem` knownBlockTypes) do + tell [EEUnknownBlockElementLevel2 $ UnknownBlockElementLevel2Error blockType val] + -- break the message in two separate `Text` pieces. + go Nothing (prependMbCurrentToPrevious mbCurPiece prevPieces) es Right (BEL2ElementText elementText) -> do let etTextB = fromText elementText.etText if (elementText.etStyle >>= styCode) == Just True - -- ignore inline code block - then go Nothing (prependMbCurrentToPrevious mbCurPiece prevPieces) es - else go (Just $ maybe etTextB (<> etTextB) mbCurPiece) prevPieces es + then + -- ignore inline code block + go Nothing (prependMbCurrentToPrevious mbCurPiece prevPieces) es + else + -- collate this block's text with any adjacent text-like block + go (Just $ maybe etTextB (<> etTextB) mbCurPiece) prevPieces es Right (BEL2ElementLink elementLink) -> do let linkText = fromText elementLink.elText go (Just $ maybe linkText (<> linkText) mbCurPiece) prevPieces es @@ -220,3 +231,10 @@ extractPieces mBlocks = runWriter $ concat <$> mapM goMessageBlock mBlocks prependMbCurrentToPrevious :: Maybe Builder -> [Text] -> [Text] prependMbCurrentToPrevious mbCurPiece prevPieces = maybe prevPieces ((: prevPieces) . cs . toLazyText) mbCurPiece + + knownBlockTypes :: [Text] + knownBlockTypes = + [ "user" -- A user tag, e.g. `@gromak` + , "usergroup" -- A tag for a user group, e.g. `@ligo-lsp-project + , "broadcast" -- A tag like `@here` or `@channel` + ] diff --git a/test/Test/TzBot/MessageBlocksSpec.hs b/test/Test/TzBot/MessageBlocksSpec.hs index eabf7ec..b688926 100644 --- a/test/Test/TzBot/MessageBlocksSpec.hs +++ b/test/Test/TzBot/MessageBlocksSpec.hs @@ -9,13 +9,13 @@ module Test.TzBot.MessageBlocksSpec import TzPrelude -import Data.Aeson (decode) +import Data.Aeson (Value, parseJSON) +import Data.Aeson.QQ.Simple (aesonQQ) +import Data.Aeson.Types (parseMaybe) import Data.Maybe (fromJust) -import Data.String.Conversions import Test.Tasty (TestTree) import Test.Tasty.HUnit (testCase, (@?=)) import Test.Tasty.Runners (TestTree(..)) -import Text.Interpolation.Nyan import TzBot.ProcessEvents.Common (ignoreCodeBlocksManually) import TzBot.Slack.API.MessageBlock @@ -45,7 +45,7 @@ test_ignoreManuallySpec = TestGroup "Test `ignoreCodeBlocksManually`" $ test_messageBlocksSpec :: TestTree test_messageBlocksSpec = TestGroup "Message blocks" $ [ testCase "Correct extraction of all text pieces ignoring code blocks" $ do - let justEverything = fromJust $ decode $ cs justEverythingRaw + let justEverything = fromJust $ parseMaybe @_ @[MessageBlock] parseJSON messageBlocksJSON res = extractPieces justEverything fst res @?= [ "1plain " @@ -54,355 +54,372 @@ test_messageBlocksSpec = TestGroup "Message blocks" $ , "3.1quote block " , " 3.2quote block" , "4.1plain " - , " 4.1strike " - , " 4.1bold " + , " 4.1strike 4.1bold" , "4.2plain " - , " 4.2strike github 4.2bold " + , " 4.2strike github 4.2bold" , "between the lists\n" , "5.1plain " - , " 5.1strike " - , " 5.1bold" + , " 5 .1strike " + , " 5 .1bold" , "5.2something " , " 10am " , " 10" , "am I a human?" , "end!" ] - getLevel2Errors (snd res) @?= - [ "emoji", "user", "broadcast" - ] + getLevel2Errors (snd res) @?= [] ] -{- | The original Slack message: -1plain `code` ~1strike~ `code` *bold* ~_1strikeditalic_~ +{- | The original Slack message (in markdown format): + +@ +1plain `code` ~1strike~ `code` *bold* _~1strikeditalic~_ ```2big code block``` -> 3.1quote block `code` 3.2quote block -1. 4.1plain `code` ~4.1strike~ :slightly_smiling_face: *4.1bold* -2. 4.2plain `code` ~4.2strike~ *4.2bold* +> 3.1quote block `code` 3.2quote block +1. 4.1plain `code` ~4.1strike~ *:slightly_smiling_face:* *4.1bold* +2. 4.2plain `code` ~4.2strike~ [github](http://github.com) *4.2bold* between the lists -\8226 5.1plain `code` 5~.1strike~ `code` 5*.1bold* -\8226 5.2something <@U04FQH806E9> 10am 10 -\8226 am I a human? +* 5.1plain `code` 5 ~.1strike~ `code` 5 *.1bold* +* 5.2something @dc 10am @here 10 +* am I a human? end! - -} -justEverythingRaw :: ByteString -justEverythingRaw = [int|| +@ + +To obtain the corresponding block elements: + +1. Paste this message into a slack channel +2. Right-click the message, "Copy link". + The link should contain the channel ID and message timestamp, + e.g.: https://diogotest.slack.com/archives/C02N85E82LV/p1694174625423609 +3. Retrieve the block elements via the Web API. + Use the command below, replacing the Bot Token, channel ID, and message timestamp. + +@ +curl 'https://slack.com/api/conversations.history' \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer ' \ + -d '{"channel": "C02N85E82LV", "latest": "1694174625.423609", "limit": 1, "inclusive": true}' \ + | jq '.messages[0].blocks' +@ + +-} +messageBlocksJSON :: Value +messageBlocksJSON = [aesonQQ| [ - { - "block_id": "tH8as", + { + "type": "rich_text", + "block_id": "h35BL", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "text", + "text": "1plain " + }, + { + "type": "text", + "text": "code", + "style": { + "code": true + } + }, + { + "type": "text", + "text": " " + }, + { + "type": "text", + "text": "1strike", + "style": { + "strike": true + } + }, + { + "type": "text", + "text": " " + }, + { + "type": "text", + "text": "code", + "style": { + "code": true + } + }, + { + "type": "text", + "text": " " + }, + { + "type": "text", + "text": "bold", + "style": { + "bold": true + } + }, + { + "type": "text", + "text": " " + }, + { + "type": "text", + "text": "1strikeditalic", + "style": { + "italic": true, + "strike": true + } + }, + { + "type": "text", + "text": "\n" + } + ] + }, + { + "type": "rich_text_preformatted", + "elements": [ + { + "type": "text", + "text": "2big code block" + } + ], + "border": 0 + }, + { + "type": "rich_text_quote", "elements": [ - { - "elements": [ - { - "text": "1plain ", - "type": "text" - }, - { - "style": { - "code": true - }, - "text": "code", - "type": "text" - }, - { - "text": " ", - "type": "text" - }, - { - "style": { - "strike": true - }, - "text": "1strike", - "type": "text" - }, - { - "text": " ", - "type": "text" - }, - { - "style": { - "code": true - }, - "text": "code", - "type": "text" - }, - { - "text": " ", - "type": "text" - }, - { - "style": { - "bold": true - }, - "text": "bold", - "type": "text" - }, - { - "text": " ", - "type": "text" - }, - { - "style": { - "italic": true, - "strike": true - }, - "text": "1strikeditalic", - "type": "text" - }, - { - "text": "\\n", - "type": "text" - } - ], - "type": "rich_text_section" - }, - { - "border": 0, - "elements": [ - { - "text": "2big code block", - "type": "text" - } - ], - "type": "rich_text_preformatted" - }, - { - "elements": [ - { - "text": "3.1quote block ", - "type": "text" - }, - { - "style": { - "code": true - }, - "text": "code", - "type": "text" - }, - { - "text": " 3.2quote block", - "type": "text" - } - ], - "type": "rich_text_quote" - }, - { - "border": 0, - "elements": [ - { - "elements": [ - { - "text": "4.1plain ", - "type": "text" - }, - { - "style": { - "code": true - }, - "text": "code", - "type": "text" - }, - { - "text": " ", - "type": "text" - }, - { - "style": { - "strike": true - }, - "text": "4.1strike", - "type": "text" - }, - { - "text": " ", - "type": "text" - }, - { - "name": "slightly_smiling_face", - "style": { - "bold": true - }, - "type": "emoji", - "unicode": "1f642" - }, - { - "text": " ", - "type": "text" - }, - { - "style": { - "bold": true - }, - "text": "4.1bold ", - "type": "text" - } - ], - "type": "rich_text_section" - }, - { - "elements": [ - { - "text": "4.2plain ", - "type": "text" - }, - { - "style": { - "code": true - }, - "text": "code", - "type": "text" - }, - { - "text": " ", - "type": "text" - }, - { - "style": { - "strike": true - }, - "text": "4.2strike", - "type": "text" - }, - { - "text": " ", - "type": "text" - }, - { - "text": "github", - "type": "link", - "url": "http://github.com" - }, - { - "text": " ", - "type": "text" - }, - { - "style": { - "bold": true - }, - "text": "4.2bold ", - "type": "text" - } - ], - "type": "rich_text_section" - } - ], - "indent": 0, - "style": "ordered", - "type": "rich_text_list" - }, - { - "elements": [ - { - "text": "between the lists\\n", - "type": "text" - } - ], - "type": "rich_text_section" - }, - { - "border": 0, - "elements": [ - { - "elements": [ - { - "text": "5.1plain ", - "type": "text" - }, - { - "style": { - "code": true - }, - "text": "code", - "type": "text" - }, - { - "text": " 5", - "type": "text" - }, - { - "style": { - "strike": true - }, - "text": ".1strike", - "type": "text" - }, - { - "text": " ", - "type": "text" - }, - { - "style": { - "code": true - }, - "text": "code", - "type": "text" - }, - { - "text": " 5", - "type": "text" - }, - { - "style": { - "bold": true - }, - "text": ".1bold", - "type": "text" - } - ], - "type": "rich_text_section" - }, - { - "elements": [ - { - "text": "5.2something ", - "type": "text" - }, - { - "type": "user", - "user_id": "U04FQH806E9" - }, - { - "text": " 10am ", - "type": "text" - }, - { - "range": "here", - "type": "broadcast" - }, - { - "text": " 10", - "type": "text" - } - ], - "type": "rich_text_section" - }, - { - "elements": [ - { - "text": "am I a human?", - "type": "text" - } - ], - "type": "rich_text_section" - } - ], - "indent": 0, - "style": "bullet", - "type": "rich_text_list" - }, - { - "elements": [ - { - "text": "end!", - "type": "text" - } - ], - "type": "rich_text_section" + { + "type": "text", + "text": "3.1quote block " + }, + { + "type": "text", + "text": "code", + "style": { + "code": true } + }, + { + "type": "text", + "text": " 3.2quote block" + } + ] + }, + { + "type": "rich_text_list", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "text", + "text": "4.1plain " + }, + { + "type": "text", + "text": "code", + "style": { + "code": true + } + }, + { + "type": "text", + "text": " " + }, + { + "type": "text", + "text": "4.1strike", + "style": { + "strike": true + } + }, + { + "type": "text", + "text": " " + }, + { + "type": "emoji", + "name": "slightly_smiling_face", + "unicode": "1f642", + "style": { + "bold": true + } + }, + { + "type": "text", + "text": " " + }, + { + "type": "text", + "text": "4.1bold", + "style": { + "bold": true + } + } + ] + }, + { + "type": "rich_text_section", + "elements": [ + { + "type": "text", + "text": "4.2plain " + }, + { + "type": "text", + "text": "code", + "style": { + "code": true + } + }, + { + "type": "text", + "text": " " + }, + { + "type": "text", + "text": "4.2strike", + "style": { + "strike": true + } + }, + { + "type": "text", + "text": " " + }, + { + "type": "link", + "url": "http://github.com", + "text": "github" + }, + { + "type": "text", + "text": " " + }, + { + "type": "text", + "text": "4.2bold", + "style": { + "bold": true + } + } + ] + } + ], + "style": "ordered", + "indent": 0, + "border": 0 + }, + { + "type": "rich_text_section", + "elements": [ + { + "type": "text", + "text": "between the lists\n" + } + ] + }, + { + "type": "rich_text_list", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "text", + "text": "5.1plain " + }, + { + "type": "text", + "text": "code", + "style": { + "code": true + } + }, + { + "type": "text", + "text": " 5 " + }, + { + "type": "text", + "text": ".1strike", + "style": { + "strike": true + } + }, + { + "type": "text", + "text": " " + }, + { + "type": "text", + "text": "code", + "style": { + "code": true + } + }, + { + "type": "text", + "text": " 5 " + }, + { + "type": "text", + "text": ".1bold", + "style": { + "bold": true + } + } + ] + }, + { + "type": "rich_text_section", + "elements": [ + { + "type": "text", + "text": "5.2something " + }, + { + "type": "user", + "user_id": "U02N85E78QM" + }, + { + "type": "text", + "text": " 10am " + }, + { + "type": "broadcast", + "range": "here" + }, + { + "type": "text", + "text": " 10" + } + ] + }, + { + "type": "rich_text_section", + "elements": [ + { + "type": "text", + "text": "am I a human?" + } + ] + } ], - "type": "rich_text" - } + "style": "bullet", + "indent": 0, + "border": 0 + }, + { + "type": "rich_text_section", + "elements": [ + { + "type": "text", + "text": "end!" + } + ] + } + ] + } ] - |] diff --git a/tzbot.cabal b/tzbot.cabal index a058012..00bd46b 100644 --- a/tzbot.cabal +++ b/tzbot.cabal @@ -377,7 +377,6 @@ test-suite tzbot-test , katip , nyan-interpolation , servant-client-core - , string-conversions , tasty , tasty-hspec , tasty-hunit