I want to compose Daml Triggers from small building blocks, based on the insight that instance Action (TriggerA s)
, just like instance Action Update
or instance Action Script
.
This is how I decomposed and recomposed the autoReply
trigger form the gsg-trigger
template.
Can I do it even simpler and/or more elegantly?
module ChatBot where
import qualified Daml.Trigger as T
import qualified User
import qualified DA.List.Total as List
import DA.Action (when)
import DA.Optional (whenSome)
import Daml.Script
import DA.Map qualified as Maps
import Daml.Trigger.Assert
import DA.Assert
-- THE ORIGINAL TRIGGER
autoReply : T.Trigger ()
autoReply = T.Trigger
{ initialize = pure ()
, updateState = \_ -> pure ()
, rule = \p -> do
message_contracts <- T.query @User.Message
let messages = map snd message_contracts
debug $ "Messages so far: " <> show (length messages)
let lastMessage = List.maximumOn (.receivedAt) messages
debug $ "Last message: " <> show lastMessage
whenSome lastMessage $ \m ->
when (m.receiver == p) $ do
users <- T.query @User.User
debug $ "users: " <> show users
let isSender = (\user -> user.username == m.sender)
let replyTo = List.head $ filter (\(_, user) -> isSender user) users
whenSome replyTo $ \(sender, _) ->
T.dedupExercise sender (User.SendMessage p "Please, tell me more about that.")
, registeredTemplates = T.AllInDar
, heartbeat = None
}
test : Script ()
test = do
alice <- allocateParty "Alice"
bob <- allocateParty "Bob"
aliceUserCid <- submit alice $ Daml.Script.createCmd User.User with username = alice, following = [bob]
debug $ "Alide user cid: " <> show aliceUserCid
bobUserCid <- submit bob $ Daml.Script.createCmd User.User with username = bob, following = [alice]
debug $ "Bob user cid: " <> show bobUserCid
bobMsgCid <- submit bob $ Daml.Script.exerciseCmd aliceUserCid User.SendMessage with sender = bob, content = "Hello Alice"
let acsBuilder = toACS aliceUserCid <> toACS bobUserCid <> toACS bobMsgCid
-- TESTING THE ORITINAL TRIGGER
((), cmds) <- testRule autoReply alice [] acsBuilder Maps.empty ()
debug $ length cmds
assertExerciseCmd (flattenCommands cmds) $ \(cid, choiceArg) -> do
cid === bobUserCid
debug choiceArg
choiceArg === User.SendMessage with
sender = alice
content = "Please, tell me more about that."
-- COMPOSABLE BUILDING BLOCKS
queryMessages : T.TriggerA () [User.Message]
queryMessages =
map snd <$> T.query @User.Message
filterForReceivedByReplier : Party -> [User.Message] -> T.TriggerA () [User.Message]
filterForReceivedByReplier replyingParty messages = do
return $ filter (\m -> m.receiver == replyingParty) $ messages
pickLastMessage : [User.Message] -> T.TriggerA () (Optional User.Message)
pickLastMessage listOfUsers = do
return $ List.maximumOn (.receivedAt) $ listOfUsers
checkIfReceivedByReplier : Party -> Optional User.Message -> T.TriggerA () (Optional User.Message)
checkIfReceivedByReplier replyingParty messageOpt = do
case messageOpt of
None -> return None
Some m -> return $ if m.receiver == replyingParty then Some m else None
fromMessageToSenderUserCid : Optional User.Message -> T.TriggerA () (Optional (ContractId User.User))
fromMessageToSenderUserCid messageOpt = do
case messageOpt of
None -> return None
Some m -> do
userTuples <- T.query @User.User
return $ List.head . map fst . filter (\(_, user) -> user.username == m.sender) $ userTuples
replyToUserCid : Party -> T.TriggerA () (Optional (ContractId User.User))
replyToUserCid replyingParty = queryMessages
>>= filterForReceivedByReplier replyingParty
>>= pickLastMessage
>>= checkIfReceivedByReplier replyingParty
>>= fromMessageToSenderUserCid
replyChoice : Party -> User.SendMessage
replyChoice replyingParty = User.SendMessage with
sender = replyingParty
content = "Please, tell me more about that."
sendReply : Party -> (Optional (ContractId User.User)) -> T.TriggerA () ()
sendReply replyingParty replyToUserCidOpt = do
case replyToUserCidOpt of
None -> return ()
Some cid -> T.dedupExercise cid (replyChoice replyingParty)
autoReplyRule : Party -> T.TriggerA () ()
autoReplyRule replyingParty =
replyToUserCid replyingParty
>>= sendReply replyingParty
-- THE SAME TRIGGER ASSEMBLED FROM BUILDING BLOCKS
autoReply' : T.Trigger ()
autoReply' = T.Trigger
{ initialize = pure ()
, updateState = \_ -> pure ()
, rule = autoReplyRule
, registeredTemplates = T.AllInDar
, heartbeat = None
}
test' : Script ()
test' = do
alice <- allocateParty "Alice"
bob <- allocateParty "Bob"
aliceUserCid <- submit alice $ Daml.Script.createCmd User.User with username = alice, following = [bob]
debug $ "Alide user cid: " <> show aliceUserCid
bobUserCid <- submit bob $ Daml.Script.createCmd User.User with username = bob, following = [alice]
debug $ "Bob user cid: " <> show bobUserCid
bobMsgCid <- submit bob $ Daml.Script.exerciseCmd aliceUserCid User.SendMessage with sender = bob, content = "Hello Alice"
let acsBuilder = toACS aliceUserCid <> toACS bobUserCid <> toACS bobMsgCid
-- TESTING THE MODIFIED TRIGGER
((), cmds) <- testRule autoReply' alice [] acsBuilder Maps.empty ()
debug $ length cmds
assertExerciseCmd (flattenCommands cmds) $ \(cid, choiceArg) -> do
cid === bobUserCid
debug choiceArg
choiceArg === User.SendMessage with
sender = alice
content = "Please, tell me more about that."