Skip to content

Commit

Permalink
Add Mattermost Connector and its documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
bibich committed Mar 11, 2025
1 parent faede7a commit 1e856dc
Show file tree
Hide file tree
Showing 16 changed files with 623 additions and 3 deletions.
59 changes: 59 additions & 0 deletions bot/connector-mattermost/README.md
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

64 changes: 64 additions & 0 deletions bot/connector-mattermost/pom.xml
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 bot/connector-mattermost/src/main/kotlin/MattermostBuilders.kt
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 bot/connector-mattermost/src/main/kotlin/MattermostClient.kt
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 bot/connector-mattermost/src/main/kotlin/MattermostConnector.kt
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)
}
}
}
Loading

0 comments on commit 1e856dc

Please sign in to comment.