Send notifications via Slack independently of the targets

This commit is contained in:
ViacheslavKlimov 2023-01-12 15:01:23 +02:00
parent 6eea14bcf7
commit 91a4de7780
9 changed files with 156 additions and 38 deletions

View File

@ -64,6 +64,7 @@ import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpd
import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -130,27 +131,43 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
.build(); .build();
ctx.init(); ctx.init();
Set<NotificationDeliveryMethod> deliveryMethods = ctx.getDeliveryMethods();
List<ListenableFuture<Void>> results = new ArrayList<>(); List<ListenableFuture<Void>> results = new ArrayList<>();
for (NotificationTargetId targetId : notificationRequest.getTargets()) { for (NotificationTargetId targetId : notificationRequest.getTargets()) {
DaoUtil.processBatches(pageLink -> { DaoUtil.processBatches(pageLink -> {
return notificationTargetService.findRecipientsForNotificationTarget(tenantId, ctx.getCustomerId(), targetId, pageLink); return notificationTargetService.findRecipientsForNotificationTarget(tenantId, ctx.getCustomerId(), targetId, pageLink);
}, 200, recipientsBatch -> { }, 200, recipientsBatch -> {
for (NotificationDeliveryMethod deliveryMethod : ctx.getDeliveryMethods()) { for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) {
if (deliveryMethod.isIndependent()) continue;
List<User> recipients = recipientsBatch.getData(); List<User> recipients = recipientsBatch.getData();
log.debug("Sending {} notifications for request {} to recipients batch ({})", deliveryMethod, savedNotificationRequest.getId(), recipients.size()); log.debug("Sending {} notifications for request {} to recipients batch ({})", deliveryMethod, savedNotificationRequest.getId(), recipients.size());
NotificationChannel notificationChannel = channels.get(deliveryMethod); NotificationChannel notificationChannel = channels.get(deliveryMethod);
for (User recipient : recipients) { for (User recipient : recipients) {
ListenableFuture<Void> resultFuture = processForRecipient(notificationChannel, recipient, ctx); ListenableFuture<Void> resultFuture = process(notificationChannel, recipient, ctx);
DonAsynchron.withCallback(resultFuture, result -> { DonAsynchron.withCallback(resultFuture, result -> {
ctx.getStats().reportSent(deliveryMethod, recipient); ctx.getStats().reportSent(deliveryMethod, recipient);
}, error -> { }, error -> {
ctx.getStats().reportError(deliveryMethod, recipient, error); ctx.getStats().reportError(deliveryMethod, error, recipient);
}); });
results.add(resultFuture); results.add(resultFuture);
} }
} }
}); });
} }
for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) {
if (deliveryMethod.isIndependent()) {
NotificationChannel notificationChannel = channels.get(deliveryMethod);
ListenableFuture<Void> resultFuture = process(notificationChannel, null, ctx);
DonAsynchron.withCallback(resultFuture, result -> {
ctx.getStats().reportSent(deliveryMethod, null);
}, error -> {
ctx.getStats().reportError(deliveryMethod, error, null);
});
results.add(resultFuture);
}
}
Futures.whenAllComplete(results).run(() -> { Futures.whenAllComplete(results).run(() -> {
NotificationRequestStats stats = ctx.getStats(); NotificationRequestStats stats = ctx.getStats();
@ -177,33 +194,21 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
return savedNotificationRequest; return savedNotificationRequest;
} }
private ListenableFuture<Void> processForRecipient(NotificationChannel notificationChannel, User recipient, NotificationProcessingContext ctx) { private ListenableFuture<Void> process(NotificationChannel notificationChannel, User recipient, NotificationProcessingContext ctx) {
NotificationDeliveryMethod deliveryMethod = notificationChannel.getDeliveryMethod(); NotificationDeliveryMethod deliveryMethod = notificationChannel.getDeliveryMethod();
if (ctx.getStats().contains(deliveryMethod, recipient.getId())) { if (recipient != null && ctx.getStats().contains(deliveryMethod, recipient.getId())) {
return Futures.immediateFailedFuture(new AlreadySentException()); return Futures.immediateFailedFuture(new AlreadySentException());
} }
DeliveryMethodNotificationTemplate processedTemplate; DeliveryMethodNotificationTemplate processedTemplate;
try { try {
processedTemplate = ctx.getProcessedTemplate(deliveryMethod, recipient); Map<String, String> templateContext = recipient != null ? ctx.createTemplateContext(recipient) : Collections.emptyMap();
processedTemplate = ctx.getProcessedTemplate(deliveryMethod, templateContext);
} catch (Exception e) { } catch (Exception e) {
return Futures.immediateFailedFuture(e); return Futures.immediateFailedFuture(e);
} }
return notificationChannel.sendNotification(recipient, processedTemplate, ctx); return notificationChannel.sendNotification(recipient, processedTemplate, ctx);
} }
private void forwardToNotificationSchedulerService(TenantId tenantId, NotificationRequestId notificationRequestId) {
TransportProtos.NotificationSchedulerServiceMsg.Builder msg = TransportProtos.NotificationSchedulerServiceMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setRequestIdMSB(notificationRequestId.getId().getMostSignificantBits())
.setRequestIdLSB(notificationRequestId.getId().getLeastSignificantBits())
.setTs(System.currentTimeMillis());
TransportProtos.ToCoreMsg toCoreMsg = TransportProtos.ToCoreMsg.newBuilder()
.setNotificationSchedulerServiceMsg(msg)
.build();
clusterService.pushMsgToCore(tenantId, notificationRequestId, toCoreMsg, null);
}
@Override @Override
public ListenableFuture<Void> sendNotification(User recipient, PushDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) { public ListenableFuture<Void> sendNotification(User recipient, PushDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) {
NotificationRequest request = ctx.getRequest(); NotificationRequest request = ctx.getRequest();
@ -302,6 +307,19 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
return notificationRequest; return notificationRequest;
} }
private void forwardToNotificationSchedulerService(TenantId tenantId, NotificationRequestId notificationRequestId) {
TransportProtos.NotificationSchedulerServiceMsg.Builder msg = TransportProtos.NotificationSchedulerServiceMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setRequestIdMSB(notificationRequestId.getId().getMostSignificantBits())
.setRequestIdLSB(notificationRequestId.getId().getLeastSignificantBits())
.setTs(System.currentTimeMillis());
TransportProtos.ToCoreMsg toCoreMsg = TransportProtos.ToCoreMsg.newBuilder()
.setNotificationSchedulerServiceMsg(msg)
.build();
clusterService.pushMsgToCore(tenantId, notificationRequestId, toCoreMsg, null);
}
private ListenableFuture<Void> onNotificationUpdate(TenantId tenantId, UserId recipientId, NotificationUpdate update) { private ListenableFuture<Void> onNotificationUpdate(TenantId tenantId, UserId recipientId, NotificationUpdate update) {
log.trace("Submitting notification update for recipient {}: {}", recipientId, update); log.trace("Submitting notification update for recipient {}: {}", recipientId, update);
return Futures.submit(() -> { return Futures.submit(() -> {

View File

@ -90,8 +90,11 @@ public class NotificationProcessingContext {
return (C) settings.getDeliveryMethodsConfigs().get(deliveryMethod); return (C) settings.getDeliveryMethodsConfigs().get(deliveryMethod);
} }
protected <T extends DeliveryMethodNotificationTemplate> T getProcessedTemplate(NotificationDeliveryMethod deliveryMethod, User recipient) { protected <T extends DeliveryMethodNotificationTemplate> T getProcessedTemplate(NotificationDeliveryMethod deliveryMethod, Map<String, String> templateContext) {
Map<String, String> templateContext = createTemplateContext(recipient); if (request.getInfo() != null) {
templateContext = new HashMap<>(templateContext);
templateContext.putAll(request.getInfo().getTemplateData());
}
T template = (T) templates.get(deliveryMethod).copy(); T template = (T) templates.get(deliveryMethod).copy();
template.setBody(processTemplate(template.getBody(), templateContext)); template.setBody(processTemplate(template.getBody(), templateContext));
@ -106,14 +109,11 @@ public class NotificationProcessingContext {
return TbNodeUtils.processTemplate(template, context); return TbNodeUtils.processTemplate(template, context);
} }
private Map<String, String> createTemplateContext(User recipient) { public Map<String, String> createTemplateContext(User recipient) {
Map<String, String> templateContext = new HashMap<>(); Map<String, String> templateContext = new HashMap<>();
templateContext.put("email", recipient.getEmail()); templateContext.put("email", recipient.getEmail());
templateContext.put("firstName", Strings.nullToEmpty(recipient.getFirstName())); templateContext.put("firstName", Strings.nullToEmpty(recipient.getFirstName()));
templateContext.put("lastName", Strings.nullToEmpty(recipient.getLastName())); templateContext.put("lastName", Strings.nullToEmpty(recipient.getLastName()));
if (request.getInfo() != null) {
templateContext.putAll(request.getInfo().getTemplateData());
}
return templateContext; return templateContext;
} }

View File

@ -25,13 +25,16 @@ import org.thingsboard.rule.engine.api.slack.SlackService;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.NotificationTemplateId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig; import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
import org.thingsboard.server.common.data.notification.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.UserOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.info.UserOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.UserListNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.UserListNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
@ -52,6 +55,7 @@ import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public abstract class AbstractNotificationApiTest extends AbstractControllerTest { public abstract class AbstractNotificationApiTest extends AbstractControllerTest {
@ -94,20 +98,28 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
deliveryMethods = new NotificationDeliveryMethod[]{NotificationDeliveryMethod.PUSH}; deliveryMethods = new NotificationDeliveryMethod[]{NotificationDeliveryMethod.PUSH};
} }
NotificationTemplate notificationTemplate = createNotificationTemplate(DEFAULT_NOTIFICATION_TYPE, DEFAULT_NOTIFICATION_SUBJECT, text, deliveryMethods); NotificationTemplate notificationTemplate = createNotificationTemplate(DEFAULT_NOTIFICATION_TYPE, DEFAULT_NOTIFICATION_SUBJECT, text, deliveryMethods);
return submitNotificationRequest(targets, notificationTemplate.getId(), delayInSec);
}
protected NotificationRequest submitNotificationRequest(List<NotificationTargetId> targets, NotificationTemplateId notificationTemplateId, int delayInSec) {
NotificationRequestConfig config = new NotificationRequestConfig(); NotificationRequestConfig config = new NotificationRequestConfig();
config.setSendingDelayInSec(delayInSec); config.setSendingDelayInSec(delayInSec);
UserOriginatedNotificationInfo notificationInfo = new UserOriginatedNotificationInfo(); UserOriginatedNotificationInfo notificationInfo = new UserOriginatedNotificationInfo();
notificationInfo.setDescription("The text: " + text); notificationInfo.setDescription("My description");
NotificationRequest notificationRequest = NotificationRequest.builder() NotificationRequest notificationRequest = NotificationRequest.builder()
.tenantId(tenantId) .tenantId(tenantId)
.targets(targets) .targets(targets)
.templateId(notificationTemplate.getId()) .templateId(notificationTemplateId)
.info(notificationInfo) .info(notificationInfo)
.additionalConfig(config) .additionalConfig(config)
.build(); .build();
return doPost("/api/notification/request", notificationRequest, NotificationRequest.class); return doPost("/api/notification/request", notificationRequest, NotificationRequest.class);
} }
protected NotificationRequestStats getStats(NotificationRequestId notificationRequestId) throws Exception {
return findNotificationRequest(notificationRequestId).getStats();
}
protected NotificationTemplate createNotificationTemplate(NotificationType notificationType, String subject, protected NotificationTemplate createNotificationTemplate(NotificationType notificationType, String subject,
String text, NotificationDeliveryMethod... deliveryMethods) { String text, NotificationDeliveryMethod... deliveryMethods) {
NotificationTemplate notificationTemplate = new NotificationTemplate(); NotificationTemplate notificationTemplate = new NotificationTemplate();
@ -143,9 +155,17 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
config.getDeliveryMethodsTemplates().put(deliveryMethod, deliveryMethodNotificationTemplate); config.getDeliveryMethodsTemplates().put(deliveryMethod, deliveryMethodNotificationTemplate);
} }
notificationTemplate.setConfiguration(config); notificationTemplate.setConfiguration(config);
return saveNotificationTemplate(notificationTemplate);
}
protected NotificationTemplate saveNotificationTemplate(NotificationTemplate notificationTemplate) {
return doPost("/api/notification/template", notificationTemplate, NotificationTemplate.class); return doPost("/api/notification/template", notificationTemplate, NotificationTemplate.class);
} }
protected void saveNotificationSettings(NotificationSettings notificationSettings) throws Exception {
doPost("/api/notification/settings", notificationSettings).andExpect(status().isOk());
}
protected Pair<User, NotificationApiWsClient> createUserAndConnectWsClient(Authority authority) throws Exception { protected Pair<User, NotificationApiWsClient> createUserAndConnectWsClient(Authority authority) throws Exception {
User user = new User(); User user = new User();
user.setTenantId(tenantId); user.setTenantId(tenantId);

View File

@ -30,9 +30,16 @@ import org.thingsboard.server.common.data.notification.NotificationDeliveryMetho
import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestStats; import org.thingsboard.server.common.data.notification.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus; import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.UserOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.info.UserOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.targets.AllUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.AllUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig;
import org.thingsboard.server.common.data.notification.template.SlackConversation;
import org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
@ -43,6 +50,7 @@ import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsCou
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate; import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -55,6 +63,7 @@ import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith; import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ -312,8 +321,8 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
}); });
await().atMost(2, TimeUnit.SECONDS) await().atMost(2, TimeUnit.SECONDS)
.until(() -> findNotificationRequest(notificationRequest.getId()).getStats() != null); .until(() -> getStats(notificationRequest.getId()) != null);
NotificationRequestStats stats = findNotificationRequest(notificationRequest.getId()).getStats(); NotificationRequestStats stats = getStats(notificationRequest.getId());
assertThat(stats.getSent().get(NotificationDeliveryMethod.PUSH)).hasValue(usersCount); assertThat(stats.getSent().get(NotificationDeliveryMethod.PUSH)).hasValue(usersCount);
assertThat(stats.getSent().get(NotificationDeliveryMethod.EMAIL)).hasValue(usersCount); assertThat(stats.getSent().get(NotificationDeliveryMethod.EMAIL)).hasValue(usersCount);
@ -341,8 +350,8 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
wsClient.waitForUpdate(); wsClient.waitForUpdate();
await().atMost(2, TimeUnit.SECONDS) await().atMost(2, TimeUnit.SECONDS)
.until(() -> findNotificationRequest(notificationRequest.getId()).getStats() != null); .until(() -> getStats(notificationRequest.getId()) != null);
NotificationRequestStats stats = findNotificationRequest(notificationRequest.getId()).getStats(); NotificationRequestStats stats = getStats(notificationRequest.getId());
assertThat(stats.getSent().get(NotificationDeliveryMethod.PUSH)).hasValue(1); assertThat(stats.getSent().get(NotificationDeliveryMethod.PUSH)).hasValue(1);
assertThat(stats.getSent().get(NotificationDeliveryMethod.EMAIL)).hasValue(1); assertThat(stats.getSent().get(NotificationDeliveryMethod.EMAIL)).hasValue(1);
@ -360,7 +369,6 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
user.setAuthority(Authority.TENANT_ADMIN); user.setAuthority(Authority.TENANT_ADMIN);
user.setEmail("test-user-" + i + "@thingsboard.org"); user.setEmail("test-user-" + i + "@thingsboard.org");
user = doPost("/api/user", user, User.class); user = doPost("/api/user", user, User.class);
System.err.println(i);
users.add(user); users.add(user);
} }
@ -389,10 +397,57 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
assertThat(sentNotifications.getData()).extracting(Notification::getRecipientId) assertThat(sentNotifications.getData()).extracting(Notification::getRecipientId)
.containsAll(users.stream().map(User::getId).collect(Collectors.toSet())); .containsAll(users.stream().map(User::getId).collect(Collectors.toSet()));
NotificationRequestStats stats = findNotificationRequest(notificationRequest.getId()).getStats(); NotificationRequestStats stats = getStats(notificationRequest.getId());
assertThat(stats.getSent().values().stream().mapToInt(AtomicInteger::get).sum()).isGreaterThanOrEqualTo(usersCount); assertThat(stats.getSent().values().stream().mapToInt(AtomicInteger::get).sum()).isGreaterThanOrEqualTo(usersCount);
} }
@Test
public void testSlackNotifications() throws Exception {
NotificationSettings settings = new NotificationSettings();
SlackNotificationDeliveryMethodConfig slackConfig = new SlackNotificationDeliveryMethodConfig();
slackConfig.setMethod(NotificationDeliveryMethod.SLACK);
slackConfig.setEnabled(true);
String slackToken = "xoxb-123123123";
slackConfig.setBotToken(slackToken);
settings.setDeliveryMethodsConfigs(Map.of(
NotificationDeliveryMethod.SLACK, slackConfig
));
saveNotificationSettings(settings);
NotificationTemplate notificationTemplate = new NotificationTemplate();
notificationTemplate.setName("Slack notification template");
notificationTemplate.setNotificationType(NotificationType.GENERAL);
NotificationTemplateConfig config = new NotificationTemplateConfig();
config.setDefaultTextTemplate("To Slack :)");
SlackDeliveryMethodNotificationTemplate slackNotificationTemplate = new SlackDeliveryMethodNotificationTemplate();
slackNotificationTemplate.setEnabled(true);
slackNotificationTemplate.setConversationType(SlackConversation.Type.PUBLIC_CHANNEL);
String conversationId = "U154475415";
slackNotificationTemplate.setConversationId(conversationId);
config.setDeliveryMethodsTemplates(Map.of(
NotificationDeliveryMethod.SLACK, slackNotificationTemplate
));
notificationTemplate.setConfiguration(config);
notificationTemplate = saveNotificationTemplate(notificationTemplate);
NotificationRequest successfulNotificationRequest = submitNotificationRequest(Collections.emptyList(), notificationTemplate.getId(), 0);
await().atMost(2, TimeUnit.SECONDS)
.until(() -> getStats(successfulNotificationRequest.getId()) != null);
verify(slackService).sendMessage(eq(tenantId), eq(slackToken), eq(conversationId), eq(config.getDefaultTextTemplate()));
NotificationRequestStats stats = getStats(successfulNotificationRequest.getId());
assertThat(stats.getSent().get(NotificationDeliveryMethod.SLACK)).hasValue(1);
String errorMessage = "Error!!!";
doThrow(new RuntimeException(errorMessage)).when(slackService).sendMessage(any(), any(), any(), any());
NotificationRequest failedNotificationRequest = submitNotificationRequest(Collections.emptyList(), notificationTemplate.getId(), 0);
await().atMost(2, TimeUnit.SECONDS)
.until(() -> getStats(failedNotificationRequest.getId()) != null);
stats = getStats(failedNotificationRequest.getId());
assertThat(stats.getErrors().get(NotificationDeliveryMethod.SLACK).values()).containsExactly(errorMessage);
}
private void checkFullNotificationsUpdate(UnreadNotificationsUpdate notificationsUpdate, String... expectedNotifications) { private void checkFullNotificationsUpdate(UnreadNotificationsUpdate notificationsUpdate, String... expectedNotifications) {
assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getText).containsOnly(expectedNotifications); assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getText).containsOnly(expectedNotifications);
assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getType).containsOnly(DEFAULT_NOTIFICATION_TYPE); assertThat(notificationsUpdate.getNotifications()).extracting(Notification::getType).containsOnly(DEFAULT_NOTIFICATION_TYPE);

