Simplified conversation logic

This commit is contained in:
Eugene Zadyra
2018-07-12 12:10:55 +02:00
parent 341d54f3df
commit 951ce55725
16 changed files with 88 additions and 108 deletions

View File

@@ -38,12 +38,12 @@ class Account(val userId: UserId, bot: Bot, dataService: DataService, val locali
entryPoint(askAction)
def askAction: Step =
question { _ =>
ask { _ =>
val credentials = dataService.getUserCredentials(userId.userId)
val currentAccount = credentials.find(c => c.accountId == userId.accountId).getOrElse(sys.error("Can't determine current account"))
val buttons = Seq(Button(lang.addAccount, -1L), Button(lang.deleteAccount, -2L)) ++ credentials.map(c => Button(s"🔐️ ${c.username}", c.accountId))
bot.sendMessage(userId.source, lang.pleaseChooseAccount(currentAccount.username), inlineKeyboard = createInlineKeyboard(buttons, columns = 1))
} answer {
} onReply {
case Msg(cmd@CallbackCommand(actionStr), _) =>
val action = actionStr.toLong
action match {

View File

@@ -84,12 +84,12 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da
}(requestNext = requestDateFrom)
private def requestDateFrom: Step =
question { bookingData =>
ask { bookingData =>
datePicker ! InitConversation
datePicker ! StartConversation
datePicker ! DateFromMode
datePicker ! bookingData.dateFrom
} answer {
} onReply {
case Msg(cmd: Command, _) =>
datePicker ! cmd
stay()
@@ -98,37 +98,28 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da
}
private def requestDateTo: Step =
question { bookingData =>
ask { bookingData =>
datePicker ! InitConversation
datePicker ! StartConversation
datePicker ! DateToMode
datePicker ! bookingData.dateFrom.plusDays(1)
} answer {
} onReply {
case Msg(cmd: Command, _) =>
datePicker ! cmd
stay()
case Msg(date: ZonedDateTime, bookingData: BookingData) =>
goto(requestDayTime) using bookingData.copy(dateTo = date)
}
private def requestDayTime: Step =
question { _ =>
bot.sendMessage(userId.source, lang.chooseTimeOfDay,
inlineKeyboard = createInlineKeyboard(lang.timeOfDay.map { case (id, label) => Button(label, id.toString) }.toSeq, columns = 1))
} answer {
case Msg(Command(_, msg, Some(timeIdStr)), bookingData: BookingData) =>
val timeId = timeIdStr.toInt
bot.sendEditMessage(userId.source, msg.messageId, lang.preferredTimeIs(timeId))
goto(requestAction) using bookingData.copy(timeOfDay = timeId)
goto(requestAction) using bookingData.copy(dateTo = date)
}
private def requestAction: Step =
question { bookingData =>
ask { bookingData =>
dataService.storeAppointment(userId.accountId, bookingData)
bot.sendMessage(userId.source,
lang.bookingSummary(bookingData),
inlineKeyboard = createInlineKeyboard(Seq(Button(lang.findTerms, Tags.FindTerms), Button(lang.modifyDate, Tags.ModifyDate))))
} answer {
inlineKeyboard = createInlineKeyboard(
Seq(Button(lang.findTerms, Tags.FindTerms), Button(lang.modifyDate, Tags.ModifyDate))
))
} onReply {
case Msg(CallbackCommand(Tags.FindTerms), _) =>
goto(requestTerm)
case Msg(CallbackCommand(Tags.ModifyDate), _) =>
@@ -136,14 +127,14 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da
}
private def requestTerm: Step =
question { bookingData =>
ask { bookingData =>
val availableTerms = apiService.getAvailableTerms(userId.accountId, bookingData.cityId.id,
bookingData.clinicId.optionalId, bookingData.serviceId.id, bookingData.doctorId.optionalId,
bookingData.dateFrom, Some(bookingData.dateTo), timeOfDay = bookingData.timeOfDay)
termsPager ! InitConversation
termsPager ! StartConversation
termsPager ! availableTerms
} answer {
} onReply {
case Msg(cmd: Command, _) =>
termsPager ! cmd
stay()
@@ -163,10 +154,10 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da
}
private def askNoTermsAction: Step =
question { _ =>
ask { _ =>
bot.sendMessage(userId.source, lang.noTermsFound, inlineKeyboard =
createInlineKeyboard(Seq(Button(lang.modifyDate, Tags.ModifyDate), Button(lang.createMonitoring, Tags.CreateMonitoring))))
} answer {
} onReply {
case Msg(CallbackCommand(Tags.ModifyDate), _) =>
goto(requestDateFrom)
case Msg(CallbackCommand(Tags.CreateMonitoring), _) =>
@@ -203,17 +194,17 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da
}
private def askMonitoringOptions: Step =
question { _ =>
ask { _ =>
bot.sendMessage(userId.source, lang.chooseTypeOfMonitoring,
inlineKeyboard = createInlineKeyboard(Seq(Button(lang.bookByApplication, Tags.BookByApplication), Button(lang.bookManually, Tags.BookManually)), columns = 1))
} answer {
} onReply {
case Msg(CallbackCommand(autobookStr), bookingData: BookingData) =>
val autobook = autobookStr.toBoolean
goto(createMonitoring) using bookingData.copy(autobook = autobook)
}
private def createMonitoring: Step =
internalConfig { bookingData =>
process { bookingData =>
debug(s"Creating monitoring for $bookingData")
try {
monitoringService.createMonitoring((userId -> bookingData).mapTo[Monitoring])

View File

@@ -40,11 +40,13 @@ class Bug(val userId: UserId, bot: Bot, dataService: DataService, bugPagerActorF
private val bugPager = bugPagerActorFactory(userId, self)
entryPoint(askAction)
def askAction: Step =
question { _ =>
ask { _ =>
bot.sendMessage(userId.source, lang.bugAction, inlineKeyboard =
createInlineKeyboard(Seq(Button(lang.createNewBug, Tags.SubmitNew), Button(lang.showSubmittedBugs, Tags.ListSubmitted))))
} answer {
} onReply {
case Msg(Command(_, _, Some(Tags.SubmitNew)), _) =>
goto(askBugDescription)
case Msg(Command(_, _, Some(Tags.ListSubmitted)), _) =>
@@ -52,7 +54,7 @@ class Bug(val userId: UserId, bot: Bot, dataService: DataService, bugPagerActorF
}
def displaySubmittedBugs: Step =
internalConfig { _ =>
process { _ =>
val bugs = dataService.getBugs(userId.userId)
bugPager ! InitConversation
bugPager ! StartConversation
@@ -71,16 +73,14 @@ class Bug(val userId: UserId, bot: Bot, dataService: DataService, bugPagerActorF
}
def askBugDescription: Step =
question { _ =>
ask { _ =>
bot.sendMessage(userId.source, lang.enterIssueDetails)
} answer {
} onReply {
case Msg(MessageExtractors.TextCommand(details), _) =>
val bugId = dataService.submitBug(userId.userId, userId.source.sourceSystem.id, details)
bot.sendMessage(userId.source, lang.bugHasBeenCreated(bugId.getOrElse(-1L)))
end()
}
entryPoint(askAction)
}
object Bug {

View File

@@ -109,7 +109,7 @@ class Chat(val userId: UserId, dataService: DataService, monitoringService: Moni
stay()
}
private def actorDialogue(actor: ActorRef)(mainStateFunction: AnswerFn): Step =
private def actorDialogue(actor: ActorRef)(mainStateFunction: MessageProcessorFn): Step =
monologue {
case event: Msg =>
if (mainStateFunction.isDefinedAt(event)) mainStateFunction(event)
@@ -119,7 +119,7 @@ class Chat(val userId: UserId, dataService: DataService, monitoringService: Moni
}
}
private def secondaryState(actor: ActorRef): AnswerFn = {
private def secondaryState(actor: ActorRef): MessageProcessorFn = {
case Msg(cmd@TextCommand("/bug"), _) =>
self ! cmd
goto(bugChat)

View File

@@ -49,7 +49,7 @@ class DatePicker(val userId: UserId, val bot: Bot, val localization: Localizatio
entryPoint(configure)
def configure: Step =
externalConfig {
monologue {
case Msg(newMode: Mode, _) =>
mode = newMode
stay()
@@ -58,13 +58,13 @@ class DatePicker(val userId: UserId, val bot: Bot, val localization: Localizatio
}
def requestDate: Step =
question { initialDate =>
ask { initialDate =>
val message = mode match {
case DateFromMode => lang.chooseDateFrom
case DateToMode => lang.chooseDateTo
}
bot.sendMessage(userId.source, message, inlineKeyboard = dateButtons(initialDate))
} answer {
} onReply {
case Msg(Command(_, msg, Some(Tags.Done)), finalDate) =>
val (message, updatedDate) = mode match {
case DateFromMode =>

View File

@@ -40,7 +40,7 @@ class History(val userId: UserId, bot: Bot, apiService: ApiService, val localiza
entryPoint(prepareData)
def prepareData: Step =
internalConfig { _ =>
process { _ =>
val visits = apiService.visitsHistory(userId.accountId)
historyPager ! InitConversation
historyPager ! StartConversation

View File

@@ -49,23 +49,23 @@ class Login(source: MessageSource, bot: Bot, dataService: DataService, apiServic
}
def requestUsername: Step =
question { _ =>
ask { _ =>
bot.sendMessage(source, lang.provideUsername)
} answer {
} onReply {
case Msg(MessageExtractors.OptionalTextCommand(username), _) =>
goto(requestPassword) using LoginData(username = username)
}
def requestPassword: Step =
question { _ =>
ask { _ =>
bot.sendMessage(source, lang.providePassword)
} answer {
} onReply {
case Msg(MessageExtractors.OptionalTextCommand(password), loginData: LoginData) =>
goto(processLoginInformation) using loginData.copy(password = password.map(textEncryptor.encrypt))
}
def processLoginInformation: Step = {
internalConfig { case LoginData(Some(username), Some(password)) =>
process { case LoginData(Some(username), Some(password)) =>
val loginResult = apiService.login(username, password)
loginResult match {
case Left(error) =>

View File

@@ -41,7 +41,7 @@ class Monitorings(val userId: UserId, bot: Bot, monitoringService: MonitoringSer
entryPoint(prepareData)
def prepareData: Step =
internalConfig { _ =>
process { _ =>
val monitorings = monitoringService.getActiveMonitorings(userId.accountId)
monitoringsPager ! InitConversation
monitoringsPager ! StartConversation
@@ -62,10 +62,10 @@ class Monitorings(val userId: UserId, bot: Bot, monitoringService: MonitoringSer
}
def askToDeactivateMonitoring: Step =
question { monitoring =>
ask { monitoring =>
bot.sendMessage(userId.source, lang.deactivateMonitoring(monitoring), inlineKeyboard =
createInlineKeyboard(Seq(Button(lang.no, Tags.No), Button(lang.yes, Tags.Yes))))
} answer {
} onReply {
case Msg(Command(_, _, Some(Tags.No)), _) =>
bot.sendMessage(userId.source, lang.monitoringWasNotDeactivated)
end()

View File

@@ -43,7 +43,7 @@ class Pager[Data](val userId: UserId, bot: Bot, makeMessage: (Data, Int, Int) =>
entryPoint(awaitForData)
private def awaitForData: Step =
externalConfig {
monologue {
case Msg(Left(error: Throwable), _) =>
bot.sendMessage(userId.source, error.getMessage)
end()
@@ -55,9 +55,9 @@ class Pager[Data](val userId: UserId, bot: Bot, makeMessage: (Data, Int, Int) =>
}
private def displayPage: Step =
question { case (registry, massageIdMaybe) =>
ask { case (registry, massageIdMaybe) =>
sendPage(registry.page, registry.pages, massageIdMaybe)
} answer {
} onReply {
case Msg(Command(_, msg, Some(Tags.Next)), (registry, _)) =>
val page = registry.page + 1
goto(displayPage) using registry.copy(page = page) -> Some(msg.messageId)

View File

@@ -37,19 +37,19 @@ class Settings(val userId: UserId, bot: Bot, dataService: DataService, val local
entryPoint(askForAction)
def askForAction: Step =
question { _ =>
ask { _ =>
bot.sendMessage(userId.source, lang.settingsHeader, inlineKeyboard =
createInlineKeyboard(Seq(Button(lang.language, Tags.Language))))
} answer {
} onReply {
case Msg(Command(_, _, Some(Tags.Language)), _) =>
goto(askLanguage)
}
def askLanguage: Step =
question { _ =>
ask { _ =>
bot.sendMessage(userId.source, lang.chooseLanguage,
inlineKeyboard = createInlineKeyboard(Lang.Langs.map(l => Button(l.label, l.id)), columns = 1))
} answer {
} onReply {
case Msg(Command(_, _, Some(langIdStr)), _) =>
val langId = langIdStr.toInt
localization.updateLanguage(userId.userId, Lang(langId))

View File

@@ -41,16 +41,16 @@ class StaticData(val userId: UserId, bot: Bot, val localization: Localization, o
entryPoint(AwaitConfig)
def AwaitConfig: Step =
externalConfig {
monologue {
case Msg(newConfig: StaticDataConfig, _) =>
config = newConfig
goto(askForLatestOption)
}
def askForLatestOption: Step =
question { _ =>
ask { _ =>
originator ! LatestOptions
} answer {
} onReply {
case Msg(LatestOptions(options), _) if options.isEmpty =>
val callbackTags = anySelectOption
goto(askForUserInput) using callbackTags
@@ -60,10 +60,10 @@ class StaticData(val userId: UserId, bot: Bot, val localization: Localization, o
}
def askForUserInput: Step =
question { callbackTags =>
ask { callbackTags =>
bot.sendMessage(userId.source, lang.pleaseEnterStaticDataNameOrPrevious(config),
inlineKeyboard = createInlineKeyboard(callbackTags, columns = 1))
} answer {
} onReply {
case Msg(Command(_, msg, Some(tag)), callbackTags) =>
val id = tag.toLong
val label = callbackTags.find(_.tag == tag).map(_.label).getOrElse(sys.error("Unable to get callback tag label"))

View File

@@ -35,7 +35,7 @@ trait StaticDataForBooking extends Conversation[BookingData] {
private[actor] def staticData: ActorRef
protected def withFunctions(latestOptions: => Seq[IdName], staticOptions: => Either[Throwable, List[IdName]], applyId: IdName => BookingData): Step => AnswerFn = {
protected def withFunctions(latestOptions: => Seq[IdName], staticOptions: => Either[Throwable, List[IdName]], applyId: IdName => BookingData): Step => MessageProcessorFn = {
nextStep: Step => {
case Msg(cmd: Command, _) =>
staticData ! cmd
@@ -51,12 +51,12 @@ trait StaticDataForBooking extends Conversation[BookingData] {
}
}
protected def staticData(staticDataConfig: => StaticDataConfig)(functions: BookingData => Step => AnswerFn)(requestNext: Step): Step = {
question { _ =>
protected def staticData(staticDataConfig: => StaticDataConfig)(functions: BookingData => Step => MessageProcessorFn)(requestNext: Step): Step = {
ask { _ =>
staticData ! InitConversation
staticData ! StartConversation
staticData ! staticDataConfig
} answer {
} onReply {
case msg@Msg(_, bookingData: BookingData) =>
val fn = functions(bookingData)(requestNext)
fn(msg)

View File

@@ -42,7 +42,7 @@ class Visits(val userId: UserId, bot: Bot, apiService: ApiService, val localizat
entryPoint(prepareData)
def prepareData: Step =
internalConfig { _ =>
process { _ =>
val visits = apiService.reservedVisits(userId.accountId)
reservedVisitsPager ! InitConversation
reservedVisitsPager ! StartConversation
@@ -63,10 +63,10 @@ class Visits(val userId: UserId, bot: Bot, apiService: ApiService, val localizat
}
def askToCancelVisit: Step =
question { visit =>
ask { visit =>
bot.sendMessage(userId.source, lang.areYouSureToCancelAppointment(visit),
inlineKeyboard = createInlineKeyboard(Seq(Button(lang.no, Tags.No), Button(lang.yes, Tags.Yes))))
} answer {
} onReply {
case Msg(Command(_, _, Some(Tags.No)), _) =>
bot.sendMessage(userId.source, lang.appointmentWasNotCancelled)
end()

View File

@@ -15,13 +15,13 @@ trait Conversation[D] extends Actor with Domain[D] with Logger {
private var startWithStep: Step = _
private val defaultMsgHandler: AnswerFn = {
private val defaultMsgHandler: MessageProcessorFn = {
case Msg(any, data) =>
debug(s"Unhandled message received. [$any, $data]")
NextStep(currentStep, Some(data))
}
private var msgHandler: AnswerFn = defaultMsgHandler
private var msgHandler: MessageProcessorFn = defaultMsgHandler
private var runAfterInit: () => Unit = () => {}
@@ -34,8 +34,8 @@ trait Conversation[D] extends Actor with Domain[D] with Logger {
def execute(): Unit = {
try {
currentStep match {
case qa: QuestionAnswer => qa.question.questionFn(currentData)
case InternalConfiguration(fn) =>
case qa: Dialogue => qa.askFn(currentData)
case Process(fn) =>
val nextStep = fn(currentData)
moveToNextStep(nextStep)
case _ => //do nothing
@@ -56,10 +56,7 @@ trait Conversation[D] extends Actor with Domain[D] with Logger {
}
currentStep match {
case ExternalConfiguration(fn) =>
val conf = Msg(any, currentData)
handle(conf, fn, msgHandler)
case QuestionAnswer(_, Answer(fn)) =>
case Dialogue(_, fn) =>
val fact = Msg(any, currentData)
handle(fact, fn, msgHandler)
case Monologue(fn) =>
@@ -87,13 +84,11 @@ trait Conversation[D] extends Actor with Domain[D] with Logger {
init()
}
protected def monologue(answerFn: AnswerFn): Monologue = Monologue(answerFn)
protected def monologue(answerFn: MessageProcessorFn): Monologue = Monologue(answerFn)
protected def question(questionFn: D => Unit): Question = Question(questionFn)
protected def ask(askFn: D => Unit): Ask = Ask(askFn)
protected def externalConfig(receiveConfFunction: ExternalConfigFn): ExternalConfiguration = ExternalConfiguration(receiveConfFunction)
protected def internalConfig(receiveConfFunction: InternalConfigFn): InternalConfiguration = InternalConfiguration(receiveConfFunction)
protected def process(processFn: ProcessFn): Process = Process(processFn)
protected def end(): NextStep = NextStep(End)
@@ -104,7 +99,7 @@ trait Conversation[D] extends Actor with Domain[D] with Logger {
protected def stay(): NextStep = NextStep(currentStep)
protected def whenUnhandledMsg(receiveMsgFn: AnswerFn): Unit = {
protected def whenUnhandledMsg(receiveMsgFn: MessageProcessorFn): Unit = {
msgHandler = receiveMsgFn orElse defaultMsgHandler
}

View File

@@ -1,13 +1,11 @@
package com.lbs.server.actor.conversation
trait Domain[D] {
protected type QuestionFn = D => Unit
protected type AskFn = D => Unit
protected type AnswerFn = PartialFunction[Msg, NextStep]
protected type MessageProcessorFn = PartialFunction[Msg, NextStep]
protected type ExternalConfigFn = AnswerFn
protected type InternalConfigFn = D => NextStep
protected type ProcessFn = D => NextStep
protected case class Msg(message: Any, data: D)
@@ -15,22 +13,18 @@ trait Domain[D] {
private[conversation] object End extends Step
protected case class ExternalConfiguration(configFn: ExternalConfigFn) extends Step
protected case class Process(processFn: ProcessFn) extends Step
protected case class InternalConfiguration(configFn: InternalConfigFn) extends Step
protected case class Dialogue(askFn: AskFn, replyProcessorFn: MessageProcessorFn) extends Step
protected case class QuestionAnswer(question: Question, answer: Answer) extends Step
protected case class Monologue(answerFn: AnswerFn) extends Step
protected case class Monologue(replyProcessorFn: MessageProcessorFn) extends Step
private[conversation] case class NextStep(step: Step, data: Option[D] = None)
private[conversation] case class Question(questionFn: QuestionFn)
private[conversation] case class Ask(askFn: AskFn)
private[conversation] case class Answer(answerFn: AnswerFn)
protected implicit class RichQuestion(question: Question) {
def answer(answerFn: AnswerFn): QuestionAnswer = QuestionAnswer(question, Answer(answerFn))
protected implicit class RichQuestion(ask: Ask) {
def onReply(replyProcessorFn: MessageProcessorFn): Dialogue = Dialogue(ask.askFn, replyProcessorFn)
}
protected implicit class NextStepOps(nextStep: NextStep) {

View File

@@ -23,32 +23,32 @@ class ConversationSpec extends AkkaTestKit {
private var conf: String = _
def configure: Step =
externalConfig {
monologue {
case Msg(confStr: String, data) =>
conf = confStr
goto(askHello) using data.copy(configured = true)
}
def askHello: Step =
question { data =>
ask { data =>
self ! Hello
} answer {
} onReply {
case Msg(Hello, data) =>
goto(askWorld) using data.copy(hello = "hello")
}
def askWorld: Step =
question { data =>
ask { data =>
self ! World
} answer {
} onReply {
case Msg(World, data) =>
goto(askDialogue) using data.copy(world = "world")
}
def askDialogue: Step =
question { data =>
ask { data =>
self ! Dialogue
} answer {
} onReply {
case Msg(Dialogue, data) =>
originator ! data.copy(people = "dialogue") -> conf
end()
@@ -81,19 +81,19 @@ class ConversationSpec extends AkkaTestKit {
class TestActor(originator: ActorRef) extends Conversation[Data] {
def configure1: Step =
internalConfig { _ =>
process { _ =>
goto(configure2) using Data(configured = true)
}
def configure2: Step =
internalConfig { data =>
process { data =>
goto(askMessage2) using data.copy(message1 = "hello")
}
def askMessage2: Step =
question { _ =>
ask { _ =>
self ! InvokeEnrichMessage
} answer {
} onReply {
case Msg(InvokeEnrichMessage, data) =>
originator ! data.copy(message2 = "world")
end()