Browse Source

Merge with hotfix/3.6.3

pull/10555/head
Igor Kulikov 2 years ago
parent
commit
ce44bbc248
  1. 123
      application/src/main/data/upgrade/3.6.3/schema_update.sql
  2. 136
      application/src/main/data/upgrade/3.6.4/schema_update.sql
  3. 23
      application/src/main/java/org/thingsboard/server/controller/NotificationController.java
  4. 5
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  5. 5
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  6. 32
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  7. 50
      application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java
  8. 22
      application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java
  9. 8
      application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java
  10. 10
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
  11. 80
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  12. 9
      common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationService.java
  13. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/Notification.java
  14. 3
      common/edge-api/src/main/proto/edge.proto
  15. 4
      common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java
  16. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  17. 7
      dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationEntity.java
  18. 19
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java
  19. 11
      dao/src/main/java/org/thingsboard/server/dao/notification/NotificationDao.java
  20. 24
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java
  21. 33
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationRepository.java
  22. 4
      dao/src/main/resources/sql/schema-entities-idx.sql
  23. 3
      dao/src/main/resources/sql/schema-entities.sql
  24. 2
      msa/tb/pom.xml
  25. 2
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java
  26. 2
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/notification/FirebaseService.java

123
application/src/main/data/upgrade/3.6.3/schema_update.sql

@ -14,123 +14,14 @@
-- limitations under the License.
--
-- create new attribute_kv table schema
DO
$$
BEGIN
-- in case of running the upgrade script a second time:
IF EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'attribute_kv' and column_name='entity_type') THEN
DROP VIEW IF EXISTS device_info_view;
DROP VIEW IF EXISTS device_info_active_attribute_view;
ALTER INDEX IF EXISTS idx_attribute_kv_by_key_and_last_update_ts RENAME TO idx_attribute_kv_by_key_and_last_update_ts_old;
IF EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'attribute_kv_pkey') THEN
ALTER TABLE attribute_kv RENAME CONSTRAINT attribute_kv_pkey TO attribute_kv_pkey_old;
END IF;
ALTER TABLE attribute_kv RENAME TO attribute_kv_old;
CREATE TABLE IF NOT EXISTS attribute_kv
(
entity_id uuid,
attribute_type int,
attribute_key int,
bool_v boolean,
str_v varchar(10000000),
long_v bigint,
dbl_v double precision,
json_v json,
last_update_ts bigint,
CONSTRAINT attribute_kv_pkey PRIMARY KEY (entity_id, attribute_type, attribute_key)
);
END IF;
END;
$$;
-- NOTIFICATIONS UPDATE START
-- rename ts_kv_dictionary table to key_dictionary or create table if not exists
DO
$$
BEGIN
IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'ts_kv_dictionary') THEN
ALTER TABLE ts_kv_dictionary RENAME CONSTRAINT ts_key_id_pkey TO key_dictionary_id_pkey;
ALTER TABLE ts_kv_dictionary RENAME TO key_dictionary;
ELSE CREATE TABLE IF NOT EXISTS key_dictionary(
key varchar(255) NOT NULL,
key_id serial UNIQUE,
CONSTRAINT key_dictionary_id_pkey PRIMARY KEY (key)
);
END IF;
END;
$$;
ALTER TABLE notification ADD COLUMN IF NOT EXISTS delivery_method VARCHAR(50) NOT NULL default 'WEB';
-- insert keys into key_dictionary
DO
$$
BEGIN
IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'attribute_kv_old') THEN
INSERT INTO key_dictionary(key) SELECT DISTINCT attribute_key FROM attribute_kv_old ON CONFLICT DO NOTHING;
END IF;
END;
$$;
DROP INDEX IF EXISTS idx_notification_recipient_id_created_time;
DROP INDEX IF EXISTS idx_notification_recipient_id_unread;
-- migrate attributes from attribute_kv_old to attribute_kv
DO
$$
DECLARE
row_num_old integer;
row_num integer;
BEGIN
IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'attribute_kv_old') THEN
INSERT INTO attribute_kv(entity_id, attribute_type, attribute_key, bool_v, str_v, long_v, dbl_v, json_v, last_update_ts)
SELECT a.entity_id, CASE
WHEN a.attribute_type = 'CLIENT_SCOPE' THEN 1
WHEN a.attribute_type = 'SERVER_SCOPE' THEN 2
WHEN a.attribute_type = 'SHARED_SCOPE' THEN 3
ELSE 0
END,
k.key_id, a.bool_v, a.str_v, a.long_v, a.dbl_v, a.json_v, a.last_update_ts
FROM attribute_kv_old a INNER JOIN key_dictionary k ON (a.attribute_key = k.key);
SELECT COUNT(*) INTO row_num_old FROM attribute_kv_old;
SELECT COUNT(*) INTO row_num FROM attribute_kv;
RAISE NOTICE 'Migrated % of % rows', row_num, row_num_old;
CREATE INDEX IF NOT EXISTS idx_notification_delivery_method_recipient_id_created_time ON notification(delivery_method, recipient_id, created_time DESC);
CREATE INDEX IF NOT EXISTS idx_notification_delivery_method_recipient_id_unread ON notification(delivery_method, recipient_id) WHERE status <> 'READ';
IF row_num != 0 THEN
DROP TABLE IF EXISTS attribute_kv_old;
ELSE
RAISE EXCEPTION 'Table attribute_kv is empty';
END IF;
CREATE INDEX IF NOT EXISTS idx_attribute_kv_by_key_and_last_update_ts ON attribute_kv(entity_id, attribute_key, last_update_ts desc);
END IF;
EXCEPTION
WHEN others THEN
ROLLBACK;
RAISE EXCEPTION 'Error during COPY: %', SQLERRM;
END
$$;
-- OAUTH2 PARAMS ALTER TABLE START
ALTER TABLE oauth2_params
ADD COLUMN IF NOT EXISTS edge_enabled boolean DEFAULT false;
-- OAUTH2 PARAMS ALTER TABLE END
-- QUEUE STATS UPDATE START
CREATE TABLE IF NOT EXISTS queue_stats (
id uuid NOT NULL CONSTRAINT queue_stats_pkey PRIMARY KEY,
created_time bigint NOT NULL,
tenant_id uuid NOT NULL,
queue_name varchar(255) NOT NULL,
service_id varchar(255) NOT NULL,
CONSTRAINT queue_stats_name_unq_key UNIQUE (tenant_id, queue_name, service_id)
);
INSERT INTO queue_stats
SELECT id, created_time, tenant_id, substring(name FROM 1 FOR position('_' IN name) - 1) AS queue_name,
substring(name FROM position('_' IN name) + 1) AS service_id
FROM asset
WHERE type = 'TbServiceQueue' and name LIKE '%\_%';
DELETE FROM asset WHERE type='TbServiceQueue';
DELETE FROM asset_profile WHERE name ='TbServiceQueue';
-- QUEUE STATS UPDATE END
-- NOTIFICATIONS UPDATE END