View File

@ -15,6 +15,24 @@
*/ */
package org.thingsboard.server.common.data.notification; package org.thingsboard.server.common.data.notification;
import lombok.Getter;
public enum NotificationDeliveryMethod { public enum NotificationDeliveryMethod {
PUSH, EMAIL, SMS, SLACK
PUSH,
EMAIL,
SMS,
SLACK(true);
@Getter
private final boolean independent; // TODO: think of a better name
NotificationDeliveryMethod() {
this(false);
}
NotificationDeliveryMethod(boolean independent) {
this.independent = independent;
}
} }

View File

@ -46,7 +46,7 @@ import java.util.List;
public class NotificationRequest extends BaseData<NotificationRequestId> implements HasTenantId, HasName { public class NotificationRequest extends BaseData<NotificationRequestId> implements HasTenantId, HasName {
private TenantId tenantId; private TenantId tenantId;
@NotEmpty @NotNull
private List<NotificationTargetId> targets; private List<NotificationTargetId> targets;
@NotNull @NotNull

View File

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.UserId;
import java.util.Collections; import java.util.Collections;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -52,15 +53,18 @@ public class NotificationRequestStats {
public void reportSent(NotificationDeliveryMethod deliveryMethod, User recipient) { public void reportSent(NotificationDeliveryMethod deliveryMethod, User recipient) {
sent.computeIfAbsent(deliveryMethod, k -> new AtomicInteger()).incrementAndGet(); sent.computeIfAbsent(deliveryMethod, k -> new AtomicInteger()).incrementAndGet();
processedRecipients.computeIfAbsent(deliveryMethod, k -> ConcurrentHashMap.newKeySet()).add(recipient.getId()); if (recipient != null) {
processedRecipients.computeIfAbsent(deliveryMethod, k -> ConcurrentHashMap.newKeySet()).add(recipient.getId());
}
} }
public void reportError(NotificationDeliveryMethod deliveryMethod, User recipient, Throwable error) { public void reportError(NotificationDeliveryMethod deliveryMethod, Throwable error, User recipient) {
if (error instanceof AlreadySentException) { if (error instanceof AlreadySentException) {
return; return;
} }
String errorMessage = error.getMessage(); String errorMessage = error.getMessage();
errors.computeIfAbsent(deliveryMethod, k -> new ConcurrentHashMap<>()).put(recipient.getEmail(), errorMessage); String key = Optional.ofNullable(recipient).map(User::getEmail).orElse("");
errors.computeIfAbsent(deliveryMethod, k -> new ConcurrentHashMap<>()).put(key, errorMessage);
} }
public boolean contains(NotificationDeliveryMethod deliveryMethod) { public boolean contains(NotificationDeliveryMethod deliveryMethod) {

View File

@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.notification.settings;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import javax.validation.Valid;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.util.Map; import java.util.Map;
@ -25,6 +26,7 @@ import java.util.Map;
public class NotificationSettings { public class NotificationSettings {
@NotNull @NotNull
@Valid
// location on the screen, shown notifications count, timings of displaying // location on the screen, shown notifications count, timings of displaying
private Map<NotificationDeliveryMethod, NotificationDeliveryMethodConfig> deliveryMethodsConfigs; private Map<NotificationDeliveryMethod, NotificationDeliveryMethodConfig> deliveryMethodsConfigs;

View File

@ -37,6 +37,7 @@ public class NotificationTemplateConfig {
@JsonIgnore @JsonIgnore
@AssertTrue(message = "defaultTextTemplate and notificationSubject must be specified if one absent for delivery method") @AssertTrue(message = "defaultTextTemplate and notificationSubject must be specified if one absent for delivery method")
public boolean isValid() { public boolean isValid() {
if (deliveryMethodsTemplates == null) return true;
for (DeliveryMethodNotificationTemplate template : deliveryMethodsTemplates.values()) { for (DeliveryMethodNotificationTemplate template : deliveryMethodsTemplates.values()) {
if (StringUtils.isEmpty(template.getBody()) && StringUtils.isEmpty(defaultTextTemplate)) { if (StringUtils.isEmpty(template.getBody()) && StringUtils.isEmpty(defaultTextTemplate)) {
return false; return false;