diff --git a/channels/facebook/src/main/kotlin/com/justai/jaicf/channel/facebook/FacebookChannel.kt b/channels/facebook/src/main/kotlin/com/justai/jaicf/channel/facebook/FacebookChannel.kt index 863a79e9..b0d04bc8 100644 --- a/channels/facebook/src/main/kotlin/com/justai/jaicf/channel/facebook/FacebookChannel.kt +++ b/channels/facebook/src/main/kotlin/com/justai/jaicf/channel/facebook/FacebookChannel.kt @@ -2,7 +2,7 @@ package com.justai.jaicf.channel.facebook import com.github.messenger4j.exception.MessengerVerificationException import com.justai.jaicf.api.BotApi -import com.justai.jaicf.channel.facebook.api.FacebookGatewayRequest +import com.justai.jaicf.channel.facebook.api.FacebookInvocationRequest import com.justai.jaicf.channel.facebook.api.toBotRequest import com.justai.jaicf.channel.facebook.messenger.Messenger import com.justai.jaicf.channel.http.HttpBotRequest @@ -11,13 +11,14 @@ import com.justai.jaicf.channel.http.asTextHttpBotResponse import com.justai.jaicf.channel.jaicp.JaicpCompatibleAsyncBotChannel import com.justai.jaicf.channel.jaicp.JaicpCompatibleAsyncChannelFactory import com.justai.jaicf.context.RequestContext -import com.justai.jaicf.gateway.BotGateway -import com.justai.jaicf.gateway.BotGatewayRequest +import com.justai.jaicf.channel.invocationapi.InvocableBotChannel +import com.justai.jaicf.channel.invocationapi.InvocationRequest import java.util.* class FacebookChannel private constructor( override val botApi: BotApi -) : JaicpCompatibleAsyncBotChannel, BotGateway() { +) : JaicpCompatibleAsyncBotChannel, + InvocableBotChannel { private lateinit var messenger: Messenger @@ -55,13 +56,13 @@ class FacebookChannel private constructor( override val channelType = "facebook" override fun create(botApi: BotApi, apiUrl: String) = FacebookChannel(botApi, apiUrl) - private const val REQUEST_TEMPLATE_PATH = "/FacebookRequestTemplate.json" + internal const val REQUEST_TEMPLATE_PATH = "/FacebookRequestTemplate.json" } - override fun processGatewayRequest(request: BotGatewayRequest, requestContext: RequestContext) { + override fun processExternalInvocation(request: InvocationRequest, requestContext: RequestContext) { val template = getRequestTemplateFromResources(request, REQUEST_TEMPLATE_PATH) messenger.onReceiveEvents(template, Optional.empty()) { event -> - FacebookGatewayRequest.create(request, event.asTextMessageEvent())?.let { + FacebookInvocationRequest.create(request, event.asTextMessageEvent())?.let { botApi.process(it, FacebookReactions(messenger, it), requestContext) } } diff --git a/channels/facebook/src/main/kotlin/com/justai/jaicf/channel/facebook/api/FacebookBotRequest.kt b/channels/facebook/src/main/kotlin/com/justai/jaicf/channel/facebook/api/FacebookBotRequest.kt index b3821663..692a89f9 100644 --- a/channels/facebook/src/main/kotlin/com/justai/jaicf/channel/facebook/api/FacebookBotRequest.kt +++ b/channels/facebook/src/main/kotlin/com/justai/jaicf/channel/facebook/api/FacebookBotRequest.kt @@ -5,9 +5,9 @@ import com.github.messenger4j.webhook.event.* import com.justai.jaicf.api.BotRequest import com.justai.jaicf.api.EventBotRequest import com.justai.jaicf.api.QueryBotRequest -import com.justai.jaicf.gateway.BotGatewayEventRequest -import com.justai.jaicf.gateway.BotGatewayQueryRequest -import com.justai.jaicf.gateway.BotGatewayRequest +import com.justai.jaicf.channel.invocationapi.InvocationEventRequest +import com.justai.jaicf.channel.invocationapi.InvocationQueryRequest +import com.justai.jaicf.channel.invocationapi.InvocationRequest val BotRequest.facebook get() = this as? FacebookBotRequest @@ -97,26 +97,26 @@ internal fun Event.toBotRequest(): FacebookBotRequest = when { else -> FacebookFallbackBotRequest(FallbackEvent(senderId(), recipientId(), timestamp())) } -interface FacebookGatewayRequest : FacebookBotRequest, BotGatewayRequest { +interface FacebookInvocationRequest : FacebookBotRequest, InvocationRequest { companion object { - fun create(r: BotGatewayRequest, event: BaseEvent) = when (r) { - is BotGatewayEventRequest -> FacebookGatewayEventRequest(event, r.clientId, r.input, r.requestData) - is BotGatewayQueryRequest -> FacebookGatewayQueryRequest(event, r.clientId, r.input, r.requestData) + fun create(r: InvocationRequest, event: BaseEvent) = when (r) { + is InvocationEventRequest -> FacebookInvocationEventRequest(event, r.clientId, r.input, r.requestData) + is InvocationQueryRequest -> FacebookInvocationQueryRequest(event, r.clientId, r.input, r.requestData) else -> null } } } -data class FacebookGatewayEventRequest( +data class FacebookInvocationEventRequest( override val event: BaseEvent, override val clientId: String, override val input: String, override val requestData: String -) : FacebookGatewayRequest, BotGatewayEventRequest(clientId, input, requestData) +) : FacebookInvocationRequest, InvocationEventRequest(clientId, input, requestData) -data class FacebookGatewayQueryRequest( +data class FacebookInvocationQueryRequest( override val event: BaseEvent, override val clientId: String, override val input: String, override val requestData: String -) : FacebookGatewayRequest, BotGatewayQueryRequest(clientId, input, requestData) \ No newline at end of file +) : FacebookInvocationRequest, InvocationQueryRequest(clientId, input, requestData) \ No newline at end of file diff --git a/channels/facebook/src/main/resources/FacebookRequestTemplate.json b/channels/facebook/src/main/resources/FacebookRequestTemplate.json index 1ee454c4..5c1b1fd9 100644 --- a/channels/facebook/src/main/resources/FacebookRequestTemplate.json +++ b/channels/facebook/src/main/resources/FacebookRequestTemplate.json @@ -2,8 +2,8 @@ "object": "page", "entry": [ { - "id": "110387787249256", - "time": 1611153924637, + "id": "{{ randomInt }}", + "time": "{{ timestamp }}", "messaging": [ { "sender": { @@ -12,9 +12,9 @@ "recipient": { "id": "{{ clientId }}" }, - "timestamp": 1611153924412, + "timestamp": "{{ timestamp }}", "message": { - "mid": "predefined_message_id", + "mid": "{{ messageId }}", "text": "{{ text }}" } } diff --git a/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/JaicpEvents.kt b/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/JaicpEvents.kt index df496424..6b5c3f62 100644 --- a/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/JaicpEvents.kt +++ b/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/JaicpEvents.kt @@ -6,10 +6,10 @@ package com.justai.jaicf.channel.jaicp * @property fileEvent - is sent when user sends file or image. File will be uploaded to s3 storage, * link will be provided in request.telephony?.jaicp?.data * @property liveChatFinished - is sent when livechat is finished and request execution is returned to bot. - * @property noLivechatOperatorsOnline - is sent when scenario attempted switchToOperator, but no operators were online. + * @property noLivechatOperatorsOnline - is sent when scenario attempted switchToLiveChat, but no operators were online. * * @see com.justai.jaicf.channel.jaicp.channels.TelephonyEvents - * @see com.justai.jaicf.channel.jaicp.reactions.switchToOperator + * @see com.justai.jaicf.channel.jaicp.reactions.switchToLiveChat * */ @Suppress("MemberVisibilityCanBePrivate") object JaicpEvents { diff --git a/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/dto/JaicpBotRequest.kt b/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/dto/JaicpBotRequest.kt index 2bd0dd14..ae09242d 100644 --- a/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/dto/JaicpBotRequest.kt +++ b/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/dto/JaicpBotRequest.kt @@ -35,7 +35,7 @@ data class JaicpBotRequest( fun stringify() = JSON.encodeToString(serializer(), this) - internal fun isGatewayRequest() = rawRequest["commonType"]?.jsonPrimitive?.contentOrNull == "COMMON" + internal fun isExternalInvocationRequest() = rawRequest["commonType"]?.jsonPrimitive?.contentOrNull == "COMMON" internal fun asHttpBotRequest() = raw.asHttpBotRequest(stringify()) } \ No newline at end of file diff --git a/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/dto/JaicpReplies.kt b/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/dto/JaicpReplies.kt index 9317f63a..ca20f473 100644 --- a/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/dto/JaicpReplies.kt +++ b/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/dto/JaicpReplies.kt @@ -95,7 +95,7 @@ class TelephonySwitchReply( * @param sendMessagesToOperator - true to send conversation history to operator. * @param sendMessageHistoryAmount - amount of last conversation history messages to send to operator. * - * @see com.justai.jaicf.channel.jaicp.reactions.switchToOperator + * @see com.justai.jaicf.channel.jaicp.reactions.switchToLiveChat * */ @Serializable data class LiveChatSwitchReply( diff --git a/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/execution/ThreadPoolRequestExecutor.kt b/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/execution/ThreadPoolRequestExecutor.kt index e9350b14..112d7e72 100644 --- a/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/execution/ThreadPoolRequestExecutor.kt +++ b/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/execution/ThreadPoolRequestExecutor.kt @@ -1,15 +1,16 @@ package com.justai.jaicf.channel.jaicp.execution import com.justai.jaicf.channel.BotChannel -import com.justai.jaicf.channel.http.asHttpBotRequest import com.justai.jaicf.channel.jaicp.* import com.justai.jaicf.channel.jaicp.JSON import com.justai.jaicf.channel.jaicp.channels.JaicpNativeBotChannel import com.justai.jaicf.channel.jaicp.dto.JaicpBotRequest import com.justai.jaicf.channel.jaicp.dto.JaicpBotResponse import com.justai.jaicf.channel.jaicp.dto.fromRequest -import com.justai.jaicf.channel.jaicp.gateway.BotGatewayRequestAdapter -import com.justai.jaicf.gateway.BotGateway +import com.justai.jaicf.channel.jaicp.invocationapi.InvocationRequestData +import com.justai.jaicf.context.RequestContext +import com.justai.jaicf.channel.invocationapi.InvocationEventRequest +import com.justai.jaicf.channel.invocationapi.InvocableBotChannel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.async @@ -51,14 +52,13 @@ class ThreadPoolRequestExecutor(nThreads: Int) : CoroutineScope { channel.processCompatible(request) private fun executeAsync(channel: JaicpCompatibleAsyncBotChannel, request: JaicpBotRequest) { - if (channel is BotGateway && BotGatewayRequestAdapter.ensureGatewayRequest(channel, request)) { - return + val isProcessed = tryProcessAsExternalInvocation(channel, request) + if (!isProcessed) { + channel.process(request.asHttpBotRequest()) } - channel.process(request.asHttpBotRequest()) } } - private fun JaicpCompatibleBotChannel.processCompatible( botRequest: JaicpBotRequest ): JaicpBotResponse { @@ -81,3 +81,22 @@ private fun addRawReply(rawResponse: JsonElement) = buildJsonObject { }) } } + +private fun tryProcessAsExternalInvocation( + channel: JaicpCompatibleAsyncBotChannel, + request: JaicpBotRequest +): Boolean { + if (channel !is InvocableBotChannel) return false + if (!request.isExternalInvocationRequest()) return false + val event = request.event ?: return false + val data = try { + JSON.decodeFromString(InvocationRequestData.serializer(), request.raw) + } catch (e: Exception) { + return false + } + channel.processExternalInvocation( + request = InvocationEventRequest(data.chatId, event, request.raw), + requestContext = RequestContext.fromHttp(request.asHttpBotRequest()) + ) + return true +} diff --git a/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/gateway/BotGatewayRequestAdapter.kt b/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/gateway/BotGatewayRequestAdapter.kt deleted file mode 100644 index 9914f065..00000000 --- a/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/gateway/BotGatewayRequestAdapter.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.justai.jaicf.channel.jaicp.gateway - -import com.justai.jaicf.channel.jaicp.JSON -import com.justai.jaicf.channel.jaicp.dto.JaicpBotRequest -import com.justai.jaicf.context.RequestContext -import com.justai.jaicf.gateway.BotGateway -import com.justai.jaicf.gateway.BotGatewayEventRequest - -internal object BotGatewayRequestAdapter { - - /** - * Ensures jaicp bot gateway request processing. - * - * @return true is request is [BotGatewayEventRequest] and it is processed - * - * @see BotGateway - * @see BotGatewayEventRequest - * */ - fun ensureGatewayRequest(channel: BotGateway, request: JaicpBotRequest): Boolean { - val event = request.event ?: return false - if (!request.isGatewayRequest()) { - return false - } - val data = try { - JSON.decodeFromString(BotGatewayRequestData.serializer(), request.raw) - } catch (e: Exception) { - return false - } - channel.processGatewayRequest( - request = BotGatewayEventRequest(data.chatId, event, request.raw), - requestContext = RequestContext.fromHttp(request.asHttpBotRequest()) - ) - return true - } -} \ No newline at end of file diff --git a/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/gateway/BotGatewayRequestData.kt b/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/invocationapi/InvocationRequestData.kt similarity index 82% rename from channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/gateway/BotGatewayRequestData.kt rename to channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/invocationapi/InvocationRequestData.kt index 9195ec0e..ab5691f0 100644 --- a/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/gateway/BotGatewayRequestData.kt +++ b/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/invocationapi/InvocationRequestData.kt @@ -1,10 +1,10 @@ -package com.justai.jaicf.channel.jaicp.gateway +package com.justai.jaicf.channel.jaicp.invocationapi import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonObject @Serializable -internal data class BotGatewayRequestData( +internal data class InvocationRequestData( val commonType: String, val chatId: String, val timestamp: Long, diff --git a/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/livechat/exceptions/LiveChatExceptions.kt b/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/livechat/exceptions/LiveChatExceptions.kt index 8f5a6bef..9fce63d4 100644 --- a/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/livechat/exceptions/LiveChatExceptions.kt +++ b/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/livechat/exceptions/LiveChatExceptions.kt @@ -1,27 +1,27 @@ package com.justai.jaicf.channel.jaicp.livechat.exceptions import com.justai.jaicf.channel.jaicp.dto.JaicpBotRequest -import com.justai.jaicf.channel.jaicp.reactions.switchToOperator +import com.justai.jaicf.channel.jaicp.reactions.switchToLiveChat import com.justai.jaicf.channel.jaicp.dto.LiveChatSwitchReply import com.justai.jaicf.reactions.jaicp.JaicpCompatibleAsyncReactions /** - * An exception thrown by [switchToOperator] reaction when no operators are available to switch conversation to operator. + * An exception thrown by [switchToLiveChat] reaction when no operators are available to switch conversation to operator. * [LiveChatSwitchReply.ignoreOffline] parameter can be used in switch reply to ignore if operators are offline. * * @see [JaicpCompatibleAsyncReactions] base interface for jaicp asynchronous reactions * @see [LiveChatSwitchReply] full object with livechat switch parameters - * @see [switchToOperator] jaicpAsync reaction + * @see [switchToLiveChat] jaicpAsync reaction * */ class NoOperatorsOnlineException(request: JaicpBotRequest) : RuntimeException() { override val message: String = "No operators online for channel ${request.channelBotId}" } /** - * An exception thrown by [switchToOperator] reaction when livechat is not configured for current channel. + * An exception thrown by [switchToLiveChat] reaction when livechat is not configured for current channel. * * @see [JaicpCompatibleAsyncReactions] base interface for jaicp asynchronous reactions - * @see [switchToOperator] jaicpAsync reaction + * @see [switchToLiveChat] jaicpAsync reaction * */ class NoOperatorChannelConfiguredException(request: JaicpBotRequest) : RuntimeException() { override val message: String = "No operator channel configured for channel ${request.channelBotId}" diff --git a/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/logging/JaicpConversationLogger.kt b/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/logging/JaicpConversationLogger.kt index da7d28d2..d564b114 100644 --- a/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/logging/JaicpConversationLogger.kt +++ b/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/logging/JaicpConversationLogger.kt @@ -58,7 +58,7 @@ open class JaicpConversationLogger( internal open fun createLog(req: JaicpBotRequest, ctx: LoggingContext, session: SessionData) = JaicpLogModel.fromRequest(req, ctx, session).also { - logger.debug("Send log with sessionId: ${it.sessionId} isNewSession: ${it.isNewSession}") + logger.trace("Send log with sessionId: ${it.sessionId} isNewSession: ${it.isNewSession}") } } diff --git a/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/reactions/JaicpAsyncReactions.kt b/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/reactions/JaicpAsyncReactions.kt index b478e265..bdb5f900 100644 --- a/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/reactions/JaicpAsyncReactions.kt +++ b/channels/jaicp/src/main/kotlin/com/justai/jaicf/channel/jaicp/reactions/JaicpAsyncReactions.kt @@ -5,7 +5,9 @@ import com.justai.jaicf.channel.jaicp.http.ChatAdapterConnector import com.justai.jaicf.channel.jaicp.livechat.LiveChatInitRequest import com.justai.jaicf.channel.jaicp.livechat.exceptions.NoOperatorChannelConfiguredException import com.justai.jaicf.channel.jaicp.livechat.exceptions.NoOperatorsOnlineException +import com.justai.jaicf.logging.Reaction import com.justai.jaicf.reactions.jaicp.JaicpCompatibleAsyncReactions +import kotlinx.serialization.json.JsonObject /** * Switches to livechat operator if channel is connected to livechat in JAICP App Console. @@ -15,8 +17,8 @@ import com.justai.jaicf.reactions.jaicp.JaicpCompatibleAsyncReactions * @throws NoOperatorsOnlineException when no livechat operators are available * @throws NoOperatorChannelConfiguredException when current channel has no livechat configured * */ -fun JaicpCompatibleAsyncReactions.switchToOperator(message: String) = - switchToOperator(LiveChatSwitchReply(firstMessage = message)) +fun JaicpCompatibleAsyncReactions.switchToLiveChat(message: String) = + switchToLiveChat(LiveChatSwitchReply(firstMessage = message)) /** @@ -29,7 +31,42 @@ fun JaicpCompatibleAsyncReactions.switchToOperator(message: String) = * @throws NoOperatorsOnlineException when no livechat operators are available * @throws NoOperatorChannelConfiguredException when current channel has no livechat configured * */ -fun JaicpCompatibleAsyncReactions.switchToOperator(reply: LiveChatSwitchReply) = - LiveChatInitRequest.create(loggingContext, reply)?.let { - ChatAdapterConnector.getIfExists()?.initLiveChat(it) - } \ No newline at end of file +fun JaicpCompatibleAsyncReactions.switchToLiveChat(reply: LiveChatSwitchReply): SwitchReaction? { + val switchRequest = LiveChatInitRequest.create(loggingContext, reply) ?: return null + val connector = ChatAdapterConnector.getIfExists() ?: return null + connector.initLiveChat(switchRequest) + return SwitchReaction.fromReply(switchRequest.switchData, loggingContext.botContext.dialogContext.currentState) + .also { loggingContext.reactions.add(it) } +} + +data class SwitchReaction( + val firstMessage: String? = null, + val closeChatPhrases: List = emptyList(), + val appendCloseChatButton: Boolean = false, + val ignoreOffline: Boolean = false, + val oneTimeMessage: Boolean = false, + val destination: String? = null, + val lastMessage: String? = null, + val attributes: JsonObject? = null, + val hiddenAttributes: JsonObject? = null, + val sendMessagesToOperator: Boolean = false, + val sendMessageHistoryAmount: Int? = null, + override val fromState: String +) : Reaction(fromState) { + companion object { + fun fromReply(switchReply: LiveChatSwitchReply, state: String) = SwitchReaction( + firstMessage = switchReply.firstMessage, + closeChatPhrases = switchReply.closeChatPhrases, + appendCloseChatButton = switchReply.appendCloseChatButton, + ignoreOffline = switchReply.ignoreOffline, + oneTimeMessage = switchReply.oneTimeMessage, + destination = switchReply.destination, + lastMessage = switchReply.lastMessage, + attributes = switchReply.attributes, + hiddenAttributes = switchReply.hiddenAttributes, + sendMessageHistoryAmount = switchReply.sendMessageHistoryAmount, + sendMessagesToOperator = switchReply.sendMessagesToOperator, + fromState = state + ) + } +} \ No newline at end of file diff --git a/channels/jaicp/src/test/kotlin/com/justai/jaicf/channel/jaicp/livechat/JaicpLiveChatsTest.kt b/channels/jaicp/src/test/kotlin/com/justai/jaicf/channel/jaicp/livechat/JaicpLiveChatsTest.kt index 0f5d42b9..77180031 100644 --- a/channels/jaicp/src/test/kotlin/com/justai/jaicf/channel/jaicp/livechat/JaicpLiveChatsTest.kt +++ b/channels/jaicp/src/test/kotlin/com/justai/jaicf/channel/jaicp/livechat/JaicpLiveChatsTest.kt @@ -6,7 +6,7 @@ import com.justai.jaicf.channel.jaicp.JaicpTestChannel import com.justai.jaicf.channel.jaicp.ScenarioFactory.echoWithAction import com.justai.jaicf.channel.jaicp.channels.ChatWidgetChannel import com.justai.jaicf.channel.jaicp.dto.LiveChatSwitchReply -import com.justai.jaicf.channel.jaicp.reactions.switchToOperator +import com.justai.jaicf.channel.jaicp.reactions.switchToLiveChat import com.justai.jaicf.reactions.jaicp.jaicpAsync import org.junit.jupiter.api.Test import kotlin.test.assertEquals @@ -18,7 +18,7 @@ internal class JaicpLiveChatsTest : JaicpBaseTest() { fun `001 livechat should switch to operator`() { val message = "My First Message" val scenario = echoWithAction { - reactions.jaicpAsync?.switchToOperator(message) + reactions.jaicpAsync?.switchToLiveChat(message) } JaicpTestChannel(scenario, ChatWidgetChannel).process(request) @@ -36,7 +36,7 @@ internal class JaicpLiveChatsTest : JaicpBaseTest() { oneTimeMessage = false ) val scenario = echoWithAction { - reactions.jaicpAsync?.switchToOperator(expected) + reactions.jaicpAsync?.switchToLiveChat(expected) } JaicpTestChannel(scenario, ChatWidgetChannel).process(request) diff --git a/channels/slack/src/main/kotlin/com/justai/jaicf/channel/slack/SlackBotRequest.kt b/channels/slack/src/main/kotlin/com/justai/jaicf/channel/slack/SlackBotRequest.kt index 9d4f030f..6363dd17 100644 --- a/channels/slack/src/main/kotlin/com/justai/jaicf/channel/slack/SlackBotRequest.kt +++ b/channels/slack/src/main/kotlin/com/justai/jaicf/channel/slack/SlackBotRequest.kt @@ -3,9 +3,9 @@ package com.justai.jaicf.channel.slack import com.justai.jaicf.api.BotRequest import com.justai.jaicf.api.BotRequestType import com.justai.jaicf.api.QueryBotRequest -import com.justai.jaicf.gateway.BotGatewayEventRequest -import com.justai.jaicf.gateway.BotGatewayQueryRequest -import com.justai.jaicf.gateway.BotGatewayRequest +import com.justai.jaicf.channel.invocationapi.InvocationEventRequest +import com.justai.jaicf.channel.invocationapi.InvocationQueryRequest +import com.justai.jaicf.channel.invocationapi.InvocationRequest import com.slack.api.app_backend.events.payload.EventsApiPayload import com.slack.api.app_backend.interactive_components.payload.BlockActionPayload import com.slack.api.app_backend.slash_commands.payload.SlashCommandPayload @@ -61,24 +61,24 @@ data class SlackActionRequest( input = payload.actions[0].value ) -interface SlackGatewayRequest : SlackBotRequest, BotGatewayRequest { +interface SlackInvocationRequest : SlackBotRequest, InvocationRequest { companion object { - fun create(r: BotGatewayRequest) = when (r) { - is BotGatewayEventRequest -> SlackGatewayEventRequest(r.clientId, r.input, r.requestData) - is BotGatewayQueryRequest -> SlackGatewayQueryRequest(r.clientId, r.input, r.requestData) + fun create(r: InvocationRequest) = when (r) { + is InvocationEventRequest -> SlackInvocationEventRequest(r.clientId, r.input, r.requestData) + is InvocationQueryRequest -> SlackInvocationQueryRequest(r.clientId, r.input, r.requestData) else -> null } } } -data class SlackGatewayEventRequest( +data class SlackInvocationEventRequest( override val clientId: String, override val input: String, override val requestData: String -) : SlackGatewayRequest, BotGatewayEventRequest(clientId, input, requestData) +) : SlackInvocationRequest, InvocationEventRequest(clientId, input, requestData) -data class SlackGatewayQueryRequest( +data class SlackInvocationQueryRequest( override val clientId: String, override val input: String, override val requestData: String -) : SlackGatewayRequest, BotGatewayQueryRequest(clientId, input, requestData) +) : SlackInvocationRequest, InvocationQueryRequest(clientId, input, requestData) diff --git a/channels/slack/src/main/kotlin/com/justai/jaicf/channel/slack/SlackChannel.kt b/channels/slack/src/main/kotlin/com/justai/jaicf/channel/slack/SlackChannel.kt index 3c854efe..96ca7f4c 100644 --- a/channels/slack/src/main/kotlin/com/justai/jaicf/channel/slack/SlackChannel.kt +++ b/channels/slack/src/main/kotlin/com/justai/jaicf/channel/slack/SlackChannel.kt @@ -7,8 +7,8 @@ import com.justai.jaicf.channel.http.asHttpBotRequest import com.justai.jaicf.channel.jaicp.JaicpCompatibleAsyncBotChannel import com.justai.jaicf.channel.jaicp.JaicpCompatibleAsyncChannelFactory import com.justai.jaicf.context.RequestContext -import com.justai.jaicf.gateway.BotGateway -import com.justai.jaicf.gateway.BotGatewayRequest +import com.justai.jaicf.channel.invocationapi.InvocableBotChannel +import com.justai.jaicf.channel.invocationapi.InvocationRequest import com.justai.jaicf.helpers.http.toUrl import com.justai.jaicf.helpers.kotlin.PropertyWithBackingField import com.slack.api.Slack @@ -23,11 +23,12 @@ import com.slack.api.methods.MethodsConfig import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit class SlackChannel private constructor( override val botApi: BotApi ) : JaicpCompatibleAsyncBotChannel, - BotGateway(), + InvocableBotChannel, CoroutineScope by CoroutineScope(Dispatchers.Default) { private lateinit var app: App @@ -98,12 +99,12 @@ class SlackChannel private constructor( botApi.process(request, reactions, RequestContext.fromHttp(httpBotRequest)) } - override fun processGatewayRequest(request: BotGatewayRequest, requestContext: RequestContext) { - val gwRequest = SlackGatewayRequest.create(request) ?: return - val slackRequest = - buildSlackRequest(getRequestTemplateFromResources(request, REQUEST_TEMPLATE_PATH).asHttpBotRequest()) - SlackGatewayRequest.create(request) - botApi.process(gwRequest, SlackReactions(slackRequest.context), requestContext) + override fun provideTimestamp(): String = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()).toString() + + override fun processExternalInvocation(request: InvocationRequest, requestContext: RequestContext) { + val invocationRequest = SlackInvocationRequest.create(request) ?: return + val slackRequest = buildSlackRequest(getRequestTemplateFromResources(request, REQUEST_TEMPLATE_PATH).asHttpBotRequest()) + botApi.process(invocationRequest, SlackReactions(slackRequest.context), requestContext) } override fun process(request: HttpBotRequest): HttpBotResponse { diff --git a/channels/slack/src/main/resources/SlackRequestTemplate.json b/channels/slack/src/main/resources/SlackRequestTemplate.json index a6811a5a..22b038fd 100644 --- a/channels/slack/src/main/resources/SlackRequestTemplate.json +++ b/channels/slack/src/main/resources/SlackRequestTemplate.json @@ -3,11 +3,11 @@ "team_id": "", "api_app_id": "", "event": { - "client_msg_id": "", + "client_msg_id": "{{ messageId }}", "type": "message", "text": "{{ text }}", "user": "", - "ts": "1611160062.001100", + "ts": "{{ timestamp }}.001100", "team": "", "blocks": [ { @@ -27,7 +27,7 @@ } ], "channel": "{{ clientId }}", - "event_ts": "1611160062.001100", + "event_ts": "{{ timestamp }}.001100", "channel_type": "im" }, "type": "event_callback", @@ -37,13 +37,13 @@ { "enterprise_id": null, "team_id": "", - "user_id": "", + "user_id": "{{ clientId }}", "is_bot": true, "is_enterprise_install": false } ], "is_ext_shared_channel": false, - "event_context": "1-message-T1EQM7A4D-D01CXF85Q9K", + "event_context": "1-message-{{ messageId }}-{{ clientId }}", "user_info": { "real_name_normalized": "GatewayAPI User", "display_name_normalized": "" diff --git a/channels/telegram/src/main/kotlin/com/justai/jaicf/channel/telegram/TelegramBotRequest.kt b/channels/telegram/src/main/kotlin/com/justai/jaicf/channel/telegram/TelegramBotRequest.kt index 395b0f8c..df3319e9 100644 --- a/channels/telegram/src/main/kotlin/com/justai/jaicf/channel/telegram/TelegramBotRequest.kt +++ b/channels/telegram/src/main/kotlin/com/justai/jaicf/channel/telegram/TelegramBotRequest.kt @@ -6,9 +6,9 @@ import com.github.kotlintelegrambot.entities.stickers.Sticker import com.justai.jaicf.api.BotRequest import com.justai.jaicf.api.EventBotRequest import com.justai.jaicf.api.QueryBotRequest -import com.justai.jaicf.gateway.BotGatewayEventRequest -import com.justai.jaicf.gateway.BotGatewayQueryRequest -import com.justai.jaicf.gateway.BotGatewayRequest +import com.justai.jaicf.channel.invocationapi.InvocationEventRequest +import com.justai.jaicf.channel.invocationapi.InvocationQueryRequest +import com.justai.jaicf.channel.invocationapi.InvocationRequest val BotRequest.telegram get() = this as? TelegramBotRequest @@ -25,7 +25,6 @@ val TelegramBotRequest.sticker get() = this as? TelegramStickerRequest val TelegramBotRequest.video get() = this as? TelegramVideoRequest val TelegramBotRequest.videoNote get() = this as? TelegramVideoNoteRequest val TelegramBotRequest.voice get() = this as? TelegramVoiceRequest -val TelegramBotRequest.gateway get() = this as? TelegramGatewayRequest internal val Message.clientId get() = chat.id.toString() @@ -98,26 +97,26 @@ data class TelegramVoiceRequest( val voice: Voice ): TelegramBotRequest, EventBotRequest(clientId = message.clientId, input = TelegramEvent.VOICE) -interface TelegramGatewayRequest : TelegramBotRequest, BotGatewayRequest { +interface TelegramInvocationRequest : TelegramBotRequest, InvocationRequest { companion object { - fun create(r: BotGatewayRequest, message: Message) = when (r) { - is BotGatewayEventRequest -> TelegramGatewayEventRequest(message, r.clientId, r.input, r.requestData) - is BotGatewayQueryRequest -> TelegramGatewayQueryRequest(message, r.clientId, r.input, r.requestData) + fun create(r: InvocationRequest, message: Message) = when (r) { + is InvocationEventRequest -> TelegramInvocationEventRequest(message, r.clientId, r.input, r.requestData) + is InvocationQueryRequest -> TelegramInvocationQueryRequest(message, r.clientId, r.input, r.requestData) else -> null } } } -data class TelegramGatewayEventRequest( +data class TelegramInvocationEventRequest( override val message: Message, override val clientId: String, override val input: String, override val requestData: String -) : TelegramGatewayRequest, BotGatewayEventRequest(clientId, input, requestData) +) : TelegramInvocationRequest, InvocationEventRequest(clientId, input, requestData) -data class TelegramGatewayQueryRequest( +data class TelegramInvocationQueryRequest( override val message: Message, override val clientId: String, override val input: String, override val requestData: String -) : TelegramGatewayRequest, BotGatewayQueryRequest(clientId, input, requestData) +) : TelegramInvocationRequest, InvocationQueryRequest(clientId, input, requestData) diff --git a/channels/telegram/src/main/kotlin/com/justai/jaicf/channel/telegram/TelegramChannel.kt b/channels/telegram/src/main/kotlin/com/justai/jaicf/channel/telegram/TelegramChannel.kt index db2f16a5..b67231c1 100644 --- a/channels/telegram/src/main/kotlin/com/justai/jaicf/channel/telegram/TelegramChannel.kt +++ b/channels/telegram/src/main/kotlin/com/justai/jaicf/channel/telegram/TelegramChannel.kt @@ -13,14 +13,15 @@ import com.justai.jaicf.channel.jaicp.JaicpCompatibleAsyncBotChannel import com.justai.jaicf.channel.jaicp.JaicpCompatibleAsyncChannelFactory import com.justai.jaicf.context.RequestContext import com.justai.jaicf.helpers.kotlin.PropertyWithBackingField -import com.justai.jaicf.gateway.BotGateway -import com.justai.jaicf.gateway.BotGatewayRequest +import com.justai.jaicf.channel.invocationapi.InvocableBotChannel +import com.justai.jaicf.channel.invocationapi.InvocationRequest +import java.util.concurrent.TimeUnit class TelegramChannel( override val botApi: BotApi, private val telegramBotToken: String, private val telegramApiUrl: String = "https://api.telegram.org/" -) : JaicpCompatibleAsyncBotChannel, BotGateway() { +) : JaicpCompatibleAsyncBotChannel, InvocableBotChannel { private val gson = GsonFactory.createForApiClient() @@ -99,10 +100,12 @@ class TelegramChannel( return null } - override fun processGatewayRequest(request: BotGatewayRequest, requestContext: RequestContext) { + override fun provideTimestamp(): String = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()).toString() + + override fun processExternalInvocation(request: InvocationRequest, requestContext: RequestContext) { val template = getRequestTemplateFromResources(request, REQUEST_TEMPLATE_PATH) val message = gson.fromJson(template, Update::class.java).message ?: return - val telegramRequest = TelegramGatewayRequest.create(request, message) ?: return + val telegramRequest = TelegramInvocationRequest.create(request, message) ?: return botApi.process(telegramRequest, TelegramReactions(bot, telegramRequest), requestContext) } diff --git a/channels/telegram/src/main/resources/TelegramRequestTemplate.json b/channels/telegram/src/main/resources/TelegramRequestTemplate.json index e80eb864..93bd2692 100644 --- a/channels/telegram/src/main/resources/TelegramRequestTemplate.json +++ b/channels/telegram/src/main/resources/TelegramRequestTemplate.json @@ -1,9 +1,9 @@ { - "update_id": 0, + "update_id": "{{ randomInt }}", "message": { - "message_id": 0, + "message_id": "{{ randomInt }}", "from": { - "id": 0, + "id": "{{ randomInt }}", "is_bot": false, "first_name": "First", "last_name": "Name", @@ -12,12 +12,12 @@ }, "chat": { "id": "{{ clientId }}", - "first_name": "Vladislav", - "last_name": "Metelyagin", - "username": "vmetelyagin", + "first_name": "InvocationAPI FirstName", + "last_name": "InvocationAPI LastName", + "username": "InvocationAPI Username", "type": "private" }, - "date": 1611064356, + "date": "{{ timestamp }}", "text": "{{ text }}" } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/justai/jaicf/channel/invocationapi/InvocableBotChannel.kt b/core/src/main/kotlin/com/justai/jaicf/channel/invocationapi/InvocableBotChannel.kt new file mode 100644 index 00000000..0eca9786 --- /dev/null +++ b/core/src/main/kotlin/com/justai/jaicf/channel/invocationapi/InvocableBotChannel.kt @@ -0,0 +1,81 @@ +package com.justai.jaicf.channel.invocationapi + +import com.justai.jaicf.channel.http.HttpBotRequest +import com.justai.jaicf.channel.http.asHttpBotRequest +import com.justai.jaicf.context.RequestContext +import com.justai.jaicf.helpers.logging.WithLogger +import java.util.* +import kotlin.random.Random + +/** + * Base class for all channels able to process requests from external service. + * Allows to send [InvocationEventRequest] or [InvocationQueryRequest] with client identifier to implementations at any time. + * + * @see InvocationRequest + * + * */ +interface InvocableBotChannel : WithLogger { + /** + * Processes an [InvocationRequest] + * + * @param request an [InvocationRequest] + * @param requestContext additional general request's data that can be used during the request processing + * + * @see InvocableBotChannel + */ + fun processExternalInvocation(request: InvocationRequest, requestContext: RequestContext) + + /** + * Provides a messageId for substitution in request template + * */ + fun provideMessageId(): String = UUID.randomUUID().toString() + + /** + * Provides a timestamp for substitution in request template + * */ + fun provideTimestamp(): String = System.currentTimeMillis().toString() + + /** + * Loads a channel request template from resources and substitutes essential parameters (clientId, input) in request + * + * @return serialized JSON with substituted request parameters + * */ + @Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + fun getRequestTemplateFromResources(request: InvocationRequest, resourceName: String) = + this.javaClass.getResource(resourceName).readText() + .replace("{{ clientId }}", request.clientId) + .replace("{{ text }}", request.input) + + .replace("\"{{ timestamp }}\"", provideTimestamp()) + .replace("{{ messageId }}", provideMessageId()) + + .replace("\"{{ randomInt }}\"", randomInt.toString()) + .replace("\"{{ randomLong }}\"", randomLong.toString()) + + .also { logger.trace("Generated template request: $it") } +} + +/** + * Processes request ktor routing extensions or servlet. + * + * @param queryParams + * @param requestData a stringified data sent with request. + * + * @see InvocationServlet + * @see botInvocationRouting + * */ +internal fun InvocableBotChannel.processExternalInvocation( + queryParams: InvocationQueryParams, + requestData: String +) = processExternalInvocation( + request = when (queryParams.type) { + InvocationRequestType.EVENT -> InvocationEventRequest(queryParams.clientId, queryParams.input, requestData) + InvocationRequestType.QUERY -> InvocationQueryRequest(queryParams.clientId, queryParams.input, requestData) + }, + requestContext = RequestContext.fromHttp(requestData.asHttpBotRequest()) +) + +private val randomInt get() = Random.nextInt() + +private val randomLong get() = Random.nextLong() + diff --git a/core/src/main/kotlin/com/justai/jaicf/channel/invocationapi/InvocationQueryParams.kt b/core/src/main/kotlin/com/justai/jaicf/channel/invocationapi/InvocationQueryParams.kt new file mode 100644 index 00000000..0240215c --- /dev/null +++ b/core/src/main/kotlin/com/justai/jaicf/channel/invocationapi/InvocationQueryParams.kt @@ -0,0 +1,45 @@ +package com.justai.jaicf.channel.invocationapi + +import io.ktor.request.* +import io.ktor.util.* +import javax.servlet.http.HttpServletRequest + + +/** + * @property clientId a recipient, chat or channel identifier from a concrete channel implementation. + * @property input query or event sent to invoke channel activation. + * @property type a [InvocationRequestType] of [InvocationRequest]. + * */ +internal class InvocationQueryParams(queryParamsMap: Map>) { + private val event: String? = queryParamsMap["event"]?.firstOrNull() + private val query: String? = queryParamsMap["query"]?.firstOrNull() + + constructor(request: ApplicationRequest) : this(request.queryParameters.toMap()) + + @Suppress("UNCHECKED_CAST") + constructor(request: HttpServletRequest) : this(request.parameterMap.map { (k, v) -> + (k as String) to (v as Array).toList() + }.toMap()) + + val type: InvocationRequestType = when { + event != null -> InvocationRequestType.EVENT + query != null -> InvocationRequestType.QUERY + else -> error("event or query must be specified in query parameters") + } + + val input = when (type) { + InvocationRequestType.EVENT -> requireNotNull(event) + InvocationRequestType.QUERY -> requireNotNull(query) + } + + val clientId: String = requireNotNull(queryParamsMap["clientId"]?.firstOrNull()) { + "clientId path variable must be specified for gateway call" + } +} + +/** + * Type of invocation request + * */ +internal enum class InvocationRequestType { + EVENT, QUERY; +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/justai/jaicf/gateway/BotGatewayRequest.kt b/core/src/main/kotlin/com/justai/jaicf/channel/invocationapi/InvocationRequest.kt similarity index 65% rename from core/src/main/kotlin/com/justai/jaicf/gateway/BotGatewayRequest.kt rename to core/src/main/kotlin/com/justai/jaicf/channel/invocationapi/InvocationRequest.kt index 12c3f38d..2be48cb2 100644 --- a/core/src/main/kotlin/com/justai/jaicf/gateway/BotGatewayRequest.kt +++ b/core/src/main/kotlin/com/justai/jaicf/channel/invocationapi/InvocationRequest.kt @@ -1,44 +1,44 @@ -package com.justai.jaicf.gateway +package com.justai.jaicf.channel.invocationapi import com.justai.jaicf.api.BotRequest import com.justai.jaicf.api.EventBotRequest import com.justai.jaicf.api.QueryBotRequest -val BotRequest.gateway get() = this as? BotGatewayRequest +val BotRequest.invocationApi get() = this as? InvocationRequest /** - * A request processed by [BotGateway] implementation. + * A request processed by [InvocableBotChannel] implementation. * * @property clientId inherited from [BotRequest] is a recipient or channel identifier from a concrete channel implementation. - * @property input inherited from [BotRequest] a input (text or event) sent via gateway. + * @property input inherited from [BotRequest] a input (text or event) sent via [InvocableBotChannel]. * @property requestData a stringified data sent with request. * */ -interface BotGatewayRequest : BotRequest { +interface InvocationRequest : BotRequest { val requestData: String } /** - * An [EventBotRequest] sent via [BotGateway]. + * An [EventBotRequest] sent via [InvocableBotChannel]. * * @property clientId inherited from [BotRequest] is a recipient or channel identifier from a concrete channel implementation. - * @property input inherited from [BotRequest] a input (text or event) sent via gateway. + * @property input inherited from [BotRequest] a input (text or event) sent via [InvocableBotChannel]. * @property requestData a stringified data sent with request. * */ -open class BotGatewayEventRequest( +open class InvocationEventRequest( override val clientId: String, override val input: String, override val requestData: String -) : BotGatewayRequest, EventBotRequest(clientId, input) +) : InvocationRequest, EventBotRequest(clientId, input) /** - * A [QueryBotRequest] sent via [BotGateway]. + * A [QueryBotRequest] sent via [InvocableBotChannel]. * * @property clientId inherited from [BotRequest] is a recipient or channel identifier from a concrete channel implementation. - * @property input inherited from [BotRequest] a input (text or event) sent via gateway. + * @property input inherited from [BotRequest] a input (text or event) sent via [InvocableBotChannel]. * @property requestData a stringified data sent with request. * */ -open class BotGatewayQueryRequest( +open class InvocationQueryRequest( override val clientId: String, override val input: String, override val requestData: String -) : BotGatewayRequest, QueryBotRequest(clientId, input) +) : InvocationRequest, QueryBotRequest(clientId, input) diff --git a/core/src/main/kotlin/com/justai/jaicf/channel/invocationapi/InvocationRouting.kt b/core/src/main/kotlin/com/justai/jaicf/channel/invocationapi/InvocationRouting.kt new file mode 100644 index 00000000..558d114b --- /dev/null +++ b/core/src/main/kotlin/com/justai/jaicf/channel/invocationapi/InvocationRouting.kt @@ -0,0 +1,48 @@ +package com.justai.jaicf.channel.invocationapi + +import io.ktor.application.* +import io.ktor.request.* +import io.ktor.routing.* + +typealias RouteToInvocableChannel = Pair + +/** + * A helper extensions for Ktor framework with [InvocableBotChannel] routing. + * + * Usage example: + * ``` + * embeddedServer(Netty, 8000) { + * routing { + * botInvocationRouting( + * "/invocation/telegram" to telegramChannel, + * "/invocation/facebook" to facebookChannel + * ) + * } + * }.start(wait = true) + * ``` + * + * example requests: + * curl -X POST {host}/invocation/telegram?clientId={clientId}&event=myEvent -d '{"key": "value"}' + * + * @see InvocableBotChannel + * @see InvocationRequest + * @see InvocationRequestType + */ + + +fun Routing.botInvocationRouting(vararg routes: RouteToInvocableChannel) { + routes.forEach { channel -> + post(channel.first) { + channel.second.processExternalInvocation(call) + } + + get(channel.first) { + channel.second.processExternalInvocation(call) + } + } +} + +private suspend fun InvocableBotChannel.processExternalInvocation(call: ApplicationCall) = + processExternalInvocation(queryParams = InvocationQueryParams(call.request), requestData = call.receiveText()) + + diff --git a/core/src/main/kotlin/com/justai/jaicf/channel/invocationapi/InvocationServlet.kt b/core/src/main/kotlin/com/justai/jaicf/channel/invocationapi/InvocationServlet.kt new file mode 100644 index 00000000..4bd4d395 --- /dev/null +++ b/core/src/main/kotlin/com/justai/jaicf/channel/invocationapi/InvocationServlet.kt @@ -0,0 +1,49 @@ +package com.justai.jaicf.channel.invocationapi + +import com.justai.jaicf.helpers.logging.WithLogger +import javax.servlet.http.HttpServlet +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +/** + * HttpServlet implementation that processes GET/POST requests to trigger request processing in [InvocableBotChannel]. + * + * A helper extensions for Ktor framework with [InvocableBotChannel] routing. + * + * Usage example (with spring boot): + * ``` + * @Configuration + * @ServletComponentScan + * class AppConfiguration { + * + * @WebServlet("/telegram") + * class MyInvocationServlet : InvocationServlet(telegramChannel) + * } + * ``` + * + * example requests: + * curl -X POST {host}/invocation/telegram?clientId={clientId}&event=myEvent -d '{"key": "value"}' + * + * @see InvocableBotChannel + * @see InvocationRequest + * @see InvocationRequestType + */ +@Suppress("unchecked_cast") +open class InvocationServlet( + private val channel: InvocableBotChannel +) : HttpServlet(), WithLogger { + + override fun doPost(req: HttpServletRequest?, resp: HttpServletResponse?) { + req?.run { channel.processExternalInvocation(req) } + } + + override fun doGet(req: HttpServletRequest?, resp: HttpServletResponse?) { + req?.run { channel.processExternalInvocation(req) } + } +} + +/** + * Processes invocation request from [InvocationServlet] + * */ +private fun InvocableBotChannel.processExternalInvocation(req: HttpServletRequest) = + processExternalInvocation(InvocationQueryParams(req), req.inputStream.bufferedReader().readText()) diff --git a/core/src/main/kotlin/com/justai/jaicf/gateway/BotGateway.kt b/core/src/main/kotlin/com/justai/jaicf/gateway/BotGateway.kt deleted file mode 100644 index 772e3bf5..00000000 --- a/core/src/main/kotlin/com/justai/jaicf/gateway/BotGateway.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.justai.jaicf.gateway - -import com.justai.jaicf.context.RequestContext - -/** - * Base class for all channels able to process requests from external service. - * Allows to send [BotGatewayEventRequest] or [BotGatewayQueryRequest] with client identifier to implementations at any time. - * - * @see BotGatewayRequest - * - * */ -abstract class BotGateway { - abstract fun processGatewayRequest(request: BotGatewayRequest, requestContext: RequestContext) - - @Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") - protected fun getRequestTemplateFromResources(request: BotGatewayRequest, resourceName: String) = - this.javaClass.getResource(resourceName).readText() - .replace("{{ clientId }}", request.clientId) - .replace("{{ text }}", request.input) -} diff --git a/core/src/main/kotlin/com/justai/jaicf/gateway/BotGatewayRouting.kt b/core/src/main/kotlin/com/justai/jaicf/gateway/BotGatewayRouting.kt deleted file mode 100644 index 291b678e..00000000 --- a/core/src/main/kotlin/com/justai/jaicf/gateway/BotGatewayRouting.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.justai.jaicf.gateway - -import com.justai.jaicf.channel.http.asHttpBotRequest -import com.justai.jaicf.context.RequestContext -import io.ktor.application.* -import io.ktor.request.* -import io.ktor.routing.* -import io.ktor.util.* - -typealias RouteToGatewayChannel = Pair - -/** - * A helper extensions for Ktor framework with [BotGateway] routing. - * - * Usage example: - * ``` - * embeddedServer(Netty, 8000) { - * routing { - * httpBotRouting( - * "/gw/telegram" to telegramChannel, - * "/gw/facebook" to facebookChannel - * ) - * } - * }.start(wait = true) - * ``` - * - * example requests: - * curl -X POST {host}/gw/telegram/{clientId}?event=gatewayEvent -d '{"key": "value"}' - * - * @see BotGateway - * @see BotGatewayRequest - * @see BotGatewayRequestType - */ -fun Routing.botGatewayRouting(vararg routes: RouteToGatewayChannel) { - routes.forEach { channel -> - post("${channel.first}/{clientId}") { - val clientId = requireNotNull(call.parameters["clientId"]) { - "clientId path variable must be specified for gateway call" - } - val requestData = call.receiveText() - val queryParameters = BotGatewayQueryParams(call.request.queryParameters.toMap()) - if (!queryParameters.isValid()) { - error("event or text must be specified in query parameters") - } - processGatewayRequest( - clientId = clientId, - input = queryParameters.getInput(), - type = queryParameters.getType(), - requestData = requestData, - bot = channel.second - ) - } - } -} - -/** - * Processes request from [botGatewayRouting]. - * - * @param clientId a recipient, chat or channel identifier from a concrete channel implementation. - * @param input text or event sent via gateway. - * @param type a [BotGatewayRequestType] of [BotGatewayRequest]. - * @param requestData a stringified data sent with request. - * @param bot a [BotGateway] implementation to process request. - * */ -private fun processGatewayRequest( - clientId: String, - input: String, - type: BotGatewayRequestType, - requestData: String, - bot: BotGateway -) = bot.processGatewayRequest( - request = when (type) { - BotGatewayRequestType.EVENT -> BotGatewayEventRequest(clientId, input, requestData) - BotGatewayRequestType.TEXT -> BotGatewayQueryRequest(clientId, input, requestData) - }, - requestContext = RequestContext.fromHttp(requestData.asHttpBotRequest()) -) - - -private class BotGatewayQueryParams(queryParamsMap: Map>) { - val event: String? = queryParamsMap["event"]?.firstOrNull() - val text: String? = queryParamsMap["text"]?.firstOrNull() - - fun isValid() = event != null || text != null - - fun getType(): BotGatewayRequestType { - if (event != null) return BotGatewayRequestType.EVENT - if (text != null) return BotGatewayRequestType.TEXT - error("event or text must be specified in query parameters") - } - - fun getInput() = when (getType()) { - BotGatewayRequestType.EVENT -> event!! - BotGatewayRequestType.TEXT -> text!! - } -} - -private enum class BotGatewayRequestType { - EVENT, TEXT; -} \ No newline at end of file