136
application/src/main/data/upgrade/3.6.4/schema_update.sql

@ -0,0 +1,136 @@
--
-- Copyright © 2016-2024 The Thingsboard Authors
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- create new attribute_kv table schema
DO
$$
BEGIN
-- in case of running the upgrade script a second time:
IF EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name = 'attribute_kv' and column_name='entity_type') THEN
DROP VIEW IF EXISTS device_info_view;
DROP VIEW IF EXISTS device_info_active_attribute_view;
ALTER INDEX IF EXISTS idx_attribute_kv_by_key_and_last_update_ts RENAME TO idx_attribute_kv_by_key_and_last_update_ts_old;
IF EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'attribute_kv_pkey') THEN
ALTER TABLE attribute_kv RENAME CONSTRAINT attribute_kv_pkey TO attribute_kv_pkey_old;
END IF;
ALTER TABLE attribute_kv RENAME TO attribute_kv_old;
CREATE TABLE IF NOT EXISTS attribute_kv
(
entity_id uuid,
attribute_type int,
attribute_key int,
bool_v boolean,
str_v varchar(10000000),
long_v bigint,
dbl_v double precision,
json_v json,
last_update_ts bigint,
CONSTRAINT attribute_kv_pkey PRIMARY KEY (entity_id, attribute_type, attribute_key)
);
END IF;
END;
$$;
-- rename ts_kv_dictionary table to key_dictionary or create table if not exists
DO
$$
BEGIN
IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'ts_kv_dictionary') THEN
ALTER TABLE ts_kv_dictionary RENAME CONSTRAINT ts_key_id_pkey TO key_dictionary_id_pkey;
ALTER TABLE ts_kv_dictionary RENAME TO key_dictionary;
ELSE CREATE TABLE IF NOT EXISTS key_dictionary(
key varchar(255) NOT NULL,
key_id serial UNIQUE,
CONSTRAINT key_dictionary_id_pkey PRIMARY KEY (key)
);
END IF;
END;
$$;
-- insert keys into key_dictionary
DO
$$
BEGIN
IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'attribute_kv_old') THEN
INSERT INTO key_dictionary(key) SELECT DISTINCT attribute_key FROM attribute_kv_old ON CONFLICT DO NOTHING;
END IF;
END;
$$;
-- migrate attributes from attribute_kv_old to attribute_kv
DO
$$
DECLARE
row_num_old integer;
row_num integer;
BEGIN
IF EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'attribute_kv_old') THEN
INSERT INTO attribute_kv(entity_id, attribute_type, attribute_key, bool_v, str_v, long_v, dbl_v, json_v, last_update_ts)
SELECT a.entity_id, CASE
WHEN a.attribute_type = 'CLIENT_SCOPE' THEN 1
WHEN a.attribute_type = 'SERVER_SCOPE' THEN 2
WHEN a.attribute_type = 'SHARED_SCOPE' THEN 3
ELSE 0
END,
k.key_id, a.bool_v, a.str_v, a.long_v, a.dbl_v, a.json_v, a.last_update_ts
FROM attribute_kv_old a INNER JOIN key_dictionary k ON (a.attribute_key = k.key);
SELECT COUNT(*) INTO row_num_old FROM attribute_kv_old;
SELECT COUNT(*) INTO row_num FROM attribute_kv;
RAISE NOTICE 'Migrated % of % rows', row_num, row_num_old;
IF row_num != 0 THEN
DROP TABLE IF EXISTS attribute_kv_old;
ELSE
RAISE EXCEPTION 'Table attribute_kv is empty';
END IF;
CREATE INDEX IF NOT EXISTS idx_attribute_kv_by_key_and_last_update_ts ON attribute_kv(entity_id, attribute_key, last_update_ts desc);
END IF;
EXCEPTION
WHEN others THEN
ROLLBACK;
RAISE EXCEPTION 'Error during COPY: %', SQLERRM;
END
$$;
-- OAUTH2 PARAMS ALTER TABLE START
ALTER TABLE oauth2_params
ADD COLUMN IF NOT EXISTS edge_enabled boolean DEFAULT false;
-- OAUTH2 PARAMS ALTER TABLE END
-- QUEUE STATS UPDATE START
CREATE TABLE IF NOT EXISTS queue_stats (
id uuid NOT NULL CONSTRAINT queue_stats_pkey PRIMARY KEY,
created_time bigint NOT NULL,
tenant_id uuid NOT NULL,
queue_name varchar(255) NOT NULL,
service_id varchar(255) NOT NULL,
CONSTRAINT queue_stats_name_unq_key UNIQUE (tenant_id, queue_name, service_id)
);
INSERT INTO queue_stats
SELECT id, created_time, tenant_id, substring(name FROM 1 FOR position('_' IN name) - 1) AS queue_name,
substring(name FROM position('_' IN name) + 1) AS service_id
FROM asset
WHERE type = 'TbServiceQueue' and name LIKE '%\_%';
DELETE FROM asset WHERE type='TbServiceQueue';
DELETE FROM asset_profile WHERE name ='TbServiceQueue';
-- QUEUE STATS UPDATE END

23
application/src/main/java/org/thingsboard/server/controller/NotificationController.java

