546 changed files with 25790 additions and 2034 deletions
@ -0,0 +1,67 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.actors.ruleChain; |
|||
|
|||
import org.thingsboard.server.actors.ActorSystemContext; |
|||
import org.thingsboard.server.actors.TbRuleNodeUpdateException; |
|||
import org.thingsboard.server.actors.service.ComponentActor; |
|||
import org.thingsboard.server.actors.shared.ComponentMsgProcessor; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.RuleChainId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; |
|||
import org.thingsboard.server.common.msg.TbActorStopReason; |
|||
import org.thingsboard.server.dao.notification.trigger.RuleEngineComponentLifecycleEventTrigger; |
|||
|
|||
public abstract class RuleEngineComponentActor<T extends EntityId, P extends ComponentMsgProcessor<T>> extends ComponentActor<T, P> { |
|||
|
|||
public RuleEngineComponentActor(ActorSystemContext systemContext, TenantId tenantId, T id) { |
|||
super(systemContext, tenantId, id); |
|||
} |
|||
|
|||
@Override |
|||
protected void logLifecycleEvent(ComponentLifecycleEvent event, Exception e) { |
|||
super.logLifecycleEvent(event, e); |
|||
if (e instanceof TbRuleNodeUpdateException || (event == ComponentLifecycleEvent.STARTED && e != null)) { |
|||
return; |
|||
} |
|||
processNotificationRule(event, e); |
|||
} |
|||
|
|||
@Override |
|||
public void destroy(TbActorStopReason stopReason, Throwable cause) { |
|||
super.destroy(stopReason, cause); |
|||
if (stopReason == TbActorStopReason.INIT_FAILED && cause != null) { |
|||
processNotificationRule(ComponentLifecycleEvent.STARTED, cause); |
|||
} |
|||
} |
|||
|
|||
private void processNotificationRule(ComponentLifecycleEvent event, Throwable e) { |
|||
systemContext.getNotificationRuleProcessingService().process(tenantId, RuleEngineComponentLifecycleEventTrigger.builder() |
|||
.ruleChainId(getRuleChainId()) |
|||
.ruleChainName(getRuleChainName()) |
|||
.componentId(id) |
|||
.componentName(processor.getComponentName()) |
|||
.eventType(event) |
|||
.error(e) |
|||
.build()); |
|||
} |
|||
|
|||
protected abstract RuleChainId getRuleChainId(); |
|||
|
|||
protected abstract String getRuleChainName(); |
|||
|
|||
} |
|||
@ -0,0 +1,320 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.controller; |
|||
|
|||
import io.swagger.annotations.ApiOperation; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.security.core.annotation.AuthenticationPrincipal; |
|||
import org.springframework.web.bind.annotation.DeleteMapping; |
|||
import org.springframework.web.bind.annotation.GetMapping; |
|||
import org.springframework.web.bind.annotation.PathVariable; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.PutMapping; |
|||
import org.springframework.web.bind.annotation.RequestBody; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RequestParam; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
import org.thingsboard.rule.engine.api.NotificationCenter; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
import org.thingsboard.server.common.data.User; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.common.data.id.NotificationId; |
|||
import org.thingsboard.server.common.data.id.NotificationRequestId; |
|||
import org.thingsboard.server.common.data.id.NotificationTargetId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.notification.Notification; |
|||
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; |
|||
import org.thingsboard.server.common.data.notification.NotificationRequest; |
|||
import org.thingsboard.server.common.data.notification.NotificationRequestInfo; |
|||
import org.thingsboard.server.common.data.notification.NotificationRequestPreview; |
|||
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.NotificationTargetType; |
|||
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; |
|||
import org.thingsboard.server.common.data.notification.template.NotificationTemplate; |
|||
import org.thingsboard.server.common.data.page.PageData; |
|||
import org.thingsboard.server.common.data.page.PageLink; |
|||
import org.thingsboard.server.dao.notification.NotificationRequestService; |
|||
import org.thingsboard.server.dao.notification.NotificationService; |
|||
import org.thingsboard.server.dao.notification.NotificationSettingsService; |
|||
import org.thingsboard.server.dao.notification.NotificationTargetService; |
|||
import org.thingsboard.server.dao.notification.NotificationTemplateService; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
import org.thingsboard.server.service.notification.NotificationProcessingContext; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
import org.thingsboard.server.service.security.permission.Operation; |
|||
import org.thingsboard.server.service.security.permission.Resource; |
|||
|
|||
import javax.validation.Valid; |
|||
import java.util.Collections; |
|||
import java.util.HashMap; |
|||
import java.util.LinkedHashSet; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Set; |
|||
import java.util.UUID; |
|||
import java.util.stream.Collectors; |
|||
|
|||
import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION; |
|||
|
|||
@RestController |
|||
@TbCoreComponent |
|||
@RequestMapping("/api") |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class NotificationController extends BaseController { |
|||
|
|||
private final NotificationService notificationService; |
|||
private final NotificationRequestService notificationRequestService; |
|||
private final NotificationTemplateService notificationTemplateService; |
|||
private final NotificationTargetService notificationTargetService; |
|||
private final NotificationCenter notificationCenter; |
|||
private final NotificationSettingsService notificationSettingsService; |
|||
|
|||
@ApiOperation(value = "Get notifications (getNotifications)", |
|||
notes = "**WebSocket API**:\n\n" + |
|||
"There are 2 types of subscriptions: one for unread notifications count, another for unread notifications themselves.\n\n" + |
|||
"The URI for opening WS session for notifications: `/api/ws/plugins/notifications`.\n\n" + |
|||
"Subscription command for unread notifications count:\n" + |
|||
"```\n{\n \"unreadCountSubCmd\": {\n \"cmdId\": 1234\n }\n}\n```\n" + |
|||
"To subscribe for latest unread notifications:\n" + |
|||
"```\n{\n \"unreadSubCmd\": {\n \"cmdId\": 1234,\n \"limit\": 10\n }\n}\n```\n" + |
|||
"To unsubscribe from any subscription:\n" + |
|||
"```\n{\n \"unsubCmd\": {\n \"cmdId\": 1234\n }\n}\n```\n" + |
|||
"To mark certain notifications as read, use following command:\n" + |
|||
"```\n{\n \"markAsReadCmd\": {\n \"cmdId\": 1234,\n \"notifications\": [\n \"6f860330-7fc2-11ed-b855-7dd3b7d2faa9\",\n \"5b6dfee0-8d0d-11ed-b61f-35a57b03dade\"\n ]\n }\n}\n\n```\n" + |
|||
"To mark all notifications as read:\n" + |
|||
"```\n{\n \"markAllAsReadCmd\": {\n \"cmdId\": 1234\n }\n}\n```\n" + |
|||
"\n\n" + |
|||
"Update structure for unread **notifications count subscription**:\n" + |
|||
"```\n{\n \"cmdId\": 1234,\n \"totalUnreadCount\": 55\n}\n```\n" + |
|||
"For **notifications subscription**:\n" + |
|||
"- full update of latest unread notifications:\n" + |
|||
"```\n{\n" + |
|||
" \"cmdId\": 1234,\n" + |
|||
" \"notifications\": [\n" + |
|||
" {\n" + |
|||
" \"id\": {\n" + |
|||
" \"entityType\": \"NOTIFICATION\",\n" + |
|||
" \"id\": \"6f860330-7fc2-11ed-b855-7dd3b7d2faa9\"\n" + |
|||
" },\n" + |
|||
" ...\n" + |
|||
" }\n" + |
|||
" ],\n" + |
|||
" \"totalUnreadCount\": 1\n" + |
|||
"}\n```\n" + |
|||
"- when new notification arrives or shown notification is updated:\n" + |
|||
"```\n{\n" + |
|||
" \"cmdId\": 1234,\n" + |
|||
" \"update\": {\n" + |
|||
" \"id\": {\n" + |
|||
" \"entityType\": \"NOTIFICATION\",\n" + |
|||
" \"id\": \"6f860330-7fc2-11ed-b855-7dd3b7d2faa9\"\n" + |
|||
" },\n" + |
|||
" # updated notification info, text, subject etc.\n" + |
|||
" ...\n" + |
|||
" },\n" + |
|||
" \"totalUnreadCount\": 2\n" + |
|||
"}\n```\n" + |
|||
"- when unread notifications count changes:\n" + |
|||
"```\n{\n" + |
|||
" \"cmdId\": 1234,\n" + |
|||
" \"totalUnreadCount\": 5\n" + |
|||
"}\n```") |
|||
@GetMapping("/notifications") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
public PageData<Notification> getNotifications(@RequestParam int pageSize, |
|||
@RequestParam int page, |
|||
@RequestParam(required = false) String textSearch, |
|||
@RequestParam(required = false) String sortProperty, |
|||
@RequestParam(required = false) String sortOrder, |
|||
@RequestParam(defaultValue = "false") boolean unreadOnly, |
|||
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { |
|||
// no permissions
|
|||
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); |
|||
return notificationService.findNotificationsByRecipientIdAndReadStatus(user.getTenantId(), user.getId(), unreadOnly, pageLink); |
|||
} |
|||
|
|||
@PutMapping("/notification/{id}/read") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
public void markNotificationAsRead(@PathVariable UUID id, |
|||
@AuthenticationPrincipal SecurityUser user) { |
|||
// no permissions
|
|||
NotificationId notificationId = new NotificationId(id); |
|||
notificationCenter.markNotificationAsRead(user.getTenantId(), user.getId(), notificationId); |
|||
} |
|||
|
|||
@PutMapping("/notifications/read") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
public void markAllNotificationsAsRead(@AuthenticationPrincipal SecurityUser user) { |
|||
// no permissions
|
|||
notificationCenter.markAllNotificationsAsRead(user.getTenantId(), user.getId()); |
|||
} |
|||
|
|||
@DeleteMapping("/notification/{id}") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
|||
public void deleteNotification(@PathVariable UUID id, |
|||
@AuthenticationPrincipal SecurityUser user) { |
|||
// no permissions
|
|||
NotificationId notificationId = new NotificationId(id); |
|||
notificationCenter.deleteNotification(user.getTenantId(), user.getId(), notificationId); |
|||
} |
|||
|
|||
@PostMapping("/notification/request") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public NotificationRequest createNotificationRequest(@RequestBody @Valid NotificationRequest notificationRequest, |
|||
@AuthenticationPrincipal SecurityUser user) throws Exception { |
|||
if (notificationRequest.getId() != null) { |
|||
throw new IllegalArgumentException("Notification request cannot be updated. You may only cancel/delete it"); |
|||
} |
|||
notificationRequest.setTenantId(user.getTenantId()); |
|||
checkEntity(notificationRequest.getId(), notificationRequest, NOTIFICATION); |
|||
|
|||
notificationRequest.setOriginatorEntityId(user.getId()); |
|||
if (notificationRequest.getInfo() != null && !(notificationRequest.getInfo() instanceof UserOriginatedNotificationInfo)) { |
|||
throw new IllegalArgumentException("Unsupported notification info type"); |
|||
} |
|||
notificationRequest.setRuleId(null); |
|||
notificationRequest.setStatus(null); |
|||
notificationRequest.setStats(null); |
|||
|
|||
return doSaveAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, notificationCenter::processNotificationRequest); |
|||
} |
|||
|
|||
@PostMapping("/notification/request/preview") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public NotificationRequestPreview getNotificationRequestPreview(@RequestBody @Valid NotificationRequest request, |
|||
@RequestParam(defaultValue = "20") int recipientsPreviewSize, |
|||
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { |
|||
NotificationRequestPreview preview = new NotificationRequestPreview(); |
|||
|
|||
request.setOriginatorEntityId(user.getId()); |
|||
NotificationTemplate template; |
|||
if (request.getTemplateId() != null) { |
|||
template = checkEntityId(request.getTemplateId(), notificationTemplateService::findNotificationTemplateById, Operation.READ); |
|||
} else { |
|||
template = request.getTemplate(); |
|||
} |
|||
if (template == null) { |
|||
throw new IllegalArgumentException("Template is missing"); |
|||
} |
|||
NotificationProcessingContext tmpProcessingCtx = NotificationProcessingContext.builder() |
|||
.tenantId(user.getTenantId()) |
|||
.request(request) |
|||
.settings(null) |
|||
.template(template) |
|||
.build(); |
|||
|
|||
Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> processedTemplates = tmpProcessingCtx.getDeliveryMethods().stream() |
|||
.collect(Collectors.toMap(m -> m, deliveryMethod -> { |
|||
Map<String, String> templateContext; |
|||
if (NotificationTargetType.PLATFORM_USERS.getSupportedDeliveryMethods().contains(deliveryMethod)) { |
|||
templateContext = tmpProcessingCtx.createTemplateContext(user); |
|||
} else { |
|||
templateContext = Collections.emptyMap(); |
|||
} |
|||
return tmpProcessingCtx.getProcessedTemplate(deliveryMethod, templateContext); |
|||
})); |
|||
preview.setProcessedTemplates(processedTemplates); |
|||
|
|||
// generic permission
|
|||
Set<User> recipientsPreview = new LinkedHashSet<>(); |
|||
Map<String, Integer> recipientsCountByTarget = new HashMap<>(); |
|||
List<NotificationTarget> targets = notificationTargetService.findNotificationTargetsByTenantIdAndIds(user.getTenantId(), |
|||
request.getTargets().stream().map(NotificationTargetId::new).collect(Collectors.toList())); |
|||
for (NotificationTarget target : targets) { |
|||
int recipientsCount; |
|||
if (target.getConfiguration().getType() == NotificationTargetType.PLATFORM_USERS) { |
|||
PageData<User> recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), null, |
|||
target.getConfiguration(), new PageLink(recipientsPreviewSize)); |
|||
recipientsCount = (int) recipients.getTotalElements(); |
|||
for (User recipient : recipients.getData()) { |
|||
if (recipientsPreview.size() < recipientsPreviewSize) { |
|||
recipientsPreview.add(recipient); |
|||
} else { |
|||
break; |
|||
} |
|||
} |
|||
} else { |
|||
recipientsCount = 1; |
|||
} |
|||
recipientsCountByTarget.put(target.getName(), recipientsCount); |
|||
} |
|||
preview.setRecipientsPreview(recipientsPreview); |
|||
preview.setRecipientsCountByTarget(recipientsCountByTarget); |
|||
preview.setTotalRecipientsCount(recipientsCountByTarget.values().stream().mapToInt(Integer::intValue).sum()); |
|||
|
|||
return preview; |
|||
} |
|||
|
|||
@GetMapping("/notification/request/{id}") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public NotificationRequestInfo getNotificationRequestById(@PathVariable UUID id) throws ThingsboardException { |
|||
NotificationRequestId notificationRequestId = new NotificationRequestId(id); |
|||
return checkEntityId(notificationRequestId, notificationRequestService::findNotificationRequestInfoById, Operation.READ); |
|||
} |
|||
|
|||
@GetMapping("/notification/requests") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public PageData<NotificationRequestInfo> getNotificationRequests(@RequestParam int pageSize, |
|||
@RequestParam int page, |
|||
@RequestParam(required = false) String textSearch, |
|||
@RequestParam(required = false) String sortProperty, |
|||
@RequestParam(required = false) String sortOrder, |
|||
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { |
|||
// generic permission
|
|||
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); |
|||
return notificationRequestService.findNotificationRequestsInfosByTenantIdAndOriginatorType(user.getTenantId(), EntityType.USER, pageLink); |
|||
} |
|||
|
|||
@DeleteMapping("/notification/request/{id}") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public void deleteNotificationRequest(@PathVariable UUID id) throws Exception { |
|||
NotificationRequestId notificationRequestId = new NotificationRequestId(id); |
|||
NotificationRequest notificationRequest = checkEntityId(notificationRequestId, notificationRequestService::findNotificationRequestById, Operation.DELETE); |
|||
doDeleteAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, notificationCenter::deleteNotificationRequest); |
|||
} |
|||
|
|||
|
|||
@PostMapping("/notification/settings") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public NotificationSettings saveNotificationSettings(@RequestBody @Valid NotificationSettings notificationSettings, |
|||
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { |
|||
accessControlService.checkPermission(user, Resource.ADMIN_SETTINGS, Operation.WRITE); |
|||
TenantId tenantId = user.isSystemAdmin() ? TenantId.SYS_TENANT_ID : user.getTenantId(); |
|||
notificationSettingsService.saveNotificationSettings(tenantId, notificationSettings); |
|||
return notificationSettings; |
|||
} |
|||
|
|||
@GetMapping("/notification/settings") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public NotificationSettings getNotificationSettings(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { |
|||
accessControlService.checkPermission(user, Resource.ADMIN_SETTINGS, Operation.READ); |
|||
TenantId tenantId = user.isSystemAdmin() ? TenantId.SYS_TENANT_ID : user.getTenantId(); |
|||
return notificationSettingsService.findNotificationSettings(tenantId); |
|||
} |
|||
|
|||
@GetMapping("/notification/deliveryMethods") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public Set<NotificationDeliveryMethod> getAvailableDeliveryMethods(@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { |
|||
accessControlService.checkPermission(user, Resource.ADMIN_SETTINGS, Operation.READ); |
|||
return notificationCenter.getAvailableDeliveryMethods(user.getTenantId()); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,103 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.controller; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.security.core.annotation.AuthenticationPrincipal; |
|||
import org.springframework.web.bind.annotation.DeleteMapping; |
|||
import org.springframework.web.bind.annotation.GetMapping; |
|||
import org.springframework.web.bind.annotation.PathVariable; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.RequestBody; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RequestParam; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.common.data.id.NotificationRuleId; |
|||
import org.thingsboard.server.common.data.notification.rule.NotificationRule; |
|||
import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; |
|||
import org.thingsboard.server.common.data.page.PageData; |
|||
import org.thingsboard.server.common.data.page.PageLink; |
|||
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; |
|||
import org.thingsboard.server.dao.notification.NotificationRuleService; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
import org.thingsboard.server.service.security.permission.Operation; |
|||
|
|||
import javax.validation.Valid; |
|||
import java.util.UUID; |
|||
|
|||
import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION; |
|||
|
|||
@RestController |
|||
@TbCoreComponent |
|||
@RequestMapping("/api/notification") |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class NotificationRuleController extends BaseController { |
|||
|
|||
private final NotificationRuleService notificationRuleService; |
|||
|
|||
@PostMapping("/rule") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public NotificationRule saveNotificationRule(@RequestBody @Valid NotificationRule notificationRule, |
|||
@AuthenticationPrincipal SecurityUser user) throws Exception { |
|||
notificationRule.setTenantId(user.getTenantId()); |
|||
checkEntity(notificationRule.getId(), notificationRule, NOTIFICATION); |
|||
|
|||
NotificationRuleTriggerType triggerType = notificationRule.getTriggerType(); |
|||
if ((user.isTenantAdmin() && !triggerType.isTenantLevel()) || (user.isSystemAdmin() && triggerType.isTenantLevel())) { |
|||
throw new IllegalArgumentException("Trigger type " + triggerType + " is not available"); |
|||
} |
|||
|
|||
return doSaveAndLog(EntityType.NOTIFICATION_RULE, notificationRule, notificationRuleService::saveNotificationRule); |
|||
} |
|||
|
|||
@GetMapping("/rule/{id}") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public NotificationRuleInfo getNotificationRuleById(@PathVariable UUID id) throws ThingsboardException { |
|||
NotificationRuleId notificationRuleId = new NotificationRuleId(id); |
|||
return checkEntityId(notificationRuleId, notificationRuleService::findNotificationRuleInfoById, Operation.READ); |
|||
} |
|||
|
|||
@GetMapping("/rules") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public PageData<NotificationRuleInfo> getNotificationRules(@RequestParam int pageSize, |
|||
@RequestParam int page, |
|||
@RequestParam(required = false) String textSearch, |
|||
@RequestParam(required = false) String sortProperty, |
|||
@RequestParam(required = false) String sortOrder, |
|||
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { |
|||
// generic permission
|
|||
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); |
|||
return notificationRuleService.findNotificationRulesInfosByTenantId(user.getTenantId(), pageLink); |
|||
} |
|||
|
|||
@DeleteMapping("/rule/{id}") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public void deleteNotificationRule(@PathVariable UUID id, |
|||
@AuthenticationPrincipal SecurityUser user) throws Exception { |
|||
NotificationRuleId notificationRuleId = new NotificationRuleId(id); |
|||
NotificationRule notificationRule = checkEntityId(notificationRuleId, notificationRuleService::findNotificationRuleById, Operation.DELETE); |
|||
doDeleteAndLog(EntityType.NOTIFICATION_RULE, notificationRule, notificationRuleService::deleteNotificationRuleById); |
|||
tbClusterService.broadcastEntityStateChangeEvent(user.getTenantId(), notificationRuleId, ComponentLifecycleEvent.DELETED); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,199 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.controller; |
|||
|
|||
import io.swagger.annotations.ApiOperation; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.commons.collections.CollectionUtils; |
|||
import org.springframework.security.access.AccessDeniedException; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.security.core.annotation.AuthenticationPrincipal; |
|||
import org.springframework.web.bind.annotation.DeleteMapping; |
|||
import org.springframework.web.bind.annotation.GetMapping; |
|||
import org.springframework.web.bind.annotation.PathVariable; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.RequestBody; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RequestParam; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
import org.thingsboard.server.common.data.User; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.common.data.id.CustomerId; |
|||
import org.thingsboard.server.common.data.id.NotificationTargetId; |
|||
import org.thingsboard.server.common.data.id.UserId; |
|||
import org.thingsboard.server.common.data.notification.NotificationType; |
|||
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; |
|||
import org.thingsboard.server.common.data.notification.targets.NotificationTargetConfig; |
|||
import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; |
|||
import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; |
|||
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; |
|||
import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; |
|||
import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; |
|||
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; |
|||
import org.thingsboard.server.common.data.page.PageData; |
|||
import org.thingsboard.server.common.data.page.PageLink; |
|||
import org.thingsboard.server.dao.notification.NotificationTargetService; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
import org.thingsboard.server.service.security.permission.Operation; |
|||
|
|||
import javax.validation.Valid; |
|||
import java.util.Arrays; |
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
import java.util.stream.Collectors; |
|||
|
|||
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; |
|||
import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION; |
|||
|
|||
@RestController |
|||
@TbCoreComponent |
|||
@RequestMapping("/api/notification") |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class NotificationTargetController extends BaseController { |
|||
|
|||
private final NotificationTargetService notificationTargetService; |
|||
|
|||
@ApiOperation(value = "Save notification target (saveNotificationTarget)", |
|||
notes = "Create or update notification target.\n\n" + |
|||
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) |
|||
@PostMapping("/target") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public NotificationTarget saveNotificationTarget(@RequestBody @Valid NotificationTarget notificationTarget, |
|||
@AuthenticationPrincipal SecurityUser user) throws Exception { |
|||
notificationTarget.setTenantId(user.getTenantId()); |
|||
checkEntity(notificationTarget.getId(), notificationTarget, NOTIFICATION); |
|||
|
|||
NotificationTargetConfig targetConfig = notificationTarget.getConfiguration(); |
|||
if (targetConfig.getType() == NotificationTargetType.PLATFORM_USERS) { |
|||
checkTargetUsers(user, targetConfig); |
|||
} |
|||
|
|||
return doSaveAndLog(EntityType.NOTIFICATION_TARGET, notificationTarget, notificationTargetService::saveNotificationTarget); |
|||
} |
|||
|
|||
@ApiOperation(value = "Get notification target by id (getNotificationTargetById)", |
|||
notes = "Fetch saved notification target by id." + |
|||
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) |
|||
@GetMapping("/target/{id}") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public NotificationTarget getNotificationTargetById(@PathVariable UUID id) throws ThingsboardException { |
|||
NotificationTargetId notificationTargetId = new NotificationTargetId(id); |
|||
return checkEntityId(notificationTargetId, notificationTargetService::findNotificationTargetById, Operation.READ); |
|||
} |
|||
|
|||
@ApiOperation(value = "Get recipients for notification target config (getRecipientsForNotificationTargetConfig)", |
|||
notes = "Get the list (page) of recipients (users) for such notification target configuration." + |
|||
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) |
|||
@PostMapping("/target/recipients") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public PageData<User> getRecipientsForNotificationTargetConfig(@RequestBody NotificationTarget notificationTarget, |
|||
@RequestParam int pageSize, |
|||
@RequestParam int page, |
|||
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { |
|||
// generic permission
|
|||
NotificationTargetConfig targetConfig = notificationTarget.getConfiguration(); |
|||
if (targetConfig.getType() == NotificationTargetType.PLATFORM_USERS) { |
|||
checkTargetUsers(user, targetConfig); |
|||
} else { |
|||
throw new IllegalArgumentException("Target type is not platform users"); |
|||
} |
|||
|
|||
PageLink pageLink = createPageLink(pageSize, page, null, null, null); |
|||
return notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), null, notificationTarget.getConfiguration(), pageLink); |
|||
} |
|||
|
|||
@GetMapping(value = "/targets", params = {"ids"}) |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public List<NotificationTarget> getNotificationTargetsByIds(@RequestParam("ids") UUID[] ids, |
|||
@AuthenticationPrincipal SecurityUser user) { |
|||
// generic permission
|
|||
List<NotificationTargetId> targetsIds = Arrays.stream(ids).map(NotificationTargetId::new).collect(Collectors.toList()); |
|||
return notificationTargetService.findNotificationTargetsByTenantIdAndIds(user.getTenantId(), targetsIds); |
|||
} |
|||
|
|||
@ApiOperation(value = "Get notification targets (getNotificationTargets)", |
|||
notes = "Fetch the page of notification targets owned by sysadmin or tenant." + |
|||
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) |
|||
@GetMapping("/targets") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public PageData<NotificationTarget> getNotificationTargets(@RequestParam int pageSize, |
|||
@RequestParam int page, |
|||
@RequestParam(required = false) String textSearch, |
|||
@RequestParam(required = false) String sortProperty, |
|||
@RequestParam(required = false) String sortOrder, |
|||
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { |
|||
// generic permission
|
|||
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); |
|||
return notificationTargetService.findNotificationTargetsByTenantId(user.getTenantId(), pageLink); |
|||
} |
|||
|
|||
@GetMapping(value = "/targets", params = "notificationType") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public PageData<NotificationTarget> getNotificationTargetsBySupportedNotificationType(@RequestParam int pageSize, |
|||
@RequestParam int page, |
|||
@RequestParam(required = false) String textSearch, |
|||
@RequestParam(required = false) String sortProperty, |
|||
@RequestParam(required = false) String sortOrder, |
|||
@RequestParam(required = false) NotificationType notificationType, |
|||
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { |
|||
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); |
|||
return notificationTargetService.findNotificationTargetsByTenantIdAndSupportedNotificationType(user.getTenantId(), notificationType, pageLink); |
|||
} |
|||
|
|||
@ApiOperation(value = "Delete notification target by id (deleteNotificationTargetById)", |
|||
notes = "Delete notification target by its id.\n\n" + |
|||
"This target cannot be referenced by existing scheduled notification requests or any notification rules." + |
|||
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) |
|||
@DeleteMapping("/target/{id}") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public void deleteNotificationTargetById(@PathVariable UUID id) throws Exception { |
|||
NotificationTargetId notificationTargetId = new NotificationTargetId(id); |
|||
NotificationTarget notificationTarget = checkEntityId(notificationTargetId, notificationTargetService::findNotificationTargetById, Operation.DELETE); |
|||
doDeleteAndLog(EntityType.NOTIFICATION_TARGET, notificationTarget, notificationTargetService::deleteNotificationTargetById); |
|||
} |
|||
|
|||
private void checkTargetUsers(SecurityUser user, NotificationTargetConfig targetConfig) throws ThingsboardException { |
|||
if (user.isSystemAdmin()) { |
|||
return; |
|||
} |
|||
// generic permission for users
|
|||
UsersFilter usersFilter = ((PlatformUsersNotificationTargetConfig) targetConfig).getUsersFilter(); |
|||
switch (usersFilter.getType()) { |
|||
case USER_LIST: |
|||
for (UUID recipientId : ((UserListFilter) usersFilter).getUsersIds()) { |
|||
checkUserId(new UserId(recipientId), Operation.READ); |
|||
} |
|||
break; |
|||
case CUSTOMER_USERS: |
|||
CustomerId customerId = new CustomerId(((CustomerUsersFilter) usersFilter).getCustomerId()); |
|||
checkEntityId(customerId, Operation.READ); |
|||
break; |
|||
case TENANT_ADMINISTRATORS: |
|||
if (CollectionUtils.isNotEmpty(((TenantAdministratorsFilter) usersFilter).getTenantsIds()) || |
|||
CollectionUtils.isNotEmpty(((TenantAdministratorsFilter) usersFilter).getTenantProfilesIds())) { |
|||
throw new AccessDeniedException(""); |
|||
} |
|||
break; |
|||
case SYSTEM_ADMINISTRATORS: |
|||
throw new AccessDeniedException(""); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,151 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.controller; |
|||
|
|||
import io.swagger.annotations.ApiOperation; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.security.core.annotation.AuthenticationPrincipal; |
|||
import org.springframework.web.bind.annotation.DeleteMapping; |
|||
import org.springframework.web.bind.annotation.GetMapping; |
|||
import org.springframework.web.bind.annotation.PathVariable; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.RequestBody; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RequestParam; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
import org.thingsboard.rule.engine.api.slack.SlackService; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardException; |
|||
import org.thingsboard.server.common.data.id.NotificationTemplateId; |
|||
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; |
|||
import org.thingsboard.server.common.data.notification.NotificationType; |
|||
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.slack.SlackConversationType; |
|||
import org.thingsboard.server.common.data.notification.template.NotificationTemplate; |
|||
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; |
|||
import org.thingsboard.server.common.data.page.PageData; |
|||
import org.thingsboard.server.common.data.page.PageLink; |
|||
import org.thingsboard.server.dao.notification.NotificationSettingsService; |
|||
import org.thingsboard.server.dao.notification.NotificationTemplateService; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
import org.thingsboard.server.service.security.permission.Operation; |
|||
|
|||
import javax.validation.Valid; |
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
|
|||
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; |
|||
import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION; |
|||
|
|||
@RestController |
|||
@TbCoreComponent |
|||
@RequiredArgsConstructor |
|||
@RequestMapping("/api/notification") |
|||
public class NotificationTemplateController extends BaseController { |
|||
|
|||
private final NotificationTemplateService notificationTemplateService; |
|||
private final NotificationSettingsService notificationSettingsService; |
|||
private final SlackService slackService; |
|||
|
|||
@ApiOperation(value = "Save notification template (saveNotificationTemplate)", |
|||
notes = "Create or update notification template.\n\n" + |
|||
"Example:\n" + |
|||
"```\n{\n \"name\": \"Hello to all my users\",\n" + |
|||
" \"notificationType\": \"Message from administrator\",\n" + |
|||
" \"configuration\": {\n" + |
|||
" \"defaultTextTemplate\": \"Hello everyone\", # required if any of the templates' bodies is not set\n" + |
|||
" \"templates\": {\n" + |
|||
" \"PUSH\": {\n \"method\": \"PUSH\",\n \"body\": null # defaultTextTemplate will be used if body is not set\n },\n" + |
|||
" \"SMS\": {\n \"method\": \"SMS\",\n \"body\": null\n },\n" + |
|||
" \"EMAIL\": {\n \"method\": \"EMAIL\",\n \"body\": \"Non-default value for email notification: <body>Hello everyone</body>\",\n \"subject\": \"Message from administrator\"\n },\n" + |
|||
" \"SLACK\": {\n \"method\": \"SLACK\",\n \"body\": null,\n \"conversationType\": \"PUBLIC_CHANNEL\",\n \"conversationId\": \"U02LD7BJOU2\" # received from listSlackConversations API method\n }\n" + |
|||
" }\n" + |
|||
" }\n}\n```" + |
|||
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) |
|||
@PostMapping("/template") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public NotificationTemplate saveNotificationTemplate(@RequestBody @Valid NotificationTemplate notificationTemplate) throws Exception { |
|||
notificationTemplate.setTenantId(getTenantId()); |
|||
checkEntity(notificationTemplate.getId(), notificationTemplate, NOTIFICATION); |
|||
return doSaveAndLog(EntityType.NOTIFICATION_TEMPLATE, notificationTemplate, notificationTemplateService::saveNotificationTemplate); |
|||
} |
|||
|
|||
@ApiOperation(value = "Get notification template by id (getNotificationTemplateById)", |
|||
notes = "Fetch notification template by id." + |
|||
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) |
|||
@GetMapping("/template/{id}") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public NotificationTemplate getNotificationTemplateById(@PathVariable UUID id) throws ThingsboardException { |
|||
NotificationTemplateId notificationTemplateId = new NotificationTemplateId(id); |
|||
return checkEntityId(notificationTemplateId, notificationTemplateService::findNotificationTemplateById, Operation.READ); |
|||
} |
|||
|
|||
@ApiOperation(value = "Get notification templates (getNotificationTemplates)", |
|||
notes = "Fetch the page of notification templates owned by sysadmin or tenant." + |
|||
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) |
|||
@GetMapping("/templates") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public PageData<NotificationTemplate> getNotificationTemplates(@RequestParam int pageSize, |
|||
@RequestParam int page, |
|||
@RequestParam(required = false) String textSearch, |
|||
@RequestParam(required = false) String sortProperty, |
|||
@RequestParam(required = false) String sortOrder, |
|||
@RequestParam(required = false) NotificationType[] notificationTypes, |
|||
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { |
|||
// generic permission
|
|||
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); |
|||
if (notificationTypes == null || notificationTypes.length == 0) { |
|||
notificationTypes = NotificationType.values(); |
|||
} |
|||
return notificationTemplateService.findNotificationTemplatesByTenantIdAndNotificationTypes(user.getTenantId(), |
|||
List.of(notificationTypes), pageLink); |
|||
} |
|||
|
|||
@ApiOperation(value = "Delete notification template by id (deleteNotificationTemplateById", |
|||
notes = "Delete notification template by its id.\n\n" + |
|||
"This template cannot be referenced by existing scheduled notification requests or any notification rules." + |
|||
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) |
|||
@DeleteMapping("/template/{id}") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public void deleteNotificationTemplateById(@PathVariable UUID id) throws Exception { |
|||
NotificationTemplateId notificationTemplateId = new NotificationTemplateId(id); |
|||
NotificationTemplate notificationTemplate = checkEntityId(notificationTemplateId, notificationTemplateService::findNotificationTemplateById, Operation.DELETE); |
|||
doDeleteAndLog(EntityType.NOTIFICATION_TEMPLATE, notificationTemplate, notificationTemplateService::deleteNotificationTemplateById); |
|||
} |
|||
|
|||
@ApiOperation(value = "List Slack conversations (listSlackConversations)", |
|||
notes = "List available Slack conversations by type to use in notification template.\n\n" + |
|||
"Slack must be configured in notification settings." + |
|||
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) |
|||
@GetMapping("/slack/conversations") |
|||
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") |
|||
public List<SlackConversation> listSlackConversations(@RequestParam SlackConversationType type, |
|||
@AuthenticationPrincipal SecurityUser user) { |
|||
// generic permission
|
|||
NotificationSettings settings = notificationSettingsService.findNotificationSettings(user.getTenantId()); |
|||
SlackNotificationDeliveryMethodConfig slackConfig = (SlackNotificationDeliveryMethodConfig) |
|||
settings.getDeliveryMethodsConfigs().get(NotificationDeliveryMethod.SLACK); |
|||
if (slackConfig == null) { |
|||
throw new IllegalArgumentException("Slack is not configured"); |
|||
} |
|||
|
|||
return slackService.listConversations(user.getTenantId(), slackConfig.getBotToken(), type); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.apiusage.limits; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; |
|||
|
|||
import java.util.function.Function; |
|||
|
|||
@RequiredArgsConstructor |
|||
public enum LimitedApi { |
|||
|
|||
ENTITY_EXPORT(DefaultTenantProfileConfiguration::getTenantEntityExportRateLimit), |
|||
ENTITY_IMPORT(DefaultTenantProfileConfiguration::getTenantEntityImportRateLimit), |
|||
NOTIFICATION_REQUEST(DefaultTenantProfileConfiguration::getTenantNotificationRequestsRateLimit); |
|||
|
|||
private final Function<DefaultTenantProfileConfiguration, String> configExtractor; |
|||
|
|||
public String getLimitConfig(DefaultTenantProfileConfiguration profileConfiguration) { |
|||
return configExtractor.apply(profileConfiguration); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.executors; |
|||
|
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.common.util.AbstractListeningExecutor; |
|||
|
|||
@Component |
|||
public class NotificationExecutorService extends AbstractListeningExecutor { |
|||
|
|||
@Value("${notification_system.thread_pool_size:10}") |
|||
private int threadPoolSize; |
|||
|
|||
@Override |
|||
protected int getThreadPollSize() { |
|||
return threadPoolSize; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,431 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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; |
|||
|
|||
import com.google.common.util.concurrent.Futures; |
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.common.util.DonAsynchron; |
|||
import org.thingsboard.rule.engine.api.MailService; |
|||
import org.thingsboard.rule.engine.api.NotificationCenter; |
|||
import org.thingsboard.rule.engine.api.SmsService; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
import org.thingsboard.server.common.data.User; |
|||
import org.thingsboard.server.common.data.id.NotificationId; |
|||
import org.thingsboard.server.common.data.id.NotificationRequestId; |
|||
import org.thingsboard.server.common.data.id.NotificationTargetId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.id.UserId; |
|||
import org.thingsboard.server.common.data.notification.AlreadySentException; |
|||
import org.thingsboard.server.common.data.notification.Notification; |
|||
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; |
|||
import org.thingsboard.server.common.data.notification.NotificationRequest; |
|||
import org.thingsboard.server.common.data.notification.NotificationRequestConfig; |
|||
import org.thingsboard.server.common.data.notification.NotificationRequestStats; |
|||
import org.thingsboard.server.common.data.notification.NotificationRequestStatus; |
|||
import org.thingsboard.server.common.data.notification.NotificationStatus; |
|||
import org.thingsboard.server.common.data.notification.NotificationType; |
|||
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.settings.NotificationSettings; |
|||
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; |
|||
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; |
|||
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; |
|||
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType; |
|||
import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig; |
|||
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; |
|||
import org.thingsboard.server.common.data.notification.template.NotificationTemplate; |
|||
import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate; |
|||
import org.thingsboard.server.common.data.page.PageDataIterable; |
|||
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; |
|||
import org.thingsboard.server.common.msg.queue.ServiceType; |
|||
import org.thingsboard.server.common.msg.queue.TbCallback; |
|||
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; |
|||
import org.thingsboard.server.common.msg.tools.TbRateLimitsException; |
|||
import org.thingsboard.server.dao.notification.NotificationRequestService; |
|||
import org.thingsboard.server.dao.notification.NotificationService; |
|||
import org.thingsboard.server.dao.notification.NotificationSettingsService; |
|||
import org.thingsboard.server.dao.notification.NotificationTargetService; |
|||
import org.thingsboard.server.dao.notification.NotificationTemplateService; |
|||
import org.thingsboard.server.dao.user.UserService; |
|||
import org.thingsboard.server.gen.transport.TransportProtos; |
|||
import org.thingsboard.server.queue.common.TbProtoQueueMsg; |
|||
import org.thingsboard.server.queue.discovery.NotificationsTopicService; |
|||
import org.thingsboard.server.queue.provider.TbQueueProducerProvider; |
|||
import org.thingsboard.server.service.apiusage.limits.LimitedApi; |
|||
import org.thingsboard.server.service.apiusage.limits.RateLimitService; |
|||
import org.thingsboard.server.service.executors.DbCallbackExecutorService; |
|||
import org.thingsboard.server.service.executors.NotificationExecutorService; |
|||
import org.thingsboard.server.service.notification.channels.NotificationChannel; |
|||
import org.thingsboard.server.service.subscription.TbSubscriptionUtils; |
|||
import org.thingsboard.server.service.telemetry.AbstractSubscriptionService; |
|||
import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate; |
|||
import org.thingsboard.server.service.ws.notification.sub.NotificationUpdate; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.Collections; |
|||
import java.util.HashSet; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Set; |
|||
import java.util.UUID; |
|||
import java.util.stream.Collectors; |
|||
|
|||
@Service |
|||
@Slf4j |
|||
@RequiredArgsConstructor |
|||
@SuppressWarnings({"UnstableApiUsage", "rawtypes"}) |
|||
public class DefaultNotificationCenter extends AbstractSubscriptionService implements NotificationCenter, NotificationChannel<User, WebDeliveryMethodNotificationTemplate> { |
|||
|
|||
private final NotificationTargetService notificationTargetService; |
|||
private final NotificationRequestService notificationRequestService; |
|||
private final NotificationService notificationService; |
|||
private final NotificationTemplateService notificationTemplateService; |
|||
private final NotificationSettingsService notificationSettingsService; |
|||
private final UserService userService; |
|||
private final NotificationExecutorService notificationExecutor; |
|||
private final DbCallbackExecutorService dbCallbackExecutorService; |
|||
private final NotificationsTopicService notificationsTopicService; |
|||
private final TbQueueProducerProvider producerProvider; |
|||
private final RateLimitService rateLimitService; |
|||
private final MailService mailService; |
|||
private final SmsService smsService; |
|||
|
|||
private Map<NotificationDeliveryMethod, NotificationChannel> channels; |
|||
|
|||
|
|||
@Override |
|||
public NotificationRequest processNotificationRequest(TenantId tenantId, NotificationRequest notificationRequest) { |
|||
if (!rateLimitService.checkRateLimit(tenantId, LimitedApi.NOTIFICATION_REQUEST)) { |
|||
throw new TbRateLimitsException(EntityType.TENANT); |
|||
} |
|||
NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId); |
|||
NotificationTemplate notificationTemplate; |
|||
if (notificationRequest.getTemplateId() != null) { |
|||
notificationTemplate = notificationTemplateService.findNotificationTemplateById(tenantId, notificationRequest.getTemplateId()); |
|||
} else { |
|||
notificationTemplate = notificationRequest.getTemplate(); |
|||
} |
|||
if (notificationTemplate == null) throw new IllegalArgumentException("Template is missing"); |
|||
|
|||
List<NotificationTarget> targets = notificationRequest.getTargets().stream().map(NotificationTargetId::new) |
|||
.map(id -> notificationTargetService.findNotificationTargetById(tenantId, id)).collect(Collectors.toList()); |
|||
Set<NotificationDeliveryMethod> availableDeliveryMethods = getAvailableDeliveryMethods(tenantId); |
|||
|
|||
notificationTemplate.getConfiguration().getDeliveryMethodsTemplates().forEach((deliveryMethod, template) -> { |
|||
if (!template.isEnabled()) return; |
|||
if (!availableDeliveryMethods.contains(deliveryMethod)) { |
|||
throw new IllegalArgumentException("Settings for " + deliveryMethod.getName() + " are missing"); |
|||
} |
|||
if (notificationRequest.getRuleId() == null) { |
|||
if (targets.stream().noneMatch(target -> target.getConfiguration().getType().getSupportedDeliveryMethods().contains(deliveryMethod))) { |
|||
throw new IllegalArgumentException("Target for " + deliveryMethod.getName() + " delivery method is missing"); |
|||
} |
|||
} |
|||
}); |
|||
|
|||
if (notificationRequest.getAdditionalConfig() != null) { |
|||
NotificationRequestConfig config = notificationRequest.getAdditionalConfig(); |
|||
if (config.getSendingDelayInSec() > 0 && notificationRequest.getId() == null) { |
|||
notificationRequest.setStatus(NotificationRequestStatus.SCHEDULED); |
|||
NotificationRequest savedNotificationRequest = notificationRequestService.saveNotificationRequest(tenantId, notificationRequest); |
|||
forwardToNotificationSchedulerService(tenantId, savedNotificationRequest.getId()); |
|||
return savedNotificationRequest; |
|||
} |
|||
} |
|||
|
|||
log.debug("Processing notification request (tenantId: {}, targets: {})", tenantId, notificationRequest.getTargets()); |
|||
notificationRequest.setStatus(NotificationRequestStatus.PROCESSING); |
|||
NotificationRequest savedNotificationRequest = notificationRequestService.saveNotificationRequest(tenantId, notificationRequest); |
|||
|
|||
NotificationProcessingContext ctx = NotificationProcessingContext.builder() |
|||
.tenantId(tenantId) |
|||
.request(savedNotificationRequest) |
|||
.settings(settings) |
|||
.template(notificationTemplate) |
|||
.build(); |
|||
|
|||
notificationExecutor.submit(() -> { |
|||
List<ListenableFuture<Void>> results = new ArrayList<>(); |
|||
|
|||
for (NotificationTarget target : targets) { |
|||
List<ListenableFuture<Void>> result = processForTarget(target, ctx); |
|||
results.addAll(result); |
|||
} |
|||
|
|||
Futures.whenAllComplete(results).run(() -> { |
|||
NotificationRequestId requestId = savedNotificationRequest.getId(); |
|||
log.debug("[{}] Notification request processing is finished", requestId); |
|||
NotificationRequestStats stats = ctx.getStats(); |
|||
try { |
|||
notificationRequestService.updateNotificationRequest(tenantId, requestId, NotificationRequestStatus.SENT, stats); |
|||
} catch (Exception e) { |
|||
log.error("[{}] Failed to update stats for notification request", requestId, e); |
|||
} |
|||
}, dbCallbackExecutorService); |
|||
}); |
|||
|
|||
return savedNotificationRequest; |
|||
} |
|||
|
|||
private List<ListenableFuture<Void>> processForTarget(NotificationTarget target, NotificationProcessingContext ctx) { |
|||
Iterable<? extends NotificationRecipient> recipients; |
|||
switch (target.getConfiguration().getType()) { |
|||
case PLATFORM_USERS: { |
|||
PlatformUsersNotificationTargetConfig platformUsersTargetConfig = (PlatformUsersNotificationTargetConfig) target.getConfiguration(); |
|||
if (platformUsersTargetConfig.getUsersFilter().getType() == UsersFilterType.AFFECTED_USER) { |
|||
if (ctx.getRequest().getInfo() instanceof RuleOriginatedNotificationInfo) { |
|||
UserId targetUserId = ((RuleOriginatedNotificationInfo) ctx.getRequest().getInfo()).getTargetUserId(); |
|||
if (targetUserId != null) { |
|||
recipients = List.of(userService.findUserById(ctx.getTenantId(), targetUserId)); |
|||
break; |
|||
} |
|||
} |
|||
recipients = Collections.emptyList(); |
|||
} else { |
|||
recipients = new PageDataIterable<>(pageLink -> { |
|||
return notificationTargetService.findRecipientsForNotificationTargetConfig(ctx.getTenantId(), ctx.getCustomerId(), platformUsersTargetConfig, pageLink); |
|||
}, 500); |
|||
} |
|||
break; |
|||
} |
|||
case SLACK: { |
|||
SlackNotificationTargetConfig slackTargetConfig = (SlackNotificationTargetConfig) target.getConfiguration(); |
|||
recipients = List.of(slackTargetConfig.getConversation()); |
|||
break; |
|||
} |
|||
default: { |
|||
recipients = Collections.emptyList(); |
|||
} |
|||
} |
|||
|
|||
Set<NotificationDeliveryMethod> deliveryMethods = new HashSet<>(ctx.getDeliveryMethods()); |
|||
deliveryMethods.removeIf(deliveryMethod -> !target.getConfiguration().getType().getSupportedDeliveryMethods().contains(deliveryMethod)); |
|||
log.debug("[{}] Processing notification request for {} target ({}) for delivery methods {}", ctx.getRequest().getId(), target.getConfiguration().getType(), target.getId(), deliveryMethods); |
|||
|
|||
List<ListenableFuture<Void>> results = new ArrayList<>(); |
|||
if (!deliveryMethods.isEmpty()) { |
|||
for (NotificationRecipient recipient : recipients) { |
|||
for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) { |
|||
ListenableFuture<Void> resultFuture = processForRecipient(deliveryMethod, recipient, ctx); |
|||
DonAsynchron.withCallback(resultFuture, result -> { |
|||
ctx.getStats().reportSent(deliveryMethod, recipient); |
|||
}, error -> { |
|||
ctx.getStats().reportError(deliveryMethod, error, recipient); |
|||
}); |
|||
results.add(resultFuture); |
|||
} |
|||
} |
|||
} |
|||
return results; |
|||
} |
|||
|
|||
private ListenableFuture<Void> processForRecipient(NotificationDeliveryMethod deliveryMethod, NotificationRecipient recipient, NotificationProcessingContext ctx) { |
|||
if (ctx.getStats().contains(deliveryMethod, recipient.getId())) { |
|||
return Futures.immediateFailedFuture(new AlreadySentException()); |
|||
} |
|||
Map<String, String> templateContext; |
|||
if (recipient instanceof User) { |
|||
templateContext = ctx.createTemplateContext(((User) recipient)); |
|||
} else { |
|||
templateContext = Collections.emptyMap(); |
|||
} |
|||
DeliveryMethodNotificationTemplate processedTemplate; |
|||
try { |
|||
processedTemplate = ctx.getProcessedTemplate(deliveryMethod, templateContext); |
|||
} catch (Exception e) { |
|||
return Futures.immediateFailedFuture(e); |
|||
} |
|||
|
|||
NotificationChannel notificationChannel = channels.get(deliveryMethod); |
|||
log.trace("[{}] Sending {} notification for recipient {}", ctx.getRequest().getId(), deliveryMethod, recipient); |
|||
return notificationChannel.sendNotification(recipient, processedTemplate, ctx); |
|||
} |
|||
|
|||
@Override |
|||
public ListenableFuture<Void> sendNotification(User recipient, WebDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) { |
|||
NotificationRequest request = ctx.getRequest(); |
|||
Notification notification = Notification.builder() |
|||
.requestId(request.getId()) |
|||
.recipientId(recipient.getId()) |
|||
.type(ctx.getNotificationTemplate().getNotificationType()) |
|||
.subject(processedTemplate.getSubject()) |
|||
.text(processedTemplate.getBody()) |
|||
.additionalConfig(processedTemplate.getAdditionalConfig()) |
|||
.info(request.getInfo()) |
|||
.status(NotificationStatus.SENT) |
|||
.build(); |
|||
try { |
|||
notification = notificationService.saveNotification(recipient.getTenantId(), notification); |
|||
} catch (Exception e) { |
|||
log.error("Failed to create notification for recipient {}", recipient.getId(), e); |
|||
return Futures.immediateFailedFuture(e); |
|||
} |
|||
|
|||
NotificationUpdate update = NotificationUpdate.builder() |
|||
.created(true) |
|||
.notification(notification) |
|||
.build(); |
|||
return onNotificationUpdate(recipient.getTenantId(), recipient.getId(), update); |
|||
} |
|||
|
|||
@Override |
|||
public void sendBasicNotification(TenantId tenantId, UserId recipientId, String subject, String text) { |
|||
Notification notification = Notification.builder() |
|||
.recipientId(recipientId) |
|||
.type(NotificationType.GENERAL) |
|||
.subject(subject) |
|||
.text(text) |
|||
.status(NotificationStatus.SENT) |
|||
.build(); |
|||
notification = notificationService.saveNotification(TenantId.SYS_TENANT_ID, notification); |
|||
|
|||
NotificationUpdate update = NotificationUpdate.builder() |
|||
.created(true) |
|||
.notification(notification) |
|||
.build(); |
|||
onNotificationUpdate(tenantId, recipientId, update); |
|||
} |
|||
|
|||
@Override |
|||
public void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId) { |
|||
boolean updated = notificationService.markNotificationAsRead(tenantId, recipientId, notificationId); |
|||
if (updated) { |
|||
log.trace("Marked notification {} as read (recipient id: {}, tenant id: {})", notificationId, recipientId, tenantId); |
|||
NotificationUpdate update = NotificationUpdate.builder() |
|||
.updated(true) |
|||
.notificationId(notificationId) |
|||
.newStatus(NotificationStatus.READ) |
|||
.build(); |
|||
onNotificationUpdate(tenantId, recipientId, update); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void markAllNotificationsAsRead(TenantId tenantId, UserId recipientId) { |
|||
int updatedCount = notificationService.markAllNotificationsAsRead(tenantId, recipientId); |
|||
if (updatedCount > 0) { |
|||
log.trace("Marked all notifications as read (recipient id: {}, tenant id: {})", recipientId, tenantId); |
|||
NotificationUpdate update = NotificationUpdate.builder() |
|||
.updated(true) |
|||
.allNotifications(true) |
|||
.newStatus(NotificationStatus.READ) |
|||
.build(); |
|||
onNotificationUpdate(tenantId, recipientId, update); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId) { |
|||
Notification notification = notificationService.findNotificationById(tenantId, notificationId); |
|||
boolean deleted = notificationService.deleteNotification(tenantId, recipientId, notificationId); |
|||
if (deleted) { |
|||
NotificationUpdate update = NotificationUpdate.builder() |
|||
.deleted(true) |
|||
.notification(notification) |
|||
.build(); |
|||
onNotificationUpdate(tenantId, recipientId, update); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Set<NotificationDeliveryMethod> getAvailableDeliveryMethods(TenantId tenantId) { |
|||
Set<NotificationDeliveryMethod> deliveryMethods = new HashSet<>(); |
|||
deliveryMethods.add(NotificationDeliveryMethod.WEB); |
|||
NotificationSettings notificationSettings = notificationSettingsService.findNotificationSettings(tenantId); |
|||
if (notificationSettings.getDeliveryMethodsConfigs().containsKey(NotificationDeliveryMethod.SLACK)) { |
|||
deliveryMethods.add(NotificationDeliveryMethod.SLACK); |
|||
} |
|||
try { |
|||
mailService.testConnection(tenantId); |
|||
deliveryMethods.add(NotificationDeliveryMethod.EMAIL); |
|||
} catch (Exception e) {} |
|||
if (smsService.isConfigured(tenantId)) { |
|||
deliveryMethods.add(NotificationDeliveryMethod.SMS); |
|||
} |
|||
return deliveryMethods; |
|||
} |
|||
|
|||
@Override |
|||
public void deleteNotificationRequest(TenantId tenantId, NotificationRequestId notificationRequestId) { |
|||
log.debug("Deleting notification request {}", notificationRequestId); |
|||
NotificationRequest notificationRequest = notificationRequestService.findNotificationRequestById(tenantId, notificationRequestId); |
|||
notificationRequestService.deleteNotificationRequest(tenantId, notificationRequestId); |
|||
|
|||
if (notificationRequest.isSent()) { |
|||
// TODO: no need to send request update for other than PLATFORM_USERS target type
|
|||
onNotificationRequestUpdate(tenantId, NotificationRequestUpdate.builder() |
|||
.notificationRequestId(notificationRequestId) |
|||
.deleted(true) |
|||
.build()); |
|||
} else if (notificationRequest.isScheduled()) { |
|||
clusterService.broadcastEntityStateChangeEvent(tenantId, notificationRequestId, ComponentLifecycleEvent.DELETED); |
|||
} |
|||
} |
|||
|
|||
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) { |
|||
log.trace("Submitting notification update for recipient {}: {}", recipientId, update); |
|||
return Futures.submit(() -> { |
|||
forwardToSubscriptionManagerService(tenantId, recipientId, subscriptionManagerService -> { |
|||
subscriptionManagerService.onNotificationUpdate(tenantId, recipientId, update, TbCallback.EMPTY); |
|||
}, () -> TbSubscriptionUtils.notificationUpdateToProto(tenantId, recipientId, update)); |
|||
}, wsCallBackExecutor); |
|||
} |
|||
|
|||
private void onNotificationRequestUpdate(TenantId tenantId, NotificationRequestUpdate update) { |
|||
log.trace("Submitting notification request update: {}", update); |
|||
wsCallBackExecutor.submit(() -> { |
|||
TransportProtos.ToCoreNotificationMsg notificationRequestUpdateProto = TbSubscriptionUtils.notificationRequestUpdateToProto(tenantId, update); |
|||
Set<String> coreServices = new HashSet<>(partitionService.getAllServiceIds(ServiceType.TB_CORE)); |
|||
for (String serviceId : coreServices) { |
|||
TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceId); |
|||
producerProvider.getTbCoreNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), notificationRequestUpdateProto), null); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
@Override |
|||
public NotificationDeliveryMethod getDeliveryMethod() { |
|||
return NotificationDeliveryMethod.WEB; |
|||
} |
|||
|
|||
@Override |
|||
protected String getExecutorPrefix() { |
|||
return "notification"; |
|||
} |
|||
|
|||
@Autowired |
|||
public void setChannels(List<NotificationChannel> channels, NotificationCenter webNotificationChannel) { |
|||
this.channels = channels.stream().collect(Collectors.toMap(NotificationChannel::getDeliveryMethod, c -> c)); |
|||
this.channels.put(NotificationDeliveryMethod.WEB, (NotificationChannel) webNotificationChannel); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,176 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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; |
|||
|
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import lombok.Data; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.context.event.EventListener; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.rule.engine.api.NotificationCenter; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.NotificationRequestId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.id.UserId; |
|||
import org.thingsboard.server.common.data.notification.NotificationRequest; |
|||
import org.thingsboard.server.common.data.notification.NotificationRequestConfig; |
|||
import org.thingsboard.server.common.data.page.PageDataIterable; |
|||
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; |
|||
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; |
|||
import org.thingsboard.server.common.msg.queue.ServiceType; |
|||
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; |
|||
import org.thingsboard.server.dao.notification.NotificationRequestService; |
|||
import org.thingsboard.server.queue.scheduler.SchedulerComponent; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
import org.thingsboard.server.service.executors.NotificationExecutorService; |
|||
import org.thingsboard.server.service.partition.AbstractPartitionBasedService; |
|||
|
|||
import javax.annotation.PostConstruct; |
|||
import java.util.Collections; |
|||
import java.util.HashSet; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Optional; |
|||
import java.util.Set; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import java.util.concurrent.ScheduledFuture; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
@TbCoreComponent |
|||
@Service |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
@SuppressWarnings("UnstableApiUsage") |
|||
public class DefaultNotificationSchedulerService extends AbstractPartitionBasedService<NotificationRequestId> implements NotificationSchedulerService { |
|||
|
|||
private final NotificationCenter notificationCenter; |
|||
private final NotificationRequestService notificationRequestService; |
|||
private final SchedulerComponent scheduler; |
|||
private final NotificationExecutorService notificationExecutor; |
|||
|
|||
private final Map<NotificationRequestId, ScheduledRequestMetadata> scheduledNotificationRequests = new ConcurrentHashMap<>(); |
|||
|
|||
@PostConstruct |
|||
public void init() { |
|||
super.init(); |
|||
} |
|||
|
|||
@Override |
|||
protected Map<TopicPartitionInfo, List<ListenableFuture<?>>> onAddedPartitions(Set<TopicPartitionInfo> addedPartitions) { |
|||
PageDataIterable<NotificationRequest> notificationRequests = new PageDataIterable<>(pageLink -> { |
|||
return notificationRequestService.findScheduledNotificationRequests(pageLink); |
|||
}, 1000); |
|||
for (NotificationRequest notificationRequest : notificationRequests) { |
|||
TopicPartitionInfo requestPartition = partitionService.resolve(ServiceType.TB_CORE, notificationRequest.getTenantId(), notificationRequest.getId()); |
|||
if (addedPartitions.contains(requestPartition)) { |
|||
partitionedEntities.computeIfAbsent(requestPartition, k -> ConcurrentHashMap.newKeySet()).add(notificationRequest.getId()); |
|||
if (!scheduledNotificationRequests.containsKey(notificationRequest.getId())) { |
|||
scheduleNotificationRequest(notificationRequest.getTenantId(), notificationRequest, notificationRequest.getCreatedTime()); |
|||
} |
|||
} |
|||
} |
|||
return Collections.emptyMap(); |
|||
} |
|||
|
|||
@Override |
|||
public void scheduleNotificationRequest(TenantId tenantId, NotificationRequestId notificationRequestId, long requestTs) { |
|||
NotificationRequest notificationRequest = notificationRequestService.findNotificationRequestById(tenantId, notificationRequestId); |
|||
scheduleNotificationRequest(tenantId, notificationRequest, requestTs); |
|||
} |
|||
|
|||
private void scheduleNotificationRequest(TenantId tenantId, NotificationRequest request, long requestTs) { |
|||
int delayInSec = Optional.ofNullable(request) |
|||
.map(NotificationRequest::getAdditionalConfig) |
|||
.map(NotificationRequestConfig::getSendingDelayInSec) |
|||
.orElse(0); |
|||
if (delayInSec <= 0) return; |
|||
long delayInMs = TimeUnit.SECONDS.toMillis(delayInSec) - (System.currentTimeMillis() - requestTs); |
|||
if (delayInMs < 0) { |
|||
delayInMs = 0; |
|||
} |
|||
|
|||
ScheduledFuture<?> scheduledTask = scheduler.schedule(() -> { |
|||
NotificationRequest notificationRequest = notificationRequestService.findNotificationRequestById(tenantId, request.getId()); |
|||
if (notificationRequest == null) return; |
|||
|
|||
notificationExecutor.executeAsync(() -> { |
|||
try { |
|||
notificationCenter.processNotificationRequest(tenantId, notificationRequest); |
|||
} catch (Exception e) { |
|||
log.error("Failed to process scheduled notification request {}", notificationRequest.getId(), e); |
|||
UserId senderId = notificationRequest.getSenderId(); |
|||
if (senderId != null) { |
|||
notificationCenter.sendBasicNotification(tenantId, senderId, "Notification failure", |
|||
"Failed to process scheduled notification (request " + notificationRequest.getId() + "): " + e.getMessage()); |
|||
} |
|||
} |
|||
}); |
|||
scheduledNotificationRequests.remove(notificationRequest.getId()); |
|||
}, delayInMs, TimeUnit.MILLISECONDS); |
|||
scheduledNotificationRequests.put(request.getId(), new ScheduledRequestMetadata(tenantId, scheduledTask)); |
|||
} |
|||
|
|||
@EventListener(ComponentLifecycleMsg.class) |
|||
public void handleComponentLifecycleEvent(ComponentLifecycleMsg event) { |
|||
if (event.getEvent() == ComponentLifecycleEvent.DELETED) { |
|||
EntityId entityId = event.getEntityId(); |
|||
switch (entityId.getEntityType()) { |
|||
case NOTIFICATION_REQUEST: |
|||
cancelAndRemove((NotificationRequestId) entityId); |
|||
break; |
|||
case TENANT: |
|||
Set<NotificationRequestId> toCancel = new HashSet<>(); |
|||
scheduledNotificationRequests.forEach((notificationRequestId, scheduledRequestMetadata) -> { |
|||
if (scheduledRequestMetadata.getTenantId().equals(entityId)) { |
|||
toCancel.add(notificationRequestId); |
|||
} |
|||
}); |
|||
toCancel.forEach(this::cancelAndRemove); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
protected void cleanupEntityOnPartitionRemoval(NotificationRequestId notificationRequestId) { |
|||
cancelAndRemove(notificationRequestId); |
|||
} |
|||
|
|||
private void cancelAndRemove(NotificationRequestId notificationRequestId) { |
|||
ScheduledRequestMetadata md = scheduledNotificationRequests.remove(notificationRequestId); |
|||
if (md != null) { |
|||
md.getFuture().cancel(false); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
protected String getServiceName() { |
|||
return "Notifications scheduler"; |
|||
} |
|||
|
|||
@Override |
|||
protected String getSchedulerExecutorName() { |
|||
return "notifications-scheduler"; |
|||
} |
|||
|
|||
@Data |
|||
private static class ScheduledRequestMetadata { |
|||
private final TenantId tenantId; |
|||
private final ScheduledFuture<?> future; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,163 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import com.fasterxml.jackson.databind.node.ObjectNode; |
|||
import com.fasterxml.jackson.databind.node.TextNode; |
|||
import com.google.common.base.Strings; |
|||
import lombok.Builder; |
|||
import lombok.Getter; |
|||
import org.apache.commons.lang3.StringUtils; |
|||
import org.thingsboard.server.common.data.User; |
|||
import org.thingsboard.server.common.data.id.CustomerId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; |
|||
import org.thingsboard.server.common.data.notification.NotificationRequest; |
|||
import org.thingsboard.server.common.data.notification.NotificationRequestStats; |
|||
import org.thingsboard.server.common.data.notification.info.NotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.settings.NotificationDeliveryMethodConfig; |
|||
import org.thingsboard.server.common.data.notification.settings.NotificationSettings; |
|||
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; |
|||
import org.thingsboard.server.common.data.notification.template.HasSubject; |
|||
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.WebDeliveryMethodNotificationTemplate; |
|||
|
|||
import java.util.EnumMap; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
import java.util.Optional; |
|||
import java.util.Set; |
|||
import java.util.regex.Pattern; |
|||
|
|||
@SuppressWarnings("unchecked") |
|||
public class NotificationProcessingContext { |
|||
|
|||
@Getter |
|||
private final TenantId tenantId; |
|||
private final NotificationSettings settings; |
|||
@Getter |
|||
private final NotificationRequest request; |
|||
|
|||
@Getter |
|||
private final NotificationTemplate notificationTemplate; |
|||
private final Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> templates; |
|||
@Getter |
|||
private Set<NotificationDeliveryMethod> deliveryMethods; |
|||
@Getter |
|||
private final NotificationRequestStats stats; |
|||
|
|||
private static final Pattern TEMPLATE_PARAM_PATTERN = Pattern.compile("\\$\\{([a-zA-Z]+)(:[a-zA-Z]+)?}"); |
|||
|
|||
@Builder |
|||
public NotificationProcessingContext(TenantId tenantId, NotificationRequest request, NotificationSettings settings, |
|||
NotificationTemplate template) { |
|||
this.tenantId = tenantId; |
|||
this.request = request; |
|||
this.settings = settings; |
|||
this.notificationTemplate = template; |
|||
this.templates = new EnumMap<>(NotificationDeliveryMethod.class); |
|||
this.stats = new NotificationRequestStats(); |
|||
init(); |
|||
} |
|||
|
|||
private void init() { |
|||
NotificationTemplateConfig templateConfig = notificationTemplate.getConfiguration(); |
|||
templateConfig.getDeliveryMethodsTemplates().forEach((deliveryMethod, template) -> { |
|||
if (template.isEnabled()) { |
|||
templates.put(deliveryMethod, template); |
|||
} |
|||
}); |
|||
deliveryMethods = templates.keySet(); |
|||
} |
|||
|
|||
public <C extends NotificationDeliveryMethodConfig> C getDeliveryMethodConfig(NotificationDeliveryMethod deliveryMethod) { |
|||
return (C) settings.getDeliveryMethodsConfigs().get(deliveryMethod); |
|||
} |
|||
|
|||
public <T extends DeliveryMethodNotificationTemplate> T getProcessedTemplate(NotificationDeliveryMethod deliveryMethod, Map<String, String> templateContext) { |
|||
NotificationInfo info = request.getInfo(); |
|||
if (info != null) { |
|||
templateContext = new HashMap<>(templateContext); |
|||
templateContext.putAll(info.getTemplateData()); |
|||
} |
|||
|
|||
T template = (T) templates.get(deliveryMethod).copy(); |
|||
template.setBody(processTemplate(template.getBody(), templateContext)); |
|||
if (template instanceof HasSubject) { |
|||
String subject = ((HasSubject) template).getSubject(); |
|||
((HasSubject) template).setSubject(processTemplate(subject, templateContext)); |
|||
} |
|||
|
|||
if (deliveryMethod == NotificationDeliveryMethod.WEB) { |
|||
WebDeliveryMethodNotificationTemplate webNotificationTemplate = (WebDeliveryMethodNotificationTemplate) template; |
|||
Optional<ObjectNode> buttonConfig = Optional.ofNullable(webNotificationTemplate.getAdditionalConfig()) |
|||
.map(config -> config.get("actionButtonConfig")).filter(JsonNode::isObject) |
|||
.map(config -> (ObjectNode) config); |
|||
if (buttonConfig.isPresent()) { |
|||
JsonNode text = buttonConfig.get().get("text"); |
|||
if (text != null && text.isTextual()) { |
|||
text = new TextNode(processTemplate(text.asText(), templateContext)); |
|||
buttonConfig.get().set("text", text); |
|||
} |
|||
JsonNode link = buttonConfig.get().get("link"); |
|||
if (link != null && link.isTextual()) { |
|||
link = new TextNode(processTemplate(link.asText(), templateContext)); |
|||
buttonConfig.get().set("link", link); |
|||
} |
|||
} |
|||
} |
|||
return template; |
|||
} |
|||
|
|||
private static String processTemplate(String template, Map<String, String> context) { |
|||
return TEMPLATE_PARAM_PATTERN.matcher(template).replaceAll(matchResult -> { |
|||
String key = matchResult.group(1); |
|||
String value = Strings.nullToEmpty(context.get(key)); |
|||
String function = matchResult.group(2); |
|||
if (function != null) { |
|||
switch (function) { |
|||
case ":upperCase": |
|||
return value.toUpperCase(); |
|||
case ":lowerCase": |
|||
return value.toLowerCase(); |
|||
case ":capitalize": |
|||
return StringUtils.capitalize(value.toLowerCase()); |
|||
} |
|||
} |
|||
return value; |
|||
}); |
|||
} |
|||
|
|||
public Map<String, String> createTemplateContext(User recipient) { |
|||
Map<String, String> templateContext = new HashMap<>(); |
|||
templateContext.put("recipientEmail", recipient.getEmail()); |
|||
templateContext.put("recipientFirstName", Strings.nullToEmpty(recipient.getFirstName())); |
|||
templateContext.put("recipientLastName", Strings.nullToEmpty(recipient.getLastName())); |
|||
return templateContext; |
|||
} |
|||
|
|||
public CustomerId getCustomerId() { |
|||
if (request.getInfo() instanceof RuleOriginatedNotificationInfo) { |
|||
return ((RuleOriginatedNotificationInfo) request.getInfo()).getOriginatorEntityCustomerId(); |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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; |
|||
|
|||
import org.thingsboard.server.common.data.id.NotificationRequestId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
|
|||
public interface NotificationSchedulerService { |
|||
|
|||
void scheduleNotificationRequest(TenantId tenantId, NotificationRequestId notificationRequestId, long requestTs); |
|||
|
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.google.common.util.concurrent.ListenableFuture; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.rule.engine.api.MailService; |
|||
import org.thingsboard.server.common.data.User; |
|||
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; |
|||
import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; |
|||
import org.thingsboard.server.service.mail.MailExecutorService; |
|||
import org.thingsboard.server.service.notification.NotificationProcessingContext; |
|||
|
|||
@Component |
|||
@RequiredArgsConstructor |
|||
public class EmailNotificationChannel implements NotificationChannel<User, EmailDeliveryMethodNotificationTemplate> { |
|||
|
|||
private final MailService mailService; |
|||
private final MailExecutorService executor; |
|||
|
|||
@Override |
|||
public ListenableFuture<Void> sendNotification(User recipient, EmailDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) { |
|||
return executor.submit(() -> { |
|||
mailService.sendEmail(recipient.getTenantId(), recipient.getEmail(), processedTemplate.getSubject(), processedTemplate.getBody()); |
|||
return null; |
|||
}); |
|||
} |
|||
|
|||
@Override |
|||
public NotificationDeliveryMethod getDeliveryMethod() { |
|||
return NotificationDeliveryMethod.EMAIL; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.google.common.util.concurrent.ListenableFuture; |
|||
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; |
|||
import org.thingsboard.server.service.notification.NotificationProcessingContext; |
|||
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; |
|||
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; |
|||
|
|||
public interface NotificationChannel<R extends NotificationRecipient, T extends DeliveryMethodNotificationTemplate> { |
|||
|
|||
ListenableFuture<Void> sendNotification(R recipient, T processedTemplate, NotificationProcessingContext ctx); |
|||
|
|||
NotificationDeliveryMethod getDeliveryMethod(); |
|||
|
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.google.common.util.concurrent.ListenableFuture; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.rule.engine.api.slack.SlackService; |
|||
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; |
|||
import org.thingsboard.server.service.notification.NotificationProcessingContext; |
|||
import org.thingsboard.server.common.data.notification.settings.SlackNotificationDeliveryMethodConfig; |
|||
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation; |
|||
import org.thingsboard.server.common.data.notification.template.SlackDeliveryMethodNotificationTemplate; |
|||
import org.thingsboard.server.service.executors.ExternalCallExecutorService; |
|||
|
|||
@Component |
|||
@RequiredArgsConstructor |
|||
public class SlackNotificationChannel implements NotificationChannel<SlackConversation, SlackDeliveryMethodNotificationTemplate> { |
|||
|
|||
private final SlackService slackService; |
|||
private final ExternalCallExecutorService executor; |
|||
|
|||
@Override |
|||
public ListenableFuture<Void> sendNotification(SlackConversation conversation, SlackDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) { |
|||
SlackNotificationDeliveryMethodConfig config = ctx.getDeliveryMethodConfig(NotificationDeliveryMethod.SLACK); |
|||
return executor.submit(() -> { |
|||
slackService.sendMessage(ctx.getTenantId(), config.getBotToken(), conversation.getId(), processedTemplate.getBody()); |
|||
return null; |
|||
}); |
|||
} |
|||
|
|||
@Override |
|||
public NotificationDeliveryMethod getDeliveryMethod() { |
|||
return NotificationDeliveryMethod.SLACK; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.google.common.util.concurrent.Futures; |
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.apache.commons.lang3.StringUtils; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.rule.engine.api.SmsService; |
|||
import org.thingsboard.server.common.data.User; |
|||
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; |
|||
import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate; |
|||
import org.thingsboard.server.service.notification.NotificationProcessingContext; |
|||
import org.thingsboard.server.service.sms.SmsExecutorService; |
|||
|
|||
@Component |
|||
@RequiredArgsConstructor |
|||
public class SmsNotificationChannel implements NotificationChannel<User, SmsDeliveryMethodNotificationTemplate> { |
|||
|
|||
private final SmsService smsService; |
|||
private final SmsExecutorService executor; |
|||
|
|||
@Override |
|||
public ListenableFuture<Void> sendNotification(User recipient, SmsDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) { |
|||
String phone = recipient.getPhone(); |
|||
if (StringUtils.isBlank(phone)) { |
|||
return Futures.immediateFailedFuture(new RuntimeException("User does not have phone number")); |
|||
} |
|||
|
|||
return executor.submit(() -> { |
|||
smsService.sendSms(recipient.getTenantId(), recipient.getCustomerId(), new String[]{phone}, processedTemplate.getBody()); |
|||
return null; |
|||
}); |
|||
} |
|||
|
|||
@Override |
|||
public NotificationDeliveryMethod getDeliveryMethod() { |
|||
return NotificationDeliveryMethod.SMS; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,201 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.rule; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.context.annotation.Lazy; |
|||
import org.springframework.context.event.EventListener; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.rule.engine.api.NotificationCenter; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.NotificationRequestId; |
|||
import org.thingsboard.server.common.data.id.NotificationRuleId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.notification.NotificationRequest; |
|||
import org.thingsboard.server.common.data.notification.NotificationRequestConfig; |
|||
import org.thingsboard.server.common.data.notification.NotificationRequestStatus; |
|||
import org.thingsboard.server.common.data.notification.info.NotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.rule.NotificationRule; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; |
|||
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; |
|||
import org.thingsboard.server.dao.notification.NotificationRequestService; |
|||
import org.thingsboard.server.dao.notification.NotificationRuleProcessingService; |
|||
import org.thingsboard.server.dao.notification.NotificationRuleService; |
|||
import org.thingsboard.server.dao.notification.trigger.RuleEngineMsgTrigger; |
|||
import org.thingsboard.server.service.executors.NotificationExecutorService; |
|||
import org.thingsboard.server.service.notification.rule.trigger.NotificationRuleTriggerProcessor; |
|||
import org.thingsboard.server.service.notification.rule.trigger.RuleEngineMsgNotificationRuleTriggerProcessor; |
|||
|
|||
import java.util.Collection; |
|||
import java.util.EnumMap; |
|||
import java.util.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Set; |
|||
import java.util.UUID; |
|||
import java.util.stream.Collectors; |
|||
|
|||
@Service |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
@SuppressWarnings({"rawtypes", "unchecked"}) |
|||
public class DefaultNotificationRuleProcessingService implements NotificationRuleProcessingService { |
|||
|
|||
private final NotificationRuleService notificationRuleService; |
|||
private final NotificationRequestService notificationRequestService; |
|||
@Autowired @Lazy |
|||
private NotificationCenter notificationCenter; |
|||
private final NotificationExecutorService notificationExecutor; |
|||
|
|||
private final Map<NotificationRuleTriggerType, NotificationRuleTriggerProcessor> triggerProcessors = new EnumMap<>(NotificationRuleTriggerType.class); |
|||
|
|||
private final Map<String, NotificationRuleTriggerType> ruleEngineMsgTypeToTriggerType = new HashMap<>(); |
|||
|
|||
@Override |
|||
public void process(TenantId tenantId, NotificationRuleTrigger trigger) { |
|||
List<NotificationRule> rules = notificationRuleService.findNotificationRulesByTenantIdAndTriggerType( |
|||
trigger.getType().isTenantLevel() ? tenantId : TenantId.SYS_TENANT_ID, trigger.getType()); |
|||
for (NotificationRule rule : rules) { |
|||
notificationExecutor.submit(() -> { |
|||
try { |
|||
processNotificationRule(tenantId, rule, trigger); |
|||
} catch (Throwable e) { |
|||
log.error("Failed to process notification rule {} for trigger type {} with trigger object {}", rule.getId(), rule.getTriggerType(), trigger, e); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void process(TenantId tenantId, TbMsg ruleEngineMsg) { |
|||
NotificationRuleTriggerType triggerType = ruleEngineMsgTypeToTriggerType.get(ruleEngineMsg.getType()); |
|||
if (triggerType == null) { |
|||
return; |
|||
} |
|||
process(tenantId, RuleEngineMsgTrigger.builder() |
|||
.msg(ruleEngineMsg) |
|||
.triggerType(triggerType) |
|||
.build()); |
|||
} |
|||
|
|||
private void processNotificationRule(TenantId tenantId, NotificationRule rule, NotificationRuleTrigger trigger) { |
|||
NotificationRuleTriggerConfig triggerConfig = rule.getTriggerConfig(); |
|||
log.debug("Processing notification rule '{}' for trigger type {}", rule.getName(), rule.getTriggerType()); |
|||
|
|||
if (matchesClearRule(trigger, triggerConfig)) { |
|||
List<NotificationRequest> notificationRequests = notificationRequestService.findNotificationRequestsByRuleIdAndOriginatorEntityId(tenantId, rule.getId(), trigger.getOriginatorEntityId()); |
|||
if (notificationRequests.isEmpty()) { |
|||
return; |
|||
} |
|||
|
|||
List<UUID> targets = notificationRequests.stream() |
|||
.filter(NotificationRequest::isSent) |
|||
.flatMap(notificationRequest -> notificationRequest.getTargets().stream()) |
|||
.distinct().collect(Collectors.toList()); |
|||
NotificationInfo notificationInfo = constructNotificationInfo(trigger, triggerConfig); |
|||
submitNotificationRequest(tenantId, targets, rule, trigger.getOriginatorEntityId(), notificationInfo, 0); |
|||
|
|||
notificationRequests.forEach(notificationRequest -> { |
|||
if (notificationRequest.isScheduled()) { |
|||
notificationCenter.deleteNotificationRequest(tenantId, notificationRequest.getId()); |
|||
} |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
if (matchesFilter(trigger, triggerConfig)) { |
|||
NotificationInfo notificationInfo = constructNotificationInfo(trigger, triggerConfig); |
|||
rule.getRecipientsConfig().getTargetsTable().forEach((delay, targets) -> { |
|||
submitNotificationRequest(tenantId, targets, rule, trigger.getOriginatorEntityId(), notificationInfo, delay); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
private boolean matchesFilter(NotificationRuleTrigger trigger, NotificationRuleTriggerConfig triggerConfig) { |
|||
return triggerProcessors.get(triggerConfig.getTriggerType()).matchesFilter(trigger, triggerConfig); |
|||
} |
|||
|
|||
private boolean matchesClearRule(NotificationRuleTrigger trigger, NotificationRuleTriggerConfig triggerConfig) { |
|||
return triggerProcessors.get(triggerConfig.getTriggerType()).matchesClearRule(trigger, triggerConfig); |
|||
} |
|||
|
|||
private NotificationInfo constructNotificationInfo(NotificationRuleTrigger trigger, NotificationRuleTriggerConfig triggerConfig) { |
|||
return triggerProcessors.get(triggerConfig.getTriggerType()).constructNotificationInfo(trigger, triggerConfig); |
|||
} |
|||
|
|||
private void submitNotificationRequest(TenantId tenantId, List<UUID> targets, NotificationRule rule, |
|||
EntityId originatorEntityId, NotificationInfo notificationInfo, int delayInSec) { |
|||
NotificationRequestConfig config = new NotificationRequestConfig(); |
|||
if (delayInSec > 0) { |
|||
config.setSendingDelayInSec(delayInSec); |
|||
} |
|||
NotificationRequest notificationRequest = NotificationRequest.builder() |
|||
.tenantId(tenantId) |
|||
.targets(targets) |
|||
.templateId(rule.getTemplateId()) |
|||
.additionalConfig(config) |
|||
.info(notificationInfo) |
|||
.ruleId(rule.getId()) |
|||
.originatorEntityId(originatorEntityId) |
|||
.build(); |
|||
notificationExecutor.submit(() -> { |
|||
try { |
|||
log.debug("Submitting notification request for rule '{}' with delay of {} sec to targets {}", rule.getName(), delayInSec, targets); |
|||
notificationCenter.processNotificationRequest(tenantId, notificationRequest); |
|||
} catch (Exception e) { |
|||
log.error("Failed to process notification request for rule {}", rule.getId(), e); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
@EventListener(ComponentLifecycleMsg.class) |
|||
public void onNotificationRuleDeleted(ComponentLifecycleMsg componentLifecycleMsg) { |
|||
if (componentLifecycleMsg.getEvent() != ComponentLifecycleEvent.DELETED || |
|||
componentLifecycleMsg.getEntityId().getEntityType() != EntityType.NOTIFICATION_RULE) { |
|||
return; |
|||
} |
|||
|
|||
TenantId tenantId = componentLifecycleMsg.getTenantId(); |
|||
NotificationRuleId notificationRuleId = (NotificationRuleId) componentLifecycleMsg.getEntityId(); |
|||
notificationExecutor.submit(() -> { |
|||
List<NotificationRequestId> scheduledForRule = notificationRequestService.findNotificationRequestsIdsByStatusAndRuleId(tenantId, NotificationRequestStatus.SCHEDULED, notificationRuleId); |
|||
for (NotificationRequestId notificationRequestId : scheduledForRule) { |
|||
notificationCenter.deleteNotificationRequest(tenantId, notificationRequestId); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
@Autowired |
|||
public void setTriggerProcessors(Collection<NotificationRuleTriggerProcessor> processors) { |
|||
processors.forEach(processor -> { |
|||
triggerProcessors.put(processor.getTriggerType(), processor); |
|||
if (processor instanceof RuleEngineMsgNotificationRuleTriggerProcessor) { |
|||
Set<String> supportedMsgTypes = ((RuleEngineMsgNotificationRuleTriggerProcessor<?>) processor).getSupportedMsgTypes(); |
|||
supportedMsgTypes.forEach(supportedMsgType -> { |
|||
ruleEngineMsgTypeToTriggerType.put(supportedMsgType, processor.getTriggerType()); |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.rule.trigger; |
|||
|
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.data.DataConstants; |
|||
import org.thingsboard.server.common.data.alarm.Alarm; |
|||
import org.thingsboard.server.common.data.alarm.AlarmAssignee; |
|||
import org.thingsboard.server.common.data.alarm.AlarmInfo; |
|||
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; |
|||
import org.thingsboard.server.common.data.notification.info.AlarmAssignmentNotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.info.NotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentNotificationRuleTriggerConfig; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentNotificationRuleTriggerConfig.Action; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; |
|||
import org.thingsboard.server.dao.notification.trigger.RuleEngineMsgTrigger; |
|||
|
|||
import java.util.Set; |
|||
|
|||
import static org.apache.commons.collections.CollectionUtils.isEmpty; |
|||
|
|||
@Service |
|||
public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmAssignmentNotificationRuleTriggerConfig> { |
|||
|
|||
@Override |
|||
public boolean matchesFilter(RuleEngineMsgTrigger trigger, AlarmAssignmentNotificationRuleTriggerConfig triggerConfig) { |
|||
Action action = trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGN) ? Action.ASSIGNED : Action.UNASSIGNED; |
|||
if (!triggerConfig.getNotifyOn().contains(action)) { |
|||
return false; |
|||
} |
|||
Alarm alarm = JacksonUtil.fromString(trigger.getMsg().getData(), Alarm.class); |
|||
return (isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType())) && |
|||
(isEmpty(triggerConfig.getAlarmSeverities()) || triggerConfig.getAlarmSeverities().contains(alarm.getSeverity())) && |
|||
(isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm)); |
|||
} |
|||
|
|||
@Override |
|||
public NotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger, AlarmAssignmentNotificationRuleTriggerConfig triggerConfig) { |
|||
AlarmInfo alarmInfo = JacksonUtil.fromString(trigger.getMsg().getData(), AlarmInfo.class); |
|||
AlarmAssignee assignee = alarmInfo.getAssignee(); |
|||
return AlarmAssignmentNotificationInfo.builder() |
|||
.action(trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGN) ? "assigned" : "unassigned") |
|||
.assigneeFirstName(assignee != null ? assignee.getFirstName() : null) |
|||
.assigneeLastName(assignee != null ? assignee.getLastName() : null) |
|||
.assigneeEmail(assignee != null ? assignee.getEmail() : null) |
|||
.assigneeId(assignee != null ? assignee.getId() : null) |
|||
.userName(trigger.getMsg().getMetaData().getValue("userName")) |
|||
.alarmId(alarmInfo.getUuidId()) |
|||
.alarmType(alarmInfo.getType()) |
|||
.alarmOriginator(alarmInfo.getOriginator()) |
|||
.alarmOriginatorName(alarmInfo.getOriginatorName()) |
|||
.alarmSeverity(alarmInfo.getSeverity()) |
|||
.alarmStatus(alarmInfo.getStatus()) |
|||
.alarmCustomerId(alarmInfo.getCustomerId()) |
|||
.build(); |
|||
} |
|||
|
|||
@Override |
|||
public NotificationRuleTriggerType getTriggerType() { |
|||
return NotificationRuleTriggerType.ALARM_ASSIGNMENT; |
|||
} |
|||
|
|||
@Override |
|||
public Set<String> getSupportedMsgTypes() { |
|||
return Set.of(DataConstants.ALARM_ASSIGN, DataConstants.ALARM_UNASSIGN); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.rule.trigger; |
|||
|
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.data.DataConstants; |
|||
import org.thingsboard.server.common.data.alarm.Alarm; |
|||
import org.thingsboard.server.common.data.alarm.AlarmComment; |
|||
import org.thingsboard.server.common.data.alarm.AlarmCommentType; |
|||
import org.thingsboard.server.common.data.alarm.AlarmInfo; |
|||
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; |
|||
import org.thingsboard.server.common.data.notification.info.AlarmCommentNotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.info.NotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentNotificationRuleTriggerConfig; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
import org.thingsboard.server.dao.notification.trigger.RuleEngineMsgTrigger; |
|||
|
|||
import java.util.Set; |
|||
|
|||
import static org.apache.commons.collections.CollectionUtils.isEmpty; |
|||
|
|||
@Service |
|||
public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmCommentNotificationRuleTriggerConfig> { |
|||
|
|||
@Override |
|||
public boolean matchesFilter(RuleEngineMsgTrigger trigger, AlarmCommentNotificationRuleTriggerConfig triggerConfig) { |
|||
TbMsg msg = trigger.getMsg(); |
|||
if (msg.getMetaData().getValue("comment") == null) { |
|||
return false; |
|||
} |
|||
if (msg.getType().equals(DataConstants.COMMENT_UPDATED) && !triggerConfig.isNotifyOnCommentUpdate()) { |
|||
return false; |
|||
} |
|||
if (triggerConfig.isOnlyUserComments()) { |
|||
AlarmComment comment = JacksonUtil.fromString(msg.getMetaData().getValue("comment"), AlarmComment.class); |
|||
if (comment.getType() == AlarmCommentType.SYSTEM) { |
|||
return false; |
|||
} |
|||
} |
|||
Alarm alarm = JacksonUtil.fromString(msg.getData(), Alarm.class); |
|||
return (isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType())) && |
|||
(isEmpty(triggerConfig.getAlarmSeverities()) || triggerConfig.getAlarmSeverities().contains(alarm.getSeverity())) && |
|||
(isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm)); |
|||
} |
|||
|
|||
@Override |
|||
public NotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger, AlarmCommentNotificationRuleTriggerConfig triggerConfig) { |
|||
TbMsg msg = trigger.getMsg(); |
|||
AlarmComment comment = JacksonUtil.fromString(msg.getMetaData().getValue("comment"), AlarmComment.class); |
|||
AlarmInfo alarmInfo = JacksonUtil.fromString(msg.getData(), AlarmInfo.class); |
|||
return AlarmCommentNotificationInfo.builder() |
|||
.comment(comment.getComment().get("text").asText()) |
|||
.action(msg.getType().equals(DataConstants.COMMENT_CREATED) ? "added" : "updated") |
|||
.userName(msg.getMetaData().getValue("userName")) |
|||
.alarmId(alarmInfo.getUuidId()) |
|||
.alarmType(alarmInfo.getType()) |
|||
.alarmOriginator(alarmInfo.getOriginator()) |
|||
.alarmOriginatorName(alarmInfo.getOriginatorName()) |
|||
.alarmSeverity(alarmInfo.getSeverity()) |
|||
.alarmStatus(alarmInfo.getStatus()) |
|||
.alarmCustomerId(alarmInfo.getCustomerId()) |
|||
.build(); |
|||
} |
|||
|
|||
@Override |
|||
public NotificationRuleTriggerType getTriggerType() { |
|||
return NotificationRuleTriggerType.ALARM_COMMENT; |
|||
} |
|||
|
|||
@Override |
|||
public Set<String> getSupportedMsgTypes() { |
|||
return Set.of(DataConstants.COMMENT_CREATED, DataConstants.COMMENT_UPDATED); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,120 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.rule.trigger; |
|||
|
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.common.data.alarm.Alarm; |
|||
import org.thingsboard.server.common.data.alarm.AlarmInfo; |
|||
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; |
|||
import org.thingsboard.server.common.data.notification.info.AlarmNotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.info.NotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.ClearRule; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; |
|||
import org.thingsboard.server.dao.alarm.AlarmApiCallResult; |
|||
import org.thingsboard.server.dao.notification.trigger.AlarmTrigger; |
|||
|
|||
import static org.apache.commons.collections.CollectionUtils.isEmpty; |
|||
import static org.apache.commons.collections.CollectionUtils.isNotEmpty; |
|||
|
|||
@Service |
|||
public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<AlarmTrigger, AlarmNotificationRuleTriggerConfig> { |
|||
|
|||
@Override |
|||
public boolean matchesFilter(AlarmTrigger trigger, AlarmNotificationRuleTriggerConfig triggerConfig) { |
|||
AlarmApiCallResult alarmUpdate = trigger.getAlarmUpdate(); |
|||
Alarm alarm = alarmUpdate.getAlarm(); |
|||
if (!typeMatches(alarm, triggerConfig)) { |
|||
return false; |
|||
} |
|||
|
|||
if (alarmUpdate.isCreated()) { |
|||
if (triggerConfig.getNotifyOn().contains(AlarmAction.CREATED)) { |
|||
return severityMatches(alarm, triggerConfig); |
|||
} |
|||
} else if (alarmUpdate.isSeverityChanged()) { |
|||
if (triggerConfig.getNotifyOn().contains(AlarmAction.SEVERITY_CHANGED)) { |
|||
return severityMatches(alarmUpdate.getOld(), triggerConfig) || severityMatches(alarm, triggerConfig); |
|||
} else { |
|||
// if we haven't yet sent notification about the alarm
|
|||
return !severityMatches(alarmUpdate.getOld(), triggerConfig) && severityMatches(alarm, triggerConfig); |
|||
} |
|||
} else if (alarmUpdate.isAcknowledged()) { |
|||
if (triggerConfig.getNotifyOn().contains(AlarmAction.ACKNOWLEDGED)) { |
|||
return severityMatches(alarm, triggerConfig); |
|||
} |
|||
} else if (alarmUpdate.isCleared()) { |
|||
if (triggerConfig.getNotifyOn().contains(AlarmAction.CLEARED)) { |
|||
return severityMatches(alarm, triggerConfig); |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
@Override |
|||
public boolean matchesClearRule(AlarmTrigger trigger, AlarmNotificationRuleTriggerConfig triggerConfig) { |
|||
AlarmApiCallResult alarmUpdate = trigger.getAlarmUpdate(); |
|||
Alarm alarm = alarmUpdate.getAlarm(); |
|||
if (!typeMatches(alarm, triggerConfig)) { |
|||
return false; |
|||
} |
|||
if (alarmUpdate.isDeleted()) { |
|||
return true; |
|||
} |
|||
ClearRule clearRule = triggerConfig.getClearRule(); |
|||
if (clearRule != null) { |
|||
if (isNotEmpty(clearRule.getAlarmStatuses())) { |
|||
return AlarmStatusFilter.from(clearRule.getAlarmStatuses()).matches(alarm); |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
private boolean severityMatches(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) { |
|||
return isEmpty(triggerConfig.getAlarmSeverities()) || triggerConfig.getAlarmSeverities().contains(alarm.getSeverity()); |
|||
} |
|||
|
|||
private boolean typeMatches(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) { |
|||
return isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType()); |
|||
} |
|||
|
|||
@Override |
|||
public NotificationInfo constructNotificationInfo(AlarmTrigger trigger, AlarmNotificationRuleTriggerConfig triggerConfig) { |
|||
AlarmApiCallResult alarmUpdate = trigger.getAlarmUpdate(); |
|||
AlarmInfo alarmInfo = alarmUpdate.getAlarm(); |
|||
return AlarmNotificationInfo.builder() |
|||
.alarmId(alarmInfo.getUuidId()) |
|||
.alarmType(alarmInfo.getType()) |
|||
.action(alarmUpdate.isCreated() ? "created" : |
|||
alarmUpdate.isSeverityChanged() ? "severity changed" : |
|||
alarmUpdate.isAcknowledged() ? "acknowledged" : |
|||
alarmUpdate.isCleared() ? "cleared" : |
|||
alarmUpdate.isDeleted() ? "deleted" : null) |
|||
.alarmOriginator(alarmInfo.getOriginator()) |
|||
.alarmOriginatorName(alarmInfo.getOriginatorName()) |
|||
.alarmSeverity(alarmInfo.getSeverity()) |
|||
.alarmStatus(alarmInfo.getStatus()) |
|||
.alarmCustomerId(alarmInfo.getCustomerId()) |
|||
.build(); |
|||
} |
|||
|
|||
@Override |
|||
public NotificationRuleTriggerType getTriggerType() { |
|||
return NotificationRuleTriggerType.ALARM; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.rule.trigger; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import org.apache.commons.collections.CollectionUtils; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.common.data.DataConstants; |
|||
import org.thingsboard.server.common.data.DeviceProfile; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.notification.info.DeviceInactivityNotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.info.NotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.DeviceInactivityNotificationRuleTriggerConfig; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
import org.thingsboard.server.dao.notification.trigger.RuleEngineMsgTrigger; |
|||
import org.thingsboard.server.service.profile.TbDeviceProfileCache; |
|||
|
|||
import java.util.Set; |
|||
|
|||
@Service |
|||
@RequiredArgsConstructor |
|||
public class DeviceInactivityTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<DeviceInactivityNotificationRuleTriggerConfig> { |
|||
|
|||
private final TbDeviceProfileCache deviceProfileCache; |
|||
|
|||
@Override |
|||
public boolean matchesFilter(RuleEngineMsgTrigger trigger, DeviceInactivityNotificationRuleTriggerConfig triggerConfig) { |
|||
DeviceId deviceId = (DeviceId) trigger.getMsg().getOriginator(); |
|||
if (CollectionUtils.isNotEmpty(triggerConfig.getDevices())) { |
|||
return triggerConfig.getDevices().contains(deviceId.getId()); |
|||
} else if (CollectionUtils.isNotEmpty(triggerConfig.getDeviceProfiles())) { |
|||
DeviceProfile deviceProfile = deviceProfileCache.get(TenantId.SYS_TENANT_ID, deviceId); |
|||
return deviceProfile != null && triggerConfig.getDeviceProfiles().contains(deviceProfile.getUuidId()); |
|||
} else { |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public NotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger, DeviceInactivityNotificationRuleTriggerConfig triggerConfig) { |
|||
TbMsg msg = trigger.getMsg(); |
|||
return DeviceInactivityNotificationInfo.builder() |
|||
.deviceId(msg.getOriginator().getId()) |
|||
.deviceName(msg.getMetaData().getValue("deviceName")) |
|||
.deviceType(msg.getMetaData().getValue("deviceType")) |
|||
.deviceLabel(msg.getMetaData().getValue("deviceLabel")) |
|||
.deviceCustomerId(msg.getCustomerId()) |
|||
.build(); |
|||
} |
|||
|
|||
@Override |
|||
public NotificationRuleTriggerType getTriggerType() { |
|||
return NotificationRuleTriggerType.DEVICE_INACTIVITY; |
|||
} |
|||
|
|||
@Override |
|||
public Set<String> getSupportedMsgTypes() { |
|||
return Set.of(DataConstants.INACTIVITY_EVENT); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.rule.trigger; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.common.data.notification.info.EntitiesLimitNotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.info.NotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; |
|||
import org.thingsboard.server.dao.notification.trigger.EntitiesLimitTrigger; |
|||
import org.thingsboard.server.dao.tenant.TenantService; |
|||
|
|||
import static org.apache.commons.collections.CollectionUtils.isNotEmpty; |
|||
|
|||
@Service |
|||
@RequiredArgsConstructor |
|||
public class EntitiesLimitTriggerProcessor implements NotificationRuleTriggerProcessor<EntitiesLimitTrigger, EntitiesLimitNotificationRuleTriggerConfig> { |
|||
|
|||
private final TenantService tenantService; |
|||
|
|||
@Override |
|||
public boolean matchesFilter(EntitiesLimitTrigger trigger, EntitiesLimitNotificationRuleTriggerConfig triggerConfig) { |
|||
if (isNotEmpty(triggerConfig.getEntityTypes()) && !triggerConfig.getEntityTypes().contains(trigger.getEntityType())) { |
|||
return false; |
|||
} |
|||
return (int) (trigger.getLimit() * triggerConfig.getThreshold()) == trigger.getCurrentCount(); // strict comparing not to send notification on each new entity
|
|||
} |
|||
|
|||
@Override |
|||
public NotificationInfo constructNotificationInfo(EntitiesLimitTrigger trigger, EntitiesLimitNotificationRuleTriggerConfig triggerConfig) { |
|||
return EntitiesLimitNotificationInfo.builder() |
|||
.entityType(trigger.getEntityType()) |
|||
.currentCount(trigger.getCurrentCount()) |
|||
.limit(trigger.getLimit()) |
|||
.percents((int) (((float)trigger.getCurrentCount() / trigger.getLimit()) * 100)) |
|||
.tenantId(trigger.getTenantId()) |
|||
.tenantName(tenantService.findTenantById(trigger.getTenantId()).getName()) |
|||
.build(); |
|||
} |
|||
|
|||
@Override |
|||
public NotificationRuleTriggerType getTriggerType() { |
|||
return NotificationRuleTriggerType.ENTITIES_LIMIT; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,89 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.rule.trigger; |
|||
|
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.common.data.DataConstants; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
import org.thingsboard.server.common.data.audit.ActionType; |
|||
import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.info.NotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; |
|||
import org.thingsboard.server.common.msg.TbMsg; |
|||
import org.thingsboard.server.dao.notification.trigger.RuleEngineMsgTrigger; |
|||
|
|||
import java.util.Optional; |
|||
import java.util.Set; |
|||
import java.util.UUID; |
|||
|
|||
@Service |
|||
public class EntityActionTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<EntityActionNotificationRuleTriggerConfig> { |
|||
|
|||
@Override |
|||
public boolean matchesFilter(RuleEngineMsgTrigger trigger, EntityActionNotificationRuleTriggerConfig triggerConfig) { |
|||
String msgType = trigger.getMsg().getType(); |
|||
if (msgType.equals(DataConstants.ENTITY_CREATED)) { |
|||
if (!triggerConfig.isCreated()) { |
|||
return false; |
|||
} |
|||
} else if (msgType.equals(DataConstants.ENTITY_UPDATED)) { |
|||
if (!triggerConfig.isUpdated()) { |
|||
return false; |
|||
} |
|||
} else if (msgType.equals(DataConstants.ENTITY_DELETED)) { |
|||
if (!triggerConfig.isDeleted()) { |
|||
return false; |
|||
} |
|||
} else { |
|||
return false; |
|||
} |
|||
return triggerConfig.getEntityType() == null || getEntityType(trigger.getMsg()) == triggerConfig.getEntityType(); |
|||
} |
|||
|
|||
@Override |
|||
public NotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger, EntityActionNotificationRuleTriggerConfig triggerConfig) { |
|||
TbMsg msg = trigger.getMsg(); |
|||
String msgType = msg.getType(); |
|||
ActionType actionType = msgType.equals(DataConstants.ENTITY_CREATED) ? ActionType.ADDED : |
|||
msgType.equals(DataConstants.ENTITY_UPDATED) ? ActionType.UPDATED : |
|||
msgType.equals(DataConstants.ENTITY_DELETED) ? ActionType.DELETED : null; |
|||
return EntityActionNotificationInfo.builder() |
|||
.entityId(msg.getOriginator()) |
|||
.entityName(msg.getMetaData().getValue("entityName")) |
|||
.actionType(actionType) |
|||
.originatorUserId(UUID.fromString(msg.getMetaData().getValue("userId"))) |
|||
.originatorUserName(msg.getMetaData().getValue("userName")) |
|||
.entityCustomerId(msg.getCustomerId()) |
|||
.build(); |
|||
} |
|||
|
|||
private static EntityType getEntityType(TbMsg msg) { |
|||
return Optional.ofNullable(msg.getMetaData().getValue("entityType")) |
|||
.map(EntityType::valueOf).orElse(null); |
|||
} |
|||
|
|||
@Override |
|||
public NotificationRuleTriggerType getTriggerType() { |
|||
return NotificationRuleTriggerType.ENTITY_ACTION; |
|||
} |
|||
|
|||
@Override |
|||
public Set<String> getSupportedMsgTypes() { |
|||
return Set.of(DataConstants.ENTITY_CREATED, DataConstants.ENTITY_UPDATED, DataConstants.ENTITY_DELETED); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.rule.trigger; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.notification.info.NewPlatformVersionNotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.info.NotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionNotificationRuleTriggerConfig; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.NewPlatformVersionTrigger; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; |
|||
import org.thingsboard.server.common.msg.queue.ServiceType; |
|||
import org.thingsboard.server.queue.discovery.PartitionService; |
|||
|
|||
@Service |
|||
@RequiredArgsConstructor |
|||
public class NewPlatformVersionTriggerProcessor implements NotificationRuleTriggerProcessor<NewPlatformVersionTrigger, NewPlatformVersionNotificationRuleTriggerConfig> { |
|||
|
|||
private final PartitionService partitionService; |
|||
|
|||
@Override |
|||
public boolean matchesFilter(NewPlatformVersionTrigger trigger, NewPlatformVersionNotificationRuleTriggerConfig triggerConfig) { |
|||
// todo: don't send repetitive notification after platform restart?
|
|||
if (!partitionService.resolve(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID).isMyPartition()) { |
|||
return false; |
|||
} |
|||
return trigger.getMessage().isUpdateAvailable(); |
|||
} |
|||
|
|||
@Override |
|||
public NotificationInfo constructNotificationInfo(NewPlatformVersionTrigger trigger, NewPlatformVersionNotificationRuleTriggerConfig triggerConfig) { |
|||
return NewPlatformVersionNotificationInfo.builder() |
|||
.message(trigger.getMessage().getMessage()) |
|||
.build(); |
|||
} |
|||
|
|||
@Override |
|||
public NotificationRuleTriggerType getTriggerType() { |
|||
return NotificationRuleTriggerType.NEW_PLATFORM_VERSION; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.rule.trigger; |
|||
|
|||
import org.thingsboard.server.common.data.notification.info.NotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; |
|||
|
|||
public interface NotificationRuleTriggerProcessor<T extends NotificationRuleTrigger, C extends NotificationRuleTriggerConfig> { |
|||
|
|||
boolean matchesFilter(T trigger, C triggerConfig); |
|||
|
|||
default boolean matchesClearRule(T trigger, C triggerConfig) { |
|||
return false; |
|||
} |
|||
|
|||
NotificationInfo constructNotificationInfo(T trigger, C triggerConfig); |
|||
|
|||
NotificationRuleTriggerType getTriggerType(); |
|||
|
|||
} |
|||
@ -0,0 +1,98 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.rule.trigger; |
|||
|
|||
import org.apache.commons.collections.CollectionUtils; |
|||
import org.apache.commons.lang3.StringUtils; |
|||
import org.apache.commons.lang3.exception.ExceptionUtils; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.common.data.EntityType; |
|||
import org.thingsboard.server.common.data.notification.info.NotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.info.RuleEngineComponentLifecycleEventNotificationInfo; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; |
|||
import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; |
|||
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; |
|||
import org.thingsboard.server.dao.notification.trigger.RuleEngineComponentLifecycleEventTrigger; |
|||
|
|||
import java.io.PrintWriter; |
|||
import java.io.StringWriter; |
|||
import java.util.Set; |
|||
|
|||
@Service |
|||
public class RuleEngineComponentLifecycleEventTriggerProcessor implements NotificationRuleTriggerProcessor<RuleEngineComponentLifecycleEventTrigger, RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig> { |
|||
|
|||
@Override |
|||
public boolean matchesFilter(RuleEngineComponentLifecycleEventTrigger trigger, RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig) { |
|||
if (CollectionUtils.isNotEmpty(triggerConfig.getRuleChains())) { |
|||
if (!triggerConfig.getRuleChains().contains(trigger.getRuleChainId().getId())) { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
EntityType componentType = trigger.getComponentId().getEntityType(); |
|||
Set<ComponentLifecycleEvent> trackedEvents; |
|||
boolean onlyFailures; |
|||
if (componentType == EntityType.RULE_CHAIN) { |
|||
trackedEvents = triggerConfig.getRuleChainEvents(); |
|||
onlyFailures = triggerConfig.isOnlyRuleChainLifecycleFailures(); |
|||
} else if (componentType == EntityType.RULE_NODE && triggerConfig.isTrackRuleNodeEvents()) { |
|||
trackedEvents = triggerConfig.getRuleNodeEvents(); |
|||
onlyFailures = triggerConfig.isOnlyRuleNodeLifecycleFailures(); |
|||
} else { |
|||
return false; |
|||
} |
|||
if (CollectionUtils.isEmpty(trackedEvents)) { |
|||
trackedEvents = Set.of(ComponentLifecycleEvent.STARTED, ComponentLifecycleEvent.UPDATED, ComponentLifecycleEvent.STOPPED); |
|||
} |
|||
|
|||
if (!trackedEvents.contains(trigger.getEventType())) { |
|||
return false; |
|||
} |
|||
if (onlyFailures) { |
|||
return trigger.getError() != null; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public NotificationInfo constructNotificationInfo(RuleEngineComponentLifecycleEventTrigger trigger, RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig) { |
|||
return RuleEngineComponentLifecycleEventNotificationInfo.builder() |
|||
.ruleChainId(trigger.getRuleChainId()) |
|||
.ruleChainName(trigger.getRuleChainName()) |
|||
.componentId(trigger.getComponentId()) |
|||
.componentName(trigger.getComponentName()) |
|||
.action(trigger.getEventType() == ComponentLifecycleEvent.STARTED ? "start" : |
|||
trigger.getEventType() == ComponentLifecycleEvent.UPDATED ? "update" : |
|||
trigger.getEventType() == ComponentLifecycleEvent.STOPPED ? "stop" : null) |
|||
.eventType(trigger.getEventType()) |
|||
.error(getErrorMsg(trigger.getError())) |
|||
.build(); |
|||
} |
|||
|
|||
private String getErrorMsg(Throwable error) { |
|||
if (error == null) return null; |
|||
|
|||
StringWriter sw = new StringWriter(); |
|||
error.printStackTrace(new PrintWriter(sw)); |
|||
return StringUtils.abbreviate(ExceptionUtils.getStackTrace(error), 200); |
|||
} |
|||
|
|||
@Override |
|||
public NotificationRuleTriggerType getTriggerType() { |
|||
return NotificationRuleTriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.rule.trigger; |
|||
|
|||
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig; |
|||
import org.thingsboard.server.dao.notification.trigger.RuleEngineMsgTrigger; |
|||
|
|||
import java.util.Set; |
|||
|
|||
public interface RuleEngineMsgNotificationRuleTriggerProcessor<C extends NotificationRuleTriggerConfig> extends NotificationRuleTriggerProcessor<RuleEngineMsgTrigger, C> { |
|||
|
|||
Set<String> getSupportedMsgTypes(); |
|||
|
|||
} |
|||
@ -0,0 +1,153 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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.slack; |
|||
|
|||
import com.github.benmanes.caffeine.cache.Cache; |
|||
import com.github.benmanes.caffeine.cache.Caffeine; |
|||
import com.slack.api.Slack; |
|||
import com.slack.api.methods.MethodsClient; |
|||
import com.slack.api.methods.SlackApiRequest; |
|||
import com.slack.api.methods.SlackApiTextResponse; |
|||
import com.slack.api.methods.request.chat.ChatPostMessageRequest; |
|||
import com.slack.api.methods.request.conversations.ConversationsListRequest; |
|||
import com.slack.api.methods.request.users.UsersListRequest; |
|||
import com.slack.api.methods.response.conversations.ConversationsListResponse; |
|||
import com.slack.api.methods.response.users.UsersListResponse; |
|||
import com.slack.api.model.ConversationType; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.apache.commons.lang3.StringUtils; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.rule.engine.api.slack.SlackService; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; |
|||
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.slack.SlackConversation; |
|||
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversationType; |
|||
import org.thingsboard.server.common.data.util.ThrowingBiFunction; |
|||
import org.thingsboard.server.dao.notification.NotificationSettingsService; |
|||
|
|||
import java.util.List; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.stream.Collectors; |
|||
|
|||
@Service |
|||
@RequiredArgsConstructor |
|||
public class DefaultSlackService implements SlackService { |
|||
|
|||
private final NotificationSettingsService notificationSettingsService; |
|||
|
|||
private final Slack slack = Slack.getInstance(); |
|||
private final Cache<String, List<SlackConversation>> cache = Caffeine.newBuilder() |
|||
.expireAfterWrite(20, TimeUnit.SECONDS) |
|||
.maximumSize(100) |
|||
.build(); |
|||
private static final int CONVERSATIONS_LOAD_LIMIT = 1000; |
|||
|
|||
@Override |
|||
public void sendMessage(TenantId tenantId, String token, String conversationId, String message) { |
|||
ChatPostMessageRequest request = ChatPostMessageRequest.builder() |
|||
.channel(conversationId) |
|||
.text(message) |
|||
.build(); |
|||
sendRequest(token, request, MethodsClient::chatPostMessage); |
|||
} |
|||
|
|||
@Override |
|||
public List<SlackConversation> listConversations(TenantId tenantId, String token, SlackConversationType conversationType) { |
|||
return cache.get(conversationType + ":" + token, k -> { |
|||
if (conversationType == SlackConversationType.DIRECT) { |
|||
UsersListRequest request = UsersListRequest.builder() |
|||
.limit(CONVERSATIONS_LOAD_LIMIT) |
|||
.build(); |
|||
|
|||
UsersListResponse response = sendRequest(token, request, MethodsClient::usersList); |
|||
return response.getMembers().stream() |
|||
.filter(user -> !user.isDeleted() && !user.isStranger() && !user.isBot()) |
|||
.map(user -> { |
|||
SlackConversation conversation = new SlackConversation(); |
|||
conversation.setId(user.getId()); |
|||
conversation.setName(String.format("@%s (%s)", user.getName(), user.getRealName())); |
|||
return conversation; |
|||
}) |
|||
.collect(Collectors.toList()); |
|||
} else { |
|||
ConversationsListRequest request = ConversationsListRequest.builder() |
|||
.types(List.of(conversationType == SlackConversationType.PUBLIC_CHANNEL ? |
|||
ConversationType.PUBLIC_CHANNEL : |
|||
ConversationType.PRIVATE_CHANNEL)) |
|||
.limit(CONVERSATIONS_LOAD_LIMIT) |
|||
.excludeArchived(true) |
|||
.build(); |
|||
|
|||
ConversationsListResponse response = sendRequest(token, request, MethodsClient::conversationsList); |
|||
return response.getChannels().stream() |
|||
.filter(channel -> !channel.isArchived()) |
|||
.map(channel -> { |
|||
SlackConversation conversation = new SlackConversation(); |
|||
conversation.setId(channel.getId()); |
|||
conversation.setName("#" + channel.getName()); |
|||
return conversation; |
|||
}) |
|||
.collect(Collectors.toList()); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
@Override |
|||
public SlackConversation findConversation(TenantId tenantId, String token, SlackConversationType conversationType, String namePattern) { |
|||
List<SlackConversation> conversations = listConversations(tenantId, token, conversationType); |
|||
return conversations.stream() |
|||
.filter(conversation -> StringUtils.containsIgnoreCase(conversation.getName(), namePattern)) |
|||
.findFirst().orElse(null); |
|||
} |
|||
|
|||
@Override |
|||
public String getToken(TenantId tenantId) { |
|||
NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId); |
|||
SlackNotificationDeliveryMethodConfig slackConfig = (SlackNotificationDeliveryMethodConfig) |
|||
settings.getDeliveryMethodsConfigs().get(NotificationDeliveryMethod.SLACK); |
|||
if (slackConfig != null) { |
|||
return slackConfig.getBotToken(); |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
private <T extends SlackApiRequest, R extends SlackApiTextResponse> R sendRequest(String token, T request, ThrowingBiFunction<MethodsClient, T, R> method) { |
|||
MethodsClient client = slack.methods(token); |
|||
R response; |
|||
try { |
|||
response = method.apply(client, request); |
|||
} catch (Exception e) { |
|||
throw new RuntimeException(e.getMessage(), e); |
|||
} |
|||
|
|||
if (!response.isOk()) { |
|||
String error = response.getError(); |
|||
if (error == null) { |
|||
error = "unknown error"; |
|||
} else if (error.contains("missing_scope")) { |
|||
String neededScope = response.getNeeded(); |
|||
error = "bot token scope '" + neededScope + "' is needed"; |
|||
} |
|||
throw new RuntimeException("Failed to send message via Slack: " + error); |
|||
} |
|||
|
|||
return response; |
|||
} |
|||
|
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue