packages feed

line 3.1.0 → 4.0.0

raw patch · 10 files changed

+364/−54 lines, 10 files

Files

CHANGELOG.md view
@@ -1,3 +1,13 @@+## 4.0.0 (19 Sep 2017)++* Use the latest Stack resolver 9.5+* Add userID fields to event sources+* Add 'Get group/room member profile' APIs+* Add 'image carousel' template message+* Add 'datetimepicker' template action type+* Implement 'Get group/room member IDs' APIs+* Use DuplicateRecordFields to simplify field names+ ## 3.1.0 (26 May 2017)  * Add support for file message
line.cabal view
@@ -1,5 +1,5 @@ name:                line-version:             3.1.0+version:             4.0.0 synopsis:            Haskell SDK for the LINE API homepage:            https://github.com/noraesae/line license:             BSD3
src/Line/Messaging/API.hs view
@@ -21,6 +21,10 @@   reply,   getContent,   getProfile,+  getGroupMemberProfile,+  getRoomMemberProfile,+  getGroupMemberIDs,+  getRoomMemberIDs,   leaveRoom,   leaveGroup,   ) where@@ -29,7 +33,8 @@ import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Reader (runReaderT, ReaderT, ask) import Control.Monad.Trans.Except (runExceptT, ExceptT, throwE, catchE)-import Data.Aeson (ToJSON(..), (.=), object, decode', eitherDecode')+import Data.Aeson+import Data.Maybe (fromMaybe) import Data.Text.Encoding (encodeUtf8) import Line.Messaging.API.Types import Line.Messaging.Types (ReplyToken)@@ -193,6 +198,64 @@   case eitherDecode' bs of     Right profile -> return profile     Left errStr -> lift . throwE . JSONDecodeError $ errStr++-- | Get a profile of a user in a group, specified by the group ID and the user ID.+--+-- Please refer to <https://devdocs.line.me/en/#get-group-room-member-profile its API reference>+-- for the difference between this API and 'getProfile'.+getGroupMemberProfile :: ID -> ID -> APIIO Profile+getGroupMemberProfile groupId userId = do+  let url = "https://api.line.me/v2/bot/group/" ++ T.unpack groupId ++ "/member/" ++ T.unpack userId+  bs <- get url+  case eitherDecode' bs of+    Right profile -> return profile+    Left errStr -> lift . throwE . JSONDecodeError $ errStr++-- | Get a profile of a user in a room, specified by the room ID and the user ID.+--+-- Please refer to <https://devdocs.line.me/en/#get-group-room-member-profile its API reference>+-- for the difference between this API and 'getProfile'.+getRoomMemberProfile :: ID -> ID -> APIIO Profile+getRoomMemberProfile roomId userId = do+  let url = "https://api.line.me/v2/bot/room/" ++ T.unpack roomId ++ "/member/" ++ T.unpack userId+  bs <- get url+  case eitherDecode' bs of+    Right profile -> return profile+    Left errStr -> lift . throwE . JSONDecodeError $ errStr++data MemberIDs = MemberIDs [ID] (Maybe ContinuationToken)+type ContinuationToken = T.Text++instance FromJSON MemberIDs where+  parseJSON (Object v) = MemberIDs <$> v .: "memberIds" <*> v .:? "next"+  parseJSON _ = fail "ContList"++getMemberIDs :: String -> Maybe T.Text -> ID -> APIIO [ID]+getMemberIDs base token id' = do+  let url = base ++ T.unpack id' ++ "/members/ids" ++ fromMaybe "" (("?start=" ++) . T.unpack <$> token)+  bs <- get url+  case eitherDecode' bs of+    Right (MemberIDs ids Nothing) -> return ids+    Right (MemberIDs ids token') -> (ids ++) <$> getMemberIDs base token' id'+    Left errStr -> lift . throwE . JSONDecodeError $ errStr++-- | Gets the user profile of a member of a group that the bot is in.+--+-- FYI: This feature is only available for LINE@ Approved accounts or official accounts.+--+-- For more information, please refer to+-- <https://devdocs.line.me/en/#get-group-room-member-ids its API reference>.+getGroupMemberIDs :: ID -> APIIO [ID]+getGroupMemberIDs = getMemberIDs "https://api.line.me/v2/bot/group/" Nothing++-- | Gets the user profile of a member of a room that the bot is in.+--+-- FYI: This feature is only available for LINE@ Approved accounts or official accounts.+--+-- For more information, please refer to+-- <https://devdocs.line.me/en/#get-group-room-member-ids its API reference>.+getRoomMemberIDs :: ID -> APIIO [ID]+getRoomMemberIDs = getMemberIDs "https://api.line.me/v2/bot/room/" Nothing  leave :: String -> ID -> APIIO () leave type' id' = do
src/Line/Messaging/API/Types.hs view
@@ -2,6 +2,7 @@ This module provides types to be used with "Line.Messaging.API". -} +{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE StandaloneDeriving #-}@@ -34,9 +35,12 @@   Buttons (..),   Confirm (..),   Carousel (..),+  ImageCarousel (..),   Column (..),+  ImageColumn (..),   Label,   TemplateAction (..),+  DatetimeMode (..),   -- * Profile   Profile (..),   -- * Error types@@ -115,8 +119,8 @@ -- -- It contains URLs of an original image and its preview. Its corresponding JSON -- spec is described <https://devdocs.line.me/en/#image here>.-data Image = Image { getImageURL :: URL-                   , getImagePreviewURL :: URL+data Image = Image { getURL :: URL+                   , getPreviewURL :: URL                    }              deriving (Eq, Show) @@ -130,8 +134,8 @@ -- -- It contains URLs of an original video and its preview. Its corresponding JSON -- spec is described <https://devdocs.line.me/en/#video here>.-data Video = Video { getVideoURL :: URL-                   , getVideoPreviewURL :: URL+data Video = Video { getURL :: URL+                   , getPreviewURL :: URL                    }              deriving (Eq, Show) @@ -146,8 +150,8 @@ -- It contains a URL of an audio, and its duration in milliseconds. Its -- corresponding JSON spec is described -- <https://devdocs.line.me/en/#audio here>.-data Audio = Audio { getAudioURL :: URL-                   , getAudioDuration :: Integer+data Audio = Audio { getURL :: URL+                   , getDuration :: Integer                    }              deriving (Eq, Show) @@ -167,7 +171,7 @@ -- About the webhook usage, please refer to -- <./Line-Messaging-Webhook-Types.html#t:EventMessage EventMessage> in -- "Line.Messaging.Webhook.Types".-data Location = Location { getLocationTitle :: T.Text+data Location = Location { getTitle :: T.Text                          , getAddress :: T.Text                          , getLatitude :: Double                          , getLongitude :: Double@@ -221,13 +225,13 @@ -- <https://devdocs.line.me/en/#imagemap-message image map message spec>. data ImageMap = ImageMap { getBaseImageURL :: URL                            -- ^ <https://devdocs.line.me/en/#base-url Base URL> of images-                         , getIMAltText :: T.Text+                         , getAltText :: T.Text                            -- ^ Alt text for devices not supporting image map                          , getBaseImageSize :: (Integer, Integer)                            -- ^ Image size tuple, (width, height) specifically.                            -- The width to be set to 1040, the height to be set to                            -- the value corresponding to a width of 1040.-                         , getIMActions :: [ImageMapAction]+                         , getActions :: [ImageMapAction]                            -- ^ Actions to be executed when each area is tapped                          }                 deriving (Eq, Show)@@ -273,14 +277,14 @@ -- -- It has a type parameter @t@ which means a template content type. The type is -- polymolphic, but 'Messageable' instances are defined only for 'Buttons',--- 'Confirm', and 'Carousel'.+-- 'Confirm', 'Carousel', and 'ImageCarousel'. -- -- About how to send template message and what each field means, please -- refer to -- <https://devdocs.line.me/en/#template-messages template message spec>.-data Template t = Template { getTemplateAltText :: T.Text+data Template t = Template { getAltText :: T.Text                              -- ^ Alt text for devices not supporting template message-                           , getTemplate :: t+                           , getTemplateContent :: t                              -- ^ Template content type                            }                   deriving (Eq, Show)@@ -305,6 +309,10 @@   toType = templateType   toObject = templateToObject +instance Messageable (Template ImageCarousel) where+  toType = templateType+  toObject = templateToObject+ -- | The buttons content type for template message. -- -- <<https://devdocs.line.me/images/buttons.png Buttons template>>@@ -312,13 +320,13 @@ -- For more details of each field, please refer to the -- <https://devdocs.line.me/en/#buttons Buttons> section in the LINE -- documentation.-data Buttons = Buttons { getButtonsThumbnailURL :: Maybe URL+data Buttons = Buttons { getThumbnailURL :: Maybe URL                        -- ^ URL for thumbnail image-                       , getButtonsTitle :: Maybe T.Text+                       , getTitle :: Maybe T.Text                        -- ^ Title text-                       , getButtonsText :: T.Text+                       , getText :: T.Text                        -- ^ Description text-                       , getButtonsActions :: [TemplateAction]+                       , getActions :: [TemplateAction]                        -- ^ A list of template actions, each of which represents                        -- a button (max: 4)                        }@@ -342,9 +350,9 @@ -- For more details of each field, please refer to the -- <https://devdocs.line.me/en/#confirm Confirm> section in the LINE -- documentation.-data Confirm = Confirm { getConfirmText :: T.Text+data Confirm = Confirm { getText :: T.Text                        -- ^ Confirm text-                       , getConfirmActions :: [TemplateAction]+                       , getActions :: [TemplateAction]                        -- ^ A list of template actions, each of which represents                        -- a button (max: 2)                        }@@ -377,13 +385,13 @@ -- -- It has the same fields as 'Buttons', except that the number of actions is -- up to 3.-data Column = Column { getColumnThumbnailURL :: Maybe URL+data Column = Column { getThumbnailURL :: Maybe URL                      -- ^ URL for thumbnail image-                     , getColumnTitle :: Maybe T.Text+                     , getTitle :: Maybe T.Text                      -- ^ Title text-                     , getColumnText :: T.Text+                     , getText :: T.Text                      -- ^ Description text-                     , getColumnActions :: [TemplateAction]+                     , getActions :: [TemplateAction]                      -- ^ A list of template actions, each of which represents                      -- a button (max: 3)                      }@@ -399,6 +407,40 @@       , maybeToList $ ("title" .=) <$> maybeTitle       ] +-- | The image carousel content type for template message.+--+-- <<https://devdocs.line.me/images/image_carousel.png Image carousel template>>+--+-- For more details of each field, please refer to the+-- <https://devdocs.line.me/en/#image-carousel Image carousel> section in the LINE+-- documentation.+data ImageCarousel = ImageCarousel { getColumns :: [ImageColumn]+                                   -- ^ A list of columns for an image carousel template+                                   }+                   deriving (Eq, Show)++instance ToJSON ImageCarousel where+  toJSON (ImageCarousel columns) = object [ "type" .= ("image_carousel" :: T.Text)+                                          , "columns" .= toJSON columns+                                          ]++-- | Actual contents of carousel template.+--+-- It has the same fields as 'Buttons', except that the number of actions is+-- up to 3.+data ImageColumn = ImageColumn { getImageURL :: URL+                               -- ^ URL for thumbnail image+                               , getAction :: TemplateAction+                               -- ^ A template action+                               }+                 deriving (Eq, Show)++instance ToJSON ImageColumn where+  toJSON (ImageColumn url action) =+    object [ "imageUrl" .= url+           , "action" .= toJSON action+           ]+ -- | Just a type alias for 'T.Text', used with 'TemplateAction'. type Label = T.Text @@ -410,21 +452,33 @@                       -- ^ Message action. When clicked, a specified text will                       -- be sent into the same room by a user who clicked the                       -- button.-                    | TplPostbackAction Label Postback (Maybe T.Text)+                    | TplPostbackAction Label T.Text (Maybe T.Text)                       -- ^ Postback action. When clicked, a specified text will                       -- be sent, and postback data will be sent to webhook                       -- server as a postback event.                     | TplURIAction Label URL                       -- ^ URI action. When clicked, a web page with a specified                       -- URI will open in the in-app browser.+                    | TplDatetimePickerAction { label' :: (Maybe Label)+                                              , data' :: T.Text+                                              , mode' :: DatetimeMode+                                              , initial' :: (Maybe T.Text)+                                              , max' :: (Maybe T.Text)+                                              , min' :: (Maybe T.Text)+                                              }+                      -- ^ Datetime picker action. When clicked, a postback action+                      -- will be sent with the date and time selected by the+                      -- user from the date and time selection dialog. For the+                      -- detailed information of datetime picker, please refer+                      -- to the <https://devdocs.line.me/en/#datetime-picker-action official documentation>.                     deriving (Eq, Show)  instance ToJSON TemplateAction where-  toJSON (TplPostbackAction label data' maybeText) =+  toJSON (TplPostbackAction label data'' maybeText) =     object . concat $       [ [ "type" .= ("postback" :: T.Text)         , "label" .= label-        , "data" .= data'+        , "data" .= data''         ]       , maybeToList $ ("text" .=) <$> maybeText       ]@@ -433,9 +487,26 @@                                                 , "text" .= text                                                 ]   toJSON (TplURIAction label uri) = object [ "type" .= ("uri" :: T.Text)-                                           , "label" .= label-                                           , "uri" .= uri-                                           ]+                                            , "label" .= label+                                            , "uri" .= uri+                                            ]+  toJSON (TplDatetimePickerAction label data'' mode initial max'' min'') =+    object . concat $ [ [ "type" .= ("datetimepicker" :: T.Text)+                        , "data" .= data''+                        , "mode" .= mode+                        ]+                      , maybeToList $ ("label" .=) <$> label+                      , maybeToList $ ("initial" .=) <$> initial+                      , maybeToList $ ("max" .=) <$> max''+                      , maybeToList $ ("min" .=) <$> min''+                      ]++data DatetimeMode = Date | Time | Datetime deriving (Eq, Show)++instance ToJSON DatetimeMode where+  toJSON Date = "date"+  toJSON Time = "time"+  toJSON Datetime = "datetime"  -- | A type to represent a user's profile. --
src/Line/Messaging/Common/Types.hs view
@@ -9,7 +9,6 @@   -- * LINE API types   ChannelSecret,   ChannelAccessToken,-  Postback,   ) where  import qualified Data.Text as T@@ -27,6 +26,3 @@ -- | A type alias to specify a channel access token. About issueing and using -- the channel access token, please refer to corresponding LINE documentations. type ChannelAccessToken = T.Text---- | A type alias for postback data.-type Postback = T.Text
src/Line/Messaging/Webhook/Types.hs view
@@ -35,9 +35,11 @@   getBeacon,   -- *** Event source   EventSource (..),-  getID,   -- *** Message event   EventMessage (..),+  -- *** Postback event+  Postback (..),+  PostbackParams (..),   -- *** Beacon event   BeaconData (..),   getHWID,@@ -175,7 +177,7 @@       "unfollow" -> UnfollowEvent <$> (nonReplyable v <*> none)       "join" -> JoinEvent <$> (replyable v <*> none)       "leave" -> LeaveEvent <$> (nonReplyable v <*> none)-      "postback" -> PostbackEvent <$> (replyable v <*> ((v .: "postback") >>= (.: "data")))+      "postback" -> PostbackEvent <$> (replyable v <*> v .: "postback")       "beacon" -> BeaconEvent <$> (replyable v <*> v .: "beacon")       _ -> fail "Event"     where@@ -190,23 +192,17 @@  -- | A source from which an event is sent. It can be retrieved from events with -- 'getSource'.-data EventSource = User ID-                 | Group ID-                 | Room ID+data EventSource = User { userID :: ID }+                 | Group { groupID :: ID, groupUserId :: Maybe ID }+                 | Room { roomID :: ID, roomUserId :: Maybe ID }                  deriving (Eq, Show) --- | Retrieve identifier from event source-getID :: EventSource -> ID-getID (User i) = i-getID (Group i) = i-getID (Room i) = i- instance FromJSON EventSource where   parseJSON (Object v) = v .: "type" >>= \ t ->     case t :: T.Text of       "user" -> User <$> v .: "userId"-      "group" -> Group <$> v .: "groupId"-      "room" -> Room <$> v .: "roomId"+      "group" -> Group <$> v .: "groupId" <*> v .:? "userId"+      "room" -> Room <$> v .: "roomId" <*> v .:? "userId"       _ -> fail "EventSource"   parseJSON _ = fail "EventSource" @@ -242,6 +238,39 @@       _ -> fail "EventMessage"   parseJSON _ = fail "IncommingMessage" +-- | Represent Postback data.+data Postback = Postback { data' :: T.Text+                         , params :: Maybe PostbackParams+                         }+              deriving (Eq, Show)+++instance FromJSON Postback where+  parseJSON (Object v) = Postback <$> v .: "data" <*> v .:? "params"+  parseJSON _ = fail "Postback"++-- | Represent params of Postback data.+--+-- It's currently used only for the datetime picker action. For the detail,+-- please refer to the <https://devdocs.line.me/en/#postback-params-object official documentation>.+data PostbackParams = PostbackParamsDate T.Text+                    | PostbackParamsTime T.Text+                    | PostbackParamsDatetime T.Text+                    | PostbackParamsUnknown+                    deriving (Eq, Show)++instance FromJSON PostbackParams where+  parseJSON (Object v) = go [ ("date", PostbackParamsDate)+                            , ("time", PostbackParamsTime)+                            , ("datetime", PostbackParamsDatetime)+                            ]+    where+    go ((field, constructor):xs) = v .:? field >>= \m -> case m of+      Just str -> return $ constructor str+      Nothing -> go xs+    go [] = return PostbackParamsUnknown+  parseJSON _ = fail "PostbackParams"+ -- | Represent beacon data. data BeaconData = BeaconEnter ID (Maybe T.Text)                 | BeaconLeave ID (Maybe T.Text)@@ -249,13 +278,13 @@                 deriving (Eq, Show)  --- |  Get hardware ID of the beacon.+-- | Get hardware ID of the beacon. getHWID :: BeaconData -> ID getHWID (BeaconEnter hwid _) = hwid getHWID (BeaconLeave hwid _) = hwid getHWID (BeaconBanner hwid _) = hwid --- |  Get device message from the beacon, if exists.+-- | Get device message from the beacon, if exists. getDeviceMessage :: BeaconData -> Maybe T.Text getDeviceMessage (BeaconEnter _ dm) = dm getDeviceMessage (BeaconLeave _ dm) = dm
test/Line/Messaging/API/TypesSpec.hs view
@@ -68,6 +68,7 @@        [ TplPostbackAction "Buy" "action=buy&itemid=123" Nothing        , TplPostbackAction "Add to cart" "action=add&itemid=123" Nothing        , TplURIAction "View detail" "http://example.com/page/123"+       , TplDatetimePickerAction (Just "yes label") "pickpick" Datetime Nothing Nothing Nothing        ])       buttonsTemplateMessageableResult @@ -76,6 +77,7 @@        Confirm "Are you sure?"        [ TplMessageAction "Yes" "yes"        , TplMessageAction "No" "no"+       , TplDatetimePickerAction Nothing "pickpick2" Date (Just "1990-01-01") (Just "2100-12-31") (Just "1900-01-01")        ])       confirmTemplateMessageableResult @@ -87,6 +89,7 @@                          [ TplPostbackAction "Buy" "action=buy&itemid=111" Nothing                          , TplPostbackAction "Add to cart" "action=add&itemid=111" Nothing                          , TplURIAction "View detail" "http://example.com/page/111"+                         , TplDatetimePickerAction Nothing "pickpick3" Time Nothing (Just "23:23") (Just "11:11")                          ]                 , Column (Just "https://example.com/bot/images/item2.jpg")                          (Just "this is menu")@@ -97,6 +100,16 @@                          ]                 ])       carouselTemplateMessageableResult++    messageableSpec "template image carousel"+      (Template "this is an image carousel template" $+       ImageCarousel [ ImageColumn "https://example.com/bot/images/item1.jpg"+                         (TplPostbackAction "Buy" "action=buy&itemid=111" Nothing)+                     , ImageColumn "https://example.com/bot/images/item2.jpg"+                         (TplPostbackAction "Buy" "action=buy&itemid=222" Nothing)+                     ]+      )+      imageCarouselTemplateMessageableResult    describe "JSON decode" $ do     describe "profile" $ fromJSONSpec
test/Line/Messaging/API/TypesSpecHelper.hs view
@@ -120,6 +120,12 @@             "type": "uri",             "label": "View detail",             "uri": "http://example.com/page/123"+          },+          {+            "type": "datetimepicker",+            "label": "yes label",+            "data": "pickpick",+            "mode": "datetime"           }       ]   }@@ -144,6 +150,14 @@             "type": "message",             "label": "No",             "text": "no"+          },+          {+            "type": "datetimepicker",+            "data": "pickpick2",+            "mode": "date",+            "initial": "1990-01-01",+            "max": "2100-12-31",+            "min": "1900-01-01"           }       ]   }@@ -177,6 +191,13 @@                     "type": "uri",                     "label": "View detail",                     "uri": "http://example.com/page/111"+                },+                {+                  "type": "datetimepicker",+                  "data": "pickpick3",+                  "mode": "time",+                  "max": "23:23",+                  "min": "11:11"                 }             ]           },@@ -201,6 +222,35 @@                     "uri": "http://example.com/page/222"                 }             ]+          }+      ]+  }+}+|]++imageCarouselTemplateMessageableResult :: BL.ByteString+imageCarouselTemplateMessageableResult = [r|+{+  "type": "template",+  "altText": "this is an image carousel template",+  "template": {+      "type": "image_carousel",+      "columns": [+          {+              "imageUrl": "https://example.com/bot/images/item1.jpg",+              "action": {+                  "type": "postback",+                  "label": "Buy",+                  "data": "action=buy&itemid=111"+              }+          },+          {+              "imageUrl": "https://example.com/bot/images/item2.jpg",+              "action": {+                  "type": "postback",+                  "label": "Buy",+                  "data": "action=buy&itemid=222"+              }           }       ]   }
test/Line/Messaging/Webhook/TypesSpec.hs view
@@ -33,8 +33,10 @@    describe "event source" $ fromJSONSpec     [ ( goodUser, Just $ User "123" )-    , ( goodGroup, Just $ Group "456" )-    , ( goodRoom, Just $ Room "789" )+    , ( goodGroup, Just $ Group "456" Nothing)+    , ( goodRoom, Just $ Room "789" Nothing)+    , ( goodGroupWithUserId, Just $ Group "456" $ Just "789")+    , ( goodRoomWithUserId, Just $ Room "789" $ Just "456")     , ( badSource, Nothing )     ] @@ -108,7 +110,7 @@    describe "join event" $ fromJSONSpec     [ ( goodJoin, Just $ Body $ [-        JoinEvent ( Group "cxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"+        JoinEvent ( Group "cxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" Nothing                   , datetime 1462629479859                   , "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA"                   , ()@@ -119,7 +121,7 @@    describe "leave event" $ fromJSONSpec     [ ( goodLeave, Just $ Body $ [-          LeaveEvent ( Group "cxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"+          LeaveEvent ( Group "cxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" Nothing                      , datetime 1462629479859                      , ()                      , ()@@ -129,7 +131,10 @@     ]    describe "postback event" $ fromJSONSpec-    [ ( goodPostback, replyE PostbackEvent "action=buyItem&itemId=123123&color=red" )+    [ ( goodPostback, replyE PostbackEvent (Postback "action=buyItem&itemId=123123&color=red" Nothing) )+    , ( goodPostbackWithDate, replyE PostbackEvent (Postback "action=buyItem&itemId=123123&color=red" (Just $ PostbackParamsDate "1990-01-01")) )+    , ( goodPostbackWithTime, replyE PostbackEvent (Postback "action=buyItem&itemId=123123&color=red" (Just $ PostbackParamsTime "12:34")) )+    , ( goodPostbackWithDatetime, replyE PostbackEvent (Postback "action=buyItem&itemId=123123&color=red" (Just $ PostbackParamsDatetime "1990-01-01 12:34")) )     , ( badPostback, Nothing )     ] 
test/Line/Messaging/Webhook/TypesSpecHelper.hs view
@@ -30,6 +30,16 @@ { "type": "room", "roomId": "789" } |] +goodGroupWithUserId :: BL.ByteString+goodGroupWithUserId = [r|+{ "type": "group", "groupId": "456", "userId": "789" }+|]++goodRoomWithUserId :: BL.ByteString+goodRoomWithUserId = [r|+{ "type": "room", "roomId": "789", "userId": "456" }+|]+ badSource :: BL.ByteString badSource = [r| { "type": "bad", "userId": "123 }@@ -449,6 +459,69 @@   },   "postback": {     "data": "action=buyItem&itemId=123123&color=red"+  }+}+] }+|]++goodPostbackWithDate :: BL.ByteString+goodPostbackWithDate = [r|+{ "events": [+{+  "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",+  "type": "postback",+  "timestamp": 1462629479859,+  "source": {+    "type": "user",+    "userId": "U206d25c2ea6bd87c17655609a1c37cb8"+  },+  "postback": {+    "data": "action=buyItem&itemId=123123&color=red",+    "params": {+      "date": "1990-01-01"+    }+  }+}+] }+|]++goodPostbackWithTime :: BL.ByteString+goodPostbackWithTime = [r|+{ "events": [+{+  "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",+  "type": "postback",+  "timestamp": 1462629479859,+  "source": {+    "type": "user",+    "userId": "U206d25c2ea6bd87c17655609a1c37cb8"+  },+  "postback": {+    "data": "action=buyItem&itemId=123123&color=red",+    "params": {+      "time": "12:34"+    }+  }+}+] }+|]++goodPostbackWithDatetime :: BL.ByteString+goodPostbackWithDatetime = [r|+{ "events": [+{+  "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",+  "type": "postback",+  "timestamp": 1462629479859,+  "source": {+    "type": "user",+    "userId": "U206d25c2ea6bd87c17655609a1c37cb8"+  },+  "postback": {+    "data": "action=buyItem&itemId=123123&color=red",+    "params": {+      "datetime": "1990-01-01 12:34"+    }   } } ] }