@ -105,6 +105,8 @@ public class NotificationController extends BaseController {
private final NotificationCenter notificationCenter;
private final NotificationSettingsService notificationSettingsService;
private static final String DELIVERY_METHOD_ALLOWABLE_VALUES = "WEB,MOBILE_APP";
@ApiOperation(value = "Get notifications (getNotifications)",
notes = "Returns the page of notifications for current user." + NEW_LINE +
PAGE_DATA_PARAMETERS +
@ -172,10 +174,23 @@ public class NotificationController extends BaseController {
@RequestParam(required = false) String sortOrder,
@Parameter(description = "To search for unread notifications only")
@RequestParam(defaultValue = "false") boolean unreadOnly,
@ApiParam(value = "Delivery method", allowableValues = DELIVERY_METHOD_ALLOWABLE_VALUES)
@RequestParam(defaultValue = "WEB") NotificationDeliveryMethod deliveryMethod,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// no permissions
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return notificationService.findNotificationsByRecipientIdAndReadStatus(user.getTenantId(), user.getId(), unreadOnly, pageLink);
return notificationService.findNotificationsByRecipientIdAndReadStatus(user.getTenantId(), deliveryMethod, user.getId(), unreadOnly, pageLink);
}
@ApiOperation(value = "Get unread notifications count (getUnreadNotificationsCount)",
notes = "Returns unread notifications count for chosen delivery method." +
AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@GetMapping("/notifications/unread/count")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public Integer getUnreadNotificationsCount(@ApiParam(value = "Delivery method", allowableValues = DELIVERY_METHOD_ALLOWABLE_VALUES)
@RequestParam(defaultValue = "MOBILE_APP") NotificationDeliveryMethod deliveryMethod,
@AuthenticationPrincipal SecurityUser user) {
return notificationService.countUnreadNotificationsByRecipientId(user.getTenantId(), deliveryMethod, user.getId());
}
@ApiOperation(value = "Mark notification as read (markNotificationAsRead)",
@ -195,9 +210,11 @@ public class NotificationController extends BaseController {
AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PutMapping("/notifications/read")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void markAllNotificationsAsRead(@AuthenticationPrincipal SecurityUser user) {
public void markAllNotificationsAsRead(@ApiParam(value = "Delivery method", allowableValues = DELIVERY_METHOD_ALLOWABLE_VALUES)
@RequestParam(defaultValue = "WEB") NotificationDeliveryMethod deliveryMethod,
@AuthenticationPrincipal SecurityUser user) {
// no permissions
notificationCenter.markAllNotificationsAsRead(user.getTenantId(), user.getId());
notificationCenter.markAllNotificationsAsRead(user.getTenantId(), deliveryMethod, user.getId());
}
@ApiOperation(value = "Delete notification (deleteNotification)",

5
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -128,8 +128,11 @@ public class ThingsboardInstallService {
databaseEntitiesUpgradeService.upgradeDatabase("3.6.2");
systemDataLoaderService.updateDefaultNotificationConfigs();
case "3.6.3":
log.info("Upgrading ThingsBoard from version 3.6.3 to 3.7.0 ...");
log.info("Upgrading ThingsBoard from version 3.6.3 to 3.6.4 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.6.3");
case "3.6.4":
log.info("Upgrading ThingsBoard from version 3.6.4 to 3.7.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.6.4");
//TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache
break;
default:

5
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -116,7 +116,10 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
updateSchema("3.6.2", 3006002, "3.6.3", 3006003, null);
break;
case "3.6.3":
updateSchema("3.6.3", 3006003, "3.7.0", 3007000, null);
updateSchema("3.6.3", 3006003, "3.6.4", 3006004, null);
break;
case "3.6.4":
updateSchema("3.6.4", 3006004, "3.7.0", 3007000, null);
break;
default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);

32
application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java

@ -81,6 +81,8 @@ import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.WEB;
@Service
@Slf4j
@RequiredArgsConstructor
@ -191,7 +193,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
NotificationProcessingContext ctx = NotificationProcessingContext.builder()
.tenantId(tenantId)
.request(notificationRequest)
.deliveryMethods(Set.of(NotificationDeliveryMethod.WEB))
.deliveryMethods(Set.of(WEB))
.template(template)
.build();
@ -322,6 +324,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
.requestId(request.getId())
.recipientId(recipient.getId())
.type(ctx.getNotificationType())
.deliveryMethod(WEB)
.subject(processedTemplate.getSubject())
.text(processedTemplate.getBody())
.additionalConfig(processedTemplate.getAdditionalConfig())
@ -347,19 +350,22 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
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.getId())
.newStatus(NotificationStatus.READ)
.build();
onNotificationUpdate(tenantId, recipientId, update);
Notification notification = notificationService.findNotificationById(tenantId, notificationId);
if (notification.getDeliveryMethod() == WEB) {
NotificationUpdate update = NotificationUpdate.builder()
.updated(true)
.notificationId(notificationId.getId())
.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) {
public void markAllNotificationsAsRead(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId) {
int updatedCount = notificationService.markAllNotificationsAsRead(tenantId, deliveryMethod, recipientId);
if (updatedCount > 0 && deliveryMethod == WEB) {
log.trace("Marked all notifications as read (recipient id: {}, tenant id: {})", recipientId, tenantId);
NotificationUpdate update = NotificationUpdate.builder()
.updated(true)
@ -374,7 +380,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
public void deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId) {
Notification notification = notificationService.findNotificationById(tenantId, notificationId);
boolean deleted = notificationService.deleteNotification(tenantId, recipientId, notificationId);
if (deleted) {
if (deleted && notification.getDeliveryMethod() == WEB) {
NotificationUpdate update = NotificationUpdate.builder()
.deleted(true)
.notification(notification)
@ -451,7 +457,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
@Override
public NotificationDeliveryMethod getDeliveryMethod() {
return NotificationDeliveryMethod.WEB;
return WEB;
}
@Override
@ -462,7 +468,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
@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);
this.channels.put(WEB, (NotificationChannel) webNotificationChannel);
}
}

50
application/src/main/java/org/thingsboard/server/service/notification/channels/MobileAppNotificationChannel.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.notification.channels;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.MessagingErrorCode;
@ -26,11 +27,15 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.notification.FirebaseService;
import org.thingsboard.server.common.data.User;
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.NotificationStatus;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.settings.MobileAppNotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.template.MobileAppDeliveryMethodNotificationTemplate;
import org.thingsboard.server.dao.notification.NotificationService;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.notification.NotificationProcessingContext;
@ -41,6 +46,8 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.MOBILE_APP;
@Component
@RequiredArgsConstructor
@Slf4j
@ -48,25 +55,54 @@ public class MobileAppNotificationChannel implements NotificationChannel<User, M
private final FirebaseService firebaseService;
private final UserService userService;
private final NotificationService notificationService;
private final NotificationSettingsService notificationSettingsService;
@Override
public void sendNotification(User recipient, MobileAppDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception {
NotificationRequest request = ctx.getRequest();
NotificationInfo info = request.getInfo();
if (info != null && info.getDashboardId() != null) {
ObjectNode additionalConfig = JacksonUtil.asObject(processedTemplate.getAdditionalConfig());
ObjectNode onClick = JacksonUtil.asObject(additionalConfig.get("onClick"));
if (onClick.get("enabled") == null || !Boolean.parseBoolean(onClick.get("enabled").asText())) {
onClick.put("enabled", true);
onClick.put("linkType", "DASHBOARD");
onClick.put("setEntityIdInState", true);
onClick.put("dashboardId", info.getDashboardId().toString());
additionalConfig.set("onClick", onClick);
}
processedTemplate.setAdditionalConfig(additionalConfig);
}
Notification notification = Notification.builder()
.requestId(request.getId())
.recipientId(recipient.getId())
.type(ctx.getNotificationType())
.deliveryMethod(MOBILE_APP)
.subject(processedTemplate.getSubject())
.text(processedTemplate.getBody())
.additionalConfig(processedTemplate.getAdditionalConfig())
.info(info)
.status(NotificationStatus.SENT)
.build();
notificationService.saveNotification(recipient.getTenantId(), notification);
var mobileSessions = userService.findMobileSessions(recipient.getTenantId(), recipient.getId());
if (mobileSessions.isEmpty()) {
throw new IllegalArgumentException("User doesn't use the mobile app");
}
MobileAppNotificationDeliveryMethodConfig config = ctx.getDeliveryMethodConfig(NotificationDeliveryMethod.MOBILE_APP);
MobileAppNotificationDeliveryMethodConfig config = ctx.getDeliveryMethodConfig(MOBILE_APP);
String credentials = config.getFirebaseServiceAccountCredentials();
Set<String> validTokens = new HashSet<>(mobileSessions.keySet());
String subject = processedTemplate.getSubject();
String body = processedTemplate.getBody();
Map<String, String> data = getNotificationData(processedTemplate, ctx);
int unreadCount = notificationService.countUnreadNotificationsByRecipientId(ctx.getTenantId(), MOBILE_APP, recipient.getId());
for (String token : mobileSessions.keySet()) {
try {
firebaseService.sendMessage(ctx.getTenantId(), credentials, token, subject, body, data);
firebaseService.sendMessage(ctx.getTenantId(), credentials, token, subject, body, data, unreadCount);
} catch (FirebaseMessagingException e) {
MessagingErrorCode errorCode = e.getMessagingErrorCode();
if (errorCode == MessagingErrorCode.UNREGISTERED || errorCode == MessagingErrorCode.INVALID_ARGUMENT) {
@ -92,12 +128,6 @@ public class MobileAppNotificationChannel implements NotificationChannel<User, M
Optional.ofNullable(info.getStateEntityId()).ifPresent(stateEntityId -> {
data.put("stateEntityId", stateEntityId.getId().toString());
data.put("stateEntityType", stateEntityId.getEntityType().name());
if (!"true".equals(data.get("onClick.enabled")) && info.getDashboardId() != null) {
data.put("onClick.enabled", "true");
data.put("onClick.linkType", "DASHBOARD");
data.put("onClick.setEntityIdInState", "true");
data.put("onClick.dashboardId", info.getDashboardId().toString());
}
});
data.put("notificationType", ctx.getNotificationType().name());
switch (ctx.getNotificationType()) {
@ -116,14 +146,14 @@ public class MobileAppNotificationChannel implements NotificationChannel<User, M
@Override
public void check(TenantId tenantId) throws Exception {
NotificationSettings systemSettings = notificationSettingsService.findNotificationSettings(TenantId.SYS_TENANT_ID);
if (!systemSettings.getDeliveryMethodsConfigs().containsKey(NotificationDeliveryMethod.MOBILE_APP)) {
if (!systemSettings.getDeliveryMethodsConfigs().containsKey(MOBILE_APP)) {
throw new RuntimeException("Push-notifications to mobile are not configured");
}
}
@Override
public NotificationDeliveryMethod getDeliveryMethod() {
return NotificationDeliveryMethod.MOBILE_APP;
return MOBILE_APP;
}
}

22
application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java

@ -54,7 +54,8 @@ public class DefaultFirebaseService implements FirebaseService {
.build();
@Override
public void sendMessage(TenantId tenantId, String credentials, String fcmToken, String title, String body, Map<String, String> data) throws FirebaseMessagingException {
public void sendMessage(TenantId tenantId, String credentials, String fcmToken, String title, String body,
Map<String, String> data, Integer badge) throws FirebaseMessagingException {
FirebaseContext firebaseContext = contexts.asMap().compute(tenantId.toString(), (key, context) -> {
if (context == null) {
return new FirebaseContext(key, credentials);
@ -64,6 +65,12 @@ public class DefaultFirebaseService implements FirebaseService {
}
});
Aps.Builder apsConfig = Aps.builder()
.setSound("default");
if (badge != null) {
apsConfig.setBadge(badge);
}
Message message = Message.builder()
.setToken(fcmToken)
.setNotification(Notification.builder()
@ -74,14 +81,17 @@ public class DefaultFirebaseService implements FirebaseService {
.setPriority(AndroidConfig.Priority.HIGH)
.build())
.setApnsConfig(ApnsConfig.builder()
.setAps(Aps.builder()
.setContentAvailable(true)
.build())
.setAps(apsConfig.build())
.build())
.putAllData(data)
.build();
firebaseContext.getMessaging().send(message);
log.trace("[{}] Sent message for FCM token {}", tenantId, fcmToken);
try {
firebaseContext.getMessaging().send(message);
log.trace("[{}] Sent message for FCM token {}", tenantId, fcmToken);
} catch (Throwable t) {
log.debug("[{}] Failed to send message for FCM token {}", tenantId, fcmToken, t);
throw t;
}
}
public static class FirebaseContext {

8
application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java

@ -51,6 +51,8 @@ import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.WEB;
@Service
@TbCoreComponent
@RequiredArgsConstructor
@ -104,7 +106,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private void fetchUnreadNotifications(NotificationsSubscription subscription) {
log.trace("[{}, subId: {}] Fetching unread notifications from DB", subscription.getSessionId(), subscription.getSubscriptionId());
PageData<Notification> notifications = notificationService.findLatestUnreadNotificationsByRecipientId(subscription.getTenantId(),
(UserId) subscription.getEntityId(), subscription.getLimit());
WEB, (UserId) subscription.getEntityId(), subscription.getLimit());
subscription.getLatestUnreadNotifications().clear();
notifications.getData().forEach(notification -> {
subscription.getLatestUnreadNotifications().put(notification.getUuidId(), notification);
@ -114,7 +116,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private void fetchUnreadNotificationsCount(NotificationsCountSubscription subscription) {
log.trace("[{}, subId: {}] Fetching unread notifications count from DB", subscription.getSessionId(), subscription.getSubscriptionId());
int unreadCount = notificationService.countUnreadNotificationsByRecipientId(subscription.getTenantId(), (UserId) subscription.getEntityId());
int unreadCount = notificationService.countUnreadNotificationsByRecipientId(subscription.getTenantId(), WEB, (UserId) subscription.getEntityId());
subscription.getTotalUnreadCounter().set(unreadCount);
}
@ -235,7 +237,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
@Override
public void handleMarkAllAsReadCmd(WebSocketSessionRef sessionRef, MarkAllNotificationsAsReadCmd cmd) {
SecurityUser securityCtx = sessionRef.getSecurityCtx();
notificationCenter.markAllNotificationsAsRead(securityCtx.getTenantId(), securityCtx.getId());
notificationCenter.markAllNotificationsAsRead(securityCtx.getTenantId(), WEB, securityCtx.getId());
}
@Override

10
application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java

@ -145,7 +145,7 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
protected NotificationRequest submitNotificationRequest(List<NotificationTargetId> targets, String text, int delayInSec, NotificationDeliveryMethod... deliveryMethods) {
if (deliveryMethods.length == 0) {
deliveryMethods = new NotificationDeliveryMethod[]{NotificationDeliveryMethod.WEB};
deliveryMethods = new NotificationDeliveryMethod[]{NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.MOBILE_APP};
}
NotificationTemplate notificationTemplate = createNotificationTemplate(DEFAULT_NOTIFICATION_TYPE, DEFAULT_NOTIFICATION_SUBJECT, text, deliveryMethods);
return submitNotificationRequest(targets, notificationTemplate.getId(), delayInSec);
@ -247,8 +247,12 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
}
protected List<Notification> getMyNotifications(boolean unreadOnly, int limit) throws Exception {
return doGetTypedWithPageLink("/api/notifications?unreadOnly={unreadOnly}&", new TypeReference<PageData<Notification>>() {},
new PageLink(limit, 0), unreadOnly).getData();
return getMyNotifications(NotificationDeliveryMethod.WEB, unreadOnly, limit);
}
protected List<Notification> getMyNotifications(NotificationDeliveryMethod deliveryMethod, boolean unreadOnly, int limit) throws Exception {
return doGetTypedWithPageLink("/api/notifications?unreadOnly={unreadOnly}&deliveryMethod={deliveryMethod}&", new TypeReference<PageData<Notification>>() {},
new PageLink(limit, 0), unreadOnly, deliveryMethod).getData();
}
protected NotificationRule createNotificationRule(NotificationRuleTriggerConfig triggerConfig, String subject, String text, NotificationTargetId... targets) {

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

@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.NotificationRuleId;
@ -52,6 +53,7 @@ import org.thingsboard.server.common.data.notification.NotificationRequestPrevie
import org.thingsboard.server.common.data.notification.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.AlarmCommentNotificationInfo;
import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.settings.MobileAppNotificationDeliveryMethodConfig;
@ -81,7 +83,6 @@ import org.thingsboard.server.common.data.notification.template.WebDeliveryMetho
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.notification.DefaultNotifications;
import org.thingsboard.server.dao.notification.NotificationDao;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.notification.channels.MicrosoftTeamsNotificationChannel;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate;
@ -116,17 +117,25 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
@Autowired
private NotificationCenter notificationCenter;
@Autowired
private NotificationDao notificationDao;
@Autowired
private MicrosoftTeamsNotificationChannel microsoftTeamsNotificationChannel;
@MockBean
private FirebaseService firebaseService;
private static final String TEST_MOBILE_TOKEN = "tenantFcmToken";
@Before
public void beforeEach() throws Exception {
loginCustomerUser();
wsClient = getWsClient();
loginSysAdmin();
MobileAppNotificationDeliveryMethodConfig config = new MobileAppNotificationDeliveryMethodConfig();
config.setFirebaseServiceAccountCredentials("testCredentials");
saveNotificationSettings(config);
loginTenantAdmin();
mobileToken = TEST_MOBILE_TOKEN;
doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk());
}
@Test
@ -242,7 +251,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
int notificationsCount = 20;
wsClient.registerWaitForUpdate(notificationsCount);
for (int i = 1; i <= notificationsCount; i++) {
submitNotificationRequest(target.getId(), "Test " + i, NotificationDeliveryMethod.WEB);
submitNotificationRequest(target.getId(), "Test " + i, NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.MOBILE_APP);
}
wsClient.waitForUpdate(true);
assertThat(wsClient.getLastDataUpdate().getTotalUnreadCount()).isEqualTo(notificationsCount);
@ -306,7 +315,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
NotificationTarget target = createNotificationTarget(savedDifferentTenantUser.getId());
int notificationsCount = 20;
for (int i = 0; i < notificationsCount; i++) {
NotificationRequest request = submitNotificationRequest(target.getId(), "Test " + i, NotificationDeliveryMethod.WEB);
NotificationRequest request = submitNotificationRequest(target.getId(), "Test " + i, NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.MOBILE_APP);
awaitNotificationRequest(request.getId());
}
List<NotificationRequest> requests = notificationRequestService.findNotificationRequestsByTenantIdAndOriginatorType(tenantId, EntityType.USER, new PageLink(100)).getData();
@ -500,7 +509,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
@Test
public void testNotificationRequestInfo() throws Exception {
NotificationDeliveryMethod[] deliveryMethods = new NotificationDeliveryMethod[]{
NotificationDeliveryMethod.WEB
NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.MOBILE_APP
};
NotificationTemplate template = createNotificationTemplate(NotificationType.GENERAL, "Test subject", "Test text", deliveryMethods);
NotificationTarget target = createNotificationTarget(tenantAdminUserId);
@ -525,7 +534,6 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
await().atMost(2, TimeUnit.SECONDS)
.until(() -> findNotificationRequest(notificationRequest.getId()).isSent());
NotificationRequestStats stats = getStats(notificationRequest.getId());
assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB)).hasValue(1);
}
@ -727,17 +735,12 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
@Test
public void testMobileAppNotifications() throws Exception {
loginSysAdmin();
MobileAppNotificationDeliveryMethodConfig config = new MobileAppNotificationDeliveryMethodConfig();
config.setFirebaseServiceAccountCredentials("testCredentials");
saveNotificationSettings(config);
loginCustomerUser();
mobileToken = "customerFcmToken";
doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk());
loginTenantAdmin();
mobileToken = "tenantFcmToken1";
mobileToken = TEST_MOBILE_TOKEN;
doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk());
mobileToken = "tenantFcmToken2";
doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk());
@ -746,7 +749,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
loginTenantAdmin();
NotificationTarget target = createNotificationTarget(new AllUsersFilter());
NotificationTemplate template = createNotificationTemplate(NotificationType.GENERAL, "Title", "Message", NotificationDeliveryMethod.MOBILE_APP);
NotificationTemplate template = createNotificationTemplate(NotificationType.GENERAL, "Title", "Message", NotificationDeliveryMethod.MOBILE_APP, NotificationDeliveryMethod.WEB);
((MobileAppDeliveryMethodNotificationTemplate) template.getConfiguration().getDeliveryMethodsTemplates().get(NotificationDeliveryMethod.MOBILE_APP))
.setAdditionalConfig(JacksonUtil.newObjectNode().set("test", JacksonUtil.newObjectNode().put("test", "test")));
saveNotificationTemplate(template);
@ -758,11 +761,19 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
.contains("doesn't use the mobile app");
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"),
eq("tenantFcmToken1"), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test"))));
eq(TEST_MOBILE_TOKEN), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test"))), eq(1));
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"),
eq("tenantFcmToken2"), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test"))));
eq("tenantFcmToken2"), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test"))), eq(1));
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"),
eq("customerFcmToken"), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test"))));
eq("customerFcmToken"), eq("Title"), eq("Message"), argThat(data -> "test".equals(data.get("test.test"))), eq(1));
assertThat(getMyNotifications(NotificationDeliveryMethod.MOBILE_APP, true, 10)).singleElement().satisfies(notification -> {
assertThat(notification.getDeliveryMethod()).isEqualTo(NotificationDeliveryMethod.MOBILE_APP);
assertThat(notification.getText()).isEqualTo("Message");
assertThat(notification.getSubject()).isEqualTo("Title");
});
assertThat(getMyNotifications(true, 10)).singleElement().satisfies(notification -> {
assertThat(notification.getDeliveryMethod()).isEqualTo(NotificationDeliveryMethod.WEB);
});
verifyNoMoreInteractions(firebaseService);
clearInvocations(firebaseService);
@ -770,26 +781,24 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
request = submitNotificationRequest(List.of(target.getId()), template.getId(), 0);
awaitNotificationRequest(request.getId());
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"),
eq("tenantFcmToken1"), eq("Title"), eq("Message"), anyMap());
eq(TEST_MOBILE_TOKEN), eq("Title"), eq("Message"), anyMap(), eq(2));
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"),
eq("customerFcmToken"), eq("Title"), eq("Message"), anyMap());
eq("customerFcmToken"), eq("Title"), eq("Message"), anyMap(), eq(2));
verifyNoMoreInteractions(firebaseService);
Integer unreadCount = doGet("/api/notifications/unread/count", Integer.class);
assertThat(unreadCount).isEqualTo(2);
}
@Test
public void testMobileAppNotifications_ruleBased() throws Exception {
loginSysAdmin();
MobileAppNotificationDeliveryMethodConfig config = new MobileAppNotificationDeliveryMethodConfig();
config.setFirebaseServiceAccountCredentials("testCredentials");
saveNotificationSettings(config);
loginTenantAdmin();
mobileToken = "tenantFcmToken";
mobileToken = TEST_MOBILE_TOKEN;
doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk());
createNotificationRule(AlarmCommentNotificationRuleTriggerConfig.builder().onlyUserComments(true).build(),
DefaultNotifications.alarmComment.getSubject(), DefaultNotifications.alarmComment.getText(),
List.of(createNotificationTarget(tenantAdminUserId).getId()), NotificationDeliveryMethod.MOBILE_APP);
List.of(createNotificationTarget(tenantAdminUserId).getId()), NotificationDeliveryMethod.MOBILE_APP, NotificationDeliveryMethod.WEB);
Device device = createDevice("test", "test");
UUID alarmDashboardId = UUID.randomUUID();
@ -802,18 +811,21 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
.put("dashboardId", alarmDashboardId.toString()))
.build();
alarm = doPost("/api/alarm", alarm, Alarm.class);
AlarmId alarmId = alarm.getId();
AlarmComment comment = new AlarmComment();
comment.setComment(JacksonUtil.newObjectNode()
.put("text", "text"));
doPost("/api/alarm/" + alarm.getId() + "/comment", comment, AlarmComment.class);
doPost("/api/alarm/" + alarmId + "/comment", comment, AlarmComment.class);
String expectedSubject = "Comment on 'test' alarm";
String expectedBody = TENANT_ADMIN_EMAIL + " added comment: text";
ArgumentCaptor<Map<String, String>> msgCaptor = ArgumentCaptor.forClass(Map.class);
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"),
eq("tenantFcmToken"), eq("Comment on 'test' alarm"),
eq(TENANT_ADMIN_EMAIL + " added comment: text"),
msgCaptor.capture());
eq(TEST_MOBILE_TOKEN), eq(expectedSubject),
eq(expectedBody),
msgCaptor.capture(), eq(1));
});
Map<String, String> firebaseMessageData = msgCaptor.getValue();
assertThat(firebaseMessageData.keySet()).doesNotContainNull().doesNotContain("");
@ -823,6 +835,14 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
assertThat(firebaseMessageData.get("onClick.enabled")).isEqualTo("true");
assertThat(firebaseMessageData.get("onClick.linkType")).isEqualTo("DASHBOARD");
assertThat(firebaseMessageData.get("onClick.dashboardId")).isEqualTo(alarmDashboardId.toString());
assertThat(getMyNotifications(NotificationDeliveryMethod.MOBILE_APP, true, 10)).singleElement().satisfies(notification -> {
assertThat(notification.getDeliveryMethod()).isEqualTo(NotificationDeliveryMethod.MOBILE_APP);
assertThat(notification.getSubject()).isEqualTo(expectedSubject);
assertThat(notification.getText()).isEqualTo(expectedBody);
assertThat(notification.getInfo()).asInstanceOf(type(AlarmCommentNotificationInfo.class))
.matches(info -> info.getAlarmId().equals(alarmId.getId()) && info.getDashboardId().getId().equals(alarmDashboardId));
});
}
@Test

9
common/dao-api/src/main/java/org/thingsboard/server/dao/notification/NotificationService.java

@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.id.NotificationId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -30,13 +31,13 @@ public interface NotificationService {
boolean markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId);
int markAllNotificationsAsRead(TenantId tenantId, UserId recipientId);
int markAllNotificationsAsRead(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId);
PageData<Notification> findNotificationsByRecipientIdAndReadStatus(TenantId tenantId, UserId recipientId, boolean unreadOnly, PageLink pageLink);
PageData<Notification> findNotificationsByRecipientIdAndReadStatus(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, boolean unreadOnly, PageLink pageLink);
PageData<Notification> findLatestUnreadNotificationsByRecipientId(TenantId tenantId, UserId recipientId, int limit);
PageData<Notification> findLatestUnreadNotificationsByRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, int limit);
int countUnreadNotificationsByRecipientId(TenantId tenantId, UserId recipientId);
int countUnreadNotificationsByRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId);
boolean deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId);

3
common/data/src/main/java/org/thingsboard/server/common/data/notification/Notification.java

@ -36,13 +36,12 @@ public class Notification extends BaseData<NotificationId> {
private NotificationRequestId requestId;
private UserId recipientId;
private NotificationType type;
private NotificationDeliveryMethod deliveryMethod;
private String subject;
private String text;
private JsonNode additionalConfig;
private NotificationInfo info;
private NotificationStatus status;
}

3
common/edge-api/src/main/proto/edge.proto

@ -37,7 +37,8 @@ enum EdgeVersion {
V_3_6_0 = 3;
V_3_6_1 = 4;
V_3_6_2 = 5;
V_3_7_0 = 6;
V_3_6_4 = 6;
V_3_7_0 = 7;
}
/**

4
common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java

@ -255,6 +255,10 @@ public class JacksonUtil {
return node;
}
public static ObjectNode asObject(JsonNode node) {
return node != null && node.isObject() ? ((ObjectNode) node) : newObjectNode();
}
public static void replaceUuidsRecursively(JsonNode node, Set<String> skippedRootFields, Pattern includedFieldsPattern, UnaryOperator<UUID> replacer, boolean root) {
if (node == null) {
return;

1
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -629,6 +629,7 @@ public class ModelConstants {
public static final String NOTIFICATION_REQUEST_ID_PROPERTY = "request_id";
public static final String NOTIFICATION_RECIPIENT_ID_PROPERTY = "recipient_id";
public static final String NOTIFICATION_TYPE_PROPERTY = "type";
public static final String NOTIFICATION_DELIVERY_METHOD_PROPERTY = "delivery_method";
public static final String NOTIFICATION_SUBJECT_PROPERTY = "subject";
public static final String NOTIFICATION_TEXT_PROPERTY = "body";
public static final String NOTIFICATION_ADDITIONAL_CONFIG_PROPERTY = "additional_config";

7
dao/src/main/java/org/thingsboard/server/dao/model/sql/NotificationEntity.java

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.NotificationId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
@ -54,6 +55,10 @@ public class NotificationEntity extends BaseSqlEntity<Notification> {
@Column(name = ModelConstants.NOTIFICATION_TYPE_PROPERTY, nullable = false)
private NotificationType type;
@Enumerated(EnumType.STRING)
@Column(name = ModelConstants.NOTIFICATION_DELIVERY_METHOD_PROPERTY, nullable = false)
private NotificationDeliveryMethod deliveryMethod;
@Column(name = ModelConstants.NOTIFICATION_SUBJECT_PROPERTY)
private String subject;
@ -80,6 +85,7 @@ public class NotificationEntity extends BaseSqlEntity<Notification> {
setRequestId(getUuid(notification.getRequestId()));
setRecipientId(getUuid(notification.getRecipientId()));
setType(notification.getType());
setDeliveryMethod(notification.getDeliveryMethod());
setSubject(notification.getSubject());
setText(notification.getText());
setAdditionalConfig(notification.getAdditionalConfig());
@ -95,6 +101,7 @@ public class NotificationEntity extends BaseSqlEntity<Notification> {
notification.setRequestId(getEntityId(requestId, NotificationRequestId::new));
notification.setRecipientId(getEntityId(recipientId, UserId::new));
notification.setType(type);
notification.setDeliveryMethod(deliveryMethod);
notification.setSubject(subject);
notification.setText(text);
notification.setAdditionalConfig(additionalConfig);

19
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationService.java

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.NotificationId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -57,29 +58,29 @@ public class DefaultNotificationService implements NotificationService, EntityDa
}
@Override
public int markAllNotificationsAsRead(TenantId tenantId, UserId recipientId) {
return notificationDao.updateStatusByRecipientId(tenantId, recipientId, NotificationStatus.READ);
public int markAllNotificationsAsRead(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId) {
return notificationDao.updateStatusByDeliveryMethodAndRecipientId(tenantId, deliveryMethod, recipientId, NotificationStatus.READ);
}
@Override
public PageData<Notification> findNotificationsByRecipientIdAndReadStatus(TenantId tenantId, UserId recipientId, boolean unreadOnly, PageLink pageLink) {
public PageData<Notification> findNotificationsByRecipientIdAndReadStatus(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, boolean unreadOnly, PageLink pageLink) {
if (unreadOnly) {
return notificationDao.findUnreadByRecipientIdAndPageLink(tenantId, recipientId, pageLink);
return notificationDao.findUnreadByDeliveryMethodAndRecipientIdAndPageLink(tenantId, deliveryMethod, recipientId, pageLink);
} else {
return notificationDao.findByRecipientIdAndPageLink(tenantId, recipientId, pageLink);
return notificationDao.findByDeliveryMethodAndRecipientIdAndPageLink(tenantId, deliveryMethod, recipientId, pageLink);
}
}
@Override
public PageData<Notification> findLatestUnreadNotificationsByRecipientId(TenantId tenantId, UserId recipientId, int limit) {
public PageData<Notification> findLatestUnreadNotificationsByRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, int limit) {
SortOrder sortOrder = new SortOrder(EntityKeyMapping.CREATED_TIME, SortOrder.Direction.DESC);
PageLink pageLink = new PageLink(limit, 0, null, sortOrder);
return findNotificationsByRecipientIdAndReadStatus(tenantId, recipientId, true, pageLink);
return findNotificationsByRecipientIdAndReadStatus(tenantId, deliveryMethod, recipientId, true, pageLink);
}
@Override
public int countUnreadNotificationsByRecipientId(TenantId tenantId, UserId recipientId) {
return notificationDao.countUnreadByRecipientId(tenantId, recipientId);
public int countUnreadNotificationsByRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId) {
return notificationDao.countUnreadByDeliveryMethodAndRecipientId(tenantId, deliveryMethod, recipientId);
}
@Override

11
dao/src/main/java/org/thingsboard/server/dao/notification/NotificationDao.java

@ -20,6 +20,7 @@ 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.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -27,19 +28,17 @@ import org.thingsboard.server.dao.Dao;
public interface NotificationDao extends Dao<Notification> {
PageData<Notification> findUnreadByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink);
PageData<Notification> findUnreadByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink);
PageData<Notification> findByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink);
PageData<Notification> findByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink);
boolean updateStatusByIdAndRecipientId(TenantId tenantId, UserId recipientId, NotificationId notificationId, NotificationStatus status);
int countUnreadByRecipientId(TenantId tenantId, UserId recipientId);
PageData<Notification> findByRequestId(TenantId tenantId, NotificationRequestId notificationRequestId, PageLink pageLink);
int countUnreadByDeliveryMethodAndRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId);
boolean deleteByIdAndRecipientId(TenantId tenantId, UserId recipientId, NotificationId notificationId);
int updateStatusByRecipientId(TenantId tenantId, UserId recipientId, NotificationStatus status);
int updateStatusByDeliveryMethodAndRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, NotificationStatus status);
void deleteByRequestId(TenantId tenantId, NotificationRequestId requestId);

24
dao/src/main/java/org/thingsboard/server/dao/sql/notification/JpaNotificationDao.java

@ -25,6 +25,7 @@ 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.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -51,14 +52,14 @@ public class JpaNotificationDao extends JpaPartitionedAbstractDao<NotificationEn
private int partitionSizeInHours;
@Override
public PageData<Notification> findUnreadByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink) {
return DaoUtil.toPageData(notificationRepository.findByRecipientIdAndStatusNot(recipientId.getId(), NotificationStatus.READ,
pageLink.getTextSearch(), DaoUtil.toPageable(pageLink)));
public PageData<Notification> findUnreadByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink) {
return DaoUtil.toPageData(notificationRepository.findByDeliveryMethodAndRecipientIdAndStatusNot(deliveryMethod,
recipientId.getId(), NotificationStatus.READ, pageLink.getTextSearch(), DaoUtil.toPageable(pageLink)));
}
@Override
public PageData<Notification> findByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink) {
return DaoUtil.toPageData(notificationRepository.findByRecipientId(recipientId.getId(),
public PageData<Notification> findByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink) {
return DaoUtil.toPageData(notificationRepository.findByDeliveryMethodAndRecipientId(deliveryMethod, recipientId.getId(),
pageLink.getTextSearch(), DaoUtil.toPageable(pageLink)));
}
@ -71,13 +72,8 @@ public class JpaNotificationDao extends JpaPartitionedAbstractDao<NotificationEn
* For this hot method, the partial index `idx_notification_recipient_id_unread` was introduced since 3.6.0
* */
@Override
public int countUnreadByRecipientId(TenantId tenantId, UserId recipientId) {
return notificationRepository.countByRecipientIdAndStatusNot(recipientId.getId(), NotificationStatus.READ);
}
@Override
public PageData<Notification> findByRequestId(TenantId tenantId, NotificationRequestId notificationRequestId, PageLink pageLink) {
return DaoUtil.toPageData(notificationRepository.findByRequestId(notificationRequestId.getId(), DaoUtil.toPageable(pageLink)));
public int countUnreadByDeliveryMethodAndRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId) {
return notificationRepository.countByDeliveryMethodAndRecipientIdAndStatusNot(deliveryMethod, recipientId.getId(), NotificationStatus.READ);
}
@Override
@ -86,8 +82,8 @@ public class JpaNotificationDao extends JpaPartitionedAbstractDao<NotificationEn
}
@Override
public int updateStatusByRecipientId(TenantId tenantId, UserId recipientId, NotificationStatus status) {
return notificationRepository.updateStatusByRecipientId(recipientId.getId(), status);
public int updateStatusByDeliveryMethodAndRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, NotificationStatus status) {
return notificationRepository.updateStatusByDeliveryMethodAndRecipientIdAndStatusNot(deliveryMethod, recipientId.getId(), status);
}
@Override

