@@ -25,6 +25,8 @@ import ai.tock.bot.connector.media.MediaCard
25
25
import ai.tock.bot.connector.media.MediaCarousel
26
26
import ai.tock.bot.connector.media.MediaFile
27
27
import ai.tock.bot.connector.media.MediaMessage
28
+ import ai.tock.bot.connector.web.channel.ChannelMongoDAO
29
+ import ai.tock.bot.connector.web.channel.Channels
28
30
import ai.tock.bot.connector.web.send.UrlButton
29
31
import ai.tock.bot.connector.web.send.WebCard
30
32
import ai.tock.bot.connector.web.send.WebCarousel
@@ -36,21 +38,27 @@ import ai.tock.bot.engine.event.Event
36
38
import ai.tock.bot.engine.user.PlayerId
37
39
import ai.tock.bot.engine.user.UserPreferences
38
40
import ai.tock.shared.Executor
41
+ import ai.tock.shared.booleanProperty
39
42
import ai.tock.shared.injector
40
43
import ai.tock.shared.jackson.mapper
41
44
import ai.tock.shared.provide
45
+ import com.fasterxml.jackson.databind.module.SimpleModule
46
+ import com.fasterxml.jackson.databind.ser.std.ToStringSerializer
42
47
import com.fasterxml.jackson.module.kotlin.readValue
43
48
import io.vertx.core.http.HttpMethod
44
49
import io.vertx.ext.web.RoutingContext
45
50
import io.vertx.ext.web.handler.CorsHandler
46
51
import mu.KotlinLogging
47
52
48
53
internal const val WEB_CONNECTOR_ID = " web"
54
+
49
55
/* *
50
56
* The web (REST) connector type.
51
57
*/
52
58
val webConnectorType = ConnectorType (WEB_CONNECTOR_ID )
53
59
60
+ private val sseEnabled = booleanProperty(" tock_web_sse" , false )
61
+
54
62
class WebConnector internal constructor(
55
63
val applicationId : String ,
56
64
val path : String
@@ -62,18 +70,53 @@ class WebConnector internal constructor(
62
70
63
71
private val executor: Executor get() = injector.provide()
64
72
73
+ private val channels = Channels (ChannelMongoDAO )
74
+
75
+ private val webMapper = mapper.copy().registerModule(
76
+ SimpleModule ().apply {
77
+ // fallback for serializing CharSequence
78
+ addSerializer(CharSequence ::class .java, ToStringSerializer ())
79
+ }
80
+ )
81
+
65
82
override fun register (controller : ConnectorController ) {
83
+
66
84
controller.registerServices(path) { router ->
67
85
logger.debug(" deploy web connector services for root path $path " )
68
86
69
87
router.route(path)
70
88
.handler(
71
89
CorsHandler .create(" *" )
72
90
.allowedMethod(HttpMethod .POST )
91
+ .run {
92
+ if (sseEnabled) allowedMethod(HttpMethod .GET ) else this
93
+ }
73
94
.allowedHeader(" Access-Control-Allow-Origin" )
74
95
.allowedHeader(" Content-Type" )
75
96
.allowedHeader(" X-Requested-With" )
76
97
)
98
+ if (sseEnabled) {
99
+ router.route(path + " /sse" )
100
+ .handler { context ->
101
+ try {
102
+ val userId = context.queryParams()[" userId" ]
103
+ val response = context.response()
104
+ response.isChunked = true
105
+ response.headers().add(" Content-Type" , " text/event-stream;charset=UTF-8" )
106
+ response.headers().add(" Connection" , " keep-alive" )
107
+ response.headers().add(" Cache-Control" , " no-cache" )
108
+ val channelId = channels.register(userId) { webConnectorResponse ->
109
+ response.write(" event: message\n " )
110
+ response.write(" data: ${webMapper.writeValueAsString(webConnectorResponse)} \n\n " )
111
+ }
112
+ response.closeHandler {
113
+ channels.unregister(channelId)
114
+ }
115
+ } catch (t: Throwable ) {
116
+ context.fail(t)
117
+ }
118
+ }
119
+ }
77
120
router.post(path)
78
121
.handler { context ->
79
122
try {
@@ -96,7 +139,7 @@ class WebConnector internal constructor(
96
139
try {
97
140
logger.debug { " Web request input : $body " }
98
141
val request: WebConnectorRequest = mapper.readValue(body)
99
- val callback = WebConnectorCallback (applicationId, request.locale, context)
142
+ val callback = WebConnectorCallback (applicationId = applicationId, locale = request.locale, context = context, webMapper = webMapper )
100
143
controller.handle(request.toEvent(applicationId), ConnectorData (callback))
101
144
} catch (t: Throwable ) {
102
145
BotRepository .requestTimer.throwable(t, timerData)
@@ -110,6 +153,7 @@ class WebConnector internal constructor(
110
153
val c = callback as ? WebConnectorCallback
111
154
c?.addAction(event)
112
155
if (event is Action ) {
156
+ channels.send(event)
113
157
if (event.metadata.lastAnswer) {
114
158
c?.sendResponse()
115
159
}
@@ -141,7 +185,7 @@ class WebConnector internal constructor(
141
185
WebMessage (card = WebCard (
142
186
title = message.title,
143
187
subTitle = message.subTitle,
144
- file = message.file?.url?.let { MediaFile (message.file?.url as String , message.file?.name as String )},
188
+ file = message.file?.url?.let { MediaFile (message.file?.url as String , message.file?.name as String ) },
145
189
buttons = message.actions.map { UrlButton (it.title.toString(), it.url.toString()) }
146
190
))
147
191
}
@@ -150,7 +194,7 @@ class WebConnector internal constructor(
150
194
WebCard (
151
195
title = mediaCard.title,
152
196
subTitle = mediaCard.subTitle,
153
- file = mediaCard.file?.url?.let { MediaFile (mediaCard.file?.url as String , mediaCard.file?.name as String )},
197
+ file = mediaCard.file?.url?.let { MediaFile (mediaCard.file?.url as String , mediaCard.file?.name as String ) },
154
198
buttons = mediaCard.actions.map { button -> UrlButton (button.title.toString(), button.url.toString()) }
155
199
)
156
200
}))
0 commit comments