Browse Source

Notification center: migrate from Office 365 Connectors to Microsoft Teams Workflows (#11583)

* PROD-4064: Migrate from Office 365 Connectors to Microsoft Teams Workflows. Replace MessageCard by AdaptiveCard

* PROD-4064: Migrate from Office 365 Connectors to Microsoft Teams Workflows. Replace MessageCard by AdaptiveCard

* PROD-4064: Migrate from Office 365 Connectors to Microsoft Teams Workflows. Replace MessageCard by AdaptiveCard

* PROD-4064: Migrate from Office 365 Connectors to Microsoft Teams Workflows. Replace MessageCard by AdaptiveCard

* PROD-4064: Migrate from Office 365 Connectors to Microsoft Teams Workflows. Replace MessageCard by AdaptiveCard

* PROD-4064: Migrate from Office 365 Connectors to Microsoft Teams Workflows. Replace MessageCard by AdaptiveCard

* PROD-4064: Migrate from Office 365 Connectors to Microsoft Teams Workflows. Replace MessageCard by AdaptiveCard

* UI: Change configuration Microsoft Team recipient config

* UI: Add null check in Microsoft Team recipient config

* PROD-4064: Migrate from Office 365 Connectors to Microsoft Teams Workflows. Replace MessageCard by AdaptiveCard

* PROD-4064: Migrate from Office 365 Connectors to Microsoft Teams Workflows. Replace MessageCard by AdaptiveCard

* PROD-4064: Migrate from Office 365 Connectors to Microsoft Teams Workflows. Replace MessageCard by AdaptiveCard

* PROD-4064: Migrate from Office 365 Connectors to Microsoft Teams Workflows. Replace MessageCard by AdaptiveCard

* PROD-4064: Migrate from Office 365 Connectors to Microsoft Teams Workflows. Replace MessageCard by AdaptiveCard

* PROD-4064: Migrate from Office 365 Connectors to Microsoft Teams Workflows. Replace MessageCard by AdaptiveCard

* PROD-4064: Migrate from Office 365 Connectors to Microsoft Teams Workflows. Replace MessageCard by AdaptiveCard

* Resolved PR comments

* Resolved PR comments

---------

Co-authored-by: sskoryi <sskoryi@thingsboard.io>
Co-authored-by: Vladyslav_Prykhodko <vprykhodko@thingsboard.io>
pull/11644/head
Serhii Skoryi 2 years ago
committed by GitHub
parent
commit
29d8abcc1c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 179
      application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java
  2. 93
      application/src/main/java/org/thingsboard/server/service/notification/channels/TeamsAdaptiveCard.java
  3. 92
      application/src/main/java/org/thingsboard/server/service/notification/channels/TeamsMessageCard.java
  4. 86
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  5. 1
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java
  6. 90
      dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java
  7. 13
      ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html
  8. 8
      ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.ts
  9. 1
      ui-ngx/src/app/shared/models/notification.models.ts
  10. 2
      ui-ngx/src/assets/locale/locale.constant-en_US.json

179
application/src/main/java/org/thingsboard/server/service/notification/channels/MicrosoftTeamsNotificationChannel.java

@ -15,16 +15,17 @@
*/
package org.thingsboard.server.service.notification.channels;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.common.util.JacksonUtil;
@ -37,6 +38,8 @@ import org.thingsboard.server.common.data.notification.template.MicrosoftTeamsDe
import org.thingsboard.server.service.notification.NotificationProcessingContext;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.List;
@ -56,17 +59,93 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel<Mi
@Override
public void sendNotification(MicrosoftTeamsNotificationTargetConfig targetConfig, MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception {
Message message = new Message();
message.setThemeColor(Strings.emptyToNull(processedTemplate.getThemeColor()));
if (targetConfig.getUseOldApi() == null || Boolean.TRUE.equals(targetConfig.getUseOldApi())) {
sendTeamsMessageCard(targetConfig, processedTemplate, ctx);
} else {
sendTeamsAdaptiveCard(targetConfig, processedTemplate, ctx);
}
}
private void sendTeamsAdaptiveCard(MicrosoftTeamsNotificationTargetConfig targetConfig, MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws URISyntaxException, JsonProcessingException {
TeamsAdaptiveCard teamsAdaptiveCard = new TeamsAdaptiveCard();
TeamsAdaptiveCard.Attachment attachment = new TeamsAdaptiveCard.Attachment();
teamsAdaptiveCard.setAttachments(List.of(attachment));
TeamsAdaptiveCard.AdaptiveCard adaptiveCard = new TeamsAdaptiveCard.AdaptiveCard();
attachment.setContent(adaptiveCard);
TeamsAdaptiveCard.BackgroundImage backgroundImage = new TeamsAdaptiveCard.BackgroundImage(processedTemplate.getThemeColor());
adaptiveCard.setBackgroundImage(backgroundImage);
if (StringUtils.isEmpty(processedTemplate.getSubject())) {
message.setText(processedTemplate.getBody());
TeamsAdaptiveCard.TextBlock textBlock = new TeamsAdaptiveCard.TextBlock();
textBlock.setText(processedTemplate.getBody());
textBlock.setWeight("Normal");
textBlock.setSize("Medium");
textBlock.setColor(processedTemplate.getThemeColor());
adaptiveCard.getTextBlocks().add(textBlock);
} else {
message.setSummary(processedTemplate.getSubject());
Message.Section section = new Message.Section();
TeamsAdaptiveCard.TextBlock subjectTextBlock = new TeamsAdaptiveCard.TextBlock();
subjectTextBlock.setText(processedTemplate.getSubject());
subjectTextBlock.setWeight("Bolder");
subjectTextBlock.setSize("Large");
subjectTextBlock.setColor(processedTemplate.getThemeColor());
adaptiveCard.getTextBlocks().add(subjectTextBlock);
TeamsAdaptiveCard.TextBlock bodyTextBlock = new TeamsAdaptiveCard.TextBlock();
bodyTextBlock.setText(processedTemplate.getBody());
bodyTextBlock.setWeight("Lighter");
bodyTextBlock.setSize("Medium");
bodyTextBlock.setColor(processedTemplate.getThemeColor());
adaptiveCard.getTextBlocks().add(bodyTextBlock);
}
String uri = getButtonUri(processedTemplate, ctx);
if (StringUtils.isNotBlank(uri) && processedTemplate.getButton().getText() != null) {
TeamsAdaptiveCard.ActionOpenUrl actionOpenUrl = new TeamsAdaptiveCard.ActionOpenUrl();
actionOpenUrl.setTitle(processedTemplate.getButton().getText());
actionOpenUrl.setUrl(uri);
adaptiveCard.getActions().add(actionOpenUrl);
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> request = new HttpEntity<>(JacksonUtil.toString(teamsAdaptiveCard), headers);
restTemplate.postForEntity(new URI(targetConfig.getWebhookUrl()), request, String.class);
}
private void sendTeamsMessageCard(MicrosoftTeamsNotificationTargetConfig targetConfig, MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws JsonProcessingException, URISyntaxException {
TeamsMessageCard teamsMessageCard = new TeamsMessageCard();
teamsMessageCard.setThemeColor(Strings.emptyToNull(processedTemplate.getThemeColor()));
if (StringUtils.isEmpty(processedTemplate.getSubject())) {
teamsMessageCard.setText(processedTemplate.getBody());
} else {
teamsMessageCard.setSummary(processedTemplate.getSubject());
TeamsMessageCard.Section section = new TeamsMessageCard.Section();
section.setActivityTitle(processedTemplate.getSubject());
section.setActivitySubtitle(processedTemplate.getBody());
message.setSections(List.of(section));
teamsMessageCard.setSections(List.of(section));
}
var button = processedTemplate.getButton();
String uri = getButtonUri(processedTemplate, ctx);
if (StringUtils.isNotBlank(uri) && button.getText() != null) {
TeamsMessageCard.ActionCard actionCard = new TeamsMessageCard.ActionCard();
actionCard.setType("OpenUri");
actionCard.setName(button.getText());
var target = new TeamsMessageCard.ActionCard.Target("default", uri);
actionCard.setTargets(List.of(target));
teamsMessageCard.setPotentialAction(List.of(actionCard));
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> request = new HttpEntity<>(JacksonUtil.toString(teamsMessageCard), headers);
restTemplate.postForEntity(new URI(targetConfig.getWebhookUrl()), request, String.class);
}
private String getButtonUri(MicrosoftTeamsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws JsonProcessingException {
var button = processedTemplate.getButton();
if (button != null && button.isEnabled()) {
String uri;
@ -99,17 +178,9 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel<Mi
} else {
uri = button.getLink();
}
if (StringUtils.isNotBlank(uri) && button.getText() != null) {
Message.ActionCard actionCard = new Message.ActionCard();
actionCard.setType("OpenUri");
actionCard.setName(button.getText());
var target = new Message.ActionCard.Target("default", uri);
actionCard.setTargets(List.of(target));
message.setPotentialAction(List.of(actionCard));
}
return uri;
}
restTemplate.postForEntity(targetConfig.getWebhookUrl(), message, String.class);
return null;
}
@Override
@ -121,74 +192,4 @@ public class MicrosoftTeamsNotificationChannel implements NotificationChannel<Mi
return NotificationDeliveryMethod.MICROSOFT_TEAMS;
}
@Data
public static class Message {
@JsonProperty("@type")
private final String type = "MessageCard";
@JsonProperty("@context")
private final String context = "http://schema.org/extensions";
private String themeColor;
private String summary;
private String text;
private List<Section> sections;
private List<ActionCard> potentialAction;
@Data
public static class Section {
private String activityTitle;
private String activitySubtitle;
private String activityImage;
private List<Fact> facts;
private boolean markdown;
@Data
public static class Fact {
private final String name;
private final String value;
}
}
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class ActionCard {
@JsonProperty("@type")
private String type; // ActionCard, OpenUri
private String name;
private List<Input> inputs; // for ActionCard
private List<Action> actions; // for ActionCard
private List<Target> targets;
@Data
public static class Input {
@JsonProperty("@type")
private String type; // TextInput, DateInput, MultichoiceInput
private String id;
private boolean isMultiple;
private String title;
private boolean isMultiSelect;
@Data
public static class Choice {
private final String display;
private final String value;
}
}
@Data
public static class Action {
@JsonProperty("@type")
private final String type; // HttpPOST
private final String name;
private final String target; // url
}
@Data
public static class Target {
private final String os;
private final String uri;
}
}
}
}

93
application/src/main/java/org/thingsboard/server/service/notification/channels/TeamsAdaptiveCard.java

@ -0,0 +1,93 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* 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 org.thingsboard.server.service.notification.channels;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.dao.util.ImageUtils;
import java.util.ArrayList;
import java.util.List;
/**
* @link <a href="https://adaptivecards.io/designer/">AdaptiveCard Designer</a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TeamsAdaptiveCard {
private String type = "message";
private List<Attachment> attachments;
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Attachment {
private String contentType = "application/vnd.microsoft.card.adaptive";
private AdaptiveCard content;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class AdaptiveCard {
@JsonProperty("$schema")
private final String schema = "http://adaptivecards.io/schemas/adaptive-card.json";
private final String type = "AdaptiveCard";
private BackgroundImage backgroundImage;
@JsonProperty("body")
private List<TextBlock> textBlocks = new ArrayList<>();
private List<ActionOpenUrl> actions = new ArrayList<>();
}
@Data
@NoArgsConstructor
public static class BackgroundImage {
private String url;
private final String fillMode = "repeat";
public BackgroundImage(String color) {
// This is the only one way how to specify color the custom color for the card
url = ImageUtils.getEmbeddedBase64EncodedImg(color);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class TextBlock {
private final String type = "TextBlock";
private String text;
private String weight = "Normal";
private String size = "Medium";
private String spacing = "None";
private String color = "#FFFFFF";
private final boolean wrap = true;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class ActionOpenUrl {
private final String type = "Action.OpenUrl";
private String title;
private String url;
}
}

92
application/src/main/java/org/thingsboard/server/service/notification/channels/TeamsMessageCard.java

@ -0,0 +1,92 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* 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 org.thingsboard.server.service.notification.channels;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
public class TeamsMessageCard {
@JsonProperty("@type")
private final String type = "MessageCard";
@JsonProperty("@context")
private final String context = "http://schema.org/extensions";
private String themeColor;
private String summary;
private String text;
private List<Section> sections;
private List<ActionCard> potentialAction;
@Data
public static class Section {
private String activityTitle;
private String activitySubtitle;
private String activityImage;
private List<Fact> facts;
private boolean markdown;
@Data
public static class Fact {
private final String name;
private final String value;
}
}
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class ActionCard {
@JsonProperty("@type")
private String type; // ActionCard, OpenUri
private String name;
private List<Input> inputs; // for ActionCard
private List<Action> actions; // for ActionCard
private List<Target> targets;
@Data
public static class Input {
@JsonProperty("@type")
private String type; // TextInput, DateInput, MultichoiceInput
private String id;
private boolean isMultiple;
private String title;
private boolean isMultiSelect;
@Data
public static class Choice {
private final String display;
private final String value;
}
}
@Data
public static class Action {
@JsonProperty("@type")
private final String type; // HttpPOST
private final String name;
private final String target; // url
}
@Data
public static class Target {
private final String os;
private final String uri;
}
}
}

86
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java

@ -25,6 +25,7 @@ import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpEntity;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.web.client.RestTemplate;
import org.thingsboard.common.util.JacksonUtil;
@ -85,8 +86,12 @@ import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.notification.DefaultNotifications;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.notification.channels.MicrosoftTeamsNotificationChannel;
import org.thingsboard.server.service.notification.channels.TeamsAdaptiveCard;
import org.thingsboard.server.service.notification.channels.TeamsMessageCard;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -752,7 +757,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
}
@Test
public void testMicrosoftTeamsNotifications() throws Exception {
public void testMicrosoftTeamsNotificationsWithOfficeConnector() throws URISyntaxException {
RestTemplate restTemplate = mock(RestTemplate.class);
microsoftTeamsNotificationChannel.setRestTemplate(restTemplate);
@ -760,6 +765,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
var targetConfig = new MicrosoftTeamsNotificationTargetConfig();
targetConfig.setWebhookUrl(webhookUrl);
targetConfig.setChannelName("My channel");
targetConfig.setUseOldApi(true);
NotificationTarget target = new NotificationTarget();
target.setName("Microsoft Teams channel");
target.setConfiguration(targetConfig);
@ -770,7 +776,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
String templateParams = "${recipientTitle} - ${entityType}";
template.setSubject("Subject: " + templateParams);
template.setBody("Body: " + templateParams);
template.setThemeColor("ff0000");
template.setThemeColor("#ff0000");
var button = new MicrosoftTeamsDeliveryMethodNotificationTemplate.Button();
button.setEnabled(true);
button.setText("Button: " + templateParams);
@ -803,11 +809,13 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
assertThat(preview.getRecipientsCountByTarget().get(target.getName())).isEqualTo(1);
assertThat(preview.getRecipientsPreview()).containsOnly(targetConfig.getChannelName());
var messageCaptor = ArgumentCaptor.forClass(MicrosoftTeamsNotificationChannel.Message.class);
ArgumentCaptor<HttpEntity<String>> messageCaptor = ArgumentCaptor.forClass(HttpEntity.class);
notificationCenter.processNotificationRequest(tenantId, notificationRequest, null);
verify(restTemplate, timeout(20000)).postForEntity(eq(webhookUrl), messageCaptor.capture(), any());
verify(restTemplate, timeout(20000)).postForEntity(eq(new URI(webhookUrl)), messageCaptor.capture(), any());
HttpEntity<String> value = messageCaptor.getValue();
TeamsMessageCard message = JacksonUtil.fromString(value.getBody(), TeamsMessageCard.class);
var message = messageCaptor.getValue();
String expectedParams = "My channel - Device";
assertThat(message.getThemeColor()).isEqualTo(template.getThemeColor());
assertThat(message.getSections().get(0).getActivityTitle()).isEqualTo("Subject: " + expectedParams);
@ -816,6 +824,74 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
assertThat(message.getPotentialAction().get(0).getTargets().get(0).getUri()).isEqualTo("https://" + expectedParams);
}
@Test
public void testMicrosoftTeamsNotificationsWithWorkflow() throws Exception {
RestTemplate restTemplate = mock(RestTemplate.class);
microsoftTeamsNotificationChannel.setRestTemplate(restTemplate);
String webhookUrl = "https://webhook.com/webhookb2/9628fa60-d873-11ed-913c-a196b1f9b445";
var targetConfig = new MicrosoftTeamsNotificationTargetConfig();
targetConfig.setWebhookUrl(webhookUrl);
targetConfig.setChannelName("My channel");
targetConfig.setUseOldApi(false);
NotificationTarget target = new NotificationTarget();
target.setName("Microsoft Teams channel");
target.setConfiguration(targetConfig);
target = saveNotificationTarget(target);
var template = new MicrosoftTeamsDeliveryMethodNotificationTemplate();
template.setEnabled(true);
String templateParams = "${recipientTitle} - ${entityType}";
template.setSubject("Subject: " + templateParams);
template.setBody("Body: " + templateParams);
template.setThemeColor("#ff0000");
var button = new MicrosoftTeamsDeliveryMethodNotificationTemplate.Button();
button.setEnabled(true);
button.setText("Button: " + templateParams);
button.setLinkType(LinkType.LINK);
button.setLink("https://" + templateParams);
template.setButton(button);
NotificationTemplate notificationTemplate = new NotificationTemplate();
notificationTemplate.setName("Notification to Teams");
notificationTemplate.setNotificationType(NotificationType.GENERAL);
NotificationTemplateConfig templateConfig = new NotificationTemplateConfig();
templateConfig.setDeliveryMethodsTemplates(Map.of(
NotificationDeliveryMethod.MICROSOFT_TEAMS, template
));
notificationTemplate.setConfiguration(templateConfig);
notificationTemplate = saveNotificationTemplate(notificationTemplate);
NotificationRequest notificationRequest = NotificationRequest.builder()
.tenantId(tenantId)
.originatorEntityId(tenantAdminUserId)
.templateId(notificationTemplate.getId())
.targets(List.of(target.getUuidId()))
.info(EntityActionNotificationInfo.builder()
.entityId(new DeviceId(UUID.randomUUID()))
.actionType(ActionType.ADDED)
.userId(tenantAdminUserId.getId())
.build())
.build();
NotificationRequestPreview preview = doPost("/api/notification/request/preview", notificationRequest, NotificationRequestPreview.class);
assertThat(preview.getRecipientsCountByTarget().get(target.getName())).isEqualTo(1);
assertThat(preview.getRecipientsPreview()).containsOnly(targetConfig.getChannelName());
ArgumentCaptor<HttpEntity<String>> messageCaptor = ArgumentCaptor.forClass(HttpEntity.class);
notificationCenter.processNotificationRequest(tenantId, notificationRequest, null);
verify(restTemplate, timeout(20000)).postForEntity(eq(new URI(webhookUrl)), messageCaptor.capture(), any());
HttpEntity<String> value = messageCaptor.getValue();
TeamsAdaptiveCard message = JacksonUtil.fromString(value.getBody(), TeamsAdaptiveCard.class);
String expectedParams = "My channel - Device";
assertThat(message).isNotNull();
assertThat(message.getAttachments().get(0).getContent().getBackgroundImage().getUrl()).isNotEmpty();
assertThat(message.getAttachments().get(0).getContent().getTextBlocks().get(0).getText()).isEqualTo("Subject: " + expectedParams);
assertThat(message.getAttachments().get(0).getContent().getTextBlocks().get(1).getText()).isEqualTo("Body: " + expectedParams);
assertThat(message.getAttachments().get(0).getContent().getActions().get(0).getTitle()).isEqualTo("Button: " + expectedParams);
assertThat(message.getAttachments().get(0).getContent().getActions().get(0).getUrl()).isEqualTo("https://" + expectedParams);
}
@Test
public void testMobileAppNotifications() throws Exception {
loginCustomerUser();

1
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/MicrosoftTeamsNotificationTargetConfig.java

@ -28,6 +28,7 @@ public class MicrosoftTeamsNotificationTargetConfig extends NotificationTargetCo
private String webhookUrl;
@NotEmpty
private String channelName;
private Boolean useOldApi = Boolean.TRUE;
@Override
public NotificationTargetType getType() {

90
dao/src/main/java/org/thingsboard/server/dao/util/ImageUtils.java

@ -46,9 +46,11 @@ import org.thingsboard.server.common.data.StringUtils;
import org.w3c.dom.Document;
import javax.imageio.ImageIO;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Base64;
import java.util.Map;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@ -265,6 +267,94 @@ public class ImageUtils {
return new int[]{thumbnailWidth, thumbnailHeight};
}
public static String getEmbeddedBase64EncodedImg(String colorStr) {
try {
Color color = parseColor(colorStr); // Support for hex, rgb, hsla
BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
image.setRGB(0, 0, color.getRGB());
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, "png", outputStream);
byte[] imageBytes = outputStream.toByteArray();
String base64String = Base64.getEncoder().encodeToString(imageBytes);
return "data:image/png;base64," + base64String;
} catch (Exception e) {
log.warn("Failed to generate embedded image for color: {}", colorStr, e);
return null;
}
}
private static Color parseColor(String colorStr) {
if (colorStr.startsWith("#")) {
return Color.decode(colorStr);
}
if (colorStr.startsWith("rgb")) {
return parseRgbColor(colorStr);
}
if (colorStr.startsWith("hsl")) {
return parseHslaColor(colorStr);
}
throw new IllegalArgumentException("Unsupported color format: " + colorStr);
}
private static Color parseRgbColor(String rgb) {
String[] rgbValues = rgb.replaceAll("[^0-9,]", "").split(",");
int r = Integer.parseInt(rgbValues[0]);
int g = Integer.parseInt(rgbValues[1]);
int b = Integer.parseInt(rgbValues[2]);
return new Color(r, g, b);
}
private static Color parseHslaColor(String hsla) {
String[] hslaValues = hsla.replaceAll("[^0-9.,]", "").split(",");
float h = Float.parseFloat(hslaValues[0]);
float s = Float.parseFloat(hslaValues[1]) / 100;
float l = Float.parseFloat(hslaValues[2]) / 100;
float a = hslaValues.length > 3 ? Float.parseFloat(hslaValues[3]) : 1.0f;
return hslaToColor(h, s, l, a);
}
private static Color hslaToColor(float h, float s, float l, float alpha) {
float c = (1 - Math.abs(2 * l - 1)) * s;
float x = c * (1 - Math.abs((h / 60) % 2 - 1));
float m = l - c / 2;
float r = 0, g = 0, b = 0;
if (h < 60) {
r = c;
g = x;
} else if (h < 120) {
r = x;
g = c;
} else if (h < 180) {
g = c;
b = x;
} else if (h < 240) {
g = x;
b = c;
} else if (h < 300) {
r = x;
b = c;
} else {
r = c;
b = x;
}
r += m;
g += m;
b += m;
return new Color(clamp(r), clamp(g), clamp(b), clamp(alpha));
}
private static float clamp(float value) {
return Math.max(0, Math.min(1, value));
}
@Data
@AllArgsConstructor
@NoArgsConstructor

13
ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html

@ -121,6 +121,19 @@
</tb-slack-conversation-autocomplete>
</section>
<section *ngIf="targetNotificationForm.get('configuration.type').value === notificationTargetType.MICROSOFT_TEAMS">
<section class="tb-form-row" style="margin-bottom: 16px">
<mat-slide-toggle class="mat-slide margin" formControlName="useOldApi">
{{ "notification.use-old-api" | translate }}
</mat-slide-toggle>
<a mat-icon-button
href="https://devblogs.microsoft.com/microsoft365dev/retirement-of-office-365-connectors-within-microsoft-teams/"
target="_blank"
matTooltip="{{ 'notification.use-deprecated-webhook-connectors' | translate }}"
matTooltipPosition="above"
class="tb-mat-20">
<mat-icon>open_in_new</mat-icon>
</a>
</section>
<mat-form-field class="mat-block">
<mat-label translate>notification.webhook-url</mat-label>
<input matInput formControlName="webhookUrl">

8
ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.ts

@ -32,7 +32,7 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { NotificationService } from '@core/http/notification.service';
import { EntityType } from '@shared/models/entity-type.models';
import { deepTrim, isDefinedAndNotNull } from '@core/utils';
import { deepTrim, isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Authority } from '@shared/models/authority.enum';
@ -100,6 +100,7 @@ export class RecipientNotificationDialogComponent extends
conversation: [{value: '', disabled: true}, Validators.required],
webhookUrl: [{value: '', disabled: true}, Validators.required],
channelName: [{value: '', disabled: true}, Validators.required],
useOldApi: [{value: !this.isAdd, disabled: true}],
description: [null]
})
});
@ -120,6 +121,7 @@ export class RecipientNotificationDialogComponent extends
case NotificationTargetType.MICROSOFT_TEAMS:
this.targetNotificationForm.get('configuration.webhookUrl').enable({emitEvent: false});
this.targetNotificationForm.get('configuration.channelName').enable({emitEvent: false});
this.targetNotificationForm.get('configuration.useOldApi').enable({emitEvent: false});
break;
}
this.targetNotificationForm.get('configuration.type').enable({emitEvent: false});
@ -169,6 +171,10 @@ export class RecipientNotificationDialogComponent extends
this.targetNotificationForm.get('configuration.usersFilter.filterByTenants')
.patchValue(!Array.isArray(this.data.target.configuration.usersFilter.tenantProfilesIds), {onlySelf: true});
}
if (data.target.configuration.type === NotificationTargetType.MICROSOFT_TEAMS
&& isUndefinedOrNull(this.data.target.configuration.useOldApi)) {
this.targetNotificationForm.get('configuration.useOldApi').patchValue(true, {emitEvent: false});
}
}
}

1
ui-ngx/src/app/shared/models/notification.models.ts

@ -290,6 +290,7 @@ export interface SlackNotificationTargetConfig {
export interface MicrosoftTeamsNotificationTargetConfig {
webhookUrl: string;
channelName: string;
useOldApi?: boolean;
}
export enum NotificationTargetType {
PLATFORM_USERS = 'PLATFORM_USERS',

2
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -4295,6 +4295,8 @@
"type": "Type",
"unread": "Unread",
"updated": "Updated",
"use-deprecated-webhook-connectors": "Use deprecated Webhook connectors",
"use-old-api": "Use old API",
"use-template": "Use template",
"view-all": "View all",
"warning": "Warning",

Loading…
Cancel
Save