33
dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationRepository.java

@ -23,6 +23,7 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.dao.model.sql.NotificationEntity;
@ -31,20 +32,23 @@ import java.util.UUID;
@Repository
public interface NotificationRepository extends JpaRepository<NotificationEntity, UUID> {
@Query("SELECT n FROM NotificationEntity n WHERE n.recipientId = :recipientId AND n.status <> :status " +
@Query("SELECT n FROM NotificationEntity n WHERE n.deliveryMethod = :deliveryMethod " +
"AND n.recipientId = :recipientId AND n.status <> :status " +
"AND (:searchText is NULL OR ilike(n.subject, concat('%', :searchText, '%')) = true " +
"OR ilike(n.text, concat('%', :searchText, '%')) = true)")
Page<NotificationEntity> findByRecipientIdAndStatusNot(@Param("recipientId") UUID recipientId,
@Param("status") NotificationStatus status,
@Param("searchText") String searchText,
Pageable pageable);
Page<NotificationEntity> findByDeliveryMethodAndRecipientIdAndStatusNot(@Param("deliveryMethod") NotificationDeliveryMethod deliveryMethod,
@Param("recipientId") UUID recipientId,
@Param("status") NotificationStatus status,
@Param("searchText") String searchText,
Pageable pageable);
@Query("SELECT n FROM NotificationEntity n WHERE n.recipientId = :recipientId " +
@Query("SELECT n FROM NotificationEntity n WHERE n.deliveryMethod = :deliveryMethod AND n.recipientId = :recipientId " +
"AND (:searchText is NULL OR ilike(n.subject, concat('%', :searchText, '%')) = true " +
"OR ilike(n.text, concat('%', :searchText, '%')) = true)")
Page<NotificationEntity> findByRecipientId(@Param("recipientId") UUID recipientId,
@Param("searchText") String searchText,
Pageable pageable);
Page<NotificationEntity> findByDeliveryMethodAndRecipientId(@Param("deliveryMethod") NotificationDeliveryMethod deliveryMethod,
@Param("recipientId") UUID recipientId,
@Param("searchText") String searchText,
Pageable pageable);
@Modifying
@Transactional
@ -54,9 +58,7 @@ public interface NotificationRepository extends JpaRepository<NotificationEntity
@Param("recipientId") UUID recipientId,
@Param("status") NotificationStatus status);
int countByRecipientIdAndStatusNot(UUID recipientId, NotificationStatus status);
Page<NotificationEntity> findByRequestId(UUID requestId, Pageable pageable);
int countByDeliveryMethodAndRecipientIdAndStatusNot(NotificationDeliveryMethod deliveryMethod, UUID recipientId, NotificationStatus status);
@Transactional
@Modifying
@ -76,8 +78,9 @@ public interface NotificationRepository extends JpaRepository<NotificationEntity
@Modifying
@Transactional
@Query("UPDATE NotificationEntity n SET n.status = :status " +
"WHERE n.recipientId = :recipientId AND n.status <> :status")
int updateStatusByRecipientId(@Param("recipientId") UUID recipientId,
@Param("status") NotificationStatus status);
"WHERE n.deliveryMethod = :deliveryMethod AND n.recipientId = :recipientId AND n.status <> :status")
int updateStatusByDeliveryMethodAndRecipientIdAndStatusNot(@Param("deliveryMethod") NotificationDeliveryMethod deliveryMethod,
@Param("recipientId") UUID recipientId,
@Param("status") NotificationStatus status);
}

