Skip to content

Commit

Permalink
resolves #1708 web_connector: implement Connector#notify
Browse files Browse the repository at this point in the history
  • Loading branch information
Fabilin authored and vsct-jburet committed Sep 30, 2024
1 parent 890f699 commit 91ef37d
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 23 deletions.
9 changes: 9 additions & 0 deletions bot/connector-web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,15 @@ and attempts to send them if and when the SSE connection is re-established.
The `tock_web_sse_keepalive_delay` optional property can be used to configure the number of seconds between
two SSE pings (default: 10).

#### Push messages

When SSE is enabled, the web connector allows sending push messages through the
[`notify` method](https://github.com/theopenconversationkit/tock/blob/master/bot/engine/src/main/kotlin/definition/DefinitionBuilders.kt).

Note that unlike with messaging apps, there is absolutely no guarantee that a user receives the message,
as they may have closed their browser since the last interaction. If they reopen the corresponding browsing tab,
they may still receive the message thanks to the aforementioned retry mechanism.

### React chat widget

The [`tock-react-kit`](https://github.com/theopenconversationkit/tock-react-kit) component provides integration with
Expand Down
85 changes: 66 additions & 19 deletions bot/connector-web/src/main/kotlin/WebConnector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,15 @@ import ai.tock.bot.connector.web.send.PostbackButton
import ai.tock.bot.connector.web.send.UrlButton
import ai.tock.bot.connector.web.send.WebCard
import ai.tock.bot.connector.web.send.WebCarousel
import ai.tock.bot.definition.IntentAware
import ai.tock.bot.definition.StoryHandlerDefinition
import ai.tock.bot.definition.StoryStep
import ai.tock.bot.engine.BotBus
import ai.tock.bot.engine.BotRepository
import ai.tock.bot.engine.ConnectorController
import ai.tock.bot.engine.action.Action
import ai.tock.bot.engine.action.ActionNotificationType
import ai.tock.bot.engine.action.SendChoice
import ai.tock.bot.engine.event.Event
import ai.tock.bot.engine.event.MetadataEvent
import ai.tock.bot.engine.user.PlayerId
Expand All @@ -52,6 +57,7 @@ import ai.tock.bot.orchestration.shared.SecondaryBotEligibilityResponse
import ai.tock.shared.Dice
import ai.tock.shared.Executor
import ai.tock.shared.booleanProperty
import ai.tock.shared.defaultLocale
import ai.tock.shared.injector
import ai.tock.shared.jackson.mapper
import ai.tock.shared.listProperty
Expand All @@ -71,6 +77,7 @@ import io.vertx.core.json.JsonObject
import io.vertx.ext.web.RoutingContext
import io.vertx.ext.web.handler.CorsHandler
import java.time.Duration
import java.util.Locale
import java.util.UUID
import mu.KotlinLogging

Expand Down Expand Up @@ -262,25 +269,7 @@ class WebConnector internal constructor(
val event = request.toEvent(applicationId)
val requestInfos = WebRequestInfos(context.request())
WebRequestInfosByEvent.put(event.id.toString(), requestInfos)
val callback = WebConnectorCallback(
applicationId = applicationId,
locale = request.locale,
context = context,
webMapper = webMapper,
eventId = event.id.toString(),
messageProcessor = messageProcessor,
)
if (sseEnabled) {
// Uniquely identify each response, so they can be reconciliated between SSE and POST
callback.addMetadata(MetadataEvent.responseId(UUID.randomUUID(), applicationId))
}
controller.handle(
event,
ConnectorData(
callback = callback,
metadata = extraHeadersAsMetadata(requestInfos)
)
)
handleEvent(applicationId, request.locale, event, controller, context, extraHeadersAsMetadata(requestInfos))
} catch (t: Throwable) {
BotRepository.requestTimer.throwable(t, timerData)
context.fail(t)
Expand All @@ -289,6 +278,64 @@ class WebConnector internal constructor(
}
}

private fun handleEvent(
applicationId: String,
locale: Locale,
event: Event,
controller: ConnectorController,
context: RoutingContext?,
headersMetadata: Map<String, String>,
) {
val callback = WebConnectorCallback(
applicationId = applicationId,
locale = locale,
context = context,
webMapper = webMapper,
eventId = event.id.toString(),
messageProcessor = messageProcessor,
)
if (sseEnabled) {
// Uniquely identify each response, so they can be reconciliated between SSE and POST
callback.addMetadata(MetadataEvent.responseId(UUID.randomUUID(), applicationId))
}
controller.handle(
event,
ConnectorData(
callback = callback,
metadata = headersMetadata
)
)
}

override fun notify(
controller: ConnectorController,
recipientId: PlayerId,
intent: IntentAware,
step: StoryStep<out StoryHandlerDefinition>?,
parameters: Map<String, String>,
notificationType: ActionNotificationType?,
errorListener: (Throwable) -> Unit
) {
if (!sseEnabled) {
throw UnsupportedOperationException("Web Connector only supports notifications when SSE is enabled")
}
handleEvent(
applicationId = applicationId,
locale = defaultLocale,
event = SendChoice(
recipientId,
applicationId,
PlayerId(applicationId, bot),
intent.wrappedIntent().name,
step,
parameters
),
controller = controller,
context = null,
headersMetadata = emptyMap(),
)
}

/**
* add extra configured Header to Metadata
* accessible if "tock_web_connector_use_extra_header_as_metadata_request" is true
Expand Down
8 changes: 4 additions & 4 deletions bot/connector-web/src/main/kotlin/WebConnectorCallback.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import java.util.concurrent.CopyOnWriteArrayList
internal class WebConnectorCallback(
applicationId: String,
val locale: Locale,
private val context: RoutingContext,
private val context: RoutingContext?,
private val actions: MutableList<Action> = CopyOnWriteArrayList(),
private val metadata: MutableMap<String, String> = mutableMapOf(),
private val webMapper: ObjectMapper,
Expand All @@ -51,8 +51,8 @@ internal class WebConnectorCallback(

fun sendResponse() {
WebRequestInfosByEvent.invalidate(eventId)
context.response()
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
.end(webMapper.writeValueAsString(createResponse(actions)))
context?.response()
?.putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
?.end(webMapper.writeValueAsString(createResponse(actions)))
}
}

0 comments on commit 91ef37d

Please sign in to comment.