Browse Source

Merge with hotfix/3.6.3

pull/10555/head
Igor Kulikov 3 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. -- limitations under the License.
-- --
-- create new attribute_kv table schema -- NOTIFICATIONS UPDATE START
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 ALTER TABLE notification ADD COLUMN IF NOT EXISTS delivery_method VARCHAR(50) NOT NULL default 'WEB';
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 DROP INDEX IF EXISTS idx_notification_recipient_id_created_time;
DO DROP INDEX IF EXISTS idx_notification_recipient_id_unread;
$$
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 CREATE INDEX IF NOT EXISTS idx_notification_delivery_method_recipient_id_created_time ON notification(delivery_method, recipient_id, created_time DESC);
DO CREATE INDEX IF NOT EXISTS idx_notification_delivery_method_recipient_id_unread ON notification(delivery_method, recipient_id) WHERE status <> 'READ';
$$
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 -- NOTIFICATIONS UPDATE END
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

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 NotificationCenter notificationCenter;
private final NotificationSettingsService notificationSettingsService; private final NotificationSettingsService notificationSettingsService;
private static final String DELIVERY_METHOD_ALLOWABLE_VALUES = "WEB,MOBILE_APP";
@ApiOperation(value = "Get notifications (getNotifications)", @ApiOperation(value = "Get notifications (getNotifications)",
notes = "Returns the page of notifications for current user." + NEW_LINE + notes = "Returns the page of notifications for current user." + NEW_LINE +
PAGE_DATA_PARAMETERS + PAGE_DATA_PARAMETERS +
@ -172,10 +174,23 @@ public class NotificationController extends BaseController {
@RequestParam(required = false) String sortOrder, @RequestParam(required = false) String sortOrder,
@Parameter(description = "To search for unread notifications only") @Parameter(description = "To search for unread notifications only")
@RequestParam(defaultValue = "false") boolean unreadOnly, @RequestParam(defaultValue = "false") boolean unreadOnly,
@ApiParam(value = "Delivery method", allowableValues = DELIVERY_METHOD_ALLOWABLE_VALUES)
@RequestParam(defaultValue = "WEB") NotificationDeliveryMethod deliveryMethod,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException { @AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// no permissions // no permissions
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); 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)", @ApiOperation(value = "Mark notification as read (markNotificationAsRead)",
@ -195,9 +210,11 @@ public class NotificationController extends BaseController {
AVAILABLE_FOR_ANY_AUTHORIZED_USER) AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PutMapping("/notifications/read") @PutMapping("/notifications/read")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @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 // no permissions
notificationCenter.markAllNotificationsAsRead(user.getTenantId(), user.getId()); notificationCenter.markAllNotificationsAsRead(user.getTenantId(), deliveryMethod, user.getId());
} }
@ApiOperation(value = "Delete notification (deleteNotification)", @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"); databaseEntitiesUpgradeService.upgradeDatabase("3.6.2");
systemDataLoaderService.updateDefaultNotificationConfigs(); systemDataLoaderService.updateDefaultNotificationConfigs();
case "3.6.3": 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"); 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 //TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache
break; break;
default: 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); updateSchema("3.6.2", 3006002, "3.6.3", 3006003, null);
break; break;
case "3.6.3": 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; break;
default: default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); 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.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.WEB;
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@ -191,7 +193,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
NotificationProcessingContext ctx = NotificationProcessingContext.builder() NotificationProcessingContext ctx = NotificationProcessingContext.builder()
.tenantId(tenantId) .tenantId(tenantId)
.request(notificationRequest) .request(notificationRequest)
.deliveryMethods(Set.of(NotificationDeliveryMethod.WEB)) .deliveryMethods(Set.of(WEB))
.template(template) .template(template)
.build(); .build();
@ -322,6 +324,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
.requestId(request.getId()) .requestId(request.getId())
.recipientId(recipient.getId()) .recipientId(recipient.getId())
.type(ctx.getNotificationType()) .type(ctx.getNotificationType())
.deliveryMethod(WEB)
.subject(processedTemplate.getSubject()) .subject(processedTemplate.getSubject())
.text(processedTemplate.getBody()) .text(processedTemplate.getBody())
.additionalConfig(processedTemplate.getAdditionalConfig()) .additionalConfig(processedTemplate.getAdditionalConfig())
@ -347,19 +350,22 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
boolean updated = notificationService.markNotificationAsRead(tenantId, recipientId, notificationId); boolean updated = notificationService.markNotificationAsRead(tenantId, recipientId, notificationId);
if (updated) { if (updated) {
log.trace("Marked notification {} as read (recipient id: {}, tenant id: {})", notificationId, recipientId, tenantId); log.trace("Marked notification {} as read (recipient id: {}, tenant id: {})", notificationId, recipientId, tenantId);
NotificationUpdate update = NotificationUpdate.builder() Notification notification = notificationService.findNotificationById(tenantId, notificationId);
.updated(true) if (notification.getDeliveryMethod() == WEB) {
.notificationId(notificationId.getId()) NotificationUpdate update = NotificationUpdate.builder()
.newStatus(NotificationStatus.READ) .updated(true)
.build(); .notificationId(notificationId.getId())
onNotificationUpdate(tenantId, recipientId, update); .newStatus(NotificationStatus.READ)
.build();
onNotificationUpdate(tenantId, recipientId, update);
}
} }
} }
@Override @Override
public void markAllNotificationsAsRead(TenantId tenantId, UserId recipientId) { public void markAllNotificationsAsRead(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId) {
int updatedCount = notificationService.markAllNotificationsAsRead(tenantId, recipientId); int updatedCount = notificationService.markAllNotificationsAsRead(tenantId, deliveryMethod, recipientId);
if (updatedCount > 0) { if (updatedCount > 0 && deliveryMethod == WEB) {
log.trace("Marked all notifications as read (recipient id: {}, tenant id: {})", recipientId, tenantId); log.trace("Marked all notifications as read (recipient id: {}, tenant id: {})", recipientId, tenantId);
NotificationUpdate update = NotificationUpdate.builder() NotificationUpdate update = NotificationUpdate.builder()
.updated(true) .updated(true)
@ -374,7 +380,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
public void deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId) { public void deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId) {
Notification notification = notificationService.findNotificationById(tenantId, notificationId); Notification notification = notificationService.findNotificationById(tenantId, notificationId);
boolean deleted = notificationService.deleteNotification(tenantId, recipientId, notificationId); boolean deleted = notificationService.deleteNotification(tenantId, recipientId, notificationId);
if (deleted) { if (deleted && notification.getDeliveryMethod() == WEB) {
NotificationUpdate update = NotificationUpdate.builder() NotificationUpdate update = NotificationUpdate.builder()
.deleted(true) .deleted(true)
.notification(notification) .notification(notification)
@ -451,7 +457,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
@Override @Override
public NotificationDeliveryMethod getDeliveryMethod() { public NotificationDeliveryMethod getDeliveryMethod() {
return NotificationDeliveryMethod.WEB; return WEB;
} }
@Override @Override
@ -462,7 +468,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
@Autowired @Autowired
public void setChannels(List<NotificationChannel> channels, NotificationCenter webNotificationChannel) { public void setChannels(List<NotificationChannel> channels, NotificationCenter webNotificationChannel) {
this.channels = channels.stream().collect(Collectors.toMap(NotificationChannel::getDeliveryMethod, c -> c)); 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; package org.thingsboard.server.service.notification.channels;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.firebase.messaging.FirebaseMessagingException; import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.MessagingErrorCode; 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.rule.engine.api.notification.FirebaseService;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.TenantId; 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.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.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.settings.MobileAppNotificationDeliveryMethodConfig; 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.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.template.MobileAppDeliveryMethodNotificationTemplate; 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.notification.NotificationSettingsService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.notification.NotificationProcessingContext; import org.thingsboard.server.service.notification.NotificationProcessingContext;
@ -41,6 +46,8 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.MOBILE_APP;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
@ -48,25 +55,54 @@ public class MobileAppNotificationChannel implements NotificationChannel<User, M
private final FirebaseService firebaseService; private final FirebaseService firebaseService;
private final UserService userService; private final UserService userService;
private final NotificationService notificationService;
private final NotificationSettingsService notificationSettingsService; private final NotificationSettingsService notificationSettingsService;
@Override @Override
public void sendNotification(User recipient, MobileAppDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception { 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()); var mobileSessions = userService.findMobileSessions(recipient.getTenantId(), recipient.getId());
if (mobileSessions.isEmpty()) { if (mobileSessions.isEmpty()) {
throw new IllegalArgumentException("User doesn't use the mobile app"); 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(); String credentials = config.getFirebaseServiceAccountCredentials();
Set<String> validTokens = new HashSet<>(mobileSessions.keySet()); Set<String> validTokens = new HashSet<>(mobileSessions.keySet());
String subject = processedTemplate.getSubject(); String subject = processedTemplate.getSubject();
String body = processedTemplate.getBody(); String body = processedTemplate.getBody();
Map<String, String> data = getNotificationData(processedTemplate, ctx); Map<String, String> data = getNotificationData(processedTemplate, ctx);
int unreadCount = notificationService.countUnreadNotificationsByRecipientId(ctx.getTenantId(), MOBILE_APP, recipient.getId());
for (String token : mobileSessions.keySet()) { for (String token : mobileSessions.keySet()) {
try { try {
firebaseService.sendMessage(ctx.getTenantId(), credentials, token, subject, body, data); firebaseService.sendMessage(ctx.getTenantId(), credentials, token, subject, body, data, unreadCount);
} catch (FirebaseMessagingException e) { } catch (FirebaseMessagingException e) {
MessagingErrorCode errorCode = e.getMessagingErrorCode(); MessagingErrorCode errorCode = e.getMessagingErrorCode();
if (errorCode == MessagingErrorCode.UNREGISTERED || errorCode == MessagingErrorCode.INVALID_ARGUMENT) { 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 -> { Optional.ofNullable(info.getStateEntityId()).ifPresent(stateEntityId -> {
data.put("stateEntityId", stateEntityId.getId().toString()); data.put("stateEntityId", stateEntityId.getId().toString());
data.put("stateEntityType", stateEntityId.getEntityType().name()); 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()); data.put("notificationType", ctx.getNotificationType().name());
switch (ctx.getNotificationType()) { switch (ctx.getNotificationType()) {
@ -116,14 +146,14 @@ public class MobileAppNotificationChannel implements NotificationChannel<User, M
@Override @Override
public void check(TenantId tenantId) throws Exception { public void check(TenantId tenantId) throws Exception {
NotificationSettings systemSettings = notificationSettingsService.findNotificationSettings(TenantId.SYS_TENANT_ID); 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"); throw new RuntimeException("Push-notifications to mobile are not configured");
} }
} }
@Override @Override
public NotificationDeliveryMethod getDeliveryMethod() { 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(); .build();
@Override @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) -> { FirebaseContext firebaseContext = contexts.asMap().compute(tenantId.toString(), (key, context) -> {
if (context == null) { if (context == null) {
return new FirebaseContext(key, credentials); 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() Message message = Message.builder()
.setToken(fcmToken) .setToken(fcmToken)
.setNotification(Notification.builder() .setNotification(Notification.builder()
@ -74,14 +81,17 @@ public class DefaultFirebaseService implements FirebaseService {
.setPriority(AndroidConfig.Priority.HIGH) .setPriority(AndroidConfig.Priority.HIGH)
.build()) .build())
.setApnsConfig(ApnsConfig.builder() .setApnsConfig(ApnsConfig.builder()
.setAps(Aps.builder() .setAps(apsConfig.build())
.setContentAvailable(true)
.build())
.build()) .build())
.putAllData(data) .putAllData(data)
.build(); .build();
firebaseContext.getMessaging().send(message); try {
log.trace("[{}] Sent message for FCM token {}", tenantId, fcmToken); 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 { 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.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.WEB;
@Service @Service
@TbCoreComponent @TbCoreComponent
@RequiredArgsConstructor @RequiredArgsConstructor
@ -104,7 +106,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private void fetchUnreadNotifications(NotificationsSubscription subscription) { private void fetchUnreadNotifications(NotificationsSubscription subscription) {
log.trace("[{}, subId: {}] Fetching unread notifications from DB", subscription.getSessionId(), subscription.getSubscriptionId()); log.trace("[{}, subId: {}] Fetching unread notifications from DB", subscription.getSessionId(), subscription.getSubscriptionId());
PageData<Notification> notifications = notificationService.findLatestUnreadNotificationsByRecipientId(subscription.getTenantId(), PageData<Notification> notifications = notificationService.findLatestUnreadNotificationsByRecipientId(subscription.getTenantId(),
(UserId) subscription.getEntityId(), subscription.getLimit()); WEB, (UserId) subscription.getEntityId(), subscription.getLimit());
subscription.getLatestUnreadNotifications().clear(); subscription.getLatestUnreadNotifications().clear();
notifications.getData().forEach(notification -> { notifications.getData().forEach(notification -> {
subscription.getLatestUnreadNotifications().put(notification.getUuidId(), notification); subscription.getLatestUnreadNotifications().put(notification.getUuidId(), notification);
@ -114,7 +116,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
private void fetchUnreadNotificationsCount(NotificationsCountSubscription subscription) { private void fetchUnreadNotificationsCount(NotificationsCountSubscription subscription) {
log.trace("[{}, subId: {}] Fetching unread notifications count from DB", subscription.getSessionId(), subscription.getSubscriptionId()); 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); subscription.getTotalUnreadCounter().set(unreadCount);
} }
@ -235,7 +237,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
@Override @Override
public void handleMarkAllAsReadCmd(WebSocketSessionRef sessionRef, MarkAllNotificationsAsReadCmd cmd) { public void handleMarkAllAsReadCmd(WebSocketSessionRef sessionRef, MarkAllNotificationsAsReadCmd cmd) {
SecurityUser securityCtx = sessionRef.getSecurityCtx(); SecurityUser securityCtx = sessionRef.getSecurityCtx();
notificationCenter.markAllNotificationsAsRead(securityCtx.getTenantId(), securityCtx.getId()); notificationCenter.markAllNotificationsAsRead(securityCtx.getTenantId(), WEB, securityCtx.getId());
} }
@Override @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) { protected NotificationRequest submitNotificationRequest(List<NotificationTargetId> targets, String text, int delayInSec, NotificationDeliveryMethod... deliveryMethods) {
if (deliveryMethods.length == 0) { 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); NotificationTemplate notificationTemplate = createNotificationTemplate(DEFAULT_NOTIFICATION_TYPE, DEFAULT_NOTIFICATION_SUBJECT, text, deliveryMethods);
return submitNotificationRequest(targets, notificationTemplate.getId(), delayInSec); 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 { protected List<Notification> getMyNotifications(boolean unreadOnly, int limit) throws Exception {
return doGetTypedWithPageLink("/api/notifications?unreadOnly={unreadOnly}&", new TypeReference<PageData<Notification>>() {}, return getMyNotifications(NotificationDeliveryMethod.WEB, unreadOnly, limit);
new PageLink(limit, 0), unreadOnly).getData(); }
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) { 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.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.audit.ActionType; 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.DeviceId;
import org.thingsboard.server.common.data.id.NotificationRequestId; import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.NotificationRuleId; 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.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus; import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.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.info.EntityActionNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.settings.MobileAppNotificationDeliveryMethodConfig; 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.page.PageLink;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.notification.DefaultNotifications; 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.dao.service.DaoSqlTest;
import org.thingsboard.server.service.notification.channels.MicrosoftTeamsNotificationChannel; import org.thingsboard.server.service.notification.channels.MicrosoftTeamsNotificationChannel;
import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate; import org.thingsboard.server.service.ws.notification.cmd.UnreadNotificationsUpdate;
@ -116,17 +117,25 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
@Autowired @Autowired
private NotificationCenter notificationCenter; private NotificationCenter notificationCenter;
@Autowired @Autowired
private NotificationDao notificationDao;
@Autowired
private MicrosoftTeamsNotificationChannel microsoftTeamsNotificationChannel; private MicrosoftTeamsNotificationChannel microsoftTeamsNotificationChannel;
@MockBean @MockBean
private FirebaseService firebaseService; private FirebaseService firebaseService;
private static final String TEST_MOBILE_TOKEN = "tenantFcmToken";
@Before @Before
public void beforeEach() throws Exception { public void beforeEach() throws Exception {
loginCustomerUser(); loginCustomerUser();
wsClient = getWsClient(); wsClient = getWsClient();
loginSysAdmin();
MobileAppNotificationDeliveryMethodConfig config = new MobileAppNotificationDeliveryMethodConfig();
config.setFirebaseServiceAccountCredentials("testCredentials");
saveNotificationSettings(config);
loginTenantAdmin(); loginTenantAdmin();
mobileToken = TEST_MOBILE_TOKEN;
doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk());
} }
@Test @Test
@ -242,7 +251,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
int notificationsCount = 20; int notificationsCount = 20;
wsClient.registerWaitForUpdate(notificationsCount); wsClient.registerWaitForUpdate(notificationsCount);
for (int i = 1; i <= notificationsCount; i++) { 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); wsClient.waitForUpdate(true);
assertThat(wsClient.getLastDataUpdate().getTotalUnreadCount()).isEqualTo(notificationsCount); assertThat(wsClient.getLastDataUpdate().getTotalUnreadCount()).isEqualTo(notificationsCount);
@ -306,7 +315,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
NotificationTarget target = createNotificationTarget(savedDifferentTenantUser.getId()); NotificationTarget target = createNotificationTarget(savedDifferentTenantUser.getId());
int notificationsCount = 20; int notificationsCount = 20;
for (int i = 0; i < notificationsCount; i++) { 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()); awaitNotificationRequest(request.getId());
} }
List<NotificationRequest> requests = notificationRequestService.findNotificationRequestsByTenantIdAndOriginatorType(tenantId, EntityType.USER, new PageLink(100)).getData(); List<NotificationRequest> requests = notificationRequestService.findNotificationRequestsByTenantIdAndOriginatorType(tenantId, EntityType.USER, new PageLink(100)).getData();
@ -500,7 +509,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
@Test @Test
public void testNotificationRequestInfo() throws Exception { public void testNotificationRequestInfo() throws Exception {
NotificationDeliveryMethod[] deliveryMethods = new NotificationDeliveryMethod[]{ NotificationDeliveryMethod[] deliveryMethods = new NotificationDeliveryMethod[]{
NotificationDeliveryMethod.WEB NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.MOBILE_APP
}; };
NotificationTemplate template = createNotificationTemplate(NotificationType.GENERAL, "Test subject", "Test text", deliveryMethods); NotificationTemplate template = createNotificationTemplate(NotificationType.GENERAL, "Test subject", "Test text", deliveryMethods);
NotificationTarget target = createNotificationTarget(tenantAdminUserId); NotificationTarget target = createNotificationTarget(tenantAdminUserId);
@ -525,7 +534,6 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
await().atMost(2, TimeUnit.SECONDS) await().atMost(2, TimeUnit.SECONDS)
.until(() -> findNotificationRequest(notificationRequest.getId()).isSent()); .until(() -> findNotificationRequest(notificationRequest.getId()).isSent());
NotificationRequestStats stats = getStats(notificationRequest.getId()); NotificationRequestStats stats = getStats(notificationRequest.getId());
assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB)).hasValue(1); assertThat(stats.getSent().get(NotificationDeliveryMethod.WEB)).hasValue(1);
} }
@ -727,17 +735,12 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
@Test @Test
public void testMobileAppNotifications() throws Exception { public void testMobileAppNotifications() throws Exception {
loginSysAdmin();
MobileAppNotificationDeliveryMethodConfig config = new MobileAppNotificationDeliveryMethodConfig();
config.setFirebaseServiceAccountCredentials("testCredentials");
saveNotificationSettings(config);
loginCustomerUser(); loginCustomerUser();
mobileToken = "customerFcmToken"; mobileToken = "customerFcmToken";
doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk()); doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk());
loginTenantAdmin(); loginTenantAdmin();
mobileToken = "tenantFcmToken1"; mobileToken = TEST_MOBILE_TOKEN;
doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk()); doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk());
mobileToken = "tenantFcmToken2"; mobileToken = "tenantFcmToken2";
doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk()); doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk());
@ -746,7 +749,7 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
loginTenantAdmin(); loginTenantAdmin();
NotificationTarget target = createNotificationTarget(new AllUsersFilter()); 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)) ((MobileAppDeliveryMethodNotificationTemplate) template.getConfiguration().getDeliveryMethodsTemplates().get(NotificationDeliveryMethod.MOBILE_APP))
.setAdditionalConfig(JacksonUtil.newObjectNode().set("test", JacksonUtil.newObjectNode().put("test", "test"))); .setAdditionalConfig(JacksonUtil.newObjectNode().set("test", JacksonUtil.newObjectNode().put("test", "test")));
saveNotificationTemplate(template); saveNotificationTemplate(template);
@ -758,11 +761,19 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
.contains("doesn't use the mobile app"); .contains("doesn't use the mobile app");
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), 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"), 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"), 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); verifyNoMoreInteractions(firebaseService);
clearInvocations(firebaseService); clearInvocations(firebaseService);
@ -770,26 +781,24 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
request = submitNotificationRequest(List.of(target.getId()), template.getId(), 0); request = submitNotificationRequest(List.of(target.getId()), template.getId(), 0);
awaitNotificationRequest(request.getId()); awaitNotificationRequest(request.getId());
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), 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"), 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); verifyNoMoreInteractions(firebaseService);
Integer unreadCount = doGet("/api/notifications/unread/count", Integer.class);
assertThat(unreadCount).isEqualTo(2);
} }
@Test @Test
public void testMobileAppNotifications_ruleBased() throws Exception { public void testMobileAppNotifications_ruleBased() throws Exception {
loginSysAdmin();
MobileAppNotificationDeliveryMethodConfig config = new MobileAppNotificationDeliveryMethodConfig();
config.setFirebaseServiceAccountCredentials("testCredentials");
saveNotificationSettings(config);
loginTenantAdmin(); loginTenantAdmin();
mobileToken = "tenantFcmToken"; mobileToken = TEST_MOBILE_TOKEN;
doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk()); doPost("/api/user/mobile/session", new MobileSessionInfo()).andExpect(status().isOk());
createNotificationRule(AlarmCommentNotificationRuleTriggerConfig.builder().onlyUserComments(true).build(), createNotificationRule(AlarmCommentNotificationRuleTriggerConfig.builder().onlyUserComments(true).build(),
DefaultNotifications.alarmComment.getSubject(), DefaultNotifications.alarmComment.getText(), 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"); Device device = createDevice("test", "test");
UUID alarmDashboardId = UUID.randomUUID(); UUID alarmDashboardId = UUID.randomUUID();
@ -802,18 +811,21 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
.put("dashboardId", alarmDashboardId.toString())) .put("dashboardId", alarmDashboardId.toString()))
.build(); .build();
alarm = doPost("/api/alarm", alarm, Alarm.class); alarm = doPost("/api/alarm", alarm, Alarm.class);
AlarmId alarmId = alarm.getId();
AlarmComment comment = new AlarmComment(); AlarmComment comment = new AlarmComment();
comment.setComment(JacksonUtil.newObjectNode() comment.setComment(JacksonUtil.newObjectNode()
.put("text", "text")); .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); ArgumentCaptor<Map<String, String>> msgCaptor = ArgumentCaptor.forClass(Map.class);
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"),
eq("tenantFcmToken"), eq("Comment on 'test' alarm"), eq(TEST_MOBILE_TOKEN), eq(expectedSubject),
eq(TENANT_ADMIN_EMAIL + " added comment: text"), eq(expectedBody),
msgCaptor.capture()); msgCaptor.capture(), eq(1));
}); });
Map<String, String> firebaseMessageData = msgCaptor.getValue(); Map<String, String> firebaseMessageData = msgCaptor.getValue();
assertThat(firebaseMessageData.keySet()).doesNotContainNull().doesNotContain(""); 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.enabled")).isEqualTo("true");
assertThat(firebaseMessageData.get("onClick.linkType")).isEqualTo("DASHBOARD"); assertThat(firebaseMessageData.get("onClick.linkType")).isEqualTo("DASHBOARD");
assertThat(firebaseMessageData.get("onClick.dashboardId")).isEqualTo(alarmDashboardId.toString()); 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 @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.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
@ -30,13 +31,13 @@ public interface NotificationService {
boolean markNotificationAsRead(TenantId tenantId, UserId recipientId, NotificationId notificationId); 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); 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 NotificationRequestId requestId;
private UserId recipientId; private UserId recipientId;
private NotificationType type; private NotificationType type;
private NotificationDeliveryMethod deliveryMethod;
private String subject; private String subject;
private String text; private String text;
private JsonNode additionalConfig; private JsonNode additionalConfig;
private NotificationInfo info; private NotificationInfo info;
private NotificationStatus status; 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_0 = 3;
V_3_6_1 = 4; V_3_6_1 = 4;
V_3_6_2 = 5; 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; 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) { public static void replaceUuidsRecursively(JsonNode node, Set<String> skippedRootFields, Pattern includedFieldsPattern, UnaryOperator<UUID> replacer, boolean root) {
if (node == null) { if (node == null) {
return; 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_REQUEST_ID_PROPERTY = "request_id";
public static final String NOTIFICATION_RECIPIENT_ID_PROPERTY = "recipient_id"; public static final String NOTIFICATION_RECIPIENT_ID_PROPERTY = "recipient_id";
public static final String NOTIFICATION_TYPE_PROPERTY = "type"; 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_SUBJECT_PROPERTY = "subject";
public static final String NOTIFICATION_TEXT_PROPERTY = "body"; public static final String NOTIFICATION_TEXT_PROPERTY = "body";
public static final String NOTIFICATION_ADDITIONAL_CONFIG_PROPERTY = "additional_config"; 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.NotificationRequestId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.NotificationInfo; 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) @Column(name = ModelConstants.NOTIFICATION_TYPE_PROPERTY, nullable = false)
private NotificationType type; private NotificationType type;
@Enumerated(EnumType.STRING)
@Column(name = ModelConstants.NOTIFICATION_DELIVERY_METHOD_PROPERTY, nullable = false)
private NotificationDeliveryMethod deliveryMethod;
@Column(name = ModelConstants.NOTIFICATION_SUBJECT_PROPERTY) @Column(name = ModelConstants.NOTIFICATION_SUBJECT_PROPERTY)
private String subject; private String subject;
@ -80,6 +85,7 @@ public class NotificationEntity extends BaseSqlEntity<Notification> {
setRequestId(getUuid(notification.getRequestId())); setRequestId(getUuid(notification.getRequestId()));
setRecipientId(getUuid(notification.getRecipientId())); setRecipientId(getUuid(notification.getRecipientId()));
setType(notification.getType()); setType(notification.getType());
setDeliveryMethod(notification.getDeliveryMethod());
setSubject(notification.getSubject()); setSubject(notification.getSubject());
setText(notification.getText()); setText(notification.getText());
setAdditionalConfig(notification.getAdditionalConfig()); setAdditionalConfig(notification.getAdditionalConfig());
@ -95,6 +101,7 @@ public class NotificationEntity extends BaseSqlEntity<Notification> {
notification.setRequestId(getEntityId(requestId, NotificationRequestId::new)); notification.setRequestId(getEntityId(requestId, NotificationRequestId::new));
notification.setRecipientId(getEntityId(recipientId, UserId::new)); notification.setRecipientId(getEntityId(recipientId, UserId::new));
notification.setType(type); notification.setType(type);
notification.setDeliveryMethod(deliveryMethod);
notification.setSubject(subject); notification.setSubject(subject);
notification.setText(text); notification.setText(text);
notification.setAdditionalConfig(additionalConfig); 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.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
@ -57,29 +58,29 @@ public class DefaultNotificationService implements NotificationService, EntityDa
} }
@Override @Override
public int markAllNotificationsAsRead(TenantId tenantId, UserId recipientId) { public int markAllNotificationsAsRead(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId) {
return notificationDao.updateStatusByRecipientId(tenantId, recipientId, NotificationStatus.READ); return notificationDao.updateStatusByDeliveryMethodAndRecipientId(tenantId, deliveryMethod, recipientId, NotificationStatus.READ);
} }
@Override @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) { if (unreadOnly) {
return notificationDao.findUnreadByRecipientIdAndPageLink(tenantId, recipientId, pageLink); return notificationDao.findUnreadByDeliveryMethodAndRecipientIdAndPageLink(tenantId, deliveryMethod, recipientId, pageLink);
} else { } else {
return notificationDao.findByRecipientIdAndPageLink(tenantId, recipientId, pageLink); return notificationDao.findByDeliveryMethodAndRecipientIdAndPageLink(tenantId, deliveryMethod, recipientId, pageLink);
} }
} }
@Override @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); SortOrder sortOrder = new SortOrder(EntityKeyMapping.CREATED_TIME, SortOrder.Direction.DESC);
PageLink pageLink = new PageLink(limit, 0, null, sortOrder); PageLink pageLink = new PageLink(limit, 0, null, sortOrder);
return findNotificationsByRecipientIdAndReadStatus(tenantId, recipientId, true, pageLink); return findNotificationsByRecipientIdAndReadStatus(tenantId, deliveryMethod, recipientId, true, pageLink);
} }
@Override @Override
public int countUnreadNotificationsByRecipientId(TenantId tenantId, UserId recipientId) { public int countUnreadNotificationsByRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId) {
return notificationDao.countUnreadByRecipientId(tenantId, recipientId); return notificationDao.countUnreadByDeliveryMethodAndRecipientId(tenantId, deliveryMethod, recipientId);
} }
@Override @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.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
@ -27,19 +28,17 @@ import org.thingsboard.server.dao.Dao;
public interface NotificationDao extends Dao<Notification> { 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); boolean updateStatusByIdAndRecipientId(TenantId tenantId, UserId recipientId, NotificationId notificationId, NotificationStatus status);
int countUnreadByRecipientId(TenantId tenantId, UserId recipientId); int countUnreadByDeliveryMethodAndRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId);
PageData<Notification> findByRequestId(TenantId tenantId, NotificationRequestId notificationRequestId, PageLink pageLink);
boolean deleteByIdAndRecipientId(TenantId tenantId, UserId recipientId, NotificationId notificationId); 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); 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.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationStatus; import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
@ -51,14 +52,14 @@ public class JpaNotificationDao extends JpaPartitionedAbstractDao<NotificationEn
private int partitionSizeInHours; private int partitionSizeInHours;
@Override @Override
public PageData<Notification> findUnreadByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink) { public PageData<Notification> findUnreadByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink) {
return DaoUtil.toPageData(notificationRepository.findByRecipientIdAndStatusNot(recipientId.getId(), NotificationStatus.READ, return DaoUtil.toPageData(notificationRepository.findByDeliveryMethodAndRecipientIdAndStatusNot(deliveryMethod,
pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); recipientId.getId(), NotificationStatus.READ, pageLink.getTextSearch(), DaoUtil.toPageable(pageLink)));
} }
@Override @Override
public PageData<Notification> findByRecipientIdAndPageLink(TenantId tenantId, UserId recipientId, PageLink pageLink) { public PageData<Notification> findByDeliveryMethodAndRecipientIdAndPageLink(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, PageLink pageLink) {
return DaoUtil.toPageData(notificationRepository.findByRecipientId(recipientId.getId(), return DaoUtil.toPageData(notificationRepository.findByDeliveryMethodAndRecipientId(deliveryMethod, recipientId.getId(),
pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); 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 * For this hot method, the partial index `idx_notification_recipient_id_unread` was introduced since 3.6.0
* */ * */
@Override @Override
public int countUnreadByRecipientId(TenantId tenantId, UserId recipientId) { public int countUnreadByDeliveryMethodAndRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId) {
return notificationRepository.countByRecipientIdAndStatusNot(recipientId.getId(), NotificationStatus.READ); return notificationRepository.countByDeliveryMethodAndRecipientIdAndStatusNot(deliveryMethod, 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)));
} }
@Override @Override
@ -86,8 +82,8 @@ public class JpaNotificationDao extends JpaPartitionedAbstractDao<NotificationEn
} }
@Override @Override
public int updateStatusByRecipientId(TenantId tenantId, UserId recipientId, NotificationStatus status) { public int updateStatusByDeliveryMethodAndRecipientId(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId, NotificationStatus status) {
return notificationRepository.updateStatusByRecipientId(recipientId.getId(), status); return notificationRepository.updateStatusByDeliveryMethodAndRecipientIdAndStatusNot(deliveryMethod, recipientId.getId(), status);
} }
@Override @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.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional; 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.common.data.notification.NotificationStatus;
import org.thingsboard.server.dao.model.sql.NotificationEntity; import org.thingsboard.server.dao.model.sql.NotificationEntity;
@ -31,20 +32,23 @@ import java.util.UUID;
@Repository @Repository
public interface NotificationRepository extends JpaRepository<NotificationEntity, UUID> { 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 " + "AND (:searchText is NULL OR ilike(n.subject, concat('%', :searchText, '%')) = true " +
"OR ilike(n.text, concat('%', :searchText, '%')) = true)") "OR ilike(n.text, concat('%', :searchText, '%')) = true)")
Page<NotificationEntity> findByRecipientIdAndStatusNot(@Param("recipientId") UUID recipientId, Page<NotificationEntity> findByDeliveryMethodAndRecipientIdAndStatusNot(@Param("deliveryMethod") NotificationDeliveryMethod deliveryMethod,
@Param("status") NotificationStatus status, @Param("recipientId") UUID recipientId,
@Param("searchText") String searchText, @Param("status") NotificationStatus status,
Pageable pageable); @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 " + "AND (:searchText is NULL OR ilike(n.subject, concat('%', :searchText, '%')) = true " +
"OR ilike(n.text, concat('%', :searchText, '%')) = true)") "OR ilike(n.text, concat('%', :searchText, '%')) = true)")
Page<NotificationEntity> findByRecipientId(@Param("recipientId") UUID recipientId, Page<NotificationEntity> findByDeliveryMethodAndRecipientId(@Param("deliveryMethod") NotificationDeliveryMethod deliveryMethod,
@Param("searchText") String searchText, @Param("recipientId") UUID recipientId,
Pageable pageable); @Param("searchText") String searchText,
Pageable pageable);
@Modifying @Modifying
@Transactional @Transactional
@ -54,9 +58,7 @@ public interface NotificationRepository extends JpaRepository<NotificationEntity
@Param("recipientId") UUID recipientId, @Param("recipientId") UUID recipientId,
@Param("status") NotificationStatus status); @Param("status") NotificationStatus status);
int countByRecipientIdAndStatusNot(UUID recipientId, NotificationStatus status); int countByDeliveryMethodAndRecipientIdAndStatusNot(NotificationDeliveryMethod deliveryMethod, UUID recipientId, NotificationStatus status);
Page<NotificationEntity> findByRequestId(UUID requestId, Pageable pageable);
@Transactional @Transactional
@Modifying @Modifying
@ -76,8 +78,9 @@ public interface NotificationRepository extends JpaRepository<NotificationEntity
@Modifying @Modifying
@Transactional @Transactional
@Query("UPDATE NotificationEntity n SET n.status = :status " + @Query("UPDATE NotificationEntity n SET n.status = :status " +
"WHERE n.recipientId = :recipientId AND n.status <> :status") "WHERE n.deliveryMethod = :deliveryMethod AND n.recipientId = :recipientId AND n.status <> :status")
int updateStatusByRecipientId(@Param("recipientId") UUID recipientId, int updateStatusByDeliveryMethodAndRecipientIdAndStatusNot(@Param("deliveryMethod") NotificationDeliveryMethod deliveryMethod,
@Param("status") NotificationStatus status); @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_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); 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 BEGIN
IF (SELECT COUNT(*) FROM tb_schema_settings) = 0 THEN 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 IF;
END; END;
$$; $$;
@ -864,6 +864,7 @@ CREATE TABLE IF NOT EXISTS notification (
request_id UUID, request_id UUID,
recipient_id UUID NOT NULL, recipient_id UUID NOT NULL,
type VARCHAR(50) NOT NULL, type VARCHAR(50) NOT NULL,
delivery_method VARCHAR(50) NOT NULL,
subject VARCHAR(255), subject VARCHAR(255),
body VARCHAR(1000) NOT NULL, body VARCHAR(1000) NOT NULL,
additional_config VARCHAR(1000), additional_config VARCHAR(1000),

2
msa/tb/pom.xml

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