Skip to content

Commit

Permalink
Merge pull request #10420 from thingsboard/fix/mobile-notifications
Browse files Browse the repository at this point in the history
Improvements for mobile notifications
  • Loading branch information
ashvayka authored Apr 9, 2024
2 parents f0cc6f3 + 641008c commit e7f68e3
Show file tree
Hide file tree
Showing 21 changed files with 250 additions and 122 deletions.
27 changes: 27 additions & 0 deletions application/src/main/data/upgrade/3.6.3/schema_update.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
--
-- 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.
--

-- NOTIFICATIONS UPDATE START

ALTER TABLE notification ADD COLUMN IF NOT EXISTS delivery_method VARCHAR(50) NOT NULL default 'WEB';

DROP INDEX IF EXISTS idx_notification_recipient_id_created_time;
DROP INDEX IF EXISTS idx_notification_recipient_id_unread;

CREATE INDEX IF NOT EXISTS idx_notification_delivery_method_recipient_id_created_time ON notification(delivery_method, recipient_id, created_time DESC);
CREATE INDEX IF NOT EXISTS idx_notification_delivery_method_recipient_id_unread ON notification(delivery_method, recipient_id) WHERE status <> 'READ';

-- NOTIFICATIONS UPDATE END
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ public class NotificationController extends BaseController {
private final NotificationCenter notificationCenter;
private final NotificationSettingsService notificationSettingsService;

private static final String DELIVERY_METHOD_ALLOWABLE_VALUES = "WEB,MOBILE_APP";

@ApiOperation(value = "Get notifications (getNotifications)",
notes = "Returns the page of notifications for current user." + NEW_LINE +
PAGE_DATA_PARAMETERS +
Expand Down Expand Up @@ -172,10 +174,23 @@ public PageData<Notification> getNotifications(@ApiParam(value = PAGE_SIZE_DESCR
@RequestParam(required = false) String sortOrder,
@ApiParam(value = "To search for unread notifications only")
@RequestParam(defaultValue = "false") boolean unreadOnly,
@ApiParam(value = "Delivery method", allowableValues = DELIVERY_METHOD_ALLOWABLE_VALUES)
@RequestParam(defaultValue = "WEB") NotificationDeliveryMethod deliveryMethod,
@AuthenticationPrincipal SecurityUser user) throws ThingsboardException {
// no permissions
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return notificationService.findNotificationsByRecipientIdAndReadStatus(user.getTenantId(), user.getId(), unreadOnly, pageLink);
return notificationService.findNotificationsByRecipientIdAndReadStatus(user.getTenantId(), deliveryMethod, user.getId(), unreadOnly, pageLink);
}

@ApiOperation(value = "Get unread notifications count (getUnreadNotificationsCount)",
notes = "Returns unread notifications count for chosen delivery method." +
AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@GetMapping("/notifications/unread/count")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public Integer getUnreadNotificationsCount(@ApiParam(value = "Delivery method", allowableValues = DELIVERY_METHOD_ALLOWABLE_VALUES)
@RequestParam(defaultValue = "MOBILE_APP") NotificationDeliveryMethod deliveryMethod,
@AuthenticationPrincipal SecurityUser user) {
return notificationService.countUnreadNotificationsByRecipientId(user.getTenantId(), deliveryMethod, user.getId());
}

@ApiOperation(value = "Mark notification as read (markNotificationAsRead)",
Expand All @@ -195,9 +210,11 @@ public void markNotificationAsRead(@PathVariable UUID id,
AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PutMapping("/notifications/read")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void markAllNotificationsAsRead(@AuthenticationPrincipal SecurityUser user) {
public void markAllNotificationsAsRead(@ApiParam(value = "Delivery method", allowableValues = DELIVERY_METHOD_ALLOWABLE_VALUES)
@RequestParam(defaultValue = "WEB") NotificationDeliveryMethod deliveryMethod,
@AuthenticationPrincipal SecurityUser user) {
// no permissions
notificationCenter.markAllNotificationsAsRead(user.getTenantId(), user.getId());
notificationCenter.markAllNotificationsAsRead(user.getTenantId(), deliveryMethod, user.getId());
}

@ApiOperation(value = "Delete notification (deleteNotification)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
import java.util.UUID;
import java.util.stream.Collectors;

import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.WEB;

@Service
@Slf4j
@RequiredArgsConstructor
Expand Down Expand Up @@ -192,7 +194,7 @@ public void sendGeneralWebNotification(TenantId tenantId, UsersFilter recipients
NotificationProcessingContext ctx = NotificationProcessingContext.builder()
.tenantId(tenantId)
.request(notificationRequest)
.deliveryMethods(Set.of(NotificationDeliveryMethod.WEB))
.deliveryMethods(Set.of(WEB))
.template(template)
.build();

Expand Down Expand Up @@ -323,6 +325,7 @@ public void sendNotification(User recipient, WebDeliveryMethodNotificationTempla
.requestId(request.getId())
.recipientId(recipient.getId())
.type(ctx.getNotificationType())
.deliveryMethod(WEB)
.subject(processedTemplate.getSubject())
.text(processedTemplate.getBody())
.additionalConfig(processedTemplate.getAdditionalConfig())
Expand All @@ -348,19 +351,22 @@ public void markNotificationAsRead(TenantId tenantId, UserId recipientId, Notifi
boolean updated = notificationService.markNotificationAsRead(tenantId, recipientId, notificationId);
if (updated) {
log.trace("Marked notification {} as read (recipient id: {}, tenant id: {})", notificationId, recipientId, tenantId);
NotificationUpdate update = NotificationUpdate.builder()
.updated(true)
.notificationId(notificationId.getId())
.newStatus(NotificationStatus.READ)
.build();
onNotificationUpdate(tenantId, recipientId, update);
Notification notification = notificationService.findNotificationById(tenantId, notificationId);
if (notification.getDeliveryMethod() == WEB) {
NotificationUpdate update = NotificationUpdate.builder()
.updated(true)
.notificationId(notificationId.getId())
.newStatus(NotificationStatus.READ)
.build();
onNotificationUpdate(tenantId, recipientId, update);
}
}
}

@Override
public void markAllNotificationsAsRead(TenantId tenantId, UserId recipientId) {
int updatedCount = notificationService.markAllNotificationsAsRead(tenantId, recipientId);
if (updatedCount > 0) {
public void markAllNotificationsAsRead(TenantId tenantId, NotificationDeliveryMethod deliveryMethod, UserId recipientId) {
int updatedCount = notificationService.markAllNotificationsAsRead(tenantId, deliveryMethod, recipientId);
if (updatedCount > 0 && deliveryMethod == WEB) {
log.trace("Marked all notifications as read (recipient id: {}, tenant id: {})", recipientId, tenantId);
NotificationUpdate update = NotificationUpdate.builder()
.updated(true)
Expand All @@ -375,7 +381,7 @@ public void markAllNotificationsAsRead(TenantId tenantId, UserId recipientId) {
public void deleteNotification(TenantId tenantId, UserId recipientId, NotificationId notificationId) {
Notification notification = notificationService.findNotificationById(tenantId, notificationId);
boolean deleted = notificationService.deleteNotification(tenantId, recipientId, notificationId);
if (deleted) {
if (deleted && notification.getDeliveryMethod() == WEB) {
NotificationUpdate update = NotificationUpdate.builder()
.deleted(true)
.notification(notification)
Expand Down Expand Up @@ -455,7 +461,7 @@ private void onNotificationRequestUpdate(TenantId tenantId, NotificationRequestU

@Override
public NotificationDeliveryMethod getDeliveryMethod() {
return NotificationDeliveryMethod.WEB;
return WEB;
}

@Override
Expand All @@ -466,7 +472,7 @@ protected String getExecutorPrefix() {
@Autowired
public void setChannels(List<NotificationChannel> channels, NotificationCenter webNotificationChannel) {
this.channels = channels.stream().collect(Collectors.toMap(NotificationChannel::getDeliveryMethod, c -> c));
this.channels.put(NotificationDeliveryMethod.WEB, (NotificationChannel) webNotificationChannel);
this.channels.put(WEB, (NotificationChannel) webNotificationChannel);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package org.thingsboard.server.service.notification.channels;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.MessagingErrorCode;
Expand All @@ -26,11 +27,15 @@
import org.thingsboard.rule.engine.api.notification.FirebaseService;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationStatus;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.settings.MobileAppNotificationDeliveryMethodConfig;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.template.MobileAppDeliveryMethodNotificationTemplate;
import org.thingsboard.server.dao.notification.NotificationService;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.notification.NotificationProcessingContext;
Expand All @@ -41,32 +46,63 @@
import java.util.Optional;
import java.util.Set;

import static org.thingsboard.server.common.data.notification.NotificationDeliveryMethod.MOBILE_APP;

@Component
@RequiredArgsConstructor
@Slf4j
public class MobileAppNotificationChannel implements NotificationChannel<User, MobileAppDeliveryMethodNotificationTemplate> {

private final FirebaseService firebaseService;
private final UserService userService;
private final NotificationService notificationService;
private final NotificationSettingsService notificationSettingsService;

@Override
public void sendNotification(User recipient, MobileAppDeliveryMethodNotificationTemplate processedTemplate, NotificationProcessingContext ctx) throws Exception {
NotificationRequest request = ctx.getRequest();
NotificationInfo info = request.getInfo();
if (info != null && info.getDashboardId() != null) {
ObjectNode additionalConfig = JacksonUtil.asObject(processedTemplate.getAdditionalConfig());
ObjectNode onClick = JacksonUtil.asObject(additionalConfig.get("onClick"));
if (onClick.get("enabled") == null || !Boolean.parseBoolean(onClick.get("enabled").asText())) {
onClick.put("enabled", true);
onClick.put("linkType", "DASHBOARD");
onClick.put("setEntityIdInState", true);
onClick.put("dashboardId", info.getDashboardId().toString());
additionalConfig.set("onClick", onClick);
}
processedTemplate.setAdditionalConfig(additionalConfig);
}
Notification notification = Notification.builder()
.requestId(request.getId())
.recipientId(recipient.getId())
.type(ctx.getNotificationType())
.deliveryMethod(MOBILE_APP)
.subject(processedTemplate.getSubject())
.text(processedTemplate.getBody())
.additionalConfig(processedTemplate.getAdditionalConfig())
.info(info)
.status(NotificationStatus.SENT)
.build();
notificationService.saveNotification(recipient.getTenantId(), notification);

var mobileSessions = userService.findMobileSessions(recipient.getTenantId(), recipient.getId());
if (mobileSessions.isEmpty()) {
throw new IllegalArgumentException("User doesn't use the mobile app");
}

MobileAppNotificationDeliveryMethodConfig config = ctx.getDeliveryMethodConfig(NotificationDeliveryMethod.MOBILE_APP);
MobileAppNotificationDeliveryMethodConfig config = ctx.getDeliveryMethodConfig(MOBILE_APP);
String credentials = config.getFirebaseServiceAccountCredentials();
Set<String> validTokens = new HashSet<>(mobileSessions.keySet());

String subject = processedTemplate.getSubject();
String body = processedTemplate.getBody();
Map<String, String> data = getNotificationData(processedTemplate, ctx);
int unreadCount = notificationService.countUnreadNotificationsByRecipientId(ctx.getTenantId(), MOBILE_APP, recipient.getId());
for (String token : mobileSessions.keySet()) {
try {
firebaseService.sendMessage(ctx.getTenantId(), credentials, token, subject, body, data);
firebaseService.sendMessage(ctx.getTenantId(), credentials, token, subject, body, data, unreadCount);
} catch (FirebaseMessagingException e) {
MessagingErrorCode errorCode = e.getMessagingErrorCode();
if (errorCode == MessagingErrorCode.UNREGISTERED || errorCode == MessagingErrorCode.INVALID_ARGUMENT) {
Expand All @@ -92,12 +128,6 @@ private Map<String, String> getNotificationData(MobileAppDeliveryMethodNotificat
Optional.ofNullable(info.getStateEntityId()).ifPresent(stateEntityId -> {
data.put("stateEntityId", stateEntityId.getId().toString());
data.put("stateEntityType", stateEntityId.getEntityType().name());
if (!"true".equals(data.get("onClick.enabled")) && info.getDashboardId() != null) {
data.put("onClick.enabled", "true");
data.put("onClick.linkType", "DASHBOARD");
data.put("onClick.setEntityIdInState", "true");
data.put("onClick.dashboardId", info.getDashboardId().toString());
}
});
data.put("notificationType", ctx.getNotificationType().name());
switch (ctx.getNotificationType()) {
Expand All @@ -116,14 +146,14 @@ private Map<String, String> getNotificationData(MobileAppDeliveryMethodNotificat
@Override
public void check(TenantId tenantId) throws Exception {
NotificationSettings systemSettings = notificationSettingsService.findNotificationSettings(TenantId.SYS_TENANT_ID);
if (!systemSettings.getDeliveryMethodsConfigs().containsKey(NotificationDeliveryMethod.MOBILE_APP)) {
if (!systemSettings.getDeliveryMethodsConfigs().containsKey(MOBILE_APP)) {
throw new RuntimeException("Push-notifications to mobile are not configured");
}
}

@Override
public NotificationDeliveryMethod getDeliveryMethod() {
return NotificationDeliveryMethod.MOBILE_APP;
return MOBILE_APP;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ public class DefaultFirebaseService implements FirebaseService {
.build();

@Override
public void sendMessage(TenantId tenantId, String credentials, String fcmToken, String title, String body, Map<String, String> data) throws FirebaseMessagingException {
public void sendMessage(TenantId tenantId, String credentials, String fcmToken, String title, String body,
Map<String, String> data, Integer badge) throws FirebaseMessagingException {
FirebaseContext firebaseContext = contexts.asMap().compute(tenantId.toString(), (key, context) -> {
if (context == null) {
return new FirebaseContext(key, credentials);
Expand All @@ -64,6 +65,12 @@ public void sendMessage(TenantId tenantId, String credentials, String fcmToken,
}
});

Aps.Builder apsConfig = Aps.builder()
.setSound("default");
if (badge != null) {
apsConfig.setBadge(badge);
}

Message message = Message.builder()
.setToken(fcmToken)
.setNotification(Notification.builder()
Expand All @@ -74,14 +81,17 @@ public void sendMessage(TenantId tenantId, String credentials, String fcmToken,
.setPriority(AndroidConfig.Priority.HIGH)
.build())
.setApnsConfig(ApnsConfig.builder()
.setAps(Aps.builder()
.setContentAvailable(true)
.build())
.setAps(apsConfig.build())
.build())
.putAllData(data)
.build();
firebaseContext.getMessaging().send(message);
log.trace("[{}] Sent message for FCM token {}", tenantId, fcmToken);
try {
firebaseContext.getMessaging().send(message);
log.trace("[{}] Sent message for FCM token {}", tenantId, fcmToken);
} catch (Throwable t) {
log.debug("[{}] Failed to send message for FCM token {}", tenantId, fcmToken, t);
throw t;
}
}

public static class FirebaseContext {
Expand Down
Loading

0 comments on commit e7f68e3

Please sign in to comment.