diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index ac07e0d5ba..8b14d2d8b7 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -280,9 +280,6 @@ public class ThingsboardInstallService { log.info("Upgrading ThingsBoard from version 3.6.2 to 3.6.3 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.6.2"); //TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache - case "3.6.2": - log.info("Upgrading ThingsBoard from version 3.6.2 to 3.6.3 ..."); - databaseEntitiesUpgradeService.upgradeDatabase("3.6.2"); break; default: throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion); diff --git a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java index b8aca35403..8b16f40ee9 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java @@ -245,11 +245,11 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple if (targetConfig.getUsersFilter().getType().isForRules() && ctx.getRequest().getInfo() instanceof RuleOriginatedNotificationInfo) { recipients = new PageDataIterable<>(pageLink -> { return notificationTargetService.findRecipientsForRuleNotificationTargetConfig(ctx.getTenantId(), targetConfig, (RuleOriginatedNotificationInfo) ctx.getRequest().getInfo(), pageLink); - }, 500); + }, 256); } else { recipients = new PageDataIterable<>(pageLink -> { return notificationTargetService.findRecipientsForNotificationTargetConfig(ctx.getTenantId(), targetConfig, pageLink); - }, 500); + }, 256); } break; } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java b/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java index e6fdc55902..898547ff75 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/provider/DefaultFirebaseService.java @@ -26,6 +26,7 @@ import com.google.firebase.messaging.FirebaseMessagingException; import com.google.firebase.messaging.Message; import com.google.firebase.messaging.Notification; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.notification.FirebaseService; @@ -36,6 +37,7 @@ import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; @Service +@Slf4j public class DefaultFirebaseService implements FirebaseService { private final Cache contexts = Caffeine.newBuilder() @@ -66,6 +68,7 @@ public class DefaultFirebaseService implements FirebaseService { .setToken(fcmToken) .build(); firebaseContext.getMessaging().send(message); + log.trace("[{}] Sent message for FCM token {}", tenantId, fcmToken); } public static class FirebaseContext { @@ -100,13 +103,16 @@ public class DefaultFirebaseService implements FirebaseService { } catch (IllegalStateException alreadyExists) { // should never normally happen messaging = FirebaseMessaging.getInstance(app); } + log.debug("[{}] Initialized new FirebaseContext", key); } public void check(String credentials) { if (!this.credentials.equals(credentials)) { - app.delete(); + destroy(); this.credentials = credentials; init(); + } else if (app == null || messaging == null) { + throw new IllegalStateException("Firebase app couldn't be initialized"); } } @@ -114,6 +120,7 @@ public class DefaultFirebaseService implements FirebaseService { app.delete(); app = null; messaging = null; + log.debug("[{}] Destroyed FirebaseContext", key); } } diff --git a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java index 6c28d4f880..a0e908a600 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java @@ -41,6 +41,7 @@ import org.thingsboard.server.common.data.notification.rule.DefaultNotificationR import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.settings.NotificationDeliveryMethodConfig; import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; @@ -48,6 +49,8 @@ import org.thingsboard.server.common.data.notification.targets.platform.UserList import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.notification.template.HasSubject; +import org.thingsboard.server.common.data.notification.template.MobileAppDeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; import org.thingsboard.server.common.data.notification.template.SmsDeliveryMethodNotificationTemplate; @@ -168,26 +171,28 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest DeliveryMethodNotificationTemplate deliveryMethodNotificationTemplate; switch (deliveryMethod) { case WEB: { - WebDeliveryMethodNotificationTemplate template = new WebDeliveryMethodNotificationTemplate(); - template.setSubject(subject); - deliveryMethodNotificationTemplate = template; + deliveryMethodNotificationTemplate = new WebDeliveryMethodNotificationTemplate(); break; } case EMAIL: { - EmailDeliveryMethodNotificationTemplate template = new EmailDeliveryMethodNotificationTemplate(); - template.setSubject(subject); - deliveryMethodNotificationTemplate = template; + deliveryMethodNotificationTemplate = new EmailDeliveryMethodNotificationTemplate(); break; } case SMS: { deliveryMethodNotificationTemplate = new SmsDeliveryMethodNotificationTemplate(); break; } + case MOBILE_APP: + deliveryMethodNotificationTemplate = new MobileAppDeliveryMethodNotificationTemplate(); + break; default: throw new IllegalArgumentException("Unsupported delivery method " + deliveryMethod); } deliveryMethodNotificationTemplate.setEnabled(true); deliveryMethodNotificationTemplate.setBody(text); + if (deliveryMethodNotificationTemplate instanceof HasSubject) { + ((HasSubject) deliveryMethodNotificationTemplate).setSubject(subject); + } config.getDeliveryMethodsTemplates().put(deliveryMethod, deliveryMethodNotificationTemplate); } notificationTemplate.setConfiguration(config); @@ -202,6 +207,15 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest doPost("/api/notification/settings", notificationSettings).andExpect(status().isOk()); } + protected void saveNotificationSettings(NotificationDeliveryMethodConfig... configs) throws Exception { + NotificationSettings settings = new NotificationSettings(); + settings.setDeliveryMethodsConfigs(Arrays.stream(configs) + .collect(Collectors.toMap( + NotificationDeliveryMethodConfig::getMethod, config -> config + ))); + saveNotificationSettings(settings); + } + protected Pair createUserAndConnectWsClient(Authority authority) throws Exception { User user = new User(); user.setTenantId(tenantId); diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java index 379f7648ad..5941ff8bc8 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java @@ -24,10 +24,14 @@ import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.ResultActions; import org.springframework.web.client.RestTemplate; import org.thingsboard.rule.engine.api.NotificationCenter; +import org.thingsboard.rule.engine.api.notification.FirebaseService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.UserMobileInfo; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.NotificationRequestId; @@ -44,11 +48,13 @@ 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.EntityActionNotificationInfo; +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.SlackNotificationDeliveryMethodConfig; import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings; import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; +import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter; @@ -91,6 +97,8 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest @Slf4j @@ -104,6 +112,8 @@ public class NotificationApiTest extends AbstractNotificationApiTest { private DbCallbackExecutorService executor; @Autowired private MicrosoftTeamsNotificationChannel microsoftTeamsNotificationChannel; + @MockBean + private FirebaseService firebaseService; @Before public void beforeEach() throws Exception { @@ -708,6 +718,56 @@ public class NotificationApiTest extends AbstractNotificationApiTest { assertThat(message.getPotentialAction().get(0).getTargets().get(0).getUri()).isEqualTo("https://" + expectedParams); } + @Test + public void testMobileAppNotifications() throws Exception { + loginSysAdmin(); + MobileAppNotificationDeliveryMethodConfig config = new MobileAppNotificationDeliveryMethodConfig(); + config.setFirebaseServiceAccountCredentials("testCredentials"); + saveNotificationSettings(config); + + loginCustomerUser(); + UserMobileInfo customerMobileInfo = new UserMobileInfo(); + customerMobileInfo.setFcmToken("customerFcmToken"); + doPost("/api/user/mobile/info", customerMobileInfo).andExpect(status().isOk()); + + loginTenantAdmin(); + UserMobileInfo tenantMobileInfo = new UserMobileInfo(); + tenantMobileInfo.setFcmToken("tenantFcmToken"); + doPost("/api/user/mobile/info", tenantMobileInfo).andExpect(status().isOk()); + + loginDifferentCustomer(); // with no mobile info + + loginTenantAdmin(); + NotificationTarget target = createNotificationTarget(new AllUsersFilter()); + NotificationTemplate template = createNotificationTemplate(NotificationType.GENERAL, "Title", "Message", NotificationDeliveryMethod.MOBILE_APP); + + NotificationRequest request = submitNotificationRequest(List.of(target.getId()), template.getId(), 0); + NotificationRequestStats stats = awaitNotificationRequest(request.getId()); + assertThat(stats.getSent().get(NotificationDeliveryMethod.MOBILE_APP)).hasValue(2); + assertThat(stats.getErrors().get(NotificationDeliveryMethod.MOBILE_APP).get(differentCustomerUser.getEmail())) + .contains("doesn't use the mobile app"); + + verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), + eq("tenantFcmToken"), eq("Title"), eq("Message")); + verify(firebaseService).sendMessage(eq(tenantId), eq("testCredentials"), + eq("customerFcmToken"), eq("Title"), eq("Message")); + verifyNoMoreInteractions(firebaseService); + } + + @Test + public void testMobileSettings_tenantLevel() throws Exception { + MobileAppNotificationDeliveryMethodConfig config = new MobileAppNotificationDeliveryMethodConfig(); + config.setFirebaseServiceAccountCredentials("testCredentials"); + NotificationSettings settings = new NotificationSettings(); + settings.setDeliveryMethodsConfigs(Map.of( + NotificationDeliveryMethod.MOBILE_APP, config + )); + + ResultActions result = doPost("/api/notification/settings", settings) + .andExpect(status().isBadRequest()); + assertThat(getErrorMessage(result)).contains("can only be configured by system administrator"); + } + private NotificationRequestStats submitNotificationRequestAndWait(NotificationRequest notificationRequest) throws Exception { SettableFuture future = SettableFuture.create(); notificationCenter.processNotificationRequest(notificationRequest.getTenantId(), notificationRequest, new FutureCallback<>() {