line 3.1.0 → 4.0.0
raw patch · 10 files changed
+364/−54 lines, 10 files
Files
- CHANGELOG.md +10/−0
- line.cabal +1/−1
- src/Line/Messaging/API.hs +64/−1
- src/Line/Messaging/API/Types.hs +99/−28
- src/Line/Messaging/Common/Types.hs +0/−4
- src/Line/Messaging/Webhook/Types.hs +44/−15
- test/Line/Messaging/API/TypesSpec.hs +13/−0
- test/Line/Messaging/API/TypesSpecHelper.hs +50/−0
- test/Line/Messaging/Webhook/TypesSpec.hs +10/−5
- test/Line/Messaging/Webhook/TypesSpecHelper.hs +73/−0
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"+ } } } ] }