4
dao/src/main/resources/sql/schema-entities-idx.sql

@ -118,9 +118,9 @@ CREATE INDEX IF NOT EXISTS idx_notification_id ON notification(id);
CREATE INDEX IF NOT EXISTS idx_notification_notification_request_id ON notification(request_id);
CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_created_time ON notification(recipient_id, created_time DESC);
CREATE INDEX IF NOT EXISTS idx_notification_delivery_method_recipient_id_created_time ON notification(delivery_method, recipient_id, created_time DESC);
CREATE INDEX IF NOT EXISTS idx_notification_recipient_id_unread ON notification(recipient_id) WHERE status <> 'READ';
CREATE INDEX IF NOT EXISTS idx_notification_delivery_method_recipient_id_unread ON notification(delivery_method, recipient_id) WHERE status <> 'READ';
CREATE INDEX IF NOT EXISTS idx_resource_etag ON resource(tenant_id, etag);

3
dao/src/main/resources/sql/schema-entities.sql

@ -25,7 +25,7 @@ CREATE OR REPLACE PROCEDURE insert_tb_schema_settings()
$$
BEGIN
IF (SELECT COUNT(*) FROM tb_schema_settings) = 0 THEN
INSERT INTO tb_schema_settings (schema_version) VALUES (3006000);
INSERT INTO tb_schema_settings (schema_version) VALUES (3006004);
END IF;
END;
$$;
@ -864,6 +864,7 @@ CREATE TABLE IF NOT EXISTS notification (
request_id UUID,
recipient_id UUID NOT NULL,
type VARCHAR(50) NOT NULL,
delivery_method VARCHAR(50) NOT NULL,
subject VARCHAR(255),
body VARCHAR(1000) NOT NULL,
additional_config VARCHAR(1000),

2
msa/tb/pom.xml

@ -38,7 +38,7 @@
<tb-postgres.docker.name>tb-postgres</tb-postgres.docker.name>
<tb-cassandra.docker.name>tb-cassandra</tb-cassandra.docker.name>
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
<pkg.upgradeVersion>3.6.3</pkg.upgradeVersion>
<pkg.upgradeVersion>3.6.4</pkg.upgradeVersion>
</properties>
<dependencies>

2
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NotificationCenter.java

@ -38,7 +38,7 @@ public interface NotificationCenter {
void markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId);
void markAllNotificationsAsRead(TenantId tenantId, UserId recipientId);
void markAllNotificationsAsRead(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId);
void deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId);

2
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/notification/FirebaseService.java

@ -21,6 +21,6 @@ import java.util.Map;
public interface FirebaseService {
void sendMessage(TenantId tenantId, String credentials, String fcmToken, String title, String body, Map<String, String> data) throws Exception;
void sendMessage(TenantId tenantId, String credentials, String fcmToken, String title, String body, Map<String, String> data, Integer badge) throws Exception;
}

Loading…
Cancel
Save