-
Notifications
You must be signed in to change notification settings - Fork 140
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Mattermost Connector and its documentation
- Loading branch information
Showing
16 changed files
with
623 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
# Tock Mattermost Connector | ||
|
||
This connector is quite basic and will allow us to manage several simple scenarios: | ||
|
||
- reply to all messages of a public channel with or without trigger word | ||
- reply on any public channel with a trigger word (_work in progress_) | ||
- reply in any channel (public or private) thanks to a slash command. | ||
|
||
## Install: | ||
|
||
On our Mattermost Workspace, we will add an incoming webhook and an outgoing webhook. | ||
|
||
We're going to consider that we have a self-hosted instance of mattermost on `https://mattermost.mydomain.com` and a Tock instance with Tock api listening on `https://tock-api.mydomain.com` | ||
|
||
### Configure Mattermost | ||
|
||
First, we should modify a configuration value to allow our Mattermost server to request an external address. To do so, we have to go in Mattermost to `System Console` > `environment` > `developer` and add the value `tock-api.mydomain.com` to the `Allow untrusted internal` value. | ||
|
||
### Incoming webhook | ||
|
||
The Incoming webhook will be used to receive messages from Tock. | ||
|
||
Go to the integration page in Mattermost and create a new Incoming Webhook. Set the Title and select a Channel. | ||
Once validated, Mattermost will give us the full URL of the webhook that will look at something like this : `https://mattermost.mydomain.com/hooks/7otffm545tfabgu6w8wnozrnuc` | ||
|
||
In Tock Studio, we will create a new configuration for Mattermost connector in `Settings` > `Configuration`. | ||
|
||
In the `Connector Custom Configuration` section we will set the : | ||
|
||
- `Mattermost Server Url` to : `https://mattermost.mydomain.com` | ||
- `Incoming webhook token` to : `7otffm545tfabgu6w8wnozrnuc` | ||
|
||
See https://developers.mattermost.com/integrate/webhooks/incoming/ for more info. | ||
|
||
### Outgoing webhook | ||
|
||
The outgoing webhook will be used to send a message from Mattermost to our Tock instance. | ||
|
||
Go to the integration page in Mattermost and create a new Outgoing Webhook. | ||
|
||
Set the title and select a channel, the same as the incoming webhook. In this case the `Trigger words` is optional and all messages will be sent to Tock. We could also set a trigger word to selectively send message to Tock. | ||
|
||
Finally set the callback URL in `Callback URLs`. The URL to set is given by the Tock connector configuration. It will be the URL of our Tock api associated with the `Relative REST path` in the connector configuration. e.g. : `https://tock-api.mydomain.com/io/app/new_assistant/mm1` | ||
|
||
Once validated, Mattermost will give us a token that we should set in the `Connector Custom Configuration` in the `Outgoing webhook token` of Tock. | ||
|
||
See https://developers.mattermost.com/integrate/webhooks/outgoing/ for more info. | ||
|
||
### Other option | ||
|
||
Instead of an outgoing webhook, we can configure a slash command inside Mattermost. The configuration is not really different. The token given should be set in the `Outgoing webhook token` of Tock. | ||
|
||
See https://developers.mattermost.com/integrate/slash-commands/custom/ for more info on custom slash commands. | ||
|
||
### todo | ||
|
||
- reply on the original channel | ||
- add unit tests | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!-- | ||
~ Copyright (C) 2017/2024 e-voyageurs technologies | ||
~ | ||
~ Licensed under the Apache License, Version 2.0 (the "License"); | ||
~ you may not use this file except in compliance with the License. | ||
~ You may obtain a copy of the License at | ||
~ | ||
~ http://www.apache.org/licenses/LICENSE-2.0 | ||
~ | ||
~ Unless required by applicable law or agreed to in writing, software | ||
~ distributed under the License is distributed on an "AS IS" BASIS, | ||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
~ See the License for the specific language governing permissions and | ||
~ limitations under the License. | ||
--> | ||
|
||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<parent> | ||
<groupId>ai.tock</groupId> | ||
<artifactId>tock-bot</artifactId> | ||
<version>24.9.9-SNAPSHOT</version> | ||
</parent> | ||
|
||
<artifactId>tock-bot-connector-mattermost</artifactId> | ||
<name>Tock Bot Mattermost Connector</name> | ||
<description>Bot Connector for Mattermost</description> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>ai.tock</groupId> | ||
<artifactId>tock-bot-engine-jackson</artifactId> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>org.jetbrains.kotlin</groupId> | ||
<artifactId>kotlin-test-junit5</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.junit.jupiter</groupId> | ||
<artifactId>junit-jupiter-engine</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.junit.jupiter</groupId> | ||
<artifactId>junit-jupiter-api</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.junit.jupiter</groupId> | ||
<artifactId>junit-jupiter-params</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.mockk</groupId> | ||
<artifactId>mockk-jvm</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
</dependencies> | ||
|
||
</project> |
29 changes: 29 additions & 0 deletions
29
bot/connector-mattermost/src/main/kotlin/MattermostBuilders.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
/* | ||
* Copyright (C) 2017/2024 e-voyageurs technologies | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package ai.tock.bot.connector.mattermost | ||
|
||
import ai.tock.bot.connector.ConnectorType | ||
|
||
/** | ||
* | ||
*/ | ||
internal const val MATTERMOST_CONNECTOR_TYPE_ID = "mattermost" | ||
|
||
/** | ||
* The Mattermost connector type. | ||
*/ | ||
val mattermostConnectorType = ConnectorType(MATTERMOST_CONNECTOR_TYPE_ID) |
58 changes: 58 additions & 0 deletions
58
bot/connector-mattermost/src/main/kotlin/MattermostClient.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
/* | ||
* Copyright (C) 2017/2024 e-voyageurs technologies | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package ai.tock.bot.connector.mattermost | ||
|
||
import ai.tock.bot.connector.mattermost.model.MattermostMessageOut | ||
import ai.tock.shared.addJacksonConverter | ||
import ai.tock.shared.create | ||
import ai.tock.shared.longProperty | ||
import ai.tock.shared.retrofitBuilderWithTimeoutAndLogger | ||
import mu.KotlinLogging | ||
import retrofit2.Call | ||
import retrofit2.http.Body | ||
import retrofit2.http.Headers | ||
import retrofit2.http.POST | ||
import retrofit2.http.Path | ||
|
||
/** | ||
* | ||
*/ | ||
internal class MattermostClient( | ||
mattermostUrl: String, | ||
private val mattermostToken: String, | ||
) { | ||
private interface MattermostApi { | ||
@Headers("Content-Type: application/json") | ||
@POST("hooks/{token}") | ||
fun sendMessage(@Path("token") token: String, @Body message: MattermostMessageOut): Call<Void> | ||
} | ||
|
||
private val logger = KotlinLogging.logger {} | ||
private val mattermostApi: MattermostApi = retrofitBuilderWithTimeoutAndLogger( | ||
longProperty("tock_mattermost_request_timeout_ms", 30000), | ||
logger | ||
) | ||
.baseUrl(mattermostUrl) | ||
.addJacksonConverter() | ||
.build() | ||
.create() | ||
|
||
fun sendMessage(message: MattermostMessageOut) { | ||
val response = mattermostApi.sendMessage(mattermostToken, message).execute() | ||
logger.debug { response } | ||
} | ||
} |
114 changes: 114 additions & 0 deletions
114
bot/connector-mattermost/src/main/kotlin/MattermostConnector.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
/* | ||
* Copyright (C) 2017/2024 e-voyageurs technologies | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package ai.tock.bot.connector.mattermost | ||
|
||
import ai.tock.bot.connector.ConnectorBase | ||
import ai.tock.bot.connector.ConnectorCallback | ||
import ai.tock.bot.connector.mattermost.model.MattermostMessageIn | ||
import ai.tock.bot.connector.mattermost.model.MattermostMessageOut | ||
import ai.tock.bot.engine.BotRepository | ||
import ai.tock.bot.engine.ConnectorController | ||
import ai.tock.bot.engine.action.SendSentence | ||
import ai.tock.bot.engine.event.Event | ||
import ai.tock.bot.engine.monitoring.logError | ||
import ai.tock.shared.Executor | ||
import ai.tock.shared.error | ||
import ai.tock.shared.injector | ||
import ai.tock.shared.jackson.mapper | ||
import com.fasterxml.jackson.module.kotlin.readValue | ||
import com.github.salomonbrys.kodein.instance | ||
import io.vertx.core.json.Json | ||
import io.vertx.core.json.JsonObject | ||
import mu.KotlinLogging | ||
import java.time.Duration | ||
|
||
class MattermostConnector( | ||
private val applicationId: String, | ||
private val path: String, | ||
private val url: String, | ||
private val token: String, | ||
private val channelId: String? = null, | ||
private val outgoingToken: String | ||
) : ConnectorBase(mattermostConnectorType) { | ||
|
||
companion object { | ||
private val logger = KotlinLogging.logger {} | ||
} | ||
|
||
private val client: MattermostClient = MattermostClient(url, token) | ||
private val executor: Executor by injector.instance() | ||
|
||
override fun register(controller: ConnectorController) { | ||
controller.registerServices(path) { router -> | ||
logger.info { "Register mattermost service $path" } | ||
router.post(path).handler { context -> | ||
logger.debug { "Handle mattermost request ${context.body().asString()}" } | ||
// see https://developers.mattermost.com/integrate/webhooks/outgoing/ | ||
val requestTimerData = BotRepository.requestTimer.start("mattermost_webhook") | ||
try { | ||
val body = when(context.request().getHeader("Content-Type")) { | ||
"application/x-www-form-urlencoded" -> { | ||
val metadata: JsonObject = JsonObject() | ||
for ((key, value) in context.request().formAttributes().entries()) { | ||
metadata.put(key, value) | ||
} | ||
Json.encode(metadata) | ||
} | ||
// else consider application/json | ||
else -> context.body().asString() | ||
} | ||
|
||
val message: MattermostMessageIn = mapper.readValue(body) | ||
|
||
if (message.token != outgoingToken) { | ||
logger.error { "Failed to validate token : ${message.token}" } | ||
context.fail(400) | ||
} | ||
|
||
try { | ||
val event = MattermostRequestConverter.toEvent(message, applicationId) | ||
controller.handle(event) | ||
} catch (e: Throwable) { | ||
logger.logError(e, requestTimerData) | ||
} | ||
} catch (e: Exception) { | ||
logger.logError(e, requestTimerData) | ||
} finally { | ||
try { | ||
BotRepository.requestTimer.end(requestTimerData) | ||
context.response().end() | ||
} catch (e: Throwable) { | ||
logger.error(e) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
override fun send(event: Event, callback: ConnectorCallback, delayInMs: Long) { | ||
if (event is SendSentence && event.text != null) { | ||
sendMessage(event.stringText!!, delayInMs) | ||
} | ||
} | ||
|
||
private fun sendMessage(message: String, delayInMs: Long) { | ||
executor.executeBlocking(Duration.ofMillis(delayInMs)) { | ||
val mattermostMessage = MattermostMessageOut(message, channelId) | ||
client.sendMessage(mattermostMessage) | ||
} | ||
} | ||
} |
Oops, something went wrong.