Skip to content

Commit

Permalink
introduce sink for MS Teams (#2248)
Browse files Browse the repository at this point in the history
* refactor: remove duplicate description for secret input

* feat: add text property with options for multiline and placeholders

* feat: implement sink for MS Teams

* style: address checkstyle violations

* fix test-suite

* adress review comments
  • Loading branch information
bossenti authored Dec 1, 2023
1 parent eaad05e commit 2d21904
Show file tree
Hide file tree
Showing 9 changed files with 502 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.apache.streampipes.extensions.api.migration.IModelMigrator;
import org.apache.streampipes.extensions.api.pe.IStreamPipesPipelineElement;
import org.apache.streampipes.sinks.notifications.jvm.email.EmailSink;
import org.apache.streampipes.sinks.notifications.jvm.msteams.MSTeamsSink;
import org.apache.streampipes.sinks.notifications.jvm.onesignal.OneSignalSink;
import org.apache.streampipes.sinks.notifications.jvm.slack.SlackNotificationSink;
import org.apache.streampipes.sinks.notifications.jvm.telegram.TelegramSink;
Expand All @@ -40,6 +41,7 @@ public List<StreamPipesAdapter> adapters() {
public List<IStreamPipesPipelineElement<?>> pipelineElements() {
return List.of(
new EmailSink(),
new MSTeamsSink(),
new OneSignalSink(),
new SlackNotificationSink(),
new TelegramSink()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.streampipes.sinks.notifications.jvm.msteams;

import org.apache.streampipes.commons.exceptions.SpRuntimeException;
import org.apache.streampipes.extensions.api.pe.context.EventSinkRuntimeContext;
import org.apache.streampipes.model.DataSinkType;
import org.apache.streampipes.model.graph.DataSinkDescription;
import org.apache.streampipes.model.runtime.Event;
import org.apache.streampipes.pe.shared.PlaceholderExtractor;
import org.apache.streampipes.sdk.StaticProperties;
import org.apache.streampipes.sdk.builder.DataSinkBuilder;
import org.apache.streampipes.sdk.builder.StreamRequirementsBuilder;
import org.apache.streampipes.sdk.helpers.Alternatives;
import org.apache.streampipes.sdk.helpers.EpRequirements;
import org.apache.streampipes.sdk.helpers.Labels;
import org.apache.streampipes.sdk.helpers.Locales;
import org.apache.streampipes.sdk.utils.Assets;
import org.apache.streampipes.wrapper.params.compat.SinkParams;
import org.apache.streampipes.wrapper.standalone.StreamPipesDataSink;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClients;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;

public class MSTeamsSink extends StreamPipesDataSink {

private static final String KEY_MESSAGE_ADVANCED = "messageAdvanced";
private static final String KEY_MESSAGE_ADVANCED_CONTENT = "messageContentAdvanced";
private static final String KEY_MESSAGE_SIMPLE = "messageSimple";
private static final String KEY_MESSAGE_SIMPLE_CONTENT = "messageContentSimple";
private static final String KEY_MESSAGE_TYPE_ALTERNATIVES = "messageType";
private static final String KEY_WEBHOOK_URL = "webhookUrl";
protected static final String SIMPLE_MESSAGE_TEMPLATE = "{\"text\": \"%s\"}";

private String messageContent;
private boolean isSimpleMessageMode;
private String webhookUrl;
private ObjectMapper objectMapper;

public MSTeamsSink(){
super();
this.objectMapper = new ObjectMapper();
}

@Override
public void onEvent(Event event) {

// This sink allows to use placeholders for event properties when defining the message content in the UI
// Therefore, we need to replace these placeholders based on the actual event before actually sending the message
var processedMessageContent = PlaceholderExtractor.replacePlaceholders(event, messageContent);

String teamsMessageContent;
if (isSimpleMessageMode) {
teamsMessageContent = createMessageFromSimpleContent(processedMessageContent);
} else {
teamsMessageContent = createMessageFromAdvancedContent(processedMessageContent);
}

sendPayloadToWebhook(HttpClients.createDefault(), teamsMessageContent, webhookUrl);
}

@Override
public DataSinkDescription declareModel() {
return DataSinkBuilder.create("org.apache.streampipes.sinks.notifications.jvm.msteams")
.withLocales(Locales.EN)
.withAssets(Assets.DOCUMENTATION, Assets.ICON)
.category(DataSinkType.NOTIFICATION)
.requiredStream(
StreamRequirementsBuilder
.create()
.requiredProperty(EpRequirements.anyProperty())
.build()
)
.requiredSecret(Labels.withId(KEY_WEBHOOK_URL))
.requiredAlternatives(
Labels.withId(KEY_MESSAGE_TYPE_ALTERNATIVES),
Alternatives.from(
Labels.withId(KEY_MESSAGE_SIMPLE),
StaticProperties.stringFreeTextProperty(
Labels.withId(KEY_MESSAGE_SIMPLE_CONTENT),
true,
true
),
true),
Alternatives.from(
Labels.withId(KEY_MESSAGE_ADVANCED),
StaticProperties.stringFreeTextProperty(
Labels.withId(KEY_MESSAGE_ADVANCED_CONTENT),
true,
true
)
)
)
.build();
}

@Override
public void onInvocation(
SinkParams parameters,
EventSinkRuntimeContext runtimeContext
) throws SpRuntimeException {
this.objectMapper = new ObjectMapper();

var extractor = parameters.extractor();
webhookUrl = extractor.secretValue(KEY_WEBHOOK_URL);

validateWebhookUrl(webhookUrl);

var selectedAlternative = extractor.selectedAlternativeInternalId(KEY_MESSAGE_TYPE_ALTERNATIVES);
if (selectedAlternative.equals(KEY_MESSAGE_ADVANCED)) {
isSimpleMessageMode = false;
messageContent = extractor.singleValueParameter(KEY_MESSAGE_ADVANCED_CONTENT, String.class);
} else {
isSimpleMessageMode = true;
messageContent = extractor.singleValueParameter(KEY_MESSAGE_SIMPLE_CONTENT, String.class);
}

}

@Override
public void onDetach() {
// nothing to do
}

/**
* Creates a JSON string intended for the MS Teams Webhook URL based on the provided plain message content.
* <p>
* This method utilizes a basic approach for constructing messages to be sent to MS Teams.
* If you intend to provide text in the form of Adaptive Cards, consider using
* {@link #createMessageFromAdvancedContent(String)} for a more advanced and interactive message format.
* </p>
*
* @param messageContent The plain message content to be included in the Teams message.
* @return A JSON string formatted using a predefined template with the provided message content.
*/
protected String createMessageFromSimpleContent(String messageContent) {
return SIMPLE_MESSAGE_TEMPLATE.formatted(messageContent);
}

/**
* Creates a message for MS Teams from a JSON string, specifically designed for use with Adaptive Cards.
* <p>
* This method takes a JSON string as input, which is expected to represent the content of the message.
* The content is directly forwarded to MS Teams, allowing for the utilization of Adaptive Cards.
* Adaptive Cards provide a flexible and interactive way to present content in Microsoft Teams.
* Learn more about Adaptive Cards: <a href="https://learn.microsoft.com/en-us/adaptive-cards/">here</a>
* </p>
*
* @param messageContent The JSON string representing the content of the message.
* @return The original JSON string, unchanged.
* @throws SpRuntimeException If the provided message is not a valid JSON string.
*/
protected String createMessageFromAdvancedContent(String messageContent) {
try {
objectMapper.readValue(messageContent, Object.class);
} catch (JsonProcessingException e) {
throw new SpRuntimeException(
"Advanced message content provided is not a valid JSON string: %s".formatted(messageContent),
e
);
}
return messageContent;
}

/**
* Sends a payload to a webhook using the provided HTTP client, payload, and webhook URL.
*
* @param httpClient The HTTP client used to send the payload.
* @param payload The payload to be sent to the webhook.
* @param webhookUrl The URL of the webhook to which the payload will be sent.
* @throws SpRuntimeException If an I/O error occurs while sending the payload to the webhook or
* the payload sent is not accepted by the API.
*/
protected void sendPayloadToWebhook(HttpClient httpClient, String payload, String webhookUrl) {
try {
var contentEntity = new StringEntity(payload);
contentEntity.setContentType(ContentType.APPLICATION_JSON.toString());

var postRequest = new HttpPost(webhookUrl);
postRequest.setEntity(contentEntity);

var result = httpClient.execute(postRequest);
if (result.getStatusLine().getStatusCode() == HttpStatus.SC_BAD_REQUEST) {
throw new SpRuntimeException(
"The provided message payload was not accepted by the MS Teams API: %s"
.formatted(payload)
);
}
} catch (IOException e) {
throw new SpRuntimeException("Sending notification to MS Teams failed.", e);
}
}

/**
* Validates a webhook URL to ensure it is not null, not empty, and has a valid URL format.
*
* @param webhookUrl The webhook URL to be validated.
* @throws SpRuntimeException If the webhook URL is null or empty, or if it is not a valid URL.
*/
protected void validateWebhookUrl(String webhookUrl) {
if (webhookUrl == null || webhookUrl.isEmpty()) {
throw new SpRuntimeException("Given webhook URL is empty");
}
try {
new URL(webhookUrl);
} catch (MalformedURLException e) {
throw new SpRuntimeException("The given webhook is not a valid URL: %s".formatted(webhookUrl));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You 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.
~
-->

# MS Teams Sink

<p align="center">
<img src="icon.png" width="150px;" class="pe-image-documentation"/>
</p>

---

## Description

The MS Teams Sink is a StreamPipes data sink that facilitates the sending of messages to a Microsoft Teams channel
through a Webhook URL. Whether you need to convey simple text messages or employ more advanced formatting with [Adaptive
Cards](https://adaptivecards.io/), this sink provides a versatile solution for integrating StreamPipes with Microsoft Teams.

---

## Required input

The MS Teams Sink does not have any specific requirements for incoming event types. It is designed to work seamlessly
with any type of incoming event, making it a versatile choice for various use cases.

---

## Configuration

#### Webhook URL

To configure the MS Teams Sink, you need to provide the Webhook URL that enables the sink to send messages to a specific
MS Teams channel. If you don't have a Webhook URL, you can learn how to create
one [here](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook?tabs=dotnet#create-incoming-webhooks-1).

#### Message Content Options

You can choose between two message content formats:

- **Simple Message Content:** Supports plain text and basic markdown formatting.
- **Advanced Message Content:** Expects JSON input directly forwarded to Teams without modification. This format is
highly customizable and can be used for Adaptive Cards.

Choose the format that best suits your messaging needs.

---

## Usage

#### Simple Message Format

In the simple message format, you can send plain text messages or utilize basic markdown formatting to convey
information. This is ideal for straightforward communication needs.

#### Advanced Message Format

For more sophisticated messaging requirements, the advanced message format allows you to send JSON content directly to
Microsoft Teams without modification. This feature is especially powerful when used
with [Adaptive Cards](https://learn.microsoft.com/en-us/adaptive-cards/), enabling interactive and dynamic content in
your Teams messages.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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.
#

org.apache.streampipes.sinks.notifications.jvm.msteams.title=MS Teams Sink
org.apache.streampipes.sinks.notifications.jvm.msteams.description=Facilitates the sending of messages to a Microsoft Teams channel through a Webhook URL.

messageType.title=Select the Message Type
messageType.description=Messages sent to MS Teams can be provided in a simple or an advanced format

messageContentSimple.title=Simple Message Format
messageContentSimple.description=Provide plain text messages or utilize basic markdown formatting

messageSimple.title=Simple Message Format

messageContentAdvanced.title=Advanced Message Format
messageContentAdvanced.description=This input mode allows you to freely define the message content sent to MS Teams. Therefore it is expected to be in JSON format. For more information about how messages are allowed to look like, please refer to https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL.

messageAdvanced.title=Advanced Message Format

webhookUrl.title=Webhook URL
webhookUrl.description=URL of the Webhook that allows to notifications to a dedicated channel in MS Teams.
Loading

0 comments on commit 2d21904

Please sign in to comment.